glassy-ui 0.1.1

Glassy UI components for native GPUI interfaces.
Documentation
//! Paper hex+alpha is grouped as `RRGGBB_AA`.
#![allow(clippy::unusual_byte_groupings)]

use crate::theme::{paint, rgb, Theme, ThemeKind};
use gpui::{point, px, BoxShadow, Hsla};

use crate::button::ButtonVariant;

/// Construct a `BoxShadow` for the published gpui 0.2.2 struct API.
/// (The old `BoxShadow::new(x, y, color).inset()/.blur_radius()/.spread_radius()`
/// builder is gone; `inset` shadows are not supported by this version.)
pub(crate) fn box_shadow(x: f32, y: f32, color: Hsla, blur: f32, spread: f32) -> BoxShadow {
    BoxShadow {
        color,
        offset: point(px(x), px(y)),
        blur_radius: px(blur),
        spread_radius: px(spread),
    }
}

/// 3px zinc/white spread used by focused controls.
pub(crate) fn focus_ring(theme: Theme) -> BoxShadow {
    box_shadow(
        0.,
        0.,
        if theme.is_dark() {
            paint(0xFFFFFF24)
        } else {
            paint(0x18181B24)
        },
        0.,
        3.,
    )
}

/// Rest / focus / disabled / invalid paint for Input and Textarea.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FieldState {
    #[default]
    Rest,
    Focus,
    Disabled,
    Invalid,
}

/// Per-state paint used by [`crate::Input`].
#[derive(Clone, Copy, Debug)]
pub struct FieldChrome {
    pub bg: Hsla,
    pub border: Hsla,
    pub fg: Hsla,
    pub placeholder: Hsla,
    pub caret: Hsla,
    pub inset: Hsla,
    pub shadow: Hsla,
    pub shadow_y: f32,
    pub shadow_blur: f32,
    pub ring: Option<Hsla>,
}

/// Per-variant paint used by [`crate::Button`].
#[derive(Clone, Copy, Debug)]
pub struct ButtonChrome {
    pub bg: Hsla,
    pub hover_bg: Hsla,
    pub border: Hsla,
    pub fg: Hsla,
    pub inset: Hsla,
    pub shadow: Hsla,
    pub shadow_y: f32,
    pub shadow_blur: f32,
}

pub fn button_chrome(theme: Theme, variant: ButtonVariant) -> ButtonChrome {
    match (theme.kind, variant) {
        (ThemeKind::Light, ButtonVariant::Primary) => ButtonChrome {
            bg: paint(0x18181B_B8),
            hover_bg: paint(0x18181B_CC),
            border: paint(0xFFFFFF_29),
            fg: theme.on_solid,
            inset: paint(0xFFFFFF_38),
            shadow: paint(0x0F172A_1A),
            shadow_y: 8.0,
            shadow_blur: 20.0,
        },
        (ThemeKind::Light, ButtonVariant::Secondary) => ButtonChrome {
            bg: paint(0xFFFFFF_85),
            hover_bg: paint(0xFFFFFF_99),
            border: paint(0xFFFFFF_B8),
            fg: theme.ink,
            inset: paint(0xFFFFFF_E6),
            shadow: paint(0x0F172A_0F),
            shadow_y: 6.0,
            shadow_blur: 16.0,
        },
        (ThemeKind::Light, ButtonVariant::Destructive) => ButtonChrome {
            bg: paint(0xDC2626_C7),
            hover_bg: paint(0xDC2626_DB),
            border: paint(0xFFFFFF_47),
            fg: rgb(0xFFFFFF),
            inset: paint(0xFFFFFF_47),
            shadow: paint(0xDC2626_29),
            shadow_y: 8.0,
            shadow_blur: 20.0,
        },
        (ThemeKind::Light, ButtonVariant::Outline) => ButtonChrome {
            bg: paint(0xFFFFFF_47),
            hover_bg: paint(0xFFFFFF_5C),
            border: paint(0xFFFFFF_9E),
            fg: theme.ink,
            inset: paint(0xFFFFFF_BF),
            shadow: paint(0x0F172A_0A),
            shadow_y: 4.0,
            shadow_blur: 12.0,
        },
        (ThemeKind::Light, ButtonVariant::OutlineDestructive) => ButtonChrome {
            bg: paint(0xFEE2E2_6B),
            hover_bg: paint(0xFEE2E2_85),
            border: paint(0xFECACA_B3),
            fg: theme.destructive,
            inset: paint(0xFFFFFF_B3),
            shadow: paint(0xDC2626_0F),
            shadow_y: 4.0,
            shadow_blur: 12.0,
        },
        (ThemeKind::Light, ButtonVariant::Ghost) => ButtonChrome {
            bg: paint(0xFFFFFF_29),
            hover_bg: paint(0xFFFFFF_3D),
            border: paint(0xFFFFFF_47),
            fg: theme.ink,
            inset: paint(0xFFFFFF_66),
            shadow: gpui::transparent_black(),
            shadow_y: 0.0,
            shadow_blur: 0.0,
        },
        (ThemeKind::Dark, ButtonVariant::Primary) => ButtonChrome {
            bg: paint(0xFFFFFF_29),
            hover_bg: paint(0xFFFFFF_38),
            border: paint(0xFFFFFF_38),
            fg: theme.on_solid,
            inset: paint(0xFFFFFF_47),
            shadow: paint(0x000000_47),
            shadow_y: 8.0,
            shadow_blur: 24.0,
        },
        (ThemeKind::Dark, ButtonVariant::Secondary) => ButtonChrome {
            bg: paint(0xFFFFFF_12),
            hover_bg: paint(0xFFFFFF_1A),
            border: paint(0xFFFFFF_1A),
            fg: theme.on_solid,
            inset: paint(0xFFFFFF_1F),
            shadow: paint(0x000000_2E),
            shadow_y: 6.0,
            shadow_blur: 16.0,
        },
        (ThemeKind::Dark, ButtonVariant::Destructive) => ButtonChrome {
            bg: paint(0xDC2626_9E),
            hover_bg: paint(0xDC2626_B3),
            border: paint(0xFFFFFF_29),
            fg: theme.on_solid,
            inset: paint(0xFFFFFF_2E),
            shadow: paint(0xDC2626_2E),
            shadow_y: 8.0,
            shadow_blur: 20.0,
        },
        (ThemeKind::Dark, ButtonVariant::Outline) => ButtonChrome {
            bg: paint(0xFFFFFF_0A),
            hover_bg: paint(0xFFFFFF_14),
            border: paint(0xFFFFFF_24),
            fg: theme.on_solid,
            inset: paint(0xFFFFFF_1A),
            shadow: paint(0x000000_29),
            shadow_y: 4.0,
            shadow_blur: 12.0,
        },
        (ThemeKind::Dark, ButtonVariant::OutlineDestructive) => ButtonChrome {
            bg: paint(0x7F1D1D_47),
            hover_bg: paint(0x7F1D1D_5C),
            border: paint(0xF87171_47),
            fg: theme.destructive_soft,
            inset: paint(0xFFFFFF_14),
            shadow: gpui::transparent_black(),
            shadow_y: 0.0,
            shadow_blur: 0.0,
        },
        (ThemeKind::Dark, ButtonVariant::Ghost) => ButtonChrome {
            bg: paint(0xFFFFFF_08),
            hover_bg: paint(0xFFFFFF_12),
            border: paint(0xFFFFFF_0F),
            fg: theme.on_solid,
            inset: paint(0xFFFFFF_0F),
            shadow: gpui::transparent_black(),
            shadow_y: 0.0,
            shadow_blur: 0.0,
        },
    }
}

pub fn field_chrome(theme: Theme, state: FieldState) -> FieldChrome {
    match (theme.kind, state) {
        (ThemeKind::Light, FieldState::Rest) => FieldChrome {
            bg: paint(0xFFFFFF_47),
            border: paint(0xFFFFFF_9E),
            fg: theme.ink,
            placeholder: theme.placeholder,
            caret: theme.ink,
            inset: paint(0xFFFFFF_BF),
            shadow: paint(0x0F172A_0A),
            shadow_y: 4.0,
            shadow_blur: 12.0,
            ring: None,
        },
        (ThemeKind::Light, FieldState::Focus) => FieldChrome {
            bg: paint(0xFFFFFF_5C),
            border: paint(0x18181B_47),
            fg: theme.ink,
            placeholder: theme.placeholder,
            caret: rgb(0x18181B),
            inset: paint(0xFFFFFF_BF),
            shadow: paint(0x0F172A_0A),
            shadow_y: 4.0,
            shadow_blur: 12.0,
            ring: Some(paint(0x18181B_24)),
        },
        (ThemeKind::Light, FieldState::Disabled) => FieldChrome {
            bg: paint(0xFFFFFF_29),
            border: paint(0xFFFFFF_47),
            fg: theme.muted_fg(),
            placeholder: theme.muted_fg(),
            caret: theme.muted_fg(),
            inset: paint(0xFFFFFF_66),
            shadow: gpui::transparent_black(),
            shadow_y: 0.0,
            shadow_blur: 0.0,
            ring: None,
        },
        (ThemeKind::Light, FieldState::Invalid) => FieldChrome {
            bg: paint(0xFEE2E2_6B),
            border: paint(0xFECACA_B3),
            fg: theme.ink,
            placeholder: theme.placeholder,
            caret: theme.ink,
            inset: paint(0xFFFFFF_B3),
            shadow: paint(0xDC2626_0F),
            shadow_y: 4.0,
            shadow_blur: 12.0,
            ring: None,
        },
        (ThemeKind::Dark, FieldState::Rest) => FieldChrome {
            bg: paint(0xFFFFFF_0A),
            border: paint(0xFFFFFF_24),
            fg: theme.ink,
            placeholder: theme.placeholder,
            caret: theme.ink,
            inset: paint(0xFFFFFF_1A),
            shadow: paint(0x000000_29),
            shadow_y: 4.0,
            shadow_blur: 12.0,
            ring: None,
        },
        (ThemeKind::Dark, FieldState::Focus) => FieldChrome {
            bg: paint(0xFFFFFF_14),
            border: paint(0xFFFFFF_38),
            fg: theme.ink,
            placeholder: theme.placeholder,
            caret: rgb(0xFAFAFA),
            inset: paint(0xFFFFFF_1A),
            shadow: paint(0x000000_29),
            shadow_y: 4.0,
            shadow_blur: 12.0,
            ring: Some(paint(0xFFFFFF_24)),
        },
        (ThemeKind::Dark, FieldState::Disabled) => FieldChrome {
            bg: paint(0xFFFFFF_08),
            border: paint(0xFFFFFF_0F),
            fg: theme.muted_fg(),
            placeholder: theme.muted_fg(),
            caret: theme.muted_fg(),
            inset: paint(0xFFFFFF_0F),
            shadow: gpui::transparent_black(),
            shadow_y: 0.0,
            shadow_blur: 0.0,
            ring: None,
        },
        (ThemeKind::Dark, FieldState::Invalid) => FieldChrome {
            bg: paint(0x7F1D1D_47),
            border: paint(0xF87171_47),
            fg: theme.ink,
            placeholder: theme.placeholder,
            caret: theme.ink,
            inset: paint(0xFFFFFF_14),
            shadow: gpui::transparent_black(),
            shadow_y: 0.0,
            shadow_blur: 0.0,
            ring: None,
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::button::ButtonVariant;

    #[test]
    fn light_primary_matches_paper() {
        let chrome = button_chrome(Theme::light(), ButtonVariant::Primary);
        assert_eq!(chrome.bg, paint(0x18181B_B8));
        assert_eq!(chrome.border, paint(0xFFFFFF_29));
        assert_eq!(chrome.shadow_y, 8.0);
        assert_eq!(chrome.shadow_blur, 20.0);
    }

    #[test]
    fn dark_primary_is_white_glass() {
        let chrome = button_chrome(Theme::dark(), ButtonVariant::Primary);
        assert_eq!(chrome.bg, paint(0xFFFFFF_29));
        assert_eq!(chrome.border, paint(0xFFFFFF_38));
        assert_eq!(chrome.shadow_blur, 24.0);
    }

    #[test]
    fn light_focus_matches_paper() {
        let chrome = field_chrome(Theme::light(), FieldState::Focus);
        assert_eq!(chrome.bg, paint(0xFFFFFF_5C));
        assert_eq!(chrome.border, paint(0x18181B_47));
        assert_eq!(chrome.ring, Some(paint(0x18181B_24)));
    }

    #[test]
    fn dark_invalid_matches_paper() {
        let chrome = field_chrome(Theme::dark(), FieldState::Invalid);
        assert_eq!(chrome.bg, paint(0x7F1D1D_47));
        assert_eq!(chrome.border, paint(0xF87171_47));
    }
}