Skip to main content

egui/containers/
panel.rs

1//! Panels are [`Ui`] regions taking up e.g. the left side of a [`Ui`] or screen.
2//!
3//! Panels can either be a child of a [`Ui`] (taking up a portion of the parent)
4//! or be top-level (taking up a portion of the whole screen).
5//!
6//! Together with [`crate::Window`] and [`crate::Area`]:s, top-level panels are
7//! the only places where you can put you widgets.
8//!
9//! The order in which you add panels matter!
10//! The first panel you add will always be the outermost, and the last you add will always be the innermost.
11//!
12//! You must never open one top-level panel from within another panel. Add one panel, then the next.
13//!
14//! ⚠ Always add any [`CentralPanel`] last.
15//!
16//! Add your [`crate::Window`]:s after any top-level panels.
17
18use emath::GuiRounding as _;
19
20use crate::{
21    Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, Margin, NumExt as _,
22    Order, Rangef, Rect, Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
23};
24
25fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
26    ctx.animate_bool_responsive(id, is_expanded)
27}
28
29/// [`Id`] of a panel's resize-handle widget.
30///
31/// A panel registers its handle under this same id whether it is open,
32/// mid-slide, or fully collapsed — that is what lets one uninterrupted drag
33/// collapse the panel and pull it back open. [`Panel::show_switched`] points
34/// both of its panels at one shared handle the same way.
35fn resize_widget_id(id_source: Id) -> Id {
36    id_source.with("__resize")
37}
38
39/// State regarding panels.
40#[derive(Clone, Copy, Debug)]
41#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
42pub struct PanelState {
43    /// The _outer_ rect of the panel, i.e. including the [`Frame`] margin & border.
44    ///
45    /// When animating, this will be a shifted in the animation direction,
46    /// so it is really only the size that you can count on.
47    #[cfg_attr(feature = "serde", serde(alias = "rect"))]
48    pub outer_rect: Rect,
49}
50
51impl PanelState {
52    pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
53        ctx.data_mut(|d| d.get_persisted(bar_id))
54    }
55
56    /// The _outer_ size of the panel (from previous frame),
57    /// i.e. including the [`Frame`] margin & border.
58    pub fn size(&self) -> Vec2 {
59        self.outer_rect.size()
60    }
61
62    fn store(self, ctx: &Context, bar_id: Id) {
63        ctx.data_mut(|d| d.insert_persisted(bar_id, self));
64    }
65}
66
67// ----------------------------------------------------------------------------
68
69/// Which side of a [`Ui`] or screen the panel is attached to.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71enum PanelSide {
72    Left,
73    Right,
74    Top,
75    Bottom,
76}
77
78impl PanelSide {
79    /// The axis the panel grows along: `0` (x) for left/right panels,
80    /// `1` (y) for top/bottom panels.
81    ///
82    /// Useful as an index into `Vec2`/`Pos2`.
83    fn axis(self) -> usize {
84        match self {
85            Self::Left | Self::Right => 0,
86            Self::Top | Self::Bottom => 1,
87        }
88    }
89
90    /// The axis perpendicular to [`Self::axis`].
91    fn cross_axis(self) -> usize {
92        1 - self.axis()
93    }
94
95    /// Unit vector along [`Self::axis`]: `(1, 0)` for left/right, `(0, 1)` for top/bottom.
96    fn axis_unit(self) -> Vec2 {
97        match self {
98            Self::Left | Self::Right => Vec2::X,
99            Self::Top | Self::Bottom => Vec2::Y,
100        }
101    }
102
103    /// Outward unit vector from the fixed edge:
104    /// `(-1, 0)` for [`Left`](Self::Left), `(+1, 0)` for [`Right`](Self::Right),
105    /// `(0, -1)` for [`Top`](Self::Top), `(0, +1)` for [`Bottom`](Self::Bottom).
106    fn dir_vec2(self) -> Vec2 {
107        self.sign() * self.axis_unit()
108    }
109
110    /// `-1` for sides at the near edge ([`Left`](Self::Left), [`Top`](Self::Top)),
111    /// `+1` for sides at the far edge ([`Right`](Self::Right), [`Bottom`](Self::Bottom)).
112    fn sign(self) -> f32 {
113        match self {
114            Self::Left | Self::Top => -1.0,
115            Self::Right | Self::Bottom => 1.0,
116        }
117    }
118
119    /// Coordinate of the _fixed_ side along the panel's [`axis`](Self::axis).
120    fn fixed_pos(self, rect: Rect) -> f32 {
121        match self {
122            Self::Left => rect.left(),
123            Self::Right => rect.right(),
124            Self::Top => rect.top(),
125            Self::Bottom => rect.bottom(),
126        }
127    }
128
129    /// Coordinate of the _opposite_ (resizable) side along the panel's [`axis`](Self::axis).
130    fn resize_pos(self, rect: Rect) -> f32 {
131        match self {
132            Self::Left => rect.right(),
133            Self::Right => rect.left(),
134            Self::Top => rect.bottom(),
135            Self::Bottom => rect.top(),
136        }
137    }
138
139    /// The component of `margin` on the panel's _resizable_ edge,
140    /// i.e. the edge facing the rest of the ui, where the separator line goes.
141    fn resize_margin(self, mut margin: Margin) -> i8 {
142        *self.resize_margin_mut(&mut margin)
143    }
144
145    /// Mutable version of [`Self::resize_margin`].
146    fn resize_margin_mut(self, margin: &mut Margin) -> &mut i8 {
147        match self {
148            Self::Left => &mut margin.right,
149            Self::Right => &mut margin.left,
150            Self::Top => &mut margin.bottom,
151            Self::Bottom => &mut margin.top,
152        }
153    }
154
155    /// Resize by keeping `self` side fixed, and moving the opposite side.
156    fn set_rect_size(self, rect: &mut Rect, size: f32) {
157        match self {
158            Self::Left => rect.max.x = rect.min.x + size,
159            Self::Right => rect.min.x = rect.max.x - size,
160            Self::Top => rect.max.y = rect.min.y + size,
161            Self::Bottom => rect.min.y = rect.max.y - size,
162        }
163    }
164
165    fn ui_kind(self) -> UiKind {
166        match self {
167            Self::Left => UiKind::LeftPanel,
168            Self::Right => UiKind::RightPanel,
169            Self::Top => UiKind::TopPanel,
170            Self::Bottom => UiKind::BottomPanel,
171        }
172    }
173}
174
175// ----------------------------------------------------------------------------
176
177/// A panel that covers an entire side
178/// ([`left`](Panel::left), [`right`](Panel::right),
179/// [`top`](Panel::top) or [`bottom`](Panel::bottom))
180/// of a [`Ui`] or screen.
181///
182/// The order in which you add panels matter!
183/// The first panel you add will always be the outermost, and the last you add will always be the innermost.
184///
185/// ⚠ Always add any [`CentralPanel`] last.
186///
187/// See the [module level docs](crate::containers::panel) for more details.
188///
189/// # Showing the panel
190///
191/// Pick the variant that matches the behavior you want:
192///
193/// * [`Panel::show`]: always show the panel.
194/// * [`Panel::show_collapsible`]: show or hide the panel, with a slide animation in between.
195/// * [`Panel::show_switched`]: animate between two different panels:
196///   a thin/collapsed one and a thick/expanded one.
197///
198/// ```
199/// # egui::__run_test_ui(|ui| {
200/// egui::Panel::left("my_left_panel").show(ui, |ui| {
201///    ui.label("Hello World!");
202/// });
203/// # });
204/// ```
205#[must_use = "You should call .show()"]
206pub struct Panel {
207    side: PanelSide,
208    id: Id,
209    frame: Option<Frame>,
210    resizable: bool,
211    drag_to_open: bool,
212    show_separator_line: bool,
213
214    /// _Outer_ size (including [`Frame`] margin & border):
215    /// the width for a vertical panel, or the height for a horizontal panel.
216    default_outer_size: Option<f32>,
217
218    /// _Outer_ size range (including [`Frame`] margin & border):
219    /// the width for a vertical panel, or the height for a horizontal panel.
220    outer_size_range: Rangef,
221
222    /// `1.0` = panel fully visible (the normal case),
223    /// `0.0` = panel fully slid off-screen toward its fixed edge.
224    ///
225    /// Used by [`Self::show_collapsible`] to animate a panel sliding in/out.
226    /// While `slide_fraction != 1.0` the panel does _not_ persist its [`PanelState`].
227    slide_fraction: f32,
228
229    /// Override for the [`Id`] under which the resize-handle widget is registered.
230    ///
231    /// Used by [`Self::show_switched`] so the collapsed and
232    /// expanded panels share a single resize widget — that way a drag on either
233    /// one can flip `is_expanded` and the gesture survives the swap.
234    resize_id_source: Option<Id>,
235
236    /// Size below which drag-to-collapse fires, when set.
237    ///
238    /// Defaults to `outer_size_range.min`. Used by
239    /// [`Self::show_switched`] to set the threshold at the
240    /// collapsed panel's size, so the swap happens exactly when the slide
241    /// matches the collapsed size visually.
242    collapse_threshold: Option<f32>,
243}
244
245impl Panel {
246    /// Create a left panel.
247    ///
248    /// The id should be globally unique, e.g. `Id::new("my_left_panel")`.
249    pub fn left(id: impl Into<Id>) -> Self {
250        Self::new(PanelSide::Left, id)
251    }
252
253    /// Create a right panel.
254    ///
255    /// The id should be globally unique, e.g. `Id::new("my_right_panel")`.
256    pub fn right(id: impl Into<Id>) -> Self {
257        Self::new(PanelSide::Right, id)
258    }
259
260    /// Create a top panel.
261    ///
262    /// The id should be globally unique, e.g. `Id::new("my_top_panel")`.
263    ///
264    /// By default this is NOT resizable.
265    pub fn top(id: impl Into<Id>) -> Self {
266        Self::new(PanelSide::Top, id).resizable(false)
267    }
268
269    /// Create a bottom panel.
270    ///
271    /// The id should be globally unique, e.g. `Id::new("my_bottom_panel")`.
272    ///
273    /// By default this is NOT resizable.
274    pub fn bottom(id: impl Into<Id>) -> Self {
275        Self::new(PanelSide::Bottom, id).resizable(false)
276    }
277
278    /// Create a panel.
279    ///
280    /// The id should be globally unique, e.g. `Id::new("my_panel")`.
281    fn new(side: PanelSide, id: impl Into<Id>) -> Self {
282        let default_outer_size: Option<f32> = match side {
283            PanelSide::Left | PanelSide::Right => Some(200.0),
284            PanelSide::Top | PanelSide::Bottom => None,
285        };
286
287        let outer_size_range: Rangef = match side {
288            PanelSide::Left | PanelSide::Right => Rangef::new(96.0, f32::INFINITY),
289            PanelSide::Top | PanelSide::Bottom => Rangef::new(20.0, f32::INFINITY),
290        };
291
292        Self {
293            side,
294            id: id.into(),
295            frame: None,
296            resizable: true,
297            drag_to_open: true,
298            show_separator_line: true,
299            default_outer_size,
300            outer_size_range,
301            slide_fraction: 1.0,
302            resize_id_source: None,
303            collapse_threshold: None,
304        }
305    }
306
307    /// Can panel be resized by dragging the edge of it?
308    ///
309    /// Default is `true`.
310    ///
311    /// If you want your panel to be resizable you also need to make the ui use
312    /// the available space.
313    ///
314    /// This can be done by using [`Ui::take_available_space`], or using a
315    /// widget in it that takes up more space as you resize it, such as:
316    /// * Wrapping text ([`Ui::horizontal_wrapped`]).
317    /// * A [`crate::ScrollArea`].
318    /// * A [`crate::Separator`].
319    /// * A [`crate::TextEdit`].
320    /// * …
321    #[inline]
322    pub fn resizable(mut self, resizable: bool) -> Self {
323        self.resizable = resizable;
324        self
325    }
326
327    /// Can a fully collapsed panel be dragged back open?
328    ///
329    /// Default: `true`.
330    ///
331    /// When enabled, a panel that [`Self::show_collapsible`] has collapsed all
332    /// the way still leaves a thin grab handle at its fixed edge. The handle is
333    /// invisible until hovered, at which point it lights up like a normal resize
334    /// handle. Dragging it outward past [`Self::min_size`] — or double-clicking
335    /// it — reopens the panel.
336    ///
337    /// This is the counterpart to drag-to-collapse, and like it requires
338    /// [`Self::resizable`] to be `true`.
339    #[inline]
340    pub fn drag_to_open(mut self, drag_to_open: bool) -> Self {
341        self.drag_to_open = drag_to_open;
342        self
343    }
344
345    /// Show a separator line, even when not interacting with it?
346    ///
347    /// The separator line sits on the panel's inner edge, i.e. the edge facing the rest of the ui.
348    /// It is painted _outside_ the [`Frame`]'s outline, in room the panel reserves for it in the
349    /// frame's [`Frame::outer_margin`], so that going from the panel contents outwards you get:
350    ///
351    /// contents | [`Frame::inner_margin`] | [`Frame::stroke`] | separator line | [`Frame::outer_margin`]
352    ///
353    /// Turning this off removes that reserved room too, so the panel gets no permanent gap along
354    /// that edge.
355    ///
356    /// A `resizable` panel still shows a line while hovered or dragged, regardless of this setting.
357    /// With this setting off there is no room reserved for it, so that transient line is painted
358    /// just outside the frame's outline, overlapping the [`Frame::outer_margin`].
359    ///
360    /// Default: `true`.
361    #[inline]
362    pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
363        self.show_separator_line = show_separator_line;
364        self
365    }
366
367    /// The initial wrapping width of the [`Panel`], including margins.
368    #[inline]
369    pub fn default_size(mut self, default_size: f32) -> Self {
370        self.default_outer_size = Some(default_size);
371        self.outer_size_range = Rangef::new(
372            self.outer_size_range.min.at_most(default_size),
373            self.outer_size_range.max.at_least(default_size),
374        );
375        self
376    }
377
378    /// Minimum size of the panel, including margins.
379    #[inline]
380    pub fn min_size(mut self, min_size: f32) -> Self {
381        self.outer_size_range = Rangef::new(min_size, self.outer_size_range.max.at_least(min_size));
382        self
383    }
384
385    /// Maximum size of the panel, including margins.
386    #[inline]
387    pub fn max_size(mut self, max_size: f32) -> Self {
388        self.outer_size_range = Rangef::new(self.outer_size_range.min.at_most(max_size), max_size);
389        self
390    }
391
392    /// The allowable size range for the panel, including margins.
393    #[inline]
394    pub fn size_range(mut self, size_range: impl Into<Rangef>) -> Self {
395        let size_range = size_range.into();
396        self.default_outer_size = self
397            .default_outer_size
398            .map(|default_size| clamp_to_range(default_size, size_range));
399        self.outer_size_range = size_range;
400        self
401    }
402
403    /// Enforce this exact size, including margins.
404    #[inline]
405    pub fn exact_size(mut self, size: f32) -> Self {
406        self.default_outer_size = Some(size);
407        self.outer_size_range = Rangef::point(size);
408        self
409    }
410
411    /// Change the background color, margins, etc.
412    #[inline]
413    pub fn frame(mut self, frame: Frame) -> Self {
414        self.frame = Some(frame);
415        self
416    }
417}
418
419// Public showing methods
420impl Panel {
421    /// Show the panel inside a [`Ui`].
422    pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
423        self.show_inside_dyn(ui, None, Box::new(add_contents))
424    }
425
426    /// Renamed to [`Self::show`].
427    #[deprecated = "Renamed to `show`"]
428    pub fn show_inside<R>(
429        self,
430        ui: &mut Ui,
431        add_contents: impl FnOnce(&mut Ui) -> R,
432    ) -> InnerResponse<R> {
433        self.show(ui, add_contents)
434    }
435
436    /// Show the panel if `*is_expanded` is `true`,
437    /// otherwise hide it, with a slide animation in between.
438    ///
439    /// During the animation `add_contents` runs against the real panel, and the
440    /// panel slides off-screen toward its fixed edge (clipped against the parent).
441    /// The parent only reserves the _visible_ portion, so neighboring widgets follow.
442    ///
443    /// `is_expanded` is taken by `&mut` so the panel can flip it to `false` when
444    /// the user drags the resize handle past the panel's minimum size, and back
445    /// to `true` if the user drags the handle outward while the panel is closed.
446    /// When [`Self::resizable`] is `true`, double-clicking the resize edge also
447    /// flips `*is_expanded`.
448    ///
449    /// A fully collapsed panel keeps a thin grab handle at its fixed edge, so the
450    /// user can drag it back open. See [`Self::drag_to_open`] to opt out.
451    pub fn show_collapsible<R>(
452        self,
453        ui: &mut Ui,
454        is_expanded: &mut bool,
455        add_contents: impl FnOnce(&mut Ui) -> R,
456    ) -> Option<InnerResponse<R>> {
457        let how_expanded = animate_expansion(ui, self.id.with("animation"), *is_expanded);
458
459        if how_expanded == 0.0 {
460            // Panel is fully closed, but we still leave a grab handle at its fixed
461            // edge so the user can drag it back open.
462            if self.resizable && self.drag_to_open {
463                self.collapsed_resize_handle(ui, is_expanded);
464            }
465
466            // Make sure the ids of the next widgets are the same whether we show the panel or not:
467            ui.skip_ahead_auto_ids(1);
468            return None;
469        }
470
471        // Don't lose the drag during the slide-back-open animation:
472        let drag_in_progress = ui
473            .read_response(self.resize_id())
474            .is_some_and(|r| r.dragged());
475
476        let panel = if how_expanded < 1.0 {
477            if drag_in_progress {
478                // Mid-animation but the user is dragging — keep resize live so the
479                // drag-to-reopen gesture flows straight into a normal resize.
480                self.with_slide_fraction(how_expanded)
481            } else {
482                self.with_slide_fraction(how_expanded).resizable(false) // avoid flicker when the handle moved under the pointer during the animation
483            }
484        } else {
485            self
486        };
487
488        Some(panel.show_inside_dyn(ui, Some(is_expanded), Box::new(add_contents)))
489    }
490
491    /// Renamed to [`Self::show_collapsible`].
492    ///
493    /// Note: [`Self::show_collapsible`] takes `is_expanded` by `&mut` so it can
494    /// flip it to `false` when the user drags the panel closed. To opt in,
495    /// migrate to the new name.
496    #[deprecated = "Renamed to `show_collapsible`"]
497    pub fn show_animated_inside<R>(
498        self,
499        ui: &mut Ui,
500        mut is_expanded: bool,
501        add_contents: impl FnOnce(&mut Ui) -> R,
502    ) -> Option<InnerResponse<R>> {
503        self.show_collapsible(ui, &mut is_expanded, add_contents)
504    }
505
506    /// Show either a collapsed or expanded panel, with a nice slide animation between.
507    ///
508    /// The `collapsed_panel` is shown only when fully collapsed; during the
509    /// animation, the `expanded_panel` slides in/out toward its fixed edge,
510    /// interpolating its visible size between the two panels' sizes.
511    /// `add_contents` receives `expanded = true` whenever the expanded panel is
512    /// rendered (including mid-animation), and `false` for the collapsed view.
513    ///
514    /// **Give the two panels distinct ids** so their persisted sizes don't
515    /// overwrite each other.
516    ///
517    /// # Drag-to-collapse / drag-to-expand
518    ///
519    /// The user can resize the panel by dragging its edge. Pulling that edge
520    /// past the size limits flips `*is_expanded`:
521    ///
522    /// * `.resizable(true)` on the **expanded** panel enables **drag-to-collapse**:
523    ///   shrinking past `min_size` sets `*is_expanded = false`.
524    /// * `.resizable(true)` on the **collapsed** panel enables **drag-to-expand**:
525    ///   growing past `max_size` sets `*is_expanded = true`. (Use
526    ///   [`Self::exact_size`] or [`Self::max_size`] to set a tight cap so a small
527    ///   outward drag is enough to trigger the swap.)
528    ///
529    /// Both panels share a single resize-handle widget under the hood (keyed to
530    /// the expanded panel's id), so a single uninterrupted drag can collapse and
531    /// re-expand the panel without releasing.
532    ///
533    /// Double-clicking the resize edge also flips `*is_expanded` (whichever
534    /// panel is currently shown is the one whose edge you click).
535    ///
536    /// ```
537    /// # egui::__run_test_ui(|ui| {
538    /// let mut is_expanded = true;
539    /// // `.resizable(true)` on both panels enables drag-to-collapse + drag-to-expand:
540    /// let collapsed = egui::Panel::top("top_collapsed")
541    ///     .resizable(true)
542    ///     .default_size(20.0);
543    /// let expanded = egui::Panel::top("top_expanded")
544    ///     .resizable(true)
545    ///     .default_size(120.0);
546    /// egui::Panel::show_switched(
547    ///     ui,
548    ///     &mut is_expanded,
549    ///     collapsed,
550    ///     expanded,
551    ///     |ui, expanded| {
552    ///         if expanded {
553    ///             ui.heading("Expanded");
554    ///             ui.label("More content here…");
555    ///         } else {
556    ///             ui.label("Collapsed toolbar");
557    ///         }
558    ///     },
559    /// );
560    /// ui.toggle_value(&mut is_expanded, "Expand");
561    /// # });
562    /// ```
563    pub fn show_switched<R>(
564        ui: &mut Ui,
565        is_expanded: &mut bool,
566        collapsed_panel: Self,
567        expanded_panel: Self,
568        add_contents: impl FnOnce(&mut Ui, bool) -> R,
569    ) -> InnerResponse<R> {
570        debug_assert!(
571            collapsed_panel.id != expanded_panel.id,
572            "show_switched: the collapsed and expanded panels must have distinct ids \
573             (their persisted sizes are stored per-id, and sharing one id would let the collapsed \
574             size overwrite the expanded size)."
575        );
576        // Share one resize-handle widget across the collapsed and expanded panels
577        // by routing both through the expanded panel's id. A drag that starts on
578        // either panel survives the swap to the other view.
579        let resize_id_source = expanded_panel.id;
580        // Drag-to-collapse fires when the drag crosses the collapsed panel's
581        // size, so the swap lines up with the visual size at that moment.
582        let collapse_threshold = collapsed_panel.outer_size(ui);
583
584        // Is the resize handle currently being dragged?
585        let drag_in_progress = ui
586            .read_response(resize_widget_id(resize_id_source))
587            .is_some_and(|r| r.dragged());
588
589        let animation_id = expanded_panel.id.with("animation");
590        let how_expanded = animate_expansion(ui, animation_id, *is_expanded);
591
592        // When expanding, the user sees the expanded content the moment animation starts.
593        // When collapsing, keep showing the expanded content until past the midpoint,
594        // then swap to the collapsed content for the rest of the slide-out.
595        let show_expanded_contents = *is_expanded || 0.5 < how_expanded;
596
597        if how_expanded == 0.0 {
598            // Fully collapsed. The collapsed panel registers the shared resize
599            // widget so drag-to-expand works, and `is_expanded` is flipped to
600            // `true` when the user drags past its `max_size`.
601            collapsed_panel
602                .with_resize_id_source(resize_id_source)
603                .show_inside_dyn(
604                    ui,
605                    Some(is_expanded),
606                    Box::new(|ui| add_contents(ui, false)),
607                )
608        } else {
609            let expanded_panel = expanded_panel.with_collapse_threshold(collapse_threshold);
610            let panel = if how_expanded < 1.0 {
611                // Animate the visible size from collapsed_size to expanded_size,
612                // so the slide picks up where the collapsed panel left off.
613                let expanded_size = if drag_in_progress {
614                    // During a drag the pointer sets the size, clamped to `min_size`
615                    // — so that, not the (stale) persisted size, is where the slide
616                    // meets the collapsed panel, whether opening or closing. Get it
617                    // wrong and the panel jumps the gap between the two sizes in one
618                    // frame.
619                    expanded_panel
620                        .outer_size_range
621                        .min
622                        .at_least(collapse_threshold)
623                } else {
624                    expanded_panel.outer_size(ui)
625                };
626                let visible_size = lerp(collapse_threshold..=expanded_size, how_expanded);
627                let slide_fraction = if 0.0 < expanded_size {
628                    visible_size / expanded_size
629                } else {
630                    1.0
631                };
632                let panel = expanded_panel.with_slide_fraction(slide_fraction);
633                // Keep the resize handle live during the slide if the drag is
634                // ongoing — otherwise disabling it would kill the gesture.
635                if drag_in_progress {
636                    panel
637                } else {
638                    panel.resizable(false) // avoid flicker when the handle moved under the pointer during the animation
639                }
640            } else {
641                expanded_panel
642            };
643            // Pass `is_expanded` so dragging the resize handle past the
644            // collapsed panel's size collapses to `collapsed_panel`.
645            panel.show_inside_dyn(
646                ui,
647                Some(is_expanded),
648                Box::new(|ui| add_contents(ui, show_expanded_contents)),
649            )
650        }
651    }
652
653    /// Renamed to [`Self::show_switched`].
654    ///
655    /// Note: [`Self::show_switched`] takes `is_expanded` by `&mut` (to allow
656    /// drag-to-collapse / drag-to-expand to flip it) and passes a `bool` to
657    /// `add_contents` instead of an `f32` animation fraction. To opt in,
658    /// migrate to the new name.
659    #[deprecated = "Renamed to `show_switched`"]
660    pub fn show_animated_between_inside<R>(
661        ui: &mut Ui,
662        is_expanded: bool,
663        collapsed_panel: Self,
664        expanded_panel: Self,
665        add_contents: impl FnOnce(&mut Ui, f32) -> R,
666    ) -> InnerResponse<R> {
667        let mut is_expanded = is_expanded;
668        Self::show_switched(
669            ui,
670            &mut is_expanded,
671            collapsed_panel,
672            expanded_panel,
673            |ui, expanded| add_contents(ui, if expanded { 1.0 } else { 0.0 }),
674        )
675    }
676}
677
678// Private methods to support the various show methods
679impl Panel {
680    /// Show the panel inside a [`Ui`].
681    ///
682    /// `is_expanded` is `Some` for the animated entry points
683    /// ([`Self::show_collapsible`], [`Self::show_switched`]);
684    /// when present, dragging the resize handle past the minimum size collapses
685    /// the panel by setting `*is_expanded = false`.
686    fn show_inside_dyn<'c, R>(
687        mut self,
688        parent_ui: &mut Ui,
689        mut is_expanded: Option<&mut bool>,
690        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
691    ) -> InnerResponse<R> {
692        let side = self.side;
693        let id = self.id;
694        let resizable = self.resizable;
695        let show_separator_line = self.show_separator_line;
696
697        let available_rect = parent_ui.available_rect_before_wrap();
698
699        {
700            // Never overflow out parent's available width:
701            self.outer_size_range = self.outer_size_range.as_positive();
702            self.outer_size_range.max = f32::min(
703                self.outer_size_range.max,
704                available_rect.size_along(side.axis()),
705            );
706        }
707
708        let frame = self.resolve_frame(parent_ui);
709
710        // We are NEVER allowed to overflow over this.
711        // If we do, we do so by clipping the contents,
712        // without reporting that extra size to the parent!
713        let max_rect = {
714            let mut max_rect = available_rect;
715            self.side
716                .set_rect_size(&mut max_rect, self.outer_size_range.max);
717            max_rect
718        };
719
720        let mut outer_size = self
721            .outer_size(parent_ui)
722            .at_most(available_rect.size_along(self.side.axis()));
723
724        let mut outer_rect = {
725            let mut outer_rect = available_rect;
726            self.side.set_rect_size(&mut outer_rect, outer_size);
727            outer_rect
728        };
729
730        // Check for duplicate id
731        parent_ui.check_for_id_clash(id, outer_rect, "Panel");
732
733        // True iff the user is currently dragging the resize handle (set in the block below).
734        let mut resize_drag_in_progress = false;
735
736        if resizable {
737            // Resolve the resize interaction first to avoid frame latency in the resize.
738            // We also recompute the size on the release frame (`drag_stopped`) so the
739            // released size gets persisted into [`PanelState`] — without this the
740            // store-skipped-during-drag rule would leave the stored size at the
741            // pre-drag value.
742            let resize_id = self.resize_id();
743            let resize_response = parent_ui.read_response(resize_id);
744
745            // Double-click on the resize edge toggles `*is_expanded` for the
746            // animated entry points (`show_collapsible` / `show_switched`).
747            if let Some(resize_response) = resize_response.as_ref()
748                && resize_response.double_clicked()
749                && let Some(is_expanded) = is_expanded.as_deref_mut()
750            {
751                *is_expanded = !*is_expanded;
752            }
753
754            if let Some(resize_response) = resize_response
755                && (resize_response.dragged() || resize_response.drag_stopped())
756                && let Some(pointer) = resize_response.interact_pointer_pos()
757            {
758                resize_drag_in_progress = resize_response.dragged();
759                let axis = side.axis();
760                let prev_outer_size = outer_size;
761                // Signed distance from the fixed edge to the pointer along the
762                // panel's axis. Going past the fixed edge yields a negative size,
763                // which `clamp_to_range` then snaps up to `min` — DON'T use
764                // `.abs()` here, that would mirror the drag and spuriously
765                // trigger drag-to-expand once the pointer crosses the edge.
766                let raw_outer_size = -side.sign() * (pointer[axis] - side.fixed_pos(outer_rect));
767                outer_size = clamp_to_range(raw_outer_size, self.outer_size_range)
768                    .at_most(available_rect.size_along(axis));
769                side.set_rect_size(&mut outer_rect, outer_size);
770
771                if let Some(is_expanded) = is_expanded {
772                    // Drag-to-collapse: shrink past the threshold → close.
773                    // The threshold defaults to `min_size`, but
774                    // `show_switched` overrides it to the
775                    // collapsed panel's size so the swap happens exactly when
776                    // the drag visually crosses the collapsed size.
777                    // Use `raw_outer_size` (pre-clamp) so a tight `exact_size`
778                    // panel can still detect inward overshoot.
779                    let collapse_threshold =
780                        self.collapse_threshold.unwrap_or(self.outer_size_range.min);
781                    if raw_outer_size < collapse_threshold && raw_outer_size < prev_outer_size {
782                        *is_expanded = false;
783                    }
784                    // Drag-to-expand: pointer pulled outward past `max_size` → open.
785                    // Triggers when this panel is acting as the collapsed view of
786                    // `show_switched`, with `resize_id_source` set
787                    // to the expanded panel's id. `raw_outer_size` is required
788                    // because `outer_size` is clamped to `max` and would never
789                    // exceed it (so `exact_size` panels couldn't otherwise expand).
790                    if self.outer_size_range.max < raw_outer_size {
791                        *is_expanded = true;
792                    }
793                }
794            }
795        }
796
797        // NOTE(shark98): This must be **after** the resizable preparation, as the size
798        // may change and round_ui() uses the size.
799        outer_rect = outer_rect.round_ui();
800
801        // Slide animation: translate the panel off-screen toward its fixed edge.
802        // When `slide_fraction == 1.0` this is a no-op.
803        let slide_distance = (1.0 - self.slide_fraction) * outer_size;
804        let shifted_outer_rect = if slide_distance == 0.0 {
805            outer_rect
806        } else {
807            outer_rect
808                .translate(slide_distance * side.dir_vec2())
809                .round_ui()
810        };
811
812        // The portion of the panel actually visible inside the parent's available area.
813        // The parent only allocates this much; neighbors follow the slide.
814        let visible_outer_rect = shifted_outer_rect.intersect(max_rect);
815
816        let mut panel_ui = parent_ui.new_child(
817            UiBuilder::new()
818                .id_salt(id)
819                .ui_stack_info(UiStackInfo::new(side.ui_kind()))
820                .max_rect(shifted_outer_rect)
821                .layout(Layout::top_down(Align::Min)),
822        );
823        panel_ui.expand_to_include_rect(shifted_outer_rect);
824        panel_ui.set_clip_rect(visible_outer_rect); // Hides the off-screen part during a slide; also prevents overflow (#4475).
825
826        let axis = side.axis();
827        let panel_axis_min =
828            (self.outer_size_range.min - frame.total_margin().sum()[axis]).at_least(0.0);
829        let mut inner_response = frame.show(&mut panel_ui, |content_ui| {
830            // Make sure the frame fills the cross-axis fully:
831            let cross_axis_size = content_ui.max_rect().size_along(side.cross_axis());
832            if axis == 0 {
833                content_ui.set_min_height(cross_axis_size);
834                content_ui.set_min_width(panel_axis_min);
835            } else {
836                content_ui.set_min_width(cross_axis_size);
837                content_ui.set_min_height(panel_axis_min);
838            }
839
840            add_contents(content_ui)
841        });
842
843        if self.outer_size_range.max < inner_response.response.rect.size_along(axis) {
844            self.side
845                .set_rect_size(&mut inner_response.response.rect, self.outer_size_range.max);
846        }
847
848        // `Frame::show` returns the panel's (shifted) _outer_ rect, including margin & border.
849        let shifted_outer_rect = inner_response.response.rect;
850        let visible_outer_rect = shifted_outer_rect.intersect(max_rect);
851
852        {
853            let mut cursor = parent_ui.cursor();
854            match side {
855                PanelSide::Left | PanelSide::Top => {
856                    cursor.min[axis] = visible_outer_rect.max[axis];
857                }
858                PanelSide::Right | PanelSide::Bottom => {
859                    cursor.max[axis] = visible_outer_rect.min[axis];
860                }
861            }
862            parent_ui.set_cursor(cursor);
863        }
864
865        parent_ui.expand_to_include_rect(visible_outer_rect);
866
867        let (resize_hover, is_resizing) = if resizable {
868            // Now we do the actual resize interaction, on top of all the contents,
869            // otherwise its input could be eaten by the contents, e.g. a
870            // `ScrollArea` on either side of the panel boundary.
871            let resize_response = self.resize_panel(shifted_outer_rect, parent_ui);
872            (resize_response.hovered(), resize_response.dragged())
873        } else {
874            (false, false)
875        };
876
877        if resize_hover || is_resizing {
878            parent_ui.set_cursor_icon(self.cursor_icon(outer_size));
879        }
880
881        let is_animating = 0.0 < self.slide_fraction && self.slide_fraction < 1.0;
882        if !resize_drag_in_progress && !is_animating || PanelState::load(parent_ui, id).is_none() {
883            // We skip stoing state during a drag, so that the
884            // stored size reflects the panel's pre-drag size.
885            // This is so that drag-to-close followed by a drag-to-reopen restores the original size.
886
887            // Skipping when `!persist_state` keeps interpolated sizes (set by the
888            // collapse animation in `show_switched`) from polluting the panel's
889            // natural persisted size.
890
891            // Finally, we always store the state if it's not already stored,
892            // so we get a good estimate for the final size when first expanding a panel.
893
894            PanelState {
895                outer_rect: shifted_outer_rect,
896            }
897            .store(parent_ui, id);
898        }
899
900        // The highlight follows the pointer all the way down to zero size, where
901        // `collapsed_resize_handle` picks it straight up again — so the user never
902        // loses sight of the edge they are dragging. The dim idle separator does
903        // get hidden once the panel is mostly slid off, since there it would just
904        // be a stray dash hovering near the parent edge.
905        let stroke = if is_resizing {
906            parent_ui.style().visuals.widgets.active.fg_stroke // highly visible
907        } else if resize_hover {
908            parent_ui.style().visuals.widgets.hovered.fg_stroke // highly visible
909        } else if show_separator_line && 0.01 < self.slide_fraction {
910            // TODO(emilk): distinguish resizable from non-resizable
911            parent_ui.style().visuals.widgets.noninteractive.bg_stroke // dim
912        } else {
913            Stroke::NONE
914        };
915        if 0.0 < stroke.width {
916            // Nudged inward, to keep the line inside the panel's own (shifted)
917            // rect: `parent_ui`'s painter sits below the panels that come after
918            // this one, so anything drawn past the fixed edge is covered by them.
919            // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
920
921            // The line goes just _outside_ the frame's outline, in the room `resolve_frame`
922            // reserved for it in the outer margin, i.e.:
923            //
924            // contents | `inner_margin` | outline | separator line | `outer_margin`
925            let outer_margin = f32::from(side.resize_margin(frame.outer_margin));
926            let outline_edge = side.resize_pos(shifted_outer_rect) + side.sign() * outer_margin;
927            let line_pos = outline_edge - 0.5 * side.sign() * stroke.width;
928            let cross_range = shifted_outer_rect.range_along(side.cross_axis());
929            if axis == 0 {
930                parent_ui.painter().vline(line_pos, cross_range, stroke);
931            } else {
932                parent_ui.painter().hline(cross_range, line_pos, stroke);
933            }
934        }
935
936        inner_response
937    }
938
939    /// [`Id`] of this panel's resize-handle widget.
940    ///
941    /// See [`resize_widget_id`] for why open and collapsed panels must share it.
942    fn resize_id(&self) -> Id {
943        resize_widget_id(self.resize_id_source.unwrap_or(self.id))
944    }
945
946    /// The configured [`Frame`], or the default side/top panel frame for this [`Ui`].
947    fn resolve_frame(&self, ui: &Ui) -> Frame {
948        let mut frame = self
949            .frame
950            .unwrap_or_else(|| Frame::side_top_panel(ui.style()));
951
952        if self.show_separator_line {
953            // Reserve room for the separator line in the frame's _outer_ margin, so the line
954            // lands just outside the frame's outline instead of painting on top of it:
955            //
956            // contents | `inner_margin` | outline | separator line | `outer_margin`
957            //
958            // We deliberately don't do this for a `resizable` panel that has opted out of the
959            // separator line: the line it shows while hovered/dragged is a transient affordance,
960            // and reserving room for it would leave a permanently visible gap.
961            let widgets = &ui.style().visuals.widgets;
962            let stroke_width = widgets.noninteractive.bg_stroke.width.round() as i8;
963
964            let margin_side = self.side.resize_margin_mut(&mut frame.outer_margin);
965            *margin_side = (*margin_side).saturating_add(stroke_width);
966        }
967
968        frame
969    }
970
971    /// The grab handle of a fully collapsed panel: a thin strip along the panel's
972    /// fixed edge, invisible until hovered.
973    ///
974    /// Dragging it outward past the minimum size — or double-clicking it —
975    /// reopens the panel. Registering it under the same id as the expanded
976    /// panel's resize handle also keeps an in-progress drag-to-collapse gesture
977    /// alive, so the user can drag the panel straight back out without releasing.
978    fn collapsed_resize_handle(&self, ui: &Ui, is_expanded: &mut bool) {
979        let side = self.side;
980        let axis = side.axis();
981
982        let available_rect = ui.available_rect_before_wrap();
983        let fixed_edge_pos = side.fixed_pos(available_rect);
984        let cross_range = available_rect.range_along(side.cross_axis());
985
986        // The strip lies just _inside_ the fixed edge, so it never reaches
987        // outside the area the panel is allowed to occupy.
988        let mut resize_rect = if axis == 0 {
989            Rect::from_x_y_ranges(Rangef::point(fixed_edge_pos), cross_range)
990        } else {
991            Rect::from_x_y_ranges(cross_range, Rangef::point(fixed_edge_pos))
992        };
993        side.set_rect_size(
994            &mut resize_rect,
995            ui.style().interaction.resize_grab_radius_side,
996        );
997
998        let resize_id = self.resize_id();
999        let response = ui.interact(resize_rect, resize_id, Sense::click_and_drag());
1000
1001        if response.double_clicked() {
1002            *is_expanded = true;
1003        }
1004
1005        if response.hovered() || response.dragged() {
1006            // Advertise that the panel can be pulled out. Also keeps the resize
1007            // cursor for a drag that started before the panel closed, instead of
1008            // snapping back to the default mid-gesture.
1009            ui.set_cursor_icon(self.cursor_icon(0.0));
1010        }
1011
1012        if response.dragged()
1013            && let Some(pointer) = response.interact_pointer_pos()
1014        {
1015            // Signed distance from the fixed edge to the pointer along the panel's
1016            // axis. Only counts as "pulled outward" while positive — going past the
1017            // fixed edge gives a negative value, NOT a mirrored positive one (no
1018            // `.abs()`), so dragging past the screen edge can't spuriously reopen.
1019            //
1020            // We require the full minimum size, so the panel never jumps ahead of
1021            // the pointer: it opens exactly when the drag reaches the size it will
1022            // open at, and follows the pointer from there.
1023            let dragged_size = -side.sign() * (pointer[axis] - fixed_edge_pos);
1024            if self.outer_size_range.min < dragged_size {
1025                *is_expanded = true;
1026            }
1027        }
1028
1029        // Invisible until hovered, so the handle doesn't read as a stray line at
1030        // the edge of the screen.
1031        let stroke = if response.dragged() {
1032            ui.style().visuals.widgets.active.fg_stroke
1033        } else if response.hovered() {
1034            ui.style().visuals.widgets.hovered.fg_stroke
1035        } else {
1036            Stroke::NONE
1037        };
1038        if 0.0 < stroke.width {
1039            // The collapsed panel occupies no space of its own, so the line has to
1040            // go _inside_ the area the following panels use — which means painting
1041            // in a layer above them, or they would cover it.
1042            // TODO(emilk): use the panel's own layer once https://github.com/emilk/egui/issues/1516 is done
1043            let painter = ui
1044                .ctx()
1045                .layer_painter(LayerId::new(Order::Middle, resize_id))
1046                .with_clip_rect(resize_rect);
1047
1048            // Nudge the line inward so it isn't half-clipped by the edge.
1049            let line_pos = fixed_edge_pos - 0.5 * side.sign() * stroke.width;
1050            if axis == 0 {
1051                painter.vline(line_pos, cross_range, stroke);
1052            } else {
1053                painter.hline(cross_range, line_pos, stroke);
1054            }
1055        }
1056    }
1057
1058    /// Get the current _outer_ width or height of the panel (from previous frame),
1059    /// including the [`Frame`] margin & border, or fall back to some default.
1060    ///
1061    /// Always clamped to [`Self::outer_size_range`] so callers get the size the
1062    /// panel would actually render at — never a stale persisted size from a
1063    /// previous build with a different range.
1064    fn outer_size(&self, ui: &Ui) -> f32 {
1065        let axis = self.side.axis();
1066        let raw = if let Some(state) = PanelState::load(ui, self.id) {
1067            state.outer_rect.size_along(axis)
1068        } else if let Some(default_outer_size) = self.default_outer_size {
1069            default_outer_size
1070        } else {
1071            let frame = self.resolve_frame(ui);
1072            ui.style().spacing.interact_size[axis] + frame.total_margin().sum()[axis]
1073        };
1074        clamp_to_range(raw, self.outer_size_range)
1075    }
1076
1077    fn resize_panel(&self, outer_rect: Rect, ui: &Ui) -> Response {
1078        let resize_pos = self.side.resize_pos(outer_rect);
1079        let panel_axis_range = Rangef::point(resize_pos);
1080        let cross_range = outer_rect.range_along(self.side.cross_axis());
1081        let (resize_x, resize_y) = if self.side.axis() == 0 {
1082            (panel_axis_range, cross_range)
1083        } else {
1084            (cross_range, panel_axis_range)
1085        };
1086        let amount = ui.style().interaction.resize_grab_radius_side * self.side.axis_unit();
1087
1088        // Use `resize_id_source` so collapsed/expanded panels in
1089        // `show_switched` share one resize widget.
1090        let resize_id = self.resize_id();
1091        let resize_rect = Rect::from_x_y_ranges(resize_x, resize_y).expand2(amount);
1092        ui.interact(resize_rect, resize_id, Sense::click_and_drag())
1093    }
1094
1095    fn cursor_icon(&self, outer_size: f32) -> CursorIcon {
1096        // When this panel is the collapsed view of `show_switched`
1097        // (`resize_id_source` is set), dragging past `max_size` triggers
1098        // drag-to-expand — so the user can always grow further. Treat the cap
1099        // as `INFINITY` for cursor purposes, otherwise we'd advertise
1100        // "can only shrink" while sitting on a drag-to-expand affordance.
1101        let can_drag_to_expand = self.resize_id_source.is_some();
1102        let max_for_cursor = if can_drag_to_expand {
1103            f32::INFINITY
1104        } else {
1105            self.outer_size_range.max
1106        };
1107
1108        if outer_size <= self.outer_size_range.min {
1109            // Can only grow (toward the resizable side):
1110            match self.side {
1111                PanelSide::Left => CursorIcon::ResizeEast,
1112                PanelSide::Right => CursorIcon::ResizeWest,
1113                PanelSide::Top => CursorIcon::ResizeSouth,
1114                PanelSide::Bottom => CursorIcon::ResizeNorth,
1115            }
1116        } else if outer_size < max_for_cursor {
1117            if self.side.axis() == 0 {
1118                CursorIcon::ResizeHorizontal
1119            } else {
1120                CursorIcon::ResizeVertical
1121            }
1122        } else {
1123            // Can only shrink (toward the fixed side):
1124            match self.side {
1125                PanelSide::Left => CursorIcon::ResizeWest,
1126                PanelSide::Right => CursorIcon::ResizeEast,
1127                PanelSide::Top => CursorIcon::ResizeNorth,
1128                PanelSide::Bottom => CursorIcon::ResizeSouth,
1129            }
1130        }
1131    }
1132
1133    /// Slide the panel toward its fixed edge. `1.0` = fully visible, `0.0` = fully off-screen.
1134    #[inline]
1135    fn with_slide_fraction(mut self, slide_fraction: f32) -> Self {
1136        self.slide_fraction = slide_fraction;
1137        self
1138    }
1139
1140    /// Register the resize-handle widget under this `Id` instead of `self.id`.
1141    ///
1142    /// Used by [`Self::show_switched`] to share one widget across
1143    /// the collapsed and expanded panels.
1144    #[inline]
1145    fn with_resize_id_source(mut self, id: Id) -> Self {
1146        self.resize_id_source = Some(id);
1147        self
1148    }
1149
1150    /// Override the drag-to-collapse threshold (defaults to `min_size`).
1151    #[inline]
1152    fn with_collapse_threshold(mut self, threshold: f32) -> Self {
1153        self.collapse_threshold = Some(threshold);
1154        self
1155    }
1156}
1157
1158// ----------------------------------------------------------------------------
1159
1160/// A panel that covers the remainder of the screen,
1161/// i.e. whatever area is left after adding other panels.
1162///
1163/// This acts very similar to [`Frame::central_panel`], but always expands
1164/// to use up all available space.
1165///
1166/// The order in which you add panels matter!
1167/// The first panel you add will always be the outermost, and the last you add will always be the innermost.
1168///
1169/// ⚠ [`CentralPanel`] must be added after all other panels!
1170///
1171/// NOTE: Any [`crate::Window`]s and [`crate::Area`]s will cover the top-level [`CentralPanel`].
1172///
1173/// See the [module level docs](crate::containers::panel) for more details.
1174///
1175/// ```
1176/// # egui::__run_test_ui(|ui| {
1177/// egui::Panel::top("my_panel").show(ui, |ui| {
1178///    ui.label("Hello World! From `Panel`, that must be before `CentralPanel`!");
1179/// });
1180/// egui::CentralPanel::default().show(ui, |ui| {
1181///    ui.label("Hello World!");
1182/// });
1183/// # });
1184/// ```
1185#[must_use = "You should call .show()"]
1186#[derive(Default)]
1187pub struct CentralPanel {
1188    frame: Option<Frame>,
1189}
1190
1191impl CentralPanel {
1192    /// A central panel with no margin or background color
1193    pub fn no_frame() -> Self {
1194        Self {
1195            frame: Some(Frame::NONE),
1196        }
1197    }
1198
1199    /// A central panel with a background color and some inner margins
1200    pub fn default_margins() -> Self {
1201        Self { frame: None }
1202    }
1203
1204    /// Change the background color, margins, etc.
1205    #[inline]
1206    pub fn frame(mut self, frame: Frame) -> Self {
1207        self.frame = Some(frame);
1208        self
1209    }
1210
1211    /// Show the panel inside a [`Ui`].
1212    pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
1213        self.show_inside_dyn(ui, Box::new(add_contents))
1214    }
1215
1216    /// Renamed to [`Self::show`].
1217    #[deprecated = "Renamed to `show`"]
1218    pub fn show_inside<R>(
1219        self,
1220        ui: &mut Ui,
1221        add_contents: impl FnOnce(&mut Ui) -> R,
1222    ) -> InnerResponse<R> {
1223        self.show(ui, add_contents)
1224    }
1225
1226    /// Show the panel inside a [`Ui`].
1227    fn show_inside_dyn<'c, R>(
1228        self,
1229        ui: &mut Ui,
1230        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
1231    ) -> InnerResponse<R> {
1232        let Self { frame } = self;
1233
1234        let outer_rect = ui.available_rect_before_wrap();
1235        let mut panel_ui = ui.new_child(
1236            UiBuilder::new()
1237                .ui_stack_info(UiStackInfo::new(UiKind::CentralPanel))
1238                .max_rect(outer_rect)
1239                .layout(Layout::top_down(Align::Min)),
1240        );
1241        panel_ui.set_clip_rect(outer_rect); // If we overflow, don't do so visibly (#4475)
1242
1243        let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style()));
1244        let response = frame.show(&mut panel_ui, |ui| {
1245            ui.expand_to_include_rect(ui.max_rect()); // Expand frame to include it all
1246            add_contents(ui)
1247        });
1248
1249        // Use up space in the parent:
1250        ui.advance_cursor_after_rect(response.response.rect);
1251
1252        response
1253    }
1254}
1255
1256fn clamp_to_range(x: f32, range: Rangef) -> f32 {
1257    let range = range.as_positive();
1258    x.clamp(range.min, range.max)
1259}