From 5b6a0196f91bfd5222d42c8f47595a8c297cc815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Par=C3=A9-Simard?= Date: Tue, 18 Nov 2025 09:46:01 -0500 Subject: [PATCH] Add `Panel` to replace `SidePanel` and `TopBottomPanel` (#5659) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This combines `SidePanel` and `TopBottomPanel` into a single `Panel`. The old types are still there as type aliases, but are deprecated. `.min_width(…)` etc are now called `.min_size(…)` etc. Again, the old names are still there, but deprecated. (edited by @emilk) --------- Co-authored-by: Emil Ernerfeldt --- crates/eframe/src/epi.rs | 2 +- crates/egui/src/containers/mod.rs | 4 +- crates/egui/src/containers/panel.rs | 1395 ++++++++--------- crates/egui/src/context.rs | 2 +- crates/egui/src/lib.rs | 6 +- crates/egui/src/menu.rs | 2 +- crates/egui/src/ui.rs | 2 +- crates/egui/src/ui_stack.rs | 8 +- .../src/accessibility_inspector.rs | 11 +- crates/egui_demo_app/src/apps/http_app.rs | 2 +- crates/egui_demo_app/src/apps/image_viewer.rs | 6 +- crates/egui_demo_app/src/wrap_app.rs | 4 +- .../src/demo/demo_app_windows.rs | 10 +- crates/egui_demo_lib/src/demo/panels.rs | 20 +- crates/egui_demo_lib/src/demo/tooltips.rs | 2 +- .../src/easy_mark/easy_mark_editor.rs | 2 +- crates/egui_glow/examples/pure_glow.rs | 2 +- examples/hello_android/src/lib.rs | 2 +- tests/test_size_pass/src/main.rs | 2 +- tests/test_ui_stack/src/main.rs | 6 +- 20 files changed, 695 insertions(+), 795 deletions(-) diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index d6c9a10b8..a7bcfd6ef 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -137,7 +137,7 @@ impl CreationContext<'_> { pub trait App { /// Called each time the UI needs repainting, which may be many times per second. /// - /// Put your widgets into a [`egui::SidePanel`], [`egui::TopBottomPanel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`]. + /// Put your widgets into a [`egui::Panel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`]. /// /// The [`egui::Context`] can be cloned and saved if you like. /// diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index 4312385da..a8f3306e9 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -1,4 +1,4 @@ -//! Containers are pieces of the UI which wraps other pieces of UI. Examples: [`Window`], [`ScrollArea`], [`Resize`], [`SidePanel`], etc. +//! Containers are pieces of the UI which wraps other pieces of UI. Examples: [`Window`], [`ScrollArea`], [`Resize`], [`Panel`], etc. //! //! For instance, a [`Frame`] adds a frame and background to some contained UI. @@ -27,7 +27,7 @@ pub use { frame::Frame, modal::{Modal, ModalResponse}, old_popup::*, - panel::{CentralPanel, SidePanel, TopBottomPanel}, + panel::*, popup::*, resize::Resize, scene::{DragPanButtons, Scene}, diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 6e582b428..eb60f5f21 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -15,7 +15,7 @@ //! //! Add your [`crate::Window`]:s after any top-level panels. -use emath::GuiRounding as _; +use emath::{GuiRounding as _, Pos2}; use crate::{ Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _, Rangef, @@ -51,21 +51,25 @@ impl PanelState { // ---------------------------------------------------------------------------- -/// [`Left`](Side::Left) or [`Right`](Side::Right) +/// [`Left`](VerticalSide::Left) or [`Right`](VerticalSide::Right) #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Side { +enum VerticalSide { Left, Right, } -impl Side { - fn opposite(self) -> Self { +impl VerticalSide { + pub fn opposite(self) -> Self { match self { Self::Left => Self::Right, Self::Right => Self::Left, } } + /// `self` is the _fixed_ side. + /// + /// * Left panels are resized on their right side + /// * Right panels are resized on their left side fn set_rect_width(self, rect: &mut Rect, width: f32) { match self { Self::Left => rect.max.x = rect.min.x + width, @@ -73,22 +77,211 @@ impl Side { } } - fn side_x(self, rect: Rect) -> f32 { - match self { - Self::Left => rect.left(), - Self::Right => rect.right(), - } - } - fn sign(self) -> f32 { match self { Self::Left => -1.0, Self::Right => 1.0, } } + + fn side_x(self, rect: Rect) -> f32 { + match self { + Self::Left => rect.left(), + Self::Right => rect.right(), + } + } } -/// A panel that covers the entire left or right side of a [`Ui`] or screen. +/// [`Top`](HorizontalSide::Top) or [`Bottom`](HorizontalSide::Bottom) +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum HorizontalSide { + Top, + Bottom, +} + +impl HorizontalSide { + pub fn opposite(self) -> Self { + match self { + Self::Top => Self::Bottom, + Self::Bottom => Self::Top, + } + } + + /// `self` is the _fixed_ side. + /// + /// * Top panels are resized on their bottom side + /// * Bottom panels are resized upwards + fn set_rect_height(self, rect: &mut Rect, height: f32) { + match self { + Self::Top => rect.max.y = rect.min.y + height, + Self::Bottom => rect.min.y = rect.max.y - height, + } + } + + fn sign(self) -> f32 { + match self { + Self::Top => -1.0, + Self::Bottom => 1.0, + } + } + + fn side_y(self, rect: Rect) -> f32 { + match self { + Self::Top => rect.top(), + Self::Bottom => rect.bottom(), + } + } +} + +// Intentionally private because I'm not sure of the naming. +// TODO(emilk): decide on good names and make public. +// "VerticalSide" and "HorizontalSide" feels inverted to me. +/// [`Horizontal`](PanelSide::Horizontal) or [`Vertical`](PanelSide::Vertical) +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PanelSide { + /// Left or right. + Vertical(VerticalSide), + + /// Top or bottom + Horizontal(HorizontalSide), +} + +impl From for PanelSide { + fn from(side: HorizontalSide) -> Self { + Self::Horizontal(side) + } +} + +impl From for PanelSide { + fn from(side: VerticalSide) -> Self { + Self::Vertical(side) + } +} + +impl PanelSide { + pub const LEFT: Self = Self::Vertical(VerticalSide::Left); + pub const RIGHT: Self = Self::Vertical(VerticalSide::Right); + pub const TOP: Self = Self::Horizontal(HorizontalSide::Top); + pub const BOTTOM: Self = Self::Horizontal(HorizontalSide::Bottom); + + /// Resize by keeping the [`self`] side fixed, and moving the opposite side. + fn set_rect_size(self, rect: &mut Rect, size: f32) { + match self { + Self::Vertical(side) => side.set_rect_width(rect, size), + Self::Horizontal(side) => side.set_rect_height(rect, size), + } + } + + fn ui_kind(self) -> UiKind { + match self { + Self::Vertical(side) => match side { + VerticalSide::Left => UiKind::LeftPanel, + VerticalSide::Right => UiKind::RightPanel, + }, + Self::Horizontal(side) => match side { + HorizontalSide::Top => UiKind::TopPanel, + HorizontalSide::Bottom => UiKind::BottomPanel, + }, + } + } +} + +// ---------------------------------------------------------------------------- + +/// Intermediate structure to abstract some portion of [`Panel::show_inside`](Panel::show_inside). +struct PanelSizer<'a> { + panel: &'a Panel, + frame: Frame, + available_rect: Rect, + size: f32, + panel_rect: Rect, +} + +impl<'a> PanelSizer<'a> { + fn new(panel: &'a Panel, ui: &Ui) -> Self { + let frame = panel + .frame + .unwrap_or_else(|| Frame::side_top_panel(ui.style())); + let available_rect = ui.available_rect_before_wrap(); + let size = PanelSizer::get_size_from_state_or_default(panel, ui, frame); + let panel_rect = PanelSizer::panel_rect(panel, available_rect, size); + + Self { + panel, + frame, + available_rect, + size, + panel_rect, + } + } + + fn get_size_from_state_or_default(panel: &Panel, ui: &Ui, frame: Frame) -> f32 { + if let Some(state) = PanelState::load(ui.ctx(), panel.id) { + match panel.side { + PanelSide::Vertical(_) => state.rect.width(), + PanelSide::Horizontal(_) => state.rect.height(), + } + } else { + match panel.side { + PanelSide::Vertical(_) => panel.default_size.unwrap_or_else(|| { + ui.style().spacing.interact_size.x + frame.inner_margin.sum().x + }), + PanelSide::Horizontal(_) => panel.default_size.unwrap_or_else(|| { + ui.style().spacing.interact_size.y + frame.inner_margin.sum().y + }), + } + } + } + + fn panel_rect(panel: &Panel, available_rect: Rect, mut size: f32) -> Rect { + let side = panel.side; + let size_range = panel.size_range; + + let mut panel_rect = available_rect; + + match side { + PanelSide::Vertical(_) => { + size = clamp_to_range(size, size_range).at_most(available_rect.width()); + } + PanelSide::Horizontal(_) => { + size = clamp_to_range(size, size_range).at_most(available_rect.height()); + } + } + side.set_rect_size(&mut panel_rect, size); + panel_rect + } + + fn prepare_resizing_response(&mut self, is_resizing: bool, pointer: Option) { + let side = self.panel.side; + let size_range = self.panel.size_range; + + if is_resizing && pointer.is_some() { + let pointer = pointer.unwrap(); + + match side { + PanelSide::Vertical(side) => { + self.size = (pointer.x - side.side_x(self.panel_rect)).abs(); + self.size = + clamp_to_range(self.size, size_range).at_most(self.available_rect.width()); + } + PanelSide::Horizontal(side) => { + self.size = (pointer.y - side.side_y(self.panel_rect)).abs(); + self.size = + clamp_to_range(self.size, size_range).at_most(self.available_rect.height()); + } + } + + side.set_rect_size(&mut self.panel_rect, self.size); + } + } +} + +// ---------------------------------------------------------------------------- + +/// A panel that covers an entire side +/// ([`left`](Panel::left), [`right`](Panel::right), +/// [`top`](Panel::top) or [`bottom`](Panel::bottom)) +/// of a [`Ui`] or screen. /// /// The order in which you add panels matter! /// The first panel you add will always be the outermost, and the last you add will always be the innermost. @@ -99,45 +292,83 @@ impl Side { /// /// ``` /// # egui::__run_test_ctx(|ctx| { -/// egui::SidePanel::left("my_left_panel").show(ctx, |ui| { +/// egui::Panel::left("my_left_panel").show(ctx, |ui| { /// ui.label("Hello World!"); /// }); /// # }); /// ``` -/// -/// See also [`TopBottomPanel`]. #[must_use = "You should call .show()"] -pub struct SidePanel { - side: Side, +pub struct Panel { + side: PanelSide, id: Id, frame: Option, resizable: bool, show_separator_line: bool, - default_width: f32, - width_range: Rangef, + + /// The size is defined as being either the width for a Vertical Panel + /// or the height for a Horizontal Panel. + default_size: Option, + + /// The size is defined as being either the width for a Vertical Panel + /// or the height for a Horizontal Panel. + size_range: Rangef, } -impl SidePanel { +impl Panel { + /// Create a left panel. + /// /// The id should be globally unique, e.g. `Id::new("my_left_panel")`. pub fn left(id: impl Into) -> Self { - Self::new(Side::Left, id) + Self::new(PanelSide::LEFT, id) } + /// Create a right panel. + /// /// The id should be globally unique, e.g. `Id::new("my_right_panel")`. pub fn right(id: impl Into) -> Self { - Self::new(Side::Right, id) + Self::new(PanelSide::RIGHT, id) } + /// Create a top panel. + /// + /// The id should be globally unique, e.g. `Id::new("my_top_panel")`. + /// + /// By default this is NOT resizable. + pub fn top(id: impl Into) -> Self { + Self::new(PanelSide::TOP, id).resizable(false) + } + + /// Create a bottom panel. + /// + /// The id should be globally unique, e.g. `Id::new("my_bottom_panel")`. + /// + /// By default this is NOT resizable. + pub fn bottom(id: impl Into) -> Self { + Self::new(PanelSide::BOTTOM, id).resizable(false) + } + + /// Create a panel. + /// /// The id should be globally unique, e.g. `Id::new("my_panel")`. - pub fn new(side: Side, id: impl Into) -> Self { + fn new(side: PanelSide, id: impl Into) -> Self { + let default_size: Option = match side { + PanelSide::Vertical(_) => Some(200.0), + PanelSide::Horizontal(_) => None, + }; + + let size_range: Rangef = match side { + PanelSide::Vertical(_) => Rangef::new(96.0, f32::INFINITY), + PanelSide::Horizontal(_) => Rangef::new(20.0, f32::INFINITY), + }; + Self { side, id: id.into(), frame: None, resizable: true, show_separator_line: true, - default_width: 200.0, - width_range: Rangef::new(96.0, f32::INFINITY), + default_size, + size_range, } } @@ -170,45 +401,47 @@ impl SidePanel { self } - /// The initial wrapping width of the [`SidePanel`], including margins. + /// The initial wrapping width of the [`Panel`], including margins. #[inline] - pub fn default_width(mut self, default_width: f32) -> Self { - self.default_width = default_width; - self.width_range = Rangef::new( - self.width_range.min.at_most(default_width), - self.width_range.max.at_least(default_width), + pub fn default_size(mut self, default_size: f32) -> Self { + self.default_size = Some(default_size); + self.size_range = Rangef::new( + self.size_range.min.at_most(default_size), + self.size_range.max.at_least(default_size), ); self } - /// Minimum width of the panel, including margins. + /// Minimum size of the panel, including margins. #[inline] - pub fn min_width(mut self, min_width: f32) -> Self { - self.width_range = Rangef::new(min_width, self.width_range.max.at_least(min_width)); + pub fn min_size(mut self, min_size: f32) -> Self { + self.size_range = Rangef::new(min_size, self.size_range.max.at_least(min_size)); self } - /// Maximum width of the panel, including margins. + /// Maximum size of the panel, including margins. #[inline] - pub fn max_width(mut self, max_width: f32) -> Self { - self.width_range = Rangef::new(self.width_range.min.at_most(max_width), max_width); + pub fn max_size(mut self, max_size: f32) -> Self { + self.size_range = Rangef::new(self.size_range.min.at_most(max_size), max_size); self } - /// The allowable width range for the panel, including margins. + /// The allowable size range for the panel, including margins. #[inline] - pub fn width_range(mut self, width_range: impl Into) -> Self { - let width_range = width_range.into(); - self.default_width = clamp_to_range(self.default_width, width_range); - self.width_range = width_range; + pub fn size_range(mut self, size_range: impl Into) -> Self { + let size_range = size_range.into(); + self.default_size = self + .default_size + .map(|default_size| clamp_to_range(default_size, size_range)); + self.size_range = size_range; self } - /// Enforce this exact width, including margins. + /// Enforce this exact size, including margins. #[inline] - pub fn exact_width(mut self, width: f32) -> Self { - self.default_width = width; - self.width_range = Rangef::point(width); + pub fn exact_size(mut self, size: f32) -> Self { + self.default_size = Some(size); + self.size_range = Rangef::point(size); self } @@ -220,7 +453,61 @@ impl SidePanel { } } -impl SidePanel { +// Deprecated +impl Panel { + #[deprecated = "Renamed default_size"] + pub fn default_width(self, default_size: f32) -> Self { + self.default_size(default_size) + } + + #[deprecated = "Renamed min_size"] + pub fn min_width(self, min_size: f32) -> Self { + self.min_size(min_size) + } + + #[deprecated = "Renamed max_size"] + pub fn max_width(self, max_size: f32) -> Self { + self.max_size(max_size) + } + + #[deprecated = "Renamed size_range"] + pub fn width_range(self, size_range: impl Into) -> Self { + self.size_range(size_range) + } + + #[deprecated = "Renamed exact_size"] + pub fn exact_width(self, size: f32) -> Self { + self.exact_size(size) + } + + #[deprecated = "Renamed default_size"] + pub fn default_height(self, default_size: f32) -> Self { + self.default_size(default_size) + } + + #[deprecated = "Renamed min_size"] + pub fn min_height(self, min_size: f32) -> Self { + self.min_size(min_size) + } + + #[deprecated = "Renamed max_size"] + pub fn max_height(self, max_size: f32) -> Self { + self.max_size(max_size) + } + + #[deprecated = "Renamed size_range"] + pub fn height_range(self, size_range: impl Into) -> Self { + self.size_range(size_range) + } + + #[deprecated = "Renamed exact_size"] + pub fn exact_height(self, size: f32) -> Self { + self.exact_size(size) + } +} + +// Public showing methods +impl Panel { /// Show the panel inside a [`Ui`]. pub fn show_inside( self, @@ -230,70 +517,170 @@ impl SidePanel { self.show_inside_dyn(ui, Box::new(add_contents)) } + /// Show the panel at the top level. + pub fn show( + self, + ctx: &Context, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse { + self.show_dyn(ctx, Box::new(add_contents)) + } + + /// Show the panel if `is_expanded` is `true`, + /// otherwise don't show it, but with a nice animation between collapsed and expanded. + pub fn show_animated( + self, + ctx: &Context, + is_expanded: bool, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> Option> { + let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded); + + let animated_panel = self.get_animated_panel(ctx, is_expanded)?; + + if how_expanded < 1.0 { + // Show a fake panel in this in-between animation state: + animated_panel.show(ctx, |_ui| {}); + None + } else { + // Show the real panel: + Some(animated_panel.show(ctx, add_contents)) + } + } + + /// Show the panel if `is_expanded` is `true`, + /// otherwise don't show it, but with a nice animation between collapsed and expanded. + pub fn show_animated_inside( + self, + ui: &mut Ui, + is_expanded: bool, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> Option> { + let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded); + + // Get either the fake or the real panel to animate + let animated_panel = self.get_animated_panel(ui.ctx(), is_expanded)?; + + if how_expanded < 1.0 { + // Show a fake panel in this in-between animation state: + animated_panel.show_inside(ui, |_ui| {}); + None + } else { + // Show the real panel: + Some(animated_panel.show_inside(ui, add_contents)) + } + } + + /// Show either a collapsed or a expanded panel, with a nice animation between. + pub fn show_animated_between( + ctx: &Context, + is_expanded: bool, + collapsed_panel: Self, + expanded_panel: Self, + add_contents: impl FnOnce(&mut Ui, f32) -> R, + ) -> Option> { + let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded); + + // Get either the fake or the real panel to animate + let animated_between_panel = + Self::get_animated_between_panel(ctx, is_expanded, collapsed_panel, expanded_panel); + + if 0.0 == how_expanded { + Some(animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded))) + } else if how_expanded < 1.0 { + // Show animation: + animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded)); + None + } else { + Some(animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded))) + } + } + + /// Show either a collapsed or a expanded panel, with a nice animation between. + pub fn show_animated_between_inside( + ui: &mut Ui, + is_expanded: bool, + collapsed_panel: Self, + expanded_panel: Self, + add_contents: impl FnOnce(&mut Ui, f32) -> R, + ) -> InnerResponse { + let how_expanded = + animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded); + + let animated_between_panel = Self::get_animated_between_panel( + ui.ctx(), + is_expanded, + collapsed_panel, + expanded_panel, + ); + + if 0.0 == how_expanded { + animated_between_panel.show_inside(ui, |ui| add_contents(ui, how_expanded)) + } else if how_expanded < 1.0 { + // Show animation: + animated_between_panel.show_inside(ui, |ui| add_contents(ui, how_expanded)) + } else { + animated_between_panel.show_inside(ui, |ui| add_contents(ui, how_expanded)) + } + } +} + +// Private methods to support the various show methods +impl Panel { /// Show the panel inside a [`Ui`]. fn show_inside_dyn<'c, R>( self, ui: &mut Ui, add_contents: Box R + 'c>, ) -> InnerResponse { - let Self { - side, - id, - frame, - resizable, - show_separator_line, - default_width, - width_range, - } = self; + let side = self.side; + let id = self.id; + let resizable = self.resizable; + let show_separator_line = self.show_separator_line; + let size_range = self.size_range; - let available_rect = ui.available_rect_before_wrap(); - let mut panel_rect = available_rect; - let mut width = default_width; - { - if let Some(state) = PanelState::load(ui.ctx(), id) { - width = state.rect.width(); - } - width = clamp_to_range(width, width_range).at_most(available_rect.width()); - side.set_rect_width(&mut panel_rect, width); - ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel"); + // Define the sizing of the panel. + let mut panel_sizer = PanelSizer::new(&self, ui); + + // Check for duplicate id + ui.ctx() + .check_for_id_clash(id, panel_sizer.panel_rect, "Panel"); + + if self.resizable { + // Prepare the resizable panel to avoid frame latency in the resize + self.prepare_resizable_panel(&mut panel_sizer, ui); } - let resize_id = id.with("__resize"); - let mut resize_hover = false; - let mut is_resizing = false; - if resizable { - // First we read the resize interaction results, to avoid frame latency in the resize: - if let Some(resize_response) = ui.ctx().read_response(resize_id) { - resize_hover = resize_response.hovered(); - is_resizing = resize_response.dragged(); - - if is_resizing && let Some(pointer) = resize_response.interact_pointer_pos() { - width = (pointer.x - side.side_x(panel_rect)).abs(); - width = clamp_to_range(width, width_range).at_most(available_rect.width()); - side.set_rect_width(&mut panel_rect, width); - } - } - } - - panel_rect = panel_rect.round_ui(); + // NOTE(shark98): This must be **after** the resizable preparation, as the size + // may change and round_ui() uses the size. + panel_sizer.panel_rect = panel_sizer.panel_rect.round_ui(); let mut panel_ui = ui.new_child( UiBuilder::new() .id_salt(id) - .ui_stack_info(UiStackInfo::new(match side { - Side::Left => UiKind::LeftPanel, - Side::Right => UiKind::RightPanel, - })) - .max_rect(panel_rect) + .ui_stack_info(UiStackInfo::new(side.ui_kind())) + .max_rect(panel_sizer.panel_rect) .layout(Layout::top_down(Align::Min)), ); - panel_ui.expand_to_include_rect(panel_rect); - panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475) + panel_ui.expand_to_include_rect(panel_sizer.panel_rect); + panel_ui.set_clip_rect(panel_sizer.panel_rect); // If we overflow, don't do so visibly (#4475) + + let inner_response = panel_sizer.frame.show(&mut panel_ui, |ui| { + match side { + PanelSide::Vertical(_) => { + ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height + ui.set_min_width( + (size_range.min - panel_sizer.frame.inner_margin.sum().x).at_least(0.0), + ); + } + PanelSide::Horizontal(_) => { + ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width + ui.set_min_height( + (size_range.min - panel_sizer.frame.inner_margin.sum().y).at_least(0.0), + ); + } + } - let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); - let inner_response = frame.show(&mut panel_ui, |ui| { - ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height - ui.set_min_width((width_range.min - frame.inner_margin.sum().x).at_least(0.0)); add_contents(ui) }); @@ -302,44 +689,31 @@ impl SidePanel { { let mut cursor = ui.cursor(); match side { - Side::Left => { - cursor.min.x = rect.max.x; - } - Side::Right => { - cursor.max.x = rect.min.x; - } + PanelSide::Vertical(side) => match side { + VerticalSide::Left => cursor.min.x = rect.max.x, + VerticalSide::Right => cursor.max.x = rect.min.x, + }, + PanelSide::Horizontal(side) => match side { + HorizontalSide::Top => cursor.min.y = rect.max.y, + HorizontalSide::Bottom => cursor.max.y = rect.min.y, + }, } ui.set_cursor(cursor); } + ui.expand_to_include_rect(rect); + let mut resize_hover = false; + let mut is_resizing = false; if resizable { - // Now we do the actual resize interaction, on top of all the contents. - // Otherwise its input could be eaten by the contents, e.g. a + // Now we do the actual resize interaction, on top of all the contents, + // otherwise its input could be eaten by the contents, e.g. a // `ScrollArea` on either side of the panel boundary. - let resize_x = side.opposite().side_x(panel_rect); - let resize_rect = Rect::from_x_y_ranges(resize_x..=resize_x, panel_rect.y_range()) - .expand2(vec2(ui.style().interaction.resize_grab_radius_side, 0.0)); - let resize_response = ui.interact(resize_rect, resize_id, Sense::drag()); - resize_hover = resize_response.hovered(); - is_resizing = resize_response.dragged(); + (resize_hover, is_resizing) = self.resize_panel(&panel_sizer, ui); } if resize_hover || is_resizing { - let cursor_icon = if width <= width_range.min { - match self.side { - Side::Left => CursorIcon::ResizeEast, - Side::Right => CursorIcon::ResizeWest, - } - } else if width < width_range.max { - CursorIcon::ResizeHorizontal - } else { - match self.side { - Side::Left => CursorIcon::ResizeWest, - Side::Right => CursorIcon::ResizeEast, - } - }; - ui.ctx().set_cursor_icon(cursor_icon); + ui.ctx().set_cursor_icon(self.cursor_icon(&panel_sizer)); } PanelState { rect }.store(ui.ctx(), id); @@ -356,25 +730,23 @@ impl SidePanel { Stroke::NONE }; // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done - let resize_x = side.opposite().side_x(rect); - - // Make sure the line is on the inside of the panel: - let resize_x = resize_x + 0.5 * side.sign() * stroke.width; - ui.painter().vline(resize_x, panel_rect.y_range(), stroke); + match side { + PanelSide::Vertical(side) => { + let x = side.opposite().side_x(rect) + 0.5 * side.sign() * stroke.width; + ui.painter() + .vline(x, panel_sizer.panel_rect.y_range(), stroke); + } + PanelSide::Horizontal(side) => { + let y = side.opposite().side_y(rect) + 0.5 * side.sign() * stroke.width; + ui.painter() + .hline(panel_sizer.panel_rect.x_range(), y, stroke); + } + } } inner_response } - /// Show the panel at the top level. - pub fn show( - self, - ctx: &Context, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> InnerResponse { - self.show_dyn(ctx, Box::new(add_contents)) - } - /// Show the panel at the top level. fn show_dyn<'c, R>( self, @@ -399,663 +771,177 @@ impl SidePanel { let rect = inner_response.response.rect; match side { - Side::Left => ctx.pass_state_mut(|state| { - state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)); - }), - Side::Right => ctx.pass_state_mut(|state| { - state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)); - }), + PanelSide::Vertical(side) => match side { + VerticalSide::Left => ctx.pass_state_mut(|state| { + state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)); + }), + VerticalSide::Right => ctx.pass_state_mut(|state| { + state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)); + }), + }, + PanelSide::Horizontal(side) => match side { + HorizontalSide::Top => { + ctx.pass_state_mut(|state| { + state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max)); + }); + } + HorizontalSide::Bottom => { + ctx.pass_state_mut(|state| { + state.allocate_bottom_panel(Rect::from_min_max( + rect.min, + available_rect.max, + )); + }); + } + }, } inner_response } - /// Show the panel if `is_expanded` is `true`, - /// otherwise don't show it, but with a nice animation between collapsed and expanded. - pub fn show_animated( - self, - ctx: &Context, - is_expanded: bool, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> Option> { - let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded); + fn prepare_resizable_panel(&self, panel_sizer: &mut PanelSizer<'_>, ui: &Ui) { + let resize_id = self.id.with("__resize"); + let resize_response = ui.ctx().read_response(resize_id); - if 0.0 == how_expanded { - None - } else if how_expanded < 1.0 { - // Show a fake panel in this in-between animation state: - // TODO(emilk): move the panel out-of-screen instead of changing its width. - // Then we can actually paint it as it animates. - let expanded_width = PanelState::load(ctx, self.id) - .map_or(self.default_width, |state| state.rect.width()); - let fake_width = how_expanded * expanded_width; - Self { - id: self.id.with("animating_panel"), - ..self + if resize_response.is_some() { + let resize_response = resize_response.unwrap(); + + // NOTE(sharky98): The original code was initializing to + // false first, but it doesn't seem necessary. + let is_resizing = resize_response.dragged(); + let pointer = resize_response.interact_pointer_pos(); + panel_sizer.prepare_resizing_response(is_resizing, pointer); + } + } + + fn resize_panel(&self, panel_sizer: &PanelSizer<'_>, ui: &Ui) -> (bool, bool) { + let (resize_x, resize_y, amount): (Rangef, Rangef, Vec2) = match self.side { + PanelSide::Vertical(side) => { + let resize_x = side.opposite().side_x(panel_sizer.panel_rect); + let resize_y = panel_sizer.panel_rect.y_range(); + ( + Rangef::from(resize_x..=resize_x), + resize_y, + vec2(ui.style().interaction.resize_grab_radius_side, 0.0), + ) } - .resizable(false) - .exact_width(fake_width) - .show(ctx, |_ui| {}); - None - } else { - // Show the real panel: - Some(self.show(ctx, add_contents)) - } - } - - /// Show the panel if `is_expanded` is `true`, - /// otherwise don't show it, but with a nice animation between collapsed and expanded. - pub fn show_animated_inside( - self, - ui: &mut Ui, - is_expanded: bool, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> Option> { - let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded); - - if 0.0 == how_expanded { - None - } else if how_expanded < 1.0 { - // Show a fake panel in this in-between animation state: - // TODO(emilk): move the panel out-of-screen instead of changing its width. - // Then we can actually paint it as it animates. - let expanded_width = PanelState::load(ui.ctx(), self.id) - .map_or(self.default_width, |state| state.rect.width()); - let fake_width = how_expanded * expanded_width; - Self { - id: self.id.with("animating_panel"), - ..self + PanelSide::Horizontal(side) => { + let resize_x = panel_sizer.panel_rect.x_range(); + let resize_y = side.opposite().side_y(panel_sizer.panel_rect); + ( + resize_x, + Rangef::from(resize_y..=resize_y), + vec2(0.0, ui.style().interaction.resize_grab_radius_side), + ) } - .resizable(false) - .exact_width(fake_width) - .show_inside(ui, |_ui| {}); - None - } else { - // Show the real panel: - Some(self.show_inside(ui, add_contents)) - } - } - - /// Show either a collapsed or a expanded panel, with a nice animation between. - pub fn show_animated_between( - ctx: &Context, - is_expanded: bool, - collapsed_panel: Self, - expanded_panel: Self, - add_contents: impl FnOnce(&mut Ui, f32) -> R, - ) -> Option> { - let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded); - - if 0.0 == how_expanded { - Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded))) - } else if how_expanded < 1.0 { - // Show animation: - let collapsed_width = PanelState::load(ctx, collapsed_panel.id) - .map_or(collapsed_panel.default_width, |state| state.rect.width()); - let expanded_width = PanelState::load(ctx, expanded_panel.id) - .map_or(expanded_panel.default_width, |state| state.rect.width()); - let fake_width = lerp(collapsed_width..=expanded_width, how_expanded); - Self { - id: expanded_panel.id.with("animating_panel"), - ..expanded_panel - } - .resizable(false) - .exact_width(fake_width) - .show(ctx, |ui| add_contents(ui, how_expanded)); - None - } else { - Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded))) - } - } - - /// Show either a collapsed or a expanded panel, with a nice animation between. - pub fn show_animated_between_inside( - ui: &mut Ui, - is_expanded: bool, - collapsed_panel: Self, - expanded_panel: Self, - add_contents: impl FnOnce(&mut Ui, f32) -> R, - ) -> InnerResponse { - let how_expanded = - animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded); - - if 0.0 == how_expanded { - collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded)) - } else if how_expanded < 1.0 { - // Show animation: - let collapsed_width = PanelState::load(ui.ctx(), collapsed_panel.id) - .map_or(collapsed_panel.default_width, |state| state.rect.width()); - let expanded_width = PanelState::load(ui.ctx(), expanded_panel.id) - .map_or(expanded_panel.default_width, |state| state.rect.width()); - let fake_width = lerp(collapsed_width..=expanded_width, how_expanded); - Self { - id: expanded_panel.id.with("animating_panel"), - ..expanded_panel - } - .resizable(false) - .exact_width(fake_width) - .show_inside(ui, |ui| add_contents(ui, how_expanded)) - } else { - expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded)) - } - } -} - -// ---------------------------------------------------------------------------- - -/// [`Top`](TopBottomSide::Top) or [`Bottom`](TopBottomSide::Bottom) -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum TopBottomSide { - Top, - Bottom, -} - -impl TopBottomSide { - fn opposite(self) -> Self { - match self { - Self::Top => Self::Bottom, - Self::Bottom => Self::Top, - } - } - - fn set_rect_height(self, rect: &mut Rect, height: f32) { - match self { - Self::Top => rect.max.y = rect.min.y + height, - Self::Bottom => rect.min.y = rect.max.y - height, - } - } - - fn side_y(self, rect: Rect) -> f32 { - match self { - Self::Top => rect.top(), - Self::Bottom => rect.bottom(), - } - } - - fn sign(self) -> f32 { - match self { - Self::Top => -1.0, - Self::Bottom => 1.0, - } - } -} - -/// A panel that covers the entire top or bottom of a [`Ui`] or screen. -/// -/// The order in which you add panels matter! -/// The first panel you add will always be the outermost, and the last you add will always be the innermost. -/// -/// ⚠ Always add any [`CentralPanel`] last. -/// -/// See the [module level docs](crate::containers::panel) for more details. -/// -/// ``` -/// # egui::__run_test_ctx(|ctx| { -/// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| { -/// ui.label("Hello World!"); -/// }); -/// # }); -/// ``` -/// -/// See also [`SidePanel`]. -#[must_use = "You should call .show()"] -pub struct TopBottomPanel { - side: TopBottomSide, - id: Id, - frame: Option, - resizable: bool, - show_separator_line: bool, - default_height: Option, - height_range: Rangef, -} - -impl TopBottomPanel { - /// The id should be globally unique, e.g. `Id::new("my_top_panel")`. - pub fn top(id: impl Into) -> Self { - Self::new(TopBottomSide::Top, id) - } - - /// The id should be globally unique, e.g. `Id::new("my_bottom_panel")`. - pub fn bottom(id: impl Into) -> Self { - Self::new(TopBottomSide::Bottom, id) - } - - /// The id should be globally unique, e.g. `Id::new("my_panel")`. - pub fn new(side: TopBottomSide, id: impl Into) -> Self { - Self { - side, - id: id.into(), - frame: None, - resizable: false, - show_separator_line: true, - default_height: None, - height_range: Rangef::new(20.0, f32::INFINITY), - } - } - - /// Can panel be resized by dragging the edge of it? - /// - /// Default is `false`. - /// - /// If you want your panel to be resizable you also need to make the ui use - /// the available space. - /// - /// This can be done by using [`Ui::take_available_space`], or using a - /// widget in it that takes up more space as you resize it, such as: - /// * Wrapping text ([`Ui::horizontal_wrapped`]). - /// * A [`crate::ScrollArea`]. - /// * A [`crate::Separator`]. - /// * A [`crate::TextEdit`]. - /// * … - #[inline] - pub fn resizable(mut self, resizable: bool) -> Self { - self.resizable = resizable; - self - } - - /// Show a separator line, even when not interacting with it? - /// - /// Default: `true`. - #[inline] - pub fn show_separator_line(mut self, show_separator_line: bool) -> Self { - self.show_separator_line = show_separator_line; - self - } - - /// The initial height of the [`TopBottomPanel`], including margins. - /// Defaults to [`crate::style::Spacing::interact_size`].y, plus frame margins. - #[inline] - pub fn default_height(mut self, default_height: f32) -> Self { - self.default_height = Some(default_height); - self.height_range = Rangef::new( - self.height_range.min.at_most(default_height), - self.height_range.max.at_least(default_height), - ); - self - } - - /// Minimum height of the panel, including margins. - #[inline] - pub fn min_height(mut self, min_height: f32) -> Self { - self.height_range = Rangef::new(min_height, self.height_range.max.at_least(min_height)); - self - } - - /// Maximum height of the panel, including margins. - #[inline] - pub fn max_height(mut self, max_height: f32) -> Self { - self.height_range = Rangef::new(self.height_range.min.at_most(max_height), max_height); - self - } - - /// The allowable height range for the panel, including margins. - #[inline] - pub fn height_range(mut self, height_range: impl Into) -> Self { - let height_range = height_range.into(); - self.default_height = self - .default_height - .map(|default_height| clamp_to_range(default_height, height_range)); - self.height_range = height_range; - self - } - - /// Enforce this exact height, including margins. - #[inline] - pub fn exact_height(mut self, height: f32) -> Self { - self.default_height = Some(height); - self.height_range = Rangef::point(height); - self - } - - /// Change the background color, margins, etc. - #[inline] - pub fn frame(mut self, frame: Frame) -> Self { - self.frame = Some(frame); - self - } -} - -impl TopBottomPanel { - /// Show the panel inside a [`Ui`]. - pub fn show_inside( - self, - ui: &mut Ui, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> InnerResponse { - self.show_inside_dyn(ui, Box::new(add_contents)) - } - - /// Show the panel inside a [`Ui`]. - fn show_inside_dyn<'c, R>( - self, - ui: &mut Ui, - add_contents: Box R + 'c>, - ) -> InnerResponse { - let Self { - side, - id, - frame, - resizable, - show_separator_line, - default_height, - height_range, - } = self; - - let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); - - let available_rect = ui.available_rect_before_wrap(); - let mut panel_rect = available_rect; - - let mut height = if let Some(state) = PanelState::load(ui.ctx(), id) { - state.rect.height() - } else { - default_height - .unwrap_or_else(|| ui.style().spacing.interact_size.y + frame.inner_margin.sum().y) }; - { - height = clamp_to_range(height, height_range).at_most(available_rect.height()); - side.set_rect_height(&mut panel_rect, height); - ui.ctx() - .check_for_id_clash(id, panel_rect, "TopBottomPanel"); - } - let resize_id = id.with("__resize"); - let mut resize_hover = false; - let mut is_resizing = false; - if resizable { - // First we read the resize interaction results, to avoid frame latency in the resize: - if let Some(resize_response) = ui.ctx().read_response(resize_id) { - resize_hover = resize_response.hovered(); - is_resizing = resize_response.dragged(); + let resize_id = self.id.with("__resize"); + let resize_rect = Rect::from_x_y_ranges(resize_x, resize_y).expand2(amount); + let resize_response = ui.interact(resize_rect, resize_id, Sense::drag()); - if is_resizing && let Some(pointer) = resize_response.interact_pointer_pos() { - height = (pointer.y - side.side_y(panel_rect)).abs(); - height = clamp_to_range(height, height_range).at_most(available_rect.height()); - side.set_rect_height(&mut panel_rect, height); - } - } - } - - panel_rect = panel_rect.round_ui(); - - let mut panel_ui = ui.new_child( - UiBuilder::new() - .id_salt(id) - .ui_stack_info(UiStackInfo::new(match side { - TopBottomSide::Top => UiKind::TopPanel, - TopBottomSide::Bottom => UiKind::BottomPanel, - })) - .max_rect(panel_rect) - .layout(Layout::top_down(Align::Min)), - ); - panel_ui.expand_to_include_rect(panel_rect); - panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475) - - let inner_response = frame.show(&mut panel_ui, |ui| { - ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width - ui.set_min_height((height_range.min - frame.inner_margin.sum().y).at_least(0.0)); - add_contents(ui) - }); - - let rect = inner_response.response.rect; - - { - let mut cursor = ui.cursor(); - match side { - TopBottomSide::Top => { - cursor.min.y = rect.max.y; - } - TopBottomSide::Bottom => { - cursor.max.y = rect.min.y; - } - } - ui.set_cursor(cursor); - } - ui.expand_to_include_rect(rect); - - if resizable { - // Now we do the actual resize interaction, on top of all the contents. - // Otherwise its input could be eaten by the contents, e.g. a - // `ScrollArea` on either side of the panel boundary. - - let resize_y = side.opposite().side_y(panel_rect); - let resize_rect = Rect::from_x_y_ranges(panel_rect.x_range(), resize_y..=resize_y) - .expand2(vec2(0.0, ui.style().interaction.resize_grab_radius_side)); - let resize_response = ui.interact(resize_rect, resize_id, Sense::drag()); - resize_hover = resize_response.hovered(); - is_resizing = resize_response.dragged(); - } - - if resize_hover || is_resizing { - let cursor_icon = if height <= height_range.min { - match self.side { - TopBottomSide::Top => CursorIcon::ResizeSouth, - TopBottomSide::Bottom => CursorIcon::ResizeNorth, - } - } else if height < height_range.max { - CursorIcon::ResizeVertical - } else { - match self.side { - TopBottomSide::Top => CursorIcon::ResizeNorth, - TopBottomSide::Bottom => CursorIcon::ResizeSouth, - } - }; - ui.ctx().set_cursor_icon(cursor_icon); - } - - PanelState { rect }.store(ui.ctx(), id); - - { - let stroke = if is_resizing { - ui.style().visuals.widgets.active.fg_stroke // highly visible - } else if resize_hover { - ui.style().visuals.widgets.hovered.fg_stroke // highly visible - } else if show_separator_line { - // TODO(emilk): distinguish resizable from non-resizable - ui.style().visuals.widgets.noninteractive.bg_stroke // dim - } else { - Stroke::NONE - }; - // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done - let resize_y = side.opposite().side_y(rect); - - // Make sure the line is on the inside of the panel: - let resize_y = resize_y + 0.5 * side.sign() * stroke.width; - ui.painter().hline(panel_rect.x_range(), resize_y, stroke); - } - - inner_response + (resize_response.hovered(), resize_response.dragged()) } - /// Show the panel at the top level. - pub fn show( - self, - ctx: &Context, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> InnerResponse { - self.show_dyn(ctx, Box::new(add_contents)) - } - - /// Show the panel at the top level. - fn show_dyn<'c, R>( - self, - ctx: &Context, - add_contents: Box R + 'c>, - ) -> InnerResponse { - let available_rect = ctx.available_rect(); - let side = self.side; - - let mut panel_ui = Ui::new( - ctx.clone(), - self.id, - UiBuilder::new() - .layer_id(LayerId::background()) - .max_rect(available_rect), - ); - panel_ui.set_clip_rect(ctx.content_rect()); - - let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); - let rect = inner_response.response.rect; - - match side { - TopBottomSide::Top => { - ctx.pass_state_mut(|state| { - state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max)); - }); + fn cursor_icon(&self, panel_sizer: &PanelSizer<'_>) -> CursorIcon { + if panel_sizer.size <= self.size_range.min { + match self.side { + PanelSide::Vertical(side) => match side { + VerticalSide::Left => CursorIcon::ResizeEast, + VerticalSide::Right => CursorIcon::ResizeWest, + }, + PanelSide::Horizontal(side) => match side { + HorizontalSide::Top => CursorIcon::ResizeSouth, + HorizontalSide::Bottom => CursorIcon::ResizeNorth, + }, } - TopBottomSide::Bottom => { - ctx.pass_state_mut(|state| { - state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max)); - }); + } else if panel_sizer.size < self.size_range.max { + match self.side { + PanelSide::Vertical(_) => CursorIcon::ResizeHorizontal, + PanelSide::Horizontal(_) => CursorIcon::ResizeVertical, + } + } else { + match self.side { + PanelSide::Vertical(side) => match side { + VerticalSide::Left => CursorIcon::ResizeWest, + VerticalSide::Right => CursorIcon::ResizeEast, + }, + PanelSide::Horizontal(side) => match side { + HorizontalSide::Top => CursorIcon::ResizeNorth, + HorizontalSide::Bottom => CursorIcon::ResizeSouth, + }, } } - - inner_response } - /// Show the panel if `is_expanded` is `true`, - /// otherwise don't show it, but with a nice animation between collapsed and expanded. - pub fn show_animated( - self, - ctx: &Context, - is_expanded: bool, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> Option> { + /// Get the real or fake panel to animate if `is_expanded` is `true`. + fn get_animated_panel(self, ctx: &Context, is_expanded: bool) -> Option { let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded); if 0.0 == how_expanded { None } else if how_expanded < 1.0 { // Show a fake panel in this in-between animation state: - // TODO(emilk): move the panel out-of-screen instead of changing its height. + // TODO(emilk): move the panel out-of-screen instead of changing its width. // Then we can actually paint it as it animates. - let expanded_height = PanelState::load(ctx, self.id) - .map(|state| state.rect.height()) - .or(self.default_height) - .unwrap_or_else(|| ctx.style().spacing.interact_size.y); - let fake_height = how_expanded * expanded_height; - Self { - id: self.id.with("animating_panel"), - ..self - } - .resizable(false) - .exact_height(fake_height) - .show(ctx, |_ui| {}); - None + let expanded_size = Self::animated_size(ctx, &self); + let fake_size = how_expanded * expanded_size; + Some( + Self { + id: self.id.with("animating_panel"), + ..self + } + .resizable(false) + .exact_size(fake_size), + ) } else { // Show the real panel: - Some(self.show(ctx, add_contents)) + Some(self) } } - /// Show the panel if `is_expanded` is `true`, - /// otherwise don't show it, but with a nice animation between collapsed and expanded. - pub fn show_animated_inside( - self, - ui: &mut Ui, - is_expanded: bool, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> Option> { - let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded); - - if 0.0 == how_expanded { - None - } else if how_expanded < 1.0 { - // Show a fake panel in this in-between animation state: - // TODO(emilk): move the panel out-of-screen instead of changing its height. - // Then we can actually paint it as it animates. - let expanded_height = PanelState::load(ui.ctx(), self.id) - .map(|state| state.rect.height()) - .or(self.default_height) - .unwrap_or_else(|| ui.style().spacing.interact_size.y); - let fake_height = how_expanded * expanded_height; - Self { - id: self.id.with("animating_panel"), - ..self - } - .resizable(false) - .exact_height(fake_height) - .show_inside(ui, |_ui| {}); - None - } else { - // Show the real panel: - Some(self.show_inside(ui, add_contents)) - } - } - - /// Show either a collapsed or a expanded panel, with a nice animation between. - pub fn show_animated_between( + /// Get either the collapsed or expended panel to animate. + fn get_animated_between_panel( ctx: &Context, is_expanded: bool, collapsed_panel: Self, expanded_panel: Self, - add_contents: impl FnOnce(&mut Ui, f32) -> R, - ) -> Option> { + ) -> Self { let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded); if 0.0 == how_expanded { - Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded))) + collapsed_panel } else if how_expanded < 1.0 { - // Show animation: - let collapsed_height = PanelState::load(ctx, collapsed_panel.id) - .map(|state| state.rect.height()) - .or(collapsed_panel.default_height) - .unwrap_or_else(|| ctx.style().spacing.interact_size.y); + let collapsed_size = Self::animated_size(ctx, &collapsed_panel); + let expanded_size = Self::animated_size(ctx, &expanded_panel); - let expanded_height = PanelState::load(ctx, expanded_panel.id) - .map(|state| state.rect.height()) - .or(expanded_panel.default_height) - .unwrap_or_else(|| ctx.style().spacing.interact_size.y); + let fake_size = lerp(collapsed_size..=expanded_size, how_expanded); - let fake_height = lerp(collapsed_height..=expanded_height, how_expanded); Self { id: expanded_panel.id.with("animating_panel"), ..expanded_panel } .resizable(false) - .exact_height(fake_height) - .show(ctx, |ui| add_contents(ui, how_expanded)); - None + .exact_size(fake_size) } else { - Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded))) + expanded_panel } } - /// Show either a collapsed or a expanded panel, with a nice animation between. - pub fn show_animated_between_inside( - ui: &mut Ui, - is_expanded: bool, - collapsed_panel: Self, - expanded_panel: Self, - add_contents: impl FnOnce(&mut Ui, f32) -> R, - ) -> InnerResponse { - let how_expanded = - animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded); + fn animated_size(ctx: &Context, panel: &Self) -> f32 { + let get_rect_state_size = |state: PanelState| match panel.side { + PanelSide::Vertical(_) => state.rect.width(), + PanelSide::Horizontal(_) => state.rect.height(), + }; - if 0.0 == how_expanded { - collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded)) - } else if how_expanded < 1.0 { - // Show animation: - let collapsed_height = PanelState::load(ui.ctx(), collapsed_panel.id) - .map(|state| state.rect.height()) - .or(collapsed_panel.default_height) - .unwrap_or_else(|| ui.style().spacing.interact_size.y); + let get_spacing_size = || match panel.side { + PanelSide::Vertical(_) => ctx.style().spacing.interact_size.x, + PanelSide::Horizontal(_) => ctx.style().spacing.interact_size.y, + }; - let expanded_height = PanelState::load(ui.ctx(), expanded_panel.id) - .map(|state| state.rect.height()) - .or(expanded_panel.default_height) - .unwrap_or_else(|| ui.style().spacing.interact_size.y); - - let fake_height = lerp(collapsed_height..=expanded_height, how_expanded); - Self { - id: expanded_panel.id.with("animating_panel"), - ..expanded_panel - } - .resizable(false) - .exact_height(fake_height) - .show_inside(ui, |ui| add_contents(ui, how_expanded)) - } else { - expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded)) - } + PanelState::load(ctx, panel.id) + .map(get_rect_state_size) + .or(panel.default_size) + .unwrap_or(get_spacing_size()) } } @@ -1075,8 +961,8 @@ impl TopBottomPanel { /// /// ``` /// # egui::__run_test_ctx(|ctx| { -/// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| { -/// ui.label("Hello World! From `TopBottomPanel`, that must be before `CentralPanel`!"); +/// egui::Panel::top("my_panel").show(ctx, |ui| { +/// ui.label("Hello World! From `Panel`, that must be before `CentralPanel`!"); /// }); /// egui::CentralPanel::default().show(ctx, |ui| { /// ui.label("Hello World!"); @@ -1158,6 +1044,13 @@ impl CentralPanel { ); panel_ui.set_clip_rect(ctx.content_rect()); + if false { + // TODO(emilk): @lucasmerlin shouldn't we enable this? + panel_ui + .response() + .widget_info(|| WidgetInfo::new(WidgetType::Panel)); + } + let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); // Only inform ctx about what we actually used, so we can shrink the native window to fit. @@ -1171,3 +1064,11 @@ fn clamp_to_range(x: f32, range: Rangef) -> f32 { let range = range.as_positive(); x.clamp(range.min, range.max) } + +// ---------------------------------------------------------------------------- + +#[deprecated = "Use Panel::left or Panel::right instead"] +pub type SidePanel = super::Panel; + +#[deprecated = "Use Panel::top or Panel::bottom instead"] +pub type TopBottomPanel = super::Panel; diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 587c3b377..2ca7af4fc 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -764,7 +764,7 @@ impl Context { /// and only on the rare occasion that [`Context::request_discard`] is called. /// Usually, it `run_ui` will only be called once. /// - /// Put your widgets into a [`crate::SidePanel`], [`crate::TopBottomPanel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`]. + /// Put your widgets into a [`crate::Panel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`]. /// /// Instead of calling `run`, you can alternatively use [`Self::begin_pass`] and [`Context::end_pass`]. /// diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 3071f7196..bd4319f0b 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -9,7 +9,7 @@ //! which uses [`eframe`](https://docs.rs/eframe). //! //! To create a GUI using egui you first need a [`Context`] (by convention referred to by `ctx`). -//! Then you add a [`Window`] or a [`SidePanel`] to get a [`Ui`], which is what you'll be using to add all the buttons and labels that you need. +//! Then you add a [`Window`] or a [`Panel`] to get a [`Ui`], which is what you'll be using to add all the buttons and labels that you need. //! //! //! ## Feature flags @@ -45,7 +45,7 @@ //! //! ### Getting a [`Ui`] //! -//! Use one of [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`] to +//! Use one of [`Panel`], [`CentralPanel`], [`Window`] or [`Area`] to //! get access to an [`Ui`] where you can put widgets. For example: //! //! ``` @@ -322,7 +322,7 @@ //! when you release the panel/window shrinks again. //! This is an artifact of immediate mode, and here are some alternatives on how to avoid it: //! -//! 1. Turn off resizing with [`Window::resizable`], [`SidePanel::resizable`], [`TopBottomPanel::resizable`]. +//! 1. Turn off resizing with [`Window::resizable`], [`Panel::resizable`]. //! 2. Wrap your panel contents in a [`ScrollArea`], or use [`Window::vscroll`] and [`Window::hscroll`]. //! 3. Use a justified layout: //! diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 4d746c074..e5fb04b0d 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -84,7 +84,7 @@ fn set_menu_style(style: &mut Style) { } } -/// The menu bar goes well in a [`crate::TopBottomPanel::top`], +/// The menu bar goes well in a [`crate::Panel::top`], /// but can also be placed in a [`crate::Window`]. /// In the latter case you may want to wrap it in [`Frame`]. #[deprecated = "Use `egui::MenuBar::new().ui(` instead"] diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 08bb9cee5..3c7fca2f3 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -119,7 +119,7 @@ impl Ui { /// Create a new top-level [`Ui`]. /// /// Normally you would not use this directly, but instead use - /// [`crate::SidePanel`], [`crate::TopBottomPanel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`]. + /// [`crate::Panel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`]. pub fn new(ctx: Context, id: Id, ui_builder: UiBuilder) -> Self { let UiBuilder { id_salt, diff --git a/crates/egui/src/ui_stack.rs b/crates/egui/src/ui_stack.rs index 4136218bd..07026c45b 100644 --- a/crates/egui/src/ui_stack.rs +++ b/crates/egui/src/ui_stack.rs @@ -12,16 +12,16 @@ pub enum UiKind { /// A [`crate::CentralPanel`]. CentralPanel, - /// A left [`crate::SidePanel`]. + /// A left [`crate::Panel`]. LeftPanel, - /// A right [`crate::SidePanel`]. + /// A right [`crate::Panel`]. RightPanel, - /// A top [`crate::TopBottomPanel`]. + /// A top [`crate::Panel`]. TopPanel, - /// A bottom [`crate::TopBottomPanel`]. + /// A bottom [`crate::Panel`]. BottomPanel, /// A modal [`crate::Modal`]. diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index f721b710c..9ba3a8082 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -1,12 +1,13 @@ +use std::mem; + use accesskit::{Action, ActionRequest, NodeId}; use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler}; + use eframe::epaint::text::TextWrapMode; -use egui::collapsing_header::CollapsingState; use egui::{ Button, Color32, Context, Event, Frame, FullOutput, Id, Key, KeyboardShortcut, Label, - Modifiers, RawInput, RichText, ScrollArea, SidePanel, TopBottomPanel, Ui, + Modifiers, Panel, RawInput, RichText, ScrollArea, Ui, collapsing_header::CollapsingState, }; -use std::mem; /// This [`egui::Plugin`] adds an inspector Panel. /// @@ -86,10 +87,10 @@ impl egui::Plugin for AccessibilityInspectorPlugin { ctx.enable_accesskit(); - SidePanel::right(Self::id()).show(ctx, |ui| { + Panel::right(Self::id()).show(ctx, |ui| { ui.heading("🔎 AccessKit Inspector"); if let Some(selected_node) = self.selected_node { - TopBottomPanel::bottom(Self::id().with("details_panel")) + Panel::bottom(Self::id().with("details_panel")) .frame(Frame::new()) .show_separator_line(false) .show_inside(ui, |ui| { diff --git a/crates/egui_demo_app/src/apps/http_app.rs b/crates/egui_demo_app/src/apps/http_app.rs index f16aa5969..2630fa862 100644 --- a/crates/egui_demo_app/src/apps/http_app.rs +++ b/crates/egui_demo_app/src/apps/http_app.rs @@ -61,7 +61,7 @@ impl Default for HttpApp { impl eframe::App for HttpApp { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - egui::TopBottomPanel::bottom("http_bottom").show(ctx, |ui| { + egui::Panel::bottom("http_bottom").show(ctx, |ui| { let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true); ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| { ui.add(egui_demo_lib::egui_github_link_file!()) diff --git a/crates/egui_demo_app/src/apps/image_viewer.rs b/crates/egui_demo_app/src/apps/image_viewer.rs index c341d2385..052996eef 100644 --- a/crates/egui_demo_app/src/apps/image_viewer.rs +++ b/crates/egui_demo_app/src/apps/image_viewer.rs @@ -2,8 +2,6 @@ use egui::ImageFit; use egui::Slider; use egui::Vec2; use egui::emath::Rot2; -use egui::panel::Side; -use egui::panel::TopBottomSide; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct ImageViewer { @@ -52,7 +50,7 @@ impl Default for ImageViewer { impl eframe::App for ImageViewer { fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { - egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| { + egui::Panel::top("url bar").show(ctx, |ui| { ui.horizontal_centered(|ui| { let label = ui.label("URI:"); ui.text_edit_singleline(&mut self.uri_edit_text) @@ -73,7 +71,7 @@ impl eframe::App for ImageViewer { }); }); - egui::SidePanel::new(Side::Left, "controls").show(ctx, |ui| { + egui::Panel::left("controls").show(ctx, |ui| { // uv ui.label("UV"); ui.add(Slider::new(&mut self.image_options.uv.min.x, 0.0..=1.0).text("min x")); diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index bec0aacf3..c118f280c 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -295,7 +295,7 @@ impl eframe::App for WrapApp { } let mut cmd = Command::Nothing; - egui::TopBottomPanel::top("wrap_app_top_bar") + egui::Panel::top("wrap_app_top_bar") .frame(egui::Frame::new().inner_margin(4)) .show(ctx, |ui| { ui.horizontal_wrapped(|ui| { @@ -341,7 +341,7 @@ impl WrapApp { let mut cmd = Command::Nothing; - egui::SidePanel::left("backend_panel") + egui::Panel::left("backend_panel") .resizable(false) .show_animated(ctx, is_open, |ui| { ui.add_space(4.0); diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 8033539dd..179543680 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -236,7 +236,7 @@ impl DemoWindows { } fn mobile_top_bar(&mut self, ctx: &Context) { - egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { + egui::Panel::top("menu_bar").show(ctx, |ui| { menu::MenuBar::new() .config(menu::MenuConfig::new().style(StyleModifier::default())) .ui(ui, |ui| { @@ -262,10 +262,10 @@ impl DemoWindows { } fn desktop_ui(&mut self, ctx: &Context) { - egui::SidePanel::right("egui_demo_panel") + egui::Panel::right("egui_demo_panel") .resizable(false) - .default_width(160.0) - .min_width(160.0) + .default_size(160.0) + .min_size(160.0) .show(ctx, |ui| { ui.add_space(4.0); ui.vertical_centered(|ui| { @@ -289,7 +289,7 @@ impl DemoWindows { self.demo_list_ui(ui); }); - egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { + egui::Panel::top("menu_bar").show(ctx, |ui| { menu::MenuBar::new().ui(ui, |ui| { file_menu_button(ui); }); diff --git a/crates/egui_demo_lib/src/demo/panels.rs b/crates/egui_demo_lib/src/demo/panels.rs index f94513866..55771c1a1 100644 --- a/crates/egui_demo_lib/src/demo/panels.rs +++ b/crates/egui_demo_lib/src/demo/panels.rs @@ -22,9 +22,9 @@ impl crate::View for Panels { fn ui(&mut self, ui: &mut egui::Ui) { // Note that the order we add the panels is very important! - egui::TopBottomPanel::top("top_panel") + egui::Panel::top("top_panel") .resizable(true) - .min_height(32.0) + .min_size(32.0) .show_inside(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { ui.vertical_centered(|ui| { @@ -34,10 +34,10 @@ impl crate::View for Panels { }); }); - egui::SidePanel::left("left_panel") + egui::Panel::left("left_panel") .resizable(true) - .default_width(150.0) - .width_range(80.0..=200.0) + .default_size(150.0) + .size_range(80.0..=200.0) .show_inside(ui, |ui| { ui.vertical_centered(|ui| { ui.heading("Left Panel"); @@ -47,10 +47,10 @@ impl crate::View for Panels { }); }); - egui::SidePanel::right("right_panel") + egui::Panel::right("right_panel") .resizable(true) - .default_width(150.0) - .width_range(80.0..=200.0) + .default_size(150.0) + .size_range(80.0..=200.0) .show_inside(ui, |ui| { ui.vertical_centered(|ui| { ui.heading("Right Panel"); @@ -60,9 +60,9 @@ impl crate::View for Panels { }); }); - egui::TopBottomPanel::bottom("bottom_panel") + egui::Panel::bottom("bottom_panel") .resizable(false) - .min_height(0.0) + .min_size(0.0) .show_inside(ui, |ui| { ui.vertical_centered(|ui| { ui.heading("Bottom Panel"); diff --git a/crates/egui_demo_lib/src/demo/tooltips.rs b/crates/egui_demo_lib/src/demo/tooltips.rs index 0e391c553..cc474bd4d 100644 --- a/crates/egui_demo_lib/src/demo/tooltips.rs +++ b/crates/egui_demo_lib/src/demo/tooltips.rs @@ -35,7 +35,7 @@ impl crate::View for Tooltips { ui.add(crate::egui_github_link_file_line!()); }); - egui::SidePanel::right("scroll_test").show_inside(ui, |ui| { + egui::Panel::right("scroll_test").show_inside(ui, |ui| { ui.label( "The scroll area below has many labels with interactive tooltips. \ The purpose is to test that the tooltips close when you scroll.", diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index 9a66b8bc5..28e12630e 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -33,7 +33,7 @@ impl Default for EasyMarkEditor { impl EasyMarkEditor { pub fn panels(&mut self, ctx: &egui::Context) { - egui::TopBottomPanel::bottom("easy_mark_bottom").show(ctx, |ui| { + egui::Panel::bottom("easy_mark_bottom").show(ctx, |ui| { let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true); ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| { ui.add(crate::egui_github_link_file!()) diff --git a/crates/egui_glow/examples/pure_glow.rs b/crates/egui_glow/examples/pure_glow.rs index a56b85bc1..3671c8d79 100644 --- a/crates/egui_glow/examples/pure_glow.rs +++ b/crates/egui_glow/examples/pure_glow.rs @@ -218,7 +218,7 @@ impl winit::application::ApplicationHandler for GlowApp { self.egui_glow.as_mut().unwrap().run( self.gl_window.as_mut().unwrap().window(), |egui_ctx| { - egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| { + egui::Panel::left("my_side_panel").show(egui_ctx, |ui| { ui.heading("Hello World!"); if ui.button("Quit").clicked() { quit = true; diff --git a/examples/hello_android/src/lib.rs b/examples/hello_android/src/lib.rs index c138b97e1..1de7684f5 100644 --- a/examples/hello_android/src/lib.rs +++ b/examples/hello_android/src/lib.rs @@ -41,7 +41,7 @@ impl eframe::App for MyApp { // TODO(lucasmerlin): This is a pretty big hack, should be fixed once safe_area implemented // for android: // https://github.com/rust-windowing/winit/issues/3910 - egui::TopBottomPanel::top("status_bar_space").show(ctx, |ui| { + egui::Panel::top("status_bar_space").show(ctx, |ui| { ui.set_height(32.0); }); diff --git a/tests/test_size_pass/src/main.rs b/tests/test_size_pass/src/main.rs index 6bb293302..ce645eb98 100644 --- a/tests/test_size_pass/src/main.rs +++ b/tests/test_size_pass/src/main.rs @@ -9,7 +9,7 @@ fn main() -> eframe::Result { let options = eframe::NativeOptions::default(); eframe::run_simple_native("My egui App", options, move |ctx, _frame| { // A bottom panel to force the tooltips to consider if the fit below or under the widget: - egui::TopBottomPanel::bottom("bottom").show(ctx, |ui| { + egui::Panel::bottom("bottom").show(ctx, |ui| { ui.horizontal(|ui| { ui.vertical(|ui| { ui.label("Single tooltips:"); diff --git a/tests/test_ui_stack/src/main.rs b/tests/test_ui_stack/src/main.rs index 47b4ee5ca..bb2158297 100644 --- a/tests/test_ui_stack/src/main.rs +++ b/tests/test_ui_stack/src/main.rs @@ -34,7 +34,7 @@ impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { ctx.all_styles_mut(|style| style.interaction.tooltip_delay = 0.0); - egui::SidePanel::left("side_panel_left").show(ctx, |ui| { + egui::Panel::left("side_panel_left").show(ctx, |ui| { ui.heading("Information"); ui.label( "This is a demo/test environment of the `UiStack` feature. The tables display \ @@ -82,7 +82,7 @@ impl eframe::App for MyApp { }); }); - egui::SidePanel::right("side_panel_right").show(ctx, |ui| { + egui::Panel::right("side_panel_right").show(ctx, |ui| { egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| { stack_ui(ui); @@ -170,7 +170,7 @@ impl eframe::App for MyApp { }); }); - egui::TopBottomPanel::bottom("bottom_panel") + egui::Panel::bottom("bottom_panel") .resizable(true) .show(ctx, |ui| { egui::ScrollArea::vertical()