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