diff --git a/examples/animations/src/main.rs b/examples/animations/src/main.rs index b0fb8e01..ae489f4d 100644 --- a/examples/animations/src/main.rs +++ b/examples/animations/src/main.rs @@ -4,59 +4,108 @@ use floem::{ animate::{animation, EasingFn}, event::EventListener, peniko::Color, - reactive::create_signal, + reactive::{create_rw_signal, create_signal}, + style_class, view::View, - views::{empty, label, stack, text, v_stack, Decorators}, + views::{container, empty, h_stack, label, stack, static_label, text, v_stack, Decorators}, }; fn app_view() -> impl View { v_stack((progress_bar_container(), cube_container())) } +style_class!(pub Button); fn progress_bar_container() -> impl View { let width = 300.0; - let (anim_id, set_anim_id) = create_signal(None); - let (is_paused, set_is_paused) = create_signal(false); + let anim_id = create_rw_signal(None); + let is_stopped = create_rw_signal(false); + let is_paused = create_rw_signal(false); v_stack(( text("Progress bar"), - empty() - .style(|s| { - s.border(1.0) - .border_color(Color::DIM_GRAY) - .background(Color::LIME_GREEN) - .border_radius(10) - .width(0) - .height(20.) - .margin_vert(10) - .active(|s| s.color(Color::BLACK)) - }) - .animation( - //TODO: add on_update so we can track the animation state(completed/paused/running etc.) - animation() - .on_create(move |id| set_anim_id.update(|aid| *aid = Some(id))) - // Animate from 0 to 300px in 10 seconds - .width(move || width) - .easing_fn(EasingFn::Quartic) - .ease_in_out() - .duration(Duration::from_secs(10)), - ), - //TODO: add restart - label(move || if is_paused.get() { "Resume" } else { "Pause" }) - .on_click_stop(move |_| { - if let Some(anim_id) = anim_id.get() { + container( + empty() + .style(|s| { + s.border_color(Color::DIM_GRAY) + .background(Color::LIME_GREEN) + .border_radius(3) + .width(0) + .height(20.) + .active(|s| s.color(Color::BLACK)) + }) + .animation( + animation() + .on_create(move |id| anim_id.update(|aid| *aid = Some(id))) + // Animate from 0 to 300px in 10 seconds + .width(move || width) + .easing_fn(EasingFn::Quartic) + .ease_in_out() + .duration(Duration::from_secs(10)), + ), + ) + .style(move |s| { + s.width(width) + .border(1.0) + .border_radius(2) + .box_shadow_blur(3.0) + .border_color(Color::DIM_GRAY) + .background(Color::DIM_GRAY) + .margin_vert(10) + }), + h_stack(( + label(move || if is_stopped.get() { "Start" } else { "Stop" }) + .on_click_stop(move |_| { + let anim_id = anim_id.get().expect("id should be set in on_create"); + let stopped = is_stopped.get(); + if stopped { + anim_id.start() + } else { + anim_id.stop() + } + is_stopped.update(|val| *val = !stopped); + is_paused.update(|val| *val = false); + }) + .class(Button), + label(move || if is_paused.get() { "Resume" } else { "Pause" }) + .on_click_stop(move |_| { + let anim_id = anim_id.get().expect("id should be set in on_create"); let paused = is_paused.get(); if paused { anim_id.resume() } else { anim_id.pause() } - set_is_paused.update(|val| *val = !paused); - } - }) - .style(|s| s.width(70).border(1.0).padding_left(10).border_radius(5)), + is_paused.update(|val| *val = !paused); + }) + .disabled(move || is_stopped.get()) + .class(Button), + static_label("Restart") + .on_click_stop(move |_| { + let anim_id = anim_id.get().expect("id should be set in on_create"); + anim_id.stop(); + anim_id.start(); + is_stopped.update(|val| *val = false); + is_paused.update(|val| *val = false); + }) + .class(Button), + )), )) - .style(|s| s.margin_bottom(80).padding_left(5)) + .style(|s| { + s.margin_vert(20) + .margin_horiz(10) + .padding(8) + .class(Button, |s| { + s.width(70) + .border(1.0) + .padding_left(10) + .border_radius(5) + .margin_left(5.) + .disabled(|s| s.background(Color::DIM_GRAY)) + }) + .width(400) + .border(1.0) + .border_color(Color::DIM_GRAY) + }) } fn cube_container() -> impl View { diff --git a/src/animate/anim_id.rs b/src/animate/anim_id.rs index 97494070..86e4b241 100644 --- a/src/animate/anim_id.rs +++ b/src/animate/anim_id.rs @@ -21,6 +21,20 @@ impl AnimId { AnimId(id) } + pub fn start(&self) { + ANIM_UPDATE_MESSAGES.with(|msgs| { + let mut msgs = msgs.borrow_mut(); + msgs.push(AnimUpdateMsg::Start(*self)); + }); + } + + pub fn stop(&self) { + ANIM_UPDATE_MESSAGES.with(|msgs| { + let mut msgs = msgs.borrow_mut(); + msgs.push(AnimUpdateMsg::Stop(*self)); + }); + } + pub fn pause(&self) { ANIM_UPDATE_MESSAGES.with(|msgs| { let mut msgs = msgs.borrow_mut(); diff --git a/src/animate/anim_state.rs b/src/animate/anim_state.rs index abf1131c..a07c0219 100644 --- a/src/animate/anim_state.rs +++ b/src/animate/anim_state.rs @@ -3,6 +3,7 @@ use std::time::{Duration, Instant}; #[derive(Debug, Clone)] pub(crate) enum AnimState { Idle, + Stopped, Paused { elapsed: Option, }, @@ -24,10 +25,11 @@ pub(crate) enum AnimState { }, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum AnimStateKind { Idle, Paused, + Stopped, PassInProgress, PassFinished, Completed, diff --git a/src/animate/animation.rs b/src/animate/animation.rs index 59d35e68..f298cf07 100644 --- a/src/animate/animation.rs +++ b/src/animate/animation.rs @@ -66,7 +66,8 @@ pub enum AnimUpdateMsg { }, Pause(AnimId), Resume(AnimId), - //TODO: restart/stop + Start(AnimId), + Stop(AnimId), } #[derive(Clone, Debug)] @@ -92,22 +93,35 @@ impl Animation { } pub fn is_idle(&self) -> bool { - matches!(self.state_kind(), AnimStateKind::Idle) + self.state_kind() == AnimStateKind::Idle } pub fn is_in_progress(&self) -> bool { - matches!(self.state_kind(), AnimStateKind::PassInProgress) + self.state_kind() == AnimStateKind::PassInProgress } pub fn is_completed(&self) -> bool { - matches!(self.state_kind(), AnimStateKind::Completed) + self.state_kind() == AnimStateKind::Completed + } + + pub fn is_stopped(&self) -> bool { + self.state_kind() == AnimStateKind::Stopped + } + + pub fn can_advance(&self) -> bool { + match self.state_kind() { + AnimStateKind::PassFinished | AnimStateKind::PassInProgress | AnimStateKind::Idle => { + true + } + AnimStateKind::Paused | AnimStateKind::Stopped | AnimStateKind::Completed => false, + } } pub fn is_auto_reverse(&self) -> bool { self.auto_reverse } - /// Returns the ID of the animation. Use this when you want to control(pause or resume) the animation + /// Returns the ID of the animation. Use this when you want to control(stop/pause/resume) the animation pub fn on_create(mut self, on_create_fn: impl Fn(AnimId) + 'static) -> Self { self.on_create_listener = Some(Rc::new(on_create_fn)); self @@ -223,17 +237,21 @@ impl Animation { self.ease_mode(EasingMode::InOut) } - //TODO: Pausing is currently suboptimal because it will keep requesting styling even though the anim - // won't change pub fn pause(&mut self) { - // TODO: Should we warn/error if the animation is already paused or completed? + debug_assert!( + self.state_kind() != AnimStateKind::Paused, + "Tried to pause an already paused animation" + ); self.state = AnimState::Paused { elapsed: self.elapsed(), }; } pub(crate) fn resume(&mut self) { - // TODO: Should we warn/error if the user tries to resume an animation that is not paused? + debug_assert!( + self.state_kind() == AnimStateKind::Paused, + "Tried to resume an animation that is not paused" + ); if let AnimState::Paused { elapsed } = &self.state { self.state = AnimState::PassInProgress { started_on: Instant::now(), @@ -242,7 +260,7 @@ impl Animation { } } - pub fn begin(&mut self) { + pub fn start(&mut self) { self.repeat_count = 0; self.state = AnimState::PassInProgress { started_on: Instant::now(), @@ -251,27 +269,14 @@ impl Animation { } pub fn stop(&mut self) { - match &mut self.state { - AnimState::Idle - | AnimState::Completed { .. } - | AnimState::PassFinished { .. } - | AnimState::Paused { .. } => {} - AnimState::PassInProgress { - started_on, - elapsed, - } => { - let duration = Instant::now() - *started_on; - let elapsed = *elapsed + duration; - self.state = AnimState::Completed { - elapsed: Some(elapsed), - } - } - } + self.repeat_count = 0; + self.state = AnimState::Stopped; } pub fn state_kind(&self) -> AnimStateKind { match self.state { AnimState::Idle => AnimStateKind::Idle, + AnimState::Stopped => AnimStateKind::Stopped, AnimState::PassInProgress { .. } => AnimStateKind::PassInProgress, AnimState::PassFinished { .. } => AnimStateKind::PassFinished, AnimState::Completed { .. } => AnimStateKind::Completed, @@ -282,6 +287,7 @@ impl Animation { pub fn elapsed(&self) -> Option { match &self.state { AnimState::Idle => None, + AnimState::Stopped => None, AnimState::PassInProgress { started_on, elapsed, @@ -298,7 +304,7 @@ impl Animation { pub fn advance(&mut self) { match &mut self.state { AnimState::Idle => { - self.begin(); + self.start(); } AnimState::PassInProgress { started_on, @@ -333,7 +339,12 @@ impl Animation { } } }, - AnimState::Paused { .. } => {} + AnimState::Paused { .. } => { + debug_assert!(false, "Tried to advance a paused animation") + } + AnimState::Stopped => { + debug_assert!(false, "Tried to advance a stopped animation") + } AnimState::Completed { .. } => {} } } diff --git a/src/view_data.rs b/src/view_data.rs index a5f8cb2b..573ec9e5 100644 --- a/src/view_data.rs +++ b/src/view_data.rs @@ -233,6 +233,7 @@ impl ViewState { 'anim: { if let Some(animation) = self.animation.as_mut() { + // Means effectively no changes should be applied - bail out if animation.is_completed() && animation.is_auto_reverse() { break 'anim; } @@ -258,8 +259,10 @@ impl ViewState { } } - animation.advance(); - debug_assert!(!animation.is_idle()); + if animation.can_advance() { + animation.advance(); + debug_assert!(!animation.is_idle()); + } } } diff --git a/src/views/text_input.rs b/src/views/text_input.rs index 0b6ad429..7d9626cb 100644 --- a/src/views/text_input.rs +++ b/src/views/text_input.rs @@ -1058,6 +1058,7 @@ impl Widget for TextInput { let text_node = self.text_node.unwrap(); // FIXME: This layout is undefined. + #[allow(clippy::unwrap_or_default)] let layout = cx.app_state.get_layout(self.id()).unwrap_or(Layout::new()); let style = cx.app_state_mut().get_builtin_style(self.id()); let node_width = layout.size.width; diff --git a/src/window_handle.rs b/src/window_handle.rs index 7368bce2..6d971112 100644 --- a/src/window_handle.rs +++ b/src/window_handle.rs @@ -1042,18 +1042,31 @@ impl WindowHandle { let view_id = self.app_state.get_view_id_by_anim_id(anim_id); self.process_update_anim_prop(view_id, kind, val); } - AnimUpdateMsg::Pause(id) => { - let view_id = self.app_state.get_view_id_by_anim_id(id); - let view_state = self.app_state.view_state(view_id); - if let Some(anim) = view_state.animation.as_mut() { + AnimUpdateMsg::Resume(anim_id) => { + let view_id = self.app_state.get_view_id_by_anim_id(anim_id); + if let Some(anim) = self.app_state.view_state(view_id).animation.as_mut() { + anim.resume(); + self.app_state.request_style(view_id) + } + } + AnimUpdateMsg::Pause(anim_id) => { + let view_id = self.app_state.get_view_id_by_anim_id(anim_id); + if let Some(anim) = self.app_state.view_state(view_id).animation.as_mut() { anim.pause(); } } - AnimUpdateMsg::Resume(id) => { - let view_id = self.app_state.get_view_id_by_anim_id(id); - let view_state = self.app_state.view_state(view_id); - if let Some(anim) = view_state.animation.as_mut() { - anim.resume(); + AnimUpdateMsg::Start(anim_id) => { + let view_id = self.app_state.get_view_id_by_anim_id(anim_id); + if let Some(anim) = self.app_state.view_state(view_id).animation.as_mut() { + anim.start(); + self.app_state.request_style(view_id) + } + } + AnimUpdateMsg::Stop(anim_id) => { + let view_id = self.app_state.get_view_id_by_anim_id(anim_id); + if let Some(anim) = self.app_state.view_state(view_id).animation.as_mut() { + anim.stop(); + self.app_state.request_style(view_id) } } } @@ -1102,7 +1115,7 @@ impl WindowHandle { // TODO: logic based on the old val to make the animation smoother when overriding an old // animation that was in progress anim.props_mut().insert(kind, prop); - anim.begin(); + anim.start(); self.app_state.request_style(view_id); }