1//! A list element that can be used to render a large number of differently sized elements
2//! efficiently. Clients of this API need to ensure that elements outside of the scrolled
3//! area do not change their height for this element to function correctly. If your elements
4//! do change height, notify the list element via [`ListState::splice`] or [`ListState::reset`].
5//! In order to minimize re-renders, this element's state is stored intrusively
6//! on your own views, so that your code can coordinate directly with the list element's cached state.
7//!
8//! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API
9
10use crate::{
11 AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
12 FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
13 Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
14 Window, point, px, size,
15};
16use collections::VecDeque;
17use refineable::Refineable as _;
18use std::{cell::RefCell, ops::Range, rc::Rc};
19use sum_tree::{Bias, Dimensions, SumTree};
20
21type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static;
22
23/// Construct a new list element
24pub fn list(
25 state: ListState,
26 render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static,
27) -> List {
28 List {
29 state,
30 render_item: Box::new(render_item),
31 style: StyleRefinement::default(),
32 sizing_behavior: ListSizingBehavior::default(),
33 }
34}
35
36/// A list element
37pub struct List {
38 state: ListState,
39 render_item: Box<RenderItemFn>,
40 style: StyleRefinement,
41 sizing_behavior: ListSizingBehavior,
42}
43
44impl List {
45 /// Set the sizing behavior for the list.
46 pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
47 self.sizing_behavior = behavior;
48 self
49 }
50}
51
52/// The list state that views must hold on behalf of the list element.
53#[derive(Clone)]
54pub struct ListState(Rc<RefCell<StateInner>>);
55
56impl std::fmt::Debug for ListState {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.write_str("ListState")
59 }
60}
61
62struct StateInner {
63 last_layout_bounds: Option<Bounds<Pixels>>,
64 last_padding: Option<Edges<Pixels>>,
65 items: SumTree<ListItem>,
66 logical_scroll_top: Option<ListOffset>,
67 alignment: ListAlignment,
68 overdraw: Pixels,
69 reset: bool,
70 #[allow(clippy::type_complexity)]
71 scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
72 scrollbar_drag_start_height: Option<Pixels>,
73 measuring_behavior: ListMeasuringBehavior,
74}
75
76/// Whether the list is scrolling from top to bottom or bottom to top.
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum ListAlignment {
79 /// The list is scrolling from top to bottom, like most lists.
80 Top,
81 /// The list is scrolling from bottom to top, like a chat log.
82 Bottom,
83}
84
85/// A scroll event that has been converted to be in terms of the list's items.
86pub struct ListScrollEvent {
87 /// The range of items currently visible in the list, after applying the scroll event.
88 pub visible_range: Range<usize>,
89
90 /// The number of items that are currently visible in the list, after applying the scroll event.
91 pub count: usize,
92
93 /// Whether the list has been scrolled.
94 pub is_scrolled: bool,
95}
96
97/// The sizing behavior to apply during layout.
98#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub enum ListSizingBehavior {
100 /// The list should calculate its size based on the size of its items.
101 Infer,
102 /// The list should not calculate a fixed size.
103 #[default]
104 Auto,
105}
106
107/// The measuring behavior to apply during layout.
108#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
109pub enum ListMeasuringBehavior {
110 /// Measure all items in the list.
111 /// Note: This can be expensive for the first frame in a large list.
112 Measure(bool),
113 /// Only measure visible items
114 #[default]
115 Visible,
116}
117
118impl ListMeasuringBehavior {
119 fn reset(&mut self) {
120 match self {
121 ListMeasuringBehavior::Measure(has_measured) => *has_measured = false,
122 ListMeasuringBehavior::Visible => {}
123 }
124 }
125}
126
127/// The horizontal sizing behavior to apply during layout.
128#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
129pub enum ListHorizontalSizingBehavior {
130 /// List items' width can never exceed the width of the list.
131 #[default]
132 FitList,
133 /// List items' width may go over the width of the list, if any item is wider.
134 Unconstrained,
135}
136
137struct LayoutItemsResponse {
138 max_item_width: Pixels,
139 scroll_top: ListOffset,
140 item_layouts: VecDeque<ItemLayout>,
141}
142
143struct ItemLayout {
144 index: usize,
145 element: AnyElement,
146 size: Size<Pixels>,
147}
148
149/// Frame state used by the [List] element after layout.
150pub struct ListPrepaintState {
151 hitbox: Hitbox,
152 layout: LayoutItemsResponse,
153}
154
155#[derive(Clone)]
156enum ListItem {
157 Unmeasured {
158 focus_handle: Option<FocusHandle>,
159 },
160 Measured {
161 size: Size<Pixels>,
162 focus_handle: Option<FocusHandle>,
163 },
164}
165
166impl ListItem {
167 fn size(&self) -> Option<Size<Pixels>> {
168 if let ListItem::Measured { size, .. } = self {
169 Some(*size)
170 } else {
171 None
172 }
173 }
174
175 fn focus_handle(&self) -> Option<FocusHandle> {
176 match self {
177 ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
178 focus_handle.clone()
179 }
180 }
181 }
182
183 fn contains_focused(&self, window: &Window, cx: &App) -> bool {
184 match self {
185 ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
186 focus_handle
187 .as_ref()
188 .is_some_and(|handle| handle.contains_focused(window, cx))
189 }
190 }
191 }
192}
193
194#[derive(Clone, Debug, Default, PartialEq)]
195struct ListItemSummary {
196 count: usize,
197 rendered_count: usize,
198 unrendered_count: usize,
199 height: Pixels,
200 has_focus_handles: bool,
201}
202
203#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
204struct Count(usize);
205
206#[derive(Clone, Debug, Default)]
207struct Height(Pixels);
208
209impl ListState {
210 /// Construct a new list state, for storage on a view.
211 ///
212 /// The overdraw parameter controls how much extra space is rendered
213 /// above and below the visible area. Elements within this area will
214 /// be measured even though they are not visible. This can help ensure
215 /// that the list doesn't flicker or pop in when scrolling.
216 pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {
217 let this = Self(Rc::new(RefCell::new(StateInner {
218 last_layout_bounds: None,
219 last_padding: None,
220 items: SumTree::default(),
221 logical_scroll_top: None,
222 alignment,
223 overdraw,
224 scroll_handler: None,
225 reset: false,
226 scrollbar_drag_start_height: None,
227 measuring_behavior: ListMeasuringBehavior::default(),
228 })));
229 this.splice(0..0, item_count);
230 this
231 }
232
233 /// Set the list to measure all items in the list in the first layout phase.
234 ///
235 /// This is useful for ensuring that the scrollbar size is correct instead of based on only rendered elements.
236 pub fn measure_all(self) -> Self {
237 self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false);
238 self
239 }
240
241 /// Reset this instantiation of the list state.
242 ///
243 /// Note that this will cause scroll events to be dropped until the next paint.
244 pub fn reset(&self, element_count: usize) {
245 let old_count = {
246 let state = &mut *self.0.borrow_mut();
247 state.reset = true;
248 state.measuring_behavior.reset();
249 state.logical_scroll_top = None;
250 state.scrollbar_drag_start_height = None;
251 state.items.summary().count
252 };
253
254 self.splice(0..old_count, element_count);
255 }
256
257 /// Remeasure all items without changing scroll position.
258 ///
259 /// Use this when item heights may have changed (e.g., font size changes)
260 /// but the number and identity of items remains the same.
261 pub fn remeasure(&self) {
262 let state = &mut *self.0.borrow_mut();
263 let items = state.items.clone();
264 state.measuring_behavior.reset();
265
266 let new_items = items.cursor::<Count>(()).map(|item| ListItem::Unmeasured {
267 focus_handle: item.focus_handle(),
268 });
269
270 state.items = SumTree::from_iter(new_items, ());
271 }
272
273 /// The number of items in this list.
274 pub fn item_count(&self) -> usize {
275 self.0.borrow().items.summary().count
276 }
277
278 /// Inform the list state that the items in `old_range` have been replaced
279 /// by `count` new items that must be recalculated.
280 pub fn splice(&self, old_range: Range<usize>, count: usize) {
281 self.splice_focusable(old_range, (0..count).map(|_| None))
282 }
283
284 /// Register with the list state that the items in `old_range` have been replaced
285 /// by new items. As opposed to [`Self::splice`], this method allows an iterator of optional focus handles
286 /// to be supplied to properly integrate with items in the list that can be focused. If a focused item
287 /// is scrolled out of view, the list will continue to render it to allow keyboard interaction.
288 pub fn splice_focusable(
289 &self,
290 old_range: Range<usize>,
291 focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
292 ) {
293 let state = &mut *self.0.borrow_mut();
294
295 let mut old_items = state.items.cursor::<Count>(());
296 let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right);
297 old_items.seek_forward(&Count(old_range.end), Bias::Right);
298
299 let mut spliced_count = 0;
300 new_items.extend(
301 focus_handles.into_iter().map(|focus_handle| {
302 spliced_count += 1;
303 ListItem::Unmeasured { focus_handle }
304 }),
305 (),
306 );
307 new_items.append(old_items.suffix(), ());
308 drop(old_items);
309 state.items = new_items;
310
311 if let Some(ListOffset {
312 item_ix,
313 offset_in_item,
314 }) = state.logical_scroll_top.as_mut()
315 {
316 if old_range.contains(item_ix) {
317 *item_ix = old_range.start;
318 *offset_in_item = px(0.);
319 } else if old_range.end <= *item_ix {
320 *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
321 }
322 }
323 }
324
325 /// Set a handler that will be called when the list is scrolled.
326 pub fn set_scroll_handler(
327 &self,
328 handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static,
329 ) {
330 self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
331 }
332
333 /// Get the current scroll offset, in terms of the list's items.
334 pub fn logical_scroll_top(&self) -> ListOffset {
335 self.0.borrow().logical_scroll_top()
336 }
337
338 /// Scroll the list by the given offset
339 pub fn scroll_by(&self, distance: Pixels) {
340 if distance == px(0.) {
341 return;
342 }
343
344 let current_offset = self.logical_scroll_top();
345 let state = &mut *self.0.borrow_mut();
346 let mut cursor = state.items.cursor::<ListItemSummary>(());
347 cursor.seek(&Count(current_offset.item_ix), Bias::Right);
348
349 let start_pixel_offset = cursor.start().height + current_offset.offset_in_item;
350 let new_pixel_offset = (start_pixel_offset + distance).max(px(0.));
351 if new_pixel_offset > start_pixel_offset {
352 cursor.seek_forward(&Height(new_pixel_offset), Bias::Right);
353 } else {
354 cursor.seek(&Height(new_pixel_offset), Bias::Right);
355 }
356
357 state.logical_scroll_top = Some(ListOffset {
358 item_ix: cursor.start().count,
359 offset_in_item: new_pixel_offset - cursor.start().height,
360 });
361 }
362
363 /// Scroll the list to the given offset
364 pub fn scroll_to(&self, mut scroll_top: ListOffset) {
365 let state = &mut *self.0.borrow_mut();
366 let item_count = state.items.summary().count;
367 if scroll_top.item_ix >= item_count {
368 scroll_top.item_ix = item_count;
369 scroll_top.offset_in_item = px(0.);
370 }
371
372 state.logical_scroll_top = Some(scroll_top);
373 }
374
375 /// Scroll the list to the given item, such that the item is fully visible.
376 pub fn scroll_to_reveal_item(&self, ix: usize) {
377 let state = &mut *self.0.borrow_mut();
378
379 let mut scroll_top = state.logical_scroll_top();
380 let height = state
381 .last_layout_bounds
382 .map_or(px(0.), |bounds| bounds.size.height);
383 let padding = state.last_padding.unwrap_or_default();
384
385 if ix <= scroll_top.item_ix {
386 scroll_top.item_ix = ix;
387 scroll_top.offset_in_item = px(0.);
388 } else {
389 let mut cursor = state.items.cursor::<ListItemSummary>(());
390 cursor.seek(&Count(ix + 1), Bias::Right);
391 let bottom = cursor.start().height + padding.top;
392 let goal_top = px(0.).max(bottom - height + padding.bottom);
393
394 cursor.seek(&Height(goal_top), Bias::Left);
395 let start_ix = cursor.start().count;
396 let start_item_top = cursor.start().height;
397
398 if start_ix >= scroll_top.item_ix {
399 scroll_top.item_ix = start_ix;
400 scroll_top.offset_in_item = goal_top - start_item_top;
401 }
402 }
403
404 state.logical_scroll_top = Some(scroll_top);
405 }
406
407 /// Get the bounds for the given item in window coordinates, if it's
408 /// been rendered.
409 pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
410 let state = &*self.0.borrow();
411
412 let bounds = state.last_layout_bounds.unwrap_or_default();
413 let scroll_top = state.logical_scroll_top();
414 if ix < scroll_top.item_ix {
415 return None;
416 }
417
418 let mut cursor = state.items.cursor::<Dimensions<Count, Height>>(());
419 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
420
421 let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item;
422
423 cursor.seek_forward(&Count(ix), Bias::Right);
424 if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
425 let &Dimensions(Count(count), Height(top), _) = cursor.start();
426 if count == ix {
427 let top = bounds.top() + top - scroll_top;
428 return Some(Bounds::from_corners(
429 point(bounds.left(), top),
430 point(bounds.right(), top + size.height),
431 ));
432 }
433 }
434 None
435 }
436
437 /// Call this method when the user starts dragging the scrollbar.
438 ///
439 /// This will prevent the height reported to the scrollbar from changing during the drag
440 /// as items in the overdraw get measured, and help offset scroll position changes accordingly.
441 pub fn scrollbar_drag_started(&self) {
442 let mut state = self.0.borrow_mut();
443 state.scrollbar_drag_start_height = Some(state.items.summary().height);
444 }
445
446 /// Called when the user stops dragging the scrollbar.
447 ///
448 /// See `scrollbar_drag_started`.
449 pub fn scrollbar_drag_ended(&self) {
450 self.0.borrow_mut().scrollbar_drag_start_height.take();
451 }
452
453 /// Set the offset from the scrollbar
454 pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
455 self.0.borrow_mut().set_offset_from_scrollbar(point);
456 }
457
458 /// Returns the maximum scroll offset according to the items we have measured.
459 /// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
460 pub fn max_offset_for_scrollbar(&self) -> Size<Pixels> {
461 let state = self.0.borrow();
462 let bounds = state.last_layout_bounds.unwrap_or_default();
463
464 let height = state
465 .scrollbar_drag_start_height
466 .unwrap_or_else(|| state.items.summary().height);
467
468 Size::new(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height))
469 }
470
471 /// Returns the current scroll offset adjusted for the scrollbar
472 pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
473 let state = &self.0.borrow();
474 let logical_scroll_top = state.logical_scroll_top();
475
476 let mut cursor = state.items.cursor::<ListItemSummary>(());
477 let summary: ListItemSummary =
478 cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right);
479 let content_height = state.items.summary().height;
480 let drag_offset =
481 // if dragging the scrollbar, we want to offset the point if the height changed
482 content_height - state.scrollbar_drag_start_height.unwrap_or(content_height);
483 let offset = summary.height + logical_scroll_top.offset_in_item - drag_offset;
484
485 Point::new(px(0.), -offset)
486 }
487
488 /// Return the bounds of the viewport in pixels.
489 pub fn viewport_bounds(&self) -> Bounds<Pixels> {
490 self.0.borrow().last_layout_bounds.unwrap_or_default()
491 }
492}
493
494impl StateInner {
495 fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
496 let mut cursor = self.items.cursor::<ListItemSummary>(());
497 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
498 let start_y = cursor.start().height + scroll_top.offset_in_item;
499 cursor.seek_forward(&Height(start_y + height), Bias::Left);
500 scroll_top.item_ix..cursor.start().count + 1
501 }
502
503 fn scroll(
504 &mut self,
505 scroll_top: &ListOffset,
506 height: Pixels,
507 delta: Point<Pixels>,
508 current_view: EntityId,
509 window: &mut Window,
510 cx: &mut App,
511 ) {
512 // Drop scroll events after a reset, since we can't calculate
513 // the new logical scroll top without the item heights
514 if self.reset {
515 return;
516 }
517
518 let padding = self.last_padding.unwrap_or_default();
519 let scroll_max =
520 (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
521 let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
522 .max(px(0.))
523 .min(scroll_max);
524
525 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
526 self.logical_scroll_top = None;
527 } else {
528 let (start, ..) =
529 self.items
530 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
531 let item_ix = start.count;
532 let offset_in_item = new_scroll_top - start.height;
533 self.logical_scroll_top = Some(ListOffset {
534 item_ix,
535 offset_in_item,
536 });
537 }
538
539 if self.scroll_handler.is_some() {
540 let visible_range = self.visible_range(height, scroll_top);
541 self.scroll_handler.as_mut().unwrap()(
542 &ListScrollEvent {
543 visible_range,
544 count: self.items.summary().count,
545 is_scrolled: self.logical_scroll_top.is_some(),
546 },
547 window,
548 cx,
549 );
550 }
551
552 cx.notify(current_view);
553 }
554
555 fn logical_scroll_top(&self) -> ListOffset {
556 self.logical_scroll_top
557 .unwrap_or_else(|| match self.alignment {
558 ListAlignment::Top => ListOffset {
559 item_ix: 0,
560 offset_in_item: px(0.),
561 },
562 ListAlignment::Bottom => ListOffset {
563 item_ix: self.items.summary().count,
564 offset_in_item: px(0.),
565 },
566 })
567 }
568
569 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
570 let (start, ..) = self.items.find::<ListItemSummary, _>(
571 (),
572 &Count(logical_scroll_top.item_ix),
573 Bias::Right,
574 );
575 start.height + logical_scroll_top.offset_in_item
576 }
577
578 fn layout_all_items(
579 &mut self,
580 available_width: Pixels,
581 render_item: &mut RenderItemFn,
582 window: &mut Window,
583 cx: &mut App,
584 ) {
585 match &mut self.measuring_behavior {
586 ListMeasuringBehavior::Visible => {
587 return;
588 }
589 ListMeasuringBehavior::Measure(has_measured) => {
590 if *has_measured {
591 return;
592 }
593 *has_measured = true;
594 }
595 }
596
597 let mut cursor = self.items.cursor::<Count>(());
598 let available_item_space = size(
599 AvailableSpace::Definite(available_width),
600 AvailableSpace::MinContent,
601 );
602
603 let mut measured_items = Vec::default();
604
605 for (ix, item) in cursor.enumerate() {
606 let size = item.size().unwrap_or_else(|| {
607 let mut element = render_item(ix, window, cx);
608 element.layout_as_root(available_item_space, window, cx)
609 });
610
611 measured_items.push(ListItem::Measured {
612 size,
613 focus_handle: item.focus_handle(),
614 });
615 }
616
617 self.items = SumTree::from_iter(measured_items, ());
618 }
619
620 fn layout_items(
621 &mut self,
622 available_width: Option<Pixels>,
623 available_height: Pixels,
624 padding: &Edges<Pixels>,
625 render_item: &mut RenderItemFn,
626 window: &mut Window,
627 cx: &mut App,
628 ) -> LayoutItemsResponse {
629 let old_items = self.items.clone();
630 let mut measured_items = VecDeque::new();
631 let mut item_layouts = VecDeque::new();
632 let mut rendered_height = padding.top;
633 let mut max_item_width = px(0.);
634 let mut scroll_top = self.logical_scroll_top();
635 let mut rendered_focused_item = false;
636
637 let available_item_space = size(
638 available_width.map_or(AvailableSpace::MinContent, |width| {
639 AvailableSpace::Definite(width)
640 }),
641 AvailableSpace::MinContent,
642 );
643
644 let mut cursor = old_items.cursor::<Count>(());
645
646 // Render items after the scroll top, including those in the trailing overdraw
647 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
648 for (ix, item) in cursor.by_ref().enumerate() {
649 let visible_height = rendered_height - scroll_top.offset_in_item;
650 if visible_height >= available_height + self.overdraw {
651 break;
652 }
653
654 // Use the previously cached height and focus handle if available
655 let mut size = item.size();
656
657 // If we're within the visible area or the height wasn't cached, render and measure the item's element
658 if visible_height < available_height || size.is_none() {
659 let item_index = scroll_top.item_ix + ix;
660 let mut element = render_item(item_index, window, cx);
661 let element_size = element.layout_as_root(available_item_space, window, cx);
662 size = Some(element_size);
663 if visible_height < available_height {
664 item_layouts.push_back(ItemLayout {
665 index: item_index,
666 element,
667 size: element_size,
668 });
669 if item.contains_focused(window, cx) {
670 rendered_focused_item = true;
671 }
672 }
673 }
674
675 let size = size.unwrap();
676 rendered_height += size.height;
677 max_item_width = max_item_width.max(size.width);
678 measured_items.push_back(ListItem::Measured {
679 size,
680 focus_handle: item.focus_handle(),
681 });
682 }
683 rendered_height += padding.bottom;
684
685 // Prepare to start walking upward from the item at the scroll top.
686 cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
687
688 // If the rendered items do not fill the visible region, then adjust
689 // the scroll top upward.
690 if rendered_height - scroll_top.offset_in_item < available_height {
691 while rendered_height < available_height {
692 cursor.prev();
693 if let Some(item) = cursor.item() {
694 let item_index = cursor.start().0;
695 let mut element = render_item(item_index, window, cx);
696 let element_size = element.layout_as_root(available_item_space, window, cx);
697 let focus_handle = item.focus_handle();
698 rendered_height += element_size.height;
699 measured_items.push_front(ListItem::Measured {
700 size: element_size,
701 focus_handle,
702 });
703 item_layouts.push_front(ItemLayout {
704 index: item_index,
705 element,
706 size: element_size,
707 });
708 if item.contains_focused(window, cx) {
709 rendered_focused_item = true;
710 }
711 } else {
712 break;
713 }
714 }
715
716 scroll_top = ListOffset {
717 item_ix: cursor.start().0,
718 offset_in_item: rendered_height - available_height,
719 };
720
721 match self.alignment {
722 ListAlignment::Top => {
723 scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
724 self.logical_scroll_top = Some(scroll_top);
725 }
726 ListAlignment::Bottom => {
727 scroll_top = ListOffset {
728 item_ix: cursor.start().0,
729 offset_in_item: rendered_height - available_height,
730 };
731 self.logical_scroll_top = None;
732 }
733 };
734 }
735
736 // Measure items in the leading overdraw
737 let mut leading_overdraw = scroll_top.offset_in_item;
738 while leading_overdraw < self.overdraw {
739 cursor.prev();
740 if let Some(item) = cursor.item() {
741 let size = if let ListItem::Measured { size, .. } = item {
742 *size
743 } else {
744 let mut element = render_item(cursor.start().0, window, cx);
745 element.layout_as_root(available_item_space, window, cx)
746 };
747
748 leading_overdraw += size.height;
749 measured_items.push_front(ListItem::Measured {
750 size,
751 focus_handle: item.focus_handle(),
752 });
753 } else {
754 break;
755 }
756 }
757
758 let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
759 let mut cursor = old_items.cursor::<Count>(());
760 let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right);
761 new_items.extend(measured_items, ());
762 cursor.seek(&Count(measured_range.end), Bias::Right);
763 new_items.append(cursor.suffix(), ());
764 self.items = new_items;
765
766 // If none of the visible items are focused, check if an off-screen item is focused
767 // and include it to be rendered after the visible items so keyboard interaction continues
768 // to work for it.
769 if !rendered_focused_item {
770 let mut cursor = self
771 .items
772 .filter::<_, Count>((), |summary| summary.has_focus_handles);
773 cursor.next();
774 while let Some(item) = cursor.item() {
775 if item.contains_focused(window, cx) {
776 let item_index = cursor.start().0;
777 let mut element = render_item(cursor.start().0, window, cx);
778 let size = element.layout_as_root(available_item_space, window, cx);
779 item_layouts.push_back(ItemLayout {
780 index: item_index,
781 element,
782 size,
783 });
784 break;
785 }
786 cursor.next();
787 }
788 }
789
790 LayoutItemsResponse {
791 max_item_width,
792 scroll_top,
793 item_layouts,
794 }
795 }
796
797 fn prepaint_items(
798 &mut self,
799 bounds: Bounds<Pixels>,
800 padding: Edges<Pixels>,
801 autoscroll: bool,
802 render_item: &mut RenderItemFn,
803 window: &mut Window,
804 cx: &mut App,
805 ) -> Result<LayoutItemsResponse, ListOffset> {
806 window.transact(|window| {
807 match self.measuring_behavior {
808 ListMeasuringBehavior::Measure(has_measured) if !has_measured => {
809 self.layout_all_items(bounds.size.width, render_item, window, cx);
810 }
811 _ => {}
812 }
813
814 let mut layout_response = self.layout_items(
815 Some(bounds.size.width),
816 bounds.size.height,
817 &padding,
818 render_item,
819 window,
820 cx,
821 );
822
823 // Avoid honoring autoscroll requests from elements other than our children.
824 window.take_autoscroll();
825
826 // Only paint the visible items, if there is actually any space for them (taking padding into account)
827 if bounds.size.height > padding.top + padding.bottom {
828 let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
829 item_origin.y -= layout_response.scroll_top.offset_in_item;
830 for item in &mut layout_response.item_layouts {
831 window.with_content_mask(Some(ContentMask { bounds }), |window| {
832 item.element.prepaint_at(item_origin, window, cx);
833 });
834
835 if let Some(autoscroll_bounds) = window.take_autoscroll()
836 && autoscroll
837 {
838 if autoscroll_bounds.top() < bounds.top() {
839 return Err(ListOffset {
840 item_ix: item.index,
841 offset_in_item: autoscroll_bounds.top() - item_origin.y,
842 });
843 } else if autoscroll_bounds.bottom() > bounds.bottom() {
844 let mut cursor = self.items.cursor::<Count>(());
845 cursor.seek(&Count(item.index), Bias::Right);
846 let mut height = bounds.size.height - padding.top - padding.bottom;
847
848 // Account for the height of the element down until the autoscroll bottom.
849 height -= autoscroll_bounds.bottom() - item_origin.y;
850
851 // Keep decreasing the scroll top until we fill all the available space.
852 while height > Pixels::ZERO {
853 cursor.prev();
854 let Some(item) = cursor.item() else { break };
855
856 let size = item.size().unwrap_or_else(|| {
857 let mut item = render_item(cursor.start().0, window, cx);
858 let item_available_size =
859 size(bounds.size.width.into(), AvailableSpace::MinContent);
860 item.layout_as_root(item_available_size, window, cx)
861 });
862 height -= size.height;
863 }
864
865 return Err(ListOffset {
866 item_ix: cursor.start().0,
867 offset_in_item: if height < Pixels::ZERO {
868 -height
869 } else {
870 Pixels::ZERO
871 },
872 });
873 }
874 }
875
876 item_origin.y += item.size.height;
877 }
878 } else {
879 layout_response.item_layouts.clear();
880 }
881
882 Ok(layout_response)
883 })
884 }
885
886 // Scrollbar support
887
888 fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
889 let Some(bounds) = self.last_layout_bounds else {
890 return;
891 };
892 let height = bounds.size.height;
893
894 let padding = self.last_padding.unwrap_or_default();
895 let content_height = self.items.summary().height;
896 let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
897 let drag_offset =
898 // if dragging the scrollbar, we want to offset the point if the height changed
899 content_height - self.scrollbar_drag_start_height.unwrap_or(content_height);
900 let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max);
901
902 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
903 self.logical_scroll_top = None;
904 } else {
905 let (start, _, _) =
906 self.items
907 .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
908
909 let item_ix = start.count;
910 let offset_in_item = new_scroll_top - start.height;
911 self.logical_scroll_top = Some(ListOffset {
912 item_ix,
913 offset_in_item,
914 });
915 }
916 }
917}
918
919impl std::fmt::Debug for ListItem {
920 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
921 match self {
922 Self::Unmeasured { .. } => write!(f, "Unrendered"),
923 Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
924 }
925 }
926}
927
928/// An offset into the list's items, in terms of the item index and the number
929/// of pixels off the top left of the item.
930#[derive(Debug, Clone, Copy, Default)]
931pub struct ListOffset {
932 /// The index of an item in the list
933 pub item_ix: usize,
934 /// The number of pixels to offset from the item index.
935 pub offset_in_item: Pixels,
936}
937
938impl Element for List {
939 type RequestLayoutState = ();
940 type PrepaintState = ListPrepaintState;
941
942 fn id(&self) -> Option<crate::ElementId> {
943 None
944 }
945
946 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
947 None
948 }
949
950 fn request_layout(
951 &mut self,
952 _id: Option<&GlobalElementId>,
953 _inspector_id: Option<&InspectorElementId>,
954 window: &mut Window,
955 cx: &mut App,
956 ) -> (crate::LayoutId, Self::RequestLayoutState) {
957 let layout_id = match self.sizing_behavior {
958 ListSizingBehavior::Infer => {
959 let mut style = Style::default();
960 style.overflow.y = Overflow::Scroll;
961 style.refine(&self.style);
962 window.with_text_style(style.text_style().cloned(), |window| {
963 let state = &mut *self.state.0.borrow_mut();
964
965 let available_height = if let Some(last_bounds) = state.last_layout_bounds {
966 last_bounds.size.height
967 } else {
968 // If we don't have the last layout bounds (first render),
969 // we might just use the overdraw value as the available height to layout enough items.
970 state.overdraw
971 };
972 let padding = style.padding.to_pixels(
973 state.last_layout_bounds.unwrap_or_default().size.into(),
974 window.rem_size(),
975 );
976
977 let layout_response = state.layout_items(
978 None,
979 available_height,
980 &padding,
981 &mut self.render_item,
982 window,
983 cx,
984 );
985 let max_element_width = layout_response.max_item_width;
986
987 let summary = state.items.summary();
988 let total_height = summary.height;
989
990 window.request_measured_layout(
991 style,
992 move |known_dimensions, available_space, _window, _cx| {
993 let width =
994 known_dimensions
995 .width
996 .unwrap_or(match available_space.width {
997 AvailableSpace::Definite(x) => x,
998 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
999 max_element_width
1000 }
1001 });
1002 let height = match available_space.height {
1003 AvailableSpace::Definite(height) => total_height.min(height),
1004 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1005 total_height
1006 }
1007 };
1008 size(width, height)
1009 },
1010 )
1011 })
1012 }
1013 ListSizingBehavior::Auto => {
1014 let mut style = Style::default();
1015 style.refine(&self.style);
1016 window.with_text_style(style.text_style().cloned(), |window| {
1017 window.request_layout(style, None, cx)
1018 })
1019 }
1020 };
1021 (layout_id, ())
1022 }
1023
1024 fn prepaint(
1025 &mut self,
1026 _id: Option<&GlobalElementId>,
1027 _inspector_id: Option<&InspectorElementId>,
1028 bounds: Bounds<Pixels>,
1029 _: &mut Self::RequestLayoutState,
1030 window: &mut Window,
1031 cx: &mut App,
1032 ) -> ListPrepaintState {
1033 let state = &mut *self.state.0.borrow_mut();
1034 state.reset = false;
1035
1036 let mut style = Style::default();
1037 style.refine(&self.style);
1038
1039 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1040
1041 // If the width of the list has changed, invalidate all cached item heights
1042 if state
1043 .last_layout_bounds
1044 .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width)
1045 {
1046 let new_items = SumTree::from_iter(
1047 state.items.iter().map(|item| ListItem::Unmeasured {
1048 focus_handle: item.focus_handle(),
1049 }),
1050 (),
1051 );
1052
1053 state.items = new_items;
1054 }
1055
1056 let padding = style
1057 .padding
1058 .to_pixels(bounds.size.into(), window.rem_size());
1059 let layout =
1060 match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
1061 Ok(layout) => layout,
1062 Err(autoscroll_request) => {
1063 state.logical_scroll_top = Some(autoscroll_request);
1064 state
1065 .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
1066 .unwrap()
1067 }
1068 };
1069
1070 state.last_layout_bounds = Some(bounds);
1071 state.last_padding = Some(padding);
1072 ListPrepaintState { hitbox, layout }
1073 }
1074
1075 fn paint(
1076 &mut self,
1077 _id: Option<&GlobalElementId>,
1078 _inspector_id: Option<&InspectorElementId>,
1079 bounds: Bounds<crate::Pixels>,
1080 _: &mut Self::RequestLayoutState,
1081 prepaint: &mut Self::PrepaintState,
1082 window: &mut Window,
1083 cx: &mut App,
1084 ) {
1085 let current_view = window.current_view();
1086 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1087 for item in &mut prepaint.layout.item_layouts {
1088 item.element.paint(window, cx);
1089 }
1090 });
1091
1092 let list_state = self.state.clone();
1093 let height = bounds.size.height;
1094 let scroll_top = prepaint.layout.scroll_top;
1095 let hitbox_id = prepaint.hitbox.id;
1096 let mut accumulated_scroll_delta = ScrollDelta::default();
1097 window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
1098 if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
1099 accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
1100 let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
1101 list_state.0.borrow_mut().scroll(
1102 &scroll_top,
1103 height,
1104 pixel_delta,
1105 current_view,
1106 window,
1107 cx,
1108 )
1109 }
1110 });
1111 }
1112}
1113
1114impl IntoElement for List {
1115 type Element = Self;
1116
1117 fn into_element(self) -> Self::Element {
1118 self
1119 }
1120}
1121
1122impl Styled for List {
1123 fn style(&mut self) -> &mut StyleRefinement {
1124 &mut self.style
1125 }
1126}
1127
1128impl sum_tree::Item for ListItem {
1129 type Summary = ListItemSummary;
1130
1131 fn summary(&self, _: ()) -> Self::Summary {
1132 match self {
1133 ListItem::Unmeasured { focus_handle } => ListItemSummary {
1134 count: 1,
1135 rendered_count: 0,
1136 unrendered_count: 1,
1137 height: px(0.),
1138 has_focus_handles: focus_handle.is_some(),
1139 },
1140 ListItem::Measured {
1141 size, focus_handle, ..
1142 } => ListItemSummary {
1143 count: 1,
1144 rendered_count: 1,
1145 unrendered_count: 0,
1146 height: size.height,
1147 has_focus_handles: focus_handle.is_some(),
1148 },
1149 }
1150 }
1151}
1152
1153impl sum_tree::ContextLessSummary for ListItemSummary {
1154 fn zero() -> Self {
1155 Default::default()
1156 }
1157
1158 fn add_summary(&mut self, summary: &Self) {
1159 self.count += summary.count;
1160 self.rendered_count += summary.rendered_count;
1161 self.unrendered_count += summary.unrendered_count;
1162 self.height += summary.height;
1163 self.has_focus_handles |= summary.has_focus_handles;
1164 }
1165}
1166
1167impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
1168 fn zero(_cx: ()) -> Self {
1169 Default::default()
1170 }
1171
1172 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1173 self.0 += summary.count;
1174 }
1175}
1176
1177impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
1178 fn zero(_cx: ()) -> Self {
1179 Default::default()
1180 }
1181
1182 fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) {
1183 self.0 += summary.height;
1184 }
1185}
1186
1187impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count {
1188 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1189 self.0.partial_cmp(&other.count).unwrap()
1190 }
1191}
1192
1193impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
1194 fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering {
1195 self.0.partial_cmp(&other.height).unwrap()
1196 }
1197}
1198
1199#[cfg(test)]
1200mod test {
1201
1202 use gpui::{ScrollDelta, ScrollWheelEvent};
1203
1204 use crate::{self as gpui, TestAppContext};
1205
1206 #[gpui::test]
1207 fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
1208 use crate::{
1209 AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
1210 list, point, px, size,
1211 };
1212
1213 let cx = cx.add_empty_window();
1214
1215 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1216
1217 // Ensure that the list is scrolled to the top
1218 state.scroll_to(gpui::ListOffset {
1219 item_ix: 0,
1220 offset_in_item: px(0.0),
1221 });
1222
1223 struct TestView(ListState);
1224 impl Render for TestView {
1225 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1226 list(self.0.clone(), |_, _, _| {
1227 div().h(px(10.)).w_full().into_any()
1228 })
1229 .w_full()
1230 .h_full()
1231 }
1232 }
1233
1234 // Paint
1235 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| {
1236 cx.new(|_| TestView(state.clone()))
1237 });
1238
1239 // Reset
1240 state.reset(5);
1241
1242 // And then receive a scroll event _before_ the next paint
1243 cx.simulate_event(ScrollWheelEvent {
1244 position: point(px(1.), px(1.)),
1245 delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
1246 ..Default::default()
1247 });
1248
1249 // Scroll position should stay at the top of the list
1250 assert_eq!(state.logical_scroll_top().item_ix, 0);
1251 assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
1252 }
1253
1254 #[gpui::test]
1255 fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
1256 use crate::{
1257 AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
1258 list, point, px, size,
1259 };
1260
1261 let cx = cx.add_empty_window();
1262
1263 let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
1264
1265 struct TestView(ListState);
1266 impl Render for TestView {
1267 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1268 list(self.0.clone(), |_, _, _| {
1269 div().h(px(20.)).w_full().into_any()
1270 })
1271 .w_full()
1272 .h_full()
1273 }
1274 }
1275
1276 // Paint
1277 cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
1278 cx.new(|_| TestView(state.clone()))
1279 });
1280
1281 // Test positive distance: start at item 1, move down 30px
1282 state.scroll_by(px(30.));
1283
1284 // Should move to item 2
1285 let offset = state.logical_scroll_top();
1286 assert_eq!(offset.item_ix, 1);
1287 assert_eq!(offset.offset_in_item, px(10.));
1288
1289 // Test negative distance: start at item 2, move up 30px
1290 state.scroll_by(px(-30.));
1291
1292 // Should move back to item 1
1293 let offset = state.logical_scroll_top();
1294 assert_eq!(offset.item_ix, 0);
1295 assert_eq!(offset.offset_in_item, px(0.));
1296
1297 // Test zero distance
1298 state.scroll_by(px(0.));
1299 let offset = state.logical_scroll_top();
1300 assert_eq!(offset.item_ix, 0);
1301 assert_eq!(offset.offset_in_item, px(0.));
1302 }
1303}