feat(wayland): host override-redirect X11 windows as unmanaged windows
- commit
- b3f6b3aec87e27dae2c1c26a4b3e5a6d5a4eb11c
- author
- brice · · 2026-10-22 15:19 UTC
- committer
- brice · · 2026-10-22 17:54 UTC
- parent
- 27b8b9e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +271 -8
M Makefile +1 -1
| @@ -70,7 +70,7 @@ adapter-imports: | |||
| 70 | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 71 | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 72 | 72 | perf-check: | |
| 73 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations)$$' -count=1 | |
| 73 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations|TestOverrideMovedUnchangedAllocations)$$' -count=1 | |
| 74 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | |
| 75 | 75 | check: vet test arch fakes-check log-check perf-check | |
| 76 | 76 | ||
| @@ -70,7 +70,7 @@ adapter-imports: | |||||
| 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | ||
| 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 72 | perf-check: | 72 | perf-check: | ||
| 73 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations)$$' -count=1 | 73 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations|TestOverrideMovedUnchangedAllocations)$$' -count=1 |
| 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | ||
| 75 | check: vet test arch fakes-check log-check perf-check | 75 | check: vet test arch fakes-check log-check perf-check | ||
| 76 | 76 | ||||
M internal/adapters/wayland/frames.go +2 -0
| @@ -214,6 +214,8 @@ func (s *Server) suspended(surf *surface) bool { | |||
| 214 | 214 | func (s *Server) frameOutput(surf *surface) string { | |
| 215 | 215 | surf = toplevelRoot(surf) | |
| 216 | 216 | switch { | |
| 217 | + | case surf.x11win != nil && surf.x11win.isOverride(): | |
| 218 | + | return s.overrideOutput(surf.x11win) | |
| 217 | 219 | case surf.toplevelWindow() != nil: | |
| 218 | 220 | if s.suspended(surf) { | |
| 219 | 221 | return suspendedFrameQueue | |
| @@ -214,6 +214,8 @@ func (s *Server) suspended(surf *surface) bool { | |||||
| 214 | func (s *Server) frameOutput(surf *surface) string { | 214 | func (s *Server) frameOutput(surf *surface) string { | ||
| 215 | surf = toplevelRoot(surf) | 215 | surf = toplevelRoot(surf) | ||
| 216 | switch { | 216 | switch { | ||
| 217 | + | case surf.x11win != nil && surf.x11win.isOverride(): | |||
| 218 | + | return s.overrideOutput(surf.x11win) | |||
| 217 | case surf.toplevelWindow() != nil: | 219 | case surf.toplevelWindow() != nil: | ||
| 218 | if s.suspended(surf) { | 220 | if s.suspended(surf) { | ||
| 219 | return suspendedFrameQueue | 221 | return suspendedFrameQueue | ||
M internal/adapters/wayland/outputs.go +3 -0
| @@ -209,6 +209,9 @@ func (s *Server) setOutputs(c ports.SetOutputs) { | |||
| 209 | 209 | changed := !slices.Equal(s.outputPlaces, c.Outputs) | |
| 210 | 210 | focusChanged := s.focusedOutput != c.Focused | |
| 211 | 211 | s.outputPlaces = append(ports.Layout(nil), c.Outputs...) | |
| 212 | + | if s.xwm != nil { | |
| 213 | + | s.xwm.SetLayout(s.outputPlaces) | |
| 214 | + | } | |
| 212 | 215 | defer func() { | |
| 213 | 216 | if changed { | |
| 214 | 217 | s.refreshOutputManagers() | |
| @@ -209,6 +209,9 @@ func (s *Server) setOutputs(c ports.SetOutputs) { | |||||
| 209 | changed := !slices.Equal(s.outputPlaces, c.Outputs) | 209 | changed := !slices.Equal(s.outputPlaces, c.Outputs) | ||
| 210 | focusChanged := s.focusedOutput != c.Focused | 210 | focusChanged := s.focusedOutput != c.Focused | ||
| 211 | s.outputPlaces = append(ports.Layout(nil), c.Outputs...) | 211 | s.outputPlaces = append(ports.Layout(nil), c.Outputs...) | ||
| 212 | + | if s.xwm != nil { | |||
| 213 | + | s.xwm.SetLayout(s.outputPlaces) | |||
| 214 | + | } | |||
| 212 | defer func() { | 215 | defer func() { | ||
| 213 | if changed { | 216 | if changed { | ||
| 214 | s.refreshOutputManagers() | 217 | s.refreshOutputManagers() | ||
M internal/adapters/wayland/scale.go +2 -0
| @@ -25,6 +25,8 @@ func (s *Server) outputOfSurface(surf *surface) *output { | |||
| 25 | 25 | } | |
| 26 | 26 | name := "" | |
| 27 | 27 | switch { | |
| 28 | + | case surf.x11win != nil && surf.x11win.isOverride(): | |
| 29 | + | name = s.overrideOutput(surf.x11win) | |
| 28 | 30 | case surf.toplevelWindow() != nil: | |
| 29 | 31 | name = surf.toplevelWindow().last.Output | |
| 30 | 32 | case surf.layer != nil && surf.layer.output != nil: | |
| @@ -25,6 +25,8 @@ func (s *Server) outputOfSurface(surf *surface) *output { | |||||
| 25 | } | 25 | } | ||
| 26 | name := "" | 26 | name := "" | ||
| 27 | switch { | 27 | switch { | ||
| 28 | + | case surf.x11win != nil && surf.x11win.isOverride(): | |||
| 29 | + | name = s.overrideOutput(surf.x11win) | |||
| 28 | case surf.toplevelWindow() != nil: | 30 | case surf.toplevelWindow() != nil: | ||
| 29 | name = surf.toplevelWindow().last.Output | 31 | name = surf.toplevelWindow().last.Output | ||
| 30 | case surf.layer != nil && surf.layer.output != nil: | 32 | case surf.layer != nil && surf.layer.output != nil: | ||
M internal/adapters/wayland/seat.go +1 -1
| @@ -300,7 +300,7 @@ func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Ke | |||
| 300 | 300 | // already dead. Admission is enforced before entering or delivering. | |
| 301 | 301 | return l.surface.wl, s.clientKeyboards(l.surface.wl.Client()) | |
| 302 | 302 | } | |
| 303 | - | if w := s.windows[id]; w != nil && w.mapped { | |
| 303 | + | if w := s.windows[id]; w != nil && w.mapped && !w.isOverride() { | |
| 304 | 304 | if surf := w.surfaceResource(); surf != nil { | |
| 305 | 305 | return surf, s.windowKeyboards(w) | |
| 306 | 306 | } | |
| @@ -300,7 +300,7 @@ func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Ke | |||||
| 300 | // already dead. Admission is enforced before entering or delivering. | 300 | // already dead. Admission is enforced before entering or delivering. | ||
| 301 | return l.surface.wl, s.clientKeyboards(l.surface.wl.Client()) | 301 | return l.surface.wl, s.clientKeyboards(l.surface.wl.Client()) | ||
| 302 | } | 302 | } | ||
| 303 | - | if w := s.windows[id]; w != nil && w.mapped { | 303 | + | if w := s.windows[id]; w != nil && w.mapped && !w.isOverride() { |
| 304 | if surf := w.surfaceResource(); surf != nil { | 304 | if surf := w.surfaceResource(); surf != nil { | ||
| 305 | return surf, s.windowKeyboards(w) | 305 | return surf, s.windowKeyboards(w) | ||
| 306 | } | 306 | } | ||
M internal/adapters/wayland/window.go +1 -1
| @@ -11,7 +11,7 @@ import ( | |||
| 11 | 11 | // role. | |
| 12 | 12 | ||
| 13 | 13 | // isToplevel reports whether w is a toplevel (xdg or X11), not a popup. | |
| 14 | - | func (w *window) isToplevel() bool { return w.toplevel != nil || w.x11 != nil } | |
| 14 | + | func (w *window) isToplevel() bool { return w.toplevel != nil || (w.x11 != nil && !w.x11.override) } | |
| 15 | 15 | ||
| 16 | 16 | // srv is the server the window belongs to. | |
| 17 | 17 | func (w *window) srv() *Server { | |
| @@ -11,7 +11,7 @@ import ( | |||||
| 11 | // role. | 11 | // role. | ||
| 12 | 12 | ||||
| 13 | // isToplevel reports whether w is a toplevel (xdg or X11), not a popup. | 13 | // isToplevel reports whether w is a toplevel (xdg or X11), not a popup. | ||
| 14 | - | func (w *window) isToplevel() bool { return w.toplevel != nil || w.x11 != nil } | 14 | + | func (w *window) isToplevel() bool { return w.toplevel != nil || (w.x11 != nil && !w.x11.override) } |
| 15 | 15 | ||||
| 16 | // srv is the server the window belongs to. | 16 | // srv is the server the window belongs to. | ||
| 17 | func (w *window) srv() *Server { | 17 | func (w *window) srv() *Server { | ||
A internal/adapters/wayland/x11override_test.go +107 -0
| @@ -0,0 +1,107 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 7 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | const ovWin = 0x500002 | |
| 12 | + | ||
| 13 | + | // overrideWindow maps an override-redirect X window and pairs its surface. | |
| 14 | + | func (r *x11Rig) overrideWindow(win uint32, x, y int16, serial uint64) uint32 { | |
| 15 | + | r.t.Helper() | |
| 16 | + | r.peer.createOverride(win, x, y) | |
| 17 | + | r.peer.mapNotify(win) | |
| 18 | + | return r.pair(win, serial) | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func (r *x11Rig) state(f func()) { | |
| 22 | + | r.t.Helper() | |
| 23 | + | r.s.display.Do(f) | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | func TestOverrideMapsOnFirstCommitAndMoves(t *testing.T) { | |
| 27 | + | r := newX11Rig(t) | |
| 28 | + | r.overrideWindow(ovWin, 30, 40, x11Serial) | |
| 29 | + | ||
| 30 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |
| 31 | + | if m.X != 30 || m.Y != 40 || m.Width != 100 || m.Height != 50 || m.NoInput { | |
| 32 | + | t.Fatalf("mapped %+v", m) | |
| 33 | + | } | |
| 34 | + | r.state(func() { | |
| 35 | + | w := r.s.windows[m.ID] | |
| 36 | + | if w == nil || !w.isOverride() || w.isToplevel() || !w.mapped { | |
| 37 | + | t.Errorf("override window %+v", w) | |
| 38 | + | } | |
| 39 | + | if surf, _ := r.s.focusTarget(m.ID); surf != nil { | |
| 40 | + | t.Error("override window is a keyboard focus target") | |
| 41 | + | } | |
| 42 | + | }) | |
| 43 | + | ||
| 44 | + | r.peer.configureNotify(ovWin, 60, 70, 100, 50) | |
| 45 | + | if mv := awaitEvent[ports.UnmanagedMoved](t, r.events); mv.ID != m.ID || mv.X != 60 || mv.Y != 70 { | |
| 46 | + | t.Fatalf("moved %+v", mv) | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | r.peer.unmapNotify(ovWin) | |
| 50 | + | if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID { | |
| 51 | + | t.Fatalf("unmapped %+v", u) | |
| 52 | + | } | |
| 53 | + | r.state(func() { | |
| 54 | + | if r.s.windows[m.ID] != nil { | |
| 55 | + | t.Error("override window kept after unmap") | |
| 56 | + | } | |
| 57 | + | }) | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func TestOverrideSurfaceDestroyUnmaps(t *testing.T) { | |
| 61 | + | r := newX11Rig(t) | |
| 62 | + | surf := r.overrideWindow(ovWin, 0, 0, x11Serial) | |
| 63 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |
| 64 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy) | |
| 65 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 66 | + | t.Fatal(err) | |
| 67 | + | } | |
| 68 | + | if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID { | |
| 69 | + | t.Fatalf("unmapped %+v", u) | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | func TestOverrideTakesPointerAndOutput(t *testing.T) { | |
| 74 | + | r := newX11Rig(t) | |
| 75 | + | r.commands <- ports.SetOutputs{Outputs: ports.Layout{ | |
| 76 | + | {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |
| 77 | + | {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |
| 78 | + | }, Focused: "A"} | |
| 79 | + | r.overrideWindow(ovWin, 100, 10, x11Serial) | |
| 80 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |
| 81 | + | r.state(func() { | |
| 82 | + | w := r.s.windows[m.ID] | |
| 83 | + | if got := r.s.frameOutput(w.x11.surface); got != "A" { | |
| 84 | + | t.Errorf("frame output %q, want A", got) | |
| 85 | + | } | |
| 86 | + | if surf, _, _, _ := r.s.pointerSurface(m.ID, 1, 1); surf == nil { | |
| 87 | + | t.Error("pointer cannot enter the override surface") | |
| 88 | + | } | |
| 89 | + | }) | |
| 90 | + | r.peer.configureNotify(ovWin, 2000, 10, 100, 50) | |
| 91 | + | awaitEvent[ports.UnmanagedMoved](t, r.events) | |
| 92 | + | r.state(func() { | |
| 93 | + | if got := r.s.frameOutput(r.s.windows[m.ID].x11.surface); got != "B" { | |
| 94 | + | t.Errorf("frame output %q after move, want B", got) | |
| 95 | + | } | |
| 96 | + | }) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | func TestOverrideMovedUnchangedAllocations(t *testing.T) { | |
| 100 | + | s := &Server{windows: map[ports.WindowID]*window{}, x11Slots: map[xwm.Slot]*window{}} | |
| 101 | + | w := &window{id: 1, x11: &x11Window{override: true, rect: ports.Rect{X: 1, Y: 2, W: 3, H: 4}}} | |
| 102 | + | s.x11Slots[1] = w | |
| 103 | + | h := hostImpl{s} | |
| 104 | + | if n := testing.AllocsPerRun(1000, func() { h.OverrideMoved(1, 1, 2, 3, 4) }); n != 0 { | |
| 105 | + | t.Fatalf("%v allocs", n) | |
| 106 | + | } | |
| 107 | + | } | |
| @@ -0,0 +1,107 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 7 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | const ovWin = 0x500002 | |||
| 12 | + | ||||
| 13 | + | // overrideWindow maps an override-redirect X window and pairs its surface. | |||
| 14 | + | func (r *x11Rig) overrideWindow(win uint32, x, y int16, serial uint64) uint32 { | |||
| 15 | + | r.t.Helper() | |||
| 16 | + | r.peer.createOverride(win, x, y) | |||
| 17 | + | r.peer.mapNotify(win) | |||
| 18 | + | return r.pair(win, serial) | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func (r *x11Rig) state(f func()) { | |||
| 22 | + | r.t.Helper() | |||
| 23 | + | r.s.display.Do(f) | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | func TestOverrideMapsOnFirstCommitAndMoves(t *testing.T) { | |||
| 27 | + | r := newX11Rig(t) | |||
| 28 | + | r.overrideWindow(ovWin, 30, 40, x11Serial) | |||
| 29 | + | ||||
| 30 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |||
| 31 | + | if m.X != 30 || m.Y != 40 || m.Width != 100 || m.Height != 50 || m.NoInput { | |||
| 32 | + | t.Fatalf("mapped %+v", m) | |||
| 33 | + | } | |||
| 34 | + | r.state(func() { | |||
| 35 | + | w := r.s.windows[m.ID] | |||
| 36 | + | if w == nil || !w.isOverride() || w.isToplevel() || !w.mapped { | |||
| 37 | + | t.Errorf("override window %+v", w) | |||
| 38 | + | } | |||
| 39 | + | if surf, _ := r.s.focusTarget(m.ID); surf != nil { | |||
| 40 | + | t.Error("override window is a keyboard focus target") | |||
| 41 | + | } | |||
| 42 | + | }) | |||
| 43 | + | ||||
| 44 | + | r.peer.configureNotify(ovWin, 60, 70, 100, 50) | |||
| 45 | + | if mv := awaitEvent[ports.UnmanagedMoved](t, r.events); mv.ID != m.ID || mv.X != 60 || mv.Y != 70 { | |||
| 46 | + | t.Fatalf("moved %+v", mv) | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | r.peer.unmapNotify(ovWin) | |||
| 50 | + | if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID { | |||
| 51 | + | t.Fatalf("unmapped %+v", u) | |||
| 52 | + | } | |||
| 53 | + | r.state(func() { | |||
| 54 | + | if r.s.windows[m.ID] != nil { | |||
| 55 | + | t.Error("override window kept after unmap") | |||
| 56 | + | } | |||
| 57 | + | }) | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func TestOverrideSurfaceDestroyUnmaps(t *testing.T) { | |||
| 61 | + | r := newX11Rig(t) | |||
| 62 | + | surf := r.overrideWindow(ovWin, 0, 0, x11Serial) | |||
| 63 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |||
| 64 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy) | |||
| 65 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 66 | + | t.Fatal(err) | |||
| 67 | + | } | |||
| 68 | + | if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID { | |||
| 69 | + | t.Fatalf("unmapped %+v", u) | |||
| 70 | + | } | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func TestOverrideTakesPointerAndOutput(t *testing.T) { | |||
| 74 | + | r := newX11Rig(t) | |||
| 75 | + | r.commands <- ports.SetOutputs{Outputs: ports.Layout{ | |||
| 76 | + | {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |||
| 77 | + | {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |||
| 78 | + | }, Focused: "A"} | |||
| 79 | + | r.overrideWindow(ovWin, 100, 10, x11Serial) | |||
| 80 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |||
| 81 | + | r.state(func() { | |||
| 82 | + | w := r.s.windows[m.ID] | |||
| 83 | + | if got := r.s.frameOutput(w.x11.surface); got != "A" { | |||
| 84 | + | t.Errorf("frame output %q, want A", got) | |||
| 85 | + | } | |||
| 86 | + | if surf, _, _, _ := r.s.pointerSurface(m.ID, 1, 1); surf == nil { | |||
| 87 | + | t.Error("pointer cannot enter the override surface") | |||
| 88 | + | } | |||
| 89 | + | }) | |||
| 90 | + | r.peer.configureNotify(ovWin, 2000, 10, 100, 50) | |||
| 91 | + | awaitEvent[ports.UnmanagedMoved](t, r.events) | |||
| 92 | + | r.state(func() { | |||
| 93 | + | if got := r.s.frameOutput(r.s.windows[m.ID].x11.surface); got != "B" { | |||
| 94 | + | t.Errorf("frame output %q after move, want B", got) | |||
| 95 | + | } | |||
| 96 | + | }) | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func TestOverrideMovedUnchangedAllocations(t *testing.T) { | |||
| 100 | + | s := &Server{windows: map[ports.WindowID]*window{}, x11Slots: map[xwm.Slot]*window{}} | |||
| 101 | + | w := &window{id: 1, x11: &x11Window{override: true, rect: ports.Rect{X: 1, Y: 2, W: 3, H: 4}}} | |||
| 102 | + | s.x11Slots[1] = w | |||
| 103 | + | h := hostImpl{s} | |||
| 104 | + | if n := testing.AllocsPerRun(1000, func() { h.OverrideMoved(1, 1, 2, 3, 4) }); n != 0 { | |||
| 105 | + | t.Fatalf("%v allocs", n) | |||
| 106 | + | } | |||
| 107 | + | } | |||
M internal/adapters/wayland/x11peer_test.go +34 -0
| @@ -244,6 +244,40 @@ func (p *x11Peer) createNotify(win uint32) { | |||
| 244 | 244 | }) | |
| 245 | 245 | } | |
| 246 | 246 | ||
| 247 | + | // createOverride announces an override-redirect window of 100x50 at x,y. | |
| 248 | + | func (p *x11Peer) createOverride(win uint32, x, y int16) { | |
| 249 | + | p.event(16, func(b *[32]byte) { | |
| 250 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |
| 251 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 252 | + | binary.LittleEndian.PutUint16(b[12:], uint16(x)) | |
| 253 | + | binary.LittleEndian.PutUint16(b[14:], uint16(y)) | |
| 254 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |
| 255 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |
| 256 | + | b[22] = 1 | |
| 257 | + | }) | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | func (p *x11Peer) mapNotify(win uint32) { | |
| 261 | + | p.event(19, func(b *[32]byte) { | |
| 262 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |
| 263 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 264 | + | b[12] = 1 | |
| 265 | + | }) | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | // configureNotify moves and resizes a window (root SubstructureNotify form). | |
| 269 | + | func (p *x11Peer) configureNotify(win uint32, x, y int16, w, h uint16) { | |
| 270 | + | p.event(22, func(b *[32]byte) { | |
| 271 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |
| 272 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 273 | + | binary.LittleEndian.PutUint16(b[16:], uint16(x)) | |
| 274 | + | binary.LittleEndian.PutUint16(b[18:], uint16(y)) | |
| 275 | + | binary.LittleEndian.PutUint16(b[20:], w) | |
| 276 | + | binary.LittleEndian.PutUint16(b[22:], h) | |
| 277 | + | b[24] = 1 | |
| 278 | + | }) | |
| 279 | + | } | |
| 280 | + | ||
| 247 | 281 | func (p *x11Peer) windowEvent(code uint8, win uint32) { | |
| 248 | 282 | p.event(code, func(b *[32]byte) { | |
| 249 | 283 | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |
| @@ -244,6 +244,40 @@ func (p *x11Peer) createNotify(win uint32) { | |||||
| 244 | }) | 244 | }) | ||
| 245 | } | 245 | } | ||
| 246 | 246 | ||||
| 247 | + | // createOverride announces an override-redirect window of 100x50 at x,y. | |||
| 248 | + | func (p *x11Peer) createOverride(win uint32, x, y int16) { | |||
| 249 | + | p.event(16, func(b *[32]byte) { | |||
| 250 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 251 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 252 | + | binary.LittleEndian.PutUint16(b[12:], uint16(x)) | |||
| 253 | + | binary.LittleEndian.PutUint16(b[14:], uint16(y)) | |||
| 254 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |||
| 255 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |||
| 256 | + | b[22] = 1 | |||
| 257 | + | }) | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | func (p *x11Peer) mapNotify(win uint32) { | |||
| 261 | + | p.event(19, func(b *[32]byte) { | |||
| 262 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 263 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 264 | + | b[12] = 1 | |||
| 265 | + | }) | |||
| 266 | + | } | |||
| 267 | + | ||||
| 268 | + | // configureNotify moves and resizes a window (root SubstructureNotify form). | |||
| 269 | + | func (p *x11Peer) configureNotify(win uint32, x, y int16, w, h uint16) { | |||
| 270 | + | p.event(22, func(b *[32]byte) { | |||
| 271 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 272 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 273 | + | binary.LittleEndian.PutUint16(b[16:], uint16(x)) | |||
| 274 | + | binary.LittleEndian.PutUint16(b[18:], uint16(y)) | |||
| 275 | + | binary.LittleEndian.PutUint16(b[20:], w) | |||
| 276 | + | binary.LittleEndian.PutUint16(b[22:], h) | |||
| 277 | + | b[24] = 1 | |||
| 278 | + | }) | |||
| 279 | + | } | |||
| 280 | + | ||||
| 247 | func (p *x11Peer) windowEvent(code uint8, win uint32) { | 281 | func (p *x11Peer) windowEvent(code uint8, win uint32) { | ||
| 248 | p.event(code, func(b *[32]byte) { | 282 | p.event(code, func(b *[32]byte) { | ||
| 249 | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | 283 | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | ||
M internal/adapters/wayland/x11window.go +120 -5
| @@ -18,6 +18,12 @@ type x11Window struct { | |||
| 18 | 18 | info xwm.WindowInfo | |
| 19 | 19 | // ready is set once the XWM read the window's properties (Ready). | |
| 20 | 20 | ready bool | |
| 21 | + | // override marks an override-redirect window: tracked and drawn, never a | |
| 22 | + | // toplevel nor keyboard focus. rect is its layout rect, noInput keeps the | |
| 23 | + | // pointer off it. | |
| 24 | + | override bool | |
| 25 | + | noInput bool | |
| 26 | + | rect ports.Rect | |
| 21 | 27 | } | |
| 22 | 28 | ||
| 23 | 29 | // xwaylandRestricted are the globals only the Xwayland client may see. | |
| @@ -103,6 +109,7 @@ func (s *Server) installXWM(x *xwm.XWM) { | |||
| 103 | 109 | x.SetSerial(surf.identity, surf.x11Serial) | |
| 104 | 110 | } | |
| 105 | 111 | } | |
| 112 | + | x.SetLayout(s.outputPlaces) | |
| 106 | 113 | } | |
| 107 | 114 | ||
| 108 | 115 | // teardownXWM closes the window manager, dropping its windows. Idempotent. | |
| @@ -312,12 +319,120 @@ func (x *x11Window) tryMap() { | |||
| 312 | 319 | } | |
| 313 | 320 | } | |
| 314 | 321 | ||
| 315 | - | // OverrideMapped is a no-op until X7.5 wires override-redirect windows. | |
| 322 | + | // OverrideMapped starts tracking an override-redirect window. It is announced | |
| 323 | + | // to core on the paired surface's first buffer commit, so it is never drawn | |
| 324 | + | // without content. | |
| 316 | 325 | func (h hostImpl) OverrideMapped(slot xwm.Slot, xid uint32, surfaceID uint64, x, y, w, h2 int, noInput bool) { | |
| 326 | + | s := h.s | |
| 327 | + | surf := s.surfaceByIdentity[surfaceID] | |
| 328 | + | if surf == nil || surf.kind != roleX11 || surf.x11win != nil { | |
| 329 | + | return | |
| 330 | + | } | |
| 331 | + | if old := s.x11Slots[slot]; old != nil { | |
| 332 | + | h.OverrideGone(slot, true) | |
| 333 | + | } | |
| 334 | + | win := &window{id: s.nextWindow} | |
| 335 | + | win.x11 = &x11Window{server: s, slot: slot, xid: xid, override: true, noInput: noInput, rect: ports.Rect{X: x, Y: y, W: w, H: h2}} | |
| 336 | + | s.nextWindow++ | |
| 337 | + | s.windows[win.id] = win | |
| 338 | + | s.x11Slots[slot] = win | |
| 339 | + | ox := win.x11 | |
| 340 | + | ox.surface, surf.x11win = surf, win | |
| 341 | + | surf.role = func(buffer bool) { | |
| 342 | + | switch { | |
| 343 | + | case surf.destroyed: | |
| 344 | + | ox.hide() | |
| 345 | + | ox.surface = nil | |
| 346 | + | surf.x11win = nil | |
| 347 | + | case buffer: | |
| 348 | + | ox.show() | |
| 349 | + | case win.mapped: | |
| 350 | + | ox.hide() | |
| 351 | + | } | |
| 352 | + | } | |
| 353 | + | ox.show() | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | // OverrideMoved records the new layout rect of an override window and, once | |
| 357 | + | // shown, tells core. An unchanged rect costs nothing. | |
| 358 | + | func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) { | |
| 359 | + | win := h.s.x11Slots[slot] | |
| 360 | + | if win == nil || win.x11 == nil || !win.x11.override { | |
| 361 | + | return | |
| 362 | + | } | |
| 363 | + | r := ports.Rect{X: x, Y: y, W: w, H: h2} | |
| 364 | + | if r == win.x11.rect { | |
| 365 | + | return | |
| 366 | + | } | |
| 367 | + | win.x11.rect = r | |
| 368 | + | if win.mapped { | |
| 369 | + | h.s.emit(ports.UnmanagedMoved{ID: win.id, X: x, Y: y, Width: w, Height: h2}) | |
| 370 | + | win.x11.surface.sendTreeScale() | |
| 371 | + | } | |
| 372 | + | } | |
| 373 | + | ||
| 374 | + | // OverrideGone ends an override window: unmapped, or destroyed. | |
| 375 | + | func (h hostImpl) OverrideGone(slot xwm.Slot, destroyed bool) { | |
| 376 | + | s := h.s | |
| 377 | + | win := s.x11Slots[slot] | |
| 378 | + | if win == nil || win.x11 == nil || !win.x11.override { | |
| 379 | + | return | |
| 380 | + | } | |
| 381 | + | win.x11.hide() | |
| 382 | + | win.x11.drop() | |
| 383 | + | delete(s.windows, win.id) | |
| 384 | + | delete(s.x11Slots, slot) | |
| 385 | + | } | |
| 386 | + | ||
| 387 | + | // show announces the window to core once its surface has content. | |
| 388 | + | func (x *x11Window) show() { | |
| 389 | + | surf := x.surface | |
| 390 | + | if surf == nil || surf.x11win == nil || surf.destroyed || surf.current == nil { | |
| 391 | + | return | |
| 392 | + | } | |
| 393 | + | w := surf.x11win | |
| 394 | + | if w.mapped { | |
| 395 | + | return | |
| 396 | + | } | |
| 397 | + | s := x.server | |
| 398 | + | w.mapped = true | |
| 399 | + | r := x.rect | |
| 400 | + | s.emit(ports.UnmanagedMapped{ID: w.id, X: r.X, Y: r.Y, Width: r.W, Height: r.H, NoInput: x.noInput}) | |
| 401 | + | surf.sendTreeScale() | |
| 402 | + | if surf.has { | |
| 403 | + | surf.resetInputEmission() | |
| 404 | + | surf.redraw() | |
| 405 | + | surf.emitInput() | |
| 406 | + | } | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | // hide withdraws a shown override window from core and the seat. | |
| 410 | + | func (x *x11Window) hide() { | |
| 411 | + | if x.surface == nil || x.surface.x11win == nil { | |
| 412 | + | return | |
| 413 | + | } | |
| 414 | + | w := x.surface.x11win | |
| 415 | + | s := x.server | |
| 416 | + | if s.seat.pointerFocus == w.id { | |
| 417 | + | s.changePointerFocus(0, 0, 0) | |
| 418 | + | } | |
| 419 | + | if !w.mapped { | |
| 420 | + | return | |
| 421 | + | } | |
| 422 | + | w.mapped = false | |
| 423 | + | x.surface.resetInputEmission() | |
| 424 | + | s.emit(ports.UnmanagedUnmapped{ID: w.id}) | |
| 425 | + | s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |
| 317 | 426 | } | |
| 318 | 427 | ||
| 319 | - | // OverrideMoved is a no-op until X7.5 wires override-redirect windows. | |
| 320 | - | func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) {} | |
| 428 | + | // isOverride reports whether w is an override-redirect X11 window. | |
| 429 | + | func (w *window) isOverride() bool { return w.x11 != nil && w.x11.override } | |
| 321 | 430 | ||
| 322 | - | // OverrideGone is a no-op until X7.5 wires override-redirect windows. | |
| 323 | - | func (h hostImpl) OverrideGone(slot xwm.Slot, destroyed bool) {} | |
| 431 | + | // overrideOutput is the name of the output under the origin of an override | |
| 432 | + | // window's rect, "" when none. | |
| 433 | + | func (s *Server) overrideOutput(w *window) string { | |
| 434 | + | if o, ok := s.outputPlaces.At(float64(w.x11.rect.X), float64(w.x11.rect.Y)); ok { | |
| 435 | + | return o.Info.Name | |
| 436 | + | } | |
| 437 | + | return "" | |
| 438 | + | } | |
| @@ -18,6 +18,12 @@ type x11Window struct { | |||||
| 18 | info xwm.WindowInfo | 18 | info xwm.WindowInfo | ||
| 19 | // ready is set once the XWM read the window's properties (Ready). | 19 | // ready is set once the XWM read the window's properties (Ready). | ||
| 20 | ready bool | 20 | ready bool | ||
| 21 | + | // override marks an override-redirect window: tracked and drawn, never a | |||
| 22 | + | // toplevel nor keyboard focus. rect is its layout rect, noInput keeps the | |||
| 23 | + | // pointer off it. | |||
| 24 | + | override bool | |||
| 25 | + | noInput bool | |||
| 26 | + | rect ports.Rect | |||
| 21 | } | 27 | } | ||
| 22 | 28 | ||||
| 23 | // xwaylandRestricted are the globals only the Xwayland client may see. | 29 | // xwaylandRestricted are the globals only the Xwayland client may see. | ||
| @@ -103,6 +109,7 @@ func (s *Server) installXWM(x *xwm.XWM) { | |||||
| 103 | x.SetSerial(surf.identity, surf.x11Serial) | 109 | x.SetSerial(surf.identity, surf.x11Serial) | ||
| 104 | } | 110 | } | ||
| 105 | } | 111 | } | ||
| 112 | + | x.SetLayout(s.outputPlaces) | |||
| 106 | } | 113 | } | ||
| 107 | 114 | ||||
| 108 | // teardownXWM closes the window manager, dropping its windows. Idempotent. | 115 | // teardownXWM closes the window manager, dropping its windows. Idempotent. | ||
| @@ -312,12 +319,120 @@ func (x *x11Window) tryMap() { | |||||
| 312 | } | 319 | } | ||
| 313 | } | 320 | } | ||
| 314 | 321 | ||||
| 315 | - | // OverrideMapped is a no-op until X7.5 wires override-redirect windows. | 322 | + | // OverrideMapped starts tracking an override-redirect window. It is announced |
| 323 | + | // to core on the paired surface's first buffer commit, so it is never drawn | |||
| 324 | + | // without content. | |||
| 316 | func (h hostImpl) OverrideMapped(slot xwm.Slot, xid uint32, surfaceID uint64, x, y, w, h2 int, noInput bool) { | 325 | func (h hostImpl) OverrideMapped(slot xwm.Slot, xid uint32, surfaceID uint64, x, y, w, h2 int, noInput bool) { | ||
| 326 | + | s := h.s | |||
| 327 | + | surf := s.surfaceByIdentity[surfaceID] | |||
| 328 | + | if surf == nil || surf.kind != roleX11 || surf.x11win != nil { | |||
| 329 | + | return | |||
| 330 | + | } | |||
| 331 | + | if old := s.x11Slots[slot]; old != nil { | |||
| 332 | + | h.OverrideGone(slot, true) | |||
| 333 | + | } | |||
| 334 | + | win := &window{id: s.nextWindow} | |||
| 335 | + | win.x11 = &x11Window{server: s, slot: slot, xid: xid, override: true, noInput: noInput, rect: ports.Rect{X: x, Y: y, W: w, H: h2}} | |||
| 336 | + | s.nextWindow++ | |||
| 337 | + | s.windows[win.id] = win | |||
| 338 | + | s.x11Slots[slot] = win | |||
| 339 | + | ox := win.x11 | |||
| 340 | + | ox.surface, surf.x11win = surf, win | |||
| 341 | + | surf.role = func(buffer bool) { | |||
| 342 | + | switch { | |||
| 343 | + | case surf.destroyed: | |||
| 344 | + | ox.hide() | |||
| 345 | + | ox.surface = nil | |||
| 346 | + | surf.x11win = nil | |||
| 347 | + | case buffer: | |||
| 348 | + | ox.show() | |||
| 349 | + | case win.mapped: | |||
| 350 | + | ox.hide() | |||
| 351 | + | } | |||
| 352 | + | } | |||
| 353 | + | ox.show() | |||
| 354 | + | } | |||
| 355 | + | ||||
| 356 | + | // OverrideMoved records the new layout rect of an override window and, once | |||
| 357 | + | // shown, tells core. An unchanged rect costs nothing. | |||
| 358 | + | func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) { | |||
| 359 | + | win := h.s.x11Slots[slot] | |||
| 360 | + | if win == nil || win.x11 == nil || !win.x11.override { | |||
| 361 | + | return | |||
| 362 | + | } | |||
| 363 | + | r := ports.Rect{X: x, Y: y, W: w, H: h2} | |||
| 364 | + | if r == win.x11.rect { | |||
| 365 | + | return | |||
| 366 | + | } | |||
| 367 | + | win.x11.rect = r | |||
| 368 | + | if win.mapped { | |||
| 369 | + | h.s.emit(ports.UnmanagedMoved{ID: win.id, X: x, Y: y, Width: w, Height: h2}) | |||
| 370 | + | win.x11.surface.sendTreeScale() | |||
| 371 | + | } | |||
| 372 | + | } | |||
| 373 | + | ||||
| 374 | + | // OverrideGone ends an override window: unmapped, or destroyed. | |||
| 375 | + | func (h hostImpl) OverrideGone(slot xwm.Slot, destroyed bool) { | |||
| 376 | + | s := h.s | |||
| 377 | + | win := s.x11Slots[slot] | |||
| 378 | + | if win == nil || win.x11 == nil || !win.x11.override { | |||
| 379 | + | return | |||
| 380 | + | } | |||
| 381 | + | win.x11.hide() | |||
| 382 | + | win.x11.drop() | |||
| 383 | + | delete(s.windows, win.id) | |||
| 384 | + | delete(s.x11Slots, slot) | |||
| 385 | + | } | |||
| 386 | + | ||||
| 387 | + | // show announces the window to core once its surface has content. | |||
| 388 | + | func (x *x11Window) show() { | |||
| 389 | + | surf := x.surface | |||
| 390 | + | if surf == nil || surf.x11win == nil || surf.destroyed || surf.current == nil { | |||
| 391 | + | return | |||
| 392 | + | } | |||
| 393 | + | w := surf.x11win | |||
| 394 | + | if w.mapped { | |||
| 395 | + | return | |||
| 396 | + | } | |||
| 397 | + | s := x.server | |||
| 398 | + | w.mapped = true | |||
| 399 | + | r := x.rect | |||
| 400 | + | s.emit(ports.UnmanagedMapped{ID: w.id, X: r.X, Y: r.Y, Width: r.W, Height: r.H, NoInput: x.noInput}) | |||
| 401 | + | surf.sendTreeScale() | |||
| 402 | + | if surf.has { | |||
| 403 | + | surf.resetInputEmission() | |||
| 404 | + | surf.redraw() | |||
| 405 | + | surf.emitInput() | |||
| 406 | + | } | |||
| 407 | + | } | |||
| 408 | + | ||||
| 409 | + | // hide withdraws a shown override window from core and the seat. | |||
| 410 | + | func (x *x11Window) hide() { | |||
| 411 | + | if x.surface == nil || x.surface.x11win == nil { | |||
| 412 | + | return | |||
| 413 | + | } | |||
| 414 | + | w := x.surface.x11win | |||
| 415 | + | s := x.server | |||
| 416 | + | if s.seat.pointerFocus == w.id { | |||
| 417 | + | s.changePointerFocus(0, 0, 0) | |||
| 418 | + | } | |||
| 419 | + | if !w.mapped { | |||
| 420 | + | return | |||
| 421 | + | } | |||
| 422 | + | w.mapped = false | |||
| 423 | + | x.surface.resetInputEmission() | |||
| 424 | + | s.emit(ports.UnmanagedUnmapped{ID: w.id}) | |||
| 425 | + | s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |||
| 317 | } | 426 | } | ||
| 318 | 427 | ||||
| 319 | - | // OverrideMoved is a no-op until X7.5 wires override-redirect windows. | 428 | + | // isOverride reports whether w is an override-redirect X11 window. |
| 320 | - | func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) {} | 429 | + | func (w *window) isOverride() bool { return w.x11 != nil && w.x11.override } |
| 321 | 430 | ||||
| 322 | - | // OverrideGone is a no-op until X7.5 wires override-redirect windows. | 431 | + | // overrideOutput is the name of the output under the origin of an override |
| 323 | - | func (h hostImpl) OverrideGone(slot xwm.Slot, destroyed bool) {} | 432 | + | // window's rect, "" when none. |
| 433 | + | func (s *Server) overrideOutput(w *window) string { | |||
| 434 | + | if o, ok := s.outputPlaces.At(float64(w.x11.rect.X), float64(w.x11.rect.Y)); ok { | |||
| 435 | + | return o.Info.Name | |||
| 436 | + | } | |||
| 437 | + | return "" | |||
| 438 | + | } | |||