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}