1use gpui::{App, BorrowAppContext, Div, Hsla, ParentElement, Pixels, Refineable, Styled};
4use herogpui_core::{element_id, FieldVariant, Prominence};
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 prominence_bg(prominence: Prominence, cx: &App) -> Option<Hsla> {
225 let colors = cx.colors();
226 match prominence {
227 Prominence::Transparent => None,
228 Prominence::Default => Some(colors.surface.background),
229 Prominence::Secondary => Some(colors.surface_secondary),
230 Prominence::Tertiary => Some(colors.surface_tertiary),
231 }
232}
233
234pub fn apply_field_chrome<T: Styled>(
246 el: T,
247 variant: FieldVariant,
248 is_invalid: bool,
249 is_focused: bool,
250 radius_override: Option<Pixels>,
251 cx: &App,
252) -> T {
253 apply_field_chrome_with_focus_ring(
254 el,
255 variant,
256 is_invalid,
257 is_focused,
258 true,
259 radius_override,
260 cx,
261 )
262}
263
264pub fn apply_field_chrome_with_focus_ring<T: Styled>(
270 el: T,
271 variant: FieldVariant,
272 is_invalid: bool,
273 is_focused: bool,
274 show_focus_ring: bool,
275 radius_override: Option<Pixels>,
276 cx: &App,
277) -> T {
278 let colors = cx.colors();
279 let layout = cx.layout();
280
281 let radius = radius_override.unwrap_or_else(|| field_radius(cx));
282 let mut el = el.rounded(radius).bg(match variant {
283 FieldVariant::Primary => colors.field.background,
284 FieldVariant::Secondary => colors.default.color,
288 });
289
290 let mut shadows = if variant == FieldVariant::Primary {
296 layout.field_shadow.clone()
297 } else {
298 Vec::new()
299 };
300
301 if is_invalid {
302 if is_focused && show_focus_ring {
303 shadows.push(gpui::BoxShadow {
304 color: colors.danger.color,
305 offset: gpui::point(gpui::px(0.), gpui::px(0.)),
306 blur_radius: gpui::px(1.),
307 spread_radius: gpui::px(2.),
308 inset: false,
309 });
310 } else {
311 el = el
312 .border(layout.border_width.max(gpui::px(1.)))
313 .border_color(colors.danger.color);
314 }
315 } else if is_focused && show_focus_ring {
316 shadows.extend(focus_ring_shadows(false, cx));
317 } else if layout.field_border_width > gpui::px(0.) {
318 el = el
319 .border(layout.field_border_width)
320 .border_color(colors.field.border);
321 }
322
323 if shadows.is_empty() {
324 el
325 } else {
326 el.shadow(shadows)
327 }
328}
329
330pub fn apply_field_chrome_ringless<T: Styled>(
342 el: T,
343 variant: FieldVariant,
344 is_invalid: bool,
345 is_focused: bool,
346 show_focus_ring: bool,
347 radius_override: Option<Pixels>,
348 cx: &App,
349) -> T {
350 let colors = cx.colors();
351 let layout = cx.layout();
352
353 let radius = radius_override.unwrap_or_else(|| field_radius(cx));
354 let mut el = el.rounded(radius).bg(match variant {
355 FieldVariant::Primary => colors.field.background,
356 FieldVariant::Secondary => colors.default.color,
357 });
358
359 let shadows = if variant == FieldVariant::Primary {
360 layout.field_shadow.clone()
361 } else {
362 Vec::new()
363 };
364
365 if field_ring_color(is_invalid, is_focused, show_focus_ring, cx).is_none() {
368 if is_invalid {
369 el = el
370 .border(layout.border_width.max(gpui::px(1.)))
371 .border_color(colors.danger.color);
372 } else if layout.field_border_width > gpui::px(0.) {
373 el = el
374 .border(layout.field_border_width)
375 .border_color(colors.field.border);
376 }
377 }
378
379 if shadows.is_empty() {
380 el
381 } else {
382 el.shadow(shadows)
383 }
384}
385
386pub fn field_ring_color(
394 is_invalid: bool,
395 is_focused: bool,
396 show_focus_ring: bool,
397 cx: &App,
398) -> Option<Hsla> {
399 if !(is_focused && show_focus_ring) {
400 return None;
401 }
402 Some(if is_invalid {
403 cx.colors().danger.color
404 } else {
405 cx.colors().focus
406 })
407}
408
409pub fn with_field_ring_overlay<T: ParentElement>(
412 el: T,
413 ring: Option<Hsla>,
414 radius: Pixels,
415 cx: &App,
416) -> T {
417 match ring {
418 Some(color) => el.child(ring_overlay_in(radius, false, color, cx)),
419 None => el,
420 }
421}
422
423pub fn field_ring_carrier(
439 shell: impl gpui::IntoElement,
440 ring: Option<Hsla>,
441 radius: Pixels,
442 cx: &App,
443) -> Div {
444 with_field_ring_overlay(
445 gpui::div().relative().flex().flex_col().child(shell),
446 ring,
447 radius,
448 cx,
449 )
450}
451
452pub fn apply_field_chrome_overlay<T: Styled + ParentElement>(
461 el: T,
462 variant: FieldVariant,
463 is_invalid: bool,
464 is_focused: bool,
465 show_focus_ring: bool,
466 radius_override: Option<Pixels>,
467 cx: &App,
468) -> T {
469 let radius = radius_override.unwrap_or_else(|| field_radius(cx));
470 let ring = field_ring_color(is_invalid, is_focused, show_focus_ring, cx);
471 let el = apply_field_chrome_ringless(
472 el,
473 variant,
474 is_invalid,
475 is_focused,
476 show_focus_ring,
477 Some(radius),
478 cx,
479 );
480 with_field_ring_overlay(el, ring, radius, cx)
481}
482
483pub(crate) fn paint_disc(
488 center: gpui::Point<Pixels>,
489 radius: Pixels,
490 color: Hsla,
491 window: &mut gpui::Window,
492) {
493 let mut builder = gpui::PathBuilder::fill();
494 for step in 0..16 {
495 let angle = std::f32::consts::TAU * step as f32 / 16.;
496 let point = gpui::point(
497 center.x + radius * angle.cos(),
498 center.y + radius * angle.sin(),
499 );
500 if step == 0 {
501 builder.move_to(point);
502 } else {
503 builder.line_to(point);
504 }
505 }
506 builder.close();
507 if let Ok(path) = builder.build() {
508 window.paint_path(path, color);
509 }
510}
511
512pub fn floating(el: impl gpui::IntoElement) -> gpui::Deferred {
520 gpui::deferred(el)
521}
522
523pub(crate) fn window_overlay(el: impl gpui::IntoElement, window: &gpui::Window) -> gpui::Deferred {
524 use gpui::InteractiveElement;
525
526 let viewport = window.viewport_size();
527 floating(
528 gpui::anchored()
529 .position(gpui::point(gpui::px(0.), gpui::px(0.)))
530 .child(
531 gpui::div()
532 .relative()
533 .occlude()
534 .w(viewport.width)
535 .h(viewport.height)
536 .flex_shrink_0()
537 .child(el),
538 ),
539 )
540}
541
542#[derive(Clone, Copy, Debug, PartialEq, Eq)]
547pub enum DismissResult {
548 Handled,
549 Declined,
550}
551
552#[derive(Default)]
553struct OverlayStack {
554 entries: Vec<gpui::WeakEntity<OverlayRegistration>>,
555 next_order: u64,
556}
557
558impl gpui::Global for OverlayStack {}
559
560#[derive(Clone)]
561struct OverlayRegistration {
562 window_id: gpui::WindowId,
563 order: u64,
564 phase: OverlayPhase,
565 keep_exiting: bool,
566 exit_generation: u64,
567 escape_capture: Option<std::sync::Arc<dyn Fn(&mut gpui::Window, &mut App) -> DismissResult>>,
568}
569
570#[derive(Clone)]
575pub struct OverlayToken {
576 registration: gpui::WeakEntity<OverlayRegistration>,
577 window_id: gpui::WindowId,
578}
579
580fn ensure_overlay_stack(cx: &mut App) {
581 if cx.try_global::<OverlayStack>().is_none() {
582 cx.set_global(OverlayStack::default());
583 }
584}
585
586fn prune_overlay_stack(stack: &mut OverlayStack, cx: &App) {
587 stack.entries.retain(|entry| {
588 let Some(entry) = entry.upgrade() else {
589 return false;
590 };
591 entry.read(cx).phase == OverlayPhase::Open
592 });
593}
594
595fn sync_overlay_stack(registration: &gpui::Entity<OverlayRegistration>, cx: &mut App) {
596 ensure_overlay_stack(cx);
597 let weak = registration.downgrade();
598 let active = registration.read(cx).phase == OverlayPhase::Open;
599 cx.update_global::<OverlayStack, _>(|stack, cx| {
600 prune_overlay_stack(stack, cx);
601 if active && !stack.entries.iter().any(|entry| entry == &weak) {
602 stack.entries.push(weak);
603 }
604 });
605}
606
607fn is_topmost(token: &OverlayToken, cx: &mut App) -> bool {
608 ensure_overlay_stack(cx);
609 cx.update_global::<OverlayStack, _>(|stack, cx| {
610 prune_overlay_stack(stack, cx);
611 let Some(registration) = token.registration.upgrade() else {
612 return false;
613 };
614 let state = registration.read(cx);
615 if state.window_id != token.window_id || state.phase != OverlayPhase::Open {
616 return false;
617 }
618 stack
619 .entries
620 .iter()
621 .filter_map(|entry| entry.upgrade())
622 .filter(|entry| entry.read(cx).window_id == token.window_id)
623 .max_by_key(|entry| entry.read(cx).order)
624 .is_some_and(|entry| entry == registration)
625 })
626}
627
628pub fn capture_escape(
636 token: &OverlayToken,
637 handler: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
638 cx: &mut App,
639) {
640 if let Some(registration) = token.registration.upgrade() {
641 let handler = shared(handler);
642 registration.update(cx, |state, _| state.escape_capture = Some(handler));
643 }
644}
645
646fn dismiss_captured_escape(window: &mut gpui::Window, cx: &mut App) -> bool {
647 let window_id = window.window_handle().window_id();
648 ensure_overlay_stack(cx);
649 let handler = cx.update_global::<OverlayStack, _>(|stack, cx| {
650 prune_overlay_stack(stack, cx);
651 stack
652 .entries
653 .iter()
654 .filter_map(|entry| entry.upgrade())
655 .filter(|entry| entry.read(cx).window_id == window_id)
656 .filter_map(|entry| {
657 let state = entry.read(cx);
658 state
659 .escape_capture
660 .clone()
661 .map(|handler| (state.order, handler))
662 })
663 .max_by_key(|(order, _)| *order)
664 .map(|(_, handler)| handler)
665 });
666 handler.is_some_and(|handler| handler(window, cx) == DismissResult::Handled)
667}
668
669pub fn dismiss_on_escape_with_token<E: gpui::InteractiveElement>(
684 el: E,
685 token: OverlayToken,
686 close: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
687) -> E {
688 el.on_key_down(move |event: &gpui::KeyDownEvent, window, cx| {
689 if event.keystroke.key == "escape"
690 && is_topmost(&token, cx)
691 && close(window, cx) == DismissResult::Handled
692 {
693 cx.stop_propagation();
694 }
695 })
696}
697
698pub fn dismiss_on_press_outside_with_token<E: gpui::InteractiveElement>(
703 el: E,
704 token: OverlayToken,
705 close: impl Fn(&mut gpui::Window, &mut App) -> DismissResult + 'static,
706) -> E {
707 dismiss_on_press_outside_with_token_event(el, token, move |_, window, cx| close(window, cx))
708}
709
710pub fn dismiss_on_press_outside_with_token_event<E: gpui::InteractiveElement>(
716 el: E,
717 token: OverlayToken,
718 close: impl Fn(&gpui::MouseDownEvent, &mut gpui::Window, &mut App) -> DismissResult + 'static,
719) -> E {
720 el.on_mouse_down_out(move |event, window, cx| {
721 if is_topmost(&token, cx) && close(event, window, cx) == DismissResult::Handled {
722 cx.stop_propagation();
723 }
724 })
725}
726
727#[derive(Clone, Debug, Default)]
739struct PanelFocusState {
740 was_open: bool,
741 restore: Option<gpui::WeakFocusHandle>,
742}
743
744pub fn panel_restore_focus(
750 window: &mut gpui::Window,
751 cx: &mut App,
752 base: &gpui::ElementId,
753) -> gpui::FocusHandle {
754 window
755 .use_keyed_state(
756 element_id::scoped(base, "panel-restore-focus"),
757 cx,
758 |_, cx| cx.focus_handle(),
759 )
760 .read(cx)
761 .clone()
762}
763
764pub fn panel_focus(
765 window: &mut gpui::Window,
766 cx: &mut App,
767 base: &gpui::ElementId,
768 open: bool,
769) -> gpui::FocusHandle {
770 let held = window.use_keyed_state(element_id::scoped(base, "panel-focus"), cx, |_, cx| {
771 cx.focus_handle()
772 });
773 let handle = held.read(cx).clone();
774 let state =
775 window.use_keyed_state(element_id::scoped(base, "panel-focus-state"), cx, |_, _| {
776 PanelFocusState::default()
777 });
778 let current = state.read(cx).clone();
779
780 if open && !current.was_open {
781 let trigger = panel_restore_focus(window, cx, base);
782 let restore = window
787 .focused(cx)
788 .filter(|focused| focused.tab_stop)
789 .map(|focused| focused.downgrade())
790 .or_else(|| Some(trigger.downgrade()));
791 window.focus(&handle, cx);
792 state.update(cx, |state, _| {
793 state.was_open = true;
794 state.restore = restore;
795 });
796 } else if !open && current.was_open {
797 if handle.contains_focused(window, cx) {
798 if let Some(restore) = current.restore.and_then(|handle| handle.upgrade()) {
799 window.focus(&restore, cx);
800 }
801 }
802 state.update(cx, |state, _| {
803 state.was_open = false;
804 state.restore = None;
805 });
806 }
807 handle
808}
809
810#[derive(Clone, Copy, Debug, Default)]
818struct CloseOnBlurState {
819 seen_inside: bool,
821}
822
823#[derive(Clone)]
824pub struct FocusLeave {
825 handle: gpui::FocusHandle,
826 subscription: gpui::Entity<Option<gpui::Subscription>>,
827 state: gpui::Entity<CloseOnBlurState>,
828}
829
830impl FocusLeave {
831 pub fn focus_handle(&self) -> gpui::FocusHandle {
832 self.handle.clone()
833 }
834
835 pub fn consume(&self, cx: &mut App) {
837 self.state.update(cx, |state, _| state.seen_inside = false);
838 self.subscription.update(cx, |slot, _| *slot = None);
839 }
840}
841
842pub fn close_on_blur(
843 window: &mut gpui::Window,
844 cx: &mut App,
845 base: &gpui::ElementId,
846 open: bool,
847 close: impl Fn(&mut gpui::Window, &mut App) + 'static,
848) -> gpui::FocusHandle {
849 on_focus_leave(window, cx, base, open, close).focus_handle()
850}
851
852pub fn on_focus_leave(
854 window: &mut gpui::Window,
855 cx: &mut App,
856 base: &gpui::ElementId,
857 active: bool,
858 leave: impl Fn(&mut gpui::Window, &mut App) + 'static,
859) -> FocusLeave {
860 let held_scope = window.use_keyed_state(
861 element_id::scoped(base, "close-on-blur-scope"),
862 cx,
863 |_, cx| cx.focus_handle().tab_stop(false),
864 );
865 let scope = held_scope.read(cx).clone();
866 let subscription = window.use_keyed_state(
869 element_id::scoped(base, "close-on-blur-subscription"),
870 cx,
871 |_, _| None::<gpui::Subscription>,
872 );
873 let state = window.use_keyed_state(
874 element_id::scoped(base, "close-on-blur-state"),
875 cx,
876 |_, _| CloseOnBlurState::default(),
877 );
878 let armed = subscription.read(cx).is_some();
879 let leave = std::rc::Rc::new(leave);
882
883 if active && !armed {
887 let disarmer = subscription.downgrade();
890 let edge = state.downgrade();
891 let leave = std::rc::Rc::clone(&leave);
892 let listener = window.on_focus_out(&scope, cx, move |_, window, cx| {
893 let Some(edge) = edge.upgrade() else {
894 return;
895 };
896 let due = edge.read(cx).seen_inside;
897 if let Some(disarmer) = disarmer.upgrade() {
898 disarmer.update(cx, |slot, _| *slot = None);
899 }
900 if !due {
901 return;
902 }
903 edge.update(cx, |state, _| state.seen_inside = false);
904 leave(window, cx);
905 });
906 subscription.update(cx, |slot, _| *slot = Some(listener));
907 }
908
909 if active {
916 if scope.contains_focused(window, cx) {
917 state.update(cx, |state, _| state.seen_inside = true);
918 } else if state.read(cx).seen_inside
919 && window.focused(cx).is_some_and(|focused| focused.tab_stop)
920 {
921 state.update(cx, |state, _| state.seen_inside = false);
922 subscription.update(cx, |slot, _| *slot = None);
923 leave(window, cx);
924 }
925 } else {
926 if state.read(cx).seen_inside {
927 state.update(cx, |state, _| state.seen_inside = false);
928 }
929 if armed {
930 subscription.update(cx, |slot, _| *slot = None);
931 }
932 }
933
934 FocusLeave {
935 handle: scope,
936 subscription,
937 state,
938 }
939}
940
941#[allow(clippy::arc_with_non_send_sync)]
948pub fn shared<F: 'static>(f: F) -> std::sync::Arc<F> {
949 std::sync::Arc::new(f)
950}
951
952pub fn placed_panel(placement: herogpui_core::Placement, offset: Pixels) -> Div {
960 use herogpui_core::PlacementAlign;
961
962 let base = gpui::div().absolute();
963 if placement.is_side() {
964 let base = if placement.is_start_side() {
969 base.right_full().mr(offset)
970 } else {
971 base.left_full().ml(offset)
972 };
973 return match placement.align() {
974 PlacementAlign::End => base.bottom(gpui::px(0.)),
975 _ => base.top(gpui::px(0.)),
976 };
977 }
978 let base = if placement.is_above() {
979 base.bottom_full().mb(offset)
980 } else {
981 base.top_full().mt(offset)
982 };
983 match placement.align() {
984 PlacementAlign::Start => base.left(gpui::px(0.)),
985 PlacementAlign::End => base.right(gpui::px(0.)),
986 PlacementAlign::Center => base
989 .left(gpui::px(0.))
990 .right(gpui::px(0.))
991 .flex()
992 .justify_center(),
993 }
994}
995
996pub fn placed_field_panel(placement: herogpui_core::Placement, offset: Pixels) -> Div {
1002 let base = gpui::div().absolute();
1003 if placement.is_side() {
1004 return if placement.is_start_side() {
1005 base.right_full().top(gpui::px(0.)).mr(offset)
1006 } else {
1007 base.left_full().top(gpui::px(0.)).ml(offset)
1008 };
1009 }
1010 if placement.is_above() {
1011 base.bottom_full()
1012 .left(gpui::px(0.))
1013 .right(gpui::px(0.))
1014 .mb(offset)
1015 } else {
1016 base.top_full()
1017 .left(gpui::px(0.))
1018 .right(gpui::px(0.))
1019 .mt(offset)
1020 }
1021}
1022
1023pub fn focus_once(
1029 window: &mut gpui::Window,
1030 cx: &mut App,
1031 key: impl Into<gpui::ElementId>,
1032 handle: &gpui::FocusHandle,
1033) {
1034 let done = window.use_keyed_state(key.into(), cx, |_, _| false);
1035 if !*done.read(cx) {
1036 window.focus(handle, cx);
1037 done.update(cx, |d, _| *d = true);
1038 }
1039}
1040
1041#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1043pub enum OverlayPhase {
1044 #[default]
1046 Closed,
1047 Open,
1049 Exiting,
1051}
1052
1053#[derive(Clone, Copy, Debug, Default)]
1055struct PhaseState {
1056 was_open: bool,
1057 exiting: bool,
1058 exit_generation: u64,
1059}
1060
1061pub fn overlay_scope(
1067 window: &mut gpui::Window,
1068 cx: &mut App,
1069 key: impl Into<gpui::ElementId>,
1070 is_open: bool,
1071 keep_exiting: bool,
1072) -> (OverlayPhase, OverlayToken) {
1073 overlay_scope_with_exit(
1074 window,
1075 cx,
1076 key,
1077 is_open,
1078 keep_exiting,
1079 crate::anim::EXITING_MS,
1080 )
1081}
1082
1083pub fn overlay_scope_with_exit(
1085 window: &mut gpui::Window,
1086 cx: &mut App,
1087 key: impl Into<gpui::ElementId>,
1088 is_open: bool,
1089 keep_exiting: bool,
1090 exit_ms: u64,
1091) -> (OverlayPhase, OverlayToken) {
1092 let key = key.into();
1093 let window_id = window.window_handle().window_id();
1094 let registration = window.use_keyed_state(key, cx, |_, _| OverlayRegistration {
1095 window_id,
1096 order: 0,
1097 phase: OverlayPhase::Closed,
1098 keep_exiting,
1099 exit_generation: 0,
1100 escape_capture: None,
1101 });
1102 let current = registration.read(cx).clone();
1103
1104 let phase = if is_open {
1105 if current.phase != OverlayPhase::Open {
1106 ensure_overlay_stack(cx);
1107 let order = cx.update_global::<OverlayStack, _>(|stack, _| {
1108 stack.next_order = stack.next_order.saturating_add(1);
1109 stack.next_order
1110 });
1111 registration.update(cx, |state, _| {
1112 state.order = order;
1113 state.phase = OverlayPhase::Open;
1114 state.keep_exiting = keep_exiting;
1115 });
1116 }
1117 OverlayPhase::Open
1118 } else if current.phase == OverlayPhase::Open && keep_exiting {
1119 let exit_generation = current.exit_generation.saturating_add(1);
1120 registration.update(cx, |state, _| {
1121 state.phase = OverlayPhase::Exiting;
1122 state.keep_exiting = keep_exiting;
1123 state.exit_generation = exit_generation;
1124 });
1125 let held = registration.downgrade();
1126 cx.spawn(async move |cx: &mut gpui::AsyncApp| {
1127 cx.background_executor()
1128 .timer(std::time::Duration::from_millis(exit_ms))
1129 .await;
1130 cx.update(|cx| {
1131 let _ = held.update(cx, |state, cx| {
1132 if state.phase == OverlayPhase::Exiting
1133 && state.exit_generation == exit_generation
1134 {
1135 state.phase = OverlayPhase::Closed;
1136 cx.notify();
1137 }
1138 });
1139 });
1140 })
1141 .detach();
1142 OverlayPhase::Exiting
1143 } else if current.phase == OverlayPhase::Open {
1144 registration.update(cx, |state, _| {
1145 state.phase = OverlayPhase::Closed;
1146 state.keep_exiting = keep_exiting;
1147 });
1148 OverlayPhase::Closed
1149 } else if current.phase == OverlayPhase::Exiting {
1150 OverlayPhase::Exiting
1151 } else {
1152 if current.keep_exiting != keep_exiting {
1153 registration.update(cx, |state, _| state.keep_exiting = keep_exiting);
1154 }
1155 OverlayPhase::Closed
1156 };
1157
1158 sync_overlay_stack(®istration, cx);
1159 (
1160 phase,
1161 OverlayToken {
1162 registration: registration.downgrade(),
1163 window_id,
1164 },
1165 )
1166}
1167
1168pub fn overlay_phase(
1181 window: &mut gpui::Window,
1182 cx: &mut App,
1183 key: impl Into<gpui::ElementId>,
1184 is_open: bool,
1185) -> OverlayPhase {
1186 retained_phase(window, cx, key, is_open, true, crate::anim::EXITING_MS)
1187}
1188
1189pub fn panel_phase(
1194 window: &mut gpui::Window,
1195 cx: &mut App,
1196 key: impl Into<gpui::ElementId>,
1197 is_open: bool,
1198 keep_exiting: bool,
1199 exit_ms: u64,
1200) -> OverlayPhase {
1201 retained_phase(window, cx, key, is_open, keep_exiting, exit_ms)
1202}
1203
1204fn retained_phase(
1205 window: &mut gpui::Window,
1206 cx: &mut App,
1207 key: impl Into<gpui::ElementId>,
1208 is_open: bool,
1209 keep_exiting: bool,
1210 exit_ms: u64,
1211) -> OverlayPhase {
1212 let key = key.into();
1213 let held = window.use_keyed_state(key, cx, |_, _| PhaseState::default());
1214 let current = *held.read(cx);
1215
1216 if is_open {
1217 if !current.was_open {
1218 held.update(cx, |s, _| {
1219 s.was_open = true;
1220 s.exiting = false;
1221 });
1222 }
1223 OverlayPhase::Open
1224 } else if current.was_open && keep_exiting {
1225 let exit_generation = current.exit_generation.saturating_add(1);
1227 held.update(cx, |s, _| {
1228 s.was_open = false;
1229 s.exiting = true;
1230 s.exit_generation = exit_generation;
1231 });
1232 let held = held.clone();
1233 cx.spawn(async move |cx: &mut gpui::AsyncApp| {
1234 cx.background_executor()
1235 .timer(std::time::Duration::from_millis(exit_ms))
1236 .await;
1237 cx.update(|cx| {
1238 held.update(cx, |s, cx| {
1239 if s.exiting && s.exit_generation == exit_generation {
1240 s.exiting = false;
1241 cx.notify();
1242 }
1243 });
1244 });
1245 })
1246 .detach();
1247 OverlayPhase::Exiting
1248 } else if current.was_open {
1249 held.update(cx, |s, _| {
1250 s.was_open = false;
1251 s.exiting = false;
1252 });
1253 OverlayPhase::Closed
1254 } else if current.exiting {
1255 OverlayPhase::Exiting
1256 } else {
1257 OverlayPhase::Closed
1258 }
1259}
1260
1261pub fn seed_once(
1268 window: &mut gpui::Window,
1269 cx: &mut App,
1270 key: impl Into<gpui::ElementId>,
1271 apply: impl FnOnce(&mut App),
1272) {
1273 let done = window.use_keyed_state(key.into(), cx, |_, _| false);
1274 if !*done.read(cx) {
1275 done.update(cx, |d, _| *d = true);
1276 apply(cx);
1277 }
1278}
1279
1280pub fn controlled<T>(
1290 window: &mut gpui::Window,
1291 cx: &mut App,
1292 key: impl Into<gpui::ElementId>,
1293 controlled: Option<T>,
1294 default: T,
1295) -> (T, Option<gpui::Entity<T>>)
1296where
1297 T: Clone + 'static,
1298{
1299 match controlled {
1300 Some(v) => (v, None),
1302 None => {
1303 let held = window.use_keyed_state(key.into(), cx, move |_, _| default);
1304 let current = held.read(cx).clone();
1305 (current, Some(held))
1306 }
1307 }
1308}
1309
1310struct FocusVisible(bool);
1322impl gpui::Global for FocusVisible {}
1323
1324#[derive(Default)]
1325struct ActiveKeyboardPresses(Vec<(gpui::WindowId, String, gpui::WeakEntity<(bool, bool)>)>);
1326impl gpui::Global for ActiveKeyboardPresses {}
1327
1328#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1341pub struct FieldFocus {
1342 pub is_focused: bool,
1344 pub is_focus_within: bool,
1346 pub is_focus_visible: bool,
1348}
1349
1350pub struct SelectionValue<'a> {
1358 pub selected_items: &'a [gpui::SharedString],
1363 pub selected_indices: &'a [usize],
1366 pub selected_keys: Option<&'a [gpui::SharedString]>,
1374 pub selected_text: &'a str,
1377 pub is_placeholder: bool,
1379 pub default_children: gpui::AnyElement,
1381}
1382
1383#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1392pub struct InteractiveState {
1393 pub is_hovered: bool,
1396 pub is_pressed: bool,
1398 pub is_focused: bool,
1400 pub is_focus_visible: bool,
1402 pub is_selected: bool,
1404 pub is_disabled: bool,
1406 pub is_pending: bool,
1408 pub is_indeterminate: bool,
1411}
1412
1413pub type Interaction = gpui::Entity<(bool, bool)>;
1415
1416fn has_active_keyboard_press(slot: &Interaction, window: &gpui::Window, cx: &App) -> bool {
1417 let weak = slot.downgrade();
1418 let window_id = window.window_handle().window_id();
1419 cx.try_global::<ActiveKeyboardPresses>()
1420 .is_some_and(|pressed| {
1421 pressed.0.iter().any(|(active_window, _, interaction)| {
1422 *active_window == window_id && interaction == &weak
1423 })
1424 })
1425}
1426
1427pub(crate) fn begin_keyboard_press(
1428 slot: &Interaction,
1429 event: &gpui::KeyDownEvent,
1430 window: &gpui::Window,
1431 cx: &mut App,
1432) {
1433 if event.is_held || !matches!(event.keystroke.key.as_str(), "enter" | "space") {
1434 return;
1435 }
1436 let began = slot.update(cx, |state, cx| {
1437 if state.1 {
1438 false
1439 } else {
1440 state.1 = true;
1441 cx.notify();
1442 true
1443 }
1444 });
1445 if began {
1446 if cx.try_global::<ActiveKeyboardPresses>().is_none() {
1447 cx.set_global(ActiveKeyboardPresses::default());
1448 }
1449 let active = (
1450 window.window_handle().window_id(),
1451 event.keystroke.key.clone(),
1452 slot.downgrade(),
1453 );
1454 cx.update_global::<ActiveKeyboardPresses, _>(|pressed, _| {
1455 pressed
1456 .0
1457 .retain(|(_, _, interaction)| interaction.upgrade().is_some());
1458 pressed.0.push(active);
1459 });
1460 }
1461}
1462
1463pub fn interaction(id: gpui::ElementId, window: &mut gpui::Window, cx: &mut App) -> Interaction {
1469 window.use_keyed_state(id, cx, |_, _| (false, false))
1470}
1471
1472pub fn track_interaction<T>(el: T, slot: &Interaction) -> T
1474where
1475 T: gpui::StatefulInteractiveElement + ParentElement,
1476{
1477 track_interaction_on_mouse_down(el, slot, |_, _| {})
1478}
1479
1480pub(crate) fn track_interaction_on_mouse_down<T>(
1483 el: T,
1484 slot: &Interaction,
1485 on_mouse_down: impl Fn(&mut gpui::Window, &mut App) + 'static,
1486) -> T
1487where
1488 T: gpui::StatefulInteractiveElement + ParentElement,
1489{
1490 let hover = slot.clone();
1491 let down = slot.clone();
1492 let up = slot.clone();
1493 let key_down = slot.clone();
1494 let key_up = slot.clone();
1495 let outside_up = slot.clone();
1496 let release = gpui::canvas(
1504 |bounds, _, _| bounds,
1505 move |_, _, window, _| {
1506 window.on_mouse_event(move |event: &gpui::MouseUpEvent, phase, window, cx| {
1507 if phase == gpui::DispatchPhase::Capture
1508 && event.button == gpui::MouseButton::Left
1509 && !has_active_keyboard_press(&outside_up, window, cx)
1510 {
1511 outside_up.update(cx, |state, cx| {
1512 if state.1 {
1513 state.1 = false;
1514 cx.notify();
1515 }
1516 });
1517 }
1518 });
1519 },
1520 )
1521 .absolute()
1522 .inset_0();
1523 el.on_hover(move |over, _, cx| {
1524 let over = *over;
1525 hover.update(cx, |state, cx| {
1526 if state.0 != over {
1527 state.0 = over;
1528 cx.notify();
1529 }
1530 });
1531 })
1532 .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
1533 down.update(cx, |state, cx| {
1534 if !state.1 {
1535 state.1 = true;
1536 cx.notify();
1537 }
1538 });
1539 on_mouse_down(window, cx);
1540 })
1541 .on_mouse_up(gpui::MouseButton::Left, move |_, window, cx| {
1542 if !has_active_keyboard_press(&up, window, cx) {
1543 up.update(cx, |state, cx| {
1544 if state.1 {
1545 state.1 = false;
1546 cx.notify();
1547 }
1548 });
1549 }
1550 })
1551 .on_key_down(move |event, window, cx| {
1552 begin_keyboard_press(&key_down, event, window, cx);
1553 })
1554 .on_key_up(move |event, _, cx| {
1555 if matches!(event.keystroke.key.as_str(), "enter" | "space") {
1556 key_up.update(cx, |state, cx| {
1557 if state.1 {
1558 state.1 = false;
1559 cx.notify();
1560 }
1561 });
1562 }
1563 })
1564 .child(release)
1565}
1566
1567pub fn focus_visible(cx: &App) -> bool {
1568 cx.try_global::<FocusVisible>().is_some_and(|v| v.0)
1569}
1570
1571pub fn set_focus_visible(visible: bool, cx: &mut App) {
1572 if focus_visible(cx) != visible {
1573 cx.set_global(FocusVisible(visible));
1574 cx.refresh_windows();
1575 }
1576}
1577
1578pub(crate) fn shows_focus_ring(target: bool, cx: &App) -> bool {
1580 target && focus_visible(cx)
1581}
1582
1583pub(crate) fn key_enables_focus_visible(key: &str) -> bool {
1589 !matches!(
1590 key,
1591 "escape"
1592 | "shift"
1593 | "control"
1594 | "ctrl"
1595 | "alt"
1596 | "option"
1597 | "meta"
1598 | "command"
1599 | "win"
1600 | "windows"
1601 | "fn"
1602 | "function"
1603 )
1604}
1605
1606pub fn app_focus_root<T>(el: T, window: &mut gpui::Window, cx: &mut App) -> T
1621where
1622 T: gpui::InteractiveElement,
1623{
1624 let root = window
1625 .use_keyed_state(
1626 gpui::ElementId::Name("herogpui-focus-root".into()),
1627 cx,
1628 |_, cx| cx.focus_handle(),
1629 )
1630 .read(cx)
1631 .clone();
1632 if !root.contains_focused(window, cx) {
1633 window.focus(&root, cx);
1634 }
1635 el.track_focus(&root)
1636 .capture_any_mouse_down(|_, _, cx| set_focus_visible(false, cx))
1637 .capture_key_down(|event, window, cx| {
1638 if event.keystroke.key == "escape" && dismiss_captured_escape(window, cx) {
1639 cx.stop_propagation();
1640 }
1641 })
1642 .on_key_down(|event, window, cx| {
1643 if key_enables_focus_visible(&event.keystroke.key) {
1644 set_focus_visible(true, cx);
1645 }
1646 if event.keystroke.key == "tab" {
1647 if event.keystroke.modifiers.shift {
1648 window.focus_prev(cx);
1649 } else {
1650 window.focus_next(cx);
1651 }
1652 cx.stop_propagation();
1653 }
1654 })
1655 .on_key_up(|event, window, cx| {
1656 if !matches!(event.keystroke.key.as_str(), "enter" | "space")
1657 || cx.try_global::<ActiveKeyboardPresses>().is_none()
1658 {
1659 return;
1660 }
1661 let window_id = window.window_handle().window_id();
1662 let key = event.keystroke.key.as_str();
1663 let interactions = cx.update_global::<ActiveKeyboardPresses, _>(|pressed, _| {
1664 let mut released = Vec::new();
1665 pressed
1666 .0
1667 .retain(|(active_window, active_key, interaction)| {
1668 if *active_window == window_id && active_key == key {
1669 released.push(interaction.clone());
1670 false
1671 } else {
1672 interaction.upgrade().is_some()
1673 }
1674 });
1675 released
1676 });
1677 for interaction in interactions {
1678 if let Some(interaction) = interaction.upgrade() {
1679 interaction.update(cx, |state, cx| {
1680 if state.1 {
1681 state.1 = false;
1682 cx.notify();
1683 }
1684 });
1685 }
1686 }
1687 })
1688}
1689
1690pub fn tab_stop_handle(
1697 id: gpui::ElementId,
1698 window: &mut gpui::Window,
1699 cx: &mut App,
1700) -> gpui::FocusHandle {
1701 window
1702 .use_keyed_state(id, cx, |_, cx| cx.focus_handle().tab_stop(true))
1703 .read(cx)
1704 .clone()
1705}
1706
1707const RING_WIDTH: Pixels = gpui::px(2.);
1710
1711pub fn focus_ring_shadows(offset: bool, cx: &App) -> Vec<gpui::BoxShadow> {
1730 let colors = cx.colors();
1731 let layout = cx.layout();
1732 let ring = RING_WIDTH;
1733 let blur = gpui::px(1.);
1734 let gap = if offset {
1735 layout.ring_offset_width
1736 } else {
1737 gpui::px(0.)
1738 };
1739 let mut shadows = vec![gpui::BoxShadow {
1740 color: colors.focus,
1741 offset: gpui::point(gpui::px(0.), gpui::px(0.)),
1742 blur_radius: blur,
1743 spread_radius: gap + ring,
1744 inset: false,
1745 }];
1746 if gap > gpui::px(0.) {
1747 shadows.push(gpui::BoxShadow {
1748 color: colors.background,
1749 offset: gpui::point(gpui::px(0.), gpui::px(0.)),
1750 blur_radius: blur,
1751 spread_radius: gap,
1752 inset: false,
1753 });
1754 }
1755 shadows
1756}
1757
1758pub fn focus_ring_overlay(radius: Pixels, offset: bool, cx: &App) -> Div {
1797 ring_overlay_in(radius, offset, cx.colors().focus, cx)
1798}
1799
1800pub(crate) fn ring_overlay_in(radius: Pixels, offset: bool, color: Hsla, cx: &App) -> Div {
1803 let gap = if offset {
1804 cx.layout().ring_offset_width
1805 } else {
1806 gpui::px(0.)
1807 };
1808 let outer = ring_overlay_band(radius, gap, color);
1809 if gap > gpui::px(0.) {
1810 outer.child(ring_overlay_gap(radius, gap, cx.colors().background))
1811 } else {
1812 outer
1813 }
1814}
1815
1816const RING_BLUR_REACH: Pixels = gpui::px(3.);
1818const RING_BLUR_SIGMA: f32 = 0.7;
1821const RING_UNDERLAP: f32 = 1.5;
1825
1826fn ring_overlay_band(radius: Pixels, gap: Pixels, color: Hsla) -> Div {
1841 let reach = RING_WIDTH + RING_BLUR_REACH;
1842 let corner = radius + gap;
1843 let canvas = gpui::canvas(
1844 |_, _, _| (),
1845 move |bounds, _, window, cx| {
1846 let w = f32::from(bounds.size.width);
1847 let h = f32::from(bounds.size.height);
1848 if w <= 0. || h <= 0. {
1849 return;
1850 }
1851 let ring = f32::from(RING_WIDTH);
1852 let inner = f32::from(reach);
1853 let rg = f32::from(corner);
1854 let stroke_w = ring + RING_UNDERLAP;
1855 let stroke_at = f32::from(RING_BLUR_REACH) + stroke_w / 2.;
1856 let svg = format!(
1857 concat!(
1858 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{w:.2}\" height=\"{h:.2}\" ",
1859 "viewBox=\"0 0 {w:.2} {h:.2}\"><defs>",
1860 "<filter id=\"b\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">",
1861 "<feGaussianBlur stdDeviation=\"{sigma:.3}\"/></filter>",
1862 "<mask id=\"m\"><rect width=\"{w:.2}\" height=\"{h:.2}\" fill=\"#fff\"/>",
1863 "<rect x=\"{inner:.2}\" y=\"{inner:.2}\" width=\"{iw:.2}\" height=\"{ih:.2}\" rx=\"{rg:.2}\" fill=\"#000\"/>",
1864 "</mask></defs><g mask=\"url(#m)\">",
1865 "<rect x=\"{sx:.2}\" y=\"{sx:.2}\" width=\"{sw:.2}\" height=\"{sh:.2}\" rx=\"{srx:.2}\" ",
1866 "fill=\"none\" stroke=\"#000\" stroke-width=\"{ring:.2}\" filter=\"url(#b)\"/></g></svg>"
1867 ),
1868 w = w,
1869 h = h,
1870 sigma = RING_BLUR_SIGMA,
1871 inner = inner,
1872 iw = (w - 2. * inner).max(0.),
1873 ih = (h - 2. * inner).max(0.),
1874 rg = rg,
1875 sx = stroke_at,
1876 sw = (w - 2. * stroke_at).max(0.),
1877 sh = (h - 2. * stroke_at).max(0.),
1878 srx = (rg + ring - stroke_w / 2.).max(0.),
1879 ring = stroke_w,
1880 );
1881 let key: gpui::SharedString =
1882 format!("herogpui://focus-ring/{w:.1}x{h:.1}/r{rg:.1}").into();
1883 let _ = window.paint_svg(
1884 bounds,
1885 key,
1886 Some(svg.as_bytes()),
1887 gpui::TransformationMatrix::unit(),
1888 color,
1889 cx,
1890 );
1891 },
1892 )
1893 .absolute()
1894 .inset(-reach);
1895 gpui::div()
1896 .absolute()
1897 .inset(-gap)
1898 .rounded(corner)
1899 .child(canvas)
1900}
1901
1902fn ring_overlay_gap(radius: Pixels, gap: Pixels, background: Hsla) -> Div {
1906 gpui::div()
1907 .absolute()
1908 .inset(gpui::px(0.))
1909 .rounded(radius + gap)
1910 .border(gap)
1911 .border_color(background)
1912}
1913
1914pub fn with_focus_ring_overlay<T: Styled + ParentElement>(
1921 el: T,
1922 focused: bool,
1923 offset: bool,
1924 radius: Pixels,
1925 base: Vec<gpui::BoxShadow>,
1926 cx: &App,
1927) -> T {
1928 let el = if base.is_empty() { el } else { el.shadow(base) };
1929 if !focused {
1930 return el;
1931 }
1932 el.child(focus_ring_overlay(radius, offset, cx))
1933}
1934
1935pub fn ring_overlay_if_focused<T: Styled + ParentElement>(
1937 el: T,
1938 handle: &gpui::FocusHandle,
1939 offset: bool,
1940 radius: Pixels,
1941 base: Vec<gpui::BoxShadow>,
1942 window: &gpui::Window,
1943 cx: &App,
1944) -> T {
1945 let focused = shows_focus_ring(handle.is_focused(window), cx);
1946 with_focus_ring_overlay(el, focused, offset, radius, base, cx)
1947}
1948
1949pub fn trap_tab<T: gpui::InteractiveElement>(el: T, scope: &gpui::FocusHandle) -> T {
1962 let scope = scope.clone();
1963 el.on_key_down(move |event: &gpui::KeyDownEvent, window, cx| {
1964 if event.keystroke.key != "tab" {
1965 return;
1966 }
1967 cx.stop_propagation();
1971 set_focus_visible(true, cx);
1972 let back = event.keystroke.modifiers.shift;
1973 if back {
1974 window.focus_prev(cx);
1975 } else {
1976 window.focus_next(cx);
1977 }
1978 if scope.contains_focused(window, cx) {
1979 return;
1980 }
1981 window.focus(&scope, cx);
1983 window.focus_next(cx);
1984 if !back {
1985 return;
1986 }
1987 for _ in 0..256 {
1989 window.focus_next(cx);
1990 if !scope.contains_focused(window, cx) {
1991 window.focus_prev(cx);
1992 return;
1993 }
1994 }
1995 })
1996}
1997
1998pub fn inset_focus_ring(cx: &App) -> Div {
2012 let colors = cx.colors();
2013 gpui::div()
2014 .absolute()
2015 .inset_0()
2016 .border_2()
2017 .border_color(colors.focus)
2018 .rounded(key_radius(cx))
2019}
2020
2021pub fn with_focus_ring<T: Styled>(
2027 el: T,
2028 focused: bool,
2029 offset: bool,
2030 base: Vec<gpui::BoxShadow>,
2031 cx: &App,
2032) -> T {
2033 if !focused {
2034 return if base.is_empty() { el } else { el.shadow(base) };
2035 }
2036 let mut all = base;
2037 all.extend(focus_ring_shadows(offset, cx));
2038 el.shadow(all)
2039}
2040
2041pub fn focusable<T>(
2046 el: T,
2047 id: gpui::ElementId,
2048 offset: bool,
2049 window: &mut gpui::Window,
2050 cx: &mut App,
2051) -> T
2052where
2053 T: Styled + gpui::InteractiveElement,
2054{
2055 let handle = tab_stop_handle(id, window, cx);
2056 ring_if_focused(
2057 el.track_focus(&handle),
2058 &handle,
2059 offset,
2060 Vec::new(),
2061 window,
2062 cx,
2063 )
2064}
2065
2066pub fn ring_if_focused<T: Styled>(
2071 el: T,
2072 handle: &gpui::FocusHandle,
2073 offset: bool,
2074 base: Vec<gpui::BoxShadow>,
2075 window: &gpui::Window,
2076 cx: &App,
2077) -> T {
2078 let focused = shows_focus_ring(handle.is_focused(window), cx);
2079 with_focus_ring(el, focused, offset, base, cx)
2080}
2081
2082pub fn capture_sx(style: impl FnOnce(Div) -> Div) -> Box<gpui::StyleRefinement> {
2095 Box::new(style(gpui::div()).style().clone())
2096}
2097
2098pub fn apply_sx<T: Styled>(el: T, sx: &Option<Box<gpui::StyleRefinement>>) -> T {
2104 let Some(sx) = sx else { return el };
2105 let mut el = el;
2106 el.style().refine(sx);
2107 el
2108}
2109
2110pub fn sx_background(sx: &Option<Box<gpui::StyleRefinement>>) -> Option<Hsla> {
2115 match sx.as_ref()?.background {
2116 Some(gpui::Fill::Color(background)) => background.as_solid(),
2117 _ => None,
2118 }
2119}
2120
2121pub fn sx_border_color(sx: &Option<Box<gpui::StyleRefinement>>) -> Option<Hsla> {
2127 sx.as_ref()?.border_color
2128}
2129
2130pub fn sx_pixel_size(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Size<Option<Pixels>> {
2136 fn definite(length: gpui::Length) -> Option<Pixels> {
2137 match length {
2138 gpui::Length::Definite(gpui::DefiniteLength::Absolute(
2139 gpui::AbsoluteLength::Pixels(pixels),
2140 )) => Some(pixels),
2141 _ => None,
2142 }
2143 }
2144 let Some(sx) = sx else {
2145 return gpui::Size {
2146 width: None,
2147 height: None,
2148 };
2149 };
2150 gpui::Size {
2151 width: sx.size.width.and_then(definite),
2152 height: sx.size.height.and_then(definite),
2153 }
2154}
2155
2156pub(crate) fn fade_endpoints(
2170 variant: Option<(Hsla, Hsla)>,
2171 sx_background: Option<Hsla>,
2172 hover_bg: Option<Hsla>,
2173) -> Option<(Hsla, Hsla)> {
2174 let resting = sx_background.or_else(|| variant.map(|(idle, _)| idle));
2175 match (hover_bg, resting) {
2176 (Some(hover), Some(resting)) => Some((resting, hover)),
2177 _ => variant.map(|colors| sx_background.map_or(colors, |color| (color, color))),
2178 }
2179}
2180
2181pub(crate) fn leading_for(text_size: Pixels) -> Option<Pixels> {
2186 let size = f32::from(text_size);
2187 if (size - 12.0).abs() < f32::EPSILON {
2188 Some(gpui::px(16.))
2189 } else if (size - 14.0).abs() < f32::EPSILON {
2190 Some(gpui::px(20.))
2191 } else if (size - 16.0).abs() < f32::EPSILON {
2192 Some(gpui::px(24.))
2193 } else {
2194 None
2195 }
2196}
2197
2198pub(crate) fn fill_unspecified_corners(
2206 mut corners: gpui::Corners<Option<Pixels>>,
2207 radius: Option<Pixels>,
2208) -> gpui::Corners<Option<Pixels>> {
2209 let Some(radius) = radius else {
2210 return corners;
2211 };
2212 if corners.top_left.is_none() {
2213 corners.top_left = Some(radius);
2214 }
2215 if corners.top_right.is_none() {
2216 corners.top_right = Some(radius);
2217 }
2218 if corners.bottom_right.is_none() {
2219 corners.bottom_right = Some(radius);
2220 }
2221 if corners.bottom_left.is_none() {
2222 corners.bottom_left = Some(radius);
2223 }
2224 corners
2225}
2226
2227pub(crate) fn round_sx_corners<T: Styled>(el: T, corners: &gpui::Corners<Option<Pixels>>) -> T {
2228 let mut el = el;
2229 if let Some(pixels) = corners.top_left {
2230 el = el.rounded_tl(pixels);
2231 }
2232 if let Some(pixels) = corners.top_right {
2233 el = el.rounded_tr(pixels);
2234 }
2235 if let Some(pixels) = corners.bottom_right {
2236 el = el.rounded_br(pixels);
2237 }
2238 if let Some(pixels) = corners.bottom_left {
2239 el = el.rounded_bl(pixels);
2240 }
2241 el
2242}
2243
2244pub fn sx_padding(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Edges<Option<Pixels>> {
2252 fn definite(length: gpui::DefiniteLength) -> Option<Pixels> {
2253 match length {
2254 gpui::DefiniteLength::Absolute(gpui::AbsoluteLength::Pixels(pixels)) => Some(pixels),
2255 _ => None,
2256 }
2257 }
2258 let Some(sx) = sx else {
2259 return gpui::Edges::all(None);
2260 };
2261 gpui::Edges {
2262 top: sx.padding.top.and_then(definite),
2263 right: sx.padding.right.and_then(definite),
2264 bottom: sx.padding.bottom.and_then(definite),
2265 left: sx.padding.left.and_then(definite),
2266 }
2267}
2268
2269pub fn sx_radius(sx: &Option<Box<gpui::StyleRefinement>>) -> gpui::Corners<Option<Pixels>> {
2275 fn absolute(length: gpui::AbsoluteLength) -> Option<Pixels> {
2276 match length {
2277 gpui::AbsoluteLength::Pixels(pixels) => Some(pixels),
2278 gpui::AbsoluteLength::Rems(_) => None,
2279 }
2280 }
2281 let Some(sx) = sx else {
2282 return gpui::Corners::default();
2283 };
2284 gpui::Corners {
2285 top_left: sx.corner_radii.top_left.and_then(absolute),
2286 top_right: sx.corner_radii.top_right.and_then(absolute),
2287 bottom_right: sx.corner_radii.bottom_right.and_then(absolute),
2288 bottom_left: sx.corner_radii.bottom_left.and_then(absolute),
2289 }
2290}
2291
2292#[cfg(test)]
2293mod sx_extraction_tests {
2294 use super::*;
2295 use gpui::{px, relative, rems, Div, Styled};
2296
2297 fn captured(style: impl FnOnce(Div) -> Div) -> Option<Box<gpui::StyleRefinement>> {
2298 Some(capture_sx(style))
2299 }
2300
2301 #[test]
2302 #[allow(clippy::float_cmp)] fn field_box_defaults_are_the_stock_field_metrics() {
2304 let field = FieldBox::default();
2305 assert_eq!(field.resolved_height(), FIELD_HEIGHT);
2306 assert_eq!(f32::from(field.resolved_padding_x()), 12.);
2307 }
2308
2309 #[test]
2310 fn no_override_extracts_nothing() {
2311 assert_eq!(sx_padding(&None), gpui::Edges::all(None));
2312 assert_eq!(sx_radius(&None), gpui::Corners::default());
2313 assert_eq!(sx_border_color(&None), None);
2314 }
2315
2316 #[test]
2317 fn border_color_override_is_preserved_for_child_chrome() {
2318 let color = gpui::hsla(0.58, 0.7, 0.4, 1.0);
2319 let sx = captured(|d| d.border_color(color));
2320 assert_eq!(sx_border_color(&sx), Some(color));
2321 }
2322
2323 #[test]
2324 fn uniform_pixel_padding_extracts_on_every_edge() {
2325 let sx = captured(|d| d.p(px(8.)));
2326 assert_eq!(sx_padding(&sx), gpui::Edges::all(Some(px(8.))));
2327 }
2328
2329 #[test]
2330 fn per_edge_padding_preserves_each_edge() {
2331 let sx = captured(|d| d.pt(px(1.)).pr(px(2.)).pb(px(3.)).pl(px(4.)));
2332 assert_eq!(
2333 sx_padding(&sx),
2334 gpui::Edges {
2335 top: Some(px(1.)),
2336 right: Some(px(2.)),
2337 bottom: Some(px(3.)),
2338 left: Some(px(4.)),
2339 }
2340 );
2341 }
2342
2343 #[test]
2344 fn zero_padding_is_a_real_override() {
2345 let sx = captured(|d| d.p(px(0.)));
2346 assert_eq!(sx_padding(&sx), gpui::Edges::all(Some(px(0.))));
2347 }
2348
2349 #[test]
2350 fn rems_and_fractions_stay_unsupported_edge_by_edge() {
2351 let sx = captured(|d| d.pt(px(2.)).pb(rems(1.)).pl(relative(0.5)));
2352 assert_eq!(
2353 sx_padding(&sx),
2354 gpui::Edges {
2355 top: Some(px(2.)),
2356 right: None,
2357 bottom: None,
2358 left: None,
2359 }
2360 );
2361 let raw = sx.as_ref().unwrap();
2364 assert!(matches!(
2365 raw.padding.bottom,
2366 Some(gpui::DefiniteLength::Absolute(gpui::AbsoluteLength::Rems(
2367 _
2368 )))
2369 ));
2370 assert!(matches!(
2371 raw.padding.left,
2372 Some(gpui::DefiniteLength::Fraction(_))
2373 ));
2374 }
2375
2376 #[test]
2377 fn uniform_and_per_corner_radius_extract() {
2378 let sx = captured(|d| d.rounded(px(6.)));
2379 assert_eq!(
2380 sx_radius(&sx),
2381 gpui::Corners {
2382 top_left: Some(px(6.)),
2383 top_right: Some(px(6.)),
2384 bottom_right: Some(px(6.)),
2385 bottom_left: Some(px(6.)),
2386 }
2387 );
2388
2389 let sx = captured(|d| d.rounded_tl(px(1.)).rounded_br(px(3.)));
2390 assert_eq!(
2391 sx_radius(&sx),
2392 gpui::Corners {
2393 top_left: Some(px(1.)),
2394 top_right: None,
2395 bottom_right: Some(px(3.)),
2396 bottom_left: None,
2397 }
2398 );
2399 }
2400
2401 #[test]
2402 fn leading_pairs_follow_the_v3_steps() {
2403 assert_eq!(leading_for(px(12.)), Some(px(16.)));
2404 assert_eq!(leading_for(px(14.)), Some(px(20.)));
2405 assert_eq!(leading_for(px(16.)), Some(px(24.)));
2406 assert_eq!(leading_for(px(13.)), None, "an unpairable size stays unset");
2407 }
2408
2409 #[test]
2410 fn explicit_sx_corners_refine_each_corner_individually() {
2411 let sx = captured(|d| d.rounded_tl(px(2.)).rounded_br(px(6.)));
2412 let corners = sx_radius(&sx);
2413 let mut el = round_sx_corners(gpui::div().rounded(px(4.)), &corners);
2414 let radii = el.style().corner_radii.clone();
2415 assert_eq!(
2416 radii.top_left,
2417 Some(gpui::AbsoluteLength::Pixels(px(2.))),
2418 "the explicit top-left corner must win"
2419 );
2420 assert_eq!(
2421 radii.bottom_right,
2422 Some(gpui::AbsoluteLength::Pixels(px(6.))),
2423 "the explicit bottom-right corner must win"
2424 );
2425 assert_eq!(
2426 radii.top_right,
2427 Some(gpui::AbsoluteLength::Pixels(px(4.))),
2428 "an unnamed corner keeps the component's own radius"
2429 );
2430 assert_eq!(
2431 radii.bottom_left,
2432 Some(gpui::AbsoluteLength::Pixels(px(4.))),
2433 "an unnamed corner keeps the component's own radius"
2434 );
2435 }
2436
2437 #[test]
2438 fn rem_radius_stays_unsupported_for_that_corner() {
2439 let sx = captured(|d| d.rounded_tl(rems(1.)).rounded_br(px(3.)));
2440 assert_eq!(
2441 sx_radius(&sx),
2442 gpui::Corners {
2443 top_left: None,
2444 top_right: None,
2445 bottom_right: Some(px(3.)),
2446 bottom_left: None,
2447 }
2448 );
2449 }
2450}
2451
2452#[cfg(test)]
2453mod overlay_stack_tests {
2454 use super::*;
2455 use gpui::AppContext;
2456
2457 fn registration(
2458 cx: &mut gpui::TestAppContext,
2459 window_id: gpui::WindowId,
2460 order: u64,
2461 ) -> gpui::Entity<OverlayRegistration> {
2462 cx.new(|_| OverlayRegistration {
2463 window_id,
2464 order,
2465 phase: OverlayPhase::Open,
2466 keep_exiting: false,
2467 exit_generation: 0,
2468 escape_capture: None,
2469 })
2470 }
2471
2472 #[gpui::test]
2473 fn sibling_order_is_stable_when_open_registrations_repaint(cx: &mut gpui::TestAppContext) {
2474 let outer = registration(cx, gpui::WindowId::from(1), 1);
2475 let inner = registration(cx, gpui::WindowId::from(1), 2);
2476 cx.update(|cx| {
2477 sync_overlay_stack(&outer, cx);
2478 sync_overlay_stack(&inner, cx);
2479 sync_overlay_stack(&outer, cx);
2480 sync_overlay_stack(&inner, cx);
2481 let stack = cx.global::<OverlayStack>();
2482 assert_eq!(stack.entries.len(), 2);
2483 assert_eq!(stack.entries[0].upgrade().unwrap().read(cx).order, 1);
2484 assert_eq!(stack.entries[1].upgrade().unwrap().read(cx).order, 2);
2485 });
2486 }
2487
2488 #[gpui::test]
2489 fn topmost_registration_is_window_scoped(cx: &mut gpui::TestAppContext) {
2490 let first = registration(cx, gpui::WindowId::from(1), 1);
2491 let second = registration(cx, gpui::WindowId::from(2), 2);
2492 let first_token = OverlayToken {
2493 registration: first.downgrade(),
2494 window_id: gpui::WindowId::from(1),
2495 };
2496 let second_token = OverlayToken {
2497 registration: second.downgrade(),
2498 window_id: gpui::WindowId::from(2),
2499 };
2500 cx.update(|cx| {
2501 cx.set_global(OverlayStack {
2502 entries: vec![first.downgrade(), second.downgrade()],
2503 next_order: 2,
2504 });
2505 assert!(is_topmost(&first_token, cx));
2506 assert!(is_topmost(&second_token, cx));
2507 });
2508 }
2509
2510 #[gpui::test]
2511 fn dead_registration_is_pruned_from_the_stack(cx: &mut gpui::TestAppContext) {
2512 let registration = registration(cx, gpui::WindowId::from(1), 1);
2513 let weak = registration.downgrade();
2514 cx.update(|cx| {
2515 cx.set_global(OverlayStack {
2516 entries: vec![weak.clone()],
2517 next_order: 1,
2518 });
2519 });
2520 drop(registration);
2521 cx.update(|cx| {
2522 cx.update_global::<OverlayStack, _>(|stack, cx| {
2523 prune_overlay_stack(stack, cx);
2524 assert!(stack.entries.is_empty());
2525 });
2526 });
2527 }
2528}
2529
2530#[cfg(test)]
2531mod focus_ring_overlay_tests {
2532 use super::*;
2533 use gpui::{px, AbsoluteLength, Length, Styled};
2534
2535 fn inset(el: &mut Div) -> Length {
2536 el.style().inset.top.unwrap()
2537 }
2538
2539 fn radius(el: &mut Div) -> AbsoluteLength {
2540 el.style().corner_radii.top_left.unwrap()
2541 }
2542
2543 fn border(el: &mut Div) -> AbsoluteLength {
2544 el.style().border_widths.top.unwrap()
2545 }
2546
2547 #[test]
2550 fn unoffset_ring_carrier_is_the_control_box() {
2551 let mut carrier = ring_overlay_band(px(8.), px(0.), gpui::red());
2552 assert_eq!(inset(&mut carrier), Length::Definite(px(0.).into()));
2553 assert_eq!(radius(&mut carrier), AbsoluteLength::Pixels(px(8.)));
2554 assert!(carrier.style().border_widths.top.is_none());
2555 assert!(
2556 carrier.style().box_shadow.is_none(),
2557 "the band is an svg, not a shadow"
2558 );
2559 }
2560
2561 #[test]
2565 fn offset_ring_pushes_the_carrier_out_and_paints_the_gap() {
2566 let gap = px(2.);
2567 let mut carrier = ring_overlay_band(px(8.), gap, gpui::red());
2568 assert_eq!(inset(&mut carrier), Length::Definite(px(-2.).into()));
2569 assert_eq!(radius(&mut carrier), AbsoluteLength::Pixels(px(10.)));
2570
2571 let mut inner = ring_overlay_gap(px(8.), gap, gpui::blue());
2572 assert_eq!(inset(&mut inner), Length::Definite(px(0.).into()));
2573 assert_eq!(radius(&mut inner), AbsoluteLength::Pixels(px(10.)));
2574 assert_eq!(border(&mut inner), AbsoluteLength::Pixels(gap));
2575 }
2576
2577 #[gpui::test]
2580 fn the_offset_gap_comes_from_the_layout_theme(cx: &mut gpui::TestAppContext) {
2581 cx.update(|cx| {
2582 herogpui_theme::ThemeProvider::init(cx);
2583 let gap = cx.layout().ring_offset_width;
2584 let mut ring = focus_ring_overlay(px(8.), true, cx);
2585 assert_eq!(inset(&mut ring), Length::Definite((-gap).into()));
2586 assert_eq!(radius(&mut ring), AbsoluteLength::Pixels(px(8.) + gap));
2587
2588 let mut flat = focus_ring_overlay(px(8.), false, cx);
2589 assert_eq!(inset(&mut flat), Length::Definite(px(0.).into()));
2590 assert_eq!(radius(&mut flat), AbsoluteLength::Pixels(px(8.)));
2591 });
2592 }
2593}