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    pub const fn with_durations(enter_ms: u32, exit_ms: u32) -> Self {
83        Self { enter_ms, exit_ms }
84    }
85
86    /// Returns the duration (in ms) corresponding to the
87    /// given phase. Returns `0` for terminal phases
88    /// (`Entered`, `Exited`) — these don't tick.
89    ///
90    /// # Arguments
91    ///
92    /// - `TransitionPhase` - A `TransitionPhase` parameter.
93    ///
94    /// # Returns
95    ///
96    /// - `u32` - A 32-bit unsigned integer.
97    pub fn duration_for(&self, phase: TransitionPhase) -> u32 {
98        match phase {
99            TransitionPhase::Entering => self.get_enter_ms(),
100            TransitionPhase::Exiting => self.get_exit_ms(),
101            TransitionPhase::Entered | TransitionPhase::Exited => 0,
102        }
103    }
104}
105
106/// Default-construction for [`TransitionConfig`].
107impl Default for TransitionConfig {
108    /// Constructs a default [`TransitionConfig`] value.
109    fn default() -> Self {
110        // Matches the CSS defaults used elsewhere in
111        // euv-ui (var!(duration-normal) ≈ 200ms).
112        Self::with_ms(200)
113    }
114}
115
116/// Inherent implementation of [`TransitionState`].
117impl TransitionState {
118    /// Returns `true` if the element is currently
119    /// animating (i.e. `Entering` or `Exiting`).
120    ///
121    /// # Returns
122    ///
123    /// - `bool` - `true` when an animation is currently running.
124    pub fn is_animating(&self) -> bool {
125        matches!(
126            self.get_phase().get(),
127            TransitionPhase::Entering | TransitionPhase::Exiting
128        )
129    }
130
131    /// Replaces the duration config.
132    ///
133    /// Named `change_config` (not `set_config`) to avoid
134    /// colliding with the `set_config` setter generated
135    /// by `#[derive(Data)]` on the struct field.
136    ///
137    /// # Arguments
138    ///
139    /// - `TransitionConfig` - A `TransitionConfig` parameter.
140    pub fn change_config(&self, config: TransitionConfig) {
141        self.get_config().set(config);
142    }
143
144    /// Starts the enter animation. Sets the phase to
145    /// `Entering` and resets progress to `0.0`. No-op if
146    /// the transition is already in `Entering` / `Entered`.
147    pub fn enter(&self) {
148        let current: TransitionPhase = self.get_phase().get();
149        if matches!(
150            current,
151            TransitionPhase::Entering | TransitionPhase::Entered
152        ) {
153            return;
154        }
155        self.get_phase().set(TransitionPhase::Entering);
156        self.get_progress().set(0.0);
157    }
158
159    /// Starts the exit animation. Sets the phase to
160    /// `Exiting` and starts progress from `1.0`. No-op if
161    /// the transition is already in `Exiting` / `Exited`.
162    pub fn exit(&self) {
163        let current: TransitionPhase = self.get_phase().get();
164        if matches!(current, TransitionPhase::Exiting | TransitionPhase::Exited) {
165            return;
166        }
167        self.get_phase().set(TransitionPhase::Exiting);
168        self.get_progress().set(1.0);
169    }
170
171    /// Toggles between `Entered` and `Exited`. Equivalent
172    /// to `enter()` if currently `Exiting` / `Exited`,
173    /// and `exit()` if currently `Entering` / `Entered`.
174    pub fn toggle(&self) {
175        match self.get_phase().get() {
176            TransitionPhase::Entered | TransitionPhase::Entering => {
177                self.exit();
178            }
179            TransitionPhase::Exiting | TransitionPhase::Exited => {
180                self.enter();
181            }
182        }
183    }
184
185    /// Advances the transition by `elapsed_ms`
186    /// milliseconds. Updates `progress` and, if the
187    /// transition has reached its end, advances the phase
188    /// to the corresponding terminal phase (`Entered` or
189    /// `Exited`).
190    ///
191    /// This is the primitive the consumer drives from a
192    /// `setInterval` or `requestAnimationFrame` loop. The
193    /// state itself does NOT spawn a timer — that would
194    /// require `wasm_bindgen_futures::spawn_local` and
195    /// would prevent the primitive from being usable on
196    /// native targets. Instead, the consumer is expected
197    /// to wire up the timer (see the docs on
198    /// `tick_until_done` for a helper that drives `tick`
199    /// in a loop).
200    ///
201    /// # Arguments
202    ///
203    /// - `u32` - The number of milliseconds that have
204    ///   elapsed since the last `tick` call. Must be
205    ///   non-negative. The state does not validate this;
206    ///   passing `0` is a no-op, passing a value larger
207    ///   than the remaining duration jumps directly to
208    ///   the terminal phase.
209    pub fn tick(&self, elapsed_ms: u32) {
210        match self.get_phase().get() {
211            TransitionPhase::Exited | TransitionPhase::Entered => {
212                // Terminal phases don't tick.
213            }
214            TransitionPhase::Entering => {
215                let total: u32 = self.get_config().get().enter_ms;
216                let current: f64 = self.get_progress().get();
217                if total == 0 {
218                    // Zero-duration enter jumps straight
219                    // to `Entered`.
220                    self.get_progress().set(1.0);
221                    self.get_phase().set(TransitionPhase::Entered);
222                    return;
223                }
224                let next: f64 = current + (elapsed_ms as f64) / (total as f64);
225                if next >= 1.0 {
226                    self.get_progress().set(1.0);
227                    self.get_phase().set(TransitionPhase::Entered);
228                } else {
229                    self.get_progress().set(next);
230                }
231            }
232            TransitionPhase::Exiting => {
233                let total: u32 = self.get_config().get().exit_ms;
234                let current: f64 = self.get_progress().get();
235                if total == 0 {
236                    self.get_progress().set(0.0);
237                    self.get_phase().set(TransitionPhase::Exited);
238                    return;
239                }
240                let next: f64 = current - (elapsed_ms as f64) / (total as f64);
241                if next <= 0.0 {
242                    self.get_progress().set(0.0);
243                    self.get_phase().set(TransitionPhase::Exited);
244                } else {
245                    self.get_progress().set(next);
246                }
247            }
248        }
249    }
250
251    /// Drives `tick` until the transition reaches a
252    /// terminal phase (`Entered` or `Exited`), using
253    /// `step_ms` as the per-tick delta.
254    ///
255    /// Useful for tests and for native builds that want
256    /// to fast-forward a transition synchronously. On
257    /// wasm the consumer should NOT use this — instead
258    /// drive `tick` from a real timer loop and render
259    /// each frame.
260    ///
261    /// # Arguments
262    ///
263    /// - `u32` - The per-tick delta, in milliseconds.
264    ///   Typical value is `16` (≈60 fps).
265    pub fn tick_until_done(&self, step_ms: u32) {
266        while self.is_animating() {
267            self.tick(step_ms);
268        }
269    }
270
271    /// Resets the transition back to the `Exited` state
272    /// (progress = `0.0`). Cancels any in-flight
273    /// animation immediately. Useful for "the user
274    /// closed the dialog before the exit animation
275    /// finished, force-reset" flows.
276    pub fn reset(&self) {
277        self.get_phase().set(TransitionPhase::Exited);
278        self.get_progress().set(0.0);
279    }
280
281    /// Returns the time remaining (in ms) until the
282    /// current transition completes. Returns `0` for
283    /// terminal phases.
284    ///
285    /// # Returns
286    ///
287    /// - `u32` - Milliseconds remaining before the transition ends.
288    pub fn remaining_ms(&self) -> u32 {
289        match self.get_phase().get() {
290            TransitionPhase::Exited | TransitionPhase::Entered => 0,
291            TransitionPhase::Entering => {
292                let total: u32 = self.get_config().get().enter_ms;
293                let current: f64 = self.get_progress().get() * total as f64;
294                (total as f64 - current).max(0.0) as u32
295            }
296            TransitionPhase::Exiting => {
297                let total: u32 = self.get_config().get().exit_ms;
298                let current: f64 = self.get_progress().get() * total as f64;
299                current as u32
300            }
301        }
302    }
303}