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