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(
805 h_flex()
806 .h(px(32.))
807 .w_full()
808 .justify_between()
809 .child(Label::new("Pick a Keymap")),
810 )
811 .child(
812 h_flex().gap_2().children(
813 vec![
814 ("Zed", VectorName::ZedLogo, 4),
815 ("Atom", VectorName::ZedLogo, 5),
816 ("JetBrains", VectorName::ZedLogo, 6),
817 ("Sublime", VectorName::ZedLogo, 7),
818 ("VSCode", VectorName::ZedLogo, 8),
819 ("Emacs", VectorName::ZedLogo, 9),
820 ("TextMate", VectorName::ZedLogo, 10),
821 ]
822 .into_iter()
823 .map(|(label, icon, index)| {
824 let is_focused = is_page_focused && focused_item == index;
825 let current_keymap = BaseKeymap::get_global(cx).to_string();
826 let is_selected = current_keymap == label;
827
828 v_flex()
829 .w(px(60.))
830 .gap_1()
831 .items_center()
832 .justify_center()
833 .text_center()
834 .child(
835 h_flex()
836 .id(("keymap", index))
837 .size(px(40.))
838 .rounded(px(8.))
839 .items_center()
840 .justify_center()
841 .border_1()
842 .border_color(if is_selected {
843 cx.theme().colors().border_selected
844 } else {
845 transparent_black()
846 })
847 .when(is_focused, |this| {
848 this.border_color(
849 cx.theme().colors().border_focused,
850 )
851 })
852 .when(is_selected, |this| {
853 this.bg(cx.theme().status().info.opacity(0.08))
854 })
855 .child(
856 h_flex()
857 .size(px(34.))
858 .rounded(px(6.))
859 .border_2()
860 .border_color(cx.theme().colors().border)
861 .items_center()
862 .justify_center()
863 .shadow_hairline()
864 .child(
865 Vector::new(icon, rems(1.25), rems(1.25))
866 .color(if is_selected {
867 Color::Info
868 } else {
869 Color::Default
870 }),
871 ),
872 )
873 .on_click(cx.listener(move |this, _, window, cx| {
874 SettingsStore::update_global(
875 cx,
876 move |store, cx| {
877 let base_keymap = match label {
878 "Zed" => "None",
879 "Atom" => "Atom",
880 "JetBrains" => "JetBrains",
881 "Sublime" => "SublimeText",
882 "VSCode" => "VSCode",
883 "Emacs" => "Emacs",
884 "TextMate" => "TextMate",
885 _ => "VSCode",
886 };
887 let mut settings =
888 store.raw_user_settings().clone();
889 settings["base_keymap"] =
890 serde_json::json!(base_keymap);
891 store
892 .set_user_settings(
893 &settings.to_string(),
894 cx,
895 )
896 .ok();
897 },
898 );
899 cx.notify();
900 })),
901 )
902 .child(
903 div()
904 .text_color(cx.theme().colors().text)
905 .text_size(px(12.))
906 .child(label),
907 )
908 }),
909 ),
910 ),
911 )
912 // Settings checkboxes
913 .child(
914 v_flex()
915 .gap_3()
916 .mt_6()
917 .child({
918 let vim_enabled = VimModeSetting::get_global(cx).0;
919 h_flex()
920 .id("vim_mode_container")
921 .gap_2()
922 .items_center()
923 .p_1()
924 .rounded_md()
925 .when(is_page_focused && focused_item == 11, |this| {
926 this.border_2()
927 .border_color(cx.theme().colors().border_focused)
928 })
929 .child(
930 div()
931 .id("vim_mode_checkbox")
932 .w_4()
933 .h_4()
934 .rounded_sm()
935 .border_1()
936 .border_color(cx.theme().colors().border)
937 .when(vim_enabled, |this| {
938 this.bg(cx.theme().colors().element_selected)
939 .border_color(cx.theme().colors().border_selected)
940 })
941 .hover(|this| this.bg(cx.theme().colors().element_hover))
942 .child(div().when(vim_enabled, |this| {
943 this.size_full()
944 .flex()
945 .items_center()
946 .justify_center()
947 .child(Icon::new(IconName::Check))
948 })),
949 )
950 .child(Label::new("Enable Vim Mode"))
951 .cursor_pointer()
952 .on_click(cx.listener(move |this, _, _window, cx| {
953 let current = VimModeSetting::get_global(cx).0;
954 SettingsStore::update_global(cx, move |store, cx| {
955 let mut settings = store.raw_user_settings().clone();
956 settings["vim_mode"] = serde_json::json!(!current);
957 store.set_user_settings(&settings.to_string(), cx).ok();
958 });
959 }))
960 })
961 .child({
962 let crash_reports_enabled = TelemetrySettings::get_global(cx).diagnostics;
963 h_flex()
964 .id("crash_reports_container")
965 .gap_2()
966 .items_center()
967 .p_1()
968 .rounded_md()
969 .when(is_page_focused && focused_item == 12, |this| {
970 this.border_2()
971 .border_color(cx.theme().colors().border_focused)
972 })
973 .child(
974 div()
975 .id("crash_reports_checkbox")
976 .w_4()
977 .h_4()
978 .rounded_sm()
979 .border_1()
980 .border_color(cx.theme().colors().border)
981 .when(crash_reports_enabled, |this| {
982 this.bg(cx.theme().colors().element_selected)
983 .border_color(cx.theme().colors().border_selected)
984 })
985 .hover(|this| this.bg(cx.theme().colors().element_hover))
986 .child(div().when(crash_reports_enabled, |this| {
987 this.size_full()
988 .flex()
989 .items_center()
990 .justify_center()
991 .child(Icon::new(IconName::Check))
992 })),
993 )
994 .child(Label::new("Send Crash Reports"))
995 .cursor_pointer()
996 .on_click(cx.listener(move |this, _, _window, cx| {
997 let current = TelemetrySettings::get_global(cx).diagnostics;
998 SettingsStore::update_global(cx, move |store, cx| {
999 let mut settings = store.raw_user_settings().clone();
1000 if settings.get("telemetry").is_none() {
1001 settings["telemetry"] = serde_json::json!({});
1002 }
1003 settings["telemetry"]["diagnostics"] =
1004 serde_json::json!(!current);
1005 store.set_user_settings(&settings.to_string(), cx).ok();
1006 });
1007 }))
1008 })
1009 .child({
1010 let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
1011 h_flex()
1012 .id("telemetry_container")
1013 .gap_2()
1014 .items_center()
1015 .p_1()
1016 .rounded_md()
1017 .when(is_page_focused && focused_item == 13, |this| {
1018 this.border_2()
1019 .border_color(cx.theme().colors().border_focused)
1020 })
1021 .child(
1022 div()
1023 .id("telemetry_checkbox")
1024 .w_4()
1025 .h_4()
1026 .rounded_sm()
1027 .border_1()
1028 .border_color(cx.theme().colors().border)
1029 .when(telemetry_enabled, |this| {
1030 this.bg(cx.theme().colors().element_selected)
1031 .border_color(cx.theme().colors().border_selected)
1032 })
1033 .hover(|this| this.bg(cx.theme().colors().element_hover))
1034 .child(div().when(telemetry_enabled, |this| {
1035 this.size_full()
1036 .flex()
1037 .items_center()
1038 .justify_center()
1039 .child(Icon::new(IconName::Check))
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
1195impl Item for OnboardingUI {
1196 type Event = ItemEvent;
1197
1198 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1199 "Onboarding".into()
1200 }
1201
1202 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1203 f(event.clone())
1204 }
1205
1206 fn added_to_workspace(
1207 &mut self,
1208 workspace: &mut Workspace,
1209 _window: &mut Window,
1210 _cx: &mut Context<Self>,
1211 ) {
1212 self.workspace_id = workspace.database_id();
1213 }
1214
1215 fn show_toolbar(&self) -> bool {
1216 false
1217 }
1218
1219 fn clone_on_split(
1220 &self,
1221 _workspace_id: Option<WorkspaceId>,
1222 window: &mut Window,
1223 cx: &mut Context<Self>,
1224 ) -> Option<Entity<Self>> {
1225 let weak_workspace = self.workspace.clone();
1226 let client = self.client.clone();
1227 if let Some(workspace) = weak_workspace.upgrade() {
1228 workspace.update(cx, |workspace, cx| {
1229 Some(cx.new(|cx| OnboardingUI::new(workspace, client, cx)))
1230 })
1231 } else {
1232 None
1233 }
1234 }
1235}
1236
1237impl SerializableItem for OnboardingUI {
1238 fn serialized_item_kind() -> &'static str {
1239 "OnboardingUI"
1240 }
1241
1242 fn deserialize(
1243 _project: Entity<Project>,
1244 workspace: WeakEntity<Workspace>,
1245 workspace_id: WorkspaceId,
1246 item_id: u64,
1247 window: &mut Window,
1248 cx: &mut App,
1249 ) -> Task<anyhow::Result<Entity<Self>>> {
1250 window.spawn(cx, async move |cx| {
1251 let (current_page, completed_pages) = if let Some((page_str, completed_str)) =
1252 ONBOARDING_DB.get_state(item_id, workspace_id)?
1253 {
1254 let page = match page_str.as_str() {
1255 "basics" => OnboardingPage::Basics,
1256 "editing" => OnboardingPage::Editing,
1257 "ai_setup" => OnboardingPage::AiSetup,
1258 "welcome" => OnboardingPage::Welcome,
1259 _ => OnboardingPage::Basics,
1260 };
1261 let completed = OnboardingUI::completed_pages_from_string(&completed_str);
1262 (page, completed)
1263 } else {
1264 (OnboardingPage::Basics, [false; 4])
1265 };
1266
1267 cx.update(|window, cx| {
1268 let workspace = workspace
1269 .upgrade()
1270 .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?;
1271
1272 workspace.update(cx, |workspace, cx| {
1273 let client = workspace.client().clone();
1274 Ok(cx.new(|cx| {
1275 let mut onboarding = OnboardingUI::new(workspace, client, cx);
1276 onboarding.current_page = current_page;
1277 onboarding.completed_pages = completed_pages;
1278 onboarding
1279 }))
1280 })
1281 })?
1282 })
1283 }
1284
1285 fn serialize(
1286 &mut self,
1287 _workspace: &mut Workspace,
1288 item_id: u64,
1289 _closing: bool,
1290 _window: &mut Window,
1291 cx: &mut Context<Self>,
1292 ) -> Option<Task<anyhow::Result<()>>> {
1293 let workspace_id = self.workspace_id?;
1294 let current_page = match self.current_page {
1295 OnboardingPage::Basics => "basics",
1296 OnboardingPage::Editing => "editing",
1297 OnboardingPage::AiSetup => "ai_setup",
1298 OnboardingPage::Welcome => "welcome",
1299 }
1300 .to_string();
1301 let completed_pages = self.completed_pages_to_string();
1302
1303 Some(cx.background_spawn(async move {
1304 ONBOARDING_DB
1305 .save_state(item_id, workspace_id, current_page, completed_pages)
1306 .await
1307 }))
1308 }
1309
1310 fn cleanup(
1311 _workspace_id: WorkspaceId,
1312 _item_ids: Vec<u64>,
1313 _window: &mut Window,
1314 _cx: &mut App,
1315 ) -> Task<anyhow::Result<()>> {
1316 Task::ready(Ok(()))
1317 }
1318
1319 fn should_serialize(&self, _event: &ItemEvent) -> bool {
1320 true
1321 }
1322}