detached 7ef926f

feat(xwm): convert Place through Space and track override-redirect windows

SetLayout feeds Space; Place stores layout coordinates and converts at flush.
Mapped override windows read their type once, pair, and report layout rects
through OverrideMapped/Moved/Gone. FocusIn ignores grab modes and unmanaged
windows; WM_CHANGE_STATE is ignored.
commit
7ef926ff6e5ebcb72e13e87a0ac1c93db1d75e33
author
brice · · 2026-10-22 15:14 UTC
committer
brice · · 2026-10-22 17:54 UTC
parent
5a832c8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
12 files · +668 -6
M Makefile +1 -1
@@ -70,7 +70,7 @@ adapter-imports:
7070 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
7171 # Fast, mandatory guards for tiled content publications and SHM copies.
7272 perf-check:
73- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations)$$' -count=1
73+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations)$$' -count=1
7474 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1
7575 check: vet test arch fakes-check log-check perf-check
7676
@@ -70,7 +70,7 @@ adapter-imports:
70 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }70 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
71 # Fast, mandatory guards for tiled content publications and SHM copies.71 # Fast, mandatory guards for tiled content publications and SHM copies.
72 perf-check:72 perf-check:
73- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations)$$' -count=173+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations)$$' -count=1
74 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=174 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1
75 check: vet test arch fakes-check log-check perf-check75 check: vet test arch fakes-check log-check perf-check
76 76
M internal/adapters/wayland/x11window.go +10 -0
@@ -305,3 +305,13 @@ func (x *x11Window) tryMap() {
305305 surf.emitInput()
306306 }
307307 }
308+
309+// OverrideMapped is a no-op until X7.5 wires override-redirect windows.
310+func (h hostImpl) OverrideMapped(slot xwm.Slot, xid uint32, surfaceID uint64, x, y, w, h2 int, noInput bool) {
311+}
312+
313+// OverrideMoved is a no-op until X7.5 wires override-redirect windows.
314+func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) {}
315+
316+// OverrideGone is a no-op until X7.5 wires override-redirect windows.
317+func (h hostImpl) OverrideGone(slot xwm.Slot, destroyed bool) {}
@@ -305,3 +305,13 @@ func (x *x11Window) tryMap() {
305 surf.emitInput()305 surf.emitInput()
306 }306 }
307 }307 }
308+
309+// OverrideMapped is a no-op until X7.5 wires override-redirect windows.
310+func (h hostImpl) OverrideMapped(slot xwm.Slot, xid uint32, surfaceID uint64, x, y, w, h2 int, noInput bool) {
311+}
312+
313+// OverrideMoved is a no-op until X7.5 wires override-redirect windows.
314+func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) {}
315+
316+// OverrideGone is a no-op until X7.5 wires override-redirect windows.
317+func (h hostImpl) OverrideGone(slot xwm.Slot, destroyed bool) {}
M internal/adapters/wayland/xwm/configure.go +40 -1
@@ -87,11 +87,46 @@ func (x *XWM) Place(s Slot, px, py int) {
8787 if r == nil {
8888 return
8989 }
90- r.pend.x, r.pend.y = int32(px), int32(py)
90+ r.lx, r.ly, r.placed = int32(px), int32(py), true
91+ r.pend.x, r.pend.y = x.toX(r)
9192 x.markDirty(s, r)
9293 x.wake()
9394 }
9495
96+// toX converts the layout position of r to X coordinates.
97+func (x *XWM) toX(r *record) (px, py int32) {
98+ ix, iy := x.space.ToX(int(r.lx), int(r.ly))
99+ return int32(ix), int32(iy)
100+}
101+
102+// SetLayout installs the output layout that X coordinates derive from. When it
103+// changes, placed managed windows are repositioned and mapped override windows
104+// are re-reported in layout coordinates. Display goroutine only.
105+func (x *XWM) SetLayout(l ports.Layout) {
106+ if x.closed {
107+ return
108+ }
109+ changed := x.space.Set(l)
110+ if n := x.space.Dropped(); n > 0 && !x.warnedSpace {
111+ x.warnedSpace = true
112+ x.log.Warn().Int("dropped", n).Int("max", maxRegions).Msg("more outputs than the X11 layout holds, extra outputs ignored")
113+ }
114+ if !changed {
115+ return
116+ }
117+ for i := range x.tab.recs {
118+ r, s := &x.tab.recs[i], Slot(i+1)
119+ switch {
120+ case r.kind == kindManaged && r.placed:
121+ r.pend.x, r.pend.y = x.toX(r)
122+ x.markDirty(s, r)
123+ case r.kind == kindUnmanaged && r.ovPaired:
124+ x.queueOverride(s, r)
125+ }
126+ }
127+ x.wake()
128+}
129+
95130 // flushConfigures sends the pending configures, once per display iteration,
96131 // then what focus left to do.
97132 func (x *XWM) flushConfigures() {
@@ -101,6 +136,7 @@ func (x *XWM) flushConfigures() {
101136 }
102137 }
103138 x.dirty = x.dirty[:0]
139+ x.flushOverrides()
104140 x.flushFocus()
105141 }
106142
@@ -262,5 +298,8 @@ func (x *XWM) onConfigureNotify(ev *xturbo.Event) {
262298 }
263299 if r := x.tab.rec(x.tab.lookup(uint32(e.Window()))); r != nil && r.kind != kindFree {
264300 r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height())
301+ if r.kind == kindUnmanaged && r.ovPaired {
302+ x.queueOverride(x.tab.lookup(r.xid), r)
303+ }
265304 }
266305 }
@@ -87,11 +87,46 @@ func (x *XWM) Place(s Slot, px, py int) {
87 if r == nil {87 if r == nil {
88 return88 return
89 }89 }
90- r.pend.x, r.pend.y = int32(px), int32(py)90+ r.lx, r.ly, r.placed = int32(px), int32(py), true
91+ r.pend.x, r.pend.y = x.toX(r)
91 x.markDirty(s, r)92 x.markDirty(s, r)
92 x.wake()93 x.wake()
93 }94 }
94 95
96+// toX converts the layout position of r to X coordinates.
97+func (x *XWM) toX(r *record) (px, py int32) {
98+ ix, iy := x.space.ToX(int(r.lx), int(r.ly))
99+ return int32(ix), int32(iy)
100+}
101+
102+// SetLayout installs the output layout that X coordinates derive from. When it
103+// changes, placed managed windows are repositioned and mapped override windows
104+// are re-reported in layout coordinates. Display goroutine only.
105+func (x *XWM) SetLayout(l ports.Layout) {
106+ if x.closed {
107+ return
108+ }
109+ changed := x.space.Set(l)
110+ if n := x.space.Dropped(); n > 0 && !x.warnedSpace {
111+ x.warnedSpace = true
112+ x.log.Warn().Int("dropped", n).Int("max", maxRegions).Msg("more outputs than the X11 layout holds, extra outputs ignored")
113+ }
114+ if !changed {
115+ return
116+ }
117+ for i := range x.tab.recs {
118+ r, s := &x.tab.recs[i], Slot(i+1)
119+ switch {
120+ case r.kind == kindManaged && r.placed:
121+ r.pend.x, r.pend.y = x.toX(r)
122+ x.markDirty(s, r)
123+ case r.kind == kindUnmanaged && r.ovPaired:
124+ x.queueOverride(s, r)
125+ }
126+ }
127+ x.wake()
128+}
129+
95 // flushConfigures sends the pending configures, once per display iteration,130 // flushConfigures sends the pending configures, once per display iteration,
96 // then what focus left to do.131 // then what focus left to do.
97 func (x *XWM) flushConfigures() {132 func (x *XWM) flushConfigures() {
@@ -101,6 +136,7 @@ func (x *XWM) flushConfigures() {
101 }136 }
102 }137 }
103 x.dirty = x.dirty[:0]138 x.dirty = x.dirty[:0]
139+ x.flushOverrides()
104 x.flushFocus()140 x.flushFocus()
105 }141 }
106 142
@@ -262,5 +298,8 @@ func (x *XWM) onConfigureNotify(ev *xturbo.Event) {
262 }298 }
263 if r := x.tab.rec(x.tab.lookup(uint32(e.Window()))); r != nil && r.kind != kindFree {299 if r := x.tab.rec(x.tab.lookup(uint32(e.Window()))); r != nil && r.kind != kindFree {
264 r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height())300 r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height())
301+ if r.kind == kindUnmanaged && r.ovPaired {
302+ x.queueOverride(x.tab.lookup(r.xid), r)
303+ }
265 }304 }
266 }305 }
M internal/adapters/wayland/xwm/events.go +10 -1
@@ -20,6 +20,7 @@ const (
2020 kindRootMask
2121 kindVersion
2222 kindTree
23+ kindOverrideType
2324 )
2425
2526 // onEvent dispatches an X event. Before Attach there is no host: only the
@@ -109,6 +110,8 @@ func (x *XWM) onReply(r *xturbo.Reply) {
109110 x.onGeomReply(s, rec, r)
110111 case kindClientIds:
111112 x.onClientIdsReply(s, rec, r)
113+ case kindOverrideType:
114+ x.onOverrideType(s, rec, r)
112115 }
113116 }
114117
@@ -127,7 +130,7 @@ func (x *XWM) onError(e *xturbo.XError) {
127130 if e.Code == xproto.BadAccess {
128131 x.bootErr = ErrWMExists
129132 }
130- case kindProp, kindGeom, kindClientIds, kindConfigure, kindFocus:
133+ case kindProp, kindGeom, kindClientIds, kindConfigure, kindFocus, kindOverrideType:
131134 // Usually BadWindow: the window died between request and reply and
132135 // DestroyNotify follows. Whatever the code, the awaited reply will
133136 // never come, so the map must not wait for it.
@@ -143,6 +146,11 @@ func (x *XWM) onError(e *xturbo.XError) {
143146 rec.pending &^= propGeom
144147 case kindClientIds:
145148 rec.pending &^= propClientIDs
149+ case kindOverrideType:
150+ if e.Code != xproto.BadWindow { // typed unknown: the window still shows
151+ x.overrideReady(s)
152+ }
153+ return
146154 default:
147155 return
148156 }
@@ -226,6 +234,7 @@ func (x *XWM) onAttrReply(rec *record, r *xturbo.Reply) {
226234 if a.OverrideRedirect() {
227235 rec.override = true
228236 rec.kind = kindUnmanaged
237+ rec.mapped = a.MapState() == xproto.MapStateViewable
229238 x.stats.UnmanagedSeen++
230239 return
231240 }
@@ -20,6 +20,7 @@ const (
20 kindRootMask20 kindRootMask
21 kindVersion21 kindVersion
22 kindTree22 kindTree
23+ kindOverrideType
23 )24 )
24 25
25 // onEvent dispatches an X event. Before Attach there is no host: only the26 // onEvent dispatches an X event. Before Attach there is no host: only the
@@ -109,6 +110,8 @@ func (x *XWM) onReply(r *xturbo.Reply) {
109 x.onGeomReply(s, rec, r)110 x.onGeomReply(s, rec, r)
110 case kindClientIds:111 case kindClientIds:
111 x.onClientIdsReply(s, rec, r)112 x.onClientIdsReply(s, rec, r)
113+ case kindOverrideType:
114+ x.onOverrideType(s, rec, r)
112 }115 }
113 }116 }
114 117
@@ -127,7 +130,7 @@ func (x *XWM) onError(e *xturbo.XError) {
127 if e.Code == xproto.BadAccess {130 if e.Code == xproto.BadAccess {
128 x.bootErr = ErrWMExists131 x.bootErr = ErrWMExists
129 }132 }
130- case kindProp, kindGeom, kindClientIds, kindConfigure, kindFocus:133+ case kindProp, kindGeom, kindClientIds, kindConfigure, kindFocus, kindOverrideType:
131 // Usually BadWindow: the window died between request and reply and134 // Usually BadWindow: the window died between request and reply and
132 // DestroyNotify follows. Whatever the code, the awaited reply will135 // DestroyNotify follows. Whatever the code, the awaited reply will
133 // never come, so the map must not wait for it.136 // never come, so the map must not wait for it.
@@ -143,6 +146,11 @@ func (x *XWM) onError(e *xturbo.XError) {
143 rec.pending &^= propGeom146 rec.pending &^= propGeom
144 case kindClientIds:147 case kindClientIds:
145 rec.pending &^= propClientIDs148 rec.pending &^= propClientIDs
149+ case kindOverrideType:
150+ if e.Code != xproto.BadWindow { // typed unknown: the window still shows
151+ x.overrideReady(s)
152+ }
153+ return
146 default:154 default:
147 return155 return
148 }156 }
@@ -226,6 +234,7 @@ func (x *XWM) onAttrReply(rec *record, r *xturbo.Reply) {
226 if a.OverrideRedirect() {234 if a.OverrideRedirect() {
227 rec.override = true235 rec.override = true
228 rec.kind = kindUnmanaged236 rec.kind = kindUnmanaged
237+ rec.mapped = a.MapState() == xproto.MapStateViewable
229 x.stats.UnmanagedSeen++238 x.stats.UnmanagedSeen++
230 return239 return
231 }240 }
M internal/adapters/wayland/xwm/focus.go +12 -0
@@ -21,10 +21,18 @@ func (x *XWM) onFocusIn(ev *xturbo.Event) {
2121 if e.Detail() == xproto.NotifyDetailPointer {
2222 return
2323 }
24+ // Keyboard grabs are how menus steal focus legitimately (wlroots does
25+ // the same): fighting them would close every Steam menu.
26+ if e.Mode() == xproto.NotifyModeGrab || e.Mode() == xproto.NotifyModeUngrab {
27+ return
28+ }
2429 s := x.tab.lookup(uint32(e.Event()))
2530 if s == 0 || s == x.active {
2631 return
2732 }
33+ if r := x.tab.rec(s); r.kind == kindUnmanaged {
34+ return
35+ }
2836 x.refocus = true
2937 x.wake()
3038 }
@@ -173,6 +181,10 @@ func (x *XWM) forgetWindow(s Slot) {
173181 return
174182 }
175183 x.dropWindow(s)
184+ if r.kind == kindUnmanaged && r.ovPaired && x.host != nil {
185+ r.ovPaired = false
186+ x.host.OverrideGone(s, true)
187+ }
176188 if r.kind == kindManaged && r.id != 0 && x.host != nil {
177189 r.id = 0
178190 x.host.Dissociate(s, true)
@@ -21,10 +21,18 @@ func (x *XWM) onFocusIn(ev *xturbo.Event) {
21 if e.Detail() == xproto.NotifyDetailPointer {21 if e.Detail() == xproto.NotifyDetailPointer {
22 return22 return
23 }23 }
24+ // Keyboard grabs are how menus steal focus legitimately (wlroots does
25+ // the same): fighting them would close every Steam menu.
26+ if e.Mode() == xproto.NotifyModeGrab || e.Mode() == xproto.NotifyModeUngrab {
27+ return
28+ }
24 s := x.tab.lookup(uint32(e.Event()))29 s := x.tab.lookup(uint32(e.Event()))
25 if s == 0 || s == x.active {30 if s == 0 || s == x.active {
26 return31 return
27 }32 }
33+ if r := x.tab.rec(s); r.kind == kindUnmanaged {
34+ return
35+ }
28 x.refocus = true36 x.refocus = true
29 x.wake()37 x.wake()
30 }38 }
@@ -173,6 +181,10 @@ func (x *XWM) forgetWindow(s Slot) {
173 return181 return
174 }182 }
175 x.dropWindow(s)183 x.dropWindow(s)
184+ if r.kind == kindUnmanaged && r.ovPaired && x.host != nil {
185+ r.ovPaired = false
186+ x.host.OverrideGone(s, true)
187+ }
176 if r.kind == kindManaged && r.id != 0 && x.host != nil {188 if r.kind == kindManaged && r.id != 0 && x.host != nil {
177 r.id = 0189 r.id = 0
178 x.host.Dissociate(s, true)190 x.host.Dissociate(s, true)
M internal/adapters/wayland/xwm/host.go +8 -0
@@ -40,6 +40,14 @@ type host interface {
4040 // Fullscreen forwards a fullscreen request (_NET_WM_STATE client message
4141 // or initial state); the host emits WindowFullscreenRequest.
4242 Fullscreen(slot Slot, on bool)
43+
44+ // OverrideMapped reports an override-redirect window that is mapped,
45+ // typed and paired with its wl_surface. Rect is in layout coordinates.
46+ OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x, y, w, h int, noInput bool)
47+ // OverrideMoved reports a changed layout rect of a mapped override window.
48+ OverrideMoved(slot Slot, x, y, w, h int)
49+ // OverrideGone ends a mapped override window: unmapped, or destroyed.
50+ OverrideGone(slot Slot, destroyed bool)
4351 }
4452
4553 // fdSource is the display's event source for the X socket (*server.Source).
@@ -40,6 +40,14 @@ type host interface {
40 // Fullscreen forwards a fullscreen request (_NET_WM_STATE client message40 // Fullscreen forwards a fullscreen request (_NET_WM_STATE client message
41 // or initial state); the host emits WindowFullscreenRequest.41 // or initial state); the host emits WindowFullscreenRequest.
42 Fullscreen(slot Slot, on bool)42 Fullscreen(slot Slot, on bool)
43+
44+ // OverrideMapped reports an override-redirect window that is mapped,
45+ // typed and paired with its wl_surface. Rect is in layout coordinates.
46+ OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x, y, w, h int, noInput bool)
47+ // OverrideMoved reports a changed layout rect of a mapped override window.
48+ OverrideMoved(slot Slot, x, y, w, h int)
49+ // OverrideGone ends a mapped override window: unmapped, or destroyed.
50+ OverrideGone(slot Slot, destroyed bool)
43 }51 }
44 52
45 // fdSource is the display's event source for the X socket (*server.Source).53 // fdSource is the display's event source for the X socket (*server.Source).
M internal/adapters/wayland/xwm/host_mock_test.go +192 -0
@@ -286,6 +286,198 @@ func (_c *mockhost_Fullscreen_Call) RunAndReturn(run func(slot Slot, on bool)) *
286286 return _c
287287 }
288288
289+// OverrideGone provides a mock function for the type mockhost
290+func (_mock *mockhost) OverrideGone(slot Slot, destroyed bool) {
291+ _mock.Called(slot, destroyed)
292+ return
293+}
294+
295+// mockhost_OverrideGone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideGone'
296+type mockhost_OverrideGone_Call struct {
297+ *mock.Call
298+}
299+
300+// OverrideGone is a helper method to define mock.On call
301+// - slot Slot
302+// - destroyed bool
303+func (_e *mockhost_Expecter) OverrideGone(slot any, destroyed any) *mockhost_OverrideGone_Call {
304+ return &mockhost_OverrideGone_Call{Call: _e.mock.On("OverrideGone", slot, destroyed)}
305+}
306+
307+func (_c *mockhost_OverrideGone_Call) Run(run func(slot Slot, destroyed bool)) *mockhost_OverrideGone_Call {
308+ _c.Call.Run(func(args mock.Arguments) {
309+ var arg0 Slot
310+ if args[0] != nil {
311+ arg0 = args[0].(Slot)
312+ }
313+ var arg1 bool
314+ if args[1] != nil {
315+ arg1 = args[1].(bool)
316+ }
317+ run(
318+ arg0,
319+ arg1,
320+ )
321+ })
322+ return _c
323+}
324+
325+func (_c *mockhost_OverrideGone_Call) Return() *mockhost_OverrideGone_Call {
326+ _c.Call.Return()
327+ return _c
328+}
329+
330+func (_c *mockhost_OverrideGone_Call) RunAndReturn(run func(slot Slot, destroyed bool)) *mockhost_OverrideGone_Call {
331+ _c.Run(run)
332+ return _c
333+}
334+
335+// OverrideMapped provides a mock function for the type mockhost
336+func (_mock *mockhost) OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool) {
337+ _mock.Called(slot, xid, surfaceID, x, y, w, h, noInput)
338+ return
339+}
340+
341+// mockhost_OverrideMapped_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMapped'
342+type mockhost_OverrideMapped_Call struct {
343+ *mock.Call
344+}
345+
346+// OverrideMapped is a helper method to define mock.On call
347+// - slot Slot
348+// - xid uint32
349+// - surfaceID uint64
350+// - x int
351+// - y int
352+// - w int
353+// - h int
354+// - noInput bool
355+func (_e *mockhost_Expecter) OverrideMapped(slot any, xid any, surfaceID any, x any, y any, w any, h any, noInput any) *mockhost_OverrideMapped_Call {
356+ return &mockhost_OverrideMapped_Call{Call: _e.mock.On("OverrideMapped", slot, xid, surfaceID, x, y, w, h, noInput)}
357+}
358+
359+func (_c *mockhost_OverrideMapped_Call) Run(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call {
360+ _c.Call.Run(func(args mock.Arguments) {
361+ var arg0 Slot
362+ if args[0] != nil {
363+ arg0 = args[0].(Slot)
364+ }
365+ var arg1 uint32
366+ if args[1] != nil {
367+ arg1 = args[1].(uint32)
368+ }
369+ var arg2 uint64
370+ if args[2] != nil {
371+ arg2 = args[2].(uint64)
372+ }
373+ var arg3 int
374+ if args[3] != nil {
375+ arg3 = args[3].(int)
376+ }
377+ var arg4 int
378+ if args[4] != nil {
379+ arg4 = args[4].(int)
380+ }
381+ var arg5 int
382+ if args[5] != nil {
383+ arg5 = args[5].(int)
384+ }
385+ var arg6 int
386+ if args[6] != nil {
387+ arg6 = args[6].(int)
388+ }
389+ var arg7 bool
390+ if args[7] != nil {
391+ arg7 = args[7].(bool)
392+ }
393+ run(
394+ arg0,
395+ arg1,
396+ arg2,
397+ arg3,
398+ arg4,
399+ arg5,
400+ arg6,
401+ arg7,
402+ )
403+ })
404+ return _c
405+}
406+
407+func (_c *mockhost_OverrideMapped_Call) Return() *mockhost_OverrideMapped_Call {
408+ _c.Call.Return()
409+ return _c
410+}
411+
412+func (_c *mockhost_OverrideMapped_Call) RunAndReturn(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call {
413+ _c.Run(run)
414+ return _c
415+}
416+
417+// OverrideMoved provides a mock function for the type mockhost
418+func (_mock *mockhost) OverrideMoved(slot Slot, x int, y int, w int, h int) {
419+ _mock.Called(slot, x, y, w, h)
420+ return
421+}
422+
423+// mockhost_OverrideMoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMoved'
424+type mockhost_OverrideMoved_Call struct {
425+ *mock.Call
426+}
427+
428+// OverrideMoved is a helper method to define mock.On call
429+// - slot Slot
430+// - x int
431+// - y int
432+// - w int
433+// - h int
434+func (_e *mockhost_Expecter) OverrideMoved(slot any, x any, y any, w any, h any) *mockhost_OverrideMoved_Call {
435+ return &mockhost_OverrideMoved_Call{Call: _e.mock.On("OverrideMoved", slot, x, y, w, h)}
436+}
437+
438+func (_c *mockhost_OverrideMoved_Call) Run(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call {
439+ _c.Call.Run(func(args mock.Arguments) {
440+ var arg0 Slot
441+ if args[0] != nil {
442+ arg0 = args[0].(Slot)
443+ }
444+ var arg1 int
445+ if args[1] != nil {
446+ arg1 = args[1].(int)
447+ }
448+ var arg2 int
449+ if args[2] != nil {
450+ arg2 = args[2].(int)
451+ }
452+ var arg3 int
453+ if args[3] != nil {
454+ arg3 = args[3].(int)
455+ }
456+ var arg4 int
457+ if args[4] != nil {
458+ arg4 = args[4].(int)
459+ }
460+ run(
461+ arg0,
462+ arg1,
463+ arg2,
464+ arg3,
465+ arg4,
466+ )
467+ })
468+ return _c
469+}
470+
471+func (_c *mockhost_OverrideMoved_Call) Return() *mockhost_OverrideMoved_Call {
472+ _c.Call.Return()
473+ return _c
474+}
475+
476+func (_c *mockhost_OverrideMoved_Call) RunAndReturn(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call {
477+ _c.Run(run)
478+ return _c
479+}
480+
289481 // Parent provides a mock function for the type mockhost
290482 func (_mock *mockhost) Parent(slot Slot, parent Slot) {
291483 _mock.Called(slot, parent)
@@ -286,6 +286,198 @@ func (_c *mockhost_Fullscreen_Call) RunAndReturn(run func(slot Slot, on bool)) *
286 return _c286 return _c
287 }287 }
288 288
289+// OverrideGone provides a mock function for the type mockhost
290+func (_mock *mockhost) OverrideGone(slot Slot, destroyed bool) {
291+ _mock.Called(slot, destroyed)
292+ return
293+}
294+
295+// mockhost_OverrideGone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideGone'
296+type mockhost_OverrideGone_Call struct {
297+ *mock.Call
298+}
299+
300+// OverrideGone is a helper method to define mock.On call
301+// - slot Slot
302+// - destroyed bool
303+func (_e *mockhost_Expecter) OverrideGone(slot any, destroyed any) *mockhost_OverrideGone_Call {
304+ return &mockhost_OverrideGone_Call{Call: _e.mock.On("OverrideGone", slot, destroyed)}
305+}
306+
307+func (_c *mockhost_OverrideGone_Call) Run(run func(slot Slot, destroyed bool)) *mockhost_OverrideGone_Call {
308+ _c.Call.Run(func(args mock.Arguments) {
309+ var arg0 Slot
310+ if args[0] != nil {
311+ arg0 = args[0].(Slot)
312+ }
313+ var arg1 bool
314+ if args[1] != nil {
315+ arg1 = args[1].(bool)
316+ }
317+ run(
318+ arg0,
319+ arg1,
320+ )
321+ })
322+ return _c
323+}
324+
325+func (_c *mockhost_OverrideGone_Call) Return() *mockhost_OverrideGone_Call {
326+ _c.Call.Return()
327+ return _c
328+}
329+
330+func (_c *mockhost_OverrideGone_Call) RunAndReturn(run func(slot Slot, destroyed bool)) *mockhost_OverrideGone_Call {
331+ _c.Run(run)
332+ return _c
333+}
334+
335+// OverrideMapped provides a mock function for the type mockhost
336+func (_mock *mockhost) OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool) {
337+ _mock.Called(slot, xid, surfaceID, x, y, w, h, noInput)
338+ return
339+}
340+
341+// mockhost_OverrideMapped_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMapped'
342+type mockhost_OverrideMapped_Call struct {
343+ *mock.Call
344+}
345+
346+// OverrideMapped is a helper method to define mock.On call
347+// - slot Slot
348+// - xid uint32
349+// - surfaceID uint64
350+// - x int
351+// - y int
352+// - w int
353+// - h int
354+// - noInput bool
355+func (_e *mockhost_Expecter) OverrideMapped(slot any, xid any, surfaceID any, x any, y any, w any, h any, noInput any) *mockhost_OverrideMapped_Call {
356+ return &mockhost_OverrideMapped_Call{Call: _e.mock.On("OverrideMapped", slot, xid, surfaceID, x, y, w, h, noInput)}
357+}
358+
359+func (_c *mockhost_OverrideMapped_Call) Run(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call {
360+ _c.Call.Run(func(args mock.Arguments) {
361+ var arg0 Slot
362+ if args[0] != nil {
363+ arg0 = args[0].(Slot)
364+ }
365+ var arg1 uint32
366+ if args[1] != nil {
367+ arg1 = args[1].(uint32)
368+ }
369+ var arg2 uint64
370+ if args[2] != nil {
371+ arg2 = args[2].(uint64)
372+ }
373+ var arg3 int
374+ if args[3] != nil {
375+ arg3 = args[3].(int)
376+ }
377+ var arg4 int
378+ if args[4] != nil {
379+ arg4 = args[4].(int)
380+ }
381+ var arg5 int
382+ if args[5] != nil {
383+ arg5 = args[5].(int)
384+ }
385+ var arg6 int
386+ if args[6] != nil {
387+ arg6 = args[6].(int)
388+ }
389+ var arg7 bool
390+ if args[7] != nil {
391+ arg7 = args[7].(bool)
392+ }
393+ run(
394+ arg0,
395+ arg1,
396+ arg2,
397+ arg3,
398+ arg4,
399+ arg5,
400+ arg6,
401+ arg7,
402+ )
403+ })
404+ return _c
405+}
406+
407+func (_c *mockhost_OverrideMapped_Call) Return() *mockhost_OverrideMapped_Call {
408+ _c.Call.Return()
409+ return _c
410+}
411+
412+func (_c *mockhost_OverrideMapped_Call) RunAndReturn(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call {
413+ _c.Run(run)
414+ return _c
415+}
416+
417+// OverrideMoved provides a mock function for the type mockhost
418+func (_mock *mockhost) OverrideMoved(slot Slot, x int, y int, w int, h int) {
419+ _mock.Called(slot, x, y, w, h)
420+ return
421+}
422+
423+// mockhost_OverrideMoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMoved'
424+type mockhost_OverrideMoved_Call struct {
425+ *mock.Call
426+}
427+
428+// OverrideMoved is a helper method to define mock.On call
429+// - slot Slot
430+// - x int
431+// - y int
432+// - w int
433+// - h int
434+func (_e *mockhost_Expecter) OverrideMoved(slot any, x any, y any, w any, h any) *mockhost_OverrideMoved_Call {
435+ return &mockhost_OverrideMoved_Call{Call: _e.mock.On("OverrideMoved", slot, x, y, w, h)}
436+}
437+
438+func (_c *mockhost_OverrideMoved_Call) Run(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call {
439+ _c.Call.Run(func(args mock.Arguments) {
440+ var arg0 Slot
441+ if args[0] != nil {
442+ arg0 = args[0].(Slot)
443+ }
444+ var arg1 int
445+ if args[1] != nil {
446+ arg1 = args[1].(int)
447+ }
448+ var arg2 int
449+ if args[2] != nil {
450+ arg2 = args[2].(int)
451+ }
452+ var arg3 int
453+ if args[3] != nil {
454+ arg3 = args[3].(int)
455+ }
456+ var arg4 int
457+ if args[4] != nil {
458+ arg4 = args[4].(int)
459+ }
460+ run(
461+ arg0,
462+ arg1,
463+ arg2,
464+ arg3,
465+ arg4,
466+ )
467+ })
468+ return _c
469+}
470+
471+func (_c *mockhost_OverrideMoved_Call) Return() *mockhost_OverrideMoved_Call {
472+ _c.Call.Return()
473+ return _c
474+}
475+
476+func (_c *mockhost_OverrideMoved_Call) RunAndReturn(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call {
477+ _c.Run(run)
478+ return _c
479+}
480+
289 // Parent provides a mock function for the type mockhost481 // Parent provides a mock function for the type mockhost
290 func (_mock *mockhost) Parent(slot Slot, parent Slot) {482 func (_mock *mockhost) Parent(slot Slot, parent Slot) {
291 _mock.Called(slot, parent)483 _mock.Called(slot, parent)
M internal/adapters/wayland/xwm/mapflow.go +21 -2
@@ -45,8 +45,15 @@ func (x *XWM) onMapRequest(ev *xturbo.Event) {
4545 }
4646
4747 func (x *XWM) onMapNotify(ev *xturbo.Event) {
48- if s := x.tab.lookup(uint32(xproto.MapNotifyOf(ev).Window())); s != 0 {
49- x.tab.rec(s).mapped = true
48+ s := x.tab.lookup(uint32(xproto.MapNotifyOf(ev).Window()))
49+ if s == 0 {
50+ return
51+ }
52+ rec := x.tab.rec(s)
53+ again := rec.mapped
54+ rec.mapped = true
55+ if rec.kind == kindUnmanaged && !again {
56+ x.requestOverrideType(s)
5057 }
5158 }
5259
@@ -63,6 +70,10 @@ func (x *XWM) onUnmapNotify(ev *xturbo.Event) {
6370 }
6471 rec := x.tab.rec(s)
6572 rec.mapped = false
73+ if rec.kind == kindUnmanaged {
74+ x.overrideUnmapped(s, rec)
75+ return
76+ }
6677 if rec.kind != kindManaged {
6778 return
6879 }
@@ -175,6 +186,8 @@ func (x *XWM) onClientMessage(ev *xturbo.Event) {
175186 }
176187 case x.atoms[atomNetWMState]:
177188 x.onNetWMState(e)
189+ case x.atoms[atomWMChangeState]:
190+ // Iconify requests are ignored: windows stay mapped.
178191 }
179192 }
180193
@@ -277,6 +290,8 @@ func (x *XWM) tryPair(s Slot) {
277290 if rec.kind == kindManaged {
278291 x.log.Debug().Uint32("xid", rec.xid).Uint64("surface", rec.surface).Msg("x11 window paired")
279292 x.host.Associate(s, rec.surface)
293+ } else {
294+ x.overrideMapped(s, rec)
280295 }
281296 return
282297 }
@@ -331,6 +346,10 @@ func (x *XWM) SurfaceGone(surfaceID uint64) {
331346 for i := range x.tab.recs {
332347 if r := &x.tab.recs[i]; r.kind != kindFree && r.surface == surfaceID {
333348 r.surface = 0
349+ if r.kind == kindUnmanaged && r.ovPaired {
350+ r.ovPaired = false
351+ x.host.OverrideGone(Slot(i+1), false)
352+ }
334353 }
335354 }
336355 x.wake()
@@ -45,8 +45,15 @@ func (x *XWM) onMapRequest(ev *xturbo.Event) {
45 }45 }
46 46
47 func (x *XWM) onMapNotify(ev *xturbo.Event) {47 func (x *XWM) onMapNotify(ev *xturbo.Event) {
48- if s := x.tab.lookup(uint32(xproto.MapNotifyOf(ev).Window())); s != 0 {48+ s := x.tab.lookup(uint32(xproto.MapNotifyOf(ev).Window()))
49- x.tab.rec(s).mapped = true49+ if s == 0 {
50+ return
51+ }
52+ rec := x.tab.rec(s)
53+ again := rec.mapped
54+ rec.mapped = true
55+ if rec.kind == kindUnmanaged && !again {
56+ x.requestOverrideType(s)
50 }57 }
51 }58 }
52 59
@@ -63,6 +70,10 @@ func (x *XWM) onUnmapNotify(ev *xturbo.Event) {
63 }70 }
64 rec := x.tab.rec(s)71 rec := x.tab.rec(s)
65 rec.mapped = false72 rec.mapped = false
73+ if rec.kind == kindUnmanaged {
74+ x.overrideUnmapped(s, rec)
75+ return
76+ }
66 if rec.kind != kindManaged {77 if rec.kind != kindManaged {
67 return78 return
68 }79 }
@@ -175,6 +186,8 @@ func (x *XWM) onClientMessage(ev *xturbo.Event) {
175 }186 }
176 case x.atoms[atomNetWMState]:187 case x.atoms[atomNetWMState]:
177 x.onNetWMState(e)188 x.onNetWMState(e)
189+ case x.atoms[atomWMChangeState]:
190+ // Iconify requests are ignored: windows stay mapped.
178 }191 }
179 }192 }
180 193
@@ -277,6 +290,8 @@ func (x *XWM) tryPair(s Slot) {
277 if rec.kind == kindManaged {290 if rec.kind == kindManaged {
278 x.log.Debug().Uint32("xid", rec.xid).Uint64("surface", rec.surface).Msg("x11 window paired")291 x.log.Debug().Uint32("xid", rec.xid).Uint64("surface", rec.surface).Msg("x11 window paired")
279 x.host.Associate(s, rec.surface)292 x.host.Associate(s, rec.surface)
293+ } else {
294+ x.overrideMapped(s, rec)
280 }295 }
281 return296 return
282 }297 }
@@ -331,6 +346,10 @@ func (x *XWM) SurfaceGone(surfaceID uint64) {
331 for i := range x.tab.recs {346 for i := range x.tab.recs {
332 if r := &x.tab.recs[i]; r.kind != kindFree && r.surface == surfaceID {347 if r := &x.tab.recs[i]; r.kind != kindFree && r.surface == surfaceID {
333 r.surface = 0348 r.surface = 0
349+ if r.kind == kindUnmanaged && r.ovPaired {
350+ r.ovPaired = false
351+ x.host.OverrideGone(Slot(i+1), false)
352+ }
334 }353 }
335 }354 }
336 x.wake()355 x.wake()
M internal/adapters/wayland/xwm/table.go +13 -0
@@ -28,6 +28,9 @@ type pending struct {
2828 fullscreen, activated, dirty bool
2929 }
3030
31+// ovRect is a layout rectangle.
32+type ovRect struct{ x, y, w, h int32 }
33+
3134 // record is one X window known to the XWM. Strings are the only per-record
3235 // heap data.
3336 type record struct {
@@ -58,6 +61,16 @@ type record struct {
5861 serial uint64
5962 surface uint64
6063
64+ // layout position of a managed window, from Place
65+ lx, ly int32
66+ placed bool
67+
68+ // unmanaged (override-redirect) windows
69+ noInput bool // tooltip, DND, notification: never takes the pointer
70+ ovPaired bool // OverrideMapped sent, OverrideGone owed
71+ ovQueued bool // slot is in XWM.ovDirty
72+ ov ovRect // layout rect last sent to the host
73+
6174 // configure
6275 pend, sent pending
6376 stateSent uint8 // _NET_WM_STATE bits last written
@@ -28,6 +28,9 @@ type pending struct {
28 fullscreen, activated, dirty bool28 fullscreen, activated, dirty bool
29 }29 }
30 30
31+// ovRect is a layout rectangle.
32+type ovRect struct{ x, y, w, h int32 }
33+
31 // record is one X window known to the XWM. Strings are the only per-record34 // record is one X window known to the XWM. Strings are the only per-record
32 // heap data.35 // heap data.
33 type record struct {36 type record struct {
@@ -58,6 +61,16 @@ type record struct {
58 serial uint6461 serial uint64
59 surface uint6462 surface uint64
60 63
64+ // layout position of a managed window, from Place
65+ lx, ly int32
66+ placed bool
67+
68+ // unmanaged (override-redirect) windows
69+ noInput bool // tooltip, DND, notification: never takes the pointer
70+ ovPaired bool // OverrideMapped sent, OverrideGone owed
71+ ovQueued bool // slot is in XWM.ovDirty
72+ ov ovRect // layout rect last sent to the host
73+
61 // configure74 // configure
62 pend, sent pending75 pend, sent pending
63 stateSent uint8 // _NET_WM_STATE bits last written76 stateSent uint8 // _NET_WM_STATE bits last written
A internal/adapters/wayland/xwm/unmanaged.go +109 -0
@@ -0,0 +1,109 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "bnema.dev/xturbo/xproto"
6+)
7+
8+// Override-redirect windows are tracked, never managed: the XWM reads their
9+// window type once at map, pairs them with their wl_surface and reports the
10+// mapped rect to the host in layout coordinates.
11+
12+// maxTypeWords bounds the _NET_WM_WINDOW_TYPE read.
13+const maxTypeWords = 32
14+
15+// requestOverrideType asks for the window type of a freshly mapped override
16+// window; the reply completes its readiness.
17+func (x *XWM) requestOverrideType(s Slot) {
18+ rec := x.tab.rec(s)
19+ seq := xproto.GetProperty(x.conn.Buf(), false, xproto.Window(rec.xid),
20+ xproto.Atom(x.atoms[atomNetWMWindowType]), xproto.AtomAny, 0, maxTypeWords)
21+ x.expect(seq, kindOverrideType, x.tab.tag(s), 0)
22+ x.wake()
23+}
24+
25+func (x *XWM) onOverrideType(s Slot, rec *record, r *xturbo.Reply) {
26+ rec.noInput = false
27+ if rep, ok := xproto.ParseGetPropertyReply(r.Raw); ok && rep.Format() == 32 {
28+ val := rep.Value()
29+ for i := 0; i+4 <= len(val); i += 4 {
30+ switch x.windowType(word(val, i/4)) {
31+ case atomNetWMWindowTypeTooltip, atomNetWMWindowTypeNotification, atomNetWMWindowTypeDND:
32+ rec.noInput = true
33+ }
34+ }
35+ }
36+ x.overrideReady(s)
37+}
38+
39+// overrideReady marks a mapped override window typed and pairs it.
40+func (x *XWM) overrideReady(s Slot) {
41+ rec := x.tab.rec(s)
42+ if rec == nil || rec.kind != kindUnmanaged || !rec.mapped {
43+ return
44+ }
45+ rec.ready = true
46+ x.tryPair(s)
47+ x.overrideMapped(s, rec)
48+}
49+
50+// overrideMapped reports the window once it is mapped, typed and paired.
51+func (x *XWM) overrideMapped(s Slot, rec *record) {
52+ if x.host == nil || rec.ovPaired || !rec.mapped || !rec.ready || rec.surface == 0 {
53+ return
54+ }
55+ rec.ovPaired = true
56+ rec.ov = x.layoutRect(rec)
57+ o := rec.ov
58+ x.host.OverrideMapped(s, rec.xid, rec.surface, int(o.x), int(o.y), int(o.w), int(o.h), rec.noInput)
59+}
60+
61+// overrideUnmapped ends the mapped life of an override window; a remap reads
62+// the type again and pairs with a new surface.
63+func (x *XWM) overrideUnmapped(s Slot, rec *record) {
64+ rec.ready, rec.serial, rec.surface, rec.noInput = false, 0, 0, false
65+ rec.gen++ // a type reply still in flight names a dead tag
66+ if rec.gen == 0 {
67+ rec.gen = 1
68+ }
69+ if rec.ovPaired {
70+ rec.ovPaired = false
71+ x.host.OverrideGone(s, false)
72+ }
73+}
74+
75+func (x *XWM) layoutRect(rec *record) ovRect {
76+ lx, ly := x.space.ToLayout(int(rec.x), int(rec.y))
77+ return ovRect{int32(lx), int32(ly), int32(rec.w), int32(rec.h)}
78+}
79+
80+// queueOverride schedules a geometry report for s; flushOverrides sends it
81+// once per iteration, only if the layout rect changed.
82+func (x *XWM) queueOverride(s Slot, rec *record) {
83+ if rec.ovQueued || s == 0 {
84+ return
85+ }
86+ if len(x.ovDirty) == cap(x.ovDirty) {
87+ x.flushOverrides()
88+ }
89+ rec.ovQueued = true
90+ x.ovDirty = append(x.ovDirty, s)
91+}
92+
93+func (x *XWM) flushOverrides() {
94+ for _, s := range x.ovDirty {
95+ rec := x.tab.rec(s)
96+ if rec == nil || rec.kind != kindUnmanaged || !rec.ovQueued {
97+ continue
98+ }
99+ rec.ovQueued = false
100+ if !rec.ovPaired {
101+ continue
102+ }
103+ if o := x.layoutRect(rec); o != rec.ov {
104+ rec.ov = o
105+ x.host.OverrideMoved(s, int(o.x), int(o.y), int(o.w), int(o.h))
106+ }
107+ }
108+ x.ovDirty = x.ovDirty[:0]
109+}
@@ -0,0 +1,109 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "bnema.dev/xturbo/xproto"
6+)
7+
8+// Override-redirect windows are tracked, never managed: the XWM reads their
9+// window type once at map, pairs them with their wl_surface and reports the
10+// mapped rect to the host in layout coordinates.
11+
12+// maxTypeWords bounds the _NET_WM_WINDOW_TYPE read.
13+const maxTypeWords = 32
14+
15+// requestOverrideType asks for the window type of a freshly mapped override
16+// window; the reply completes its readiness.
17+func (x *XWM) requestOverrideType(s Slot) {
18+ rec := x.tab.rec(s)
19+ seq := xproto.GetProperty(x.conn.Buf(), false, xproto.Window(rec.xid),
20+ xproto.Atom(x.atoms[atomNetWMWindowType]), xproto.AtomAny, 0, maxTypeWords)
21+ x.expect(seq, kindOverrideType, x.tab.tag(s), 0)
22+ x.wake()
23+}
24+
25+func (x *XWM) onOverrideType(s Slot, rec *record, r *xturbo.Reply) {
26+ rec.noInput = false
27+ if rep, ok := xproto.ParseGetPropertyReply(r.Raw); ok && rep.Format() == 32 {
28+ val := rep.Value()
29+ for i := 0; i+4 <= len(val); i += 4 {
30+ switch x.windowType(word(val, i/4)) {
31+ case atomNetWMWindowTypeTooltip, atomNetWMWindowTypeNotification, atomNetWMWindowTypeDND:
32+ rec.noInput = true
33+ }
34+ }
35+ }
36+ x.overrideReady(s)
37+}
38+
39+// overrideReady marks a mapped override window typed and pairs it.
40+func (x *XWM) overrideReady(s Slot) {
41+ rec := x.tab.rec(s)
42+ if rec == nil || rec.kind != kindUnmanaged || !rec.mapped {
43+ return
44+ }
45+ rec.ready = true
46+ x.tryPair(s)
47+ x.overrideMapped(s, rec)
48+}
49+
50+// overrideMapped reports the window once it is mapped, typed and paired.
51+func (x *XWM) overrideMapped(s Slot, rec *record) {
52+ if x.host == nil || rec.ovPaired || !rec.mapped || !rec.ready || rec.surface == 0 {
53+ return
54+ }
55+ rec.ovPaired = true
56+ rec.ov = x.layoutRect(rec)
57+ o := rec.ov
58+ x.host.OverrideMapped(s, rec.xid, rec.surface, int(o.x), int(o.y), int(o.w), int(o.h), rec.noInput)
59+}
60+
61+// overrideUnmapped ends the mapped life of an override window; a remap reads
62+// the type again and pairs with a new surface.
63+func (x *XWM) overrideUnmapped(s Slot, rec *record) {
64+ rec.ready, rec.serial, rec.surface, rec.noInput = false, 0, 0, false
65+ rec.gen++ // a type reply still in flight names a dead tag
66+ if rec.gen == 0 {
67+ rec.gen = 1
68+ }
69+ if rec.ovPaired {
70+ rec.ovPaired = false
71+ x.host.OverrideGone(s, false)
72+ }
73+}
74+
75+func (x *XWM) layoutRect(rec *record) ovRect {
76+ lx, ly := x.space.ToLayout(int(rec.x), int(rec.y))
77+ return ovRect{int32(lx), int32(ly), int32(rec.w), int32(rec.h)}
78+}
79+
80+// queueOverride schedules a geometry report for s; flushOverrides sends it
81+// once per iteration, only if the layout rect changed.
82+func (x *XWM) queueOverride(s Slot, rec *record) {
83+ if rec.ovQueued || s == 0 {
84+ return
85+ }
86+ if len(x.ovDirty) == cap(x.ovDirty) {
87+ x.flushOverrides()
88+ }
89+ rec.ovQueued = true
90+ x.ovDirty = append(x.ovDirty, s)
91+}
92+
93+func (x *XWM) flushOverrides() {
94+ for _, s := range x.ovDirty {
95+ rec := x.tab.rec(s)
96+ if rec == nil || rec.kind != kindUnmanaged || !rec.ovQueued {
97+ continue
98+ }
99+ rec.ovQueued = false
100+ if !rec.ovPaired {
101+ continue
102+ }
103+ if o := x.layoutRect(rec); o != rec.ov {
104+ rec.ov = o
105+ x.host.OverrideMoved(s, int(o.x), int(o.y), int(o.w), int(o.h))
106+ }
107+ }
108+ x.ovDirty = x.ovDirty[:0]
109+}
A internal/adapters/wayland/xwm/unmanaged_test.go +238 -0
@@ -0,0 +1,238 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "testing"
6+
7+ "bnema.dev/xturbo"
8+ "bnema.dev/xturbo/xproto"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+func layoutOf(os ...ports.OutputPlacement) ports.Layout { return ports.Layout(os) }
14+
15+func rootConfigureNotify(win uint32, x, y int16, w, h uint16) xturbo.Event {
16+ ev := xturbo.Event{Kind: xproto.KindConfigureNotify}
17+ binary.LittleEndian.PutUint32(ev.Raw[4:], peerRoot)
18+ binary.LittleEndian.PutUint32(ev.Raw[8:], win)
19+ binary.LittleEndian.PutUint16(ev.Raw[16:], uint16(x))
20+ binary.LittleEndian.PutUint16(ev.Raw[18:], uint16(y))
21+ binary.LittleEndian.PutUint16(ev.Raw[20:], w)
22+ binary.LittleEndian.PutUint16(ev.Raw[22:], h)
23+ return ev
24+}
25+
26+func (p *xpeer) rootConfigureNotify(win uint32, x, y int16, w, h uint16) {
27+ p.event(22, func(b *[32]byte) {
28+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
29+ binary.LittleEndian.PutUint32(b[8:], win)
30+ binary.LittleEndian.PutUint16(b[16:], uint16(x))
31+ binary.LittleEndian.PutUint16(b[18:], uint16(y))
32+ binary.LittleEndian.PutUint16(b[20:], w)
33+ binary.LittleEndian.PutUint16(b[22:], h)
34+ })
35+}
36+
37+func TestPlaceConvertsLayoutToXCoordinates(t *testing.T) {
38+ x, p, h, _, _ := attached(t)
39+ s := newWin(t, x, p, h, 0x500001)
40+ x.SetLayout(layoutOf(out(-640, 0, 640, 480), out(0, 0, 1920, 1080)))
41+ n := settle(t, x, p)
42+
43+ x.Place(s, -640+10, 20)
44+ settle(t, x, p)
45+ cw := since(p, n, opConfigureWindow)
46+ require.Len(t, cw, 1)
47+ require.EqualValues(t, 10, cw[0].u32(12))
48+ require.EqualValues(t, 20, cw[0].u32(16))
49+}
50+
51+func TestSetLayoutRepositionsPlacedWindows(t *testing.T) {
52+ x, p, h, _, _ := attached(t)
53+ s := newWin(t, x, p, h, 0x500001)
54+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
55+ x.Place(s, 100, 50)
56+ n := settle(t, x, p)
57+
58+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080))) // unchanged: nothing resent
59+ settle(t, x, p)
60+ require.Empty(t, since(p, n, opConfigureWindow))
61+
62+ x.SetLayout(layoutOf(out(-100, 0, 100, 100), out(0, 0, 1920, 1080)))
63+ settle(t, x, p)
64+ cw := since(p, n, opConfigureWindow)
65+ require.Len(t, cw, 1)
66+ require.EqualValues(t, 200, cw[0].u32(12))
67+ require.EqualValues(t, 50, cw[0].u32(16))
68+}
69+
70+func TestSetLayoutWarnsOnceWhenOutputsDropped(t *testing.T) {
71+ x, _, _, _, _ := attached(t)
72+ var l ports.Layout
73+ for i := 0; i < maxRegions+2; i++ {
74+ l = append(l, out(i*100, 0, 100, 100))
75+ }
76+ x.SetLayout(l)
77+ require.True(t, x.warnedSpace)
78+ require.Equal(t, 2, x.space.Dropped())
79+}
80+
81+// overrideWindow creates and maps an override window, answers its type read
82+// and pairs it; the mocked host must expect OverrideMapped beforehand.
83+func overrideWindow(t *testing.T, x *XWM, p *xpeer, win uint32, typ string) Slot {
84+ t.Helper()
85+ if typ != "" {
86+ p.setProp(win, p.atom("_NET_WM_WINDOW_TYPE"), uint32(xproto.AtomAtom), 32, le32(p.atom(typ)))
87+ }
88+ p.createNotify(win, true)
89+ readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 })
90+ s := x.tab.lookup(win)
91+ x.feed(t, p, func() { p.rootConfigureNotify(win, 30, 40, 100, 50) })
92+ x.feed(t, p, func() { p.mapNotify(win) })
93+ return s
94+}
95+
96+func TestUnmanagedMapReadsTypeThenPairsThenReports(t *testing.T) {
97+ x, p, h, _, _ := attached(t)
98+ x.SetLayout(layoutOf(out(-640, 0, 640, 480), out(0, 0, 1920, 1080)))
99+ const win = 0x500009
100+ s := overrideWindow(t, x, p, win, "_NET_WM_WINDOW_TYPE_TOOLTIP")
101+ x.flush()
102+ readUntil(t, x, func() bool { return x.tab.rec(s).ready })
103+ typeReads := 0
104+ for _, q := range p.requests() {
105+ if q.major == opGetProperty && q.u32(4) == win {
106+ typeReads++
107+ }
108+ }
109+ require.Equal(t, 1, typeReads)
110+
111+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), -640+30, 40, 100, 50, true).Once()
112+ x.SetSerial(7, 9)
113+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
114+ require.True(t, x.tab.rec(s).ovPaired)
115+}
116+
117+func TestUnmanagedDefaultTypeTakesInput(t *testing.T) {
118+ x, p, h, _, _ := attached(t)
119+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
120+ const win = 0x500009
121+ s := overrideWindow(t, x, p, win, "_NET_WM_WINDOW_TYPE_POPUP_MENU")
122+ x.SetSerial(7, 9)
123+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once()
124+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
125+ readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired })
126+}
127+
128+func TestUnmanagedMoveAndLayoutChangeAndUnmap(t *testing.T) {
129+ x, p, h, _, _ := attached(t)
130+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
131+ const win = 0x500009
132+ s := overrideWindow(t, x, p, win, "")
133+ x.SetSerial(7, 9)
134+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once()
135+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
136+ readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired })
137+
138+ h.EXPECT().OverrideMoved(s, 35, 45, 100, 60).Once()
139+ x.feed(t, p, func() { p.rootConfigureNotify(win, 35, 45, 100, 60) })
140+ x.flush()
141+ x.feed(t, p, func() { p.rootConfigureNotify(win, 35, 45, 100, 60) }) // same rect: no call
142+ x.flush()
143+
144+ h.EXPECT().OverrideMoved(s, -65, 45, 100, 60).Once() // the layout origin moved: same X point, new layout point
145+ x.SetLayout(layoutOf(out(-100, 0, 100, 100), out(0, 0, 1920, 1080)))
146+ x.flush()
147+
148+ h.EXPECT().OverrideGone(s, false).Once()
149+ x.feed(t, p, func() { p.unmapNotify(win, false) })
150+ require.False(t, x.tab.rec(s).ovPaired)
151+}
152+
153+func TestUnmanagedDestroyReportsGone(t *testing.T) {
154+ x, p, h, _, _ := attached(t)
155+ const win = 0x500009
156+ s := overrideWindow(t, x, p, win, "")
157+ x.SetSerial(7, 9)
158+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once()
159+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
160+ readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired })
161+ h.EXPECT().OverrideGone(s, true).Once()
162+ x.feed(t, p, func() { p.destroyNotify(win) })
163+ require.Zero(t, x.tab.lookup(win))
164+}
165+
166+func TestFocusInIgnoresUnmanagedAndGrabModes(t *testing.T) {
167+ x, p, h, _, _ := attached(t)
168+ sa := newWin(t, x, p, h, 0x500001)
169+ newWin(t, x, p, h, 0x500002)
170+ x.Focus(sa)
171+ p.createNotify(0x500009, true)
172+ readUntil(t, x, func() bool { return x.tab.lookup(0x500009) != 0 })
173+ n := settle(t, x, p)
174+
175+ focusIn := func(win uint32, mode byte) {
176+ p.event(9, func(e *[32]byte) {
177+ binary.LittleEndian.PutUint32(e[4:], win)
178+ e[8] = mode
179+ })
180+ }
181+ x.feed(t, p, func() { focusIn(0x500009, 0) })
182+ x.feed(t, p, func() { focusIn(0x500002, byte(xproto.NotifyModeGrab)) })
183+ x.feed(t, p, func() { focusIn(0x500002, byte(xproto.NotifyModeUngrab)) })
184+ require.False(t, x.refocus)
185+ settle(t, x, p)
186+ require.Empty(t, since(p, n, opSetInputFocus))
187+
188+ x.feed(t, p, func() { focusIn(0x500002, 0) })
189+ settle(t, x, p)
190+ require.Len(t, since(p, n, opSetInputFocus), 1, "a normal FocusIn on another managed window is still denied")
191+}
192+
193+func TestWMChangeStateIgnored(t *testing.T) {
194+ x, p, h, _, _ := attached(t)
195+ newWin(t, x, p, h, 0x500001)
196+ n := settle(t, x, p)
197+ x.feed(t, p, func() { p.clientMessage(0x500001, p.atom("WM_CHANGE_STATE"), 3) })
198+ settle(t, x, p)
199+ require.Empty(t, since(p, n, opConfigureWindow)) // the mocked host sees no call either
200+}
201+
202+func TestUnmanagedGeometryAllocations(t *testing.T) {
203+ x, p, _, _, _ := attached(t)
204+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
205+ s := x.insert(0x500009)
206+ r := x.tab.rec(s)
207+ r.kind, r.override, r.mapped, r.ready, r.ovPaired = kindUnmanaged, true, true, true, true
208+ r.x, r.y, r.w, r.h = 30, 40, 100, 50
209+ r.ov = x.layoutRect(r)
210+ sinkPeer(t, x, p)
211+ a := rootConfigureNotify(0x500009, 31, 41, 100, 50)
212+ b := rootConfigureNotify(0x500009, 30, 40, 100, 50) // back to the reported rect
213+ allocs := testing.AllocsPerRun(1000, func() {
214+ x.onEvent(&a)
215+ x.onEvent(&b)
216+ x.flush()
217+ })
218+ require.Zero(t, allocs)
219+ require.True(t, r.ovPaired)
220+}
221+
222+func TestFocusInUnmanagedAllocations(t *testing.T) {
223+ x, p, _, _, _ := attached(t)
224+ s := x.insert(0x500009)
225+ x.tab.rec(s).kind = kindUnmanaged
226+ sinkPeer(t, x, p)
227+ ev := xturbo.Event{Kind: xproto.KindFocusIn}
228+ binary.LittleEndian.PutUint32(ev.Raw[4:], 0x500009)
229+ grab := ev
230+ grab.Raw[8] = byte(xproto.NotifyModeGrab)
231+ allocs := testing.AllocsPerRun(1000, func() {
232+ x.onEvent(&ev)
233+ x.onEvent(&grab)
234+ x.flush()
235+ })
236+ require.Zero(t, allocs)
237+ require.False(t, x.refocus)
238+}
@@ -0,0 +1,238 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "testing"
6+
7+ "bnema.dev/xturbo"
8+ "bnema.dev/xturbo/xproto"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+func layoutOf(os ...ports.OutputPlacement) ports.Layout { return ports.Layout(os) }
14+
15+func rootConfigureNotify(win uint32, x, y int16, w, h uint16) xturbo.Event {
16+ ev := xturbo.Event{Kind: xproto.KindConfigureNotify}
17+ binary.LittleEndian.PutUint32(ev.Raw[4:], peerRoot)
18+ binary.LittleEndian.PutUint32(ev.Raw[8:], win)
19+ binary.LittleEndian.PutUint16(ev.Raw[16:], uint16(x))
20+ binary.LittleEndian.PutUint16(ev.Raw[18:], uint16(y))
21+ binary.LittleEndian.PutUint16(ev.Raw[20:], w)
22+ binary.LittleEndian.PutUint16(ev.Raw[22:], h)
23+ return ev
24+}
25+
26+func (p *xpeer) rootConfigureNotify(win uint32, x, y int16, w, h uint16) {
27+ p.event(22, func(b *[32]byte) {
28+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
29+ binary.LittleEndian.PutUint32(b[8:], win)
30+ binary.LittleEndian.PutUint16(b[16:], uint16(x))
31+ binary.LittleEndian.PutUint16(b[18:], uint16(y))
32+ binary.LittleEndian.PutUint16(b[20:], w)
33+ binary.LittleEndian.PutUint16(b[22:], h)
34+ })
35+}
36+
37+func TestPlaceConvertsLayoutToXCoordinates(t *testing.T) {
38+ x, p, h, _, _ := attached(t)
39+ s := newWin(t, x, p, h, 0x500001)
40+ x.SetLayout(layoutOf(out(-640, 0, 640, 480), out(0, 0, 1920, 1080)))
41+ n := settle(t, x, p)
42+
43+ x.Place(s, -640+10, 20)
44+ settle(t, x, p)
45+ cw := since(p, n, opConfigureWindow)
46+ require.Len(t, cw, 1)
47+ require.EqualValues(t, 10, cw[0].u32(12))
48+ require.EqualValues(t, 20, cw[0].u32(16))
49+}
50+
51+func TestSetLayoutRepositionsPlacedWindows(t *testing.T) {
52+ x, p, h, _, _ := attached(t)
53+ s := newWin(t, x, p, h, 0x500001)
54+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
55+ x.Place(s, 100, 50)
56+ n := settle(t, x, p)
57+
58+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080))) // unchanged: nothing resent
59+ settle(t, x, p)
60+ require.Empty(t, since(p, n, opConfigureWindow))
61+
62+ x.SetLayout(layoutOf(out(-100, 0, 100, 100), out(0, 0, 1920, 1080)))
63+ settle(t, x, p)
64+ cw := since(p, n, opConfigureWindow)
65+ require.Len(t, cw, 1)
66+ require.EqualValues(t, 200, cw[0].u32(12))
67+ require.EqualValues(t, 50, cw[0].u32(16))
68+}
69+
70+func TestSetLayoutWarnsOnceWhenOutputsDropped(t *testing.T) {
71+ x, _, _, _, _ := attached(t)
72+ var l ports.Layout
73+ for i := 0; i < maxRegions+2; i++ {
74+ l = append(l, out(i*100, 0, 100, 100))
75+ }
76+ x.SetLayout(l)
77+ require.True(t, x.warnedSpace)
78+ require.Equal(t, 2, x.space.Dropped())
79+}
80+
81+// overrideWindow creates and maps an override window, answers its type read
82+// and pairs it; the mocked host must expect OverrideMapped beforehand.
83+func overrideWindow(t *testing.T, x *XWM, p *xpeer, win uint32, typ string) Slot {
84+ t.Helper()
85+ if typ != "" {
86+ p.setProp(win, p.atom("_NET_WM_WINDOW_TYPE"), uint32(xproto.AtomAtom), 32, le32(p.atom(typ)))
87+ }
88+ p.createNotify(win, true)
89+ readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 })
90+ s := x.tab.lookup(win)
91+ x.feed(t, p, func() { p.rootConfigureNotify(win, 30, 40, 100, 50) })
92+ x.feed(t, p, func() { p.mapNotify(win) })
93+ return s
94+}
95+
96+func TestUnmanagedMapReadsTypeThenPairsThenReports(t *testing.T) {
97+ x, p, h, _, _ := attached(t)
98+ x.SetLayout(layoutOf(out(-640, 0, 640, 480), out(0, 0, 1920, 1080)))
99+ const win = 0x500009
100+ s := overrideWindow(t, x, p, win, "_NET_WM_WINDOW_TYPE_TOOLTIP")
101+ x.flush()
102+ readUntil(t, x, func() bool { return x.tab.rec(s).ready })
103+ typeReads := 0
104+ for _, q := range p.requests() {
105+ if q.major == opGetProperty && q.u32(4) == win {
106+ typeReads++
107+ }
108+ }
109+ require.Equal(t, 1, typeReads)
110+
111+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), -640+30, 40, 100, 50, true).Once()
112+ x.SetSerial(7, 9)
113+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
114+ require.True(t, x.tab.rec(s).ovPaired)
115+}
116+
117+func TestUnmanagedDefaultTypeTakesInput(t *testing.T) {
118+ x, p, h, _, _ := attached(t)
119+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
120+ const win = 0x500009
121+ s := overrideWindow(t, x, p, win, "_NET_WM_WINDOW_TYPE_POPUP_MENU")
122+ x.SetSerial(7, 9)
123+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once()
124+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
125+ readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired })
126+}
127+
128+func TestUnmanagedMoveAndLayoutChangeAndUnmap(t *testing.T) {
129+ x, p, h, _, _ := attached(t)
130+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
131+ const win = 0x500009
132+ s := overrideWindow(t, x, p, win, "")
133+ x.SetSerial(7, 9)
134+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once()
135+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
136+ readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired })
137+
138+ h.EXPECT().OverrideMoved(s, 35, 45, 100, 60).Once()
139+ x.feed(t, p, func() { p.rootConfigureNotify(win, 35, 45, 100, 60) })
140+ x.flush()
141+ x.feed(t, p, func() { p.rootConfigureNotify(win, 35, 45, 100, 60) }) // same rect: no call
142+ x.flush()
143+
144+ h.EXPECT().OverrideMoved(s, -65, 45, 100, 60).Once() // the layout origin moved: same X point, new layout point
145+ x.SetLayout(layoutOf(out(-100, 0, 100, 100), out(0, 0, 1920, 1080)))
146+ x.flush()
147+
148+ h.EXPECT().OverrideGone(s, false).Once()
149+ x.feed(t, p, func() { p.unmapNotify(win, false) })
150+ require.False(t, x.tab.rec(s).ovPaired)
151+}
152+
153+func TestUnmanagedDestroyReportsGone(t *testing.T) {
154+ x, p, h, _, _ := attached(t)
155+ const win = 0x500009
156+ s := overrideWindow(t, x, p, win, "")
157+ x.SetSerial(7, 9)
158+ h.EXPECT().OverrideMapped(s, uint32(win), uint64(7), 30, 40, 100, 50, false).Once()
159+ x.feed(t, p, func() { p.clientMessage(win, p.atom("WL_SURFACE_SERIAL"), 9, 0) })
160+ readUntil(t, x, func() bool { return x.tab.rec(s).ovPaired })
161+ h.EXPECT().OverrideGone(s, true).Once()
162+ x.feed(t, p, func() { p.destroyNotify(win) })
163+ require.Zero(t, x.tab.lookup(win))
164+}
165+
166+func TestFocusInIgnoresUnmanagedAndGrabModes(t *testing.T) {
167+ x, p, h, _, _ := attached(t)
168+ sa := newWin(t, x, p, h, 0x500001)
169+ newWin(t, x, p, h, 0x500002)
170+ x.Focus(sa)
171+ p.createNotify(0x500009, true)
172+ readUntil(t, x, func() bool { return x.tab.lookup(0x500009) != 0 })
173+ n := settle(t, x, p)
174+
175+ focusIn := func(win uint32, mode byte) {
176+ p.event(9, func(e *[32]byte) {
177+ binary.LittleEndian.PutUint32(e[4:], win)
178+ e[8] = mode
179+ })
180+ }
181+ x.feed(t, p, func() { focusIn(0x500009, 0) })
182+ x.feed(t, p, func() { focusIn(0x500002, byte(xproto.NotifyModeGrab)) })
183+ x.feed(t, p, func() { focusIn(0x500002, byte(xproto.NotifyModeUngrab)) })
184+ require.False(t, x.refocus)
185+ settle(t, x, p)
186+ require.Empty(t, since(p, n, opSetInputFocus))
187+
188+ x.feed(t, p, func() { focusIn(0x500002, 0) })
189+ settle(t, x, p)
190+ require.Len(t, since(p, n, opSetInputFocus), 1, "a normal FocusIn on another managed window is still denied")
191+}
192+
193+func TestWMChangeStateIgnored(t *testing.T) {
194+ x, p, h, _, _ := attached(t)
195+ newWin(t, x, p, h, 0x500001)
196+ n := settle(t, x, p)
197+ x.feed(t, p, func() { p.clientMessage(0x500001, p.atom("WM_CHANGE_STATE"), 3) })
198+ settle(t, x, p)
199+ require.Empty(t, since(p, n, opConfigureWindow)) // the mocked host sees no call either
200+}
201+
202+func TestUnmanagedGeometryAllocations(t *testing.T) {
203+ x, p, _, _, _ := attached(t)
204+ x.SetLayout(layoutOf(out(0, 0, 1920, 1080)))
205+ s := x.insert(0x500009)
206+ r := x.tab.rec(s)
207+ r.kind, r.override, r.mapped, r.ready, r.ovPaired = kindUnmanaged, true, true, true, true
208+ r.x, r.y, r.w, r.h = 30, 40, 100, 50
209+ r.ov = x.layoutRect(r)
210+ sinkPeer(t, x, p)
211+ a := rootConfigureNotify(0x500009, 31, 41, 100, 50)
212+ b := rootConfigureNotify(0x500009, 30, 40, 100, 50) // back to the reported rect
213+ allocs := testing.AllocsPerRun(1000, func() {
214+ x.onEvent(&a)
215+ x.onEvent(&b)
216+ x.flush()
217+ })
218+ require.Zero(t, allocs)
219+ require.True(t, r.ovPaired)
220+}
221+
222+func TestFocusInUnmanagedAllocations(t *testing.T) {
223+ x, p, _, _, _ := attached(t)
224+ s := x.insert(0x500009)
225+ x.tab.rec(s).kind = kindUnmanaged
226+ sinkPeer(t, x, p)
227+ ev := xturbo.Event{Kind: xproto.KindFocusIn}
228+ binary.LittleEndian.PutUint32(ev.Raw[4:], 0x500009)
229+ grab := ev
230+ grab.Raw[8] = byte(xproto.NotifyModeGrab)
231+ allocs := testing.AllocsPerRun(1000, func() {
232+ x.onEvent(&ev)
233+ x.onEvent(&grab)
234+ x.flush()
235+ })
236+ require.Zero(t, allocs)
237+ require.False(t, x.refocus)
238+}
M internal/adapters/wayland/xwm/xwm.go +14 -1
@@ -80,6 +80,10 @@ type XWM struct {
8080 listsDirty uint8 // client lists to rewrite in flush
8181 unpaired [unpairedCap]struct{ serial, surface uint64 }
8282 unpairedN int
83+
84+ space Space // layout <-> X coordinates
85+ warnedSpace bool // more outputs than the Space holds, logged once
86+ ovDirty []Slot // mapped unmanaged slots whose geometry changed, capacity == table size
8387 }
8488
8589 func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM {
@@ -95,6 +99,7 @@ func (x *XWM) fit() {
9599 if cap(x.dirty) < n {
96100 x.dirty = append(make([]Slot, 0, n), x.dirty...)
97101 x.clients = append(make([]uint32, 0, n), x.clients...)
102+ x.ovDirty = append(make([]Slot, 0, n), x.ovDirty...)
98103 x.stacking = append(make([]uint32, 0, n), x.stacking...)
99104 x.scratch = make([]byte, 0, 4*n)
100105 }
@@ -315,6 +320,9 @@ func (x *XWM) attach(src fdSource, h host, post func(func())) {
315320 }
316321 for i := range x.tab.recs {
317322 r := &x.tab.recs[i]
323+ if r.kind == kindUnmanaged && r.mapped && !r.ready { // mapped before Attach
324+ x.requestOverrideType(Slot(i + 1))
325+ }
318326 if r.kind != kindManaged || r.id != 0 {
319327 continue
320328 }
@@ -345,9 +353,14 @@ func (x *XWM) Close() {
345353 }
346354 if x.host != nil {
347355 for i := range x.tab.recs {
348- if r := &x.tab.recs[i]; r.kind == kindManaged && r.id != 0 {
356+ r := &x.tab.recs[i]
357+ switch {
358+ case r.kind == kindManaged && r.id != 0:
349359 r.id = 0
350360 x.host.Dissociate(Slot(i+1), true)
361+ case r.kind == kindUnmanaged && r.ovPaired:
362+ r.ovPaired = false
363+ x.host.OverrideGone(Slot(i+1), true)
351364 }
352365 }
353366 }
@@ -80,6 +80,10 @@ type XWM struct {
80 listsDirty uint8 // client lists to rewrite in flush80 listsDirty uint8 // client lists to rewrite in flush
81 unpaired [unpairedCap]struct{ serial, surface uint64 }81 unpaired [unpairedCap]struct{ serial, surface uint64 }
82 unpairedN int82 unpairedN int
83+
84+ space Space // layout <-> X coordinates
85+ warnedSpace bool // more outputs than the Space holds, logged once
86+ ovDirty []Slot // mapped unmanaged slots whose geometry changed, capacity == table size
83 }87 }
84 88
85 func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM {89 func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM {
@@ -95,6 +99,7 @@ func (x *XWM) fit() {
95 if cap(x.dirty) < n {99 if cap(x.dirty) < n {
96 x.dirty = append(make([]Slot, 0, n), x.dirty...)100 x.dirty = append(make([]Slot, 0, n), x.dirty...)
97 x.clients = append(make([]uint32, 0, n), x.clients...)101 x.clients = append(make([]uint32, 0, n), x.clients...)
102+ x.ovDirty = append(make([]Slot, 0, n), x.ovDirty...)
98 x.stacking = append(make([]uint32, 0, n), x.stacking...)103 x.stacking = append(make([]uint32, 0, n), x.stacking...)
99 x.scratch = make([]byte, 0, 4*n)104 x.scratch = make([]byte, 0, 4*n)
100 }105 }
@@ -315,6 +320,9 @@ func (x *XWM) attach(src fdSource, h host, post func(func())) {
315 }320 }
316 for i := range x.tab.recs {321 for i := range x.tab.recs {
317 r := &x.tab.recs[i]322 r := &x.tab.recs[i]
323+ if r.kind == kindUnmanaged && r.mapped && !r.ready { // mapped before Attach
324+ x.requestOverrideType(Slot(i + 1))
325+ }
318 if r.kind != kindManaged || r.id != 0 {326 if r.kind != kindManaged || r.id != 0 {
319 continue327 continue
320 }328 }
@@ -345,9 +353,14 @@ func (x *XWM) Close() {
345 }353 }
346 if x.host != nil {354 if x.host != nil {
347 for i := range x.tab.recs {355 for i := range x.tab.recs {
348- if r := &x.tab.recs[i]; r.kind == kindManaged && r.id != 0 {356+ r := &x.tab.recs[i]
357+ switch {
358+ case r.kind == kindManaged && r.id != 0:
349 r.id = 0359 r.id = 0
350 x.host.Dissociate(Slot(i+1), true)360 x.host.Dissociate(Slot(i+1), true)
361+ case r.kind == kindUnmanaged && r.ovPaired:
362+ r.ovPaired = false
363+ x.host.OverrideGone(Slot(i+1), true)
351 }364 }
352 }365 }
353 }366 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 26

Tags 10

↑↓ moveenter openesc close