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 set_ai_assistance(
499 &mut self,
500 selection: &ToggleState,
501 _: &mut Window,
502 cx: &mut Context<Self>,
503 ) {
504 let enabled = selection == &ToggleState::Selected;
505
506 if let Some(workspace) = self.workspace.upgrade() {
507 let fs = workspace.read(cx).app_state().fs.clone();
508 AllLanguageSettings::set_ai_assistance(enabled, fs, cx);
509
510 cx.notify();
511 }
512 }
513
514 fn handle_jump_to_basics(
515 &mut self,
516 _: &JumpToBasics,
517 window: &mut Window,
518 cx: &mut Context<Self>,
519 ) {
520 self.jump_to_page(OnboardingPage::Basics, window, cx);
521 }
522
523 fn handle_jump_to_editing(
524 &mut self,
525 _: &JumpToEditing,
526 window: &mut Window,
527 cx: &mut Context<Self>,
528 ) {
529 self.jump_to_page(OnboardingPage::Editing, window, cx);
530 }
531
532 fn handle_jump_to_ai_setup(
533 &mut self,
534 _: &JumpToAiSetup,
535 window: &mut Window,
536 cx: &mut Context<Self>,
537 ) {
538 self.jump_to_page(OnboardingPage::AiSetup, window, cx);
539 }
540
541 fn handle_jump_to_welcome(
542 &mut self,
543 _: &JumpToWelcome,
544 window: &mut Window,
545 cx: &mut Context<Self>,
546 ) {
547 self.jump_to_page(OnboardingPage::Welcome, window, cx);
548 }
549
550 fn handle_next_page(&mut self, _: &NextPage, window: &mut Window, cx: &mut Context<Self>) {
551 self.next_page(window, cx);
552 }
553
554 fn handle_enable_ai_assistance(
555 &mut self,
556 _: &zed_actions::EnableAiAssistance,
557 _window: &mut Window,
558 cx: &mut Context<Self>,
559 ) {
560 if let Some(workspace) = self.workspace.upgrade() {
561 let fs = workspace.read(cx).app_state().fs.clone();
562 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
563 file.features
564 .get_or_insert(Default::default())
565 .ai_assistance = Some(true);
566 });
567 cx.notify();
568 }
569 }
570
571 fn handle_disable_ai_assistance(
572 &mut self,
573 _: &zed_actions::DisableAiAssistance,
574 _window: &mut Window,
575 cx: &mut Context<Self>,
576 ) {
577 if let Some(workspace) = self.workspace.upgrade() {
578 let fs = workspace.read(cx).app_state().fs.clone();
579 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
580 file.features
581 .get_or_insert(Default::default())
582 .ai_assistance = Some(false);
583 });
584 cx.notify();
585 }
586 }
587
588 fn handle_previous_page(
589 &mut self,
590 _: &PreviousPage,
591 window: &mut Window,
592 cx: &mut Context<Self>,
593 ) {
594 self.previous_page(window, cx);
595 }
596
597 fn render_navigation(
598 &mut self,
599 window: &mut Window,
600 cx: &mut Context<Self>,
601 ) -> impl gpui::IntoElement {
602 let client = self.client.clone();
603
604 v_flex()
605 .h_full()
606 .w(px(256.))
607 .gap_2()
608 .justify_between()
609 .child(
610 v_flex()
611 .w_full()
612 .gap_px()
613 .child(
614 h_flex()
615 .w_full()
616 .justify_between()
617 .py(px(24.))
618 .pl(px(24.))
619 .pr(px(12.))
620 .child(
621 Vector::new(VectorName::ZedLogo, rems(2.), rems(2.))
622 .color(Color::Custom(cx.theme().colors().icon.opacity(0.5))),
623 )
624 .child(
625 Button::new("sign_in", "Sign in")
626 .color(Color::Muted)
627 .label_size(LabelSize::Small)
628 .when(
629 self.focus_area == FocusArea::Navigation
630 && self.nav_focus == NavigationFocusItem::SignIn,
631 |this| this.color(Color::Accent),
632 )
633 .size(ButtonSize::Compact)
634 .on_click(cx.listener(move |_, _, window, cx| {
635 let client = client.clone();
636 window
637 .spawn(cx, async move |cx| {
638 client
639 .authenticate_and_connect(true, &cx)
640 .await
641 .into_response()
642 .notify_async_err(cx);
643 })
644 .detach();
645 })),
646 ),
647 )
648 .child(
649 v_flex()
650 .gap_px()
651 .py(px(16.))
652 .gap(px(2.))
653 .child(self.render_nav_item(
654 OnboardingPage::Basics,
655 "The Basics",
656 "1",
657 cx,
658 ))
659 .child(self.render_nav_item(
660 OnboardingPage::Editing,
661 "Editing Experience",
662 "2",
663 cx,
664 ))
665 .child(self.render_nav_item(
666 OnboardingPage::AiSetup,
667 "AI Setup",
668 "3",
669 cx,
670 ))
671 .child(self.render_nav_item(
672 OnboardingPage::Welcome,
673 "Welcome",
674 "4",
675 cx,
676 )),
677 ),
678 )
679 .child(self.render_bottom_controls(window, cx))
680 }
681
682 fn render_nav_item(
683 &mut self,
684 page: OnboardingPage,
685 label: impl Into<SharedString>,
686 shortcut: impl Into<SharedString>,
687 cx: &mut Context<Self>,
688 ) -> impl gpui::IntoElement {
689 let is_selected = self.current_page == page;
690 let label = label.into();
691 let shortcut = shortcut.into();
692 let id = ElementId::Name(label.clone());
693 let corner_radius = px(4.);
694
695 let item_focused = match page {
696 OnboardingPage::Basics => self.nav_focus == NavigationFocusItem::Basics,
697 OnboardingPage::Editing => self.nav_focus == NavigationFocusItem::Editing,
698 OnboardingPage::AiSetup => self.nav_focus == NavigationFocusItem::AiSetup,
699 OnboardingPage::Welcome => self.nav_focus == NavigationFocusItem::Welcome,
700 };
701
702 let area_focused = self.focus_area == FocusArea::Navigation;
703
704 FocusOutline::new(corner_radius, item_focused, px(2.))
705 .active(area_focused && item_focused)
706 .child(
707 h_flex()
708 .id(id)
709 .h(rems(1.625))
710 .w_full()
711 .rounded(corner_radius)
712 .px_3()
713 .when(is_selected, |this| {
714 this.bg(cx.theme().colors().border_focused.opacity(0.16))
715 })
716 .child(
717 h_flex()
718 .flex_1()
719 .justify_between()
720 .items_center()
721 .child(
722 Label::new(label)
723 .weight(FontWeight::MEDIUM)
724 .color(Color::Muted)
725 .when(item_focused, |this| this.color(Color::Default)),
726 )
727 .child(
728 Label::new(format!("⌘{}", shortcut.clone()))
729 .color(Color::Placeholder)
730 .size(LabelSize::XSmall),
731 ),
732 )
733 .on_click(cx.listener(move |this, _, window, cx| {
734 this.jump_to_page(page, window, cx);
735 })),
736 )
737 }
738
739 fn render_bottom_controls(
740 &mut self,
741 window: &mut gpui::Window,
742 cx: &mut Context<Self>,
743 ) -> impl gpui::IntoElement {
744 h_flex().w_full().p(px(12.)).child(
745 JuicyButton::new(if self.current_page == OnboardingPage::Welcome {
746 "Get Started"
747 } else {
748 "Next"
749 })
750 .keybinding(ui::KeyBinding::for_action_in(
751 &NextPage,
752 &self.focus_handle,
753 window,
754 cx,
755 ))
756 .on_click(cx.listener(|this, _, window, cx| {
757 this.next_page(window, cx);
758 })),
759 )
760 }
761
762 fn render_active_page(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
763 match self.current_page {
764 OnboardingPage::Basics => self.render_basics_page(cx),
765 OnboardingPage::Editing => self.render_editing_page(cx),
766 OnboardingPage::AiSetup => self.render_ai_setup_page(cx),
767 OnboardingPage::Welcome => self.render_welcome_page(cx),
768 }
769 }
770
771 fn render_basics_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
772 let page_index = 0; // Basics page index
773 let focused_item = self.page_focus[page_index].0;
774 let is_page_focused = self.focus_area == FocusArea::PageContent;
775
776 use theme_preview::ThemePreviewTile;
777
778 // Get available themes
779 let theme_registry = ThemeRegistry::default_global(cx);
780 let theme_names = theme_registry.list_names();
781 let current_theme = cx.theme().clone();
782
783 v_flex()
784 .id("theme-selector")
785 .h_full()
786 .w_full()
787 .overflow_y_scroll()
788 .child({
789 let vim_enabled = VimModeSetting::get_global(cx).0;
790 CheckboxRow::new("Enable Vim Mode")
791 .checked(vim_enabled)
792 .on_click(move |_window, cx| {
793 let current = VimModeSetting::get_global(cx).0;
794 SettingsStore::update_global(cx, move |store, cx| {
795 let mut settings = store.raw_user_settings().clone();
796 settings["vim_mode"] = serde_json::json!(!current);
797 store.set_user_settings(&settings.to_string(), cx).ok();
798 });
799 })
800 })
801 // Theme selector section
802 .child(
803 v_flex()
804 .w_full()
805 .overflow_hidden()
806 .child(
807 HeaderRow::new("Pick a Theme").end_slot(
808 Button::new("more_themes", "More Themes")
809 .style(ButtonStyle::Subtle)
810 .color(Color::Muted)
811 .on_click(cx.listener(|_, _, window, cx| {
812 window.dispatch_action(
813 zed_actions::theme_selector::Toggle::default()
814 .boxed_clone(),
815 cx,
816 );
817 })),
818 ),
819 )
820 .child(
821 h_flex().w_full().overflow_hidden().gap_3().children(
822 vec![
823 ("One Dark", "One Dark"),
824 ("Gruvbox Dark", "Gruvbox Dark"),
825 ("One Light", "One Light"),
826 ("Gruvbox Light", "Gruvbox Light"),
827 ]
828 .into_iter()
829 .enumerate()
830 .map(|(i, (label, theme_name))| {
831 let is_selected = current_theme.name == *theme_name;
832 let is_focused = is_page_focused && focused_item == i;
833
834 v_flex()
835 .flex_1()
836 .gap_1p5()
837 .justify_center()
838 .text_center()
839 .child(
840 div()
841 .id(("theme", i))
842 .rounded(px(8.))
843 .h(px(90.))
844 .w_full()
845 .overflow_hidden()
846 .border_1()
847 .border_color(if is_focused {
848 cx.theme().colors().border_focused
849 } else {
850 transparent_black()
851 })
852 .child(
853 if let Ok(theme) = theme_registry.get(theme_name) {
854 ThemePreviewTile::new(theme, is_selected, 0.5)
855 .into_any_element()
856 } else {
857 div()
858 .size_full()
859 .bg(cx.theme().colors().surface_background)
860 .rounded_md()
861 .into_any_element()
862 },
863 )
864 .on_click(cx.listener(move |this, _, window, cx| {
865 SettingsStore::update_global(
866 cx,
867 move |store, cx| {
868 let mut settings =
869 store.raw_user_settings().clone();
870 settings["theme"] =
871 serde_json::json!(theme_name);
872 store
873 .set_user_settings(
874 &settings.to_string(),
875 cx,
876 )
877 .ok();
878 },
879 );
880 cx.notify();
881 })),
882 )
883 .child(
884 div()
885 .text_color(cx.theme().colors().text)
886 .text_size(px(12.))
887 .child(label),
888 )
889 }),
890 ),
891 ),
892 )
893 // Keymap selector section
894 .child(
895 v_flex()
896 .gap_1()
897 .child(HeaderRow::new("Pick a Keymap"))
898 .child(
899 h_flex().gap_2().children(
900 vec![
901 ("Zed", VectorName::ZedLogo, 4),
902 ("Atom", VectorName::AtomLogo, 5),
903 ("JetBrains", VectorName::ZedLogo, 6),
904 ("Sublime", VectorName::ZedLogo, 7),
905 ("VSCode", VectorName::ZedLogo, 8),
906 ("Emacs", VectorName::ZedLogo, 9),
907 ("TextMate", VectorName::ZedLogo, 10),
908 ]
909 .into_iter()
910 .map(|(label, icon, index)| {
911 let is_focused = is_page_focused && focused_item == index;
912 let current_keymap = BaseKeymap::get_global(cx).to_string();
913 let is_selected = current_keymap == label;
914
915 v_flex()
916 .w(px(72.))
917 .gap_1()
918 .items_center()
919 .justify_center()
920 .text_center()
921 .child(
922 h_flex()
923 .id(("keymap", index))
924 .size(px(48.))
925 .rounded(px(8.))
926 .items_center()
927 .justify_center()
928 .border_1()
929 .border_color(if is_selected {
930 cx.theme().colors().border_selected
931 } else {
932 transparent_black()
933 })
934 .when(is_focused, |this| {
935 this.border_color(
936 cx.theme().colors().border_focused,
937 )
938 })
939 .when(is_selected, |this| {
940 this.bg(cx.theme().status().info.opacity(0.08))
941 })
942 .child(
943 h_flex()
944 .size(px(34.))
945 .rounded(px(6.))
946 .border_2()
947 .border_color(cx.theme().colors().border)
948 .items_center()
949 .justify_center()
950 .shadow_hairline()
951 .child(
952 Vector::new(icon, rems(1.25), rems(1.25))
953 .color(if is_selected {
954 Color::Info
955 } else {
956 Color::Default
957 }),
958 ),
959 )
960 .on_click(cx.listener(move |this, _, window, cx| {
961 SettingsStore::update_global(
962 cx,
963 move |store, cx| {
964 let base_keymap = match label {
965 "Zed" => "None",
966 "Atom" => "Atom",
967 "JetBrains" => "JetBrains",
968 "Sublime" => "SublimeText",
969 "VSCode" => "VSCode",
970 "Emacs" => "Emacs",
971 "TextMate" => "TextMate",
972 _ => "VSCode",
973 };
974 let mut settings =
975 store.raw_user_settings().clone();
976 settings["base_keymap"] =
977 serde_json::json!(base_keymap);
978 store
979 .set_user_settings(
980 &settings.to_string(),
981 cx,
982 )
983 .ok();
984 },
985 );
986 cx.notify();
987 })),
988 )
989 .child(
990 div()
991 .text_color(cx.theme().colors().text)
992 .text_size(px(12.))
993 .child(label),
994 )
995 }),
996 ),
997 ),
998 )
999 // Settings checkboxes
1000 .child(
1001 v_flex()
1002 .gap_1()
1003 .child(HeaderRow::new("Help Improve Zed"))
1004 .child({
1005 let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
1006 CheckboxRow::new("Send Telemetry")
1007 .description("Help improve Zed by sending anonymous usage data")
1008 .checked(telemetry_enabled)
1009 .on_click(move |_window, cx| {
1010 let current = TelemetrySettings::get_global(cx).metrics;
1011 SettingsStore::update_global(cx, move |store, cx| {
1012 let mut settings = store.raw_user_settings().clone();
1013 if settings.get("telemetry").is_none() {
1014 settings["telemetry"] = serde_json::json!({});
1015 }
1016 settings["telemetry"]["metrics"] = serde_json::json!(!current);
1017 store.set_user_settings(&settings.to_string(), cx).ok();
1018 });
1019 })
1020 })
1021 .child({
1022 let crash_reports_enabled = TelemetrySettings::get_global(cx).diagnostics;
1023 CheckboxRow::new("Send Crash Reports")
1024 .description("We use crash reports to help us fix issues")
1025 .checked(crash_reports_enabled)
1026 .on_click(move |_window, cx| {
1027 let current = TelemetrySettings::get_global(cx).diagnostics;
1028 SettingsStore::update_global(cx, move |store, cx| {
1029 let mut settings = store.raw_user_settings().clone();
1030 if settings.get("telemetry").is_none() {
1031 settings["telemetry"] = serde_json::json!({});
1032 }
1033 settings["telemetry"]["diagnostics"] =
1034 serde_json::json!(!current);
1035 store.set_user_settings(&settings.to_string(), cx).ok();
1036 });
1037 })
1038 }),
1039 )
1040 .into_any_element()
1041 }
1042
1043 fn render_editing_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1044 let page_index = 1; // Editing page index
1045 let focused_item = self.page_focus[page_index].0;
1046 let is_page_focused = self.focus_area == FocusArea::PageContent;
1047
1048 v_flex()
1049 .h_full()
1050 .w_full()
1051 .items_center()
1052 .justify_center()
1053 .gap_4()
1054 .child(
1055 Label::new("Editing Features")
1056 .size(LabelSize::Large)
1057 .color(Color::Default),
1058 )
1059 .child(
1060 v_flex()
1061 .gap_2()
1062 .mt_4()
1063 .child(
1064 Button::new("try_multi_cursor", "Try Multi-cursor Editing")
1065 .style(ButtonStyle::Filled)
1066 .when(is_page_focused && focused_item == 0, |this| {
1067 this.color(Color::Accent)
1068 })
1069 .on_click(cx.listener(|_, _, _, cx| {
1070 cx.notify();
1071 })),
1072 )
1073 .child(
1074 Button::new("learn_shortcuts", "Learn Keyboard Shortcuts")
1075 .style(ButtonStyle::Filled)
1076 .when(is_page_focused && focused_item == 1, |this| {
1077 this.color(Color::Accent)
1078 })
1079 .on_click(cx.listener(|_, _, _, cx| {
1080 cx.notify();
1081 })),
1082 )
1083 .child(
1084 Button::new("explore_actions", "Explore Command Palette")
1085 .style(ButtonStyle::Filled)
1086 .when(is_page_focused && focused_item == 2, |this| {
1087 this.color(Color::Accent)
1088 })
1089 .on_click(cx.listener(|_, _, _, cx| {
1090 cx.notify();
1091 })),
1092 ),
1093 )
1094 .into_any_element()
1095 }
1096
1097 fn render_ai_setup_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1098 let page_index = 2; // AI Setup page index
1099 let focused_item = self.page_focus[page_index].0;
1100 let is_page_focused = self.focus_area == FocusArea::PageContent;
1101
1102 let ai_enabled = all_language_settings(None, cx).is_ai_assistance_enabled();
1103
1104 v_flex()
1105 .h_full()
1106 .w_full()
1107 .gap_4()
1108 .child(
1109 h_flex()
1110 .justify_start()
1111 .child(
1112 CheckboxWithLabel::new(
1113 "disable_ai",
1114 Label::new("Enable AI Features"),
1115 if ai_enabled {
1116 ToggleState::Selected
1117 } else {
1118 ToggleState::Unselected
1119 },
1120 cx.listener(Self::set_ai_assistance),
1121 )))
1122 .child(
1123 CalloutRow::new("We don't use your code to train AI models")
1124 .line("You choose which providers you enable, and they have their own privacy policies.")
1125 .line("Read more about our privacy practices in our Privacy Policy.")
1126 )
1127 .child(
1128 HeaderRow::new("Choose your AI Providers")
1129 )
1130 .into_any_element()
1131 }
1132
1133 fn render_welcome_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1134 // Lazy-initialize the welcome page if needed
1135 if self.welcome_page.is_none() {
1136 if let Some(workspace) = self.workspace.upgrade() {
1137 let _ = workspace.update(cx, |workspace, cx| {
1138 self.welcome_page = Some(WelcomePage::new(workspace, cx));
1139 });
1140 }
1141 }
1142
1143 // Render the welcome page if it exists, otherwise show a fallback
1144 if let Some(welcome_page) = &self.welcome_page {
1145 welcome_page.clone().into_any_element()
1146 } else {
1147 // Fallback UI if we couldn't create the welcome page
1148 v_flex()
1149 .h_full()
1150 .w_full()
1151 .items_center()
1152 .justify_center()
1153 .child(
1154 Label::new("Unable to load welcome page")
1155 .size(LabelSize::Default)
1156 .color(Color::Error),
1157 )
1158 .into_any_element()
1159 }
1160 }
1161}
1162
1163impl Item for OnboardingUI {
1164 type Event = ItemEvent;
1165
1166 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1167 "Onboarding".into()
1168 }
1169
1170 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1171 f(event.clone())
1172 }
1173
1174 fn added_to_workspace(
1175 &mut self,
1176 workspace: &mut Workspace,
1177 _window: &mut Window,
1178 _cx: &mut Context<Self>,
1179 ) {
1180 self.workspace_id = workspace.database_id();
1181 }
1182
1183 fn show_toolbar(&self) -> bool {
1184 false
1185 }
1186
1187 fn clone_on_split(
1188 &self,
1189 _workspace_id: Option<WorkspaceId>,
1190 window: &mut Window,
1191 cx: &mut Context<Self>,
1192 ) -> Option<Entity<Self>> {
1193 let weak_workspace = self.workspace.clone();
1194 let client = self.client.clone();
1195 if let Some(workspace) = weak_workspace.upgrade() {
1196 workspace.update(cx, |workspace, cx| {
1197 Some(cx.new(|cx| OnboardingUI::new(workspace, client, cx)))
1198 })
1199 } else {
1200 None
1201 }
1202 }
1203}
1204
1205impl SerializableItem for OnboardingUI {
1206 fn serialized_item_kind() -> &'static str {
1207 "OnboardingUI"
1208 }
1209
1210 fn deserialize(
1211 _project: Entity<Project>,
1212 workspace: WeakEntity<Workspace>,
1213 workspace_id: WorkspaceId,
1214 item_id: u64,
1215 window: &mut Window,
1216 cx: &mut App,
1217 ) -> Task<anyhow::Result<Entity<Self>>> {
1218 window.spawn(cx, async move |cx| {
1219 let (current_page, completed_pages) = if let Some((page_str, completed_str)) =
1220 ONBOARDING_DB.get_state(item_id, workspace_id)?
1221 {
1222 let page = match page_str.as_str() {
1223 "basics" => OnboardingPage::Basics,
1224 "editing" => OnboardingPage::Editing,
1225 "ai_setup" => OnboardingPage::AiSetup,
1226 "welcome" => OnboardingPage::Welcome,
1227 _ => OnboardingPage::Basics,
1228 };
1229 let completed = OnboardingUI::completed_pages_from_string(&completed_str);
1230 (page, completed)
1231 } else {
1232 (OnboardingPage::Basics, HashSet::new())
1233 };
1234
1235 cx.update(|window, cx| {
1236 let workspace = workspace
1237 .upgrade()
1238 .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?;
1239
1240 workspace.update(cx, |workspace, cx| {
1241 let client = workspace.client().clone();
1242 Ok(cx.new(|cx| {
1243 let mut onboarding = OnboardingUI::new(workspace, client, cx);
1244 onboarding.current_page = current_page;
1245 onboarding.completed_pages = completed_pages;
1246 onboarding
1247 }))
1248 })
1249 })?
1250 })
1251 }
1252
1253 fn serialize(
1254 &mut self,
1255 _workspace: &mut Workspace,
1256 item_id: u64,
1257 _closing: bool,
1258 _window: &mut Window,
1259 cx: &mut Context<Self>,
1260 ) -> Option<Task<anyhow::Result<()>>> {
1261 let workspace_id = self.workspace_id?;
1262 let current_page = match self.current_page {
1263 OnboardingPage::Basics => "basics",
1264 OnboardingPage::Editing => "editing",
1265 OnboardingPage::AiSetup => "ai_setup",
1266 OnboardingPage::Welcome => "welcome",
1267 }
1268 .to_string();
1269 let completed_pages = self.completed_pages_to_string();
1270
1271 Some(cx.background_spawn(async move {
1272 ONBOARDING_DB
1273 .save_state(item_id, workspace_id, current_page, completed_pages)
1274 .await
1275 }))
1276 }
1277
1278 fn cleanup(
1279 _workspace_id: WorkspaceId,
1280 _item_ids: Vec<u64>,
1281 _window: &mut Window,
1282 _cx: &mut App,
1283 ) -> Task<anyhow::Result<()>> {
1284 Task::ready(Ok(()))
1285 }
1286
1287 fn should_serialize(&self, _event: &ItemEvent) -> bool {
1288 true
1289 }
1290}