Tune the focus-visible heuristics a bit (#41314)

Mikayla Maki created

This isn't quite right yet, as a proper solution would remember the
input modality at the moment of focus change, rather than at painting
time. But this gets us close enough for now.

Release Notes:

- N/A

Change summary

crates/gpui/src/interactive.rs | 22 ++++++++++++++++++++++
crates/gpui/src/window.rs      | 25 ++++++++++++++++---------
2 files changed, 38 insertions(+), 9 deletions(-)

Detailed changes

crates/gpui/src/interactive.rs 🔗

@@ -115,6 +115,16 @@ impl InputEvent for MouseDownEvent {
 }
 impl MouseEvent for MouseDownEvent {}
 
+impl MouseDownEvent {
+    /// Returns true if this mouse up event should focus the element.
+    pub fn is_focusing(&self) -> bool {
+        match self.button {
+            MouseButton::Left => true,
+            _ => false,
+        }
+    }
+}
+
 /// A mouse up event from the platform
 #[derive(Clone, Debug, Default)]
 pub struct MouseUpEvent {
@@ -137,8 +147,19 @@ impl InputEvent for MouseUpEvent {
         PlatformInput::MouseUp(self)
     }
 }
+
 impl MouseEvent for MouseUpEvent {}
 
+impl MouseUpEvent {
+    /// Returns true if this mouse up event should focus the element.
+    pub fn is_focusing(&self) -> bool {
+        match self.button {
+            MouseButton::Left => true,
+            _ => false,
+        }
+    }
+}
+
 /// A click event, generated when a mouse button is pressed and released.
 #[derive(Clone, Debug, Default)]
 pub struct MouseClickEvent {
@@ -482,6 +503,7 @@ impl InputEvent for MouseExitEvent {
         PlatformInput::MouseExited(self)
     }
 }
+
 impl MouseEvent for MouseExitEvent {}
 
 impl Deref for MouseExitEvent {

crates/gpui/src/window.rs 🔗

@@ -822,6 +822,12 @@ impl Frame {
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
+enum InputModality {
+    Mouse,
+    Keyboard,
+}
+
 /// Holds the state for a specific window.
 pub struct Window {
     pub(crate) handle: AnyWindowHandle,
@@ -870,7 +876,7 @@ pub struct Window {
     hovered: Rc<Cell<bool>>,
     pub(crate) needs_present: Rc<Cell<bool>>,
     pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
-    last_input_was_keyboard: bool,
+    last_input_modality: InputModality,
     pub(crate) refreshing: bool,
     pub(crate) activation_observers: SubscriberSet<(), AnyObserver>,
     pub(crate) focus: Option<FocusId>,
@@ -1254,7 +1260,7 @@ impl Window {
             hovered,
             needs_present,
             last_input_timestamp,
-            last_input_was_keyboard: false,
+            last_input_modality: InputModality::Mouse,
             refreshing: false,
             activation_observers: SubscriberSet::new(),
             focus: None,
@@ -1910,7 +1916,7 @@ impl Window {
     /// Returns true if the last input event was keyboard-based (key press, tab navigation, etc.)
     /// This is used for focus-visible styling to show focus indicators only for keyboard navigation.
     pub fn last_input_was_keyboard(&self) -> bool {
-        self.last_input_was_keyboard
+        self.last_input_modality == InputModality::Keyboard
     }
 
     /// The current state of the keyboard's capslock
@@ -3591,12 +3597,13 @@ impl Window {
         self.last_input_timestamp.set(Instant::now());
 
         // Track whether this input was keyboard-based for focus-visible styling
-        self.last_input_was_keyboard = matches!(
-            event,
-            PlatformInput::KeyDown(_)
-                | PlatformInput::KeyUp(_)
-                | PlatformInput::ModifiersChanged(_)
-        );
+        self.last_input_modality = match &event {
+            PlatformInput::KeyDown(_) | PlatformInput::ModifiersChanged(_) => {
+                InputModality::Keyboard
+            }
+            PlatformInput::MouseDown(e) if e.is_focusing() => InputModality::Mouse,
+            _ => self.last_input_modality,
+        };
 
         // Handlers may set this to false by calling `stop_propagation`.
         cx.propagate_event = true;