Skip to main content

sequoia_view/
scroll_view.rs

1use std::rc::Rc;
2
3use sequoia_core::{Color, Constraints, Length, Point, Rect, Size, Vec2};
4use sequoia_input::{Event, EventPhase, Key, Modifiers, PointerEvent};
5use sequoia_layout::{LayoutCtx, LayoutStyle};
6
7use crate::any::AnyView;
8use crate::runtime::{ElementId, Runtime};
9use crate::view::{EventCtx, PaintCtx, View};
10
11const DEFAULT_SCROLLBAR_W: f32 = 8.0;
12
13const SCROLLBAR_PAD: f32 = 2.0;
14
15const THUMB_MIN: f32 = 24.0;
16
17const DEFAULT_STEP: f32 = 40.0;
18
19const HOVER_EXTRA: f32 = 4.0;
20
21const TOUCH_PAN_THRESHOLD_SQ: f32 = 64.0;
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum ScrollAxis {
26    #[default]
27    Vertical,
28
29    Horizontal,
30
31    Both,
32}
33
34impl ScrollAxis {
35    pub fn scrolls_x(self) -> bool {
36        matches!(self, Self::Horizontal | Self::Both)
37    }
38
39    pub fn scrolls_y(self) -> bool {
40        matches!(self, Self::Vertical | Self::Both)
41    }
42}
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum ScrollbarVisibility {
47    #[default]
48    Auto,
49
50    Always,
51
52    Never,
53
54    OnHover,
55
56    Overlay,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq)]
60enum Drag {
61    V {
62        grab: f32,
63    },
64
65    H {
66        grab: f32,
67    },
68
69    Touch {
70        id: u64,
71        start: Point,
72        last: Point,
73        moved: bool,
74    },
75}
76
77struct Bars {
78    v_track: Option<Rect>,
79    v_thumb: Option<Rect>,
80    h_track: Option<Rect>,
81    h_thumb: Option<Rect>,
82}
83
84#[derive(Clone, Copy, Default)]
85struct ScrollState {
86    offset: Vec2,
87    content: Size,
88    viewport: Size,
89    drag: Option<Drag>,
90    arranged: Vec2,
91
92    arranged_epoch: u64,
93    frame: Rect,
94
95    bar_hover: bool,
96}
97
98#[derive(Clone)]
99pub struct ScrollView {
100    child: AnyView,
101    axis: ScrollAxis,
102    scrollbar: ScrollbarVisibility,
103    bar_width: f32,
104    track_color: Option<Color>,
105    thumb_color: Option<Color>,
106    step: f32,
107    on_scroll: Option<Rc<dyn Fn(Vec2)>>,
108    style: LayoutStyle,
109}
110
111impl ScrollView {
112    pub fn new(child: impl View + 'static) -> Self {
113        Self {
114            child: AnyView::new(child),
115            axis: ScrollAxis::Vertical,
116            scrollbar: ScrollbarVisibility::Auto,
117            bar_width: DEFAULT_SCROLLBAR_W,
118            track_color: None,
119            thumb_color: None,
120            step: DEFAULT_STEP,
121            on_scroll: None,
122            style: LayoutStyle {
123                width: Length::Fill(1.0),
124                height: Length::Fill(1.0),
125                ..LayoutStyle::default()
126            },
127        }
128    }
129
130    pub fn with_child(&self, child: impl View + 'static) -> Self {
131        let mut out = self.clone();
132        out.child = AnyView::new(child);
133        out
134    }
135
136    pub fn axis(mut self, axis: ScrollAxis) -> Self {
137        self.axis = axis;
138        self
139    }
140
141    pub fn vertical(mut self) -> Self {
142        self.axis = ScrollAxis::Vertical;
143        self
144    }
145
146    pub fn horizontal(mut self) -> Self {
147        self.axis = ScrollAxis::Horizontal;
148        self
149    }
150
151    pub fn both(mut self) -> Self {
152        self.axis = ScrollAxis::Both;
153        self
154    }
155
156    pub fn scrollbar_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
157        self.scrollbar = visibility;
158        self
159    }
160
161    pub fn show_scrollbar(mut self, show: bool) -> Self {
162        self.scrollbar = if show {
163            ScrollbarVisibility::Always
164        } else {
165            ScrollbarVisibility::Never
166        };
167        self
168    }
169
170    pub fn scrollbar_width(mut self, width: f32) -> Self {
171        self.bar_width = width;
172        self
173    }
174
175    pub fn scrollbar_track(mut self, color: Color) -> Self {
176        self.track_color = Some(color);
177        self
178    }
179
180    pub fn scrollbar_thumb(mut self, color: Color) -> Self {
181        self.thumb_color = Some(color);
182        self
183    }
184
185    pub fn scroll_step(mut self, step: f32) -> Self {
186        self.step = step.max(0.0);
187        self
188    }
189
190    pub fn on_scroll(mut self, handler: impl Fn(Vec2) + 'static) -> Self {
191        self.on_scroll = Some(Rc::new(handler));
192        self
193    }
194
195    pub fn scroll_axis(&self) -> ScrollAxis {
196        self.axis
197    }
198
199    pub fn fill_width(mut self) -> Self {
200        self.style = self.style.width(Length::Fill(1.0));
201        self
202    }
203
204    pub fn fill_height(mut self) -> Self {
205        self.style = self.style.height(Length::Fill(1.0));
206        self
207    }
208
209    pub fn fill_size(self) -> Self {
210        self.fill_width().fill_height()
211    }
212
213    fn load(rt: &Runtime, el: ElementId) -> ScrollState {
214        rt.read_state::<ScrollState, ScrollState>(el, |s| *s)
215    }
216
217    pub fn offset(&self, rt: &Runtime, el: ElementId) -> f32 {
218        Self::load(rt, el).offset.y
219    }
220
221    pub fn offset_x(&self, rt: &Runtime, el: ElementId) -> f32 {
222        Self::load(rt, el).offset.x
223    }
224
225    pub fn offset_y(&self, rt: &Runtime, el: ElementId) -> f32 {
226        Self::load(rt, el).offset.y
227    }
228
229    pub fn offset_vec(&self, rt: &Runtime, el: ElementId) -> Vec2 {
230        Self::load(rt, el).offset
231    }
232
233    pub fn content_height(&self, rt: &Runtime, el: ElementId) -> f32 {
234        Self::load(rt, el).content.height
235    }
236
237    pub fn content_width(&self, rt: &Runtime, el: ElementId) -> f32 {
238        Self::load(rt, el).content.width
239    }
240
241    pub fn content_size(&self, rt: &Runtime, el: ElementId) -> Size {
242        Self::load(rt, el).content
243    }
244
245    pub fn viewport_size(&self, rt: &Runtime, el: ElementId) -> Size {
246        Self::load(rt, el).viewport
247    }
248
249    pub fn viewport_width(&self, rt: &Runtime, el: ElementId) -> f32 {
250        Self::load(rt, el).viewport.width
251    }
252
253    pub fn viewport_height(&self, rt: &Runtime, el: ElementId) -> f32 {
254        Self::load(rt, el).viewport.height
255    }
256
257    pub fn max_offset(&self, rt: &Runtime, el: ElementId) -> f32 {
258        let st = Self::load(rt, el);
259        (st.content.height - st.viewport.height).max(0.0)
260    }
261
262    pub fn max_offset_x(&self, rt: &Runtime, el: ElementId) -> f32 {
263        let st = Self::load(rt, el);
264        (st.content.width - st.viewport.width).max(0.0)
265    }
266
267    pub fn max_offset_y(&self, rt: &Runtime, el: ElementId) -> f32 {
268        self.max_offset(rt, el)
269    }
270
271    pub fn max_offset_vec(&self, rt: &Runtime, el: ElementId) -> Vec2 {
272        Vec2::new(self.max_offset_x(rt, el), self.max_offset(rt, el))
273    }
274
275    pub fn can_scroll_x(&self, rt: &Runtime, el: ElementId) -> bool {
276        self.axis.scrolls_x() && self.max_offset_x(rt, el) > 0.0
277    }
278
279    pub fn can_scroll_y(&self, rt: &Runtime, el: ElementId) -> bool {
280        self.axis.scrolls_y() && self.max_offset(rt, el) > 0.0
281    }
282
283    pub fn scroll_fraction(&self, rt: &Runtime, el: ElementId) -> Vec2 {
284        Vec2::new(
285            self.scroll_fraction_x(rt, el),
286            self.scroll_fraction_y(rt, el),
287        )
288    }
289
290    pub fn scroll_fraction_x(&self, rt: &Runtime, el: ElementId) -> f32 {
291        let max = self.max_offset_x(rt, el);
292        if max > 0.0 {
293            (Self::load(rt, el).offset.x / max).clamp(0.0, 1.0)
294        } else {
295            0.0
296        }
297    }
298
299    pub fn scroll_fraction_y(&self, rt: &Runtime, el: ElementId) -> f32 {
300        let max = self.max_offset(rt, el);
301        if max > 0.0 {
302            (Self::load(rt, el).offset.y / max).clamp(0.0, 1.0)
303        } else {
304            0.0
305        }
306    }
307
308    pub fn scroll_to(&self, rt: &Runtime, el: ElementId, offset: Vec2) {
309        let max = self.max_offset_vec(rt, el);
310        let o = Self::load(rt, el).offset;
311        let next = Vec2::new(
312            if self.axis.scrolls_x() {
313                offset.x.clamp(0.0, max.x)
314            } else {
315                o.x.clamp(0.0, max.x)
316            },
317            if self.axis.scrolls_y() {
318                offset.y.clamp(0.0, max.y)
319            } else {
320                o.y.clamp(0.0, max.y)
321            },
322        );
323        self.apply(rt, el, next);
324    }
325
326    pub fn scroll_to_xy(&self, rt: &Runtime, el: ElementId, x: f32, y: f32) {
327        self.scroll_to(rt, el, Vec2::new(x, y));
328    }
329
330    pub fn scroll_to_x(&self, rt: &Runtime, el: ElementId, x: f32) {
331        let o = Self::load(rt, el).offset;
332        self.scroll_to(rt, el, Vec2::new(x, o.y));
333    }
334
335    pub fn scroll_to_y(&self, rt: &Runtime, el: ElementId, y: f32) {
336        let o = Self::load(rt, el).offset;
337        self.scroll_to(rt, el, Vec2::new(o.x, y));
338    }
339
340    pub fn scroll_by(&self, rt: &Runtime, el: ElementId, delta: Vec2) {
341        let o = Self::load(rt, el).offset;
342        self.scroll_to(rt, el, Vec2::new(o.x + delta.x, o.y + delta.y));
343    }
344
345    pub fn scroll_to_top(&self, rt: &Runtime, el: ElementId) {
346        self.scroll_to_y(rt, el, 0.0);
347    }
348
349    pub fn scroll_to_bottom(&self, rt: &Runtime, el: ElementId) {
350        self.scroll_to_y(rt, el, self.max_offset(rt, el));
351    }
352
353    pub fn scroll_into_view(&self, rt: &Runtime, el: ElementId, rect: Rect) {
354        let st = Self::load(rt, el);
355        let mut next = st.offset;
356        if self.axis.scrolls_y() {
357            let vh = st.viewport.height;
358            if rect.top() < next.y {
359                next.y = rect.top();
360            } else if rect.bottom() > next.y + vh {
361                next.y = rect.bottom() - vh;
362            }
363        }
364        if self.axis.scrolls_x() {
365            let vw = st.viewport.width;
366            if rect.left() < next.x {
367                next.x = rect.left();
368            } else if rect.right() > next.x + vw {
369                next.x = rect.right() - vw;
370            }
371        }
372        self.scroll_to(rt, el, next);
373    }
374
375    pub fn scroll_element_into_view(&self, rt: &Runtime, el: ElementId, child: ElementId) {
376        let Some(r) = rt.rect_of(child) else {
377            return;
378        };
379        let (frame, off) = rt.read_state(el, |s: &ScrollState| (s.frame, s.offset));
380        let content = Rect::from_xywh(
381            r.left() - frame.left() + off.x,
382            r.top() - frame.top() + off.y,
383            r.size.width,
384            r.size.height,
385        );
386        self.scroll_into_view(rt, el, content);
387    }
388
389    pub fn scroll_to_left(&self, rt: &Runtime, el: ElementId) {
390        self.scroll_to_x(rt, el, 0.0);
391    }
392
393    pub fn scroll_to_right(&self, rt: &Runtime, el: ElementId) {
394        self.scroll_to_x(rt, el, self.max_offset_x(rt, el));
395    }
396
397    pub fn page_up(&self, rt: &Runtime, el: ElementId) {
398        let st = Self::load(rt, el);
399        self.scroll_to(
400            rt,
401            el,
402            Vec2::new(st.offset.x, st.offset.y - st.viewport.height),
403        );
404    }
405
406    pub fn page_down(&self, rt: &Runtime, el: ElementId) {
407        let st = Self::load(rt, el);
408        self.scroll_to(
409            rt,
410            el,
411            Vec2::new(st.offset.x, st.offset.y + st.viewport.height),
412        );
413    }
414
415    pub fn page_left(&self, rt: &Runtime, el: ElementId) {
416        let st = Self::load(rt, el);
417        self.scroll_to(
418            rt,
419            el,
420            Vec2::new(st.offset.x - st.viewport.width, st.offset.y),
421        );
422    }
423
424    pub fn page_right(&self, rt: &Runtime, el: ElementId) {
425        let st = Self::load(rt, el);
426        self.scroll_to(
427            rt,
428            el,
429            Vec2::new(st.offset.x + st.viewport.width, st.offset.y),
430        );
431    }
432
433    pub fn ensure_visible(&self, rt: &Runtime, el: ElementId, rect: Rect) -> bool {
434        let st = Self::load(rt, el);
435        let vp = st.viewport;
436        let o = st.offset;
437        let max = self.max_offset_vec(rt, el);
438        let mut next = o;
439        if self.axis.scrolls_x() && vp.width > 0.0 {
440            if rect.size.width >= vp.width || rect.left() < o.x {
441                next.x = rect.left();
442            } else if rect.right() > o.x + vp.width {
443                next.x = rect.right() - vp.width;
444            }
445            next.x = next.x.clamp(0.0, max.x);
446        }
447        if self.axis.scrolls_y() && vp.height > 0.0 {
448            if rect.size.height >= vp.height || rect.top() < o.y {
449                next.y = rect.top();
450            } else if rect.bottom() > o.y + vp.height {
451                next.y = rect.bottom() - vp.height;
452            }
453            next.y = next.y.clamp(0.0, max.y);
454        }
455        if next != o {
456            self.apply(rt, el, next);
457            true
458        } else {
459            false
460        }
461    }
462
463    fn apply(&self, rt: &Runtime, el: ElementId, next: Vec2) {
464        let prev = rt.with_state::<ScrollState, Vec2>(el, |s| {
465            let prev = s.offset;
466            s.offset = next;
467            prev
468        });
469        if next != prev {
470            rt.invalidate_layout();
471            if let Some(cb) = &self.on_scroll {
472                cb(next);
473            }
474        }
475    }
476
477    fn shows_v(&self, st: &ScrollState) -> bool {
478        if !self.axis.scrolls_y() || self.bar_width <= 0.0 {
479            return false;
480        }
481        match self.scrollbar {
482            ScrollbarVisibility::Never => false,
483            ScrollbarVisibility::Always => true,
484            ScrollbarVisibility::Auto => st.content.height > st.viewport.height,
485            ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay => {
486                (st.bar_hover || st.drag.is_some()) && st.content.height > st.viewport.height
487            }
488        }
489    }
490
491    fn shows_h(&self, st: &ScrollState) -> bool {
492        if !self.axis.scrolls_x() || self.bar_width <= 0.0 {
493            return false;
494        }
495        match self.scrollbar {
496            ScrollbarVisibility::Never => false,
497            ScrollbarVisibility::Always => true,
498            ScrollbarVisibility::Auto => st.content.width > st.viewport.width,
499            ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay => {
500                (st.bar_hover || st.drag.is_some()) && st.content.width > st.viewport.width
501            }
502        }
503    }
504
505    fn gutter(&self) -> f32 {
506        (self.bar_width + SCROLLBAR_PAD * 2.0 + HOVER_EXTRA).max(0.0)
507    }
508
509    fn in_gutter(&self, rect: Rect, pos: Point) -> bool {
510        let g = self.gutter();
511        if g <= 0.0 {
512            return false;
513        }
514        let v = self.axis.scrolls_y()
515            && pos.x >= rect.right() - g
516            && pos.x <= rect.right()
517            && pos.y >= rect.top()
518            && pos.y <= rect.bottom();
519        let h = self.axis.scrolls_x()
520            && pos.y >= rect.bottom() - g
521            && pos.y <= rect.bottom()
522            && pos.x >= rect.left()
523            && pos.x <= rect.right();
524        v || h
525    }
526
527    fn track_hover(&self, rt: &Runtime, el: ElementId, rect: Rect, pos: Point) -> bool {
528        if !matches!(
529            self.scrollbar,
530            ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay
531        ) {
532            return false;
533        }
534        let hov = self.in_gutter(rect, pos);
535        rt.with_state(el, |s: &mut ScrollState| {
536            let old = s.bar_hover;
537            s.bar_hover = hov;
538            old != hov
539        })
540    }
541
542    fn v_bar(&self, st: &ScrollState, viewport: Rect) -> Option<(Rect, Rect)> {
543        if !self.shows_v(st) {
544            return None;
545        }
546        let w = self.bar_width;
547        let track = Rect::from_xywh(
548            viewport.right() - w - SCROLLBAR_PAD,
549            viewport.top() + SCROLLBAR_PAD,
550            w,
551            (viewport.size.height - SCROLLBAR_PAD * 2.0).max(0.0),
552        );
553        if track.is_empty() {
554            return None;
555        }
556        Some((track, self.v_thumb(st, track)))
557    }
558
559    fn v_thumb(&self, st: &ScrollState, track: Rect) -> Rect {
560        let content = st.content.height;
561        let vh = st.viewport.height;
562        let max_off = (content - vh).max(0.0);
563        if max_off <= 0.0 || content <= 0.0 {
564            return track;
565        }
566        let thumb_h = (track.size.height * vh / content)
567            .max(THUMB_MIN)
568            .min(track.size.height);
569        let travel = (track.size.height - thumb_h).max(0.0);
570        let t = (st.offset.y / max_off).clamp(0.0, 1.0);
571        Rect::from_xywh(
572            track.left(),
573            track.top() + t * travel,
574            track.size.width,
575            thumb_h,
576        )
577    }
578
579    fn h_bar(&self, st: &ScrollState, viewport: Rect) -> Option<(Rect, Rect)> {
580        if !self.shows_h(st) {
581            return None;
582        }
583        let h = self.bar_width;
584        let track = Rect::from_xywh(
585            viewport.left() + SCROLLBAR_PAD,
586            viewport.bottom() - h - SCROLLBAR_PAD,
587            (viewport.size.width - SCROLLBAR_PAD * 2.0).max(0.0),
588            h,
589        );
590        if track.is_empty() {
591            return None;
592        }
593        Some((track, self.h_thumb(st, track)))
594    }
595
596    fn h_thumb(&self, st: &ScrollState, track: Rect) -> Rect {
597        let content = st.content.width;
598        let vw = st.viewport.width;
599        let max_off = (content - vw).max(0.0);
600        if max_off <= 0.0 || content <= 0.0 {
601            return track;
602        }
603        let thumb_w = (track.size.width * vw / content)
604            .max(THUMB_MIN)
605            .min(track.size.width);
606        let travel = (track.size.width - thumb_w).max(0.0);
607        let t = (st.offset.x / max_off).clamp(0.0, 1.0);
608        Rect::from_xywh(
609            track.left() + t * travel,
610            track.top(),
611            thumb_w,
612            track.size.height,
613        )
614    }
615
616    fn bars(&self, st: &ScrollState, viewport: Rect) -> Bars {
617        let (v_track, v_thumb) = self.v_bar(st, viewport).unzip();
618        let (h_track, h_thumb) = self.h_bar(st, viewport).unzip();
619        Bars {
620            v_track,
621            v_thumb,
622            h_track,
623            h_thumb,
624        }
625    }
626
627    fn drag_offset(
628        &self,
629        st: &ScrollState,
630        bars: &Bars,
631        drag: Drag,
632        x: f32,
633        y: f32,
634    ) -> Option<Vec2> {
635        let o = st.offset;
636        match drag {
637            Drag::Touch { .. } => None,
638            Drag::V { grab } => {
639                let (track, thumb) = bars.v_track.zip(bars.v_thumb)?;
640                let travel = (track.size.height - thumb.size.height).max(0.0);
641                let max_off = (st.content.height - st.viewport.height).max(0.0);
642                if travel <= 0.0 || max_off <= 0.0 {
643                    return None;
644                }
645                let top = (y - grab - track.top()).clamp(0.0, travel);
646                Some(Vec2::new(o.x, top / travel * max_off))
647            }
648            Drag::H { grab } => {
649                let (track, thumb) = bars.h_track.zip(bars.h_thumb)?;
650                let travel = (track.size.width - thumb.size.width).max(0.0);
651                let max_off = (st.content.width - st.viewport.width).max(0.0);
652                if travel <= 0.0 || max_off <= 0.0 {
653                    return None;
654                }
655                let left = (x - grab - track.left()).clamp(0.0, travel);
656                Some(Vec2::new(left / travel * max_off, o.y))
657            }
658        }
659    }
660
661    fn on_wheel(
662        &self,
663        rt: &Runtime,
664        el: ElementId,
665        delta: Vec2,
666        modifiers: Modifiers,
667        ctx: &mut EventCtx,
668    ) {
669        let (mut dx, mut dy) = (delta.x, delta.y);
670        if modifiers.contains(Modifiers::SHIFT) && self.axis.scrolls_x() {
671            dx = dy;
672            dy = 0.0;
673        }
674        let st = Self::load(rt, el);
675        let o = st.offset;
676        let max = Vec2::new(
677            (st.content.width - st.viewport.width).max(0.0),
678            (st.content.height - st.viewport.height).max(0.0),
679        );
680        let nx = if self.axis.scrolls_x() {
681            (o.x - dx).clamp(0.0, max.x)
682        } else {
683            o.x.clamp(0.0, max.x)
684        };
685        let ny = if self.axis.scrolls_y() {
686            (o.y - dy).clamp(0.0, max.y)
687        } else {
688            o.y.clamp(0.0, max.y)
689        };
690        let next = Vec2::new(nx, ny);
691        if next != o {
692            self.apply(rt, el, next);
693            ctx.request_frame();
694            ctx.handled();
695        }
696    }
697
698    fn on_key(
699        &self,
700        rt: &Runtime,
701        el: ElementId,
702        key: Key,
703        modifiers: Modifiers,
704        ctx: &mut EventCtx,
705    ) {
706        let st = Self::load(rt, el);
707        let o = st.offset;
708        let max = Vec2::new(
709            (st.content.width - st.viewport.width).max(0.0),
710            (st.content.height - st.viewport.height).max(0.0),
711        );
712        let vp = st.viewport;
713        let step = if self.step.is_finite() {
714            self.step.max(0.0)
715        } else {
716            0.0
717        };
718        let next = match (key, modifiers) {
719            (Key::ARROW_UP, Modifiers::NONE) if self.axis.scrolls_y() => Vec2::new(o.x, o.y - step),
720            (Key::ARROW_DOWN, Modifiers::NONE) if self.axis.scrolls_y() => {
721                Vec2::new(o.x, o.y + step)
722            }
723            (Key::ARROW_LEFT, Modifiers::NONE) if self.axis.scrolls_x() => {
724                Vec2::new(o.x - step, o.y)
725            }
726            (Key::ARROW_RIGHT, Modifiers::NONE) if self.axis.scrolls_x() => {
727                Vec2::new(o.x + step, o.y)
728            }
729            (Key::PAGE_UP, Modifiers::NONE) if self.axis.scrolls_y() => {
730                Vec2::new(o.x, o.y - vp.height)
731            }
732            (Key::PAGE_DOWN, Modifiers::NONE) if self.axis.scrolls_y() => {
733                Vec2::new(o.x, o.y + vp.height)
734            }
735            (Key::HOME, Modifiers::NONE) if self.axis.scrolls_y() => Vec2::new(o.x, 0.0),
736            (Key::END, Modifiers::NONE) if self.axis.scrolls_y() => Vec2::new(o.x, max.y),
737            (Key::HOME, m) if m == Modifiers::CONTROL => Vec2::new(0.0, 0.0),
738            (Key::END, m) if m == Modifiers::CONTROL => Vec2::new(max.x, max.y),
739            _ => return,
740        };
741        let next = Vec2::new(next.x.clamp(0.0, max.x), next.y.clamp(0.0, max.y));
742        if next != o {
743            self.apply(rt, el, next);
744            ctx.request_frame();
745            ctx.handled();
746        }
747    }
748}
749
750impl View for ScrollView {
751    fn body(&self) -> impl View + 'static {
752        crate::views::Unit
753    }
754
755    fn children_override(&self) -> Option<Vec<AnyView>> {
756        Some(vec![self.child.clone()])
757    }
758
759    fn layout_style(&self) -> LayoutStyle {
760        self.style
761    }
762
763    fn intrinsic_size(&self, _ctx: &mut LayoutCtx) -> Option<Size> {
764        None
765    }
766
767    fn contain_size(&self) -> bool {
768        true
769    }
770
771    fn arrange_override(
772        &self,
773        rt: &Runtime,
774        el: ElementId,
775        rect: Rect,
776        ctx: &mut LayoutCtx,
777    ) -> bool {
778        let children = rt.children(el);
779        let Some(&child) = children.first() else {
780            rt.with_state(el, |s: &mut ScrollState| {
781                s.viewport = rect.size;
782                s.content = Size::ZERO;
783                s.frame = rect;
784            });
785            return true;
786        };
787
788        let gx = if self.scrollbar == ScrollbarVisibility::OnHover && self.axis.scrolls_y() {
789            self.gutter()
790        } else {
791            0.0
792        };
793        let gy = if self.scrollbar == ScrollbarVisibility::OnHover && self.axis.scrolls_x() {
794            self.gutter()
795        } else {
796            0.0
797        };
798        let max_w = if self.axis.scrolls_x() {
799            f32::INFINITY
800        } else {
801            (rect.size.width - gx).max(0.0)
802        };
803        let max_h = if self.axis.scrolls_y() {
804            f32::INFINITY
805        } else {
806            (rect.size.height - gy).max(0.0)
807        };
808        let unconstrained = Constraints::loose(Size::new(max_w, max_h));
809
810        let content_size = rt.measure_node(child, unconstrained, ctx);
811
812        let max = Vec2::new(
813            (content_size.width - rect.size.width).max(0.0),
814            (content_size.height - rect.size.height).max(0.0),
815        );
816        let off = rt.with_state(el, |s: &mut ScrollState| {
817            s.offset = Vec2::new(s.offset.x.clamp(0.0, max.x), s.offset.y.clamp(0.0, max.y));
818            s.offset
819        });
820
821        let (prev_frame, prev_content, prev_off, prev_epoch) = rt
822            .read_state(el, |s: &ScrollState| {
823                (s.frame, s.content, s.arranged, s.arranged_epoch)
824            });
825        let prev_content_w = if self.axis.scrolls_x() {
826            prev_content.width
827        } else {
828            prev_content.width.max(prev_frame.size.width)
829        };
830        let prev_content_h = if self.axis.scrolls_y() {
831            prev_content.height
832        } else {
833            prev_content.height.max(prev_frame.size.height)
834        };
835        let new_w = if self.axis.scrolls_x() {
836            content_size.width
837        } else {
838            content_size.width.max(rect.size.width - gx)
839        };
840        let new_h = if self.axis.scrolls_y() {
841            content_size.height
842        } else {
843            content_size.height.max(rect.size.height - gy)
844        };
845
846        rt.with_state(el, |s: &mut ScrollState| {
847            s.viewport = rect.size;
848            s.content = content_size;
849        });
850
851        if !prev_frame.size.is_empty()
852            && prev_content_w == new_w
853            && prev_content_h == new_h
854            && prev_epoch == rt.layout_epoch()
855        {
856            let dx = (rect.left() - prev_frame.left()) - (off.x - prev_off.x);
857            let dy = (rect.top() - prev_frame.top()) - (off.y - prev_off.y);
858            if dx != 0.0 || dy != 0.0 {
859                rt.translate_subtree(child, Vec2::new(dx, dy));
860            }
861            rt.with_state(el, |s: &mut ScrollState| {
862                s.arranged = off;
863                s.frame = rect;
864            });
865            return true;
866        }
867
868        let w = new_w;
869        let h = new_h;
870        let child_rect = Rect::from_xywh(rect.left() - off.x, rect.top() - off.y, w, h);
871        let epoch = rt.layout_epoch();
872        rt.arrange_node(child, child_rect, ctx);
873        rt.with_state(el, |s: &mut ScrollState| {
874            s.arranged = off;
875            s.arranged_epoch = epoch;
876            s.frame = rect;
877        });
878        true
879    }
880
881    fn clip_self(&self) -> bool {
882        true
883    }
884
885    fn captures_pointer(&self) -> bool {
886        false
887    }
888
889    fn wants_hover_updates(&self) -> bool {
890        matches!(
891            self.scrollbar,
892            ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay
893        )
894    }
895
896    fn captures_pointer_ctx(&self, rt: &Runtime, el: ElementId) -> bool {
897        rt.read_state(el, |s: &ScrollState| s.drag.is_some())
898    }
899
900    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
901        let rt = ctx.runtime().clone();
902        let el = ctx.element();
903        match event {
904            Event::Pointer(PointerEvent::Wheel {
905                delta, modifiers, ..
906            }) => {
907                if matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
908                    self.on_wheel(&rt, el, *delta, *modifiers, ctx);
909                }
910            }
911            Event::Pointer(PointerEvent::Down { pos, .. }) => {
912                if !matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
913                    return;
914                }
915
916                if self.track_hover(&rt, el, ctx.rect, *pos) {
917                    ctx.request_frame();
918                }
919                let st = Self::load(&rt, el);
920                if st.drag.is_some() {
921                    return;
922                }
923                let bars = self.bars(&st, ctx.rect);
924
925                if let Some(thumb) = bars.v_thumb
926                    && thumb.contains(*pos)
927                {
928                    rt.with_state(el, |s: &mut ScrollState| {
929                        s.drag = Some(Drag::V {
930                            grab: pos.y - thumb.top(),
931                        });
932                    });
933                    ctx.request_frame();
934                    ctx.handled();
935                    return;
936                }
937                if let Some(thumb) = bars.h_thumb
938                    && thumb.contains(*pos)
939                {
940                    rt.with_state(el, |s: &mut ScrollState| {
941                        s.drag = Some(Drag::H {
942                            grab: pos.x - thumb.left(),
943                        });
944                    });
945                    ctx.request_frame();
946                    ctx.handled();
947                    return;
948                }
949                if let Some(track) = bars.v_track
950                    && track.contains(*pos)
951                    && let Some(thumb) = bars.v_thumb
952                {
953                    let travel = (track.size.height - thumb.size.height).max(0.0);
954                    let max_off = (st.content.height - st.viewport.height).max(0.0);
955                    if travel > 0.0 && max_off > 0.0 {
956                        let top =
957                            (pos.y - thumb.size.height * 0.5 - track.top()).clamp(0.0, travel);
958                        let o = st.offset;
959                        self.apply(&rt, el, Vec2::new(o.x, top / travel * max_off));
960                    }
961                    rt.with_state(el, |s: &mut ScrollState| {
962                        s.drag = Some(Drag::V {
963                            grab: thumb.size.height * 0.5,
964                        });
965                    });
966                    ctx.request_frame();
967                    ctx.handled();
968                    return;
969                }
970                if let Some(track) = bars.h_track
971                    && track.contains(*pos)
972                    && let Some(thumb) = bars.h_thumb
973                {
974                    let travel = (track.size.width - thumb.size.width).max(0.0);
975                    let max_off = (st.content.width - st.viewport.width).max(0.0);
976                    if travel > 0.0 && max_off > 0.0 {
977                        let left =
978                            (pos.x - thumb.size.width * 0.5 - track.left()).clamp(0.0, travel);
979                        let o = st.offset;
980                        self.apply(&rt, el, Vec2::new(left / travel * max_off, o.y));
981                    }
982                    rt.with_state(el, |s: &mut ScrollState| {
983                        s.drag = Some(Drag::H {
984                            grab: thumb.size.width * 0.5,
985                        });
986                    });
987                    ctx.request_frame();
988                    ctx.handled();
989                }
990            }
991            Event::Pointer(PointerEvent::Move { pos, .. }) => {
992                if ctx.phase != EventPhase::Target {
993                    return;
994                }
995                let st = Self::load(&rt, el);
996                if let Some(drag) = st.drag {
997                    let bars = self.bars(&st, ctx.rect);
998                    if let Some(next) = self.drag_offset(&st, &bars, drag, pos.x, pos.y) {
999                        let max = Vec2::new(
1000                            (st.content.width - st.viewport.width).max(0.0),
1001                            (st.content.height - st.viewport.height).max(0.0),
1002                        );
1003                        let next = Vec2::new(next.x.clamp(0.0, max.x), next.y.clamp(0.0, max.y));
1004                        if next != st.offset {
1005                            self.apply(&rt, el, next);
1006                            ctx.request_frame();
1007                        }
1008                        ctx.handled();
1009                    }
1010                }
1011            }
1012            Event::Pointer(PointerEvent::Up { pos, .. }) => {
1013                if ctx.phase != EventPhase::Target {
1014                    return;
1015                }
1016
1017                let mut reframed = false;
1018                if matches!(
1019                    self.scrollbar,
1020                    ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay
1021                ) && self.track_hover(&rt, el, ctx.rect, *pos)
1022                {
1023                    ctx.request_frame();
1024                    reframed = true;
1025                }
1026                let had = rt.with_state(el, |s: &mut ScrollState| s.drag.take().is_some());
1027                if had && !reframed {
1028                    ctx.request_frame();
1029                }
1030                if had {
1031                    ctx.handled();
1032                }
1033            }
1034            Event::Pointer(PointerEvent::TouchDown { id, pos, .. }) => {
1035                if !matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
1036                    return;
1037                }
1038                let st = Self::load(&rt, el);
1039
1040                if st.drag.is_some() {
1041                    return;
1042                }
1043
1044                rt.with_state(el, |s: &mut ScrollState| {
1045                    s.drag = Some(Drag::Touch {
1046                        id: *id,
1047                        start: *pos,
1048                        last: *pos,
1049                        moved: false,
1050                    });
1051                });
1052            }
1053            Event::Pointer(PointerEvent::TouchMove { id, pos, .. }) => {
1054                if !matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
1055                    return;
1056                }
1057                let st = Self::load(&rt, el);
1058                let Some(Drag::Touch {
1059                    id: did,
1060                    start,
1061                    last,
1062                    moved,
1063                }) = st.drag
1064                else {
1065                    return;
1066                };
1067                if did != *id {
1068                    return;
1069                }
1070                let dx_total = pos.x - start.x;
1071                let dy_total = pos.y - start.y;
1072                if !moved && dx_total * dx_total + dy_total * dy_total < TOUCH_PAN_THRESHOLD_SQ {
1073                    return;
1074                }
1075                if !moved {
1076                    ctx.capture_pointer();
1077                }
1078
1079                let max = Vec2::new(
1080                    (st.content.width - st.viewport.width).max(0.0),
1081                    (st.content.height - st.viewport.height).max(0.0),
1082                );
1083                let next = Vec2::new(
1084                    if self.axis.scrolls_x() {
1085                        (st.offset.x - (pos.x - last.x)).clamp(0.0, max.x)
1086                    } else {
1087                        st.offset.x
1088                    },
1089                    if self.axis.scrolls_y() {
1090                        (st.offset.y - (pos.y - last.y)).clamp(0.0, max.y)
1091                    } else {
1092                        st.offset.y
1093                    },
1094                );
1095                rt.with_state(el, |s: &mut ScrollState| {
1096                    s.drag = Some(Drag::Touch {
1097                        id: *id,
1098                        start,
1099                        last: *pos,
1100                        moved: true,
1101                    });
1102                });
1103                if next != st.offset {
1104                    self.apply(&rt, el, next);
1105                    ctx.request_frame();
1106                }
1107                ctx.handled();
1108            }
1109            Event::Pointer(PointerEvent::TouchUp { id, .. }) => {
1110                if !matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
1111                    return;
1112                }
1113
1114                let is_touch = matches!(
1115                    Self::load(&rt, el).drag,
1116                    Some(Drag::Touch { id: did, .. }) if did == *id
1117                );
1118                if !is_touch {
1119                    return;
1120                }
1121                let moved = rt.with_state(el, |s: &mut ScrollState| {
1122                    let moved = matches!(s.drag, Some(Drag::Touch { moved: true, .. }));
1123                    s.drag.take();
1124                    moved
1125                });
1126                if moved {
1127                    ctx.release_pointer();
1128                    ctx.request_frame();
1129                    ctx.handled();
1130                }
1131            }
1132            Event::Pointer(PointerEvent::Cancel) => {
1133                if ctx.phase != EventPhase::Target {
1134                    return;
1135                }
1136                let had = rt.with_state(el, |s: &mut ScrollState| s.drag.take().is_some());
1137                let unhov = rt.with_state(el, |s: &mut ScrollState| {
1138                    let old = s.bar_hover;
1139                    s.bar_hover = false;
1140                    old
1141                });
1142                if had || unhov {
1143                    ctx.request_frame();
1144                }
1145                if had {
1146                    ctx.handled();
1147                }
1148            }
1149            Event::Key(key) => {
1150                if matches!(ctx.phase, EventPhase::Target | EventPhase::Bubble) {
1151                    self.on_key(&rt, el, key.key, key.modifiers, ctx);
1152                }
1153            }
1154            Event::Ime(_) => {}
1155            Event::FilesHovered { .. } | Event::FilesDropped { .. } | Event::FilesCancelled => {}
1156
1157            _ => {}
1158        }
1159    }
1160
1161    fn paint_self(&self, ctx: &mut PaintCtx) {
1162        let rt = ctx.runtime().clone();
1163        let el = ctx.element();
1164        let viewport = ctx.rect;
1165        rt.with_state(el, |s: &mut ScrollState| {
1166            s.viewport = viewport.size;
1167        });
1168    }
1169
1170    fn paint_foreground(&self, ctx: &mut PaintCtx) {
1171        let rt = ctx.runtime().clone();
1172        let el = ctx.element();
1173        let viewport = ctx.rect;
1174
1175        let hov = matches!(
1176            self.scrollbar,
1177            ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay
1178        ) && rt
1179            .pointer_pos()
1180            .is_some_and(|p| self.in_gutter(viewport, p));
1181        rt.with_state(el, |s: &mut ScrollState| {
1182            s.viewport = viewport.size;
1183            if matches!(
1184                self.scrollbar,
1185                ScrollbarVisibility::OnHover | ScrollbarVisibility::Overlay
1186            ) {
1187                s.bar_hover = hov;
1188            }
1189        });
1190        let st = Self::load(&rt, el);
1191
1192        let bars = self.bars(&st, viewport);
1193        if bars.v_track.is_none() && bars.h_track.is_none() {
1194            return;
1195        }
1196
1197        let theme = ctx.theme();
1198        let track_color = self.track_color.unwrap_or(theme.border);
1199        let thumb_color = self.thumb_color.unwrap_or(theme.text_secondary);
1200        let radius = self.bar_width * 0.375;
1201        if let (Some(track), Some(thumb)) = (bars.v_track, bars.v_thumb) {
1202            ctx.quad(track, track_color, radius);
1203            ctx.quad(thumb, thumb_color, radius);
1204        }
1205        if let (Some(track), Some(thumb)) = (bars.h_track, bars.h_thumb) {
1206            ctx.quad(track, track_color, radius);
1207            ctx.quad(thumb, thumb_color, radius);
1208        }
1209
1210        if let (Some(v), Some(h)) = (bars.v_track, bars.h_track) {
1211            let corner = Rect::from_xywh(v.left(), h.top(), v.size.width, h.size.height);
1212            if !corner.is_empty() {
1213                ctx.quad(corner, track_color, 0.0);
1214            }
1215        }
1216    }
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221    use super::*;
1222
1223    #[test]
1224    fn fill_helpers_keep_scroll_view_filled() {
1225        let s = ScrollView::new(crate::views::Unit).fill_size();
1226        let style = s.layout_style();
1227        assert_eq!(style.width, Length::Fill(1.0));
1228        assert_eq!(style.height, Length::Fill(1.0));
1229    }
1230}