1#![allow(unused, dead_code)]
2mod persistence;
3
4use client::Client;
5use command_palette_hooks::CommandPaletteFilter;
6use feature_flags::FeatureFlagAppExt as _;
7use gpui::{
8 Entity, EventEmitter, FocusHandle, Focusable, KeyBinding, Task, WeakEntity, actions, prelude::*,
9};
10use menu;
11use persistence::ONBOARDING_DB;
12
13use project::Project;
14use settings_ui::SettingsUiFeatureFlag;
15use std::sync::Arc;
16use ui::{ListItem, Vector, VectorName, prelude::*};
17use util::ResultExt;
18use workspace::{
19 Workspace, WorkspaceId,
20 item::{Item, ItemEvent, SerializableItem},
21 notifications::NotifyResultExt,
22};
23
24actions!(
25 onboarding,
26 [
27 ShowOnboarding,
28 JumpToBasics,
29 JumpToEditing,
30 JumpToAiSetup,
31 JumpToWelcome,
32 NextPage,
33 PreviousPage,
34 ToggleFocus,
35 ResetOnboarding,
36 ]
37);
38
39pub fn init(cx: &mut App) {
40 cx.observe_new(|workspace: &mut Workspace, _, _cx| {
41 workspace.register_action(|workspace, _: &ShowOnboarding, window, cx| {
42 let client = workspace.client().clone();
43 let onboarding = cx.new(|cx| OnboardingUI::new(workspace, client, cx));
44 workspace.add_item_to_active_pane(Box::new(onboarding), None, true, window, cx);
45 });
46 })
47 .detach();
48
49 workspace::register_serializable_item::<OnboardingUI>(cx);
50
51 feature_gate_onboarding_ui_actions(cx);
52}
53
54fn feature_gate_onboarding_ui_actions(cx: &mut App) {
55 const ONBOARDING_ACTION_NAMESPACE: &str = "onboarding_ui";
56
57 CommandPaletteFilter::update_global(cx, |filter, _cx| {
58 filter.hide_namespace(ONBOARDING_ACTION_NAMESPACE);
59 });
60
61 cx.observe_flag::<SettingsUiFeatureFlag, _>({
62 move |is_enabled, cx| {
63 CommandPaletteFilter::update_global(cx, |filter, _cx| {
64 if is_enabled {
65 filter.show_namespace(ONBOARDING_ACTION_NAMESPACE);
66 } else {
67 filter.hide_namespace(ONBOARDING_ACTION_NAMESPACE);
68 }
69 });
70 }
71 })
72 .detach();
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum OnboardingPage {
77 Basics,
78 Editing,
79 AiSetup,
80 Welcome,
81}
82
83impl OnboardingPage {
84 fn next(&self) -> Option<Self> {
85 match self {
86 Self::Basics => Some(Self::Editing),
87 Self::Editing => Some(Self::AiSetup),
88 Self::AiSetup => Some(Self::Welcome),
89 Self::Welcome => None,
90 }
91 }
92
93 fn previous(&self) -> Option<Self> {
94 match self {
95 Self::Basics => None,
96 Self::Editing => Some(Self::Basics),
97 Self::AiSetup => Some(Self::Editing),
98 Self::Welcome => Some(Self::AiSetup),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum NavigationFocusItem {
105 SignIn,
106 Basics,
107 Editing,
108 AiSetup,
109 Welcome,
110 Next,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct PageFocusItem(pub usize);
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum FocusArea {
118 Navigation,
119 PageContent,
120}
121
122pub struct OnboardingUI {
123 focus_handle: FocusHandle,
124 current_page: OnboardingPage,
125 nav_focus: NavigationFocusItem,
126 page_focus: [PageFocusItem; 4],
127 completed_pages: [bool; 4],
128 focus_area: FocusArea,
129
130 // Workspace reference for Item trait
131 workspace: WeakEntity<Workspace>,
132 workspace_id: Option<WorkspaceId>,
133 client: Arc<Client>,
134}
135
136impl EventEmitter<ItemEvent> for OnboardingUI {}
137
138impl Focusable for OnboardingUI {
139 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
140 self.focus_handle.clone()
141 }
142}
143
144#[derive(Clone)]
145pub enum OnboardingEvent {
146 PageCompleted(OnboardingPage),
147}
148
149impl Render for OnboardingUI {
150 fn render(
151 &mut self,
152 window: &mut gpui::Window,
153 cx: &mut Context<Self>,
154 ) -> impl gpui::IntoElement {
155 div()
156 .bg(cx.theme().colors().editor_background)
157 .size_full()
158 .key_context("OnboardingUI")
159 .on_action(cx.listener(Self::select_next))
160 .on_action(cx.listener(Self::select_previous))
161 .on_action(cx.listener(Self::confirm))
162 .on_action(cx.listener(Self::cancel))
163 .on_action(cx.listener(Self::toggle_focus))
164 .flex()
165 .items_center()
166 .justify_center()
167 .overflow_hidden()
168 .child(
169 h_flex()
170 .id("onboarding-ui")
171 .key_context("Onboarding")
172 .track_focus(&self.focus_handle)
173 .on_action(cx.listener(Self::handle_jump_to_basics))
174 .on_action(cx.listener(Self::handle_jump_to_editing))
175 .on_action(cx.listener(Self::handle_jump_to_ai_setup))
176 .on_action(cx.listener(Self::handle_jump_to_welcome))
177 .on_action(cx.listener(Self::handle_next_page))
178 .on_action(cx.listener(Self::handle_previous_page))
179 .w(px(904.))
180 .gap(px(24.))
181 .child(
182 h_flex()
183 .h(px(500.))
184 .w_full()
185 .gap(px(48.))
186 .child(self.render_navigation(window, cx))
187 .child(
188 v_flex()
189 .h_full()
190 .flex_1()
191 .when(self.focus_area == FocusArea::PageContent, |this| {
192 this.border_2()
193 .border_color(cx.theme().colors().border_focused)
194 })
195 .rounded_lg()
196 .p_4()
197 .child(
198 div().flex_1().child(self.render_active_page(window, cx)),
199 ),
200 ),
201 ),
202 )
203 }
204}
205
206impl OnboardingUI {
207 pub fn new(workspace: &Workspace, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
208 Self {
209 focus_handle: cx.focus_handle(),
210 current_page: OnboardingPage::Basics,
211 nav_focus: NavigationFocusItem::Basics,
212 page_focus: [PageFocusItem(0); 4],
213 completed_pages: [false; 4],
214 focus_area: FocusArea::Navigation,
215 workspace: workspace.weak_handle(),
216 workspace_id: workspace.database_id(),
217 client,
218 }
219 }
220
221 fn completed_pages_to_string(&self) -> String {
222 self.completed_pages
223 .iter()
224 .map(|&completed| if completed { '1' } else { '0' })
225 .collect()
226 }
227
228 fn completed_pages_from_string(s: &str) -> [bool; 4] {
229 let mut result = [false; 4];
230 for (i, ch) in s.chars().take(4).enumerate() {
231 result[i] = ch == '1';
232 }
233 result
234 }
235
236 fn jump_to_page(
237 &mut self,
238 page: OnboardingPage,
239 _window: &mut gpui::Window,
240 cx: &mut Context<Self>,
241 ) {
242 self.current_page = page;
243 cx.emit(ItemEvent::UpdateTab);
244 cx.notify();
245 }
246
247 fn next_page(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) {
248 if let Some(next) = self.current_page.next() {
249 self.current_page = next;
250 cx.notify();
251 }
252 }
253
254 fn previous_page(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) {
255 if let Some(prev) = self.current_page.previous() {
256 self.current_page = prev;
257 cx.notify();
258 }
259 }
260
261 fn reset(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) {
262 self.current_page = OnboardingPage::Basics;
263 self.focus_area = FocusArea::Navigation;
264 self.completed_pages = [false; 4];
265 cx.notify();
266 }
267
268 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
269 match self.focus_area {
270 FocusArea::Navigation => {
271 self.nav_focus = match self.nav_focus {
272 NavigationFocusItem::SignIn => NavigationFocusItem::Basics,
273 NavigationFocusItem::Basics => NavigationFocusItem::Editing,
274 NavigationFocusItem::Editing => NavigationFocusItem::AiSetup,
275 NavigationFocusItem::AiSetup => NavigationFocusItem::Welcome,
276 NavigationFocusItem::Welcome => NavigationFocusItem::Next,
277 NavigationFocusItem::Next => NavigationFocusItem::SignIn,
278 };
279 }
280 FocusArea::PageContent => {
281 let page_index = match self.current_page {
282 OnboardingPage::Basics => 0,
283 OnboardingPage::Editing => 1,
284 OnboardingPage::AiSetup => 2,
285 OnboardingPage::Welcome => 3,
286 };
287 // Bounds checking for page items
288 let max_items = match self.current_page {
289 OnboardingPage::Basics => 3, // 3 buttons
290 OnboardingPage::Editing => 3, // 3 buttons
291 OnboardingPage::AiSetup => 2, // Will have 2 items
292 OnboardingPage::Welcome => 1, // Will have 1 item
293 };
294
295 if self.page_focus[page_index].0 < max_items - 1 {
296 self.page_focus[page_index].0 += 1;
297 } else {
298 // Wrap to start
299 self.page_focus[page_index].0 = 0;
300 }
301 }
302 }
303 cx.notify();
304 }
305
306 fn select_previous(
307 &mut self,
308 _: &menu::SelectPrevious,
309 _window: &mut Window,
310 cx: &mut Context<Self>,
311 ) {
312 match self.focus_area {
313 FocusArea::Navigation => {
314 self.nav_focus = match self.nav_focus {
315 NavigationFocusItem::SignIn => NavigationFocusItem::Next,
316 NavigationFocusItem::Basics => NavigationFocusItem::SignIn,
317 NavigationFocusItem::Editing => NavigationFocusItem::Basics,
318 NavigationFocusItem::AiSetup => NavigationFocusItem::Editing,
319 NavigationFocusItem::Welcome => NavigationFocusItem::AiSetup,
320 NavigationFocusItem::Next => NavigationFocusItem::Welcome,
321 };
322 }
323 FocusArea::PageContent => {
324 let page_index = match self.current_page {
325 OnboardingPage::Basics => 0,
326 OnboardingPage::Editing => 1,
327 OnboardingPage::AiSetup => 2,
328 OnboardingPage::Welcome => 3,
329 };
330 // Bounds checking for page items
331 let max_items = match self.current_page {
332 OnboardingPage::Basics => 3, // 3 buttons
333 OnboardingPage::Editing => 3, // 3 buttons
334 OnboardingPage::AiSetup => 2, // Will have 2 items
335 OnboardingPage::Welcome => 1, // Will have 1 item
336 };
337
338 if self.page_focus[page_index].0 > 0 {
339 self.page_focus[page_index].0 -= 1;
340 } else {
341 // Wrap to end
342 self.page_focus[page_index].0 = max_items - 1;
343 }
344 }
345 }
346 cx.notify();
347 }
348
349 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
350 match self.focus_area {
351 FocusArea::Navigation => {
352 match self.nav_focus {
353 NavigationFocusItem::SignIn => {
354 // Handle sign in action
355 // TODO: Implement sign in action
356 }
357 NavigationFocusItem::Basics => {
358 self.jump_to_page(OnboardingPage::Basics, window, cx)
359 }
360 NavigationFocusItem::Editing => {
361 self.jump_to_page(OnboardingPage::Editing, window, cx)
362 }
363 NavigationFocusItem::AiSetup => {
364 self.jump_to_page(OnboardingPage::AiSetup, window, cx)
365 }
366 NavigationFocusItem::Welcome => {
367 self.jump_to_page(OnboardingPage::Welcome, window, cx)
368 }
369 NavigationFocusItem::Next => {
370 // Handle next button action
371 self.next_page(window, cx);
372 }
373 }
374 // After confirming navigation item (except Next), switch focus to page content
375 if self.nav_focus != NavigationFocusItem::Next {
376 self.focus_area = FocusArea::PageContent;
377 }
378 }
379 FocusArea::PageContent => {
380 // Handle page-specific item selection
381 let page_index = match self.current_page {
382 OnboardingPage::Basics => 0,
383 OnboardingPage::Editing => 1,
384 OnboardingPage::AiSetup => 2,
385 OnboardingPage::Welcome => 3,
386 };
387 let item_index = self.page_focus[page_index].0;
388
389 // Trigger the action for the focused item
390 match self.current_page {
391 OnboardingPage::Basics => {
392 match item_index {
393 0 => {
394 // Open file action
395 cx.notify();
396 }
397 1 => {
398 // Create project action
399 cx.notify();
400 }
401 2 => {
402 // Explore UI action
403 cx.notify();
404 }
405 _ => {}
406 }
407 }
408 OnboardingPage::Editing => {
409 // Similar handling for editing page
410 cx.notify();
411 }
412 _ => {
413 cx.notify();
414 }
415 }
416 }
417 }
418 cx.notify();
419 }
420
421 fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
422 match self.focus_area {
423 FocusArea::PageContent => {
424 // Switch focus back to navigation
425 self.focus_area = FocusArea::Navigation;
426 }
427 FocusArea::Navigation => {
428 // If already in navigation, maybe close the onboarding?
429 // For now, just stay in navigation
430 }
431 }
432 cx.notify();
433 }
434
435 fn toggle_focus(&mut self, _: &ToggleFocus, _window: &mut Window, cx: &mut Context<Self>) {
436 self.focus_area = match self.focus_area {
437 FocusArea::Navigation => FocusArea::PageContent,
438 FocusArea::PageContent => FocusArea::Navigation,
439 };
440 cx.notify();
441 }
442
443 fn mark_page_completed(
444 &mut self,
445 page: OnboardingPage,
446 _window: &mut gpui::Window,
447 cx: &mut Context<Self>,
448 ) {
449 let index = match page {
450 OnboardingPage::Basics => 0,
451 OnboardingPage::Editing => 1,
452 OnboardingPage::AiSetup => 2,
453 OnboardingPage::Welcome => 3,
454 };
455 self.completed_pages[index] = true;
456 cx.notify();
457 }
458
459 fn handle_jump_to_basics(
460 &mut self,
461 _: &JumpToBasics,
462 window: &mut Window,
463 cx: &mut Context<Self>,
464 ) {
465 self.jump_to_page(OnboardingPage::Basics, window, cx);
466 }
467
468 fn handle_jump_to_editing(
469 &mut self,
470 _: &JumpToEditing,
471 window: &mut Window,
472 cx: &mut Context<Self>,
473 ) {
474 self.jump_to_page(OnboardingPage::Editing, window, cx);
475 }
476
477 fn handle_jump_to_ai_setup(
478 &mut self,
479 _: &JumpToAiSetup,
480 window: &mut Window,
481 cx: &mut Context<Self>,
482 ) {
483 self.jump_to_page(OnboardingPage::AiSetup, window, cx);
484 }
485
486 fn handle_jump_to_welcome(
487 &mut self,
488 _: &JumpToWelcome,
489 window: &mut Window,
490 cx: &mut Context<Self>,
491 ) {
492 self.jump_to_page(OnboardingPage::Welcome, window, cx);
493 }
494
495 fn handle_next_page(&mut self, _: &NextPage, window: &mut Window, cx: &mut Context<Self>) {
496 self.next_page(window, cx);
497 }
498
499 fn handle_previous_page(
500 &mut self,
501 _: &PreviousPage,
502 window: &mut Window,
503 cx: &mut Context<Self>,
504 ) {
505 self.previous_page(window, cx);
506 }
507
508 fn render_navigation(
509 &mut self,
510 window: &mut Window,
511 cx: &mut Context<Self>,
512 ) -> impl gpui::IntoElement {
513 let client = self.client.clone();
514
515 v_flex()
516 .h_full()
517 .w(px(256.))
518 .gap_2()
519 .justify_between()
520 .child(
521 v_flex()
522 .w_full()
523 .gap_px()
524 .child(
525 h_flex()
526 .w_full()
527 .justify_between()
528 .py(px(24.))
529 .pl(px(24.))
530 .pr(px(12.))
531 .child(
532 Vector::new(VectorName::ZedLogo, rems(2.), rems(2.))
533 .color(Color::Custom(cx.theme().colors().icon.opacity(0.5))),
534 )
535 .child(
536 Button::new("sign_in", "Sign in")
537 .color(Color::Muted)
538 .label_size(LabelSize::Small)
539 .when(
540 self.focus_area == FocusArea::Navigation
541 && self.nav_focus == NavigationFocusItem::SignIn,
542 |this| this.color(Color::Accent),
543 )
544 .size(ButtonSize::Compact)
545 .on_click(cx.listener(move |_, _, window, cx| {
546 let client = client.clone();
547 window
548 .spawn(cx, async move |cx| {
549 client
550 .authenticate_and_connect(true, &cx)
551 .await
552 .into_response()
553 .notify_async_err(cx);
554 })
555 .detach();
556 })),
557 ),
558 )
559 .child(
560 v_flex()
561 .gap_px()
562 .py(px(16.))
563 .gap(px(12.))
564 .child(self.render_nav_item(
565 OnboardingPage::Basics,
566 "The Basics",
567 "1",
568 cx,
569 ))
570 .child(self.render_nav_item(
571 OnboardingPage::Editing,
572 "Editing Experience",
573 "2",
574 cx,
575 ))
576 .child(self.render_nav_item(
577 OnboardingPage::AiSetup,
578 "AI Setup",
579 "3",
580 cx,
581 ))
582 .child(self.render_nav_item(
583 OnboardingPage::Welcome,
584 "Welcome",
585 "4",
586 cx,
587 )),
588 ),
589 )
590 .child(self.render_bottom_controls(window, cx))
591 }
592
593 fn render_nav_item(
594 &mut self,
595 page: OnboardingPage,
596 label: impl Into<SharedString>,
597 shortcut: impl Into<SharedString>,
598 cx: &mut Context<Self>,
599 ) -> impl gpui::IntoElement {
600 let selected = self.current_page == page;
601 let label = label.into();
602 let shortcut = shortcut.into();
603 let id = ElementId::Name(label.clone());
604
605 let is_focused = match page {
606 OnboardingPage::Basics => self.nav_focus == NavigationFocusItem::Basics,
607 OnboardingPage::Editing => self.nav_focus == NavigationFocusItem::Editing,
608 OnboardingPage::AiSetup => self.nav_focus == NavigationFocusItem::AiSetup,
609 OnboardingPage::Welcome => self.nav_focus == NavigationFocusItem::Welcome,
610 };
611
612 let area_focused = self.focus_area == FocusArea::Navigation;
613
614 h_flex()
615 .id(id)
616 .h(rems(1.5))
617 .w_full()
618 .when(is_focused, |this| {
619 this.bg(if area_focused {
620 cx.theme().colors().border_focused.opacity(0.16)
621 } else {
622 cx.theme().colors().border.opacity(0.24)
623 })
624 })
625 .child(
626 div()
627 .w(px(3.))
628 .h_full()
629 .when(selected, |this| this.bg(cx.theme().colors().border_focused)),
630 )
631 .child(
632 h_flex()
633 .pl(px(23.))
634 .flex_1()
635 .justify_between()
636 .items_center()
637 .child(Label::new(label).when(is_focused, |this| this.color(Color::Default)))
638 .child(Label::new(format!("⌘{}", shortcut.clone())).color(Color::Muted)),
639 )
640 .on_click(cx.listener(move |this, _, window, cx| {
641 this.jump_to_page(page, window, cx);
642 }))
643 }
644
645 fn render_bottom_controls(
646 &mut self,
647 window: &mut gpui::Window,
648 cx: &mut Context<Self>,
649 ) -> impl gpui::IntoElement {
650 h_flex().w_full().p(px(12.)).pl(px(24.)).child(
651 Button::new(
652 "next",
653 if self.current_page == OnboardingPage::Welcome {
654 "Get Started"
655 } else {
656 "Next"
657 },
658 )
659 .style(ButtonStyle::Filled)
660 .when(
661 self.focus_area == FocusArea::Navigation
662 && self.nav_focus == NavigationFocusItem::Next,
663 |this| this.color(Color::Accent),
664 )
665 .key_binding(ui::KeyBinding::for_action_in(
666 &NextPage,
667 &self.focus_handle,
668 window,
669 cx,
670 ))
671 .on_click(cx.listener(|this, _, window, cx| {
672 this.next_page(window, cx);
673 })),
674 )
675 }
676
677 fn render_active_page(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
678 match self.current_page {
679 OnboardingPage::Basics => self.render_basics_page(cx),
680 OnboardingPage::Editing => self.render_editing_page(cx),
681 OnboardingPage::AiSetup => self.render_ai_setup_page(cx),
682 OnboardingPage::Welcome => self.render_welcome_page(cx),
683 }
684 }
685
686 fn render_basics_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
687 let page_index = 0; // Basics page index
688 let focused_item = self.page_focus[page_index].0;
689 let is_page_focused = self.focus_area == FocusArea::PageContent;
690
691 v_flex()
692 .h_full()
693 .w_full()
694 .items_center()
695 .justify_center()
696 .gap_4()
697 .child(
698 Label::new("Welcome to Zed!")
699 .size(LabelSize::Large)
700 .color(Color::Default),
701 )
702 .child(
703 Label::new("Let's get you started with the basics")
704 .size(LabelSize::Default)
705 .color(Color::Muted),
706 )
707 .child(
708 v_flex()
709 .gap_2()
710 .mt_4()
711 .child(
712 Button::new("open_file", "Open a File")
713 .style(ButtonStyle::Filled)
714 .when(is_page_focused && focused_item == 0, |this| {
715 this.color(Color::Accent)
716 })
717 .on_click(cx.listener(|_, _, _, cx| {
718 // TODO: Trigger open file action
719 cx.notify();
720 })),
721 )
722 .child(
723 Button::new("create_project", "Create a Project")
724 .style(ButtonStyle::Filled)
725 .when(is_page_focused && focused_item == 1, |this| {
726 this.color(Color::Accent)
727 })
728 .on_click(cx.listener(|_, _, _, cx| {
729 // TODO: Trigger create project action
730 cx.notify();
731 })),
732 )
733 .child(
734 Button::new("explore_ui", "Explore the UI")
735 .style(ButtonStyle::Filled)
736 .when(is_page_focused && focused_item == 2, |this| {
737 this.color(Color::Accent)
738 })
739 .on_click(cx.listener(|_, _, _, cx| {
740 // TODO: Trigger explore UI action
741 cx.notify();
742 })),
743 ),
744 )
745 .into_any_element()
746 }
747
748 fn render_editing_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
749 let page_index = 1; // Editing page index
750 let focused_item = self.page_focus[page_index].0;
751 let is_page_focused = self.focus_area == FocusArea::PageContent;
752
753 v_flex()
754 .h_full()
755 .w_full()
756 .items_center()
757 .justify_center()
758 .gap_4()
759 .child(
760 Label::new("Editing Features")
761 .size(LabelSize::Large)
762 .color(Color::Default),
763 )
764 .child(
765 v_flex()
766 .gap_2()
767 .mt_4()
768 .child(
769 Button::new("try_multi_cursor", "Try Multi-cursor Editing")
770 .style(ButtonStyle::Filled)
771 .when(is_page_focused && focused_item == 0, |this| {
772 this.color(Color::Accent)
773 })
774 .on_click(cx.listener(|_, _, _, cx| {
775 cx.notify();
776 })),
777 )
778 .child(
779 Button::new("learn_shortcuts", "Learn Keyboard Shortcuts")
780 .style(ButtonStyle::Filled)
781 .when(is_page_focused && focused_item == 1, |this| {
782 this.color(Color::Accent)
783 })
784 .on_click(cx.listener(|_, _, _, cx| {
785 cx.notify();
786 })),
787 )
788 .child(
789 Button::new("explore_actions", "Explore Command Palette")
790 .style(ButtonStyle::Filled)
791 .when(is_page_focused && focused_item == 2, |this| {
792 this.color(Color::Accent)
793 })
794 .on_click(cx.listener(|_, _, _, cx| {
795 cx.notify();
796 })),
797 ),
798 )
799 .into_any_element()
800 }
801
802 fn render_ai_setup_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
803 let page_index = 2; // AI Setup page index
804 let focused_item = self.page_focus[page_index].0;
805 let is_page_focused = self.focus_area == FocusArea::PageContent;
806
807 v_flex()
808 .h_full()
809 .w_full()
810 .items_center()
811 .justify_center()
812 .gap_4()
813 .child(
814 Label::new("AI Assistant Setup")
815 .size(LabelSize::Large)
816 .color(Color::Default),
817 )
818 .child(
819 v_flex()
820 .gap_2()
821 .mt_4()
822 .child(
823 Button::new("configure_ai", "Configure AI Provider")
824 .style(ButtonStyle::Filled)
825 .when(is_page_focused && focused_item == 0, |this| {
826 this.color(Color::Accent)
827 })
828 .on_click(cx.listener(|_, _, _, cx| {
829 cx.notify();
830 })),
831 )
832 .child(
833 Button::new("try_ai_chat", "Try AI Chat")
834 .style(ButtonStyle::Filled)
835 .when(is_page_focused && focused_item == 1, |this| {
836 this.color(Color::Accent)
837 })
838 .on_click(cx.listener(|_, _, _, cx| {
839 cx.notify();
840 })),
841 ),
842 )
843 .into_any_element()
844 }
845
846 fn render_welcome_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
847 let page_index = 3; // Welcome page index
848 let focused_item = self.page_focus[page_index].0;
849 let is_page_focused = self.focus_area == FocusArea::PageContent;
850
851 v_flex()
852 .h_full()
853 .w_full()
854 .items_center()
855 .justify_center()
856 .gap_4()
857 .child(
858 Label::new("Welcome to Zed!")
859 .size(LabelSize::Large)
860 .color(Color::Default),
861 )
862 .child(
863 Label::new("You're all set up and ready to code")
864 .size(LabelSize::Default)
865 .color(Color::Muted),
866 )
867 .child(
868 Button::new("finish_onboarding", "Start Coding!")
869 .style(ButtonStyle::Filled)
870 .size(ButtonSize::Large)
871 .when(is_page_focused && focused_item == 0, |this| {
872 this.color(Color::Accent)
873 })
874 .on_click(cx.listener(|_, _, _, cx| {
875 // TODO: Close onboarding and start coding
876 cx.notify();
877 })),
878 )
879 .into_any_element()
880 }
881
882 fn render_keyboard_help(&self, cx: &mut Context<Self>) -> AnyElement {
883 let help_text = match self.focus_area {
884 FocusArea::Navigation => {
885 "Use ↑/↓ to navigate • Enter to select page • Tab to switch to page content"
886 }
887 FocusArea::PageContent => {
888 "Use ↑/↓ to navigate • Enter to activate • Esc to return to navigation"
889 }
890 };
891
892 h_flex()
893 .w_full()
894 .justify_center()
895 .p_2()
896 .child(
897 Label::new(help_text)
898 .size(LabelSize::Small)
899 .color(Color::Muted),
900 )
901 .into_any_element()
902 }
903}
904
905impl Item for OnboardingUI {
906 type Event = ItemEvent;
907
908 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
909 "Onboarding".into()
910 }
911
912 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
913 f(event.clone())
914 }
915
916 fn added_to_workspace(
917 &mut self,
918 workspace: &mut Workspace,
919 _window: &mut Window,
920 _cx: &mut Context<Self>,
921 ) {
922 self.workspace_id = workspace.database_id();
923 }
924
925 fn show_toolbar(&self) -> bool {
926 false
927 }
928
929 fn clone_on_split(
930 &self,
931 _workspace_id: Option<WorkspaceId>,
932 window: &mut Window,
933 cx: &mut Context<Self>,
934 ) -> Option<Entity<Self>> {
935 let weak_workspace = self.workspace.clone();
936 let client = self.client.clone();
937 if let Some(workspace) = weak_workspace.upgrade() {
938 workspace.update(cx, |workspace, cx| {
939 Some(cx.new(|cx| OnboardingUI::new(workspace, client, cx)))
940 })
941 } else {
942 None
943 }
944 }
945}
946
947impl SerializableItem for OnboardingUI {
948 fn serialized_item_kind() -> &'static str {
949 "OnboardingUI"
950 }
951
952 fn deserialize(
953 _project: Entity<Project>,
954 workspace: WeakEntity<Workspace>,
955 workspace_id: WorkspaceId,
956 item_id: u64,
957 window: &mut Window,
958 cx: &mut App,
959 ) -> Task<anyhow::Result<Entity<Self>>> {
960 window.spawn(cx, async move |cx| {
961 let (current_page, completed_pages) = if let Some((page_str, completed_str)) =
962 ONBOARDING_DB.get_state(item_id, workspace_id)?
963 {
964 let page = match page_str.as_str() {
965 "basics" => OnboardingPage::Basics,
966 "editing" => OnboardingPage::Editing,
967 "ai_setup" => OnboardingPage::AiSetup,
968 "welcome" => OnboardingPage::Welcome,
969 _ => OnboardingPage::Basics,
970 };
971 let completed = OnboardingUI::completed_pages_from_string(&completed_str);
972 (page, completed)
973 } else {
974 (OnboardingPage::Basics, [false; 4])
975 };
976
977 cx.update(|window, cx| {
978 let workspace = workspace
979 .upgrade()
980 .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?;
981
982 workspace.update(cx, |workspace, cx| {
983 let client = workspace.client().clone();
984 Ok(cx.new(|cx| {
985 let mut onboarding = OnboardingUI::new(workspace, client, cx);
986 onboarding.current_page = current_page;
987 onboarding.completed_pages = completed_pages;
988 onboarding
989 }))
990 })
991 })?
992 })
993 }
994
995 fn serialize(
996 &mut self,
997 _workspace: &mut Workspace,
998 item_id: u64,
999 _closing: bool,
1000 _window: &mut Window,
1001 cx: &mut Context<Self>,
1002 ) -> Option<Task<anyhow::Result<()>>> {
1003 let workspace_id = self.workspace_id?;
1004 let current_page = match self.current_page {
1005 OnboardingPage::Basics => "basics",
1006 OnboardingPage::Editing => "editing",
1007 OnboardingPage::AiSetup => "ai_setup",
1008 OnboardingPage::Welcome => "welcome",
1009 }
1010 .to_string();
1011 let completed_pages = self.completed_pages_to_string();
1012
1013 Some(cx.background_spawn(async move {
1014 ONBOARDING_DB
1015 .save_state(item_id, workspace_id, current_page, completed_pages)
1016 .await
1017 }))
1018 }
1019
1020 fn cleanup(
1021 _workspace_id: WorkspaceId,
1022 _item_ids: Vec<u64>,
1023 _window: &mut Window,
1024 _cx: &mut App,
1025 ) -> Task<anyhow::Result<()>> {
1026 Task::ready(Ok(()))
1027 }
1028
1029 fn should_serialize(&self, _event: &ItemEvent) -> bool {
1030 true
1031 }
1032}