1use std::collections::HashMap;
22
23use yog_api::ui::{Action, StateFlags, Units, Widget, widget};
24
25use crate::Update;
26
27pub const SELECT: u16 = Action::SELECT;
29
30const OVERSCAN: u32 = 2;
32const FIRST_WINDOW: u32 = 32;
34
35pub struct ItemsHost<T> {
37 owner: u16,
38 path: &'static [u16],
39 item_height: Option<f32>,
40 scroller: Option<u16>,
41 selectable: bool,
42 insts: Vec<(u64, T)>,
44 empty: Option<Widget>,
46 showing_empty: bool,
47}
48
49impl<T> ItemsHost<T> {
50 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 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 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
128pub struct Item<'c, T> {
130 pub inst: &'c mut T,
131 pub w: &'c mut Widget,
132 pub u: &'c mut Update,
133}
134
135pub 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
154fn 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 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 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 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}