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 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-05 17:23:17 +02:00
co-authored by Claude Fable 5
parent b538dd545a
commit 2daeaa7078
4 changed files with 57 additions and 2 deletions
+43
View File
@@ -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: <shown> && !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() }
}
}
+1
View File
@@ -24,6 +24,7 @@ PanelSeparator 1.0 PanelSeparator.qml
PanelSlider 1.0 PanelSlider.qml PanelSlider 1.0 PanelSlider.qml
PanelToolTip 1.0 PanelToolTip.qml PanelToolTip 1.0 PanelToolTip.qml
PointerMoveGate 1.0 PointerMoveGate.qml PointerMoveGate 1.0 PointerMoveGate.qml
ScreenMoveRemap 1.0 ScreenMoveRemap.qml
PopupCard 1.0 PopupCard.qml PopupCard 1.0 PopupCard.qml
SearchableDropdown 1.0 SearchableDropdown.qml SearchableDropdown 1.0 SearchableDropdown.qml
TextField 1.0 TextField.qml TextField 1.0 TextField.qml
+7 -1
View File
@@ -5,6 +5,7 @@ import QtQuick
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Shapes import QtQuick.Shapes
import qs.Commons import qs.Commons
import qs.Ui
Item { Item {
id: root id: root
@@ -185,8 +186,13 @@ Item {
required property var modelData required property var modelData
screen: modelData screen: modelData
visible: true visible: !remapGuard.remapping
anchors { top: true; bottom: true; left: true; right: true } anchors { top: true; bottom: true; left: true; right: true }
ScreenMoveRemap {
id: remapGuard
window: panel
}
color: "transparent" color: "transparent"
// Keep render updates enabled. The background layer has been observed to // Keep render updates enabled. The background layer has been observed to
// lose its committed buffer while parked with updatesEnabled=false, // lose its committed buffer while parked with updatesEnabled=false,
+6 -1
View File
@@ -938,7 +938,12 @@ Item {
component BarPanel: PanelWindow { component BarPanel: PanelWindow {
id: barWindow id: barWindow
visible: !root.barHidden visible: !root.barHidden && !remapGuard.remapping
ScreenMoveRemap {
id: remapGuard
window: barWindow
}
anchors { anchors {
top: root.position === "top" || root.vertical top: root.position === "top" || root.vertical