1use std::sync::Arc;
2
3use gpui::{App, FontFeatures, FontWeight};
4use settings::{EditableSettingControl, Settings};
5use theme::{FontFamilyCache, SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings};
6use ui::{
7 CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
8 ToggleButton, prelude::*,
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 ThemeSettings::get_global(cx).ui_font_size(cx)
243 }
244
245 fn apply(
246 settings: &mut <Self::Settings as Settings>::FileContent,
247 value: Self::Value,
248 _cx: &App,
249 ) {
250 settings.ui_font_size = Some(value.into());
251 }
252}
253
254impl RenderOnce for UiFontSizeControl {
255 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
256 let value = Self::read(cx);
257
258 h_flex()
259 .gap_2()
260 .child(Icon::new(IconName::FontSize))
261 .child(NumericStepper::new(
262 "ui-font-size",
263 value.to_string(),
264 move |_, _, cx| {
265 Self::write(value - px(1.), cx);
266 },
267 move |_, _, cx| {
268 Self::write(value + px(1.), cx);
269 },
270 ))
271 }
272}
273
274#[derive(IntoElement)]
275struct UiFontWeightControl;
276
277impl EditableSettingControl for UiFontWeightControl {
278 type Value = FontWeight;
279 type Settings = ThemeSettings;
280
281 fn name(&self) -> SharedString {
282 "UI Font Weight".into()
283 }
284
285 fn read(cx: &App) -> Self::Value {
286 let settings = ThemeSettings::get_global(cx);
287 settings.ui_font.weight
288 }
289
290 fn apply(
291 settings: &mut <Self::Settings as Settings>::FileContent,
292 value: Self::Value,
293 _cx: &App,
294 ) {
295 settings.ui_font_weight = Some(value.0);
296 }
297}
298
299impl RenderOnce for UiFontWeightControl {
300 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
301 let value = Self::read(cx);
302
303 h_flex()
304 .gap_2()
305 .child(Icon::new(IconName::FontWeight))
306 .child(DropdownMenu::new(
307 "ui-font-weight",
308 value.0.to_string(),
309 ContextMenu::build(window, cx, |mut menu, _window, _cx| {
310 for weight in FontWeight::ALL {
311 menu = menu.custom_entry(
312 move |_window, _cx| Label::new(weight.0.to_string()).into_any_element(),
313 {
314 move |_window, cx| {
315 Self::write(weight, cx);
316 }
317 },
318 )
319 }
320
321 menu
322 }),
323 ))
324 }
325}
326
327#[derive(IntoElement)]
328struct UiFontLigaturesControl;
329
330impl EditableSettingControl for UiFontLigaturesControl {
331 type Value = bool;
332 type Settings = ThemeSettings;
333
334 fn name(&self) -> SharedString {
335 "UI Font Ligatures".into()
336 }
337
338 fn read(cx: &App) -> Self::Value {
339 let settings = ThemeSettings::get_global(cx);
340 settings.ui_font.features.is_calt_enabled().unwrap_or(true)
341 }
342
343 fn apply(
344 settings: &mut <Self::Settings as Settings>::FileContent,
345 value: Self::Value,
346 _cx: &App,
347 ) {
348 let value = if value { 1 } else { 0 };
349
350 let mut features = settings
351 .ui_font_features
352 .as_ref()
353 .map(|features| features.tag_value_list().to_vec())
354 .unwrap_or_default();
355
356 if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
357 features[calt_index].1 = value;
358 } else {
359 features.push(("calt".into(), value));
360 }
361
362 settings.ui_font_features = Some(FontFeatures(Arc::new(features)));
363 }
364}
365
366impl RenderOnce for UiFontLigaturesControl {
367 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
368 let value = Self::read(cx);
369
370 CheckboxWithLabel::new(
371 "ui-font-ligatures",
372 Label::new(self.name()),
373 value.into(),
374 |selection, _, cx| {
375 Self::write(
376 match selection {
377 ToggleState::Selected => true,
378 ToggleState::Unselected | ToggleState::Indeterminate => false,
379 },
380 cx,
381 );
382 },
383 )
384 }
385}