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
- d12983453696dc9fa301c5636615e93ca06b9a4f
- author
- brice · · 2026-10-22 15:14 UTC
- parent
- 65960a3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
12 files · +668 -6
M Makefile +1 -1
| @@ -70,7 +70,7 @@ adapter-imports: | |||
| 70 | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 71 | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 72 | 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=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 | |
| 74 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | |
| 75 | 75 | check: vet test arch fakes-check log-check perf-check | |
| 76 | 76 | ||
| @@ -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=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 |
| 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | ||
| 75 | check: vet test arch fakes-check log-check perf-check | 75 | 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() { | |||
| 305 | 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) {} | |
| @@ -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) { | |||
| 87 | 87 | if r == nil { | |
| 88 | 88 | 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 | 92 | x.markDirty(s, r) | |
| 92 | 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 | 130 | // flushConfigures sends the pending configures, once per display iteration, | |
| 96 | 131 | // then what focus left to do. | |
| 97 | 132 | func (x *XWM) flushConfigures() { | |
| @@ -101,6 +136,7 @@ func (x *XWM) flushConfigures() { | |||
| 101 | 136 | } | |
| 102 | 137 | } | |
| 103 | 138 | x.dirty = x.dirty[:0] | |
| 139 | + | x.flushOverrides() | |
| 104 | 140 | x.flushFocus() | |
| 105 | 141 | } | |
| 106 | 142 | ||
| @@ -262,5 +298,8 @@ func (x *XWM) onConfigureNotify(ev *xturbo.Event) { | |||
| 262 | 298 | } | |
| 263 | 299 | if r := x.tab.rec(x.tab.lookup(uint32(e.Window()))); r != nil && r.kind != kindFree { | |
| 264 | 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 | } | |
| @@ -87,11 +87,46 @@ func (x *XWM) Place(s Slot, px, py int) { | |||||
| 87 | if r == nil { | 87 | if r == nil { | ||
| 88 | return | 88 | 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 ( | |||
| 20 | 20 | kindRootMask | |
| 21 | 21 | kindVersion | |
| 22 | 22 | kindTree | |
| 23 | + | kindOverrideType | |
| 23 | 24 | ) | |
| 24 | 25 | ||
| 25 | 26 | // 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 | 110 | x.onGeomReply(s, rec, r) | |
| 110 | 111 | case kindClientIds: | |
| 111 | 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 | 130 | if e.Code == xproto.BadAccess { | |
| 128 | 131 | x.bootErr = ErrWMExists | |
| 129 | 132 | } | |
| 130 | - | case kindProp, kindGeom, kindClientIds, kindConfigure, kindFocus: | |
| 133 | + | case kindProp, kindGeom, kindClientIds, kindConfigure, kindFocus, kindOverrideType: | |
| 131 | 134 | // Usually BadWindow: the window died between request and reply and | |
| 132 | 135 | // DestroyNotify follows. Whatever the code, the awaited reply will | |
| 133 | 136 | // never come, so the map must not wait for it. | |
| @@ -143,6 +146,11 @@ func (x *XWM) onError(e *xturbo.XError) { | |||
| 143 | 146 | rec.pending &^= propGeom | |
| 144 | 147 | case kindClientIds: | |
| 145 | 148 | 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 | 154 | default: | |
| 147 | 155 | return | |
| 148 | 156 | } | |
| @@ -226,6 +234,7 @@ func (x *XWM) onAttrReply(rec *record, r *xturbo.Reply) { | |||
| 226 | 234 | if a.OverrideRedirect() { | |
| 227 | 235 | rec.override = true | |
| 228 | 236 | rec.kind = kindUnmanaged | |
| 237 | + | rec.mapped = a.MapState() == xproto.MapStateViewable | |
| 229 | 238 | x.stats.UnmanagedSeen++ | |
| 230 | 239 | return | |
| 231 | 240 | } | |
| @@ -20,6 +20,7 @@ const ( | |||||
| 20 | kindRootMask | 20 | kindRootMask | ||
| 21 | kindVersion | 21 | kindVersion | ||
| 22 | kindTree | 22 | kindTree | ||
| 23 | + | kindOverrideType | |||
| 23 | ) | 24 | ) | ||
| 24 | 25 | ||||
| 25 | // onEvent dispatches an X event. Before Attach there is no host: only the | 26 | // 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 = ErrWMExists | 131 | 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 and | 134 | // Usually BadWindow: the window died between request and reply and | ||
| 132 | // DestroyNotify follows. Whatever the code, the awaited reply will | 135 | // 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 &^= propGeom | 146 | rec.pending &^= propGeom | ||
| 144 | case kindClientIds: | 147 | case kindClientIds: | ||
| 145 | rec.pending &^= propClientIDs | 148 | 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 | return | 155 | 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 = true | 235 | rec.override = true | ||
| 228 | rec.kind = kindUnmanaged | 236 | rec.kind = kindUnmanaged | ||
| 237 | + | rec.mapped = a.MapState() == xproto.MapStateViewable | |||
| 229 | x.stats.UnmanagedSeen++ | 238 | x.stats.UnmanagedSeen++ | ||
| 230 | return | 239 | return | ||
| 231 | } | 240 | } | ||
M internal/adapters/wayland/xwm/focus.go +12 -0
| @@ -21,10 +21,18 @@ func (x *XWM) onFocusIn(ev *xturbo.Event) { | |||
| 21 | 21 | if e.Detail() == xproto.NotifyDetailPointer { | |
| 22 | 22 | 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 | 29 | s := x.tab.lookup(uint32(e.Event())) | |
| 25 | 30 | if s == 0 || s == x.active { | |
| 26 | 31 | return | |
| 27 | 32 | } | |
| 33 | + | if r := x.tab.rec(s); r.kind == kindUnmanaged { | |
| 34 | + | return | |
| 35 | + | } | |
| 28 | 36 | x.refocus = true | |
| 29 | 37 | x.wake() | |
| 30 | 38 | } | |
| @@ -173,6 +181,10 @@ func (x *XWM) forgetWindow(s Slot) { | |||
| 173 | 181 | return | |
| 174 | 182 | } | |
| 175 | 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 | 188 | if r.kind == kindManaged && r.id != 0 && x.host != nil { | |
| 177 | 189 | r.id = 0 | |
| 178 | 190 | 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 | return | 22 | 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 | return | 31 | return | ||
| 27 | } | 32 | } | ||
| 33 | + | if r := x.tab.rec(s); r.kind == kindUnmanaged { | |||
| 34 | + | return | |||
| 35 | + | } | |||
| 28 | x.refocus = true | 36 | x.refocus = true | ||
| 29 | x.wake() | 37 | x.wake() | ||
| 30 | } | 38 | } | ||
| @@ -173,6 +181,10 @@ func (x *XWM) forgetWindow(s Slot) { | |||||
| 173 | return | 181 | 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 = 0 | 189 | 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 { | |||
| 40 | 40 | // Fullscreen forwards a fullscreen request (_NET_WM_STATE client message | |
| 41 | 41 | // or initial state); the host emits WindowFullscreenRequest. | |
| 42 | 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 | 53 | // 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 message | 40 | // 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)) * | |||
| 286 | 286 | 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 | 481 | // Parent provides a mock function for the type mockhost | |
| 290 | 482 | func (_mock *mockhost) Parent(slot Slot, parent Slot) { | |
| 291 | 483 | _mock.Called(slot, parent) | |
| @@ -286,6 +286,198 @@ func (_c *mockhost_Fullscreen_Call) RunAndReturn(run func(slot Slot, on bool)) * | |||||
| 286 | return _c | 286 | 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 mockhost | 481 | // 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) { | |||
| 45 | 45 | } | |
| 46 | 46 | ||
| 47 | 47 | 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) | |
| 50 | 57 | } | |
| 51 | 58 | } | |
| 52 | 59 | ||
| @@ -63,6 +70,10 @@ func (x *XWM) onUnmapNotify(ev *xturbo.Event) { | |||
| 63 | 70 | } | |
| 64 | 71 | rec := x.tab.rec(s) | |
| 65 | 72 | rec.mapped = false | |
| 73 | + | if rec.kind == kindUnmanaged { | |
| 74 | + | x.overrideUnmapped(s, rec) | |
| 75 | + | return | |
| 76 | + | } | |
| 66 | 77 | if rec.kind != kindManaged { | |
| 67 | 78 | return | |
| 68 | 79 | } | |
| @@ -175,6 +186,8 @@ func (x *XWM) onClientMessage(ev *xturbo.Event) { | |||
| 175 | 186 | } | |
| 176 | 187 | case x.atoms[atomNetWMState]: | |
| 177 | 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 | 290 | if rec.kind == kindManaged { | |
| 278 | 291 | x.log.Debug().Uint32("xid", rec.xid).Uint64("surface", rec.surface).Msg("x11 window paired") | |
| 279 | 292 | x.host.Associate(s, rec.surface) | |
| 293 | + | } else { | |
| 294 | + | x.overrideMapped(s, rec) | |
| 280 | 295 | } | |
| 281 | 296 | return | |
| 282 | 297 | } | |
| @@ -331,6 +346,10 @@ func (x *XWM) SurfaceGone(surfaceID uint64) { | |||
| 331 | 346 | for i := range x.tab.recs { | |
| 332 | 347 | if r := &x.tab.recs[i]; r.kind != kindFree && r.surface == surfaceID { | |
| 333 | 348 | 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 | 355 | 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 = true | 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) | |||
| 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 = false | 72 | 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 | return | 78 | 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 | return | 296 | 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 = 0 | 348 | 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 { | |||
| 28 | 28 | 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 | 34 | // record is one X window known to the XWM. Strings are the only per-record | |
| 32 | 35 | // heap data. | |
| 33 | 36 | type record struct { | |
| @@ -58,6 +61,16 @@ type record struct { | |||
| 58 | 61 | serial uint64 | |
| 59 | 62 | 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 | 74 | // configure | |
| 62 | 75 | pend, sent pending | |
| 63 | 76 | stateSent uint8 // _NET_WM_STATE bits last written | |
| @@ -28,6 +28,9 @@ type pending struct { | |||||
| 28 | fullscreen, activated, dirty bool | 28 | 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-record | 34 | // 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 uint64 | 61 | serial uint64 | ||
| 59 | surface uint64 | 62 | 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 | // configure | 74 | // configure | ||
| 62 | pend, sent pending | 75 | pend, sent pending | ||
| 63 | stateSent uint8 // _NET_WM_STATE bits last written | 76 | 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 { | |||
| 80 | 80 | listsDirty uint8 // client lists to rewrite in flush | |
| 81 | 81 | unpaired [unpairedCap]struct{ serial, surface uint64 } | |
| 82 | 82 | 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 | 89 | func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM { | |
| @@ -95,6 +99,7 @@ func (x *XWM) fit() { | |||
| 95 | 99 | if cap(x.dirty) < n { | |
| 96 | 100 | x.dirty = append(make([]Slot, 0, n), x.dirty...) | |
| 97 | 101 | x.clients = append(make([]uint32, 0, n), x.clients...) | |
| 102 | + | x.ovDirty = append(make([]Slot, 0, n), x.ovDirty...) | |
| 98 | 103 | x.stacking = append(make([]uint32, 0, n), x.stacking...) | |
| 99 | 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 | 321 | for i := range x.tab.recs { | |
| 317 | 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 | 326 | if r.kind != kindManaged || r.id != 0 { | |
| 319 | 327 | continue | |
| 320 | 328 | } | |
| @@ -345,9 +353,14 @@ func (x *XWM) Close() { | |||
| 345 | 353 | } | |
| 346 | 354 | if x.host != nil { | |
| 347 | 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 | 359 | r.id = 0 | |
| 350 | 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 | } | |
| @@ -80,6 +80,10 @@ type XWM struct { | |||||
| 80 | listsDirty uint8 // client lists to rewrite in flush | 80 | listsDirty uint8 // client lists to rewrite in flush | ||
| 81 | unpaired [unpairedCap]struct{ serial, surface uint64 } | 81 | unpaired [unpairedCap]struct{ serial, surface uint64 } | ||
| 82 | unpairedN int | 82 | 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 | continue | 327 | 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 = 0 | 359 | 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 | } | ||