floem/views/
scroll.rs

1#![deny(missing_docs)]
2//! Scroll View
3
4use floem_reactive::create_effect;
5use peniko::kurbo::{Point, Rect, Size, Stroke, Vec2};
6use peniko::{Brush, Color};
7
8use crate::style::CustomStylable;
9use crate::unit::PxPct;
10use crate::{
11    app_state::AppState,
12    context::{ComputeLayoutCx, PaintCx},
13    event::{Event, EventPropagation},
14    id::ViewId,
15    prop, prop_extractor,
16    style::{Background, BorderColor, BorderRadius, Style, StyleSelector},
17    style_class,
18    unit::Px,
19    view::{IntoView, View},
20    Renderer,
21};
22
23use super::Decorators;
24
25enum ScrollState {
26    EnsureVisible(Rect),
27    ScrollDelta(Vec2),
28    ScrollTo(Point),
29    ScrollToPercent(f32),
30    ScrollToView(ViewId),
31}
32
33/// Minimum length for any scrollbar to be when measured on that
34/// scrollbar's primary axis.
35const SCROLLBAR_MIN_SIZE: f64 = 10.0;
36
37/// Denotes which scrollbar, if any, is currently being dragged.
38#[derive(Debug, Copy, Clone)]
39enum BarHeldState {
40    /// Neither scrollbar is being dragged.
41    None,
42    /// Vertical scrollbar is being dragged. Contains an `f64` with
43    /// the initial y-offset of the dragging input.
44    Vertical(f64, Vec2),
45    /// Horizontal scrollbar is being dragged. Contains an `f64` with
46    /// the initial x-offset of the dragging input.
47    Horizontal(f64, Vec2),
48}
49
50style_class!(
51    /// Style class that will be applied to the handles of the scroll view
52    pub Handle
53);
54style_class!(
55    /// Style class that will be applied to the scroll tracks of the scroll view
56    pub Track
57);
58
59prop!(
60    /// Determines if scroll handles should be rounded (defaults to true on macOS).
61    pub Rounded: bool {} = cfg!(target_os = "macos")
62);
63prop!(
64    /// Specifies the thickness of scroll handles in pixels.
65    pub Thickness: Px {} = Px(10.0)
66);
67prop!(
68    /// Defines the border width of a scroll track in pixels.
69    pub Border: Px {} = Px(0.0)
70);
71
72prop_extractor! {
73    ScrollTrackStyle {
74        color: Background,
75        border_radius: BorderRadius,
76        border_color: BorderColor,
77        border: Border,
78        rounded: Rounded,
79        thickness: Thickness,
80    }
81}
82
83prop!(
84    /// Specifies the vertical inset of the scrollable area in pixels.
85    pub VerticalInset: Px {} = Px(0.0)
86);
87
88prop!(
89    /// Defines the horizontal inset of the scrollable area in pixels.
90    pub HorizontalInset: Px {} = Px(0.0)
91);
92
93prop!(
94    /// Controls the visibility of scroll bars. When true, bars are hidden.
95    pub HideBars: bool {} = false
96);
97
98prop!(
99    /// Determines if pointer wheel events should propagate to parent elements.
100    pub PropagatePointerWheel: bool {} = true
101);
102
103prop!(
104    /// When true, vertical scroll input is interpreted as horizontal scrolling.
105    pub VerticalScrollAsHorizontal: bool {} = false
106);
107
108prop!(
109    /// Enables clipping of overflowing content when set to true.
110    pub OverflowClip: bool {} = true
111);
112
113prop_extractor!(ScrollStyle {
114    vertical_bar_inset: VerticalInset,
115    horizontal_bar_inset: HorizontalInset,
116    hide_bar: HideBars,
117    propagate_pointer_wheel: PropagatePointerWheel,
118    vertical_scroll_as_horizontal: VerticalScrollAsHorizontal,
119    overflow_clip: OverflowClip,
120});
121
122const HANDLE_COLOR: Brush = Brush::Solid(Color::rgba8(0, 0, 0, 120));
123
124style_class!(
125    /// Style class that is applied to every scroll view
126    pub ScrollClass
127);
128
129/// A scroll view
130pub struct Scroll {
131    id: ViewId,
132    child: ViewId,
133
134    total_rect: Rect,
135
136    /// the actual rect of the scroll view excluding padding and borders. The origin is relative to this view.
137    content_rect: Rect,
138
139    child_size: Size,
140
141    /// The origin is relative to `actual_rect`.
142    child_viewport: Rect,
143
144    /// This is the value of `child_viewport` for the last `compute_layout`. This is used in
145    /// handling for `ScrollToView` as scrolling updates may mutate `child_viewport`.
146    /// The origin is relative to `actual_rect`.
147    computed_child_viewport: Rect,
148
149    onscroll: Option<Box<dyn Fn(Rect)>>,
150    held: BarHeldState,
151    v_handle_hover: bool,
152    h_handle_hover: bool,
153    v_track_hover: bool,
154    h_track_hover: bool,
155    handle_style: ScrollTrackStyle,
156    handle_active_style: ScrollTrackStyle,
157    handle_hover_style: ScrollTrackStyle,
158    track_style: ScrollTrackStyle,
159    track_hover_style: ScrollTrackStyle,
160    scroll_style: ScrollStyle,
161}
162
163/// Create a new scroll view
164pub fn scroll<V: IntoView + 'static>(child: V) -> Scroll {
165    let id = ViewId::new();
166    let child = child.into_view();
167    let child_id = child.id();
168    id.set_children(vec![child]);
169
170    Scroll {
171        id,
172        child: child_id,
173        content_rect: Rect::ZERO,
174        total_rect: Rect::ZERO,
175        child_size: Size::ZERO,
176        child_viewport: Rect::ZERO,
177        computed_child_viewport: Rect::ZERO,
178        onscroll: None,
179        held: BarHeldState::None,
180        v_handle_hover: false,
181        h_handle_hover: false,
182        v_track_hover: false,
183        h_track_hover: false,
184        handle_style: Default::default(),
185        handle_active_style: Default::default(),
186        handle_hover_style: Default::default(),
187        track_style: Default::default(),
188        track_hover_style: Default::default(),
189        scroll_style: Default::default(),
190    }
191    .class(ScrollClass)
192}
193
194impl Scroll {
195    /// Sets a callback that will be triggered whenever the scroll position changes.
196    ///
197    /// This callback receives the viewport rectangle that represents the currently
198    /// visible portion of the scrollable content.
199    pub fn on_scroll(mut self, onscroll: impl Fn(Rect) + 'static) -> Self {
200        self.onscroll = Some(Box::new(onscroll));
201        self
202    }
203
204    /// Ensures that a specific rectangular area is visible within the scroll view by automatically
205    /// scrolling to it if necessary.
206    ///
207    /// # Reactivity
208    /// The viewport will automatically update to include the target rectangle whenever the rectangle's
209    /// position or size changes, as determined by the `to` function which will update any time there are
210    /// changes in the signals that it depends on.
211    pub fn ensure_visible(self, to: impl Fn() -> Rect + 'static) -> Self {
212        let id = self.id();
213        create_effect(move |_| {
214            let rect = to();
215            id.update_state_deferred(ScrollState::EnsureVisible(rect));
216        });
217
218        self
219    }
220
221    /// Scrolls the view by the specified delta vector.
222    ///
223    /// # Reactivity
224    /// The scroll position will automatically update whenever the delta vector changes,
225    /// as determined by the `delta` function which will update any time there are changes in the signals that it depends on.
226    pub fn scroll_delta(self, delta: impl Fn() -> Vec2 + 'static) -> Self {
227        let id = self.id();
228        create_effect(move |_| {
229            let delta = delta();
230            id.update_state(ScrollState::ScrollDelta(delta));
231        });
232
233        self
234    }
235
236    /// Scrolls the view to the specified target point.
237    ///
238    /// # Reactivity
239    /// The scroll position will automatically update whenever the target point changes,
240    /// as determined by the `origin` function which will update any time there are changes in the signals that it depends on.
241    pub fn scroll_to(self, origin: impl Fn() -> Option<Point> + 'static) -> Self {
242        let id = self.id();
243        create_effect(move |_| {
244            if let Some(origin) = origin() {
245                id.update_state_deferred(ScrollState::ScrollTo(origin));
246            }
247        });
248
249        self
250    }
251
252    /// Scrolls the view to the specified percentage (0-100) of its scrollable content.
253    ///
254    /// # Reactivity
255    /// The scroll position will automatically update whenever the target percentage changes,
256    /// as determined by the `percent` function which will update any time there are changes in the signals that it depends on.
257    pub fn scroll_to_percent(self, percent: impl Fn() -> f32 + 'static) -> Self {
258        let id = self.id();
259        create_effect(move |_| {
260            let percent = percent() / 100.;
261            id.update_state_deferred(ScrollState::ScrollToPercent(percent));
262        });
263        self
264    }
265
266    /// Scrolls the view to make a specific view visible.
267    ///
268    /// # Reactivity
269    /// The scroll position will automatically update whenever the target view changes,
270    /// as determined by the `view` function which will update any time there are changes in the signals that it depends on.
271    pub fn scroll_to_view(self, view: impl Fn() -> Option<ViewId> + 'static) -> Self {
272        let id = self.id();
273        create_effect(move |_| {
274            if let Some(view) = view() {
275                id.update_state_deferred(ScrollState::ScrollToView(view));
276            }
277        });
278
279        self
280    }
281
282    fn do_scroll_delta(&mut self, app_state: &mut AppState, delta: Vec2) {
283        let new_origin = self.child_viewport.origin() + delta;
284        self.clamp_child_viewport(app_state, self.child_viewport.with_origin(new_origin));
285    }
286
287    fn do_scroll_to(&mut self, app_state: &mut AppState, origin: Point) {
288        self.clamp_child_viewport(app_state, self.child_viewport.with_origin(origin));
289    }
290
291    /// Pan the smallest distance that makes the target [`Rect`] visible.
292    ///
293    /// If the target rect is larger than viewport size, we will prioritize
294    /// the region of the target closest to its origin.
295    pub fn pan_to_visible(&mut self, app_state: &mut AppState, rect: Rect) {
296        /// Given a position and the min and max edges of an axis,
297        /// return a delta by which to adjust that axis such that the value
298        /// falls between its edges.
299        ///
300        /// if the value already falls between the two edges, return 0.0.
301        fn closest_on_axis(val: f64, min: f64, max: f64) -> f64 {
302            assert!(min <= max);
303            if val > min && val < max {
304                0.0
305            } else if val <= min {
306                val - min
307            } else {
308                val - max
309            }
310        }
311
312        // clamp the target region size to our own size.
313        // this means we will show the portion of the target region that
314        // includes the origin.
315        let target_size = Size::new(
316            rect.width().min(self.child_viewport.width()),
317            rect.height().min(self.child_viewport.height()),
318        );
319        let rect = rect.with_size(target_size);
320
321        let x0 = closest_on_axis(
322            rect.min_x(),
323            self.child_viewport.min_x(),
324            self.child_viewport.max_x(),
325        );
326        let x1 = closest_on_axis(
327            rect.max_x(),
328            self.child_viewport.min_x(),
329            self.child_viewport.max_x(),
330        );
331        let y0 = closest_on_axis(
332            rect.min_y(),
333            self.child_viewport.min_y(),
334            self.child_viewport.max_y(),
335        );
336        let y1 = closest_on_axis(
337            rect.max_y(),
338            self.child_viewport.min_y(),
339            self.child_viewport.max_y(),
340        );
341
342        let delta_x = if x0.abs() > x1.abs() { x0 } else { x1 };
343        let delta_y = if y0.abs() > y1.abs() { y0 } else { y1 };
344        let new_origin = self.child_viewport.origin() + Vec2::new(delta_x, delta_y);
345        self.clamp_child_viewport(app_state, self.child_viewport.with_origin(new_origin));
346    }
347
348    fn update_size(&mut self) {
349        self.child_size = self.child_size();
350        self.content_rect = self.id.get_content_rect();
351        self.total_rect = self.id.get_size().unwrap_or_default().to_rect();
352    }
353
354    fn clamp_child_viewport(
355        &mut self,
356        app_state: &mut AppState,
357        child_viewport: Rect,
358    ) -> Option<()> {
359        let actual_rect = self.content_rect;
360        let actual_size = actual_rect.size();
361        let width = actual_rect.width();
362        let height = actual_rect.height();
363        let child_size = self.child_size;
364
365        let mut child_viewport = child_viewport;
366        if width >= child_size.width {
367            child_viewport.x0 = 0.0;
368        } else if child_viewport.x0 > child_size.width - width {
369            child_viewport.x0 = child_size.width - width;
370        } else if child_viewport.x0 < 0.0 {
371            child_viewport.x0 = 0.0;
372        }
373
374        if height >= child_size.height {
375            child_viewport.y0 = 0.0;
376        } else if child_viewport.y0 > child_size.height - height {
377            child_viewport.y0 = child_size.height - height;
378        } else if child_viewport.y0 < 0.0 {
379            child_viewport.y0 = 0.0;
380        }
381        child_viewport = child_viewport.with_size(actual_size);
382
383        if child_viewport != self.child_viewport {
384            self.child.set_viewport(child_viewport);
385            app_state.request_compute_layout_recursive(self.id());
386            app_state.request_paint(self.id());
387            self.child_viewport = child_viewport;
388            if let Some(onscroll) = &self.onscroll {
389                onscroll(child_viewport);
390            }
391        } else {
392            return None;
393        }
394        Some(())
395    }
396
397    fn child_size(&self) -> Size {
398        self.child
399            .get_layout()
400            .map(|layout| Size::new(layout.size.width as f64, layout.size.height as f64))
401            .unwrap()
402    }
403
404    fn v_handle_style(&self) -> &ScrollTrackStyle {
405        if let BarHeldState::Vertical(..) = self.held {
406            &self.handle_active_style
407        } else if self.v_handle_hover {
408            &self.handle_hover_style
409        } else {
410            &self.handle_style
411        }
412    }
413
414    fn h_handle_style(&self) -> &ScrollTrackStyle {
415        if let BarHeldState::Horizontal(..) = self.held {
416            &self.handle_active_style
417        } else if self.h_handle_hover {
418            &self.handle_hover_style
419        } else {
420            &self.handle_style
421        }
422    }
423
424    fn draw_bars(&self, cx: &mut PaintCx) {
425        let scroll_offset = self.child_viewport.origin().to_vec2();
426        let radius = |style: &ScrollTrackStyle, rect: Rect, vertical| {
427            if style.rounded() {
428                if vertical {
429                    (rect.x1 - rect.x0) / 2.
430                } else {
431                    (rect.y1 - rect.y0) / 2.
432                }
433            } else {
434                match style.border_radius() {
435                    crate::unit::PxPct::Px(px) => px,
436                    crate::unit::PxPct::Pct(pct) => rect.size().min_side() * (pct / 100.),
437                }
438            }
439        };
440
441        if let Some(bounds) = self.calc_vertical_bar_bounds(cx.app_state) {
442            let style = self.v_handle_style();
443            let track_style =
444                if self.v_track_hover || matches!(self.held, BarHeldState::Vertical(..)) {
445                    &self.track_hover_style
446                } else {
447                    &self.track_style
448                };
449
450            if let Some(color) = track_style.color() {
451                let mut bounds = bounds - scroll_offset;
452                bounds.y0 = self.total_rect.y0;
453                bounds.y1 = self.total_rect.y1;
454                cx.fill(&bounds, &color, 0.0);
455            }
456            let edge_width = style.border().0;
457            let rect = (bounds - scroll_offset).inset(-edge_width / 2.0);
458            let rect = rect.to_rounded_rect(radius(style, rect, true));
459            cx.fill(&rect, &style.color().unwrap_or(HANDLE_COLOR), 0.0);
460            if edge_width > 0.0 {
461                cx.stroke(&rect, &style.border_color(), &Stroke::new(edge_width));
462            }
463        }
464
465        // Horizontal bar
466        if let Some(bounds) = self.calc_horizontal_bar_bounds(cx.app_state) {
467            let style = self.h_handle_style();
468            let track_style =
469                if self.h_track_hover || matches!(self.held, BarHeldState::Horizontal(..)) {
470                    &self.track_hover_style
471                } else {
472                    &self.track_style
473                };
474
475            if let Some(color) = track_style.color() {
476                let mut bounds = bounds - scroll_offset;
477                bounds.x0 = self.total_rect.x0;
478                bounds.x1 = self.total_rect.x1;
479                cx.fill(&bounds, &color, 0.0);
480            }
481            let edge_width = style.border().0;
482            let rect = (bounds - scroll_offset).inset(-edge_width / 2.0);
483            let rect = rect.to_rounded_rect(radius(style, rect, false));
484            cx.fill(&rect, &style.color().unwrap_or(HANDLE_COLOR), 0.0);
485            if edge_width > 0.0 {
486                cx.stroke(&rect, &style.border_color(), &Stroke::new(edge_width));
487            }
488        }
489    }
490
491    fn calc_vertical_bar_bounds(&self, _app_state: &mut AppState) -> Option<Rect> {
492        let viewport_size = self.child_viewport.size();
493        let content_size = self.child_size;
494        let scroll_offset = self.child_viewport.origin().to_vec2();
495
496        // dbg!(viewport_size.height, content_size.height);
497        if viewport_size.height >= content_size.height - 1. {
498            return None;
499        }
500
501        let style = self.v_handle_style();
502
503        let bar_width = style.thickness().0;
504        let bar_pad = self.scroll_style.vertical_bar_inset().0;
505
506        let percent_visible = viewport_size.height / content_size.height;
507        let percent_scrolled = scroll_offset.y / (content_size.height - viewport_size.height);
508
509        let length = (percent_visible * self.total_rect.height()).ceil();
510        // Vertical scroll bar must have ast least the same height as it's width
511        let length = length.max(style.thickness().0);
512
513        let top_y_offset = ((self.total_rect.height() - length) * percent_scrolled).ceil();
514        let bottom_y_offset = top_y_offset + length;
515
516        let x0 = scroll_offset.x + self.total_rect.width() - bar_width - bar_pad;
517        let y0 = scroll_offset.y + top_y_offset;
518
519        let x1 = scroll_offset.x + self.total_rect.width() - bar_pad;
520        let y1 = scroll_offset.y + bottom_y_offset;
521
522        Some(Rect::new(x0, y0, x1, y1))
523    }
524
525    fn calc_horizontal_bar_bounds(&self, _app_state: &mut AppState) -> Option<Rect> {
526        let viewport_size = self.child_viewport.size();
527        let content_size = self.child_size;
528        let scroll_offset = self.child_viewport.origin().to_vec2();
529
530        if viewport_size.width >= content_size.width - 1. {
531            return None;
532        }
533
534        let style = self.h_handle_style();
535
536        let bar_width = style.thickness().0;
537        let bar_pad = self.scroll_style.horizontal_bar_inset().0;
538
539        let percent_visible = viewport_size.width / content_size.width;
540        let percent_scrolled = scroll_offset.x / (content_size.width - viewport_size.width);
541
542        let length = (percent_visible * self.total_rect.width()).ceil();
543        let length = length.max(SCROLLBAR_MIN_SIZE);
544
545        let horizontal_padding = if viewport_size.height >= content_size.height {
546            0.0
547        } else {
548            bar_pad + bar_pad + bar_width
549        };
550
551        let left_x_offset =
552            ((self.total_rect.width() - length - horizontal_padding) * percent_scrolled).ceil();
553        let right_x_offset = left_x_offset + length;
554
555        let x0 = scroll_offset.x + left_x_offset;
556        let y0 = scroll_offset.y + self.total_rect.height() - bar_width - bar_pad;
557
558        let x1 = scroll_offset.x + right_x_offset;
559        let y1 = scroll_offset.y + self.total_rect.height() - bar_pad;
560
561        Some(Rect::new(x0, y0, x1, y1))
562    }
563
564    fn click_vertical_bar_area(&mut self, app_state: &mut AppState, pos: Point) {
565        let new_y = (pos.y / self.content_rect.height()) * self.child_size.height
566            - self.content_rect.height() / 2.0;
567        let mut new_origin = self.child_viewport.origin();
568        new_origin.y = new_y;
569        self.do_scroll_to(app_state, new_origin);
570    }
571
572    fn click_horizontal_bar_area(&mut self, app_state: &mut AppState, pos: Point) {
573        let new_x = (pos.x / self.content_rect.width()) * self.child_size.width
574            - self.content_rect.width() / 2.0;
575        let mut new_origin = self.child_viewport.origin();
576        new_origin.x = new_x;
577        self.do_scroll_to(app_state, new_origin);
578    }
579
580    fn point_hits_vertical_bar(&self, app_state: &mut AppState, pos: Point) -> bool {
581        if let Some(mut bounds) = self.calc_vertical_bar_bounds(app_state) {
582            // Stretch hitbox to edge of widget
583            let scroll_offset = self.child_viewport.origin().to_vec2();
584            bounds.x1 = self.total_rect.x1 + scroll_offset.x;
585            pos.x >= bounds.x0 && pos.x <= bounds.x1
586        } else {
587            false
588        }
589    }
590
591    fn point_hits_horizontal_bar(&self, app_state: &mut AppState, pos: Point) -> bool {
592        if let Some(mut bounds) = self.calc_horizontal_bar_bounds(app_state) {
593            // Stretch hitbox to edge of widget
594            let scroll_offset = self.child_viewport.origin().to_vec2();
595            bounds.y1 = self.total_rect.y1 + scroll_offset.y;
596            pos.y >= bounds.y0 && pos.y <= bounds.y1
597        } else {
598            false
599        }
600    }
601
602    fn point_hits_vertical_handle(&self, app_state: &mut AppState, pos: Point) -> bool {
603        if let Some(mut bounds) = self.calc_vertical_bar_bounds(app_state) {
604            // Stretch hitbox to edge of widget
605            let scroll_offset = self.child_viewport.origin().to_vec2();
606            bounds.x1 = self.total_rect.x1 + scroll_offset.x;
607            bounds.contains(pos)
608        } else {
609            false
610        }
611    }
612
613    fn point_hits_horizontal_handle(&self, app_state: &mut AppState, pos: Point) -> bool {
614        if let Some(mut bounds) = self.calc_horizontal_bar_bounds(app_state) {
615            // Stretch hitbox to edge of widget
616            let scroll_offset = self.child_viewport.origin().to_vec2();
617            bounds.y1 = self.total_rect.y1 + scroll_offset.y;
618            bounds.contains(pos)
619        } else {
620            false
621        }
622    }
623
624    /// true if either scrollbar is currently held down/being dragged
625    fn are_bars_held(&self) -> bool {
626        !matches!(self.held, BarHeldState::None)
627    }
628
629    fn update_hover_states(&mut self, app_state: &mut AppState, pos: Point) {
630        let scroll_offset = self.child_viewport.origin().to_vec2();
631        let pos = pos + scroll_offset;
632        let hover = self.point_hits_vertical_handle(app_state, pos);
633        if self.v_handle_hover != hover {
634            self.v_handle_hover = hover;
635            app_state.request_paint(self.id());
636        }
637        let hover = self.point_hits_horizontal_handle(app_state, pos);
638        if self.h_handle_hover != hover {
639            self.h_handle_hover = hover;
640            app_state.request_paint(self.id());
641        }
642        let hover = self.point_hits_vertical_bar(app_state, pos);
643        if self.v_track_hover != hover {
644            self.v_track_hover = hover;
645            app_state.request_paint(self.id());
646        }
647        let hover = self.point_hits_horizontal_bar(app_state, pos);
648        if self.h_track_hover != hover {
649            self.h_track_hover = hover;
650            app_state.request_paint(self.id());
651        }
652    }
653
654    fn do_scroll_to_view(
655        &mut self,
656        app_state: &mut AppState,
657        target: ViewId,
658        target_rect: Option<Rect>,
659    ) {
660        if target.get_layout().is_some() && !target.is_hidden_recursive() {
661            let mut rect = target.layout_rect();
662
663            if let Some(target_rect) = target_rect {
664                rect = rect + target_rect.origin().to_vec2();
665
666                let new_size = target_rect
667                    .size()
668                    .to_rect()
669                    .intersect(rect.size().to_rect())
670                    .size();
671                rect = rect.with_size(new_size);
672            }
673
674            // `get_layout_rect` is window-relative so we have to
675            // convert it to child view relative.
676
677            // TODO: How to deal with nested viewports / scrolls?
678            let rect = rect.with_origin(
679                rect.origin()
680                    - self.id.layout_rect().origin().to_vec2()
681                    - self.content_rect.origin().to_vec2()
682                    + self.computed_child_viewport.origin().to_vec2(),
683            );
684
685            self.pan_to_visible(app_state, rect);
686        }
687    }
688
689    /// Sets the custom style properties of the `Scroll`.
690    pub fn scroll_style(
691        self,
692        style: impl Fn(ScrollCustomStyle) -> ScrollCustomStyle + 'static,
693    ) -> Self {
694        self.custom_style(style)
695    }
696}
697
698impl View for Scroll {
699    fn id(&self) -> ViewId {
700        self.id
701    }
702
703    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
704        "Scroll".into()
705    }
706
707    fn view_style(&self) -> Option<Style> {
708        Some(Style::new().items_start())
709    }
710
711    fn update(&mut self, cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
712        if let Ok(state) = state.downcast::<ScrollState>() {
713            match *state {
714                ScrollState::EnsureVisible(rect) => {
715                    self.pan_to_visible(cx.app_state, rect);
716                }
717                ScrollState::ScrollDelta(delta) => {
718                    self.do_scroll_delta(cx.app_state, delta);
719                }
720                ScrollState::ScrollTo(origin) => {
721                    self.do_scroll_to(cx.app_state, origin);
722                }
723                ScrollState::ScrollToPercent(percent) => {
724                    let mut child_size = self.child_size;
725                    child_size *= percent as f64;
726                    let point = child_size.to_vec2().to_point();
727                    self.do_scroll_to(cx.app_state, point);
728                }
729                ScrollState::ScrollToView(id) => {
730                    self.do_scroll_to_view(cx.app_state, id, None);
731                }
732            }
733            self.id.request_layout();
734        }
735    }
736
737    fn scroll_to(&mut self, cx: &mut AppState, target: ViewId, rect: Option<Rect>) -> bool {
738        let found = self.child.view().borrow_mut().scroll_to(cx, target, rect);
739        if found {
740            self.do_scroll_to_view(cx, target, rect);
741        }
742        found
743    }
744
745    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
746        let style = cx.style();
747
748        self.scroll_style.read(cx);
749
750        let handle_style = style.clone().apply_class(Handle);
751        self.handle_style.read_style(cx, &handle_style);
752        self.handle_hover_style.read_style(
753            cx,
754            &handle_style
755                .clone()
756                .apply_selectors(&[StyleSelector::Hover]),
757        );
758        self.handle_active_style
759            .read_style(cx, &handle_style.apply_selectors(&[StyleSelector::Active]));
760
761        let track_style = style.apply_class(Track);
762        self.track_style.read_style(cx, &track_style);
763        self.track_hover_style
764            .read_style(cx, &track_style.apply_selectors(&[StyleSelector::Hover]));
765
766        cx.style_view(self.child);
767    }
768
769    fn compute_layout(&mut self, cx: &mut ComputeLayoutCx) -> Option<Rect> {
770        self.update_size();
771        self.clamp_child_viewport(cx.app_state_mut(), self.child_viewport);
772        self.computed_child_viewport = self.child_viewport;
773        cx.compute_view_layout(self.child);
774        None
775    }
776
777    fn event_before_children(
778        &mut self,
779        cx: &mut crate::context::EventCx,
780        event: &Event,
781    ) -> EventPropagation {
782        let viewport_size = self.child_viewport.size();
783        let scroll_offset = self.child_viewport.origin().to_vec2();
784        let content_size = self.child_size;
785
786        match &event {
787            Event::PointerDown(event) => {
788                if !self.scroll_style.hide_bar() && event.button.is_primary() {
789                    self.held = BarHeldState::None;
790
791                    let pos = event.pos + scroll_offset;
792
793                    if self.point_hits_vertical_bar(cx.app_state, pos) {
794                        if self.point_hits_vertical_handle(cx.app_state, pos) {
795                            self.held = BarHeldState::Vertical(
796                                // The bounds must be non-empty, because the point hits the scrollbar.
797                                event.pos.y,
798                                scroll_offset,
799                            );
800                            cx.update_active(self.id());
801                            // Force a repaint.
802                            self.id.request_paint();
803                            return EventPropagation::Stop;
804                        }
805                        self.click_vertical_bar_area(cx.app_state, event.pos);
806                        let scroll_offset = self.child_viewport.origin().to_vec2();
807                        self.held = BarHeldState::Vertical(
808                            // The bounds must be non-empty, because the point hits the scrollbar.
809                            event.pos.y,
810                            scroll_offset,
811                        );
812                        cx.update_active(self.id());
813                        return EventPropagation::Stop;
814                    } else if self.point_hits_horizontal_bar(cx.app_state, pos) {
815                        if self.point_hits_horizontal_handle(cx.app_state, pos) {
816                            self.held = BarHeldState::Horizontal(
817                                // The bounds must be non-empty, because the point hits the scrollbar.
818                                event.pos.x,
819                                scroll_offset,
820                            );
821                            cx.update_active(self.id());
822                            // Force a repaint.
823                            cx.app_state.request_paint(self.id());
824                            return EventPropagation::Stop;
825                        }
826                        self.click_horizontal_bar_area(cx.app_state, event.pos);
827                        let scroll_offset = self.child_viewport.origin().to_vec2();
828                        self.held = BarHeldState::Horizontal(
829                            // The bounds must be non-empty, because the point hits the scrollbar.
830                            event.pos.x,
831                            scroll_offset,
832                        );
833                        cx.update_active(self.id());
834                        return EventPropagation::Stop;
835                    }
836                }
837            }
838            Event::PointerUp(_event) => {
839                if self.are_bars_held() {
840                    self.held = BarHeldState::None;
841                    // Force a repaint.
842                    cx.app_state.request_paint(self.id());
843                }
844            }
845            Event::PointerMove(event) => {
846                if !self.scroll_style.hide_bar() {
847                    let pos = event.pos + scroll_offset;
848                    self.update_hover_states(cx.app_state, event.pos);
849
850                    if self.are_bars_held() {
851                        match self.held {
852                            BarHeldState::Vertical(offset, initial_scroll_offset) => {
853                                let scale_y = viewport_size.height / content_size.height;
854                                let y = initial_scroll_offset.y + (event.pos.y - offset) / scale_y;
855                                self.clamp_child_viewport(
856                                    cx.app_state,
857                                    self.child_viewport
858                                        .with_origin(Point::new(initial_scroll_offset.x, y)),
859                                );
860                            }
861                            BarHeldState::Horizontal(offset, initial_scroll_offset) => {
862                                let scale_x = viewport_size.width / content_size.width;
863                                let x = initial_scroll_offset.x + (event.pos.x - offset) / scale_x;
864                                self.clamp_child_viewport(
865                                    cx.app_state,
866                                    self.child_viewport
867                                        .with_origin(Point::new(x, initial_scroll_offset.y)),
868                                );
869                            }
870                            BarHeldState::None => {}
871                        }
872                    } else if self.point_hits_vertical_bar(cx.app_state, pos)
873                        || self.point_hits_horizontal_bar(cx.app_state, pos)
874                    {
875                        return EventPropagation::Continue;
876                    }
877                }
878            }
879            Event::PointerLeave => {
880                self.v_handle_hover = false;
881                self.h_handle_hover = false;
882                self.v_track_hover = false;
883                self.h_track_hover = false;
884                cx.app_state.request_paint(self.id());
885            }
886            _ => {}
887        }
888        EventPropagation::Continue
889    }
890
891    fn event_after_children(
892        &mut self,
893        cx: &mut crate::context::EventCx,
894        event: &Event,
895    ) -> EventPropagation {
896        if let Event::PointerWheel(pointer_event) = &event {
897            if let Some(listener) = event.listener() {
898                if self
899                    .id
900                    .apply_event(&listener, event)
901                    .is_some_and(|prop| prop.is_processed())
902                {
903                    return EventPropagation::Stop;
904                }
905            }
906            let delta = pointer_event.delta;
907            let delta = if self.scroll_style.vertical_scroll_as_horizontal()
908                && delta.x == 0.0
909                && delta.y != 0.0
910            {
911                Vec2::new(delta.y, delta.x)
912            } else {
913                delta
914            };
915            let any_change = self.clamp_child_viewport(cx.app_state, self.child_viewport + delta);
916
917            // Check if the scroll bars now hover
918            self.update_hover_states(cx.app_state, pointer_event.pos);
919
920            return if self.scroll_style.propagate_pointer_wheel() && any_change.is_none() {
921                EventPropagation::Continue
922            } else {
923                EventPropagation::Stop
924            };
925        }
926
927        EventPropagation::Continue
928    }
929
930    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
931        cx.save();
932        let radius = match self.id.state().borrow().combined_style.get(BorderRadius) {
933            crate::unit::PxPct::Px(px) => px,
934            crate::unit::PxPct::Pct(pct) => self.total_rect.size().min_side() * (pct / 100.),
935        };
936        if self.scroll_style.overflow_clip() {
937            if radius > 0.0 {
938                let rect = self.total_rect.to_rounded_rect(radius);
939                cx.clip(&rect);
940            } else {
941                cx.clip(&self.total_rect);
942            }
943        }
944        cx.offset((-self.child_viewport.x0, -self.child_viewport.y0));
945        cx.paint_view(self.child);
946        cx.restore();
947
948        if !self.scroll_style.hide_bar() {
949            self.draw_bars(cx);
950        }
951    }
952}
953/// Represents a custom style for a `Label`.
954#[derive(Default, Debug, Clone)]
955pub struct ScrollCustomStyle(Style);
956impl From<ScrollCustomStyle> for Style {
957    fn from(value: ScrollCustomStyle) -> Self {
958        value.0
959    }
960}
961
962impl CustomStylable<ScrollCustomStyle> for Scroll {
963    type DV = Self;
964}
965
966impl ScrollCustomStyle {
967    /// Creates a new `ScrollCustomStyle`.
968    pub fn new() -> Self {
969        Self(Style::new())
970    }
971
972    /// Configures the scroll view to allow the viewport to be smaller than the inner content,
973    /// while still taking up the full available space in its container.
974    ///
975    /// Use this when you need a scroll view that can shrink its viewport size to fit within
976    /// the container, ensuring the content remains scrollable even if the inner content is
977    /// greater than the parent size.
978    ///
979    /// Internally this does a `s.min_size(0., 0.).size_full()`.
980    pub fn shrink_to_fit(mut self) -> Self {
981        self = Self(self.0.min_size(0., 0.).size_full());
982        self
983    }
984
985    /// Conditionally configures the scroll view to clip the overflow of the content.
986    pub fn overflow_clip(mut self, clip: bool) -> Self {
987        self = Self(self.0.set(OverflowClip, clip));
988        self
989    }
990
991    /// Sets the background color for the handle.
992    pub fn handle_background(mut self, color: impl Into<Brush>) -> Self {
993        self = Self(self.0.class(Handle, |s| s.background(color.into())));
994        self
995    }
996
997    /// Sets the border radius for the handle.
998    pub fn handle_border_radius(mut self, border_radius: impl Into<PxPct>) -> Self {
999        self = Self(self.0.class(Handle, |s| s.border_radius(border_radius)));
1000        self
1001    }
1002
1003    /// Sets the border color for the handle.
1004    pub fn handle_border_color(mut self, border_color: impl Into<Brush>) -> Self {
1005        self = Self(self.0.class(Handle, |s| s.border_color(border_color)));
1006        self
1007    }
1008
1009    /// Sets the border thickness for the handle.
1010    pub fn handle_border(mut self, border: impl Into<Px>) -> Self {
1011        self = Self(self.0.class(Handle, |s| s.set(Border, border)));
1012        self
1013    }
1014
1015    /// Sets whether the handle should have rounded corners.
1016    pub fn handle_rounded(mut self, rounded: impl Into<bool>) -> Self {
1017        self = Self(self.0.class(Handle, |s| s.set(Rounded, rounded)));
1018        self
1019    }
1020
1021    /// Sets the thickness of the handle.
1022    pub fn handle_thickness(mut self, thickness: impl Into<Px>) -> Self {
1023        self = Self(self.0.class(Handle, |s| s.set(Thickness, thickness)));
1024        self
1025    }
1026
1027    /// Sets the background color for the track.
1028    pub fn track_background(mut self, color: impl Into<Brush>) -> Self {
1029        self = Self(self.0.class(Track, |s| s.background(color.into())));
1030        self
1031    }
1032
1033    /// Sets the border radius for the track.
1034    pub fn track_border_radius(mut self, border_radius: impl Into<PxPct>) -> Self {
1035        self = Self(self.0.class(Track, |s| s.border_radius(border_radius)));
1036        self
1037    }
1038
1039    /// Sets the border color for the track.
1040    pub fn track_border_color(mut self, border_color: impl Into<Brush>) -> Self {
1041        self = Self(self.0.class(Track, |s| s.border_color(border_color)));
1042        self
1043    }
1044
1045    /// Sets the border thickness for the track.
1046    pub fn track_border(mut self, border: impl Into<Px>) -> Self {
1047        self = Self(self.0.class(Track, |s| s.set(Border, border)));
1048        self
1049    }
1050
1051    /// Sets whether the track should have rounded corners.
1052    pub fn track_rounded(mut self, rounded: impl Into<bool>) -> Self {
1053        self = Self(self.0.class(Track, |s| s.set(Rounded, rounded)));
1054        self
1055    }
1056
1057    /// Sets the thickness of the track.
1058    pub fn track_thickness(mut self, thickness: impl Into<Px>) -> Self {
1059        self = Self(self.0.class(Track, |s| s.set(Thickness, thickness)));
1060        self
1061    }
1062
1063    /// Sets the vertical track inset.
1064    pub fn vertical_track_inset(mut self, inset: impl Into<Px>) -> Self {
1065        self = Self(self.0.set(VerticalInset, inset));
1066        self
1067    }
1068
1069    /// Sets the horizontal track inset.
1070    pub fn horizontal_track_inset(mut self, inset: impl Into<Px>) -> Self {
1071        self = Self(self.0.set(HorizontalInset, inset));
1072        self
1073    }
1074
1075    /// Controls the visibility of the scroll bars.
1076    pub fn hide_bars(mut self, hide: impl Into<bool>) -> Self {
1077        self = Self(self.0.set(HideBars, hide));
1078        self
1079    }
1080
1081    /// Sets whether the pointer wheel events should be propagated.
1082    pub fn propagate_pointer_wheel(mut self, propagate: impl Into<bool>) -> Self {
1083        self = Self(self.0.set(PropagatePointerWheel, propagate));
1084        self
1085    }
1086
1087    /// Sets whether vertical scrolling should be interpreted as horizontal scrolling.
1088    pub fn vertical_scroll_as_horizontal(mut self, vert_as_horiz: impl Into<bool>) -> Self {
1089        self = Self(self.0.set(VerticalScrollAsHorizontal, vert_as_horiz));
1090        self
1091    }
1092}
1093
1094/// A trait that adds a `scroll` method to any type that implements `IntoView`.
1095pub trait ScrollExt {
1096    /// Wrap the view in a scroll view.
1097    fn scroll(self) -> Scroll;
1098}
1099
1100impl<T: IntoView + 'static> ScrollExt for T {
1101    fn scroll(self) -> Scroll {
1102        scroll(self)
1103    }
1104}