diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 03380a60a6..ce40a93839 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -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, @@ -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", diff --git a/crates/gpui/src/elements/animation.rs b/crates/gpui/src/elements/animation.rs index 29506a6224..def3d8c53a 100644 --- a/crates/gpui/src/elements/animation.rs +++ b/crates/gpui/src/elements/animation.rs @@ -170,6 +170,8 @@ impl Element for AnimationElement { } mod easing { + use std::f32::consts::PI; + /// The linear easing function, or delta itself pub fn linear(delta: f32) -> f32 { delta @@ -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) + } + } } diff --git a/crates/ui/src/components/label/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs index a050215e58..7120dc7d48 100644 --- a/crates/ui/src/components/label/highlighted_label.rs +++ b/crates/ui/src/components/label/highlighted_label.rs @@ -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( diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs index 5c61dcbc83..f29e4656e9 100644 --- a/crates/ui/src/components/label/label.rs +++ b/crates/ui/src/components/label/label.rs @@ -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 { diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index 4b830e656f..b76fe84588 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -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)] @@ -53,6 +56,7 @@ pub struct LabelLike { strikethrough: bool, italic: bool, children: SmallVec<[AnyElement; 2]>, + alpha: Option, } impl LabelLike { @@ -66,6 +70,7 @@ impl LabelLike { strikethrough: false, italic: false, children: SmallVec::new(), + alpha: None, } } } @@ -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 { @@ -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( @@ -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) } diff --git a/crates/ui/src/components/stories/label.rs b/crates/ui/src/components/stories/label.rs index 967512d898..f4b30fb36e 100644 --- a/crates/ui/src/components/stories/label.rs +++ b/crates/ui/src/components/stories/label.rs @@ -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; @@ -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), + ), + ) } }