ui: Add Chip component (#34521)

Danilo Leal created

Possibly the simplest component in our set, but a nice one to have so we
can standardize how it looks across the app.

Release Notes:

- N/A

Change summary

crates/agent_ui/src/agent_configuration.rs  |  23 +---
crates/component/src/component_layout.rs    |  12 +-
crates/extensions_ui/src/extensions_ui.rs   |  17 ---
crates/ui/src/components.rs                 |   2 
crates/ui/src/components/chip.rs            | 106 +++++++++++++++++++++++
crates/ui/src/components/keybinding_hint.rs |   2 
crates/ui/src/components/tooltip.rs         |   2 
7 files changed, 124 insertions(+), 40 deletions(-)

Detailed changes

crates/agent_ui/src/agent_configuration.rs 🔗

@@ -27,7 +27,7 @@ use project::{
 use proto::Plan;
 use settings::{Settings, update_settings_file};
 use ui::{
-    ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
+    Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
     Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*,
 };
 use util::ResultExt as _;
@@ -227,7 +227,7 @@ impl AgentConfiguration {
                                             )
                                             .map(|this| {
                                                 if is_zed_provider {
-                                                    this.child(
+                                                    this.gap_2().child(
                                                         self.render_zed_plan_info(current_plan, cx),
                                                     )
                                                 } else {
@@ -474,26 +474,15 @@ impl AgentConfiguration {
                 .opacity(0.5)
                 .blend(cx.theme().colors().text_accent.opacity(0.2));
 
-            let (plan_name, plan_color, bg_color) = match plan {
+            let (plan_name, label_color, bg_color) = match plan {
                 Plan::Free => ("Free", Color::Default, free_chip_bg),
                 Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
                 Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
             };
 
-            h_flex()
-                .ml_1()
-                .px_1()
-                .rounded_sm()
-                .border_1()
-                .border_color(cx.theme().colors().border)
-                .bg(bg_color)
-                .overflow_hidden()
-                .child(
-                    Label::new(plan_name.to_string())
-                        .color(plan_color)
-                        .size(LabelSize::XSmall)
-                        .buffer_font(cx),
-                )
+            Chip::new(plan_name.to_string())
+                .bg_color(bg_color)
+                .label_color(label_color)
                 .into_any_element()
         } else {
             div().into_any_element()

crates/component/src/component_layout.rs 🔗

@@ -48,20 +48,20 @@ impl RenderOnce for ComponentExample {
             )
             .child(
                 div()
-                    .flex()
-                    .w_full()
-                    .rounded_xl()
                     .min_h(px(100.))
-                    .justify_center()
+                    .w_full()
                     .p_8()
+                    .flex()
+                    .items_center()
+                    .justify_center()
+                    .rounded_xl()
                     .border_1()
                     .border_color(cx.theme().colors().border.opacity(0.5))
                     .bg(pattern_slash(
-                        cx.theme().colors().surface_background.opacity(0.5),
+                        cx.theme().colors().surface_background.opacity(0.25),
                         12.0,
                         12.0,
                     ))
-                    .shadow_xs()
                     .child(self.element),
             )
             .into_any_element()

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -24,7 +24,7 @@ use settings::Settings;
 use strum::IntoEnumIterator as _;
 use theme::ThemeSettings;
 use ui::{
-    CheckboxWithLabel, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState,
+    CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState,
     ToggleButton, Tooltip, prelude::*,
 };
 use vim_mode_setting::VimModeSetting;
@@ -759,20 +759,7 @@ impl ExtensionsPage {
                                                     _ => {}
                                                 }
 
-                                                Some(
-                                                    div()
-                                                        .px_1()
-                                                        .border_1()
-                                                        .rounded_sm()
-                                                        .border_color(cx.theme().colors().border)
-                                                        .bg(cx.theme().colors().element_background)
-                                                        .child(
-                                                            Label::new(extension_provides_label(
-                                                                *provides,
-                                                            ))
-                                                            .size(LabelSize::XSmall),
-                                                        ),
-                                                )
+                                                Some(Chip::new(extension_provides_label(*provides)))
                                             })
                                             .collect::<Vec<_>>(),
                                     ),

crates/ui/src/components.rs 🔗

@@ -2,6 +2,7 @@ mod avatar;
 mod banner;
 mod button;
 mod callout;
+mod chip;
 mod content_group;
 mod context_menu;
 mod disclosure;
@@ -43,6 +44,7 @@ pub use avatar::*;
 pub use banner::*;
 pub use button::*;
 pub use callout::*;
+pub use chip::*;
 pub use content_group::*;
 pub use context_menu::*;
 pub use disclosure::*;

crates/ui/src/components/chip.rs 🔗

@@ -0,0 +1,106 @@
+use crate::prelude::*;
+use gpui::{AnyElement, Hsla, IntoElement, ParentElement, Styled};
+
+/// Chips provide a container for an informative label.
+///
+/// # Usage Example
+///
+/// ```
+/// use ui::{Chip};
+///
+///    Chip::new("This Chip")
+/// ```
+#[derive(IntoElement, RegisterComponent)]
+pub struct Chip {
+    label: SharedString,
+    label_color: Color,
+    label_size: LabelSize,
+    bg_color: Option<Hsla>,
+}
+
+impl Chip {
+    /// Creates a new `Chip` component with the specified label.
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+            label_color: Color::Default,
+            label_size: LabelSize::XSmall,
+            bg_color: None,
+        }
+    }
+
+    /// Sets the color of the label.
+    pub fn label_color(mut self, color: Color) -> Self {
+        self.label_color = color;
+        self
+    }
+
+    /// Sets the size of the label.
+    pub fn label_size(mut self, size: LabelSize) -> Self {
+        self.label_size = size;
+        self
+    }
+
+    /// Sets a custom background color for the callout content.
+    pub fn bg_color(mut self, color: Hsla) -> Self {
+        self.bg_color = Some(color);
+        self
+    }
+}
+
+impl RenderOnce for Chip {
+    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
+        let bg_color = self
+            .bg_color
+            .unwrap_or(cx.theme().colors().element_background);
+
+        h_flex()
+            .min_w_0()
+            .flex_initial()
+            .px_1()
+            .border_1()
+            .rounded_sm()
+            .border_color(cx.theme().colors().border)
+            .bg(bg_color)
+            .overflow_hidden()
+            .child(
+                Label::new(self.label)
+                    .size(self.label_size)
+                    .color(self.label_color)
+                    .buffer_font(cx),
+            )
+    }
+}
+
+impl Component for Chip {
+    fn scope() -> ComponentScope {
+        ComponentScope::DataDisplay
+    }
+
+    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+        let chip_examples = vec![
+            single_example("Default", Chip::new("Chip Example").into_any_element()),
+            single_example(
+                "Customized Label Color",
+                Chip::new("Chip Example")
+                    .label_color(Color::Accent)
+                    .into_any_element(),
+            ),
+            single_example(
+                "Customized Label Size",
+                Chip::new("Chip Example")
+                    .label_size(LabelSize::Large)
+                    .label_color(Color::Accent)
+                    .into_any_element(),
+            ),
+            single_example(
+                "Customized Background Color",
+                Chip::new("Chip Example")
+                    .bg_color(cx.theme().colors().text_accent.opacity(0.1))
+                    .into_any_element(),
+            ),
+        ];
+
+        Some(example_group(chip_examples).vertical().into_any_element())
+    }
+}

crates/ui/src/components/keybinding_hint.rs 🔗

@@ -206,7 +206,7 @@ impl RenderOnce for KeybindingHint {
 
 impl Component for KeybindingHint {
     fn scope() -> ComponentScope {
-        ComponentScope::None
+        ComponentScope::DataDisplay
     }
 
     fn description() -> Option<&'static str> {

crates/ui/src/components/tooltip.rs 🔗

@@ -274,7 +274,7 @@ impl Render for LinkPreview {
 
 impl Component for Tooltip {
     fn scope() -> ComponentScope {
-        ComponentScope::None
+        ComponentScope::DataDisplay
     }
 
     fn description() -> Option<&'static str> {