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