Merge branch 'feat/x6-configure' into feat/x6-xwm
Reconcile X6.4 and X6.5: one dropWindow/forgetWindow, client lists only via addClient/removeClient flushed once in flushFocus, record.floating set by props as the single floating source, unmap routed through dropWindow. Streams regenerated (atom ids shifted); steady state is a focused window.
13 files · +1086 -132
M Makefile +1 -1
| @@ -69,7 +69,7 @@ adapter-imports: | |||
| 69 | 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 70 | 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 71 | 71 | perf-check: | |
| 72 | - | 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)$$' -count=1 | |
| 72 | + | 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)$$' -count=1 | |
| 73 | 73 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | |
| 74 | 74 | check: vet test arch fakes-check log-check perf-check | |
| 75 | 75 | ||
| @@ -69,7 +69,7 @@ adapter-imports: | |||||
| 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | ||
| 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 71 | perf-check: | 71 | perf-check: | ||
| 72 | - | 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)$$' -count=1 | 72 | + | 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)$$' -count=1 |
| 73 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | 73 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | ||
| 74 | check: vet test arch fakes-check log-check perf-check | 74 | check: vet test arch fakes-check log-check perf-check | ||
| 75 | 75 | ||||
M internal/adapters/wayland/xwm/configure.go +219 -7
| @@ -2,17 +2,229 @@ package xwm | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bnema.dev/xturbo" | |
| 5 | + | "bnema.dev/xturbo/xproto" | |
| 5 | 6 | "github.com/bnema/neferwl/internal/ports" | |
| 6 | 7 | ) | |
| 7 | 8 | ||
| 8 | - | func (x *XWM) onConfigureRequest(ev *xturbo.Event) { _ = ev } | |
| 9 | - | func (x *XWM) onConfigureNotify(ev *xturbo.Event) { _ = ev } | |
| 9 | + | // maxClientSize bounds the size a floating window may ask for. | |
| 10 | + | const maxClientSize = 16384 | |
| 10 | 11 | ||
| 11 | - | // Configure records the size and state core wants for s; sent at flush. | |
| 12 | - | func (x *XWM) Configure(s Slot, c ports.ConfigureWindow) { _, _ = s, c } | |
| 12 | + | // _NET_WM_STATE bits tracked in record.stateSent. | |
| 13 | + | const ( | |
| 14 | + | stateFullscreen uint8 = 1 << iota | |
| 15 | + | stateFocused | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // size is the size to send: the requested one, else the last known. | |
| 19 | + | func (r *record) size() (w, h uint32) { | |
| 20 | + | w, h = r.pend.w, r.pend.h | |
| 21 | + | if w == 0 { | |
| 22 | + | w = r.w | |
| 23 | + | } | |
| 24 | + | if h == 0 { | |
| 25 | + | h = r.h | |
| 26 | + | } | |
| 27 | + | return w, h | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // differs reports whether the pending configure is not what the X server has. | |
| 31 | + | func (r *record) differs() bool { | |
| 32 | + | w, h := r.size() | |
| 33 | + | p, s := &r.pend, &r.sent | |
| 34 | + | return p.x != s.x || p.y != s.y || w != s.w || h != s.h || | |
| 35 | + | p.fullscreen != s.fullscreen || p.activated != s.activated | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // markDirty queues s for the next flush when its pending configure differs. | |
| 39 | + | func (x *XWM) markDirty(s Slot, r *record) { | |
| 40 | + | if r.pend.dirty || !r.differs() { | |
| 41 | + | return | |
| 42 | + | } | |
| 43 | + | if len(x.dirty) == cap(x.dirty) { | |
| 44 | + | // Stale entries of freed and reused slots filled the queue: send | |
| 45 | + | // what is queued rather than grow it. r stays valid, nothing here | |
| 46 | + | // grows the table. | |
| 47 | + | x.flushConfigures() | |
| 48 | + | } | |
| 49 | + | r.pend.dirty = true | |
| 50 | + | x.dirty = append(x.dirty, s) | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // managedRec returns the record of a live managed window. | |
| 54 | + | func (x *XWM) managedRec(s Slot) *record { | |
| 55 | + | if x.closed { | |
| 56 | + | return nil | |
| 57 | + | } | |
| 58 | + | if r := x.tab.rec(s); r != nil && r.kind == kindManaged { | |
| 59 | + | return r | |
| 60 | + | } | |
| 61 | + | return nil | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // Configure records the size and state core wants for s; sent at flush. A | |
| 65 | + | // zero size keeps the current one. | |
| 66 | + | func (x *XWM) Configure(s Slot, c ports.ConfigureWindow) { | |
| 67 | + | r := x.managedRec(s) | |
| 68 | + | if r == nil { | |
| 69 | + | return | |
| 70 | + | } | |
| 71 | + | if c.Width > 0 { | |
| 72 | + | r.pend.w = uint32(min(c.Width, maxClientSize)) | |
| 73 | + | } | |
| 74 | + | if c.Height > 0 { | |
| 75 | + | r.pend.h = uint32(min(c.Height, maxClientSize)) | |
| 76 | + | } | |
| 77 | + | r.pend.fullscreen, r.pend.activated = c.Fullscreen, c.Activated | |
| 78 | + | x.markDirty(s, r) | |
| 79 | + | x.wake() | |
| 80 | + | } | |
| 13 | 81 | ||
| 14 | 82 | // Place records the layout position of the client rectangle of s. | |
| 15 | - | func (x *XWM) Place(s Slot, px, py int) { _, _, _ = s, px, py } | |
| 83 | + | func (x *XWM) Place(s Slot, px, py int) { | |
| 84 | + | r := x.managedRec(s) | |
| 85 | + | if r == nil { | |
| 86 | + | return | |
| 87 | + | } | |
| 88 | + | r.pend.x, r.pend.y = int32(px), int32(py) | |
| 89 | + | x.markDirty(s, r) | |
| 90 | + | x.wake() | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // flushConfigures sends the pending configures, once per display iteration, | |
| 94 | + | // then what focus left to do. | |
| 95 | + | func (x *XWM) flushConfigures() { | |
| 96 | + | for _, s := range x.dirty { | |
| 97 | + | if r := x.tab.rec(s); r != nil && r.kind == kindManaged && r.pend.dirty { | |
| 98 | + | x.sendPending(s, r) | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | x.dirty = x.dirty[:0] | |
| 102 | + | x.flushFocus() | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // sendPending writes the geometry (with the synthetic ConfigureNotify ICCCM | |
| 106 | + | // 4.1.5 asks for, since Xwayland may not move the window) and the | |
| 107 | + | // _NET_WM_STATE bits that changed. | |
| 108 | + | func (x *XWM) sendPending(s Slot, r *record) { | |
| 109 | + | r.pend.dirty = false | |
| 110 | + | p := &r.pend | |
| 111 | + | w, h := r.size() | |
| 112 | + | if w > 0 && h > 0 && (p.x != r.sent.x || p.y != r.sent.y || w != r.sent.w || h != r.sent.h) { | |
| 113 | + | var v xproto.ConfigureWindowValues | |
| 114 | + | v.SetX(p.x).SetY(p.y).SetWidth(w).SetHeight(h).SetBorderWidth(0) | |
| 115 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0) | |
| 116 | + | r.x, r.y, r.w, r.h = p.x, p.y, w, h | |
| 117 | + | x.sendConfigureNotify(r) | |
| 118 | + | r.sent.x, r.sent.y, r.sent.w, r.sent.h = p.x, p.y, w, h | |
| 119 | + | } | |
| 120 | + | var bits uint8 | |
| 121 | + | if p.fullscreen { | |
| 122 | + | bits |= stateFullscreen | |
| 123 | + | } | |
| 124 | + | if p.activated { | |
| 125 | + | bits |= stateFocused | |
| 126 | + | } | |
| 127 | + | r.sent.fullscreen, r.sent.activated = p.fullscreen, p.activated | |
| 128 | + | if bits != r.stateSent { | |
| 129 | + | r.stateSent = bits | |
| 130 | + | x.writeState(r) | |
| 131 | + | } | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | // writeState rewrites _NET_WM_STATE of r from its bits. | |
| 135 | + | func (x *XWM) writeState(r *record) { | |
| 136 | + | var list [3]uint32 | |
| 137 | + | n := 0 | |
| 138 | + | if r.modal { | |
| 139 | + | list[n] = x.atoms[atomNetWMStateModal] | |
| 140 | + | n++ | |
| 141 | + | } | |
| 142 | + | if r.stateSent&stateFullscreen != 0 { | |
| 143 | + | list[n] = x.atoms[atomNetWMStateFullscreen] | |
| 144 | + | n++ | |
| 145 | + | } | |
| 146 | + | if r.stateSent&stateFocused != 0 { | |
| 147 | + | list[n] = x.atoms[atomNetWMStateFocused] | |
| 148 | + | n++ | |
| 149 | + | } | |
| 150 | + | x.setProp32(r.xid, atomNetWMState, xproto.AtomAtom, list[:n]) | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // sendConfigureNotify tells the client its geometry as the XWM sees it. | |
| 154 | + | func (x *XWM) sendConfigureNotify(r *record) { | |
| 155 | + | var raw [32]byte | |
| 156 | + | d := xproto.ConfigureNotifyEventData{ | |
| 157 | + | Event: xproto.Window(r.xid), Window: xproto.Window(r.xid), | |
| 158 | + | X: int16(r.x), Y: int16(r.y), Width: uint16(r.w), Height: uint16(r.h), | |
| 159 | + | } | |
| 160 | + | d.Put(&raw) | |
| 161 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(r.xid), xproto.EventMaskStructureNotify, &raw) | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // onConfigureRequest answers a client resizing itself. Tiled windows are told | |
| 165 | + | // their current geometry; floating ones get the size they ask for; unmanaged | |
| 166 | + | // and unknown windows are configured as requested. | |
| 167 | + | func (x *XWM) onConfigureRequest(ev *xturbo.Event) { | |
| 168 | + | e := xproto.ConfigureRequestOf(ev) | |
| 169 | + | xid := uint32(e.Window()) | |
| 170 | + | mask := e.ValueMask() | |
| 171 | + | s := x.tab.lookup(xid) | |
| 172 | + | r := x.managedRec(s) | |
| 173 | + | if r == nil { | |
| 174 | + | var v xproto.ConfigureWindowValues | |
| 175 | + | if mask&xproto.ConfigWindowX != 0 { | |
| 176 | + | v.SetX(int32(e.X())) | |
| 177 | + | } | |
| 178 | + | if mask&xproto.ConfigWindowY != 0 { | |
| 179 | + | v.SetY(int32(e.Y())) | |
| 180 | + | } | |
| 181 | + | if mask&xproto.ConfigWindowWidth != 0 { | |
| 182 | + | v.SetWidth(uint32(e.Width())) | |
| 183 | + | } | |
| 184 | + | if mask&xproto.ConfigWindowHeight != 0 { | |
| 185 | + | v.SetHeight(uint32(e.Height())) | |
| 186 | + | } | |
| 187 | + | if mask&xproto.ConfigWindowBorderWidth != 0 { | |
| 188 | + | v.SetBorderWidth(uint32(e.BorderWidth())) | |
| 189 | + | } | |
| 190 | + | if mask&xproto.ConfigWindowSibling != 0 { | |
| 191 | + | v.SetSibling(e.Sibling()) | |
| 192 | + | } | |
| 193 | + | if mask&xproto.ConfigWindowStackMode != 0 { | |
| 194 | + | v.SetStackMode(e.StackMode()) | |
| 195 | + | } | |
| 196 | + | xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v) | |
| 197 | + | return | |
| 198 | + | } | |
| 199 | + | if r.floating && mask&(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 { | |
| 200 | + | w, h := r.w, r.h | |
| 201 | + | if mask&xproto.ConfigWindowWidth != 0 { | |
| 202 | + | w = uint32(min(max(int(e.Width()), 1), maxClientSize)) | |
| 203 | + | } | |
| 204 | + | if mask&xproto.ConfigWindowHeight != 0 { | |
| 205 | + | h = uint32(min(max(int(e.Height()), 1), maxClientSize)) | |
| 206 | + | } | |
| 207 | + | if w > 0 && h > 0 { | |
| 208 | + | var v xproto.ConfigureWindowValues | |
| 209 | + | v.SetWidth(w).SetHeight(h) | |
| 210 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v), kindConfigure, x.tab.tag(s), 0) | |
| 211 | + | r.w, r.h = w, h | |
| 212 | + | // The size is the client's own: core learns it from the commit | |
| 213 | + | // and Configure only resends it when core wants another. | |
| 214 | + | r.pend.w, r.pend.h = w, h | |
| 215 | + | r.sent.w, r.sent.h = w, h | |
| 216 | + | } | |
| 217 | + | } | |
| 218 | + | x.sendConfigureNotify(r) | |
| 219 | + | } | |
| 16 | 220 | ||
| 17 | - | // flushConfigures sends the pending configures, once per display iteration. | |
| 18 | - | func (x *XWM) flushConfigures() {} | |
| 221 | + | // onConfigureNotify keeps the known geometry of every top-level window. | |
| 222 | + | func (x *XWM) onConfigureNotify(ev *xturbo.Event) { | |
| 223 | + | e := xproto.ConfigureNotifyOf(ev) | |
| 224 | + | if e.Event() != xproto.Window(x.root) { | |
| 225 | + | return | |
| 226 | + | } | |
| 227 | + | if r := x.tab.rec(x.tab.lookup(uint32(e.Window()))); r != nil && r.kind != kindFree { | |
| 228 | + | r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height()) | |
| 229 | + | } | |
| 230 | + | } | |
| @@ -2,17 +2,229 @@ package xwm | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "bnema.dev/xturbo" | 4 | "bnema.dev/xturbo" | ||
| 5 | + | "bnema.dev/xturbo/xproto" | |||
| 5 | "github.com/bnema/neferwl/internal/ports" | 6 | "github.com/bnema/neferwl/internal/ports" | ||
| 6 | ) | 7 | ) | ||
| 7 | 8 | ||||
| 8 | - | func (x *XWM) onConfigureRequest(ev *xturbo.Event) { _ = ev } | 9 | + | // maxClientSize bounds the size a floating window may ask for. |
| 9 | - | func (x *XWM) onConfigureNotify(ev *xturbo.Event) { _ = ev } | 10 | + | const maxClientSize = 16384 |
| 10 | 11 | ||||
| 11 | - | // Configure records the size and state core wants for s; sent at flush. | 12 | + | // _NET_WM_STATE bits tracked in record.stateSent. |
| 12 | - | func (x *XWM) Configure(s Slot, c ports.ConfigureWindow) { _, _ = s, c } | 13 | + | const ( |
| 14 | + | stateFullscreen uint8 = 1 << iota | |||
| 15 | + | stateFocused | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // size is the size to send: the requested one, else the last known. | |||
| 19 | + | func (r *record) size() (w, h uint32) { | |||
| 20 | + | w, h = r.pend.w, r.pend.h | |||
| 21 | + | if w == 0 { | |||
| 22 | + | w = r.w | |||
| 23 | + | } | |||
| 24 | + | if h == 0 { | |||
| 25 | + | h = r.h | |||
| 26 | + | } | |||
| 27 | + | return w, h | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // differs reports whether the pending configure is not what the X server has. | |||
| 31 | + | func (r *record) differs() bool { | |||
| 32 | + | w, h := r.size() | |||
| 33 | + | p, s := &r.pend, &r.sent | |||
| 34 | + | return p.x != s.x || p.y != s.y || w != s.w || h != s.h || | |||
| 35 | + | p.fullscreen != s.fullscreen || p.activated != s.activated | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // markDirty queues s for the next flush when its pending configure differs. | |||
| 39 | + | func (x *XWM) markDirty(s Slot, r *record) { | |||
| 40 | + | if r.pend.dirty || !r.differs() { | |||
| 41 | + | return | |||
| 42 | + | } | |||
| 43 | + | if len(x.dirty) == cap(x.dirty) { | |||
| 44 | + | // Stale entries of freed and reused slots filled the queue: send | |||
| 45 | + | // what is queued rather than grow it. r stays valid, nothing here | |||
| 46 | + | // grows the table. | |||
| 47 | + | x.flushConfigures() | |||
| 48 | + | } | |||
| 49 | + | r.pend.dirty = true | |||
| 50 | + | x.dirty = append(x.dirty, s) | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // managedRec returns the record of a live managed window. | |||
| 54 | + | func (x *XWM) managedRec(s Slot) *record { | |||
| 55 | + | if x.closed { | |||
| 56 | + | return nil | |||
| 57 | + | } | |||
| 58 | + | if r := x.tab.rec(s); r != nil && r.kind == kindManaged { | |||
| 59 | + | return r | |||
| 60 | + | } | |||
| 61 | + | return nil | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // Configure records the size and state core wants for s; sent at flush. A | |||
| 65 | + | // zero size keeps the current one. | |||
| 66 | + | func (x *XWM) Configure(s Slot, c ports.ConfigureWindow) { | |||
| 67 | + | r := x.managedRec(s) | |||
| 68 | + | if r == nil { | |||
| 69 | + | return | |||
| 70 | + | } | |||
| 71 | + | if c.Width > 0 { | |||
| 72 | + | r.pend.w = uint32(min(c.Width, maxClientSize)) | |||
| 73 | + | } | |||
| 74 | + | if c.Height > 0 { | |||
| 75 | + | r.pend.h = uint32(min(c.Height, maxClientSize)) | |||
| 76 | + | } | |||
| 77 | + | r.pend.fullscreen, r.pend.activated = c.Fullscreen, c.Activated | |||
| 78 | + | x.markDirty(s, r) | |||
| 79 | + | x.wake() | |||
| 80 | + | } | |||
| 13 | 81 | ||||
| 14 | // Place records the layout position of the client rectangle of s. | 82 | // Place records the layout position of the client rectangle of s. | ||
| 15 | - | func (x *XWM) Place(s Slot, px, py int) { _, _, _ = s, px, py } | 83 | + | func (x *XWM) Place(s Slot, px, py int) { |
| 84 | + | r := x.managedRec(s) | |||
| 85 | + | if r == nil { | |||
| 86 | + | return | |||
| 87 | + | } | |||
| 88 | + | r.pend.x, r.pend.y = int32(px), int32(py) | |||
| 89 | + | x.markDirty(s, r) | |||
| 90 | + | x.wake() | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | // flushConfigures sends the pending configures, once per display iteration, | |||
| 94 | + | // then what focus left to do. | |||
| 95 | + | func (x *XWM) flushConfigures() { | |||
| 96 | + | for _, s := range x.dirty { | |||
| 97 | + | if r := x.tab.rec(s); r != nil && r.kind == kindManaged && r.pend.dirty { | |||
| 98 | + | x.sendPending(s, r) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | x.dirty = x.dirty[:0] | |||
| 102 | + | x.flushFocus() | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // sendPending writes the geometry (with the synthetic ConfigureNotify ICCCM | |||
| 106 | + | // 4.1.5 asks for, since Xwayland may not move the window) and the | |||
| 107 | + | // _NET_WM_STATE bits that changed. | |||
| 108 | + | func (x *XWM) sendPending(s Slot, r *record) { | |||
| 109 | + | r.pend.dirty = false | |||
| 110 | + | p := &r.pend | |||
| 111 | + | w, h := r.size() | |||
| 112 | + | if w > 0 && h > 0 && (p.x != r.sent.x || p.y != r.sent.y || w != r.sent.w || h != r.sent.h) { | |||
| 113 | + | var v xproto.ConfigureWindowValues | |||
| 114 | + | v.SetX(p.x).SetY(p.y).SetWidth(w).SetHeight(h).SetBorderWidth(0) | |||
| 115 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0) | |||
| 116 | + | r.x, r.y, r.w, r.h = p.x, p.y, w, h | |||
| 117 | + | x.sendConfigureNotify(r) | |||
| 118 | + | r.sent.x, r.sent.y, r.sent.w, r.sent.h = p.x, p.y, w, h | |||
| 119 | + | } | |||
| 120 | + | var bits uint8 | |||
| 121 | + | if p.fullscreen { | |||
| 122 | + | bits |= stateFullscreen | |||
| 123 | + | } | |||
| 124 | + | if p.activated { | |||
| 125 | + | bits |= stateFocused | |||
| 126 | + | } | |||
| 127 | + | r.sent.fullscreen, r.sent.activated = p.fullscreen, p.activated | |||
| 128 | + | if bits != r.stateSent { | |||
| 129 | + | r.stateSent = bits | |||
| 130 | + | x.writeState(r) | |||
| 131 | + | } | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | // writeState rewrites _NET_WM_STATE of r from its bits. | |||
| 135 | + | func (x *XWM) writeState(r *record) { | |||
| 136 | + | var list [3]uint32 | |||
| 137 | + | n := 0 | |||
| 138 | + | if r.modal { | |||
| 139 | + | list[n] = x.atoms[atomNetWMStateModal] | |||
| 140 | + | n++ | |||
| 141 | + | } | |||
| 142 | + | if r.stateSent&stateFullscreen != 0 { | |||
| 143 | + | list[n] = x.atoms[atomNetWMStateFullscreen] | |||
| 144 | + | n++ | |||
| 145 | + | } | |||
| 146 | + | if r.stateSent&stateFocused != 0 { | |||
| 147 | + | list[n] = x.atoms[atomNetWMStateFocused] | |||
| 148 | + | n++ | |||
| 149 | + | } | |||
| 150 | + | x.setProp32(r.xid, atomNetWMState, xproto.AtomAtom, list[:n]) | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // sendConfigureNotify tells the client its geometry as the XWM sees it. | |||
| 154 | + | func (x *XWM) sendConfigureNotify(r *record) { | |||
| 155 | + | var raw [32]byte | |||
| 156 | + | d := xproto.ConfigureNotifyEventData{ | |||
| 157 | + | Event: xproto.Window(r.xid), Window: xproto.Window(r.xid), | |||
| 158 | + | X: int16(r.x), Y: int16(r.y), Width: uint16(r.w), Height: uint16(r.h), | |||
| 159 | + | } | |||
| 160 | + | d.Put(&raw) | |||
| 161 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(r.xid), xproto.EventMaskStructureNotify, &raw) | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | // onConfigureRequest answers a client resizing itself. Tiled windows are told | |||
| 165 | + | // their current geometry; floating ones get the size they ask for; unmanaged | |||
| 166 | + | // and unknown windows are configured as requested. | |||
| 167 | + | func (x *XWM) onConfigureRequest(ev *xturbo.Event) { | |||
| 168 | + | e := xproto.ConfigureRequestOf(ev) | |||
| 169 | + | xid := uint32(e.Window()) | |||
| 170 | + | mask := e.ValueMask() | |||
| 171 | + | s := x.tab.lookup(xid) | |||
| 172 | + | r := x.managedRec(s) | |||
| 173 | + | if r == nil { | |||
| 174 | + | var v xproto.ConfigureWindowValues | |||
| 175 | + | if mask&xproto.ConfigWindowX != 0 { | |||
| 176 | + | v.SetX(int32(e.X())) | |||
| 177 | + | } | |||
| 178 | + | if mask&xproto.ConfigWindowY != 0 { | |||
| 179 | + | v.SetY(int32(e.Y())) | |||
| 180 | + | } | |||
| 181 | + | if mask&xproto.ConfigWindowWidth != 0 { | |||
| 182 | + | v.SetWidth(uint32(e.Width())) | |||
| 183 | + | } | |||
| 184 | + | if mask&xproto.ConfigWindowHeight != 0 { | |||
| 185 | + | v.SetHeight(uint32(e.Height())) | |||
| 186 | + | } | |||
| 187 | + | if mask&xproto.ConfigWindowBorderWidth != 0 { | |||
| 188 | + | v.SetBorderWidth(uint32(e.BorderWidth())) | |||
| 189 | + | } | |||
| 190 | + | if mask&xproto.ConfigWindowSibling != 0 { | |||
| 191 | + | v.SetSibling(e.Sibling()) | |||
| 192 | + | } | |||
| 193 | + | if mask&xproto.ConfigWindowStackMode != 0 { | |||
| 194 | + | v.SetStackMode(e.StackMode()) | |||
| 195 | + | } | |||
| 196 | + | xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v) | |||
| 197 | + | return | |||
| 198 | + | } | |||
| 199 | + | if r.floating && mask&(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 { | |||
| 200 | + | w, h := r.w, r.h | |||
| 201 | + | if mask&xproto.ConfigWindowWidth != 0 { | |||
| 202 | + | w = uint32(min(max(int(e.Width()), 1), maxClientSize)) | |||
| 203 | + | } | |||
| 204 | + | if mask&xproto.ConfigWindowHeight != 0 { | |||
| 205 | + | h = uint32(min(max(int(e.Height()), 1), maxClientSize)) | |||
| 206 | + | } | |||
| 207 | + | if w > 0 && h > 0 { | |||
| 208 | + | var v xproto.ConfigureWindowValues | |||
| 209 | + | v.SetWidth(w).SetHeight(h) | |||
| 210 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v), kindConfigure, x.tab.tag(s), 0) | |||
| 211 | + | r.w, r.h = w, h | |||
| 212 | + | // The size is the client's own: core learns it from the commit | |||
| 213 | + | // and Configure only resends it when core wants another. | |||
| 214 | + | r.pend.w, r.pend.h = w, h | |||
| 215 | + | r.sent.w, r.sent.h = w, h | |||
| 216 | + | } | |||
| 217 | + | } | |||
| 218 | + | x.sendConfigureNotify(r) | |||
| 219 | + | } | |||
| 16 | 220 | ||||
| 17 | - | // flushConfigures sends the pending configures, once per display iteration. | 221 | + | // onConfigureNotify keeps the known geometry of every top-level window. |
| 18 | - | func (x *XWM) flushConfigures() {} | 222 | + | func (x *XWM) onConfigureNotify(ev *xturbo.Event) { |
| 223 | + | e := xproto.ConfigureNotifyOf(ev) | |||
| 224 | + | if e.Event() != xproto.Window(x.root) { | |||
| 225 | + | return | |||
| 226 | + | } | |||
| 227 | + | if r := x.tab.rec(x.tab.lookup(uint32(e.Window()))); r != nil && r.kind != kindFree { | |||
| 228 | + | r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height()) | |||
| 229 | + | } | |||
| 230 | + | } | |||
A internal/adapters/wayland/xwm/configure_test.go +562 -0
| @@ -0,0 +1,562 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "bnema.dev/xturbo/xproto" | |
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |
| 10 | + | "github.com/stretchr/testify/mock" | |
| 11 | + | "github.com/stretchr/testify/require" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // newWin makes a ready managed window known to the XWM and the mocked host. | |
| 15 | + | func newWin(t *testing.T, x *XWM, p *xpeer, h *mockhost, xid uint32) Slot { | |
| 16 | + | t.Helper() | |
| 17 | + | h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(xid)).Once() | |
| 18 | + | p.createNotify(xid, false) | |
| 19 | + | readUntil(t, x, func() bool { return x.Slot(xid) != 0 }) | |
| 20 | + | s := x.Slot(xid) | |
| 21 | + | r := x.tab.rec(s) | |
| 22 | + | r.ready, r.w, r.h = true, 300, 200 | |
| 23 | + | return s | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // settle flushes the XWM and waits until the peer has seen everything sent so | |
| 27 | + | // far; it returns the number of requests received. | |
| 28 | + | func settle(t *testing.T, x *XWM, p *xpeer) int { | |
| 29 | + | t.Helper() | |
| 30 | + | x.flush() | |
| 31 | + | n := len(p.requests()) | |
| 32 | + | xproto.GetInputFocus(x.conn.Buf()) | |
| 33 | + | require.NoError(t, x.conn.Flush()) | |
| 34 | + | _, n = p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |
| 35 | + | return n | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // since returns the requests of the given major opcode received after skip. | |
| 39 | + | func since(p *xpeer, skip int, major uint8) []xpeerReq { | |
| 40 | + | var out []xpeerReq | |
| 41 | + | for _, r := range p.requests()[skip:] { | |
| 42 | + | if r.major == major { | |
| 43 | + | out = append(out, r) | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | return out | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // sentEvents returns the 32-byte events of the SendEvent requests to dest. | |
| 50 | + | func sentEvents(p *xpeer, skip int, dest uint32) [][]byte { | |
| 51 | + | var out [][]byte | |
| 52 | + | for _, r := range since(p, skip, opSendEvent) { | |
| 53 | + | if r.u32(4) == dest { | |
| 54 | + | out = append(out, r.raw[12:44]) | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | return out | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func propsTo(p *xpeer, skip int, win uint32, atom string) []xpeerReq { | |
| 61 | + | var out []xpeerReq | |
| 62 | + | a := p.atom(atom) | |
| 63 | + | for _, r := range since(p, skip, opChangeProperty) { | |
| 64 | + | if r.u32(4) == win && r.u32(8) == a { | |
| 65 | + | out = append(out, r) | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | return out | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | func (x *XWM) feed(t *testing.T, p *xpeer, send func()) { | |
| 72 | + | t.Helper() | |
| 73 | + | ev := x.stats.Events | |
| 74 | + | send() | |
| 75 | + | readUntil(t, x, func() bool { return x.stats.Events > ev }) | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func TestConfigureCoalescesIntoOneRequest(t *testing.T) { | |
| 79 | + | x, p, h, _, runs := attached(t) | |
| 80 | + | const win = 0x500001 | |
| 81 | + | s := newWin(t, x, p, h, win) | |
| 82 | + | n := settle(t, x, p) | |
| 83 | + | ||
| 84 | + | x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480}) | |
| 85 | + | x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true}) | |
| 86 | + | x.Place(s, 10, 20) | |
| 87 | + | require.Len(t, *runs, 1, "one flush posted") | |
| 88 | + | n2 := settle(t, x, p) | |
| 89 | + | ||
| 90 | + | cw := since(p, n, opConfigureWindow) | |
| 91 | + | require.Len(t, cw, 1) | |
| 92 | + | require.Equal(t, uint32(win), cw[0].u32(4)) | |
| 93 | + | require.EqualValues(t, 0x1f, binary.LittleEndian.Uint16(cw[0].raw[8:]), "x y width height border") | |
| 94 | + | require.Equal(t, []uint32{10, 20, 800, 600, 0}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24), cw[0].u32(28)}) | |
| 95 | + | ||
| 96 | + | evs := sentEvents(p, n, win) | |
| 97 | + | require.Len(t, evs, 1, "one synthetic ConfigureNotify") | |
| 98 | + | require.EqualValues(t, 22, evs[0][0]) | |
| 99 | + | require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][4:])) | |
| 100 | + | require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][8:])) | |
| 101 | + | require.EqualValues(t, 10, binary.LittleEndian.Uint16(evs[0][16:])) | |
| 102 | + | require.EqualValues(t, 20, binary.LittleEndian.Uint16(evs[0][18:])) | |
| 103 | + | require.EqualValues(t, 800, binary.LittleEndian.Uint16(evs[0][20:])) | |
| 104 | + | require.EqualValues(t, 600, binary.LittleEndian.Uint16(evs[0][22:])) | |
| 105 | + | require.Equal(t, xproto.EventMaskStructureNotify, xproto.EventMask(since(p, n, opSendEvent)[0].u32(8))) | |
| 106 | + | ||
| 107 | + | // Unchanged: nothing is sent. | |
| 108 | + | x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true}) | |
| 109 | + | x.Place(s, 10, 20) | |
| 110 | + | settle(t, x, p) | |
| 111 | + | require.Len(t, since(p, n2, opConfigureWindow), 0) | |
| 112 | + | require.Len(t, sentEvents(p, n2, win), 0) | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | func TestConfigureWritesNetWMState(t *testing.T) { | |
| 116 | + | x, p, h, _, _ := attached(t) | |
| 117 | + | const win = 0x500001 | |
| 118 | + | s := newWin(t, x, p, h, win) | |
| 119 | + | x.tab.rec(s).modal = true | |
| 120 | + | n := settle(t, x, p) | |
| 121 | + | ||
| 122 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100}) | |
| 123 | + | settle(t, x, p) | |
| 124 | + | require.Empty(t, propsTo(p, n, win, "_NET_WM_STATE"), "no state bit changed") | |
| 125 | + | ||
| 126 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Fullscreen: true, Activated: true}) | |
| 127 | + | settle(t, x, p) | |
| 128 | + | st, ok := p.prop(win, "_NET_WM_STATE") | |
| 129 | + | require.True(t, ok) | |
| 130 | + | require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FULLSCREEN"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |
| 131 | + | ||
| 132 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Activated: true}) | |
| 133 | + | settle(t, x, p) | |
| 134 | + | st, _ = p.prop(win, "_NET_WM_STATE") | |
| 135 | + | require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | func TestConfigureBadWindowIsAttributedAndIgnored(t *testing.T) { | |
| 139 | + | x, p, h, _, _ := attached(t) | |
| 140 | + | const win = 0x500001 | |
| 141 | + | s := newWin(t, x, p, h, win) | |
| 142 | + | n := settle(t, x, p) | |
| 143 | + | x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480}) | |
| 144 | + | x.flush() | |
| 145 | + | cw := []xpeerReq{} | |
| 146 | + | r, _ := p.waitReq(n, func(r xpeerReq) bool { return r.major == opConfigureWindow }) | |
| 147 | + | cw = append(cw, r) | |
| 148 | + | ||
| 149 | + | p.mu.Lock() | |
| 150 | + | e := make([]byte, 32) | |
| 151 | + | e[1] = xproto.BadWindow | |
| 152 | + | binary.LittleEndian.PutUint16(e[2:], cw[0].seq) | |
| 153 | + | binary.LittleEndian.PutUint32(e[4:], win) | |
| 154 | + | e[10] = opConfigureWindow | |
| 155 | + | p.write(e) | |
| 156 | + | p.mu.Unlock() | |
| 157 | + | readUntil(t, x, func() bool { return x.stats.Errors == 1 }) | |
| 158 | + | require.Zero(t, x.stats.Unattributed) | |
| 159 | + | require.False(t, x.closed) | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | func sendConfigureRequest(p *xpeer, win uint32, mask uint16, x, y int16, w, h uint16) { | |
| 163 | + | p.event(23, func(b *[32]byte) { | |
| 164 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 165 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 166 | + | binary.LittleEndian.PutUint16(b[16:], uint16(x)) | |
| 167 | + | binary.LittleEndian.PutUint16(b[18:], uint16(y)) | |
| 168 | + | binary.LittleEndian.PutUint16(b[20:], w) | |
| 169 | + | binary.LittleEndian.PutUint16(b[22:], h) | |
| 170 | + | binary.LittleEndian.PutUint16(b[26:], mask) | |
| 171 | + | }) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | func TestConfigureRequestTiledOnlyGetsNotify(t *testing.T) { | |
| 175 | + | x, p, h, _, _ := attached(t) | |
| 176 | + | const win = 0x500001 | |
| 177 | + | newWin(t, x, p, h, win) | |
| 178 | + | n := settle(t, x, p) | |
| 179 | + | ||
| 180 | + | x.feed(t, p, func() { sendConfigureRequest(p, win, 0xf, 5, 6, 1000, 900) }) | |
| 181 | + | settle(t, x, p) | |
| 182 | + | require.Empty(t, since(p, n, opConfigureWindow), "a tiled window cannot resize itself") | |
| 183 | + | evs := sentEvents(p, n, win) | |
| 184 | + | require.Len(t, evs, 1) | |
| 185 | + | require.EqualValues(t, 22, evs[0][0]) | |
| 186 | + | require.EqualValues(t, 300, binary.LittleEndian.Uint16(evs[0][20:]), "current geometry") | |
| 187 | + | require.EqualValues(t, 200, binary.LittleEndian.Uint16(evs[0][22:])) | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | func TestConfigureRequestFloatingHonoursSize(t *testing.T) { | |
| 191 | + | x, p, h, _, _ := attached(t) | |
| 192 | + | const win = 0x500001 | |
| 193 | + | s := newWin(t, x, p, h, win) | |
| 194 | + | x.tab.rec(s).floating = true | |
| 195 | + | n := settle(t, x, p) | |
| 196 | + | ||
| 197 | + | x.feed(t, p, func() { | |
| 198 | + | sendConfigureRequest(p, win, uint16(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight), 0, 0, 70000&0xffff, 20000) | |
| 199 | + | }) | |
| 200 | + | settle(t, x, p) | |
| 201 | + | cw := since(p, n, opConfigureWindow) | |
| 202 | + | require.Len(t, cw, 1) | |
| 203 | + | require.EqualValues(t, xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, binary.LittleEndian.Uint16(cw[0].raw[8:])) | |
| 204 | + | require.EqualValues(t, 4464, cw[0].u32(12)) | |
| 205 | + | require.EqualValues(t, maxClientSize, cw[0].u32(16), "clamped") | |
| 206 | + | require.Len(t, sentEvents(p, n, win), 1) | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | func TestConfigureRequestUnmanagedHonouredVerbatim(t *testing.T) { | |
| 210 | + | x, p, _, _, _ := attached(t) | |
| 211 | + | const win = 0x500009 | |
| 212 | + | p.createNotify(win, true) | |
| 213 | + | readUntil(t, x, func() bool { return x.Slot(win) != 0 }) | |
| 214 | + | n := settle(t, x, p) | |
| 215 | + | ||
| 216 | + | x.feed(t, p, func() { | |
| 217 | + | sendConfigureRequest(p, win, uint16(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth), 30, 40, 50, 60) | |
| 218 | + | }) | |
| 219 | + | settle(t, x, p) | |
| 220 | + | cw := since(p, n, opConfigureWindow) | |
| 221 | + | require.Len(t, cw, 1) | |
| 222 | + | require.EqualValues(t, 7, binary.LittleEndian.Uint16(cw[0].raw[8:])) | |
| 223 | + | require.Equal(t, []uint32{30, 40, 50}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20)}) | |
| 224 | + | require.Empty(t, sentEvents(p, n, win)) | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | func TestConfigureStaleAndDuplicateDirtySlots(t *testing.T) { | |
| 228 | + | x, p, h, _, _ := attached(t) | |
| 229 | + | const a, b = 0x500001, 0x500002 | |
| 230 | + | s := newWin(t, x, p, h, a) | |
| 231 | + | for i := 1; i <= 50; i++ { | |
| 232 | + | x.Configure(s, ports.ConfigureWindow{Width: 100 + i, Height: 100}) | |
| 233 | + | } | |
| 234 | + | require.Len(t, x.dirty, 1, "queued once per false→true transition") | |
| 235 | + | ||
| 236 | + | // The window dies with its entry queued; the slot is reused. | |
| 237 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 238 | + | p.destroyNotify(a) | |
| 239 | + | readUntil(t, x, func() bool { return x.Slot(a) == 0 }) | |
| 240 | + | s2 := newWin(t, x, p, h, b) | |
| 241 | + | require.Equal(t, s, s2, "LIFO slot reuse") | |
| 242 | + | n := len(p.requests()) | |
| 243 | + | x.Configure(s2, ports.ConfigureWindow{Width: 640, Height: 480}) | |
| 244 | + | require.Len(t, x.dirty, 1) | |
| 245 | + | x.dirty = append(x.dirty, s, s) // stale and duplicate entries must be skipped | |
| 246 | + | settle(t, x, p) | |
| 247 | + | cw := since(p, n, opConfigureWindow) | |
| 248 | + | require.Len(t, cw, 1, "sent once despite duplicates") | |
| 249 | + | require.Equal(t, uint32(b), cw[0].u32(4)) | |
| 250 | + | require.Empty(t, x.dirty) | |
| 251 | + | require.LessOrEqual(t, cap(x.dirty), len(x.tab.recs)) | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | func TestDirtyQueueNeverGrows(t *testing.T) { | |
| 255 | + | x, p, h, _, _ := attached(t) | |
| 256 | + | c0 := cap(x.dirty) | |
| 257 | + | const base = 0x600000 | |
| 258 | + | for i := 0; i < 3*c0; i++ { // each round leaves a stale entry behind | |
| 259 | + | xid := uint32(base + i) | |
| 260 | + | h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(1)).Once() | |
| 261 | + | p.createNotify(xid, false) | |
| 262 | + | readUntil(t, x, func() bool { return x.Slot(xid) != 0 }) | |
| 263 | + | s := x.Slot(xid) | |
| 264 | + | x.tab.rec(s).ready = true | |
| 265 | + | x.Configure(s, ports.ConfigureWindow{Width: 10, Height: 10}) | |
| 266 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 267 | + | p.destroyNotify(xid) | |
| 268 | + | readUntil(t, x, func() bool { return x.Slot(xid) == 0 }) | |
| 269 | + | } | |
| 270 | + | require.Equal(t, c0, cap(x.dirty)) | |
| 271 | + | require.LessOrEqual(t, len(x.dirty), c0) | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | func TestPublicCommandsIgnoredWhenClosed(t *testing.T) { | |
| 275 | + | x, p, h, src, _ := attached(t) | |
| 276 | + | const win = 0x500001 | |
| 277 | + | s := newWin(t, x, p, h, win) | |
| 278 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 279 | + | src.EXPECT().Remove().Once() | |
| 280 | + | x.Close() | |
| 281 | + | require.NotPanics(t, func() { | |
| 282 | + | x.Configure(s, ports.ConfigureWindow{Width: 1, Height: 1}) | |
| 283 | + | x.Place(s, 1, 2) | |
| 284 | + | x.Focus(s) | |
| 285 | + | x.CloseWindow(s) | |
| 286 | + | }) | |
| 287 | + | require.Empty(t, x.dirty) | |
| 288 | + | require.Zero(t, x.active) | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | // Focus. | |
| 292 | + | ||
| 293 | + | type focusModel struct { | |
| 294 | + | name string | |
| 295 | + | input, take bool | |
| 296 | + | setFocus bool // SetInputFocus on the window | |
| 297 | + | takeFocus bool // WM_TAKE_FOCUS sent | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | func TestFocusInputModels(t *testing.T) { | |
| 301 | + | for _, m := range []focusModel{ | |
| 302 | + | {"passive", true, false, true, false}, | |
| 303 | + | {"locally active", true, true, true, true}, | |
| 304 | + | {"globally active", false, true, false, true}, | |
| 305 | + | {"no input", false, false, false, false}, | |
| 306 | + | } { | |
| 307 | + | t.Run(m.name, func(t *testing.T) { | |
| 308 | + | x, p, h, _, _ := attached(t) | |
| 309 | + | const win = 0x500001 | |
| 310 | + | s := newWin(t, x, p, h, win) | |
| 311 | + | r := x.tab.rec(s) | |
| 312 | + | r.input, r.takeFocus = m.input, m.take | |
| 313 | + | n := settle(t, x, p) | |
| 314 | + | ||
| 315 | + | x.Focus(s) | |
| 316 | + | settle(t, x, p) | |
| 317 | + | var focuses []xpeerReq | |
| 318 | + | for _, r := range since(p, n, opSetInputFocus) { | |
| 319 | + | if r.u32(4) == win { | |
| 320 | + | focuses = append(focuses, r) | |
| 321 | + | } | |
| 322 | + | } | |
| 323 | + | if m.setFocus { | |
| 324 | + | require.Len(t, focuses, 1) | |
| 325 | + | require.EqualValues(t, xproto.InputFocusNone, focuses[0].raw[1], "never PointerRoot") | |
| 326 | + | require.Zero(t, focuses[0].u32(8), "CurrentTime") | |
| 327 | + | } else { | |
| 328 | + | require.Empty(t, focuses) | |
| 329 | + | } | |
| 330 | + | var takes int | |
| 331 | + | for _, ev := range sentEvents(p, n, win) { | |
| 332 | + | if ev[0] == 33 && binary.LittleEndian.Uint32(ev[8:]) == p.atom("WM_PROTOCOLS") { | |
| 333 | + | require.EqualValues(t, 32, ev[1]) | |
| 334 | + | require.Equal(t, p.atom("WM_TAKE_FOCUS"), binary.LittleEndian.Uint32(ev[12:])) | |
| 335 | + | require.Zero(t, binary.LittleEndian.Uint32(ev[16:]), "CurrentTime") | |
| 336 | + | takes++ | |
| 337 | + | } | |
| 338 | + | } | |
| 339 | + | require.Equal(t, map[bool]int{true: 1, false: 0}[m.takeFocus], takes) | |
| 340 | + | ||
| 341 | + | act, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 342 | + | require.True(t, ok) | |
| 343 | + | require.Equal(t, []uint32{win}, atomsOf(act), "named even for No-input windows") | |
| 344 | + | st, ok := p.prop(win, "_NET_WM_STATE") | |
| 345 | + | require.True(t, ok) | |
| 346 | + | require.Contains(t, atomsOf(st), p.atom("_NET_WM_STATE_FOCUSED")) | |
| 347 | + | }) | |
| 348 | + | } | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | func TestFocusActiveWindowWrittenOnlyOnChange(t *testing.T) { | |
| 352 | + | x, p, h, _, _ := attached(t) | |
| 353 | + | const a, b = 0x500001, 0x500002 | |
| 354 | + | sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b) | |
| 355 | + | n := settle(t, x, p) | |
| 356 | + | ||
| 357 | + | x.Focus(sa) | |
| 358 | + | x.Focus(sa) | |
| 359 | + | settle(t, x, p) | |
| 360 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_ACTIVE_WINDOW"), 1) | |
| 361 | + | ||
| 362 | + | x.Focus(sb) | |
| 363 | + | settle(t, x, p) | |
| 364 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 365 | + | require.Equal(t, []uint32{b}, atomsOf(act)) | |
| 366 | + | sta, _ := p.prop(a, "_NET_WM_STATE") | |
| 367 | + | require.NotContains(t, atomsOf(sta), p.atom("_NET_WM_STATE_FOCUSED"), "old window loses FOCUSED") | |
| 368 | + | stb, _ := p.prop(b, "_NET_WM_STATE") | |
| 369 | + | require.Contains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED")) | |
| 370 | + | ||
| 371 | + | n = settle(t, x, p) | |
| 372 | + | x.Focus(0) | |
| 373 | + | settle(t, x, p) | |
| 374 | + | none := since(p, n, opSetInputFocus) | |
| 375 | + | require.Len(t, none, 1) | |
| 376 | + | require.Zero(t, none[0].u32(4), "focus None") | |
| 377 | + | require.EqualValues(t, xproto.InputFocusNone, none[0].raw[1]) | |
| 378 | + | act, _ = p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 379 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |
| 380 | + | stb, _ = p.prop(b, "_NET_WM_STATE") | |
| 381 | + | require.NotContains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED")) | |
| 382 | + | require.Zero(t, x.active) | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | func TestFocusStackingLists(t *testing.T) { | |
| 386 | + | x, p, h, _, _ := attached(t) | |
| 387 | + | const a, b, c = 0x500001, 0x500002, 0x500003 | |
| 388 | + | sa := newWin(t, x, p, h, a) | |
| 389 | + | newWin(t, x, p, h, b) | |
| 390 | + | newWin(t, x, p, h, c) | |
| 391 | + | for _, w := range []uint32{a, b, c} { | |
| 392 | + | x.addClient(w) | |
| 393 | + | } | |
| 394 | + | settle(t, x, p) | |
| 395 | + | list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 396 | + | require.Equal(t, []uint32{a, b, c}, atomsOf(list)) | |
| 397 | + | n := settle(t, x, p) | |
| 398 | + | ||
| 399 | + | x.Focus(sa) | |
| 400 | + | settle(t, x, p) | |
| 401 | + | stack, _ := p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING") | |
| 402 | + | require.Equal(t, []uint32{b, c, a}, atomsOf(stack), "bottom to top") | |
| 403 | + | list, _ = p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 404 | + | require.Equal(t, []uint32{a, b, c}, atomsOf(list), "map order") | |
| 405 | + | require.Empty(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), "clients list untouched by focus") | |
| 406 | + | var raised bool | |
| 407 | + | for _, r := range since(p, n, opConfigureWindow) { | |
| 408 | + | if r.u32(4) == a && binary.LittleEndian.Uint16(r.raw[8:]) == uint16(xproto.ConfigWindowStackMode) { | |
| 409 | + | require.EqualValues(t, xproto.StackModeAbove, r.u32(12)) | |
| 410 | + | raised = true | |
| 411 | + | } | |
| 412 | + | } | |
| 413 | + | require.True(t, raised) | |
| 414 | + | ||
| 415 | + | // A burst costs one write per list. | |
| 416 | + | n = settle(t, x, p) | |
| 417 | + | x.removeClient(b) | |
| 418 | + | x.removeClient(c) | |
| 419 | + | settle(t, x, p) | |
| 420 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), 1) | |
| 421 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST_STACKING"), 1) | |
| 422 | + | stack, _ = p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING") | |
| 423 | + | require.Equal(t, []uint32{a}, atomsOf(stack)) | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | func TestDestroyedActiveWindowClearsActive(t *testing.T) { | |
| 427 | + | x, p, h, _, _ := attached(t) | |
| 428 | + | const win = 0x500001 | |
| 429 | + | s := newWin(t, x, p, h, win) | |
| 430 | + | x.addClient(win) | |
| 431 | + | x.Focus(s) | |
| 432 | + | settle(t, x, p) | |
| 433 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 434 | + | p.destroyNotify(win) | |
| 435 | + | readUntil(t, x, func() bool { return x.Slot(win) == 0 }) | |
| 436 | + | settle(t, x, p) | |
| 437 | + | require.Zero(t, x.active) | |
| 438 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 439 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |
| 440 | + | list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 441 | + | require.Empty(t, list.data) | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | func TestFocusInDenied(t *testing.T) { | |
| 445 | + | x, p, h, _, _ := attached(t) | |
| 446 | + | const a, b = 0x500001, 0x500002 | |
| 447 | + | sa := newWin(t, x, p, h, a) | |
| 448 | + | newWin(t, x, p, h, b) | |
| 449 | + | x.Focus(sa) | |
| 450 | + | n := settle(t, x, p) | |
| 451 | + | ||
| 452 | + | focusIn := func(win uint32, detail byte) { | |
| 453 | + | p.event(9, func(e *[32]byte) { | |
| 454 | + | e[1] = detail | |
| 455 | + | binary.LittleEndian.PutUint32(e[4:], win) | |
| 456 | + | }) | |
| 457 | + | } | |
| 458 | + | focusCount := func(skip int) int { | |
| 459 | + | c := 0 | |
| 460 | + | for _, r := range since(p, skip, opSetInputFocus) { | |
| 461 | + | if r.u32(4) == a { | |
| 462 | + | c++ | |
| 463 | + | } | |
| 464 | + | } | |
| 465 | + | return c | |
| 466 | + | } | |
| 467 | + | x.feed(t, p, func() { focusIn(a, 0) }) // the active window itself: fine | |
| 468 | + | x.feed(t, p, func() { focusIn(b, byte(xproto.NotifyDetailPointer)) }) | |
| 469 | + | settle(t, x, p) | |
| 470 | + | require.Zero(t, focusCount(n)) | |
| 471 | + | ||
| 472 | + | x.feed(t, p, func() { focusIn(b, 0); focusIn(b, 0) }) | |
| 473 | + | readUntil(t, x, func() bool { return x.stats.Events >= 5 }) | |
| 474 | + | settle(t, x, p) | |
| 475 | + | require.Equal(t, 1, focusCount(n), "denied once per iteration") | |
| 476 | + | n = settle(t, x, p) | |
| 477 | + | settle(t, x, p) | |
| 478 | + | require.Zero(t, focusCount(n)) | |
| 479 | + | } | |
| 480 | + | ||
| 481 | + | func TestCloseWindow(t *testing.T) { | |
| 482 | + | x, p, h, _, _ := attached(t) | |
| 483 | + | const a, b = 0x500001, 0x500002 | |
| 484 | + | sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b) | |
| 485 | + | x.tab.rec(sa).deleteWindow = true | |
| 486 | + | n := settle(t, x, p) | |
| 487 | + | ||
| 488 | + | x.CloseWindow(sa) | |
| 489 | + | x.CloseWindow(sb) | |
| 490 | + | settle(t, x, p) | |
| 491 | + | evs := sentEvents(p, n, a) | |
| 492 | + | require.Len(t, evs, 1) | |
| 493 | + | require.EqualValues(t, 33, evs[0][0]) | |
| 494 | + | require.EqualValues(t, 32, evs[0][1]) | |
| 495 | + | require.Equal(t, p.atom("WM_PROTOCOLS"), binary.LittleEndian.Uint32(evs[0][8:])) | |
| 496 | + | require.Equal(t, p.atom("WM_DELETE_WINDOW"), binary.LittleEndian.Uint32(evs[0][12:])) | |
| 497 | + | require.Zero(t, binary.LittleEndian.Uint32(evs[0][16:])) | |
| 498 | + | require.Empty(t, sentEvents(p, n, b)) | |
| 499 | + | ||
| 500 | + | kills := since(p, n, opKillClient) | |
| 501 | + | require.Len(t, kills, 1) | |
| 502 | + | require.Equal(t, uint32(b), kills[0].u32(4)) | |
| 503 | + | } | |
| 504 | + | ||
| 505 | + | // Allocation guards. The peer sinks requests so the connection never blocks; | |
| 506 | + | // its goroutine is idle in the sink loop before measuring. | |
| 507 | + | ||
| 508 | + | func sinkPeer(t *testing.T, x *XWM, p *xpeer) { | |
| 509 | + | t.Helper() | |
| 510 | + | p.sink.Store(true) | |
| 511 | + | n := len(p.requests()) | |
| 512 | + | xproto.GetInputFocus(x.conn.Buf()) // served by the old loop iteration | |
| 513 | + | require.NoError(t, x.conn.Flush()) | |
| 514 | + | p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |
| 515 | + | time.Sleep(50 * time.Millisecond) | |
| 516 | + | x.src = nil // no source: write backpressure would not allocate either | |
| 517 | + | x.post = func(func()) {} | |
| 518 | + | x.warnedRing = true // the sink never answers: Expect slots stay used | |
| 519 | + | } | |
| 520 | + | ||
| 521 | + | func TestConfigureFlushAllocations(t *testing.T) { | |
| 522 | + | x, p, h, _, _ := attached(t) | |
| 523 | + | s := newWin(t, x, p, h, 0x500001) | |
| 524 | + | sinkPeer(t, x, p) | |
| 525 | + | i := 0 | |
| 526 | + | allocs := testing.AllocsPerRun(300, func() { | |
| 527 | + | i++ | |
| 528 | + | x.Configure(s, ports.ConfigureWindow{Width: 100 + i%2, Height: 100, Fullscreen: i%3 == 0, Activated: i%2 == 0}) | |
| 529 | + | x.Place(s, i%5, i%7) | |
| 530 | + | x.posted = false | |
| 531 | + | x.flush() | |
| 532 | + | }) | |
| 533 | + | require.Zero(t, allocs) | |
| 534 | + | require.False(t, x.closed) | |
| 535 | + | } | |
| 536 | + | ||
| 537 | + | func TestFocusAllocations(t *testing.T) { | |
| 538 | + | x, p, h, _, _ := attached(t) | |
| 539 | + | a, b := newWin(t, x, p, h, 0x500001), newWin(t, x, p, h, 0x500002) | |
| 540 | + | x.addClient(x.tab.rec(a).xid) | |
| 541 | + | x.addClient(x.tab.rec(b).xid) | |
| 542 | + | x.tab.rec(b).takeFocus = true | |
| 543 | + | x.tab.rec(a).input, x.tab.rec(b).input = true, true | |
| 544 | + | sinkPeer(t, x, p) | |
| 545 | + | i := 0 | |
| 546 | + | allocs := testing.AllocsPerRun(150, func() { | |
| 547 | + | i++ | |
| 548 | + | switch i % 3 { | |
| 549 | + | case 0: | |
| 550 | + | x.Focus(a) | |
| 551 | + | case 1: | |
| 552 | + | x.Focus(b) | |
| 553 | + | default: | |
| 554 | + | x.Focus(0) | |
| 555 | + | } | |
| 556 | + | x.refocus = i%5 == 0 | |
| 557 | + | x.posted = false | |
| 558 | + | x.flush() | |
| 559 | + | }) | |
| 560 | + | require.Zero(t, allocs) | |
| 561 | + | require.False(t, x.closed) | |
| 562 | + | } | |
| @@ -0,0 +1,562 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "bnema.dev/xturbo/xproto" | |||
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 10 | + | "github.com/stretchr/testify/mock" | |||
| 11 | + | "github.com/stretchr/testify/require" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // newWin makes a ready managed window known to the XWM and the mocked host. | |||
| 15 | + | func newWin(t *testing.T, x *XWM, p *xpeer, h *mockhost, xid uint32) Slot { | |||
| 16 | + | t.Helper() | |||
| 17 | + | h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(xid)).Once() | |||
| 18 | + | p.createNotify(xid, false) | |||
| 19 | + | readUntil(t, x, func() bool { return x.Slot(xid) != 0 }) | |||
| 20 | + | s := x.Slot(xid) | |||
| 21 | + | r := x.tab.rec(s) | |||
| 22 | + | r.ready, r.w, r.h = true, 300, 200 | |||
| 23 | + | return s | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // settle flushes the XWM and waits until the peer has seen everything sent so | |||
| 27 | + | // far; it returns the number of requests received. | |||
| 28 | + | func settle(t *testing.T, x *XWM, p *xpeer) int { | |||
| 29 | + | t.Helper() | |||
| 30 | + | x.flush() | |||
| 31 | + | n := len(p.requests()) | |||
| 32 | + | xproto.GetInputFocus(x.conn.Buf()) | |||
| 33 | + | require.NoError(t, x.conn.Flush()) | |||
| 34 | + | _, n = p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |||
| 35 | + | return n | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // since returns the requests of the given major opcode received after skip. | |||
| 39 | + | func since(p *xpeer, skip int, major uint8) []xpeerReq { | |||
| 40 | + | var out []xpeerReq | |||
| 41 | + | for _, r := range p.requests()[skip:] { | |||
| 42 | + | if r.major == major { | |||
| 43 | + | out = append(out, r) | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | return out | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // sentEvents returns the 32-byte events of the SendEvent requests to dest. | |||
| 50 | + | func sentEvents(p *xpeer, skip int, dest uint32) [][]byte { | |||
| 51 | + | var out [][]byte | |||
| 52 | + | for _, r := range since(p, skip, opSendEvent) { | |||
| 53 | + | if r.u32(4) == dest { | |||
| 54 | + | out = append(out, r.raw[12:44]) | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | return out | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func propsTo(p *xpeer, skip int, win uint32, atom string) []xpeerReq { | |||
| 61 | + | var out []xpeerReq | |||
| 62 | + | a := p.atom(atom) | |||
| 63 | + | for _, r := range since(p, skip, opChangeProperty) { | |||
| 64 | + | if r.u32(4) == win && r.u32(8) == a { | |||
| 65 | + | out = append(out, r) | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | return out | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | func (x *XWM) feed(t *testing.T, p *xpeer, send func()) { | |||
| 72 | + | t.Helper() | |||
| 73 | + | ev := x.stats.Events | |||
| 74 | + | send() | |||
| 75 | + | readUntil(t, x, func() bool { return x.stats.Events > ev }) | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func TestConfigureCoalescesIntoOneRequest(t *testing.T) { | |||
| 79 | + | x, p, h, _, runs := attached(t) | |||
| 80 | + | const win = 0x500001 | |||
| 81 | + | s := newWin(t, x, p, h, win) | |||
| 82 | + | n := settle(t, x, p) | |||
| 83 | + | ||||
| 84 | + | x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480}) | |||
| 85 | + | x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true}) | |||
| 86 | + | x.Place(s, 10, 20) | |||
| 87 | + | require.Len(t, *runs, 1, "one flush posted") | |||
| 88 | + | n2 := settle(t, x, p) | |||
| 89 | + | ||||
| 90 | + | cw := since(p, n, opConfigureWindow) | |||
| 91 | + | require.Len(t, cw, 1) | |||
| 92 | + | require.Equal(t, uint32(win), cw[0].u32(4)) | |||
| 93 | + | require.EqualValues(t, 0x1f, binary.LittleEndian.Uint16(cw[0].raw[8:]), "x y width height border") | |||
| 94 | + | require.Equal(t, []uint32{10, 20, 800, 600, 0}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24), cw[0].u32(28)}) | |||
| 95 | + | ||||
| 96 | + | evs := sentEvents(p, n, win) | |||
| 97 | + | require.Len(t, evs, 1, "one synthetic ConfigureNotify") | |||
| 98 | + | require.EqualValues(t, 22, evs[0][0]) | |||
| 99 | + | require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][4:])) | |||
| 100 | + | require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][8:])) | |||
| 101 | + | require.EqualValues(t, 10, binary.LittleEndian.Uint16(evs[0][16:])) | |||
| 102 | + | require.EqualValues(t, 20, binary.LittleEndian.Uint16(evs[0][18:])) | |||
| 103 | + | require.EqualValues(t, 800, binary.LittleEndian.Uint16(evs[0][20:])) | |||
| 104 | + | require.EqualValues(t, 600, binary.LittleEndian.Uint16(evs[0][22:])) | |||
| 105 | + | require.Equal(t, xproto.EventMaskStructureNotify, xproto.EventMask(since(p, n, opSendEvent)[0].u32(8))) | |||
| 106 | + | ||||
| 107 | + | // Unchanged: nothing is sent. | |||
| 108 | + | x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true}) | |||
| 109 | + | x.Place(s, 10, 20) | |||
| 110 | + | settle(t, x, p) | |||
| 111 | + | require.Len(t, since(p, n2, opConfigureWindow), 0) | |||
| 112 | + | require.Len(t, sentEvents(p, n2, win), 0) | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | func TestConfigureWritesNetWMState(t *testing.T) { | |||
| 116 | + | x, p, h, _, _ := attached(t) | |||
| 117 | + | const win = 0x500001 | |||
| 118 | + | s := newWin(t, x, p, h, win) | |||
| 119 | + | x.tab.rec(s).modal = true | |||
| 120 | + | n := settle(t, x, p) | |||
| 121 | + | ||||
| 122 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100}) | |||
| 123 | + | settle(t, x, p) | |||
| 124 | + | require.Empty(t, propsTo(p, n, win, "_NET_WM_STATE"), "no state bit changed") | |||
| 125 | + | ||||
| 126 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Fullscreen: true, Activated: true}) | |||
| 127 | + | settle(t, x, p) | |||
| 128 | + | st, ok := p.prop(win, "_NET_WM_STATE") | |||
| 129 | + | require.True(t, ok) | |||
| 130 | + | require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FULLSCREEN"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |||
| 131 | + | ||||
| 132 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Activated: true}) | |||
| 133 | + | settle(t, x, p) | |||
| 134 | + | st, _ = p.prop(win, "_NET_WM_STATE") | |||
| 135 | + | require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | func TestConfigureBadWindowIsAttributedAndIgnored(t *testing.T) { | |||
| 139 | + | x, p, h, _, _ := attached(t) | |||
| 140 | + | const win = 0x500001 | |||
| 141 | + | s := newWin(t, x, p, h, win) | |||
| 142 | + | n := settle(t, x, p) | |||
| 143 | + | x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480}) | |||
| 144 | + | x.flush() | |||
| 145 | + | cw := []xpeerReq{} | |||
| 146 | + | r, _ := p.waitReq(n, func(r xpeerReq) bool { return r.major == opConfigureWindow }) | |||
| 147 | + | cw = append(cw, r) | |||
| 148 | + | ||||
| 149 | + | p.mu.Lock() | |||
| 150 | + | e := make([]byte, 32) | |||
| 151 | + | e[1] = xproto.BadWindow | |||
| 152 | + | binary.LittleEndian.PutUint16(e[2:], cw[0].seq) | |||
| 153 | + | binary.LittleEndian.PutUint32(e[4:], win) | |||
| 154 | + | e[10] = opConfigureWindow | |||
| 155 | + | p.write(e) | |||
| 156 | + | p.mu.Unlock() | |||
| 157 | + | readUntil(t, x, func() bool { return x.stats.Errors == 1 }) | |||
| 158 | + | require.Zero(t, x.stats.Unattributed) | |||
| 159 | + | require.False(t, x.closed) | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | func sendConfigureRequest(p *xpeer, win uint32, mask uint16, x, y int16, w, h uint16) { | |||
| 163 | + | p.event(23, func(b *[32]byte) { | |||
| 164 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |||
| 165 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 166 | + | binary.LittleEndian.PutUint16(b[16:], uint16(x)) | |||
| 167 | + | binary.LittleEndian.PutUint16(b[18:], uint16(y)) | |||
| 168 | + | binary.LittleEndian.PutUint16(b[20:], w) | |||
| 169 | + | binary.LittleEndian.PutUint16(b[22:], h) | |||
| 170 | + | binary.LittleEndian.PutUint16(b[26:], mask) | |||
| 171 | + | }) | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | func TestConfigureRequestTiledOnlyGetsNotify(t *testing.T) { | |||
| 175 | + | x, p, h, _, _ := attached(t) | |||
| 176 | + | const win = 0x500001 | |||
| 177 | + | newWin(t, x, p, h, win) | |||
| 178 | + | n := settle(t, x, p) | |||
| 179 | + | ||||
| 180 | + | x.feed(t, p, func() { sendConfigureRequest(p, win, 0xf, 5, 6, 1000, 900) }) | |||
| 181 | + | settle(t, x, p) | |||
| 182 | + | require.Empty(t, since(p, n, opConfigureWindow), "a tiled window cannot resize itself") | |||
| 183 | + | evs := sentEvents(p, n, win) | |||
| 184 | + | require.Len(t, evs, 1) | |||
| 185 | + | require.EqualValues(t, 22, evs[0][0]) | |||
| 186 | + | require.EqualValues(t, 300, binary.LittleEndian.Uint16(evs[0][20:]), "current geometry") | |||
| 187 | + | require.EqualValues(t, 200, binary.LittleEndian.Uint16(evs[0][22:])) | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | func TestConfigureRequestFloatingHonoursSize(t *testing.T) { | |||
| 191 | + | x, p, h, _, _ := attached(t) | |||
| 192 | + | const win = 0x500001 | |||
| 193 | + | s := newWin(t, x, p, h, win) | |||
| 194 | + | x.tab.rec(s).floating = true | |||
| 195 | + | n := settle(t, x, p) | |||
| 196 | + | ||||
| 197 | + | x.feed(t, p, func() { | |||
| 198 | + | sendConfigureRequest(p, win, uint16(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight), 0, 0, 70000&0xffff, 20000) | |||
| 199 | + | }) | |||
| 200 | + | settle(t, x, p) | |||
| 201 | + | cw := since(p, n, opConfigureWindow) | |||
| 202 | + | require.Len(t, cw, 1) | |||
| 203 | + | require.EqualValues(t, xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, binary.LittleEndian.Uint16(cw[0].raw[8:])) | |||
| 204 | + | require.EqualValues(t, 4464, cw[0].u32(12)) | |||
| 205 | + | require.EqualValues(t, maxClientSize, cw[0].u32(16), "clamped") | |||
| 206 | + | require.Len(t, sentEvents(p, n, win), 1) | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | func TestConfigureRequestUnmanagedHonouredVerbatim(t *testing.T) { | |||
| 210 | + | x, p, _, _, _ := attached(t) | |||
| 211 | + | const win = 0x500009 | |||
| 212 | + | p.createNotify(win, true) | |||
| 213 | + | readUntil(t, x, func() bool { return x.Slot(win) != 0 }) | |||
| 214 | + | n := settle(t, x, p) | |||
| 215 | + | ||||
| 216 | + | x.feed(t, p, func() { | |||
| 217 | + | sendConfigureRequest(p, win, uint16(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth), 30, 40, 50, 60) | |||
| 218 | + | }) | |||
| 219 | + | settle(t, x, p) | |||
| 220 | + | cw := since(p, n, opConfigureWindow) | |||
| 221 | + | require.Len(t, cw, 1) | |||
| 222 | + | require.EqualValues(t, 7, binary.LittleEndian.Uint16(cw[0].raw[8:])) | |||
| 223 | + | require.Equal(t, []uint32{30, 40, 50}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20)}) | |||
| 224 | + | require.Empty(t, sentEvents(p, n, win)) | |||
| 225 | + | } | |||
| 226 | + | ||||
| 227 | + | func TestConfigureStaleAndDuplicateDirtySlots(t *testing.T) { | |||
| 228 | + | x, p, h, _, _ := attached(t) | |||
| 229 | + | const a, b = 0x500001, 0x500002 | |||
| 230 | + | s := newWin(t, x, p, h, a) | |||
| 231 | + | for i := 1; i <= 50; i++ { | |||
| 232 | + | x.Configure(s, ports.ConfigureWindow{Width: 100 + i, Height: 100}) | |||
| 233 | + | } | |||
| 234 | + | require.Len(t, x.dirty, 1, "queued once per false→true transition") | |||
| 235 | + | ||||
| 236 | + | // The window dies with its entry queued; the slot is reused. | |||
| 237 | + | h.EXPECT().Dissociate(s, true).Once() | |||
| 238 | + | p.destroyNotify(a) | |||
| 239 | + | readUntil(t, x, func() bool { return x.Slot(a) == 0 }) | |||
| 240 | + | s2 := newWin(t, x, p, h, b) | |||
| 241 | + | require.Equal(t, s, s2, "LIFO slot reuse") | |||
| 242 | + | n := len(p.requests()) | |||
| 243 | + | x.Configure(s2, ports.ConfigureWindow{Width: 640, Height: 480}) | |||
| 244 | + | require.Len(t, x.dirty, 1) | |||
| 245 | + | x.dirty = append(x.dirty, s, s) // stale and duplicate entries must be skipped | |||
| 246 | + | settle(t, x, p) | |||
| 247 | + | cw := since(p, n, opConfigureWindow) | |||
| 248 | + | require.Len(t, cw, 1, "sent once despite duplicates") | |||
| 249 | + | require.Equal(t, uint32(b), cw[0].u32(4)) | |||
| 250 | + | require.Empty(t, x.dirty) | |||
| 251 | + | require.LessOrEqual(t, cap(x.dirty), len(x.tab.recs)) | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | func TestDirtyQueueNeverGrows(t *testing.T) { | |||
| 255 | + | x, p, h, _, _ := attached(t) | |||
| 256 | + | c0 := cap(x.dirty) | |||
| 257 | + | const base = 0x600000 | |||
| 258 | + | for i := 0; i < 3*c0; i++ { // each round leaves a stale entry behind | |||
| 259 | + | xid := uint32(base + i) | |||
| 260 | + | h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(1)).Once() | |||
| 261 | + | p.createNotify(xid, false) | |||
| 262 | + | readUntil(t, x, func() bool { return x.Slot(xid) != 0 }) | |||
| 263 | + | s := x.Slot(xid) | |||
| 264 | + | x.tab.rec(s).ready = true | |||
| 265 | + | x.Configure(s, ports.ConfigureWindow{Width: 10, Height: 10}) | |||
| 266 | + | h.EXPECT().Dissociate(s, true).Once() | |||
| 267 | + | p.destroyNotify(xid) | |||
| 268 | + | readUntil(t, x, func() bool { return x.Slot(xid) == 0 }) | |||
| 269 | + | } | |||
| 270 | + | require.Equal(t, c0, cap(x.dirty)) | |||
| 271 | + | require.LessOrEqual(t, len(x.dirty), c0) | |||
| 272 | + | } | |||
| 273 | + | ||||
| 274 | + | func TestPublicCommandsIgnoredWhenClosed(t *testing.T) { | |||
| 275 | + | x, p, h, src, _ := attached(t) | |||
| 276 | + | const win = 0x500001 | |||
| 277 | + | s := newWin(t, x, p, h, win) | |||
| 278 | + | h.EXPECT().Dissociate(s, true).Once() | |||
| 279 | + | src.EXPECT().Remove().Once() | |||
| 280 | + | x.Close() | |||
| 281 | + | require.NotPanics(t, func() { | |||
| 282 | + | x.Configure(s, ports.ConfigureWindow{Width: 1, Height: 1}) | |||
| 283 | + | x.Place(s, 1, 2) | |||
| 284 | + | x.Focus(s) | |||
| 285 | + | x.CloseWindow(s) | |||
| 286 | + | }) | |||
| 287 | + | require.Empty(t, x.dirty) | |||
| 288 | + | require.Zero(t, x.active) | |||
| 289 | + | } | |||
| 290 | + | ||||
| 291 | + | // Focus. | |||
| 292 | + | ||||
| 293 | + | type focusModel struct { | |||
| 294 | + | name string | |||
| 295 | + | input, take bool | |||
| 296 | + | setFocus bool // SetInputFocus on the window | |||
| 297 | + | takeFocus bool // WM_TAKE_FOCUS sent | |||
| 298 | + | } | |||
| 299 | + | ||||
| 300 | + | func TestFocusInputModels(t *testing.T) { | |||
| 301 | + | for _, m := range []focusModel{ | |||
| 302 | + | {"passive", true, false, true, false}, | |||
| 303 | + | {"locally active", true, true, true, true}, | |||
| 304 | + | {"globally active", false, true, false, true}, | |||
| 305 | + | {"no input", false, false, false, false}, | |||
| 306 | + | } { | |||
| 307 | + | t.Run(m.name, func(t *testing.T) { | |||
| 308 | + | x, p, h, _, _ := attached(t) | |||
| 309 | + | const win = 0x500001 | |||
| 310 | + | s := newWin(t, x, p, h, win) | |||
| 311 | + | r := x.tab.rec(s) | |||
| 312 | + | r.input, r.takeFocus = m.input, m.take | |||
| 313 | + | n := settle(t, x, p) | |||
| 314 | + | ||||
| 315 | + | x.Focus(s) | |||
| 316 | + | settle(t, x, p) | |||
| 317 | + | var focuses []xpeerReq | |||
| 318 | + | for _, r := range since(p, n, opSetInputFocus) { | |||
| 319 | + | if r.u32(4) == win { | |||
| 320 | + | focuses = append(focuses, r) | |||
| 321 | + | } | |||
| 322 | + | } | |||
| 323 | + | if m.setFocus { | |||
| 324 | + | require.Len(t, focuses, 1) | |||
| 325 | + | require.EqualValues(t, xproto.InputFocusNone, focuses[0].raw[1], "never PointerRoot") | |||
| 326 | + | require.Zero(t, focuses[0].u32(8), "CurrentTime") | |||
| 327 | + | } else { | |||
| 328 | + | require.Empty(t, focuses) | |||
| 329 | + | } | |||
| 330 | + | var takes int | |||
| 331 | + | for _, ev := range sentEvents(p, n, win) { | |||
| 332 | + | if ev[0] == 33 && binary.LittleEndian.Uint32(ev[8:]) == p.atom("WM_PROTOCOLS") { | |||
| 333 | + | require.EqualValues(t, 32, ev[1]) | |||
| 334 | + | require.Equal(t, p.atom("WM_TAKE_FOCUS"), binary.LittleEndian.Uint32(ev[12:])) | |||
| 335 | + | require.Zero(t, binary.LittleEndian.Uint32(ev[16:]), "CurrentTime") | |||
| 336 | + | takes++ | |||
| 337 | + | } | |||
| 338 | + | } | |||
| 339 | + | require.Equal(t, map[bool]int{true: 1, false: 0}[m.takeFocus], takes) | |||
| 340 | + | ||||
| 341 | + | act, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |||
| 342 | + | require.True(t, ok) | |||
| 343 | + | require.Equal(t, []uint32{win}, atomsOf(act), "named even for No-input windows") | |||
| 344 | + | st, ok := p.prop(win, "_NET_WM_STATE") | |||
| 345 | + | require.True(t, ok) | |||
| 346 | + | require.Contains(t, atomsOf(st), p.atom("_NET_WM_STATE_FOCUSED")) | |||
| 347 | + | }) | |||
| 348 | + | } | |||
| 349 | + | } | |||
| 350 | + | ||||
| 351 | + | func TestFocusActiveWindowWrittenOnlyOnChange(t *testing.T) { | |||
| 352 | + | x, p, h, _, _ := attached(t) | |||
| 353 | + | const a, b = 0x500001, 0x500002 | |||
| 354 | + | sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b) | |||
| 355 | + | n := settle(t, x, p) | |||
| 356 | + | ||||
| 357 | + | x.Focus(sa) | |||
| 358 | + | x.Focus(sa) | |||
| 359 | + | settle(t, x, p) | |||
| 360 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_ACTIVE_WINDOW"), 1) | |||
| 361 | + | ||||
| 362 | + | x.Focus(sb) | |||
| 363 | + | settle(t, x, p) | |||
| 364 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |||
| 365 | + | require.Equal(t, []uint32{b}, atomsOf(act)) | |||
| 366 | + | sta, _ := p.prop(a, "_NET_WM_STATE") | |||
| 367 | + | require.NotContains(t, atomsOf(sta), p.atom("_NET_WM_STATE_FOCUSED"), "old window loses FOCUSED") | |||
| 368 | + | stb, _ := p.prop(b, "_NET_WM_STATE") | |||
| 369 | + | require.Contains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED")) | |||
| 370 | + | ||||
| 371 | + | n = settle(t, x, p) | |||
| 372 | + | x.Focus(0) | |||
| 373 | + | settle(t, x, p) | |||
| 374 | + | none := since(p, n, opSetInputFocus) | |||
| 375 | + | require.Len(t, none, 1) | |||
| 376 | + | require.Zero(t, none[0].u32(4), "focus None") | |||
| 377 | + | require.EqualValues(t, xproto.InputFocusNone, none[0].raw[1]) | |||
| 378 | + | act, _ = p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |||
| 379 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |||
| 380 | + | stb, _ = p.prop(b, "_NET_WM_STATE") | |||
| 381 | + | require.NotContains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED")) | |||
| 382 | + | require.Zero(t, x.active) | |||
| 383 | + | } | |||
| 384 | + | ||||
| 385 | + | func TestFocusStackingLists(t *testing.T) { | |||
| 386 | + | x, p, h, _, _ := attached(t) | |||
| 387 | + | const a, b, c = 0x500001, 0x500002, 0x500003 | |||
| 388 | + | sa := newWin(t, x, p, h, a) | |||
| 389 | + | newWin(t, x, p, h, b) | |||
| 390 | + | newWin(t, x, p, h, c) | |||
| 391 | + | for _, w := range []uint32{a, b, c} { | |||
| 392 | + | x.addClient(w) | |||
| 393 | + | } | |||
| 394 | + | settle(t, x, p) | |||
| 395 | + | list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST") | |||
| 396 | + | require.Equal(t, []uint32{a, b, c}, atomsOf(list)) | |||
| 397 | + | n := settle(t, x, p) | |||
| 398 | + | ||||
| 399 | + | x.Focus(sa) | |||
| 400 | + | settle(t, x, p) | |||
| 401 | + | stack, _ := p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING") | |||
| 402 | + | require.Equal(t, []uint32{b, c, a}, atomsOf(stack), "bottom to top") | |||
| 403 | + | list, _ = p.prop(peerRoot, "_NET_CLIENT_LIST") | |||
| 404 | + | require.Equal(t, []uint32{a, b, c}, atomsOf(list), "map order") | |||
| 405 | + | require.Empty(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), "clients list untouched by focus") | |||
| 406 | + | var raised bool | |||
| 407 | + | for _, r := range since(p, n, opConfigureWindow) { | |||
| 408 | + | if r.u32(4) == a && binary.LittleEndian.Uint16(r.raw[8:]) == uint16(xproto.ConfigWindowStackMode) { | |||
| 409 | + | require.EqualValues(t, xproto.StackModeAbove, r.u32(12)) | |||
| 410 | + | raised = true | |||
| 411 | + | } | |||
| 412 | + | } | |||
| 413 | + | require.True(t, raised) | |||
| 414 | + | ||||
| 415 | + | // A burst costs one write per list. | |||
| 416 | + | n = settle(t, x, p) | |||
| 417 | + | x.removeClient(b) | |||
| 418 | + | x.removeClient(c) | |||
| 419 | + | settle(t, x, p) | |||
| 420 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), 1) | |||
| 421 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST_STACKING"), 1) | |||
| 422 | + | stack, _ = p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING") | |||
| 423 | + | require.Equal(t, []uint32{a}, atomsOf(stack)) | |||
| 424 | + | } | |||
| 425 | + | ||||
| 426 | + | func TestDestroyedActiveWindowClearsActive(t *testing.T) { | |||
| 427 | + | x, p, h, _, _ := attached(t) | |||
| 428 | + | const win = 0x500001 | |||
| 429 | + | s := newWin(t, x, p, h, win) | |||
| 430 | + | x.addClient(win) | |||
| 431 | + | x.Focus(s) | |||
| 432 | + | settle(t, x, p) | |||
| 433 | + | h.EXPECT().Dissociate(s, true).Once() | |||
| 434 | + | p.destroyNotify(win) | |||
| 435 | + | readUntil(t, x, func() bool { return x.Slot(win) == 0 }) | |||
| 436 | + | settle(t, x, p) | |||
| 437 | + | require.Zero(t, x.active) | |||
| 438 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |||
| 439 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |||
| 440 | + | list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST") | |||
| 441 | + | require.Empty(t, list.data) | |||
| 442 | + | } | |||
| 443 | + | ||||
| 444 | + | func TestFocusInDenied(t *testing.T) { | |||
| 445 | + | x, p, h, _, _ := attached(t) | |||
| 446 | + | const a, b = 0x500001, 0x500002 | |||
| 447 | + | sa := newWin(t, x, p, h, a) | |||
| 448 | + | newWin(t, x, p, h, b) | |||
| 449 | + | x.Focus(sa) | |||
| 450 | + | n := settle(t, x, p) | |||
| 451 | + | ||||
| 452 | + | focusIn := func(win uint32, detail byte) { | |||
| 453 | + | p.event(9, func(e *[32]byte) { | |||
| 454 | + | e[1] = detail | |||
| 455 | + | binary.LittleEndian.PutUint32(e[4:], win) | |||
| 456 | + | }) | |||
| 457 | + | } | |||
| 458 | + | focusCount := func(skip int) int { | |||
| 459 | + | c := 0 | |||
| 460 | + | for _, r := range since(p, skip, opSetInputFocus) { | |||
| 461 | + | if r.u32(4) == a { | |||
| 462 | + | c++ | |||
| 463 | + | } | |||
| 464 | + | } | |||
| 465 | + | return c | |||
| 466 | + | } | |||
| 467 | + | x.feed(t, p, func() { focusIn(a, 0) }) // the active window itself: fine | |||
| 468 | + | x.feed(t, p, func() { focusIn(b, byte(xproto.NotifyDetailPointer)) }) | |||
| 469 | + | settle(t, x, p) | |||
| 470 | + | require.Zero(t, focusCount(n)) | |||
| 471 | + | ||||
| 472 | + | x.feed(t, p, func() { focusIn(b, 0); focusIn(b, 0) }) | |||
| 473 | + | readUntil(t, x, func() bool { return x.stats.Events >= 5 }) | |||
| 474 | + | settle(t, x, p) | |||
| 475 | + | require.Equal(t, 1, focusCount(n), "denied once per iteration") | |||
| 476 | + | n = settle(t, x, p) | |||
| 477 | + | settle(t, x, p) | |||
| 478 | + | require.Zero(t, focusCount(n)) | |||
| 479 | + | } | |||
| 480 | + | ||||
| 481 | + | func TestCloseWindow(t *testing.T) { | |||
| 482 | + | x, p, h, _, _ := attached(t) | |||
| 483 | + | const a, b = 0x500001, 0x500002 | |||
| 484 | + | sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b) | |||
| 485 | + | x.tab.rec(sa).deleteWindow = true | |||
| 486 | + | n := settle(t, x, p) | |||
| 487 | + | ||||
| 488 | + | x.CloseWindow(sa) | |||
| 489 | + | x.CloseWindow(sb) | |||
| 490 | + | settle(t, x, p) | |||
| 491 | + | evs := sentEvents(p, n, a) | |||
| 492 | + | require.Len(t, evs, 1) | |||
| 493 | + | require.EqualValues(t, 33, evs[0][0]) | |||
| 494 | + | require.EqualValues(t, 32, evs[0][1]) | |||
| 495 | + | require.Equal(t, p.atom("WM_PROTOCOLS"), binary.LittleEndian.Uint32(evs[0][8:])) | |||
| 496 | + | require.Equal(t, p.atom("WM_DELETE_WINDOW"), binary.LittleEndian.Uint32(evs[0][12:])) | |||
| 497 | + | require.Zero(t, binary.LittleEndian.Uint32(evs[0][16:])) | |||
| 498 | + | require.Empty(t, sentEvents(p, n, b)) | |||
| 499 | + | ||||
| 500 | + | kills := since(p, n, opKillClient) | |||
| 501 | + | require.Len(t, kills, 1) | |||
| 502 | + | require.Equal(t, uint32(b), kills[0].u32(4)) | |||
| 503 | + | } | |||
| 504 | + | ||||
| 505 | + | // Allocation guards. The peer sinks requests so the connection never blocks; | |||
| 506 | + | // its goroutine is idle in the sink loop before measuring. | |||
| 507 | + | ||||
| 508 | + | func sinkPeer(t *testing.T, x *XWM, p *xpeer) { | |||
| 509 | + | t.Helper() | |||
| 510 | + | p.sink.Store(true) | |||
| 511 | + | n := len(p.requests()) | |||
| 512 | + | xproto.GetInputFocus(x.conn.Buf()) // served by the old loop iteration | |||
| 513 | + | require.NoError(t, x.conn.Flush()) | |||
| 514 | + | p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |||
| 515 | + | time.Sleep(50 * time.Millisecond) | |||
| 516 | + | x.src = nil // no source: write backpressure would not allocate either | |||
| 517 | + | x.post = func(func()) {} | |||
| 518 | + | x.warnedRing = true // the sink never answers: Expect slots stay used | |||
| 519 | + | } | |||
| 520 | + | ||||
| 521 | + | func TestConfigureFlushAllocations(t *testing.T) { | |||
| 522 | + | x, p, h, _, _ := attached(t) | |||
| 523 | + | s := newWin(t, x, p, h, 0x500001) | |||
| 524 | + | sinkPeer(t, x, p) | |||
| 525 | + | i := 0 | |||
| 526 | + | allocs := testing.AllocsPerRun(300, func() { | |||
| 527 | + | i++ | |||
| 528 | + | x.Configure(s, ports.ConfigureWindow{Width: 100 + i%2, Height: 100, Fullscreen: i%3 == 0, Activated: i%2 == 0}) | |||
| 529 | + | x.Place(s, i%5, i%7) | |||
| 530 | + | x.posted = false | |||
| 531 | + | x.flush() | |||
| 532 | + | }) | |||
| 533 | + | require.Zero(t, allocs) | |||
| 534 | + | require.False(t, x.closed) | |||
| 535 | + | } | |||
| 536 | + | ||||
| 537 | + | func TestFocusAllocations(t *testing.T) { | |||
| 538 | + | x, p, h, _, _ := attached(t) | |||
| 539 | + | a, b := newWin(t, x, p, h, 0x500001), newWin(t, x, p, h, 0x500002) | |||
| 540 | + | x.addClient(x.tab.rec(a).xid) | |||
| 541 | + | x.addClient(x.tab.rec(b).xid) | |||
| 542 | + | x.tab.rec(b).takeFocus = true | |||
| 543 | + | x.tab.rec(a).input, x.tab.rec(b).input = true, true | |||
| 544 | + | sinkPeer(t, x, p) | |||
| 545 | + | i := 0 | |||
| 546 | + | allocs := testing.AllocsPerRun(150, func() { | |||
| 547 | + | i++ | |||
| 548 | + | switch i % 3 { | |||
| 549 | + | case 0: | |||
| 550 | + | x.Focus(a) | |||
| 551 | + | case 1: | |||
| 552 | + | x.Focus(b) | |||
| 553 | + | default: | |||
| 554 | + | x.Focus(0) | |||
| 555 | + | } | |||
| 556 | + | x.refocus = i%5 == 0 | |||
| 557 | + | x.posted = false | |||
| 558 | + | x.flush() | |||
| 559 | + | }) | |||
| 560 | + | require.Zero(t, allocs) | |||
| 561 | + | require.False(t, x.closed) | |||
| 562 | + | } | |||
M internal/adapters/wayland/xwm/events.go +1 -5
| @@ -176,11 +176,7 @@ func (x *XWM) onDestroyNotify(ev *xturbo.Event) { | |||
| 176 | 176 | if s == 0 { | |
| 177 | 177 | return | |
| 178 | 178 | } | |
| 179 | - | if r := x.tab.rec(s); r.kind == kindManaged && r.id != 0 && x.host != nil { | |
| 180 | - | r.id = 0 | |
| 181 | - | x.host.Dissociate(s, true) | |
| 182 | - | } | |
| 183 | - | x.tab.remove(s) | |
| 179 | + | x.forgetWindow(s) | |
| 184 | 180 | } | |
| 185 | 181 | ||
| 186 | 182 | // onTreeReply (boot): record every existing child and ask for its attributes. | |
| @@ -176,11 +176,7 @@ func (x *XWM) onDestroyNotify(ev *xturbo.Event) { | |||||
| 176 | if s == 0 { | 176 | if s == 0 { | ||
| 177 | return | 177 | return | ||
| 178 | } | 178 | } | ||
| 179 | - | if r := x.tab.rec(s); r.kind == kindManaged && r.id != 0 && x.host != nil { | 179 | + | x.forgetWindow(s) |
| 180 | - | r.id = 0 | |||
| 181 | - | x.host.Dissociate(s, true) | |||
| 182 | - | } | |||
| 183 | - | x.tab.remove(s) | |||
| 184 | } | 180 | } | ||
| 185 | 181 | ||||
| 186 | // onTreeReply (boot): record every existing child and ask for its attributes. | 182 | // onTreeReply (boot): record every existing child and ask for its attributes. | ||
M internal/adapters/wayland/xwm/focus.go +187 -4
| @@ -1,11 +1,194 @@ | |||
| 1 | 1 | package xwm | |
| 2 | 2 | ||
| 3 | - | import "bnema.dev/xturbo" | |
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "slices" | |
| 4 | 6 | ||
| 5 | - | func (x *XWM) onFocusIn(ev *xturbo.Event) { _ = ev } | |
| 7 | + | "bnema.dev/xturbo" | |
| 8 | + | "bnema.dev/xturbo/xproto" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Bits of XWM.listsDirty: the client lists to rewrite at flush. | |
| 12 | + | const ( | |
| 13 | + | listClients uint8 = 1 << iota | |
| 14 | + | listStacking | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // onFocusIn denies a focus change the XWM did not make: Wayland is the truth. | |
| 18 | + | // The active window's focus is re-issued once at the next flush. | |
| 19 | + | func (x *XWM) onFocusIn(ev *xturbo.Event) { | |
| 20 | + | e := xproto.FocusInOf(ev) | |
| 21 | + | if e.Detail() == xproto.NotifyDetailPointer { | |
| 22 | + | return | |
| 23 | + | } | |
| 24 | + | s := x.tab.lookup(uint32(e.Event())) | |
| 25 | + | if s == 0 || s == x.active { | |
| 26 | + | return | |
| 27 | + | } | |
| 28 | + | x.refocus = true | |
| 29 | + | x.wake() | |
| 30 | + | } | |
| 6 | 31 | ||
| 7 | 32 | // Focus gives keyboard focus to s; 0 means a native Wayland window or none. | |
| 8 | - | func (x *XWM) Focus(s Slot) { _ = s } | |
| 33 | + | // Focus never uses PointerRoot: without an X window to focus it is None. | |
| 34 | + | func (x *XWM) Focus(s Slot) { | |
| 35 | + | if x.closed { | |
| 36 | + | return | |
| 37 | + | } | |
| 38 | + | r := x.managedRec(s) | |
| 39 | + | if r == nil { | |
| 40 | + | s = 0 | |
| 41 | + | } | |
| 42 | + | if s == x.active { | |
| 43 | + | return | |
| 44 | + | } | |
| 45 | + | if old := x.managedRec(x.active); old != nil { | |
| 46 | + | old.pend.activated = false | |
| 47 | + | x.markDirty(x.active, old) | |
| 48 | + | } | |
| 49 | + | x.active = s | |
| 50 | + | if r == nil { | |
| 51 | + | x.issueFocus(0, nil) | |
| 52 | + | x.setActive(0) | |
| 53 | + | x.wake() | |
| 54 | + | return | |
| 55 | + | } | |
| 56 | + | r.pend.activated = true | |
| 57 | + | x.markDirty(s, r) | |
| 58 | + | x.issueFocus(s, r) | |
| 59 | + | x.setActive(r.xid) | |
| 60 | + | x.raise(r) | |
| 61 | + | x.wake() | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // issueFocus applies the ICCCM input model of r: Passive and Locally active | |
| 65 | + | // windows get SetInputFocus, Locally and Globally active ones WM_TAKE_FOCUS, | |
| 66 | + | // No-input windows nothing. A nil r clears the X focus. | |
| 67 | + | func (x *XWM) issueFocus(s Slot, r *record) { | |
| 68 | + | b := x.conn.Buf() | |
| 69 | + | if r == nil { | |
| 70 | + | xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0) | |
| 71 | + | return | |
| 72 | + | } | |
| 73 | + | if r.input { | |
| 74 | + | x.expect(xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.Window(r.xid), 0), kindFocus, x.tab.tag(s), 0) | |
| 75 | + | } | |
| 76 | + | if r.takeFocus { | |
| 77 | + | x.sendProtocol(r.xid, atomWMTakeFocus) | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // sendProtocol sends a WM_PROTOCOLS client message [proto, CurrentTime]. | |
| 82 | + | func (x *XWM) sendProtocol(xid uint32, proto int) { | |
| 83 | + | var raw [32]byte | |
| 84 | + | raw[0], raw[1] = 33, 32 // ClientMessage, format 32 | |
| 85 | + | binary.LittleEndian.PutUint32(raw[4:], xid) | |
| 86 | + | binary.LittleEndian.PutUint32(raw[8:], x.atoms[atomWMProtocols]) | |
| 87 | + | binary.LittleEndian.PutUint32(raw[12:], x.atoms[proto]) | |
| 88 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(xid), xproto.EventMaskNoEvent, &raw) | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // setActive writes _NET_ACTIVE_WINDOW on the root. | |
| 92 | + | func (x *XWM) setActive(xid uint32) { | |
| 93 | + | v := [1]uint32{xid} | |
| 94 | + | x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, v[:]) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // raise restacks r above its siblings and at the top of the stacking list. | |
| 98 | + | func (x *XWM) raise(r *record) { | |
| 99 | + | var v xproto.ConfigureWindowValues | |
| 100 | + | v.SetStackMode(xproto.StackModeAbove) | |
| 101 | + | xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v) | |
| 102 | + | if i := slices.Index(x.stacking, r.xid); i >= 0 { | |
| 103 | + | x.stacking = append(x.stacking[:i], x.stacking[i+1:]...) | |
| 104 | + | x.stacking = append(x.stacking, r.xid) | |
| 105 | + | x.listsDirty |= listStacking | |
| 106 | + | } | |
| 107 | + | } | |
| 9 | 108 | ||
| 10 | 109 | // CloseWindow asks s to close (WM_DELETE_WINDOW), or kills its client. | |
| 11 | - | func (x *XWM) CloseWindow(s Slot) { _ = s } | |
| 110 | + | func (x *XWM) CloseWindow(s Slot) { | |
| 111 | + | r := x.managedRec(s) | |
| 112 | + | if r == nil { | |
| 113 | + | return | |
| 114 | + | } | |
| 115 | + | if r.deleteWindow { | |
| 116 | + | x.sendProtocol(r.xid, atomWMDeleteWindow) | |
| 117 | + | } else { | |
| 118 | + | xproto.KillClient(x.conn.Buf(), r.xid) | |
| 119 | + | } | |
| 120 | + | x.wake() | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // addClient puts a ready window at the end of both client lists. | |
| 124 | + | func (x *XWM) addClient(xid uint32) { | |
| 125 | + | if !slices.Contains(x.clients, xid) { | |
| 126 | + | x.clients = append(x.clients, xid) | |
| 127 | + | x.listsDirty |= listClients | |
| 128 | + | } | |
| 129 | + | if !slices.Contains(x.stacking, xid) { | |
| 130 | + | x.stacking = append(x.stacking, xid) | |
| 131 | + | x.listsDirty |= listStacking | |
| 132 | + | } | |
| 133 | + | x.wake() | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | // removeClient drops an unmapped or destroyed window from both client lists. | |
| 137 | + | func (x *XWM) removeClient(xid uint32) { | |
| 138 | + | if i := slices.Index(x.clients, xid); i >= 0 { | |
| 139 | + | x.clients = slices.Delete(x.clients, i, i+1) | |
| 140 | + | x.listsDirty |= listClients | |
| 141 | + | } | |
| 142 | + | if i := slices.Index(x.stacking, xid); i >= 0 { | |
| 143 | + | x.stacking = slices.Delete(x.stacking, i, i+1) | |
| 144 | + | x.listsDirty |= listStacking | |
| 145 | + | } | |
| 146 | + | x.wake() | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // dropWindow takes s out of the client lists, the focus and the configure | |
| 150 | + | // queue when it stops being mapped (unmap, destroy). It is idempotent. | |
| 151 | + | func (x *XWM) dropWindow(s Slot) { | |
| 152 | + | r := x.tab.rec(s) | |
| 153 | + | if r == nil || r.kind != kindManaged { | |
| 154 | + | return | |
| 155 | + | } | |
| 156 | + | x.removeClient(r.xid) | |
| 157 | + | r.pend.dirty = false // the queued slot is skipped at flush | |
| 158 | + | if x.active == s { | |
| 159 | + | x.active = 0 | |
| 160 | + | x.refocus = false | |
| 161 | + | x.setActive(0) | |
| 162 | + | } | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | // forgetWindow ends the life of s (destroy, reparent away): it is dropped, | |
| 166 | + | // the host is told if it knew the window, and the record is freed. | |
| 167 | + | func (x *XWM) forgetWindow(s Slot) { | |
| 168 | + | r := x.tab.rec(s) | |
| 169 | + | if r == nil || r.kind == kindFree { | |
| 170 | + | return | |
| 171 | + | } | |
| 172 | + | x.dropWindow(s) | |
| 173 | + | if r.kind == kindManaged && r.id != 0 && x.host != nil { | |
| 174 | + | r.id = 0 | |
| 175 | + | x.host.Dissociate(s, true) | |
| 176 | + | } | |
| 177 | + | x.tab.remove(s) | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // flushFocus rewrites the client lists that changed and re-issues a denied | |
| 181 | + | // focus, once per display iteration. | |
| 182 | + | func (x *XWM) flushFocus() { | |
| 183 | + | if x.listsDirty&listClients != 0 { | |
| 184 | + | x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, x.clients) | |
| 185 | + | } | |
| 186 | + | if x.listsDirty&listStacking != 0 { | |
| 187 | + | x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, x.stacking) | |
| 188 | + | } | |
| 189 | + | x.listsDirty = 0 | |
| 190 | + | if x.refocus { | |
| 191 | + | x.refocus = false | |
| 192 | + | x.issueFocus(x.active, x.managedRec(x.active)) | |
| 193 | + | } | |
| 194 | + | } | |
| @@ -1,11 +1,194 @@ | |||||
| 1 | package xwm | 1 | package xwm | ||
| 2 | 2 | ||||
| 3 | - | import "bnema.dev/xturbo" | 3 | + | import ( |
| 4 | + | "encoding/binary" | |||
| 5 | + | "slices" | |||
| 4 | 6 | ||||
| 5 | - | func (x *XWM) onFocusIn(ev *xturbo.Event) { _ = ev } | 7 | + | "bnema.dev/xturbo" |
| 8 | + | "bnema.dev/xturbo/xproto" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Bits of XWM.listsDirty: the client lists to rewrite at flush. | |||
| 12 | + | const ( | |||
| 13 | + | listClients uint8 = 1 << iota | |||
| 14 | + | listStacking | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | // onFocusIn denies a focus change the XWM did not make: Wayland is the truth. | |||
| 18 | + | // The active window's focus is re-issued once at the next flush. | |||
| 19 | + | func (x *XWM) onFocusIn(ev *xturbo.Event) { | |||
| 20 | + | e := xproto.FocusInOf(ev) | |||
| 21 | + | if e.Detail() == xproto.NotifyDetailPointer { | |||
| 22 | + | return | |||
| 23 | + | } | |||
| 24 | + | s := x.tab.lookup(uint32(e.Event())) | |||
| 25 | + | if s == 0 || s == x.active { | |||
| 26 | + | return | |||
| 27 | + | } | |||
| 28 | + | x.refocus = true | |||
| 29 | + | x.wake() | |||
| 30 | + | } | |||
| 6 | 31 | ||||
| 7 | // Focus gives keyboard focus to s; 0 means a native Wayland window or none. | 32 | // Focus gives keyboard focus to s; 0 means a native Wayland window or none. | ||
| 8 | - | func (x *XWM) Focus(s Slot) { _ = s } | 33 | + | // Focus never uses PointerRoot: without an X window to focus it is None. |
| 34 | + | func (x *XWM) Focus(s Slot) { | |||
| 35 | + | if x.closed { | |||
| 36 | + | return | |||
| 37 | + | } | |||
| 38 | + | r := x.managedRec(s) | |||
| 39 | + | if r == nil { | |||
| 40 | + | s = 0 | |||
| 41 | + | } | |||
| 42 | + | if s == x.active { | |||
| 43 | + | return | |||
| 44 | + | } | |||
| 45 | + | if old := x.managedRec(x.active); old != nil { | |||
| 46 | + | old.pend.activated = false | |||
| 47 | + | x.markDirty(x.active, old) | |||
| 48 | + | } | |||
| 49 | + | x.active = s | |||
| 50 | + | if r == nil { | |||
| 51 | + | x.issueFocus(0, nil) | |||
| 52 | + | x.setActive(0) | |||
| 53 | + | x.wake() | |||
| 54 | + | return | |||
| 55 | + | } | |||
| 56 | + | r.pend.activated = true | |||
| 57 | + | x.markDirty(s, r) | |||
| 58 | + | x.issueFocus(s, r) | |||
| 59 | + | x.setActive(r.xid) | |||
| 60 | + | x.raise(r) | |||
| 61 | + | x.wake() | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // issueFocus applies the ICCCM input model of r: Passive and Locally active | |||
| 65 | + | // windows get SetInputFocus, Locally and Globally active ones WM_TAKE_FOCUS, | |||
| 66 | + | // No-input windows nothing. A nil r clears the X focus. | |||
| 67 | + | func (x *XWM) issueFocus(s Slot, r *record) { | |||
| 68 | + | b := x.conn.Buf() | |||
| 69 | + | if r == nil { | |||
| 70 | + | xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0) | |||
| 71 | + | return | |||
| 72 | + | } | |||
| 73 | + | if r.input { | |||
| 74 | + | x.expect(xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.Window(r.xid), 0), kindFocus, x.tab.tag(s), 0) | |||
| 75 | + | } | |||
| 76 | + | if r.takeFocus { | |||
| 77 | + | x.sendProtocol(r.xid, atomWMTakeFocus) | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // sendProtocol sends a WM_PROTOCOLS client message [proto, CurrentTime]. | |||
| 82 | + | func (x *XWM) sendProtocol(xid uint32, proto int) { | |||
| 83 | + | var raw [32]byte | |||
| 84 | + | raw[0], raw[1] = 33, 32 // ClientMessage, format 32 | |||
| 85 | + | binary.LittleEndian.PutUint32(raw[4:], xid) | |||
| 86 | + | binary.LittleEndian.PutUint32(raw[8:], x.atoms[atomWMProtocols]) | |||
| 87 | + | binary.LittleEndian.PutUint32(raw[12:], x.atoms[proto]) | |||
| 88 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(xid), xproto.EventMaskNoEvent, &raw) | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | // setActive writes _NET_ACTIVE_WINDOW on the root. | |||
| 92 | + | func (x *XWM) setActive(xid uint32) { | |||
| 93 | + | v := [1]uint32{xid} | |||
| 94 | + | x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, v[:]) | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // raise restacks r above its siblings and at the top of the stacking list. | |||
| 98 | + | func (x *XWM) raise(r *record) { | |||
| 99 | + | var v xproto.ConfigureWindowValues | |||
| 100 | + | v.SetStackMode(xproto.StackModeAbove) | |||
| 101 | + | xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v) | |||
| 102 | + | if i := slices.Index(x.stacking, r.xid); i >= 0 { | |||
| 103 | + | x.stacking = append(x.stacking[:i], x.stacking[i+1:]...) | |||
| 104 | + | x.stacking = append(x.stacking, r.xid) | |||
| 105 | + | x.listsDirty |= listStacking | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 9 | 108 | ||||
| 10 | // CloseWindow asks s to close (WM_DELETE_WINDOW), or kills its client. | 109 | // CloseWindow asks s to close (WM_DELETE_WINDOW), or kills its client. | ||
| 11 | - | func (x *XWM) CloseWindow(s Slot) { _ = s } | 110 | + | func (x *XWM) CloseWindow(s Slot) { |
| 111 | + | r := x.managedRec(s) | |||
| 112 | + | if r == nil { | |||
| 113 | + | return | |||
| 114 | + | } | |||
| 115 | + | if r.deleteWindow { | |||
| 116 | + | x.sendProtocol(r.xid, atomWMDeleteWindow) | |||
| 117 | + | } else { | |||
| 118 | + | xproto.KillClient(x.conn.Buf(), r.xid) | |||
| 119 | + | } | |||
| 120 | + | x.wake() | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | // addClient puts a ready window at the end of both client lists. | |||
| 124 | + | func (x *XWM) addClient(xid uint32) { | |||
| 125 | + | if !slices.Contains(x.clients, xid) { | |||
| 126 | + | x.clients = append(x.clients, xid) | |||
| 127 | + | x.listsDirty |= listClients | |||
| 128 | + | } | |||
| 129 | + | if !slices.Contains(x.stacking, xid) { | |||
| 130 | + | x.stacking = append(x.stacking, xid) | |||
| 131 | + | x.listsDirty |= listStacking | |||
| 132 | + | } | |||
| 133 | + | x.wake() | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | // removeClient drops an unmapped or destroyed window from both client lists. | |||
| 137 | + | func (x *XWM) removeClient(xid uint32) { | |||
| 138 | + | if i := slices.Index(x.clients, xid); i >= 0 { | |||
| 139 | + | x.clients = slices.Delete(x.clients, i, i+1) | |||
| 140 | + | x.listsDirty |= listClients | |||
| 141 | + | } | |||
| 142 | + | if i := slices.Index(x.stacking, xid); i >= 0 { | |||
| 143 | + | x.stacking = slices.Delete(x.stacking, i, i+1) | |||
| 144 | + | x.listsDirty |= listStacking | |||
| 145 | + | } | |||
| 146 | + | x.wake() | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | // dropWindow takes s out of the client lists, the focus and the configure | |||
| 150 | + | // queue when it stops being mapped (unmap, destroy). It is idempotent. | |||
| 151 | + | func (x *XWM) dropWindow(s Slot) { | |||
| 152 | + | r := x.tab.rec(s) | |||
| 153 | + | if r == nil || r.kind != kindManaged { | |||
| 154 | + | return | |||
| 155 | + | } | |||
| 156 | + | x.removeClient(r.xid) | |||
| 157 | + | r.pend.dirty = false // the queued slot is skipped at flush | |||
| 158 | + | if x.active == s { | |||
| 159 | + | x.active = 0 | |||
| 160 | + | x.refocus = false | |||
| 161 | + | x.setActive(0) | |||
| 162 | + | } | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | // forgetWindow ends the life of s (destroy, reparent away): it is dropped, | |||
| 166 | + | // the host is told if it knew the window, and the record is freed. | |||
| 167 | + | func (x *XWM) forgetWindow(s Slot) { | |||
| 168 | + | r := x.tab.rec(s) | |||
| 169 | + | if r == nil || r.kind == kindFree { | |||
| 170 | + | return | |||
| 171 | + | } | |||
| 172 | + | x.dropWindow(s) | |||
| 173 | + | if r.kind == kindManaged && r.id != 0 && x.host != nil { | |||
| 174 | + | r.id = 0 | |||
| 175 | + | x.host.Dissociate(s, true) | |||
| 176 | + | } | |||
| 177 | + | x.tab.remove(s) | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // flushFocus rewrites the client lists that changed and re-issues a denied | |||
| 181 | + | // focus, once per display iteration. | |||
| 182 | + | func (x *XWM) flushFocus() { | |||
| 183 | + | if x.listsDirty&listClients != 0 { | |||
| 184 | + | x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, x.clients) | |||
| 185 | + | } | |||
| 186 | + | if x.listsDirty&listStacking != 0 { | |||
| 187 | + | x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, x.stacking) | |||
| 188 | + | } | |||
| 189 | + | x.listsDirty = 0 | |||
| 190 | + | if x.refocus { | |||
| 191 | + | x.refocus = false | |||
| 192 | + | x.issueFocus(x.active, x.managedRec(x.active)) | |||
| 193 | + | } | |||
| 194 | + | } | |||
M internal/adapters/wayland/xwm/mapflow.go +6 -49
| @@ -68,13 +68,14 @@ func (x *XWM) onUnmapNotify(ev *xturbo.Event) { | |||
| 68 | 68 | } | |
| 69 | 69 | wasMapping := rec.mapping | |
| 70 | 70 | rec.ready, rec.mapping, rec.pending = false, false, 0 | |
| 71 | + | rec.pend.dirty = false | |
| 71 | 72 | rec.serial, rec.surface = 0, 0 | |
| 72 | 73 | if wasMapping { | |
| 73 | 74 | xid := rec.xid | |
| 74 | 75 | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateWithdrawn, 0}) | |
| 75 | 76 | xproto.DeleteProperty(x.conn.Buf(), xproto.Window(xid), xproto.Atom(x.atoms[atomNetWMState])) | |
| 76 | - | x.unlist(xid) | |
| 77 | 77 | } | |
| 78 | + | x.dropWindow(s) | |
| 78 | 79 | if rec.id != 0 { | |
| 79 | 80 | x.host.Dissociate(s, false) | |
| 80 | 81 | } | |
| @@ -92,6 +93,7 @@ func (x *XWM) checkReady(s Slot) { | |||
| 92 | 93 | return | |
| 93 | 94 | } | |
| 94 | 95 | rec.ready = true | |
| 96 | + | x.classify(rec) | |
| 95 | 97 | info := x.info(s) | |
| 96 | 98 | xid := rec.xid | |
| 97 | 99 | ||
| @@ -107,9 +109,7 @@ func (x *XWM) checkReady(s Slot) { | |||
| 107 | 109 | } | |
| 108 | 110 | x.setProp32(xid, atomNetWMState, xproto.AtomAtom, state[:n]) | |
| 109 | 111 | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateNormal, 0}) | |
| 110 | - | x.clients = append(x.clients, xid) | |
| 111 | - | x.stacking = append(x.stacking, xid) | |
| 112 | - | x.publishLists() | |
| 112 | + | x.addClient(xid) | |
| 113 | 113 | ||
| 114 | 114 | x.host.Ready(s, info) | |
| 115 | 115 | x.tryPair(s) | |
| @@ -119,10 +119,9 @@ func (x *XWM) checkReady(s Slot) { | |||
| 119 | 119 | func (x *XWM) info(s Slot) WindowInfo { | |
| 120 | 120 | rec := x.tab.rec(s) | |
| 121 | 121 | parent := x.managedSlot(rec.transient) | |
| 122 | - | fixed := rec.minW > 0 && rec.minH > 0 && rec.minW == rec.maxW && rec.minH == rec.maxH | |
| 123 | 122 | info := WindowInfo{ | |
| 124 | 123 | PID: int(rec.pid), | |
| 125 | - | Floating: floatingType(rec.wtype) || parent != 0 || fixed, | |
| 124 | + | Floating: rec.floating, | |
| 126 | 125 | Parent: parent, | |
| 127 | 126 | Modal: rec.modal, | |
| 128 | 127 | Fullscreen: rec.fullscreen, | |
| @@ -143,31 +142,6 @@ func (x *XWM) onGeomReply(s Slot, rec *record, r *xturbo.Reply) { | |||
| 143 | 142 | x.checkReady(s) | |
| 144 | 143 | } | |
| 145 | 144 | ||
| 146 | - | // publishLists rewrites _NET_CLIENT_LIST and _NET_CLIENT_LIST_STACKING. | |
| 147 | - | func (x *XWM) publishLists() { | |
| 148 | - | x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, x.clients) | |
| 149 | - | x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, x.stacking) | |
| 150 | - | } | |
| 151 | - | ||
| 152 | - | // unlist drops xid from both client lists and republishes them if it was in. | |
| 153 | - | func (x *XWM) unlist(xid uint32) { | |
| 154 | - | was := len(x.clients) | |
| 155 | - | x.clients = removeID(x.clients, xid) | |
| 156 | - | x.stacking = removeID(x.stacking, xid) | |
| 157 | - | if len(x.clients) != was { | |
| 158 | - | x.publishLists() | |
| 159 | - | } | |
| 160 | - | } | |
| 161 | - | ||
| 162 | - | func removeID(l []uint32, xid uint32) []uint32 { | |
| 163 | - | for i, v := range l { | |
| 164 | - | if v == xid { | |
| 165 | - | return append(l[:i], l[i+1:]...) | |
| 166 | - | } | |
| 167 | - | } | |
| 168 | - | return l | |
| 169 | - | } | |
| 170 | - | ||
| 171 | 145 | // onClientMessage handles WL_SURFACE_SERIAL (pairing), the legacy | |
| 172 | 146 | // WL_SURFACE_ID (counted, ignored) and _NET_WM_STATE requests. | |
| 173 | 147 | func (x *XWM) onClientMessage(ev *xturbo.Event) { | |
| @@ -253,7 +227,7 @@ func (x *XWM) onReparentNotify(ev *xturbo.Event) { | |||
| 253 | 227 | s := x.tab.lookup(xid) | |
| 254 | 228 | if uint32(e.Parent()) != x.root { | |
| 255 | 229 | if s != 0 { | |
| 256 | - | x.dropWindow(s) | |
| 230 | + | x.forgetWindow(s) | |
| 257 | 231 | } | |
| 258 | 232 | return | |
| 259 | 233 | } | |
| @@ -277,23 +251,6 @@ func (x *XWM) onReparentNotify(ev *xturbo.Event) { | |||
| 277 | 251 | } | |
| 278 | 252 | } | |
| 279 | 253 | ||
| 280 | - | // dropWindow forgets s: the host is told if it knew the window. | |
| 281 | - | func (x *XWM) dropWindow(s Slot) { | |
| 282 | - | rec := x.tab.rec(s) | |
| 283 | - | xid := rec.xid | |
| 284 | - | if rec.kind == kindManaged { | |
| 285 | - | x.unlist(xid) | |
| 286 | - | if x.active == s { | |
| 287 | - | x.active = 0 | |
| 288 | - | } | |
| 289 | - | if rec.id != 0 && x.host != nil { | |
| 290 | - | rec.id = 0 | |
| 291 | - | x.host.Dissociate(s, true) | |
| 292 | - | } | |
| 293 | - | } | |
| 294 | - | x.tab.remove(s) | |
| 295 | - | } | |
| 296 | - | ||
| 297 | 254 | // tryPair links the window to the wl_surface carrying its serial, once both | |
| 298 | 255 | // are known and the window is ready (the host maps on the surface's first | |
| 299 | 256 | // buffer, so it must know the window's info by then). Unmanaged windows keep | |
| @@ -68,13 +68,14 @@ func (x *XWM) onUnmapNotify(ev *xturbo.Event) { | |||||
| 68 | } | 68 | } | ||
| 69 | wasMapping := rec.mapping | 69 | wasMapping := rec.mapping | ||
| 70 | rec.ready, rec.mapping, rec.pending = false, false, 0 | 70 | rec.ready, rec.mapping, rec.pending = false, false, 0 | ||
| 71 | + | rec.pend.dirty = false | |||
| 71 | rec.serial, rec.surface = 0, 0 | 72 | rec.serial, rec.surface = 0, 0 | ||
| 72 | if wasMapping { | 73 | if wasMapping { | ||
| 73 | xid := rec.xid | 74 | xid := rec.xid | ||
| 74 | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateWithdrawn, 0}) | 75 | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateWithdrawn, 0}) | ||
| 75 | xproto.DeleteProperty(x.conn.Buf(), xproto.Window(xid), xproto.Atom(x.atoms[atomNetWMState])) | 76 | xproto.DeleteProperty(x.conn.Buf(), xproto.Window(xid), xproto.Atom(x.atoms[atomNetWMState])) | ||
| 76 | - | x.unlist(xid) | |||
| 77 | } | 77 | } | ||
| 78 | + | x.dropWindow(s) | |||
| 78 | if rec.id != 0 { | 79 | if rec.id != 0 { | ||
| 79 | x.host.Dissociate(s, false) | 80 | x.host.Dissociate(s, false) | ||
| 80 | } | 81 | } | ||
| @@ -92,6 +93,7 @@ func (x *XWM) checkReady(s Slot) { | |||||
| 92 | return | 93 | return | ||
| 93 | } | 94 | } | ||
| 94 | rec.ready = true | 95 | rec.ready = true | ||
| 96 | + | x.classify(rec) | |||
| 95 | info := x.info(s) | 97 | info := x.info(s) | ||
| 96 | xid := rec.xid | 98 | xid := rec.xid | ||
| 97 | 99 | ||||
| @@ -107,9 +109,7 @@ func (x *XWM) checkReady(s Slot) { | |||||
| 107 | } | 109 | } | ||
| 108 | x.setProp32(xid, atomNetWMState, xproto.AtomAtom, state[:n]) | 110 | x.setProp32(xid, atomNetWMState, xproto.AtomAtom, state[:n]) | ||
| 109 | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateNormal, 0}) | 111 | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateNormal, 0}) | ||
| 110 | - | x.clients = append(x.clients, xid) | 112 | + | x.addClient(xid) |
| 111 | - | x.stacking = append(x.stacking, xid) | |||
| 112 | - | x.publishLists() | |||
| 113 | 113 | ||||
| 114 | x.host.Ready(s, info) | 114 | x.host.Ready(s, info) | ||
| 115 | x.tryPair(s) | 115 | x.tryPair(s) | ||
| @@ -119,10 +119,9 @@ func (x *XWM) checkReady(s Slot) { | |||||
| 119 | func (x *XWM) info(s Slot) WindowInfo { | 119 | func (x *XWM) info(s Slot) WindowInfo { | ||
| 120 | rec := x.tab.rec(s) | 120 | rec := x.tab.rec(s) | ||
| 121 | parent := x.managedSlot(rec.transient) | 121 | parent := x.managedSlot(rec.transient) | ||
| 122 | - | fixed := rec.minW > 0 && rec.minH > 0 && rec.minW == rec.maxW && rec.minH == rec.maxH | |||
| 123 | info := WindowInfo{ | 122 | info := WindowInfo{ | ||
| 124 | PID: int(rec.pid), | 123 | PID: int(rec.pid), | ||
| 125 | - | Floating: floatingType(rec.wtype) || parent != 0 || fixed, | 124 | + | Floating: rec.floating, |
| 126 | Parent: parent, | 125 | Parent: parent, | ||
| 127 | Modal: rec.modal, | 126 | Modal: rec.modal, | ||
| 128 | Fullscreen: rec.fullscreen, | 127 | Fullscreen: rec.fullscreen, | ||
| @@ -143,31 +142,6 @@ func (x *XWM) onGeomReply(s Slot, rec *record, r *xturbo.Reply) { | |||||
| 143 | x.checkReady(s) | 142 | x.checkReady(s) | ||
| 144 | } | 143 | } | ||
| 145 | 144 | ||||
| 146 | - | // publishLists rewrites _NET_CLIENT_LIST and _NET_CLIENT_LIST_STACKING. | |||
| 147 | - | func (x *XWM) publishLists() { | |||
| 148 | - | x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, x.clients) | |||
| 149 | - | x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, x.stacking) | |||
| 150 | - | } | |||
| 151 | - | ||||
| 152 | - | // unlist drops xid from both client lists and republishes them if it was in. | |||
| 153 | - | func (x *XWM) unlist(xid uint32) { | |||
| 154 | - | was := len(x.clients) | |||
| 155 | - | x.clients = removeID(x.clients, xid) | |||
| 156 | - | x.stacking = removeID(x.stacking, xid) | |||
| 157 | - | if len(x.clients) != was { | |||
| 158 | - | x.publishLists() | |||
| 159 | - | } | |||
| 160 | - | } | |||
| 161 | - | ||||
| 162 | - | func removeID(l []uint32, xid uint32) []uint32 { | |||
| 163 | - | for i, v := range l { | |||
| 164 | - | if v == xid { | |||
| 165 | - | return append(l[:i], l[i+1:]...) | |||
| 166 | - | } | |||
| 167 | - | } | |||
| 168 | - | return l | |||
| 169 | - | } | |||
| 170 | - | ||||
| 171 | // onClientMessage handles WL_SURFACE_SERIAL (pairing), the legacy | 145 | // onClientMessage handles WL_SURFACE_SERIAL (pairing), the legacy | ||
| 172 | // WL_SURFACE_ID (counted, ignored) and _NET_WM_STATE requests. | 146 | // WL_SURFACE_ID (counted, ignored) and _NET_WM_STATE requests. | ||
| 173 | func (x *XWM) onClientMessage(ev *xturbo.Event) { | 147 | func (x *XWM) onClientMessage(ev *xturbo.Event) { | ||
| @@ -253,7 +227,7 @@ func (x *XWM) onReparentNotify(ev *xturbo.Event) { | |||||
| 253 | s := x.tab.lookup(xid) | 227 | s := x.tab.lookup(xid) | ||
| 254 | if uint32(e.Parent()) != x.root { | 228 | if uint32(e.Parent()) != x.root { | ||
| 255 | if s != 0 { | 229 | if s != 0 { | ||
| 256 | - | x.dropWindow(s) | 230 | + | x.forgetWindow(s) |
| 257 | } | 231 | } | ||
| 258 | return | 232 | return | ||
| 259 | } | 233 | } | ||
| @@ -277,23 +251,6 @@ func (x *XWM) onReparentNotify(ev *xturbo.Event) { | |||||
| 277 | } | 251 | } | ||
| 278 | } | 252 | } | ||
| 279 | 253 | ||||
| 280 | - | // dropWindow forgets s: the host is told if it knew the window. | |||
| 281 | - | func (x *XWM) dropWindow(s Slot) { | |||
| 282 | - | rec := x.tab.rec(s) | |||
| 283 | - | xid := rec.xid | |||
| 284 | - | if rec.kind == kindManaged { | |||
| 285 | - | x.unlist(xid) | |||
| 286 | - | if x.active == s { | |||
| 287 | - | x.active = 0 | |||
| 288 | - | } | |||
| 289 | - | if rec.id != 0 && x.host != nil { | |||
| 290 | - | rec.id = 0 | |||
| 291 | - | x.host.Dissociate(s, true) | |||
| 292 | - | } | |||
| 293 | - | } | |||
| 294 | - | x.tab.remove(s) | |||
| 295 | - | } | |||
| 296 | - | ||||
| 297 | // tryPair links the window to the wl_surface carrying its serial, once both | 254 | // tryPair links the window to the wl_surface carrying its serial, once both | ||
| 298 | // are known and the window is ready (the host maps on the surface's first | 255 | // are known and the window is ready (the host maps on the surface's first | ||
| 299 | // buffer, so it must know the window's info by then). Unmanaged windows keep | 256 | // buffer, so it must know the window's info by then). Unmanaged windows keep | ||
M internal/adapters/wayland/xwm/props.go +10 -0
| @@ -203,6 +203,7 @@ func (x *XWM) applyProp(s Slot, idx int, format uint8, val []byte) { | |||
| 203 | 203 | rec.maxW, rec.maxH = int32(word(val, 7)), int32(word(val, 8)) | |
| 204 | 204 | } | |
| 205 | 205 | } | |
| 206 | + | x.classify(rec) | |
| 206 | 207 | case propProtocols: | |
| 207 | 208 | rec.deleteWindow, rec.takeFocus = false, false | |
| 208 | 209 | for i := 0; i+4 <= len(val); i += 4 { | |
| @@ -222,6 +223,7 @@ func (x *XWM) applyProp(s Slot, idx int, format uint8, val []byte) { | |||
| 222 | 223 | return | |
| 223 | 224 | } | |
| 224 | 225 | rec.transient = t | |
| 226 | + | x.classify(rec) | |
| 225 | 227 | if rec.ready { | |
| 226 | 228 | x.host.Parent(s, x.managedSlot(t)) | |
| 227 | 229 | } | |
| @@ -233,6 +235,7 @@ func (x *XWM) applyProp(s Slot, idx int, format uint8, val []byte) { | |||
| 233 | 235 | break | |
| 234 | 236 | } | |
| 235 | 237 | } | |
| 238 | + | x.classify(rec) | |
| 236 | 239 | case propState: | |
| 237 | 240 | var modal, fs bool | |
| 238 | 241 | for i := 0; i+4 <= len(val); i += 4 { | |
| @@ -292,6 +295,13 @@ func floatingType(t uint8) bool { | |||
| 292 | 295 | return false | |
| 293 | 296 | } | |
| 294 | 297 | ||
| 298 | + | // classify sets rec.floating, the single source of truth: a dialog-like | |
| 299 | + | // window type, a transient for a managed window, or a fixed size. | |
| 300 | + | func (x *XWM) classify(rec *record) { | |
| 301 | + | fixed := rec.minW > 0 && rec.minH > 0 && rec.minW == rec.maxW && rec.minH == rec.maxH | |
| 302 | + | rec.floating = floatingType(rec.wtype) || x.managedSlot(rec.transient) != 0 || fixed | |
| 303 | + | } | |
| 304 | + | ||
| 295 | 305 | // managedSlot returns the slot of the managed window xid, or 0. | |
| 296 | 306 | func (x *XWM) managedSlot(xid uint32) Slot { | |
| 297 | 307 | if xid == 0 { | |
| @@ -203,6 +203,7 @@ func (x *XWM) applyProp(s Slot, idx int, format uint8, val []byte) { | |||||
| 203 | rec.maxW, rec.maxH = int32(word(val, 7)), int32(word(val, 8)) | 203 | rec.maxW, rec.maxH = int32(word(val, 7)), int32(word(val, 8)) | ||
| 204 | } | 204 | } | ||
| 205 | } | 205 | } | ||
| 206 | + | x.classify(rec) | |||
| 206 | case propProtocols: | 207 | case propProtocols: | ||
| 207 | rec.deleteWindow, rec.takeFocus = false, false | 208 | rec.deleteWindow, rec.takeFocus = false, false | ||
| 208 | for i := 0; i+4 <= len(val); i += 4 { | 209 | for i := 0; i+4 <= len(val); i += 4 { | ||
| @@ -222,6 +223,7 @@ func (x *XWM) applyProp(s Slot, idx int, format uint8, val []byte) { | |||||
| 222 | return | 223 | return | ||
| 223 | } | 224 | } | ||
| 224 | rec.transient = t | 225 | rec.transient = t | ||
| 226 | + | x.classify(rec) | |||
| 225 | if rec.ready { | 227 | if rec.ready { | ||
| 226 | x.host.Parent(s, x.managedSlot(t)) | 228 | x.host.Parent(s, x.managedSlot(t)) | ||
| 227 | } | 229 | } | ||
| @@ -233,6 +235,7 @@ func (x *XWM) applyProp(s Slot, idx int, format uint8, val []byte) { | |||||
| 233 | break | 235 | break | ||
| 234 | } | 236 | } | ||
| 235 | } | 237 | } | ||
| 238 | + | x.classify(rec) | |||
| 236 | case propState: | 239 | case propState: | ||
| 237 | var modal, fs bool | 240 | var modal, fs bool | ||
| 238 | for i := 0; i+4 <= len(val); i += 4 { | 241 | for i := 0; i+4 <= len(val); i += 4 { | ||
| @@ -292,6 +295,13 @@ func floatingType(t uint8) bool { | |||||
| 292 | return false | 295 | return false | ||
| 293 | } | 296 | } | ||
| 294 | 297 | ||||
| 298 | + | // classify sets rec.floating, the single source of truth: a dialog-like | |||
| 299 | + | // window type, a transient for a managed window, or a fixed size. | |||
| 300 | + | func (x *XWM) classify(rec *record) { | |||
| 301 | + | fixed := rec.minW > 0 && rec.minH > 0 && rec.minW == rec.maxW && rec.minH == rec.maxH | |||
| 302 | + | rec.floating = floatingType(rec.wtype) || x.managedSlot(rec.transient) != 0 || fixed | |||
| 303 | + | } | |||
| 304 | + | ||||
| 295 | // managedSlot returns the slot of the managed window xid, or 0. | 305 | // managedSlot returns the slot of the managed window xid, or 0. | ||
| 296 | func (x *XWM) managedSlot(xid uint32) Slot { | 306 | func (x *XWM) managedSlot(xid uint32) Slot { | ||
| 297 | if xid == 0 { | 307 | if xid == 0 { | ||
M internal/adapters/wayland/xwm/streams_test.go +5 -0
| @@ -64,6 +64,11 @@ func streamScenario(t *testing.T) *harness { | |||
| 64 | 64 | hs := newHarness(t, nil) | |
| 65 | 65 | hs.classic(streamWin) | |
| 66 | 66 | hs.mapAndWait(streamWin) | |
| 67 | + | // The steady state is a focused window: a FocusIn on it is not denied, | |
| 68 | + | // so no request depends on how events are batched into flushes. The | |
| 69 | + | // poke makes sure the focus requests are out before the peer pauses. | |
| 70 | + | hs.x.Focus(hs.x.Slot(streamWin)) | |
| 71 | + | poke(hs) | |
| 67 | 72 | return hs | |
| 68 | 73 | } | |
| 69 | 74 | ||
| @@ -64,6 +64,11 @@ func streamScenario(t *testing.T) *harness { | |||||
| 64 | hs := newHarness(t, nil) | 64 | hs := newHarness(t, nil) | ||
| 65 | hs.classic(streamWin) | 65 | hs.classic(streamWin) | ||
| 66 | hs.mapAndWait(streamWin) | 66 | hs.mapAndWait(streamWin) | ||
| 67 | + | // The steady state is a focused window: a FocusIn on it is not denied, | |||
| 68 | + | // so no request depends on how events are batched into flushes. The | |||
| 69 | + | // poke makes sure the focus requests are out before the peer pauses. | |||
| 70 | + | hs.x.Focus(hs.x.Slot(streamWin)) | |||
| 71 | + | poke(hs) | |||
| 67 | return hs | 72 | return hs | ||
| 68 | } | 73 | } | ||
| 69 | 74 | ||||
M internal/adapters/wayland/xwm/table.go +1 -2
| @@ -1,5 +1,3 @@ | |||
| 1 | - | //lint:file-ignore U1000 pend, sent and stateSent are used by the configure flow | |
| 2 | - | ||
| 3 | 1 | package xwm | |
| 4 | 2 | ||
| 5 | 3 | import ( | |
| @@ -53,6 +51,7 @@ type record struct { | |||
| 53 | 51 | minW, minH, maxW, maxH int32 | |
| 54 | 52 | title, class string // owned, replaced only on change | |
| 55 | 53 | hasNetName bool | |
| 54 | + | floating bool // dialog-like type; set with the properties | |
| 56 | 55 | ||
| 57 | 56 | // pairing | |
| 58 | 57 | serial uint64 | |
| @@ -1,5 +1,3 @@ | |||||
| 1 | - | //lint:file-ignore U1000 pend, sent and stateSent are used by the configure flow | |||
| 2 | - | ||||
| 3 | package xwm | 1 | package xwm | ||
| 4 | 2 | ||||
| 5 | import ( | 3 | import ( | ||
| @@ -53,6 +51,7 @@ type record struct { | |||||
| 53 | minW, minH, maxW, maxH int32 | 51 | minW, minH, maxW, maxH int32 | ||
| 54 | title, class string // owned, replaced only on change | 52 | title, class string // owned, replaced only on change | ||
| 55 | hasNetName bool | 53 | hasNetName bool | ||
| 54 | + | floating bool // dialog-like type; set with the properties | |||
| 56 | 55 | ||||
| 57 | // pairing | 56 | // pairing | ||
| 58 | serial uint64 | 57 | serial uint64 | ||
M internal/adapters/wayland/xwm/testdata/streams/property_unchanged.hex +19 -19
| @@ -1,22 +1,3 @@ | |||
| 1 | - | 1c005f0001005000f30300000000000000000000000000000000000000000000 | |
| 2 | - | 0108600003000000ef0300000000000009000000000000000000000000000000 | |
| 3 | - | 6e6574207469746c650000001c00600001005000f30300000000000000000000 | |
| 4 | - | 0000000000000000000000000108610003000000ef0300000000000009000000 | |
| 5 | - | 0000000000000000000000006e6574207469746c650000001c00610001005000 | |
| 6 | - | f303000000000000000000000000000000000000000000000108620003000000 | |
| 7 | - | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 8 | - | 650000001c00620001005000f303000000000000000000000000000000000000 | |
| 9 | - | 000000000108630003000000ef03000000000000090000000000000000000000 | |
| 10 | - | 000000006e6574207469746c650000001c00630001005000f303000000000000 | |
| 11 | - | 000000000000000000000000000000000108640003000000ef03000000000000 | |
| 12 | - | 090000000000000000000000000000006e6574207469746c650000001c006400 | |
| 13 | - | 01005000f3030000000000000000000000000000000000000000000001086500 | |
| 14 | - | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 15 | - | 7469746c650000001c00650001005000f3030000000000000000000000000000 | |
| 16 | - | 00000000000000000108660003000000ef030000000000000900000000000000 | |
| 17 | - | 00000000000000006e6574207469746c650000001c00660001005000f3030000 | |
| 18 | - | 00000000000000000000000000000000000000000108670003000000ef030000 | |
| 19 | - | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 20 | 1 | 1c00670001005000f30300000000000000000000000000000000000000000000 | |
| 21 | 2 | 0108680003000000ef0300000000000009000000000000000000000000000000 | |
| 22 | 3 | 6e6574207469746c650000001c00680001005000f30300000000000000000000 | |
| @@ -948,3 +929,22 @@ ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 948 | 929 | 00000000000000006e6574207469746c650000001c00ee0101005000f3030000 | |
| 949 | 930 | 00000000000000000000000000000000000000000108ef0103000000ef030000 | |
| 950 | 931 | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 932 | + | 1c00ef0101005000f30300000000000000000000000000000000000000000000 | |
| 933 | + | 0108f00103000000ef0300000000000009000000000000000000000000000000 | |
| 934 | + | 6e6574207469746c650000001c00f00101005000f30300000000000000000000 | |
| 935 | + | 0000000000000000000000000108f10103000000ef0300000000000009000000 | |
| 936 | + | 0000000000000000000000006e6574207469746c650000001c00f10101005000 | |
| 937 | + | f303000000000000000000000000000000000000000000000108f20103000000 | |
| 938 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 939 | + | 650000001c00f20101005000f303000000000000000000000000000000000000 | |
| 940 | + | 000000000108f30103000000ef03000000000000090000000000000000000000 | |
| 941 | + | 000000006e6574207469746c650000001c00f30101005000f303000000000000 | |
| 942 | + | 000000000000000000000000000000000108f40103000000ef03000000000000 | |
| 943 | + | 090000000000000000000000000000006e6574207469746c650000001c00f401 | |
| 944 | + | 01005000f303000000000000000000000000000000000000000000000108f501 | |
| 945 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 946 | + | 7469746c650000001c00f50101005000f3030000000000000000000000000000 | |
| 947 | + | 00000000000000000108f60103000000ef030000000000000900000000000000 | |
| 948 | + | 00000000000000006e6574207469746c650000001c00f60101005000f3030000 | |
| 949 | + | 00000000000000000000000000000000000000000108f70103000000ef030000 | |
| 950 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| @@ -1,22 +1,3 @@ | |||||
| 1 | - | 1c005f0001005000f30300000000000000000000000000000000000000000000 | |||
| 2 | - | 0108600003000000ef0300000000000009000000000000000000000000000000 | |||
| 3 | - | 6e6574207469746c650000001c00600001005000f30300000000000000000000 | |||
| 4 | - | 0000000000000000000000000108610003000000ef0300000000000009000000 | |||
| 5 | - | 0000000000000000000000006e6574207469746c650000001c00610001005000 | |||
| 6 | - | f303000000000000000000000000000000000000000000000108620003000000 | |||
| 7 | - | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 8 | - | 650000001c00620001005000f303000000000000000000000000000000000000 | |||
| 9 | - | 000000000108630003000000ef03000000000000090000000000000000000000 | |||
| 10 | - | 000000006e6574207469746c650000001c00630001005000f303000000000000 | |||
| 11 | - | 000000000000000000000000000000000108640003000000ef03000000000000 | |||
| 12 | - | 090000000000000000000000000000006e6574207469746c650000001c006400 | |||
| 13 | - | 01005000f3030000000000000000000000000000000000000000000001086500 | |||
| 14 | - | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 15 | - | 7469746c650000001c00650001005000f3030000000000000000000000000000 | |||
| 16 | - | 00000000000000000108660003000000ef030000000000000900000000000000 | |||
| 17 | - | 00000000000000006e6574207469746c650000001c00660001005000f3030000 | |||
| 18 | - | 00000000000000000000000000000000000000000108670003000000ef030000 | |||
| 19 | - | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 20 | 1c00670001005000f30300000000000000000000000000000000000000000000 | 1 | 1c00670001005000f30300000000000000000000000000000000000000000000 | ||
| 21 | 0108680003000000ef0300000000000009000000000000000000000000000000 | 2 | 0108680003000000ef0300000000000009000000000000000000000000000000 | ||
| 22 | 6e6574207469746c650000001c00680001005000f30300000000000000000000 | 3 | 6e6574207469746c650000001c00680001005000f30300000000000000000000 | ||
| @@ -948,3 +929,22 @@ ef03000000000000090000000000000000000000000000006e6574207469746c | |||||
| 948 | 00000000000000006e6574207469746c650000001c00ee0101005000f3030000 | 929 | 00000000000000006e6574207469746c650000001c00ee0101005000f3030000 | ||
| 949 | 00000000000000000000000000000000000000000108ef0103000000ef030000 | 930 | 00000000000000000000000000000000000000000108ef0103000000ef030000 | ||
| 950 | 00000000090000000000000000000000000000006e6574207469746c65000000 | 931 | 00000000090000000000000000000000000000006e6574207469746c65000000 | ||
| 932 | + | 1c00ef0101005000f30300000000000000000000000000000000000000000000 | |||
| 933 | + | 0108f00103000000ef0300000000000009000000000000000000000000000000 | |||
| 934 | + | 6e6574207469746c650000001c00f00101005000f30300000000000000000000 | |||
| 935 | + | 0000000000000000000000000108f10103000000ef0300000000000009000000 | |||
| 936 | + | 0000000000000000000000006e6574207469746c650000001c00f10101005000 | |||
| 937 | + | f303000000000000000000000000000000000000000000000108f20103000000 | |||
| 938 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 939 | + | 650000001c00f20101005000f303000000000000000000000000000000000000 | |||
| 940 | + | 000000000108f30103000000ef03000000000000090000000000000000000000 | |||
| 941 | + | 000000006e6574207469746c650000001c00f30101005000f303000000000000 | |||
| 942 | + | 000000000000000000000000000000000108f40103000000ef03000000000000 | |||
| 943 | + | 090000000000000000000000000000006e6574207469746c650000001c00f401 | |||
| 944 | + | 01005000f303000000000000000000000000000000000000000000000108f501 | |||
| 945 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 946 | + | 7469746c650000001c00f50101005000f3030000000000000000000000000000 | |||
| 947 | + | 00000000000000000108f60103000000ef030000000000000900000000000000 | |||
| 948 | + | 00000000000000006e6574207469746c650000001c00f60101005000f3030000 | |||
| 949 | + | 00000000000000000000000000000000000000000108f70103000000ef030000 | |||
| 950 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
M internal/adapters/wayland/xwm/testdata/streams/steady.hex +43 -43
| @@ -1,46 +1,3 @@ | |||
| 1 | - | 1c005f0001005000f30300000000000000000000000000000000000000000000 | |
| 2 | - | 0108600003000000ef0300000000000009000000000000000000000000000000 | |
| 3 | - | 6e6574207469746c650000001600600001005000010050000000000000000000 | |
| 4 | - | 0a0014002c01c800000000000900600001005000000000000000000000000000 | |
| 5 | - | 0000000000000000000000001300600000010000010050000000000000000000 | |
| 6 | - | 0000000000000000000000001c00600001005000f30300000000000000000000 | |
| 7 | - | 0000000000000000000000000108610003000000ef0300000000000009000000 | |
| 8 | - | 0000000000000000000000006e6574207469746c650000001600610001005000 | |
| 9 | - | 0100500000000000000000000a0014002c01c800000000000900610001005000 | |
| 10 | - | 0000000000000000000000000000000000000000000000001300610000010000 | |
| 11 | - | 0100500000000000000000000000000000000000000000001c00610001005000 | |
| 12 | - | f303000000000000000000000000000000000000000000000108620003000000 | |
| 13 | - | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 14 | - | 6500000016006200010050000100500000000000000000000a0014002c01c800 | |
| 15 | - | 0000000009006200010050000000000000000000000000000000000000000000 | |
| 16 | - | 0000000013006200000100000100500000000000000000000000000000000000 | |
| 17 | - | 000000001c00620001005000f303000000000000000000000000000000000000 | |
| 18 | - | 000000000108630003000000ef03000000000000090000000000000000000000 | |
| 19 | - | 000000006e6574207469746c6500000016006300010050000100500000000000 | |
| 20 | - | 000000000a0014002c01c8000000000009006300010050000000000000000000 | |
| 21 | - | 0000000000000000000000000000000013006300000100000100500000000000 | |
| 22 | - | 000000000000000000000000000000001c00630001005000f303000000000000 | |
| 23 | - | 000000000000000000000000000000000108640003000000ef03000000000000 | |
| 24 | - | 090000000000000000000000000000006e6574207469746c6500000016006400 | |
| 25 | - | 010050000100500000000000000000000a0014002c01c8000000000009006400 | |
| 26 | - | 0100500000000000000000000000000000000000000000000000000013006400 | |
| 27 | - | 000100000100500000000000000000000000000000000000000000001c006400 | |
| 28 | - | 01005000f3030000000000000000000000000000000000000000000001086500 | |
| 29 | - | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 30 | - | 7469746c6500000016006500010050000100500000000000000000000a001400 | |
| 31 | - | 2c01c80000000000090065000100500000000000000000000000000000000000 | |
| 32 | - | 0000000000000000130065000001000001005000000000000000000000000000 | |
| 33 | - | 00000000000000001c00650001005000f3030000000000000000000000000000 | |
| 34 | - | 00000000000000000108660003000000ef030000000000000900000000000000 | |
| 35 | - | 00000000000000006e6574207469746c65000000160066000100500001005000 | |
| 36 | - | 00000000000000000a0014002c01c80000000000090066000100500000000000 | |
| 37 | - | 0000000000000000000000000000000000000000130066000001000001005000 | |
| 38 | - | 00000000000000000000000000000000000000001c00660001005000f3030000 | |
| 39 | - | 00000000000000000000000000000000000000000108670003000000ef030000 | |
| 40 | - | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 41 | - | 16006700010050000100500000000000000000000a0014002c01c80000000000 | |
| 42 | - | 0900670001005000000000000000000000000000000000000000000000000000 | |
| 43 | - | 1300670000010000010050000000000000000000000000000000000000000000 | |
| 44 | 1 | 1c00670001005000f30300000000000000000000000000000000000000000000 | |
| 45 | 2 | 0108680003000000ef0300000000000009000000000000000000000000000000 | |
| 46 | 3 | 6e6574207469746c650000001600680001005000010050000000000000000000 | |
| @@ -2148,3 +2105,46 @@ ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 2148 | 2105 | 1600ef01010050000100500000000000000000000a0014002c01c80000000000 | |
| 2149 | 2106 | 0900ef0101005000000000000000000000000000000000000000000000000000 | |
| 2150 | 2107 | 1300ef0100010000010050000000000000000000000000000000000000000000 | |
| 2108 | + | 1c00ef0101005000f30300000000000000000000000000000000000000000000 | |
| 2109 | + | 0108f00103000000ef0300000000000009000000000000000000000000000000 | |
| 2110 | + | 6e6574207469746c650000001600f00101005000010050000000000000000000 | |
| 2111 | + | 0a0014002c01c800000000000900f00101005000000000000000000000000000 | |
| 2112 | + | 0000000000000000000000001300f00100010000010050000000000000000000 | |
| 2113 | + | 0000000000000000000000001c00f00101005000f30300000000000000000000 | |
| 2114 | + | 0000000000000000000000000108f10103000000ef0300000000000009000000 | |
| 2115 | + | 0000000000000000000000006e6574207469746c650000001600f10101005000 | |
| 2116 | + | 0100500000000000000000000a0014002c01c800000000000900f10101005000 | |
| 2117 | + | 0000000000000000000000000000000000000000000000001300f10100010000 | |
| 2118 | + | 0100500000000000000000000000000000000000000000001c00f10101005000 | |
| 2119 | + | f303000000000000000000000000000000000000000000000108f20103000000 | |
| 2120 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 2121 | + | 650000001600f201010050000100500000000000000000000a0014002c01c800 | |
| 2122 | + | 000000000900f201010050000000000000000000000000000000000000000000 | |
| 2123 | + | 000000001300f201000100000100500000000000000000000000000000000000 | |
| 2124 | + | 000000001c00f20101005000f303000000000000000000000000000000000000 | |
| 2125 | + | 000000000108f30103000000ef03000000000000090000000000000000000000 | |
| 2126 | + | 000000006e6574207469746c650000001600f301010050000100500000000000 | |
| 2127 | + | 000000000a0014002c01c800000000000900f301010050000000000000000000 | |
| 2128 | + | 000000000000000000000000000000001300f301000100000100500000000000 | |
| 2129 | + | 000000000000000000000000000000001c00f30101005000f303000000000000 | |
| 2130 | + | 000000000000000000000000000000000108f40103000000ef03000000000000 | |
| 2131 | + | 090000000000000000000000000000006e6574207469746c650000001600f401 | |
| 2132 | + | 010050000100500000000000000000000a0014002c01c800000000000900f401 | |
| 2133 | + | 010050000000000000000000000000000000000000000000000000001300f401 | |
| 2134 | + | 000100000100500000000000000000000000000000000000000000001c00f401 | |
| 2135 | + | 01005000f303000000000000000000000000000000000000000000000108f501 | |
| 2136 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 2137 | + | 7469746c650000001600f501010050000100500000000000000000000a001400 | |
| 2138 | + | 2c01c800000000000900f5010100500000000000000000000000000000000000 | |
| 2139 | + | 00000000000000001300f5010001000001005000000000000000000000000000 | |
| 2140 | + | 00000000000000001c00f50101005000f3030000000000000000000000000000 | |
| 2141 | + | 00000000000000000108f60103000000ef030000000000000900000000000000 | |
| 2142 | + | 00000000000000006e6574207469746c650000001600f6010100500001005000 | |
| 2143 | + | 00000000000000000a0014002c01c800000000000900f6010100500000000000 | |
| 2144 | + | 00000000000000000000000000000000000000001300f6010001000001005000 | |
| 2145 | + | 00000000000000000000000000000000000000001c00f60101005000f3030000 | |
| 2146 | + | 00000000000000000000000000000000000000000108f70103000000ef030000 | |
| 2147 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 2148 | + | 1600f701010050000100500000000000000000000a0014002c01c80000000000 | |
| 2149 | + | 0900f70101005000000000000000000000000000000000000000000000000000 | |
| 2150 | + | 1300f70100010000010050000000000000000000000000000000000000000000 | |
| @@ -1,46 +1,3 @@ | |||||
| 1 | - | 1c005f0001005000f30300000000000000000000000000000000000000000000 | |||
| 2 | - | 0108600003000000ef0300000000000009000000000000000000000000000000 | |||
| 3 | - | 6e6574207469746c650000001600600001005000010050000000000000000000 | |||
| 4 | - | 0a0014002c01c800000000000900600001005000000000000000000000000000 | |||
| 5 | - | 0000000000000000000000001300600000010000010050000000000000000000 | |||
| 6 | - | 0000000000000000000000001c00600001005000f30300000000000000000000 | |||
| 7 | - | 0000000000000000000000000108610003000000ef0300000000000009000000 | |||
| 8 | - | 0000000000000000000000006e6574207469746c650000001600610001005000 | |||
| 9 | - | 0100500000000000000000000a0014002c01c800000000000900610001005000 | |||
| 10 | - | 0000000000000000000000000000000000000000000000001300610000010000 | |||
| 11 | - | 0100500000000000000000000000000000000000000000001c00610001005000 | |||
| 12 | - | f303000000000000000000000000000000000000000000000108620003000000 | |||
| 13 | - | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 14 | - | 6500000016006200010050000100500000000000000000000a0014002c01c800 | |||
| 15 | - | 0000000009006200010050000000000000000000000000000000000000000000 | |||
| 16 | - | 0000000013006200000100000100500000000000000000000000000000000000 | |||
| 17 | - | 000000001c00620001005000f303000000000000000000000000000000000000 | |||
| 18 | - | 000000000108630003000000ef03000000000000090000000000000000000000 | |||
| 19 | - | 000000006e6574207469746c6500000016006300010050000100500000000000 | |||
| 20 | - | 000000000a0014002c01c8000000000009006300010050000000000000000000 | |||
| 21 | - | 0000000000000000000000000000000013006300000100000100500000000000 | |||
| 22 | - | 000000000000000000000000000000001c00630001005000f303000000000000 | |||
| 23 | - | 000000000000000000000000000000000108640003000000ef03000000000000 | |||
| 24 | - | 090000000000000000000000000000006e6574207469746c6500000016006400 | |||
| 25 | - | 010050000100500000000000000000000a0014002c01c8000000000009006400 | |||
| 26 | - | 0100500000000000000000000000000000000000000000000000000013006400 | |||
| 27 | - | 000100000100500000000000000000000000000000000000000000001c006400 | |||
| 28 | - | 01005000f3030000000000000000000000000000000000000000000001086500 | |||
| 29 | - | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 30 | - | 7469746c6500000016006500010050000100500000000000000000000a001400 | |||
| 31 | - | 2c01c80000000000090065000100500000000000000000000000000000000000 | |||
| 32 | - | 0000000000000000130065000001000001005000000000000000000000000000 | |||
| 33 | - | 00000000000000001c00650001005000f3030000000000000000000000000000 | |||
| 34 | - | 00000000000000000108660003000000ef030000000000000900000000000000 | |||
| 35 | - | 00000000000000006e6574207469746c65000000160066000100500001005000 | |||
| 36 | - | 00000000000000000a0014002c01c80000000000090066000100500000000000 | |||
| 37 | - | 0000000000000000000000000000000000000000130066000001000001005000 | |||
| 38 | - | 00000000000000000000000000000000000000001c00660001005000f3030000 | |||
| 39 | - | 00000000000000000000000000000000000000000108670003000000ef030000 | |||
| 40 | - | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 41 | - | 16006700010050000100500000000000000000000a0014002c01c80000000000 | |||
| 42 | - | 0900670001005000000000000000000000000000000000000000000000000000 | |||
| 43 | - | 1300670000010000010050000000000000000000000000000000000000000000 | |||
| 44 | 1c00670001005000f30300000000000000000000000000000000000000000000 | 1 | 1c00670001005000f30300000000000000000000000000000000000000000000 | ||
| 45 | 0108680003000000ef0300000000000009000000000000000000000000000000 | 2 | 0108680003000000ef0300000000000009000000000000000000000000000000 | ||
| 46 | 6e6574207469746c650000001600680001005000010050000000000000000000 | 3 | 6e6574207469746c650000001600680001005000010050000000000000000000 | ||
| @@ -2148,3 +2105,46 @@ ef03000000000000090000000000000000000000000000006e6574207469746c | |||||
| 2148 | 1600ef01010050000100500000000000000000000a0014002c01c80000000000 | 2105 | 1600ef01010050000100500000000000000000000a0014002c01c80000000000 | ||
| 2149 | 0900ef0101005000000000000000000000000000000000000000000000000000 | 2106 | 0900ef0101005000000000000000000000000000000000000000000000000000 | ||
| 2150 | 1300ef0100010000010050000000000000000000000000000000000000000000 | 2107 | 1300ef0100010000010050000000000000000000000000000000000000000000 | ||
| 2108 | + | 1c00ef0101005000f30300000000000000000000000000000000000000000000 | |||
| 2109 | + | 0108f00103000000ef0300000000000009000000000000000000000000000000 | |||
| 2110 | + | 6e6574207469746c650000001600f00101005000010050000000000000000000 | |||
| 2111 | + | 0a0014002c01c800000000000900f00101005000000000000000000000000000 | |||
| 2112 | + | 0000000000000000000000001300f00100010000010050000000000000000000 | |||
| 2113 | + | 0000000000000000000000001c00f00101005000f30300000000000000000000 | |||
| 2114 | + | 0000000000000000000000000108f10103000000ef0300000000000009000000 | |||
| 2115 | + | 0000000000000000000000006e6574207469746c650000001600f10101005000 | |||
| 2116 | + | 0100500000000000000000000a0014002c01c800000000000900f10101005000 | |||
| 2117 | + | 0000000000000000000000000000000000000000000000001300f10100010000 | |||
| 2118 | + | 0100500000000000000000000000000000000000000000001c00f10101005000 | |||
| 2119 | + | f303000000000000000000000000000000000000000000000108f20103000000 | |||
| 2120 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |||
| 2121 | + | 650000001600f201010050000100500000000000000000000a0014002c01c800 | |||
| 2122 | + | 000000000900f201010050000000000000000000000000000000000000000000 | |||
| 2123 | + | 000000001300f201000100000100500000000000000000000000000000000000 | |||
| 2124 | + | 000000001c00f20101005000f303000000000000000000000000000000000000 | |||
| 2125 | + | 000000000108f30103000000ef03000000000000090000000000000000000000 | |||
| 2126 | + | 000000006e6574207469746c650000001600f301010050000100500000000000 | |||
| 2127 | + | 000000000a0014002c01c800000000000900f301010050000000000000000000 | |||
| 2128 | + | 000000000000000000000000000000001300f301000100000100500000000000 | |||
| 2129 | + | 000000000000000000000000000000001c00f30101005000f303000000000000 | |||
| 2130 | + | 000000000000000000000000000000000108f40103000000ef03000000000000 | |||
| 2131 | + | 090000000000000000000000000000006e6574207469746c650000001600f401 | |||
| 2132 | + | 010050000100500000000000000000000a0014002c01c800000000000900f401 | |||
| 2133 | + | 010050000000000000000000000000000000000000000000000000001300f401 | |||
| 2134 | + | 000100000100500000000000000000000000000000000000000000001c00f401 | |||
| 2135 | + | 01005000f303000000000000000000000000000000000000000000000108f501 | |||
| 2136 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |||
| 2137 | + | 7469746c650000001600f501010050000100500000000000000000000a001400 | |||
| 2138 | + | 2c01c800000000000900f5010100500000000000000000000000000000000000 | |||
| 2139 | + | 00000000000000001300f5010001000001005000000000000000000000000000 | |||
| 2140 | + | 00000000000000001c00f50101005000f3030000000000000000000000000000 | |||
| 2141 | + | 00000000000000000108f60103000000ef030000000000000900000000000000 | |||
| 2142 | + | 00000000000000006e6574207469746c650000001600f6010100500001005000 | |||
| 2143 | + | 00000000000000000a0014002c01c800000000000900f6010100500000000000 | |||
| 2144 | + | 00000000000000000000000000000000000000001300f6010001000001005000 | |||
| 2145 | + | 00000000000000000000000000000000000000001c00f60101005000f3030000 | |||
| 2146 | + | 00000000000000000000000000000000000000000108f70103000000ef030000 | |||
| 2147 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |||
| 2148 | + | 1600f701010050000100500000000000000000000a0014002c01c80000000000 | |||
| 2149 | + | 0900f70101005000000000000000000000000000000000000000000000000000 | |||
| 2150 | + | 1300f70100010000010050000000000000000000000000000000000000000000 | |||
M internal/adapters/wayland/xwm/xpeer_test.go +32 -0
| @@ -90,6 +90,8 @@ type xpeer struct { | |||
| 90 | 90 | record bool | |
| 91 | 91 | ||
| 92 | 92 | gate atomic.Pointer[chan struct{}] // closed = reading allowed | |
| 93 | + | sbuf [4096]byte | |
| 94 | + | sink atomic.Bool // discard requests without recording or allocating | |
| 93 | 95 | done chan struct{} | |
| 94 | 96 | } | |
| 95 | 97 | ||
| @@ -159,6 +161,12 @@ func (p *xpeer) serve() { | |||
| 159 | 161 | } | |
| 160 | 162 | for { | |
| 161 | 163 | <-*p.gate.Load() | |
| 164 | + | if p.sink.Load() { | |
| 165 | + | if !p.discard() { | |
| 166 | + | return | |
| 167 | + | } | |
| 168 | + | continue | |
| 169 | + | } | |
| 162 | 170 | var h [4]byte | |
| 163 | 171 | if _, err := io.ReadFull(p.f, h[:]); err != nil { | |
| 164 | 172 | return | |
| @@ -187,6 +195,30 @@ func (p *xpeer) serve() { | |||
| 187 | 195 | } | |
| 188 | 196 | } | |
| 189 | 197 | ||
| 198 | + | // discard reads and drops one request without allocating. It reports false | |
| 199 | + | // when the connection ended. | |
| 200 | + | func (p *xpeer) discard() bool { | |
| 201 | + | if _, err := io.ReadFull(p.f, p.sbuf[:4]); err != nil { | |
| 202 | + | return false | |
| 203 | + | } | |
| 204 | + | n := int(binary.LittleEndian.Uint16(p.sbuf[2:])) * 4 | |
| 205 | + | left := n - 4 | |
| 206 | + | if n == 0 { | |
| 207 | + | if _, err := io.ReadFull(p.f, p.sbuf[:4]); err != nil { | |
| 208 | + | return false | |
| 209 | + | } | |
| 210 | + | left = int(binary.LittleEndian.Uint32(p.sbuf[:4]))*4 - 8 | |
| 211 | + | } | |
| 212 | + | for left > 0 { | |
| 213 | + | m, err := p.f.Read(p.sbuf[:min(left, len(p.sbuf))]) | |
| 214 | + | if err != nil { | |
| 215 | + | return false | |
| 216 | + | } | |
| 217 | + | left -= m | |
| 218 | + | } | |
| 219 | + | return true | |
| 220 | + | } | |
| 221 | + | ||
| 190 | 222 | // write sends bytes to the client. The caller holds p.mu. | |
| 191 | 223 | func (p *xpeer) write(b []byte) { | |
| 192 | 224 | if p.record { | |
| @@ -90,6 +90,8 @@ type xpeer struct { | |||||
| 90 | record bool | 90 | record bool | ||
| 91 | 91 | ||||
| 92 | gate atomic.Pointer[chan struct{}] // closed = reading allowed | 92 | gate atomic.Pointer[chan struct{}] // closed = reading allowed | ||
| 93 | + | sbuf [4096]byte | |||
| 94 | + | sink atomic.Bool // discard requests without recording or allocating | |||
| 93 | done chan struct{} | 95 | done chan struct{} | ||
| 94 | } | 96 | } | ||
| 95 | 97 | ||||
| @@ -159,6 +161,12 @@ func (p *xpeer) serve() { | |||||
| 159 | } | 161 | } | ||
| 160 | for { | 162 | for { | ||
| 161 | <-*p.gate.Load() | 163 | <-*p.gate.Load() | ||
| 164 | + | if p.sink.Load() { | |||
| 165 | + | if !p.discard() { | |||
| 166 | + | return | |||
| 167 | + | } | |||
| 168 | + | continue | |||
| 169 | + | } | |||
| 162 | var h [4]byte | 170 | var h [4]byte | ||
| 163 | if _, err := io.ReadFull(p.f, h[:]); err != nil { | 171 | if _, err := io.ReadFull(p.f, h[:]); err != nil { | ||
| 164 | return | 172 | return | ||
| @@ -187,6 +195,30 @@ func (p *xpeer) serve() { | |||||
| 187 | } | 195 | } | ||
| 188 | } | 196 | } | ||
| 189 | 197 | ||||
| 198 | + | // discard reads and drops one request without allocating. It reports false | |||
| 199 | + | // when the connection ended. | |||
| 200 | + | func (p *xpeer) discard() bool { | |||
| 201 | + | if _, err := io.ReadFull(p.f, p.sbuf[:4]); err != nil { | |||
| 202 | + | return false | |||
| 203 | + | } | |||
| 204 | + | n := int(binary.LittleEndian.Uint16(p.sbuf[2:])) * 4 | |||
| 205 | + | left := n - 4 | |||
| 206 | + | if n == 0 { | |||
| 207 | + | if _, err := io.ReadFull(p.f, p.sbuf[:4]); err != nil { | |||
| 208 | + | return false | |||
| 209 | + | } | |||
| 210 | + | left = int(binary.LittleEndian.Uint32(p.sbuf[:4]))*4 - 8 | |||
| 211 | + | } | |||
| 212 | + | for left > 0 { | |||
| 213 | + | m, err := p.f.Read(p.sbuf[:min(left, len(p.sbuf))]) | |||
| 214 | + | if err != nil { | |||
| 215 | + | return false | |||
| 216 | + | } | |||
| 217 | + | left -= m | |||
| 218 | + | } | |||
| 219 | + | return true | |||
| 220 | + | } | |||
| 221 | + | ||||
| 190 | // write sends bytes to the client. The caller holds p.mu. | 222 | // write sends bytes to the client. The caller holds p.mu. | ||
| 191 | func (p *xpeer) write(b []byte) { | 223 | func (p *xpeer) write(b []byte) { | ||
| 192 | if p.record { | 224 | if p.record { | ||
M internal/adapters/wayland/xwm/xwm.go +0 -2
| @@ -1,5 +1,3 @@ | |||
| 1 | - | //lint:file-ignore U1000 refocus and listsDirty are used by the focus and configure flows | |
| 2 | - | ||
| 3 | 1 | // Package xwm is the built-in X11 window manager for Xwayland: it speaks the | |
| 4 | 2 | // X protocol over the -wm socket with xturbo and reports managed windows to | |
| 5 | 3 | // the Wayland server through the unexported host interface. | |
| @@ -1,5 +1,3 @@ | |||||
| 1 | - | //lint:file-ignore U1000 refocus and listsDirty are used by the focus and configure flows | |||
| 2 | - | ||||
| 3 | // Package xwm is the built-in X11 window manager for Xwayland: it speaks the | 1 | // Package xwm is the built-in X11 window manager for Xwayland: it speaks the | ||
| 4 | // X protocol over the -wm socket with xturbo and reports managed windows to | 2 | // X protocol over the -wm socket with xturbo and reports managed windows to | ||
| 5 | // the Wayland server through the unexported host interface. | 3 | // the Wayland server through the unexported host interface. | ||