assistant panel: Animate assistant label if message is pending (#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
This commit is contained in:
parent
b36d1386a9
commit
af36d4934c
6 changed files with 98 additions and 10 deletions
|
@ -34,9 +34,9 @@ use editor::{
|
||||||
use editor::{display_map::CreaseId, FoldPlaceholder};
|
use editor::{display_map::CreaseId, FoldPlaceholder};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
canvas, div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView,
|
canvas, div, percentage, point, pulsating_between, Action, Animation, AnimationExt, AnyElement,
|
||||||
AppContext, AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity,
|
AnyView, AppContext, AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty,
|
||||||
EntityId, EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement,
|
Entity, EntityId, EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement,
|
||||||
IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render, SharedString,
|
IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render, SharedString,
|
||||||
StatefulInteractiveElement, Styled, Subscription, Task, Transformation, UpdateGlobal, View,
|
StatefulInteractiveElement, Styled, Subscription, Task, Transformation, UpdateGlobal, View,
|
||||||
ViewContext, VisualContext, WeakView, WindowContext,
|
ViewContext, VisualContext, WeakView, WindowContext,
|
||||||
|
@ -2953,13 +2953,38 @@ impl ContextEditor {
|
||||||
let context = self.context.clone();
|
let context = self.context.clone();
|
||||||
move |cx| {
|
move |cx| {
|
||||||
let message_id = message.id;
|
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")
|
let sender = ButtonLike::new("role")
|
||||||
.style(ButtonStyle::Filled)
|
.style(ButtonStyle::Filled)
|
||||||
.child(match message.role {
|
.child(label)
|
||||||
Role::User => Label::new("You").color(Color::Default),
|
|
||||||
Role::Assistant => Label::new("Assistant").color(Color::Info),
|
|
||||||
Role::System => Label::new("System").color(Color::Warning),
|
|
||||||
})
|
|
||||||
.tooltip(|cx| {
|
.tooltip(|cx| {
|
||||||
Tooltip::with_meta(
|
Tooltip::with_meta(
|
||||||
"Toggle message role",
|
"Toggle message role",
|
||||||
|
|
|
@ -170,6 +170,8 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod easing {
|
mod easing {
|
||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
/// The linear easing function, or delta itself
|
/// The linear easing function, or delta itself
|
||||||
pub fn linear(delta: f32) -> f32 {
|
pub fn linear(delta: f32) -> f32 {
|
||||||
delta
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,11 @@ impl LabelCommon for HighlightedLabel {
|
||||||
self.base = self.base.italic(italic);
|
self.base = self.base.italic(italic);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn alpha(mut self, alpha: f32) -> Self {
|
||||||
|
self.base = self.base.alpha(alpha);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_ranges(
|
pub fn highlight_ranges(
|
||||||
|
|
|
@ -156,6 +156,20 @@ impl LabelCommon for Label {
|
||||||
self.base = self.base.italic(italic);
|
self.base = self.base.italic(italic);
|
||||||
self
|
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 {
|
impl RenderOnce for Label {
|
||||||
|
|
|
@ -41,6 +41,9 @@ pub trait LabelCommon {
|
||||||
|
|
||||||
/// Sets the italic property of the label.
|
/// Sets the italic property of the label.
|
||||||
fn italic(self, italic: bool) -> Self;
|
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)]
|
#[derive(IntoElement)]
|
||||||
|
@ -53,6 +56,7 @@ pub struct LabelLike {
|
||||||
strikethrough: bool,
|
strikethrough: bool,
|
||||||
italic: bool,
|
italic: bool,
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
alpha: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LabelLike {
|
impl LabelLike {
|
||||||
|
@ -66,6 +70,7 @@ impl LabelLike {
|
||||||
strikethrough: false,
|
strikethrough: false,
|
||||||
italic: false,
|
italic: false,
|
||||||
children: SmallVec::new(),
|
children: SmallVec::new(),
|
||||||
|
alpha: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,6 +116,11 @@ impl LabelCommon for LabelLike {
|
||||||
self.italic = italic;
|
self.italic = italic;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn alpha(mut self, alpha: f32) -> Self {
|
||||||
|
self.alpha = Some(alpha);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParentElement for LabelLike {
|
impl ParentElement for LabelLike {
|
||||||
|
@ -123,6 +133,11 @@ impl RenderOnce for LabelLike {
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
let settings = ThemeSettings::get_global(cx);
|
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
|
self.base
|
||||||
.when(self.strikethrough, |this| {
|
.when(self.strikethrough, |this| {
|
||||||
this.relative().child(
|
this.relative().child(
|
||||||
|
@ -144,7 +159,7 @@ impl RenderOnce for LabelLike {
|
||||||
this.line_height(relative(1.))
|
this.line_height(relative(1.))
|
||||||
})
|
})
|
||||||
.when(self.italic, |this| this.italic())
|
.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))
|
.font_weight(self.weight.unwrap_or(settings.ui_font.weight))
|
||||||
.children(self.children)
|
.children(self.children)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::{prelude::*, HighlightedLabel, Label};
|
use crate::{prelude::*, HighlightedLabel, Label};
|
||||||
use gpui::Render;
|
use gpui::{pulsating_between, Animation, AnimationExt, Render};
|
||||||
use story::Story;
|
use story::Story;
|
||||||
|
|
||||||
pub struct LabelStory;
|
pub struct LabelStory;
|
||||||
|
@ -23,5 +25,14 @@ impl Render for LabelStory {
|
||||||
.child(
|
.child(
|
||||||
HighlightedLabel::new("Hello, world!", vec![0, 1, 2, 7, 8, 12]).color(Color::Error),
|
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),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue