Skip to main content

floem/views/
scroll.rs

1#![deny(missing_docs)]
2//! Scroll View
3
4use floem_reactive::Effect;
5use peniko::kurbo::{Affine, Axis, Point, Rect, RoundedRect, RoundedRectRadii, Stroke, Vec2};
6use peniko::{Brush, Color};
7use std::time::Duration;
8use std::{cell::RefCell, rc::Rc};
9use taffy::Overflow;
10use ui_events::pointer::{PointerButton, PointerEvent, PointerId};
11
12use crate::easing::Linear;
13use crate::event::{
14    DragEvent, DragSourceEvent, PointerCaptureEvent, PointerScrollEventExt, RouteKind, ScrollTo,
15};
16use crate::prelude::EventListenerTrait;
17use crate::prelude::el::UpdatePhaseLayout;
18use crate::style::ScrollbarWidth;
19use crate::{
20    BoxTree, ElementId, Renderer,
21    context::{EventCx, PaintCx, StyleCx},
22    event::{Event, EventPropagation, Phase},
23    prop, prop_extractor,
24    style::{
25        Background, BorderBottomColor, BorderBottomLeftRadius, BorderBottomRightRadius,
26        BorderLeftColor, BorderRightColor, BorderTopColor, BorderTopLeftRadius,
27        BorderTopRightRadius, CustomStylable, CustomStyle, OverflowX, OverflowY, Style, StyleClass,
28    },
29    style_class,
30    unit::{Px, PxPct},
31    view::{IntoView, View},
32};
33use crate::{ViewId, custom_event};
34use understory_box_tree::NodeFlags;
35
36use super::Decorators;
37
38/// Event fired when a scroll view's scroll position changes
39///
40/// This event is fired whenever the visible viewport of the scroll view changes,
41/// either through user interaction (scrolling with mouse wheel, dragging scrollbars)
42/// or programmatic changes to the scroll offset.
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct ScrollChanged {
45    /// The scroll offset as a vector (how far scrolled from origin)
46    pub offset: Vec2,
47}
48custom_event!(ScrollChanged);
49
50#[derive(Debug, Clone, Copy)]
51enum ScrollState {
52    EnsureVisible(Rect),
53    ScrollDelta(Vec2),
54    ScrollTo(Point),
55    ScrollToPercent(f32),
56    ScrollToElement(ElementId),
57}
58
59struct ScrollEventResult {
60    propagation: EventPropagation,
61    new_offset: Option<Vec2>,
62}
63
64trait Vec2Ext {
65    /// Returns a new Vec2 with the maximum x and y components from self and other
66    fn max_by_component(self, other: Self) -> Self;
67
68    /// Returns a new Vec2 with the minimum x and y components from self and other
69    fn min_by_component(self, other: Self) -> Self;
70}
71
72impl Vec2Ext for Vec2 {
73    fn max_by_component(self, other: Self) -> Self {
74        Vec2::new(self.x.max(other.x), self.y.max(other.y))
75    }
76
77    fn min_by_component(self, other: Self) -> Self {
78        Vec2::new(self.x.min(other.x), self.y.min(other.y))
79    }
80}
81
82#[derive(Debug, Clone)]
83struct ScrollHandle {
84    element_id: ElementId,
85    box_tree: Rc<RefCell<BoxTree>>,
86    axis: Axis,
87    /// The initial pointer position when dragging started
88    style: ScrollTrackStyle,
89    initial_offset: Vec2,
90}
91
92impl ScrollHandle {
93    fn new(parent_id: ViewId, axis: Axis) -> Self {
94        let box_tree = parent_id.box_tree();
95        let element_id = parent_id.create_child_element_id(2);
96
97        Self {
98            element_id,
99            box_tree,
100            axis,
101            style: Default::default(),
102            initial_offset: Vec2::ZERO,
103        }
104    }
105
106    fn style(&mut self, cx: &mut StyleCx) {
107        let resolved =
108            cx.resolve_nested_maps(Style::new(), &[Handle::class_ref()], self.element_id);
109        if self.style.read_style_for(cx, &resolved, self.element_id) {
110            self.element_id.owning_id().request_paint();
111        }
112    }
113
114    fn event(
115        &mut self,
116        cx: &mut EventCx,
117        parent_id: ViewId,
118        child_id: ViewId,
119    ) -> ScrollEventResult {
120        match &cx.event {
121            Event::Pointer(PointerEvent::Down(e)) => {
122                if let Some(pointer_id) = e.pointer.pointer_id
123                    && e.state.buttons.contains(PointerButton::Primary)
124                {
125                    cx.window_state
126                        .set_pointer_capture(pointer_id, self.element_id);
127                }
128                cx.window_state.request_paint(parent_id);
129            }
130            Event::PointerCapture(PointerCaptureEvent::Gained(drag)) => {
131                self.initial_offset = parent_id.get_child_translation();
132                cx.start_drag(
133                    *drag,
134                    crate::event::DragConfig::new(0., Duration::ZERO, Linear),
135                    false,
136                );
137            }
138            Event::Drag(DragEvent::Source(DragSourceEvent::Move(dme))) => {
139                let pos = dme.current_state.logical_point();
140
141                // Calculate scale (content_size / viewport_size)
142                let viewport_size = parent_id
143                    .get_content_rect_local()
144                    .size()
145                    .get_coord(self.axis);
146                let content_size = child_id.get_layout_rect_local().size().get_coord(self.axis);
147                let scale = content_size / viewport_size;
148
149                let scroll_delta = (pos.get_coord(self.axis)
150                    - dme.start_state.logical_point().get_coord(self.axis))
151                    * scale;
152
153                let mut new_offset: Vec2 = self.initial_offset;
154                new_offset.set_coord(
155                    self.axis,
156                    self.initial_offset.get_coord(self.axis) + scroll_delta,
157                );
158
159                // Apply scroll
160                let viewport_size_vec = parent_id.get_content_rect_local().size();
161                let content_size_vec = child_id.get_layout_rect_local().size();
162                let max_scroll = (content_size_vec.to_vec2() - viewport_size_vec.to_vec2())
163                    .max_by_component(Vec2::ZERO);
164
165                let new_offset = new_offset
166                    .max_by_component(Vec2::ZERO)
167                    .min_by_component(max_scroll);
168
169                return ScrollEventResult {
170                    propagation: EventPropagation::Stop,
171                    new_offset: Some(new_offset),
172                };
173            }
174
175            _ => {
176                return ScrollEventResult {
177                    propagation: EventPropagation::Continue,
178                    new_offset: None,
179                };
180            }
181        }
182        ScrollEventResult {
183            propagation: EventPropagation::Stop,
184            new_offset: None,
185        }
186    }
187
188    fn set_position(
189        &mut self,
190        scroll_offset: Vec2,
191        viewport: Rect,
192        full_rect: Rect,
193        content_size: peniko::kurbo::Size,
194        scrollbar_width: f64,
195        bar_inset: f64,
196    ) {
197        let viewport_size = viewport.size().get_coord(self.axis);
198        let content_size_val = content_size.get_coord(self.axis);
199        let full_rect_size = full_rect.size().get_coord(self.axis);
200
201        // No scrollbar if content fits in viewport
202        if viewport_size >= (content_size_val - f64::EPSILON) {
203            // Hide the handle
204            self.box_tree
205                .borrow_mut()
206                .set_flags(self.element_id.0, NodeFlags::empty());
207            return;
208        }
209
210        // Calculate scrollbar handle size and position
211        let percent_visible = viewport_size / content_size_val;
212        let max_scroll = content_size_val - viewport_size;
213        let scroll_offset_val = scroll_offset.get_coord(self.axis);
214
215        let percent_scrolled = if max_scroll > 0.0 {
216            scroll_offset_val / max_scroll
217        } else {
218            0.0
219        };
220
221        let handle_length = (percent_visible * full_rect_size).ceil().max(15.);
222
223        let track_length = full_rect_size;
224        let available_travel = track_length - handle_length;
225        let handle_offset = (available_travel * percent_scrolled).ceil();
226
227        let rect = match self.axis {
228            Axis::Vertical => {
229                let x0 = full_rect.width() - scrollbar_width - bar_inset;
230                let y0 = handle_offset;
231                let x1 = full_rect.width() - bar_inset;
232                let y1 = handle_offset + handle_length;
233                Rect::new(x0, y0, x1, y1)
234            }
235            Axis::Horizontal => {
236                let x0 = handle_offset;
237                let y0 = full_rect.height() - scrollbar_width - bar_inset;
238                let x1 = handle_offset + handle_length;
239                let y1 = full_rect.height() - bar_inset;
240                Rect::new(x0, y0, x1, y1)
241            }
242        };
243
244        self.box_tree
245            .borrow_mut()
246            .set_local_bounds(self.element_id.0, rect);
247        self.box_tree
248            .borrow_mut()
249            .set_flags(self.element_id.0, NodeFlags::VISIBLE | NodeFlags::PICKABLE);
250    }
251
252    fn paint(&self, cx: &mut PaintCx) {
253        let box_tree = self.box_tree.borrow();
254        let rect = box_tree.local_bounds(self.element_id.0).unwrap_or_default();
255
256        let radius = if self.style.rounded() {
257            match self.axis {
258                Axis::Vertical => RoundedRectRadii::from_single_radius((rect.x1 - rect.x0) / 2.),
259                Axis::Horizontal => RoundedRectRadii::from_single_radius((rect.y1 - rect.y0) / 2.),
260            }
261        } else {
262            let size = rect.size().min_side();
263            let border_radius = self.style.border_radius();
264            RoundedRectRadii {
265                top_left: crate::view::border_radius(
266                    border_radius.top_left.unwrap_or(PxPct::Px(0.)),
267                    size,
268                ),
269                top_right: crate::view::border_radius(
270                    border_radius.top_right.unwrap_or(PxPct::Px(0.)),
271                    size,
272                ),
273                bottom_left: crate::view::border_radius(
274                    border_radius.bottom_left.unwrap_or(PxPct::Px(0.)),
275                    size,
276                ),
277                bottom_right: crate::view::border_radius(
278                    border_radius.bottom_right.unwrap_or(PxPct::Px(0.)),
279                    size,
280                ),
281            }
282        };
283
284        let edge_width = self.style.border().0;
285        let rect_with_border = rect.inset(-edge_width / 2.0);
286        let rounded_rect = rect_with_border.to_rounded_rect(radius);
287
288        cx.fill(
289            &rounded_rect,
290            &self.style.color().unwrap_or(HANDLE_COLOR),
291            0.0,
292        );
293
294        if edge_width > 0.0
295            && let Some(color) = self.style.border_color().right
296        {
297            cx.stroke(&rounded_rect, &color, &Stroke::new(edge_width));
298        }
299    }
300}
301
302#[derive(Debug, Clone)]
303struct ScrollTrack {
304    element_id: ElementId,
305    handle_element_id: ElementId,
306    box_tree: Rc<RefCell<BoxTree>>,
307    axis: Axis,
308    style: ScrollTrackStyle,
309}
310
311impl ScrollTrack {
312    fn new(parent_id: ViewId, handle_element_id: ElementId, axis: Axis) -> Self {
313        let box_tree = parent_id.box_tree();
314        let element_id = parent_id.create_child_element_id(1);
315
316        Self {
317            element_id,
318            handle_element_id,
319            box_tree,
320            axis,
321            style: Default::default(),
322        }
323    }
324
325    fn style(&mut self, cx: &mut StyleCx) {
326        let resolved = cx.resolve_nested_maps(Style::new(), &[Track::class_ref()], self.element_id);
327        if self.style.read_style_for(cx, &resolved, self.element_id) {
328            self.element_id.owning_id().request_paint();
329        }
330    }
331
332    fn event(
333        &mut self,
334        cx: &mut EventCx,
335        parent_id: ViewId,
336        child_id: ViewId,
337    ) -> ScrollEventResult {
338        match &cx.event {
339            Event::Pointer(PointerEvent::Down(e)) => {
340                if e.state.buttons.contains(PointerButton::Primary) {
341                    cx.window_state
342                        .set_pointer_capture(PointerId::PRIMARY, self.handle_element_id);
343                }
344                let pos = e.state.logical_point();
345
346                // Inline click_track logic
347                let viewport = parent_id.get_content_rect_local();
348                let full_rect = parent_id.get_layout_rect_local();
349                let content_size = child_id.get_layout_rect_local().size();
350
351                let pos_val = pos.get_coord(self.axis);
352                let viewport_size = viewport.size().get_coord(self.axis);
353                let content_size_val = content_size.get_coord(self.axis);
354                let full_rect_size = full_rect.size().get_coord(self.axis);
355
356                let percent_visible = viewport_size / content_size_val;
357                let handle_length = (percent_visible * full_rect_size).ceil().max(15.);
358                let max_scroll = content_size_val - viewport_size;
359
360                let track_length = full_rect_size;
361                let available_travel = track_length - handle_length;
362
363                let target_handle_offset = (pos_val - handle_length / 2.0)
364                    .max(0.0)
365                    .min(available_travel);
366                let target_percent = if available_travel > 0.0 {
367                    target_handle_offset / available_travel
368                } else {
369                    0.0
370                };
371
372                let new_offset = (target_percent * max_scroll).clamp(0.0, max_scroll);
373
374                cx.window_state.request_paint(parent_id);
375
376                let mut offset = parent_id.get_child_translation();
377                offset.set_coord(self.axis, new_offset);
378                ScrollEventResult {
379                    propagation: EventPropagation::Stop,
380                    new_offset: Some(offset),
381                }
382            }
383            _ => ScrollEventResult {
384                propagation: EventPropagation::Continue,
385                new_offset: None,
386            },
387        }
388    }
389
390    fn set_position(
391        &mut self,
392        viewport: Rect,
393        full_rect: Rect,
394        content_size: peniko::kurbo::Size,
395        scrollbar_width: f64,
396        bar_inset: f64,
397    ) {
398        let viewport_size = viewport.size().get_coord(self.axis);
399        let content_size_val = content_size.get_coord(self.axis);
400
401        // No scrollbar if content fits in viewport
402        if viewport_size >= (content_size_val - f64::EPSILON) {
403            // Hide the track
404            self.box_tree
405                .borrow_mut()
406                .set_flags(self.element_id.0, NodeFlags::empty());
407            return;
408        }
409
410        let rect = match self.axis {
411            Axis::Vertical => {
412                let x0 = full_rect.width() - scrollbar_width - bar_inset;
413                let y0 = 0.0;
414                let x1 = full_rect.width() - bar_inset;
415                let y1 = full_rect.height();
416                Rect::new(x0, y0, x1, y1)
417            }
418            Axis::Horizontal => {
419                let x0 = 0.0;
420                let y0 = full_rect.height() - scrollbar_width - bar_inset;
421                let x1 = full_rect.width();
422                let y1 = full_rect.height() - bar_inset;
423                Rect::new(x0, y0, x1, y1)
424            }
425        };
426
427        self.box_tree
428            .borrow_mut()
429            .set_local_bounds(self.element_id.0, rect);
430        self.box_tree
431            .borrow_mut()
432            .set_flags(self.element_id.0, NodeFlags::VISIBLE | NodeFlags::PICKABLE);
433    }
434
435    fn paint(&self, cx: &mut PaintCx) {
436        let box_tree = self.box_tree.borrow();
437        let rect = box_tree.local_bounds(self.element_id.0).unwrap_or_default();
438
439        if let Some(color) = self.style.color() {
440            cx.fill(&rect, &color, 0.0);
441        }
442    }
443}
444
445style_class!(
446    /// Style class that will be applied to the handles of the scroll view
447    pub Handle
448);
449style_class!(
450    /// Style class that will be applied to the scroll tracks of the scroll view
451    pub Track
452);
453
454prop!(
455    /// Determines if scroll handles should be rounded (defaults to true on macOS).
456    pub Rounded: bool {} = cfg!(target_os = "macos")
457);
458prop!(
459    /// Defines the border width of a scroll track in pixels.
460    pub Border: Px {} = Px(0.0)
461);
462
463prop_extractor! {
464    ScrollTrackStyle {
465        color: Background,
466        border_top_left_radius: BorderTopLeftRadius,
467        border_top_right_radius: BorderTopRightRadius,
468        border_bottom_left_radius: BorderBottomLeftRadius,
469        border_bottom_right_radius: BorderBottomRightRadius,
470        border_left_color: BorderLeftColor,
471        border_top_color: BorderTopColor,
472        border_right_color: BorderRightColor,
473        border_bottom_color: BorderBottomColor,
474        border: Border,
475        rounded: Rounded,
476    }
477}
478
479impl ScrollTrackStyle {
480    fn border_radius(&self) -> crate::style::BorderRadius {
481        crate::style::BorderRadius {
482            top_left: Some(self.border_top_left_radius()),
483            top_right: Some(self.border_top_right_radius()),
484            bottom_left: Some(self.border_bottom_left_radius()),
485            bottom_right: Some(self.border_bottom_right_radius()),
486        }
487    }
488
489    fn border_color(&self) -> crate::style::BorderColor {
490        crate::style::BorderColor {
491            left: self.border_left_color(),
492            top: self.border_top_color(),
493            right: self.border_right_color(),
494            bottom: self.border_bottom_color(),
495        }
496    }
497}
498
499prop!(
500    /// Specifies the vertical inset of the scrollable area in pixels.
501    pub VerticalInset: Px {} = Px(0.0)
502);
503
504prop!(
505    /// Defines the horizontal inset of the scrollable area in pixels.
506    pub HorizontalInset: Px {} = Px(0.0)
507);
508
509prop!(
510    /// Controls the visibility of scroll bars. When true, bars are hidden.
511    pub HideBars: bool {} = false
512);
513
514prop!(
515    /// Controls whether scroll bars are shown when not scrolling. When false, bars are only shown during scroll interactions.
516    pub ShowBarsWhenIdle: bool {} = true
517);
518
519prop!(
520    /// Determines if pointer wheel events should propagate to parent elements.
521    pub PropagatePointerWheel: bool {} = true
522);
523
524prop!(
525    /// When true, vertical scroll input is interpreted as horizontal scrolling.
526    pub VerticalScrollAsHorizontal: bool {} = false
527);
528
529prop_extractor!(ScrollStyle {
530    vertical_bar_inset: VerticalInset,
531    horizontal_bar_inset: HorizontalInset,
532    hide_bar: HideBars,
533    show_bars_when_idle: ShowBarsWhenIdle,
534    propagate_pointer_wheel: PropagatePointerWheel,
535    vertical_scroll_as_horizontal: VerticalScrollAsHorizontal,
536    overflow_x: OverflowX,
537    overflow_y: OverflowY,
538    scrollbar_width: ScrollbarWidth,
539});
540
541const HANDLE_COLOR: Brush = Brush::Solid(Color::from_rgba8(0, 0, 0, 120));
542
543style_class!(
544    /// Style class that is applied to every scroll view
545    pub ScrollClass
546);
547
548/// A scroll view
549pub struct Scroll {
550    id: ViewId,
551    child: ViewId,
552    // any time this changes, we must update the scroll_offset in the ViewState.
553    scroll_offset: Vec2,
554    v_handle: ScrollHandle,
555    h_handle: ScrollHandle,
556    v_track: ScrollTrack,
557    h_track: ScrollTrack,
558    scroll_style: ScrollStyle,
559}
560
561/// Create a new scroll view
562#[deprecated(since = "0.2.0", note = "Use Scroll::new() instead")]
563pub fn scroll<V: IntoView + 'static>(child: V) -> Scroll {
564    Scroll::new(child)
565}
566
567impl Scroll {
568    /// Creates a new scroll view wrapping the given child view.
569    ///
570    /// ## Example
571    /// ```rust
572    /// use floem::views::*;
573    ///
574    /// let content = Label::new("Scrollable content");
575    /// let scrollable = Scroll::new(content);
576    /// ```
577    pub fn new(child: impl IntoView) -> Self {
578        let id = ViewId::new();
579        id.register_listener(UpdatePhaseLayout::listener_key());
580
581        let child = child.into_any();
582        let child_id = child.id();
583        id.add_child(child);
584        // we need to first set the clip rect to zero so that virtual items don't set a large initial size
585        id.set_box_tree_clip(Some(RoundedRect::from_rect(Rect::ZERO, 0.)));
586
587        let v_handle = ScrollHandle::new(id, Axis::Vertical);
588        let h_handle = ScrollHandle::new(id, Axis::Horizontal);
589
590        Scroll {
591            id,
592            child: child_id,
593            scroll_offset: Vec2::ZERO,
594            v_track: ScrollTrack::new(id, v_handle.element_id, Axis::Vertical),
595            h_track: ScrollTrack::new(id, h_handle.element_id, Axis::Horizontal),
596            v_handle,
597            h_handle,
598            scroll_style: Default::default(),
599        }
600        .class(ScrollClass)
601    }
602}
603
604impl Scroll {
605    /// Ensures that a specific rectangular area is visible within the scroll view by automatically
606    /// scrolling to it if necessary.
607    ///
608    /// # Reactivity
609    /// The viewport will automatically update to include the target rectangle whenever the rectangle's
610    /// position or size changes, as determined by the `to` function which will update any time there are
611    /// changes in the signals that it depends on.
612    pub fn ensure_visible(self, to: impl Fn() -> Rect + 'static) -> Self {
613        let id = self.id();
614        Effect::new(move |_| {
615            let rect = to();
616            id.update_state_deferred(ScrollState::EnsureVisible(rect));
617        });
618
619        self
620    }
621
622    /// Scrolls the view by the specified delta vector.
623    ///
624    /// # Reactivity
625    /// The scroll position will automatically update whenever the delta vector changes,
626    /// as determined by the `delta` function which will update any time there are changes in the signals that it depends on.
627    pub fn scroll_delta(self, delta: impl Fn() -> Vec2 + 'static) -> Self {
628        let id = self.id();
629        Effect::new(move |_| {
630            let delta = delta();
631            id.update_state(ScrollState::ScrollDelta(delta));
632        });
633
634        self
635    }
636
637    /// Scrolls the view to the specified target point.
638    ///
639    /// # Reactivity
640    /// The scroll position will automatically update whenever the target point changes,
641    /// as determined by the `origin` function which will update any time there are changes in the signals that it depends on.
642    pub fn scroll_to(self, origin: impl Fn() -> Option<Point> + 'static) -> Self {
643        let id = self.id();
644        Effect::new(move |_| {
645            if let Some(origin) = origin() {
646                id.update_state_deferred(ScrollState::ScrollTo(origin));
647            }
648        });
649
650        self
651    }
652
653    /// Scrolls the view to the specified percentage (0-100) of its scrollable content.
654    ///
655    /// # Reactivity
656    /// The scroll position will automatically update whenever the target percentage changes,
657    /// as determined by the `percent` function which will update any time there are changes in the signals that it depends on.
658    pub fn scroll_to_percent(self, percent: impl Fn() -> f32 + 'static) -> Self {
659        let id = self.id();
660        Effect::new(move |_| {
661            let percent = percent() / 100.;
662            id.update_state_deferred(ScrollState::ScrollToPercent(percent));
663        });
664        self
665    }
666
667    /// Scrolls the view to make a specific view visible.
668    ///
669    /// # Reactivity
670    /// The scroll position will automatically update whenever the target view changes,
671    /// as determined by the `view` function which will update any time there are changes in the signals that it depends on.
672    pub fn scroll_to_view(self, view: impl Fn() -> Option<ViewId> + 'static) -> Self {
673        let id = self.id();
674        Effect::new(move |_| {
675            if let Some(view) = view() {
676                id.update_state_deferred(ScrollState::ScrollToElement(view.get_element_id()));
677            }
678        });
679
680        self
681    }
682}
683
684/// internal methods
685impl Scroll {
686    /// this applies a delta, set the viewport in the window state and returns the delta that was actually applied
687    ///
688    /// If the delta is positive, the view will scroll down, negative will scroll up.
689    fn apply_scroll_delta(&mut self, delta: Vec2) -> Option<Vec2> {
690        let viewport_size = self.id.get_content_rect_local().size();
691        let content_size = self.child.get_layout_rect_local().size();
692
693        // Calculate max scroll based on overflow settings
694        let mut max_scroll =
695            (content_size.to_vec2() - viewport_size.to_vec2()).max_by_component(Vec2::ZERO);
696
697        // Zero out scroll in axes that aren't scrollable
698        let can_scroll_x = matches!(self.scroll_style.overflow_x(), taffy::Overflow::Scroll);
699        let can_scroll_y = matches!(self.scroll_style.overflow_y(), taffy::Overflow::Scroll);
700
701        let mut new_scroll_offset = self.scroll_offset + delta;
702        if !can_scroll_x {
703            new_scroll_offset.x = 0.0;
704            max_scroll.x = 0.0;
705        }
706        if !can_scroll_y {
707            new_scroll_offset.y = 0.0;
708            max_scroll.y = 0.0;
709        }
710
711        let old_scroll_offset = self.scroll_offset;
712        self.scroll_offset = new_scroll_offset
713            .max_by_component(Vec2::ZERO)
714            .min_by_component(max_scroll);
715        let change = self.id.set_child_translation(self.scroll_offset);
716        if change {
717            self.id.route_event(
718                Event::new_custom(ScrollChanged {
719                    offset: self.scroll_offset,
720                }),
721                RouteKind::Directed {
722                    target: self.id.get_element_id(),
723                    phases: crate::context::Phases::TARGET,
724                },
725            );
726        }
727
728        if change {
729            self.set_positions();
730            Some(self.scroll_offset - old_scroll_offset)
731        } else {
732            None
733        }
734    }
735
736    /// Scroll to a specific offset position.
737    ///
738    /// Sets the scroll offset to the given point, clamping to valid scroll bounds.
739    /// The offset represents how much content has scrolled out of view at the top-left.
740    ///
741    /// # Arguments
742    /// * `offset` - The desired scroll offset. Will be clamped to valid range [0, max_scroll]
743    fn do_scroll_to(&mut self, offset: Point) {
744        self.apply_scroll_delta(offset.to_vec2() - self.scroll_offset);
745    }
746
747    /// Ensure that an entire area is visible in the scroll view.
748    ///
749    /// Scrolls the minimum distance necessary to make the entire rect visible.
750    /// If the rect is larger than the viewport, prioritizes showing the top-left.
751    ///
752    /// # Arguments
753    /// * `rect` - The rectangle in content coordinates (relative to the child's layout)
754    pub fn do_ensure_visible(&mut self, rect: Rect) {
755        let viewport = self.id.get_content_rect_local();
756        let viewport_size = viewport.size();
757
758        // Calculate the rect's position relative to current scroll position
759        let visible_rect = Rect::from_origin_size(self.scroll_offset.to_point(), viewport_size);
760
761        // If rect is already fully visible, no need to scroll
762        if visible_rect.contains_rect(rect) {
763            return;
764        }
765
766        let mut new_offset = self.scroll_offset;
767
768        // Scroll horizontally if needed
769        if rect.width() > viewport_size.width {
770            // Rect is wider than viewport - show left edge
771            new_offset.x = rect.x0;
772        } else if rect.x0 < visible_rect.x0 {
773            // Rect is cut off on left - scroll left
774            new_offset.x = rect.x0;
775        } else if rect.x1 > visible_rect.x1 {
776            // Rect is cut off on right - scroll right
777            new_offset.x = rect.x1 - viewport_size.width;
778        }
779
780        // Scroll vertically if needed
781        if rect.height() > viewport_size.height {
782            // Rect is taller than viewport - show top edge
783            new_offset.y = rect.y0;
784        } else if rect.y0 < visible_rect.y0 {
785            // Rect is cut off on top - scroll up
786            new_offset.y = rect.y0;
787        } else if rect.y1 > visible_rect.y1 {
788            // Rect is cut off on bottom - scroll down
789            new_offset.y = rect.y1 - viewport_size.height;
790        }
791
792        self.do_scroll_to(new_offset.to_point());
793    }
794
795    fn do_scroll_to_element(&mut self, scroll_to: ScrollTo) -> EventPropagation {
796        let child_element_id = self.child.get_element_id();
797        let box_tree = self.id.box_tree();
798        let mut box_tree = box_tree.borrow_mut();
799
800        let Some(target_local_rect) = scroll_to
801            .rect
802            .or_else(|| box_tree.local_bounds(scroll_to.id.0))
803        else {
804            return EventPropagation::Continue;
805        };
806
807        let target_transform = box_tree
808            .get_or_compute_world_transform(scroll_to.id.0)
809            .unwrap_or(Affine::IDENTITY);
810        let child_transform = box_tree
811            .get_or_compute_world_transform(child_element_id.0)
812            .unwrap_or(Affine::IDENTITY);
813
814        let target_world_rect = target_transform.transform_rect_bbox(target_local_rect);
815        let child_world_origin = child_transform * Point::ZERO;
816
817        let target_rect = Rect::new(
818            target_world_rect.x0 - child_world_origin.x,
819            target_world_rect.y0 - child_world_origin.y,
820            target_world_rect.x1 - child_world_origin.x,
821            target_world_rect.y1 - child_world_origin.y,
822        );
823        drop(box_tree);
824
825        self.do_ensure_visible(target_rect);
826
827        let viewport_size = self.id.get_content_rect_local().size();
828        let visible_rect = Rect::from_origin_size(self.scroll_offset.to_point(), viewport_size);
829
830        if visible_rect.contains_rect(target_rect) {
831            EventPropagation::Stop
832        } else {
833            EventPropagation::Continue
834        }
835    }
836
837    fn set_positions(&mut self) {
838        let viewport = self.id.get_content_rect_local();
839        let full_rect = self.id.get_layout_rect_local();
840        let content_size = self.child.get_layout_rect_local().size();
841        let scrollbar_width = self.scroll_style.scrollbar_width().0;
842        let v_bar_inset = self.scroll_style.vertical_bar_inset().0;
843        let h_bar_inset = self.scroll_style.horizontal_bar_inset().0;
844
845        self.v_track.set_position(
846            viewport,
847            full_rect,
848            content_size,
849            scrollbar_width,
850            v_bar_inset,
851        );
852        self.h_track.set_position(
853            viewport,
854            full_rect,
855            content_size,
856            scrollbar_width,
857            h_bar_inset,
858        );
859
860        self.v_handle.set_position(
861            self.scroll_offset,
862            viewport,
863            full_rect,
864            content_size,
865            scrollbar_width,
866            v_bar_inset,
867        );
868        self.h_handle.set_position(
869            self.scroll_offset,
870            viewport,
871            full_rect,
872            content_size,
873            scrollbar_width,
874            h_bar_inset,
875        );
876    }
877}
878
879impl View for Scroll {
880    fn id(&self) -> ViewId {
881        self.id
882    }
883
884    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
885        "Scroll".into()
886    }
887
888    fn view_style(&self) -> Option<Style> {
889        Some(
890            Style::new()
891                .items_start()
892                .overflow_x(Overflow::Scroll)
893                .overflow_y(Overflow::Scroll),
894        )
895    }
896
897    fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
898        if let Ok(state) = state.downcast::<ScrollState>() {
899            match *state {
900                ScrollState::EnsureVisible(rect) => {
901                    self.do_ensure_visible(rect);
902                }
903                ScrollState::ScrollDelta(delta) => {
904                    self.apply_scroll_delta(delta);
905                }
906                ScrollState::ScrollTo(origin) => {
907                    self.do_scroll_to(origin);
908                }
909                ScrollState::ScrollToPercent(percent) => {
910                    let content_size = self.child.get_layout_rect_local().size();
911                    let viewport_size = self.id.get_content_rect_local().size();
912
913                    // Calculate max scroll (content size - viewport size)
914                    let max_scroll = (content_size.to_vec2() - viewport_size.to_vec2())
915                        .max_by_component(Vec2::ZERO);
916
917                    // Apply percentage to max scroll
918                    let target_offset = max_scroll * (percent as f64);
919
920                    self.do_scroll_to(target_offset.to_point());
921                }
922                ScrollState::ScrollToElement(id) => {
923                    self.do_scroll_to_element(ScrollTo { id, rect: None });
924                }
925            }
926            self.id.request_box_tree_update_for_view();
927        }
928    }
929
930    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
931        self.scroll_style.read(cx);
932
933        // If the reason implies nested style maps must be resolved, restyle everything.
934        if cx.reason.needs_resolve_nested_maps() {
935            self.v_handle.style(cx);
936            self.h_handle.style(cx);
937            self.v_track.style(cx);
938            self.h_track.style(cx);
939            return;
940        }
941
942        for (element_id, _reason) in cx.targeted_elements.clone() {
943            if element_id == self.v_handle.element_id {
944                self.v_handle.style(cx);
945            } else if element_id == self.h_handle.element_id {
946                self.h_handle.style(cx);
947            } else if element_id == self.v_track.element_id {
948                self.v_track.style(cx);
949            } else if element_id == self.h_track.element_id {
950                self.h_track.style(cx);
951            }
952        }
953    }
954
955    fn event(&mut self, cx: &mut EventCx) -> EventPropagation {
956        // in order to use this we had to set `id.has_layout_listener`.
957        if UpdatePhaseLayout::extract(&cx.event).is_some() {
958            self.set_positions();
959            return EventPropagation::Stop;
960        }
961
962        if let Some(scroll_to) = ScrollTo::extract(&cx.event) {
963            return self.do_scroll_to_element(*scroll_to);
964        }
965        // Handle events targeted at our visual IDs (handles and tracks)
966        if cx.phase == Phase::Target {
967            if cx.target == self.v_handle.element_id {
968                let result = self.v_handle.event(cx, self.id, self.child);
969                if let Some(new_offset) = result.new_offset
970                    && self
971                        .apply_scroll_delta(new_offset - self.scroll_offset)
972                        .is_some()
973                {
974                    cx.window_state.request_paint(self.id);
975                }
976                return result.propagation;
977            }
978            if cx.target == self.h_handle.element_id {
979                let result = self.h_handle.event(cx, self.id, self.child);
980                if let Some(new_offset) = result.new_offset
981                    && self
982                        .apply_scroll_delta(new_offset - self.scroll_offset)
983                        .is_some()
984                {
985                    cx.window_state.request_paint(self.id);
986                }
987                return result.propagation;
988            }
989            if cx.target == self.v_track.element_id {
990                let result = self.v_track.event(cx, self.id, self.child);
991                if let Some(new_offset) = result.new_offset
992                    && self
993                        .apply_scroll_delta(new_offset - self.scroll_offset)
994                        .is_some()
995                {
996                    cx.window_state.request_paint(self.id);
997                }
998                return result.propagation;
999            }
1000            if cx.target == self.h_track.element_id {
1001                let result = self.h_track.event(cx, self.id, self.child);
1002                if let Some(new_offset) = result.new_offset
1003                    && self
1004                        .apply_scroll_delta(new_offset - self.scroll_offset)
1005                        .is_some()
1006                {
1007                    cx.window_state.request_paint(self.id);
1008                }
1009                return result.propagation;
1010            }
1011        }
1012
1013        // Handle scroll wheel events in bubble phase
1014        if let Event::Pointer(PointerEvent::Scroll(pse)) = &cx.event {
1015            let size = self.id.get_layout_rect_local().size();
1016            let delta = pse.resolve_to_points(None, Some(size));
1017            let delta = -if self.scroll_style.vertical_scroll_as_horizontal()
1018                && delta.x == 0.0
1019                && delta.y != 0.0
1020            {
1021                Vec2::new(delta.y, delta.x)
1022            } else {
1023                delta
1024            };
1025
1026            let change = self.apply_scroll_delta(delta);
1027
1028            if change.is_some() {
1029                cx.window_state.request_paint(self.id);
1030            }
1031
1032            return if self.scroll_style.propagate_pointer_wheel() && change.is_none() {
1033                EventPropagation::Continue
1034            } else {
1035                EventPropagation::Stop
1036            };
1037        }
1038
1039        EventPropagation::Continue
1040    }
1041
1042    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
1043        // this apply scroll delta of zero is cheap.
1044        // it is here in the case that the available delta changed, this will catch it and update it to a better size
1045        self.apply_scroll_delta(Vec2::ZERO);
1046
1047        // Check which visual node we're painting
1048        // Scroll view creates multiple visual IDs for scrollbars/tracks
1049        if cx.target_id == self.id.get_element_id() {
1050            // Main scroll container - children painted automatically by traversal
1051        } else if cx.target_id == self.v_handle.element_id {
1052            // Painting vertical scrollbar handle
1053            if !self.scroll_style.hide_bar() && (self.scroll_style.show_bars_when_idle()) {
1054                self.v_handle.paint(cx);
1055            }
1056        } else if cx.target_id == self.h_handle.element_id {
1057            // Painting horizontal scrollbar handle
1058            if !self.scroll_style.hide_bar() && (self.scroll_style.show_bars_when_idle()) {
1059                self.h_handle.paint(cx);
1060            }
1061        } else if cx.target_id == self.v_track.element_id {
1062            // Painting vertical scrollbar track
1063            if !self.scroll_style.hide_bar() && (self.scroll_style.show_bars_when_idle()) {
1064                self.v_track.paint(cx);
1065            }
1066        } else if cx.target_id == self.h_track.element_id {
1067            // Painting horizontal scrollbar track
1068            if !self.scroll_style.hide_bar() && (self.scroll_style.show_bars_when_idle()) {
1069                self.h_track.paint(cx);
1070            }
1071        }
1072    }
1073}
1074/// Represents a custom style for a `Scroll`.
1075#[derive(Default, Debug, Clone)]
1076pub struct ScrollCustomStyle(Style);
1077impl From<ScrollCustomStyle> for Style {
1078    fn from(value: ScrollCustomStyle) -> Self {
1079        value.0
1080    }
1081}
1082impl From<Style> for ScrollCustomStyle {
1083    fn from(value: Style) -> Self {
1084        Self(value)
1085    }
1086}
1087impl CustomStyle for ScrollCustomStyle {
1088    type StyleClass = ScrollClass;
1089}
1090
1091impl CustomStylable<ScrollCustomStyle> for Scroll {
1092    type DV = Self;
1093}
1094
1095impl ScrollCustomStyle {
1096    /// Creates a new `ScrollCustomStyle`.
1097    pub fn new() -> Self {
1098        Self(Style::new())
1099    }
1100
1101    /// Configures the scroll view to allow the viewport to be smaller than the inner content,
1102    /// while still taking up the full available space in its container.
1103    ///
1104    /// Use this when you need a scroll view that can shrink its viewport size to fit within
1105    /// the container, ensuring the content remains scrollable even if the inner content is
1106    /// greater than the parent size.
1107    ///
1108    /// Internally this does a `s.min_size(0., 0.).size_full()`.
1109    pub fn shrink_to_fit(mut self) -> Self {
1110        self = Self(
1111            self.0
1112                .min_size(0., 0.)
1113                .size_full()
1114                .flex_grow(1.)
1115                .flex_basis(0.),
1116        );
1117        self
1118    }
1119
1120    /// Sets the background color for the handle.
1121    pub fn handle_background(mut self, color: impl Into<Brush>) -> Self {
1122        self = Self(self.0.class(Handle, |s| s.background(color.into())));
1123        self
1124    }
1125
1126    /// Sets the border radius for the handle.
1127    pub fn handle_border_radius(mut self, border_radius: impl Into<PxPct>) -> Self {
1128        self = Self(self.0.class(Handle, |s| s.border_radius(border_radius)));
1129        self
1130    }
1131
1132    /// Sets the border color for the handle.
1133    pub fn handle_border_color(mut self, border_color: impl Into<Brush>) -> Self {
1134        self = Self(self.0.class(Handle, |s| s.border_color(border_color)));
1135        self
1136    }
1137
1138    /// Sets the border thickness for the handle.
1139    pub fn handle_border(mut self, border: impl Into<Px>) -> Self {
1140        self = Self(self.0.class(Handle, |s| s.set(Border, border)));
1141        self
1142    }
1143
1144    /// Sets whether the handle should have rounded corners.
1145    pub fn handle_rounded(mut self, rounded: impl Into<bool>) -> Self {
1146        self = Self(self.0.class(Handle, |s| s.set(Rounded, rounded)));
1147        self
1148    }
1149
1150    /// Sets the background color for the track.
1151    pub fn track_background(mut self, color: impl Into<Brush>) -> Self {
1152        self = Self(self.0.class(Track, |s| s.background(color.into())));
1153        self
1154    }
1155
1156    /// Sets the border radius for the track.
1157    pub fn track_border_radius(mut self, border_radius: impl Into<PxPct>) -> Self {
1158        self = Self(self.0.class(Track, |s| s.border_radius(border_radius)));
1159        self
1160    }
1161
1162    /// Sets the border color for the track.
1163    pub fn track_border_color(mut self, border_color: impl Into<Brush>) -> Self {
1164        self = Self(self.0.class(Track, |s| s.border_color(border_color)));
1165        self
1166    }
1167
1168    /// Sets the border thickness for the track.
1169    pub fn track_border(mut self, border: impl Into<Px>) -> Self {
1170        self = Self(self.0.class(Track, |s| s.set(Border, border)));
1171        self
1172    }
1173
1174    /// Sets whether the track should have rounded corners.
1175    pub fn track_rounded(mut self, rounded: impl Into<bool>) -> Self {
1176        self = Self(self.0.class(Track, |s| s.set(Rounded, rounded)));
1177        self
1178    }
1179
1180    /// Sets the vertical track inset.
1181    pub fn vertical_track_inset(mut self, inset: impl Into<Px>) -> Self {
1182        self = Self(self.0.set(VerticalInset, inset));
1183        self
1184    }
1185
1186    /// Sets the horizontal track inset.
1187    pub fn horizontal_track_inset(mut self, inset: impl Into<Px>) -> Self {
1188        self = Self(self.0.set(HorizontalInset, inset));
1189        self
1190    }
1191
1192    /// Controls the visibility of the scroll bars.
1193    pub fn hide_bars(mut self, hide: impl Into<bool>) -> Self {
1194        self = Self(self.0.set(HideBars, hide));
1195        self
1196    }
1197
1198    /// Sets whether the pointer wheel events should be propagated.
1199    pub fn propagate_pointer_wheel(mut self, propagate: impl Into<bool>) -> Self {
1200        self = Self(self.0.set(PropagatePointerWheel, propagate));
1201        self
1202    }
1203
1204    /// Sets whether vertical scrolling should be interpreted as horizontal scrolling.
1205    pub fn vertical_scroll_as_horizontal(mut self, vert_as_horiz: impl Into<bool>) -> Self {
1206        self = Self(self.0.set(VerticalScrollAsHorizontal, vert_as_horiz));
1207        self
1208    }
1209
1210    /// Controls whether scroll bars are shown when not scrolling. When false, bars are only shown during scroll interactions.
1211    pub fn show_bars_when_idle(mut self, show: impl Into<bool>) -> Self {
1212        self = Self(self.0.set(ShowBarsWhenIdle, show));
1213        self
1214    }
1215}
1216
1217/// A trait that adds a `scroll` method to any type that implements `IntoView`.
1218pub trait ScrollExt {
1219    /// Wrap the view in a scroll view.
1220    fn scroll(self) -> Scroll;
1221}
1222
1223impl<T: IntoView + 'static> ScrollExt for T {
1224    fn scroll(self) -> Scroll {
1225        Scroll::new(self)
1226    }
1227}