1use crate::{
2 geometry::{
3 rect::RectF,
4 vector::{vec2f, Vector2F},
5 },
6 json::json,
7 sum_tree::{self, Bias, SumTree},
8 DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
9 SizeConstraint,
10};
11use std::{cell::RefCell, ops::Range, rc::Rc};
12
13pub struct List {
14 state: ListState,
15}
16
17#[derive(Clone)]
18pub struct ListState(Rc<RefCell<StateInner>>);
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum Orientation {
22 Top,
23 Bottom,
24}
25
26struct StateInner {
27 last_layout_width: Option<f32>,
28 render_item: Box<dyn FnMut(usize, &mut LayoutContext) -> ElementBox>,
29 rendered_range: Range<usize>,
30 items: SumTree<ListItem>,
31 logical_scroll_top: Option<ListOffset>,
32 orientation: Orientation,
33 overdraw: f32,
34 scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut EventContext)>>,
35}
36
37#[derive(Clone, Copy, Debug, Default, PartialEq)]
38pub struct ListOffset {
39 item_ix: usize,
40 offset_in_item: f32,
41}
42
43#[derive(Clone)]
44enum ListItem {
45 Unrendered,
46 Rendered(ElementRc),
47 Removed(f32),
48}
49
50impl std::fmt::Debug for ListItem {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 Self::Unrendered => write!(f, "Unrendered"),
54 Self::Rendered(_) => f.debug_tuple("Rendered").finish(),
55 Self::Removed(height) => f.debug_tuple("Removed").field(height).finish(),
56 }
57 }
58}
59
60#[derive(Clone, Debug, Default, PartialEq)]
61struct ListItemSummary {
62 count: usize,
63 rendered_count: usize,
64 unrendered_count: usize,
65 height: f32,
66}
67
68#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
69struct Count(usize);
70
71#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
72struct RenderedCount(usize);
73
74#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
75struct UnrenderedCount(usize);
76
77#[derive(Clone, Debug, Default)]
78struct Height(f32);
79
80impl List {
81 pub fn new(state: ListState) -> Self {
82 Self { state }
83 }
84}
85
86impl Element for List {
87 type LayoutState = ListOffset;
88 type PaintState = ();
89
90 fn layout(
91 &mut self,
92 constraint: SizeConstraint,
93 cx: &mut LayoutContext,
94 ) -> (Vector2F, Self::LayoutState) {
95 let state = &mut *self.state.0.borrow_mut();
96 let size = constraint.max;
97 let mut item_constraint = constraint;
98 item_constraint.min.set_y(0.);
99 item_constraint.max.set_y(f32::INFINITY);
100
101 if state.last_layout_width != Some(size.x()) {
102 state.rendered_range = 0..0;
103 state.items = SumTree::from_iter(
104 (0..state.items.summary().count).map(|_| ListItem::Unrendered),
105 &(),
106 )
107 }
108
109 let overdraw = state.overdraw;
110 let old_rendered_range = state.rendered_range.clone();
111 let old_items = state.items.clone();
112 let orientation = state.orientation;
113 let mut stored_scroll_top = state.logical_scroll_top;
114 let mut new_items = SumTree::new();
115
116 let mut render_item = |ix, old_item: &ListItem| {
117 let element = if let ListItem::Rendered(element) = old_item {
118 element.clone()
119 } else {
120 let mut element = (state.render_item)(ix, cx);
121 element.layout(item_constraint, cx);
122 element.into()
123 };
124 element
125 };
126
127 // Determine the items that need to be rendered in order to cover the visible area plus the
128 // overdraw. When parked at the end of a bottom-oriented list, this requires rendering items
129 // starting from the end of the list until the visible region is full. In other cases, the
130 // stored scroll can be used.
131 let mut new_rendered_start_ix;
132 let mut rendered_items = Vec::new();
133 let mut scroll_top;
134 if let (Orientation::Bottom, None) = (orientation, stored_scroll_top) {
135 let mut rendered_height = 0.;
136 let mut cursor = old_items.cursor::<Count, ()>();
137 cursor.seek(&Count(old_items.summary().count), Bias::Left, &());
138
139 while let Some(item) = cursor.item() {
140 let element = render_item(cursor.seek_start().0, item);
141 rendered_height += element.size().y();
142 rendered_items.push(ListItem::Rendered(element));
143 if rendered_height >= size.y() {
144 break;
145 } else {
146 cursor.prev(&());
147 }
148 }
149 scroll_top = ListOffset {
150 item_ix: cursor.seek_start().0,
151 offset_in_item: rendered_height - size.y(),
152 };
153
154 cursor.prev(&());
155 while let Some(item) = cursor.item() {
156 let element = render_item(cursor.seek_start().0, item);
157 rendered_height += element.size().y();
158 rendered_items.push(ListItem::Rendered(element));
159 if rendered_height >= size.y() + overdraw {
160 break;
161 } else {
162 cursor.prev(&());
163 }
164 }
165
166 rendered_items.reverse();
167 new_rendered_start_ix = cursor.seek_start().0;
168 } else {
169 scroll_top = stored_scroll_top.unwrap_or_default();
170 let mut rendered_height = 0.;
171 let mut cursor = old_items.cursor::<Count, ()>();
172 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
173 for (ix, item) in cursor.by_ref().enumerate() {
174 if rendered_height >= scroll_top.offset_in_item + size.y() + overdraw {
175 break;
176 }
177
178 let element = render_item(scroll_top.item_ix + ix, item);
179 rendered_height += element.size().y();
180 rendered_items.push(ListItem::Rendered(element));
181 }
182 rendered_items.reverse();
183
184 let mut adjust_scroll_top = (rendered_height - scroll_top.offset_in_item) < size.y();
185
186 new_rendered_start_ix = scroll_top.item_ix;
187 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
188 cursor.prev(&());
189 let mut remaining_overdraw = overdraw - scroll_top.offset_in_item;
190
191 if adjust_scroll_top && rendered_height >= size.y() {
192 let offset_in_item = rendered_height - size.y();
193 scroll_top = ListOffset {
194 item_ix: new_rendered_start_ix,
195 offset_in_item,
196 };
197 stored_scroll_top = Some(scroll_top);
198 remaining_overdraw = overdraw - offset_in_item;
199 adjust_scroll_top = false;
200 }
201
202 while let Some(item) = cursor.item() {
203 let element = render_item(cursor.seek_start().0, item);
204 let next_rendered_height = rendered_height + element.size().y();
205
206 if adjust_scroll_top && next_rendered_height >= size.y() {
207 let offset_in_item = next_rendered_height - size.y();
208 scroll_top = ListOffset {
209 item_ix: new_rendered_start_ix - 1,
210 offset_in_item,
211 };
212 stored_scroll_top = Some(scroll_top);
213 remaining_overdraw = overdraw + (element.size().y() - offset_in_item);
214 adjust_scroll_top = false;
215 }
216
217 if !adjust_scroll_top && remaining_overdraw <= 0. {
218 break;
219 }
220
221 rendered_height = next_rendered_height;
222 remaining_overdraw -= element.size().y();
223 rendered_items.push(ListItem::Rendered(element));
224
225 new_rendered_start_ix -= 1;
226
227 cursor.prev(&());
228 }
229 rendered_items.reverse();
230
231 if rendered_height < size.y() {
232 stored_scroll_top = None;
233 scroll_top = match orientation {
234 Orientation::Top => Default::default(),
235 Orientation::Bottom => ListOffset {
236 item_ix: cursor.seek_start().0,
237 offset_in_item: rendered_height - size.y(),
238 },
239 };
240 }
241 }
242 let new_rendered_range =
243 new_rendered_start_ix..new_rendered_start_ix + rendered_items.len();
244
245 let mut cursor = old_items.cursor::<Count, ()>();
246
247 // Discard any rendered elements before the new rendered range.
248 if old_rendered_range.start < new_rendered_range.start {
249 new_items.push_tree(
250 cursor.slice(&Count(old_rendered_range.start), Bias::Right, &()),
251 &(),
252 );
253 let remove_to = old_rendered_range.end.min(new_rendered_range.start);
254 while cursor.seek_start().0 < remove_to {
255 new_items.push(cursor.item().unwrap().remove(), &());
256 cursor.next(&());
257 }
258 }
259
260 new_items.push_tree(
261 cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()),
262 &(),
263 );
264 new_items.extend(rendered_items, &());
265 cursor.seek(&Count(new_rendered_range.end), Bias::Right, &());
266
267 if new_rendered_range.end < old_rendered_range.start {
268 new_items.push_tree(
269 cursor.slice(&Count(old_rendered_range.start), Bias::Right, &()),
270 &(),
271 );
272 }
273
274 while cursor.seek_start().0 < old_rendered_range.end {
275 new_items.push(cursor.item().unwrap().remove(), &());
276 cursor.next(&());
277 }
278
279 new_items.push_tree(cursor.suffix(&()), &());
280
281 drop(cursor);
282 state.items = new_items;
283 state.rendered_range = new_rendered_range;
284 state.last_layout_width = Some(size.x());
285 state.logical_scroll_top = stored_scroll_top;
286 (size, scroll_top)
287 }
288
289 fn paint(&mut self, bounds: RectF, scroll_top: &mut ListOffset, cx: &mut PaintContext) {
290 cx.scene.push_layer(Some(bounds));
291
292 let state = &mut *self.state.0.borrow_mut();
293 for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
294 element.paint(origin, cx);
295 }
296
297 cx.scene.pop_layer();
298 }
299
300 fn dispatch_event(
301 &mut self,
302 event: &Event,
303 bounds: RectF,
304 scroll_top: &mut ListOffset,
305 _: &mut (),
306 cx: &mut EventContext,
307 ) -> bool {
308 let mut handled = false;
309
310 let mut state = self.state.0.borrow_mut();
311 for (mut element, _) in state.visible_elements(bounds, scroll_top) {
312 handled = element.dispatch_event(event, cx) || handled;
313 }
314
315 match event {
316 Event::ScrollWheel {
317 position,
318 delta,
319 precise,
320 } => {
321 if bounds.contains_point(*position) {
322 if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) {
323 handled = true;
324 }
325 }
326 }
327 _ => {}
328 }
329
330 handled
331 }
332
333 fn debug(
334 &self,
335 bounds: RectF,
336 scroll_top: &Self::LayoutState,
337 _: &(),
338 cx: &DebugContext,
339 ) -> serde_json::Value {
340 let state = self.state.0.borrow_mut();
341 let visible_elements = state
342 .visible_elements(bounds, scroll_top)
343 .map(|e| e.0.debug(cx))
344 .collect::<Vec<_>>();
345 let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len());
346 json!({
347 "visible_range": visible_range,
348 "visible_elements": visible_elements,
349 "scroll_top": state.logical_scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
350 })
351 }
352}
353
354impl ListState {
355 pub fn new<F>(
356 element_count: usize,
357 orientation: Orientation,
358 overdraw: f32,
359 render_item: F,
360 ) -> Self
361 where
362 F: 'static + FnMut(usize, &mut LayoutContext) -> ElementBox,
363 {
364 let mut items = SumTree::new();
365 items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
366 Self(Rc::new(RefCell::new(StateInner {
367 last_layout_width: None,
368 render_item: Box::new(render_item),
369 rendered_range: 0..0,
370 items,
371 logical_scroll_top: None,
372 orientation,
373 overdraw,
374 scroll_handler: None,
375 })))
376 }
377
378 pub fn reset(&self, element_count: usize) {
379 let state = &mut *self.0.borrow_mut();
380 state.logical_scroll_top = None;
381 state.items = SumTree::new();
382 state
383 .items
384 .extend((0..element_count).map(|_| ListItem::Unrendered), &());
385 }
386
387 pub fn splice(&self, old_range: Range<usize>, count: usize) {
388 let state = &mut *self.0.borrow_mut();
389
390 if let Some(ListOffset {
391 item_ix,
392 offset_in_item,
393 }) = state.logical_scroll_top.as_mut()
394 {
395 if old_range.contains(item_ix) {
396 *item_ix = old_range.start;
397 *offset_in_item = 0.;
398 } else if old_range.end <= *item_ix {
399 *item_ix = *item_ix - (old_range.end - old_range.start) + count;
400 }
401 }
402
403 let new_end = old_range.start + count;
404 if old_range.start < state.rendered_range.start {
405 state.rendered_range.start =
406 new_end + state.rendered_range.start.saturating_sub(old_range.end);
407 }
408 if old_range.start < state.rendered_range.end {
409 state.rendered_range.end =
410 new_end + state.rendered_range.end.saturating_sub(old_range.end);
411 }
412
413 let mut old_heights = state.items.cursor::<Count, ()>();
414 let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
415 old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
416
417 new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
418 new_heights.push_tree(old_heights.suffix(&()), &());
419 drop(old_heights);
420 state.items = new_heights;
421 }
422
423 pub fn set_scroll_handler(
424 &mut self,
425 handler: impl FnMut(Range<usize>, &mut EventContext) + 'static,
426 ) {
427 self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
428 }
429}
430
431impl StateInner {
432 fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range<usize> {
433 let mut cursor = self.items.cursor::<Count, Height>();
434 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
435 let start_y = cursor.sum_start().0 + scroll_top.offset_in_item;
436 let mut cursor = cursor.swap_dimensions();
437 cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
438 scroll_top.item_ix..cursor.sum_start().0 + 1
439 }
440
441 fn visible_elements<'a>(
442 &'a self,
443 bounds: RectF,
444 scroll_top: &ListOffset,
445 ) -> impl Iterator<Item = (ElementRc, Vector2F)> + 'a {
446 let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
447 let mut cursor = self.items.cursor::<Count, ()>();
448 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
449 std::iter::from_fn(move || {
450 while let Some(item) = cursor.item() {
451 if item_origin.y() > bounds.max_y() {
452 break;
453 }
454
455 if let ListItem::Rendered(element) = item {
456 let result = (element.clone(), item_origin);
457 item_origin.set_y(item_origin.y() + element.size().y());
458 cursor.next(&());
459 return Some(result);
460 }
461
462 cursor.next(&());
463 }
464
465 None
466 })
467 }
468
469 fn scroll(
470 &mut self,
471 scroll_top: &ListOffset,
472 height: f32,
473 mut delta: Vector2F,
474 precise: bool,
475 cx: &mut EventContext,
476 ) -> bool {
477 if !precise {
478 delta *= 20.;
479 }
480
481 let scroll_max = (self.items.summary().height - height).max(0.);
482 let new_scroll_top = (self.scroll_top(scroll_top) + delta.y())
483 .max(0.)
484 .min(scroll_max);
485
486 if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
487 self.logical_scroll_top = None;
488 } else {
489 let mut cursor = self.items.cursor::<Height, Count>();
490 cursor.seek(&Height(new_scroll_top), Bias::Right, &());
491 let item_ix = cursor.sum_start().0;
492 let offset_in_item = new_scroll_top - cursor.seek_start().0;
493 self.logical_scroll_top = Some(ListOffset {
494 item_ix,
495 offset_in_item,
496 });
497 }
498
499 if self.scroll_handler.is_some() {
500 let visible_range = self.visible_range(height, scroll_top);
501 self.scroll_handler.as_mut().unwrap()(visible_range, cx);
502 }
503 cx.notify();
504
505 true
506 }
507
508 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
509 let mut cursor = self.items.cursor::<Count, Height>();
510 cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
511 cursor.sum_start().0 + logical_scroll_top.offset_in_item
512 }
513}
514
515impl ListItem {
516 fn remove(&self) -> Self {
517 match self {
518 ListItem::Unrendered => ListItem::Unrendered,
519 ListItem::Rendered(element) => ListItem::Removed(element.size().y()),
520 ListItem::Removed(height) => ListItem::Removed(*height),
521 }
522 }
523}
524
525impl sum_tree::Item for ListItem {
526 type Summary = ListItemSummary;
527
528 fn summary(&self) -> Self::Summary {
529 match self {
530 ListItem::Unrendered => ListItemSummary {
531 count: 1,
532 rendered_count: 0,
533 unrendered_count: 1,
534 height: 0.,
535 },
536 ListItem::Rendered(element) => ListItemSummary {
537 count: 1,
538 rendered_count: 1,
539 unrendered_count: 0,
540 height: element.size().y(),
541 },
542 ListItem::Removed(height) => ListItemSummary {
543 count: 1,
544 rendered_count: 0,
545 unrendered_count: 1,
546 height: *height,
547 },
548 }
549 }
550}
551
552impl sum_tree::Summary for ListItemSummary {
553 type Context = ();
554
555 fn add_summary(&mut self, summary: &Self, _: &()) {
556 self.count += summary.count;
557 self.rendered_count += summary.rendered_count;
558 self.unrendered_count += summary.unrendered_count;
559 self.height += summary.height;
560 }
561}
562
563impl<'a> sum_tree::Dimension<'a, ListItemSummary> for ListItemSummary {
564 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
565 sum_tree::Summary::add_summary(self, summary, &());
566 }
567}
568
569impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
570 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
571 self.0 += summary.count;
572 }
573}
574
575impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
576 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
577 self.0 += summary.rendered_count;
578 }
579}
580
581impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
582 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
583 self.0 += summary.unrendered_count;
584 }
585}
586
587impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
588 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
589 self.0 += summary.height;
590 }
591}
592
593impl<'a> sum_tree::SeekDimension<'a, ListItemSummary> for Height {
594 fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
595 self.0.partial_cmp(&other.0).unwrap()
596 }
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602 use crate::geometry::vector::vec2f;
603 use rand::prelude::*;
604 use std::env;
605
606 #[crate::test(self)]
607 fn test_layout(cx: &mut crate::MutableAppContext) {
608 let mut presenter = cx.build_presenter(0, 0.);
609 let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
610
611 let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
612 let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, {
613 let elements = elements.clone();
614 move |ix, _| {
615 let (id, height) = elements.borrow()[ix];
616 TestElement::new(id, height).boxed()
617 }
618 });
619
620 let mut list = List::new(state.clone());
621 let (size, _) = list.layout(constraint, &mut presenter.build_layout_context(cx));
622 assert_eq!(size, vec2f(100., 40.));
623 assert_eq!(
624 state.0.borrow().items.summary(),
625 ListItemSummary {
626 count: 3,
627 rendered_count: 3,
628 unrendered_count: 0,
629 height: 150.
630 }
631 );
632
633 state.0.borrow_mut().scroll(
634 &ListOffset {
635 item_ix: 0,
636 offset_in_item: 0.,
637 },
638 40.,
639 vec2f(0., 54.),
640 true,
641 &mut presenter.build_event_context(cx),
642 );
643 let (_, logical_scroll_top) =
644 list.layout(constraint, &mut presenter.build_layout_context(cx));
645 assert_eq!(
646 logical_scroll_top,
647 ListOffset {
648 item_ix: 2,
649 offset_in_item: 4.
650 }
651 );
652 assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.);
653
654 elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]);
655 elements.borrow_mut().push((5, 60.));
656 state.splice(1..2, 2);
657 state.splice(4..4, 1);
658 assert_eq!(
659 state.0.borrow().items.summary(),
660 ListItemSummary {
661 count: 5,
662 rendered_count: 2,
663 unrendered_count: 3,
664 height: 120.
665 }
666 );
667
668 let (size, logical_scroll_top) =
669 list.layout(constraint, &mut presenter.build_layout_context(cx));
670 assert_eq!(size, vec2f(100., 40.));
671 assert_eq!(
672 state.0.borrow().items.summary(),
673 ListItemSummary {
674 count: 5,
675 rendered_count: 5,
676 unrendered_count: 0,
677 height: 270.
678 }
679 );
680 assert_eq!(
681 logical_scroll_top,
682 ListOffset {
683 item_ix: 3,
684 offset_in_item: 4.
685 }
686 );
687 assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
688 }
689
690 #[crate::test(self, iterations = 10000, seed = 0)]
691 fn test_random(cx: &mut crate::MutableAppContext, mut rng: StdRng) {
692 let operations = env::var("OPERATIONS")
693 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
694 .unwrap_or(10);
695
696 let mut presenter = cx.build_presenter(0, 0.);
697 let mut next_id = 0;
698 let elements = Rc::new(RefCell::new(
699 (0..rng.gen_range(0..=20))
700 .map(|_| {
701 let id = next_id;
702 next_id += 1;
703 (id, rng.gen_range(0..=200) as f32 / 2.0)
704 })
705 .collect::<Vec<_>>(),
706 ));
707 let orientation = *[Orientation::Top, Orientation::Bottom]
708 .choose(&mut rng)
709 .unwrap();
710 let overdraw = rng.gen_range(1..=100) as f32;
711 let state = ListState::new(elements.borrow().len(), orientation, overdraw, {
712 let elements = elements.clone();
713 move |ix, _| {
714 let (id, height) = elements.borrow()[ix];
715 TestElement::new(id, height).boxed()
716 }
717 });
718
719 let mut width = rng.gen_range(0..=2000) as f32 / 2.;
720 let mut height = rng.gen_range(0..=2000) as f32 / 2.;
721 log::info!("orientation: {:?}", orientation);
722 log::info!("overdraw: {}", overdraw);
723 log::info!("elements: {:?}", elements.borrow());
724 log::info!("size: ({:?}, {:?})", width, height);
725 log::info!("==================");
726
727 let mut last_logical_scroll_top = None;
728 for _ in 0..operations {
729 match rng.gen_range(0..=100) {
730 0..=29 if last_logical_scroll_top.is_some() => {
731 let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw));
732 log::info!(
733 "Scrolling by {:?}, previous scroll top: {:?}",
734 delta,
735 last_logical_scroll_top.unwrap()
736 );
737 state.0.borrow_mut().scroll(
738 last_logical_scroll_top.as_ref().unwrap(),
739 height,
740 delta,
741 true,
742 &mut presenter.build_event_context(cx),
743 );
744 }
745 30..=34 => {
746 width = rng.gen_range(0..=2000) as f32 / 2.;
747 log::info!("changing width: {:?}", width);
748 }
749 35..=54 => {
750 height = rng.gen_range(0..=1000) as f32 / 2.;
751 log::info!("changing height: {:?}", height);
752 }
753 _ => {
754 let mut elements = elements.borrow_mut();
755 let end_ix = rng.gen_range(0..=elements.len());
756 let start_ix = rng.gen_range(0..=end_ix);
757 let new_elements = (0..rng.gen_range(0..10))
758 .map(|_| {
759 let id = next_id;
760 next_id += 1;
761 (id, rng.gen_range(0..=200) as f32 / 2.)
762 })
763 .collect::<Vec<_>>();
764 log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements);
765 state.splice(start_ix..end_ix, new_elements.len());
766 elements.splice(start_ix..end_ix, new_elements);
767 for (ix, item) in state.0.borrow().items.cursor::<(), ()>().enumerate() {
768 if let ListItem::Rendered(element) = item {
769 let (expected_id, _) = elements[ix];
770 element.with_metadata(|metadata: Option<&usize>| {
771 assert_eq!(*metadata.unwrap(), expected_id);
772 });
773 }
774 }
775 }
776 }
777
778 let mut list = List::new(state.clone());
779 let (size, logical_scroll_top) = list.layout(
780 SizeConstraint::new(vec2f(0., 0.), vec2f(width, height)),
781 &mut presenter.build_layout_context(cx),
782 );
783 assert_eq!(size, vec2f(width, height));
784 last_logical_scroll_top = Some(logical_scroll_top);
785
786 let state = state.0.borrow();
787 log::info!("items {:?}", state.items.items(&()));
788
789 let scroll_top = state.scroll_top(&logical_scroll_top);
790 let rendered_top = (scroll_top - overdraw).max(0.);
791 let rendered_bottom = scroll_top + height + overdraw;
792 let mut item_top = 0.;
793
794 log::info!(
795 "rendered top {:?}, rendered bottom {:?}, scroll top {:?}",
796 rendered_top,
797 rendered_bottom,
798 scroll_top,
799 );
800
801 let mut first_rendered_element_top = None;
802 let mut last_rendered_element_bottom = None;
803 assert_eq!(state.items.summary().count, elements.borrow().len());
804 for (ix, item) in state.items.cursor::<(), ()>().enumerate() {
805 match item {
806 ListItem::Unrendered => {
807 let item_bottom = item_top;
808 assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
809 item_top = item_bottom;
810 }
811 ListItem::Removed(height) => {
812 let (id, expected_height) = elements.borrow()[ix];
813 assert_eq!(
814 *height, expected_height,
815 "element {} height didn't match",
816 id
817 );
818 let item_bottom = item_top + height;
819 assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
820 item_top = item_bottom;
821 }
822 ListItem::Rendered(element) => {
823 let (expected_id, expected_height) = elements.borrow()[ix];
824 element.with_metadata(|metadata: Option<&usize>| {
825 assert_eq!(*metadata.unwrap(), expected_id);
826 });
827 assert_eq!(element.size().y(), expected_height);
828 let item_bottom = item_top + element.size().y();
829 first_rendered_element_top.get_or_insert(item_top);
830 last_rendered_element_bottom = Some(item_bottom);
831 assert!(item_bottom > rendered_top || item_top < rendered_bottom);
832 item_top = item_bottom;
833 }
834 }
835 }
836
837 match orientation {
838 Orientation::Top => {
839 if let Some(first_rendered_element_top) = first_rendered_element_top {
840 assert!(first_rendered_element_top <= scroll_top);
841 }
842 }
843 Orientation::Bottom => {
844 if let Some(last_rendered_element_bottom) = last_rendered_element_bottom {
845 assert!(last_rendered_element_bottom >= scroll_top + height);
846 }
847 }
848 }
849 }
850 }
851
852 struct TestElement {
853 id: usize,
854 size: Vector2F,
855 }
856
857 impl TestElement {
858 fn new(id: usize, height: f32) -> Self {
859 Self {
860 id,
861 size: vec2f(100., height),
862 }
863 }
864 }
865
866 impl Element for TestElement {
867 type LayoutState = ();
868 type PaintState = ();
869
870 fn layout(&mut self, _: SizeConstraint, _: &mut LayoutContext) -> (Vector2F, ()) {
871 (self.size, ())
872 }
873
874 fn paint(&mut self, _: RectF, _: &mut (), _: &mut PaintContext) {
875 todo!()
876 }
877
878 fn dispatch_event(
879 &mut self,
880 _: &Event,
881 _: RectF,
882 _: &mut (),
883 _: &mut (),
884 _: &mut EventContext,
885 ) -> bool {
886 todo!()
887 }
888
889 fn debug(&self, _: RectF, _: &(), _: &(), _: &DebugContext) -> serde_json::Value {
890 self.id.into()
891 }
892
893 fn metadata(&self) -> Option<&dyn std::any::Any> {
894 Some(&self.id)
895 }
896 }
897}