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