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. In order to minimize
4//! re-renders, this element's state is stored intrusively on your own views, so that your code
5//! can coordinate directly with the list element's cached state.
6//!
7//! If all of your elements are the same height, see [`UniformList`] for a simpler API
8
9use crate::{
10 point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges,
11 Element, FocusHandle, GlobalElementId, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent,
12 Size, Style, StyleRefinement, Styled, WindowContext,
13};
14use collections::VecDeque;
15use refineable::Refineable as _;
16use std::{cell::RefCell, ops::Range, rc::Rc};
17use sum_tree::{Bias, SumTree};
18use taffy::style::Overflow;
19
20/// Construct a new list element
21pub fn list(state: ListState) -> List {
22 List {
23 state,
24 style: StyleRefinement::default(),
25 sizing_behavior: ListSizingBehavior::default(),
26 }
27}
28
29/// A list element
30pub struct List {
31 state: ListState,
32 style: StyleRefinement,
33 sizing_behavior: ListSizingBehavior,
34}
35
36impl List {
37 /// Set the sizing behavior for the list.
38 pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
39 self.sizing_behavior = behavior;
40 self
41 }
42}
43
44/// The list state that views must hold on behalf of the list element.
45#[derive(Clone)]
46pub struct ListState(Rc<RefCell<StateInner>>);
47
48struct StateInner {
49 last_layout_bounds: Option<Bounds<Pixels>>,
50 last_padding: Option<Edges<Pixels>>,
51 render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
52 items: SumTree<ListItem>,
53 logical_scroll_top: Option<ListOffset>,
54 alignment: ListAlignment,
55 overdraw: Pixels,
56 reset: bool,
57 #[allow(clippy::type_complexity)]
58 scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
59}
60
61/// Whether the list is scrolling from top to bottom or bottom to top.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum ListAlignment {
64 /// The list is scrolling from top to bottom, like most lists.
65 Top,
66 /// The list is scrolling from bottom to top, like a chat log.
67 Bottom,
68}
69
70/// A scroll event that has been converted to be in terms of the list's items.
71pub struct ListScrollEvent {
72 /// The range of items currently visible in the list, after applying the scroll event.
73 pub visible_range: Range<usize>,
74
75 /// The number of items that are currently visible in the list, after applying the scroll event.
76 pub count: usize,
77
78 /// Whether the list has been scrolled.
79 pub is_scrolled: bool,
80}
81
82/// The sizing behavior to apply during layout.
83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub enum ListSizingBehavior {
85 /// The list should calculate its size based on the size of its items.
86 Infer,
87 /// The list should not calculate a fixed size.
88 #[default]
89 Auto,
90}
91
92/// The horizontal sizing behavior to apply during layout.
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
94pub enum ListHorizontalSizingBehavior {
95 /// List items' width can never exceed the width of the list.
96 #[default]
97 FitList,
98 /// List items' width may go over the width of the list, if any item is wider.
99 Unconstrained,
100}
101
102struct LayoutItemsResponse {
103 max_item_width: Pixels,
104 scroll_top: ListOffset,
105 item_layouts: VecDeque<ItemLayout>,
106}
107
108struct ItemLayout {
109 index: usize,
110 element: AnyElement,
111 size: Size<Pixels>,
112}
113
114/// Frame state used by the [List] element after layout.
115pub struct ListPrepaintState {
116 hitbox: Hitbox,
117 layout: LayoutItemsResponse,
118}
119
120#[derive(Clone)]
121enum ListItem {
122 Unmeasured {
123 focus_handle: Option<FocusHandle>,
124 },
125 Measured {
126 size: Size<Pixels>,
127 focus_handle: Option<FocusHandle>,
128 },
129}
130
131impl ListItem {
132 fn size(&self) -> Option<Size<Pixels>> {
133 if let ListItem::Measured { size, .. } = self {
134 Some(*size)
135 } else {
136 None
137 }
138 }
139
140 fn focus_handle(&self) -> Option<FocusHandle> {
141 match self {
142 ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
143 focus_handle.clone()
144 }
145 }
146 }
147
148 fn contains_focused(&self, cx: &WindowContext) -> bool {
149 match self {
150 ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
151 focus_handle
152 .as_ref()
153 .is_some_and(|handle| handle.contains_focused(cx))
154 }
155 }
156 }
157}
158
159#[derive(Clone, Debug, Default, PartialEq)]
160struct ListItemSummary {
161 count: usize,
162 rendered_count: usize,
163 unrendered_count: usize,
164 height: Pixels,
165 has_focus_handles: bool,
166}
167
168#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
169struct Count(usize);
170
171#[derive(Clone, Debug, Default)]
172struct Height(Pixels);
173
174impl ListState {
175 /// Construct a new list state, for storage on a view.
176 ///
177 /// The overdraw parameter controls how much extra space is rendered
178 /// above and below the visible area. Elements within this area will
179 /// be measured even though they are not visible. This can help ensure
180 /// that the list doesn't flicker or pop in when scrolling.
181 pub fn new<R>(
182 item_count: usize,
183 alignment: ListAlignment,
184 overdraw: Pixels,
185 render_item: R,
186 ) -> Self
187 where
188 R: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
189 {
190 let this = Self(Rc::new(RefCell::new(StateInner {
191 last_layout_bounds: None,
192 last_padding: None,
193 render_item: Box::new(render_item),
194 items: SumTree::default(),
195 logical_scroll_top: None,
196 alignment,
197 overdraw,
198 scroll_handler: None,
199 reset: false,
200 })));
201 this.splice(0..0, item_count);
202 this
203 }
204
205 /// Reset this instantiation of the list state.
206 ///
207 /// Note that this will cause scroll events to be dropped until the next paint.
208 pub fn reset(&self, element_count: usize) {
209 let old_count = {
210 let state = &mut *self.0.borrow_mut();
211 state.reset = true;
212 state.logical_scroll_top = None;
213 state.items.summary().count
214 };
215
216 self.splice(0..old_count, element_count);
217 }
218
219 /// The number of items in this list.
220 pub fn item_count(&self) -> usize {
221 self.0.borrow().items.summary().count
222 }
223
224 /// Inform the list state that the items in `old_range` have been replaced
225 /// by `count` new items that must be recalculated.
226 pub fn splice(&self, old_range: Range<usize>, count: usize) {
227 self.splice_focusable(old_range, (0..count).map(|_| None))
228 }
229
230 /// Register with the list state that the items in `old_range` have been replaced
231 /// by new items. As opposed to [`splice`], this method allows an iterator of optional focus handles
232 /// to be supplied to properly integrate with items in the list that can be focused. If a focused item
233 /// is scrolled out of view, the list will continue to render it to allow keyboard interaction.
234 pub fn splice_focusable(
235 &self,
236 old_range: Range<usize>,
237 focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
238 ) {
239 let state = &mut *self.0.borrow_mut();
240
241 let mut old_items = state.items.cursor::<Count>(&());
242 let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right, &());
243 old_items.seek_forward(&Count(old_range.end), Bias::Right, &());
244
245 let mut spliced_count = 0;
246 new_items.extend(
247 focus_handles.into_iter().map(|focus_handle| {
248 spliced_count += 1;
249 ListItem::Unmeasured { focus_handle }
250 }),
251 &(),
252 );
253 new_items.append(old_items.suffix(&()), &());
254 drop(old_items);
255 state.items = new_items;
256
257 if let Some(ListOffset {
258 item_ix,
259 offset_in_item,
260 }) = state.logical_scroll_top.as_mut()
261 {
262 if old_range.contains(item_ix) {
263 *item_ix = old_range.start;
264 *offset_in_item = px(0.);
265 } else if old_range.end <= *item_ix {
266 *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
267 }
268 }
269 }
270
271 /// Set a handler that will be called when the list is scrolled.
272 pub fn set_scroll_handler(
273 &self,
274 handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
275 ) {
276 self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
277 }
278
279 /// Get the current scroll offset, in terms of the list's items.
280 pub fn logical_scroll_top(&self) -> ListOffset {
281 self.0.borrow().logical_scroll_top()
282 }
283
284 /// Scroll the list to the given offset
285 pub fn scroll_to(&self, mut scroll_top: ListOffset) {
286 let state = &mut *self.0.borrow_mut();
287 let item_count = state.items.summary().count;
288 if scroll_top.item_ix >= item_count {
289 scroll_top.item_ix = item_count;
290 scroll_top.offset_in_item = px(0.);
291 }
292
293 state.logical_scroll_top = Some(scroll_top);
294 }
295
296 /// Scroll the list to the given item, such that the item is fully visible.
297 pub fn scroll_to_reveal_item(&self, ix: usize) {
298 let state = &mut *self.0.borrow_mut();
299
300 let mut scroll_top = state.logical_scroll_top();
301 let height = state
302 .last_layout_bounds
303 .map_or(px(0.), |bounds| bounds.size.height);
304 let padding = state.last_padding.unwrap_or_default();
305
306 if ix <= scroll_top.item_ix {
307 scroll_top.item_ix = ix;
308 scroll_top.offset_in_item = px(0.);
309 } else {
310 let mut cursor = state.items.cursor::<ListItemSummary>(&());
311 cursor.seek(&Count(ix + 1), Bias::Right, &());
312 let bottom = cursor.start().height + padding.top;
313 let goal_top = px(0.).max(bottom - height + padding.bottom);
314
315 cursor.seek(&Height(goal_top), Bias::Left, &());
316 let start_ix = cursor.start().count;
317 let start_item_top = cursor.start().height;
318
319 if start_ix >= scroll_top.item_ix {
320 scroll_top.item_ix = start_ix;
321 scroll_top.offset_in_item = goal_top - start_item_top;
322 }
323 }
324
325 state.logical_scroll_top = Some(scroll_top);
326 }
327
328 /// Get the bounds for the given item in window coordinates, if it's
329 /// been rendered.
330 pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
331 let state = &*self.0.borrow();
332
333 let bounds = state.last_layout_bounds.unwrap_or_default();
334 let scroll_top = state.logical_scroll_top();
335 if ix < scroll_top.item_ix {
336 return None;
337 }
338
339 let mut cursor = state.items.cursor::<(Count, Height)>(&());
340 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
341
342 let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
343
344 cursor.seek_forward(&Count(ix), Bias::Right, &());
345 if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
346 let &(Count(count), Height(top)) = cursor.start();
347 if count == ix {
348 let top = bounds.top() + top - scroll_top;
349 return Some(Bounds::from_corners(
350 point(bounds.left(), top),
351 point(bounds.right(), top + size.height),
352 ));
353 }
354 }
355 None
356 }
357}
358
359impl StateInner {
360 fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
361 let mut cursor = self.items.cursor::<ListItemSummary>(&());
362 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
363 let start_y = cursor.start().height + scroll_top.offset_in_item;
364 cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
365 scroll_top.item_ix..cursor.start().count + 1
366 }
367
368 fn scroll(
369 &mut self,
370 scroll_top: &ListOffset,
371 height: Pixels,
372 delta: Point<Pixels>,
373 cx: &mut WindowContext,
374 ) {
375 // Drop scroll events after a reset, since we can't calculate
376 // the new logical scroll top without the item heights
377 if self.reset {
378 return;
379 }
380
381 let padding = self.last_padding.unwrap_or_default();
382 let scroll_max =
383 (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
384 let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
385 .max(px(0.))
386 .min(scroll_max);
387
388 if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
389 self.logical_scroll_top = None;
390 } else {
391 let mut cursor = self.items.cursor::<ListItemSummary>(&());
392 cursor.seek(&Height(new_scroll_top), Bias::Right, &());
393 let item_ix = cursor.start().count;
394 let offset_in_item = new_scroll_top - cursor.start().height;
395 self.logical_scroll_top = Some(ListOffset {
396 item_ix,
397 offset_in_item,
398 });
399 }
400
401 if self.scroll_handler.is_some() {
402 let visible_range = self.visible_range(height, scroll_top);
403 self.scroll_handler.as_mut().unwrap()(
404 &ListScrollEvent {
405 visible_range,
406 count: self.items.summary().count,
407 is_scrolled: self.logical_scroll_top.is_some(),
408 },
409 cx,
410 );
411 }
412
413 cx.refresh();
414 }
415
416 fn logical_scroll_top(&self) -> ListOffset {
417 self.logical_scroll_top
418 .unwrap_or_else(|| match self.alignment {
419 ListAlignment::Top => ListOffset {
420 item_ix: 0,
421 offset_in_item: px(0.),
422 },
423 ListAlignment::Bottom => ListOffset {
424 item_ix: self.items.summary().count,
425 offset_in_item: px(0.),
426 },
427 })
428 }
429
430 fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
431 let mut cursor = self.items.cursor::<ListItemSummary>(&());
432 cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
433 cursor.start().height + logical_scroll_top.offset_in_item
434 }
435
436 fn layout_items(
437 &mut self,
438 available_width: Option<Pixels>,
439 available_height: Pixels,
440 padding: &Edges<Pixels>,
441 cx: &mut WindowContext,
442 ) -> LayoutItemsResponse {
443 let old_items = self.items.clone();
444 let mut measured_items = VecDeque::new();
445 let mut item_layouts = VecDeque::new();
446 let mut rendered_height = padding.top;
447 let mut max_item_width = px(0.);
448 let mut scroll_top = self.logical_scroll_top();
449 let mut rendered_focused_item = false;
450
451 let available_item_space = size(
452 available_width.map_or(AvailableSpace::MinContent, |width| {
453 AvailableSpace::Definite(width)
454 }),
455 AvailableSpace::MinContent,
456 );
457
458 let mut cursor = old_items.cursor::<Count>(&());
459
460 // Render items after the scroll top, including those in the trailing overdraw
461 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
462 for (ix, item) in cursor.by_ref().enumerate() {
463 let visible_height = rendered_height - scroll_top.offset_in_item;
464 if visible_height >= available_height + self.overdraw {
465 break;
466 }
467
468 // Use the previously cached height and focus handle if available
469 let mut size = item.size();
470
471 // If we're within the visible area or the height wasn't cached, render and measure the item's element
472 if visible_height < available_height || size.is_none() {
473 let item_index = scroll_top.item_ix + ix;
474 let mut element = (self.render_item)(item_index, cx);
475 let element_size = element.layout_as_root(available_item_space, cx);
476 size = Some(element_size);
477 if visible_height < available_height {
478 item_layouts.push_back(ItemLayout {
479 index: item_index,
480 element,
481 size: element_size,
482 });
483 if item.contains_focused(cx) {
484 rendered_focused_item = true;
485 }
486 }
487 }
488
489 let size = size.unwrap();
490 rendered_height += size.height;
491 max_item_width = max_item_width.max(size.width);
492 measured_items.push_back(ListItem::Measured {
493 size,
494 focus_handle: item.focus_handle(),
495 });
496 }
497 rendered_height += padding.bottom;
498
499 // Prepare to start walking upward from the item at the scroll top.
500 cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
501
502 // If the rendered items do not fill the visible region, then adjust
503 // the scroll top upward.
504 if rendered_height - scroll_top.offset_in_item < available_height {
505 while rendered_height < available_height {
506 cursor.prev(&());
507 if let Some(item) = cursor.item() {
508 let item_index = cursor.start().0;
509 let mut element = (self.render_item)(item_index, cx);
510 let element_size = element.layout_as_root(available_item_space, cx);
511 let focus_handle = item.focus_handle();
512 rendered_height += element_size.height;
513 measured_items.push_front(ListItem::Measured {
514 size: element_size,
515 focus_handle,
516 });
517 item_layouts.push_front(ItemLayout {
518 index: item_index,
519 element,
520 size: element_size,
521 });
522 if item.contains_focused(cx) {
523 rendered_focused_item = true;
524 }
525 } else {
526 break;
527 }
528 }
529
530 scroll_top = ListOffset {
531 item_ix: cursor.start().0,
532 offset_in_item: rendered_height - available_height,
533 };
534
535 match self.alignment {
536 ListAlignment::Top => {
537 scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
538 self.logical_scroll_top = Some(scroll_top);
539 }
540 ListAlignment::Bottom => {
541 scroll_top = ListOffset {
542 item_ix: cursor.start().0,
543 offset_in_item: rendered_height - available_height,
544 };
545 self.logical_scroll_top = None;
546 }
547 };
548 }
549
550 // Measure items in the leading overdraw
551 let mut leading_overdraw = scroll_top.offset_in_item;
552 while leading_overdraw < self.overdraw {
553 cursor.prev(&());
554 if let Some(item) = cursor.item() {
555 let size = if let ListItem::Measured { size, .. } = item {
556 *size
557 } else {
558 let mut element = (self.render_item)(cursor.start().0, cx);
559 element.layout_as_root(available_item_space, cx)
560 };
561
562 leading_overdraw += size.height;
563 measured_items.push_front(ListItem::Measured {
564 size,
565 focus_handle: item.focus_handle(),
566 });
567 } else {
568 break;
569 }
570 }
571
572 let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
573 let mut cursor = old_items.cursor::<Count>(&());
574 let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
575 new_items.extend(measured_items, &());
576 cursor.seek(&Count(measured_range.end), Bias::Right, &());
577 new_items.append(cursor.suffix(&()), &());
578 self.items = new_items;
579
580 // If none of the visible items are focused, check if an off-screen item is focused
581 // and include it to be rendered after the visible items so keyboard interaction continues
582 // to work for it.
583 if !rendered_focused_item {
584 let mut cursor = self
585 .items
586 .filter::<_, Count>(&(), |summary| summary.has_focus_handles);
587 cursor.next(&());
588 while let Some(item) = cursor.item() {
589 if item.contains_focused(cx) {
590 let item_index = cursor.start().0;
591 let mut element = (self.render_item)(cursor.start().0, cx);
592 let size = element.layout_as_root(available_item_space, cx);
593 item_layouts.push_back(ItemLayout {
594 index: item_index,
595 element,
596 size,
597 });
598 break;
599 }
600 cursor.next(&());
601 }
602 }
603
604 LayoutItemsResponse {
605 max_item_width,
606 scroll_top,
607 item_layouts,
608 }
609 }
610
611 fn prepaint_items(
612 &mut self,
613 bounds: Bounds<Pixels>,
614 padding: Edges<Pixels>,
615 autoscroll: bool,
616 cx: &mut WindowContext,
617 ) -> Result<LayoutItemsResponse, ListOffset> {
618 cx.transact(|cx| {
619 let mut layout_response =
620 self.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx);
621
622 // Avoid honoring autoscroll requests from elements other than our children.
623 cx.take_autoscroll();
624
625 // Only paint the visible items, if there is actually any space for them (taking padding into account)
626 if bounds.size.height > padding.top + padding.bottom {
627 let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
628 item_origin.y -= layout_response.scroll_top.offset_in_item;
629 for item in &mut layout_response.item_layouts {
630 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
631 item.element.prepaint_at(item_origin, cx);
632 });
633
634 if let Some(autoscroll_bounds) = cx.take_autoscroll() {
635 if autoscroll {
636 if autoscroll_bounds.top() < bounds.top() {
637 return Err(ListOffset {
638 item_ix: item.index,
639 offset_in_item: autoscroll_bounds.top() - item_origin.y,
640 });
641 } else if autoscroll_bounds.bottom() > bounds.bottom() {
642 let mut cursor = self.items.cursor::<Count>(&());
643 cursor.seek(&Count(item.index), Bias::Right, &());
644 let mut height = bounds.size.height - padding.top - padding.bottom;
645
646 // Account for the height of the element down until the autoscroll bottom.
647 height -= autoscroll_bounds.bottom() - item_origin.y;
648
649 // Keep decreasing the scroll top until we fill all the available space.
650 while height > Pixels::ZERO {
651 cursor.prev(&());
652 let Some(item) = cursor.item() else { break };
653
654 let size = item.size().unwrap_or_else(|| {
655 let mut item = (self.render_item)(cursor.start().0, cx);
656 let item_available_size = size(
657 bounds.size.width.into(),
658 AvailableSpace::MinContent,
659 );
660 item.layout_as_root(item_available_size, cx)
661 });
662 height -= size.height;
663 }
664
665 return Err(ListOffset {
666 item_ix: cursor.start().0,
667 offset_in_item: if height < Pixels::ZERO {
668 -height
669 } else {
670 Pixels::ZERO
671 },
672 });
673 }
674 }
675 }
676
677 item_origin.y += item.size.height;
678 }
679 } else {
680 layout_response.item_layouts.clear();
681 }
682
683 Ok(layout_response)
684 })
685 }
686}
687
688impl std::fmt::Debug for ListItem {
689 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
690 match self {
691 Self::Unmeasured { .. } => write!(f, "Unrendered"),
692 Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
693 }
694 }
695}
696
697/// An offset into the list's items, in terms of the item index and the number
698/// of pixels off the top left of the item.
699#[derive(Debug, Clone, Copy, Default)]
700pub struct ListOffset {
701 /// The index of an item in the list
702 pub item_ix: usize,
703 /// The number of pixels to offset from the item index.
704 pub offset_in_item: Pixels,
705}
706
707impl Element for List {
708 type RequestLayoutState = ();
709 type PrepaintState = ListPrepaintState;
710
711 fn id(&self) -> Option<crate::ElementId> {
712 None
713 }
714
715 fn request_layout(
716 &mut self,
717 _id: Option<&GlobalElementId>,
718 cx: &mut crate::WindowContext,
719 ) -> (crate::LayoutId, Self::RequestLayoutState) {
720 let layout_id = match self.sizing_behavior {
721 ListSizingBehavior::Infer => {
722 let mut style = Style::default();
723 style.overflow.y = Overflow::Scroll;
724 style.refine(&self.style);
725 cx.with_text_style(style.text_style().cloned(), |cx| {
726 let state = &mut *self.state.0.borrow_mut();
727
728 let available_height = if let Some(last_bounds) = state.last_layout_bounds {
729 last_bounds.size.height
730 } else {
731 // If we don't have the last layout bounds (first render),
732 // we might just use the overdraw value as the available height to layout enough items.
733 state.overdraw
734 };
735 let padding = style.padding.to_pixels(
736 state.last_layout_bounds.unwrap_or_default().size.into(),
737 cx.rem_size(),
738 );
739
740 let layout_response = state.layout_items(None, available_height, &padding, cx);
741 let max_element_width = layout_response.max_item_width;
742
743 let summary = state.items.summary();
744 let total_height = summary.height;
745
746 cx.request_measured_layout(
747 style,
748 move |known_dimensions, available_space, _cx| {
749 let width =
750 known_dimensions
751 .width
752 .unwrap_or(match available_space.width {
753 AvailableSpace::Definite(x) => x,
754 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
755 max_element_width
756 }
757 });
758 let height = match available_space.height {
759 AvailableSpace::Definite(height) => total_height.min(height),
760 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
761 total_height
762 }
763 };
764 size(width, height)
765 },
766 )
767 })
768 }
769 ListSizingBehavior::Auto => {
770 let mut style = Style::default();
771 style.refine(&self.style);
772 cx.with_text_style(style.text_style().cloned(), |cx| {
773 cx.request_layout(style, None)
774 })
775 }
776 };
777 (layout_id, ())
778 }
779
780 fn prepaint(
781 &mut self,
782 _id: Option<&GlobalElementId>,
783 bounds: Bounds<Pixels>,
784 _: &mut Self::RequestLayoutState,
785 cx: &mut WindowContext,
786 ) -> ListPrepaintState {
787 let state = &mut *self.state.0.borrow_mut();
788 state.reset = false;
789
790 let mut style = Style::default();
791 style.refine(&self.style);
792
793 let hitbox = cx.insert_hitbox(bounds, false);
794
795 // If the width of the list has changed, invalidate all cached item heights
796 if state.last_layout_bounds.map_or(true, |last_bounds| {
797 last_bounds.size.width != bounds.size.width
798 }) {
799 let new_items = SumTree::from_iter(
800 state.items.iter().map(|item| ListItem::Unmeasured {
801 focus_handle: item.focus_handle(),
802 }),
803 &(),
804 );
805
806 state.items = new_items;
807 }
808
809 let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
810 let layout = match state.prepaint_items(bounds, padding, true, cx) {
811 Ok(layout) => layout,
812 Err(autoscroll_request) => {
813 state.logical_scroll_top = Some(autoscroll_request);
814 state.prepaint_items(bounds, padding, false, cx).unwrap()
815 }
816 };
817
818 state.last_layout_bounds = Some(bounds);
819 state.last_padding = Some(padding);
820 ListPrepaintState { hitbox, layout }
821 }
822
823 fn paint(
824 &mut self,
825 _id: Option<&GlobalElementId>,
826 bounds: Bounds<crate::Pixels>,
827 _: &mut Self::RequestLayoutState,
828 prepaint: &mut Self::PrepaintState,
829 cx: &mut crate::WindowContext,
830 ) {
831 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
832 for item in &mut prepaint.layout.item_layouts {
833 item.element.paint(cx);
834 }
835 });
836
837 let list_state = self.state.clone();
838 let height = bounds.size.height;
839 let scroll_top = prepaint.layout.scroll_top;
840 let hitbox_id = prepaint.hitbox.id;
841 cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
842 if phase == DispatchPhase::Bubble && hitbox_id.is_hovered(cx) {
843 list_state.0.borrow_mut().scroll(
844 &scroll_top,
845 height,
846 event.delta.pixel_delta(px(20.)),
847 cx,
848 )
849 }
850 });
851 }
852}
853
854impl IntoElement for List {
855 type Element = Self;
856
857 fn into_element(self) -> Self::Element {
858 self
859 }
860}
861
862impl Styled for List {
863 fn style(&mut self) -> &mut StyleRefinement {
864 &mut self.style
865 }
866}
867
868impl sum_tree::Item for ListItem {
869 type Summary = ListItemSummary;
870
871 fn summary(&self, _: &()) -> Self::Summary {
872 match self {
873 ListItem::Unmeasured { focus_handle } => ListItemSummary {
874 count: 1,
875 rendered_count: 0,
876 unrendered_count: 1,
877 height: px(0.),
878 has_focus_handles: focus_handle.is_some(),
879 },
880 ListItem::Measured {
881 size, focus_handle, ..
882 } => ListItemSummary {
883 count: 1,
884 rendered_count: 1,
885 unrendered_count: 0,
886 height: size.height,
887 has_focus_handles: focus_handle.is_some(),
888 },
889 }
890 }
891}
892
893impl sum_tree::Summary for ListItemSummary {
894 type Context = ();
895
896 fn zero(_cx: &()) -> Self {
897 Default::default()
898 }
899
900 fn add_summary(&mut self, summary: &Self, _: &()) {
901 self.count += summary.count;
902 self.rendered_count += summary.rendered_count;
903 self.unrendered_count += summary.unrendered_count;
904 self.height += summary.height;
905 self.has_focus_handles |= summary.has_focus_handles;
906 }
907}
908
909impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
910 fn zero(_cx: &()) -> Self {
911 Default::default()
912 }
913
914 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
915 self.0 += summary.count;
916 }
917}
918
919impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
920 fn zero(_cx: &()) -> Self {
921 Default::default()
922 }
923
924 fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
925 self.0 += summary.height;
926 }
927}
928
929impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
930 fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
931 self.0.partial_cmp(&other.count).unwrap()
932 }
933}
934
935impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
936 fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
937 self.0.partial_cmp(&other.height).unwrap()
938 }
939}
940
941#[cfg(test)]
942mod test {
943
944 use gpui::{ScrollDelta, ScrollWheelEvent};
945
946 use crate::{self as gpui, TestAppContext};
947
948 #[gpui::test]
949 fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
950 use crate::{div, list, point, px, size, Element, ListState, Styled};
951
952 let cx = cx.add_empty_window();
953
954 let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _| {
955 div().h(px(10.)).w_full().into_any()
956 });
957
958 // Ensure that the list is scrolled to the top
959 state.scroll_to(gpui::ListOffset {
960 item_ix: 0,
961 offset_in_item: px(0.0),
962 });
963
964 // Paint
965 cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_| {
966 list(state.clone()).w_full().h_full()
967 });
968
969 // Reset
970 state.reset(5);
971
972 // And then receive a scroll event _before_ the next paint
973 cx.simulate_event(ScrollWheelEvent {
974 position: point(px(1.), px(1.)),
975 delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
976 ..Default::default()
977 });
978
979 // Scroll position should stay at the top of the list
980 assert_eq!(state.logical_scroll_top().item_ix, 0);
981 assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
982 }
983}