From 24003b17a320d009fee0558b256042b97ac0c64b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Aug 2020 10:26:16 +0200 Subject: [PATCH] [animation] add Context::animate_bool helper function --- egui/src/animation_manager.rs | 61 ++++++++++++++++++ egui/src/containers/collapsing_header.rs | 80 ++++++++++-------------- egui/src/containers/scroll_area.rs | 33 +--------- egui/src/containers/window.rs | 5 +- egui/src/context.rs | 28 ++++++++- egui/src/input.rs | 3 +- egui/src/lib.rs | 1 + 7 files changed, 128 insertions(+), 83 deletions(-) create mode 100644 egui/src/animation_manager.rs diff --git a/egui/src/animation_manager.rs b/egui/src/animation_manager.rs new file mode 100644 index 00000000..eef722b9 --- /dev/null +++ b/egui/src/animation_manager.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; + +use crate::{math::remap_clamp, Id, InputState}; + +#[derive(Clone, Default)] +pub(crate) struct AnimationManager { + bools: HashMap, +} + +#[derive(Clone, Debug)] +struct BoolAnim { + value: bool, + /// when did `value` last toggle? + toggle_time: f64, +} + +impl AnimationManager { + /// See `Context::animate_bool` for documentation + pub fn animate_bool( + &mut self, + input: &InputState, + animation_time: f32, + id: Id, + value: bool, + ) -> f32 { + match self.bools.get_mut(&id) { + None => { + self.bools.insert( + id, + BoolAnim { + value, + toggle_time: -f64::INFINITY, // long time ago + }, + ); + if value { + 1.0 + } else { + 0.0 + } + } + Some(anim) => { + if anim.value != value { + anim.value = value; + anim.toggle_time = input.time; + } + + let time_since_toggle = (input.time - anim.toggle_time) as f32; + + // On the frame we toggle we don't want to return the old value, + // so we extrapolate forwards: + let time_since_toggle = time_since_toggle + input.predicted_dt; + + if value { + remap_clamp(time_since_toggle, 0.0..=animation_time, 0.0..=1.0) + } else { + remap_clamp(time_since_toggle, 0.0..=animation_time, 1.0..=0.0) + } + } + } + } +} diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index ef3bad4e..8899e736 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -13,10 +13,6 @@ use crate::{ pub(crate) struct State { open: bool, - // Times are relative, and we don't want to continue animations anyway, hence `serde(skip)` - #[cfg_attr(feature = "serde", serde(skip))] - toggle_time: f64, - /// Height of the region when open. Used for animations open_height: Option, } @@ -25,7 +21,6 @@ impl Default for State { fn default() -> Self { Self { open: false, - toggle_time: -f64::INFINITY, open_height: None, } } @@ -49,59 +44,22 @@ impl State { pub fn toggle(&mut self, ui: &Ui) { self.open = !self.open; - self.toggle_time = ui.input().time; ui.ctx().request_repaint(); } /// 0 for closed, 1 for open, with tweening - pub fn openness(&self, ui: &Ui) -> f32 { - let animation_time = ui.style().animation_time; - let time_since_toggle = (ui.input().time - self.toggle_time) as f32; - let time_since_toggle = time_since_toggle + ui.input().predicted_dt; // Instant feedback - if time_since_toggle <= animation_time { - ui.ctx().request_repaint(); - } - if self.open { - remap_clamp(time_since_toggle, 0.0..=animation_time, 0.0..=1.0) - } else { - remap_clamp(time_since_toggle, 0.0..=animation_time, 1.0..=0.0) - } - } - - /// Paint the arrow icon that indicated if the region is open or not - pub fn paint_icon(&self, ui: &mut Ui, response: &Response) { - let stroke_color = ui.style().interact(response).stroke_color; - let stroke_width = ui.style().interact(response).stroke_width; - - let rect = response.rect; - - let openness = self.openness(ui); - - // Draw a pointy triangle arrow: - let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75); - let mut points = [rect.left_top(), rect.right_top(), rect.center_bottom()]; - let rotation = Vec2::angled(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0)); - for p in &mut points { - let v = *p - rect.center(); - let v = rotation.rotate_other(v); - *p = rect.center() + v; - } - - ui.painter().add(PaintCmd::Path { - path: Path::from_point_loop(&points), - closed: true, - fill: None, - outline: Some(LineStyle::new(stroke_width, stroke_color)), - }); + pub fn openness(&self, ctx: &Context, id: Id) -> f32 { + ctx.animate_bool(id, self.open) } /// Show contents if we are open, with a nice animation between closed and open pub fn add_contents( &mut self, ui: &mut Ui, + id: Id, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option<(R, Rect)> { - let openness = self.openness(ui); + let openness = self.openness(ui.ctx(), id); let animate = 0.0 < openness && openness < 1.0; if animate { Some(ui.add_custom(|child_ui| { @@ -145,6 +103,31 @@ impl State { } } +/// Paint the arrow icon that indicated if the region is open or not +pub fn paint_icon(ui: &mut Ui, openness: f32, response: &Response) { + let stroke_color = ui.style().interact(response).stroke_color; + let stroke_width = ui.style().interact(response).stroke_width; + + let rect = response.rect; + + // Draw a pointy triangle arrow: + let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75); + let mut points = [rect.left_top(), rect.right_top(), rect.center_bottom()]; + let rotation = Vec2::angled(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0)); + for p in &mut points { + let v = *p - rect.center(); + let v = rotation.rotate_other(v); + *p = rect.center() + v; + } + + ui.painter().add(PaintCmd::Path { + path: Path::from_point_loop(&points), + closed: true, + fill: None, + outline: Some(LineStyle::new(stroke_width, stroke_color)), + }); +} + /// A header which can be collapsed/expanded, revealing a contained `Ui` region. pub struct CollapsingHeader { label: Label, @@ -231,7 +214,8 @@ impl CollapsingHeader { rect: icon_rect, ..response.clone() }; - state.paint_icon(ui, &icon_response); + let openness = state.openness(ui.ctx(), id); + paint_icon(ui, openness, &icon_response); } let painter = ui.painter(); @@ -257,7 +241,7 @@ impl CollapsingHeader { pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Option { let Prepared { id, mut state } = self.begin(ui); - let ret_response = state.add_contents(ui, |ui| ui.indent(id, add_contents).0); + let ret_response = state.add_contents(ui, id, |ui| ui.indent(id, add_contents).0); let ret = ret_response.map(|ri| ri.0); ui.memory().collapsing_headers.insert(id, state); ret diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index 81a03fe9..193e0841 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -8,11 +8,6 @@ pub(crate) struct State { offset: Vec2, show_scroll: bool, - - // Times are relative, and we don't want to continue animations anyway, hence `serde(skip)` - /// Used to animate the showing of the scroll bar - #[cfg_attr(feature = "serde", serde(skip))] - toggle_time: f64, } impl Default for State { @@ -20,7 +15,6 @@ impl Default for State { Self { offset: Vec2::zero(), show_scroll: false, - toggle_time: f64::NEG_INFINITY, } } } @@ -90,24 +84,7 @@ impl ScrollArea { let current_scroll_bar_width = if always_show_scroll { max_scroll_bar_width } else { - let time_since_toggle = (ui.input().time - state.toggle_time) as f32; - let animation_time = ui.style().animation_time; - if time_since_toggle <= animation_time { - ui.ctx().request_repaint(); - } - if state.show_scroll { - remap_clamp( - time_since_toggle, - 0.0..=animation_time, - 0.0..=max_scroll_bar_width, - ) - } else { - remap_clamp( - time_since_toggle, - 0.0..=animation_time, - max_scroll_bar_width..=0.0, - ) - } + max_scroll_bar_width * ui.ctx().animate_bool(id, state.show_scroll) }; let outer_size = vec2( @@ -192,12 +169,7 @@ impl Prepared { if show_scroll_this_frame && current_scroll_bar_width <= 0.0 { // Avoid frame delay; start showing scroll bar right away: - current_scroll_bar_width = remap_clamp( - ui.input().predicted_dt, - 0.0..=ui.style().animation_time, - 0.0..=max_scroll_bar_width, - ); - ui.ctx().request_repaint(); + current_scroll_bar_width = max_scroll_bar_width * ui.ctx().animate_bool(id, true); } if current_scroll_bar_width > 0.0 { @@ -290,7 +262,6 @@ impl Prepared { ui.allocate_space(size); if show_scroll_this_frame != state.show_scroll { - state.toggle_time = ui.input().time; ui.ctx().request_repaint(); } diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index ee4de8f1..a4c9bd7f 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -252,7 +252,7 @@ impl<'open> Window<'open> { resize.min_size.x = resize.min_size.x.max(title_bar.rect.width()); // Prevent making window smaller than title bar width let content_rect = collapsing - .add_contents(&mut frame.content_ui, |ui| { + .add_contents(&mut frame.content_ui, collapsing_id, |ui| { resize.show(ui, |ui| { // Add some spacing between title and content: ui.allocate_space(ui.style().item_spacing); @@ -603,7 +603,8 @@ fn show_title_bar( if collapse_button_response.clicked { collapsing.toggle(ui); } - collapsing.paint_icon(ui, &collapse_button_response); + let openness = collapsing.openness(ui.ctx(), collapsing_id); + collapsing_header::paint_icon(ui, openness, &collapse_button_response); } let title_galley = title_label.layout(ui); diff --git a/egui/src/context.rs b/egui/src/context.rs index ab520ccc..534e1dc0 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -8,7 +8,7 @@ use { parking_lot::{Mutex, MutexGuard}, }; -use crate::{paint::*, *}; +use crate::{animation_manager::AnimationManager, paint::*, *}; #[derive(Clone, Copy, Default)] struct PaintStats { @@ -18,6 +18,7 @@ struct PaintStats { num_triangles: usize, } +// TODO: too many mutexes. Maybe put it all behind one Mutex instead. /// Contains the input, style and output of all GUI commands. /// `Ui`:s keep an Arc pointer to this. /// This allows us to create several child `Ui`:s at once, @@ -31,6 +32,7 @@ pub struct Context { fonts: Option>, font_definitions: Mutex, memory: Arc>, + animation_manager: Arc>, input: InputState, @@ -54,6 +56,7 @@ impl Clone for Context { fonts: self.fonts.clone(), font_definitions: Mutex::new(self.font_definitions.lock().clone()), memory: self.memory.clone(), + animation_manager: self.animation_manager.clone(), input: self.input.clone(), graphics: Mutex::new(self.graphics.lock().clone()), output: Mutex::new(self.output.lock().clone()), @@ -466,6 +469,29 @@ impl Context { } } +/// ## Animation +impl Context { + /// Returns a value in the range [0, 1], to indicate "how on" this thing is. + /// + /// The first time called it will return `if value { 1.0 } else { 0.0 }` + /// Calling this with `value = true` will always yield a number larger than zero, quickly going towards one. + /// Calling this with `value = false` will always yield a number less than one, quickly going towards zero. + /// + /// The function will call `request_repaint()` when appropriate. + pub fn animate_bool(&self, id: Id, value: bool) -> f32 { + let animation_time = self.style().animation_time; + let animated_value = + self.animation_manager + .lock() + .animate_bool(&self.input, animation_time, id, value); + let animation_in_progress = 0.0 < animated_value && animated_value < 1.0; + if animation_in_progress { + self.request_repaint(); + } + animated_value + } +} + /// ## Painting impl Context { pub fn debug_painter(self: &Arc) -> Painter { diff --git a/egui/src/input.rs b/egui/src/input.rs index 69e321d5..b827530d 100644 --- a/egui/src/input.rs +++ b/egui/src/input.rs @@ -79,7 +79,8 @@ pub struct InputState { /// This can be very unstable in reactive mode (when we don't paint each frame). pub unstable_dt: f32, - /// Can be used to fast-forward to next frame for instance feedback. hacky. + /// Used for animations to get instant feedback (avoid frame delay). + /// Should be set to the expected time between frames when painting at vsync speeds. pub predicted_dt: f32, /// Local time. Only used for the clock in the demo app. diff --git a/egui/src/lib.rs b/egui/src/lib.rs index de19167e..364cd113 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -44,6 +44,7 @@ rust_2018_idioms, )] +mod animation_manager; pub mod app; pub mod containers; mod context;