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