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