Skip to content

Commit

Permalink
assistant panel: Animate assistant label if message is pending (zed-i…
Browse files Browse the repository at this point in the history
…ndustries#16152)

This adds a pulsating effect to the assistant header in case the message
is pending.

The pulsating effect is capped between 0.2 and 1.0 and I tried (with the
help of Claude) to give it a "breathing" effect, since I found the
normal bounce a bit too much.

Also opted for setting the `alpha` on the `LabelLike` things, vs.
overwriting the color, since I think that's cleaner instead of exposing
the color and mutating that.


https://github.com/user-attachments/assets/4a94a1c5-8dc7-4c40-b30f-d92d112db7b5


Release Notes:

- N/A
  • Loading branch information
mrnugget authored Aug 13, 2024
1 parent b36d138 commit af36d49
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 10 deletions.
41 changes: 33 additions & 8 deletions crates/assistant/src/assistant_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ use editor::{
use editor::{display_map::CreaseId, FoldPlaceholder};
use fs::Fs;
use gpui::{
canvas, div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView,
AppContext, AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity,
EntityId, EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement,
canvas, div, percentage, point, pulsating_between, Action, Animation, AnimationExt, AnyElement,
AnyView, AppContext, AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty,
Entity, EntityId, EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement,
IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render, SharedString,
StatefulInteractiveElement, Styled, Subscription, Task, Transformation, UpdateGlobal, View,
ViewContext, VisualContext, WeakView, WindowContext,
Expand Down Expand Up @@ -2953,13 +2953,38 @@ impl ContextEditor {
let context = self.context.clone();
move |cx| {
let message_id = message.id;
let show_spinner = message.role == Role::Assistant
&& message.status == MessageStatus::Pending;

let label = match message.role {
Role::User => {
Label::new("You").color(Color::Default).into_any_element()
}
Role::Assistant => {
let label = Label::new("Assistant").color(Color::Info);
if show_spinner {
label
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.2, 1.0)),
|label, delta| label.alpha(delta),
)
.into_any_element()
} else {
label.into_any_element()
}
}

Role::System => Label::new("System")
.color(Color::Warning)
.into_any_element(),
};

let sender = ButtonLike::new("role")
.style(ButtonStyle::Filled)
.child(match message.role {
Role::User => Label::new("You").color(Color::Default),
Role::Assistant => Label::new("Assistant").color(Color::Info),
Role::System => Label::new("System").color(Color::Warning),
})
.child(label)
.tooltip(|cx| {
Tooltip::with_meta(
"Toggle message role",
Expand Down
18 changes: 18 additions & 0 deletions crates/gpui/src/elements/animation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
}

mod easing {
use std::f32::consts::PI;

/// The linear easing function, or delta itself
pub fn linear(delta: f32) -> f32 {
delta
Expand Down Expand Up @@ -200,4 +202,20 @@ mod easing {
}
}
}

/// A custom easing function for pulsating alpha that slows down as it approaches 0.1
pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
let range = max - min;

move |delta| {
// Use a combination of sine and cubic functions for a more natural breathing rhythm
let t = (delta * 2.0 * PI).sin();
let breath = (t * t * t + t) / 2.0;

// Map the breath to our desired alpha range
let normalized_alpha = (breath + 1.0) / 2.0;

min + (normalized_alpha * range)
}
}
}
5 changes: 5 additions & 0 deletions crates/ui/src/components/label/highlighted_label.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ impl LabelCommon for HighlightedLabel {
self.base = self.base.italic(italic);
self
}

fn alpha(mut self, alpha: f32) -> Self {
self.base = self.base.alpha(alpha);
self
}
}

pub fn highlight_ranges(
Expand Down
14 changes: 14 additions & 0 deletions crates/ui/src/components/label/label.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,20 @@ impl LabelCommon for Label {
self.base = self.base.italic(italic);
self
}

/// Sets the alpha property of the color of label.
///
/// # Examples
///
/// ```
/// use ui::prelude::*;
///
/// let my_label = Label::new("Hello, World!").alpha(0.5);
/// ```
fn alpha(mut self, alpha: f32) -> Self {
self.base = self.base.alpha(alpha);
self
}
}

impl RenderOnce for Label {
Expand Down
17 changes: 16 additions & 1 deletion crates/ui/src/components/label/label_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub trait LabelCommon {

/// Sets the italic property of the label.
fn italic(self, italic: bool) -> Self;

/// Sets the alpha property of the label, overwriting the alpha value of the color.
fn alpha(self, alpha: f32) -> Self;
}

#[derive(IntoElement)]
Expand All @@ -53,6 +56,7 @@ pub struct LabelLike {
strikethrough: bool,
italic: bool,
children: SmallVec<[AnyElement; 2]>,
alpha: Option<f32>,
}

impl LabelLike {
Expand All @@ -66,6 +70,7 @@ impl LabelLike {
strikethrough: false,
italic: false,
children: SmallVec::new(),
alpha: None,
}
}
}
Expand Down Expand Up @@ -111,6 +116,11 @@ impl LabelCommon for LabelLike {
self.italic = italic;
self
}

fn alpha(mut self, alpha: f32) -> Self {
self.alpha = Some(alpha);
self
}
}

impl ParentElement for LabelLike {
Expand All @@ -123,6 +133,11 @@ impl RenderOnce for LabelLike {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);

let mut color = self.color.color(cx);
if let Some(alpha) = self.alpha {
color.fade_out(1.0 - alpha);
}

self.base
.when(self.strikethrough, |this| {
this.relative().child(
Expand All @@ -144,7 +159,7 @@ impl RenderOnce for LabelLike {
this.line_height(relative(1.))
})
.when(self.italic, |this| this.italic())
.text_color(self.color.color(cx))
.text_color(color)
.font_weight(self.weight.unwrap_or(settings.ui_font.weight))
.children(self.children)
}
Expand Down
13 changes: 12 additions & 1 deletion crates/ui/src/components/stories/label.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::time::Duration;

use crate::{prelude::*, HighlightedLabel, Label};
use gpui::Render;
use gpui::{pulsating_between, Animation, AnimationExt, Render};
use story::Story;

pub struct LabelStory;
Expand All @@ -23,5 +25,14 @@ impl Render for LabelStory {
.child(
HighlightedLabel::new("Hello, world!", vec![0, 1, 2, 7, 8, 12]).color(Color::Error),
)
.child(
Label::new("This text is pulsating").with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.2, 1.0)),
|label, delta| label.alpha(delta),
),
)
}
}

0 comments on commit af36d49

Please sign in to comment.