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    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}