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:
parent
da96fcacd3
commit
f7a15a34f9
4 changed files with 332 additions and 35 deletions
|
@ -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)).
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue