agent_ui: Fix issues with mention crease (#45683)

Danilo Leal created

This PR introduces the `MentionCrease` component, aimed at solving two
issues with mention creases in the agent panel:
- Previously, the mention crease was using a button with a regular size,
which is bigger than the default buffer font line height. That made the
crease look clipped and also overlapping with one another when in a
multiple line scenario where the creases would be on top of each other.
`MentionCrease` uses the window line height value to set the button
height, with a small one pixel vertical padding just for a bit of
spacing.
- Previously, given the crease used a `Label`, its font size wouldn't
scale if you changed the `agent_buffer_font_size` setting. Now,
`MentionCrease` uses that font size value, which makes regular text and
its text grow together as you'd expect.

Release Notes:

- agent: Fix a bug where mention creases didn't scale with
`agent_buffer_font_size` and got clipped/jumbled when rendered one above
the other.

Change summary

crates/agent_ui/src/mention_set.rs       |  73 +++---------------
crates/agent_ui/src/ui.rs                |   2 
crates/agent_ui/src/ui/mention_crease.rs | 100 ++++++++++++++++++++++++++
3 files changed, 117 insertions(+), 58 deletions(-)

Detailed changes

crates/agent_ui/src/mention_set.rs 🔗

@@ -12,8 +12,8 @@ use editor::{
 };
 use futures::{AsyncReadExt as _, FutureExt as _, future::Shared};
 use gpui::{
-    Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Empty, Entity, EntityId,
-    Image, ImageFormat, Img, SharedString, Task, WeakEntity, pulsating_between,
+    AppContext, ClipboardEntry, Context, Empty, Entity, EntityId, Image, ImageFormat, Img,
+    SharedString, Task, WeakEntity,
 };
 use http_client::{AsyncBody, HttpClientWithUrl};
 use itertools::Either;
@@ -32,13 +32,14 @@ use std::{
     path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
-    time::Duration,
 };
 use text::OffsetRangeExt;
-use ui::{ButtonLike, Disclosure, TintColor, Toggleable, prelude::*};
+use ui::{Disclosure, Toggleable, prelude::*};
 use util::{ResultExt, debug_panic, rel_path::RelPath};
 use workspace::{Workspace, notifications::NotifyResultExt as _};
 
+use crate::ui::MentionCrease;
+
 pub type MentionTask = Shared<Task<Result<Mention, String>>>;
 
 #[derive(Debug, Clone, Eq, PartialEq)]
@@ -754,25 +755,8 @@ fn render_fold_icon_button(
                 .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
                 .unwrap_or_default();
 
-            ButtonLike::new(fold_id)
-                .style(ButtonStyle::Filled)
-                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                .toggle_state(is_in_text_selection)
-                .child(
-                    h_flex()
-                        .gap_1()
-                        .child(
-                            Icon::from_path(icon_path.clone())
-                                .size(IconSize::XSmall)
-                                .color(Color::Muted),
-                        )
-                        .child(
-                            Label::new(label.clone())
-                                .size(LabelSize::Small)
-                                .buffer_font(cx)
-                                .single_line(),
-                        ),
-                )
+            MentionCrease::new(fold_id, icon_path.clone(), label.clone())
+                .is_toggled(is_in_text_selection)
                 .into_any_element()
         }
     })
@@ -947,12 +931,14 @@ impl Render for LoadingContext {
             .editor
             .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
             .unwrap_or_default();
-        ButtonLike::new(("loading-context", self.id))
-            .style(ButtonStyle::Filled)
-            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-            .toggle_state(is_in_text_selection)
-            .when_some(self.image.clone(), |el, image_task| {
-                el.hoverable_tooltip(move |_, cx| {
+
+        let id = ElementId::from(("loading_context", self.id));
+
+        MentionCrease::new(id, self.icon.clone(), self.label.clone())
+            .is_toggled(is_in_text_selection)
+            .is_loading(self.loading.is_some())
+            .when_some(self.image.clone(), |this, image_task| {
+                this.image_preview(move |_, cx| {
                     let image = image_task.peek().cloned().transpose().ok().flatten();
                     let image_task = image_task.clone();
                     cx.new::<ImageHover>(|cx| ImageHover {
@@ -971,35 +957,6 @@ impl Render for LoadingContext {
                     .into()
                 })
             })
-            .child(
-                h_flex()
-                    .gap_1()
-                    .child(
-                        Icon::from_path(self.icon.clone())
-                            .size(IconSize::XSmall)
-                            .color(Color::Muted),
-                    )
-                    .child(
-                        Label::new(self.label.clone())
-                            .size(LabelSize::Small)
-                            .buffer_font(cx)
-                            .single_line(),
-                    )
-                    .map(|el| {
-                        if self.loading.is_some() {
-                            el.with_animation(
-                                "loading-context-crease",
-                                Animation::new(Duration::from_secs(2))
-                                    .repeat()
-                                    .with_easing(pulsating_between(0.4, 0.8)),
-                                |label, delta| label.opacity(delta),
-                            )
-                            .into_any()
-                        } else {
-                            el.into_any()
-                        }
-                    }),
-            )
     }
 }
 

crates/agent_ui/src/ui.rs 🔗

@@ -4,6 +4,7 @@ mod burn_mode_tooltip;
 mod claude_code_onboarding_modal;
 mod end_trial_upsell;
 mod hold_for_default;
+mod mention_crease;
 mod model_selector_components;
 mod onboarding_modal;
 mod usage_callout;
@@ -14,6 +15,7 @@ pub use burn_mode_tooltip::*;
 pub use claude_code_onboarding_modal::*;
 pub use end_trial_upsell::*;
 pub use hold_for_default::*;
+pub use mention_crease::*;
 pub use model_selector_components::*;
 pub use onboarding_modal::*;
 pub use usage_callout::*;

crates/agent_ui/src/ui/mention_crease.rs 🔗

@@ -0,0 +1,100 @@
+use std::time::Duration;
+
+use gpui::{Animation, AnimationExt, AnyView, IntoElement, Window, pulsating_between};
+use settings::Settings;
+use theme::ThemeSettings;
+use ui::{ButtonLike, TintColor, prelude::*};
+
+#[derive(IntoElement)]
+pub struct MentionCrease {
+    id: ElementId,
+    icon: SharedString,
+    label: SharedString,
+    is_toggled: bool,
+    is_loading: bool,
+    image_preview: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
+}
+
+impl MentionCrease {
+    pub fn new(
+        id: impl Into<ElementId>,
+        icon: impl Into<SharedString>,
+        label: impl Into<SharedString>,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            icon: icon.into(),
+            label: label.into(),
+            is_toggled: false,
+            is_loading: false,
+            image_preview: None,
+        }
+    }
+
+    pub fn is_toggled(mut self, is_toggled: bool) -> Self {
+        self.is_toggled = is_toggled;
+        self
+    }
+
+    pub fn is_loading(mut self, is_loading: bool) -> Self {
+        self.is_loading = is_loading;
+        self
+    }
+
+    pub fn image_preview(
+        mut self,
+        builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
+    ) -> Self {
+        self.image_preview = Some(Box::new(builder));
+        self
+    }
+}
+
+impl RenderOnce for MentionCrease {
+    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let settings = ThemeSettings::get_global(cx);
+        let font_size = settings.agent_buffer_font_size(cx);
+        let buffer_font = settings.buffer_font.clone();
+
+        let button_height = DefiniteLength::Absolute(AbsoluteLength::Pixels(
+            px(window.line_height().into()) - px(1.),
+        ));
+
+        ButtonLike::new(self.id)
+            .style(ButtonStyle::Outlined)
+            .size(ButtonSize::Compact)
+            .height(button_height)
+            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+            .toggle_state(self.is_toggled)
+            .when_some(self.image_preview, |this, image_preview| {
+                this.hoverable_tooltip(image_preview)
+            })
+            .child(
+                h_flex()
+                    .pb_px()
+                    .gap_1()
+                    .font(buffer_font)
+                    .text_size(font_size)
+                    .child(
+                        Icon::from_path(self.icon.clone())
+                            .size(IconSize::XSmall)
+                            .color(Color::Muted),
+                    )
+                    .child(self.label.clone())
+                    .map(|this| {
+                        if self.is_loading {
+                            this.with_animation(
+                                "loading-context-crease",
+                                Animation::new(Duration::from_secs(2))
+                                    .repeat()
+                                    .with_easing(pulsating_between(0.4, 0.8)),
+                                |label, delta| label.opacity(delta),
+                            )
+                            .into_any()
+                        } else {
+                            this.into_any()
+                        }
+                    }),
+            )
+    }
+}