diff --git a/CHANGELOG.md b/CHANGELOG.md index f738b00295b..78be94a284d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,13 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ## Unreleased -* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)). * ⚠️ BREAKING: egui now expects integrations to do all color blending in gamma space ([#2071](https://github.com/emilk/egui/pull/2071)). +### Added ⭐ +* Added helper functions for animating panels that collapse/expand ([#2190](https://github.com/emilk/egui/pull/2190)). + ### Fixed 🐛 +* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)). * Improved text rendering ([#2071](https://github.com/emilk/egui/pull/2071)). diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 233ceae4470..73b3f8d39ac 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -19,17 +19,23 @@ use std::ops::RangeInclusive; use crate::*; +/// State regarding panels. #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -struct PanelState { - rect: Rect, +pub struct PanelState { + pub rect: Rect, } impl PanelState { - fn load(ctx: &Context, bar_id: Id) -> Option { + pub fn load(ctx: &Context, bar_id: Id) -> Option { ctx.data().get_persisted(bar_id) } + /// The size of the panel (from previous frame). + pub fn size(&self) -> Vec2 { + self.rect.size() + } + fn store(self, ctx: &Context, bar_id: Id) { ctx.data().insert_persisted(bar_id, self); } @@ -96,21 +102,21 @@ pub struct SidePanel { } impl SidePanel { - /// `id_source`: Something unique, e.g. `"my_left_panel"`. - pub fn left(id_source: impl std::hash::Hash) -> Self { - Self::new(Side::Left, id_source) + /// 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) } - /// `id_source`: Something unique, e.g. `"my_right_panel"`. - pub fn right(id_source: impl std::hash::Hash) -> Self { - Self::new(Side::Right, id_source) + /// 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) } - /// `id_source`: Something unique, e.g. `"my_panel"`. - pub fn new(side: Side, id_source: impl std::hash::Hash) -> Self { + /// The id should be globally unique, e.g. `Id::new("my_panel")`. + pub fn new(side: Side, id: impl Into) -> Self { Self { side, - id: Id::new(id_source), + id: id.into(), frame: None, resizable: true, default_width: 200.0, @@ -327,6 +333,135 @@ impl SidePanel { } 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 = ctx.animate_bool(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: + 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 + } + .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 = ui + .ctx() + .animate_bool(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: + 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 + } + .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 = ctx.animate_bool(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 = ui + .ctx() + .animate_bool(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)) + } + } } // ---------------------------------------------------------------------------- @@ -390,21 +525,21 @@ pub struct TopBottomPanel { } impl TopBottomPanel { - /// `id_source`: Something unique, e.g. `"my_top_panel"`. - pub fn top(id_source: impl std::hash::Hash) -> Self { - Self::new(TopBottomSide::Top, id_source) + /// 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) } - /// `id_source`: Something unique, e.g. `"my_bottom_panel"`. - pub fn bottom(id_source: impl std::hash::Hash) -> Self { - Self::new(TopBottomSide::Bottom, id_source) + /// 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) } - /// `id_source`: Something unique, e.g. `"my_panel"`. - pub fn new(side: TopBottomSide, id_source: impl std::hash::Hash) -> Self { + /// 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::new(id_source), + id: id.into(), frame: None, resizable: false, default_height: None, @@ -632,6 +767,151 @@ impl TopBottomPanel { 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 = ctx.animate_bool(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: + 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 + } 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 = ui + .ctx() + .animate_bool(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: + 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( + ctx: &Context, + is_expanded: bool, + collapsed_panel: Self, + expanded_panel: Self, + add_contents: impl FnOnce(&mut Ui, f32) -> R, + ) -> Option> { + let how_expanded = ctx.animate_bool(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_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 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_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 + } 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 = ui + .ctx() + .animate_bool(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_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 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)) + } + } } // ---------------------------------------------------------------------------- diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 74290ad4af9..d4a52e7719c 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -77,6 +77,14 @@ impl std::fmt::Debug for Id { } } +/// Convenience +impl From<&'static str> for Id { + #[inline] + fn from(string: &'static str) -> Self { + Self::new(string) + } +} + // ---------------------------------------------------------------------------- // Idea taken from the `nohash_hasher` crate. diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 6abe45debc3..f68cdb050a9 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -200,19 +200,8 @@ impl eframe::App for WrapApp { self.state.backend_panel.update(ctx, frame); - if !is_mobile(ctx) - && (self.state.backend_panel.open || ctx.memory().everything_is_visible()) - { - egui::SidePanel::left("backend_panel") - .resizable(false) - .show(ctx, |ui| { - ui.vertical_centered(|ui| { - ui.heading("💻 Backend"); - }); - - ui.separator(); - self.backend_panel_contents(ui, frame); - }); + if !is_mobile(ctx) { + self.backend_panel(ctx, frame); } self.show_selected_app(ctx, frame); @@ -236,6 +225,23 @@ impl eframe::App for WrapApp { } impl WrapApp { + fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + // The backend-panel can be toggled on/off. + // We show a little animation when the user switches it. + let is_open = self.state.backend_panel.open || ctx.memory().everything_is_visible(); + + egui::SidePanel::left("backend_panel") + .resizable(false) + .show_animated(ctx, is_open, |ui| { + ui.vertical_centered(|ui| { + ui.heading("💻 Backend"); + }); + + ui.separator(); + self.backend_panel_contents(ui, frame); + }); + } + fn backend_panel_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { self.state.backend_panel.ui(ui, frame);