fluidattacks-designs 0.11.0

Fluid Attacks design system, GPUI variant: desktop widgets over the shared tokens
//! `NotificationSign` — the web's alert badge: an 8px dot pinned over its
//! anchor (give the parent `relative()`), or an inline count pill when a
//! number indicator is set.

use fluidattacks_designs_domain::notification_sign::{
    display_count, Variant, COUNT_HEIGHT, COUNT_LINE_HEIGHT, COUNT_MIN_WIDTH, COUNT_PADDING_X,
    DOT_PADDING, TEXT,
};
use fluidattacks_designs_domain::typography::{TextSize, FONT_PRIMARY};
use gpui::prelude::FluentBuilder as _;
use gpui::{div, px, rgb, App, IntoElement, ParentElement as _, RenderOnce, Styled as _, Window};

use crate::theme::ActiveTheme as _;

#[derive(IntoElement)]
pub struct NotificationSign {
    variant: Variant,
    count: Option<u32>,
    show: bool,
    top: Option<f32>,
    left: Option<f32>,
}

impl NotificationSign {
    #[must_use]
    pub const fn new() -> Self {
        Self {
            variant: Variant::Error,
            count: None,
            show: true,
            top: None,
            left: None,
        }
    }

    #[must_use]
    pub const fn variant(mut self, variant: Variant) -> Self {
        self.variant = variant;
        self
    }

    /// Switches the dot into the inline count pill (values above 9 show 9,
    /// like the web).
    #[must_use]
    pub const fn count(mut self, count: u32) -> Self {
        self.count = Some(count);
        self
    }

    #[must_use]
    pub const fn show(mut self, show: bool) -> Self {
        self.show = show;
        self
    }

    /// Dot offset from the anchor's top, in pixels.
    #[must_use]
    pub const fn top(mut self, top: f32) -> Self {
        self.top = Some(top);
        self
    }

    /// Dot offset from the anchor's left, in pixels.
    #[must_use]
    pub const fn left(mut self, left: f32) -> Self {
        self.left = Some(left);
        self
    }
}

impl Default for NotificationSign {
    fn default() -> Self {
        Self::new()
    }
}

impl RenderOnce for NotificationSign {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        if !self.show {
            return div();
        }
        let theme = cx.theme();
        let background = rgb(theme.shade(self.variant.background()));
        let text = rgb(theme.shade(TEXT));
        let (top, left) = (self.top, self.left);
        self.count.filter(|&count| count > 0).map_or_else(
            || {
                div()
                    .absolute()
                    .when_some(top, |el, top| el.top(px(top)))
                    .when_some(left, |el, left| el.left(px(left)))
                    .p(px(DOT_PADDING))
                    .rounded_full()
                    .bg(background)
            },
            |count| {
                div()
                    .flex()
                    .items_center()
                    .justify_center()
                    .min_w(px(COUNT_MIN_WIDTH))
                    .h(px(COUNT_HEIGHT))
                    .px(px(COUNT_PADDING_X))
                    .rounded_full()
                    .bg(background)
                    .font_family(FONT_PRIMARY)
                    .text_size(px(TextSize::Sm.px()))
                    .line_height(px(COUNT_LINE_HEIGHT))
                    .text_color(text)
                    .child(display_count(count).to_string())
            },
        )
    }
}