Panel collapse/expansion animation (#2190)

* Add API for querying the size of a panel

* demo app: animate backend panel collapse

* Add helper function for animating panels

* More animation functions

* Add line to changelog
This commit is contained in:
Emil Ernerfeldt 2022-10-28 11:51:56 +02:00 committed by GitHub
parent da96fcacd3
commit f7a15a34f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 332 additions and 35 deletions

View file

@ -5,10 +5,13 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
## Unreleased ## 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)). * ⚠️ 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 🐛 ### 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)). * Improved text rendering ([#2071](https://github.com/emilk/egui/pull/2071)).

View file

@ -19,17 +19,23 @@ use std::ops::RangeInclusive;
use crate::*; use crate::*;
/// State regarding panels.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct PanelState { pub struct PanelState {
rect: Rect, pub rect: Rect,
} }
impl PanelState { impl PanelState {
fn load(ctx: &Context, bar_id: Id) -> Option<Self> { pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
ctx.data().get_persisted(bar_id) 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) { fn store(self, ctx: &Context, bar_id: Id) {
ctx.data().insert_persisted(bar_id, self); ctx.data().insert_persisted(bar_id, self);
} }
@ -96,21 +102,21 @@ pub struct SidePanel {
} }
impl SidePanel { impl SidePanel {
/// `id_source`: Something unique, e.g. `"my_left_panel"`. /// The id should be globally unique, e.g. `Id::new("my_left_panel")`.
pub fn left(id_source: impl std::hash::Hash) -> Self { pub fn left(id: impl Into<Id>) -> Self {
Self::new(Side::Left, id_source) Self::new(Side::Left, id)
} }
/// `id_source`: Something unique, e.g. `"my_right_panel"`. /// The id should be globally unique, e.g. `Id::new("my_right_panel")`.
pub fn right(id_source: impl std::hash::Hash) -> Self { pub fn right(id: impl Into<Id>) -> Self {
Self::new(Side::Right, id_source) Self::new(Side::Right, id)
} }
/// `id_source`: Something unique, e.g. `"my_panel"`. /// The id should be globally unique, e.g. `Id::new("my_panel")`.
pub fn new(side: Side, id_source: impl std::hash::Hash) -> Self { pub fn new(side: Side, id: impl Into<Id>) -> Self {
Self { Self {
side, side,
id: Id::new(id_source), id: id.into(),
frame: None, frame: None,
resizable: true, resizable: true,
default_width: 200.0, default_width: 200.0,
@ -327,6 +333,135 @@ impl SidePanel {
} }
inner_response 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<R>(
self,
ctx: &Context,
is_expanded: bool,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
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<R>(
self,
ui: &mut Ui,
is_expanded: bool,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
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<R>(
ctx: &Context,
is_expanded: bool,
collapsed_panel: Self,
expanded_panel: Self,
add_contents: impl FnOnce(&mut Ui, f32) -> R,
) -> Option<InnerResponse<R>> {
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<R>(
ui: &mut Ui,
is_expanded: bool,
collapsed_panel: Self,
expanded_panel: Self,
add_contents: impl FnOnce(&mut Ui, f32) -> R,
) -> InnerResponse<R> {
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 { impl TopBottomPanel {
/// `id_source`: Something unique, e.g. `"my_top_panel"`. /// The id should be globally unique, e.g. `Id::new("my_top_panel")`.
pub fn top(id_source: impl std::hash::Hash) -> Self { pub fn top(id: impl Into<Id>) -> Self {
Self::new(TopBottomSide::Top, id_source) Self::new(TopBottomSide::Top, id)
} }
/// `id_source`: Something unique, e.g. `"my_bottom_panel"`. /// The id should be globally unique, e.g. `Id::new("my_bottom_panel")`.
pub fn bottom(id_source: impl std::hash::Hash) -> Self { pub fn bottom(id: impl Into<Id>) -> Self {
Self::new(TopBottomSide::Bottom, id_source) Self::new(TopBottomSide::Bottom, id)
} }
/// `id_source`: Something unique, e.g. `"my_panel"`. /// The id should be globally unique, e.g. `Id::new("my_panel")`.
pub fn new(side: TopBottomSide, id_source: impl std::hash::Hash) -> Self { pub fn new(side: TopBottomSide, id: impl Into<Id>) -> Self {
Self { Self {
side, side,
id: Id::new(id_source), id: id.into(),
frame: None, frame: None,
resizable: false, resizable: false,
default_height: None, default_height: None,
@ -632,6 +767,151 @@ impl TopBottomPanel {
inner_response 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<R>(
self,
ctx: &Context,
is_expanded: bool,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
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<R>(
self,
ui: &mut Ui,
is_expanded: bool,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
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<R>(
ctx: &Context,
is_expanded: bool,
collapsed_panel: Self,
expanded_panel: Self,
add_contents: impl FnOnce(&mut Ui, f32) -> R,
) -> Option<InnerResponse<R>> {
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<R>(
ui: &mut Ui,
is_expanded: bool,
collapsed_panel: Self,
expanded_panel: Self,
add_contents: impl FnOnce(&mut Ui, f32) -> R,
) -> InnerResponse<R> {
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))
}
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View file

@ -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. // Idea taken from the `nohash_hasher` crate.

View file

@ -200,19 +200,8 @@ impl eframe::App for WrapApp {
self.state.backend_panel.update(ctx, frame); self.state.backend_panel.update(ctx, frame);
if !is_mobile(ctx) if !is_mobile(ctx) {
&& (self.state.backend_panel.open || ctx.memory().everything_is_visible()) self.backend_panel(ctx, frame);
{
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);
});
} }
self.show_selected_app(ctx, frame); self.show_selected_app(ctx, frame);
@ -236,6 +225,23 @@ impl eframe::App for WrapApp {
} }
impl 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) { fn backend_panel_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
self.state.backend_panel.ui(ui, frame); self.state.backend_panel.ui(ui, frame);