1use std::sync::Arc;
2
3use gpui::{App, FontFeatures, FontWeight};
4use settings::{EditableSettingControl, Settings};
5use theme::{FontFamilyCache, SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings};
6use ui::{
7 prelude::*, CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer,
8 SettingsGroup, ToggleButton,
9};
10
11#[derive(IntoElement)]
12pub struct AppearanceSettingsControls {}
13
14impl AppearanceSettingsControls {
15 pub fn new() -> Self {
16 Self {}
17 }
18}
19
20impl RenderOnce for AppearanceSettingsControls {
21 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
22 SettingsContainer::new()
23 .child(
24 SettingsGroup::new("Theme").child(
25 h_flex()
26 .gap_2()
27 .justify_between()
28 .child(ThemeControl)
29 .child(ThemeModeControl),
30 ),
31 )
32 .child(
33 SettingsGroup::new("Font")
34 .child(
35 h_flex()
36 .gap_2()
37 .justify_between()
38 .child(UiFontFamilyControl)
39 .child(UiFontWeightControl),
40 )
41 .child(UiFontSizeControl)
42 .child(UiFontLigaturesControl),
43 )
44 }
45}
46
47#[derive(IntoElement)]
48struct ThemeControl;
49
50impl EditableSettingControl for ThemeControl {
51 type Value = String;
52 type Settings = ThemeSettings;
53
54 fn name(&self) -> SharedString {
55 "Theme".into()
56 }
57
58 fn read(cx: &App) -> Self::Value {
59 let settings = ThemeSettings::get_global(cx);
60 let appearance = SystemAppearance::global(cx);
61 settings
62 .theme_selection
63 .as_ref()
64 .map(|selection| selection.theme(appearance.0).to_string())
65 .unwrap_or_else(|| ThemeSettings::default_theme(*appearance).to_string())
66 }
67
68 fn apply(
69 settings: &mut <Self::Settings as Settings>::FileContent,
70 value: Self::Value,
71 cx: &App,
72 ) {
73 let appearance = SystemAppearance::global(cx);
74 settings.set_theme(value, appearance.0);
75 }
76}
77
78impl RenderOnce for ThemeControl {
79 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
80 let value = Self::read(cx);
81
82 DropdownMenu::new(
83 "theme",
84 value.clone(),
85 ContextMenu::build(window, cx, |mut menu, _, cx| {
86 let theme_registry = ThemeRegistry::global(cx);
87
88 for theme in theme_registry.list_names() {
89 menu = menu.custom_entry(
90 {
91 let theme = theme.clone();
92 move |_window, _cx| Label::new(theme.clone()).into_any_element()
93 },
94 {
95 let theme = theme.clone();
96 move |_window, cx| {
97 Self::write(theme.to_string(), cx);
98 }
99 },
100 )
101 }
102
103 menu
104 }),
105 )
106 .full_width(true)
107 }
108}
109
110#[derive(IntoElement)]
111struct ThemeModeControl;
112
113impl EditableSettingControl for ThemeModeControl {
114 type Value = ThemeMode;
115 type Settings = ThemeSettings;
116
117 fn name(&self) -> SharedString {
118 "Theme Mode".into()
119 }
120
121 fn read(cx: &App) -> Self::Value {
122 let settings = ThemeSettings::get_global(cx);
123 settings
124 .theme_selection
125 .as_ref()
126 .and_then(|selection| selection.mode())
127 .unwrap_or_default()
128 }
129
130 fn apply(
131 settings: &mut <Self::Settings as Settings>::FileContent,
132 value: Self::Value,
133 _cx: &App,
134 ) {
135 settings.set_mode(value);
136 }
137}
138
139impl RenderOnce for ThemeModeControl {
140 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
141 let value = Self::read(cx);
142
143 h_flex()
144 .child(
145 ToggleButton::new("light", "Light")
146 .style(ButtonStyle::Filled)
147 .size(ButtonSize::Large)
148 .toggle_state(value == ThemeMode::Light)
149 .on_click(|_, _, cx| Self::write(ThemeMode::Light, cx))
150 .first(),
151 )
152 .child(
153 ToggleButton::new("system", "System")
154 .style(ButtonStyle::Filled)
155 .size(ButtonSize::Large)
156 .toggle_state(value == ThemeMode::System)
157 .on_click(|_, _, cx| Self::write(ThemeMode::System, cx))
158 .middle(),
159 )
160 .child(
161 ToggleButton::new("dark", "Dark")
162 .style(ButtonStyle::Filled)
163 .size(ButtonSize::Large)
164 .toggle_state(value == ThemeMode::Dark)
165 .on_click(|_, _, cx| Self::write(ThemeMode::Dark, cx))
166 .last(),
167 )
168 }
169}
170
171#[derive(IntoElement)]
172struct UiFontFamilyControl;
173
174impl EditableSettingControl for UiFontFamilyControl {
175 type Value = SharedString;
176 type Settings = ThemeSettings;
177
178 fn name(&self) -> SharedString {
179 "UI Font Family".into()
180 }
181
182 fn read(cx: &App) -> Self::Value {
183 let settings = ThemeSettings::get_global(cx);
184 settings.ui_font.family.clone()
185 }
186
187 fn apply(
188 settings: &mut <Self::Settings as Settings>::FileContent,
189 value: Self::Value,
190 _cx: &App,
191 ) {
192 settings.ui_font_family = Some(value.to_string());
193 }
194}
195
196impl RenderOnce for UiFontFamilyControl {
197 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
198 let value = Self::read(cx);
199
200 h_flex()
201 .gap_2()
202 .child(Icon::new(IconName::Font))
203 .child(DropdownMenu::new(
204 "ui-font-family",
205 value.clone(),
206 ContextMenu::build(window, cx, |mut menu, _, cx| {
207 let font_family_cache = FontFamilyCache::global(cx);
208
209 for font_name in font_family_cache.list_font_families(cx) {
210 menu = menu.custom_entry(
211 {
212 let font_name = font_name.clone();
213 move |_window, _cx| Label::new(font_name.clone()).into_any_element()
214 },
215 {
216 let font_name = font_name.clone();
217 move |_window, cx| {
218 Self::write(font_name.clone(), cx);
219 }
220 },
221 )
222 }
223
224 menu
225 }),
226 ))
227 }
228}
229
230#[derive(IntoElement)]
231struct UiFontSizeControl;
232
233impl EditableSettingControl for UiFontSizeControl {
234 type Value = Pixels;
235 type Settings = ThemeSettings;
236
237 fn name(&self) -> SharedString {
238 "UI Font Size".into()
239 }
240
241 fn read(cx: &App) -> Self::Value {
242 let settings = ThemeSettings::get_global(cx);
243 settings.ui_font_size
244 }
245
246 fn apply(
247 settings: &mut <Self::Settings as Settings>::FileContent,
248 value: Self::Value,
249 _cx: &App,
250 ) {
251 settings.ui_font_size = Some(value.into());
252 }
253}
254
255impl RenderOnce for UiFontSizeControl {
256 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
257 let value = Self::read(cx);
258
259 h_flex()
260 .gap_2()
261 .child(Icon::new(IconName::FontSize))
262 .child(NumericStepper::new(
263 "ui-font-size",
264 value.to_string(),
265 move |_, _, cx| {
266 Self::write(value - px(1.), cx);
267 },
268 move |_, _, cx| {
269 Self::write(value + px(1.), cx);
270 },
271 ))
272 }
273}
274
275#[derive(IntoElement)]
276struct UiFontWeightControl;
277
278impl EditableSettingControl for UiFontWeightControl {
279 type Value = FontWeight;
280 type Settings = ThemeSettings;
281
282 fn name(&self) -> SharedString {
283 "UI Font Weight".into()
284 }
285
286 fn read(cx: &App) -> Self::Value {
287 let settings = ThemeSettings::get_global(cx);
288 settings.ui_font.weight
289 }
290
291 fn apply(
292 settings: &mut <Self::Settings as Settings>::FileContent,
293 value: Self::Value,
294 _cx: &App,
295 ) {
296 settings.ui_font_weight = Some(value.0);
297 }
298}
299
300impl RenderOnce for UiFontWeightControl {
301 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
302 let value = Self::read(cx);
303
304 h_flex()
305 .gap_2()
306 .child(Icon::new(IconName::FontWeight))
307 .child(DropdownMenu::new(
308 "ui-font-weight",
309 value.0.to_string(),
310 ContextMenu::build(window, cx, |mut menu, _window, _cx| {
311 for weight in FontWeight::ALL {
312 menu = menu.custom_entry(
313 move |_window, _cx| Label::new(weight.0.to_string()).into_any_element(),
314 {
315 move |_window, cx| {
316 Self::write(weight, cx);
317 }
318 },
319 )
320 }
321
322 menu
323 }),
324 ))
325 }
326}
327
328#[derive(IntoElement)]
329struct UiFontLigaturesControl;
330
331impl EditableSettingControl for UiFontLigaturesControl {
332 type Value = bool;
333 type Settings = ThemeSettings;
334
335 fn name(&self) -> SharedString {
336 "UI Font Ligatures".into()
337 }
338
339 fn read(cx: &App) -> Self::Value {
340 let settings = ThemeSettings::get_global(cx);
341 settings.ui_font.features.is_calt_enabled().unwrap_or(true)
342 }
343
344 fn apply(
345 settings: &mut <Self::Settings as Settings>::FileContent,
346 value: Self::Value,
347 _cx: &App,
348 ) {
349 let value = if value { 1 } else { 0 };
350
351 let mut features = settings
352 .ui_font_features
353 .as_ref()
354 .map(|features| features.tag_value_list().to_vec())
355 .unwrap_or_default();
356
357 if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
358 features[calt_index].1 = value;
359 } else {
360 features.push(("calt".into(), value));
361 }
362
363 settings.ui_font_features = Some(FontFeatures(Arc::new(features)));
364 }
365}
366
367impl RenderOnce for UiFontLigaturesControl {
368 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
369 let value = Self::read(cx);
370
371 CheckboxWithLabel::new(
372 "ui-font-ligatures",
373 Label::new(self.name()),
374 value.into(),
375 |selection, _, cx| {
376 Self::write(
377 match selection {
378 ToggleState::Selected => true,
379 ToggleState::Unselected | ToggleState::Indeterminate => false,
380 },
381 cx,
382 );
383 },
384 )
385 }
386}