appearance_settings_controls.rs

  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}