onboarding_ui.rs

   1#![allow(unused, dead_code)]
   2mod persistence;
   3
   4use client::Client;
   5use command_palette_hooks::CommandPaletteFilter;
   6use feature_flags::FeatureFlagAppExt as _;
   7use gpui::{
   8    Entity, EventEmitter, FocusHandle, Focusable, KeyBinding, Task, WeakEntity, actions, prelude::*,
   9};
  10use menu;
  11use persistence::ONBOARDING_DB;
  12
  13use project::Project;
  14use settings_ui::SettingsUiFeatureFlag;
  15use std::sync::Arc;
  16use ui::{ListItem, Vector, VectorName, prelude::*};
  17use util::ResultExt;
  18use workspace::{
  19    Workspace, WorkspaceId,
  20    item::{Item, ItemEvent, SerializableItem},
  21    notifications::NotifyResultExt,
  22};
  23
  24actions!(
  25    onboarding,
  26    [
  27        ShowOnboarding,
  28        JumpToBasics,
  29        JumpToEditing,
  30        JumpToAiSetup,
  31        JumpToWelcome,
  32        NextPage,
  33        PreviousPage,
  34        ToggleFocus,
  35        ResetOnboarding,
  36    ]
  37);
  38
  39pub fn init(cx: &mut App) {
  40    cx.observe_new(|workspace: &mut Workspace, _, _cx| {
  41        workspace.register_action(|workspace, _: &ShowOnboarding, window, cx| {
  42            let client = workspace.client().clone();
  43            let onboarding = cx.new(|cx| OnboardingUI::new(workspace, client, cx));
  44            workspace.add_item_to_active_pane(Box::new(onboarding), None, true, window, cx);
  45        });
  46    })
  47    .detach();
  48
  49    workspace::register_serializable_item::<OnboardingUI>(cx);
  50
  51    feature_gate_onboarding_ui_actions(cx);
  52}
  53
  54fn feature_gate_onboarding_ui_actions(cx: &mut App) {
  55    const ONBOARDING_ACTION_NAMESPACE: &str = "onboarding_ui";
  56
  57    CommandPaletteFilter::update_global(cx, |filter, _cx| {
  58        filter.hide_namespace(ONBOARDING_ACTION_NAMESPACE);
  59    });
  60
  61    cx.observe_flag::<SettingsUiFeatureFlag, _>({
  62        move |is_enabled, cx| {
  63            CommandPaletteFilter::update_global(cx, |filter, _cx| {
  64                if is_enabled {
  65                    filter.show_namespace(ONBOARDING_ACTION_NAMESPACE);
  66                } else {
  67                    filter.hide_namespace(ONBOARDING_ACTION_NAMESPACE);
  68                }
  69            });
  70        }
  71    })
  72    .detach();
  73}
  74
  75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
  76pub enum OnboardingPage {
  77    Basics,
  78    Editing,
  79    AiSetup,
  80    Welcome,
  81}
  82
  83impl OnboardingPage {
  84    fn next(&self) -> Option<Self> {
  85        match self {
  86            Self::Basics => Some(Self::Editing),
  87            Self::Editing => Some(Self::AiSetup),
  88            Self::AiSetup => Some(Self::Welcome),
  89            Self::Welcome => None,
  90        }
  91    }
  92
  93    fn previous(&self) -> Option<Self> {
  94        match self {
  95            Self::Basics => None,
  96            Self::Editing => Some(Self::Basics),
  97            Self::AiSetup => Some(Self::Editing),
  98            Self::Welcome => Some(Self::AiSetup),
  99        }
 100    }
 101}
 102
 103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 104pub enum NavigationFocusItem {
 105    SignIn,
 106    Basics,
 107    Editing,
 108    AiSetup,
 109    Welcome,
 110    Next,
 111}
 112
 113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 114pub struct PageFocusItem(pub usize);
 115
 116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 117pub enum FocusArea {
 118    Navigation,
 119    PageContent,
 120}
 121
 122pub struct OnboardingUI {
 123    focus_handle: FocusHandle,
 124    current_page: OnboardingPage,
 125    nav_focus: NavigationFocusItem,
 126    page_focus: [PageFocusItem; 4],
 127    completed_pages: [bool; 4],
 128    focus_area: FocusArea,
 129
 130    // Workspace reference for Item trait
 131    workspace: WeakEntity<Workspace>,
 132    workspace_id: Option<WorkspaceId>,
 133    client: Arc<Client>,
 134}
 135
 136impl EventEmitter<ItemEvent> for OnboardingUI {}
 137
 138impl Focusable for OnboardingUI {
 139    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
 140        self.focus_handle.clone()
 141    }
 142}
 143
 144#[derive(Clone)]
 145pub enum OnboardingEvent {
 146    PageCompleted(OnboardingPage),
 147}
 148
 149impl Render for OnboardingUI {
 150    fn render(
 151        &mut self,
 152        window: &mut gpui::Window,
 153        cx: &mut Context<Self>,
 154    ) -> impl gpui::IntoElement {
 155        div()
 156            .bg(cx.theme().colors().editor_background)
 157            .size_full()
 158            .key_context("OnboardingUI")
 159            .on_action(cx.listener(Self::select_next))
 160            .on_action(cx.listener(Self::select_previous))
 161            .on_action(cx.listener(Self::confirm))
 162            .on_action(cx.listener(Self::cancel))
 163            .on_action(cx.listener(Self::toggle_focus))
 164            .flex()
 165            .items_center()
 166            .justify_center()
 167            .overflow_hidden()
 168            .child(
 169                h_flex()
 170                    .id("onboarding-ui")
 171                    .key_context("Onboarding")
 172                    .track_focus(&self.focus_handle)
 173                    .on_action(cx.listener(Self::handle_jump_to_basics))
 174                    .on_action(cx.listener(Self::handle_jump_to_editing))
 175                    .on_action(cx.listener(Self::handle_jump_to_ai_setup))
 176                    .on_action(cx.listener(Self::handle_jump_to_welcome))
 177                    .on_action(cx.listener(Self::handle_next_page))
 178                    .on_action(cx.listener(Self::handle_previous_page))
 179                    .w(px(904.))
 180                    .gap(px(24.))
 181                    .child(
 182                        h_flex()
 183                            .h(px(500.))
 184                            .w_full()
 185                            .gap(px(48.))
 186                            .child(self.render_navigation(window, cx))
 187                            .child(
 188                                v_flex()
 189                                    .h_full()
 190                                    .flex_1()
 191                                    .when(self.focus_area == FocusArea::PageContent, |this| {
 192                                        this.border_2()
 193                                            .border_color(cx.theme().colors().border_focused)
 194                                    })
 195                                    .rounded_lg()
 196                                    .p_4()
 197                                    .child(
 198                                        div().flex_1().child(self.render_active_page(window, cx)),
 199                                    ),
 200                            ),
 201                    ),
 202            )
 203    }
 204}
 205
 206impl OnboardingUI {
 207    pub fn new(workspace: &Workspace, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
 208        Self {
 209            focus_handle: cx.focus_handle(),
 210            current_page: OnboardingPage::Basics,
 211            nav_focus: NavigationFocusItem::Basics,
 212            page_focus: [PageFocusItem(0); 4],
 213            completed_pages: [false; 4],
 214            focus_area: FocusArea::Navigation,
 215            workspace: workspace.weak_handle(),
 216            workspace_id: workspace.database_id(),
 217            client,
 218        }
 219    }
 220
 221    fn completed_pages_to_string(&self) -> String {
 222        self.completed_pages
 223            .iter()
 224            .map(|&completed| if completed { '1' } else { '0' })
 225            .collect()
 226    }
 227
 228    fn completed_pages_from_string(s: &str) -> [bool; 4] {
 229        let mut result = [false; 4];
 230        for (i, ch) in s.chars().take(4).enumerate() {
 231            result[i] = ch == '1';
 232        }
 233        result
 234    }
 235
 236    fn jump_to_page(
 237        &mut self,
 238        page: OnboardingPage,
 239        _window: &mut gpui::Window,
 240        cx: &mut Context<Self>,
 241    ) {
 242        self.current_page = page;
 243        cx.emit(ItemEvent::UpdateTab);
 244        cx.notify();
 245    }
 246
 247    fn next_page(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) {
 248        if let Some(next) = self.current_page.next() {
 249            self.current_page = next;
 250            cx.notify();
 251        }
 252    }
 253
 254    fn previous_page(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) {
 255        if let Some(prev) = self.current_page.previous() {
 256            self.current_page = prev;
 257            cx.notify();
 258        }
 259    }
 260
 261    fn reset(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) {
 262        self.current_page = OnboardingPage::Basics;
 263        self.focus_area = FocusArea::Navigation;
 264        self.completed_pages = [false; 4];
 265        cx.notify();
 266    }
 267
 268    fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
 269        match self.focus_area {
 270            FocusArea::Navigation => {
 271                self.nav_focus = match self.nav_focus {
 272                    NavigationFocusItem::SignIn => NavigationFocusItem::Basics,
 273                    NavigationFocusItem::Basics => NavigationFocusItem::Editing,
 274                    NavigationFocusItem::Editing => NavigationFocusItem::AiSetup,
 275                    NavigationFocusItem::AiSetup => NavigationFocusItem::Welcome,
 276                    NavigationFocusItem::Welcome => NavigationFocusItem::Next,
 277                    NavigationFocusItem::Next => NavigationFocusItem::SignIn,
 278                };
 279            }
 280            FocusArea::PageContent => {
 281                let page_index = match self.current_page {
 282                    OnboardingPage::Basics => 0,
 283                    OnboardingPage::Editing => 1,
 284                    OnboardingPage::AiSetup => 2,
 285                    OnboardingPage::Welcome => 3,
 286                };
 287                // Bounds checking for page items
 288                let max_items = match self.current_page {
 289                    OnboardingPage::Basics => 3,  // 3 buttons
 290                    OnboardingPage::Editing => 3, // 3 buttons
 291                    OnboardingPage::AiSetup => 2, // Will have 2 items
 292                    OnboardingPage::Welcome => 1, // Will have 1 item
 293                };
 294
 295                if self.page_focus[page_index].0 < max_items - 1 {
 296                    self.page_focus[page_index].0 += 1;
 297                } else {
 298                    // Wrap to start
 299                    self.page_focus[page_index].0 = 0;
 300                }
 301            }
 302        }
 303        cx.notify();
 304    }
 305
 306    fn select_previous(
 307        &mut self,
 308        _: &menu::SelectPrevious,
 309        _window: &mut Window,
 310        cx: &mut Context<Self>,
 311    ) {
 312        match self.focus_area {
 313            FocusArea::Navigation => {
 314                self.nav_focus = match self.nav_focus {
 315                    NavigationFocusItem::SignIn => NavigationFocusItem::Next,
 316                    NavigationFocusItem::Basics => NavigationFocusItem::SignIn,
 317                    NavigationFocusItem::Editing => NavigationFocusItem::Basics,
 318                    NavigationFocusItem::AiSetup => NavigationFocusItem::Editing,
 319                    NavigationFocusItem::Welcome => NavigationFocusItem::AiSetup,
 320                    NavigationFocusItem::Next => NavigationFocusItem::Welcome,
 321                };
 322            }
 323            FocusArea::PageContent => {
 324                let page_index = match self.current_page {
 325                    OnboardingPage::Basics => 0,
 326                    OnboardingPage::Editing => 1,
 327                    OnboardingPage::AiSetup => 2,
 328                    OnboardingPage::Welcome => 3,
 329                };
 330                // Bounds checking for page items
 331                let max_items = match self.current_page {
 332                    OnboardingPage::Basics => 3,  // 3 buttons
 333                    OnboardingPage::Editing => 3, // 3 buttons
 334                    OnboardingPage::AiSetup => 2, // Will have 2 items
 335                    OnboardingPage::Welcome => 1, // Will have 1 item
 336                };
 337
 338                if self.page_focus[page_index].0 > 0 {
 339                    self.page_focus[page_index].0 -= 1;
 340                } else {
 341                    // Wrap to end
 342                    self.page_focus[page_index].0 = max_items - 1;
 343                }
 344            }
 345        }
 346        cx.notify();
 347    }
 348
 349    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 350        match self.focus_area {
 351            FocusArea::Navigation => {
 352                match self.nav_focus {
 353                    NavigationFocusItem::SignIn => {
 354                        // Handle sign in action
 355                        // TODO: Implement sign in action
 356                    }
 357                    NavigationFocusItem::Basics => {
 358                        self.jump_to_page(OnboardingPage::Basics, window, cx)
 359                    }
 360                    NavigationFocusItem::Editing => {
 361                        self.jump_to_page(OnboardingPage::Editing, window, cx)
 362                    }
 363                    NavigationFocusItem::AiSetup => {
 364                        self.jump_to_page(OnboardingPage::AiSetup, window, cx)
 365                    }
 366                    NavigationFocusItem::Welcome => {
 367                        self.jump_to_page(OnboardingPage::Welcome, window, cx)
 368                    }
 369                    NavigationFocusItem::Next => {
 370                        // Handle next button action
 371                        self.next_page(window, cx);
 372                    }
 373                }
 374                // After confirming navigation item (except Next), switch focus to page content
 375                if self.nav_focus != NavigationFocusItem::Next {
 376                    self.focus_area = FocusArea::PageContent;
 377                }
 378            }
 379            FocusArea::PageContent => {
 380                // Handle page-specific item selection
 381                let page_index = match self.current_page {
 382                    OnboardingPage::Basics => 0,
 383                    OnboardingPage::Editing => 1,
 384                    OnboardingPage::AiSetup => 2,
 385                    OnboardingPage::Welcome => 3,
 386                };
 387                let item_index = self.page_focus[page_index].0;
 388
 389                // Trigger the action for the focused item
 390                match self.current_page {
 391                    OnboardingPage::Basics => {
 392                        match item_index {
 393                            0 => {
 394                                // Open file action
 395                                cx.notify();
 396                            }
 397                            1 => {
 398                                // Create project action
 399                                cx.notify();
 400                            }
 401                            2 => {
 402                                // Explore UI action
 403                                cx.notify();
 404                            }
 405                            _ => {}
 406                        }
 407                    }
 408                    OnboardingPage::Editing => {
 409                        // Similar handling for editing page
 410                        cx.notify();
 411                    }
 412                    _ => {
 413                        cx.notify();
 414                    }
 415                }
 416            }
 417        }
 418        cx.notify();
 419    }
 420
 421    fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
 422        match self.focus_area {
 423            FocusArea::PageContent => {
 424                // Switch focus back to navigation
 425                self.focus_area = FocusArea::Navigation;
 426            }
 427            FocusArea::Navigation => {
 428                // If already in navigation, maybe close the onboarding?
 429                // For now, just stay in navigation
 430            }
 431        }
 432        cx.notify();
 433    }
 434
 435    fn toggle_focus(&mut self, _: &ToggleFocus, _window: &mut Window, cx: &mut Context<Self>) {
 436        self.focus_area = match self.focus_area {
 437            FocusArea::Navigation => FocusArea::PageContent,
 438            FocusArea::PageContent => FocusArea::Navigation,
 439        };
 440        cx.notify();
 441    }
 442
 443    fn mark_page_completed(
 444        &mut self,
 445        page: OnboardingPage,
 446        _window: &mut gpui::Window,
 447        cx: &mut Context<Self>,
 448    ) {
 449        let index = match page {
 450            OnboardingPage::Basics => 0,
 451            OnboardingPage::Editing => 1,
 452            OnboardingPage::AiSetup => 2,
 453            OnboardingPage::Welcome => 3,
 454        };
 455        self.completed_pages[index] = true;
 456        cx.notify();
 457    }
 458
 459    fn handle_jump_to_basics(
 460        &mut self,
 461        _: &JumpToBasics,
 462        window: &mut Window,
 463        cx: &mut Context<Self>,
 464    ) {
 465        self.jump_to_page(OnboardingPage::Basics, window, cx);
 466    }
 467
 468    fn handle_jump_to_editing(
 469        &mut self,
 470        _: &JumpToEditing,
 471        window: &mut Window,
 472        cx: &mut Context<Self>,
 473    ) {
 474        self.jump_to_page(OnboardingPage::Editing, window, cx);
 475    }
 476
 477    fn handle_jump_to_ai_setup(
 478        &mut self,
 479        _: &JumpToAiSetup,
 480        window: &mut Window,
 481        cx: &mut Context<Self>,
 482    ) {
 483        self.jump_to_page(OnboardingPage::AiSetup, window, cx);
 484    }
 485
 486    fn handle_jump_to_welcome(
 487        &mut self,
 488        _: &JumpToWelcome,
 489        window: &mut Window,
 490        cx: &mut Context<Self>,
 491    ) {
 492        self.jump_to_page(OnboardingPage::Welcome, window, cx);
 493    }
 494
 495    fn handle_next_page(&mut self, _: &NextPage, window: &mut Window, cx: &mut Context<Self>) {
 496        self.next_page(window, cx);
 497    }
 498
 499    fn handle_previous_page(
 500        &mut self,
 501        _: &PreviousPage,
 502        window: &mut Window,
 503        cx: &mut Context<Self>,
 504    ) {
 505        self.previous_page(window, cx);
 506    }
 507
 508    fn render_navigation(
 509        &mut self,
 510        window: &mut Window,
 511        cx: &mut Context<Self>,
 512    ) -> impl gpui::IntoElement {
 513        let client = self.client.clone();
 514
 515        v_flex()
 516            .h_full()
 517            .w(px(256.))
 518            .gap_2()
 519            .justify_between()
 520            .child(
 521                v_flex()
 522                    .w_full()
 523                    .gap_px()
 524                    .child(
 525                        h_flex()
 526                            .w_full()
 527                            .justify_between()
 528                            .py(px(24.))
 529                            .pl(px(24.))
 530                            .pr(px(12.))
 531                            .child(
 532                                Vector::new(VectorName::ZedLogo, rems(2.), rems(2.))
 533                                    .color(Color::Custom(cx.theme().colors().icon.opacity(0.5))),
 534                            )
 535                            .child(
 536                                Button::new("sign_in", "Sign in")
 537                                    .color(Color::Muted)
 538                                    .label_size(LabelSize::Small)
 539                                    .when(
 540                                        self.focus_area == FocusArea::Navigation
 541                                            && self.nav_focus == NavigationFocusItem::SignIn,
 542                                        |this| this.color(Color::Accent),
 543                                    )
 544                                    .size(ButtonSize::Compact)
 545                                    .on_click(cx.listener(move |_, _, window, cx| {
 546                                        let client = client.clone();
 547                                        window
 548                                            .spawn(cx, async move |cx| {
 549                                                client
 550                                                    .authenticate_and_connect(true, &cx)
 551                                                    .await
 552                                                    .into_response()
 553                                                    .notify_async_err(cx);
 554                                            })
 555                                            .detach();
 556                                    })),
 557                            ),
 558                    )
 559                    .child(
 560                        v_flex()
 561                            .gap_px()
 562                            .py(px(16.))
 563                            .gap(px(12.))
 564                            .child(self.render_nav_item(
 565                                OnboardingPage::Basics,
 566                                "The Basics",
 567                                "1",
 568                                cx,
 569                            ))
 570                            .child(self.render_nav_item(
 571                                OnboardingPage::Editing,
 572                                "Editing Experience",
 573                                "2",
 574                                cx,
 575                            ))
 576                            .child(self.render_nav_item(
 577                                OnboardingPage::AiSetup,
 578                                "AI Setup",
 579                                "3",
 580                                cx,
 581                            ))
 582                            .child(self.render_nav_item(
 583                                OnboardingPage::Welcome,
 584                                "Welcome",
 585                                "4",
 586                                cx,
 587                            )),
 588                    ),
 589            )
 590            .child(self.render_bottom_controls(window, cx))
 591    }
 592
 593    fn render_nav_item(
 594        &mut self,
 595        page: OnboardingPage,
 596        label: impl Into<SharedString>,
 597        shortcut: impl Into<SharedString>,
 598        cx: &mut Context<Self>,
 599    ) -> impl gpui::IntoElement {
 600        let selected = self.current_page == page;
 601        let label = label.into();
 602        let shortcut = shortcut.into();
 603        let id = ElementId::Name(label.clone());
 604
 605        let is_focused = match page {
 606            OnboardingPage::Basics => self.nav_focus == NavigationFocusItem::Basics,
 607            OnboardingPage::Editing => self.nav_focus == NavigationFocusItem::Editing,
 608            OnboardingPage::AiSetup => self.nav_focus == NavigationFocusItem::AiSetup,
 609            OnboardingPage::Welcome => self.nav_focus == NavigationFocusItem::Welcome,
 610        };
 611
 612        let area_focused = self.focus_area == FocusArea::Navigation;
 613
 614        h_flex()
 615            .id(id)
 616            .h(rems(1.5))
 617            .w_full()
 618            .when(is_focused, |this| {
 619                this.bg(if area_focused {
 620                    cx.theme().colors().border_focused.opacity(0.16)
 621                } else {
 622                    cx.theme().colors().border.opacity(0.24)
 623                })
 624            })
 625            .child(
 626                div()
 627                    .w(px(3.))
 628                    .h_full()
 629                    .when(selected, |this| this.bg(cx.theme().colors().border_focused)),
 630            )
 631            .child(
 632                h_flex()
 633                    .pl(px(23.))
 634                    .flex_1()
 635                    .justify_between()
 636                    .items_center()
 637                    .child(Label::new(label).when(is_focused, |this| this.color(Color::Default)))
 638                    .child(Label::new(format!("{}", shortcut.clone())).color(Color::Muted)),
 639            )
 640            .on_click(cx.listener(move |this, _, window, cx| {
 641                this.jump_to_page(page, window, cx);
 642            }))
 643    }
 644
 645    fn render_bottom_controls(
 646        &mut self,
 647        window: &mut gpui::Window,
 648        cx: &mut Context<Self>,
 649    ) -> impl gpui::IntoElement {
 650        h_flex().w_full().p(px(12.)).pl(px(24.)).child(
 651            Button::new(
 652                "next",
 653                if self.current_page == OnboardingPage::Welcome {
 654                    "Get Started"
 655                } else {
 656                    "Next"
 657                },
 658            )
 659            .style(ButtonStyle::Filled)
 660            .when(
 661                self.focus_area == FocusArea::Navigation
 662                    && self.nav_focus == NavigationFocusItem::Next,
 663                |this| this.color(Color::Accent),
 664            )
 665            .key_binding(ui::KeyBinding::for_action_in(
 666                &NextPage,
 667                &self.focus_handle,
 668                window,
 669                cx,
 670            ))
 671            .on_click(cx.listener(|this, _, window, cx| {
 672                this.next_page(window, cx);
 673            })),
 674        )
 675    }
 676
 677    fn render_active_page(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 678        match self.current_page {
 679            OnboardingPage::Basics => self.render_basics_page(cx),
 680            OnboardingPage::Editing => self.render_editing_page(cx),
 681            OnboardingPage::AiSetup => self.render_ai_setup_page(cx),
 682            OnboardingPage::Welcome => self.render_welcome_page(cx),
 683        }
 684    }
 685
 686    fn render_basics_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
 687        let page_index = 0; // Basics page index
 688        let focused_item = self.page_focus[page_index].0;
 689        let is_page_focused = self.focus_area == FocusArea::PageContent;
 690
 691        v_flex()
 692            .h_full()
 693            .w_full()
 694            .items_center()
 695            .justify_center()
 696            .gap_4()
 697            .child(
 698                Label::new("Welcome to Zed!")
 699                    .size(LabelSize::Large)
 700                    .color(Color::Default),
 701            )
 702            .child(
 703                Label::new("Let's get you started with the basics")
 704                    .size(LabelSize::Default)
 705                    .color(Color::Muted),
 706            )
 707            .child(
 708                v_flex()
 709                    .gap_2()
 710                    .mt_4()
 711                    .child(
 712                        Button::new("open_file", "Open a File")
 713                            .style(ButtonStyle::Filled)
 714                            .when(is_page_focused && focused_item == 0, |this| {
 715                                this.color(Color::Accent)
 716                            })
 717                            .on_click(cx.listener(|_, _, _, cx| {
 718                                // TODO: Trigger open file action
 719                                cx.notify();
 720                            })),
 721                    )
 722                    .child(
 723                        Button::new("create_project", "Create a Project")
 724                            .style(ButtonStyle::Filled)
 725                            .when(is_page_focused && focused_item == 1, |this| {
 726                                this.color(Color::Accent)
 727                            })
 728                            .on_click(cx.listener(|_, _, _, cx| {
 729                                // TODO: Trigger create project action
 730                                cx.notify();
 731                            })),
 732                    )
 733                    .child(
 734                        Button::new("explore_ui", "Explore the UI")
 735                            .style(ButtonStyle::Filled)
 736                            .when(is_page_focused && focused_item == 2, |this| {
 737                                this.color(Color::Accent)
 738                            })
 739                            .on_click(cx.listener(|_, _, _, cx| {
 740                                // TODO: Trigger explore UI action
 741                                cx.notify();
 742                            })),
 743                    ),
 744            )
 745            .into_any_element()
 746    }
 747
 748    fn render_editing_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
 749        let page_index = 1; // Editing page index
 750        let focused_item = self.page_focus[page_index].0;
 751        let is_page_focused = self.focus_area == FocusArea::PageContent;
 752
 753        v_flex()
 754            .h_full()
 755            .w_full()
 756            .items_center()
 757            .justify_center()
 758            .gap_4()
 759            .child(
 760                Label::new("Editing Features")
 761                    .size(LabelSize::Large)
 762                    .color(Color::Default),
 763            )
 764            .child(
 765                v_flex()
 766                    .gap_2()
 767                    .mt_4()
 768                    .child(
 769                        Button::new("try_multi_cursor", "Try Multi-cursor Editing")
 770                            .style(ButtonStyle::Filled)
 771                            .when(is_page_focused && focused_item == 0, |this| {
 772                                this.color(Color::Accent)
 773                            })
 774                            .on_click(cx.listener(|_, _, _, cx| {
 775                                cx.notify();
 776                            })),
 777                    )
 778                    .child(
 779                        Button::new("learn_shortcuts", "Learn Keyboard Shortcuts")
 780                            .style(ButtonStyle::Filled)
 781                            .when(is_page_focused && focused_item == 1, |this| {
 782                                this.color(Color::Accent)
 783                            })
 784                            .on_click(cx.listener(|_, _, _, cx| {
 785                                cx.notify();
 786                            })),
 787                    )
 788                    .child(
 789                        Button::new("explore_actions", "Explore Command Palette")
 790                            .style(ButtonStyle::Filled)
 791                            .when(is_page_focused && focused_item == 2, |this| {
 792                                this.color(Color::Accent)
 793                            })
 794                            .on_click(cx.listener(|_, _, _, cx| {
 795                                cx.notify();
 796                            })),
 797                    ),
 798            )
 799            .into_any_element()
 800    }
 801
 802    fn render_ai_setup_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
 803        let page_index = 2; // AI Setup page index
 804        let focused_item = self.page_focus[page_index].0;
 805        let is_page_focused = self.focus_area == FocusArea::PageContent;
 806
 807        v_flex()
 808            .h_full()
 809            .w_full()
 810            .items_center()
 811            .justify_center()
 812            .gap_4()
 813            .child(
 814                Label::new("AI Assistant Setup")
 815                    .size(LabelSize::Large)
 816                    .color(Color::Default),
 817            )
 818            .child(
 819                v_flex()
 820                    .gap_2()
 821                    .mt_4()
 822                    .child(
 823                        Button::new("configure_ai", "Configure AI Provider")
 824                            .style(ButtonStyle::Filled)
 825                            .when(is_page_focused && focused_item == 0, |this| {
 826                                this.color(Color::Accent)
 827                            })
 828                            .on_click(cx.listener(|_, _, _, cx| {
 829                                cx.notify();
 830                            })),
 831                    )
 832                    .child(
 833                        Button::new("try_ai_chat", "Try AI Chat")
 834                            .style(ButtonStyle::Filled)
 835                            .when(is_page_focused && focused_item == 1, |this| {
 836                                this.color(Color::Accent)
 837                            })
 838                            .on_click(cx.listener(|_, _, _, cx| {
 839                                cx.notify();
 840                            })),
 841                    ),
 842            )
 843            .into_any_element()
 844    }
 845
 846    fn render_welcome_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
 847        let page_index = 3; // Welcome page index
 848        let focused_item = self.page_focus[page_index].0;
 849        let is_page_focused = self.focus_area == FocusArea::PageContent;
 850
 851        v_flex()
 852            .h_full()
 853            .w_full()
 854            .items_center()
 855            .justify_center()
 856            .gap_4()
 857            .child(
 858                Label::new("Welcome to Zed!")
 859                    .size(LabelSize::Large)
 860                    .color(Color::Default),
 861            )
 862            .child(
 863                Label::new("You're all set up and ready to code")
 864                    .size(LabelSize::Default)
 865                    .color(Color::Muted),
 866            )
 867            .child(
 868                Button::new("finish_onboarding", "Start Coding!")
 869                    .style(ButtonStyle::Filled)
 870                    .size(ButtonSize::Large)
 871                    .when(is_page_focused && focused_item == 0, |this| {
 872                        this.color(Color::Accent)
 873                    })
 874                    .on_click(cx.listener(|_, _, _, cx| {
 875                        // TODO: Close onboarding and start coding
 876                        cx.notify();
 877                    })),
 878            )
 879            .into_any_element()
 880    }
 881
 882    fn render_keyboard_help(&self, cx: &mut Context<Self>) -> AnyElement {
 883        let help_text = match self.focus_area {
 884            FocusArea::Navigation => {
 885                "Use ↑/↓ to navigate • Enter to select page • Tab to switch to page content"
 886            }
 887            FocusArea::PageContent => {
 888                "Use ↑/↓ to navigate • Enter to activate • Esc to return to navigation"
 889            }
 890        };
 891
 892        h_flex()
 893            .w_full()
 894            .justify_center()
 895            .p_2()
 896            .child(
 897                Label::new(help_text)
 898                    .size(LabelSize::Small)
 899                    .color(Color::Muted),
 900            )
 901            .into_any_element()
 902    }
 903}
 904
 905impl Item for OnboardingUI {
 906    type Event = ItemEvent;
 907
 908    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 909        "Onboarding".into()
 910    }
 911
 912    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
 913        f(event.clone())
 914    }
 915
 916    fn added_to_workspace(
 917        &mut self,
 918        workspace: &mut Workspace,
 919        _window: &mut Window,
 920        _cx: &mut Context<Self>,
 921    ) {
 922        self.workspace_id = workspace.database_id();
 923    }
 924
 925    fn show_toolbar(&self) -> bool {
 926        false
 927    }
 928
 929    fn clone_on_split(
 930        &self,
 931        _workspace_id: Option<WorkspaceId>,
 932        window: &mut Window,
 933        cx: &mut Context<Self>,
 934    ) -> Option<Entity<Self>> {
 935        let weak_workspace = self.workspace.clone();
 936        let client = self.client.clone();
 937        if let Some(workspace) = weak_workspace.upgrade() {
 938            workspace.update(cx, |workspace, cx| {
 939                Some(cx.new(|cx| OnboardingUI::new(workspace, client, cx)))
 940            })
 941        } else {
 942            None
 943        }
 944    }
 945}
 946
 947impl SerializableItem for OnboardingUI {
 948    fn serialized_item_kind() -> &'static str {
 949        "OnboardingUI"
 950    }
 951
 952    fn deserialize(
 953        _project: Entity<Project>,
 954        workspace: WeakEntity<Workspace>,
 955        workspace_id: WorkspaceId,
 956        item_id: u64,
 957        window: &mut Window,
 958        cx: &mut App,
 959    ) -> Task<anyhow::Result<Entity<Self>>> {
 960        window.spawn(cx, async move |cx| {
 961            let (current_page, completed_pages) = if let Some((page_str, completed_str)) =
 962                ONBOARDING_DB.get_state(item_id, workspace_id)?
 963            {
 964                let page = match page_str.as_str() {
 965                    "basics" => OnboardingPage::Basics,
 966                    "editing" => OnboardingPage::Editing,
 967                    "ai_setup" => OnboardingPage::AiSetup,
 968                    "welcome" => OnboardingPage::Welcome,
 969                    _ => OnboardingPage::Basics,
 970                };
 971                let completed = OnboardingUI::completed_pages_from_string(&completed_str);
 972                (page, completed)
 973            } else {
 974                (OnboardingPage::Basics, [false; 4])
 975            };
 976
 977            cx.update(|window, cx| {
 978                let workspace = workspace
 979                    .upgrade()
 980                    .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?;
 981
 982                workspace.update(cx, |workspace, cx| {
 983                    let client = workspace.client().clone();
 984                    Ok(cx.new(|cx| {
 985                        let mut onboarding = OnboardingUI::new(workspace, client, cx);
 986                        onboarding.current_page = current_page;
 987                        onboarding.completed_pages = completed_pages;
 988                        onboarding
 989                    }))
 990                })
 991            })?
 992        })
 993    }
 994
 995    fn serialize(
 996        &mut self,
 997        _workspace: &mut Workspace,
 998        item_id: u64,
 999        _closing: bool,
1000        _window: &mut Window,
1001        cx: &mut Context<Self>,
1002    ) -> Option<Task<anyhow::Result<()>>> {
1003        let workspace_id = self.workspace_id?;
1004        let current_page = match self.current_page {
1005            OnboardingPage::Basics => "basics",
1006            OnboardingPage::Editing => "editing",
1007            OnboardingPage::AiSetup => "ai_setup",
1008            OnboardingPage::Welcome => "welcome",
1009        }
1010        .to_string();
1011        let completed_pages = self.completed_pages_to_string();
1012
1013        Some(cx.background_spawn(async move {
1014            ONBOARDING_DB
1015                .save_state(item_id, workspace_id, current_page, completed_pages)
1016                .await
1017        }))
1018    }
1019
1020    fn cleanup(
1021        _workspace_id: WorkspaceId,
1022        _item_ids: Vec<u64>,
1023        _window: &mut Window,
1024        _cx: &mut App,
1025    ) -> Task<anyhow::Result<()>> {
1026        Task::ready(Ok(()))
1027    }
1028
1029    fn should_serialize(&self, _event: &ItemEvent) -> bool {
1030        true
1031    }
1032}