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