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 handle_jump_to_basics(
 499        &mut self,
 500        _: &JumpToBasics,
 501        window: &mut Window,
 502        cx: &mut Context<Self>,
 503    ) {
 504        self.jump_to_page(OnboardingPage::Basics, window, cx);
 505    }
 506
 507    fn handle_jump_to_editing(
 508        &mut self,
 509        _: &JumpToEditing,
 510        window: &mut Window,
 511        cx: &mut Context<Self>,
 512    ) {
 513        self.jump_to_page(OnboardingPage::Editing, window, cx);
 514    }
 515
 516    fn handle_jump_to_ai_setup(
 517        &mut self,
 518        _: &JumpToAiSetup,
 519        window: &mut Window,
 520        cx: &mut Context<Self>,
 521    ) {
 522        self.jump_to_page(OnboardingPage::AiSetup, window, cx);
 523    }
 524
 525    fn handle_jump_to_welcome(
 526        &mut self,
 527        _: &JumpToWelcome,
 528        window: &mut Window,
 529        cx: &mut Context<Self>,
 530    ) {
 531        self.jump_to_page(OnboardingPage::Welcome, window, cx);
 532    }
 533
 534    fn handle_next_page(&mut self, _: &NextPage, window: &mut Window, cx: &mut Context<Self>) {
 535        self.next_page(window, cx);
 536    }
 537
 538    fn handle_enable_ai_assistance(
 539        &mut self,
 540        _: &zed_actions::EnableAiAssistance,
 541        _window: &mut Window,
 542        cx: &mut Context<Self>,
 543    ) {
 544        if let Some(workspace) = self.workspace.upgrade() {
 545            let fs = workspace.read(cx).app_state().fs.clone();
 546            update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
 547                file.features
 548                    .get_or_insert(Default::default())
 549                    .ai_assistance = Some(true);
 550            });
 551            cx.notify();
 552        }
 553    }
 554
 555    fn handle_disable_ai_assistance(
 556        &mut self,
 557        _: &zed_actions::DisableAiAssistance,
 558        _window: &mut Window,
 559        cx: &mut Context<Self>,
 560    ) {
 561        if let Some(workspace) = self.workspace.upgrade() {
 562            let fs = workspace.read(cx).app_state().fs.clone();
 563            update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
 564                file.features
 565                    .get_or_insert(Default::default())
 566                    .ai_assistance = Some(false);
 567            });
 568            cx.notify();
 569        }
 570    }
 571
 572    fn handle_previous_page(
 573        &mut self,
 574        _: &PreviousPage,
 575        window: &mut Window,
 576        cx: &mut Context<Self>,
 577    ) {
 578        self.previous_page(window, cx);
 579    }
 580
 581    fn render_navigation(
 582        &mut self,
 583        window: &mut Window,
 584        cx: &mut Context<Self>,
 585    ) -> impl gpui::IntoElement {
 586        let client = self.client.clone();
 587
 588        v_flex()
 589            .h_full()
 590            .w(px(256.))
 591            .gap_2()
 592            .justify_between()
 593            .child(
 594                v_flex()
 595                    .w_full()
 596                    .gap_px()
 597                    .child(
 598                        h_flex()
 599                            .w_full()
 600                            .justify_between()
 601                            .py(px(24.))
 602                            .pl(px(24.))
 603                            .pr(px(12.))
 604                            .child(
 605                                Vector::new(VectorName::ZedLogo, rems(2.), rems(2.))
 606                                    .color(Color::Custom(cx.theme().colors().icon.opacity(0.5))),
 607                            )
 608                            .child(
 609                                Button::new("sign_in", "Sign in")
 610                                    .color(Color::Muted)
 611                                    .label_size(LabelSize::Small)
 612                                    .when(
 613                                        self.focus_area == FocusArea::Navigation
 614                                            && self.nav_focus == NavigationFocusItem::SignIn,
 615                                        |this| this.color(Color::Accent),
 616                                    )
 617                                    .size(ButtonSize::Compact)
 618                                    .on_click(cx.listener(move |_, _, window, cx| {
 619                                        let client = client.clone();
 620                                        window
 621                                            .spawn(cx, async move |cx| {
 622                                                client
 623                                                    .authenticate_and_connect(true, &cx)
 624                                                    .await
 625                                                    .into_response()
 626                                                    .notify_async_err(cx);
 627                                            })
 628                                            .detach();
 629                                    })),
 630                            ),
 631                    )
 632                    .child(
 633                        v_flex()
 634                            .gap_px()
 635                            .py(px(16.))
 636                            .gap(px(2.))
 637                            .child(self.render_nav_item(
 638                                OnboardingPage::Basics,
 639                                "The Basics",
 640                                "1",
 641                                cx,
 642                            ))
 643                            .child(self.render_nav_item(
 644                                OnboardingPage::Editing,
 645                                "Editing Experience",
 646                                "2",
 647                                cx,
 648                            ))
 649                            .child(self.render_nav_item(
 650                                OnboardingPage::AiSetup,
 651                                "AI Setup",
 652                                "3",
 653                                cx,
 654                            ))
 655                            .child(self.render_nav_item(
 656                                OnboardingPage::Welcome,
 657                                "Welcome",
 658                                "4",
 659                                cx,
 660                            )),
 661                    ),
 662            )
 663            .child(self.render_bottom_controls(window, cx))
 664    }
 665
 666    fn render_nav_item(
 667        &mut self,
 668        page: OnboardingPage,
 669        label: impl Into<SharedString>,
 670        shortcut: impl Into<SharedString>,
 671        cx: &mut Context<Self>,
 672    ) -> impl gpui::IntoElement {
 673        let is_selected = self.current_page == page;
 674        let label = label.into();
 675        let shortcut = shortcut.into();
 676        let id = ElementId::Name(label.clone());
 677        let corner_radius = px(4.);
 678
 679        let item_focused = match page {
 680            OnboardingPage::Basics => self.nav_focus == NavigationFocusItem::Basics,
 681            OnboardingPage::Editing => self.nav_focus == NavigationFocusItem::Editing,
 682            OnboardingPage::AiSetup => self.nav_focus == NavigationFocusItem::AiSetup,
 683            OnboardingPage::Welcome => self.nav_focus == NavigationFocusItem::Welcome,
 684        };
 685
 686        let area_focused = self.focus_area == FocusArea::Navigation;
 687
 688        FocusOutline::new(corner_radius, item_focused, px(2.))
 689            .active(area_focused && item_focused)
 690            .child(
 691                h_flex()
 692                    .id(id)
 693                    .h(rems(1.625))
 694                    .w_full()
 695                    .rounded(corner_radius)
 696                    .px_3()
 697                    .when(is_selected, |this| {
 698                        this.bg(cx.theme().colors().border_focused.opacity(0.16))
 699                    })
 700                    .child(
 701                        h_flex()
 702                            .flex_1()
 703                            .justify_between()
 704                            .items_center()
 705                            .child(
 706                                Label::new(label)
 707                                    .weight(FontWeight::MEDIUM)
 708                                    .color(Color::Muted)
 709                                    .when(item_focused, |this| this.color(Color::Default)),
 710                            )
 711                            .child(
 712                                Label::new(format!("{}", shortcut.clone()))
 713                                    .color(Color::Placeholder)
 714                                    .size(LabelSize::XSmall),
 715                            ),
 716                    )
 717                    .on_click(cx.listener(move |this, _, window, cx| {
 718                        this.jump_to_page(page, window, cx);
 719                    })),
 720            )
 721    }
 722
 723    fn render_bottom_controls(
 724        &mut self,
 725        window: &mut gpui::Window,
 726        cx: &mut Context<Self>,
 727    ) -> impl gpui::IntoElement {
 728        h_flex().w_full().p(px(12.)).child(
 729            JuicyButton::new(if self.current_page == OnboardingPage::Welcome {
 730                "Get Started"
 731            } else {
 732                "Next"
 733            })
 734            .keybinding(ui::KeyBinding::for_action_in(
 735                &NextPage,
 736                &self.focus_handle,
 737                window,
 738                cx,
 739            ))
 740            .on_click(cx.listener(|this, _, window, cx| {
 741                this.next_page(window, cx);
 742            })),
 743        )
 744    }
 745
 746    fn render_active_page(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 747        match self.current_page {
 748            OnboardingPage::Basics => self.render_basics_page(cx),
 749            OnboardingPage::Editing => self.render_editing_page(cx),
 750            OnboardingPage::AiSetup => self.render_ai_setup_page(cx),
 751            OnboardingPage::Welcome => self.render_welcome_page(cx),
 752        }
 753    }
 754
 755    fn render_basics_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
 756        let page_index = 0; // Basics page index
 757        let focused_item = self.page_focus[page_index].0;
 758        let is_page_focused = self.focus_area == FocusArea::PageContent;
 759
 760        use theme_preview::ThemePreviewTile;
 761
 762        // Get available themes
 763        let theme_registry = ThemeRegistry::default_global(cx);
 764        let theme_names = theme_registry.list_names();
 765        let current_theme = cx.theme().clone();
 766
 767        v_flex()
 768            .id("theme-selector")
 769            .h_full()
 770            .w_full()
 771            .overflow_y_scroll()
 772            .child({
 773                let vim_enabled = VimModeSetting::get_global(cx).0;
 774                CheckboxRow::new("Enable Vim Mode")
 775                    .checked(vim_enabled)
 776                    .on_click(move |_window, cx| {
 777                        let current = VimModeSetting::get_global(cx).0;
 778                        SettingsStore::update_global(cx, move |store, cx| {
 779                            let mut settings = store.raw_user_settings().clone();
 780                            settings["vim_mode"] = serde_json::json!(!current);
 781                            store.set_user_settings(&settings.to_string(), cx).ok();
 782                        });
 783                    })
 784            })
 785            // Theme selector section
 786            .child(
 787                v_flex()
 788                    .w_full()
 789                    .overflow_hidden()
 790                    .child(
 791                        HeaderRow::new("Pick a Theme").end_slot(
 792                            Button::new("more_themes", "More Themes")
 793                                .style(ButtonStyle::Subtle)
 794                                .color(Color::Muted)
 795                                .on_click(cx.listener(|_, _, window, cx| {
 796                                    window.dispatch_action(
 797                                        zed_actions::theme_selector::Toggle::default()
 798                                            .boxed_clone(),
 799                                        cx,
 800                                    );
 801                                })),
 802                        ),
 803                    )
 804                    .child(
 805                        h_flex().w_full().overflow_hidden().gap_3().children(
 806                            vec![
 807                                ("One Dark", "One Dark"),
 808                                ("Gruvbox Dark", "Gruvbox Dark"),
 809                                ("One Light", "One Light"),
 810                                ("Gruvbox Light", "Gruvbox Light"),
 811                            ]
 812                            .into_iter()
 813                            .enumerate()
 814                            .map(|(i, (label, theme_name))| {
 815                                let is_selected = current_theme.name == *theme_name;
 816                                let is_focused = is_page_focused && focused_item == i;
 817
 818                                v_flex()
 819                                    .flex_1()
 820                                    .gap_1p5()
 821                                    .justify_center()
 822                                    .text_center()
 823                                    .child(
 824                                        div()
 825                                            .id(("theme", i))
 826                                            .rounded(px(8.))
 827                                            .h(px(90.))
 828                                            .w_full()
 829                                            .overflow_hidden()
 830                                            .border_1()
 831                                            .border_color(if is_focused {
 832                                                cx.theme().colors().border_focused
 833                                            } else {
 834                                                transparent_black()
 835                                            })
 836                                            .child(
 837                                                if let Ok(theme) = theme_registry.get(theme_name) {
 838                                                    ThemePreviewTile::new(theme, is_selected, 0.5)
 839                                                        .into_any_element()
 840                                                } else {
 841                                                    div()
 842                                                        .size_full()
 843                                                        .bg(cx.theme().colors().surface_background)
 844                                                        .rounded_md()
 845                                                        .into_any_element()
 846                                                },
 847                                            )
 848                                            .on_click(cx.listener(move |this, _, window, cx| {
 849                                                SettingsStore::update_global(
 850                                                    cx,
 851                                                    move |store, cx| {
 852                                                        let mut settings =
 853                                                            store.raw_user_settings().clone();
 854                                                        settings["theme"] =
 855                                                            serde_json::json!(theme_name);
 856                                                        store
 857                                                            .set_user_settings(
 858                                                                &settings.to_string(),
 859                                                                cx,
 860                                                            )
 861                                                            .ok();
 862                                                    },
 863                                                );
 864                                                cx.notify();
 865                                            })),
 866                                    )
 867                                    .child(
 868                                        div()
 869                                            .text_color(cx.theme().colors().text)
 870                                            .text_size(px(12.))
 871                                            .child(label),
 872                                    )
 873                            }),
 874                        ),
 875                    ),
 876            )
 877            // Keymap selector section
 878            .child(
 879                v_flex()
 880                    .gap_1()
 881                    .child(HeaderRow::new("Pick a Keymap"))
 882                    .child(
 883                        h_flex().gap_2().children(
 884                            vec![
 885                                ("Zed", VectorName::ZedLogo, 4),
 886                                ("Atom", VectorName::AtomLogo, 5),
 887                                ("JetBrains", VectorName::ZedLogo, 6),
 888                                ("Sublime", VectorName::ZedLogo, 7),
 889                                ("VSCode", VectorName::ZedLogo, 8),
 890                                ("Emacs", VectorName::ZedLogo, 9),
 891                                ("TextMate", VectorName::ZedLogo, 10),
 892                            ]
 893                            .into_iter()
 894                            .map(|(label, icon, index)| {
 895                                let is_focused = is_page_focused && focused_item == index;
 896                                let current_keymap = BaseKeymap::get_global(cx).to_string();
 897                                let is_selected = current_keymap == label;
 898
 899                                v_flex()
 900                                    .w(px(72.))
 901                                    .gap_1()
 902                                    .items_center()
 903                                    .justify_center()
 904                                    .text_center()
 905                                    .child(
 906                                        h_flex()
 907                                            .id(("keymap", index))
 908                                            .size(px(48.))
 909                                            .rounded(px(8.))
 910                                            .items_center()
 911                                            .justify_center()
 912                                            .border_1()
 913                                            .border_color(if is_selected {
 914                                                cx.theme().colors().border_selected
 915                                            } else {
 916                                                transparent_black()
 917                                            })
 918                                            .when(is_focused, |this| {
 919                                                this.border_color(
 920                                                    cx.theme().colors().border_focused,
 921                                                )
 922                                            })
 923                                            .when(is_selected, |this| {
 924                                                this.bg(cx.theme().status().info.opacity(0.08))
 925                                            })
 926                                            .child(
 927                                                h_flex()
 928                                                    .size(px(34.))
 929                                                    .rounded(px(6.))
 930                                                    .border_2()
 931                                                    .border_color(cx.theme().colors().border)
 932                                                    .items_center()
 933                                                    .justify_center()
 934                                                    .shadow_hairline()
 935                                                    .child(
 936                                                        Vector::new(icon, rems(1.25), rems(1.25))
 937                                                            .color(if is_selected {
 938                                                                Color::Info
 939                                                            } else {
 940                                                                Color::Default
 941                                                            }),
 942                                                    ),
 943                                            )
 944                                            .on_click(cx.listener(move |this, _, window, cx| {
 945                                                SettingsStore::update_global(
 946                                                    cx,
 947                                                    move |store, cx| {
 948                                                        let base_keymap = match label {
 949                                                            "Zed" => "None",
 950                                                            "Atom" => "Atom",
 951                                                            "JetBrains" => "JetBrains",
 952                                                            "Sublime" => "SublimeText",
 953                                                            "VSCode" => "VSCode",
 954                                                            "Emacs" => "Emacs",
 955                                                            "TextMate" => "TextMate",
 956                                                            _ => "VSCode",
 957                                                        };
 958                                                        let mut settings =
 959                                                            store.raw_user_settings().clone();
 960                                                        settings["base_keymap"] =
 961                                                            serde_json::json!(base_keymap);
 962                                                        store
 963                                                            .set_user_settings(
 964                                                                &settings.to_string(),
 965                                                                cx,
 966                                                            )
 967                                                            .ok();
 968                                                    },
 969                                                );
 970                                                cx.notify();
 971                                            })),
 972                                    )
 973                                    .child(
 974                                        div()
 975                                            .text_color(cx.theme().colors().text)
 976                                            .text_size(px(12.))
 977                                            .child(label),
 978                                    )
 979                            }),
 980                        ),
 981                    ),
 982            )
 983            // Settings checkboxes
 984            .child(
 985                v_flex()
 986                    .gap_1()
 987                    .child(HeaderRow::new("Help Improve Zed"))
 988                    .child({
 989                        let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
 990                        CheckboxRow::new("Send Telemetry")
 991                            .description("Help improve Zed by sending anonymous usage data")
 992                            .checked(telemetry_enabled)
 993                            .on_click(move |_window, cx| {
 994                                let current = TelemetrySettings::get_global(cx).metrics;
 995                                SettingsStore::update_global(cx, move |store, cx| {
 996                                    let mut settings = store.raw_user_settings().clone();
 997                                    if settings.get("telemetry").is_none() {
 998                                        settings["telemetry"] = serde_json::json!({});
 999                                    }
1000                                    settings["telemetry"]["metrics"] = serde_json::json!(!current);
1001                                    store.set_user_settings(&settings.to_string(), cx).ok();
1002                                });
1003                            })
1004                    })
1005                    .child({
1006                        let crash_reports_enabled = TelemetrySettings::get_global(cx).diagnostics;
1007                        CheckboxRow::new("Send Crash Reports")
1008                            .description("We use crash reports to help us fix issues")
1009                            .checked(crash_reports_enabled)
1010                            .on_click(move |_window, cx| {
1011                                let current = TelemetrySettings::get_global(cx).diagnostics;
1012                                SettingsStore::update_global(cx, move |store, cx| {
1013                                    let mut settings = store.raw_user_settings().clone();
1014                                    if settings.get("telemetry").is_none() {
1015                                        settings["telemetry"] = serde_json::json!({});
1016                                    }
1017                                    settings["telemetry"]["diagnostics"] =
1018                                        serde_json::json!(!current);
1019                                    store.set_user_settings(&settings.to_string(), cx).ok();
1020                                });
1021                            })
1022                    }),
1023            )
1024            .into_any_element()
1025    }
1026
1027    fn render_editing_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1028        let page_index = 1; // Editing page index
1029        let focused_item = self.page_focus[page_index].0;
1030        let is_page_focused = self.focus_area == FocusArea::PageContent;
1031
1032        v_flex()
1033            .h_full()
1034            .w_full()
1035            .items_center()
1036            .justify_center()
1037            .gap_4()
1038            .child(
1039                Label::new("Editing Features")
1040                    .size(LabelSize::Large)
1041                    .color(Color::Default),
1042            )
1043            .child(
1044                v_flex()
1045                    .gap_2()
1046                    .mt_4()
1047                    .child(
1048                        Button::new("try_multi_cursor", "Try Multi-cursor Editing")
1049                            .style(ButtonStyle::Filled)
1050                            .when(is_page_focused && focused_item == 0, |this| {
1051                                this.color(Color::Accent)
1052                            })
1053                            .on_click(cx.listener(|_, _, _, cx| {
1054                                cx.notify();
1055                            })),
1056                    )
1057                    .child(
1058                        Button::new("learn_shortcuts", "Learn Keyboard Shortcuts")
1059                            .style(ButtonStyle::Filled)
1060                            .when(is_page_focused && focused_item == 1, |this| {
1061                                this.color(Color::Accent)
1062                            })
1063                            .on_click(cx.listener(|_, _, _, cx| {
1064                                cx.notify();
1065                            })),
1066                    )
1067                    .child(
1068                        Button::new("explore_actions", "Explore Command Palette")
1069                            .style(ButtonStyle::Filled)
1070                            .when(is_page_focused && focused_item == 2, |this| {
1071                                this.color(Color::Accent)
1072                            })
1073                            .on_click(cx.listener(|_, _, _, cx| {
1074                                cx.notify();
1075                            })),
1076                    ),
1077            )
1078            .into_any_element()
1079    }
1080
1081    fn render_ai_setup_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1082        let page_index = 2; // AI Setup page index
1083        let focused_item = self.page_focus[page_index].0;
1084        let is_page_focused = self.focus_area == FocusArea::PageContent;
1085
1086        let ai_enabled = ai_enabled(cx);
1087
1088        let workspace = self.workspace.clone();
1089
1090        v_flex()
1091            .h_full()
1092            .w_full()
1093            .gap_4()
1094            .child(
1095                h_flex()
1096                    .justify_start()
1097                    .child(
1098                CheckboxWithLabel::new(
1099                "disable_ai",
1100                Label::new("Enable AI Features"),
1101                if ai_enabled {
1102                    ToggleState::Selected
1103                } else {
1104                    ToggleState::Unselected
1105                },
1106                move |state, _, cx| {
1107                    let enabled = state == &ToggleState::Selected;
1108                    if let Some(workspace) = workspace.upgrade() {
1109                        let fs = workspace.read(cx).app_state().fs.clone();
1110                        update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
1111                            file.features
1112                                .get_or_insert(Default::default())
1113                                .ai_assistance = Some(enabled);
1114                        });
1115                    }
1116                },
1117            )))
1118            .child(
1119                CalloutRow::new("We don't use your code to train AI models")
1120                    .line("You choose which providers you enable, and they have their own privacy policies.")
1121                    .line("Read more about our privacy practices in our Privacy Policy.")
1122            )
1123            .child(
1124                HeaderRow::new("Choose your AI Providers")
1125            )
1126            .into_any_element()
1127    }
1128
1129    fn render_welcome_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
1130        // Lazy-initialize the welcome page if needed
1131        if self.welcome_page.is_none() {
1132            if let Some(workspace) = self.workspace.upgrade() {
1133                let _ = workspace.update(cx, |workspace, cx| {
1134                    self.welcome_page = Some(WelcomePage::new(workspace, cx));
1135                });
1136            }
1137        }
1138
1139        // Render the welcome page if it exists, otherwise show a fallback
1140        if let Some(welcome_page) = &self.welcome_page {
1141            welcome_page.clone().into_any_element()
1142        } else {
1143            // Fallback UI if we couldn't create the welcome page
1144            v_flex()
1145                .h_full()
1146                .w_full()
1147                .items_center()
1148                .justify_center()
1149                .child(
1150                    Label::new("Unable to load welcome page")
1151                        .size(LabelSize::Default)
1152                        .color(Color::Error),
1153                )
1154                .into_any_element()
1155        }
1156    }
1157}
1158
1159impl Item for OnboardingUI {
1160    type Event = ItemEvent;
1161
1162    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1163        "Onboarding".into()
1164    }
1165
1166    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1167        f(event.clone())
1168    }
1169
1170    fn added_to_workspace(
1171        &mut self,
1172        workspace: &mut Workspace,
1173        _window: &mut Window,
1174        _cx: &mut Context<Self>,
1175    ) {
1176        self.workspace_id = workspace.database_id();
1177    }
1178
1179    fn show_toolbar(&self) -> bool {
1180        false
1181    }
1182
1183    fn clone_on_split(
1184        &self,
1185        _workspace_id: Option<WorkspaceId>,
1186        window: &mut Window,
1187        cx: &mut Context<Self>,
1188    ) -> Option<Entity<Self>> {
1189        let weak_workspace = self.workspace.clone();
1190        let client = self.client.clone();
1191        if let Some(workspace) = weak_workspace.upgrade() {
1192            workspace.update(cx, |workspace, cx| {
1193                Some(cx.new(|cx| OnboardingUI::new(workspace, client, cx)))
1194            })
1195        } else {
1196            None
1197        }
1198    }
1199}
1200
1201impl SerializableItem for OnboardingUI {
1202    fn serialized_item_kind() -> &'static str {
1203        "OnboardingUI"
1204    }
1205
1206    fn deserialize(
1207        _project: Entity<Project>,
1208        workspace: WeakEntity<Workspace>,
1209        workspace_id: WorkspaceId,
1210        item_id: u64,
1211        window: &mut Window,
1212        cx: &mut App,
1213    ) -> Task<anyhow::Result<Entity<Self>>> {
1214        window.spawn(cx, async move |cx| {
1215            let (current_page, completed_pages) = if let Some((page_str, completed_str)) =
1216                ONBOARDING_DB.get_state(item_id, workspace_id)?
1217            {
1218                let page = match page_str.as_str() {
1219                    "basics" => OnboardingPage::Basics,
1220                    "editing" => OnboardingPage::Editing,
1221                    "ai_setup" => OnboardingPage::AiSetup,
1222                    "welcome" => OnboardingPage::Welcome,
1223                    _ => OnboardingPage::Basics,
1224                };
1225                let completed = OnboardingUI::completed_pages_from_string(&completed_str);
1226                (page, completed)
1227            } else {
1228                (OnboardingPage::Basics, HashSet::new())
1229            };
1230
1231            cx.update(|window, cx| {
1232                let workspace = workspace
1233                    .upgrade()
1234                    .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?;
1235
1236                workspace.update(cx, |workspace, cx| {
1237                    let client = workspace.client().clone();
1238                    Ok(cx.new(|cx| {
1239                        let mut onboarding = OnboardingUI::new(workspace, client, cx);
1240                        onboarding.current_page = current_page;
1241                        onboarding.completed_pages = completed_pages;
1242                        onboarding
1243                    }))
1244                })
1245            })?
1246        })
1247    }
1248
1249    fn serialize(
1250        &mut self,
1251        _workspace: &mut Workspace,
1252        item_id: u64,
1253        _closing: bool,
1254        _window: &mut Window,
1255        cx: &mut Context<Self>,
1256    ) -> Option<Task<anyhow::Result<()>>> {
1257        let workspace_id = self.workspace_id?;
1258        let current_page = match self.current_page {
1259            OnboardingPage::Basics => "basics",
1260            OnboardingPage::Editing => "editing",
1261            OnboardingPage::AiSetup => "ai_setup",
1262            OnboardingPage::Welcome => "welcome",
1263        }
1264        .to_string();
1265        let completed_pages = self.completed_pages_to_string();
1266
1267        Some(cx.background_spawn(async move {
1268            ONBOARDING_DB
1269                .save_state(item_id, workspace_id, current_page, completed_pages)
1270                .await
1271        }))
1272    }
1273
1274    fn cleanup(
1275        _workspace_id: WorkspaceId,
1276        _item_ids: Vec<u64>,
1277        _window: &mut Window,
1278        _cx: &mut App,
1279    ) -> Task<anyhow::Result<()>> {
1280        Task::ready(Ok(()))
1281    }
1282
1283    fn should_serialize(&self, _event: &ItemEvent) -> bool {
1284        true
1285    }
1286}