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::{widget, Action, StateFlags, Units, 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        if a.owner == owner {
151            a.index = index;
152        }
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            if index < first || index >= last {
176                return None;
177            }
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 {
206            inst,
207            w,
208            u: self.u,
209        })
210    }
211
212    /// Ends the update. With `empty` (the `.Empty` template's builder) and
213    /// no items, shows that widget and returns it for its bindings.
214    pub fn finish(self, empty: Option<fn() -> Widget>) -> Option<(&'a mut Widget, &'a mut Update)> {
215        let n = self.index;
216        if !self.old.is_empty() {
217            self.u.relayout();
218        }
219        self.u.list_info(self.owner, n, self.selected);
220        let mut children = self.widgets;
221        if let (Some(ih), Some(_)) = (self.item_height, self.window) {
222            let stride = ih + self.container.style.gap;
223            let gap = self.container.style.gap;
224            let first = self.first_built.unwrap_or(n.min(self.window.map_or(0, |w| w.0)));
225            let after = n.saturating_sub(self.last_built.max(first));
226            let spacer = |rows: u32| {
227                widget::spacer()
228                    .w(Units::Px(0.0))
229                    .h(Units::Px((rows as f32 * stride - gap).max(0.0)))
230                    .visible(rows > 0)
231            };
232            children.insert(0, spacer(first));
233            children.push(spacer(after));
234        }
235        if n == 0 {
236            if let Some(build) = empty {
237                let w = self.host_empty.take().unwrap_or_else(build);
238                children.push(w);
239                *self.showing_empty = true;
240                self.container.children = children;
241                let w = self.container.children.last_mut().expect("just pushed");
242                return Some((w, self.u));
243            }
244        }
245        self.container.children = children;
246        None
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use crate::host::Viewport;
254    use yog_api::ui::FlexDir;
255
256    struct Row;
257
258    fn row() -> (Row, Widget) {
259        (Row, widget::label(""))
260    }
261
262    fn root() -> Widget {
263        widget::panel(FlexDir::Column).child(widget::scroll_view().gap(2.0))
264    }
265
266    fn run(host: &mut ItemsHost<Row>, root: &mut Widget, u: &mut Update, keys: &[u64], sel: u64) -> u32 {
267        let mut built = 0;
268        let mut it = host.begin(root, u);
269        for &k in keys {
270            if let Some(item) = it.next(k, k == sel, row) {
271                let Row = item.inst;
272                item.w.set_text(k.to_string());
273                built += 1;
274            }
275        }
276        it.finish(Some(|| widget::label("empty")));
277        built
278    }
279
280    #[test]
281    fn reconciles_by_key_and_stamps_indices() {
282        let mut host = ItemsHost::new(1, &[0], None, None, true);
283        let mut r = root();
284        let mut u = Update::new();
285        run(&mut host, &mut r, &mut u, &[10, 20, 30], 20);
286        let list = &r.children[0];
287        assert_eq!(list.children.len(), 3);
288        assert_eq!(list.children[1].action.unwrap().index, 1);
289        assert!(list.children[1].state.contains(StateFlags::SELECTED));
290        assert_eq!(u.lists, vec![(1, 3, Some(1))]);
291
292        let mut u = Update::new();
293        run(&mut host, &mut r, &mut u, &[30, 10], 0);
294        let list = &r.children[0];
295        assert_eq!(list.children.len(), 2);
296        assert_eq!(list.children[0].action.unwrap().index, 0);
297        assert!(!list.children[1].state.contains(StateFlags::SELECTED));
298        assert!(u.needs_relayout(), "an item was removed");
299
300        let mut u = Update::new();
301        run(&mut host, &mut r, &mut u, &[], 0);
302        let list = &r.children[0];
303        assert_eq!(list.children.len(), 1, "empty state shown");
304        let mut u = Update::new();
305        run(&mut host, &mut r, &mut u, &[5], 0);
306        assert_eq!(r.children[0].children.len(), 1);
307        assert!(host.empty.is_some(), "empty widget kept for later");
308    }
309
310    #[test]
311    fn virtualizes_with_item_height() {
312        let mut host = ItemsHost::new(1, &[0], Some(10.0), Some(0), true);
313        let mut r = root();
314        let keys: Vec<u64> = (0..1000).collect();
315        let mut u = Update {
316            viewports: vec![Viewport {
317                offset: 1200.0,
318                height: 60.0,
319            }],
320            ..Update::new()
321        };
322        let built = run(&mut host, &mut r, &mut u, &keys, 0);
323        // stride 12: first visible row 100, 5 rows + 1, overscan 2 each side
324        assert_eq!(built, 10);
325        let list = &r.children[0];
326        assert_eq!(list.children.len(), built as usize + 2);
327        let top = &list.children[0];
328        assert_eq!(top.style.h, Units::Px(98.0 * 12.0 - 2.0));
329        assert_eq!(list.children[1].action.unwrap().index, 98);
330        let bottom = list.children.last().unwrap();
331        assert_eq!(bottom.style.h, Units::Px((1000.0 - 108.0) * 12.0 - 2.0));
332        assert_eq!(u.lists, vec![(1, 1000, Some(0))]);
333    }
334}