[animation] add Context::animate_bool helper function

This commit is contained in:
Emil Ernerfeldt 2020-08-30 10:26:16 +02:00
parent c23dfd155c
commit 24003b17a3
7 changed files with 128 additions and 83 deletions

View file

@ -0,0 +1,61 @@
use std::collections::HashMap;
use crate::{math::remap_clamp, Id, InputState};
#[derive(Clone, Default)]
pub(crate) struct AnimationManager {
bools: HashMap<Id, BoolAnim>,
}
#[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)
}
}
}
}
}

View file

@ -13,10 +13,6 @@ use crate::{
pub(crate) struct State { pub(crate) struct State {
open: bool, 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 /// Height of the region when open. Used for animations
open_height: Option<f32>, open_height: Option<f32>,
} }
@ -25,7 +21,6 @@ impl Default for State {
fn default() -> Self { fn default() -> Self {
Self { Self {
open: false, open: false,
toggle_time: -f64::INFINITY,
open_height: None, open_height: None,
} }
} }
@ -49,59 +44,22 @@ impl State {
pub fn toggle(&mut self, ui: &Ui) { pub fn toggle(&mut self, ui: &Ui) {
self.open = !self.open; self.open = !self.open;
self.toggle_time = ui.input().time;
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }
/// 0 for closed, 1 for open, with tweening /// 0 for closed, 1 for open, with tweening
pub fn openness(&self, ui: &Ui) -> f32 { pub fn openness(&self, ctx: &Context, id: Id) -> f32 {
let animation_time = ui.style().animation_time; ctx.animate_bool(id, self.open)
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)),
});
} }
/// Show contents if we are open, with a nice animation between closed and open /// Show contents if we are open, with a nice animation between closed and open
pub fn add_contents<R>( pub fn add_contents<R>(
&mut self, &mut self,
ui: &mut Ui, ui: &mut Ui,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<(R, Rect)> { ) -> Option<(R, Rect)> {
let openness = self.openness(ui); let openness = self.openness(ui.ctx(), id);
let animate = 0.0 < openness && openness < 1.0; let animate = 0.0 < openness && openness < 1.0;
if animate { if animate {
Some(ui.add_custom(|child_ui| { 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. /// A header which can be collapsed/expanded, revealing a contained `Ui` region.
pub struct CollapsingHeader { pub struct CollapsingHeader {
label: Label, label: Label,
@ -231,7 +214,8 @@ impl CollapsingHeader {
rect: icon_rect, rect: icon_rect,
..response.clone() ..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(); let painter = ui.painter();
@ -257,7 +241,7 @@ impl CollapsingHeader {
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Option<R> { pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Option<R> {
let Prepared { id, mut state } = self.begin(ui); 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); let ret = ret_response.map(|ri| ri.0);
ui.memory().collapsing_headers.insert(id, state); ui.memory().collapsing_headers.insert(id, state);
ret ret

View file

@ -8,11 +8,6 @@ pub(crate) struct State {
offset: Vec2, offset: Vec2,
show_scroll: bool, 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 { impl Default for State {
@ -20,7 +15,6 @@ impl Default for State {
Self { Self {
offset: Vec2::zero(), offset: Vec2::zero(),
show_scroll: false, show_scroll: false,
toggle_time: f64::NEG_INFINITY,
} }
} }
} }
@ -90,24 +84,7 @@ impl ScrollArea {
let current_scroll_bar_width = if always_show_scroll { let current_scroll_bar_width = if always_show_scroll {
max_scroll_bar_width max_scroll_bar_width
} else { } else {
let time_since_toggle = (ui.input().time - state.toggle_time) as f32; max_scroll_bar_width * ui.ctx().animate_bool(id, state.show_scroll)
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,
)
}
}; };
let outer_size = vec2( let outer_size = vec2(
@ -192,12 +169,7 @@ impl Prepared {
if show_scroll_this_frame && current_scroll_bar_width <= 0.0 { if show_scroll_this_frame && current_scroll_bar_width <= 0.0 {
// Avoid frame delay; start showing scroll bar right away: // Avoid frame delay; start showing scroll bar right away:
current_scroll_bar_width = remap_clamp( current_scroll_bar_width = max_scroll_bar_width * ui.ctx().animate_bool(id, true);
ui.input().predicted_dt,
0.0..=ui.style().animation_time,
0.0..=max_scroll_bar_width,
);
ui.ctx().request_repaint();
} }
if current_scroll_bar_width > 0.0 { if current_scroll_bar_width > 0.0 {
@ -290,7 +262,6 @@ impl Prepared {
ui.allocate_space(size); ui.allocate_space(size);
if show_scroll_this_frame != state.show_scroll { if show_scroll_this_frame != state.show_scroll {
state.toggle_time = ui.input().time;
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }

View file

@ -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 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 let content_rect = collapsing
.add_contents(&mut frame.content_ui, |ui| { .add_contents(&mut frame.content_ui, collapsing_id, |ui| {
resize.show(ui, |ui| { resize.show(ui, |ui| {
// Add some spacing between title and content: // Add some spacing between title and content:
ui.allocate_space(ui.style().item_spacing); ui.allocate_space(ui.style().item_spacing);
@ -603,7 +603,8 @@ fn show_title_bar(
if collapse_button_response.clicked { if collapse_button_response.clicked {
collapsing.toggle(ui); 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); let title_galley = title_label.layout(ui);

View file

@ -8,7 +8,7 @@ use {
parking_lot::{Mutex, MutexGuard}, parking_lot::{Mutex, MutexGuard},
}; };
use crate::{paint::*, *}; use crate::{animation_manager::AnimationManager, paint::*, *};
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default)]
struct PaintStats { struct PaintStats {
@ -18,6 +18,7 @@ struct PaintStats {
num_triangles: usize, 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. /// Contains the input, style and output of all GUI commands.
/// `Ui`:s keep an Arc pointer to this. /// `Ui`:s keep an Arc pointer to this.
/// This allows us to create several child `Ui`:s at once, /// This allows us to create several child `Ui`:s at once,
@ -31,6 +32,7 @@ pub struct Context {
fonts: Option<Arc<Fonts>>, fonts: Option<Arc<Fonts>>,
font_definitions: Mutex<FontDefinitions>, font_definitions: Mutex<FontDefinitions>,
memory: Arc<Mutex<Memory>>, memory: Arc<Mutex<Memory>>,
animation_manager: Arc<Mutex<AnimationManager>>,
input: InputState, input: InputState,
@ -54,6 +56,7 @@ impl Clone for Context {
fonts: self.fonts.clone(), fonts: self.fonts.clone(),
font_definitions: Mutex::new(self.font_definitions.lock().clone()), font_definitions: Mutex::new(self.font_definitions.lock().clone()),
memory: self.memory.clone(), memory: self.memory.clone(),
animation_manager: self.animation_manager.clone(),
input: self.input.clone(), input: self.input.clone(),
graphics: Mutex::new(self.graphics.lock().clone()), graphics: Mutex::new(self.graphics.lock().clone()),
output: Mutex::new(self.output.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 /// ## Painting
impl Context { impl Context {
pub fn debug_painter(self: &Arc<Self>) -> Painter { pub fn debug_painter(self: &Arc<Self>) -> Painter {

View file

@ -79,7 +79,8 @@ pub struct InputState {
/// This can be very unstable in reactive mode (when we don't paint each frame). /// This can be very unstable in reactive mode (when we don't paint each frame).
pub unstable_dt: f32, 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, pub predicted_dt: f32,
/// Local time. Only used for the clock in the demo app. /// Local time. Only used for the clock in the demo app.

View file

@ -44,6 +44,7 @@
rust_2018_idioms, rust_2018_idioms,
)] )]
mod animation_manager;
pub mod app; pub mod app;
pub mod containers; pub mod containers;
mod context; mod context;