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 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}