basics_page.rs

  1use std::sync::Arc;
  2
  3use client::TelemetrySettings;
  4use fs::Fs;
  5use gpui::{Action, App, IntoElement};
  6use project::project_settings::ProjectSettings;
  7use settings::{BaseKeymap, Settings, update_settings_file};
  8use theme::{
  9    Appearance, SystemAppearance, ThemeAppearanceMode, ThemeName, ThemeRegistry, ThemeSelection,
 10    ThemeSettings,
 11};
 12use ui::{
 13    Divider, StatefulInteractiveElement, SwitchField, TintColor, ToggleButtonGroup,
 14    ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, Tooltip, prelude::*,
 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| match theme_mode {
212            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            }
222            ThemeAppearanceMode::Light => theme::set_theme(
223                settings,
224                theme,
225                Appearance::Light,
226                *SystemAppearance::global(cx),
227            ),
228            ThemeAppearanceMode::Dark => theme::set_theme(
229                settings,
230                theme,
231                Appearance::Dark,
232                *SystemAppearance::global(cx),
233            ),
234        });
235    }
236}
237
238fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
239    let fs = <dyn Fs>::global(cx);
240
241    v_flex()
242        .gap_4()
243        .child(
244            SwitchField::new(
245                "onboarding-telemetry-metrics",
246                None::<&str>,
247                Some("Help improve Zed by sending anonymous usage data".into()),
248                if TelemetrySettings::get_global(cx).metrics {
249                    ui::ToggleState::Selected
250                } else {
251                    ui::ToggleState::Unselected
252                },
253                {
254                    let fs = fs.clone();
255                    move |selection, _, cx| {
256                        let enabled = match selection {
257                            ToggleState::Selected => true,
258                            ToggleState::Unselected => false,
259                            ToggleState::Indeterminate => {
260                                return;
261                            }
262                        };
263
264                        update_settings_file(fs.clone(), cx, move |setting, _| {
265                            setting.telemetry.get_or_insert_default().metrics = Some(enabled);
266                        });
267
268                        // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
269                        // and can fix it in a timely manner to respect a user's choice.
270                        telemetry::event!(
271                            "Welcome Page Telemetry Metrics Toggled",
272                            options = if enabled { "on" } else { "off" }
273                        );
274                    }
275                },
276            )
277            .tab_index({
278                *tab_index += 1;
279                *tab_index
280            }),
281        )
282        .child(
283            SwitchField::new(
284                "onboarding-telemetry-crash-reports",
285                None::<&str>,
286                Some(
287                    "Help fix Zed by sending crash reports so we can fix critical issues fast"
288                        .into(),
289                ),
290                if TelemetrySettings::get_global(cx).diagnostics {
291                    ui::ToggleState::Selected
292                } else {
293                    ui::ToggleState::Unselected
294                },
295                {
296                    let fs = fs.clone();
297                    move |selection, _, cx| {
298                        let enabled = match selection {
299                            ToggleState::Selected => true,
300                            ToggleState::Unselected => false,
301                            ToggleState::Indeterminate => {
302                                return;
303                            }
304                        };
305
306                        update_settings_file(fs.clone(), cx, move |setting, _| {
307                            setting.telemetry.get_or_insert_default().diagnostics = Some(enabled);
308                        });
309
310                        // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
311                        // and can fix it in a timely manner to respect a user's choice.
312                        telemetry::event!(
313                            "Welcome Page Telemetry Diagnostics Toggled",
314                            options = if enabled { "on" } else { "off" }
315                        );
316                    }
317                },
318            )
319            .tab_index({
320                *tab_index += 1;
321                *tab_index
322            }),
323        )
324}
325
326fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
327    let base_keymap = match BaseKeymap::get_global(cx) {
328        BaseKeymap::VSCode => Some(0),
329        BaseKeymap::JetBrains => Some(1),
330        BaseKeymap::SublimeText => Some(2),
331        BaseKeymap::Atom => Some(3),
332        BaseKeymap::Emacs => Some(4),
333        BaseKeymap::Cursor => Some(5),
334        BaseKeymap::TextMate | BaseKeymap::None => None,
335    };
336
337    return v_flex().gap_2().child(Label::new("Base Keymap")).child(
338        ToggleButtonGroup::two_rows(
339            "base_keymap_selection",
340            [
341                ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| {
342                    write_keymap_base(BaseKeymap::VSCode, cx);
343                }),
344                ToggleButtonWithIcon::new("JetBrains", IconName::EditorJetBrains, |_, _, cx| {
345                    write_keymap_base(BaseKeymap::JetBrains, cx);
346                }),
347                ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| {
348                    write_keymap_base(BaseKeymap::SublimeText, cx);
349                }),
350            ],
351            [
352                ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| {
353                    write_keymap_base(BaseKeymap::Atom, cx);
354                }),
355                ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
356                    write_keymap_base(BaseKeymap::Emacs, cx);
357                }),
358                ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| {
359                    write_keymap_base(BaseKeymap::Cursor, cx);
360                }),
361            ],
362        )
363        .when_some(base_keymap, |this, base_keymap| {
364            this.selected_index(base_keymap)
365        })
366        .full_width()
367        .tab_index(tab_index)
368        .size(ui::ToggleButtonGroupSize::Medium)
369        .style(ui::ToggleButtonGroupStyle::Outlined),
370    );
371
372    fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
373        let fs = <dyn Fs>::global(cx);
374
375        update_settings_file(fs, cx, move |setting, _| {
376            setting.base_keymap = Some(keymap_base.into());
377        });
378
379        telemetry::event!("Welcome Keymap Changed", keymap = keymap_base);
380    }
381}
382
383fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
384    let toggle_state = if VimModeSetting::get_global(cx).0 {
385        ui::ToggleState::Selected
386    } else {
387        ui::ToggleState::Unselected
388    };
389    SwitchField::new(
390        "onboarding-vim-mode",
391        Some("Vim Mode"),
392        Some("Coming from Neovim? Use our first-class implementation of Vim Mode".into()),
393        toggle_state,
394        {
395            let fs = <dyn Fs>::global(cx);
396            move |&selection, _, cx| {
397                let vim_mode = match selection {
398                    ToggleState::Selected => true,
399                    ToggleState::Unselected => false,
400                    ToggleState::Indeterminate => {
401                        return;
402                    }
403                };
404                update_settings_file(fs.clone(), cx, move |setting, _| {
405                    setting.vim_mode = Some(vim_mode);
406                });
407
408                telemetry::event!(
409                    "Welcome Vim Mode Toggled",
410                    options = if vim_mode { "on" } else { "off" },
411                );
412            }
413        },
414    )
415    .tab_index({
416        *tab_index += 1;
417        *tab_index - 1
418    })
419}
420
421fn render_worktree_auto_trust_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
422    let toggle_state = if ProjectSettings::get_global(cx).session.trust_all_worktrees {
423        ui::ToggleState::Selected
424    } else {
425        ui::ToggleState::Unselected
426    };
427
428    let tooltip_description = "Zed can only allow services like language servers, project settings, and MCP servers to run after you mark a new project as trusted.";
429
430    SwitchField::new(
431        "onboarding-auto-trust-worktrees",
432        Some("Trust All Projects By Default"),
433        Some("Automatically mark all new projects as trusted to unlock all Zed's features".into()),
434        toggle_state,
435        {
436            let fs = <dyn Fs>::global(cx);
437            move |&selection, _, cx| {
438                let trust = match selection {
439                    ToggleState::Selected => true,
440                    ToggleState::Unselected => false,
441                    ToggleState::Indeterminate => {
442                        return;
443                    }
444                };
445                update_settings_file(fs.clone(), cx, move |setting, _| {
446                    setting.session.get_or_insert_default().trust_all_worktrees = Some(trust);
447                });
448
449                telemetry::event!(
450                    "Welcome Page Worktree Auto Trust Toggled",
451                    options = if trust { "on" } else { "off" }
452                );
453            }
454        },
455    )
456    .tab_index({
457        *tab_index += 1;
458        *tab_index - 1
459    })
460    .tooltip(Tooltip::text(tooltip_description))
461}
462
463fn render_setting_import_button(
464    tab_index: isize,
465    label: SharedString,
466    action: &dyn Action,
467    imported: bool,
468) -> impl IntoElement + 'static {
469    let action = action.boxed_clone();
470
471    Button::new(label.clone(), label.clone())
472        .style(ButtonStyle::OutlinedGhost)
473        .size(ButtonSize::Medium)
474        .label_size(LabelSize::Small)
475        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
476        .toggle_state(imported)
477        .tab_index(tab_index)
478        .when(imported, |this| {
479            this.end_icon(Icon::new(IconName::Check).size(IconSize::Small))
480                .color(Color::Success)
481        })
482        .on_click(move |_, window, cx| {
483            telemetry::event!("Welcome Import Settings", import_source = label,);
484            window.dispatch_action(action.boxed_clone(), cx);
485        })
486}
487
488fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
489    let import_state = SettingsImportState::global(cx);
490    let imports: [(SharedString, &dyn Action, bool); 2] = [
491        (
492            "VS Code".into(),
493            &ImportVsCodeSettings { skip_prompt: false },
494            import_state.vscode,
495        ),
496        (
497            "Cursor".into(),
498            &ImportCursorSettings { skip_prompt: false },
499            import_state.cursor,
500        ),
501    ];
502
503    let [vscode, cursor] = imports.map(|(label, action, imported)| {
504        *tab_index += 1;
505        render_setting_import_button(*tab_index - 1, label, action, imported)
506    });
507
508    h_flex()
509        .gap_2()
510        .flex_wrap()
511        .justify_between()
512        .child(
513            v_flex()
514                .gap_0p5()
515                .max_w_5_6()
516                .child(Label::new("Import Settings"))
517                .child(
518                    Label::new("Automatically pull your settings from other editors")
519                        .color(Color::Muted),
520                ),
521        )
522        .child(h_flex().gap_1().child(vscode).child(cursor))
523}
524
525pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement {
526    let mut tab_index = 0;
527    v_flex()
528        .id("basics-page")
529        .gap_6()
530        .child(render_theme_section(&mut tab_index, cx))
531        .child(render_base_keymap_section(&mut tab_index, cx))
532        .child(render_import_settings_section(&mut tab_index, cx))
533        .child(render_vim_mode_switch(&mut tab_index, cx))
534        .child(render_worktree_auto_trust_switch(&mut tab_index, cx))
535        .child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
536        .child(render_telemetry_section(&mut tab_index, cx))
537}