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