Skip to main content

ui/
data.rs

1// data -- data views: grid (table with columns), tree, badges, status dots, meters
2//
3//   grid_begin(id, &mut st, cols, keys) -> (first, end)
4//        a virtualized table (fills its parent unless next_size says otherwise): a fixed
5//        header (click: sort by the column, again: descending;
6//        drag a column's right edge: its width, double click there: the default width) and
7//        the rows below it in a vertical scroll view (row height and header height from
8//        StyleData::grid_metrics).  keys: one stable key per row in display order (the app
9//        sorts and filters; the selection is kept by key, so it survives refreshes).
10//        Emit rows first .. end-1: grid_row(&mut st, i), then per column grid_cell(&st, c)
11//        and any widget (it is placed into the cell), or grid_text(&st, c, s).
12//        Mouse: click selects, Ctrl+click toggles, Shift+click selects a range, double click
13//        activates, right click selects the row and reports it.  Keys (grid focused): Up /
14//        Down / PgUp / PgDn / Home / End move the current row (Shift: extend), Ctrl+A all,
15//        Space toggles the current row, Enter activates.
16//   grid_row(&mut st, i) -> EV        row i (widget id id + 1 + i)
17//   grid_cell(&st, c)                 the next widget goes into column c of the current row
18//   grid_text(&st, c, s)              text in column c (ellipsis, the column's alignment)
19//   grid_end(&mut st) -> GridEv       ev: EV_ACT selection changed, EV_DOUBLE a row was
20//                                     activated, EV_RCLICK context menu requested; row: that
21//                                     row's index; sort_changed: re-sort the keys
22//   tree(id, &mut st, rows, row_h) -> TreeEv
23//        a virtualized tree from the app's visible rows (TreeRow: key, depth, children,
24//        check state, icon, label, detail); the chevron / Right / Left expand and collapse
25//        (st.expanded; TreeEv::expanded names a newly opened node for lazy loading), the
26//        check box / Space report TreeEv::check (the app applies it, e.g. with cascade),
27//        double click / Enter TreeEv::activated
28//   badge(kind, label)                status pill (BADGE_NEUTRAL .. BADGE_ACCENT; next_icon); in
29//                                     a narrower cell the label is cut with "…"
30//   status_dot(kind)                  a small status dot (10 DIP)
31//   meter(id, v, max, kind)           a capacity bar (next_size sets its size)
32//
33// Ids: the header box (id << 3) | 0x80000000, the header cells ((id ^ 0xA5A5) << 3 | k) |
34// 0x80000000 with k = 1 + column (sort), the grips ((id ^ 0x5A5A) << 3 | k) | 0x80000000; the
35// rows' scroll view id (its bars (id << 3 | 1 / 2) | 0x80000000); rows id + 1 + i; tree chevrons / check boxes
36// (id + 1 + i) | 0x40000000 / | 0x20000000.
37
38use alloc::collections::{BTreeMap, BTreeSet};
39use alloc::string::String;
40use alloc::vec::Vec;
41
42use crate::consts::*;
43use crate::keys::*;
44use crate::list::{into_view, SCR_F_LIST};
45use crate::{Sp, Ui};
46
47/// a grid column: title, width and minimum width in DIP, GC_* flags
48#[derive(Clone, Copy, Debug)]
49pub struct GridCol<'a> {
50    pub title: &'a str,
51    pub width: i32,
52    pub min: i32,
53    pub flags: u32,
54}
55
56impl<'a> GridCol<'a> {
57    /// a column `width` DIP wide (at least 48)
58    pub const fn new(title: &'a str, width: i32) -> GridCol<'a> {
59        GridCol { title, width, min: 48, flags: 0 }
60    }
61    /// the same column with GC_* flags
62    pub const fn flags(mut self, flags: u32) -> GridCol<'a> {
63        self.flags = flags;
64        self
65    }
66    /// the same column with a minimum width (DIP)
67    pub const fn min(mut self, min: i32) -> GridCol<'a> {
68        self.min = min;
69        self
70    }
71}
72
73/// the per-pass data of a grid between grid_begin and grid_end
74#[derive(Clone, Default, Debug)]
75struct GridFrame {
76    id: u32,
77    keys: Vec<u64>,
78    /// column x (relative to the grid), width, GC_* flags (px)
79    cols: Vec<(i32, i32, u32)>,
80    x0: i32,
81    row_h: i32,
82    pad: i32,
83    next: i32,
84    vev: u32,
85    ev: u32,
86    row: i32,
87    row_rect: [i32; 4],
88    cur_row: i32,
89    focus_visible: bool,
90}
91
92/// the app-owned state of a grid: selection (by key), current row, sort, column widths
93#[derive(Clone, Default, Debug)]
94pub struct GridState {
95    /// the selected rows' keys
96    pub sel: BTreeSet<u64>,
97    /// the current row (keyboard focus / last clicked)
98    pub cur: Option<u64>,
99    anchor: Option<u64>,
100    /// the sort column and direction (true = descending); None = the app's order
101    pub sort: Option<(usize, bool)>,
102    /// column widths set by the user (DIP, 0 = the column's own)
103    pub widths: Vec<i32>,
104    /// alternate row shading
105    pub striped: bool,
106    f: GridFrame,
107}
108
109impl GridState {
110    /// the keys selected, in display order of the last pass
111    pub fn selected_in_order(&self) -> Vec<u64> {
112        self.f.keys.iter().copied().filter(|k| self.sel.contains(k)).collect()
113    }
114    /// select exactly this key (None: nothing) and make it current
115    pub fn select_only(&mut self, key: Option<u64>) {
116        self.sel.clear();
117        if let Some(k) = key {
118            self.sel.insert(k);
119        }
120        self.cur = key;
121        self.anchor = key;
122    }
123    /// drop selected keys that are no longer shown (after a refresh)
124    pub fn retain(&mut self, keys: &[u64]) {
125        let set: BTreeSet<u64> = keys.iter().copied().collect();
126        self.sel.retain(|k| set.contains(k));
127        if self.cur.is_some_and(|k| !set.contains(&k)) {
128            self.cur = None;
129        }
130        if self.anchor.is_some_and(|k| !set.contains(&k)) {
131            self.anchor = None;
132        }
133    }
134}
135
136/// what happened in a grid during the pass
137#[derive(Clone, Copy, Default, Debug)]
138pub struct GridEv {
139    /// EV_ACT selection changed, EV_DOUBLE a row was activated, EV_RCLICK context requested
140    pub ev: u32,
141    /// the activated / context row (-1 = none)
142    pub row: i32,
143    /// the sort column or direction changed
144    pub sort_changed: bool,
145}
146
147/// one visible row of a tree (the app flattens its model through TreeState::expanded)
148#[derive(Clone, Copy, Debug)]
149pub struct TreeRow<'a> {
150    pub key: u64,
151    pub depth: u16,
152    pub has_children: bool,
153    /// check box state 0 / 1 / 2 (indeterminate); None = no check box
154    pub check: Option<u8>,
155    pub icon: u32,
156    pub label: &'a str,
157    /// secondary text after the label (size, date)
158    pub detail: Option<&'a str>,
159}
160
161/// the app-owned state of a tree: expanded nodes, current node
162#[derive(Clone, Default, Debug)]
163pub struct TreeState {
164    pub expanded: BTreeSet<u64>,
165    pub cur: Option<u64>,
166    /// app data per node (free use, e.g. a loading flag)
167    pub marks: BTreeMap<u64, u32>,
168}
169
170/// what happened in a tree during the pass
171#[derive(Clone, Copy, Default, Debug)]
172pub struct TreeEv {
173    /// a node opened in this pass (load its children)
174    pub expanded: Option<u64>,
175    /// a node closed in this pass
176    pub collapsed: Option<u64>,
177    /// a check box was toggled: (key, new state 0 / 1)
178    pub check: Option<(u64, u8)>,
179    /// double click / Enter on a node
180    pub activated: Option<u64>,
181    /// the current node changed
182    pub sel_changed: bool,
183}
184
185fn part(id: u32, k: u32) -> u32 {
186    id.wrapping_mul(8).wrapping_add(k) | 0x8000_0000
187}
188
189/// a header cell: not part(id, 1 + column) -- the rows' scroll view (id) has its scroll bars
190/// there (part 1 vertical, 2 horizontal), a click on the bar sorted by the first column
191fn head(id: u32, column: u32) -> u32 {
192    part(id ^ 0xA5A5, 1 + column)
193}
194
195impl Ui {
196    // ------------------------------------------------------------------ grid
197
198    /// ui_grid_begin(id, &mut st, cols, keys) -> (first, end): the rows to emit
199    #[cfg_attr(jfgui_dev, track_caller)]
200    pub fn grid_begin(&mut self, id: u32, st: &mut GridState, cols: &[GridCol], keys: &[u64]) -> (i32, i32) {
201        let _src = self.src_scope();
202        let sd = self.style.data();
203        let (hh, rh, pad) = sd.grid_metrics(self);
204        let (hh, rh, pad) = (self.dp(hh), self.dp(rh).max(1), self.dp(pad));
205        // outer column: header + rows; without next_size the grid fills its parent
206        if self.wp.lay.next_w == 0 && self.wp.lay.next_h == 0 && self.wp.lay.next_rect_set == 0 {
207            self.next_size(-1, -1);
208        }
209        self.box_begin(part(id, 0), BOX_V, 0);
210        let outer = self.box_rect();
211        // column geometry: user widths, the fill column takes what is left
212        if st.widths.len() != cols.len() {
213            st.widths.resize(cols.len(), 0);
214        }
215        let mut ws: Vec<i32> = cols
216            .iter()
217            .zip(&st.widths)
218            .map(|(c, &u)| self.dp(if u > 0 { u.max(c.min) } else { c.width.max(c.min) }))
219            .collect();
220        let fill = cols.iter().position(|c| c.flags & GC_FILL != 0).unwrap_or(cols.len().saturating_sub(1));
221        let total: i32 = ws.iter().sum();
222        if !ws.is_empty() && total < outer[2] {
223            ws[fill] += outer[2] - total;
224        } else if !ws.is_empty() && total > outer[2] && st.widths[fill] == 0 {
225            // too narrow: the fill column gives way down to its minimum (then the grid scrolls)
226            let floor = self.dp(cols[fill].min.max(48));
227            ws[fill] = (ws[fill] - (total - outer[2])).max(floor.min(ws[fill]));
228        }
229        let mut colv = Vec::with_capacity(cols.len());
230        let mut x = 0;
231        for (c, &w) in cols.iter().zip(&ws) {
232            colv.push((x, w, c.flags));
233            x += w;
234        }
235        // header strip
236        let hr = self.place(0, hh);
237        let mut sp = Sp::default();
238        sp.place(hr);
239        sd.grid_header_bg(self, &sp);
240        self.clip_push(hr[0], hr[1], hr[2], hr[3]);
241        let mut sort_changed = false;
242        for (i, c) in cols.iter().enumerate() {
243            let (cx, cw, cf) = colv[i];
244            let r = [hr[0] + cx, hr[1], cw, hh];
245            self.wp.ctx.last_rect = r;
246            let sortable = cf & GC_NOSORT == 0;
247            let ev = self.behave(head(id, i as u32), if sortable { 0 } else { HB_DISABLED });
248            if ev & EV_ACT != 0 && sortable {
249                st.sort = match st.sort {
250                    Some((s, desc)) if s == i => Some((i, !desc)),
251                    _ => Some((i, false)),
252                };
253                sort_changed = true;
254                self.repass();
255            }
256            let mut f = 0;
257            match st.sort {
258                Some((s, false)) if s == i => f |= GH_SORT_ASC,
259                Some((s, true)) if s == i => f |= GH_SORT_DESC,
260                _ => {}
261            }
262            if i + 1 == cols.len() {
263                f |= GH_LAST;
264            }
265            if cf & GC_RIGHT != 0 {
266                f |= GH_RIGHT;
267            }
268            let mut sp = Sp { ev, flags: f, label: Some(c.title), ..Sp::default() };
269            sp.place(r);
270            sp.s[0] = pad;
271            sp.state = if self.wp.ctx.last_state == SS_DISABLED as u32 { 0 } else { sp.state };
272            sd.grid_header(self, &sp);
273            self.wp.ctx.last_rect = r;
274            self.wp.ctx.last_state = 0;
275            self.sem(K_BUTTON, head(id, i as u32), Some(c.title), f as i32);
276        }
277        // column resize grips (on top of the header cells)
278        for (i, c) in cols.iter().enumerate() {
279            if i + 1 == cols.len() {
280                break;
281            }
282            let (cx, cw, _) = colv[i];
283            let gw = self.dp(8);
284            let gid = (id ^ 0x5A5A).wrapping_mul(8).wrapping_add(1 + i as u32) | 0x8000_0000;
285            let ev = self.hit_rect(gid, hr[0] + cx + cw - (gw >> 1), hr[1], gw, hh, CUR_SIZE_WE << HB_CURSOR_SHIFT);
286            if ev & EV_DOUBLE != 0 {
287                st.widths[i] = 0;
288                self.repass();
289            } else if ev & EV_HELD != 0 {
290                let w = self.px2dip(self.wp.ctx.mx - (hr[0] + cx)).max(c.min);
291                if w != st.widths[i] {
292                    st.widths[i] = w;
293                    self.repass();
294                }
295            }
296        }
297        self.clip_pop();
298        // the rows: a vertical scroll view that takes the keyboard focus (the menu key on it
299        // is the current row's right click, kbnav.rs)
300        self.next_size(-1, -1);
301        if let Some(c) = st.cur.and_then(|k| keys.iter().position(|&x| x == k)) {
302            self.kb_ctx_redirect(id, id.wrapping_add(1).wrapping_add(c as u32));
303        }
304        let vev = self.scr_open(id, SC_V | SCR_F_LIST, HB_FOCUS | HB_OWNKEYS);
305        let count = keys.len() as i32;
306        st.f = GridFrame {
307            id,
308            keys: keys.to_vec(),
309            cols: colv,
310            x0: hr[0],
311            row_h: rh,
312            pad,
313            next: 0,
314            vev,
315            ev: if sort_changed { 0x8000_0000 } else { 0 },
316            row: -1,
317            row_rect: [0; 4],
318            cur_row: st.cur.and_then(|k| keys.iter().position(|&x| x == k)).map_or(-1, |p| p as i32),
319            focus_visible: false,
320        };
321        if vev & EV_FOCUS != 0 {
322            self.grid_keys(st);
323        }
324        self.scr_offset();
325        let y0 = self.box_rect()[1];
326        let k = self.wp.ctx.uiclip;
327        let first = (k[1].wrapping_sub(y0).max(0) / rh).min(count);
328        let end = ((k[3].wrapping_sub(y0).max(0) + rh - 1) / rh).min(count).max(first);
329        st.f.next = first;
330        if first != 0 {
331            self.place(0, first * rh);
332        }
333        st.f.focus_visible = vev & EV_FOCUS != 0 && self.wp.ctx.focus_visible;
334        (first, end)
335    }
336
337    // grid_keys: the selection keys of a focused grid
338    fn grid_keys(&mut self, st: &mut GridState) {
339        let n = st.f.keys.len() as i32;
340        if n == 0 {
341            return;
342        }
343        let (_, view) = self.scr_view();
344        let page = (view[3] / st.f.row_h).max(1);
345        let cur = st.f.cur_row;
346        let shift = self.mods & MOD_SHIFT != 0;
347        let ctrl = self.mods & MOD_CTRL != 0;
348        let mut new = cur;
349        if self.key(VK_UP) {
350            new = if cur > 0 { cur - 1 } else { 0 };
351        }
352        if self.key(VK_DOWN) {
353            new = if cur < 0 { 0 } else { cur + 1 };
354        }
355        if self.key(VK_HOME) {
356            new = 0;
357        }
358        if self.key(VK_END) {
359            new = n - 1;
360        }
361        if self.key(VK_PRIOR) {
362            new = cur - page;
363        }
364        if self.key(VK_NEXT) {
365            new = if cur < 0 { page } else { cur + page };
366        }
367        new = new.clamp(0, n - 1);
368        if ctrl && self.key(b'A' as u32) {
369            st.sel = st.f.keys.iter().copied().collect();
370            st.f.ev |= EV_ACT;
371            self.repass();
372        }
373        if self.key(VK_SPACE) && cur >= 0 {
374            let k = st.f.keys[cur as usize];
375            if !st.sel.remove(&k) {
376                st.sel.insert(k);
377            }
378            st.anchor = Some(k);
379            st.f.ev |= EV_ACT;
380            self.repass();
381        }
382        if self.key(VK_RETURN) && cur >= 0 {
383            st.f.ev |= EV_DOUBLE;
384            st.f.row = cur;
385        }
386        if new == cur {
387            return;
388        }
389        let k = st.f.keys[new as usize];
390        st.cur = Some(k);
391        st.f.cur_row = new;
392        if shift {
393            let a = st.anchor.and_then(|a| st.f.keys.iter().position(|&x| x == a)).map_or(new, |p| p as i32);
394            let (lo, hi) = (a.min(new) as usize, a.max(new) as usize);
395            st.sel = st.f.keys[lo..=hi].iter().copied().collect();
396        } else {
397            st.sel.clear();
398            st.sel.insert(k);
399            st.anchor = Some(k);
400        }
401        st.f.ev |= EV_ACT;
402        self.wp.ctx.focus_visible = true;
403        self.repass();
404        // scroll the row into view at once
405        let (vid, view) = self.scr_view();
406        let rh = st.f.row_h;
407        let s = self.slot(vid);
408        let d = into_view(new * rh, rh, s.scrolly, view[3]);
409        s.scrolly += d;
410        s.adur = s.scrolly as u32;
411    }
412
413    /// ui_grid_row(&mut st, i) -> EV_* of row i (EV_ACT clicked)
414    #[cfg_attr(jfgui_dev, track_caller)]
415    pub fn grid_row(&mut self, st: &mut GridState, i: i32) -> u32 {
416        let _src = self.src_scope();
417        let f = &st.f;
418        let (id, rh) = (f.id, f.row_h);
419        let d = i - f.next;
420        if d > 0 {
421            self.place(0, d * rh);
422        }
423        st.f.next = i + 1;
424        let r = self.place(0, rh);
425        let iid = id.wrapping_add(1).wrapping_add(i as u32);
426        let ev = self.behave(iid, 0);
427        let key = st.f.keys.get(i as usize).copied().unwrap_or(0);
428        // a press gives the grid the keyboard focus
429        let c = &mut self.wp.ctx;
430        if c.active == iid && c.pressed & (1 << BTN_LEFT) != 0 {
431            c.focus = id;
432            c.focus_visible = false;
433        }
434        if ev & EV_ACT != 0 {
435            let m = self.mods;
436            if m & MOD_SHIFT != 0 {
437                let a = st.anchor.and_then(|a| st.f.keys.iter().position(|&x| x == a)).map_or(i, |p| p as i32);
438                let (lo, hi) = (a.min(i) as usize, a.max(i) as usize);
439                st.sel = st.f.keys[lo..=hi.min(st.f.keys.len() - 1)].iter().copied().collect();
440            } else if m & MOD_CTRL != 0 {
441                if !st.sel.remove(&key) {
442                    st.sel.insert(key);
443                }
444                st.anchor = Some(key);
445            } else {
446                st.sel.clear();
447                st.sel.insert(key);
448                st.anchor = Some(key);
449            }
450            st.cur = Some(key);
451            st.f.cur_row = i;
452            st.f.ev |= EV_ACT;
453            self.repass();
454        }
455        if ev & EV_RCLICK != 0 {
456            if !st.sel.contains(&key) {
457                st.sel.clear();
458                st.sel.insert(key);
459                st.anchor = Some(key);
460                st.f.ev |= EV_ACT;
461            }
462            st.cur = Some(key);
463            st.f.cur_row = i;
464            st.f.ev |= EV_RCLICK;
465            st.f.row = i;
466            self.repass();
467        }
468        if ev & EV_DOUBLE != 0 {
469            st.f.ev |= EV_DOUBLE;
470            st.f.row = i;
471        }
472        let selected = st.sel.contains(&key);
473        let mut fl = 0;
474        if st.f.cur_row == i {
475            fl |= if st.f.focus_visible { GR_FOCUS } else { GR_CURRENT };
476        }
477        if st.striped && i & 1 != 0 {
478            fl |= GR_STRIPE;
479        }
480        let mut sp = Sp { ev, value: selected as i32, value2: i, flags: fl, ..Sp::default() };
481        sp.place(r);
482        let sd = self.style.data();
483        sd.grid_row(self, &sp);
484        self.wp.ctx.last_rect = r;
485        if selected {
486            self.wp.ctx.last_state |= SS_SELECTED as u32;
487        }
488        self.sem(K_LISTITEM, iid, None, i);
489        st.f.row_rect = r;
490        ev
491    }
492
493    /// ui_grid_cell(&st, c): the next widget is placed into column c of the current row
494    /// (inset by the cell padding, full row height)
495    #[cfg_attr(jfgui_dev, track_caller)]
496    pub fn grid_cell(&mut self, st: &GridState, c: usize) {
497        let _src = self.src_scope();
498        let r = st.f.row_rect;
499        let Some(&(cx, cw, _)) = st.f.cols.get(c) else { return };
500        let pad = st.f.pad;
501        let x = st.f.x0 + cx + pad;
502        let w = (cw - pad * 2).max(1);
503        self.next_rect_px(x, r[1], w, r[3]);
504    }
505
506    /// ui_grid_text(&st, c, s): body text in column c (cut with "…", the column's alignment)
507    #[cfg_attr(jfgui_dev, track_caller)]
508    pub fn grid_text(&mut self, st: &GridState, c: usize, s: &str) {
509        let _src = self.src_scope();
510        self.grid_text_ts(st, c, TS_BODY, s);
511    }
512
513    /// ui_grid_text_ts(&st, c, ts, s): text in column c with a text style (TS_* | TX_*)
514    #[cfg_attr(jfgui_dev, track_caller)]
515    pub fn grid_text_ts(&mut self, st: &GridState, c: usize, ts: u32, s: &str) {
516        let _src = self.src_scope();
517        let f = st.f.cols.get(c).map_or(0, |c| c.2);
518        let mut style = ts | TX_ELLIPSIS;
519        if f & GC_RIGHT != 0 {
520            style |= TX_RIGHT;
521        } else if f & GC_CENTER != 0 {
522            style |= TX_CENTER;
523        }
524        self.grid_cell(st, c);
525        self.text(style, s);
526    }
527
528    /// ui_grid_end(&mut st) -> GridEv
529    #[cfg_attr(jfgui_dev, track_caller)]
530    pub fn grid_end(&mut self, st: &mut GridState) -> GridEv {
531        let _src = self.src_scope();
532        let n = st.f.keys.len() as i32;
533        let d = n - st.f.next;
534        if d > 0 {
535            self.place(0, d * st.f.row_h);
536        }
537        self.scr_close();
538        // focus visual on the whole view when nothing is current
539        if st.f.vev & EV_FOCUS != 0 && self.wp.ctx.focus_visible && st.f.cur_row < 0 {
540            let r = self.wp.ctx.last_rect;
541            let s = self.style;
542            s.focus_rect(self, r[0], r[1], r[2], r[3]);
543        }
544        let mut stt = 0;
545        if st.f.vev & EV_HOVER != 0 {
546            stt |= SS_HOVER as u32;
547        }
548        if st.f.vev & EV_FOCUS != 0 {
549            stt |= SS_FOCUSED as u32;
550        }
551        self.wp.ctx.last_state = stt;
552        self.sem(K_LIST, st.f.id, None, st.f.cur_row).max = n;
553        self.box_end(); // outer column
554        let sort_changed = st.f.ev & 0x8000_0000 != 0;
555        GridEv { ev: st.f.ev & 0x7FFF_FFFF, row: st.f.row, sort_changed }
556    }
557
558    // ------------------------------------------------------------------ tree
559
560    /// ui_tree(id, &mut st, rows, row_h) -> TreeEv: the visible rows of a tree (row_h DIP,
561    /// 0 = the grid row height)
562    #[cfg_attr(jfgui_dev, track_caller)]
563    pub fn tree(&mut self, id: u32, st: &mut TreeState, rows: &[TreeRow], row_h: i32) -> TreeEv {
564        let _src = self.src_scope();
565        let sd = self.style.data();
566        let rh = self.dp(if row_h > 0 { row_h } else { sd.grid_metrics(self).1 }).max(1);
567        let mut out = TreeEv::default();
568        let n = rows.len() as i32;
569        if self.wp.lay.next_w == 0 && self.wp.lay.next_h == 0 && self.wp.lay.next_rect_set == 0 {
570            self.next_size(-1, -1);
571        }
572        let mut cur = st.cur.and_then(|k| rows.iter().position(|r| r.key == k)).map_or(-1, |p| p as i32);
573        if cur >= 0 {
574            self.kb_ctx_redirect(id, id.wrapping_add(1).wrapping_add(cur as u32)); // the menu key: the current row
575        }
576        let vev = self.scr_open(id, SC_V | SCR_F_LIST, HB_FOCUS | HB_OWNKEYS);
577        if vev & EV_FOCUS != 0 && n > 0 {
578            let mut new = cur;
579            if self.key(VK_UP) {
580                new = if cur > 0 { cur - 1 } else { 0 };
581            }
582            if self.key(VK_DOWN) {
583                new = if cur < 0 { 0 } else { (cur + 1).min(n - 1) };
584            }
585            if self.key(VK_HOME) {
586                new = 0;
587            }
588            if self.key(VK_END) {
589                new = n - 1;
590            }
591            if cur >= 0 {
592                let r = rows[cur as usize];
593                if self.key(VK_RIGHT) {
594                    if r.has_children && !st.expanded.contains(&r.key) {
595                        st.expanded.insert(r.key);
596                        out.expanded = Some(r.key);
597                    } else if r.has_children && cur + 1 < n {
598                        new = cur + 1;
599                    }
600                }
601                if self.key(VK_LEFT) {
602                    if st.expanded.contains(&r.key) {
603                        st.expanded.remove(&r.key);
604                        out.collapsed = Some(r.key);
605                    } else if r.depth > 0 {
606                        // to the parent: the nearest row above with a smaller depth
607                        if let Some(p) = rows[..cur as usize].iter().rposition(|x| x.depth < r.depth) {
608                            new = p as i32;
609                        }
610                    }
611                }
612                if self.key(VK_SPACE) {
613                    if let Some(c) = r.check {
614                        out.check = Some((r.key, if c == 1 { 0 } else { 1 }));
615                    }
616                }
617                if self.key(VK_RETURN) {
618                    out.activated = Some(r.key);
619                }
620            }
621            if new != cur && new >= 0 {
622                cur = new;
623                st.cur = Some(rows[new as usize].key);
624                out.sel_changed = true;
625                self.wp.ctx.focus_visible = true;
626                let (vid, view) = self.scr_view();
627                let s = self.slot(vid);
628                let d = into_view(new * rh, rh, s.scrolly, view[3]);
629                s.scrolly += d;
630                s.adur = s.scrolly as u32;
631            }
632            if out.expanded.is_some() || out.collapsed.is_some() || out.check.is_some() || out.sel_changed {
633                self.repass();
634            }
635        }
636        self.scr_offset();
637        let y0 = self.box_rect()[1];
638        let k = self.wp.ctx.uiclip;
639        let first = (k[1].wrapping_sub(y0).max(0) / rh).min(n);
640        let end = ((k[3].wrapping_sub(y0).max(0) + rh - 1) / rh).min(n).max(first);
641        if first != 0 {
642            self.place(0, first * rh);
643        }
644        let focus_visible = vev & EV_FOCUS != 0 && self.wp.ctx.focus_visible;
645        let indent = self.dp(18);
646        let tw = self.dp(20);
647        for i in first..end {
648            let row = rows[i as usize];
649            let r = self.place(0, rh);
650            let iid = id.wrapping_add(1).wrapping_add(i as u32);
651            let ev = self.behave(iid, 0);
652            let c = &mut self.wp.ctx;
653            if c.active == iid && c.pressed & (1 << BTN_LEFT) != 0 {
654                c.focus = id;
655                c.focus_visible = false;
656            }
657            if ev & (EV_ACT | EV_RCLICK) != 0 && cur != i {
658                cur = i;
659                st.cur = Some(row.key);
660                out.sel_changed = true;
661                self.repass();
662            }
663            if ev & EV_DOUBLE != 0 {
664                if row.has_children {
665                    if !st.expanded.remove(&row.key) {
666                        st.expanded.insert(row.key);
667                        out.expanded = Some(row.key);
668                    } else {
669                        out.collapsed = Some(row.key);
670                    }
671                    self.repass();
672                }
673                out.activated = Some(row.key);
674            }
675            let sel = cur == i;
676            let mut sp = Sp { ev, value: sel as i32, value2: i, flags: if sel && focus_visible { GR_FOCUS } else { 0 }, ..Sp::default() };
677            sp.place(r);
678            sd.grid_row(self, &sp);
679            if sel {
680                self.wp.ctx.last_state |= SS_SELECTED as u32;
681            }
682            self.sem(K_LISTITEM, iid, Some(row.label), i);
683            // content: indentation, chevron, check box, icon, label, detail
684            let mut x = r[0] + self.dp(8) + indent * row.depth as i32;
685            if row.has_children {
686                let cr = [x, r[1] + ((r[3] - tw) >> 1), tw, tw];
687                let tid = iid | 0x4000_0000;
688                let tev = self.hit_rect(tid, cr[0], cr[1], cr[2], cr[3], 0);
689                let open = st.expanded.contains(&row.key);
690                if tev & EV_ACT != 0 {
691                    if open {
692                        st.expanded.remove(&row.key);
693                        out.collapsed = Some(row.key);
694                    } else {
695                        st.expanded.insert(row.key);
696                        out.expanded = Some(row.key);
697                    }
698                    self.repass();
699                }
700                let mut tsp = Sp { ev: tev, value: open as i32, ..Sp::default() };
701                tsp.place(cr);
702                sd.tree_twisty(self, &tsp);
703                self.wp.ctx.last_rect = cr;
704                self.sem(K_BUTTON, tid, Some(crate::i18n::tr(if open { "Zuklappen" } else { "Aufklappen" })), open as i32);
705            }
706            x += tw + self.dp(4);
707            if let Some(cv) = row.check {
708                let st_ = self.style;
709                let mut csp = Sp::default();
710                let (cw, ch) = st_.checkbox_size(self, &mut csp);
711                let cw = cw.min(self.dp(20)).max(1);
712                let cr = [x, r[1] + ((r[3] - ch) >> 1), cw, ch];
713                let cid = iid | 0x2000_0000;
714                let cev = self.hit_rect(cid, cr[0], cr[1], cr[2], cr[3], 0);
715                if cev & EV_ACT != 0 {
716                    out.check = Some((row.key, if cv == 1 { 0 } else { 1 }));
717                    self.repass();
718                }
719                self.sp_placed(&mut csp, cev);
720                csp.value = cv as i32;
721                csp.label = None;
722                st_.checkbox(self, &csp);
723                self.wp.ctx.last_rect = cr;
724                if cv != 0 {
725                    self.wp.ctx.last_state |= SS_CHECKED as u32;
726                }
727                self.sem(K_CHECKBOX, cid, Some(row.label), cv as i32);
728                x += cw + self.dp(8);
729            }
730            if row.icon != 0 {
731                let px = self.dp(16);
732                let c = self.color(T_TEXT2);
733                crate::stock::icon_in(self, row.icon, [x, r[1], px, r[3]], px, c);
734                x += px + self.dp(8);
735            }
736            let end_x = r[0] + r[2] - self.dp(12);
737            let mut label_end = end_x;
738            if let Some(dt) = row.detail {
739                self.ts_font(TS_CAPTION);
740                let dw = self.text.width(dt);
741                let dx = end_x - dw;
742                let c = self.color(T_TEXT2);
743                crate::stock::text_at(self, TS_CAPTION, dx, r, dt, c, 0);
744                label_end = dx - self.dp(12);
745            }
746            let c = self.color(T_TEXT);
747            self.ts_font(TS_BODY);
748            let full = self.text.width(row.label);
749            let maxw = (label_end - x).max(1);
750            if full > maxw {
751                let (e, _) = self.ellipsize(row.label.as_bytes(), maxw);
752                crate::stock::text_at(self, TS_BODY, x, r, &e, c, 0);
753            } else {
754                crate::stock::text_at(self, TS_BODY, x, r, row.label, c, 0);
755            }
756        }
757        if n - end > 0 {
758            self.place(0, (n - end) * rh);
759        }
760        self.scr_close();
761        let mut stt = 0;
762        if vev & EV_HOVER != 0 {
763            stt |= SS_HOVER as u32;
764        }
765        if vev & EV_FOCUS != 0 {
766            stt |= SS_FOCUSED as u32;
767        }
768        self.wp.ctx.last_state = stt;
769        self.sem(K_LIST, id, None, cur).max = n;
770        out
771    }
772
773    // ------------------------------------------------------------------ badges, dots, meters
774
775    /// ui_badge(kind, label): a status pill (BADGE_*; next_icon puts an icon before the text);
776    /// placed narrower than the pill (a grid cell), the label is cut with "…"
777    #[cfg_attr(jfgui_dev, track_caller)]
778    pub fn badge(&mut self, kind: u32, label: &str) {
779        let _src = self.src_scope();
780        let label = crate::basic::cstr(label);
781        let icon = self.take_icon();
782        self.take_flags();
783        let sd = self.style.data();
784        let mut short = String::new();
785        let mut sp = Sp { flags: kind & 7, icon, label: Some(label), ..Sp::default() };
786        let (mut w, h) = sd.badge_size(self, &mut sp);
787        let r = self.place(w, h);
788        // too narrow: the label cut to the room the pill's padding leaves (measured again by
789        // the style, whose font may differ a little)
790        if w > r[2] {
791            self.ts_font(TS_CAPTION);
792            let mut room = r[2] - (w - self.text.width(label));
793            for _ in 0..4 {
794                self.ts_font(TS_CAPTION);
795                short = self.ellipsize(label.as_bytes(), room).0;
796                let mut t = Sp { label: Some(&short), ..sp };
797                let (sw, _) = sd.badge_size(self, &mut t);
798                w = sw;
799                if sw <= r[2] || short == "…" {
800                    break;
801                }
802                room -= sw - r[2];
803            }
804            sp.label = Some(&short);
805            sd.badge_size(self, &mut sp);
806        }
807        // keep the pill's own size inside a stretched cell
808        let (bw, bh) = (w.min(r[2]), h.min(r[3]));
809        sp.place([r[0], r[1] + ((r[3] - bh) >> 1), bw, bh]);
810        sd.badge(self, &sp);
811        self.wp.ctx.last_state = 0;
812        self.sem(K_TEXT, 0, Some(label), kind as i32);
813    }
814
815    /// ui_status_dot(kind): a 10 DIP status dot (BADGE_*)
816    #[cfg_attr(jfgui_dev, track_caller)]
817    pub fn status_dot(&mut self, kind: u32) {
818        let _src = self.src_scope();
819        let s = self.dp(10);
820        let r = self.place(s, s);
821        let sd = self.style.data();
822        let mut sp = Sp { flags: kind & 7, ..Sp::default() };
823        sp.place([r[0] + ((r[2] - s) >> 1), r[1] + ((r[3] - s) >> 1), s, s]);
824        sd.status_dot(self, &sp);
825    }
826
827    /// ui_meter(id, v, max, kind): a capacity bar (natural 120 x 8 DIP; next_size)
828    #[cfg_attr(jfgui_dev, track_caller)]
829    pub fn meter(&mut self, id: u32, v: i64, max: i64, kind: u32) {
830        let _src = self.src_scope();
831        let r = self.place(self.dp(120), self.dp(8));
832        // scale 64-bit values (bytes) into the i32 of the record
833        let (mut a, mut b) = (v.max(0), max.max(0));
834        while b > i32::MAX as i64 {
835            a >>= 4;
836            b >>= 4;
837        }
838        let sd = self.style.data();
839        let mut sp = Sp { flags: kind & 7, value: a as i32, value2: b as i32, ..Sp::default() };
840        sp.place(r);
841        sd.meter(self, &sp);
842        self.wp.ctx.last_state = 0;
843        self.sem(K_PROGRESS, id, None, a as i32).max = b as i32;
844    }
845}