1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//! 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)
}
}