theme/theme/install.rs
1//! Installing a palette as the gpui global, and the per-app palette builder.
2
3use crate::AppExt as _;
4use gpui::{App, Global};
5
6use crate::{
7 Appearance, paint,
8 theme::{Theme, layout},
9};
10
11impl Theme {
12 /// Install the palette for `appearance` as the gpui global and point the
13 /// context-free paint helpers at it. The **only** way the appearance should
14 /// change — setting the global directly leaves
15 /// [`current_appearance`](crate::paint::current_appearance) stale.
16 ///
17 /// Which palette that is comes from [`crate::AppExt::set_palette`], so an app with its own
18 /// colours keeps them across a light/dark switch.
19 pub fn install(appearance: Appearance, cx: &mut App) {
20 let mut theme = match cx.try_global::<Palette>() {
21 Some(palette) => (palette.0)(appearance),
22 None => Self::for_appearance(appearance),
23 };
24 let brand = cx.brand();
25 brand.apply(&mut theme);
26 layout::set_base_radius(brand.radius);
27 Self::install_custom(theme, cx);
28 }
29
30 /// Install a palette the caller built: brand colours, one retuned token, or
31 /// a wholesale replacement. `Theme` is a plain struct with public fields, so
32 /// the usual shape is `Theme::light()` with a few fields overwritten.
33 ///
34 /// Use this rather than `cx.set_global(theme)`. The context-free paint
35 /// helpers ([`ink`], [`hairline`], [`wash`], …) read [`current_appearance`]
36 /// and not the global, so a palette installed around this one leaves them
37 /// painting for whatever appearance was last installed — light washes over a
38 /// dark palette, and nothing to point at.
39 ///
40 /// One-shot: [`appearance::apply`] rebuilds the palette whenever the
41 /// appearance changes, so what is installed here is replaced on a light/dark
42 /// switch. For colours that survive that, register a builder with
43 /// [`crate::AppExt::set_palette`] instead.
44 ///
45 /// [`appearance::apply`]: crate::appearance::apply
46 /// [`ink`]: crate::paint::ink
47 /// [`hairline`]: crate::paint::hairline
48 /// [`wash`]: crate::paint::wash
49 /// [`current_appearance`]: crate::paint::current_appearance
50 pub fn install_custom(theme: Theme, cx: &mut App) {
51 paint::set_current_appearance(theme.appearance);
52 paint::bump_generation();
53 cx.set_global(theme);
54 // Every window, cached subtrees included. gpui busts a view's cache
55 // when an *entity* it read changes, and a palette is a global — so a
56 // cached view would go on painting the palette it was built under, and
57 // an app that caches anything would find the appearance switch half
58 // working.
59 cx.refresh_windows();
60 }
61
62 /// Read the theme global.
63 pub fn of(cx: &App) -> &Theme {
64 cx.global::<Theme>()
65 }
66}
67
68/// How the app builds a palette for an appearance. See [`crate::AppExt::set_palette`].
69struct Palette(Box<dyn Fn(Appearance) -> Theme>);
70
71impl Global for Palette {}
72
73/// Teach bezel how this app builds its palette, so light/dark switching rebuilds
74/// *your* colours instead of replacing them with the built-in ones.
75///
76/// A palette installed with [`Theme::install_custom`] alone lasts only until the
77/// appearance changes, because [`appearance::apply`] rebuilds from scratch.
78/// Registering the builder is what makes brand colours survive:
79///
80/// ```ignore
81/// fn palette(appearance: Appearance) -> Theme {
82/// let mut theme = Theme::for_appearance(appearance);
83/// theme.accent = my_brand_accent(appearance);
84/// theme
85/// }
86/// cx.set_palette(palette); // before appearance::init
87/// ```
88///
89/// Call it before [`appearance::init`], which installs the first palette. Later
90/// than that, follow it with [`appearance::apply`] to repaint.
91///
92/// [`appearance::init`]: crate::appearance::init
93/// [`appearance::apply`]: crate::appearance::apply
94pub(crate) fn set_palette(build: impl Fn(Appearance) -> Theme + 'static, cx: &mut App) {
95 cx.set_global(Palette(Box::new(build)));
96}