egui/containers/
scroll_area.rs

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