UI: Route node picker input through WebView

Picker input starts in each platform UI, while protocol state lives in
LibWebView. Route movement, primary clicks, Escape, and leave events
through the WebView picker API so each frontend drives the same hit-test
and picker-event path.

The selected click is consumed because picker mode uses it to choose an
element for DevTools rather than activate page content.

AppKit routes normal mouse input through WebViewBridge, which converts
view points to device pixels before forwarding to LibWeb. Picker input
bypasses that enqueue path, so apply the same scaling before requesting
a picker hit test.
This commit is contained in:
Sam Atkins 2026-05-28 12:18:49 +01:00
parent 3a332b23e5
commit e5e3099ce7
4 changed files with 189 additions and 0 deletions

View file

@ -51,6 +51,14 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
return static_cast<u64>([screen_number unsignedLongLongValue]);
}
static Web::DevicePixelPoint node_picker_position_for(Ladybird::WebViewBridge const& web_view_bridge, Web::DevicePixelPoint widget_position)
{
return {
widget_position.x().value() * web_view_bridge.device_pixel_ratio(),
widget_position.y().value() * web_view_bridge.device_pixel_ratio(),
};
}
@interface LadybirdWebViewContentLayer : CALayer
@end
@ -931,6 +939,11 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
if (!self.current_key_down_event)
return;
if (m_web_view_bridge->is_node_picker_active()) {
self.current_key_down_event = nil;
return;
}
auto key_event = Ladybird::ns_event_to_key_event(Web::KeyEvent::Type::KeyDown, self.current_key_down_event);
m_web_view_bridge->enqueue_input_event(move(key_event));
@ -1128,6 +1141,11 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
- (void)mouseExited:(NSEvent*)event
{
if (m_web_view_bridge->is_node_picker_active()) {
m_web_view_bridge->clear_node_picker();
return;
}
Web::MouseEvent mouse_event { Web::MouseEvent::Type::MouseLeave, {}, {}, Web::UIEvents::MouseButton::None, Web::UIEvents::MouseButton::None, Web::UIEvents::KeyModifier::Mod_None, 0, 0, 0, nullptr };
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
@ -1135,11 +1153,19 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
- (void)mouseMoved:(NSEvent*)event
{
auto mouse_event = Ladybird::ns_event_to_mouse_event(Web::MouseEvent::Type::MouseMove, event, self, Web::UIEvents::MouseButton::None);
if (m_web_view_bridge->is_node_picker_active()) {
m_web_view_bridge->node_picker_hover(node_picker_position_for(*m_web_view_bridge, mouse_event.position));
return;
}
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
- (void)scrollWheel:(NSEvent*)event
{
if (m_web_view_bridge->is_node_picker_active())
return;
auto mouse_event = Ladybird::ns_event_to_mouse_event(Web::MouseEvent::Type::MouseWheel, event, self, Web::UIEvents::MouseButton::Middle);
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
@ -1149,17 +1175,31 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
[[self window] makeFirstResponder:self];
auto mouse_event = Ladybird::ns_event_to_mouse_event(Web::MouseEvent::Type::MouseDown, event, self, Web::UIEvents::MouseButton::Primary);
if (m_web_view_bridge->is_node_picker_active()) {
if ((event.modifierFlags & NSEventModifierFlagCommand) != 0)
m_web_view_bridge->node_picker_preview(node_picker_position_for(*m_web_view_bridge, mouse_event.position));
else
m_web_view_bridge->node_picker_pick(node_picker_position_for(*m_web_view_bridge, mouse_event.position));
return;
}
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
- (void)mouseUp:(NSEvent*)event
{
if (m_web_view_bridge->is_node_picker_active())
return;
auto mouse_event = Ladybird::ns_event_to_mouse_event(Web::MouseEvent::Type::MouseUp, event, self, Web::UIEvents::MouseButton::Primary);
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
- (void)mouseDragged:(NSEvent*)event
{
if (m_web_view_bridge->is_node_picker_active())
return;
auto mouse_event = Ladybird::ns_event_to_mouse_event(Web::MouseEvent::Type::MouseMove, event, self, Web::UIEvents::MouseButton::Primary);
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
@ -1168,18 +1208,27 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
{
[[self window] makeFirstResponder:self];
if (m_web_view_bridge->is_node_picker_active())
return;
auto mouse_event = Ladybird::ns_event_to_mouse_event(Web::MouseEvent::Type::MouseDown, event, self, Web::UIEvents::MouseButton::Secondary);
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
- (void)rightMouseUp:(NSEvent*)event
{
if (m_web_view_bridge->is_node_picker_active())
return;
auto mouse_event = Ladybird::ns_event_to_mouse_event(Web::MouseEvent::Type::MouseUp, event, self, Web::UIEvents::MouseButton::Secondary);
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
- (void)rightMouseDragged:(NSEvent*)event
{
if (m_web_view_bridge->is_node_picker_active())
return;
auto mouse_event = Ladybird::ns_event_to_mouse_event(Web::MouseEvent::Type::MouseMove, event, self, Web::UIEvents::MouseButton::Secondary);
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
@ -1191,6 +1240,9 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
[[self window] makeFirstResponder:self];
if (m_web_view_bridge->is_node_picker_active())
return;
auto mouse_event = Ladybird::ns_event_to_mouse_event(Web::MouseEvent::Type::MouseDown, event, self, Web::UIEvents::MouseButton::Middle);
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
@ -1200,6 +1252,9 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
if (event.buttonNumber != 2)
return;
if (m_web_view_bridge->is_node_picker_active())
return;
auto mouse_event = Ladybird::ns_event_to_mouse_event(Web::MouseEvent::Type::MouseUp, event, self, Web::UIEvents::MouseButton::Middle);
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
@ -1209,6 +1264,9 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
if (event.buttonNumber != 2)
return;
if (m_web_view_bridge->is_node_picker_active())
return;
auto mouse_event = Ladybird::ns_event_to_mouse_event(Web::MouseEvent::Type::MouseMove, event, self, Web::UIEvents::MouseButton::Middle);
m_web_view_bridge->enqueue_input_event(move(mouse_event));
}
@ -1235,6 +1293,13 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
return;
}
if (m_web_view_bridge->is_node_picker_active()) {
auto key_event = Ladybird::ns_event_to_key_event(Web::KeyEvent::Type::KeyDown, event);
if (key_event.key == Web::UIEvents::KeyCode::Key_Escape)
m_web_view_bridge->node_picker_cancel();
return;
}
self.current_key_down_event = event;
[self interpretKeyEvents:@[ event ]];
}
@ -1245,6 +1310,9 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
return;
}
if (m_web_view_bridge->is_node_picker_active())
return;
auto key_event = Ladybird::ns_event_to_key_event(Web::KeyEvent::Type::KeyUp, event);
m_web_view_bridge->enqueue_input_event(move(key_event));
}
@ -1255,6 +1323,11 @@ static Optional<u64> display_id_for_screen(NSScreen* screen)
return;
}
if (m_web_view_bridge->is_node_picker_active()) {
m_modifier_flags = event.modifierFlags;
return;
}
auto enqueue_event_if_needed = [&](auto flag) {
auto is_flag_set = [&](auto flags) { return (flags & flag) != 0; };
Web::KeyEvent::Type type;

View file

@ -87,6 +87,12 @@ static gboolean on_key_pressed(GtkEventControllerKey*, guint keyval, guint, GdkM
if (!self->impl)
return GDK_EVENT_PROPAGATE;
if (self->impl->is_node_picker_active()) {
if (keyval == GDK_KEY_Escape)
self->impl->node_picker_cancel();
return GDK_EVENT_STOP;
}
self->impl->enqueue_native_event(Web::KeyEvent::Type::KeyDown, keyval, state);
return GDK_EVENT_STOP;
}
@ -97,6 +103,9 @@ static void on_key_released(GtkEventControllerKey*, guint keyval, guint, GdkModi
if (!self->impl)
return;
if (self->impl->is_node_picker_active())
return;
self->impl->enqueue_native_event(Web::KeyEvent::Type::KeyUp, keyval, state);
}
@ -110,6 +119,18 @@ static void on_mouse_pressed(GtkGestureClick* gesture, gint n_press, gdouble x,
auto button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
auto state = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(gesture));
if (self->impl->is_node_picker_active()) {
if (button == GDK_BUTTON_PRIMARY) {
auto position = Web::DevicePixelPoint { static_cast<int>(x * self->impl->device_pixel_ratio()), static_cast<int>(y * self->impl->device_pixel_ratio()) };
if (state & GDK_CONTROL_MASK)
self->impl->node_picker_preview(position);
else
self->impl->node_picker_pick(position);
}
gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_CLAIMED);
return;
}
self->impl->enqueue_native_event(Web::MouseEvent::Type::MouseDown, x, y, button, state, n_press);
}
@ -119,6 +140,11 @@ static void on_mouse_released(GtkGestureClick* gesture, gint n_press, gdouble x,
if (!self->impl)
return;
if (self->impl->is_node_picker_active()) {
gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_CLAIMED);
return;
}
auto button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
auto state = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(gesture));
self->impl->enqueue_native_event(Web::MouseEvent::Type::MouseUp, x, y, button, state, n_press);
@ -132,6 +158,12 @@ static void on_mouse_motion(GtkEventControllerMotion* controller, gdouble x, gdo
if (!self->impl)
return;
if (self->impl->is_node_picker_active()) {
auto position = Web::DevicePixelPoint { static_cast<int>(x * self->impl->device_pixel_ratio()), static_cast<int>(y * self->impl->device_pixel_ratio()) };
self->impl->node_picker_hover(position);
return;
}
auto state = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(controller));
self->impl->enqueue_native_event(Web::MouseEvent::Type::MouseMove, x, y, 0, state, 0);
}
@ -142,6 +174,11 @@ static void on_mouse_leave(GtkEventControllerMotion*, gpointer user_data)
if (!self->impl)
return;
if (self->impl->is_node_picker_active()) {
self->impl->clear_node_picker();
return;
}
self->impl->enqueue_native_event(Web::MouseEvent::Type::MouseLeave, 0, 0, 0, static_cast<GdkModifierType>(0), 0);
}
@ -151,6 +188,9 @@ static gboolean on_scroll(GtkEventControllerScroll* controller, gdouble dx, gdou
if (!self->impl)
return GDK_EVENT_PROPAGATE;
if (self->impl->is_node_picker_active())
return GDK_EVENT_STOP;
auto state = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(controller));
// Ctrl+scroll = zoom

View file

@ -420,16 +420,33 @@ static bool is_browser_reserved_shortcut(QKeyEvent const& event)
void WebContentView::keyPressEvent(QKeyEvent* event)
{
if (is_node_picker_active()) {
if (event->key() == Qt::Key_Escape)
node_picker_cancel();
event->accept();
return;
}
enqueue_native_event(Web::KeyEvent::Type::KeyDown, *event);
}
void WebContentView::keyReleaseEvent(QKeyEvent* event)
{
if (is_node_picker_active()) {
event->accept();
return;
}
enqueue_native_event(Web::KeyEvent::Type::KeyUp, *event);
}
void WebContentView::inputMethodEvent(QInputMethodEvent* event)
{
if (is_node_picker_active()) {
event->accept();
return;
}
if (!event->commitString().isEmpty()) {
QKeyEvent keyEvent(QEvent::KeyPress, 0, Qt::NoModifier, event->commitString());
keyPressEvent(&keyEvent);
@ -444,6 +461,12 @@ QVariant WebContentView::inputMethodQuery(Qt::InputMethodQuery) const
void WebContentView::leaveEvent(QEvent* event)
{
if (is_node_picker_active()) {
clear_node_picker();
QWidget::leaveEvent(event);
return;
}
static QMouseEvent mouse_event { QEvent::Type::Leave, {}, {}, Qt::MouseButton::NoButton, Qt::MouseButton::NoButton, Qt::KeyboardModifier::NoModifier };
enqueue_native_event(Web::MouseEvent::Type::MouseLeave, mouse_event);
@ -452,6 +475,12 @@ void WebContentView::leaveEvent(QEvent* event)
void WebContentView::mouseMoveEvent(QMouseEvent* event)
{
if (is_node_picker_active()) {
node_picker_hover(node_picker_position_for(*event));
event->accept();
return;
}
if (!m_tooltip_override) {
if (QToolTip::isVisible())
QToolTip::hideText();
@ -464,6 +493,18 @@ void WebContentView::mouseMoveEvent(QMouseEvent* event)
void WebContentView::mousePressEvent(QMouseEvent* event)
{
if (is_node_picker_active()) {
if (event->button() == Qt::MouseButton::LeftButton) {
auto position = node_picker_position_for(*event);
if (event->modifiers().testFlag(Qt::ControlModifier))
node_picker_preview(position);
else
node_picker_pick(position);
}
event->accept();
return;
}
auto elapsed = event->timestamp() - m_last_click_timestamp;
auto distance = (event->position() - m_last_click_position).manhattanLength();
@ -482,6 +523,11 @@ void WebContentView::mousePressEvent(QMouseEvent* event)
void WebContentView::mouseReleaseEvent(QMouseEvent* event)
{
if (is_node_picker_active()) {
event->accept();
return;
}
enqueue_native_event(Web::MouseEvent::Type::MouseUp, *event);
if (event->button() == Qt::MouseButton::BackButton)
@ -492,6 +538,11 @@ void WebContentView::mouseReleaseEvent(QMouseEvent* event)
void WebContentView::wheelEvent(QWheelEvent* event)
{
if (is_node_picker_active()) {
event->accept();
return;
}
if (event->modifiers().testFlag(Qt::ControlModifier)) {
event->ignore();
return;
@ -509,6 +560,11 @@ void WebContentView::mouseDoubleClickEvent(QMouseEvent* event)
void WebContentView::dragEnterEvent(QDragEnterEvent* event)
{
if (is_node_picker_active()) {
event->ignore();
return;
}
if (!event->mimeData()->hasUrls())
return;
@ -518,12 +574,20 @@ void WebContentView::dragEnterEvent(QDragEnterEvent* event)
void WebContentView::dragMoveEvent(QDragMoveEvent* event)
{
if (is_node_picker_active()) {
event->ignore();
return;
}
enqueue_native_event(Web::DragEvent::Type::DragMove, *event);
event->acceptProposedAction();
}
void WebContentView::dragLeaveEvent(QDragLeaveEvent*)
{
if (is_node_picker_active())
return;
// QDragLeaveEvent does not contain any mouse position or button information.
Web::DragEvent event {};
event.type = Web::DragEvent::Type::DragEnd;
@ -533,6 +597,11 @@ void WebContentView::dragLeaveEvent(QDragLeaveEvent*)
void WebContentView::dropEvent(QDropEvent* event)
{
if (is_node_picker_active()) {
event->ignore();
return;
}
enqueue_native_event(Web::DragEvent::Type::Drop, *event);
event->acceptProposedAction();
}
@ -813,6 +882,11 @@ void WebContentView::update_cursor(Gfx::Cursor cursor)
});
}
Web::DevicePixelPoint WebContentView::node_picker_position_for(QSinglePointEvent const& event) const
{
return { event.position().x() * m_device_pixel_ratio, event.position().y() * m_device_pixel_ratio };
}
Web::DevicePixelSize WebContentView::viewport_size() const
{
return m_viewport_size.to_type<Web::DevicePixels>();

View file

@ -97,6 +97,8 @@ private:
void update_cursor(Gfx::Cursor cursor);
void update_compositor_display_metadata();
Web::DevicePixelPoint node_picker_position_for(QSinglePointEvent const&) const;
void enqueue_native_event(Web::MouseEvent::Type, QSinglePointEvent const& event);
void enqueue_native_event(Web::DragEvent::Type, QDropEvent const& event);