1use crate::{
8 AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, Entity,
9 GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
10 ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled,
11 Window, point, px, size,
12};
13use smallvec::SmallVec;
14use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
15
16use super::ListHorizontalSizingBehavior;
17
18#[track_caller]
22pub fn uniform_list<R>(
23 id: impl Into<ElementId>,
24 item_count: usize,
25 f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
26) -> UniformList
27where
28 R: IntoElement,
29{
30 let id = id.into();
31 let mut base_style = StyleRefinement::default();
32 base_style.overflow.y = Some(Overflow::Scroll);
33
34 let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| {
35 f(range, window, cx)
36 .into_iter()
37 .map(|component| component.into_any_element())
38 .collect()
39 };
40
41 UniformList {
42 item_count,
43 item_to_measure_index: 0,
44 render_items: Box::new(render_range),
45 decorations: Vec::new(),
46 interactivity: Interactivity {
47 element_id: Some(id),
48 base_style: Box::new(base_style),
49 ..Interactivity::new()
50 },
51 scroll_handle: None,
52 sizing_behavior: ListSizingBehavior::default(),
53 horizontal_sizing_behavior: ListHorizontalSizingBehavior::default(),
54 }
55}
56
57pub struct UniformList {
59 item_count: usize,
60 item_to_measure_index: usize,
61 render_items: Box<
62 dyn for<'a> Fn(Range<usize>, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>,
63 >,
64 decorations: Vec<Box<dyn UniformListDecoration>>,
65 interactivity: Interactivity,
66 scroll_handle: Option<UniformListScrollHandle>,
67 sizing_behavior: ListSizingBehavior,
68 horizontal_sizing_behavior: ListHorizontalSizingBehavior,
69}
70
71pub struct UniformListFrameState {
73 items: SmallVec<[AnyElement; 32]>,
74 decorations: SmallVec<[AnyElement; 2]>,
75}
76
77#[derive(Clone, Debug, Default)]
80pub struct UniformListScrollHandle(pub Rc<RefCell<UniformListScrollState>>);
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub enum ScrollStrategy {
85 Top,
87 Center,
91 Bottom,
95 Nearest,
99}
100
101#[derive(Clone, Copy, Debug)]
102#[allow(missing_docs)]
103pub struct DeferredScrollToItem {
104 pub item_index: usize,
106 pub strategy: ScrollStrategy,
108 pub offset: usize,
110 pub scroll_strict: bool,
111}
112
113#[derive(Clone, Debug, Default)]
114#[allow(missing_docs)]
115pub struct UniformListScrollState {
116 pub base_handle: ScrollHandle,
117 pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
118 pub last_item_size: Option<ItemSize>,
120 pub y_flipped: bool,
122}
123
124#[derive(Copy, Clone, Debug, Default)]
125pub struct ItemSize {
127 pub item: Size<Pixels>,
129 pub contents: Size<Pixels>,
132}
133
134impl UniformListScrollHandle {
135 pub fn new() -> Self {
137 Self(Rc::new(RefCell::new(UniformListScrollState {
138 base_handle: ScrollHandle::new(),
139 deferred_scroll_to_item: None,
140 last_item_size: None,
141 y_flipped: false,
142 })))
143 }
144
145 pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
151 self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
152 item_index: ix,
153 strategy,
154 offset: 0,
155 scroll_strict: false,
156 });
157 }
158
159 pub fn scroll_to_item_strict(&self, ix: usize, strategy: ScrollStrategy) {
164 self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
165 item_index: ix,
166 strategy,
167 offset: 0,
168 scroll_strict: true,
169 });
170 }
171
172 pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) {
183 self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
184 item_index: ix,
185 strategy,
186 offset,
187 scroll_strict: false,
188 });
189 }
190
191 pub fn scroll_to_item_strict_with_offset(
202 &self,
203 ix: usize,
204 strategy: ScrollStrategy,
205 offset: usize,
206 ) {
207 self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
208 item_index: ix,
209 strategy,
210 offset,
211 scroll_strict: true,
212 });
213 }
214
215 pub fn y_flipped(&self) -> bool {
217 self.0.borrow().y_flipped
218 }
219
220 #[cfg(any(test, feature = "test-support"))]
222 pub fn logical_scroll_top_index(&self) -> usize {
223 let this = self.0.borrow();
224 this.deferred_scroll_to_item
225 .as_ref()
226 .map(|deferred| deferred.item_index)
227 .unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
228 }
229
230 pub fn is_scrollable(&self) -> bool {
232 if let Some(size) = self.0.borrow().last_item_size {
233 size.contents.height > size.item.height
234 } else {
235 false
236 }
237 }
238
239 pub fn is_scrolled_to_end(&self) -> Option<bool> {
242 let state = self.0.borrow();
243 let max_offset = state.base_handle.max_offset();
244 if max_offset.y <= px(0.) {
245 return None;
246 }
247 let offset = state.base_handle.offset();
248 Some(-offset.y >= max_offset.y)
249 }
250
251 pub fn scroll_to_bottom(&self) {
253 self.scroll_to_item(usize::MAX, ScrollStrategy::Bottom);
254 }
255}
256
257impl Styled for UniformList {
258 fn style(&mut self) -> &mut StyleRefinement {
259 &mut self.interactivity.base_style
260 }
261}
262
263impl Element for UniformList {
264 type RequestLayoutState = UniformListFrameState;
265 type PrepaintState = Option<Hitbox>;
266
267 fn id(&self) -> Option<ElementId> {
268 self.interactivity.element_id.clone()
269 }
270
271 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
272 None
273 }
274
275 fn request_layout(
276 &mut self,
277 global_id: Option<&GlobalElementId>,
278 window: &mut Window,
279 cx: &mut App,
280 ) -> (LayoutId, Self::RequestLayoutState) {
281 let max_items = self.item_count;
282 let item_size = self.measure_item(None, window, cx);
283 let layout_id = self.interactivity.request_layout(
284 global_id,
285 window,
286 cx,
287 |style, window, cx| match self.sizing_behavior {
288 ListSizingBehavior::Infer => {
289 window.with_text_style(style.text_style().cloned(), |window| {
290 window.request_measured_layout(
291 style,
292 move |known_dimensions, available_space, _window, _cx| {
293 let desired_height = item_size.height * max_items;
294 let width = known_dimensions.width.unwrap_or(match available_space
295 .width
296 {
297 AvailableSpace::Definite(x) => x,
298 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
299 item_size.width
300 }
301 });
302 let height = match available_space.height {
303 AvailableSpace::Definite(height) => desired_height.min(height),
304 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
305 desired_height
306 }
307 };
308 size(width, height)
309 },
310 )
311 })
312 }
313 ListSizingBehavior::Auto => window
314 .with_text_style(style.text_style().cloned(), |window| {
315 window.request_layout(style, None, cx)
316 }),
317 },
318 );
319
320 (
321 layout_id,
322 UniformListFrameState {
323 items: SmallVec::new(),
324 decorations: SmallVec::new(),
325 },
326 )
327 }
328
329 fn prepaint(
330 &mut self,
331 global_id: Option<&GlobalElementId>,
332 bounds: Bounds<Pixels>,
333 frame_state: &mut Self::RequestLayoutState,
334 window: &mut Window,
335 cx: &mut App,
336 ) -> Option<Hitbox> {
337 let style = self
338 .interactivity
339 .compute_style(global_id, None, window, cx);
340 let border = style.border_widths.to_pixels(window.rem_size());
341 let padding = style
342 .padding
343 .to_pixels(bounds.size.into(), window.rem_size());
344
345 let padded_bounds = Bounds::from_corners(
346 bounds.origin + point(border.left + padding.left, border.top + padding.top),
347 bounds.bottom_right()
348 - point(border.right + padding.right, border.bottom + padding.bottom),
349 );
350
351 let can_scroll_horizontally = matches!(
352 self.horizontal_sizing_behavior,
353 ListHorizontalSizingBehavior::Unconstrained
354 );
355
356 let longest_item_size = self.measure_item(None, window, cx);
357 let content_width = if can_scroll_horizontally {
358 padded_bounds.size.width.max(longest_item_size.width)
359 } else {
360 padded_bounds.size.width
361 };
362 let content_size = Size {
363 width: content_width,
364 height: longest_item_size.height * self.item_count,
365 };
366
367 let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
368 let item_height = longest_item_size.height;
369 let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
370 let mut handle = handle.0.borrow_mut();
371 handle.last_item_size = Some(ItemSize {
372 item: padded_bounds.size,
373 contents: content_size,
374 });
375 handle.deferred_scroll_to_item.take()
376 });
377
378 self.interactivity.prepaint(
379 global_id,
380 bounds,
381 content_size,
382 window,
383 cx,
384 |_style, mut scroll_offset, hitbox, window, cx| {
385 let y_flipped = if let Some(scroll_handle) = &self.scroll_handle {
386 let scroll_state = scroll_handle.0.borrow();
387 scroll_state.y_flipped
388 } else {
389 false
390 };
391
392 if self.item_count > 0 {
393 let content_height = item_height * self.item_count;
394
395 let is_scrolled_vertically = !scroll_offset.y.is_zero();
396 let max_scroll_offset = padded_bounds.size.height - content_height;
397
398 if is_scrolled_vertically && scroll_offset.y < max_scroll_offset {
399 shared_scroll_offset.borrow_mut().y = max_scroll_offset;
400 scroll_offset.y = max_scroll_offset;
401 }
402
403 let content_width = content_size.width + padding.left + padding.right;
404 let is_scrolled_horizontally =
405 can_scroll_horizontally && !scroll_offset.x.is_zero();
406 if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
407 shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
408 scroll_offset.x = Pixels::ZERO;
409 }
410
411 if let Some(DeferredScrollToItem {
412 mut item_index,
413 mut strategy,
414 offset,
415 scroll_strict,
416 }) = shared_scroll_to_item
417 {
418 if y_flipped {
419 item_index = self.item_count.saturating_sub(item_index + 1);
420 }
421 let list_height = padded_bounds.size.height;
422 let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
423 let item_top = item_height * item_index;
424 let item_bottom = item_top + item_height;
425 let scroll_top = -updated_scroll_offset.y;
426 let offset_pixels = item_height * offset;
427
428 let is_above = item_top < scroll_top + offset_pixels;
430 let is_below = item_bottom > scroll_top + list_height;
431
432 if scroll_strict || is_above || is_below {
433 if strategy == ScrollStrategy::Nearest {
434 if is_above {
435 strategy = ScrollStrategy::Top;
436 } else if is_below {
437 strategy = ScrollStrategy::Bottom;
438 }
439 }
440
441 let max_scroll_offset =
442 (content_height - list_height).max(Pixels::ZERO);
443 match strategy {
444 ScrollStrategy::Top => {
445 updated_scroll_offset.y = -(item_top - offset_pixels)
446 .clamp(Pixels::ZERO, max_scroll_offset);
447 }
448 ScrollStrategy::Center => {
449 let item_center = item_top + item_height / 2.0;
450
451 let viewport_height = list_height - offset_pixels;
452 let viewport_center = offset_pixels + viewport_height / 2.0;
453 let target_scroll_top = item_center - viewport_center;
454 updated_scroll_offset.y =
455 -target_scroll_top.clamp(Pixels::ZERO, max_scroll_offset);
456 }
457 ScrollStrategy::Bottom => {
458 updated_scroll_offset.y = -(item_bottom - list_height)
459 .clamp(Pixels::ZERO, max_scroll_offset);
460 }
461 ScrollStrategy::Nearest => {
462 }
464 }
465 }
466 scroll_offset = *updated_scroll_offset
467 }
468
469 let scroll_top = -scroll_offset.y.to_f64();
470 let item_height_f64 = item_height.to_f64();
471 let first_visible_element_ix =
472 ((scroll_top - padding.top.to_f64()) / item_height_f64).floor() as usize;
473 let last_visible_element_ix = ((scroll_top
474 + padded_bounds.size.height.to_f64())
475 / item_height_f64)
476 .ceil() as usize;
477
478 let visible_range = first_visible_element_ix
479 ..cmp::min(last_visible_element_ix, self.item_count);
480
481 let items = if y_flipped {
482 let flipped_range = self.item_count.saturating_sub(visible_range.end)
483 ..self.item_count.saturating_sub(visible_range.start);
484 let mut items = (self.render_items)(flipped_range, window, cx);
485 items.reverse();
486 items
487 } else {
488 (self.render_items)(visible_range.clone(), window, cx)
489 };
490
491 let content_mask = ContentMask { bounds };
492 window.with_content_mask(Some(content_mask), |window| {
493 for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
494 let item_top = item_height_f64 * ix as f64 - scroll_top;
495 let item_origin = padded_bounds.origin
496 + point(scroll_offset.x, Pixels::from(item_top));
497
498 let available_width = if can_scroll_horizontally {
499 padded_bounds.size.width + scroll_offset.x.abs()
500 } else {
501 padded_bounds.size.width
502 };
503 let available_space = size(
504 AvailableSpace::Definite(available_width),
505 AvailableSpace::Definite(item_height),
506 );
507 item.layout_as_root(available_space, window, cx);
508 item.prepaint_at(item_origin, window, cx);
509 frame_state.items.push(item);
510 }
511
512 let bounds =
513 Bounds::new(padded_bounds.origin + scroll_offset, padded_bounds.size);
514 for decoration in &self.decorations {
515 let mut decoration = decoration.as_ref().compute(
516 visible_range.clone(),
517 bounds,
518 scroll_offset,
519 item_height,
520 self.item_count,
521 window,
522 cx,
523 );
524 let available_space = size(
525 AvailableSpace::Definite(bounds.size.width),
526 AvailableSpace::Definite(bounds.size.height),
527 );
528 decoration.layout_as_root(available_space, window, cx);
529 decoration.prepaint_at(bounds.origin, window, cx);
530 frame_state.decorations.push(decoration);
531 }
532 });
533 }
534
535 hitbox
536 },
537 )
538 }
539
540 fn paint(
541 &mut self,
542 global_id: Option<&GlobalElementId>,
543 bounds: Bounds<crate::Pixels>,
544 request_layout: &mut Self::RequestLayoutState,
545 hitbox: &mut Option<Hitbox>,
546 window: &mut Window,
547 cx: &mut App,
548 ) {
549 self.interactivity.paint(
550 global_id,
551 bounds,
552 hitbox.as_ref(),
553 window,
554 cx,
555 |_, window, cx| {
556 for item in &mut request_layout.items {
557 item.paint(window, cx);
558 }
559 for decoration in &mut request_layout.decorations {
560 decoration.paint(window, cx);
561 }
562 },
563 )
564 }
565}
566
567impl IntoElement for UniformList {
568 type Element = Self;
569
570 fn into_element(self) -> Self::Element {
571 self
572 }
573}
574
575pub trait UniformListDecoration {
578 fn compute(
581 &self,
582 visible_range: Range<usize>,
583 bounds: Bounds<Pixels>,
584 scroll_offset: Point<Pixels>,
585 item_height: Pixels,
586 item_count: usize,
587 window: &mut Window,
588 cx: &mut App,
589 ) -> AnyElement;
590}
591
592impl<T: UniformListDecoration + 'static> UniformListDecoration for Entity<T> {
593 fn compute(
594 &self,
595 visible_range: Range<usize>,
596 bounds: Bounds<Pixels>,
597 scroll_offset: Point<Pixels>,
598 item_height: Pixels,
599 item_count: usize,
600 window: &mut Window,
601 cx: &mut App,
602 ) -> AnyElement {
603 self.update(cx, |inner, cx| {
604 inner.compute(
605 visible_range,
606 bounds,
607 scroll_offset,
608 item_height,
609 item_count,
610 window,
611 cx,
612 )
613 })
614 }
615}
616
617impl UniformList {
618 pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
620 self.item_to_measure_index = item_index.unwrap_or(0);
621 self
622 }
623
624 pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
626 self.sizing_behavior = behavior;
627 self
628 }
629
630 pub fn with_horizontal_sizing_behavior(
634 mut self,
635 behavior: ListHorizontalSizingBehavior,
636 ) -> Self {
637 self.horizontal_sizing_behavior = behavior;
638 match behavior {
639 ListHorizontalSizingBehavior::FitList => {
640 self.interactivity.base_style.overflow.x = None;
641 }
642 ListHorizontalSizingBehavior::Unconstrained => {
643 self.interactivity.base_style.overflow.x = Some(Overflow::Scroll);
644 }
645 }
646 self
647 }
648
649 pub fn with_decoration(mut self, decoration: impl UniformListDecoration + 'static) -> Self {
651 self.decorations.push(Box::new(decoration));
652 self
653 }
654
655 fn measure_item(
656 &self,
657 list_width: Option<Pixels>,
658 window: &mut Window,
659 cx: &mut App,
660 ) -> Size<Pixels> {
661 if self.item_count == 0 {
662 return Size::default();
663 }
664
665 let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
666 let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx);
667 let Some(mut item_to_measure) = items.pop() else {
668 return Size::default();
669 };
670 let available_space = size(
671 list_width.map_or(AvailableSpace::MaxContent, |width| {
672 AvailableSpace::Definite(width)
673 }),
674 AvailableSpace::MinContent,
675 );
676 item_to_measure.layout_as_root(available_space, window, cx)
677 }
678
679 pub fn track_scroll(mut self, handle: &UniformListScrollHandle) -> Self {
681 self.interactivity.tracked_scroll_handle = Some(handle.0.borrow().base_handle.clone());
682 self.scroll_handle = Some(handle.clone());
683 self
684 }
685
686 pub fn y_flipped(mut self, y_flipped: bool) -> Self {
688 if let Some(ref scroll_handle) = self.scroll_handle {
689 let mut scroll_state = scroll_handle.0.borrow_mut();
690 let mut base_handle = &scroll_state.base_handle;
691 let offset = base_handle.offset();
692 match scroll_state.last_item_size {
693 Some(last_size) if scroll_state.y_flipped != y_flipped => {
694 let new_y_offset =
695 -(offset.y + last_size.contents.height - last_size.item.height);
696 base_handle.set_offset(point(offset.x, new_y_offset));
697 scroll_state.y_flipped = y_flipped;
698 }
699 None if y_flipped => {
701 base_handle.set_offset(point(offset.x, Pixels::MIN));
702 scroll_state.y_flipped = y_flipped;
703 }
704 _ => {}
705 }
706 }
707 self
708 }
709}
710
711impl InteractiveElement for UniformList {
712 fn interactivity(&mut self) -> &mut crate::Interactivity {
713 &mut self.interactivity
714 }
715}
716
717#[cfg(test)]
718mod test {
719 use crate::TestAppContext;
720
721 #[gpui::test]
722 fn test_scroll_strategy_nearest(cx: &mut TestAppContext) {
723 use crate::{
724 Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, div, prelude::*,
725 px, uniform_list,
726 };
727 use std::ops::Range;
728
729 actions!(example, [SelectNext, SelectPrev]);
730
731 struct TestView {
732 index: usize,
733 length: usize,
734 scroll_handle: UniformListScrollHandle,
735 focus_handle: FocusHandle,
736 visible_range: Range<usize>,
737 }
738
739 impl TestView {
740 pub fn select_next(
741 &mut self,
742 _: &SelectNext,
743 window: &mut Window,
744 _: &mut Context<Self>,
745 ) {
746 if self.index + 1 == self.length {
747 self.index = 0
748 } else {
749 self.index += 1;
750 }
751 self.scroll_handle
752 .scroll_to_item(self.index, ScrollStrategy::Nearest);
753 window.refresh();
754 }
755
756 pub fn select_previous(
757 &mut self,
758 _: &SelectPrev,
759 window: &mut Window,
760 _: &mut Context<Self>,
761 ) {
762 if self.index == 0 {
763 self.index = self.length - 1
764 } else {
765 self.index -= 1;
766 }
767 self.scroll_handle
768 .scroll_to_item(self.index, ScrollStrategy::Nearest);
769 window.refresh();
770 }
771 }
772
773 impl Render for TestView {
774 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
775 div()
776 .id("list-example")
777 .track_focus(&self.focus_handle)
778 .on_action(cx.listener(Self::select_next))
779 .on_action(cx.listener(Self::select_previous))
780 .size_full()
781 .child(
782 uniform_list(
783 "entries",
784 self.length,
785 cx.processor(|this, range: Range<usize>, _window, _cx| {
786 this.visible_range = range.clone();
787 range
788 .map(|ix| div().id(ix).h(px(20.0)).child(format!("Item {ix}")))
789 .collect()
790 }),
791 )
792 .track_scroll(&self.scroll_handle)
793 .h(px(200.0)),
794 )
795 }
796 }
797
798 let (view, cx) = cx.add_window_view(|window, cx| {
799 let focus_handle = cx.focus_handle();
800 window.focus(&focus_handle, cx);
801 TestView {
802 scroll_handle: UniformListScrollHandle::new(),
803 index: 0,
804 focus_handle,
805 length: 47,
806 visible_range: 0..0,
807 }
808 });
809
810 for ix in 1..10 {
814 cx.dispatch_action(SelectNext);
815 view.read_with(cx, |view, _| {
816 assert_eq!(view.index, ix);
817 assert_eq!(view.visible_range, 0..10);
818 })
819 }
820
821 for ix in 10..47 {
823 cx.dispatch_action(SelectNext);
824 view.read_with(cx, |view, _| {
825 assert_eq!(view.index, ix);
826 assert_eq!(view.visible_range, ix - 9..ix + 1);
827 })
828 }
829
830 cx.dispatch_action(SelectNext);
832 view.read_with(cx, |view, _| {
833 assert_eq!(view.index, 0);
834 assert_eq!(view.visible_range, 0..10);
835 });
836
837 cx.dispatch_action(SelectPrev);
839 view.read_with(cx, |view, _| {
840 assert_eq!(view.index, 46);
841 assert_eq!(view.visible_range, 37..47);
842 });
843
844 for ix in (37..46).rev() {
846 cx.dispatch_action(SelectPrev);
847 view.read_with(cx, |view, _| {
848 assert_eq!(view.index, ix);
849 assert_eq!(view.visible_range, 37..47);
850 })
851 }
852
853 for ix in (0..37).rev() {
855 cx.dispatch_action(SelectPrev);
856 view.read_with(cx, |view, _| {
857 assert_eq!(view.index, ix);
858 assert_eq!(view.visible_range, ix..ix + 10);
859 })
860 }
861 }
862
863 #[gpui::test]
864 fn test_item_positions_far_down_long_list(cx: &mut TestAppContext) {
865 use crate::{
866 Bounds, Context, Pixels, UniformListScrollHandle, Window, canvas, point, prelude::*,
867 px, uniform_list,
868 };
869 use std::{cell::RefCell, ops::Range, rc::Rc};
870
871 struct TestView {
872 scroll_handle: UniformListScrollHandle,
873 item_bounds: Rc<RefCell<Vec<(usize, Bounds<Pixels>)>>>,
874 }
875
876 impl Render for TestView {
877 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
878 let item_bounds = self.item_bounds.clone();
879 uniform_list(
880 "entries",
881 10_000_000,
882 cx.processor(move |_, range: Range<usize>, _window, _cx| {
883 range
884 .map(|ix| {
885 let item_bounds = item_bounds.clone();
886 canvas(
887 move |bounds, _, _| item_bounds.borrow_mut().push((ix, bounds)),
888 |_, _, _, _| {},
889 )
890 .w_full()
891 .h(px(24.0))
892 })
893 .collect()
894 }),
895 )
896 .track_scroll(&self.scroll_handle)
897 .h(px(200.0))
898 }
899 }
900
901 let item_bounds = Rc::new(RefCell::new(Vec::new()));
902 let (view, cx) = cx.add_window_view(|_, _| TestView {
903 scroll_handle: UniformListScrollHandle::new(),
904 item_bounds: item_bounds.clone(),
905 });
906
907 item_bounds.borrow_mut().clear();
910 view.update(cx, |view, cx| {
911 view.scroll_handle
912 .0
913 .borrow()
914 .base_handle
915 .set_offset(point(px(0.0), px(-216_000_064.0)));
916 cx.notify();
917 });
918 cx.run_until_parked();
919
920 let item_tops = item_bounds
921 .borrow()
922 .iter()
923 .map(|(ix, bounds)| (*ix, bounds.origin.y))
924 .collect::<Vec<_>>();
925 let expected_tops = (9_000_002..9_000_011)
926 .map(|ix| (ix, px(-16.0 + 24.0 * (ix - 9_000_002) as f32)))
927 .collect::<Vec<_>>();
928 assert_eq!(item_tops, expected_tops);
929 }
930}