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