1#![allow(unused, dead_code)]
2mod components;
3mod juicy_button;
4mod persistence;
5mod theme_preview;
6
7use self::components::{CalloutRow, CheckboxRow, HeaderRow, SelectableTile, SelectableTileRow};
8use self::juicy_button::JuicyButton;
9use client::{Client, TelemetrySettings};
10use command_palette_hooks::CommandPaletteFilter;
11use feature_flags::FeatureFlagAppExt as _;
12use gpui::{
13 Action, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyBinding, Subscription,
14 Task, UpdateGlobal, WeakEntity, actions, prelude::*, svg, transparent_black,
15};
16use menu;
17use persistence::ONBOARDING_DB;
18
19use language::language_settings::{AllLanguageSettings, ai_enabled, all_language_settings};
20use project::Project;
21use serde_json;
22use settings::{Settings, SettingsStore, update_settings_file};
23use settings_ui::SettingsUiFeatureFlag;
24use std::collections::HashSet;
25use std::sync::Arc;
26use theme::{Theme, ThemeRegistry, ThemeSettings};
27use ui::{
28 CheckboxWithLabel, ContentGroup, FocusOutline, KeybindingHint, ListItem, ToggleState, Vector,
29 VectorName, prelude::*,
30};
31use util::ResultExt;
32use vim_mode_setting::VimModeSetting;
33use welcome::{BaseKeymap, WelcomePage};
34use workspace::{
35 Workspace, WorkspaceId,
36 item::{Item, ItemEvent, SerializableItem},
37 notifications::NotifyResultExt,
38};
39use zed_actions;
40
41actions!(
42 onboarding,
43 [
44 ShowOnboarding,
45 JumpToBasics,
46 JumpToEditing,
47 JumpToAiSetup,
48 JumpToWelcome,
49 NextPage,
50 PreviousPage,
51 ToggleFocus,
52 ResetOnboarding,
53 ]
54);
55
56pub fn init(cx: &mut App) {
57 cx.observe_new(|workspace: &mut Workspace, _, _cx| {
58 workspace.register_action(|workspace, _: &ShowOnboarding, window, cx| {
59 let client = workspace.client().clone();
60 let onboarding = cx.new(|cx| OnboardingUI::new(workspace, client, cx));
61 workspace.add_item_to_active_pane(Box::new(onboarding), None, true, window, cx);
62 });
63 })
64 .detach();
65
66 workspace::register_serializable_item::<OnboardingUI>(cx);
67
68 feature_gate_onboarding_ui_actions(cx);
69}
70
71fn feature_gate_onboarding_ui_actions(cx: &mut App) {
72 const ONBOARDING_ACTION_NAMESPACE: &str = "onboarding_ui";
73
74 CommandPaletteFilter::update_global(cx, |filter, _cx| {
75 filter.hide_namespace(ONBOARDING_ACTION_NAMESPACE);
76 });
77
78 cx.observe_flag::<SettingsUiFeatureFlag, _>({
79 move |is_enabled, cx| {
80 CommandPaletteFilter::update_global(cx, |filter, _cx| {
81 if is_enabled {
82 filter.show_namespace(ONBOARDING_ACTION_NAMESPACE);
83 } else {
84 filter.hide_namespace(ONBOARDING_ACTION_NAMESPACE);
85 }
86 });
87 }
88 })
89 .detach();
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub enum OnboardingPage {
94 Basics,
95 Editing,
96 AiSetup,
97 Welcome,
98}
99
100impl OnboardingPage {
101 fn next(&self) -> Option<Self> {
102 match self {
103 Self::Basics => Some(Self::Editing),
104 Self::Editing => Some(Self::AiSetup),
105 Self::AiSetup => Some(Self::Welcome),
106 Self::Welcome => None,
107 }
108 }
109
110 fn previous(&self) -> Option<Self> {
111 match self {
112 Self::Basics => None,
113 Self::Editing => Some(Self::Basics),
114 Self::AiSetup => Some(Self::Editing),
115 Self::Welcome => Some(Self::AiSetup),
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum NavigationFocusItem {
122 SignIn,
123 Basics,
124 Editing,
125 AiSetup,
126 Welcome,
127 Next,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct PageFocusItem(pub usize);
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum FocusArea {
135 Navigation,
136 PageContent,
137}
138
139pub struct OnboardingUI {
140 focus_handle: FocusHandle,
141 current_page: OnboardingPage,
142 nav_focus: NavigationFocusItem,
143 page_focus: [PageFocusItem; 4],
144 completed_pages: HashSet<OnboardingPage>,
145 focus_area: FocusArea,
146
147 // Workspace reference for Item trait
148 workspace: WeakEntity<Workspace>,
149 workspace_id: Option<WorkspaceId>,
150 client: Arc<Client>,
151 welcome_page: Option<Entity<WelcomePage>>,
152 _settings_subscription: Option<Subscription>,
153}
154
155impl OnboardingUI {}
156
157impl EventEmitter<ItemEvent> for OnboardingUI {}
158
159impl Focusable for OnboardingUI {
160 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
161 self.focus_handle.clone()
162 }
163}
164
165#[derive(Clone)]
166pub enum OnboardingEvent {
167 PageCompleted(OnboardingPage),
168}
169
170impl Render for OnboardingUI {
171 fn render(
172 &mut self,
173 window: &mut gpui::Window,
174 cx: &mut Context<Self>,
175 ) -> impl gpui::IntoElement {
176 div()
177 .bg(cx.theme().colors().editor_background)
178 .size_full()
179 .key_context("OnboardingUI")
180 .on_action(cx.listener(Self::select_next))
181 .on_action(cx.listener(Self::select_previous))
182 .on_action(cx.listener(Self::confirm))
183 .on_action(cx.listener(Self::cancel))
184 .on_action(cx.listener(Self::toggle_focus))
185 .on_action(cx.listener(Self::handle_enable_ai_assistance))
186 .on_action(cx.listener(Self::handle_disable_ai_assistance))
187 .flex()
188 .items_center()
189 .justify_center()
190 .overflow_hidden()
191 .child(
192 h_flex()
193 .id("onboarding-ui")
194 .key_context("Onboarding")
195 .track_focus(&self.focus_handle)
196 .on_action(cx.listener(Self::handle_jump_to_basics))
197 .on_action(cx.listener(Self::handle_jump_to_editing))
198 .on_action(cx.listener(Self::handle_jump_to_ai_setup))
199 .on_action(cx.listener(Self::handle_jump_to_welcome))
200 .on_action(cx.listener(Self::handle_next_page))
201 .on_action(cx.listener(Self::handle_previous_page))
202 .w(px(984.))
203 .overflow_hidden()
204 .gap(px(24.))
205 .child(
206 h_flex()
207 .h(px(500.))
208 .w_full()
209 .overflow_hidden()
210 .gap(px(48.))
211 .child(self.render_navigation(window, cx))
212 .child(
213 v_flex()
214 .h_full()
215 .flex_1()
216 .overflow_hidden()
217 .child(self.render_active_page(window, cx)),
218 ),
219 ),
220 )
221 }
222}
223
224impl OnboardingUI {
225 pub fn new(workspace: &Workspace, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
226 let settings_subscription = cx.observe_global::<SettingsStore>(|_, cx| {
227 cx.notify();
228 });
229
230 Self {
231 focus_handle: cx.focus_handle(),
232 current_page: OnboardingPage::Basics,
233 nav_focus: NavigationFocusItem::Basics,
234 page_focus: [PageFocusItem(0); 4],
235 completed_pages: HashSet::new(),
236 focus_area: FocusArea::Navigation,
237 workspace: workspace.weak_handle(),
238 workspace_id: workspace.database_id(),
239 client,
240 welcome_page: None,
241 _settings_subscription: Some(settings_subscription),
242 }
243 }
244
245 fn completed_pages_to_string(&self) -> String {
246 let mut result = String::new();
247 for i in 0..4 {
248 let page = match i {
249 0 => OnboardingPage::Basics,
250 1 => OnboardingPage::Editing,
251 2 => OnboardingPage::AiSetup,
252 3 => OnboardingPage::Welcome,
253 _ => unreachable!(),
254 };
255 result.push(if self.completed_pages.contains(&page) {
256 '1'
257 } else {
258 '0'
259 });
260 }
261 result
262 }
263
264 fn completed_pages_from_string(s: &str) -> HashSet<OnboardingPage> {
265 let mut result = HashSet::new();
266 for (i, ch) in s.chars().take(4).enumerate() {
267 if ch == '1' {
268 let page = match i {
269 0 => OnboardingPage::Basics,
270 1 => OnboardingPage::Editing,
271 2 => OnboardingPage::AiSetup,
272 3 => OnboardingPage::Welcome,
273 _ => continue,
274 };
275 result.insert(page);
276 }
277 }
278 result
279 }
280
281 fn jump_to_page(
282 &mut self,
283 page: OnboardingPage,
284 _window: &mut gpui::Window,
285 cx: &mut Context<Self>,
286 ) {
287 self.current_page = page;
288 cx.emit(ItemEvent::UpdateTab);
289 cx.notify();
290 }
291
292 fn next_page(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) {
293 if let Some(next) = self.current_page.next() {
294 self.current_page = next;
295 cx.notify();
296 }
297 }
298
299 fn previous_page(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) {
300 if let Some(prev) = self.current_page.previous() {
301 self.current_page = prev;
302 cx.notify();
303 }
304 }
305
306 fn reset(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) {
307 self.current_page = OnboardingPage::Basics;
308 self.focus_area = FocusArea::Navigation;
309 self.completed_pages = HashSet::new();
310 cx.notify();
311 }
312
313 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
314 match self.focus_area {
315 FocusArea::Navigation => {
316 self.nav_focus = match self.nav_focus {
317 NavigationFocusItem::SignIn => NavigationFocusItem::Basics,
318 NavigationFocusItem::Basics => NavigationFocusItem::Editing,
319 NavigationFocusItem::Editing => NavigationFocusItem::AiSetup,
320 NavigationFocusItem::AiSetup => NavigationFocusItem::Welcome,
321 NavigationFocusItem::Welcome => NavigationFocusItem::Next,
322 NavigationFocusItem::Next => NavigationFocusItem::SignIn,
323 };
324 }
325 FocusArea::PageContent => {
326 let page_index = match self.current_page {
327 OnboardingPage::Basics => 0,
328 OnboardingPage::Editing => 1,
329 OnboardingPage::AiSetup => 2,
330 OnboardingPage::Welcome => 3,
331 };
332 // Bounds checking for page items
333 let max_items = match self.current_page {
334 OnboardingPage::Basics => 14, // 4 themes + 7 keymaps + 3 checkboxes
335 OnboardingPage::Editing => 3, // 3 buttons
336 OnboardingPage::AiSetup => 2, // Will have 2 items
337 OnboardingPage::Welcome => 1, // Will have 1 item
338 };
339
340 if self.page_focus[page_index].0 < max_items - 1 {
341 self.page_focus[page_index].0 += 1;
342 } else {
343 // Wrap to start
344 self.page_focus[page_index].0 = 0;
345 }
346 }
347 }
348 cx.notify();
349 }
350
351 fn select_previous(
352 &mut self,
353 _: &menu::SelectPrevious,
354 _window: &mut Window,
355 cx: &mut Context<Self>,
356 ) {
357 match self.focus_area {
358 FocusArea::Navigation => {
359 self.nav_focus = match self.nav_focus {
360 NavigationFocusItem::SignIn => NavigationFocusItem::Next,
361 NavigationFocusItem::Basics => NavigationFocusItem::SignIn,
362 NavigationFocusItem::Editing => NavigationFocusItem::Basics,
363 NavigationFocusItem::AiSetup => NavigationFocusItem::Editing,
364 NavigationFocusItem::Welcome => NavigationFocusItem::AiSetup,
365 NavigationFocusItem::Next => NavigationFocusItem::Welcome,
366 };
367 }
368 FocusArea::PageContent => {
369 let page_index = match self.current_page {
370 OnboardingPage::Basics => 0,
371 OnboardingPage::Editing => 1,
372 OnboardingPage::AiSetup => 2,
373 OnboardingPage::Welcome => 3,
374 };
375 // Bounds checking for page items
376 let max_items = match self.current_page {
377 OnboardingPage::Basics => 14, // 4 themes + 7 keymaps + 3 checkboxes
378 OnboardingPage::Editing => 3, // 3 buttons
379 OnboardingPage::AiSetup => 2, // Will have 2 items
380 OnboardingPage::Welcome => 1, // Will have 1 item
381 };
382
383 if self.page_focus[page_index].0 > 0 {
384 self.page_focus[page_index].0 -= 1;
385 } else {
386 // Wrap to end
387 self.page_focus[page_index].0 = max_items - 1;
388 }
389 }
390 }
391 cx.notify();
392 }
393
394 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
395 match self.focus_area {
396 FocusArea::Navigation => {
397 match self.nav_focus {
398 NavigationFocusItem::SignIn => {
399 // Handle sign in action
400 // TODO: Implement sign in action
401 }
402 NavigationFocusItem::Basics => {
403 self.jump_to_page(OnboardingPage::Basics, window, cx)
404 }
405 NavigationFocusItem::Editing => {
406 self.jump_to_page(OnboardingPage::Editing, window, cx)
407 }
408 NavigationFocusItem::AiSetup => {
409 self.jump_to_page(OnboardingPage::AiSetup, window, cx)
410 }
411 NavigationFocusItem::Welcome => {
412 self.jump_to_page(OnboardingPage::Welcome, window, cx)
413 }
414 NavigationFocusItem::Next => {
415 // Handle next button action
416 self.next_page(window, cx);
417 }
418 }
419 // After confirming navigation item (except Next), switch focus to page content
420 if self.nav_focus != NavigationFocusItem::Next {
421 self.focus_area = FocusArea::PageContent;
422 }
423 }
424 FocusArea::PageContent => {
425 // Handle page-specific item selection
426 let page_index = match self.current_page {
427 OnboardingPage::Basics => 0,
428 OnboardingPage::Editing => 1,
429 OnboardingPage::AiSetup => 2,
430 OnboardingPage::Welcome => 3,
431 };
432 let item_index = self.page_focus[page_index].0;
433
434 // Trigger the action for the focused item
435 match self.current_page {
436 OnboardingPage::Basics => {
437 match item_index {
438 0..=3 => {
439 // Theme selection
440 cx.notify();
441 }
442 4..=10 => {
443 // Keymap selection
444 cx.notify();
445 }
446 11..=13 => {
447 // Checkbox toggles (handled by their own listeners)
448 cx.notify();
449 }
450 _ => {}
451 }
452 }
453 OnboardingPage::Editing => {
454 // Similar handling for editing page
455 cx.notify();
456 }
457 _ => {
458 cx.notify();
459 }
460 }
461 }
462 }
463 cx.notify();
464 }
465
466 fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
467 match self.focus_area {
468 FocusArea::PageContent => {
469 // Switch focus back to navigation
470 self.focus_area = FocusArea::Navigation;
471 }
472 FocusArea::Navigation => {
473 // If already in navigation, maybe close the onboarding?
474 // For now, just stay in navigation
475 }
476 }
477 cx.notify();
478 }
479
480 fn toggle_focus(&mut self, _: &ToggleFocus, _window: &mut Window, cx: &mut Context<Self>) {
481 self.focus_area = match self.focus_area {
482 FocusArea::Navigation => FocusArea::PageContent,
483 FocusArea::PageContent => FocusArea::Navigation,
484 };
485 cx.notify();
486 }
487
488 fn mark_page_completed(
489 &mut self,
490 page: OnboardingPage,
491 _window: &mut gpui::Window,
492 cx: &mut Context<Self>,
493 ) {
494 self.completed_pages.insert(page);
495 cx.notify();
496 }
497
498 fn handle_jump_to_basics(
499 &mut self,
500 _: &JumpToBasics,
501 window: &mut Window,
502 cx: &mut Context<Self>,
503 ) {
504 self.jump_to_page(OnboardingPage::Basics, window, cx);
505 }
506
507 fn handle_jump_to_editing(
508 &mut self,
509 _: &JumpToEditing,
510 window: &mut Window,
511 cx: &mut Context<Self>,
512 ) {
513 self.jump_to_page(OnboardingPage::Editing, window, cx);
514 }
515
516 fn handle_jump_to_ai_setup(
517 &mut self,
518 _: &JumpToAiSetup,
519 window: &mut Window,
520 cx: &mut Context<Self>,
521 ) {
522 self.jump_to_page(OnboardingPage::AiSetup, window, cx);
523 }
524
525 fn handle_jump_to_welcome(
526 &mut self,
527 _: &JumpToWelcome,
528 window: &mut Window,
529 cx: &mut Context<Self>,
530 ) {
531 self.jump_to_page(OnboardingPage::Welcome, window, cx);
532 }
533
534 fn handle_next_page(&mut self, _: &NextPage, window: &mut Window, cx: &mut Context<Self>) {
535 self.next_page(window, cx);
536 }
537
538 fn handle_enable_ai_assistance(
539 &mut self,
540 _: &zed_actions::EnableAiAssistance,
541 _window: &mut Window,
542 cx: &mut Context<Self>,
543 ) {
544 if let Some(workspace) = self.workspace.upgrade() {
545 let fs = workspace.read(cx).app_state().fs.clone();
546 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
547 file.features
548 .get_or_insert(Default::default())
549 .ai_assistance = Some(true);
550 });
551 cx.notify();
552 }
553 }
554
555 fn handle_disable_ai_assistance(
556 &mut self,
557 _: &zed_actions::DisableAiAssistance,
558 _window: &mut Window,
559 cx: &mut Context<Self>,
560 ) {
561 if let Some(workspace) = self.workspace.upgrade() {
562 let fs = workspace.read(cx).app_state().fs.clone();
563 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
564 file.features
565 .get_or_insert(Default::default())
566 .ai_assistance = Some(false);
567 });
568 cx.notify();
569 }
570 }
571
572 fn handle_previous_page(
573 &mut self,
574 _: &PreviousPage,
575 window: &mut Window,
576 cx: &mut Context<Self>,
577 ) {
578 self.previous_page(window, cx);
579 }
580
581 fn render_navigation(
582 &mut self,
583 window: &mut Window,
584 cx: &mut Context<Self>,
585 ) -> impl gpui::IntoElement {
586 let client = self.client.clone();
587
588 v_flex()
589 .h_full()
590 .w(px(256.))
591 .gap_2()
592 .justify_between()
593 .child(
594 v_flex()
595 .w_full()
596 .gap_px()
597 .child(
598 h_flex()
599 .w_full()
600 .justify_between()
601 .py(px(24.))
602 .pl(px(24.))
603 .pr(px(12.))
604 .child(
605 Vector::new(VectorName::ZedLogo, rems(2.), rems(2.))
606 .color(Color::Custom(cx.theme().colors().icon.opacity(0.5))),
607 )
608 .child(
609 Button::new("sign_in", "Sign in")
610 .color(Color::Muted)
611 .label_size(LabelSize::Small)
612 .when(
613 self.focus_area == FocusArea::Navigation
614 && self.nav_focus == NavigationFocusItem::SignIn,
615 |this| this.color(Color::Accent),
616 )
617 .size(ButtonSize::Compact)
618 .on_click(cx.listener(move |_, _, window, cx| {
619 let client = client.clone();
620 window
621 .spawn(cx, async move |cx| {
622 client
623 .authenticate_and_connect(true, &cx)
624 .await
625 .into_response()
626 .notify_async_err(cx);
627 })
628 .detach();
629 })),
630 ),
631 )
632 .child(
633 v_flex()
634 .gap_px()
635 .py(px(16.))
636 .gap(px(2.))
637 .child(self.render_nav_item(
638 OnboardingPage::Basics,
639 "The Basics",
640 "1",
641 cx,
642 ))
643 .child(self.render_nav_item(
644 OnboardingPage::Editing,
645 "Editing Experience",
646 "2",
647 cx,
648 ))
649 .child(self.render_nav_item(
650 OnboardingPage::AiSetup,
651 "AI Setup",
652 "3",
653 cx,
654 ))
655 .child(self.render_nav_item(
656 OnboardingPage::Welcome,
657 "Welcome",
658 "4",
659 cx,
660 )),
661 ),
662 )
663 .child(self.render_bottom_controls(window, cx))
664 }
665
666 fn render_nav_item(
667 &mut self,
668 page: OnboardingPage,
669 label: impl Into<SharedString>,
670 shortcut: impl Into<SharedString>,
671 cx: &mut Context<Self>,
672 ) -> impl gpui::IntoElement {
673 let is_selected = self.current_page == page;
674 let label = label.into();
675 let shortcut = shortcut.into();
676 let id = ElementId::Name(label.clone());
677 let corner_radius = px(4.);
678
679 let item_focused = match page {
680 OnboardingPage::Basics => self.nav_focus == NavigationFocusItem::Basics,
681 OnboardingPage::Editing => self.nav_focus == NavigationFocusItem::Editing,
682 OnboardingPage::AiSetup => self.nav_focus == NavigationFocusItem::AiSetup,
683 OnboardingPage::Welcome => self.nav_focus == NavigationFocusItem::Welcome,
684 };
685
686 let area_focused = self.focus_area == FocusArea::Navigation;
687
688 FocusOutline::new(corner_radius, item_focused, px(2.))
689 .active(area_focused && item_focused)
690 .child(
691 h_flex()
692 .id(id)
693 .h(rems(1.625))
694 .w_full()
695 .rounded(corner_radius)
696 .px_3()
697 .when(is_selected, |this| {
698 this.bg(cx.theme().colors().border_focused.opacity(0.16))
699 })
700 .child(
701 h_flex()
702 .flex_1()
703 .justify_between()
704 .items_center()
705 .child(
706 Label::new(label)
707 .weight(FontWeight::MEDIUM)
708 .color(Color::Muted)
709 .when(item_focused, |this| this.color(Color::Default)),
710 )
711 .child(
712 Label::new(format!("⌘{}", shortcut.clone()))
713 .color(Color::Placeholder)
714 .size(LabelSize::XSmall),
715 ),
716 )
717 .on_click(cx.listener(move |this, _, window, cx| {
718 this.jump_to_page(page, window, cx);
719 })),
720 )
721 }
722
723 fn render_bottom_controls(
724 &mut self,
725 window: &mut gpui::Window,
726 cx: &mut Context<Self>,
727 ) -> impl gpui::IntoElement {
728 h_flex().w_full().p(px(12.)).child(
729 JuicyButton::new(if self.current_page == OnboardingPage::Welcome {
730 "Get Started"
731 } else {
732 "Next"
733 })
734 .keybinding(ui::KeyBinding::for_action_in(
735 &NextPage,
736 &self.focus_handle,
737 window,
738 cx,
739 ))
740 .on_click(cx.listener(|this, _, window, cx| {
741 this.next_page(window, cx);
742 })),
743 )
744 }
745
746 fn render_active_page(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
747 match self.current_page {
748 OnboardingPage::Basics => self.render_basics_page(cx),
749 OnboardingPage::Editing => self.render_editing_page(cx),
750 OnboardingPage::AiSetup => self.render_ai_setup_page(cx),
751 OnboardingPage::Welcome => self.render_welcome_page(cx),
752 }
753 }
754
755 fn render_basics_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
756 let page_index = 0; // Basics page index
757 let focused_item = self.page_focus[page_index].0;
758 let is_page_focused = self.focus_area == FocusArea::PageContent;
759
760 use theme_preview::ThemePreviewTile;
761
762 // Get available themes
763 let theme_registry = ThemeRegistry::default_global(cx);
764 let theme_names = theme_registry.list_names();
765 let current_theme = cx.theme().clone();
766
767 v_flex()
768 .id("theme-selector")
769 .h_full()
770 .w_full()
771 .overflow_y_scroll()
772 .child({
773 let vim_enabled = VimModeSetting::get_global(cx).0;
774 CheckboxRow::new("Enable Vim Mode")
775 .checked(vim_enabled)
776 .on_click(move |_window, cx| {
777 let current = VimModeSetting::get_global(cx).0;
778 SettingsStore::update_global(cx, move |store, cx| {
779 let mut settings = store.raw_user_settings().clone();
780 settings["vim_mode"] = serde_json::json!(!current);
781 store.set_user_settings(&settings.to_string(), cx).ok();
782 });
783 })
784 })
785 // Theme selector section
786 .child(
787 v_flex()
788 .w_full()
789 .overflow_hidden()
790 .child(
791 HeaderRow::new("Pick a Theme").end_slot(
792 Button::new("more_themes", "More Themes")
793 .style(ButtonStyle::Subtle)
794 .color(Color::Muted)
795 .on_click(cx.listener(|_, _, window, cx| {
796 window.dispatch_action(
797 zed_actions::theme_selector::Toggle::default()
798 .boxed_clone(),
799 cx,
800 );
801 })),
802 ),
803 )
804 .child(
805 h_flex().w_full().overflow_hidden().gap_3().children(
806 vec![
807 ("One Dark", "One Dark"),
808 ("Gruvbox Dark", "Gruvbox Dark"),
809 ("One Light", "One Light"),
810 ("Gruvbox Light", "Gruvbox Light"),
811 ]
812 .into_iter()
813 .enumerate()
814 .map(|(i, (label, theme_name))| {
815 let is_selected = current_theme.name == *theme_name;
816 let is_focused = is_page_focused && focused_item == i;
817
818 v_flex()
819 .flex_1()
820 .gap_1p5()
821 .justify_center()
822 .text_center()
823 .child(
824 div()
825 .id(("theme", i))
826 .rounded(px(8.))
827 .h(px(90.))
828 .w_full()
829 .overflow_hidden()
830 .border_1()
831 .border_color(if is_focused {
832 cx.theme().colors().border_focused
833 } else {
834 transparent_black()
835 })
836 .child(
837 if let Ok(theme) = theme_registry.get(theme_name) {
838 ThemePreviewTile::new(theme, is_selected, 0.5)
839 .into_any_element()
840 } else {
841 div()
842 .size_full()
843 .bg(cx.theme().colors().surface_background)
844 .rounded_md()
845 .into_any_element()
846 },
847 )
848 .on_click(cx.listener(move |this, _, window, cx| {
849 SettingsStore::update_global(
850 cx,
851 move |store, cx| {
852 let mut settings =
853 store.raw_user_settings().clone();
854 settings["theme"] =
855 serde_json::json!(theme_name);
856 store
857 .set_user_settings(
858 &settings.to_string(),
859 cx,
860 )
861 .ok();
862 },
863 );
864 cx.notify();
865 })),
866 )
867 .child(
868 div()
869 .text_color(cx.theme().colors().text)
870 .text_size(px(12.))
871 .child(label),
872 )
873 }),
874 ),
875 ),
876 )
877 // Keymap selector section
878 .child(
879 v_flex()
880 .gap_1()
881 .child(HeaderRow::new("Pick a Keymap"))
882 .child(
883 h_flex().gap_2().children(
884 vec![
885 ("Zed", VectorName::ZedLogo, 4),
886 ("Atom", VectorName::AtomLogo, 5),
887 ("JetBrains", VectorName::ZedLogo, 6),
888 ("Sublime", VectorName::ZedLogo, 7),
889 ("VSCode", VectorName::ZedLogo, 8),
890 ("Emacs", VectorName::ZedLogo, 9),
891 ("TextMate", VectorName::ZedLogo, 10),
892 ]
893 .into_iter()
894 .map(|(label, icon, index)| {
895 let is_focused = is_page_focused && focused_item == index;
896 let current_keymap = BaseKeymap::get_global(cx).to_string();
897 let is_selected = current_keymap == label;
898
899 v_flex()
900 .w(px(72.))
901 .gap_1()
902 .items_center()
903 .justify_center()
904 .text_center()
905 .child(
906 h_flex()
907 .id(("keymap", index))
908 .size(px(48.))
909 .rounded(px(8.))
910 .items_center()
911 .justify_center()
912 .border_1()
913 .border_color(if is_selected {
914 cx.theme().colors().border_selected
915 } else {
916 transparent_black()
917 })
918 .when(is_focused, |this| {
919 this.border_color(
920 cx.theme().colors().border_focused,
921 )
922 })
923 .when(is_selected, |this| {
924 this.bg(cx.theme().status().info.opacity(0.08))
925 })
926 .child(
927 h_flex()
928 .size(px(34.))
929 .rounded(px(6.))
930 .border_2()
931 .border_color(cx.theme().colors().border)
932 .items_center()
933 .justify_center()
934 .shadow_hairline()
935 .child(
936 Vector::new(icon, rems(1.25), rems(1.25))
937 .color(if is_selected {
938 Color::Info
939 } else {
940 Color::Default
941 }),
942 ),
943 )
944 .on_click(cx.listener(move |this, _, window, cx| {
945 SettingsStore::update_global(
946 cx,
947 move |store, cx| {
948 let base_keymap = match label {
949 "Zed" => "None",
950 "Atom" => "Atom",
951 "JetBrains" => "JetBrains",
952 "Sublime" => "SublimeText",
953 "VSCode" => "VSCode",
954 "Emacs" => "Emacs",
955 "TextMate" => "TextMate",
956 _ => "VSCode",
957 };
958 let mut settings =
959 store.raw_user_settings().clone();
960 settings["base_keymap"] =
961 serde_json::json!(base_keymap);
962 store
963 .set_user_settings(
964 &settings.to_string(),
965 cx,
966 )
967 .ok();
968 },
969 );
970 cx.notify();
971 })),
972 )
973 .child(
974 div()
975 .text_color(cx.theme().colors().text)
976 .text_size(px(12.))
977 .child(label),
978 )
979 }),
980 ),
981 ),
982 )
983 // Settings checkboxes
984 .child(
985 v_flex()
986 .gap_1()
987 .child(HeaderRow::new("Help Improve Zed"))
988 .child({
989 let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
990 CheckboxRow::new("Send Telemetry")
991 .description("Help improve Zed by sending anonymous usage data")
992 .checked(telemetry_enabled)
993 .on_click(move |_window, cx| {
994 let current = TelemetrySettings::get_global(cx).metrics;
995 SettingsStore::update_global(cx, move |store, cx| {
996 let mut settings = store.raw_user_settings().clone();
997 if settings.get("telemetry").is_none() {
998 settings["telemetry"] = serde_json::json!({});
999 }
1000 settings["telemetry"]["metrics"] = serde_json::json!(!current);
1001 store.set_user_settings(&settings.to_string(), cx).ok();
1002 });
1003 })
1004 })
1005 .child({
1006 let crash_reports_enabled = TelemetrySettings::get_global(cx).diagnostics;
1007 CheckboxRow::new("Send Crash Reports")
1008 .description("We use crash reports to help us fix issues")
1009 .checked(crash_reports_enabled)
1010 .on_click(move |_window, cx| {
1011 let current = TelemetrySettings::get_global(cx).diagnostics;
1012 SettingsStore::update_global(cx, move |store, cx| {
1013 let mut settings = store.raw_user_settings().clone();
1014 if settings.get("telemetry").is_none() {
1015 settings["telemetry"] = serde_json::json!({});
1016 }
1017 settings["telemetry"]["diagnostics"] =
1018 serde_json::json!(!current);
1019 store.set_user_settings(&settings.to_string(), cx).ok();
1020 });
1021 })
1022 }),
1023 )
1024 .into_any_element()
1025 }
1026
1027 fn render_editing_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1028 let page_index = 1; // Editing page index
1029 let focused_item = self.page_focus[page_index].0;
1030 let is_page_focused = self.focus_area == FocusArea::PageContent;
1031
1032 v_flex()
1033 .h_full()
1034 .w_full()
1035 .items_center()
1036 .justify_center()
1037 .gap_4()
1038 .child(
1039 Label::new("Editing Features")
1040 .size(LabelSize::Large)
1041 .color(Color::Default),
1042 )
1043 .child(
1044 v_flex()
1045 .gap_2()
1046 .mt_4()
1047 .child(
1048 Button::new("try_multi_cursor", "Try Multi-cursor Editing")
1049 .style(ButtonStyle::Filled)
1050 .when(is_page_focused && focused_item == 0, |this| {
1051 this.color(Color::Accent)
1052 })
1053 .on_click(cx.listener(|_, _, _, cx| {
1054 cx.notify();
1055 })),
1056 )
1057 .child(
1058 Button::new("learn_shortcuts", "Learn Keyboard Shortcuts")
1059 .style(ButtonStyle::Filled)
1060 .when(is_page_focused && focused_item == 1, |this| {
1061 this.color(Color::Accent)
1062 })
1063 .on_click(cx.listener(|_, _, _, cx| {
1064 cx.notify();
1065 })),
1066 )
1067 .child(
1068 Button::new("explore_actions", "Explore Command Palette")
1069 .style(ButtonStyle::Filled)
1070 .when(is_page_focused && focused_item == 2, |this| {
1071 this.color(Color::Accent)
1072 })
1073 .on_click(cx.listener(|_, _, _, cx| {
1074 cx.notify();
1075 })),
1076 ),
1077 )
1078 .into_any_element()
1079 }
1080
1081 fn render_ai_setup_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1082 let page_index = 2; // AI Setup page index
1083 let focused_item = self.page_focus[page_index].0;
1084 let is_page_focused = self.focus_area == FocusArea::PageContent;
1085
1086 let ai_enabled = ai_enabled(cx);
1087
1088 let workspace = self.workspace.clone();
1089
1090 v_flex()
1091 .h_full()
1092 .w_full()
1093 .gap_4()
1094 .child(
1095 h_flex()
1096 .justify_start()
1097 .child(
1098 CheckboxWithLabel::new(
1099 "disable_ai",
1100 Label::new("Enable AI Features"),
1101 if ai_enabled {
1102 ToggleState::Selected
1103 } else {
1104 ToggleState::Unselected
1105 },
1106 move |state, _, cx| {
1107 let enabled = state == &ToggleState::Selected;
1108 if let Some(workspace) = workspace.upgrade() {
1109 let fs = workspace.read(cx).app_state().fs.clone();
1110 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
1111 file.features
1112 .get_or_insert(Default::default())
1113 .ai_assistance = Some(enabled);
1114 });
1115 }
1116 },
1117 )))
1118 .child(
1119 CalloutRow::new("We don't use your code to train AI models")
1120 .line("You choose which providers you enable, and they have their own privacy policies.")
1121 .line("Read more about our privacy practices in our Privacy Policy.")
1122 )
1123 .child(
1124 HeaderRow::new("Choose your AI Providers")
1125 )
1126 .into_any_element()
1127 }
1128
1129 fn render_welcome_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1130 // Lazy-initialize the welcome page if needed
1131 if self.welcome_page.is_none() {
1132 if let Some(workspace) = self.workspace.upgrade() {
1133 let _ = workspace.update(cx, |workspace, cx| {
1134 self.welcome_page = Some(WelcomePage::new(workspace, cx));
1135 });
1136 }
1137 }
1138
1139 // Render the welcome page if it exists, otherwise show a fallback
1140 if let Some(welcome_page) = &self.welcome_page {
1141 welcome_page.clone().into_any_element()
1142 } else {
1143 // Fallback UI if we couldn't create the welcome page
1144 v_flex()
1145 .h_full()
1146 .w_full()
1147 .items_center()
1148 .justify_center()
1149 .child(
1150 Label::new("Unable to load welcome page")
1151 .size(LabelSize::Default)
1152 .color(Color::Error),
1153 )
1154 .into_any_element()
1155 }
1156 }
1157}
1158
1159impl Item for OnboardingUI {
1160 type Event = ItemEvent;
1161
1162 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1163 "Onboarding".into()
1164 }
1165
1166 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1167 f(event.clone())
1168 }
1169
1170 fn added_to_workspace(
1171 &mut self,
1172 workspace: &mut Workspace,
1173 _window: &mut Window,
1174 _cx: &mut Context<Self>,
1175 ) {
1176 self.workspace_id = workspace.database_id();
1177 }
1178
1179 fn show_toolbar(&self) -> bool {
1180 false
1181 }
1182
1183 fn clone_on_split(
1184 &self,
1185 _workspace_id: Option<WorkspaceId>,
1186 window: &mut Window,
1187 cx: &mut Context<Self>,
1188 ) -> Option<Entity<Self>> {
1189 let weak_workspace = self.workspace.clone();
1190 let client = self.client.clone();
1191 if let Some(workspace) = weak_workspace.upgrade() {
1192 workspace.update(cx, |workspace, cx| {
1193 Some(cx.new(|cx| OnboardingUI::new(workspace, client, cx)))
1194 })
1195 } else {
1196 None
1197 }
1198 }
1199}
1200
1201impl SerializableItem for OnboardingUI {
1202 fn serialized_item_kind() -> &'static str {
1203 "OnboardingUI"
1204 }
1205
1206 fn deserialize(
1207 _project: Entity<Project>,
1208 workspace: WeakEntity<Workspace>,
1209 workspace_id: WorkspaceId,
1210 item_id: u64,
1211 window: &mut Window,
1212 cx: &mut App,
1213 ) -> Task<anyhow::Result<Entity<Self>>> {
1214 window.spawn(cx, async move |cx| {
1215 let (current_page, completed_pages) = if let Some((page_str, completed_str)) =
1216 ONBOARDING_DB.get_state(item_id, workspace_id)?
1217 {
1218 let page = match page_str.as_str() {
1219 "basics" => OnboardingPage::Basics,
1220 "editing" => OnboardingPage::Editing,
1221 "ai_setup" => OnboardingPage::AiSetup,
1222 "welcome" => OnboardingPage::Welcome,
1223 _ => OnboardingPage::Basics,
1224 };
1225 let completed = OnboardingUI::completed_pages_from_string(&completed_str);
1226 (page, completed)
1227 } else {
1228 (OnboardingPage::Basics, HashSet::new())
1229 };
1230
1231 cx.update(|window, cx| {
1232 let workspace = workspace
1233 .upgrade()
1234 .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?;
1235
1236 workspace.update(cx, |workspace, cx| {
1237 let client = workspace.client().clone();
1238 Ok(cx.new(|cx| {
1239 let mut onboarding = OnboardingUI::new(workspace, client, cx);
1240 onboarding.current_page = current_page;
1241 onboarding.completed_pages = completed_pages;
1242 onboarding
1243 }))
1244 })
1245 })?
1246 })
1247 }
1248
1249 fn serialize(
1250 &mut self,
1251 _workspace: &mut Workspace,
1252 item_id: u64,
1253 _closing: bool,
1254 _window: &mut Window,
1255 cx: &mut Context<Self>,
1256 ) -> Option<Task<anyhow::Result<()>>> {
1257 let workspace_id = self.workspace_id?;
1258 let current_page = match self.current_page {
1259 OnboardingPage::Basics => "basics",
1260 OnboardingPage::Editing => "editing",
1261 OnboardingPage::AiSetup => "ai_setup",
1262 OnboardingPage::Welcome => "welcome",
1263 }
1264 .to_string();
1265 let completed_pages = self.completed_pages_to_string();
1266
1267 Some(cx.background_spawn(async move {
1268 ONBOARDING_DB
1269 .save_state(item_id, workspace_id, current_page, completed_pages)
1270 .await
1271 }))
1272 }
1273
1274 fn cleanup(
1275 _workspace_id: WorkspaceId,
1276 _item_ids: Vec<u64>,
1277 _window: &mut Window,
1278 _cx: &mut App,
1279 ) -> Task<anyhow::Result<()>> {
1280 Task::ready(Ok(()))
1281 }
1282
1283 fn should_serialize(&self, _event: &ItemEvent) -> bool {
1284 true
1285 }
1286}