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