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