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