Added glfwDragWindow function to all platforms to begin a system window drag operation

This commit is contained in:
yokljo 2026-04-21 17:38:53 +10:00
parent d0be6518f5
commit 198614840f
18 changed files with 266 additions and 31 deletions

View file

@ -3998,6 +3998,33 @@ GLFWAPI void glfwHideWindow(GLFWwindow* window);
*/
GLFWAPI void glfwFocusWindow(GLFWwindow* window);
/*! @brief Hand off an in-progress pointer drag to the window manager so it
* moves this window until the user releases the pointer button.
*
* This is the equivalent of the standard custom-titlebar drag pattern, but
* initiated explicitly rather than from a titlebar hit test. It's primarily
* useful when a window is created mid-drag (e.g. tearing out a docked tab
* into a new viewport) and should seamlessly follow the cursor as if the
* initial button press had been on this new window.
*
* Requires a pointer button to currently be held. On Wayland this issues
* xdg_toplevel.move (or libdecor_frame_move) using the latest user-event
* serial. On Win32 it issues ReleaseCapture + WM_SYSCOMMAND SC_MOVE. On
* X11 it sends a _NET_WM_MOVERESIZE ClientMessage. All of these use the
* cursor's current location to anchor the drag, so no position needs to
* be passed in.
*
* @param[in] window The window to move.
*
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref
* GLFW_PLATFORM_ERROR.
*
* @thread_safety This function must only be called from the main thread.
*
* @ingroup window
*/
GLFWAPI void glfwDragWindow(GLFWwindow* window);
/*! @brief Requests user attention to the specified window.
*
* This function requests user attention to the specified window. On

View file

@ -507,6 +507,7 @@ GLFWbool _glfwConnectCocoa(int platformID, _GLFWplatform* platform)
.hideWindow = _glfwHideWindowCocoa,
.requestWindowAttention = _glfwRequestWindowAttentionCocoa,
.focusWindow = _glfwFocusWindowCocoa,
.dragWindow = _glfwDragWindowCocoa,
.setWindowMonitor = _glfwSetWindowMonitorCocoa,
.windowFocused = _glfwWindowFocusedCocoa,
.windowIconified = _glfwWindowIconifiedCocoa,

View file

@ -234,6 +234,7 @@ void _glfwShowWindowCocoa(_GLFWwindow* window);
void _glfwHideWindowCocoa(_GLFWwindow* window);
void _glfwRequestWindowAttentionCocoa(_GLFWwindow* window);
void _glfwFocusWindowCocoa(_GLFWwindow* window);
void _glfwDragWindowCocoa(_GLFWwindow* window);
void _glfwSetWindowMonitorCocoa(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate);
GLFWbool _glfwWindowFocusedCocoa(_GLFWwindow* window);
GLFWbool _glfwWindowIconifiedCocoa(_GLFWwindow* window);

View file

@ -1240,6 +1240,22 @@ void _glfwFocusWindowCocoa(_GLFWwindow* window)
} // autoreleasepool
}
void _glfwDragWindowCocoa(_GLFWwindow* window)
{
@autoreleasepool {
// -[NSWindow performWindowDragWithEvent:] requires a currently-dispatching
// mouse-down NSEvent. We don't have easy access to the original event here,
// so fall back to the closest available current event. If there isn't a
// live drag event, this is a no-op — matching Wayland's "no serial" gate.
NSEvent* event = [NSApp currentEvent];
if (event && ([event type] == NSEventTypeLeftMouseDown ||
[event type] == NSEventTypeLeftMouseDragged))
{
[window->ns.object performWindowDragWithEvent:event];
}
} // autoreleasepool
}
void _glfwSetWindowMonitorCocoa(_GLFWwindow* window,
_GLFWmonitor* monitor,
int xpos, int ypos,

View file

@ -737,6 +737,7 @@ struct _GLFWplatform
void (*hideWindow)(_GLFWwindow*);
void (*requestWindowAttention)(_GLFWwindow*);
void (*focusWindow)(_GLFWwindow*);
void (*dragWindow)(_GLFWwindow*);
void (*setWindowMonitor)(_GLFWwindow*,_GLFWmonitor*,int,int,int,int,int);
GLFWbool (*windowFocused)(_GLFWwindow*);
GLFWbool (*windowIconified)(_GLFWwindow*);

View file

@ -88,6 +88,7 @@ GLFWbool _glfwConnectNull(int platformID, _GLFWplatform* platform)
.hideWindow = _glfwHideWindowNull,
.requestWindowAttention = _glfwRequestWindowAttentionNull,
.focusWindow = _glfwFocusWindowNull,
.dragWindow = _glfwDragWindowNull,
.setWindowMonitor = _glfwSetWindowMonitorNull,
.windowFocused = _glfwWindowFocusedNull,
.windowIconified = _glfwWindowIconifiedNull,

View file

@ -252,6 +252,7 @@ void _glfwShowWindowNull(_GLFWwindow* window);
void _glfwRequestWindowAttentionNull(_GLFWwindow* window);
void _glfwHideWindowNull(_GLFWwindow* window);
void _glfwFocusWindowNull(_GLFWwindow* window);
void _glfwDragWindowNull(_GLFWwindow* window);
GLFWbool _glfwWindowFocusedNull(_GLFWwindow* window);
GLFWbool _glfwWindowIconifiedNull(_GLFWwindow* window);
GLFWbool _glfwWindowVisibleNull(_GLFWwindow* window);

View file

@ -474,6 +474,11 @@ void _glfwFocusWindowNull(_GLFWwindow* window)
_glfwInputWindowFocus(window, GLFW_TRUE);
}
void _glfwDragWindowNull(_GLFWwindow* window)
{
// No windowing system to hand the drag off to.
}
GLFWbool _glfwWindowFocusedNull(_GLFWwindow* window)
{
return _glfw.null.focusedWindow == window;

View file

@ -625,6 +625,7 @@ GLFWbool _glfwConnectWin32(int platformID, _GLFWplatform* platform)
.hideWindow = _glfwHideWindowWin32,
.requestWindowAttention = _glfwRequestWindowAttentionWin32,
.focusWindow = _glfwFocusWindowWin32,
.dragWindow = _glfwDragWindowWin32,
.setWindowMonitor = _glfwSetWindowMonitorWin32,
.windowFocused = _glfwWindowFocusedWin32,
.windowIconified = _glfwWindowIconifiedWin32,

View file

@ -502,6 +502,7 @@ void _glfwShowWindowWin32(_GLFWwindow* window);
void _glfwHideWindowWin32(_GLFWwindow* window);
void _glfwRequestWindowAttentionWin32(_GLFWwindow* window);
void _glfwFocusWindowWin32(_GLFWwindow* window);
void _glfwDragWindowWin32(_GLFWwindow* window);
void _glfwSetWindowMonitorWin32(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate);
GLFWbool _glfwWindowFocusedWin32(_GLFWwindow* window);
GLFWbool _glfwWindowIconifiedWin32(_GLFWwindow* window);

View file

@ -1972,6 +1972,16 @@ void _glfwFocusWindowWin32(_GLFWwindow* window)
SetFocus(window->win32.handle);
}
void _glfwDragWindowWin32(_GLFWwindow* window)
{
// Hand the in-progress pointer drag off to the window manager. Windows
// uses the cursor's current location to anchor the move, same as
// xdg_toplevel.move on Wayland. Standard pattern for custom titlebar
// drag (Chrome, Electron, anything with a non-native caption).
ReleaseCapture();
SendMessageW(window->win32.handle, WM_SYSCOMMAND, SC_MOVE | HTCAPTION, 0);
}
void _glfwSetWindowMonitorWin32(_GLFWwindow* window,
_GLFWmonitor* monitor,
int xpos, int ypos,

View file

@ -888,6 +888,17 @@ GLFWAPI void glfwFocusWindow(GLFWwindow* handle)
_glfw.platform.focusWindow(window);
}
GLFWAPI void glfwDragWindow(GLFWwindow* handle)
{
_GLFW_REQUIRE_INIT();
_GLFWwindow* window = (_GLFWwindow*) handle;
assert(window != NULL);
if (_glfw.platform.dragWindow)
_glfw.platform.dragWindow(window);
}
GLFWAPI int glfwGetWindowAttrib(GLFWwindow* handle, int attrib)
{
_GLFW_REQUIRE_INIT_OR_RETURN(0);

View file

@ -503,6 +503,7 @@ GLFWbool _glfwConnectWayland(int platformID, _GLFWplatform* platform)
.hideWindow = _glfwHideWindowWayland,
.requestWindowAttention = _glfwRequestWindowAttentionWayland,
.focusWindow = _glfwFocusWindowWayland,
.dragWindow = _glfwDragWindowWayland,
.setWindowMonitor = _glfwSetWindowMonitorWayland,
.windowFocused = _glfwWindowFocusedWayland,
.windowIconified = _glfwWindowIconifiedWayland,

View file

@ -409,12 +409,17 @@ typedef struct _GLFWwindowWayland
int pendingPosX, pendingPosY;
GLFWbool pendingPosSet;
// Captured from wndconfig.focused at create time. ImGui's GLFW backend
// unconditionally calls glfwWindowHint(GLFW_FOCUSED, false) for viewports;
// app-created toplevels (splash, main editor) leave the default `true`.
// That gives us a reliable "this is an ImGui viewport" signal that doesn't
// depend on version-gated hints like GLFW_FOCUS_ON_SHOW.
GLFWbool createdUnfocused;
// Set on the first wl_surface.frame fire, which only happens after the
// compositor has processed a real buffer commit (Vulkan's first present).
// xdg_toplevel.move against an unmapped surface has been observed to
// crash KWin, so we defer the request until this is true.
GLFWbool mapped;
struct wl_callback* mappedCallback;
// If glfwDragWindow was called before the surface was mapped, the press
// serial is stashed here and the move is issued from the map callback.
GLFWbool dragPending;
uint32_t dragPendingSerial;
struct {
struct libdecor_frame* frame;
@ -497,6 +502,16 @@ typedef struct _GLFWlibraryWayland
int cursorTimerfd;
uint32_t serial;
uint32_t pointerEnterSerial;
// Most-recently-received wl_pointer.button press serial. xdg_toplevel.move
// and xdg_popup.grab both require a serial tied to a user press event;
// _glfw.wl.serial is the latest serial of *any* kind (motion, enter,
// keyboard), which is invalid for those requests and can crash strict
// compositors (KWin has been observed to crash).
uint32_t pointerButtonSerial;
// Count of currently-held pointer buttons. Deferred drag-moves only fire
// while this is non-zero; otherwise the press serial is stale and passing
// it to xdg_toplevel.move is undefined per spec.
int pointerButtonsDown;
int keyRepeatTimerfd;
int32_t keyRepeatRate;
@ -696,6 +711,7 @@ void _glfwShowWindowWayland(_GLFWwindow* window);
void _glfwHideWindowWayland(_GLFWwindow* window);
void _glfwRequestWindowAttentionWayland(_GLFWwindow* window);
void _glfwFocusWindowWayland(_GLFWwindow* window);
void _glfwDragWindowWayland(_GLFWwindow* window);
void _glfwSetWindowMonitorWayland(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate);
GLFWbool _glfwWindowFocusedWayland(_GLFWwindow* window);
GLFWbool _glfwWindowIconifiedWayland(_GLFWwindow* window);

View file

@ -1391,6 +1391,14 @@ static GLFWbool createXdgPopupShellObjects(_GLFWwindow* window,
}
xdg_popup_add_listener(window->wl.xdg.popup, &xdgPopupListener, window);
// Request a grab tied to the most recent pointer-button press. This is
// what makes the compositor auto-dismiss the popup (sending popup_done)
// on any outside click, app switch, etc. — matching native menu behavior.
// Must use a press serial; any other kind is undefined per the spec and
// strict compositors may reject or crash.
if (_glfw.wl.seat && _glfw.wl.pointerButtonSerial)
xdg_popup_grab(window->wl.xdg.popup, _glfw.wl.seat, _glfw.wl.pointerButtonSerial);
wl_surface_commit(window->wl.surface);
wl_display_roundtrip(_glfw.wl.display);
return GLFW_TRUE;
@ -1468,28 +1476,84 @@ static GLFWbool createXdgShellObjects(_GLFWwindow* window)
return GLFW_TRUE;
}
static void issuePendingDragMove(_GLFWwindow* window)
{
if (!window->wl.dragPending || !_glfw.wl.seat)
return;
window->wl.dragPending = GLFW_FALSE;
// If the user released the button while we were waiting for map, the
// stashed serial is stale — passing it to move is undefined per spec.
if (_glfw.wl.pointerButtonsDown <= 0)
return;
struct xdg_toplevel* toplevel = window->wl.xdg.toplevel;
if (!toplevel && window->wl.libdecor.frame)
toplevel = libdecor_frame_get_xdg_toplevel(window->wl.libdecor.frame);
if (!toplevel)
return;
xdg_toplevel_move(toplevel, _glfw.wl.seat, window->wl.dragPendingSerial);
}
static void mappedFrameHandleDone(void* userData,
struct wl_callback* callback,
uint32_t data)
{
_GLFWwindow* window = userData;
wl_callback_destroy(callback);
window->wl.mappedCallback = NULL;
window->wl.mapped = GLFW_TRUE;
// A pending drag request queued before mapping fires now; the compositor
// has processed at least one buffer so xdg_toplevel.move is safe.
issuePendingDragMove(window);
}
static const struct wl_callback_listener mappedFrameListener =
{
mappedFrameHandleDone
};
static void armMappedFrameCallback(_GLFWwindow* window)
{
if (window->wl.mapped || window->wl.mappedCallback)
return;
window->wl.mappedCallback = wl_surface_frame(window->wl.surface);
if (window->wl.mappedCallback)
{
wl_callback_add_listener(window->wl.mappedCallback,
&mappedFrameListener, window);
}
}
static GLFWbool createShellObjects(_GLFWwindow* window)
{
// Treat ImGui-style transient viewports as xdg_popups so they appear
// relative to the main window on Wayland (toplevel positioning is
// protocol-forbidden). The signal is `createdUnfocused` — ImGui's GLFW
// backend unconditionally sets GLFW_FOCUSED=false for viewport windows,
// whereas app-created toplevels (splash, main editor) leave the default
// `true`, so they stay as real toplevels.
if (window->wl.createdUnfocused && !window->decorated && !window->monitor)
// Opt into xdg_popup for windows the caller marked as menu-style via
// GLFW_FOCUS_ON_SHOW=false. Undecorated alone isn't enough — detached
// dockable viewports are also undecorated but should remain real
// toplevels so they can be moved by the compositor (xdg_toplevel.move)
// and participate in normal window stacking.
if (!window->focusOnShow && !window->decorated && !window->monitor)
{
_GLFWwindow* parent = findWaylandPopupParent(window);
if (parent)
return createXdgPopupShellObjects(window, parent);
}
if (_glfw.wl.libdecor.context)
{
if (createLibdecorFrame(window))
return GLFW_TRUE;
}
GLFWbool ok;
if (_glfw.wl.libdecor.context && createLibdecorFrame(window))
ok = GLFW_TRUE;
else
ok = createXdgShellObjects(window);
return createXdgShellObjects(window);
// Register a one-shot frame callback so we learn when the compositor has
// first processed a buffer — our "truly mapped" signal. Any deferred
// drag-move waits for this before firing.
if (ok)
armMappedFrameCallback(window);
return ok;
}
static void destroyShellObjects(_GLFWwindow* window)
@ -1511,12 +1575,18 @@ static void destroyShellObjects(_GLFWwindow* window)
if (window->wl.xdg.surface)
xdg_surface_destroy(window->wl.xdg.surface);
if (window->wl.mappedCallback)
wl_callback_destroy(window->wl.mappedCallback);
window->wl.libdecor.frame = NULL;
window->wl.xdg.decoration = NULL;
window->wl.xdg.decorationMode = 0;
window->wl.xdg.popup = NULL;
window->wl.xdg.toplevel = NULL;
window->wl.xdg.surface = NULL;
window->wl.mappedCallback = NULL;
window->wl.mapped = GLFW_FALSE;
window->wl.dragPending = GLFW_FALSE;
}
static GLFWbool createNativeSurface(_GLFWwindow* window,
@ -2046,6 +2116,15 @@ static void pointerHandleButton(void* userData,
return;
_glfw.wl.serial = serial;
if (state == WL_POINTER_BUTTON_STATE_PRESSED)
{
_glfw.wl.pointerButtonSerial = serial;
_glfw.wl.pointerButtonsDown++;
}
else if (_glfw.wl.pointerButtonsDown > 0)
{
_glfw.wl.pointerButtonsDown--;
}
const int button = buttonID - BTN_LEFT;
const int action = (state == WL_POINTER_BUTTON_STATE_PRESSED);
@ -2747,8 +2826,6 @@ GLFWbool _glfwCreateWindowWayland(_GLFWwindow* window,
const _GLFWctxconfig* ctxconfig,
const _GLFWfbconfig* fbconfig)
{
window->wl.createdUnfocused = !wndconfig->focused;
if (!createNativeSurface(window, wndconfig, fbconfig))
return GLFW_FALSE;
@ -2910,16 +2987,18 @@ void _glfwGetWindowPosWayland(_GLFWwindow* window, int* xpos, int* ypos)
void _glfwSetWindowPosWayland(_GLFWwindow* window, int xpos, int ypos)
{
if (!window->wl.xdg.surface && !window->wl.libdecor.frame)
{
window->wl.pendingPosX = xpos;
window->wl.pendingPosY = ypos;
window->wl.pendingPosSet = GLFW_TRUE;
return;
}
_glfwInputError(GLFW_FEATURE_UNAVAILABLE,
"Wayland: The platform does not support setting the window position after it has been mapped");
// Wayland has no global coordinate space, so we can't physically move a
// mapped toplevel. But we still mirror whatever the caller writes:
// ImGui's multi-viewport input-forwarding adds glfwGetWindowPos to each
// per-surface pointer coord to produce absolute coordinates, and its
// hit-testing assumes viewport->Pos matches that same value. The two
// drift together harmlessly as long as we always mirror the write;
// making one stable while the other isn't breaks widget clicks.
// Before mapping, this stashed value also feeds the xdg_popup
// positioner as the anchor-rect origin.
window->wl.pendingPosX = xpos;
window->wl.pendingPosY = ypos;
window->wl.pendingPosSet = GLFW_TRUE;
}
void _glfwGetWindowSizeWayland(_GLFWwindow* window, int* width, int* height)
@ -3168,6 +3247,33 @@ void _glfwFocusWindowWayland(_GLFWwindow* window)
xdg_activation_token_v1_commit(window->wl.activationToken);
}
void _glfwDragWindowWayland(_GLFWwindow* window)
{
if (!_glfw.wl.seat || !_glfw.wl.pointerButtonSerial)
return;
// Fire immediately when the toplevel is already mapped; otherwise stash
// the press serial and let the mapped-frame callback issue the move once
// the compositor has processed the first buffer. The pre-show case
// matters for tab-tear (caller invokes this before glfwShowWindow maps
// anything), and the pre-map case matters because calling
// xdg_toplevel.move on an unmapped surface crashes KWin.
if (!window->wl.mapped)
{
window->wl.dragPendingSerial = _glfw.wl.pointerButtonSerial;
window->wl.dragPending = GLFW_TRUE;
return;
}
struct xdg_toplevel* toplevel = window->wl.xdg.toplevel;
if (!toplevel && window->wl.libdecor.frame)
toplevel = libdecor_frame_get_xdg_toplevel(window->wl.libdecor.frame);
if (!toplevel)
return;
xdg_toplevel_move(toplevel, _glfw.wl.seat, _glfw.wl.pointerButtonSerial);
}
void _glfwSetWindowMonitorWayland(_GLFWwindow* window,
_GLFWmonitor* monitor,
int xpos, int ypos,

View file

@ -1226,6 +1226,7 @@ GLFWbool _glfwConnectX11(int platformID, _GLFWplatform* platform)
.hideWindow = _glfwHideWindowX11,
.requestWindowAttention = _glfwRequestWindowAttentionX11,
.focusWindow = _glfwFocusWindowX11,
.dragWindow = _glfwDragWindowX11,
.setWindowMonitor = _glfwSetWindowMonitorX11,
.windowFocused = _glfwWindowFocusedX11,
.windowIconified = _glfwWindowIconifiedX11,

View file

@ -921,6 +921,7 @@ void _glfwShowWindowX11(_GLFWwindow* window);
void _glfwHideWindowX11(_GLFWwindow* window);
void _glfwRequestWindowAttentionX11(_GLFWwindow* window);
void _glfwFocusWindowX11(_GLFWwindow* window);
void _glfwDragWindowX11(_GLFWwindow* window);
void _glfwSetWindowMonitorX11(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate);
GLFWbool _glfwWindowFocusedX11(_GLFWwindow* window);
GLFWbool _glfwWindowIconifiedX11(_GLFWwindow* window);

View file

@ -2613,6 +2613,40 @@ void _glfwFocusWindowX11(_GLFWwindow* window)
XFlush(_glfw.x11.display);
}
void _glfwDragWindowX11(_GLFWwindow* window)
{
// Equivalent of xdg_toplevel.move / WM_SYSCOMMAND SC_MOVE for X11: send
// _NET_WM_MOVERESIZE with direction=MOVE, and the WM takes over pointer
// tracking until the user releases the button. Matches the behaviour
// used by the titlebar-drag path in the button-press handler.
if (!_glfw.x11.NET_WM_MOVERESIZE)
return;
Window root, child;
int rootX, rootY, winX, winY;
unsigned int mask;
if (!XQueryPointer(_glfw.x11.display, window->x11.handle,
&root, &child, &rootX, &rootY, &winX, &winY, &mask))
return;
XUngrabPointer(_glfw.x11.display, CurrentTime);
XEvent xev = { 0 };
xev.type = ClientMessage;
xev.xclient.window = window->x11.handle;
xev.xclient.message_type = _glfw.x11.NET_WM_MOVERESIZE;
xev.xclient.format = 32;
xev.xclient.data.l[0] = rootX;
xev.xclient.data.l[1] = rootY;
xev.xclient.data.l[2] = 8; // _NET_WM_MOVERESIZE_MOVE
xev.xclient.data.l[3] = Button1;
xev.xclient.data.l[4] = 1; // source: normal app
XSendEvent(_glfw.x11.display, _glfw.x11.root, False,
SubstructureRedirectMask | SubstructureNotifyMask, &xev);
XFlush(_glfw.x11.display);
}
void _glfwSetWindowMonitorX11(_GLFWwindow* window,
_GLFWmonitor* monitor,
int xpos, int ypos,