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