[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 {
|
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
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue