1use crate::{
8 AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, Entity,
9 GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,
10 IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size,
11 StyleRefinement, Styled, 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 inspector_id: Option<&InspectorElementId>,
279 window: &mut Window,
280 cx: &mut App,
281 ) -> (LayoutId, Self::RequestLayoutState) {
282 let max_items = self.item_count;
283 let item_size = self.measure_item(None, window, cx);
284 let layout_id = self.interactivity.request_layout(
285 global_id,
286 inspector_id,
287 window,
288 cx,
289 |style, window, cx| match self.sizing_behavior {
290 ListSizingBehavior::Infer => {
291 window.with_text_style(style.text_style().cloned(), |window| {
292 window.request_measured_layout(
293 style,
294 move |known_dimensions, available_space, _window, _cx| {
295 let desired_height = item_size.height * max_items;
296 let width = known_dimensions.width.unwrap_or(match available_space
297 .width
298 {
299 AvailableSpace::Definite(x) => x,
300 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
301 item_size.width
302 }
303 });
304 let height = match available_space.height {
305 AvailableSpace::Definite(height) => desired_height.min(height),
306 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
307 desired_height
308 }
309 };
310 size(width, height)
311 },
312 )
313 })
314 }
315 ListSizingBehavior::Auto => window
316 .with_text_style(style.text_style().cloned(), |window| {
317 window.request_layout(style, None, cx)
318 }),
319 },
320 );
321
322 (
323 layout_id,
324 UniformListFrameState {
325 items: SmallVec::new(),
326 decorations: SmallVec::new(),
327 },
328 )
329 }
330
331 fn prepaint(
332 &mut self,
333 global_id: Option<&GlobalElementId>,
334 inspector_id: Option<&InspectorElementId>,
335 bounds: Bounds<Pixels>,
336 frame_state: &mut Self::RequestLayoutState,
337 window: &mut Window,
338 cx: &mut App,
339 ) -> Option<Hitbox> {
340 let style = self
341 .interactivity
342 .compute_style(global_id, None, window, cx);
343 let border = style.border_widths.to_pixels(window.rem_size());
344 let padding = style
345 .padding
346 .to_pixels(bounds.size.into(), window.rem_size());
347
348 let padded_bounds = Bounds::from_corners(
349 bounds.origin + point(border.left + padding.left, border.top + padding.top),
350 bounds.bottom_right()
351 - point(border.right + padding.right, border.bottom + padding.bottom),
352 );
353
354 let can_scroll_horizontally = matches!(
355 self.horizontal_sizing_behavior,
356 ListHorizontalSizingBehavior::Unconstrained
357 );
358
359 let longest_item_size = self.measure_item(None, window, cx);
360 let content_width = if can_scroll_horizontally {
361 padded_bounds.size.width.max(longest_item_size.width)
362 } else {
363 padded_bounds.size.width
364 };
365 let content_size = Size {
366 width: content_width,
367 height: longest_item_size.height * self.item_count,
368 };
369
370 let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
371 let item_height = longest_item_size.height;
372 let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
373 let mut handle = handle.0.borrow_mut();
374 handle.last_item_size = Some(ItemSize {
375 item: padded_bounds.size,
376 contents: content_size,
377 });
378 handle.deferred_scroll_to_item.take()
379 });
380
381 self.interactivity.prepaint(
382 global_id,
383 inspector_id,
384 bounds,
385 content_size,
386 window,
387 cx,
388 |_style, mut scroll_offset, hitbox, window, cx| {
389 let y_flipped = if let Some(scroll_handle) = &self.scroll_handle {
390 let scroll_state = scroll_handle.0.borrow();
391 scroll_state.y_flipped
392 } else {
393 false
394 };
395
396 if self.item_count > 0 {
397 let content_height = item_height * self.item_count;
398
399 let is_scrolled_vertically = !scroll_offset.y.is_zero();
400 let max_scroll_offset = padded_bounds.size.height - content_height;
401
402 if is_scrolled_vertically && scroll_offset.y < max_scroll_offset {
403 shared_scroll_offset.borrow_mut().y = max_scroll_offset;
404 scroll_offset.y = max_scroll_offset;
405 }
406
407 let content_width = content_size.width + padding.left + padding.right;
408 let is_scrolled_horizontally =
409 can_scroll_horizontally && !scroll_offset.x.is_zero();
410 if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
411 shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
412 scroll_offset.x = Pixels::ZERO;
413 }
414
415 if let Some(DeferredScrollToItem {
416 mut item_index,
417 mut strategy,
418 offset,
419 scroll_strict,
420 }) = shared_scroll_to_item
421 {
422 if y_flipped {
423 item_index = self.item_count.saturating_sub(item_index + 1);
424 }
425 let list_height = padded_bounds.size.height;
426 let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
427 let item_top = item_height * item_index;
428 let item_bottom = item_top + item_height;
429 let scroll_top = -updated_scroll_offset.y;
430 let offset_pixels = item_height * offset;
431
432 let is_above = item_top < scroll_top + offset_pixels;
434 let is_below = item_bottom > scroll_top + list_height;
435
436 if scroll_strict || is_above || is_below {
437 if strategy == ScrollStrategy::Nearest {
438 if is_above {
439 strategy = ScrollStrategy::Top;
440 } else if is_below {
441 strategy = ScrollStrategy::Bottom;
442 }
443 }
444
445 let max_scroll_offset =
446 (content_height - list_height).max(Pixels::ZERO);
447 match strategy {
448 ScrollStrategy::Top => {
449 updated_scroll_offset.y = -(item_top - offset_pixels)
450 .clamp(Pixels::ZERO, max_scroll_offset);
451 }
452 ScrollStrategy::Center => {
453 let item_center = item_top + item_height / 2.0;
454
455 let viewport_height = list_height - offset_pixels;
456 let viewport_center = offset_pixels + viewport_height / 2.0;
457 let target_scroll_top = item_center - viewport_center;
458 updated_scroll_offset.y =
459 -target_scroll_top.clamp(Pixels::ZERO, max_scroll_offset);
460 }
461 ScrollStrategy::Bottom => {
462 updated_scroll_offset.y = -(item_bottom - list_height)
463 .clamp(Pixels::ZERO, max_scroll_offset);
464 }
465 ScrollStrategy::Nearest => {
466 }
468 }
469 }
470 scroll_offset = *updated_scroll_offset
471 }
472
473 let scroll_top = -scroll_offset.y.to_f64();
474 let item_height_f64 = item_height.to_f64();
475 let first_visible_element_ix =
476 ((scroll_top - padding.top.to_f64()) / item_height_f64).floor() as usize;
477 let last_visible_element_ix = ((scroll_top
478 + padded_bounds.size.height.to_f64())
479 / item_height_f64)
480 .ceil() as usize;
481
482 let visible_range = first_visible_element_ix
483 ..cmp::min(last_visible_element_ix, self.item_count);
484
485 let items = if y_flipped {
486 let flipped_range = self.item_count.saturating_sub(visible_range.end)
487 ..self.item_count.saturating_sub(visible_range.start);
488 let mut items = (self.render_items)(flipped_range, window, cx);
489 items.reverse();
490 items
491 } else {
492 (self.render_items)(visible_range.clone(), window, cx)
493 };
494
495 let content_mask = ContentMask { bounds };
496 window.with_content_mask(Some(content_mask), |window| {
497 for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
498 let item_top = item_height_f64 * ix as f64 - scroll_top;
499 let item_origin = padded_bounds.origin
500 + point(scroll_offset.x, Pixels::from(item_top));
501
502 let available_width = if can_scroll_horizontally {
503 padded_bounds.size.width + scroll_offset.x.abs()
504 } else {
505 padded_bounds.size.width
506 };
507 let available_space = size(
508 AvailableSpace::Definite(available_width),
509 AvailableSpace::Definite(item_height),
510 );
511 item.layout_as_root(available_space, window, cx);
512 item.prepaint_at(item_origin, window, cx);
513 frame_state.items.push(item);
514 }
515
516 let bounds =
517 Bounds::new(padded_bounds.origin + scroll_offset, padded_bounds.size);
518 for decoration in &self.decorations {
519 let mut decoration = decoration.as_ref().compute(
520 visible_range.clone(),
521 bounds,
522 scroll_offset,
523 item_height,
524 self.item_count,
525 window,
526 cx,
527 );
528 let available_space = size(
529 AvailableSpace::Definite(bounds.size.width),
530 AvailableSpace::Definite(bounds.size.height),
531 );
532 decoration.layout_as_root(available_space, window, cx);
533 decoration.prepaint_at(bounds.origin, window, cx);
534 frame_state.decorations.push(decoration);
535 }
536 });
537 }
538
539 hitbox
540 },
541 )
542 }
543
544 fn paint(
545 &mut self,
546 global_id: Option<&GlobalElementId>,
547 inspector_id: Option<&InspectorElementId>,
548 bounds: Bounds<crate::Pixels>,
549 request_layout: &mut Self::RequestLayoutState,
550 hitbox: &mut Option<Hitbox>,
551 window: &mut Window,
552 cx: &mut App,
553 ) {
554 self.interactivity.paint(
555 global_id,
556 inspector_id,
557 bounds,
558 hitbox.as_ref(),
559 window,
560 cx,
561 |_, window, cx| {
562 for item in &mut request_layout.items {
563 item.paint(window, cx);
564 }
565 for decoration in &mut request_layout.decorations {
566 decoration.paint(window, cx);
567 }
568 },
569 )
570 }
571}
572
573impl IntoElement for UniformList {
574 type Element = Self;
575
576 fn into_element(self) -> Self::Element {
577 self
578 }
579}
580
581pub trait UniformListDecoration {
584 fn compute(
587 &self,
588 visible_range: Range<usize>,
589 bounds: Bounds<Pixels>,
590 scroll_offset: Point<Pixels>,
591 item_height: Pixels,
592 item_count: usize,
593 window: &mut Window,
594 cx: &mut App,
595 ) -> AnyElement;
596}
597
598impl<T: UniformListDecoration + 'static> UniformListDecoration for Entity<T> {
599 fn compute(
600 &self,
601 visible_range: Range<usize>,
602 bounds: Bounds<Pixels>,
603 scroll_offset: Point<Pixels>,
604 item_height: Pixels,
605 item_count: usize,
606 window: &mut Window,
607 cx: &mut App,
608 ) -> AnyElement {
609 self.update(cx, |inner, cx| {
610 inner.compute(
611 visible_range,
612 bounds,
613 scroll_offset,
614 item_height,
615 item_count,
616 window,
617 cx,
618 )
619 })
620 }
621}
622
623impl UniformList {
624 pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
626 self.item_to_measure_index = item_index.unwrap_or(0);
627 self
628 }
629
630 pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
632 self.sizing_behavior = behavior;
633 self
634 }
635
636 pub fn with_horizontal_sizing_behavior(
640 mut self,
641 behavior: ListHorizontalSizingBehavior,
642 ) -> Self {
643 self.horizontal_sizing_behavior = behavior;
644 match behavior {
645 ListHorizontalSizingBehavior::FitList => {
646 self.interactivity.base_style.overflow.x = None;
647 }
648 ListHorizontalSizingBehavior::Unconstrained => {
649 self.interactivity.base_style.overflow.x = Some(Overflow::Scroll);
650 }
651 }
652 self
653 }
654
655 pub fn with_decoration(mut self, decoration: impl UniformListDecoration + 'static) -> Self {
657 self.decorations.push(Box::new(decoration));
658 self
659 }
660
661 fn measure_item(
662 &self,
663 list_width: Option<Pixels>,
664 window: &mut Window,
665 cx: &mut App,
666 ) -> Size<Pixels> {
667 if self.item_count == 0 {
668 return Size::default();
669 }
670
671 let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
672 let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx);
673 let Some(mut item_to_measure) = items.pop() else {
674 return Size::default();
675 };
676 let available_space = size(
677 list_width.map_or(AvailableSpace::MaxContent, |width| {
678 AvailableSpace::Definite(width)
679 }),
680 AvailableSpace::MinContent,
681 );
682 item_to_measure.layout_as_root(available_space, window, cx)
683 }
684
685 pub fn track_scroll(mut self, handle: &UniformListScrollHandle) -> Self {
687 self.interactivity.tracked_scroll_handle = Some(handle.0.borrow().base_handle.clone());
688 self.scroll_handle = Some(handle.clone());
689 self
690 }
691
692 pub fn y_flipped(mut self, y_flipped: bool) -> Self {
694 if let Some(ref scroll_handle) = self.scroll_handle {
695 let mut scroll_state = scroll_handle.0.borrow_mut();
696 let mut base_handle = &scroll_state.base_handle;
697 let offset = base_handle.offset();
698 match scroll_state.last_item_size {
699 Some(last_size) if scroll_state.y_flipped != y_flipped => {
700 let new_y_offset =
701 -(offset.y + last_size.contents.height - last_size.item.height);
702 base_handle.set_offset(point(offset.x, new_y_offset));
703 scroll_state.y_flipped = y_flipped;
704 }
705 None if y_flipped => {
707 base_handle.set_offset(point(offset.x, Pixels::MIN));
708 scroll_state.y_flipped = y_flipped;
709 }
710 _ => {}
711 }
712 }
713 self
714 }
715}
716
717impl InteractiveElement for UniformList {
718 fn interactivity(&mut self) -> &mut crate::Interactivity {
719 &mut self.interactivity
720 }
721}
722
723#[cfg(test)]
724mod test {
725 use crate::TestAppContext;
726
727 #[gpui::test]
728 fn test_scroll_strategy_nearest(cx: &mut TestAppContext) {
729 use crate::{
730 Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, div, prelude::*,
731 px, uniform_list,
732 };
733 use std::ops::Range;
734
735 actions!(example, [SelectNext, SelectPrev]);
736
737 struct TestView {
738 index: usize,
739 length: usize,
740 scroll_handle: UniformListScrollHandle,
741 focus_handle: FocusHandle,
742 visible_range: Range<usize>,
743 }
744
745 impl TestView {
746 pub fn select_next(
747 &mut self,
748 _: &SelectNext,
749 window: &mut Window,
750 _: &mut Context<Self>,
751 ) {
752 if self.index + 1 == self.length {
753 self.index = 0
754 } else {
755 self.index += 1;
756 }
757 self.scroll_handle
758 .scroll_to_item(self.index, ScrollStrategy::Nearest);
759 window.refresh();
760 }
761
762 pub fn select_previous(
763 &mut self,
764 _: &SelectPrev,
765 window: &mut Window,
766 _: &mut Context<Self>,
767 ) {
768 if self.index == 0 {
769 self.index = self.length - 1
770 } else {
771 self.index -= 1;
772 }
773 self.scroll_handle
774 .scroll_to_item(self.index, ScrollStrategy::Nearest);
775 window.refresh();
776 }
777 }
778
779 impl Render for TestView {
780 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
781 div()
782 .id("list-example")
783 .track_focus(&self.focus_handle)
784 .on_action(cx.listener(Self::select_next))
785 .on_action(cx.listener(Self::select_previous))
786 .size_full()
787 .child(
788 uniform_list(
789 "entries",
790 self.length,
791 cx.processor(|this, range: Range<usize>, _window, _cx| {
792 this.visible_range = range.clone();
793 range
794 .map(|ix| div().id(ix).h(px(20.0)).child(format!("Item {ix}")))
795 .collect()
796 }),
797 )
798 .track_scroll(&self.scroll_handle)
799 .h(px(200.0)),
800 )
801 }
802 }
803
804 let (view, cx) = cx.add_window_view(|window, cx| {
805 let focus_handle = cx.focus_handle();
806 window.focus(&focus_handle, cx);
807 TestView {
808 scroll_handle: UniformListScrollHandle::new(),
809 index: 0,
810 focus_handle,
811 length: 47,
812 visible_range: 0..0,
813 }
814 });
815
816 for ix in 1..10 {
820 cx.dispatch_action(SelectNext);
821 view.read_with(cx, |view, _| {
822 assert_eq!(view.index, ix);
823 assert_eq!(view.visible_range, 0..10);
824 })
825 }
826
827 for ix in 10..47 {
829 cx.dispatch_action(SelectNext);
830 view.read_with(cx, |view, _| {
831 assert_eq!(view.index, ix);
832 assert_eq!(view.visible_range, ix - 9..ix + 1);
833 })
834 }
835
836 cx.dispatch_action(SelectNext);
838 view.read_with(cx, |view, _| {
839 assert_eq!(view.index, 0);
840 assert_eq!(view.visible_range, 0..10);
841 });
842
843 cx.dispatch_action(SelectPrev);
845 view.read_with(cx, |view, _| {
846 assert_eq!(view.index, 46);
847 assert_eq!(view.visible_range, 37..47);
848 });
849
850 for ix in (37..46).rev() {
852 cx.dispatch_action(SelectPrev);
853 view.read_with(cx, |view, _| {
854 assert_eq!(view.index, ix);
855 assert_eq!(view.visible_range, 37..47);
856 })
857 }
858
859 for ix in (0..37).rev() {
861 cx.dispatch_action(SelectPrev);
862 view.read_with(cx, |view, _| {
863 assert_eq!(view.index, ix);
864 assert_eq!(view.visible_range, ix..ix + 10);
865 })
866 }
867 }
868
869 #[gpui::test]
870 fn test_item_positions_far_down_long_list(cx: &mut TestAppContext) {
871 use crate::{
872 Bounds, Context, Pixels, UniformListScrollHandle, Window, canvas, point, prelude::*,
873 px, uniform_list,
874 };
875 use std::{cell::RefCell, ops::Range, rc::Rc};
876
877 struct TestView {
878 scroll_handle: UniformListScrollHandle,
879 item_bounds: Rc<RefCell<Vec<(usize, Bounds<Pixels>)>>>,
880 }
881
882 impl Render for TestView {
883 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
884 let item_bounds = self.item_bounds.clone();
885 uniform_list(
886 "entries",
887 10_000_000,
888 cx.processor(move |_, range: Range<usize>, _window, _cx| {
889 range
890 .map(|ix| {
891 let item_bounds = item_bounds.clone();
892 canvas(
893 move |bounds, _, _| item_bounds.borrow_mut().push((ix, bounds)),
894 |_, _, _, _| {},
895 )
896 .w_full()
897 .h(px(24.0))
898 })
899 .collect()
900 }),
901 )
902 .track_scroll(&self.scroll_handle)
903 .h(px(200.0))
904 }
905 }
906
907 let item_bounds = Rc::new(RefCell::new(Vec::new()));
908 let (view, cx) = cx.add_window_view(|_, _| TestView {
909 scroll_handle: UniformListScrollHandle::new(),
910 item_bounds: item_bounds.clone(),
911 });
912
913 item_bounds.borrow_mut().clear();
916 view.update(cx, |view, cx| {
917 view.scroll_handle
918 .0
919 .borrow()
920 .base_handle
921 .set_offset(point(px(0.0), px(-216_000_064.0)));
922 cx.notify();
923 });
924 cx.run_until_parked();
925
926 let item_tops = item_bounds
927 .borrow()
928 .iter()
929 .map(|(ix, bounds)| (*ix, bounds.origin.y))
930 .collect::<Vec<_>>();
931 let expected_tops = (9_000_002..9_000_011)
932 .map(|ix| (ix, px(-16.0 + 24.0 * (ix - 9_000_002) as f32)))
933 .collect::<Vec<_>>();
934 assert_eq!(item_tops, expected_tops);
935 }
936}