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