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