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