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