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