agent: Polish `Generating...` animation (#28379)

Bennet Bo Fenner created

https://github.com/user-attachments/assets/9e798a50-9403-4e1c-a3df-2931e748b77d



Release Notes:

- N/A

Change summary

crates/agent/src/active_thread.rs     | 33 +++++++++++++------
crates/gpui/src/elements/animation.rs | 48 ++++++++++++++++++++++------
2 files changed, 61 insertions(+), 20 deletions(-)

Detailed changes

crates/agent/src/active_thread.rs 🔗

@@ -1222,17 +1222,30 @@ impl ActiveThread {
             Label::new("Generating")
                 .color(Color::Muted)
                 .size(LabelSize::Small)
-                .with_animation(
+                .with_animations(
                     "generating-label",
-                    Animation::new(Duration::from_secs(1)).repeat(),
-                    |mut label, delta| {
-                        let text = match delta {
-                            d if d < 0.25 => "Generating",
-                            d if d < 0.5 => "Generating.",
-                            d if d < 0.75 => "Generating..",
-                            _ => "Generating...",
-                        };
-                        label.set_text(text);
+                    vec![
+                        Animation::new(Duration::from_secs(1)),
+                        Animation::new(Duration::from_secs(1)).repeat(),
+                    ],
+                    |mut label, animation_ix, delta| {
+                        match animation_ix {
+                            0 => {
+                                let chars_to_show = (delta * 10.).ceil() as usize;
+                                let text = &"Generating"[0..chars_to_show];
+                                label.set_text(text);
+                            }
+                            1 => {
+                                let text = match delta {
+                                    d if d < 0.25 => "Generating",
+                                    d if d < 0.5 => "Generating.",
+                                    d if d < 0.75 => "Generating..",
+                                    _ => "Generating...",
+                                };
+                                label.set_text(text);
+                            }
+                            _ => {}
+                        }
                         label
                     },
                 )

crates/gpui/src/elements/animation.rs 🔗

@@ -3,6 +3,7 @@ use std::time::{Duration, Instant};
 use crate::{AnyElement, App, Element, ElementId, GlobalElementId, IntoElement, Window};
 
 pub use easing::*;
+use smallvec::SmallVec;
 
 /// An animation that can be applied to an element.
 pub struct Animation {
@@ -50,6 +51,24 @@ pub trait AnimationExt {
         animation: Animation,
         animator: impl Fn(Self, f32) -> Self + 'static,
     ) -> AnimationElement<Self>
+    where
+        Self: Sized,
+    {
+        AnimationElement {
+            id: id.into(),
+            element: Some(self),
+            animator: Box::new(move |this, _, value| animator(this, value)),
+            animations: smallvec::smallvec![animation],
+        }
+    }
+
+    /// Render this component or element with a chain of animations
+    fn with_animations(
+        self,
+        id: impl Into<ElementId>,
+        animations: Vec<Animation>,
+        animator: impl Fn(Self, usize, f32) -> Self + 'static,
+    ) -> AnimationElement<Self>
     where
         Self: Sized,
     {
@@ -57,7 +76,7 @@ pub trait AnimationExt {
             id: id.into(),
             element: Some(self),
             animator: Box::new(animator),
-            animation,
+            animations: animations.into(),
         }
     }
 }
@@ -68,8 +87,8 @@ impl<E> AnimationExt for E {}
 pub struct AnimationElement<E> {
     id: ElementId,
     element: Option<E>,
-    animation: Animation,
-    animator: Box<dyn Fn(E, f32) -> E + 'static>,
+    animations: SmallVec<[Animation; 1]>,
+    animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
 }
 
 impl<E> AnimationElement<E> {
@@ -91,6 +110,7 @@ impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
 
 struct AnimationState {
     start: Instant,
+    animation_ix: usize,
 }
 
 impl<E: IntoElement + 'static> Element for AnimationElement<E> {
@@ -108,22 +128,30 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
         cx: &mut App,
     ) -> (crate::LayoutId, Self::RequestLayoutState) {
         window.with_element_state(global_id.unwrap(), |state, window| {
-            let state = state.unwrap_or_else(|| AnimationState {
+            let mut state = state.unwrap_or_else(|| AnimationState {
                 start: Instant::now(),
+                animation_ix: 0,
             });
-            let mut delta =
-                state.start.elapsed().as_secs_f32() / self.animation.duration.as_secs_f32();
+            let animation_ix = state.animation_ix;
+
+            let mut delta = state.start.elapsed().as_secs_f32()
+                / self.animations[animation_ix].duration.as_secs_f32();
 
             let mut done = false;
             if delta > 1.0 {
-                if self.animation.oneshot {
-                    done = true;
+                if self.animations[animation_ix].oneshot {
+                    if animation_ix >= self.animations.len() - 1 {
+                        done = true;
+                    } else {
+                        state.start = Instant::now();
+                        state.animation_ix += 1;
+                    }
                     delta = 1.0;
                 } else {
                     delta %= 1.0;
                 }
             }
-            let delta = (self.animation.easing)(delta);
+            let delta = (self.animations[animation_ix].easing)(delta);
 
             debug_assert!(
                 (0.0..=1.0).contains(&delta),
@@ -131,7 +159,7 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
             );
 
             let element = self.element.take().expect("should only be called once");
-            let mut element = (self.animator)(element, delta).into_any_element();
+            let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
 
             if !done {
                 window.request_animation_frame();