egui/containers/
scroll_area.rs

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