onboarding_ui.rs

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