egui/containers/
scroll_area.rs

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