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