1//! A scrollable list of elements with uniform height, optimized for large lists.
2//! Rather than use the full taffy layout system, uniform_list simply measures
3//! the first element and then lays out all remaining elements in a line based on that
4//! measurement. This is much faster than the full layout system, but only works for
5//! elements with uniform height.
6
7use 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, size,
12};
13use smallvec::SmallVec;
14use std::{cell::RefCell, cmp, ops::Range, rc::Rc, usize};
15
16use super::ListHorizontalSizingBehavior;
17
18/// uniform_list provides lazy rendering for a set of items that are of uniform height.
19/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
20/// uniform_list will only render the visible subset of items.
21#[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
57/// A list element for efficiently laying out and displaying a list of uniform-height elements.
58pub 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
71/// Frame state used by the [UniformList].
72pub struct UniformListFrameState {
73 items: SmallVec<[AnyElement; 32]>,
74 decorations: SmallVec<[AnyElement; 2]>,
75}
76
77/// A handle for controlling the scroll position of a uniform list.
78/// This should be stored in your view and passed to the uniform_list on each frame.
79#[derive(Clone, Debug, Default)]
80pub struct UniformListScrollHandle(pub Rc<RefCell<UniformListScrollState>>);
81
82/// Where to place the element scrolled to.
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub enum ScrollStrategy {
85 /// Place the element at the top of the list's viewport.
86 Top,
87 /// Attempt to place the element in the middle of the list's viewport.
88 /// May not be possible if there's not enough list items above the item scrolled to:
89 /// in this case, the element will be placed at the closest possible position.
90 Center,
91 /// Attempt to place the element at the bottom of the list's viewport.
92 /// May not be possible if there's not enough list items above the item scrolled to:
93 /// in this case, the element will be placed at the closest possible position.
94 Bottom,
95 /// If the element is not visible attempt to place it at:
96 /// - The top of the list's viewport if the target element is above currently visible elements.
97 /// - The bottom of the list's viewport if the target element is above currently visible elements.
98 Nearest,
99}
100
101#[derive(Clone, Copy, Debug)]
102#[allow(missing_docs)]
103pub struct DeferredScrollToItem {
104 /// The item index to scroll to
105 pub item_index: usize,
106 /// The scroll strategy to use
107 pub strategy: ScrollStrategy,
108 /// The offset in number of items
109 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 /// Size of the item, captured during last layout.
119 pub last_item_size: Option<ItemSize>,
120 /// Whether the list was vertically flipped during last layout.
121 pub y_flipped: bool,
122}
123
124#[derive(Copy, Clone, Debug, Default)]
125/// The size of the item and its contents.
126pub struct ItemSize {
127 /// The size of the item.
128 pub item: Size<Pixels>,
129 /// The size of the item's contents, which may be larger than the item itself,
130 /// if the item was bounded by a parent element.
131 pub contents: Size<Pixels>,
132}
133
134impl UniformListScrollHandle {
135 /// Create a new scroll handle to bind to a uniform list.
136 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 /// Scroll the list so that the given item index is visible.
146 ///
147 /// This uses non-strict scrolling: if the item is already fully visible, no scrolling occurs.
148 /// If the item is out of view, it scrolls the minimum amount to bring it into view according
149 /// to the strategy.
150 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 /// Scroll the list so that the given item index is at scroll strategy position.
160 ///
161 /// This uses strict scrolling: the item will always be scrolled to match the strategy position,
162 /// even if it's already visible. Use this when you need precise positioning.
163 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 /// Scroll the list to the given item index with an offset in number of items.
173 ///
174 /// This uses non-strict scrolling: if the item is already visible within the offset region,
175 /// no scrolling occurs.
176 ///
177 /// The offset parameter shrinks the effective viewport by the specified number of items
178 /// from the corresponding edge, then applies the scroll strategy within that reduced viewport:
179 /// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top
180 /// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport
181 /// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom
182 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 /// Scroll the list so that the given item index is at the exact scroll strategy position with an offset.
192 ///
193 /// This uses strict scrolling: the item will always be scrolled to match the strategy position,
194 /// even if it's already visible.
195 ///
196 /// The offset parameter shrinks the effective viewport by the specified number of items
197 /// from the corresponding edge, then applies the scroll strategy within that reduced viewport:
198 /// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top
199 /// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport
200 /// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom
201 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 /// Check if the list is flipped vertically.
216 pub fn y_flipped(&self) -> bool {
217 self.0.borrow().y_flipped
218 }
219
220 /// Get the index of the topmost visible child.
221 #[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 /// Checks if the list can be scrolled vertically.
231 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 /// Scroll to the bottom of the list.
240 pub fn scroll_to_bottom(&self) {
241 self.scroll_to_item(usize::MAX, ScrollStrategy::Bottom);
242 }
243}
244
245impl Styled for UniformList {
246 fn style(&mut self) -> &mut StyleRefinement {
247 &mut self.interactivity.base_style
248 }
249}
250
251impl Element for UniformList {
252 type RequestLayoutState = UniformListFrameState;
253 type PrepaintState = Option<Hitbox>;
254
255 fn id(&self) -> Option<ElementId> {
256 self.interactivity.element_id.clone()
257 }
258
259 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
260 None
261 }
262
263 fn request_layout(
264 &mut self,
265 global_id: Option<&GlobalElementId>,
266 inspector_id: Option<&InspectorElementId>,
267 window: &mut Window,
268 cx: &mut App,
269 ) -> (LayoutId, Self::RequestLayoutState) {
270 let max_items = self.item_count;
271 let item_size = self.measure_item(None, window, cx);
272 let layout_id = self.interactivity.request_layout(
273 global_id,
274 inspector_id,
275 window,
276 cx,
277 |style, window, cx| match self.sizing_behavior {
278 ListSizingBehavior::Infer => {
279 window.with_text_style(style.text_style().cloned(), |window| {
280 window.request_measured_layout(
281 style,
282 move |known_dimensions, available_space, _window, _cx| {
283 let desired_height = item_size.height * max_items;
284 let width = known_dimensions.width.unwrap_or(match available_space
285 .width
286 {
287 AvailableSpace::Definite(x) => x,
288 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
289 item_size.width
290 }
291 });
292 let height = match available_space.height {
293 AvailableSpace::Definite(height) => desired_height.min(height),
294 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
295 desired_height
296 }
297 };
298 size(width, height)
299 },
300 )
301 })
302 }
303 ListSizingBehavior::Auto => window
304 .with_text_style(style.text_style().cloned(), |window| {
305 window.request_layout(style, None, cx)
306 }),
307 },
308 );
309
310 (
311 layout_id,
312 UniformListFrameState {
313 items: SmallVec::new(),
314 decorations: SmallVec::new(),
315 },
316 )
317 }
318
319 fn prepaint(
320 &mut self,
321 global_id: Option<&GlobalElementId>,
322 inspector_id: Option<&InspectorElementId>,
323 bounds: Bounds<Pixels>,
324 frame_state: &mut Self::RequestLayoutState,
325 window: &mut Window,
326 cx: &mut App,
327 ) -> Option<Hitbox> {
328 let style = self
329 .interactivity
330 .compute_style(global_id, None, window, cx);
331 let border = style.border_widths.to_pixels(window.rem_size());
332 let padding = style
333 .padding
334 .to_pixels(bounds.size.into(), window.rem_size());
335
336 let padded_bounds = Bounds::from_corners(
337 bounds.origin + point(border.left + padding.left, border.top + padding.top),
338 bounds.bottom_right()
339 - point(border.right + padding.right, border.bottom + padding.bottom),
340 );
341
342 let can_scroll_horizontally = matches!(
343 self.horizontal_sizing_behavior,
344 ListHorizontalSizingBehavior::Unconstrained
345 );
346
347 let longest_item_size = self.measure_item(None, window, cx);
348 let content_width = if can_scroll_horizontally {
349 padded_bounds.size.width.max(longest_item_size.width)
350 } else {
351 padded_bounds.size.width
352 };
353 let content_size = Size {
354 width: content_width,
355 height: longest_item_size.height * self.item_count,
356 };
357
358 let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
359 let item_height = longest_item_size.height;
360 let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
361 let mut handle = handle.0.borrow_mut();
362 handle.last_item_size = Some(ItemSize {
363 item: padded_bounds.size,
364 contents: content_size,
365 });
366 handle.deferred_scroll_to_item.take()
367 });
368
369 self.interactivity.prepaint(
370 global_id,
371 inspector_id,
372 bounds,
373 content_size,
374 window,
375 cx,
376 |_style, mut scroll_offset, hitbox, window, cx| {
377 let y_flipped = if let Some(scroll_handle) = &self.scroll_handle {
378 let scroll_state = scroll_handle.0.borrow();
379 scroll_state.y_flipped
380 } else {
381 false
382 };
383
384 if self.item_count > 0 {
385 let content_height = item_height * self.item_count;
386
387 let is_scrolled_vertically = !scroll_offset.y.is_zero();
388 let max_scroll_offset = padded_bounds.size.height - content_height;
389
390 if is_scrolled_vertically && scroll_offset.y < max_scroll_offset {
391 shared_scroll_offset.borrow_mut().y = max_scroll_offset;
392 scroll_offset.y = max_scroll_offset;
393 }
394
395 let content_width = content_size.width + padding.left + padding.right;
396 let is_scrolled_horizontally =
397 can_scroll_horizontally && !scroll_offset.x.is_zero();
398 if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
399 shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
400 scroll_offset.x = Pixels::ZERO;
401 }
402
403 if let Some(DeferredScrollToItem {
404 mut item_index,
405 mut strategy,
406 offset,
407 scroll_strict,
408 }) = shared_scroll_to_item
409 {
410 if y_flipped {
411 item_index = self.item_count.saturating_sub(item_index + 1);
412 }
413 let list_height = padded_bounds.size.height;
414 let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
415 let item_top = item_height * item_index;
416 let item_bottom = item_top + item_height;
417 let scroll_top = -updated_scroll_offset.y;
418 let offset_pixels = item_height * offset;
419
420 // is the selected item above/below currently visible items
421 let is_above = item_top < scroll_top + offset_pixels;
422 let is_below = item_bottom > scroll_top + list_height;
423
424 if scroll_strict || is_above || is_below {
425 if strategy == ScrollStrategy::Nearest {
426 if is_above {
427 strategy = ScrollStrategy::Top;
428 } else if is_below {
429 strategy = ScrollStrategy::Bottom;
430 }
431 }
432
433 let max_scroll_offset =
434 (content_height - list_height).max(Pixels::ZERO);
435 match strategy {
436 ScrollStrategy::Top => {
437 updated_scroll_offset.y = -(item_top - offset_pixels)
438 .clamp(Pixels::ZERO, max_scroll_offset);
439 }
440 ScrollStrategy::Center => {
441 let item_center = item_top + item_height / 2.0;
442
443 let viewport_height = list_height - offset_pixels;
444 let viewport_center = offset_pixels + viewport_height / 2.0;
445 let target_scroll_top = item_center - viewport_center;
446 updated_scroll_offset.y =
447 -target_scroll_top.clamp(Pixels::ZERO, max_scroll_offset);
448 }
449 ScrollStrategy::Bottom => {
450 updated_scroll_offset.y = -(item_bottom - list_height)
451 .clamp(Pixels::ZERO, max_scroll_offset);
452 }
453 ScrollStrategy::Nearest => {
454 // Nearest, but the item is visible -> no scroll is required
455 }
456 }
457 }
458 scroll_offset = *updated_scroll_offset
459 }
460
461 let first_visible_element_ix =
462 (-(scroll_offset.y + padding.top) / item_height).floor() as usize;
463 let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
464 / item_height)
465 .ceil() as usize;
466
467 let visible_range = first_visible_element_ix
468 ..cmp::min(last_visible_element_ix, self.item_count);
469
470 let items = if y_flipped {
471 let flipped_range = self.item_count.saturating_sub(visible_range.end)
472 ..self.item_count.saturating_sub(visible_range.start);
473 let mut items = (self.render_items)(flipped_range, window, cx);
474 items.reverse();
475 items
476 } else {
477 (self.render_items)(visible_range.clone(), window, cx)
478 };
479
480 let content_mask = ContentMask { bounds };
481 window.with_content_mask(Some(content_mask), |window| {
482 for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
483 let item_origin = padded_bounds.origin
484 + scroll_offset
485 + point(Pixels::ZERO, item_height * ix);
486
487 let available_width = if can_scroll_horizontally {
488 padded_bounds.size.width + scroll_offset.x.abs()
489 } else {
490 padded_bounds.size.width
491 };
492 let available_space = size(
493 AvailableSpace::Definite(available_width),
494 AvailableSpace::Definite(item_height),
495 );
496 item.layout_as_root(available_space, window, cx);
497 item.prepaint_at(item_origin, window, cx);
498 frame_state.items.push(item);
499 }
500
501 let bounds =
502 Bounds::new(padded_bounds.origin + scroll_offset, padded_bounds.size);
503 for decoration in &self.decorations {
504 let mut decoration = decoration.as_ref().compute(
505 visible_range.clone(),
506 bounds,
507 scroll_offset,
508 item_height,
509 self.item_count,
510 window,
511 cx,
512 );
513 let available_space = size(
514 AvailableSpace::Definite(bounds.size.width),
515 AvailableSpace::Definite(bounds.size.height),
516 );
517 decoration.layout_as_root(available_space, window, cx);
518 decoration.prepaint_at(bounds.origin, window, cx);
519 frame_state.decorations.push(decoration);
520 }
521 });
522 }
523
524 hitbox
525 },
526 )
527 }
528
529 fn paint(
530 &mut self,
531 global_id: Option<&GlobalElementId>,
532 inspector_id: Option<&InspectorElementId>,
533 bounds: Bounds<crate::Pixels>,
534 request_layout: &mut Self::RequestLayoutState,
535 hitbox: &mut Option<Hitbox>,
536 window: &mut Window,
537 cx: &mut App,
538 ) {
539 self.interactivity.paint(
540 global_id,
541 inspector_id,
542 bounds,
543 hitbox.as_ref(),
544 window,
545 cx,
546 |_, window, cx| {
547 for item in &mut request_layout.items {
548 item.paint(window, cx);
549 }
550 for decoration in &mut request_layout.decorations {
551 decoration.paint(window, cx);
552 }
553 },
554 )
555 }
556}
557
558impl IntoElement for UniformList {
559 type Element = Self;
560
561 fn into_element(self) -> Self::Element {
562 self
563 }
564}
565
566/// A decoration for a [`UniformList`]. This can be used for various things,
567/// such as rendering indent guides, or other visual effects.
568pub trait UniformListDecoration {
569 /// Compute the decoration element, given the visible range of list items,
570 /// the bounds of the list, and the height of each item.
571 fn compute(
572 &self,
573 visible_range: Range<usize>,
574 bounds: Bounds<Pixels>,
575 scroll_offset: Point<Pixels>,
576 item_height: Pixels,
577 item_count: usize,
578 window: &mut Window,
579 cx: &mut App,
580 ) -> AnyElement;
581}
582
583impl<T: UniformListDecoration + 'static> UniformListDecoration for Entity<T> {
584 fn compute(
585 &self,
586 visible_range: Range<usize>,
587 bounds: Bounds<Pixels>,
588 scroll_offset: Point<Pixels>,
589 item_height: Pixels,
590 item_count: usize,
591 window: &mut Window,
592 cx: &mut App,
593 ) -> AnyElement {
594 self.update(cx, |inner, cx| {
595 inner.compute(
596 visible_range,
597 bounds,
598 scroll_offset,
599 item_height,
600 item_count,
601 window,
602 cx,
603 )
604 })
605 }
606}
607
608impl UniformList {
609 /// Selects a specific list item for measurement.
610 pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
611 self.item_to_measure_index = item_index.unwrap_or(0);
612 self
613 }
614
615 /// Sets the sizing behavior, similar to the `List` element.
616 pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
617 self.sizing_behavior = behavior;
618 self
619 }
620
621 /// Sets the horizontal sizing behavior, controlling the way list items laid out horizontally.
622 /// With [`ListHorizontalSizingBehavior::Unconstrained`] behavior, every item and the list itself will
623 /// have the size of the widest item and lay out pushing the `end_slot` to the right end.
624 pub fn with_horizontal_sizing_behavior(
625 mut self,
626 behavior: ListHorizontalSizingBehavior,
627 ) -> Self {
628 self.horizontal_sizing_behavior = behavior;
629 match behavior {
630 ListHorizontalSizingBehavior::FitList => {
631 self.interactivity.base_style.overflow.x = None;
632 }
633 ListHorizontalSizingBehavior::Unconstrained => {
634 self.interactivity.base_style.overflow.x = Some(Overflow::Scroll);
635 }
636 }
637 self
638 }
639
640 /// Adds a decoration element to the list.
641 pub fn with_decoration(mut self, decoration: impl UniformListDecoration + 'static) -> Self {
642 self.decorations.push(Box::new(decoration));
643 self
644 }
645
646 fn measure_item(
647 &self,
648 list_width: Option<Pixels>,
649 window: &mut Window,
650 cx: &mut App,
651 ) -> Size<Pixels> {
652 if self.item_count == 0 {
653 return Size::default();
654 }
655
656 let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
657 let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx);
658 let Some(mut item_to_measure) = items.pop() else {
659 return Size::default();
660 };
661 let available_space = size(
662 list_width.map_or(AvailableSpace::MinContent, |width| {
663 AvailableSpace::Definite(width)
664 }),
665 AvailableSpace::MinContent,
666 );
667 item_to_measure.layout_as_root(available_space, window, cx)
668 }
669
670 /// Track and render scroll state of this list with reference to the given scroll handle.
671 pub fn track_scroll(mut self, handle: &UniformListScrollHandle) -> Self {
672 self.interactivity.tracked_scroll_handle = Some(handle.0.borrow().base_handle.clone());
673 self.scroll_handle = Some(handle.clone());
674 self
675 }
676
677 /// Sets whether the list is flipped vertically, such that item 0 appears at the bottom.
678 pub fn y_flipped(mut self, y_flipped: bool) -> Self {
679 if let Some(ref scroll_handle) = self.scroll_handle {
680 let mut scroll_state = scroll_handle.0.borrow_mut();
681 let mut base_handle = &scroll_state.base_handle;
682 let offset = base_handle.offset();
683 match scroll_state.last_item_size {
684 Some(last_size) if scroll_state.y_flipped != y_flipped => {
685 let new_y_offset =
686 -(offset.y + last_size.contents.height - last_size.item.height);
687 base_handle.set_offset(point(offset.x, new_y_offset));
688 scroll_state.y_flipped = y_flipped;
689 }
690 // Handle case where list is initially flipped.
691 None if y_flipped => {
692 base_handle.set_offset(point(offset.x, Pixels::MIN));
693 scroll_state.y_flipped = y_flipped;
694 }
695 _ => {}
696 }
697 }
698 self
699 }
700}
701
702impl InteractiveElement for UniformList {
703 fn interactivity(&mut self) -> &mut crate::Interactivity {
704 &mut self.interactivity
705 }
706}
707
708#[cfg(test)]
709mod test {
710 use crate::TestAppContext;
711
712 #[gpui::test]
713 fn test_scroll_strategy_nearest(cx: &mut TestAppContext) {
714 use crate::{
715 Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, actions, div,
716 prelude::*, px, uniform_list,
717 };
718 use std::ops::Range;
719
720 actions!(example, [SelectNext, SelectPrev]);
721
722 struct TestView {
723 index: usize,
724 length: usize,
725 scroll_handle: UniformListScrollHandle,
726 focus_handle: FocusHandle,
727 visible_range: Range<usize>,
728 }
729
730 impl TestView {
731 pub fn select_next(
732 &mut self,
733 _: &SelectNext,
734 window: &mut Window,
735 _: &mut Context<Self>,
736 ) {
737 if self.index + 1 == self.length {
738 self.index = 0
739 } else {
740 self.index += 1;
741 }
742 self.scroll_handle
743 .scroll_to_item(self.index, ScrollStrategy::Nearest);
744 window.refresh();
745 }
746
747 pub fn select_previous(
748 &mut self,
749 _: &SelectPrev,
750 window: &mut Window,
751 _: &mut Context<Self>,
752 ) {
753 if self.index == 0 {
754 self.index = self.length - 1
755 } else {
756 self.index -= 1;
757 }
758 self.scroll_handle
759 .scroll_to_item(self.index, ScrollStrategy::Nearest);
760 window.refresh();
761 }
762 }
763
764 impl Render for TestView {
765 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
766 div()
767 .id("list-example")
768 .track_focus(&self.focus_handle)
769 .on_action(cx.listener(Self::select_next))
770 .on_action(cx.listener(Self::select_previous))
771 .size_full()
772 .child(
773 uniform_list(
774 "entries",
775 self.length,
776 cx.processor(|this, range: Range<usize>, _window, _cx| {
777 this.visible_range = range.clone();
778 range
779 .map(|ix| div().id(ix).h(px(20.0)).child(format!("Item {ix}")))
780 .collect()
781 }),
782 )
783 .track_scroll(&self.scroll_handle)
784 .h(px(200.0)),
785 )
786 }
787 }
788
789 let (view, cx) = cx.add_window_view(|window, cx| {
790 let focus_handle = cx.focus_handle();
791 window.focus(&focus_handle);
792 TestView {
793 scroll_handle: UniformListScrollHandle::new(),
794 index: 0,
795 focus_handle,
796 length: 47,
797 visible_range: 0..0,
798 }
799 });
800
801 // 10 out of 47 items are visible
802
803 // First 9 times selecting next item does not scroll
804 for ix in 1..10 {
805 cx.dispatch_action(SelectNext);
806 view.read_with(cx, |view, _| {
807 assert_eq!(view.index, ix);
808 assert_eq!(view.visible_range, 0..10);
809 })
810 }
811
812 // Now each time the list scrolls down by 1
813 for ix in 10..47 {
814 cx.dispatch_action(SelectNext);
815 view.read_with(cx, |view, _| {
816 assert_eq!(view.index, ix);
817 assert_eq!(view.visible_range, ix - 9..ix + 1);
818 })
819 }
820
821 // After the last item we move back to the start
822 cx.dispatch_action(SelectNext);
823 view.read_with(cx, |view, _| {
824 assert_eq!(view.index, 0);
825 assert_eq!(view.visible_range, 0..10);
826 });
827
828 // Return to the last element
829 cx.dispatch_action(SelectPrev);
830 view.read_with(cx, |view, _| {
831 assert_eq!(view.index, 46);
832 assert_eq!(view.visible_range, 37..47);
833 });
834
835 // First 9 times selecting previous does not scroll
836 for ix in (37..46).rev() {
837 cx.dispatch_action(SelectPrev);
838 view.read_with(cx, |view, _| {
839 assert_eq!(view.index, ix);
840 assert_eq!(view.visible_range, 37..47);
841 })
842 }
843
844 // Now each time the list scrolls up by 1
845 for ix in (0..37).rev() {
846 cx.dispatch_action(SelectPrev);
847 view.read_with(cx, |view, _| {
848 assert_eq!(view.index, ix);
849 assert_eq!(view.visible_range, ix..ix + 10);
850 })
851 }
852 }
853}