diff --git a/crates/core/src/badge.rs b/crates/core/src/badge.rs index 8773802..09c040b 100644 --- a/crates/core/src/badge.rs +++ b/crates/core/src/badge.rs @@ -1,11 +1,19 @@ +use std::rc::Rc; + +use gpui::prelude::FluentBuilder as _; use gpui::{ - App, Hsla, IntoElement, ParentElement as _, RenderOnce, SharedString, StyleRefinement, - Styled as _, Window, + AnyView, App, ElementId, Hsla, InteractiveElement as _, IntoElement, ParentElement as _, + RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, + Window, transparent_black, }; +use gpui_native_icons::Icon; use gpui_native_theme::{ActiveTheme as _, Size, Theme}; use crate::common::{Sizable, StyledExt, h_flex}; +/// The tooltip view builder gpui's `.tooltip(...)` expects. +type TooltipBuilder = Rc AnyView>; + /// How loud a badge is. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum BadgeVariant { @@ -20,18 +28,42 @@ pub enum BadgeVariant { Warning, /// The destructive ramp. Danger, + /// No fill, just a border; the quietest badge. + Outline, } impl BadgeVariant { - fn tokens(self, theme: &Theme) -> (Hsla, Hsla) { + fn tokens(self, theme: &Theme) -> (Hsla, Hsla, Hsla) { match self { - Self::Neutral => (theme.colors.muted, theme.colors.muted_foreground), - Self::Accent => (theme.ramps.primary.base, theme.colors.primary_foreground), - Self::Success => (theme.ramps.success.base, theme.colors.success_foreground), - Self::Warning => (theme.ramps.warning.base, theme.colors.warning_foreground), + Self::Neutral => ( + theme.colors.muted, + theme.colors.muted_foreground, + transparent_black(), + ), + Self::Accent => ( + theme.ramps.primary.base, + theme.colors.primary_foreground, + transparent_black(), + ), + Self::Success => ( + theme.ramps.success.base, + theme.colors.success_foreground, + transparent_black(), + ), + Self::Warning => ( + theme.ramps.warning.base, + theme.colors.warning_foreground, + transparent_black(), + ), Self::Danger => ( theme.ramps.destructive.base, theme.colors.destructive_foreground, + transparent_black(), + ), + Self::Outline => ( + transparent_black(), + theme.colors.foreground, + theme.colors.border, ), } } @@ -40,22 +72,29 @@ impl BadgeVariant { /// A small pill for a count or a state. #[derive(IntoElement)] pub struct Badge { + id: ElementId, label: SharedString, + icon: Option, variant: BadgeVariant, size: Size, + tooltip: Option, style: StyleRefinement, } impl Badge { /// Creates a badge showing `label` — a count or state word the caller formats. - pub fn new(label: L) -> Self + pub fn new(id: I, label: L) -> Self where + I: Into, L: Into, { Self { + id: id.into(), label: label.into(), + icon: None, variant: BadgeVariant::default(), size: Size::default(), + tooltip: None, style: StyleRefinement::default(), } } @@ -65,6 +104,24 @@ impl Badge { self.variant = variant; self } + + /// Shows an icon before the label. + pub fn icon(mut self, icon: I) -> Self + where + I: Into, + { + self.icon = Some(icon.into()); + self + } + + /// Shows a tooltip view next to the badge after it is hovered. + pub fn tooltip(mut self, tooltip: F) -> Self + where + F: Fn(&mut Window, &mut App) -> AnyView + 'static, + { + self.tooltip = Some(Rc::new(tooltip)); + self + } } impl Sizable for Badge { @@ -84,18 +141,28 @@ impl RenderOnce for Badge { let theme = cx.theme(); let metrics = theme.sizes.resolve(self.size); let label_size = metrics.caption_size; - let (background, foreground) = self.variant.tokens(theme); + let (background, foreground, border) = self.variant.tokens(theme); h_flex() + .id(self.id.clone()) .flex_none() .justify_center() + .gap_1() .rounded(theme.radius.full) + .border_1() + .border_color(border) .bg(background) .text_color(foreground) .text_size(label_size) .px(metrics.dense_padding_x) .py_0p5() + .when_some(self.icon, |badge, icon| { + badge.child(icon.size(metrics.dense_icon_size).color(foreground)) + }) .child(self.label) .refine_style(&self.style) + .when_some(self.tooltip, |badge, tooltip| { + badge.tooltip(move |window, cx| tooltip(window, cx)) + }) } } diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 0b5ad73..02e900e 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -1869,13 +1869,27 @@ impl Render for BadgeDemo { h_flex() .flex_wrap() .gap_3() - .child(Badge::new("3")) - .child(Badge::new("new").variant(BadgeVariant::Accent)) - .child(Badge::new("synced").variant(BadgeVariant::Success)) - .child(Badge::new("drift").variant(BadgeVariant::Warning)) - .child(Badge::new("12").variant(BadgeVariant::Danger)) - .child(Badge::new("small").small()) - .child(Badge::new("large").large()), + .child(Badge::new("badge-count", "3")) + .child(Badge::new("badge-new", "new").variant(BadgeVariant::Accent)) + .child( + Badge::new("badge-synced", "synced") + .icon(IconRole::Check) + .variant(BadgeVariant::Success), + ) + .child(Badge::new("badge-drift", "drift").variant(BadgeVariant::Warning)) + .child(Badge::new("badge-danger", "12").variant(BadgeVariant::Danger)) + .child(Badge::new("badge-outline", "outline").variant(BadgeVariant::Outline)) + .child(Badge::new("badge-small", "small").small()) + .child(Badge::new("badge-large", "large").large()), + )) + .child(demo_section( + "tooltip", + colors, + h_flex().flex_wrap().gap_3().child( + Badge::new("badge-help", "beta") + .variant(BadgeVariant::Accent) + .tooltip(Tooltip::text("This build is not ready for release")), + ), )) } } @@ -2741,7 +2755,11 @@ impl Render for DropdownMenuDemo { MenuItem::new("Save as template"), MenuItem::new("Save a copy"), MenuItem::separator(), - MenuItem::new("Export"), + MenuItem::new("Export").submenu(vec![ + MenuItem::new("PNG"), + MenuItem::new("SVG"), + MenuItem::new("PDF"), + ]), ] }); @@ -3751,7 +3769,8 @@ impl Render for SidebarDemo { .prefix(row_icon()) .detail("24") .suffix( - Badge::new("new").variant(BadgeVariant::Accent), + Badge::new("sidebar-demo-new-badge", "new") + .variant(BadgeVariant::Accent), ), SidebarEntry::row("Archive").disabled(true), SidebarEntry::header("Locations"),