1use crate::one_themes::one_dark;
2use crate::{SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent};
3use anyhow::Result;
4use gpui::{
5 px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels, Subscription, ViewContext,
6};
7use refineable::Refineable;
8use schemars::{
9 gen::SchemaGenerator,
10 schema::{InstanceType, Schema, SchemaObject},
11 JsonSchema,
12};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use settings::{Settings, SettingsJsonSchemaParams};
16use std::sync::Arc;
17use util::ResultExt as _;
18
19const MIN_FONT_SIZE: Pixels = px(6.0);
20const MIN_LINE_HEIGHT: f32 = 1.0;
21
22#[derive(Clone)]
23pub struct ThemeSettings {
24 pub ui_font_size: Pixels,
25 pub ui_font: Font,
26 pub buffer_font: Font,
27 pub buffer_font_size: Pixels,
28 pub buffer_line_height: BufferLineHeight,
29 pub requested_theme: Option<String>,
30 pub active_theme: Arc<Theme>,
31 pub theme_overrides: Option<ThemeStyleContent>,
32}
33
34#[derive(Default)]
35pub(crate) struct AdjustedBufferFontSize(Pixels);
36
37#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
38pub struct ThemeSettingsContent {
39 #[serde(default)]
40 pub ui_font_size: Option<f32>,
41 #[serde(default)]
42 pub ui_font_family: Option<String>,
43 #[serde(default)]
44 pub ui_font_features: Option<FontFeatures>,
45 #[serde(default)]
46 pub buffer_font_family: Option<String>,
47 #[serde(default)]
48 pub buffer_font_size: Option<f32>,
49 #[serde(default)]
50 pub buffer_line_height: Option<BufferLineHeight>,
51 #[serde(default)]
52 pub buffer_font_features: Option<FontFeatures>,
53 #[serde(default)]
54 pub theme: Option<String>,
55
56 /// EXPERIMENTAL: Overrides for the current theme.
57 ///
58 /// These values will override the ones on the current theme specified in `theme`.
59 #[serde(rename = "experimental.theme_overrides", default)]
60 pub theme_overrides: Option<ThemeStyleContent>,
61}
62
63#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
64#[serde(rename_all = "snake_case")]
65pub enum BufferLineHeight {
66 #[default]
67 Comfortable,
68 Standard,
69 Custom(f32),
70}
71
72impl BufferLineHeight {
73 pub fn value(&self) -> f32 {
74 match self {
75 BufferLineHeight::Comfortable => 1.618,
76 BufferLineHeight::Standard => 1.3,
77 BufferLineHeight::Custom(line_height) => *line_height,
78 }
79 }
80}
81
82impl ThemeSettings {
83 pub fn buffer_font_size(&self, cx: &AppContext) -> Pixels {
84 cx.try_global::<AdjustedBufferFontSize>()
85 .map_or(self.buffer_font_size, |size| size.0)
86 .max(MIN_FONT_SIZE)
87 }
88
89 pub fn line_height(&self) -> f32 {
90 f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
91 }
92
93 /// Switches to the theme with the given name, if it exists.
94 ///
95 /// Returns a `Some` containing the new theme if it was successful.
96 /// Returns `None` otherwise.
97 pub fn switch_theme(&mut self, theme: &str, cx: &mut AppContext) -> Option<Arc<Theme>> {
98 let themes = ThemeRegistry::default_global(cx);
99
100 let mut new_theme = None;
101
102 if let Some(theme) = themes.get(&theme).log_err() {
103 self.active_theme = theme.clone();
104 new_theme = Some(theme);
105 }
106
107 self.apply_theme_overrides();
108
109 new_theme
110 }
111
112 /// Applies the theme overrides, if there are any, to the current theme.
113 pub fn apply_theme_overrides(&mut self) {
114 if let Some(theme_overrides) = &self.theme_overrides {
115 let mut base_theme = (*self.active_theme).clone();
116
117 base_theme
118 .styles
119 .colors
120 .refine(&theme_overrides.theme_colors_refinement());
121 base_theme
122 .styles
123 .status
124 .refine(&theme_overrides.status_colors_refinement());
125 base_theme.styles.syntax = Arc::new(SyntaxTheme {
126 highlights: {
127 let mut highlights = base_theme.styles.syntax.highlights.clone();
128 // Overrides come second in the highlight list so that they take precedence
129 // over the ones in the base theme.
130 highlights.extend(theme_overrides.syntax_overrides());
131 highlights
132 },
133 });
134
135 self.active_theme = Arc::new(base_theme);
136 }
137 }
138}
139
140pub fn observe_buffer_font_size_adjustment<V: 'static>(
141 cx: &mut ViewContext<V>,
142 f: impl 'static + Fn(&mut V, &mut ViewContext<V>),
143) -> Subscription {
144 cx.observe_global::<AdjustedBufferFontSize>(f)
145}
146
147pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
148 if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::<AdjustedBufferFontSize>() {
149 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
150 let delta = *adjusted_size - buffer_font_size;
151 size + delta
152 } else {
153 size
154 }
155 .max(MIN_FONT_SIZE)
156}
157
158pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) {
159 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
160 let mut adjusted_size = cx
161 .try_global::<AdjustedBufferFontSize>()
162 .map_or(buffer_font_size, |adjusted_size| adjusted_size.0);
163
164 f(&mut adjusted_size);
165 adjusted_size = adjusted_size.max(MIN_FONT_SIZE);
166 cx.set_global(AdjustedBufferFontSize(adjusted_size));
167 cx.refresh();
168}
169
170pub fn reset_font_size(cx: &mut AppContext) {
171 if cx.has_global::<AdjustedBufferFontSize>() {
172 cx.remove_global::<AdjustedBufferFontSize>();
173 cx.refresh();
174 }
175}
176
177impl settings::Settings for ThemeSettings {
178 const KEY: Option<&'static str> = None;
179
180 type FileContent = ThemeSettingsContent;
181
182 fn load(
183 defaults: &Self::FileContent,
184 user_values: &[&Self::FileContent],
185 cx: &mut AppContext,
186 ) -> Result<Self> {
187 let themes = ThemeRegistry::default_global(cx);
188
189 let mut this = Self {
190 ui_font_size: defaults.ui_font_size.unwrap().into(),
191 ui_font: Font {
192 family: defaults.ui_font_family.clone().unwrap().into(),
193 features: defaults.ui_font_features.clone().unwrap(),
194 weight: Default::default(),
195 style: Default::default(),
196 },
197 buffer_font: Font {
198 family: defaults.buffer_font_family.clone().unwrap().into(),
199 features: defaults.buffer_font_features.clone().unwrap(),
200 weight: FontWeight::default(),
201 style: FontStyle::default(),
202 },
203 buffer_font_size: defaults.buffer_font_size.unwrap().into(),
204 buffer_line_height: defaults.buffer_line_height.unwrap(),
205 requested_theme: defaults.theme.clone(),
206 active_theme: themes
207 .get(defaults.theme.as_ref().unwrap())
208 .or(themes.get(&one_dark().name))
209 .unwrap(),
210 theme_overrides: None,
211 };
212
213 for value in user_values.into_iter().copied().cloned() {
214 if let Some(value) = value.buffer_font_family {
215 this.buffer_font.family = value.into();
216 }
217 if let Some(value) = value.buffer_font_features {
218 this.buffer_font.features = value;
219 }
220
221 if let Some(value) = value.ui_font_family {
222 this.ui_font.family = value.into();
223 }
224 if let Some(value) = value.ui_font_features {
225 this.ui_font.features = value;
226 }
227
228 if let Some(value) = &value.theme {
229 this.requested_theme = Some(value.clone());
230
231 if let Some(theme) = themes.get(value).log_err() {
232 this.active_theme = theme;
233 }
234 }
235
236 this.theme_overrides = value.theme_overrides;
237 this.apply_theme_overrides();
238
239 merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into));
240 merge(
241 &mut this.buffer_font_size,
242 value.buffer_font_size.map(Into::into),
243 );
244 merge(&mut this.buffer_line_height, value.buffer_line_height);
245 }
246
247 Ok(this)
248 }
249
250 fn json_schema(
251 generator: &mut SchemaGenerator,
252 params: &SettingsJsonSchemaParams,
253 cx: &AppContext,
254 ) -> schemars::schema::RootSchema {
255 let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
256 let theme_names = ThemeRegistry::global(cx)
257 .list_names(params.staff_mode)
258 .into_iter()
259 .map(|theme_name| Value::String(theme_name.to_string()))
260 .collect();
261
262 let theme_name_schema = SchemaObject {
263 instance_type: Some(InstanceType::String.into()),
264 enum_values: Some(theme_names),
265 ..Default::default()
266 };
267
268 let available_fonts = params
269 .font_names
270 .iter()
271 .cloned()
272 .map(Value::String)
273 .collect();
274 let fonts_schema = SchemaObject {
275 instance_type: Some(InstanceType::String.into()),
276 enum_values: Some(available_fonts),
277 ..Default::default()
278 };
279 root_schema.definitions.extend([
280 ("ThemeName".into(), theme_name_schema.into()),
281 ("FontFamilies".into(), fonts_schema.into()),
282 ]);
283
284 root_schema
285 .schema
286 .object
287 .as_mut()
288 .unwrap()
289 .properties
290 .extend([
291 (
292 "theme".to_owned(),
293 Schema::new_ref("#/definitions/ThemeName".into()),
294 ),
295 (
296 "buffer_font_family".to_owned(),
297 Schema::new_ref("#/definitions/FontFamilies".into()),
298 ),
299 (
300 "ui_font_family".to_owned(),
301 Schema::new_ref("#/definitions/FontFamilies".into()),
302 ),
303 ]);
304
305 root_schema
306 }
307}
308
309fn merge<T: Copy>(target: &mut T, value: Option<T>) {
310 if let Some(value) = value {
311 *target = value;
312 }
313}