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 Unverified
B5690EEEBB952194The 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 | |||
| 1091 | 1091 | continue | |
| 1092 | 1092 | } | |
| 1093 | 1093 | 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 | |
| 1156 | 1099 | } | |
| 1157 | 1100 | if err != nil { | |
| 1158 | 1101 | if errors.Is(err, errOverlayDropped) { | |
| @@ -1091,68 +1091,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1091 | continue | 1091 | continue | ||
| 1092 | } | 1092 | } | ||
| 1093 | start := time.Now() | 1093 | start := time.Now() | ||
| 1094 | - | // shows is what the frame puts on screen: presentation feedback | 1094 | + | 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 := false | 1098 | + | 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) { | |||
| 102 | 102 | t.used = true | |
| 103 | 103 | s := t.server | |
| 104 | 104 | 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) | |
| 107 | 107 | token := rand.Text() | |
| 108 | 108 | if valid { | |
| 109 | 109 | s.pruneTokens() | |
| @@ -114,10 +114,10 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) { | |||
| 114 | 114 | ||
| 115 | 115 | // focusClient is the client with the keyboard focus. | |
| 116 | 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 | 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 | 121 | return l.resource.Client() | |
| 122 | 122 | } | |
| 123 | 123 | return server.Client{} | |
| @@ -102,8 +102,8 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) { | |||||
| 102 | t.used = true | 102 | t.used = true | ||
| 103 | s := t.server | 103 | 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) { | |||
| 148 | 148 | done := make(chan struct{}) | |
| 149 | 149 | s.display.Do(func() { | |
| 150 | 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 | 152 | serial = s.serial | |
| 153 | 153 | close(done) | |
| 154 | 154 | }) | |
| @@ -189,7 +189,7 @@ func waitFocus(t *testing.T, s *Server, id ports.WindowID) { | |||
| 189 | 189 | t.Helper() | |
| 190 | 190 | for deadline := time.Now().Add(time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) { | |
| 191 | 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 | 193 | if <-done == id { | |
| 194 | 194 | return | |
| 195 | 195 | } | |
| @@ -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.serial | 152 | 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 | return | 194 | return | ||
| 195 | } | 195 | } | ||
M internal/adapters/wayland/axis_test.go +6 -6
| @@ -12,7 +12,7 @@ func TestWheelSteps(t *testing.T) { | |||
| 12 | 12 | s := &Server{} | |
| 13 | 13 | var values [2]float64 | |
| 14 | 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 | 16 | Vertical: ports.ScrollAxis{Set: v != 0, V120: v, Value: float64(v) / 8}, | |
| 17 | 17 | Horizontal: ports.ScrollAxis{Set: h != 0, V120: h}, | |
| 18 | 18 | }) | |
| @@ -37,18 +37,18 @@ func TestWheelSteps(t *testing.T) { | |||
| 37 | 37 | // The held value sums the frames since the last step. | |
| 38 | 38 | *s = Server{} | |
| 39 | 39 | 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]) | |
| 42 | 42 | } | |
| 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]) | |
| 45 | 45 | } | |
| 46 | 46 | // A direction change drops what was held the other way. | |
| 47 | 47 | wheel(60, 0) | |
| 48 | 48 | if wheel(-180, 0) != [2]int32{-1, 0} || values[0] != -22.5 { | |
| 49 | 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 | 52 | t.Fatal("finger scroll made discrete steps") | |
| 53 | 53 | } | |
| 54 | 54 | } | |
| @@ -12,7 +12,7 @@ func TestWheelSteps(t *testing.T) { | |||||
| 12 | s := &Server{} | 12 | s := &Server{} | ||
| 13 | var values [2]float64 | 13 | 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 { | |||
| 79 | 79 | ||
| 80 | 80 | // focusedClient is the client with keyboard focus. | |
| 81 | 81 | func (s *Server) focusedClient() (server.Client, bool) { | |
| 82 | - | surf, _ := s.focusTarget(s.focused) | |
| 82 | + | surf, _ := s.focusTarget(s.seat.focused) | |
| 83 | 83 | if surf == nil { | |
| 84 | 84 | return server.Client{}, false | |
| 85 | 85 | } | |
| @@ -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{}, false | 84 | 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. | |||
| 110 | 110 | res.OnDestroy = func() { | |
| 111 | 111 | if surf.colorControl == h { | |
| 112 | 112 | 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 | 165 | func (h *colorSurface) Destroy(*cm.WpColorManagementSurfaceV1) { | |
| 166 | 166 | if h.surf != nil && !h.surf.destroyed { | |
| 167 | - | h.surf.pendingColor = SurfaceColor{} | |
| 167 | + | h.surf.next.color = SurfaceColor{} | |
| 168 | 168 | } | |
| 169 | 169 | } | |
| 170 | 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 | 185 | r.PostError(uint32(cm.WpColorManagementSurfaceV1ErrorImageDescription), "description not ready") | |
| 186 | 186 | return | |
| 187 | 187 | } | |
| 188 | - | h.surf.pendingColor = d.SurfaceColor | |
| 188 | + | h.surf.next.color = d.SurfaceColor | |
| 189 | 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 | 191 | func (h *colorSurface) UnsetImageDescription(r *cm.WpColorManagementSurfaceV1) { | |
| @@ -193,7 +193,7 @@ func (h *colorSurface) UnsetImageDescription(r *cm.WpColorManagementSurfaceV1) { | |||
| 193 | 193 | r.PostError(uint32(cm.WpColorManagementSurfaceV1ErrorInert), "surface destroyed") | |
| 194 | 194 | return | |
| 195 | 195 | } | |
| 196 | - | h.surf.pendingColor = SurfaceColor{} | |
| 196 | + | h.surf.next.color = SurfaceColor{} | |
| 197 | 197 | h.surf.server.log.Debug().Str("component", "wayland").Msg("surface color description unset") | |
| 198 | 198 | } | |
| 199 | 199 | ||
| @@ -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 = nil | 112 | 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 | return | 186 | return | ||
| 187 | } | 187 | } | ||
| 188 | - | h.surf.pendingColor = d.SurfaceColor | 188 | + | 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 | return | 194 | 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 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 4 | 6 | "github.com/bnema/neferwl/internal/ports" | |
| 5 | 7 | cm "github.com/bnema/purego-libwayland/protocol/colormanagement" | |
| 6 | 8 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 7 | - | "testing" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| 10 | 11 | func TestColorManagerErrors(t *testing.T) { | |
| @@ -1,10 +1,11 @@ | |||||
| 1 | package wayland | 1 | 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 | |||
| 58 | 58 | res.OnDestroy = func() { | |
| 59 | 59 | if surf.representationControl == h { | |
| 60 | 60 | 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 | 72 | func (h *representationSurface) Destroy(*colorrepresentation.WpColorRepresentationSurfaceV1) { | |
| 73 | 73 | if h.surf != nil && !h.surf.destroyed { | |
| 74 | - | h.surf.pendingRepresentation = surfaceRepresentation{} | |
| 74 | + | h.surf.next.representation = surfaceRepresentation{} | |
| 75 | 75 | } | |
| 76 | 76 | } | |
| 77 | 77 | func (h *representationSurface) live(r *colorrepresentation.WpColorRepresentationSurfaceV1) bool { | |
| @@ -89,7 +89,7 @@ func (h *representationSurface) SetAlphaMode(r *colorrepresentation.WpColorRepre | |||
| 89 | 89 | r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorAlphaMode), "unsupported alpha mode") | |
| 90 | 90 | return | |
| 91 | 91 | } | |
| 92 | - | h.surf.pendingRepresentation.alpha = uint8(mode) | |
| 92 | + | h.surf.next.representation.alpha = uint8(mode) | |
| 93 | 93 | } | |
| 94 | 94 | func (h *representationSurface) SetCoefficientsAndRange(r *colorrepresentation.WpColorRepresentationSurfaceV1, coeff, ran uint32) { | |
| 95 | 95 | if !h.live(r) { | |
| @@ -101,7 +101,7 @@ func (h *representationSurface) SetCoefficientsAndRange(r *colorrepresentation.W | |||
| 101 | 101 | r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorCoefficients), "unsupported coefficients or range") | |
| 102 | 102 | 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 | 106 | func (h *representationSurface) SetChromaLocation(r *colorrepresentation.WpColorRepresentationSurfaceV1, chroma uint32) { | |
| 107 | 107 | if !h.live(r) { | |
| @@ -111,7 +111,7 @@ func (h *representationSurface) SetChromaLocation(r *colorrepresentation.WpColor | |||
| 111 | 111 | r.PostError(uint32(colorrepresentation.WpColorRepresentationSurfaceV1ErrorChromaLocation), "unsupported chroma location") | |
| 112 | 112 | return | |
| 113 | 113 | } | |
| 114 | - | h.surf.pendingRepresentation.chroma = uint8(chroma) | |
| 114 | + | h.surf.next.representation.chroma = uint8(chroma) | |
| 115 | 115 | } | |
| 116 | 116 | ||
| 117 | 117 | // The metadata is checked against the buffer the commit will show, | |
| @@ -119,7 +119,7 @@ func (h *representationSurface) SetChromaLocation(r *colorrepresentation.WpColor | |||
| 119 | 119 | // buffer: then the one of the last queued commit that attached, or the | |
| 120 | 120 | // current one. | |
| 121 | 121 | func (s *surface) checkRepresentationCommit() bool { | |
| 122 | - | rep := s.pendingRepresentation | |
| 122 | + | rep := s.next.representation | |
| 123 | 123 | if rep.coefficients == 0 && rep.chroma == 0 { | |
| 124 | 124 | return true | |
| 125 | 125 | } | |
| @@ -130,8 +130,8 @@ func (s *surface) checkRepresentationCommit() bool { | |||
| 130 | 130 | break | |
| 131 | 131 | } | |
| 132 | 132 | } | |
| 133 | - | if s.attached { | |
| 134 | - | buffer = s.pending | |
| 133 | + | if s.next.attached { | |
| 134 | + | buffer = s.next.buffer | |
| 135 | 135 | } | |
| 136 | 136 | if buffer == nil { | |
| 137 | 137 | 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 = nil | 60 | 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 | return | 90 | 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 | return | 102 | 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 | return | 112 | 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 the | 119 | // 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.pendingRepresentation | 122 | + | rep := s.next.representation |
| 123 | if rep.coefficients == 0 && rep.chroma == 0 { | 123 | if rep.coefficients == 0 && rep.chroma == 0 { | ||
| 124 | return true | 124 | return true | ||
| 125 | } | 125 | } | ||
| @@ -130,8 +130,8 @@ func (s *surface) checkRepresentationCommit() bool { | |||||
| 130 | break | 130 | break | ||
| 131 | } | 131 | } | ||
| 132 | } | 132 | } | ||
| 133 | - | if s.attached { | 133 | + | if s.next.attached { |
| 134 | - | buffer = s.pending | 134 | + | buffer = s.next.buffer |
| 135 | } | 135 | } | ||
| 136 | if buffer == nil { | 136 | if buffer == nil { | ||
| 137 | return true | 137 | return true | ||
M internal/adapters/wayland/colorrepresentation_test.go +3 -3
| @@ -142,8 +142,8 @@ func TestColorRepresentationQueuedCommit(t *testing.T) { | |||
| 142 | 142 | surf.queue = nil | |
| 143 | 143 | surf.applyUpdate(u) | |
| 144 | 144 | } | |
| 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) | |
| 147 | 147 | } | |
| 148 | 148 | } | |
| 149 | 149 | }) | |
| @@ -176,7 +176,7 @@ func TestColorRepresentationChecksQueuedBuffer(t *testing.T) { | |||
| 176 | 176 | // The RGB commit waits in the queue; the next one only sets | |
| 177 | 177 | // YUV coefficients. | |
| 178 | 178 | surf.queueUpdate() | |
| 179 | - | surf.pendingRepresentation.coefficients = coefficient709 | |
| 179 | + | surf.next.representation.coefficients = coefficient709 | |
| 180 | 180 | if surf.checkRepresentationCommit() { | |
| 181 | 181 | t.Error("YUV coefficients accepted for a queued RGB buffer") | |
| 182 | 182 | } | |
| @@ -142,8 +142,8 @@ func TestColorRepresentationQueuedCommit(t *testing.T) { | |||||
| 142 | surf.queue = nil | 142 | 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 sets | 176 | // 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 = coefficient709 | 179 | + | 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 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "reflect" | |
| 4 | 5 | "testing" | |
| 5 | 6 | "time" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 9 | "github.com/bnema/purego-libwayland/protocol/fifo" | |
| 10 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |
| 9 | 11 | "github.com/bnema/purego-libwayland/protocol/viewporter" | |
| 10 | 12 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 11 | 13 | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |
| @@ -22,30 +24,30 @@ func TestCapturedCommitKeepsLaterRequests(t *testing.T) { | |||
| 22 | 24 | srv.constraints[surf] = cons | |
| 23 | 25 | firstRegion := &ports.Rect{W: 2} | |
| 24 | 26 | 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 | |
| 26 | 28 | 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 | |
| 30 | 32 | surf.queueUpdate() | |
| 31 | 33 | ||
| 32 | 34 | laterRegion := &ports.Rect{W: 3} | |
| 33 | 35 | 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 | |
| 35 | 37 | surf.sub.pendingLayout = []childLayout{{x: 7}} | |
| 36 | 38 | 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 | |
| 41 | 43 | laterTime := time.Now().Add(time.Second) | |
| 42 | - | surf.pendingTime = laterTime | |
| 44 | + | surf.next.at = laterTime | |
| 43 | 45 | ||
| 44 | 46 | surf.queue[0].applyGraph() | |
| 45 | 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 | 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 | 51 | t.Fatal("application consumed a later request") | |
| 50 | 52 | } | |
| 51 | 53 | surf.queueUpdate() | |
| @@ -131,13 +133,13 @@ func TestQueuedAttachKeepsLaterBuffer(t *testing.T) { | |||
| 131 | 133 | 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 | 137 | t.Error("queued attach lost its captured or later buffer") | |
| 136 | 138 | return | |
| 137 | 139 | } | |
| 138 | 140 | surf.barrier = false | |
| 139 | 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 | 143 | t.Error("applying captured attach consumed later request") | |
| 142 | 144 | } | |
| 143 | 145 | }) | |
| @@ -247,17 +249,17 @@ func TestQueuedDestructionDropsUpdates(t *testing.T) { | |||
| 247 | 249 | s.display.Do(func() { | |
| 248 | 250 | for _, surf := range s.surfaces { | |
| 249 | 251 | 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}} | |
| 252 | 254 | if kind == "viewport" { | |
| 253 | 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 | 258 | if kind == "buffer" { | |
| 257 | 259 | for resource := range s.buffers { | |
| 258 | 260 | 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 | |
| 261 | 263 | } | |
| 262 | 264 | } | |
| 263 | 265 | } | |
| @@ -312,7 +314,7 @@ func TestQueuedDestructionDropsUpdates(t *testing.T) { | |||
| 312 | 314 | // A dropped update must be cleared before a different surface reuses it. | |
| 313 | 315 | func TestDroppedUpdateRecycledWithoutState(t *testing.T) { | |
| 314 | 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 | 318 | old.queueUpdate() | |
| 317 | 319 | old.destroyed = true | |
| 318 | 320 | old.dropQueue() | |
| @@ -390,3 +392,25 @@ func TestCapturedCommitApplyAllocations(t *testing.T) { | |||
| 390 | 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 | + | } | |
| @@ -1,11 +1,13 @@ | |||||
| 1 | package wayland | 1 | 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] = cons | 24 | srv.constraints[surf] = cons | ||
| 23 | firstRegion := &ports.Rect{W: 2} | 25 | firstRegion := &ports.Rect{W: 2} | ||
| 24 | cons.pending = firstRegion | 26 | 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 |
| 26 | surf.xdg.pendingGeometry = ports.Rect{W: 10} | 28 | surf.xdg.pendingGeometry = ports.Rect{W: 10} | ||
| 27 | - | surf.pendingInputSet, surf.pendingInputAll = true, true | 29 | + | 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 = true | 31 | + | 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 = laterRegion | 35 | 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 |
| 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, false | 39 | + | 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 = true | 42 | + | surf.next.barrier = true |
| 41 | laterTime := time.Now().Add(time.Second) | 43 | laterTime := time.Now().Add(time.Second) | ||
| 42 | - | surf.pendingTime = laterTime | 44 | + | 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 = candidate | 133 | 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 | return | 138 | return | ||
| 137 | } | 139 | } | ||
| 138 | surf.barrier = false | 140 | 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 = 3 | 252 | + | 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 = true | 262 | + | 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 = true | 319 | 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() { | |||
| 235 | 235 | // pointer and keyboard focus, and deactivates any other. | |
| 236 | 236 | func (s *Server) updateConstraint() { | |
| 237 | 237 | 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 | 239 | if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) { | |
| 240 | 240 | want = c | |
| 241 | 241 | } | |
| @@ -301,7 +301,7 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 301 | 301 | // activates only there, so it never warps the pointer. | |
| 302 | 302 | func (c *constraint) contains(w *window) bool { | |
| 303 | 303 | 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 | 305 | all, rects := c.surface.effectiveInput() | |
| 306 | 306 | if !all { | |
| 307 | 307 | 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 *constraint | 237 | 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 = c | 240 | 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.region | 303 | 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() } | |||
| 103 | 103 | // hasPointerFocus reports whether the client owns the window or layer | |
| 104 | 104 | // under the pointer. | |
| 105 | 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 | 107 | return ok && owner == c | |
| 108 | 108 | } | |
| 109 | 109 | ||
| @@ -103,7 +103,7 @@ func (pointer) Release(r *wayland.Pointer) { r.Destroy() } | |||||
| 103 | // hasPointerFocus reports whether the client owns the window or layer | 103 | // 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 == c | 107 | return ok && owner == c | ||
| 108 | } | 108 | } | ||
| 109 | 109 | ||||
M internal/adapters/wayland/fifo.go +29 -56
| @@ -8,7 +8,6 @@ import ( | |||
| 8 | 8 | "github.com/bnema/purego-libwayland/protocol/committiming" | |
| 9 | 9 | "github.com/bnema/purego-libwayland/protocol/contenttype" | |
| 10 | 10 | "github.com/bnema/purego-libwayland/protocol/fifo" | |
| 11 | - | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |
| 12 | 11 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 13 | 12 | "github.com/bnema/purego-libwayland/server" | |
| 14 | 13 | "golang.org/x/sys/unix" | |
| @@ -78,12 +77,12 @@ func (h *fifoHandler) live(r *fifo.WpFifoV1) bool { | |||
| 78 | 77 | } | |
| 79 | 78 | func (h *fifoHandler) SetBarrier(r *fifo.WpFifoV1) { | |
| 80 | 79 | if h.live(r) { | |
| 81 | - | h.surface.pendingBarrier = true | |
| 80 | + | h.surface.next.barrier = true | |
| 82 | 81 | } | |
| 83 | 82 | } | |
| 84 | 83 | func (h *fifoHandler) WaitBarrier(r *fifo.WpFifoV1) { | |
| 85 | 84 | if h.live(r) { | |
| 86 | - | h.surface.pendingWait = true | |
| 85 | + | h.surface.next.wait = true | |
| 87 | 86 | } | |
| 88 | 87 | } | |
| 89 | 88 | func (*fifoHandler) Destroy(*fifo.WpFifoV1) {} | |
| @@ -121,10 +120,10 @@ func (h *timerHandler) SetTimestamp(r *committiming.WpCommitTimerV1, secHi, secL | |||
| 121 | 120 | r.PostError(uint32(committiming.WpCommitTimerV1ErrorSurfaceDestroyed), "surface destroyed") | |
| 122 | 121 | case nsec >= 1e9: | |
| 123 | 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 | 124 | r.PostError(uint32(committiming.WpCommitTimerV1ErrorTimestampExists), "timestamp already set") | |
| 126 | 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 | 129 | func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {} | |
| @@ -174,7 +173,7 @@ func (m contentTypeManager) GetSurfaceContentType(r *contenttype.WpContentTypeMa | |||
| 174 | 173 | // Destroying it sets the type back to none, double-buffered. | |
| 175 | 174 | if state.contentType == h { | |
| 176 | 175 | state.contentType = nil | |
| 177 | - | state.pendingKind = 0 | |
| 176 | + | state.next.kind = 0 | |
| 178 | 177 | } | |
| 179 | 178 | } | |
| 180 | 179 | } | |
| @@ -183,7 +182,7 @@ type contentTypeHandler struct{ surface *surface } | |||
| 183 | 182 | ||
| 184 | 183 | func (h *contentTypeHandler) SetContentType(_ *contenttype.WpContentTypeV1, kind uint32) { | |
| 185 | 184 | if h.surface != nil && !h.surface.destroyed && h.surface.contentType == h { | |
| 186 | - | h.surface.pendingKind = kind | |
| 185 | + | h.surface.next.kind = kind | |
| 187 | 186 | } | |
| 188 | 187 | } | |
| 189 | 188 | func (*contentTypeHandler) Destroy(*contenttype.WpContentTypeV1) {} | |
| @@ -199,44 +198,26 @@ func (s *Server) surfaceOf(w *wayland.Surface) *surface { | |||
| 199 | 198 | ||
| 200 | 199 | // update owns a captured commit until it applies or is discarded. | |
| 201 | 200 | 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 | |
| 240 | 221 | // refs counts incoming deps and prev links. Retired updates remain readable | |
| 241 | 222 | // until their last incoming link goes away, even after dropQueue. | |
| 242 | 223 | refs int | |
| @@ -310,17 +291,9 @@ func sameLayout(a, b []childLayout) bool { | |||
| 310 | 291 | // takePending moves the pending state into an update: one-shot state is | |
| 311 | 292 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | |
| 312 | 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} | |
| 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} | |
| 324 | 297 | if v := s.viewport; v != nil { | |
| 325 | 298 | 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 | } | |
| @@ -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 = true | 80 | + | 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 = true | 85 | + | 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 = nil | 175 | state.contentType = nil | ||
| 177 | - | state.pendingKind = 0 | 176 | + | 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 = kind | 185 | + | 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 bool | 201 | + | // pendingCommit is the wl_surface state captured by the commit. |
| 203 | - | inputRects []ports.Rect | 202 | + | pendingCommit |
| 204 | - | attached bool | 203 | + | vp *viewport |
| 205 | - | buffer *wayland.Buffer | 204 | + | vpW, vpH int32 |
| 206 | - | scale int | 205 | + | vpSet bool |
| 207 | - | transform ports.BufferTransform | 206 | + | vpSrc [4]server.Fixed |
| 208 | - | opaque []ports.Rect | 207 | + | vpCrop bool |
| 209 | - | opaqueSet bool | 208 | + | layout []childLayout |
| 210 | - | async bool | 209 | + | deps []*update |
| 211 | - | kind uint32 | 210 | + | owner *surface |
| 212 | - | callbacks []*wayland.Callback | 211 | + | prev *update |
| 213 | - | vp *viewport | 212 | + | synced, bound bool |
| 214 | - | vpW, vpH int32 | 213 | + | xdg *xdgSurface |
| 215 | - | vpSet bool | 214 | + | geometry ports.Rect |
| 216 | - | vpSrc [4]server.Fixed | 215 | + | cons *constraint |
| 217 | - | vpCrop bool | 216 | + | region *ports.Rect |
| 218 | - | layout []childLayout | 217 | + | layer *layerSurface |
| 219 | - | deps []*update | 218 | + | layerNext layerState |
| 220 | - | owner *surface | 219 | + | readyGeneration uint64 |
| 221 | - | prev *update | 220 | + | 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 readable | 221 | // 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 int | 223 | refs int | ||
| @@ -310,17 +291,9 @@ func sameLayout(a, b []childLayout) bool { | |||||
| 310 | // takePending moves the pending state into an update: one-shot state is | 291 | // 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.pendingInputRects | 295 | + | // One-shot requests apply once; sticky ones carry over. |
| 315 | - | s.pendingInputSet = false | 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} |
| 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.pendingCrop | 298 | 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) { | |||
| 222 | 222 | child := &surface{server: srv} | |
| 223 | 223 | child.sub.parent, child.sub.synced = parent, true | |
| 224 | 224 | parent.sub.children = []*surface{child} | |
| 225 | - | child.pendingScale = 2 | |
| 225 | + | child.next.scale = 2 | |
| 226 | 226 | child.queueUpdate() | |
| 227 | 227 | old := child.queue[0] | |
| 228 | - | parent.pendingScale = 3 | |
| 228 | + | parent.next.scale = 3 | |
| 229 | 229 | parent.queueUpdate() | |
| 230 | 230 | pu := parent.queue[0] | |
| 231 | 231 | if old.refs != 1 || len(pu.deps) != 1 || pu.deps[0] != old { | |
| @@ -239,7 +239,7 @@ func TestQueuedParentRetainsRetiredChild(t *testing.T) { | |||
| 239 | 239 | if !old.retired || old.refs != 1 { | |
| 240 | 240 | t.Fatalf("retired child ref count: retired=%v refs=%d", old.retired, old.refs) | |
| 241 | 241 | } | |
| 242 | - | child.pendingScale = 7 | |
| 242 | + | child.next.scale = 7 | |
| 243 | 243 | child.queueUpdate() | |
| 244 | 244 | if child.queue[0] == old || old.scale != 2 || pu.deps[0] != old { | |
| 245 | 245 | 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, true | 223 | child.sub.parent, child.sub.synced = parent, true | ||
| 224 | parent.sub.children = []*surface{child} | 224 | parent.sub.children = []*surface{child} | ||
| 225 | - | child.pendingScale = 2 | 225 | + | child.next.scale = 2 |
| 226 | child.queueUpdate() | 226 | child.queueUpdate() | ||
| 227 | old := child.queue[0] | 227 | old := child.queue[0] | ||
| 228 | - | parent.pendingScale = 3 | 228 | + | 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 = 7 | 242 | + | 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | - | "golang.org/x/sys/unix" | |
| 6 | 5 | "slices" | |
| 7 | 6 | "time" | |
| 8 | 7 | ||
| 8 | + | "golang.org/x/sys/unix" | |
| 9 | + | ||
| 9 | 10 | "github.com/bnema/neferwl/internal/ports" | |
| 10 | 11 | ||
| 11 | 12 | "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 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "os" | |
| 5 | + | ||
| 4 | 6 | "github.com/bnema/neferwl/internal/ports" | |
| 5 | 7 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 6 | 8 | "github.com/bnema/purego-libwayland/server" | |
| 7 | - | "os" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| 10 | 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 | 56 | r, e := wayland.NewSeat(c, int32(v), id, seat{s}) | |
| 56 | 57 | if e == nil { | |
| 57 | 58 | capabilities := uint32(wayland.SeatCapabilityPointer) | |
| 58 | - | if s.keymapFD >= 0 { | |
| 59 | + | if s.seat.keymapFD >= 0 { | |
| 59 | 60 | capabilities |= uint32(wayland.SeatCapabilityKeyboard) | |
| 60 | 61 | } | |
| 61 | 62 | r.SendCapabilities(capabilities) | |
| @@ -383,9 +384,9 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) { | |||
| 383 | 384 | return | |
| 384 | 385 | } | |
| 385 | 386 | 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 | 388 | p.OnDestroy = func() { | |
| 388 | - | list := s.pointers[r.Client()] | |
| 389 | + | list := s.seat.pointers[r.Client()] | |
| 389 | 390 | for i, item := range list { | |
| 390 | 391 | if item == p { | |
| 391 | 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 | 396 | if len(list) == 0 { | |
| 396 | - | delete(s.pointers, r.Client()) | |
| 397 | + | delete(s.seat.pointers, r.Client()) | |
| 397 | 398 | } else { | |
| 398 | - | s.pointers[r.Client()] = list | |
| 399 | + | s.seat.pointers[r.Client()] = list | |
| 399 | 400 | } | |
| 400 | 401 | } | |
| 401 | 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 | 404 | s.serial++ | |
| 404 | 405 | p.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y)) | |
| 405 | 406 | pointerFrame(p) | |
| @@ -413,7 +414,7 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) { | |||
| 413 | 414 | } | |
| 414 | 415 | s := h.server | |
| 415 | 416 | k.OnDestroy = func() { | |
| 416 | - | list := s.keyboards[r.Client()] | |
| 417 | + | list := s.seat.keyboards[r.Client()] | |
| 417 | 418 | for i, item := range list { | |
| 418 | 419 | if item == k { | |
| 419 | 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 | 424 | if len(list) == 0 { | |
| 424 | - | delete(s.keyboards, r.Client()) | |
| 425 | + | delete(s.seat.keyboards, r.Client()) | |
| 425 | 426 | } else { | |
| 426 | - | s.keyboards[r.Client()] = list | |
| 427 | + | s.seat.keyboards[r.Client()] = list | |
| 427 | 428 | } | |
| 428 | 429 | } | |
| 429 | - | if s.keymapFD < 0 { | |
| 430 | + | if s.seat.keymapFD < 0 { | |
| 430 | 431 | 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 | 434 | fd, size := s.currentKeymap() | |
| 434 | 435 | k.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size) | |
| 435 | 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 | 440 | s.serial++ | |
| 440 | 441 | k.SendEnter(s.serial, surf, []byte{}) | |
| 441 | 442 | s.sendModifiers(k) | |
| @@ -1,10 +1,11 @@ | |||||
| 1 | package wayland | 1 | 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 | return | 384 | return | ||
| 384 | } | 385 | } | ||
| 385 | s := h.server | 386 | 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()] = list | 399 | + | 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.server | 415 | 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()] = list | 427 | + | s.seat.keyboards[r.Client()] = list |
| 427 | } | 428 | } | ||
| 428 | } | 429 | } | ||
| 429 | - | if s.keymapFD < 0 { | 430 | + | if s.seat.keymapFD < 0 { |
| 430 | return | 431 | 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() { | |||
| 111 | 111 | } | |
| 112 | 112 | ||
| 113 | 113 | 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 { | |
| 118 | 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 | } | |
| @@ -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 = true | 114 | + | s.next.inputSet = true |
| 115 | - | s.pendingInputAll = reg == nil || reg.Resource == nil | 115 | + | s.next.inputAll = reg == nil || reg.Resource == nil |
| 116 | - | s.pendingInputRects = nil | 116 | + | 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 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "sort" | |
| 5 | + | ||
| 4 | 6 | "github.com/bnema/neferwl/internal/ports" | |
| 5 | 7 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 6 | 8 | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |
| 7 | 9 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 8 | 10 | "github.com/bnema/purego-libwayland/server" | |
| 9 | - | "sort" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| 12 | 13 | type layerShell struct{ server *Server } | |
| @@ -43,7 +44,7 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32 | |||
| 43 | 44 | return | |
| 44 | 45 | } | |
| 45 | 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 | 48 | r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed") | |
| 48 | 49 | return | |
| 49 | 50 | } | |
| @@ -77,8 +78,8 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32 | |||
| 77 | 78 | state.layer = nil | |
| 78 | 79 | state.role = nil | |
| 79 | 80 | 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 | |
| 82 | 83 | } | |
| 83 | 84 | } | |
| 84 | 85 | func (l *layerSurface) SetSize(_ *wlrlayershell.ZwlrLayerSurfaceV1, w, h uint32) { | |
| @@ -156,10 +157,10 @@ func (l *layerSurface) unmap() { | |||
| 156 | 157 | if !l.mapped { | |
| 157 | 158 | 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 | 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 | 164 | s.changePointerFocus(0, 0, 0) | |
| 164 | 165 | } | |
| 165 | 166 | l.mapped = false | |
| @@ -1,12 +1,13 @@ | |||||
| 1 | package wayland | 1 | 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 | return | 44 | 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 | return | 49 | return | ||
| 49 | } | 50 | } | ||
| @@ -77,8 +78,8 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32 | |||||
| 77 | state.layer = nil | 78 | state.layer = nil | ||
| 78 | state.role = nil | 79 | state.role = nil | ||
| 79 | state.dropQueue() | 80 | state.dropQueue() | ||
| 80 | - | state.current, state.pending = nil, nil | 81 | + | state.current, state.next.buffer = nil, nil |
| 81 | - | state.attached = false | 82 | + | 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 | return | 158 | 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 = false | 166 | l.mapped = false | ||
M internal/adapters/wayland/layer_test.go +5 -4
| @@ -2,16 +2,17 @@ package wayland | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | + | "os" | |
| 6 | + | "os/exec" | |
| 7 | + | "testing" | |
| 8 | + | "time" | |
| 9 | + | ||
| 5 | 10 | "github.com/bnema/neferwl/internal/ports" | |
| 6 | 11 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 7 | 12 | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |
| 8 | 13 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 9 | 14 | "github.com/bnema/wlturbo" | |
| 10 | 15 | "golang.org/x/sys/unix" | |
| 11 | - | "os" | |
| 12 | - | "os/exec" | |
| 13 | - | "testing" | |
| 14 | - | "time" | |
| 15 | 16 | ) | |
| 16 | 17 | ||
| 17 | 18 | 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 | |||
| 82 | 82 | w.unmap() | |
| 83 | 83 | pp.done = true | |
| 84 | 84 | 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 | |
| 87 | 87 | delete(s.windows, w.id) | |
| 88 | 88 | x.window = nil | |
| 89 | 89 | x.surface.role = nil | |
| @@ -115,7 +115,7 @@ func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) { | |||
| 115 | 115 | // Only a grab answering the client's last press may take the keyboard, | |
| 116 | 116 | // and a popup over a popup grabs only if its parent does. | |
| 117 | 117 | 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 | 119 | if !fresh || p.parentID() == 0 || !s.topGrab(r.Client(), p.parent) { | |
| 120 | 120 | p.dismiss() | |
| 121 | 121 | 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 = true | 83 | pp.done = true | ||
| 84 | x.surface.dropQueue() | 84 | x.surface.dropQueue() | ||
| 85 | - | x.surface.current, x.surface.pending = nil, nil | 85 | + | x.surface.current, x.surface.next.buffer = nil, nil |
| 86 | - | x.surface.attached = false | 86 | + | x.surface.next.attached = false |
| 87 | delete(s.windows, w.id) | 87 | delete(s.windows, w.id) | ||
| 88 | x.window = nil | 88 | x.window = nil | ||
| 89 | x.surface.role = nil | 89 | 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.server | 117 | 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 | return | 121 | return | ||
M internal/adapters/wayland/presentation.go +1 -1
| @@ -63,7 +63,7 @@ func (p presentation) Feedback(r *presentationtime.WpPresentation, surf *wayland | |||
| 63 | 63 | fb.Destroy() | |
| 64 | 64 | return | |
| 65 | 65 | } | |
| 66 | - | state.pendingFeedback = append(state.pendingFeedback, fb) | |
| 66 | + | state.next.feedback = append(state.next.feedback, fb) | |
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | 69 | type feedbackHandler struct{} | |
| @@ -63,7 +63,7 @@ func (p presentation) Feedback(r *presentationtime.WpPresentation, surf *wayland | |||||
| 63 | fb.Destroy() | 63 | fb.Destroy() | ||
| 64 | return | 64 | 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 ( | |||
| 4 | 4 | "bytes" | |
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | - | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 7 | "os" | |
| 9 | 8 | "path/filepath" | |
| 10 | 9 | "testing" | |
| 11 | 10 | "time" | |
| 12 | 11 | ||
| 12 | + | "github.com/bnema/neferwl/internal/ports" | |
| 13 | + | ||
| 13 | 14 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 14 | 15 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 15 | 16 | "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 { | |||
| 125 | 125 | return false | |
| 126 | 126 | } | |
| 127 | 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 | 129 | return true | |
| 130 | 130 | } | |
| 131 | 131 | for _, u := range surf.queue { | |
| @@ -125,7 +125,7 @@ func (s *Server) bufferReferenced(b *wayland.Buffer) bool { | |||||
| 125 | return false | 125 | 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 true | 129 | 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 | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | - | source "github.com/bnema/purego-libwayland/protocol/extimagecapturesource" | |
| 7 | - | ext "github.com/bnema/purego-libwayland/protocol/extimagecopycapture" | |
| 8 | 6 | "image" | |
| 9 | 7 | "reflect" | |
| 10 | 8 | "slices" | |
| @@ -12,6 +10,9 @@ import ( | |||
| 12 | 10 | "testing" | |
| 13 | 11 | "time" | |
| 14 | 12 | ||
| 13 | + | source "github.com/bnema/purego-libwayland/protocol/extimagecapturesource" | |
| 14 | + | ext "github.com/bnema/purego-libwayland/protocol/extimagecopycapture" | |
| 15 | + | ||
| 15 | 16 | "github.com/bnema/neferwl/internal/logging" | |
| 16 | 17 | "github.com/bnema/neferwl/internal/ports" | |
| 17 | 18 | "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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | - | "encoding/binary" | |
| 6 | 5 | "fmt" | |
| 7 | - | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |
| 8 | 6 | "os" | |
| 9 | 7 | "sync" | |
| 10 | 8 | "time" | |
| 11 | 9 | ||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |
| 11 | + | ||
| 12 | 12 | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 13 | 13 | "github.com/bnema/neferwl/internal/ports" | |
| 14 | 14 | "github.com/bnema/purego-libwayland/protocol/fractionalscale" | |
| @@ -107,34 +107,14 @@ type Server struct { | |||
| 107 | 107 | colorOutputs map[*colorOutput]struct{} | |
| 108 | 108 | colorFeedbacks map[*colorFeedback]struct{} | |
| 109 | 109 | 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 | |
| 138 | 118 | // eventMu protects only the notification queue, not display-owned window state. | |
| 139 | 119 | // The event queue is unbounded by design: it grows only if core stops draining | |
| 140 | 120 | // 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 | 216 | d.Close() | |
| 237 | 217 | 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 | 220 | s.clock = opts.Clock | |
| 241 | 221 | if s.clock == nil { | |
| 242 | 222 | s.clock = clock.System{} | |
| @@ -281,11 +261,11 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 281 | 261 | d.Close() | |
| 282 | 262 | return nil, e | |
| 283 | 263 | } | |
| 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 | |
| 285 | 265 | } | |
| 286 | 266 | 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) | |
| 289 | 269 | } | |
| 290 | 270 | s.dmabuf.close() | |
| 291 | 271 | if s.syncWait != nil { | |
| @@ -585,106 +565,8 @@ func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) ( | |||
| 585 | 565 | ||
| 586 | 566 | func (s *Server) apply(cmd ports.ClientCommand) { | |
| 587 | 567 | 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) | |
| 688 | 570 | case ports.ConfigurePopup: | |
| 689 | 571 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | |
| 690 | 572 | w.popup.configure(c) | |
| @@ -741,299 +623,3 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 741 | 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 | - | } | |
| @@ -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 uint32 | 109 | serial uint32 | ||
| 110 | - | // press is the serial of the last button or key press, sent to | 110 | + | ctx context.Context |
| 111 | - | // pressClient: popup grabs must come from it. | 111 | + | windows map[ports.WindowID]*window |
| 112 | - | press uint32 | 112 | + | layers map[ports.WindowID]*layerSurface |
| 113 | - | pressClient server.Client | 113 | + | 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.Time | 116 | + | // seat is the keyboard and pointer state (seat.go). |
| 117 | - | ctx context.Context | 117 | + | 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 draining | 119 | // 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). A | 120 | // 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, err | 217 | 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.Clock | 220 | 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, e | 262 | return nil, e | ||
| 283 | } | 263 | } | ||
| 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 |
| 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 | |||
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| 5 | 5 | "context" | |
| 6 | - | "github.com/bnema/neferwl/internal/ports" | |
| 7 | 6 | "os" | |
| 8 | 7 | "os/exec" | |
| 9 | 8 | "path/filepath" | |
| @@ -13,6 +12,8 @@ import ( | |||
| 13 | 12 | "testing" | |
| 14 | 13 | "time" | |
| 15 | 14 | ||
| 15 | + | "github.com/bnema/neferwl/internal/ports" | |
| 16 | + | ||
| 16 | 17 | "github.com/bnema/neferwl/internal/logging" | |
| 17 | 18 | ) | |
| 18 | 19 | ||
| @@ -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 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "golang.org/x/sys/unix" | |
| 5 | 4 | "os" | |
| 6 | 5 | "path/filepath" | |
| 7 | 6 | "testing" | |
| 7 | + | ||
| 8 | + | "golang.org/x/sys/unix" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| 10 | 11 | func TestListen(t *testing.T) { | |
| @@ -1,10 +1,11 @@ | |||||
| 1 | package wayland | 1 | 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 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |
| 5 | 4 | "image" | |
| 6 | 5 | "math" | |
| 7 | 6 | "slices" | |
| 8 | 7 | "time" | |
| 9 | 8 | ||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |
| 10 | + | ||
| 10 | 11 | "github.com/bnema/neferwl/internal/ports" | |
| 11 | 12 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 12 | 13 | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |
| @@ -28,27 +29,22 @@ type surface struct { | |||
| 28 | 29 | wl *wayland.Surface | |
| 29 | 30 | viewport *viewport | |
| 30 | 31 | 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 | |
| 52 | 48 | // hotX, hotY is the click point of a cursor surface (logical). | |
| 53 | 49 | hotX, hotY int | |
| 54 | 50 | // on is the output the surface entered; scale is the last scale sent. | |
| @@ -56,14 +52,14 @@ type surface struct { | |||
| 56 | 52 | scale float64 | |
| 57 | 53 | // content is the last content built from this surface's own buffer; | |
| 58 | 54 | // 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 | |
| 67 | 63 | // cachedTree is never modified once returned to emitContent. | |
| 68 | 64 | cachedTree []ports.Subsurface | |
| 69 | 65 | treeDirty bool | |
| @@ -71,39 +67,64 @@ type surface struct { | |||
| 71 | 67 | queuedScale int | |
| 72 | 68 | queuedBuffer uint32 | |
| 73 | 69 | 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 | |
| 91 | 85 | // 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 | |
| 107 | 128 | } | |
| 108 | 129 | ||
| 109 | 130 | // commitSync is a commit's explicit-sync points and acquire wait. | |
| @@ -255,10 +276,10 @@ func (s *surface) detach() { | |||
| 255 | 276 | ||
| 256 | 277 | func (s *surface) Destroy(*wayland.Surface) { | |
| 257 | 278 | s.destroyed = true | |
| 258 | - | for _, fb := range s.pendingFeedback { | |
| 279 | + | for _, fb := range s.next.feedback { | |
| 259 | 280 | discard(fb) | |
| 260 | 281 | } | |
| 261 | - | s.pendingFeedback = nil | |
| 282 | + | s.next.feedback = nil | |
| 262 | 283 | s.tearing = nil // the control becomes inert | |
| 263 | 284 | s.colorControl = nil | |
| 264 | 285 | s.representationControl = nil | |
| @@ -276,18 +297,18 @@ func (s *surface) Destroy(*wayland.Surface) { | |||
| 276 | 297 | } | |
| 277 | 298 | s.sub.children = nil | |
| 278 | 299 | s.sub.layout, s.sub.pendingLayout = nil, nil | |
| 279 | - | for _, cb := range s.callbacks { | |
| 300 | + | for _, cb := range s.next.callbacks { | |
| 280 | 301 | cb.Destroy() | |
| 281 | 302 | } | |
| 282 | - | s.callbacks = nil | |
| 303 | + | s.next.callbacks = nil | |
| 283 | 304 | // 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 | |
| 286 | 307 | if s.current != nil { | |
| 287 | 308 | s.server.releaseBuffer(s, s.current, s.hold) | |
| 288 | 309 | s.hold = syncHold{} | |
| 289 | 310 | } | |
| 290 | - | s.current, s.pending = nil, nil | |
| 311 | + | s.current, s.next.buffer = nil, nil | |
| 291 | 312 | if s.role != nil { | |
| 292 | 313 | s.role(false) | |
| 293 | 314 | } | |
| @@ -296,21 +317,21 @@ func (s *surface) Destroy(*wayland.Surface) { | |||
| 296 | 317 | } | |
| 297 | 318 | } | |
| 298 | 319 | 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 | |
| 301 | 322 | } | |
| 302 | 323 | func (s *surface) Frame(r *wayland.Surface, id uint32) { | |
| 303 | 324 | cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{}) | |
| 304 | 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 | 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 | 331 | s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack") | |
| 311 | 332 | 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 | 335 | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | |
| 315 | 336 | return | |
| 316 | 337 | } | |
| @@ -481,13 +502,13 @@ func (s *surface) applyCommit(u *update) { | |||
| 481 | 502 | } | |
| 482 | 503 | } | |
| 483 | 504 | 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)}) | |
| 486 | 507 | } | |
| 487 | 508 | } | |
| 488 | 509 | 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)}) | |
| 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 | 564 | // SetOpaqueRegion copies the region: a nil region means nothing is opaque. | |
| 544 | 565 | 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 | |
| 547 | 568 | if reg != nil && reg.Resource != nil { | |
| 548 | 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 | 577 | r.PostError(uint32(wayland.SurfaceErrorInvalidTransform), "invalid buffer transform") | |
| 557 | 578 | return | |
| 558 | 579 | } | |
| 559 | - | s.pendingTransform = ports.BufferTransform(v) | |
| 580 | + | s.next.transform = ports.BufferTransform(v) | |
| 560 | 581 | } | |
| 561 | 582 | ||
| 562 | 583 | // opaqueCovers reports whether the committed opaque region covers the whole | |
| @@ -612,7 +633,7 @@ func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | |||
| 612 | 633 | r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive") | |
| 613 | 634 | return | |
| 614 | 635 | } | |
| 615 | - | s.pendingScale = int(v) | |
| 636 | + | s.next.scale = int(v) | |
| 616 | 637 | } | |
| 617 | 638 | func (*surface) Offset(*wayland.Surface, int32, int32) {} | |
| 618 | 639 | func (*surface) GetRelease(*wayland.Surface, uint32) {} | |
| @@ -1,12 +1,13 @@ | |||||
| 1 | package wayland | 1 | 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.Surface | 29 | wl *wayland.Surface | ||
| 29 | viewport *viewport | 30 | viewport *viewport | ||
| 30 | committedViewport viewportState // value snapshot; no allocation on viewport commits | 31 | 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 int | 33 | + | next pendingCommit |
| 33 | - | // transform is committed; pendingTransform is set by set_buffer_transform | 34 | + | // 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.BufferTransform | 36 | + | 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 the | 38 | + | opaque []ports.Rect |
| 38 | - | // current buffer's own opacity (x formats). | 39 | + | formatOpaque bool |
| 39 | - | opaque, pendingOpaque []ports.Rect | 40 | + | kind roleKind |
| 40 | - | pendingOpaqueSet bool | 41 | + | xdg *xdgSurface |
| 41 | - | formatOpaque bool | 42 | + | layer *layerSurface |
| 42 | - | kind roleKind | 43 | + | server *Server |
| 43 | - | xdg *xdgSurface | 44 | + | current *wayland.Buffer |
| 44 | - | layer *layerSurface | 45 | + | lastW, lastH int // last logged buffer size |
| 45 | - | server *Server | 46 | + | role func(bool) |
| 46 | - | current, pending *wayland.Buffer | 47 | + | 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 int | 49 | 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 float64 | 52 | 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 bool | 55 | + | inputAll bool |
| 60 | - | inputRects, pendingInputRects []ports.Rect | 56 | + | inputRects []ports.Rect |
| 61 | - | sentInput, lastInputAll bool | 57 | + | sentInput, lastInputAll bool |
| 62 | - | lastInputRects []ports.Rect | 58 | + | lastInputRects []ports.Rect |
| 63 | - | content ports.SurfaceContent | 59 | + | content ports.SurfaceContent |
| 64 | - | has bool | 60 | + | has bool |
| 65 | - | identity uint64 | 61 | + | identity uint64 |
| 66 | - | version uint64 | 62 | + | version uint64 |
| 67 | // cachedTree is never modified once returned to emitContent. | 63 | // cachedTree is never modified once returned to emitContent. | ||
| 68 | cachedTree []ports.Subsurface | 64 | cachedTree []ports.Subsurface | ||
| 69 | treeDirty bool | 65 | treeDirty bool | ||
| @@ -71,39 +67,64 @@ type surface struct { | |||||
| 71 | queuedScale int | 67 | queuedScale int | ||
| 72 | queuedBuffer uint32 | 68 | queuedBuffer uint32 | ||
| 73 | sub subState | 69 | sub subState | ||
| 74 | - | // tearing is the surface's wp_tearing_control_v1; async is the | 70 | + | // tearing is the surface's wp_tearing_control_v1; async the committed |
| 75 | - | // committed hint, pendingAsync the requested one. | 71 | + | // hint. |
| 76 | - | tearing *tearingHandler | 72 | + | tearing *tearingHandler |
| 77 | - | async, pendingAsync bool | 73 | + | async bool |
| 78 | - | colorControl *colorSurface | 74 | + | colorControl *colorSurface |
| 79 | - | color, pendingColor SurfaceColor | 75 | + | color SurfaceColor |
| 80 | - | representation, pendingRepresentation surfaceRepresentation | 76 | + | representation surfaceRepresentation |
| 81 | - | representationControl *representationSurface | 77 | + | representationControl *representationSurface |
| 82 | - | // Presentation constraints (fifo.go): pending fifo requests and commit | 78 | + | // 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 *fifoHandler | 80 | + | fifo *fifoHandler |
| 85 | - | timer *timerHandler | 81 | + | timer *timerHandler |
| 86 | - | pendingBarrier, pendingWait bool | 82 | + | barrier bool |
| 87 | - | pendingTime time.Time | 83 | + | barrierAt time.Time |
| 88 | - | barrier bool | 84 | + | 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 *contentTypeHandler | 86 | + | contentType *contentTypeHandler |
| 93 | - | contentKind, pendingKind uint32 | 87 | + | contentKind uint32 |
| 94 | - | // Damage requested since the last commit: in surface (logical) and | 88 | + | // committed is what the last commit changed, in buffer pixels (full: |
| 95 | - | // buffer pixels; committed is what the last commit changed, in buffer | 89 | + | // 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.Rect | 91 | + | // Explicit sync (syncobj.go): the surface's syncobj object and the |
| 98 | - | committed damage | 92 | + | // current buffer's hold. |
| 99 | - | // pendingFeedback are wp_presentation feedbacks for the next commit. | 93 | + | sync *syncState |
| 100 | - | pendingFeedback []*presentationtime.WpPresentationFeedback | 94 | + | hold syncHold |
| 101 | - | // Explicit sync (syncobj.go): the surface's syncobj object; the | 95 | + | } |
| 102 | - | // committed points of the pending commit (pendingSync, with the wait | 96 | + | |
| 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 *syncState | 98 | + | // last commit. A commit captures it whole into an update (takePending): |
| 105 | - | pendingSync *commitSync | 99 | + | // one-shot parts are cleared, sticky parts (scale, transform, hints, color) |
| 106 | - | hold syncHold | 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 | |||
| 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 = true | 278 | 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 = nil | 282 | + | s.next.feedback = nil |
| 262 | s.tearing = nil // the control becomes inert | 283 | s.tearing = nil // the control becomes inert | ||
| 263 | s.colorControl = nil | 284 | s.colorControl = nil | ||
| 264 | s.representationControl = nil | 285 | s.representationControl = nil | ||
| @@ -276,18 +297,18 @@ func (s *surface) Destroy(*wayland.Surface) { | |||||
| 276 | } | 297 | } | ||
| 277 | s.sub.children = nil | 298 | s.sub.children = nil | ||
| 278 | s.sub.layout, s.sub.pendingLayout = nil, nil | 299 | 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 = nil | 303 | + | 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 = nil | 306 | + | 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, nil | 311 | + | 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 = b | 320 | + | s.next.buffer = b |
| 300 | - | s.attached = true | 321 | + | 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 | return | 332 | 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 | return | 336 | 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 = true | 566 | + | s.next.opaqueSet = true |
| 546 | - | s.pendingOpaque = nil | 567 | + | 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 | return | 578 | 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 whole | 583 | // 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 | return | 634 | 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 { | |||
| 149 | 149 | } | |
| 150 | 150 | a, r := st.acquire, st.release | |
| 151 | 151 | // 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 | |
| 153 | 153 | res := st.resource | |
| 154 | 154 | switch { | |
| 155 | 155 | case !a.set() && !r.set(): | |
| @@ -176,8 +176,8 @@ func (s *surface) checkSyncCommit() bool { | |||
| 176 | 176 | ||
| 177 | 177 | // pendingIsSHM reports whether the buffer of the next commit is wl_shm. | |
| 178 | 178 | func (s *surface) pendingIsSHM() bool { | |
| 179 | - | b := s.pending | |
| 180 | - | if !s.attached { | |
| 179 | + | b := s.next.buffer | |
| 180 | + | if !s.next.attached { | |
| 181 | 181 | b = s.current | |
| 182 | 182 | } | |
| 183 | 183 | if b == nil { | |
| @@ -373,7 +373,7 @@ func (s *surface) takeSyncPoints() { | |||
| 373 | 373 | } else { | |
| 374 | 374 | cs.wait = w | |
| 375 | 375 | } | |
| 376 | - | s.pendingSync = cs | |
| 376 | + | s.next.sync = cs | |
| 377 | 377 | } | |
| 378 | 378 | ||
| 379 | 379 | // 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.release | 150 | 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 != nil | 152 | + | hasBuffer := s.next.attached && s.next.buffer != nil |
| 153 | res := st.resource | 153 | 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.pending | 179 | + | b := s.next.buffer |
| 180 | - | if !s.attached { | 180 | + | if !s.next.attached { |
| 181 | b = s.current | 181 | 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 = w | 374 | cs.wait = w | ||
| 375 | } | 375 | } | ||
| 376 | - | s.pendingSync = cs | 376 | + | 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 | |||
| 39 | 39 | // Back to vsync on the next commit. | |
| 40 | 40 | if state.tearing == h { | |
| 41 | 41 | state.tearing = nil | |
| 42 | - | state.pendingAsync = false | |
| 42 | + | state.next.async = false | |
| 43 | 43 | } | |
| 44 | 44 | } | |
| 45 | 45 | } | |
| @@ -48,7 +48,7 @@ type tearingHandler struct{ surface *surface } | |||
| 48 | 48 | ||
| 49 | 49 | func (h *tearingHandler) SetPresentationHint(_ *tearingcontrol.WpTearingControlV1, hint uint32) { | |
| 50 | 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 | ||
| @@ -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 = nil | 41 | state.tearing = nil | ||
| 42 | - | state.pendingAsync = false | 42 | + | 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 | |||
| 67 | 67 | } | |
| 68 | 68 | 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 | 71 | t.enter(surf) | |
| 72 | 72 | } | |
| 73 | 73 | } | |
| @@ -168,7 +168,7 @@ func (s *Server) textCommitted(t *textInput, reenabled bool) { | |||
| 168 | 168 | // text inputs on another surface, then enter for those of the focused | |
| 169 | 169 | // client, as the spec orders them. | |
| 170 | 170 | func (s *Server) textInputFocus() { | |
| 171 | - | surf, _ := s.focusTarget(s.focused) | |
| 171 | + | surf, _ := s.focusTarget(s.seat.focused) | |
| 172 | 172 | for _, t := range s.textInputs { | |
| 173 | 173 | if t.focus != nil && (surf == nil || t.focus.Resource != surf.Resource) { | |
| 174 | 174 | t.leave() | |
| @@ -417,12 +417,12 @@ func (im *inputMethod) GrabKeyboard(r *inputmethod.ZwpInputMethodV2, id uint32) | |||
| 417 | 417 | } | |
| 418 | 418 | } | |
| 419 | 419 | 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) | |
| 422 | 422 | } | |
| 423 | - | res.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay)) | |
| 423 | + | res.SendRepeatInfo(int32(s.seat.repeatRate), int32(s.seat.repeatDelay)) | |
| 424 | 424 | s.serial++ | |
| 425 | - | m := s.modState | |
| 425 | + | m := s.seat.modState | |
| 426 | 426 | res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group) | |
| 427 | 427 | s.log.Debug().Msg("input method grabbed keyboard") | |
| 428 | 428 | } | |
| @@ -468,29 +468,29 @@ func (s *Server) grab() *keyboardGrab { | |||
| 468 | 468 | func (s *Server) grabKey(c ports.ForwardKey) bool { | |
| 469 | 469 | code := c.Key.Keycode | |
| 470 | 470 | g := s.grab() | |
| 471 | - | owned := !c.Key.Pressed && s.grabKeys[code] | |
| 471 | + | owned := !c.Key.Pressed && s.seat.grabKeys[code] | |
| 472 | 472 | if owned { | |
| 473 | - | delete(s.grabKeys, code) | |
| 473 | + | delete(s.seat.grabKeys, code) | |
| 474 | 474 | if g == nil { | |
| 475 | 475 | s.grabReleaseDropped(c.Key.State) | |
| 476 | 476 | 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 | 479 | return false | |
| 480 | 480 | } | |
| 481 | 481 | state := uint32(0) | |
| 482 | 482 | if c.Key.Pressed { | |
| 483 | 483 | 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) | |
| 486 | 486 | } | |
| 487 | - | s.grabKeys[code] = true | |
| 487 | + | s.seat.grabKeys[code] = true | |
| 488 | 488 | } | |
| 489 | 489 | s.serial++ | |
| 490 | 490 | 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 | |
| 494 | 494 | s.serial++ | |
| 495 | 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 | 500 | // grabReleaseDropped keeps the focused client's modifiers right when the | |
| 501 | 501 | // release of a grab-owned key is dropped. | |
| 502 | 502 | func (s *Server) grabReleaseDropped(m ports.ModState) { | |
| 503 | - | if m == s.modState { | |
| 503 | + | if m == s.seat.modState { | |
| 504 | 504 | return | |
| 505 | 505 | } | |
| 506 | - | s.modState = m | |
| 506 | + | s.seat.modState = m | |
| 507 | 507 | s.serial++ | |
| 508 | 508 | for _, k := range s.clientKeyboards(s.focusClient()) { | |
| 509 | 509 | s.sendModifiers(k) | |
| @@ -525,8 +525,8 @@ func (s *Server) sendGrabKeymap(keymap bool) { | |||
| 525 | 525 | if g == nil { | |
| 526 | 526 | return | |
| 527 | 527 | } | |
| 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) | |
| 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 | } | |
| @@ -67,7 +67,7 @@ func (m textInputManager) GetTextInput(r *textinput.ZwpTextInputManagerV3, id ui | |||||
| 67 | } | 67 | } | ||
| 68 | t.focus = nil | 68 | 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 focused | 168 | // 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.server | 419 | 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.modState | 425 | + | 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.Keycode | 469 | 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 true | 476 | 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 false | 479 | return false | ||
| 480 | } | 480 | } | ||
| 481 | state := uint32(0) | 481 | state := uint32(0) | ||
| 482 | if c.Key.Pressed { | 482 | if c.Key.Pressed { | ||
| 483 | state = 1 | 483 | 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] = true | 487 | + | 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.State | 492 | + | s.seat.modState = c.Key.State |
| 493 | - | m := s.modState | 493 | + | 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 the | 500 | // 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 | return | 504 | return | ||
| 505 | } | 505 | } | ||
| 506 | - | s.modState = m | 506 | + | 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 | return | 526 | 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) { | |||
| 196 | 196 | func TestCapturedTransformAndOpaqueKeepLaterRequests(t *testing.T) { | |
| 197 | 197 | srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1), constraints: map[*surface]*constraint{}} | |
| 198 | 198 | 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 | |
| 201 | 201 | 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 | |
| 204 | 204 | surf.queue[0].applyGraph() | |
| 205 | 205 | if surf.transform != 1 || len(surf.opaque) != 1 { | |
| 206 | 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 | 209 | t.Fatal("application consumed a later request") | |
| 210 | 210 | } | |
| 211 | 211 | 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 = 1 | 199 | + | surf.next.transform = 1 |
| 200 | - | surf.pendingOpaque, surf.pendingOpaqueSet = []ports.Rect{{W: 4, H: 4}}, true | 200 | + | surf.next.opaque, surf.next.opaqueSet = []ports.Rect{{W: 4, H: 4}}, true |
| 201 | surf.queueUpdate() | 201 | surf.queueUpdate() | ||
| 202 | - | surf.pendingTransform = 2 | 202 | + | surf.next.transform = 2 |
| 203 | - | surf.pendingOpaque, surf.pendingOpaqueSet = nil, true | 203 | + | 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 | |||
| 73 | 73 | k.fd, k.size = copyFD, copySize | |
| 74 | 74 | old := k.server.activeKeymapText() | |
| 75 | 75 | 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 | 77 | // Clients must see the new layout before the next key. | |
| 78 | 78 | s.sendKeymapAll(k.fd, k.size) | |
| 79 | 79 | } | |
| @@ -107,13 +107,13 @@ func (k *virtualKeyboard) keyboards(r *virtualkeyboard.ZwpVirtualKeyboardV1) []* | |||
| 107 | 107 | return nil | |
| 108 | 108 | } | |
| 109 | 109 | s := k.server | |
| 110 | - | _, keyboards := s.focusTarget(s.focused) | |
| 110 | + | _, keyboards := s.focusTarget(s.seat.focused) | |
| 111 | 111 | if len(keyboards) == 0 { | |
| 112 | 112 | return nil | |
| 113 | 113 | } | |
| 114 | - | if k.focus != s.focused { | |
| 114 | + | if k.focus != s.seat.focused { | |
| 115 | 115 | clear(k.pressed) | |
| 116 | - | k.focus = s.focused | |
| 116 | + | k.focus = s.seat.focused | |
| 117 | 117 | } | |
| 118 | 118 | s.useKeymap(k) | |
| 119 | 119 | s.serial++ | |
| @@ -148,8 +148,8 @@ func (*virtualKeyboard) Destroy(*virtualkeyboard.ZwpVirtualKeyboardV1) {} | |||
| 148 | 148 | // keys still held on the focused client are released. | |
| 149 | 149 | func (k *virtualKeyboard) release() { | |
| 150 | 150 | 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) | |
| 153 | 153 | var ts unix.Timespec | |
| 154 | 154 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | |
| 155 | 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 | 165 | s.useKeymap(nil) | |
| 166 | 166 | } | |
| 167 | 167 | if k.fd >= 0 { | |
| @@ -173,11 +173,11 @@ func (k *virtualKeyboard) release() { | |||
| 173 | 173 | // useKeymap makes every keyboard carry the keymap of k, or the seat keymap | |
| 174 | 174 | // when k is nil. Switching back restores the seat modifiers too. | |
| 175 | 175 | func (s *Server) useKeymap(k *virtualKeyboard) { | |
| 176 | - | if s.keymapOwner == k { | |
| 176 | + | if s.seat.keymapOwner == k { | |
| 177 | 177 | return | |
| 178 | 178 | } | |
| 179 | 179 | old := s.activeKeymapText() | |
| 180 | - | s.keymapOwner = k | |
| 180 | + | s.seat.keymapOwner = k | |
| 181 | 181 | // Like wlroots and smithay, a keymap equal to the active one is not | |
| 182 | 182 | // sent again: tools such as dictation apps create a virtual keyboard | |
| 183 | 183 | // with the seat layout per use, and each resend makes every client | |
| @@ -188,7 +188,7 @@ func (s *Server) useKeymap(k *virtualKeyboard) { | |||
| 188 | 188 | // Modifiers follow the keyboard that types: a skipped keymap no longer | |
| 189 | 189 | // resets them, so a virtual keyboard never types through seat Shift or | |
| 190 | 190 | // Caps Lock, and the seat gets its own back. | |
| 191 | - | m := s.modState | |
| 191 | + | m := s.seat.modState | |
| 192 | 192 | if k != nil { | |
| 193 | 193 | m = k.mods | |
| 194 | 194 | } | |
| @@ -200,7 +200,7 @@ func (s *Server) useKeymap(k *virtualKeyboard) { | |||
| 200 | 200 | } | |
| 201 | 201 | ||
| 202 | 202 | func (s *Server) sendKeymapAll(fd int, size uint32) { | |
| 203 | - | for _, list := range s.keyboards { | |
| 203 | + | for _, list := range s.seat.keyboards { | |
| 204 | 204 | for _, kb := range list { | |
| 205 | 205 | if kb.Resource.Alive() { | |
| 206 | 206 | kb.SendKeymap(uint32(wayland.KeyboardKeymapFormatXkbV1), fd, size) | |
| @@ -211,16 +211,16 @@ func (s *Server) sendKeymapAll(fd int, size uint32) { | |||
| 211 | 211 | ||
| 212 | 212 | // activeKeymapText is the text of the keymap keyboards carry. | |
| 213 | 213 | func (s *Server) activeKeymapText() string { | |
| 214 | - | if k := s.keymapOwner; k != nil { | |
| 214 | + | if k := s.seat.keymapOwner; k != nil { | |
| 215 | 215 | return k.text | |
| 216 | 216 | } | |
| 217 | - | return s.keymapText | |
| 217 | + | return s.seat.keymapText | |
| 218 | 218 | } | |
| 219 | 219 | ||
| 220 | 220 | // currentKeymap is the keymap a new keyboard must start with. | |
| 221 | 221 | func (s *Server) currentKeymap() (int, uint32) { | |
| 222 | - | if k := s.keymapOwner; k != nil { | |
| 222 | + | if k := s.seat.keymapOwner; k != nil { | |
| 223 | 223 | return k.fd, k.size | |
| 224 | 224 | } | |
| 225 | - | return s.keymapFD, s.keymapSize | |
| 225 | + | return s.seat.keymapFD, s.seat.keymapSize | |
| 226 | 226 | } | |
| @@ -73,7 +73,7 @@ func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format | |||||
| 73 | k.fd, k.size = copyFD, copySize | 73 | k.fd, k.size = copyFD, copySize | ||
| 74 | old := k.server.activeKeymapText() | 74 | old := k.server.activeKeymapText() | ||
| 75 | k.text = text | 75 | 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 nil | 107 | return nil | ||
| 108 | } | 108 | } | ||
| 109 | s := k.server | 109 | 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 nil | 112 | 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.focused | 116 | + | 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.server | 150 | 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.Timespec | 153 | 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 keymap | 173 | // 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 | return | 177 | return | ||
| 178 | } | 178 | } | ||
| 179 | old := s.activeKeymapText() | 179 | old := s.activeKeymapText() | ||
| 180 | - | s.keymapOwner = k | 180 | + | s.seat.keymapOwner = k |
| 181 | // Like wlroots and smithay, a keymap equal to the active one is not | 181 | // Like wlroots and smithay, a keymap equal to the active one is not | ||
| 182 | // sent again: tools such as dictation apps create a virtual keyboard | 182 | // sent again: tools such as dictation apps create a virtual keyboard | ||
| 183 | // with the seat layout per use, and each resend makes every client | 183 | // 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 longer | 188 | // Modifiers follow the keyboard that types: a skipped keymap no longer | ||
| 189 | // resets them, so a virtual keyboard never types through seat Shift or | 189 | // 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.modState | 191 | + | m := s.seat.modState |
| 192 | if k != nil { | 192 | if k != nil { | ||
| 193 | m = k.mods | 193 | 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.text | 215 | return k.text | ||
| 216 | } | 216 | } | ||
| 217 | - | return s.keymapText | 217 | + | 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.size | 223 | return k.fd, k.size | ||
| 224 | } | 224 | } | ||
| 225 | - | return s.keymapFD, s.keymapSize | 225 | + | 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) { | |||
| 222 | 222 | target, proxy := focusedTarget(t, s, events, commands, dir) | |
| 223 | 223 | const capsLocked = 2 | |
| 224 | 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 | 226 | <-set | |
| 227 | 227 | proxy.locked = nil | |
| 228 | 228 | 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 = 2 | 223 | 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 | <-set | 226 | <-set | ||
| 227 | proxy.locked = nil | 227 | 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/binary" | |
| 5 | + | ||
| 5 | 6 | "github.com/bnema/neferwl/internal/ports" | |
| 6 | 7 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 7 | 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 | 188 | func (w *window) unmap() { | |
| 188 | - | if w.xdg.server.pointerFocus == w.id { | |
| 189 | + | if w.xdg.server.seat.pointerFocus == w.id { | |
| 189 | 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 | 193 | w.xdg.server.changeFocus(0) | |
| 193 | 194 | } | |
| 194 | 195 | if !w.mapped { | |
| @@ -245,8 +246,8 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||
| 245 | 246 | t.OnDestroy = func() { | |
| 246 | 247 | w.unmap() | |
| 247 | 248 | 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 | |
| 250 | 251 | delete(x.server.windows, w.id) | |
| 251 | 252 | x.window = nil | |
| 252 | 253 | 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, nil | 249 | + | x.surface.current, x.surface.next.buffer = nil, nil |
| 249 | - | x.surface.attached = false | 250 | + | x.surface.next.attached = false |
| 250 | delete(x.server.windows, w.id) | 251 | delete(x.server.windows, w.id) | ||
| 251 | x.window = nil | 252 | x.window = nil | ||
| 252 | x.surface.role = nil | 253 | 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 ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | - | "github.com/bnema/neferwl/internal/ports" | |
| 8 | - | "maps" | |
| 9 | 7 | "slices" | |
| 10 | 8 | "strconv" | |
| 11 | 9 | "strings" | |
| 12 | 10 | "time" | |
| 13 | 11 | "unicode" | |
| 12 | + | ||
| 13 | + | "github.com/bnema/neferwl/internal/ports" | |
| 14 | 14 | ) | |
| 15 | 15 | ||
| 16 | 16 | var ErrQuit = errors.New("quit requested") | |
| @@ -70,8 +70,7 @@ type Core struct { | |||
| 70 | 70 | focusScreen int | |
| 71 | 71 | binds map[binding]Action | |
| 72 | 72 | pressed map[string]bool | |
| 73 | - | sent map[WindowID]ports.ConfigureWindow | |
| 74 | - | focus WindowID | |
| 73 | + | configures configures | |
| 75 | 74 | pointer WindowID | |
| 76 | 75 | grab WindowID | |
| 77 | 76 | buttons map[uint32]bool | |
| @@ -93,28 +92,13 @@ type Core struct { | |||
| 93 | 92 | toSpawn []slotKey | |
| 94 | 93 | sentPending bool | |
| 95 | 94 | 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 | |
| 102 | 98 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | |
| 103 | 99 | popups map[WindowID]*popupState | |
| 104 | 100 | 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 | |
| 118 | 102 | // activity is when core last told wayland about user input | |
| 119 | 103 | // (ports.UserActivity), sent at most once per ActivityInterval. | |
| 120 | 104 | activity time.Time | |
| @@ -316,7 +300,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) { | |||
| 316 | 300 | return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1") | |
| 317 | 301 | } | |
| 318 | 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 | 304 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | |
| 321 | 305 | if err := c.apply(cfg); err != nil { | |
| 322 | 306 | return nil, err | |
| @@ -388,35 +372,26 @@ func (c *Core) windowFocus() map[*screen]WindowID { | |||
| 388 | 372 | return m | |
| 389 | 373 | } | |
| 390 | 374 | ||
| 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). | |
| 395 | 376 | func (c *Core) keyboardFocus() WindowID { | |
| 396 | - | var layer WindowID | |
| 377 | + | var exclusive WindowID | |
| 397 | 378 | for _, sc := range c.screens { | |
| 398 | 379 | 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 | |
| 401 | 382 | } | |
| 402 | 383 | } | |
| 403 | 384 | } | |
| 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 | + | }) | |
| 420 | 395 | } | |
| 421 | 396 | ||
| 422 | 397 | // setLayers splits the mapped layer surfaces by output. A surface without an | |
| @@ -438,7 +413,7 @@ func (c *Core) setLayers(all []ports.LayerSurface) { | |||
| 438 | 413 | sc.layers = append(sc.layers, l) | |
| 439 | 414 | } | |
| 440 | 415 | for id := range previous { | |
| 441 | - | delete(c.inputRegions, id) | |
| 416 | + | c.windows.layerGone(id) | |
| 442 | 417 | } | |
| 443 | 418 | for _, sc := range c.screens { | |
| 444 | 419 | sc.arrange() | |
| @@ -466,7 +441,6 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 466 | 441 | if err := c.publishPending(ctx); err != nil { | |
| 467 | 442 | return err | |
| 468 | 443 | } | |
| 469 | - | alive := map[WindowID]bool{} | |
| 470 | 444 | focus := c.keyboardFocus() | |
| 471 | 445 | scenes := make([]ports.Scene, 0, len(c.screens)) | |
| 472 | 446 | // Before the first output (and after the last is unplugged) the | |
| @@ -484,7 +458,6 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 484 | 458 | scene.Separators = overviewOutline(layout, max(c.cfg.Border.Width, 2)) | |
| 485 | 459 | } | |
| 486 | 460 | for _, p := range layout { | |
| 487 | - | alive[p.ID] = true | |
| 488 | 461 | // Only the focused output has an activated window. | |
| 489 | 462 | focused := p.Focused && i == c.focusScreen | |
| 490 | 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 | 465 | sw.Dim = c.cfg.Stash.Dim | |
| 493 | 466 | } | |
| 494 | 467 | 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) | |
| 531 | 472 | } | |
| 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 { | |
| 539 | 474 | if err := c.command(ctx, v); err != nil { | |
| 540 | 475 | return err | |
| 541 | 476 | } | |
| 542 | - | c.sent[p.ID] = v | |
| 477 | + | c.configures.mark(v) | |
| 543 | 478 | } | |
| 544 | 479 | } | |
| 545 | 480 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | |
| 546 | 481 | scenes = append(scenes, scene) | |
| 547 | 482 | } | |
| 548 | - | for id := range c.sent { | |
| 549 | - | if !alive[id] { | |
| 550 | - | delete(c.sent, id) | |
| 551 | - | } | |
| 552 | - | } | |
| 483 | + | c.configures.prune() | |
| 553 | 484 | // A workspace switch can hide the window under the pointer; it must not get | |
| 554 | 485 | // clicks. The next motion re-runs hit-testing. | |
| 555 | 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 | 495 | if err := c.command(ctx, ports.FocusWindow{ID: focus}); err != nil { | |
| 565 | 496 | return err | |
| 566 | 497 | } | |
| 567 | - | c.focus = focus | |
| 498 | + | c.keyboard.sent = focus | |
| 568 | 499 | } | |
| 569 | 500 | if err := c.updateInhibit(ctx); err != nil { | |
| 570 | 501 | return err | |
| @@ -627,21 +558,18 @@ func (c *Core) rehit(ctx context.Context) error { | |||
| 627 | 558 | // updateInhibit makes the shortcuts inhibitor of the keyboard focus the | |
| 628 | 559 | // active one and tells wayland when it changes. | |
| 629 | 560 | 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 { | |
| 635 | 564 | return nil | |
| 636 | 565 | } | |
| 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 { | |
| 639 | 568 | return err | |
| 640 | 569 | } | |
| 641 | 570 | } | |
| 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}) | |
| 645 | 573 | } | |
| 646 | 574 | return nil | |
| 647 | 575 | } | |
| @@ -715,16 +643,7 @@ func (c *Core) allLayers() []ports.LayerSurface { | |||
| 715 | 643 | } | |
| 716 | 644 | ||
| 717 | 645 | 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) | |
| 728 | 647 | } | |
| 729 | 648 | ||
| 730 | 649 | // 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 | 738 | c.layerChanged = true | |
| 820 | 739 | c.setLayers(v.Layers) | |
| 821 | 740 | case ports.InputRegionChanged: | |
| 822 | - | c.inputRegions[v.ID] = v | |
| 741 | + | c.windows.setRegion(v) | |
| 823 | 742 | case ports.WindowMapped: | |
| 824 | - | c.clients[v.ID] = v | |
| 825 | - | c.mappedAt[v.ID] = c.now() | |
| 743 | + | c.windows.mapped(v, c.now()) | |
| 826 | 744 | c.placement.place(c, v) | |
| 827 | 745 | case ports.WindowResized: | |
| 828 | 746 | if _, w := c.screenOf(v.ID); w != nil { | |
| @@ -837,11 +755,7 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 837 | 755 | p.mapped = true | |
| 838 | 756 | } | |
| 839 | 757 | 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) | |
| 845 | 759 | if err := c.updateInhibit(ctx); err != nil { | |
| 846 | 760 | return nil | |
| 847 | 761 | } | |
| @@ -851,18 +765,11 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 851 | 765 | c.screens[i].off = !v.On | |
| 852 | 766 | } | |
| 853 | 767 | 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) | |
| 859 | 769 | c.publishState() | |
| 860 | 770 | continue | |
| 861 | 771 | 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) | |
| 866 | 773 | case ports.WindowUnmapped: | |
| 867 | 774 | if c.popups[v.ID] != nil { | |
| 868 | 775 | if err := c.dropPopup(ctx, v.ID); err != nil { | |
| @@ -872,11 +779,7 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 872 | 779 | if err := c.closePopupsOf(ctx, v.ID); err != nil { | |
| 873 | 780 | return nil | |
| 874 | 781 | } | |
| 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) | |
| 880 | 783 | if s, _ := c.screenOf(v.ID); s != nil { | |
| 881 | 784 | s.mon.RemoveWindow(v.ID) | |
| 882 | 785 | } | |
| @@ -890,7 +793,7 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 890 | 793 | case ports.PointerConstrained: | |
| 891 | 794 | c.constrained = v | |
| 892 | 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 | 797 | continue | |
| 895 | 798 | } | |
| 896 | 799 | if s, _ := c.screenOf(v.ID); s != nil { | |
| @@ -1054,22 +957,20 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 1054 | 957 | return nil | |
| 1055 | 958 | } | |
| 1056 | 959 | // 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()) { | |
| 1060 | 961 | if err := c.publish(ctx); err != nil { | |
| 1061 | 962 | 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 | 965 | // A click anywhere else takes the keyboard back. | |
| 1065 | - | c.layerFocus = 0 | |
| 966 | + | c.keyboard.takeBack() | |
| 1066 | 967 | if err := c.publish(ctx); err != nil { | |
| 1067 | 968 | return nil | |
| 1068 | 969 | } | |
| 1069 | 970 | } | |
| 1070 | 971 | // A click focuses the window and its output. | |
| 1071 | 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 | 974 | w.Click(id) | |
| 1074 | 975 | c.focusScreen = c.screenIndex(s.name()) | |
| 1075 | 976 | if err := c.publish(ctx); err != nil { | |
| @@ -1140,14 +1041,14 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 1140 | 1041 | // A focused window inhibiting shortcuts gets every key: | |
| 1141 | 1042 | // no bind runs (emergency quit and VT switch are handled by | |
| 1142 | 1043 | // input before core). | |
| 1143 | - | if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() { | |
| 1044 | + | if c.keyboard.inhibited(c.keyboardFocus()) { | |
| 1144 | 1045 | if held := heldKey(key); !key.Pressed && c.pressed[held] { | |
| 1145 | 1046 | // Its press ran a bind before inhibiting began: the | |
| 1146 | 1047 | // window never saw it. | |
| 1147 | 1048 | delete(c.pressed, held) | |
| 1148 | 1049 | 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 | 1052 | return nil | |
| 1152 | 1053 | } | |
| 1153 | 1054 | continue | |
| @@ -1197,7 +1098,7 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 1197 | 1098 | } | |
| 1198 | 1099 | before := c.cur().mon.Current() | |
| 1199 | 1100 | swiped := c.swipedWorkspace() | |
| 1200 | - | c.layerFocus = 0 // a bind acts on the windows | |
| 1101 | + | c.keyboard.takeBack() // a bind acts on the windows | |
| 1201 | 1102 | effect := c.applyAction(action) | |
| 1202 | 1103 | if effect.Quit { | |
| 1203 | 1104 | 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 int | 70 | focusScreen int | ||
| 71 | binds map[binding]Action | 71 | binds map[binding]Action | ||
| 72 | pressed map[string]bool | 72 | pressed map[string]bool | ||
| 73 | - | sent map[WindowID]ports.ConfigureWindow | 73 | + | configures configures |
| 74 | - | focus WindowID | |||
| 75 | pointer WindowID | 74 | pointer WindowID | ||
| 76 | grab WindowID | 75 | grab WindowID | ||
| 77 | buttons map[uint32]bool | 76 | buttons map[uint32]bool | ||
| @@ -93,28 +92,13 @@ type Core struct { | |||||
| 93 | toSpawn []slotKey | 92 | toSpawn []slotKey | ||
| 94 | sentPending bool | 93 | sentPending bool | ||
| 95 | firstTerminalResolved bool | 94 | firstTerminalResolved bool | ||
| 96 | - | // clients holds the app ID and PID of mapped windows, for State. | 95 | + | windows windowRegistry |
| 97 | - | clients map[WindowID]ports.WindowMapped | 96 | + | sentState ports.State |
| 98 | - | mappedAt map[WindowID]time.Time | 97 | + | 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]*popupState | 99 | popups map[WindowID]*popupState | ||
| 104 | popupOrder []WindowID | 100 | popupOrder []WindowID | ||
| 105 | - | // layerFocus is the on-demand layer surface the user clicked; it keeps | 101 | + | 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 input | 102 | // 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.Time | 104 | 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, err | 306 | return nil, err | ||
| @@ -388,35 +372,26 @@ func (c *Core) windowFocus() map[*screen]WindowID { | |||||
| 388 | return m | 372 | return m | ||
| 389 | } | 373 | } | ||
| 390 | 374 | ||||
| 391 | - | // keyboardFocus is the mapped top/overlay layer with exclusive keyboard | 375 | + | // 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 WindowID | 377 | + | 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.ID | 381 | + | exclusive = l.ID |
| 401 | } | 382 | } | ||
| 402 | } | 383 | } | ||
| 403 | } | 384 | } | ||
| 404 | - | if layer != 0 { | 385 | + | window, _ := c.cur().mon.Focused() |
| 405 | - | return layer | 386 | + | 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 g | 390 | + | 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, nil | 394 | + | }) |
| 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 an | 397 | // 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 err | 442 | 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) the | 446 | // 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.focusScreen | 462 | 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.Dim | 465 | 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.Floating | 468 | + | 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 is | 470 | + | // Only a sized configure needs the client size. |
| 498 | - | // only deactivated and marked invisible. One mapped hidden | 471 | + | 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 err | 475 | return err | ||
| 541 | } | 476 | } | ||
| 542 | - | c.sent[p.ID] = v | 477 | + | 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 get | 484 | // 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 err | 496 | return err | ||
| 566 | } | 497 | } | ||
| 567 | - | c.focus = focus | 498 | + | c.keyboard.sent = focus |
| 568 | } | 499 | } | ||
| 569 | if err := c.updateInhibit(ctx); err != nil { | 500 | if err := c.updateInhibit(ctx); err != nil { | ||
| 570 | return err | 501 | return err | ||
| @@ -627,21 +558,18 @@ func (c *Core) rehit(ctx context.Context) error { | |||||
| 627 | // updateInhibit makes the shortcuts inhibitor of the keyboard focus the | 558 | // 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 = f | 563 | + | if !changed { |
| 633 | - | } | |||
| 634 | - | if want == c.inhibiting { | |||
| 635 | return nil | 564 | 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 err | 568 | return err | ||
| 640 | } | 569 | } | ||
| 641 | } | 570 | } | ||
| 642 | - | c.inhibiting = want | 571 | + | 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 nil | 574 | 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 in | 649 | // 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 = true | 738 | 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] = v | 741 | + | c.windows.setRegion(v) |
| 823 | case ports.WindowMapped: | 742 | case ports.WindowMapped: | ||
| 824 | - | c.clients[v.ID] = v | 743 | + | 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 = true | 755 | 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 nil | 760 | return nil | ||
| 847 | } | 761 | } | ||
| @@ -851,18 +765,11 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 851 | c.screens[i].off = !v.On | 765 | 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 | continue | 770 | 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 nil | 780 | 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 = v | 794 | 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 | continue | 797 | 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 nil | 957 | 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 nil | 962 | 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 = 0 | 966 | + | c.keyboard.takeBack() |
| 1066 | if err := c.publish(ctx); err != nil { | 967 | if err := c.publish(ctx); err != nil { | ||
| 1067 | return nil | 968 | 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 by | 1042 | // 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: the | 1046 | // 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 | continue | 1049 | 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 nil | 1052 | return nil | ||
| 1152 | } | 1053 | } | ||
| 1153 | continue | 1054 | 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 windows | 1101 | + | 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 ErrQuit | 1104 | return ErrQuit | ||
M internal/core/core_test.go +5 -4
| @@ -3,14 +3,15 @@ package core_test | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 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 | 6 | "slices" | |
| 11 | 7 | "sync/atomic" | |
| 12 | 8 | "testing" | |
| 13 | 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 | 17 | 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) { | |||
| 207 | 207 | // A column wider than the view aligns as focus moves align it. | |
| 208 | 208 | w.scroll() | |
| 209 | 209 | c.focusScreen = c.screenIndex(g.screen.name()) | |
| 210 | - | c.layerFocus = 0 | |
| 210 | + | c.keyboard.takeBack() | |
| 211 | 211 | } | |
| 212 | 212 | w.shift = shown - float64(w.ViewX) | |
| 213 | 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 | 233 | if !e.Cancelled { | |
| 234 | 234 | c.focusScreen = c.screenIndex(g.screen.name()) | |
| 235 | - | c.layerFocus = 0 | |
| 235 | + | c.keyboard.takeBack() | |
| 236 | 236 | } | |
| 237 | 237 | m.switchOff, m.switchList = off, g.list | |
| 238 | 238 | m.switchMotion = newMotion(workspaceSpring(off, velocity), now) | |
| @@ -244,7 +244,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||
| 244 | 244 | before := m.Current() | |
| 245 | 245 | m.ToggleOverview() | |
| 246 | 246 | c.focusScreen = c.screenIndex(g.screen.name()) | |
| 247 | - | c.layerFocus = 0 | |
| 247 | + | c.keyboard.takeBack() | |
| 248 | 248 | return m.Current() != before | |
| 249 | 249 | case swipeDiscrete: | |
| 250 | 250 | p := g.tracker.projectedEnd() | |
| @@ -266,7 +266,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||
| 266 | 266 | return false | |
| 267 | 267 | } | |
| 268 | 268 | before := c.cur().mon.Current() | |
| 269 | - | c.layerFocus = 0 | |
| 269 | + | c.keyboard.takeBack() | |
| 270 | 270 | if mon := c.cur().mon; mon.overview { | |
| 271 | 271 | mon.overviewSwipe(a) | |
| 272 | 272 | } 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 = 0 | 210 | + | 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 = 0 | 235 | + | c.keyboard.takeBack() |
| 236 | } | 236 | } | ||
| 237 | m.switchOff, m.switchList = off, g.list | 237 | 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 = 0 | 247 | + | c.keyboard.takeBack() |
| 248 | return m.Current() != before | 248 | 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 false | 266 | return false | ||
| 267 | } | 267 | } | ||
| 268 | before := c.cur().mon.Current() | 268 | before := c.cur().mon.Current() | ||
| 269 | - | c.layerFocus = 0 | 269 | + | 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | - | "github.com/bnema/neferwl/internal/ports" | |
| 6 | 5 | "math/bits" | |
| 7 | 6 | "slices" | |
| 8 | 7 | "strconv" | |
| 9 | 8 | "strings" | |
| 10 | 9 | "time" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/neferwl/internal/ports" | |
| 11 | 12 | ) | |
| 12 | 13 | ||
| 13 | 14 | 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.WindowID | 14 | 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) { | |||
| 374 | 374 | return false, nil | |
| 375 | 375 | } | |
| 376 | 376 | sc.mon.OverviewPick(id) | |
| 377 | - | c.focusScreen, c.layerFocus = i, 0 | |
| 377 | + | c.focusScreen = i | |
| 378 | + | c.keyboard.takeBack() | |
| 378 | 379 | if err := c.workspaceVisible(ctx, true); err != nil { | |
| 379 | 380 | return true, err | |
| 380 | 381 | } | |
| @@ -374,7 +374,8 @@ func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) { | |||||
| 374 | return false, nil | 374 | return false, nil | ||
| 375 | } | 375 | } | ||
| 376 | sc.mon.OverviewPick(id) | 376 | sc.mon.OverviewPick(id) | ||
| 377 | - | c.focusScreen, c.layerFocus = i, 0 | 377 | + | 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, err | 380 | return true, err | ||
| 380 | } | 381 | } | ||
M internal/core/state.go +2 -2
| @@ -44,8 +44,8 @@ func (c *Core) state() ports.State { | |||
| 44 | 44 | for _, w := range m.all() { | |
| 45 | 45 | n := indexOf(m.Workspaces, w) + 1 | |
| 46 | 46 | 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)} | |
| 49 | 49 | if i := w.stashIndex(id); i >= 0 { | |
| 50 | 50 | v.StashIndex, v.StashCount, v.Hidden = i+1, len(w.Stash), w.stashHidden | |
| 51 | 51 | } | |
| @@ -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) + 1 | 45 | 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.stashHidden | 50 | 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 | + | } | |||