onboarding_ui.rs

   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, KeyBinding, Task, UpdateGlobal,
  12    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::{ListItem, 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(12.))
 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 selected = self.current_page == page;
 608        let label = label.into();
 609        let shortcut = shortcut.into();
 610        let id = ElementId::Name(label.clone());
 611
 612        let is_focused = match page {
 613            OnboardingPage::Basics => self.nav_focus == NavigationFocusItem::Basics,
 614            OnboardingPage::Editing => self.nav_focus == NavigationFocusItem::Editing,
 615            OnboardingPage::AiSetup => self.nav_focus == NavigationFocusItem::AiSetup,
 616            OnboardingPage::Welcome => self.nav_focus == NavigationFocusItem::Welcome,
 617        };
 618
 619        let area_focused = self.focus_area == FocusArea::Navigation;
 620
 621        h_flex()
 622            .id(id)
 623            .h(rems(1.5))
 624            .w_full()
 625            .when(is_focused, |this| {
 626                this.bg(if area_focused {
 627                    cx.theme().colors().border_focused.opacity(0.16)
 628                } else {
 629                    cx.theme().colors().border.opacity(0.24)
 630                })
 631            })
 632            .child(
 633                div()
 634                    .w(px(3.))
 635                    .h_full()
 636                    .when(selected, |this| this.bg(cx.theme().colors().border_focused)),
 637            )
 638            .child(
 639                h_flex()
 640                    .pl(px(23.))
 641                    .flex_1()
 642                    .justify_between()
 643                    .items_center()
 644                    .child(Label::new(label).when(is_focused, |this| this.color(Color::Default)))
 645                    .child(Label::new(format!("{}", shortcut.clone())).color(Color::Muted)),
 646            )
 647            .on_click(cx.listener(move |this, _, window, cx| {
 648                this.jump_to_page(page, window, cx);
 649            }))
 650    }
 651
 652    fn render_bottom_controls(
 653        &mut self,
 654        window: &mut gpui::Window,
 655        cx: &mut Context<Self>,
 656    ) -> impl gpui::IntoElement {
 657        h_flex().w_full().p(px(12.)).pl(px(24.)).child(
 658            JuicyButton::new(if self.current_page == OnboardingPage::Welcome {
 659                "Get Started"
 660            } else {
 661                "Next"
 662            })
 663            .keybinding(ui::KeyBinding::for_action_in(
 664                &NextPage,
 665                &self.focus_handle,
 666                window,
 667                cx,
 668            ))
 669            .on_click(cx.listener(|this, _, window, cx| {
 670                this.next_page(window, cx);
 671            })),
 672        )
 673    }
 674
 675    fn render_active_page(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 676        match self.current_page {
 677            OnboardingPage::Basics => self.render_basics_page(cx),
 678            OnboardingPage::Editing => self.render_editing_page(cx),
 679            OnboardingPage::AiSetup => self.render_ai_setup_page(cx),
 680            OnboardingPage::Welcome => self.render_welcome_page(cx),
 681        }
 682    }
 683
 684    fn render_basics_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
 685        let page_index = 0; // Basics page index
 686        let focused_item = self.page_focus[page_index].0;
 687        let is_page_focused = self.focus_area == FocusArea::PageContent;
 688
 689        use theme_preview::ThemePreviewTile;
 690
 691        // Get available themes
 692        let theme_registry = ThemeRegistry::default_global(cx);
 693        let theme_names = theme_registry.list_names();
 694        let current_theme = cx.theme().clone();
 695
 696        v_flex()
 697            .id("theme-selector")
 698            .h_full()
 699            .w_full()
 700            .gap_6()
 701            .overflow_y_scroll()
 702            // Theme selector section
 703            .child(
 704                v_flex()
 705                    .w_full()
 706                    .overflow_hidden()
 707                    .child(
 708                        h_flex()
 709                            .h(px(32.))
 710                            .w_full()
 711                            .justify_between()
 712                            .child(Label::new("Pick a Theme"))
 713                            .child(
 714                                Button::new("more_themes", "More Themes")
 715                                    .style(ButtonStyle::Subtle)
 716                                    .color(Color::Muted)
 717                                    .on_click(cx.listener(|_, _, window, cx| {
 718                                        window.dispatch_action(
 719                                            zed_actions::theme_selector::Toggle::default()
 720                                                .boxed_clone(),
 721                                            cx,
 722                                        );
 723                                    })),
 724                            ),
 725                    )
 726                    .child(
 727                        h_flex().w_full().overflow_hidden().gap_3().children(
 728                            vec![
 729                                ("One Dark", "One Dark"),
 730                                ("Gruvbox Dark", "Gruvbox Dark"),
 731                                ("One Light", "One Light"),
 732                                ("Gruvbox Light", "Gruvbox Light"),
 733                            ]
 734                            .into_iter()
 735                            .enumerate()
 736                            .map(|(i, (label, theme_name))| {
 737                                let is_selected = current_theme.name == *theme_name;
 738                                let is_focused = is_page_focused && focused_item == i;
 739
 740                                v_flex()
 741                                    .flex_1()
 742                                    .gap_1p5()
 743                                    .justify_center()
 744                                    .text_center()
 745                                    .child(
 746                                        div()
 747                                            .id(("theme", i))
 748                                            .rounded(px(8.))
 749                                            .h(px(90.))
 750                                            .w_full()
 751                                            .overflow_hidden()
 752                                            .border_1()
 753                                            .border_color(if is_focused {
 754                                                cx.theme().colors().border_focused
 755                                            } else {
 756                                                transparent_black()
 757                                            })
 758                                            .child(
 759                                                if let Ok(theme) = theme_registry.get(theme_name) {
 760                                                    ThemePreviewTile::new(theme, is_selected, 0.5)
 761                                                        .into_any_element()
 762                                                } else {
 763                                                    div()
 764                                                        .size_full()
 765                                                        .bg(cx.theme().colors().surface_background)
 766                                                        .rounded_md()
 767                                                        .into_any_element()
 768                                                },
 769                                            )
 770                                            .on_click(cx.listener(move |this, _, window, cx| {
 771                                                SettingsStore::update_global(
 772                                                    cx,
 773                                                    move |store, cx| {
 774                                                        let mut settings =
 775                                                            store.raw_user_settings().clone();
 776                                                        settings["theme"] =
 777                                                            serde_json::json!(theme_name);
 778                                                        store
 779                                                            .set_user_settings(
 780                                                                &settings.to_string(),
 781                                                                cx,
 782                                                            )
 783                                                            .ok();
 784                                                    },
 785                                                );
 786                                                cx.notify();
 787                                            })),
 788                                    )
 789                                    .child(
 790                                        div()
 791                                            .text_color(cx.theme().colors().text)
 792                                            .text_size(px(12.))
 793                                            .child(label),
 794                                    )
 795                            }),
 796                        ),
 797                    ),
 798            )
 799            // Keymap selector section
 800            .child(
 801                v_flex()
 802                    .gap_3()
 803                    .mt_4()
 804                    .child(
 805                        h_flex()
 806                            .h(px(32.))
 807                            .w_full()
 808                            .justify_between()
 809                            .child(Label::new("Pick a Keymap")),
 810                    )
 811                    .child(
 812                        h_flex().gap_2().children(
 813                            vec![
 814                                ("Zed", VectorName::ZedLogo, 4),
 815                                ("Atom", VectorName::ZedLogo, 5),
 816                                ("JetBrains", VectorName::ZedLogo, 6),
 817                                ("Sublime", VectorName::ZedLogo, 7),
 818                                ("VSCode", VectorName::ZedLogo, 8),
 819                                ("Emacs", VectorName::ZedLogo, 9),
 820                                ("TextMate", VectorName::ZedLogo, 10),
 821                            ]
 822                            .into_iter()
 823                            .map(|(label, icon, index)| {
 824                                let is_focused = is_page_focused && focused_item == index;
 825                                let current_keymap = BaseKeymap::get_global(cx).to_string();
 826                                let is_selected = current_keymap == label;
 827
 828                                v_flex()
 829                                    .w(px(60.))
 830                                    .gap_1()
 831                                    .items_center()
 832                                    .justify_center()
 833                                    .text_center()
 834                                    .child(
 835                                        h_flex()
 836                                            .id(("keymap", index))
 837                                            .size(px(40.))
 838                                            .rounded(px(8.))
 839                                            .items_center()
 840                                            .justify_center()
 841                                            .border_1()
 842                                            .border_color(if is_selected {
 843                                                cx.theme().colors().border_selected
 844                                            } else {
 845                                                transparent_black()
 846                                            })
 847                                            .when(is_focused, |this| {
 848                                                this.border_color(
 849                                                    cx.theme().colors().border_focused,
 850                                                )
 851                                            })
 852                                            .when(is_selected, |this| {
 853                                                this.bg(cx.theme().status().info.opacity(0.08))
 854                                            })
 855                                            .child(
 856                                                h_flex()
 857                                                    .size(px(34.))
 858                                                    .rounded(px(6.))
 859                                                    .border_2()
 860                                                    .border_color(cx.theme().colors().border)
 861                                                    .items_center()
 862                                                    .justify_center()
 863                                                    .shadow_hairline()
 864                                                    .child(
 865                                                        Vector::new(icon, rems(1.25), rems(1.25))
 866                                                            .color(if is_selected {
 867                                                                Color::Info
 868                                                            } else {
 869                                                                Color::Default
 870                                                            }),
 871                                                    ),
 872                                            )
 873                                            .on_click(cx.listener(move |this, _, window, cx| {
 874                                                SettingsStore::update_global(
 875                                                    cx,
 876                                                    move |store, cx| {
 877                                                        let base_keymap = match label {
 878                                                            "Zed" => "None",
 879                                                            "Atom" => "Atom",
 880                                                            "JetBrains" => "JetBrains",
 881                                                            "Sublime" => "SublimeText",
 882                                                            "VSCode" => "VSCode",
 883                                                            "Emacs" => "Emacs",
 884                                                            "TextMate" => "TextMate",
 885                                                            _ => "VSCode",
 886                                                        };
 887                                                        let mut settings =
 888                                                            store.raw_user_settings().clone();
 889                                                        settings["base_keymap"] =
 890                                                            serde_json::json!(base_keymap);
 891                                                        store
 892                                                            .set_user_settings(
 893                                                                &settings.to_string(),
 894                                                                cx,
 895                                                            )
 896                                                            .ok();
 897                                                    },
 898                                                );
 899                                                cx.notify();
 900                                            })),
 901                                    )
 902                                    .child(
 903                                        div()
 904                                            .text_color(cx.theme().colors().text)
 905                                            .text_size(px(12.))
 906                                            .child(label),
 907                                    )
 908                            }),
 909                        ),
 910                    ),
 911            )
 912            // Settings checkboxes
 913            .child(
 914                v_flex()
 915                    .gap_3()
 916                    .mt_6()
 917                    .child({
 918                        let vim_enabled = VimModeSetting::get_global(cx).0;
 919                        h_flex()
 920                            .id("vim_mode_container")
 921                            .gap_2()
 922                            .items_center()
 923                            .p_1()
 924                            .rounded_md()
 925                            .when(is_page_focused && focused_item == 11, |this| {
 926                                this.border_2()
 927                                    .border_color(cx.theme().colors().border_focused)
 928                            })
 929                            .child(
 930                                div()
 931                                    .id("vim_mode_checkbox")
 932                                    .w_4()
 933                                    .h_4()
 934                                    .rounded_sm()
 935                                    .border_1()
 936                                    .border_color(cx.theme().colors().border)
 937                                    .when(vim_enabled, |this| {
 938                                        this.bg(cx.theme().colors().element_selected)
 939                                            .border_color(cx.theme().colors().border_selected)
 940                                    })
 941                                    .hover(|this| this.bg(cx.theme().colors().element_hover))
 942                                    .child(div().when(vim_enabled, |this| {
 943                                        this.size_full()
 944                                            .flex()
 945                                            .items_center()
 946                                            .justify_center()
 947                                            .child(Icon::new(IconName::Check))
 948                                    })),
 949                            )
 950                            .child(Label::new("Enable Vim Mode"))
 951                            .cursor_pointer()
 952                            .on_click(cx.listener(move |this, _, _window, cx| {
 953                                let current = VimModeSetting::get_global(cx).0;
 954                                SettingsStore::update_global(cx, move |store, cx| {
 955                                    let mut settings = store.raw_user_settings().clone();
 956                                    settings["vim_mode"] = serde_json::json!(!current);
 957                                    store.set_user_settings(&settings.to_string(), cx).ok();
 958                                });
 959                            }))
 960                    })
 961                    .child({
 962                        let crash_reports_enabled = TelemetrySettings::get_global(cx).diagnostics;
 963                        h_flex()
 964                            .id("crash_reports_container")
 965                            .gap_2()
 966                            .items_center()
 967                            .p_1()
 968                            .rounded_md()
 969                            .when(is_page_focused && focused_item == 12, |this| {
 970                                this.border_2()
 971                                    .border_color(cx.theme().colors().border_focused)
 972                            })
 973                            .child(
 974                                div()
 975                                    .id("crash_reports_checkbox")
 976                                    .w_4()
 977                                    .h_4()
 978                                    .rounded_sm()
 979                                    .border_1()
 980                                    .border_color(cx.theme().colors().border)
 981                                    .when(crash_reports_enabled, |this| {
 982                                        this.bg(cx.theme().colors().element_selected)
 983                                            .border_color(cx.theme().colors().border_selected)
 984                                    })
 985                                    .hover(|this| this.bg(cx.theme().colors().element_hover))
 986                                    .child(div().when(crash_reports_enabled, |this| {
 987                                        this.size_full()
 988                                            .flex()
 989                                            .items_center()
 990                                            .justify_center()
 991                                            .child(Icon::new(IconName::Check))
 992                                    })),
 993                            )
 994                            .child(Label::new("Send Crash Reports"))
 995                            .cursor_pointer()
 996                            .on_click(cx.listener(move |this, _, _window, cx| {
 997                                let current = TelemetrySettings::get_global(cx).diagnostics;
 998                                SettingsStore::update_global(cx, move |store, cx| {
 999                                    let mut settings = store.raw_user_settings().clone();
1000                                    if settings.get("telemetry").is_none() {
1001                                        settings["telemetry"] = serde_json::json!({});
1002                                    }
1003                                    settings["telemetry"]["diagnostics"] =
1004                                        serde_json::json!(!current);
1005                                    store.set_user_settings(&settings.to_string(), cx).ok();
1006                                });
1007                            }))
1008                    })
1009                    .child({
1010                        let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
1011                        h_flex()
1012                            .id("telemetry_container")
1013                            .gap_2()
1014                            .items_center()
1015                            .p_1()
1016                            .rounded_md()
1017                            .when(is_page_focused && focused_item == 13, |this| {
1018                                this.border_2()
1019                                    .border_color(cx.theme().colors().border_focused)
1020                            })
1021                            .child(
1022                                div()
1023                                    .id("telemetry_checkbox")
1024                                    .w_4()
1025                                    .h_4()
1026                                    .rounded_sm()
1027                                    .border_1()
1028                                    .border_color(cx.theme().colors().border)
1029                                    .when(telemetry_enabled, |this| {
1030                                        this.bg(cx.theme().colors().element_selected)
1031                                            .border_color(cx.theme().colors().border_selected)
1032                                    })
1033                                    .hover(|this| this.bg(cx.theme().colors().element_hover))
1034                                    .child(div().when(telemetry_enabled, |this| {
1035                                        this.size_full()
1036                                            .flex()
1037                                            .items_center()
1038                                            .justify_center()
1039                                            .child(Icon::new(IconName::Check))
1040                                    })),
1041                            )
1042                            .child(Label::new("Send Telemetry"))
1043                            .cursor_pointer()
1044                            .on_click(cx.listener(move |this, _, _window, cx| {
1045                                let current = TelemetrySettings::get_global(cx).metrics;
1046                                SettingsStore::update_global(cx, move |store, cx| {
1047                                    let mut settings = store.raw_user_settings().clone();
1048                                    if settings.get("telemetry").is_none() {
1049                                        settings["telemetry"] = serde_json::json!({});
1050                                    }
1051                                    settings["telemetry"]["metrics"] = serde_json::json!(!current);
1052                                    store.set_user_settings(&settings.to_string(), cx).ok();
1053                                });
1054                            }))
1055                    }),
1056            )
1057            .into_any_element()
1058    }
1059
1060    fn render_editing_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1061        let page_index = 1; // Editing page index
1062        let focused_item = self.page_focus[page_index].0;
1063        let is_page_focused = self.focus_area == FocusArea::PageContent;
1064
1065        v_flex()
1066            .h_full()
1067            .w_full()
1068            .items_center()
1069            .justify_center()
1070            .gap_4()
1071            .child(
1072                Label::new("Editing Features")
1073                    .size(LabelSize::Large)
1074                    .color(Color::Default),
1075            )
1076            .child(
1077                v_flex()
1078                    .gap_2()
1079                    .mt_4()
1080                    .child(
1081                        Button::new("try_multi_cursor", "Try Multi-cursor Editing")
1082                            .style(ButtonStyle::Filled)
1083                            .when(is_page_focused && focused_item == 0, |this| {
1084                                this.color(Color::Accent)
1085                            })
1086                            .on_click(cx.listener(|_, _, _, cx| {
1087                                cx.notify();
1088                            })),
1089                    )
1090                    .child(
1091                        Button::new("learn_shortcuts", "Learn Keyboard Shortcuts")
1092                            .style(ButtonStyle::Filled)
1093                            .when(is_page_focused && focused_item == 1, |this| {
1094                                this.color(Color::Accent)
1095                            })
1096                            .on_click(cx.listener(|_, _, _, cx| {
1097                                cx.notify();
1098                            })),
1099                    )
1100                    .child(
1101                        Button::new("explore_actions", "Explore Command Palette")
1102                            .style(ButtonStyle::Filled)
1103                            .when(is_page_focused && focused_item == 2, |this| {
1104                                this.color(Color::Accent)
1105                            })
1106                            .on_click(cx.listener(|_, _, _, cx| {
1107                                cx.notify();
1108                            })),
1109                    ),
1110            )
1111            .into_any_element()
1112    }
1113
1114    fn render_ai_setup_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1115        let page_index = 2; // AI Setup page index
1116        let focused_item = self.page_focus[page_index].0;
1117        let is_page_focused = self.focus_area == FocusArea::PageContent;
1118
1119        v_flex()
1120            .h_full()
1121            .w_full()
1122            .items_center()
1123            .justify_center()
1124            .gap_4()
1125            .child(
1126                Label::new("AI Assistant Setup")
1127                    .size(LabelSize::Large)
1128                    .color(Color::Default),
1129            )
1130            .child(
1131                v_flex()
1132                    .gap_2()
1133                    .mt_4()
1134                    .child(
1135                        Button::new("configure_ai", "Configure AI Provider")
1136                            .style(ButtonStyle::Filled)
1137                            .when(is_page_focused && focused_item == 0, |this| {
1138                                this.color(Color::Accent)
1139                            })
1140                            .on_click(cx.listener(|_, _, _, cx| {
1141                                cx.notify();
1142                            })),
1143                    )
1144                    .child(
1145                        Button::new("try_ai_chat", "Try AI Chat")
1146                            .style(ButtonStyle::Filled)
1147                            .when(is_page_focused && focused_item == 1, |this| {
1148                                this.color(Color::Accent)
1149                            })
1150                            .on_click(cx.listener(|_, _, _, cx| {
1151                                cx.notify();
1152                            })),
1153                    ),
1154            )
1155            .into_any_element()
1156    }
1157
1158    fn render_welcome_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1159        let page_index = 3; // Welcome page index
1160        let focused_item = self.page_focus[page_index].0;
1161        let is_page_focused = self.focus_area == FocusArea::PageContent;
1162
1163        v_flex()
1164            .h_full()
1165            .w_full()
1166            .items_center()
1167            .justify_center()
1168            .gap_4()
1169            .child(
1170                Label::new("Welcome to Zed!")
1171                    .size(LabelSize::Large)
1172                    .color(Color::Default),
1173            )
1174            .child(
1175                Label::new("You're all set up and ready to code")
1176                    .size(LabelSize::Default)
1177                    .color(Color::Muted),
1178            )
1179            .child(
1180                Button::new("finish_onboarding", "Start Coding!")
1181                    .style(ButtonStyle::Filled)
1182                    .size(ButtonSize::Large)
1183                    .when(is_page_focused && focused_item == 0, |this| {
1184                        this.color(Color::Accent)
1185                    })
1186                    .on_click(cx.listener(|_, _, _, cx| {
1187                        // TODO: Close onboarding and start coding
1188                        cx.notify();
1189                    })),
1190            )
1191            .into_any_element()
1192    }
1193}
1194
1195impl Item for OnboardingUI {
1196    type Event = ItemEvent;
1197
1198    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1199        "Onboarding".into()
1200    }
1201
1202    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1203        f(event.clone())
1204    }
1205
1206    fn added_to_workspace(
1207        &mut self,
1208        workspace: &mut Workspace,
1209        _window: &mut Window,
1210        _cx: &mut Context<Self>,
1211    ) {
1212        self.workspace_id = workspace.database_id();
1213    }
1214
1215    fn show_toolbar(&self) -> bool {
1216        false
1217    }
1218
1219    fn clone_on_split(
1220        &self,
1221        _workspace_id: Option<WorkspaceId>,
1222        window: &mut Window,
1223        cx: &mut Context<Self>,
1224    ) -> Option<Entity<Self>> {
1225        let weak_workspace = self.workspace.clone();
1226        let client = self.client.clone();
1227        if let Some(workspace) = weak_workspace.upgrade() {
1228            workspace.update(cx, |workspace, cx| {
1229                Some(cx.new(|cx| OnboardingUI::new(workspace, client, cx)))
1230            })
1231        } else {
1232            None
1233        }
1234    }
1235}
1236
1237impl SerializableItem for OnboardingUI {
1238    fn serialized_item_kind() -> &'static str {
1239        "OnboardingUI"
1240    }
1241
1242    fn deserialize(
1243        _project: Entity<Project>,
1244        workspace: WeakEntity<Workspace>,
1245        workspace_id: WorkspaceId,
1246        item_id: u64,
1247        window: &mut Window,
1248        cx: &mut App,
1249    ) -> Task<anyhow::Result<Entity<Self>>> {
1250        window.spawn(cx, async move |cx| {
1251            let (current_page, completed_pages) = if let Some((page_str, completed_str)) =
1252                ONBOARDING_DB.get_state(item_id, workspace_id)?
1253            {
1254                let page = match page_str.as_str() {
1255                    "basics" => OnboardingPage::Basics,
1256                    "editing" => OnboardingPage::Editing,
1257                    "ai_setup" => OnboardingPage::AiSetup,
1258                    "welcome" => OnboardingPage::Welcome,
1259                    _ => OnboardingPage::Basics,
1260                };
1261                let completed = OnboardingUI::completed_pages_from_string(&completed_str);
1262                (page, completed)
1263            } else {
1264                (OnboardingPage::Basics, [false; 4])
1265            };
1266
1267            cx.update(|window, cx| {
1268                let workspace = workspace
1269                    .upgrade()
1270                    .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?;
1271
1272                workspace.update(cx, |workspace, cx| {
1273                    let client = workspace.client().clone();
1274                    Ok(cx.new(|cx| {
1275                        let mut onboarding = OnboardingUI::new(workspace, client, cx);
1276                        onboarding.current_page = current_page;
1277                        onboarding.completed_pages = completed_pages;
1278                        onboarding
1279                    }))
1280                })
1281            })?
1282        })
1283    }
1284
1285    fn serialize(
1286        &mut self,
1287        _workspace: &mut Workspace,
1288        item_id: u64,
1289        _closing: bool,
1290        _window: &mut Window,
1291        cx: &mut Context<Self>,
1292    ) -> Option<Task<anyhow::Result<()>>> {
1293        let workspace_id = self.workspace_id?;
1294        let current_page = match self.current_page {
1295            OnboardingPage::Basics => "basics",
1296            OnboardingPage::Editing => "editing",
1297            OnboardingPage::AiSetup => "ai_setup",
1298            OnboardingPage::Welcome => "welcome",
1299        }
1300        .to_string();
1301        let completed_pages = self.completed_pages_to_string();
1302
1303        Some(cx.background_spawn(async move {
1304            ONBOARDING_DB
1305                .save_state(item_id, workspace_id, current_page, completed_pages)
1306                .await
1307        }))
1308    }
1309
1310    fn cleanup(
1311        _workspace_id: WorkspaceId,
1312        _item_ids: Vec<u64>,
1313        _window: &mut Window,
1314        _cx: &mut App,
1315    ) -> Task<anyhow::Result<()>> {
1316        Task::ready(Ok(()))
1317    }
1318
1319    fn should_serialize(&self, _event: &ItemEvent) -> bool {
1320        true
1321    }
1322}