gpui-box-kit 0.1.0

GPUI Box Kit design-system components and interaction primitives
Documentation
use gpui::{
    App, Hsla, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div, px,
};
use gpui_kit_semantics::{NodeSpec, Role, Semantic};
use gpui_kit_theme::{ActiveTheme, Theme, TypeScale};

use crate::foundation::{Ident, StyledExt};

/// The severity a status surface claims.
///
/// A tone is a statement about the state, not a decoration: Success on an idle
/// thing says the thing succeeded, which is a different and untrue sentence.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Tone {
    #[default]
    Neutral,
    Accent,
    Success,
    Warning,
    Danger,
    Info,
}

impl Tone {
    /// The name a semantic node publishes, so a test can assert the severity
    /// a surface reported rather than the color it painted.
    pub fn name(self) -> &'static str {
        match self {
            Self::Neutral => "neutral",
            Self::Accent => "accent",
            Self::Success => "success",
            Self::Warning => "warning",
            Self::Danger => "danger",
            Self::Info => "info",
        }
    }

    pub(crate) fn color(self, theme: &Theme) -> Hsla {
        match self {
            Self::Neutral => theme.colors.text_faint,
            Self::Accent => theme.colors.accent,
            Self::Success => theme.colors.success,
            Self::Warning => theme.colors.warning,
            Self::Danger => theme.colors.danger,
            Self::Info => theme.colors.info,
        }
    }
}

/// A compact status label.
///
/// A badge only carries a semantic node when the caller gives it an id, so
/// decorative badges do not add noise to assertion snapshots.
#[derive(Debug, IntoElement)]
pub struct Badge {
    ident: Option<Ident>,
    label: SharedString,
    tone: Tone,
}

impl Badge {
    pub fn new(label: impl Into<SharedString>) -> Self {
        Self {
            ident: None,
            label: label.into(),
            tone: Tone::default(),
        }
    }

    pub fn id(mut self, ident: impl Into<Ident>) -> Self {
        self.ident = Some(ident.into());
        self
    }

    pub fn tone(mut self, tone: Tone) -> Self {
        self.tone = tone;
        self
    }

    pub fn neutral(self) -> Self {
        self.tone(Tone::Neutral)
    }

    pub fn accent(self) -> Self {
        self.tone(Tone::Accent)
    }

    pub fn success(self) -> Self {
        self.tone(Tone::Success)
    }

    pub fn warning(self) -> Self {
        self.tone(Tone::Warning)
    }

    pub fn danger(self) -> Self {
        self.tone(Tone::Danger)
    }

    pub fn info(self) -> Self {
        self.tone(Tone::Info)
    }
}

impl RenderOnce for Badge {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let theme = cx.theme().clone();
        // A badge is the tone itself, as a block. The tint is carried far
        // enough that the block reads on any surface the badge can land on,
        // which is what lets the outline go: an outline around a shape that
        // is already a colour was only ever compensating for a tint too weak
        // to see.
        let (foreground, background) = match self.tone {
            Tone::Neutral => (theme.colors.text_muted, theme.colors.raised),
            tone => {
                let color = tone.color(&theme);
                (color, color.opacity(0.18))
            }
        };

        let element = div()
            .flex_none()
            .px_token(&theme, gpui_kit_theme::Space::Sm)
            .py(px(2.0))
            .rounded_full()
            .bg(background)
            .type_scale(&theme, TypeScale::Caption)
            .text_color(foreground)
            .child(self.label.clone());
        match self.ident {
            Some(ident) => element
                .semantic_in(
                    cx,
                    NodeSpec::new(ident.semantic_id(), Role::Status).text(self.label.clone()),
                )
                .into_any_element(),
            None => element.into_any_element(),
        }
    }
}