Skip to main content

yog_kit/
items.rs

1//! Lists: an [`ItemsHost`] keeps one template instance per item of an
2//! `ItemsControl` / `ListView` / `ContentPresenter`, reconciled by key, so
3//! an update only patches what changed. With a fixed `item_height` only the
4//! rows in (and near) view are built — the rest are two spacers.
5//!
6//! Generated code drives it like this:
7//!
8//! ```ignore
9//! let mut it = self.l0.begin(root, u);
10//! for (index, item) in vm.visible().enumerate() {
11//!     let selected = bind::is_selected(&sel, &item.id);
12//!     let Some(row) = it.next(bind::key(&item.id), selected, __T0::build) else { continue };
13//!     if row.inst.b0.set(bind::text(&item.name)) { row.w.at_path_mut(&[0]).set_text(...); row.u.relayout(); }
14//! }
15//! if let Some((w, u)) = it.finish(Some(__E0)) { /* empty-state bindings */ }
16//! ```
17
18use std::collections::HashMap;
19
20use yog_api::ui::{Action, StateFlags, Units, Widget, widget};
21
22use crate::Update;
23
24/// `Action::slot` of an item container: clicking it selects the item.
25pub const SELECT: u16 = Action::SELECT;
26
27/// Rows built beyond each edge of the visible window.
28const OVERSCAN: u32 = 2;
29/// Rows built before the list's height is known.
30const FIRST_WINDOW: u32 = 32;
31
32/// The instances of one list.
33pub struct ItemsHost<T> {
34    owner: u16,
35    path: &'static [u16],
36    item_height: Option<f32>,
37    scroller: Option<u16>,
38    selectable: bool,
39    /// Keys and instances of the built items, in child order.
40    insts: Vec<(u64, T)>,
41    /// The empty-state widget while it isn't shown.
42    empty: Option<Widget>,
43    showing_empty: bool,
44}
45
46impl<T> ItemsHost<T> {
47    /// The list `owner` whose items container is at `path`; `item_height`
48    /// virtualizes it (rows are that tall), `scroller` is the index of its
49    /// scroll view in the view's parts; `selectable` gives item containers
50    /// the [`SELECT`] action.
51    pub fn new(
52        owner: u16,
53        path: &'static [u16],
54        item_height: Option<f32>,
55        scroller: Option<u16>,
56        selectable: bool,
57    ) -> Self {
58        Self {
59            owner,
60            path,
61            item_height,
62            scroller,
63            selectable,
64            insts: Vec::new(),
65            empty: None,
66            showing_empty: false,
67        }
68    }
69
70    /// Starts an update of the items under `root` (the tree `path` is
71    /// relative to).
72    pub fn begin<'a>(&'a mut self, root: &'a mut Widget, u: &'a mut Update) -> ItemsCursor<'a, T> {
73        let container = root.at_path_mut(self.path);
74        let mut old_children = std::mem::take(&mut container.children);
75        if self.showing_empty {
76            self.empty = old_children.pop();
77            self.showing_empty = false;
78        }
79        let window = self.item_height.map(|ih| {
80            let stride = ih + container.style.gap;
81            let vp = self.scroller.map(|s| u.viewport(s)).unwrap_or_default();
82            if vp.height <= 0.0 || stride <= 0.0 {
83                return (0, FIRST_WINDOW);
84            }
85            let top = (vp.offset - container.style.pad[0]).max(0.0);
86            let first = (top / stride) as u32;
87            let rows = (vp.height / stride).ceil() as u32 + 1;
88            (first.saturating_sub(OVERSCAN), first + rows + OVERSCAN)
89        });
90        if window.is_some() {
91            // [top spacer, items..., bottom spacer]
92            if !old_children.is_empty() {
93                old_children.remove(0);
94                old_children.pop();
95            }
96        }
97        let mut old = HashMap::with_capacity(self.insts.len());
98        for ((key, inst), w) in self.insts.drain(..).zip(old_children) {
99            old.entry(key).or_insert((inst, w));
100        }
101        ItemsCursor {
102            owner: self.owner,
103            selectable: self.selectable,
104            item_height: self.item_height,
105            window,
106            host_insts: &mut self.insts,
107            host_empty: &mut self.empty,
108            showing_empty: &mut self.showing_empty,
109            container,
110            u,
111            old,
112            widgets: Vec::new(),
113            index: 0,
114            first_built: None,
115            last_built: 0,
116            selected: None,
117        }
118    }
119}
120
121/// One item handed to the template's bindings.
122pub struct Item<'c, T> {
123    pub inst: &'c mut T,
124    pub w: &'c mut Widget,
125    pub u: &'c mut Update,
126}
127
128/// An update of an [`ItemsHost`] in progress.
129pub struct ItemsCursor<'a, T> {
130    owner: u16,
131    selectable: bool,
132    item_height: Option<f32>,
133    window: Option<(u32, u32)>,
134    host_insts: &'a mut Vec<(u64, T)>,
135    host_empty: &'a mut Option<Widget>,
136    showing_empty: &'a mut bool,
137    container: &'a mut Widget,
138    u: &'a mut Update,
139    old: HashMap<u64, (T, Widget)>,
140    widgets: Vec<Widget>,
141    index: u32,
142    first_built: Option<u32>,
143    last_built: u32,
144    selected: Option<u32>,
145}
146
147/// Points every action of this list in `w`'s subtree at item `index`.
148fn stamp(w: &mut Widget, owner: u16, index: u32) {
149    if let Some(a) = &mut w.action
150        && a.owner == owner
151    {
152        a.index = index;
153    }
154    for c in &mut w.children {
155        stamp(c, owner, index);
156    }
157}
158
159impl<'a, T> ItemsCursor<'a, T> {
160    /// The next item, keyed `key` (see [`crate::bind::key`]); `build`
161    /// makes a new instance. `None` when it's outside the visible window
162    /// (virtualized lists) — skip its bindings.
163    pub fn next(
164        &mut self,
165        key: u64,
166        selected: bool,
167        build: impl FnOnce() -> (T, Widget),
168    ) -> Option<Item<'_, T>> {
169        let index = self.index;
170        self.index += 1;
171        if selected {
172            self.selected = Some(index);
173        }
174        if let Some((first, last)) = self.window
175            && (index < first || index >= last)
176        {
177            return None;
178        }
179        let (inst, mut w) = match self.old.remove(&key) {
180            Some(x) => x,
181            None => {
182                let (inst, mut w) = build();
183                if self.selectable && w.action.is_none() {
184                    w.action = Some(Action {
185                        owner: self.owner,
186                        slot: SELECT,
187                        index,
188                    });
189                }
190                self.u.relayout();
191                (inst, w)
192            }
193        };
194        stamp(&mut w, self.owner, index);
195        if w.state.contains(StateFlags::SELECTED) != selected {
196            w.set_state_inherited(StateFlags::SELECTED, selected);
197            self.u.repaint();
198        }
199        self.first_built.get_or_insert(index);
200        self.last_built = index + 1;
201        self.host_insts.push((key, inst));
202        self.widgets.push(w);
203        let (_, inst) = self.host_insts.last_mut().expect("just pushed");
204        let w = self.widgets.last_mut().expect("just pushed");
205        Some(Item { inst, w, u: self.u })
206    }
207
208    /// Ends the update. With `empty` (the `.Empty` template's builder) and
209    /// no items, shows that widget and returns it for its bindings.
210    pub fn finish(self, empty: Option<fn() -> Widget>) -> Option<(&'a mut Widget, &'a mut Update)> {
211        let n = self.index;
212        if !self.old.is_empty() {
213            self.u.relayout();
214        }
215        self.u.list_info(self.owner, n, self.selected);
216        let mut children = self.widgets;
217        if let (Some(ih), Some(_)) = (self.item_height, self.window) {
218            let stride = ih + self.container.style.gap;
219            let gap = self.container.style.gap;
220            let first = self
221                .first_built
222                .unwrap_or(n.min(self.window.map_or(0, |w| w.0)));
223            let after = n.saturating_sub(self.last_built.max(first));
224            let spacer = |rows: u32| {
225                widget::spacer()
226                    .w(Units::Px(0.0))
227                    .h(Units::Px((rows as f32 * stride - gap).max(0.0)))
228                    .visible(rows > 0)
229            };
230            children.insert(0, spacer(first));
231            children.push(spacer(after));
232        }
233        if n == 0
234            && let Some(build) = empty
235        {
236            let w = self.host_empty.take().unwrap_or_else(build);
237            children.push(w);
238            *self.showing_empty = true;
239            self.container.children = children;
240            let w = self.container.children.last_mut().expect("just pushed");
241            return Some((w, self.u));
242        }
243        self.container.children = children;
244        None
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use crate::host::Viewport;
252    use yog_api::ui::FlexDir;
253
254    struct Row;
255
256    fn row() -> (Row, Widget) {
257        (Row, widget::label(""))
258    }
259
260    fn root() -> Widget {
261        widget::panel(FlexDir::Column).child(widget::scroll_view().gap(2.0))
262    }
263
264    fn run(
265        host: &mut ItemsHost<Row>,
266        root: &mut Widget,
267        u: &mut Update,
268        keys: &[u64],
269        sel: u64,
270    ) -> u32 {
271        let mut built = 0;
272        let mut it = host.begin(root, u);
273        for &k in keys {
274            if let Some(item) = it.next(k, k == sel, row) {
275                let Row = item.inst;
276                item.w.set_text(k.to_string());
277                built += 1;
278            }
279        }
280        it.finish(Some(|| widget::label("empty")));
281        built
282    }
283
284    #[test]
285    fn reconciles_by_key_and_stamps_indices() {
286        let mut host = ItemsHost::new(1, &[0], None, None, true);
287        let mut r = root();
288        let mut u = Update::new();
289        run(&mut host, &mut r, &mut u, &[10, 20, 30], 20);
290        let list = &r.children[0];
291        assert_eq!(list.children.len(), 3);
292        assert_eq!(list.children[1].action.unwrap().index, 1);
293        assert!(list.children[1].state.contains(StateFlags::SELECTED));
294        assert_eq!(u.lists, vec![(1, 3, Some(1))]);
295
296        let mut u = Update::new();
297        run(&mut host, &mut r, &mut u, &[30, 10], 0);
298        let list = &r.children[0];
299        assert_eq!(list.children.len(), 2);
300        assert_eq!(list.children[0].action.unwrap().index, 0);
301        assert!(!list.children[1].state.contains(StateFlags::SELECTED));
302        assert!(u.needs_relayout(), "an item was removed");
303
304        let mut u = Update::new();
305        run(&mut host, &mut r, &mut u, &[], 0);
306        let list = &r.children[0];
307        assert_eq!(list.children.len(), 1, "empty state shown");
308        let mut u = Update::new();
309        run(&mut host, &mut r, &mut u, &[5], 0);
310        assert_eq!(r.children[0].children.len(), 1);
311        assert!(host.empty.is_some(), "empty widget kept for later");
312    }
313
314    #[test]
315    fn virtualizes_with_item_height() {
316        let mut host = ItemsHost::new(1, &[0], Some(10.0), Some(0), true);
317        let mut r = root();
318        let keys: Vec<u64> = (0..1000).collect();
319        let mut u = Update {
320            viewports: vec![Viewport {
321                offset: 1200.0,
322                height: 60.0,
323            }],
324            ..Update::new()
325        };
326        let built = run(&mut host, &mut r, &mut u, &keys, 0);
327        // stride 12: first visible row 100, 5 rows + 1, overscan 2 each side
328        assert_eq!(built, 10);
329        let list = &r.children[0];
330        assert_eq!(list.children.len(), built as usize + 2);
331        let top = &list.children[0];
332        assert_eq!(top.style.h, Units::Px(98.0 * 12.0 - 2.0));
333        assert_eq!(list.children[1].action.unwrap().index, 98);
334        let bottom = list.children.last().unwrap();
335        assert_eq!(bottom.style.h, Units::Px((1000.0 - 108.0) * 12.0 - 2.0));
336        assert_eq!(u.lists, vec![(1, 1000, Some(0))]);
337    }
338}