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}