Skip to main content

gpui_base/
theme.rs

1use gpui::{App, Global};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5use crate::{PlotMotion, ScrollbarMode, ScrollbarMotion, ScrollbarStyles, SemanticThemeTokens};
6
7/// Application-wide defaults for Base behavior modules.
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "lowercase")]
10pub enum ThemeAppearance {
11    #[default]
12    Light,
13    Dark,
14}
15
16#[derive(Clone, Default)]
17pub struct Theme {
18    pub appearance: ThemeAppearance,
19    pub tokens: SemanticThemeTokens,
20    pub scrollbar: ScrollbarTheme,
21    pub resizable: ResizableTheme,
22    pub plot: PlotTheme,
23}
24
25impl Global for Theme {}
26
27impl Theme {
28    pub fn global(cx: &App) -> Self {
29        cx.try_global::<Self>().cloned().unwrap_or_default()
30    }
31
32    pub fn global_mut(cx: &mut App) -> &mut Self {
33        if !cx.has_global::<Self>() {
34            cx.set_global(Self::default());
35        }
36        cx.global_mut::<Self>()
37    }
38}
39
40/// Access to the active base theme through an application context.
41pub(crate) trait ActiveTheme {
42    fn theme(&self) -> Theme;
43}
44
45impl ActiveTheme for App {
46    #[inline(always)]
47    fn theme(&self) -> Theme {
48        Theme::global(self)
49    }
50}
51
52/// Global defaults used by [`crate::Scrollbar`].
53///
54/// `motion` defaults to motionless. Styled layers project their own timing;
55/// Base never installs a fade or slide of its own.
56#[derive(Clone, Default)]
57pub struct ScrollbarTheme {
58    mode: ScrollbarMode,
59    motion: ScrollbarMotion,
60    styles: ScrollbarStyles,
61}
62
63impl ScrollbarTheme {
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    pub fn with_mode(mut self, mode: ScrollbarMode) -> Self {
69        self.mode = mode;
70        self
71    }
72
73    pub fn with_motion(mut self, motion: ScrollbarMotion) -> Self {
74        self.motion = motion;
75        self
76    }
77
78    pub fn with_styles(mut self, styles: ScrollbarStyles) -> Self {
79        self.styles = styles;
80        self
81    }
82
83    pub fn mode(&self) -> ScrollbarMode {
84        self.mode
85    }
86
87    pub fn motion(&self) -> ScrollbarMotion {
88        self.motion
89    }
90
91    pub fn styles(&self) -> &ScrollbarStyles {
92        &self.styles
93    }
94}
95
96/// Global defaults used by [`crate::plot`].
97///
98/// `motion` defaults to motionless. Styled layers project their own timing;
99/// Base never installs a fade or glide of its own.
100#[derive(Clone, Default)]
101pub struct PlotTheme {
102    motion: PlotMotion,
103}
104
105impl PlotTheme {
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    pub fn with_motion(mut self, motion: PlotMotion) -> Self {
111        self.motion = motion;
112        self
113    }
114
115    pub fn motion(&self) -> &PlotMotion {
116        &self.motion
117    }
118}
119
120/// Global visual defaults used by resizable panel handles.
121///
122/// `None` means *unset*, not invisible: a handle with nothing projected onto it
123/// resolves from the active [`SemanticThemeTokens`] -- `border` at rest, `ring`
124/// while dragging -- which are the tokens those two states already mean
125/// everywhere else.
126///
127/// These were plain colors, so the Base default was `Hsla::default()`: fully
128/// transparent. That reads as a deliberate choice next to a styled façade,
129/// which projects its own values and never sees it, and as a missing divider
130/// to anything that does not -- and a consumer with no façade has no way to
131/// project anything. Making them optional keeps the projection exactly as it
132/// was while giving the unprojected case an answer.
133#[derive(Clone, Copy, Default)]
134pub struct ResizableTheme {
135    pub handle: Option<gpui::Hsla>,
136    pub active_handle: Option<gpui::Hsla>,
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn scrollbar_theme_supports_builder_construction() {
145        let mode = ScrollbarMode::Hover;
146        let motion = ScrollbarMotion::default().with_enter(std::time::Duration::from_millis(120));
147        let styles = ScrollbarStyles::default();
148        let theme = ScrollbarTheme::new()
149            .with_mode(mode)
150            .with_motion(motion)
151            .with_styles(styles);
152
153        assert_eq!(theme.mode(), mode);
154        assert_eq!(theme.motion(), motion);
155        let _ = theme.styles();
156    }
157}