basics_page.rs

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