123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604(* A LongList is a layout composed of a list of layouts to be displayed on a
given region, with scroll bar when necessary, and memory management: only a
limited number of items are kept alive in memory *)(* There is no data (variable) attached to a long list. Data management should
be implemented by the user, for instance via widgets, cf example 34. Cf also
table.ml *)(* L'interaction utilisateur vient uniquement de la barre de scrolling (slider).
On utilise donc le TVar, comme dans les scrolling habituels (Layout.clip).
Cependant ici la variation de cette variable doit entraîner d'autres
modifications.
* D'une part on est lié au voffset du layout principal (avec Tvar, car si le
voffset est changé par ailleurs (ex une animation) il faut que la barre de
scrolling change aussi)
* D'autre part le voffset du Layout doit être calculé en fonction de la position
actuelle de la Longue Liste. Peut-être pas de façon bidirectionnelle (sauf si
on autorise l'utilisateur à modifier directement la position de la Longue
Liste).
* Le principe de base est
1. le layout généré (room) a en gros 5 (= factor) fois la hauteur de l'élément
voulu (de façon à pouvoir déjà scroller un peu sans générer de nouvelle
entrées, soit 2 avant, 2 après. Peut-être augmenter le chiffre 5 ?
2. on a une taille mémoire à ne pas dépasser: on supprime les textures des
entrées déjà calculées si on dépasse cette taille. La mémoire utilisée est
calculée seulement par la surface (en pixels²) des textures. Ça dépend du
scaling imposé par le thème, donc on utilise Layout.get_physical_size.
Le layout virtuel qui contiendrait l'ensemble de la liste possède une hauteur
qu'il est important de connaître, pour ajuster la position de la barre de
scrolling, mais qu'on ne connaît pas au début, puisqu'on ne veut pas générer
toutes les entrées d'un coup. On va commencer par estimer cette hauteur
totale en faisant la moyenne des hauteurs de chaque entrée calculée,
multipliée par le nombre d'entrées.
*)(*
________________
^ | | ^
| | virtual (ll) | |
| | | | ll.offset >0
| | | |
| | ________________ |
| | | | | ^ ^
| | | | | | | scroll_margin
| | |----------------| | | v
| | | layout | | | container.geometry.voffset <0
| | | (room) | | |
| | | | v v
| | | _______________
ll.height | | | | | S ^
| | | | | C |
| | | | real display | R | max = length slider units
| | | | (container) | O | to represent the whole
| | | | | L | ll.offset range
| | | |_______________| L v
| | | |
| | |----------------| ^
| | | | | scroll_margin
| | |________________| v
| | |
v |________________|
constraint: voffset <= 0 and -voffset + display.height <= layout.height
warning: slider is from bottom to top: 0 = bottom position
*)(* TODO bug d'affichage lorsqu'on change de room, certaines anciennes entrées
ont une mauvaise géométrie et d'affichent par dessous les nouvelles. Un
Trigger.push_redraw ???; dans Layout.set_rooms permettrait de corriger ça,
mais ce n'est certainement pas le mieux... *)(* TODO BUG deal with resizing layout to include more/less rows in the
container. Its works at startup but as soon as we scroll it stops working *)openTsdlopenB_utilsmoduleLayout=B_layoutmoduleWidget=B_widgetmoduleAvar=B_avarmoduleTheme=B_thememoduleTime=B_timemoduleVar=B_varmoduleTvar=B_tvarmoduleTrigger=B_triggermoduleDraw=B_drawmoduleSlider=B_slidertypeentry=|Void|Freed|ComputedofLayout.ttypedirection=|Up|Downletfactor=5(* ok? factor > 1 to ensure smoother scrolling *)letmin_tick_size=10(* scrollbar handle min size *)letscroll_margin=70(* we try to keep at least this amount of pixels above and below the clipped
layout in order to allow normal mouse wheel scroll *)(* unless specified, "pixel" means "logical pixel". The real ("physical") size
onscreen is obtained by multiplying by Theme.scale *)typet={length:int;(* total number of elements *)mutabletotal_height:intoption;(* = Total height pixels that would be necessary to render all
entries. None if we are not sure *)mutablecomputed_height:int;(* = total height in pixels of computed entries, ie entries present in the
current room *)offset:(intAvar.t)Var.t;(* = the starting vertical position we want to see onscreen *)mutablecomputed:int;(* = number of already generated entries (even if they have been freed
afterward) *)mutablerendered_height:int;(* = approx. height of the computed layout; it's just used as a hint. In
principle it will be factor * height of the target (clipped)
layout. The real height will differ because we always render an integer
number of entries. *)generate:(int->Layout.t);(* the function to generate the ith entry *)cleanup:(Layout.t->unit);(* cleanup the memory associated with the entry layout *)max_memory:intoption;(* = if not None, then tell the program to do memory management to use only
some approximate memory maximum for storing the textures (in pixel²) *)mutableused_memory:int;array:entryarray;(* we store everything in an array. This choice is questionable, because for
a large list, only a small part will be kept in memory. The solution we
take here is to set "None" to entries we want to forget, hoping that this
won't take much memory space. Maybe we could use a Weak.array *)linear:bool;(* linear scale for slider (by default) *)mutablefirst:int;mutablelast:int;(* = index of first & last entries (starting from 0) computed in the room
below *)mutablefirst_mem:int;mutablelast_mem:int;(* = index of first and last entries computed and still in memory (in the
array) *)(* room : Layout.t; *)(* NOT USED *)(* = the complete layout to clip & display. It contains entries from ll.first
to ll.last, inclusive. *)(* the geometry.voffset of the room should always be 0; scrolling is done by
vofsetting a container layout (see below). The absolute position of the
room in ll is ll.offset *)mutablecontainer_voffset:int;(* currently, the container voffset may be changed directly by mouse wheel
(see bogue.ml and Layout.scroll). Hence we need to save the value here in
order to sync with these external changes *)heights:(intoption)array;(* = the array of heights of all entries. It may or may not be initialized
at startup. Value None means the we don't know, the real height will
then be computed on the fly.*)}letto_str=function|Up->"Up"|Down->"Down"(* When an entry i of the ll.array is freed, the field ll.last_mem should be
updated. *)letupdate_last_memlli=printddebug_memory"New memory range for Long_list = [%u,_%u_]"ll.first_memll.last_mem;letrecloopj=ifj<0then(ll.last_mem<-0;failwith"BOOOh")(* this should not happen... *)elsematchll.array.(j)with|Computed_->ll.last_mem<-j|Void|Freed->loop(j-1)inloopi(* idem *)letupdate_first_memlli=printddebug_memory"New memory range for Long_list = [_%u_,%u]"ll.first_memll.last_mem;letrecloopj=ifj>=ll.lengththen(ll.last_mem<-ll.length-1;failwith"BAAAAh")elsematchll.array.(j)with|Computed_->ll.first_mem<-j|Void|Freed->loop(j+1)inloopi(* reduce memory usage by deleting some entries *)(* REMARK: instead of this complicated memory management, one could instead
store entries in a Weak Array, and let them be collected by the GC. (and the
"free" function can be called via Gc.finalise) *)(* TODO do we check that we don't delete the one that has just been created? *)letreduce_memorylldirection=printddebug_memory"Long list: Reduce_memory...";letmm=matchll.max_memorywith|Somemm->mm|None->failwith"reduce_memory is only called with ll.max_memory is not None"inletrecloopjnext=ifj<0||j>=ll.lengththenprintddebug_error"Memory usage for LongList exceeds maximum value. Beware."elseletj'=nextjinmatchll.array.(j)with|Void|Freed->loopj'next|Computedl->ifj>=ll.first&&j<=ll.lastthenprintddebug_error"OOPS! cannot remove Longlist entry #%u because it belongs to the current room..."j(* TODO = this is not completely correct because this function is
called before the room is finalized... I think it can be really
problematic in some situations with big jumps *)elsebeginletmem=let(w,h)=Layout.get_physical_sizelinw*hinprintddebug_memory"Cleaning up entry #%d of LongList"j;ll.cleanupl;(* not necessary in principle. This is done by the Gc.finalise (NOT
anymore)*)Layout.send_to_cemeteryl;(* for debugging *)ll.array.(j)<-Freed;ifj>=ll.last_memthenupdate_last_memlljelseifj<=ll.first_memthenupdate_first_memllj;ll.used_memory<-ll.used_memory-mem;ifll.used_memory>mmthenloopj'next(* TODO use a factor eg 3/4 to reduce more memory at once? but then
make sure that (3/4)memory is enough to avoid deleting currently
viewed items... *)endinmatchdirectionwith|Down->(* this means that we are generating entries at the bottom: we delete
from the top: *)printddebug_memory"...from top";loopll.first_mem(funi->i+1)|Up->printddebug_memory"...from bottom";loopll.last_mem(funi->i-1)(* return value or approximation of the total height: *)lettotal_heightll=matchll.total_heightwith|Someh->h|None->round(float(ll.computed_height*ll.length)/.(floatll.computed))(* get ith entry *)(* TODO: not thread safe *)letgetllidirection=matchll.array.(i)with|Computedl->l|Void|Freed->begin(* print_string (sprintf "GET (compute) %d " i); *)letentry=ll.generateiinlet(w,h)=Layout.get_physical_sizeentryinll.used_memory<-ll.used_memory+w*h;ifi>ll.last_memthenll.last_mem<-ielseifi<ll.first_memthenll.first_mem<-i;printddebug_memory"Long list: used memory: %d"ll.used_memory;ifll.array.(i)=Voidthen(* we may have to update height *)beginleth=Layout.heightentryinmatchll.heights.(i)with|None->ll.computed_height<-ll.computed_height+h;ll.computed<-ll.computed+1;ifll.computed=ll.lengththenll.total_height<-Somell.computed_height;ll.heights.(i)<-Someh|Somehh->ifhh<>hthenbeginprintddebug_error"Computed height (%u) for long_list element #%u differs from given height (%u)"hihh;ll.heights.(i)<-Someh;ll.computed_height<-ll.computed_height+h-hh;do_optionll.total_height(fun_->ll.total_height<-Somell.computed_height)endend;ll.array.(i)<-Computedentry;do_optionll.max_memory(funmm->ifll.used_memory>mmthenreduce_memorylldirection);entryend(* TODO write a cleanup function, to update everything in case the user modifies
the layouts updtream (like in a model-view system) *)(* compute entries until reaching at most the given height. updates
ll.first/last. Return the room and the index of last generated entry *)letcompute_room~heightlli_startdirection=ll.first<-i_start;(* print_endline (sprintf "COMPUTE start=%d" i_start); *)letrecloopi~hlist=ifh>=height||i>=ll.lengththenList.revlist,(i-1)elsebeginll.last<-i;(* = this is to protect from cleaning up already generated entries *)letline=getllidirectioninletdh=Layout.heightlineinloop(i+1)~h:(h+dh)(line::list)endinletlist,i_final=loopi_start~h:0[]inletroom=Layout.tower~name:"long_list room"~sep:0~hmargin:0~vmargin:0listinif!debugthenassert(ll.last=i_final);room,i_final(* lookup new entries in the given direction, starting from index "start"
(included) until the sum of the heights of all new entries added reaches the
desired height parameter. Return the computed height and i = next after the
last looked-up entry *)(* for very long lists, this can take a lot of time if the user didn't provide
the heights array; thus we change the cursor in case of wait > 100 ms *)letaddup_entriesll~start~heightdirection=letheights=ll.heightsinlettime=Time.now()inletslow=reffalseinletcursor=refNoneinletrecloopih=ifh>=height||i<0||i>ll.length-1thenh,i(* note, i = -1 is a valid output *)elseletdh=matchheights.(i)with|None->Layout.height(getllidirection)|Somedh->dhinifnot!slow&&Time.now()-time>100then(slow:=true;cursor:=Sdl.get_cursor();Sdl.set_cursor(Some(go(Draw.create_system_cursorSdl.System_cursor.wait))));loop(ifdirection=Uptheni-1elsei+1)(h+dh)inleth,i=loopstart0inif!slowthenSdl.set_cursor!cursor;(* print_endline (sprintf "ADDUP dir=%s start=%d height=%d ==> h=%u, i=%d" (to_str direction) start height h i); *)h,iletupdate_voffsetcontainerdv=ifdv<>0(* this test is important because shift_offset creates a new
animation... *)thenLayout.shift_voffsetcontainerdv(* Given the required new value offset for of ll.offset we do all the necessary
side-effects: changing the container voffset and possibly compute a new room.
*)letupdate_roomllcontainero=letroom,active_bg=letopenLayoutinmatchcontainer.contentwith|Rooms[superp]->(matchsuperp.contentwith|Rooms[room;active_bg]->room,active_bg|_->failwith"The container should contain a single layout with a list \
of 2 rooms!")|_->failwith"The container should contain a single layout with a list of \
2 rooms!"inleth=Layout.heightcontainerinletll_height=total_heightllinVar.protectLayout.(container.geometry.voffset);(* useful? *)letoffset=Avar.get(Var.getll.offset)inletvoffset=Layout.get_voffsetcontainerinletoffset,o=ifvoffset<>ll.container_voffset(* we need to shift both ll.offset and o; this can happen after mouse wheel
scroll *)thenletoffset=offset+ll.container_voffset-voffsetinAvar.set(Var.getll.offset)offset;leto=o+ll.container_voffset-voffsetinll.container_voffset<-voffset;offset,oelseoffset,oinVar.protect_doll.offset(fun()->letvoffset2=voffset+offset-oinif((voffset2+scroll_margin<0)||(ll.first=0))&&((h-voffset2+scroll_margin<Layout.heightroom)||(ll.last=ll.length-1))thenbegin(* then the room is still usable *)(* in case of equality voffset2 = 0 or voffset2 = ... , one should still
do the update except at very top or bottom of list, in order to allow
mouse wheel scroll to go past the computed room. *)update_voffsetcontainer(voffset2-voffset);(* = offset - o *)(* ==> the new value of the container voffset is voffset2 *)Var.releaseLayout.(container.geometry.voffset);ll.container_voffset<-voffset2;endelsebeginletroom'=(* need to compute a new room *)printddebug_memory"UPDATE LONG_LIST [%d,%d] => newoffset=%d oldoffset=%d voffset=%d \
voffset2=%d (approx)height=%d, rendered_height=%d, room.height=%d, \
MEM=[%d,%d] "ll.firstll.lastooffsetvoffsetvoffset2ll_heightll.rendered_height(Layout.heightroom)ll.first_memll.last_mem;ifvoffset2+scroll_margin>=0(* we need to compute upwards *)thenbeginletdirection=Upinletadd_h=max((ll.rendered_height-h)/2-scroll_margin)(voffset2+scroll_margin)inletadd_h=min(offset+voffset2)add_hinletdh,i_first=addup_entriesll~start:(ll.first-1)~height:add_hdirectioninletroom',_=compute_room~height:ll.rendered_heightll(i_first+1)directionin(* Avar.set (Var.get ll.offset) o; *)(* redundant with tvar... *)letnew_voffset=voffset2-dhinupdate_voffsetcontainer(new_voffset-voffset);Var.releaseLayout.(container.geometry.voffset);ll.container_voffset<-new_voffset;room'endelsebeginletdirection=Downinletexcess_below=(h-voffset2+scroll_margin-Layout.heightroom)in(* TODO should we make sure we don't exceed ll_height ? but this
should be taken care of by addup_entries *)letdh_min,_=addup_entriesll~start:(ll.last+1)~height:excess_belowdirectioninletwanted_dh=maxdh_min((ll.rendered_height-h)/2-scroll_margin)in(* since we want to add wanted_dh pixels below, we need to remove
approx same amout above: so we need to compute the new ll.first *)letdh,i_first=addup_entriesll~start:ll.first~height:wanted_dhdirectioninletroom',_=compute_room~height:ll.rendered_heightlli_firstdirectionin(* Avar.set (Var.get ll.offset) o; *)letnew_voffset=voffset2+dhinupdate_voffsetcontainer(new_voffset-voffset);Var.releaseLayout.(container.geometry.voffset);ll.container_voffset<-new_voffset;room'endinprintddebug_graphics"Room for Long_list is replaced with new range [%d,%d]"ll.firstll.last;(* finally we replace the old room by the new one *)letactive_bg'=Widget.empty~w:(Layout.widthcontainer)~h:(Layout.heightroom')()in(* TODO we could also keed the old active_bg and just change its
size... *)(* Remark: don't use kill_rooms on room or container, because it would
also kill the entries that are kept in the ll.array. *)(* We replace rooms immediately, not waiting for sync, because the
slider will likely call again this function before rendering
(rendering a slider involves a call to the Tvar), and then it should
have the new room. Otherwise we sometimes have artifacts when the old
room interferes with the new one (and some entries are displayed on
top of each other, probably because their geometry is not updated
correctly). The problem is that the scrollbar is on the right, so it
is naturally rendered *after* the room... too bad *)Layout.(set_rooms~sync:falsecontainer[superpose[residentactive_bg';room']]);Layout.fix_contentcontainer;Layout.set_heightcontainerh;(* Sync.push (fun () -> Layout.detach room; Layout.kill room); *)(* ne sert à rien ? et en plus fait bugguer board.mouse_focus *)Layout.removeroom;Layout.removeactive_bg;List.iterLayout.send_to_cemetery[room;active_bg];(* TODO the house of room should also be killed (removed from the table)
and the board should be notified (event?) to make sure the room is
not selected as focus -- like for mouse wheel scrolling. *)end)letcreate~w~h~length?(first=0)~generate?height_fn?(cleanup=Layout.delete_textures)?max_memory?(linear=true)?(scrollbar_width=10)()=letwh=Theme.((scale_intw)*(scale_int(h+2*scroll_margin)))in(* Now some memory computations... TODO they are not completely correct,
because they assume that all generated layouts will have same
width... which is desirable but not enforced a this point. As a rule of
thumb, the minimal factor should be 2, ie the memory should be enough to
store 2x the size of the display. *)letmax_memory=map_optionmax_memory(funmm->ifmm<2*whthen(printddebug_error"Memory for long_list is insufficient; taking %u instead"(2*wh);2*wh)elsemm)inletrendered_height=(* it is crucial that max_memory be enough to fill the rendered_height +
possible extra due to the fact we render an integer number of entries. As a
rule of thumb we take 2/3 of the "theoretical height". *)(* TODO verify if scroll_margin should be taken into account *)matchmax_memorywith|None->2*factor*h/3|Somemm->ifmm<wh*factorthen(printddebug_memory"Memory for this long_list should be at least %u for smoother behaviour"(wh*factor);Theme.(unscale_int(2*mm/(3*scale_intw))))else2*factor*h/3inletno_height_fn_provided=(height_fn=None)inletheight_fn=defaultheight_fn(fun_->None)inletheights=Array.initlengthheight_fninletcomputed_height,computed=ifno_height_fn_providedthen0,0elseletrecloopicomph=ifi>=lengththenh,compelseletcomp',h'=matchheight_fniwith|None->comp,h|Somey->(comp+1),(h+y)inloop(i+1)comp'h'inloop000in(*let box = Widget.box ~w ~h ~background:(Box.Solid Draw.none) () in *)(*let dummy_room = Layout.resident box in*)letll={total_height=ifcomputed=lengththenSomecomputed_heightelseNone;computed_height;length;offset=Var.create(Avar.var0);computed;rendered_height;generate;cleanup;max_memory;used_memory=0;array=Array.makelengthVoid;linear;first;last=first;first_mem=0;last_mem=0;container_voffset=0;heights;(* or Array.make length None, if it takes too long *)}inletroom,i_final=compute_room~height:rendered_heightllfirstDowninletll_height=total_heightllinprintddebug_memory"Long list of height %d was initialized with %d entries (%d..%d) ouf of %d \
and height=%d, rendered_height=%d, approx. total height is %d"h(i_final+1-first)firsti_finalll.length(Layout.heightroom)ll.rendered_heightll_height;(* cf comments in Layout.clip *)letactive_bg=Widget.empty~w~h:(Layout.heightroom)()inletcontainer=Layout.(tower~name:"long_list container"~sep:0~hmargin:0~vmargin:0[superpose[resident~name:"active_bg"active_bg;room]])inLayout.fix_contentcontainer;Layout.set_sizecontainer(w,h);Layout.set_clipcontainer;(* this allows the mouse wheel to change the
container.voffset *)ifh>=ll_height&&i_final=ll.length-1thencontainer(* no need for scrollbar *)elsebeginletclicked_value=refNone(* TODO protect this *)inletsteps=maxll.lengthhin(* TODO can do better, taking tick size into
account *)letvar=Tvar.createll.offset(* the var for the scrollbar (slider) *)~t_from:(* from offset we set slider new position *)(funv->leto=Avar.getvin(* here we just have to verify if the user
did a mouse wheel scroll... *)(* TODO it would be better not to call update_room
each time we want the value of this var. On the
other hand now I have modified slider.ml to
reduce the number of calls. *)update_roomllcontainero;lettt_height=total_heightllinleto_new=Avar.getvinsteps-round(float(steps*o_new)/.(float(tt_height-h))))~t_to:(* from slider position we compute the offset *)(funs->letlf=floatstepsinletss=iflinearthenlf-.floatselsematch!clicked_valuewith|None->lf-.floats|Somecv->letx0=1.-.floatcv/.lfinSlider.slow4lfx0(1.-.floats/.lf)inlettt_height=total_heightllinleto=round(float(tt_height-h)*.(ss)/.lf)inupdate_roomllcontainero;Avar.varo)in(* note that in the definition of this Tvar the container is a global
variable. Thus it should not be destroyed. However it should be ok to
modify its contents *)letslider=Widget.slider~kind:Slider.Vertical~length:h~step:1~thickness:scrollbar_width~tick_size:(maxmin_tick_size((h*h)/ll_height))~varstepsinleton_clicksl__=clicked_value:=Slider.clicked_value(Widget.get_slidersl)inletc=Widget.connect_mainsliderslideron_clickTrigger.buttons_downinWidget.add_connectionsliderc;leton_release___=clicked_value:=Noneinletc2=Widget.connect_mainsliderslideron_releaseTrigger.buttons_upinWidget.add_connectionsliderc2;letbar=Layout.(resident~background:(color_bgDraw.scrollbar_color)slider)inLayout.(flat~name:"long_list"~sep:0~hmargin:0~vmargin:0[container;bar])end