UV editor: keep the mouse capture balanced

The canvas captured the mouse on every button press without checking whether
it already held one, released it in a single place, and handled no capture
loss at all. Two sequences leaked a capture: pressing a second button during a
drag nested a second one that the single release on button-up could not undo,
and a modal R/S skipped that release entirely while waiting for a confirming
click that may never come.

A leaked capture is not a local problem on macOS, where wxEVT_MOUSE_CAPTURE_LOST
is never sent and nothing recovers it. While any wx window holds a capture,
wxOSX routes every mouse event to that window and never calls through to
NSWindow, so the application stops seeing motion and enter/leave, and native
tooltips stop appearing anywhere in it.

Capture through grab_mouse()/drop_mouse() so there is at most one, give it back
on any button-up including a modal gesture (which tracks the pointer and needs
no capture), and cancel on wxEVT_MOUSE_CAPTURE_LOST: commit nothing, put back
what a modal rotate or scale already applied, and do not release a capture that
is already gone.
This commit is contained in:
ExPikaPaka
2026-10-08 09:04:01 +02:00
parent 97b0d2c879
commit 19c1d633cc
2 changed files with 57 additions and 3 deletions
+45 -3
View File
@@ -176,6 +176,7 @@ UVEditorCanvas::UVEditorCanvas(wxWindow *parent)
Bind(wxEVT_MIDDLE_DOWN, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_MIDDLE_UP, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_MOTION, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, &UVEditorCanvas::on_capture_lost, this);
Bind(wxEVT_MOUSEWHEEL, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_LEAVE_WINDOW, &UVEditorCanvas::on_leave, this);
Bind(wxEVT_KEY_DOWN, &UVEditorCanvas::on_key, this);
@@ -727,10 +728,47 @@ void UVEditorCanvas::end_gesture()
m_rot_raw_deg = 0.f;
m_rot_applied_deg = 0.f;
m_modal_scale_accum = 1.f;
drop_mouse();
}
void UVEditorCanvas::cancel_gesture()
{
// Undo what the gesture already applied live, as the Esc path does, and commit nothing.
if (m_on_island_edit) {
if (m_gesture == Gesture::RotateIslandModal && m_rot_applied_deg != 0.f)
m_on_island_edit(m_selected_island, Vec2f::Zero(), -m_rot_applied_deg, 1.f, false);
if (m_gesture == Gesture::ScaleIslandModal && m_modal_scale_accum != 1.f)
m_on_island_edit(m_selected_island, Vec2f::Zero(), 0.f, 1.f / m_modal_scale_accum, false);
}
m_gesture = Gesture::None;
m_rot_raw_deg = 0.f;
m_rot_applied_deg = 0.f;
m_modal_scale_accum = 1.f;
m_vertex_edit_moved = false;
}
void UVEditorCanvas::grab_mouse()
{
if (!HasCapture())
CaptureMouse();
}
void UVEditorCanvas::drop_mouse()
{
if (HasCapture())
ReleaseMouse();
}
// The capture was taken from us (a dialog opened, another application grabbed the pointer). wx
// requires this to cancel the gesture: no commit, no Skip(), and no ReleaseMouse() - the capture is
// already gone, and releasing it again would unbalance the stack.
void UVEditorCanvas::on_capture_lost(wxMouseCaptureLostEvent &)
{
cancel_gesture();
Refresh();
}
void UVEditorCanvas::on_key(wxKeyEvent &evt)
{
const int key = evt.GetKeyCode();
@@ -909,7 +947,7 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
}
m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan;
}
CaptureMouse();
grab_mouse();
Refresh();
} else if (type == wxEVT_RIGHT_DOWN && m_selected_island >= 0 && m_select_mode == SelectMode::Island) {
const Vec2f rel = screen_to_uv(pos) - island_centroid(m_selected_island);
@@ -919,12 +957,16 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
m_rot_base_deg = island_rotation_deg(m_selected_island);
m_rot_display_deg = m_rot_base_deg;
m_gesture_last_angle = std::atan2(rel.y(), rel.x());
CaptureMouse();
grab_mouse();
} else if (type == wxEVT_MIDDLE_DOWN) {
m_gesture = Gesture::Pan;
m_drag_last_px = pos;
CaptureMouse();
grab_mouse();
} else if (type == wxEVT_LEFT_UP || type == wxEVT_RIGHT_UP || type == wxEVT_MIDDLE_UP) {
// The drag is over either way. A modal R/S keeps running until a click confirms it, but it
// tracks the pointer over this canvas and needs no capture to do so, so the capture goes back
// here rather than waiting for that click - which may never come.
drop_mouse();
if (m_gesture != Gesture::RotateIslandModal && m_gesture != Gesture::ScaleIslandModal) {
end_gesture();
Refresh();
+12
View File
@@ -10,6 +10,7 @@
// pulls in the platform's real GL/gl.h, and glad/gl.h errors out if that happens first (it wants
// to be the one to define the standard include guards GL/gl.h itself defines).
#include <glad/gl.h>
#include <wx/event.h>
#include <wx/glcanvas.h>
#include <wx/panel.h>
#include <wx/button.h>
@@ -194,6 +195,7 @@ private:
void on_paint(wxPaintEvent &evt);
void on_size(wxSizeEvent &evt);
void on_mouse(wxMouseEvent &evt);
void on_capture_lost(wxMouseCaptureLostEvent &evt);
void on_key(wxKeyEvent &evt);
void on_leave(wxMouseEvent &evt); // drops the +/- cursor hint when the pointer leaves the canvas
void on_erase_background(wxEraseEvent &evt) {} // required to avoid flicker on MSW, deliberately a no-op
@@ -238,6 +240,16 @@ private:
// vertex of some *other* island, in texture-UV space. Zero if nothing is within reach (#2).
Vec2f snap_correction(int island) const;
void end_gesture();
// Cancels the gesture instead of finishing it: nothing is committed, and a modal rotate/scale is
// put back the way Esc puts it back. Used when the capture is taken away from us.
void cancel_gesture();
// One capture at a time, released exactly once. Pressing a second button mid-drag would otherwise
// nest a second capture that the single release on button-up cannot undo, and macOS never sends
// wxEVT_MOUSE_CAPTURE_LOST to recover from that. A leaked capture is not a local problem there:
// while any wx window holds one, wxOSX routes every mouse event to it and the application stops
// seeing enter/leave and motion entirely, which also takes its tooltips down.
void grab_mouse();
void drop_mouse();
// Rebuilds the status line from the current gesture/selection and pushes it to m_on_status.
void update_status();
// Re-picks what a click at `pos` would grab in the current select mode, and repaints when that changed.