onboarding_ui.rs

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