From 33beded81825a6519585e64682c89c0ea871d65a Mon Sep 17 00:00:00 2001 From: Evan Mezeske Date: Fri, 12 Jun 2026 14:32:16 -0700 Subject: [PATCH] X11: Write WM_NORMAL_HINTS in a single call On Linux a window does not open where it is put. A DialogWindow launched with componentToCentreAround set, which should appear centred on its owner, opens at the centre of the screen under mutter or in a bottom corner under openbox, wherever the window manager's own placement policy decides to put it. The same property is also responsible for windows being resizable below their constrainer minimum. To reproduce, run on Xorg under a window manager that has a placement policy, move the main window well off the centre of the display, and open a dialog through DialogWindow::LaunchOptions with componentToCentreAround pointing at a component in that window. The dialog lands somewhere unrelated to its owner, and nothing reports anything wrong: the peer's own idea of its bounds stays correct right up until the manager's ConfigureNotify arrives and overwrites it. WM_NORMAL_HINTS on the dialog carries the size limits and no user position at all. XSetWMNormalHints replaces the whole property, and the peer writes that property from two places: setBounds writes USSize and USPosition, and updateConstraints writes PMinSize and PMaxSize. Whichever one runs second wipes the other one's fields, so a window never carries both. Without the constraints the window manager may resize a window below its minimum; without USPosition it may ignore the requested geometry entirely. Reordering the two writes only trades one symptom for the other. Both halves are now filled into one XSizeHints and written once, by updateSizeHints. updateWindowBounds calls it after refreshing the bounds and the scale factor rather than before, since the hints restate the geometry it has just read back. Also clamp the constrainer limits before scaling them. Constrainers commonly use the maximum int value to mean unbounded, so for scale factors greater than 1.0 the conversion back to int was undefined behaviour and advertised garbage maximum sizes; combined with a zero horizontal frame border that pinned windows to their minimum width on scaled displays. --- .../native/juce_Windowing_linux.cpp | 10 ++- .../native/juce_XWindowSystem_linux.cpp | 63 ++++++++++++------- .../native/juce_XWindowSystem_linux.h | 3 +- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_linux.cpp b/modules/juce_gui_basics/native/juce_Windowing_linux.cpp index 4db6a46607f..e56fbab1ec2 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_linux.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_linux.cpp @@ -410,13 +410,17 @@ class LinuxComponentPeer final : public ComponentPeer, return; } - if (isConstrainedNativeWindow()) - XWindowSystem::getInstance()->updateConstraints (windowH); - physicalBounds = XWindowSystem::getInstance()->getWindowBounds (windowH, parentWindow); fullScreen = XWindowSystem::getInstance()->isFullScreen (windowH); updateScaleFactorFromNewBounds (physicalBounds, true); + // After the bounds and scale factor have been refreshed, not before: + // the hints restate the window's geometry as well as its size limits + // (they share one property -- see XWindowSystem::updateSizeHints), and + // both are derived from the values just read back. + if (isConstrainedNativeWindow()) + XWindowSystem::getInstance()->updateSizeHints (windowH, *this, physicalBounds); + updateVBlankTimer(); } diff --git a/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp b/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp index f1bcf88d890..167ce63292a 100644 --- a/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp +++ b/modules/juce_gui_basics/native/juce_XWindowSystem_linux.cpp @@ -1902,20 +1902,19 @@ std::optional XWindowSystem::setBounds (::Window windowH, Rectang } } - updateConstraints (windowH, *peer); + // The user position/size and the min/max constraints have to go out in + // a single write: XSetWMNormalHints replaces the whole WM_NORMAL_HINTS + // property, so writing them separately makes whichever call comes + // second erase the other's fields. Dropping PMinSize/PMaxSize leaves + // the window manager free to resize the window below its minimum size; + // dropping USPosition/USSize leaves it free to ignore the requested + // position entirely and place the window wherever its own policy says + // (screen centre under mutter, a corner under openbox), which is how a + // dialog centred on its owner ends up somewhere else. + updateSizeHints (windowH, *peer, newBounds); XWindowSystemUtilities::ScopedXLock xLock; - if (auto hints = makeXFreePtr (X11Symbols::getInstance()->xAllocSizeHints())) - { - hints->flags = USSize | USPosition; - hints->x = newBounds.getX(); - hints->y = newBounds.getY(); - hints->width = newBounds.getWidth(); - hints->height = newBounds.getHeight(); - X11Symbols::getInstance()->xSetWMNormalHints (display, windowH, hints.get()); - } - const auto nativeWindowBorder = std::invoke ([&]() -> BorderSize { if (const auto& frameSize = peer->getFrameSizeIfPresent()) @@ -1998,23 +1997,27 @@ void XWindowSystem::startHostManagedResize (::Window windowH, unalignedPointerCast (&clientMsg)); } -void XWindowSystem::updateConstraints (::Window windowH) const -{ - if (auto* peer = getPeerFor (windowH)) - updateConstraints (windowH, *peer); -} - -void XWindowSystem::updateConstraints (::Window windowH, ComponentPeer& peer) const +// Writes the whole of WM_NORMAL_HINTS: the window's requested position and +// size, and the size limits its style flags or constrainer impose. These have +// to be written together because XSetWMNormalHints replaces the entire +// property -- see the call in setBounds() for what each half is holding up. +void XWindowSystem::updateSizeHints (::Window windowH, ComponentPeer& peer, Rectangle physicalBounds) const { XWindowSystemUtilities::ScopedXLock xLock; if (auto hints = makeXFreePtr (X11Symbols::getInstance()->xAllocSizeHints())) { + hints->flags = USSize | USPosition; + hints->x = physicalBounds.getX(); + hints->y = physicalBounds.getY(); + hints->width = physicalBounds.getWidth(); + hints->height = physicalBounds.getHeight(); + if ((peer.getStyleFlags() & ComponentPeer::windowIsResizable) == 0) { hints->min_width = hints->max_width = (int) (peer.getPlatformScaleFactor() * peer.getBounds().getWidth()); hints->min_height = hints->max_height = (int) (peer.getPlatformScaleFactor() * peer.getBounds().getHeight()); - hints->flags = PMinSize | PMaxSize; + hints->flags |= PMinSize | PMaxSize; } else if (auto* c = peer.getConstrainer()) { @@ -2029,11 +2032,23 @@ void XWindowSystem::updateConstraints (::Window windowH, ComponentPeer& peer) co const auto factor = peer.getPlatformScaleFactor(); const auto leftAndRight = windowBorder.getLeftAndRight(); const auto topAndBottom = windowBorder.getTopAndBottom(); - hints->min_width = jmax (1, (int) (factor * c->getMinimumWidth()) - leftAndRight); - hints->max_width = jmax (1, (int) (factor * c->getMaximumWidth()) - leftAndRight); - hints->min_height = jmax (1, (int) (factor * c->getMinimumHeight()) - topAndBottom); - hints->max_height = jmax (1, (int) (factor * c->getMaximumHeight()) - topAndBottom); - hints->flags = PMinSize | PMaxSize; + + // Constrainers commonly use the maximum int value to mean + // "unbounded", so the scaled limit must be clamped before the + // conversion back to int: for scale factors greater than 1.0 the + // conversion is otherwise undefined behaviour, and the garbage + // maximum sizes it produced made some window managers pin the + // window to its minimum size on scaled displays. + const auto scaledLimit = [factor] (int limit, int border) + { + const auto scaled = jmin ((double) std::numeric_limits::max(), factor * limit); + return jmax (1, (int) scaled - border); + }; + hints->min_width = scaledLimit (c->getMinimumWidth(), leftAndRight); + hints->max_width = scaledLimit (c->getMaximumWidth(), leftAndRight); + hints->min_height = scaledLimit (c->getMinimumHeight(), topAndBottom); + hints->max_height = scaledLimit (c->getMaximumHeight(), topAndBottom); + hints->flags |= PMinSize | PMaxSize; } X11Symbols::getInstance()->xSetWMNormalHints (display, windowH, hints.get()); diff --git a/modules/juce_gui_basics/native/juce_XWindowSystem_linux.h b/modules/juce_gui_basics/native/juce_XWindowSystem_linux.h index e419e88a31b..f0a64cb59bc 100644 --- a/modules/juce_gui_basics/native/juce_XWindowSystem_linux.h +++ b/modules/juce_gui_basics/native/juce_XWindowSystem_linux.h @@ -189,7 +189,7 @@ class XWindowSystem : public DeletedAtShutdown void setIcon (::Window , const Image&) const; void setVisible (::Window, bool shouldBeVisible) const; [[nodiscard]] std::optional setBounds (::Window, Rectangle, bool fullScreen) const; - void updateConstraints (::Window) const; + void updateSizeHints (::Window, ComponentPeer&, Rectangle physicalBounds) const; ComponentPeer::OptionalBorderSize getBorderSize (::Window) const; Rectangle getWindowBounds (::Window, ::Window parentWindow); @@ -344,7 +344,6 @@ class XWindowSystem : public DeletedAtShutdown void dismissBlockingModals (LinuxComponentPeer*) const; void dismissBlockingModals (LinuxComponentPeer*, const XConfigureEvent&) const; - void updateConstraints (::Window, ComponentPeer&) const; ::Window findTopLevelWindowOf (::Window) const;