123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934(* This file is part of BOGUE, by San Vu Ngoc *)(* 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
b_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) | |
A | | | | | ll.offset >0
| | | | |
v | | ________________ |
| | | | | ^ ^
| | | | | | | scroll_margin
| | |--- ---| | | v
| | | layout | | | container.geometry.voffset <0
| | | (room+screen) | | |
| | | | v v
| | | _______________
ll.height | | | |(0,0) | 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
+ ll.offset takes values between 0 (included) and ll.height - containrer height (included)
A = ll.offset + voffset
*)(*
Because total height is not known a priori, one has to be very careful when
directly jumping to last entry (clicking on the bottom of the slider)
now the expected behaviour is:
+ If we went too far; then we adjust the voffset and the last entry should show
up at the very bottom of the container.
+ If we didn't reach the bottom by clicking on the bottom of the slider, we stay
at the reached position, but we adjust the slider to show that there is some
more room left to visit below.
*)openTsdlopenB_utilsmoduleLayout=B_layoutmoduleWidget=B_widgetmoduleAvar=B_avarmoduleRGBA=B_rgbamoduleTheme=B_thememoduleTime=B_timemoduleVar=B_varmoduleTvar=B_tvarmoduleTrigger=B_triggermoduleDraw=B_drawmoduleSlider=B_slidermoduleSync=B_syncmoduleUpdate=B_updatetypeentry=|Void|Freed|ComputedofLayout.ttypedirection=|Up|Downletfactor=5(* Texture memory for functioning is roughly [factor] times the memory of the
visible texture. It must be > 3/2. Ensures 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 Theme.scale *)typeinternal={length:int;(* total number of elements (= rows, entries). *)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 that have been
present at some point in the current room. *)offset:(intAvar.t)Var.t;(* = the starting vertical position we want to see onscreen. 0 means first
entry on top. *)mutablecomputed:int;(* = number of already generated entries (even if they have been freed
afterward). *)mutablemin_rendering_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 ieth 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²). It
should be at least twice the area of the visible texture, and also twice
the area of the largest entry. *)mutableused_memory:int;array:entryarray;(* We store all the computed layouts in an array, and free them when they are
not used anymore to monitor memory footprint. 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) *)(* MUTABLE room : Layout.t; *)(* NOT USED mais ça pourrait être pratique*)(* = 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.*)mutablewidth_warning:bool;(* Record if a width warning has been sent, see [check_width]. It will be
reset to false if the width becomes ok again. *)scale_width:bool(* If [scale_width] is true, the width of the entries follow the width of the
main layout. *)}typet={layout:Layout.t;slider:Widget.t;regenerate:unit->unit;ll:internal}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 when [ll.max_memory] is not None"inletrecloopjnext=ifj<0||j>=ll.lengththenprintd(debug_error+debug_memory)"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 Long_list 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;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))(* What is the minimal height (mh) that we should really render to the [room]
layout? Here are some explanations.
1. mh should be at least the height of the container (h) (container = visible
part, see the sketch above) plus twice the [scroll_margin]:
mh >= h + 2*scroll_margin
otherwise the room will be constantly updated.
2. When we scroll to the end of the room, as soon as we reach the bottom
margin, we update the room, and place the current entry roughly in the middle
of the room, see [update_room]. The current entry finds itself at the bottom
of the container, and has roughly half of the rendering height above
it. Therefore, this half should be no less than h + scroll_margin:
mh/2 >= h + scroll_margin
This gives mh >= 2*h + 2*scroll_margin, so this is strictly stronger than the
first constraint 1. above, which we may hence forget.
3. For safety, and also for reducing the number of calls to [update_room]
(for instance: in order to get a fluid scrolling), it is good to be far above
the constraint 2. Hence we introduce a [factor] variable and decide to use a
video memory equal to [factor] times the area of the visible container. This
means roughly that the rendering height will be [factor] times [h], but not
quite: the memory should also keep spare bits for possible extra due to the
fact we render an integer number of entries. This extra is a most twice the
maximum height of the entries (one for the first displayed entry, one for the
last displayed entry). But, we don't know this height... unless we scan all
entries, which we don't want to do. So, as a rule of thumb, we reserve 1/3
memory for the extra: we take mh to be 2/3 of the "theoretical height"
[factor * h].
In view of 2., we need [2 * factor * h / 3 >= 2*h + 2*scroll_margin], so
factor >= 3 * (1 + scroll_margin/h)
For standard layouts, scroll_margin<h, so factor=6 is good. In problematic
cases, Instead of modifying [factor] we simply take [mh] to be the max of the
two constraints.
4. When [max_memory] is hinted by the user, we need to compute the
corresponding factor, which we call then [x]. *)letcompute_min_rendering_heightll(w,h)=assert(w*h<>0);letmh=matchll.max_memorywith|SomemmwhenTheme.scale_was_init()->letcontainer_area=Theme.((scale_intw)*(scale_inth))+1inletx=floatmm/.floatcontainer_areain(* the required memory contains [x] times the container; So [x] is the
custom [factor]. *)letmin_factor=3.*.(1.+.floatscroll_margin/.floath)inifx<min_factorthenprintd(debug_error+debug_memory+debug_user)"[max_memory=%i] for Long_list is too small; we need at least %i"mm(round(min_factor*.floatcontainer_area));letx=maxxmin_factorinround(2.*.x*.(floath)/.3.)|_->2*factor*h/3inletmh=imax(2*scroll_margin+2*h+2)mhinprintddebug_memory"Long_list [min_rendering_height] = %i (h=%i, w=%i)"mhhw;mh(* Get ieth entry; if it was already computed, we return it but also detach it
from its house. *)(* TODO: not thread safe *)letgetllidirection=matchll.array.(i)with|Computedl->ifnot(Layout.is_detachedl)thenLayout.detachl;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(* Compute entries until reaching at most the given height. Update
ll.first/last. Return the room and the index of last generated entry. Note
that the width of the room may vary with i_start. Note that contrary to
[addup_entries], [i_start] is really the first (smallest) index; here,
[direction] is only used for memory management.
WARNING: this modifies the previous [room]! Because already computed entries
will be detached from [room] to be placed in the the one.
Warning, the [room] height should never be modified after creation! *)letcompute_room~height~widthlli_startdirection=printddebug_custom"[Long_list.compute_room] start = %i"i_start;ifll.length>0thenbeginassert(i_start>=0);ll.first<-i_start;(* print "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=getllidirectionindo_optionwidth(Layout.set_widthline);letdh=Layout.heightlineinloop(i+1)~h:(h+dh)(line::list)endinletlist,i_final=loopi_start~h:0[]inletroom=Layout.tower~name:(Printf.sprintf"long_list room %i"i_start)~margins:0listinif!debugthenassert(ll.last=i_final);room,i_finalendelsebeginprintd(debug_warning+debug_user)"Long_list is empty.";Layout.empty~w:0~h:0(),-1endletadd_heights_NO~first~lastlldirection=letheights=ll.heightsinletrecloopih=ifi>lastthenhelseletdh=matchheights.(i)with|None->Layout.height(getllidirection)|Somedh->dhinloop(i+1)(h+dh)inloopfirst0(* 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 integer
after the last looked-up entry (next means + ou - 1 depending on
[direction]). *)(* 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=assert(height>=0);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=loopstart0inifh<heightthenprintddebug_warning"Long_list: [addup_entries] bottom reached before desired height.";if!slowthenSdl.set_cursor!cursor;printddebug_memory"Long_list ADDUP dir=%s start=%d height=%d ==> h=%u, i=%d"(to_strdirection)startheighthi;h,iletshift_voffsetcontainerdv=ifdv<>0(* : this test is important because shift_offset creates a new animation... *)thenLayout.shift_voffsetcontainerdvletcheck_widthllwroom=matchll.width_warning,Layout.widthroom>wwith|true,true->()|false,true->printddebug_user"Long_list rows are larger than the room width.";ll.width_warning<-true|true,false->ll.width_warning<-false|false,false->()(* Given the required new value offset [o] for of [ll.offset] we do all the
necessary side-effects: changing the container voffset and possibly compute a
new room. Note that even if [o] does not change, the height of the container
way have been modified. *)letupdate_room?(force=false)llcontainero=letscrolling,room=letopenLayoutinmatchcontainer.contentwith|Rooms[scrolling]->(matchscrolling.contentwith|Rooms[_active_bg;room]->scrolling,room|_->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_heightllin(* Var.protect Layout.(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()->(* We compute the required [voffset] according to [o]: if [o] increases,
then voffset should decrease (remember: voffset<=0) *)(* In case of equality voffset2 = 0 or voffset2 = - height of room , one
should still do the room update (except at very top or bottom of
list), in order to allow mouse wheel scroll to go past the computed
room. This is the role of [scroll_margin]. *)letvoffset2=voffset+offset-oinifnotforce&&((voffset2+scroll_margin<0)(* top margin is still enough *)||(ll.first=0)(* this is the first entry of the list *))&&((h-voffset2<Layout.heightroom-scroll_margin)(* bottom margin is still enough *)||(ll.last=ll.length-1)(* last entry of the list *))thenbegin(* the room is still usable *)printddebug_custom"Long_list: room still usable, o=%i"o;shift_voffsetcontainer(voffset2-voffset);(* = offset - o *)(* ==> the new value of the container voffset is voffset2 *)(* Var.release Layout.(container.geometry.voffset); *)ll.container_voffset<-voffset2;check_widthll(Layout.widthcontainer)roomendelsebeginletwidth=ifll.scale_widththenSome(Layout.widthscrolling)elseNoneinletroom2=(* need to compute a new room *)(* print_string "NEW ROOM"; *)printddebug_memory"Update Long_list [%d,%d] => newoffset=%d oldoffset=%d voffset=%d \
voffset2=%d (approx)height=%d, min_rendering_height=%d, \
room.height=%d, MEM=[%d,%d] "ll.firstll.lastooffsetvoffsetvoffset2ll_heightll.min_rendering_height(Layout.heightroom)ll.first_memll.last_mem;ifvoffset2+scroll_margin>=0&&ll.first>0thenbegin(* we need to compute upwards *)(* print_endline "==>UP"; *)letdirection=Upinletadd_h=imax(* how many pixels to add above the room *)((ll.min_rendering_height-h)/2-scroll_margin)(* : we try to keep in the middle of min_rendering_height *)(voffset2+scroll_margin)(* : if >0, this is how far we are above the top margin *)inletadd_h=min(offset+voffset2)add_hin(* we cannot ask for more than the distance to the first entry (=A
in the sketch) *)(* dh is the exact number of pixels that we finally add: *)letdh,i_first=addup_entriesll~start:(ll.first-1)~height:add_hdirectioninletroom',_=compute_room~width~height:ll.min_rendering_heightll(i_first+1)directionin(* Avar.set (Var.get ll.offset) o; *)(* redundant with tvar... *)letnew_voffset=voffset2-dhinshift_voffsetcontainer(new_voffset-voffset);(* Var.release Layout.(container.geometry.voffset); *)ll.container_voffset<-new_voffset;room'endelsebegin(* print_endline "==>DOWN"; *)letdirection=Downinletadd_h=(* height between bottom margin of [room] and bottom of
[container]. If >0, this means that the container is too low
(below the margin). *)h-voffset2-Layout.heightroom+scroll_marginin(* Here we don't check that we don't exceed [ll_height], because
this should be taken care of by [addup_entries]. *)letadd_h=imaxadd_h((ll.min_rendering_height-h)/2-scroll_margin)in(* : we try to keep in the middle of min_rendering_height *)(* This determines our new tentative last entry: *)lethdown,i_last=addup_entriesll~start:(ll.last+1)~height:add_hdirectionin(* Now we compute the new [ll.first]: *)lethup,i_first=addup_entriesll~start:(i_last-1)~height:ll.min_rendering_heightUpin(* dh is the exact number of pixels that we finally add above: *)letdh=hup-Layout.heightroom-hdowninletroom',_=compute_room~width~height:hupll(i_first+1)directionin(* Avar.set (Var.get ll.offset) o; *)letnew_voffset=voffset2-dhinshift_voffsetcontainer(new_voffset-voffset);(* Var.release Layout.(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;(* if !debug then printd debug_custom "Before replace: %s" *)(* (string_of_option B_print.layout_down (Layout.get_house room)); *)(* Finally we replace the old room by the new one: *)Layout.(set_height~keep_resize:truescrolling(heightroom2));assert(Layout.replace_room~by:room2room);(* We immediately update the entry positions so that a new mouse focus
can be detected without waiting another frame after redraw: *)Layout.update_current_geomcontainer;(* if !debug then printd debug_custom "After replace: %s" *)(* (string_of_option B_print.layout_down (Layout.get_house room2)); *)ifwidth=Nonethenbegincheck_widthll(Layout.widthcontainer)room2;Layout.disable_resizeroom2endelseLayout.resize_follow_widthroom2;Layout.removeroom;Layout.send_to_cemeteryroom(* 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 *)end)(* [free_all] forces recomputing all layouts and heights.
not widely tested... be careful *)(* Remarque: pour le fun on pourrait ne libérer que certains et les autres on les
anime pour retourner à leur place, haha *)(* TODO mettre à jour computed_height et computed *)letfree_allcontainerll=ifll.length>0thenbeginll.used_memory<-0;leta=ll.arrayinfori=0toll.length-1doa.(i)<-Voiddone;ll.first_mem<-0;ll.last_mem<-0;update_room~force:truellcontainer(Avar.get(Var.getll.offset))end(* [dummy_clip] is used for an empty list. The (dummy) slider needs to be part
of the (dummy) layout for the refresh function to operate. *)letdummy_clip(w,h)=letslider=Widget.slider0inletdum=Layout.resident~w~hsliderinLayout.set_showdumfalse;Layout.hide~duration:0dum;dum,dum,slider(* A variant of Layout.make_clip with virtual height and optional nonlinear
slider. [steps] is the minimal number of steps that the slider should
have. [bottom_reached] is true if the provided room contains the bottom of
the virtual room. The scrollbar is added to the right, but inside the
required [w] width. (Hence it may cover the right part of the room content.)
If [scale_width] is true, all entries will be stretched to the desired width
[w]. *)letmake_clip?name~w~h~scrollbar_widthllroom=check_widthllwroom;letmoduleL=Layoutin(* cf comments in Layout.clip *)(* let background = L.color_bg Draw.(set_alpha 40 red) in (\* DEBUG *\) *)letactive_bg=Widget.empty~w~h:(L.heightroom)()|>L.resident~name:"active_bg"(* ~background *)inletscrolling=L.superpose~name:"scrolling"~w[active_bg;room]inifll.scale_widththenL.resize_follow_widthroomelseL.disable_resizeroom;(* set this via a parameter? Scaling the width could be interesting, but it's
difficult because a priori we don't know what is the max width of
entries. *)letcontainer=L.(tower~name:"long_list container"~clip:true~margins:0[scrolling])inL.set_size~keep_resize:truecontainer~w~h;(* Because of [clip:true] when creating [container], the [resize] field of
[scrolling] is not automatically created. *)L.resize_follow_widthscrolling;letll_height=total_heightllinletclicked_value=refNoneinlettick_size=maxmin_tick_size((h*h)/ll_height)inletsteps=imaxll.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(* print "FROM o=%i" o; *)(* 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.getvinleth=L.heightcontainerin(* may change in case of resize *)steps-round(float(steps*o_new)/.(float(tt_height-h))))~t_to:(* from slider position we compute the offset *)(funs->(* print_endline "TO"; *)letlf=floatstepsinletss=ifll.linearthenlf-.floatselsematch!clicked_valuewith|None->lf-.floats|Somecv->letx0=1.-.floatcv/.lfinSlider.slow4lfx0(1.-.floats/.lf)inlettt_height=total_heightllinleth=L.heightcontainerinleto=imax0(round(float(tt_height-h)*.ss/.lf))inupdate_roomllcontainero;(* now [total_height ll] may have a better precision *)(* we re-update in case we went too far. *)leto2=imin(total_heightll-h)oinifo<>o2thenbegin(* o > o2 *)printddebug_custom"Long_list: slider went too far; o=%i o2=%i h=%i"oo2h;shift_voffsetcontainer(o-o2);ll.container_voffset<-ll.container_voffset+o-o2end;Avar.varo2)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~varstepsinifnotll.linearthenbeginleton_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_connectionsliderc2end;letbar=L.(resident~name:"bar"~background:(color_bgRGBA.scrollbar_color)slider)inletname=defaultname"long_list"inletlayout=L.(superpose~name[container;bar])inL.disable_resizebar;container.resize<-(fun(w,hh)->(* size of the long_list layout *)letopenLinletopenResizeinletth=total_heightllinleth=iminhhthinset_heightbarhh;set_sizecontainer~w~h;setxbar(w-widthbar);ll.min_rendering_height<-compute_min_rendering_heightll(w,h);lets=Tvar.getvarin(* this can trigger update_room (too much?) *)(* print "slider=%i, height=%i" s (total_height ll); *)letsli=Widget.get_slidersliderinifs<0thenSlider.setsli0;(* one could simply use Tvar.set var 0 *)ifh<>hhthenbegin(* hh > ll.total_height: we display everything and remove the bar *)set_voffsetcontainer0;(* we don't use not Tvar for setting voffset because the bar will be
removed and hence won't look up Tvar. *)ifis_shownbarthenrec_set_showfalsebar;(* set_width container w *)endelsebeginSlider.set_tick_sizesli(imaxmin_tick_size(h*h/th));ifnot(is_shownbar)thenrec_set_showtruebar;set_widthcontainer(imax0(w-widthbar));end);container.resize(w,h);(* this takes care of show/hide bar at startup *)(* if !debug then printd debug_custom "At creation: %s" *)(* (string_of_option B_print.layout_down (Layout.get_house room)); *)container,layout,sliderletpixel_areawh=Theme.((scale_intw)*(scale_int(h+2*scroll_margin)))(* Increase max_memory if necessary; no decrease. *)letadjust_max_memory~w~h=function|None->None|Somemm->letwh=pixel_areawhinifmm<wh*factorthen(printd(debug_user+debug_memory)"Memory for this long_list should be at least %u for smoother \
behaviour"(wh*factor);Some(wh*factor))elseSomemm(* Public interface *)letcreate?name~w~h~length?(first=0)~generate?height_fn?(cleanup=Layout.delete_textures)?max_memory?(linear=true)?(scrollbar_width=10)?(scale_width=false)():t=letlength=iflength>=0thenlengthelsebeginprintd(debug_error+debug_user)"[Long_list.create]: the [length] argument should be non negative, got \
[%i] instead."length;0endinleth=ifh>0thenhelsebeginprintd(debug_error+debug_user)"Long_list height should be positive, got [%i] instead."h;10endinletw=ifw>0thenwelsebeginprintd(debug_error+debug_user)"Long_list width should be positive, got [%i] instead."w;20endin(* 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=adjust_max_memory~h~wmax_memoryinletmin_rendering_height=2*factor*h/3in(* will be updated below *)letno_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*)letfirst=iffirst<length&&first>=0thenfirstelsebeginiflength<>0thenprintd(debug_error+debug_user)"[Long_list.create]: the [first] argument should be between 0 and \
[length-1], got [%i] instead."first;iffirst<0then0elselength-1(* so length = 0 && first = -1 is "valid" *)endinletll={total_height=ifcomputed=lengththenSomecomputed_heightelseNone;computed_height;length;offset=Var.create(Avar.var0);computed;min_rendering_height;generate;cleanup;max_memory;used_memory=0;array=Array.makelengthVoid;(* or Array.make length None, if it takes too long *)linear;first;last=first;first_mem=0;last_mem=0;container_voffset=0;heights;width_warning=false;scale_width}inSync.push(fun()->(* We sync this because of Theme.scale. Most of the time this is not
necessary because creation of room has already initialized Video, and
[min_rendering_height] is already set by the call to [container.resize]
above. *)ll.min_rendering_height<-compute_min_rendering_heightll(w,h));letwidth=ifscale_widththenSomewelseNoneinletroom,i_final=compute_room~height:min_rendering_height~widthllfirstDowninletll_height=total_heightllinprintd(debug_memory+debug_board)"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.min_rendering_heightll_height;letcontainer,layout,slider=iflength>0thenmake_clip?name~w~h~scrollbar_widthllroomelsedummy_clip(w,h)inletregenerate()=free_allcontainerllin{regenerate;layout;slider;ll}letget_layoutt=t.layoutletredrawt=Update.pusht.sliderletregeneratet=t.regenerate()letcreate_layout?name~w~h~length?(first=0)~generate?height_fn?(cleanup=Layout.delete_textures)?max_memory?(linear=true)?(scrollbar_width=10)?(scale_width=false)()=get_layout(create?name~w~h~length~first~generate?height_fn~cleanup?max_memory~linear~scrollbar_width~scale_width())(** Return the maximal value of the scrollbar attached to the Long_list (if any). *)letget_scroll_stepst=Slider.get_max(Widget.get_slidert.slider)letget_scroll_valuet=lets=Widget.get_slidert.sliderinSlider.update_values;letv=Slider.valuesinprintddebug_custom"Scroll value=%i, ll.offset=%i"v(Avar.get(Var.gett.ll.offset));vletset_scroll_valuetv=letoldo=(Avar.get(Var.gett.ll.offset))inSlider.set(Widget.get_slidert.slider)v;printddebug_custom"Set_scroll %i, old offset=%i, new_offset=%i"voldo(Avar.get(Var.gett.ll.offset));redrawtletget_scrollt=(* percentage *)1.-.float(get_scroll_valuet)/.(float(get_scroll_stepst))letset_scrolltx=set_scroll_valuet(round((1.-.x)*.(float(get_scroll_stepst))))letiter_computed_layoutsft=fori=t.first_memtot.last_memdomatcht.array.(i)with|Computedroom->firoom|_->()done