detached e0b15ca

refactor: deepen core, wayland and drm modules (#57)

* refactor(core): keep per-window state in one registry

Mapped client info, map time, input region and inhibit flags live in one
windowRegistry record; unmapping drops it in one call instead of clearing
five maps by hand.

* refactor(core): decide window configures in one module

configures owns the last configure sent per window and turns a placement
into the next one (hidden, preview or sized), so publish no longer repeats
three diff-and-record branches. Table tests cover each case.

* refactor(core): own keyboard focus rules in one module

keyboard holds the clicked on-demand layer, the focus last sent and the
active shortcuts inhibitor, and applies the focus order (exclusive layer,
grabbing popup, clicked layer, window). Core feeds it candidates; tests
cover the order, layer loss and inhibitor switches directly.

* refactor(wayland): hold requested surface state in one pendingCommit

The double-buffered wl_surface requests live in surface.next and a
queued update embeds the same pendingCommit, so a commit captures them in
one copy and resets one-shot parts by keeping only the sticky ones. A test
pins which fields carry over.

* refactor(drm): submit a frame as one transaction

submitFrame owns scanout, the refused-scanout and refused-overlay fallbacks,
capture writes and the composed commit; Output.Run only reacts to its
result. Tests drive scanout, refusals and render failure through it
without the event loop.

* refactor(wayland): group seat state and input delivery

Keyboard and pointer focus, bound devices, keymap, wheel accumulation and
the last press move from Server into seatState. Input commands are applied
by applyInput in seat.go, beside the focus and pointer helpers, leaving
Server.apply with window, output and session commands.
commit
e0b15ca70a72c1279857f8c292a48cfc777ad024
author
bnema · · 2026-09-29 04:19 UTC
committer
GitHub · · 2026-09-29 04:19 UTC
parent
56de283
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
50 files · +1540 -941
A internal/adapters/drm/frame_submit.go +85 -0
@@ -0,0 +1,85 @@
1+package drm
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/neferwl/internal/adapters/capture"
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// renderError is a frame the renderer failed to draw: the output stops.
11+type renderError struct{ err error }
12+
13+func (e renderError) Error() string { return "render frame: " + e.err.Error() }
14+func (e renderError) Unwrap() error { return e.err }
15+
16+// submitFrame puts one frame of the scene on screen: the fullscreen window
17+// scanned out directly when the kernel takes it, else a composed frame with
18+// an optional overlay plane, dropped when the kernel refuses it. Captures
19+// force composition and are answered from the composed frame (they are
20+// consumed either way). direct reports a scanned-out frame.
21+//
22+// On success the frame is in flight; errOverlayDropped asks for a new frame
23+// without the overlay; a renderError stops the output; other errors are
24+// commit errors for commitFailed.
25+func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, seen map[ports.WindowID]uint64, requests []ports.CaptureRequest, captured chan<- ports.CaptureDone) (direct bool, err error) {
26+ // shows is what the frame puts on screen: presentation feedback of
27+ // windows it does not draw is discarded, not presented.
28+ f := pendingFrame{shows: o.shownBy(scene, seen)}
29+ decision := o.decideFrame(scene, surfaces, len(requests) > 0)
30+ if decision.fb != 0 {
31+ c := decision.content
32+ if direct, err = o.commitScanout(decision.fb, c, pendingFrame{shows: o.directShownBy(c.ID, seen[c.ID])}); direct {
33+ return true, err
34+ }
35+ decision = o.composeFrame(scene, surfaces)
36+ }
37+ ov, composed := decision.overlay, decision.composed
38+ r.UseTarget(o.back)
39+ done, rerr := r.Render(composed, surfaces)
40+ if rerr != nil {
41+ ov.close()
42+ return false, renderError{rerr}
43+ }
44+ if ov.fb != 0 && !o.testOverlay(o.fbs[o.back], ov) {
45+ // Refused: compose the window too (cached per buffer).
46+ if done != nil {
47+ done.Close()
48+ }
49+ ov.close()
50+ ov = overlayWin{}
51+ if done, rerr = r.Render(scene, surfaces); rerr != nil {
52+ return false, renderError{rerr}
53+ }
54+ }
55+ if len(requests) > 0 {
56+ // Scanout and the overlay plane were skipped for this frame.
57+ o.log.Debug().Str("connector", o.conn.name).Int("captures", len(requests)).Msg("capture frame composed")
58+ }
59+ for _, q := range requests {
60+ capture.Write(ctx, q, r, captured)
61+ }
62+ // The overlay buffer is on screen like a scanned-out one: it is
63+ // reported shown, so it is not released under the plane.
64+ f.queued, f.zeroCopy, f.composed = ov.buf, ov.id, true
65+ // A covering fullscreen window keeps VRR while composed (overlay
66+ // layer, subsurfaces, size mismatch).
67+ game := o.vrrProp != 0 && fullscreenShown(&scene)
68+ err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(game), f, ov)
69+ if err != nil {
70+ // The GPU may still read client buffers for this frame.
71+ o.holdRead(done)
72+ }
73+ if done != nil {
74+ done.Close()
75+ }
76+ ov.close()
77+ if err != nil && o.overlayConflict(err, ov.buf) {
78+ return false, errOverlayDropped
79+ }
80+ if err == nil {
81+ o.queued = ov.buf
82+ o.back = 1 - o.back
83+ }
84+ return false, err
85+}
@@ -0,0 +1,85 @@
1+package drm
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/neferwl/internal/adapters/capture"
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// renderError is a frame the renderer failed to draw: the output stops.
11+type renderError struct{ err error }
12+
13+func (e renderError) Error() string { return "render frame: " + e.err.Error() }
14+func (e renderError) Unwrap() error { return e.err }
15+
16+// submitFrame puts one frame of the scene on screen: the fullscreen window
17+// scanned out directly when the kernel takes it, else a composed frame with
18+// an optional overlay plane, dropped when the kernel refuses it. Captures
19+// force composition and are answered from the composed frame (they are
20+// consumed either way). direct reports a scanned-out frame.
21+//
22+// On success the frame is in flight; errOverlayDropped asks for a new frame
23+// without the overlay; a renderError stops the output; other errors are
24+// commit errors for commitFailed.
25+func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, seen map[ports.WindowID]uint64, requests []ports.CaptureRequest, captured chan<- ports.CaptureDone) (direct bool, err error) {
26+ // shows is what the frame puts on screen: presentation feedback of
27+ // windows it does not draw is discarded, not presented.
28+ f := pendingFrame{shows: o.shownBy(scene, seen)}
29+ decision := o.decideFrame(scene, surfaces, len(requests) > 0)
30+ if decision.fb != 0 {
31+ c := decision.content
32+ if direct, err = o.commitScanout(decision.fb, c, pendingFrame{shows: o.directShownBy(c.ID, seen[c.ID])}); direct {
33+ return true, err
34+ }
35+ decision = o.composeFrame(scene, surfaces)
36+ }
37+ ov, composed := decision.overlay, decision.composed
38+ r.UseTarget(o.back)
39+ done, rerr := r.Render(composed, surfaces)
40+ if rerr != nil {
41+ ov.close()
42+ return false, renderError{rerr}
43+ }
44+ if ov.fb != 0 && !o.testOverlay(o.fbs[o.back], ov) {
45+ // Refused: compose the window too (cached per buffer).
46+ if done != nil {
47+ done.Close()
48+ }
49+ ov.close()
50+ ov = overlayWin{}
51+ if done, rerr = r.Render(scene, surfaces); rerr != nil {
52+ return false, renderError{rerr}
53+ }
54+ }
55+ if len(requests) > 0 {
56+ // Scanout and the overlay plane were skipped for this frame.
57+ o.log.Debug().Str("connector", o.conn.name).Int("captures", len(requests)).Msg("capture frame composed")
58+ }
59+ for _, q := range requests {
60+ capture.Write(ctx, q, r, captured)
61+ }
62+ // The overlay buffer is on screen like a scanned-out one: it is
63+ // reported shown, so it is not released under the plane.
64+ f.queued, f.zeroCopy, f.composed = ov.buf, ov.id, true
65+ // A covering fullscreen window keeps VRR while composed (overlay
66+ // layer, subsurfaces, size mismatch).
67+ game := o.vrrProp != 0 && fullscreenShown(&scene)
68+ err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(game), f, ov)
69+ if err != nil {
70+ // The GPU may still read client buffers for this frame.
71+ o.holdRead(done)
72+ }
73+ if done != nil {
74+ done.Close()
75+ }
76+ ov.close()
77+ if err != nil && o.overlayConflict(err, ov.buf) {
78+ return false, errOverlayDropped
79+ }
80+ if err == nil {
81+ o.queued = ov.buf
82+ o.back = 1 - o.back
83+ }
84+ return false, err
85+}
A internal/adapters/drm/frame_submit_test.go +127 -0
@@ -0,0 +1,127 @@
1+package drm
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "testing"
8+
9+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10+ "github.com/bnema/neferwl/internal/ports"
11+ "github.com/stretchr/testify/mock"
12+ "golang.org/x/sys/unix"
13+)
14+
15+// A fullscreen dmabuf the kernel takes is scanned out: nothing renders.
16+func TestSubmitFrameScanout(t *testing.T) {
17+ o, k, commits := testOutput(t)
18+ o.cursor = nil
19+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
20+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
21+ s, c := fullscreenScene()
22+ r := portsmocks.NewMockRenderer(t)
23+ direct, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
24+ if err != nil || !direct {
25+ t.Fatalf("direct %v err %v", direct, err)
26+ }
27+ if v, _ := (*commits)[len(*commits)-1].req.value(tPrimary, pFB); v != 88 {
28+ t.Fatalf("primary fb %d", v)
29+ }
30+ if o.queued != 9 {
31+ t.Fatalf("queued %d", o.queued)
32+ }
33+}
34+
35+// A scanout flip the kernel refuses composes the same frame instead.
36+func TestSubmitFrameScanoutRefusedComposes(t *testing.T) {
37+ o, k, commits := testOutput(t, unix.EINVAL)
38+ o.cursor, o.tearing, o.vrrProp = nil, false, 0
39+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
40+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
41+ s, c := fullscreenScene()
42+ r := portsmocks.NewMockRenderer(t)
43+ r.EXPECT().UseTarget(0).Return().Once()
44+ r.EXPECT().Render(s, c).Return(nil, nil).Once()
45+ direct, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
46+ if err != nil || direct {
47+ t.Fatalf("direct %v err %v", direct, err)
48+ }
49+ if v, _ := (*commits)[len(*commits)-1].req.value(tPrimary, pFB); v != 70 || o.back != 1 {
50+ t.Fatalf("primary fb %d back %d", v, o.back)
51+ }
52+ if o.reason != "flip_refused" {
53+ t.Fatalf("reason %q", o.reason)
54+ }
55+}
56+
57+// An overlay the kernel refuses in its test commit is composed too.
58+func TestSubmitFrameOverlayRefusedComposesAll(t *testing.T) {
59+ o, k, commits := overlayOutput(t, unix.EINVAL)
60+ o.cursor = nil
61+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
62+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
63+ s, c := overlayScene()
64+ r := portsmocks.NewMockRenderer(t)
65+ r.EXPECT().UseTarget(0).Return().Once()
66+ var drawn []int
67+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
68+ drawn = append(drawn, len(s.Windows))
69+ return nil, nil
70+ }).Twice()
71+ direct, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
72+ if err != nil || direct {
73+ t.Fatalf("direct %v err %v", direct, err)
74+ }
75+ // First without the overlay window, then the whole scene.
76+ if len(drawn) != 2 || drawn[0] != 1 || drawn[1] != 2 {
77+ t.Fatalf("rendered windows %v", drawn)
78+ }
79+ if v, ok := (*commits)[len(*commits)-1].req.value(tOverlay, pFB); ok && v != 0 {
80+ t.Fatalf("overlay committed: fb %d", v)
81+ }
82+}
83+
84+// An overlay commit the kernel refuses after its test passed drops the
85+// overlay: the caller asks for a new, fully composed frame.
86+func TestSubmitFrameOverlayCommitRefusedDropsOverlay(t *testing.T) {
87+ o, k, _ := overlayOutput(t, nil, unix.EINVAL)
88+ o.cursor, o.vrrProp = nil, 0
89+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
90+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
91+ s, c := overlayScene()
92+ r := portsmocks.NewMockRenderer(t)
93+ r.EXPECT().UseTarget(0).Return().Once()
94+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
95+ if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); !errors.Is(err, errOverlayDropped) {
96+ t.Fatalf("err %v", err)
97+ }
98+ if o.back != 0 || o.clientFBs[9].overlayFailed != "cursor_conflict" {
99+ t.Fatalf("back %d reason %q", o.back, o.clientFBs[9].overlayFailed)
100+ }
101+}
102+
103+// A render failure stops the output and is not a commit error.
104+func TestSubmitFrameRenderError(t *testing.T) {
105+ o, _, commits := testOutput(t)
106+ boom := errors.New("boom")
107+ r := portsmocks.NewMockRenderer(t)
108+ r.EXPECT().UseTarget(0).Return().Once()
109+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, boom).Once()
110+ _, err := o.submitFrame(context.Background(), r, ports.Scene{Seq: 1}, nil, map[ports.WindowID]uint64{}, nil, nil)
111+ var fatal renderError
112+ if !errors.As(err, &fatal) || !errors.Is(err, boom) {
113+ t.Fatalf("err %v", err)
114+ }
115+ if len(*commits) != 0 || o.back != 0 {
116+ t.Fatalf("committed %d back %d", len(*commits), o.back)
117+ }
118+}
119+
120+func fullscreenScene() (ports.Scene, map[ports.WindowID]ports.SurfaceContent) {
121+ s := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{
122+ {ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true},
123+ }}
124+ return s, map[ports.WindowID]ports.SurfaceContent{
125+ 1: {ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 9, Width: 200, Height: 100, Format: fourccXRGB}},
126+ }
127+}
@@ -0,0 +1,127 @@
1+package drm
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "testing"
8+
9+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10+ "github.com/bnema/neferwl/internal/ports"
11+ "github.com/stretchr/testify/mock"
12+ "golang.org/x/sys/unix"
13+)
14+
15+// A fullscreen dmabuf the kernel takes is scanned out: nothing renders.
16+func TestSubmitFrameScanout(t *testing.T) {
17+ o, k, commits := testOutput(t)
18+ o.cursor = nil
19+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
20+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
21+ s, c := fullscreenScene()
22+ r := portsmocks.NewMockRenderer(t)
23+ direct, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
24+ if err != nil || !direct {
25+ t.Fatalf("direct %v err %v", direct, err)
26+ }
27+ if v, _ := (*commits)[len(*commits)-1].req.value(tPrimary, pFB); v != 88 {
28+ t.Fatalf("primary fb %d", v)
29+ }
30+ if o.queued != 9 {
31+ t.Fatalf("queued %d", o.queued)
32+ }
33+}
34+
35+// A scanout flip the kernel refuses composes the same frame instead.
36+func TestSubmitFrameScanoutRefusedComposes(t *testing.T) {
37+ o, k, commits := testOutput(t, unix.EINVAL)
38+ o.cursor, o.tearing, o.vrrProp = nil, false, 0
39+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
40+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
41+ s, c := fullscreenScene()
42+ r := portsmocks.NewMockRenderer(t)
43+ r.EXPECT().UseTarget(0).Return().Once()
44+ r.EXPECT().Render(s, c).Return(nil, nil).Once()
45+ direct, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
46+ if err != nil || direct {
47+ t.Fatalf("direct %v err %v", direct, err)
48+ }
49+ if v, _ := (*commits)[len(*commits)-1].req.value(tPrimary, pFB); v != 70 || o.back != 1 {
50+ t.Fatalf("primary fb %d back %d", v, o.back)
51+ }
52+ if o.reason != "flip_refused" {
53+ t.Fatalf("reason %q", o.reason)
54+ }
55+}
56+
57+// An overlay the kernel refuses in its test commit is composed too.
58+func TestSubmitFrameOverlayRefusedComposesAll(t *testing.T) {
59+ o, k, commits := overlayOutput(t, unix.EINVAL)
60+ o.cursor = nil
61+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
62+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
63+ s, c := overlayScene()
64+ r := portsmocks.NewMockRenderer(t)
65+ r.EXPECT().UseTarget(0).Return().Once()
66+ var drawn []int
67+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
68+ drawn = append(drawn, len(s.Windows))
69+ return nil, nil
70+ }).Twice()
71+ direct, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil)
72+ if err != nil || direct {
73+ t.Fatalf("direct %v err %v", direct, err)
74+ }
75+ // First without the overlay window, then the whole scene.
76+ if len(drawn) != 2 || drawn[0] != 1 || drawn[1] != 2 {
77+ t.Fatalf("rendered windows %v", drawn)
78+ }
79+ if v, ok := (*commits)[len(*commits)-1].req.value(tOverlay, pFB); ok && v != 0 {
80+ t.Fatalf("overlay committed: fb %d", v)
81+ }
82+}
83+
84+// An overlay commit the kernel refuses after its test passed drops the
85+// overlay: the caller asks for a new, fully composed frame.
86+func TestSubmitFrameOverlayCommitRefusedDropsOverlay(t *testing.T) {
87+ o, k, _ := overlayOutput(t, nil, unix.EINVAL)
88+ o.cursor, o.vrrProp = nil, 0
89+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
90+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
91+ s, c := overlayScene()
92+ r := portsmocks.NewMockRenderer(t)
93+ r.EXPECT().UseTarget(0).Return().Once()
94+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
95+ if _, err := o.submitFrame(context.Background(), r, s, c, map[ports.WindowID]uint64{}, nil, nil); !errors.Is(err, errOverlayDropped) {
96+ t.Fatalf("err %v", err)
97+ }
98+ if o.back != 0 || o.clientFBs[9].overlayFailed != "cursor_conflict" {
99+ t.Fatalf("back %d reason %q", o.back, o.clientFBs[9].overlayFailed)
100+ }
101+}
102+
103+// A render failure stops the output and is not a commit error.
104+func TestSubmitFrameRenderError(t *testing.T) {
105+ o, _, commits := testOutput(t)
106+ boom := errors.New("boom")
107+ r := portsmocks.NewMockRenderer(t)
108+ r.EXPECT().UseTarget(0).Return().Once()
109+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, boom).Once()
110+ _, err := o.submitFrame(context.Background(), r, ports.Scene{Seq: 1}, nil, map[ports.WindowID]uint64{}, nil, nil)
111+ var fatal renderError
112+ if !errors.As(err, &fatal) || !errors.Is(err, boom) {
113+ t.Fatalf("err %v", err)
114+ }
115+ if len(*commits) != 0 || o.back != 0 {
116+ t.Fatalf("committed %d back %d", len(*commits), o.back)
117+ }
118+}
119+
120+func fullscreenScene() (ports.Scene, map[ports.WindowID]ports.SurfaceContent) {
121+ s := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{
122+ {ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true},
123+ }}
124+ return s, map[ports.WindowID]ports.SurfaceContent{
125+ 1: {ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 9, Width: 200, Height: 100, Format: fourccXRGB}},
126+ }
127+}
M internal/adapters/drm/output.go +5 -62
@@ -1091,68 +1091,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
10911091 continue
10921092 }
10931093 start := time.Now()
1094- // shows is what the frame puts on screen: presentation feedback
1095- // of windows it does not draw is discarded, not presented.
1096- f := pendingFrame{shows: o.shownBy(scene, seen)}
1097- decision := o.decideFrame(scene, surfaces, len(requests) > 0)
1098- direct := false
1099- if decision.fb != 0 {
1100- c := decision.content
1101- direct, err = o.commitScanout(decision.fb, c, pendingFrame{shows: o.directShownBy(c.ID, seen[c.ID])})
1102- }
1103- if !direct {
1104- ov, composed := decision.overlay, decision.composed
1105- if decision.fb != 0 {
1106- decision = o.composeFrame(scene, surfaces)
1107- ov, composed = decision.overlay, decision.composed
1108- }
1109- r.UseTarget(o.back)
1110- done, rerr := r.Render(composed, surfaces)
1111- if rerr != nil {
1112- ov.close()
1113- return fmt.Errorf("render frame: %w", rerr)
1114- }
1115- if ov.fb != 0 && !o.testOverlay(o.fbs[o.back], ov) {
1116- // Refused: compose the window too (cached per buffer).
1117- if done != nil {
1118- done.Close()
1119- }
1120- ov.close()
1121- ov = overlayWin{}
1122- if done, rerr = r.Render(scene, surfaces); rerr != nil {
1123- return fmt.Errorf("render frame: %w", rerr)
1124- }
1125- }
1126- if len(requests) > 0 {
1127- // Scanout and the overlay plane were skipped for this frame.
1128- o.log.Debug().Str("connector", o.conn.name).Int("captures", len(requests)).Msg("capture frame composed")
1129- }
1130- for _, q := range requests {
1131- capture.Write(ctx, q, r, captured)
1132- }
1133- requests = nil
1134- // The overlay buffer is on screen like a scanned-out one: it
1135- // is reported shown, so it is not released under the plane.
1136- f.queued, f.zeroCopy, f.composed = ov.buf, ov.id, true
1137- // A covering fullscreen window keeps VRR while composed
1138- // (overlay layer, subsurfaces, size mismatch).
1139- game := o.vrrProp != 0 && fullscreenShown(&scene)
1140- err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(game), f, ov)
1141- if err != nil {
1142- // The GPU may still read client buffers for this frame.
1143- o.holdRead(done)
1144- }
1145- if done != nil {
1146- done.Close()
1147- }
1148- ov.close()
1149- if err != nil && o.overlayConflict(err, ov.buf) {
1150- err = errOverlayDropped
1151- }
1152- if err == nil {
1153- o.queued = ov.buf
1154- o.back = 1 - o.back
1155- }
1094+ direct, err := o.submitFrame(ctx, r, scene, surfaces, seen, requests, captured)
1095+ requests = nil
1096+ var fatal renderError
1097+ if errors.As(err, &fatal) {
1098+ return err
11561099 }
11571100 if err != nil {
11581101 if errors.Is(err, errOverlayDropped) {
@@ -1091,68 +1091,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1091 continue1091 continue
1092 }1092 }
1093 start := time.Now()1093 start := time.Now()
1094- // shows is what the frame puts on screen: presentation feedback1094+ direct, err := o.submitFrame(ctx, r, scene, surfaces, seen, requests, captured)
1095- // of windows it does not draw is discarded, not presented.1095+ requests = nil
1096- f := pendingFrame{shows: o.shownBy(scene, seen)}1096+ var fatal renderError
1097- decision := o.decideFrame(scene, surfaces, len(requests) > 0)1097+ if errors.As(err, &fatal) {
1098- direct := false1098+ return err
1099- if decision.fb != 0 {
1100- c := decision.content
1101- direct, err = o.commitScanout(decision.fb, c, pendingFrame{shows: o.directShownBy(c.ID, seen[c.ID])})
1102- }
1103- if !direct {
1104- ov, composed := decision.overlay, decision.composed
1105- if decision.fb != 0 {
1106- decision = o.composeFrame(scene, surfaces)
1107- ov, composed = decision.overlay, decision.composed
1108- }
1109- r.UseTarget(o.back)
1110- done, rerr := r.Render(composed, surfaces)
1111- if rerr != nil {
1112- ov.close()
1113- return fmt.Errorf("render frame: %w", rerr)
1114- }
1115- if ov.fb != 0 && !o.testOverlay(o.fbs[o.back], ov) {
1116- // Refused: compose the window too (cached per buffer).
1117- if done != nil {
1118- done.Close()
1119- }
1120- ov.close()
1121- ov = overlayWin{}
1122- if done, rerr = r.Render(scene, surfaces); rerr != nil {
1123- return fmt.Errorf("render frame: %w", rerr)
1124- }
1125- }
1126- if len(requests) > 0 {
1127- // Scanout and the overlay plane were skipped for this frame.
1128- o.log.Debug().Str("connector", o.conn.name).Int("captures", len(requests)).Msg("capture frame composed")
1129- }
1130- for _, q := range requests {
1131- capture.Write(ctx, q, r, captured)
1132- }
1133- requests = nil
1134- // The overlay buffer is on screen like a scanned-out one: it
1135- // is reported shown, so it is not released under the plane.
1136- f.queued, f.zeroCopy, f.composed = ov.buf, ov.id, true
1137- // A covering fullscreen window keeps VRR while composed
1138- // (overlay layer, subsurfaces, size mismatch).
1139- game := o.vrrProp != 0 && fullscreenShown(&scene)
1140- err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(game), f, ov)
1141- if err != nil {
1142- // The GPU may still read client buffers for this frame.
1143- o.holdRead(done)
1144- }
1145- if done != nil {
1146- done.Close()
1147- }
1148- ov.close()
1149- if err != nil && o.overlayConflict(err, ov.buf) {
1150- err = errOverlayDropped
1151- }
1152- if err == nil {
1153- o.queued = ov.buf
1154- o.back = 1 - o.back
1155- }
1156 }1099 }
1157 if err != nil {1100 if err != nil {
1158 if errors.Is(err, errOverlayDropped) {1101 if errors.Is(err, errOverlayDropped) {
M internal/adapters/wayland/activation.go +4 -4
@@ -102,8 +102,8 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) {
102102 t.used = true
103103 s := t.server
104104 c := r.Client()
105- fresh := time.Since(s.pressAt) <= tokenLifetime && !s.pressAt.Before(s.focusAt)
106- valid := s.focusClient() == c || (t.hasSerial && t.serial == s.press && s.pressClient == c && fresh)
105+ fresh := time.Since(s.seat.pressAt) <= tokenLifetime && !s.seat.pressAt.Before(s.seat.focusAt)
106+ valid := s.focusClient() == c || (t.hasSerial && t.serial == s.seat.press && s.seat.pressClient == c && fresh)
107107 token := rand.Text()
108108 if valid {
109109 s.pruneTokens()
@@ -114,10 +114,10 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) {
114114
115115 // focusClient is the client with the keyboard focus.
116116 func (s *Server) focusClient() server.Client {
117- if w := s.windows[s.focused]; w != nil && w.xdg.resource.Resource.Alive() {
117+ if w := s.windows[s.seat.focused]; w != nil && w.xdg.resource.Resource.Alive() {
118118 return w.xdg.resource.Client()
119119 }
120- if l := s.layers[s.focused]; l != nil && l.resource.Resource.Alive() {
120+ if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() {
121121 return l.resource.Client()
122122 }
123123 return server.Client{}
@@ -102,8 +102,8 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) {
102 t.used = true102 t.used = true
103 s := t.server103 s := t.server
104 c := r.Client()104 c := r.Client()
105- fresh := time.Since(s.pressAt) <= tokenLifetime && !s.pressAt.Before(s.focusAt)105+ fresh := time.Since(s.seat.pressAt) <= tokenLifetime && !s.seat.pressAt.Before(s.seat.focusAt)
106- valid := s.focusClient() == c || (t.hasSerial && t.serial == s.press && s.pressClient == c && fresh)106+ valid := s.focusClient() == c || (t.hasSerial && t.serial == s.seat.press && s.seat.pressClient == c && fresh)
107 token := rand.Text()107 token := rand.Text()
108 if valid {108 if valid {
109 s.pruneTokens()109 s.pruneTokens()
@@ -114,10 +114,10 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) {
114 114
115 // focusClient is the client with the keyboard focus.115 // focusClient is the client with the keyboard focus.
116 func (s *Server) focusClient() server.Client {116 func (s *Server) focusClient() server.Client {
117- if w := s.windows[s.focused]; w != nil && w.xdg.resource.Resource.Alive() {117+ if w := s.windows[s.seat.focused]; w != nil && w.xdg.resource.Resource.Alive() {
118 return w.xdg.resource.Client()118 return w.xdg.resource.Client()
119 }119 }
120- if l := s.layers[s.focused]; l != nil && l.resource.Resource.Alive() {120+ if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() {
121 return l.resource.Client()121 return l.resource.Client()
122 }122 }
123 return server.Client{}123 return server.Client{}
M internal/adapters/wayland/activation_test.go +2 -2
@@ -148,7 +148,7 @@ func TestActivationPressSerial(t *testing.T) {
148148 done := make(chan struct{})
149149 s.display.Do(func() {
150150 s.serial++
151- s.press, s.pressClient, s.pressAt = s.serial, s.windows[w.ID].xdg.resource.Client(), time.Now()
151+ s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, s.windows[w.ID].xdg.resource.Client(), time.Now()
152152 serial = s.serial
153153 close(done)
154154 })
@@ -189,7 +189,7 @@ func waitFocus(t *testing.T, s *Server, id ports.WindowID) {
189189 t.Helper()
190190 for deadline := time.Now().Add(time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) {
191191 done := make(chan ports.WindowID, 1)
192- s.display.Do(func() { done <- s.focused })
192+ s.display.Do(func() { done <- s.seat.focused })
193193 if <-done == id {
194194 return
195195 }
@@ -148,7 +148,7 @@ func TestActivationPressSerial(t *testing.T) {
148 done := make(chan struct{})148 done := make(chan struct{})
149 s.display.Do(func() {149 s.display.Do(func() {
150 s.serial++150 s.serial++
151- s.press, s.pressClient, s.pressAt = s.serial, s.windows[w.ID].xdg.resource.Client(), time.Now()151+ s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, s.windows[w.ID].xdg.resource.Client(), time.Now()
152 serial = s.serial152 serial = s.serial
153 close(done)153 close(done)
154 })154 })
@@ -189,7 +189,7 @@ func waitFocus(t *testing.T, s *Server, id ports.WindowID) {
189 t.Helper()189 t.Helper()
190 for deadline := time.Now().Add(time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) {190 for deadline := time.Now().Add(time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) {
191 done := make(chan ports.WindowID, 1)191 done := make(chan ports.WindowID, 1)
192- s.display.Do(func() { done <- s.focused })192+ s.display.Do(func() { done <- s.seat.focused })
193 if <-done == id {193 if <-done == id {
194 return194 return
195 }195 }
M internal/adapters/wayland/axis_test.go +6 -6
@@ -12,7 +12,7 @@ func TestWheelSteps(t *testing.T) {
1212 s := &Server{}
1313 var values [2]float64
1414 wheel := func(v, h int32) (steps [2]int32) {
15- steps, values = s.wheelSteps(ports.PointerAxis{
15+ steps, values = s.seat.wheelSteps(ports.PointerAxis{
1616 Vertical: ports.ScrollAxis{Set: v != 0, V120: v, Value: float64(v) / 8},
1717 Horizontal: ports.ScrollAxis{Set: h != 0, V120: h},
1818 })
@@ -37,18 +37,18 @@ func TestWheelSteps(t *testing.T) {
3737 // The held value sums the frames since the last step.
3838 *s = Server{}
3939 wheel(60, 0)
40- if s.wheelHeld[0] != 7.5 {
41- t.Fatal("held", s.wheelHeld[0])
40+ if s.seat.wheelHeld[0] != 7.5 {
41+ t.Fatal("held", s.seat.wheelHeld[0])
4242 }
43- if wheel(60, 0) != [2]int32{1, 0} || s.wheelHeld[0] != 0 || values[0] != 15 {
44- t.Fatal("held after step", s.wheelHeld[0])
43+ if wheel(60, 0) != [2]int32{1, 0} || s.seat.wheelHeld[0] != 0 || values[0] != 15 {
44+ t.Fatal("held after step", s.seat.wheelHeld[0])
4545 }
4646 // A direction change drops what was held the other way.
4747 wheel(60, 0)
4848 if wheel(-180, 0) != [2]int32{-1, 0} || values[0] != -22.5 {
4949 t.Fatal("after reversal", values)
5050 }
51- if steps, _ := s.wheelSteps(ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, V120: 240}}); steps != [2]int32{} {
51+ if steps, _ := s.seat.wheelSteps(ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, V120: 240}}); steps != [2]int32{} {
5252 t.Fatal("finger scroll made discrete steps")
5353 }
5454 }
@@ -12,7 +12,7 @@ func TestWheelSteps(t *testing.T) {
12 s := &Server{}12 s := &Server{}
13 var values [2]float6413 var values [2]float64
14 wheel := func(v, h int32) (steps [2]int32) {14 wheel := func(v, h int32) (steps [2]int32) {
15- steps, values = s.wheelSteps(ports.PointerAxis{15+ steps, values = s.seat.wheelSteps(ports.PointerAxis{
16 Vertical: ports.ScrollAxis{Set: v != 0, V120: v, Value: float64(v) / 8},16 Vertical: ports.ScrollAxis{Set: v != 0, V120: v, Value: float64(v) / 8},
17 Horizontal: ports.ScrollAxis{Set: h != 0, V120: h},17 Horizontal: ports.ScrollAxis{Set: h != 0, V120: h},
18 })18 })
@@ -37,18 +37,18 @@ func TestWheelSteps(t *testing.T) {
37 // The held value sums the frames since the last step.37 // The held value sums the frames since the last step.
38 *s = Server{}38 *s = Server{}
39 wheel(60, 0)39 wheel(60, 0)
40- if s.wheelHeld[0] != 7.5 {40+ if s.seat.wheelHeld[0] != 7.5 {
41- t.Fatal("held", s.wheelHeld[0])41+ t.Fatal("held", s.seat.wheelHeld[0])
42 }42 }
43- if wheel(60, 0) != [2]int32{1, 0} || s.wheelHeld[0] != 0 || values[0] != 15 {43+ if wheel(60, 0) != [2]int32{1, 0} || s.seat.wheelHeld[0] != 0 || values[0] != 15 {
44- t.Fatal("held after step", s.wheelHeld[0])44+ t.Fatal("held after step", s.seat.wheelHeld[0])
45 }45 }
46 // A direction change drops what was held the other way.46 // A direction change drops what was held the other way.
47 wheel(60, 0)47 wheel(60, 0)
48 if wheel(-180, 0) != [2]int32{-1, 0} || values[0] != -22.5 {48 if wheel(-180, 0) != [2]int32{-1, 0} || values[0] != -22.5 {
49 t.Fatal("after reversal", values)49 t.Fatal("after reversal", values)
50 }50 }
51- if steps, _ := s.wheelSteps(ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, V120: 240}}); steps != [2]int32{} {51+ if steps, _ := s.seat.wheelSteps(ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, V120: 240}}); steps != [2]int32{} {
52 t.Fatal("finger scroll made discrete steps")52 t.Fatal("finger scroll made discrete steps")
53 }53 }
54 }54 }
M internal/adapters/wayland/clipboard.go +1 -1
@@ -79,7 +79,7 @@ func registerClipboard(d *server.Display, s *Server) error {
7979
8080 // focusedClient is the client with keyboard focus.
8181 func (s *Server) focusedClient() (server.Client, bool) {
82- surf, _ := s.focusTarget(s.focused)
82+ surf, _ := s.focusTarget(s.seat.focused)
8383 if surf == nil {
8484 return server.Client{}, false
8585 }
@@ -79,7 +79,7 @@ func registerClipboard(d *server.Display, s *Server) error {
79 79
80 // focusedClient is the client with keyboard focus.80 // focusedClient is the client with keyboard focus.
81 func (s *Server) focusedClient() (server.Client, bool) {81 func (s *Server) focusedClient() (server.Client, bool) {
82- surf, _ := s.focusTarget(s.focused)82+ surf, _ := s.focusTarget(s.seat.focused)
83 if surf == nil {83 if surf == nil {
84 return server.Client{}, false84 return server.Client{}, false
85 }85 }
M internal/adapters/wayland/colormanagement.go +4 -4
@@ -110,7 +110,7 @@ func (m colorManager) GetSurface(r *cm.WpColorManagerV1, id uint32, wl *wayland.
110110 res.OnDestroy = func() {
111111 if surf.colorControl == h {
112112 surf.colorControl = nil
113- surf.pendingColor = SurfaceColor{}
113+ surf.next.color = SurfaceColor{}
114114 }
115115 }
116116 }
@@ -164,7 +164,7 @@ type colorSurface struct{ surf *surface }
164164
165165 func (h *colorSurface) Destroy(*cm.WpColorManagementSurfaceV1) {
166166 if h.surf != nil && !h.surf.destroyed {
167- h.surf.pendingColor = SurfaceColor{}
167+ h.surf.next.color = SurfaceColor{}
168168 }
169169 }
170170 func (h *colorSurface) SetImageDescription(r *cm.WpColorManagementSurfaceV1, desc *cm.WpImageDescriptionV1, intent uint32) {
@@ -185,7 +185,7 @@ func (h *colorSurface) SetImageDescription(r *cm.WpColorManagementSurfaceV1, des
185185 r.PostError(uint32(cm.WpColorManagementSurfaceV1ErrorImageDescription), "description not ready")
186186 return
187187 }
188- h.surf.pendingColor = d.SurfaceColor
188+ h.surf.next.color = d.SurfaceColor
189189 h.surf.server.log.Debug().Str("component", "wayland").Uint32("tf", d.TF).Uint32("primaries", d.Primaries).Msg("surface color description set")
190190 }
191191 func (h *colorSurface) UnsetImageDescription(r *cm.WpColorManagementSurfaceV1) {
@@ -193,7 +193,7 @@ func (h *colorSurface) UnsetImageDescription(r *cm.WpColorManagementSurfaceV1) {
193193 r.PostError(uint32(cm.WpColorManagementSurfaceV1ErrorInert), "surface destroyed")
194194 return
195195 }
196- h.surf.pendingColor = SurfaceColor{}
196+ h.surf.next.color = SurfaceColor{}
197197 h.surf.server.log.Debug().Str("component", "wayland").Msg("surface color description unset")
198198 }
199199
@@ -110,7 +110,7 @@ func (m colorManager) GetSurface(r *cm.WpColorManagerV1, id uint32, wl *wayland.
110 res.OnDestroy = func() {110 res.OnDestroy = func() {
111 if surf.colorControl == h {111 if surf.colorControl == h {
112 surf.colorControl = nil112 surf.colorControl = nil
113- surf.pendingColor = SurfaceColor{}113+ surf.next.color = SurfaceColor{}
114 }114 }
115 }115 }
116 }116 }
@@ -164,7 +164,7 @@ type colorSurface struct{ surf *surface }
164 164
165 func (h *colorSurface) Destroy(*cm.WpColorManagementSurfaceV1) {165 func (h *colorSurface) Destroy(*cm.WpColorManagementSurfaceV1) {
166 if h.surf != nil && !h.surf.destroyed {166 if h.surf != nil && !h.surf.destroyed {
167- h.surf.pendingColor = SurfaceColor{}167+ h.surf.next.color = SurfaceColor{}
168 }168 }
169 }169 }
170 func (h *colorSurface) SetImageDescription(r *cm.WpColorManagementSurfaceV1, desc *cm.WpImageDescriptionV1, intent uint32) {170 func (h *colorSurface) SetImageDescription(r *cm.WpColorManagementSurfaceV1, desc *cm.WpImageDescriptionV1, intent uint32) {
@@ -185,7 +185,7 @@ func (h *colorSurface) SetImageDescription(r *cm.WpColorManagementSurfaceV1, des
185 r.PostError(uint32(cm.WpColorManagementSurfaceV1ErrorImageDescription), "description not ready")185 r.PostError(uint32(cm.WpColorManagementSurfaceV1ErrorImageDescription), "description not ready")
186 return186 return
187 }187 }
188- h.surf.pendingColor = d.SurfaceColor188+ h.surf.next.color = d.SurfaceColor
189 h.surf.server.log.Debug().Str("component", "wayland").Uint32("tf", d.TF).Uint32("primaries", d.Primaries).Msg("surface color description set")189 h.surf.server.log.Debug().Str("component", "wayland").Uint32("tf", d.TF).Uint32("primaries", d.Primaries).Msg("surface color description set")
190 }190 }
191 func (h *colorSurface) UnsetImageDescription(r *cm.WpColorManagementSurfaceV1) {191 func (h *colorSurface) UnsetImageDescription(r *cm.WpColorManagementSurfaceV1) {
@@ -193,7 +193,7 @@ func (h *colorSurface) UnsetImageDescription(r *cm.WpColorManagementSurfaceV1) {
193 r.PostError(uint32(cm.WpColorManagementSurfaceV1ErrorInert), "surface destroyed")193 r.PostError(uint32(cm.WpColorManagementSurfaceV1ErrorInert), "surface destroyed")
194 return194 return
195 }195 }
196- h.surf.pendingColor = SurfaceColor{}196+ h.surf.next.color = SurfaceColor{}
197 h.surf.server.log.Debug().Str("component", "wayland").Msg("surface color description unset")197 h.surf.server.log.Debug().Str("component", "wayland").Msg("surface color description unset")
198 }198 }
199 199
M internal/adapters/wayland/colormanagement_errors_test.go +2 -1
@@ -1,10 +1,11 @@
11 package wayland
22
33 import (
4+ "testing"
5+
46 "github.com/bnema/neferwl/internal/ports"
57 cm "github.com/bnema/purego-libwayland/protocol/colormanagement"
68 "github.com/bnema/purego-libwayland/protocol/wayland"
7- "testing"
89 )
910
1011 func TestColorManagerErrors(t *testing.T) {
@@ -1,10 +1,11 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "testing"
5+
4 "github.com/bnema/neferwl/internal/ports"6 "github.com/bnema/neferwl/internal/ports"
5 cm "github.com/bnema/purego-libwayland/protocol/colormanagement"7 cm "github.com/bnema/purego-libwayland/protocol/colormanagement"
6 "github.com/bnema/purego-libwayland/protocol/wayland"8 "github.com/bnema/purego-libwayland/protocol/wayland"
7- "testing"
8 )9 )
9 10
10 func TestColorManagerErrors(t *testing.T) {11 func TestColorManagerErrors(t *testing.T) {
M internal/adapters/wayland/colorrepresentation.go +8 -8
@@ -58,7 +58,7 @@ func (m representationManager) GetSurface(r *colorrepresentation.WpColorRepresen
5858 res.OnDestroy = func() {
5959 if surf.representationControl == h {
6060 surf.representationControl = nil
61- surf.pendingRepresentation = surfaceRepresentation{}
61+ surf.next.representation = surfaceRepresentation{}
6262 }
6363 }
6464 }
@@ -71,7 +71,7 @@ type representationSurface struct {
7171
7272 func (h *representationSurface) Destroy(*colorrepresentation.WpColorRepresentationSurfaceV1) {
7373 if h.surf != nil && !h.surf.destroyed {
74- h.surf.pendingRepresentation = surfaceRepresentation{}
74+ h.surf.next.representation = surfaceRepresentation{}
7575 }
7676 }
7777 func (h *representationSurface) live(r *colorrepresentation.WpColorRepresentationSurfaceV1) bool {
@@ -89,7 +89,7 @@ func (h *representationSurface) SetAlphaMode(r *colorrepresentation.WpColorRepre
8989 r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorAlphaMode), "unsupported alpha mode")
9090 return
9191 }
92- h.surf.pendingRepresentation.alpha = uint8(mode)
92+ h.surf.next.representation.alpha = uint8(mode)
9393 }
9494 func (h *representationSurface) SetCoefficientsAndRange(r *colorrepresentation.WpColorRepresentationSurfaceV1, coeff, ran uint32) {
9595 if !h.live(r) {
@@ -101,7 +101,7 @@ func (h *representationSurface) SetCoefficientsAndRange(r *colorrepresentation.W
101101 r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorCoefficients), "unsupported coefficients or range")
102102 return
103103 }
104- h.surf.pendingRepresentation.coefficients, h.surf.pendingRepresentation.rangeValue = uint8(coeff), uint8(ran)
104+ h.surf.next.representation.coefficients, h.surf.next.representation.rangeValue = uint8(coeff), uint8(ran)
105105 }
106106 func (h *representationSurface) SetChromaLocation(r *colorrepresentation.WpColorRepresentationSurfaceV1, chroma uint32) {
107107 if !h.live(r) {
@@ -111,7 +111,7 @@ func (h *representationSurface) SetChromaLocation(r *colorrepresentation.WpColor
111111 r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorChromaLocation), "unsupported chroma location")
112112 return
113113 }
114- h.surf.pendingRepresentation.chroma = uint8(chroma)
114+ h.surf.next.representation.chroma = uint8(chroma)
115115 }
116116
117117 // The metadata is checked against the buffer the commit will show,
@@ -119,7 +119,7 @@ func (h *representationSurface) SetChromaLocation(r *colorrepresentation.WpColor
119119 // buffer: then the one of the last queued commit that attached, or the
120120 // current one.
121121 func (s *surface) checkRepresentationCommit() bool {
122- rep := s.pendingRepresentation
122+ rep := s.next.representation
123123 if rep.coefficients == 0 && rep.chroma == 0 {
124124 return true
125125 }
@@ -130,8 +130,8 @@ func (s *surface) checkRepresentationCommit() bool {
130130 break
131131 }
132132 }
133- if s.attached {
134- buffer = s.pending
133+ if s.next.attached {
134+ buffer = s.next.buffer
135135 }
136136 if buffer == nil {
137137 return true
@@ -58,7 +58,7 @@ func (m representationManager) GetSurface(r *colorrepresentation.WpColorRepresen
58 res.OnDestroy = func() {58 res.OnDestroy = func() {
59 if surf.representationControl == h {59 if surf.representationControl == h {
60 surf.representationControl = nil60 surf.representationControl = nil
61- surf.pendingRepresentation = surfaceRepresentation{}61+ surf.next.representation = surfaceRepresentation{}
62 }62 }
63 }63 }
64 }64 }
@@ -71,7 +71,7 @@ type representationSurface struct {
71 71
72 func (h *representationSurface) Destroy(*colorrepresentation.WpColorRepresentationSurfaceV1) {72 func (h *representationSurface) Destroy(*colorrepresentation.WpColorRepresentationSurfaceV1) {
73 if h.surf != nil && !h.surf.destroyed {73 if h.surf != nil && !h.surf.destroyed {
74- h.surf.pendingRepresentation = surfaceRepresentation{}74+ h.surf.next.representation = surfaceRepresentation{}
75 }75 }
76 }76 }
77 func (h *representationSurface) live(r *colorrepresentation.WpColorRepresentationSurfaceV1) bool {77 func (h *representationSurface) live(r *colorrepresentation.WpColorRepresentationSurfaceV1) bool {
@@ -89,7 +89,7 @@ func (h *representationSurface) SetAlphaMode(r *colorrepresentation.WpColorRepre
89 r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorAlphaMode), "unsupported alpha mode")89 r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorAlphaMode), "unsupported alpha mode")
90 return90 return
91 }91 }
92- h.surf.pendingRepresentation.alpha = uint8(mode)92+ h.surf.next.representation.alpha = uint8(mode)
93 }93 }
94 func (h *representationSurface) SetCoefficientsAndRange(r *colorrepresentation.WpColorRepresentationSurfaceV1, coeff, ran uint32) {94 func (h *representationSurface) SetCoefficientsAndRange(r *colorrepresentation.WpColorRepresentationSurfaceV1, coeff, ran uint32) {
95 if !h.live(r) {95 if !h.live(r) {
@@ -101,7 +101,7 @@ func (h *representationSurface) SetCoefficientsAndRange(r *colorrepresentation.W
101 r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorCoefficients), "unsupported coefficients or range")101 r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorCoefficients), "unsupported coefficients or range")
102 return102 return
103 }103 }
104- h.surf.pendingRepresentation.coefficients, h.surf.pendingRepresentation.rangeValue = uint8(coeff), uint8(ran)104+ h.surf.next.representation.coefficients, h.surf.next.representation.rangeValue = uint8(coeff), uint8(ran)
105 }105 }
106 func (h *representationSurface) SetChromaLocation(r *colorrepresentation.WpColorRepresentationSurfaceV1, chroma uint32) {106 func (h *representationSurface) SetChromaLocation(r *colorrepresentation.WpColorRepresentationSurfaceV1, chroma uint32) {
107 if !h.live(r) {107 if !h.live(r) {
@@ -111,7 +111,7 @@ func (h *representationSurface) SetChromaLocation(r *colorrepresentation.WpColor
111 r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorChromaLocation), "unsupported chroma location")111 r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorChromaLocation), "unsupported chroma location")
112 return112 return
113 }113 }
114- h.surf.pendingRepresentation.chroma = uint8(chroma)114+ h.surf.next.representation.chroma = uint8(chroma)
115 }115 }
116 116
117 // The metadata is checked against the buffer the commit will show,117 // The metadata is checked against the buffer the commit will show,
@@ -119,7 +119,7 @@ func (h *representationSurface) SetChromaLocation(r *colorrepresentation.WpColor
119 // buffer: then the one of the last queued commit that attached, or the119 // buffer: then the one of the last queued commit that attached, or the
120 // current one.120 // current one.
121 func (s *surface) checkRepresentationCommit() bool {121 func (s *surface) checkRepresentationCommit() bool {
122- rep := s.pendingRepresentation122+ rep := s.next.representation
123 if rep.coefficients == 0 && rep.chroma == 0 {123 if rep.coefficients == 0 && rep.chroma == 0 {
124 return true124 return true
125 }125 }
@@ -130,8 +130,8 @@ func (s *surface) checkRepresentationCommit() bool {
130 break130 break
131 }131 }
132 }132 }
133- if s.attached {133+ if s.next.attached {
134- buffer = s.pending134+ buffer = s.next.buffer
135 }135 }
136 if buffer == nil {136 if buffer == nil {
137 return true137 return true
M internal/adapters/wayland/colorrepresentation_test.go +3 -3
@@ -142,8 +142,8 @@ func TestColorRepresentationQueuedCommit(t *testing.T) {
142142 surf.queue = nil
143143 surf.applyUpdate(u)
144144 }
145- if surf.representation.coefficients != coefficient709 || surf.pendingRepresentation.coefficients != coefficient2020 {
146- t.Errorf("applied=%+v pending=%+v", surf.representation, surf.pendingRepresentation)
145+ if surf.representation.coefficients != coefficient709 || surf.next.representation.coefficients != coefficient2020 {
146+ t.Errorf("applied=%+v pending=%+v", surf.representation, surf.next.representation)
147147 }
148148 }
149149 })
@@ -176,7 +176,7 @@ func TestColorRepresentationChecksQueuedBuffer(t *testing.T) {
176176 // The RGB commit waits in the queue; the next one only sets
177177 // YUV coefficients.
178178 surf.queueUpdate()
179- surf.pendingRepresentation.coefficients = coefficient709
179+ surf.next.representation.coefficients = coefficient709
180180 if surf.checkRepresentationCommit() {
181181 t.Error("YUV coefficients accepted for a queued RGB buffer")
182182 }
@@ -142,8 +142,8 @@ func TestColorRepresentationQueuedCommit(t *testing.T) {
142 surf.queue = nil142 surf.queue = nil
143 surf.applyUpdate(u)143 surf.applyUpdate(u)
144 }144 }
145- if surf.representation.coefficients != coefficient709 || surf.pendingRepresentation.coefficients != coefficient2020 {145+ if surf.representation.coefficients != coefficient709 || surf.next.representation.coefficients != coefficient2020 {
146- t.Errorf("applied=%+v pending=%+v", surf.representation, surf.pendingRepresentation)146+ t.Errorf("applied=%+v pending=%+v", surf.representation, surf.next.representation)
147 }147 }
148 }148 }
149 })149 })
@@ -176,7 +176,7 @@ func TestColorRepresentationChecksQueuedBuffer(t *testing.T) {
176 // The RGB commit waits in the queue; the next one only sets176 // The RGB commit waits in the queue; the next one only sets
177 // YUV coefficients.177 // YUV coefficients.
178 surf.queueUpdate()178 surf.queueUpdate()
179- surf.pendingRepresentation.coefficients = coefficient709179+ surf.next.representation.coefficients = coefficient709
180 if surf.checkRepresentationCommit() {180 if surf.checkRepresentationCommit() {
181 t.Error("YUV coefficients accepted for a queued RGB buffer")181 t.Error("YUV coefficients accepted for a queued RGB buffer")
182 }182 }
M internal/adapters/wayland/commit_lifecycle_test.go +43 -19
@@ -1,11 +1,13 @@
11 package wayland
22
33 import (
4+ "reflect"
45 "testing"
56 "time"
67
78 "github.com/bnema/neferwl/internal/ports"
89 "github.com/bnema/purego-libwayland/protocol/fifo"
10+ "github.com/bnema/purego-libwayland/protocol/presentationtime"
911 "github.com/bnema/purego-libwayland/protocol/viewporter"
1012 "github.com/bnema/purego-libwayland/protocol/wayland"
1113 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
@@ -22,30 +24,30 @@ func TestCapturedCommitKeepsLaterRequests(t *testing.T) {
2224 srv.constraints[surf] = cons
2325 firstRegion := &ports.Rect{W: 2}
2426 cons.pending = firstRegion
25- surf.pendingScale, surf.pendingAsync, surf.pendingKind = 2, true, 1
27+ surf.next.scale, surf.next.async, surf.next.kind = 2, true, 1
2628 surf.xdg.pendingGeometry = ports.Rect{W: 10}
27- surf.pendingInputSet, surf.pendingInputAll = true, true
28- surf.pendingDamage = []ports.Rect{{W: 2}}
29- surf.pendingBarrier = true
29+ surf.next.inputSet, surf.next.inputAll = true, true
30+ surf.next.damage = []ports.Rect{{W: 2}}
31+ surf.next.barrier = true
3032 surf.queueUpdate()
3133
3234 laterRegion := &ports.Rect{W: 3}
3335 cons.pending = laterRegion
34- surf.pendingScale, surf.pendingAsync, surf.pendingKind = 3, false, 2
36+ surf.next.scale, surf.next.async, surf.next.kind = 3, false, 2
3537 surf.sub.pendingLayout = []childLayout{{x: 7}}
3638 surf.xdg.pendingGeometry = ports.Rect{W: 20}
37- surf.pendingInputSet, surf.pendingInputAll = true, false
38- surf.pendingInputRects = []ports.Rect{{W: 3}}
39- surf.pendingDamage = []ports.Rect{{W: 3}}
40- surf.pendingBarrier = true
39+ surf.next.inputSet, surf.next.inputAll = true, false
40+ surf.next.inputRects = []ports.Rect{{W: 3}}
41+ surf.next.damage = []ports.Rect{{W: 3}}
42+ surf.next.barrier = true
4143 laterTime := time.Now().Add(time.Second)
42- surf.pendingTime = laterTime
44+ surf.next.at = laterTime
4345
4446 surf.queue[0].applyGraph()
4547 if surf.bufferScale != 2 || !surf.async || surf.contentKind != 1 || len(surf.sub.layout) != 0 || surf.xdg.geometry.W != 10 || cons.region.W != 2 || !surf.inputAll || !surf.barrier {
4648 t.Fatalf("captured state changed: scale=%d async=%v kind=%d geometry=%v region=%v input=%v barrier=%v", surf.bufferScale, surf.async, surf.contentKind, surf.xdg.geometry, cons.region, surf.inputAll, surf.barrier)
4749 }
48- if surf.pendingScale != 3 || surf.pendingAsync || surf.pendingKind != 2 || len(surf.sub.pendingLayout) != 1 || surf.xdg.pendingGeometry.W != 20 || cons.pending != laterRegion || !surf.pendingInputSet || surf.pendingInputAll || len(surf.pendingInputRects) != 1 || len(surf.pendingDamage) != 1 || !surf.pendingBarrier || !surf.pendingTime.Equal(laterTime) {
50+ if surf.next.scale != 3 || surf.next.async || surf.next.kind != 2 || len(surf.sub.pendingLayout) != 1 || surf.xdg.pendingGeometry.W != 20 || cons.pending != laterRegion || !surf.next.inputSet || surf.next.inputAll || len(surf.next.inputRects) != 1 || len(surf.next.damage) != 1 || !surf.next.barrier || !surf.next.at.Equal(laterTime) {
4951 t.Fatal("application consumed a later request")
5052 }
5153 surf.queueUpdate()
@@ -131,13 +133,13 @@ func TestQueuedAttachKeepsLaterBuffer(t *testing.T) {
131133 surf = candidate
132134 }
133135 }
134- if surf == nil || len(surf.queue) != 1 || surf.queue[0].buffer.ID() != first || surf.pending.ID() != second {
136+ if surf == nil || len(surf.queue) != 1 || surf.queue[0].buffer.ID() != first || surf.next.buffer.ID() != second {
135137 t.Error("queued attach lost its captured or later buffer")
136138 return
137139 }
138140 surf.barrier = false
139141 s.tickFifo(time.Now(), nil)
140- if surf.current == nil || surf.current.ID() != first || surf.pending == nil || surf.pending.ID() != second {
142+ if surf.current == nil || surf.current.ID() != first || surf.next.buffer == nil || surf.next.buffer.ID() != second {
141143 t.Error("applying captured attach consumed later request")
142144 }
143145 })
@@ -247,17 +249,17 @@ func TestQueuedDestructionDropsUpdates(t *testing.T) {
247249 s.display.Do(func() {
248250 for _, surf := range s.surfaces {
249251 if surf.wl != nil && surf.wl.ID() == surfID {
250- surf.pendingScale = 3
251- surf.pendingDamage = []ports.Rect{{W: 3}}
252+ surf.next.scale = 3
253+ surf.next.damage = []ports.Rect{{W: 3}}
252254 if kind == "viewport" {
253255 surf.viewport.SetDestination(surf.viewport.resource, 2, 2)
254256 }
255- surf.pendingTime = time.Now().Add(time.Second)
257+ surf.next.at = time.Now().Add(time.Second)
256258 if kind == "buffer" {
257259 for resource := range s.buffers {
258260 if resource.ID() == bufferID {
259- surf.pending = wayland.WrapBuffer(resource)
260- surf.attached = true
261+ surf.next.buffer = wayland.WrapBuffer(resource)
262+ surf.next.attached = true
261263 }
262264 }
263265 }
@@ -312,7 +314,7 @@ func TestQueuedDestructionDropsUpdates(t *testing.T) {
312314 // A dropped update must be cleared before a different surface reuses it.
313315 func TestDroppedUpdateRecycledWithoutState(t *testing.T) {
314316 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}
315- old := &surface{server: srv, pendingScale: 2, pendingBarrier: true, pendingDamage: []ports.Rect{{W: 2}}}
317+ old := &surface{server: srv, next: pendingCommit{scale: 2, barrier: true, damage: []ports.Rect{{W: 2}}}}
316318 old.queueUpdate()
317319 old.destroyed = true
318320 old.dropQueue()
@@ -390,3 +392,25 @@ func TestCapturedCommitApplyAllocations(t *testing.T) {
390392 t.Errorf("steady-state queue/apply: %.1f allocs/commit; want 0", allocs)
391393 }
392394 }
395+
396+// Capture takes every requested field; only sticky hints carry over.
397+func TestTakePendingKeepsStickyState(t *testing.T) {
398+ srv := &Server{constraints: map[*surface]*constraint{}}
399+ surf := &surface{server: srv}
400+ all := pendingCommit{
401+ buffer: &wayland.Buffer{}, attached: true, scale: 2, transform: 1,
402+ opaque: []ports.Rect{{W: 1}}, opaqueSet: true, inputAll: true, inputSet: true, inputRects: []ports.Rect{{W: 1}},
403+ callbacks: []*wayland.Callback{{}}, async: true, color: SurfaceColor{Set: true}, representation: surfaceRepresentation{alpha: 1}, kind: 3,
404+ barrier: true, wait: true, at: time.Unix(1, 0), damage: []ports.Rect{{W: 1}}, bufDamage: []ports.Rect{{W: 1}},
405+ feedback: []*presentationtime.WpPresentationFeedback{{}}, sync: &commitSync{},
406+ }
407+ surf.next = all
408+ u := surf.takePending()
409+ if !reflect.DeepEqual(u.pendingCommit, all) {
410+ t.Fatalf("captured = %+v", u.pendingCommit)
411+ }
412+ want := pendingCommit{scale: 2, transform: 1, async: true, color: SurfaceColor{Set: true}, representation: surfaceRepresentation{alpha: 1}, kind: 3}
413+ if !reflect.DeepEqual(surf.next, want) {
414+ t.Fatalf("left pending = %+v, want %+v", surf.next, want)
415+ }
416+}
@@ -1,11 +1,13 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "reflect"
4 "testing"5 "testing"
5 "time"6 "time"
6 7
7 "github.com/bnema/neferwl/internal/ports"8 "github.com/bnema/neferwl/internal/ports"
8 "github.com/bnema/purego-libwayland/protocol/fifo"9 "github.com/bnema/purego-libwayland/protocol/fifo"
10+ "github.com/bnema/purego-libwayland/protocol/presentationtime"
9 "github.com/bnema/purego-libwayland/protocol/viewporter"11 "github.com/bnema/purego-libwayland/protocol/viewporter"
10 "github.com/bnema/purego-libwayland/protocol/wayland"12 "github.com/bnema/purego-libwayland/protocol/wayland"
11 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"13 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
@@ -22,30 +24,30 @@ func TestCapturedCommitKeepsLaterRequests(t *testing.T) {
22 srv.constraints[surf] = cons24 srv.constraints[surf] = cons
23 firstRegion := &ports.Rect{W: 2}25 firstRegion := &ports.Rect{W: 2}
24 cons.pending = firstRegion26 cons.pending = firstRegion
25- surf.pendingScale, surf.pendingAsync, surf.pendingKind = 2, true, 127+ surf.next.scale, surf.next.async, surf.next.kind = 2, true, 1
26 surf.xdg.pendingGeometry = ports.Rect{W: 10}28 surf.xdg.pendingGeometry = ports.Rect{W: 10}
27- surf.pendingInputSet, surf.pendingInputAll = true, true29+ surf.next.inputSet, surf.next.inputAll = true, true
28- surf.pendingDamage = []ports.Rect{{W: 2}}30+ surf.next.damage = []ports.Rect{{W: 2}}
29- surf.pendingBarrier = true31+ surf.next.barrier = true
30 surf.queueUpdate()32 surf.queueUpdate()
31 33
32 laterRegion := &ports.Rect{W: 3}34 laterRegion := &ports.Rect{W: 3}
33 cons.pending = laterRegion35 cons.pending = laterRegion
34- surf.pendingScale, surf.pendingAsync, surf.pendingKind = 3, false, 236+ surf.next.scale, surf.next.async, surf.next.kind = 3, false, 2
35 surf.sub.pendingLayout = []childLayout{{x: 7}}37 surf.sub.pendingLayout = []childLayout{{x: 7}}
36 surf.xdg.pendingGeometry = ports.Rect{W: 20}38 surf.xdg.pendingGeometry = ports.Rect{W: 20}
37- surf.pendingInputSet, surf.pendingInputAll = true, false39+ surf.next.inputSet, surf.next.inputAll = true, false
38- surf.pendingInputRects = []ports.Rect{{W: 3}}40+ surf.next.inputRects = []ports.Rect{{W: 3}}
39- surf.pendingDamage = []ports.Rect{{W: 3}}41+ surf.next.damage = []ports.Rect{{W: 3}}
40- surf.pendingBarrier = true42+ surf.next.barrier = true
41 laterTime := time.Now().Add(time.Second)43 laterTime := time.Now().Add(time.Second)
42- surf.pendingTime = laterTime44+ surf.next.at = laterTime
43 45
44 surf.queue[0].applyGraph()46 surf.queue[0].applyGraph()
45 if surf.bufferScale != 2 || !surf.async || surf.contentKind != 1 || len(surf.sub.layout) != 0 || surf.xdg.geometry.W != 10 || cons.region.W != 2 || !surf.inputAll || !surf.barrier {47 if surf.bufferScale != 2 || !surf.async || surf.contentKind != 1 || len(surf.sub.layout) != 0 || surf.xdg.geometry.W != 10 || cons.region.W != 2 || !surf.inputAll || !surf.barrier {
46 t.Fatalf("captured state changed: scale=%d async=%v kind=%d geometry=%v region=%v input=%v barrier=%v", surf.bufferScale, surf.async, surf.contentKind, surf.xdg.geometry, cons.region, surf.inputAll, surf.barrier)48 t.Fatalf("captured state changed: scale=%d async=%v kind=%d geometry=%v region=%v input=%v barrier=%v", surf.bufferScale, surf.async, surf.contentKind, surf.xdg.geometry, cons.region, surf.inputAll, surf.barrier)
47 }49 }
48- if surf.pendingScale != 3 || surf.pendingAsync || surf.pendingKind != 2 || len(surf.sub.pendingLayout) != 1 || surf.xdg.pendingGeometry.W != 20 || cons.pending != laterRegion || !surf.pendingInputSet || surf.pendingInputAll || len(surf.pendingInputRects) != 1 || len(surf.pendingDamage) != 1 || !surf.pendingBarrier || !surf.pendingTime.Equal(laterTime) {50+ if surf.next.scale != 3 || surf.next.async || surf.next.kind != 2 || len(surf.sub.pendingLayout) != 1 || surf.xdg.pendingGeometry.W != 20 || cons.pending != laterRegion || !surf.next.inputSet || surf.next.inputAll || len(surf.next.inputRects) != 1 || len(surf.next.damage) != 1 || !surf.next.barrier || !surf.next.at.Equal(laterTime) {
49 t.Fatal("application consumed a later request")51 t.Fatal("application consumed a later request")
50 }52 }
51 surf.queueUpdate()53 surf.queueUpdate()
@@ -131,13 +133,13 @@ func TestQueuedAttachKeepsLaterBuffer(t *testing.T) {
131 surf = candidate133 surf = candidate
132 }134 }
133 }135 }
134- if surf == nil || len(surf.queue) != 1 || surf.queue[0].buffer.ID() != first || surf.pending.ID() != second {136+ if surf == nil || len(surf.queue) != 1 || surf.queue[0].buffer.ID() != first || surf.next.buffer.ID() != second {
135 t.Error("queued attach lost its captured or later buffer")137 t.Error("queued attach lost its captured or later buffer")
136 return138 return
137 }139 }
138 surf.barrier = false140 surf.barrier = false
139 s.tickFifo(time.Now(), nil)141 s.tickFifo(time.Now(), nil)
140- if surf.current == nil || surf.current.ID() != first || surf.pending == nil || surf.pending.ID() != second {142+ if surf.current == nil || surf.current.ID() != first || surf.next.buffer == nil || surf.next.buffer.ID() != second {
141 t.Error("applying captured attach consumed later request")143 t.Error("applying captured attach consumed later request")
142 }144 }
143 })145 })
@@ -247,17 +249,17 @@ func TestQueuedDestructionDropsUpdates(t *testing.T) {
247 s.display.Do(func() {249 s.display.Do(func() {
248 for _, surf := range s.surfaces {250 for _, surf := range s.surfaces {
249 if surf.wl != nil && surf.wl.ID() == surfID {251 if surf.wl != nil && surf.wl.ID() == surfID {
250- surf.pendingScale = 3252+ surf.next.scale = 3
251- surf.pendingDamage = []ports.Rect{{W: 3}}253+ surf.next.damage = []ports.Rect{{W: 3}}
252 if kind == "viewport" {254 if kind == "viewport" {
253 surf.viewport.SetDestination(surf.viewport.resource, 2, 2)255 surf.viewport.SetDestination(surf.viewport.resource, 2, 2)
254 }256 }
255- surf.pendingTime = time.Now().Add(time.Second)257+ surf.next.at = time.Now().Add(time.Second)
256 if kind == "buffer" {258 if kind == "buffer" {
257 for resource := range s.buffers {259 for resource := range s.buffers {
258 if resource.ID() == bufferID {260 if resource.ID() == bufferID {
259- surf.pending = wayland.WrapBuffer(resource)261+ surf.next.buffer = wayland.WrapBuffer(resource)
260- surf.attached = true262+ surf.next.attached = true
261 }263 }
262 }264 }
263 }265 }
@@ -312,7 +314,7 @@ func TestQueuedDestructionDropsUpdates(t *testing.T) {
312 // A dropped update must be cleared before a different surface reuses it.314 // A dropped update must be cleared before a different surface reuses it.
313 func TestDroppedUpdateRecycledWithoutState(t *testing.T) {315 func TestDroppedUpdateRecycledWithoutState(t *testing.T) {
314 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}316 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}
315- old := &surface{server: srv, pendingScale: 2, pendingBarrier: true, pendingDamage: []ports.Rect{{W: 2}}}317+ old := &surface{server: srv, next: pendingCommit{scale: 2, barrier: true, damage: []ports.Rect{{W: 2}}}}
316 old.queueUpdate()318 old.queueUpdate()
317 old.destroyed = true319 old.destroyed = true
318 old.dropQueue()320 old.dropQueue()
@@ -390,3 +392,25 @@ func TestCapturedCommitApplyAllocations(t *testing.T) {
390 t.Errorf("steady-state queue/apply: %.1f allocs/commit; want 0", allocs)392 t.Errorf("steady-state queue/apply: %.1f allocs/commit; want 0", allocs)
391 }393 }
392 }394 }
395+
396+// Capture takes every requested field; only sticky hints carry over.
397+func TestTakePendingKeepsStickyState(t *testing.T) {
398+ srv := &Server{constraints: map[*surface]*constraint{}}
399+ surf := &surface{server: srv}
400+ all := pendingCommit{
401+ buffer: &wayland.Buffer{}, attached: true, scale: 2, transform: 1,
402+ opaque: []ports.Rect{{W: 1}}, opaqueSet: true, inputAll: true, inputSet: true, inputRects: []ports.Rect{{W: 1}},
403+ callbacks: []*wayland.Callback{{}}, async: true, color: SurfaceColor{Set: true}, representation: surfaceRepresentation{alpha: 1}, kind: 3,
404+ barrier: true, wait: true, at: time.Unix(1, 0), damage: []ports.Rect{{W: 1}}, bufDamage: []ports.Rect{{W: 1}},
405+ feedback: []*presentationtime.WpPresentationFeedback{{}}, sync: &commitSync{},
406+ }
407+ surf.next = all
408+ u := surf.takePending()
409+ if !reflect.DeepEqual(u.pendingCommit, all) {
410+ t.Fatalf("captured = %+v", u.pendingCommit)
411+ }
412+ want := pendingCommit{scale: 2, transform: 1, async: true, color: SurfaceColor{Set: true}, representation: surfaceRepresentation{alpha: 1}, kind: 3}
413+ if !reflect.DeepEqual(surf.next, want) {
414+ t.Fatalf("left pending = %+v, want %+v", surf.next, want)
415+ }
416+}
M internal/adapters/wayland/constraints.go +2 -2
@@ -235,7 +235,7 @@ func (s *Server) deactivateConstraint() {
235235 // pointer and keyboard focus, and deactivates any other.
236236 func (s *Server) updateConstraint() {
237237 var want *constraint
238- if w := s.windows[s.pointerFocus]; w != nil && w.mapped && s.pointerFocus == s.focused {
238+ if w := s.windows[s.seat.pointerFocus]; w != nil && w.mapped && s.seat.pointerFocus == s.seat.focused {
239239 if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) {
240240 want = c
241241 }
@@ -301,7 +301,7 @@ func (s *Server) emitConstraint(c *constraint) {
301301 // activates only there, so it never warps the pointer.
302302 func (c *constraint) contains(w *window) bool {
303303 r := c.region
304- x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY)
304+ x, y := w.surfacePoint(w.xdg.server.seat.pointerX, w.xdg.server.seat.pointerY)
305305 all, rects := c.surface.effectiveInput()
306306 if !all {
307307 localX, localY := x-float64(w.xdg.geometry.X), y-float64(w.xdg.geometry.Y)
@@ -235,7 +235,7 @@ func (s *Server) deactivateConstraint() {
235 // pointer and keyboard focus, and deactivates any other.235 // pointer and keyboard focus, and deactivates any other.
236 func (s *Server) updateConstraint() {236 func (s *Server) updateConstraint() {
237 var want *constraint237 var want *constraint
238- if w := s.windows[s.pointerFocus]; w != nil && w.mapped && s.pointerFocus == s.focused {238+ if w := s.windows[s.seat.pointerFocus]; w != nil && w.mapped && s.seat.pointerFocus == s.seat.focused {
239 if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) {239 if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) {
240 want = c240 want = c
241 }241 }
@@ -301,7 +301,7 @@ func (s *Server) emitConstraint(c *constraint) {
301 // activates only there, so it never warps the pointer.301 // activates only there, so it never warps the pointer.
302 func (c *constraint) contains(w *window) bool {302 func (c *constraint) contains(w *window) bool {
303 r := c.region303 r := c.region
304- x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY)304+ x, y := w.surfacePoint(w.xdg.server.seat.pointerX, w.xdg.server.seat.pointerY)
305 all, rects := c.surface.effectiveInput()305 all, rects := c.surface.effectiveInput()
306 if !all {306 if !all {
307 localX, localY := x-float64(w.xdg.geometry.X), y-float64(w.xdg.geometry.Y)307 localX, localY := x-float64(w.xdg.geometry.X), y-float64(w.xdg.geometry.Y)
M internal/adapters/wayland/cursor.go +1 -1
@@ -103,7 +103,7 @@ func (pointer) Release(r *wayland.Pointer) { r.Destroy() }
103103 // hasPointerFocus reports whether the client owns the window or layer
104104 // under the pointer.
105105 func (s *Server) hasPointerFocus(c server.Client) bool {
106- owner, _, ok := s.pointerTarget(s.pointerFocus)
106+ owner, _, ok := s.pointerTarget(s.seat.pointerFocus)
107107 return ok && owner == c
108108 }
109109
@@ -103,7 +103,7 @@ func (pointer) Release(r *wayland.Pointer) { r.Destroy() }
103 // hasPointerFocus reports whether the client owns the window or layer103 // hasPointerFocus reports whether the client owns the window or layer
104 // under the pointer.104 // under the pointer.
105 func (s *Server) hasPointerFocus(c server.Client) bool {105 func (s *Server) hasPointerFocus(c server.Client) bool {
106- owner, _, ok := s.pointerTarget(s.pointerFocus)106+ owner, _, ok := s.pointerTarget(s.seat.pointerFocus)
107 return ok && owner == c107 return ok && owner == c
108 }108 }
109 109
M internal/adapters/wayland/fifo.go +29 -56
@@ -8,7 +8,6 @@ import (
88 "github.com/bnema/purego-libwayland/protocol/committiming"
99 "github.com/bnema/purego-libwayland/protocol/contenttype"
1010 "github.com/bnema/purego-libwayland/protocol/fifo"
11- "github.com/bnema/purego-libwayland/protocol/presentationtime"
1211 "github.com/bnema/purego-libwayland/protocol/wayland"
1312 "github.com/bnema/purego-libwayland/server"
1413 "golang.org/x/sys/unix"
@@ -78,12 +77,12 @@ func (h *fifoHandler) live(r *fifo.WpFifoV1) bool {
7877 }
7978 func (h *fifoHandler) SetBarrier(r *fifo.WpFifoV1) {
8079 if h.live(r) {
81- h.surface.pendingBarrier = true
80+ h.surface.next.barrier = true
8281 }
8382 }
8483 func (h *fifoHandler) WaitBarrier(r *fifo.WpFifoV1) {
8584 if h.live(r) {
86- h.surface.pendingWait = true
85+ h.surface.next.wait = true
8786 }
8887 }
8988 func (*fifoHandler) Destroy(*fifo.WpFifoV1) {}
@@ -121,10 +120,10 @@ func (h *timerHandler) SetTimestamp(r *committiming.WpCommitTimerV1, secHi, secL
121120 r.PostError(uint32(committiming.WpCommitTimerV1ErrorSurfaceDestroyed), "surface destroyed")
122121 case nsec >= 1e9:
123122 r.PostError(uint32(committiming.WpCommitTimerV1ErrorInvalidTimestamp), "tv_nsec out of range")
124- case !h.surface.pendingTime.IsZero():
123+ case !h.surface.next.at.IsZero():
125124 r.PostError(uint32(committiming.WpCommitTimerV1ErrorTimestampExists), "timestamp already set")
126125 default:
127- h.surface.pendingTime = monotonicTime(uint64(secHi)<<32|uint64(secLo), int64(nsec))
126+ h.surface.next.at = monotonicTime(uint64(secHi)<<32|uint64(secLo), int64(nsec))
128127 }
129128 }
130129 func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}
@@ -174,7 +173,7 @@ func (m contentTypeManager) GetSurfaceContentType(r *contenttype.WpContentTypeMa
174173 // Destroying it sets the type back to none, double-buffered.
175174 if state.contentType == h {
176175 state.contentType = nil
177- state.pendingKind = 0
176+ state.next.kind = 0
178177 }
179178 }
180179 }
@@ -183,7 +182,7 @@ type contentTypeHandler struct{ surface *surface }
183182
184183 func (h *contentTypeHandler) SetContentType(_ *contenttype.WpContentTypeV1, kind uint32) {
185184 if h.surface != nil && !h.surface.destroyed && h.surface.contentType == h {
186- h.surface.pendingKind = kind
185+ h.surface.next.kind = kind
187186 }
188187 }
189188 func (*contentTypeHandler) Destroy(*contenttype.WpContentTypeV1) {}
@@ -199,44 +198,26 @@ func (s *Server) surfaceOf(w *wayland.Surface) *surface {
199198
200199 // update owns a captured commit until it applies or is discarded.
201200 type update struct {
202- inputSet, inputAll bool
203- inputRects []ports.Rect
204- attached bool
205- buffer *wayland.Buffer
206- scale int
207- transform ports.BufferTransform
208- opaque []ports.Rect
209- opaqueSet bool
210- async bool
211- kind uint32
212- callbacks []*wayland.Callback
213- vp *viewport
214- vpW, vpH int32
215- vpSet bool
216- vpSrc [4]server.Fixed
217- vpCrop bool
218- layout []childLayout
219- deps []*update
220- owner *surface
221- prev *update
222- synced, bound bool
223- xdg *xdgSurface
224- geometry ports.Rect
225- cons *constraint
226- region *ports.Rect
227- layer *layerSurface
228- layerNext layerState
229- barrier bool
230- wait bool
231- at time.Time
232- feedback []*presentationtime.WpPresentationFeedback
233- sync *commitSync
234- color SurfaceColor
235- representation surfaceRepresentation
236- damage []ports.Rect
237- bufDamage []ports.Rect
238- readyGeneration uint64
239- readyResult bool
201+ // pendingCommit is the wl_surface state captured by the commit.
202+ pendingCommit
203+ vp *viewport
204+ vpW, vpH int32
205+ vpSet bool
206+ vpSrc [4]server.Fixed
207+ vpCrop bool
208+ layout []childLayout
209+ deps []*update
210+ owner *surface
211+ prev *update
212+ synced, bound bool
213+ xdg *xdgSurface
214+ geometry ports.Rect
215+ cons *constraint
216+ region *ports.Rect
217+ layer *layerSurface
218+ layerNext layerState
219+ readyGeneration uint64
220+ readyResult bool
240221 // refs counts incoming deps and prev links. Retired updates remain readable
241222 // until their last incoming link goes away, even after dropQueue.
242223 refs int
@@ -310,17 +291,9 @@ func sameLayout(a, b []childLayout) bool {
310291 // takePending moves the pending state into an update: one-shot state is
311292 // cleared, sticky state (scale, hints, viewport, geometry, layer) kept.
312293 func (s *surface) takePending() update {
313- u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback, sync: s.pendingSync, color: s.pendingColor, representation: s.pendingRepresentation}
314- u.inputSet, u.inputAll, u.inputRects = s.pendingInputSet, s.pendingInputAll, s.pendingInputRects
315- s.pendingInputSet = false
316- s.pendingInputRects = nil
317- u.transform = s.pendingTransform
318- u.opaque, u.opaqueSet = s.pendingOpaque, s.pendingOpaqueSet
319- s.pendingOpaque, s.pendingOpaqueSet = nil, false
320- s.attached, s.pending, s.callbacks = false, nil, nil
321- s.pendingFeedback, s.pendingSync = nil, nil
322- s.pendingDamage, s.pendingBufDamage = nil, nil
323- s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}
294+ u := update{pendingCommit: s.next}
295+ // One-shot requests apply once; sticky ones carry over.
296+ s.next = pendingCommit{scale: s.next.scale, transform: s.next.transform, async: s.next.async, color: s.next.color, representation: s.next.representation, kind: s.next.kind}
324297 if v := s.viewport; v != nil {
325298 u.vp, u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v, v.pendingW, v.pendingH, v.pendingSet, v.pendingSrc, v.pendingCrop
326299 }
@@ -8,7 +8,6 @@ import (
8 "github.com/bnema/purego-libwayland/protocol/committiming"8 "github.com/bnema/purego-libwayland/protocol/committiming"
9 "github.com/bnema/purego-libwayland/protocol/contenttype"9 "github.com/bnema/purego-libwayland/protocol/contenttype"
10 "github.com/bnema/purego-libwayland/protocol/fifo"10 "github.com/bnema/purego-libwayland/protocol/fifo"
11- "github.com/bnema/purego-libwayland/protocol/presentationtime"
12 "github.com/bnema/purego-libwayland/protocol/wayland"11 "github.com/bnema/purego-libwayland/protocol/wayland"
13 "github.com/bnema/purego-libwayland/server"12 "github.com/bnema/purego-libwayland/server"
14 "golang.org/x/sys/unix"13 "golang.org/x/sys/unix"
@@ -78,12 +77,12 @@ func (h *fifoHandler) live(r *fifo.WpFifoV1) bool {
78 }77 }
79 func (h *fifoHandler) SetBarrier(r *fifo.WpFifoV1) {78 func (h *fifoHandler) SetBarrier(r *fifo.WpFifoV1) {
80 if h.live(r) {79 if h.live(r) {
81- h.surface.pendingBarrier = true80+ h.surface.next.barrier = true
82 }81 }
83 }82 }
84 func (h *fifoHandler) WaitBarrier(r *fifo.WpFifoV1) {83 func (h *fifoHandler) WaitBarrier(r *fifo.WpFifoV1) {
85 if h.live(r) {84 if h.live(r) {
86- h.surface.pendingWait = true85+ h.surface.next.wait = true
87 }86 }
88 }87 }
89 func (*fifoHandler) Destroy(*fifo.WpFifoV1) {}88 func (*fifoHandler) Destroy(*fifo.WpFifoV1) {}
@@ -121,10 +120,10 @@ func (h *timerHandler) SetTimestamp(r *committiming.WpCommitTimerV1, secHi, secL
121 r.PostError(uint32(committiming.WpCommitTimerV1ErrorSurfaceDestroyed), "surface destroyed")120 r.PostError(uint32(committiming.WpCommitTimerV1ErrorSurfaceDestroyed), "surface destroyed")
122 case nsec >= 1e9:121 case nsec >= 1e9:
123 r.PostError(uint32(committiming.WpCommitTimerV1ErrorInvalidTimestamp), "tv_nsec out of range")122 r.PostError(uint32(committiming.WpCommitTimerV1ErrorInvalidTimestamp), "tv_nsec out of range")
124- case !h.surface.pendingTime.IsZero():123+ case !h.surface.next.at.IsZero():
125 r.PostError(uint32(committiming.WpCommitTimerV1ErrorTimestampExists), "timestamp already set")124 r.PostError(uint32(committiming.WpCommitTimerV1ErrorTimestampExists), "timestamp already set")
126 default:125 default:
127- h.surface.pendingTime = monotonicTime(uint64(secHi)<<32|uint64(secLo), int64(nsec))126+ h.surface.next.at = monotonicTime(uint64(secHi)<<32|uint64(secLo), int64(nsec))
128 }127 }
129 }128 }
130 func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}129 func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}
@@ -174,7 +173,7 @@ func (m contentTypeManager) GetSurfaceContentType(r *contenttype.WpContentTypeMa
174 // Destroying it sets the type back to none, double-buffered.173 // Destroying it sets the type back to none, double-buffered.
175 if state.contentType == h {174 if state.contentType == h {
176 state.contentType = nil175 state.contentType = nil
177- state.pendingKind = 0176+ state.next.kind = 0
178 }177 }
179 }178 }
180 }179 }
@@ -183,7 +182,7 @@ type contentTypeHandler struct{ surface *surface }
183 182
184 func (h *contentTypeHandler) SetContentType(_ *contenttype.WpContentTypeV1, kind uint32) {183 func (h *contentTypeHandler) SetContentType(_ *contenttype.WpContentTypeV1, kind uint32) {
185 if h.surface != nil && !h.surface.destroyed && h.surface.contentType == h {184 if h.surface != nil && !h.surface.destroyed && h.surface.contentType == h {
186- h.surface.pendingKind = kind185+ h.surface.next.kind = kind
187 }186 }
188 }187 }
189 func (*contentTypeHandler) Destroy(*contenttype.WpContentTypeV1) {}188 func (*contentTypeHandler) Destroy(*contenttype.WpContentTypeV1) {}
@@ -199,44 +198,26 @@ func (s *Server) surfaceOf(w *wayland.Surface) *surface {
199 198
200 // update owns a captured commit until it applies or is discarded.199 // update owns a captured commit until it applies or is discarded.
201 type update struct {200 type update struct {
202- inputSet, inputAll bool201+ // pendingCommit is the wl_surface state captured by the commit.
203- inputRects []ports.Rect202+ pendingCommit
204- attached bool203+ vp *viewport
205- buffer *wayland.Buffer204+ vpW, vpH int32
206- scale int205+ vpSet bool
207- transform ports.BufferTransform206+ vpSrc [4]server.Fixed
208- opaque []ports.Rect207+ vpCrop bool
209- opaqueSet bool208+ layout []childLayout
210- async bool209+ deps []*update
211- kind uint32210+ owner *surface
212- callbacks []*wayland.Callback211+ prev *update
213- vp *viewport212+ synced, bound bool
214- vpW, vpH int32213+ xdg *xdgSurface
215- vpSet bool214+ geometry ports.Rect
216- vpSrc [4]server.Fixed215+ cons *constraint
217- vpCrop bool216+ region *ports.Rect
218- layout []childLayout217+ layer *layerSurface
219- deps []*update218+ layerNext layerState
220- owner *surface219+ readyGeneration uint64
221- prev *update220+ readyResult bool
222- synced, bound bool
223- xdg *xdgSurface
224- geometry ports.Rect
225- cons *constraint
226- region *ports.Rect
227- layer *layerSurface
228- layerNext layerState
229- barrier bool
230- wait bool
231- at time.Time
232- feedback []*presentationtime.WpPresentationFeedback
233- sync *commitSync
234- color SurfaceColor
235- representation surfaceRepresentation
236- damage []ports.Rect
237- bufDamage []ports.Rect
238- readyGeneration uint64
239- readyResult bool
240 // refs counts incoming deps and prev links. Retired updates remain readable221 // refs counts incoming deps and prev links. Retired updates remain readable
241 // until their last incoming link goes away, even after dropQueue.222 // until their last incoming link goes away, even after dropQueue.
242 refs int223 refs int
@@ -310,17 +291,9 @@ func sameLayout(a, b []childLayout) bool {
310 // takePending moves the pending state into an update: one-shot state is291 // takePending moves the pending state into an update: one-shot state is
311 // cleared, sticky state (scale, hints, viewport, geometry, layer) kept.292 // cleared, sticky state (scale, hints, viewport, geometry, layer) kept.
312 func (s *surface) takePending() update {293 func (s *surface) takePending() update {
313- u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback, sync: s.pendingSync, color: s.pendingColor, representation: s.pendingRepresentation}294+ u := update{pendingCommit: s.next}
314- u.inputSet, u.inputAll, u.inputRects = s.pendingInputSet, s.pendingInputAll, s.pendingInputRects295+ // One-shot requests apply once; sticky ones carry over.
315- s.pendingInputSet = false296+ s.next = pendingCommit{scale: s.next.scale, transform: s.next.transform, async: s.next.async, color: s.next.color, representation: s.next.representation, kind: s.next.kind}
316- s.pendingInputRects = nil
317- u.transform = s.pendingTransform
318- u.opaque, u.opaqueSet = s.pendingOpaque, s.pendingOpaqueSet
319- s.pendingOpaque, s.pendingOpaqueSet = nil, false
320- s.attached, s.pending, s.callbacks = false, nil, nil
321- s.pendingFeedback, s.pendingSync = nil, nil
322- s.pendingDamage, s.pendingBufDamage = nil, nil
323- s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}
324 if v := s.viewport; v != nil {297 if v := s.viewport; v != nil {
325 u.vp, u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v, v.pendingW, v.pendingH, v.pendingSet, v.pendingSrc, v.pendingCrop298 u.vp, u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v, v.pendingW, v.pendingH, v.pendingSet, v.pendingSrc, v.pendingCrop
326 }299 }
M internal/adapters/wayland/fifo_test.go +3 -3
@@ -222,10 +222,10 @@ func TestQueuedParentRetainsRetiredChild(t *testing.T) {
222222 child := &surface{server: srv}
223223 child.sub.parent, child.sub.synced = parent, true
224224 parent.sub.children = []*surface{child}
225- child.pendingScale = 2
225+ child.next.scale = 2
226226 child.queueUpdate()
227227 old := child.queue[0]
228- parent.pendingScale = 3
228+ parent.next.scale = 3
229229 parent.queueUpdate()
230230 pu := parent.queue[0]
231231 if old.refs != 1 || len(pu.deps) != 1 || pu.deps[0] != old {
@@ -239,7 +239,7 @@ func TestQueuedParentRetainsRetiredChild(t *testing.T) {
239239 if !old.retired || old.refs != 1 {
240240 t.Fatalf("retired child ref count: retired=%v refs=%d", old.retired, old.refs)
241241 }
242- child.pendingScale = 7
242+ child.next.scale = 7
243243 child.queueUpdate()
244244 if child.queue[0] == old || old.scale != 2 || pu.deps[0] != old {
245245 t.Fatal("live dependency was recycled as a new child commit")
@@ -222,10 +222,10 @@ func TestQueuedParentRetainsRetiredChild(t *testing.T) {
222 child := &surface{server: srv}222 child := &surface{server: srv}
223 child.sub.parent, child.sub.synced = parent, true223 child.sub.parent, child.sub.synced = parent, true
224 parent.sub.children = []*surface{child}224 parent.sub.children = []*surface{child}
225- child.pendingScale = 2225+ child.next.scale = 2
226 child.queueUpdate()226 child.queueUpdate()
227 old := child.queue[0]227 old := child.queue[0]
228- parent.pendingScale = 3228+ parent.next.scale = 3
229 parent.queueUpdate()229 parent.queueUpdate()
230 pu := parent.queue[0]230 pu := parent.queue[0]
231 if old.refs != 1 || len(pu.deps) != 1 || pu.deps[0] != old {231 if old.refs != 1 || len(pu.deps) != 1 || pu.deps[0] != old {
@@ -239,7 +239,7 @@ func TestQueuedParentRetainsRetiredChild(t *testing.T) {
239 if !old.retired || old.refs != 1 {239 if !old.retired || old.refs != 1 {
240 t.Fatalf("retired child ref count: retired=%v refs=%d", old.retired, old.refs)240 t.Fatalf("retired child ref count: retired=%v refs=%d", old.retired, old.refs)
241 }241 }
242- child.pendingScale = 7242+ child.next.scale = 7
243 child.queueUpdate()243 child.queueUpdate()
244 if child.queue[0] == old || old.scale != 2 || pu.deps[0] != old {244 if child.queue[0] == old || old.scale != 2 || pu.deps[0] != old {
245 t.Fatal("live dependency was recycled as a new child commit")245 t.Fatal("live dependency was recycled as a new child commit")
M internal/adapters/wayland/frames.go +2 -1
@@ -2,10 +2,11 @@ package wayland
22
33 import (
44 "context"
5- "golang.org/x/sys/unix"
65 "slices"
76 "time"
87
8+ "golang.org/x/sys/unix"
9+
910 "github.com/bnema/neferwl/internal/ports"
1011
1112 "github.com/bnema/purego-libwayland/protocol/wayland"
@@ -2,10 +2,11 @@ package wayland
2 2
3 import (3 import (
4 "context"4 "context"
5- "golang.org/x/sys/unix"
6 "slices"5 "slices"
7 "time"6 "time"
8 7
8+ "golang.org/x/sys/unix"
9+
9 "github.com/bnema/neferwl/internal/ports"10 "github.com/bnema/neferwl/internal/ports"
10 11
11 "github.com/bnema/purego-libwayland/protocol/wayland"12 "github.com/bnema/purego-libwayland/protocol/wayland"
M internal/adapters/wayland/globals.go +15 -14
@@ -1,10 +1,11 @@
11 package wayland
22
33 import (
4+ "os"
5+
46 "github.com/bnema/neferwl/internal/ports"
57 "github.com/bnema/purego-libwayland/protocol/wayland"
68 "github.com/bnema/purego-libwayland/server"
7- "os"
89 )
910
1011 func registerGlobals(d *server.Display, o Options, s *Server) error {
@@ -55,7 +56,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
5556 r, e := wayland.NewSeat(c, int32(v), id, seat{s})
5657 if e == nil {
5758 capabilities := uint32(wayland.SeatCapabilityPointer)
58- if s.keymapFD >= 0 {
59+ if s.seat.keymapFD >= 0 {
5960 capabilities |= uint32(wayland.SeatCapabilityKeyboard)
6061 }
6162 r.SendCapabilities(capabilities)
@@ -383,9 +384,9 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
383384 return
384385 }
385386 s := h.server
386- s.pointers[r.Client()] = append(s.pointers[r.Client()], p)
387+ s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p)
387388 p.OnDestroy = func() {
388- list := s.pointers[r.Client()]
389+ list := s.seat.pointers[r.Client()]
389390 for i, item := range list {
390391 if item == p {
391392 list = append(list[:i], list[i+1:]...)
@@ -393,13 +394,13 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
393394 }
394395 }
395396 if len(list) == 0 {
396- delete(s.pointers, r.Client())
397+ delete(s.seat.pointers, r.Client())
397398 } else {
398- s.pointers[r.Client()] = list
399+ s.seat.pointers[r.Client()] = list
399400 }
400401 }
401402 if s.hasPointerFocus(r.Client()) {
402- if surf, _, x, y := s.pointerSurface(s.pointerFocus, s.pointerX, s.pointerY); surf != nil {
403+ if surf, _, x, y := s.pointerSurface(s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY); surf != nil {
403404 s.serial++
404405 p.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y))
405406 pointerFrame(p)
@@ -413,7 +414,7 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
413414 }
414415 s := h.server
415416 k.OnDestroy = func() {
416- list := s.keyboards[r.Client()]
417+ list := s.seat.keyboards[r.Client()]
417418 for i, item := range list {
418419 if item == k {
419420 list = append(list[:i], list[i+1:]...)
@@ -421,21 +422,21 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
421422 }
422423 }
423424 if len(list) == 0 {
424- delete(s.keyboards, r.Client())
425+ delete(s.seat.keyboards, r.Client())
425426 } else {
426- s.keyboards[r.Client()] = list
427+ s.seat.keyboards[r.Client()] = list
427428 }
428429 }
429- if s.keymapFD < 0 {
430+ if s.seat.keymapFD < 0 {
430431 return
431432 }
432- s.keyboards[r.Client()] = append(s.keyboards[r.Client()], k)
433+ s.seat.keyboards[r.Client()] = append(s.seat.keyboards[r.Client()], k)
433434 fd, size := s.currentKeymap()
434435 k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size)
435436 if r.Version() >= 4 {
436- k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
437+ k.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
437438 }
438- if surf, _ := s.focusTarget(s.focused); surf != nil && surf.Client() == r.Client() {
439+ if surf, _ := s.focusTarget(s.seat.focused); surf != nil && surf.Client() == r.Client() {
439440 s.serial++
440441 k.SendEnter(s.serial, surf, []byte{})
441442 s.sendModifiers(k)
@@ -1,10 +1,11 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "os"
5+
4 "github.com/bnema/neferwl/internal/ports"6 "github.com/bnema/neferwl/internal/ports"
5 "github.com/bnema/purego-libwayland/protocol/wayland"7 "github.com/bnema/purego-libwayland/protocol/wayland"
6 "github.com/bnema/purego-libwayland/server"8 "github.com/bnema/purego-libwayland/server"
7- "os"
8 )9 )
9 10
10 func registerGlobals(d *server.Display, o Options, s *Server) error {11 func registerGlobals(d *server.Display, o Options, s *Server) error {
@@ -55,7 +56,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
55 r, e := wayland.NewSeat(c, int32(v), id, seat{s})56 r, e := wayland.NewSeat(c, int32(v), id, seat{s})
56 if e == nil {57 if e == nil {
57 capabilities := uint32(wayland.SeatCapabilityPointer)58 capabilities := uint32(wayland.SeatCapabilityPointer)
58- if s.keymapFD >= 0 {59+ if s.seat.keymapFD >= 0 {
59 capabilities |= uint32(wayland.SeatCapabilityKeyboard)60 capabilities |= uint32(wayland.SeatCapabilityKeyboard)
60 }61 }
61 r.SendCapabilities(capabilities)62 r.SendCapabilities(capabilities)
@@ -383,9 +384,9 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
383 return384 return
384 }385 }
385 s := h.server386 s := h.server
386- s.pointers[r.Client()] = append(s.pointers[r.Client()], p)387+ s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p)
387 p.OnDestroy = func() {388 p.OnDestroy = func() {
388- list := s.pointers[r.Client()]389+ list := s.seat.pointers[r.Client()]
389 for i, item := range list {390 for i, item := range list {
390 if item == p {391 if item == p {
391 list = append(list[:i], list[i+1:]...)392 list = append(list[:i], list[i+1:]...)
@@ -393,13 +394,13 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
393 }394 }
394 }395 }
395 if len(list) == 0 {396 if len(list) == 0 {
396- delete(s.pointers, r.Client())397+ delete(s.seat.pointers, r.Client())
397 } else {398 } else {
398- s.pointers[r.Client()] = list399+ s.seat.pointers[r.Client()] = list
399 }400 }
400 }401 }
401 if s.hasPointerFocus(r.Client()) {402 if s.hasPointerFocus(r.Client()) {
402- if surf, _, x, y := s.pointerSurface(s.pointerFocus, s.pointerX, s.pointerY); surf != nil {403+ if surf, _, x, y := s.pointerSurface(s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY); surf != nil {
403 s.serial++404 s.serial++
404 p.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y))405 p.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y))
405 pointerFrame(p)406 pointerFrame(p)
@@ -413,7 +414,7 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
413 }414 }
414 s := h.server415 s := h.server
415 k.OnDestroy = func() {416 k.OnDestroy = func() {
416- list := s.keyboards[r.Client()]417+ list := s.seat.keyboards[r.Client()]
417 for i, item := range list {418 for i, item := range list {
418 if item == k {419 if item == k {
419 list = append(list[:i], list[i+1:]...)420 list = append(list[:i], list[i+1:]...)
@@ -421,21 +422,21 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
421 }422 }
422 }423 }
423 if len(list) == 0 {424 if len(list) == 0 {
424- delete(s.keyboards, r.Client())425+ delete(s.seat.keyboards, r.Client())
425 } else {426 } else {
426- s.keyboards[r.Client()] = list427+ s.seat.keyboards[r.Client()] = list
427 }428 }
428 }429 }
429- if s.keymapFD < 0 {430+ if s.seat.keymapFD < 0 {
430 return431 return
431 }432 }
432- s.keyboards[r.Client()] = append(s.keyboards[r.Client()], k)433+ s.seat.keyboards[r.Client()] = append(s.seat.keyboards[r.Client()], k)
433 fd, size := s.currentKeymap()434 fd, size := s.currentKeymap()
434 k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size)435 k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size)
435 if r.Version() >= 4 {436 if r.Version() >= 4 {
436- k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))437+ k.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
437 }438 }
438- if surf, _ := s.focusTarget(s.focused); surf != nil && surf.Client() == r.Client() {439+ if surf, _ := s.focusTarget(s.seat.focused); surf != nil && surf.Client() == r.Client() {
439 s.serial++440 s.serial++
440 k.SendEnter(s.serial, surf, []byte{})441 k.SendEnter(s.serial, surf, []byte{})
441 s.sendModifiers(k)442 s.sendModifiers(k)
M internal/adapters/wayland/input_region.go +5 -5
@@ -111,12 +111,12 @@ func (s *surface) emitInput() {
111111 }
112112
113113 func (s *surface) SetInputRegion(_ *wayland.Surface, reg *wayland.Region) {
114- s.pendingInputSet = true
115- s.pendingInputAll = reg == nil || reg.Resource == nil
116- s.pendingInputRects = nil
117- if !s.pendingInputAll {
114+ s.next.inputSet = true
115+ s.next.inputAll = reg == nil || reg.Resource == nil
116+ s.next.inputRects = nil
117+ if !s.next.inputAll {
118118 if g := s.server.regions[reg.Resource]; g != nil {
119- s.pendingInputRects = slices.Clone(g.rects)
119+ s.next.inputRects = slices.Clone(g.rects)
120120 }
121121 }
122122 }
@@ -111,12 +111,12 @@ func (s *surface) emitInput() {
111 }111 }
112 112
113 func (s *surface) SetInputRegion(_ *wayland.Surface, reg *wayland.Region) {113 func (s *surface) SetInputRegion(_ *wayland.Surface, reg *wayland.Region) {
114- s.pendingInputSet = true114+ s.next.inputSet = true
115- s.pendingInputAll = reg == nil || reg.Resource == nil115+ s.next.inputAll = reg == nil || reg.Resource == nil
116- s.pendingInputRects = nil116+ s.next.inputRects = nil
117- if !s.pendingInputAll {117+ if !s.next.inputAll {
118 if g := s.server.regions[reg.Resource]; g != nil {118 if g := s.server.regions[reg.Resource]; g != nil {
119- s.pendingInputRects = slices.Clone(g.rects)119+ s.next.inputRects = slices.Clone(g.rects)
120 }120 }
121 }121 }
122 }122 }
M internal/adapters/wayland/layer.go +7 -6
@@ -1,12 +1,13 @@
11 package wayland
22
33 import (
4+ "sort"
5+
46 "github.com/bnema/neferwl/internal/ports"
57 "github.com/bnema/purego-libwayland/protocol/wayland"
68 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
79 "github.com/bnema/purego-libwayland/protocol/xdgshell"
810 "github.com/bnema/purego-libwayland/server"
9- "sort"
1011 )
1112
1213 type layerShell struct{ server *Server }
@@ -43,7 +44,7 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
4344 return
4445 }
4546 state := h.server.surfaces[w.Resource]
46- if state.kind != roleNone || (state.attached && state.pending != nil) || state.current != nil {
47+ if state.kind != roleNone || (state.next.attached && state.next.buffer != nil) || state.current != nil {
4748 r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed")
4849 return
4950 }
@@ -77,8 +78,8 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
7778 state.layer = nil
7879 state.role = nil
7980 state.dropQueue()
80- state.current, state.pending = nil, nil
81- state.attached = false
81+ state.current, state.next.buffer = nil, nil
82+ state.next.attached = false
8283 }
8384 }
8485 func (l *layerSurface) SetSize(_ *wlrlayershell.ZwlrLayerSurfaceV1, w, h uint32) {
@@ -156,10 +157,10 @@ func (l *layerSurface) unmap() {
156157 if !l.mapped {
157158 return
158159 }
159- if s := l.shell.server; s.focused == l.id {
160+ if s := l.shell.server; s.seat.focused == l.id {
160161 s.changeFocus(0)
161162 }
162- if s := l.shell.server; s.pointerFocus == l.id {
163+ if s := l.shell.server; s.seat.pointerFocus == l.id {
163164 s.changePointerFocus(0, 0, 0)
164165 }
165166 l.mapped = false
@@ -1,12 +1,13 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "sort"
5+
4 "github.com/bnema/neferwl/internal/ports"6 "github.com/bnema/neferwl/internal/ports"
5 "github.com/bnema/purego-libwayland/protocol/wayland"7 "github.com/bnema/purego-libwayland/protocol/wayland"
6 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"8 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
7 "github.com/bnema/purego-libwayland/protocol/xdgshell"9 "github.com/bnema/purego-libwayland/protocol/xdgshell"
8 "github.com/bnema/purego-libwayland/server"10 "github.com/bnema/purego-libwayland/server"
9- "sort"
10 )11 )
11 12
12 type layerShell struct{ server *Server }13 type layerShell struct{ server *Server }
@@ -43,7 +44,7 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
43 return44 return
44 }45 }
45 state := h.server.surfaces[w.Resource]46 state := h.server.surfaces[w.Resource]
46- if state.kind != roleNone || (state.attached && state.pending != nil) || state.current != nil {47+ if state.kind != roleNone || (state.next.attached && state.next.buffer != nil) || state.current != nil {
47 r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed")48 r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed")
48 return49 return
49 }50 }
@@ -77,8 +78,8 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
77 state.layer = nil78 state.layer = nil
78 state.role = nil79 state.role = nil
79 state.dropQueue()80 state.dropQueue()
80- state.current, state.pending = nil, nil81+ state.current, state.next.buffer = nil, nil
81- state.attached = false82+ state.next.attached = false
82 }83 }
83 }84 }
84 func (l *layerSurface) SetSize(_ *wlrlayershell.ZwlrLayerSurfaceV1, w, h uint32) {85 func (l *layerSurface) SetSize(_ *wlrlayershell.ZwlrLayerSurfaceV1, w, h uint32) {
@@ -156,10 +157,10 @@ func (l *layerSurface) unmap() {
156 if !l.mapped {157 if !l.mapped {
157 return158 return
158 }159 }
159- if s := l.shell.server; s.focused == l.id {160+ if s := l.shell.server; s.seat.focused == l.id {
160 s.changeFocus(0)161 s.changeFocus(0)
161 }162 }
162- if s := l.shell.server; s.pointerFocus == l.id {163+ if s := l.shell.server; s.seat.pointerFocus == l.id {
163 s.changePointerFocus(0, 0, 0)164 s.changePointerFocus(0, 0, 0)
164 }165 }
165 l.mapped = false166 l.mapped = false
M internal/adapters/wayland/layer_test.go +5 -4
@@ -2,16 +2,17 @@ package wayland
22
33 import (
44 "fmt"
5+ "os"
6+ "os/exec"
7+ "testing"
8+ "time"
9+
510 "github.com/bnema/neferwl/internal/ports"
611 "github.com/bnema/purego-libwayland/protocol/wayland"
712 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
813 "github.com/bnema/purego-libwayland/protocol/xdgshell"
914 "github.com/bnema/wlturbo"
1015 "golang.org/x/sys/unix"
11- "os"
12- "os/exec"
13- "testing"
14- "time"
1516 )
1617
1718 type layerProxy struct {
@@ -2,16 +2,17 @@ package wayland
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "os"
6+ "os/exec"
7+ "testing"
8+ "time"
9+
5 "github.com/bnema/neferwl/internal/ports"10 "github.com/bnema/neferwl/internal/ports"
6 "github.com/bnema/purego-libwayland/protocol/wayland"11 "github.com/bnema/purego-libwayland/protocol/wayland"
7 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"12 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
8 "github.com/bnema/purego-libwayland/protocol/xdgshell"13 "github.com/bnema/purego-libwayland/protocol/xdgshell"
9 "github.com/bnema/wlturbo"14 "github.com/bnema/wlturbo"
10 "golang.org/x/sys/unix"15 "golang.org/x/sys/unix"
11- "os"
12- "os/exec"
13- "testing"
14- "time"
15 )16 )
16 17
17 type layerProxy struct {18 type layerProxy struct {
M internal/adapters/wayland/popup.go +3 -3
@@ -82,8 +82,8 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
8282 w.unmap()
8383 pp.done = true
8484 x.surface.dropQueue()
85- x.surface.current, x.surface.pending = nil, nil
86- x.surface.attached = false
85+ x.surface.current, x.surface.next.buffer = nil, nil
86+ x.surface.next.attached = false
8787 delete(s.windows, w.id)
8888 x.window = nil
8989 x.surface.role = nil
@@ -115,7 +115,7 @@ func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) {
115115 // Only a grab answering the client's last press may take the keyboard,
116116 // and a popup over a popup grabs only if its parent does.
117117 s := p.w.xdg.server
118- fresh := serial == s.press && s.pressClient == r.Client()
118+ fresh := serial == s.seat.press && s.seat.pressClient == r.Client()
119119 if !fresh || p.parentID() == 0 || !s.topGrab(r.Client(), p.parent) {
120120 p.dismiss()
121121 return
@@ -82,8 +82,8 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
82 w.unmap()82 w.unmap()
83 pp.done = true83 pp.done = true
84 x.surface.dropQueue()84 x.surface.dropQueue()
85- x.surface.current, x.surface.pending = nil, nil85+ x.surface.current, x.surface.next.buffer = nil, nil
86- x.surface.attached = false86+ x.surface.next.attached = false
87 delete(s.windows, w.id)87 delete(s.windows, w.id)
88 x.window = nil88 x.window = nil
89 x.surface.role = nil89 x.surface.role = nil
@@ -115,7 +115,7 @@ func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) {
115 // Only a grab answering the client's last press may take the keyboard,115 // Only a grab answering the client's last press may take the keyboard,
116 // and a popup over a popup grabs only if its parent does.116 // and a popup over a popup grabs only if its parent does.
117 s := p.w.xdg.server117 s := p.w.xdg.server
118- fresh := serial == s.press && s.pressClient == r.Client()118+ fresh := serial == s.seat.press && s.seat.pressClient == r.Client()
119 if !fresh || p.parentID() == 0 || !s.topGrab(r.Client(), p.parent) {119 if !fresh || p.parentID() == 0 || !s.topGrab(r.Client(), p.parent) {
120 p.dismiss()120 p.dismiss()
121 return121 return
M internal/adapters/wayland/presentation.go +1 -1
@@ -63,7 +63,7 @@ func (p presentation) Feedback(r *presentationtime.WpPresentation, surf *wayland
6363 fb.Destroy()
6464 return
6565 }
66- state.pendingFeedback = append(state.pendingFeedback, fb)
66+ state.next.feedback = append(state.next.feedback, fb)
6767 }
6868
6969 type feedbackHandler struct{}
@@ -63,7 +63,7 @@ func (p presentation) Feedback(r *presentationtime.WpPresentation, surf *wayland
63 fb.Destroy()63 fb.Destroy()
64 return64 return
65 }65 }
66- state.pendingFeedback = append(state.pendingFeedback, fb)66+ state.next.feedback = append(state.next.feedback, fb)
67 }67 }
68 68
69 type feedbackHandler struct{}69 type feedbackHandler struct{}
M internal/adapters/wayland/protocol_test.go +2 -1
@@ -4,12 +4,13 @@ import (
44 "bytes"
55 "errors"
66 "fmt"
7- "github.com/bnema/neferwl/internal/ports"
87 "os"
98 "path/filepath"
109 "testing"
1110 "time"
1211
12+ "github.com/bnema/neferwl/internal/ports"
13+
1314 "github.com/bnema/purego-libwayland/protocol/wayland"
1415 "github.com/bnema/purego-libwayland/protocol/xdgshell"
1516 "github.com/bnema/wlturbo"
@@ -4,12 +4,13 @@ import (
4 "bytes"4 "bytes"
5 "errors"5 "errors"
6 "fmt"6 "fmt"
7- "github.com/bnema/neferwl/internal/ports"
8 "os"7 "os"
9 "path/filepath"8 "path/filepath"
10 "testing"9 "testing"
11 "time"10 "time"
12 11
12+ "github.com/bnema/neferwl/internal/ports"
13+
13 "github.com/bnema/purego-libwayland/protocol/wayland"14 "github.com/bnema/purego-libwayland/protocol/wayland"
14 "github.com/bnema/purego-libwayland/protocol/xdgshell"15 "github.com/bnema/purego-libwayland/protocol/xdgshell"
15 "github.com/bnema/wlturbo"16 "github.com/bnema/wlturbo"
M internal/adapters/wayland/release.go +1 -1
@@ -125,7 +125,7 @@ func (s *Server) bufferReferenced(b *wayland.Buffer) bool {
125125 return false
126126 }
127127 for _, surf := range s.surfaces {
128- if surf.attached && sameBuffer(surf.pending, b) {
128+ if surf.next.attached && sameBuffer(surf.next.buffer, b) {
129129 return true
130130 }
131131 for _, u := range surf.queue {
@@ -125,7 +125,7 @@ func (s *Server) bufferReferenced(b *wayland.Buffer) bool {
125 return false125 return false
126 }126 }
127 for _, surf := range s.surfaces {127 for _, surf := range s.surfaces {
128- if surf.attached && sameBuffer(surf.pending, b) {128+ if surf.next.attached && sameBuffer(surf.next.buffer, b) {
129 return true129 return true
130 }130 }
131 for _, u := range surf.queue {131 for _, u := range surf.queue {
M internal/adapters/wayland/screencopy_test.go +3 -2
@@ -3,8 +3,6 @@ package wayland
33 import (
44 "context"
55 "fmt"
6- source "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
7- ext "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
86 "image"
97 "reflect"
108 "slices"
@@ -12,6 +10,9 @@ import (
1210 "testing"
1311 "time"
1412
13+ source "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
14+ ext "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
15+
1516 "github.com/bnema/neferwl/internal/logging"
1617 "github.com/bnema/neferwl/internal/ports"
1718 "github.com/bnema/purego-libwayland/protocol/wayland"
@@ -3,8 +3,6 @@ package wayland
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6- source "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
7- ext "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
8 "image"6 "image"
9 "reflect"7 "reflect"
10 "slices"8 "slices"
@@ -12,6 +10,9 @@ import (
12 "testing"10 "testing"
13 "time"11 "time"
14 12
13+ source "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
14+ ext "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
15+
15 "github.com/bnema/neferwl/internal/logging"16 "github.com/bnema/neferwl/internal/logging"
16 "github.com/bnema/neferwl/internal/ports"17 "github.com/bnema/neferwl/internal/ports"
17 "github.com/bnema/purego-libwayland/protocol/wayland"18 "github.com/bnema/purego-libwayland/protocol/wayland"
A internal/adapters/wayland/seat.go +445 -0
@@ -0,0 +1,445 @@
1+package wayland
2+
3+import (
4+ "encoding/binary"
5+ "fmt"
6+ "time"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+ "github.com/bnema/purego-libwayland/server"
11+ "golang.org/x/sys/unix"
12+)
13+
14+// seatState is the wl_seat state: who has keyboard and pointer focus, the
15+// bound keyboards and pointers, the keymap they carry, and the last press.
16+// It belongs to the display goroutine.
17+type seatState struct {
18+ focused ports.WindowID
19+ pointerFocus ports.WindowID
20+ // pointerX, pointerY is the pointer as core placed it in pointerFocus;
21+ // pointerSurface and surfacePoint turn it into surface coordinates.
22+ pointerX, pointerY float64
23+ wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
24+ wheelHeld [2]float64 // axis value held back with it for pre-v8 clients
25+ pointers map[server.Client][]*wayland.Pointer
26+ keyboards map[server.Client][]*wayland.Keyboard
27+ modState ports.ModState
28+ heldKeys map[uint32]bool
29+ grabKeys map[uint32]bool // pressed through the input method grab
30+ keymapFD int
31+ keymapSize uint32
32+ keymapText string // the seat keymap, to compare virtual keymaps with
33+ keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
34+ repeatRate int
35+ repeatDelay int
36+ // press is the serial of the last button or key press, sent to
37+ // pressClient: popup grabs must come from it.
38+ press uint32
39+ pressClient server.Client
40+ // pressAt and focusAt date the last press and keyboard focus change,
41+ // for xdg-activation tokens.
42+ pressAt, focusAt time.Time
43+}
44+
45+// applyInput delivers a core input command to the focused client.
46+func (s *Server) applyInput(cmd ports.ClientCommand) {
47+ switch c := cmd.(type) {
48+ case ports.PointerFocus:
49+ s.changePointerFocus(c.ID, c.X, c.Y)
50+ case ports.PointerMotionTo:
51+ if l := s.layers[c.ID]; l != nil && c.ID == s.seat.pointerFocus {
52+ s.seat.pointerX, s.seat.pointerY = c.X, c.Y
53+ for _, p := range s.layerPointers(l) {
54+ p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
55+ pointerFrame(p)
56+ }
57+ return
58+ }
59+ if w := s.windows[c.ID]; w != nil && c.ID == s.seat.pointerFocus {
60+ if s.relativeMotion(w, c) && s.locked(w) {
61+ for _, p := range s.windowPointers(w) {
62+ pointerFrame(p)
63+ }
64+ }
65+ if s.locked(w) {
66+ // Only relative motion reaches a locked pointer.
67+ return
68+ }
69+ x, y := w.surfacePoint(c.X, c.Y)
70+ s.seat.pointerX, s.seat.pointerY = c.X, c.Y
71+ // A constraint waits for the pointer to enter its region.
72+ if s.constraint == nil {
73+ s.updateConstraint()
74+ }
75+ for _, p := range s.windowPointers(w) {
76+ p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))
77+ pointerFrame(p)
78+ }
79+ }
80+ case ports.PointerButtonTo:
81+ // Releases may target the implicit-grab window after focus has moved.
82+ if client, pointers, ok := s.pointerTarget(c.ID); ok {
83+ s.serial++
84+ state := uint32(0)
85+ if c.Pressed {
86+ state = 1
87+ s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()
88+ }
89+ for _, p := range pointers {
90+ p.SendButton(s.serial, c.TimeMsec, c.Button, state)
91+ pointerFrame(p)
92+ }
93+ }
94+ case ports.PointerAxisTo:
95+ // Scroll applies to the entered surface only.
96+ if c.ID != s.seat.pointerFocus {
97+ return
98+ }
99+ steps, values := s.seat.wheelSteps(c.Axis)
100+ _, pointers, _ := s.pointerTarget(c.ID)
101+ for _, p := range pointers {
102+ sendAxis(p, c.Axis, steps, values)
103+ }
104+ case ports.SetKeymap:
105+ s.setKeymap(c)
106+ case ports.FocusWindow:
107+ if s.seat.focused != c.ID {
108+ s.changeFocus(c.ID)
109+ }
110+ case ports.ForwardKey:
111+ if s.grabKey(c) {
112+ return
113+ }
114+ _, keyboards := s.focusTarget(c.ID)
115+ if c.ID != s.seat.focused || len(keyboards) == 0 {
116+ s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")
117+ return
118+ }
119+ s.useKeymap(nil)
120+ if s.seat.heldKeys == nil {
121+ s.seat.heldKeys = make(map[uint32]bool)
122+ }
123+ if c.Key.Pressed {
124+ s.seat.heldKeys[c.Key.Keycode] = true
125+ } else {
126+ delete(s.seat.heldKeys, c.Key.Keycode)
127+ }
128+ s.serial++
129+ state := uint32(0)
130+ if c.Key.Pressed {
131+ state = 1
132+ }
133+ if c.Key.Pressed {
134+ if surf, _ := s.focusTarget(c.ID); surf != nil {
135+ s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, surf.Client(), time.Now()
136+ }
137+ }
138+ for _, k := range keyboards {
139+ k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
140+ }
141+ if c.Key.State != s.seat.modState {
142+ s.seat.modState = c.Key.State
143+ s.serial++
144+ for _, k := range keyboards {
145+ s.sendModifiers(k)
146+ }
147+ }
148+ }
149+}
150+
151+func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
152+ if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
153+ return nil
154+ }
155+ return s.clientKeyboards(w.xdg.resource.Client())
156+}
157+
158+func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard {
159+ var alive []*wayland.Keyboard
160+ for _, k := range s.seat.keyboards[c] {
161+ if k.Resource.Alive() {
162+ alive = append(alive, k)
163+ }
164+ }
165+ return alive
166+}
167+
168+// focusTarget resolves a window or layer ID to its wl_surface and keyboards.
169+func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Keyboard) {
170+ if w := s.windows[id]; w != nil && w.mapped {
171+ if surf := w.xdg.surfaceResource(); surf != nil {
172+ return surf, s.windowKeyboards(w)
173+ }
174+ }
175+ if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
176+ if surf := l.surfaceResource(); surf != nil {
177+ return surf, s.clientKeyboards(l.resource.Client())
178+ }
179+ }
180+ return nil, nil
181+}
182+
183+// pointerTarget resolves a mapped window or layer ID to its client and
184+// live pointers.
185+func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Pointer, bool) {
186+ if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() {
187+ return w.xdg.resource.Client(), s.windowPointers(w), true
188+ }
189+ if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
190+ return l.resource.Client(), s.layerPointers(l), true
191+ }
192+ return server.Client{}, nil, false
193+}
194+
195+// pointerSurface is the wl_surface of a mapped window or layer and the
196+// point in its surface coordinates.
197+func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surface, []*wayland.Pointer, float64, float64) {
198+ if w := s.windows[id]; w != nil {
199+ if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() {
200+ x, y := w.surfacePoint(x, y)
201+ return surf, s.windowPointers(w), x, y
202+ }
203+ }
204+ if l := s.layers[id]; l != nil && l.mapped {
205+ if surf := l.surfaceResource(); surf != nil {
206+ return surf, s.layerPointers(l), x, y
207+ }
208+ }
209+ return nil, nil, 0, 0
210+}
211+
212+// keymapFile writes a sealed memfd holding the NUL-terminated keymap.
213+func keymapFile(keymap string) (int, uint32, error) {
214+ if uint64(len(keymap))+1 > uint64(^uint32(0)) {
215+ return -1, 0, fmt.Errorf("keymap too large")
216+ }
217+ fd, err := unix.MemfdCreate("neferwl-keymap", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
218+ if err != nil {
219+ return -1, 0, err
220+ }
221+ n, err := unix.Write(fd, append([]byte(keymap), 0))
222+ if err == nil && n != len(keymap)+1 {
223+ err = fmt.Errorf("short keymap write")
224+ }
225+ if err == nil {
226+ _, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE|unix.F_SEAL_SEAL)
227+ }
228+ if err != nil {
229+ unix.Close(fd)
230+ return -1, 0, err
231+ }
232+ return fd, uint32(n), nil
233+}
234+
235+// setKeymap sends repeat info, and a new keymap when one is given, to every keyboard.
236+// A new keymap resets held keys and modifiers: the focused client gets leave, then
237+// enter with no keys.
238+func (s *Server) setKeymap(c ports.SetKeymap) {
239+ s.seat.repeatRate, s.seat.repeatDelay = c.RepeatRate, c.RepeatDelay
240+ if c.Keymap == "" {
241+ for _, list := range s.seat.keyboards {
242+ for _, k := range list {
243+ if k.Version() >= 4 {
244+ k.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
245+ }
246+ }
247+ }
248+ s.sendGrabKeymap(false)
249+ s.log.Info().Int("rate", s.seat.repeatRate).Int("delay", s.seat.repeatDelay).Msg("repeat updated")
250+ return
251+ }
252+ fd, size, err := keymapFile(c.Keymap)
253+ if err != nil {
254+ s.log.Warn().Err(err).Msg("keymap update failed")
255+ return
256+ }
257+ if s.seat.keymapFD >= 0 {
258+ unix.Close(s.seat.keymapFD)
259+ }
260+ s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, c.Keymap
261+ s.seat.keymapOwner = nil
262+ focused := s.seat.focused
263+ s.changeFocus(0)
264+ s.seat.heldKeys, s.seat.grabKeys = nil, nil
265+ s.seat.modState = ports.ModState{}
266+ for _, list := range s.seat.keyboards {
267+ for _, k := range list {
268+ k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.seat.keymapFD, s.seat.keymapSize)
269+ if k.Version() >= 4 {
270+ k.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
271+ }
272+ }
273+ }
274+ s.sendGrabKeymap(true)
275+ s.changeFocus(focused)
276+ s.log.Info().Uint32("size", size).Int("rate", s.seat.repeatRate).Int("delay", s.seat.repeatDelay).Msg("keymap updated")
277+}
278+
279+func (s *Server) sendModifiers(k *wayland.Keyboard) {
280+ m := s.seat.modState
281+ k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
282+}
283+
284+func (s *Server) changeFocus(id ports.WindowID) {
285+ // Enter carries seat held keys and modifiers: they need the seat keymap.
286+ s.useKeymap(nil)
287+ old := s.focusClient()
288+ defer func() {
289+ // Tokens of a client losing the focus die with it.
290+ if now := s.focusClient(); now != old {
291+ s.dropTokens(old)
292+ s.seat.focusAt = time.Now()
293+ }
294+ }()
295+ if surf, keyboards := s.focusTarget(s.seat.focused); surf != nil {
296+ for _, k := range keyboards {
297+ s.serial++
298+ k.SendLeave(s.serial, surf)
299+ }
300+ }
301+ s.seat.focused = 0
302+ if surf, keyboards := s.focusTarget(id); surf != nil {
303+ s.seat.focused = id
304+ s.focusSelections()
305+ for _, k := range keyboards {
306+ s.serial++
307+ var keys []byte
308+ for code := range s.seat.heldKeys {
309+ keys = binary.LittleEndian.AppendUint32(keys, code)
310+ }
311+ k.SendEnter(s.serial, surf, keys)
312+ s.sendModifiers(k)
313+ }
314+ }
315+ s.textInputFocus()
316+ s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus")
317+ s.updateConstraint()
318+}
319+
320+// wheelSteps adds wheel v120 to the rest per axis and returns the whole
321+// detents for axis_discrete (seat before v8). High-resolution wheels send
322+// fractions of 120; a direction change drops the rest, as wlroots does.
323+// wheelHeld keeps the axis value of the frames without a step; values is
324+// what a pre-v8 client gets with a step: the held value plus this frame's.
325+func (st *seatState) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2]float64) {
326+ if a.Source != ports.AxisWheel {
327+ return steps, values
328+ }
329+ for i, ax := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {
330+ if !ax.Set || ax.V120 == 0 {
331+ continue
332+ }
333+ if (st.wheelRest[i] < 0) != (ax.V120 < 0) {
334+ st.wheelRest[i], st.wheelHeld[i] = 0, 0
335+ }
336+ st.wheelRest[i] += ax.V120
337+ st.wheelHeld[i] += ax.Value
338+ steps[i] = st.wheelRest[i] / 120
339+ st.wheelRest[i] -= steps[i] * 120
340+ if steps[i] != 0 {
341+ values[i], st.wheelHeld[i] = st.wheelHeld[i], 0
342+ }
343+ }
344+ return steps, values
345+}
346+
347+// sendAxis sends one scroll frame, each event gated by the pointer version.
348+// steps are whole wheel detents for axis_discrete. A pre-v8 client gets a
349+// wheel axis only with a step, carrying values (all the value since the
350+// last step), so it never counts smooth and discrete scroll.
351+func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {
352+ v := p.Version()
353+ axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal}
354+ for i := range axes {
355+ if v < 8 && a.Source == ports.AxisWheel && axes[i].V120 != 0 && steps[i] == 0 {
356+ axes[i].Set = false
357+ }
358+ }
359+ if !axes[0].Set && !axes[1].Set {
360+ return
361+ }
362+ if v >= 5 {
363+ p.SendAxisSource(uint32(a.Source))
364+ }
365+ for axis, s := range axes {
366+ if !s.Set {
367+ continue
368+ }
369+ if s.Stop {
370+ if v >= 5 {
371+ p.SendAxisStop(a.TimeMsec, uint32(axis))
372+ }
373+ continue
374+ }
375+ value := s.Value
376+ if a.Source == ports.AxisWheel && s.V120 != 0 {
377+ switch {
378+ case v >= 8:
379+ p.SendAxisValue120(uint32(axis), s.V120)
380+ default:
381+ value = values[axis]
382+ if v >= 5 {
383+ p.SendAxisDiscrete(uint32(axis), steps[axis])
384+ }
385+ }
386+ }
387+ p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value))
388+ }
389+ pointerFrame(p)
390+}
391+
392+func pointerFrame(p *wayland.Pointer) {
393+ if p.Version() >= 5 {
394+ p.SendFrame()
395+ }
396+}
397+func (s *Server) windowPointers(w *window) []*wayland.Pointer {
398+ if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
399+ return nil
400+ }
401+ return s.clientPointers(w.xdg.resource.Client())
402+}
403+
404+func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer {
405+ if l == nil || !l.mapped || !l.resource.Resource.Alive() {
406+ return nil
407+ }
408+ return s.clientPointers(l.resource.Client())
409+}
410+
411+func (s *Server) clientPointers(c server.Client) []*wayland.Pointer {
412+ var result []*wayland.Pointer
413+ for _, p := range s.seat.pointers[c] {
414+ if p.Resource.Alive() {
415+ result = append(result, p)
416+ }
417+ }
418+ return result
419+}
420+func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
421+ if id == s.seat.pointerFocus {
422+ return
423+ }
424+ s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
425+ // The new client sets its own cursor on enter; until then, the arrow.
426+ s.cursorSurface = nil
427+ s.setCursor(ports.CursorChange{})
428+ if surface, pointers, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0); surface != nil {
429+ for _, p := range pointers {
430+ s.serial++
431+ p.SendLeave(s.serial, surface)
432+ pointerFrame(p)
433+ }
434+ }
435+ s.seat.pointerFocus = 0
436+ if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil {
437+ s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY = id, x, y
438+ for _, p := range pointers {
439+ s.serial++
440+ p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy))
441+ pointerFrame(p)
442+ }
443+ }
444+ s.updateConstraint()
445+}
@@ -0,0 +1,445 @@
1+package wayland
2+
3+import (
4+ "encoding/binary"
5+ "fmt"
6+ "time"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+ "github.com/bnema/purego-libwayland/server"
11+ "golang.org/x/sys/unix"
12+)
13+
14+// seatState is the wl_seat state: who has keyboard and pointer focus, the
15+// bound keyboards and pointers, the keymap they carry, and the last press.
16+// It belongs to the display goroutine.
17+type seatState struct {
18+ focused ports.WindowID
19+ pointerFocus ports.WindowID
20+ // pointerX, pointerY is the pointer as core placed it in pointerFocus;
21+ // pointerSurface and surfacePoint turn it into surface coordinates.
22+ pointerX, pointerY float64
23+ wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
24+ wheelHeld [2]float64 // axis value held back with it for pre-v8 clients
25+ pointers map[server.Client][]*wayland.Pointer
26+ keyboards map[server.Client][]*wayland.Keyboard
27+ modState ports.ModState
28+ heldKeys map[uint32]bool
29+ grabKeys map[uint32]bool // pressed through the input method grab
30+ keymapFD int
31+ keymapSize uint32
32+ keymapText string // the seat keymap, to compare virtual keymaps with
33+ keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
34+ repeatRate int
35+ repeatDelay int
36+ // press is the serial of the last button or key press, sent to
37+ // pressClient: popup grabs must come from it.
38+ press uint32
39+ pressClient server.Client
40+ // pressAt and focusAt date the last press and keyboard focus change,
41+ // for xdg-activation tokens.
42+ pressAt, focusAt time.Time
43+}
44+
45+// applyInput delivers a core input command to the focused client.
46+func (s *Server) applyInput(cmd ports.ClientCommand) {
47+ switch c := cmd.(type) {
48+ case ports.PointerFocus:
49+ s.changePointerFocus(c.ID, c.X, c.Y)
50+ case ports.PointerMotionTo:
51+ if l := s.layers[c.ID]; l != nil && c.ID == s.seat.pointerFocus {
52+ s.seat.pointerX, s.seat.pointerY = c.X, c.Y
53+ for _, p := range s.layerPointers(l) {
54+ p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
55+ pointerFrame(p)
56+ }
57+ return
58+ }
59+ if w := s.windows[c.ID]; w != nil && c.ID == s.seat.pointerFocus {
60+ if s.relativeMotion(w, c) && s.locked(w) {
61+ for _, p := range s.windowPointers(w) {
62+ pointerFrame(p)
63+ }
64+ }
65+ if s.locked(w) {
66+ // Only relative motion reaches a locked pointer.
67+ return
68+ }
69+ x, y := w.surfacePoint(c.X, c.Y)
70+ s.seat.pointerX, s.seat.pointerY = c.X, c.Y
71+ // A constraint waits for the pointer to enter its region.
72+ if s.constraint == nil {
73+ s.updateConstraint()
74+ }
75+ for _, p := range s.windowPointers(w) {
76+ p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))
77+ pointerFrame(p)
78+ }
79+ }
80+ case ports.PointerButtonTo:
81+ // Releases may target the implicit-grab window after focus has moved.
82+ if client, pointers, ok := s.pointerTarget(c.ID); ok {
83+ s.serial++
84+ state := uint32(0)
85+ if c.Pressed {
86+ state = 1
87+ s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()
88+ }
89+ for _, p := range pointers {
90+ p.SendButton(s.serial, c.TimeMsec, c.Button, state)
91+ pointerFrame(p)
92+ }
93+ }
94+ case ports.PointerAxisTo:
95+ // Scroll applies to the entered surface only.
96+ if c.ID != s.seat.pointerFocus {
97+ return
98+ }
99+ steps, values := s.seat.wheelSteps(c.Axis)
100+ _, pointers, _ := s.pointerTarget(c.ID)
101+ for _, p := range pointers {
102+ sendAxis(p, c.Axis, steps, values)
103+ }
104+ case ports.SetKeymap:
105+ s.setKeymap(c)
106+ case ports.FocusWindow:
107+ if s.seat.focused != c.ID {
108+ s.changeFocus(c.ID)
109+ }
110+ case ports.ForwardKey:
111+ if s.grabKey(c) {
112+ return
113+ }
114+ _, keyboards := s.focusTarget(c.ID)
115+ if c.ID != s.seat.focused || len(keyboards) == 0 {
116+ s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")
117+ return
118+ }
119+ s.useKeymap(nil)
120+ if s.seat.heldKeys == nil {
121+ s.seat.heldKeys = make(map[uint32]bool)
122+ }
123+ if c.Key.Pressed {
124+ s.seat.heldKeys[c.Key.Keycode] = true
125+ } else {
126+ delete(s.seat.heldKeys, c.Key.Keycode)
127+ }
128+ s.serial++
129+ state := uint32(0)
130+ if c.Key.Pressed {
131+ state = 1
132+ }
133+ if c.Key.Pressed {
134+ if surf, _ := s.focusTarget(c.ID); surf != nil {
135+ s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, surf.Client(), time.Now()
136+ }
137+ }
138+ for _, k := range keyboards {
139+ k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
140+ }
141+ if c.Key.State != s.seat.modState {
142+ s.seat.modState = c.Key.State
143+ s.serial++
144+ for _, k := range keyboards {
145+ s.sendModifiers(k)
146+ }
147+ }
148+ }
149+}
150+
151+func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
152+ if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
153+ return nil
154+ }
155+ return s.clientKeyboards(w.xdg.resource.Client())
156+}
157+
158+func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard {
159+ var alive []*wayland.Keyboard
160+ for _, k := range s.seat.keyboards[c] {
161+ if k.Resource.Alive() {
162+ alive = append(alive, k)
163+ }
164+ }
165+ return alive
166+}
167+
168+// focusTarget resolves a window or layer ID to its wl_surface and keyboards.
169+func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Keyboard) {
170+ if w := s.windows[id]; w != nil && w.mapped {
171+ if surf := w.xdg.surfaceResource(); surf != nil {
172+ return surf, s.windowKeyboards(w)
173+ }
174+ }
175+ if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
176+ if surf := l.surfaceResource(); surf != nil {
177+ return surf, s.clientKeyboards(l.resource.Client())
178+ }
179+ }
180+ return nil, nil
181+}
182+
183+// pointerTarget resolves a mapped window or layer ID to its client and
184+// live pointers.
185+func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Pointer, bool) {
186+ if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() {
187+ return w.xdg.resource.Client(), s.windowPointers(w), true
188+ }
189+ if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
190+ return l.resource.Client(), s.layerPointers(l), true
191+ }
192+ return server.Client{}, nil, false
193+}
194+
195+// pointerSurface is the wl_surface of a mapped window or layer and the
196+// point in its surface coordinates.
197+func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surface, []*wayland.Pointer, float64, float64) {
198+ if w := s.windows[id]; w != nil {
199+ if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() {
200+ x, y := w.surfacePoint(x, y)
201+ return surf, s.windowPointers(w), x, y
202+ }
203+ }
204+ if l := s.layers[id]; l != nil && l.mapped {
205+ if surf := l.surfaceResource(); surf != nil {
206+ return surf, s.layerPointers(l), x, y
207+ }
208+ }
209+ return nil, nil, 0, 0
210+}
211+
212+// keymapFile writes a sealed memfd holding the NUL-terminated keymap.
213+func keymapFile(keymap string) (int, uint32, error) {
214+ if uint64(len(keymap))+1 > uint64(^uint32(0)) {
215+ return -1, 0, fmt.Errorf("keymap too large")
216+ }
217+ fd, err := unix.MemfdCreate("neferwl-keymap", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
218+ if err != nil {
219+ return -1, 0, err
220+ }
221+ n, err := unix.Write(fd, append([]byte(keymap), 0))
222+ if err == nil && n != len(keymap)+1 {
223+ err = fmt.Errorf("short keymap write")
224+ }
225+ if err == nil {
226+ _, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE|unix.F_SEAL_SEAL)
227+ }
228+ if err != nil {
229+ unix.Close(fd)
230+ return -1, 0, err
231+ }
232+ return fd, uint32(n), nil
233+}
234+
235+// setKeymap sends repeat info, and a new keymap when one is given, to every keyboard.
236+// A new keymap resets held keys and modifiers: the focused client gets leave, then
237+// enter with no keys.
238+func (s *Server) setKeymap(c ports.SetKeymap) {
239+ s.seat.repeatRate, s.seat.repeatDelay = c.RepeatRate, c.RepeatDelay
240+ if c.Keymap == "" {
241+ for _, list := range s.seat.keyboards {
242+ for _, k := range list {
243+ if k.Version() >= 4 {
244+ k.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
245+ }
246+ }
247+ }
248+ s.sendGrabKeymap(false)
249+ s.log.Info().Int("rate", s.seat.repeatRate).Int("delay", s.seat.repeatDelay).Msg("repeat updated")
250+ return
251+ }
252+ fd, size, err := keymapFile(c.Keymap)
253+ if err != nil {
254+ s.log.Warn().Err(err).Msg("keymap update failed")
255+ return
256+ }
257+ if s.seat.keymapFD >= 0 {
258+ unix.Close(s.seat.keymapFD)
259+ }
260+ s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, c.Keymap
261+ s.seat.keymapOwner = nil
262+ focused := s.seat.focused
263+ s.changeFocus(0)
264+ s.seat.heldKeys, s.seat.grabKeys = nil, nil
265+ s.seat.modState = ports.ModState{}
266+ for _, list := range s.seat.keyboards {
267+ for _, k := range list {
268+ k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.seat.keymapFD, s.seat.keymapSize)
269+ if k.Version() >= 4 {
270+ k.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
271+ }
272+ }
273+ }
274+ s.sendGrabKeymap(true)
275+ s.changeFocus(focused)
276+ s.log.Info().Uint32("size", size).Int("rate", s.seat.repeatRate).Int("delay", s.seat.repeatDelay).Msg("keymap updated")
277+}
278+
279+func (s *Server) sendModifiers(k *wayland.Keyboard) {
280+ m := s.seat.modState
281+ k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
282+}
283+
284+func (s *Server) changeFocus(id ports.WindowID) {
285+ // Enter carries seat held keys and modifiers: they need the seat keymap.
286+ s.useKeymap(nil)
287+ old := s.focusClient()
288+ defer func() {
289+ // Tokens of a client losing the focus die with it.
290+ if now := s.focusClient(); now != old {
291+ s.dropTokens(old)
292+ s.seat.focusAt = time.Now()
293+ }
294+ }()
295+ if surf, keyboards := s.focusTarget(s.seat.focused); surf != nil {
296+ for _, k := range keyboards {
297+ s.serial++
298+ k.SendLeave(s.serial, surf)
299+ }
300+ }
301+ s.seat.focused = 0
302+ if surf, keyboards := s.focusTarget(id); surf != nil {
303+ s.seat.focused = id
304+ s.focusSelections()
305+ for _, k := range keyboards {
306+ s.serial++
307+ var keys []byte
308+ for code := range s.seat.heldKeys {
309+ keys = binary.LittleEndian.AppendUint32(keys, code)
310+ }
311+ k.SendEnter(s.serial, surf, keys)
312+ s.sendModifiers(k)
313+ }
314+ }
315+ s.textInputFocus()
316+ s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus")
317+ s.updateConstraint()
318+}
319+
320+// wheelSteps adds wheel v120 to the rest per axis and returns the whole
321+// detents for axis_discrete (seat before v8). High-resolution wheels send
322+// fractions of 120; a direction change drops the rest, as wlroots does.
323+// wheelHeld keeps the axis value of the frames without a step; values is
324+// what a pre-v8 client gets with a step: the held value plus this frame's.
325+func (st *seatState) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2]float64) {
326+ if a.Source != ports.AxisWheel {
327+ return steps, values
328+ }
329+ for i, ax := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {
330+ if !ax.Set || ax.V120 == 0 {
331+ continue
332+ }
333+ if (st.wheelRest[i] < 0) != (ax.V120 < 0) {
334+ st.wheelRest[i], st.wheelHeld[i] = 0, 0
335+ }
336+ st.wheelRest[i] += ax.V120
337+ st.wheelHeld[i] += ax.Value
338+ steps[i] = st.wheelRest[i] / 120
339+ st.wheelRest[i] -= steps[i] * 120
340+ if steps[i] != 0 {
341+ values[i], st.wheelHeld[i] = st.wheelHeld[i], 0
342+ }
343+ }
344+ return steps, values
345+}
346+
347+// sendAxis sends one scroll frame, each event gated by the pointer version.
348+// steps are whole wheel detents for axis_discrete. A pre-v8 client gets a
349+// wheel axis only with a step, carrying values (all the value since the
350+// last step), so it never counts smooth and discrete scroll.
351+func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {
352+ v := p.Version()
353+ axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal}
354+ for i := range axes {
355+ if v < 8 && a.Source == ports.AxisWheel && axes[i].V120 != 0 && steps[i] == 0 {
356+ axes[i].Set = false
357+ }
358+ }
359+ if !axes[0].Set && !axes[1].Set {
360+ return
361+ }
362+ if v >= 5 {
363+ p.SendAxisSource(uint32(a.Source))
364+ }
365+ for axis, s := range axes {
366+ if !s.Set {
367+ continue
368+ }
369+ if s.Stop {
370+ if v >= 5 {
371+ p.SendAxisStop(a.TimeMsec, uint32(axis))
372+ }
373+ continue
374+ }
375+ value := s.Value
376+ if a.Source == ports.AxisWheel && s.V120 != 0 {
377+ switch {
378+ case v >= 8:
379+ p.SendAxisValue120(uint32(axis), s.V120)
380+ default:
381+ value = values[axis]
382+ if v >= 5 {
383+ p.SendAxisDiscrete(uint32(axis), steps[axis])
384+ }
385+ }
386+ }
387+ p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value))
388+ }
389+ pointerFrame(p)
390+}
391+
392+func pointerFrame(p *wayland.Pointer) {
393+ if p.Version() >= 5 {
394+ p.SendFrame()
395+ }
396+}
397+func (s *Server) windowPointers(w *window) []*wayland.Pointer {
398+ if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
399+ return nil
400+ }
401+ return s.clientPointers(w.xdg.resource.Client())
402+}
403+
404+func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer {
405+ if l == nil || !l.mapped || !l.resource.Resource.Alive() {
406+ return nil
407+ }
408+ return s.clientPointers(l.resource.Client())
409+}
410+
411+func (s *Server) clientPointers(c server.Client) []*wayland.Pointer {
412+ var result []*wayland.Pointer
413+ for _, p := range s.seat.pointers[c] {
414+ if p.Resource.Alive() {
415+ result = append(result, p)
416+ }
417+ }
418+ return result
419+}
420+func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
421+ if id == s.seat.pointerFocus {
422+ return
423+ }
424+ s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
425+ // The new client sets its own cursor on enter; until then, the arrow.
426+ s.cursorSurface = nil
427+ s.setCursor(ports.CursorChange{})
428+ if surface, pointers, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0); surface != nil {
429+ for _, p := range pointers {
430+ s.serial++
431+ p.SendLeave(s.serial, surface)
432+ pointerFrame(p)
433+ }
434+ }
435+ s.seat.pointerFocus = 0
436+ if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil {
437+ s.seat.pointerFocus, s.seat.pointerX, s.seat.pointerY = id, x, y
438+ for _, p := range pointers {
439+ s.serial++
440+ p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy))
441+ pointerFrame(p)
442+ }
443+ }
444+ s.updateConstraint()
445+}
M internal/adapters/wayland/server.go +16 -430
@@ -2,13 +2,13 @@ package wayland
22
33 import (
44 "context"
5- "encoding/binary"
65 "fmt"
7- "github.com/bnema/purego-libwayland/protocol/relativepointer"
86 "os"
97 "sync"
108 "time"
119
10+ "github.com/bnema/purego-libwayland/protocol/relativepointer"
11+
1212 "github.com/bnema/neferwl/internal/adapters/clock"
1313 "github.com/bnema/neferwl/internal/ports"
1414 "github.com/bnema/purego-libwayland/protocol/fractionalscale"
@@ -107,34 +107,14 @@ type Server struct {
107107 colorOutputs map[*colorOutput]struct{}
108108 colorFeedbacks map[*colorFeedback]struct{}
109109 serial uint32
110- // press is the serial of the last button or key press, sent to
111- // pressClient: popup grabs must come from it.
112- press uint32
113- pressClient server.Client
114- // pressAt and focusAt date the last press and keyboard focus change,
115- // for xdg-activation tokens.
116- pressAt, focusAt time.Time
117- ctx context.Context
118- windows map[ports.WindowID]*window
119- layers map[ports.WindowID]*layerSurface
120- nextWindow ports.WindowID
121- nextPool uint64
122- nextSurface uint64
123- focused ports.WindowID
124- pointerFocus ports.WindowID
125- wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
126- wheelHeld [2]float64 // axis value held back with it for pre-v8 clients
127- pointerX, pointerY float64
128- pointers map[server.Client][]*wayland.Pointer
129- modState ports.ModState
130- keyboards map[server.Client][]*wayland.Keyboard
131- heldKeys map[uint32]bool
132- grabKeys map[uint32]bool // pressed through the input method grab
133- keymapFD int
134- keymapSize uint32
135- keymapText string // the seat keymap, to compare virtual keymaps with
136- keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
137- repeatRate, repeatDelay int
110+ ctx context.Context
111+ windows map[ports.WindowID]*window
112+ layers map[ports.WindowID]*layerSurface
113+ nextWindow ports.WindowID
114+ nextPool uint64
115+ nextSurface uint64
116+ // seat is the keyboard and pointer state (seat.go).
117+ seat seatState
138118 // eventMu protects only the notification queue, not display-owned window state.
139119 // The event queue is unbounded by design: it grows only if core stops draining
140120 // Events, which happens only at shutdown (core owns the receiving end). A
@@ -236,7 +216,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
236216 d.Close()
237217 return nil, err
238218 }
239- s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
219+ s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}}
240220 s.clock = opts.Clock
241221 if s.clock == nil {
242222 s.clock = clock.System{}
@@ -281,11 +261,11 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
281261 d.Close()
282262 return nil, e
283263 }
284- s.keymapFD, s.keymapSize, s.keymapText = fd, size, opts.Keymap
264+ s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap
285265 }
286266 s.cleanup = func() {
287- if s.keymapFD >= 0 {
288- unix.Close(s.keymapFD)
267+ if s.seat.keymapFD >= 0 {
268+ unix.Close(s.seat.keymapFD)
289269 }
290270 s.dmabuf.close()
291271 if s.syncWait != nil {
@@ -585,106 +565,8 @@ func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) (
585565
586566 func (s *Server) apply(cmd ports.ClientCommand) {
587567 switch c := cmd.(type) {
588- case ports.PointerFocus:
589- s.changePointerFocus(c.ID, c.X, c.Y)
590- case ports.PointerMotionTo:
591- if l := s.layers[c.ID]; l != nil && c.ID == s.pointerFocus {
592- s.pointerX, s.pointerY = c.X, c.Y
593- for _, p := range s.layerPointers(l) {
594- p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
595- pointerFrame(p)
596- }
597- return
598- }
599- if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus {
600- if s.relativeMotion(w, c) && s.locked(w) {
601- for _, p := range s.windowPointers(w) {
602- pointerFrame(p)
603- }
604- }
605- if s.locked(w) {
606- // Only relative motion reaches a locked pointer.
607- return
608- }
609- x, y := w.surfacePoint(c.X, c.Y)
610- s.pointerX, s.pointerY = c.X, c.Y
611- // A constraint waits for the pointer to enter its region.
612- if s.constraint == nil {
613- s.updateConstraint()
614- }
615- for _, p := range s.windowPointers(w) {
616- p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))
617- pointerFrame(p)
618- }
619- }
620- case ports.PointerButtonTo:
621- // Releases may target the implicit-grab window after focus has moved.
622- if client, pointers, ok := s.pointerTarget(c.ID); ok {
623- s.serial++
624- state := uint32(0)
625- if c.Pressed {
626- state = 1
627- s.press, s.pressClient, s.pressAt = s.serial, client, time.Now()
628- }
629- for _, p := range pointers {
630- p.SendButton(s.serial, c.TimeMsec, c.Button, state)
631- pointerFrame(p)
632- }
633- }
634- case ports.PointerAxisTo:
635- // Scroll applies to the entered surface only.
636- if c.ID != s.pointerFocus {
637- return
638- }
639- steps, values := s.wheelSteps(c.Axis)
640- _, pointers, _ := s.pointerTarget(c.ID)
641- for _, p := range pointers {
642- sendAxis(p, c.Axis, steps, values)
643- }
644- case ports.SetKeymap:
645- s.setKeymap(c)
646- case ports.FocusWindow:
647- if s.focused != c.ID {
648- s.changeFocus(c.ID)
649- }
650- case ports.ForwardKey:
651- if s.grabKey(c) {
652- return
653- }
654- _, keyboards := s.focusTarget(c.ID)
655- if c.ID != s.focused || len(keyboards) == 0 {
656- s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")
657- return
658- }
659- s.useKeymap(nil)
660- if s.heldKeys == nil {
661- s.heldKeys = make(map[uint32]bool)
662- }
663- if c.Key.Pressed {
664- s.heldKeys[c.Key.Keycode] = true
665- } else {
666- delete(s.heldKeys, c.Key.Keycode)
667- }
668- s.serial++
669- state := uint32(0)
670- if c.Key.Pressed {
671- state = 1
672- }
673- if c.Key.Pressed {
674- if surf, _ := s.focusTarget(c.ID); surf != nil {
675- s.press, s.pressClient, s.pressAt = s.serial, surf.Client(), time.Now()
676- }
677- }
678- for _, k := range keyboards {
679- k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
680- }
681- if c.Key.State != s.modState {
682- s.modState = c.Key.State
683- s.serial++
684- for _, k := range keyboards {
685- s.sendModifiers(k)
686- }
687- }
568+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
569+ s.applyInput(c)
688570 case ports.ConfigurePopup:
689571 if w := s.windows[c.ID]; w != nil && w.popup != nil {
690572 w.popup.configure(c)
@@ -741,299 +623,3 @@ func (s *Server) apply(cmd ports.ClientCommand) {
741623 w.toplevel.SendClose()
742624 }
743625 }
744-
745-func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
746- if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
747- return nil
748- }
749- return s.clientKeyboards(w.xdg.resource.Client())
750-}
751-
752-func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard {
753- var alive []*wayland.Keyboard
754- for _, k := range s.keyboards[c] {
755- if k.Resource.Alive() {
756- alive = append(alive, k)
757- }
758- }
759- return alive
760-}
761-
762-// focusTarget resolves a window or layer ID to its wl_surface and keyboards.
763-func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Keyboard) {
764- if w := s.windows[id]; w != nil && w.mapped {
765- if surf := w.xdg.surfaceResource(); surf != nil {
766- return surf, s.windowKeyboards(w)
767- }
768- }
769- if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
770- if surf := l.surfaceResource(); surf != nil {
771- return surf, s.clientKeyboards(l.resource.Client())
772- }
773- }
774- return nil, nil
775-}
776-
777-// pointerTarget resolves a mapped window or layer ID to its client and
778-// live pointers.
779-func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Pointer, bool) {
780- if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() {
781- return w.xdg.resource.Client(), s.windowPointers(w), true
782- }
783- if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
784- return l.resource.Client(), s.layerPointers(l), true
785- }
786- return server.Client{}, nil, false
787-}
788-
789-// pointerSurface is the wl_surface of a mapped window or layer and the
790-// point in its surface coordinates.
791-func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surface, []*wayland.Pointer, float64, float64) {
792- if w := s.windows[id]; w != nil {
793- if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() {
794- x, y := w.surfacePoint(x, y)
795- return surf, s.windowPointers(w), x, y
796- }
797- }
798- if l := s.layers[id]; l != nil && l.mapped {
799- if surf := l.surfaceResource(); surf != nil {
800- return surf, s.layerPointers(l), x, y
801- }
802- }
803- return nil, nil, 0, 0
804-}
805-
806-// keymapFile writes a sealed memfd holding the NUL-terminated keymap.
807-func keymapFile(keymap string) (int, uint32, error) {
808- if uint64(len(keymap))+1 > uint64(^uint32(0)) {
809- return -1, 0, fmt.Errorf("keymap too large")
810- }
811- fd, err := unix.MemfdCreate("neferwl-keymap", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
812- if err != nil {
813- return -1, 0, err
814- }
815- n, err := unix.Write(fd, append([]byte(keymap), 0))
816- if err == nil && n != len(keymap)+1 {
817- err = fmt.Errorf("short keymap write")
818- }
819- if err == nil {
820- _, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE|unix.F_SEAL_SEAL)
821- }
822- if err != nil {
823- unix.Close(fd)
824- return -1, 0, err
825- }
826- return fd, uint32(n), nil
827-}
828-
829-// setKeymap sends repeat info, and a new keymap when one is given, to every keyboard.
830-// A new keymap resets held keys and modifiers: the focused client gets leave, then
831-// enter with no keys.
832-func (s *Server) setKeymap(c ports.SetKeymap) {
833- s.repeatRate, s.repeatDelay = c.RepeatRate, c.RepeatDelay
834- if c.Keymap == "" {
835- for _, list := range s.keyboards {
836- for _, k := range list {
837- if k.Version() >= 4 {
838- k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
839- }
840- }
841- }
842- s.sendGrabKeymap(false)
843- s.log.Info().Int("rate", s.repeatRate).Int("delay", s.repeatDelay).Msg("repeat updated")
844- return
845- }
846- fd, size, err := keymapFile(c.Keymap)
847- if err != nil {
848- s.log.Warn().Err(err).Msg("keymap update failed")
849- return
850- }
851- if s.keymapFD >= 0 {
852- unix.Close(s.keymapFD)
853- }
854- s.keymapFD, s.keymapSize, s.keymapText = fd, size, c.Keymap
855- s.keymapOwner = nil
856- focused := s.focused
857- s.changeFocus(0)
858- s.heldKeys, s.grabKeys = nil, nil
859- s.modState = ports.ModState{}
860- for _, list := range s.keyboards {
861- for _, k := range list {
862- k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.keymapFD, s.keymapSize)
863- if k.Version() >= 4 {
864- k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
865- }
866- }
867- }
868- s.sendGrabKeymap(true)
869- s.changeFocus(focused)
870- s.log.Info().Uint32("size", size).Int("rate", s.repeatRate).Int("delay", s.repeatDelay).Msg("keymap updated")
871-}
872-
873-func (s *Server) sendModifiers(k *wayland.Keyboard) {
874- m := s.modState
875- k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
876-}
877-
878-func (s *Server) changeFocus(id ports.WindowID) {
879- // Enter carries seat held keys and modifiers: they need the seat keymap.
880- s.useKeymap(nil)
881- old := s.focusClient()
882- defer func() {
883- // Tokens of a client losing the focus die with it.
884- if now := s.focusClient(); now != old {
885- s.dropTokens(old)
886- s.focusAt = time.Now()
887- }
888- }()
889- if surf, keyboards := s.focusTarget(s.focused); surf != nil {
890- for _, k := range keyboards {
891- s.serial++
892- k.SendLeave(s.serial, surf)
893- }
894- }
895- s.focused = 0
896- if surf, keyboards := s.focusTarget(id); surf != nil {
897- s.focused = id
898- s.focusSelections()
899- for _, k := range keyboards {
900- s.serial++
901- var keys []byte
902- for code := range s.heldKeys {
903- keys = binary.LittleEndian.AppendUint32(keys, code)
904- }
905- k.SendEnter(s.serial, surf, keys)
906- s.sendModifiers(k)
907- }
908- }
909- s.textInputFocus()
910- s.log.Debug().Uint64("id", uint64(s.focused)).Msg("keyboard focus")
911- s.updateConstraint()
912-}
913-
914-// wheelSteps adds wheel v120 to the rest per axis and returns the whole
915-// detents for axis_discrete (seat before v8). High-resolution wheels send
916-// fractions of 120; a direction change drops the rest, as wlroots does.
917-// wheelHeld keeps the axis value of the frames without a step; values is
918-// what a pre-v8 client gets with a step: the held value plus this frame's.
919-func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2]float64) {
920- if a.Source != ports.AxisWheel {
921- return steps, values
922- }
923- for i, ax := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {
924- if !ax.Set || ax.V120 == 0 {
925- continue
926- }
927- if (s.wheelRest[i] < 0) != (ax.V120 < 0) {
928- s.wheelRest[i], s.wheelHeld[i] = 0, 0
929- }
930- s.wheelRest[i] += ax.V120
931- s.wheelHeld[i] += ax.Value
932- steps[i] = s.wheelRest[i] / 120
933- s.wheelRest[i] -= steps[i] * 120
934- if steps[i] != 0 {
935- values[i], s.wheelHeld[i] = s.wheelHeld[i], 0
936- }
937- }
938- return steps, values
939-}
940-
941-// sendAxis sends one scroll frame, each event gated by the pointer version.
942-// steps are whole wheel detents for axis_discrete. A pre-v8 client gets a
943-// wheel axis only with a step, carrying values (all the value since the
944-// last step), so it never counts smooth and discrete scroll.
945-func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {
946- v := p.Version()
947- axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal}
948- for i := range axes {
949- if v < 8 && a.Source == ports.AxisWheel && axes[i].V120 != 0 && steps[i] == 0 {
950- axes[i].Set = false
951- }
952- }
953- if !axes[0].Set && !axes[1].Set {
954- return
955- }
956- if v >= 5 {
957- p.SendAxisSource(uint32(a.Source))
958- }
959- for axis, s := range axes {
960- if !s.Set {
961- continue
962- }
963- if s.Stop {
964- if v >= 5 {
965- p.SendAxisStop(a.TimeMsec, uint32(axis))
966- }
967- continue
968- }
969- value := s.Value
970- if a.Source == ports.AxisWheel && s.V120 != 0 {
971- switch {
972- case v >= 8:
973- p.SendAxisValue120(uint32(axis), s.V120)
974- default:
975- value = values[axis]
976- if v >= 5 {
977- p.SendAxisDiscrete(uint32(axis), steps[axis])
978- }
979- }
980- }
981- p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value))
982- }
983- pointerFrame(p)
984-}
985-
986-func pointerFrame(p *wayland.Pointer) {
987- if p.Version() >= 5 {
988- p.SendFrame()
989- }
990-}
991-func (s *Server) windowPointers(w *window) []*wayland.Pointer {
992- if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
993- return nil
994- }
995- return s.clientPointers(w.xdg.resource.Client())
996-}
997-
998-func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer {
999- if l == nil || !l.mapped || !l.resource.Resource.Alive() {
1000- return nil
1001- }
1002- return s.clientPointers(l.resource.Client())
1003-}
1004-
1005-func (s *Server) clientPointers(c server.Client) []*wayland.Pointer {
1006- var result []*wayland.Pointer
1007- for _, p := range s.pointers[c] {
1008- if p.Resource.Alive() {
1009- result = append(result, p)
1010- }
1011- }
1012- return result
1013-}
1014-func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
1015- if id == s.pointerFocus {
1016- return
1017- }
1018- s.wheelRest, s.wheelHeld = [2]int32{}, [2]float64{}
1019- // The new client sets its own cursor on enter; until then, the arrow.
1020- s.cursorSurface = nil
1021- s.setCursor(ports.CursorChange{})
1022- if surface, pointers, _, _ := s.pointerSurface(s.pointerFocus, 0, 0); surface != nil {
1023- for _, p := range pointers {
1024- s.serial++
1025- p.SendLeave(s.serial, surface)
1026- pointerFrame(p)
1027- }
1028- }
1029- s.pointerFocus = 0
1030- if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil {
1031- s.pointerFocus, s.pointerX, s.pointerY = id, x, y
1032- for _, p := range pointers {
1033- s.serial++
1034- p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy))
1035- pointerFrame(p)
1036- }
1037- }
1038- s.updateConstraint()
1039-}
@@ -2,13 +2,13 @@ package wayland
2 2
3 import (3 import (
4 "context"4 "context"
5- "encoding/binary"
6 "fmt"5 "fmt"
7- "github.com/bnema/purego-libwayland/protocol/relativepointer"
8 "os"6 "os"
9 "sync"7 "sync"
10 "time"8 "time"
11 9
10+ "github.com/bnema/purego-libwayland/protocol/relativepointer"
11+
12 "github.com/bnema/neferwl/internal/adapters/clock"12 "github.com/bnema/neferwl/internal/adapters/clock"
13 "github.com/bnema/neferwl/internal/ports"13 "github.com/bnema/neferwl/internal/ports"
14 "github.com/bnema/purego-libwayland/protocol/fractionalscale"14 "github.com/bnema/purego-libwayland/protocol/fractionalscale"
@@ -107,34 +107,14 @@ type Server struct {
107 colorOutputs map[*colorOutput]struct{}107 colorOutputs map[*colorOutput]struct{}
108 colorFeedbacks map[*colorFeedback]struct{}108 colorFeedbacks map[*colorFeedback]struct{}
109 serial uint32109 serial uint32
110- // press is the serial of the last button or key press, sent to110+ ctx context.Context
111- // pressClient: popup grabs must come from it.111+ windows map[ports.WindowID]*window
112- press uint32112+ layers map[ports.WindowID]*layerSurface
113- pressClient server.Client113+ nextWindow ports.WindowID
114- // pressAt and focusAt date the last press and keyboard focus change,114+ nextPool uint64
115- // for xdg-activation tokens.115+ nextSurface uint64
116- pressAt, focusAt time.Time116+ // seat is the keyboard and pointer state (seat.go).
117- ctx context.Context117+ seat seatState
118- windows map[ports.WindowID]*window
119- layers map[ports.WindowID]*layerSurface
120- nextWindow ports.WindowID
121- nextPool uint64
122- nextSurface uint64
123- focused ports.WindowID
124- pointerFocus ports.WindowID
125- wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
126- wheelHeld [2]float64 // axis value held back with it for pre-v8 clients
127- pointerX, pointerY float64
128- pointers map[server.Client][]*wayland.Pointer
129- modState ports.ModState
130- keyboards map[server.Client][]*wayland.Keyboard
131- heldKeys map[uint32]bool
132- grabKeys map[uint32]bool // pressed through the input method grab
133- keymapFD int
134- keymapSize uint32
135- keymapText string // the seat keymap, to compare virtual keymaps with
136- keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
137- repeatRate, repeatDelay int
138 // eventMu protects only the notification queue, not display-owned window state.118 // eventMu protects only the notification queue, not display-owned window state.
139 // The event queue is unbounded by design: it grows only if core stops draining119 // The event queue is unbounded by design: it grows only if core stops draining
140 // Events, which happens only at shutdown (core owns the receiving end). A120 // Events, which happens only at shutdown (core owns the receiving end). A
@@ -236,7 +216,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
236 d.Close()216 d.Close()
237 return nil, err217 return nil, err
238 }218 }
239- s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}219+ s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}}
240 s.clock = opts.Clock220 s.clock = opts.Clock
241 if s.clock == nil {221 if s.clock == nil {
242 s.clock = clock.System{}222 s.clock = clock.System{}
@@ -281,11 +261,11 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
281 d.Close()261 d.Close()
282 return nil, e262 return nil, e
283 }263 }
284- s.keymapFD, s.keymapSize, s.keymapText = fd, size, opts.Keymap264+ s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap
285 }265 }
286 s.cleanup = func() {266 s.cleanup = func() {
287- if s.keymapFD >= 0 {267+ if s.seat.keymapFD >= 0 {
288- unix.Close(s.keymapFD)268+ unix.Close(s.seat.keymapFD)
289 }269 }
290 s.dmabuf.close()270 s.dmabuf.close()
291 if s.syncWait != nil {271 if s.syncWait != nil {
@@ -585,106 +565,8 @@ func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) (
585 565
586 func (s *Server) apply(cmd ports.ClientCommand) {566 func (s *Server) apply(cmd ports.ClientCommand) {
587 switch c := cmd.(type) {567 switch c := cmd.(type) {
588- case ports.PointerFocus:568+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
589- s.changePointerFocus(c.ID, c.X, c.Y)569+ s.applyInput(c)
590- case ports.PointerMotionTo:
591- if l := s.layers[c.ID]; l != nil && c.ID == s.pointerFocus {
592- s.pointerX, s.pointerY = c.X, c.Y
593- for _, p := range s.layerPointers(l) {
594- p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
595- pointerFrame(p)
596- }
597- return
598- }
599- if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus {
600- if s.relativeMotion(w, c) && s.locked(w) {
601- for _, p := range s.windowPointers(w) {
602- pointerFrame(p)
603- }
604- }
605- if s.locked(w) {
606- // Only relative motion reaches a locked pointer.
607- return
608- }
609- x, y := w.surfacePoint(c.X, c.Y)
610- s.pointerX, s.pointerY = c.X, c.Y
611- // A constraint waits for the pointer to enter its region.
612- if s.constraint == nil {
613- s.updateConstraint()
614- }
615- for _, p := range s.windowPointers(w) {
616- p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))
617- pointerFrame(p)
618- }
619- }
620- case ports.PointerButtonTo:
621- // Releases may target the implicit-grab window after focus has moved.
622- if client, pointers, ok := s.pointerTarget(c.ID); ok {
623- s.serial++
624- state := uint32(0)
625- if c.Pressed {
626- state = 1
627- s.press, s.pressClient, s.pressAt = s.serial, client, time.Now()
628- }
629- for _, p := range pointers {
630- p.SendButton(s.serial, c.TimeMsec, c.Button, state)
631- pointerFrame(p)
632- }
633- }
634- case ports.PointerAxisTo:
635- // Scroll applies to the entered surface only.
636- if c.ID != s.pointerFocus {
637- return
638- }
639- steps, values := s.wheelSteps(c.Axis)
640- _, pointers, _ := s.pointerTarget(c.ID)
641- for _, p := range pointers {
642- sendAxis(p, c.Axis, steps, values)
643- }
644- case ports.SetKeymap:
645- s.setKeymap(c)
646- case ports.FocusWindow:
647- if s.focused != c.ID {
648- s.changeFocus(c.ID)
649- }
650- case ports.ForwardKey:
651- if s.grabKey(c) {
652- return
653- }
654- _, keyboards := s.focusTarget(c.ID)
655- if c.ID != s.focused || len(keyboards) == 0 {
656- s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key")
657- return
658- }
659- s.useKeymap(nil)
660- if s.heldKeys == nil {
661- s.heldKeys = make(map[uint32]bool)
662- }
663- if c.Key.Pressed {
664- s.heldKeys[c.Key.Keycode] = true
665- } else {
666- delete(s.heldKeys, c.Key.Keycode)
667- }
668- s.serial++
669- state := uint32(0)
670- if c.Key.Pressed {
671- state = 1
672- }
673- if c.Key.Pressed {
674- if surf, _ := s.focusTarget(c.ID); surf != nil {
675- s.press, s.pressClient, s.pressAt = s.serial, surf.Client(), time.Now()
676- }
677- }
678- for _, k := range keyboards {
679- k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
680- }
681- if c.Key.State != s.modState {
682- s.modState = c.Key.State
683- s.serial++
684- for _, k := range keyboards {
685- s.sendModifiers(k)
686- }
687- }
688 case ports.ConfigurePopup:570 case ports.ConfigurePopup:
689 if w := s.windows[c.ID]; w != nil && w.popup != nil {571 if w := s.windows[c.ID]; w != nil && w.popup != nil {
690 w.popup.configure(c)572 w.popup.configure(c)
@@ -741,299 +623,3 @@ func (s *Server) apply(cmd ports.ClientCommand) {
741 w.toplevel.SendClose()623 w.toplevel.SendClose()
742 }624 }
743 }625 }
744-
745-func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
746- if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
747- return nil
748- }
749- return s.clientKeyboards(w.xdg.resource.Client())
750-}
751-
752-func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard {
753- var alive []*wayland.Keyboard
754- for _, k := range s.keyboards[c] {
755- if k.Resource.Alive() {
756- alive = append(alive, k)
757- }
758- }
759- return alive
760-}
761-
762-// focusTarget resolves a window or layer ID to its wl_surface and keyboards.
763-func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Keyboard) {
764- if w := s.windows[id]; w != nil && w.mapped {
765- if surf := w.xdg.surfaceResource(); surf != nil {
766- return surf, s.windowKeyboards(w)
767- }
768- }
769- if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
770- if surf := l.surfaceResource(); surf != nil {
771- return surf, s.clientKeyboards(l.resource.Client())
772- }
773- }
774- return nil, nil
775-}
776-
777-// pointerTarget resolves a mapped window or layer ID to its client and
778-// live pointers.
779-func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Pointer, bool) {
780- if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() {
781- return w.xdg.resource.Client(), s.windowPointers(w), true
782- }
783- if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() {
784- return l.resource.Client(), s.layerPointers(l), true
785- }
786- return server.Client{}, nil, false
787-}
788-
789-// pointerSurface is the wl_surface of a mapped window or layer and the
790-// point in its surface coordinates.
791-func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surface, []*wayland.Pointer, float64, float64) {
792- if w := s.windows[id]; w != nil {
793- if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() {
794- x, y := w.surfacePoint(x, y)
795- return surf, s.windowPointers(w), x, y
796- }
797- }
798- if l := s.layers[id]; l != nil && l.mapped {
799- if surf := l.surfaceResource(); surf != nil {
800- return surf, s.layerPointers(l), x, y
801- }
802- }
803- return nil, nil, 0, 0
804-}
805-
806-// keymapFile writes a sealed memfd holding the NUL-terminated keymap.
807-func keymapFile(keymap string) (int, uint32, error) {
808- if uint64(len(keymap))+1 > uint64(^uint32(0)) {
809- return -1, 0, fmt.Errorf("keymap too large")
810- }
811- fd, err := unix.MemfdCreate("neferwl-keymap", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
812- if err != nil {
813- return -1, 0, err
814- }
815- n, err := unix.Write(fd, append([]byte(keymap), 0))
816- if err == nil && n != len(keymap)+1 {
817- err = fmt.Errorf("short keymap write")
818- }
819- if err == nil {
820- _, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE|unix.F_SEAL_SEAL)
821- }
822- if err != nil {
823- unix.Close(fd)
824- return -1, 0, err
825- }
826- return fd, uint32(n), nil
827-}
828-
829-// setKeymap sends repeat info, and a new keymap when one is given, to every keyboard.
830-// A new keymap resets held keys and modifiers: the focused client gets leave, then
831-// enter with no keys.
832-func (s *Server) setKeymap(c ports.SetKeymap) {
833- s.repeatRate, s.repeatDelay = c.RepeatRate, c.RepeatDelay
834- if c.Keymap == "" {
835- for _, list := range s.keyboards {
836- for _, k := range list {
837- if k.Version() >= 4 {
838- k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
839- }
840- }
841- }
842- s.sendGrabKeymap(false)
843- s.log.Info().Int("rate", s.repeatRate).Int("delay", s.repeatDelay).Msg("repeat updated")
844- return
845- }
846- fd, size, err := keymapFile(c.Keymap)
847- if err != nil {
848- s.log.Warn().Err(err).Msg("keymap update failed")
849- return
850- }
851- if s.keymapFD >= 0 {
852- unix.Close(s.keymapFD)
853- }
854- s.keymapFD, s.keymapSize, s.keymapText = fd, size, c.Keymap
855- s.keymapOwner = nil
856- focused := s.focused
857- s.changeFocus(0)
858- s.heldKeys, s.grabKeys = nil, nil
859- s.modState = ports.ModState{}
860- for _, list := range s.keyboards {
861- for _, k := range list {
862- k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.keymapFD, s.keymapSize)
863- if k.Version() >= 4 {
864- k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
865- }
866- }
867- }
868- s.sendGrabKeymap(true)
869- s.changeFocus(focused)
870- s.log.Info().Uint32("size", size).Int("rate", s.repeatRate).Int("delay", s.repeatDelay).Msg("keymap updated")
871-}
872-
873-func (s *Server) sendModifiers(k *wayland.Keyboard) {
874- m := s.modState
875- k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
876-}
877-
878-func (s *Server) changeFocus(id ports.WindowID) {
879- // Enter carries seat held keys and modifiers: they need the seat keymap.
880- s.useKeymap(nil)
881- old := s.focusClient()
882- defer func() {
883- // Tokens of a client losing the focus die with it.
884- if now := s.focusClient(); now != old {
885- s.dropTokens(old)
886- s.focusAt = time.Now()
887- }
888- }()
889- if surf, keyboards := s.focusTarget(s.focused); surf != nil {
890- for _, k := range keyboards {
891- s.serial++
892- k.SendLeave(s.serial, surf)
893- }
894- }
895- s.focused = 0
896- if surf, keyboards := s.focusTarget(id); surf != nil {
897- s.focused = id
898- s.focusSelections()
899- for _, k := range keyboards {
900- s.serial++
901- var keys []byte
902- for code := range s.heldKeys {
903- keys = binary.LittleEndian.AppendUint32(keys, code)
904- }
905- k.SendEnter(s.serial, surf, keys)
906- s.sendModifiers(k)
907- }
908- }
909- s.textInputFocus()
910- s.log.Debug().Uint64("id", uint64(s.focused)).Msg("keyboard focus")
911- s.updateConstraint()
912-}
913-
914-// wheelSteps adds wheel v120 to the rest per axis and returns the whole
915-// detents for axis_discrete (seat before v8). High-resolution wheels send
916-// fractions of 120; a direction change drops the rest, as wlroots does.
917-// wheelHeld keeps the axis value of the frames without a step; values is
918-// what a pre-v8 client gets with a step: the held value plus this frame's.
919-func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2]float64) {
920- if a.Source != ports.AxisWheel {
921- return steps, values
922- }
923- for i, ax := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {
924- if !ax.Set || ax.V120 == 0 {
925- continue
926- }
927- if (s.wheelRest[i] < 0) != (ax.V120 < 0) {
928- s.wheelRest[i], s.wheelHeld[i] = 0, 0
929- }
930- s.wheelRest[i] += ax.V120
931- s.wheelHeld[i] += ax.Value
932- steps[i] = s.wheelRest[i] / 120
933- s.wheelRest[i] -= steps[i] * 120
934- if steps[i] != 0 {
935- values[i], s.wheelHeld[i] = s.wheelHeld[i], 0
936- }
937- }
938- return steps, values
939-}
940-
941-// sendAxis sends one scroll frame, each event gated by the pointer version.
942-// steps are whole wheel detents for axis_discrete. A pre-v8 client gets a
943-// wheel axis only with a step, carrying values (all the value since the
944-// last step), so it never counts smooth and discrete scroll.
945-func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {
946- v := p.Version()
947- axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal}
948- for i := range axes {
949- if v < 8 && a.Source == ports.AxisWheel && axes[i].V120 != 0 && steps[i] == 0 {
950- axes[i].Set = false
951- }
952- }
953- if !axes[0].Set && !axes[1].Set {
954- return
955- }
956- if v >= 5 {
957- p.SendAxisSource(uint32(a.Source))
958- }
959- for axis, s := range axes {
960- if !s.Set {
961- continue
962- }
963- if s.Stop {
964- if v >= 5 {
965- p.SendAxisStop(a.TimeMsec, uint32(axis))
966- }
967- continue
968- }
969- value := s.Value
970- if a.Source == ports.AxisWheel && s.V120 != 0 {
971- switch {
972- case v >= 8:
973- p.SendAxisValue120(uint32(axis), s.V120)
974- default:
975- value = values[axis]
976- if v >= 5 {
977- p.SendAxisDiscrete(uint32(axis), steps[axis])
978- }
979- }
980- }
981- p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value))
982- }
983- pointerFrame(p)
984-}
985-
986-func pointerFrame(p *wayland.Pointer) {
987- if p.Version() >= 5 {
988- p.SendFrame()
989- }
990-}
991-func (s *Server) windowPointers(w *window) []*wayland.Pointer {
992- if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
993- return nil
994- }
995- return s.clientPointers(w.xdg.resource.Client())
996-}
997-
998-func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer {
999- if l == nil || !l.mapped || !l.resource.Resource.Alive() {
1000- return nil
1001- }
1002- return s.clientPointers(l.resource.Client())
1003-}
1004-
1005-func (s *Server) clientPointers(c server.Client) []*wayland.Pointer {
1006- var result []*wayland.Pointer
1007- for _, p := range s.pointers[c] {
1008- if p.Resource.Alive() {
1009- result = append(result, p)
1010- }
1011- }
1012- return result
1013-}
1014-func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
1015- if id == s.pointerFocus {
1016- return
1017- }
1018- s.wheelRest, s.wheelHeld = [2]int32{}, [2]float64{}
1019- // The new client sets its own cursor on enter; until then, the arrow.
1020- s.cursorSurface = nil
1021- s.setCursor(ports.CursorChange{})
1022- if surface, pointers, _, _ := s.pointerSurface(s.pointerFocus, 0, 0); surface != nil {
1023- for _, p := range pointers {
1024- s.serial++
1025- p.SendLeave(s.serial, surface)
1026- pointerFrame(p)
1027- }
1028- }
1029- s.pointerFocus = 0
1030- if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil {
1031- s.pointerFocus, s.pointerX, s.pointerY = id, x, y
1032- for _, p := range pointers {
1033- s.serial++
1034- p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy))
1035- pointerFrame(p)
1036- }
1037- }
1038- s.updateConstraint()
1039-}
M internal/adapters/wayland/server_test.go +2 -1
@@ -3,7 +3,6 @@ package wayland
33 import (
44 "bytes"
55 "context"
6- "github.com/bnema/neferwl/internal/ports"
76 "os"
87 "os/exec"
98 "path/filepath"
@@ -13,6 +12,8 @@ import (
1312 "testing"
1413 "time"
1514
15+ "github.com/bnema/neferwl/internal/ports"
16+
1617 "github.com/bnema/neferwl/internal/logging"
1718 )
1819
@@ -3,7 +3,6 @@ package wayland
3 import (3 import (
4 "bytes"4 "bytes"
5 "context"5 "context"
6- "github.com/bnema/neferwl/internal/ports"
7 "os"6 "os"
8 "os/exec"7 "os/exec"
9 "path/filepath"8 "path/filepath"
@@ -13,6 +12,8 @@ import (
13 "testing"12 "testing"
14 "time"13 "time"
15 14
15+ "github.com/bnema/neferwl/internal/ports"
16+
16 "github.com/bnema/neferwl/internal/logging"17 "github.com/bnema/neferwl/internal/logging"
17 )18 )
18 19
M internal/adapters/wayland/socket_test.go +2 -1
@@ -1,10 +1,11 @@
11 package wayland
22
33 import (
4- "golang.org/x/sys/unix"
54 "os"
65 "path/filepath"
76 "testing"
7+
8+ "golang.org/x/sys/unix"
89 )
910
1011 func TestListen(t *testing.T) {
@@ -1,10 +1,11 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4- "golang.org/x/sys/unix"
5 "os"4 "os"
6 "path/filepath"5 "path/filepath"
7 "testing"6 "testing"
7+
8+ "golang.org/x/sys/unix"
8 )9 )
9 10
10 func TestListen(t *testing.T) {11 func TestListen(t *testing.T) {
M internal/adapters/wayland/surface.go +104 -83
@@ -1,12 +1,13 @@
11 package wayland
22
33 import (
4- "github.com/bnema/purego-libwayland/protocol/presentationtime"
54 "image"
65 "math"
76 "slices"
87 "time"
98
9+ "github.com/bnema/purego-libwayland/protocol/presentationtime"
10+
1011 "github.com/bnema/neferwl/internal/ports"
1112 "github.com/bnema/purego-libwayland/protocol/wayland"
1213 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
@@ -28,27 +29,22 @@ type surface struct {
2829 wl *wayland.Surface
2930 viewport *viewport
3031 committedViewport viewportState // value snapshot; no allocation on viewport commits
31- // bufferScale is committed state; pendingScale is set by set_buffer_scale.
32- bufferScale, pendingScale int
33- // transform is committed; pendingTransform is set by set_buffer_transform
34- // and, like the buffer scale, copied by every commit.
35- transform, pendingTransform ports.BufferTransform
36- // opaque is the committed opaque region (surface-local, logical);
37- // pendingOpaque is set by set_opaque_region. formatOpaque is the
38- // current buffer's own opacity (x formats).
39- opaque, pendingOpaque []ports.Rect
40- pendingOpaqueSet bool
41- formatOpaque bool
42- kind roleKind
43- xdg *xdgSurface
44- layer *layerSurface
45- server *Server
46- current, pending *wayland.Buffer
47- attached bool
48- lastW, lastH int // last logged buffer size
49- callbacks []*wayland.Callback
50- role func(bool)
51- destroyed bool
32+ // next is the state requested since the last commit (see pendingCommit).
33+ next pendingCommit
34+ // bufferScale, transform and opaque are committed state. formatOpaque is
35+ // the current buffer's own opacity (x formats).
36+ bufferScale int
37+ transform ports.BufferTransform
38+ opaque []ports.Rect
39+ formatOpaque bool
40+ kind roleKind
41+ xdg *xdgSurface
42+ layer *layerSurface
43+ server *Server
44+ current *wayland.Buffer
45+ lastW, lastH int // last logged buffer size
46+ role func(bool)
47+ destroyed bool
5248 // hotX, hotY is the click point of a cursor surface (logical).
5349 hotX, hotY int
5450 // on is the output the surface entered; scale is the last scale sent.
@@ -56,14 +52,14 @@ type surface struct {
5652 scale float64
5753 // content is the last content built from this surface's own buffer;
5854 // has is false while no buffer is attached.
59- inputAll, pendingInputAll, pendingInputSet bool
60- inputRects, pendingInputRects []ports.Rect
61- sentInput, lastInputAll bool
62- lastInputRects []ports.Rect
63- content ports.SurfaceContent
64- has bool
65- identity uint64
66- version uint64
55+ inputAll bool
56+ inputRects []ports.Rect
57+ sentInput, lastInputAll bool
58+ lastInputRects []ports.Rect
59+ content ports.SurfaceContent
60+ has bool
61+ identity uint64
62+ version uint64
6763 // cachedTree is never modified once returned to emitContent.
6864 cachedTree []ports.Subsurface
6965 treeDirty bool
@@ -71,39 +67,64 @@ type surface struct {
7167 queuedScale int
7268 queuedBuffer uint32
7369 sub subState
74- // tearing is the surface's wp_tearing_control_v1; async is the
75- // committed hint, pendingAsync the requested one.
76- tearing *tearingHandler
77- async, pendingAsync bool
78- colorControl *colorSurface
79- color, pendingColor SurfaceColor
80- representation, pendingRepresentation surfaceRepresentation
81- representationControl *representationSurface
82- // Presentation constraints (fifo.go): pending fifo requests and commit
83- // timestamp, the barrier, and the commits waiting to apply.
84- fifo *fifoHandler
85- timer *timerHandler
86- pendingBarrier, pendingWait bool
87- pendingTime time.Time
88- barrier bool
89- barrierAt time.Time
90- queue []*update
70+ // tearing is the surface's wp_tearing_control_v1; async the committed
71+ // hint.
72+ tearing *tearingHandler
73+ async bool
74+ colorControl *colorSurface
75+ color SurfaceColor
76+ representation surfaceRepresentation
77+ representationControl *representationSurface
78+ // Presentation constraints (fifo.go): the barrier, and the commits
79+ // waiting to apply.
80+ fifo *fifoHandler
81+ timer *timerHandler
82+ barrier bool
83+ barrierAt time.Time
84+ queue []*update
9185 // contentType is the wp_content_type_v1; contentKind the committed type.
92- contentType *contentTypeHandler
93- contentKind, pendingKind uint32
94- // Damage requested since the last commit: in surface (logical) and
95- // buffer pixels; committed is what the last commit changed, in buffer
96- // pixels (full: everything), read by the window's damage history.
97- pendingDamage, pendingBufDamage []ports.Rect
98- committed damage
99- // pendingFeedback are wp_presentation feedbacks for the next commit.
100- pendingFeedback []*presentationtime.WpPresentationFeedback
101- // Explicit sync (syncobj.go): the surface's syncobj object; the
102- // committed points of the pending commit (pendingSync, with the wait
103- // for its acquire point); and the current buffer's hold.
104- sync *syncState
105- pendingSync *commitSync
106- hold syncHold
86+ contentType *contentTypeHandler
87+ contentKind uint32
88+ // committed is what the last commit changed, in buffer pixels (full:
89+ // everything), read by the window's damage history.
90+ committed damage
91+ // Explicit sync (syncobj.go): the surface's syncobj object and the
92+ // current buffer's hold.
93+ sync *syncState
94+ hold syncHold
95+}
96+
97+// pendingCommit is the double-buffered wl_surface state requested since the
98+// last commit. A commit captures it whole into an update (takePending):
99+// one-shot parts are cleared, sticky parts (scale, transform, hints, color)
100+// carry over to the next commit.
101+type pendingCommit struct {
102+ // buffer is attached (attached reports an attach request, nil detaches).
103+ buffer *wayland.Buffer
104+ attached bool
105+ // scale is set by set_buffer_scale, transform by set_buffer_transform.
106+ scale int
107+ transform ports.BufferTransform
108+ // opaque and the input region are set by their requests (the Set flags).
109+ opaque []ports.Rect
110+ opaqueSet bool
111+ inputAll, inputSet bool
112+ inputRects []ports.Rect
113+ callbacks []*wayland.Callback
114+ async bool
115+ color SurfaceColor
116+ representation surfaceRepresentation
117+ kind uint32
118+ // barrier and wait are fifo requests; at the commit timestamp.
119+ barrier, wait bool
120+ at time.Time
121+ // damage is in surface (logical) pixels, bufDamage in buffer pixels.
122+ damage, bufDamage []ports.Rect
123+ // feedback are wp_presentation feedbacks for this commit.
124+ feedback []*presentationtime.WpPresentationFeedback
125+ // sync is the commit's explicit-sync points, with the wait for its
126+ // acquire point.
127+ sync *commitSync
107128 }
108129
109130 // commitSync is a commit's explicit-sync points and acquire wait.
@@ -255,10 +276,10 @@ func (s *surface) detach() {
255276
256277 func (s *surface) Destroy(*wayland.Surface) {
257278 s.destroyed = true
258- for _, fb := range s.pendingFeedback {
279+ for _, fb := range s.next.feedback {
259280 discard(fb)
260281 }
261- s.pendingFeedback = nil
282+ s.next.feedback = nil
262283 s.tearing = nil // the control becomes inert
263284 s.colorControl = nil
264285 s.representationControl = nil
@@ -276,18 +297,18 @@ func (s *surface) Destroy(*wayland.Surface) {
276297 }
277298 s.sub.children = nil
278299 s.sub.layout, s.sub.pendingLayout = nil, nil
279- for _, cb := range s.callbacks {
300+ for _, cb := range s.next.callbacks {
280301 cb.Destroy()
281302 }
282- s.callbacks = nil
303+ s.next.callbacks = nil
283304 // The client may reuse the buffer on another surface.
284- s.dropSync(s.pendingSync)
285- s.pendingSync = nil
305+ s.dropSync(s.next.sync)
306+ s.next.sync = nil
286307 if s.current != nil {
287308 s.server.releaseBuffer(s, s.current, s.hold)
288309 s.hold = syncHold{}
289310 }
290- s.current, s.pending = nil, nil
311+ s.current, s.next.buffer = nil, nil
291312 if s.role != nil {
292313 s.role(false)
293314 }
@@ -296,21 +317,21 @@ func (s *surface) Destroy(*wayland.Surface) {
296317 }
297318 }
298319 func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) {
299- s.pending = b
300- s.attached = true
320+ s.next.buffer = b
321+ s.next.attached = true
301322 }
302323 func (s *surface) Frame(r *wayland.Surface, id uint32) {
303324 cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{})
304325 if err == nil {
305- s.callbacks = append(s.callbacks, cb)
326+ s.next.callbacks = append(s.next.callbacks, cb)
306327 }
307328 }
308329 func (s *surface) Commit(*wayland.Surface) {
309- if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked {
330+ if s.layer != nil && s.next.attached && s.next.buffer != nil && !s.layer.acked {
310331 s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack")
311332 return
312333 }
313- if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked {
334+ if s.xdg != nil && s.next.attached && s.next.buffer != nil && !s.xdg.acked {
314335 s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")
315336 return
316337 }
@@ -481,13 +502,13 @@ func (s *surface) applyCommit(u *update) {
481502 }
482503 }
483504 func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) {
484- if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects {
485- s.pendingDamage = append(s.pendingDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})
505+ if w > 0 && h > 0 && len(s.next.damage) <= maxDamageRects {
506+ s.next.damage = append(s.next.damage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})
486507 }
487508 }
488509 func (s *surface) DamageBuffer(_ *wayland.Surface, x, y, w, h int32) {
489- if w > 0 && h > 0 && len(s.pendingBufDamage) <= maxDamageRects {
490- s.pendingBufDamage = append(s.pendingBufDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})
510+ if w > 0 && h > 0 && len(s.next.bufDamage) <= maxDamageRects {
511+ s.next.bufDamage = append(s.next.bufDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})
491512 }
492513 }
493514
@@ -542,11 +563,11 @@ func (s *surface) commitDamage(u *update, fresh, resized bool, bw, bh int) {
542563
543564 // SetOpaqueRegion copies the region: a nil region means nothing is opaque.
544565 func (s *surface) SetOpaqueRegion(_ *wayland.Surface, reg *wayland.Region) {
545- s.pendingOpaqueSet = true
546- s.pendingOpaque = nil
566+ s.next.opaqueSet = true
567+ s.next.opaque = nil
547568 if reg != nil && reg.Resource != nil {
548569 if g := s.server.regions[reg.Resource]; g != nil {
549- s.pendingOpaque = slices.Clone(g.rects)
570+ s.next.opaque = slices.Clone(g.rects)
550571 }
551572 }
552573 }
@@ -556,7 +577,7 @@ func (s *surface) SetBufferTransform(r *wayland.Surface, v int32) {
556577 r.PostError(uint32(wayland.SurfaceErrorInvalidTransform), "invalid buffer transform")
557578 return
558579 }
559- s.pendingTransform = ports.BufferTransform(v)
580+ s.next.transform = ports.BufferTransform(v)
560581 }
561582
562583 // opaqueCovers reports whether the committed opaque region covers the whole
@@ -612,7 +633,7 @@ func (s *surface) SetBufferScale(r *wayland.Surface, v int32) {
612633 r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive")
613634 return
614635 }
615- s.pendingScale = int(v)
636+ s.next.scale = int(v)
616637 }
617638 func (*surface) Offset(*wayland.Surface, int32, int32) {}
618639 func (*surface) GetRelease(*wayland.Surface, uint32) {}
@@ -1,12 +1,13 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4- "github.com/bnema/purego-libwayland/protocol/presentationtime"
5 "image"4 "image"
6 "math"5 "math"
7 "slices"6 "slices"
8 "time"7 "time"
9 8
9+ "github.com/bnema/purego-libwayland/protocol/presentationtime"
10+
10 "github.com/bnema/neferwl/internal/ports"11 "github.com/bnema/neferwl/internal/ports"
11 "github.com/bnema/purego-libwayland/protocol/wayland"12 "github.com/bnema/purego-libwayland/protocol/wayland"
12 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"13 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
@@ -28,27 +29,22 @@ type surface struct {
28 wl *wayland.Surface29 wl *wayland.Surface
29 viewport *viewport30 viewport *viewport
30 committedViewport viewportState // value snapshot; no allocation on viewport commits31 committedViewport viewportState // value snapshot; no allocation on viewport commits
31- // bufferScale is committed state; pendingScale is set by set_buffer_scale.32+ // next is the state requested since the last commit (see pendingCommit).
32- bufferScale, pendingScale int33+ next pendingCommit
33- // transform is committed; pendingTransform is set by set_buffer_transform34+ // bufferScale, transform and opaque are committed state. formatOpaque is
34- // and, like the buffer scale, copied by every commit.35+ // the current buffer's own opacity (x formats).
35- transform, pendingTransform ports.BufferTransform36+ bufferScale int
36- // opaque is the committed opaque region (surface-local, logical);37+ transform ports.BufferTransform
37- // pendingOpaque is set by set_opaque_region. formatOpaque is the38+ opaque []ports.Rect
38- // current buffer's own opacity (x formats).39+ formatOpaque bool
39- opaque, pendingOpaque []ports.Rect40+ kind roleKind
40- pendingOpaqueSet bool41+ xdg *xdgSurface
41- formatOpaque bool42+ layer *layerSurface
42- kind roleKind43+ server *Server
43- xdg *xdgSurface44+ current *wayland.Buffer
44- layer *layerSurface45+ lastW, lastH int // last logged buffer size
45- server *Server46+ role func(bool)
46- current, pending *wayland.Buffer47+ destroyed bool
47- attached bool
48- lastW, lastH int // last logged buffer size
49- callbacks []*wayland.Callback
50- role func(bool)
51- destroyed bool
52 // hotX, hotY is the click point of a cursor surface (logical).48 // hotX, hotY is the click point of a cursor surface (logical).
53 hotX, hotY int49 hotX, hotY int
54 // on is the output the surface entered; scale is the last scale sent.50 // on is the output the surface entered; scale is the last scale sent.
@@ -56,14 +52,14 @@ type surface struct {
56 scale float6452 scale float64
57 // content is the last content built from this surface's own buffer;53 // content is the last content built from this surface's own buffer;
58 // has is false while no buffer is attached.54 // has is false while no buffer is attached.
59- inputAll, pendingInputAll, pendingInputSet bool55+ inputAll bool
60- inputRects, pendingInputRects []ports.Rect56+ inputRects []ports.Rect
61- sentInput, lastInputAll bool57+ sentInput, lastInputAll bool
62- lastInputRects []ports.Rect58+ lastInputRects []ports.Rect
63- content ports.SurfaceContent59+ content ports.SurfaceContent
64- has bool60+ has bool
65- identity uint6461+ identity uint64
66- version uint6462+ version uint64
67 // cachedTree is never modified once returned to emitContent.63 // cachedTree is never modified once returned to emitContent.
68 cachedTree []ports.Subsurface64 cachedTree []ports.Subsurface
69 treeDirty bool65 treeDirty bool
@@ -71,39 +67,64 @@ type surface struct {
71 queuedScale int67 queuedScale int
72 queuedBuffer uint3268 queuedBuffer uint32
73 sub subState69 sub subState
74- // tearing is the surface's wp_tearing_control_v1; async is the70+ // tearing is the surface's wp_tearing_control_v1; async the committed
75- // committed hint, pendingAsync the requested one.71+ // hint.
76- tearing *tearingHandler72+ tearing *tearingHandler
77- async, pendingAsync bool73+ async bool
78- colorControl *colorSurface74+ colorControl *colorSurface
79- color, pendingColor SurfaceColor75+ color SurfaceColor
80- representation, pendingRepresentation surfaceRepresentation76+ representation surfaceRepresentation
81- representationControl *representationSurface77+ representationControl *representationSurface
82- // Presentation constraints (fifo.go): pending fifo requests and commit78+ // Presentation constraints (fifo.go): the barrier, and the commits
83- // timestamp, the barrier, and the commits waiting to apply.79+ // waiting to apply.
84- fifo *fifoHandler80+ fifo *fifoHandler
85- timer *timerHandler81+ timer *timerHandler
86- pendingBarrier, pendingWait bool82+ barrier bool
87- pendingTime time.Time83+ barrierAt time.Time
88- barrier bool84+ queue []*update
89- barrierAt time.Time
90- queue []*update
91 // contentType is the wp_content_type_v1; contentKind the committed type.85 // contentType is the wp_content_type_v1; contentKind the committed type.
92- contentType *contentTypeHandler86+ contentType *contentTypeHandler
93- contentKind, pendingKind uint3287+ contentKind uint32
94- // Damage requested since the last commit: in surface (logical) and88+ // committed is what the last commit changed, in buffer pixels (full:
95- // buffer pixels; committed is what the last commit changed, in buffer89+ // everything), read by the window's damage history.
96- // pixels (full: everything), read by the window's damage history.90+ committed damage
97- pendingDamage, pendingBufDamage []ports.Rect91+ // Explicit sync (syncobj.go): the surface's syncobj object and the
98- committed damage92+ // current buffer's hold.
99- // pendingFeedback are wp_presentation feedbacks for the next commit.93+ sync *syncState
100- pendingFeedback []*presentationtime.WpPresentationFeedback94+ hold syncHold
101- // Explicit sync (syncobj.go): the surface's syncobj object; the95+}
102- // committed points of the pending commit (pendingSync, with the wait96+
103- // for its acquire point); and the current buffer's hold.97+// pendingCommit is the double-buffered wl_surface state requested since the
104- sync *syncState98+// last commit. A commit captures it whole into an update (takePending):
105- pendingSync *commitSync99+// one-shot parts are cleared, sticky parts (scale, transform, hints, color)
106- hold syncHold100+// carry over to the next commit.
101+type pendingCommit struct {
102+ // buffer is attached (attached reports an attach request, nil detaches).
103+ buffer *wayland.Buffer
104+ attached bool
105+ // scale is set by set_buffer_scale, transform by set_buffer_transform.
106+ scale int
107+ transform ports.BufferTransform
108+ // opaque and the input region are set by their requests (the Set flags).
109+ opaque []ports.Rect
110+ opaqueSet bool
111+ inputAll, inputSet bool
112+ inputRects []ports.Rect
113+ callbacks []*wayland.Callback
114+ async bool
115+ color SurfaceColor
116+ representation surfaceRepresentation
117+ kind uint32
118+ // barrier and wait are fifo requests; at the commit timestamp.
119+ barrier, wait bool
120+ at time.Time
121+ // damage is in surface (logical) pixels, bufDamage in buffer pixels.
122+ damage, bufDamage []ports.Rect
123+ // feedback are wp_presentation feedbacks for this commit.
124+ feedback []*presentationtime.WpPresentationFeedback
125+ // sync is the commit's explicit-sync points, with the wait for its
126+ // acquire point.
127+ sync *commitSync
107 }128 }
108 129
109 // commitSync is a commit's explicit-sync points and acquire wait.130 // commitSync is a commit's explicit-sync points and acquire wait.
@@ -255,10 +276,10 @@ func (s *surface) detach() {
255 276
256 func (s *surface) Destroy(*wayland.Surface) {277 func (s *surface) Destroy(*wayland.Surface) {
257 s.destroyed = true278 s.destroyed = true
258- for _, fb := range s.pendingFeedback {279+ for _, fb := range s.next.feedback {
259 discard(fb)280 discard(fb)
260 }281 }
261- s.pendingFeedback = nil282+ s.next.feedback = nil
262 s.tearing = nil // the control becomes inert283 s.tearing = nil // the control becomes inert
263 s.colorControl = nil284 s.colorControl = nil
264 s.representationControl = nil285 s.representationControl = nil
@@ -276,18 +297,18 @@ func (s *surface) Destroy(*wayland.Surface) {
276 }297 }
277 s.sub.children = nil298 s.sub.children = nil
278 s.sub.layout, s.sub.pendingLayout = nil, nil299 s.sub.layout, s.sub.pendingLayout = nil, nil
279- for _, cb := range s.callbacks {300+ for _, cb := range s.next.callbacks {
280 cb.Destroy()301 cb.Destroy()
281 }302 }
282- s.callbacks = nil303+ s.next.callbacks = nil
283 // The client may reuse the buffer on another surface.304 // The client may reuse the buffer on another surface.
284- s.dropSync(s.pendingSync)305+ s.dropSync(s.next.sync)
285- s.pendingSync = nil306+ s.next.sync = nil
286 if s.current != nil {307 if s.current != nil {
287 s.server.releaseBuffer(s, s.current, s.hold)308 s.server.releaseBuffer(s, s.current, s.hold)
288 s.hold = syncHold{}309 s.hold = syncHold{}
289 }310 }
290- s.current, s.pending = nil, nil311+ s.current, s.next.buffer = nil, nil
291 if s.role != nil {312 if s.role != nil {
292 s.role(false)313 s.role(false)
293 }314 }
@@ -296,21 +317,21 @@ func (s *surface) Destroy(*wayland.Surface) {
296 }317 }
297 }318 }
298 func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) {319 func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) {
299- s.pending = b320+ s.next.buffer = b
300- s.attached = true321+ s.next.attached = true
301 }322 }
302 func (s *surface) Frame(r *wayland.Surface, id uint32) {323 func (s *surface) Frame(r *wayland.Surface, id uint32) {
303 cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{})324 cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{})
304 if err == nil {325 if err == nil {
305- s.callbacks = append(s.callbacks, cb)326+ s.next.callbacks = append(s.next.callbacks, cb)
306 }327 }
307 }328 }
308 func (s *surface) Commit(*wayland.Surface) {329 func (s *surface) Commit(*wayland.Surface) {
309- if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked {330+ if s.layer != nil && s.next.attached && s.next.buffer != nil && !s.layer.acked {
310 s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack")331 s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack")
311 return332 return
312 }333 }
313- if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked {334+ if s.xdg != nil && s.next.attached && s.next.buffer != nil && !s.xdg.acked {
314 s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")335 s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")
315 return336 return
316 }337 }
@@ -481,13 +502,13 @@ func (s *surface) applyCommit(u *update) {
481 }502 }
482 }503 }
483 func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) {504 func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) {
484- if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects {505+ if w > 0 && h > 0 && len(s.next.damage) <= maxDamageRects {
485- s.pendingDamage = append(s.pendingDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})506+ s.next.damage = append(s.next.damage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})
486 }507 }
487 }508 }
488 func (s *surface) DamageBuffer(_ *wayland.Surface, x, y, w, h int32) {509 func (s *surface) DamageBuffer(_ *wayland.Surface, x, y, w, h int32) {
489- if w > 0 && h > 0 && len(s.pendingBufDamage) <= maxDamageRects {510+ if w > 0 && h > 0 && len(s.next.bufDamage) <= maxDamageRects {
490- s.pendingBufDamage = append(s.pendingBufDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})511+ s.next.bufDamage = append(s.next.bufDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})
491 }512 }
492 }513 }
493 514
@@ -542,11 +563,11 @@ func (s *surface) commitDamage(u *update, fresh, resized bool, bw, bh int) {
542 563
543 // SetOpaqueRegion copies the region: a nil region means nothing is opaque.564 // SetOpaqueRegion copies the region: a nil region means nothing is opaque.
544 func (s *surface) SetOpaqueRegion(_ *wayland.Surface, reg *wayland.Region) {565 func (s *surface) SetOpaqueRegion(_ *wayland.Surface, reg *wayland.Region) {
545- s.pendingOpaqueSet = true566+ s.next.opaqueSet = true
546- s.pendingOpaque = nil567+ s.next.opaque = nil
547 if reg != nil && reg.Resource != nil {568 if reg != nil && reg.Resource != nil {
548 if g := s.server.regions[reg.Resource]; g != nil {569 if g := s.server.regions[reg.Resource]; g != nil {
549- s.pendingOpaque = slices.Clone(g.rects)570+ s.next.opaque = slices.Clone(g.rects)
550 }571 }
551 }572 }
552 }573 }
@@ -556,7 +577,7 @@ func (s *surface) SetBufferTransform(r *wayland.Surface, v int32) {
556 r.PostError(uint32(wayland.SurfaceErrorInvalidTransform), "invalid buffer transform")577 r.PostError(uint32(wayland.SurfaceErrorInvalidTransform), "invalid buffer transform")
557 return578 return
558 }579 }
559- s.pendingTransform = ports.BufferTransform(v)580+ s.next.transform = ports.BufferTransform(v)
560 }581 }
561 582
562 // opaqueCovers reports whether the committed opaque region covers the whole583 // opaqueCovers reports whether the committed opaque region covers the whole
@@ -612,7 +633,7 @@ func (s *surface) SetBufferScale(r *wayland.Surface, v int32) {
612 r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive")633 r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive")
613 return634 return
614 }635 }
615- s.pendingScale = int(v)636+ s.next.scale = int(v)
616 }637 }
617 func (*surface) Offset(*wayland.Surface, int32, int32) {}638 func (*surface) Offset(*wayland.Surface, int32, int32) {}
618 func (*surface) GetRelease(*wayland.Surface, uint32) {}639 func (*surface) GetRelease(*wayland.Surface, uint32) {}
M internal/adapters/wayland/syncobj.go +4 -4
@@ -149,7 +149,7 @@ func (s *surface) checkSyncCommit() bool {
149149 }
150150 a, r := st.acquire, st.release
151151 // Points belong only to a commit that attaches a non-null buffer.
152- hasBuffer := s.attached && s.pending != nil
152+ hasBuffer := s.next.attached && s.next.buffer != nil
153153 res := st.resource
154154 switch {
155155 case !a.set() && !r.set():
@@ -176,8 +176,8 @@ func (s *surface) checkSyncCommit() bool {
176176
177177 // pendingIsSHM reports whether the buffer of the next commit is wl_shm.
178178 func (s *surface) pendingIsSHM() bool {
179- b := s.pending
180- if !s.attached {
179+ b := s.next.buffer
180+ if !s.next.attached {
181181 b = s.current
182182 }
183183 if b == nil {
@@ -373,7 +373,7 @@ func (s *surface) takeSyncPoints() {
373373 } else {
374374 cs.wait = w
375375 }
376- s.pendingSync = cs
376+ s.next.sync = cs
377377 }
378378
379379 // syncReady reports whether a commit's acquire point has its fence.
@@ -149,7 +149,7 @@ func (s *surface) checkSyncCommit() bool {
149 }149 }
150 a, r := st.acquire, st.release150 a, r := st.acquire, st.release
151 // Points belong only to a commit that attaches a non-null buffer.151 // Points belong only to a commit that attaches a non-null buffer.
152- hasBuffer := s.attached && s.pending != nil152+ hasBuffer := s.next.attached && s.next.buffer != nil
153 res := st.resource153 res := st.resource
154 switch {154 switch {
155 case !a.set() && !r.set():155 case !a.set() && !r.set():
@@ -176,8 +176,8 @@ func (s *surface) checkSyncCommit() bool {
176 176
177 // pendingIsSHM reports whether the buffer of the next commit is wl_shm.177 // pendingIsSHM reports whether the buffer of the next commit is wl_shm.
178 func (s *surface) pendingIsSHM() bool {178 func (s *surface) pendingIsSHM() bool {
179- b := s.pending179+ b := s.next.buffer
180- if !s.attached {180+ if !s.next.attached {
181 b = s.current181 b = s.current
182 }182 }
183 if b == nil {183 if b == nil {
@@ -373,7 +373,7 @@ func (s *surface) takeSyncPoints() {
373 } else {373 } else {
374 cs.wait = w374 cs.wait = w
375 }375 }
376- s.pendingSync = cs376+ s.next.sync = cs
377 }377 }
378 378
379 // syncReady reports whether a commit's acquire point has its fence.379 // syncReady reports whether a commit's acquire point has its fence.
M internal/adapters/wayland/tearing.go +2 -2
@@ -39,7 +39,7 @@ func (m tearingManager) GetTearingControl(r *tearingcontrol.WpTearingControlMana
3939 // Back to vsync on the next commit.
4040 if state.tearing == h {
4141 state.tearing = nil
42- state.pendingAsync = false
42+ state.next.async = false
4343 }
4444 }
4545 }
@@ -48,7 +48,7 @@ type tearingHandler struct{ surface *surface }
4848
4949 func (h *tearingHandler) SetPresentationHint(_ *tearingcontrol.WpTearingControlV1, hint uint32) {
5050 if h.surface != nil && h.surface.tearing == h {
51- h.surface.pendingAsync = hint == uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync)
51+ h.surface.next.async = hint == uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync)
5252 }
5353 }
5454
@@ -39,7 +39,7 @@ func (m tearingManager) GetTearingControl(r *tearingcontrol.WpTearingControlMana
39 // Back to vsync on the next commit.39 // Back to vsync on the next commit.
40 if state.tearing == h {40 if state.tearing == h {
41 state.tearing = nil41 state.tearing = nil
42- state.pendingAsync = false42+ state.next.async = false
43 }43 }
44 }44 }
45 }45 }
@@ -48,7 +48,7 @@ type tearingHandler struct{ surface *surface }
48 48
49 func (h *tearingHandler) SetPresentationHint(_ *tearingcontrol.WpTearingControlV1, hint uint32) {49 func (h *tearingHandler) SetPresentationHint(_ *tearingcontrol.WpTearingControlV1, hint uint32) {
50 if h.surface != nil && h.surface.tearing == h {50 if h.surface != nil && h.surface.tearing == h {
51- h.surface.pendingAsync = hint == uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync)51+ h.surface.next.async = hint == uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync)
52 }52 }
53 }53 }
54 54
M internal/adapters/wayland/textinput.go +20 -20
@@ -67,7 +67,7 @@ func (m textInputManager) GetTextInput(r *textinput.ZwpTextInputManagerV3, id ui
6767 }
6868 t.focus = nil
6969 }
70- if surf, _ := s.focusTarget(s.focused); surf != nil && surf.Client() == t.client {
70+ if surf, _ := s.focusTarget(s.seat.focused); surf != nil && surf.Client() == t.client {
7171 t.enter(surf)
7272 }
7373 }
@@ -168,7 +168,7 @@ func (s *Server) textCommitted(t *textInput, reenabled bool) {
168168 // text inputs on another surface, then enter for those of the focused
169169 // client, as the spec orders them.
170170 func (s *Server) textInputFocus() {
171- surf, _ := s.focusTarget(s.focused)
171+ surf, _ := s.focusTarget(s.seat.focused)
172172 for _, t := range s.textInputs {
173173 if t.focus != nil && (surf == nil || t.focus.Resource != surf.Resource) {
174174 t.leave()
@@ -417,12 +417,12 @@ func (im *inputMethod) GrabKeyboard(r *inputmethod.ZwpInputMethodV2, id uint32)
417417 }
418418 }
419419 s := im.server
420- if s.keymapFD >= 0 {
421- res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.keymapFD, s.keymapSize)
420+ if s.seat.keymapFD >= 0 {
421+ res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.seat.keymapFD, s.seat.keymapSize)
422422 }
423- res.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
423+ res.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
424424 s.serial++
425- m := s.modState
425+ m := s.seat.modState
426426 res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
427427 s.log.Debug().Msg("input method grabbed keyboard")
428428 }
@@ -468,29 +468,29 @@ func (s *Server) grab() *keyboardGrab {
468468 func (s *Server) grabKey(c ports.ForwardKey) bool {
469469 code := c.Key.Keycode
470470 g := s.grab()
471- owned := !c.Key.Pressed && s.grabKeys[code]
471+ owned := !c.Key.Pressed && s.seat.grabKeys[code]
472472 if owned {
473- delete(s.grabKeys, code)
473+ delete(s.seat.grabKeys, code)
474474 if g == nil {
475475 s.grabReleaseDropped(c.Key.State)
476476 return true
477477 }
478- } else if g == nil || c.ID != s.focused || !c.Key.Pressed && s.heldKeys[code] {
478+ } else if g == nil || c.ID != s.seat.focused || !c.Key.Pressed && s.seat.heldKeys[code] {
479479 return false
480480 }
481481 state := uint32(0)
482482 if c.Key.Pressed {
483483 state = 1
484- if s.grabKeys == nil {
485- s.grabKeys = make(map[uint32]bool)
484+ if s.seat.grabKeys == nil {
485+ s.seat.grabKeys = make(map[uint32]bool)
486486 }
487- s.grabKeys[code] = true
487+ s.seat.grabKeys[code] = true
488488 }
489489 s.serial++
490490 g.res.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
491- if c.Key.State != s.modState {
492- s.modState = c.Key.State
493- m := s.modState
491+ if c.Key.State != s.seat.modState {
492+ s.seat.modState = c.Key.State
493+ m := s.seat.modState
494494 s.serial++
495495 g.res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
496496 }
@@ -500,10 +500,10 @@ func (s *Server) grabKey(c ports.ForwardKey) bool {
500500 // grabReleaseDropped keeps the focused client's modifiers right when the
501501 // release of a grab-owned key is dropped.
502502 func (s *Server) grabReleaseDropped(m ports.ModState) {
503- if m == s.modState {
503+ if m == s.seat.modState {
504504 return
505505 }
506- s.modState = m
506+ s.seat.modState = m
507507 s.serial++
508508 for _, k := range s.clientKeyboards(s.focusClient()) {
509509 s.sendModifiers(k)
@@ -525,8 +525,8 @@ func (s *Server) sendGrabKeymap(keymap bool) {
525525 if g == nil {
526526 return
527527 }
528- if keymap && s.keymapFD >= 0 {
529- g.res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.keymapFD, s.keymapSize)
528+ if keymap && s.seat.keymapFD >= 0 {
529+ g.res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.seat.keymapFD, s.seat.keymapSize)
530530 }
531- g.res.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))
531+ g.res.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
532532 }
@@ -67,7 +67,7 @@ func (m textInputManager) GetTextInput(r *textinput.ZwpTextInputManagerV3, id ui
67 }67 }
68 t.focus = nil68 t.focus = nil
69 }69 }
70- if surf, _ := s.focusTarget(s.focused); surf != nil && surf.Client() == t.client {70+ if surf, _ := s.focusTarget(s.seat.focused); surf != nil && surf.Client() == t.client {
71 t.enter(surf)71 t.enter(surf)
72 }72 }
73 }73 }
@@ -168,7 +168,7 @@ func (s *Server) textCommitted(t *textInput, reenabled bool) {
168 // text inputs on another surface, then enter for those of the focused168 // text inputs on another surface, then enter for those of the focused
169 // client, as the spec orders them.169 // client, as the spec orders them.
170 func (s *Server) textInputFocus() {170 func (s *Server) textInputFocus() {
171- surf, _ := s.focusTarget(s.focused)171+ surf, _ := s.focusTarget(s.seat.focused)
172 for _, t := range s.textInputs {172 for _, t := range s.textInputs {
173 if t.focus != nil && (surf == nil || t.focus.Resource != surf.Resource) {173 if t.focus != nil && (surf == nil || t.focus.Resource != surf.Resource) {
174 t.leave()174 t.leave()
@@ -417,12 +417,12 @@ func (im *inputMethod) GrabKeyboard(r *inputmethod.ZwpInputMethodV2, id uint32)
417 }417 }
418 }418 }
419 s := im.server419 s := im.server
420- if s.keymapFD >= 0 {420+ if s.seat.keymapFD >= 0 {
421- res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.keymapFD, s.keymapSize)421+ res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.seat.keymapFD, s.seat.keymapSize)
422 }422 }
423- res.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))423+ res.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
424 s.serial++424 s.serial++
425- m := s.modState425+ m := s.seat.modState
426 res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)426 res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
427 s.log.Debug().Msg("input method grabbed keyboard")427 s.log.Debug().Msg("input method grabbed keyboard")
428 }428 }
@@ -468,29 +468,29 @@ func (s *Server) grab() *keyboardGrab {
468 func (s *Server) grabKey(c ports.ForwardKey) bool {468 func (s *Server) grabKey(c ports.ForwardKey) bool {
469 code := c.Key.Keycode469 code := c.Key.Keycode
470 g := s.grab()470 g := s.grab()
471- owned := !c.Key.Pressed && s.grabKeys[code]471+ owned := !c.Key.Pressed && s.seat.grabKeys[code]
472 if owned {472 if owned {
473- delete(s.grabKeys, code)473+ delete(s.seat.grabKeys, code)
474 if g == nil {474 if g == nil {
475 s.grabReleaseDropped(c.Key.State)475 s.grabReleaseDropped(c.Key.State)
476 return true476 return true
477 }477 }
478- } else if g == nil || c.ID != s.focused || !c.Key.Pressed && s.heldKeys[code] {478+ } else if g == nil || c.ID != s.seat.focused || !c.Key.Pressed && s.seat.heldKeys[code] {
479 return false479 return false
480 }480 }
481 state := uint32(0)481 state := uint32(0)
482 if c.Key.Pressed {482 if c.Key.Pressed {
483 state = 1483 state = 1
484- if s.grabKeys == nil {484+ if s.seat.grabKeys == nil {
485- s.grabKeys = make(map[uint32]bool)485+ s.seat.grabKeys = make(map[uint32]bool)
486 }486 }
487- s.grabKeys[code] = true487+ s.seat.grabKeys[code] = true
488 }488 }
489 s.serial++489 s.serial++
490 g.res.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)490 g.res.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
491- if c.Key.State != s.modState {491+ if c.Key.State != s.seat.modState {
492- s.modState = c.Key.State492+ s.seat.modState = c.Key.State
493- m := s.modState493+ m := s.seat.modState
494 s.serial++494 s.serial++
495 g.res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)495 g.res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
496 }496 }
@@ -500,10 +500,10 @@ func (s *Server) grabKey(c ports.ForwardKey) bool {
500 // grabReleaseDropped keeps the focused client's modifiers right when the500 // grabReleaseDropped keeps the focused client's modifiers right when the
501 // release of a grab-owned key is dropped.501 // release of a grab-owned key is dropped.
502 func (s *Server) grabReleaseDropped(m ports.ModState) {502 func (s *Server) grabReleaseDropped(m ports.ModState) {
503- if m == s.modState {503+ if m == s.seat.modState {
504 return504 return
505 }505 }
506- s.modState = m506+ s.seat.modState = m
507 s.serial++507 s.serial++
508 for _, k := range s.clientKeyboards(s.focusClient()) {508 for _, k := range s.clientKeyboards(s.focusClient()) {
509 s.sendModifiers(k)509 s.sendModifiers(k)
@@ -525,8 +525,8 @@ func (s *Server) sendGrabKeymap(keymap bool) {
525 if g == nil {525 if g == nil {
526 return526 return
527 }527 }
528- if keymap && s.keymapFD >= 0 {528+ if keymap && s.seat.keymapFD >= 0 {
529- g.res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.keymapFD, s.keymapSize)529+ g.res.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), s.seat.keymapFD, s.seat.keymapSize)
530 }530 }
531- g.res.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay))531+ g.res.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay))
532 }532 }
M internal/adapters/wayland/transform_test.go +5 -5
@@ -196,16 +196,16 @@ func TestOpaqueRegionContent(t *testing.T) {
196196 func TestCapturedTransformAndOpaqueKeepLaterRequests(t *testing.T) {
197197 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1), constraints: map[*surface]*constraint{}}
198198 surf := &surface{server: srv}
199- surf.pendingTransform = 1
200- surf.pendingOpaque, surf.pendingOpaqueSet = []ports.Rect{{W: 4, H: 4}}, true
199+ surf.next.transform = 1
200+ surf.next.opaque, surf.next.opaqueSet = []ports.Rect{{W: 4, H: 4}}, true
201201 surf.queueUpdate()
202- surf.pendingTransform = 2
203- surf.pendingOpaque, surf.pendingOpaqueSet = nil, true
202+ surf.next.transform = 2
203+ surf.next.opaque, surf.next.opaqueSet = nil, true
204204 surf.queue[0].applyGraph()
205205 if surf.transform != 1 || len(surf.opaque) != 1 {
206206 t.Fatalf("captured state: transform %d opaque %v", surf.transform, surf.opaque)
207207 }
208- if surf.pendingTransform != 2 || !surf.pendingOpaqueSet || surf.pendingOpaque != nil {
208+ if surf.next.transform != 2 || !surf.next.opaqueSet || surf.next.opaque != nil {
209209 t.Fatal("application consumed a later request")
210210 }
211211 surf.queueUpdate()
@@ -196,16 +196,16 @@ func TestOpaqueRegionContent(t *testing.T) {
196 func TestCapturedTransformAndOpaqueKeepLaterRequests(t *testing.T) {196 func TestCapturedTransformAndOpaqueKeepLaterRequests(t *testing.T) {
197 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1), constraints: map[*surface]*constraint{}}197 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1), constraints: map[*surface]*constraint{}}
198 surf := &surface{server: srv}198 surf := &surface{server: srv}
199- surf.pendingTransform = 1199+ surf.next.transform = 1
200- surf.pendingOpaque, surf.pendingOpaqueSet = []ports.Rect{{W: 4, H: 4}}, true200+ surf.next.opaque, surf.next.opaqueSet = []ports.Rect{{W: 4, H: 4}}, true
201 surf.queueUpdate()201 surf.queueUpdate()
202- surf.pendingTransform = 2202+ surf.next.transform = 2
203- surf.pendingOpaque, surf.pendingOpaqueSet = nil, true203+ surf.next.opaque, surf.next.opaqueSet = nil, true
204 surf.queue[0].applyGraph()204 surf.queue[0].applyGraph()
205 if surf.transform != 1 || len(surf.opaque) != 1 {205 if surf.transform != 1 || len(surf.opaque) != 1 {
206 t.Fatalf("captured state: transform %d opaque %v", surf.transform, surf.opaque)206 t.Fatalf("captured state: transform %d opaque %v", surf.transform, surf.opaque)
207 }207 }
208- if surf.pendingTransform != 2 || !surf.pendingOpaqueSet || surf.pendingOpaque != nil {208+ if surf.next.transform != 2 || !surf.next.opaqueSet || surf.next.opaque != nil {
209 t.Fatal("application consumed a later request")209 t.Fatal("application consumed a later request")
210 }210 }
211 surf.queueUpdate()211 surf.queueUpdate()
M internal/adapters/wayland/virtualkeyboard.go +15 -15
@@ -73,7 +73,7 @@ func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format
7373 k.fd, k.size = copyFD, copySize
7474 old := k.server.activeKeymapText()
7575 k.text = text
76- if s := k.server; s.keymapOwner == k && text != old {
76+ if s := k.server; s.seat.keymapOwner == k && text != old {
7777 // Clients must see the new layout before the next key.
7878 s.sendKeymapAll(k.fd, k.size)
7979 }
@@ -107,13 +107,13 @@ func (k *virtualKeyboard) keyboards(r *virtualkeyboard.ZwpVirtualKeyboardV1) []*
107107 return nil
108108 }
109109 s := k.server
110- _, keyboards := s.focusTarget(s.focused)
110+ _, keyboards := s.focusTarget(s.seat.focused)
111111 if len(keyboards) == 0 {
112112 return nil
113113 }
114- if k.focus != s.focused {
114+ if k.focus != s.seat.focused {
115115 clear(k.pressed)
116- k.focus = s.focused
116+ k.focus = s.seat.focused
117117 }
118118 s.useKeymap(k)
119119 s.serial++
@@ -148,8 +148,8 @@ func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {}
148148 // keys still held on the focused client are released.
149149 func (k *virtualKeyboard) release() {
150150 s := k.server
151- if len(k.pressed) > 0 && k.focus == s.focused {
152- _, keyboards := s.focusTarget(s.focused)
151+ if len(k.pressed) > 0 && k.focus == s.seat.focused {
152+ _, keyboards := s.focusTarget(s.seat.focused)
153153 var ts unix.Timespec
154154 _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
155155 // Input events carry CLOCK_MONOTONIC milliseconds (libinput's clock).
@@ -161,7 +161,7 @@ func (k *virtualKeyboard) release() {
161161 }
162162 }
163163 }
164- if s.keymapOwner == k {
164+ if s.seat.keymapOwner == k {
165165 s.useKeymap(nil)
166166 }
167167 if k.fd >= 0 {
@@ -173,11 +173,11 @@ func (k *virtualKeyboard) release() {
173173 // useKeymap makes every keyboard carry the keymap of k, or the seat keymap
174174 // when k is nil. Switching back restores the seat modifiers too.
175175 func (s *Server) useKeymap(k *virtualKeyboard) {
176- if s.keymapOwner == k {
176+ if s.seat.keymapOwner == k {
177177 return
178178 }
179179 old := s.activeKeymapText()
180- s.keymapOwner = k
180+ s.seat.keymapOwner = k
181181 // Like wlroots and smithay, a keymap equal to the active one is not
182182 // sent again: tools such as dictation apps create a virtual keyboard
183183 // with the seat layout per use, and each resend makes every client
@@ -188,7 +188,7 @@ func (s *Server) useKeymap(k *virtualKeyboard) {
188188 // Modifiers follow the keyboard that types: a skipped keymap no longer
189189 // resets them, so a virtual keyboard never types through seat Shift or
190190 // Caps Lock, and the seat gets its own back.
191- m := s.modState
191+ m := s.seat.modState
192192 if k != nil {
193193 m = k.mods
194194 }
@@ -200,7 +200,7 @@ func (s *Server) useKeymap(k *virtualKeyboard) {
200200 }
201201
202202 func (s *Server) sendKeymapAll(fd int, size uint32) {
203- for _, list := range s.keyboards {
203+ for _, list := range s.seat.keyboards {
204204 for _, kb := range list {
205205 if kb.Resource.Alive() {
206206 kb.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size)
@@ -211,16 +211,16 @@ func (s *Server) sendKeymapAll(fd int, size uint32) {
211211
212212 // activeKeymapText is the text of the keymap keyboards carry.
213213 func (s *Server) activeKeymapText() string {
214- if k := s.keymapOwner; k != nil {
214+ if k := s.seat.keymapOwner; k != nil {
215215 return k.text
216216 }
217- return s.keymapText
217+ return s.seat.keymapText
218218 }
219219
220220 // currentKeymap is the keymap a new keyboard must start with.
221221 func (s *Server) currentKeymap() (int, uint32) {
222- if k := s.keymapOwner; k != nil {
222+ if k := s.seat.keymapOwner; k != nil {
223223 return k.fd, k.size
224224 }
225- return s.keymapFD, s.keymapSize
225+ return s.seat.keymapFD, s.seat.keymapSize
226226 }
@@ -73,7 +73,7 @@ func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format
73 k.fd, k.size = copyFD, copySize73 k.fd, k.size = copyFD, copySize
74 old := k.server.activeKeymapText()74 old := k.server.activeKeymapText()
75 k.text = text75 k.text = text
76- if s := k.server; s.keymapOwner == k && text != old {76+ if s := k.server; s.seat.keymapOwner == k && text != old {
77 // Clients must see the new layout before the next key.77 // Clients must see the new layout before the next key.
78 s.sendKeymapAll(k.fd, k.size)78 s.sendKeymapAll(k.fd, k.size)
79 }79 }
@@ -107,13 +107,13 @@ func (k *virtualKeyboard) keyboards(r *virtualkeyboard.ZwpVirtualKeyboardV1) []*
107 return nil107 return nil
108 }108 }
109 s := k.server109 s := k.server
110- _, keyboards := s.focusTarget(s.focused)110+ _, keyboards := s.focusTarget(s.seat.focused)
111 if len(keyboards) == 0 {111 if len(keyboards) == 0 {
112 return nil112 return nil
113 }113 }
114- if k.focus != s.focused {114+ if k.focus != s.seat.focused {
115 clear(k.pressed)115 clear(k.pressed)
116- k.focus = s.focused116+ k.focus = s.seat.focused
117 }117 }
118 s.useKeymap(k)118 s.useKeymap(k)
119 s.serial++119 s.serial++
@@ -148,8 +148,8 @@ func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {}
148 // keys still held on the focused client are released.148 // keys still held on the focused client are released.
149 func (k *virtualKeyboard) release() {149 func (k *virtualKeyboard) release() {
150 s := k.server150 s := k.server
151- if len(k.pressed) > 0 && k.focus == s.focused {151+ if len(k.pressed) > 0 && k.focus == s.seat.focused {
152- _, keyboards := s.focusTarget(s.focused)152+ _, keyboards := s.focusTarget(s.seat.focused)
153 var ts unix.Timespec153 var ts unix.Timespec
154 _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)154 _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
155 // Input events carry CLOCK_MONOTONIC milliseconds (libinput's clock).155 // Input events carry CLOCK_MONOTONIC milliseconds (libinput's clock).
@@ -161,7 +161,7 @@ func (k *virtualKeyboard) release() {
161 }161 }
162 }162 }
163 }163 }
164- if s.keymapOwner == k {164+ if s.seat.keymapOwner == k {
165 s.useKeymap(nil)165 s.useKeymap(nil)
166 }166 }
167 if k.fd >= 0 {167 if k.fd >= 0 {
@@ -173,11 +173,11 @@ func (k *virtualKeyboard) release() {
173 // useKeymap makes every keyboard carry the keymap of k, or the seat keymap173 // useKeymap makes every keyboard carry the keymap of k, or the seat keymap
174 // when k is nil. Switching back restores the seat modifiers too.174 // when k is nil. Switching back restores the seat modifiers too.
175 func (s *Server) useKeymap(k *virtualKeyboard) {175 func (s *Server) useKeymap(k *virtualKeyboard) {
176- if s.keymapOwner == k {176+ if s.seat.keymapOwner == k {
177 return177 return
178 }178 }
179 old := s.activeKeymapText()179 old := s.activeKeymapText()
180- s.keymapOwner = k180+ s.seat.keymapOwner = k
181 // Like wlroots and smithay, a keymap equal to the active one is not181 // Like wlroots and smithay, a keymap equal to the active one is not
182 // sent again: tools such as dictation apps create a virtual keyboard182 // sent again: tools such as dictation apps create a virtual keyboard
183 // with the seat layout per use, and each resend makes every client183 // with the seat layout per use, and each resend makes every client
@@ -188,7 +188,7 @@ func (s *Server) useKeymap(k *virtualKeyboard) {
188 // Modifiers follow the keyboard that types: a skipped keymap no longer188 // Modifiers follow the keyboard that types: a skipped keymap no longer
189 // resets them, so a virtual keyboard never types through seat Shift or189 // resets them, so a virtual keyboard never types through seat Shift or
190 // Caps Lock, and the seat gets its own back.190 // Caps Lock, and the seat gets its own back.
191- m := s.modState191+ m := s.seat.modState
192 if k != nil {192 if k != nil {
193 m = k.mods193 m = k.mods
194 }194 }
@@ -200,7 +200,7 @@ func (s *Server) useKeymap(k *virtualKeyboard) {
200 }200 }
201 201
202 func (s *Server) sendKeymapAll(fd int, size uint32) {202 func (s *Server) sendKeymapAll(fd int, size uint32) {
203- for _, list := range s.keyboards {203+ for _, list := range s.seat.keyboards {
204 for _, kb := range list {204 for _, kb := range list {
205 if kb.Resource.Alive() {205 if kb.Resource.Alive() {
206 kb.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size)206 kb.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size)
@@ -211,16 +211,16 @@ func (s *Server) sendKeymapAll(fd int, size uint32) {
211 211
212 // activeKeymapText is the text of the keymap keyboards carry.212 // activeKeymapText is the text of the keymap keyboards carry.
213 func (s *Server) activeKeymapText() string {213 func (s *Server) activeKeymapText() string {
214- if k := s.keymapOwner; k != nil {214+ if k := s.seat.keymapOwner; k != nil {
215 return k.text215 return k.text
216 }216 }
217- return s.keymapText217+ return s.seat.keymapText
218 }218 }
219 219
220 // currentKeymap is the keymap a new keyboard must start with.220 // currentKeymap is the keymap a new keyboard must start with.
221 func (s *Server) currentKeymap() (int, uint32) {221 func (s *Server) currentKeymap() (int, uint32) {
222- if k := s.keymapOwner; k != nil {222+ if k := s.seat.keymapOwner; k != nil {
223 return k.fd, k.size223 return k.fd, k.size
224 }224 }
225- return s.keymapFD, s.keymapSize225+ return s.seat.keymapFD, s.seat.keymapSize
226 }226 }
M internal/adapters/wayland/virtualkeyboard_test.go +1 -1
@@ -222,7 +222,7 @@ func TestVirtualKeyboardSameKeymapOwnModifiers(t *testing.T) {
222222 target, proxy := focusedTarget(t, s, events, commands, dir)
223223 const capsLocked = 2
224224 set := make(chan struct{})
225- s.display.Do(func() { s.modState = ports.ModState{Locked: capsLocked}; close(set) })
225+ s.display.Do(func() { s.seat.modState = ports.ModState{Locked: capsLocked}; close(set) })
226226 <-set
227227 proxy.locked = nil
228228 typer, kb := virtualTyper(t, s, dir)
@@ -222,7 +222,7 @@ func TestVirtualKeyboardSameKeymapOwnModifiers(t *testing.T) {
222 target, proxy := focusedTarget(t, s, events, commands, dir)222 target, proxy := focusedTarget(t, s, events, commands, dir)
223 const capsLocked = 2223 const capsLocked = 2
224 set := make(chan struct{})224 set := make(chan struct{})
225- s.display.Do(func() { s.modState = ports.ModState{Locked: capsLocked}; close(set) })225+ s.display.Do(func() { s.seat.modState = ports.ModState{Locked: capsLocked}; close(set) })
226 <-set226 <-set
227 proxy.locked = nil227 proxy.locked = nil
228 typer, kb := virtualTyper(t, s, dir)228 typer, kb := virtualTyper(t, s, dir)
M internal/adapters/wayland/xdg.go +5 -4
@@ -2,6 +2,7 @@ package wayland
22
33 import (
44 "encoding/binary"
5+
56 "github.com/bnema/neferwl/internal/ports"
67 "github.com/bnema/purego-libwayland/protocol/wayland"
78 "github.com/bnema/purego-libwayland/protocol/xdgshell"
@@ -185,10 +186,10 @@ func toplevelStates(c ports.ConfigureWindow, version int32) []byte {
185186 }
186187
187188 func (w *window) unmap() {
188- if w.xdg.server.pointerFocus == w.id {
189+ if w.xdg.server.seat.pointerFocus == w.id {
189190 w.xdg.server.changePointerFocus(0, 0, 0)
190191 }
191- if w.xdg.server.focused == w.id {
192+ if w.xdg.server.seat.focused == w.id {
192193 w.xdg.server.changeFocus(0)
193194 }
194195 if !w.mapped {
@@ -245,8 +246,8 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
245246 t.OnDestroy = func() {
246247 w.unmap()
247248 x.surface.dropQueue()
248- x.surface.current, x.surface.pending = nil, nil
249- x.surface.attached = false
249+ x.surface.current, x.surface.next.buffer = nil, nil
250+ x.surface.next.attached = false
250251 delete(x.server.windows, w.id)
251252 x.window = nil
252253 x.surface.role = nil
@@ -2,6 +2,7 @@ package wayland
2 2
3 import (3 import (
4 "encoding/binary"4 "encoding/binary"
5+
5 "github.com/bnema/neferwl/internal/ports"6 "github.com/bnema/neferwl/internal/ports"
6 "github.com/bnema/purego-libwayland/protocol/wayland"7 "github.com/bnema/purego-libwayland/protocol/wayland"
7 "github.com/bnema/purego-libwayland/protocol/xdgshell"8 "github.com/bnema/purego-libwayland/protocol/xdgshell"
@@ -185,10 +186,10 @@ func toplevelStates(c ports.ConfigureWindow, version int32) []byte {
185 }186 }
186 187
187 func (w *window) unmap() {188 func (w *window) unmap() {
188- if w.xdg.server.pointerFocus == w.id {189+ if w.xdg.server.seat.pointerFocus == w.id {
189 w.xdg.server.changePointerFocus(0, 0, 0)190 w.xdg.server.changePointerFocus(0, 0, 0)
190 }191 }
191- if w.xdg.server.focused == w.id {192+ if w.xdg.server.seat.focused == w.id {
192 w.xdg.server.changeFocus(0)193 w.xdg.server.changeFocus(0)
193 }194 }
194 if !w.mapped {195 if !w.mapped {
@@ -245,8 +246,8 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
245 t.OnDestroy = func() {246 t.OnDestroy = func() {
246 w.unmap()247 w.unmap()
247 x.surface.dropQueue()248 x.surface.dropQueue()
248- x.surface.current, x.surface.pending = nil, nil249+ x.surface.current, x.surface.next.buffer = nil, nil
249- x.surface.attached = false250+ x.surface.next.attached = false
250 delete(x.server.windows, w.id)251 delete(x.server.windows, w.id)
251 x.window = nil252 x.window = nil
252 x.surface.role = nil253 x.surface.role = nil
A internal/core/configures.go +71 -0
@@ -0,0 +1,71 @@
1+package core
2+
3+import "github.com/bnema/neferwl/internal/ports"
4+
5+// configureTarget is where a placement is shown, as the configure needs it.
6+type configureTarget struct {
7+ output string
8+ area Rect // the output, to tell whether the window is on screen
9+ focused bool // activated: focused on the focused output
10+ // client is the placement minus borders; imposed reports core sized a
11+ // floating window (native floats pick their own size).
12+ client Rect
13+ imposed bool
14+}
15+
16+// configures owns the last configure sent to each window and decides the
17+// next one. Configures are sent only when they change.
18+type configures struct {
19+ sent map[WindowID]ports.ConfigureWindow
20+ seen map[WindowID]bool
21+}
22+
23+func newConfigures() configures {
24+ return configures{sent: map[WindowID]ports.ConfigureWindow{}, seen: map[WindowID]bool{}}
25+}
26+
27+// next returns the configure for p at t and whether it must be sent. The
28+// caller records it with mark once sent.
29+func (s *configures) next(p Placement, t configureTarget) (ports.ConfigureWindow, bool) {
30+ s.seen[p.ID] = true
31+ old, ok := s.sent[p.ID]
32+ var v ports.ConfigureWindow
33+ switch {
34+ case p.Hidden:
35+ // A hidden window keeps its size, state and output; it is only
36+ // deactivated and marked invisible. One mapped hidden is told so
37+ // at once, sized by the client.
38+ v = old
39+ if !ok {
40+ v = ports.ConfigureWindow{ID: p.ID, Floating: p.Floating}
41+ }
42+ v.Activated, v.Visible, v.Output = false, false, t.output
43+ case p.Preview > 0:
44+ // A preview keeps its client's size: only its focus changes.
45+ v = old
46+ if !ok {
47+ v = ports.ConfigureWindow{ID: p.ID, Width: int(float64(p.Rect.W) / p.Preview), Height: int(float64(p.Rect.H) / p.Preview)}
48+ }
49+ v.Activated, v.Visible, v.Output = t.focused, onScreen(p, t.area), t.output
50+ default:
51+ v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: onScreen(p, t.area)}
52+ if v.Floating && !t.imposed {
53+ v.Width, v.Height = 0, 0
54+ }
55+ }
56+ return v, !ok || v != old
57+}
58+
59+// mark records v as sent.
60+func (s *configures) mark(v ports.ConfigureWindow) { s.sent[v.ID] = v }
61+
62+// prune forgets the windows no next call saw since the last prune: they
63+// left the layout.
64+func (s *configures) prune() {
65+ for id := range s.sent {
66+ if !s.seen[id] {
67+ delete(s.sent, id)
68+ }
69+ }
70+ clear(s.seen)
71+}
@@ -0,0 +1,71 @@
1+package core
2+
3+import "github.com/bnema/neferwl/internal/ports"
4+
5+// configureTarget is where a placement is shown, as the configure needs it.
6+type configureTarget struct {
7+ output string
8+ area Rect // the output, to tell whether the window is on screen
9+ focused bool // activated: focused on the focused output
10+ // client is the placement minus borders; imposed reports core sized a
11+ // floating window (native floats pick their own size).
12+ client Rect
13+ imposed bool
14+}
15+
16+// configures owns the last configure sent to each window and decides the
17+// next one. Configures are sent only when they change.
18+type configures struct {
19+ sent map[WindowID]ports.ConfigureWindow
20+ seen map[WindowID]bool
21+}
22+
23+func newConfigures() configures {
24+ return configures{sent: map[WindowID]ports.ConfigureWindow{}, seen: map[WindowID]bool{}}
25+}
26+
27+// next returns the configure for p at t and whether it must be sent. The
28+// caller records it with mark once sent.
29+func (s *configures) next(p Placement, t configureTarget) (ports.ConfigureWindow, bool) {
30+ s.seen[p.ID] = true
31+ old, ok := s.sent[p.ID]
32+ var v ports.ConfigureWindow
33+ switch {
34+ case p.Hidden:
35+ // A hidden window keeps its size, state and output; it is only
36+ // deactivated and marked invisible. One mapped hidden is told so
37+ // at once, sized by the client.
38+ v = old
39+ if !ok {
40+ v = ports.ConfigureWindow{ID: p.ID, Floating: p.Floating}
41+ }
42+ v.Activated, v.Visible, v.Output = false, false, t.output
43+ case p.Preview > 0:
44+ // A preview keeps its client's size: only its focus changes.
45+ v = old
46+ if !ok {
47+ v = ports.ConfigureWindow{ID: p.ID, Width: int(float64(p.Rect.W) / p.Preview), Height: int(float64(p.Rect.H) / p.Preview)}
48+ }
49+ v.Activated, v.Visible, v.Output = t.focused, onScreen(p, t.area), t.output
50+ default:
51+ v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: onScreen(p, t.area)}
52+ if v.Floating && !t.imposed {
53+ v.Width, v.Height = 0, 0
54+ }
55+ }
56+ return v, !ok || v != old
57+}
58+
59+// mark records v as sent.
60+func (s *configures) mark(v ports.ConfigureWindow) { s.sent[v.ID] = v }
61+
62+// prune forgets the windows no next call saw since the last prune: they
63+// left the layout.
64+func (s *configures) prune() {
65+ for id := range s.sent {
66+ if !s.seen[id] {
67+ delete(s.sent, id)
68+ }
69+ }
70+ clear(s.seen)
71+}
A internal/core/configures_test.go +65 -0
@@ -0,0 +1,65 @@
1+package core
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+func TestConfiguresNext(t *testing.T) {
10+ area := Rect{W: 1000, H: 800}
11+ tiled := Placement{ID: 1, Rect: Rect{W: 500, H: 800}}
12+ target := configureTarget{output: "A", area: area, focused: true, client: Rect{W: 496, H: 796}}
13+ tests := []struct {
14+ name string
15+ first *Placement // sent before p, with target
16+ p Placement
17+ t configureTarget
18+ want ports.ConfigureWindow
19+ send bool
20+ }{
21+ {name: "tiled", p: tiled, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A", Visible: true}, send: true},
22+ {name: "unchanged", first: &tiled, p: tiled, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A", Visible: true}},
23+ {name: "scrolled off", p: Placement{ID: 1, Rect: Rect{X: 1000, W: 500, H: 800}}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A"}, send: true},
24+ {name: "native float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Activated: true, Floating: true, Output: "A", Visible: true}, send: true},
25+ {name: "imposed float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: configureTarget{output: "A", area: area, client: Rect{W: 300, H: 200}, imposed: true}, want: ports.ConfigureWindow{ID: 1, Width: 300, Height: 200, Floating: true, Output: "A", Visible: true}, send: true},
26+ {name: "fullscreen float", p: Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, t: configureTarget{output: "A", area: area, client: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true},
27+ {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true},
28+ {name: "hidden keeps size", first: &tiled, p: Placement{ID: 1, Hidden: true}, t: configureTarget{output: "B"}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "B"}, send: true},
29+ {name: "preview new", p: Placement{ID: 1, Rect: Rect{W: 250, H: 200}, Preview: 0.5}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 500, Height: 400, Activated: true, Output: "A", Visible: true}, send: true},
30+ {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true},
31+ }
32+ for _, tt := range tests {
33+ t.Run(tt.name, func(t *testing.T) {
34+ s := newConfigures()
35+ if tt.first != nil {
36+ v, _ := s.next(*tt.first, target)
37+ s.mark(v)
38+ }
39+ got, send := s.next(tt.p, tt.t)
40+ if got != tt.want || send != tt.send {
41+ t.Fatalf("next = %+v, %v; want %+v, %v", got, send, tt.want, tt.send)
42+ }
43+ })
44+ }
45+}
46+
47+func TestConfiguresPruneForgetsUnseen(t *testing.T) {
48+ s := newConfigures()
49+ for _, id := range []WindowID{1, 2} {
50+ v, _ := s.next(Placement{ID: id}, configureTarget{})
51+ s.mark(v)
52+ }
53+ s.prune()
54+ s.next(Placement{ID: 1}, configureTarget{})
55+ s.prune()
56+ if _, ok := s.sent[2]; ok {
57+ t.Fatal("window out of the layout kept")
58+ }
59+ if _, send := s.next(Placement{ID: 1}, configureTarget{}); send {
60+ t.Fatal("window still in the layout configured again")
61+ }
62+ if _, send := s.next(Placement{ID: 2}, configureTarget{}); !send {
63+ t.Fatal("returning window not configured")
64+ }
65+}
@@ -0,0 +1,65 @@
1+package core
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+func TestConfiguresNext(t *testing.T) {
10+ area := Rect{W: 1000, H: 800}
11+ tiled := Placement{ID: 1, Rect: Rect{W: 500, H: 800}}
12+ target := configureTarget{output: "A", area: area, focused: true, client: Rect{W: 496, H: 796}}
13+ tests := []struct {
14+ name string
15+ first *Placement // sent before p, with target
16+ p Placement
17+ t configureTarget
18+ want ports.ConfigureWindow
19+ send bool
20+ }{
21+ {name: "tiled", p: tiled, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A", Visible: true}, send: true},
22+ {name: "unchanged", first: &tiled, p: tiled, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A", Visible: true}},
23+ {name: "scrolled off", p: Placement{ID: 1, Rect: Rect{X: 1000, W: 500, H: 800}}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A"}, send: true},
24+ {name: "native float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Activated: true, Floating: true, Output: "A", Visible: true}, send: true},
25+ {name: "imposed float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: configureTarget{output: "A", area: area, client: Rect{W: 300, H: 200}, imposed: true}, want: ports.ConfigureWindow{ID: 1, Width: 300, Height: 200, Floating: true, Output: "A", Visible: true}, send: true},
26+ {name: "fullscreen float", p: Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, t: configureTarget{output: "A", area: area, client: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true},
27+ {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true},
28+ {name: "hidden keeps size", first: &tiled, p: Placement{ID: 1, Hidden: true}, t: configureTarget{output: "B"}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "B"}, send: true},
29+ {name: "preview new", p: Placement{ID: 1, Rect: Rect{W: 250, H: 200}, Preview: 0.5}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 500, Height: 400, Activated: true, Output: "A", Visible: true}, send: true},
30+ {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true},
31+ }
32+ for _, tt := range tests {
33+ t.Run(tt.name, func(t *testing.T) {
34+ s := newConfigures()
35+ if tt.first != nil {
36+ v, _ := s.next(*tt.first, target)
37+ s.mark(v)
38+ }
39+ got, send := s.next(tt.p, tt.t)
40+ if got != tt.want || send != tt.send {
41+ t.Fatalf("next = %+v, %v; want %+v, %v", got, send, tt.want, tt.send)
42+ }
43+ })
44+ }
45+}
46+
47+func TestConfiguresPruneForgetsUnseen(t *testing.T) {
48+ s := newConfigures()
49+ for _, id := range []WindowID{1, 2} {
50+ v, _ := s.next(Placement{ID: id}, configureTarget{})
51+ s.mark(v)
52+ }
53+ s.prune()
54+ s.next(Placement{ID: 1}, configureTarget{})
55+ s.prune()
56+ if _, ok := s.sent[2]; ok {
57+ t.Fatal("window out of the layout kept")
58+ }
59+ if _, send := s.next(Placement{ID: 1}, configureTarget{}); send {
60+ t.Fatal("window still in the layout configured again")
61+ }
62+ if _, send := s.next(Placement{ID: 2}, configureTarget{}); !send {
63+ t.Fatal("returning window not configured")
64+ }
65+}
M internal/core/core.go +54 -153
@@ -4,13 +4,13 @@ import (
44 "context"
55 "errors"
66 "fmt"
7- "github.com/bnema/neferwl/internal/ports"
8- "maps"
97 "slices"
108 "strconv"
119 "strings"
1210 "time"
1311 "unicode"
12+
13+ "github.com/bnema/neferwl/internal/ports"
1414 )
1515
1616 var ErrQuit = errors.New("quit requested")
@@ -70,8 +70,7 @@ type Core struct {
7070 focusScreen int
7171 binds map[binding]Action
7272 pressed map[string]bool
73- sent map[WindowID]ports.ConfigureWindow
74- focus WindowID
73+ configures configures
7574 pointer WindowID
7675 grab WindowID
7776 buttons map[uint32]bool
@@ -93,28 +92,13 @@ type Core struct {
9392 toSpawn []slotKey
9493 sentPending bool
9594 firstTerminalResolved bool
96- // clients holds the app ID and PID of mapped windows, for State.
97- clients map[WindowID]ports.WindowMapped
98- mappedAt map[WindowID]time.Time
99- inputRegions map[WindowID]ports.InputRegionChanged
100- sentState ports.State
101- sentWorkspaces ports.Workspaces
95+ windows windowRegistry
96+ sentState ports.State
97+ sentWorkspaces ports.Workspaces
10298 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.
10399 popups map[WindowID]*popupState
104100 popupOrder []WindowID
105- // layerFocus is the on-demand layer surface the user clicked; it keeps
106- // the keyboard while it stays mapped on-demand and no window focus
107- // changes on any output (layerOver, taken at the click): any click
108- // elsewhere, bind, activation or new window takes the keyboard back. The
109- // pointer moving to another output does not.
110- layerFocus WindowID
111- layerOver map[*screen]WindowID
112- // inhibitors are the windows asking to keep compositor binds while
113- // focused, inhibiting the one active now (0: none); idle are those
114- // keeping the session awake.
115- inhibitors map[WindowID]bool
116- inhibiting WindowID
117- idle map[WindowID]bool
101+ keyboard keyboard
118102 // activity is when core last told wayland about user input
119103 // (ports.UserActivity), sent at most once per ActivityInterval.
120104 activity time.Time
@@ -316,7 +300,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) {
316300 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")
317301 }
318302 // A placeholder screen holds windows until the first output arrives.
319- c := &Core{slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), clients: map[WindowID]ports.WindowMapped{}, mappedAt: map[WindowID]time.Time{}, inputRegions: map[WindowID]ports.InputRegionChanged{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}, inhibitors: map[WindowID]bool{}, idle: map[WindowID]bool{}}
303+ c := &Core{slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, configures: newConfigures()}
320304 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}
321305 if err := c.apply(cfg); err != nil {
322306 return nil, err
@@ -388,35 +372,26 @@ func (c *Core) windowFocus() map[*screen]WindowID {
388372 return m
389373 }
390374
391-// keyboardFocus is the mapped top/overlay layer with exclusive keyboard
392-// interactivity and the highest ID on any output, else a grabbing popup,
393-// else a clicked on-demand layer (see layerFocus), else the focused window
394-// of the focused output.
375+// keyboardFocus is the surface holding the keyboard (see keyboard.focus).
395376 func (c *Core) keyboardFocus() WindowID {
396- var layer WindowID
377+ var exclusive WindowID
397378 for _, sc := range c.screens {
398379 for _, l := range sc.layers {
399- if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer {
400- layer = l.ID
380+ if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > exclusive {
381+ exclusive = l.ID
401382 }
402383 }
403384 }
404- if layer != 0 {
405- return layer
406- }
407- // A menu with a grab takes the keyboard until it closes.
408- if g := c.grabFocus(); g != 0 {
409- return g
410- }
411- // A layer hidden by a fullscreen window loses the keyboard.
412- if c.layerFocus != 0 && (!c.onDemand(c.layerFocus) || !c.visible(c.layerFocus) || !maps.Equal(c.layerOver, c.windowFocus())) {
413- c.layerFocus, c.layerOver = 0, nil
414- }
415- if c.layerFocus != 0 {
416- return c.layerFocus
417- }
418- id, _ := c.cur().mon.Focused()
419- return id
385+ window, _ := c.cur().mon.Focused()
386+ return c.keyboard.focus(keyboardCandidates{
387+ exclusive: exclusive,
388+ grab: c.grabFocus(),
389+ window: window,
390+ layerShown: func(id WindowID) bool {
391+ return c.onDemand(id) && c.visible(id)
392+ },
393+ windows: c.windowFocus,
394+ })
420395 }
421396
422397 // setLayers splits the mapped layer surfaces by output. A surface without an
@@ -438,7 +413,7 @@ func (c *Core) setLayers(all []ports.LayerSurface) {
438413 sc.layers = append(sc.layers, l)
439414 }
440415 for id := range previous {
441- delete(c.inputRegions, id)
416+ c.windows.layerGone(id)
442417 }
443418 for _, sc := range c.screens {
444419 sc.arrange()
@@ -466,7 +441,6 @@ func (c *Core) publish(ctx context.Context) error {
466441 if err := c.publishPending(ctx); err != nil {
467442 return err
468443 }
469- alive := map[WindowID]bool{}
470444 focus := c.keyboardFocus()
471445 scenes := make([]ports.Scene, 0, len(c.screens))
472446 // Before the first output (and after the last is unplugged) the
@@ -484,7 +458,6 @@ func (c *Core) publish(ctx context.Context) error {
484458 scene.Separators = overviewOutline(layout, max(c.cfg.Border.Width, 2))
485459 }
486460 for _, p := range layout {
487- alive[p.ID] = true
488461 // Only the focused output has an activated window.
489462 focused := p.Focused && i == c.focusScreen
490463 sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Inset: p.Inset, Preview: p.Preview}
@@ -492,64 +465,22 @@ func (c *Core) publish(ctx context.Context) error {
492465 sw.Dim = c.cfg.Stash.Dim
493466 }
494467 scene.Windows = append(scene.Windows, sw)
495- floating := p.Floating
496- if p.Hidden {
497- // A hidden window keeps its size, state and output; it is
498- // only deactivated and marked invisible. One mapped hidden
499- // is told so at once, sized by the client.
500- old, ok := c.sent[p.ID]
501- v := old
502- if !ok {
503- v = ports.ConfigureWindow{ID: p.ID, Floating: floating}
504- }
505- v.Activated, v.Visible, v.Output = false, false, sc.name()
506- if ok && v == old {
507- continue
508- }
509- if err := c.command(ctx, v); err != nil {
510- return err
511- }
512- c.sent[p.ID] = v
513- continue
514- }
515- if p.Preview > 0 {
516- // A preview keeps its client's size: only its focus changes.
517- old, ok := c.sent[p.ID]
518- v := old
519- if !ok {
520- v = ports.ConfigureWindow{ID: p.ID, Width: int(float64(p.Rect.W) / p.Preview), Height: int(float64(p.Rect.H) / p.Preview)}
521- }
522- v.Activated, v.Visible, v.Output = focused, onScreen(p, o), sc.name()
523- if ok && v == old {
524- continue
525- }
526- if err := c.command(ctx, v); err != nil {
527- return err
528- }
529- c.sent[p.ID] = v
530- continue
468+ t := configureTarget{output: sc.name(), area: o, focused: focused}
469+ if !p.Hidden && p.Preview == 0 {
470+ // Only a sized configure needs the client size.
471+ t.client, t.imposed = c.clientRect(p), sc.mon.Current().imposedFloat(p.ID)
531472 }
532- r := c.clientRect(p)
533- v := ports.ConfigureWindow{ID: p.ID, Width: r.W, Height: r.H, Fullscreen: p.Fullscreen, Activated: focused, Floating: floating && !p.Fullscreen, Output: sc.name(), Visible: onScreen(p, o)}
534- if v.Floating && !sc.mon.Current().imposedFloat(p.ID) {
535- // Native floating windows pick their own size.
536- v.Width, v.Height = 0, 0
537- }
538- if old, ok := c.sent[p.ID]; !ok || old != v {
473+ if v, send := c.configures.next(p, t); send {
539474 if err := c.command(ctx, v); err != nil {
540475 return err
541476 }
542- c.sent[p.ID] = v
477+ c.configures.mark(v)
543478 }
544479 }
545480 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
546481 scenes = append(scenes, scene)
547482 }
548- for id := range c.sent {
549- if !alive[id] {
550- delete(c.sent, id)
551- }
552- }
483+ c.configures.prune()
553484 // A workspace switch can hide the window under the pointer; it must not get
554485 // clicks. The next motion re-runs hit-testing.
555486 if c.pointer != 0 && !c.visible(c.pointer) {
@@ -560,11 +491,11 @@ func (c *Core) publish(ctx context.Context) error {
560491 }
561492 }
562493 }
563- if focus != c.focus {
494+ if focus != c.keyboard.sent {
564495 if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil {
565496 return err
566497 }
567- c.focus = focus
498+ c.keyboard.sent = focus
568499 }
569500 if err := c.updateInhibit(ctx); err != nil {
570501 return err
@@ -627,21 +558,18 @@ func (c *Core) rehit(ctx context.Context) error {
627558 // updateInhibit makes the shortcuts inhibitor of the keyboard focus the
628559 // active one and tells wayland when it changes.
629560 func (c *Core) updateInhibit(ctx context.Context) error {
630- want := WindowID(0)
631- if f := c.keyboardFocus(); f != 0 && c.inhibitors[f] {
632- want = f
633- }
634- if want == c.inhibiting {
561+ f := c.keyboardFocus()
562+ release, activate, changed := c.keyboard.inhibit(f, c.windows.lookup(f).inhibitShortcuts)
563+ if !changed {
635564 return nil
636565 }
637- if c.inhibiting != 0 {
638- if err := c.command(ctx, ports.ShortcutsInhibitState{Window: c.inhibiting}); err != nil {
566+ if release != 0 {
567+ if err := c.command(ctx, ports.ShortcutsInhibitState{Window: release}); err != nil {
639568 return err
640569 }
641570 }
642- c.inhibiting = want
643- if want != 0 {
644- return c.command(ctx, ports.ShortcutsInhibitState{Window: want, Active: true})
571+ if activate != 0 {
572+ return c.command(ctx, ports.ShortcutsInhibitState{Window: activate, Active: true})
645573 }
646574 return nil
647575 }
@@ -715,16 +643,7 @@ func (c *Core) allLayers() []ports.LayerSurface {
715643 }
716644
717645 func (c *Core) acceptsInput(id WindowID, x, y float64) bool {
718- r, ok := c.inputRegions[id]
719- if !ok || r.All {
720- return true
721- }
722- for _, box := range r.Rects {
723- if x >= float64(box.X) && x < float64(box.X+box.W) && y >= float64(box.Y) && y < float64(box.Y+box.H) {
724- return true
725- }
726- }
727- return false
646+ return c.windows.acceptsInput(id, x, y)
728647 }
729648
730649 // hit returns the surface under the global logical point and the point in
@@ -819,10 +738,9 @@ func (c *Core) Run(ctx context.Context) error {
819738 c.layerChanged = true
820739 c.setLayers(v.Layers)
821740 case ports.InputRegionChanged:
822- c.inputRegions[v.ID] = v
741+ c.windows.setRegion(v)
823742 case ports.WindowMapped:
824- c.clients[v.ID] = v
825- c.mappedAt[v.ID] = c.now()
743+ c.windows.mapped(v, c.now())
826744 c.placement.place(c, v)
827745 case ports.WindowResized:
828746 if _, w := c.screenOf(v.ID); w != nil {
@@ -837,11 +755,7 @@ func (c *Core) Run(ctx context.Context) error {
837755 p.mapped = true
838756 }
839757 case ports.ShortcutsInhibit:
840- if v.Active {
841- c.inhibitors[v.Window] = true
842- } else {
843- delete(c.inhibitors, v.Window)
844- }
758+ c.windows.setInhibitShortcuts(v.Window, v.Active)
845759 if err := c.updateInhibit(ctx); err != nil {
846760 return nil
847761 }
@@ -851,18 +765,11 @@ func (c *Core) Run(ctx context.Context) error {
851765 c.screens[i].off = !v.On
852766 }
853767 case ports.IdleInhibit:
854- if v.Active {
855- c.idle[v.Window] = true
856- } else {
857- delete(c.idle, v.Window)
858- }
768+ c.windows.setIdleInhibit(v.Window, v.Active)
859769 c.publishState()
860770 continue
861771 case ports.WindowAppID:
862- if info, ok := c.clients[v.ID]; ok {
863- info.AppID = v.AppID
864- c.clients[v.ID] = info
865- }
772+ c.windows.setAppID(v.ID, v.AppID)
866773 case ports.WindowUnmapped:
867774 if c.popups[v.ID] != nil {
868775 if err := c.dropPopup(ctx, v.ID); err != nil {
@@ -872,11 +779,7 @@ func (c *Core) Run(ctx context.Context) error {
872779 if err := c.closePopupsOf(ctx, v.ID); err != nil {
873780 return nil
874781 }
875- delete(c.clients, v.ID)
876- delete(c.mappedAt, v.ID)
877- delete(c.inputRegions, v.ID)
878- delete(c.inhibitors, v.ID)
879- delete(c.idle, v.ID)
782+ c.windows.drop(v.ID)
880783 if s, _ := c.screenOf(v.ID); s != nil {
881784 s.mon.RemoveWindow(v.ID)
882785 }
@@ -890,7 +793,7 @@ func (c *Core) Run(ctx context.Context) error {
890793 case ports.PointerConstrained:
891794 c.constrained = v
892795 case ports.WindowFullscreenRequest:
893- if v.Fullscreen && !v.External && c.now().Sub(c.mappedAt[v.ID]) < fullscreenGrace {
796+ if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
894797 continue
895798 }
896799 if s, _ := c.screenOf(v.ID); s != nil {
@@ -1054,22 +957,20 @@ func (c *Core) Run(ctx context.Context) error {
1054957 return nil
1055958 }
1056959 // A click on an on-demand layer gives it the keyboard.
1057- if v.Pressed && c.onDemand(id) && c.layerFocus != id {
1058- c.layerFocus = id
1059- c.layerOver = c.windowFocus()
960+ if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
1060961 if err := c.publish(ctx); err != nil {
1061962 return nil
1062963 }
1063- } else if v.Pressed && c.layerFocus != 0 && id != c.layerFocus && c.popupRoot(id) != c.layerFocus {
964+ } else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
1064965 // A click anywhere else takes the keyboard back.
1065- c.layerFocus = 0
966+ c.keyboard.takeBack()
1066967 if err := c.publish(ctx); err != nil {
1067968 return nil
1068969 }
1069970 }
1070971 // A click focuses the window and its output.
1071972 s, w := c.screenOf(id)
1072- if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) {
973+ if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) {
1073974 w.Click(id)
1074975 c.focusScreen = c.screenIndex(s.name())
1075976 if err := c.publish(ctx); err != nil {
@@ -1140,14 +1041,14 @@ func (c *Core) Run(ctx context.Context) error {
11401041 // A focused window inhibiting shortcuts gets every key:
11411042 // no bind runs (emergency quit and VT switch are handled by
11421043 // input before core).
1143- if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() {
1044+ if c.keyboard.inhibited(c.keyboardFocus()) {
11441045 if held := heldKey(key); !key.Pressed && c.pressed[held] {
11451046 // Its press ran a bind before inhibiting began: the
11461047 // window never saw it.
11471048 delete(c.pressed, held)
11481049 continue
11491050 }
1150- if err := c.command(ctx, ports.ForwardKey{ID: c.inhibiting, Key: key}); err != nil {
1051+ if err := c.command(ctx, ports.ForwardKey{ID: c.keyboard.inhibiting, Key: key}); err != nil {
11511052 return nil
11521053 }
11531054 continue
@@ -1197,7 +1098,7 @@ func (c *Core) Run(ctx context.Context) error {
11971098 }
11981099 before := c.cur().mon.Current()
11991100 swiped := c.swipedWorkspace()
1200- c.layerFocus = 0 // a bind acts on the windows
1101+ c.keyboard.takeBack() // a bind acts on the windows
12011102 effect := c.applyAction(action)
12021103 if effect.Quit {
12031104 return ErrQuit
@@ -4,13 +4,13 @@ import (
4 "context"4 "context"
5 "errors"5 "errors"
6 "fmt"6 "fmt"
7- "github.com/bnema/neferwl/internal/ports"
8- "maps"
9 "slices"7 "slices"
10 "strconv"8 "strconv"
11 "strings"9 "strings"
12 "time"10 "time"
13 "unicode"11 "unicode"
12+
13+ "github.com/bnema/neferwl/internal/ports"
14 )14 )
15 15
16 var ErrQuit = errors.New("quit requested")16 var ErrQuit = errors.New("quit requested")
@@ -70,8 +70,7 @@ type Core struct {
70 focusScreen int70 focusScreen int
71 binds map[binding]Action71 binds map[binding]Action
72 pressed map[string]bool72 pressed map[string]bool
73- sent map[WindowID]ports.ConfigureWindow73+ configures configures
74- focus WindowID
75 pointer WindowID74 pointer WindowID
76 grab WindowID75 grab WindowID
77 buttons map[uint32]bool76 buttons map[uint32]bool
@@ -93,28 +92,13 @@ type Core struct {
93 toSpawn []slotKey92 toSpawn []slotKey
94 sentPending bool93 sentPending bool
95 firstTerminalResolved bool94 firstTerminalResolved bool
96- // clients holds the app ID and PID of mapped windows, for State.95+ windows windowRegistry
97- clients map[WindowID]ports.WindowMapped96+ sentState ports.State
98- mappedAt map[WindowID]time.Time97+ sentWorkspaces ports.Workspaces
99- inputRegions map[WindowID]ports.InputRegionChanged
100- sentState ports.State
101- sentWorkspaces ports.Workspaces
102 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.98 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.
103 popups map[WindowID]*popupState99 popups map[WindowID]*popupState
104 popupOrder []WindowID100 popupOrder []WindowID
105- // layerFocus is the on-demand layer surface the user clicked; it keeps101+ keyboard keyboard
106- // the keyboard while it stays mapped on-demand and no window focus
107- // changes on any output (layerOver, taken at the click): any click
108- // elsewhere, bind, activation or new window takes the keyboard back. The
109- // pointer moving to another output does not.
110- layerFocus WindowID
111- layerOver map[*screen]WindowID
112- // inhibitors are the windows asking to keep compositor binds while
113- // focused, inhibiting the one active now (0: none); idle are those
114- // keeping the session awake.
115- inhibitors map[WindowID]bool
116- inhibiting WindowID
117- idle map[WindowID]bool
118 // activity is when core last told wayland about user input102 // activity is when core last told wayland about user input
119 // (ports.UserActivity), sent at most once per ActivityInterval.103 // (ports.UserActivity), sent at most once per ActivityInterval.
120 activity time.Time104 activity time.Time
@@ -316,7 +300,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) {
316 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")300 return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1")
317 }301 }
318 // A placeholder screen holds windows until the first output arrives.302 // A placeholder screen holds windows until the first output arrives.
319- c := &Core{slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), clients: map[WindowID]ports.WindowMapped{}, mappedAt: map[WindowID]time.Time{}, inputRegions: map[WindowID]ports.InputRegionChanged{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}, inhibitors: map[WindowID]bool{}, idle: map[WindowID]bool{}}303+ c := &Core{slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, configures: newConfigures()}
320 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}304 c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}}
321 if err := c.apply(cfg); err != nil {305 if err := c.apply(cfg); err != nil {
322 return nil, err306 return nil, err
@@ -388,35 +372,26 @@ func (c *Core) windowFocus() map[*screen]WindowID {
388 return m372 return m
389 }373 }
390 374
391-// keyboardFocus is the mapped top/overlay layer with exclusive keyboard375+// keyboardFocus is the surface holding the keyboard (see keyboard.focus).
392-// interactivity and the highest ID on any output, else a grabbing popup,
393-// else a clicked on-demand layer (see layerFocus), else the focused window
394-// of the focused output.
395 func (c *Core) keyboardFocus() WindowID {376 func (c *Core) keyboardFocus() WindowID {
396- var layer WindowID377+ var exclusive WindowID
397 for _, sc := range c.screens {378 for _, sc := range c.screens {
398 for _, l := range sc.layers {379 for _, l := range sc.layers {
399- if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer {380+ if l.Keyboard == 1 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > exclusive {
400- layer = l.ID381+ exclusive = l.ID
401 }382 }
402 }383 }
403 }384 }
404- if layer != 0 {385+ window, _ := c.cur().mon.Focused()
405- return layer386+ return c.keyboard.focus(keyboardCandidates{
406- }387+ exclusive: exclusive,
407- // A menu with a grab takes the keyboard until it closes.388+ grab: c.grabFocus(),
408- if g := c.grabFocus(); g != 0 {389+ window: window,
409- return g390+ layerShown: func(id WindowID) bool {
410- }391+ return c.onDemand(id) && c.visible(id)
411- // A layer hidden by a fullscreen window loses the keyboard.392+ },
412- if c.layerFocus != 0 && (!c.onDemand(c.layerFocus) || !c.visible(c.layerFocus) || !maps.Equal(c.layerOver, c.windowFocus())) {393+ windows: c.windowFocus,
413- c.layerFocus, c.layerOver = 0, nil394+ })
414- }
415- if c.layerFocus != 0 {
416- return c.layerFocus
417- }
418- id, _ := c.cur().mon.Focused()
419- return id
420 }395 }
421 396
422 // setLayers splits the mapped layer surfaces by output. A surface without an397 // setLayers splits the mapped layer surfaces by output. A surface without an
@@ -438,7 +413,7 @@ func (c *Core) setLayers(all []ports.LayerSurface) {
438 sc.layers = append(sc.layers, l)413 sc.layers = append(sc.layers, l)
439 }414 }
440 for id := range previous {415 for id := range previous {
441- delete(c.inputRegions, id)416+ c.windows.layerGone(id)
442 }417 }
443 for _, sc := range c.screens {418 for _, sc := range c.screens {
444 sc.arrange()419 sc.arrange()
@@ -466,7 +441,6 @@ func (c *Core) publish(ctx context.Context) error {
466 if err := c.publishPending(ctx); err != nil {441 if err := c.publishPending(ctx); err != nil {
467 return err442 return err
468 }443 }
469- alive := map[WindowID]bool{}
470 focus := c.keyboardFocus()444 focus := c.keyboardFocus()
471 scenes := make([]ports.Scene, 0, len(c.screens))445 scenes := make([]ports.Scene, 0, len(c.screens))
472 // Before the first output (and after the last is unplugged) the446 // Before the first output (and after the last is unplugged) the
@@ -484,7 +458,6 @@ func (c *Core) publish(ctx context.Context) error {
484 scene.Separators = overviewOutline(layout, max(c.cfg.Border.Width, 2))458 scene.Separators = overviewOutline(layout, max(c.cfg.Border.Width, 2))
485 }459 }
486 for _, p := range layout {460 for _, p := range layout {
487- alive[p.ID] = true
488 // Only the focused output has an activated window.461 // Only the focused output has an activated window.
489 focused := p.Focused && i == c.focusScreen462 focused := p.Focused && i == c.focusScreen
490 sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Inset: p.Inset, Preview: p.Preview}463 sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Inset: p.Inset, Preview: p.Preview}
@@ -492,64 +465,22 @@ func (c *Core) publish(ctx context.Context) error {
492 sw.Dim = c.cfg.Stash.Dim465 sw.Dim = c.cfg.Stash.Dim
493 }466 }
494 scene.Windows = append(scene.Windows, sw)467 scene.Windows = append(scene.Windows, sw)
495- floating := p.Floating468+ t := configureTarget{output: sc.name(), area: o, focused: focused}
496- if p.Hidden {469+ if !p.Hidden && p.Preview == 0 {
497- // A hidden window keeps its size, state and output; it is470+ // Only a sized configure needs the client size.
498- // only deactivated and marked invisible. One mapped hidden471+ t.client, t.imposed = c.clientRect(p), sc.mon.Current().imposedFloat(p.ID)
499- // is told so at once, sized by the client.
500- old, ok := c.sent[p.ID]
501- v := old
502- if !ok {
503- v = ports.ConfigureWindow{ID: p.ID, Floating: floating}
504- }
505- v.Activated, v.Visible, v.Output = false, false, sc.name()
506- if ok && v == old {
507- continue
508- }
509- if err := c.command(ctx, v); err != nil {
510- return err
511- }
512- c.sent[p.ID] = v
513- continue
514- }
515- if p.Preview > 0 {
516- // A preview keeps its client's size: only its focus changes.
517- old, ok := c.sent[p.ID]
518- v := old
519- if !ok {
520- v = ports.ConfigureWindow{ID: p.ID, Width: int(float64(p.Rect.W) / p.Preview), Height: int(float64(p.Rect.H) / p.Preview)}
521- }
522- v.Activated, v.Visible, v.Output = focused, onScreen(p, o), sc.name()
523- if ok && v == old {
524- continue
525- }
526- if err := c.command(ctx, v); err != nil {
527- return err
528- }
529- c.sent[p.ID] = v
530- continue
531 }472 }
532- r := c.clientRect(p)473+ if v, send := c.configures.next(p, t); send {
533- v := ports.ConfigureWindow{ID: p.ID, Width: r.W, Height: r.H, Fullscreen: p.Fullscreen, Activated: focused, Floating: floating && !p.Fullscreen, Output: sc.name(), Visible: onScreen(p, o)}
534- if v.Floating && !sc.mon.Current().imposedFloat(p.ID) {
535- // Native floating windows pick their own size.
536- v.Width, v.Height = 0, 0
537- }
538- if old, ok := c.sent[p.ID]; !ok || old != v {
539 if err := c.command(ctx, v); err != nil {474 if err := c.command(ctx, v); err != nil {
540 return err475 return err
541 }476 }
542- c.sent[p.ID] = v477+ c.configures.mark(v)
543 }478 }
544 }479 }
545 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)480 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
546 scenes = append(scenes, scene)481 scenes = append(scenes, scene)
547 }482 }
548- for id := range c.sent {483+ c.configures.prune()
549- if !alive[id] {
550- delete(c.sent, id)
551- }
552- }
553 // A workspace switch can hide the window under the pointer; it must not get484 // A workspace switch can hide the window under the pointer; it must not get
554 // clicks. The next motion re-runs hit-testing.485 // clicks. The next motion re-runs hit-testing.
555 if c.pointer != 0 && !c.visible(c.pointer) {486 if c.pointer != 0 && !c.visible(c.pointer) {
@@ -560,11 +491,11 @@ func (c *Core) publish(ctx context.Context) error {
560 }491 }
561 }492 }
562 }493 }
563- if focus != c.focus {494+ if focus != c.keyboard.sent {
564 if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil {495 if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil {
565 return err496 return err
566 }497 }
567- c.focus = focus498+ c.keyboard.sent = focus
568 }499 }
569 if err := c.updateInhibit(ctx); err != nil {500 if err := c.updateInhibit(ctx); err != nil {
570 return err501 return err
@@ -627,21 +558,18 @@ func (c *Core) rehit(ctx context.Context) error {
627 // updateInhibit makes the shortcuts inhibitor of the keyboard focus the558 // updateInhibit makes the shortcuts inhibitor of the keyboard focus the
628 // active one and tells wayland when it changes.559 // active one and tells wayland when it changes.
629 func (c *Core) updateInhibit(ctx context.Context) error {560 func (c *Core) updateInhibit(ctx context.Context) error {
630- want := WindowID(0)561+ f := c.keyboardFocus()
631- if f := c.keyboardFocus(); f != 0 && c.inhibitors[f] {562+ release, activate, changed := c.keyboard.inhibit(f, c.windows.lookup(f).inhibitShortcuts)
632- want = f563+ if !changed {
633- }
634- if want == c.inhibiting {
635 return nil564 return nil
636 }565 }
637- if c.inhibiting != 0 {566+ if release != 0 {
638- if err := c.command(ctx, ports.ShortcutsInhibitState{Window: c.inhibiting}); err != nil {567+ if err := c.command(ctx, ports.ShortcutsInhibitState{Window: release}); err != nil {
639 return err568 return err
640 }569 }
641 }570 }
642- c.inhibiting = want571+ if activate != 0 {
643- if want != 0 {572+ return c.command(ctx, ports.ShortcutsInhibitState{Window: activate, Active: true})
644- return c.command(ctx, ports.ShortcutsInhibitState{Window: want, Active: true})
645 }573 }
646 return nil574 return nil
647 }575 }
@@ -715,16 +643,7 @@ func (c *Core) allLayers() []ports.LayerSurface {
715 }643 }
716 644
717 func (c *Core) acceptsInput(id WindowID, x, y float64) bool {645 func (c *Core) acceptsInput(id WindowID, x, y float64) bool {
718- r, ok := c.inputRegions[id]646+ return c.windows.acceptsInput(id, x, y)
719- if !ok || r.All {
720- return true
721- }
722- for _, box := range r.Rects {
723- if x >= float64(box.X) && x < float64(box.X+box.W) && y >= float64(box.Y) && y < float64(box.Y+box.H) {
724- return true
725- }
726- }
727- return false
728 }647 }
729 648
730 // hit returns the surface under the global logical point and the point in649 // hit returns the surface under the global logical point and the point in
@@ -819,10 +738,9 @@ func (c *Core) Run(ctx context.Context) error {
819 c.layerChanged = true738 c.layerChanged = true
820 c.setLayers(v.Layers)739 c.setLayers(v.Layers)
821 case ports.InputRegionChanged:740 case ports.InputRegionChanged:
822- c.inputRegions[v.ID] = v741+ c.windows.setRegion(v)
823 case ports.WindowMapped:742 case ports.WindowMapped:
824- c.clients[v.ID] = v743+ c.windows.mapped(v, c.now())
825- c.mappedAt[v.ID] = c.now()
826 c.placement.place(c, v)744 c.placement.place(c, v)
827 case ports.WindowResized:745 case ports.WindowResized:
828 if _, w := c.screenOf(v.ID); w != nil {746 if _, w := c.screenOf(v.ID); w != nil {
@@ -837,11 +755,7 @@ func (c *Core) Run(ctx context.Context) error {
837 p.mapped = true755 p.mapped = true
838 }756 }
839 case ports.ShortcutsInhibit:757 case ports.ShortcutsInhibit:
840- if v.Active {758+ c.windows.setInhibitShortcuts(v.Window, v.Active)
841- c.inhibitors[v.Window] = true
842- } else {
843- delete(c.inhibitors, v.Window)
844- }
845 if err := c.updateInhibit(ctx); err != nil {759 if err := c.updateInhibit(ctx); err != nil {
846 return nil760 return nil
847 }761 }
@@ -851,18 +765,11 @@ func (c *Core) Run(ctx context.Context) error {
851 c.screens[i].off = !v.On765 c.screens[i].off = !v.On
852 }766 }
853 case ports.IdleInhibit:767 case ports.IdleInhibit:
854- if v.Active {768+ c.windows.setIdleInhibit(v.Window, v.Active)
855- c.idle[v.Window] = true
856- } else {
857- delete(c.idle, v.Window)
858- }
859 c.publishState()769 c.publishState()
860 continue770 continue
861 case ports.WindowAppID:771 case ports.WindowAppID:
862- if info, ok := c.clients[v.ID]; ok {772+ c.windows.setAppID(v.ID, v.AppID)
863- info.AppID = v.AppID
864- c.clients[v.ID] = info
865- }
866 case ports.WindowUnmapped:773 case ports.WindowUnmapped:
867 if c.popups[v.ID] != nil {774 if c.popups[v.ID] != nil {
868 if err := c.dropPopup(ctx, v.ID); err != nil {775 if err := c.dropPopup(ctx, v.ID); err != nil {
@@ -872,11 +779,7 @@ func (c *Core) Run(ctx context.Context) error {
872 if err := c.closePopupsOf(ctx, v.ID); err != nil {779 if err := c.closePopupsOf(ctx, v.ID); err != nil {
873 return nil780 return nil
874 }781 }
875- delete(c.clients, v.ID)782+ c.windows.drop(v.ID)
876- delete(c.mappedAt, v.ID)
877- delete(c.inputRegions, v.ID)
878- delete(c.inhibitors, v.ID)
879- delete(c.idle, v.ID)
880 if s, _ := c.screenOf(v.ID); s != nil {783 if s, _ := c.screenOf(v.ID); s != nil {
881 s.mon.RemoveWindow(v.ID)784 s.mon.RemoveWindow(v.ID)
882 }785 }
@@ -890,7 +793,7 @@ func (c *Core) Run(ctx context.Context) error {
890 case ports.PointerConstrained:793 case ports.PointerConstrained:
891 c.constrained = v794 c.constrained = v
892 case ports.WindowFullscreenRequest:795 case ports.WindowFullscreenRequest:
893- if v.Fullscreen && !v.External && c.now().Sub(c.mappedAt[v.ID]) < fullscreenGrace {796+ if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
894 continue797 continue
895 }798 }
896 if s, _ := c.screenOf(v.ID); s != nil {799 if s, _ := c.screenOf(v.ID); s != nil {
@@ -1054,22 +957,20 @@ func (c *Core) Run(ctx context.Context) error {
1054 return nil957 return nil
1055 }958 }
1056 // A click on an on-demand layer gives it the keyboard.959 // A click on an on-demand layer gives it the keyboard.
1057- if v.Pressed && c.onDemand(id) && c.layerFocus != id {960+ if v.Pressed && c.onDemand(id) && c.keyboard.clickLayer(id, c.windowFocus()) {
1058- c.layerFocus = id
1059- c.layerOver = c.windowFocus()
1060 if err := c.publish(ctx); err != nil {961 if err := c.publish(ctx); err != nil {
1061 return nil962 return nil
1062 }963 }
1063- } else if v.Pressed && c.layerFocus != 0 && id != c.layerFocus && c.popupRoot(id) != c.layerFocus {964+ } else if l := c.keyboard.layer; v.Pressed && l != 0 && id != l && c.popupRoot(id) != l {
1064 // A click anywhere else takes the keyboard back.965 // A click anywhere else takes the keyboard back.
1065- c.layerFocus = 0966+ c.keyboard.takeBack()
1066 if err := c.publish(ctx); err != nil {967 if err := c.publish(ctx); err != nil {
1067 return nil968 return nil
1068 }969 }
1069 }970 }
1070 // A click focuses the window and its output.971 // A click focuses the window and its output.
1071 s, w := c.screenOf(id)972 s, w := c.screenOf(id)
1072- if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) {973+ if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) {
1073 w.Click(id)974 w.Click(id)
1074 c.focusScreen = c.screenIndex(s.name())975 c.focusScreen = c.screenIndex(s.name())
1075 if err := c.publish(ctx); err != nil {976 if err := c.publish(ctx); err != nil {
@@ -1140,14 +1041,14 @@ func (c *Core) Run(ctx context.Context) error {
1140 // A focused window inhibiting shortcuts gets every key:1041 // A focused window inhibiting shortcuts gets every key:
1141 // no bind runs (emergency quit and VT switch are handled by1042 // no bind runs (emergency quit and VT switch are handled by
1142 // input before core).1043 // input before core).
1143- if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() {1044+ if c.keyboard.inhibited(c.keyboardFocus()) {
1144 if held := heldKey(key); !key.Pressed && c.pressed[held] {1045 if held := heldKey(key); !key.Pressed && c.pressed[held] {
1145 // Its press ran a bind before inhibiting began: the1046 // Its press ran a bind before inhibiting began: the
1146 // window never saw it.1047 // window never saw it.
1147 delete(c.pressed, held)1048 delete(c.pressed, held)
1148 continue1049 continue
1149 }1050 }
1150- if err := c.command(ctx, ports.ForwardKey{ID: c.inhibiting, Key: key}); err != nil {1051+ if err := c.command(ctx, ports.ForwardKey{ID: c.keyboard.inhibiting, Key: key}); err != nil {
1151 return nil1052 return nil
1152 }1053 }
1153 continue1054 continue
@@ -1197,7 +1098,7 @@ func (c *Core) Run(ctx context.Context) error {
1197 }1098 }
1198 before := c.cur().mon.Current()1099 before := c.cur().mon.Current()
1199 swiped := c.swipedWorkspace()1100 swiped := c.swipedWorkspace()
1200- c.layerFocus = 0 // a bind acts on the windows1101+ c.keyboard.takeBack() // a bind acts on the windows
1201 effect := c.applyAction(action)1102 effect := c.applyAction(action)
1202 if effect.Quit {1103 if effect.Quit {
1203 return ErrQuit1104 return ErrQuit
M internal/core/core_test.go +5 -4
@@ -3,14 +3,15 @@ package core_test
33 import (
44 "context"
55 "errors"
6- "github.com/bnema/neferwl/internal/adapters/config"
7- "github.com/bnema/neferwl/internal/core"
8- portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9- "github.com/bnema/neferwl/internal/ports"
106 "slices"
117 "sync/atomic"
128 "testing"
139 "time"
10+
11+ "github.com/bnema/neferwl/internal/adapters/config"
12+ "github.com/bnema/neferwl/internal/core"
13+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
14+ "github.com/bnema/neferwl/internal/ports"
1415 )
1516
1617 func receive[T any](t *testing.T, ch <-chan T) T {
@@ -3,14 +3,15 @@ package core_test
3 import (3 import (
4 "context"4 "context"
5 "errors"5 "errors"
6- "github.com/bnema/neferwl/internal/adapters/config"
7- "github.com/bnema/neferwl/internal/core"
8- portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9- "github.com/bnema/neferwl/internal/ports"
10 "slices"6 "slices"
11 "sync/atomic"7 "sync/atomic"
12 "testing"8 "testing"
13 "time"9 "time"
10+
11+ "github.com/bnema/neferwl/internal/adapters/config"
12+ "github.com/bnema/neferwl/internal/core"
13+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
14+ "github.com/bnema/neferwl/internal/ports"
14 )15 )
15 16
16 func receive[T any](t *testing.T, ch <-chan T) T {17 func receive[T any](t *testing.T, ch <-chan T) T {
M internal/core/gesture.go +4 -4
@@ -207,7 +207,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
207207 // A column wider than the view aligns as focus moves align it.
208208 w.scroll()
209209 c.focusScreen = c.screenIndex(g.screen.name())
210- c.layerFocus = 0
210+ c.keyboard.takeBack()
211211 }
212212 w.shift = shown - float64(w.ViewX)
213213 w.motion = newMotion(viewSpring(w.shift, g.tracker.velocity()*scale), now)
@@ -232,7 +232,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
232232 }
233233 if !e.Cancelled {
234234 c.focusScreen = c.screenIndex(g.screen.name())
235- c.layerFocus = 0
235+ c.keyboard.takeBack()
236236 }
237237 m.switchOff, m.switchList = off, g.list
238238 m.switchMotion = newMotion(workspaceSpring(off, velocity), now)
@@ -244,7 +244,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
244244 before := m.Current()
245245 m.ToggleOverview()
246246 c.focusScreen = c.screenIndex(g.screen.name())
247- c.layerFocus = 0
247+ c.keyboard.takeBack()
248248 return m.Current() != before
249249 case swipeDiscrete:
250250 p := g.tracker.projectedEnd()
@@ -266,7 +266,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
266266 return false
267267 }
268268 before := c.cur().mon.Current()
269- c.layerFocus = 0
269+ c.keyboard.takeBack()
270270 if mon := c.cur().mon; mon.overview {
271271 mon.overviewSwipe(a)
272272 } else {
@@ -207,7 +207,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
207 // A column wider than the view aligns as focus moves align it.207 // A column wider than the view aligns as focus moves align it.
208 w.scroll()208 w.scroll()
209 c.focusScreen = c.screenIndex(g.screen.name())209 c.focusScreen = c.screenIndex(g.screen.name())
210- c.layerFocus = 0210+ c.keyboard.takeBack()
211 }211 }
212 w.shift = shown - float64(w.ViewX)212 w.shift = shown - float64(w.ViewX)
213 w.motion = newMotion(viewSpring(w.shift, g.tracker.velocity()*scale), now)213 w.motion = newMotion(viewSpring(w.shift, g.tracker.velocity()*scale), now)
@@ -232,7 +232,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
232 }232 }
233 if !e.Cancelled {233 if !e.Cancelled {
234 c.focusScreen = c.screenIndex(g.screen.name())234 c.focusScreen = c.screenIndex(g.screen.name())
235- c.layerFocus = 0235+ c.keyboard.takeBack()
236 }236 }
237 m.switchOff, m.switchList = off, g.list237 m.switchOff, m.switchList = off, g.list
238 m.switchMotion = newMotion(workspaceSpring(off, velocity), now)238 m.switchMotion = newMotion(workspaceSpring(off, velocity), now)
@@ -244,7 +244,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
244 before := m.Current()244 before := m.Current()
245 m.ToggleOverview()245 m.ToggleOverview()
246 c.focusScreen = c.screenIndex(g.screen.name())246 c.focusScreen = c.screenIndex(g.screen.name())
247- c.layerFocus = 0247+ c.keyboard.takeBack()
248 return m.Current() != before248 return m.Current() != before
249 case swipeDiscrete:249 case swipeDiscrete:
250 p := g.tracker.projectedEnd()250 p := g.tracker.projectedEnd()
@@ -266,7 +266,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) {
266 return false266 return false
267 }267 }
268 before := c.cur().mon.Current()268 before := c.cur().mon.Current()
269- c.layerFocus = 0269+ c.keyboard.takeBack()
270 if mon := c.cur().mon; mon.overview {270 if mon := c.cur().mon; mon.overview {
271 mon.overviewSwipe(a)271 mon.overviewSwipe(a)
272 } else {272 } else {
A internal/core/keyboard.go +90 -0
@@ -0,0 +1,90 @@
1+package core
2+
3+import "maps"
4+
5+// keyboardCandidates is what the keyboard focus picks from, strongest first.
6+// The layer checks are lazy: they only run while a clicked layer holds the
7+// keyboard.
8+type keyboardCandidates struct {
9+ // exclusive is the mapped top/overlay layer with exclusive keyboard
10+ // interactivity and the highest ID; grab the topmost grabbing popup.
11+ exclusive, grab WindowID
12+ // window is the focused window of the focused output.
13+ window WindowID
14+ // layerShown reports whether a layer is still mapped on-demand and
15+ // visible; windows is the focused window of each output.
16+ layerShown func(WindowID) bool
17+ windows func() map[*screen]WindowID
18+}
19+
20+// keyboard owns who holds the keyboard: the rules picking the focus, the
21+// on-demand layer the user clicked, the focus last told to wayland and the
22+// shortcuts inhibitor active now.
23+type keyboard struct {
24+ // layer is the on-demand layer surface the user clicked; it keeps the
25+ // keyboard while it stays mapped on-demand and no window focus changes
26+ // on any output (over, taken at the click): any click elsewhere, bind,
27+ // activation or new window takes the keyboard back. The pointer moving
28+ // to another output does not.
29+ layer WindowID
30+ over map[*screen]WindowID
31+ // sent is the focus last sent to wayland; inhibiting the shortcuts
32+ // inhibitor active now (0: none).
33+ sent WindowID
34+ inhibiting WindowID
35+}
36+
37+// focus is the exclusive layer, else a grabbing popup, else the clicked
38+// on-demand layer, else the focused window. A clicked layer that is hidden
39+// (by a fullscreen window) or outlived its window focus loses the keyboard
40+// for good.
41+func (k *keyboard) focus(c keyboardCandidates) WindowID {
42+ if c.exclusive != 0 {
43+ return c.exclusive
44+ }
45+ // A menu with a grab takes the keyboard until it closes.
46+ if c.grab != 0 {
47+ return c.grab
48+ }
49+ if k.layer != 0 && (!c.layerShown(k.layer) || !maps.Equal(k.over, c.windows())) {
50+ k.layer, k.over = 0, nil
51+ }
52+ if k.layer != 0 {
53+ return k.layer
54+ }
55+ return c.window
56+}
57+
58+// clickLayer gives the keyboard to the clicked on-demand layer while the
59+// window focus stays windows. It reports whether the holder changed.
60+func (k *keyboard) clickLayer(id WindowID, windows map[*screen]WindowID) bool {
61+ if k.layer == id {
62+ return false
63+ }
64+ k.layer, k.over = id, windows
65+ return true
66+}
67+
68+// takeBack returns the keyboard from a clicked layer to the windows.
69+func (k *keyboard) takeBack() { k.layer, k.over = 0, nil }
70+
71+// inhibit makes the focus the active shortcuts inhibitor when it asks to
72+// be (wants). It returns the inhibitor to release and the one to activate
73+// (0: none) when the active one changes.
74+func (k *keyboard) inhibit(focus WindowID, wants bool) (release, activate WindowID, changed bool) {
75+ want := WindowID(0)
76+ if focus != 0 && wants {
77+ want = focus
78+ }
79+ if want == k.inhibiting {
80+ return 0, 0, false
81+ }
82+ release, k.inhibiting = k.inhibiting, want
83+ return release, want, true
84+}
85+
86+// inhibited reports whether the focus gets every key: it is the active
87+// shortcuts inhibitor.
88+func (k *keyboard) inhibited(focus WindowID) bool {
89+ return k.inhibiting != 0 && k.inhibiting == focus
90+}
@@ -0,0 +1,90 @@
1+package core
2+
3+import "maps"
4+
5+// keyboardCandidates is what the keyboard focus picks from, strongest first.
6+// The layer checks are lazy: they only run while a clicked layer holds the
7+// keyboard.
8+type keyboardCandidates struct {
9+ // exclusive is the mapped top/overlay layer with exclusive keyboard
10+ // interactivity and the highest ID; grab the topmost grabbing popup.
11+ exclusive, grab WindowID
12+ // window is the focused window of the focused output.
13+ window WindowID
14+ // layerShown reports whether a layer is still mapped on-demand and
15+ // visible; windows is the focused window of each output.
16+ layerShown func(WindowID) bool
17+ windows func() map[*screen]WindowID
18+}
19+
20+// keyboard owns who holds the keyboard: the rules picking the focus, the
21+// on-demand layer the user clicked, the focus last told to wayland and the
22+// shortcuts inhibitor active now.
23+type keyboard struct {
24+ // layer is the on-demand layer surface the user clicked; it keeps the
25+ // keyboard while it stays mapped on-demand and no window focus changes
26+ // on any output (over, taken at the click): any click elsewhere, bind,
27+ // activation or new window takes the keyboard back. The pointer moving
28+ // to another output does not.
29+ layer WindowID
30+ over map[*screen]WindowID
31+ // sent is the focus last sent to wayland; inhibiting the shortcuts
32+ // inhibitor active now (0: none).
33+ sent WindowID
34+ inhibiting WindowID
35+}
36+
37+// focus is the exclusive layer, else a grabbing popup, else the clicked
38+// on-demand layer, else the focused window. A clicked layer that is hidden
39+// (by a fullscreen window) or outlived its window focus loses the keyboard
40+// for good.
41+func (k *keyboard) focus(c keyboardCandidates) WindowID {
42+ if c.exclusive != 0 {
43+ return c.exclusive
44+ }
45+ // A menu with a grab takes the keyboard until it closes.
46+ if c.grab != 0 {
47+ return c.grab
48+ }
49+ if k.layer != 0 && (!c.layerShown(k.layer) || !maps.Equal(k.over, c.windows())) {
50+ k.layer, k.over = 0, nil
51+ }
52+ if k.layer != 0 {
53+ return k.layer
54+ }
55+ return c.window
56+}
57+
58+// clickLayer gives the keyboard to the clicked on-demand layer while the
59+// window focus stays windows. It reports whether the holder changed.
60+func (k *keyboard) clickLayer(id WindowID, windows map[*screen]WindowID) bool {
61+ if k.layer == id {
62+ return false
63+ }
64+ k.layer, k.over = id, windows
65+ return true
66+}
67+
68+// takeBack returns the keyboard from a clicked layer to the windows.
69+func (k *keyboard) takeBack() { k.layer, k.over = 0, nil }
70+
71+// inhibit makes the focus the active shortcuts inhibitor when it asks to
72+// be (wants). It returns the inhibitor to release and the one to activate
73+// (0: none) when the active one changes.
74+func (k *keyboard) inhibit(focus WindowID, wants bool) (release, activate WindowID, changed bool) {
75+ want := WindowID(0)
76+ if focus != 0 && wants {
77+ want = focus
78+ }
79+ if want == k.inhibiting {
80+ return 0, 0, false
81+ }
82+ release, k.inhibiting = k.inhibiting, want
83+ return release, want, true
84+}
85+
86+// inhibited reports whether the focus gets every key: it is the active
87+// shortcuts inhibitor.
88+func (k *keyboard) inhibited(focus WindowID) bool {
89+ return k.inhibiting != 0 && k.inhibiting == focus
90+}
A internal/core/keyboard_test.go +83 -0
@@ -0,0 +1,83 @@
1+package core
2+
3+import "testing"
4+
5+func TestKeyboardFocusOrder(t *testing.T) {
6+ a := &screen{}
7+ windows := map[*screen]WindowID{a: 1}
8+ shown := true
9+ cand := keyboardCandidates{window: 1, layerShown: func(WindowID) bool { return shown }, windows: func() map[*screen]WindowID { return windows }}
10+ var k keyboard
11+ if got := k.focus(cand); got != 1 {
12+ t.Fatalf("focus = %d, want the window", got)
13+ }
14+ if !k.clickLayer(5, map[*screen]WindowID{a: 1}) || k.clickLayer(5, windows) {
15+ t.Fatal("clickLayer change report")
16+ }
17+ if got := k.focus(cand); got != 5 {
18+ t.Fatalf("focus = %d, want the clicked layer", got)
19+ }
20+ grab := cand
21+ grab.grab = 7
22+ if got := k.focus(grab); got != 7 {
23+ t.Fatalf("focus = %d, want the grabbing popup", got)
24+ }
25+ grab.exclusive = 9
26+ if got := k.focus(grab); got != 9 {
27+ t.Fatalf("focus = %d, want the exclusive layer", got)
28+ }
29+ // The grab closed: the clicked layer still holds the keyboard.
30+ if got := k.focus(cand); got != 5 {
31+ t.Fatalf("focus = %d, want the clicked layer back", got)
32+ }
33+ // A window focus change on any output takes it back for good.
34+ windows = map[*screen]WindowID{a: 2}
35+ cand.window = 2
36+ if got := k.focus(cand); got != 2 {
37+ t.Fatalf("focus = %d, want the new window", got)
38+ }
39+ windows = map[*screen]WindowID{a: 1}
40+ cand.window = 1
41+ if got := k.focus(cand); got != 1 || k.layer != 0 {
42+ t.Fatalf("focus = %d, layer %d; the layer must not come back", got, k.layer)
43+ }
44+}
45+
46+func TestKeyboardHiddenLayerLosesFocus(t *testing.T) {
47+ windows := map[*screen]WindowID{}
48+ shown := false
49+ cand := keyboardCandidates{window: 1, layerShown: func(WindowID) bool { return shown }, windows: func() map[*screen]WindowID { return windows }}
50+ var k keyboard
51+ k.clickLayer(5, map[*screen]WindowID{})
52+ if got := k.focus(cand); got != 1 {
53+ t.Fatalf("focus = %d, want the window", got)
54+ }
55+ shown = true
56+ if got := k.focus(cand); got != 1 {
57+ t.Fatalf("focus = %d, the hidden layer must not come back", got)
58+ }
59+ k.clickLayer(5, map[*screen]WindowID{})
60+ k.takeBack()
61+ if got := k.focus(cand); got != 1 {
62+ t.Fatalf("focus = %d after takeBack", got)
63+ }
64+}
65+
66+func TestKeyboardInhibit(t *testing.T) {
67+ var k keyboard
68+ if _, _, changed := k.inhibit(1, false); changed {
69+ t.Fatal("no inhibitor changed")
70+ }
71+ if rel, act, changed := k.inhibit(1, true); rel != 0 || act != 1 || !changed || !k.inhibited(1) {
72+ t.Fatalf("activate = %d, %d, %v", rel, act, changed)
73+ }
74+ if _, _, changed := k.inhibit(1, true); changed {
75+ t.Fatal("same inhibitor changed")
76+ }
77+ if rel, act, changed := k.inhibit(2, true); rel != 1 || act != 2 || !changed {
78+ t.Fatalf("switch = %d, %d, %v", rel, act, changed)
79+ }
80+ if rel, act, changed := k.inhibit(0, true); rel != 2 || act != 0 || !changed || k.inhibited(0) {
81+ t.Fatalf("release = %d, %d, %v", rel, act, changed)
82+ }
83+}
@@ -0,0 +1,83 @@
1+package core
2+
3+import "testing"
4+
5+func TestKeyboardFocusOrder(t *testing.T) {
6+ a := &screen{}
7+ windows := map[*screen]WindowID{a: 1}
8+ shown := true
9+ cand := keyboardCandidates{window: 1, layerShown: func(WindowID) bool { return shown }, windows: func() map[*screen]WindowID { return windows }}
10+ var k keyboard
11+ if got := k.focus(cand); got != 1 {
12+ t.Fatalf("focus = %d, want the window", got)
13+ }
14+ if !k.clickLayer(5, map[*screen]WindowID{a: 1}) || k.clickLayer(5, windows) {
15+ t.Fatal("clickLayer change report")
16+ }
17+ if got := k.focus(cand); got != 5 {
18+ t.Fatalf("focus = %d, want the clicked layer", got)
19+ }
20+ grab := cand
21+ grab.grab = 7
22+ if got := k.focus(grab); got != 7 {
23+ t.Fatalf("focus = %d, want the grabbing popup", got)
24+ }
25+ grab.exclusive = 9
26+ if got := k.focus(grab); got != 9 {
27+ t.Fatalf("focus = %d, want the exclusive layer", got)
28+ }
29+ // The grab closed: the clicked layer still holds the keyboard.
30+ if got := k.focus(cand); got != 5 {
31+ t.Fatalf("focus = %d, want the clicked layer back", got)
32+ }
33+ // A window focus change on any output takes it back for good.
34+ windows = map[*screen]WindowID{a: 2}
35+ cand.window = 2
36+ if got := k.focus(cand); got != 2 {
37+ t.Fatalf("focus = %d, want the new window", got)
38+ }
39+ windows = map[*screen]WindowID{a: 1}
40+ cand.window = 1
41+ if got := k.focus(cand); got != 1 || k.layer != 0 {
42+ t.Fatalf("focus = %d, layer %d; the layer must not come back", got, k.layer)
43+ }
44+}
45+
46+func TestKeyboardHiddenLayerLosesFocus(t *testing.T) {
47+ windows := map[*screen]WindowID{}
48+ shown := false
49+ cand := keyboardCandidates{window: 1, layerShown: func(WindowID) bool { return shown }, windows: func() map[*screen]WindowID { return windows }}
50+ var k keyboard
51+ k.clickLayer(5, map[*screen]WindowID{})
52+ if got := k.focus(cand); got != 1 {
53+ t.Fatalf("focus = %d, want the window", got)
54+ }
55+ shown = true
56+ if got := k.focus(cand); got != 1 {
57+ t.Fatalf("focus = %d, the hidden layer must not come back", got)
58+ }
59+ k.clickLayer(5, map[*screen]WindowID{})
60+ k.takeBack()
61+ if got := k.focus(cand); got != 1 {
62+ t.Fatalf("focus = %d after takeBack", got)
63+ }
64+}
65+
66+func TestKeyboardInhibit(t *testing.T) {
67+ var k keyboard
68+ if _, _, changed := k.inhibit(1, false); changed {
69+ t.Fatal("no inhibitor changed")
70+ }
71+ if rel, act, changed := k.inhibit(1, true); rel != 0 || act != 1 || !changed || !k.inhibited(1) {
72+ t.Fatalf("activate = %d, %d, %v", rel, act, changed)
73+ }
74+ if _, _, changed := k.inhibit(1, true); changed {
75+ t.Fatal("same inhibitor changed")
76+ }
77+ if rel, act, changed := k.inhibit(2, true); rel != 1 || act != 2 || !changed {
78+ t.Fatalf("switch = %d, %d, %v", rel, act, changed)
79+ }
80+ if rel, act, changed := k.inhibit(0, true); rel != 2 || act != 0 || !changed || k.inhibited(0) {
81+ t.Fatalf("release = %d, %d, %v", rel, act, changed)
82+ }
83+}
M internal/core/layout.go +2 -1
@@ -2,12 +2,13 @@ package core
22
33 import (
44 "fmt"
5- "github.com/bnema/neferwl/internal/ports"
65 "math/bits"
76 "slices"
87 "strconv"
98 "strings"
109 "time"
10+
11+ "github.com/bnema/neferwl/internal/ports"
1112 )
1213
1314 type WindowID = ports.WindowID
@@ -2,12 +2,13 @@ package core
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5- "github.com/bnema/neferwl/internal/ports"
6 "math/bits"5 "math/bits"
7 "slices"6 "slices"
8 "strconv"7 "strconv"
9 "strings"8 "strings"
10 "time"9 "time"
10+
11+ "github.com/bnema/neferwl/internal/ports"
11 )12 )
12 13
13 type WindowID = ports.WindowID14 type WindowID = ports.WindowID
M internal/core/overview.go +2 -1
@@ -374,7 +374,8 @@ func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) {
374374 return false, nil
375375 }
376376 sc.mon.OverviewPick(id)
377- c.focusScreen, c.layerFocus = i, 0
377+ c.focusScreen = i
378+ c.keyboard.takeBack()
378379 if err := c.workspaceVisible(ctx, true); err != nil {
379380 return true, err
380381 }
@@ -374,7 +374,8 @@ func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) {
374 return false, nil374 return false, nil
375 }375 }
376 sc.mon.OverviewPick(id)376 sc.mon.OverviewPick(id)
377- c.focusScreen, c.layerFocus = i, 0377+ c.focusScreen = i
378+ c.keyboard.takeBack()
378 if err := c.workspaceVisible(ctx, true); err != nil {379 if err := c.workspaceVisible(ctx, true); err != nil {
379 return true, err380 return true, err
380 }381 }
M internal/core/state.go +2 -2
@@ -44,8 +44,8 @@ func (c *Core) state() ports.State {
4444 for _, w := range m.all() {
4545 n := indexOf(m.Workspaces, w) + 1
4646 for _, id := range w.windows() {
47- info := c.clients[id]
48- v := ports.WindowState{ID: id, AppID: info.AppID, PID: info.PID, Output: s.name(), Workspace: n, Visible: shown[id], IdleInhibit: c.idle[id], Floating: w.isFloat(id)}
47+ rec := c.windows.lookup(id)
48+ v := ports.WindowState{ID: id, AppID: rec.client.AppID, PID: rec.client.PID, Output: s.name(), Workspace: n, Visible: shown[id], IdleInhibit: rec.idleInhibit, Floating: w.isFloat(id)}
4949 if i := w.stashIndex(id); i >= 0 {
5050 v.StashIndex, v.StashCount, v.Hidden = i+1, len(w.Stash), w.stashHidden
5151 }
@@ -44,8 +44,8 @@ func (c *Core) state() ports.State {
44 for _, w := range m.all() {44 for _, w := range m.all() {
45 n := indexOf(m.Workspaces, w) + 145 n := indexOf(m.Workspaces, w) + 1
46 for _, id := range w.windows() {46 for _, id := range w.windows() {
47- info := c.clients[id]47+ rec := c.windows.lookup(id)
48- v := ports.WindowState{ID: id, AppID: info.AppID, PID: info.PID, Output: s.name(), Workspace: n, Visible: shown[id], IdleInhibit: c.idle[id], Floating: w.isFloat(id)}48+ v := ports.WindowState{ID: id, AppID: rec.client.AppID, PID: rec.client.PID, Output: s.name(), Workspace: n, Visible: shown[id], IdleInhibit: rec.idleInhibit, Floating: w.isFloat(id)}
49 if i := w.stashIndex(id); i >= 0 {49 if i := w.stashIndex(id); i >= 0 {
50 v.StashIndex, v.StashCount, v.Hidden = i+1, len(w.Stash), w.stashHidden50 v.StashIndex, v.StashCount, v.Hidden = i+1, len(w.Stash), w.stashHidden
51 }51 }
A internal/core/windows.go +110 -0
@@ -0,0 +1,110 @@
1+package core
2+
3+import (
4+ "time"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+// windowRecord is what core knows about one client surface besides its
10+// place in the layout. Fields fill in as wayland reports them, in any order.
11+type windowRecord struct {
12+ // client holds the app ID and PID of a mapped window, for State.
13+ client ports.WindowMapped
14+ mappedAt time.Time
15+ // region is the input region; nil accepts input everywhere.
16+ region *ports.InputRegionChanged
17+ // inhibitShortcuts asks to keep compositor binds while focused;
18+ // idleInhibit keeps the session awake.
19+ inhibitShortcuts bool
20+ idleInhibit bool
21+}
22+
23+// windowRegistry owns the per-surface records. Dropping a surface removes
24+// everything core keeps about it in one call.
25+type windowRegistry struct {
26+ records map[WindowID]*windowRecord
27+}
28+
29+func newWindowRegistry() windowRegistry {
30+ return windowRegistry{records: map[WindowID]*windowRecord{}}
31+}
32+
33+// at returns the record of id, creating it.
34+func (r *windowRegistry) at(id WindowID) *windowRecord {
35+ rec := r.records[id]
36+ if rec == nil {
37+ rec = &windowRecord{}
38+ r.records[id] = rec
39+ }
40+ return rec
41+}
42+
43+// lookup returns the record of id, or the zero record when there is none.
44+func (r *windowRegistry) lookup(id WindowID) windowRecord {
45+ if rec := r.records[id]; rec != nil {
46+ return *rec
47+ }
48+ return windowRecord{}
49+}
50+
51+func (r *windowRegistry) mapped(v ports.WindowMapped, now time.Time) {
52+ rec := r.at(v.ID)
53+ rec.client, rec.mappedAt = v, now
54+}
55+
56+// setAppID updates the app ID of a mapped window only.
57+func (r *windowRegistry) setAppID(id WindowID, appID string) {
58+ if rec := r.records[id]; rec != nil && rec.client.ID != 0 {
59+ rec.client.AppID = appID
60+ }
61+}
62+
63+func (r *windowRegistry) setRegion(v ports.InputRegionChanged) { r.at(v.ID).region = &v }
64+
65+func (r *windowRegistry) setInhibitShortcuts(id WindowID, on bool) {
66+ if on {
67+ r.at(id).inhibitShortcuts = true
68+ } else if rec := r.records[id]; rec != nil {
69+ rec.inhibitShortcuts = false
70+ }
71+}
72+
73+func (r *windowRegistry) setIdleInhibit(id WindowID, on bool) {
74+ if on {
75+ r.at(id).idleInhibit = true
76+ } else if rec := r.records[id]; rec != nil {
77+ rec.idleInhibit = false
78+ }
79+}
80+
81+// drop forgets the surface.
82+func (r *windowRegistry) drop(id WindowID) { delete(r.records, id) }
83+
84+// layerGone forgets the input region of a layer surface that is no longer
85+// mapped; layers get no WindowUnmapped. The record goes once it is empty.
86+func (r *windowRegistry) layerGone(id WindowID) {
87+ rec := r.records[id]
88+ if rec == nil {
89+ return
90+ }
91+ rec.region = nil
92+ if *rec == (windowRecord{}) {
93+ delete(r.records, id)
94+ }
95+}
96+
97+// acceptsInput reports whether the surface-local point is in id's input
98+// region.
99+func (r *windowRegistry) acceptsInput(id WindowID, x, y float64) bool {
100+ rec := r.records[id]
101+ if rec == nil || rec.region == nil || rec.region.All {
102+ return true
103+ }
104+ for _, box := range rec.region.Rects {
105+ if x >= float64(box.X) && x < float64(box.X+box.W) && y >= float64(box.Y) && y < float64(box.Y+box.H) {
106+ return true
107+ }
108+ }
109+ return false
110+}
@@ -0,0 +1,110 @@
1+package core
2+
3+import (
4+ "time"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+// windowRecord is what core knows about one client surface besides its
10+// place in the layout. Fields fill in as wayland reports them, in any order.
11+type windowRecord struct {
12+ // client holds the app ID and PID of a mapped window, for State.
13+ client ports.WindowMapped
14+ mappedAt time.Time
15+ // region is the input region; nil accepts input everywhere.
16+ region *ports.InputRegionChanged
17+ // inhibitShortcuts asks to keep compositor binds while focused;
18+ // idleInhibit keeps the session awake.
19+ inhibitShortcuts bool
20+ idleInhibit bool
21+}
22+
23+// windowRegistry owns the per-surface records. Dropping a surface removes
24+// everything core keeps about it in one call.
25+type windowRegistry struct {
26+ records map[WindowID]*windowRecord
27+}
28+
29+func newWindowRegistry() windowRegistry {
30+ return windowRegistry{records: map[WindowID]*windowRecord{}}
31+}
32+
33+// at returns the record of id, creating it.
34+func (r *windowRegistry) at(id WindowID) *windowRecord {
35+ rec := r.records[id]
36+ if rec == nil {
37+ rec = &windowRecord{}
38+ r.records[id] = rec
39+ }
40+ return rec
41+}
42+
43+// lookup returns the record of id, or the zero record when there is none.
44+func (r *windowRegistry) lookup(id WindowID) windowRecord {
45+ if rec := r.records[id]; rec != nil {
46+ return *rec
47+ }
48+ return windowRecord{}
49+}
50+
51+func (r *windowRegistry) mapped(v ports.WindowMapped, now time.Time) {
52+ rec := r.at(v.ID)
53+ rec.client, rec.mappedAt = v, now
54+}
55+
56+// setAppID updates the app ID of a mapped window only.
57+func (r *windowRegistry) setAppID(id WindowID, appID string) {
58+ if rec := r.records[id]; rec != nil && rec.client.ID != 0 {
59+ rec.client.AppID = appID
60+ }
61+}
62+
63+func (r *windowRegistry) setRegion(v ports.InputRegionChanged) { r.at(v.ID).region = &v }
64+
65+func (r *windowRegistry) setInhibitShortcuts(id WindowID, on bool) {
66+ if on {
67+ r.at(id).inhibitShortcuts = true
68+ } else if rec := r.records[id]; rec != nil {
69+ rec.inhibitShortcuts = false
70+ }
71+}
72+
73+func (r *windowRegistry) setIdleInhibit(id WindowID, on bool) {
74+ if on {
75+ r.at(id).idleInhibit = true
76+ } else if rec := r.records[id]; rec != nil {
77+ rec.idleInhibit = false
78+ }
79+}
80+
81+// drop forgets the surface.
82+func (r *windowRegistry) drop(id WindowID) { delete(r.records, id) }
83+
84+// layerGone forgets the input region of a layer surface that is no longer
85+// mapped; layers get no WindowUnmapped. The record goes once it is empty.
86+func (r *windowRegistry) layerGone(id WindowID) {
87+ rec := r.records[id]
88+ if rec == nil {
89+ return
90+ }
91+ rec.region = nil
92+ if *rec == (windowRecord{}) {
93+ delete(r.records, id)
94+ }
95+}
96+
97+// acceptsInput reports whether the surface-local point is in id's input
98+// region.
99+func (r *windowRegistry) acceptsInput(id WindowID, x, y float64) bool {
100+ rec := r.records[id]
101+ if rec == nil || rec.region == nil || rec.region.All {
102+ return true
103+ }
104+ for _, box := range rec.region.Rects {
105+ if x >= float64(box.X) && x < float64(box.X+box.W) && y >= float64(box.Y) && y < float64(box.Y+box.H) {
106+ return true
107+ }
108+ }
109+ return false
110+}
A internal/core/windows_test.go +62 -0
@@ -0,0 +1,62 @@
1+package core
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+func TestWindowRegistryDropForgetsEverything(t *testing.T) {
11+ r := newWindowRegistry()
12+ now := time.Unix(10, 0)
13+ r.mapped(ports.WindowMapped{ID: 1, AppID: "a", PID: 7}, now)
14+ r.setAppID(1, "b")
15+ r.setRegion(ports.InputRegionChanged{ID: 1, Rects: []ports.Rect{{W: 1, H: 1}}})
16+ r.setInhibitShortcuts(1, true)
17+ r.setIdleInhibit(1, true)
18+ rec := r.lookup(1)
19+ if rec.client.AppID != "b" || rec.client.PID != 7 || !rec.mappedAt.Equal(now) || !rec.inhibitShortcuts || !rec.idleInhibit {
20+ t.Fatalf("record = %+v", rec)
21+ }
22+ if r.acceptsInput(1, 5, 5) {
23+ t.Fatal("point outside the region accepted")
24+ }
25+ r.drop(1)
26+ if len(r.records) != 0 {
27+ t.Fatalf("records left after drop: %v", r.records)
28+ }
29+ if !r.acceptsInput(1, 5, 5) {
30+ t.Fatal("dropped window keeps its region")
31+ }
32+}
33+
34+func TestWindowRegistryAppIDNeedsMap(t *testing.T) {
35+ r := newWindowRegistry()
36+ r.setIdleInhibit(1, true)
37+ r.setAppID(1, "a")
38+ if got := r.lookup(1).client.AppID; got != "" {
39+ t.Fatalf("unmapped window got app ID %q", got)
40+ }
41+}
42+
43+func TestWindowRegistryLayerGone(t *testing.T) {
44+ r := newWindowRegistry()
45+ r.setRegion(ports.InputRegionChanged{ID: 2, Rects: []ports.Rect{{W: 1, H: 1}}})
46+ r.setRegion(ports.InputRegionChanged{ID: 3, Rects: []ports.Rect{{W: 1, H: 1}}})
47+ r.setIdleInhibit(3, true)
48+ r.layerGone(2)
49+ r.layerGone(3)
50+ if _, ok := r.records[2]; ok {
51+ t.Fatal("empty layer record kept")
52+ }
53+ if rec := r.records[3]; rec == nil || rec.region != nil || !rec.idleInhibit {
54+ t.Fatalf("layer 3 record = %+v", rec)
55+ }
56+ // Turning off an unknown surface creates nothing.
57+ r.setInhibitShortcuts(4, false)
58+ r.setIdleInhibit(4, false)
59+ if _, ok := r.records[4]; ok {
60+ t.Fatal("record created on release")
61+ }
62+}
@@ -0,0 +1,62 @@
1+package core
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+func TestWindowRegistryDropForgetsEverything(t *testing.T) {
11+ r := newWindowRegistry()
12+ now := time.Unix(10, 0)
13+ r.mapped(ports.WindowMapped{ID: 1, AppID: "a", PID: 7}, now)
14+ r.setAppID(1, "b")
15+ r.setRegion(ports.InputRegionChanged{ID: 1, Rects: []ports.Rect{{W: 1, H: 1}}})
16+ r.setInhibitShortcuts(1, true)
17+ r.setIdleInhibit(1, true)
18+ rec := r.lookup(1)
19+ if rec.client.AppID != "b" || rec.client.PID != 7 || !rec.mappedAt.Equal(now) || !rec.inhibitShortcuts || !rec.idleInhibit {
20+ t.Fatalf("record = %+v", rec)
21+ }
22+ if r.acceptsInput(1, 5, 5) {
23+ t.Fatal("point outside the region accepted")
24+ }
25+ r.drop(1)
26+ if len(r.records) != 0 {
27+ t.Fatalf("records left after drop: %v", r.records)
28+ }
29+ if !r.acceptsInput(1, 5, 5) {
30+ t.Fatal("dropped window keeps its region")
31+ }
32+}
33+
34+func TestWindowRegistryAppIDNeedsMap(t *testing.T) {
35+ r := newWindowRegistry()
36+ r.setIdleInhibit(1, true)
37+ r.setAppID(1, "a")
38+ if got := r.lookup(1).client.AppID; got != "" {
39+ t.Fatalf("unmapped window got app ID %q", got)
40+ }
41+}
42+
43+func TestWindowRegistryLayerGone(t *testing.T) {
44+ r := newWindowRegistry()
45+ r.setRegion(ports.InputRegionChanged{ID: 2, Rects: []ports.Rect{{W: 1, H: 1}}})
46+ r.setRegion(ports.InputRegionChanged{ID: 3, Rects: []ports.Rect{{W: 1, H: 1}}})
47+ r.setIdleInhibit(3, true)
48+ r.layerGone(2)
49+ r.layerGone(3)
50+ if _, ok := r.records[2]; ok {
51+ t.Fatal("empty layer record kept")
52+ }
53+ if rec := r.records[3]; rec == nil || rec.region != nil || !rec.idleInhibit {
54+ t.Fatalf("layer 3 record = %+v", rec)
55+ }
56+ // Turning off an unknown surface creates nothing.
57+ r.setInhibitShortcuts(4, false)
58+ r.setIdleInhibit(4, false)
59+ if _, ok := r.records[4]; ok {
60+ t.Fatal("record created on release")
61+ }
62+}

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 23

Tags 10

↑↓ moveenter openesc close