[animation] add Context::animate_bool helper function
This commit is contained in:
parent
c23dfd155c
commit
24003b17a3
7 changed files with 128 additions and 83 deletions
61
egui/src/animation_manager.rs
Normal file
61
egui/src/animation_manager.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<f32>,
|
||||
}
|
||||
|
@ -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<R>(
|
||||
&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<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Option<R> {
|
||||
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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<Arc<Fonts>>,
|
||||
font_definitions: Mutex<FontDefinitions>,
|
||||
memory: Arc<Mutex<Memory>>,
|
||||
animation_manager: Arc<Mutex<AnimationManager>>,
|
||||
|
||||
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<Self>) -> Painter {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
rust_2018_idioms,
|
||||
)]
|
||||
|
||||
mod animation_manager;
|
||||
pub mod app;
|
||||
pub mod containers;
|
||||
mod context;
|
||||
|
|
Loading…
Reference in a new issue