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    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_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        .gap_4()
233        .child(
234            SwitchField::new(
235                "onboarding-telemetry-metrics",
236                None::<&str>,
237                Some("Help improve Zed by sending anonymous usage data".into()),
238                if TelemetrySettings::get_global(cx).metrics {
239                    ui::ToggleState::Selected
240                } else {
241                    ui::ToggleState::Unselected
242                },
243                {
244                    let fs = fs.clone();
245                    move |selection, _, cx| {
246                        let enabled = match selection {
247                            ToggleState::Selected => true,
248                            ToggleState::Unselected => false,
249                            ToggleState::Indeterminate => {
250                                return;
251                            }
252                        };
253
254                        update_settings_file(fs.clone(), cx, move |setting, _| {
255                            setting.telemetry.get_or_insert_default().metrics = Some(enabled);
256                        });
257
258                        // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
259                        // and can fix it in a timely manner to respect a user's choice.
260                        telemetry::event!(
261                            "Welcome Page Telemetry Metrics Toggled",
262                            options = if enabled { "on" } else { "off" }
263                        );
264                    }
265                },
266            )
267            .tab_index({
268                *tab_index += 1;
269                *tab_index
270            }),
271        )
272        .child(
273            SwitchField::new(
274                "onboarding-telemetry-crash-reports",
275                None::<&str>,
276                Some(
277                    "Help fix Zed by sending crash reports so we can fix critical issues fast"
278                        .into(),
279                ),
280                if TelemetrySettings::get_global(cx).diagnostics {
281                    ui::ToggleState::Selected
282                } else {
283                    ui::ToggleState::Unselected
284                },
285                {
286                    let fs = fs.clone();
287                    move |selection, _, cx| {
288                        let enabled = match selection {
289                            ToggleState::Selected => true,
290                            ToggleState::Unselected => false,
291                            ToggleState::Indeterminate => {
292                                return;
293                            }
294                        };
295
296                        update_settings_file(fs.clone(), cx, move |setting, _| {
297                            setting.telemetry.get_or_insert_default().diagnostics = Some(enabled);
298                        });
299
300                        // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
301                        // and can fix it in a timely manner to respect a user's choice.
302                        telemetry::event!(
303                            "Welcome Page Telemetry Diagnostics Toggled",
304                            options = if enabled { "on" } else { "off" }
305                        );
306                    }
307                },
308            )
309            .tab_index({
310                *tab_index += 1;
311                *tab_index
312            }),
313        )
314}
315
316fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
317    let base_keymap = match BaseKeymap::get_global(cx) {
318        BaseKeymap::VSCode => Some(0),
319        BaseKeymap::JetBrains => Some(1),
320        BaseKeymap::SublimeText => Some(2),
321        BaseKeymap::Atom => Some(3),
322        BaseKeymap::Emacs => Some(4),
323        BaseKeymap::Cursor => Some(5),
324        BaseKeymap::TextMate | BaseKeymap::None => None,
325    };
326
327    return v_flex().gap_2().child(Label::new("Base Keymap")).child(
328        ToggleButtonGroup::two_rows(
329            "base_keymap_selection",
330            [
331                ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| {
332                    write_keymap_base(BaseKeymap::VSCode, cx);
333                }),
334                ToggleButtonWithIcon::new("JetBrains", IconName::EditorJetBrains, |_, _, cx| {
335                    write_keymap_base(BaseKeymap::JetBrains, cx);
336                }),
337                ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| {
338                    write_keymap_base(BaseKeymap::SublimeText, cx);
339                }),
340            ],
341            [
342                ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| {
343                    write_keymap_base(BaseKeymap::Atom, cx);
344                }),
345                ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
346                    write_keymap_base(BaseKeymap::Emacs, cx);
347                }),
348                ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| {
349                    write_keymap_base(BaseKeymap::Cursor, cx);
350                }),
351            ],
352        )
353        .when_some(base_keymap, |this, base_keymap| {
354            this.selected_index(base_keymap)
355        })
356        .full_width()
357        .tab_index(tab_index)
358        .size(ui::ToggleButtonGroupSize::Medium)
359        .style(ui::ToggleButtonGroupStyle::Outlined),
360    );
361
362    fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
363        let fs = <dyn Fs>::global(cx);
364
365        update_settings_file(fs, cx, move |setting, _| {
366            setting.base_keymap = Some(keymap_base.into());
367        });
368
369        telemetry::event!("Welcome Keymap Changed", keymap = keymap_base);
370    }
371}
372
373fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
374    let toggle_state = if VimModeSetting::get_global(cx).0 {
375        ui::ToggleState::Selected
376    } else {
377        ui::ToggleState::Unselected
378    };
379    SwitchField::new(
380        "onboarding-vim-mode",
381        Some("Vim Mode"),
382        Some("Coming from Neovim? Use our first-class implementation of Vim Mode".into()),
383        toggle_state,
384        {
385            let fs = <dyn Fs>::global(cx);
386            move |&selection, _, cx| {
387                let vim_mode = match selection {
388                    ToggleState::Selected => true,
389                    ToggleState::Unselected => false,
390                    ToggleState::Indeterminate => {
391                        return;
392                    }
393                };
394                update_settings_file(fs.clone(), cx, move |setting, _| {
395                    setting.vim_mode = Some(vim_mode);
396                });
397
398                telemetry::event!(
399                    "Welcome Vim Mode Toggled",
400                    options = if vim_mode { "on" } else { "off" },
401                );
402            }
403        },
404    )
405    .tab_index({
406        *tab_index += 1;
407        *tab_index - 1
408    })
409}
410
411fn render_setting_import_button(
412    tab_index: isize,
413    label: SharedString,
414    action: &dyn Action,
415    imported: bool,
416) -> impl IntoElement + 'static {
417    let action = action.boxed_clone();
418
419    Button::new(label.clone(), label.clone())
420        .style(ButtonStyle::OutlinedGhost)
421        .size(ButtonSize::Medium)
422        .label_size(LabelSize::Small)
423        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
424        .toggle_state(imported)
425        .tab_index(tab_index)
426        .when(imported, |this| {
427            this.icon(IconName::Check)
428                .icon_size(IconSize::Small)
429                .color(Color::Success)
430        })
431        .on_click(move |_, window, cx| {
432            telemetry::event!("Welcome Import Settings", import_source = label,);
433            window.dispatch_action(action.boxed_clone(), cx);
434        })
435}
436
437fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
438    let import_state = SettingsImportState::global(cx);
439    let imports: [(SharedString, &dyn Action, bool); 2] = [
440        (
441            "VS Code".into(),
442            &ImportVsCodeSettings { skip_prompt: false },
443            import_state.vscode,
444        ),
445        (
446            "Cursor".into(),
447            &ImportCursorSettings { skip_prompt: false },
448            import_state.cursor,
449        ),
450    ];
451
452    let [vscode, cursor] = imports.map(|(label, action, imported)| {
453        *tab_index += 1;
454        render_setting_import_button(*tab_index - 1, label, action, imported)
455    });
456
457    h_flex()
458        .gap_2()
459        .flex_wrap()
460        .justify_between()
461        .child(
462            v_flex()
463                .gap_0p5()
464                .max_w_5_6()
465                .child(Label::new("Import Settings"))
466                .child(
467                    Label::new("Automatically pull your settings from other editors")
468                        .color(Color::Muted),
469                ),
470        )
471        .child(h_flex().gap_1().child(vscode).child(cursor))
472}
473
474pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement {
475    let mut tab_index = 0;
476    v_flex()
477        .id("basics-page")
478        .gap_6()
479        .child(render_theme_section(&mut tab_index, cx))
480        .child(render_base_keymap_section(&mut tab_index, cx))
481        .child(render_import_settings_section(&mut tab_index, cx))
482        .child(render_vim_mode_switch(&mut tab_index, cx))
483        .child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
484        .child(render_telemetry_section(&mut tab_index, cx))
485}