detached b3f6b3a

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +271 -8
M Makefile +1 -1
@@ -70,7 +70,7 @@ adapter-imports:
7070 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
7171 # Fast, mandatory guards for tiled content publications and SHM copies.
7272 perf-check:
73- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|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
7474 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1
7575 check: vet test arch fakes-check log-check perf-check
7676
@@ -70,7 +70,7 @@ adapter-imports:
70 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }70 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
71 # Fast, mandatory guards for tiled content publications and SHM copies.71 # Fast, mandatory guards for tiled content publications and SHM copies.
72 perf-check:72 perf-check:
73- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations)$$' -count=173+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations|TestOverrideMovedUnchangedAllocations)$$' -count=1
74 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=174 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1
75 check: vet test arch fakes-check log-check perf-check75 check: vet test arch fakes-check log-check perf-check
76 76
M internal/adapters/wayland/frames.go +2 -0
@@ -214,6 +214,8 @@ func (s *Server) suspended(surf *surface) bool {
214214 func (s *Server) frameOutput(surf *surface) string {
215215 surf = toplevelRoot(surf)
216216 switch {
217+ case surf.x11win != nil && surf.x11win.isOverride():
218+ return s.overrideOutput(surf.x11win)
217219 case surf.toplevelWindow() != nil:
218220 if s.suspended(surf) {
219221 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 suspendedFrameQueue221 return suspendedFrameQueue
M internal/adapters/wayland/outputs.go +3 -0
@@ -209,6 +209,9 @@ func (s *Server) setOutputs(c ports.SetOutputs) {
209209 changed := !slices.Equal(s.outputPlaces, c.Outputs)
210210 focusChanged := s.focusedOutput != c.Focused
211211 s.outputPlaces = append(ports.Layout(nil), c.Outputs...)
212+ if s.xwm != nil {
213+ s.xwm.SetLayout(s.outputPlaces)
214+ }
212215 defer func() {
213216 if changed {
214217 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.Focused210 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 {
2525 }
2626 name := ""
2727 switch {
28+ case surf.x11win != nil && surf.x11win.isOverride():
29+ name = s.overrideOutput(surf.x11win)
2830 case surf.toplevelWindow() != nil:
2931 name = surf.toplevelWindow().last.Output
3032 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.Output31 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
300300 // already dead. Admission is enforced before entering or delivering.
301301 return l.surface.wl, s.clientKeyboards(l.surface.wl.Client())
302302 }
303- if w := s.windows[id]; w != nil && w.mapped {
303+ if w := s.windows[id]; w != nil && w.mapped && !w.isOverride() {
304304 if surf := w.surfaceResource(); surf != nil {
305305 return surf, s.windowKeyboards(w)
306306 }
@@ -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 (
1111 // role.
1212
1313 // 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) }
1515
1616 // srv is the server the window belongs to.
1717 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) {
244244 })
245245 }
246246
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+
247281 func (p *x11Peer) windowEvent(code uint8, win uint32) {
248282 p.event(code, func(b *[32]byte) {
249283 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 {
1818 info xwm.WindowInfo
1919 // ready is set once the XWM read the window's properties (Ready).
2020 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
2127 }
2228
2329 // xwaylandRestricted are the globals only the Xwayland client may see.
@@ -103,6 +109,7 @@ func (s *Server) installXWM(x *xwm.XWM) {
103109 x.SetSerial(surf.identity, surf.x11Serial)
104110 }
105111 }
112+ x.SetLayout(s.outputPlaces)
106113 }
107114
108115 // teardownXWM closes the window manager, dropping its windows. Idempotent.
@@ -312,12 +319,120 @@ func (x *x11Window) tryMap() {
312319 }
313320 }
314321
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.
316325 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})
317426 }
318427
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 }
321430
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.WindowInfo18 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 bool20 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+}

Keyboard shortcuts

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

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

Branches 25

Tags 10

↑↓ moveenter openesc close