1use crate::one_themes::one_dark;
2use crate::{Theme, ThemeRegistry};
3use anyhow::Result;
4use gpui::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels};
5use schemars::{
6 gen::SchemaGenerator,
7 schema::{InstanceType, Schema, SchemaObject},
8 JsonSchema,
9};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use settings::{Settings, SettingsJsonSchemaParams};
13use std::sync::Arc;
14use util::ResultExt as _;
15
16const MIN_FONT_SIZE: Pixels = px(6.0);
17const MIN_LINE_HEIGHT: f32 = 1.0;
18
19#[derive(Clone)]
20pub struct ThemeSettings {
21 pub ui_font_size: Pixels,
22 pub ui_font: Font,
23 pub buffer_font: Font,
24 pub buffer_font_size: Pixels,
25 pub buffer_line_height: BufferLineHeight,
26 pub active_theme: Arc<Theme>,
27}
28
29#[derive(Default)]
30pub struct AdjustedBufferFontSize(Option<Pixels>);
31
32#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
33pub struct ThemeSettingsContent {
34 #[serde(default)]
35 pub ui_font_size: Option<f32>,
36 #[serde(default)]
37 pub buffer_font_family: Option<String>,
38 #[serde(default)]
39 pub buffer_font_size: Option<f32>,
40 #[serde(default)]
41 pub buffer_line_height: Option<BufferLineHeight>,
42 #[serde(default)]
43 pub buffer_font_features: Option<FontFeatures>,
44 #[serde(default)]
45 pub theme: Option<String>,
46}
47
48#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
49#[serde(rename_all = "snake_case")]
50pub enum BufferLineHeight {
51 #[default]
52 Comfortable,
53 Standard,
54 Custom(f32),
55}
56
57impl BufferLineHeight {
58 pub fn value(&self) -> f32 {
59 match self {
60 BufferLineHeight::Comfortable => 1.618,
61 BufferLineHeight::Standard => 1.3,
62 BufferLineHeight::Custom(line_height) => *line_height,
63 }
64 }
65}
66
67impl ThemeSettings {
68 pub fn buffer_font_size(&self, cx: &mut AppContext) -> Pixels {
69 let font_size = *cx
70 .default_global::<AdjustedBufferFontSize>()
71 .0
72 .get_or_insert(self.buffer_font_size.into());
73 font_size.max(MIN_FONT_SIZE)
74 }
75
76 pub fn line_height(&self) -> f32 {
77 f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
78 }
79}
80
81pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
82 if let Some(adjusted_size) = cx.default_global::<AdjustedBufferFontSize>().0 {
83 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
84 let delta = adjusted_size - buffer_font_size;
85 size + delta
86 } else {
87 size
88 }
89 .max(MIN_FONT_SIZE)
90}
91
92pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) {
93 let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
94 let adjusted_size = cx
95 .default_global::<AdjustedBufferFontSize>()
96 .0
97 .get_or_insert(buffer_font_size);
98 f(adjusted_size);
99 *adjusted_size = (*adjusted_size).max(MIN_FONT_SIZE - buffer_font_size);
100 cx.refresh();
101}
102
103pub fn reset_font_size(cx: &mut AppContext) {
104 if cx.has_global::<AdjustedBufferFontSize>() {
105 cx.global_mut::<AdjustedBufferFontSize>().0 = None;
106 cx.refresh();
107 }
108}
109
110impl settings::Settings for ThemeSettings {
111 const KEY: Option<&'static str> = None;
112
113 type FileContent = ThemeSettingsContent;
114
115 fn load(
116 defaults: &Self::FileContent,
117 user_values: &[&Self::FileContent],
118 cx: &mut AppContext,
119 ) -> Result<Self> {
120 let themes = cx.default_global::<ThemeRegistry>();
121
122 let mut this = Self {
123 ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(),
124 ui_font: Font {
125 family: "Helvetica".into(),
126 features: Default::default(),
127 weight: Default::default(),
128 style: Default::default(),
129 },
130 buffer_font: Font {
131 family: defaults.buffer_font_family.clone().unwrap().into(),
132 features: defaults.buffer_font_features.clone().unwrap(),
133 weight: FontWeight::default(),
134 style: FontStyle::default(),
135 },
136 buffer_font_size: defaults.buffer_font_size.unwrap().into(),
137 buffer_line_height: defaults.buffer_line_height.unwrap(),
138 active_theme: themes
139 .get(defaults.theme.as_ref().unwrap())
140 .or(themes.get(&one_dark().name))
141 .unwrap(),
142 };
143
144 for value in user_values.into_iter().copied().cloned() {
145 if let Some(value) = value.buffer_font_family {
146 this.buffer_font.family = value.into();
147 }
148 if let Some(value) = value.buffer_font_features {
149 this.buffer_font.features = value;
150 }
151
152 if let Some(value) = &value.theme {
153 if let Some(theme) = themes.get(value).log_err() {
154 this.active_theme = theme;
155 }
156 }
157
158 merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into));
159 merge(
160 &mut this.buffer_font_size,
161 value.buffer_font_size.map(Into::into),
162 );
163 merge(&mut this.buffer_line_height, value.buffer_line_height);
164 }
165
166 Ok(this)
167 }
168
169 fn json_schema(
170 generator: &mut SchemaGenerator,
171 params: &SettingsJsonSchemaParams,
172 cx: &AppContext,
173 ) -> schemars::schema::RootSchema {
174 let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
175 let theme_names = cx
176 .global::<Arc<ThemeRegistry>>()
177 .list_names(params.staff_mode)
178 .map(|theme_name| Value::String(theme_name.to_string()))
179 .collect();
180
181 let theme_name_schema = SchemaObject {
182 instance_type: Some(InstanceType::String.into()),
183 enum_values: Some(theme_names),
184 ..Default::default()
185 };
186
187 root_schema
188 .definitions
189 .extend([("ThemeName".into(), theme_name_schema.into())]);
190
191 root_schema
192 .schema
193 .object
194 .as_mut()
195 .unwrap()
196 .properties
197 .extend([(
198 "theme".to_owned(),
199 Schema::new_ref("#/definitions/ThemeName".into()),
200 )]);
201
202 root_schema
203 }
204}
205
206fn merge<T: Copy>(target: &mut T, value: Option<T>) {
207 if let Some(value) = value {
208 *target = value;
209 }
210}