egui/containers/
scroll_area.rs

1//! See [`ScrollArea`] for docs.
2
3#![expect(clippy::needless_range_loop)]
4
5use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
6
7use emath::GuiRounding as _;
8use epaint::{Color32, Direction, Margin, Shape};
9
10use crate::{
11    Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
12    UiKind, UiStackInfo, Vec2, Vec2b, WidgetInfo, emath, epaint, lerp, pass_state, pos2, remap,
13    remap_clamp,
14};
15
16#[derive(Clone, Copy, Debug)]
17#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
18struct ScrollingToTarget {
19    animation_time_span: (f64, f64),
20    target_offset: f32,
21}
22
23#[derive(Clone, Copy, Debug)]
24#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
25#[cfg_attr(feature = "serde", serde(default))]
26pub struct State {
27    /// Positive offset means scrolling down/right
28    pub offset: Vec2,
29
30    /// If set, quickly but smoothly scroll to this target offset.
31    offset_target: [Option<ScrollingToTarget>; 2],
32
33    /// Were the scroll bars visible last frame?
34    show_scroll: Vec2b,
35
36    /// The content were to large to fit large frame.
37    content_is_too_large: Vec2b,
38
39    /// Did the user interact (hover or drag) the scroll bars last frame?
40    scroll_bar_interaction: Vec2b,
41
42    /// Momentum, used for kinetic scrolling
43    #[cfg_attr(feature = "serde", serde(skip))]
44    vel: Vec2,
45
46    /// Mouse offset relative to the top of the handle when started moving the handle.
47    scroll_start_offset_from_top_left: [Option<f32>; 2],
48
49    /// Is the scroll sticky. This is true while scroll handle is in the end position
50    /// and remains that way until the user moves the `scroll_handle`. Once unstuck (false)
51    /// it remains false until the scroll touches the end position, which reenables stickiness.
52    scroll_stuck_to_end: Vec2b,
53
54    /// Area that can be dragged. This is the size of the content from the last frame.
55    interact_rect: Option<Rect>,
56}
57
58impl Default for State {
59    fn default() -> Self {
60        Self {
61            offset: Vec2::ZERO,
62            offset_target: Default::default(),
63            show_scroll: Vec2b::FALSE,
64            content_is_too_large: Vec2b::FALSE,
65            scroll_bar_interaction: Vec2b::FALSE,
66            vel: Vec2::ZERO,
67            scroll_start_offset_from_top_left: [None; 2],
68            scroll_stuck_to_end: Vec2b::TRUE,
69            interact_rect: None,
70        }
71    }
72}
73
74impl State {
75    pub fn load(ctx: &Context, id: Id) -> Option<Self> {
76        ctx.data_mut(|d| d.get_persisted(id))
77    }
78
79    pub fn store(self, ctx: &Context, id: Id) {
80        ctx.data_mut(|d| d.insert_persisted(id, self));
81    }
82
83    /// Get the current kinetic scrolling velocity.
84    pub fn velocity(&self) -> Vec2 {
85        self.vel
86    }
87}
88
89pub struct ScrollAreaOutput<R> {
90    /// What the user closure returned.
91    pub inner: R,
92
93    /// [`Id`] of the [`ScrollArea`].
94    pub id: Id,
95
96    /// The current state of the scroll area.
97    pub state: State,
98
99    /// The size of the content. If this is larger than [`Self::inner_rect`],
100    /// then there was need for scrolling.
101    pub content_size: Vec2,
102
103    /// Where on the screen the content is (excludes scroll bars).
104    pub inner_rect: Rect,
105}
106
107/// Indicate whether the horizontal and vertical scroll bars must be always visible, hidden or visible when needed.
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
110pub enum ScrollBarVisibility {
111    /// Hide scroll bar even if they are needed.
112    ///
113    /// You can still scroll, with the scroll-wheel
114    /// and by dragging the contents, but there is no
115    /// visual indication of how far you have scrolled.
116    AlwaysHidden,
117
118    /// Show scroll bars only when the content size exceeds the container,
119    /// i.e. when there is any need to scroll.
120    ///
121    /// This is the default.
122    VisibleWhenNeeded,
123
124    /// Always show the scroll bar, even if the contents fit in the container
125    /// and there is no need to scroll.
126    AlwaysVisible,
127}
128
129impl Default for ScrollBarVisibility {
130    #[inline]
131    fn default() -> Self {
132        Self::VisibleWhenNeeded
133    }
134}
135
136impl ScrollBarVisibility {
137    pub const ALL: [Self; 3] = [
138        Self::AlwaysHidden,
139        Self::VisibleWhenNeeded,
140        Self::AlwaysVisible,
141    ];
142}
143
144/// What is the source of scrolling for a [`ScrollArea`].
145#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
147pub struct ScrollSource {
148    /// Scroll the area by dragging a scroll bar.
149    ///
150    /// By default the scroll bars remain visible to show current position.
151    /// To hide them use [`ScrollArea::scroll_bar_visibility()`].
152    pub scroll_bar: bool,
153
154    /// Scroll the area by dragging the contents.
155    pub drag: bool,
156
157    /// Scroll the area by scrolling (or shift scrolling) the mouse wheel with
158    /// the mouse cursor over the [`ScrollArea`].
159    pub mouse_wheel: bool,
160}
161
162impl Default for ScrollSource {
163    fn default() -> Self {
164        Self::ALL
165    }
166}
167
168impl ScrollSource {
169    pub const NONE: Self = Self {
170        scroll_bar: false,
171        drag: false,
172        mouse_wheel: false,
173    };
174    pub const ALL: Self = Self {
175        scroll_bar: true,
176        drag: true,
177        mouse_wheel: true,
178    };
179    pub const SCROLL_BAR: Self = Self {
180        scroll_bar: true,
181        drag: false,
182        mouse_wheel: false,
183    };
184    pub const DRAG: Self = Self {
185        scroll_bar: false,
186        drag: true,
187        mouse_wheel: false,
188    };
189    pub const MOUSE_WHEEL: Self = Self {
190        scroll_bar: false,
191        drag: false,
192        mouse_wheel: true,
193    };
194
195    /// Is everything disabled?
196    #[inline]
197    pub fn is_none(&self) -> bool {
198        self == &Self::NONE
199    }
200
201    /// Is anything enabled?
202    #[inline]
203    pub fn any(&self) -> bool {
204        self.scroll_bar | self.drag | self.mouse_wheel
205    }
206
207    /// Is everything enabled?
208    #[inline]
209    pub fn is_all(&self) -> bool {
210        self.scroll_bar & self.drag & self.mouse_wheel
211    }
212}
213
214impl BitOr for ScrollSource {
215    type Output = Self;
216
217    #[inline]
218    fn bitor(self, rhs: Self) -> Self::Output {
219        Self {
220            scroll_bar: self.scroll_bar | rhs.scroll_bar,
221            drag: self.drag | rhs.drag,
222            mouse_wheel: self.mouse_wheel | rhs.mouse_wheel,
223        }
224    }
225}
226
227#[expect(clippy::suspicious_arithmetic_impl)]
228impl Add for ScrollSource {
229    type Output = Self;
230
231    #[inline]
232    fn add(self, rhs: Self) -> Self::Output {
233        self | rhs
234    }
235}
236
237impl BitOrAssign for ScrollSource {
238    #[inline]
239    fn bitor_assign(&mut self, rhs: Self) {
240        *self = *self | rhs;
241    }
242}
243
244impl AddAssign for ScrollSource {
245    #[inline]
246    fn add_assign(&mut self, rhs: Self) {
247        *self = *self + rhs;
248    }
249}
250
251/// Add vertical and/or horizontal scrolling to a contained [`Ui`].
252///
253/// By default, scroll bars only show up when needed, i.e. when the contents
254/// is larger than the container.
255/// This is controlled by [`Self::scroll_bar_visibility`].
256///
257/// There are two flavors of scroll areas: solid and floating.
258/// Solid scroll bars use up space, reducing the amount of space available
259/// to the contents. Floating scroll bars float on top of the contents, covering it.
260/// You can change the scroll style by changing the [`crate::style::Spacing::scroll`].
261///
262/// ### Coordinate system
263/// * content: size of contents (generally large; that's why we want scroll bars)
264/// * outer: size of scroll area including scroll bar(s)
265/// * inner: excluding scroll bar(s). The area we clip the contents to. Includes `content_margin`.
266///
267/// If the floating scroll bars settings is turned on then `inner == outer`.
268///
269/// ## Example
270/// ```
271/// # egui::__run_test_ui(|ui| {
272/// egui::ScrollArea::vertical().show(ui, |ui| {
273///     // Add a lot of widgets here.
274/// });
275/// # });
276/// ```
277///
278/// You can scroll to an element using [`crate::Response::scroll_to_me`], [`Ui::scroll_to_cursor`] and [`Ui::scroll_to_rect`].
279///
280/// ## See also
281/// If you want to allow zooming, use [`crate::Scene`].
282#[derive(Clone, Debug)]
283#[must_use = "You should call .show()"]
284pub struct ScrollArea {
285    /// Do we have horizontal/vertical scrolling enabled?
286    direction_enabled: Vec2b,
287
288    auto_shrink: Vec2b,
289    max_size: Vec2,
290    min_scrolled_size: Vec2,
291    scroll_bar_visibility: ScrollBarVisibility,
292    scroll_bar_rect: Option<Rect>,
293    id_salt: Option<Id>,
294    offset_x: Option<f32>,
295    offset_y: Option<f32>,
296    on_hover_cursor: Option<CursorIcon>,
297    on_drag_cursor: Option<CursorIcon>,
298    scroll_source: ScrollSource,
299    wheel_scroll_multiplier: Vec2,
300
301    content_margin: Option<Margin>,
302
303    /// If true for vertical or horizontal the scroll wheel will stick to the
304    /// end position until user manually changes position. It will become true
305    /// again once scroll handle makes contact with end.
306    stick_to_end: Vec2b,
307
308    /// If false, `scroll_to_*` functions will not be animated
309    animated: bool,
310}
311
312impl ScrollArea {
313    /// Create a horizontal scroll area.
314    #[inline]
315    pub fn horizontal() -> Self {
316        Self::new([true, false])
317    }
318
319    /// Create a vertical scroll area.
320    #[inline]
321    pub fn vertical() -> Self {
322        Self::new([false, true])
323    }
324
325    /// Create a bi-directional (horizontal and vertical) scroll area.
326    #[inline]
327    pub fn both() -> Self {
328        Self::new([true, true])
329    }
330
331    /// Create a scroll area where both direction of scrolling is disabled.
332    /// It's unclear why you would want to do this.
333    #[inline]
334    pub fn neither() -> Self {
335        Self::new([false, false])
336    }
337
338    /// Create a scroll area where you decide which axis has scrolling enabled.
339    /// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling.
340    pub fn new(direction_enabled: impl Into<Vec2b>) -> Self {
341        Self {
342            direction_enabled: direction_enabled.into(),
343            auto_shrink: Vec2b::TRUE,
344            max_size: Vec2::INFINITY,
345            min_scrolled_size: Vec2::splat(64.0),
346            scroll_bar_visibility: Default::default(),
347            scroll_bar_rect: None,
348            id_salt: None,
349            offset_x: None,
350            offset_y: None,
351            on_hover_cursor: None,
352            on_drag_cursor: None,
353            scroll_source: ScrollSource::default(),
354            wheel_scroll_multiplier: Vec2::splat(1.0),
355            content_margin: None,
356            stick_to_end: Vec2b::FALSE,
357            animated: true,
358        }
359    }
360
361    /// The maximum width of the outer frame of the scroll area.
362    ///
363    /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default).
364    ///
365    /// See also [`Self::auto_shrink`].
366    #[inline]
367    pub fn max_width(mut self, max_width: f32) -> Self {
368        self.max_size.x = max_width;
369        self
370    }
371
372    /// The maximum height of the outer frame of the scroll area.
373    ///
374    /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default).
375    ///
376    /// See also [`Self::auto_shrink`].
377    #[inline]
378    pub fn max_height(mut self, max_height: f32) -> Self {
379        self.max_size.y = max_height;
380        self
381    }
382
383    /// The minimum width of a horizontal scroll area which requires scroll bars.
384    ///
385    /// The [`ScrollArea`] will only become smaller than this if the content is smaller than this
386    /// (and so we don't require scroll bars).
387    ///
388    /// Default: `64.0`.
389    #[inline]
390    pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self {
391        self.min_scrolled_size.x = min_scrolled_width;
392        self
393    }
394
395    /// The minimum height of a vertical scroll area which requires scroll bars.
396    ///
397    /// The [`ScrollArea`] will only become smaller than this if the content is smaller than this
398    /// (and so we don't require scroll bars).
399    ///
400    /// Default: `64.0`.
401    #[inline]
402    pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self {
403        self.min_scrolled_size.y = min_scrolled_height;
404        self
405    }
406
407    /// Set the visibility of both horizontal and vertical scroll bars.
408    ///
409    /// With `ScrollBarVisibility::VisibleWhenNeeded` (default), the scroll bar will be visible only when needed.
410    #[inline]
411    pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self {
412        self.scroll_bar_visibility = scroll_bar_visibility;
413        self
414    }
415
416    /// Specify within which screen-space rectangle to show the scroll bars.
417    ///
418    /// This can be used to move the scroll bars to a smaller region of the `ScrollArea`,
419    /// for instance if you are painting a sticky header on top of it.
420    #[inline]
421    pub fn scroll_bar_rect(mut self, scroll_bar_rect: Rect) -> Self {
422        self.scroll_bar_rect = Some(scroll_bar_rect);
423        self
424    }
425
426    /// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`.
427    #[inline]
428    #[deprecated = "Renamed id_salt"]
429    pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
430        self.id_salt(id_salt)
431    }
432
433    /// A source for the unique [`Id`], e.g. `.id_salt("second_scroll_area")` or `.id_salt(loop_index)`.
434    #[inline]
435    pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
436        self.id_salt = Some(Id::new(id_salt));
437        self
438    }
439
440    /// Set the horizontal and vertical scroll offset position.
441    ///
442    /// Positive offset means scrolling down/right.
443    ///
444    /// See also: [`Self::vertical_scroll_offset`], [`Self::horizontal_scroll_offset`],
445    /// [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
446    /// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
447    #[inline]
448    pub fn scroll_offset(mut self, offset: Vec2) -> Self {
449        self.offset_x = Some(offset.x);
450        self.offset_y = Some(offset.y);
451        self
452    }
453
454    /// Set the vertical scroll offset position.
455    ///
456    /// Positive offset means scrolling down.
457    ///
458    /// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
459    /// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
460    #[inline]
461    pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
462        self.offset_y = Some(offset);
463        self
464    }
465
466    /// Set the horizontal scroll offset position.
467    ///
468    /// Positive offset means scrolling right.
469    ///
470    /// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
471    /// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
472    #[inline]
473    pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
474        self.offset_x = Some(offset);
475        self
476    }
477
478    /// Set the cursor used when the mouse pointer is hovering over the [`ScrollArea`].
479    ///
480    /// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
481    ///
482    /// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
483    /// override this setting.
484    #[inline]
485    pub fn on_hover_cursor(mut self, cursor: CursorIcon) -> Self {
486        self.on_hover_cursor = Some(cursor);
487        self
488    }
489
490    /// Set the cursor used when the [`ScrollArea`] is being dragged.
491    ///
492    /// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
493    ///
494    /// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
495    /// override this setting.
496    #[inline]
497    pub fn on_drag_cursor(mut self, cursor: CursorIcon) -> Self {
498        self.on_drag_cursor = Some(cursor);
499        self
500    }
501
502    /// Turn on/off scrolling on the horizontal axis.
503    #[inline]
504    pub fn hscroll(mut self, hscroll: bool) -> Self {
505        self.direction_enabled[0] = hscroll;
506        self
507    }
508
509    /// Turn on/off scrolling on the vertical axis.
510    #[inline]
511    pub fn vscroll(mut self, vscroll: bool) -> Self {
512        self.direction_enabled[1] = vscroll;
513        self
514    }
515
516    /// Turn on/off scrolling on the horizontal/vertical axes.
517    ///
518    /// You can pass in `false`, `true`, `[false, true]` etc.
519    #[inline]
520    pub fn scroll(mut self, direction_enabled: impl Into<Vec2b>) -> Self {
521        self.direction_enabled = direction_enabled.into();
522        self
523    }
524
525    /// Control the scrolling behavior.
526    ///
527    /// * If `true` (default), the scroll area will respond to user scrolling.
528    /// * If `false`, the scroll area will not respond to user scrolling.
529    ///
530    /// This can be used, for example, to optionally freeze scrolling while the user
531    /// is typing text in a [`crate::TextEdit`] widget contained within the scroll area.
532    ///
533    /// This controls both scrolling directions.
534    #[deprecated = "Use `ScrollArea::scroll_source()"]
535    #[inline]
536    pub fn enable_scrolling(mut self, enable: bool) -> Self {
537        self.scroll_source = if enable {
538            ScrollSource::ALL
539        } else {
540            ScrollSource::NONE
541        };
542        self
543    }
544
545    /// Can the user drag the scroll area to scroll?
546    ///
547    /// This is useful for touch screens.
548    ///
549    /// If `true`, the [`ScrollArea`] will sense drags.
550    ///
551    /// Default: `true`.
552    #[deprecated = "Use `ScrollArea::scroll_source()"]
553    #[inline]
554    pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
555        self.scroll_source.drag = drag_to_scroll;
556        self
557    }
558
559    /// What sources does the [`ScrollArea`] use for scrolling the contents.
560    #[inline]
561    pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self {
562        self.scroll_source = scroll_source;
563        self
564    }
565
566    /// The scroll amount caused by a mouse wheel scroll is multiplied by this amount.
567    ///
568    /// Independent for each scroll direction. Defaults to `Vec2{x: 1.0, y: 1.0}`.
569    ///
570    /// This can invert or effectively disable mouse scrolling.
571    #[inline]
572    pub fn wheel_scroll_multiplier(mut self, multiplier: Vec2) -> Self {
573        self.wheel_scroll_multiplier = multiplier;
574        self
575    }
576
577    /// For each axis, should the containing area shrink if the content is small?
578    ///
579    /// * If `true`, egui will add blank space outside the scroll area.
580    /// * If `false`, egui will add blank space inside the scroll area.
581    ///
582    /// Default: `true`.
583    #[inline]
584    pub fn auto_shrink(mut self, auto_shrink: impl Into<Vec2b>) -> Self {
585        self.auto_shrink = auto_shrink.into();
586        self
587    }
588
589    /// Should the scroll area animate `scroll_to_*` functions?
590    ///
591    /// Default: `true`.
592    #[inline]
593    pub fn animated(mut self, animated: bool) -> Self {
594        self.animated = animated;
595        self
596    }
597
598    /// Is any scrolling enabled?
599    pub(crate) fn is_any_scroll_enabled(&self) -> bool {
600        self.direction_enabled[0] || self.direction_enabled[1]
601    }
602
603    /// Extra margin added around the contents.
604    ///
605    /// The scroll bars will be either on top of this margin, or outside of it,
606    /// depending on the value of [`crate::style::ScrollStyle::floating`].
607    ///
608    /// Default: [`crate::style::ScrollStyle::content_margin`].
609    #[inline]
610    pub fn content_margin(mut self, margin: impl Into<Margin>) -> Self {
611        self.content_margin = Some(margin.into());
612        self
613    }
614
615    /// The scroll handle will stick to the rightmost position even while the content size
616    /// changes dynamically. This can be useful to simulate text scrollers coming in from right
617    /// hand side. The scroll handle remains stuck until user manually changes position. Once "unstuck"
618    /// it will remain focused on whatever content viewport the user left it on. If the scroll
619    /// handle is dragged all the way to the right it will again become stuck and remain there
620    /// until manually pulled from the end position.
621    #[inline]
622    pub fn stick_to_right(mut self, stick: bool) -> Self {
623        self.stick_to_end[0] = stick;
624        self
625    }
626
627    /// The scroll handle will stick to the bottom position even while the content size
628    /// changes dynamically. This can be useful to simulate terminal UIs or log/info scrollers.
629    /// The scroll handle remains stuck until user manually changes position. Once "unstuck"
630    /// it will remain focused on whatever content viewport the user left it on. If the scroll
631    /// handle is dragged to the bottom it will again become stuck and remain there until manually
632    /// pulled from the end position.
633    #[inline]
634    pub fn stick_to_bottom(mut self, stick: bool) -> Self {
635        self.stick_to_end[1] = stick;
636        self
637    }
638}
639
640struct Prepared {
641    id: Id,
642    state: State,
643
644    auto_shrink: Vec2b,
645
646    /// Does this `ScrollArea` have horizontal/vertical scrolling enabled?
647    direction_enabled: Vec2b,
648
649    /// Smoothly interpolated boolean of whether or not to show the scroll bars.
650    show_bars_factor: Vec2,
651
652    /// How much horizontal and vertical space are used up by the
653    /// width of the vertical bar, and the height of the horizontal bar?
654    ///
655    /// This is always zero for floating scroll bars.
656    ///
657    /// Note that this is a `yx` swizzling of [`Self::show_bars_factor`]
658    /// times the maximum bar with.
659    /// That's because horizontal scroll uses up vertical space,
660    /// and vice versa.
661    current_bar_use: Vec2,
662
663    scroll_bar_visibility: ScrollBarVisibility,
664    scroll_bar_rect: Option<Rect>,
665
666    /// Where on the screen the content is (excludes scroll bars; includes `content_margin`).
667    inner_rect: Rect,
668
669    content_ui: Ui,
670
671    /// Relative coordinates: the offset and size of the view of the inner UI.
672    /// `viewport.min == ZERO` means we scrolled to the top.
673    viewport: Rect,
674
675    scroll_source: ScrollSource,
676    wheel_scroll_multiplier: Vec2,
677    stick_to_end: Vec2b,
678
679    /// If there was a scroll target before the [`ScrollArea`] was added this frame, it's
680    /// not for us to handle so we save it and restore it after this [`ScrollArea`] is done.
681    saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
682
683    /// The response from dragging the background (if enabled)
684    background_drag_response: Option<Response>,
685
686    animated: bool,
687}
688
689impl ScrollArea {
690    fn begin(self, ui: &mut Ui) -> Prepared {
691        let Self {
692            direction_enabled,
693            auto_shrink,
694            max_size,
695            min_scrolled_size,
696            scroll_bar_visibility,
697            scroll_bar_rect,
698            id_salt,
699            offset_x,
700            offset_y,
701            on_hover_cursor,
702            on_drag_cursor,
703            scroll_source,
704            wheel_scroll_multiplier,
705            content_margin: _, // Used elsewhere
706            stick_to_end,
707            animated,
708        } = self;
709
710        let ctx = ui.ctx().clone();
711
712        let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
713        let id = ui.make_persistent_id(id_salt);
714        ctx.check_for_id_clash(
715            id,
716            Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO),
717            "ScrollArea",
718        );
719        let mut state = State::load(&ctx, id).unwrap_or_default();
720
721        state.offset.x = offset_x.unwrap_or(state.offset.x);
722        state.offset.y = offset_y.unwrap_or(state.offset.y);
723
724        let show_bars: Vec2b = match scroll_bar_visibility {
725            ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
726            ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
727            ScrollBarVisibility::AlwaysVisible => direction_enabled,
728        };
729
730        let show_bars_factor = Vec2::new(
731            ctx.animate_bool_responsive(id.with("h"), show_bars[0]),
732            ctx.animate_bool_responsive(id.with("v"), show_bars[1]),
733        );
734
735        let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width();
736
737        let available_outer = ui.available_rect_before_wrap();
738
739        let outer_size = available_outer.size().at_most(max_size);
740
741        let inner_size = {
742            let mut inner_size = outer_size - current_bar_use;
743
744            // Don't go so far that we shrink to zero.
745            // In particular, if we put a [`ScrollArea`] inside of a [`ScrollArea`], the inner
746            // one shouldn't collapse into nothingness.
747            // See https://github.com/emilk/egui/issues/1097
748            for d in 0..2 {
749                if direction_enabled[d] {
750                    inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
751                }
752            }
753            inner_size
754        };
755
756        let inner_rect = Rect::from_min_size(available_outer.min, inner_size);
757
758        let mut content_max_size = inner_size;
759
760        if true {
761            // Tell the inner Ui to *try* to fit the content without needing to scroll,
762            // i.e. better to wrap text and shrink images than showing a horizontal scrollbar!
763        } else {
764            // Tell the inner Ui to use as much space as possible, we can scroll to see it!
765            for d in 0..2 {
766                if direction_enabled[d] {
767                    content_max_size[d] = f32::INFINITY;
768                }
769            }
770        }
771
772        let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
773
774        // Round to pixels to avoid widgets appearing to "float" when scrolling fractional amounts:
775        let content_max_rect = content_max_rect
776            .round_to_pixels(ui.pixels_per_point())
777            .round_ui();
778
779        let mut content_ui = ui.new_child(
780            UiBuilder::new()
781                .ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
782                .max_rect(content_max_rect),
783        );
784
785        {
786            // Clip the content, but only when we really need to:
787            let clip_rect_margin = ui.visuals().clip_rect_margin;
788            let mut content_clip_rect = ui.clip_rect();
789            for d in 0..2 {
790                if direction_enabled[d] {
791                    content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
792                    content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
793                } else {
794                    // Nice handling of forced resizing beyond the possible:
795                    content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
796                }
797            }
798            // Make sure we didn't accidentally expand the clip rect
799            content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
800            content_ui.set_clip_rect(content_clip_rect);
801        }
802
803        let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
804        let dt = ui.input(|i| i.stable_dt).at_most(0.1);
805
806        let background_drag_response =
807            if scroll_source.drag && ui.is_enabled() && state.content_is_too_large.any() {
808                // Drag contents to scroll (for touch screens mostly).
809                // We must do this BEFORE adding content to the `ScrollArea`,
810                // or we will steal input from the widgets we contain.
811                let content_response_option = state
812                    .interact_rect
813                    .map(|rect| ui.interact(rect, id.with("area"), Sense::DRAG));
814
815                if content_response_option
816                    .as_ref()
817                    .is_some_and(|response| response.dragged())
818                {
819                    for d in 0..2 {
820                        if direction_enabled[d] {
821                            ui.input(|input| {
822                                state.offset[d] -= input.pointer.delta()[d];
823                            });
824                            state.scroll_stuck_to_end[d] = false;
825                            state.offset_target[d] = None;
826                        }
827                    }
828                } else {
829                    // Apply the cursor velocity to the scroll area when the user releases the drag.
830                    if content_response_option
831                        .as_ref()
832                        .is_some_and(|response| response.drag_stopped())
833                    {
834                        state.vel = direction_enabled.to_vec2()
835                            * ui.input(|input| input.pointer.velocity());
836                    }
837                    for d in 0..2 {
838                        // Kinetic scrolling
839                        let stop_speed = 20.0; // Pixels per second.
840                        let friction_coeff = 1000.0; // Pixels per second squared.
841
842                        let friction = friction_coeff * dt;
843                        if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
844                            state.vel[d] = 0.0;
845                        } else {
846                            state.vel[d] -= friction * state.vel[d].signum();
847                            // Offset has an inverted coordinate system compared to
848                            // the velocity, so we subtract it instead of adding it
849                            state.offset[d] -= state.vel[d] * dt;
850                            ctx.request_repaint();
851                        }
852                    }
853                }
854
855                // Set the desired mouse cursors.
856                if let Some(response) = &content_response_option {
857                    if response.dragged()
858                        && let Some(cursor) = on_drag_cursor
859                    {
860                        ui.set_cursor_icon(cursor);
861                    } else if response.hovered()
862                        && let Some(cursor) = on_hover_cursor
863                    {
864                        ui.set_cursor_icon(cursor);
865                    }
866                }
867
868                content_response_option
869            } else {
870                None
871            };
872
873        // Scroll with an animation if we have a target offset (that hasn't been cleared by the code
874        // above).
875        for d in 0..2 {
876            if let Some(scroll_target) = state.offset_target[d] {
877                state.vel[d] = 0.0;
878
879                if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
880                    // Arrived
881                    state.offset[d] = scroll_target.target_offset;
882                    state.offset_target[d] = None;
883                } else {
884                    // Move towards target
885                    let t = emath::interpolation_factor(
886                        scroll_target.animation_time_span,
887                        ui.input(|i| i.time),
888                        dt,
889                        emath::ease_in_ease_out,
890                    );
891                    if t < 1.0 {
892                        state.offset[d] =
893                            emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
894                        ctx.request_repaint();
895                    } else {
896                        // Arrived
897                        state.offset[d] = scroll_target.target_offset;
898                        state.offset_target[d] = None;
899                    }
900                }
901            }
902        }
903
904        let saved_scroll_target = content_ui
905            .ctx()
906            .pass_state_mut(|state| std::mem::take(&mut state.scroll_target));
907
908        Prepared {
909            id,
910            state,
911            auto_shrink,
912            direction_enabled,
913            show_bars_factor,
914            current_bar_use,
915            scroll_bar_visibility,
916            scroll_bar_rect,
917            inner_rect,
918            content_ui,
919            viewport,
920            scroll_source,
921            wheel_scroll_multiplier,
922            stick_to_end,
923            saved_scroll_target,
924            background_drag_response,
925            animated,
926        }
927    }
928
929    /// Show the [`ScrollArea`], and add the contents to the viewport.
930    ///
931    /// If the inner area can be very long, consider using [`Self::show_rows`] instead.
932    pub fn show<R>(
933        self,
934        ui: &mut Ui,
935        add_contents: impl FnOnce(&mut Ui) -> R,
936    ) -> ScrollAreaOutput<R> {
937        self.show_viewport_dyn(ui, Box::new(|ui, _viewport| add_contents(ui)))
938    }
939
940    /// Efficiently show only the visible part of a large number of rows.
941    ///
942    /// ```
943    /// # egui::__run_test_ui(|ui| {
944    /// let text_style = egui::TextStyle::Body;
945    /// let row_height = ui.text_style_height(&text_style);
946    /// // let row_height = ui.spacing().interact_size.y; // if you are adding buttons instead of labels.
947    /// let total_rows = 10_000;
948    /// egui::ScrollArea::vertical().show_rows(ui, row_height, total_rows, |ui, row_range| {
949    ///     for row in row_range {
950    ///         let text = format!("Row {}/{}", row + 1, total_rows);
951    ///         ui.label(text);
952    ///     }
953    /// });
954    /// # });
955    /// ```
956    pub fn show_rows<R>(
957        self,
958        ui: &mut Ui,
959        row_height_sans_spacing: f32,
960        total_rows: usize,
961        add_contents: impl FnOnce(&mut Ui, std::ops::Range<usize>) -> R,
962    ) -> ScrollAreaOutput<R> {
963        let spacing = ui.spacing().item_spacing;
964        let row_height_with_spacing = row_height_sans_spacing + spacing.y;
965        self.show_viewport(ui, |ui, viewport| {
966            ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0));
967
968            let mut min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
969            let mut max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
970            if max_row > total_rows {
971                let diff = max_row.saturating_sub(min_row);
972                max_row = total_rows;
973                min_row = total_rows.saturating_sub(diff);
974            }
975
976            let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing;
977            let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing;
978
979            let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
980
981            ui.scope_builder(UiBuilder::new().max_rect(rect), |viewport_ui| {
982                viewport_ui.skip_ahead_auto_ids(min_row); // Make sure we get consistent IDs.
983                add_contents(viewport_ui, min_row..max_row)
984            })
985            .inner
986        })
987    }
988
989    /// This can be used to only paint the visible part of the contents.
990    ///
991    /// `add_contents` is given the viewport rectangle, which is the relative view of the content.
992    /// So if the passed rect has min = zero, then show the top left content (the user has not scrolled).
993    pub fn show_viewport<R>(
994        self,
995        ui: &mut Ui,
996        add_contents: impl FnOnce(&mut Ui, Rect) -> R,
997    ) -> ScrollAreaOutput<R> {
998        self.show_viewport_dyn(ui, Box::new(add_contents))
999    }
1000
1001    fn show_viewport_dyn<'c, R>(
1002        self,
1003        ui: &mut Ui,
1004        add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
1005    ) -> ScrollAreaOutput<R> {
1006        let margin = self
1007            .content_margin
1008            .unwrap_or_else(|| ui.spacing().scroll.content_margin);
1009
1010        let mut prepared = self.begin(ui);
1011        let id = prepared.id;
1012        let inner_rect = prepared.inner_rect;
1013
1014        let inner = crate::Frame::NONE
1015            .inner_margin(margin)
1016            .show(&mut prepared.content_ui, |ui| {
1017                add_contents(ui, prepared.viewport)
1018            })
1019            .inner;
1020
1021        let (content_size, state) = prepared.end(ui);
1022        let output = ScrollAreaOutput {
1023            inner,
1024            id,
1025            state,
1026            content_size,
1027            inner_rect,
1028        };
1029
1030        paint_fade_areas(ui, &output);
1031
1032        output
1033    }
1034}
1035
1036impl Prepared {
1037    /// Returns content size and state
1038    fn end(self, ui: &mut Ui) -> (Vec2, State) {
1039        let Self {
1040            id,
1041            mut state,
1042            inner_rect,
1043            auto_shrink,
1044            direction_enabled,
1045            mut show_bars_factor,
1046            current_bar_use,
1047            scroll_bar_visibility,
1048            scroll_bar_rect,
1049            content_ui,
1050            viewport: _,
1051            scroll_source,
1052            wheel_scroll_multiplier,
1053            stick_to_end,
1054            saved_scroll_target,
1055            background_drag_response,
1056            animated,
1057        } = self;
1058
1059        let content_size = content_ui.min_size();
1060
1061        let scroll_delta = content_ui
1062            .ctx()
1063            .pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
1064
1065        for d in 0..2 {
1066            // PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it.
1067            let mut delta = -scroll_delta.0[d];
1068            let mut animation = scroll_delta.1;
1069
1070            // We always take both scroll targets regardless of which scroll axes are enabled. This
1071            // is to avoid them leaking to other scroll areas.
1072            let scroll_target = content_ui
1073                .ctx()
1074                .pass_state_mut(|state| state.scroll_target[d].take());
1075
1076            if direction_enabled[d] {
1077                if let Some(target) = scroll_target {
1078                    let pass_state::ScrollTarget {
1079                        range,
1080                        align,
1081                        animation: animation_update,
1082                    } = target;
1083                    let min = content_ui.min_rect().min[d];
1084                    let clip_rect = content_ui.clip_rect();
1085                    let visible_range = min..=min + clip_rect.size()[d];
1086                    let (start, end) = (range.min, range.max);
1087                    let clip_start = clip_rect.min[d];
1088                    let clip_end = clip_rect.max[d];
1089                    let mut spacing = content_ui.spacing().item_spacing[d];
1090
1091                    let delta_update = if let Some(align) = align {
1092                        let center_factor = align.to_factor();
1093
1094                        let offset =
1095                            lerp(range, center_factor) - lerp(visible_range, center_factor);
1096
1097                        // Depending on the alignment we need to add or subtract the spacing
1098                        spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
1099
1100                        offset + spacing - state.offset[d]
1101                    } else if start < clip_start && end < clip_end {
1102                        -(clip_start - start + spacing).min(clip_end - end - spacing)
1103                    } else if end > clip_end && start > clip_start {
1104                        (end - clip_end + spacing).min(start - clip_start - spacing)
1105                    } else {
1106                        // Ui is already in view, no need to adjust scroll.
1107                        0.0
1108                    };
1109
1110                    delta += delta_update;
1111                    animation = animation_update;
1112                }
1113
1114                if delta != 0.0 {
1115                    let target_offset = state.offset[d] + delta;
1116
1117                    if !animated {
1118                        state.offset[d] = target_offset;
1119                    } else if let Some(animation) = &mut state.offset_target[d] {
1120                        // For instance: the user is continuously calling `ui.scroll_to_cursor`,
1121                        // so we don't want to reset the animation, but perhaps update the target:
1122                        animation.target_offset = target_offset;
1123                    } else {
1124                        // The further we scroll, the more time we take.
1125                        let now = ui.input(|i| i.time);
1126                        let animation_duration = (delta.abs() / animation.points_per_second)
1127                            .clamp(animation.duration.min, animation.duration.max);
1128                        state.offset_target[d] = Some(ScrollingToTarget {
1129                            animation_time_span: (now, now + animation_duration as f64),
1130                            target_offset,
1131                        });
1132                    }
1133                    ui.request_repaint();
1134                }
1135            }
1136        }
1137
1138        // Restore scroll target meant for ScrollAreas up the stack (if any)
1139        ui.ctx().pass_state_mut(|state| {
1140            for d in 0..2 {
1141                if saved_scroll_target[d].is_some() {
1142                    state.scroll_target[d] = saved_scroll_target[d].clone();
1143                }
1144            }
1145        });
1146
1147        let inner_rect = {
1148            // At this point this is the available size for the inner rect.
1149            let mut inner_size = inner_rect.size();
1150
1151            for d in 0..2 {
1152                inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
1153                    (true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small
1154                    (true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space
1155                    (false, true) => content_size[d], // Follow the content (expand/contract to fit it).
1156                    (false, false) => inner_size[d].max(content_size[d]), // Expand to fit content
1157                };
1158            }
1159
1160            Rect::from_min_size(inner_rect.min, inner_size)
1161        };
1162
1163        let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
1164
1165        let content_is_too_large = Vec2b::new(
1166            direction_enabled[0] && inner_rect.width() < content_size.x,
1167            direction_enabled[1] && inner_rect.height() < content_size.y,
1168        );
1169
1170        let max_offset = content_size - inner_rect.size();
1171
1172        // Drag-to-scroll?
1173        let is_dragging_background = background_drag_response
1174            .as_ref()
1175            .is_some_and(|r| r.dragged());
1176
1177        let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
1178            && ui.ctx().dragged_id().is_none()
1179            || is_dragging_background;
1180
1181        if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
1182            let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
1183                && direction_enabled[0] != direction_enabled[1];
1184            for d in 0..2 {
1185                if direction_enabled[d] {
1186                    let scroll_delta = ui.input(|input| {
1187                        if always_scroll_enabled_direction {
1188                            // no bidirectional scrolling; allow horizontal scrolling without pressing shift
1189                            input.smooth_scroll_delta()[0] + input.smooth_scroll_delta()[1]
1190                        } else {
1191                            input.smooth_scroll_delta()[d]
1192                        }
1193                    });
1194                    let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
1195
1196                    let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
1197                    let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
1198
1199                    if scrolling_up || scrolling_down {
1200                        state.offset[d] -= scroll_delta;
1201
1202                        // Clear scroll delta so no parent scroll will use it:
1203                        ui.input_mut(|input| {
1204                            if always_scroll_enabled_direction {
1205                                input.smooth_scroll_delta = Vec2::ZERO;
1206                            } else {
1207                                input.smooth_scroll_delta[d] = 0.0;
1208                            }
1209                        });
1210
1211                        state.scroll_stuck_to_end[d] = false;
1212                        state.offset_target[d] = None;
1213                    }
1214                }
1215            }
1216        }
1217
1218        let show_scroll_this_frame = match scroll_bar_visibility {
1219            ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
1220            ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
1221            ScrollBarVisibility::AlwaysVisible => direction_enabled,
1222        };
1223
1224        // Avoid frame delay; start showing scroll bar right away:
1225        if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 {
1226            show_bars_factor.x = ui.ctx().animate_bool_responsive(id.with("h"), true);
1227        }
1228        if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 {
1229            show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
1230        }
1231
1232        let scroll_style = ui.spacing().scroll;
1233
1234        // Paint the bars:
1235        let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
1236        for d in 0..2 {
1237            // maybe force increase in offset to keep scroll stuck to end position
1238            if stick_to_end[d] && state.scroll_stuck_to_end[d] {
1239                state.offset[d] = content_size[d] - inner_rect.size()[d];
1240            }
1241
1242            let show_factor = show_bars_factor[d];
1243            if show_factor == 0.0 {
1244                state.scroll_bar_interaction[d] = false;
1245                continue;
1246            }
1247
1248            let interact_id = id.with(d);
1249
1250            // Margin on either side of the scroll bar:
1251            let inner_margin = show_factor * scroll_style.bar_inner_margin;
1252            let outer_margin = show_factor * scroll_style.bar_outer_margin;
1253
1254            // bottom of a horizontal scroll (d==0).
1255            // right of a vertical scroll (d==1).
1256            let mut max_cross = outer_rect.max[1 - d] - outer_margin;
1257
1258            if ui.clip_rect().max[1 - d] - outer_margin < max_cross {
1259                // Move the scrollbar so it is visible. This is needed in some cases.
1260                // For instance:
1261                // * When we have a vertical-only scroll area in a top level panel,
1262                //   and that panel is not wide enough for the contents.
1263                // * When one ScrollArea is nested inside another, and the outer
1264                //   is scrolled so that the scroll-bars of the inner ScrollArea (us)
1265                //   is outside the clip rectangle.
1266                // Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
1267                // clip_rect_margin is quite a hack. It would be nice to get rid of it.
1268                max_cross = ui.clip_rect().max[1 - d] - outer_margin;
1269            }
1270
1271            let full_width = scroll_style.bar_width;
1272
1273            // The bounding rect of a fully visible bar.
1274            // When we hover this area, we should show the full bar:
1275            let max_bar_rect = if d == 0 {
1276                outer_rect.with_min_y(max_cross - full_width)
1277            } else {
1278                outer_rect.with_min_x(max_cross - full_width)
1279            };
1280
1281            let sense = if scroll_source.scroll_bar && ui.is_enabled() {
1282                Sense::CLICK | Sense::DRAG
1283            } else {
1284                Sense::hover()
1285            };
1286
1287            // We always sense interaction with the full width, even if we antimate it growing/shrinking.
1288            // This is to present a more consistent target for our hit test code,
1289            // and to avoid producing jitter in "thin widget" heuristics there.
1290            // Also: it make sense to detect any hover where the scroll bar _will_ be.
1291            let response = ui.interact(max_bar_rect, interact_id, sense);
1292
1293            response.widget_info(|| WidgetInfo::new(crate::WidgetType::ScrollBar));
1294
1295            // top/bottom of a horizontal scroll (d==0).
1296            // left/rigth of a vertical scroll (d==1).
1297            let cross = if scroll_style.floating {
1298                let is_hovering_bar_area = response.hovered() || state.scroll_bar_interaction[d];
1299
1300                let is_hovering_bar_area_t = ui
1301                    .ctx()
1302                    .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
1303
1304                let width = show_factor
1305                    * lerp(
1306                        scroll_style.floating_width..=full_width,
1307                        is_hovering_bar_area_t,
1308                    );
1309
1310                let min_cross = max_cross - width;
1311                Rangef::new(min_cross, max_cross)
1312            } else {
1313                let min_cross = inner_rect.max[1 - d] + inner_margin;
1314                Rangef::new(min_cross, max_cross)
1315            };
1316
1317            let outer_scroll_bar_rect = if d == 0 {
1318                Rect::from_x_y_ranges(scroll_bar_rect.x_range(), cross)
1319            } else {
1320                Rect::from_x_y_ranges(cross, scroll_bar_rect.y_range())
1321            };
1322
1323            let from_content = |content| {
1324                remap_clamp(
1325                    content,
1326                    0.0..=content_size[d],
1327                    scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
1328                )
1329            };
1330
1331            let calculate_handle_rect = |d, offset: &Vec2| {
1332                let handle_size = if d == 0 {
1333                    from_content(offset.x + inner_rect.width()) - from_content(offset.x)
1334                } else {
1335                    from_content(offset.y + inner_rect.height()) - from_content(offset.y)
1336                }
1337                .max(scroll_style.handle_min_length);
1338
1339                let handle_start_point = remap_clamp(
1340                    offset[d],
1341                    0.0..=max_offset[d],
1342                    scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_size),
1343                );
1344
1345                if d == 0 {
1346                    Rect::from_min_max(
1347                        pos2(handle_start_point, cross.min),
1348                        pos2(handle_start_point + handle_size, cross.max),
1349                    )
1350                } else {
1351                    Rect::from_min_max(
1352                        pos2(cross.min, handle_start_point),
1353                        pos2(cross.max, handle_start_point + handle_size),
1354                    )
1355                }
1356            };
1357
1358            let handle_rect = calculate_handle_rect(d, &state.offset);
1359
1360            state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
1361
1362            if let Some(pointer_pos) = response.interact_pointer_pos() {
1363                let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
1364                    .get_or_insert_with(|| {
1365                        if handle_rect.contains(pointer_pos) {
1366                            pointer_pos[d] - handle_rect.min[d]
1367                        } else {
1368                            let handle_top_pos_at_bottom =
1369                                scroll_bar_rect.max[d] - handle_rect.size()[d];
1370                            // Calculate the new handle top position, centering the handle on the mouse.
1371                            let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
1372                                .clamp(scroll_bar_rect.min[d], handle_top_pos_at_bottom);
1373                            pointer_pos[d] - new_handle_top_pos
1374                        }
1375                    });
1376
1377                let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
1378                let handle_travel =
1379                    scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]);
1380                state.offset[d] = if handle_travel.start() == handle_travel.end() {
1381                    0.0
1382                } else {
1383                    remap(new_handle_top, handle_travel, 0.0..=max_offset[d])
1384                };
1385
1386                // some manual action taken, scroll not stuck
1387                state.scroll_stuck_to_end[d] = false;
1388                state.offset_target[d] = None;
1389            } else {
1390                state.scroll_start_offset_from_top_left[d] = None;
1391            }
1392
1393            let unbounded_offset = state.offset[d];
1394            state.offset[d] = state.offset[d].max(0.0);
1395            state.offset[d] = state.offset[d].min(max_offset[d]);
1396
1397            if state.offset[d] != unbounded_offset {
1398                state.vel[d] = 0.0;
1399            }
1400
1401            if ui.is_rect_visible(outer_scroll_bar_rect) {
1402                // Avoid frame-delay by calculating a new handle rect:
1403                let handle_rect = calculate_handle_rect(d, &state.offset);
1404
1405                let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
1406                    // Pick visuals based on interaction with the handle.
1407                    // Remember that the response is for the whole scroll bar!
1408                    let is_hovering_handle = response.hovered()
1409                        && ui.input(|i| {
1410                            i.pointer
1411                                .latest_pos()
1412                                .is_some_and(|p| handle_rect.contains(p))
1413                        });
1414                    let visuals = ui.visuals();
1415                    if response.is_pointer_button_down_on() {
1416                        &visuals.widgets.active
1417                    } else if is_hovering_handle {
1418                        &visuals.widgets.hovered
1419                    } else {
1420                        &visuals.widgets.inactive
1421                    }
1422                } else {
1423                    &ui.visuals().widgets.inactive
1424                };
1425
1426                let handle_opacity = if scroll_style.floating {
1427                    if response.hovered() || response.dragged() {
1428                        scroll_style.interact_handle_opacity
1429                    } else {
1430                        let is_hovering_outer_rect_t = ui.ctx().animate_bool_responsive(
1431                            id.with((d, "is_hovering_outer_rect")),
1432                            is_hovering_outer_rect,
1433                        );
1434                        lerp(
1435                            scroll_style.dormant_handle_opacity
1436                                ..=scroll_style.active_handle_opacity,
1437                            is_hovering_outer_rect_t,
1438                        )
1439                    }
1440                } else {
1441                    1.0
1442                };
1443
1444                let background_opacity = if scroll_style.floating {
1445                    if response.hovered() || response.dragged() {
1446                        scroll_style.interact_background_opacity
1447                    } else if is_hovering_outer_rect {
1448                        scroll_style.active_background_opacity
1449                    } else {
1450                        scroll_style.dormant_background_opacity
1451                    }
1452                } else {
1453                    1.0
1454                };
1455
1456                let handle_color = if scroll_style.foreground_color {
1457                    visuals.fg_stroke.color
1458                } else {
1459                    visuals.bg_fill
1460                };
1461
1462                // Background:
1463                ui.painter().add(epaint::Shape::rect_filled(
1464                    outer_scroll_bar_rect,
1465                    visuals.corner_radius,
1466                    ui.visuals()
1467                        .extreme_bg_color
1468                        .gamma_multiply(background_opacity),
1469                ));
1470
1471                // Handle:
1472                ui.painter().add(epaint::Shape::rect_filled(
1473                    handle_rect,
1474                    visuals.corner_radius,
1475                    handle_color.gamma_multiply(handle_opacity),
1476                ));
1477            }
1478        }
1479
1480        ui.advance_cursor_after_rect(outer_rect);
1481
1482        if show_scroll_this_frame != state.show_scroll {
1483            ui.request_repaint();
1484        }
1485
1486        let available_offset = content_size - inner_rect.size();
1487        state.offset = state.offset.min(available_offset);
1488        state.offset = state.offset.max(Vec2::ZERO);
1489
1490        // Is scroll handle at end of content, or is there no scrollbar
1491        // yet (not enough content), but sticking is requested? If so, enter sticky mode.
1492        // Only has an effect if stick_to_end is enabled but we save in
1493        // state anyway so that entering sticky mode at an arbitrary time
1494        // has appropriate effect.
1495        state.scroll_stuck_to_end = Vec2b::new(
1496            (state.offset[0] == available_offset[0])
1497                || (self.stick_to_end[0] && available_offset[0] < 0.0),
1498            (state.offset[1] == available_offset[1])
1499                || (self.stick_to_end[1] && available_offset[1] < 0.0),
1500        );
1501
1502        state.show_scroll = show_scroll_this_frame;
1503        state.content_is_too_large = content_is_too_large;
1504        state.interact_rect = Some(inner_rect);
1505
1506        state.store(ui.ctx(), id);
1507
1508        (content_size, state)
1509    }
1510}
1511
1512/// Paint fade-out gradients at the top and/or bottom of a scroll area to
1513/// indicate that more content is available beyond the visible region.
1514fn paint_fade_areas<R>(ui: &Ui, scroll_output: &ScrollAreaOutput<R>) {
1515    let crate::style::ScrollFadeStyle {
1516        strength,
1517        size: fade_size,
1518    } = ui.spacing().scroll.fade;
1519
1520    if strength <= 0.0 {
1521        return;
1522    }
1523
1524    let bg = ui.stack().bg_color();
1525
1526    let offset = scroll_output.state.offset;
1527    let overflow = scroll_output.content_size - scroll_output.inner_rect.size();
1528
1529    let paint_rect = scroll_output
1530        .inner_rect
1531        .intersect(ui.min_rect())
1532        .expand(ui.visuals().clip_rect_margin);
1533
1534    // Top fade: animate opacity based on how far we've scrolled down.
1535    if 0.0 < offset.y {
1536        let t = (offset.y / fade_size).clamp(0.0, 1.0) * strength;
1537        let bg_faded = bg.gamma_multiply(t);
1538        let rect = Rect::from_min_max(
1539            paint_rect.left_top(),
1540            pos2(paint_rect.right(), paint_rect.top() + fade_size),
1541        );
1542        ui.painter().add(Shape::gradient_rect(
1543            rect,
1544            Direction::TopDown,
1545            [bg_faded, Color32::TRANSPARENT],
1546        ));
1547    }
1548
1549    // Bottom fade: animate opacity based on distance from the bottom.
1550    let distance_from_bottom = overflow.y - offset.y;
1551    if 0.0 < distance_from_bottom {
1552        let t = (distance_from_bottom / fade_size).clamp(0.0, 1.0) * strength;
1553        let bg_faded = bg.gamma_multiply(t);
1554        let rect = Rect::from_min_max(
1555            pos2(paint_rect.left(), paint_rect.bottom() - fade_size),
1556            paint_rect.right_bottom(),
1557        );
1558        ui.painter().add(Shape::gradient_rect(
1559            rect,
1560            Direction::BottomUp,
1561            [bg_faded, Color32::TRANSPARENT],
1562        ));
1563    }
1564
1565    // Left fade: animate opacity based on how far we've scrolled right.
1566    if 0.0 < offset.x {
1567        let t = (offset.x / fade_size).clamp(0.0, 1.0) * strength;
1568        let bg_faded = bg.gamma_multiply(t);
1569        let rect = Rect::from_min_max(
1570            paint_rect.left_top(),
1571            pos2(paint_rect.left() + fade_size, paint_rect.bottom()),
1572        );
1573        ui.painter().add(Shape::gradient_rect(
1574            rect,
1575            Direction::LeftToRight,
1576            [bg_faded, Color32::TRANSPARENT],
1577        ));
1578    }
1579
1580    // Right fade: animate opacity based on distance from the right edge.
1581    let distance_from_right = overflow.x - offset.x;
1582    if 0.0 < distance_from_right {
1583        let t = (distance_from_right / fade_size).clamp(0.0, 1.0) * strength;
1584        let bg_faded = bg.gamma_multiply(t);
1585        let rect = Rect::from_min_max(
1586            pos2(paint_rect.right() - fade_size, paint_rect.top()),
1587            paint_rect.right_bottom(),
1588        );
1589        ui.painter().add(Shape::gradient_rect(
1590            rect,
1591            Direction::RightToLeft,
1592            [bg_faded, Color32::TRANSPARENT],
1593        ));
1594    }
1595}