use gpui::{App, BorrowAppContext, Div, Hsla, ParentElement, Pixels, Refineable, Styled};
use herogpui_core::{element_id, FieldVariant};
use herogpui_theme::ActiveTheme;
pub(crate) const MONO_FONT: &str = if cfg!(target_family = "wasm") {
"JetBrains Mono"
} else {
"Consolas"
};
pub const FIELD_HEIGHT: Pixels = gpui::px(36.);
pub const FIELD_TEXT: Pixels = gpui::px(14.);
pub const FIELD_ICON: Pixels = gpui::px(16.);
pub(crate) fn tabular_font_features() -> gpui::FontFeatures {
gpui::FontFeatures(std::sync::Arc::new(vec![("tnum".to_owned(), 1)]))
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct FieldBox {
pub(crate) height: Option<Pixels>,
pub(crate) padding_x: Option<Pixels>,
pub(crate) padding_y: Option<Pixels>,
pub(crate) is_bare: bool,
pub(crate) is_bare_is_set: bool,
pub(crate) focus_ring: Option<bool>,
}
impl FieldBox {
pub(crate) fn resolved_height(&self) -> Pixels {
self.height.unwrap_or(FIELD_HEIGHT)
}
pub(crate) fn resolved_padding_x(&self) -> Pixels {
self.padding_x.unwrap_or(gpui::px(12.))
}
}
pub fn cursor_interactive<T: Styled>(el: T, cx: &App) -> T {
el.cursor(interactive_cursor(cx))
}
pub fn interactive_cursor(cx: &App) -> gpui::CursorStyle {
cx.layout().cursor_interactive
}
pub fn control_radius(cx: &App) -> Pixels {
let layout = cx.layout();
layout.capped(layout.radius_3xl())
}
pub fn soft_radius(cx: &App) -> Pixels {
let layout = cx.layout();
layout.capped(layout.radius_2xl())
}
pub fn small_radius(cx: &App) -> Pixels {
let layout = cx.layout();
layout.capped(layout.radius_xl())
}
pub fn mark_radius(cx: &App) -> Pixels {
let layout = cx.layout();
layout.capped(layout.radius_md())
}
pub fn key_radius(cx: &App) -> Pixels {
let layout = cx.layout();
layout.capped(layout.radius_lg())
}
pub fn hairline_radius(cx: &App) -> Pixels {
let layout = cx.layout();
layout.capped(layout.radius_sm())
}
pub fn micro_radius(cx: &App) -> Pixels {
let layout = cx.layout();
layout.capped(layout.radius_xs())
}
pub fn field_radius(cx: &App) -> Pixels {
let layout = cx.layout();
layout.capped(layout.field_radius)
}
pub fn container_radius(cx: &App) -> Pixels {
let layout = cx.layout();
layout.capped(layout.radius_3xl())
}
pub fn inner_fill_radius(outer: Pixels, border: Pixels) -> Pixels {
(outer - border).max(gpui::px(0.))
}
pub fn shift_wheel_scroll_x(handle: &gpui::ScrollHandle, event: &gpui::ScrollWheelEvent) {
#[cfg(target_family = "wasm")]
{
if !event.modifiers.shift {
return;
}
let vertical = match event.delta {
gpui::ScrollDelta::Pixels(delta) => {
if delta.x != gpui::px(0.) {
return;
}
delta.y
}
gpui::ScrollDelta::Lines(delta) => {
if delta.x != 0.0 {
return;
}
gpui::px(delta.y * 16.0)
}
};
if vertical == gpui::px(0.) {
return;
}
let at = handle.offset();
handle.set_offset(gpui::point(at.x + vertical, at.y));
}
#[cfg(not(target_family = "wasm"))]
{
let _ = (handle, event);
}
}
pub fn apply_field_chrome<T: Styled>(
el: T,
variant: FieldVariant,
is_invalid: bool,
is_focused: bool,
radius_override: Option<Pixels>,
cx: &App,
) -> T {
apply_field_chrome_with_focus_ring(
el,
variant,
is_invalid,
is_focused,
true,
radius_override,
cx,
)
}
pub fn apply_field_chrome_with_focus_ring<T: Styled>(
el: T,
variant: FieldVariant,
is_invalid: bool,
is_focused: bool,
show_focus_ring: bool,
radius_override: Option<Pixels>,
cx: &App,
) -> T {
let colors = cx.colors();
let layout = cx.layout();
let radius = radius_override.unwrap_or_else(|| field_radius(cx));
let mut el = el.rounded(radius).bg(match variant {
FieldVariant::Primary => colors.field.background,
FieldVariant::Secondary => colors.default.color,
});
let mut shadows = if variant == FieldVariant::Primary {
layout.field_shadow.clone()
} else {
Vec::new()
};
if is_invalid {
if is_focused && show_focus_ring {
shadows.push(gpui::BoxShadow {
color: colors.danger.color,
offset: gpui::point(gpui::px(0.), gpui::px(0.)),
blur_radius: gpui::px(1.),
spread_radius: gpui::px(2.),
inset: false,
});
} else {
el = el
.border(layout.border_width.max(gpui::px(1.)))
.border_color(colors.danger.color);
}
} else if is_focused && show_focus_ring {
shadows.extend(focus_ring_shadows(false, cx));
} else if layout.field_border_width > gpui::px(0.) {
el = el
.border(layout.field_border_width)
.border_color(colors.field.border);
}
if shadows.is_empty() {
el
} else {
el.shadow(shadows)
}
}
pub fn apply_field_chrome_ringless<T: Styled>(
el: T,
variant: FieldVariant,
is_invalid: bool,
is_focused: bool,
show_focus_ring: bool,
radius_override: Option<Pixels>,
cx: &App,
) -> T {
let colors = cx.colors();
let layout = cx.layout();
let radius = radius_override.unwrap_or_else(|| field_radius(cx));
let mut el = el.rounded(radius).bg(match variant {
FieldVariant::Primary => colors.field.background,
FieldVariant::Secondary => colors.default.color,
});
let shadows = if variant == FieldVariant::Primary {
layout.field_shadow.clone()
} else {
Vec::new()
};
if field_ring_color(is_invalid, is_focused, show_focus_ring, cx).is_none() {
if is_invalid {
el = el
.border(layout.border_width.max(gpui::px(1.)))
.border_color(colors.danger.color);
} else if layout.field_border_width > gpui::px(0.) {
el = el
.border(layout.field_border_width)
.border_color(colors.field.border);
}
}
if shadows.is_empty() {
el
} else {
el.shadow(shadows)
}
}
pub fn field_ring_color(
is_invalid: bool,
is_focused: bool,
show_focus_ring: bool,
cx: &App,
) -> Option<Hsla> {
if !(is_focused && show_focus_ring) {
return None;
}
Some(if is_invalid {
cx.colors().danger.color
} else {
cx.colors().focus
})
}
pub fn with_field_ring_overlay<T: ParentElement>(
el: T,
ring: Option<Hsla>,
radius: Pixels,
cx: &App,
) -> T {
match ring {
Some(color) => el.child(ring_overlay_in(radius, false, color, cx)),
None => el,
}
}
pub fn field_ring_carrier(
shell: impl gpui::IntoElement,
ring: Option<Hsla>,
radius: Pixels,
cx: &App,
) -> Div {
with_field_ring_overlay(
gpui::div().relative().flex().flex_col().child(shell),
ring,
radius,
cx,
)
}
pub fn apply_field_chrome_overlay<T: Styled + ParentElement>(
el: T,
variant: FieldVariant,
is_invalid: bool,
is_focused: bool,
show_focus_ring: bool,
radius_override: Option<Pixels>,
cx: &App,
) -> T {
let radius = radius_override.unwrap_or_else(|| field_radius(cx));
let ring = field_ring_color(is_invalid, is_focused, show_focus_ring, cx);
let el = apply_field_chrome_ringless(
el,
variant,
is_invalid,
is_focused,
show_focus_ring,
Some(radius),
cx,
);
with_field_ring_overlay(el, ring, radius, cx)
}
pub(crate) fn paint_disc(
center: gpui::Point<Pixels>,
radius: Pixels,
color: Hsla,
window: &mut gpui::Window,
) {
let mut builder = gpui::PathBuilder::fill();
for step in 0..16 {
let angle = std::f32::consts::TAU * step as f32 / 16.;
let point = gpui::point(
center.x + radius * angle.cos(),
center.y + radius * angle.sin(),
);
if step == 0 {
builder.move_to(point);
} else {
builder.line_to(point);
}
}
builder.close();
if let Ok(path) = builder.build() {
window.paint_path(path, color);
}
}
pub fn floating(el: impl gpui::IntoElement) -> gpui::Deferred {
gpui::deferred(el)
}
pub(crate) fn window_overlay(el: impl gpui::IntoElement, window: &gpui::Window) -> gpui::Deferred {
use gpui::InteractiveElement;
let viewport = window.viewport_size();
floating(
gpui::anchored()
.position(gpui::point(gpui::px(0.), gpui::px(0.)))
.child(
gpui::div()
.relative()
.occlude()
.w(viewport.width)
.h(viewport.height)
.flex_shrink_0()
.child(el),
),
)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DismissResult {
Handled,
Declined,
}
#[derive(Default)]
struct OverlayStack {
entries: Vec<gpui::WeakEntity<OverlayRegistration>>,
next_order: u64,
}
impl gpui::Global for OverlayStack {}
#[derive(Clone)]
struct OverlayRegistration {
window_id: gpui::WindowId,
order: u64,
phase: OverlayPhase,
keep_exiting: bool,
exit_generation: u64,
escape_capture: Option<std::sync::Arc<dyn Fn(&mut gpui::Window, &mut App) -> DismissResult>>,
}
#[derive(Clone)]
pub struct OverlayToken {
registration: gpui::WeakEntity<OverlayRegistration>,
window_id: gpui::WindowId,
}
fn ensure_overlay_stack(cx: &mut App) {
if cx.try_global::<OverlayStack>().is_none() {
cx.set_global(OverlayStack::default());
}
}
fn prune_overlay_stack(stack: &mut OverlayStack, cx: &App) {
stack.entries.retain(|entry| {
let Some(entry) = entry.upgrade() else {
return false;
};
entry.read(cx).phase == OverlayPhase::Open
});
}
fn sync_overlay_stack(registration: &gpui::Entity<OverlayRegistration>, cx: &mut App) {
ensure_overlay_stack(cx);
let weak = registration.downgrade();
let active = registration.read(cx).phase == OverlayPhase::Open;
cx.update_global::<OverlayStack, _>(|stack, cx| {
prune_overlay_stack(stack, cx);
if active && !stack.entries.iter().any(|entry| entry == &weak) {
stack.entries.push(weak);
}
});
}
fn is_topmost(token: &OverlayToken, cx: &mut App) -> bool {
ensure_overlay_stack(cx);
cx.update_global::<OverlayStack, _>(|stack, cx| {
prune_overlay_stack(stack, cx);
let Some(registration) = token.registration.upgrade() else {
return false;
};
let state = registration.read(cx);
if state.window_id != token.window_id || state.phase != OverlayPhase::Open {
return false;
}
stack
.entries
.iter()
.filter_map(|entry| entry.upgrade())
.filter(|entry| entry.read(cx).window_id == token.window_id)
.max_by_key(|entry| entry.read(cx).order)
.is_some_and(|entry| entry == registration)
})
}
pub fn capture_escape(
token: &OverlayToken,
handler: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
cx: &mut App,
) {
if let Some(registration) = token.registration.upgrade() {
let handler = shared(handler);
registration.update(cx, |state, _| state.escape_capture = Some(handler));
}
}
fn dismiss_captured_escape(window: &mut gpui::Window, cx: &mut App) -> bool {
let window_id = window.window_handle().window_id();
ensure_overlay_stack(cx);
let handler = cx.update_global::<OverlayStack, _>(|stack, cx| {
prune_overlay_stack(stack, cx);
stack
.entries
.iter()
.filter_map(|entry| entry.upgrade())
.filter(|entry| entry.read(cx).window_id == window_id)
.filter_map(|entry| {
let state = entry.read(cx);
state
.escape_capture
.clone()
.map(|handler| (state.order, handler))
})
.max_by_key(|(order, _)| *order)
.map(|(_, handler)| handler)
});
handler.is_some_and(|handler| handler(window, cx) == DismissResult::Handled)
}
pub fn dismiss_on_escape_with_token<E: gpui::InteractiveElement>(
el: E,
token: OverlayToken,
close: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
) -> E {
el.on_key_down(move |event: &gpui::KeyDownEvent, window, cx| {
if event.keystroke.key == "escape"
&& is_topmost(&token, cx)
&& close(window, cx) == DismissResult::Handled
{
cx.stop_propagation();
}
})
}
pub fn dismiss_on_press_outside_with_token<E: gpui::InteractiveElement>(
el: E,
token: OverlayToken,
close: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
) -> E {
dismiss_on_press_outside_with_token_event(el, token, move |_, window, cx| close(window, cx))
}
pub fn dismiss_on_press_outside_with_token_event<E: gpui::InteractiveElement>(
el: E,
token: OverlayToken,
close: impl Fn(&gpui::MouseDownEvent, &mut gpui::Window, &mut App) -> DismissResult + 'static,
) -> E {
el.on_mouse_down_out(move |event, window, cx| {
if is_topmost(&token, cx) && close(event, window, cx) == DismissResult::Handled {
cx.stop_propagation();
}
})
}
#[derive(Clone, Debug, Default)]
struct PanelFocusState {
was_open: bool,
restore: Option<gpui::WeakFocusHandle>,
}
pub fn panel_restore_focus(
window: &mut gpui::Window,
cx: &mut App,
base: &gpui::ElementId,
) -> gpui::FocusHandle {
window
.use_keyed_state(
element_id::scoped(base, "panel-restore-focus"),
cx,
|_, cx| cx.focus_handle(),
)
.read(cx)
.clone()
}
pub fn panel_focus(
window: &mut gpui::Window,
cx: &mut App,
base: &gpui::ElementId,
open: bool,
) -> gpui::FocusHandle {
let held = window.use_keyed_state(element_id::scoped(base, "panel-focus"), cx, |_, cx| {
cx.focus_handle()
});
let handle = held.read(cx).clone();
let state =
window.use_keyed_state(element_id::scoped(base, "panel-focus-state"), cx, |_, _| {
PanelFocusState::default()
});
let current = state.read(cx).clone();
if open && !current.was_open {
let trigger = panel_restore_focus(window, cx, base);
let restore = window
.focused(cx)
.filter(|focused| focused.tab_stop)
.map(|focused| focused.downgrade())
.or_else(|| Some(trigger.downgrade()));
window.focus(&handle, cx);
state.update(cx, |state, _| {
state.was_open = true;
state.restore = restore;
});
} else if !open && current.was_open {
if handle.contains_focused(window, cx) {
if let Some(restore) = current.restore.and_then(|handle| handle.upgrade()) {
window.focus(&restore, cx);
}
}
state.update(cx, |state, _| {
state.was_open = false;
state.restore = None;
});
}
handle
}
#[derive(Clone, Copy, Debug, Default)]
struct CloseOnBlurState {
seen_inside: bool,
}
#[derive(Clone)]
pub struct FocusLeave {
handle: gpui::FocusHandle,
subscription: gpui::Entity<Option<gpui::Subscription>>,
state: gpui::Entity<CloseOnBlurState>,
}
impl FocusLeave {
pub fn focus_handle(&self) -> gpui::FocusHandle {
self.handle.clone()
}
pub fn consume(&self, cx: &mut App) {
self.state.update(cx, |state, _| state.seen_inside = false);
self.subscription.update(cx, |slot, _| *slot = None);
}
}
pub fn close_on_blur(
window: &mut gpui::Window,
cx: &mut App,
base: &gpui::ElementId,
open: bool,
close: impl Fn(&mut gpui::Window, &mut App) + 'static,
) -> gpui::FocusHandle {
on_focus_leave(window, cx, base, open, close).focus_handle()
}
pub fn on_focus_leave(
window: &mut gpui::Window,
cx: &mut App,
base: &gpui::ElementId,
active: bool,
leave: impl Fn(&mut gpui::Window, &mut App) + 'static,
) -> FocusLeave {
let held_scope = window.use_keyed_state(
element_id::scoped(base, "close-on-blur-scope"),
cx,
|_, cx| cx.focus_handle().tab_stop(false),
);
let scope = held_scope.read(cx).clone();
let subscription = window.use_keyed_state(
element_id::scoped(base, "close-on-blur-subscription"),
cx,
|_, _| None::<gpui::Subscription>,
);
let state = window.use_keyed_state(
element_id::scoped(base, "close-on-blur-state"),
cx,
|_, _| CloseOnBlurState::default(),
);
let armed = subscription.read(cx).is_some();
let leave = std::rc::Rc::new(leave);
if active && !armed {
let disarmer = subscription.downgrade();
let edge = state.downgrade();
let leave = std::rc::Rc::clone(&leave);
let listener = window.on_focus_out(&scope, cx, move |_, window, cx| {
let Some(edge) = edge.upgrade() else {
return;
};
let due = edge.read(cx).seen_inside;
if let Some(disarmer) = disarmer.upgrade() {
disarmer.update(cx, |slot, _| *slot = None);
}
if !due {
return;
}
edge.update(cx, |state, _| state.seen_inside = false);
leave(window, cx);
});
subscription.update(cx, |slot, _| *slot = Some(listener));
}
if active {
if scope.contains_focused(window, cx) {
state.update(cx, |state, _| state.seen_inside = true);
} else if state.read(cx).seen_inside
&& window.focused(cx).is_some_and(|focused| focused.tab_stop)
{
state.update(cx, |state, _| state.seen_inside = false);
subscription.update(cx, |slot, _| *slot = None);
leave(window, cx);
}
} else {
if state.read(cx).seen_inside {
state.update(cx, |state, _| state.seen_inside = false);
}
if armed {
subscription.update(cx, |slot, _| *slot = None);
}
}
FocusLeave {
handle: scope,
subscription,
state,
}
}
#[allow(clippy::arc_with_non_send_sync)]
pub fn shared<F: 'static>(f: F) -> std::sync::Arc<F> {
std::sync::Arc::new(f)
}
pub fn focus_once(
window: &mut gpui::Window,
cx: &mut App,
key: impl Into<gpui::ElementId>,
handle: &gpui::FocusHandle,
) {
let done = window.use_keyed_state(key.into(), cx, |_, _| false);
if !*done.read(cx) {
window.focus(handle, cx);
done.update(cx, |d, _| *d = true);
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OverlayPhase {
#[default]
Closed,
Open,
Exiting,
}
#[derive(Clone, Copy, Debug, Default)]
struct PhaseState {
was_open: bool,
exiting: bool,
exit_generation: u64,
}
pub fn overlay_scope(
window: &mut gpui::Window,
cx: &mut App,
key: impl Into<gpui::ElementId>,
is_open: bool,
keep_exiting: bool,
) -> (OverlayPhase, OverlayToken) {
overlay_scope_with_exit(
window,
cx,
key,
is_open,
keep_exiting,
crate::anim::EXITING_MS,
)
}
pub fn overlay_scope_with_exit(
window: &mut gpui::Window,
cx: &mut App,
key: impl Into<gpui::ElementId>,
is_open: bool,
keep_exiting: bool,
exit_ms: u64,
) -> (OverlayPhase, OverlayToken) {
let key = key.into();
let window_id = window.window_handle().window_id();
let registration = window.use_keyed_state(key, cx, |_, _| OverlayRegistration {
window_id,
order: 0,
phase: OverlayPhase::Closed,
keep_exiting,
exit_generation: 0,
escape_capture: None,
});
let current = registration.read(cx).clone();
let phase = if is_open {
if current.phase != OverlayPhase::Open {
ensure_overlay_stack(cx);
let order = cx.update_global::<OverlayStack, _>(|stack, _| {
stack.next_order = stack.next_order.saturating_add(1);
stack.next_order
});
registration.update(cx, |state, _| {
state.order = order;
state.phase = OverlayPhase::Open;
state.keep_exiting = keep_exiting;
});
}
OverlayPhase::Open
} else if current.phase == OverlayPhase::Open && keep_exiting {
let exit_generation = current.exit_generation.saturating_add(1);
registration.update(cx, |state, _| {
state.phase = OverlayPhase::Exiting;
state.keep_exiting = keep_exiting;
state.exit_generation = exit_generation;
});
let held = registration.downgrade();
cx.spawn(async move |cx: &mut gpui::AsyncApp| {
cx.background_executor()
.timer(std::time::Duration::from_millis(exit_ms))
.await;
cx.update(|cx| {
let _ = held.update(cx, |state, cx| {
if state.phase == OverlayPhase::Exiting
&& state.exit_generation == exit_generation
{
state.phase = OverlayPhase::Closed;
cx.notify();
}
});
});
})
.detach();
OverlayPhase::Exiting
} else if current.phase == OverlayPhase::Open {
registration.update(cx, |state, _| {
state.phase = OverlayPhase::Closed;
state.keep_exiting = keep_exiting;
});
OverlayPhase::Closed
} else if current.phase == OverlayPhase::Exiting {
OverlayPhase::Exiting
} else {
if current.keep_exiting != keep_exiting {
registration.update(cx, |state, _| state.keep_exiting = keep_exiting);
}
OverlayPhase::Closed
};
sync_overlay_stack(®istration, cx);
(
phase,
OverlayToken {
registration: registration.downgrade(),
window_id,
},
)
}
pub fn overlay_phase(
window: &mut gpui::Window,
cx: &mut App,
key: impl Into<gpui::ElementId>,
is_open: bool,
) -> OverlayPhase {
retained_phase(window, cx, key, is_open, true, crate::anim::EXITING_MS)
}
pub fn panel_phase(
window: &mut gpui::Window,
cx: &mut App,
key: impl Into<gpui::ElementId>,
is_open: bool,
keep_exiting: bool,
exit_ms: u64,
) -> OverlayPhase {
retained_phase(window, cx, key, is_open, keep_exiting, exit_ms)
}
fn retained_phase(
window: &mut gpui::Window,
cx: &mut App,
key: impl Into<gpui::ElementId>,
is_open: bool,
keep_exiting: bool,
exit_ms: u64,
) -> OverlayPhase {
let key = key.into();
let held = window.use_keyed_state(key, cx, |_, _| PhaseState::default());
let current = *held.read(cx);
if is_open {
if !current.was_open {
held.update(cx, |s, _| {
s.was_open = true;
s.exiting = false;
});
}
OverlayPhase::Open
} else if current.was_open && keep_exiting {
let exit_generation = current.exit_generation.saturating_add(1);
held.update(cx, |s, _| {
s.was_open = false;
s.exiting = true;
s.exit_generation = exit_generation;
});
let held = held.clone();
cx.spawn(async move |cx: &mut gpui::AsyncApp| {
cx.background_executor()
.timer(std::time::Duration::from_millis(exit_ms))
.await;
cx.update(|cx| {
held.update(cx, |s, cx| {
if s.exiting && s.exit_generation == exit_generation {
s.exiting = false;
cx.notify();
}
});
});
})
.detach();
OverlayPhase::Exiting
} else if current.was_open {
held.update(cx, |s, _| {
s.was_open = false;
s.exiting = false;
});
OverlayPhase::Closed
} else if current.exiting {
OverlayPhase::Exiting
} else {
OverlayPhase::Closed
}
}
pub fn seed_once(
window: &mut gpui::Window,
cx: &mut App,
key: impl Into<gpui::ElementId>,
apply: impl FnOnce(&mut App),
) {
let done = window.use_keyed_state(key.into(), cx, |_, _| false);
if !*done.read(cx) {
done.update(cx, |d, _| *d = true);
apply(cx);
}
}
pub fn controlled<T>(
window: &mut gpui::Window,
cx: &mut App,
key: impl Into<gpui::ElementId>,
controlled: Option<T>,
default: T,
) -> (T, Option<gpui::Entity<T>>)
where
T: Clone + 'static,
{
match controlled {
Some(v) => (v, None),
None => {
let held = window.use_keyed_state(key.into(), cx, move |_, _| default);
let current = held.read(cx).clone();
(current, Some(held))
}
}
}
struct FocusVisible(bool);
impl gpui::Global for FocusVisible {}
#[derive(Default)]
struct ActiveKeyboardPresses(Vec<(gpui::WindowId, String, gpui::WeakEntity<(bool, bool)>)>);
impl gpui::Global for ActiveKeyboardPresses {}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct FieldFocus {
pub is_focused: bool,
pub is_focus_within: bool,
pub is_focus_visible: bool,
}
#[non_exhaustive]
pub struct SelectionValue<'a> {
pub selected_items: &'a [gpui::SharedString],
pub selected_indices: &'a [usize],
pub selected_keys: Option<&'a [gpui::SharedString]>,
pub selected_text: &'a str,
pub is_placeholder: bool,
pub default_children: gpui::AnyElement,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct InteractiveState {
pub is_hovered: bool,
pub is_pressed: bool,
pub is_focused: bool,
pub is_focus_visible: bool,
pub is_selected: bool,
pub is_disabled: bool,
pub is_pending: bool,
pub is_indeterminate: bool,
}
pub type Interaction = gpui::Entity<(bool, bool)>;
fn has_active_keyboard_press(slot: &Interaction, window: &gpui::Window, cx: &App) -> bool {
let weak = slot.downgrade();
let window_id = window.window_handle().window_id();
cx.try_global::<ActiveKeyboardPresses>()
.is_some_and(|pressed| {
pressed.0.iter().any(|(active_window, _, interaction)| {
*active_window == window_id && interaction == &weak
})
})
}
pub(crate) fn begin_keyboard_press(
slot: &Interaction,
event: &gpui::KeyDownEvent,
window: &gpui::Window,
cx: &mut App,
) {
if event.is_held || !matches!(event.keystroke.key.as_str(), "enter" | "space") {
return;
}
let began = slot.update(cx, |state, cx| {
if state.1 {
false
} else {
state.1 = true;
cx.notify();
true
}
});
if began {
if cx.try_global::<ActiveKeyboardPresses>().is_none() {
cx.set_global(ActiveKeyboardPresses::default());
}
let active = (
window.window_handle().window_id(),
event.keystroke.key.clone(),
slot.downgrade(),
);
cx.update_global::<ActiveKeyboardPresses, _>(|pressed, _| {
pressed
.0
.retain(|(_, _, interaction)| interaction.upgrade().is_some());
pressed.0.push(active);
});
}
}
pub fn interaction(id: gpui::ElementId, window: &mut gpui::Window, cx: &mut App) -> Interaction {
window.use_keyed_state(id, cx, |_, _| (false, false))
}
pub fn track_interaction<T>(el: T, slot: &Interaction) -> T
where
T: gpui::StatefulInteractiveElement + ParentElement,
{
track_interaction_on_mouse_down(el, slot, |_, _| {})
}
pub(crate) fn track_interaction_on_mouse_down<T>(
el: T,
slot: &Interaction,
on_mouse_down: impl Fn(&mut gpui::Window, &mut App) + 'static,
) -> T
where
T: gpui::StatefulInteractiveElement + ParentElement,
{
let hover = slot.clone();
let down = slot.clone();
let up = slot.clone();
let key_down = slot.clone();
let key_up = slot.clone();
let outside_up = slot.clone();
let release = gpui::canvas(
|bounds, _, _| bounds,
move |_, _, window, _| {
window.on_mouse_event(move |event: &gpui::MouseUpEvent, phase, window, cx| {
if phase == gpui::DispatchPhase::Capture
&& event.button == gpui::MouseButton::Left
&& !has_active_keyboard_press(&outside_up, window, cx)
{
outside_up.update(cx, |state, cx| {
if state.1 {
state.1 = false;
cx.notify();
}
});
}
});
},
)
.absolute()
.inset_0();
el.on_hover(move |over, _, cx| {
let over = *over;
hover.update(cx, |state, cx| {
if state.0 != over {
state.0 = over;
cx.notify();
}
});
})
.on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
down.update(cx, |state, cx| {
if !state.1 {
state.1 = true;
cx.notify();
}
});
on_mouse_down(window, cx);
})
.on_mouse_up(gpui::MouseButton::Left, move |_, window, cx| {
if !has_active_keyboard_press(&up, window, cx) {
up.update(cx, |state, cx| {
if state.1 {
state.1 = false;
cx.notify();
}
});
}
})
.on_key_down(move |event, window, cx| {
begin_keyboard_press(&key_down, event, window, cx);
})
.on_key_up(move |event, _, cx| {
if matches!(event.keystroke.key.as_str(), "enter" | "space") {
key_up.update(cx, |state, cx| {
if state.1 {
state.1 = false;
cx.notify();
}
});
}
})
.child(release)
}
pub fn focus_visible(cx: &App) -> bool {
cx.try_global::<FocusVisible>().is_some_and(|v| v.0)
}
pub fn set_focus_visible(visible: bool, cx: &mut App) {
if focus_visible(cx) != visible {
cx.set_global(FocusVisible(visible));
cx.refresh_windows();
}
}
pub(crate) fn shows_focus_ring(target: bool, cx: &App) -> bool {
target && focus_visible(cx)
}
pub(crate) fn key_enables_focus_visible(key: &str) -> bool {
!matches!(
key,
"escape"
| "shift"
| "control"
| "ctrl"
| "alt"
| "option"
| "meta"
| "command"
| "win"
| "windows"
| "fn"
| "function"
)
}
struct FocusRoot {
handle: gpui::FocusHandle,
painted_focus: Option<gpui::WeakFocusHandle>,
}
pub fn app_focus_root<T>(el: T, window: &mut gpui::Window, cx: &mut App) -> T
where
T: gpui::InteractiveElement,
{
let state = window.use_keyed_state(
gpui::ElementId::Name("herogpui-focus-root".into()),
cx,
|_, cx| FocusRoot {
handle: cx.focus_handle(),
painted_focus: None,
},
);
let root = state.read(cx).handle.clone();
let claim = match window.focused(cx) {
None => true,
Some(focused) if root.contains_focused(window, cx) => {
state.update(cx, |state, _| {
state.painted_focus = Some(focused.downgrade());
});
false
}
Some(focused) => state
.read(cx)
.painted_focus
.as_ref()
.is_some_and(|painted| *painted == focused),
};
if claim {
window.focus(&root, cx);
}
el.track_focus(&root)
.capture_any_mouse_down(|_, _, cx| set_focus_visible(false, cx))
.capture_key_down(|event, window, cx| {
if event.keystroke.key == "escape" && dismiss_captured_escape(window, cx) {
cx.stop_propagation();
}
})
.on_key_down(|event, window, cx| {
if key_enables_focus_visible(&event.keystroke.key) {
set_focus_visible(true, cx);
}
if event.keystroke.key == "tab" {
if event.keystroke.modifiers.shift {
window.focus_prev(cx);
} else {
window.focus_next(cx);
}
cx.stop_propagation();
}
})
.on_key_up(|event, window, cx| {
if !matches!(event.keystroke.key.as_str(), "enter" | "space")
|| cx.try_global::<ActiveKeyboardPresses>().is_none()
{
return;
}
let window_id = window.window_handle().window_id();
let key = event.keystroke.key.as_str();
let interactions = cx.update_global::<ActiveKeyboardPresses, _>(|pressed, _| {
let mut released = Vec::new();
pressed
.0
.retain(|(active_window, active_key, interaction)| {
if *active_window == window_id && active_key == key {
released.push(interaction.clone());
false
} else {
interaction.upgrade().is_some()
}
});
released
});
for interaction in interactions {
if let Some(interaction) = interaction.upgrade() {
interaction.update(cx, |state, cx| {
if state.1 {
state.1 = false;
cx.notify();
}
});
}
}
})
}
pub fn tab_stop_handle(
id: gpui::ElementId,
window: &mut gpui::Window,
cx: &mut App,
) -> gpui::FocusHandle {
window
.use_keyed_state(id, cx, |_, cx| cx.focus_handle().tab_stop(true))
.read(cx)
.clone()
}
const RING_WIDTH: Pixels = gpui::px(2.);
pub fn focus_ring_shadows(offset: bool, cx: &App) -> Vec<gpui::BoxShadow> {
let colors = cx.colors();
let layout = cx.layout();
let ring = RING_WIDTH;
let blur = gpui::px(1.);
let gap = if offset {
layout.ring_offset_width
} else {
gpui::px(0.)
};
let mut shadows = vec![gpui::BoxShadow {
color: colors.focus,
offset: gpui::point(gpui::px(0.), gpui::px(0.)),
blur_radius: blur,
spread_radius: gap + ring,
inset: false,
}];
if gap > gpui::px(0.) {
shadows.push(gpui::BoxShadow {
color: colors.background,
offset: gpui::point(gpui::px(0.), gpui::px(0.)),
blur_radius: blur,
spread_radius: gap,
inset: false,
});
}
shadows
}
pub fn focus_ring_overlay(radius: Pixels, offset: bool, cx: &App) -> Div {
ring_overlay_in(radius, offset, cx.colors().focus, cx)
}
pub(crate) fn ring_overlay_in(radius: Pixels, offset: bool, color: Hsla, cx: &App) -> Div {
let gap = if offset {
cx.layout().ring_offset_width
} else {
gpui::px(0.)
};
let outer = ring_overlay_band(radius, gap, color);
if gap > gpui::px(0.) {
outer.child(ring_overlay_gap(radius, gap, cx.colors().background))
} else {
outer
}
}
const RING_BLUR_REACH: Pixels = gpui::px(3.);
const RING_BLUR_SIGMA: f32 = 0.7;
const RING_UNDERLAP: f32 = 1.5;
fn ring_overlay_band(radius: Pixels, gap: Pixels, color: Hsla) -> Div {
let reach = RING_WIDTH + RING_BLUR_REACH;
let corner = radius + gap;
let canvas = gpui::canvas(
|_, _, _| (),
move |bounds, _, window, cx| {
let w = f32::from(bounds.size.width);
let h = f32::from(bounds.size.height);
if w <= 0. || h <= 0. {
return;
}
let ring = f32::from(RING_WIDTH);
let inner = f32::from(reach);
let rg = f32::from(corner);
let stroke_w = ring + RING_UNDERLAP;
let stroke_at = f32::from(RING_BLUR_REACH) + stroke_w / 2.;
let svg = format!(
concat!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{w:.2}\" height=\"{h:.2}\" ",
"viewBox=\"0 0 {w:.2} {h:.2}\"><defs>",
"<filter id=\"b\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">",
"<feGaussianBlur stdDeviation=\"{sigma:.3}\"/></filter>",
"<mask id=\"m\"><rect width=\"{w:.2}\" height=\"{h:.2}\" fill=\"#fff\"/>",
"<rect x=\"{inner:.2}\" y=\"{inner:.2}\" width=\"{iw:.2}\" height=\"{ih:.2}\" rx=\"{rg:.2}\" fill=\"#000\"/>",
"</mask></defs><g mask=\"url(#m)\">",
"<rect x=\"{sx:.2}\" y=\"{sx:.2}\" width=\"{sw:.2}\" height=\"{sh:.2}\" rx=\"{srx:.2}\" ",
"fill=\"none\" stroke=\"#000\" stroke-width=\"{ring:.2}\" filter=\"url(#b)\"/></g></svg>"
),
w = w,
h = h,
sigma = RING_BLUR_SIGMA,
inner = inner,
iw = (w - 2. * inner).max(0.),
ih = (h - 2. * inner).max(0.),
rg = rg,
sx = stroke_at,
sw = (w - 2. * stroke_at).max(0.),
sh = (h - 2. * stroke_at).max(0.),
srx = (rg + ring - stroke_w / 2.).max(0.),
ring = stroke_w,
);
let key: gpui::SharedString =
format!("herogpui://focus-ring/{w:.1}x{h:.1}/r{rg:.1}").into();
let _ = window.paint_svg(
bounds,
key,
Some(svg.as_bytes()),
gpui::TransformationMatrix::unit(),
color,
cx,
);
},
)
.absolute()
.inset(-reach);
gpui::div()
.absolute()
.inset(-gap)
.rounded(corner)
.child(canvas)
}
fn ring_overlay_gap(radius: Pixels, gap: Pixels, background: Hsla) -> Div {
gpui::div()
.absolute()
.inset(gpui::px(0.))
.rounded(radius + gap)
.border(gap)
.border_color(background)
}
pub fn with_focus_ring_overlay<T: Styled + ParentElement>(
el: T,
focused: bool,
offset: bool,
radius: Pixels,
base: Vec<gpui::BoxShadow>,
cx: &App,
) -> T {
let el = if base.is_empty() { el } else { el.shadow(base) };
if !focused {
return el;
}
el.child(focus_ring_overlay(radius, offset, cx))
}
pub fn ring_overlay_if_focused<T: Styled + ParentElement>(
el: T,
handle: &gpui::FocusHandle,
offset: bool,
radius: Pixels,
base: Vec<gpui::BoxShadow>,
window: &gpui::Window,
cx: &App,
) -> T {
let focused = shows_focus_ring(handle.is_focused(window), cx);
with_focus_ring_overlay(el, focused, offset, radius, base, cx)
}
pub fn trap_tab<T: gpui::InteractiveElement>(el: T, scope: &gpui::FocusHandle) -> T {
let scope = scope.clone();
el.on_key_down(move |event: &gpui::KeyDownEvent, window, cx| {
if event.keystroke.key != "tab" {
return;
}
cx.stop_propagation();
set_focus_visible(true, cx);
let back = event.keystroke.modifiers.shift;
if back {
window.focus_prev(cx);
} else {
window.focus_next(cx);
}
if scope.contains_focused(window, cx) {
return;
}
window.focus(&scope, cx);
window.focus_next(cx);
if !back {
return;
}
for _ in 0..256 {
window.focus_next(cx);
if !scope.contains_focused(window, cx) {
window.focus_prev(cx);
return;
}
}
})
}
pub fn inset_focus_ring(cx: &App) -> Div {
let colors = cx.colors();
gpui::div()
.absolute()
.inset_0()
.border_2()
.border_color(colors.focus)
.rounded(key_radius(cx))
}
pub fn with_focus_ring<T: Styled>(
el: T,
focused: bool,
offset: bool,
base: Vec<gpui::BoxShadow>,
cx: &App,
) -> T {
if !focused {
return if base.is_empty() { el } else { el.shadow(base) };
}
let mut all = base;
all.extend(focus_ring_shadows(offset, cx));
el.shadow(all)
}
pub fn ring_if_focused<T: Styled>(
el: T,
handle: &gpui::FocusHandle,
offset: bool,
base: Vec<gpui::BoxShadow>,
window: &gpui::Window,
cx: &App,
) -> T {
let focused = shows_focus_ring(handle.is_focused(window), cx);
with_focus_ring(el, focused, offset, base, cx)
}
pub fn capture_sx(style: impl FnOnce(Div) -> Div) -> Box<gpui::StyleRefinement> {
Box::new(style(gpui::div()).style().clone())
}
pub(crate) fn refine_sx(
sx: &mut Option<Box<gpui::StyleRefinement>>,
style: impl FnOnce(Div) -> Div,
) {
let next = capture_sx(style);
if let Some(current) = sx {
current.as_mut().refine(&next);
} else {
*sx = Some(next);
}
}
macro_rules! impl_component_styled {
($($component:ty),+ $(,)?) => {
$(
impl gpui::Styled for $component {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.sx
.get_or_insert_with(|| Box::new(gpui::StyleRefinement::default()))
.as_mut()
}
}
)+
};
}
pub(crate) use impl_component_styled;
#[derive(Clone, Default)]
struct FocusedBounds(
std::rc::Rc<
std::cell::RefCell<
std::collections::HashMap<
gpui::WindowId,
(gpui::WeakFocusHandle, gpui::Bounds<Pixels>),
>,
>,
>,
);
impl gpui::Global for FocusedBounds {}
pub(crate) fn record_focus_bounds<T: ParentElement>(
el: T,
handle: &gpui::FocusHandle,
window: &gpui::Window,
cx: &mut App,
) -> T {
if !handle.is_focused(window) {
return el;
}
let registry = if let Some(registry) = cx.try_global::<FocusedBounds>() {
registry.clone()
} else {
let registry = FocusedBounds::default();
cx.set_global(registry.clone());
registry
};
let weak = handle.downgrade();
el.child(
gpui::canvas(
move |bounds, window, _| {
registry
.0
.borrow_mut()
.insert(window.window_handle().window_id(), (weak, bounds));
},
|_, _, _, _| {},
)
.absolute()
.inset_0(),
)
}
pub(crate) fn focused_element_bounds(
window: &gpui::Window,
cx: &App,
) -> Option<gpui::Bounds<Pixels>> {
let focused = window.focused(cx)?;
let registry = cx.try_global::<FocusedBounds>()?;
let bounds = registry.0.borrow();
let (handle, bounds) = bounds.get(&window.window_handle().window_id())?;
(*handle == focused).then_some(*bounds)
}
pub fn apply_sx<T: Styled>(el: T, sx: &Option<Box<gpui::StyleRefinement>>) -> T {
let Some(sx) = sx else { return el };
let mut el = el;
el.style().refine(sx);
el
}
pub fn sx_background(sx: &Option<Box<gpui::StyleRefinement>>) -> Option<Hsla> {
match sx.as_ref()?.background {
Some(gpui::Fill::Color(background)) => background.as_solid(),
_ => None,
}
}
pub fn sx_border_color(sx: &Option<Box<gpui::StyleRefinement>>) -> Option<Hsla> {
sx.as_ref()?.border_color
}
pub fn sx_pixel_size(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Size<Option<Pixels>> {
fn definite(length: gpui::Length) -> Option<Pixels> {
match length {
gpui::Length::Definite(gpui::DefiniteLength::Absolute(
gpui::AbsoluteLength::Pixels(pixels),
)) => Some(pixels),
_ => None,
}
}
let Some(sx) = sx else {
return gpui::Size {
width: None,
height: None,
};
};
gpui::Size {
width: sx.size.width.and_then(definite),
height: sx.size.height.and_then(definite),
}
}
pub(crate) fn fade_endpoints(
variant: Option<(Hsla, Hsla)>,
sx_background: Option<Hsla>,
hover_bg: Option<Hsla>,
) -> Option<(Hsla, Hsla)> {
let resting = sx_background.or_else(|| variant.map(|(idle, _)| idle));
match (hover_bg, resting) {
(Some(hover), Some(resting)) => Some((resting, hover)),
_ => variant.map(|colors| sx_background.map_or(colors, |color| (color, color))),
}
}
pub(crate) fn leading_for(text_size: Pixels) -> Option<Pixels> {
let size = f32::from(text_size);
if (size - 12.0).abs() < f32::EPSILON {
Some(gpui::px(16.))
} else if (size - 14.0).abs() < f32::EPSILON {
Some(gpui::px(20.))
} else if (size - 16.0).abs() < f32::EPSILON {
Some(gpui::px(24.))
} else {
None
}
}
pub(crate) fn fill_unspecified_corners(
mut corners: gpui::Corners<Option<Pixels>>,
radius: Option<Pixels>,
) -> gpui::Corners<Option<Pixels>> {
let Some(radius) = radius else {
return corners;
};
if corners.top_left.is_none() {
corners.top_left = Some(radius);
}
if corners.top_right.is_none() {
corners.top_right = Some(radius);
}
if corners.bottom_right.is_none() {
corners.bottom_right = Some(radius);
}
if corners.bottom_left.is_none() {
corners.bottom_left = Some(radius);
}
corners
}
pub(crate) fn round_sx_corners<T: Styled>(el: T, corners: &gpui::Corners<Option<Pixels>>) -> T {
let mut el = el;
if let Some(pixels) = corners.top_left {
el = el.rounded_tl(pixels);
}
if let Some(pixels) = corners.top_right {
el = el.rounded_tr(pixels);
}
if let Some(pixels) = corners.bottom_right {
el = el.rounded_br(pixels);
}
if let Some(pixels) = corners.bottom_left {
el = el.rounded_bl(pixels);
}
el
}
#[allow(dead_code)]
pub fn sx_padding(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Edges<Option<Pixels>> {
fn definite(length: gpui::DefiniteLength) -> Option<Pixels> {
match length {
gpui::DefiniteLength::Absolute(gpui::AbsoluteLength::Pixels(pixels)) => Some(pixels),
_ => None,
}
}
let Some(sx) = sx else {
return gpui::Edges::all(None);
};
gpui::Edges {
top: sx.padding.top.and_then(definite),
right: sx.padding.right.and_then(definite),
bottom: sx.padding.bottom.and_then(definite),
left: sx.padding.left.and_then(definite),
}
}
pub fn sx_radius(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Corners<Option<Pixels>> {
fn absolute(length: gpui::AbsoluteLength) -> Option<Pixels> {
match length {
gpui::AbsoluteLength::Pixels(pixels) => Some(pixels),
gpui::AbsoluteLength::Rems(_) => None,
}
}
let Some(sx) = sx else {
return gpui::Corners::default();
};
gpui::Corners {
top_left: sx.corner_radii.top_left.and_then(absolute),
top_right: sx.corner_radii.top_right.and_then(absolute),
bottom_right: sx.corner_radii.bottom_right.and_then(absolute),
bottom_left: sx.corner_radii.bottom_left.and_then(absolute),
}
}
#[cfg(test)]
mod sx_extraction_tests {
use super::*;
use gpui::{px, relative, rems, Div, Styled};
fn captured(style: impl FnOnce(Div) -> Div) -> Option<Box<gpui::StyleRefinement>> {
Some(capture_sx(style))
}
#[test]
#[allow(clippy::float_cmp)] fn field_box_defaults_are_the_stock_field_metrics() {
let field = FieldBox::default();
assert_eq!(field.resolved_height(), FIELD_HEIGHT);
assert_eq!(f32::from(field.resolved_padding_x()), 12.);
}
#[test]
fn no_override_extracts_nothing() {
assert_eq!(sx_padding(&None), gpui::Edges::all(None));
assert_eq!(sx_radius(&None), gpui::Corners::default());
assert_eq!(sx_border_color(&None), None);
}
#[test]
fn border_color_override_is_preserved_for_child_chrome() {
let color = gpui::hsla(0.58, 0.7, 0.4, 1.0);
let sx = captured(|d| d.border_color(color));
assert_eq!(sx_border_color(&sx), Some(color));
}
#[test]
fn uniform_pixel_padding_extracts_on_every_edge() {
let sx = captured(|d| d.p(px(8.)));
assert_eq!(sx_padding(&sx), gpui::Edges::all(Some(px(8.))));
}
#[test]
fn per_edge_padding_preserves_each_edge() {
let sx = captured(|d| d.pt(px(1.)).pr(px(2.)).pb(px(3.)).pl(px(4.)));
assert_eq!(
sx_padding(&sx),
gpui::Edges {
top: Some(px(1.)),
right: Some(px(2.)),
bottom: Some(px(3.)),
left: Some(px(4.)),
}
);
}
#[test]
fn zero_padding_is_a_real_override() {
let sx = captured(|d| d.p(px(0.)));
assert_eq!(sx_padding(&sx), gpui::Edges::all(Some(px(0.))));
}
#[test]
fn rems_and_fractions_stay_unsupported_edge_by_edge() {
let sx = captured(|d| d.pt(px(2.)).pb(rems(1.)).pl(relative(0.5)));
assert_eq!(
sx_padding(&sx),
gpui::Edges {
top: Some(px(2.)),
right: None,
bottom: None,
left: None,
}
);
let raw = sx.as_ref().unwrap();
assert!(matches!(
raw.padding.bottom,
Some(gpui::DefiniteLength::Absolute(gpui::AbsoluteLength::Rems(
_
)))
));
assert!(matches!(
raw.padding.left,
Some(gpui::DefiniteLength::Fraction(_))
));
}
#[test]
fn uniform_and_per_corner_radius_extract() {
let sx = captured(|d| d.rounded(px(6.)));
assert_eq!(
sx_radius(&sx),
gpui::Corners {
top_left: Some(px(6.)),
top_right: Some(px(6.)),
bottom_right: Some(px(6.)),
bottom_left: Some(px(6.)),
}
);
let sx = captured(|d| d.rounded_tl(px(1.)).rounded_br(px(3.)));
assert_eq!(
sx_radius(&sx),
gpui::Corners {
top_left: Some(px(1.)),
top_right: None,
bottom_right: Some(px(3.)),
bottom_left: None,
}
);
}
#[test]
fn leading_pairs_follow_the_v3_steps() {
assert_eq!(leading_for(px(12.)), Some(px(16.)));
assert_eq!(leading_for(px(14.)), Some(px(20.)));
assert_eq!(leading_for(px(16.)), Some(px(24.)));
assert_eq!(leading_for(px(13.)), None, "an unpairable size stays unset");
}
#[test]
fn explicit_sx_corners_refine_each_corner_individually() {
let sx = captured(|d| d.rounded_tl(px(2.)).rounded_br(px(6.)));
let corners = sx_radius(&sx);
let mut el = round_sx_corners(gpui::div().rounded(px(4.)), &corners);
let radii = el.style().corner_radii.clone();
assert_eq!(
radii.top_left,
Some(gpui::AbsoluteLength::Pixels(px(2.))),
"the explicit top-left corner must win"
);
assert_eq!(
radii.bottom_right,
Some(gpui::AbsoluteLength::Pixels(px(6.))),
"the explicit bottom-right corner must win"
);
assert_eq!(
radii.top_right,
Some(gpui::AbsoluteLength::Pixels(px(4.))),
"an unnamed corner keeps the component's own radius"
);
assert_eq!(
radii.bottom_left,
Some(gpui::AbsoluteLength::Pixels(px(4.))),
"an unnamed corner keeps the component's own radius"
);
}
#[test]
fn rem_radius_stays_unsupported_for_that_corner() {
let sx = captured(|d| d.rounded_tl(rems(1.)).rounded_br(px(3.)));
assert_eq!(
sx_radius(&sx),
gpui::Corners {
top_left: None,
top_right: None,
bottom_right: Some(px(3.)),
bottom_left: None,
}
);
}
}
#[cfg(test)]
mod overlay_stack_tests {
use super::*;
use gpui::AppContext;
fn registration(
cx: &mut gpui::TestAppContext,
window_id: gpui::WindowId,
order: u64,
) -> gpui::Entity<OverlayRegistration> {
cx.new(|_| OverlayRegistration {
window_id,
order,
phase: OverlayPhase::Open,
keep_exiting: false,
exit_generation: 0,
escape_capture: None,
})
}
#[gpui::test]
fn sibling_order_is_stable_when_open_registrations_repaint(cx: &mut gpui::TestAppContext) {
let outer = registration(cx, gpui::WindowId::from(1), 1);
let inner = registration(cx, gpui::WindowId::from(1), 2);
cx.update(|cx| {
sync_overlay_stack(&outer, cx);
sync_overlay_stack(&inner, cx);
sync_overlay_stack(&outer, cx);
sync_overlay_stack(&inner, cx);
let stack = cx.global::<OverlayStack>();
assert_eq!(stack.entries.len(), 2);
assert_eq!(stack.entries[0].upgrade().unwrap().read(cx).order, 1);
assert_eq!(stack.entries[1].upgrade().unwrap().read(cx).order, 2);
});
}
#[gpui::test]
fn topmost_registration_is_window_scoped(cx: &mut gpui::TestAppContext) {
let first = registration(cx, gpui::WindowId::from(1), 1);
let second = registration(cx, gpui::WindowId::from(2), 2);
let first_token = OverlayToken {
registration: first.downgrade(),
window_id: gpui::WindowId::from(1),
};
let second_token = OverlayToken {
registration: second.downgrade(),
window_id: gpui::WindowId::from(2),
};
cx.update(|cx| {
cx.set_global(OverlayStack {
entries: vec![first.downgrade(), second.downgrade()],
next_order: 2,
});
assert!(is_topmost(&first_token, cx));
assert!(is_topmost(&second_token, cx));
});
}
#[gpui::test]
fn dead_registration_is_pruned_from_the_stack(cx: &mut gpui::TestAppContext) {
let registration = registration(cx, gpui::WindowId::from(1), 1);
let weak = registration.downgrade();
cx.update(|cx| {
cx.set_global(OverlayStack {
entries: vec![weak.clone()],
next_order: 1,
});
});
drop(registration);
cx.update(|cx| {
cx.update_global::<OverlayStack, _>(|stack, cx| {
prune_overlay_stack(stack, cx);
assert!(stack.entries.is_empty());
});
});
}
}
#[cfg(test)]
mod focus_ring_overlay_tests {
use super::*;
use gpui::{px, AbsoluteLength, Length, Styled};
fn inset(el: &mut Div) -> Length {
el.style().inset.top.unwrap()
}
fn radius(el: &mut Div) -> AbsoluteLength {
el.style().corner_radii.top_left.unwrap()
}
fn border(el: &mut Div) -> AbsoluteLength {
el.style().border_widths.top.unwrap()
}
#[test]
fn unoffset_ring_carrier_is_the_control_box() {
let mut carrier = ring_overlay_band(px(8.), px(0.), gpui::red());
assert_eq!(inset(&mut carrier), Length::Definite(px(0.).into()));
assert_eq!(radius(&mut carrier), AbsoluteLength::Pixels(px(8.)));
assert!(carrier.style().border_widths.top.is_none());
assert!(
carrier.style().box_shadow.is_none(),
"the band is an svg, not a shadow"
);
}
#[test]
fn offset_ring_pushes_the_carrier_out_and_paints_the_gap() {
let gap = px(2.);
let mut carrier = ring_overlay_band(px(8.), gap, gpui::red());
assert_eq!(inset(&mut carrier), Length::Definite(px(-2.).into()));
assert_eq!(radius(&mut carrier), AbsoluteLength::Pixels(px(10.)));
let mut inner = ring_overlay_gap(px(8.), gap, gpui::blue());
assert_eq!(inset(&mut inner), Length::Definite(px(0.).into()));
assert_eq!(radius(&mut inner), AbsoluteLength::Pixels(px(10.)));
assert_eq!(border(&mut inner), AbsoluteLength::Pixels(gap));
}
#[gpui::test]
fn the_offset_gap_comes_from_the_layout_theme(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
herogpui_theme::ThemeProvider::init(cx);
let gap = cx.layout().ring_offset_width;
let mut ring = focus_ring_overlay(px(8.), true, cx);
assert_eq!(inset(&mut ring), Length::Definite((-gap).into()));
assert_eq!(radius(&mut ring), AbsoluteLength::Pixels(px(8.) + gap));
let mut flat = focus_ring_overlay(px(8.), false, cx);
assert_eq!(inset(&mut flat), Length::Definite(px(0.).into()));
assert_eq!(radius(&mut flat), AbsoluteLength::Pixels(px(8.)));
});
}
}