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