onboarding_ui.rs

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