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
* ⚠️ 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)).

View file

@ -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<Self> {
pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
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<Id>) -> 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<Id>) -> 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<Id>) -> 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<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 {
/// `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<Id>) -> 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<Id>) -> 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<Id>) -> 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<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.

View file

@ -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);