Skip to main content

egui/containers/
scroll_area.rs

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