1use std::{
14 cell::RefCell,
15 cmp,
16 ops::{Deref, Range},
17 rc::Rc,
18};
19
20use gpui::{
21 div, point, px, size, Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask,
22 Context, DeferredScrollToItem, Div, Element, ElementId, Entity, GlobalElementId, Half, Hitbox,
23 InteractiveElement, IntoElement, IsZero as _, ListSizingBehavior, Pixels, Point, Render,
24 ScrollHandle, ScrollStrategy, Size, Stateful, StatefulInteractiveElement, StyleRefinement,
25 Styled, Window,
26};
27use smallvec::SmallVec;
28
29use crate::{scroll::ScrollHandleOffsetable, AxisExt, PixelsExt};
30
31struct VirtualListScrollHandleState {
32 axis: Axis,
33 items_count: usize,
34 pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
35}
36
37#[derive(Clone)]
38pub struct VirtualListScrollHandle {
39 state: Rc<RefCell<VirtualListScrollHandleState>>,
40 base_handle: ScrollHandle,
41}
42
43impl From<ScrollHandle> for VirtualListScrollHandle {
44 fn from(handle: ScrollHandle) -> Self {
45 let mut this = VirtualListScrollHandle::new();
46 this.base_handle = handle;
47 this
48 }
49}
50
51impl AsRef<ScrollHandle> for VirtualListScrollHandle {
52 fn as_ref(&self) -> &ScrollHandle {
53 &self.base_handle
54 }
55}
56
57impl ScrollHandleOffsetable for VirtualListScrollHandle {
58 fn offset(&self) -> Point<Pixels> {
59 self.base_handle.offset()
60 }
61
62 fn set_offset(&self, offset: Point<Pixels>) {
63 self.base_handle.set_offset(offset);
64 }
65
66 fn content_size(&self) -> Size<Pixels> {
67 self.base_handle.content_size()
68 }
69}
70
71impl Deref for VirtualListScrollHandle {
72 type Target = ScrollHandle;
73
74 fn deref(&self) -> &Self::Target {
75 &self.base_handle
76 }
77}
78
79impl VirtualListScrollHandle {
80 pub fn new() -> Self {
81 VirtualListScrollHandle {
82 state: Rc::new(RefCell::new(VirtualListScrollHandleState {
83 axis: Axis::Vertical,
84 items_count: 0,
85 deferred_scroll_to_item: None,
86 })),
87 base_handle: ScrollHandle::default(),
88 }
89 }
90
91 pub fn base_handle(&self) -> &ScrollHandle {
92 &self.base_handle
93 }
94
95 pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
97 self.scroll_to_item_with_offset(ix, strategy, 0);
98 }
99
100 fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) {
102 let mut state = self.state.borrow_mut();
103 state.deferred_scroll_to_item = Some(DeferredScrollToItem {
104 item_index: ix,
105 strategy,
106 offset,
107 scroll_strict: false,
108 });
109 }
110
111 pub fn scroll_to_bottom(&self) {
113 let items_count = self.state.borrow().items_count;
114 self.scroll_to_item(items_count.saturating_sub(1), ScrollStrategy::Top);
115 }
116}
117
118#[inline]
127pub fn v_virtual_list<R, V>(
128 view: Entity<V>,
129 id: impl Into<ElementId>,
130 item_sizes: Rc<Vec<Size<Pixels>>>,
131 f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
132) -> VirtualList
133where
134 R: IntoElement,
135 V: Render,
136{
137 virtual_list(view, id, Axis::Vertical, item_sizes, f)
138}
139
140#[inline]
147pub fn h_virtual_list<R, V>(
148 view: Entity<V>,
149 id: impl Into<ElementId>,
150 item_sizes: Rc<Vec<Size<Pixels>>>,
151 f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
152) -> VirtualList
153where
154 R: IntoElement,
155 V: Render,
156{
157 virtual_list(view, id, Axis::Horizontal, item_sizes, f)
158}
159
160pub(crate) fn virtual_list<R, V>(
161 view: Entity<V>,
162 id: impl Into<ElementId>,
163 axis: Axis,
164 item_sizes: Rc<Vec<Size<Pixels>>>,
165 f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
166) -> VirtualList
167where
168 R: IntoElement,
169 V: Render,
170{
171 let id: ElementId = id.into();
172 let scroll_handle = VirtualListScrollHandle::new();
173 let render_range = move |visible_range, window: &mut Window, cx: &mut App| {
174 view.update(cx, |this, cx| {
175 f(this, visible_range, window, cx)
176 .into_iter()
177 .map(|component| component.into_any_element())
178 .collect()
179 })
180 };
181
182 VirtualList {
183 id: id.clone(),
184 axis,
185 base: div()
186 .id(id)
187 .size_full()
188 .overflow_scroll()
189 .track_scroll(&scroll_handle),
190 scroll_handle,
191 items_count: item_sizes.len(),
192 item_sizes,
193 render_items: Box::new(render_range),
194 sizing_behavior: ListSizingBehavior::default(),
195 }
196}
197
198pub struct VirtualList {
200 id: ElementId,
201 axis: Axis,
202 base: Stateful<Div>,
203 scroll_handle: VirtualListScrollHandle,
204 items_count: usize,
205 item_sizes: Rc<Vec<Size<Pixels>>>,
206 render_items: Box<
207 dyn for<'a> Fn(Range<usize>, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>,
208 >,
209 sizing_behavior: ListSizingBehavior,
210}
211
212impl Styled for VirtualList {
213 fn style(&mut self) -> &mut StyleRefinement {
214 self.base.style()
215 }
216}
217
218impl VirtualList {
219 pub fn track_scroll(mut self, scroll_handle: &VirtualListScrollHandle) -> Self {
220 self.base = self.base.track_scroll(&scroll_handle);
221 self.scroll_handle = scroll_handle.clone();
222 self
223 }
224
225 pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
227 self.sizing_behavior = behavior;
228 self
229 }
230
231 pub(crate) fn with_scroll_handle(mut self, scroll_handle: &VirtualListScrollHandle) -> Self {
235 self.base = div().id(self.id.clone()).size_full();
236 self.scroll_handle = scroll_handle.clone();
237 self
238 }
239
240 fn scroll_to_deferred_item(
241 &self,
242 scroll_offset: Point<Pixels>,
243 items_bounds: &[Bounds<Pixels>],
244 content_bounds: &Bounds<Pixels>,
245 scroll_to_item: DeferredScrollToItem,
246 ) -> Point<Pixels> {
247 let Some(bounds) = items_bounds
248 .get(scroll_to_item.item_index + scroll_to_item.offset)
249 .cloned()
250 else {
251 return scroll_offset;
252 };
253
254 let mut scroll_offset = scroll_offset;
255 match scroll_to_item.strategy {
256 ScrollStrategy::Center => {
257 if self.axis.is_vertical() {
258 scroll_offset.y = content_bounds.top() + content_bounds.size.height.half()
259 - bounds.top()
260 - bounds.size.height.half()
261 } else {
262 scroll_offset.x = content_bounds.left() + content_bounds.size.width.half()
263 - bounds.left()
264 - bounds.size.width.half()
265 }
266 }
267 _ => {
268 if self.axis.is_vertical() {
270 if bounds.top() + scroll_offset.y < content_bounds.top() {
271 scroll_offset.y = content_bounds.top() - bounds.top()
272 } else if bounds.bottom() + scroll_offset.y > content_bounds.bottom() {
273 scroll_offset.y = content_bounds.bottom() - bounds.bottom();
274 }
275 } else {
276 if bounds.left() + scroll_offset.x < content_bounds.left() {
277 scroll_offset.x = content_bounds.left() - bounds.left();
278 } else if bounds.right() + scroll_offset.x > content_bounds.right() {
279 scroll_offset.x = content_bounds.right() - bounds.right();
280 }
281 }
282 }
283 }
284 self.scroll_handle.set_offset(scroll_offset);
285 scroll_offset
286 }
287
288 fn measure_item(
290 &self,
291 list_width: Option<Pixels>,
292 window: &mut Window,
293 cx: &mut App,
294 ) -> Size<Pixels> {
295 if self.items_count == 0 {
296 return Size::default();
297 }
298
299 let item_ix = 0;
300 let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx);
301 let Some(mut item_to_measure) = items.pop() else {
302 return Size::default();
303 };
304 let available_space = size(
305 list_width.map_or(AvailableSpace::MinContent, |width| {
306 AvailableSpace::Definite(width)
307 }),
308 AvailableSpace::MinContent,
309 );
310 item_to_measure.layout_as_root(available_space, window, cx)
311 }
312}
313
314pub struct VirtualListFrameState {
316 items: SmallVec<[AnyElement; 32]>,
318 size_layout: ItemSizeLayout,
319}
320
321#[derive(Default, Clone)]
322pub struct ItemSizeLayout {
323 items_sizes: Rc<Vec<Size<Pixels>>>,
324 content_size: Size<Pixels>,
325 sizes: Vec<Pixels>,
326 origins: Vec<Pixels>,
327 last_layout_bounds: Bounds<Pixels>,
328}
329
330impl IntoElement for VirtualList {
331 type Element = Self;
332
333 fn into_element(self) -> Self::Element {
334 self
335 }
336}
337
338impl Element for VirtualList {
339 type RequestLayoutState = VirtualListFrameState;
340 type PrepaintState = Option<Hitbox>;
341
342 fn id(&self) -> Option<ElementId> {
343 Some(self.id.clone())
344 }
345
346 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
347 None
348 }
349
350 fn request_layout(
351 &mut self,
352 global_id: Option<&GlobalElementId>,
353 inspector_id: Option<&gpui::InspectorElementId>,
354 window: &mut Window,
355 cx: &mut App,
356 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
357 let rem_size = window.rem_size();
358 let font_size = window.text_style().font_size.to_pixels(rem_size);
359 let mut size_layout = ItemSizeLayout::default();
360 let longest_item_size = self.measure_item(None, window, cx);
361
362 let layout_id = self.base.interactivity().request_layout(
363 global_id,
364 inspector_id,
365 window,
366 cx,
367 |style, window, cx| {
368 size_layout = window.with_element_state(
369 global_id.unwrap(),
370 |state: Option<ItemSizeLayout>, _window| {
371 let mut state = state.unwrap_or(ItemSizeLayout::default());
372
373 let gap = style
375 .gap
376 .along(self.axis)
377 .to_pixels(font_size.into(), rem_size);
378
379 if state.items_sizes != self.item_sizes {
380 state.items_sizes = self.item_sizes.clone();
381 state.sizes = self
383 .item_sizes
384 .iter()
385 .enumerate()
386 .map(|(i, size)| {
387 let size = size.along(self.axis);
388 if i + 1 == self.items_count {
389 size
390 } else {
391 size + gap
392 }
393 })
394 .collect::<Vec<_>>();
395
396 state.origins = state
398 .sizes
399 .iter()
400 .scan(px(0.), |cumulative, size| match self.axis {
401 Axis::Horizontal => {
402 let x = *cumulative;
403 *cumulative += *size;
404 Some(x)
405 }
406 Axis::Vertical => {
407 let y = *cumulative;
408 *cumulative += *size;
409 Some(y)
410 }
411 })
412 .collect::<Vec<_>>();
413
414 state.content_size = if self.axis.is_horizontal() {
415 Size {
416 width: px(state
417 .sizes
418 .iter()
419 .map(|size| size.as_f32())
420 .sum::<f32>()),
421 height: longest_item_size.height,
422 }
423 } else {
424 Size {
425 width: longest_item_size.width,
426 height: px(state
427 .sizes
428 .iter()
429 .map(|size| size.as_f32())
430 .sum::<f32>()),
431 }
432 };
433 }
434
435 (state.clone(), state)
436 },
437 );
438
439 let axis = self.axis;
440 let layout_id =
441 match self.sizing_behavior {
442 ListSizingBehavior::Infer => {
443 window.with_text_style(style.text_style().cloned(), |window| {
444 let size_layout = size_layout.clone();
445
446 window.request_measured_layout(style, {
447 move |known_dimensions, available_space, _, _| {
448 let mut size = Size::default();
449 if axis.is_horizontal() {
450 size.width = known_dimensions.width.unwrap_or(
451 match available_space.width {
452 AvailableSpace::Definite(x) => x,
453 AvailableSpace::MinContent
454 | AvailableSpace::MaxContent => {
455 size_layout.content_size.width
456 }
457 },
458 );
459 size.height = known_dimensions.width.unwrap_or(
460 match available_space.height {
461 AvailableSpace::Definite(x) => x,
462 AvailableSpace::MinContent
463 | AvailableSpace::MaxContent => {
464 size_layout.content_size.height
465 }
466 },
467 );
468 } else {
469 size.width = known_dimensions.width.unwrap_or(
470 match available_space.width {
471 AvailableSpace::Definite(x) => x,
472 AvailableSpace::MinContent
473 | AvailableSpace::MaxContent => {
474 size_layout.content_size.width
475 }
476 },
477 );
478 size.height = known_dimensions.height.unwrap_or(
479 match available_space.height {
480 AvailableSpace::Definite(x) => x,
481 AvailableSpace::MinContent
482 | AvailableSpace::MaxContent => {
483 size_layout.content_size.height
484 }
485 },
486 );
487 }
488
489 size
490 }
491 })
492 })
493 }
494 ListSizingBehavior::Auto => window
495 .with_text_style(style.text_style().cloned(), |window| {
496 window.request_layout(style, None, cx)
497 }),
498 };
499
500 layout_id
501 },
502 );
503
504 (
505 layout_id,
506 VirtualListFrameState {
507 items: SmallVec::new(),
508 size_layout,
509 },
510 )
511 }
512
513 fn prepaint(
514 &mut self,
515 global_id: Option<&GlobalElementId>,
516 inspector_id: Option<&gpui::InspectorElementId>,
517 bounds: Bounds<Pixels>,
518 layout: &mut Self::RequestLayoutState,
519 window: &mut Window,
520 cx: &mut App,
521 ) -> Self::PrepaintState {
522 layout.size_layout.last_layout_bounds = bounds;
523
524 let style = self
525 .base
526 .interactivity()
527 .compute_style(global_id, None, window, cx);
528 let border_widths = style.border_widths.to_pixels(window.rem_size());
529 let paddings = style
530 .padding
531 .to_pixels(bounds.size.into(), window.rem_size());
532
533 let item_sizes = &layout.size_layout.sizes;
534 let item_origins = &layout.size_layout.origins;
535
536 let content_bounds = Bounds::from_corners(
537 bounds.origin
538 + point(
539 border_widths.left + paddings.left,
540 border_widths.top + paddings.top,
541 ),
542 bounds.bottom_right()
543 - point(
544 border_widths.right + paddings.right,
545 border_widths.bottom + paddings.bottom,
546 ),
547 );
548
549 let items_bounds = item_origins
551 .iter()
552 .enumerate()
553 .map(|(i, &origin)| {
554 let item_size = item_sizes[i];
555
556 Bounds {
557 origin: match self.axis {
558 Axis::Horizontal => point(content_bounds.left() + origin, px(0.)),
559 Axis::Vertical => point(px(0.), content_bounds.top() + origin),
560 },
561 size: match self.axis {
562 Axis::Horizontal => size(item_size, content_bounds.size.height),
563 Axis::Vertical => size(content_bounds.size.width, item_size),
564 },
565 }
566 })
567 .collect::<Vec<_>>();
568
569 let axis = self.axis;
570
571 let mut scroll_state = self.scroll_handle.state.borrow_mut();
572 scroll_state.axis = axis;
573 scroll_state.items_count = self.items_count;
574
575 let mut scroll_offset = self.scroll_handle.offset();
576 if let Some(scroll_to_item) = scroll_state.deferred_scroll_to_item.take() {
577 scroll_offset = self.scroll_to_deferred_item(
578 scroll_offset,
579 &items_bounds,
580 &content_bounds,
581 scroll_to_item,
582 );
583 }
584
585 scroll_offset = scroll_offset
586 .max(&point(
587 content_bounds.size.width - layout.size_layout.content_size.width,
588 content_bounds.size.height - layout.size_layout.content_size.height,
589 ))
590 .min(&point(px(0.), px(0.)));
591 if scroll_offset != self.scroll_handle.offset() {
592 self.scroll_handle.set_offset(scroll_offset);
593 }
594
595 self.base.interactivity().prepaint(
596 global_id,
597 inspector_id,
598 bounds,
599 layout.size_layout.content_size,
600 window,
601 cx,
602 |_style, _, hitbox, window, cx| {
603 if self.items_count > 0 {
604 let min_scroll_offset = content_bounds.size.along(self.axis)
605 - layout.size_layout.content_size.along(self.axis);
606
607 let is_scrolled = !scroll_offset.along(self.axis).is_zero();
608 if is_scrolled {
609 match self.axis {
610 Axis::Horizontal if scroll_offset.x < min_scroll_offset => {
611 scroll_offset.x = min_scroll_offset;
612 self.scroll_handle.set_offset(scroll_offset);
613 }
614 Axis::Vertical if scroll_offset.y < min_scroll_offset => {
615 scroll_offset.y = min_scroll_offset;
616 self.scroll_handle.set_offset(scroll_offset);
617 }
618 _ => {}
619 }
620 }
621
622 let (first_visible_element_ix, last_visible_element_ix) = match self.axis {
623 Axis::Horizontal => {
624 let mut cumulative_size = px(0.);
625 let mut first_visible_element_ix = 0;
626 for (i, &size) in item_sizes.iter().enumerate() {
627 cumulative_size += size;
628 if cumulative_size > -(scroll_offset.x + paddings.left) {
629 first_visible_element_ix = i;
630 break;
631 }
632 }
633
634 cumulative_size = px(0.);
635 let mut last_visible_element_ix = 0;
636 for (i, &size) in item_sizes.iter().enumerate() {
637 cumulative_size += size;
638 if cumulative_size > (-scroll_offset.x + content_bounds.size.width)
639 {
640 last_visible_element_ix = i + 1;
641 break;
642 }
643 }
644 if last_visible_element_ix == 0 {
645 last_visible_element_ix = self.items_count;
646 } else {
647 last_visible_element_ix += 1;
648 }
649 (first_visible_element_ix, last_visible_element_ix)
650 }
651 Axis::Vertical => {
652 let mut cumulative_size = px(0.);
653 let mut first_visible_element_ix = 0;
654 for (i, &size) in item_sizes.iter().enumerate() {
655 cumulative_size += size;
656 if cumulative_size > -(scroll_offset.y + paddings.top) {
657 first_visible_element_ix = i;
658 break;
659 }
660 }
661
662 cumulative_size = px(0.);
663 let mut last_visible_element_ix = 0;
664 for (i, &size) in item_sizes.iter().enumerate() {
665 cumulative_size += size;
666 if cumulative_size > (-scroll_offset.y + content_bounds.size.height)
667 {
668 last_visible_element_ix = i + 1;
669 break;
670 }
671 }
672 if last_visible_element_ix == 0 {
673 last_visible_element_ix = self.items_count;
674 } else {
675 last_visible_element_ix += 1;
676 }
677 (first_visible_element_ix, last_visible_element_ix)
678 }
679 };
680
681 let visible_range = first_visible_element_ix
682 ..cmp::min(last_visible_element_ix, self.items_count);
683
684 let items = (self.render_items)(visible_range.clone(), window, cx);
685
686 let content_mask = ContentMask { bounds };
687 window.with_content_mask(Some(content_mask), |window| {
688 for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
689 let item_origin = match self.axis {
690 Axis::Horizontal => {
691 content_bounds.origin
692 + point(item_origins[ix] + scroll_offset.x, scroll_offset.y)
693 }
694 Axis::Vertical => {
695 content_bounds.origin
696 + point(scroll_offset.x, item_origins[ix] + scroll_offset.y)
697 }
698 };
699
700 let available_space = match self.axis {
701 Axis::Horizontal => size(
702 AvailableSpace::Definite(item_sizes[ix]),
703 AvailableSpace::Definite(content_bounds.size.height),
704 ),
705 Axis::Vertical => size(
706 AvailableSpace::Definite(content_bounds.size.width),
707 AvailableSpace::Definite(item_sizes[ix]),
708 ),
709 };
710
711 item.layout_as_root(available_space, window, cx);
712 item.prepaint_at(item_origin, window, cx);
713 layout.items.push(item);
714 }
715 });
716 }
717
718 hitbox
719 },
720 )
721 }
722
723 fn paint(
724 &mut self,
725 global_id: Option<&GlobalElementId>,
726 inspector_id: Option<&gpui::InspectorElementId>,
727 bounds: Bounds<Pixels>,
728 layout: &mut Self::RequestLayoutState,
729 hitbox: &mut Self::PrepaintState,
730 window: &mut Window,
731 cx: &mut App,
732 ) {
733 self.base.interactivity().paint(
734 global_id,
735 inspector_id,
736 bounds,
737 hitbox.as_ref(),
738 window,
739 cx,
740 |_, window, cx| {
741 for item in &mut layout.items {
742 item.paint(window, cx);
743 }
744 },
745 )
746 }
747}