Skip to main content

ui/
vlist.rs

1// vlist -- lists whose rows have different heights, virtualized: only the rows in view are
2// built, laid out and drawn, for any number of rows
3//
4//   ui.vlist_begin(id, &mut VListState, count) -> (first, end)   the rows to emit, first ..
5//        end - 1 (the view takes its place like a box: natural 320 x 240 DIP, next_size(-1, -1)
6//        fills)
7//   ui.vlist_item_begin(i) -> EV       row i: the style's list row (hover, selection, focus)
8//        and a BOX_V of the list's width at the row's place; the box's natural height is the
9//        row's height (measured every pass it is built, unless st.set_height gave one).
10//        EV_ACT clicked, EV_DOUBLE, EV_RCLICK
11//   ui.vlist_item_end()
12//   ui.vlist_end(&mut VListState) -> EV   EV_ACT the selection changed (a click, the keys),
13//        EV_DOUBLE Enter or a double click, EV_FOCUS, EV_HOVER
14//   ui.vlist_sel() -> i32               the selection while the rows are emitted
15//   VListState::new() / sel (-1: none) / estimate (DIP of a row not measured yet; 0: 40, then
16//        the mean of the measured rows) / set_height(i, dip) (0: measure again) /
17//        invalidate(i) / invalidate_all() / insert(at, n) / remove(at, n) (rows above the view:
18//        it keeps showing the same rows) / reveal(i) / scroll_to(i) / first_visible() ->
19//        (row, px of it above the view) / row_height(i) (px, measured or estimated) /
20//        row_known(i) / set_count(n) (vlist_begin does it) / scroll_pos() / set_scroll_pos(px)
21//        / total_height() (px; rows not measured count with the estimate)
22//
23// Scrolling is anchored at the first visible row (its index and how far it is scrolled out):
24// rows above it may change their height, come or go without moving what is shown.  The
25// heights sit in Fenwick trees (the heights known and how many are known; the others count
26// with the estimate), so a position and the row at a position cost O(log n); the scroll bar
27// shows the anchor's position in the total.  Keys (focused): Up / Down, PgUp / PgDn (a view of
28// rows), Home / End move the selection and scroll it into view (exactly, once the rows on the
29// way are measured), Enter: EV_DOUBLE.  The wheel scrolls (smoothly unless animations are
30// off), the scroll bar drags.  A change of dpi, width or text scale measures every row again.
31// Ids: id the view (focus; K_LIST: value sel, max count), id + 1 + i the rows (K_LISTITEM,
32// value i), (id + 1 + i) | 0xC000_0000 their content boxes, part_id(id, 1) the scroll bar.
33//
34// Rows (crate): the height cache and anchor on their own -- the Markdown viewer (md_view.rs)
35// scrolls its blocks with it.
36
37use alloc::vec;
38use alloc::vec::Vec;
39
40use crate::consts::*;
41use crate::keys::*;
42use crate::list::{anim_decay, anim_steps};
43use crate::value::part_id;
44use crate::{Sp, Ui};
45
46const NAT_W: i32 = 320;
47const NAT_H: i32 = 240;
48const EST_DIP: i32 = 40;
49const WHEEL_DIP: i32 = 48;
50const REVEAL_TRIES: u8 = 8;
51
52// ------------------------------------------------------------------ the height cache
53
54/// heights of rows and the scroll anchor (see the module header)
55#[derive(Clone, Default)]
56pub(crate) struct Rows {
57    // per row: > 0 a known height (px), < 0 an estimate of its own (-px), 0 the common estimate
58    h: Vec<i32>,
59    // Fenwick trees, 1-based: the heights given (|h|), how many are given
60    sum: Vec<i64>,
61    cnt: Vec<i32>,
62    /// px of a row without a height of its own
63    pub(crate) est: i32,
64    /// the anchor: the first visible row and how many px of it are above the view
65    pub(crate) top: usize,
66    pub(crate) off: i32,
67}
68
69fn val(v: i32) -> (i64, i32) {
70    if v == 0 { (0, 0) } else { (v.unsigned_abs() as i64, 1) }
71}
72
73impl Rows {
74    pub(crate) fn len(&self) -> usize {
75        self.h.len()
76    }
77
78    fn est(&self) -> i64 {
79        self.est.max(1) as i64
80    }
81
82    fn rebuild(&mut self) {
83        let n = self.h.len();
84        self.sum = vec![0; n + 1];
85        self.cnt = vec![0; n + 1];
86        for i in 1..=n {
87            let (s, c) = val(self.h[i - 1]);
88            self.sum[i] += s;
89            self.cnt[i] += c;
90            let j = i + (i & i.wrapping_neg());
91            if j <= n {
92                self.sum[j] += self.sum[i];
93                self.cnt[j] += self.cnt[i];
94            }
95        }
96    }
97
98    fn add(&mut self, i: usize, ds: i64, dc: i32) {
99        let mut k = i + 1;
100        while k < self.sum.len() {
101            self.sum[k] += ds;
102            self.cnt[k] += dc;
103            k += k & k.wrapping_neg();
104        }
105    }
106
107    fn put(&mut self, i: usize, v: i32) -> bool {
108        let Some(&old) = self.h.get(i) else { return false };
109        if old == v {
110            return false;
111        }
112        let (s0, c0) = val(old);
113        let (s1, c1) = val(v);
114        self.h[i] = v;
115        self.add(i, s1 - s0, c1 - c0);
116        true
117    }
118
119    /// the number of rows (new ones at the end, dropped ones from the end)
120    pub(crate) fn resize(&mut self, n: usize) {
121        if n == self.h.len() && self.sum.len() == n + 1 {
122            return;
123        }
124        self.h.resize(n, 0);
125        self.rebuild();
126        if self.top >= n {
127            self.top = n.saturating_sub(1);
128            self.off = 0;
129        }
130    }
131
132    /// the height of row i (px): known, estimated or the common estimate
133    pub(crate) fn height(&self, i: usize) -> i32 {
134        match self.h.get(i) {
135            Some(&v) if v != 0 => v.abs(),
136            _ => self.est.max(1),
137        }
138    }
139
140    /// row i's height is known (measured or given)
141    pub(crate) fn known(&self, i: usize) -> bool {
142        self.h.get(i).is_some_and(|&v| v > 0)
143    }
144
145    /// a known height -> it changed
146    pub(crate) fn set(&mut self, i: usize, h: i32) -> bool {
147        self.put(i, h.max(1))
148    }
149
150    /// an estimate of row i's own (kept until its height is known)
151    pub(crate) fn guess(&mut self, i: usize, h: i32) {
152        if self.h.get(i).is_some_and(|&v| v <= 0) {
153            self.put(i, -h.max(1));
154        }
155    }
156
157    /// row i is measured again
158    pub(crate) fn forget(&mut self, i: usize) {
159        self.put(i, 0);
160    }
161
162    /// every row is measured again
163    pub(crate) fn forget_all(&mut self) {
164        self.h.iter_mut().for_each(|v| *v = 0);
165        self.rebuild();
166    }
167
168    /// the mean of the known heights (None: none known)
169    pub(crate) fn mean(&self) -> Option<i32> {
170        let n = self.h.len();
171        let (mut s, mut c, mut k) = (0i64, 0i64, n);
172        while k > 0 {
173            s += self.sum[k];
174            c += self.cnt[k] as i64;
175            k &= k - 1;
176        }
177        (c > 0).then(|| (s / c).clamp(1, i32::MAX as i64) as i32)
178    }
179
180    /// the height of rows 0 .. i
181    pub(crate) fn prefix(&self, i: usize) -> i64 {
182        let i = i.min(self.h.len());
183        let (mut s, mut c, mut k) = (0i64, 0i64, i);
184        while k > 0 {
185            s += self.sum[k];
186            c += self.cnt[k] as i64;
187            k &= k - 1;
188        }
189        s + (i as i64 - c) * self.est()
190    }
191
192    /// the height of every row
193    pub(crate) fn total(&self) -> i64 {
194        self.prefix(self.h.len())
195    }
196
197    /// the row at y (px from the first row's top, clamped) and y's offset into it
198    pub(crate) fn find(&self, y: i64) -> (usize, i32) {
199        let n = self.h.len();
200        if n == 0 {
201            return (0, 0);
202        }
203        let est = self.est();
204        let (mut pos, mut rem) = (0usize, y.max(0));
205        let mut step = 1usize << (usize::BITS - 1 - n.leading_zeros());
206        while step > 0 {
207            let k = pos + step;
208            if k <= n {
209                let v = self.sum[k] + (step as i64 - self.cnt[k] as i64) * est;
210                if v <= rem {
211                    pos = k;
212                    rem -= v;
213                }
214            }
215            step >>= 1;
216        }
217        if pos >= n { (n - 1, self.height(n - 1)) } else { (pos, rem.min(i32::MAX as i64) as i32) }
218    }
219
220    /// the scroll position: px from the first row's top to the view's top
221    pub(crate) fn pos(&self) -> i64 {
222        self.prefix(self.top) + self.off as i64
223    }
224
225    pub(crate) fn set_pos(&mut self, y: i64) {
226        let (t, o) = self.find(y);
227        self.top = t;
228        self.off = o;
229    }
230
231    /// the anchor inside the rows, the view not beyond the end (view: its height px)
232    pub(crate) fn clamp(&mut self, view: i32) {
233        if self.h.is_empty() {
234            self.top = 0;
235            self.off = 0;
236            return;
237        }
238        let n = self.h.len();
239        if self.top >= n {
240            self.top = n - 1;
241            self.off = 0;
242        }
243        // the anchor walks into the row it is in now (rows measured since it was set must not
244        // move it: it is not taken from the absolute position again)
245        while self.off >= self.height(self.top) && self.top + 1 < n {
246            self.off -= self.height(self.top);
247            self.top += 1;
248        }
249        while self.off < 0 && self.top > 0 {
250            self.top -= 1;
251            self.off += self.height(self.top);
252        }
253        self.off = self.off.clamp(0, self.height(self.top) - 1);
254        let max = (self.total() - view as i64).max(0);
255        if self.pos() > max {
256            self.set_pos(max);
257        }
258    }
259
260    /// n rows inserted before row `at` (the anchor stays on its row)
261    pub(crate) fn insert(&mut self, at: usize, n: usize) {
262        let at = at.min(self.h.len());
263        self.h.splice(at..at, core::iter::repeat_n(0, n));
264        self.rebuild();
265        if at <= self.top && self.h.len() > n {
266            self.top += n;
267        }
268    }
269
270    /// rows at .. at + n removed (the anchor stays on its row; on the first row after them
271    /// when it was one of them)
272    pub(crate) fn remove(&mut self, at: usize, n: usize) {
273        let at = at.min(self.h.len());
274        let end = at.saturating_add(n).min(self.h.len());
275        self.h.drain(at..end);
276        self.rebuild();
277        if self.top >= end {
278            self.top -= end - at;
279        } else if self.top >= at {
280            self.top = at;
281            self.off = 0;
282        }
283        if self.top >= self.h.len() {
284            self.top = self.h.len().saturating_sub(1);
285            self.off = 0;
286        }
287    }
288
289    /// row i fully in a view of `view` px (at the top when it is above or taller than the view)
290    pub(crate) fn reveal(&mut self, i: usize, view: i32) {
291        if i >= self.h.len() {
292            return;
293        }
294        let y = self.prefix(i) - self.pos();
295        let h = self.height(i) as i64;
296        if y < 0 || h >= view as i64 {
297            self.top = i;
298            self.off = 0;
299        } else if y + h > view as i64 {
300            let p = self.prefix(i) + h - view as i64;
301            self.set_pos(p);
302        }
303    }
304}
305
306// ------------------------------------------------------------------ the app's state
307
308/// the state of a list with rows of different heights (see the module header)
309#[derive(Clone)]
310pub struct VListState {
311    /// the selected row, -1 none
312    pub sel: i32,
313    /// the height of a row that was not measured yet (DIP; 0: 40 at first, then the mean of
314    /// the measured rows)
315    pub estimate: i32,
316    rows: Rows,
317    // heights the app gave (DIP; 0: measured)
318    given: Vec<i32>,
319    // dpi, width px, text scale of the measurements
320    key: (i32, i32, u32),
321    // smooth scrolling: px still to go, its clock
322    pending: i32,
323    t0: i32,
324    reveal: Option<(usize, u8)>,
325    // the pointer's y and the bar's position when the scroll bar last moved the view
326    sb_y: i32,
327    sb_pos: i32,
328}
329
330impl Default for VListState {
331    fn default() -> VListState {
332        VListState::new()
333    }
334}
335
336impl VListState {
337    /// no rows yet, nothing selected
338    pub fn new() -> VListState {
339        let rows = Rows { est: EST_DIP, ..Rows::default() };
340        VListState { sel: -1, estimate: 0, rows, given: Vec::new(), key: (0, 0, 0), pending: 0, t0: 0, reveal: None, sb_y: i32::MIN, sb_pos: 0 }
341    }
342
343    fn dpi(&self) -> i32 {
344        if self.key.0 > 0 { self.key.0 } else { 96 }
345    }
346
347    /// row i is `dip` high (the app knows it: its box is not measured); 0 measures it again
348    pub fn set_height(&mut self, i: usize, dip: i32) {
349        if self.given.len() <= i {
350            if dip <= 0 {
351                return;
352            }
353            self.given.resize(i + 1, 0);
354        }
355        self.given[i] = dip.max(0);
356        if i < self.rows.len() {
357            if dip > 0 {
358                let px = crate::layout::dp_at(dip, self.dpi()).max(1);
359                self.rows.set(i, px);
360            } else {
361                self.rows.forget(i);
362            }
363        }
364    }
365
366    /// row i is measured again the next time it is built
367    pub fn invalidate(&mut self, i: usize) {
368        if self.given.get(i).is_none_or(|&g| g == 0) {
369            self.rows.forget(i);
370        }
371    }
372
373    /// every row is measured again
374    pub fn invalidate_all(&mut self) {
375        self.rows.forget_all();
376        self.key.1 = -1;
377    }
378
379    /// n rows were inserted before row `at`: the view keeps showing the rows it showed (also
380    /// the selection stays on its row)
381    pub fn insert(&mut self, at: usize, n: usize) {
382        self.rows.insert(at, n);
383        if self.given.len() > at {
384            self.given.splice(at..at, core::iter::repeat_n(0, n));
385        }
386        if self.sel >= 0 && self.sel as usize >= at {
387            self.sel += n as i32;
388        }
389    }
390
391    /// rows at .. at + n were removed (the selection on one of them: none)
392    pub fn remove(&mut self, at: usize, n: usize) {
393        self.rows.remove(at, n);
394        if self.given.len() > at {
395            let end = at.saturating_add(n).min(self.given.len());
396            self.given.drain(at..end);
397        }
398        if self.sel >= 0 {
399            let s = self.sel as usize;
400            if s >= at.saturating_add(n) {
401                self.sel -= n as i32;
402            } else if s >= at {
403                self.sel = -1;
404            }
405        }
406    }
407
408    /// scroll row i fully into view (the next pass)
409    pub fn reveal(&mut self, i: usize) {
410        self.reveal = Some((i, 0));
411    }
412
413    /// show row i at the top of the view
414    pub fn scroll_to(&mut self, i: usize) {
415        self.rows.top = i;
416        self.rows.off = 0;
417        self.pending = 0;
418        self.reveal = None;
419    }
420
421    /// the first visible row and how many px of it are scrolled out above the view
422    pub fn first_visible(&self) -> (usize, i32) {
423        (self.rows.top, self.rows.off)
424    }
425
426    /// row i's height in px (measured or given; else the estimate)
427    pub fn row_height(&self, i: usize) -> i32 {
428        self.rows.height(i)
429    }
430
431    /// row i's height is measured or given
432    pub fn row_known(&self, i: usize) -> bool {
433        self.rows.known(i)
434    }
435
436    /// the number of rows (vlist_begin sets it from its count; rows come and go at the end)
437    pub fn set_count(&mut self, n: usize) {
438        self.rows.resize(n);
439        for (i, &g) in self.given.iter().enumerate().take(n) {
440            if g > 0 {
441                let px = crate::layout::dp_at(g, self.dpi()).max(1);
442                self.rows.set(i, px);
443            }
444        }
445    }
446
447    /// the scroll position: px from the first row's top to the view's top (rows not measured
448    /// count with the estimate)
449    pub fn scroll_pos(&self) -> i64 {
450        self.rows.pos()
451    }
452
453    /// scroll to a position (px; the next pass keeps the view inside the rows)
454    pub fn set_scroll_pos(&mut self, px: i64) {
455        self.rows.set_pos(px);
456        self.pending = 0;
457        self.reveal = None;
458    }
459
460    /// the height of all rows (px; rows not measured count with the estimate)
461    pub fn total_height(&self) -> i64 {
462        self.rows.total()
463    }
464}
465
466// ------------------------------------------------------------------ the widget
467
468// an open list (between vlist_begin and vlist_end)
469struct Open {
470    id: u32,
471    st: VListState,
472    count: usize,
473    rect: [i32; 4],
474    // the rows' column (px): x, width
475    rx: i32,
476    rw: i32,
477    // the next row's top, the next row expected, the row being built
478    y: i32,
479    next: usize,
480    cur: usize,
481    cur_y: i32,
482    vev: u32,
483    ev: u32,
484    old: i32,
485    kdbl: bool,
486    changed: bool,
487    hit0: usize,
488    layer: u32,
489    // the row to reveal was built: its final top and height
490    shown: Option<(i32, i32)>,
491    // the rows are a scroll copy region (scrollcopy.rs)
492    copy: bool,
493}
494
495/// shared state (Ui::m.vlist): the open lists
496#[derive(Default)]
497pub(crate) struct S {
498    open: Vec<Open>,
499}
500
501impl Ui {
502    /// ui_vlist_begin(id, &mut st, count) -> (first, end): the rows to emit (see the module
503    /// header)
504    #[cfg_attr(jfgui_dev, track_caller)]
505    pub fn vlist_begin(&mut self, id: u32, st: &mut VListState, count: usize) -> (usize, usize) {
506        let _src = self.src_scope();
507        let mut s = core::mem::take(st);
508        let r = self.place(self.dp(NAT_W), self.dp(NAT_H));
509        let sbw = self.style.scrollbar_thickness(self);
510        let view = r[3].max(1);
511        // the measurements belong to a dpi, a width and a text scale (and theme / style)
512        let key = (self.wp.rt.dpi, r[2], self.layout_scale());
513        if key != s.key {
514            let dpi_changed = key.0 != s.key.0;
515            s.key = key;
516            s.rows.forget_all();
517            if dpi_changed || s.rows.len() != count {
518                s.rows.resize(count);
519            }
520            for (i, &g) in s.given.iter().enumerate() {
521                if g > 0 && i < count {
522                    s.rows.set(i, self.dp(g).max(1));
523                }
524            }
525        }
526        if s.rows.len() != count {
527            s.rows.resize(count);
528            for (i, &g) in s.given.iter().enumerate().take(count) {
529                if g > 0 {
530                    s.rows.set(i, self.dp(g).max(1));
531                }
532            }
533        }
534        if s.sel >= count as i32 {
535            s.sel = count as i32 - 1;
536        }
537        s.rows.est = if s.estimate > 0 { self.dp(s.estimate) } else { s.rows.mean().unwrap_or(self.dp(EST_DIP)) }.max(1);
538        // the view: focus and keys
539        self.wp.ctx.last_rect = r;
540        let vev = self.behave(id, HB_FOCUS | HB_OWNKEYS);
541        let old = s.sel;
542        let mut kdbl = false;
543        if vev & EV_FOCUS != 0 && count > 0 {
544            kdbl = self.vl_keys(&mut s, count, view);
545        }
546        // smooth scrolling
547        if s.pending != 0 {
548            let p0 = s.pending;
549            if self.anim_off {
550                s.pending = 0;
551            } else {
552                let steps = anim_steps(self.now_ms, &mut s.t0);
553                s.pending = anim_decay(p0, 0, steps);
554            }
555            let moved = p0 - s.pending;
556            let p = s.rows.pos() + moved as i64;
557            s.rows.set_pos(p);
558            if s.pending != 0 {
559                self.animating = true;
560            }
561        }
562        // the finger's way (touch.rs)
563        let (_, pan) = self.pan_take(id);
564        if pan != 0 {
565            s.pending = 0;
566            s.reveal = None;
567            let p = s.rows.pos() + pan as i64;
568            s.rows.set_pos(p);
569        }
570        if let Some((i, _)) = s.reveal {
571            s.rows.reveal(i, view);
572        }
573        s.rows.clamp(view);
574        let first = s.rows.top.min(count);
575        let mut end = first;
576        let mut y = r[1] - s.rows.off;
577        while end < count && y < r[1] + view {
578            y += s.rows.height(end);
579            end += 1;
580        }
581        self.clip_push(r[0], r[1], r[2], r[3]);
582        // the rows move with the position (scrollcopy.rs)
583        let copy = s.rows.total() > view as i64;
584        if copy {
585            self.scroll_copy_begin(id, r[0] as i64, r[1] as i64 - s.rows.pos());
586        }
587        let c = &self.wp.ctx;
588        let (hit0, layer) = (c.hits[c.hit_cur].len(), c.cmd_layer_ui);
589        let rw = (r[2] - sbw).max(1);
590        let y0 = r[1] - s.rows.off;
591        self.m.vlist.open.push(Open {
592            id,
593            st: s,
594            count,
595            rect: r,
596            rx: r[0],
597            rw,
598            y: y0,
599            next: first,
600            cur: usize::MAX,
601            cur_y: 0,
602            vev,
603            ev: 0,
604            old,
605            kdbl,
606            changed: false,
607            hit0,
608            layer,
609            shown: None,
610            copy,
611        });
612        (first, end)
613    }
614
615    // the keys of a focused list -> Enter was pressed
616    fn vl_keys(&mut self, s: &mut VListState, count: usize, view: i32) -> bool {
617        let n = count as i64;
618        let cur = s.sel as i64;
619        let start = if cur < 0 { s.rows.top as i64 } else { cur };
620        // the row a page away (by the heights)
621        let page = |s: &VListState, from: i64, down: bool| -> i64 {
622            let (mut i, mut acc) = (from, 0i64);
623            loop {
624                let next = if down { i + 1 } else { i - 1 };
625                if next < 0 || next >= n {
626                    return i;
627                }
628                acc += s.rows.height(next as usize) as i64;
629                if acc > view as i64 {
630                    return if i == from { next } else { i };
631                }
632                i = next;
633            }
634        };
635        let mut new = None;
636        if self.key(VK_UP) {
637            new = Some(if cur < 0 { start } else { cur - 1 });
638        }
639        if self.key(VK_DOWN) {
640            new = Some(if cur < 0 { start } else { cur + 1 });
641        }
642        if self.key(VK_HOME) {
643            new = Some(0);
644        }
645        if self.key(VK_END) {
646            new = Some(n - 1);
647        }
648        if self.key(VK_PRIOR) {
649            new = Some(page(s, start, false));
650        }
651        if self.key(VK_NEXT) {
652            new = Some(page(s, start, true));
653        }
654        let enter = cur >= 0 && self.key(VK_RETURN);
655        if let Some(v) = new {
656            let v = v.clamp(0, n - 1);
657            if v != cur {
658                s.sel = v as i32;
659                self.repass();
660            }
661            s.reveal = Some((v as usize, 0));
662            self.wp.ctx.focus_visible = true;
663        }
664        if enter {
665            s.reveal = Some((cur as usize, 0));
666            self.repass();
667        }
668        enter
669    }
670
671    /// ui_vlist_item_begin(i) -> EV_*: row i (see the module header)
672    #[cfg_attr(jfgui_dev, track_caller)]
673    pub fn vlist_item_begin(&mut self, i: usize) -> u32 {
674        let _src = self.src_scope();
675        let Some(mut o) = self.m.vlist.open.pop() else {
676            self.warn("ui_vlist_item_begin outside a vlist");
677            return 0;
678        };
679        // rows the caller skipped keep their space
680        if o.next < i {
681            for k in o.next..i.min(o.count) {
682                o.y += o.st.rows.height(k);
683            }
684        }
685        o.next = i + 1;
686        let h = o.st.rows.height(i);
687        let rid = o.id.wrapping_add(1).wrapping_add(i as u32);
688        let rect = [o.rx, o.y, o.rw, h];
689        self.wp.ctx.last_rect = rect;
690        let ev0 = self.behave(rid, 0);
691        let mut ev = ev0;
692        let c = &mut self.wp.ctx;
693        if c.active == rid && c.pressed & (1 << BTN_LEFT) != 0 {
694            c.focus = o.id;
695            c.focus_visible = false;
696        }
697        if ev & EV_ACT != 0 && o.st.sel != i as i32 {
698            o.st.sel = i as i32;
699            o.ev |= EV_ACT;
700            self.repass();
701        }
702        let selected = o.st.sel == i as i32;
703        if selected && core::mem::take(&mut o.kdbl) {
704            ev |= EV_DOUBLE;
705        }
706        if ev & EV_DOUBLE != 0 {
707            o.ev |= EV_DOUBLE;
708        }
709        let fl = if o.vev & EV_FOCUS != 0 && self.wp.ctx.focus_visible { 2 } else { 0 };
710        let disabled = self.wp.ctx.last_state == SS_DISABLED as u32;
711        let mut sp = Sp { ev, value: selected as i32, value2: disabled as i32, flags: fl, ..Sp::default() };
712        sp.place(rect);
713        let st = self.style;
714        st.list_row(self, &sp);
715        if selected {
716            self.wp.ctx.last_state |= SS_SELECTED as u32;
717        }
718        self.sem(K_LISTITEM, rid, None, i as i32);
719        // the content: natural height (or the height the app gave)
720        let given = o.st.given.get(i).copied().unwrap_or(0) > 0;
721        let px = self.dp(8);
722        self.next_rect_px(o.rx + px, o.y, (o.rw - px * 2).max(1), if given { h } else { 0 });
723        o.cur = i;
724        o.cur_y = o.y;
725        self.m.vlist.open.push(o);
726        self.box_begin(rid | 0xC000_0000, BOX_V | if given { BOX_FIXED } else { 0 } | pad(4), 4);
727        ev0 | (ev & EV_DOUBLE)
728    }
729
730    /// ui_vlist_item_end: the row's height is measured
731    #[cfg_attr(jfgui_dev, track_caller)]
732    pub fn vlist_item_end(&mut self) {
733        let _src = self.src_scope();
734        self.box_end();
735        let Some(mut o) = self.m.vlist.open.pop() else {
736            return;
737        };
738        let i = o.cur;
739        if i < o.count {
740            let rid = o.id.wrapping_add(1).wrapping_add(i as u32);
741            let given = o.st.given.get(i).copied().unwrap_or(0) > 0;
742            if !given {
743                let mh = self.slot(rid | 0xC000_0000).meash.max(1);
744                if o.st.rows.set(i, mh) {
745                    o.changed = true;
746                }
747            }
748            let h = o.st.rows.height(i);
749            if o.st.reveal.is_some_and(|r| r.0 == i) {
750                o.shown = Some((o.cur_y - o.rect[1], h));
751            }
752            o.y = o.cur_y + h;
753        }
754        o.cur = usize::MAX;
755        self.m.vlist.open.push(o);
756    }
757
758    /// the selection of the innermost vlist while its rows are emitted (-1 outside one)
759    pub fn vlist_sel(&self) -> i32 {
760        self.m.vlist.open.last().map_or(-1, |o| o.st.sel)
761    }
762
763    /// ui_vlist_end(&mut st) -> EV_* (see the module header)
764    #[cfg_attr(jfgui_dev, track_caller)]
765    pub fn vlist_end(&mut self, st: &mut VListState) -> u32 {
766        let _src = self.src_scope();
767        let Some(mut o) = self.m.vlist.open.pop() else {
768            return 0;
769        };
770        if o.copy {
771            self.scroll_copy_end();
772        }
773        self.clip_pop();
774        let r = o.rect;
775        let view = r[3].max(1);
776        let s = &mut o.st;
777        // the row to reveal: done when it was built and shows whole (its rows measured)
778        if let Some((i, tries)) = s.reveal {
779            let done = match o.shown {
780                Some((y, h)) => (y >= 0 && y + h <= view) || (y == 0 && h >= view) || !o.changed,
781                None => false,
782            };
783            if done || tries >= REVEAL_TRIES || i >= o.count {
784                s.reveal = None;
785            } else {
786                s.reveal = Some((i, tries + 1));
787                self.repass();
788            }
789        }
790        // rows shorter than thought leave room below: another pass fills it
791        if o.changed {
792            self.repass();
793        }
794        // the scroll bar
795        let total = s.rows.total().min(i32::MAX as i64) as i32;
796        if total > view {
797            let sbw = self.style.scrollbar_thickness(self);
798            // a dragged thumb moves the view when the pointer moves, not when rows measured in
799            // this pass change the total (that would chase the thumb pass after pass): while
800            // the pointer stays, the bar compares with the position it reported last
801            let sid = part_id(o.id, 1);
802            let c = &self.wp.ctx;
803            let (my, press) = (c.my, c.pressed & (1 << BTN_LEFT) != 0);
804            let held = c.active == sid && c.mbtn & (1 << BTN_LEFT) != 0;
805            let mut pos = if held && !press && my == s.sb_y { s.sb_pos } else { s.rows.pos().min(i32::MAX as i64) as i32 };
806            self.next_rect_px(r[0] + r[2] - sbw, r[1], sbw, r[3]);
807            let ev = self.scrollbar(sid, &mut pos, total, view, true);
808            if ev & EV_ACT != 0 && (press || my != o.st.sb_y) {
809                o.st.rows.set_pos(pos as i64);
810                o.st.pending = 0;
811                o.st.reveal = None;
812                self.repass();
813            }
814            o.st.sb_y = my;
815            o.st.sb_pos = pos;
816        }
817        let s = &mut o.st;
818        // the wheel over the list (one of its hit boxes is hot)
819        let c = &self.wp.ctx;
820        let wy = c.wheel_y;
821        let hot = c.hot;
822        let over = hot != 0 && c.hits[c.hit_cur].get(o.hit0..).is_some_and(|v| v.iter().any(|h| h.id == hot && h.layer as u32 == o.layer));
823        if (over || o.vev & EV_HOVER != 0) && wy != 0 {
824            let step = self.dp(WHEEL_DIP);
825            let d = wy.wrapping_neg().wrapping_mul(step) / 120;
826            let p = s.rows.pos() + (s.pending + d) as i64;
827            let max = (s.rows.total() - view as i64).max(0);
828            if (d < 0 && s.rows.pos() > 0) || (d > 0 && s.rows.pos() < max) || p.clamp(0, max) != s.rows.pos() {
829                if s.pending == 0 {
830                    s.t0 = self.now_ms as i32;
831                }
832                s.pending += d;
833                self.wp.ctx.wheel_y = 0;
834                s.reveal = None;
835                if self.anim_off {
836                    self.repass();
837                } else {
838                    self.animating = true;
839                }
840            }
841        }
842        // a finger may roll it (touch.rs)
843        let max = (s.rows.total() - view as i64).clamp(0, i32::MAX as i64) as i32;
844        let pos = s.rows.pos().clamp(0, i32::MAX as i64) as i32;
845        self.pan_push(o.id, o.hit0, o.layer as u8, true, super::touch::AX_V, [0, pos], [0, max]);
846        let s = &mut o.st;
847        // the focus visual on the whole list when no row is selected
848        if o.vev & EV_FOCUS != 0 && self.wp.ctx.focus_visible && s.sel < 0 {
849            let st = self.style;
850            st.focus_rect(self, r[0], r[1], r[2], r[3]);
851        }
852        let mut stt = 0;
853        if o.vev & EV_HOVER != 0 {
854            stt |= SS_HOVER as u32;
855        }
856        if o.vev & EV_FOCUS != 0 {
857            stt |= SS_FOCUSED as u32;
858        }
859        self.wp.ctx.last_rect = r;
860        self.wp.ctx.last_state = stt;
861        let sel = s.sel;
862        self.sem(K_LIST, o.id, None, sel).max = o.count as i32;
863        let mut ev = o.ev | (o.vev & (EV_FOCUS | EV_HOVER));
864        if sel != o.old {
865            ev |= EV_ACT;
866        }
867        if o.kdbl {
868            ev |= EV_DOUBLE; // Enter while the selected row was not built
869        }
870        *st = o.st;
871        self.wp.ctx.last_rect = r;
872        ev
873    }
874}