Skip to main content

euv_ui/hook/transition/
impl.rs

1use super::*;
2
3/// Implements [`HookContextTransitionExt`] for [`HookContext`].
4impl HookContextTransitionExt for HookContext {
5    /// Returns a fresh [`TransitionState`] driven by `config`.
6    ///
7    /// # Arguments
8    ///
9    /// - `TransitionConfig` - A `TransitionConfig` parameter.
10    ///
11    /// # Returns
12    ///
13    /// - `TransitionState` - A `TransitionState` value.
14    fn transition(config: TransitionConfig) -> TransitionState {
15        let hook_context: HookContext = Self::current();
16        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
17            return TransitionState::new(
18                Signal::create(TransitionPhase::Exited),
19                Signal::create(0.0_f64),
20                Signal::create(config),
21            );
22        };
23        let index: usize = inner.get_hook_index();
24        inner.set_hook_index(index + 1);
25        if index < inner.get_hooks().len()
26            && let Some(existing) = inner.get_hooks()[index].downcast_ref::<TransitionState>()
27        {
28            existing.change_config(config);
29            return existing.clone();
30        }
31        drop(inner);
32        HookContext::use_hook(|| {
33            TransitionState::new(
34                Signal::create(TransitionPhase::Exited),
35                Signal::create(0.0_f64),
36                Signal::create(config),
37            )
38        })
39    }
40}
41
42/// Inherent implementation of [`TransitionPhase`].
43impl TransitionPhase {
44    /// Returns a fresh `TransitionPhase` value of
45    /// `Exited`. Convenience for `Signal::create` call
46    /// sites.
47    pub const fn exited() -> Self {
48        TransitionPhase::Exited
49    }
50}
51
52/// Inherent implementation of [`TransitionConfig`].
53impl TransitionConfig {
54    /// Returns a config with both enter and exit durations
55    /// set to `ms`.
56    ///
57    /// Named `with_ms` (not `new`) to avoid colliding
58    /// with the `new` constructor generated by
59    /// `#[derive(New)]`. (We don't derive `New` here
60    /// because the field name `enter_ms` would generate
61    /// a `set_enter_ms` setter that nobody asked for —
62    /// keeping the struct `Copy` and writing it via
63    /// `TransitionConfig { enter_ms, exit_ms }` is
64    /// simpler.)
65    ///
66    /// # Arguments
67    ///
68    /// - `u32` - A 32-bit unsigned integer (`u32`).
69    pub const fn with_ms(ms: u32) -> Self {
70        Self {
71            enter_ms: ms,
72            exit_ms: ms,
73        }
74    }
75
76    /// Returns a config with separate enter and exit
77    /// durations. See `with_ms` for the naming note.
78    ///
79    /// # Arguments
80    ///
81    /// - `u32` - A 32-bit unsigned integer (`u32`).
82    /// - `u32` - A 32-bit unsigned integer (`u32`).
83    pub const fn with_durations(enter_ms: u32, exit_ms: u32) -> Self {
84        Self { enter_ms, exit_ms }
85    }
86
87    /// Returns the duration (in ms) corresponding to the
88    /// given phase. Returns `0` for terminal phases
89    /// (`Entered`, `Exited`) — these don't tick.
90    ///
91    /// # Arguments
92    ///
93    /// - `TransitionPhase` - A `TransitionPhase` parameter.
94    ///
95    /// # Returns
96    ///
97    /// - `u32` - A 32-bit unsigned integer.
98    pub fn duration_for(&self, phase: TransitionPhase) -> u32 {
99        match phase {
100            TransitionPhase::Entering => self.get_enter_ms(),
101            TransitionPhase::Exiting => self.get_exit_ms(),
102            TransitionPhase::Entered | TransitionPhase::Exited => 0,
103        }
104    }
105}
106
107/// Default-construction for [`TransitionConfig`].
108impl Default for TransitionConfig {
109    /// Constructs a default [`TransitionConfig`] value.
110    fn default() -> Self {
111        // Matches the CSS defaults used elsewhere in
112        // euv-ui (var!(duration-normal) ≈ 200ms).
113        Self::with_ms(200)
114    }
115}
116
117/// Inherent implementation of [`TransitionState`].
118impl TransitionState {
119    /// Returns `true` if the element is currently
120    /// animating (i.e. `Entering` or `Exiting`).
121    ///
122    /// # Returns
123    ///
124    /// - `bool` - `true` when an animation is currently running.
125    pub fn is_animating(&self) -> bool {
126        matches!(
127            self.get_phase().get(),
128            TransitionPhase::Entering | TransitionPhase::Exiting
129        )
130    }
131
132    /// Replaces the duration config.
133    ///
134    /// Named `change_config` (not `set_config`) to avoid
135    /// colliding with the `set_config` setter generated
136    /// by `#[derive(Data)]` on the struct field.
137    ///
138    /// # Arguments
139    ///
140    /// - `TransitionConfig` - A `TransitionConfig` parameter.
141    pub fn change_config(&self, config: TransitionConfig) {
142        self.get_config().set(config);
143    }
144
145    /// Starts the enter animation. Sets the phase to
146    /// `Entering` and resets progress to `0.0`. No-op if
147    /// the transition is already in `Entering` / `Entered`.
148    pub fn enter(&self) {
149        let current: TransitionPhase = self.get_phase().get();
150        if matches!(
151            current,
152            TransitionPhase::Entering | TransitionPhase::Entered
153        ) {
154            return;
155        }
156        self.get_phase().set(TransitionPhase::Entering);
157        self.get_progress().set(0.0);
158    }
159
160    /// Starts the exit animation. Sets the phase to
161    /// `Exiting` and starts progress from `1.0`. No-op if
162    /// the transition is already in `Exiting` / `Exited`.
163    pub fn exit(&self) {
164        let current: TransitionPhase = self.get_phase().get();
165        if matches!(current, TransitionPhase::Exiting | TransitionPhase::Exited) {
166            return;
167        }
168        self.get_phase().set(TransitionPhase::Exiting);
169        self.get_progress().set(1.0);
170    }
171
172    /// Toggles between `Entered` and `Exited`. Equivalent
173    /// to `enter()` if currently `Exiting` / `Exited`,
174    /// and `exit()` if currently `Entering` / `Entered`.
175    pub fn toggle(&self) {
176        match self.get_phase().get() {
177            TransitionPhase::Entered | TransitionPhase::Entering => {
178                self.exit();
179            }
180            TransitionPhase::Exiting | TransitionPhase::Exited => {
181                self.enter();
182            }
183        }
184    }
185
186    /// Advances the transition by `elapsed_ms`
187    /// milliseconds. Updates `progress` and, if the
188    /// transition has reached its end, advances the phase
189    /// to the corresponding terminal phase (`Entered` or
190    /// `Exited`).
191    ///
192    /// This is the primitive the consumer drives from a
193    /// `setInterval` or `requestAnimationFrame` loop. The
194    /// state itself does NOT spawn a timer — that would
195    /// require `wasm_bindgen_futures::spawn_local` and
196    /// would prevent the primitive from being usable on
197    /// native targets. Instead, the consumer is expected
198    /// to wire up the timer (see the docs on
199    /// `tick_until_done` for a helper that drives `tick`
200    /// in a loop).
201    ///
202    /// # Arguments
203    ///
204    /// - `u32` - The number of milliseconds that have
205    ///   elapsed since the last `tick` call. Must be
206    ///   non-negative. The state does not validate this;
207    ///   passing `0` is a no-op, passing a value larger
208    ///   than the remaining duration jumps directly to
209    ///   the terminal phase.
210    pub fn tick(&self, elapsed_ms: u32) {
211        match self.get_phase().get() {
212            TransitionPhase::Exited | TransitionPhase::Entered => {
213                // Terminal phases don't tick.
214            }
215            TransitionPhase::Entering => {
216                let total: u32 = self.get_config().get().enter_ms;
217                let current: f64 = self.get_progress().get();
218                if total == 0 {
219                    // Zero-duration enter jumps straight
220                    // to `Entered`.
221                    self.get_progress().set(1.0);
222                    self.get_phase().set(TransitionPhase::Entered);
223                    return;
224                }
225                let next: f64 = current + (elapsed_ms as f64) / (total as f64);
226                if next >= 1.0 {
227                    self.get_progress().set(1.0);
228                    self.get_phase().set(TransitionPhase::Entered);
229                } else {
230                    self.get_progress().set(next);
231                }
232            }
233            TransitionPhase::Exiting => {
234                let total: u32 = self.get_config().get().exit_ms;
235                let current: f64 = self.get_progress().get();
236                if total == 0 {
237                    self.get_progress().set(0.0);
238                    self.get_phase().set(TransitionPhase::Exited);
239                    return;
240                }
241                let next: f64 = current - (elapsed_ms as f64) / (total as f64);
242                if next <= 0.0 {
243                    self.get_progress().set(0.0);
244                    self.get_phase().set(TransitionPhase::Exited);
245                } else {
246                    self.get_progress().set(next);
247                }
248            }
249        }
250    }
251
252    /// Drives `tick` until the transition reaches a
253    /// terminal phase (`Entered` or `Exited`), using
254    /// `step_ms` as the per-tick delta.
255    ///
256    /// Useful for tests and for native builds that want
257    /// to fast-forward a transition synchronously. On
258    /// wasm the consumer should NOT use this — instead
259    /// drive `tick` from a real timer loop and render
260    /// each frame.
261    ///
262    /// # Arguments
263    ///
264    /// - `u32` - The per-tick delta, in milliseconds.
265    ///   Typical value is `16` (≈60 fps).
266    pub fn tick_until_done(&self, step_ms: u32) {
267        while self.is_animating() {
268            self.tick(step_ms);
269        }
270    }
271
272    /// Resets the transition back to the `Exited` state
273    /// (progress = `0.0`). Cancels any in-flight
274    /// animation immediately. Useful for "the user
275    /// closed the dialog before the exit animation
276    /// finished, force-reset" flows.
277    pub fn reset(&self) {
278        self.get_phase().set(TransitionPhase::Exited);
279        self.get_progress().set(0.0);
280    }
281
282    /// Returns the time remaining (in ms) until the
283    /// current transition completes. Returns `0` for
284    /// terminal phases.
285    ///
286    /// # Returns
287    ///
288    /// - `u32` - Milliseconds remaining before the transition ends.
289    pub fn remaining_ms(&self) -> u32 {
290        match self.get_phase().get() {
291            TransitionPhase::Exited | TransitionPhase::Entered => 0,
292            TransitionPhase::Entering => {
293                let total: u32 = self.get_config().get().enter_ms;
294                let current: f64 = self.get_progress().get() * total as f64;
295                (total as f64 - current).max(0.0) as u32
296            }
297            TransitionPhase::Exiting => {
298                let total: u32 = self.get_config().get().exit_ms;
299                let current: f64 = self.get_progress().get() * total as f64;
300                current as u32
301            }
302        }
303    }
304}