1use fret_core::{Px, Size};
25use fret_ui::theme::CubicBezier;
26use fret_ui::{ElementContext, UiHost};
27use std::time::Duration;
28
29pub use crate::headless::transition::{TransitionOutput, TransitionTimeline};
30
31#[derive(Clone, Copy)]
36pub struct TransitionProfile {
37 pub open_ticks: u64,
38 pub close_ticks: u64,
39 pub ease: fn(f32) -> f32,
40}
41
42impl TransitionProfile {
43 pub fn new(open_ticks: u64, close_ticks: u64, ease: fn(f32) -> f32) -> Self {
44 Self {
45 open_ticks,
46 close_ticks,
47 ease,
48 }
49 }
50}
51
52#[track_caller]
54pub fn drive_transition<H: UiHost>(
55 cx: &mut ElementContext<'_, H>,
56 open: bool,
57 ticks: u64,
58) -> TransitionOutput {
59 crate::declarative::transition::drive_transition(cx, open, ticks)
60}
61
62#[track_caller]
64pub fn drive_transition_with_durations<H: UiHost>(
65 cx: &mut ElementContext<'_, H>,
66 open: bool,
67 open_ticks: u64,
68 close_ticks: u64,
69) -> TransitionOutput {
70 crate::declarative::transition::drive_transition_with_durations(
71 cx,
72 open,
73 open_ticks,
74 close_ticks,
75 )
76}
77
78#[track_caller]
80pub fn drive_transition_with_durations_and_easing<H: UiHost>(
81 cx: &mut ElementContext<'_, H>,
82 open: bool,
83 open_ticks: u64,
84 close_ticks: u64,
85 ease: fn(f32) -> f32,
86) -> TransitionOutput {
87 crate::declarative::transition::drive_transition_with_durations_and_easing(
88 cx,
89 open,
90 open_ticks,
91 close_ticks,
92 ease,
93 )
94}
95
96#[track_caller]
101pub fn drive_transition_with_durations_and_easing_with_mount_behavior<H: UiHost>(
102 cx: &mut ElementContext<'_, H>,
103 open: bool,
104 open_ticks: u64,
105 close_ticks: u64,
106 ease: fn(f32) -> f32,
107 animate_on_mount: bool,
108) -> TransitionOutput {
109 crate::declarative::transition::drive_transition_with_durations_and_easing_with_mount_behavior(
110 cx,
111 open,
112 open_ticks,
113 close_ticks,
114 ease,
115 animate_on_mount,
116 )
117}
118
119#[track_caller]
124pub fn drive_transition_with_durations_and_cubic_bezier_duration_with_mount_behavior<H: UiHost>(
125 cx: &mut ElementContext<'_, H>,
126 open: bool,
127 open_duration: Duration,
128 close_duration: Duration,
129 bezier: CubicBezier,
130 animate_on_mount: bool,
131) -> TransitionOutput {
132 crate::declarative::transition::drive_transition_with_durations_and_cubic_bezier_duration_with_mount_behavior(
133 cx,
134 open,
135 open_duration,
136 close_duration,
137 bezier,
138 animate_on_mount,
139 )
140}
141
142#[track_caller]
144pub fn drive_transition_with_profile<H: UiHost>(
145 cx: &mut ElementContext<'_, H>,
146 open: bool,
147 profile: TransitionProfile,
148) -> TransitionOutput {
149 drive_transition_with_durations_and_easing(
150 cx,
151 open,
152 profile.open_ticks,
153 profile.close_ticks,
154 profile.ease,
155 )
156}
157
158#[track_caller]
160pub fn drive_transition_with_profile_and_mount_behavior<H: UiHost>(
161 cx: &mut ElementContext<'_, H>,
162 open: bool,
163 profile: TransitionProfile,
164 animate_on_mount: bool,
165) -> TransitionOutput {
166 drive_transition_with_durations_and_easing_with_mount_behavior(
167 cx,
168 open,
169 profile.open_ticks,
170 profile.close_ticks,
171 profile.ease,
172 animate_on_mount,
173 )
174}
175
176pub fn lerp_px(from: Px, to: Px, t: f32) -> Px {
178 let t = t.clamp(0.0, 1.0);
179 Px(from.0 + (to.0 - from.0) * t)
180}
181
182pub fn lerp_size(from: Size, to: Size, t: f32) -> Size {
184 Size::new(
185 lerp_px(from.width, to.width, t),
186 lerp_px(from.height, to.height, t),
187 )
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use fret_app::App;
194 use fret_core::{AppWindowId, Point, Rect, Size as CoreSize};
195 use fret_runtime::{FrameId, TickId};
196
197 fn bounds() -> Rect {
198 Rect::new(
199 Point::new(Px(0.0), Px(0.0)),
200 CoreSize::new(Px(200.0), Px(120.0)),
201 )
202 }
203
204 #[test]
205 fn lerp_px_is_clamped_and_monotonic() {
206 assert_eq!(lerp_px(Px(0.0), Px(10.0), -1.0), Px(0.0));
207 assert_eq!(lerp_px(Px(0.0), Px(10.0), 0.0), Px(0.0));
208 assert_eq!(lerp_px(Px(0.0), Px(10.0), 0.5), Px(5.0));
209 assert_eq!(lerp_px(Px(0.0), Px(10.0), 1.0), Px(10.0));
210 assert_eq!(lerp_px(Px(0.0), Px(10.0), 2.0), Px(10.0));
211 }
212
213 #[test]
214 fn wrapper_drivers_keep_independent_state_per_call_site() {
215 let window = AppWindowId::default();
216 let mut app = App::new();
217
218 app.set_tick_id(TickId(1));
219 app.set_frame_id(FrameId(1));
220
221 let (a, b) = fret_ui::elements::with_element_cx(&mut app, window, bounds(), "t", |cx| {
222 let a = drive_transition_with_durations(cx, true, 6, 6);
223 let b = drive_transition_with_durations(cx, false, 6, 6);
224 (a, b)
225 });
226
227 assert!(a.present);
228 assert!(a.animating);
229 assert!(a.progress > 0.0 && a.progress < 1.0);
230
231 assert!(!b.present);
232 assert!(!b.animating);
233 assert_eq!(b.progress, 0.0);
234 }
235
236 #[test]
237 fn wrapper_can_snap_on_mount_then_animate_on_toggle() {
238 let window = AppWindowId::default();
239 let mut app = App::new();
240
241 app.set_tick_id(TickId(1));
242 app.set_frame_id(FrameId(1));
243
244 let open = fret_ui::elements::with_element_cx(&mut app, window, bounds(), "t3", |cx| {
245 drive_transition_with_profile_and_mount_behavior(
246 cx,
247 true,
248 TransitionProfile::new(6, 6, crate::headless::easing::smoothstep),
249 false,
250 )
251 });
252 assert!(open.present);
253 assert!(!open.animating);
254 assert_eq!(open.progress, 1.0);
255
256 app.set_tick_id(TickId(2));
257 app.set_frame_id(FrameId(2));
258 let close = fret_ui::elements::with_element_cx(&mut app, window, bounds(), "t3", |cx| {
259 drive_transition_with_profile_and_mount_behavior(
260 cx,
261 false,
262 TransitionProfile::new(6, 6, crate::headless::easing::smoothstep),
263 false,
264 )
265 });
266 assert!(!close.present);
267 assert!(!close.animating);
268 assert_eq!(close.progress, 0.0);
269 }
270}