basics_page.rs

  1use std::sync::Arc;
  2
  3use client::TelemetrySettings;
  4use fs::Fs;
  5use gpui::{App, IntoElement};
  6use settings::{BaseKeymap, Settings, update_settings_file};
  7use theme::{
  8    Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection,
  9    ThemeSettings,
 10};
 11use ui::{
 12    ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup,
 13    ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px,
 14};
 15use vim_mode_setting::VimModeSetting;
 16
 17use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile};
 18
 19const LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
 20const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
 21const FAMILY_NAMES: [SharedString; 3] = [
 22    SharedString::new_static("One"),
 23    SharedString::new_static("Ayu"),
 24    SharedString::new_static("Gruvbox"),
 25];
 26
 27fn get_theme_family_themes(theme_name: &str) -> Option<(&'static str, &'static str)> {
 28    for i in 0..LIGHT_THEMES.len() {
 29        if LIGHT_THEMES[i] == theme_name || DARK_THEMES[i] == theme_name {
 30            return Some((LIGHT_THEMES[i], DARK_THEMES[i]));
 31        }
 32    }
 33    None
 34}
 35
 36fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
 37    let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone();
 38    let system_appearance = theme::SystemAppearance::global(cx);
 39    let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic {
 40        mode: match *system_appearance {
 41            Appearance::Light => ThemeMode::Light,
 42            Appearance::Dark => ThemeMode::Dark,
 43        },
 44        light: ThemeName("One Light".into()),
 45        dark: ThemeName("One Dark".into()),
 46    });
 47
 48    let theme_mode = theme_selection
 49        .mode()
 50        .unwrap_or_else(|| match *system_appearance {
 51            Appearance::Light => ThemeMode::Light,
 52            Appearance::Dark => ThemeMode::Dark,
 53        });
 54
 55    return v_flex()
 56        .gap_2()
 57        .child(
 58            h_flex().justify_between().child(Label::new("Theme")).child(
 59                ToggleButtonGroup::single_row(
 60                    "theme-selector-onboarding-dark-light",
 61                    [ThemeMode::Light, ThemeMode::Dark, ThemeMode::System].map(|mode| {
 62                        const MODE_NAMES: [SharedString; 3] = [
 63                            SharedString::new_static("Light"),
 64                            SharedString::new_static("Dark"),
 65                            SharedString::new_static("System"),
 66                        ];
 67                        ToggleButtonSimple::new(
 68                            MODE_NAMES[mode as usize].clone(),
 69                            move |_, _, cx| {
 70                                write_mode_change(mode, cx);
 71                            },
 72                        )
 73                    }),
 74                )
 75                .tab_index(tab_index)
 76                .selected_index(theme_mode as usize)
 77                .style(ui::ToggleButtonGroupStyle::Outlined)
 78                .width(rems_from_px(3. * 64.)),
 79            ),
 80        )
 81        .child(
 82            h_flex()
 83                .gap_4()
 84                .justify_between()
 85                .children(render_theme_previews(tab_index, &theme_selection, cx)),
 86        );
 87
 88    fn render_theme_previews(
 89        tab_index: &mut isize,
 90        theme_selection: &ThemeSelection,
 91        cx: &mut App,
 92    ) -> [impl IntoElement; 3] {
 93        let system_appearance = SystemAppearance::global(cx);
 94        let theme_registry = ThemeRegistry::global(cx);
 95
 96        let theme_seed = 0xBEEF as f32;
 97        let theme_mode = theme_selection
 98            .mode()
 99            .unwrap_or_else(|| match *system_appearance {
100                Appearance::Light => ThemeMode::Light,
101                Appearance::Dark => ThemeMode::Dark,
102            });
103        let appearance = match theme_mode {
104            ThemeMode::Light => Appearance::Light,
105            ThemeMode::Dark => Appearance::Dark,
106            ThemeMode::System => *system_appearance,
107        };
108        let current_theme_name = theme_selection.theme(appearance);
109
110        let theme_names = match appearance {
111            Appearance::Light => LIGHT_THEMES,
112            Appearance::Dark => DARK_THEMES,
113        };
114
115        let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap());
116
117        [0, 1, 2].map(|index| {
118            let theme = &themes[index];
119            let is_selected = theme.name == current_theme_name;
120            let name = theme.name.clone();
121            let colors = cx.theme().colors();
122
123            v_flex()
124                .w_full()
125                .items_center()
126                .gap_1()
127                .child(
128                    h_flex()
129                        .id(name)
130                        .relative()
131                        .w_full()
132                        .border_2()
133                        .border_color(colors.border_transparent)
134                        .rounded(ThemePreviewTile::ROOT_RADIUS)
135                        .map(|this| {
136                            if is_selected {
137                                this.border_color(colors.border_selected)
138                            } else {
139                                this.opacity(0.8).hover(|s| s.border_color(colors.border))
140                            }
141                        })
142                        .tab_index({
143                            *tab_index += 1;
144                            *tab_index - 1
145                        })
146                        .focus(|mut style| {
147                            style.border_color = Some(colors.border_focused);
148                            style
149                        })
150                        .on_click({
151                            let theme_name = theme.name.clone();
152                            move |_, _, cx| {
153                                write_theme_change(theme_name.clone(), theme_mode, cx);
154                            }
155                        })
156                        .map(|this| {
157                            if theme_mode == ThemeMode::System {
158                                let (light, dark) = (
159                                    theme_registry.get(LIGHT_THEMES[index]).unwrap(),
160                                    theme_registry.get(DARK_THEMES[index]).unwrap(),
161                                );
162                                this.child(
163                                    ThemePreviewTile::new(light, theme_seed)
164                                        .style(ThemePreviewStyle::SideBySide(dark)),
165                                )
166                            } else {
167                                this.child(
168                                    ThemePreviewTile::new(theme.clone(), theme_seed)
169                                        .style(ThemePreviewStyle::Bordered),
170                                )
171                            }
172                        }),
173                )
174                .child(
175                    Label::new(FAMILY_NAMES[index].clone())
176                        .color(Color::Muted)
177                        .size(LabelSize::Small),
178                )
179        })
180    }
181
182    fn write_mode_change(mode: ThemeMode, cx: &mut App) {
183        let fs = <dyn Fs>::global(cx);
184        update_settings_file::<ThemeSettings>(fs, cx, move |settings, _cx| {
185            settings.set_mode(mode);
186        });
187    }
188
189    fn write_theme_change(theme: impl Into<Arc<str>>, theme_mode: ThemeMode, cx: &mut App) {
190        let fs = <dyn Fs>::global(cx);
191        let theme = theme.into();
192        update_settings_file::<ThemeSettings>(fs, cx, move |settings, cx| {
193            if theme_mode == ThemeMode::System {
194                let (light_theme, dark_theme) =
195                    get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref()));
196
197                settings.theme = Some(ThemeSelection::Dynamic {
198                    mode: ThemeMode::System,
199                    light: ThemeName(light_theme.into()),
200                    dark: ThemeName(dark_theme.into()),
201                });
202            } else {
203                let appearance = *SystemAppearance::global(cx);
204                settings.set_theme(theme, appearance);
205            }
206        });
207    }
208}
209
210fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
211    let fs = <dyn Fs>::global(cx);
212
213    v_flex()
214        .pt_6()
215        .gap_4()
216        .border_t_1()
217        .border_color(cx.theme().colors().border_variant.opacity(0.5))
218        .child(Label::new("Telemetry").size(LabelSize::Large))
219        .child(SwitchField::new(
220            "onboarding-telemetry-metrics",
221            "Help Improve Zed",
222            Some("Anonymous usage data helps us build the right features and improve your experience.".into()),
223            if TelemetrySettings::get_global(cx).metrics {
224                ui::ToggleState::Selected
225            } else {
226                ui::ToggleState::Unselected
227            },
228            {
229            let fs = fs.clone();
230            move |selection, _, cx| {
231                let enabled = match selection {
232                    ToggleState::Selected => true,
233                    ToggleState::Unselected => false,
234                    ToggleState::Indeterminate => { return; },
235                };
236
237                update_settings_file::<TelemetrySettings>(
238                    fs.clone(),
239                    cx,
240                    move |setting, _| setting.metrics = Some(enabled),
241                );
242            }},
243        ).tab_index({
244            *tab_index += 1;
245            *tab_index
246        }))
247        .child(SwitchField::new(
248            "onboarding-telemetry-crash-reports",
249            "Help Fix Zed",
250            Some("Send crash reports so we can fix critical issues fast.".into()),
251            if TelemetrySettings::get_global(cx).diagnostics {
252                ui::ToggleState::Selected
253            } else {
254                ui::ToggleState::Unselected
255            },
256            {
257                let fs = fs.clone();
258                move |selection, _, cx| {
259                    let enabled = match selection {
260                        ToggleState::Selected => true,
261                        ToggleState::Unselected => false,
262                        ToggleState::Indeterminate => { return; },
263                    };
264
265                    update_settings_file::<TelemetrySettings>(
266                        fs.clone(),
267                        cx,
268                        move |setting, _| setting.diagnostics = Some(enabled),
269                    );
270                }
271            }
272        ).tab_index({
273                    *tab_index += 1;
274                    *tab_index
275                }))
276}
277
278fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
279    let base_keymap = match BaseKeymap::get_global(cx) {
280        BaseKeymap::VSCode => Some(0),
281        BaseKeymap::JetBrains => Some(1),
282        BaseKeymap::SublimeText => Some(2),
283        BaseKeymap::Atom => Some(3),
284        BaseKeymap::Emacs => Some(4),
285        BaseKeymap::Cursor => Some(5),
286        BaseKeymap::TextMate | BaseKeymap::None => None,
287    };
288
289    return v_flex().gap_2().child(Label::new("Base Keymap")).child(
290        ToggleButtonGroup::two_rows(
291            "base_keymap_selection",
292            [
293                ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| {
294                    write_keymap_base(BaseKeymap::VSCode, cx);
295                }),
296                ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| {
297                    write_keymap_base(BaseKeymap::JetBrains, cx);
298                }),
299                ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| {
300                    write_keymap_base(BaseKeymap::SublimeText, cx);
301                }),
302            ],
303            [
304                ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| {
305                    write_keymap_base(BaseKeymap::Atom, cx);
306                }),
307                ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
308                    write_keymap_base(BaseKeymap::Emacs, cx);
309                }),
310                ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| {
311                    write_keymap_base(BaseKeymap::Cursor, cx);
312                }),
313            ],
314        )
315        .when_some(base_keymap, |this, base_keymap| {
316            this.selected_index(base_keymap)
317        })
318        .full_width()
319        .tab_index(tab_index)
320        .size(ui::ToggleButtonGroupSize::Medium)
321        .style(ui::ToggleButtonGroupStyle::Outlined),
322    );
323
324    fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
325        let fs = <dyn Fs>::global(cx);
326
327        update_settings_file::<BaseKeymap>(fs, cx, move |setting, _| {
328            setting.base_keymap = Some(keymap_base);
329        });
330    }
331}
332
333fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
334    let toggle_state = if VimModeSetting::get_global(cx).0 {
335        ui::ToggleState::Selected
336    } else {
337        ui::ToggleState::Unselected
338    };
339    SwitchField::new(
340        "onboarding-vim-mode",
341        "Vim Mode",
342        Some("Coming from Neovim? Use our first-class implementation of Vim Mode.".into()),
343        toggle_state,
344        {
345            let fs = <dyn Fs>::global(cx);
346            move |&selection, _, cx| {
347                update_settings_file::<VimModeSetting>(fs.clone(), cx, move |setting, _| {
348                    *setting = match selection {
349                        ToggleState::Selected => Some(true),
350                        ToggleState::Unselected => Some(false),
351                        ToggleState::Indeterminate => None,
352                    }
353                });
354            }
355        },
356    )
357    .tab_index({
358        *tab_index += 1;
359        *tab_index - 1
360    })
361}
362
363pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement {
364    let mut tab_index = 0;
365    v_flex()
366        .gap_6()
367        .child(render_theme_section(&mut tab_index, cx))
368        .child(render_base_keymap_section(&mut tab_index, cx))
369        .child(render_vim_mode_switch(&mut tab_index, cx))
370        .child(render_telemetry_section(&mut tab_index, cx))
371}