{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "virtual-list",
  "type": "registry:item",
  "title": "Virtual List",
  "description": "Virtualized native rows with stable identity, selection, keyboard navigation and fixed row heights.",
  "files": [
    {
      "path": "registry/theme/primitives.rs",
      "type": "registry:file",
      "target": "~/src/ui/primitives.rs",
      "content": "//! Shared visual recipes between the theme tokens and the components.\n//!\n//! shadcn expresses these as Tailwind class strings; here each one is a function\n//! that applies the same styles to any GPUI element, so every component draws\n//! surfaces, controls, focus, text and images the same way, and a change to a\n//! recipe reaches the whole catalog. The registry lint fails components that\n//! rewrite one of these by hand. Everything takes `&UiTheme` so it works in\n//! `RenderOnce` and `Render` alike without re-reading the global.\n\nuse gpui_icons::{LucideIcon, lucide};\nuse gpui_kit::{\n    AnyElement, Axis, BoxShadow, Div, FontWeight, ImageSource, Img, InteractiveElement,\n    IntoElement, MouseButton, ObjectFit, ParentElement, Pixels, Rgba, SharedString,\n    StatefulInteractiveElement, StyleRefinement, Styled, StyledImage as _, Window, div, img, px,\n};\n\nuse super::{UiTextStyle, UiTheme};\n\n/// Text sizes from the type scale, each with its line height and the body font.\npub mod text {\n    use super::*;\n\n    /// Applies one step of the type scale and the body font.\n    pub fn style<T: Styled>(element: T, theme: &UiTheme, style: UiTextStyle) -> T {\n        let style = theme.text_style(style);\n        element\n            .font_family(theme.fonts.body.clone())\n            .text_size(style.size)\n            .line_height(style.line_height)\n    }\n    /// Captions, key caps and timestamps.\n    pub fn xs<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.xs)\n    }\n    /// Controls and body copy; the Nova default.\n    pub fn sm<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.sm)\n    }\n    /// Prose and dialog titles.\n    pub fn base<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.base)\n    }\n    /// Section headings.\n    pub fn lg<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.lg)\n    }\n    /// Page headings.\n    pub fn xl<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.xl)\n    }\n    /// A medium-weight heading in the heading font at the given step.\n    pub fn heading<T: Styled>(element: T, theme: &UiTheme, size: UiTextStyle) -> T {\n        style(element, theme, size)\n            .font_family(theme.fonts.heading.clone())\n            .font_weight(FontWeight::MEDIUM)\n    }\n    /// Switches to the monospace font, keeping the current size.\n    pub fn mono<T: Styled>(element: T, theme: &UiTheme) -> T {\n        element.font_family(theme.fonts.mono.clone())\n    }\n    /// Muted supporting text at the small size.\n    pub fn muted<T: Styled>(element: T, theme: &UiTheme) -> T {\n        sm(element, theme).text_color(theme.colors.muted_foreground)\n    }\n}\n\n/// Icon sizes in spacing units, so icons scale with control density.\npub mod icon {\n    use super::*;\n\n    /// 12px: inside badges and tags.\n    pub fn xs(theme: &UiTheme) -> Pixels {\n        theme.space(3.)\n    }\n    /// 14px: inside small buttons and chips.\n    pub fn sm(theme: &UiTheme) -> Pixels {\n        theme.space(3.5)\n    }\n    /// 16px: the default inside controls and menu items.\n    pub fn md(theme: &UiTheme) -> Pixels {\n        theme.space(4.)\n    }\n    /// 20px: list leading icons.\n    pub fn lg(theme: &UiTheme) -> Pixels {\n        theme.space(5.)\n    }\n    /// 24px: empty states and headers.\n    pub fn xl(theme: &UiTheme) -> Pixels {\n        theme.space(6.)\n    }\n}\n\n/// Keyboard focus treatment: shadcn Nova's `ring-3 ring-ring/50` outside the\n/// border, with the border taking the ring color. Apply through\n/// `focus_visible` so pointer presses leave it alone.\npub mod focus {\n    use super::*;\n\n    /// The ring as a box shadow, for elements that draw their own border.\n    pub fn shadow(color: Rgba) -> Vec<BoxShadow> {\n        vec![BoxShadow::new(px(0.), px(0.), color.into()).spread_radius(px(3.))]\n    }\n    /// The focus ring for an ordinary control.\n    pub fn ring(style: StyleRefinement, theme: &UiTheme) -> StyleRefinement {\n        let control = theme.control();\n        style\n            .border_color(theme.colors.ring)\n            .shadow(shadow(control.ring))\n    }\n    /// The focus ring for an invalid control, in the destructive color.\n    pub fn invalid_ring(style: StyleRefinement, theme: &UiTheme) -> StyleRefinement {\n        let control = theme.control();\n        style\n            .border_color(theme.colors.destructive)\n            .shadow(shadow(control.invalid_ring))\n    }\n    /// Picks the valid or invalid ring.\n    pub fn ring_for(style: StyleRefinement, theme: &UiTheme, invalid: bool) -> StyleRefinement {\n        if invalid {\n            invalid_ring(style, theme)\n        } else {\n            ring(style, theme)\n        }\n    }\n    /// Draws the ring on `element` while `focused`, for controls whose focus is\n    /// tracked on a parent (a grouped checkbox or radio draws it on its box).\n    pub fn shown<T: Styled>(element: T, theme: &UiTheme, focused: bool) -> T {\n        shown_for(element, theme, focused, false)\n    }\n    /// [`shown`] with the invalid ring when `invalid`.\n    pub fn shown_for<T: Styled>(element: T, theme: &UiTheme, focused: bool, invalid: bool) -> T {\n        if focused {\n            super::super::apply_style(\n                element,\n                &ring_for(StyleRefinement::default(), theme, invalid),\n            )\n        } else {\n            element\n        }\n    }\n}\n\n/// Disabled and interactive cursor treatment.\npub mod disabled {\n    use super::*;\n\n    /// Nova's `disabled:opacity-50` plus the blocked cursor.\n    pub fn style<T: Styled>(element: T) -> T {\n        element.opacity(0.5).cursor_not_allowed()\n    }\n    /// A pointer cursor when enabled, the disabled treatment otherwise.\n    pub fn interactive<T: Styled>(element: T, disabled: bool) -> T {\n        if disabled {\n            style(element)\n        } else {\n            element.cursor_pointer()\n        }\n    }\n    /// Half opacity while `dimmed`, for content that is unavailable rather than\n    /// a control that is disabled: uploading pictures, disabled list rows.\n    pub fn dim<T: Styled>(element: T, dimmed: bool) -> T {\n        if dimmed {\n            element.opacity(0.5)\n        } else {\n            element\n        }\n    }\n}\n\n/// Elevated surfaces. One rule per kind, so every popover, menu, dialog, card\n/// and tooltip in the catalog shares border, radius, shadow and text defaults.\npub mod surface {\n    use super::*;\n\n    fn popover_base<T: Styled>(element: T, theme: &UiTheme) -> T {\n        text::sm(element, theme)\n            .border_1()\n            .border_color(theme.control().surface_border)\n            .bg(theme.colors.popover)\n            .text_color(theme.colors.popover_foreground)\n    }\n    /// Popovers, hover cards, select and combobox lists: `rounded-lg shadow-md`.\n    pub fn popover<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme)\n            .rounded(theme.radius.lg)\n            .shadow(theme.shadows.md.to_vec())\n    }\n    /// Menus and context menus: a popover with Nova's 4px inset.\n    pub fn menu<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover(element, theme).p(theme.space(1.))\n    }\n    /// Dialogs and command palettes: `rounded-xl shadow-lg`.\n    pub fn modal<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme)\n            .rounded(theme.radius.xl)\n            .shadow(theme.shadows.lg.to_vec())\n    }\n    /// A sheet or drawer panel: the modal surface without a radius, which the\n    /// caller rounds on the open edge.\n    pub fn sheet<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme).border_color(theme.colors.border)\n    }\n    /// Toasts: `rounded-2xl` on the popover surface with the plain border.\n    pub fn toast<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme)\n            .rounded(theme.radius.two_xl)\n            .border_color(theme.colors.border)\n            .shadow(theme.shadows.lg.to_vec())\n    }\n    /// Cards and empty states: `rounded-xl border shadow-sm` on the card color.\n    pub fn card<T: Styled>(element: T, theme: &UiTheme) -> T {\n        text::sm(element, theme)\n            .rounded(theme.radius.xl)\n            .border_1()\n            .border_color(theme.colors.border)\n            .bg(theme.colors.card)\n            .text_color(theme.colors.card_foreground)\n            .shadow(theme.shadows.sm.to_vec())\n    }\n    /// A floating action bar over content, as on a hovered message.\n    pub fn floating<T: Styled>(element: T, theme: &UiTheme) -> T {\n        element\n            .rounded(theme.radius.lg)\n            .border_1()\n            .border_color(theme.colors.border)\n            .bg(theme.colors.background)\n            .shadow(theme.shadows.sm.to_vec())\n    }\n    /// Tooltips: inverted colors at the small radius and caption size.\n    pub fn tooltip<T: Styled>(element: T, theme: &UiTheme) -> T {\n        text::xs(element, theme)\n            .rounded(theme.radius.sm)\n            .px(theme.space(3.))\n            .py(theme.space(1.5))\n            .bg(theme.colors.foreground)\n            .text_color(theme.colors.background)\n    }\n    /// A one-pixel rule in the border color, across or down.\n    pub fn divider<T: Styled>(element: T, theme: &UiTheme, axis: Axis) -> T {\n        let element = match axis {\n            Axis::Horizontal => element.w_full().h(px(1.)),\n            Axis::Vertical => element.h_full().w(px(1.)),\n        };\n        element.bg(theme.colors.border)\n    }\n}\n\n/// Form control pieces shared by single controls and their grouped variants.\npub mod control {\n    use super::*;\n\n    /// Corner radius of a 16px check box: Nova's `rounded-[4px]` at the default radius.\n    pub fn box_radius(theme: &UiTheme) -> Pixels {\n        theme.radius.sm * (2. / 3.)\n    }\n\n    /// The frame every text-like control shares: inputs, textareas, selects,\n    /// number and OTP cells. Large radius, input border and fill, small text.\n    /// Callers set the size: `h(theme.space(8.))` and `px(theme.space(2.5))`\n    /// for a single-line control.\n    pub fn frame<T: Styled>(element: T, theme: &UiTheme, invalid: bool) -> T {\n        let control = theme.control();\n        text::sm(element, theme)\n            .rounded(theme.radius.lg)\n            .border_1()\n            .border_color(if invalid {\n                theme.colors.destructive\n            } else {\n                control.border\n            })\n            .bg(control.background)\n            .text_color(theme.colors.foreground)\n    }\n\n    /// Styles `element` as the 16px check box with its mark, for checkboxes and\n    /// grouped checkboxes. The element is the interactive root of the control.\n    pub fn checkbox_box<T: Styled + ParentElement>(\n        element: T,\n        theme: &UiTheme,\n        checked: bool,\n        indeterminate: bool,\n    ) -> T {\n        let colors = theme.colors;\n        let control = theme.control();\n        let selected = checked || indeterminate;\n        let element = element\n            .flex()\n            .flex_shrink_0()\n            .items_center()\n            .justify_center()\n            .size(theme.space(4.))\n            .rounded(box_radius(theme))\n            .border_1()\n            .border_color(if selected {\n                colors.primary\n            } else {\n                control.border\n            })\n            .bg(if selected {\n                colors.primary\n            } else {\n                control.background\n            });\n        if selected {\n            element.child(\n                lucide(if indeterminate {\n                    LucideIcon::Minus\n                } else {\n                    LucideIcon::Check\n                })\n                .size(icon::sm(theme))\n                .text_color(colors.primary_foreground),\n            )\n        } else {\n            element\n        }\n    }\n\n    /// Styles `element` as the 16px radio circle with its dot.\n    pub fn radio_dot<T: Styled + ParentElement>(element: T, theme: &UiTheme, checked: bool) -> T {\n        let colors = theme.colors;\n        let control = theme.control();\n        let element = element\n            .flex()\n            .flex_shrink_0()\n            .items_center()\n            .justify_center()\n            .size(theme.space(4.))\n            .rounded_full()\n            .border_1()\n            .border_color(if checked {\n                colors.primary\n            } else {\n                control.border\n            })\n            .bg(if checked {\n                colors.primary\n            } else {\n                control.background\n            });\n        if checked {\n            element.child(\n                div()\n                    .size(theme.space(2.))\n                    .rounded_full()\n                    .bg(colors.primary_foreground),\n            )\n        } else {\n            element\n        }\n    }\n\n    /// The toggle look shared by Toggle and Toggle Group items: transparent at\n    /// rest, muted when hovered or pressed, an input border when outlined.\n    pub fn toggle<T: Styled>(\n        element: T,\n        theme: &UiTheme,\n        pressed: bool,\n        disabled: bool,\n        outline: bool,\n    ) -> T {\n        let colors = theme.colors;\n        let control = theme.control();\n        let element = text::sm(element, theme)\n            .border_1()\n            .border_color(if outline {\n                control.border\n            } else {\n                colors.background.opacity(0.)\n            })\n            .text_color(colors.foreground)\n            .bg(if pressed {\n                control.pressed\n            } else {\n                colors.background.opacity(0.)\n            });\n        disabled::interactive(element, disabled)\n    }\n}\n\n/// The switch track and thumb colors for a checked or unchecked switch.\npub fn switch_colors(theme: &UiTheme, checked: bool) -> (Rgba, Rgba) {\n    let control = theme.control();\n    if checked {\n        (theme.colors.primary, control.thumb_checked)\n    } else {\n        (control.track, control.thumb)\n    }\n}\n\n/// An image inside a rounded frame. GPUI clips children to rectangles, so the\n/// frame rounds itself and the image rounds to the frame's inner radius; the\n/// image fills the frame's box and never sizes the frame from its aspect ratio.\npub fn clipped_image(\n    source: impl Into<ImageSource>,\n    radius: Pixels,\n    fit: ObjectFit,\n    theme: &UiTheme,\n) -> Div {\n    clipped_image_with(source, radius, fit, theme, |image| image)\n}\n\n/// [`clipped_image`] with access to the picture, for loading and fallback views.\npub fn clipped_image_with(\n    source: impl Into<ImageSource>,\n    radius: Pixels,\n    fit: ObjectFit,\n    theme: &UiTheme,\n    configure: impl FnOnce(Img) -> Img,\n) -> Div {\n    div()\n        .relative()\n        .flex_shrink_0()\n        .overflow_hidden()\n        .rounded(radius)\n        .bg(theme.colors.muted)\n        .child(configure(\n            img(source)\n                .absolute()\n                .inset_0()\n                .size_full()\n                .rounded(radius)\n                .object_fit(fit),\n        ))\n}\n\n/// A circular image, as in avatars.\npub fn round_image(source: impl Into<ImageSource>, size: Pixels, theme: &UiTheme) -> Div {\n    clipped_image(source, size / 2., ObjectFit::Cover, theme).size(size)\n}\n\n/// Dismisses an overlay from a press outside it or the Escape key.\npub mod dismiss {\n    use super::*;\n\n    /// Runs `handler` when the pointer presses outside `element` or Escape is\n    /// pressed while focus is inside it.\n    pub fn on_outside<E: StatefulInteractiveElement>(\n        element: E,\n        handler: impl Fn(&mut Window, &mut gpui_kit::App) + 'static,\n    ) -> E {\n        let handler = std::rc::Rc::new(handler);\n        let on_press = handler.clone();\n        element\n            .on_mouse_down_out(move |event, window, cx| {\n                if event.button == MouseButton::Left {\n                    on_press(window, cx)\n                }\n            })\n            .on_key_down(move |event, window, cx| {\n                if event.keystroke.key == \"escape\" {\n                    handler(window, cx);\n                    cx.stop_propagation();\n                }\n            })\n    }\n}\n\n/// Hover-revealed content that never changes layout: the element keeps its\n/// size and only its opacity follows `visible`.\npub fn hover_reveal<T: Styled>(element: T, visible: bool) -> T {\n    element.opacity(if visible { 1. } else { 0. })\n}\n\n/// Content shown while the pointer is over the enclosing `.group(name)`\n/// element, with no state and no re-render: opacity 0 at rest, 1 on hover.\n/// Pass `pinned` to keep it visible, as for a selected row's actions.\npub fn hover_reveal_in_group<T: Styled + InteractiveElement>(\n    element: T,\n    group: impl Into<SharedString>,\n    pinned: bool,\n) -> T {\n    if pinned {\n        element\n    } else {\n        element\n            .opacity(0.)\n            .group_hover(group, |style| style.opacity(1.))\n    }\n}\n\n/// Erases an element's type after styling, for callers that store children.\npub fn erase(element: impl IntoElement) -> AnyElement {\n    element.into_any_element()\n}\n"
    },
    {
      "path": "registry/theme/theme.rs",
      "type": "registry:file",
      "target": "~/src/ui/theme.rs",
      "content": "//! The app-owned shadcn Neutral theme for gpuicn.\n//!\n//! Source: shadcn/ui 4.19.0 at\n//! `1773ecfeeb4a04366978d353e69b5c7ded78dcb2`, Nova style.\n\nuse std::{sync::Arc, time::Duration};\n\nuse gpui_kit::{\n    App, BoxShadow, ElementId, Global, Pixels, Rgba, SharedString, Styled, Subscription, Window,\n    WindowAppearance, black, px,\n};\n\n#[path = \"primitives.rs\"]\npub mod primitives;\n\ngpui_kit::actions!(\n    gpuicn,\n    [\n        /// Move to the next keyboard tab stop.\n        FocusNext,\n        /// Move to the previous keyboard tab stop.\n        FocusPrevious\n    ]\n);\n\nstruct Initialized;\nimpl Global for Initialized {}\n\n/// Installs component actions, keyboard traversal and the default theme once.\n/// Call this from application startup for both copied source and crate usage.\npub fn init(cx: &mut App) {\n    if cx.has_global::<Initialized>() {\n        return;\n    }\n    seed_component_fonts(cx);\n    gpui_kit::init(cx);\n    cx.set_global(Initialized);\n    if !cx.has_global::<UiTheme>() {\n        UiTheme::set(cx, UiTheme::neutral_light());\n    } else {\n        let theme = UiTheme::read(cx).clone();\n        sync_component_theme(cx, theme);\n    }\n    cx.bind_keys([\n        gpui_kit::KeyBinding::new(\"tab\", FocusNext, None),\n        gpui_kit::KeyBinding::new(\"shift-tab\", FocusPrevious, None),\n    ]);\n    cx.on_action(|_: &FocusNext, cx| advance_focus(false, cx));\n    cx.on_action(|_: &FocusPrevious, cx| advance_focus(true, cx));\n    // Scoped component bindings take precedence over these window-wide defaults.\n    #[cfg(target_family = \"wasm\")]\n    {\n        // Browser previews also accept macOS editing shortcuts.\n        use gpui_kit::KeyBinding;\n        use gpui_kit::base::input::{\n            Copy, Cut, MoveEnd, MoveHome, MoveToNextWord, MoveToPreviousWord, Paste, Redo,\n            SelectAll, SelectToNextWordEnd, SelectToPreviousWordStart, Undo,\n        };\n        cx.bind_keys([\n            KeyBinding::new(\"cmd-a\", SelectAll, Some(\"Input\")),\n            KeyBinding::new(\"cmd-c\", Copy, Some(\"Input\")),\n            KeyBinding::new(\"cmd-v\", Paste, Some(\"Input\")),\n            KeyBinding::new(\"cmd-x\", Cut, Some(\"Input\")),\n            KeyBinding::new(\"cmd-left\", MoveHome, Some(\"Input\")),\n            KeyBinding::new(\"cmd-right\", MoveEnd, Some(\"Input\")),\n            KeyBinding::new(\"cmd-z\", Undo, Some(\"Input\")),\n            KeyBinding::new(\"cmd-shift-z\", Redo, Some(\"Input\")),\n            KeyBinding::new(\"alt-left\", MoveToPreviousWord, Some(\"Input\")),\n            KeyBinding::new(\"alt-right\", MoveToNextWord, Some(\"Input\")),\n            KeyBinding::new(\"alt-shift-left\", SelectToPreviousWordStart, Some(\"Input\")),\n            KeyBinding::new(\"alt-shift-right\", SelectToNextWordEnd, Some(\"Input\")),\n        ]);\n    }\n}\n\nfn seed_component_fonts(cx: &mut App) {\n    use gpui_kit::component::Theme;\n\n    if cx.has_global::<Theme>() {\n        return;\n    }\n    let fonts = if cx.has_global::<UiTheme>() {\n        UiTheme::read(cx).fonts.clone()\n    } else {\n        UiTheme::neutral_light().fonts\n    };\n    let mut component = Theme::default();\n    component.font_family = fonts.body;\n    component.mono_font_family = fonts.mono;\n    cx.set_global(component);\n}\n\nfn advance_focus(reverse: bool, cx: &mut App) {\n    let Some(handle) = cx.active_window() else {\n        return;\n    };\n    // Action dispatch still holds the window borrow until this callback returns.\n    cx.defer(move |cx| {\n        let _ = handle.update(cx, |_, window, cx| {\n            if reverse {\n                window.focus_prev(cx);\n            } else {\n                window.focus_next(cx);\n            }\n        });\n    });\n}\n\n/// The active color mode.\n#[derive(Clone, Copy, Debug, Eq, PartialEq)]\npub enum ThemeMode {\n    /// The Neutral light palette.\n    Light,\n    /// The Neutral dark palette.\n    Dark,\n}\n\n/// Which of an application's two palettes is shown.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum Appearance {\n    /// Always the light palette.\n    Light,\n    /// Always the dark palette.\n    Dark,\n    /// The palette matching the operating system's window appearance.\n    #[default]\n    System,\n}\n\nimpl Appearance {\n    /// The mode this appearance shows under the given system appearance.\n    pub fn resolve(self, system: WindowAppearance) -> ThemeMode {\n        match self {\n            Appearance::Light => ThemeMode::Light,\n            Appearance::Dark => ThemeMode::Dark,\n            Appearance::System => match system {\n                WindowAppearance::Dark | WindowAppearance::VibrantDark => ThemeMode::Dark,\n                WindowAppearance::Light | WindowAppearance::VibrantLight => ThemeMode::Light,\n            },\n        }\n    }\n}\n\n/// The application's light and dark themes and which one it shows.\n#[derive(Clone)]\nstruct ThemePair {\n    light: UiTheme,\n    dark: UiTheme,\n    appearance: Appearance,\n}\nimpl Global for ThemePair {}\n\n/// Swaps palettes when the operating system's appearance changes while the\n/// application follows it. Call it for each window the application opens and\n/// keep the subscription; `app::run` does this for the first window.\npub fn follow_system(window: &mut Window, cx: &mut App) -> Subscription {\n    UiTheme::apply_appearance(cx);\n    window.observe_window_appearance(|_, cx| UiTheme::apply_appearance(cx))\n}\n\n/// Semantic colors consumed by installed components.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiColors {\n    /// App background.\n    pub background: Rgba,\n    /// Default foreground.\n    pub foreground: Rgba,\n    /// Card background.\n    pub card: Rgba,\n    /// Card foreground.\n    pub card_foreground: Rgba,\n    /// Popover background.\n    pub popover: Rgba,\n    /// Popover foreground.\n    pub popover_foreground: Rgba,\n    /// Primary control background.\n    pub primary: Rgba,\n    /// Primary control foreground.\n    pub primary_foreground: Rgba,\n    /// Secondary control background.\n    pub secondary: Rgba,\n    /// Secondary control foreground.\n    pub secondary_foreground: Rgba,\n    /// Muted background.\n    pub muted: Rgba,\n    /// Muted foreground.\n    pub muted_foreground: Rgba,\n    /// Accent background.\n    pub accent: Rgba,\n    /// Accent foreground.\n    pub accent_foreground: Rgba,\n    /// Destructive action color.\n    pub destructive: Rgba,\n    /// Text on a solid `destructive` surface.\n    pub destructive_foreground: Rgba,\n    /// Positive status, such as a completed upload or an online presence dot.\n    pub success: Rgba,\n    /// Text on a `success` fill.\n    pub success_foreground: Rgba,\n    /// Cautionary status, such as a permalink highlight or a pending state.\n    pub warning: Rgba,\n    /// Text on a `warning` fill.\n    pub warning_foreground: Rgba,\n    /// Informational status.\n    pub info: Rgba,\n    /// Text on an `info` fill.\n    pub info_foreground: Rgba,\n    /// Default border.\n    pub border: Rgba,\n    /// Input border.\n    pub input: Rgba,\n    /// Focus ring.\n    pub ring: Rgba,\n    /// First chart color.\n    pub chart_1: Rgba,\n    /// Second chart color.\n    pub chart_2: Rgba,\n    /// Third chart color.\n    pub chart_3: Rgba,\n    /// Fourth chart color.\n    pub chart_4: Rgba,\n    /// Fifth chart color.\n    pub chart_5: Rgba,\n    /// Sidebar background.\n    pub sidebar: Rgba,\n    /// Sidebar foreground.\n    pub sidebar_foreground: Rgba,\n    /// Sidebar primary background.\n    pub sidebar_primary: Rgba,\n    /// Sidebar primary foreground.\n    pub sidebar_primary_foreground: Rgba,\n    /// Sidebar accent background.\n    pub sidebar_accent: Rgba,\n    /// Sidebar accent foreground.\n    pub sidebar_accent_foreground: Rgba,\n    /// Sidebar border.\n    pub sidebar_border: Rgba,\n    /// Sidebar focus border color.\n    pub sidebar_ring: Rgba,\n    /// Scrim behind modal surfaces.\n    pub overlay: Rgba,\n}\n\n/// Theme font family names. Applications own font loading.\n#[derive(Clone, Debug, PartialEq)]\npub struct UiFonts {\n    /// Default body family.\n    pub body: SharedString,\n    /// Default heading family.\n    pub heading: SharedString,\n    /// Default monospace family.\n    pub mono: SharedString,\n}\n\n/// Corner radii derived from shadcn's 10px base radius.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiRadius {\n    /// Backward-compatible alias for [`Self::lg`].\n    pub base: Pixels,\n    /// Six pixels (`radius * 0.6`).\n    pub sm: Pixels,\n    /// Eight pixels (`radius * 0.8`).\n    pub md: Pixels,\n    /// Ten pixels (the configured base radius).\n    pub lg: Pixels,\n    /// Fourteen pixels (`radius * 1.4`).\n    pub xl: Pixels,\n    /// Eighteen pixels (`radius * 1.8`).\n    pub two_xl: Pixels,\n    /// Twenty-two pixels (`radius * 2.2`).\n    pub three_xl: Pixels,\n    /// Twenty-six pixels (`radius * 2.6`).\n    pub four_xl: Pixels,\n}\n\nimpl UiRadius {\n    /// Derives the complete Nova radius scale from one nonnegative base radius.\n    pub fn new(base: Pixels) -> Self {\n        assert!(f32::from(base).is_finite() && base >= px(0.));\n        Self {\n            base,\n            sm: base * 0.6,\n            md: base * 0.8,\n            lg: base,\n            xl: base * 1.4,\n            two_xl: base * 1.8,\n            three_xl: base * 2.2,\n            four_xl: base * 2.6,\n        }\n    }\n}\n\n/// Shared UI transition durations. Honor these alongside `App::reduce_motion()`.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiMotion {\n    /// Thumb and small-state transitions (150ms by default).\n    pub fast: Duration,\n    /// Panel/layout transitions (200ms by default).\n    pub normal: Duration,\n    /// Application preference; true removes movement without removing state feedback.\n    pub reduced: bool,\n    /// Shared easing for reversible state transitions.\n    pub easing: UiEasing,\n}\nimpl Default for UiMotion {\n    fn default() -> Self {\n        Self {\n            fast: Duration::from_millis(150),\n            normal: Duration::from_millis(200),\n            reduced: false,\n            easing: UiEasing::EaseInOut,\n        }\n    }\n}\n\n/// Curves supported by the shared native transition helper.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum UiEasing {\n    /// Constant-speed motion, as used by shadcn sidebar layout transitions.\n    Linear,\n    /// Fast response followed by a smooth stop.\n    EaseOut,\n    /// Smooth acceleration and deceleration for changes within a layout.\n    #[default]\n    EaseInOut,\n    /// Quick start and a long settle (`cubic-bezier(0.22, 1, 0.36, 1)`), for\n    /// surfaces that slide in from an edge, such as drawers and sheets.\n    Emphasized,\n}\nimpl UiEasing {\n    fn sample(self, progress: f32) -> f32 {\n        match self {\n            Self::Linear => progress,\n            Self::EaseOut => gpui_kit::ease_out_quint()(progress),\n            Self::EaseInOut => gpui_kit::ease_in_out(progress),\n            Self::Emphasized => gpui_kit::base::motion::Easing::CubicBezier {\n                x1: 0.22,\n                y1: 1.,\n                x2: 0.36,\n                y2: 1.,\n            }\n            .sample(progress),\n        }\n    }\n}\n\n/// Returns a smoothly retargetable value for one stable, caller-named property.\n/// Starts at the target on mount; rapid reversals continue from the current value.\n/// Honors theme and GPUI reduced-motion settings and schedules no idle frames.\n/// Use for state changes, never for pointer-drag coordinates or keyboard navigation.\n/// Native layout transitions perform layout work; retain expensive child entities.\npub fn transition_value(\n    id: impl Into<ElementId>,\n    target: f32,\n    duration: Duration,\n    window: &mut Window,\n    cx: &mut App,\n) -> f32 {\n    let easing = UiTheme::read(cx).motion.easing;\n    transition_with(id, target, duration, easing, window, cx)\n}\n\n/// [`transition_value`] with an explicit curve, for the few surfaces whose motion\n/// differs from the theme default (drawers slide with [`UiEasing::Emphasized`]).\npub fn transition_with(\n    id: impl Into<ElementId>,\n    target: f32,\n    duration: Duration,\n    easing: UiEasing,\n    window: &mut Window,\n    cx: &mut App,\n) -> f32 {\n    assert!(target.is_finite(), \"transition targets must be finite\");\n    gpui_kit::base::motion::transition(\n        id.into(),\n        target,\n        motion_policy(duration, easing, cx),\n        window,\n        cx,\n    )\n}\n\n/// Shares the theme's disclosure and notification timing with Kit's presence lifecycle.\n/// Returns a sample whose `progress` runs 0→1 while entering and 1→0 while leaving, so\n/// a surface fades or scales with it; `should_render()` keeps the element mounted\n/// until the exit transition has finished.\n#[doc(hidden)]\npub fn presence(\n    id: impl Into<ElementId>,\n    present: bool,\n    window: &mut Window,\n    cx: &mut App,\n) -> gpui_kit::base::motion::PresenceSample {\n    let motion = UiTheme::read(cx).motion;\n    presence_with(id, present, motion.fast, motion.easing, window, cx)\n}\n\n/// [`presence`] with explicit timing and curve, for panels that take the theme's\n/// `normal` duration or a different easing.\n#[doc(hidden)]\npub fn presence_with(\n    id: impl Into<ElementId>,\n    present: bool,\n    duration: Duration,\n    easing: UiEasing,\n    window: &mut Window,\n    cx: &mut App,\n) -> gpui_kit::base::motion::PresenceSample {\n    gpui_kit::base::motion::Presence::new(id.into(), present)\n        .transition(motion_policy(duration, easing, cx))\n        .sample(window, cx)\n}\n\n/// The one place motion honors the theme and platform reduced-motion settings.\nfn motion_policy(\n    duration: Duration,\n    easing: UiEasing,\n    cx: &App,\n) -> gpui_kit::base::motion::Transition {\n    let duration = if UiTheme::read(cx).motion.reduced || cx.reduce_motion() {\n        Duration::ZERO\n    } else {\n        duration\n    };\n    gpui_kit::base::motion::Transition::new(duration).ease(move |progress| easing.sample(progress))\n}\n\n/// Shared shadcn elevation tokens. Each level is shared, so cloning a theme\n/// every render costs no allocation; `to_vec()` the level you apply.\n#[derive(Clone, Debug, PartialEq)]\npub struct UiShadows {\n    /// Tailwind `shadow-sm`.\n    pub sm: Arc<[BoxShadow]>,\n    /// Tailwind `shadow-md`.\n    pub md: Arc<[BoxShadow]>,\n    /// Tailwind `shadow-lg`.\n    pub lg: Arc<[BoxShadow]>,\n}\n\n/// One step of the type scale: a text size and the line height that goes with it.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiTextStyle {\n    /// Font size before `text_scale`.\n    pub size: Pixels,\n    /// Line height before `text_scale`.\n    pub line_height: Pixels,\n}\n\n/// The type scale, Tailwind's by default. Components read sizes from here through\n/// `primitives::text`, so `text_scale` and a custom scale reach every label.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiTypography {\n    /// Captions and key caps (12/16).\n    pub xs: UiTextStyle,\n    /// Controls and body copy in dense desktop layouts (14/20); the Nova default.\n    pub sm: UiTextStyle,\n    /// Prose and dialog titles (16/24).\n    pub base: UiTextStyle,\n    /// Section headings (18/28).\n    pub lg: UiTextStyle,\n    /// Page headings (20/28).\n    pub xl: UiTextStyle,\n}\n\nimpl Default for UiTypography {\n    fn default() -> Self {\n        let style = |size: f32, line_height: f32| UiTextStyle {\n            size: px(size),\n            line_height: px(line_height),\n        };\n        Self {\n            xs: style(12., 16.),\n            sm: style(14., 20.),\n            base: style(16., 24.),\n            lg: style(18., 28.),\n            xl: style(20., 28.),\n        }\n    }\n}\n\n/// Window widths at which layouts change shape.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiBreakpoints {\n    /// Below this width, sidebars become sheets and drawers fill the window (768px).\n    pub mobile: Pixels,\n    /// Below this width, side drawers take three quarters of the window (640px).\n    pub compact: Pixels,\n}\n\nimpl Default for UiBreakpoints {\n    fn default() -> Self {\n        Self {\n            mobile: px(768.),\n            compact: px(640.),\n        }\n    }\n}\n\n/// Colors every form control shares, derived once from the palette and mode so\n/// no component chooses its own dark-mode alpha. These follow shadcn Nova's\n/// `dark:bg-input/30`, `dark:hover:bg-input/50` and `dark:border-input` rules.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct ControlColors {\n    /// Resting fill of inputs, checkboxes, radios and outline buttons.\n    pub background: Rgba,\n    /// Resting border of the same controls.\n    pub border: Rgba,\n    /// Hover fill of outline, ghost and toggle controls.\n    pub hover: Rgba,\n    /// Hover fill of ghost buttons and plain icon buttons.\n    pub ghost_hover: Rgba,\n    /// Fill of a pressed toggle or selected tab.\n    pub pressed: Rgba,\n    /// Unchecked switch track.\n    pub track: Rgba,\n    /// Switch thumb on the unchecked track.\n    pub thumb: Rgba,\n    /// Switch thumb on the checked (`primary`) track.\n    pub thumb_checked: Rgba,\n    /// Tinted destructive fill, for destructive buttons, badges and tags.\n    pub destructive_background: Rgba,\n    /// Hover of the tinted destructive fill.\n    pub destructive_hover: Rgba,\n    /// Hairline around tinted destructive chips.\n    pub destructive_border: Rgba,\n    /// Focus ring color at Nova's 50% opacity.\n    pub ring: Rgba,\n    /// Focus ring of an invalid control.\n    pub invalid_ring: Rgba,\n    /// Hairline around popovers and dialogs (`ring-foreground/10`).\n    pub surface_border: Rgba,\n}\n\n/// Base spacing unit from the pinned Nova style.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiSpacing {\n    /// Four pixels.\n    pub unit: Pixels,\n}\n\n/// The application-owned theme stored in GPUI global state.\n#[derive(Clone, Debug, PartialEq)]\npub struct UiTheme {\n    /// Active color mode.\n    pub mode: ThemeMode,\n    /// Active semantic colors.\n    pub colors: UiColors,\n    /// Font family names.\n    pub fonts: UiFonts,\n    /// Base corner radius.\n    pub radius: UiRadius,\n    /// Base spacing unit.\n    pub spacing: UiSpacing,\n    /// Shared elevation tokens.\n    pub shadows: UiShadows,\n    /// Shared motion durations and reduced-motion preference.\n    pub motion: UiMotion,\n    /// Typography scale, independent of control density (1 by default).\n    pub text_scale: f32,\n    /// Text sizes and line heights, multiplied by `text_scale` when applied.\n    pub typography: UiTypography,\n    /// Window widths at which layouts change shape.\n    pub breakpoints: UiBreakpoints,\n}\n\nimpl Global for UiTheme {}\n\nimpl Default for UiTheme {\n    fn default() -> Self {\n        Self::neutral_light()\n    }\n}\n\nimpl UiTheme {\n    /// Creates the pinned Neutral light theme.\n    pub fn neutral_light() -> Self {\n        Self::new(ThemeMode::Light, neutral_light_colors())\n    }\n\n    /// Creates the pinned Neutral dark theme.\n    pub fn neutral_dark() -> Self {\n        Self::new(ThemeMode::Dark, neutral_dark_colors())\n    }\n\n    /// Installs light and dark themes together and shows the one the current\n    /// [`Appearance`] selects (System until [`UiTheme::set_appearance`] says\n    /// otherwise). [`follow_system`] keeps the pair in step with the OS.\n    pub fn set_pair(cx: &mut App, light: Self, dark: Self) {\n        let appearance = cx\n            .try_global::<ThemePair>()\n            .map_or(Appearance::System, |pair| pair.appearance);\n        cx.set_global(ThemePair {\n            light,\n            dark,\n            appearance,\n        });\n        Self::apply_appearance(cx);\n    }\n\n    /// Chooses which of the installed pair to show. Without a pair, the\n    /// pinned Neutral palettes stand in.\n    pub fn set_appearance(cx: &mut App, appearance: Appearance) {\n        match cx.try_global::<ThemePair>() {\n            Some(_) => cx.global_mut::<ThemePair>().appearance = appearance,\n            None => cx.set_global(ThemePair {\n                light: Self::neutral_light(),\n                dark: Self::neutral_dark(),\n                appearance,\n            }),\n        }\n        Self::apply_appearance(cx);\n    }\n\n    /// The appearance the application asked for, System when none was set.\n    pub fn appearance(cx: &App) -> Appearance {\n        cx.try_global::<ThemePair>()\n            .map_or(Appearance::System, |pair| pair.appearance)\n    }\n\n    /// Installs the pair member the appearance selects, if a pair is installed.\n    fn apply_appearance(cx: &mut App) {\n        let Some(pair) = cx.try_global::<ThemePair>().cloned() else {\n            return;\n        };\n        let theme = match pair.appearance.resolve(cx.window_appearance()) {\n            ThemeMode::Light => pair.light,\n            ThemeMode::Dark => pair.dark,\n        };\n        let current = cx.try_global::<Self>();\n        if current\n            .is_some_and(|current| current.mode == theme.mode && current.colors == theme.colors)\n        {\n            return;\n        }\n        Self::set(cx, theme);\n    }\n\n    /// Installs a theme into the application.\n    pub fn set(cx: &mut App, theme: Self) {\n        assert!(f32::from(theme.spacing.unit).is_finite() && theme.spacing.unit > px(0.));\n        assert!(theme.text_scale.is_finite() && theme.text_scale > 0.);\n        sync_component_theme(cx, theme.clone());\n        cx.set_global(theme);\n        cx.refresh_windows();\n    }\n\n    /// Reads the installed theme.\n    ///\n    /// Panics when the application has not installed a theme.\n    #[track_caller]\n    pub fn read(cx: &App) -> &Self {\n        cx.try_global::<Self>().expect(\n            \"gpuicn theme is not installed: call `ui::theme::init(cx)` once at application startup\",\n        )\n    }\n\n    /// Builds a theme from shadcn theme CSS, such as the output of the shadcn\n    /// theme picker or tweakcn.\n    ///\n    /// Light reads `:root`; dark reads `:root` and then `.dark`, like the\n    /// browser cascade. Recognized `--background`, `--primary`, `--sidebar-*`,\n    /// `--chart-*` and `--radius` declarations replace the Neutral defaults;\n    /// other declarations are ignored. Colors may use `oklch()`, `hsl()`,\n    /// bare HSL triplets, `rgb()` or hex. Build both modes and call\n    /// [`UiTheme::set`] to switch; [`UiTheme::switch`] restores Neutral colors.\n    pub fn from_shadcn_css(css: &str, mode: ThemeMode) -> Result<Self, ThemeCssError> {\n        let blocks = css_blocks(css);\n        let mut theme = match mode {\n            ThemeMode::Light => Self::neutral_light(),\n            ThemeMode::Dark => Self::neutral_dark(),\n        };\n        let mut selectors = vec![\":root\"];\n        if mode == ThemeMode::Dark {\n            selectors.push(\".dark\");\n        }\n        for selector in selectors {\n            let declarations = blocks\n                .iter()\n                .filter(|(name, _)| name.split(',').any(|part| part.trim() == selector))\n                .flat_map(|(_, body)| body.split(';'))\n                .collect::<Vec<_>>();\n            if declarations.is_empty() {\n                return Err(ThemeCssError(format!(\"no `{selector}` block found\")));\n            }\n            for declaration in declarations {\n                let Some((name, value)) = declaration.split_once(':') else {\n                    continue;\n                };\n                let (name, value) = (name.trim(), value.trim());\n                let Some(name) = name.strip_prefix(\"--\") else {\n                    continue;\n                };\n                if name == \"radius\" {\n                    theme.radius = UiRadius::new(css_length(value).ok_or_else(|| {\n                        ThemeCssError(format!(\"unsupported length `--radius: {value}`\"))\n                    })?);\n                } else if let Some(slot) = theme.colors.slot(name) {\n                    *slot = css_color(value).ok_or_else(|| {\n                        ThemeCssError(format!(\"unsupported color `--{name}: {value}`\"))\n                    })?;\n                }\n            }\n        }\n        Ok(theme)\n    }\n\n    /// Selects the pinned palette, preserving non-color tokens or initializing defaults.\n    pub fn switch(cx: &mut App, mode: ThemeMode) {\n        let mut theme = cx\n            .try_global::<Self>()\n            .cloned()\n            .unwrap_or_else(Self::neutral_light);\n        theme.mode = mode;\n        theme.colors = match mode {\n            ThemeMode::Light => neutral_light_colors(),\n            ThemeMode::Dark => neutral_dark_colors(),\n        };\n        Self::set(cx, theme);\n    }\n\n    /// Returns a multiple of the configured spacing unit (2 means 8px by default).\n    pub fn space(&self, units: f32) -> Pixels {\n        self.spacing.unit * units\n    }\n\n    /// Scales typography independently of spacing; sizes use Nova's default pixels.\n    pub fn text(&self, size: f32) -> Pixels {\n        px(size * self.text_scale)\n    }\n\n    fn new(mode: ThemeMode, colors: UiColors) -> Self {\n        Self {\n            mode,\n            colors,\n            fonts: UiFonts {\n                body: \"Geist\".into(),\n                heading: \"Geist\".into(),\n                mono: \"Geist Mono\".into(),\n            },\n            radius: UiRadius::new(px(10.)),\n            motion: UiMotion::default(),\n            text_scale: 1.,\n            typography: UiTypography::default(),\n            breakpoints: UiBreakpoints::default(),\n            spacing: UiSpacing { unit: px(4.) },\n            shadows: UiShadows {\n                sm: Arc::from([\n                    BoxShadow::new(px(0.), px(1.), black().alpha(0.10)).blur_radius(px(3.)),\n                    BoxShadow::new(px(0.), px(1.), black().alpha(0.10))\n                        .blur_radius(px(2.))\n                        .spread_radius(px(-1.)),\n                ]),\n                md: Arc::from([\n                    BoxShadow::new(px(0.), px(4.), black().alpha(0.10))\n                        .blur_radius(px(6.))\n                        .spread_radius(px(-1.)),\n                    BoxShadow::new(px(0.), px(2.), black().alpha(0.10))\n                        .blur_radius(px(4.))\n                        .spread_radius(px(-2.)),\n                ]),\n                lg: Arc::from([\n                    BoxShadow::new(px(0.), px(10.), black().alpha(0.10))\n                        .blur_radius(px(15.))\n                        .spread_radius(px(-3.)),\n                    BoxShadow::new(px(0.), px(4.), black().alpha(0.10))\n                        .blur_radius(px(6.))\n                        .spread_radius(px(-4.)),\n                ]),\n            },\n        }\n    }\n\n    /// The shared form-control colors for the active mode. See [`ControlColors`].\n    pub fn control(&self) -> ControlColors {\n        let colors = self.colors;\n        let dark = self.mode == ThemeMode::Dark;\n        ControlColors {\n            background: if dark {\n                colors.input.opacity(0.30)\n            } else {\n                colors.background.opacity(0.)\n            },\n            border: colors.input,\n            hover: if dark {\n                colors.input.opacity(0.50)\n            } else {\n                colors.muted\n            },\n            ghost_hover: if dark {\n                colors.muted.opacity(0.50)\n            } else {\n                colors.muted\n            },\n            pressed: colors.muted,\n            track: if dark {\n                colors.input.opacity(0.80)\n            } else {\n                colors.input\n            },\n            thumb: if dark {\n                colors.foreground\n            } else {\n                colors.background\n            },\n            thumb_checked: if dark {\n                colors.primary_foreground\n            } else {\n                colors.background\n            },\n            destructive_background: colors.destructive.opacity(if dark { 0.20 } else { 0.10 }),\n            destructive_hover: colors.destructive.opacity(if dark { 0.30 } else { 0.20 }),\n            destructive_border: colors.destructive.opacity(0.20),\n            ring: colors.ring.opacity(0.50),\n            invalid_ring: colors.destructive.opacity(0.20),\n            surface_border: colors.foreground.opacity(0.10),\n        }\n    }\n\n    /// A step of the type scale multiplied by `text_scale`.\n    pub fn text_style(&self, style: UiTextStyle) -> UiTextStyle {\n        UiTextStyle {\n            size: style.size * self.text_scale,\n            line_height: style.line_height * self.text_scale,\n        }\n    }\n}\n\nfn sync_component_theme(cx: &mut App, theme: UiTheme) {\n    use gpui_kit::component::{Theme, ThemeTokens};\n\n    if !cx.has_global::<Theme>() {\n        return;\n    }\n\n    let colors = theme.colors;\n    let component = Theme::global_mut(cx);\n    component.mode = match theme.mode {\n        ThemeMode::Light => gpui_kit::component::ThemeMode::Light,\n        ThemeMode::Dark => gpui_kit::component::ThemeMode::Dark,\n    };\n    // Start from Kit's palette for this mode, so colors gpuicn doesn't set\n    // below, such as scrollbar tracks, never keep the other mode's values.\n    component.colors = *match theme.mode {\n        ThemeMode::Light => gpui_kit::component::ThemeColor::light(),\n        ThemeMode::Dark => gpui_kit::component::ThemeColor::dark(),\n    };\n    component.font_family = theme.fonts.body;\n    component.mono_font_family = theme.fonts.mono;\n    component.radius = theme.radius.md;\n    component.radius_lg = theme.radius.lg;\n    component.background = colors.background.into();\n    component.foreground = colors.foreground.into();\n    component.popover = colors.popover.into();\n    component.popover_foreground = colors.popover_foreground.into();\n    component.primary = colors.primary.into();\n    component.primary_foreground = colors.primary_foreground.into();\n    component.secondary = colors.secondary.into();\n    component.secondary_foreground = colors.secondary_foreground.into();\n    component.muted = colors.muted.into();\n    component.muted_foreground = colors.muted_foreground.into();\n    component.accent = colors.accent.into();\n    component.accent_foreground = colors.accent_foreground.into();\n    component.primary_hover = colors.primary.opacity(0.9).into();\n    component.primary_active = colors.primary.opacity(0.8).into();\n    component.secondary_hover = colors.secondary.opacity(0.8).into();\n    component.secondary_active = colors.secondary.opacity(0.7).into();\n    component.border = colors.border.into();\n    component.input = colors.input.into();\n    component.ring = colors.ring.into();\n    component.overlay = colors.overlay.into();\n    component.caret = colors.foreground.into();\n    component.window_border = colors.border.into();\n    component.danger = colors.destructive.into();\n    component.danger_foreground = colors.destructive_foreground.into();\n    component.danger_hover = colors.destructive.opacity(0.9).into();\n    component.danger_active = colors.destructive.opacity(0.8).into();\n    component.success = colors.success.into();\n    component.success_foreground = colors.success_foreground.into();\n    component.success_hover = colors.success.opacity(0.9).into();\n    component.success_active = colors.success.opacity(0.8).into();\n    component.warning = colors.warning.into();\n    component.warning_foreground = colors.warning_foreground.into();\n    component.warning_hover = colors.warning.opacity(0.9).into();\n    component.warning_active = colors.warning.opacity(0.8).into();\n    component.info = colors.info.into();\n    component.info_foreground = colors.info_foreground.into();\n    component.info_hover = colors.info.opacity(0.9).into();\n    component.info_active = colors.info.opacity(0.8).into();\n    component.chart_1 = colors.chart_1.into();\n    component.chart_2 = colors.chart_2.into();\n    component.chart_3 = colors.chart_3.into();\n    component.chart_4 = colors.chart_4.into();\n    component.chart_5 = colors.chart_5.into();\n    component.chart_bullish = colors.success.into();\n    component.chart_bearish = colors.destructive.into();\n    component.chart_grid = colors.border.opacity(0.6).into();\n    // Buttons: Kit's plain button is shadcn `outline`; the variants follow their token.\n    component.button = colors.background.into();\n    component.button_foreground = colors.foreground.into();\n    component.button_hover = colors.accent.into();\n    component.button_active = colors.muted.into();\n    component.button_primary = component.primary;\n    component.button_primary_foreground = component.primary_foreground;\n    component.button_primary_hover = component.primary_hover;\n    component.button_primary_active = component.primary_active;\n    component.button_secondary = component.secondary;\n    component.button_secondary_foreground = component.secondary_foreground;\n    component.button_secondary_hover = component.secondary_hover;\n    component.button_secondary_active = component.secondary_active;\n    component.button_danger = component.danger;\n    component.button_danger_foreground = component.danger_foreground;\n    component.button_danger_hover = component.danger_hover;\n    component.button_danger_active = component.danger_active;\n    component.button_success = component.success;\n    component.button_success_foreground = component.success_foreground;\n    component.button_success_hover = component.success_hover;\n    component.button_success_active = component.success_active;\n    component.button_warning = component.warning;\n    component.button_warning_foreground = component.warning_foreground;\n    component.button_warning_hover = component.warning_hover;\n    component.button_warning_active = component.warning_active;\n    component.button_info = component.info;\n    component.button_info_foreground = component.info_foreground;\n    component.button_info_hover = component.info_hover;\n    component.button_info_active = component.info_active;\n    // Nova scrollbars float over content with no track.\n    component.scrollbar = gpui_kit::transparent_black();\n    component.scrollbar_thumb = colors.border.into();\n    component.scrollbar_thumb_hover = colors.muted_foreground.opacity(0.72).into();\n    component.sidebar = colors.sidebar.into();\n    component.sidebar_foreground = colors.sidebar_foreground.into();\n    component.sidebar_border = colors.sidebar_border.into();\n    component.sidebar_accent = colors.sidebar_accent.into();\n    component.sidebar_accent_foreground = colors.sidebar_accent_foreground.into();\n    component.sidebar_primary = colors.sidebar_primary.into();\n    component.sidebar_primary_foreground = colors.sidebar_primary_foreground.into();\n    component.title_bar = colors.sidebar.into();\n    component.title_bar_border = colors.border.into();\n    component.status_bar = colors.sidebar.into();\n    component.status_bar_border = colors.border.into();\n    // Rich text: links, selection, table headings and code highlighting.\n    component.link = colors.primary.into();\n    component.link_hover = colors.primary.opacity(0.9).into();\n    component.link_active = colors.primary.opacity(0.8).into();\n    component.selection = colors.ring.opacity(0.3).into();\n    component.description_list_label = colors.muted.into();\n    component.description_list_label_foreground = colors.muted_foreground.into();\n    component.group_box = colors.card.into();\n    component.group_box_foreground = colors.card_foreground.into();\n    component.accordion = colors.card.into();\n    // Lists and tables share the `muted` zebra and the `accent` hover of menus.\n    component.colors.list = colors.background.into();\n    component.list_even = colors.muted.opacity(0.5).into();\n    component.list_head = colors.muted.into();\n    component.list_hover = colors.accent.into();\n    component.list_active = colors.accent.into();\n    component.list_active_border = colors.ring.into();\n    component.table = colors.background.into();\n    component.table_even = colors.muted.opacity(0.5).into();\n    component.table_hover = colors.muted.opacity(0.5).into();\n    component.table_active = colors.accent.into();\n    component.table_active_border = colors.ring.into();\n    component.table_row_border = colors.border.into();\n    component.table_head = colors.muted.into();\n    component.table_head_foreground = colors.foreground.into();\n    component.table_foot = colors.muted.opacity(0.5).into();\n    component.table_foot_foreground = colors.foreground.into();\n    // Controls: shadcn tracks are `input`, thumbs and active tabs are `background`.\n    component.switch = colors.input.into();\n    component.switch_thumb = colors.background.into();\n    component.slider_bar = colors.primary.into();\n    component.slider_thumb = colors.background.into();\n    component.progress_bar = colors.primary.into();\n    component.skeleton = colors.accent.into();\n    component.tab_bar = colors.muted.into();\n    component.tab_bar_segmented = colors.muted.into();\n    component.tab_foreground = colors.muted_foreground.into();\n    component.tab_active = colors.background.into();\n    component.tab_active_foreground = colors.foreground.into();\n    component.drag_border = colors.ring.into();\n    component.drop_target = colors.ring.opacity(0.15).into();\n    component.highlight_theme = match theme.mode {\n        ThemeMode::Light => gpui_kit::component::highlighter::HighlightTheme::default_light(),\n        ThemeMode::Dark => gpui_kit::component::highlighter::HighlightTheme::default_dark(),\n    };\n    component.tokens = ThemeTokens::from(&component.colors);\n\n    Theme::sync_base(cx);\n}\n\n/// Why [`UiTheme::from_shadcn_css`] could not read a theme.\n#[derive(Clone, Debug, PartialEq)]\npub struct ThemeCssError(pub String);\n\nimpl std::fmt::Display for ThemeCssError {\n    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(formatter, \"shadcn theme CSS: {}\", self.0)\n    }\n}\n\nimpl std::error::Error for ThemeCssError {}\n\nimpl UiColors {\n    #[doc(hidden)]\n    pub fn slot(&mut self, variable: &str) -> Option<&mut Rgba> {\n        Some(match variable {\n            \"background\" => &mut self.background,\n            \"foreground\" => &mut self.foreground,\n            \"card\" => &mut self.card,\n            \"card-foreground\" => &mut self.card_foreground,\n            \"popover\" => &mut self.popover,\n            \"popover-foreground\" => &mut self.popover_foreground,\n            \"primary\" => &mut self.primary,\n            \"primary-foreground\" => &mut self.primary_foreground,\n            \"secondary\" => &mut self.secondary,\n            \"secondary-foreground\" => &mut self.secondary_foreground,\n            \"muted\" => &mut self.muted,\n            \"muted-foreground\" => &mut self.muted_foreground,\n            \"accent\" => &mut self.accent,\n            \"accent-foreground\" => &mut self.accent_foreground,\n            \"destructive\" => &mut self.destructive,\n            \"destructive-foreground\" => &mut self.destructive_foreground,\n            \"success\" => &mut self.success,\n            \"success-foreground\" => &mut self.success_foreground,\n            \"warning\" => &mut self.warning,\n            \"warning-foreground\" => &mut self.warning_foreground,\n            \"info\" => &mut self.info,\n            \"info-foreground\" => &mut self.info_foreground,\n            \"border\" => &mut self.border,\n            \"input\" => &mut self.input,\n            \"ring\" => &mut self.ring,\n            \"chart-1\" => &mut self.chart_1,\n            \"chart-2\" => &mut self.chart_2,\n            \"chart-3\" => &mut self.chart_3,\n            \"chart-4\" => &mut self.chart_4,\n            \"chart-5\" => &mut self.chart_5,\n            \"sidebar\" | \"sidebar-background\" => &mut self.sidebar,\n            \"sidebar-foreground\" => &mut self.sidebar_foreground,\n            \"sidebar-primary\" => &mut self.sidebar_primary,\n            \"sidebar-primary-foreground\" => &mut self.sidebar_primary_foreground,\n            \"sidebar-accent\" => &mut self.sidebar_accent,\n            \"sidebar-accent-foreground\" => &mut self.sidebar_accent_foreground,\n            \"sidebar-border\" => &mut self.sidebar_border,\n            \"sidebar-ring\" => &mut self.sidebar_ring,\n            _ => return None,\n        })\n    }\n}\n\n/// Innermost `selector { body }` pairs with comments removed.\nfn css_blocks(css: &str) -> Vec<(String, String)> {\n    let mut text = String::with_capacity(css.len());\n    let mut rest = css;\n    while let Some(start) = rest.find(\"/*\") {\n        text.push_str(&rest[..start]);\n        rest = rest[start + 2..]\n            .split_once(\"*/\")\n            .map_or(\"\", |(_, after)| after);\n    }\n    text.push_str(rest);\n    let mut blocks = Vec::new();\n    let mut selector_start = 0;\n    let mut open = None;\n    for (index, character) in text.char_indices() {\n        match character {\n            '{' => {\n                open = Some((text[selector_start..index].trim().to_owned(), index + 1));\n                selector_start = index + 1;\n            }\n            '}' => {\n                if let Some((selector, body)) = open.take() {\n                    blocks.push((selector, text[body..index].to_owned()));\n                }\n                selector_start = index + 1;\n            }\n            ';' if open.is_none() => selector_start = index + 1,\n            _ => {}\n        }\n    }\n    blocks\n}\n\nfn css_length(value: &str) -> Option<Pixels> {\n    let value = value.trim();\n    let pixels = if let Some(rem) = value.strip_suffix(\"rem\") {\n        rem.trim().parse::<f32>().ok()? * 16.\n    } else if let Some(pixels) = value.strip_suffix(\"px\") {\n        pixels.trim().parse::<f32>().ok()?\n    } else {\n        value.parse::<f32>().ok().filter(|value| *value == 0.)?\n    };\n    (pixels.is_finite() && pixels >= 0.).then(|| px(pixels))\n}\n\n/// A CSS number or percentage; `percent` is the value of `100%`.\nfn css_number(value: &str, percent: f64) -> Option<f64> {\n    let value = value.trim();\n    let number = match value.strip_suffix('%') {\n        Some(value) => value.trim().parse::<f64>().ok()? / 100. * percent,\n        None => value\n            .strip_suffix(\"deg\")\n            .unwrap_or(value)\n            .parse::<f64>()\n            .ok()?,\n    };\n    number.is_finite().then_some(number)\n}\n\nfn css_color(value: &str) -> Option<Rgba> {\n    let value = value.trim().trim_end_matches(\"!important\").trim();\n    if let Some(hex) = value.strip_prefix('#') {\n        return css_hex(hex);\n    }\n    let (function, arguments) = match value.split_once('(') {\n        Some((function, arguments)) => (\n            function.trim().to_ascii_lowercase(),\n            arguments.strip_suffix(')')?,\n        ),\n        // shadcn v3 themes store bare HSL triplets such as `0 0% 100%`.\n        None => (\"hsl\".to_owned(), value),\n    };\n    let (channels, alpha) = match arguments.split_once('/') {\n        Some((channels, alpha)) => (channels, css_number(alpha, 1.)?),\n        None => (arguments, 1.),\n    };\n    let channels: Vec<_> = channels\n        .split(|character: char| character == ',' || character.is_whitespace())\n        .filter(|part| !part.is_empty())\n        .collect();\n    let (channels, alpha) = match channels.len() {\n        3 => (channels, alpha),\n        4 => (channels[..3].to_vec(), css_number(channels[3], 1.)?),\n        _ => return None,\n    };\n    let color = match function.as_str() {\n        \"oklch\" => oklch(\n            css_number(channels[0], 1.)?,\n            css_number(channels[1], 0.4)?,\n            css_number(channels[2], 1.)?,\n        ),\n        \"hsl\" | \"hsla\" => hsl(\n            css_number(channels[0], 1.)?,\n            css_number(channels[1], 1.)?,\n            css_number(channels[2], 1.)?,\n        ),\n        \"rgb\" | \"rgba\" => [\n            css_number(channels[0], 255.)? / 255.,\n            css_number(channels[1], 255.)? / 255.,\n            css_number(channels[2], 255.)? / 255.,\n        ],\n        _ => return None,\n    };\n    Some(Rgba {\n        r: color[0].clamp(0., 1.) as f32,\n        g: color[1].clamp(0., 1.) as f32,\n        b: color[2].clamp(0., 1.) as f32,\n        a: alpha.clamp(0., 1.) as f32,\n    })\n}\n\nfn css_hex(hex: &str) -> Option<Rgba> {\n    if !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {\n        return None;\n    }\n    let digits: Vec<u8> = match hex.len() {\n        3 | 4 => hex\n            .chars()\n            .map(|digit| u8::from_str_radix(&format!(\"{digit}{digit}\"), 16).ok())\n            .collect::<Option<_>>()?,\n        6 | 8 => (0..hex.len())\n            .step_by(2)\n            .map(|index| u8::from_str_radix(&hex[index..index + 2], 16).ok())\n            .collect::<Option<_>>()?,\n        _ => return None,\n    };\n    Some(Rgba {\n        r: digits[0] as f32 / 255.,\n        g: digits[1] as f32 / 255.,\n        b: digits[2] as f32 / 255.,\n        a: digits.get(3).map_or(1., |alpha| *alpha as f32 / 255.),\n    })\n}\n\n/// OKLCH to gamma-encoded sRGB, matching the pinned Neutral conversion when chroma is 0.\nfn oklch(lightness: f64, chroma: f64, hue: f64) -> [f64; 3] {\n    let (a, b) = (\n        chroma * hue.to_radians().cos(),\n        chroma * hue.to_radians().sin(),\n    );\n    let l = (lightness + 0.396_337_777_4 * a + 0.215_803_757_3 * b).powi(3);\n    let m = (lightness - 0.105_561_345_8 * a - 0.063_854_172_8 * b).powi(3);\n    let s = (lightness - 0.089_484_177_5 * a - 1.291_485_548 * b).powi(3);\n    [\n        4.076_741_662_1 * l - 3.307_711_591_3 * m + 0.230_969_929_2 * s,\n        -1.268_438_004_6 * l + 2.609_757_401_1 * m - 0.341_319_396_5 * s,\n        -0.004_196_086_3 * l - 0.703_418_614_7 * m + 1.707_614_701 * s,\n    ]\n    .map(srgb_channel)\n}\n\nfn hsl(hue: f64, saturation: f64, lightness: f64) -> [f64; 3] {\n    let hue = hue.rem_euclid(360.) / 60.;\n    let chroma = (1. - (2. * lightness - 1.).abs()) * saturation;\n    let x = chroma * (1. - (hue % 2. - 1.).abs());\n    let (r, g, b) = match hue as u32 {\n        0 => (chroma, x, 0.),\n        1 => (x, chroma, 0.),\n        2 => (0., chroma, x),\n        3 => (0., x, chroma),\n        4 => (x, 0., chroma),\n        _ => (chroma, 0., x),\n    };\n    let base = lightness - chroma / 2.;\n    [r + base, g + base, b + base]\n}\n\nfn neutral_light_colors() -> UiColors {\n    UiColors {\n        background: neutral(1.0),\n        foreground: neutral(0.145),\n        card: neutral(1.0),\n        card_foreground: neutral(0.145),\n        popover: neutral(1.0),\n        popover_foreground: neutral(0.145),\n        primary: neutral(0.205),\n        primary_foreground: neutral(0.985),\n        secondary: neutral(0.97),\n        secondary_foreground: neutral(0.205),\n        muted: neutral(0.97),\n        muted_foreground: neutral(0.556),\n        accent: neutral(0.97),\n        accent_foreground: neutral(0.205),\n        destructive: srgb(0xdc, 0x26, 0x26),\n        destructive_foreground: neutral(0.985),\n        success: srgb(0x16, 0xa3, 0x4a),\n        success_foreground: neutral(0.985),\n        warning: srgb(0xd9, 0x77, 0x06),\n        warning_foreground: neutral(0.985),\n        info: srgb(0x25, 0x63, 0xeb),\n        info_foreground: neutral(0.985),\n        border: neutral(0.922),\n        input: neutral(0.922),\n        ring: neutral(0.708),\n        chart_1: neutral(0.87),\n        chart_2: neutral(0.556),\n        chart_3: neutral(0.439),\n        chart_4: neutral(0.371),\n        chart_5: neutral(0.269),\n        sidebar: neutral(0.985),\n        sidebar_foreground: neutral(0.145),\n        sidebar_primary: neutral(0.205),\n        sidebar_primary_foreground: neutral(0.985),\n        sidebar_accent: neutral(0.97),\n        sidebar_accent_foreground: neutral(0.205),\n        sidebar_border: neutral(0.922),\n        sidebar_ring: neutral(0.708),\n        overlay: black().alpha(0.10).into(),\n    }\n}\n\nfn neutral_dark_colors() -> UiColors {\n    UiColors {\n        background: neutral(0.145),\n        foreground: neutral(0.985),\n        card: neutral(0.205),\n        card_foreground: neutral(0.985),\n        popover: neutral(0.205),\n        popover_foreground: neutral(0.985),\n        primary: neutral(0.922),\n        primary_foreground: neutral(0.205),\n        secondary: neutral(0.269),\n        secondary_foreground: neutral(0.985),\n        muted: neutral(0.269),\n        muted_foreground: neutral(0.708),\n        accent: neutral(0.269),\n        accent_foreground: neutral(0.985),\n        destructive: srgb(0xff, 0x64, 0x67),\n        destructive_foreground: neutral(0.985),\n        success: srgb(0x4a, 0xde, 0x80),\n        success_foreground: neutral(0.145),\n        warning: srgb(0xfb, 0xbf, 0x24),\n        warning_foreground: neutral(0.145),\n        info: srgb(0x60, 0xa5, 0xfa),\n        info_foreground: neutral(0.145),\n        border: neutral_alpha(1.0, 0.10),\n        input: neutral_alpha(1.0, 0.15),\n        ring: neutral(0.556),\n        chart_1: neutral(0.87),\n        chart_2: neutral(0.556),\n        chart_3: neutral(0.439),\n        chart_4: neutral(0.371),\n        chart_5: neutral(0.269),\n        sidebar: neutral(0.205),\n        sidebar_foreground: neutral(0.985),\n        sidebar_primary: srgb(0x14, 0x47, 0xe6),\n        sidebar_primary_foreground: neutral(0.985),\n        sidebar_accent: neutral(0.269),\n        sidebar_accent_foreground: neutral(0.985),\n        sidebar_border: neutral_alpha(1.0, 0.10),\n        sidebar_ring: neutral(0.556),\n        // shadcn pairs black/10 with a backdrop blur GPUI lacks; without it a\n        // 10% scrim disappears on a near-black page.\n        overlay: black().alpha(0.50).into(),\n    }\n}\n\n#[doc(hidden)]\npub fn neutral(l: f64) -> Rgba {\n    neutral_alpha(l, 1.0)\n}\n\nfn neutral_alpha(l: f64, alpha: f64) -> Rgba {\n    let channel = srgb_channel(l * l * l) as f32;\n\n    Rgba {\n        r: channel,\n        g: channel,\n        b: channel,\n        a: alpha.clamp(0.0, 1.0) as f32,\n    }\n}\n\nconst fn srgb(red: u8, green: u8, blue: u8) -> Rgba {\n    Rgba {\n        r: red as f32 / 255.0,\n        g: green as f32 / 255.0,\n        b: blue as f32 / 255.0,\n        a: 1.0,\n    }\n}\n\nfn srgb_channel(channel: f64) -> f64 {\n    if channel >= 0.003_130_8 {\n        (1.055 * channel.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0)\n    } else {\n        (12.92 * channel).clamp(0.0, 1.0)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn converts_pinned_neutral_tokens() {\n        assert_eq!(\n            neutral(1.0),\n            Rgba {\n                r: 1.0,\n                g: 1.0,\n                b: 1.0,\n                a: 1.0,\n            }\n        );\n        assert_eq!(\n            neutral(0.0),\n            Rgba {\n                r: 0.0,\n                g: 0.0,\n                b: 0.0,\n                a: 1.0,\n            }\n        );\n\n        assert_eq!(neutral_light_colors().destructive, srgb(0xdc, 0x26, 0x26));\n        assert_eq!(neutral_dark_colors().destructive, srgb(0xff, 0x64, 0x67));\n        assert_eq!(\n            neutral_dark_colors().sidebar_primary,\n            srgb(0x14, 0x47, 0xe6)\n        );\n    }\n\n    fn close(left: Rgba, right: Rgba) -> bool {\n        [\n            (left.r, right.r),\n            (left.g, right.g),\n            (left.b, right.b),\n            (left.a, right.a),\n        ]\n        .iter()\n        .all(|(left, right)| (left - right).abs() < 1.5 / 255.)\n    }\n\n    #[test]\n    fn imports_shadcn_theme_css() {\n        let css = r#\"\n            @layer base {\n              :root {\n                --radius: 0.5rem; /* 8px */\n                --background: oklch(1 0 0);\n                --foreground: oklch(0.145 0 0);\n                --primary: #2563eb;\n                --ring: rgb(37 99 235 / 50%);\n                --sidebar-background: 0 0% 98%;\n                --font-sans: Inter, sans-serif;\n              }\n              .dark {\n                --background: oklch(0.145 0 0);\n                --border: oklch(1 0 0 / 10%);\n                --sidebar-primary: oklch(0.488 0.243 264.376);\n                --chart-1: hsl(220 70% 50%);\n              }\n            }\n        \"#;\n        let light = UiTheme::from_shadcn_css(css, ThemeMode::Light).unwrap();\n        assert_eq!(light.mode, ThemeMode::Light);\n        assert_eq!(light.radius.lg, px(8.));\n        assert!(close(light.colors.background, neutral(1.0)));\n        assert!(close(light.colors.foreground, neutral(0.145)));\n        assert_eq!(light.colors.primary, srgb(0x25, 0x63, 0xeb));\n        assert!(close(\n            light.colors.ring,\n            Rgba {\n                a: 0.5,\n                ..srgb(37, 99, 235)\n            }\n        ));\n        assert!(close(light.colors.sidebar, neutral(0.985)));\n        // Variables the CSS leaves out keep the Neutral defaults.\n        assert_eq!(light.colors.muted, neutral_light_colors().muted);\n\n        let dark = UiTheme::from_shadcn_css(css, ThemeMode::Dark).unwrap();\n        assert_eq!(dark.mode, ThemeMode::Dark);\n        assert!(close(dark.colors.background, neutral(0.145)));\n        // `.dark` inherits `:root` values it does not override.\n        assert_eq!(dark.colors.primary, srgb(0x25, 0x63, 0xeb));\n        assert!(close(dark.colors.border, neutral_alpha(1.0, 0.10)));\n        assert!(close(dark.colors.sidebar_primary, srgb(0x14, 0x47, 0xe6)));\n        assert!(close(dark.colors.chart_1, srgb(0x26, 0x63, 0xd9)));\n\n        assert!(UiTheme::from_shadcn_css(\":root { --primary: blue; }\", ThemeMode::Light).is_err());\n        assert!(UiTheme::from_shadcn_css(\":root { --radius: 1em; }\", ThemeMode::Light).is_err());\n        assert!(UiTheme::from_shadcn_css(\":root { }\", ThemeMode::Dark).is_err());\n        assert_eq!(\n            css_hex(\"fff8\"),\n            Some(Rgba {\n                r: 1.,\n                g: 1.,\n                b: 1.,\n                a: 0x88 as f32 / 255.\n            })\n        );\n    }\n\n    #[test]\n    fn preserves_pinned_alpha_tokens() {\n        let dark = neutral_dark_colors();\n        assert_eq!(dark.border.a, 0.10);\n        assert_eq!(dark.input.a, 0.15);\n        assert_eq!(dark.sidebar_border.a, 0.10);\n        assert!((dark.input.opacity(0.30).a - 0.045).abs() < f32::EPSILON);\n    }\n\n    #[test]\n    fn derives_the_shadcn_radius_and_shadow_scales() {\n        let theme = UiTheme::neutral_light();\n        assert_eq!(theme.radius.sm, px(6.));\n        assert_eq!(theme.radius.md, px(8.));\n        assert_eq!(theme.radius.lg, px(10.));\n        assert_eq!(theme.radius.xl, px(14.));\n        assert_eq!(theme.shadows.sm.len(), 2);\n        assert_eq!(theme.shadows.md.len(), 2);\n        assert_eq!(theme.shadows.lg.len(), 2);\n    }\n\n    #[gpui_kit::test]\n    fn keeps_component_state_tokens_in_sync_with_ui_theme(cx: &mut gpui_kit::TestAppContext) {\n        cx.update(|cx| {\n            init(cx);\n            let component = gpui_kit::component::Theme::global(cx);\n            assert_eq!(component.mode, gpui_kit::component::ThemeMode::Light);\n            assert_eq!(\n                component.button_hover,\n                UiTheme::read(cx).colors.accent.into()\n            );\n\n            UiTheme::switch(cx, ThemeMode::Dark);\n            let component = gpui_kit::component::Theme::global(cx);\n            assert_eq!(component.mode, gpui_kit::component::ThemeMode::Dark);\n            assert_eq!(component.ring, UiTheme::read(cx).colors.ring.into());\n\n            let mut custom = UiTheme::read(cx).clone();\n            custom.colors.accent = srgb(0x22, 0x66, 0xaa);\n            custom.fonts.body = \"Custom Sans\".into();\n            custom.radius = UiRadius::new(px(12.));\n            let expected_accent = custom.colors.accent;\n            UiTheme::set(cx, custom);\n\n            let component = gpui_kit::component::Theme::global(cx);\n            assert_eq!(component.button_hover, expected_accent.into());\n            assert_eq!(component.font_family, \"Custom Sans\");\n            assert_eq!(component.radius, px(9.6));\n            assert_eq!(\n                component.tokens.button_hover.background,\n                expected_accent.into()\n            );\n\n            init(cx);\n            assert_eq!(\n                gpui_kit::component::Theme::global(cx).button_hover,\n                expected_accent.into()\n            );\n        });\n    }\n}\n\n// Refine the actual control after default styling; no extra layout or focus node.\n#[doc(hidden)]\npub fn apply_style<T: Styled>(mut element: T, style: &gpui_kit::StyleRefinement) -> T {\n    use gpui_kit::Refineable as _;\n    element.style().refine(style);\n    element\n}\n\n#[derive(gpui_kit::IntoElement)]\n#[doc(hidden)]\npub struct DisclosureIcon {\n    icon: gpui_kit::Svg,\n    open: bool,\n}\n#[doc(hidden)]\npub fn disclosure_icon(icon: gpui_kit::Svg, open: bool) -> DisclosureIcon {\n    DisclosureIcon { icon, open }\n}\nimpl gpui_kit::RenderOnce for DisclosureIcon {\n    fn render(self, window: &mut Window, cx: &mut App) -> impl gpui_kit::IntoElement {\n        let duration = UiTheme::read(cx).motion.fast;\n        let value = transition_value(\n            \"disclosure-rotation\",\n            if self.open { 1. } else { 0. },\n            duration,\n            window,\n            cx,\n        );\n        self.icon\n            .with_transformation(gpui_kit::Transformation::rotate(gpui_kit::radians(\n                value * std::f32::consts::PI,\n            )))\n    }\n}\n\n#[cfg(test)]\nmod motion_tests {\n    use super::*;\n    use gpui_kit::{\n        AppContext as _, Context, IntoElement, Render, TestAppContext, VisualTestContext, div,\n    };\n\n    struct View {\n        target: f32,\n        sampled: f32,\n    }\n    impl Render for View {\n        fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n            self.sampled = transition_value(\n                \"test-motion\",\n                self.target,\n                Duration::from_millis(200),\n                window,\n                cx,\n            );\n            div()\n        }\n    }\n\n    #[gpui_kit::test]\n    fn theme_reduced_motion_snaps_an_active_upstream_transition(cx: &mut TestAppContext) {\n        cx.update(init);\n        let window = cx.add_window(|_, _| View {\n            target: 0.,\n            sampled: 0.,\n        });\n        let mut visual = VisualTestContext::from_window(window.into(), cx);\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        window\n            .update(cx, |view, _, cx| {\n                view.target = 1.;\n                cx.notify();\n            })\n            .unwrap();\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert_eq!(\n            cx.read_window(&window, |view, cx| view.read(cx).sampled)\n                .unwrap(),\n            0.\n        );\n        cx.update(|cx| {\n            let mut theme = UiTheme::read(cx).clone();\n            theme.motion.reduced = true;\n            UiTheme::set(cx, theme);\n        });\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert_eq!(\n            cx.read_window(&window, |view, cx| view.read(cx).sampled)\n                .unwrap(),\n            1.\n        );\n    }\n}\n\n/// Shared focus traversal for the tab, radio and toggle collections.\n#[doc(hidden)]\npub struct RovingFocus {\n    pub handles: Vec<Option<gpui_kit::FocusHandle>>,\n    active: gpui_kit::Entity<Option<gpui_kit::FocusHandle>>,\n}\nimpl RovingFocus {\n    pub fn new(\n        id: gpui_kit::ElementId,\n        items: &[(gpui_kit::ElementId, bool)],\n        selected: Option<usize>,\n        window: &mut gpui_kit::Window,\n        cx: &mut App,\n    ) -> Self {\n        let active =\n            window.use_keyed_state((id, \"active\"), cx, |_, _| None::<gpui_kit::FocusHandle>);\n        let mut handles: Vec<_> = items\n            .iter()\n            .map(|(id, disabled)| {\n                let handle = window\n                    .use_keyed_state((id.clone(), \"focus\"), cx, |_, cx| cx.focus_handle())\n                    .read(cx)\n                    .clone();\n                handle.clone().tab_stop(false);\n                (!disabled).then_some(handle)\n            })\n            .collect();\n        let index = handles\n            .iter()\n            .position(|h| h.as_ref().is_some_and(|h| h.is_focused(window)))\n            .or(selected.filter(|&i| handles.get(i).is_some_and(Option::is_some)))\n            .or_else(|| {\n                handles\n                    .iter()\n                    .position(|h| h.is_some() && h.as_ref() == active.read(cx).as_ref())\n            })\n            .or_else(|| handles.iter().position(Option::is_some));\n        if let Some(index) = index {\n            let handle = handles[index].as_ref().unwrap().clone().tab_stop(true);\n            handles[index] = Some(handle.clone());\n            active.update(cx, |value, _| *value = Some(handle));\n        }\n        Self { handles, active }\n    }\n    pub fn key(\n        &self,\n        event: &gpui_kit::KeyDownEvent,\n        axis: Option<gpui_kit::Axis>,\n        window: &mut gpui_kit::Window,\n        cx: &mut App,\n    ) -> Option<usize> {\n        if event.keystroke.modifiers.modified() {\n            return None;\n        }\n        let enabled: Vec<_> = self\n            .handles\n            .iter()\n            .enumerate()\n            .filter_map(|(i, h)| h.as_ref().map(|h| (i, h)))\n            .collect();\n        let current = enabled.iter().position(|(_, h)| h.is_focused(window))?;\n        let next = match event.keystroke.key.as_str() {\n            \"home\" => 0,\n            \"end\" => enabled.len() - 1,\n            \"left\" if axis != Some(gpui_kit::Axis::Vertical) => {\n                (current + enabled.len() - 1) % enabled.len()\n            }\n            \"right\" if axis != Some(gpui_kit::Axis::Vertical) => (current + 1) % enabled.len(),\n            \"up\" if axis != Some(gpui_kit::Axis::Horizontal) => {\n                (current + enabled.len() - 1) % enabled.len()\n            }\n            \"down\" if axis != Some(gpui_kit::Axis::Horizontal) => (current + 1) % enabled.len(),\n            _ => return None,\n        };\n        enabled[current].1.clone().tab_stop(false);\n        let handle = enabled[next].1.clone().tab_stop(true);\n        handle.focus(window, cx);\n        self.active.update(cx, |value, _| *value = Some(handle));\n        window.refresh();\n        cx.stop_propagation();\n        Some(enabled[next].0)\n    }\n}\n"
    },
    {
      "path": "registry/scroll_area/scroll_area.rs",
      "type": "registry:file",
      "target": "~/src/ui/scroll_area.rs",
      "content": "//! Native scrolling with Nova-styled GPUI Kit scrollbars.\n\nuse super::theme::UiTheme;\nuse gpui_kit::base::{Scrollbar, ScrollbarHandle, ScrollbarMode};\nuse gpui_kit::{\n    AnyElement, App, Axis, ElementId, InteractiveElement as _, IntoElement, ParentElement,\n    RenderOnce, Role, ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled, Window,\n    div, prelude::FluentBuilder as _, px,\n};\n\n/// Overlay on a relative container holding a list or scroll viewport.\npub fn scroll_area_scrollbar_for<H: ScrollbarHandle + Clone>(\n    id: impl Into<ElementId>,\n    target: &H,\n    cx: &App,\n) -> Scrollbar {\n    let t = UiTheme::read(cx);\n    Scrollbar::vertical(target)\n        .id(id)\n        .mode(ScrollbarMode::Scrolling)\n        .styles(|styles| {\n            styles\n                // No rail: the thumb floats over content in every state.\n                .track(|style| style.width(t.space(2.5)).bg(gpui_kit::transparent_black()))\n                .track_hover(|style| style.bg(gpui_kit::transparent_black()))\n                .track_active(|style| {\n                    style\n                        .bg(gpui_kit::transparent_black())\n                        .border_color(gpui_kit::transparent_black())\n                })\n                .thumb(|style| {\n                    style\n                        .bg(t.colors.border)\n                        .width(t.space(2.))\n                        .inset(px(1.))\n                        .radius(t.space(1.))\n                })\n                .thumb_hover(|style| style.bg(t.colors.muted_foreground.opacity(0.72)))\n        })\n}\n\n#[derive(IntoElement)]\n/// A native scroll viewport with a Nova-styled Kit scrollbar.\npub struct ScrollArea {\n    id: ElementId,\n    style: gpui_kit::StyleRefinement,\n    label: Option<SharedString>,\n    axis: Axis,\n    scroll: Option<ScrollHandle>,\n    children: Vec<AnyElement>,\n}\nimpl ScrollArea {\n    /// Creates a `ScrollArea` with a stable caller-owned ID.\n    pub fn new(id: impl Into<ElementId>) -> Self {\n        Self {\n            id: id.into(),\n            style: Default::default(),\n            label: None,\n            axis: Axis::Vertical,\n            scroll: None,\n            children: vec![],\n        }\n    }\n    /// Sets the accessible name of the control.\n    pub fn aria_label(mut self, value: impl Into<SharedString>) -> Self {\n        self.label = Some(value.into());\n        self\n    }\n    /// Shares the scroll position with the caller, for example to scroll a\n    /// focused row into view. Without it the area keeps one per ID.\n    pub fn track_scroll(mut self, handle: &ScrollHandle) -> Self {\n        self.scroll = Some(handle.clone());\n        self\n    }\n    /// Selects horizontal or vertical layout and keyboard navigation.\n    pub fn axis(mut self, axis: Axis) -> Self {\n        self.axis = axis;\n        self\n    }\n}\nimpl ParentElement for ScrollArea {\n    fn extend(&mut self, children: impl IntoIterator<Item = AnyElement>) {\n        self.children.extend(children);\n    }\n}\nimpl Styled for ScrollArea {\n    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {\n        &mut self.style\n    }\n}\nimpl RenderOnce for ScrollArea {\n    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {\n        let t = UiTheme::read(cx).clone();\n        let handle = match self.scroll.clone() {\n            Some(handle) => handle,\n            None => window\n                .use_keyed_state((self.id.clone(), \"scroll\"), cx, |_, _| ScrollHandle::new())\n                .read(cx)\n                .clone(),\n        };\n        let keyboard = handle.clone();\n        let axis = self.axis;\n        let viewport = div()\n            .id(\"viewport\")\n            .role(Role::ScrollView)\n            .focusable()\n            .size_full()\n            .track_scroll(&handle)\n            .when(axis == Axis::Vertical, |viewport| {\n                viewport.overflow_y_scroll()\n            })\n            .when(axis == Axis::Horizontal, |viewport| {\n                viewport.overflow_x_scroll()\n            })\n            .when_some(self.label, |viewport, label| viewport.aria_label(label))\n            .on_key_down(move |event, window, cx| {\n                if event.keystroke.modifiers.modified() {\n                    return;\n                }\n                let mut offset = keyboard.offset();\n                let max = if axis == Axis::Vertical {\n                    keyboard.max_offset().y\n                } else {\n                    keyboard.max_offset().x\n                };\n                let page = if axis == Axis::Vertical {\n                    keyboard.bounds().size.height\n                } else {\n                    keyboard.bounds().size.width\n                };\n                let value = if axis == Axis::Vertical {\n                    &mut offset.y\n                } else {\n                    &mut offset.x\n                };\n                *value = match event.keystroke.key.as_str() {\n                    \"up\" if axis == Axis::Vertical => *value + px(32.),\n                    \"down\" if axis == Axis::Vertical => *value - px(32.),\n                    \"left\" if axis == Axis::Horizontal => *value + px(32.),\n                    \"right\" if axis == Axis::Horizontal => *value - px(32.),\n                    \"pageup\" => *value + page,\n                    \"pagedown\" => *value - page,\n                    \"home\" => px(0.),\n                    \"end\" => -max,\n                    _ => return,\n                };\n                *value = (*value).clamp(-max.max(px(0.)), px(0.));\n                keyboard.set_offset(offset);\n                window.refresh();\n                cx.stop_propagation();\n            })\n            .child(div().flex().flex_col().min_w_full().children(self.children));\n        let scrollbar = scroll_area_scrollbar_for(\"scrollbar\", &handle, cx).axis(axis);\n        let root = div()\n            .id(self.id)\n            .relative()\n            .min_h_0()\n            .min_w_0()\n            .font_family(t.fonts.body)\n            .text_color(t.colors.foreground)\n            .child(viewport)\n            .child(scrollbar);\n        super::theme::apply_style(root, &self.style)\n    }\n}\n"
    },
    {
      "path": "registry/tooltip/tooltip.rs",
      "type": "registry:file",
      "target": "~/src/ui/tooltip.rs",
      "content": "//! Nova tooltips for pointer hover and keyboard focus.\nuse super::theme::{UiTheme, primitives::surface};\nuse gpui_kit::base::{Button, ElementExt as _, Tooltip, TooltipOverlay, TooltipRequest};\nuse gpui_kit::{\n    AnyWindowHandle, App, AppContext as _, Bounds, Context, ElementId, Entity, FocusHandle, Global,\n    InteractiveElement as _, ParentElement as _, Pixels, SharedString,\n    StatefulInteractiveElement as _, Subscription, WeakEntity, Window, div,\n};\n\n/// Adds a tooltip to one existing button, preserving a single focus target.\n/// The button must retain its own accessible name.\npub fn tooltip(\n    id: impl Into<ElementId>,\n    label: impl Into<SharedString>,\n    trigger: Button,\n    window: &mut Window,\n    cx: &mut App,\n) -> gpui_kit::Div {\n    let state = window.use_keyed_state(id.into(), cx, TooltipState::new);\n    state.update(cx, |state, _| state.label = label.into());\n    let focus = state.read(cx).focus.clone();\n    let overlay = state.read(cx).overlay.clone();\n    let hover = state.clone();\n    let measured = state.clone();\n    div()\n        .child(\n            trigger\n                .track_focus(&focus)\n                .on_prepaint(move |bounds, _, cx| {\n                    measured.update(cx, |state, _| state.bounds = bounds)\n                })\n                .on_hover(move |hovered, window, cx| {\n                    hover.update(cx, |state, cx| {\n                        state.hovered = *hovered;\n                        state.update_visibility(window, cx);\n                    })\n                }),\n        )\n        .child(overlay)\n}\n\nstruct TooltipState {\n    focus: FocusHandle,\n    overlay: Entity<TooltipOverlay>,\n    bounds: Bounds<Pixels>,\n    label: SharedString,\n    hovered: bool,\n    active: bool,\n    dismissed: bool,\n    _subscriptions: [Subscription; 2],\n}\n\n/// One Escape interceptor for every tooltip in the application, instead of\n/// one keystroke hook per tooltip. Active tooltips register here while shown.\n#[derive(Default)]\nstruct EscapeDismissal {\n    shown: Vec<(AnyWindowHandle, WeakEntity<TooltipState>)>,\n    _subscription: Option<Subscription>,\n}\nimpl Global for EscapeDismissal {}\nimpl EscapeDismissal {\n    fn install(cx: &mut App) {\n        if cx.default_global::<Self>()._subscription.is_some() {\n            return;\n        }\n        let subscription = cx.intercept_keystrokes(|event, window, cx| {\n            if event.keystroke.key != \"escape\" {\n                return;\n            }\n            let handle = window.window_handle();\n            let shown = std::mem::take(&mut cx.global_mut::<Self>().shown);\n            let mut dismissed = false;\n            for (owner, state) in shown {\n                if owner != handle {\n                    cx.global_mut::<Self>().shown.push((owner, state));\n                    continue;\n                }\n                state\n                    .update(cx, |state, cx| {\n                        if state.active {\n                            state.overlay.update(cx, |overlay, cx| overlay.hide(cx));\n                            state.active = false;\n                            state.dismissed = true;\n                            dismissed = true;\n                        }\n                    })\n                    .ok();\n            }\n            if dismissed {\n                cx.stop_propagation();\n            }\n        });\n        cx.global_mut::<Self>()._subscription = Some(subscription);\n    }\n    fn shown(window: &Window, state: WeakEntity<TooltipState>, cx: &mut App) {\n        let handle = window.window_handle();\n        let shown = &mut cx.global_mut::<Self>().shown;\n        shown.retain(|(_, other)| {\n            other.upgrade().is_some() && other.entity_id() != state.entity_id()\n        });\n        shown.push((handle, state));\n    }\n    fn hidden(state: &WeakEntity<TooltipState>, cx: &mut App) {\n        cx.global_mut::<Self>()\n            .shown\n            .retain(|(_, other)| other.entity_id() != state.entity_id());\n    }\n}\n\nimpl TooltipState {\n    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {\n        EscapeDismissal::install(cx);\n        let focus = cx.focus_handle();\n        let subscriptions = [\n            cx.on_focus(&focus, window, |state, window, cx| {\n                state.update_visibility(window, cx)\n            }),\n            cx.on_blur(&focus, window, |state, window, cx| {\n                state.update_visibility(window, cx)\n            }),\n        ];\n        Self {\n            focus,\n            overlay: cx.new(|_| TooltipOverlay::new()),\n            bounds: Bounds::default(),\n            label: SharedString::default(),\n            hovered: false,\n            active: false,\n            dismissed: false,\n            _subscriptions: subscriptions,\n        }\n    }\n    fn update_visibility(&mut self, window: &mut Window, cx: &mut Context<Self>) {\n        let active = self.hovered || self.focus.is_focused(window);\n        if !active {\n            self.dismissed = false;\n        }\n        let active = active && !self.dismissed;\n        if active == self.active {\n            return;\n        }\n        self.active = active;\n        if active {\n            let label = self.label.clone();\n            let request =\n                TooltipRequest::new(self.bounds, move |_, cx| text_tooltip(label.clone(), cx));\n            self.overlay\n                .update(cx, |overlay, cx| overlay.request_show(request, window, cx));\n            EscapeDismissal::shown(window, cx.weak_entity(), cx);\n        } else {\n            self.overlay.update(cx, |overlay, cx| overlay.hide(cx));\n            EscapeDismissal::hidden(&cx.weak_entity(), cx);\n        }\n    }\n}\n\n/// Creates a tooltip view for GPUI's native `.tooltip(...)` attachment point.\n/// Use this on an existing interactive control to avoid nesting another button\n/// and tab stop. The host control must retain its own full accessible name.\npub fn text_tooltip(label: gpui_kit::SharedString, cx: &mut App) -> gpui_kit::AnyView {\n    use gpui_kit::AppContext as _;\n    cx.new(|_| TextTooltip(label)).into()\n}\nstruct TextTooltip(gpui_kit::SharedString);\nimpl gpui_kit::Render for TextTooltip {\n    fn render(\n        &mut self,\n        _: &mut gpui_kit::Window,\n        cx: &mut gpui_kit::Context<Self>,\n    ) -> impl gpui_kit::IntoElement {\n        use gpui_kit::ParentElement as _;\n        let theme = UiTheme::read(cx);\n        surface::tooltip(Tooltip::new(\"tooltip-content\"), theme).child(\n            div()\n                .debug_selector(|| \"tooltip-label\".into())\n                .child(self.0.clone()),\n        )\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use gpui_kit::{IntoElement, Modifiers, Render, Styled, TestAppContext, px};\n    use std::time::Duration;\n\n    #[gpui_kit::test]\n    fn focus_and_hover_show_tooltip_and_escape_dismisses_it(cx: &mut TestAppContext) {\n        struct Probe;\n        impl Render for Probe {\n            fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n                div()\n                    .child(tooltip(\n                        \"test-tooltip\",\n                        \"Help\",\n                        Button::new(\"help\").size(px(100.)).child(\"Help\"),\n                        window,\n                        cx,\n                    ))\n                    .child(Button::new(\"after\").child(\"After\"))\n            }\n        }\n        cx.update(super::super::theme::init);\n        let (_, visual) = cx.add_window_view(|_, _| Probe);\n        visual.update(|window, cx| {\n            window.activate_window();\n            window.draw(cx).clear(cx);\n        });\n        visual.simulate_mouse_move(\n            gpui_kit::point(px(200.), px(200.)),\n            None,\n            Modifiers::default(),\n        );\n        visual.simulate_keystrokes(\"tab\");\n        visual.executor().advance_clock(Duration::from_millis(550));\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert!(visual.debug_bounds(\"tooltip-label\").is_some());\n        visual.simulate_keystrokes(\"escape\");\n        assert!(visual.debug_bounds(\"tooltip-label\").is_none());\n        visual.simulate_keystrokes(\"tab shift-tab\");\n        visual.executor().advance_clock(Duration::from_millis(550));\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert!(visual.debug_bounds(\"tooltip-label\").is_some());\n        visual.simulate_keystrokes(\"tab\");\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert!(visual.debug_bounds(\"tooltip-label\").is_none());\n        // Hover the first control while keyboard focus stays on the second.\n        visual.simulate_mouse_move(\n            gpui_kit::point(px(10.), px(10.)),\n            None,\n            Modifiers::default(),\n        );\n        visual.executor().advance_clock(Duration::from_millis(550));\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert!(visual.debug_bounds(\"tooltip-label\").is_some());\n        visual.simulate_keystrokes(\"escape\");\n        assert!(visual.debug_bounds(\"tooltip-label\").is_none());\n    }\n}\n"
    },
    {
      "path": "registry/virtual_list/virtual_list.rs",
      "type": "registry:file",
      "target": "~/src/ui/virtual_list.rs",
      "content": "//! Virtualized native rows with stable identity, selection and keyboard navigation.\n//!\n//! GPUI owns measurement and scrolling; GPUI Kit owns the scrollbar. Keep this\n//! state in your view. Replace metadata only when data changes, not on every render.\n//! Domain data, loading, filtering and actions remain in the application.\n\nuse super::theme::{\n    UiTheme,\n    primitives::{disabled, text},\n};\nuse gpui_kit::accesskit::Role;\nuse gpui_kit::base::TestSupportExt as _;\nuse gpui_kit::{\n    AnyElement, App, ElementId, FontWeight, InteractiveElement as _, IntoElement, ListOffset,\n    Modifiers, MouseButton, ParentElement as _, Pixels, RenderOnce, SharedString,\n    StatefulInteractiveElement as _, Styled, UniformListScrollHandle, Window, div, point,\n    prelude::FluentBuilder as _, px, uniform_list,\n};\nuse std::{\n    cell::{Cell, RefCell},\n    collections::{HashMap, HashSet},\n    fmt,\n    rc::Rc,\n    sync::Arc,\n};\n\n/// Metadata for one stable row. The renderer supplies its visual content.\n#[derive(Clone, Debug)]\npub struct ListItem {\n    /// Stable application identity, independent of row order.\n    pub id: ElementId,\n    /// Complete accessible name, even when visual text is truncated.\n    pub label: SharedString,\n    /// Whether selection and activation are unavailable.\n    pub disabled: bool,\n}\nimpl ListItem {\n    /// IDs identify data, not its current position. Labels may repeat; IDs may not.\n    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {\n        Self {\n            id: id.into(),\n            label: label.into(),\n            disabled: false,\n        }\n    }\n    /// Disabled rows remain visible but cannot be selected or activated.\n    pub fn disabled(mut self, disabled: bool) -> Self {\n        self.disabled = disabled;\n        self\n    }\n}\n\n/// A rejected replacement leaves items, selection and scroll position unchanged.\n#[derive(Clone, Debug, PartialEq, Eq)]\npub struct DuplicateListItemId(pub ElementId);\nimpl fmt::Display for DuplicateListItemId {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"duplicate list item ID: {:?}\", self.0)\n    }\n}\nimpl std::error::Error for DuplicateListItemId {}\n\n#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]\n/// Whether gestures can select one row or several rows.\npub enum ListSelectionMode {\n    #[default]\n    /// Selection contains at most one enabled row.\n    Single,\n    /// Enable ranges, toggling and select-all.\n    Multiple,\n}\n\n/// Selection operations apply to the current order and current anchor identity.\n#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]\npub enum ListSelectionGesture {\n    #[default]\n    /// Replace selection and move the range anchor.\n    Replace,\n    /// Toggle this row and move the range anchor.\n    Toggle,\n    /// Replace selection with enabled rows between anchor and target.\n    Range,\n    /// Add the current anchor-to-target range to selection.\n    AddRange,\n}\nimpl ListSelectionGesture {\n    fn from_modifiers(modifiers: Modifiers) -> Self {\n        match (modifiers.shift, modifiers.platform || modifiers.control) {\n            (false, false) => Self::Replace,\n            (false, true) => Self::Toggle,\n            (true, false) => Self::Range,\n            (true, true) => Self::AddRange,\n        }\n    }\n}\n\n/// Events contain stable IDs. Read current selection from the view-owned state.\n#[derive(Clone, Debug, PartialEq, Eq)]\npub enum VirtualListEvent {\n    /// A pointer or keyboard selection gesture updated state.\n    SelectionChanged,\n    /// Keyboard navigation moved focus without changing selection.\n    FocusChanged,\n    /// Enter or double-click requested activation of a current enabled row.\n    Activate(ElementId),\n}\n\n/// Facts about the row being rendered. Never use `index` as an action identity.\n#[derive(Clone, Debug)]\npub struct ListRow {\n    /// Current metadata, including stable identity and accessible name.\n    pub item: ListItem,\n    /// Position in the current ordering, for rendering only.\n    pub index: usize,\n    /// Whether this row belongs to selection.\n    pub selected: bool,\n    /// Whether this is the list's active row, independent of keyboard focus.\n    pub focused: bool,\n}\n\nstruct Data {\n    items: Vec<ListItem>,\n    positions: HashMap<ElementId, usize>,\n    selected: HashSet<ElementId>,\n    focused: Option<ElementId>,\n    anchor: Option<ElementId>,\n    mode: ListSelectionMode,\n    height: Pixels,\n}\n\n/// Caller-owned retained list state. Clones refer to the same state, like GPUI's\n/// scroll handles. Use a separate state for each independently scrolling list.\n#[derive(Clone)]\npub struct VirtualListState {\n    data: Rc<RefCell<Data>>,\n    scroll: UniformListScrollHandle,\n    rows_rendered: Rc<Cell<usize>>,\n}\nimpl VirtualListState {\n    /// Build metadata and its ID index once. Only visible rows plus GPUI's\n    /// measurement row are rendered, regardless of the total item count.\n    pub fn new(items: Vec<ListItem>) -> Result<Self, DuplicateListItemId> {\n        let state = Self {\n            data: Rc::new(RefCell::new(Data {\n                items: Vec::new(),\n                positions: HashMap::new(),\n                selected: HashSet::new(),\n                focused: None,\n                anchor: None,\n                mode: ListSelectionMode::Single,\n                height: px(32.),\n            })),\n            scroll: UniformListScrollHandle::new(),\n            rows_rendered: Rc::new(Cell::new(0)),\n        };\n        state.replace_items(items)?;\n        Ok(state)\n    }\n\n    /// Replace metadata atomically. Surviving IDs retain selection and focus.\n    /// The first visible ID and its pixel offset remain anchored. If removed,\n    /// prefer the next surviving old row, then the previous one, then the start.\n    /// This is O(n) work on data changes; rendering does not rebuild the ID index.\n    pub fn replace_items(&self, items: Vec<ListItem>) -> Result<(), DuplicateListItemId> {\n        let mut positions = HashMap::with_capacity(items.len());\n        for (index, item) in items.iter().enumerate() {\n            if positions.insert(item.id.clone(), index).is_some() {\n                return Err(DuplicateListItemId(item.id.clone()));\n            }\n        }\n        let top = self.scroll_top();\n        let mut data = self.data.borrow_mut();\n        let surviving = |start: usize, enabled: bool| {\n            data.items\n                .iter()\n                .skip(start)\n                .chain(data.items.iter().take(start).rev())\n                .filter_map(|item| positions.get(&item.id).copied())\n                .find(|index| !enabled || !items[*index].disabled)\n        };\n        let exact_top = data\n            .items\n            .get(top.item_ix)\n            .and_then(|item| positions.get(&item.id))\n            .copied();\n        let new_top = exact_top\n            .or_else(|| surviving(top.item_ix, false))\n            .unwrap_or(0);\n        let new_focus = data\n            .focused\n            .as_ref()\n            .and_then(|id| {\n                positions\n                    .get(id)\n                    .copied()\n                    .filter(|index| !items[*index].disabled)\n                    .or_else(|| surviving(*data.positions.get(id).unwrap_or(&0), true))\n            })\n            .map(|index| items[index].id.clone());\n        data.selected\n            .retain(|id| positions.get(id).is_some_and(|i| !items[*i].disabled));\n        data.focused = new_focus;\n        if data\n            .anchor\n            .as_ref()\n            .is_none_or(|id| positions.get(id).is_none_or(|i| items[*i].disabled))\n        {\n            data.anchor = data.focused.clone();\n        }\n        data.items = items;\n        data.positions = positions;\n        drop(data);\n        self.scroll_to(ListOffset {\n            item_ix: new_top,\n            offset_in_item: if exact_top.is_some() {\n                top.offset_in_item\n            } else {\n                px(0.)\n            },\n        });\n        Ok(())\n    }\n\n    /// Change selection mode. Switching to Single keeps the focused selected row,\n    /// or the first selected row in current order.\n    pub fn set_selection_mode(&self, mode: ListSelectionMode) {\n        let mut data = self.data.borrow_mut();\n        data.mode = mode;\n        if mode == ListSelectionMode::Single && data.selected.len() > 1 {\n            let keep = data\n                .focused\n                .as_ref()\n                .filter(|id| data.selected.contains(*id))\n                .cloned()\n                .or_else(|| {\n                    data.items\n                        .iter()\n                        .find(|item| data.selected.contains(&item.id))\n                        .map(|item| item.id.clone())\n                });\n            data.selected.clear();\n            data.selected.extend(keep);\n        }\n    }\n\n    /// Set selection from application state, ignoring missing/disabled IDs.\n    /// In Single mode the first supplied valid ID wins. Does not scroll.\n    pub fn set_selected(&self, ids: impl IntoIterator<Item = ElementId>) {\n        let mut data = self.data.borrow_mut();\n        data.selected.clear();\n        for id in ids {\n            if data\n                .positions\n                .get(&id)\n                .is_some_and(|i| !data.items[*i].disabled)\n            {\n                data.selected.insert(id);\n                if data.mode == ListSelectionMode::Single {\n                    break;\n                }\n            }\n        }\n    }\n\n    /// Selected stable IDs in current row order. No full row scan for small selections.\n    pub fn selected(&self) -> Vec<ElementId> {\n        let data = self.data.borrow();\n        let mut ids: Vec<_> = data.selected.iter().cloned().collect();\n        ids.sort_unstable_by_key(|id| data.positions[id]);\n        ids\n    }\n    /// Current selection count without allocating a snapshot.\n    pub fn selected_count(&self) -> usize {\n        self.data.borrow().selected.len()\n    }\n    /// Current focused identity, independent of selection.\n    pub fn focused(&self) -> Option<ElementId> {\n        self.data.borrow().focused.clone()\n    }\n    /// Current row count.\n    pub fn len(&self) -> usize {\n        self.data.borrow().items.len()\n    }\n    /// Whether the current metadata is empty.\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n    /// Resolve an identity against the current data, for application intent handlers.\n    pub fn index_of(&self, id: &ElementId) -> Option<usize> {\n        self.data.borrow().positions.get(id).copied()\n    }\n    /// A metadata snapshot for one current row; this does not build a visual row.\n    pub fn item(&self, index: usize) -> Option<ListItem> {\n        self.data.borrow().items.get(index).cloned()\n    }\n    /// Number of row-renderer calls in the most recent layout, including GPUI's\n    /// measurement row. Useful for native qualification; not a count of all retained IDs.\n    pub fn rows_rendered(&self) -> usize {\n        self.rows_rendered.get()\n    }\n\n    /// Reveal by current identity. Returns false for a missing row.\n    pub fn reveal(&self, id: &ElementId) -> bool {\n        let Some(index) = self.index_of(id) else {\n            return false;\n        };\n        self.scroll_to(ListOffset {\n            item_ix: index,\n            offset_in_item: px(0.),\n        });\n        true\n    }\n    /// Apply a gesture to the current ID/order. Missing or disabled IDs are ignored,\n    /// including callbacks captured before a data replacement.\n    pub fn select(&self, id: &ElementId, gesture: ListSelectionGesture) -> bool {\n        let mut data = self.data.borrow_mut();\n        let Some(&index) = data.positions.get(id) else {\n            return false;\n        };\n        if data.items[index].disabled {\n            return false;\n        }\n        if data.mode == ListSelectionMode::Single || gesture == ListSelectionGesture::Replace {\n            data.selected.clear();\n            data.selected.insert(id.clone());\n            data.anchor = Some(id.clone());\n        } else if gesture == ListSelectionGesture::Toggle {\n            if !data.selected.remove(id) {\n                data.selected.insert(id.clone());\n            }\n            data.anchor = Some(id.clone());\n        } else {\n            let anchor = data\n                .anchor\n                .as_ref()\n                .or(data.focused.as_ref())\n                .and_then(|id| data.positions.get(id))\n                .copied()\n                .unwrap_or(index);\n            let anchor_id = data.items[anchor].id.clone();\n            if gesture == ListSelectionGesture::Range {\n                data.selected.clear();\n            }\n            for index in anchor.min(index)..=anchor.max(index) {\n                let item = &data.items[index];\n                if !item.disabled {\n                    let id = item.id.clone();\n                    data.selected.insert(id);\n                }\n            }\n            data.anchor = Some(anchor_id);\n        }\n        data.focused = Some(id.clone());\n        true\n    }\n\n    fn configure(&self, height: Pixels) {\n        let top = self.scroll_top();\n        if self.data.borrow().height != height {\n            self.data.borrow_mut().height = height;\n            self.scroll_to(top);\n        }\n    }\n\n    fn scroll_top(&self) -> ListOffset {\n        let height = self.data.borrow().height;\n        let offset = (-self.scroll.0.borrow().base_handle.offset().y).max(px(0.));\n        let item_ix = (f32::from(offset.max(px(0.))) / f32::from(height)).floor() as usize;\n        ListOffset {\n            item_ix,\n            offset_in_item: offset - height * item_ix,\n        }\n    }\n\n    fn scroll_to(&self, top: ListOffset) {\n        let height = self.data.borrow().height;\n        self.scroll\n            .0\n            .borrow()\n            .base_handle\n            .set_offset(point(px(0.), -(height * top.item_ix + top.offset_in_item)));\n    }\n\n    fn reveal_nearest(&self, index: usize) {\n        let height = self.data.borrow().height;\n        let scroll = self.scroll.0.borrow();\n        let viewport = scroll\n            .last_item_size\n            .map_or(px(0.), |size| size.item.height);\n        let offset = -scroll.base_handle.offset().y;\n        let top = height * index;\n        let next = if top < offset || viewport <= px(0.) {\n            top\n        } else if top + height > offset + viewport {\n            top + height - viewport\n        } else {\n            return;\n        };\n        scroll.base_handle.set_offset(point(px(0.), -next));\n    }\n\n    fn row(&self, index: usize) -> Option<ListRow> {\n        let data = self.data.borrow();\n        let item = data.items.get(index)?.clone();\n        Some(ListRow {\n            selected: data.selected.contains(&item.id),\n            focused: data.focused.as_ref() == Some(&item.id),\n            item,\n            index,\n        })\n    }\n\n    fn key(&self, key: &str, modifiers: Modifiers) -> Option<VirtualListEvent> {\n        let data = self.data.borrow();\n        let command = modifiers.platform || modifiers.control;\n        if modifiers.alt {\n            return None;\n        }\n        if key == \"a\" && command && data.mode == ListSelectionMode::Multiple {\n            drop(data);\n            let mut data = self.data.borrow_mut();\n            data.selected = data\n                .items\n                .iter()\n                .filter(|item| !item.disabled)\n                .map(|item| item.id.clone())\n                .collect();\n            return Some(VirtualListEvent::SelectionChanged);\n        }\n        let current = data\n            .focused\n            .as_ref()\n            .and_then(|id| data.positions.get(id))\n            .copied();\n        if key == \"enter\" {\n            return current\n                .filter(|i| !data.items[*i].disabled)\n                .map(|i| VirtualListEvent::Activate(data.items[i].id.clone()));\n        }\n        if key == \"space\" {\n            let id = current.map(|i| data.items[i].id.clone())?;\n            drop(data);\n            self.select(\n                &id,\n                if modifiers.shift {\n                    ListSelectionGesture::Range\n                } else {\n                    ListSelectionGesture::Toggle\n                },\n            );\n            return Some(VirtualListEvent::SelectionChanged);\n        }\n        let count = data.items.len();\n        if count == 0 {\n            return None;\n        }\n        let page = (f32::from(\n            self.scroll\n                .0\n                .borrow()\n                .last_item_size\n                .map_or(px(0.), |size| size.item.height),\n        ) / f32::from(data.height))\n        .floor()\n        .max(1.) as usize;\n        let (start, reverse) = match key {\n            \"up\" => (current.map_or(count - 1, |i| i.saturating_sub(1)), true),\n            \"down\" => (current.map_or(0, |i| (i + 1).min(count - 1)), false),\n            \"home\" => (0, false),\n            \"end\" => (count - 1, true),\n            \"pageup\" => (current.unwrap_or(0).saturating_sub(page), true),\n            \"pagedown\" => (\n                current.unwrap_or(0).saturating_add(page).min(count - 1),\n                false,\n            ),\n            _ => return None,\n        };\n        let index = if reverse {\n            (0..=start).rev().find(|i| !data.items[*i].disabled)\n        } else {\n            (start..count).find(|i| !data.items[*i].disabled)\n        }\n        .or(current)?;\n        let id = data.items[index].id.clone();\n        drop(data);\n        if command && !modifiers.shift {\n            self.data.borrow_mut().focused = Some(id);\n        } else {\n            self.select(&id, ListSelectionGesture::from_modifiers(modifiers));\n        }\n        self.reveal_nearest(index);\n        Some(if command && !modifiers.shift {\n            VirtualListEvent::FocusChanged\n        } else {\n            VirtualListEvent::SelectionChanged\n        })\n    }\n}\n\ntype Renderer = Rc<dyn Fn(&ListRow, &mut Window, &mut App) -> AnyElement>;\ntype EventHandler = Rc<dyn Fn(&VirtualListEvent, &mut Window, &mut App)>;\n\n/// A single-tab-stop listbox with lazy, caller-rendered rows. Arrow/Home/End/Page\n/// keys move selection; Shift extends ranges; Cmd/Ctrl toggles or selects all;\n/// Enter and double-click emit activation. Place noninteractive row content or\n/// existing ContextMenu parts inside rows, and keep other controls outside.\n#[derive(IntoElement)]\npub struct VirtualList {\n    id: ElementId,\n    label: SharedString,\n    state: VirtualListState,\n    render_item: Renderer,\n    on_event: Option<EventHandler>,\n    height: Option<Pixels>,\n    empty: Option<AnyElement>,\n    style: gpui_kit::StyleRefinement,\n}\nimpl VirtualList {\n    /// Compose a list from retained state and a visible-row renderer.\n    pub fn new(\n        id: impl Into<ElementId>,\n        label: impl Into<SharedString>,\n        state: VirtualListState,\n        render_item: impl Fn(&ListRow, &mut Window, &mut App) -> AnyElement + 'static,\n    ) -> Self {\n        Self {\n            id: id.into(),\n            label: label.into(),\n            state,\n            render_item: Rc::new(render_item),\n            on_event: None,\n            height: None,\n            empty: None,\n            style: Default::default(),\n        }\n    }\n    /// Fixed row height; defaults to eight spacing units. Invalid heights fall\n    /// back to the theme default. Keep text density usable when overriding it.\n    pub fn row_height(mut self, height: Pixels) -> Self {\n        self.height = Some(height);\n        self\n    }\n    /// Observe live selection/focus or perform an application-owned activation.\n    /// State is updated before this callback; it may replace selection or data.\n    pub fn on_event(\n        mut self,\n        handler: impl Fn(&VirtualListEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_event = Some(Rc::new(handler));\n        self\n    }\n    /// Compose loading/error/empty content from existing components. It appears\n    /// only when the caller supplies no rows; this component does not load data.\n    pub fn empty(mut self, content: impl IntoElement) -> Self {\n        self.empty = Some(content.into_any_element());\n        self\n    }\n}\nimpl Styled for VirtualList {\n    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {\n        &mut self.style\n    }\n}\nimpl RenderOnce for VirtualList {\n    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {\n        let theme = UiTheme::read(cx).clone();\n        let default_height = theme.spacing.unit * 8_f32;\n        let height = self\n            .height\n            .filter(|h| f32::from(*h).is_finite() && *h >= px(1.))\n            .unwrap_or(default_height);\n        self.state.configure(height);\n        self.state.rows_rendered.set(0);\n        let focus = window\n            .use_keyed_state(\n                ElementId::NamedChild(Arc::new(self.id.clone()), \"focus\".into()),\n                cx,\n                |_, cx| cx.focus_handle(),\n            )\n            .read(cx)\n            .clone();\n        let state = self.state.clone();\n        let change = self.on_event.clone();\n        let key_focus = focus.clone();\n        let instructions = if self.state.data.borrow().mode == ListSelectionMode::Multiple {\n            \"Multi-select list. Use arrows to navigate, Shift for a range, Command or Control to toggle.\"\n        } else {\n            \"Use arrow keys to navigate and Enter to activate.\"\n        };\n        let mut root = div()\n            .id(self.id.clone())\n            .test_support()\n            .role(Role::ListBox)\n            .aria_label(self.label)\n            .aria_description(instructions)\n            .border_1()\n            .border_color(theme.colors.background.opacity(0.))\n            .when(self.state.focused().is_none(), |el| {\n                el.focus_visible(move |el| el.border_color(theme.colors.ring))\n            })\n            .track_focus(&focus.clone().tab_stop(true))\n            .relative()\n            .size_full()\n            .min_w_0()\n            .min_h_0()\n            .overflow_hidden()\n            .map(|el| text::sm(el, &theme))\n            .bg(theme.colors.background)\n            .text_color(theme.colors.foreground)\n            .on_key_down(move |event, window, cx| {\n                if !key_focus.is_focused(window) {\n                    return;\n                }\n                if let Some(event) = state.key(&event.keystroke.key, event.keystroke.modifiers) {\n                    window.refresh();\n                    if let Some(change) = &change {\n                        change(&event, window, cx);\n                    }\n                    window.prevent_default();\n                    cx.stop_propagation();\n                }\n            });\n        if self.state.is_empty() {\n            root = root.child(self.empty.unwrap_or_else(|| {\n                div()\n                    .p(theme.spacing.unit * 4_f32)\n                    .text_color(theme.colors.muted_foreground)\n                    .child(\"No items\")\n                    .into_any_element()\n            }));\n        } else {\n            let state = self.state.clone();\n            let total = state.len();\n            let renderer = self.render_item;\n            let change = self.on_event;\n            let rows_theme = theme.clone();\n            let rows = uniform_list(\"virtual-list.rows\", total, move |range, window, cx| {\n                range\n                    .map(|index| {\n                        let Some(row) = state.row(index) else {\n                            return div().into_any_element();\n                        };\n                        state.rows_rendered.set(state.rows_rendered.get() + 1);\n                        let id = row.item.id.clone();\n                        let down_state = state.clone();\n                        let down_id = id.clone();\n                        let down_focus = focus.clone();\n                        let down_change = change.clone();\n                        let left_state = state.clone();\n                        let left_id = id.clone();\n                        let left_focus = focus.clone();\n                        let left_change = change.clone();\n                        let activate_state = state.clone();\n                        let activate_change = change.clone();\n                        let has_focus = focus.is_focused(window);\n                        let content = renderer(&row, window, cx);\n                        let tooltip_label = row.item.label.clone();\n                        let mut element = div()\n                            .id(id.clone())\n                            .test_support()\n                            .role(Role::ListBoxOption)\n                            .aria_label(row.item.label.clone())\n                            .tooltip(move |_, cx| {\n                                super::tooltip::text_tooltip(tooltip_label.clone(), cx)\n                            })\n                            .aria_selected(row.selected)\n                            .aria_position_in_set(index + 1)\n                            .aria_size_of_set(total)\n                            .when(row.focused, |el| el.aria_active_descendant())\n                            .when(row.item.disabled, |el| {\n                                disabled::dim(el.aria_description(\"Unavailable\"), true)\n                            })\n                            .w_full()\n                            .min_w_0()\n                            .flex()\n                            .items_center()\n                            .gap(rows_theme.spacing.unit * 2_f32)\n                            .px(rows_theme.spacing.unit * 2_f32)\n                            .h(height)\n                            .overflow_hidden()\n                            .rounded(rows_theme.radius.md)\n                            .border_1()\n                            .border_color(rows_theme.colors.background.opacity(0.))\n                            .when(row.selected, |el| {\n                                el.bg(rows_theme.colors.accent)\n                                    .text_color(rows_theme.colors.accent_foreground)\n                            })\n                            .when(!row.item.disabled, |el| {\n                                el.hover(|el| el.bg(rows_theme.colors.accent))\n                            })\n                            .when(row.focused && has_focus, |el| {\n                                el.border_color(rows_theme.colors.ring)\n                            })\n                            .font_weight(if row.selected {\n                                FontWeight::MEDIUM\n                            } else {\n                                FontWeight::NORMAL\n                            })\n                            .capture_any_mouse_down(move |event, window, cx| {\n                                if event.button != MouseButton::Right\n                                    || (down_focus.contains_focused(window, cx)\n                                        && !down_focus.is_focused(window))\n                                {\n                                    return;\n                                }\n                                let valid = down_state\n                                    .index_of(&down_id)\n                                    .and_then(|i| down_state.item(i))\n                                    .is_some_and(|item| !item.disabled);\n                                if !valid {\n                                    return;\n                                }\n                                down_focus.focus(window, cx);\n                                let keep_selection =\n                                    down_state.data.borrow().selected.contains(&down_id);\n                                if keep_selection {\n                                    down_state.data.borrow_mut().focused = Some(down_id.clone());\n                                } else {\n                                    down_state.select(&down_id, ListSelectionGesture::Replace);\n                                }\n                                window.refresh();\n                                if let Some(change) = &down_change {\n                                    change(&VirtualListEvent::SelectionChanged, window, cx);\n                                }\n                            })\n                            .on_mouse_down(MouseButton::Left, move |event, window, cx| {\n                                // A menu owns descendant focus while its portal is open.\n                                if left_focus.contains_focused(window, cx)\n                                    && !left_focus.is_focused(window)\n                                {\n                                    return;\n                                }\n                                if left_state.select(\n                                    &left_id,\n                                    ListSelectionGesture::from_modifiers(event.modifiers),\n                                ) {\n                                    left_focus.focus(window, cx);\n                                    window.refresh();\n                                    if let Some(change) = &left_change {\n                                        change(&VirtualListEvent::SelectionChanged, window, cx);\n                                    }\n                                }\n                            })\n                            .on_click(move |event, window, cx| {\n                                if event.click_count() == 2\n                                    && activate_state\n                                        .index_of(&id)\n                                        .and_then(|i| activate_state.item(i))\n                                        .is_some_and(|item| !item.disabled)\n                                    && let Some(change) = &activate_change\n                                {\n                                    change(&VirtualListEvent::Activate(id.clone()), window, cx);\n                                }\n                            })\n                            .child(content);\n                        if !row.item.disabled {\n                            for action in [\n                                gpui_kit::accesskit::Action::Click,\n                                gpui_kit::accesskit::Action::Focus,\n                            ] {\n                                let state = state.clone();\n                                let id = row.item.id.clone();\n                                let focus = focus.clone();\n                                let change = change.clone();\n                                element = element.on_a11y_action(action, move |_, window, cx| {\n                                    let valid = state\n                                        .index_of(&id)\n                                        .and_then(|i| state.item(i))\n                                        .is_some_and(|item| !item.disabled);\n                                    if !valid {\n                                        return;\n                                    }\n                                    let event = if action == gpui_kit::accesskit::Action::Focus {\n                                        state.data.borrow_mut().focused = Some(id.clone());\n                                        VirtualListEvent::FocusChanged\n                                    } else {\n                                        state.select(&id, ListSelectionGesture::Replace);\n                                        VirtualListEvent::SelectionChanged\n                                    };\n                                    {\n                                        focus.focus(window, cx);\n                                        if let Some(index) = state.index_of(&id) {\n                                            state.reveal_nearest(index);\n                                        }\n                                        window.refresh();\n                                        if let Some(change) = &change {\n                                            change(&event, window, cx);\n                                        }\n                                    }\n                                });\n                            }\n                        }\n                        element.into_any_element()\n                    })\n                    .collect::<Vec<_>>()\n            })\n            .track_scroll(&self.state.scroll)\n            .size_full();\n            root = root\n                .child(rows)\n                .child(super::scroll_area::scroll_area_scrollbar_for(\n                    ElementId::NamedChild(Arc::new(self.id), \"scrollbar\".into()),\n                    &self.state.scroll,\n                    cx,\n                ));\n        }\n        super::theme::apply_style(root, &self.style)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use gpui_kit::{\n        AppContext as _, Context, Render, TestAppContext, VisualTestContext, point, size,\n    };\n\n    fn items(ids: &[u64]) -> Vec<ListItem> {\n        ids.iter()\n            .map(|id| ListItem::new((\"row\", *id), format!(\"Row {id}\")))\n            .collect()\n    }\n    fn id(value: u64) -> ElementId {\n        (\"row\", value).into()\n    }\n\n    #[test]\n    fn selection_uses_current_identity_order_and_disabled_guards() {\n        let state = VirtualListState::new(items(&[1, 2, 3, 4, 5])).unwrap();\n        state.set_selection_mode(ListSelectionMode::Multiple);\n        state.select(&id(2), ListSelectionGesture::Replace);\n        state.select(&id(4), ListSelectionGesture::Range);\n        assert_eq!(state.selected(), [id(2), id(3), id(4)]);\n        state.select(&id(3), ListSelectionGesture::Toggle);\n        assert_eq!(state.selected(), [id(2), id(4)]);\n        let mut next = items(&[5, 3, 2, 4]);\n        next[2].disabled = true;\n        state.replace_items(next).unwrap();\n        assert_eq!(state.selected(), [id(4)]);\n        assert!(!state.select(&id(1), ListSelectionGesture::Replace));\n        assert!(!state.select(&id(2), ListSelectionGesture::Replace));\n        state.select(&id(5), ListSelectionGesture::Range);\n        assert_eq!(state.selected(), [id(5), id(3)]);\n        state.set_selection_mode(ListSelectionMode::Single);\n        assert_eq!(state.selected(), [id(5)]);\n        state.set_selected([id(2), id(4), id(3)]);\n        assert_eq!(state.selected(), [id(4)]);\n    }\n\n    #[test]\n    fn replacements_preserve_viewport_and_reject_duplicates_atomically() {\n        let state = VirtualListState::new(items(&[1, 2, 3, 4, 5])).unwrap();\n        state.select(&id(3), ListSelectionGesture::Replace);\n        state.scroll_to(ListOffset {\n            item_ix: 2,\n            offset_in_item: px(9.),\n        });\n        state.replace_items(items(&[5, 3, 2, 1, 4])).unwrap();\n        let top = state.scroll_top();\n        assert_eq!((top.item_ix, top.offset_in_item), (1, px(9.)));\n        assert_eq!(state.focused(), Some(id(3)));\n        assert_eq!(\n            state.replace_items(items(&[8, 8])),\n            Err(DuplicateListItemId(id(8)))\n        );\n        assert_eq!(state.selected(), [id(3)]);\n        assert_eq!(state.len(), 5);\n        state.replace_items(items(&[5, 4, 2])).unwrap();\n        assert_eq!(state.focused(), Some(id(2))); // Next surviving old identity.\n        assert!(state.selected().is_empty());\n        let top = state.scroll_top();\n        assert_eq!((top.item_ix, top.offset_in_item), (2, px(0.)));\n        state.replace_items(Vec::new()).unwrap();\n        assert_eq!(state.focused(), None);\n        assert!(!state.reveal(&id(3)));\n    }\n\n    struct View {\n        state: VirtualListState,\n        activated: Option<ElementId>,\n    }\n    impl Render for View {\n        fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n            VirtualList::new(\"list\", \"Test rows\", self.state.clone(), move |row, _, _| {\n                div().child(row.item.label.clone()).into_any_element()\n            })\n            .on_event(cx.listener(|view, event, _, _| {\n                if let VirtualListEvent::Activate(id) = event {\n                    view.activated = Some(id.clone());\n                }\n            }))\n        }\n    }\n\n    #[test]\n    fn large_list_virtualizes_reveals_and_handles_real_keyboard_and_pointer_input() {\n        {\n            // Declared first so it is dropped after `cx`: GPUI Kit 0.8 deadlocks when\n            // an app is dropped after its test scheduler.\n            let dispatcher = gpui_kit::TestDispatcher::new(0);\n            let mut cx = TestAppContext::build(dispatcher.clone(), None);\n            cx.update(|cx| UiTheme::set(cx, UiTheme::neutral_light()));\n            let state = VirtualListState::new(\n                (0..100_000u64)\n                    .map(|i| ListItem::new((\"row\", i), format!(\"Row {i}\")).disabled(i == 2))\n                    .collect(),\n            )\n            .unwrap();\n            state.set_selection_mode(ListSelectionMode::Multiple);\n            let window = cx.add_window({\n                let state = state.clone();\n                move |_, _| View {\n                    state,\n                    activated: None,\n                }\n            });\n            let mut visual = VisualTestContext::from_window(window.into(), &cx);\n            visual.simulate_resize(size(px(480.), px(320.)));\n            let draw = |cx: &mut TestAppContext| {\n                cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx))\n                    .unwrap();\n            };\n            draw(&mut cx);\n            assert!(\n                state.rows_rendered() > 0 && state.rows_rendered() < 40,\n                \"{} rows built\",\n                state.rows_rendered()\n            );\n            visual.simulate_click(point(px(60.), px(15.)), Modifiers::default());\n            visual.simulate_keystrokes(\"down shift-down\");\n            draw(&mut cx);\n            assert_eq!(state.selected(), [id(1), id(3)]);\n            assert_eq!(state.focused(), Some(id(3)));\n            visual.simulate_keystrokes(\"enter\");\n            assert_eq!(\n                cx.read_window(&window, |view, cx| view.read(cx).activated.clone())\n                    .unwrap(),\n                Some(id(3))\n            );\n            state.reveal(&id(99_999));\n            draw(&mut cx);\n            assert!(state.scroll_top().item_ix <= 99_999 && state.scroll_top().item_ix >= 99_989);\n            assert!(state.rows_rendered() < 40);\n            visual.simulate_keystrokes(\"end\");\n            draw(&mut cx);\n            assert_eq!(state.focused(), Some(id(99_999)));\n            assert!(state.scroll_top().item_ix <= 99_999 && state.scroll_top().item_ix >= 99_989);\n            visual.simulate_keystrokes(\"home\");\n            draw(&mut cx);\n            assert_eq!(state.focused(), Some(id(0)));\n            // Selection callback from the previous frame must not target the new\n            // row occupying its old index after replacement.\n            state.replace_items(items(&[11, 12])).unwrap();\n            assert!(!state.select(&id(0), ListSelectionGesture::Replace));\n            assert!(!state.selected().contains(&id(11)));\n            // Let GPUI Kit's scrollbar idle timer release its runtime handle.\n            cx.run_until_parked();\n            cx.update_window(window.into(), |_, window, _| window.remove_window())\n                .unwrap();\n            cx.executor()\n                .advance_clock(std::time::Duration::from_secs(3));\n            cx.run_until_parked();\n        }\n    }\n}\n"
    }
  ]
}
