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