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 {
206 inst,
207 w,
208 u: self.u,
209 })
210 }
211
212 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 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}