From b3ea4243b182db83bf4e2698a655682b564278d6 Mon Sep 17 00:00:00 2001 From: Bjorn Schobben Date: Wed, 12 Aug 2026 15:51:11 +0200 Subject: [PATCH] Fix GLFW X11 viewport synchronization --- backends/imgui_impl_glfw.cpp | 156 +++++++++++++++++++--- docs/CHANGELOG.txt | 9 +- docs/GLFW_X11_VIEWPORT_SYNCHRONIZATION.md | 94 +++++++++++++ imgui.cpp | 47 +++++-- imgui_internal.h | 1 + imgui_widgets.cpp | 1 + 6 files changed, 279 insertions(+), 29 deletions(-) create mode 100644 docs/GLFW_X11_VIEWPORT_SYNCHRONIZATION.md diff --git a/backends/imgui_impl_glfw.cpp b/backends/imgui_impl_glfw.cpp index b5347cda0..2f360b75f 100644 --- a/backends/imgui_impl_glfw.cpp +++ b/backends/imgui_impl_glfw.cpp @@ -31,6 +31,8 @@ // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2026-08-12: [Docking] Multi-viewport: Synchronize window manager adjustments instead of treating them as setter echoes. On X11, when the native API is available, observe synthetic ConfigureNotify events so fully rejected requests also synchronize without a timeout. (#9356, #9398, #9442) +// 2026-08-12: [Docking] Multi-viewport: Disable support on Wayland with legacy GLFW versions as global window positioning is unavailable. // 2026-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface. // 2026-04-21: Added a Win32-specific implementation of ImGui_ImplGlfw_GetContentScaleXXXX functions for legacy GLFW 3.2. // 2026-03-25: Mouse cursor is properly restored if changed by user app/code while using glfwSetInputMode(..., GLFW_CURSOR_DISABLED) or ImGuiConfigFlags_NoMouseCursorChange. Amend change from 2025-12-10. @@ -235,6 +237,12 @@ typedef Atom (*PFN_XInternAtom)(Display*, const char* ,Bool); typedef int (*PFN_XChangeProperty)(Display*, Window, Atom, Atom, int, int, const unsigned char*, int); typedef int (*PFN_XChangeWindowAttributes)(Display*, Window, unsigned long, XSetWindowAttributes*); typedef int (*PFN_XFlush)(Display*); +typedef Display* (*PFN_XOpenDisplay)(const char*); +typedef int (*PFN_XCloseDisplay)(Display*); +typedef int (*PFN_XSelectInput)(Display*, Window, long); +typedef int (*PFN_XPending)(Display*); +typedef int (*PFN_XNextEvent)(Display*, XEvent*); +typedef int (*PFN_XSync)(Display*, Bool); #endif // GLFW data @@ -281,6 +289,13 @@ struct ImGui_ImplGlfw_Data PFN_XChangeProperty XChangeProperty; PFN_XChangeWindowAttributes XChangeWindowAttributes; PFN_XFlush XFlush; + PFN_XOpenDisplay XOpenDisplay; + PFN_XCloseDisplay XCloseDisplay; + PFN_XSelectInput XSelectInput; + PFN_XPending XPending; + PFN_XNextEvent XNextEvent; + PFN_XSync XSync; + Display* X11EventObserverDisplay; #endif ImGui_ImplGlfw_Data() { memset((void*)this, 0, sizeof(*this)); } @@ -310,6 +325,9 @@ static ImGui_ImplGlfw_Data* ImGui_ImplGlfw_GetBackendData(GLFWwindow* window) static void ImGui_ImplGlfw_UpdateMonitors(); static void ImGui_ImplGlfw_InitMultiViewportSupport(); static void ImGui_ImplGlfw_ShutdownMultiViewportSupport(); +#if GLFW_HAS_X11 +static void ImGui_ImplGlfw_UpdateX11ConfigureEvents(); +#endif // Functions static bool ImGui_ImplGlfw_IsWayland() @@ -739,11 +757,7 @@ static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, Glfw bool has_viewports = false; #ifndef __EMSCRIPTEN__ - has_viewports = true; -#if GLFW_HAS_GETPLATFORM - if (glfwGetPlatform() == GLFW_PLATFORM_WAYLAND) - has_viewports = false; -#endif + has_viewports = !bd->IsWayland; if (has_viewports) io.BackendFlags |= ImGuiBackendFlags_PlatformHasViewports; // We can create multi-viewports on the Platform side (optional) #endif @@ -835,11 +849,20 @@ static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, Glfw const char* x11_module_path = "libX11.so.6"; #endif bd->X11Module = dlopen(x11_module_path, RTLD_LAZY | RTLD_LOCAL); - bd->XInternAtom = (PFN_XInternAtom)dlsym(bd->X11Module, "XInternAtom"); - bd->XChangeProperty = (PFN_XChangeProperty)dlsym(bd->X11Module, "XChangeProperty"); - bd->XChangeWindowAttributes = (PFN_XChangeWindowAttributes)dlsym(bd->X11Module, "XChangeWindowAttributes"); - bd->XFlush = (PFN_XFlush)dlsym(bd->X11Module, "XFlush"); - IM_ASSERT(bd->XInternAtom != nullptr && bd->XChangeProperty != nullptr && bd->XChangeWindowAttributes != nullptr && bd->XFlush != nullptr); + if (bd->X11Module != nullptr) + { + bd->XInternAtom = (PFN_XInternAtom)dlsym(bd->X11Module, "XInternAtom"); + bd->XChangeProperty = (PFN_XChangeProperty)dlsym(bd->X11Module, "XChangeProperty"); + bd->XChangeWindowAttributes = (PFN_XChangeWindowAttributes)dlsym(bd->X11Module, "XChangeWindowAttributes"); + bd->XFlush = (PFN_XFlush)dlsym(bd->X11Module, "XFlush"); + bd->XOpenDisplay = (PFN_XOpenDisplay)dlsym(bd->X11Module, "XOpenDisplay"); + bd->XCloseDisplay = (PFN_XCloseDisplay)dlsym(bd->X11Module, "XCloseDisplay"); + bd->XSelectInput = (PFN_XSelectInput)dlsym(bd->X11Module, "XSelectInput"); + bd->XPending = (PFN_XPending)dlsym(bd->X11Module, "XPending"); + bd->XNextEvent = (PFN_XNextEvent)dlsym(bd->X11Module, "XNextEvent"); + bd->XSync = (PFN_XSync)dlsym(bd->X11Module, "XSync"); + IM_ASSERT(bd->XInternAtom != nullptr && bd->XChangeProperty != nullptr && bd->XChangeWindowAttributes != nullptr && bd->XFlush != nullptr); + } } #endif @@ -907,6 +930,11 @@ void ImGui_ImplGlfw_Shutdown() #endif #if GLFW_HAS_X11 + if (bd->X11EventObserverDisplay != nullptr) + { + bd->XCloseDisplay(bd->X11EventObserverDisplay); + bd->X11EventObserverDisplay = nullptr; + } if (bd->X11Module != nullptr) dlclose(bd->X11Module); #endif @@ -1201,6 +1229,9 @@ void ImGui_ImplGlfw_NewFrame() // Setup main viewport size (every frame to accommodate for window resizing) ImGui_ImplGlfw_GetWindowSizeAndFramebufferScale(bd->Window, &io.DisplaySize, &io.DisplayFramebufferScale); ImGui_ImplGlfw_UpdateMonitors(); +#if GLFW_HAS_X11 + ImGui_ImplGlfw_UpdateX11ConfigureEvents(); +#endif // Setup time step // (Accept glfwGetTime() not returning a monotonically increasing value. Seems to happens on disconnecting peripherals and probably on VMs and Emscripten, see #6491, #6189, #6114, #3644) @@ -1294,6 +1325,11 @@ struct ImGui_ImplGlfw_ViewportData bool WindowOwned; int IgnoreWindowPosEventFrame; int IgnoreWindowSizeEventFrame; + int LastWindowPosRequest[2]; + int LastWindowSizeRequest[2]; +#if GLFW_HAS_X11 + ::Window X11Window; +#endif #ifdef _WIN32 WNDPROC PrevWndProc; #endif @@ -1314,30 +1350,75 @@ static void ImGui_ImplGlfw_WindowCloseCallback(GLFWwindow* window) // - on Linux it is queued and invoked during glfwPollEvents() // Because the event doesn't always fire on glfwSetWindowXXX() we use a frame counter tag to only // ignore recent glfwSetWindowXXX() calls. -static void ImGui_ImplGlfw_WindowPosCallback(GLFWwindow* window, int, int) +static void ImGui_ImplGlfw_WindowPosCallback(GLFWwindow* window, int x, int y) { if (ImGuiViewport* viewport = ImGui::FindViewportByPlatformHandle(window)) { if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) { bool ignore_event = (ImGui::GetFrameCount() <= vd->IgnoreWindowPosEventFrame + 1); - //data->IgnoreWindowPosEventFrame = -1; - if (ignore_event) + // A window manager may adjust the requested position. Only ignore the callback when it reports + // the latest position passed to glfwSetWindowPos() and the viewport still expects that position. + // Otherwise query and apply the authoritative platform position. + const bool is_latest_request = (x == vd->LastWindowPosRequest[0] && y == vd->LastWindowPosRequest[1]); + const bool is_current_viewport_pos = (x == (int)viewport->Pos.x && y == (int)viewport->Pos.y); + if (ignore_event && is_latest_request && is_current_viewport_pos) return; } viewport->PlatformRequestMove = true; } } -static void ImGui_ImplGlfw_WindowSizeCallback(GLFWwindow* window, int, int) +#if GLFW_HAS_X11 +// X11 window managers report a completely rejected ConfigureRequest with a synthetic ConfigureNotify containing the +// unchanged geometry. GLFW consumes that event but doesn't invoke its public callback when the cached geometry didn't +// change. Observe synthetic ConfigureNotify events through a separate X connection so those rejections still reach +// Dear ImGui without relying on a timing heuristic. The application should call glfwPollEvents() before this NewFrame(). +static void ImGui_ImplGlfw_UpdateX11ConfigureEvents() +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + if (bd->X11EventObserverDisplay == nullptr) + return; + + while (bd->XPending(bd->X11EventObserverDisplay) > 0) + { + XEvent event; + bd->XNextEvent(bd->X11EventObserverDisplay, &event); + if (event.type != ConfigureNotify || !event.xconfigure.send_event) + continue; + + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + for (int viewport_n = 1; viewport_n < platform_io.Viewports.Size; viewport_n++) + { + ImGuiViewport* viewport = platform_io.Viewports[viewport_n]; + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + if (vd == nullptr || vd->X11Window != event.xconfigure.window) + continue; + + int x, y, width, height; + glfwGetWindowPos(vd->Window, &x, &y); + glfwGetWindowSize(vd->Window, &width, &height); + if (x != (int)viewport->Pos.x || y != (int)viewport->Pos.y) + viewport->PlatformRequestMove = true; + if (width != (int)viewport->Size.x || height != (int)viewport->Size.y) + viewport->PlatformRequestResize = true; + break; + } + } +} +#endif + +static void ImGui_ImplGlfw_WindowSizeCallback(GLFWwindow* window, int width, int height) { if (ImGuiViewport* viewport = ImGui::FindViewportByPlatformHandle(window)) { if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) { bool ignore_event = (ImGui::GetFrameCount() <= vd->IgnoreWindowSizeEventFrame + 1); - //data->IgnoreWindowSizeEventFrame = -1; - if (ignore_event) + // Preserve delayed setter-echo suppression only while the viewport still expects that exact size. + const bool is_latest_request = (width == vd->LastWindowSizeRequest[0] && height == vd->LastWindowSizeRequest[1]); + const bool is_current_viewport_size = (width == (int)viewport->Size.x && height == (int)viewport->Size.y); + if (ignore_event && is_latest_request && is_current_viewport_size) return; } viewport->PlatformRequestResize = true; @@ -1349,7 +1430,7 @@ static void ImGui_ImplGlfw_WindowSizeCallback(GLFWwindow* window, int, int) static void ImGui_ImplGlfw_SetWindowFloating(ImGui_ImplGlfw_Data* bd, GLFWwindow* window) { #ifdef GLFW_EXPOSE_NATIVE_X11 - if (glfwGetPlatform() == GLFW_PLATFORM_X11) + if (glfwGetPlatform() == GLFW_PLATFORM_X11 && bd->XInternAtom != nullptr && bd->XChangeProperty != nullptr && bd->XChangeWindowAttributes != nullptr && bd->XFlush != nullptr) { Display* display = glfwGetX11Display(); Window xwindow = glfwGetX11Window(window); @@ -1405,6 +1486,26 @@ static void ImGui_ImplGlfw_CreateWindow(ImGuiViewport* viewport) viewport->PlatformHandleRaw = (void*)glfwGetCocoaWindow(vd->Window); #endif glfwSetWindowPos(vd->Window, (int)viewport->Pos.x, (int)viewport->Pos.y); +#if GLFW_HAS_X11 +#if GLFW_HAS_GETPLATFORM + const bool is_x11 = (glfwGetPlatform() == GLFW_PLATFORM_X11); +#else + const bool is_x11 = !bd->IsWayland; +#endif + const bool has_x11_observer_api = (bd->XOpenDisplay != nullptr && bd->XCloseDisplay != nullptr && bd->XSelectInput != nullptr && bd->XPending != nullptr && bd->XNextEvent != nullptr && bd->XSync != nullptr); + if (is_x11 && has_x11_observer_api && bd->X11EventObserverDisplay == nullptr) + bd->X11EventObserverDisplay = bd->XOpenDisplay(DisplayString(glfwGetX11Display())); + if (bd->X11EventObserverDisplay != nullptr) + { + bd->XSync(glfwGetX11Display(), False); + vd->X11Window = glfwGetX11Window(vd->Window); + if (vd->X11Window != None) + { + bd->XSelectInput(bd->X11EventObserverDisplay, vd->X11Window, StructureNotifyMask); + bd->XSync(bd->X11EventObserverDisplay, False); + } + } +#endif // Install GLFW callbacks for secondary viewports glfwSetWindowFocusCallback(vd->Window, ImGui_ImplGlfw_WindowFocusCallback); @@ -1429,6 +1530,14 @@ static void ImGui_ImplGlfw_DestroyWindow(ImGuiViewport* viewport) ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) { +#if GLFW_HAS_X11 + if (bd->X11EventObserverDisplay != nullptr && vd->X11Window != 0) + { + bd->XSelectInput(bd->X11EventObserverDisplay, vd->X11Window, NoEventMask); + bd->XSync(bd->X11EventObserverDisplay, False); + ImGui_ImplGlfw_UpdateX11ConfigureEvents(); + } +#endif if (vd->WindowOwned) { #if !GLFW_HAS_MOUSE_PASSTHROUGH && GLFW_HAS_WINDOW_HOVERED && defined(_WIN32) @@ -1499,7 +1608,9 @@ static void ImGui_ImplGlfw_SetWindowPos(ImGuiViewport* viewport, ImVec2 pos) { ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; vd->IgnoreWindowPosEventFrame = ImGui::GetFrameCount(); - glfwSetWindowPos(vd->Window, (int)pos.x, (int)pos.y); + vd->LastWindowPosRequest[0] = (int)pos.x; + vd->LastWindowPosRequest[1] = (int)pos.y; + glfwSetWindowPos(vd->Window, vd->LastWindowPosRequest[0], vd->LastWindowPosRequest[1]); } static ImVec2 ImGui_ImplGlfw_GetWindowSize(ImGuiViewport* viewport) @@ -1521,10 +1632,15 @@ static void ImGui_ImplGlfw_SetWindowSize(ImGuiViewport* viewport, ImVec2 size) int x, y, width, height; glfwGetWindowPos(vd->Window, &x, &y); glfwGetWindowSize(vd->Window, &width, &height); - glfwSetWindowPos(vd->Window, x, y - height + size.y); + vd->IgnoreWindowPosEventFrame = ImGui::GetFrameCount(); + vd->LastWindowPosRequest[0] = x; + vd->LastWindowPosRequest[1] = (int)(y - height + size.y); + glfwSetWindowPos(vd->Window, vd->LastWindowPosRequest[0], vd->LastWindowPosRequest[1]); #endif vd->IgnoreWindowSizeEventFrame = ImGui::GetFrameCount(); - glfwSetWindowSize(vd->Window, (int)size.x, (int)size.y); + vd->LastWindowSizeRequest[0] = (int)size.x; + vd->LastWindowSizeRequest[1] = (int)size.y; + glfwSetWindowSize(vd->Window, vd->LastWindowSizeRequest[0], vd->LastWindowSizeRequest[1]); } static ImVec2 ImGui_ImplGlfw_GetWindowFramebufferScale(ImGuiViewport* viewport) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 650564d7f..35027cb00 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -70,7 +70,15 @@ Other Changes: Docking+Viewports Branch: +- Viewports: fixed platform position/size requests adjusted or rejected by a window manager + leaving Dear ImGui geometry out of sync with the native window. Active window dragging now + continues from the platform-accepted position instead of repeatedly requesting rejected + geometry. (#9356, #9398, #9442) - Backends: + - GLFW: on X11, observe synthetic ConfigureNotify events when the native API is available + so completely rejected viewport geometry requests are synchronized without a timeout. + - GLFW: disabled multi-viewports on Wayland with legacy GLFW versions, where global window + positioning is unavailable. - Metal: fixed resizing windows losing framebuffer scale. (#6828, #8856) [@laikoken] - Examples: - SDL3+Metal, GLFW+Metal: fix multi-viewport support. @@ -9671,4 +9679,3 @@ Decorated log and release notes: https://github.com/ocornut/imgui/releases/tag/v Decorated log and release notes: https://github.com/ocornut/imgui/releases/tag/v1.00 - Initial release. - diff --git a/docs/GLFW_X11_VIEWPORT_SYNCHRONIZATION.md b/docs/GLFW_X11_VIEWPORT_SYNCHRONIZATION.md new file mode 100644 index 000000000..1a0b74020 --- /dev/null +++ b/docs/GLFW_X11_VIEWPORT_SYNCHRONIZATION.md @@ -0,0 +1,94 @@ +# GLFW/X11 multi-viewport geometry synchronization + +## Problem + +Dear ImGui's GLFW backend drives secondary viewport geometry through +`glfwSetWindowPos()` and `glfwSetWindowSize()`. Those calls are asynchronous on +X11: the window manager may accept, adjust, or reject the request after GLFW has +returned. + +The portable GLFW API does not expose a request identifier or an acknowledgement +containing the geometry accepted by the window manager. Its position and size +callbacks are change notifications, not completion notifications. + +This matters for a completely rejected X11 `ConfigureRequest`. ICCCM-compliant +window managers send a synthetic `ConfigureNotify` containing the unchanged +geometry, but GLFW only invokes its public position and size callbacks when that +geometry differs from GLFW's cached state. GLFW consumes the synthetic event, so +the application receives no callback. Dear ImGui can consequently retain the +requested geometry while the native window remains at its previous geometry. +Rendering, hit testing, mouse coordinates, and subsequent drag requests then use +inconsistent origins. + +This is best described as a GLFW API limitation rather than an unambiguous GLFW +bug: an unchanged position is not necessarily expected to produce a "position +changed" callback. However, the current API is insufficient for robust +requested-versus-observed window geometry synchronization on asynchronous window +systems. + +## Why the obvious alternatives are insufficient + +- **Immediate readback after a setter:** the window manager is a separate X11 + client. `glfwGetWindowPos()` may still return the old geometry before the + window manager processes the request, even when the request will be accepted. +- **Readback after a fixed timeout:** reduces the race but does not eliminate it. + It also introduces latency and frame-rate-, load-, and window-manager-dependent + behavior. +- **Removing GLFW's setter-echo suppression:** regresses delayed position/size + callback cases previously fixed for #2117 and #2756. +- **Observing GLFW's X11 connection:** GLFW owns and drains that event queue; the + backend has no public native-event hook through which to observe consumed + events. + +## Current compatibility approach + +The backend keeps the existing callback path and classifies a recent callback as +a harmless setter echo only when its integer geometry matches both the latest +issued setter and the geometry still expected by the viewport. Adjusted or stale +geometry requests an authoritative readback during the next Dear ImGui frame. + +For the unchanged-rejection case, the X11 backend optionally opens a second X11 +connection and selects `StructureNotifyMask` on secondary viewport windows. This +connection receives the synthetic `ConfigureNotify` that GLFW otherwise filters. +The backend uses it only as a signal to compare native and viewport geometry; the +normal GLFW getters remain the authoritative source. + +Using a second X11 connection requires explicit cross-connection ordering during +window creation and destruction. It is deliberately optional: if the native X11 +symbols or observer connection are unavailable, the backend falls back to the +ordinary GLFW callback behavior rather than failing initialization. + +This avoids a timing heuristic, but it is compatibility machinery that would be +preferable to remove once supported GLFW versions expose an adequate event or +acknowledgement facility. + +## Preferred GLFW-side capability + +A long-term GLFW solution could provide any one of: + +1. a configure-request completion notification, including unchanged rejection; +2. a native event hook that observes X11 events before GLFW filters them; or +3. documented delivery of synthetic `ConfigureNotify` through an additional + callback whose semantics do not imply that geometry changed. + +With such a capability, Dear ImGui could retain its portable requested/observed +state handling while removing the separate X11 observer for newer GLFW versions. + +## Required regression coverage + +The synchronization contract should cover: + +- synchronous and delayed exact setter echoes; +- adjusted/clamped position and size; +- complete rejection with unchanged native geometry; +- two outstanding requests whose responses arrive in separate frames; +- simultaneous position and size changes; +- active dragging at screen boundaries without snap-back or setter oscillation; +- collapse/uncollapse and delayed-size behavior from #2117/#2756; and +- native Xorg with Mutter, since XWayland is useful but not equivalent evidence + for this event path. + +The Test Engine mock can deterministically validate Dear ImGui's core ownership +and drag-rebasing behavior. It cannot replace backend-level or native-X11 tests, +because `-viewport-mock` replaces the GLFW platform handlers and therefore does +not execute GLFW's callback filtering or the X11 observer. diff --git a/imgui.cpp b/imgui.cpp index 98db04810..235042169 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -4334,6 +4334,7 @@ ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas) HoveredWindowUnderMovingWindow = NULL; HoveredWindowBeforeClear = NULL; MovingWindow = NULL; + MovingWindowClickOffset = ImVec2(-1, -1); WheelingWindow = NULL; WheelingWindowStartFrame = WheelingWindowScrolledFrame = -1; WheelingWindowReleaseTimer = 0.0f; @@ -5465,6 +5466,7 @@ void ImGui::StartMouseMovingWindow(ImGuiWindow* window) if (g.IO.ConfigNavCursorVisibleAuto) g.NavCursorVisible = false; g.ActiveIdClickOffset = g.IO.MouseClickedPos[0] - window->RootWindowDockTree->Pos; + g.MovingWindowClickOffset = g.ActiveIdClickOffset; g.ActiveIdNoClearOnFocusLoss = true; SetActiveIdUsingAllKeyboardKeys(); @@ -5550,7 +5552,7 @@ void ImGui::UpdateMouseMovingWindowNewFrame() const bool window_disappeared = (!moving_window->WasActive && !moving_window->Active); if (g.IO.MouseDown[0] && IsMousePosValid(&g.IO.MousePos) && !window_disappeared) { - ImVec2 pos = g.IO.MousePos - g.ActiveIdClickOffset; + ImVec2 pos = g.IO.MousePos - g.MovingWindowClickOffset; if (moving_window->Pos.x != pos.x || moving_window->Pos.y != pos.y) { SetWindowPos(moving_window, pos, ImGuiCond_Always); @@ -17210,7 +17212,22 @@ static void ImGui::UpdateViewportsNewFrame() { // Viewport->WorkPos and WorkSize will be updated below if (viewport->PlatformRequestMove) - viewport->Pos = viewport->LastPlatformPos = g.PlatformIO.Platform_GetWindowPos(viewport); + { + const ImVec2 requested_pos = viewport->Pos; + const ImVec2 platform_pos = g.PlatformIO.Platform_GetWindowPos(viewport); + // When a window manager adjusts an actively dragged viewport, rebase the mutable drag offset. + // Only rebase adjusted axes: accepted axes should retain any mouse movement received this frame. + // Keep ActiveIdClickOffset unchanged: docking uses it as the original click location. + ImGuiWindow* moving_root = g.MovingWindow ? g.MovingWindow->RootWindowDockTree : NULL; + if (moving_root && viewport->Window == moving_root && moving_root->ViewportOwned && IsMousePosValid(&g.IO.MousePos)) + { + if (platform_pos.x != requested_pos.x) + g.MovingWindowClickOffset.x = g.IO.MousePos.x - platform_pos.x; + if (platform_pos.y != requested_pos.y) + g.MovingWindowClickOffset.y = g.IO.MousePos.y - platform_pos.y; + } + viewport->Pos = viewport->LastPlatformPos = platform_pos; + } if (viewport->PlatformRequestResize) viewport->Size = viewport->LastPlatformSize = g.PlatformIO.Platform_GetWindowSize(viewport); if (g.PlatformIO.Platform_GetWindowFramebufferScale != NULL) @@ -17763,15 +17780,26 @@ void ImGui::UpdatePlatformWindows() viewport->PlatformWindowCreated = true; } + // Consume requests received before this update. Callbacks raised synchronously by setters below must survive + // until the next NewFrame(), so clear the old requests before invoking platform functions instead of afterward. + const bool platform_request_move = viewport->PlatformRequestMove; + const bool platform_request_resize = viewport->PlatformRequestResize; + viewport->PlatformRequestMove = viewport->PlatformRequestResize = false; + // Apply Position and Size (from ImGui to Platform/Renderer backends) - if ((viewport->LastPlatformPos.x != viewport->Pos.x || viewport->LastPlatformPos.y != viewport->Pos.y) && !viewport->PlatformRequestMove) + if ((viewport->LastPlatformPos.x != viewport->Pos.x || viewport->LastPlatformPos.y != viewport->Pos.y) && !platform_request_move) + { g.PlatformIO.Platform_SetWindowPos(viewport, viewport->Pos); - if ((viewport->LastPlatformSize.x != viewport->Size.x || viewport->LastPlatformSize.y != viewport->Size.y) && !viewport->PlatformRequestResize) + viewport->LastPlatformPos = viewport->Pos; + } + if ((viewport->LastPlatformSize.x != viewport->Size.x || viewport->LastPlatformSize.y != viewport->Size.y) && !platform_request_resize) + { g.PlatformIO.Platform_SetWindowSize(viewport, viewport->Size); + viewport->LastPlatformSize = viewport->Size; + } if ((viewport->LastRendererSize.x != viewport->Size.x || viewport->LastRendererSize.y != viewport->Size.y) && g.PlatformIO.Renderer_SetWindowSize) g.PlatformIO.Renderer_SetWindowSize(viewport, viewport->Size); - viewport->LastPlatformPos = viewport->Pos; - viewport->LastPlatformSize = viewport->LastRendererSize = viewport->Size; + viewport->LastRendererSize = viewport->Size; // Update title bar (if it changed) if (ImGuiWindow* window_for_title = GetWindowForTitleDisplay(viewport->Window)) @@ -17808,8 +17836,10 @@ void ImGui::UpdatePlatformWindows() g.PlatformIO.Platform_ShowWindow(viewport); } - // Clear request flags - viewport->ClearRequestFlags(); + // Position/size requests raised synchronously above need to survive until the next NewFrame(). + // Close requests keep their previous lifetime and are consumed once per platform update. + viewport->PlatformRequestClose = false; + } } @@ -19259,6 +19289,7 @@ static void ImGui::DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWind IM_ASSERT(node->WantMouseMove == true); StartMouseMovingWindow(window); g.ActiveIdClickOffset = g.IO.MouseClickedPos[0] - node->Pos; + g.MovingWindowClickOffset = g.ActiveIdClickOffset; g.MovingWindow = window; // If we are docked into a non moveable root window, StartMouseMovingWindow() won't set g.MovingWindow. Override that decision. node->WantMouseMove = false; } diff --git a/imgui_internal.h b/imgui_internal.h index 39ef9be4e..e8ef65d74 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -2492,6 +2492,7 @@ struct ImGuiContext ImGuiWindow* HoveredWindowUnderMovingWindow; // Hovered window ignoring MovingWindow. Only set if MovingWindow is set. ImGuiWindow* HoveredWindowBeforeClear; // Window the mouse is hovering. Filled even with _NoMouse. This is currently useful for multi-context compositors. ImGuiWindow* MovingWindow; // Track the window we clicked on (in order to preserve focus). The actual window that is moved is generally MovingWindow->RootWindowDockTree. + ImVec2 MovingWindowClickOffset; // Click offset used to position MovingWindow. May be rebased when a platform window manager adjusts our requested position. ImGuiWindow* WheelingWindow; // Track the window we started mouse-wheeling on. Until a timer elapse or mouse has moved, generally keep scrolling the same window even if during the course of scrolling the mouse ends up hovering a child window. ImVec2 WheelingWindowRefMousePos; int WheelingWindowStartFrame; // This may be set one frame before WheelingWindow is != NULL diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 17968a54d..c3bb41c0b 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -10908,6 +10908,7 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, g.MovingWindow = docked_window; SetActiveID(g.MovingWindow->MoveId, g.MovingWindow); g.ActiveIdClickOffset -= g.MovingWindow->Pos - bb.Min; + g.MovingWindowClickOffset = g.ActiveIdClickOffset; g.ActiveIdNoClearOnFocusLoss = true; SetActiveIdUsingAllKeyboardKeys(); }