123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429openB_utilsopenTsdlmoduleAvar=B_avarmoduleButton=B_buttonmoduleTheme=B_thememoduleTime=B_timemoduleVar=B_varmoduleTvar=B_tvarmoduleTrigger=B_triggermoduleDraw=B_drawmoduleMouse=B_mousetypekind=|Horizontal(* Horizontal bar with a small slider; values increase from left
to right. No background *)|HBar(* Horizontal bar filled up to the value *)|Vertical(* Warning: values increase from bottom to top *)|Circular(* Origin is on the positive real axis. *)typet={(* The value of the slider is a Tvar, which means that it can share a global
variable. This is used for instance for scrolling bars. When the scroll
bar is moved, the voffset of the layout is automatically updated, and
conversely if the layout is scrolled, the scrollbar is automatically
updated. *)var:(intAvar.t,int)Tvar.t;(* The local value of the Tvar is the local slider value: an integer between
0 and max. The remote value is an arbitrary 'external' integer Avar. *)(* TODO: (int Avar.t) is here to make smoother transitions. not done yet *)cache:intVar.t;(* used to avoid computing too many times the same value *)mutablepointer_motion:bool;clicked_value:(intoption)Var.t;offset:intVar.t;(* If offset=0, the tick will place itself with the mouse pointer exactly in
its middle point. Offset is used to not move the tick if one clicks at
any other position of the tick. *)mutablemax:int;(* Slider can take values from 0 to max, both included. Must be non zero. *)step:int;mutablesize:int*int;(* size in pixels (use Var.t ?). *)mutablethickness:int;(* in pixels *)mutabletick_size:int;(* in pixels. Size of the handle *)room_x:intVar.t;(* we store here the room position (unscaled) *)room_y:intVar.t;kind:kind;keyboard_focus:boolVar.t;(* we need to replicate here the keyboard_focus field of the layout, because
we use it to render the widget differently if it has keyboard_focus. It
acts similarly as the .active field of Text_input. It is set by
Widget.set_keyboard_focus. *)key_speed:floatVar.t;key_time:Time.tVar.t;render:(Draw.textureoption)Var.t;(* render is only used for circular. Otherwise all textures are created and
destroyed on the fly. Change this ? *)}letlengths=letw,h=s.sizeinmatchs.kindwith|HBar|Horizontal->w|Circular->iminwh|Vertical->hletcheck_maxm=ifm<=0then(printddebug_error"Max value of slider must be positive, not %i."m;1)elsem(* value is ignored if a var is provided *)letcreate?step?(kind=Horizontal)?(value=0)?(length=200)?(thickness=20)?w?h?tick_size?varm=lettick_size=defaulttick_size(matchkindwith|HBar->4|_->50)inletsize=matchkindwith|HBar|Horizontal|Circular->defaultwlength,defaulththickness|Vertical->defaultwthickness,defaulthlengthinletthickness=matchkindwith|HBar|Horizontal->sndsize|Vertical->fstsize|Circular->thicknessinletstep=defaultstep(max1(m/100))inletvar=default_lazyvar(lazy(Tvar.create(Var.create(Avar.varvalue))~t_from:(Avar.get)~t_to:(Avar.var)))in{var;cache=Var.create(Tvar.getvar);pointer_motion=false;max=check_maxm;clicked_value=Var.createNone;offset=Var.create0;step;size;thickness;tick_size;kind;room_x=Var.create0;room_y=Var.create0;keyboard_focus=Var.createfalse;key_speed=Var.create1.;key_time=Var.create(Time.now());render=Var.createNone}(* create a slider with a simple Tvar that executes an action each time the
local value of the slider is modified by the slider *)(* let create_with_action ?step ?kind ~value ?length ?thickness ?tick_size *)(* ~action max = *)(* let v = Var.create (Avar.var value) in *)(* let t_from a = Avar.get a in *)(* let t_to x = action x; Avar.var x in *)(* let var = Tvar.create v ~t_from ~t_to in *)(* create ?step ?kind ~var ?length ?thickness ?tick_size max;; *)letunloads=matchVar.gets.renderwith|None->()|Sometex->beginDraw.forget_texturetex;Var.sets.renderNoneend(* TODO *)letfree=unloadlethas_keyboard_focuss=Var.gets.keyboard_focusletset_focuss=Var.sets.keyboard_focustrueletunfocuss=Var.sets.keyboard_focusfalseletvalues=Var.gets.cache(* Tvar.get s.var;;*)letget_maxs=s.maxletclicked_values=Var.gets.clicked_valueletsetsvalue=Tvar.sets.varvalue;Var.sets.cachevalue(* This has to be done for each external call to this module *)(* It will update the position of the slider by looking at the s.var. Hence, if
the s.var has a nontrivial transformation, it might be that the value differs
from the value initially computed from the mouse position. See example 34. *)letupdate_values=Var.sets.cache(Tvar.gets.var)letrefresh_=()(* TODO *)(* TODO: we could use some animation to make it smoother *)letincrease?steps=letstep=defaultsteps.stepinsets(mins.max(values+step));refreshsletdecrease?steps=letstep=defaultsteps.stepinsets(max0(values-step));refreshs(* events *)(* Compute the pre-value (in principle between 0 and s.max, but sometimes can be
outside if the tick is large) from the mouse position *)letcompute_valuesev=letw,h=s.sizeinletx,y=Mouse.pointer_posevinletv=matchs.kindwith|Horizontal->ifs.tick_size=wthen0(* the value should be undefined here *)elseVar.gets.offset+(s.max*(x-s.tick_size/2-(Var.gets.room_x)))/(w-s.tick_size)|HBar->(s.max*(x-(Var.gets.room_x)))/w|Vertical->ifs.tick_size=hthen0(* undefined *)elseVar.gets.offset+s.max-(s.max*(y-s.tick_size/2-(Var.gets.room_y)))/(h-s.tick_size)|Circular->letx0=Var.gets.room_x+w/2inlety0=Var.gets.room_y+h/2inifx=x0thenify>y0then3*s.max/4elses.max/4elseleta=(floats.max)*.atan(float(y0-y)/.(float(x-x0)))/.pi/.2.inleta'=ifx>x0thenify<=y0thenaelsea+.(floats.max)elsea+.(floats.max)/.2.inrounda'in(* printd debug_custom "Mouse (%d,%d), value=%d" x y v; *)v(* This should be called on mouse_button_down. *)(* If the click is over the tick, we do *not* change value: this is the standard
behavious in most GUIs, and is a good idea imho. This requires storing an
offset. *)letclicksev=if!debugthenassert(Var.gets.offset=0);(* in some fast mouse motion it can happen that button_up is lost, so this
assertion fails. *)letold=valuesinletmouse_v=compute_valuesevinletv=ifabs(mouse_v-old)*(lengths-s.tick_size)<=s.max*s.tick_size/2thenbegin(* test à revoir: mouse over tick *)(* printd debug_custom "OVER TICK"; *)Var.sets.offset(old-mouse_v);oldendelse(max0(minmouse_vs.max))inprintddebug_board"Slider value : %d"v;Var.sets.clicked_value(Somev);(* Var.set s.keyboard_focus true; *)setsv(* we add an animation to the original Avar. For this we need some
gymnastic to get the current and final value for it *)(* TODO this works only for scrolling, because has_anim is detected for
scrolling. Otherwise, has_anim does not detect this animation yet *)(* let avar = s.var.Tvar.var in *)(* let final = Avar.get (s.var.Tvar.t_to v) in *)(* Var.set avar (Avar.fromto (Avar.get (Var.get avar)) final);; *)(* This should be called on mouse_button_up: *)letreleases=Var.sets.clicked_valueNone;s.pointer_motion<-false;Var.sets.offset0(* on mouse motion: *)letslidesev=letv=compute_valuesevinletv=(max0(minvs.max))inprintddebug_board"Slider value : %d"v;s.pointer_motion<-true;setsv(* Use this to increase the step when using keyboard. *)letchange_speeds=lett=Time.now()inifTime.(t-(Var.gets.key_time))>200(* delay too long, we return to initial speed. TODO: check that this is bigger
than system delay between two key repeats, otherwise this will always
apply *)thenVar.sets.key_speed1.elseVar.sets.key_speed(Var.gets.key_speed*.1.1);Var.sets.key_timet;letstep=s.step*(round(Var.gets.key_speed))instep(* This should be called on key_down. *)letreceive_keysev=update_values;ifhas_keyboard_focussthenbeginmatchTrigger.event_kindevwith|`Key_down->(matchSdl.Event.(getevkeyboard_keycode)with|cwhenc=Sdl.K.left->decrease~step:(change_speeds)s|cwhenc=Sdl.K.down->decrease~step:(change_speeds)s|cwhenc=Sdl.K.right->increase~step:(change_speeds)s|cwhenc=Sdl.K.up->increase~step:(change_speeds)s|c->(printddebug_event"==> Key down event discarded.";printddebug_event"Key=[%s], mod=%u, Keycode:%u"(Sdl.get_key_namec)(Sdl.get_mod_state())c))|_->printddebug_event"Warning: Event should not happen here"endletset_maxsm=s.max<-check_maxmletset_tick_sizesx=s.tick_size<-xletmin_tick_sizes=matchs.kindwith|HBar->4|Horizontal->25|Vertical->20|Circular->15(* display *)letsizes=s.sizeletresize(w,h)s=unloads;s.size<-(w,h);letthickness=matchs.kindwith|HBar|Horizontal->h|Vertical->w|Circular->s.thicknessins.thickness<-thickness(* internal *)letx_poss=Var.gets.room_x+(values)*(lengths-s.tick_size)/s.max(* internal *)lety_poss=Var.gets.room_y+lengths-s.tick_size-(values)*(lengths-s.tick_size)/s.maxletmake_box_blit~dst?(shadow=true)~focusvoffsetcanvaslayerbox=(* Let's see if it is nice to add a "shadow" to the tick *)letbox_blit=Draw.make_blit~voffset~dstcanvaslayerboxinifshadow&&focusthenletshadow_blits=Draw.box_shadow~offset:(0,0)~size:(Theme.scale_int6)canvaslayerdstinList.rev(box_blit::shadow_blits)else[box_blit]letdisplaycanvaslayersg=(* We use y_pos before updating to display a gradient box at the real mouse
position, in case of non-linear (vertical) slider (see example 34)... TODO
do the same for Horizontal sliders. *)letoldy=y_possinupdate_values;letscale=Theme.scale_intinlettick_size=scales.tick_sizeandthickness=scales.thicknessinletopenDrawinletrenderer=canvas.rendererinletgx=Theme.unscale_intg.xandgy=Theme.unscale_intg.yinifVar.gets.room_x<>gxthenVar.sets.room_xgx;ifVar.gets.room_y<>gythenVar.sets.room_ygy;letfocus=has_keyboard_focussinletshadow=true(* for testing *)inletc=ifshadowthenopaqueButton.color_onelseset_alpha200Button.color_oninletcolor=ifhas_keyboard_focuss&¬shadowthenDraw.(darkerc)elsecinletx0=scale(x_poss)in(* set_color renderer (opaque color); *)matchs.kindwith|Horizontal->(* let rect = Sdl.Rect.create ~x:x0 ~y:g.y ~w:thickness ~h:width in *)(* go (Sdl.render_fill_rect renderer (Some rect)); *)letbox=texturecanvas.renderer~color~w:tick_size~h:thicknessinletdst=Sdl.Rect.create~x:x0~y:g.y~w:tick_size~h:thicknessinforget_texturebox;(* or save ? but be careful color may change *)make_box_blit~dst~shadow~focusg.voffsetcanvaslayerbox|HBar->(* horizontal gradient for the slider *)letcolors=[opaqueButton.color_on;opaqueButton.color_off]inletbox=gradient_texturecanvas.renderer~w:(x0-g.x+tick_size)~h:thickness~angle:90.colorsinletdst=Sdl.Rect.create~x:g.x~y:g.y~w:(x0-g.x+tick_size)~h:thicknessinforget_texturebox;(* or save ? *)make_box_blit~dst~shadow~focusg.voffsetcanvaslayerbox(* [make_blit ~voffset:g.voffset ~dst canvas layer box] *)|Vertical->lety0=scale(y_poss)inletdy=scaleoldy-y0inlety=iminy0(y0+dy)inleth=imaxtick_size(absdy)in(* see example 34 .*)letbox=ifabsdy<=3||nots.pointer_motion(* the 3 is completely heuristic. See example 35. Ideally we want
0. *)thentexturecanvas.renderer~color~h~w:thicknesselseletcolors=[opaqueButton.color_on;opaqueButton.color_off]in(* let _ = print_endline (Printf.sprintf "dy = %i" dy) in *)letcolors=ifdy<0thencolorselseList.revcolorsingradient_texturecanvas.renderer~h~w:thicknesscolorsinletdst=Sdl.Rect.create~x:g.x~y~h~w:thicknessinforget_texturebox;(* or save ? *)make_box_blit~dst~shadow~focusg.voffsetcanvaslayerbox|Circular->letradius=(iming.wg.h)/2-2inlettex=matchVar.gets.renderwith|Somet->t|None->lett'=ring_texrenderer~color:(lighter(transpgrey))~radius~width:thickness(g.w/2)(g.h/2)in(* j'ai essayé de mettre une taille double puis de réduire avec
render_copy, mais apparemment ça ne fait pas d'antialiasing *)(* let t' = convolution ~emboss:false renderer *)(* (gaussian_blur ~radius:3) 3 t in *)Var.sets.render(Somet');t'inletw',h'=tex_sizetexinletdst=Sdl.Rect.create~x:(g.x)~y:(g.y)~w:w'~h:h'in(* go (Sdl.render_copy ~dst renderer tex); *)letsbox=make_blit~voffset:g.voffset~dstcanvaslayertexin(* ring renderer ~bg:(lighter (opaque grey)) ~radius:(w/2-2)
~width (x+w/2) (y+h/2); *)lettick=ray_to_layercanvaslayer~voffset:g.voffset~bg:color~thickness:tick_size~angle:(360.*.(float(s.max-values))/.(floats.max))~radius~width:thickness(g.x+g.w/2)(g.y+g.h/2)in[sbox;tick](* this function can be used for the ~t_to function to slow down the slider when
the range of values is big (bigger than the number of pixels of the slider).
When the user first move the slider, the slope will be 1 (1 pixel => 1 step),
and then of course the slope becomes higher in order to catch up. The price
to pay is that the slider position has to be corrected when mouse_button is
up. And the function has to be changed for each new starting value x0. *)(* x and x0 are between 0 and 1. Range of values is [0,M]. This is only needed
when M>1 *)(* k>=2 is the nonlinearity, it has to be even. k=2 should be enough *)(* TODO modify the formula to allow k odd *)letslowkmx0x=ifx>=x0thenif(floatk)*.(1.-.x0)*.m>=float(k-1)(* we have to slow down *)thenlett=(x-.x0)/.(1.-.x0)in(m-.1.-.x0*.m)*.(pwrkt)+.t+.x0*.melsex*.m(* just linear *)elseif(floatk)*.m*.x0>=float(k-1)(* we have to slow down *)thenlett=(x-.x0)/.x0in(1.-.x0*.m)*.(pwrkt)+.t+.x0*.melsex*.m