fluidattacks-designs 0.12.0

Fluid Attacks design system, GPUI variant: desktop widgets over the shared tokens
//! The runtime theme: a gpui [`Global`] widgets read at render time through
//! [`ActiveTheme`]. Colors resolve when painting, not when compiling, so
//! switching mode is a data swap plus [`Theme::init`], never a widget edit.
//!
//! Widgets paint two kinds of value. Roles (`theme.colors.text`) and the
//! domain functions that take a `&Colors` already carry the active mode.
//! Component tokens that name a palette shade — a table's header fill, a
//! menu's hover row — are written for the light surface and must go through
//! [`Theme::shade`] on the way to `rgb`. Resolving twice is a bug: the second
//! pass would walk the mirror again and land on the wrong end of the ramp.

use fluidattacks_designs_domain::theme::{Colors, Mode};
use fluidattacks_designs_domain::typography::{FONT_MONO, FONT_PRIMARY};
use fluidattacks_designs_domain::{code_snippet, inputs};
use gpui::{px, rgb, rgb_to_hsla, App, Global, Hsla, Result};

#[non_exhaustive]
pub struct Theme {
    pub mode: Mode,
    pub colors: Colors,
}

impl Theme {
    /// Install the theme for `mode`, bind the keymaps, and register the faces
    /// the widgets name.
    ///
    /// Call it at startup: the colors have a light fallback, but nothing else
    /// does — without it the text input takes no keystrokes and every widget
    /// asks for a typeface nothing loaded.
    ///
    /// # Errors
    ///
    /// Fails when the faces cannot be read or the platform refuses them. That
    /// is worth surfacing rather than swallowing: the window still renders, in
    /// whatever face the platform falls back to, which is the kind of defect
    /// that survives review because it looks deliberate.
    pub fn init(mode: Mode, cx: &mut App) -> Result<()> {
        // once only: cx.bind_keys appends, so a re-init (a mode switch) must
        // not grow the keymap or re-register the faces
        if !cx.has_global::<Self>() {
            // faces first: cx.bind_keys appends, so returning between the two
            // would leave the keymaps bound with nothing recording that, and
            // the next attempt would bind them a second time
            cx.text_system().add_fonts(crate::assets::faces()?)?;
            gpui_component::init(cx);
            crate::table::bind_keys(cx);
            crate::text_area::bind_keys(cx);
            crate::text_input::bind_keys(cx);
            crate::select::bind_keys(cx);
            crate::search_dropdown::bind_keys(cx);
            crate::list::bind_keys(cx);
            crate::modal::bind_keys(cx);
        }
        cx.set_global(Self {
            mode,
            colors: mode.colors(),
        });
        dress_components(mode, &mode.colors(), cx);
        Ok(())
    }

    /// Resolve a light palette shade for the active mode. Component tokens
    /// that name a shade rather than a role paint through this.
    #[must_use]
    pub const fn shade(&self, light: u32) -> u32 {
        self.mode.shade(light)
    }
}

impl Global for Theme {}

fn hsla(color: u32) -> Hsla {
    rgb_to_hsla(rgb(color))
}

// the gpui-component widgets this crate hosts (`Markdown`'s `TextView`) read
// their own theme global, so it is repainted from our roles on every mode switch
fn dress_components(mode: Mode, colors: &Colors, cx: &mut App) {
    let components_mode = match mode {
        Mode::Light => gpui_component::ThemeMode::Light,
        Mode::Dark => gpui_component::ThemeMode::Dark,
    };
    gpui_component::Theme::change(components_mode, None, cx);
    let theme = gpui_component::Theme::global_mut(cx);
    theme.font_family = FONT_PRIMARY.into();
    theme.mono_font_family = FONT_MONO.into();
    theme.mono_font_size = px(code_snippet::TEXT_SIZE.px());
    theme.colors.foreground = hsla(colors.text);
    theme.colors.background = hsla(colors.surface);
    theme.colors.border = hsla(colors.border);
    theme.colors.link = hsla(colors.text_strong);
    theme.colors.muted = hsla(mode.shade(code_snippet::SURFACE));
    theme.colors.muted_foreground = hsla(colors.text_muted);
    theme.colors.accent = hsla(colors.surface_muted);
    theme.colors.selection = hsla(mode.shade(inputs::SELECTION));
    theme.tokens = gpui_component::ThemeTokens::from(theme.colors);
}

/// Light, for contexts where no theme was installed.
static DEFAULT: Theme = Theme {
    mode: Mode::Light,
    colors: Mode::Light.colors(),
};

pub trait ActiveTheme {
    fn theme(&self) -> &Theme;
}

impl ActiveTheme for App {
    fn theme(&self) -> &Theme {
        self.try_global::<Theme>().unwrap_or(&DEFAULT)
    }
}