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}