macOS Big Sur: Mouse up events get lost while moving over a widget
This is a non-issue on Windows and macOS Catalina.
Steps to reproduce
- Create a GtkToggleButton with GDK_POINTER_MOTION_MASK
- Press the mouse on the button without releasing
- Release the mouse on the button while moving, without leaving the button's area. To repro it correctly, the motion needs to continue during / after the release. Naturally, when we perform a drag, we always stop moving the mouse on release. However, here, the motion needs to continue during / after the drag. It's surely counterintuitive, but it's easy to repro by simply clicking (pressing then releasing immediately) while moving fast.
Current behavior
The button doesn't get toggled. You can also repro it by using GtkPushButton and hooking a signal handler to "clicked"; the signal won't be emitted.
Expected outcome
The button gets toggled and signals get emitted properly.
Investigation
The following is an investigation on the GtkPushButton's "clicked" signal not being emitted. It should be similar for the GtkToggleButton, since the issue is unrelated to the controls themselves.
In gtkbutton.c, "clicked" is being emitted in gtk_button_do_release if emit_clicked = true. Some times, it is called with emit_clicked = false from places such as multipress_gesture_cancel_cb, which is the handler of the "cancel" signal of the button's GtkGesture. In gtkgesturesingle.c, this "cancel" signal is emitted by gtk_event_controller_reset in gtk_gesture_single_handle_event.
Minimal gtk_gesture_single_handle_event snippet:
guint button = 0;
switch (event->type)
{
case GDK_MOTION_NOTIFY:
if (!gtk_gesture_handles_sequence (GTK_GESTURE (controller), sequence))
return FALSE;
if (priv->touch_only && !test_touchscreen && source != GDK_SOURCE_TOUCHSCREEN)
return FALSE;
if (priv->current_button > 0 && priv->current_button <= 5 &&
(event->motion.state & (GDK_BUTTON1_MASK << (priv->current_button - 1))))
button = priv->current_button;
else if (priv->current_button == 0)
{
/* No current button, find out from the mask */
for (i = 0; i < 3; i++)
{
if ((event->motion.state & (GDK_BUTTON1_MASK << i)) == 0)
continue;
button = i + 1;
break;
}
}
break;
default:
return FALSE;
}
if (button == 0 ||
(priv->button != 0 && priv->button != button) ||
(priv->current_button != 0 && priv->current_button != button))
{
if (gtk_gesture_is_active (GTK_GESTURE (controller)))
gtk_event_controller_reset (controller);
return FALSE;
}
When this final code path is reached, button = 0 because none of the ifs' condition in GDK_MOTION_NOTIFY's case are true. More precisely, priv->button and priv->current_button = 1, but event->motion.state = 0 (doesn't match the others). motion.state should contain any pressed mouse button during the motion event, but on Big Sur, it's not always the case.
So, I've tried to find how motion.state was being set in fill_motion_event, which led to a function called _gdk_quartz_events_get_current_mouse_modifiers. It seemed pretty standard, yet OS-specific, but it meant the values provided by the OS itself were not always right.
I then stumbled across a very similar issue on Flutter's GitHub: https://github.com/flutter/flutter/issues/70529#issuecomment-733330469
"It appears that on high DPI screens in Big Sur, when a mouse up event happens, simultaneously a mouse move may also occur. Both happen in the same PointerEvent, with the move being the first datum in the list of pointer data. The button for both data is 0 (i.e. no button pressed). [...] This causes a situation where the move is rejected as there's no mouse button associated with it, the gesture is rejected and the up event is subsequently lost."
So, I tried a similar fix, by adding if (event->motion.state == 0) return FALSE;
in GDK_MOTION_NOTIFY's case, and it seems to work fine. However, I'm not aware of the regressions it could cause, since mouse move events without any button pressed might be a real code path in this gesture code outside of this very specific issue.
Version information
Version: GTK 3.24.28
OS: macOS Big Sur 11.2.3
This is a non-issue on Windows and macOS Catalina.