euv-ui 0.18.7

Reusable UI component library for the euv framework, providing buttons, cards, modals, inputs, and more.
Documentation
use super::*;

/// Implements [`HookContextTransitionExt`] for [`HookContext`].
impl HookContextTransitionExt for HookContext {
    /// Returns a fresh [`TransitionState`] driven by `config`.
    ///
    /// # Arguments
    ///
    /// - `TransitionConfig` - A `TransitionConfig` parameter.
    ///
    /// # Returns
    ///
    /// - `TransitionState` - A `TransitionState` value.
    fn transition(config: TransitionConfig) -> TransitionState {
        let hook_context: HookContext = Self::current();
        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
            return TransitionState::new(
                Signal::create(TransitionPhase::Exited),
                Signal::create(0.0_f64),
                Signal::create(config),
            );
        };
        let index: usize = inner.get_hook_index();
        inner.set_hook_index(index + 1);
        if index < inner.get_hooks().len()
            && let Some(existing) = inner.get_hooks()[index].downcast_ref::<TransitionState>()
        {
            existing.change_config(config);
            return existing.clone();
        }
        drop(inner);
        HookContext::use_hook(|| {
            TransitionState::new(
                Signal::create(TransitionPhase::Exited),
                Signal::create(0.0_f64),
                Signal::create(config),
            )
        })
    }
}

/// Inherent implementation of [`TransitionPhase`].
impl TransitionPhase {
    /// Returns a fresh `TransitionPhase` value of
    /// `Exited`. Convenience for `Signal::create` call
    /// sites.
    pub const fn exited() -> Self {
        TransitionPhase::Exited
    }
}

/// Inherent implementation of [`TransitionConfig`].
impl TransitionConfig {
    /// Returns a config with both enter and exit durations
    /// set to `ms`.
    ///
    /// Named `with_ms` (not `new`) to avoid colliding
    /// with the `new` constructor generated by
    /// `#[derive(New)]`. (We don't derive `New` here
    /// because the field name `enter_ms` would generate
    /// a `set_enter_ms` setter that nobody asked for —
    /// keeping the struct `Copy` and writing it via
    /// `TransitionConfig { enter_ms, exit_ms }` is
    /// simpler.)
    ///
    /// # Arguments
    ///
    /// - `u32` - A 32-bit unsigned integer (`u32`).
    pub const fn with_ms(ms: u32) -> Self {
        Self {
            enter_ms: ms,
            exit_ms: ms,
        }
    }

    /// Returns a config with separate enter and exit
    /// durations. See `with_ms` for the naming note.
    ///
    /// # Arguments
    ///
    /// - `u32` - A 32-bit unsigned integer (`u32`).
    /// - `u32` - A 32-bit unsigned integer (`u32`).
    pub const fn with_durations(enter_ms: u32, exit_ms: u32) -> Self {
        Self { enter_ms, exit_ms }
    }

    /// Returns the duration (in ms) corresponding to the
    /// given phase. Returns `0` for terminal phases
    /// (`Entered`, `Exited`) — these don't tick.
    ///
    /// # Arguments
    ///
    /// - `TransitionPhase` - A `TransitionPhase` parameter.
    ///
    /// # Returns
    ///
    /// - `u32` - A 32-bit unsigned integer.
    pub fn duration_for(&self, phase: TransitionPhase) -> u32 {
        match phase {
            TransitionPhase::Entering => self.get_enter_ms(),
            TransitionPhase::Exiting => self.get_exit_ms(),
            TransitionPhase::Entered | TransitionPhase::Exited => 0,
        }
    }
}

/// Default-construction for [`TransitionConfig`].
impl Default for TransitionConfig {
    /// Constructs a default [`TransitionConfig`] value.
    fn default() -> Self {
        // Matches the CSS defaults used elsewhere in
        // euv-ui (var!(duration-normal) ≈ 200ms).
        Self::with_ms(200)
    }
}

/// Inherent implementation of [`TransitionState`].
impl TransitionState {
    /// Returns `true` if the element is currently
    /// animating (i.e. `Entering` or `Exiting`).
    ///
    /// # Returns
    ///
    /// - `bool` - `true` when an animation is currently running.
    pub fn is_animating(&self) -> bool {
        matches!(
            self.get_phase().get(),
            TransitionPhase::Entering | TransitionPhase::Exiting
        )
    }

    /// Replaces the duration config.
    ///
    /// Named `change_config` (not `set_config`) to avoid
    /// colliding with the `set_config` setter generated
    /// by `#[derive(Data)]` on the struct field.
    ///
    /// # Arguments
    ///
    /// - `TransitionConfig` - A `TransitionConfig` parameter.
    pub fn change_config(&self, config: TransitionConfig) {
        self.get_config().set(config);
    }

    /// Starts the enter animation. Sets the phase to
    /// `Entering` and resets progress to `0.0`. No-op if
    /// the transition is already in `Entering` / `Entered`.
    pub fn enter(&self) {
        let current: TransitionPhase = self.get_phase().get();
        if matches!(
            current,
            TransitionPhase::Entering | TransitionPhase::Entered
        ) {
            return;
        }
        self.get_phase().set(TransitionPhase::Entering);
        self.get_progress().set(0.0);
    }

    /// Starts the exit animation. Sets the phase to
    /// `Exiting` and starts progress from `1.0`. No-op if
    /// the transition is already in `Exiting` / `Exited`.
    pub fn exit(&self) {
        let current: TransitionPhase = self.get_phase().get();
        if matches!(current, TransitionPhase::Exiting | TransitionPhase::Exited) {
            return;
        }
        self.get_phase().set(TransitionPhase::Exiting);
        self.get_progress().set(1.0);
    }

    /// Toggles between `Entered` and `Exited`. Equivalent
    /// to `enter()` if currently `Exiting` / `Exited`,
    /// and `exit()` if currently `Entering` / `Entered`.
    pub fn toggle(&self) {
        match self.get_phase().get() {
            TransitionPhase::Entered | TransitionPhase::Entering => {
                self.exit();
            }
            TransitionPhase::Exiting | TransitionPhase::Exited => {
                self.enter();
            }
        }
    }

    /// Advances the transition by `elapsed_ms`
    /// milliseconds. Updates `progress` and, if the
    /// transition has reached its end, advances the phase
    /// to the corresponding terminal phase (`Entered` or
    /// `Exited`).
    ///
    /// This is the primitive the consumer drives from a
    /// `setInterval` or `requestAnimationFrame` loop. The
    /// state itself does NOT spawn a timer — that would
    /// require `wasm_bindgen_futures::spawn_local` and
    /// would prevent the primitive from being usable on
    /// native targets. Instead, the consumer is expected
    /// to wire up the timer (see the docs on
    /// `tick_until_done` for a helper that drives `tick`
    /// in a loop).
    ///
    /// # Arguments
    ///
    /// - `u32` - The number of milliseconds that have
    ///   elapsed since the last `tick` call. Must be
    ///   non-negative. The state does not validate this;
    ///   passing `0` is a no-op, passing a value larger
    ///   than the remaining duration jumps directly to
    ///   the terminal phase.
    pub fn tick(&self, elapsed_ms: u32) {
        match self.get_phase().get() {
            TransitionPhase::Exited | TransitionPhase::Entered => {
                // Terminal phases don't tick.
            }
            TransitionPhase::Entering => {
                let total: u32 = self.get_config().get().enter_ms;
                let current: f64 = self.get_progress().get();
                if total == 0 {
                    // Zero-duration enter jumps straight
                    // to `Entered`.
                    self.get_progress().set(1.0);
                    self.get_phase().set(TransitionPhase::Entered);
                    return;
                }
                let next: f64 = current + (elapsed_ms as f64) / (total as f64);
                if next >= 1.0 {
                    self.get_progress().set(1.0);
                    self.get_phase().set(TransitionPhase::Entered);
                } else {
                    self.get_progress().set(next);
                }
            }
            TransitionPhase::Exiting => {
                let total: u32 = self.get_config().get().exit_ms;
                let current: f64 = self.get_progress().get();
                if total == 0 {
                    self.get_progress().set(0.0);
                    self.get_phase().set(TransitionPhase::Exited);
                    return;
                }
                let next: f64 = current - (elapsed_ms as f64) / (total as f64);
                if next <= 0.0 {
                    self.get_progress().set(0.0);
                    self.get_phase().set(TransitionPhase::Exited);
                } else {
                    self.get_progress().set(next);
                }
            }
        }
    }

    /// Drives `tick` until the transition reaches a
    /// terminal phase (`Entered` or `Exited`), using
    /// `step_ms` as the per-tick delta.
    ///
    /// Useful for tests and for native builds that want
    /// to fast-forward a transition synchronously. On
    /// wasm the consumer should NOT use this — instead
    /// drive `tick` from a real timer loop and render
    /// each frame.
    ///
    /// # Arguments
    ///
    /// - `u32` - The per-tick delta, in milliseconds.
    ///   Typical value is `16` (≈60 fps).
    pub fn tick_until_done(&self, step_ms: u32) {
        while self.is_animating() {
            self.tick(step_ms);
        }
    }

    /// Resets the transition back to the `Exited` state
    /// (progress = `0.0`). Cancels any in-flight
    /// animation immediately. Useful for "the user
    /// closed the dialog before the exit animation
    /// finished, force-reset" flows.
    pub fn reset(&self) {
        self.get_phase().set(TransitionPhase::Exited);
        self.get_progress().set(0.0);
    }

    /// Returns the time remaining (in ms) until the
    /// current transition completes. Returns `0` for
    /// terminal phases.
    ///
    /// # Returns
    ///
    /// - `u32` - Milliseconds remaining before the transition ends.
    pub fn remaining_ms(&self) -> u32 {
        match self.get_phase().get() {
            TransitionPhase::Exited | TransitionPhase::Entered => 0,
            TransitionPhase::Entering => {
                let total: u32 = self.get_config().get().enter_ms;
                let current: f64 = self.get_progress().get() * total as f64;
                (total as f64 - current).max(0.0) as u32
            }
            TransitionPhase::Exiting => {
                let total: u32 = self.get_config().get().exit_ms;
                let current: f64 = self.get_progress().get() * total as f64;
                current as u32
            }
        }
    }
}