123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268(* SDL Area Widget *)(* This file is part of BOGUE *)moduleBox=B_boxmoduleVar=B_varmoduleDraw=B_drawmoduleFlow=B_flowmoduleTime=B_timemoduleTrigger=B_triggermoduleTheme=B_thememoduleMouse=B_mouseopenB_utilsopenTsdltypedraw_element={id:int;name:string;mutabledisable:bool;f:Tsdl.Sdl.renderer->unit}typet={box:Box.t;(* TODO: in fact one could use 2 textures: one for the Box, one for the Area:
because the Box contains a background, and it's not always necessary to
clear the background each time we want to clear the Area... *)sheet:(draw_elementFlow.t)Var.t;(* A sheet should be a data structure that is very fast to append AND to
iterate, AND whose iteration can be split. Queues would be perfect for the
first two. We implemented Flow for this purpose. WARNING: commands in the
queue should NOT modify the sheet itself. For instance [clear] should not
be used in the sheet. *)mutableupdate:bool;(* if [update] is false, we just draw the box texture without applying the
[sheet] *)timeout:int;cache:(Draw.textureoption)Var.t;mutablepos:(int*int)option;(* For convenience, the layout position will be stored here *)}letnew_id=fresh_int()letcreate~width~height?style?(timeout=50)()={box=Box.create~width~height?style();sheet=Var.create(Flow.create());update=true;timeout;cache=Var.createNone;pos=None}letsprintel=Printf.sprintf"%u%s"el.id(ifel.name=""then""elsePrintf.sprintf" (%s)"el.name)(* Note: it would be super smart to reinstall the commands that created the
cache when we clear the cache... TODO?*)letclear_cachearea=matchVar.getarea.cachewith|None->()|Sometex->beginDraw.forget_texturetex;Var.setarea.cacheNoneendletunloadarea=Box.unloadarea.box;clear_cachearea(* force the area to be redrawn, without clearing the cache. *)letupdatearea=area.update<-true;Var.with_protectarea.sheetFlow.rewind(* not for sheet *)letcleararea=Var.setarea.sheet(Flow.create());clear_cachearea;updatearea(* not for sheet *)letfreearea=Box.freearea.box;cleararea(* Add the element to the sheet *)letadd_elementareael=Var.with_protectarea.sheet(funq->printddebug_custom"Adding element %s to the SDL Area."(sprintel);Flow.addelq;Flow.rewindq;(* we do this here just to avoid calling [update] *));area.update<-true(* Add a drawing function to the sheet and return the corresponding element. The
function should be fast, otherwise it will block the UI when the sheet is
executed. *)letadd_getarea?(name="")?(disable=false)f=letel={id=new_id();name;disable;f}inadd_elementareael;el(* Just add, don't return the element *)letaddarea?namef=add_getarea?namef|>ignore(* Clear the sheet before this point and save the drawing into the
cache. Currently, the user is responsible for saving the commands that were
used to create the cache if necessary. *)letcachearearenderer=matchVar.getarea.box.renderwith|None->failwith"Sdl_area texture was not created."|Sometex->letcache_tex=matchVar.getarea.cachewith|None->printddebug_graphics"Creating cache for Sdl_area.";letw,h=Draw.tex_sizetexinlett=Draw.create_targetrendererwhinVar.setarea.cache(Somet);t|Somet->tinFlow.forget(Var.unsafe_getarea.sheet);(* : this is dangerous since we are modifying the sheet in-place, and this
will be execured while itering it (in the display section)... However
looking at what Flow.rewind does, it looks ok: the next element should
still be accessible. *)letsave_target=Draw.push_targetrenderercache_texingo(Sdl.set_texture_blend_modetexSdl.Blend.mode_none);go(Sdl.render_copyrenderertex);Draw.pop_targetrenderersave_targetletcachearea=addarea~name:"cache"(cachearea)(* Remove the element from the sheet. OK to be slow. *)letremove_elementareaelement=updatearea;let@q=Var.with_protectarea.sheetintryFlow.remove_first_match(funel->el.id=element.id)qwithNot_found->printddebug_error"Element %s not found in SDL Area"(sprintelement)lethas_elementareaelement=let@q=Var.with_protectarea.sheetinFlow.rewindq;Flow.exists(funel->el.id=element.id)qletdisableelement=element.disable<-trueletenableelement=element.disable<-falseletsizearea=Box.sizearea.boxletresizesizearea=updatearea;Box.resizesizearea.box(* size in physical pixels *)letdrawing_sizearea=matchVar.getarea.box.renderwith|Somet->Draw.tex_sizet|None->(* HACK: TODO put this "video_init" in a proper "init" function. It has to
be done before the user runs the main board and open window. However it
should (maybe) not be called in the (stupid?) case where no video is
required, see example00. Well, actually, video_init is currently called
later anyways, and this doesn't seem to prevent example00 to run on
computers without display... *)if!Theme.scale=0.thenDraw.video_init();Box.sizearea.box|>Draw.to_pixels(* position in physical pixels with respect to the area *)letpointer_posareaev=letx0,y0=default_lazyarea.pos(lazy(printd(debug_error+debug_user)"Cannot find pointer position within the Sdl_area because it is \
not displayed yet.";(0,0)))inletx,y=Mouse.pointer_physical_posevinx-x0,y-y0letto_pixels=Draw.to_pixelsletset_rgbareargb=addarea(funrenderer->Draw.(set_colorrenderer(opaquergb)))(* Convenient shortcuts to some Draw functions. Downside: they cannot adapt
easily to resizing the area. See example 49. *)letdraw_circlearea~color~thick~radius(x,y)=addarea(Draw.circle~color~thick~radius~x~y)letfill_circlearea~color~radius(x,y)=addarea(Draw.disc~color~x0:x~y0:y~radius)letdraw_rectanglearea~color~thick~w~h(x,y)=addarea(Draw.rectangle~color~w~h~thick~x~y)letfill_rectanglearea~color~w~h(x,y)=addarea(funrenderer->Draw.boxrenderer~bg:colorxywh)letdraw_linearea~color~thick(x0,y0)(x1,y1)=ifthick=1thenaddarea(funrenderer->Draw.set_colorrenderercolor;go(Tsdl.Sdl.render_draw_linerendererx0y0x1y1))elseaddarea(Draw.line~color~thick~x0~y0~x1~y1)(* Direct access to the texture *)letget_texturearea=Var.getarea.box.renderletset_textureareatexture=Var.setarea.box.render(Sometexture);area.update<-false(************* display ***********)letdisplaywidcanvaslayerareag=area.pos<-Some(g.Draw.x,g.Draw.y);ifarea.updatethenBox.unload_texturearea.box;letblits=Box.displaycanvaslayerarea.boxginifnotarea.update&&Flow.end_reached(Var.getarea.sheet)thenblitselse(* Now we draw directly on the Box texture *)let()=printddebug_graphics"Rendering SDL Area of length %u."(Flow.length(Var.getarea.sheet))inletrenderer=canvas.rendererinlettex=matchVar.getarea.box.renderwith|Somet->t|None->failwith"The Sdl_area texture should have been create by Box \
already."inletsave_target=Draw.push_target~clear:falsecanvas.renderertexindo_option(Var.getarea.cache)(funt->printddebug_graphics"Using SDL Area cache.";go(Sdl.set_texture_blend_modetSdl.Blend.mode_none);go(Sdl.render_copycanvas.renderert));(* Executing the drawing functions cannot be done in a separate Thread
because it uses directly the SDL Renderer API. Hence we have a basic
timeout mechanism in order to be nice to the rest of the GUI. *)(* TODO Currently this mechanism does not work well (user events are
blocked) because we need to change the way events are consumed in the
main loop. *)lett0=Time.now()inVar.protect_fnarea.sheet(funq->Tsdl.Sdl.(set_render_draw_blend_moderendererBlend.mode_blend)|>go;Flow.iter_until(funel->ifnotel.disablethenbeginprintddebug_graphics"Executing SDL_Area element %s."(sprintel);el.frenderer;Time.now()-t0>area.timeoutendelsefalse)q;ifnot(Flow.end_reachedq)thenbeginprintd(debug_board+debug_warning)"The rest of the SDL Area will be rendered later.";Trigger.push_redrawwid(* ou plutôt update ? *)end);Draw.pop_targetcanvas.renderersave_target;area.update<-false;blits