Skip to content

Commit

Permalink
Panel collapse/expansion animation (#2190)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
emilk authored Oct 28, 2022
1 parent da96fca commit f7a15a3
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 35 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).


Expand Down
322 changes: 301 additions & 21 deletions crates/egui/src/containers/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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))
}
}
}

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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))
}
}
}

// ----------------------------------------------------------------------------
Expand Down
Loading

0 comments on commit f7a15a3

Please sign in to comment.