12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103(** BOGUE *)(** A GUI Library for Ocaml, using SDL2 *)(** Vu Ngoc San, December 2013 -- now *)(* doc on threads:
https://ocaml.github.io/ocamlunix/threads.html
*)openB_utilsopenTsdlmoduleAvar=B_avarmoduleDraw=B_drawmoduleE=Sdl.EventmoduleLayout=B_layoutmoduleMouse=B_mousemodulePrint=B_printmoduleShortcut=B_shortcutmoduleSync=B_syncmoduleTime=B_timemoduleTimeout=B_timeoutmoduleTrigger=B_triggermoduleUpdate=B_updatemoduleWidget=B_widgetmoduleWindow=B_windowexceptionExittypeboard={mutablewindows:Window.tlist;(* : one layout per window. This is (mostly) redundant with the next field
'windows_house' *)windows_house:Layout.t;(* : a special Layout containing the layouts of the board defined above in the
layouts field. Rarely used, in fact just the list of windows is enough. But
sometimes, it's convenient to use operations directly on the
windows_house. Warning, the layouts should NOT indicate the windows_house
in their House field. *)mutablemouse_focus:Layout.toption;(* : the room containing the mouse. It must contain a Widget. *)mutablekeyboard_focus:Layout.toption;(* : the room with keyboard focus. It must contain a Widget *)(* It is important that keyboard focus may be different from mouse focus, cf
example 12: one wants to be able to continue typing even when the mouse
goes out of the text entry widget. *)mutablebutton_down:Layout.toption;(* : the room where the button_down has been registered. Used to trigger
full_click event *)mutableshortcuts:boardShortcut.t;(* Global keyboard shortcuts. TODO some shortcuts should be executed only on
Key_up to avoid auto-repeat. => create 2 maps, shortcuts_down et
shortcuts_up. Or maybe all? *)mutableshortcut_pressed:bool;mutablemouse_alive:bool;(* True as soon as the mouse has moved. Because SDL will report position 0,0
when the window opens, but we dont want to activate a widget if it is
located at 0,0...*)on_user_event:(Sdl.event->unit)option}typeshortcuts=boardShortcut.tletexit_on_escape=(Sdl.K.escape,Sdl.Kmod.none,fun(_:board)->raiseExit)letget_frame()=!Avar.frameletget_layoutsboard=List.mapWindow.get_layoutboard.windows(* should be the same as getting the Rooms content of the windows_house *)(* We return the mouse_focus. Sometimes it does not belong to the active tree,
see example 19 (tabs) *)letget_mouse_focusboard=board.mouse_focusletset_windowsboardwindows=board.windows<-windows;board.windows_house.Layout.content<-Layout.Rooms(List.mapWindow.get_layoutwindows)(* TODO connections? widgets? *)letclose_windowwindow=letlayout=Window.get_layoutwindowinprintddebug_board"** Closing window #%u (Layout %s)"(Window.idwindow)(Layout.sprint_idlayout);(* TODO: stop all animations ? *)if!Avar.alive_animations>0thenbeginprintddebug_warning"%d animation%s not stopped. We reset the counter."!Avar.alive_animations(if!Avar.alive_animations=1then" was"else"s were");Avar.alive_animations:=0end;List.iterWidget.remove_active_connections(Layout.get_widgetslayout);ifSdl.is_text_input_active()thenSdl.stop_text_input();(* DEBUG: clipboard sometimes causes problems *)(* if Sdl.has_clipboard_text () *)(* then begin let text = go(Sdl.get_clipboard_text ()) in *)(* printd debug_warning "Clipboard has [%s]" text *)(* end; *)Layout.delete_textureslayout;(* now we destroy the canvas (renderer and window): *)Draw.destroy_canvas~bogue:window.Window.bogue(Layout.get_canvaslayout);Layout.remove_canvaslayout(* only for debugging *)letcheck_cemetery()=letnzombies=List.length!Layout.cemeteryinifLayout.check_cemetery()thenprintddebug_memory"All zombies have been killed! Congratulations!"elseif!debugthenbeginprintddebug_memory"==> Still some living deads around. Invoking GC";Gc.compact();ifLayout.check_cemetery()thenprintddebug_memory"All zombies have been killed! Congratulations!"elseprintddebug_memory"Percentage of killed zombies = %u%% (out of %u)."(round(100.-.100.*.(float(List.length!Layout.cemetery)/.(floatnzombies))))nzombiesend(* Call this to close everything. Don't use the layouts after this! *)(* However in principle you can run the board again, and then the layouts are
usable(?) *)letexit_boardboard=ifSync.execute1000thenprintddebug_warning"Some Sync action was queueing and hence executed \
before exiting board.";Update.clear();Timeout.clear();check_cemetery();List.iterclose_windowboard.windows;board.mouse_focus<-None;board.keyboard_focus<-None;board.button_down<-None;Draw.destroy_textures();(* en principe inutile: déjà fait *)(* Layout.clear_wtable (); *)Draw.check_memory();Trigger.flush_all();flush_log()(* TODO: this is not enough cleaning. + one needs an "init" function to
restart. *)letquit=Draw.quit(** redisplay the layouts to the layers *)(* there is no clear screen here *)(* it will display a layout only if window.refresh = true *)(* it should NOT be used more than once per loop (because of transparency) *)letdisplayboard=(* We flush the events asking for redrawing. TODO: make it window-specific. *)Trigger.(flushredraw);List.iter(funw->ifnot(Window.is_freshw)thenWindow.renderw)board.windows(* or Layout.render board.windows_house *)(** Render all layers and flip window buffers, only for windows with the
is_fresh=false field *)letflip?clearboard=List.iter(Window.flip?clear)board.windows;Draw.destroy_textures()(** Update layout of window that was resized by user *)letresizewindow=letlayout=Window.get_layoutwindowinifWindow.sizewindow<>Layout.get_physical_sizelayoutthenbeginprintddebug_graphics"Resize window (Layout #%u)"layout.Layout.id;Layout.resize_from_window~flip:falselayout;Window.to_refreshwindowend(* Dynamically add a new window (given by the layout) to the board, while
running. *)letadd_windowboardlayout=Layout.move_to_new_stacklayout;letwindow=Window.createlayoutinWindow.make_sdl_windowwindow;(* update board *)letwindows=List.rev(window::(List.revboard.windows))inset_windowsboardwindows;(* show *)Sdl.show_window(Layout.windowlayout);Draw.update_background(Layout.get_canvaslayout);(* run *)displayboard;do_option(get_mouse_focusboard)Layout.set_focus;flipboard;(* We send the startup_event to all widgets *)List.iter(Widget.wake_up(Trigger.startup_event()))(List.flatten(List.mapWidget.connections(Layout.get_widgetslayout)));Trigger.renew_my_event();windowletsame_windoww1w2=Sdl.(get_window_idw1=get_window_idw2)(** get window (layout) by id. Not used... (layout_event can do it somehow) *)letget_window_by_idboardid=letrecloop=function|[]->printddebug_error"There is no window with this id#%d"id;List.hdboard.windows;|w::rest->ifid=Window.idwthenwelselooprestinloopboard.windows(* Remove window and raise Exit if there are no more active windows. *)letremove_windowboardwindow=letwindows=List.filter(funw->not(Window.equalwindoww))board.windowsinset_windowsboardwindows;close_windowwindow;(* We reset all focus for safety. TODO: one could reset only those that
belonged to the removed window. *)board.mouse_focus<-None;board.keyboard_focus<-None;board.button_down<-None;ifboard.windows=[]thenbeginprintddebug_board"No more windows. We quit.";(* The `Quit` event seems to be emitted when the user clicks on the Close
button of the last open window, but not when we delete windows
manually. Hence we have to raise Exit.*)(* TODO instead we should check if there are Sync actions or Timeouts
waiting?*)raiseExitendelseifnot(List.existsWindow.is_shownboard.windows)thenbeginprintd(debug_board+debug_user)"Some windows are alive, but all windows are hidden. We quit.";(* Is there a scenario where we would not like to quit here? *)raiseExitend(*************)letshowboard=List.iter(funw->Window.show_maybew;Window.to_refreshw;Draw.update_background(Window.get_canvasw))board.windows(* return the widget with mouse focus *)letmouse_focus_widgetboard=map_option(get_mouse_focusboard)Layout.widget(* return the widget with keyboard_focus *)letkeyboard_focus_widgetboard=map_optionboard.keyboard_focusLayout.widgetletbutton_down_widgetboard=map_optionboard.button_downLayout.widget(* which layout (ie window) has mouse focus? *)letlayout_focusboard=matchSdl.get_mouse_focus()with|None->None(* the mouse is outside of the SDL windows *)|Somew->(* we return the first corresponding window (there should be only
one anyway) *)list_check_ok(funl->same_window(Layout.windowl)w)(get_layoutsboard)(* What is the window containing this house *)lettop_houseboardroom=lettop=Layout.top_houseroominlist_check_ok(funl->Layout.(top==l))(get_layoutsboard)(* which Window.t corresponds to the event? *)letwindow_of_eventboardev=tryletido=matchTrigger.event_kindevwith|`Bogue_redraw->letwid=E.(getevuser_code)inmap_option(Layout.of_widwid)(funr->letid=Sdl.get_window_id(Layout.windowr)inprintddebug_event"Redraw event window_id=%d"id;id)|_->Some(Trigger.window_idev)incheck_optionido(funid->list_check_ok(funw->id=Window.idw)board.windows)withNot_found->printddebug_error"Search window for event %s caused an error"(Trigger.sprint_evev);None(* Detect layout under mouse, with top layer (= largest "depth") *)letcheck_mouse_focusboard=ifboard.mouse_alivethenlet(x,y)=Mouse.pos()inprintddebug_board"Mouse pos:(%u,%u)"xy;check_option(layout_focusboard)(Layout.top_focusxy)elseNone(* detect layout (room or widget) under mouse; only used for testing *)letcheck_mouse_hoverboard=let(x,y)=Mouse.pos()incheck_option(layout_focusboard)(Layout.hoverxy)(* [check_mouse_motion] deals with sending the mouse_enter/mouse_leave events *)(* The optional [target] argument can be used to specify the layout that should
be considered as the new layout "under mouse", instead of really checking
mouse position. Used for keyboard interaction. *)(* This also sets the cursor. Overriding cursor is always possible by reacting
to the mouse_leave/enter events. We try to keep at most one mouse_leave event
and at most one mouse_enter event in the queue (however see remark in
trigger.ml). The rule is that this function is called only when
there is no pending event, which means that no mouse_enter/leave event will
be sent until the previous ones are dealt with. Therefore it is NOT
guaranteed that all widgets receive their due mouse_enter/leave events, in
case many of them are triggered at the same time. The program should not rely
on this. *)letcheck_mouse_motion?targetboard=letopenLayoutinletopenTriggerinletmf=matchtargetwith|Some_->target|None->check_mouse_focusboardinlet()=match(get_mouse_focusboard),mfwith(* on compare l'ancien et le nouveau. See remarks in trigger.ml *)|None,None->()|Somer,None->unset_focusr;push_mouse_leave(r.Layout.id);set_cursorNone;|None,Somer->set_focusr;push_mouse_enter(r.Layout.id);set_cursor(Somer);|Somew1,Somew2->ifnot(Widget.equal(widgetw1)(widgetw2))then(set_focusw2;unset_focusw1;(* we send mouse_leave to w1 *)push_mouse_leave(w1.Layout.id);(* we send mouse_enter to w2 *)push_mouse_enter(w2.Layout.id);set_cursor(Somew2))inboard.mouse_focus<-mf(* Rm: in case of triggered action, this is already done by the redraw/refresh
event *)letdragging=refNone(* the initial position of the dragged room *)(* put this in board, or local to the drag & drop functions? *)(* guess which widget the event should be targetted to (or None) *)lettarget_widgetboardev=letroomo=ifE.(getevtyp)=Trigger.mouse_enter||E.(getevtyp)=Trigger.mouse_leavethenletid=E.getevTrigger.room_idinLayout.of_id_opt~not_found:(fun()->printddebug_error"The room #%u has disappeared but was pointed by \
the mouse enter/leave event"id)idelsematchboard.button_downwith|Somer(*when !dragging*)->printddebug_board"Target: select button_down (%s)"(Layout.sprint_idr);Somer(* when dragging, the board.button_down has priority over all *)(* TODO: it happens also for push buttons, scroll bars, etc... *)(* OR: give board.button_down priority for ALL but for menus (find
something else, like activate what was selected in the menu...) *)|None->ifTrigger.text_eventev||map_optionboard.button_downLayout.has_keyboard_focus=Sometrue(* if the button remains down, the initial text event keeps
listening to events *)(* TODO: idem for mouse_button_up? *)then(printddebug_board"Target: select keyboard widget";board.keyboard_focus)else(printddebug_board"Target: select mouse widget";(get_mouse_focusboard))inmap_optionroomoLayout.widget(* Are all the widgets rendered up-to-date? *)letis_freshboard=(* List.fold_left (fun yes b -> yes && (Layout.is_fresh b)) true
board.layouts *)Layout.is_freshboard.windows_house(** display only widgets that need to be updated *)(* because of transparency effects, this is almost impossible to use *)letupdate_oldboard=List.iter(funw->ifnot(Window.is_freshw)&&Draw.window_is_shown(Window.sdl_windoww)then(Window.to_refreshw;Layout.update_old(Window.get_layoutw))elseprintddebug_board"Window is hidden")board.windows(* without the shown test, one could do directly: Layout.update
board.windows_house *)lethas_animboard=(* !Avar.alive_animations > 0 || *)(* useful? only if we have some animated variables that are not used in the
Layout.display *)(List.fold_left(funbw->leth=Layout.has_anim(Window.get_layoutw)inifhthenWindow.to_refreshw;h||b)falseboard.windows)(* ou bien: Layout.has_anim board.windows_house *)(* the "drop" part of drag-and-drop. It is only called by "drag" *)letdropboard=matchboard.button_downwith|None->()|Someroom->beginprintddebug_board" ----> Drop";(*board.button_down <- None;*)letopenLayoutindo_option!dragging(slide_toroom);dragging:=None;end(* to drag, we use the anim mechanism *)(* TODO: drag Rooms layouts, not only Residents *)letdragboardevroom=letopenLayoutinmatchTrigger.event_kindevwith|`Mouse_motionwhennot(!dragging<>None)&&Trigger.mm_pressedev->(* if room.keyboard_focus <> Some true (* TODO use a "dragable" property
instead *) then *)(* save initial position: *)dragging:=Some(getxroom,getyroom);follow_mouseroom;board.button_down<-Someroom;printddebug_board" ----> Drag";None(* drag *)|`Mouse_button_upwhen!dragging<>None->dropboard;Someev(* : we pass the button_up event *)|`Mouse_motionwhen(!dragging<>None&&E.(getevmouse_motion_state)=0l)(* : button is not pressed *)->dropboard;None(* we do this because the mousebuttonup event might have been deleted before
treated... Problem: if the window initially has no focus, and you drag
something directly, and move the cursor out of the window, and then release
button, the mouse_button_up event is NOT registered...?? *)(* TODO: drag and drop to another window *)|_->Someevletactivateboardroomo=board.button_down<-roomo;(matchboard.keyboard_focus,roomowith|Somekr,SomemrwhennotLayout.(kr==mr)->Layout.remove_keyboard_focuskr;Layout.ask_updatekr(* TODO à déplacer en button_up *)|Somekr,None->Layout.remove_keyboard_focuskr;Layout.ask_updatekr(* TODO idem -- et regrouper *)|_->())(* Impose mouse focus, and trigger mouse_enter/leave events as a consequence
(regardless of actual mouse position.) *)letset_mouse_focusboardtarget=check_mouse_motion?targetboardletset_keyboard_focusboardro=activateboardro;board.keyboard_focus<-ro;do_optionro(funr->Layout.set_keyboard_focusr;check_mouse_motion~target:rboard)(* = selecting something via the keyboard should also set this as mouse focus
(to get highlight, to trigger mouse_leave, etc. but without moving the
mouse cursor...) *)(* react to the TAB key. We look for the next room that is shown in one of the
windows and give it keyboard focus. Rooms with show=false OR in a detached
layout cannot be selected for keyboard focus. *)(* TODOO this should not permit to activate items that are hidden behind a
popup... Maybe we could restrict TAB nagivation to a unique layer? (or layers
above the current one?)*)lettabboard=letcurrent_room=matchboard.keyboard_focuswith|Somer->r|None->match(get_mouse_focusboard)with|Somer->r|None->matchlayout_focusboardwith|Somel->l|None->Window.get_layout(List.hdboard.windows)inlettop=matchtop_houseboardcurrent_roomwith|None->printd(debug_board+debug_custom)"Current keyboard focus %s has no Window..."(Layout.sprint_idcurrent_room);Window.get_layout(List.hdboard.windows)|Sometop->printddebug_custom"Current window is %s"(Layout.sprint_idtop);topinprintddebug_board"Current room #%u"current_room.Layout.id;Layout.keyboard_focus_before_tab:=Somecurrent_room;matchLayout.next_keyboard~topcurrent_roomwith|None->printddebug_board" ==> No keyboard focus found !"|Somerasro->printddebug_board"Activating next keyboard focus (room #%u)"r.Layout.id;set_keyboard_focusboardro(** open/close the debugging window *)lettoggle_debug_window=letwindow=refNoneinfunboard->match!windowwith|None->print_endline"OPENING DEBUG WINDOW";letdebug_window=B_debug_window.create()inletw=add_windowboarddebug_windowinwindow:=Somew|Somew->remove_windowboardw;window:=Noneletadd_debug_shortcutsshortcuts=shortcuts|>Shortcut.add_ctrl(Sdl.K.d,fun_->debug:=not!debug;print_endline(Printf.sprintf"Debug set to %b"!debug))|>Shortcut.add_ctrl_shift(Sdl.K.d,toggle_debug_window)|>Shortcut.add_ctrl_shift(Sdl.K.i,funboard->print_endline"Mouse Focus Layout parents:";print_endlinePrint.(optionlayout_upboard.mouse_focus))|>Shortcut.add_ctrl(Sdl.K.i,funboard->print_endline"Hover Layout children (don't trust this):";print_endlinePrint.(optionlayout_down(check_mouse_hoverboard)))|>Shortcut.add_ctrl_shift(Sdl.K.s,funboard->(* snapshot *)Print.dumpboard.windows_house)|>Shortcut.add_ctrl(Sdl.K.m,fun_->print_endline"Garbage collecting...";Gc.compact();Draw.memory_info())letrefresh_custom_windowsboard=List.iter(funw->printddebug_board"BOGUE WINDOW=%b"w.Window.bogue;ifnotw.Window.boguethenw.Window.is_fresh<-false)board.windowsletcheck_removedboard=do_optionboard.mouse_focus(funr->ifLayout.is_removedr||notr.Layout.showthenbeginprintddebug_board"Re-setting mouse_focus because layout %s was removed"(Layout.sprint_idr);check_mouse_motionboardend);do_optionboard.keyboard_focus(funr->ifLayout.is_removedr||notr.Layout.showthenbeginprintddebug_board"Unsetting keyboard_focus because layout %s was removed"(Layout.sprint_idr);board.keyboard_focus<-Noneend);do_optionboard.button_down(funr->ifLayout.is_removedr||notr.Layout.showthenbeginprintddebug_board"Unsetting button_down because layout %s was removed"(Layout.sprint_idr);board.button_down<-Noneend)(* EVENT LOOP *)(* First: we treat the events that should be filtered or modified. This returns
the evo_layout that the layout (& widget) is authorized to treat
thereafter. Returning None means that widgets will never react to such
event. Currently all events are returned, except for the Update and
Remove_layout events. *)letfilter_board_eventsboarde=letopenEinprintddebug_event"1==> Filtering event type: %s"(Trigger.sprint_eve);matchTrigger.event_kindewith|`Finger_motion->ifSdl.has_eventE.finger_motionthenNoneelseSomee(* There is (probably?) no use in treating finger motion if there is another
one in the queue. [This is NOT true for Mouse_motion, see below.] If really
the system was slow to treat this, we could check if the 'count' of the
event loop is not too high (TODO?). *)|`Bogue_keyboard_focus->set_keyboard_focusboard(Layout.of_id_opt(geteuser_code)~not_found:(fun()->printddebug_error"Room #%u pointed by event %s has disappeared"(geteuser_code)(Trigger.sprint_eve)));Somee(* ou None? *)|`Bogue_mouse_focus->printddebug_event"Require Mouse FOCUS";(* we filter and treat only the last event *)(* let e' = default (Trigger.get_last (Trigger.mouse_focus)) e in *)set_mouse_focusboard(Layout.of_id_opt(geteuser_code));Somee|`Bogue_mouse_enter->printddebug_event"Mouse ENTER";(* by design, only one mouse_enter event can exist in the queue if there is
no mouse_leave. *)Somee|`Bogue_mouse_leave->printddebug_event"Mouse LEAVE";(* by design, only one mouse_leave event can exist in the queue if there is
no mouse_enter. *)Somee|`Bogue_update->printddebug_event"Update";Update.executee;None|`Bogue_remove_layout->printddebug_event"Layout removed";Trigger.(flushremove_layout);(* not necessary in principle *)check_removedboard;None|`Render_targets_reset|`Render_device_reset->printd(debug_graphics+debug_error)"TODO! Reset all textures";Sdl.log"reset event";None|_->Somee(* Treat events that should be used before being sent to the layout & widget,
but without filtering. *)lettreat_layout_eventsboarde=letopenEinprintddebug_event"2==> Treating event type: %s"(Trigger.sprint_eve);beginmatchTrigger.event_kindewith|`Bogue_sync_action->(* This one should be executed before anything else. *)(* We run the actions in the Queue, and stop if the Queue is empty or
time has exceeded 10ms *)printddebug_event"Sync";ifnot(Sync.execute10)thenTrigger.flush(Trigger.sync_action)(* probably not useful *)(* Here we treat key events that have priority over the widgets *)|`Key_up->board.shortcut_pressed<-false;(* I assume auto-repeat will never trigger Key_up, but it seems that
it's not always the case... (can happen when a new window opens) *)|`Key_down->letpair=getekeyboard_keycode,getekeyboard_keymodinifnotboard.shortcut_pressedthendo_option(Shortcut.findboard.shortcutspair)(funa->board.shortcut_pressed<-true;aboard)|`Mouse_button_down|`Finger_down->(* For finger down and moving, the following events can be emitted quasi
simultaneously: (0x400) (0x401) (0x700) (0x400) (0x702) (0x400) (0x702)
(0x400) (0x702) *)Trigger.button_downe;activateboard(get_mouse_focusboard)(* | `Mouse_button_up when Trigger.has_full_click e ->
* printd debug_event "Full click" *)|`Mouse_button_up|`Finger_up->printddebug_event"Mouse button up !";Trigger.button_upe;ifnot!Trigger.too_fast&&(map2_optionboard.button_down(check_mouse_focusboard)Layout.equal=Sometrue||map_optionboard.button_downLayout.has_keyboard_focus=Sometrue)thenbeginprintddebug_event"full click";Trigger.set_full_clicke;(* full click means that the press and released were done on the same
widget. It does not mean that the click was "quick". For this, check
Trigger.single_click. *)(* Now we set keyboard_focus on "admissible" widgets: *)do_option(get_mouse_focusboard)(funx->printddebug_board"Mouse focus: %d"x.Layout.id);do_optionboard.keyboard_focus(funx->printddebug_board"Keyboard focus: %d"x.Layout.id);do_optionboard.button_down(funx->printddebug_board"Button down on #%d"x.Layout.id;Layout.set_keyboard_focusx);board.keyboard_focus<-board.button_down(* OK?? *)end|`Mouse_wheel->(* TODO change. mouse_wheel should be captured by the widget itself. *)do_option(get_mouse_focusboard)(funroom->do_option(Layout.find_clip_houseroom)(funroom->(* now we add up the number of wheel events in the queue. With
a standard mouse wheel one can easily add up to 5
events. With a touchpad, this can add up to 10 or more *)letlist=Trigger.filter_events(fune->Trigger.event_kinde<>`Mouse_wheel)inlettotal=List.fold_left(funsev->s+getevmouse_wheel_y)E.(getemouse_wheel_y)listinprintddebug_event"Total mouse wheels=%d"total;letdy=-total*50inLayout.scroll~duration:500dyroom;check_mouse_motionboard;Trigger.push_var_changedroom.Layout.id))|`Bogue_destroy_window->printd(debug_board+debug_event)"Destroy window request";do_option(window_of_eventboarde)(remove_windowboard);|`Window_event->(* https://github.com/libsdl-org/SDL/blob/main/include/SDL_video.h *)letwid=getewindow_event_idinprintddebug_event"Window event [%d]"wid;(* Warning: when resizing window by mowing the top-left corner, we trigger
trigger 6 = resize, and also 4 = "window_event_moved"... and sometimes
3 = exposed *)(* Some window events may come by pair; for instance if you middle_click
on the maximize button, it can trigger 10 (mouse enter) and then 6
(resize). *)beginmatchwindow_event_enumwidwith|`Resized->printddebug_event"(ignored) Resized => to (%lu,%lu)"(getewindow_data1)(getewindow_data2)(* https://wiki.libsdl.org/SDL_WindowEventID *)|`Size_changed->(* For resizing, we usually have wid = (4-moved), 6-size-changed,
5-resized, 3-exposed.*)printddebug_event"Size_changed => Resize to (%lu,%lu)"(getewindow_data1)(getewindow_data2);do_option(window_of_eventboarde)resize|`Exposed->(* The exposed event is triggered by X11 when part of the
window is lost and should be re-rendered. Sometimes several
exposed events are triggered because they correspond to
several regions of the window. This seems to be unreachable
via SDL. *)printddebug_event"Exposed";(* for some reason, when the size changes quickly, Exposed is
triggered but not Resized nor `Size_changed...*)do_option(window_of_eventboarde)(funw->letl=Window.get_layoutwinifWindow.sizew<>Layout.get_physical_sizelthenLayout.resize_from_window~flip:falsel;(* the renderer changed, we need to recreate all
textures *)Window.to_refreshw)|`Close->printd(debug_board+debug_event)"Asking window to close";do_option(window_of_eventboarde)(funw->ifnotw.Window.boguethen(Trigger.push_evente);(* : we relay the close event to the manually created window. *)letaction=defaultw.on_close(remove_windowboard)inactionw)|_asenum->printddebug_event"%s"(Trigger.window_event_nameenum);do_option(window_of_eventboarde)(funw->Window.to_refreshw;check_mouse_motionboard;(* Otherwise we don't get mouse_leave when the mouse leaves the
window. OK here ? *)(* Warning: the behaviour of SDL seems to be the following:
when the window has no focus and the user click on it,
there is NO Button_down NEITHER Button_up event, instead
there is a Window "Take_focus" event. We follow this
here. It means that the user has to click a second time to
activate a button.*))end;(* TODO just display the corresponding window, not all of them *)|`User_event->printd(debug_event+debug_board+debug_user)"User event";do_optionboard.on_user_event(funf->fe)|`Quit->printd(debug_event+debug_board)"Quit event";raiseExit|_->()endletfilter_drag_and_drop_eventboarde=matchboard.button_downwith|Someroom->ifLayout.draggableroomthendragboarderoom(*Layout.drag_n_drop e room*)elseSomee|None->printddebug_board"No board.button_down";Somee(* This happens for instance when you drag outside the SDL window and then
release mouse button. Still, the event will be treated by the original widget
below (in case of a keyboard_focus). *)(* The board can use the event that was filtered by the widget. Empty for the
moment. *)lettreat_remaining_events_boarde=matchTrigger.event_kindewith(* For instance | `Key_down when E.(get e keyboard_keycode) = Sdl.K.tab -> tab board e *)|_->()(* What to do with the original event before flip. This should not modify
widgets, only update board status. *)letfinal_eventsboardanime=matchTrigger.event_kindewith(* | `Mouse_motion when not anim && has_no_event ->
* if not board.mouse_alive then board.mouse_alive <- true;
* check_mouse_motion board; *)(* do_option (window_of_event board e) do_display; *)(* List.iter Window.to_refresh board.windows; *)(* TODO? display? *)(* = ou seulement ce qui a été (dé)sélectionné ? *)|`Mouse_motion->(* Even if there are several mouse_motion in the queue, it is important to
treat the ones that are followed by button_down (otherwise the focus is
not updated). Hence we don't flush them. In case of performance hit, one
could check Sdl.has_event E.mouse_motion && not (Sdl.has_event
E.mouse_button_down). *)printddebug_disable"MOTION anim=%b"anim;ifnotboard.mouse_alivethenboard.mouse_alive<-true;if(* has_no_event && *)not(Trigger.mm_pressede)thencheck_mouse_motionboard(* In most situations, when the button is pressed, we don't want to lose the
initial focus, and we don't want to activate anything else. There is one
(common) exception: when clicking a menu entry, we would like to navigate
menus while mouse button is down. How to handle this particular case? If we
remove the mm_pressed test it works nicely for menus, but it not usual for
other things (like scroll bar). I don't see any other solution than adding
a new flag 'allow_focus_change_when_mm_pressed' somewhere... TODO? *)|`Mouse_button_up|`Finger_up->board.button_down<-None;check_mouse_motionboard|`Window_event->()(* done above *)|`Bogue_redraw->(* Sometimes there are too many redraw events in the queue, this would cause
a noticeable delay if only one can be treated by iteration. Cf example
28/bis. Hence we leave at most one. Flushing all here is NOT recommended,
it can prevent the correct detection of new animations (ex: adding
sliding popups). *)(* do_option Trigger.(get_last redraw) (fun ev -> Trigger.push_event ev); *)ifnotanimthenbeginprintddebug_event"Redraw";do_option(window_of_eventboarde)Window.to_refreshend(* board.mouse_focus <- (check_mouse_focus board); *)(* ? *)(* could use window_of_event instead *)(* do_option board.mouse_focus Layout.set_focus *)(* ? *)(* | `Unknown when not !anim && E.(get e typ) = Trigger.refresh -> *)(* printd debug_event "Refresh"; (\* not used anymore *\) *)(* update board; *)(* board.mouse_focus <- (check_mouse_focus board); *)(* ? *)(* could use window_of_event instead *)(* do_option board.mouse_focus Layout.set_focus *)(* ? *)|`Bogue_mouse_at_rest->printddebug_event"Mouse AT REST";(* TODO *)|_->()letmax_events=128letevent_loopanimnew_animboard=(* let redraw_next = ref false in *)letrecloopecount=printddebug_event"Event loop %i"count;(* Note, do not flush var_changed, it is used by radiolist.ml, cf. example30
*)letevo_layout=filter_board_eventsboardeindo_optionevo_layout(treat_layout_eventsboard);(* Second, the event is treated by the global layout for drag-and-drop,
and we return the evo_widget for the widgets *)letevo_widget=check_optionevo_layout(filter_drag_and_drop_eventboard)in(* Now, the widget has the event (TODO sortir de la boucle ? ou dans une autre?) *)(* note that the widget will usually emit a redraw event *)do_optionevo_widget(funev->do_option(target_widgetboardev)(Widget.wake_up_allev));(* Now, the board again. *)do_optionevo_widget(treat_remaining_eventsboard);(* Finally we do final updates before flip with the original, unfiltered
event "e" *)(* TODO we should not use 'e' if there is an anim, because it will be an
old event *)final_eventsboardnew_anime;continuee(count+1)andcontinueecount=ifcount>max_eventsthenbeginprintd(debug_event+debug_error+debug_user)"Too many events accumulate. Maybe your system is too slow.";Sdl.flush_eventsE.first_eventE.last_eventendelseifSdl.poll_event(Somee)thenloopecountelseifanim||new_animthenprintddebug_event"Animation: we end the event loop, %i events processed."count(* : we exit [event_loop] *)elseifcount=0thenloop(Trigger.wait_event~action:Timeout.rune)count(* While we wait for events, we execute the Timeout Queue. *)elseprintddebug_event"No more events, %i events processed."count(* exit the loop (we want to wait only once) *)inlete=!Trigger.my_eventincontinuee0letstart_nop_event_fps,nop_event_fps=Time.make_fps()(* [one_step] is what is executed during the main loop *)letone_step?before_displayanim(start_fps,fps)?clearboard=let(_:Time.t)=Timeout.run()inletnew_anim=has_animboardinifnew_anim&¬animthenstart_fps();event_loopanimnew_animboard;do_optionbefore_displayrun;letanim=new_animin(* now some specifics in case of animation *)ifanimthenbegin(* if board.mouse_focus <> None then board.mouse_focus <- None; *)(* = we desactivate focus during animation?? *)printddebug_graphics" * Animation running...";(* Warning: Finally we chose this behaviour: mouse_enter/leave events are
sent only when the mouse really moves. If a widget hits the idle mouse
cursor because of an animation, these events are NOT sent. For instance
this can happen when scrolling a Select list. It is NOT ideal (what is?),
for instance when inserting new elements on the fly, like popups, one has
to tell the mouse to update its focus, even if it didn't move (otherwise
it will still connect to the widget that's below the popup...). For this
we have to push mouse_focus event.
TODO: ça affecte drad-and-drop, cf exemple5 à corriger. ça affecte aussi
example24 (cliquer sur un objet animé)
Read below for various trys... *)(* Comment this for Menu2 keyboard navigation... *)(* : even if the mouse doesn't actually move, some animated widget can
collide the mouse and become (un)selected. *)(* display board; *)List.iterWindow.to_refreshboard.windows;(* : we could select only the one which really has an animation *)(*fps 60*)end;(* else *)ifanimthenfps();(* even when there is no anim, we need to to be nice to other treads, in
particular when an event is triggered very rapidly (mouse_motion) and
captured by a connection, without anim. Should we put also here a FPS?? *)lett=Time.now()inflip?clearboard;(* This is where all rendering takes place. *)printddebug_graphics"==> Rendering took %u ms"(Time.now()-t);Avar.new_frame();(* This is used for updating animated variables. *)printddebug_graphics"---------- end of loop -----------";ifnotanimthenifLayout.is_freshboard.windows_housethennop_event_fps60elseThread.delay0.005;anim(* Create an SDL window for each top layout. *)(* One can use predefined windows, they will be used by the layouts in the
order they appear in the list. If there are fewer windows than layouts, new
windows are created. If there are more, the excess is disregarded. *)letmake_sdl_windows?windowsboard=matchwindowswith|None->List.iterWindow.make_sdl_windowboard.windows|Somelist->letrecloopsdlws=matchsdl,wswith|_,[]->()|[],rest->List.iterWindow.make_sdl_windowrest|s::srest,w::wrest->beginWindow.use_sdl_windowsw;loopsrestwrestendinlooplistboard.windows(* Create the board. *)letcreate?shortcuts?(connections=[])?on_user_eventwindows=(* let canvas = match layouts with *)(* | [] -> failwith "At least one layout is needed to make the board" *)(* | l::_ -> Layout.get_canvas l in *)(* if adjust then List.iter (Layout.adjust_window ~display:false) layouts; *)(* TODO add "adjust" property in layout. NO this should be enforced *)(* TODO one could use the position of the top layout to position the window *)letlayouts=List.mapWindow.get_layoutwindowsinletwindows_house=Layout.create_win_houselayoutsinletwidgets=(* List.flatten (List.map Layout.get_widgets layouts) *)Layout.get_widgetswindows_houseindo_option(repeatedWidget.equalwidgets)(funw->print_endline(Print.widgetw);failwith(Printf.sprintf"Widget is repeated: #%u"(Widget.idw)));List.iter(func->Widget.(add_connectionc.sourcec))connections;(* = ou bien dans "run" ? (ça modifie les widgets) *)letshortcuts=default_lazyshortcuts(lazy(Shortcut.create[]))inletshortcuts=(if!debugthenadd_debug_shortcutsshortcutselseshortcuts)|>Shortcut.add(Sdl.K.tab,tab)|>Shortcut.add_ctrl(Sdl.K.l,funboard->print_endline"User Redraw";displayboard;showboard)in{windows;windows_house;mouse_focus=None;keyboard_focus=None;button_down=None;shortcuts;shortcut_pressed=false;mouse_alive=false;on_user_event}letof_windows=create(* Create a board from layouts. Each layout in the list will be displayed in a
different window.*)letof_layouts?shortcuts?connections?on_user_eventlayouts=create?shortcuts?connections?on_user_event(List.mapWindow.createlayouts)letof_layout?shortcuts?connections?on_user_eventlayout=of_layouts?shortcuts?connections?on_user_event[layout](* for backward compatibility. Use [create], [of_windows] or [of_layouts]
instead. *)letmake?shortcutsconnectionslayouts=printd(debug_user+debug_warning)"Bogue.make is deprecated. Use Bogue.create, Bogue.of_layout, \
Bogue.of_layouts, or Bogue.of_windows instead.";of_layouts?shortcuts~connectionslayouts(** The main function that loops indefinitely *)(* one can insert code to be executed at two different places: "before_display"
means after Sync was executed and before Layout.display (except for manual
CTRL-L which would occur before it. "after_display" means just after all
textures have been calculated and rendered. Of course these two will not be
executed at all if there is no event to trigger display. *)letrun?before_display?after_displayboard=printddebug_board"==> Running board!";Trigger.flush_all();ifnot(Sync.is_empty())thenTrigger.push_action();ifnot(Update.is_empty())thenUpdate.push_all();Trigger.main_tread_id:=Thread.(id(self()));letstart,fps=Time.adaptive_fps60inmake_sdl_windowsboard;showboard;Thread.delay0.01;(* we need some delay for the initial Mouse position to be detected *)Sdl.pump_events();Sdl.stop_text_input();(* List.iter (Widget.set_canvas canvas) board.widgets; *)(* Warning: layouts may have different canvas because of different layers *)(* We have to display the board in order to detect mouse focus
(otherwise the 'show' field of layouts are not set). *)displayboard;board.mouse_focus<-check_mouse_focusboard;printddebug_board"Has focus: %s"(ifboard.mouse_focus=Nonethen"NO"else"YES");do_option(get_mouse_focusboard)(funl->Layout.set_focusl;(* we send mouse_enter event to the widget where the mouse is
positionned at startup *)(* Widget.wake_up_all Trigger.(create_event mouse_enter) (Layout.widget l);
* display board *)Trigger.push_mouse_enter(l.Layout.id));ifnot(Sync.execute50)(* The first Sync is given more time *)thenTrigger.flush(Trigger.sync_action);(* probably not useful *)flip~clear:trueboard;(* We send the startup_event to all widgets *)(* List.iter (fun l -> List.iter (Widget.wake_up ev) *)(* (List.flatten (List.map Widget.connections (Layout.get_widgets l)))) *)(* board.layouts; *)List.iter(Widget.wake_up(Trigger.startup_event()))(* TODOOOOO this event can be modified by a thread??!!! *)(List.flatten(List.mapWidget.connections(Layout.get_widgetsboard.windows_house)));Trigger.renew_my_event();start_nop_event_fps();Trigger.start_noevent_fps();letrecloopanim=letanim'=one_step?before_display~clear:trueanim(start,fps)boardindo_optionafter_display(funf->f());(* TODO? *)loopanim'intryloopfalsewith|Exit->exit_boardboard|e->letsdl_error=Sdl.get_error()inifsdl_error<>""thenprint_endline("SDL ERROR: "^sdl_error);print_endline(Print.layout_downboard.windows_house);raisee(*************)(* Shortcuts *)(*************)typeshortcut_action=boardShortcut.actionletshortcuts_empty():shortcuts=Shortcut.empty()letshortcuts_addmap?(keymod=Sdl.Kmod.none)keycodeaction:shortcuts=Shortcut.add_mapmap(keycode,keymod,action)letshortcuts_add_ctrlmapkeycodeaction:shortcuts=Shortcut.add_ctrl(keycode,action)mapletshortcuts_add_ctrl_shiftmapkeycodeaction:shortcuts=Shortcut.add_ctrl_shift(keycode,action)mapletshortcuts_of_listlist:shortcuts=Shortcut.createlist