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(Label::new("Pick a Keymap").size(LabelSize::Large))
 805                    .child(
 806                        h_flex().gap_2().children(
 807                            vec![
 808                                ("Zed", VectorName::ZedLogo, 4),
 809                                ("Atom", VectorName::ZedLogo, 5),
 810                                ("JetBrains", VectorName::ZedLogo, 6),
 811                                ("Sublime", VectorName::ZedLogo, 7),
 812                                ("VSCode", VectorName::ZedLogo, 8),
 813                                ("Emacs", VectorName::ZedLogo, 9),
 814                                ("TextMate", VectorName::ZedLogo, 10),
 815                            ]
 816                            .into_iter()
 817                            .map(|(label, icon, index)| {
 818                                let is_focused = is_page_focused && focused_item == index;
 819                                let current_keymap = BaseKeymap::get_global(cx).to_string();
 820                                let is_selected = current_keymap == label;
 821
 822                                v_flex()
 823                                    .gap_1()
 824                                    .items_center()
 825                                    .child(
 826                                        div()
 827                                            .id(("keymap", index))
 828                                            .p_3()
 829                                            .rounded_md()
 830                                            .bg(cx.theme().colors().element_background)
 831                                            .border_1()
 832                                            .border_color(if is_selected {
 833                                                cx.theme().colors().border_selected
 834                                            } else {
 835                                                cx.theme().colors().border
 836                                            })
 837                                            .when(is_focused, |this| {
 838                                                this.border_color(
 839                                                    cx.theme().colors().border_focused,
 840                                                )
 841                                            })
 842                                            .when(is_selected, |this| {
 843                                                this.bg(cx.theme().colors().element_selected)
 844                                            })
 845                                            .hover(|this| {
 846                                                this.bg(cx.theme().colors().element_hover)
 847                                            })
 848                                            .child(
 849                                                Vector::new(icon, rems(2.), rems(2.))
 850                                                    .color(Color::Muted),
 851                                            )
 852                                            .on_click(cx.listener(move |this, _, window, cx| {
 853                                                SettingsStore::update_global(
 854                                                    cx,
 855                                                    move |store, cx| {
 856                                                        let base_keymap = match label {
 857                                                            "Zed" => "None",
 858                                                            "Atom" => "Atom",
 859                                                            "JetBrains" => "JetBrains",
 860                                                            "Sublime" => "SublimeText",
 861                                                            "VSCode" => "VSCode",
 862                                                            "Emacs" => "Emacs",
 863                                                            "TextMate" => "TextMate",
 864                                                            _ => "VSCode",
 865                                                        };
 866                                                        let mut settings =
 867                                                            store.raw_user_settings().clone();
 868                                                        settings["base_keymap"] =
 869                                                            serde_json::json!(base_keymap);
 870                                                        store
 871                                                            .set_user_settings(
 872                                                                &settings.to_string(),
 873                                                                cx,
 874                                                            )
 875                                                            .ok();
 876                                                    },
 877                                                );
 878                                                cx.notify();
 879                                            })),
 880                                    )
 881                                    .child(Label::new(label).size(LabelSize::Small).color(
 882                                        if is_selected {
 883                                            Color::Default
 884                                        } else {
 885                                            Color::Muted
 886                                        },
 887                                    ))
 888                            }),
 889                        ),
 890                    ),
 891            )
 892            // Settings checkboxes
 893            .child(
 894                v_flex()
 895                    .gap_3()
 896                    .mt_6()
 897                    .child({
 898                        let vim_enabled = VimModeSetting::get_global(cx).0;
 899                        h_flex()
 900                            .id("vim_mode_container")
 901                            .gap_2()
 902                            .items_center()
 903                            .p_1()
 904                            .rounded_md()
 905                            .when(is_page_focused && focused_item == 11, |this| {
 906                                this.border_2()
 907                                    .border_color(cx.theme().colors().border_focused)
 908                            })
 909                            .child(
 910                                div()
 911                                    .id("vim_mode_checkbox")
 912                                    .w_4()
 913                                    .h_4()
 914                                    .rounded_sm()
 915                                    .border_1()
 916                                    .border_color(cx.theme().colors().border)
 917                                    .when(vim_enabled, |this| {
 918                                        this.bg(cx.theme().colors().element_selected)
 919                                            .border_color(cx.theme().colors().border_selected)
 920                                    })
 921                                    .hover(|this| this.bg(cx.theme().colors().element_hover))
 922                                    .child(div().when(vim_enabled, |this| {
 923                                        this.size_full()
 924                                            .flex()
 925                                            .items_center()
 926                                            .justify_center()
 927                                            .child(Icon::new(IconName::Check))
 928                                    })),
 929                            )
 930                            .child(Label::new("Enable Vim Mode"))
 931                            .cursor_pointer()
 932                            .on_click(cx.listener(move |this, _, _window, cx| {
 933                                let current = VimModeSetting::get_global(cx).0;
 934                                SettingsStore::update_global(cx, move |store, cx| {
 935                                    let mut settings = store.raw_user_settings().clone();
 936                                    settings["vim_mode"] = serde_json::json!(!current);
 937                                    store.set_user_settings(&settings.to_string(), cx).ok();
 938                                });
 939                            }))
 940                    })
 941                    .child({
 942                        let crash_reports_enabled = TelemetrySettings::get_global(cx).diagnostics;
 943                        h_flex()
 944                            .id("crash_reports_container")
 945                            .gap_2()
 946                            .items_center()
 947                            .p_1()
 948                            .rounded_md()
 949                            .when(is_page_focused && focused_item == 12, |this| {
 950                                this.border_2()
 951                                    .border_color(cx.theme().colors().border_focused)
 952                            })
 953                            .child(
 954                                div()
 955                                    .id("crash_reports_checkbox")
 956                                    .w_4()
 957                                    .h_4()
 958                                    .rounded_sm()
 959                                    .border_1()
 960                                    .border_color(cx.theme().colors().border)
 961                                    .when(crash_reports_enabled, |this| {
 962                                        this.bg(cx.theme().colors().element_selected)
 963                                            .border_color(cx.theme().colors().border_selected)
 964                                    })
 965                                    .hover(|this| this.bg(cx.theme().colors().element_hover))
 966                                    .child(div().when(crash_reports_enabled, |this| {
 967                                        this.size_full()
 968                                            .flex()
 969                                            .items_center()
 970                                            .justify_center()
 971                                            .child(Icon::new(IconName::Check))
 972                                    })),
 973                            )
 974                            .child(Label::new("Send Crash Reports"))
 975                            .cursor_pointer()
 976                            .on_click(cx.listener(move |this, _, _window, cx| {
 977                                let current = TelemetrySettings::get_global(cx).diagnostics;
 978                                SettingsStore::update_global(cx, move |store, cx| {
 979                                    let mut settings = store.raw_user_settings().clone();
 980                                    if settings.get("telemetry").is_none() {
 981                                        settings["telemetry"] = serde_json::json!({});
 982                                    }
 983                                    settings["telemetry"]["diagnostics"] =
 984                                        serde_json::json!(!current);
 985                                    store.set_user_settings(&settings.to_string(), cx).ok();
 986                                });
 987                            }))
 988                    })
 989                    .child({
 990                        let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
 991                        h_flex()
 992                            .id("telemetry_container")
 993                            .gap_2()
 994                            .items_center()
 995                            .p_1()
 996                            .rounded_md()
 997                            .when(is_page_focused && focused_item == 13, |this| {
 998                                this.border_2()
 999                                    .border_color(cx.theme().colors().border_focused)
1000                            })
1001                            .child(
1002                                div()
1003                                    .id("telemetry_checkbox")
1004                                    .w_4()
1005                                    .h_4()
1006                                    .rounded_sm()
1007                                    .border_1()
1008                                    .border_color(cx.theme().colors().border)
1009                                    .when(telemetry_enabled, |this| {
1010                                        this.bg(cx.theme().colors().element_selected)
1011                                            .border_color(cx.theme().colors().border_selected)
1012                                    })
1013                                    .hover(|this| this.bg(cx.theme().colors().element_hover))
1014                                    .child(div().when(telemetry_enabled, |this| {
1015                                        this.size_full()
1016                                            .flex()
1017                                            .items_center()
1018                                            .justify_center()
1019                                            .child(Icon::new(IconName::Check))
1020                                    })),
1021                            )
1022                            .child(Label::new("Send Telemetry"))
1023                            .cursor_pointer()
1024                            .on_click(cx.listener(move |this, _, _window, cx| {
1025                                let current = TelemetrySettings::get_global(cx).metrics;
1026                                SettingsStore::update_global(cx, move |store, cx| {
1027                                    let mut settings = store.raw_user_settings().clone();
1028                                    if settings.get("telemetry").is_none() {
1029                                        settings["telemetry"] = serde_json::json!({});
1030                                    }
1031                                    settings["telemetry"]["metrics"] = serde_json::json!(!current);
1032                                    store.set_user_settings(&settings.to_string(), cx).ok();
1033                                });
1034                            }))
1035                    }),
1036            )
1037            .into_any_element()
1038    }
1039
1040    fn render_editing_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1041        let page_index = 1; // Editing page index
1042        let focused_item = self.page_focus[page_index].0;
1043        let is_page_focused = self.focus_area == FocusArea::PageContent;
1044
1045        v_flex()
1046            .h_full()
1047            .w_full()
1048            .items_center()
1049            .justify_center()
1050            .gap_4()
1051            .child(
1052                Label::new("Editing Features")
1053                    .size(LabelSize::Large)
1054                    .color(Color::Default),
1055            )
1056            .child(
1057                v_flex()
1058                    .gap_2()
1059                    .mt_4()
1060                    .child(
1061                        Button::new("try_multi_cursor", "Try Multi-cursor Editing")
1062                            .style(ButtonStyle::Filled)
1063                            .when(is_page_focused && focused_item == 0, |this| {
1064                                this.color(Color::Accent)
1065                            })
1066                            .on_click(cx.listener(|_, _, _, cx| {
1067                                cx.notify();
1068                            })),
1069                    )
1070                    .child(
1071                        Button::new("learn_shortcuts", "Learn Keyboard Shortcuts")
1072                            .style(ButtonStyle::Filled)
1073                            .when(is_page_focused && focused_item == 1, |this| {
1074                                this.color(Color::Accent)
1075                            })
1076                            .on_click(cx.listener(|_, _, _, cx| {
1077                                cx.notify();
1078                            })),
1079                    )
1080                    .child(
1081                        Button::new("explore_actions", "Explore Command Palette")
1082                            .style(ButtonStyle::Filled)
1083                            .when(is_page_focused && focused_item == 2, |this| {
1084                                this.color(Color::Accent)
1085                            })
1086                            .on_click(cx.listener(|_, _, _, cx| {
1087                                cx.notify();
1088                            })),
1089                    ),
1090            )
1091            .into_any_element()
1092    }
1093
1094    fn render_ai_setup_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1095        let page_index = 2; // AI Setup page index
1096        let focused_item = self.page_focus[page_index].0;
1097        let is_page_focused = self.focus_area == FocusArea::PageContent;
1098
1099        v_flex()
1100            .h_full()
1101            .w_full()
1102            .items_center()
1103            .justify_center()
1104            .gap_4()
1105            .child(
1106                Label::new("AI Assistant Setup")
1107                    .size(LabelSize::Large)
1108                    .color(Color::Default),
1109            )
1110            .child(
1111                v_flex()
1112                    .gap_2()
1113                    .mt_4()
1114                    .child(
1115                        Button::new("configure_ai", "Configure AI Provider")
1116                            .style(ButtonStyle::Filled)
1117                            .when(is_page_focused && focused_item == 0, |this| {
1118                                this.color(Color::Accent)
1119                            })
1120                            .on_click(cx.listener(|_, _, _, cx| {
1121                                cx.notify();
1122                            })),
1123                    )
1124                    .child(
1125                        Button::new("try_ai_chat", "Try AI Chat")
1126                            .style(ButtonStyle::Filled)
1127                            .when(is_page_focused && focused_item == 1, |this| {
1128                                this.color(Color::Accent)
1129                            })
1130                            .on_click(cx.listener(|_, _, _, cx| {
1131                                cx.notify();
1132                            })),
1133                    ),
1134            )
1135            .into_any_element()
1136    }
1137
1138    fn render_welcome_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1139        let page_index = 3; // Welcome page index
1140        let focused_item = self.page_focus[page_index].0;
1141        let is_page_focused = self.focus_area == FocusArea::PageContent;
1142
1143        v_flex()
1144            .h_full()
1145            .w_full()
1146            .items_center()
1147            .justify_center()
1148            .gap_4()
1149            .child(
1150                Label::new("Welcome to Zed!")
1151                    .size(LabelSize::Large)
1152                    .color(Color::Default),
1153            )
1154            .child(
1155                Label::new("You're all set up and ready to code")
1156                    .size(LabelSize::Default)
1157                    .color(Color::Muted),
1158            )
1159            .child(
1160                Button::new("finish_onboarding", "Start Coding!")
1161                    .style(ButtonStyle::Filled)
1162                    .size(ButtonSize::Large)
1163                    .when(is_page_focused && focused_item == 0, |this| {
1164                        this.color(Color::Accent)
1165                    })
1166                    .on_click(cx.listener(|_, _, _, cx| {
1167                        // TODO: Close onboarding and start coding
1168                        cx.notify();
1169                    })),
1170            )
1171            .into_any_element()
1172    }
1173}
1174
1175impl Item for OnboardingUI {
1176    type Event = ItemEvent;
1177
1178    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1179        "Onboarding".into()
1180    }
1181
1182    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1183        f(event.clone())
1184    }
1185
1186    fn added_to_workspace(
1187        &mut self,
1188        workspace: &mut Workspace,
1189        _window: &mut Window,
1190        _cx: &mut Context<Self>,
1191    ) {
1192        self.workspace_id = workspace.database_id();
1193    }
1194
1195    fn show_toolbar(&self) -> bool {
1196        false
1197    }
1198
1199    fn clone_on_split(
1200        &self,
1201        _workspace_id: Option<WorkspaceId>,
1202        window: &mut Window,
1203        cx: &mut Context<Self>,
1204    ) -> Option<Entity<Self>> {
1205        let weak_workspace = self.workspace.clone();
1206        let client = self.client.clone();
1207        if let Some(workspace) = weak_workspace.upgrade() {
1208            workspace.update(cx, |workspace, cx| {
1209                Some(cx.new(|cx| OnboardingUI::new(workspace, client, cx)))
1210            })
1211        } else {
1212            None
1213        }
1214    }
1215}
1216
1217impl SerializableItem for OnboardingUI {
1218    fn serialized_item_kind() -> &'static str {
1219        "OnboardingUI"
1220    }
1221
1222    fn deserialize(
1223        _project: Entity<Project>,
1224        workspace: WeakEntity<Workspace>,
1225        workspace_id: WorkspaceId,
1226        item_id: u64,
1227        window: &mut Window,
1228        cx: &mut App,
1229    ) -> Task<anyhow::Result<Entity<Self>>> {
1230        window.spawn(cx, async move |cx| {
1231            let (current_page, completed_pages) = if let Some((page_str, completed_str)) =
1232                ONBOARDING_DB.get_state(item_id, workspace_id)?
1233            {
1234                let page = match page_str.as_str() {
1235                    "basics" => OnboardingPage::Basics,
1236                    "editing" => OnboardingPage::Editing,
1237                    "ai_setup" => OnboardingPage::AiSetup,
1238                    "welcome" => OnboardingPage::Welcome,
1239                    _ => OnboardingPage::Basics,
1240                };
1241                let completed = OnboardingUI::completed_pages_from_string(&completed_str);
1242                (page, completed)
1243            } else {
1244                (OnboardingPage::Basics, [false; 4])
1245            };
1246
1247            cx.update(|window, cx| {
1248                let workspace = workspace
1249                    .upgrade()
1250                    .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?;
1251
1252                workspace.update(cx, |workspace, cx| {
1253                    let client = workspace.client().clone();
1254                    Ok(cx.new(|cx| {
1255                        let mut onboarding = OnboardingUI::new(workspace, client, cx);
1256                        onboarding.current_page = current_page;
1257                        onboarding.completed_pages = completed_pages;
1258                        onboarding
1259                    }))
1260                })
1261            })?
1262        })
1263    }
1264
1265    fn serialize(
1266        &mut self,
1267        _workspace: &mut Workspace,
1268        item_id: u64,
1269        _closing: bool,
1270        _window: &mut Window,
1271        cx: &mut Context<Self>,
1272    ) -> Option<Task<anyhow::Result<()>>> {
1273        let workspace_id = self.workspace_id?;
1274        let current_page = match self.current_page {
1275            OnboardingPage::Basics => "basics",
1276            OnboardingPage::Editing => "editing",
1277            OnboardingPage::AiSetup => "ai_setup",
1278            OnboardingPage::Welcome => "welcome",
1279        }
1280        .to_string();
1281        let completed_pages = self.completed_pages_to_string();
1282
1283        Some(cx.background_spawn(async move {
1284            ONBOARDING_DB
1285                .save_state(item_id, workspace_id, current_page, completed_pages)
1286                .await
1287        }))
1288    }
1289
1290    fn cleanup(
1291        _workspace_id: WorkspaceId,
1292        _item_ids: Vec<u64>,
1293        _window: &mut Window,
1294        _cx: &mut App,
1295    ) -> Task<anyhow::Result<()>> {
1296        Task::ready(Ok(()))
1297    }
1298
1299    fn should_serialize(&self, _event: &ItemEvent) -> bool {
1300        true
1301    }
1302}