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