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