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
1531struct FocusRoot {
1533 handle: gpui::FocusHandle,
1534 painted_focus: Option<gpui::WeakFocusHandle>,
1537}
1538
1539pub fn app_focus_root<T>(el: T, window: &mut gpui::Window, cx: &mut App) -> T
1559where
1560 T: gpui::InteractiveElement,
1561{
1562 let state = window.use_keyed_state(
1563 gpui::ElementId::Name("herogpui-focus-root".into()),
1564 cx,
1565 |_, cx| FocusRoot {
1566 handle: cx.focus_handle(),
1567 painted_focus: None,
1568 },
1569 );
1570 let root = state.read(cx).handle.clone();
1571 let claim = match window.focused(cx) {
1572 None => true,
1573 Some(focused) if root.contains_focused(window, cx) => {
1574 state.update(cx, |state, _| {
1575 state.painted_focus = Some(focused.downgrade());
1576 });
1577 false
1578 }
1579 Some(focused) => state
1580 .read(cx)
1581 .painted_focus
1582 .as_ref()
1583 .is_some_and(|painted| *painted == focused),
1584 };
1585 if claim {
1586 window.focus(&root, cx);
1587 }
1588 el.track_focus(&root)
1589 .capture_any_mouse_down(|_, _, cx| set_focus_visible(false, cx))
1590 .capture_key_down(|event, window, cx| {
1591 if event.keystroke.key == "escape" && dismiss_captured_escape(window, cx) {
1592 cx.stop_propagation();
1593 }
1594 })
1595 .on_key_down(|event, window, cx| {
1596 if key_enables_focus_visible(&event.keystroke.key) {
1597 set_focus_visible(true, cx);
1598 }
1599 if event.keystroke.key == "tab" {
1600 if event.keystroke.modifiers.shift {
1601 window.focus_prev(cx);
1602 } else {
1603 window.focus_next(cx);
1604 }
1605 cx.stop_propagation();
1606 }
1607 })
1608 .on_key_up(|event, window, cx| {
1609 if !matches!(event.keystroke.key.as_str(), "enter" | "space")
1610 || cx.try_global::<ActiveKeyboardPresses>().is_none()
1611 {
1612 return;
1613 }
1614 let window_id = window.window_handle().window_id();
1615 let key = event.keystroke.key.as_str();
1616 let interactions = cx.update_global::<ActiveKeyboardPresses, _>(|pressed, _| {
1617 let mut released = Vec::new();
1618 pressed
1619 .0
1620 .retain(|(active_window, active_key, interaction)| {
1621 if *active_window == window_id && active_key == key {
1622 released.push(interaction.clone());
1623 false
1624 } else {
1625 interaction.upgrade().is_some()
1626 }
1627 });
1628 released
1629 });
1630 for interaction in interactions {
1631 if let Some(interaction) = interaction.upgrade() {
1632 interaction.update(cx, |state, cx| {
1633 if state.1 {
1634 state.1 = false;
1635 cx.notify();
1636 }
1637 });
1638 }
1639 }
1640 })
1641}
1642
1643pub fn tab_stop_handle(
1650 id: gpui::ElementId,
1651 window: &mut gpui::Window,
1652 cx: &mut App,
1653) -> gpui::FocusHandle {
1654 window
1655 .use_keyed_state(id, cx, |_, cx| cx.focus_handle().tab_stop(true))
1656 .read(cx)
1657 .clone()
1658}
1659
1660const RING_WIDTH: Pixels = gpui::px(2.);
1663
1664pub fn focus_ring_shadows(offset: bool, cx: &App) -> Vec<gpui::BoxShadow> {
1683 let colors = cx.colors();
1684 let layout = cx.layout();
1685 let ring = RING_WIDTH;
1686 let blur = gpui::px(1.);
1687 let gap = if offset {
1688 layout.ring_offset_width
1689 } else {
1690 gpui::px(0.)
1691 };
1692 let mut shadows = vec![gpui::BoxShadow {
1693 color: colors.focus,
1694 offset: gpui::point(gpui::px(0.), gpui::px(0.)),
1695 blur_radius: blur,
1696 spread_radius: gap + ring,
1697 inset: false,
1698 }];
1699 if gap > gpui::px(0.) {
1700 shadows.push(gpui::BoxShadow {
1701 color: colors.background,
1702 offset: gpui::point(gpui::px(0.), gpui::px(0.)),
1703 blur_radius: blur,
1704 spread_radius: gap,
1705 inset: false,
1706 });
1707 }
1708 shadows
1709}
1710
1711pub fn focus_ring_overlay(radius: Pixels, offset: bool, cx: &App) -> Div {
1750 ring_overlay_in(radius, offset, cx.colors().focus, cx)
1751}
1752
1753pub(crate) fn ring_overlay_in(radius: Pixels, offset: bool, color: Hsla, cx: &App) -> Div {
1756 let gap = if offset {
1757 cx.layout().ring_offset_width
1758 } else {
1759 gpui::px(0.)
1760 };
1761 let outer = ring_overlay_band(radius, gap, color);
1762 if gap > gpui::px(0.) {
1763 outer.child(ring_overlay_gap(radius, gap, cx.colors().background))
1764 } else {
1765 outer
1766 }
1767}
1768
1769const RING_BLUR_REACH: Pixels = gpui::px(3.);
1771const RING_BLUR_SIGMA: f32 = 0.7;
1774const RING_UNDERLAP: f32 = 1.5;
1778
1779fn ring_overlay_band(radius: Pixels, gap: Pixels, color: Hsla) -> Div {
1794 let reach = RING_WIDTH + RING_BLUR_REACH;
1795 let corner = radius + gap;
1796 let canvas = gpui::canvas(
1797 |_, _, _| (),
1798 move |bounds, _, window, cx| {
1799 let w = f32::from(bounds.size.width);
1800 let h = f32::from(bounds.size.height);
1801 if w <= 0. || h <= 0. {
1802 return;
1803 }
1804 let ring = f32::from(RING_WIDTH);
1805 let inner = f32::from(reach);
1806 let rg = f32::from(corner);
1807 let stroke_w = ring + RING_UNDERLAP;
1808 let stroke_at = f32::from(RING_BLUR_REACH) + stroke_w / 2.;
1809 let svg = format!(
1810 concat!(
1811 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{w:.2}\" height=\"{h:.2}\" ",
1812 "viewBox=\"0 0 {w:.2} {h:.2}\"><defs>",
1813 "<filter id=\"b\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">",
1814 "<feGaussianBlur stdDeviation=\"{sigma:.3}\"/></filter>",
1815 "<mask id=\"m\"><rect width=\"{w:.2}\" height=\"{h:.2}\" fill=\"#fff\"/>",
1816 "<rect x=\"{inner:.2}\" y=\"{inner:.2}\" width=\"{iw:.2}\" height=\"{ih:.2}\" rx=\"{rg:.2}\" fill=\"#000\"/>",
1817 "</mask></defs><g mask=\"url(#m)\">",
1818 "<rect x=\"{sx:.2}\" y=\"{sx:.2}\" width=\"{sw:.2}\" height=\"{sh:.2}\" rx=\"{srx:.2}\" ",
1819 "fill=\"none\" stroke=\"#000\" stroke-width=\"{ring:.2}\" filter=\"url(#b)\"/></g></svg>"
1820 ),
1821 w = w,
1822 h = h,
1823 sigma = RING_BLUR_SIGMA,
1824 inner = inner,
1825 iw = (w - 2. * inner).max(0.),
1826 ih = (h - 2. * inner).max(0.),
1827 rg = rg,
1828 sx = stroke_at,
1829 sw = (w - 2. * stroke_at).max(0.),
1830 sh = (h - 2. * stroke_at).max(0.),
1831 srx = (rg + ring - stroke_w / 2.).max(0.),
1832 ring = stroke_w,
1833 );
1834 let key: gpui::SharedString =
1835 format!("herogpui://focus-ring/{w:.1}x{h:.1}/r{rg:.1}").into();
1836 let _ = window.paint_svg(
1837 bounds,
1838 key,
1839 Some(svg.as_bytes()),
1840 gpui::TransformationMatrix::unit(),
1841 color,
1842 cx,
1843 );
1844 },
1845 )
1846 .absolute()
1847 .inset(-reach);
1848 gpui::div()
1849 .absolute()
1850 .inset(-gap)
1851 .rounded(corner)
1852 .child(canvas)
1853}
1854
1855fn ring_overlay_gap(radius: Pixels, gap: Pixels, background: Hsla) -> Div {
1859 gpui::div()
1860 .absolute()
1861 .inset(gpui::px(0.))
1862 .rounded(radius + gap)
1863 .border(gap)
1864 .border_color(background)
1865}
1866
1867pub fn with_focus_ring_overlay<T: Styled + ParentElement>(
1874 el: T,
1875 focused: bool,
1876 offset: bool,
1877 radius: Pixels,
1878 base: Vec<gpui::BoxShadow>,
1879 cx: &App,
1880) -> T {
1881 let el = if base.is_empty() { el } else { el.shadow(base) };
1882 if !focused {
1883 return el;
1884 }
1885 el.child(focus_ring_overlay(radius, offset, cx))
1886}
1887
1888pub fn ring_overlay_if_focused<T: Styled + ParentElement>(
1890 el: T,
1891 handle: &gpui::FocusHandle,
1892 offset: bool,
1893 radius: Pixels,
1894 base: Vec<gpui::BoxShadow>,
1895 window: &gpui::Window,
1896 cx: &App,
1897) -> T {
1898 let focused = shows_focus_ring(handle.is_focused(window), cx);
1899 with_focus_ring_overlay(el, focused, offset, radius, base, cx)
1900}
1901
1902pub fn trap_tab<T: gpui::InteractiveElement>(el: T, scope: &gpui::FocusHandle) -> T {
1915 let scope = scope.clone();
1916 el.on_key_down(move |event: &gpui::KeyDownEvent, window, cx| {
1917 if event.keystroke.key != "tab" {
1918 return;
1919 }
1920 cx.stop_propagation();
1924 set_focus_visible(true, cx);
1925 let back = event.keystroke.modifiers.shift;
1926 if back {
1927 window.focus_prev(cx);
1928 } else {
1929 window.focus_next(cx);
1930 }
1931 if scope.contains_focused(window, cx) {
1932 return;
1933 }
1934 window.focus(&scope, cx);
1936 window.focus_next(cx);
1937 if !back {
1938 return;
1939 }
1940 for _ in 0..256 {
1942 window.focus_next(cx);
1943 if !scope.contains_focused(window, cx) {
1944 window.focus_prev(cx);
1945 return;
1946 }
1947 }
1948 })
1949}
1950
1951pub fn inset_focus_ring(cx: &App) -> Div {
1965 let colors = cx.colors();
1966 gpui::div()
1967 .absolute()
1968 .inset_0()
1969 .border_2()
1970 .border_color(colors.focus)
1971 .rounded(key_radius(cx))
1972}
1973
1974pub fn with_focus_ring<T: Styled>(
1980 el: T,
1981 focused: bool,
1982 offset: bool,
1983 base: Vec<gpui::BoxShadow>,
1984 cx: &App,
1985) -> T {
1986 if !focused {
1987 return if base.is_empty() { el } else { el.shadow(base) };
1988 }
1989 let mut all = base;
1990 all.extend(focus_ring_shadows(offset, cx));
1991 el.shadow(all)
1992}
1993
1994pub fn ring_if_focused<T: Styled>(
1999 el: T,
2000 handle: &gpui::FocusHandle,
2001 offset: bool,
2002 base: Vec<gpui::BoxShadow>,
2003 window: &gpui::Window,
2004 cx: &App,
2005) -> T {
2006 let focused = shows_focus_ring(handle.is_focused(window), cx);
2007 with_focus_ring(el, focused, offset, base, cx)
2008}
2009
2010pub fn capture_sx(style: impl FnOnce(Div) -> Div) -> Box<gpui::StyleRefinement> {
2023 Box::new(style(gpui::div()).style().clone())
2024}
2025
2026pub(crate) fn refine_sx(
2030 sx: &mut Option<Box<gpui::StyleRefinement>>,
2031 style: impl FnOnce(Div) -> Div,
2032) {
2033 let next = capture_sx(style);
2034 if let Some(current) = sx {
2035 current.as_mut().refine(&next);
2036 } else {
2037 *sx = Some(next);
2038 }
2039}
2040
2041macro_rules! impl_component_styled {
2045 ($($component:ty),+ $(,)?) => {
2046 $(
2047 impl gpui::Styled for $component {
2048 fn style(&mut self) -> &mut gpui::StyleRefinement {
2049 self.sx
2050 .get_or_insert_with(|| Box::new(gpui::StyleRefinement::default()))
2051 .as_mut()
2052 }
2053 }
2054 )+
2055 };
2056}
2057pub(crate) use impl_component_styled;
2058
2059#[derive(Clone, Default)]
2064struct FocusedBounds(
2065 std::rc::Rc<
2066 std::cell::RefCell<
2067 std::collections::HashMap<
2068 gpui::WindowId,
2069 (gpui::WeakFocusHandle, gpui::Bounds<Pixels>),
2070 >,
2071 >,
2072 >,
2073);
2074
2075impl gpui::Global for FocusedBounds {}
2076
2077pub(crate) fn record_focus_bounds<T: ParentElement>(
2080 el: T,
2081 handle: &gpui::FocusHandle,
2082 window: &gpui::Window,
2083 cx: &mut App,
2084) -> T {
2085 if !handle.is_focused(window) {
2086 return el;
2087 }
2088 let registry = if let Some(registry) = cx.try_global::<FocusedBounds>() {
2089 registry.clone()
2090 } else {
2091 let registry = FocusedBounds::default();
2092 cx.set_global(registry.clone());
2093 registry
2094 };
2095 let weak = handle.downgrade();
2096 el.child(
2097 gpui::canvas(
2098 move |bounds, window, _| {
2099 registry
2100 .0
2101 .borrow_mut()
2102 .insert(window.window_handle().window_id(), (weak, bounds));
2103 },
2104 |_, _, _, _| {},
2105 )
2106 .absolute()
2107 .inset_0(),
2108 )
2109}
2110
2111pub(crate) fn focused_element_bounds(
2113 window: &gpui::Window,
2114 cx: &App,
2115) -> Option<gpui::Bounds<Pixels>> {
2116 let focused = window.focused(cx)?;
2117 let registry = cx.try_global::<FocusedBounds>()?;
2118 let bounds = registry.0.borrow();
2119 let (handle, bounds) = bounds.get(&window.window_handle().window_id())?;
2120 (*handle == focused).then_some(*bounds)
2121}
2122
2123pub fn apply_sx<T: Styled>(el: T, sx: &Option<Box<gpui::StyleRefinement>>) -> T {
2129 let Some(sx) = sx else { return el };
2130 let mut el = el;
2131 el.style().refine(sx);
2132 el
2133}
2134
2135pub fn sx_background(sx: &Option<Box<gpui::StyleRefinement>>) -> Option<Hsla> {
2140 match sx.as_ref()?.background {
2141 Some(gpui::Fill::Color(background)) => background.as_solid(),
2142 _ => None,
2143 }
2144}
2145
2146pub fn sx_border_color(sx: &Option<Box<gpui::StyleRefinement>>) -> Option<Hsla> {
2152 sx.as_ref()?.border_color
2153}
2154
2155pub fn sx_pixel_size(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Size<Option<Pixels>> {
2161 fn definite(length: gpui::Length) -> Option<Pixels> {
2162 match length {
2163 gpui::Length::Definite(gpui::DefiniteLength::Absolute(
2164 gpui::AbsoluteLength::Pixels(pixels),
2165 )) => Some(pixels),
2166 _ => None,
2167 }
2168 }
2169 let Some(sx) = sx else {
2170 return gpui::Size {
2171 width: None,
2172 height: None,
2173 };
2174 };
2175 gpui::Size {
2176 width: sx.size.width.and_then(definite),
2177 height: sx.size.height.and_then(definite),
2178 }
2179}
2180
2181pub(crate) fn fade_endpoints(
2195 variant: Option<(Hsla, Hsla)>,
2196 sx_background: Option<Hsla>,
2197 hover_bg: Option<Hsla>,
2198) -> Option<(Hsla, Hsla)> {
2199 let resting = sx_background.or_else(|| variant.map(|(idle, _)| idle));
2200 match (hover_bg, resting) {
2201 (Some(hover), Some(resting)) => Some((resting, hover)),
2202 _ => variant.map(|colors| sx_background.map_or(colors, |color| (color, color))),
2203 }
2204}
2205
2206pub(crate) fn leading_for(text_size: Pixels) -> Option<Pixels> {
2211 let size = f32::from(text_size);
2212 if (size - 12.0).abs() < f32::EPSILON {
2213 Some(gpui::px(16.))
2214 } else if (size - 14.0).abs() < f32::EPSILON {
2215 Some(gpui::px(20.))
2216 } else if (size - 16.0).abs() < f32::EPSILON {
2217 Some(gpui::px(24.))
2218 } else {
2219 None
2220 }
2221}
2222
2223pub(crate) fn fill_unspecified_corners(
2231 mut corners: gpui::Corners<Option<Pixels>>,
2232 radius: Option<Pixels>,
2233) -> gpui::Corners<Option<Pixels>> {
2234 let Some(radius) = radius else {
2235 return corners;
2236 };
2237 if corners.top_left.is_none() {
2238 corners.top_left = Some(radius);
2239 }
2240 if corners.top_right.is_none() {
2241 corners.top_right = Some(radius);
2242 }
2243 if corners.bottom_right.is_none() {
2244 corners.bottom_right = Some(radius);
2245 }
2246 if corners.bottom_left.is_none() {
2247 corners.bottom_left = Some(radius);
2248 }
2249 corners
2250}
2251
2252pub(crate) fn round_sx_corners<T: Styled>(el: T, corners: &gpui::Corners<Option<Pixels>>) -> T {
2253 let mut el = el;
2254 if let Some(pixels) = corners.top_left {
2255 el = el.rounded_tl(pixels);
2256 }
2257 if let Some(pixels) = corners.top_right {
2258 el = el.rounded_tr(pixels);
2259 }
2260 if let Some(pixels) = corners.bottom_right {
2261 el = el.rounded_br(pixels);
2262 }
2263 if let Some(pixels) = corners.bottom_left {
2264 el = el.rounded_bl(pixels);
2265 }
2266 el
2267}
2268
2269#[allow(dead_code)]
2280pub fn sx_padding(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Edges<Option<Pixels>> {
2281 fn definite(length: gpui::DefiniteLength) -> Option<Pixels> {
2282 match length {
2283 gpui::DefiniteLength::Absolute(gpui::AbsoluteLength::Pixels(pixels)) => Some(pixels),
2284 _ => None,
2285 }
2286 }
2287 let Some(sx) = sx else {
2288 return gpui::Edges::all(None);
2289 };
2290 gpui::Edges {
2291 top: sx.padding.top.and_then(definite),
2292 right: sx.padding.right.and_then(definite),
2293 bottom: sx.padding.bottom.and_then(definite),
2294 left: sx.padding.left.and_then(definite),
2295 }
2296}
2297
2298pub fn sx_radius(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Corners<Option<Pixels>> {
2304 fn absolute(length: gpui::AbsoluteLength) -> Option<Pixels> {
2305 match length {
2306 gpui::AbsoluteLength::Pixels(pixels) => Some(pixels),
2307 gpui::AbsoluteLength::Rems(_) => None,
2308 }
2309 }
2310 let Some(sx) = sx else {
2311 return gpui::Corners::default();
2312 };
2313 gpui::Corners {
2314 top_left: sx.corner_radii.top_left.and_then(absolute),
2315 top_right: sx.corner_radii.top_right.and_then(absolute),
2316 bottom_right: sx.corner_radii.bottom_right.and_then(absolute),
2317 bottom_left: sx.corner_radii.bottom_left.and_then(absolute),
2318 }
2319}
2320
2321#[cfg(test)]
2322mod sx_extraction_tests {
2323 use super::*;
2324 use gpui::{px, relative, rems, Div, Styled};
2325
2326 fn captured(style: impl FnOnce(Div) -> Div) -> Option<Box<gpui::StyleRefinement>> {
2327 Some(capture_sx(style))
2328 }
2329
2330 #[test]
2331 #[allow(clippy::float_cmp)] fn field_box_defaults_are_the_stock_field_metrics() {
2333 let field = FieldBox::default();
2334 assert_eq!(field.resolved_height(), FIELD_HEIGHT);
2335 assert_eq!(f32::from(field.resolved_padding_x()), 12.);
2336 }
2337
2338 #[test]
2339 fn no_override_extracts_nothing() {
2340 assert_eq!(sx_padding(&None), gpui::Edges::all(None));
2341 assert_eq!(sx_radius(&None), gpui::Corners::default());
2342 assert_eq!(sx_border_color(&None), None);
2343 }
2344
2345 #[test]
2346 fn border_color_override_is_preserved_for_child_chrome() {
2347 let color = gpui::hsla(0.58, 0.7, 0.4, 1.0);
2348 let sx = captured(|d| d.border_color(color));
2349 assert_eq!(sx_border_color(&sx), Some(color));
2350 }
2351
2352 #[test]
2353 fn uniform_pixel_padding_extracts_on_every_edge() {
2354 let sx = captured(|d| d.p(px(8.)));
2355 assert_eq!(sx_padding(&sx), gpui::Edges::all(Some(px(8.))));
2356 }
2357
2358 #[test]
2359 fn per_edge_padding_preserves_each_edge() {
2360 let sx = captured(|d| d.pt(px(1.)).pr(px(2.)).pb(px(3.)).pl(px(4.)));
2361 assert_eq!(
2362 sx_padding(&sx),
2363 gpui::Edges {
2364 top: Some(px(1.)),
2365 right: Some(px(2.)),
2366 bottom: Some(px(3.)),
2367 left: Some(px(4.)),
2368 }
2369 );
2370 }
2371
2372 #[test]
2373 fn zero_padding_is_a_real_override() {
2374 let sx = captured(|d| d.p(px(0.)));
2375 assert_eq!(sx_padding(&sx), gpui::Edges::all(Some(px(0.))));
2376 }
2377
2378 #[test]
2379 fn rems_and_fractions_stay_unsupported_edge_by_edge() {
2380 let sx = captured(|d| d.pt(px(2.)).pb(rems(1.)).pl(relative(0.5)));
2381 assert_eq!(
2382 sx_padding(&sx),
2383 gpui::Edges {
2384 top: Some(px(2.)),
2385 right: None,
2386 bottom: None,
2387 left: None,
2388 }
2389 );
2390 let raw = sx.as_ref().unwrap();
2393 assert!(matches!(
2394 raw.padding.bottom,
2395 Some(gpui::DefiniteLength::Absolute(gpui::AbsoluteLength::Rems(
2396 _
2397 )))
2398 ));
2399 assert!(matches!(
2400 raw.padding.left,
2401 Some(gpui::DefiniteLength::Fraction(_))
2402 ));
2403 }
2404
2405 #[test]
2406 fn uniform_and_per_corner_radius_extract() {
2407 let sx = captured(|d| d.rounded(px(6.)));
2408 assert_eq!(
2409 sx_radius(&sx),
2410 gpui::Corners {
2411 top_left: Some(px(6.)),
2412 top_right: Some(px(6.)),
2413 bottom_right: Some(px(6.)),
2414 bottom_left: Some(px(6.)),
2415 }
2416 );
2417
2418 let sx = captured(|d| d.rounded_tl(px(1.)).rounded_br(px(3.)));
2419 assert_eq!(
2420 sx_radius(&sx),
2421 gpui::Corners {
2422 top_left: Some(px(1.)),
2423 top_right: None,
2424 bottom_right: Some(px(3.)),
2425 bottom_left: None,
2426 }
2427 );
2428 }
2429
2430 #[test]
2431 fn leading_pairs_follow_the_v3_steps() {
2432 assert_eq!(leading_for(px(12.)), Some(px(16.)));
2433 assert_eq!(leading_for(px(14.)), Some(px(20.)));
2434 assert_eq!(leading_for(px(16.)), Some(px(24.)));
2435 assert_eq!(leading_for(px(13.)), None, "an unpairable size stays unset");
2436 }
2437
2438 #[test]
2439 fn explicit_sx_corners_refine_each_corner_individually() {
2440 let sx = captured(|d| d.rounded_tl(px(2.)).rounded_br(px(6.)));
2441 let corners = sx_radius(&sx);
2442 let mut el = round_sx_corners(gpui::div().rounded(px(4.)), &corners);
2443 let radii = el.style().corner_radii.clone();
2444 assert_eq!(
2445 radii.top_left,
2446 Some(gpui::AbsoluteLength::Pixels(px(2.))),
2447 "the explicit top-left corner must win"
2448 );
2449 assert_eq!(
2450 radii.bottom_right,
2451 Some(gpui::AbsoluteLength::Pixels(px(6.))),
2452 "the explicit bottom-right corner must win"
2453 );
2454 assert_eq!(
2455 radii.top_right,
2456 Some(gpui::AbsoluteLength::Pixels(px(4.))),
2457 "an unnamed corner keeps the component's own radius"
2458 );
2459 assert_eq!(
2460 radii.bottom_left,
2461 Some(gpui::AbsoluteLength::Pixels(px(4.))),
2462 "an unnamed corner keeps the component's own radius"
2463 );
2464 }
2465
2466 #[test]
2467 fn rem_radius_stays_unsupported_for_that_corner() {
2468 let sx = captured(|d| d.rounded_tl(rems(1.)).rounded_br(px(3.)));
2469 assert_eq!(
2470 sx_radius(&sx),
2471 gpui::Corners {
2472 top_left: None,
2473 top_right: None,
2474 bottom_right: Some(px(3.)),
2475 bottom_left: None,
2476 }
2477 );
2478 }
2479}
2480
2481#[cfg(test)]
2482mod overlay_stack_tests {
2483 use super::*;
2484 use gpui::AppContext;
2485
2486 fn registration(
2487 cx: &mut gpui::TestAppContext,
2488 window_id: gpui::WindowId,
2489 order: u64,
2490 ) -> gpui::Entity<OverlayRegistration> {
2491 cx.new(|_| OverlayRegistration {
2492 window_id,
2493 order,
2494 phase: OverlayPhase::Open,
2495 keep_exiting: false,
2496 exit_generation: 0,
2497 escape_capture: None,
2498 })
2499 }
2500
2501 #[gpui::test]
2502 fn sibling_order_is_stable_when_open_registrations_repaint(cx: &mut gpui::TestAppContext) {
2503 let outer = registration(cx, gpui::WindowId::from(1), 1);
2504 let inner = registration(cx, gpui::WindowId::from(1), 2);
2505 cx.update(|cx| {
2506 sync_overlay_stack(&outer, cx);
2507 sync_overlay_stack(&inner, cx);
2508 sync_overlay_stack(&outer, cx);
2509 sync_overlay_stack(&inner, cx);
2510 let stack = cx.global::<OverlayStack>();
2511 assert_eq!(stack.entries.len(), 2);
2512 assert_eq!(stack.entries[0].upgrade().unwrap().read(cx).order, 1);
2513 assert_eq!(stack.entries[1].upgrade().unwrap().read(cx).order, 2);
2514 });
2515 }
2516
2517 #[gpui::test]
2518 fn topmost_registration_is_window_scoped(cx: &mut gpui::TestAppContext) {
2519 let first = registration(cx, gpui::WindowId::from(1), 1);
2520 let second = registration(cx, gpui::WindowId::from(2), 2);
2521 let first_token = OverlayToken {
2522 registration: first.downgrade(),
2523 window_id: gpui::WindowId::from(1),
2524 };
2525 let second_token = OverlayToken {
2526 registration: second.downgrade(),
2527 window_id: gpui::WindowId::from(2),
2528 };
2529 cx.update(|cx| {
2530 cx.set_global(OverlayStack {
2531 entries: vec![first.downgrade(), second.downgrade()],
2532 next_order: 2,
2533 });
2534 assert!(is_topmost(&first_token, cx));
2535 assert!(is_topmost(&second_token, cx));
2536 });
2537 }
2538
2539 #[gpui::test]
2540 fn dead_registration_is_pruned_from_the_stack(cx: &mut gpui::TestAppContext) {
2541 let registration = registration(cx, gpui::WindowId::from(1), 1);
2542 let weak = registration.downgrade();
2543 cx.update(|cx| {
2544 cx.set_global(OverlayStack {
2545 entries: vec![weak.clone()],
2546 next_order: 1,
2547 });
2548 });
2549 drop(registration);
2550 cx.update(|cx| {
2551 cx.update_global::<OverlayStack, _>(|stack, cx| {
2552 prune_overlay_stack(stack, cx);
2553 assert!(stack.entries.is_empty());
2554 });
2555 });
2556 }
2557}
2558
2559#[cfg(test)]
2560mod focus_ring_overlay_tests {
2561 use super::*;
2562 use gpui::{px, AbsoluteLength, Length, Styled};
2563
2564 fn inset(el: &mut Div) -> Length {
2565 el.style().inset.top.unwrap()
2566 }
2567
2568 fn radius(el: &mut Div) -> AbsoluteLength {
2569 el.style().corner_radii.top_left.unwrap()
2570 }
2571
2572 fn border(el: &mut Div) -> AbsoluteLength {
2573 el.style().border_widths.top.unwrap()
2574 }
2575
2576 #[test]
2579 fn unoffset_ring_carrier_is_the_control_box() {
2580 let mut carrier = ring_overlay_band(px(8.), px(0.), gpui::red());
2581 assert_eq!(inset(&mut carrier), Length::Definite(px(0.).into()));
2582 assert_eq!(radius(&mut carrier), AbsoluteLength::Pixels(px(8.)));
2583 assert!(carrier.style().border_widths.top.is_none());
2584 assert!(
2585 carrier.style().box_shadow.is_none(),
2586 "the band is an svg, not a shadow"
2587 );
2588 }
2589
2590 #[test]
2594 fn offset_ring_pushes_the_carrier_out_and_paints_the_gap() {
2595 let gap = px(2.);
2596 let mut carrier = ring_overlay_band(px(8.), gap, gpui::red());
2597 assert_eq!(inset(&mut carrier), Length::Definite(px(-2.).into()));
2598 assert_eq!(radius(&mut carrier), AbsoluteLength::Pixels(px(10.)));
2599
2600 let mut inner = ring_overlay_gap(px(8.), gap, gpui::blue());
2601 assert_eq!(inset(&mut inner), Length::Definite(px(0.).into()));
2602 assert_eq!(radius(&mut inner), AbsoluteLength::Pixels(px(10.)));
2603 assert_eq!(border(&mut inner), AbsoluteLength::Pixels(gap));
2604 }
2605
2606 #[gpui::test]
2609 fn the_offset_gap_comes_from_the_layout_theme(cx: &mut gpui::TestAppContext) {
2610 cx.update(|cx| {
2611 herogpui_theme::ThemeProvider::init(cx);
2612 let gap = cx.layout().ring_offset_width;
2613 let mut ring = focus_ring_overlay(px(8.), true, cx);
2614 assert_eq!(inset(&mut ring), Length::Definite((-gap).into()));
2615 assert_eq!(radius(&mut ring), AbsoluteLength::Pixels(px(8.) + gap));
2616
2617 let mut flat = focus_ring_overlay(px(8.), false, cx);
2618 assert_eq!(inset(&mut flat), Length::Definite(px(0.).into()));
2619 assert_eq!(radius(&mut flat), AbsoluteLength::Pixels(px(8.)));
2620 });
2621 }
2622}