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