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