123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919(* Layout is the main object type. *)(* a layout is a 'box' which can contain 'sub-boxes'. We use the terminology of
houses: a house contains several rooms. Each room can be viewed as a house
which contains other rooms etc. Thus, this is a simple graph with variable
degree. A leaf (a room which does not contain subrooms) is called a resident;
it contains a Widget. In the whole (connected) tree, the summit is the main
layout: the only one which does not belong to any house; it is called the
top_house, and corresponds to a "physical" SDL window. The size of the SDL
window should always match the size of the top_house. *)(* Warning: a widget should *not* appear twice (or more) inside a
Layout. Otherwise, the results are not going to be satisfactory: a widget is
associated to a geometry in a layout. Instead one should use two differents
widgets with a connection between them to synchronize the data *)openTsdlopenB_utilsmoduleWidget=B_widgetmoduleAvar=B_avarmoduleChain=B_chainmoduleTheme=B_thememoduleTime=B_timemoduleVar=B_varmoduleTvar=B_tvarmoduleTrigger=B_triggermoduleSync=B_syncmoduleDraw=B_drawmoduleMouse=B_mousemoduleStyle=B_stylemoduleBox=B_boxmoduleSlider=B_slidermoduleSelection=B_selectiontypebackground=(* TODO instead we should keep track of how the box was created... in case we
want to recreate (eg. use it for another window... ?) *)(* TODO use Style.background ? Cependant Style est antérieur (et utilisé par)
à Box... *)|StyleofStyle.t|BoxofBox.tletcolor_bgcolor=Style(Style.(of_bg(color_bgcolor)))letopaque_bgcolor=color_bgDraw.(opaquecolor)letbg_color=opaque_bg@@Draw.find_colorTheme.bg_colorletstyle_bgs=Stylesletbox_bgb=Boxbtypeadjust=|Fit|Width|Height|Nothingtypetransform={angle:floatAvar.t;center:(Sdl.pointoption)Avar.t;flip:Sdl.flipAvar.t;alpha:floatAvar.t}typegeometry={x:intAvar.t;y:intAvar.t;(* The (x,y) coords define the position of the layout wrt its container (the
house). Origin is top-left. *)w:intAvar.t;h:intAvar.t;voffset:(intAvar.t)Var.t;(* The [voffset] is the vertical offset = the y value of where the content of
the layout will be drawn. It is typically used for scrolling. It is similar
to the 'y' variable', except that:
1. the clipping rect (if defined) is *not* translated in case of voffset
2. the background is not translated either *)transform:transform;}typecurrent_geom={x:int;y:int;w:int;h:int;voffset:int;}(* convert between same type in Draw... *)letto_draw_geom(g:current_geom)={Draw.x=g.x;Draw.y=g.y;Draw.w=g.w;Draw.h=g.h;Draw.voffset=g.voffset}typeroom_content=|Roomsofroomlist(* In principle, rooms in a house with the same layer should have
non-intersecting geometries, otherwise it is not clear which one gets the
mouse focus (this can be violated, eg. with Layout.superpose). Popups are
drawn on a different layer *)|ResidentofWidget.tandroom={id:int;(* unique identifier *)name:stringoption;(* If needed for debugging, one can give a name to the room. *)lock:Mutex.t;(* Lock for concurrent access to mutable fields. Anything that modifies the
layout should lock; this way, we may write functions acting on a layout
without risking the layout being modified in the middle of the
function. Instead, we could use Var.t to encapsulate the layout (or all
the mutable fields, if we think that blocking one field should not block
the others), but maybe that would be heavier *)mutablethread_id:int;adjust:adjust;(* should we adjust the size of this room to fit its content ? *)(* not implemented yet *)mutableresize:((int*int)->unit);(* The [resize] function is called when the house changed size. (int * int)
is the house size (w,h). *)mutableshow:bool;(* should we show this room ? *)mutablehidden:bool;(* The [hidden] field is only useful when [t.show = true]. Then [t.hidden =
true] if the layout is currently not displayed onscreen. (Upon creation,
all layouts are hidden.) Only used to correctly detect if animations are
running. This field is only set by the Layout.display function, it should
not be modified by user. Note that t.show has precedence for being
hidden: it [t.show = false], then t is hidden no matter what [t.hidden]
says. *)mutablegeometry:geometry;(* [geometry] contains the relative geometry of the room wrt the house. All
components are dynamic variables, that need to be recomputed at each
iteration. Note: rooms inside a house must be physically inside the
geometry of the house. If not, they will not be detected by the mouse,
for instance. *)mutablecurrent_geom:current_geom;(* [current_geom] is the current *absolute* geometry. Is updated at each
display. But because of clip, the actual rendered size can be smaller
than indicated size. Before the start of the main loop, it is equal to
the initial values of the geometry field *)(* a special case of current_geom.(x,y) is to specify window position for
the top layouts. See set_window_pos *)mutableclip:bool;(* If [clip]=true, the room (and its children) will be clipped inside its
geometry. This should be set whenever one want to scroll the content of
the layout inside the layout. This is also used (and set) by hide/show
animations. TODO replace this by a more flexible 'overflow' specification
*)mutablebackground:backgroundoption;mutableshadow:Style.shadowoption;mask:Sdl.surfaceoption;(* If there is a mask, a position (x,y) will be declared inside the layout
if it corresponds to a mask pixel with alpha value <> 0. A mask will act
as a clip if it is uniformly white, and the shape is given by nonzero
alpha values. (TODO) *)mutablecontent:room_content;mutablelayer:Draw.layer;(* [layer] is the particular layer = chain element of this layout. It should
never be an empty layer (Chain.None), except for the special layout that
contains all windows. If a room contains other Rooms, its layer should be
at least as deep as the layers of the Rooms, otherwise the "background"
might end-up not being at the background... *)(* in principle a chain of layers is attached to a window. When creating a
new window, one has to select a new layer chain (use_new_layer) *)mutablecanvas:Draw.canvasoption;(* The canvas contains the "hardware" information to render the room *)(* The canvas is not really an intrinsic property of the layout, it is used
only when rendering is required. It may change "without notice" when a
layout is copied into another window *)mutablehouse:roomoption;(* [house] = parent: this is the "room" that contains this room in his
"Rooms". This field is mutable because of cyclic definition: one cannot
set the house before defining it... It is our responsibility to make sure
that the house really corresponds to the parent element, in order to
avoid cycles etc. *)(* cache : Sdlvideo.surface; *)(* ou texture ? mettre un cache pour
accélerer l'affichage, plutôt que
d'effacer tout à chaque itération ? *)mutablemouse_focus:bool;(* set interactively when has mouse focus *)mutablekeyboard_focus:booloption;(* None = cannot have focus; Some b = has focus or not *)(* TODO: should we move the keyboard_focus to the Widget ? A layout which
contains a Rooms list cannot really have keyboard_focus...and in fact it
will not be detected by 'next_keyboard' *)(* TODO : mutable draggable : int option; *)(* None = not draggable; Some
delay = drag after delay (in ms) *)mutabledraggable:bool;(* TODO keep_focus_on_pressed: bool (default = true) CF. menu2. BUT It's not
so easy because many layouts can cover a widget. Ideally, this property
should belong to the widget. *)}typet=room(* The whole connected component of a layout is a tree, whose vertices (nodes)
are rooms and leaves are widgets (=Resident). The number of branches (=Rooms
list) from a vertex is arbitrary. The house field gives the parent of a
vertex.
There are several interesting ways of going through a tree:
- through every vertex
- only through leaves
- only leaves at a common level (=same generation number)
- nearest neighbour (left, right, up, or down) in the planar embedding
*)(* We use words "room", "layout", and "house" for the same type of object.
- "layout" will in general refer to the main house, ie containing everything
that is displayed on the window.
- "house" in general refers to a parent of some room, ie an object contaning
sub-rooms.
- "room" is the generic term for sub objects contained in the general layout.
*)exceptionFatal_errorof(t*string)(* [not_specified] is a special value used to indicate that the window position
should be guessed by the program. TODO don't use this nasty trick. *)letnot_specified=-66666letno_clip=reffalse(* The normal behaviour when a non-zero voffset is specified is to clip the
layout to the original rectangle. This permits the show/hide
animation. Setting [no_clip = true] can be a good idea for debugging
graphics. *)letdraw_boxes=Widget.draw_boxes(* this is only used for debugging. This can slow down rendering quite a bit *)letequalr1r2=r1.id=r2.idlet(==)=equalletsprint_idr=Printf.sprintf"#%u%s"r.id(matchr.namewith|None->""|Somes->Printf.sprintf" (%s)"s)moduleHash=structtypet=roomletequal=equallethashroom=room.idendmoduleWHash=Weak.Make(Hash)(* [rooms_wtable] this is a weak set of all created rooms, searchable by their
unique id. It is weak in the sense that rooms can be reclaimed by the GC when
not anymore in use, and automatically disappear from the set. *)letrooms_wtable=WHash.create50(* [cemetery] is only for debugging: we insert here the room ids we think are
not used anymore. Then we can check if the GC did remove them from the
[rooms_wtable]. *)letcemetery=ref[]letsend_to_cemeteryroom=cemetery:=room.id::!cemetery(* TODO: use GC.finalise to automatically unload (but not destroy) textures from
GCed layouts ? *)(* (or maybe better for GCed widgets) *)letrecremove_wtableroom=ifWHash.memrooms_wtableroomthenbeginprintddebug_memory"Removing room %s from Wtable"(sprint_idroom);WHash.removerooms_wtableroom;ifWHash.memrooms_wtableroomthenbeginprintddebug_error"Several instances of room %s are registered in the weak hash table."(sprint_idroom);remove_wtableroom;(* The hash can host several instances of the room. However this signals
a bug somewhere. *)end;send_to_cemeteryroom;endletclear_wtable()=WHash.clearrooms_wtable(* let rooms_table : (int, room) Hashtbl.t = Hashtbl.create 50;;*)(* this is where we store the reverse lookup: room.id ==> room *)(* of course the problem is to free this when rooms are not used anymore... to
prevent a memory leak. *)(* TODO use weak tables (or Ephemerons ???) *)(* https://caml.inria.fr/pub/docs/manual-ocaml/libref/Weak.html *)(* let of_id id = *)(* try Hashtbl.find rooms_table id with *)(* | Not_found -> failwith (Printf.sprintf "Cannot find room with id=%d" id);; *)(* Pressing the TAB key in the main loop will switch the keyboard focus to
another room. Here we save the room that had keyboard focus just before
pressing TAB. This global variable should be thread safe because it is
modified only by the main loop. Another option could be to store the room_id
in an event. (?) *)letkeyboard_focus_before_tab:toptionref=refNoneletfresh_id=fresh_int()(** make geometry *)letgeometry?(x=0)?(y=0)?(w=0)?(h=0)?(voffset=0)?transform():geometry={x=Avar.varx;y=Avar.vary;w=Avar.varw;h=Avar.varh;voffset=Var.create(Avar.varvoffset);transform=defaulttransform{angle=Avar.var0.;center=Avar.varNone;flip=Avar.varSdl.Flip.none;alpha=Avar.var1.}}(** list of all integer dynamical variables *)letget_int_avarsroom=letg=room.geometryin[g.x;g.y;g.w;g.h;Var.getg.voffset]letcurrent_geom?(x=0)?(y=0)?(w=0)?(h=0)?(voffset=0)():current_geom={x;y;w;h;voffset}(* transform geometry into current_geom *)(* lock the layout ? *)letto_current_geom(g:geometry):current_geom={x=Avar.getg.x;y=Avar.getg.y;w=Avar.getg.w;h=Avar.getg.h;voffset=Avar.get(Var.getg.voffset)}(* get current layer of layout *)letget_layerl=l.layer(* [base_layer rooms] returns the deepest layer of the list of rooms, or the
current layer if the list is empty. *)letbase_layer=function|[]->Draw.get_current_layer()|room::rooms->List.fold_leftChain.min(get_layerroom)(List.mapget_layerrooms)(* Create a new room. Rather use the [create] function below. *)letcreate_unsafe?name?(set_house=true)?(adjust=Fit)?(resize=fun_->())?layer?mask?background?shadow?house?keyboard_focus?(mouse_focus=false)?(show=true)?(clip=false)?(draggable=false)?canvasgeometrycontent=letid=fresh_id()inletlayer=matchlayerwith|Somelayer->layer|None->matchcontentwith|Roomsrooms->base_layerrooms|Resident_->Draw.get_current_layer()inletroom={id;name;lock=Mutex.create();thread_id=Thread.(id(self()));show;hidden=true;adjust;resize;geometry;current_geom=to_current_geomgeometry;clip;mask;background;(* = (Some (Solid Draw.(opaque blue))); (* DEBUG *) *)shadow;content;layer;house;keyboard_focus;mouse_focus;canvas;draggable}in(* we update the lookup table: *)(* remove is in principle not necessary *)if!debugthenifWHash.memrooms_wtableroomthen(printddebug_error"A room with same id was already in the table !";remove_wtableroom);WHash.addrooms_wtableroom;(* we update the resident room_id field *)(* we update the content's house field *)let()=matchcontentwith|Residentw->w.Widget.room_id<-Someid|Roomslist->ifset_housethenList.iter(funr->r.house<-Someroom)listin(* Gc.finalise free room;*)(* We don't do this because who knows when the background texture will be
destroyed.... maybe too late (after renderer was destroyed). TODO: what to
do to make sure the background is destroyed if the layout is not used
anymore (and we don't destroy the renderer) ? Only solution I see is to add
the renderer information to the texture... *)printddebug_board"Layout %s created."(sprint_idroom);roomlethas_residentlayout=matchlayout.contentwith|Resident_->true|Rooms_->false(* The public [create] version. *)letcreate=create_unsafe~set_house:true(* the dummy room is only used to search the Weak table *)letdummy_room=create~name:"dummy"(geometry())(Rooms[])letof_id_unsafeid:room=tryWHash.findrooms_wtable{dummy_roomwithid}with|Not_found->(printddebug_warning"Cannot find room with id=%d"id;raiseNot_found)(* A detached room is a layout that does not belong to the current layout tree.
Checking house = None is not sufficient, as it is allowed to have a unique
top layout containing a resident. Note, we could also add a 'detached' field
in the layout type. *)letis_detachedroom=room.house=None&¬(has_residentroom)(* This one is more secure: we check if the layout is not detached. *)letof_id_opt?not_foundid:roomoption=match(WHash.find_optrooms_wtable{dummy_roomwithid})with|None->(printddebug_error"Cannot find room with id=%d"id;do_optionnot_foundrun;None)|Someraso->ifis_detachedrthen(printddebug_error"Trying to access the detached room #%d"id;None)elseo(* WARNING: in "iter" and in all other search functions below, recall that
itering though a room is tricky because of mutability and threading. The
structure of the tree can be changed by another thread while we iter. Most
dangerous: it can also be changed by the itering itself, hehe. If necessary,
doing "iter lock room" should minimize the risk (but not 100%: the tree can
still be modified while we are locking..) *)(* iter through all the rooms (layouts & widgets) contained in the [room],
including the initial [room] itself. *)(* top to bottom *)letreciterfroom=froom;matchroom.contentwith|Resident_->()|Roomslist->List.iter(iterf)list(* iter through widgets *)letreciter_widgetsfroom=matchroom.contentwith|Residentw->fw|Roomslist->List.iter(iter_widgetsf)listletmap_widgetsfroom=letlist=ref[]initer_widgets(funw->list:=(fw)::!list)room;!list(* iter the direct children *)letiter_roomsfhouse=matchhouse.contentwith|Resident_->printd(debug_error+debug_board)"Layout %s has no rooms: cannot iter."(sprint_idhouse)|Roomslist->List.iterflist(* find the room containing a widget given by the wid (or None if the room has
disappeared in the air)*)letof_widwid=letw=Widget.of_idwidinletid=Widget.get_room_idwinof_id_optid(* returns the list of rooms of the layout, or Not_found if there is a
resident *)letget_roomslayout=matchlayout.contentwith|Resident_->printddebug_error"This layout %s is a leaf, not a node: \
it does not contain a list of rooms"(sprint_idlayout);raiseNot_found|Roomslist->listletsiblingsroom=matchroom.housewith|None->printddebug_error"Cannot get siblings of room %s because it does not belong to any \
house."(sprint_idroom);[]|Somehouse->get_roomshouse(* return the resident widget, or Not_found *)letwidgetlayout=matchlayout.contentwith|Rooms_->printddebug_error"This room %s is a node, not a leaf: \
it does not contain a resident widget"(sprint_idlayout);raiseNot_found(* or, return the first available widget with next_widget ? *)|Residentw->wletget_resident=widget(* return the first resident widget with show=true inside the layout, or
Not_found *)letrecfirst_show_widgetlayout=iflayout.showthenmatchlayout.contentwith|Residentw->(printddebug_board"first_show_widget selects %u"(Widget.idw);w)|Roomsrooms->letrecloop=function|[]->raiseNot_found|r::rest->tryfirst_show_widgetrwithNot_found->looprestinlooproomselseraiseNot_found(* Return the list of all texts contained in the widgets *)letget_textsroom=map_widgetsWidget.get_textroom|>List.filter(funs->s<>"")letget_textroom=get_residentroom|>Widget.get_textletset_textroomtext=Widget.set_text(get_residentroom)text(* only for debugging: *)(* check if rooms sent to cemetery have effectively been removed by GC *)letcheck_cemetery()=letcheckid=tryletr=of_id_unsafeidinprintddebug_memory"Dead room %s seems to be living. Beware of zombies."(sprint_idr);falsewith|Not_found->printddebug_memory"Dead room #%u was correctly burried by the GC. RIP."id;trueinletreclooplistnewlistempty=(* easier to use a Queue *)matchlistwith|[]->empty,newlist|id::rest->ifcheckidthenlooprestnewlistemptyelselooprest(id::newlist)falseinletempty,newlist=loop!cemetery[]trueincemetery:=newlist;empty(* lock the layout to make it available only by the locking thread. Hence two
consecutive locks by the same thread will not block. TODO mutualize with Var?
Probably better to use protect_fn anyways. *)letlockl=ifMutex.try_lockl.lockthenbeginletid=Thread.(id(self()))inprintddebug_thread"Locking room %s for thread #%i."(sprint_idl)id;l.thread_id<-idendelse(* then it was already locked *)ifThread.(id(self()))<>l.thread_id(* not same thread, we must wait *)thenbeginletid=Thread.(id(self()))inprintddebug_thread"Waiting for thread #%i to remove lock for room %s"id(sprint_idl);Mutex.lockl.lock;l.thread_id<-idendelseprintddebug_thread"Layout %s was locked, but by the same thread: we continue."(sprint_idl)letunlockl=printddebug_thread"Unlocking layout %s"(sprint_idl);ifMutex.try_lockl.lockthenprintddebug_thread" (but layout %s was already unlocked)."(sprint_idl);Mutex.unlockl.lock(* get the renderer of the layout *)letrenderert=matcht.canvaswith|Somec->c.Draw.renderer|_->failwith"Cannot get renderer because no canvas was defined"(* get the Sdl window of the layout *)letwindowt=matcht.canvaswith|Somec->c.Draw.window|_->beginprintddebug_error"Cannot get window for layout %s because no canvas was defined"(sprint_idt);raiseNot_foundend(* return the top-level layout *)(* This is relevent only once the main loop has started. Before this, the
top_house is not even created, so it will not return what you expect. *)letrectop_houselayout=matchlayout.housewith|None->layout|Somer->top_houserletis_toplayout=layout.house=Noneletget_houselayout=layout.houseletget_contentlayout=layout.contentletget_canvasl=matchl.canvaswith|Somec->c|None->raise(Fatal_error(l,Printf.sprintf"The room #%d is not associated with any canvas"l.id))(* test if layouts share the same layer (= same depth) *)letsame_layerl1l2=Chain.(get_layerl1==get_layerl2)letsame_stackl1l2=Chain.(same_stack(get_layerl1)(get_layerl2))(* get the layout background *)letget_backgroundl=l.background(* if !debug is true, we replace the background by solid red *)letdelete_backgroundroom=printddebug_memory"Delete background for room %s"(sprint_idroom);do_optionroom.background(funb->let()=room.background<-if!debugthenSome(opaque_bgDraw.red)elseNoneinmatchbwith|Styles->Style.unloads|Boxb->Box.unloadb)(* this can be used to force recreating the background, for instance after
changing the size of the room *)letunload_backgroundroom=do_optionroom.background(function|Boxb->Box.unloadb|Styles->Style.unloads)(* maybe not necessary *)(* force compute background at current size. Canvas must be created *)letcompute_background?(mustlock=true)room=do_optionroom.background(funbg->letg=room.current_geominSdl.log"COMPUTE BG w=%u h=%u"g.wg.h;letbox=matchbgwith|Stylestyle->letb=Box.(create~width:g.w~height:g.h~style())inifmustlockthenlockroom;room.background<-(Some(Boxb));ifmustlockthenunlockroom;b|Boxb->Box.unloadb;binignore(Box.display(get_canvasroom)(get_layerroom)box(Draw.scale_geom(to_draw_geomg))))(* change background *)(* can be called by a thread *)(* TODO it should not be allowed to use a background of type Box in case the box
already belongs to another room... *)letset_backgroundlb=lockl;unload_backgroundl;l.background<-b;unlocklletset_shadowls=lockl;l.shadow<-s;unlockl(** get size of layout *)letget_sizel=l.current_geom.w,l.current_geom.hletget_physical_sizel=get_sizel|>Draw.scale_size(** get width of layout *)letwidthl=l.current_geom.w(** get height *)letheightl=l.current_geom.hletresizeroom=do_option(get_houseroom)(funhouse->room.resize(get_sizehouse))letdisable_resizeroom=room.resize<-(fun_->())letfix_contenthouse=iter_roomsdisable_resizehouseletresize_contentroom=matchroom.contentwith|Roomslist->List.iterresizelist|Residentw->Widget.resizew(get_sizeroom)(* l must be the top house *)letadjust_window_sizel=ifnot(is_topl)thenprintddebug_error"[adjust_window_size] should only be called with a top house, what %s \
is not."(sprint_idl)elseifl.canvas<>Nonethenletw,h=get_physical_sizelinletwin=windowlinif(w,h)<>Draw.get_window_sizewinthenbeginDraw.set_window_sizewin~w~h;Trigger.(push_event(create_eventE.window_event_resized))endelseprintddebug_graphics"Window for layout %s already has the required size."(sprint_idl)(* Change the size of the room. By default this will cancel the resize function
of this room. If [set_size] or its derivatives [set_width] and [set_height]
are used as part of a layout resize function of the same room, this default
behaviour should be disabled to prevent the resize function to cancel itself:
use [keep_resize:true]. *)(* TODO faire un module Resize avec keep_resize=true par défaut. *)letset_size?(keep_resize=false)?(check_window=true)?(update_bg=false)?w?hl=lockl;let()=matchw,hwith|Somew,Someh->l.current_geom<-{l.current_geomwithw;h};Avar.setl.geometry.hh;Avar.setl.geometry.ww|Somew,None->l.current_geom<-{l.current_geomwithw};Avar.setl.geometry.ww|None,Someh->l.current_geom<-{l.current_geomwithh};Avar.setl.geometry.hh|None,None->()inifupdate_bg&&l.canvas<>Nonethencompute_background~mustlock:falsel;(* = ou plutot unload_background ?? *)ifnotkeep_resizethendisable_resizel;ifcheck_window&&is_toplthenadjust_window_sizel;unlockl;resize_contentlletset_height?keep_resize?check_window?update_bglh=set_size?keep_resize?check_window?update_bg~hlletset_width?keep_resize?check_window?update_bglw=set_size?keep_resize?check_window?update_bg~wl(* The public version of [set_size] *)letset_size?keep_resize?check_window?update_bgl(w,h)=set_size?keep_resize?check_window?update_bg~w~hl(** get voffset *)letget_voffsetl=(* l.current_geom.voffset;; *)Avar.get(Var.getl.geometry.voffset)(** get current absolute x position (relative to the top-left corner of the
window). Not necessarily up-to-date. *)letxposl=l.current_geom.x(** get current absolute y position *)letyposl=l.current_geom.y(* left absolute coordinate of the layout's house *)letx_originl=matchl.housewith|None->0|Someh->xposh(* top absolute coordinate of the layout's house *)lety_originl=matchl.housewith|None->0|Someh->yposh(* position of room relative to house *)letpos_fromhouseroom=xposroom-xposhouse,yposroom-yposhouse(** get current x value. *)(* WARNING don't use this inside an animation for x ! It will loop
forever. Instead use Avar.old l.geometry.x *)letgetxl=Avar.getl.geometry.xletget_oldxl=Avar.oldl.geometry.x(** get current y value *)letgetyl=Avar.getl.geometry.yletget_oldyl=Avar.oldl.geometry.y(** change x of layout, without adjusting parent house. Warning, by default this
disables the resize function. *)(* this is the x coordinate wrt the containing house *)(* this won't work if there is an animation running (see Avar.set) *)letsetx?(keep_resize=false)lx=lockl;letx0=getxlinl.current_geom<-{l.current_geomwithx=l.current_geom.x+x-x0};Avar.setl.geometry.xx;ifnotkeep_resizethendisable_resizel;(* TODO à vérifier, cf dans "flat" et "tower" *)unlockl(** change y of layout, without adjusting parent house *)(* see above *)letsety?(keep_resize=false)ly=lockl;lety0=get_oldylinl.current_geom<-{l.current_geomwithy=l.current_geom.y+y-y0};Avar.setl.geometry.yy;ifnotkeep_resizethendisable_resizel;unlockl(* see above *)(* warning, it the animation is not finished, using Avar.set has almost no
effect *)letset_voffsetlvo=lockl;Avar.set(Var.getl.geometry.voffset)vo;l.current_geom<-{l.current_geomwithvoffset=vo};unlockl(* use this to shift the voffset by a constant amount without stopping an
animation *)letshift_voffset_genericvsetldv=letav=Var.getl.geometry.voffsetinifAvar.finishedavthenset_voffsetl(Avar.getav+dv)elseletav_new=Avar.apply(funy->y+dv)avin(* let _ = Avar.get av_new in *)(* in order to start the animation. Useful ?? *)vsetl.geometry.voffsetav_newletshift_voffset=shift_voffset_genericVar.set(* not used... *)letreset_posl=lockl;letw,h=get_sizelinletg=geometry~w~h()in(* or modify l.geometry fields in-place ? *)l.geometry<-g;l.current_geom<-to_current_geomg;unlockl(* a special use of current_geom is to indicate the desired window position
within the desktop at startup. It should be set *after* Bogue.make and
*before* Bogue.run *)letget_window_poslayout=letfx=ifx=not_specifiedthenNoneelseSomexinflayout.current_geom.x,flayout.current_geom.y(* see get_window_pos. It should be set *after* Bogue.make and *before*
Bogue.run. Otherwise it has possibly no effect, or perhaps causes some
glitches. TODO make a test to ensure this ?? *)letset_window_poslayout(x,y)=letg=layout.current_geominlayout.current_geom<-{gwithx;y}(* lock l ? *)letget_transforml=lett=l.geometry.transforminletangle=Avar.gett.angleinletcenter=Avar.gett.centerinletflip=Avar.gett.flipinletalpha=Avar.gett.alphainDraw.make_transform~angle?center~flip~alpha()letget_alphal=Avar.getl.geometry.transform.alphaletdraggablel=l.draggable(* we don't lock because it will be modified only by the main loop *)letset_draggablel=l.draggable<-trueletset_clipl=lockl;l.clip<-true;unlocklletunset_clipl=lockl;l.clip<-false;unlocklletset_showlb=lockl;l.show<-b;unlocklletrecrec_set_showbl=lockl;l.show<-b;let()=matchl.contentwith|Resident_->()|Roomslist->List.iter(rec_set_showb)listinunlockl(** return absolute (x,y) position *)(* TODO optimize: test if x is up_to_date, then one can use current_geom instead ? *)(* of course this test will fail for hidden rooms *)letcompute_posroom=letrecloopx0y0r=letx,y=x0+(Avar.getr.geometry.x),y0+(Avar.getr.geometry.y)+(Avar.get(Var.getr.geometry.voffset))inmatchr.housewith|None->x,y|Someh->loopxyhinloop00room(** get absolute position of the parent house *)lethouse_posroom=matchroom.housewith|None->0,0|Someh->compute_posh;;(* not used, just to fix the vocabulary "leaf" *)letis_leafroom=matchroom.contentwith|Resident_|Rooms[]->true|_->false(* return the first resident *below (or including) room* for which test w =
true, or None *)letrecfind_residenttestroom=matchroom.contentwith|Residentw->iftestwthenSomeroomelseNone|Roomslist->letrecloop=function|[]->None|r::rest->letf=find_residenttestriniff=Nonethenlooprestelsefinlooplist(* search through the whole component of the layout (children and parents)
starting from top house containing room *)exceptionFoundoftletsearchroomscan=ifscanroomthenSomeroom(* =just in case it might speed-up things: room is the "initial guess" *)elseletfr=ifscanrthenraise(Foundr)intryiterf(top_houseroom);raiseNot_foundwith|Foundr->printddebug_warning"Search OK";Somer|Not_found->printddebug_error"Search produced no result!";None|e->raisee(** find room by id in the connected component of house *)(* cf Layout.of_id *)letfind_room_oldhouseid=printddebug_warning"Search room #%d in %d..."id(house.id);letscanr=r.id=idinsearchhousescan(* let set_canvas canvas room = *)(* iter (fun r -> r.canvas <- canvas);; *)(* find the next room in the same level of the house. In circular mode, after
the last one comes the first. In non circular mode, if room is the last one,
we return None. If [only_visible] is true, we skip all rooms that have
[.show=false] and rooms that belong to a hidden house. *)letnext?(circular=false)?(only_visible=true)room=matchroom.housewith|None->(* we must be in top_house *)None|Somehwhenonly_visible&¬h.show->(* h is hidden *)None|Someh->letrooms=get_roomshin(* It should not be empty since room is inside. *)letfirst=List.hdroomsinletreclooplistfound=matchlistwith|[]->iffoundthenifcircularthenSomefirstelseNoneelseNone(* nothing was found, so the [room] itself was hidden. *)|a::rest->iffound&&(notonly_visible||a.show)thenSomeaelselooprest(found||equalaroom)inlooproomsfalse(* find the "first" (and deepest) room (leaf) contained in the layout by going
deep and choosing always the first room of a house *)(* WARNING a room with empty content is considered a leaf too *)letrecfirst_roomr=printddebug_board"Descending to room %s"(sprint_idr);matchr.contentwith|Resident_->r|Rooms[]->r|Rooms(a::_)->first_rooma(* find a 'uncle': a room next to some parent room, going up in generation. *)letrecnext_upr=check_optionr.house(funh->matchnexthwith|None->next_uph|o->o)(* find the next leaf (=room containing a widget, or empty) in the whole layout
(which should be the connected component of [top]. If [room] does not belong
to this compoment -- which can happen after mutation -- we return the first
leaf of [top]). *)(* we first look at the same level, then below, then upstairs. *)(* repeated calls to this function will visit the whole connected component --
although this is not the optimal way to visit everything, of course -- and
start over indefinitely. Thus you should check when the returned room is the
one you started with... (which means you should start with a leaf !) *)letnext_leaf~toproom=ifnot(top_houseroom==top)thenbeginprintd(debug_board+debug_warning+debug_custom)"Room %s does not belong the the top house %s. We select the first room \
of the top house."(sprint_idroom)(sprint_idtop);first_roomtopendelsematchroom.contentwith|Rooms[]|Resident_->beginmatchnextroomwith|None->(* last one at this level; we go upstairs *)leth=matchnext_uproomwith|Somer->r|None->printddebug_board"No next widget was found; we start again from top";topinfirst_roomh|Somen->(matchn.contentwith|Resident_->n|Rooms_->first_roomn)end|Rooms_->first_roomroom(** find the next visible room with a widget that can have keyboard_focus *)(* TODO check example25 *)letnext_keyboard~toproom=letreclooprvisited=ifList.memr.idvisited(* this happens sometimes (in case of mutation) *)then(printddebug_custom"Room %s already visited"(sprint_idroom);None)elseletn=next_leaf~toprinifequalroomnthen(printd(debug_board+debug_custom)"No keyboard_focus found";None)elseifn.keyboard_focus<>None&&n.showthen(printd(debug_board+debug_custom)"Found %s"(sprint_idn);Somen)elseloopn(r.id::visited)inlooproom[](********************)(* use this to reset all widget textures (room + all children) for reducing
memory. The layout can still be used without any impact, the textures will be
recreated on the fly. If you want to really remove all created textures, you
have to use delete_backgrounds too; but then the backgrounds will *not* be
recreated. *)letunload_widget_texturesroom=unload_backgroundroom;iter_widgetsWidget.unload_textureroom(* same, but for all rooms + widgets *)letunload_texturesroom=letfr=unload_backgroundr;matchr.contentwith|Residentw->Widget.unload_texturew|_->()initerfroomletdelete_backgroundsroom=iterdelete_backgroundroomletdelete_texturesroom=unload_texturesroom;delete_backgroundsroomletremove_canvasroom=delete_texturesroom;iter(funr->r.canvas<-None)room(* What to do when a layout is not used anymore ? *)(* First check that it is detached from its house. *)(* If it has children, they will become orphans *)(* (because a room should NOT belong to several different houses). *)(* The resulting orphans are not freed. They are still available to use in a new
room (see eg. longlist.ml). Use kill_all if you want to recursively free all
subrooms *)(* mask and background are not freed because nothing prevents them from being
shared with another object (which is maybe not a good idea...) We leave this
to the GC... (?) *)(* TODO: use GC.finalise ? *)(* WARNING: be careful it's quite easy to forget that something else points to
the layout... or its children. This is easily the case for instance with
Bogue.board fields like windows_house, mouse_focus, keyboard_focus,
button_down... Not to mention widgets, which refer indirectly to layouts via
their id... So it's preferable never to use this... *)(* not used yet *)(* When to call this ? *)(* in particular, when this function is called, the layout l in principle has
already been removed from rooms_wtable *)letfreel=printddebug_memory"Freeing Layout %s"(sprint_idl);unload_backgroundl;beginmatchl.contentwith|Resident_->()|Roomslist->list_iterlist(funr->do_optionr.house(funh->ifequalhlthenprintddebug_warning"Room %s is now orphan"(sprint_idr)))end(* kill functions below are quite dangerous, beware *)(* use this when the layout + all children is not used anymore *)(* In fact don't use this, use kill_rooms instead. Because very often a layout
is created with subrooms that don't all necessarily have a name. Thus, if you
want to kill a layout, you may forget that its direct house has no name, so
it will likely stay in the table for ever. It's difficult for the user to
keep track of this. One could use ocaml's Ephemeron instead ?*)(* note that rooms we be reclaimed though their id, for instance via of_wid, or
even more devily stored in an event... At this point it DOES cause some fatal
errors that I don't know how to locate... *)letkill_all_NOroom=matchroom.housewith|Someh->printddebug_error"Cannot kill layout #%u because it still \
belongs to a house #%u"room.idh.id;|None->(* we defer it to the main loop *)Sync.push(fun()->delete_backgroundsroom;letrecloopr=remove_wtabler;matchr.contentwith|Residentw->Widget.freew|Roomslist->List.iterlooplistinlooproom)(* kill all rooms (and theirs subrooms) of this house *)(* defered to the main loop *)(* don't use this. See WARNING in "kill" above *)letkill_rooms_NOhouse=matchhouse.contentwith|Resident_->printddebug_error"House #%u does not have rooms to kill..."house.id|Roomslist->Sync.push(fun()->letrecloopr=delete_backgroundr;remove_wtabler;matchr.contentwith|Residentw->Widget.freew|Roomslist->List.iterlooplistinList.iterlooplist)(**********)(* Use this to shift all current_geometries before inserting a room inside a
house. This can be needed because inserting will trigger fit_content which
uses current_geom *)letglobal_translateroomdxdy=do_optionroom.house(funh->printddebug_warning"You are translating the current_geom of room #%u which already has a \
house #%u. This is likely to have no effect, as the current_geom is \
automatically updated at each display"room.idh.id);iter(funr->r.current_geom<-{r.current_geomwithx=r.current_geom.x+dx;y=r.current_geom.y+dy})room(* adjust layout size to the inner content in the same layer (but not to the
larger layouts, neither to the window) *)(* TODO: treat margins *)(* not used yet... *)letrecfit_content?(sep=Theme.room_margin/2)l=ifl.adjust=Nothing||l.clipthen()elseletw,h=matchl.contentwith|Residentwidget->Widget.default_sizewidget(* | Rooms [r] -> r.geometry.w, r.geometry.h *)|Roomslist->letx0=l.current_geom.xinlety0=l.current_geom.yin(List.fold_left(funmr->ifsame_layerrlthenimaxm(r.current_geom.x-x0+r.current_geom.w)elsem)0list,List.fold_left(funmr->ifsame_layerrlthenimaxm(r.current_geom.y-y0+r.current_geom.h)elsem)0list)inletg'=matchl.adjustwith|Fit->{l.current_geomwithw=w+sep;h=h+sep};|Width->{l.current_geomwithw=w+sep};|Height->{l.current_geomwithh=h+sep};|Nothing->failwith"already treated case !"inletoldg=l.current_geominifg'<>oldgthenbeginprintddebug_graphics"ADJUST %s to New SIZE %d,%d"(sprint_idl)wh;set_sizel(g'.w,g'.h);do_optionl.housefit_content(* we adjust the parent (???) *)end(** return the list of widgets used inside the layout *)letrecget_widgetslayout=matchlayout.contentwith|Roomsh->List.flatten(List.mapget_widgetsh)|Residentw->[w]lethas_keyboard_focusr=r.keyboard_focus=Sometrue(** set keyboard_focus if possible *)(* we don't lock because it will be modified only by the main loop *)letset_keyboard_focusr=matchr.keyboard_focuswith|Someb->ifnotbthenbeginprintddebug_board"Setting keyboard_focus to room %s"(sprint_idr);r.keyboard_focus<-Sometrue;matchr.contentwith|Rooms_->()|Residentw->Widget.set_keyboard_focuswend|None->printddebug_board"Cannot set keyboard_focus to room %s because if was not created \
with keyboard_focus capability."(sprint_idr)letrecremove_keyboard_focusr=do_optionr.keyboard_focus(funb->ifbthenr.keyboard_focus<-Somefalse);matchr.contentwith|Roomslist->List.iterremove_keyboard_focuslist|Residentw->Widget.remove_keyboard_focuswletclaim_focusr=ifhas_residentrthenTrigger.push_mouse_focusr.idelseprintd(debug_error+debug_board)"Cannot claim focus on room %s without resident."(sprint_idr)letclaim_keyboard_focusr=ifhas_residentrthenTrigger.push_keyboard_focusr.idelseprintd(debug_error+debug_board)"Cannot claim keyboard_focus on room %s without resident."(sprint_idr)(* Emit the close-window event to the window containing the layout *)letpush_closer=letid=Sdl.get_window_id(windowr)inletopenTriggerinlete=create_eventE.window_eventinE.(setewindow_window_idid);E.(setewindow_event_idwindow_event_close);push_evente(* center vertically the rooms of the layout (first generation only) *)letv_centerlayouty0h=matchlayout.contentwith|Resident_->()|Roomsrs->list_iterrs(funr->leth0=heightrinlety=Draw.centery0hh0insetyry)(** vertical align *)(* v_center is the same as v_align ~align:Draw.Center *)letv_align~alignlayouty0h=matchlayout.contentwith|Resident_->()|Roomsrs->list_iterrs(funr->leth0=heightrinlety=Draw.alignaligny0hh0insetyry)(** create a room (=layout) with a unique resident (=widget), in the current
layer (unless specified). No margin possible. *)(* x and y should be 0 if the room is the main layout *)(* warning, the widget is always centered *)(* x,y specification will be overwritten if the room is then included in a flat
or tower, which is essentially always the case... *)letresident?name?(x=0)?(y=0)?w?h?background?draggable?canvas?layer?keyboard_focuswidget=let(w',h')=Widget.default_sizewidgetinletw=defaultww'inleth=defaulthh'inletkeyboard_focus=matchkeyboard_focuswith|Sometrue->Somefalse|Somefalse->None|None->Widget.guess_unset_keyboard_focuswidgetinletgeometry=geometry~x~y~w~h()increate?name?background?keyboard_focus?draggable?layer?canvasgeometry(Residentwidget)letof_widget=resident(* Set the given widget as the new resident of the given room. If w,h are not
specified, the size of the room will be updated by the size of the widget. *)letchange_resident?w?hroomwidget=matchroom.contentwith|Residentresid->printddebug_board"Replacing room %s's widget by widget #%d"(sprint_idroom)(Widget.idwidget);let(w',h')=Widget.default_sizewidgetinletw=defaultww'inleth=defaulthh'inroom.content<-Residentwidget;widget.Widget.room_id<-Someroom.id;room.keyboard_focus<-Widget.guess_unset_keyboard_focuswidget;resid.Widget.room_id<-None;set_sizeroom(w,h)|_->printddebug_event"[change_resident]: but target room has no resident!"(* An empty layout can reserve some space without stealing focus (and has no
keyboard_focus) *)(* WARNING TODO in the search functions, we have assumed rooms where never
empty... *)letempty?name?background~w~h()=letgeometry=geometry~w~h()increate?name?backgroundgeometry(Rooms[])(* Simple resize function that scales the room with respect to the given
original house size (w,h) *)letscale_resize?(scale_width=true)?(scale_height=true)?(scale_x=true)?(scale_y=true)(w,h)r=letx=xposrinlety=yposrinletrw,rh=get_sizerinletkeep_resize=trueinletresize(hw,hh)=letscalexz=z*hw/winletscaleyz=z*hh/hinifscale_xthensetx~keep_resizer(scalexx);ifscale_ythensety~keep_resizer(scaleyy);ifscale_heightthenset_voffsetr(scaley(get_voffsetr));ifscale_height&&scale_widththenset_size~keep_resizer(scalexrw,scaleyrh)elseifscale_heightthenset_height~keep_resizer(scaleyrh)elseifscale_widththenset_width~keep_resizer(scalexrw)inr.resize<-resize(* convenience function for scaling all rooms by the same factor -- to be use for rooms in the same house *)letscale_resize_list?scale_width?scale_height?scale_x?scale_y(w,h)rooms=List.iter(scale_resize?scale_width?scale_height?scale_x?scale_y(w,h))rooms(* Overrides scale_resize to retreive the house size automatically. Can only be
applied if the room is already in a house. *)letscale_resize?scale_width?scale_height?scale_x?scale_yroom=matchroom.housewith|None->printddebug_error"Cannot compute the resize function of room %s since it does not \
belong to a house"(sprint_idroom)|Someh->lets=get_sizehinscale_resize?scale_width?scale_height?scale_x?scale_ysroomletauto_scalehouse=let(w,h)=get_sizehouseinmatchhouse.contentwith|Roomsrooms->scale_resize_list(w,h)rooms|Resident_->printddebug_warning"TODO: auto_scale resident"letresize_follow_houseroom=room.resize<-(funsize->set_size~keep_resize:trueroomsize)(* Not very smart, but [resize_fix_x/y] is currently used by Space. Another
possibility would be to have two resize functions: horizontally and
vertically *)letresize_fix_xroom=letf=room.resizeinletkeep_resize=trueinroom.resize<-(funsize->letx,w=getxroom,widthroominfsize;setx~keep_resizeroomx;set_width~keep_resizeroomw)letresize_fix_yroom=letf=room.resizeinletkeep_resize=trueinroom.resize<-(funsize->lety,h=getyroom,heightroominfsize;sety~keep_resizeroomy;set_height~keep_resizeroomh)(* sets l with the size of the top_house. In principle the (x,y) of the
top_house should be (0,0), we don't check this here. The (x,y) of l is set to
(0,0). Should be called dynamically after main loop starts. *)letmaximizel=lockl;setxl0;setyl0;letw,h=get_size(top_housel)inl.current_geom<-{l.current_geomwithh;w};Avar.setl.geometry.hh;Avar.setl.geometry.ww;scale_resize_list(w,h)[l];unlockl;resize_contentl(* check if a sublayer is deeper (= below = Chain.<) than the main layer, which
(in principle) should not happen *)letcheck_layersroom=letrecloophouser=matchr.contentwith|Resident_->ifChain.(get_layerhouse>.get_layerr)thenprintddebug_error"The house #%d contains a room #%d with deeper layer! (%d>%d)"house.idr.id(Chain.depth(get_layerhouse))(Chain.depth(get_layerr));|Roomsh->List.iter(loopr)hinlooproomroom(** Set the canvas of the layout. Warning! we assume that if a room has a
canvas, all smaller rooms already have the same canvas... *)(* warning: this is also used when we change the layer, but the window stays the
same *)(* let rec set_canvas canvas room = *)(* if Draw.canvas_equal room.canvas canvas *)(* then () *)(* else begin *)(* printd debug_warning "Changing room canvas"; *)(* room.canvas <- canvas; *)(* match room.content with *)(* | Rooms list -> List.iter (set_canvas canvas) list *)(* | Resident _ -> () *)(* end;; *)letset_canvascanvasroom=lockroom;room.canvas<-Somecanvas;if!debugthencheck_layersroom;unlockroom(** Set the canvas for layout and all children *)letglobal_set_canvas?(mustlock=true)roomcanvas=ifmustlockthenlockroom;iter(funr->r.canvas<-Somecanvas)room;ifmustlockthenunlockroomletcheck_layer_errorroomhouse=ifnot(Chain.same_stackroom.layerhouse.layer)thenprintddebug_error"The replacement room %s belongs to a separate set of layers disjoint \
from the house %s (or one of them has empty layer). Beware that it \
will probably never be displayed"(sprint_idroom)(sprint_idhouse)(* Move the room layer into the stack of the dst layer (this actually makes a
copy of the layer into a dst stack, without destroying the initial layer,
which may be shared by other rooms). Warning: this does not check the blit
contents of the layers; it's supposed to be done before the blits are
computed (when all layers should contain empty queues). The new copy get an
empty queue anyways.*)letmove_to_stack~dstroom=ifnot(same_stackroomdst)thenbeginletdst_layer=get_layerdstinprintddebug_board"Moving layer of %s into that of %s (stack:%u)"(sprint_idroom)(sprint_iddst)(Chain.get_stack_iddst_layer);room.layer<-Chain.copy_into~dst:dst_layer(get_layerroom);Chain.replaceroom.layer(Draw.new_layer())end(* Move the room layer and the layers of all inhabitants to the dst layer *)letmove_all_to_stack~dstroom=iter(move_to_stack~dst)room(* move all layers contained in [room] into the same stack as the [room]
layer. *)letunify_layer_stackroom=move_all_to_stack~dst:roomroomletset_new_stackwin=ifis_topwinthenbeginprintddebug_board"Creating a new stack for window layout %s."(sprint_idwin);win.layer<-Chain.copy_into~dst:None(get_layerwin);Chain.replacewin.layer(Draw.new_layer())endelseprintd(debug_board+debug_error)"Creating a new stack is only allowed for top layouts (windows), not \
for %s"(sprint_idwin)letmove_to_new_stackroom=set_new_stackroom;unify_layer_stackroom(* specialized [create] version for creating the list of all windows (= top
layouts) *)letcreate_win_housewindows=(* We make sure each window's layer belong to a different stack. (If not, we
create new stacks.) *)letreclooplayer_ids=function|[]->()|win::rest->letid=Chain.get_stack_id(get_layerwin)inletid=ifList.memidlayer_idsthenbeginset_new_stackwin;Chain.get_stack_id(get_layerwin)endelseidinunify_layer_stackwin;loop(id::layer_ids)restinloop[]windows;letlayer=Noneincreate_unsafe~set_house:false~name:"windows_house"~layer(geometry())(Roomswindows)(* use this only if you know what you are doing... *)(* remember that a room with no house will be considered a "top layout" *)(* see WARNING of the "kill" fn. If the detached room is still pointed to by the
board (eg. mouse_focus...=> the mouse will not find what you expect) *)(* TODO lock ? *)(* not used *)letdetach_roomslayout=matchlayout.contentwith|Resident_->printddebug_error"No rooms to detach from layout %s"(sprint_idlayout)|Roomsrooms->list_iterrooms(funr->ifr.house<>Nonethen(r.house<-None;printddebug_warning"Room %s was detached from House %s..."(sprint_idr)(sprint_idlayout)))(* Detach a room from its house. See detach_rooms *)letdetachroom=lockroom;let()=matchroom.housewith|None->printddebug_error"Cannot detach because room %s has no house"(sprint_idroom)|Someh->lockh;room.house<-None;letrooms=List.filter(funr->not(r==room))(get_roomsh)inh.content<-Roomsrooms;printddebug_warning"Room %s was detached from House %s."(sprint_idroom)(sprint_idh);unlockhinunlockroom(* Sets the required fields for [room] to be a room of [dst], but does not
install it within the rooms list. This has to be done separately. See for
instance [add_room] or [replace_room]. *)letattach~dstroom=room.house<-Somedst;move_to_stack~dstroom;(* if there is a canvas in layout, we copy it to all rooms *)do_optiondst.canvas(global_set_canvas~mustlock:falseroom)(* Check if [room] can be added to the content of [dst]. If [already = true] we
accept to add a room in a house that already contains it... (this is used
when we want to re-order rooms). If [loop_error = false] it doesn't raise an
error when room = dst, we just return false. *)letok_to_add_room?(already=false)?(loop_error=true)~dstroom=ifequalroomdstthenbeginprintddebug_error"Cannot add room %s to itself!"(sprint_idroom);ifloop_errortheninvalid_arg"add_room"elsefalse(* equivalent to (not ((not loop_error) || raise ...)) *)endelsematchroom.housewith|Somehwhenequalhdst->printd((ifalreadythendebug_warningelsedebug_error)+debug_board)"Room %s already belongs to %s."(sprint_idroom)(sprint_iddst);already|Someh->printd(debug_error+debug_board)"Room %s should not be added to %s because it belongs to another house \
(%s). We do it anyway."(sprint_idroom)(sprint_iddst)(sprint_idh);true|None->true(* Modify the layout content by setting new rooms *)(* Old ones are *not* freed, but they are *detached* from house *)(* Note that setting rooms that are already there is legal (can be used to
change the order). Then they are not detached, of course. *)(* With sync=false, this is highly non thread safe. Locking layout is not enough
(or, we should lock all layouts in the main loop too... Therefore, it is
better to set sync=true, which delays the execution to Sync (the main loop
Queue) *)(* mutualize with [add_room, replace_room] ?*)letset_roomslayout?(sync=true)rooms=matchlayout.contentwith|Resident_->printddebug_error"Cannot transform a leaf (Resident #%u) to a node (Rooms) because the \
resident widget would be lost"layout.id|Roomsold_rooms->(ifsyncthenSync.pushelserun)(fun()->list_iterrooms(funr->ifok_to_add_room~already:true~dst:layoutrthenbeginif!debug&&memequalrold_roomsthenprintd(debug_board+debug_warning)"Trying to insert a room (%s) that is already there (%s). We \
leave it there, no problem."(sprint_idr)(sprint_idlayout)elseattach~dst:layoutrend);(* Now we rescan the list to detach unused rooms. This could have been
done in the first iteration, but there is a [if !debug] there,
so... *)list_iterold_rooms(funr->ifnot(memequalrrooms)thendetachr);(* detach_rooms layout; *)(* we don't detach because some orphans may
want to survive longer than you
think... see WARNING in 'kill' *)layout.content<-Roomsrooms;(*fit_content layout*))(* Hum. the adjust should NOT be done at this point, because display didn't
happen yet, hence the current_geometry is not updated. Morover there is no
way to know the 'sep' optional argument *)(* TODO the example/ls example should be reviewed then ... *)(* like set_rooms but in addition the old ones are killed *)letreplace_rooms_NOlayoutrooms=kill_rooms_NOlayout;set_roomslayoutrooms(* copy the 'relocatable content' of src into dst. Of course, this should be
avoided when writing in functional style, but can be handy sometimes *)(* Warning: size will change, and this is not transmitted to the parent house *)(* Warning: the old content is not freed from memory *)(* TODO: move everything to Sync (not only set_rooms) ? *)letcopy~src~dst=locksrc;lockdst;letdx=dst.current_geom.x-src.current_geom.xinletdy=dst.current_geom.y-src.current_geom.yinglobal_translatesrcdxdy;dst.geometry<-{dst.geometrywithw=src.geometry.w;h=src.geometry.h};letw,h=get_sizesrcindst.current_geom<-{dst.current_geomwithw;h};dst.clip<-src.clip;dst.background<-src.background;beginmatchsrc.contentwith|Residentrasc->r.Widget.room_id<-Somedst.id;dst.content<-c|Roomsrooms->set_roomsdstroomsend;dst.keyboard_focus<-src.keyboard_focus;dst.draggable<-src.draggable;unlocksrc;unlockdst(* Add a room to the dst layout content (END of the list). This does *not*
enlarge the containing house. The resize function of the room is cancelled. *)(* This is used to add a pop-up *)(* warning: the room should NOT already belong to some house. *)(* TODO: write a "remove_room" function *)letadd_room?valign?halign~dstroom=ifok_to_add_room~dstroomthenbegincheck_layer_errorroomdst;letrooms=get_roomsdstin(* we cannot add room to layout which already contains a Resident *)lockdst;lockroom;(* We now check oversize. But this should not happen. The user should not
rely on this. If the added room is too large, beware that nothing outside
of the geometry of the destination room will never have mouse focus
(mouse focus is detected per house, and THEN into the children rooms. *)letwmax=(getxroom)+(widthroom)inifwmax>widthdstthen(printddebug_error"The attached Room #%u is too wide"room.id;(*set_width dst wmax*));lethmax=(getyroom)+(heightroom)inifhmax>heightdstthen(printddebug_error"The attached Room #%u is too tall"room.id;(*set_height dst hmax*));letx=default_lazy(map_optionhalign(funa->Draw.aligna0(widthdst)(widthroom)))(lazy(getxroom))inlety=default_lazy(map_optionvalign(funa->Draw.aligna0(heightdst)(heightroom)))(lazy(getyroom))insetxroomx;setyroomy;attach~dstroom;dst.content<-Rooms(List.rev(room::(List.revrooms)));(* fit_content dst;; *)unlockdst;unlockroomendletset_layer?(debug=!debug)roomlayer=lockroom;room.layer<-layer;unlockroom;ifdebugthencheck_layersroom(* TODO: do some "move layer" or translate layer instead *)letglobal_set_layerroomlayer=iter(funr->set_layer~debug:falserlayer)room(** construct a horizontal house from a list of rooms *)(* sep = horizontal space between two rooms *)(* hmargin = horizontal margin (left and right). *)(* vmargin = vertical margin (top and bottom). *)(* if margins is set, then sep, hmargin and vmargin are all set to this value *)(* WARNING: resulting layout has position (0,0). *)letflat?name?(sep=Theme.room_margin/2)?(adjust=Fit)?(hmargin=Theme.room_margin)?(vmargin=Theme.room_margin)?margins?align?background?shadow?canvas?(scale_content=true)rooms=(* List.iter (set_canvas canvas) rooms; *)letsep,hmargin,vmargin=matchmarginswith|Somem->m,m,m|None->sep,hmargin,vmargininletreclooplistxy=matchlistwith|[]->(x-sep+hmargin,y)|r::rest->setxrx;setyrvmargin;looprest(x+sep+(widthr))(maxy((heightr)+2*vmargin))inletw,h=looproomshmarginvmargininletlayout=create?name?background?shadow(geometry~w~h())~adjust(Roomsrooms)?canvasindo_optionalign(funalign->v_align~alignlayoutvmargin(h-2*vmargin));(* Now that the geometry is finalized, we may compute the resize function for
each room: *)ifscale_contentthenscale_resize_list(w,h)roomselseList.iterdisable_resizerooms;layoutlethbox=flat(* Construct a flat directly from a list of widgets that we convert to
Residents. By default it uses smaller margins than [flat]. *)letflat_of_w?name?(sep=Theme.room_margin)?h?align?background?widget_bg?canvas?scale_contentwidgets=letrooms=List.map(funwg->letname=map_optionname(funs->"Resident of ["^s^"]")inresident?name?h~x:0~y:sep?background:widget_bg?canvaswg)widgetsinflat?name~margins:sep?align?background?canvas?scale_contentroomsleth_centerlayoutx0w=matchlayout.contentwith|Resident_->()|Roomsrs->list_iterrs(funr->letw0=widthrinletx=Draw.centerx0ww0insetxrx)leth_align~alignlayoutx0w=matchlayout.contentwith|Resident_->()|Roomsrs->list_iterrs(funr->letw0=widthrinletx=Draw.alignalignx0ww0insetxrx)(* create a vertical layout ("tower") from a list of widgets *)lettower_of_w_old?name?(sep=Theme.room_margin)?w?align(* ?(adjust = Fit) *)?background?widget_bg?canvaswidgets=letreclooplistrooms(x,y)=matchlistwith|[]->rooms,(x,y)|wg::rest->letroom=resident?w~x:sep~y?background:widget_bg?canvaswginletw,h=get_sizeroominlooprest(room::rooms)(maxx(w+2*sep),y+sep+h)inletrooms,(w,h)=loopwidgets[](sep,sep)in(* let background = Solid (Draw.(lighter (opaque grey))) in *)letlayout=create(geometry~w~h())?name?background(Rooms(List.revrooms))?canvasindo_optionalign(funalign->h_align~alignlayoutsep(w-2*sep));scale_resize_list(w,h)rooms;layout(** create a tower from a list of rooms *)(* sep = vertical space between two rooms *)(* hmargin = horizontal margin (left and right). *)(* vmargin = vertical margin (top and bottom). *)lettower?name?(sep=Theme.room_margin/2)?margins?(hmargin=Theme.room_margin)?(vmargin=Theme.room_margin)?align?(adjust=Fit)?background?shadow?canvas?clip?(scale_content=true)rooms=(* List.iter (set_canvas canvas) rooms; TODO *)letsep,hmargin,vmargin=matchmarginswith|Somem->m,m,m|None->sep,hmargin,vmargininletreclooplistxy=matchlistwith|[]->(x,y-sep+vmargin)|r::rest->setxrhmargin;setyry;looprest(maxx((widthr)+2*hmargin))(y+sep+(heightr))inletw,h=looproomshmarginvmargininletlayout=create~adjust?name?background?shadow?clip(geometry~w~h())(Roomsrooms)?canvasindo_optionalign(funalign->h_align~alignlayouthmargin(w-2*hmargin));ifclip=None&&scale_contentthenscale_resize_list(w,h)roomselsefix_contentlayout;(* TODO ce n'est pas la peine de scaler la largeur si elle ne dépasse pas le
layout. Voir par exemple la demo/demo *)layout(* Construct a tower directly from a list of widgets that we convert to
Residents. *)lettower_of_w?name?(sep=Theme.room_margin)?w?align?background?widget_bg?canvas?scale_contentwidgets=letrooms=List.map(funwg->letname=map_optionname(funs->"Resident of ["^s^"]")inresident?name?w~x:sep~y:0?background:widget_bg?canvaswg)widgetsintower?name~margins:sep?align?background?canvas?scale_contentrooms(* compute the x,y,w,h that contains all rooms in the list *)letbounding_geometry=function|[]->printddebug_warning"Trying to find bounding_geometry of empty list";0,0,0,0|rooms->letrecloopxminyminxmaxymax=function|[]->(xmin,ymin,xmax-xmin,ymax-ymin)|room::rest->letx,y=getxroom,getyroominloop(iminxminx)(iminyminy)(imaxxmax(widthroom+x))(imaxymax(heightroom+y))restinloopmax_intmax_int00roomsmoduleGrid=struct(* return a Selection.t corresponding to the vertical projections of the
bounding boxes of the rooms in the house. *)letdetect_rows?(overlap=1)house=letvranges=matchhouse.contentwith|Resident_->[(0,heighthouse)]|Roomsrooms->List.map(funr->lety=getyrin(y,y+heightr-overlap))roomsinSelection.of_listvranges(* same for horizontal projections *)letdetect_columns?(overlap=1)house=lethranges=matchhouse.contentwith|Resident_->[(0,widthhouse)]|Roomsrooms->List.map(funr->letx=getxrin(x,x+widthr-overlap))roomsinSelection.of_listhrangesend(* Superpose a list of rooms without changing their relative (x,y) positions.
Unless specified by ~w ~h, the resulting layout has the *size* of the total
bounding box of all rooms. Its (x,y) *position* is such that, when displayed
at this position, all rooms should be located at the positions they
claimed. *)(* TODO it seems that only the first one gets focus... *)letsuperpose?w?h?name?background?canvas?(center=false)?(scale_content=true)rooms=letx,y,bw,bh=bounding_geometryroomsin(* We translate the rooms: *)List.iter(funr->setxr(getxr-x);setyr(getyr-y))rooms;letw=defaultwbwinleth=defaulthbhinifcenterthenList.iter(funr->setxr(Draw.center(getxr)w(widthr));setyr(Draw.center(getyr)h(heightr)))rooms;letgeometry=geometry~x~y~w~h()inifscale_contentthenscale_resize_list(w,h)roomselseList.iterdisable_resizerooms;create?name?background?canvasgeometry(Roomsrooms)(** save the layout_id in the user event *)(* not used anymore *)letsave_to_event_OLDeventroom=Sdl.Event.(seteventTrigger.room_idroom.id)(* TODO: since we only use one global event for mouse_enter or mouse_leave, it
would be more efficient to store directly the room in a global variable,
rather that storing the id, and then painfully search the room corresponding
to id... *)(** ask all the subwidgets to update themselves. *)(* in fact, this just send the redraw_event, which ask for redrawing the whole
window. Thus, it would be enough to ask this to only one widget of the layout
(since all widgets must be in the same window) *)letrecask_updateroom=matchroom.contentwith|Residentw->Widget.updatew|Roomslist->List.iterask_updatelist(** animations: *)(* animations with Anim are deprecated, use Avar instead *)letanimate_xroomx=lockroom;letg=room.geometryinAvar.stopg.x;room.geometry<-{gwithx};unlockroomletanimate_yroomy=lockroom;letg=room.geometryinAvar.stopg.y;room.geometry<-{gwithy};unlockroomletanimate_wroomw=lockroom;letg=room.geometryinAvar.stopg.w;room.geometry<-{gwithw};unlockroomletanimate_hroomh=lockroom;letg=room.geometryinAvar.stopg.h;room.geometry<-{gwithh};unlockroomletanimate_voffsetroomvoffset=lockroom;letg=room.geometryinletavar=Var.getg.voffsetinletis_current=Avar.startedavar&¬(Avar.finishedavar)inAvar.stopavar;Var.setg.voffsetvoffset;(* if the animation was already running we need to start immediately,
otherwise the value that we set here will be valid only for the next
iteration, which may cause non-immediate transitions: useful ???*)ifis_currentthenignore(get_voffsetroom);unlockroomletanimate_alpharoomalpha=lockroom;letg=room.geometryinAvar.stopg.transform.alpha;room.geometry<-{gwithtransform={g.transformwithalpha}};unlockroom;ask_updateroomletanimate_angleroomangle=lockroom;letg=room.geometryinAvar.stopg.transform.angle;room.geometry<-{gwithtransform={g.transformwithangle}};unlockroomletstop_posroom=printddebug_graphics"Stop position animation for layout %s."(sprint_idroom);lockroom;letg=room.geometryinAvar.stopg.x;Avar.stopg.y;unlockroom(** get desired room (relative) geometry after applying animation *)letgeomr=letg=r.geometryinto_current_geomg(* the calculation is there *)(** some predefined animations: *)(* warning, these animations can be set on-the-fly, so be careful with other
existing animations *)letdefault_duration=300(* add a show animation (vertical sliding) to the room; however: *)(* 1. if the room is already animated, we replace the old animation by the show,
and the duration is reduced proportionally to the current voffset of the old
animation *)(* 2. if the room is shown and without animation, we do nothing *)letshow?(duration=default_duration)?fromroom=ifroom.show&&Avar.finished(Var.getroom.geometry.voffset)thenprintd(debug_board+debug_warning)"Room %s is already shown, we don't run the show animation"(sprint_idroom)(* it is ok to show a room that currently is performing a hide animation. *)elsebeginletclip=reffalseinletinit()=clip:=room.clip;set_cliproomin(* it is important to do this AFTER the ending() of the previous
animation. *)leth=heightroominifnotroom.show&&(get_voffsetroom<>-h)then(printddebug_warning"Using a 'show' animation on a room that was not previously in a \
'hidden' state. Forcing voffset to %d."(-h);set_voffsetroom(-h));leth,duration=matchfromwith|None->letcurrent_vo=get_voffsetroominletd'=abs((current_vo*duration)/h)incurrent_vo,d'|SomeAvar.Bottom->h,duration|SomeAvar.Top->-h,duration|Some_->printddebug_board"Layout.show direction not implemented";h,durationinletending()=printddebug_board"End of show for %s"(sprint_idroom);room.clip<-!clipinletvoffset=Avar.show~init~ending~durationh0inanimate_voffsetroomvoffset;rec_set_showtrueroom;end(* add a hide animation to the room *)lethide?(duration=default_duration)?(towards=Avar.Bottom)room=if(notroom.show)(*&& Avar.finished (Var.get room.geometry.voffset)*)then()elsebeginletclip=reffalseinletinit()=clip:=room.clip;set_cliproominleth=heightroominletcurrent_vo=get_voffsetroominletd'=abs((h+current_vo)*duration)/(absh+1)in(* DEBUG *)letvo=matchtowardswith|Avar.Bottom->h|Avar.Top->-h|_->printddebug_board"Layout.show direction not implemented";hinletending_=printddebug_board"End of hide";room.clip<-!clip;rec_set_showfalseroomin(* WARNING: if the room contains subrooms with animations, they will remain
forever because a layout with show=false is not displayed and hence not
updated: the anim is not removed. Even more so with Avar. Thus compute
has_anim during display ? *)letvoffset=Avar.show~init~ending~duration:d'current_vovoinanimate_voffsetroomvoffsetend(** scrolling to a particular vertical position y, for a prescribed duration. *)(* y should be between 0 and the total height *)(* Warning: in principle, room.house.clip should be set to true for this *)letscroll_to?(duration=1000)(*?(weight=0.5)*)yroom=matchroom.housewith|None->printddebug_error"Cannot scroll the top layout (need a house)"|Somehouse->letcurrent_vo=get_voffsethouseinlety'=max0(miny(heightroom-heighthouse))inletduration=duration*(abs(current_vo+y')+1)/(abs(current_vo+y)+1)inprintddebug_graphics"Scroll room#%u in house #%u, from %d to %d, duration=%d"room.idhouse.idcurrent_vo(-y')duration;letvoffset=Avar.fromto~durationcurrent_vo(-y')in(* TODO: make a special animation when reaching bounds *)animate_voffsethousevoffset(** relative scrolling *)(* the specifications are: the scrolling must be continuous (no jump), and n
calls to (scroll dy) should end up in the same position as (scroll (n*dy)),
even if they are triggered before the previous animation is not finished. In
case of rapid mouse wheel events, it is not obvious to have the scrolling
look smooth. Thus we add the following spec: the scroll curve should be
epsilon-close to the h-translated starways curve that would be obtained with
immediate jumps, where epsilon is the jump height (50 for scroll wheel) and h
is a small time amout, that we choose to be 2*dt here (it should be less than
duration, otherwise the second part of the animation g2 is never executed) *)letscroll_delay=ref0.5letscroll_old2?(duration=default_duration)dyroom=do_optionroom.house(funhouse->letcurrent_vo=get_voffsethouseinletavar=Var.gethouse.geometry.voffsetinletjump_vo=Avar.final_valueavarinletfinal_vo=-(max0(min(dy-jump_vo)(heightroom-heighthouse)))inletduration=duration*(abs(final_vo-current_vo)+1)/(abs(jump_vo-dy-current_vo)+1)inletdt=Avar.progressavarinprintddebug_graphics"Scroll room #%u: dy=%d, current_vo=%d, jump_vo=%d, final_vo=%d, duration=%d, progress=%f"room.iddycurrent_vojump_vofinal_vodurationdt;letvoffset=ifcurrent_vo=jump_vo||duration=0then(scroll_delay:=0.5;Avar.fromto~durationcurrent_vofinal_vo)else(* the previous animation was not finished, we need to catch up *)letdv=final_vo-jump_voin(* warning, opposite sign to dy *)leth=ifdt<!scroll_delaythenscroll_delay:=max0.3!scroll_delay/.1.5elseifdt>1.5*.!scroll_delaythenscroll_delay:=min0.5(1.5*.!scroll_delay);!scroll_delayinletslope=(float(jump_vo-current_vo))*.(1.-.h)/.(h*.(float(final_vo-jump_vo)))in(* rem we may have (slope < 0.) in case of changing direction *)letg1=Avar.affine(floatcurrent_vo)(floatjump_vo)inletg2u=Avar.initial_slope~slope:(max1.(abs_floatslope))u|>Avar.affine(floatjump_vo)(floatfinal_vo)inletupdate_u=Avar.concat~weight:hg1g2u|>roundinprintddebug_graphics"Scroll: dv=%d, h=%f slope=%f"dvhslope;Avar.create~duration~updatecurrent_voinanimate_voffsethousevoffset)letlast_time=ref(Time.now());;(* TODO replace this by the time of the Avar *)letscroll?(duration=default_duration)dyroom=do_optionroom.house(funhouse->letcurrent_vo=get_voffsethouseinletavar=Var.gethouse.geometry.voffsetinletjump_vo=Avar.final_valueavarin(* à vérifier *)letfinal_vo=-(max0(min(dy-jump_vo)(heightroom-heighthouse)))inletelapsed=Time.(now()-!last_time)inletduration=if(elapsed=0)||(elapsed>300)||current_vo=final_vothendurationelseletspeed=(float(-dy))/.(floatelapsed)in(* = the speed that user expects to have, because she's rolling the
mouse wheel *this* fast. In pixels per ms. Now we need to adjust
the duration so that the expected final value is indeed reached at
that speed. *)letfinal=current_vo+(round(speed*.(floatduration)))iniffinal=current_vothendurationelseabs(duration*(final_vo-current_vo)/(final-current_vo))inprintddebug_graphics"Scroll room #%u: current:%d, final_vo=%d, duration=%d"room.idcurrent_vofinal_voduration;letvoffset=Avar.fromto~durationcurrent_vofinal_voinlast_time:=Time.now();animate_voffsethousevoffset)letscroll_old?(duration=300)dyroom=do_optionroom.house(funhouse->Avar.finish(Var.gethouse.geometry.voffset);(* TODO: do something smoother *)letprevious_vo=get_voffsethouseinprintddebug_graphics"Scroll room #%u, dy=%d, previous_vo=%d"room.iddyprevious_vo;scroll_to~duration(dy-previous_vo)room)(* find a parent whose house has the 'clip' property *)letrecfind_clip_houseroom=matchroom.housewith|None->None|Someh->ifh.clipthenSomeroomelsefind_clip_househ(** add fade_in transform to the existing animation of the room *)letfade_in?duration?(from_alpha=0.)?(to_alpha=1.)room=letalpha=Avar.fade_in?duration~from_alpha~to_alpha()inanimate_alpharoomalpha(** add fade_out transform to the existing animation of the room *)(* WARNING: fading out to alpha=0 results in a completely transparent room, but
the room is *still there*. (it's not "hidden"). Which means it can still get
mouse focus. If you want to hide it, then use hide=true *)letfade_out?duration?from_alpha?(to_alpha=0.)?(hide=false)room=letfrom_alpha=default_lazyfrom_alpha(lazy(get_alpharoom))inletending_=printddebug_board"End of complete fade_out => hiding room";rec_set_showfalseroominletending=ifhidethenSomeendingelseNoneinletalpha=Avar.fade_out?duration?ending~from_alpha~to_alpha()inanimate_alpharoomalpha(* angle in degree *)(* WARNING: it's not a global rotation. All widgets in the room will rotate
separately about their own center *)(* If you want to rotate a complete layout, use a Snapshot *)letrotate?duration?(from_angle=0.)~angleroom=letangle=Avar.fromto_float?durationfrom_angle(from_angle+.angle)inanimate_angleroomangle(* Zoom works for Resident, but for a general Rooms it will not work *)(* moreover, only few resident widgets will be ok. (image, box...) *)(* In order to zoom a general layout, use a Snapshot *)(* TODO: add zoom center *)letzoom_x?duration~from_factor~to_factorroom=letw0=round(float(widthroom)*.from_factor)inletw1=round(float(widthroom)*.to_factor)inletw=Avar.fromto?durationw0w1inprintddebug_graphics"ZOOM width from %d to %d"w0w1;(* DEBUG *)animate_wroomwletzoom_y?duration~from_factor~to_factorroom=leth0=round(float(heightroom)*.from_factor)inleth1=round(float(heightroom)*.to_factor)inleth=Avar.fromto?durationh0h1inprintddebug_graphics"ZOOM height from %d to %d"h0h1;(* DEBUG *)animate_hroomhletzoom?duration~from_factor~to_factorroom=zoom_x?duration~from_factor~to_factorroom;zoom_y?duration~from_factor~to_factorroom(** oscillate (for fun) *)letoscillate?(duration=10000)?(frequency=5.)amplituderoom=letx=Avar.oscillate~duration~frequencyamplitude(getxroom)inanimate_xroomx(** add a slide_in animation to the room *)letslide_in?from~dstroom=letx,y=Avar.slide_in?from~size:(get_sizedst)~pos:(getxroom,getyroom)()inanimate_xroomx;animate_yroomy(** translation animation *)letslide_to?(duration=default_duration)room(x0,y0)=letx1=getxroominlety1=getyroominletx=Avar.fromto~durationx1x0inlety=Avar.fromto~durationy1y0inanimate_xroomx;animate_yroomy(** follow mouse animation. *)(* Note that the window is not available before running the layout... *)(* TODO this doesn't work for touch, because get_mouse_state only captures when
the finger touches the screen, but not when it moves. One should use
pointer_pos instead. *)letmouse_motion_x?dx?modifierroom=letx0=ref0in(* we store here the dist between mouse and room *)letinit()=x0:=default_lazydx(lazy(fst(Mouse.window_pos(windowroom))-xposroom))inletupdate__=letx=fst(Mouse.window_pos(windowroom))-(x_originroom)-!x0inmatchmodifierwith|None->x|Somef->x+fxinAvar.create~duration:(-1)~update~init0letmouse_motion_y?dy?modifierroom=lety0=ref0inletinit()=y0:=default_lazydy(lazy(snd(Mouse.window_pos(windowroom))-yposroom))inletupdate__=lety=snd(Mouse.window_pos(windowroom))-(y_originroom)-!y0inmatchmodifierwith|None->y|Somef->y+fyinAvar.create~duration:(-1)~update~init0(* set the origin of the room at the mouse position - (dx,dy) *)(* if dx or dy is not specified, the default is the current distance between
room and mouse *)(* modifierx and modifiery are executed continuously during the animation, and
return an integer offset, which can typically be used to obtain a "magnetic"
effect. See examples/displays *)(* TODO merge into unique function "action" *)letfollow_mouse?dx?dy?modifierx?modifieryroom=letx=mouse_motion_x?dx?modifier:modifierxroominlety=mouse_motion_y?dy?modifier:modifieryroominanimate_xroomx;animate_yroomy(* Clip a room inside a smaller container and make it scrollable, and optionally
add a scrollbar widget (which should appear only when necessary). Currently
only vertical scrolling is implemented. *)letmake_clip?w?(scrollbar=true)?(scrollbar_inside=false)?(scrollbar_width=10)~hroom=(* iter_rooms disable_resize room; *)letname=(defaultroom.name"")^":clip"inifw<>Nonethenprintddebug_error"Horizontal scrolling is not implemented yet";letw=defaultw(widthroom)inlety0=getyroominsetyroom0;letactive_bg=Widget.empty~w:(widthroom)~h:(heightroom)()in(* We add an invisible box to make the whole area selectable by the mouse
focus. Otherwise, only the parts of the room that contain a widget will
react to the mouse wheel event. Of course, if the room was full of widgets,
this is superfluous... *)letlayer=get_layerroominletcontainer=tower~margins:0~clip:true[superpose[room;resident~layeractive_bg]]in(* The container should be a room with a unique subroom (and the active
background); the subroom can then be scrolled with respect to the container
*)set_sizecontainer(w,h);letresult=ifscrollbarthenbegin(* We first initialize the bar layout with a dummy widget, so that the
var is able to use it. This is only useful if the height of the
container is modified after creation, for instance when the user
resizes the window. *)letbar=resident~layer~background:(color_bgDraw.(lighterscrollbar_color))(Widget.empty~w:10~h:10())in(* The scrollbar is a slider. Its Tvar takes the voffset value into the
slider value, between 0 and (height room - height container). 0
corresponds to the bottom position of the slider, so this means the
*largest* scroll (voffset is the most negative). *)letvar=Tvar.createcontainer.geometry.voffset~t_from:(funvo->letdh=heightroom-heightbarinifdh<=0then0(* then the bar should be hidden *)elsedh+Avar.getvo)~t_to:(funv->letdh=heightroom-heightbarinletv=iminvdh|>imax0inAvar.var(heightbar-heightroom+v))inletwsli=Widget.slider~kind:Slider.Vertical~length:h~thickness:scrollbar_width~tick_size:(h*h/(heightroom))~var(imax0(heightroom-h))inchange_residentbarwsli;ifh>=(heightroom)thenhide~duration:0bar;letr=ifscrollbar_insidethen(setxbar(w-widthbar);set_layerbar(Chain.insert_after(Chain.last(get_layercontainer))(Draw.new_layer()));(* TODO: is this a bit too much ?? We just want to make
sure the scrollbar gets mouse focus. *)superpose~name[container;bar])elseflat~name~margins:0[container;bar]indisable_resizebar;(* We register a resize function that simultaneously sets the container
and the bar sizes. It will hide the bar when the container is large
enough to display the whole content. *)container.resize<-(fun(w,h)->letkeep_resize=trueinset_height~keep_resizebarh;ifscrollbar_insidethenset_size~keep_resizecontainer(w,h)elsebeginset_size~keep_resizecontainer(w-widthbar,h);setx~keep_resizebar(w-widthbar)end;letdh=heightroom-heightbarinletsli=Widget.get_sliderwsliinifdh>=1thenSlider.set_maxslidhelseset_voffsetcontainer0;(* Warning: we set the voffset directly, because when the bar is
hidden, the Tvar will never be activated -- except if the user
scrolls with the mouse. *)leth=heightbarinifheightroom<>0thenSlider.set_tick_sizesli(imax(Slider.min_tick_sizesli)(h*h/(heightroom)));letv=Slider.update_valuesli;Slider.valuesliinifv<0thenSlider.setsli0;ifdh<=0thenrec_set_showfalsebarelserec_set_showtruebar);rendelsecontainerinsetyresulty0;letx0=getxroominsetxroom0;setxresultx0;(* We copy the shadow. TODO: this has no effect at the moment, because of the
'clip' flag, the layout is sharply clipped to its bounding box when
rendering, so the shadow is hidden. *)letshadow=room.shadowinresult.shadow<-shadow;room.shadow<-None;resultletrelayoutcreatefn?(duration=200)layout=letrooms=get_roomslayoutinletold_pos=List.map(funr->getxr,getyr)roomsin(* [createfn] will change the rooms positions, but warning, this may also set
a new house, we have to set it back. *)let()=ignore(createfnrooms)inList.iter(funr->r.house<-Somelayout)rooms;ifduration<>0thenList.iter2(fun(oldx,oldy)room->letnewx=getxroominifoldx<>newxthenanimate_xroom(Avar.fromto~durationoldxnewx);letnewy=getyroominifoldy<>newythenanimate_yroom(Avar.fromto~durationoldynewy))old_posrooms(* adjust an existing layout to arrange its rooms in a "flat" fashion, as if
they were created by Layout.flat. Will be animated if duration <> 0 *)letreflat(*?(sep = Theme.room_margin / 2)*)?align?(hmargin=Theme.room_margin)?(vmargin=Theme.room_margin)?margins=relayout(funrooms->flat~hmargin~vmargin?margins?alignrooms)(* same as reflat but with Layout.tower *)letretower(*?(sep = Theme.room_margin / 2)*)?align?(hmargin=Theme.room_margin)?(vmargin=Theme.room_margin)?margins=relayout(funrooms->tower~hmargin~vmargin?margins?alignrooms)(* typically in a tower, enlarge all rooms to have the width of the house. This
is not recursive: only rooms of depth 1. *)letexpand_widthhouse=letw=widthhouseiniter_rooms(funroom->letx=getxroominifw-x<1thenprintddebug_warning"Cannot expand_width because house x position is larger than width.";set_widthroom(w-x))house(* Replace "room" by "by" in lieu and place of the initial room. No size
adjustments are made. Of course this is dangerous, because it modifies both
the house and "by". See also [add_room]. *)(* TODO copy old (x,y) position *)letreplace_room~byroom=matchroom.housewith|None->printddebug_error"Cannot use \"replace_room\" because room %s does not belong to a \
house."(sprint_idroom)|Somehwhenok_to_add_room~dst:hroom->printddebug_warning"Replacing room %s by room %s inside %s."(sprint_idroom)(sprint_idby)(sprint_idh);detachroom;lockh;lockby;attach~dst:hby;h.content<-Rooms(list_replace(equalroom)(get_roomsh)by);unlockby;unlockh|_->printd(debug_board+debug_error)"Cannot replace room %s"(sprint_idroom)(* move a room to a new house, trying to keep the same visual
position. Optionnally adding a scrollbar (in which case the returned layout
is not the same as the original one). *)(* WARNING this doesn't take voffset into account, so it won't work if a 'hide'
animation was used to the room. *)letrelocate~dst?(scroll=true)?(auto_scale=false)room=lockroom;(* TODO check they have the same top_house? *)letx0,y0=compute_posdstinletx1,y1=compute_posroomin(* 'pos_from' won't work here because this is called (by Select) before rooms
positions are computed... *)ifroom.house<>Nonethendetachroom;letroom2=ifnotscrollthenroomelselety2=y1-y0+heightroominprintddebug_board"Relocate room : y2=%i y1=%i y0=%i room=%i \
dst=%i"y2y1y0(heightroom)(heightdst);ify2<=heightdstthenroomelsebegin(* [here++] TODO If scroll=true it's probably better to use
make clip anyway, just in case the size of the housr
shrinks. *)(* sety room 0; *)make_clip~h:(heightdst-y1+y0)~scrollbar_inside:true~scrollbar_width:4roomendin(* We add it to the dst *)add_room~dstroom2;setxroom2(x1-x0);setyroom2(y1-y0);ifauto_scalethenscale_resizeroom2;unlockroom;room2(********************)(** display section *)(********************)letdebug_box~colorroomxy=letw,h=Draw.scale_size(get_sizeroom)inletx,y=Draw.scale_pos(x,y)inletbg=ifroom.mouse_focusthenSome(Draw.lightercolor)elseNoneinDraw.rect_to_layer?bg~color(get_canvasroom)(get_layerroom)(x,y)whletscale_rectrect=Sdl.Rect.(create~x:(Theme.scale_int(xrect))~y:(Theme.scale_int(yrect))~h:(Theme.scale_int(hrect))~w:(Theme.scale_int(wrect)))letscale_clipclip=map_optionclipscale_rectletshow_keyboard_focusroom_transformrect=(* TODO use transform *)letlayer=get_layerroominletcanvas=get_canvasroominletblits=Draw.box_shadow~offset:(0,0)~size:(Theme.scale_int3)~fill:falsecanvaslayer(scale_rectrect)inList.iterDraw.blit_to_layerblits(** Display a room: *)(* this function sends all the blits to be displayed to the layers *)(* it does not directly interact with the renderer *)(* pos0 is the position of the house containing the room *)letdisplay?pos0room=letx0,y0=matchpos0with|None->house_posroom|Somep->pinletrecdisplay_loopx0y0h0clip0tr0r=(* clip contains the rect that should contain the current room r. But of
course, clip can be much bigger than r. *)ifnotr.showthen()elsebeginletg=geomrinletx=x0+g.xandy=y0+g.y+h0andvoffset=g.voffsetin(* update current position, independent of clip *)r.current_geom<-{gwithx;y};(*print_endline ("ALPHA=" ^ (string_of_float (Avar.old
room.geometry.transform.alpha)));*)letrect=Sdl.Rect.create~x~y~w:g.w~h:g.hin(* if there is a nonzero offset, we perform a new clip : this is used for
"show/hide" animation *)(* TODO clip should be enlarged in case of shadow *)letclip=if(*voffset = 0*)notr.clip||!no_clipthenclip0elseDraw.intersect_rectclip0(Somerect)inletsclip=scale_clipclipinmatchclipwith|Someclip_rectwhennot(Sdl.has_intersectionclip_rectrect)->(r.hidden<-true;printddebug_warning"Room #%u is hidden (y=%d)"r.idy)(* Because of clip, the rendered size can be smaller than what the geom
says. *)(* If the clip is empty, there is nothing to display. Warning: this means
that all children will be hidden, even if they happen to pop out of
this rect. *)|_->beginr.hidden<-false;lettransform=lettr=get_transformrin(* printd debug_board "TRANSFORM alpha=%f" tr.Draw.alpha; *)letopenDrawin(* printd debug_board "COMPOSED TRANSFORM alpha=%f" (tr.alpha *. tr0.alpha); *)(*{ tr0 with alpha = tr0.alpha *. tr.alpha } in*)(* TODO: compose also rotations with centres, flips !! *)compose_transformtr0trin(* background (cf compute_background)*)letbg=map_optionr.background(funbg->letbox=matchbgwith|Stylestyle->(* let c = Draw.random_color () in *)(* DEBUG *)(* let style = Style.create ~background:(Style.Solid c)
* ?shadow:r.shadow () in *)letb=Box.(create~width:g.w~height:g.h~style())inlockr;r.background<-(Some(Boxb));unlockr;b|Boxb->binletblits=Box.display(get_canvasr)(get_layerr)boxDraw.(scale_geom{x;y;w=g.w;h=g.h;voffset=-voffset})inblits)in(* !!! in case of shadow, the blits contains several elements!! *)if!debug&&r.keyboard_focus=Sometruethenshow_keyboard_focusrtransformrect;beginmatchr.contentwith|Roomsh->(* We only draw the background. Make sure that the layer of the
room r is at least as deep as the layers of the Rooms h *)do_optionbg(List.iter(funblit->letopenDrawinlett=compose_transformtransformblit.transforminblit_to_layer{blitwithclip=sclip;transform=t}));if!draw_boxesthenbeginletrect=debug_box~color:(0,0,255,200)rxyinletopenDrawinlett=compose_transformtransformrect.transforminblit_to_layer{rectwithclip=sclip;transform=t}end;List.iter(display_loopxyvoffsetcliptransform)h|Residentw->letblits=Widget.display(get_canvasr)(get_layerr)wDraw.({x;y;w=g.w;h=g.h;voffset})inletblits=matchbgwith|None->blits|Someb->List.rev_appendbblitsin(* debug boxes *)letblits=if!draw_boxesthenletcolor=(255,0,0,200)inletrect=debug_box~colorrxyinrect::blitselseblitsinList.iter(funblit->letopenDrawinlett=compose_transformtransformblit.transforminletclip=sclipinblit_to_layer{blitwithclip;transform=t})blitsend;if!draw_boxes(* we print the room number at the end to make sure
it's visible *)thenletlabel=B_label.create~size:7~fg:(Draw.(transpblue))(sprint_idr)inletgeom=Draw.scale_geom{Draw.x;y;w=g.w+1;h=g.h+1;voffset}inletblits=B_label.display(get_canvasr)(get_layerr)labelgeominList.iterDraw.blit_to_layerblits;List.iterDraw.unload_blitblitsendendindisplay_loopx0y00None(Draw.make_transform())roomletget_focusroom=room.mouse_focus(* we don't lock because it will be modified only by the main loop *)letset_focusroom=room.mouse_focus<-true(* we don't lock because it will be modified only by the main loop *)letunset_focusroom=room.mouse_focus<-falseletset_cursorroomo=letcursor=matchroomowith|None->go(Draw.create_system_cursorSdl.System_cursor.arrow)|Someroom->matchroom.contentwith|Rooms_->go(Draw.create_system_cursorSdl.System_cursor.arrow)|Residentw->Widget.get_cursorwinSdl.set_cursor(Somecursor)(* comme display sauf qu'on ne trace que si nécessaire *)(* not used anymore *)letupdate_oldroom=letrecupdate_loopx0y0h0clip0room=ifnotroom.showthen()elsebeginletg=geomroomin(* attention, ça met anim <- None si anim = finished... *)letx=x0+g.xandy=y0+g.y+h0inletclip=ifg.voffset=0||!no_clipthenclip0elseDraw.intersect_rectclip0(Some(Sdl.Rect.create~x~y~w:g.w~h:g.h))inroom.current_geom<-current_geom~x~y~w:g.w~h:g.h();matchroom.contentwith|Roomsh->List.iter(update_loopxyg.voffsetclip)h|Residentw->ifnot(Widget.is_freshw)thenbegin(* if !draw_boxes then Draw.box (renderer room) ~bg:(200,10,20,50) x y g.w g.h; *)(* TODO background , transform *)letblits=Widget.display(get_canvasroom)(get_layerroom)w{Draw.x=x;y;w=g.w;h=g.h;voffset=g.voffset}inList.iter(funblit->Draw.(blit_to_layer{blitwithclip}))blitsendendinletx0,y0=house_posroominupdate_loopx0y00Noneroom(** check is the room has some non-fresh components. *)(* optimize (Bogue) ? *)letrecis_freshroom=matchroom.contentwith|Roomslist->letrecloop=function|[]->true|r::h->ifnot(is_freshr)thenfalseelseloophinlooplist|Residentw->Widget.is_freshwletroom_has_animroom=Avar.has_animroom.geometry.transform.alpha||Avar.has_animroom.geometry.transform.center||Avar.has_animroom.geometry.transform.flip||Avar.has_animroom.geometry.transform.angle||List.fold_left(funbv->b||(Avar.has_animv))false(get_int_avarsroom)(* optimize (Bogue) ? *)(* TODO one could transfer it into Layout.display, which would return the anim
status of what was displayed (and not what was hidden...) *)letrechas_animroom=if!debug&&(notroom.show)&&(room.hidden)&&(room_has_animroom)thenprintddebug_error"Room %s has unfinished animation but it is not shown."(sprint_idroom);room.show&&(notroom.hidden)&&(room_has_animroom||matchroom.contentwith|Roomslist->List.fold_left(funbr->b||(has_animr))falselist|Resident_->false)(* Flip buffers. Here the layout SHOULD be the main layout (house) of the
window. Only one canvas/renderer is used, the one specified by the layout. *)letflip?(clear=false)?(present=true)layout=printddebug_graphics"flip layout %s"(sprint_idlayout);(* go (Sdl.set_render_target (renderer layout) None); *)ifclearthenDraw.clear_canvas(get_canvaslayout);printddebug_graphics"Render layers";Var.protect_doDraw.current_layer(fun()->(* : we assume that the layout layer is in the same component as the
current_layer... TODO do better *)Draw.render_all_layers(get_layerlayout));ifpresentthenbeginprintddebug_graphics"Present";Draw.(sdl_flip(rendererlayout))end(* prerender the layout to the layers *)letrenderlayout=(* let renderer = renderer layout in *)(* go (Sdl.render_set_clip_rect renderer None); *)(* Draw.(set_color renderer (opaque black)); *)(* go (Sdl.render_clear renderer); *)(* Draw.clear_canvas (get_canvas layout); *)(* We should not clear the canvas here, since all rendering is done at the end
of the main loop, with flip *)ifDraw.window_is_shown(windowlayout)thendisplaylayoutelseprintddebug_board"Window (layout #%u) is hidden"layout.id(* the function to call when the window has been resized *)letresize_from_window?(flip=true)layout=lettop=top_houselayoutinifnot(equallayouttop)thenprintddebug_error"The layout for resizing window should be the top \
layout";letw,h=Draw.get_window_size(windowtop)|>Draw.unscale_posinletw',h'=get_sizetopinif(w',h')<>(w,h)thenbegin(* TODO in rare occasions, it might happen that this test is different
from get_physical_size top <> Draw.get_window_size win*)printddebug_graphics"Resize (%d,%d) --> (%d,%d)"w'h'wh;set_size~keep_resize:true~check_window:falsetop(w,h);Draw.update_background(get_canvastop);ifflipthenDraw.sdl_flip(renderertop)end(* : somehow we need this intermediate flip so that the renderer takes into
account the new size. Otherwise texture are still clipped to the old
size... On the other hand it might flicker if triggered too quickly *)(* fit_content layout;;*)(* not useful *)(** initialize SDL if necessary and create a window of the size of the layout *)letmake_window?windowlayout=printddebug_graphics"Make window";lettop=top_houselayoutinifnot(equallayouttop)thenprintddebug_error" The layout for creating a window should be the top layout";letw,h=get_physical_sizetopinletwmax,hmax=4096,4096in(* = TODO ? instead clip to ri_max_texture_width,ri_max_texture_height ? *)ifwmax<w||hmax<hthenprintddebug_error" The layout has size (%u,%u), which exceeds the max size (%u,%u)."whwmaxhmax;letw=minwwmaxinleth=minhhmaxinletx,y=get_window_poslayoutinletcanvas=Draw.init?window?name:layout.name?x?y~w~h()inglobal_set_canvastopcanvas(* adjust the window size to the top layout *)(* This should be enforced all the time *)(* this is not executed immediately, but sent to Sync *)(* TODO move this directly to the render loop, since it has to be done anyway,
and should not be done more than once per step. *)letadjust_window?(display=false)layout=Sync.push(fun()->lettop=top_houselayoutinifnot(equallayouttop)thenprintddebug_error"The layout for resizing window should be the top layout";letw,h=get_physical_sizetopinletwin=windowtopinprintddebug_graphics"SDL set window size %d x %d"wh;Draw.set_window_sizewin~w~h;(* resize ~flip:display top; *)(* : of course, top didn't really change size, but somehow the texture was
clipped to the old window size, and if we don't update it, the previous
clipped texture is stretched to the new window size. *)(* render top; *)(* flip top; *)(* Draw.(flip top.canvas.renderer); *)(* Now we render and flip. This is not strictly necessary, as it will surely
be done by the main loop anyway. But it doesn't hurt to do it twice... *)(* it should not be done if the window is hidden, because render targets
don't work well *)ifdisplay&&Draw.window_is_shown(windowtop)thenbeginrendertop;fliptopend)(*Draw.destroy_textures ();; *)(* the display function we export *)(* NO we need pos for snapshot.ml *)(*let display r : unit =
display r;;*)letinside_geomgeometry(x,y)=x<=geometry.x+geometry.w&&x>=geometry.x&&y<=geometry.y+geometry.h&&y>=geometry.yletinsideroom(x,y)=matchroom.maskwith|None->inside_geomroom.current_geom(x,y)|Somemask->(* TODO vérifier aussi qu'on est dans la dimension du mask *)letx0,y0=room.current_geom.x,room.current_geom.yinlet_,_,_,a=Draw.get_pixel_colormask~x:(x-x0)~y:(y-y0)ina<>0(** get the smallest room (with Resident) containing point (x,y), or None *)(* not used anymore *)letrecover_focusxyt=letg=to_current_geomt.geometry(* one should also take into account transforms, clipping... and also if a
SUBroom is animated, the geometry can extend beyond the initial (fixed)
house geometry,... TODO *)inift.show&&(inside_geomg(x,y))thenmatcht.contentwith|Resident_->Somet|Roomsh->list_check(funr->over_focus(x-g.x)(y-g.y)r)h(* we translate because geometry is relative *)elseNone(* instead of the first one, get the complete list *)(* cf remarks above *)letrecfocus_list_oldxyt=letg=to_current_geomt.geometryinift.show&&(inside_geomg(x,y))thenmatcht.contentwith|Resident_->[t]|Roomsh->List.flatten(List.map(funr->focus_list_old(x-g.x)(y-g.y)r)h)else[](* instead of the first one, get the complete list *)(* in each layer, the first element of the list has priority (TODO this is not
consistent with the fact that it is the last displayed) *)(* cf remarks above *)letrecfocus_listxyt=ift.show&&(insidet(x,y))thenmatcht.contentwith|Resident_->[t]|Roomsh->List.flatten(List.map(funr->focus_listxyr)h)else[](* get the focus element in the top layer *)lettop_focusxyt=letflist=focus_listxytinprintddebug_graphics"Number of layers detected under mouse: %u (%s)"(List.lengthflist)(String.concat" "(List.map(funr->"#"^(string_of_intr.id))flist));letcomparer1r2=Chain.compare(get_layerr1)(get_layerr2)inlist_maxcompareflist(** get the smallest room (with Rooms or Resident) containing (x,y), or None *)(* only used for testing *)(* TODO à fusionner avec le précédent pour retourner une paire ? *)(* TODO vérifier qu'on est dans le même calque (layer) *)letrechoverxyt=letg=to_current_geomt.geometryinift.show&&(inside_geomg(x,y))thenmatcht.contentwith|Resident_->Somet|Roomsh->(matchlist_check(funr->hover(x-g.x)(y-g.y)r)hwith|None->Somet|o->o)elseNone