1use gpui::{App, BorrowAppContext, Div, Hsla, ParentElement, Pixels, Refineable, Styled};
4use herogpui_core::{element_id, FieldVariant};
5use herogpui_theme::ActiveTheme;
6
7pub(crate) const MONO_FONT: &str = if cfg!(target_family = "wasm") {
9 "JetBrains Mono"
10} else {
11 "Consolas"
12};
13
14pub const FIELD_HEIGHT: Pixels = gpui::px(36.);
21pub const FIELD_TEXT: Pixels = gpui::px(14.);
23pub const FIELD_ICON: Pixels = gpui::px(16.);
25
26pub(crate) fn tabular_font_features() -> gpui::FontFeatures {
31 gpui::FontFeatures(std::sync::Arc::new(vec![("tnum".to_owned(), 1)]))
32}
33
34#[derive(Clone, Copy, Debug, Default)]
41pub(crate) struct FieldBox {
42 pub(crate) height: Option<Pixels>,
43 pub(crate) padding_x: Option<Pixels>,
44 pub(crate) padding_y: Option<Pixels>,
51 pub(crate) is_bare: bool,
52 pub(crate) is_bare_is_set: bool,
53 pub(crate) focus_ring: Option<bool>,
57}
58
59impl FieldBox {
60 pub(crate) fn resolved_height(&self) -> Pixels {
62 self.height.unwrap_or(FIELD_HEIGHT)
63 }
64
65 pub(crate) fn resolved_padding_x(&self) -> Pixels {
67 self.padding_x.unwrap_or(gpui::px(12.))
68 }
69}
70
71pub fn cursor_interactive<T: Styled>(el: T, cx: &App) -> T {
87 el.cursor(interactive_cursor(cx))
88}
89
90pub fn interactive_cursor(cx: &App) -> gpui::CursorStyle {
92 cx.layout().cursor_interactive
93}
94
95pub fn control_radius(cx: &App) -> Pixels {
97 let layout = cx.layout();
98 layout.capped(layout.radius_3xl())
99}
100
101pub fn soft_radius(cx: &App) -> Pixels {
103 let layout = cx.layout();
104 layout.capped(layout.radius_2xl())
105}
106
107pub fn small_radius(cx: &App) -> Pixels {
109 let layout = cx.layout();
110 layout.capped(layout.radius_xl())
111}
112
113pub fn mark_radius(cx: &App) -> Pixels {
116 let layout = cx.layout();
117 layout.capped(layout.radius_md())
118}
119
120pub fn key_radius(cx: &App) -> Pixels {
123 let layout = cx.layout();
124 layout.capped(layout.radius_lg())
125}
126
127pub fn hairline_radius(cx: &App) -> Pixels {
129 let layout = cx.layout();
130 layout.capped(layout.radius_sm())
131}
132
133pub fn micro_radius(cx: &App) -> Pixels {
135 let layout = cx.layout();
136 layout.capped(layout.radius_xs())
137}
138
139pub fn field_radius(cx: &App) -> Pixels {
141 let layout = cx.layout();
142 layout.capped(layout.field_radius)
143}
144
145pub fn container_radius(cx: &App) -> Pixels {
149 let layout = cx.layout();
150 layout.capped(layout.radius_3xl())
151}
152
153pub fn inner_fill_radius(outer: Pixels, border: Pixels) -> Pixels {
172 (outer - border).max(gpui::px(0.))
173}
174
175pub 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
223pub fn apply_field_chrome<T: Styled>(
235 el: T,
236 variant: FieldVariant,
237 is_invalid: bool,
238 is_focused: bool,
239 radius_override: Option<Pixels>,
240 cx: &App,
241) -> T {
242 apply_field_chrome_with_focus_ring(
243 el,
244 variant,
245 is_invalid,
246 is_focused,
247 true,
248 radius_override,
249 cx,
250 )
251}
252
253pub fn apply_field_chrome_with_focus_ring<T: Styled>(
259 el: T,
260 variant: FieldVariant,
261 is_invalid: bool,
262 is_focused: bool,
263 show_focus_ring: bool,
264 radius_override: Option<Pixels>,
265 cx: &App,
266) -> T {
267 let colors = cx.colors();
268 let layout = cx.layout();
269
270 let radius = radius_override.unwrap_or_else(|| field_radius(cx));
271 let mut el = el.rounded(radius).bg(match variant {
272 FieldVariant::Primary => colors.field.background,
273 FieldVariant::Secondary => colors.default.color,
277 });
278
279 let mut shadows = if variant == FieldVariant::Primary {
285 layout.field_shadow.clone()
286 } else {
287 Vec::new()
288 };
289
290 if is_invalid {
291 if is_focused && show_focus_ring {
292 shadows.push(gpui::BoxShadow {
293 color: colors.danger.color,
294 offset: gpui::point(gpui::px(0.), gpui::px(0.)),
295 blur_radius: gpui::px(1.),
296 spread_radius: gpui::px(2.),
297 inset: false,
298 });
299 } else {
300 el = el
301 .border(layout.border_width.max(gpui::px(1.)))
302 .border_color(colors.danger.color);
303 }
304 } else if is_focused && show_focus_ring {
305 shadows.extend(focus_ring_shadows(false, cx));
306 } else if layout.field_border_width > gpui::px(0.) {
307 el = el
308 .border(layout.field_border_width)
309 .border_color(colors.field.border);
310 }
311
312 if shadows.is_empty() {
313 el
314 } else {
315 el.shadow(shadows)
316 }
317}
318
319pub fn apply_field_chrome_ringless<T: Styled>(
331 el: T,
332 variant: FieldVariant,
333 is_invalid: bool,
334 is_focused: bool,
335 show_focus_ring: bool,
336 radius_override: Option<Pixels>,
337 cx: &App,
338) -> T {
339 let colors = cx.colors();
340 let layout = cx.layout();
341
342 let radius = radius_override.unwrap_or_else(|| field_radius(cx));
343 let mut el = el.rounded(radius).bg(match variant {
344 FieldVariant::Primary => colors.field.background,
345 FieldVariant::Secondary => colors.default.color,
346 });
347
348 let shadows = if variant == FieldVariant::Primary {
349 layout.field_shadow.clone()
350 } else {
351 Vec::new()
352 };
353
354 if field_ring_color(is_invalid, is_focused, show_focus_ring, cx).is_none() {
357 if is_invalid {
358 el = el
359 .border(layout.border_width.max(gpui::px(1.)))
360 .border_color(colors.danger.color);
361 } else if layout.field_border_width > gpui::px(0.) {
362 el = el
363 .border(layout.field_border_width)
364 .border_color(colors.field.border);
365 }
366 }
367
368 if shadows.is_empty() {
369 el
370 } else {
371 el.shadow(shadows)
372 }
373}
374
375pub fn field_ring_color(
383 is_invalid: bool,
384 is_focused: bool,
385 show_focus_ring: bool,
386 cx: &App,
387) -> Option<Hsla> {
388 if !(is_focused && show_focus_ring) {
389 return None;
390 }
391 Some(if is_invalid {
392 cx.colors().danger.color
393 } else {
394 cx.colors().focus
395 })
396}
397
398pub fn with_field_ring_overlay<T: ParentElement>(
401 el: T,
402 ring: Option<Hsla>,
403 radius: Pixels,
404 cx: &App,
405) -> T {
406 match ring {
407 Some(color) => el.child(ring_overlay_in(radius, false, color, cx)),
408 None => el,
409 }
410}
411
412pub fn field_ring_carrier(
428 shell: impl gpui::IntoElement,
429 ring: Option<Hsla>,
430 radius: Pixels,
431 cx: &App,
432) -> Div {
433 with_field_ring_overlay(
434 gpui::div().relative().flex().flex_col().child(shell),
435 ring,
436 radius,
437 cx,
438 )
439}
440
441pub fn apply_field_chrome_overlay<T: Styled + ParentElement>(
450 el: T,
451 variant: FieldVariant,
452 is_invalid: bool,
453 is_focused: bool,
454 show_focus_ring: bool,
455 radius_override: Option<Pixels>,
456 cx: &App,
457) -> T {
458 let radius = radius_override.unwrap_or_else(|| field_radius(cx));
459 let ring = field_ring_color(is_invalid, is_focused, show_focus_ring, cx);
460 let el = apply_field_chrome_ringless(
461 el,
462 variant,
463 is_invalid,
464 is_focused,
465 show_focus_ring,
466 Some(radius),
467 cx,
468 );
469 with_field_ring_overlay(el, ring, radius, cx)
470}
471
472pub(crate) fn paint_disc(
477 center: gpui::Point<Pixels>,
478 radius: Pixels,
479 color: Hsla,
480 window: &mut gpui::Window,
481) {
482 let mut builder = gpui::PathBuilder::fill();
483 for step in 0..16 {
484 let angle = std::f32::consts::TAU * step as f32 / 16.;
485 let point = gpui::point(
486 center.x + radius * angle.cos(),
487 center.y + radius * angle.sin(),
488 );
489 if step == 0 {
490 builder.move_to(point);
491 } else {
492 builder.line_to(point);
493 }
494 }
495 builder.close();
496 if let Ok(path) = builder.build() {
497 window.paint_path(path, color);
498 }
499}
500
501pub fn floating(el: impl gpui::IntoElement) -> gpui::Deferred {
509 gpui::deferred(el)
510}
511
512pub(crate) fn window_overlay(el: impl gpui::IntoElement, window: &gpui::Window) -> gpui::Deferred {
513 use gpui::InteractiveElement;
514
515 let viewport = window.viewport_size();
516 floating(
517 gpui::anchored()
518 .position(gpui::point(gpui::px(0.), gpui::px(0.)))
519 .child(
520 gpui::div()
521 .relative()
522 .occlude()
523 .w(viewport.width)
524 .h(viewport.height)
525 .flex_shrink_0()
526 .child(el),
527 ),
528 )
529}
530
531#[derive(Clone, Copy, Debug, PartialEq, Eq)]
536pub enum DismissResult {
537 Handled,
539 Declined,
541}
542
543#[derive(Default)]
544struct OverlayStack {
545 entries: Vec<gpui::WeakEntity<OverlayRegistration>>,
546 next_order: u64,
547}
548
549impl gpui::Global for OverlayStack {}
550
551#[derive(Clone)]
552struct OverlayRegistration {
553 window_id: gpui::WindowId,
554 order: u64,
555 phase: OverlayPhase,
556 keep_exiting: bool,
557 exit_generation: u64,
558 escape_capture: Option<std::sync::Arc<dyn Fn(&mut gpui::Window, &mut App) -> DismissResult>>,
559}
560
561#[derive(Clone)]
566pub struct OverlayToken {
567 registration: gpui::WeakEntity<OverlayRegistration>,
568 window_id: gpui::WindowId,
569}
570
571fn ensure_overlay_stack(cx: &mut App) {
572 if cx.try_global::<OverlayStack>().is_none() {
573 cx.set_global(OverlayStack::default());
574 }
575}
576
577fn prune_overlay_stack(stack: &mut OverlayStack, cx: &App) {
578 stack.entries.retain(|entry| {
579 let Some(entry) = entry.upgrade() else {
580 return false;
581 };
582 entry.read(cx).phase == OverlayPhase::Open
583 });
584}
585
586fn sync_overlay_stack(registration: &gpui::Entity<OverlayRegistration>, cx: &mut App) {
587 ensure_overlay_stack(cx);
588 let weak = registration.downgrade();
589 let active = registration.read(cx).phase == OverlayPhase::Open;
590 cx.update_global::<OverlayStack, _>(|stack, cx| {
591 prune_overlay_stack(stack, cx);
592 if active && !stack.entries.iter().any(|entry| entry == &weak) {
593 stack.entries.push(weak);
594 }
595 });
596}
597
598fn is_topmost(token: &OverlayToken, cx: &mut App) -> bool {
599 ensure_overlay_stack(cx);
600 cx.update_global::<OverlayStack, _>(|stack, cx| {
601 prune_overlay_stack(stack, cx);
602 let Some(registration) = token.registration.upgrade() else {
603 return false;
604 };
605 let state = registration.read(cx);
606 if state.window_id != token.window_id || state.phase != OverlayPhase::Open {
607 return false;
608 }
609 stack
610 .entries
611 .iter()
612 .filter_map(|entry| entry.upgrade())
613 .filter(|entry| entry.read(cx).window_id == token.window_id)
614 .max_by_key(|entry| entry.read(cx).order)
615 .is_some_and(|entry| entry == registration)
616 })
617}
618
619pub fn capture_escape(
627 token: &OverlayToken,
628 handler: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
629 cx: &mut App,
630) {
631 if let Some(registration) = token.registration.upgrade() {
632 let handler = shared(handler);
633 registration.update(cx, |state, _| state.escape_capture = Some(handler));
634 }
635}
636
637fn dismiss_captured_escape(window: &mut gpui::Window, cx: &mut App) -> bool {
638 let window_id = window.window_handle().window_id();
639 ensure_overlay_stack(cx);
640 let handler = cx.update_global::<OverlayStack, _>(|stack, cx| {
641 prune_overlay_stack(stack, cx);
642 stack
643 .entries
644 .iter()
645 .filter_map(|entry| entry.upgrade())
646 .filter(|entry| entry.read(cx).window_id == window_id)
647 .filter_map(|entry| {
648 let state = entry.read(cx);
649 state
650 .escape_capture
651 .clone()
652 .map(|handler| (state.order, handler))
653 })
654 .max_by_key(|(order, _)| *order)
655 .map(|(_, handler)| handler)
656 });
657 handler.is_some_and(|handler| handler(window, cx) == DismissResult::Handled)
658}
659
660pub fn dismiss_on_escape_with_token<E: gpui::InteractiveElement>(
675 el: E,
676 token: OverlayToken,
677 close: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
678) -> E {
679 el.on_key_down(move |event: &gpui::KeyDownEvent, window, cx| {
680 if event.keystroke.key == "escape"
681 && is_topmost(&token, cx)
682 && close(window, cx) == DismissResult::Handled
683 {
684 cx.stop_propagation();
685 }
686 })
687}
688
689pub fn dismiss_on_press_outside_with_token<E: gpui::InteractiveElement>(
694 el: E,
695 token: OverlayToken,
696 close: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
697) -> E {
698 dismiss_on_press_outside_with_token_event(el, token, move |_, window, cx| close(window, cx))
699}
700
701pub fn dismiss_on_press_outside_with_token_event<E: gpui::InteractiveElement>(
707 el: E,
708 token: OverlayToken,
709 close: impl Fn(&gpui::MouseDownEvent, &mut gpui::Window, &mut App) -> DismissResult + 'static,
710) -> E {
711 el.on_mouse_down_out(move |event, window, cx| {
712 if is_topmost(&token, cx) && close(event, window, cx) == DismissResult::Handled {
713 cx.stop_propagation();
714 }
715 })
716}
717
718#[derive(Clone, Debug, Default)]
730struct PanelFocusState {
731 was_open: bool,
732 restore: Option<gpui::WeakFocusHandle>,
733}
734
735pub fn panel_restore_focus(
741 window: &mut gpui::Window,
742 cx: &mut App,
743 base: &gpui::ElementId,
744) -> gpui::FocusHandle {
745 window
746 .use_keyed_state(
747 element_id::scoped(base, "panel-restore-focus"),
748 cx,
749 |_, cx| cx.focus_handle(),
750 )
751 .read(cx)
752 .clone()
753}
754
755pub fn panel_focus(
756 window: &mut gpui::Window,
757 cx: &mut App,
758 base: &gpui::ElementId,
759 open: bool,
760) -> gpui::FocusHandle {
761 let held = window.use_keyed_state(element_id::scoped(base, "panel-focus"), cx, |_, cx| {
762 cx.focus_handle()
763 });
764 let handle = held.read(cx).clone();
765 let state =
766 window.use_keyed_state(element_id::scoped(base, "panel-focus-state"), cx, |_, _| {
767 PanelFocusState::default()
768 });
769 let current = state.read(cx).clone();
770
771 if open && !current.was_open {
772 let trigger = panel_restore_focus(window, cx, base);
773 let restore = window
778 .focused(cx)
779 .filter(|focused| focused.tab_stop)
780 .map(|focused| focused.downgrade())
781 .or_else(|| Some(trigger.downgrade()));
782 window.focus(&handle, cx);
783 state.update(cx, |state, _| {
784 state.was_open = true;
785 state.restore = restore;
786 });
787 } else if !open && current.was_open {
788 if handle.contains_focused(window, cx) {
789 if let Some(restore) = current.restore.and_then(|handle| handle.upgrade()) {
790 window.focus(&restore, cx);
791 }
792 }
793 state.update(cx, |state, _| {
794 state.was_open = false;
795 state.restore = None;
796 });
797 }
798 handle
799}
800
801#[derive(Clone, Copy, Debug, Default)]
809struct CloseOnBlurState {
810 seen_inside: bool,
812}
813
814#[derive(Clone)]
815pub struct FocusLeave {
816 handle: gpui::FocusHandle,
817 subscription: gpui::Entity<Option<gpui::Subscription>>,
818 state: gpui::Entity<CloseOnBlurState>,
819}
820
821impl FocusLeave {
822 pub fn focus_handle(&self) -> gpui::FocusHandle {
823 self.handle.clone()
824 }
825
826 pub fn consume(&self, cx: &mut App) {
828 self.state.update(cx, |state, _| state.seen_inside = false);
829 self.subscription.update(cx, |slot, _| *slot = None);
830 }
831}
832
833pub fn close_on_blur(
834 window: &mut gpui::Window,
835 cx: &mut App,
836 base: &gpui::ElementId,
837 open: bool,
838 close: impl Fn(&mut gpui::Window, &mut App) + 'static,
839) -> gpui::FocusHandle {
840 on_focus_leave(window, cx, base, open, close).focus_handle()
841}
842
843pub fn on_focus_leave(
845 window: &mut gpui::Window,
846 cx: &mut App,
847 base: &gpui::ElementId,
848 active: bool,
849 leave: impl Fn(&mut gpui::Window, &mut App) + 'static,
850) -> FocusLeave {
851 let held_scope = window.use_keyed_state(
852 element_id::scoped(base, "close-on-blur-scope"),
853 cx,
854 |_, cx| cx.focus_handle().tab_stop(false),
855 );
856 let scope = held_scope.read(cx).clone();
857 let subscription = window.use_keyed_state(
860 element_id::scoped(base, "close-on-blur-subscription"),
861 cx,
862 |_, _| None::<gpui::Subscription>,
863 );
864 let state = window.use_keyed_state(
865 element_id::scoped(base, "close-on-blur-state"),
866 cx,
867 |_, _| CloseOnBlurState::default(),
868 );
869 let armed = subscription.read(cx).is_some();
870 let leave = std::rc::Rc::new(leave);
873
874 if active && !armed {
878 let disarmer = subscription.downgrade();
881 let edge = state.downgrade();
882 let leave = std::rc::Rc::clone(&leave);
883 let listener = window.on_focus_out(&scope, cx, move |_, window, cx| {
884 let Some(edge) = edge.upgrade() else {
885 return;
886 };
887 let due = edge.read(cx).seen_inside;
888 if let Some(disarmer) = disarmer.upgrade() {
889 disarmer.update(cx, |slot, _| *slot = None);
890 }
891 if !due {
892 return;
893 }
894 edge.update(cx, |state, _| state.seen_inside = false);
895 leave(window, cx);
896 });
897 subscription.update(cx, |slot, _| *slot = Some(listener));
898 }
899
900 if active {
907 if scope.contains_focused(window, cx) {
908 state.update(cx, |state, _| state.seen_inside = true);
909 } else if state.read(cx).seen_inside
910 && window.focused(cx).is_some_and(|focused| focused.tab_stop)
911 {
912 state.update(cx, |state, _| state.seen_inside = false);
913 subscription.update(cx, |slot, _| *slot = None);
914 leave(window, cx);
915 }
916 } else {
917 if state.read(cx).seen_inside {
918 state.update(cx, |state, _| state.seen_inside = false);
919 }
920 if armed {
921 subscription.update(cx, |slot, _| *slot = None);
922 }
923 }
924
925 FocusLeave {
926 handle: scope,
927 subscription,
928 state,
929 }
930}
931
932#[allow(clippy::arc_with_non_send_sync)]
939pub fn shared<F: 'static>(f: F) -> std::sync::Arc<F> {
940 std::sync::Arc::new(f)
941}
942
943pub fn focus_once(
949 window: &mut gpui::Window,
950 cx: &mut App,
951 key: impl Into<gpui::ElementId>,
952 handle: &gpui::FocusHandle,
953) {
954 let done = window.use_keyed_state(key.into(), cx, |_, _| false);
955 if !*done.read(cx) {
956 window.focus(handle, cx);
957 done.update(cx, |d, _| *d = true);
958 }
959}
960
961#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
963pub enum OverlayPhase {
964 #[default]
966 Closed,
967 Open,
969 Exiting,
971}
972
973#[derive(Clone, Copy, Debug, Default)]
975struct PhaseState {
976 was_open: bool,
977 exiting: bool,
978 exit_generation: u64,
979}
980
981pub fn overlay_scope(
987 window: &mut gpui::Window,
988 cx: &mut App,
989 key: impl Into<gpui::ElementId>,
990 is_open: bool,
991 keep_exiting: bool,
992) -> (OverlayPhase, OverlayToken) {
993 overlay_scope_with_exit(
994 window,
995 cx,
996 key,
997 is_open,
998 keep_exiting,
999 crate::anim::EXITING_MS,
1000 )
1001}
1002
1003pub fn overlay_scope_with_exit(
1005 window: &mut gpui::Window,
1006 cx: &mut App,
1007 key: impl Into<gpui::ElementId>,
1008 is_open: bool,
1009 keep_exiting: bool,
1010 exit_ms: u64,
1011) -> (OverlayPhase, OverlayToken) {
1012 let key = key.into();
1013 let window_id = window.window_handle().window_id();
1014 let registration = window.use_keyed_state(key, cx, |_, _| OverlayRegistration {
1015 window_id,
1016 order: 0,
1017 phase: OverlayPhase::Closed,
1018 keep_exiting,
1019 exit_generation: 0,
1020 escape_capture: None,
1021 });
1022 let current = registration.read(cx).clone();
1023
1024 let phase = if is_open {
1025 if current.phase != OverlayPhase::Open {
1026 ensure_overlay_stack(cx);
1027 let order = cx.update_global::<OverlayStack, _>(|stack, _| {
1028 stack.next_order = stack.next_order.saturating_add(1);
1029 stack.next_order
1030 });
1031 registration.update(cx, |state, _| {
1032 state.order = order;
1033 state.phase = OverlayPhase::Open;
1034 state.keep_exiting = keep_exiting;
1035 });
1036 }
1037 OverlayPhase::Open
1038 } else if current.phase == OverlayPhase::Open && keep_exiting {
1039 let exit_generation = current.exit_generation.saturating_add(1);
1040 registration.update(cx, |state, _| {
1041 state.phase = OverlayPhase::Exiting;
1042 state.keep_exiting = keep_exiting;
1043 state.exit_generation = exit_generation;
1044 });
1045 let held = registration.downgrade();
1046 cx.spawn(async move |cx: &mut gpui::AsyncApp| {
1047 cx.background_executor()
1048 .timer(std::time::Duration::from_millis(exit_ms))
1049 .await;
1050 cx.update(|cx| {
1051 let _ = held.update(cx, |state, cx| {
1052 if state.phase == OverlayPhase::Exiting
1053 && state.exit_generation == exit_generation
1054 {
1055 state.phase = OverlayPhase::Closed;
1056 cx.notify();
1057 }
1058 });
1059 });
1060 })
1061 .detach();
1062 OverlayPhase::Exiting
1063 } else if current.phase == OverlayPhase::Open {
1064 registration.update(cx, |state, _| {
1065 state.phase = OverlayPhase::Closed;
1066 state.keep_exiting = keep_exiting;
1067 });
1068 OverlayPhase::Closed
1069 } else if current.phase == OverlayPhase::Exiting {
1070 OverlayPhase::Exiting
1071 } else {
1072 if current.keep_exiting != keep_exiting {
1073 registration.update(cx, |state, _| state.keep_exiting = keep_exiting);
1074 }
1075 OverlayPhase::Closed
1076 };
1077
1078 sync_overlay_stack(®istration, cx);
1079 (
1080 phase,
1081 OverlayToken {
1082 registration: registration.downgrade(),
1083 window_id,
1084 },
1085 )
1086}
1087
1088pub fn overlay_phase(
1101 window: &mut gpui::Window,
1102 cx: &mut App,
1103 key: impl Into<gpui::ElementId>,
1104 is_open: bool,
1105) -> OverlayPhase {
1106 retained_phase(window, cx, key, is_open, true, crate::anim::EXITING_MS)
1107}
1108
1109pub fn panel_phase(
1114 window: &mut gpui::Window,
1115 cx: &mut App,
1116 key: impl Into<gpui::ElementId>,
1117 is_open: bool,
1118 keep_exiting: bool,
1119 exit_ms: u64,
1120) -> OverlayPhase {
1121 retained_phase(window, cx, key, is_open, keep_exiting, exit_ms)
1122}
1123
1124fn retained_phase(
1125 window: &mut gpui::Window,
1126 cx: &mut App,
1127 key: impl Into<gpui::ElementId>,
1128 is_open: bool,
1129 keep_exiting: bool,
1130 exit_ms: u64,
1131) -> OverlayPhase {
1132 let key = key.into();
1133 let held = window.use_keyed_state(key, cx, |_, _| PhaseState::default());
1134 let current = *held.read(cx);
1135
1136 if is_open {
1137 if !current.was_open {
1138 held.update(cx, |s, _| {
1139 s.was_open = true;
1140 s.exiting = false;
1141 });
1142 }
1143 OverlayPhase::Open
1144 } else if current.was_open && keep_exiting {
1145 let exit_generation = current.exit_generation.saturating_add(1);
1147 held.update(cx, |s, _| {
1148 s.was_open = false;
1149 s.exiting = true;
1150 s.exit_generation = exit_generation;
1151 });
1152 let held = held.clone();
1153 cx.spawn(async move |cx: &mut gpui::AsyncApp| {
1154 cx.background_executor()
1155 .timer(std::time::Duration::from_millis(exit_ms))
1156 .await;
1157 cx.update(|cx| {
1158 held.update(cx, |s, cx| {
1159 if s.exiting && s.exit_generation == exit_generation {
1160 s.exiting = false;
1161 cx.notify();
1162 }
1163 });
1164 });
1165 })
1166 .detach();
1167 OverlayPhase::Exiting
1168 } else if current.was_open {
1169 held.update(cx, |s, _| {
1170 s.was_open = false;
1171 s.exiting = false;
1172 });
1173 OverlayPhase::Closed
1174 } else if current.exiting {
1175 OverlayPhase::Exiting
1176 } else {
1177 OverlayPhase::Closed
1178 }
1179}
1180
1181pub fn seed_once(
1188 window: &mut gpui::Window,
1189 cx: &mut App,
1190 key: impl Into<gpui::ElementId>,
1191 apply: impl FnOnce(&mut App),
1192) {
1193 let done = window.use_keyed_state(key.into(), cx, |_, _| false);
1194 if !*done.read(cx) {
1195 done.update(cx, |d, _| *d = true);
1196 apply(cx);
1197 }
1198}
1199
1200pub fn controlled<T>(
1210 window: &mut gpui::Window,
1211 cx: &mut App,
1212 key: impl Into<gpui::ElementId>,
1213 controlled: Option<T>,
1214 default: T,
1215) -> (T, Option<gpui::Entity<T>>)
1216where
1217 T: Clone + 'static,
1218{
1219 match controlled {
1220 Some(v) => (v, None),
1222 None => {
1223 let held = window.use_keyed_state(key.into(), cx, move |_, _| default);
1224 let current = held.read(cx).clone();
1225 (current, Some(held))
1226 }
1227 }
1228}
1229
1230struct FocusVisible(bool);
1242impl gpui::Global for FocusVisible {}
1243
1244#[derive(Default)]
1245struct ActiveKeyboardPresses(Vec<(gpui::WindowId, String, gpui::WeakEntity<(bool, bool)>)>);
1246impl gpui::Global for ActiveKeyboardPresses {}
1247
1248#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1261#[non_exhaustive]
1262pub struct FieldFocus {
1263 pub is_focused: bool,
1265 pub is_focus_within: bool,
1267 pub is_focus_visible: bool,
1269}
1270
1271#[non_exhaustive]
1279pub struct SelectionValue<'a> {
1280 pub selected_items: &'a [gpui::SharedString],
1285 pub selected_indices: &'a [usize],
1288 pub selected_keys: Option<&'a [gpui::SharedString]>,
1296 pub selected_text: &'a str,
1299 pub is_placeholder: bool,
1301 pub default_children: gpui::AnyElement,
1303}
1304
1305#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1314#[non_exhaustive]
1315pub struct InteractiveState {
1316 pub is_hovered: bool,
1319 pub is_pressed: bool,
1321 pub is_focused: bool,
1323 pub is_focus_visible: bool,
1325 pub is_selected: bool,
1327 pub is_disabled: bool,
1329 pub is_pending: bool,
1331 pub is_indeterminate: bool,
1334}
1335
1336pub type Interaction = gpui::Entity<(bool, bool)>;
1338
1339fn has_active_keyboard_press(slot: &Interaction, window: &gpui::Window, cx: &App) -> bool {
1340 let weak = slot.downgrade();
1341 let window_id = window.window_handle().window_id();
1342 cx.try_global::<ActiveKeyboardPresses>()
1343 .is_some_and(|pressed| {
1344 pressed.0.iter().any(|(active_window, _, interaction)| {
1345 *active_window == window_id && interaction == &weak
1346 })
1347 })
1348}
1349
1350pub(crate) fn begin_keyboard_press(
1351 slot: &Interaction,
1352 event: &gpui::KeyDownEvent,
1353 window: &gpui::Window,
1354 cx: &mut App,
1355) {
1356 if event.is_held || !matches!(event.keystroke.key.as_str(), "enter" | "space") {
1357 return;
1358 }
1359 let began = slot.update(cx, |state, cx| {
1360 if state.1 {
1361 false
1362 } else {
1363 state.1 = true;
1364 cx.notify();
1365 true
1366 }
1367 });
1368 if began {
1369 if cx.try_global::<ActiveKeyboardPresses>().is_none() {
1370 cx.set_global(ActiveKeyboardPresses::default());
1371 }
1372 let active = (
1373 window.window_handle().window_id(),
1374 event.keystroke.key.clone(),
1375 slot.downgrade(),
1376 );
1377 cx.update_global::<ActiveKeyboardPresses, _>(|pressed, _| {
1378 pressed
1379 .0
1380 .retain(|(_, _, interaction)| interaction.upgrade().is_some());
1381 pressed.0.push(active);
1382 });
1383 }
1384}
1385
1386pub fn interaction(id: gpui::ElementId, window: &mut gpui::Window, cx: &mut App) -> Interaction {
1392 window.use_keyed_state(id, cx, |_, _| (false, false))
1393}
1394
1395pub fn track_interaction<T>(el: T, slot: &Interaction) -> T
1397where
1398 T: gpui::StatefulInteractiveElement + ParentElement,
1399{
1400 track_interaction_on_mouse_down(el, slot, |_, _| {})
1401}
1402
1403pub(crate) fn track_interaction_on_mouse_down<T>(
1406 el: T,
1407 slot: &Interaction,
1408 on_mouse_down: impl Fn(&mut gpui::Window, &mut App) + 'static,
1409) -> T
1410where
1411 T: gpui::StatefulInteractiveElement + ParentElement,
1412{
1413 let hover = slot.clone();
1414 let down = slot.clone();
1415 let up = slot.clone();
1416 let key_down = slot.clone();
1417 let key_up = slot.clone();
1418 let outside_up = slot.clone();
1419 let release = gpui::canvas(
1427 |bounds, _, _| bounds,
1428 move |_, _, window, _| {
1429 window.on_mouse_event(move |event: &gpui::MouseUpEvent, phase, window, cx| {
1430 if phase == gpui::DispatchPhase::Capture
1431 && event.button == gpui::MouseButton::Left
1432 && !has_active_keyboard_press(&outside_up, window, cx)
1433 {
1434 outside_up.update(cx, |state, cx| {
1435 if state.1 {
1436 state.1 = false;
1437 cx.notify();
1438 }
1439 });
1440 }
1441 });
1442 },
1443 )
1444 .absolute()
1445 .inset_0();
1446 el.on_hover(move |over, _, cx| {
1447 let over = *over;
1448 hover.update(cx, |state, cx| {
1449 if state.0 != over {
1450 state.0 = over;
1451 cx.notify();
1452 }
1453 });
1454 })
1455 .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
1456 down.update(cx, |state, cx| {
1457 if !state.1 {
1458 state.1 = true;
1459 cx.notify();
1460 }
1461 });
1462 on_mouse_down(window, cx);
1463 })
1464 .on_mouse_up(gpui::MouseButton::Left, move |_, window, cx| {
1465 if !has_active_keyboard_press(&up, window, cx) {
1466 up.update(cx, |state, cx| {
1467 if state.1 {
1468 state.1 = false;
1469 cx.notify();
1470 }
1471 });
1472 }
1473 })
1474 .on_key_down(move |event, window, cx| {
1475 begin_keyboard_press(&key_down, event, window, cx);
1476 })
1477 .on_key_up(move |event, _, cx| {
1478 if matches!(event.keystroke.key.as_str(), "enter" | "space") {
1479 key_up.update(cx, |state, cx| {
1480 if state.1 {
1481 state.1 = false;
1482 cx.notify();
1483 }
1484 });
1485 }
1486 })
1487 .child(release)
1488}
1489
1490pub fn focus_visible(cx: &App) -> bool {
1492 cx.try_global::<FocusVisible>().is_some_and(|v| v.0)
1493}
1494
1495pub fn set_focus_visible(visible: bool, cx: &mut App) {
1497 if focus_visible(cx) != visible {
1498 cx.set_global(FocusVisible(visible));
1499 cx.refresh_windows();
1500 }
1501}
1502
1503pub(crate) fn shows_focus_ring(target: bool, cx: &App) -> bool {
1505 target && focus_visible(cx)
1506}
1507
1508pub(crate) fn key_enables_focus_visible(key: &str) -> bool {
1514 !matches!(
1515 key,
1516 "escape"
1517 | "shift"
1518 | "control"
1519 | "ctrl"
1520 | "alt"
1521 | "option"
1522 | "meta"
1523 | "command"
1524 | "win"
1525 | "windows"
1526 | "fn"
1527 | "function"
1528 )
1529}
1530
1531pub fn app_focus_root<T>(el: T, window: &mut gpui::Window, cx: &mut App) -> T
1546where
1547 T: gpui::InteractiveElement,
1548{
1549 let root = window
1550 .use_keyed_state(
1551 gpui::ElementId::Name("herogpui-focus-root".into()),
1552 cx,
1553 |_, cx| cx.focus_handle(),
1554 )
1555 .read(cx)
1556 .clone();
1557 if !root.contains_focused(window, cx) {
1558 window.focus(&root, cx);
1559 }
1560 el.track_focus(&root)
1561 .capture_any_mouse_down(|_, _, cx| set_focus_visible(false, cx))
1562 .capture_key_down(|event, window, cx| {
1563 if event.keystroke.key == "escape" && dismiss_captured_escape(window, cx) {
1564 cx.stop_propagation();
1565 }
1566 })
1567 .on_key_down(|event, window, cx| {
1568 if key_enables_focus_visible(&event.keystroke.key) {
1569 set_focus_visible(true, cx);
1570 }
1571 if event.keystroke.key == "tab" {
1572 if event.keystroke.modifiers.shift {
1573 window.focus_prev(cx);
1574 } else {
1575 window.focus_next(cx);
1576 }
1577 cx.stop_propagation();
1578 }
1579 })
1580 .on_key_up(|event, window, cx| {
1581 if !matches!(event.keystroke.key.as_str(), "enter" | "space")
1582 || cx.try_global::<ActiveKeyboardPresses>().is_none()
1583 {
1584 return;
1585 }
1586 let window_id = window.window_handle().window_id();
1587 let key = event.keystroke.key.as_str();
1588 let interactions = cx.update_global::<ActiveKeyboardPresses, _>(|pressed, _| {
1589 let mut released = Vec::new();
1590 pressed
1591 .0
1592 .retain(|(active_window, active_key, interaction)| {
1593 if *active_window == window_id && active_key == key {
1594 released.push(interaction.clone());
1595 false
1596 } else {
1597 interaction.upgrade().is_some()
1598 }
1599 });
1600 released
1601 });
1602 for interaction in interactions {
1603 if let Some(interaction) = interaction.upgrade() {
1604 interaction.update(cx, |state, cx| {
1605 if state.1 {
1606 state.1 = false;
1607 cx.notify();
1608 }
1609 });
1610 }
1611 }
1612 })
1613}
1614
1615pub fn tab_stop_handle(
1622 id: gpui::ElementId,
1623 window: &mut gpui::Window,
1624 cx: &mut App,
1625) -> gpui::FocusHandle {
1626 window
1627 .use_keyed_state(id, cx, |_, cx| cx.focus_handle().tab_stop(true))
1628 .read(cx)
1629 .clone()
1630}
1631
1632const RING_WIDTH: Pixels = gpui::px(2.);
1635
1636pub fn focus_ring_shadows(offset: bool, cx: &App) -> Vec<gpui::BoxShadow> {
1655 let colors = cx.colors();
1656 let layout = cx.layout();
1657 let ring = RING_WIDTH;
1658 let blur = gpui::px(1.);
1659 let gap = if offset {
1660 layout.ring_offset_width
1661 } else {
1662 gpui::px(0.)
1663 };
1664 let mut shadows = vec![gpui::BoxShadow {
1665 color: colors.focus,
1666 offset: gpui::point(gpui::px(0.), gpui::px(0.)),
1667 blur_radius: blur,
1668 spread_radius: gap + ring,
1669 inset: false,
1670 }];
1671 if gap > gpui::px(0.) {
1672 shadows.push(gpui::BoxShadow {
1673 color: colors.background,
1674 offset: gpui::point(gpui::px(0.), gpui::px(0.)),
1675 blur_radius: blur,
1676 spread_radius: gap,
1677 inset: false,
1678 });
1679 }
1680 shadows
1681}
1682
1683pub fn focus_ring_overlay(radius: Pixels, offset: bool, cx: &App) -> Div {
1722 ring_overlay_in(radius, offset, cx.colors().focus, cx)
1723}
1724
1725pub(crate) fn ring_overlay_in(radius: Pixels, offset: bool, color: Hsla, cx: &App) -> Div {
1728 let gap = if offset {
1729 cx.layout().ring_offset_width
1730 } else {
1731 gpui::px(0.)
1732 };
1733 let outer = ring_overlay_band(radius, gap, color);
1734 if gap > gpui::px(0.) {
1735 outer.child(ring_overlay_gap(radius, gap, cx.colors().background))
1736 } else {
1737 outer
1738 }
1739}
1740
1741const RING_BLUR_REACH: Pixels = gpui::px(3.);
1743const RING_BLUR_SIGMA: f32 = 0.7;
1746const RING_UNDERLAP: f32 = 1.5;
1750
1751fn ring_overlay_band(radius: Pixels, gap: Pixels, color: Hsla) -> Div {
1766 let reach = RING_WIDTH + RING_BLUR_REACH;
1767 let corner = radius + gap;
1768 let canvas = gpui::canvas(
1769 |_, _, _| (),
1770 move |bounds, _, window, cx| {
1771 let w = f32::from(bounds.size.width);
1772 let h = f32::from(bounds.size.height);
1773 if w <= 0. || h <= 0. {
1774 return;
1775 }
1776 let ring = f32::from(RING_WIDTH);
1777 let inner = f32::from(reach);
1778 let rg = f32::from(corner);
1779 let stroke_w = ring + RING_UNDERLAP;
1780 let stroke_at = f32::from(RING_BLUR_REACH) + stroke_w / 2.;
1781 let svg = format!(
1782 concat!(
1783 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{w:.2}\" height=\"{h:.2}\" ",
1784 "viewBox=\"0 0 {w:.2} {h:.2}\"><defs>",
1785 "<filter id=\"b\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">",
1786 "<feGaussianBlur stdDeviation=\"{sigma:.3}\"/></filter>",
1787 "<mask id=\"m\"><rect width=\"{w:.2}\" height=\"{h:.2}\" fill=\"#fff\"/>",
1788 "<rect x=\"{inner:.2}\" y=\"{inner:.2}\" width=\"{iw:.2}\" height=\"{ih:.2}\" rx=\"{rg:.2}\" fill=\"#000\"/>",
1789 "</mask></defs><g mask=\"url(#m)\">",
1790 "<rect x=\"{sx:.2}\" y=\"{sx:.2}\" width=\"{sw:.2}\" height=\"{sh:.2}\" rx=\"{srx:.2}\" ",
1791 "fill=\"none\" stroke=\"#000\" stroke-width=\"{ring:.2}\" filter=\"url(#b)\"/></g></svg>"
1792 ),
1793 w = w,
1794 h = h,
1795 sigma = RING_BLUR_SIGMA,
1796 inner = inner,
1797 iw = (w - 2. * inner).max(0.),
1798 ih = (h - 2. * inner).max(0.),
1799 rg = rg,
1800 sx = stroke_at,
1801 sw = (w - 2. * stroke_at).max(0.),
1802 sh = (h - 2. * stroke_at).max(0.),
1803 srx = (rg + ring - stroke_w / 2.).max(0.),
1804 ring = stroke_w,
1805 );
1806 let key: gpui::SharedString =
1807 format!("herogpui://focus-ring/{w:.1}x{h:.1}/r{rg:.1}").into();
1808 let _ = window.paint_svg(
1809 bounds,
1810 key,
1811 Some(svg.as_bytes()),
1812 gpui::TransformationMatrix::unit(),
1813 color,
1814 cx,
1815 );
1816 },
1817 )
1818 .absolute()
1819 .inset(-reach);
1820 gpui::div()
1821 .absolute()
1822 .inset(-gap)
1823 .rounded(corner)
1824 .child(canvas)
1825}
1826
1827fn ring_overlay_gap(radius: Pixels, gap: Pixels, background: Hsla) -> Div {
1831 gpui::div()
1832 .absolute()
1833 .inset(gpui::px(0.))
1834 .rounded(radius + gap)
1835 .border(gap)
1836 .border_color(background)
1837}
1838
1839pub fn with_focus_ring_overlay<T: Styled + ParentElement>(
1846 el: T,
1847 focused: bool,
1848 offset: bool,
1849 radius: Pixels,
1850 base: Vec<gpui::BoxShadow>,
1851 cx: &App,
1852) -> T {
1853 let el = if base.is_empty() { el } else { el.shadow(base) };
1854 if !focused {
1855 return el;
1856 }
1857 el.child(focus_ring_overlay(radius, offset, cx))
1858}
1859
1860pub fn ring_overlay_if_focused<T: Styled + ParentElement>(
1862 el: T,
1863 handle: &gpui::FocusHandle,
1864 offset: bool,
1865 radius: Pixels,
1866 base: Vec<gpui::BoxShadow>,
1867 window: &gpui::Window,
1868 cx: &App,
1869) -> T {
1870 let focused = shows_focus_ring(handle.is_focused(window), cx);
1871 with_focus_ring_overlay(el, focused, offset, radius, base, cx)
1872}
1873
1874pub fn trap_tab<T: gpui::InteractiveElement>(el: T, scope: &gpui::FocusHandle) -> T {
1887 let scope = scope.clone();
1888 el.on_key_down(move |event: &gpui::KeyDownEvent, window, cx| {
1889 if event.keystroke.key != "tab" {
1890 return;
1891 }
1892 cx.stop_propagation();
1896 set_focus_visible(true, cx);
1897 let back = event.keystroke.modifiers.shift;
1898 if back {
1899 window.focus_prev(cx);
1900 } else {
1901 window.focus_next(cx);
1902 }
1903 if scope.contains_focused(window, cx) {
1904 return;
1905 }
1906 window.focus(&scope, cx);
1908 window.focus_next(cx);
1909 if !back {
1910 return;
1911 }
1912 for _ in 0..256 {
1914 window.focus_next(cx);
1915 if !scope.contains_focused(window, cx) {
1916 window.focus_prev(cx);
1917 return;
1918 }
1919 }
1920 })
1921}
1922
1923pub fn inset_focus_ring(cx: &App) -> Div {
1937 let colors = cx.colors();
1938 gpui::div()
1939 .absolute()
1940 .inset_0()
1941 .border_2()
1942 .border_color(colors.focus)
1943 .rounded(key_radius(cx))
1944}
1945
1946pub fn with_focus_ring<T: Styled>(
1952 el: T,
1953 focused: bool,
1954 offset: bool,
1955 base: Vec<gpui::BoxShadow>,
1956 cx: &App,
1957) -> T {
1958 if !focused {
1959 return if base.is_empty() { el } else { el.shadow(base) };
1960 }
1961 let mut all = base;
1962 all.extend(focus_ring_shadows(offset, cx));
1963 el.shadow(all)
1964}
1965
1966pub fn ring_if_focused<T: Styled>(
1971 el: T,
1972 handle: &gpui::FocusHandle,
1973 offset: bool,
1974 base: Vec<gpui::BoxShadow>,
1975 window: &gpui::Window,
1976 cx: &App,
1977) -> T {
1978 let focused = shows_focus_ring(handle.is_focused(window), cx);
1979 with_focus_ring(el, focused, offset, base, cx)
1980}
1981
1982pub fn capture_sx(style: impl FnOnce(Div) -> Div) -> Box<gpui::StyleRefinement> {
1995 Box::new(style(gpui::div()).style().clone())
1996}
1997
1998pub(crate) fn refine_sx(
2002 sx: &mut Option<Box<gpui::StyleRefinement>>,
2003 style: impl FnOnce(Div) -> Div,
2004) {
2005 let next = capture_sx(style);
2006 if let Some(current) = sx {
2007 current.as_mut().refine(&next);
2008 } else {
2009 *sx = Some(next);
2010 }
2011}
2012
2013macro_rules! impl_component_styled {
2017 ($($component:ty),+ $(,)?) => {
2018 $(
2019 impl gpui::Styled for $component {
2020 fn style(&mut self) -> &mut gpui::StyleRefinement {
2021 self.sx
2022 .get_or_insert_with(|| Box::new(gpui::StyleRefinement::default()))
2023 .as_mut()
2024 }
2025 }
2026 )+
2027 };
2028}
2029pub(crate) use impl_component_styled;
2030
2031#[derive(Clone, Default)]
2036struct FocusedBounds(
2037 std::rc::Rc<
2038 std::cell::RefCell<
2039 std::collections::HashMap<
2040 gpui::WindowId,
2041 (gpui::WeakFocusHandle, gpui::Bounds<Pixels>),
2042 >,
2043 >,
2044 >,
2045);
2046
2047impl gpui::Global for FocusedBounds {}
2048
2049pub(crate) fn record_focus_bounds<T: ParentElement>(
2052 el: T,
2053 handle: &gpui::FocusHandle,
2054 window: &gpui::Window,
2055 cx: &mut App,
2056) -> T {
2057 if !handle.is_focused(window) {
2058 return el;
2059 }
2060 let registry = if let Some(registry) = cx.try_global::<FocusedBounds>() {
2061 registry.clone()
2062 } else {
2063 let registry = FocusedBounds::default();
2064 cx.set_global(registry.clone());
2065 registry
2066 };
2067 let weak = handle.downgrade();
2068 el.child(
2069 gpui::canvas(
2070 move |bounds, window, _| {
2071 registry
2072 .0
2073 .borrow_mut()
2074 .insert(window.window_handle().window_id(), (weak, bounds));
2075 },
2076 |_, _, _, _| {},
2077 )
2078 .absolute()
2079 .inset_0(),
2080 )
2081}
2082
2083pub(crate) fn focused_element_bounds(
2085 window: &gpui::Window,
2086 cx: &App,
2087) -> Option<gpui::Bounds<Pixels>> {
2088 let focused = window.focused(cx)?;
2089 let registry = cx.try_global::<FocusedBounds>()?;
2090 let bounds = registry.0.borrow();
2091 let (handle, bounds) = bounds.get(&window.window_handle().window_id())?;
2092 (*handle == focused).then_some(*bounds)
2093}
2094
2095pub fn apply_sx<T: Styled>(el: T, sx: &Option<Box<gpui::StyleRefinement>>) -> T {
2101 let Some(sx) = sx else { return el };
2102 let mut el = el;
2103 el.style().refine(sx);
2104 el
2105}
2106
2107pub fn sx_background(sx: &Option<Box<gpui::StyleRefinement>>) -> Option<Hsla> {
2112 match sx.as_ref()?.background {
2113 Some(gpui::Fill::Color(background)) => background.as_solid(),
2114 _ => None,
2115 }
2116}
2117
2118pub fn sx_border_color(sx: &Option<Box<gpui::StyleRefinement>>) -> Option<Hsla> {
2124 sx.as_ref()?.border_color
2125}
2126
2127pub fn sx_pixel_size(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Size<Option<Pixels>> {
2133 fn definite(length: gpui::Length) -> Option<Pixels> {
2134 match length {
2135 gpui::Length::Definite(gpui::DefiniteLength::Absolute(
2136 gpui::AbsoluteLength::Pixels(pixels),
2137 )) => Some(pixels),
2138 _ => None,
2139 }
2140 }
2141 let Some(sx) = sx else {
2142 return gpui::Size {
2143 width: None,
2144 height: None,
2145 };
2146 };
2147 gpui::Size {
2148 width: sx.size.width.and_then(definite),
2149 height: sx.size.height.and_then(definite),
2150 }
2151}
2152
2153pub(crate) fn fade_endpoints(
2167 variant: Option<(Hsla, Hsla)>,
2168 sx_background: Option<Hsla>,
2169 hover_bg: Option<Hsla>,
2170) -> Option<(Hsla, Hsla)> {
2171 let resting = sx_background.or_else(|| variant.map(|(idle, _)| idle));
2172 match (hover_bg, resting) {
2173 (Some(hover), Some(resting)) => Some((resting, hover)),
2174 _ => variant.map(|colors| sx_background.map_or(colors, |color| (color, color))),
2175 }
2176}
2177
2178pub(crate) fn leading_for(text_size: Pixels) -> Option<Pixels> {
2183 let size = f32::from(text_size);
2184 if (size - 12.0).abs() < f32::EPSILON {
2185 Some(gpui::px(16.))
2186 } else if (size - 14.0).abs() < f32::EPSILON {
2187 Some(gpui::px(20.))
2188 } else if (size - 16.0).abs() < f32::EPSILON {
2189 Some(gpui::px(24.))
2190 } else {
2191 None
2192 }
2193}
2194
2195pub(crate) fn fill_unspecified_corners(
2203 mut corners: gpui::Corners<Option<Pixels>>,
2204 radius: Option<Pixels>,
2205) -> gpui::Corners<Option<Pixels>> {
2206 let Some(radius) = radius else {
2207 return corners;
2208 };
2209 if corners.top_left.is_none() {
2210 corners.top_left = Some(radius);
2211 }
2212 if corners.top_right.is_none() {
2213 corners.top_right = Some(radius);
2214 }
2215 if corners.bottom_right.is_none() {
2216 corners.bottom_right = Some(radius);
2217 }
2218 if corners.bottom_left.is_none() {
2219 corners.bottom_left = Some(radius);
2220 }
2221 corners
2222}
2223
2224pub(crate) fn round_sx_corners<T: Styled>(el: T, corners: &gpui::Corners<Option<Pixels>>) -> T {
2225 let mut el = el;
2226 if let Some(pixels) = corners.top_left {
2227 el = el.rounded_tl(pixels);
2228 }
2229 if let Some(pixels) = corners.top_right {
2230 el = el.rounded_tr(pixels);
2231 }
2232 if let Some(pixels) = corners.bottom_right {
2233 el = el.rounded_br(pixels);
2234 }
2235 if let Some(pixels) = corners.bottom_left {
2236 el = el.rounded_bl(pixels);
2237 }
2238 el
2239}
2240
2241#[allow(dead_code)]
2252pub fn sx_padding(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Edges<Option<Pixels>> {
2253 fn definite(length: gpui::DefiniteLength) -> Option<Pixels> {
2254 match length {
2255 gpui::DefiniteLength::Absolute(gpui::AbsoluteLength::Pixels(pixels)) => Some(pixels),
2256 _ => None,
2257 }
2258 }
2259 let Some(sx) = sx else {
2260 return gpui::Edges::all(None);
2261 };
2262 gpui::Edges {
2263 top: sx.padding.top.and_then(definite),
2264 right: sx.padding.right.and_then(definite),
2265 bottom: sx.padding.bottom.and_then(definite),
2266 left: sx.padding.left.and_then(definite),
2267 }
2268}
2269
2270pub fn sx_radius(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Corners<Option<Pixels>> {
2276 fn absolute(length: gpui::AbsoluteLength) -> Option<Pixels> {
2277 match length {
2278 gpui::AbsoluteLength::Pixels(pixels) => Some(pixels),
2279 gpui::AbsoluteLength::Rems(_) => None,
2280 }
2281 }
2282 let Some(sx) = sx else {
2283 return gpui::Corners::default();
2284 };
2285 gpui::Corners {
2286 top_left: sx.corner_radii.top_left.and_then(absolute),
2287 top_right: sx.corner_radii.top_right.and_then(absolute),
2288 bottom_right: sx.corner_radii.bottom_right.and_then(absolute),
2289 bottom_left: sx.corner_radii.bottom_left.and_then(absolute),
2290 }
2291}
2292
2293#[cfg(test)]
2294mod sx_extraction_tests {
2295 use super::*;
2296 use gpui::{px, relative, rems, Div, Styled};
2297
2298 fn captured(style: impl FnOnce(Div) -> Div) -> Option<Box<gpui::StyleRefinement>> {
2299 Some(capture_sx(style))
2300 }
2301
2302 #[test]
2303 #[allow(clippy::float_cmp)] fn field_box_defaults_are_the_stock_field_metrics() {
2305 let field = FieldBox::default();
2306 assert_eq!(field.resolved_height(), FIELD_HEIGHT);
2307 assert_eq!(f32::from(field.resolved_padding_x()), 12.);
2308 }
2309
2310 #[test]
2311 fn no_override_extracts_nothing() {
2312 assert_eq!(sx_padding(&None), gpui::Edges::all(None));
2313 assert_eq!(sx_radius(&None), gpui::Corners::default());
2314 assert_eq!(sx_border_color(&None), None);
2315 }
2316
2317 #[test]
2318 fn border_color_override_is_preserved_for_child_chrome() {
2319 let color = gpui::hsla(0.58, 0.7, 0.4, 1.0);
2320 let sx = captured(|d| d.border_color(color));
2321 assert_eq!(sx_border_color(&sx), Some(color));
2322 }
2323
2324 #[test]
2325 fn uniform_pixel_padding_extracts_on_every_edge() {
2326 let sx = captured(|d| d.p(px(8.)));
2327 assert_eq!(sx_padding(&sx), gpui::Edges::all(Some(px(8.))));
2328 }
2329
2330 #[test]
2331 fn per_edge_padding_preserves_each_edge() {
2332 let sx = captured(|d| d.pt(px(1.)).pr(px(2.)).pb(px(3.)).pl(px(4.)));
2333 assert_eq!(
2334 sx_padding(&sx),
2335 gpui::Edges {
2336 top: Some(px(1.)),
2337 right: Some(px(2.)),
2338 bottom: Some(px(3.)),
2339 left: Some(px(4.)),
2340 }
2341 );
2342 }
2343
2344 #[test]
2345 fn zero_padding_is_a_real_override() {
2346 let sx = captured(|d| d.p(px(0.)));
2347 assert_eq!(sx_padding(&sx), gpui::Edges::all(Some(px(0.))));
2348 }
2349
2350 #[test]
2351 fn rems_and_fractions_stay_unsupported_edge_by_edge() {
2352 let sx = captured(|d| d.pt(px(2.)).pb(rems(1.)).pl(relative(0.5)));
2353 assert_eq!(
2354 sx_padding(&sx),
2355 gpui::Edges {
2356 top: Some(px(2.)),
2357 right: None,
2358 bottom: None,
2359 left: None,
2360 }
2361 );
2362 let raw = sx.as_ref().unwrap();
2365 assert!(matches!(
2366 raw.padding.bottom,
2367 Some(gpui::DefiniteLength::Absolute(gpui::AbsoluteLength::Rems(
2368 _
2369 )))
2370 ));
2371 assert!(matches!(
2372 raw.padding.left,
2373 Some(gpui::DefiniteLength::Fraction(_))
2374 ));
2375 }
2376
2377 #[test]
2378 fn uniform_and_per_corner_radius_extract() {
2379 let sx = captured(|d| d.rounded(px(6.)));
2380 assert_eq!(
2381 sx_radius(&sx),
2382 gpui::Corners {
2383 top_left: Some(px(6.)),
2384 top_right: Some(px(6.)),
2385 bottom_right: Some(px(6.)),
2386 bottom_left: Some(px(6.)),
2387 }
2388 );
2389
2390 let sx = captured(|d| d.rounded_tl(px(1.)).rounded_br(px(3.)));
2391 assert_eq!(
2392 sx_radius(&sx),
2393 gpui::Corners {
2394 top_left: Some(px(1.)),
2395 top_right: None,
2396 bottom_right: Some(px(3.)),
2397 bottom_left: None,
2398 }
2399 );
2400 }
2401
2402 #[test]
2403 fn leading_pairs_follow_the_v3_steps() {
2404 assert_eq!(leading_for(px(12.)), Some(px(16.)));
2405 assert_eq!(leading_for(px(14.)), Some(px(20.)));
2406 assert_eq!(leading_for(px(16.)), Some(px(24.)));
2407 assert_eq!(leading_for(px(13.)), None, "an unpairable size stays unset");
2408 }
2409
2410 #[test]
2411 fn explicit_sx_corners_refine_each_corner_individually() {
2412 let sx = captured(|d| d.rounded_tl(px(2.)).rounded_br(px(6.)));
2413 let corners = sx_radius(&sx);
2414 let mut el = round_sx_corners(gpui::div().rounded(px(4.)), &corners);
2415 let radii = el.style().corner_radii.clone();
2416 assert_eq!(
2417 radii.top_left,
2418 Some(gpui::AbsoluteLength::Pixels(px(2.))),
2419 "the explicit top-left corner must win"
2420 );
2421 assert_eq!(
2422 radii.bottom_right,
2423 Some(gpui::AbsoluteLength::Pixels(px(6.))),
2424 "the explicit bottom-right corner must win"
2425 );
2426 assert_eq!(
2427 radii.top_right,
2428 Some(gpui::AbsoluteLength::Pixels(px(4.))),
2429 "an unnamed corner keeps the component's own radius"
2430 );
2431 assert_eq!(
2432 radii.bottom_left,
2433 Some(gpui::AbsoluteLength::Pixels(px(4.))),
2434 "an unnamed corner keeps the component's own radius"
2435 );
2436 }
2437
2438 #[test]
2439 fn rem_radius_stays_unsupported_for_that_corner() {
2440 let sx = captured(|d| d.rounded_tl(rems(1.)).rounded_br(px(3.)));
2441 assert_eq!(
2442 sx_radius(&sx),
2443 gpui::Corners {
2444 top_left: None,
2445 top_right: None,
2446 bottom_right: Some(px(3.)),
2447 bottom_left: None,
2448 }
2449 );
2450 }
2451}
2452
2453#[cfg(test)]
2454mod overlay_stack_tests {
2455 use super::*;
2456 use gpui::AppContext;
2457
2458 fn registration(
2459 cx: &mut gpui::TestAppContext,
2460 window_id: gpui::WindowId,
2461 order: u64,
2462 ) -> gpui::Entity<OverlayRegistration> {
2463 cx.new(|_| OverlayRegistration {
2464 window_id,
2465 order,
2466 phase: OverlayPhase::Open,
2467 keep_exiting: false,
2468 exit_generation: 0,
2469 escape_capture: None,
2470 })
2471 }
2472
2473 #[gpui::test]
2474 fn sibling_order_is_stable_when_open_registrations_repaint(cx: &mut gpui::TestAppContext) {
2475 let outer = registration(cx, gpui::WindowId::from(1), 1);
2476 let inner = registration(cx, gpui::WindowId::from(1), 2);
2477 cx.update(|cx| {
2478 sync_overlay_stack(&outer, cx);
2479 sync_overlay_stack(&inner, cx);
2480 sync_overlay_stack(&outer, cx);
2481 sync_overlay_stack(&inner, cx);
2482 let stack = cx.global::<OverlayStack>();
2483 assert_eq!(stack.entries.len(), 2);
2484 assert_eq!(stack.entries[0].upgrade().unwrap().read(cx).order, 1);
2485 assert_eq!(stack.entries[1].upgrade().unwrap().read(cx).order, 2);
2486 });
2487 }
2488
2489 #[gpui::test]
2490 fn topmost_registration_is_window_scoped(cx: &mut gpui::TestAppContext) {
2491 let first = registration(cx, gpui::WindowId::from(1), 1);
2492 let second = registration(cx, gpui::WindowId::from(2), 2);
2493 let first_token = OverlayToken {
2494 registration: first.downgrade(),
2495 window_id: gpui::WindowId::from(1),
2496 };
2497 let second_token = OverlayToken {
2498 registration: second.downgrade(),
2499 window_id: gpui::WindowId::from(2),
2500 };
2501 cx.update(|cx| {
2502 cx.set_global(OverlayStack {
2503 entries: vec![first.downgrade(), second.downgrade()],
2504 next_order: 2,
2505 });
2506 assert!(is_topmost(&first_token, cx));
2507 assert!(is_topmost(&second_token, cx));
2508 });
2509 }
2510
2511 #[gpui::test]
2512 fn dead_registration_is_pruned_from_the_stack(cx: &mut gpui::TestAppContext) {
2513 let registration = registration(cx, gpui::WindowId::from(1), 1);
2514 let weak = registration.downgrade();
2515 cx.update(|cx| {
2516 cx.set_global(OverlayStack {
2517 entries: vec![weak.clone()],
2518 next_order: 1,
2519 });
2520 });
2521 drop(registration);
2522 cx.update(|cx| {
2523 cx.update_global::<OverlayStack, _>(|stack, cx| {
2524 prune_overlay_stack(stack, cx);
2525 assert!(stack.entries.is_empty());
2526 });
2527 });
2528 }
2529}
2530
2531#[cfg(test)]
2532mod focus_ring_overlay_tests {
2533 use super::*;
2534 use gpui::{px, AbsoluteLength, Length, Styled};
2535
2536 fn inset(el: &mut Div) -> Length {
2537 el.style().inset.top.unwrap()
2538 }
2539
2540 fn radius(el: &mut Div) -> AbsoluteLength {
2541 el.style().corner_radii.top_left.unwrap()
2542 }
2543
2544 fn border(el: &mut Div) -> AbsoluteLength {
2545 el.style().border_widths.top.unwrap()
2546 }
2547
2548 #[test]
2551 fn unoffset_ring_carrier_is_the_control_box() {
2552 let mut carrier = ring_overlay_band(px(8.), px(0.), gpui::red());
2553 assert_eq!(inset(&mut carrier), Length::Definite(px(0.).into()));
2554 assert_eq!(radius(&mut carrier), AbsoluteLength::Pixels(px(8.)));
2555 assert!(carrier.style().border_widths.top.is_none());
2556 assert!(
2557 carrier.style().box_shadow.is_none(),
2558 "the band is an svg, not a shadow"
2559 );
2560 }
2561
2562 #[test]
2566 fn offset_ring_pushes_the_carrier_out_and_paints_the_gap() {
2567 let gap = px(2.);
2568 let mut carrier = ring_overlay_band(px(8.), gap, gpui::red());
2569 assert_eq!(inset(&mut carrier), Length::Definite(px(-2.).into()));
2570 assert_eq!(radius(&mut carrier), AbsoluteLength::Pixels(px(10.)));
2571
2572 let mut inner = ring_overlay_gap(px(8.), gap, gpui::blue());
2573 assert_eq!(inset(&mut inner), Length::Definite(px(0.).into()));
2574 assert_eq!(radius(&mut inner), AbsoluteLength::Pixels(px(10.)));
2575 assert_eq!(border(&mut inner), AbsoluteLength::Pixels(gap));
2576 }
2577
2578 #[gpui::test]
2581 fn the_offset_gap_comes_from_the_layout_theme(cx: &mut gpui::TestAppContext) {
2582 cx.update(|cx| {
2583 herogpui_theme::ThemeProvider::init(cx);
2584 let gap = cx.layout().ring_offset_width;
2585 let mut ring = focus_ring_overlay(px(8.), true, cx);
2586 assert_eq!(inset(&mut ring), Length::Definite((-gap).into()));
2587 assert_eq!(radius(&mut ring), AbsoluteLength::Pixels(px(8.) + gap));
2588
2589 let mut flat = focus_ring_overlay(px(8.), false, cx);
2590 assert_eq!(inset(&mut flat), Length::Definite(px(0.).into()));
2591 assert_eq!(radius(&mut flat), AbsoluteLength::Pixels(px(8.)));
2592 });
2593 }
2594}