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