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