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