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