egui/containers/
scroll_area.rs

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