123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492(*********************************************************************************)(* OCaml-Stk *)(* *)(* Copyright (C) 2023-2024 INRIA All rights reserved. *)(* Author: Maxence Guesdon, INRIA Saclay *)(* *)(* This program is free software; you can redistribute it and/or modify *)(* it under the terms of the GNU General Public License as *)(* published by the Free Software Foundation, version 3 of the License. *)(* *)(* This program is distributed in the hope that it will be useful, *)(* but WITHOUT ANY WARRANTY; without even the implied warranty of *)(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *)(* GNU General Public License for more details. *)(* *)(* You should have received a copy of the GNU General Public *)(* License along with this program; if not, write to the Free Software *)(* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA *)(* 02111-1307 USA *)(* *)(* As a special exception, you have permission to link this program *)(* with the OCaml compiler and distribute executables, as long as you *)(* follow the requirements of the GNU GPL in regard to all of the *)(* software in the executable aside from the OCaml compiler. *)(* *)(* Contact: Maxence.Guesdon@inria.fr *)(* *)(*********************************************************************************)(** Windows. *)openMiscopenTsdltype_Events.ev+=|Close:(unit->bool)Events.ev(* Close request for window; callback should return [true] to prevent closing. *)(**/**)letkeyboard_state_emptyst=letmoduleA=Bigarray.Array1inletempty=reftrueinfori=0toA.dimst-1doletv=A.getstiinifv<>0then([%debug"key %S is pressed"(Sdl.(get_key_name(get_key_from_scancodei)))];empty:=false)done;!empty(**/**)(** A window is a widget containing one widget and interfacing
with SDL window to render and handle events.
Windows should not be created directly but through
{!App.create_window}, {!App.create_scrolled_window} and
{!App.popup_menu} so that events are propagated to them
*)classwindow?classes?name?props?wdata?(flags=Sdl.Window.(opengl))?(rflags=Sdl.Renderer.software)?resizable?x?y?w?htitle=letrflags=Tsdl.Sdl.Renderer.(rflags+targettexture)inletflags=matchresizablewith|None->flags|Sometrue->Sdl.Window.(flags+resizable)|Somefalse->Sdl.Window.(flags-resizable)inlet(autosize_w,autosize_h,w,h)=ifSdl.Window.(testflagsresizable)then(false,false,Option.value~default:1w,Option.value~default:1h)elseletautosize_w,w=matchwwithNone->(true,1)|Somew->(false,w)inletautosize_h,h=matchhwithNone->(true,1)|Someh->(false,h)in(autosize_w,autosize_h,w,h)inletwin=matchSdl.create_windowtitle?x?y~w~hflagswith|Error(`Msgmsg)->Misc.sdl_errormsg|Okwin->wininletrenderer=matchSdl.(create_renderer~flags:rflagswin)with|Error(`Msgmsg)->Misc.sdl_errormsg|Okr->rinlet()=matchSdl.set_render_draw_blend_moderendererSdl.Blend.mode_blendwith|Error(`Msgmsg)->Misc.sdl_errormsg|Okx->xinletwinid=Sdl.get_window_idwininletrender_mutex=Lwt_mutex.create()inletwith_renderer_lockf=Lwt_mutex.with_lockrender_mutex(fun()->frenderer)inobject(self)inheritBin.bin?classes?name?props?wdata()assuper(** The SDL window. *)methodwindow=win(** The SDL window id. *)methodwindow_id=winid(** Set window title. *)methodset_titlestr=Sdl.set_window_titlewinstr(**/**)methodkind="window"method!top_window=Somewinvalmutablecursor=Sdl.get_cursor()method!cursor=matchcursorwith|None->Log.warn(funm->m"%s#cursor: None"self#me);None|Somec->Somecmethod!to_desktop_coords~x~y=let(wx,wy)=Sdl.get_window_positionwinin(wx+x,wy+y)methodapply_theme=super#apply_theme;self#need_resize(**/**)(** Returns SDL window position as [(x,y)]. *)methodposition=let(x,y)=Sdl.get_window_positionwinin[%debug"%s#position = (%d,%d)"self#mexy];(x,y)(** Returns the SDL window size. *)methodsize=Sdl.get_window_sizewin(**/**)methodrenderer=renderervalmutableto_render=(Layer.Map.empty:G.tLayer.Map.t)(**/**)(** [w#resize ~w ~h] resize the window with width [w] and height [h]. *)methodresize~w~h=self#update_geometry~w~h()(**/**)valmutableis_resizing=falsevalmutableneed_resize_after=falsemethodneed_resize=ifis_resizingthenneed_resize_after<-trueelse(super#need_resize;self#update_geometry())methodprivateupdate_geometry=let(w0,h0)=self#sizeinfun?(w=w0)?(h=h0)()->ifis_resizingthen()else(is_resizing<-true;need_resize_after<-false;letw=ifautosize_wthenself#min_widthelsewinleth=ifautosize_hthenself#min_heightelsehinif(w0<>w||h0<>h)thenSdl.set_window_sizeself#window~w~h;[%debug"%s#update_geometry => w:%d=>%d, h:%d=>%d"self#mew0wh0h];self#set_geometry{gwithw;h};is_resizing<-false;ifneed_resize_afterthen(need_resize_after<-false;self#update_geometry()))valmutableon_close=(fun()->())(**/**)(** Set callback on closing. *)methodset_on_closef=on_close<-f(** Closes the window (destroying it). *)methodclose=(tryon_close()withe->Log.err(funm->m"%s#close: %s"self#me(Printexc.to_stringe)));self#destroy(**/**)method!get_focus=[%debug"%s#get_focus return Some(%b)"self#me(self#get_pProps.has_focus)];(* release previous focus, but do not cal self#release_focus, since
this would update our has_focus props; start with child.
If focus is not released, then return None. *)matchmatchchildwith|None->true|Somew->w#release_focuswith|true->Some(self#get_pProps.has_focus)|false->Nonemethod!grab_focus?last()=Sdl.raise_windowself#window;(* In case the window did not get the focus, force it.
It seems to happen when a window already had focus before,
typically for dialog windows not destroyed but just hidden. *)matchSdl.get_keyboard_focus()with|SomewwhenSdl.get_window_idw=winid->true|_->matchSdl.set_window_input_focusself#windowwith|Error(`Msgmsg)->Log.err(funm->m"%s: %s"self#memsg);false|Ok()->true(*super#grab_focus ?last ()*)(* Give focus to the first focusable widget, using grab_focus on
child. *)method!child_focus_next(w:Widget.widget)=w#grab_focus()method!child_focus_prev(w:Widget.widget)=w#grab_focus~last:true()valmutablelast_key_event=Nonemethodon_window_event(e:Sdl.event)=letmust_render=matchSdl.Event.(window_event_enum(getewindow_event_id))with|`Close->(letkeep=self#trigger_eventClose()in[%debug"%s: `Close event => keep = %b"self#mekeep];matchkeepwith|true->true|false->self#close;false)|`Enter->(* when entering a window with a button pressed on another one and
not released, the `Enter event occurs only when releasing the button,
and mouse_state coords seem to refer to previous window;
we use global state and convert to our window coordinates *)let(_,(x,y))=Sdl.get_global_mouse_state()inlet(wx,wy)=Sdl.get_window_positionwininletpos=(x-wx,y-wy)inlet_=self#on_sdl_event_down~oldpos:None(Somepos)einfalse|`Exposed->true|`Focus_gained->let_=(* if a key is already pressed, set last_key_event to
`Keyboard_state to block key press events which occured
when another window had the focus. Indeed, since the
key may still be pressed, the window gaining the focus will receive
key press events, which is not what we want. So we will block
key press events when the keyboard_state is the same as when we
got the focus, except is there is no key pressed.*)letstate=Sdl.get_keyboard_state()inifnot(keyboard_state_emptystate)thenlast_key_event<-Some(`Keyboard_statestate);self#set_pProps.has_focustrue;matchself#set_has_focustruewith|true->true(* has_focus was set on a widget *)|false->(* no widget already has the focus previously, let's use
grab_focus to give to the first one *)super#grab_focus()in[%debug"%s: focus gained %a"self#meWidget.pp_widget_treeself#wtree];false|`Focus_lost->[%debug"%s: focus lost"self#me];self#set_pProps.has_focusfalse;let_=self#set_has_focusfalseinfalse|`Hidden|`Hit_test|`Leave->let_=self#on_sdl_event_down~oldpos:NoneNoneeinfalse|`Maximized|`Minimized->self#update_geometry();true|`Moved->false|`Resized->self#update_geometry();true|`Restored|`Shown->let_=self#on_sdl_event_down~oldpos:NoneNoneeinfalse|`Size_changed->self#update_geometry();true|`Take_focus|`Unknown_->falseinifmust_renderthen(self#need_render~layer:Layer.Baseg;)methodon_root_event~oldposposev=letev_type=Sdl.Event.(enum(getevtyp))in(*Log.warn (fun m -> m "%s#on_root_event last_key_event=%s"
self#me (match last_key_event with
| None -> "NONE"
| Some `Text_editing -> "`Text_editing"
| Some `Text_input -> "`Text_input"
| Some `Handled_key_press -> "`Handled_key_press"));*)matchev_type,last_key_eventwith|`Text_input,Some`Handled_key_press->(*Log.warn (fun m -> m "text_input event blocked");*)false|`Key_down,Some`Keyboard_statestwhenst=Sdl.get_keyboard_state()->[%debug"key press blocked according to keyboard state"];false|`Key_down,Some`Text_editing->(*Log.warn (fun m -> m "key_down event blocked");*)false|_->lethandled=self#on_sdl_event_down~oldposposevin(* block next text input event if key_down was handled, unblock
when a key_down event was not handled *)matchev_typewith|`Key_down->let()=matchhandled,last_key_eventwith|false,Some_->last_key_event<-None;|false,None->()|true,_whenSdl.is_text_input_active()->last_key_event<-Some`Handled_key_press|true,Some_->last_key_event<-None|true,None->()inhandled|`Key_up->last_key_event<-None;handled|`Text_input->last_key_event<-Some`Text_input;handled|`Text_editingwhenSdl.Event.(getevtext_editing_length)=0->(* to pump spurious Text_editing events when window gets the focus *)handled|`Text_editing->last_key_event<-Some`Text_editing;handled|_->handledmethod!render_child~layerrend~offset~g_none~g_child=trysuper#render_child~layerrend~offset~g_none~g_childwithe->Log.err(funm->m"%s#render_child: %s %s"self#me(Printexc.to_stringe)(Printexc.get_backtrace()));methodrender_window?layers()=[%debug"%s#render"self#me];try(matchSdl.get_renderer_inforendererwith|Error(`Msgmsg)->Log.err(funm->m"self#me: renderer_info: %s"msg)|_->());let>()=Sdl.set_render_targetrendererNoneinletgeom=let(w,h)=self#sizeinletg={G.x=0;y=0;w;h}in[%debug"%s#render g=%a"self#meG.ppg];matchlayerswith|None->[%debug"%s#render_window reset-painting %a"self#meG.ppg];Render.fill_rectrendererNoneself#bg_color_now;self#render_child~layer:Layer.Baserenderer~offset:(0,0)~g_none:g~g_child:g;Someg|Somemap->Layer.Map.fold(funlayerg_to_renderacc->[%debug"%s#render layer=%a g_to_render=%a"self#meLayer.pplayerG.ppg];matchG.intergg_to_renderwith|None->None|Someg->iflayer=Layer.Basethen([%debug"%s#render_window reset-painting %a"self#meG.ppg];Render.fill_rectrenderer(Someg)self#bg_color_now);self#render_child~layerrenderer~offset:(0,0)~g_none:g~g_child:g;matchaccwith|None->Someg|Someacc->Some(G.uniongacc))mapNonein(*let> () = Sdl.set_render_target renderer None in
let> () = Sdl.render_set_clip_rect renderer None in*)matchgeomwith|None->[%debug"%s#render: nothing to render"self#me]|Somegeom->[%debug"%s render_copy and present %a"self#meG.ppgeom];ifnotself#sensitivethenself#render_insensitiverenderer~offset:(0,0)geom;Sdl.render_presentrendererwith|e->Log.err(funm->m"%s#render_window: %s"self#me(Printexc.to_stringe))methodrender~layerrenderer~offsetg=[%debug"%s#render should not be called"self#me]methodrender_if_needed=ifnot(Layer.Map.is_emptyto_render)then(letlayers=to_renderinto_render<-Layer.Map.empty;self#render_window~layers())method!destroy=(trysuper#destroywithe->Log.err(funm->m"%s#destroy: %s"self#me(Printexc.to_stringe)));Sdl.destroy_windowwinmethodadd_to_render~layerg=[%debug"%s#add_to_render ~layer:%a %a"self#meLayer.pplayerG.ppg];(matchlayerwith|Layer.Base->self#add_to_render~layer:Layer.Menug|Layer.Menu->());matchLayer.Map.find_optlayerto_renderwith|None->ifg.w=0||g.h=0then()elseto_render<-Layer.Map.addlayergto_render|Somegr->letres=G.unionggrin[%debug"%s#add_to_render ~layer:%a => %a"self#meLayer.pplayerG.ppres];to_render<-Layer.Map.addlayerresto_rendermethodneed_render~layergeom=matchself#need_renderinggeomwith|None->()|Somegeom->self#add_to_render~layergeom(* method child_need_render ~layer geom =
[%debug "%s#child_need_render ~layer:%a on %a"
self#me Layer.pp layer G.pp geom);
*)(**/**)(** Show SDL window.*)methodshow=Sdl.show_windowwin(** Hide SDL window. *)methodhide=Sdl.hide_windowwin(** [w#move ~x ~y] moves SDL window to [(x,y)]. *)methodmove~x~y=[%debug"%s#move ~x:%d ~y:%d"self#mexy];Sdl.set_window_positionwin~x~yinitializer(* window will not be parented, so we must apply theme at creation *)self#apply_theme;with_renderer<-Somewith_renderer_lock;self#update_geometry();(* we must pump events and move again the window or else
something goes wrong with sdl and window manager, window
is not placed where asked. *)matchx,ywith|Somex,Somey->Tsdl.Sdl.pump_events();self#move~x~y|_->()end(**/**)letwindow?classes?name?props?wdata?flags?rflags?resizable?(show=true)?x?y?w?htitle=letw=newwindow?classes?name?props?wdata?flags?rflags?resizable?x?y?w?htitleinifnotshowthenw#hide;w