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