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