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