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