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