Skip to main content

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        let mut had_explicit_scroll_adjustment = Vec2b::FALSE;
1066
1067        for d in 0..2 {
1068            // PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it.
1069            let mut delta = -scroll_delta.0[d];
1070            let mut animation = scroll_delta.1;
1071
1072            // We always take both scroll targets regardless of which scroll axes are enabled. This
1073            // is to avoid them leaking to other scroll areas.
1074            let scroll_target = content_ui
1075                .ctx()
1076                .pass_state_mut(|state| state.scroll_target[d].take());
1077
1078            if direction_enabled[d] {
1079                if let Some(target) = scroll_target {
1080                    let pass_state::ScrollTarget {
1081                        range,
1082                        align,
1083                        animation: animation_update,
1084                    } = target;
1085                    let min = content_ui.min_rect().min[d];
1086                    let clip_rect = content_ui.clip_rect();
1087                    let visible_range = min..=min + clip_rect.size()[d];
1088                    let (start, end) = (range.min, range.max);
1089                    let clip_start = clip_rect.min[d];
1090                    let clip_end = clip_rect.max[d];
1091                    let mut spacing = content_ui.spacing().item_spacing[d];
1092
1093                    let delta_update = if let Some(align) = align {
1094                        let center_factor = align.to_factor();
1095
1096                        let offset =
1097                            lerp(range, center_factor) - lerp(visible_range, center_factor);
1098
1099                        // Depending on the alignment we need to add or subtract the spacing
1100                        spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
1101
1102                        offset + spacing - state.offset[d]
1103                    } else if start < clip_start && end < clip_end {
1104                        -(clip_start - start + spacing).min(clip_end - end - spacing)
1105                    } else if end > clip_end && start > clip_start {
1106                        (end - clip_end + spacing).min(start - clip_start - spacing)
1107                    } else {
1108                        // Ui is already in view, no need to adjust scroll.
1109                        0.0
1110                    };
1111
1112                    delta += delta_update;
1113                    animation = animation_update;
1114                }
1115
1116                if delta != 0.0 {
1117                    let target_offset = state.offset[d] + delta;
1118
1119                    if !animated {
1120                        state.offset[d] = target_offset;
1121                    } else if let Some(animation) = &mut state.offset_target[d] {
1122                        // For instance: the user is continuously calling `ui.scroll_to_cursor`,
1123                        // so we don't want to reset the animation, but perhaps update the target:
1124                        animation.target_offset = target_offset;
1125                    } else {
1126                        // The further we scroll, the more time we take.
1127                        let now = ui.input(|i| i.time);
1128                        let animation_duration = (delta.abs() / animation.points_per_second)
1129                            .clamp(animation.duration.min, animation.duration.max);
1130                        state.offset_target[d] = Some(ScrollingToTarget {
1131                            animation_time_span: (now, now + animation_duration as f64),
1132                            target_offset,
1133                        });
1134                    }
1135                    ui.request_repaint();
1136                }
1137            }
1138
1139            if delta != 0.0 {
1140                had_explicit_scroll_adjustment[d] = true;
1141            }
1142        }
1143
1144        // Restore scroll target meant for ScrollAreas up the stack (if any)
1145        ui.ctx().pass_state_mut(|state| {
1146            for d in 0..2 {
1147                if saved_scroll_target[d].is_some() {
1148                    state.scroll_target[d] = saved_scroll_target[d].clone();
1149                }
1150            }
1151        });
1152
1153        let inner_rect = {
1154            // At this point this is the available size for the inner rect.
1155            let mut inner_size = inner_rect.size();
1156
1157            for d in 0..2 {
1158                inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
1159                    (true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small
1160                    (true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space
1161                    (false, true) => content_size[d], // Follow the content (expand/contract to fit it).
1162                    (false, false) => inner_size[d].max(content_size[d]), // Expand to fit content
1163                };
1164            }
1165
1166            Rect::from_min_size(inner_rect.min, inner_size)
1167        };
1168
1169        let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
1170
1171        let content_is_too_large = Vec2b::new(
1172            direction_enabled[0] && inner_rect.width() < content_size.x,
1173            direction_enabled[1] && inner_rect.height() < content_size.y,
1174        );
1175
1176        let max_offset = content_size - inner_rect.size();
1177
1178        // Drag-to-scroll?
1179        let is_dragging_background = background_drag_response
1180            .as_ref()
1181            .is_some_and(|r| r.dragged());
1182
1183        let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
1184            && ui.ctx().dragged_id().is_none()
1185            || is_dragging_background;
1186
1187        if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
1188            let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
1189                && direction_enabled[0] != direction_enabled[1];
1190            for d in 0..2 {
1191                if direction_enabled[d] {
1192                    let scroll_delta = ui.input(|input| {
1193                        if always_scroll_enabled_direction {
1194                            // no bidirectional scrolling; allow horizontal scrolling without pressing shift
1195                            input.smooth_scroll_delta()[0] + input.smooth_scroll_delta()[1]
1196                        } else {
1197                            input.smooth_scroll_delta()[d]
1198                        }
1199                    });
1200                    let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
1201
1202                    let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
1203                    let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
1204
1205                    if scrolling_up || scrolling_down {
1206                        state.offset[d] -= scroll_delta;
1207
1208                        // Clear scroll delta so no parent scroll will use it:
1209                        ui.input_mut(|input| {
1210                            if always_scroll_enabled_direction {
1211                                input.smooth_scroll_delta = Vec2::ZERO;
1212                            } else {
1213                                input.smooth_scroll_delta[d] = 0.0;
1214                            }
1215                        });
1216
1217                        state.scroll_stuck_to_end[d] = false;
1218                        state.offset_target[d] = None;
1219                    }
1220                }
1221            }
1222        }
1223
1224        let show_scroll_this_frame = match scroll_bar_visibility {
1225            ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
1226            ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
1227            ScrollBarVisibility::AlwaysVisible => direction_enabled,
1228        };
1229
1230        // Avoid frame delay; start showing scroll bar right away:
1231        if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 {
1232            show_bars_factor.x = ui.ctx().animate_bool_responsive(id.with("h"), true);
1233        }
1234        if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 {
1235            show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
1236        }
1237
1238        let scroll_style = ui.spacing().scroll;
1239
1240        // Paint the bars:
1241        let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
1242        for d in 0..2 {
1243            // maybe force increase in offset to keep scroll stuck to end position,
1244            // unless this axis had an explicit scroll adjustment.
1245            if stick_to_end[d] && state.scroll_stuck_to_end[d] && !had_explicit_scroll_adjustment[d]
1246            {
1247                state.offset[d] = content_size[d] - inner_rect.size()[d];
1248            }
1249
1250            let show_factor = show_bars_factor[d];
1251            if show_factor == 0.0 {
1252                state.scroll_bar_interaction[d] = false;
1253                continue;
1254            }
1255
1256            let interact_id = id.with(d);
1257
1258            // Margin on either side of the scroll bar:
1259            let inner_margin = show_factor * scroll_style.bar_inner_margin;
1260            let outer_margin = show_factor * scroll_style.bar_outer_margin;
1261
1262            // bottom of a horizontal scroll (d==0).
1263            // right of a vertical scroll (d==1).
1264            let mut max_cross = outer_rect.max[1 - d] - outer_margin;
1265
1266            if ui.clip_rect().max[1 - d] - outer_margin < max_cross {
1267                // Move the scrollbar so it is visible. This is needed in some cases.
1268                // For instance:
1269                // * When we have a vertical-only scroll area in a top level panel,
1270                //   and that panel is not wide enough for the contents.
1271                // * When one ScrollArea is nested inside another, and the outer
1272                //   is scrolled so that the scroll-bars of the inner ScrollArea (us)
1273                //   is outside the clip rectangle.
1274                // Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
1275                // clip_rect_margin is quite a hack. It would be nice to get rid of it.
1276                max_cross = ui.clip_rect().max[1 - d] - outer_margin;
1277            }
1278
1279            let full_width = scroll_style.bar_width;
1280
1281            // The bounding rect of a fully visible bar.
1282            // When we hover this area, we should show the full bar:
1283            let max_bar_rect = if d == 0 {
1284                outer_rect.with_min_y(max_cross - full_width)
1285            } else {
1286                outer_rect.with_min_x(max_cross - full_width)
1287            };
1288
1289            let sense = if scroll_source.scroll_bar && ui.is_enabled() {
1290                Sense::CLICK | Sense::DRAG
1291            } else {
1292                Sense::hover()
1293            };
1294
1295            // We always sense interaction with the full width, even if we antimate it growing/shrinking.
1296            // This is to present a more consistent target for our hit test code,
1297            // and to avoid producing jitter in "thin widget" heuristics there.
1298            // Also: it make sense to detect any hover where the scroll bar _will_ be.
1299            let response = ui.interact(max_bar_rect, interact_id, sense);
1300
1301            response.widget_info(|| WidgetInfo::new(crate::WidgetType::ScrollBar));
1302
1303            // top/bottom of a horizontal scroll (d==0).
1304            // left/rigth of a vertical scroll (d==1).
1305            let cross = if scroll_style.floating {
1306                let is_hovering_bar_area = response.hovered() || state.scroll_bar_interaction[d];
1307
1308                let is_hovering_bar_area_t = ui
1309                    .ctx()
1310                    .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
1311
1312                let width = show_factor
1313                    * lerp(
1314                        scroll_style.floating_width..=full_width,
1315                        is_hovering_bar_area_t,
1316                    );
1317
1318                let min_cross = max_cross - width;
1319                Rangef::new(min_cross, max_cross)
1320            } else {
1321                let min_cross = inner_rect.max[1 - d] + inner_margin;
1322                Rangef::new(min_cross, max_cross)
1323            };
1324
1325            let outer_scroll_bar_rect = if d == 0 {
1326                Rect::from_x_y_ranges(scroll_bar_rect.x_range(), cross)
1327            } else {
1328                Rect::from_x_y_ranges(cross, scroll_bar_rect.y_range())
1329            };
1330
1331            let from_content = |content| {
1332                remap_clamp(
1333                    content,
1334                    0.0..=content_size[d],
1335                    scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
1336                )
1337            };
1338
1339            let calculate_handle_rect = |d, offset: &Vec2| {
1340                let handle_size = if d == 0 {
1341                    from_content(offset.x + inner_rect.width()) - from_content(offset.x)
1342                } else {
1343                    from_content(offset.y + inner_rect.height()) - from_content(offset.y)
1344                }
1345                .max(scroll_style.handle_min_length);
1346
1347                let handle_start_point = remap_clamp(
1348                    offset[d],
1349                    0.0..=max_offset[d],
1350                    scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_size),
1351                );
1352
1353                if d == 0 {
1354                    Rect::from_min_max(
1355                        pos2(handle_start_point, cross.min),
1356                        pos2(handle_start_point + handle_size, cross.max),
1357                    )
1358                } else {
1359                    Rect::from_min_max(
1360                        pos2(cross.min, handle_start_point),
1361                        pos2(cross.max, handle_start_point + handle_size),
1362                    )
1363                }
1364            };
1365
1366            let handle_rect = calculate_handle_rect(d, &state.offset);
1367
1368            state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
1369
1370            if let Some(pointer_pos) = response.interact_pointer_pos() {
1371                let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
1372                    .get_or_insert_with(|| {
1373                        if handle_rect.contains(pointer_pos) {
1374                            pointer_pos[d] - handle_rect.min[d]
1375                        } else {
1376                            let handle_top_pos_at_bottom =
1377                                scroll_bar_rect.max[d] - handle_rect.size()[d];
1378                            // Calculate the new handle top position, centering the handle on the mouse.
1379                            let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
1380                                .clamp(scroll_bar_rect.min[d], handle_top_pos_at_bottom);
1381                            pointer_pos[d] - new_handle_top_pos
1382                        }
1383                    });
1384
1385                let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
1386                let handle_travel =
1387                    scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]);
1388                state.offset[d] = if handle_travel.start() == handle_travel.end() {
1389                    0.0
1390                } else {
1391                    remap(new_handle_top, handle_travel, 0.0..=max_offset[d])
1392                };
1393
1394                // some manual action taken, scroll not stuck
1395                state.scroll_stuck_to_end[d] = false;
1396                state.offset_target[d] = None;
1397            } else {
1398                state.scroll_start_offset_from_top_left[d] = None;
1399            }
1400
1401            let unbounded_offset = state.offset[d];
1402            state.offset[d] = state.offset[d].max(0.0);
1403            state.offset[d] = state.offset[d].min(max_offset[d]);
1404
1405            if state.offset[d] != unbounded_offset {
1406                state.vel[d] = 0.0;
1407            }
1408
1409            if ui.is_rect_visible(outer_scroll_bar_rect) {
1410                // Avoid frame-delay by calculating a new handle rect:
1411                let handle_rect = calculate_handle_rect(d, &state.offset);
1412
1413                let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
1414                    // Pick visuals based on interaction with the handle.
1415                    // Remember that the response is for the whole scroll bar!
1416                    let is_hovering_handle = response.hovered()
1417                        && ui.input(|i| {
1418                            i.pointer
1419                                .latest_pos()
1420                                .is_some_and(|p| handle_rect.contains(p))
1421                        });
1422                    let visuals = ui.visuals();
1423                    if response.is_pointer_button_down_on() {
1424                        &visuals.widgets.active
1425                    } else if is_hovering_handle {
1426                        &visuals.widgets.hovered
1427                    } else {
1428                        &visuals.widgets.inactive
1429                    }
1430                } else {
1431                    &ui.visuals().widgets.inactive
1432                };
1433
1434                let handle_opacity = if scroll_style.floating {
1435                    if response.hovered() || response.dragged() {
1436                        scroll_style.interact_handle_opacity
1437                    } else {
1438                        let is_hovering_outer_rect_t = ui.ctx().animate_bool_responsive(
1439                            id.with((d, "is_hovering_outer_rect")),
1440                            is_hovering_outer_rect,
1441                        );
1442                        lerp(
1443                            scroll_style.dormant_handle_opacity
1444                                ..=scroll_style.active_handle_opacity,
1445                            is_hovering_outer_rect_t,
1446                        )
1447                    }
1448                } else {
1449                    1.0
1450                };
1451
1452                let background_opacity = if scroll_style.floating {
1453                    if response.hovered() || response.dragged() {
1454                        scroll_style.interact_background_opacity
1455                    } else if is_hovering_outer_rect {
1456                        scroll_style.active_background_opacity
1457                    } else {
1458                        scroll_style.dormant_background_opacity
1459                    }
1460                } else {
1461                    1.0
1462                };
1463
1464                let handle_color = if scroll_style.foreground_color {
1465                    visuals.fg_stroke.color
1466                } else {
1467                    visuals.bg_fill
1468                };
1469
1470                // Background:
1471                ui.painter().add(epaint::Shape::rect_filled(
1472                    outer_scroll_bar_rect,
1473                    visuals.corner_radius,
1474                    ui.visuals()
1475                        .extreme_bg_color
1476                        .gamma_multiply(background_opacity),
1477                ));
1478
1479                // Handle:
1480                ui.painter().add(epaint::Shape::rect_filled(
1481                    handle_rect,
1482                    visuals.corner_radius,
1483                    handle_color.gamma_multiply(handle_opacity),
1484                ));
1485            }
1486        }
1487
1488        ui.advance_cursor_after_rect(outer_rect);
1489
1490        if show_scroll_this_frame != state.show_scroll {
1491            ui.request_repaint();
1492        }
1493
1494        let available_offset = content_size - inner_rect.size();
1495        state.offset = state.offset.min(available_offset);
1496        state.offset = state.offset.max(Vec2::ZERO);
1497
1498        let suppress_stuck_recompute = Vec2b::new(
1499            had_explicit_scroll_adjustment[0] && state.offset_target[0].is_some(),
1500            had_explicit_scroll_adjustment[1] && state.offset_target[1].is_some(),
1501        );
1502
1503        // Is scroll handle at end of content, or is there no scrollbar
1504        // yet (not enough content), but sticking is requested? If so, enter sticky mode.
1505        // Only has an effect if stick_to_end is enabled but we save in
1506        // state anyway so that entering sticky mode at an arbitrary time
1507        // has appropriate effect.
1508        // Keep explicit target requests from being reclassified as "still stuck" in the same
1509        // frame, otherwise animated scroll-to requests never get a chance to pull away from the end.
1510        state.scroll_stuck_to_end = Vec2b::new(
1511            !suppress_stuck_recompute[0]
1512                && ((state.offset[0] == available_offset[0])
1513                    || (stick_to_end[0] && available_offset[0] < 0.0)),
1514            !suppress_stuck_recompute[1]
1515                && ((state.offset[1] == available_offset[1])
1516                    || (stick_to_end[1] && available_offset[1] < 0.0)),
1517        );
1518
1519        state.show_scroll = show_scroll_this_frame;
1520        state.content_is_too_large = content_is_too_large;
1521        state.interact_rect = Some(inner_rect);
1522
1523        state.store(ui.ctx(), id);
1524
1525        (content_size, state)
1526    }
1527}
1528
1529/// Paint fade-out gradients at the top and/or bottom of a scroll area to
1530/// indicate that more content is available beyond the visible region.
1531fn paint_fade_areas<R>(ui: &Ui, scroll_output: &ScrollAreaOutput<R>) {
1532    let crate::style::ScrollFadeStyle {
1533        strength,
1534        size: fade_size,
1535    } = ui.spacing().scroll.fade;
1536
1537    if strength <= 0.0 {
1538        return;
1539    }
1540
1541    let bg = ui.stack().bg_color();
1542
1543    let offset = scroll_output.state.offset;
1544    let overflow = scroll_output.content_size - scroll_output.inner_rect.size();
1545
1546    let paint_rect = scroll_output
1547        .inner_rect
1548        .intersect(ui.min_rect())
1549        .expand(ui.visuals().clip_rect_margin);
1550
1551    // Top fade: animate opacity based on how far we've scrolled down.
1552    if 0.0 < offset.y {
1553        let t = (offset.y / fade_size).clamp(0.0, 1.0) * strength;
1554        let bg_faded = bg.gamma_multiply(t);
1555        let rect = Rect::from_min_max(
1556            paint_rect.left_top(),
1557            pos2(paint_rect.right(), paint_rect.top() + fade_size),
1558        );
1559        ui.painter().add(Shape::gradient_rect(
1560            rect,
1561            Direction::TopDown,
1562            [bg_faded, Color32::TRANSPARENT],
1563        ));
1564    }
1565
1566    // Bottom fade: animate opacity based on distance from the bottom.
1567    let distance_from_bottom = overflow.y - offset.y;
1568    if 0.0 < distance_from_bottom {
1569        let t = (distance_from_bottom / fade_size).clamp(0.0, 1.0) * strength;
1570        let bg_faded = bg.gamma_multiply(t);
1571        let rect = Rect::from_min_max(
1572            pos2(paint_rect.left(), paint_rect.bottom() - fade_size),
1573            paint_rect.right_bottom(),
1574        );
1575        ui.painter().add(Shape::gradient_rect(
1576            rect,
1577            Direction::BottomUp,
1578            [bg_faded, Color32::TRANSPARENT],
1579        ));
1580    }
1581
1582    // Left fade: animate opacity based on how far we've scrolled right.
1583    if 0.0 < offset.x {
1584        let t = (offset.x / fade_size).clamp(0.0, 1.0) * strength;
1585        let bg_faded = bg.gamma_multiply(t);
1586        let rect = Rect::from_min_max(
1587            paint_rect.left_top(),
1588            pos2(paint_rect.left() + fade_size, paint_rect.bottom()),
1589        );
1590        ui.painter().add(Shape::gradient_rect(
1591            rect,
1592            Direction::LeftToRight,
1593            [bg_faded, Color32::TRANSPARENT],
1594        ));
1595    }
1596
1597    // Right fade: animate opacity based on distance from the right edge.
1598    let distance_from_right = overflow.x - offset.x;
1599    if 0.0 < distance_from_right {
1600        let t = (distance_from_right / fade_size).clamp(0.0, 1.0) * strength;
1601        let bg_faded = bg.gamma_multiply(t);
1602        let rect = Rect::from_min_max(
1603            pos2(paint_rect.right() - fade_size, paint_rect.top()),
1604            paint_rect.right_bottom(),
1605        );
1606        ui.painter().add(Shape::gradient_rect(
1607            rect,
1608            Direction::RightToLeft,
1609            [bg_faded, Color32::TRANSPARENT],
1610        ));
1611    }
1612}