fluidattacks-designs 0.12.0

Fluid Attacks design system, GPUI variant: desktop widgets over the shared tokens
//! `Text`, `Heading` and `Span` — the web typography components, with their
//! defaults: text and spans are regular on the `text` role, headings are
//! bold on `text_strong`. gpui text keeps embedded newlines while wrapping,
//! which matches the web's `white-space: pre-line` default.

use crate::theme::ActiveTheme as _;
use fluidattacks_designs_domain::typography::{
    HeadingSize, TextSize, Weight, FONT_PRIMARY, SPAN_LINE_HEIGHT,
};
use gpui::{
    div, px, rgb, App, FontWeight, IntoElement, ParentElement as _, RenderOnce, SharedString,
    Styled as _, Window,
};

fn font_weight(weight: Weight) -> FontWeight {
    FontWeight(f32::from(weight.value()))
}

#[derive(IntoElement)]
pub struct Text {
    content: SharedString,
    size: TextSize,
    weight: Weight,
    color: Option<u32>,
}

impl Text {
    pub fn new(content: impl Into<SharedString>) -> Self {
        Self {
            content: content.into(),
            size: TextSize::default(),
            weight: Weight::default(),
            color: None,
        }
    }

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

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

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

impl RenderOnce for Text {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let color = self.color.unwrap_or_else(|| cx.theme().colors.text);
        div()
            .font_family(FONT_PRIMARY)
            .text_size(px(self.size.px()))
            .line_height(px(self.size.line_height_px()))
            .font_weight(font_weight(self.weight))
            .text_color(rgb(color))
            .child(self.content)
    }
}

#[derive(IntoElement)]
pub struct Heading {
    content: SharedString,
    size: HeadingSize,
    weight: Weight,
    color: Option<u32>,
}

impl Heading {
    pub fn new(content: impl Into<SharedString>) -> Self {
        Self {
            content: content.into(),
            size: HeadingSize::default(),
            weight: Weight::Bold,
            color: None,
        }
    }

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

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

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

impl RenderOnce for Heading {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let color = self.color.unwrap_or_else(|| cx.theme().colors.text_strong);
        div()
            .font_family(FONT_PRIMARY)
            .text_size(px(self.size.px()))
            .line_height(px(self.size.line_height_px()))
            .font_weight(font_weight(self.weight))
            .text_color(rgb(color))
            .child(self.content)
    }
}

/// The web `Span`: like [`Text`] but its line height pins to 20px by
/// default instead of tracking the size, and can be overridden (web
/// `lineSpacing`).
#[derive(IntoElement)]
pub struct Span {
    content: SharedString,
    size: TextSize,
    weight: Weight,
    color: Option<u32>,
    line_height: f32,
}

impl Span {
    #[must_use]
    pub fn new(content: impl Into<SharedString>) -> Self {
        Self {
            content: content.into(),
            size: TextSize::default(),
            weight: Weight::default(),
            color: None,
            line_height: SPAN_LINE_HEIGHT,
        }
    }

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

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

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

    /// Line height in pixels (web `lineSpacing`, default spacing 1.25).
    #[must_use]
    pub const fn line_height(mut self, line_height: f32) -> Self {
        self.line_height = line_height;
        self
    }
}

impl RenderOnce for Span {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let color = self.color.unwrap_or_else(|| cx.theme().colors.text);
        div()
            .font_family(FONT_PRIMARY)
            .text_size(px(self.size.px()))
            .line_height(px(self.line_height))
            .font_weight(font_weight(self.weight))
            .text_color(rgb(color))
            .max_w_full()
            .child(self.content)
    }
}