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, NumExt as _, Rangef,
22    Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetType, lerp,
23    vec2,
24};
25
26fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
27    ctx.animate_bool_responsive(id, is_expanded)
28}
29
30/// State regarding panels.
31#[derive(Clone, Copy, Debug)]
32#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
33pub struct PanelState {
34    pub rect: Rect,
35}
36
37impl PanelState {
38    pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
39        ctx.data_mut(|d| d.get_persisted(bar_id))
40    }
41
42    /// The size of the panel (from previous frame).
43    pub fn size(&self) -> Vec2 {
44        self.rect.size()
45    }
46
47    fn store(self, ctx: &Context, bar_id: Id) {
48        ctx.data_mut(|d| d.insert_persisted(bar_id, self));
49    }
50}
51
52// ----------------------------------------------------------------------------
53
54/// [`Left`](Side::Left) or [`Right`](Side::Right)
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum Side {
57    Left,
58    Right,
59}
60
61impl Side {
62    fn opposite(self) -> Self {
63        match self {
64            Self::Left => Self::Right,
65            Self::Right => Self::Left,
66        }
67    }
68
69    fn set_rect_width(self, rect: &mut Rect, width: f32) {
70        match self {
71            Self::Left => rect.max.x = rect.min.x + width,
72            Self::Right => rect.min.x = rect.max.x - width,
73        }
74    }
75
76    fn side_x(self, rect: Rect) -> f32 {
77        match self {
78            Self::Left => rect.left(),
79            Self::Right => rect.right(),
80        }
81    }
82
83    fn sign(self) -> f32 {
84        match self {
85            Self::Left => -1.0,
86            Self::Right => 1.0,
87        }
88    }
89}
90
91/// A panel that covers the entire left or right side of a [`Ui`] or screen.
92///
93/// The order in which you add panels matter!
94/// The first panel you add will always be the outermost, and the last you add will always be the innermost.
95///
96/// ⚠ Always add any [`CentralPanel`] last.
97///
98/// See the [module level docs](crate::containers::panel) for more details.
99///
100/// ```
101/// # egui::__run_test_ctx(|ctx| {
102/// egui::SidePanel::left("my_left_panel").show(ctx, |ui| {
103///    ui.label("Hello World!");
104/// });
105/// # });
106/// ```
107///
108/// See also [`TopBottomPanel`].
109#[must_use = "You should call .show()"]
110pub struct SidePanel {
111    side: Side,
112    id: Id,
113    frame: Option<Frame>,
114    resizable: bool,
115    show_separator_line: bool,
116    default_width: f32,
117    width_range: Rangef,
118}
119
120impl SidePanel {
121    /// The id should be globally unique, e.g. `Id::new("my_left_panel")`.
122    pub fn left(id: impl Into<Id>) -> Self {
123        Self::new(Side::Left, id)
124    }
125
126    /// The id should be globally unique, e.g. `Id::new("my_right_panel")`.
127    pub fn right(id: impl Into<Id>) -> Self {
128        Self::new(Side::Right, id)
129    }
130
131    /// The id should be globally unique, e.g. `Id::new("my_panel")`.
132    pub fn new(side: Side, id: impl Into<Id>) -> Self {
133        Self {
134            side,
135            id: id.into(),
136            frame: None,
137            resizable: true,
138            show_separator_line: true,
139            default_width: 200.0,
140            width_range: Rangef::new(96.0, f32::INFINITY),
141        }
142    }
143
144    /// Can panel be resized by dragging the edge of it?
145    ///
146    /// Default is `true`.
147    ///
148    /// If you want your panel to be resizable you also need to make the ui use
149    /// the available space.
150    ///
151    /// This can be done by using [`Ui::take_available_space`], or using a
152    /// widget in it that takes up more space as you resize it, such as:
153    /// * Wrapping text ([`Ui::horizontal_wrapped`]).
154    /// * A [`crate::ScrollArea`].
155    /// * A [`crate::Separator`].
156    /// * A [`crate::TextEdit`].
157    /// * …
158    #[inline]
159    pub fn resizable(mut self, resizable: bool) -> Self {
160        self.resizable = resizable;
161        self
162    }
163
164    /// Show a separator line, even when not interacting with it?
165    ///
166    /// Default: `true`.
167    #[inline]
168    pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
169        self.show_separator_line = show_separator_line;
170        self
171    }
172
173    /// The initial wrapping width of the [`SidePanel`], including margins.
174    #[inline]
175    pub fn default_width(mut self, default_width: f32) -> Self {
176        self.default_width = default_width;
177        self.width_range = Rangef::new(
178            self.width_range.min.at_most(default_width),
179            self.width_range.max.at_least(default_width),
180        );
181        self
182    }
183
184    /// Minimum width of the panel, including margins.
185    #[inline]
186    pub fn min_width(mut self, min_width: f32) -> Self {
187        self.width_range = Rangef::new(min_width, self.width_range.max.at_least(min_width));
188        self
189    }
190
191    /// Maximum width of the panel, including margins.
192    #[inline]
193    pub fn max_width(mut self, max_width: f32) -> Self {
194        self.width_range = Rangef::new(self.width_range.min.at_most(max_width), max_width);
195        self
196    }
197
198    /// The allowable width range for the panel, including margins.
199    #[inline]
200    pub fn width_range(mut self, width_range: impl Into<Rangef>) -> Self {
201        let width_range = width_range.into();
202        self.default_width = clamp_to_range(self.default_width, width_range);
203        self.width_range = width_range;
204        self
205    }
206
207    /// Enforce this exact width, including margins.
208    #[inline]
209    pub fn exact_width(mut self, width: f32) -> Self {
210        self.default_width = width;
211        self.width_range = Rangef::point(width);
212        self
213    }
214
215    /// Change the background color, margins, etc.
216    #[inline]
217    pub fn frame(mut self, frame: Frame) -> Self {
218        self.frame = Some(frame);
219        self
220    }
221}
222
223impl SidePanel {
224    /// Show the panel inside a [`Ui`].
225    pub fn show_inside<R>(
226        self,
227        ui: &mut Ui,
228        add_contents: impl FnOnce(&mut Ui) -> R,
229    ) -> InnerResponse<R> {
230        self.show_inside_dyn(ui, Box::new(add_contents))
231    }
232
233    /// Show the panel inside a [`Ui`].
234    fn show_inside_dyn<'c, R>(
235        self,
236        ui: &mut Ui,
237        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
238    ) -> InnerResponse<R> {
239        let Self {
240            side,
241            id,
242            frame,
243            resizable,
244            show_separator_line,
245            default_width,
246            width_range,
247        } = self;
248
249        let available_rect = ui.available_rect_before_wrap();
250        let mut panel_rect = available_rect;
251        let mut width = default_width;
252        {
253            if let Some(state) = PanelState::load(ui.ctx(), id) {
254                width = state.rect.width();
255            }
256            width = clamp_to_range(width, width_range).at_most(available_rect.width());
257            side.set_rect_width(&mut panel_rect, width);
258            ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel");
259        }
260
261        let resize_id = id.with("__resize");
262        let mut resize_hover = false;
263        let mut is_resizing = false;
264        if resizable {
265            // First we read the resize interaction results, to avoid frame latency in the resize:
266            if let Some(resize_response) = ui.ctx().read_response(resize_id) {
267                resize_hover = resize_response.hovered();
268                is_resizing = resize_response.dragged();
269
270                if is_resizing && let Some(pointer) = resize_response.interact_pointer_pos() {
271                    width = (pointer.x - side.side_x(panel_rect)).abs();
272                    width = clamp_to_range(width, width_range).at_most(available_rect.width());
273                    side.set_rect_width(&mut panel_rect, width);
274                }
275            }
276        }
277
278        panel_rect = panel_rect.round_ui();
279
280        let mut panel_ui = ui.new_child(
281            UiBuilder::new()
282                .id_salt(id)
283                .ui_stack_info(UiStackInfo::new(match side {
284                    Side::Left => UiKind::LeftPanel,
285                    Side::Right => UiKind::RightPanel,
286                }))
287                .max_rect(panel_rect)
288                .layout(Layout::top_down(Align::Min)),
289        );
290        panel_ui.expand_to_include_rect(panel_rect);
291        panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475)
292
293        let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
294        let inner_response = frame.show(&mut panel_ui, |ui| {
295            ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height
296            ui.set_min_width((width_range.min - frame.inner_margin.sum().x).at_least(0.0));
297            add_contents(ui)
298        });
299
300        let rect = inner_response.response.rect;
301
302        {
303            let mut cursor = ui.cursor();
304            match side {
305                Side::Left => {
306                    cursor.min.x = rect.max.x;
307                }
308                Side::Right => {
309                    cursor.max.x = rect.min.x;
310                }
311            }
312            ui.set_cursor(cursor);
313        }
314        ui.expand_to_include_rect(rect);
315
316        if resizable {
317            // Now we do the actual resize interaction, on top of all the contents.
318            // Otherwise its input could be eaten by the contents, e.g. a
319            // `ScrollArea` on either side of the panel boundary.
320            let resize_x = side.opposite().side_x(panel_rect);
321            let resize_rect = Rect::from_x_y_ranges(resize_x..=resize_x, panel_rect.y_range())
322                .expand2(vec2(ui.style().interaction.resize_grab_radius_side, 0.0));
323            let resize_response = ui.interact(resize_rect, resize_id, Sense::drag());
324            resize_hover = resize_response.hovered();
325            is_resizing = resize_response.dragged();
326        }
327
328        if resize_hover || is_resizing {
329            let cursor_icon = if width <= width_range.min {
330                match self.side {
331                    Side::Left => CursorIcon::ResizeEast,
332                    Side::Right => CursorIcon::ResizeWest,
333                }
334            } else if width < width_range.max {
335                CursorIcon::ResizeHorizontal
336            } else {
337                match self.side {
338                    Side::Left => CursorIcon::ResizeWest,
339                    Side::Right => CursorIcon::ResizeEast,
340                }
341            };
342            ui.ctx().set_cursor_icon(cursor_icon);
343        }
344
345        PanelState { rect }.store(ui.ctx(), id);
346
347        {
348            let stroke = if is_resizing {
349                ui.style().visuals.widgets.active.fg_stroke // highly visible
350            } else if resize_hover {
351                ui.style().visuals.widgets.hovered.fg_stroke // highly visible
352            } else if show_separator_line {
353                // TODO(emilk): distinguish resizable from non-resizable
354                ui.style().visuals.widgets.noninteractive.bg_stroke // dim
355            } else {
356                Stroke::NONE
357            };
358            // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
359            let resize_x = side.opposite().side_x(rect);
360
361            // Make sure the line is on the inside of the panel:
362            let resize_x = resize_x + 0.5 * side.sign() * stroke.width;
363            ui.painter().vline(resize_x, panel_rect.y_range(), stroke);
364        }
365
366        inner_response
367    }
368
369    /// Show the panel at the top level.
370    pub fn show<R>(
371        self,
372        ctx: &Context,
373        add_contents: impl FnOnce(&mut Ui) -> R,
374    ) -> InnerResponse<R> {
375        self.show_dyn(ctx, Box::new(add_contents))
376    }
377
378    /// Show the panel at the top level.
379    fn show_dyn<'c, R>(
380        self,
381        ctx: &Context,
382        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
383    ) -> InnerResponse<R> {
384        let side = self.side;
385        let available_rect = ctx.available_rect();
386        let mut panel_ui = Ui::new(
387            ctx.clone(),
388            self.id,
389            UiBuilder::new()
390                .layer_id(LayerId::background())
391                .max_rect(available_rect),
392        );
393        panel_ui.set_clip_rect(ctx.content_rect());
394        panel_ui
395            .response()
396            .widget_info(|| WidgetInfo::new(WidgetType::Panel));
397
398        let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
399        let rect = inner_response.response.rect;
400
401        match side {
402            Side::Left => ctx.pass_state_mut(|state| {
403                state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
404            }),
405            Side::Right => ctx.pass_state_mut(|state| {
406                state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
407            }),
408        }
409        inner_response
410    }
411
412    /// Show the panel if `is_expanded` is `true`,
413    /// otherwise don't show it, but with a nice animation between collapsed and expanded.
414    pub fn show_animated<R>(
415        self,
416        ctx: &Context,
417        is_expanded: bool,
418        add_contents: impl FnOnce(&mut Ui) -> R,
419    ) -> Option<InnerResponse<R>> {
420        let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);
421
422        if 0.0 == how_expanded {
423            None
424        } else if how_expanded < 1.0 {
425            // Show a fake panel in this in-between animation state:
426            // TODO(emilk): move the panel out-of-screen instead of changing its width.
427            // Then we can actually paint it as it animates.
428            let expanded_width = PanelState::load(ctx, self.id)
429                .map_or(self.default_width, |state| state.rect.width());
430            let fake_width = how_expanded * expanded_width;
431            Self {
432                id: self.id.with("animating_panel"),
433                ..self
434            }
435            .resizable(false)
436            .exact_width(fake_width)
437            .show(ctx, |_ui| {});
438            None
439        } else {
440            // Show the real panel:
441            Some(self.show(ctx, add_contents))
442        }
443    }
444
445    /// Show the panel if `is_expanded` is `true`,
446    /// otherwise don't show it, but with a nice animation between collapsed and expanded.
447    pub fn show_animated_inside<R>(
448        self,
449        ui: &mut Ui,
450        is_expanded: bool,
451        add_contents: impl FnOnce(&mut Ui) -> R,
452    ) -> Option<InnerResponse<R>> {
453        let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
454
455        if 0.0 == how_expanded {
456            None
457        } else if how_expanded < 1.0 {
458            // Show a fake panel in this in-between animation state:
459            // TODO(emilk): move the panel out-of-screen instead of changing its width.
460            // Then we can actually paint it as it animates.
461            let expanded_width = PanelState::load(ui.ctx(), self.id)
462                .map_or(self.default_width, |state| state.rect.width());
463            let fake_width = how_expanded * expanded_width;
464            Self {
465                id: self.id.with("animating_panel"),
466                ..self
467            }
468            .resizable(false)
469            .exact_width(fake_width)
470            .show_inside(ui, |_ui| {});
471            None
472        } else {
473            // Show the real panel:
474            Some(self.show_inside(ui, add_contents))
475        }
476    }
477
478    /// Show either a collapsed or a expanded panel, with a nice animation between.
479    pub fn show_animated_between<R>(
480        ctx: &Context,
481        is_expanded: bool,
482        collapsed_panel: Self,
483        expanded_panel: Self,
484        add_contents: impl FnOnce(&mut Ui, f32) -> R,
485    ) -> Option<InnerResponse<R>> {
486        let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);
487
488        if 0.0 == how_expanded {
489            Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
490        } else if how_expanded < 1.0 {
491            // Show animation:
492            let collapsed_width = PanelState::load(ctx, collapsed_panel.id)
493                .map_or(collapsed_panel.default_width, |state| state.rect.width());
494            let expanded_width = PanelState::load(ctx, expanded_panel.id)
495                .map_or(expanded_panel.default_width, |state| state.rect.width());
496            let fake_width = lerp(collapsed_width..=expanded_width, how_expanded);
497            Self {
498                id: expanded_panel.id.with("animating_panel"),
499                ..expanded_panel
500            }
501            .resizable(false)
502            .exact_width(fake_width)
503            .show(ctx, |ui| add_contents(ui, how_expanded));
504            None
505        } else {
506            Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
507        }
508    }
509
510    /// Show either a collapsed or a expanded panel, with a nice animation between.
511    pub fn show_animated_between_inside<R>(
512        ui: &mut Ui,
513        is_expanded: bool,
514        collapsed_panel: Self,
515        expanded_panel: Self,
516        add_contents: impl FnOnce(&mut Ui, f32) -> R,
517    ) -> InnerResponse<R> {
518        let how_expanded =
519            animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);
520
521        if 0.0 == how_expanded {
522            collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
523        } else if how_expanded < 1.0 {
524            // Show animation:
525            let collapsed_width = PanelState::load(ui.ctx(), collapsed_panel.id)
526                .map_or(collapsed_panel.default_width, |state| state.rect.width());
527            let expanded_width = PanelState::load(ui.ctx(), expanded_panel.id)
528                .map_or(expanded_panel.default_width, |state| state.rect.width());
529            let fake_width = lerp(collapsed_width..=expanded_width, how_expanded);
530            Self {
531                id: expanded_panel.id.with("animating_panel"),
532                ..expanded_panel
533            }
534            .resizable(false)
535            .exact_width(fake_width)
536            .show_inside(ui, |ui| add_contents(ui, how_expanded))
537        } else {
538            expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
539        }
540    }
541}
542
543// ----------------------------------------------------------------------------
544
545/// [`Top`](TopBottomSide::Top) or [`Bottom`](TopBottomSide::Bottom)
546#[derive(Clone, Copy, Debug, PartialEq, Eq)]
547pub enum TopBottomSide {
548    Top,
549    Bottom,
550}
551
552impl TopBottomSide {
553    fn opposite(self) -> Self {
554        match self {
555            Self::Top => Self::Bottom,
556            Self::Bottom => Self::Top,
557        }
558    }
559
560    fn set_rect_height(self, rect: &mut Rect, height: f32) {
561        match self {
562            Self::Top => rect.max.y = rect.min.y + height,
563            Self::Bottom => rect.min.y = rect.max.y - height,
564        }
565    }
566
567    fn side_y(self, rect: Rect) -> f32 {
568        match self {
569            Self::Top => rect.top(),
570            Self::Bottom => rect.bottom(),
571        }
572    }
573
574    fn sign(self) -> f32 {
575        match self {
576            Self::Top => -1.0,
577            Self::Bottom => 1.0,
578        }
579    }
580}
581
582/// A panel that covers the entire top or bottom of a [`Ui`] or screen.
583///
584/// The order in which you add panels matter!
585/// The first panel you add will always be the outermost, and the last you add will always be the innermost.
586///
587/// ⚠ Always add any [`CentralPanel`] last.
588///
589/// See the [module level docs](crate::containers::panel) for more details.
590///
591/// ```
592/// # egui::__run_test_ctx(|ctx| {
593/// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {
594///    ui.label("Hello World!");
595/// });
596/// # });
597/// ```
598///
599/// See also [`SidePanel`].
600#[must_use = "You should call .show()"]
601pub struct TopBottomPanel {
602    side: TopBottomSide,
603    id: Id,
604    frame: Option<Frame>,
605    resizable: bool,
606    show_separator_line: bool,
607    default_height: Option<f32>,
608    height_range: Rangef,
609}
610
611impl TopBottomPanel {
612    /// The id should be globally unique, e.g. `Id::new("my_top_panel")`.
613    pub fn top(id: impl Into<Id>) -> Self {
614        Self::new(TopBottomSide::Top, id)
615    }
616
617    /// The id should be globally unique, e.g. `Id::new("my_bottom_panel")`.
618    pub fn bottom(id: impl Into<Id>) -> Self {
619        Self::new(TopBottomSide::Bottom, id)
620    }
621
622    /// The id should be globally unique, e.g. `Id::new("my_panel")`.
623    pub fn new(side: TopBottomSide, id: impl Into<Id>) -> Self {
624        Self {
625            side,
626            id: id.into(),
627            frame: None,
628            resizable: false,
629            show_separator_line: true,
630            default_height: None,
631            height_range: Rangef::new(20.0, f32::INFINITY),
632        }
633    }
634
635    /// Can panel be resized by dragging the edge of it?
636    ///
637    /// Default is `false`.
638    ///
639    /// If you want your panel to be resizable you also need to make the ui use
640    /// the available space.
641    ///
642    /// This can be done by using [`Ui::take_available_space`], or using a
643    /// widget in it that takes up more space as you resize it, such as:
644    /// * Wrapping text ([`Ui::horizontal_wrapped`]).
645    /// * A [`crate::ScrollArea`].
646    /// * A [`crate::Separator`].
647    /// * A [`crate::TextEdit`].
648    /// * …
649    #[inline]
650    pub fn resizable(mut self, resizable: bool) -> Self {
651        self.resizable = resizable;
652        self
653    }
654
655    /// Show a separator line, even when not interacting with it?
656    ///
657    /// Default: `true`.
658    #[inline]
659    pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
660        self.show_separator_line = show_separator_line;
661        self
662    }
663
664    /// The initial height of the [`TopBottomPanel`], including margins.
665    /// Defaults to [`crate::style::Spacing::interact_size`].y, plus frame margins.
666    #[inline]
667    pub fn default_height(mut self, default_height: f32) -> Self {
668        self.default_height = Some(default_height);
669        self.height_range = Rangef::new(
670            self.height_range.min.at_most(default_height),
671            self.height_range.max.at_least(default_height),
672        );
673        self
674    }
675
676    /// Minimum height of the panel, including margins.
677    #[inline]
678    pub fn min_height(mut self, min_height: f32) -> Self {
679        self.height_range = Rangef::new(min_height, self.height_range.max.at_least(min_height));
680        self
681    }
682
683    /// Maximum height of the panel, including margins.
684    #[inline]
685    pub fn max_height(mut self, max_height: f32) -> Self {
686        self.height_range = Rangef::new(self.height_range.min.at_most(max_height), max_height);
687        self
688    }
689
690    /// The allowable height range for the panel, including margins.
691    #[inline]
692    pub fn height_range(mut self, height_range: impl Into<Rangef>) -> Self {
693        let height_range = height_range.into();
694        self.default_height = self
695            .default_height
696            .map(|default_height| clamp_to_range(default_height, height_range));
697        self.height_range = height_range;
698        self
699    }
700
701    /// Enforce this exact height, including margins.
702    #[inline]
703    pub fn exact_height(mut self, height: f32) -> Self {
704        self.default_height = Some(height);
705        self.height_range = Rangef::point(height);
706        self
707    }
708
709    /// Change the background color, margins, etc.
710    #[inline]
711    pub fn frame(mut self, frame: Frame) -> Self {
712        self.frame = Some(frame);
713        self
714    }
715}
716
717impl TopBottomPanel {
718    /// Show the panel inside a [`Ui`].
719    pub fn show_inside<R>(
720        self,
721        ui: &mut Ui,
722        add_contents: impl FnOnce(&mut Ui) -> R,
723    ) -> InnerResponse<R> {
724        self.show_inside_dyn(ui, Box::new(add_contents))
725    }
726
727    /// Show the panel inside a [`Ui`].
728    fn show_inside_dyn<'c, R>(
729        self,
730        ui: &mut Ui,
731        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
732    ) -> InnerResponse<R> {
733        let Self {
734            side,
735            id,
736            frame,
737            resizable,
738            show_separator_line,
739            default_height,
740            height_range,
741        } = self;
742
743        let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
744
745        let available_rect = ui.available_rect_before_wrap();
746        let mut panel_rect = available_rect;
747
748        let mut height = if let Some(state) = PanelState::load(ui.ctx(), id) {
749            state.rect.height()
750        } else {
751            default_height
752                .unwrap_or_else(|| ui.style().spacing.interact_size.y + frame.inner_margin.sum().y)
753        };
754        {
755            height = clamp_to_range(height, height_range).at_most(available_rect.height());
756            side.set_rect_height(&mut panel_rect, height);
757            ui.ctx()
758                .check_for_id_clash(id, panel_rect, "TopBottomPanel");
759        }
760
761        let resize_id = id.with("__resize");
762        let mut resize_hover = false;
763        let mut is_resizing = false;
764        if resizable {
765            // First we read the resize interaction results, to avoid frame latency in the resize:
766            if let Some(resize_response) = ui.ctx().read_response(resize_id) {
767                resize_hover = resize_response.hovered();
768                is_resizing = resize_response.dragged();
769
770                if is_resizing && let Some(pointer) = resize_response.interact_pointer_pos() {
771                    height = (pointer.y - side.side_y(panel_rect)).abs();
772                    height = clamp_to_range(height, height_range).at_most(available_rect.height());
773                    side.set_rect_height(&mut panel_rect, height);
774                }
775            }
776        }
777
778        panel_rect = panel_rect.round_ui();
779
780        let mut panel_ui = ui.new_child(
781            UiBuilder::new()
782                .id_salt(id)
783                .ui_stack_info(UiStackInfo::new(match side {
784                    TopBottomSide::Top => UiKind::TopPanel,
785                    TopBottomSide::Bottom => UiKind::BottomPanel,
786                }))
787                .max_rect(panel_rect)
788                .layout(Layout::top_down(Align::Min)),
789        );
790        panel_ui.expand_to_include_rect(panel_rect);
791        panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475)
792
793        let inner_response = frame.show(&mut panel_ui, |ui| {
794            ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width
795            ui.set_min_height((height_range.min - frame.inner_margin.sum().y).at_least(0.0));
796            add_contents(ui)
797        });
798
799        let rect = inner_response.response.rect;
800
801        {
802            let mut cursor = ui.cursor();
803            match side {
804                TopBottomSide::Top => {
805                    cursor.min.y = rect.max.y;
806                }
807                TopBottomSide::Bottom => {
808                    cursor.max.y = rect.min.y;
809                }
810            }
811            ui.set_cursor(cursor);
812        }
813        ui.expand_to_include_rect(rect);
814
815        if resizable {
816            // Now we do the actual resize interaction, on top of all the contents.
817            // Otherwise its input could be eaten by the contents, e.g. a
818            // `ScrollArea` on either side of the panel boundary.
819
820            let resize_y = side.opposite().side_y(panel_rect);
821            let resize_rect = Rect::from_x_y_ranges(panel_rect.x_range(), resize_y..=resize_y)
822                .expand2(vec2(0.0, ui.style().interaction.resize_grab_radius_side));
823            let resize_response = ui.interact(resize_rect, resize_id, Sense::drag());
824            resize_hover = resize_response.hovered();
825            is_resizing = resize_response.dragged();
826        }
827
828        if resize_hover || is_resizing {
829            let cursor_icon = if height <= height_range.min {
830                match self.side {
831                    TopBottomSide::Top => CursorIcon::ResizeSouth,
832                    TopBottomSide::Bottom => CursorIcon::ResizeNorth,
833                }
834            } else if height < height_range.max {
835                CursorIcon::ResizeVertical
836            } else {
837                match self.side {
838                    TopBottomSide::Top => CursorIcon::ResizeNorth,
839                    TopBottomSide::Bottom => CursorIcon::ResizeSouth,
840                }
841            };
842            ui.ctx().set_cursor_icon(cursor_icon);
843        }
844
845        PanelState { rect }.store(ui.ctx(), id);
846
847        {
848            let stroke = if is_resizing {
849                ui.style().visuals.widgets.active.fg_stroke // highly visible
850            } else if resize_hover {
851                ui.style().visuals.widgets.hovered.fg_stroke // highly visible
852            } else if show_separator_line {
853                // TODO(emilk): distinguish resizable from non-resizable
854                ui.style().visuals.widgets.noninteractive.bg_stroke // dim
855            } else {
856                Stroke::NONE
857            };
858            // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
859            let resize_y = side.opposite().side_y(rect);
860
861            // Make sure the line is on the inside of the panel:
862            let resize_y = resize_y + 0.5 * side.sign() * stroke.width;
863            ui.painter().hline(panel_rect.x_range(), resize_y, stroke);
864        }
865
866        inner_response
867    }
868
869    /// Show the panel at the top level.
870    pub fn show<R>(
871        self,
872        ctx: &Context,
873        add_contents: impl FnOnce(&mut Ui) -> R,
874    ) -> InnerResponse<R> {
875        self.show_dyn(ctx, Box::new(add_contents))
876    }
877
878    /// Show the panel at the top level.
879    fn show_dyn<'c, R>(
880        self,
881        ctx: &Context,
882        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
883    ) -> InnerResponse<R> {
884        let available_rect = ctx.available_rect();
885        let side = self.side;
886
887        let mut panel_ui = Ui::new(
888            ctx.clone(),
889            self.id,
890            UiBuilder::new()
891                .layer_id(LayerId::background())
892                .max_rect(available_rect),
893        );
894        panel_ui.set_clip_rect(ctx.content_rect());
895
896        let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
897        let rect = inner_response.response.rect;
898
899        match side {
900            TopBottomSide::Top => {
901                ctx.pass_state_mut(|state| {
902                    state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
903                });
904            }
905            TopBottomSide::Bottom => {
906                ctx.pass_state_mut(|state| {
907                    state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
908                });
909            }
910        }
911
912        inner_response
913    }
914
915    /// Show the panel if `is_expanded` is `true`,
916    /// otherwise don't show it, but with a nice animation between collapsed and expanded.
917    pub fn show_animated<R>(
918        self,
919        ctx: &Context,
920        is_expanded: bool,
921        add_contents: impl FnOnce(&mut Ui) -> R,
922    ) -> Option<InnerResponse<R>> {
923        let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);
924
925        if 0.0 == how_expanded {
926            None
927        } else if how_expanded < 1.0 {
928            // Show a fake panel in this in-between animation state:
929            // TODO(emilk): move the panel out-of-screen instead of changing its height.
930            // Then we can actually paint it as it animates.
931            let expanded_height = PanelState::load(ctx, self.id)
932                .map(|state| state.rect.height())
933                .or(self.default_height)
934                .unwrap_or_else(|| ctx.style().spacing.interact_size.y);
935            let fake_height = how_expanded * expanded_height;
936            Self {
937                id: self.id.with("animating_panel"),
938                ..self
939            }
940            .resizable(false)
941            .exact_height(fake_height)
942            .show(ctx, |_ui| {});
943            None
944        } else {
945            // Show the real panel:
946            Some(self.show(ctx, add_contents))
947        }
948    }
949
950    /// Show the panel if `is_expanded` is `true`,
951    /// otherwise don't show it, but with a nice animation between collapsed and expanded.
952    pub fn show_animated_inside<R>(
953        self,
954        ui: &mut Ui,
955        is_expanded: bool,
956        add_contents: impl FnOnce(&mut Ui) -> R,
957    ) -> Option<InnerResponse<R>> {
958        let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
959
960        if 0.0 == how_expanded {
961            None
962        } else if how_expanded < 1.0 {
963            // Show a fake panel in this in-between animation state:
964            // TODO(emilk): move the panel out-of-screen instead of changing its height.
965            // Then we can actually paint it as it animates.
966            let expanded_height = PanelState::load(ui.ctx(), self.id)
967                .map(|state| state.rect.height())
968                .or(self.default_height)
969                .unwrap_or_else(|| ui.style().spacing.interact_size.y);
970            let fake_height = how_expanded * expanded_height;
971            Self {
972                id: self.id.with("animating_panel"),
973                ..self
974            }
975            .resizable(false)
976            .exact_height(fake_height)
977            .show_inside(ui, |_ui| {});
978            None
979        } else {
980            // Show the real panel:
981            Some(self.show_inside(ui, add_contents))
982        }
983    }
984
985    /// Show either a collapsed or a expanded panel, with a nice animation between.
986    pub fn show_animated_between<R>(
987        ctx: &Context,
988        is_expanded: bool,
989        collapsed_panel: Self,
990        expanded_panel: Self,
991        add_contents: impl FnOnce(&mut Ui, f32) -> R,
992    ) -> Option<InnerResponse<R>> {
993        let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);
994
995        if 0.0 == how_expanded {
996            Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
997        } else if how_expanded < 1.0 {
998            // Show animation:
999            let collapsed_height = PanelState::load(ctx, collapsed_panel.id)
1000                .map(|state| state.rect.height())
1001                .or(collapsed_panel.default_height)
1002                .unwrap_or_else(|| ctx.style().spacing.interact_size.y);
1003
1004            let expanded_height = PanelState::load(ctx, expanded_panel.id)
1005                .map(|state| state.rect.height())
1006                .or(expanded_panel.default_height)
1007                .unwrap_or_else(|| ctx.style().spacing.interact_size.y);
1008
1009            let fake_height = lerp(collapsed_height..=expanded_height, how_expanded);
1010            Self {
1011                id: expanded_panel.id.with("animating_panel"),
1012                ..expanded_panel
1013            }
1014            .resizable(false)
1015            .exact_height(fake_height)
1016            .show(ctx, |ui| add_contents(ui, how_expanded));
1017            None
1018        } else {
1019            Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
1020        }
1021    }
1022
1023    /// Show either a collapsed or a expanded panel, with a nice animation between.
1024    pub fn show_animated_between_inside<R>(
1025        ui: &mut Ui,
1026        is_expanded: bool,
1027        collapsed_panel: Self,
1028        expanded_panel: Self,
1029        add_contents: impl FnOnce(&mut Ui, f32) -> R,
1030    ) -> InnerResponse<R> {
1031        let how_expanded =
1032            animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);
1033
1034        if 0.0 == how_expanded {
1035            collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
1036        } else if how_expanded < 1.0 {
1037            // Show animation:
1038            let collapsed_height = PanelState::load(ui.ctx(), collapsed_panel.id)
1039                .map(|state| state.rect.height())
1040                .or(collapsed_panel.default_height)
1041                .unwrap_or_else(|| ui.style().spacing.interact_size.y);
1042
1043            let expanded_height = PanelState::load(ui.ctx(), expanded_panel.id)
1044                .map(|state| state.rect.height())
1045                .or(expanded_panel.default_height)
1046                .unwrap_or_else(|| ui.style().spacing.interact_size.y);
1047
1048            let fake_height = lerp(collapsed_height..=expanded_height, how_expanded);
1049            Self {
1050                id: expanded_panel.id.with("animating_panel"),
1051                ..expanded_panel
1052            }
1053            .resizable(false)
1054            .exact_height(fake_height)
1055            .show_inside(ui, |ui| add_contents(ui, how_expanded))
1056        } else {
1057            expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))
1058        }
1059    }
1060}
1061
1062// ----------------------------------------------------------------------------
1063
1064/// A panel that covers the remainder of the screen,
1065/// i.e. whatever area is left after adding other panels.
1066///
1067/// The order in which you add panels matter!
1068/// The first panel you add will always be the outermost, and the last you add will always be the innermost.
1069///
1070/// ⚠ [`CentralPanel`] must be added after all other panels!
1071///
1072/// NOTE: Any [`crate::Window`]s and [`crate::Area`]s will cover the top-level [`CentralPanel`].
1073///
1074/// See the [module level docs](crate::containers::panel) for more details.
1075///
1076/// ```
1077/// # egui::__run_test_ctx(|ctx| {
1078/// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {
1079///    ui.label("Hello World! From `TopBottomPanel`, that must be before `CentralPanel`!");
1080/// });
1081/// egui::CentralPanel::default().show(ctx, |ui| {
1082///    ui.label("Hello World!");
1083/// });
1084/// # });
1085/// ```
1086#[must_use = "You should call .show()"]
1087#[derive(Default)]
1088pub struct CentralPanel {
1089    frame: Option<Frame>,
1090}
1091
1092impl CentralPanel {
1093    /// Change the background color, margins, etc.
1094    #[inline]
1095    pub fn frame(mut self, frame: Frame) -> Self {
1096        self.frame = Some(frame);
1097        self
1098    }
1099}
1100
1101impl CentralPanel {
1102    /// Show the panel inside a [`Ui`].
1103    pub fn show_inside<R>(
1104        self,
1105        ui: &mut Ui,
1106        add_contents: impl FnOnce(&mut Ui) -> R,
1107    ) -> InnerResponse<R> {
1108        self.show_inside_dyn(ui, Box::new(add_contents))
1109    }
1110
1111    /// Show the panel inside a [`Ui`].
1112    fn show_inside_dyn<'c, R>(
1113        self,
1114        ui: &mut Ui,
1115        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
1116    ) -> InnerResponse<R> {
1117        let Self { frame } = self;
1118
1119        let panel_rect = ui.available_rect_before_wrap();
1120        let mut panel_ui = ui.new_child(
1121            UiBuilder::new()
1122                .ui_stack_info(UiStackInfo::new(UiKind::CentralPanel))
1123                .max_rect(panel_rect)
1124                .layout(Layout::top_down(Align::Min)),
1125        );
1126        panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475)
1127
1128        let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style()));
1129        frame.show(&mut panel_ui, |ui| {
1130            ui.expand_to_include_rect(ui.max_rect()); // Expand frame to include it all
1131            add_contents(ui)
1132        })
1133    }
1134
1135    /// Show the panel at the top level.
1136    pub fn show<R>(
1137        self,
1138        ctx: &Context,
1139        add_contents: impl FnOnce(&mut Ui) -> R,
1140    ) -> InnerResponse<R> {
1141        self.show_dyn(ctx, Box::new(add_contents))
1142    }
1143
1144    /// Show the panel at the top level.
1145    fn show_dyn<'c, R>(
1146        self,
1147        ctx: &Context,
1148        add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
1149    ) -> InnerResponse<R> {
1150        let id = Id::new((ctx.viewport_id(), "central_panel"));
1151
1152        let mut panel_ui = Ui::new(
1153            ctx.clone(),
1154            id,
1155            UiBuilder::new()
1156                .layer_id(LayerId::background())
1157                .max_rect(ctx.available_rect().round_ui()),
1158        );
1159        panel_ui.set_clip_rect(ctx.content_rect());
1160
1161        let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
1162
1163        // Only inform ctx about what we actually used, so we can shrink the native window to fit.
1164        ctx.pass_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));
1165
1166        inner_response
1167    }
1168}
1169
1170fn clamp_to_range(x: f32, range: Rangef) -> f32 {
1171    let range = range.as_positive();
1172    x.clamp(range.min, range.max)
1173}