Skip to main content

sequoia_view/
virtual_list.rs

1use std::cell::Cell;
2use std::rc::Rc;
3
4use sequoia_core::{Color, Length, Rect};
5use sequoia_input::{Event, EventPhase, Key, Modifiers, PointerEvent};
6use sequoia_layout::{LayoutCtx, LayoutStyle};
7use sequoia_reactive::Signal;
8
9use crate::any::AnyView;
10use crate::runtime::{ElementId, Runtime};
11use crate::scroll_view::ScrollbarVisibility;
12use crate::view::{EventCtx, PaintCtx, View};
13
14const FALLBACK_WINDOW: usize = 64;
15
16const DEFAULT_ITEM_HEIGHT: f32 = 28.0;
17
18const DEFAULT_OVERSCAN: usize = 4;
19
20const DEFAULT_BAR_W: f32 = 8.0;
21
22const BAR_PAD: f32 = 2.0;
23
24const THUMB_MIN: f32 = 24.0;
25
26const DEFAULT_STEP: f32 = 40.0;
27
28struct ItemSlot {
29    child: AnyView,
30    height: f32,
31}
32
33impl View for ItemSlot {
34    fn body(&self) -> impl View + 'static {
35        crate::views::Unit
36    }
37
38    fn children_override(&self) -> Option<Vec<AnyView>> {
39        Some(vec![self.child.clone()])
40    }
41
42    fn layout_style(&self) -> LayoutStyle {
43        LayoutStyle {
44            width: Length::Fill(1.0),
45            height: Length::Px(self.height),
46            ..LayoutStyle::default()
47        }
48    }
49}
50
51pub struct VirtualList<T> {
52    items: Rc<dyn Fn() -> Vec<T>>,
53    key: Rc<dyn Fn(&T) -> u64>,
54    item: Rc<dyn Fn(&T) -> AnyView>,
55
56    item_idx: Option<Rc<dyn Fn(usize, &T) -> AnyView>>,
57    item_height: f32,
58    overscan: usize,
59    offset: Signal<f32>,
60    viewport_h: Rc<Cell<f32>>,
61    viewport_hint: Signal<u64>,
62    window_start: Rc<Cell<usize>>,
63    style: LayoutStyle,
64    scrollbar: ScrollbarVisibility,
65    bar_width: f32,
66    track_color: Option<Color>,
67    thumb_color: Option<Color>,
68    step: f32,
69    on_scroll: Option<Rc<dyn Fn(f32)>>,
70
71    drag: Rc<Cell<Option<f32>>>,
72}
73
74impl<T: 'static> Clone for VirtualList<T> {
75    fn clone(&self) -> Self {
76        Self {
77            items: Rc::clone(&self.items),
78            key: Rc::clone(&self.key),
79            item: Rc::clone(&self.item),
80            item_idx: self.item_idx.clone(),
81            item_height: self.item_height,
82            overscan: self.overscan,
83            offset: self.offset.clone(),
84            viewport_h: Rc::clone(&self.viewport_h),
85            viewport_hint: self.viewport_hint.clone(),
86            window_start: Rc::clone(&self.window_start),
87            style: self.style,
88            scrollbar: self.scrollbar,
89            bar_width: self.bar_width,
90            track_color: self.track_color,
91            thumb_color: self.thumb_color,
92            step: self.step,
93            on_scroll: self.on_scroll.clone(),
94            drag: Rc::clone(&self.drag),
95        }
96    }
97}
98
99impl<T: 'static> VirtualList<T> {
100    pub fn new<F: Fn() -> Vec<T> + 'static>(items: F) -> Self {
101        Self {
102            items: Rc::new(items),
103            key: Rc::new(|_| 0),
104            item: Rc::new(|_| AnyView::new(crate::views::Unit)),
105            item_idx: None,
106            item_height: DEFAULT_ITEM_HEIGHT,
107            overscan: DEFAULT_OVERSCAN,
108            offset: Signal::new(0.0),
109            viewport_h: Rc::new(Cell::new(0.0)),
110            viewport_hint: Signal::new(0),
111            window_start: Rc::new(Cell::new(0)),
112            scrollbar: ScrollbarVisibility::Auto,
113            bar_width: DEFAULT_BAR_W,
114            track_color: None,
115            thumb_color: None,
116            step: DEFAULT_STEP,
117            on_scroll: None,
118            drag: Rc::new(Cell::new(None)),
119            style: LayoutStyle {
120                width: Length::Fill(1.0),
121                height: Length::Fill(1.0),
122                ..LayoutStyle::default()
123            },
124        }
125    }
126
127    pub fn key<K: Fn(&T) -> u64 + 'static>(mut self, key: K) -> Self {
128        self.key = Rc::new(key);
129        self
130    }
131
132    pub fn item<I, F>(mut self, item: F) -> Self
133    where
134        I: View + 'static,
135        F: Fn(&T) -> I + 'static,
136    {
137        self.item = Rc::new(move |t: &T| AnyView::new(item(t)));
138        self
139    }
140
141    pub fn item_indexed<I, F>(mut self, item: F) -> Self
142    where
143        I: View + 'static,
144        F: Fn(usize, &T) -> I + 'static,
145    {
146        self.item_idx = Some(Rc::new(move |i: usize, t: &T| AnyView::new(item(i, t))));
147        self
148    }
149
150    pub fn item_height(mut self, h: f32) -> Self {
151        self.item_height = h.max(1.0);
152        self
153    }
154
155    pub fn overscan(mut self, n: usize) -> Self {
156        self.overscan = n;
157        self
158    }
159
160    pub fn scrollbar_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
161        self.scrollbar = visibility;
162        self
163    }
164
165    pub fn show_scrollbar(mut self, show: bool) -> Self {
166        self.scrollbar = if show {
167            ScrollbarVisibility::Always
168        } else {
169            ScrollbarVisibility::Never
170        };
171        self
172    }
173
174    pub fn scrollbar_width(mut self, width: f32) -> Self {
175        self.bar_width = width;
176        self
177    }
178
179    pub fn scrollbar_track(mut self, color: Color) -> Self {
180        self.track_color = Some(color);
181        self
182    }
183
184    pub fn scrollbar_thumb(mut self, color: Color) -> Self {
185        self.thumb_color = Some(color);
186        self
187    }
188
189    pub fn on_scroll(mut self, handler: impl Fn(f32) + 'static) -> Self {
190        self.on_scroll = Some(Rc::new(handler));
191        self
192    }
193
194    pub fn scroll_step(mut self, step: f32) -> Self {
195        self.step = step.max(0.0);
196        self
197    }
198
199    pub fn width(mut self, width: impl Into<Length>) -> Self {
200        self.style = self.style.width(width.into());
201        self
202    }
203
204    pub fn height(mut self, height: impl Into<Length>) -> Self {
205        self.style = self.style.height(height.into());
206        self
207    }
208
209    pub fn min_width(mut self, v: f32) -> Self {
210        self.style = self.style.min_width(v);
211        self
212    }
213
214    pub fn max_width(mut self, v: f32) -> Self {
215        self.style = self.style.max_width(v);
216        self
217    }
218
219    pub fn min_height(mut self, v: f32) -> Self {
220        self.style = self.style.min_height(v);
221        self
222    }
223
224    pub fn max_height(mut self, v: f32) -> Self {
225        self.style = self.style.max_height(v);
226        self
227    }
228
229    pub fn flex(mut self, weight: f32) -> Self {
230        self.style = self.style.flex(weight);
231        self
232    }
233
234    pub fn expand(self) -> Self {
235        self.flex(1.0)
236    }
237
238    pub fn fill_width(mut self) -> Self {
239        self.style = self.style.width(Length::Fill(1.0));
240        self
241    }
242
243    pub fn fill_height(mut self) -> Self {
244        self.style = self.style.height(Length::Fill(1.0));
245        self
246    }
247
248    pub fn fill_size(self) -> Self {
249        self.fill_width().fill_height()
250    }
251
252    pub fn offset(&self) -> f32 {
253        self.offset.get_untracked()
254    }
255
256    pub fn scroll_to(&self, y: f32) {
257        self.apply(y.clamp(0.0, self.max_offset()));
258    }
259
260    fn apply(&self, next: f32) {
261        let prev = self.offset.get_untracked();
262        if next != prev {
263            self.offset.set(next);
264            if let Some(cb) = &self.on_scroll {
265                cb(next);
266            }
267        }
268    }
269
270    pub fn scroll_by(&self, dy: f32) {
271        self.scroll_to(self.offset.get_untracked() + dy);
272    }
273
274    pub fn scroll_to_top(&self) {
275        self.scroll_to(0.0);
276    }
277
278    pub fn scroll_to_bottom(&self) {
279        self.scroll_to(self.max_offset());
280    }
281
282    pub fn page_up(&self) {
283        self.scroll_by(-self.viewport_h.get());
284    }
285
286    pub fn page_down(&self) {
287        self.scroll_by(self.viewport_h.get());
288    }
289
290    fn total_height(&self) -> f32 {
291        (self.items)().len() as f32 * self.item_height
292    }
293
294    pub fn max_offset(&self) -> f32 {
295        (self.total_height() - self.viewport_h.get()).max(0.0)
296    }
297
298    fn first_visible(&self, off: f32) -> usize {
299        if self.item_height > 0.0 {
300            (off / self.item_height).floor().max(0.0) as usize
301        } else {
302            0
303        }
304    }
305
306    fn window_bounds(&self, item_count: usize, off: f32) -> (usize, usize) {
307        let vh = self.viewport_h.get();
308        let first = self.first_visible(off);
309        let start = first.saturating_sub(self.overscan).min(item_count);
310        if vh <= 0.0 {
311            let end = (start + FALLBACK_WINDOW).min(item_count);
312            return (start, end);
313        }
314        let capacity = (vh / self.item_height).ceil() as usize + 1;
315        let end = (first + capacity + self.overscan).min(item_count);
316        (start, end.min(item_count).max(start))
317    }
318
319    fn shows_bar(&self, viewport: Rect) -> bool {
320        if self.bar_width <= 0.0 {
321            return false;
322        }
323        match self.scrollbar {
324            ScrollbarVisibility::Never => false,
325            ScrollbarVisibility::Always => true,
326            ScrollbarVisibility::Auto => self.total_height() > viewport.size.height,
327
328            ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay => {
329                self.total_height() > viewport.size.height
330            }
331        }
332    }
333
334    fn track_rect(&self, viewport: Rect) -> Option<Rect> {
335        if !self.shows_bar(viewport) {
336            return None;
337        }
338        let track = Rect::from_xywh(
339            viewport.right() - self.bar_width - BAR_PAD,
340            viewport.top() + BAR_PAD,
341            self.bar_width,
342            (viewport.size.height - BAR_PAD * 2.0).max(0.0),
343        );
344        if track.is_empty() { None } else { Some(track) }
345    }
346
347    fn thumb_rect(&self, track: Rect) -> Rect {
348        let content = self.total_height();
349        let vh = self.viewport_h.get();
350        let max_off = (content - vh).max(0.0);
351        if max_off <= 0.0 || content <= 0.0 {
352            return track;
353        }
354        let thumb_h = (track.size.height * vh / content)
355            .max(THUMB_MIN)
356            .min(track.size.height);
357        let travel = (track.size.height - thumb_h).max(0.0);
358        let t = (self.offset.get_untracked() / max_off).clamp(0.0, 1.0);
359        Rect::from_xywh(
360            track.left(),
361            track.top() + t * travel,
362            track.size.width,
363            thumb_h,
364        )
365    }
366
367    fn offset_from_drag(&self, track: Rect, thumb_h: f32, y: f32, grab: f32) -> Option<f32> {
368        let travel = (track.size.height - thumb_h).max(0.0);
369        let max_off = self.max_offset();
370        if travel <= 0.0 || max_off <= 0.0 {
371            return None;
372        }
373        let top = (y - grab - track.top()).clamp(0.0, travel);
374        Some(top / travel * max_off)
375    }
376
377    fn on_wheel(&self, delta_y: f32, ctx: &mut EventCtx) {
378        let next = (self.offset.get_untracked() - delta_y).clamp(0.0, self.max_offset());
379        if next != self.offset.get_untracked() {
380            self.apply(next);
381            ctx.request_frame();
382            ctx.handled();
383        }
384    }
385
386    fn on_key(&self, key: Key, modifiers: Modifiers, ctx: &mut EventCtx) {
387        let o = self.offset.get_untracked();
388        let max = self.max_offset();
389        let vh = self.viewport_h.get().max(0.0);
390        let step = if self.step.is_finite() {
391            self.step.max(0.0)
392        } else {
393            0.0
394        };
395        let next = match (key, modifiers) {
396            (Key::ARROW_UP, Modifiers::NONE) => o - step,
397            (Key::ARROW_DOWN, Modifiers::NONE) => o + step,
398            (Key::PAGE_UP, Modifiers::NONE) => o - vh,
399            (Key::PAGE_DOWN, Modifiers::NONE) => o + vh,
400            (Key::HOME, Modifiers::NONE) => 0.0,
401            (Key::END, Modifiers::NONE) => max,
402            _ => return,
403        };
404        let next = next.clamp(0.0, max);
405        if next != o {
406            self.apply(next);
407            ctx.request_frame();
408            ctx.handled();
409        }
410    }
411}
412
413impl<T: 'static> View for VirtualList<T> {
414    fn reuse_state(&self, old: &Self) {
415        self.offset.set(old.offset.get_untracked());
416        self.viewport_h.set(old.viewport_h.get());
417        self.viewport_hint.set(old.viewport_hint.get_untracked());
418        self.window_start.set(old.window_start.get());
419    }
420
421    fn body(&self) -> impl View + 'static {
422        crate::views::Unit
423    }
424
425    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
426        let items = (self.items)();
427        let off = self.offset.get().clamp(0.0, self.max_offset());
428        let _hint = self.viewport_hint.get();
429        let (start, end) = self.window_bounds(items.len(), off);
430        self.window_start.set(start);
431        let slot_h = self.item_height;
432
433        Some(
434            items[start..end]
435                .iter()
436                .enumerate()
437                .map(|(i, it)| {
438                    let k = (self.key)(it);
439                    let v = match &self.item_idx {
440                        Some(f) => f(start + i, it),
441                        None => (self.item)(it),
442                    };
443                    (
444                        k,
445                        AnyView::new(ItemSlot {
446                            child: v,
447                            height: slot_h,
448                        }),
449                    )
450                })
451                .collect(),
452        )
453    }
454
455    fn layout_style(&self) -> LayoutStyle {
456        self.style
457    }
458
459    fn contain_size(&self) -> bool {
460        true
461    }
462
463    fn arrange_override(
464        &self,
465        rt: &Runtime,
466        el: ElementId,
467        rect: Rect,
468        ctx: &mut LayoutCtx,
469    ) -> bool {
470        self.viewport_h.set(rect.size.height);
471        let hint = (rect.size.height * 64.0) as u64;
472        if self.viewport_hint.get_untracked() != hint {
473            self.viewport_hint.set(hint);
474        }
475
476        let off = self.offset.get_untracked().clamp(0.0, self.max_offset());
477
478        let start = self.window_start.get();
479        let children = rt.children(el);
480        for (i, &child) in children.iter().enumerate() {
481            let index = start + i;
482            let y = rect.top() + index as f32 * self.item_height - off;
483            let child_rect = Rect::from_xywh(rect.left(), y, rect.size.width, self.item_height);
484            rt.arrange_node(child, child_rect, ctx);
485        }
486        true
487    }
488
489    fn clip_self(&self) -> bool {
490        true
491    }
492
493    fn focusable(&self) -> bool {
494        true
495    }
496
497    fn captures_pointer(&self) -> bool {
498        false
499    }
500
501    fn captures_pointer_ctx(&self, _rt: &Runtime, _el: ElementId) -> bool {
502        self.drag.get().is_some()
503    }
504
505    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
506        match event {
507            Event::Pointer(PointerEvent::Wheel { delta, .. }) => {
508                if matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
509                    self.on_wheel(delta.y, ctx);
510                }
511            }
512            Event::Pointer(PointerEvent::Down { pos, .. }) => {
513                if !matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
514                    return;
515                }
516                if self.drag.get().is_some() {
517                    return;
518                }
519                let Some(track) = self.track_rect(ctx.rect) else {
520                    return;
521                };
522                let thumb = self.thumb_rect(track);
523
524                if thumb.contains(*pos) {
525                    self.drag.set(Some(pos.y - thumb.top()));
526                    ctx.request_frame();
527                    ctx.handled();
528                    return;
529                }
530
531                if track.contains(*pos) {
532                    let travel = (track.size.height - thumb.size.height).max(0.0);
533                    if travel > 0.0 {
534                        let top =
535                            (pos.y - thumb.size.height * 0.5 - track.top()).clamp(0.0, travel);
536                        let max_off = self.max_offset();
537                        if max_off > 0.0 {
538                            self.apply(top / travel * max_off);
539                        }
540                    }
541                    self.drag.set(Some(thumb.size.height * 0.5));
542                    ctx.request_frame();
543                    ctx.handled();
544                }
545            }
546            Event::Pointer(PointerEvent::Move { pos, .. }) => {
547                if ctx.phase != EventPhase::Target {
548                    return;
549                }
550                if let Some(grab) = self.drag.get() {
551                    let Some(track) = self.track_rect(ctx.rect) else {
552                        return;
553                    };
554                    let thumb_h = self.thumb_rect(track).size.height;
555                    if let Some(next) = self.offset_from_drag(track, thumb_h, pos.y, grab) {
556                        if next != self.offset.get_untracked() {
557                            self.apply(next);
558                            ctx.request_frame();
559                        }
560                        ctx.handled();
561                    }
562                }
563            }
564            Event::Pointer(PointerEvent::Up { .. }) | Event::Pointer(PointerEvent::Cancel) => {
565                if ctx.phase != EventPhase::Target {
566                    return;
567                }
568                if self.drag.take().is_some() {
569                    ctx.request_frame();
570                    ctx.handled();
571                }
572            }
573            Event::Key(key) => {
574                if matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
575                    self.on_key(key.key, key.modifiers, ctx);
576                }
577            }
578            Event::Ime(_) => {}
579            Event::FilesHovered { .. } | Event::FilesDropped { .. } | Event::FilesCancelled => {}
580
581            _ => {}
582        }
583    }
584
585    fn paint_foreground(&self, ctx: &mut PaintCtx) {
586        let viewport = ctx.rect;
587        let Some(track) = self.track_rect(viewport) else {
588            return;
589        };
590        let thumb = self.thumb_rect(track);
591        let theme = ctx.theme();
592        let track_color = self.track_color.unwrap_or(theme.border);
593        let thumb_color = self.thumb_color.unwrap_or(theme.text_secondary);
594        let radius = self.bar_width * 0.375;
595        ctx.quad(track, track_color, radius);
596        ctx.quad(thumb, thumb_color, radius);
597    }
598}