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