diff --git a/.crate-docs.md b/.crate-docs.md index d6171ca..36caa93 100644 --- a/.crate-docs.md +++ b/.crate-docs.md @@ -15,7 +15,7 @@ Gooey uses a reactive data model. To see [an example][button-example] of how reactive data models work, consider this example that displays a button that increments its own label: -```rust +```rust,ignore // Create a dynamic usize. let count = Dynamic::new(0_usize); diff --git a/.rustme/docs.md b/.rustme/docs.md index 8e1f8aa..7859fb6 100644 --- a/.rustme/docs.md +++ b/.rustme/docs.md @@ -15,7 +15,7 @@ Gooey uses a reactive data model. To see [an example][button-example] of how reactive data models work, consider this example that displays a button that increments its own label: -```rust +```rust,ignore $../examples/button.rs:readme$ ``` diff --git a/README.md b/README.md index 538b7a7..30bebeb 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Gooey uses a reactive data model. To see [an example][button-example] of how reactive data models work, consider this example that displays a button that increments its own label: -```rust +```rust,ignore // Create a dynamic usize. let count = Dynamic::new(0_usize); diff --git a/examples/animation.rs b/examples/animation.rs index d987f50..780dc27 100644 --- a/examples/animation.rs +++ b/examples/animation.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use gooey::animation::{AnimationHandle, AnimationTarget, Spawn}; +use gooey::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn}; use gooey::value::Dynamic; use gooey::widgets::{Button, Label, Stack}; use gooey::{widgets, Run, WithClone}; @@ -9,6 +9,14 @@ fn main() -> gooey::Result { let animation = Dynamic::new(AnimationHandle::new()); let value = Dynamic::new(50); let label = value.map_each(|value| value.to_string()); + + // Gooey's animation system supports using a `Duration` as a step in + // animation to create a delay. This can also be used to call a function + // after a specified amount of time: + Duration::from_secs(1) + .on_complete(|| println!("Gooey animations are neat!")) + .launch(); + Stack::columns(widgets![ Button::new("To 0").on_click(animate_to(&animation, &value, 0)), Label::new(label), @@ -24,6 +32,12 @@ fn animate_to( ) -> impl FnMut(()) { (animation, value).with_clone(|(animation, value)| { move |_| { + // Here we use spawn to schedule the animation, which returns an + // `AnimationHandle`. When dropped, the animation associated with + // the `AnimationHandle` will be cancelled. The effect is that this + // line of code will ensure we only keep one animation running at + // all times in this example, despite how many times the buttons are + // pressed. animation.set( value .transition_to(target) diff --git a/src/animation.rs b/src/animation.rs index 702881f..5c74a21 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -1,4 +1,39 @@ //! Types for creating animations. +//! +//! Animations in Gooey are performed by transitioning a [`Dynamic`]'s contained +//! value over time. This starts with [`Dynamic::transition_to()`], which +//! returns a [`DynamicTransition`]. +//! +//! [`DynamicTransition`] implements [`AnimationTarget`], a trait that describes +//! types that can be updated using [linear interpolation](LinearInterpolate). +//! `AnimationTarget` is also implemented for tuples of `AnimationTarget` +//! implementors, allowing multiple transitions to be an `AnimationTarget`. +//! +//! Next, the [`AnimationTarget`] is turned into an animation by invoking +//! [`AnimationTarget::over()`] with the [`Duration`] the transition should +//! occur over. The animation can further be customized using +//! [`Animation::with_easing()`] to utilize any [`Easing`] implementor. +//! +//! ```rust +//! use std::time::Duration; +//! +//! use gooey::animation::{AnimationTarget, Spawn}; +//! use gooey::value::Dynamic; +//! +//! let value = Dynamic::new(0); +//! +//! value +//! .transition_to(100) +//! .over(Duration::from_millis(100)) +//! .launch(); +//! +//! let mut reader = value.into_reader(); +//! while reader.block_until_updated() { +//! println!("{}", reader.get()); +//! } +//! +//! assert_eq!(reader.get(), 100); +//! ``` use std::fmt::Debug; use std::marker::PhantomData; @@ -43,7 +78,11 @@ fn animation_thread() { let mut index = 0; while index < state.running.len() { let animation_id = *state.running.member(index).expect("index in bounds"); - if state.animations[animation_id].animate(elapsed).is_break() { + let animation_state = &mut state.animations[animation_id]; + if animation_state.animation.animate(elapsed).is_break() { + if !animation_state.handle_attached { + state.animations.remove(animation_id); + } state.running.remove_member(index); } else { index += 1; @@ -62,8 +101,13 @@ fn animation_thread() { } } +struct AnimationState { + animation: Box, + handle_attached: bool, +} + struct Animating { - animations: Lots>, + animations: Lots, running: Set, last_updated: Option, } @@ -78,7 +122,10 @@ impl Animating { } fn spawn(&mut self, animation: Box) -> AnimationHandle { - let id = self.animations.push(animation); + let id = self.animations.push(AnimationState { + animation, + handle_attached: true, + }); if self.running.is_empty() { NEW_ANIMATIONS.notify_one(); @@ -93,6 +140,14 @@ impl Animating { self.animations.remove(id); self.running.remove(&id); } + + fn run_unattached(&mut self, id: LotId) { + if self.running.contains(&id) { + self.animations[id].handle_attached = false; + } else { + self.animations.remove(id); + } + } } /// A type that can animate. @@ -256,6 +311,14 @@ pub trait IntoAnimate: Sized + Send + Sync { fn and_then(self, other: Other) -> Chain { Chain::new(self, other) } + + /// Invokes `on_complete` after this animation finishes. + fn on_complete(self, on_complete: F) -> OnCompleteAnimation + where + F: FnMut() + Send + Sync + 'static, + { + OnCompleteAnimation::new(self, on_complete) + } } macro_rules! impl_tuple_animate { @@ -300,6 +363,14 @@ pub trait Spawn { /// /// When the returned handle is dropped, the animation is stopped. fn spawn(self) -> AnimationHandle; + + /// Launches this animation, running it to completion in the background. + fn launch(self) + where + Self: Sized, + { + self.spawn().detach(); + } } impl Spawn for T @@ -372,6 +443,18 @@ impl AnimationHandle { thread_state().remove_animation(id); } } + + /// Detaches the animation from the [`AnimationHandle`], allowing the + /// animation to continue running to completion. + /// + /// Normally, dropping an [`AnimationHandle`] will cancel the underlying + /// animation. This API provides a way to continue running an animation + /// through completion without needing to hold onto the handle. + pub fn detach(mut self) { + if let Some(id) = self.0.take() { + thread_state().run_unattached(id); + } + } } impl Drop for AnimationHandle { @@ -437,6 +520,67 @@ where } } +/// An animation wrapper that invokes a callback upon the animation completing. +/// +/// This type guarantees the callback will only be invoked once per animation +/// completion. If the animation is restarted after completing, the callback +/// will be invoked again. +pub struct OnCompleteAnimation { + animation: A, + callback: Box, + completed: bool, +} + +impl OnCompleteAnimation { + /// Returns a pending animation that performs `animation` then invokes + /// `on_complete`. + pub fn new(animation: A, on_complete: F) -> Self + where + F: FnMut() + Send + Sync + 'static, + { + Self { + animation, + callback: Box::new(on_complete), + completed: false, + } + } +} + +impl IntoAnimate for OnCompleteAnimation +where + A: IntoAnimate, +{ + type Animate = OnCompleteAnimation; + + fn into_animate(self) -> Self::Animate { + OnCompleteAnimation { + animation: self.animation.into_animate(), + callback: self.callback, + completed: false, + } + } +} + +impl Animate for OnCompleteAnimation +where + A: Animate, +{ + fn animate(&mut self, elapsed: Duration) -> ControlFlow { + if self.completed { + ControlFlow::Break(elapsed) + } else { + match self.animation.animate(elapsed) { + ControlFlow::Break(remaining) => { + self.completed = true; + (self.callback)(); + ControlFlow::Break(remaining) + } + ControlFlow::Continue(()) => ControlFlow::Continue(()), + } + } + } +} + impl IntoAnimate for Duration { type Animate = Self; diff --git a/src/utils.rs b/src/utils.rs index 16d29fd..06a953a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -64,7 +64,7 @@ impl Deref for Lazy { } /// Invokes the provided macro with a pattern that can be matched using this -/// macro_rules expression: `$($type:ident $field:tt),+`, where `$type` is an +/// `macro_rules!` expression: `$($type:ident $field:tt),+`, where `$type` is an /// identifier to use for the generic parameter and `$field` is the field index /// inside of the tuple. macro_rules! impl_all_tuples { diff --git a/src/value.rs b/src/value.rs index 2dd06fc..89db5be 100644 --- a/src/value.rs +++ b/src/value.rs @@ -68,7 +68,7 @@ impl Dynamic { /// code may produce slightly more readable code. /// /// ```rust - /// let value = gooey::dynamic::Dynamic::new(1); + /// let value = gooey::value::Dynamic::new(1); /// /// // Using with_clone /// value.with_clone(|value| { @@ -136,7 +136,7 @@ impl Dynamic { /// Returns a new reference-based reader for this dynamic value. #[must_use] - pub fn create_ref_reader(&self) -> DynamicReader { + pub fn create_reader(&self) -> DynamicReader { self.state().readers += 1; DynamicReader { source: self.0.clone(), @@ -144,6 +144,12 @@ impl Dynamic { } } + /// Converts this [`Dynamic`] into a reader. + #[must_use] + pub fn into_reader(self) -> DynamicReader { + self.create_reader() + } + fn state(&self) -> MutexGuard<'_, State> { self.0.state() } @@ -193,7 +199,7 @@ impl Drop for Dynamic { impl From> for DynamicReader { fn from(value: Dynamic) -> Self { - value.create_ref_reader() + value.create_reader() } } @@ -375,7 +381,7 @@ impl DynamicReader { /// updated or there are no remaining writers for the value. /// /// Returns true if a newly updated value was discovered. - pub fn block_until_updated_async(&mut self) -> BlockUntilUpdatedFuture<'_, T> { + pub fn wait_until_updated(&mut self) -> BlockUntilUpdatedFuture<'_, T> { BlockUntilUpdatedFuture(self) } } @@ -424,7 +430,7 @@ impl<'a, T> Future for BlockUntilUpdatedFuture<'a, T> { #[test] fn disconnecting_reader_from_dynamic() { let value = Dynamic::new(1); - let mut ref_reader = value.create_ref_reader(); + let mut ref_reader = value.create_reader(); drop(value); assert!(!ref_reader.block_until_updated()); }