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}