From 2daeaa7078db0c004ab7b778e9f25d8b30615cba Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 5 Aug 2026 17:23:17 +0200 Subject: [PATCH] Remap bar and background surfaces when their monitor moves Hyprland leaves an already-mapped layer surface at its old global position when its monitor moves within the layout: undocking disables the internal panel, the external monitor shifts to x=0, and the bar and background keep rendering at the old offset until unmapped and remapped. Watch each screen's origin and briefly unmap the window when it moves so the compositor re-places the surface at the monitor's new origin. Co-Authored-By: Claude Fable 5 --- shell/Ui/ScreenMoveRemap.qml | 43 +++++++++++++++++++++++++ shell/Ui/qmldir | 1 + shell/plugins/background/Background.qml | 8 ++++- shell/plugins/bar/Bar.qml | 7 +++- 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 shell/Ui/ScreenMoveRemap.qml diff --git a/shell/Ui/ScreenMoveRemap.qml b/shell/Ui/ScreenMoveRemap.qml new file mode 100644 index 00000000..44d2308a --- /dev/null +++ b/shell/Ui/ScreenMoveRemap.qml @@ -0,0 +1,43 @@ +import QtQuick + +// Hyprland leaves an already-mapped layer surface at its old global position +// when its monitor moves within the layout: undocking disables the internal +// panel, the external monitor shifts to x=0, and long-lived surfaces such as +// the bar and background keep rendering at the old offset — or entirely +// off-screen — until they are unmapped and remapped. Watch the screen's +// origin and pulse `remapping` when it moves; the owning window folds that +// into its `visible` binding so the compositor re-places the surface at the +// monitor's new origin. +Item { + id: root + + required property var window + readonly property var screen: window ? window.screen : null + + // Fold into the window's binding: visible: && !guard.remapping + property bool remapping: false + + visible: false + + // A layout reshuffle can move the monitor more than once before it lands. + // Let the positions settle before the single remap pulse. + Timer { + id: settleTimer + interval: 200 + onTriggered: root.remapping = true + } + + // Hold the surface unmapped for a beat so the compositor processes the + // unmap before the remap instead of coalescing them into a no-op. + Timer { + interval: 50 + running: root.remapping + onTriggered: root.remapping = false + } + + Connections { + target: root.screen + function onXChanged() { settleTimer.restart() } + function onYChanged() { settleTimer.restart() } + } +} diff --git a/shell/Ui/qmldir b/shell/Ui/qmldir index 92d39848..aaf9892b 100644 --- a/shell/Ui/qmldir +++ b/shell/Ui/qmldir @@ -24,6 +24,7 @@ PanelSeparator 1.0 PanelSeparator.qml PanelSlider 1.0 PanelSlider.qml PanelToolTip 1.0 PanelToolTip.qml PointerMoveGate 1.0 PointerMoveGate.qml +ScreenMoveRemap 1.0 ScreenMoveRemap.qml PopupCard 1.0 PopupCard.qml SearchableDropdown 1.0 SearchableDropdown.qml TextField 1.0 TextField.qml diff --git a/shell/plugins/background/Background.qml b/shell/plugins/background/Background.qml index 151e7baf..21e1b0a9 100644 --- a/shell/plugins/background/Background.qml +++ b/shell/plugins/background/Background.qml @@ -5,6 +5,7 @@ import QtQuick import QtQuick.Effects import QtQuick.Shapes import qs.Commons +import qs.Ui Item { id: root @@ -185,8 +186,13 @@ Item { required property var modelData screen: modelData - visible: true + visible: !remapGuard.remapping anchors { top: true; bottom: true; left: true; right: true } + + ScreenMoveRemap { + id: remapGuard + window: panel + } color: "transparent" // Keep render updates enabled. The background layer has been observed to // lose its committed buffer while parked with updatesEnabled=false, diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index c5be1ff6..0d563324 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -938,7 +938,12 @@ Item { component BarPanel: PanelWindow { id: barWindow - visible: !root.barHidden + visible: !root.barHidden && !remapGuard.remapping + + ScreenMoveRemap { + id: remapGuard + window: barWindow + } anchors { top: root.position === "top" || root.vertical