Skip to main content

herogpui_components/
util.rs

1//! Shared render helpers for HeroGPUI components.
2
3use gpui::{App, BorrowAppContext, Div, Hsla, ParentElement, Pixels, Refineable, Styled};
4use herogpui_core::{element_id, FieldVariant};
5use herogpui_theme::ActiveTheme;
6
7// Browser hosts register the bundled mono family before opening a window.
8pub(crate) const MONO_FONT: &str = if cfg!(target_family = "wasm") {
9    "JetBrains Mono"
10} else {
11    "Consolas"
12};
13
14/// The one height every v3 form field has: `.date-input-group` and
15/// `.color-input-group` are `h-9`, and `.input`'s `py-2` plus its line box comes
16/// to the same 36px. v3 removed `size` from the field
17/// components (Input, Select, ComboBox, DateField, ...), keeping it only on the
18/// nineteen where a scale is documented, so a field's metrics are constants
19/// rather than a [`herogpui_core::Size`] lookup.
20pub const FIELD_HEIGHT: Pixels = gpui::px(36.);
21/// Type size inside a form field.
22pub const FIELD_TEXT: Pixels = gpui::px(14.);
23/// Glyph size for an icon inside a form field.
24pub const FIELD_ICON: Pixels = gpui::px(16.);
25
26/// HeroUI's numeric outputs use `tabular-nums` so changing a value does not
27/// move the label row. GPUI exposes the equivalent OpenType feature directly;
28/// keep the construction here so every numeric component uses the same
29/// feature tag and no component falls back to proportional figures.
30pub(crate) fn tabular_font_features() -> gpui::FontFeatures {
31    gpui::FontFeatures(std::sync::Arc::new(vec![("tnum".to_owned(), 1)]))
32}
33
34/// Optional box geometry and chrome overrides shared by the field family.
35///
36/// `Input` keeps its own copies — its grouped padding rules predate this
37/// struct — and `DateField` keeps its embedded bare flags; the rest of the
38/// family stores one of these and resolves the values in render. All defaults
39/// reproduce the stock field box exactly.
40#[derive(Clone, Copy, Debug, Default)]
41pub(crate) struct FieldBox {
42    pub(crate) height: Option<Pixels>,
43    pub(crate) padding_x: Option<Pixels>,
44    /// Vertical padding, in place of the family's unpadded `min-h` box.
45    ///
46    /// Only meaningful where the box's content can grow past one line — v3's
47    /// select trigger carries `py-2` for exactly that reason, and the rest of
48    /// the family holds a single-line editor whose height `height` already
49    /// names. `None` reproduces the stock unpadded box everywhere.
50    pub(crate) padding_y: Option<Pixels>,
51    pub(crate) is_bare: bool,
52    pub(crate) is_bare_is_set: bool,
53    /// Whether the focused field should paint its focus ring. `None` keeps the
54    /// stock treatment; `Some(false)` hides only that visual indicator while
55    /// retaining focus, editing and validation feedback.
56    pub(crate) focus_ring: Option<bool>,
57}
58
59impl FieldBox {
60    /// The explicit single-line height, or the stock [`FIELD_HEIGHT`].
61    pub(crate) fn resolved_height(&self) -> Pixels {
62        self.height.unwrap_or(FIELD_HEIGHT)
63    }
64
65    /// The explicit horizontal padding, or v3's `px-3`.
66    pub(crate) fn resolved_padding_x(&self) -> Pixels {
67        self.padding_x.unwrap_or(gpui::px(12.))
68    }
69}
70
71// v3 does not have one "control" radius: each component names its own step, and
72// they span the whole scale. `design_audit.py` diffs these against the real
73// stylesheets, so the mapping here is checked rather than asserted.
74
75/// Applies the theme's interactive cursor (`--cursor-interactive`) to `el`.
76///
77/// The token replacement for GPUI's `cursor_pointer()`: v3 puts
78/// `cursor: pointer` on every clickable control, and a theme overrides all of
79/// them at once through [`LayoutTheme::cursor_interactive`].
80///
81/// Sites that need the value inside a closure GPUI does not hand a `cx` — a
82/// `when` or `hover` body — read [`interactive_cursor`] first and call
83/// `Styled::cursor` with the copy.
84///
85/// [`LayoutTheme::cursor_interactive`]: herogpui_theme::LayoutTheme::cursor_interactive
86pub fn cursor_interactive<T: Styled>(el: T, cx: &App) -> T {
87    el.cursor(interactive_cursor(cx))
88}
89
90/// The theme's interactive cursor, for a site that must capture it by value.
91pub fn interactive_cursor(cx: &App) -> gpui::CursorStyle {
92    cx.layout().cursor_interactive
93}
94
95/// `rounded-3xl` — buttons, toggle buttons and avatars.
96pub fn control_radius(cx: &App) -> Pixels {
97    let layout = cx.layout();
98    layout.capped(layout.radius_3xl())
99}
100
101/// `rounded-2xl` — chips, menu and list rows, the colour area.
102pub fn soft_radius(cx: &App) -> Pixels {
103    let layout = cx.layout();
104    layout.capped(layout.radius_2xl())
105}
106
107/// `rounded-xl` — close buttons, tags, links, tooltips.
108pub fn small_radius(cx: &App) -> Pixels {
109    let layout = cx.layout();
110    layout.capped(layout.radius_xl())
111}
112
113/// `rounded-md` — the checkbox control (`.checkbox__control` is
114/// `size-4 rounded-md`).
115pub fn mark_radius(cx: &App) -> Pixels {
116    let layout = cx.layout();
117    layout.capped(layout.radius_md())
118}
119
120/// `rounded-lg` — the keyboard key and the radio control, which v3 draws as a
121/// rounded square rather than a circle.
122pub fn key_radius(cx: &App) -> Pixels {
123    let layout = cx.layout();
124    layout.capped(layout.radius_lg())
125}
126
127/// `rounded-sm` — separators and skeletons, which are nearly square.
128pub fn hairline_radius(cx: &App) -> Pixels {
129    let layout = cx.layout();
130    layout.capped(layout.radius_sm())
131}
132
133/// `rounded-xs` — the small ProgressBar track.
134pub fn micro_radius(cx: &App) -> Pixels {
135    let layout = cx.layout();
136    layout.capped(layout.radius_xs())
137}
138
139/// Corner radius of a form field — `--field-radius`.
140pub fn field_radius(cx: &App) -> Pixels {
141    let layout = cx.layout();
142    layout.capped(layout.field_radius)
143}
144
145/// `min(32px, --radius-3xl)` — cards, the table, and every floating panel
146/// (modal, popover, toast, alert, dropdown). Surface is not on the list:
147/// upstream `.surface` declares no radius.
148pub fn container_radius(cx: &App) -> Pixels {
149    let layout = cx.layout();
150    layout.capped(layout.radius_3xl())
151}
152
153/// Corner radius for a fill painted *inside* a rounded `overflow_hidden` clip
154/// parent on vanilla GPUI.
155///
156/// Upstream clips `overflow_hidden()` to the parent's rectangle even when the
157/// parent is rounded, so a square child fill or image squares off the
158/// parent's corners (the retired renderer fork in
159/// `docs/upstream/retired-patches/` fixed this for every component at once).
160/// Until that lands upstream, each clipped fill carries the same radius as
161/// its clip parent: the fill's own rounded corners hide the square bleed.
162/// Subtract the border width when the parent draws one, the way a CSS
163/// `border-radius` shrinks inward; clamps at zero so a hairline radius never
164/// goes negative.
165///
166/// Where a whole stack of fills shares one clip (the color area, the color
167/// slider track, checkerboards), each layer repeats the radius: GPUI's `Svg`
168/// element is not an alternative here, it renders through an alpha mask
169/// (`Window::paint_svg` → `render_alpha_mask`), so a multicolor gradient SVG
170/// can only ever paint a monochrome silhouette.
171pub fn inner_fill_radius(outer: Pixels, border: Pixels) -> Pixels {
172    (outer - border).max(gpui::px(0.))
173}
174
175/// Shift+wheel horizontal scroll for mouse wheels on the web platform.
176///
177/// A mouse wheel reports only `deltaY`; every desktop platform reads
178/// shift+wheel as the horizontal axis, and GPUI's native platforms deliver it
179/// that way. The web platform forwards the raw axes instead, so a
180/// horizontally scrollable region — the `Tabs` strip, a wide `Table` — cannot
181/// be wheel-scrolled there at all, with no fallback. Until the platform fix
182/// lands upstream (see `docs/upstream/retired-patches/`), horizontal
183/// scrollers call this from `on_scroll_wheel`: when shift is held and the
184/// event carries no horizontal component, the vertical delta drives the
185/// handle's x offset instead, matching the platform sign convention (offsets
186/// go negative as content scrolls down/right).
187///
188/// Web-only by construction (`cfg(target_family = "wasm")`): native platforms
189/// already translate, so their events keep flowing through `track_scroll`
190/// untouched. A `Lines` delta (Firefox mouse wheels) converts at 16px/line.
191pub fn shift_wheel_scroll_x(handle: &gpui::ScrollHandle, event: &gpui::ScrollWheelEvent) {
192    #[cfg(target_family = "wasm")]
193    {
194        if !event.modifiers.shift {
195            return;
196        }
197        let vertical = match event.delta {
198            gpui::ScrollDelta::Pixels(delta) => {
199                if delta.x != gpui::px(0.) {
200                    return;
201                }
202                delta.y
203            }
204            gpui::ScrollDelta::Lines(delta) => {
205                if delta.x != 0.0 {
206                    return;
207                }
208                gpui::px(delta.y * 16.0)
209            }
210        };
211        if vertical == gpui::px(0.) {
212            return;
213        }
214        let at = handle.offset();
215        handle.set_offset(gpui::point(at.x + vertical, at.y));
216    }
217    #[cfg(not(target_family = "wasm"))]
218    {
219        let _ = (handle, event);
220    }
221}
222
223/// Applies the v3 field chrome: background, radius, border and — for
224/// `primary` only — the `--field-shadow`.
225///
226/// The chrome paints the caller's resolved radius, so a per-component
227/// `radius` override survives it: pass `Some` with the value the override
228/// resolved to, or `None` to keep the shared `field_radius` helper.
229///
230/// Generic over [`Styled`] so a field that needed an `.id()` first (and is
231/// therefore a `Stateful<Div>`) can share it. Six components used to hand-roll
232/// this, and every one of them filled the `secondary` variant with
233/// `surface_secondary` instead of `--default`.
234pub fn apply_field_chrome<T: Styled>(
235    el: T,
236    variant: FieldVariant,
237    is_invalid: bool,
238    is_focused: bool,
239    radius_override: Option<Pixels>,
240    cx: &App,
241) -> T {
242    apply_field_chrome_with_focus_ring(
243        el,
244        variant,
245        is_invalid,
246        is_focused,
247        true,
248        radius_override,
249        cx,
250    )
251}
252
253/// Applies the v3 field chrome with an explicit focus-ring switch.
254///
255/// `show_focus_ring` changes only the focused visual treatment. The element
256/// remains focusable and editable, and an invalid field still reports its
257/// danger state through the one-pixel border when the focus ring is disabled.
258pub fn apply_field_chrome_with_focus_ring<T: Styled>(
259    el: T,
260    variant: FieldVariant,
261    is_invalid: bool,
262    is_focused: bool,
263    show_focus_ring: bool,
264    radius_override: Option<Pixels>,
265    cx: &App,
266) -> T {
267    let colors = cx.colors();
268    let layout = cx.layout();
269
270    let radius = radius_override.unwrap_or_else(|| field_radius(cx));
271    let mut el = el.rounded(radius).bg(match variant {
272        FieldVariant::Primary => colors.field.background,
273        // `.input--secondary` sets `--input-bg: var(--default)` and drops the
274        // shadow. This used to use `surface_secondary`, which is a different
275        // token (oklch 95.24% vs 94% in light mode) and a shade too light.
276        FieldVariant::Secondary => colors.default.color,
277    });
278
279    // `--field-border-width` is 0, so a field's states are *rings*, not borders:
280    // `status-focused-field` is `ring-2 ring-focus` with no offset, and
281    // `status-invalid-field` is a 1px danger outline that becomes a 2px danger
282    // ring once the field takes focus. Both ride on the field's own shadow,
283    // because `shadow()` replaces the list rather than adding to it.
284    let mut shadows = if variant == FieldVariant::Primary {
285        layout.field_shadow.clone()
286    } else {
287        Vec::new()
288    };
289
290    if is_invalid {
291        if is_focused && show_focus_ring {
292            shadows.push(gpui::BoxShadow {
293                color: colors.danger.color,
294                offset: gpui::point(gpui::px(0.), gpui::px(0.)),
295                blur_radius: gpui::px(1.),
296                spread_radius: gpui::px(2.),
297                inset: false,
298            });
299        } else {
300            el = el
301                .border(layout.border_width.max(gpui::px(1.)))
302                .border_color(colors.danger.color);
303        }
304    } else if is_focused && show_focus_ring {
305        shadows.extend(focus_ring_shadows(false, cx));
306    } else if layout.field_border_width > gpui::px(0.) {
307        el = el
308            .border(layout.field_border_width)
309            .border_color(colors.field.border);
310    }
311
312    if shadows.is_empty() {
313        el
314    } else {
315        el.shadow(shadows)
316    }
317}
318
319/// [`apply_field_chrome_with_focus_ring`] without the state ring, for a shell
320/// whose ring is painted by a non-clipping wrapper around it.
321///
322/// A field that clips its children -- `Input` while multi-line, and the
323/// `NumberField` and `DateField` groups, whose shells are `overflow-hidden`
324/// around segments and steppers -- cannot host the overlay ring itself: the
325/// ring hangs outside the box and the clip is the first thing to cut it.
326/// Those shells keep the fill, border and shadow here and hand the ring to
327/// their wrapper through [`field_ring_color`] and [`with_field_ring_overlay`].
328/// The focused (and focused-invalid) states paint no border at all, exactly as
329/// they do when the ring is a child: the ring replaces the chrome border.
330pub fn apply_field_chrome_ringless<T: Styled>(
331    el: T,
332    variant: FieldVariant,
333    is_invalid: bool,
334    is_focused: bool,
335    show_focus_ring: bool,
336    radius_override: Option<Pixels>,
337    cx: &App,
338) -> T {
339    let colors = cx.colors();
340    let layout = cx.layout();
341
342    let radius = radius_override.unwrap_or_else(|| field_radius(cx));
343    let mut el = el.rounded(radius).bg(match variant {
344        FieldVariant::Primary => colors.field.background,
345        FieldVariant::Secondary => colors.default.color,
346    });
347
348    let shadows = if variant == FieldVariant::Primary {
349        layout.field_shadow.clone()
350    } else {
351        Vec::new()
352    };
353
354    // The ring, wherever it is painted, replaces the chrome border; the
355    // else-if chain the shadow and overlay spellings share is the same one.
356    if field_ring_color(is_invalid, is_focused, show_focus_ring, cx).is_none() {
357        if is_invalid {
358            el = el
359                .border(layout.border_width.max(gpui::px(1.)))
360                .border_color(colors.danger.color);
361        } else if layout.field_border_width > gpui::px(0.) {
362            el = el
363                .border(layout.field_border_width)
364                .border_color(colors.field.border);
365        }
366    }
367
368    if shadows.is_empty() {
369        el
370    } else {
371        el.shadow(shadows)
372    }
373}
374
375/// The colour of the state ring a field shell shows, or `None` for a state
376/// that shows none.
377///
378/// `status-focused-field` is `ring-2 ring-focus`; `status-invalid-field`'s
379/// focused form is the same geometry in `danger`. One place resolves which,
380/// so a shell that paints its ring on a wrapper cannot drift from one that
381/// paints it as its own child.
382pub fn field_ring_color(
383    is_invalid: bool,
384    is_focused: bool,
385    show_focus_ring: bool,
386    cx: &App,
387) -> Option<Hsla> {
388    if !(is_focused && show_focus_ring) {
389        return None;
390    }
391    Some(if is_invalid {
392        cx.colors().danger.color
393    } else {
394        cx.colors().focus
395    })
396}
397
398/// Hangs the ring [`field_ring_color`] resolved on an element, as an overlay
399/// child drawn at `radius`.
400pub fn with_field_ring_overlay<T: ParentElement>(
401    el: T,
402    ring: Option<Hsla>,
403    radius: Pixels,
404    cx: &App,
405) -> T {
406    match ring {
407        Some(color) => el.child(ring_overlay_in(radius, false, color, cx)),
408        None => el,
409    }
410}
411
412/// A non-clipping carrier for a field shell that clips, so its state ring has
413/// somewhere to hang.
414///
415/// The shells that need one -- the multi-line `Input`, the `NumberField` and
416/// `DateField` groups -- are `overflow-hidden` around text, segments or
417/// steppers, and an overlay ring drawn in the margin is the first thing such a
418/// clip cuts. The carrier takes the shell's place in the tree and the shell
419/// becomes its only child, so the ring hangs outside the shell's box but
420/// inside nothing. It carries no id, no listeners and no chrome: hit-testing,
421/// the focus path and the shell's own paint are unchanged.
422///
423/// Column flow, because that is what the shell sat in: a flex column stretches
424/// its children across its width, so the shell keeps the width it had as a
425/// direct child of the field's label-to-error column, and the carrier's height
426/// is the shell's.
427pub fn field_ring_carrier(
428    shell: impl gpui::IntoElement,
429    ring: Option<Hsla>,
430    radius: Pixels,
431    cx: &App,
432) -> Div {
433    with_field_ring_overlay(
434        gpui::div().relative().flex().flex_col().child(shell),
435        ring,
436        radius,
437        cx,
438    )
439}
440
441/// [`apply_field_chrome_with_focus_ring`] with the ring as an overlay child.
442///
443/// Same chrome, except that the focused ring -- and the focused *invalid*
444/// ring, which is the same geometry in `danger` -- is painted by
445/// `ring_overlay_in` rather than by a blurred spread shadow. For a field
446/// shell that does not clip its children; the ones that do put the same
447/// overlay on a non-clipping wrapper instead and take
448/// [`apply_field_chrome_ringless`] themselves.
449pub fn apply_field_chrome_overlay<T: Styled + ParentElement>(
450    el: T,
451    variant: FieldVariant,
452    is_invalid: bool,
453    is_focused: bool,
454    show_focus_ring: bool,
455    radius_override: Option<Pixels>,
456    cx: &App,
457) -> T {
458    let radius = radius_override.unwrap_or_else(|| field_radius(cx));
459    let ring = field_ring_color(is_invalid, is_focused, show_focus_ring, cx);
460    let el = apply_field_chrome_ringless(
461        el,
462        variant,
463        is_invalid,
464        is_focused,
465        show_focus_ring,
466        Some(radius),
467        cx,
468    );
469    with_field_ring_overlay(el, ring, radius, cx)
470}
471
472/// Paints a filled 16-segment disc — the round cap and join completion both
473/// canvas-stroked marks need (`ProgressCircle`'s arc ends, the checkbox's
474/// live stroke ends and elbow), since gpui's public stroke builder has
475/// neither a round cap nor a round join to pick.
476pub(crate) fn paint_disc(
477    center: gpui::Point<Pixels>,
478    radius: Pixels,
479    color: Hsla,
480    window: &mut gpui::Window,
481) {
482    let mut builder = gpui::PathBuilder::fill();
483    for step in 0..16 {
484        let angle = std::f32::consts::TAU * step as f32 / 16.;
485        let point = gpui::point(
486            center.x + radius * angle.cos(),
487            center.y + radius * angle.sin(),
488        );
489        if step == 0 {
490            builder.move_to(point);
491        } else {
492            builder.line_to(point);
493        }
494    }
495    builder.close();
496    if let Ok(path) = builder.build() {
497        window.paint_path(path, color);
498    }
499}
500
501/// Lifts a floating panel above the rest of the page.
502///
503/// gpui paints in tree order, so an `absolute` panel is still overdrawn by any
504/// later sibling — a `Select` list opened near the top of a page would be
505/// painted over by the sections below it. `deferred` keeps the panel in the
506/// layout tree but paints it after all of its ancestors, which is what every
507/// floating surface needs.
508pub fn floating(el: impl gpui::IntoElement) -> gpui::Deferred {
509    gpui::deferred(el)
510}
511
512pub(crate) fn window_overlay(el: impl gpui::IntoElement, window: &gpui::Window) -> gpui::Deferred {
513    use gpui::InteractiveElement;
514
515    let viewport = window.viewport_size();
516    floating(
517        gpui::anchored()
518            .position(gpui::point(gpui::px(0.), gpui::px(0.)))
519            .child(
520                gpui::div()
521                    .relative()
522                    .occlude()
523                    .w(viewport.width)
524                    .h(viewport.height)
525                    .flex_shrink_0()
526                    .child(el),
527            ),
528    )
529}
530
531/// The result of an explicit overlay dismissal attempt.
532///
533/// Only `Handled` consumes the event. A declined outside press continues to
534/// the control under the pointer, matching React Aria's `useOverlay`.
535#[derive(Clone, Copy, Debug, PartialEq, Eq)]
536pub enum DismissResult {
537    /// The dismissal was handled and the event is consumed.
538    Handled,
539    /// The dismissal was declined and the event continues to the control underneath.
540    Declined,
541}
542
543#[derive(Default)]
544struct OverlayStack {
545    entries: Vec<gpui::WeakEntity<OverlayRegistration>>,
546    next_order: u64,
547}
548
549impl gpui::Global for OverlayStack {}
550
551#[derive(Clone)]
552struct OverlayRegistration {
553    window_id: gpui::WindowId,
554    order: u64,
555    phase: OverlayPhase,
556    keep_exiting: bool,
557    exit_generation: u64,
558    escape_capture: Option<std::sync::Arc<dyn Fn(&mut gpui::Window, &mut App) -> DismissResult>>,
559}
560
561/// A direct handle to one registration returned by [`overlay_scope`].
562///
563/// The token is intentionally not inferred from an element or a render-local
564/// variable. It becomes inert when its registration is unmounted.
565#[derive(Clone)]
566pub struct OverlayToken {
567    registration: gpui::WeakEntity<OverlayRegistration>,
568    window_id: gpui::WindowId,
569}
570
571fn ensure_overlay_stack(cx: &mut App) {
572    if cx.try_global::<OverlayStack>().is_none() {
573        cx.set_global(OverlayStack::default());
574    }
575}
576
577fn prune_overlay_stack(stack: &mut OverlayStack, cx: &App) {
578    stack.entries.retain(|entry| {
579        let Some(entry) = entry.upgrade() else {
580            return false;
581        };
582        entry.read(cx).phase == OverlayPhase::Open
583    });
584}
585
586fn sync_overlay_stack(registration: &gpui::Entity<OverlayRegistration>, cx: &mut App) {
587    ensure_overlay_stack(cx);
588    let weak = registration.downgrade();
589    let active = registration.read(cx).phase == OverlayPhase::Open;
590    cx.update_global::<OverlayStack, _>(|stack, cx| {
591        prune_overlay_stack(stack, cx);
592        if active && !stack.entries.iter().any(|entry| entry == &weak) {
593            stack.entries.push(weak);
594        }
595    });
596}
597
598fn is_topmost(token: &OverlayToken, cx: &mut App) -> bool {
599    ensure_overlay_stack(cx);
600    cx.update_global::<OverlayStack, _>(|stack, cx| {
601        prune_overlay_stack(stack, cx);
602        let Some(registration) = token.registration.upgrade() else {
603            return false;
604        };
605        let state = registration.read(cx);
606        if state.window_id != token.window_id || state.phase != OverlayPhase::Open {
607            return false;
608        }
609        stack
610            .entries
611            .iter()
612            .filter_map(|entry| entry.upgrade())
613            .filter(|entry| entry.read(cx).window_id == token.window_id)
614            .max_by_key(|entry| entry.read(cx).order)
615            .is_some_and(|entry| entry == registration)
616    })
617}
618
619/// Gives an overlay a document-level Escape handler.
620///
621/// React Aria uses this for Tooltip because focus may sit anywhere else in the
622/// document while hover keeps the tip open. [`app_focus_root`] invokes the
623/// handler during capture, before a focused descendant can answer the key.
624/// The newest open captured handler wins even when a non-capturing overlay was
625/// registered later, matching the document listener's precedence.
626pub fn capture_escape(
627    token: &OverlayToken,
628    handler: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
629    cx: &mut App,
630) {
631    if let Some(registration) = token.registration.upgrade() {
632        let handler = shared(handler);
633        registration.update(cx, |state, _| state.escape_capture = Some(handler));
634    }
635}
636
637fn dismiss_captured_escape(window: &mut gpui::Window, cx: &mut App) -> bool {
638    let window_id = window.window_handle().window_id();
639    ensure_overlay_stack(cx);
640    let handler = cx.update_global::<OverlayStack, _>(|stack, cx| {
641        prune_overlay_stack(stack, cx);
642        stack
643            .entries
644            .iter()
645            .filter_map(|entry| entry.upgrade())
646            .filter(|entry| entry.read(cx).window_id == window_id)
647            .filter_map(|entry| {
648                let state = entry.read(cx);
649                state
650                    .escape_capture
651                    .clone()
652                    .map(|handler| (state.order, handler))
653            })
654            .max_by_key(|(order, _)| *order)
655            .map(|(_, handler)| handler)
656    });
657    handler.is_some_and(|handler| handler(window, cx) == DismissResult::Handled)
658}
659
660/// Closes a floating panel on Escape and on a press outside it.
661///
662/// No prop table asks for this: React Aria gives every popover-like surface
663/// `useOverlay`, so v3 only documents dismissal where it is *configurable*
664/// (`isDismissable` on a dialog backdrop). A panel that closes only through its
665/// own trigger is the difference between a port that looks right and one that
666/// works -- an open menu followed the page as it scrolled and stayed open
667/// forever.
668///
669/// Attach this to the panel itself, not to a wrapper: `on_mouse_down_out` reads
670/// the element's own bounds, and the wrapper an absolute panel sits in has none,
671/// which would make every press inside the panel count as outside. Escape is a
672/// key event, so it needs the focus to be inside the panel -- pair it with
673/// [`panel_focus`] where nothing else there is focused.
674pub fn dismiss_on_escape_with_token<E: gpui::InteractiveElement>(
675    el: E,
676    token: OverlayToken,
677    close: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
678) -> E {
679    el.on_key_down(move |event: &gpui::KeyDownEvent, window, cx| {
680        if event.keystroke.key == "escape"
681            && is_topmost(&token, cx)
682            && close(window, cx) == DismissResult::Handled
683        {
684            cx.stop_propagation();
685        }
686    })
687}
688
689/// The outside-press half of overlay dismissal, for a surface whose Escape is
690/// already part of a keyboard it owns -- a select and a combo box read Escape in
691/// the same handler that reads the arrows, and binding it twice would close
692/// twice.
693pub fn dismiss_on_press_outside_with_token<E: gpui::InteractiveElement>(
694    el: E,
695    token: OverlayToken,
696    close: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
697) -> E {
698    dismiss_on_press_outside_with_token_event(el, token, move |_, window, cx| close(window, cx))
699}
700
701/// The event-aware form of [`dismiss_on_press_outside_with_token`].
702///
703/// Compound surfaces such as a menu and its deferred submenu use the pointer
704/// position to treat the union of both panels as inside, while the token still
705/// prevents a lower overlay from answering the same press.
706pub fn dismiss_on_press_outside_with_token_event<E: gpui::InteractiveElement>(
707    el: E,
708    token: OverlayToken,
709    close: impl Fn(&gpui::MouseDownEvent, &mut gpui::Window, &mut App) -> DismissResult + 'static,
710) -> E {
711    el.on_mouse_down_out(move |event, window, cx| {
712        if is_topmost(&token, cx) && close(event, window, cx) == DismissResult::Handled {
713            cx.stop_propagation();
714        }
715    })
716}
717
718/// A focus handle for a floating panel, focused as the panel opens.
719///
720/// A panel nothing focuses never sees a key, so this is what makes Escape and
721/// the arrows work at all. It is not a tab stop: the panel is transient, and Tab
722/// inside it should reach the controls it contains.
723///
724/// The open transition saves the previously focused handle and claims the
725/// panel. The close transition restores that handle only when focus is still
726/// inside the panel, so an intentional move elsewhere wins. Both transitions
727/// are remembered in keyed state derived from `base`; a per-render flag would
728/// forget ownership on the repaint caused by the opening press.
729#[derive(Clone, Debug, Default)]
730struct PanelFocusState {
731    was_open: bool,
732    restore: Option<gpui::WeakFocusHandle>,
733}
734
735/// A stable handle for the control that opens a [`panel_focus`] scope.
736///
737/// Track this on the trigger wrapper. It is deliberately not a tab stop: the
738/// trigger's own control owns that position, while this handle gives the panel
739/// a stable restoration target even when the trigger is a rebuilt child.
740pub fn panel_restore_focus(
741    window: &mut gpui::Window,
742    cx: &mut App,
743    base: &gpui::ElementId,
744) -> gpui::FocusHandle {
745    window
746        .use_keyed_state(
747            element_id::scoped(base, "panel-restore-focus"),
748            cx,
749            |_, cx| cx.focus_handle(),
750        )
751        .read(cx)
752        .clone()
753}
754
755pub fn panel_focus(
756    window: &mut gpui::Window,
757    cx: &mut App,
758    base: &gpui::ElementId,
759    open: bool,
760) -> gpui::FocusHandle {
761    let held = window.use_keyed_state(element_id::scoped(base, "panel-focus"), cx, |_, cx| {
762        cx.focus_handle()
763    });
764    let handle = held.read(cx).clone();
765    let state =
766        window.use_keyed_state(element_id::scoped(base, "panel-focus-state"), cx, |_, _| {
767            PanelFocusState::default()
768        });
769    let current = state.read(cx).clone();
770
771    if open && !current.was_open {
772        let trigger = panel_restore_focus(window, cx, base);
773        // `focused` is the actual control that opened the panel. The wrapper
774        // is only a fallback for a programmatic open with no focused control;
775        // restoring it when it merely contains the Button loses the Button's
776        // own focus-visible state and keyboard activation.
777        let restore = window
778            .focused(cx)
779            .filter(|focused| focused.tab_stop)
780            .map(|focused| focused.downgrade())
781            .or_else(|| Some(trigger.downgrade()));
782        window.focus(&handle, cx);
783        state.update(cx, |state, _| {
784            state.was_open = true;
785            state.restore = restore;
786        });
787    } else if !open && current.was_open {
788        if handle.contains_focused(window, cx) {
789            if let Some(restore) = current.restore.and_then(|handle| handle.upgrade()) {
790                window.focus(&restore, cx);
791            }
792        }
793        state.update(cx, |state, _| {
794            state.was_open = false;
795            state.restore = None;
796        });
797    }
798    handle
799}
800
801/// Returns a stable, non-tab-stop scope that closes an open popover when focus
802/// leaves its trigger-plus-panel subtree. Track it on their common root.
803///
804/// Active windows use `on_focus_out`. GPUI blanks that event's focus paths for
805/// inactive windows, including its headless test platform, so a shared
806/// render-time edge detects moves to an outside tab stop there. Both paths
807/// consume `seen_inside`, preventing duplicate closes.
808#[derive(Clone, Copy, Debug, Default)]
809struct CloseOnBlurState {
810    /// Whether the scope held the focus as of the last observed frame.
811    seen_inside: bool,
812}
813
814#[derive(Clone)]
815pub struct FocusLeave {
816    handle: gpui::FocusHandle,
817    subscription: gpui::Entity<Option<gpui::Subscription>>,
818    state: gpui::Entity<CloseOnBlurState>,
819}
820
821impl FocusLeave {
822    pub fn focus_handle(&self) -> gpui::FocusHandle {
823        self.handle.clone()
824    }
825
826    /// Marks a departure as already handled by the event that caused it.
827    pub fn consume(&self, cx: &mut App) {
828        self.state.update(cx, |state, _| state.seen_inside = false);
829        self.subscription.update(cx, |slot, _| *slot = None);
830    }
831}
832
833pub fn close_on_blur(
834    window: &mut gpui::Window,
835    cx: &mut App,
836    base: &gpui::ElementId,
837    open: bool,
838    close: impl Fn(&mut gpui::Window, &mut App) + 'static,
839) -> gpui::FocusHandle {
840    on_focus_leave(window, cx, base, open, close).focus_handle()
841}
842
843/// Observes focus leaving a stable subtree while `active`.
844pub fn on_focus_leave(
845    window: &mut gpui::Window,
846    cx: &mut App,
847    base: &gpui::ElementId,
848    active: bool,
849    leave: impl Fn(&mut gpui::Window, &mut App) + 'static,
850) -> FocusLeave {
851    let held_scope = window.use_keyed_state(
852        element_id::scoped(base, "close-on-blur-scope"),
853        cx,
854        |_, cx| cx.focus_handle().tab_stop(false),
855    );
856    let scope = held_scope.read(cx).clone();
857    // Storing the subscription is arming; dropping it is disarming. The
858    // `Option` slot flips either way without subscribing twice.
859    let subscription = window.use_keyed_state(
860        element_id::scoped(base, "close-on-blur-subscription"),
861        cx,
862        |_, _| None::<gpui::Subscription>,
863    );
864    let state = window.use_keyed_state(
865        element_id::scoped(base, "close-on-blur-state"),
866        cx,
867        |_, _| CloseOnBlurState::default(),
868    );
869    let armed = subscription.read(cx).is_some();
870    // Both observation legs hand the same closer out; gpui runs single-threaded,
871    // so an `Rc` shares it without asking the closure to be `Clone`.
872    let leave = std::rc::Rc::new(leave);
873
874    // The frame-end half (real, focused windows): the guard reads the shared
875    // edge so a render that got there first leaves this nothing to do, and
876    // firing also drops the subscription -- a transition owns its close once.
877    if active && !armed {
878        // The listener is owned by `subscription`; weak captures avoid a cycle
879        // that would otherwise retain an unmounted open component forever.
880        let disarmer = subscription.downgrade();
881        let edge = state.downgrade();
882        let leave = std::rc::Rc::clone(&leave);
883        let listener = window.on_focus_out(&scope, cx, move |_, window, cx| {
884            let Some(edge) = edge.upgrade() else {
885                return;
886            };
887            let due = edge.read(cx).seen_inside;
888            if let Some(disarmer) = disarmer.upgrade() {
889                disarmer.update(cx, |slot, _| *slot = None);
890            }
891            if !due {
892                return;
893            }
894            edge.update(cx, |state, _| state.seen_inside = false);
895            leave(window, cx);
896        });
897        subscription.update(cx, |slot, _| *slot = Some(listener));
898    }
899
900    // The render half (everywhere else): whatever the observer APIs do, a
901    // focus move always invalidates the window, so a move to another tab stop
902    // can be observed on the next frame. Requiring a tab stop avoids treating
903    // the app root's non-interactive recovery handle as a user departure.
904    // Never-before-seen counts as absent, not departed: the first frames of a
905    // freshly opened surface hold no focus yet.
906    if active {
907        if scope.contains_focused(window, cx) {
908            state.update(cx, |state, _| state.seen_inside = true);
909        } else if state.read(cx).seen_inside
910            && window.focused(cx).is_some_and(|focused| focused.tab_stop)
911        {
912            state.update(cx, |state, _| state.seen_inside = false);
913            subscription.update(cx, |slot, _| *slot = None);
914            leave(window, cx);
915        }
916    } else {
917        if state.read(cx).seen_inside {
918            state.update(cx, |state, _| state.seen_inside = false);
919        }
920        if armed {
921            subscription.update(cx, |slot, _| *slot = None);
922        }
923    }
924
925    FocusLeave {
926        handle: scope,
927        subscription,
928        state,
929    }
930}
931
932/// Wraps a callback for sharing between closures.
933///
934/// gpui callbacks take `&mut App` and therefore never leave the main thread;
935/// `Arc` is used only because `Box<dyn Fn>` is not `Clone`. clippy's
936/// `arc_with_non_send_sync` check is about cross-thread sharing, which cannot
937/// happen here.
938#[allow(clippy::arc_with_non_send_sync)]
939pub fn shared<F: 'static>(f: F) -> std::sync::Arc<F> {
940    std::sync::Arc::new(f)
941}
942
943/// Gives `handle` focus the first time this element renders, and never again.
944///
945/// This is `autoFocus`. The "first time" has to be remembered somewhere, so a
946/// one-shot flag lives in element state keyed by `key`: without it the field
947/// would steal focus back on every frame and the user could never leave it.
948pub fn focus_once(
949    window: &mut gpui::Window,
950    cx: &mut App,
951    key: impl Into<gpui::ElementId>,
952    handle: &gpui::FocusHandle,
953) {
954    let done = window.use_keyed_state(key.into(), cx, |_, _| false);
955    if !*done.read(cx) {
956        window.focus(handle, cx);
957        done.update(cx, |d, _| *d = true);
958    }
959}
960
961/// Which phase an overlay is in, so `[data-exiting]` has something to render.
962#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
963pub enum OverlayPhase {
964    /// Not rendered at all.
965    #[default]
966    Closed,
967    /// Rendered, and animating in.
968    Open,
969    /// `isOpen` has gone false, but the panel is still on screen for its exit.
970    Exiting,
971}
972
973/// What `overlay_phase` remembers between renders.
974#[derive(Clone, Copy, Debug, Default)]
975struct PhaseState {
976    was_open: bool,
977    exiting: bool,
978    exit_generation: u64,
979}
980
981/// Registers one overlay in the app-global, window-scoped dismissal stack.
982///
983/// The returned token is the only handle accepted by the explicit dismissal
984/// helpers. An overlay receives a new order only when it transitions from
985/// `Closed` or `Exiting` to `Open`; repainting an open overlay is stable.
986pub fn overlay_scope(
987    window: &mut gpui::Window,
988    cx: &mut App,
989    key: impl Into<gpui::ElementId>,
990    is_open: bool,
991    keep_exiting: bool,
992) -> (OverlayPhase, OverlayToken) {
993    overlay_scope_with_exit(
994        window,
995        cx,
996        key,
997        is_open,
998        keep_exiting,
999        crate::anim::EXITING_MS,
1000    )
1001}
1002
1003/// Registers an overlay whose exit lifetime differs from the shared 100ms.
1004pub fn overlay_scope_with_exit(
1005    window: &mut gpui::Window,
1006    cx: &mut App,
1007    key: impl Into<gpui::ElementId>,
1008    is_open: bool,
1009    keep_exiting: bool,
1010    exit_ms: u64,
1011) -> (OverlayPhase, OverlayToken) {
1012    let key = key.into();
1013    let window_id = window.window_handle().window_id();
1014    let registration = window.use_keyed_state(key, cx, |_, _| OverlayRegistration {
1015        window_id,
1016        order: 0,
1017        phase: OverlayPhase::Closed,
1018        keep_exiting,
1019        exit_generation: 0,
1020        escape_capture: None,
1021    });
1022    let current = registration.read(cx).clone();
1023
1024    let phase = if is_open {
1025        if current.phase != OverlayPhase::Open {
1026            ensure_overlay_stack(cx);
1027            let order = cx.update_global::<OverlayStack, _>(|stack, _| {
1028                stack.next_order = stack.next_order.saturating_add(1);
1029                stack.next_order
1030            });
1031            registration.update(cx, |state, _| {
1032                state.order = order;
1033                state.phase = OverlayPhase::Open;
1034                state.keep_exiting = keep_exiting;
1035            });
1036        }
1037        OverlayPhase::Open
1038    } else if current.phase == OverlayPhase::Open && keep_exiting {
1039        let exit_generation = current.exit_generation.saturating_add(1);
1040        registration.update(cx, |state, _| {
1041            state.phase = OverlayPhase::Exiting;
1042            state.keep_exiting = keep_exiting;
1043            state.exit_generation = exit_generation;
1044        });
1045        let held = registration.downgrade();
1046        cx.spawn(async move |cx: &mut gpui::AsyncApp| {
1047            cx.background_executor()
1048                .timer(std::time::Duration::from_millis(exit_ms))
1049                .await;
1050            cx.update(|cx| {
1051                let _ = held.update(cx, |state, cx| {
1052                    if state.phase == OverlayPhase::Exiting
1053                        && state.exit_generation == exit_generation
1054                    {
1055                        state.phase = OverlayPhase::Closed;
1056                        cx.notify();
1057                    }
1058                });
1059            });
1060        })
1061        .detach();
1062        OverlayPhase::Exiting
1063    } else if current.phase == OverlayPhase::Open {
1064        registration.update(cx, |state, _| {
1065            state.phase = OverlayPhase::Closed;
1066            state.keep_exiting = keep_exiting;
1067        });
1068        OverlayPhase::Closed
1069    } else if current.phase == OverlayPhase::Exiting {
1070        OverlayPhase::Exiting
1071    } else {
1072        if current.keep_exiting != keep_exiting {
1073            registration.update(cx, |state, _| state.keep_exiting = keep_exiting);
1074        }
1075        OverlayPhase::Closed
1076    };
1077
1078    sync_overlay_stack(&registration, cx);
1079    (
1080        phase,
1081        OverlayToken {
1082            registration: registration.downgrade(),
1083            window_id,
1084        },
1085    )
1086}
1087
1088/// Resolves `isOpen` into a phase that includes v3's `[data-exiting]`.
1089///
1090/// Legacy non-stack helper for components not yet migrated. New overlays must
1091/// use [`overlay_scope`] and pass its token to the explicit dismissal helpers.
1092///
1093/// A `RenderOnce` component drops out of the tree the moment `isOpen` goes
1094/// false, which leaves an exit animation nothing to play. This keeps the panel
1095/// alive for [`crate::anim::EXITING_MS`] afterwards: the flip to closed starts a
1096/// timer, and until it fires the phase is `Exiting`.
1097///
1098/// Callers render nothing on `Closed`, [`crate::anim::entering_zoom`] on `Open`
1099/// and [`crate::anim::exiting`] on `Exiting`.
1100pub fn overlay_phase(
1101    window: &mut gpui::Window,
1102    cx: &mut App,
1103    key: impl Into<gpui::ElementId>,
1104    is_open: bool,
1105) -> OverlayPhase {
1106    retained_phase(window, cx, key, is_open, true, crate::anim::EXITING_MS)
1107}
1108
1109/// Resolves a collapsible content panel into an open, exiting, or closed
1110/// phase. Unlike [`overlay_phase`], this lets a component use the source
1111/// component's own transition duration and skip the retained exit entirely
1112/// under reduced motion. It does not register an overlay dismissal token.
1113pub fn panel_phase(
1114    window: &mut gpui::Window,
1115    cx: &mut App,
1116    key: impl Into<gpui::ElementId>,
1117    is_open: bool,
1118    keep_exiting: bool,
1119    exit_ms: u64,
1120) -> OverlayPhase {
1121    retained_phase(window, cx, key, is_open, keep_exiting, exit_ms)
1122}
1123
1124fn retained_phase(
1125    window: &mut gpui::Window,
1126    cx: &mut App,
1127    key: impl Into<gpui::ElementId>,
1128    is_open: bool,
1129    keep_exiting: bool,
1130    exit_ms: u64,
1131) -> OverlayPhase {
1132    let key = key.into();
1133    let held = window.use_keyed_state(key, cx, |_, _| PhaseState::default());
1134    let current = *held.read(cx);
1135
1136    if is_open {
1137        if !current.was_open {
1138            held.update(cx, |s, _| {
1139                s.was_open = true;
1140                s.exiting = false;
1141            });
1142        }
1143        OverlayPhase::Open
1144    } else if current.was_open && keep_exiting {
1145        // Just closed: hold the panel for its exit, then drop it.
1146        let exit_generation = current.exit_generation.saturating_add(1);
1147        held.update(cx, |s, _| {
1148            s.was_open = false;
1149            s.exiting = true;
1150            s.exit_generation = exit_generation;
1151        });
1152        let held = held.clone();
1153        cx.spawn(async move |cx: &mut gpui::AsyncApp| {
1154            cx.background_executor()
1155                .timer(std::time::Duration::from_millis(exit_ms))
1156                .await;
1157            cx.update(|cx| {
1158                held.update(cx, |s, cx| {
1159                    if s.exiting && s.exit_generation == exit_generation {
1160                        s.exiting = false;
1161                        cx.notify();
1162                    }
1163                });
1164            });
1165        })
1166        .detach();
1167        OverlayPhase::Exiting
1168    } else if current.was_open {
1169        held.update(cx, |s, _| {
1170            s.was_open = false;
1171            s.exiting = false;
1172        });
1173        OverlayPhase::Closed
1174    } else if current.exiting {
1175        OverlayPhase::Exiting
1176    } else {
1177        OverlayPhase::Closed
1178    }
1179}
1180
1181/// Runs `apply` on the first render only.
1182///
1183/// This is how a `default*` prop seeds a caller-owned state entity: the entity
1184/// outlives any one render, so writing the default unconditionally would fight
1185/// the user on every frame. Keyed on `key`, so two components of the same kind
1186/// seed independently.
1187pub fn seed_once(
1188    window: &mut gpui::Window,
1189    cx: &mut App,
1190    key: impl Into<gpui::ElementId>,
1191    apply: impl FnOnce(&mut App),
1192) {
1193    let done = window.use_keyed_state(key.into(), cx, |_, _| false);
1194    if !*done.read(cx) {
1195        done.update(cx, |d, _| *d = true);
1196        apply(cx);
1197    }
1198}
1199
1200/// Resolves a controlled prop against an uncontrolled default.
1201///
1202/// This is v3's `value` / `defaultValue` pair. When the caller supplies the
1203/// controlled value it owns the state and the setter is a no-op passthrough;
1204/// when it does not, the component keeps the value itself in element state,
1205/// seeded once from `default`.
1206///
1207/// Returns the value to render and, in the uncontrolled case, the entity to
1208/// write the next value into.
1209pub fn controlled<T>(
1210    window: &mut gpui::Window,
1211    cx: &mut App,
1212    key: impl Into<gpui::ElementId>,
1213    controlled: Option<T>,
1214    default: T,
1215) -> (T, Option<gpui::Entity<T>>)
1216where
1217    T: Clone + 'static,
1218{
1219    match controlled {
1220        // The caller drives it; nothing to remember.
1221        Some(v) => (v, None),
1222        None => {
1223            let held = window.use_keyed_state(key.into(), cx, move |_, _| default);
1224            let current = held.read(cx).clone();
1225            (current, Some(held))
1226        }
1227    }
1228}
1229
1230// ---------------------------------------------------------------------------
1231// Focus rings (`status-focused`)
1232// ---------------------------------------------------------------------------
1233
1234/// Whether the last input this app saw was a keyboard-control key.
1235///
1236/// HeroUI / React Aria treat every non-modifier key, including Escape, as
1237/// focus-visible. Closing a pointer-opened menu then rings its trigger. This
1238/// port only turns the modality on for keys that actually switch to keyboard
1239/// controls; Escape and modifier-only keys leave it alone, and any pointer
1240/// down turns it off.
1241struct FocusVisible(bool);
1242impl gpui::Global for FocusVisible {}
1243
1244#[derive(Default)]
1245struct ActiveKeyboardPresses(Vec<(gpui::WindowId, String, gpui::WeakEntity<(bool, bool)>)>);
1246impl gpui::Global for ActiveKeyboardPresses {}
1247
1248/// `[data-focus-visible]` — whether a focus ring should be showing.
1249///
1250/// A browser rings a control focused by the keyboard and not one focused by a
1251/// click; React Aria says the same thing with `data-focus-visible`, and 41 of
1252/// v3's stylesheets style that state. gpui reports *that* an element has focus
1253/// but not how the focus arrived, so the app root records which kind of input
1254/// was last seen and every ring in the tree reads it.
1255/// The shared focus portion of v3 field render props.
1256///
1257/// Fields draw their focus chrome from these three values, so content closures
1258/// receive them rather than re-deriving focus. Components with additional
1259/// render props embed the same values in their component-specific state.
1260#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1261#[non_exhaustive]
1262pub struct FieldFocus {
1263    /// `isFocused` — this control holds the keyboard.
1264    pub is_focused: bool,
1265    /// `isFocusWithin` — it or something inside it does.
1266    pub is_focus_within: bool,
1267    /// `isFocusVisible` — focused, and the last input was a key.
1268    pub is_focus_visible: bool,
1269}
1270
1271/// v3's *value* render props, as one value.
1272///
1273/// `Select.Value`, `Autocomplete.Value` and `ComboBox.Value` all hand their
1274/// children a function and pass in `{defaultChildren, isPlaceholder,
1275/// selectedItems, selectedText}`. `defaultChildren` is what the slot would have
1276/// drawn, so a caller can wrap it instead of rebuilding it -- which is what v3's
1277/// own examples do (`if (isPlaceholder) return defaultChildren`).
1278#[non_exhaustive]
1279pub struct SelectionValue<'a> {
1280    /// `selectedItems` — the chosen items' text. The order is the component's
1281    /// selection order: Select walks the collection, while ComboBox and
1282    /// Autocomplete follow their selection set's insertion order, the way
1283    /// pinned react-stately 3.49.0's `Set` iterates.
1284    pub selected_items: &'a [gpui::SharedString],
1285    /// Where those items sit in the collection, for a caller keyed by index,
1286    /// in the same order as `selected_items`.
1287    pub selected_indices: &'a [usize],
1288    /// The chosen items' keys, in selection insertion order, when the
1289    /// component's collection is keyed and those keys are distinct from the
1290    /// labels (`Autocomplete`, `ComboBox`). `Select` is index-keyed and
1291    /// carries no distinct key here. `selected_items` and `selected_indices`
1292    /// only contain entries for keys that currently resolve to collection
1293    /// items, so async-loaded or missing keys can make their lengths differ
1294    /// from `selected_keys`.
1295    pub selected_keys: Option<&'a [gpui::SharedString]>,
1296    /// `selectedText` — the same items joined. Select approximates v3's en-US
1297    /// list formatter; the other two use plain comma-space in this port.
1298    pub selected_text: &'a str,
1299    /// `isPlaceholder` — nothing is chosen, so the placeholder shows.
1300    pub is_placeholder: bool,
1301    /// `defaultChildren` — the element this slot would have drawn.
1302    pub default_children: gpui::AnyElement,
1303}
1304
1305/// v3's interactive render props, as one value.
1306///
1307/// Every pressable control in v3 hands its children a function and passes these
1308/// in: `{isHovered, isPressed, isFocused, isFocusVisible, isSelected,
1309/// isDisabled}`; Button additionally supplies `isPending`. This port draws
1310/// each of those states itself, and a component
1311/// that also takes a content closure hands the same values over rather than
1312/// leaving a caller to re-derive them.
1313#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1314#[non_exhaustive]
1315pub struct InteractiveState {
1316    /// `isHovered` — the pointer is over the control. Known one frame late: gpui
1317    /// reports a hover to a *handler*, not to the render that draws it.
1318    pub is_hovered: bool,
1319    /// `isPressed` — the pointer or activation key is down, likewise one frame late.
1320    pub is_pressed: bool,
1321    /// `isFocused`
1322    pub is_focused: bool,
1323    /// `isFocusVisible` — focused *and* the last input was a key.
1324    pub is_focus_visible: bool,
1325    /// `isSelected` — for the controls where selection is a state.
1326    pub is_selected: bool,
1327    /// `isDisabled`
1328    pub is_disabled: bool,
1329    /// `isPending` — Button is waiting for an operation while remaining focusable.
1330    pub is_pending: bool,
1331    /// `isIndeterminate` — a multi-selection row where some but not all of the
1332    /// group's keys are chosen.
1333    pub is_indeterminate: bool,
1334}
1335
1336/// Where a control keeps the hover and press it will report next frame.
1337pub type Interaction = gpui::Entity<(bool, bool)>;
1338
1339fn has_active_keyboard_press(slot: &Interaction, window: &gpui::Window, cx: &App) -> bool {
1340    let weak = slot.downgrade();
1341    let window_id = window.window_handle().window_id();
1342    cx.try_global::<ActiveKeyboardPresses>()
1343        .is_some_and(|pressed| {
1344            pressed.0.iter().any(|(active_window, _, interaction)| {
1345                *active_window == window_id && interaction == &weak
1346            })
1347        })
1348}
1349
1350pub(crate) fn begin_keyboard_press(
1351    slot: &Interaction,
1352    event: &gpui::KeyDownEvent,
1353    window: &gpui::Window,
1354    cx: &mut App,
1355) {
1356    if event.is_held || !matches!(event.keystroke.key.as_str(), "enter" | "space") {
1357        return;
1358    }
1359    let began = slot.update(cx, |state, cx| {
1360        if state.1 {
1361            false
1362        } else {
1363            state.1 = true;
1364            cx.notify();
1365            true
1366        }
1367    });
1368    if began {
1369        if cx.try_global::<ActiveKeyboardPresses>().is_none() {
1370            cx.set_global(ActiveKeyboardPresses::default());
1371        }
1372        let active = (
1373            window.window_handle().window_id(),
1374            event.keystroke.key.clone(),
1375            slot.downgrade(),
1376        );
1377        cx.update_global::<ActiveKeyboardPresses, _>(|pressed, _| {
1378            pressed
1379                .0
1380                .retain(|(_, _, interaction)| interaction.upgrade().is_some());
1381            pressed.0.push(active);
1382        });
1383    }
1384}
1385
1386/// The keyed `(hovered, pressed)` slot for one control.
1387///
1388/// gpui tells a *handler* about a hover and a press; a render can only read what
1389/// the last frame recorded, which is why this is a piece of state rather than a
1390/// question asked during layout.
1391pub fn interaction(id: gpui::ElementId, window: &mut gpui::Window, cx: &mut App) -> Interaction {
1392    window.use_keyed_state(id, cx, |_, _| (false, false))
1393}
1394
1395/// Wires the hover and press handlers that keep an [`Interaction`] current.
1396pub fn track_interaction<T>(el: T, slot: &Interaction) -> T
1397where
1398    T: gpui::StatefulInteractiveElement + ParentElement,
1399{
1400    track_interaction_on_mouse_down(el, slot, |_, _| {})
1401}
1402
1403/// Wires [`track_interaction`] and performs component-specific focus work in
1404/// the same mouse-down handler that records the press.
1405pub(crate) fn track_interaction_on_mouse_down<T>(
1406    el: T,
1407    slot: &Interaction,
1408    on_mouse_down: impl Fn(&mut gpui::Window, &mut App) + 'static,
1409) -> T
1410where
1411    T: gpui::StatefulInteractiveElement + ParentElement,
1412{
1413    let hover = slot.clone();
1414    let down = slot.clone();
1415    let up = slot.clone();
1416    let key_down = slot.clone();
1417    let key_up = slot.clone();
1418    let outside_up = slot.clone();
1419    // This listener is deliberately unconditional, not armed while the slot is
1420    // pressed: `Window::on_mouse_event` is `debug_assert_paint`-only, so a
1421    // press-armed registration could not exist until the frame *after* the
1422    // mouse down, and a release dispatched before that frame completes -- a
1423    // fast click, or a press dragged outside -- would be missed and leave the
1424    // slot stuck pressed. gpui 0.2.2 has no pointer capture; the per-frame
1425    // re-registration is what makes any outside release observable at all.
1426    let release = gpui::canvas(
1427        |bounds, _, _| bounds,
1428        move |_, _, window, _| {
1429            window.on_mouse_event(move |event: &gpui::MouseUpEvent, phase, window, cx| {
1430                if phase == gpui::DispatchPhase::Capture
1431                    && event.button == gpui::MouseButton::Left
1432                    && !has_active_keyboard_press(&outside_up, window, cx)
1433                {
1434                    outside_up.update(cx, |state, cx| {
1435                        if state.1 {
1436                            state.1 = false;
1437                            cx.notify();
1438                        }
1439                    });
1440                }
1441            });
1442        },
1443    )
1444    .absolute()
1445    .inset_0();
1446    el.on_hover(move |over, _, cx| {
1447        let over = *over;
1448        hover.update(cx, |state, cx| {
1449            if state.0 != over {
1450                state.0 = over;
1451                cx.notify();
1452            }
1453        });
1454    })
1455    .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
1456        down.update(cx, |state, cx| {
1457            if !state.1 {
1458                state.1 = true;
1459                cx.notify();
1460            }
1461        });
1462        on_mouse_down(window, cx);
1463    })
1464    .on_mouse_up(gpui::MouseButton::Left, move |_, window, cx| {
1465        if !has_active_keyboard_press(&up, window, cx) {
1466            up.update(cx, |state, cx| {
1467                if state.1 {
1468                    state.1 = false;
1469                    cx.notify();
1470                }
1471            });
1472        }
1473    })
1474    .on_key_down(move |event, window, cx| {
1475        begin_keyboard_press(&key_down, event, window, cx);
1476    })
1477    .on_key_up(move |event, _, cx| {
1478        if matches!(event.keystroke.key.as_str(), "enter" | "space") {
1479            key_up.update(cx, |state, cx| {
1480                if state.1 {
1481                    state.1 = false;
1482                    cx.notify();
1483                }
1484            });
1485        }
1486    })
1487    .child(release)
1488}
1489
1490/// Whether focus rings should currently be shown, i.e. the last interaction was keyboard-driven.
1491pub fn focus_visible(cx: &App) -> bool {
1492    cx.try_global::<FocusVisible>().is_some_and(|v| v.0)
1493}
1494
1495/// Sets whether focus rings should be shown, refreshing windows when the value changes.
1496pub fn set_focus_visible(visible: bool, cx: &mut App) {
1497    if focus_visible(cx) != visible {
1498        cx.set_global(FocusVisible(visible));
1499        cx.refresh_windows();
1500    }
1501}
1502
1503/// Whether `target` should paint the keyboard focus ring.
1504pub(crate) fn shows_focus_ring(target: bool, cx: &App) -> bool {
1505    target && focus_visible(cx)
1506}
1507
1508/// Keys that switch the app into keyboard focus-ring modality.
1509///
1510/// Escape dismisses overlays; it is not a switch to keyboard controls.
1511/// Modifier-only keys also do not count. Any other key does, including Tab,
1512/// arrows, Enter/Space, typeahead and editing keys.
1513pub(crate) fn key_enables_focus_visible(key: &str) -> bool {
1514    !matches!(
1515        key,
1516        "escape"
1517            | "shift"
1518            | "control"
1519            | "ctrl"
1520            | "alt"
1521            | "option"
1522            | "meta"
1523            | "command"
1524            | "win"
1525            | "windows"
1526            | "fn"
1527            | "function"
1528    )
1529}
1530
1531/// The window-level focus root kept by [`app_focus_root`].
1532struct FocusRoot {
1533    handle: gpui::FocusHandle,
1534    /// The focused element the root last saw painted inside it. Focus that
1535    /// stays on it after it has left the tree is focus on nothing.
1536    painted_focus: Option<gpui::WeakFocusHandle>,
1537}
1538
1539/// Records keyboard-versus-pointer input, and moves the focus on Tab.
1540///
1541/// Put this on the app's root element once. Three things have to be true for a
1542/// focus ring to work at all, and this is where they are arranged:
1543///
1544/// - **The root holds the focus when nothing else does.** gpui delivers a key
1545///   event to the focused element and then up through its ancestors; with
1546///   nothing focused there is no chain, so the very first Tab would go nowhere.
1547///   The root claims the focus only when the window has no focused handle, or
1548///   when the focused element was painted inside the root and has since left
1549///   the tree. A handle focused before it first paints — a view focusing its
1550///   own handle while its window opens, or a panel focusing a field it has
1551///   not drawn yet — keeps the focus.
1552/// - **Tab moves the focus.** In a browser the platform does this. Here the app
1553///   asks for it, and gpui walks the tab stops in tree order.
1554/// - **The kind of input is recorded**, because a ring shows only after a
1555///   keyboard-control key, never after a pointer press or Escape. The mouse
1556///   half runs in the capture phase, before the press reaches whatever it
1557///   landed on.
1558pub fn app_focus_root<T>(el: T, window: &mut gpui::Window, cx: &mut App) -> T
1559where
1560    T: gpui::InteractiveElement,
1561{
1562    let state = window.use_keyed_state(
1563        gpui::ElementId::Name("herogpui-focus-root".into()),
1564        cx,
1565        |_, cx| FocusRoot {
1566            handle: cx.focus_handle(),
1567            painted_focus: None,
1568        },
1569    );
1570    let root = state.read(cx).handle.clone();
1571    let claim = match window.focused(cx) {
1572        None => true,
1573        Some(focused) if root.contains_focused(window, cx) => {
1574            state.update(cx, |state, _| {
1575                state.painted_focus = Some(focused.downgrade());
1576            });
1577            false
1578        }
1579        Some(focused) => state
1580            .read(cx)
1581            .painted_focus
1582            .as_ref()
1583            .is_some_and(|painted| *painted == focused),
1584    };
1585    if claim {
1586        window.focus(&root, cx);
1587    }
1588    el.track_focus(&root)
1589        .capture_any_mouse_down(|_, _, cx| set_focus_visible(false, cx))
1590        .capture_key_down(|event, window, cx| {
1591            if event.keystroke.key == "escape" && dismiss_captured_escape(window, cx) {
1592                cx.stop_propagation();
1593            }
1594        })
1595        .on_key_down(|event, window, cx| {
1596            if key_enables_focus_visible(&event.keystroke.key) {
1597                set_focus_visible(true, cx);
1598            }
1599            if event.keystroke.key == "tab" {
1600                if event.keystroke.modifiers.shift {
1601                    window.focus_prev(cx);
1602                } else {
1603                    window.focus_next(cx);
1604                }
1605                cx.stop_propagation();
1606            }
1607        })
1608        .on_key_up(|event, window, cx| {
1609            if !matches!(event.keystroke.key.as_str(), "enter" | "space")
1610                || cx.try_global::<ActiveKeyboardPresses>().is_none()
1611            {
1612                return;
1613            }
1614            let window_id = window.window_handle().window_id();
1615            let key = event.keystroke.key.as_str();
1616            let interactions = cx.update_global::<ActiveKeyboardPresses, _>(|pressed, _| {
1617                let mut released = Vec::new();
1618                pressed
1619                    .0
1620                    .retain(|(active_window, active_key, interaction)| {
1621                        if *active_window == window_id && active_key == key {
1622                            released.push(interaction.clone());
1623                            false
1624                        } else {
1625                            interaction.upgrade().is_some()
1626                        }
1627                    });
1628                released
1629            });
1630            for interaction in interactions {
1631                if let Some(interaction) = interaction.upgrade() {
1632                    interaction.update(cx, |state, cx| {
1633                        if state.1 {
1634                            state.1 = false;
1635                            cx.notify();
1636                        }
1637                    });
1638                }
1639            }
1640        })
1641}
1642
1643/// A focus handle the Tab key can reach, kept in the window's keyed state.
1644///
1645/// gpui registers a tab stop from the **handle's** own `tab_stop` flag; the
1646/// element's `tab_index` builder only configures a handle the element creates
1647/// for itself, which a component that has to read its own focus state cannot
1648/// use. Marking the handle is what makes `window.focus_next(cx)` see it.
1649pub fn tab_stop_handle(
1650    id: gpui::ElementId,
1651    window: &mut gpui::Window,
1652    cx: &mut App,
1653) -> gpui::FocusHandle {
1654    window
1655        .use_keyed_state(id, cx, |_, cx| cx.focus_handle().tab_stop(true))
1656        .read(cx)
1657        .clone()
1658}
1659
1660/// v3's `ring-2`: the focus ring's own thickness, shared by the shadow and
1661/// the overlay spellings of it.
1662const RING_WIDTH: Pixels = gpui::px(2.);
1663
1664/// The shadows that draw v3's focus ring.
1665///
1666/// `status-focused` is `ring-2 ring-focus` over a `ring-offset-2` in the
1667/// background colour: two rings, the inner one separating the accent from the
1668/// control. A ring costs no layout here because it is a shadow -- a border would
1669/// move the content inside it -- and they are painted largest first, since a
1670/// later shadow paints over an earlier one and that overlap is what carves the
1671/// gap.
1672///
1673/// **The blur cannot be zero.** gpui's shadow shader is a Gaussian integral: it
1674/// samples over `3 * blur_radius`, so a blur of zero integrates over nothing and
1675/// paints a completely transparent shadow -- which is why the first version of
1676/// this drew no ring at all. One pixel is the smallest blur that draws, and it
1677/// softens the ring's outer edge by about a pixel: the closest this gpui gets to
1678/// a crisp `ring-2`.
1679///
1680/// [`focus_ring_overlay`] is the default spelling now; this one remains for the
1681/// cases its scalar bands cannot draw, listed there.
1682pub fn focus_ring_shadows(offset: bool, cx: &App) -> Vec<gpui::BoxShadow> {
1683    let colors = cx.colors();
1684    let layout = cx.layout();
1685    let ring = RING_WIDTH;
1686    let blur = gpui::px(1.);
1687    let gap = if offset {
1688        layout.ring_offset_width
1689    } else {
1690        gpui::px(0.)
1691    };
1692    let mut shadows = vec![gpui::BoxShadow {
1693        color: colors.focus,
1694        offset: gpui::point(gpui::px(0.), gpui::px(0.)),
1695        blur_radius: blur,
1696        spread_radius: gap + ring,
1697        inset: false,
1698    }];
1699    if gap > gpui::px(0.) {
1700        shadows.push(gpui::BoxShadow {
1701            color: colors.background,
1702            offset: gpui::point(gpui::px(0.), gpui::px(0.)),
1703            blur_radius: blur,
1704            spread_radius: gap,
1705            inset: false,
1706        });
1707    }
1708    shadows
1709}
1710
1711/// The `status-focused` ring as geometry rather than shadow.
1712///
1713/// [`focus_ring_shadows`] is a faithful *offset* of the ring, but vanilla gpui
1714/// gets two things wrong about it that no shadow parameter can fix:
1715///
1716/// 1. A spread shadow dilates the element's box while keeping the element's
1717///    corner *radius*, so the ring's outer corner stays as tight as the
1718///    element's and reads squarer than CSS, where the outer radius of a ring is
1719///    `r + gap + ring`.
1720/// 2. The shadow shader is a Gaussian integral over `3 * blur_radius`, so a
1721///    blur of zero paints nothing at all and the ring has to carry a one-pixel
1722///    blur. Tailwind's `ring-2` is a crisp `0 0 0 2px`.
1723///
1724/// Borders have neither problem: gpui paints them through the same signed
1725/// distance field as a background, crisply antialiased, and with the radius the
1726/// element asks for. So the ring is painted as an absolutely positioned,
1727/// *bordered* child instead. Taffy lays an absolute child out against its
1728/// parent's box without the parent needing `.relative()`, and a negative
1729/// `inset` pushes the child outside that box, which is what puts the ring in the
1730/// margin where a shadow would have been. Concentric by construction: the outer
1731/// div's border sits between radius `radius + gap + ring` and `radius + gap`,
1732/// and the gap div's between `radius + gap` and `radius`.
1733///
1734/// The overlay takes neither pointer events nor focus: it has no id, no
1735/// listeners, no `occlude()`, and no mouse cursor, which is exactly the set
1736/// `Interactivity::should_insert_hitbox` checks, so it inserts no hitbox and
1737/// cannot shadow a sibling's hover.
1738///
1739/// A ring drawn outside the box is the first thing an `overflow_hidden` parent
1740/// cuts off, so an element that clips does not host the overlay itself: it
1741/// becomes the only child of a non-clipping carrier (see
1742/// [`field_ring_carrier`], and the `Switch` track's) and the overlay hangs
1743/// there instead. What is left on [`focus_ring_shadows`] is the shape it
1744/// cannot draw: a control whose four corners do not resolve to one radius --
1745/// a grouped `Button` or `ToggleButton` `Start`/`End` member, or any member an
1746/// `sx` refinement makes asymmetric -- since the overlay's bands are built
1747/// from a scalar, plus the two rings that are interpolated frame by frame
1748/// (`anim::field_chrome_ramp`'s tracked ring and the colour picker's thumb).
1749pub fn focus_ring_overlay(radius: Pixels, offset: bool, cx: &App) -> Div {
1750    ring_overlay_in(radius, offset, cx.colors().focus, cx)
1751}
1752
1753/// [`focus_ring_overlay`] in an explicit colour, for the field family's
1754/// `status-invalid-field` ring, which is the same geometry in `danger`.
1755pub(crate) fn ring_overlay_in(radius: Pixels, offset: bool, color: Hsla, cx: &App) -> Div {
1756    let gap = if offset {
1757        cx.layout().ring_offset_width
1758    } else {
1759        gpui::px(0.)
1760    };
1761    let outer = ring_overlay_band(radius, gap, color);
1762    if gap > gpui::px(0.) {
1763        outer.child(ring_overlay_gap(radius, gap, cx.colors().background))
1764    } else {
1765        outer
1766    }
1767}
1768
1769/// How far the ring's blur reaches past its outer edge, in logical pixels.
1770const RING_BLUR_REACH: Pixels = gpui::px(3.);
1771/// The Gaussian's standard deviation, in logical pixels, tuned against the
1772/// fork build's ring profile read one device pixel at a time at 2x.
1773const RING_BLUR_SIGMA: f32 = 0.7;
1774/// How far the stroke extends *under* the mask, so the blur of its inner
1775/// edge happens where the mask hides it and the ring meets the gap at full
1776/// strength, as the fork's did.
1777const RING_UNDERLAP: f32 = 1.5;
1778
1779/// The accent band: v3's `ring-2`, `gap` outside the control.
1780///
1781/// v3's ring is a crisp `0 0 0 2px` box shadow; the retired fork drew it with
1782/// a one-pixel blur (the shader's minimum, see [`focus_ring_shadows`]) and
1783/// that soft profile is the look this port keeps. Vanilla gpui cannot draw
1784/// it as a shadow with the right corners (a spread shadow keeps the element's
1785/// radius), and bordered bands stipple along the arc, so the band is an SVG:
1786/// a `stroke-width: 2` rounded rectangle under `feGaussianBlur`, masked to
1787/// the outside of the gap so the inner edge stays crisp against the control,
1788/// rasterised by resvg at device resolution and tinted through gpui's
1789/// monochrome sprite path (`Window::paint_svg`, the same alpha-mask route
1790/// the checkerboard cells take). The document is built at paint time from
1791/// the canvas bounds, so the ring fits any control size; the cache key
1792/// carries size and radius, so distinct geometries never share a raster.
1793fn ring_overlay_band(radius: Pixels, gap: Pixels, color: Hsla) -> Div {
1794    let reach = RING_WIDTH + RING_BLUR_REACH;
1795    let corner = radius + gap;
1796    let canvas = gpui::canvas(
1797        |_, _, _| (),
1798        move |bounds, _, window, cx| {
1799            let w = f32::from(bounds.size.width);
1800            let h = f32::from(bounds.size.height);
1801            if w <= 0. || h <= 0. {
1802                return;
1803            }
1804            let ring = f32::from(RING_WIDTH);
1805            let inner = f32::from(reach);
1806            let rg = f32::from(corner);
1807            let stroke_w = ring + RING_UNDERLAP;
1808            let stroke_at = f32::from(RING_BLUR_REACH) + stroke_w / 2.;
1809            let svg = format!(
1810                concat!(
1811                    "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{w:.2}\" height=\"{h:.2}\" ",
1812                    "viewBox=\"0 0 {w:.2} {h:.2}\"><defs>",
1813                    "<filter id=\"b\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">",
1814                    "<feGaussianBlur stdDeviation=\"{sigma:.3}\"/></filter>",
1815                    "<mask id=\"m\"><rect width=\"{w:.2}\" height=\"{h:.2}\" fill=\"#fff\"/>",
1816                    "<rect x=\"{inner:.2}\" y=\"{inner:.2}\" width=\"{iw:.2}\" height=\"{ih:.2}\" rx=\"{rg:.2}\" fill=\"#000\"/>",
1817                    "</mask></defs><g mask=\"url(#m)\">",
1818                    "<rect x=\"{sx:.2}\" y=\"{sx:.2}\" width=\"{sw:.2}\" height=\"{sh:.2}\" rx=\"{srx:.2}\" ",
1819                    "fill=\"none\" stroke=\"#000\" stroke-width=\"{ring:.2}\" filter=\"url(#b)\"/></g></svg>"
1820                ),
1821                w = w,
1822                h = h,
1823                sigma = RING_BLUR_SIGMA,
1824                inner = inner,
1825                iw = (w - 2. * inner).max(0.),
1826                ih = (h - 2. * inner).max(0.),
1827                rg = rg,
1828                sx = stroke_at,
1829                sw = (w - 2. * stroke_at).max(0.),
1830                sh = (h - 2. * stroke_at).max(0.),
1831                srx = (rg + ring - stroke_w / 2.).max(0.),
1832                ring = stroke_w,
1833            );
1834            let key: gpui::SharedString =
1835                format!("herogpui://focus-ring/{w:.1}x{h:.1}/r{rg:.1}").into();
1836            let _ = window.paint_svg(
1837                bounds,
1838                key,
1839                Some(svg.as_bytes()),
1840                gpui::TransformationMatrix::unit(),
1841                color,
1842                cx,
1843            );
1844        },
1845    )
1846    .absolute()
1847    .inset(-reach);
1848    gpui::div()
1849        .absolute()
1850        .inset(-gap)
1851        .rounded(corner)
1852        .child(canvas)
1853}
1854
1855/// The `ring-offset` band, in the background colour, which is what separates
1856/// the accent from the control: it fills the carrier's box, `gap` wide from
1857/// the control's edge outward.
1858fn ring_overlay_gap(radius: Pixels, gap: Pixels, background: Hsla) -> Div {
1859    gpui::div()
1860        .absolute()
1861        .inset(gpui::px(0.))
1862        .rounded(radius + gap)
1863        .border(gap)
1864        .border_color(background)
1865}
1866
1867/// [`with_focus_ring`] for an element that can host the ring as a child.
1868///
1869/// `base` is still applied as the element's shadow list whether or not it is
1870/// focused, because `shadow()` replaces rather than adds; only the *ring* moves
1871/// from the shadow list into an overlay child. The overlay is appended last so
1872/// it paints above the element's own content.
1873pub fn with_focus_ring_overlay<T: Styled + ParentElement>(
1874    el: T,
1875    focused: bool,
1876    offset: bool,
1877    radius: Pixels,
1878    base: Vec<gpui::BoxShadow>,
1879    cx: &App,
1880) -> T {
1881    let el = if base.is_empty() { el } else { el.shadow(base) };
1882    if !focused {
1883        return el;
1884    }
1885    el.child(focus_ring_overlay(radius, offset, cx))
1886}
1887
1888/// [`ring_if_focused`] painted as an overlay child instead of a shadow.
1889pub fn ring_overlay_if_focused<T: Styled + ParentElement>(
1890    el: T,
1891    handle: &gpui::FocusHandle,
1892    offset: bool,
1893    radius: Pixels,
1894    base: Vec<gpui::BoxShadow>,
1895    window: &gpui::Window,
1896    cx: &App,
1897) -> T {
1898    let focused = shows_focus_ring(handle.is_focused(window), cx);
1899    with_focus_ring_overlay(el, focused, offset, radius, base, cx)
1900}
1901
1902/// Keeps Tab inside `scope`, which is v3's `Tab` cycles elements.
1903///
1904/// gpui's tab order is the window's: a tab group only *orders* its children, so
1905/// Tab walks straight out of a dialog and into the page behind it. There is no
1906/// way to enumerate one subtree's stops either, so the trap is done by moving
1907/// and checking: step, and if the focus left the scope, come back in from the
1908/// far end. Reversing means walking forward until the focus leaves and stepping
1909/// back once, which is bounded so a dialog with no stops of its own cannot spin.
1910///
1911/// The step has to be ours rather than the app root's, so this stops
1912/// propagation: `util::app_focus_root` binds Tab to `focus_next` on a listener
1913/// higher in the tree, and both firing would move twice.
1914pub fn trap_tab<T: gpui::InteractiveElement>(el: T, scope: &gpui::FocusHandle) -> T {
1915    let scope = scope.clone();
1916    el.on_key_down(move |event: &gpui::KeyDownEvent, window, cx| {
1917        if event.keystroke.key != "tab" {
1918            return;
1919        }
1920        // Stopping propagation also skips the root's `set_focus_visible`, and a
1921        // trapped Tab that moved the focus without turning the ring on looks
1922        // like it did nothing at all.
1923        cx.stop_propagation();
1924        set_focus_visible(true, cx);
1925        let back = event.keystroke.modifiers.shift;
1926        if back {
1927            window.focus_prev(cx);
1928        } else {
1929            window.focus_next(cx);
1930        }
1931        if scope.contains_focused(window, cx) {
1932            return;
1933        }
1934        // Out of the scope: re-enter from the other side.
1935        window.focus(&scope, cx);
1936        window.focus_next(cx);
1937        if !back {
1938            return;
1939        }
1940        // Backwards: walk to the last stop inside, then stop one short.
1941        for _ in 0..256 {
1942            window.focus_next(cx);
1943            if !scope.contains_focused(window, cx) {
1944                window.focus_prev(cx);
1945                return;
1946            }
1947        }
1948    })
1949}
1950
1951/// v3's *inset* focus ring, as an overlay to hang inside the focused element.
1952///
1953/// A table is the exception to `status-focused`: `.table__cell` and
1954/// `.table__column` are `shadow-[inset_0_0_0_2px_var(--focus)]` with
1955/// `rounded-lg`, and a focused row draws the same ring split across its cells so
1956/// it reads as one continuous outline *inside* the row. An outset ring cannot
1957/// work there -- the next cell is flush against it, so a ring drawn outside is
1958/// either clipped or, on a transparent cell, bleeds through and fills it (a
1959/// focused column header came out solid accent).
1960///
1961/// gpui has no inset shadow and a border would move the content, so the ring is
1962/// an absolutely positioned child: it paints over the element and costs no
1963/// layout. The parent needs `.relative()`.
1964pub fn inset_focus_ring(cx: &App) -> Div {
1965    let colors = cx.colors();
1966    gpui::div()
1967        .absolute()
1968        .inset_0()
1969        .border_2()
1970        .border_color(colors.focus)
1971        .rounded(key_radius(cx))
1972}
1973
1974/// Applies the focus ring on top of whatever the element already casts.
1975///
1976/// `base` is the element's own shadow list, because `shadow()` replaces rather
1977/// than adds: a focused field that dropped its `field_shadow` would flatten as
1978/// it took focus.
1979pub fn with_focus_ring<T: Styled>(
1980    el: T,
1981    focused: bool,
1982    offset: bool,
1983    base: Vec<gpui::BoxShadow>,
1984    cx: &App,
1985) -> T {
1986    if !focused {
1987        return if base.is_empty() { el } else { el.shadow(base) };
1988    }
1989    let mut all = base;
1990    all.extend(focus_ring_shadows(offset, cx));
1991    el.shadow(all)
1992}
1993
1994/// The ring a control shows when it holds a keyboard focus.
1995///
1996/// The two conditions v3's selector has: the element is focused, *and* the focus
1997/// came from the keyboard.
1998pub fn ring_if_focused<T: Styled>(
1999    el: T,
2000    handle: &gpui::FocusHandle,
2001    offset: bool,
2002    base: Vec<gpui::BoxShadow>,
2003    window: &gpui::Window,
2004    cx: &App,
2005) -> T {
2006    let focused = shows_focus_ring(handle.is_focused(window), cx);
2007    with_focus_ring(el, focused, offset, base, cx)
2008}
2009
2010// The `sx` slot: this port's answer to React's `sx`. One slot per component
2011// where the caller restyles the component's root element with GPUI's own
2012// styling methods. The closure styles a scratch `Div`; only the refinement it
2013// leaves behind is kept, and each component merges it over its root style at
2014// the very end of render, so an override wins over every token-driven value
2015// the component set.
2016
2017/// Captures the styling a caller's `sx` closure leaves on a scratch `Div`.
2018///
2019/// Children, listeners and ids the closure adds to the scratch are dropped on
2020/// purpose: the slot restyles the component's own root, it does not substitute
2021/// a new one.
2022pub fn capture_sx(style: impl FnOnce(Div) -> Div) -> Box<gpui::StyleRefinement> {
2023    Box::new(style(gpui::div()).style().clone())
2024}
2025
2026/// Adds a closure's root refinement to one already set through GPUI's
2027/// `Styled` methods (or an earlier `sx` call). Builder order stays meaningful
2028/// for fields both calls set; unrelated fields remain in place.
2029pub(crate) fn refine_sx(
2030    sx: &mut Option<Box<gpui::StyleRefinement>>,
2031    style: impl FnOnce(Div) -> Div,
2032) {
2033    let next = capture_sx(style);
2034    if let Some(current) = sx {
2035        current.as_mut().refine(&next);
2036    } else {
2037        *sx = Some(next);
2038    }
2039}
2040
2041/// The `Styled` storage is the same root-only refinement that `sx` uses.
2042/// Invoke this beside each component type with an `sx` field so GPUI's full
2043/// style vocabulary can be used without forwarding styles into child parts.
2044macro_rules! impl_component_styled {
2045    ($($component:ty),+ $(,)?) => {
2046        $(
2047            impl gpui::Styled for $component {
2048                fn style(&mut self) -> &mut gpui::StyleRefinement {
2049                    self.sx
2050                        .get_or_insert_with(|| Box::new(gpui::StyleRefinement::default()))
2051                        .as_mut()
2052                }
2053            }
2054        )+
2055    };
2056}
2057pub(crate) use impl_component_styled;
2058
2059/// The last laid-out bounds of a focused HeroGPUI control in each window.
2060/// GPUI exposes the focus handle and prepaint bounds separately, so controls
2061/// record the pair for keyboard ContextMenu placement. Weak handles prevent
2062/// the registry from keeping retired controls alive.
2063#[derive(Clone, Default)]
2064struct FocusedBounds(
2065    std::rc::Rc<
2066        std::cell::RefCell<
2067            std::collections::HashMap<
2068                gpui::WindowId,
2069                (gpui::WeakFocusHandle, gpui::Bounds<Pixels>),
2070            >,
2071        >,
2072    >,
2073);
2074
2075impl gpui::Global for FocusedBounds {}
2076
2077/// Append a layout-free probe to the focus-owning element, after its content.
2078/// A stale prepaint cannot anchor a later focus: lookup checks handle identity.
2079pub(crate) fn record_focus_bounds<T: ParentElement>(
2080    el: T,
2081    handle: &gpui::FocusHandle,
2082    window: &gpui::Window,
2083    cx: &mut App,
2084) -> T {
2085    if !handle.is_focused(window) {
2086        return el;
2087    }
2088    let registry = if let Some(registry) = cx.try_global::<FocusedBounds>() {
2089        registry.clone()
2090    } else {
2091        let registry = FocusedBounds::default();
2092        cx.set_global(registry.clone());
2093        registry
2094    };
2095    let weak = handle.downgrade();
2096    el.child(
2097        gpui::canvas(
2098            move |bounds, window, _| {
2099                registry
2100                    .0
2101                    .borrow_mut()
2102                    .insert(window.window_handle().window_id(), (weak, bounds));
2103            },
2104            |_, _, _, _| {},
2105        )
2106        .absolute()
2107        .inset_0(),
2108    )
2109}
2110
2111/// Bounds recorded in the last prepaint for this window's current focus.
2112pub(crate) fn focused_element_bounds(
2113    window: &gpui::Window,
2114    cx: &App,
2115) -> Option<gpui::Bounds<Pixels>> {
2116    let focused = window.focused(cx)?;
2117    let registry = cx.try_global::<FocusedBounds>()?;
2118    let bounds = registry.0.borrow();
2119    let (handle, bounds) = bounds.get(&window.window_handle().window_id())?;
2120    (*handle == focused).then_some(*bounds)
2121}
2122
2123/// Merges a captured `sx` refinement over a root element's own style.
2124///
2125/// Call this after every value the component derived from its variant and the
2126/// active theme: `refine` replaces exactly the fields the closure set and
2127/// leaves the rest of the component's styling alone.
2128pub fn apply_sx<T: Styled>(el: T, sx: &Option<Box<gpui::StyleRefinement>>) -> T {
2129    let Some(sx) = sx else { return el };
2130    let mut el = el;
2131    el.style().refine(sx);
2132    el
2133}
2134
2135/// The solid colour an `sx` override painted as the root background, if any.
2136///
2137/// Components that draw state-driven fills of their own — Button's hover fade
2138/// is one — read this so the override holds across states, not only at rest.
2139pub fn sx_background(sx: &Option<Box<gpui::StyleRefinement>>) -> Option<Hsla> {
2140    match sx.as_ref()?.background {
2141        Some(gpui::Fill::Color(background)) => background.as_solid(),
2142        _ => None,
2143    }
2144}
2145
2146/// The solid border colour an `sx` override set on the root, if any.
2147///
2148/// Components that paint their border as a child overlay (so an edge control
2149/// can paint above it) use this to preserve the same root-level customization
2150/// that [`apply_sx`] would otherwise provide directly.
2151pub fn sx_border_color(sx: &Option<Box<gpui::StyleRefinement>>) -> Option<Hsla> {
2152    sx.as_ref()?.border_color
2153}
2154
2155/// The definite pixel size an `sx` override set on the root, axis by axis.
2156///
2157/// Fractions and rems resolve against the parent and the rem size, which a
2158/// component's own geometry cannot know; those stay with the plain refine in
2159/// [`apply_sx`].
2160pub fn sx_pixel_size(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Size<Option<Pixels>> {
2161    fn definite(length: gpui::Length) -> Option<Pixels> {
2162        match length {
2163            gpui::Length::Definite(gpui::DefiniteLength::Absolute(
2164                gpui::AbsoluteLength::Pixels(pixels),
2165            )) => Some(pixels),
2166            _ => None,
2167        }
2168    }
2169    let Some(sx) = sx else {
2170        return gpui::Size {
2171            width: None,
2172            height: None,
2173        };
2174    };
2175    gpui::Size {
2176        width: sx.size.width.and_then(definite),
2177        height: sx.size.height.and_then(definite),
2178    }
2179}
2180
2181/// The pair [`crate::anim::hover_fade`] eases between, resolving a component's
2182/// resting pair against the two caller-owned overrides.
2183///
2184/// Precedence, in one place because the three cases are easy to conflate:
2185///
2186/// - `hover_bg` set: the fade runs from the resting background — the `sx`
2187///   background if there is one, the component's resting colour otherwise —
2188///   to the named hover colour.
2189/// - only an `sx` background: both endpoints are that colour, so the fade
2190///   paints the override rather than easing the component colour back over it.
2191/// - neither: the component's own pair, untouched.
2192///
2193/// Pure so the precedence is table-testable without a window.
2194pub(crate) fn fade_endpoints(
2195    variant: Option<(Hsla, Hsla)>,
2196    sx_background: Option<Hsla>,
2197    hover_bg: Option<Hsla>,
2198) -> Option<(Hsla, Hsla)> {
2199    let resting = sx_background.or_else(|| variant.map(|(idle, _)| idle));
2200    match (hover_bg, resting) {
2201        (Some(hover), Some(resting)) => Some((resting, hover)),
2202        _ => variant.map(|colors| sx_background.map_or(colors, |color| (color, color))),
2203    }
2204}
2205
2206/// The leading v3 pairs with a Tailwind text step: 12/16, 14/20 and 16/24.
2207///
2208/// `None` for a size outside the table, so an override keeps the component's
2209/// own leading rather than guessing.
2210pub(crate) fn leading_for(text_size: Pixels) -> Option<Pixels> {
2211    let size = f32::from(text_size);
2212    if (size - 12.0).abs() < f32::EPSILON {
2213        Some(gpui::px(16.))
2214    } else if (size - 14.0).abs() < f32::EPSILON {
2215        Some(gpui::px(20.))
2216    } else if (size - 16.0).abs() < f32::EPSILON {
2217        Some(gpui::px(24.))
2218    } else {
2219        None
2220    }
2221}
2222
2223/// Refines `el`'s corners with the explicit `sx` corners, leaving each corner
2224/// with the component's own radius when the override did not name it.
2225///
2226/// Child painted parts (a slider's track/fill/knob, a switch's track/thumb)
2227/// call this after their own radius so an `sx` corner wins per corner.
2228/// Fill corners the caller did not name, so a theme radius reaches every
2229/// painted part while an explicit `sx` corner still wins per corner.
2230pub(crate) fn fill_unspecified_corners(
2231    mut corners: gpui::Corners<Option<Pixels>>,
2232    radius: Option<Pixels>,
2233) -> gpui::Corners<Option<Pixels>> {
2234    let Some(radius) = radius else {
2235        return corners;
2236    };
2237    if corners.top_left.is_none() {
2238        corners.top_left = Some(radius);
2239    }
2240    if corners.top_right.is_none() {
2241        corners.top_right = Some(radius);
2242    }
2243    if corners.bottom_right.is_none() {
2244        corners.bottom_right = Some(radius);
2245    }
2246    if corners.bottom_left.is_none() {
2247        corners.bottom_left = Some(radius);
2248    }
2249    corners
2250}
2251
2252pub(crate) fn round_sx_corners<T: Styled>(el: T, corners: &gpui::Corners<Option<Pixels>>) -> T {
2253    let mut el = el;
2254    if let Some(pixels) = corners.top_left {
2255        el = el.rounded_tl(pixels);
2256    }
2257    if let Some(pixels) = corners.top_right {
2258        el = el.rounded_tr(pixels);
2259    }
2260    if let Some(pixels) = corners.bottom_right {
2261        el = el.rounded_br(pixels);
2262    }
2263    if let Some(pixels) = corners.bottom_left {
2264        el = el.rounded_bl(pixels);
2265    }
2266    el
2267}
2268
2269/// The definite pixel padding an `sx` override set on the root, edge by edge.
2270///
2271/// Only pixels extract: a rem resolves against the root font size and a
2272/// fraction against the parent's size, neither of which a component's own
2273/// geometry can know. An unsupported edge reads `None` here while the plain
2274/// [`apply_sx`] refinement still carries the real value to the root; child
2275/// geometry simply cannot reconcile it yet.
2276// No component reconciles child geometry against an `sx` padding yet; the
2277// extraction is kept (and unit-tested) as the documented contract for the
2278// first one that does, beside `sx_radius` and `sx_border_color`.
2279#[allow(dead_code)]
2280pub fn sx_padding(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Edges<Option<Pixels>> {
2281    fn definite(length: gpui::DefiniteLength) -> Option<Pixels> {
2282        match length {
2283            gpui::DefiniteLength::Absolute(gpui::AbsoluteLength::Pixels(pixels)) => Some(pixels),
2284            _ => None,
2285        }
2286    }
2287    let Some(sx) = sx else {
2288        return gpui::Edges::all(None);
2289    };
2290    gpui::Edges {
2291        top: sx.padding.top.and_then(definite),
2292        right: sx.padding.right.and_then(definite),
2293        bottom: sx.padding.bottom.and_then(definite),
2294        left: sx.padding.left.and_then(definite),
2295    }
2296}
2297
2298/// The definite pixel corner radii an `sx` override set on the root.
2299///
2300/// `corner_radii` holds [`gpui::AbsoluteLength`]s, so the unsupported case is
2301/// a rem rather than a percentage; that corner reads `None` while the others
2302/// keep their values and [`apply_sx`] still refines the root.
2303pub fn sx_radius(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Corners<Option<Pixels>> {
2304    fn absolute(length: gpui::AbsoluteLength) -> Option<Pixels> {
2305        match length {
2306            gpui::AbsoluteLength::Pixels(pixels) => Some(pixels),
2307            gpui::AbsoluteLength::Rems(_) => None,
2308        }
2309    }
2310    let Some(sx) = sx else {
2311        return gpui::Corners::default();
2312    };
2313    gpui::Corners {
2314        top_left: sx.corner_radii.top_left.and_then(absolute),
2315        top_right: sx.corner_radii.top_right.and_then(absolute),
2316        bottom_right: sx.corner_radii.bottom_right.and_then(absolute),
2317        bottom_left: sx.corner_radii.bottom_left.and_then(absolute),
2318    }
2319}
2320
2321#[cfg(test)]
2322mod sx_extraction_tests {
2323    use super::*;
2324    use gpui::{px, relative, rems, Div, Styled};
2325
2326    fn captured(style: impl FnOnce(Div) -> Div) -> Option<Box<gpui::StyleRefinement>> {
2327        Some(capture_sx(style))
2328    }
2329
2330    #[test]
2331    #[allow(clippy::float_cmp)] // the fallback is the exact literal, not near it
2332    fn field_box_defaults_are_the_stock_field_metrics() {
2333        let field = FieldBox::default();
2334        assert_eq!(field.resolved_height(), FIELD_HEIGHT);
2335        assert_eq!(f32::from(field.resolved_padding_x()), 12.);
2336    }
2337
2338    #[test]
2339    fn no_override_extracts_nothing() {
2340        assert_eq!(sx_padding(&None), gpui::Edges::all(None));
2341        assert_eq!(sx_radius(&None), gpui::Corners::default());
2342        assert_eq!(sx_border_color(&None), None);
2343    }
2344
2345    #[test]
2346    fn border_color_override_is_preserved_for_child_chrome() {
2347        let color = gpui::hsla(0.58, 0.7, 0.4, 1.0);
2348        let sx = captured(|d| d.border_color(color));
2349        assert_eq!(sx_border_color(&sx), Some(color));
2350    }
2351
2352    #[test]
2353    fn uniform_pixel_padding_extracts_on_every_edge() {
2354        let sx = captured(|d| d.p(px(8.)));
2355        assert_eq!(sx_padding(&sx), gpui::Edges::all(Some(px(8.))));
2356    }
2357
2358    #[test]
2359    fn per_edge_padding_preserves_each_edge() {
2360        let sx = captured(|d| d.pt(px(1.)).pr(px(2.)).pb(px(3.)).pl(px(4.)));
2361        assert_eq!(
2362            sx_padding(&sx),
2363            gpui::Edges {
2364                top: Some(px(1.)),
2365                right: Some(px(2.)),
2366                bottom: Some(px(3.)),
2367                left: Some(px(4.)),
2368            }
2369        );
2370    }
2371
2372    #[test]
2373    fn zero_padding_is_a_real_override() {
2374        let sx = captured(|d| d.p(px(0.)));
2375        assert_eq!(sx_padding(&sx), gpui::Edges::all(Some(px(0.))));
2376    }
2377
2378    #[test]
2379    fn rems_and_fractions_stay_unsupported_edge_by_edge() {
2380        let sx = captured(|d| d.pt(px(2.)).pb(rems(1.)).pl(relative(0.5)));
2381        assert_eq!(
2382            sx_padding(&sx),
2383            gpui::Edges {
2384                top: Some(px(2.)),
2385                right: None,
2386                bottom: None,
2387                left: None,
2388            }
2389        );
2390        // The override itself stays on the refinement for `apply_sx`; only the
2391        // extracted geometry drops it.
2392        let raw = sx.as_ref().unwrap();
2393        assert!(matches!(
2394            raw.padding.bottom,
2395            Some(gpui::DefiniteLength::Absolute(gpui::AbsoluteLength::Rems(
2396                _
2397            )))
2398        ));
2399        assert!(matches!(
2400            raw.padding.left,
2401            Some(gpui::DefiniteLength::Fraction(_))
2402        ));
2403    }
2404
2405    #[test]
2406    fn uniform_and_per_corner_radius_extract() {
2407        let sx = captured(|d| d.rounded(px(6.)));
2408        assert_eq!(
2409            sx_radius(&sx),
2410            gpui::Corners {
2411                top_left: Some(px(6.)),
2412                top_right: Some(px(6.)),
2413                bottom_right: Some(px(6.)),
2414                bottom_left: Some(px(6.)),
2415            }
2416        );
2417
2418        let sx = captured(|d| d.rounded_tl(px(1.)).rounded_br(px(3.)));
2419        assert_eq!(
2420            sx_radius(&sx),
2421            gpui::Corners {
2422                top_left: Some(px(1.)),
2423                top_right: None,
2424                bottom_right: Some(px(3.)),
2425                bottom_left: None,
2426            }
2427        );
2428    }
2429
2430    #[test]
2431    fn leading_pairs_follow_the_v3_steps() {
2432        assert_eq!(leading_for(px(12.)), Some(px(16.)));
2433        assert_eq!(leading_for(px(14.)), Some(px(20.)));
2434        assert_eq!(leading_for(px(16.)), Some(px(24.)));
2435        assert_eq!(leading_for(px(13.)), None, "an unpairable size stays unset");
2436    }
2437
2438    #[test]
2439    fn explicit_sx_corners_refine_each_corner_individually() {
2440        let sx = captured(|d| d.rounded_tl(px(2.)).rounded_br(px(6.)));
2441        let corners = sx_radius(&sx);
2442        let mut el = round_sx_corners(gpui::div().rounded(px(4.)), &corners);
2443        let radii = el.style().corner_radii.clone();
2444        assert_eq!(
2445            radii.top_left,
2446            Some(gpui::AbsoluteLength::Pixels(px(2.))),
2447            "the explicit top-left corner must win"
2448        );
2449        assert_eq!(
2450            radii.bottom_right,
2451            Some(gpui::AbsoluteLength::Pixels(px(6.))),
2452            "the explicit bottom-right corner must win"
2453        );
2454        assert_eq!(
2455            radii.top_right,
2456            Some(gpui::AbsoluteLength::Pixels(px(4.))),
2457            "an unnamed corner keeps the component's own radius"
2458        );
2459        assert_eq!(
2460            radii.bottom_left,
2461            Some(gpui::AbsoluteLength::Pixels(px(4.))),
2462            "an unnamed corner keeps the component's own radius"
2463        );
2464    }
2465
2466    #[test]
2467    fn rem_radius_stays_unsupported_for_that_corner() {
2468        let sx = captured(|d| d.rounded_tl(rems(1.)).rounded_br(px(3.)));
2469        assert_eq!(
2470            sx_radius(&sx),
2471            gpui::Corners {
2472                top_left: None,
2473                top_right: None,
2474                bottom_right: Some(px(3.)),
2475                bottom_left: None,
2476            }
2477        );
2478    }
2479}
2480
2481#[cfg(test)]
2482mod overlay_stack_tests {
2483    use super::*;
2484    use gpui::AppContext;
2485
2486    fn registration(
2487        cx: &mut gpui::TestAppContext,
2488        window_id: gpui::WindowId,
2489        order: u64,
2490    ) -> gpui::Entity<OverlayRegistration> {
2491        cx.new(|_| OverlayRegistration {
2492            window_id,
2493            order,
2494            phase: OverlayPhase::Open,
2495            keep_exiting: false,
2496            exit_generation: 0,
2497            escape_capture: None,
2498        })
2499    }
2500
2501    #[gpui::test]
2502    fn sibling_order_is_stable_when_open_registrations_repaint(cx: &mut gpui::TestAppContext) {
2503        let outer = registration(cx, gpui::WindowId::from(1), 1);
2504        let inner = registration(cx, gpui::WindowId::from(1), 2);
2505        cx.update(|cx| {
2506            sync_overlay_stack(&outer, cx);
2507            sync_overlay_stack(&inner, cx);
2508            sync_overlay_stack(&outer, cx);
2509            sync_overlay_stack(&inner, cx);
2510            let stack = cx.global::<OverlayStack>();
2511            assert_eq!(stack.entries.len(), 2);
2512            assert_eq!(stack.entries[0].upgrade().unwrap().read(cx).order, 1);
2513            assert_eq!(stack.entries[1].upgrade().unwrap().read(cx).order, 2);
2514        });
2515    }
2516
2517    #[gpui::test]
2518    fn topmost_registration_is_window_scoped(cx: &mut gpui::TestAppContext) {
2519        let first = registration(cx, gpui::WindowId::from(1), 1);
2520        let second = registration(cx, gpui::WindowId::from(2), 2);
2521        let first_token = OverlayToken {
2522            registration: first.downgrade(),
2523            window_id: gpui::WindowId::from(1),
2524        };
2525        let second_token = OverlayToken {
2526            registration: second.downgrade(),
2527            window_id: gpui::WindowId::from(2),
2528        };
2529        cx.update(|cx| {
2530            cx.set_global(OverlayStack {
2531                entries: vec![first.downgrade(), second.downgrade()],
2532                next_order: 2,
2533            });
2534            assert!(is_topmost(&first_token, cx));
2535            assert!(is_topmost(&second_token, cx));
2536        });
2537    }
2538
2539    #[gpui::test]
2540    fn dead_registration_is_pruned_from_the_stack(cx: &mut gpui::TestAppContext) {
2541        let registration = registration(cx, gpui::WindowId::from(1), 1);
2542        let weak = registration.downgrade();
2543        cx.update(|cx| {
2544            cx.set_global(OverlayStack {
2545                entries: vec![weak.clone()],
2546                next_order: 1,
2547            });
2548        });
2549        drop(registration);
2550        cx.update(|cx| {
2551            cx.update_global::<OverlayStack, _>(|stack, cx| {
2552                prune_overlay_stack(stack, cx);
2553                assert!(stack.entries.is_empty());
2554            });
2555        });
2556    }
2557}
2558
2559#[cfg(test)]
2560mod focus_ring_overlay_tests {
2561    use super::*;
2562    use gpui::{px, AbsoluteLength, Length, Styled};
2563
2564    fn inset(el: &mut Div) -> Length {
2565        el.style().inset.top.unwrap()
2566    }
2567
2568    fn radius(el: &mut Div) -> AbsoluteLength {
2569        el.style().corner_radii.top_left.unwrap()
2570    }
2571
2572    fn border(el: &mut Div) -> AbsoluteLength {
2573        el.style().border_widths.top.unwrap()
2574    }
2575
2576    /// Without the offset the carrier is the control's own box: the blurred
2577    /// band canvas inside it reaches `2 + blur` outward from that edge.
2578    #[test]
2579    fn unoffset_ring_carrier_is_the_control_box() {
2580        let mut carrier = ring_overlay_band(px(8.), px(0.), gpui::red());
2581        assert_eq!(inset(&mut carrier), Length::Definite(px(0.).into()));
2582        assert_eq!(radius(&mut carrier), AbsoluteLength::Pixels(px(8.)));
2583        assert!(carrier.style().border_widths.top.is_none());
2584        assert!(
2585            carrier.style().box_shadow.is_none(),
2586            "the band is an svg, not a shadow"
2587        );
2588    }
2589
2590    /// With the offset the carrier moves out by the gap, and the gap band
2591    /// fills it as a `gap`-wide border in the background colour, bridging the
2592    /// element's radius `r` to the band's inner radius `r + gap`.
2593    #[test]
2594    fn offset_ring_pushes_the_carrier_out_and_paints_the_gap() {
2595        let gap = px(2.);
2596        let mut carrier = ring_overlay_band(px(8.), gap, gpui::red());
2597        assert_eq!(inset(&mut carrier), Length::Definite(px(-2.).into()));
2598        assert_eq!(radius(&mut carrier), AbsoluteLength::Pixels(px(10.)));
2599
2600        let mut inner = ring_overlay_gap(px(8.), gap, gpui::blue());
2601        assert_eq!(inset(&mut inner), Length::Definite(px(0.).into()));
2602        assert_eq!(radius(&mut inner), AbsoluteLength::Pixels(px(10.)));
2603        assert_eq!(border(&mut inner), AbsoluteLength::Pixels(gap));
2604    }
2605
2606    /// The gap the offset ring leaves is the theme's `ring_offset_width`, not a
2607    /// literal, and the unoffset ring leaves none.
2608    #[gpui::test]
2609    fn the_offset_gap_comes_from_the_layout_theme(cx: &mut gpui::TestAppContext) {
2610        cx.update(|cx| {
2611            herogpui_theme::ThemeProvider::init(cx);
2612            let gap = cx.layout().ring_offset_width;
2613            let mut ring = focus_ring_overlay(px(8.), true, cx);
2614            assert_eq!(inset(&mut ring), Length::Definite((-gap).into()));
2615            assert_eq!(radius(&mut ring), AbsoluteLength::Pixels(px(8.) + gap));
2616
2617            let mut flat = focus_ring_overlay(px(8.), false, cx);
2618            assert_eq!(inset(&mut flat), Length::Definite(px(0.).into()));
2619            assert_eq!(radius(&mut flat), AbsoluteLength::Pixels(px(8.)));
2620        });
2621    }
2622}