1use std::collections::HashMap;
19
20use yog_api::ui::{widget, Action, StateFlags, Units, Widget};
21
22use crate::Update;
23
24pub const SELECT: u16 = Action::SELECT;
26
27const OVERSCAN: u32 = 2;
29const FIRST_WINDOW: u32 = 32;
31
32pub struct ItemsHost<T> {
34 owner: u16,
35 path: &'static [u16],
36 item_height: Option<f32>,
37 scroller: Option<u16>,
38 selectable: bool,
39 insts: Vec<(u64, T)>,
41 empty: Option<Widget>,
43 showing_empty: bool,
44}
45
46impl<T> ItemsHost<T> {
47 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 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 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
121pub struct Item<'c, T> {
123 pub inst: &'c mut T,
124 pub w: &'c mut Widget,
125 pub u: &'c mut Update,
126}
127
128pub 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
147fn 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 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 { inst, w, u: self.u })
206 }
207
208 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 if let Some(build) = empty {
235 let w = self.host_empty.take().unwrap_or_else(build);
236 children.push(w);
237 *self.showing_empty = true;
238 self.container.children = children;
239 let w = self.container.children.last_mut().expect("just pushed");
240 return Some((w, self.u));
241 }
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 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}