guido 0.12.0

A reactive Rust GUI library using wgpu for Wayland layer shell widgets
Documentation
//! How a value moves, carried by the value itself.
//!
//! Every property that survives to paint takes a signal as readily as a value.
//! This is that sentence extended to time: the *how* rides with the *what*, so
//! a timing cannot name a property it does not decorate and cannot be declared
//! for a property that was never set.
//!
//! Two verbs, on everything a property setter already accepts — a value, a
//! closure, a signal:
//!
//! ```no_run
//! # use guido::prelude::*;
//! # let surface = Color::rgb(0.1, 0.1, 0.2);
//! # let open = create_signal(false);
//! # let rejections = create_signal(0u32);
//! # let shake = || Keyframes::new(320.0).at(0.0, 0.0).at(0.5, 8.0).at(1.0, 0.0);
//! container()
//!     .background(surface.transition(200.0))
//!     .width((move || if open.get() { 520.0 } else { 120.0 }).transition(SpringConfig::SNAPPY))
//!     .rotate(0.0.timeline(shake().played_by(rejections)));
//! ```
//!
//! [`Animated`] is deliberately **not** an [`IntoSignal`]. A state layer
//! supplies values for a property somebody else declared, so it has no timing
//! to give — and because the two traits are separate, saying otherwise is a
//! compile error rather than a value quietly ignored:
//!
//! ```compile_fail,E0277
//! # use guido::prelude::*;
//! const HOT: Color = Color::rgb(0.9, 0.3, 0.2);
//! // E0277 pinned: a bare `compile_fail` passes on any error at all, including
//! // a typo in the sample, so it would go on passing after it stopped meaning
//! // anything.
//! container().when_hovered(|s| s.background(HOT.transition(900.0)));
//! ```

use crate::reactive::{IntoSignal, IntoVal, Prop};

use super::{Animatable, Keyframes, TransitionConfig};

/// A value, and how it moves when it changes.
///
/// Built by [`Animate::transition`] or [`Animate::timeline`] on anything that
/// can already be a property, and accepted wherever a `Container` setter
/// declares an animatable property.
pub struct Animated<T> {
    prop: Prop<T>,
    /// Boxed and absent by default, for the reason `AnimationState` boxes its
    /// `Timeline`: every animatable setter routes through here, and almost
    /// every call declares no motion at all. A `Motion` owns a `Keyframes` and
    /// a `TransitionConfig` with two callback slots, so inline it would make
    /// `container().background(RED)` construct, move and drop a hundred and
    /// more bytes to say "nothing".
    motion: Option<Box<Motion<T>>>,
}

impl<T> Animated<T> {
    /// Start somewhere else the one time this widget appears, and travel to
    /// the declared value with the transition already named.
    ///
    /// `transition` says how a value moves; this says where it begins. They are
    /// separate questions and compose on one property, which is the split CSS
    /// makes between `transition` and `@starting-style`.
    ///
    /// ```no_run
    /// # use guido::prelude::*;
    /// # let open = create_signal(false);
    /// # let surface = Color::rgb(0.1, 0.1, 0.2);
    /// // a menu that scales open on its first layout
    /// container().scale(
    ///     (move || if open.get() { Scale::NONE } else { Scale::uniform(0.8) })
    ///         .transition(SpringConfig::SNAPPY)
    ///         .entering_from(Scale::uniform(0.8)),
    /// );
    /// // and the same verb on anything else that animates
    /// container().background(surface.transition(200.0).entering_from(Color::TRANSPARENT));
    /// ```
    ///
    /// Played once, at the first layout, and consumed there: a relayout, a
    /// resize or a state change is not an appearance and does not replay it.
    ///
    /// Panics if a timeline was declared instead of a transition: a timeline
    /// already says where it begins, so an enter has nothing to add and would
    /// be dropped in silence. Caught the first time the widget is built.
    pub fn entering_from(mut self, from: impl IntoVal<T>) -> Self {
        match self.motion.as_deref_mut() {
            Some(Motion::Ease { enter_from, .. }) => *enter_from = Some(from.into_val()),
            Some(Motion::Play { .. }) => panic!(
                "`entering_from` needs a transition to travel with, and a timeline \
                 already says where it starts — declare the enter on a \
                 `transition(..)` value instead"
            ),
            // Loud rather than silent, for the reason `into_eased` gives: an
            // `Animated<T>` with no motion is only made inside a setter, so a
            // caller cannot spell this — and the day one can, it has to say so
            // rather than dropping the value.
            None => unreachable!(
                "an `Animated<T>` with no motion is only made by `into_animated`, \
                 inside the setter that immediately consumes it"
            ),
        }
        self
    }

    /// The value and its motion, taken apart by the setter that installs them.
    pub(crate) fn into_parts(self) -> (Prop<T>, Option<Box<Motion<T>>>) {
        (self.prop, self.motion)
    }

    /// The value and the timing it eases with, for a property whose declared
    /// type is not the type it animates: a width and a height say how the box
    /// is sized with a `Length` and move an `f32`.
    ///
    /// A timeline cannot cross that gap, and cannot be written across it
    /// either — [`Animate::timeline`] requires `T: Animatable` and
    /// [`Keyframes`] has no other constructor, so a `Keyframes<Length>` does
    /// not exist. That bound is what makes this a narrowing rather than a
    /// value quietly dropped, and relaxing it would have to bring a spelling
    /// for a timeline on a size with it.
    pub(crate) fn into_eased(self) -> (Prop<T>, Option<Eased<T>>) {
        let ease = match self.motion.map(|motion| *motion) {
            Some(Motion::Ease {
                config,
                enter_from,
                exit_to,
            }) => Some((config, enter_from, exit_to)),
            None => None,
            // Loud rather than `None`, because the thing that makes this
            // unreachable is a bound three types away: silently dropping a
            // timeline here is the defect this whole shape exists to remove,
            // and the day somebody relaxes that bound is the day this has to
            // say so.
            Some(Motion::Play { .. }) => {
                unreachable!("a timeline needs T: Animatable, which this T is not")
            }
        };
        (self.prop, ease)
    }
}

impl<T: Clone + 'static> Animated<T> {
    /// Leave somewhere else when the widget is removed, travelling there with
    /// the transition already named, and be disposed when it arrives.
    ///
    /// The other half of [`entering_from`](Self::entering_from). Declared on
    /// the removed widget's own properties — a child of `.child(move || ..)`,
    /// `.children(move || ..)` or `keyed(..)` — it keeps that widget in its
    /// container, where it stood, until every exit it declared has settled.
    /// While it leaves it is inert: it takes no pointer input and no focus,
    /// and nothing it reads relays out, repaints or reconciles it. Effects
    /// created while building it are not widgets, and live until it is
    /// disposed.
    ///
    /// ```no_run
    /// # use guido::prelude::*;
    /// # let step = create_signal(1.0f32);
    /// # let name = create_signal(String::new());
    /// # const WIDTH: f32 = 120.0;
    /// # let slide = Transition::new(200.0, TimingFunction::EaseOut);
    /// container().child(move || {
    ///     container().child(text(name.get())).translate(
    ///         Translate::NONE
    ///             .transition(slide.clone())
    ///             .entering_from(Translate::new(step.get_untracked() * WIDTH, 0.0))
    ///             .exiting_to(move || Translate::new(-step.get() * WIDTH, 0.0)),
    ///     )
    /// });
    /// ```
    ///
    /// **Asked at removal, not at build.** An enter is resolved when the
    /// widget is built; an exit is not, because which way a child leaves is
    /// known only when the change that removes it happens — with two items,
    /// next and previous land on the same one. So a closure passed here runs
    /// when the widget is removed, and reads the direction as it is then.
    ///
    /// A `keyed(..)` row whose key comes back while it is leaving is not
    /// rebuilt: its exit is cancelled and it travels back from where it is.
    /// The dynamic children inside it re-run, since what they read while it
    /// was away was not watched.
    ///
    /// Only a container's own properties leave, and only the removed widget's:
    /// an exit declared on something inside it plays when that is the widget
    /// removed.
    ///
    /// Panics if a timeline was declared instead of a transition, for the
    /// reason `entering_from` does.
    pub fn exiting_to<M>(mut self, to: impl IntoSignal<T, M>) -> Self {
        let to = to.into_prop();
        match self.motion.as_deref_mut() {
            Some(Motion::Ease { exit_to, .. }) => {
                *exit_to = Some(Box::new(move || {
                    to.get_untracked()
                        .expect("a declared exit always carries a value")
                }))
            }
            Some(Motion::Play { .. }) => panic!(
                "`exiting_to` needs a transition to travel with, and a timeline \
                 plays a sequence of its own — declare the exit on a \
                 `transition(..)` value instead"
            ),
            None => unreachable!(
                "an `Animated<T>` with no motion is only made by `into_animated`, \
                 inside the setter that immediately consumes it"
            ),
        }
        self
    }
}

/// An eased motion taken apart: the transition, where it enters from and
/// where it exits to.
pub(crate) type Eased<T> = (TransitionConfig, Option<T>, Option<ExitTo<T>>);

/// Where a property goes when its widget is removed — a closure, because it is
/// asked at removal rather than at build.
pub(crate) type ExitTo<T> = Box<dyn Fn() -> T>;

/// The two ways a value can move. An implementation detail: `motion` already
/// means a pointer event here and scrolling calls its own `momentum`, so a
/// public name about easing would sit between two unrelated meanings.
pub(crate) enum Motion<T> {
    /// Ease to each new value instead of jumping to it, and — if an enter was
    /// declared — begin from somewhere else on the first layout.
    Ease {
        config: TransitionConfig,
        /// Where the property starts the one time the widget appears. `None`
        /// for almost every declaration; see [`Animated::entering_from`].
        enter_from: Option<T>,
        /// Where it goes when the widget is removed, asked then. `None` for
        /// almost every declaration; see [`Animated::exiting_to`].
        exit_to: Option<ExitTo<T>>,
    },
    /// Play a sequence when the widget appears, or on every change of its
    /// trigger if it has one, and rest on the declared value in between.
    Play { keyframes: Keyframes<T> },
}

/// The two verbs, on everything [`IntoSignal`] accepts.
///
/// There is no separate spelling for closures: the parentheses in
/// `(move || …).transition(..)` are Rust's rule for calling a method on a
/// closure literal, not a second API. The marker generic `M` is the same one
/// `IntoSignal` carries, and for the same reason — a blanket impl over `T` and
/// one over `FnMut() -> T` would overlap without it.
pub trait Animate<T: Clone + 'static, M>: IntoSignal<T, M> + Sized {
    /// Ease to each new value this holds, instead of jumping to it.
    ///
    /// A bare number is milliseconds; a [`Transition`](super::Transition) or a
    /// [`SpringConfig`](super::SpringConfig) says which curve.
    ///
    /// ```no_run
    /// # use guido::prelude::*;
    /// # let surface = Color::rgb(0.1, 0.1, 0.2);
    /// # let w = create_signal(120.0f32);
    /// container().background(surface.transition(200.0));
    /// container().width(w.transition(Transition::spring(SpringConfig::SNAPPY)));
    /// ```
    fn transition(self, transition: impl Into<TransitionConfig>) -> Animated<T> {
        Animated {
            prop: self.into_prop(),
            motion: Some(Box::new(Motion::Ease {
                config: transition.into(),
                enter_from: None,
                exit_to: None,
            })),
        }
    }

    /// Play `keyframes` when the widget appears — or, if they are
    /// [`played_by`](super::Keyframes::played_by) a trigger, on every change
    /// of it — resting on this value in between.
    ///
    /// A trigger is a count and not a flag on purpose: a second refusal has
    /// to shake as loudly as the first, and a signal that stays equal notifies
    /// nobody. The count it starts at is whatever it holds when the widget is
    /// built, so a triggered sequence plays nothing on the first frame.
    ///
    /// The resting value is required because it is what the property *is*
    /// whenever nothing is playing — a shake returns to where it began and
    /// makes it look redundant, but a sequence that ends somewhere else snaps
    /// back to it, and a sequence resting on a live signal has nowhere else to
    /// put its expression:
    ///
    /// ```no_run
    /// # use guido::prelude::*;
    /// # let rejections = create_signal(0u32);
    /// # let spin = create_signal(0.0f32);
    /// # const STEP: f32 = 90.0;
    /// # let shake = || Keyframes::new(320.0).at(0.0, 0.0).at(0.5, 8.0).at(1.0, 0.0);
    /// container().rotate(0.0.timeline(shake().played_by(rejections)));
    /// container().rotate((move || spin.get() * STEP).timeline(shake().played_by(rejections)));
    /// ```
    ///
    /// A timeline is for something that happens and is over. A change that
    /// should persist is a [`transition`](Self::transition).
    ///
    /// The `T: Animatable` bound is load-bearing beyond the `Keyframes` it
    /// names: it is what keeps a timeline off a property whose declared type
    /// is not the type it animates. A width and a height say how the box is
    /// sized with a `Length` and move an `f32`, and a `Length` is not
    /// animatable — so `width(w.timeline(..))` does not compile, rather than
    /// compiling and playing nothing.
    fn timeline(self, keyframes: Keyframes<T>) -> Animated<T>
    where
        T: Animatable,
    {
        Animated {
            prop: self.into_prop(),
            motion: Some(Box::new(Motion::Play { keyframes })),
        }
    }
}

impl<T: Clone + 'static, M, S: IntoSignal<T, M>> Animate<T, M> for S {}

#[doc(hidden)]
pub struct AnimatedMarker;

/// The marker for a value arriving with no motion, wrapping the one
/// [`IntoSignal`] picked for it.
///
/// A wrapper rather than `M` passed straight through, because coherence
/// reasons about what *could* be implemented, not only what is: a downstream
/// crate is allowed to write `IntoSignal<Their, AnimatedMarker> for
/// Animated<Their>`, which would make a blanket `IntoAnimated<T, M>` overlap
/// the impl below. Two marker types that cannot unify close that off by
/// construction.
#[doc(hidden)]
pub struct Plain<M>(std::marker::PhantomData<M>);

/// What an animatable property setter takes: everything [`IntoSignal`]
/// accepts, plus a value that carries its own motion.
///
/// The wider bound is what keeps the rule at compile time. `StateStyle`'s
/// setters keep plain `IntoSignal`, so a timing declared on an override — a
/// value for a property somebody else owns — does not compile.
pub trait IntoAnimated<T: Clone + 'static, M> {
    fn into_animated(self) -> Animated<T>;
}

impl<T: Clone + 'static, M, S: IntoSignal<T, M>> IntoAnimated<T, Plain<M>> for S {
    fn into_animated(self) -> Animated<T> {
        Animated {
            prop: self.into_prop(),
            motion: None,
        }
    }
}

impl<T: Clone + 'static> IntoAnimated<T, AnimatedMarker> for Animated<T> {
    fn into_animated(self) -> Animated<T> {
        self
    }
}