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