onboarding_ui.rs

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