123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908(** a generic menu layout with submenus *)(* can be used with entries (layouts) at arbitrary locations *)(* TODO implement the resize function *)openB_utilsopenTsdlmoduleAvar=B_avarmoduleButton=B_buttonmoduleChain=B_chainmoduleDraw=B_drawmoduleLayout=B_layoutmodulePopup=B_popupmodulePrint=B_printmoduleStyle=B_stylemoduleSync=B_syncmoduleTimeout=B_timeoutmoduleTrigger=B_triggermoduleWidget=B_widgetletpre=if!debug&&!debug_codelanddebug_custom<>0thenfuns->print_endline("[Menu] "^s)(* for local debugging *)elsenopmoduleEngine=struct(* A menu is a usual bidirectional tree, where each node is either terminal (a
leaf) and corresponds to a menu item with a action, or a submenu. However,
we don't really have to optimize functions for arbitrary trees, because it
will always be a very small tree (not deep).
The top of this tree is of type 'menu' and is the only one with a 'None'
parent_entry. *)typeaction=unit->unitandentry_type=|Menuofmenu|Actionofactionandentry={kind:entry_type;enabled:bool;mutableselected:bool;(* equivalent to highlighted *)layout:Layout.t;(* how to display the entry label *)(* Note: a Separator should be an empty Layout *)parent_menu:menu}andmenu={pos:(int*int)option;(* Relative position wrt the parent_entry *)mutableactive:bool;(* 'active' implies that the menu is shown. But a menu can be shown
without being active. Active implies that submenu will open on
mouse_over, and keyboard is active. *)mutablealways_shown:bool;(* If a menu is shown, it must be either 'active', or 'always_shown'. *)(* some menus (typically a menu bar, for instance) are always shown, but
not necessary always 'active' in the sense above. *)mutableentries:entrylist;mutableroom:Layout.t;(* the layout that contains all menu entries *)mutableparent_entry:entryoption(* the entry to which this menu is attached, or None if this is the top
menu. *)}letseparator=Action(fun()->pre"This action should not be launched.")(* 1. Functions for gearing menus interaction *)(* ------------------------------------------ *)letduration=200(* Duration of animations in ms. *)(* The 'screen' layout is used for grabbing mouse even outside of the menus
themselves. Used for closing menus when clicking outside. *)(* TODO we should maybe delay the creation of the screen to when the user
clicks on the menu, in order to make sure it will be drawn above all other
widgets (possibly created much later than the menu). Or, move it to the top
layer dynamically. *)letscreen_enablescreen=pre"ENABLE";Layout.set_showscreentrueletscreen_disablescreen=pre"DISABLE";Layout.set_showscreenfalseletentry_is_openentry=matchentry.kindwith|Action_->false|Menumenu->menu.active||menu.always_shown(* TODO don't change bg in case of custom layout?*)letset_entry_bg?bgentry=ifentry.enabledthenLayout.set_backgroundentry.layoutbg(* the entry below mouse should always be highlighted. But we also highlight
the parent of each open menu. *)lethighlight_entry?(bg=Layout.opaque_bgDraw.menu_hl_color)entry=set_entry_bg~bgentry;entry.selected<-trueletreset_entry?(bg=Layout.opaque_bgDraw.menu_bg_color)entry=set_entry_bg~bgentry;entry.selected<-false(* Iter menu downwards *)letreciterfmenu=fmenu;List.iter(funentry->matchentry.kindwith|Action_->()|Menusubmenu->iterfsubmenu)menu.entries(* not used *)letadd_submenus_to_dst_old~dstmenu=letfmenu=Layout.add_room~dstmenu.room;ifnotmenu.active&¬menu.always_shownthenLayout.set_showmenu.roomfalseinList.iter(funentry->matchentry.kindwith|Action_->()|Menusubmenu->iterfsubmenu)menu.entriesletset_menu_positionmenu=do_optionmenu.pos(fun(dx,dy)->letkeep_resize=trueinletx,y=matchmenu.parent_entrywith|None->0,0|Someentry->letm=entry.parent_menu.roominletx0,y0=Layout.(getxm,getym)inletdx0,dy0=Layout.(getxentry.layout,getyentry.layout)inx0+dx0,y0+dy0inLayout.setx~keep_resizemenu.room(x+dx);Layout.sety~keep_resizemenu.room(y+dy))letmenu_children_set_hidemenu=List.iter(fune->Layout.rec_set_showfalsee.layout)menu.entries(* Inserts all layouts inside 'dst' at the proper position. Should be done
only once, otherwise the 'repeated widgets' error will appear. *)letadd_menu_to_dst?(skip_first=false)~dstmenu=letskip_first=refskip_firstinletfmenu=printddebug_custom"MENU - Adding %s to %s"(Layout.sprint_idmenu.room)(Layout.sprint_iddst);ifnot!skip_firstthenLayout.add_room~dstmenu.room;skip_first:=false;set_menu_positionmenu;menu.room.Layout.resize<-(fun_->set_menu_positionmenu);ifnotmenu.active&¬menu.always_shownthenbeginLayout.set_showmenu.roomfalse;menu_children_set_hidemenuend;initerfmenuletadd_menu_to_layermenulayer=letfmenu=Layout.global_set_layermenu.roomlayeriniterfmenu(* Return the top menu *)letrectopmenu=pre"TOP";matchmenu.parent_entrywith|None->menu|Someentry->topentry.parent_menuletis_topmenu=menu.parent_entry=None(* Search menu entries for selected entry *)letselected_entrymenu=list_findi(funa->a.selected)menu.entries(* Search the top tree for the first (which should be unique) entry of Action
kind which is 'selected'. Is there a simpler way to loop? *)letselected_action_entrymenu=letrecmenuloopmenu=letcheckentry=ifentry.selectedthenmatchentry.kindwith|Action_->Someentry|Menumenu->menuloopmenuelseNoneinletrecentriesloop=function|[]->None|e::rest->matchcheckewith|Somee'->Somee'|None->entrieslooprestinentriesloopmenu.entriesinmenuloop(topmenu)(* use this for opening menus, not for closing *)letnew_timeout,clear_timeout=lett=refNonein(* there is only one global timeout variable because we assume only one user
can use only one menu at a time... *)(functionaction->do_option!tTimeout.cancel;t:=Some(Timeout.add150action)),(function()->do_option!tTimeout.cancel)letshowscreenmenu=screen_enablescreen;Layout.show~durationmenu.room;List.iter(fune->Layout.rec_set_showtruee.layout)menu.entries;(* Layout.rec_set_show true menu.room; *)Layout.fade_in~durationmenu.roomletactivate?(timeout=false)screenmenu=ifmenu.activethen()elsebeginifnotmenu.always_showntheniftimeoutthennew_timeout(fun()->showscreenmenu)elseshowscreenmenu;menu.active<-trueendletclose?(timeout=false)screenmenu=pre"CLOSE";(* If the parent of this menu is the top menu, this should mean that we have
no other open menus. We can disable the screen. *)do_optionmenu.parent_entry(fune->ifis_tope.parent_menuthenscreen_disablescreen;reset_entrye);ifnotmenu.always_shown&&menu.activethenbeginmenu.active<-false;menu_children_set_hidemenu;clear_timeout();letaction()=Layout.hide~duration~towards:Avar.Topmenu.room;(* il y peut y avoir des bugs qd on ouvre/ferme vite. *)Layout.fade_out~durationmenu.roominiftimeoutthenignore(Timeout.add150action)(* put 1000 for easy debugging *)elseaction()end(* We could make it more efficient and stop going down a branch as soon as a
node is aleady closed. But a Menu tree is never very long, it's probably
not worth. *)letrecclose_children?(timeout=false)screenmenu=pre(Printf.sprintf"CLOSE_CHILDREN with %i ENTRIES"(List.lengthmenu.entries));List.iter(funentry->matchentry.kindwith|Action_->()|Menum->beginclose_children~timeoutscreenm;close~timeoutscreenmend)menu.entries(* Close all closable menus, and un-activate the top menu *)letclose_treescreenmenu=pre"CLOSE_TREE";lett=topmenuinclose_childrenscreent;t.active<-falseletclose_entry~timeoutscreenentry=matchentry.kindwith|Action_->()|Menum->close~timeoutscreenm;close_children~timeoutscreenm(* Close the other menus at the same level *)letclose_others?(timeout=false)screenentry=letmenu=entry.parent_menuinletother_entries=List.filter(fune->notLayout.(e.layout==entry.layout))menu.entriesinpre(Printf.sprintf"OTHER ENTRIES = %i"(List.lengthother_entries));List.iter(close_entry~timeoutscreen)other_entriesletrun_actionscreenentry=matchentry.kindwith|Menu_->printddebug_error"Cannot run action on a Menu entry"|Actionaction->letbg=Layout.opaque_bgButton.color_oninreset_entry~bgentry;action();(* We use a Timeout to make the colored entry visible longer. Warning: it
is possible that the menu state be scrambled if the user is fast enough
to do things in the Timeout delay...*)ignore(Timeout.add100(fun()->reset_entryentry;(* reset usual background *)close_treescreenentry.parent_menu))(* Ask the board to set keyboard (and hence mouse) focus on the entry. *)letset_keyboard_focusentry_layout=letfilter=Layout.get_roomsentry_layout|>List.rev|>List.hdinif!debugthenassert(filter.Layout.name=Some"_filter");Layout.claim_keyboard_focusfilter(* 2. Functions for reacting to events *)(* ----------------------------------- *)(* The behaviour we code here is more or less the same as QT/KDE apps. It's
not exactly the same as GTK apps. *)(* button_down can open/close menus. It also toggles the 'active' state of the
parent menu, which is reponsible for opening submenus on mouse over or not,
and works only if the parent menu is 'always_shown'. *)letbutton_downscreenentry=pre"BUTTON_DOWN";ifentry.enabledthenbeginmatchentry.kindwith|Menumenu->ifmenu.activethenbeginclose_childrenscreenentry.parent_menu;highlight_entryentry;(* because closing menu will also reset the parent
entry. We don't want this here since the mouse is
over. *)ifentry.parent_menu.always_shownthenentry.parent_menu.active<-falseendelsebeginpre(Print.layout_downentry.layout);set_keyboard_focusentry.layout;activatescreenmenu;activatescreenentry.parent_menuend|Action_->()(* actions are executed on button_up *)endletbutton_upscreenentry=pre"BUTTON_UP";(* the entry here is maybe the wrong one, because it is the one that has
'focus' in the sense of main.ml, not necessarily the highlighted entry,
due to 'drag' mechanism: if the user clicked on some entry, and then
moved to another without letting the button up. So we switch:*)letentry=default(selected_action_entryentry.parent_menu)entryinifentry.enabledthenbeginmatchentry.kindwith|Menu_->()(* menus are open/closed on button_down or mouse_over *)|Action_->run_actionscreenentryend(* mouse_enter (and mouse_motion?). mouse_motion will be useful only when we
add keyboard support. *)letmouse_overscreenentry=pre"MOUSE_OVER";ifentry.enabled&¬entry.selectedthenbeginhighlight_entryentry;close_others~timeout:truescreenentry;(* set_keyboard_focus entry.layout; *)(* Attention ça génère des mouse_enter/leave... *)matchentry.kindwith|Menumenu->if(notmenu.active)&&entry.parent_menu.activethenbeginactivate~timeout:truescreenmenu;end|Action_->()endletmouse_leaveentry=pre"MOUSE_LEAVE";ifentry.enabledthenbeginifnot(entry_is_openentry)thenreset_entryentry;ifentry.parent_menu.activethenmatchentry.kindwith|Menu_->()(* if menu.active then close screen menu *)|Action_->()end(* Keyboard navigation. The main entry keeps keyboard_focus while navigating
its submenus. *)(* TODO here we use up/down as if the menu were vertical. What about if the
menu is horizontal, or even custom?? *)(* TODO vérifier pourquoi ça génère MOUSE_OVER *)(* TODO selectionner la bonne entrée lorsqu'elle a été mouse_over'ed *)letkey_downscreenentrykeycode=pre"KEY_DOWN";ifkeycode=Sdl.K.escapethenclose_treescreenentry.parent_menuelseifentry.enabledthenifkeycode=Sdl.K.return||keycode=Sdl.K.spacethenbeginmatchentry.kindwith|Menumenu->(* 1/ouvrir 2/selectionner premier *)ifmenu.activethenset_keyboard_focus(List.hdmenu.entries).layout(* vérifier liste non vide ? *)elseactivatescreenmenu|Action_->run_actionscreenentryendelseifkeycode=Sdl.K.up||keycode=Sdl.K.downthenmatchselected_entryentry.parent_menuwith|None->printddebug_error"Cannot find selected entry in menu!"|Some(_,i0)->pre(string_of_inti0);letn=List.lengthentry.parent_menu.entriesinletrecloopi(* search enabled entry upwards *)=leti=(ifkeycode=Sdl.K.upthen(i-1+n)elsei+1)modninletnew_entry=List.nthentry.parent_menu.entriesiinifnew_entry.enabledthennew_entryelseifi=i0thenentryelseloopiinletnew_entry=loopi0inset_keyboard_focusnew_entry.layout(* 3. Creation of widgets and connections. *)(* --------------------------------------- *)(* First we must coat all entry layouts using the Popup module, in order to
get the correct mouse focus. This means that menus will be drawn on a
separate layer. The coat has a widget (either Empty of Box) that will
handle the connections. *)letconnect_entryscreenlayerentry=(* 'layer' is the coating layer *)letcoat=Popup.filter_screen~keyboard_focus:false~layerentry.layoutin(* We need a coat to get mouse focus on the whole length of the menu entry,
not only on the area of the text itself (label). *)Layout.add_room~dst:entry.layoutcoat;Layout.resize_follow_housecoat;(* we don't use Popup.add_screen to avoid creating too many layers. *)letwidget=Layout.widgetcoatinWidget.set_cursorwidget(Some(go(Draw.create_system_cursorSdl.System_cursor.hand)));letaction___=button_downscreenentryinletc=Widget.connect_mainwidgetwidgetactionTrigger.buttons_downinWidget.add_connectionwidgetc;letaction___=button_upscreenentryinletc=Widget.connect_mainwidgetwidgetactionTrigger.buttons_upinWidget.add_connectionwidgetc;letaction___=mouse_overscreenentryinletc=Widget.connect_mainwidgetwidgetaction[(* Trigger.E.mouse_motion; *)Trigger.mouse_enter]in(* Warning do NOT add finger_motion, it will interfere with finger_down.
TODO finger doesn't work well yet. *)Widget.add_connectionwidgetc;letaction___=mouse_leaveentryinletc=Widget.connect_mainwidgetwidgetaction[Trigger.mouse_leave]inWidget.add_connectionwidgetc;letaction__ev=key_downscreenentrySdl.Event.(getevkeyboard_keycode)inletc=Widget.connect_mainwidgetwidgetaction[Trigger.E.key_down]inWidget.add_connectionwidgetcletrecconnect_loopscreenlayermenu=List.iter(funentry->ifLayout.get_roomsentry.layout<>[]thenconnect_entryscreenlayerentry;(* :we don't connect the separators *)matchentry.kindwith|Menusubmenu->connect_loopscreenlayersubmenu|Action_->())menu.entries(* Init, attach the menu to a destination layout. *)(* TODO: by default we should not bother providing a dst, it should
automatically attach to its house. *)letinit_now~dstt=ifLayout.(dst==t.room)thenbeginprintd(debug_error+debug_board)"The destination %s of the menu cannot be the menu itself"(Layout.sprint_iddst);invalid_arg"[Menu.init_now] t=dst=%s"(Layout.sprint_iddst);end;printddebug_custom"Menu init to dst=%s"(Layout.sprint_iddst);letdst_layer=Chain.last(Layout.get_layerdst)inletentry_layer=Popup.new_layer_abovedst_layerinadd_menu_to_layertentry_layer;letcoating_layer=Popup.new_layer_aboveentry_layerin(* the screen is used to grab all mouse focus outside of the submenus while
they are open *)letscreen=Popup.filter_screen~layer:entry_layer(* ~color:Draw.(more_transp (transp green)) *)(* DEBUG*)dstin(* Le screen couvre tout ce qui est actuellement tracé, y compris le menu,
mais les connexions pour les entrées de menu sont sur le coating_layer, qui
est encore au dessus, donc ça fonctionne. TODO ça serait plus logique que
le screen soit entre dst_layer et entry_layer. Ou alors le mettre AVANT les
entries pour qu'il soit recouvert par elles (c'est le contraire
actuellement). ATTENTION si un deuxième menu est construit après, il sera
affiché AU DESSUS de ce screen... *)(* TODO one could reserve a special layer for some usual menu types, like menu
bar on the main layout, and make sure this layer is always above anything
else. OU ALORS: définir le screen de façon dynamique quand on clique. *)connect_loopscreencoating_layert;add_menu_to_dst~skip_first:(Layout.get_houset.room<>None)~dstt;screen_disablescreen;Layout.add_room~dstscreen;Layout.resize_follow_housescreen;letw=Layout.widgetscreeninWidget.on_click~click:(fun_->pre"CLICK SCREEN";close_treescreent(* screen_disable screen *))w(* If dst is not provided we delay the initialization (push it to Sync). *)letinit?dstt=letdst=Option.(mapsomedst)inSync.optiondst(fun()->pre"Delaying Menu init to Sync";Layout.get_houset.room)(Option.iter(fundst->init_now~dstt))(* TODO add a warning if None *)end(* Now we can make a friendly API for creating elements of the menu type. *)(* ---------------------------------------------------------------------- *)(* example:
let file = Tower [{label = (Text "open"); content = (Action open_in)};
etc...] in
let edit = ... in
Flat [
{label = (Text "File"); content = (Menu file)};
{label = (Text "Edit"); content = (Menu edit)};
etc... ]
*)typet=Engine.menutypeaction=unit->unittypelabel=|Textofstring|LayoutofLayout.t(* The user (programmer) can either define the menu entry by a text -- like
'File', etc. or directly by an arbitrary layout -- useful for game menus, for
instance. In the latter case, the layout content is not altered to ensure
that its features, whether it is part of a menu or not, are not
altered. However, we cannot preserve its house (and it should not have any),
because usually the menu is relocated into the main window-layout. One can
'kind-of' preserve the house by letting it be the 'dst' parameter. But
warning, in all cases, the layout will be encapsulated into a screen, so the
'dst' will not remain its "direct house". *)typeentry={label:label;content:content}(* TODO: add "hover" field to execute an action on hovering the entry (useful
for games). Mieux: ajouter "connection" field? *)(* the content type mixes two different things: Actions and submenus. Not clean
from the point of view of the library programmer (me), but (I think) simpler
from the 'public' viewpoint. Thus, before working with this, we convert into
the Engine types. *)andcontent=|Actionofaction|Flatofentrylist|Towerofentrylist|Customofentrylist|Separatorletseparator={label=Text"Dummy separator label";content=Separator}lettext_margin=5(* Text to Layout. w and h are only used for text. maybe remove *)letformat_label?w?h=function|Texts->letres=Layout.resident?w?h(Widget.labels)in(* : here we cannot use a resident as is because we will need to add another
room later; we need to wrap it: *)letbackground=Layout.opaque_bgDraw.menu_bg_colorinLayout.flat~name:"menu entry label"~margins:text_margin~background[res]|Layoutl->letname="formatted label"inif!debugthenassert(l.Layout.name<>Somename);(* this function should be applied only ONCE to the label *)Layout.superpose~name[l](* We preserve the (x,y) position. *)(* Warning, does not check whether there is already an icon... *)letadd_icon_suffix?(icon="caret-right")layout=(* the icon used to indicate submenus *)letsubmenu_indicator=Layout.resident~name:icon(Widget.iconicon)inLayout.add_room~dst:layout~valign:Draw.Center~halign:Draw.Maxsubmenu_indicator(* really private, hackish, function...to call only after connections/filters
have been added. It relies on the fact that the icon should be the 2nd-to
last room of the list (the last one being the filter). *)letremove_icon_suffix?(icon="caret-right")layout=trybeginLayout.iter_rooms(funl->pre(Layout.sprint_idl))layout;matchList.rev(Layout.get_roomslayout)with|[]|[_]->()|filter::(this::others)->assert(defaultthis.Layout.name""=icon);(* Layout.delete_textures this; *)(* In [bar], for instance, this function is called via Sync before
creating textures, so there should be no texture to free. *)Layout.set_roomslayout(List.rev(filter::others))endwith|e->printddebug_error"Menu: Cannot remove icon suffix";raiseeletsuffix_width=10(* TODO compute this *)moduleTmp=struct(* We temporarily convert to a more programmer-friendly type, before
converting to Engine.menu. This type also carry more information
(eg. suffix)that can be modified for a customizable menu. *)(* position of the submenu wrt the parent label *)typeposition=|Below|RightOftypemenukind=|Flat|Tower|Customtypemenu={entries:tentrylist;kind:menukind}andtcontent=|Actionofaction|Menuofmenu|Separatorandtentry={label:label;(* ignored in case of Separator *)content:tcontent;mutableformatted:bool;mutablesuffix:positionoption}(* TODO add keyboard shortcuts *)letget_layoutentry=if!debugthenassertentry.formatted;matchentry.labelwith|Text_->failwith"get_layout should be called only when the Layout is \
generated. BUG."|Layoutl->lletcompute_suffixentry=do_optionentry.suffix(funp->leticon=matchpwith|Below->"caret-down"|RightOf->"caret-right"inmatchentry.contentwith|Menu_->add_icon_suffix~icon(get_layoutentry)|_->())letnext_submenu_position_old=function|Flat->pre"BELOW";SomeBelow|Tower->pre"RIGHTOF";SomeRightOf|Custom->pre"NONE";None(* Return a copy of the tree with all Text labels replaced by Layouts *)letreccompute_layoutsentry=letlayout=ifentry.formattedthenget_layoutentryelsematchentry.contentwith|Separator->letbackground=Layout.opaque_bgDraw.greyinLayout.empty~background~w:10~h:1()|Menu_|Action_->format_labelentry.labelinifnotentry.formatted&&entry.suffix<>NonethenLayout.(set_widthlayout(widthlayout+suffix_width));(* we make some room for adding the suffix later *)letcontent=matchentry.contentwith|Action_->entry.content|Menumenu->letentries=List.mapcompute_layoutsmenu.entriesinMenu{menuwithentries}|Separator->Separatorin{label=Layoutlayout;content;formatted=true;suffix=entry.suffix}letmenu_formatter=function|Flat->(funlist->letbackground=Layout.opaque_bgDraw.menu_bg_colorinletshadow=Style.mk_shadow~offset:(1,1)~size:1()inLayout.flat~name:"menu flat"~margins:0~background~shadowlist)|Tower->(funlist->letshadow=Style.mk_shadow~offset:(1,1)~size:1()inletbackground=Layout.opaque_bgDraw.menu_bg_colorinletl=Layout.tower~name:"menu tower"~margins:0~sep:0~background~shadowlistinLayout.expand_widthl;l)|Custom->(funlist->Layout.superpose~name:"menu custom"list)(* Return (x,y) option, the coordinates where the submenu should be placed
when positioned in the same layout as the parent layout. *)letsubmenu_posparentposition=letw,h=Layout.get_sizeparentinmap_optionposition(function|Below->(0,h)|RightOf->(w,0))letget_entries=function|Menumenu->menu.entries|_->pre"get_entries should be called only with Menu.";[](* Compute the room containing the menu. *)letcompute_roommenu=letlayouts=List.mapget_layoutmenu.entriesinletroom=menu_formattermenu.kindlayoutsinroom(* Convert an entry to an Engine.entry. Warning, this is not an obvious
function, because Engine.entry is bidirectional, and hence cannot be
created by a simple recursive loop. We need to use mutability: some fields
are filled in later. *)(* This should be called on a well prepared entry tree where all labels are
layouts. *)(* 'position' indicates where to put the submenu in case entry has a
submenu. *)letrecentry_to_engineparent_menuentry=letlayout=get_layoutentryin(* We add the suffixes, except for the first entry, which is dummy, see
create_engine below. *)ifnot(Engine.is_topparent_menu)thencompute_suffixentry;letkind=matchentry.contentwith|Actiona->Engine.Actiona|Separator->Engine.separator|Menumenu->letroom=compute_roommenuinletpos=submenu_poslayoutentry.suffixinletengine_menu=Engine.{pos;active=false;always_shown=false;entries=[];(* will be inserted later *)room;parent_entry=None}inEngine.Menuengine_menuinletengine_entry=Engine.{kind;enabled=entry.content<>Separator;selected=false;layout;parent_menu}in(* second pass to recursively insert the entries field *)let_=matchengine_entry.Engine.kindwith|Engine.Action_->()|Engine.Menumenu->menu.Engine.parent_entry<-Someengine_entry;letentries=List.map(entry_to_enginemenu)(get_entriesentry.content)inmenu.Engine.entries<-entries;inengine_entry(* Create an Engine.menu from a content *)letcreate_engine=function|Action_->failwith"Cannot create a menu from an Action content."|content->letdummy_parent=Layout.empty~name:"dummy parent"~w:0~h:0()inletentry=compute_layouts{label=Layoutdummy_parent;content;formatted=true;suffix=None}inletparent_menu=Engine.{pos=None;active=true;always_shown=true;entries=[];room=dummy_parent;parent_entry=None}inleteentry=entry_to_engineparent_menuentryinletopenEngineinletmenu=matcheentry.kindwith|Action_->failwith"An Action should not show up here. BUG."|Menumenu->menuinmenu.Engine.always_shown<-true;menu.Engine.parent_entry<-None;(* remove the dummy parent *)menu(* TO BE CONTINUED... *)endletnext_entry_position=function|Custom_|Separator|Action_->None|Tower_->SomeTmp.RightOf|Flat_->SomeTmp.Below(* Convert to the Tmp type, guessing a standard suffix *)letreccontent_to_tmpposition=function|Actiona->Tmp.Actiona|Flatlist->letentries=List.map(entry_to_tmpposition)listinTmp.(Menu{entries;kind=Flat})|Towerlist->letentries=List.map(entry_to_tmpposition)listinTmp.(Menu{entries;kind=Tower})|Customlist->letentries=List.map(entry_to_tmpposition)listinTmp.(Menu{entries;kind=Custom})|Separator->Tmp.Separatorandentry_to_tmppositionentry=letnext_position=next_entry_positionentry.contentin{Tmp.label=entry.label;Tmp.content=content_to_tmpnext_positionentry.content;Tmp.formatted=false;Tmp.suffix=position}letlayout_of_menumenu:Layout.t=menu.Engine.roomletset_layoutmenuroom=menu.Engine.room<-roomletraw_enginecontent=letposition=next_entry_positioncontentinlettcontent=content_to_tmppositioncontentinTmp.create_enginetcontentletmake_engine?dstcontent=lett=raw_enginecontentinEngine.init?dstt;t(* Create a generic menu layout and insert it into the dst layout. *)(* let create ~dst content =
* let t = make_engine ~dst content in
* layout_of_menu t *)letcreate=make_engine(* Specific "menu bar" creation. If dst is not prodived we sync-install the menu
in the house of the main menu layout. *)letmake_bar?dstentries=letcontent=Flatentriesinpre"make_engine";lett=make_engine?dstcontentinletroom=layout_of_menutinpre"resize...";letdst=Option.(mapsomedst)in(* "add a Some to a Some" *)Sync.optiondst(fun()->pre"getting house";Layout.get_houseroom)(function|None->printd(debug_board+debug_error)"Menu %s has no house. It will not be usable"(Layout.sprint_idroom)|Somedst->(* Expand first entry (menu bar) to the whole dst width: *)Layout.(set_widthroom(widthdst));Layout.setxroom0;Layout.setyroom0;(* All menu layouts are rooms of dst. (Warning: a submenu is not a
subroom of the room of the parent menu... They are all on the same
level.)*)(* The bar should expand when we resize the room; however we may want to
keep the menu entries left-aligned, instead of evenly spread on the
whole width. TODO. *)Layout.scale_resize~scale_height:falseroom;Layout.resize_contentdst;Layout.ask_updateroom;(* for a menu bar, we usually don't want indicator icons *)List.iter(funentry->letopenEngineinmatchentry.kindwith|Menu_->remove_icon_suffix~icon:"caret-down"entry.layout|Action_->())t.Engine.entries);Layout.set_shadowroomNone;(* this should be done before Sync *)roomletbarentries=make_barentriesletadd_bar~dstentries=let_:Layout.t=make_bar~dstentriesin()