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