From b3ea4243b182db83bf4e2698a655682b564278d6 Mon Sep 17 00:00:00 2001 From: Bjorn Schobben Date: Wed, 12 Aug 2026 15:51:11 +0200 Subject: [PATCH 1/4] 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(); } From a4fa54780f90494ab90ac7fe21a30ee8c9d601fa Mon Sep 17 00:00:00 2001 From: Daedie-git Date: Wed, 12 Aug 2026 17:03:09 +0200 Subject: [PATCH 2/4] Remove X11 synchronization design document --- docs/GLFW_X11_VIEWPORT_SYNCHRONIZATION.md | 94 ----------------------- 1 file changed, 94 deletions(-) delete mode 100644 docs/GLFW_X11_VIEWPORT_SYNCHRONIZATION.md diff --git a/docs/GLFW_X11_VIEWPORT_SYNCHRONIZATION.md b/docs/GLFW_X11_VIEWPORT_SYNCHRONIZATION.md deleted file mode 100644 index 1a0b74020..000000000 --- a/docs/GLFW_X11_VIEWPORT_SYNCHRONIZATION.md +++ /dev/null @@ -1,94 +0,0 @@ -# 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. From 876005153ffd35163045fd305bf84205655d758c Mon Sep 17 00:00:00 2001 From: Bjorn Schobben Date: Tue, 18 Aug 2026 22:39:57 +0200 Subject: [PATCH 3/4] Viewports: do not skip SetWindowPos/Size after a leftover setter echo. NewFrame already applies PlatformRequestMove/Resize to Pos/Size. Reusing those flags to suppress the next Set made synchronous echoes (Win32 WM_MOVE) drop the following drag update, so the OS window moved every other frame. --- imgui.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 235042169..4399b77f4 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -17780,19 +17780,18 @@ 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; + // Consume requests received before this update. NewFrame already applied them to Pos/Size/LastPlatform*. + // A leftover flag must not suppress a later imgui-initiated Set (e.g. continued drag after a setter echo). + // Clear before invoking platform functions so synchronous setter callbacks survive until the next NewFrame(). 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) && !platform_request_move) + if (viewport->LastPlatformPos.x != viewport->Pos.x || viewport->LastPlatformPos.y != viewport->Pos.y) { g.PlatformIO.Platform_SetWindowPos(viewport, viewport->Pos); viewport->LastPlatformPos = viewport->Pos; } - if ((viewport->LastPlatformSize.x != viewport->Size.x || viewport->LastPlatformSize.y != viewport->Size.y) && !platform_request_resize) + if (viewport->LastPlatformSize.x != viewport->Size.x || viewport->LastPlatformSize.y != viewport->Size.y) { g.PlatformIO.Platform_SetWindowSize(viewport, viewport->Size); viewport->LastPlatformSize = viewport->Size; From c65a3e2d6ea99de98ba0e1e06ceeda0d18e2a02b Mon Sep 17 00:00:00 2001 From: Bjorn Schobben Date: Wed, 19 Aug 2026 01:05:05 +0200 Subject: [PATCH 4/4] Viewports: consume exact setter echoes before Begin. Leftover PlatformRequestMove/Resize from a successful Set (Win32 WM_MOVE) stayed authoritative through the following Begin, so SetNextWindowPos/Size, clamp, and nav updates on a floating viewport were discarded. Apply the platform rect in NewFrame and clear the flag when geometry matches the last submitted request. Keep the flag when the OS actually adjusted the rect. Drag rebase compares truncated LastPlatform* so a clamped axis does not hitch the free one. GLFW: drop the second X connection. Poll live geometry after a stable unanswered X11 Set instead. Pre-3.4 IsWayland() no longer assumes Wayland when X11 native headers are missing. --- backends/imgui_impl_glfw.cpp | 182 ++++++++++++++++++----------------- docs/CHANGELOG.txt | 4 +- imgui.cpp | 45 ++++++--- 3 files changed, 128 insertions(+), 103 deletions(-) diff --git a/backends/imgui_impl_glfw.cpp b/backends/imgui_impl_glfw.cpp index 2f360b75f..c3901ab1c 100644 --- a/backends/imgui_impl_glfw.cpp +++ b/backends/imgui_impl_glfw.cpp @@ -31,7 +31,7 @@ // 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: Synchronize window manager adjustments instead of treating them as setter echoes. On X11, poll live geometry after a setter so fully rejected requests still synchronize. (#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. @@ -237,12 +237,6 @@ 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 @@ -289,13 +283,6 @@ 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)); } @@ -326,7 +313,7 @@ static void ImGui_ImplGlfw_UpdateMonitors(); static void ImGui_ImplGlfw_InitMultiViewportSupport(); static void ImGui_ImplGlfw_ShutdownMultiViewportSupport(); #if GLFW_HAS_X11 -static void ImGui_ImplGlfw_UpdateX11ConfigureEvents(); +static void ImGui_ImplGlfw_UpdateWindowGeometryRequests(); #endif // Functions @@ -341,10 +328,10 @@ static bool ImGui_ImplGlfw_IsWayland() if (strstr(version, "Wayland") == nullptr) // e.g. Ubuntu 22.04 ships with GLFW 3.3.6 compiled without Wayland return false; #ifdef GLFW_EXPOSE_NATIVE_X11 - if (glfwGetX11Display() != nullptr) - return false; + return glfwGetX11Display() == nullptr; +#else + return false; // Missing X11 native API: do not assume Wayland (keeps viewports on X11). #endif - return true; #endif } @@ -855,12 +842,6 @@ static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, Glfw 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); } } @@ -930,11 +911,6 @@ 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 @@ -1230,7 +1206,7 @@ void ImGui_ImplGlfw_NewFrame() ImGui_ImplGlfw_GetWindowSizeAndFramebufferScale(bd->Window, &io.DisplaySize, &io.DisplayFramebufferScale); ImGui_ImplGlfw_UpdateMonitors(); #if GLFW_HAS_X11 - ImGui_ImplGlfw_UpdateX11ConfigureEvents(); + ImGui_ImplGlfw_UpdateWindowGeometryRequests(); #endif // Setup time step @@ -1328,7 +1304,14 @@ struct ImGui_ImplGlfw_ViewportData int LastWindowPosRequest[2]; int LastWindowSizeRequest[2]; #if GLFW_HAS_X11 - ::Window X11Window; + int LastObservedWindowPos[2]; + int LastObservedWindowSize[2]; + int WindowPosMismatchFrameCount; + int WindowSizeMismatchFrameCount; + double WindowPosMismatchStartTime; + double WindowSizeMismatchStartTime; + bool WindowPosRequestPending; + bool WindowSizeRequestPending; #endif #ifdef _WIN32 WNDPROC PrevWndProc; @@ -1357,11 +1340,14 @@ static void ImGui_ImplGlfw_WindowPosCallback(GLFWwindow* window, int x, int y) if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) { bool ignore_event = (ImGui::GetFrameCount() <= vd->IgnoreWindowPosEventFrame + 1); - // 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. + // Ignore only the latest matching setter echo. Adjusted or stale geometry requests a platform readback. 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 GLFW_HAS_X11 + vd->WindowPosRequestPending = false; + vd->WindowPosMismatchFrameCount = 0; + vd->WindowPosMismatchStartTime = 0.0; +#endif if (ignore_event && is_latest_request && is_current_viewport_pos) return; } @@ -1370,39 +1356,56 @@ static void ImGui_ImplGlfw_WindowPosCallback(GLFWwindow* window, int x, int y) } #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() +static bool ImGui_ImplGlfw_UpdateWindowGeometryRequest(int value_0, int value_1, const int request[2], int observed[2], int* mismatch_frame_count, double* mismatch_start_time, bool* request_pending, double current_time) { - ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); - if (bd->X11EventObserverDisplay == nullptr) - return; - - while (bd->XPending(bd->X11EventObserverDisplay) > 0) + if (value_0 == request[0] && value_1 == request[1]) { - XEvent event; - bd->XNextEvent(bd->X11EventObserverDisplay, &event); - if (event.type != ConfigureNotify || !event.xconfigure.send_event) + *request_pending = false; + *mismatch_frame_count = 0; + *mismatch_start_time = 0.0; + return false; + } + if (*mismatch_frame_count > 0 && value_0 == observed[0] && value_1 == observed[1]) + (*mismatch_frame_count)++; + else + { + *mismatch_frame_count = 1; + *mismatch_start_time = current_time; + } + observed[0] = value_0; + observed[1] = value_1; + if (*mismatch_frame_count < 2 || current_time - *mismatch_start_time < 0.050) + return false; + *request_pending = false; + *mismatch_frame_count = 0; + *mismatch_start_time = 0.0; + return true; +} + +// GLFW swallows unchanged synthetic ConfigureNotify. Poll live geometry until it matches the last Set or stays rejected. +static void ImGui_ImplGlfw_UpdateWindowGeometryRequests() +{ + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + const double current_time = glfwGetTime(); + 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) continue; - - ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); - for (int viewport_n = 1; viewport_n < platform_io.Viewports.Size; viewport_n++) + if (vd->WindowPosRequestPending) { - 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; + int x, y; glfwGetWindowPos(vd->Window, &x, &y); - glfwGetWindowSize(vd->Window, &width, &height); - if (x != (int)viewport->Pos.x || y != (int)viewport->Pos.y) + if (ImGui_ImplGlfw_UpdateWindowGeometryRequest(x, y, vd->LastWindowPosRequest, vd->LastObservedWindowPos, &vd->WindowPosMismatchFrameCount, &vd->WindowPosMismatchStartTime, &vd->WindowPosRequestPending, current_time)) viewport->PlatformRequestMove = true; - if (width != (int)viewport->Size.x || height != (int)viewport->Size.y) + } + if (vd->WindowSizeRequestPending) + { + int width, height; + glfwGetWindowSize(vd->Window, &width, &height); + if (ImGui_ImplGlfw_UpdateWindowGeometryRequest(width, height, vd->LastWindowSizeRequest, vd->LastObservedWindowSize, &vd->WindowSizeMismatchFrameCount, &vd->WindowSizeMismatchStartTime, &vd->WindowSizeRequestPending, current_time)) viewport->PlatformRequestResize = true; - break; } } } @@ -1418,6 +1421,11 @@ static void ImGui_ImplGlfw_WindowSizeCallback(GLFWwindow* window, int width, int // 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 GLFW_HAS_X11 + vd->WindowSizeRequestPending = false; + vd->WindowSizeMismatchFrameCount = 0; + vd->WindowSizeMismatchStartTime = 0.0; +#endif if (ignore_event && is_latest_request && is_current_viewport_size) return; } @@ -1486,26 +1494,6 @@ 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); @@ -1530,14 +1518,6 @@ 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) @@ -1608,8 +1588,21 @@ static void ImGui_ImplGlfw_SetWindowPos(ImGuiViewport* viewport, ImVec2 pos) { ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; vd->IgnoreWindowPosEventFrame = ImGui::GetFrameCount(); +#if GLFW_HAS_X11 + const int request_x = (int)pos.x; + const int request_y = (int)pos.y; + if (request_x != vd->LastWindowPosRequest[0] || request_y != vd->LastWindowPosRequest[1]) + { + vd->WindowPosMismatchFrameCount = 0; + vd->WindowPosMismatchStartTime = 0.0; + } + vd->LastWindowPosRequest[0] = request_x; + vd->LastWindowPosRequest[1] = request_y; + vd->WindowPosRequestPending = true; +#else vd->LastWindowPosRequest[0] = (int)pos.x; vd->LastWindowPosRequest[1] = (int)pos.y; +#endif glfwSetWindowPos(vd->Window, vd->LastWindowPosRequest[0], vd->LastWindowPosRequest[1]); } @@ -1638,8 +1631,21 @@ static void ImGui_ImplGlfw_SetWindowSize(ImGuiViewport* viewport, ImVec2 size) glfwSetWindowPos(vd->Window, vd->LastWindowPosRequest[0], vd->LastWindowPosRequest[1]); #endif vd->IgnoreWindowSizeEventFrame = ImGui::GetFrameCount(); +#if GLFW_HAS_X11 + const int request_w = (int)size.x; + const int request_h = (int)size.y; + if (request_w != vd->LastWindowSizeRequest[0] || request_h != vd->LastWindowSizeRequest[1]) + { + vd->WindowSizeMismatchFrameCount = 0; + vd->WindowSizeMismatchStartTime = 0.0; + } + vd->LastWindowSizeRequest[0] = request_w; + vd->LastWindowSizeRequest[1] = request_h; + vd->WindowSizeRequestPending = true; +#else vd->LastWindowSizeRequest[0] = (int)size.x; vd->LastWindowSizeRequest[1] = (int)size.y; +#endif glfwSetWindowSize(vd->Window, vd->LastWindowSizeRequest[0], vd->LastWindowSizeRequest[1]); } diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 35027cb00..543465d7a 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -75,8 +75,8 @@ Docking+Viewports Branch: 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: on X11, poll live geometry after a setter so completely rejected viewport + requests are synchronized. (#9356, #9398, #9442) - 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] diff --git a/imgui.cpp b/imgui.cpp index 4399b77f4..44cd7bf29 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -17213,23 +17213,42 @@ static void ImGui::UpdateViewportsNewFrame() // Viewport->WorkPos and WorkSize will be updated below if (viewport->PlatformRequestMove) { - const ImVec2 requested_pos = viewport->Pos; + const ImVec2 requested_pos = viewport->LastPlatformPos; 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. + // Rebase only axes adjusted by the window manager. Platform coordinates are integer pixels. + const bool platform_pos_adjusted = (ImTrunc(platform_pos.x) != ImTrunc(requested_pos.x) || ImTrunc(platform_pos.y) != ImTrunc(requested_pos.y)); ImGuiWindow* moving_root = g.MovingWindow ? g.MovingWindow->RootWindowDockTree : NULL; - if (moving_root && viewport->Window == moving_root && moving_root->ViewportOwned && IsMousePosValid(&g.IO.MousePos)) + const bool moving_owned_viewport = (moving_root && viewport->Window == moving_root && moving_root->ViewportOwned && IsMousePosValid(&g.IO.MousePos)); + if (moving_owned_viewport) { - if (platform_pos.x != requested_pos.x) + if (ImTrunc(platform_pos.x) != ImTrunc(requested_pos.x)) g.MovingWindowClickOffset.x = g.IO.MousePos.x - platform_pos.x; - if (platform_pos.y != requested_pos.y) + if (ImTrunc(platform_pos.y) != ImTrunc(requested_pos.y)) g.MovingWindowClickOffset.y = g.IO.MousePos.y - platform_pos.y; } viewport->Pos = viewport->LastPlatformPos = platform_pos; + if (platform_pos_adjusted && viewport->Window != NULL) + { + viewport->Window->Pos = platform_pos; + MarkIniSettingsDirty(viewport->Window); + } + if (!platform_pos_adjusted || moving_owned_viewport) + viewport->PlatformRequestMove = false; } if (viewport->PlatformRequestResize) - viewport->Size = viewport->LastPlatformSize = g.PlatformIO.Platform_GetWindowSize(viewport); + { + const ImVec2 requested_size = viewport->LastPlatformSize; + const ImVec2 platform_size = g.PlatformIO.Platform_GetWindowSize(viewport); + const bool platform_size_adjusted = (ImTrunc(platform_size.x) != ImTrunc(requested_size.x) || ImTrunc(platform_size.y) != ImTrunc(requested_size.y)); + viewport->Size = viewport->LastPlatformSize = platform_size; + if (platform_size_adjusted && viewport->Window != NULL) + { + viewport->Window->Size = viewport->Window->SizeFull = platform_size; + MarkIniSettingsDirty(viewport->Window); + } + if (!platform_size_adjusted) + viewport->PlatformRequestResize = false; + } if (g.PlatformIO.Platform_GetWindowFramebufferScale != NULL) viewport->FramebufferScale = g.PlatformIO.Platform_GetWindowFramebufferScale(viewport); } @@ -17780,18 +17799,18 @@ void ImGui::UpdatePlatformWindows() viewport->PlatformWindowCreated = true; } - // Consume requests received before this update. NewFrame already applied them to Pos/Size/LastPlatform*. - // A leftover flag must not suppress a later imgui-initiated Set (e.g. continued drag after a setter echo). - // Clear before invoking platform functions so synchronous setter callbacks survive until the next NewFrame(). + // Consume requests applied by NewFrame(), before setters can raise synchronous callbacks. + 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) + if ((viewport->LastPlatformPos.x != viewport->Pos.x || viewport->LastPlatformPos.y != viewport->Pos.y) && !platform_request_move) { g.PlatformIO.Platform_SetWindowPos(viewport, viewport->Pos); viewport->LastPlatformPos = viewport->Pos; } - if (viewport->LastPlatformSize.x != viewport->Size.x || viewport->LastPlatformSize.y != viewport->Size.y) + 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;