gpui: Press enter, space to trigger click to focused element (#35075)

Release Notes:

- N/A

> Any user interaction that is equivalent to a click, such as pressing
the Space key or Enter key while the element is focused. Note that this
only applies to elements with a default key event handler, and
therefore, excludes other elements that have been made focusable by
setting the
[tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex)
attribute.

https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event

---------

Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Umesh Yadav <23421535+imumesh18@users.noreply.github.com>
This commit is contained in:
Jason Lee 2025-08-06 06:15:30 +08:00 committed by GitHub
parent b7469f5bc3
commit 0025019db4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 231 additions and 63 deletions

View file

@ -111,8 +111,24 @@ impl Render for Example {
.flex_row()
.gap_3()
.items_center()
.child(button("el1").tab_index(4).child("Button 1"))
.child(button("el2").tab_index(5).child("Button 2")),
.child(
button("el1")
.tab_index(4)
.child("Button 1")
.on_click(cx.listener(|this, _, _, cx| {
this.message = "You have clicked Button 1.".into();
cx.notify();
})),
)
.child(
button("el2")
.tab_index(5)
.child("Button 2")
.on_click(cx.listener(|this, _, _, cx| {
this.message = "You have clicked Button 2.".into();
cx.notify();
})),
),
)
}
}

View file

@ -165,8 +165,8 @@ impl Render for WindowShadow {
},
)
.on_click(|e, window, _| {
if e.down.button == MouseButton::Right {
window.show_window_menu(e.up.position);
if e.is_right_click() {
window.show_window_menu(e.position());
}
})
.text_color(black())

View file

@ -19,10 +19,10 @@ use crate::{
Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase,
Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior,
HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent,
LayoutId, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px,
size,
KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton,
MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, ParentElement, Pixels,
Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task,
TooltipId, Visibility, Window, WindowControlArea, point, px, size,
};
use collections::HashMap;
use refineable::Refineable;
@ -484,10 +484,9 @@ impl Interactivity {
where
Self: Sized,
{
self.click_listeners
.push(Box::new(move |event, window, cx| {
listener(event, window, cx)
}));
self.click_listeners.push(Rc::new(move |event, window, cx| {
listener(event, window, cx)
}));
}
/// On drag initiation, this callback will be used to create a new view to render the dragged value for a
@ -1156,7 +1155,7 @@ pub(crate) type MouseMoveListener =
pub(crate) type ScrollWheelListener =
Box<dyn Fn(&ScrollWheelEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
pub(crate) type ClickListener = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
pub(crate) type ClickListener = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
pub(crate) type DragListener =
Box<dyn Fn(&dyn Any, Point<Pixels>, &mut Window, &mut App) -> AnyView + 'static>;
@ -1950,6 +1949,12 @@ impl Interactivity {
window: &mut Window,
cx: &mut App,
) {
let is_focused = self
.tracked_focus_handle
.as_ref()
.map(|handle| handle.is_focused(window))
.unwrap_or(false);
// If this element can be focused, register a mouse down listener
// that will automatically transfer focus when hitting the element.
// This behavior can be suppressed by using `cx.prevent_default()`.
@ -2113,6 +2118,39 @@ impl Interactivity {
}
});
if is_focused {
// Press enter, space to trigger click, when the element is focused.
window.on_key_event({
let click_listeners = click_listeners.clone();
let hitbox = hitbox.clone();
move |event: &KeyUpEvent, phase, window, cx| {
if phase.bubble() && !window.default_prevented() {
let stroke = &event.keystroke;
let keyboard_button = if stroke.key.eq("enter") {
Some(KeyboardButton::Enter)
} else if stroke.key.eq("space") {
Some(KeyboardButton::Space)
} else {
None
};
if let Some(button) = keyboard_button
&& !stroke.modifiers.modified()
{
let click_event = ClickEvent::Keyboard(KeyboardClickEvent {
button,
bounds: hitbox.bounds,
});
for listener in &click_listeners {
listener(&click_event, window, cx);
}
}
}
}
});
}
window.on_mouse_event({
let mut captured_mouse_down = None;
let hitbox = hitbox.clone();
@ -2138,10 +2176,10 @@ impl Interactivity {
// Fire click handlers during the bubble phase.
DispatchPhase::Bubble => {
if let Some(mouse_down) = captured_mouse_down.take() {
let mouse_click = ClickEvent {
let mouse_click = ClickEvent::Mouse(MouseClickEvent {
down: mouse_down,
up: event.clone(),
};
});
for listener in &click_listeners {
listener(&mouse_click, window, cx);
}

View file

@ -1,6 +1,6 @@
use crate::{
Capslock, Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, Window,
point, seal::Sealed,
Bounds, Capslock, Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render,
Window, point, seal::Sealed,
};
use smallvec::SmallVec;
use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf};
@ -141,7 +141,7 @@ impl MouseEvent for MouseUpEvent {}
/// A click event, generated when a mouse button is pressed and released.
#[derive(Clone, Debug, Default)]
pub struct ClickEvent {
pub struct MouseClickEvent {
/// The mouse event when the button was pressed.
pub down: MouseDownEvent,
@ -149,18 +149,119 @@ pub struct ClickEvent {
pub up: MouseUpEvent,
}
/// A click event that was generated by a keyboard button being pressed and released.
#[derive(Clone, Debug)]
pub struct KeyboardClickEvent {
/// The keyboard button that was pressed to trigger the click.
pub button: KeyboardButton,
/// The bounds of the element that was clicked.
pub bounds: Bounds<Pixels>,
}
/// A click event, generated when a mouse button or keyboard button is pressed and released.
#[derive(Clone, Debug)]
pub enum ClickEvent {
/// A click event trigger by a mouse button being pressed and released.
Mouse(MouseClickEvent),
/// A click event trigger by a keyboard button being pressed and released.
Keyboard(KeyboardClickEvent),
}
impl ClickEvent {
/// Returns the modifiers that were held down during both the
/// mouse down and mouse up events
/// Returns the modifiers that were held during the click event
///
/// `Keyboard`: The keyboard click events never have modifiers.
/// `Mouse`: Modifiers that were held during the mouse key up event.
pub fn modifiers(&self) -> Modifiers {
Modifiers {
control: self.up.modifiers.control && self.down.modifiers.control,
alt: self.up.modifiers.alt && self.down.modifiers.alt,
shift: self.up.modifiers.shift && self.down.modifiers.shift,
platform: self.up.modifiers.platform && self.down.modifiers.platform,
function: self.up.modifiers.function && self.down.modifiers.function,
match self {
// Click events are only generated from keyboard events _without any modifiers_, so we know the modifiers are always Default
ClickEvent::Keyboard(_) => Modifiers::default(),
// Click events on the web only reflect the modifiers for the keyup event,
// tested via observing the behavior of the `ClickEvent.shiftKey` field in Chrome 138
// under various combinations of modifiers and keyUp / keyDown events.
ClickEvent::Mouse(event) => event.up.modifiers,
}
}
/// Returns the position of the click event
///
/// `Keyboard`: The bottom left corner of the clicked hitbox
/// `Mouse`: The position of the mouse when the button was released.
pub fn position(&self) -> Point<Pixels> {
match self {
ClickEvent::Keyboard(event) => event.bounds.bottom_left(),
ClickEvent::Mouse(event) => event.up.position,
}
}
/// Returns the mouse position of the click event
///
/// `Keyboard`: None
/// `Mouse`: The position of the mouse when the button was released.
pub fn mouse_position(&self) -> Option<Point<Pixels>> {
match self {
ClickEvent::Keyboard(_) => None,
ClickEvent::Mouse(event) => Some(event.up.position),
}
}
/// Returns if this was a right click
///
/// `Keyboard`: false
/// `Mouse`: Whether the right button was pressed and released
pub fn is_right_click(&self) -> bool {
match self {
ClickEvent::Keyboard(_) => false,
ClickEvent::Mouse(event) => {
event.down.button == MouseButton::Right && event.up.button == MouseButton::Right
}
}
}
/// Returns whether the click was a standard click
///
/// `Keyboard`: Always true
/// `Mouse`: Left button pressed and released
pub fn standard_click(&self) -> bool {
match self {
ClickEvent::Keyboard(_) => true,
ClickEvent::Mouse(event) => {
event.down.button == MouseButton::Left && event.up.button == MouseButton::Left
}
}
}
/// Returns whether the click focused the element
///
/// `Keyboard`: false, keyboard clicks only work if an element is already focused
/// `Mouse`: Whether this was the first focusing click
pub fn first_focus(&self) -> bool {
match self {
ClickEvent::Keyboard(_) => false,
ClickEvent::Mouse(event) => event.down.first_mouse,
}
}
/// Returns the click count of the click event
///
/// `Keyboard`: Always 1
/// `Mouse`: Count of clicks from MouseUpEvent
pub fn click_count(&self) -> usize {
match self {
ClickEvent::Keyboard(_) => 1,
ClickEvent::Mouse(event) => event.up.click_count,
}
}
}
/// An enum representing the keyboard button that was pressed for a click event.
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub enum KeyboardButton {
/// Enter key was clicked
Enter,
/// Space key was clicked
Space,
}
/// An enum representing the mouse button that was pressed.

View file

@ -79,11 +79,13 @@ pub enum DispatchPhase {
impl DispatchPhase {
/// Returns true if this represents the "bubble" phase.
#[inline]
pub fn bubble(self) -> bool {
self == DispatchPhase::Bubble
}
/// Returns true if this represents the "capture" phase.
#[inline]
pub fn capture(self) -> bool {
self == DispatchPhase::Capture
}