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