Skip to main content

gpui/
style_transitions.rs

1use scheduler::Instant;
2
3use crate::{
4    AbsoluteLength, Animated, AnimatedSample, AnyMotion, Background, Bounds, DefiniteLength, Fill,
5    Hsla, Length, Lerp, Pixels, Progress, RingColor,
6};
7
8#[derive(Clone, Copy)]
9pub(crate) struct StyleTransitionContext {
10    pub(crate) bounds: Option<Bounds<Pixels>>,
11    pub(crate) containing_bounds: Option<Bounds<Pixels>>,
12    pub(crate) rem_size: Pixels,
13}
14
15impl StyleTransitionContext {
16    pub(crate) fn new(bounds: Option<Bounds<Pixels>>, rem_size: Pixels) -> Self {
17        Self {
18            bounds,
19            containing_bounds: None,
20            rem_size,
21        }
22    }
23
24    pub(crate) fn with_containing_bounds(
25        mut self,
26        containing_bounds: Option<Bounds<Pixels>>,
27    ) -> Self {
28        self.containing_bounds = containing_bounds;
29        self
30    }
31}
32
33pub(crate) struct StyleTransitionPropertyState<T: Lerp + Clone + PartialEq> {
34    animated: Option<Animated<T, Instant>>,
35}
36
37struct StyleTransitionEvaluation<T> {
38    value: Option<T>,
39    progress: Progress,
40    is_active: bool,
41}
42
43impl<T> StyleTransitionEvaluation<T> {
44    fn from_sample(sample: AnimatedSample<T>) -> Self {
45        Self {
46            value: Some(sample.value),
47            progress: sample.progress,
48            is_active: sample.is_active,
49        }
50    }
51
52    fn at_target(value: Option<T>) -> Self {
53        Self {
54            value,
55            progress: Progress::END,
56            is_active: false,
57        }
58    }
59
60    /// An inactive sample at Progress::END presents the authored target.
61    /// Other inactive progress retains the sampled presentation, such as an
62    /// alternating run that settles at its origin or a custom easing endpoint.
63    fn presents_authored_target(&self) -> bool {
64        !self.is_active && self.progress == Progress::END
65    }
66}
67
68struct SizeTransitionState<T: Lerp + Clone + PartialEq> {
69    width: Option<StyleTransitionPropertyState<T>>,
70    height: Option<StyleTransitionPropertyState<T>>,
71}
72
73#[derive(Clone, Copy)]
74pub(crate) enum StyleTransitionAxis {
75    Width,
76    Height,
77}
78
79#[derive(Clone, Copy)]
80pub(crate) enum StyleTransitionEdge {
81    Top,
82    Right,
83    Bottom,
84    Left,
85}
86
87#[derive(Default)]
88struct AutoSizeTransitionPropertyState {
89    authored_goal: Option<Length>,
90    resolved_auto: Option<Pixels>,
91    pending_auto_capture: bool,
92    animated: Option<Animated<Pixels, Instant>>,
93}
94
95#[derive(Default)]
96struct AutoSizeTransitionState {
97    width: Option<AutoSizeTransitionPropertyState>,
98    height: Option<AutoSizeTransitionPropertyState>,
99}
100
101#[derive(Default)]
102struct InsetTransitionPropertyState {
103    authored_goal: Option<Length>,
104    resolved_auto: Option<Pixels>,
105    containing_size: Option<Pixels>,
106    active: bool,
107    animated: Option<Animated<Pixels, Instant>>,
108}
109
110#[derive(Default)]
111struct InsetTransitionState {
112    top: Option<InsetTransitionPropertyState>,
113    right: Option<InsetTransitionPropertyState>,
114    bottom: Option<InsetTransitionPropertyState>,
115    left: Option<InsetTransitionPropertyState>,
116}
117
118impl<T: Lerp + Clone + PartialEq> Default for SizeTransitionState<T> {
119    fn default() -> Self {
120        Self {
121            width: None,
122            height: None,
123        }
124    }
125}
126
127struct EdgesTransitionState<T: Lerp + Clone + PartialEq> {
128    top: Option<StyleTransitionPropertyState<T>>,
129    right: Option<StyleTransitionPropertyState<T>>,
130    bottom: Option<StyleTransitionPropertyState<T>>,
131    left: Option<StyleTransitionPropertyState<T>>,
132}
133
134impl<T: Lerp + Clone + PartialEq> Default for EdgesTransitionState<T> {
135    fn default() -> Self {
136        Self {
137            top: None,
138            right: None,
139            bottom: None,
140            left: None,
141        }
142    }
143}
144
145struct CornersTransitionState<T: Lerp + Clone + PartialEq> {
146    top_left: Option<StyleTransitionPropertyState<T>>,
147    top_right: Option<StyleTransitionPropertyState<T>>,
148    bottom_right: Option<StyleTransitionPropertyState<T>>,
149    bottom_left: Option<StyleTransitionPropertyState<T>>,
150}
151
152impl<T: Lerp + Clone + PartialEq> Default for CornersTransitionState<T> {
153    fn default() -> Self {
154        Self {
155            top_left: None,
156            top_right: None,
157            bottom_right: None,
158            bottom_left: None,
159        }
160    }
161}
162
163#[derive(Default)]
164struct TextStyleTransitionState {
165    color: Option<StyleTransitionPropertyState<Hsla>>,
166    background_color: Option<StyleTransitionPropertyState<Hsla>>,
167    font_size: Option<StyleTransitionPropertyState<AbsoluteLength>>,
168    line_height: Option<StyleTransitionPropertyState<DefiniteLength>>,
169    letter_spacing: Option<StyleTransitionPropertyState<Pixels>>,
170    line_clamp: Option<StyleTransitionPropertyState<usize>>,
171}
172
173#[derive(Default)]
174struct RingStyleTransitionState {
175    width: Option<StyleTransitionPropertyState<Pixels>>,
176    color: Option<StyleTransitionPropertyState<RingColor>>,
177}
178
179#[derive(Default)]
180pub(crate) struct StyleTransitionState {
181    inset: InsetTransitionState,
182    size: AutoSizeTransitionState,
183    min_size: SizeTransitionState<Length>,
184    max_size: SizeTransitionState<Length>,
185    margin: EdgesTransitionState<Length>,
186    padding: EdgesTransitionState<DefiniteLength>,
187    border_widths: EdgesTransitionState<AbsoluteLength>,
188    gap: SizeTransitionState<DefiniteLength>,
189    corner_radii: CornersTransitionState<AbsoluteLength>,
190    corner_smoothing: Option<StyleTransitionPropertyState<f32>>,
191    scrollbar_width: Option<StyleTransitionPropertyState<AbsoluteLength>>,
192    aspect_ratio: Option<StyleTransitionPropertyState<f32>>,
193    flex_basis: Option<StyleTransitionPropertyState<Length>>,
194    flex_grow: Option<StyleTransitionPropertyState<f32>>,
195    flex_shrink: Option<StyleTransitionPropertyState<f32>>,
196    background: Option<StyleTransitionPropertyState<Fill>>,
197    border_color: Option<StyleTransitionPropertyState<Background>>,
198    ring: RingStyleTransitionState,
199    inset_ring: RingStyleTransitionState,
200    text: TextStyleTransitionState,
201    opacity: Option<StyleTransitionPropertyState<f32>>,
202}
203
204fn apply_auto_size(
205    state: &mut Option<AutoSizeTransitionPropertyState>,
206    value: &mut Length,
207    axis: StyleTransitionAxis,
208    motion: Option<&AnyMotion>,
209    context: StyleTransitionContext,
210    now: Instant,
211    reduce_motion: bool,
212) -> bool {
213    let Some(motion) = motion else {
214        *state = None;
215        return false;
216    };
217
218    let authored_goal = *value;
219    let state = state.get_or_insert_with(Default::default);
220    let bounds_value = context.bounds.map(|bounds| match axis {
221        StyleTransitionAxis::Width => bounds.size.width,
222        StyleTransitionAxis::Height => bounds.size.height,
223    });
224    let endpoint = match authored_goal {
225        Length::Definite(DefiniteLength::Absolute(length)) => {
226            Some(length.to_pixels(context.rem_size))
227        }
228        Length::Definite(DefiniteLength::Relative(_)) => None,
229        Length::Auto => state.resolved_auto,
230    };
231
232    let Some(endpoint) = endpoint else {
233        if authored_goal == Length::Auto {
234            if let Some(bounds_value) = bounds_value {
235                state.resolved_auto = Some(bounds_value);
236                state.pending_auto_capture = false;
237                match state.animated.as_mut() {
238                    Some(animated) => animated.jump_to(bounds_value),
239                    None => state.animated = Some(Animated::new(bounds_value, motion.clone())),
240                }
241            } else {
242                state.pending_auto_capture = true;
243            }
244        } else {
245            state.animated = None;
246            state.pending_auto_capture = false;
247        }
248        state.authored_goal = Some(authored_goal);
249        return false;
250    };
251
252    if state.pending_auto_capture {
253        if let Some(bounds_value) = bounds_value {
254            state.resolved_auto = Some(bounds_value);
255            state.pending_auto_capture = false;
256            match state.animated.as_mut() {
257                Some(animated) => animated.jump_to(bounds_value),
258                None => state.animated = Some(Animated::new(bounds_value, motion.clone())),
259            }
260        }
261        state.authored_goal = Some(authored_goal);
262        return false;
263    }
264
265    let animated = state
266        .animated
267        .get_or_insert_with(|| Animated::new(endpoint, motion.clone()));
268
269    if reduce_motion {
270        animated.jump_to(endpoint);
271    } else if state.authored_goal != Some(authored_goal) || animated.value() != &endpoint {
272        animated.set(endpoint, motion, now);
273    }
274
275    let evaluation = StyleTransitionEvaluation::from_sample(animated.sample(now));
276    state.authored_goal = Some(authored_goal);
277
278    if !evaluation.presents_authored_target() {
279        let sampled_value = evaluation
280            .value
281            .expect("an Animated sample always contains a value");
282        *value = Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(
283            sampled_value,
284        )));
285    } else if authored_goal == Length::Auto
286        && let Some(bounds_value) = bounds_value
287    {
288        state.resolved_auto = Some(bounds_value);
289        animated.jump_to(bounds_value);
290    } else {
291        animated.adopt_completed_target();
292    }
293
294    evaluation.is_active
295}
296
297fn evaluate<T>(
298    state: &mut Option<StyleTransitionPropertyState<T>>,
299    target: Option<T>,
300    motion: &AnyMotion,
301    now: Instant,
302    reduce_motion: bool,
303) -> StyleTransitionEvaluation<T>
304where
305    T: Lerp + Clone + PartialEq,
306{
307    state
308        .get_or_insert_with(|| StyleTransitionPropertyState::new(target.clone(), motion))
309        .evaluate(target, motion, now, reduce_motion)
310}
311
312fn apply_required<T>(
313    state: &mut Option<StyleTransitionPropertyState<T>>,
314    value: &mut T,
315    motion: Option<&AnyMotion>,
316    now: Instant,
317    reduce_motion: bool,
318) -> bool
319where
320    T: Lerp + Clone + PartialEq,
321{
322    let target = value.clone();
323    apply_required_target(state, value, Some(target), motion, now, reduce_motion)
324}
325
326fn apply_required_target<T>(
327    state: &mut Option<StyleTransitionPropertyState<T>>,
328    value: &mut T,
329    target: Option<T>,
330    motion: Option<&AnyMotion>,
331    now: Instant,
332    reduce_motion: bool,
333) -> bool
334where
335    T: Lerp + Clone + PartialEq,
336{
337    let Some(motion) = motion else {
338        *state = None;
339        return false;
340    };
341    let Some(target) = target else {
342        return false;
343    };
344
345    let evaluation = evaluate(state, Some(target), motion, now, reduce_motion);
346    if !evaluation.presents_authored_target()
347        && let Some(evaluated_value) = evaluation.value
348    {
349        *value = evaluated_value;
350    }
351    evaluation.is_active
352}
353
354fn apply_optional<T>(
355    state: &mut Option<StyleTransitionPropertyState<T>>,
356    value: &mut Option<T>,
357    motion: Option<&AnyMotion>,
358    now: Instant,
359    reduce_motion: bool,
360) -> bool
361where
362    T: Lerp + Clone + Default + PartialEq,
363{
364    let Some(motion) = motion else {
365        *state = None;
366        return false;
367    };
368
369    let target = value.clone().unwrap_or_default();
370    let evaluation = evaluate(state, Some(target), motion, now, reduce_motion);
371    if !evaluation.presents_authored_target() {
372        *value = evaluation.value;
373    }
374    evaluation.is_active
375}
376
377fn apply_inset(
378    state: &mut Option<InsetTransitionPropertyState>,
379    value: &mut Length,
380    edge: StyleTransitionEdge,
381    motion: Option<&AnyMotion>,
382    context: StyleTransitionContext,
383    now: Instant,
384    reduce_motion: bool,
385) -> bool {
386    let Some(motion) = motion else {
387        *state = None;
388        return false;
389    };
390
391    let authored_goal = *value;
392    let state = state.get_or_insert_with(Default::default);
393
394    if let (Some(bounds), Some(containing_bounds)) = (context.bounds, context.containing_bounds) {
395        state.containing_size = Some(match edge {
396            StyleTransitionEdge::Top | StyleTransitionEdge::Bottom => containing_bounds.size.height,
397            StyleTransitionEdge::Right | StyleTransitionEdge::Left => containing_bounds.size.width,
398        });
399
400        if authored_goal == Length::Auto && !state.active {
401            let resolved_auto = match edge {
402                StyleTransitionEdge::Top => bounds.origin.y - containing_bounds.origin.y,
403                StyleTransitionEdge::Right => {
404                    containing_bounds.bottom_right().x - bounds.bottom_right().x
405                }
406                StyleTransitionEdge::Bottom => {
407                    containing_bounds.bottom_right().y - bounds.bottom_right().y
408                }
409                StyleTransitionEdge::Left => bounds.origin.x - containing_bounds.origin.x,
410            };
411            state.resolved_auto = Some(resolved_auto);
412            if let Some(animated) = state.animated.as_mut() {
413                animated.jump_to(resolved_auto);
414            }
415        }
416    }
417
418    let endpoint = match authored_goal {
419        Length::Auto => state.resolved_auto,
420        Length::Definite(length) => match length {
421            DefiniteLength::Absolute(length) => Some(length.to_pixels(context.rem_size)),
422            DefiniteLength::Relative(_) => state.containing_size.map(|containing_size| {
423                length.to_pixels(AbsoluteLength::Pixels(containing_size), context.rem_size)
424            }),
425        },
426    };
427
428    let Some(endpoint) = endpoint else {
429        state.authored_goal = Some(authored_goal);
430        state.active = false;
431        state.animated = None;
432        return false;
433    };
434
435    let previous_goal = state.authored_goal;
436    let animated = state.animated.get_or_insert_with(|| {
437        Animated::new(
438            if previous_goal == Some(Length::Auto) {
439                state.resolved_auto.unwrap_or(endpoint)
440            } else {
441                endpoint
442            },
443            motion.clone(),
444        )
445    });
446
447    if previous_goal == Some(Length::Auto) && authored_goal != Length::Auto {
448        animated.jump_to(state.resolved_auto.unwrap_or(endpoint));
449    }
450
451    if reduce_motion {
452        animated.jump_to(endpoint);
453    } else if previous_goal != Some(authored_goal) || animated.value() != &endpoint {
454        animated.set(endpoint, motion, now);
455    }
456
457    let evaluation = StyleTransitionEvaluation::from_sample(animated.sample(now));
458    state.authored_goal = Some(authored_goal);
459    state.active = evaluation.is_active;
460
461    if !evaluation.presents_authored_target() {
462        let sampled_value = evaluation
463            .value
464            .expect("an Animated sample always contains a value");
465        *value = Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(
466            sampled_value,
467        )));
468    } else {
469        animated.adopt_completed_target();
470    }
471
472    evaluation.is_active
473}
474
475impl<T> StyleTransitionPropertyState<T>
476where
477    T: Lerp + Clone + PartialEq,
478{
479    fn new(initial_goal: Option<T>, motion: &AnyMotion) -> Self {
480        Self {
481            animated: initial_goal.map(|goal| Animated::new(goal, motion.clone())),
482        }
483    }
484
485    fn jump_to(&mut self, goal: Option<T>, motion: &AnyMotion) {
486        match (self.animated.as_mut(), goal) {
487            (Some(animated), Some(goal)) => animated.jump_to(goal),
488            (_, Some(goal)) => {
489                self.animated = Some(Animated::new(goal, motion.clone()));
490            }
491            (_, None) => self.animated = None,
492        }
493    }
494
495    fn evaluate(
496        &mut self,
497        goal: Option<T>,
498        motion: &AnyMotion,
499        now: Instant,
500        reduce_motion: bool,
501    ) -> StyleTransitionEvaluation<T> {
502        if reduce_motion {
503            self.jump_to(goal, motion);
504            return StyleTransitionEvaluation::at_target(
505                self.animated
506                    .as_ref()
507                    .map(|animated| animated.value().clone()),
508            );
509        }
510
511        let Some(goal) = goal else {
512            self.animated = None;
513            return StyleTransitionEvaluation::at_target(None);
514        };
515
516        let Some(animated) = self.animated.as_mut() else {
517            self.animated = Some(Animated::new(goal.clone(), motion.clone()));
518            return StyleTransitionEvaluation::at_target(Some(goal));
519        };
520
521        animated.set(goal, motion, now);
522        let evaluation = StyleTransitionEvaluation::from_sample(animated.sample(now));
523        if evaluation.presents_authored_target() {
524            animated.adopt_completed_target();
525        }
526        evaluation
527    }
528}
529
530gpui_macros::style_transitions!();
531
532#[cfg(test)]
533mod tests {
534    use std::cell::Cell;
535    use std::rc::Rc;
536    use std::time::Duration;
537
538    use super::*;
539    use crate::{
540        AbsoluteLength, AnyWindowHandle, Bounds, Corners, DefiniteLength, Edges, FocusHandle,
541        InputEvent as _, Length, Motion, MotionDurationExt, MouseButton, MouseDownEvent,
542        MouseUpEvent, Pixels, SpringConfig, Style, TestAppContext, Window, blue, canvas, div,
543        ease_in_out, point, prelude::*, px, red, relative, rems, size,
544    };
545
546    #[derive(Clone, Debug, Default, PartialEq)]
547    struct EndpointBiased(f32);
548
549    impl Lerp for EndpointBiased {
550        fn lerp(&self, to: &Self, delta: f32) -> Self {
551            let interpolated = self.0 + (to.0 - self.0) * delta;
552            Self(if delta == 1.0 {
553                interpolated + 0.25
554            } else {
555                interpolated
556            })
557        }
558    }
559
560    fn length(value: f32) -> Length {
561        Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(value))))
562    }
563
564    fn corners(value: f32) -> Corners<AbsoluteLength> {
565        let value = AbsoluteLength::Pixels(px(value));
566        Corners {
567            top_left: value,
568            top_right: value,
569            bottom_right: value,
570            bottom_left: value,
571        }
572    }
573
574    fn size_transition_after(transitions: StyleTransitions, elapsed: Duration) -> (bool, Style) {
575        let started_at = Instant::now();
576        let mut state = StyleTransitionState::default();
577        let mut style = Style {
578            size: size(length(10.0), length(10.0)),
579            ..Style::default()
580        };
581
582        let context = StyleTransitionContext::new(None, px(16.0));
583        assert!(!transitions.apply(&mut style, &mut state, context, started_at, false));
584
585        style.size = size(length(20.0), length(20.0));
586        assert!(transitions.apply(&mut style, &mut state, context, started_at, false));
587
588        style.size = size(length(20.0), length(20.0));
589        let in_progress =
590            transitions.apply(&mut style, &mut state, context, started_at + elapsed, false);
591
592        (in_progress, style)
593    }
594
595    #[test]
596    fn alternating_style_motion_keeps_its_origin_after_completion() {
597        let started_at = Instant::now();
598        let motion = Motion::new(Duration::from_secs(1))
599            .with_delay(Duration::from_millis(500))
600            .iterations(2)
601            .alternate();
602        let transitions = StyleTransitions::new()
603            .flex_grow(motion.clone())
604            .opacity(motion);
605        let context = StyleTransitionContext::new(None, px(16.0));
606        let mut state = StyleTransitionState::default();
607        let mut style = Style {
608            flex_grow: 4.0,
609            opacity: Some(1.0),
610            ..Style::default()
611        };
612
613        assert!(!transitions.apply(&mut style, &mut state, context, started_at, false));
614
615        for (milliseconds, expected_grow, expected_opacity, active) in [
616            (0, 4.0, Some(1.0), true),
617            (500, 4.0, Some(1.0), true),
618            (1_000, 7.0, Some(0.5), true),
619            (1_500, 10.0, Some(0.0), true),
620            (2_000, 7.0, Some(0.5), true),
621            (2_500, 4.0, Some(1.0), false),
622            (3_000, 4.0, Some(1.0), false),
623        ] {
624            let mut style = Style {
625                flex_grow: 10.0,
626                opacity: None,
627                ..Style::default()
628            };
629            assert_eq!(
630                transitions.apply(
631                    &mut style,
632                    &mut state,
633                    context,
634                    started_at + Duration::from_millis(milliseconds),
635                    false,
636                ),
637                active
638            );
639            assert_eq!(style.flex_grow, expected_grow);
640            assert_eq!(style.opacity, expected_opacity);
641        }
642
643        let mut style = Style {
644            flex_grow: 20.0,
645            opacity: None,
646            ..Style::default()
647        };
648        assert!(!transitions.apply(
649            &mut style,
650            &mut state,
651            context,
652            started_at + Duration::from_millis(3_100),
653            true,
654        ));
655        assert_eq!((style.flex_grow, style.opacity), (20.0, None));
656
657        assert!(!transitions.apply(
658            &mut style,
659            &mut state,
660            context,
661            started_at + Duration::from_millis(3_200),
662            false,
663        ));
664        assert_eq!((style.flex_grow, style.opacity), (20.0, None));
665    }
666
667    #[test]
668    fn required_completion_uses_motion_progress_not_value_equality() {
669        let started_at = Instant::now();
670        let motion: AnyMotion = Motion::new(Duration::from_secs(1)).into();
671        let mut state = None;
672        let mut value = EndpointBiased(10.0);
673
674        assert!(!apply_required(
675            &mut state,
676            &mut value,
677            Some(&motion),
678            started_at,
679            false,
680        ));
681
682        value = EndpointBiased(20.0);
683        assert!(apply_required(
684            &mut state,
685            &mut value,
686            Some(&motion),
687            started_at,
688            false,
689        ));
690        assert_eq!(value, EndpointBiased(10.0));
691
692        value = EndpointBiased(20.0);
693        assert!(apply_required(
694            &mut state,
695            &mut value,
696            Some(&motion),
697            started_at + Duration::from_millis(500),
698            false,
699        ));
700        assert_eq!(value, EndpointBiased(15.0));
701
702        value = EndpointBiased(20.0);
703        assert!(!apply_required(
704            &mut state,
705            &mut value,
706            Some(&motion),
707            started_at + Duration::from_secs(1),
708            false,
709        ));
710        assert_eq!(value, EndpointBiased(20.0));
711
712        value = EndpointBiased(30.0);
713        assert!(apply_required(
714            &mut state,
715            &mut value,
716            Some(&motion),
717            started_at + Duration::from_secs(2),
718            false,
719        ));
720        assert_eq!(value, EndpointBiased(20.0));
721
722        value = EndpointBiased(30.0);
723        assert!(apply_required(
724            &mut state,
725            &mut value,
726            Some(&motion),
727            started_at + Duration::from_millis(2_500),
728            false,
729        ));
730        assert_eq!(value, EndpointBiased(25.0));
731
732        let nonstandard_endpoint: AnyMotion = Motion::new(Duration::from_secs(1))
733            .with_easing(|_| 0.5)
734            .into();
735        let mut state = None;
736        let mut value = 10.0_f32;
737        assert!(!apply_required(
738            &mut state,
739            &mut value,
740            Some(&nonstandard_endpoint),
741            started_at,
742            false,
743        ));
744
745        value = 20.0;
746        assert!(apply_required(
747            &mut state,
748            &mut value,
749            Some(&nonstandard_endpoint),
750            started_at,
751            false,
752        ));
753        assert_eq!(value, 15.0);
754
755        value = 20.0;
756        assert!(!apply_required(
757            &mut state,
758            &mut value,
759            Some(&nonstandard_endpoint),
760            started_at + Duration::from_secs(1),
761            false,
762        ));
763        assert_eq!(value, 15.0);
764    }
765
766    #[test]
767    fn optional_completion_uses_motion_progress_not_value_equality() {
768        let started_at = Instant::now();
769        let motion: AnyMotion = Motion::new(Duration::from_secs(1)).into();
770        let mut state = None;
771        let mut value = Some(EndpointBiased(10.0));
772
773        assert!(!apply_optional(
774            &mut state,
775            &mut value,
776            Some(&motion),
777            started_at,
778            false,
779        ));
780
781        value = None;
782        assert!(apply_optional(
783            &mut state,
784            &mut value,
785            Some(&motion),
786            started_at,
787            false,
788        ));
789        assert_eq!(value, Some(EndpointBiased(10.0)));
790
791        value = None;
792        assert!(!apply_optional(
793            &mut state,
794            &mut value,
795            Some(&motion),
796            started_at + Duration::from_secs(1),
797            false,
798        ));
799        assert_eq!(value, None);
800
801        value = None;
802        assert!(!apply_optional(
803            &mut state,
804            &mut value,
805            Some(&motion),
806            started_at + Duration::from_secs(2),
807            false,
808        ));
809        assert_eq!(value, None);
810
811        let nonstandard_endpoint: AnyMotion = Motion::new(Duration::from_secs(1))
812            .with_easing(|_| 0.5)
813            .into();
814        let mut state = None;
815        let mut value = Some(10.0_f32);
816        assert!(!apply_optional(
817            &mut state,
818            &mut value,
819            Some(&nonstandard_endpoint),
820            started_at,
821            false,
822        ));
823
824        value = None;
825        assert!(apply_optional(
826            &mut state,
827            &mut value,
828            Some(&nonstandard_endpoint),
829            started_at,
830            false,
831        ));
832        assert_eq!(value, Some(5.0));
833
834        value = None;
835        assert!(!apply_optional(
836            &mut state,
837            &mut value,
838            Some(&nonstandard_endpoint),
839            started_at + Duration::from_secs(1),
840            false,
841        ));
842        assert_eq!(value, Some(5.0));
843    }
844
845    #[test]
846    fn optional_some_completion_and_retarget_use_the_authored_endpoint() {
847        let started_at = Instant::now();
848        let motion: AnyMotion = Motion::new(Duration::from_secs(1)).into();
849        let mut state = None;
850        let mut value = Some(EndpointBiased(10.0));
851
852        assert!(!apply_optional(
853            &mut state,
854            &mut value,
855            Some(&motion),
856            started_at,
857            false,
858        ));
859
860        value = Some(EndpointBiased(20.0));
861        assert!(apply_optional(
862            &mut state,
863            &mut value,
864            Some(&motion),
865            started_at,
866            false,
867        ));
868        assert_eq!(value, Some(EndpointBiased(10.0)));
869
870        value = Some(EndpointBiased(20.0));
871        assert!(!apply_optional(
872            &mut state,
873            &mut value,
874            Some(&motion),
875            started_at + Duration::from_secs(1),
876            false,
877        ));
878        assert_eq!(value, Some(EndpointBiased(20.0)));
879
880        value = Some(EndpointBiased(30.0));
881        assert!(apply_optional(
882            &mut state,
883            &mut value,
884            Some(&motion),
885            started_at + Duration::from_secs(2),
886            false,
887        ));
888        assert_eq!(value, Some(EndpointBiased(20.0)));
889    }
890
891    #[test]
892    fn alternating_auto_size_and_inset_keep_resolved_origins() {
893        let started_at = Instant::now();
894        let motion = Motion::new(Duration::from_secs(1))
895            .iterations(2)
896            .alternate();
897        let transitions = StyleTransitions::new().w(motion.clone()).top(motion);
898        let layout_context = StyleTransitionContext::new(None, px(16.0));
899        let prepaint_context = StyleTransitionContext::new(
900            Some(Bounds {
901                origin: point(px(0.0), px(15.0)),
902                size: size(px(100.0), px(40.0)),
903            }),
904            px(16.0),
905        )
906        .with_containing_bounds(Some(Bounds {
907            origin: point(px(0.0), px(0.0)),
908            size: size(px(300.0), px(200.0)),
909        }));
910        let mut state = StyleTransitionState::default();
911        let mut style = Style::default();
912
913        assert!(!transitions.apply(&mut style, &mut state, layout_context, started_at, false));
914        assert!(!transitions.apply(&mut style, &mut state, prepaint_context, started_at, false,));
915
916        for (elapsed, expected_width, expected_top, active) in [
917            (Duration::ZERO, 100.0, 15.0, true),
918            (Duration::from_secs(1), 200.0, 5.0, true),
919            (Duration::from_secs(2), 100.0, 15.0, false),
920            (Duration::from_secs(3), 100.0, 15.0, false),
921        ] {
922            let mut style = Style::default();
923            style.size.width = length(200.0);
924            style.inset.top = length(5.0);
925            assert_eq!(
926                transitions.apply(
927                    &mut style,
928                    &mut state,
929                    layout_context,
930                    started_at + elapsed,
931                    false,
932                ),
933                active
934            );
935            assert_eq!(style.size.width, length(expected_width));
936            assert_eq!(style.inset.top, length(expected_top));
937        }
938
939        let mut style = Style::default();
940        style.size.width = length(240.0);
941        style.inset.top = length(8.0);
942        assert!(!transitions.apply(
943            &mut style,
944            &mut state,
945            layout_context,
946            started_at + Duration::from_millis(3_100),
947            true,
948        ));
949        assert_eq!(style.size.width, length(240.0));
950        assert_eq!(style.inset.top, length(8.0));
951    }
952
953    #[test]
954    fn spring_style_transitions_overshoot_retarget_and_settle() {
955        let config = SpringConfig::new(100.0, 6.0, 1.0);
956        let transitions = StyleTransitions::new()
957            .flex_grow(config)
958            .opacity(Motion::spring(config).with_epsilon(0.01));
959        let context = StyleTransitionContext::new(None, px(16.0));
960        let started_at = Instant::now();
961        let mut state = StyleTransitionState::default();
962        let mut style = Style::default();
963
964        assert!(!transitions.apply(&mut style, &mut state, context, started_at, false));
965
966        style.flex_grow = 10.0;
967        style.opacity = Some(1.0);
968        assert!(transitions.apply(&mut style, &mut state, context, started_at, false));
969        assert_eq!(style.flex_grow, 0.0);
970
971        let overshoot_at = started_at + Duration::from_millis(350);
972        style.flex_grow = 10.0;
973        style.opacity = Some(1.0);
974        assert!(transitions.apply(&mut style, &mut state, context, overshoot_at, false));
975        assert!(style.flex_grow > 10.0);
976        let overshoot = style.flex_grow;
977
978        style.flex_grow = 20.0;
979        style.opacity = Some(1.0);
980        assert!(transitions.apply(&mut style, &mut state, context, overshoot_at, false));
981        assert_eq!(style.flex_grow, overshoot);
982
983        style.flex_grow = 20.0;
984        style.opacity = Some(1.0);
985        assert!(!transitions.apply(
986            &mut style,
987            &mut state,
988            context,
989            overshoot_at + Duration::from_secs(5),
990            false,
991        ));
992        assert_eq!(style.flex_grow, 20.0);
993        assert_eq!(style.opacity, Some(1.0));
994    }
995
996    #[test]
997    fn style_transitions_follow_a_complete_lifecycle() {
998        let started_at = Instant::now();
999        let duration = Duration::from_secs(1);
1000        let transitions = StyleTransitions::new()
1001            .flex_grow(duration)
1002            .opacity(duration);
1003        let context = StyleTransitionContext::new(None, px(16.0));
1004        let mut state = StyleTransitionState::default();
1005        let mut style = Style::default();
1006
1007        assert!(!transitions.apply(&mut style, &mut state, context, started_at, false));
1008        assert_eq!((style.flex_grow, style.opacity), (0.0, None));
1009
1010        style.flex_grow = 10.0;
1011        style.opacity = Some(1.0);
1012        assert!(transitions.apply(&mut style, &mut state, context, started_at, false));
1013        assert_eq!((style.flex_grow, style.opacity), (0.0, Some(0.0)));
1014
1015        style.flex_grow = 10.0;
1016        style.opacity = Some(1.0);
1017        assert!(transitions.apply(
1018            &mut style,
1019            &mut state,
1020            context,
1021            started_at + duration / 2,
1022            false,
1023        ));
1024        assert_eq!((style.flex_grow, style.opacity), (5.0, Some(0.5)));
1025
1026        style.flex_grow = 20.0;
1027        style.opacity = None;
1028        assert!(transitions.apply(
1029            &mut style,
1030            &mut state,
1031            context,
1032            started_at + duration / 2,
1033            false,
1034        ));
1035        assert_eq!((style.flex_grow, style.opacity), (5.0, Some(0.5)));
1036
1037        style.flex_grow = 20.0;
1038        style.opacity = None;
1039        assert!(transitions.apply(
1040            &mut style,
1041            &mut state,
1042            context,
1043            started_at + duration,
1044            false,
1045        ));
1046        assert_eq!((style.flex_grow, style.opacity), (12.5, Some(0.25)));
1047
1048        style.flex_grow = 20.0;
1049        style.opacity = None;
1050        assert!(!transitions.apply(
1051            &mut style,
1052            &mut state,
1053            context,
1054            started_at + duration + duration / 2,
1055            false,
1056        ));
1057        assert_eq!((style.flex_grow, style.opacity), (20.0, None));
1058
1059        style.flex_grow = 30.0;
1060        style.opacity = Some(1.0);
1061        assert!(!transitions.apply(
1062            &mut style,
1063            &mut state,
1064            context,
1065            started_at + duration + duration / 2,
1066            true,
1067        ));
1068        assert_eq!((style.flex_grow, style.opacity), (30.0, Some(1.0)));
1069    }
1070
1071    #[test]
1072    fn solid_ring_color_transitions_survive_background_storage() {
1073        let started_at = Instant::now();
1074        let duration = Duration::from_secs(1);
1075        let transitions = StyleTransitions::new().ring_color(duration);
1076        let context = StyleTransitionContext::new(None, px(16.0));
1077        let mut state = StyleTransitionState::default();
1078        let mut style = Style::default();
1079        style.ring.color = RingColor::Color(red().into());
1080
1081        assert!(!transitions.apply(&mut style, &mut state, context, started_at, false));
1082
1083        style.ring.color = RingColor::Color(blue().into());
1084        assert!(transitions.apply(&mut style, &mut state, context, started_at, false));
1085
1086        style.ring.color = RingColor::Color(blue().into());
1087        assert!(transitions.apply(
1088            &mut style,
1089            &mut state,
1090            context,
1091            started_at + duration / 2,
1092            false,
1093        ));
1094        assert_eq!(
1095            style.ring.color,
1096            RingColor::Color(red().lerp(&blue(), 0.5).into())
1097        );
1098    }
1099
1100    #[test]
1101    fn inset_edge_transitions_start_from_resolved_layout_position() {
1102        #[derive(Clone, Copy)]
1103        enum Edge {
1104            Top,
1105            Right,
1106            Bottom,
1107            Left,
1108        }
1109
1110        impl Edge {
1111            fn transitions(self, duration: Duration) -> StyleTransitions {
1112                match self {
1113                    Self::Top => StyleTransitions::new().top(duration),
1114                    Self::Right => StyleTransitions::new().right(duration),
1115                    Self::Bottom => StyleTransitions::new().bottom(duration),
1116                    Self::Left => StyleTransitions::new().left(duration),
1117                }
1118            }
1119
1120            fn set(self, style: &mut Style, value: Length) {
1121                match self {
1122                    Self::Top => style.inset.top = value,
1123                    Self::Right => style.inset.right = value,
1124                    Self::Bottom => style.inset.bottom = value,
1125                    Self::Left => style.inset.left = value,
1126                }
1127            }
1128
1129            fn get(self, style: &Style) -> Length {
1130                match self {
1131                    Self::Top => style.inset.top,
1132                    Self::Right => style.inset.right,
1133                    Self::Bottom => style.inset.bottom,
1134                    Self::Left => style.inset.left,
1135                }
1136            }
1137        }
1138
1139        let started_at = Instant::now();
1140        let duration = Duration::from_secs(1);
1141        let layout_context = StyleTransitionContext::new(None, px(16.0));
1142        let prepaint_context = StyleTransitionContext::new(
1143            Some(Bounds {
1144                origin: point(px(20.0), px(15.0)),
1145                size: size(px(30.0), px(20.0)),
1146            }),
1147            px(16.0),
1148        )
1149        .with_containing_bounds(Some(Bounds {
1150            origin: point(px(0.0), px(0.0)),
1151            size: size(px(100.0), px(80.0)),
1152        }));
1153
1154        for (edge, resolved_auto, target, midpoint) in [
1155            (Edge::Top, 15.0, length(5.0), 10.0),
1156            (
1157                Edge::Right,
1158                50.0,
1159                Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Rems(rems(1.0)))),
1160                33.0,
1161            ),
1162            (Edge::Bottom, 45.0, relative(0.25).into(), 32.5),
1163            (Edge::Left, 20.0, length(10.0), 15.0),
1164        ] {
1165            let transitions = edge.transitions(duration);
1166            let mut state = StyleTransitionState::default();
1167            let mut style = Style::default();
1168
1169            assert!(!transitions.apply(&mut style, &mut state, layout_context, started_at, false,));
1170            assert!(!transitions.apply(
1171                &mut style,
1172                &mut state,
1173                prepaint_context,
1174                started_at,
1175                false,
1176            ));
1177
1178            edge.set(&mut style, target);
1179            assert!(transitions.apply(&mut style, &mut state, layout_context, started_at, false,));
1180            assert_eq!(edge.get(&style), length(resolved_auto));
1181
1182            edge.set(&mut style, target);
1183            assert!(transitions.apply(
1184                &mut style,
1185                &mut state,
1186                layout_context,
1187                started_at + duration / 2,
1188                false,
1189            ));
1190            assert_eq!(edge.get(&style), length(midpoint));
1191
1192            edge.set(&mut style, target);
1193            assert!(!transitions.apply(
1194                &mut style,
1195                &mut state,
1196                layout_context,
1197                started_at + duration,
1198                false,
1199            ));
1200            assert_eq!(edge.get(&style), target);
1201        }
1202    }
1203
1204    #[test]
1205    fn auto_size_transitions_use_stable_prepaint_bounds() {
1206        let started_at = Instant::now();
1207        let duration = Duration::from_secs(1);
1208        let transitions = StyleTransitions::new().w(duration);
1209        let layout_context = StyleTransitionContext::new(None, px(16.0));
1210        let prepaint_context = |width| {
1211            StyleTransitionContext::new(
1212                Some(Bounds {
1213                    origin: point(px(0.0), px(0.0)),
1214                    size: size(px(width), px(40.0)),
1215                }),
1216                px(16.0),
1217            )
1218        };
1219        let mut state = StyleTransitionState::default();
1220        let mut style = Style::default();
1221
1222        assert!(!transitions.apply(&mut style, &mut state, layout_context, started_at, false,));
1223        assert!(!transitions.apply(
1224            &mut style,
1225            &mut state,
1226            prepaint_context(120.0),
1227            started_at,
1228            false,
1229        ));
1230
1231        style.size.width = length(220.0);
1232        assert!(transitions.apply(&mut style, &mut state, layout_context, started_at, false,));
1233        assert_eq!(style.size.width, length(120.0));
1234
1235        style.size.width = length(220.0);
1236        assert!(transitions.apply(
1237            &mut style,
1238            &mut state,
1239            layout_context,
1240            started_at + duration / 2,
1241            false,
1242        ));
1243        assert_eq!(style.size.width, length(170.0));
1244
1245        style.size.width = length(220.0);
1246        assert!(!transitions.apply(
1247            &mut style,
1248            &mut state,
1249            layout_context,
1250            started_at + duration,
1251            false,
1252        ));
1253        assert_eq!(style.size.width, length(220.0));
1254
1255        style.size.width = Length::Auto;
1256        assert!(transitions.apply(
1257            &mut style,
1258            &mut state,
1259            layout_context,
1260            started_at + duration,
1261            false,
1262        ));
1263        assert_eq!(style.size.width, length(220.0));
1264
1265        style.size.width = Length::Auto;
1266        assert!(!transitions.apply(
1267            &mut style,
1268            &mut state,
1269            layout_context,
1270            started_at + duration * 2,
1271            false,
1272        ));
1273        assert_eq!(style.size.width, Length::Auto);
1274
1275        assert!(!transitions.apply(
1276            &mut style,
1277            &mut state,
1278            prepaint_context(140.0),
1279            started_at + duration * 2,
1280            false,
1281        ));
1282
1283        style.size.width = length(240.0);
1284        assert!(transitions.apply(
1285            &mut style,
1286            &mut state,
1287            layout_context,
1288            started_at + duration * 2,
1289            false,
1290        ));
1291        assert_eq!(style.size.width, length(140.0));
1292    }
1293
1294    #[test]
1295    fn grouped_transition_builders_interpolate_selected_properties_and_respect_order() {
1296        let (in_progress, style) = size_transition_after(
1297            StyleTransitions::new().w(Duration::from_secs(1)),
1298            Duration::from_millis(500),
1299        );
1300        assert!(in_progress);
1301        assert_eq!(style.size, size(length(15.0), length(20.0)));
1302
1303        let (in_progress, style) = size_transition_after(
1304            StyleTransitions::new()
1305                .size(Duration::from_secs(1))
1306                .w(Duration::from_secs(2)),
1307            Duration::from_secs(1),
1308        );
1309        assert!(in_progress);
1310        assert_eq!(style.size, size(length(15.0), length(20.0)));
1311
1312        let (in_progress, style) = size_transition_after(
1313            StyleTransitions::new()
1314                .w(Duration::from_secs(2))
1315                .size(Duration::from_secs(1)),
1316            Duration::from_secs(1),
1317        );
1318        assert!(!in_progress);
1319        assert_eq!(style.size, size(length(20.0), length(20.0)));
1320
1321        let started_at = Instant::now();
1322        let mut state = StyleTransitionState::default();
1323        let transitions = StyleTransitions::new()
1324            .opacity(Duration::from_secs(1))
1325            .rounded(Duration::from_secs(1))
1326            .rounded_smoothing(Duration::from_secs(1))
1327            .p(Duration::from_secs(1))
1328            .border(Duration::from_secs(1))
1329            .gap(Duration::from_secs(1))
1330            .text_size(Duration::from_secs(1));
1331        let context = StyleTransitionContext::new(
1332            Some(Bounds {
1333                origin: point(px(0.0), px(0.0)),
1334                size: size(px(100.0), px(60.0)),
1335            }),
1336            px(16.0),
1337        );
1338        let mut style = Style {
1339            corner_radii: corners(30.0),
1340            corner_smoothing: Some(0.0),
1341            ..Style::default()
1342        };
1343
1344        assert!(!transitions.apply(&mut style, &mut state, context, started_at, false,));
1345        assert_eq!(style.opacity, None);
1346        assert_eq!(style.corner_radii, corners(30.0));
1347        assert_eq!(style.corner_smoothing, Some(0.0));
1348
1349        style.opacity = Some(0.5);
1350        style.corner_radii = corners(0.0);
1351        style.corner_smoothing = Some(1.0);
1352        style.padding = Edges::all(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(20.0))));
1353        style.border_widths = Edges::all(AbsoluteLength::Pixels(px(10.0)));
1354        style.gap = size(
1355            DefiniteLength::Absolute(AbsoluteLength::Pixels(px(30.0))),
1356            DefiniteLength::Absolute(AbsoluteLength::Pixels(px(30.0))),
1357        );
1358        style.text.font_size = Some(AbsoluteLength::Pixels(px(24.0)));
1359        assert!(transitions.apply(&mut style, &mut state, context, started_at, false,));
1360
1361        style.opacity = Some(0.5);
1362        style.corner_radii = corners(0.0);
1363        style.corner_smoothing = Some(1.0);
1364        style.padding = Edges::all(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(20.0))));
1365        style.border_widths = Edges::all(AbsoluteLength::Pixels(px(10.0)));
1366        style.gap = size(
1367            DefiniteLength::Absolute(AbsoluteLength::Pixels(px(30.0))),
1368            DefiniteLength::Absolute(AbsoluteLength::Pixels(px(30.0))),
1369        );
1370        style.text.font_size = Some(AbsoluteLength::Pixels(px(24.0)));
1371        assert!(transitions.apply(
1372            &mut style,
1373            &mut state,
1374            context,
1375            started_at + Duration::from_millis(500),
1376            false,
1377        ));
1378        assert_eq!(style.opacity, Some(0.25));
1379        assert_eq!(style.corner_radii, corners(15.0));
1380        assert_eq!(style.corner_smoothing, Some(0.5));
1381        assert_eq!(
1382            style.padding,
1383            Edges::all(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(10.0))))
1384        );
1385        assert_eq!(
1386            style.border_widths,
1387            Edges::all(AbsoluteLength::Pixels(px(5.0)))
1388        );
1389        assert_eq!(
1390            style.gap,
1391            size(
1392                DefiniteLength::Absolute(AbsoluteLength::Pixels(px(15.0))),
1393                DefiniteLength::Absolute(AbsoluteLength::Pixels(px(15.0))),
1394            )
1395        );
1396        assert_eq!(style.text.font_size, Some(AbsoluteLength::Pixels(px(12.0))));
1397    }
1398
1399    struct InsetTransitionTestView {
1400        enabled: bool,
1401        presented_bounds: Rc<Cell<Bounds<Pixels>>>,
1402    }
1403
1404    impl Render for InsetTransitionTestView {
1405        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1406            let presented_bounds = self.presented_bounds.clone();
1407
1408            div().size_full().child(
1409                div()
1410                    .id("track")
1411                    .w(px(100.0))
1412                    .h(px(40.0))
1413                    .p(px(10.0))
1414                    .child(
1415                        div()
1416                            .id("knob")
1417                            .absolute()
1418                            .w(px(20.0))
1419                            .h(px(20.0))
1420                            .when(self.enabled, |knob| knob.right(px(10.0)))
1421                            .transitions(|transitions| transitions.right(Duration::from_secs(1)))
1422                            .child(canvas(
1423                                move |bounds, _, _| presented_bounds.set(bounds),
1424                                |_, _, _, _| {},
1425                            )),
1426                    ),
1427            )
1428        }
1429    }
1430
1431    #[gpui::test]
1432    fn auto_to_right_inset_moves_from_the_rendered_position_without_jumping(
1433        cx: &mut TestAppContext,
1434    ) {
1435        let presented_bounds = Rc::new(Cell::new(Bounds::default()));
1436        let window = cx.add_window({
1437            let presented_bounds = presented_bounds.clone();
1438            move |_, _| InsetTransitionTestView {
1439                enabled: false,
1440                presented_bounds,
1441            }
1442        });
1443        let any_window = AnyWindowHandle::from(window);
1444
1445        let draw = |cx: &mut TestAppContext| {
1446            cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx))
1447                .expect("failed to draw inset transition test window");
1448        };
1449        let assert_x_near = |expected: f32| {
1450            let actual = presented_bounds.get().origin.x.0;
1451            assert!(
1452                (actual - expected).abs() < 0.01,
1453                "expected knob x near {expected}, got {actual}",
1454            );
1455        };
1456
1457        draw(cx);
1458        assert_x_near(10.0);
1459
1460        window
1461            .update(cx, |view, _, cx| {
1462                view.enabled = true;
1463                cx.notify();
1464            })
1465            .expect("failed to enable inset transition test view");
1466        draw(cx);
1467        assert_x_near(10.0);
1468
1469        cx.executor().advance_clock(Duration::from_millis(500));
1470        draw(cx);
1471        assert_x_near(40.0);
1472
1473        cx.executor().advance_clock(Duration::from_millis(500));
1474        draw(cx);
1475        assert_x_near(70.0);
1476
1477        window
1478            .update(cx, |view, _, cx| {
1479                view.enabled = false;
1480                cx.notify();
1481            })
1482            .expect("failed to disable inset transition test view");
1483        draw(cx);
1484        assert_x_near(70.0);
1485
1486        cx.executor().advance_clock(Duration::from_millis(500));
1487        draw(cx);
1488        assert_x_near(40.0);
1489
1490        cx.executor().advance_clock(Duration::from_millis(500));
1491        draw(cx);
1492        assert_x_near(10.0);
1493    }
1494
1495    struct StyleTransitionTestView {
1496        transitions_enabled: bool,
1497        base_width: Pixels,
1498        presented_width: Rc<Cell<Pixels>>,
1499    }
1500
1501    impl Render for StyleTransitionTestView {
1502        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1503            let presented_width = self.presented_width.clone();
1504
1505            div().size_full().child(
1506                div()
1507                    .id("transition-target")
1508                    .h(px(50.0))
1509                    .w(self.base_width)
1510                    .when(self.transitions_enabled, |element| {
1511                        element.transitions(|transitions| transitions.w(Duration::from_millis(200)))
1512                    })
1513                    .hover(|style| style.w(px(200.0)))
1514                    .active(|style| style.w(px(50.0)))
1515                    .child(
1516                        canvas(
1517                            move |bounds, _, _| presented_width.set(bounds.size.width),
1518                            |_, _, _, _| {},
1519                        )
1520                        .size_full(),
1521                    ),
1522            )
1523        }
1524    }
1525
1526    #[gpui::test]
1527    fn style_transitions_follow_interaction_and_reset_persistent_state(cx: &mut TestAppContext) {
1528        let presented_width = Rc::new(Cell::new(px(0.0)));
1529        let window = cx.add_window({
1530            let presented_width = presented_width.clone();
1531            move |_, _| StyleTransitionTestView {
1532                transitions_enabled: true,
1533                base_width: px(100.0),
1534                presented_width,
1535            }
1536        });
1537        let any_window = AnyWindowHandle::from(window);
1538        let mouse_position = point(px(10.0), px(10.0));
1539
1540        let draw = |cx: &mut TestAppContext| {
1541            if let Err(error) =
1542                cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx))
1543            {
1544                panic!("failed to draw transition test window: {error:#}");
1545            }
1546        };
1547        let assert_width_near = |expected: f32| {
1548            assert!(
1549                (presented_width.get().0 - expected).abs() < 0.01,
1550                "expected width near {expected}, got {}",
1551                presented_width.get().0,
1552            );
1553        };
1554
1555        draw(cx);
1556        assert_width_near(100.0);
1557
1558        if let Err(error) = cx.update_window(any_window, |_, window, cx| {
1559            window.simulate_mouse_move(mouse_position, cx);
1560            window.draw(cx).clear(cx);
1561        }) {
1562            panic!("failed to move the mouse into the transition target: {error:#}");
1563        }
1564        assert_width_near(100.0);
1565
1566        cx.executor().advance_clock(Duration::from_millis(100));
1567        draw(cx);
1568        assert_width_near(150.0);
1569
1570        if let Err(error) = cx.update_window(any_window, |_, window, cx| {
1571            window.dispatch_event(
1572                MouseDownEvent {
1573                    position: mouse_position,
1574                    button: MouseButton::Left,
1575                    modifiers: Default::default(),
1576                    click_count: 1,
1577                    first_mouse: false,
1578                }
1579                .to_platform_input(),
1580                cx,
1581            );
1582            window.draw(cx).clear(cx);
1583        }) {
1584            panic!("failed to press the transition target: {error:#}");
1585        }
1586        assert_width_near(150.0);
1587
1588        cx.executor().advance_clock(Duration::from_millis(100));
1589        draw(cx);
1590        assert_width_near(100.0);
1591        cx.executor().advance_clock(Duration::from_millis(100));
1592        draw(cx);
1593        assert_width_near(50.0);
1594
1595        if let Err(error) = cx.update_window(any_window, |_, window, cx| {
1596            window.dispatch_event(
1597                MouseUpEvent {
1598                    position: mouse_position,
1599                    button: MouseButton::Left,
1600                    modifiers: Default::default(),
1601                    click_count: 1,
1602                }
1603                .to_platform_input(),
1604                cx,
1605            );
1606            window.simulate_mouse_move(point(px(300.0), px(300.0)), cx);
1607        }) {
1608            panic!("failed to release the transition target: {error:#}");
1609        }
1610        if let Err(error) = window.update(cx, |view, _, cx| {
1611            view.transitions_enabled = false;
1612            view.base_width = px(240.0);
1613            cx.notify();
1614        }) {
1615            panic!("failed to disable transitions: {error:#}");
1616        }
1617        draw(cx);
1618        assert_width_near(240.0);
1619
1620        if let Err(error) = window.update(cx, |view, _, cx| {
1621            view.transitions_enabled = true;
1622            cx.notify();
1623        }) {
1624            panic!("failed to re-enable transitions: {error:#}");
1625        }
1626        draw(cx);
1627        assert_width_near(240.0);
1628    }
1629
1630    struct HoverTransitionFocusTestView {
1631        target_focus: FocusHandle,
1632        next_focus: FocusHandle,
1633        presented_width: Rc<Cell<Pixels>>,
1634    }
1635
1636    impl Render for HoverTransitionFocusTestView {
1637        fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1638            let presented_width = self.presented_width.clone();
1639            let hover_width = if self.target_focus.is_focused(window) {
1640                px(200.0)
1641            } else {
1642                px(150.0)
1643            };
1644
1645            div()
1646                .size_full()
1647                .child(
1648                    div()
1649                        .id("transition-target")
1650                        .w(px(100.0))
1651                        .h(px(50.0))
1652                        .track_focus(&self.target_focus)
1653                        .hover(move |style| style.w(hover_width))
1654                        .transitions(|transitions| {
1655                            transitions.w(Duration::from_millis(120).with_easing(ease_in_out))
1656                        })
1657                        .on_key_down(|event, window, cx| {
1658                            if event.keystroke.key == "tab" {
1659                                window.focus_next(cx);
1660                            }
1661                        })
1662                        .child(
1663                            canvas(
1664                                move |bounds, _, _| presented_width.set(bounds.size.width),
1665                                |_, _, _, _| {},
1666                            )
1667                            .size_full(),
1668                        ),
1669                )
1670                .child(div().w(px(50.0)).h(px(50.0)).track_focus(&self.next_focus))
1671        }
1672    }
1673
1674    #[gpui::test]
1675    fn hover_transitions_follow_input_modality_across_focus_changes(cx: &mut TestAppContext) {
1676        let target_focus = cx.update(|cx| cx.focus_handle().tab_stop(true));
1677        let next_focus = cx.update(|cx| cx.focus_handle().tab_stop(true));
1678        let presented_width = Rc::new(Cell::new(px(0.0)));
1679        let window = cx.add_window({
1680            let target_focus = target_focus.clone();
1681            let next_focus = next_focus.clone();
1682            let presented_width = presented_width.clone();
1683            move |_, _| HoverTransitionFocusTestView {
1684                target_focus,
1685                next_focus,
1686                presented_width,
1687            }
1688        });
1689        let any_window = AnyWindowHandle::from(window);
1690
1691        let simulate_next_frame = |cx: &mut TestAppContext| {
1692            let callback_count = cx
1693                .update_window(any_window, |_, window, cx| window.simulate_next_frame(cx))
1694                .expect("failed to deliver the next transition frame");
1695            cx.run_until_parked();
1696            callback_count
1697        };
1698        let assert_width_near = |expected: f32| {
1699            assert!(
1700                (presented_width.get().0 - expected).abs() < 0.01,
1701                "expected width near {expected}, got {}",
1702                presented_width.get().0,
1703            );
1704        };
1705
1706        cx.update_window(any_window, |_, window, cx| {
1707            window.draw(cx).clear(cx);
1708            window.focus(&target_focus, cx);
1709            window.simulate_mouse_move(point(px(10.0), px(10.0)), cx);
1710        })
1711        .expect("failed to focus and hover the transition target");
1712        cx.run_until_parked();
1713
1714        cx.executor().advance_clock(Duration::from_millis(120));
1715        assert!(simulate_next_frame(cx) > 0);
1716        assert_width_near(200.0);
1717
1718        cx.update_window(any_window, |_, window, cx| {
1719            window.focus(&next_focus, cx);
1720        })
1721        .expect("failed to move focus while retaining mouse modality");
1722        cx.run_until_parked();
1723
1724        cx.executor().advance_clock(Duration::from_millis(120));
1725        assert!(simulate_next_frame(cx) > 0);
1726        assert_width_near(150.0);
1727
1728        cx.update_window(any_window, |_, window, cx| {
1729            window.focus(&target_focus, cx);
1730        })
1731        .expect("failed to restore focus while retaining mouse modality");
1732        cx.run_until_parked();
1733        cx.executor().advance_clock(Duration::from_millis(120));
1734        assert!(simulate_next_frame(cx) > 0);
1735        assert_width_near(200.0);
1736
1737        cx.simulate_keystrokes(any_window, "tab");
1738        assert!(
1739            cx.update_window(any_window, |_, window, _| next_focus.is_focused(window))
1740                .expect("transition test window should remain open")
1741        );
1742
1743        cx.executor().advance_clock(Duration::from_millis(120));
1744        assert!(simulate_next_frame(cx) > 0);
1745        assert_width_near(100.0);
1746        assert_eq!(
1747            simulate_next_frame(cx),
1748            0,
1749            "the completed hover transition must stop requesting frames"
1750        );
1751    }
1752}