basics_page.rs

  1use std::sync::Arc;
  2
  3use client::TelemetrySettings;
  4use fs::Fs;
  5use gpui::{Action, App, IntoElement};
  6use settings::{BaseKeymap, Settings, update_settings_file};
  7use theme::{
  8    Appearance, SystemAppearance, ThemeAppearanceMode, ThemeName, ThemeRegistry, ThemeSelection,
  9    ThemeSettings,
 10};
 11use ui::{
 12    Divider, ParentElement as _, StatefulInteractiveElement, SwitchField, TintColor,
 13    ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*,
 14    rems_from_px,
 15};
 16use vim_mode_setting::VimModeSetting;
 17
 18use crate::{
 19    ImportCursorSettings, ImportVsCodeSettings, SettingsImportState,
 20    theme_preview::{ThemePreviewStyle, ThemePreviewTile},
 21};
 22
 23const LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
 24const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
 25const FAMILY_NAMES: [SharedString; 3] = [
 26    SharedString::new_static("One"),
 27    SharedString::new_static("Ayu"),
 28    SharedString::new_static("Gruvbox"),
 29];
 30
 31fn get_theme_family_themes(theme_name: &str) -> Option<(&'static str, &'static str)> {
 32    for i in 0..LIGHT_THEMES.len() {
 33        if LIGHT_THEMES[i] == theme_name || DARK_THEMES[i] == theme_name {
 34            return Some((LIGHT_THEMES[i], DARK_THEMES[i]));
 35        }
 36    }
 37    None
 38}
 39
 40fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
 41    let theme_selection = ThemeSettings::get_global(cx).theme.clone();
 42    let system_appearance = theme::SystemAppearance::global(cx);
 43
 44    let theme_mode = theme_selection
 45        .mode()
 46        .unwrap_or_else(|| match *system_appearance {
 47            Appearance::Light => ThemeAppearanceMode::Light,
 48            Appearance::Dark => ThemeAppearanceMode::Dark,
 49        });
 50
 51    return v_flex()
 52        .gap_2()
 53        .child(
 54            h_flex().justify_between().child(Label::new("Theme")).child(
 55                ToggleButtonGroup::single_row(
 56                    "theme-selector-onboarding-dark-light",
 57                    [
 58                        ThemeAppearanceMode::Light,
 59                        ThemeAppearanceMode::Dark,
 60                        ThemeAppearanceMode::System,
 61                    ]
 62                    .map(|mode| {
 63                        const MODE_NAMES: [SharedString; 3] = [
 64                            SharedString::new_static("Light"),
 65                            SharedString::new_static("Dark"),
 66                            SharedString::new_static("System"),
 67                        ];
 68                        ToggleButtonSimple::new(
 69                            MODE_NAMES[mode as usize].clone(),
 70                            move |_, _, cx| {
 71                                write_mode_change(mode, cx);
 72
 73                                telemetry::event!(
 74                                    "Welcome Theme mode Changed",
 75                                    from = theme_mode,
 76                                    to = mode
 77                                );
 78                            },
 79                        )
 80                    }),
 81                )
 82                .size(ToggleButtonGroupSize::Medium)
 83                .tab_index(tab_index)
 84                .selected_index(theme_mode as usize)
 85                .style(ui::ToggleButtonGroupStyle::Outlined)
 86                .width(rems_from_px(3. * 64.)),
 87            ),
 88        )
 89        .child(
 90            h_flex()
 91                .gap_4()
 92                .justify_between()
 93                .children(render_theme_previews(tab_index, &theme_selection, cx)),
 94        );
 95
 96    fn render_theme_previews(
 97        tab_index: &mut isize,
 98        theme_selection: &ThemeSelection,
 99        cx: &mut App,
100    ) -> [impl IntoElement; 3] {
101        let system_appearance = SystemAppearance::global(cx);
102        let theme_registry = ThemeRegistry::global(cx);
103
104        let theme_seed = 0xBEEF as f32;
105        let theme_mode = theme_selection
106            .mode()
107            .unwrap_or_else(|| match *system_appearance {
108                Appearance::Light => ThemeAppearanceMode::Light,
109                Appearance::Dark => ThemeAppearanceMode::Dark,
110            });
111        let appearance = match theme_mode {
112            ThemeAppearanceMode::Light => Appearance::Light,
113            ThemeAppearanceMode::Dark => Appearance::Dark,
114            ThemeAppearanceMode::System => *system_appearance,
115        };
116        let current_theme_name: SharedString = theme_selection.name(appearance).0.into();
117
118        let theme_names = match appearance {
119            Appearance::Light => LIGHT_THEMES,
120            Appearance::Dark => DARK_THEMES,
121        };
122
123        let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap());
124
125        [0, 1, 2].map(|index| {
126            let theme = &themes[index];
127            let is_selected = theme.name == current_theme_name;
128            let name = theme.name.clone();
129            let colors = cx.theme().colors();
130
131            v_flex()
132                .w_full()
133                .items_center()
134                .gap_1()
135                .child(
136                    h_flex()
137                        .id(name)
138                        .relative()
139                        .w_full()
140                        .border_2()
141                        .border_color(colors.border_transparent)
142                        .rounded(ThemePreviewTile::ROOT_RADIUS)
143                        .map(|this| {
144                            if is_selected {
145                                this.border_color(colors.border_selected)
146                            } else {
147                                this.opacity(0.8).hover(|s| s.border_color(colors.border))
148                            }
149                        })
150                        .tab_index({
151                            *tab_index += 1;
152                            *tab_index - 1
153                        })
154                        .focus(|mut style| {
155                            style.border_color = Some(colors.border_focused);
156                            style
157                        })
158                        .on_click({
159                            let theme_name = theme.name.clone();
160                            let current_theme_name = current_theme_name.clone();
161
162                            move |_, _, cx| {
163                                write_theme_change(theme_name.clone(), theme_mode, cx);
164                                telemetry::event!(
165                                    "Welcome Theme Changed",
166                                    from = current_theme_name,
167                                    to = theme_name
168                                );
169                            }
170                        })
171                        .map(|this| {
172                            if theme_mode == ThemeAppearanceMode::System {
173                                let (light, dark) = (
174                                    theme_registry.get(LIGHT_THEMES[index]).unwrap(),
175                                    theme_registry.get(DARK_THEMES[index]).unwrap(),
176                                );
177                                this.child(
178                                    ThemePreviewTile::new(light, theme_seed)
179                                        .style(ThemePreviewStyle::SideBySide(dark)),
180                                )
181                            } else {
182                                this.child(
183                                    ThemePreviewTile::new(theme.clone(), theme_seed)
184                                        .style(ThemePreviewStyle::Bordered),
185                                )
186                            }
187                        }),
188                )
189                .child(
190                    Label::new(FAMILY_NAMES[index].clone())
191                        .color(Color::Muted)
192                        .size(LabelSize::Small),
193                )
194        })
195    }
196
197    fn write_mode_change(mode: ThemeAppearanceMode, cx: &mut App) {
198        let fs = <dyn Fs>::global(cx);
199        update_settings_file(fs, cx, move |settings, _cx| {
200            theme::set_mode(settings, mode);
201        });
202    }
203
204    fn write_theme_change(
205        theme: impl Into<Arc<str>>,
206        theme_mode: ThemeAppearanceMode,
207        cx: &mut App,
208    ) {
209        let fs = <dyn Fs>::global(cx);
210        let theme = theme.into();
211        update_settings_file(fs, cx, move |settings, cx| {
212            if theme_mode == ThemeAppearanceMode::System {
213                let (light_theme, dark_theme) =
214                    get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref()));
215
216                settings.theme.theme = Some(settings::ThemeSelection::Dynamic {
217                    mode: ThemeAppearanceMode::System,
218                    light: ThemeName(light_theme.into()),
219                    dark: ThemeName(dark_theme.into()),
220                });
221            } else {
222                let appearance = *SystemAppearance::global(cx);
223                theme::set_theme(settings, theme, appearance);
224            }
225        });
226    }
227}
228
229fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
230    let fs = <dyn Fs>::global(cx);
231
232    v_flex()
233        .gap_4()
234        .child(
235            SwitchField::new(
236                "onboarding-telemetry-metrics",
237                None::<&str>,
238                Some("Help improve Zed by sending anonymous usage data".into()),
239                if TelemetrySettings::get_global(cx).metrics {
240                    ui::ToggleState::Selected
241                } else {
242                    ui::ToggleState::Unselected
243                },
244                {
245                    let fs = fs.clone();
246                    move |selection, _, cx| {
247                        let enabled = match selection {
248                            ToggleState::Selected => true,
249                            ToggleState::Unselected => false,
250                            ToggleState::Indeterminate => {
251                                return;
252                            }
253                        };
254
255                        update_settings_file(fs.clone(), cx, move |setting, _| {
256                            setting.telemetry.get_or_insert_default().metrics = Some(enabled);
257                        });
258
259                        // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
260                        // and can fix it in a timely manner to respect a user's choice.
261                        telemetry::event!(
262                            "Welcome Page Telemetry Metrics Toggled",
263                            options = if enabled { "on" } else { "off" }
264                        );
265                    }
266                },
267            )
268            .tab_index({
269                *tab_index += 1;
270                *tab_index
271            }),
272        )
273        .child(
274            SwitchField::new(
275                "onboarding-telemetry-crash-reports",
276                None::<&str>,
277                Some(
278                    "Help fix Zed by sending crash reports so we can fix critical issues fast"
279                        .into(),
280                ),
281                if TelemetrySettings::get_global(cx).diagnostics {
282                    ui::ToggleState::Selected
283                } else {
284                    ui::ToggleState::Unselected
285                },
286                {
287                    let fs = fs.clone();
288                    move |selection, _, cx| {
289                        let enabled = match selection {
290                            ToggleState::Selected => true,
291                            ToggleState::Unselected => false,
292                            ToggleState::Indeterminate => {
293                                return;
294                            }
295                        };
296
297                        update_settings_file(fs.clone(), cx, move |setting, _| {
298                            setting.telemetry.get_or_insert_default().diagnostics = Some(enabled);
299                        });
300
301                        // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
302                        // and can fix it in a timely manner to respect a user's choice.
303                        telemetry::event!(
304                            "Welcome Page Telemetry Diagnostics Toggled",
305                            options = if enabled { "on" } else { "off" }
306                        );
307                    }
308                },
309            )
310            .tab_index({
311                *tab_index += 1;
312                *tab_index
313            }),
314        )
315}
316
317fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
318    let base_keymap = match BaseKeymap::get_global(cx) {
319        BaseKeymap::VSCode => Some(0),
320        BaseKeymap::JetBrains => Some(1),
321        BaseKeymap::SublimeText => Some(2),
322        BaseKeymap::Atom => Some(3),
323        BaseKeymap::Emacs => Some(4),
324        BaseKeymap::Cursor => Some(5),
325        BaseKeymap::TextMate | BaseKeymap::None => None,
326    };
327
328    return v_flex().gap_2().child(Label::new("Base Keymap")).child(
329        ToggleButtonGroup::two_rows(
330            "base_keymap_selection",
331            [
332                ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| {
333                    write_keymap_base(BaseKeymap::VSCode, cx);
334                }),
335                ToggleButtonWithIcon::new("JetBrains", IconName::EditorJetBrains, |_, _, cx| {
336                    write_keymap_base(BaseKeymap::JetBrains, cx);
337                }),
338                ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| {
339                    write_keymap_base(BaseKeymap::SublimeText, cx);
340                }),
341            ],
342            [
343                ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| {
344                    write_keymap_base(BaseKeymap::Atom, cx);
345                }),
346                ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
347                    write_keymap_base(BaseKeymap::Emacs, cx);
348                }),
349                ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| {
350                    write_keymap_base(BaseKeymap::Cursor, cx);
351                }),
352            ],
353        )
354        .when_some(base_keymap, |this, base_keymap| {
355            this.selected_index(base_keymap)
356        })
357        .full_width()
358        .tab_index(tab_index)
359        .size(ui::ToggleButtonGroupSize::Medium)
360        .style(ui::ToggleButtonGroupStyle::Outlined),
361    );
362
363    fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
364        let fs = <dyn Fs>::global(cx);
365
366        update_settings_file(fs, cx, move |setting, _| {
367            setting.base_keymap = Some(keymap_base.into());
368        });
369
370        telemetry::event!("Welcome Keymap Changed", keymap = keymap_base);
371    }
372}
373
374fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
375    let toggle_state = if VimModeSetting::get_global(cx).0 {
376        ui::ToggleState::Selected
377    } else {
378        ui::ToggleState::Unselected
379    };
380    SwitchField::new(
381        "onboarding-vim-mode",
382        Some("Vim Mode"),
383        Some("Coming from Neovim? Use our first-class implementation of Vim Mode".into()),
384        toggle_state,
385        {
386            let fs = <dyn Fs>::global(cx);
387            move |&selection, _, cx| {
388                let vim_mode = match selection {
389                    ToggleState::Selected => true,
390                    ToggleState::Unselected => false,
391                    ToggleState::Indeterminate => {
392                        return;
393                    }
394                };
395                update_settings_file(fs.clone(), cx, move |setting, _| {
396                    setting.vim_mode = Some(vim_mode);
397                });
398
399                telemetry::event!(
400                    "Welcome Vim Mode Toggled",
401                    options = if vim_mode { "on" } else { "off" },
402                );
403            }
404        },
405    )
406    .tab_index({
407        *tab_index += 1;
408        *tab_index - 1
409    })
410}
411
412fn render_setting_import_button(
413    tab_index: isize,
414    label: SharedString,
415    action: &dyn Action,
416    imported: bool,
417) -> impl IntoElement + 'static {
418    let action = action.boxed_clone();
419
420    Button::new(label.clone(), label.clone())
421        .style(ButtonStyle::OutlinedGhost)
422        .size(ButtonSize::Medium)
423        .label_size(LabelSize::Small)
424        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
425        .toggle_state(imported)
426        .tab_index(tab_index)
427        .when(imported, |this| {
428            this.icon(IconName::Check)
429                .icon_size(IconSize::Small)
430                .color(Color::Success)
431        })
432        .on_click(move |_, window, cx| {
433            telemetry::event!("Welcome Import Settings", import_source = label,);
434            window.dispatch_action(action.boxed_clone(), cx);
435        })
436}
437
438fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
439    let import_state = SettingsImportState::global(cx);
440    let imports: [(SharedString, &dyn Action, bool); 2] = [
441        (
442            "VS Code".into(),
443            &ImportVsCodeSettings { skip_prompt: false },
444            import_state.vscode,
445        ),
446        (
447            "Cursor".into(),
448            &ImportCursorSettings { skip_prompt: false },
449            import_state.cursor,
450        ),
451    ];
452
453    let [vscode, cursor] = imports.map(|(label, action, imported)| {
454        *tab_index += 1;
455        render_setting_import_button(*tab_index - 1, label, action, imported)
456    });
457
458    h_flex()
459        .gap_2()
460        .flex_wrap()
461        .justify_between()
462        .child(
463            v_flex()
464                .gap_0p5()
465                .max_w_5_6()
466                .child(Label::new("Import Settings"))
467                .child(
468                    Label::new("Automatically pull your settings from other editors")
469                        .color(Color::Muted),
470                ),
471        )
472        .child(h_flex().gap_1().child(vscode).child(cursor))
473}
474
475pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement {
476    let mut tab_index = 0;
477    v_flex()
478        .id("basics-page")
479        .gap_6()
480        .child(render_theme_section(&mut tab_index, cx))
481        .child(render_base_keymap_section(&mut tab_index, cx))
482        .child(render_import_settings_section(&mut tab_index, cx))
483        .child(render_vim_mode_switch(&mut tab_index, cx))
484        .child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
485        .child(render_telemetry_section(&mut tab_index, cx))
486}