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