appearance_settings_controls.rs

  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}