Skip to content

Commit

Permalink
Animation docs, on_complete
Browse files Browse the repository at this point in the history
  • Loading branch information
ecton committed Nov 3, 2023
1 parent 32b5e16 commit 126b324
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .crate-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion .rustme/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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$
```

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 15 additions & 1 deletion examples/animation.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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),
Expand All @@ -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)
Expand Down
150 changes: 147 additions & 3 deletions src/animation.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -62,8 +101,13 @@ fn animation_thread() {
}
}

struct AnimationState {
animation: Box<dyn Animate>,
handle_attached: bool,
}

struct Animating {
animations: Lots<Box<dyn Animate>>,
animations: Lots<AnimationState>,
running: Set<LotId>,
last_updated: Option<Instant>,
}
Expand All @@ -78,7 +122,10 @@ impl Animating {
}

fn spawn(&mut self, animation: Box<dyn Animate>) -> 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();
Expand All @@ -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.
Expand Down Expand Up @@ -256,6 +311,14 @@ pub trait IntoAnimate: Sized + Send + Sync {
fn and_then<Other: IntoAnimate>(self, other: Other) -> Chain<Self, Other> {
Chain::new(self, other)
}

/// Invokes `on_complete` after this animation finishes.
fn on_complete<F>(self, on_complete: F) -> OnCompleteAnimation<Self>
where
F: FnMut() + Send + Sync + 'static,
{
OnCompleteAnimation::new(self, on_complete)
}
}

macro_rules! impl_tuple_animate {
Expand Down Expand Up @@ -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<T> Spawn for T
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<A> {
animation: A,
callback: Box<dyn FnMut() + Send + Sync + 'static>,
completed: bool,
}

impl<A> OnCompleteAnimation<A> {
/// Returns a pending animation that performs `animation` then invokes
/// `on_complete`.
pub fn new<F>(animation: A, on_complete: F) -> Self
where
F: FnMut() + Send + Sync + 'static,
{
Self {
animation,
callback: Box::new(on_complete),
completed: false,
}
}
}

impl<A> IntoAnimate for OnCompleteAnimation<A>
where
A: IntoAnimate,
{
type Animate = OnCompleteAnimation<A::Animate>;

fn into_animate(self) -> Self::Animate {
OnCompleteAnimation {
animation: self.animation.into_animate(),
callback: self.callback,
completed: false,
}
}
}

impl<A> Animate for OnCompleteAnimation<A>
where
A: Animate,
{
fn animate(&mut self, elapsed: Duration) -> ControlFlow<Duration> {
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;

Expand Down
2 changes: 1 addition & 1 deletion src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ impl<T> Deref for Lazy<T> {
}

/// 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 {
Expand Down
16 changes: 11 additions & 5 deletions src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ impl<T> Dynamic<T> {
/// 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| {
Expand Down Expand Up @@ -136,14 +136,20 @@ impl<T> Dynamic<T> {

/// Returns a new reference-based reader for this dynamic value.
#[must_use]
pub fn create_ref_reader(&self) -> DynamicReader<T> {
pub fn create_reader(&self) -> DynamicReader<T> {
self.state().readers += 1;
DynamicReader {
source: self.0.clone(),
read_generation: self.0.state().wrapped.generation,
}
}

/// Converts this [`Dynamic`] into a reader.
#[must_use]
pub fn into_reader(self) -> DynamicReader<T> {
self.create_reader()
}

fn state(&self) -> MutexGuard<'_, State<T>> {
self.0.state()
}
Expand Down Expand Up @@ -193,7 +199,7 @@ impl<T> Drop for Dynamic<T> {

impl<T> From<Dynamic<T>> for DynamicReader<T> {
fn from(value: Dynamic<T>) -> Self {
value.create_ref_reader()
value.create_reader()
}
}

Expand Down Expand Up @@ -375,7 +381,7 @@ impl<T> DynamicReader<T> {
/// 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)
}
}
Expand Down Expand Up @@ -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());
}
Expand Down

0 comments on commit 126b324

Please sign in to comment.