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