detached b3d7f77

merge: browsers (subsurfaces, window geometry, KDE decoration, mockery v3.8.0)

commit
b3d7f7788fae8e237999add62776a5f6cdcf6d65
author
brice · · 2026-09-25 08:04 UTC
merge of
78b9d9f + 18b66fe · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
15 files · +604 -66
M .github/workflows/ci.yml +1 -1
@@ -9,7 +9,7 @@ jobs:
99 with:
1010 go-version: '1.27'
1111 - run: go install github.com/bnema/hexcheck@latest
12- - run: go install github.com/vektra/mockery/v3@v3.7.4
12+ - run: go install github.com/vektra/mockery/v3@v3.8.0
1313 - run: make check
1414 - run: make race
1515 - run: make mocks-check
@@ -9,7 +9,7 @@ jobs:
9 with:9 with:
10 go-version: '1.27'10 go-version: '1.27'
11 - run: go install github.com/bnema/hexcheck@latest11 - run: go install github.com/bnema/hexcheck@latest
12- - run: go install github.com/vektra/mockery/v3@v3.7.412+ - run: go install github.com/vektra/mockery/v3@v3.8.0
13 - run: make check13 - run: make check
14 - run: make race14 - run: make race
15 - run: make mocks-check15 - run: make mocks-check
M internal/adapters/drm/cursor_plane_mock_test.go +10 -1
@@ -14,10 +14,19 @@ func newMockcursorPlane(t interface {
1414 mock.TestingT
1515 Cleanup(func())
1616 }) *mockcursorPlane {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
1721 mock := &mockcursorPlane{}
1822 mock.Mock.Test(t)
1923
20- t.Cleanup(func() { mock.AssertExpectations(t) })
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
2130
2231 return mock
2332 }
@@ -14,10 +14,19 @@ func newMockcursorPlane(t interface {
14 mock.TestingT14 mock.TestingT
15 Cleanup(func())15 Cleanup(func())
16 }) *mockcursorPlane {16 }) *mockcursorPlane {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
17 mock := &mockcursorPlane{}21 mock := &mockcursorPlane{}
18 mock.Mock.Test(t)22 mock.Mock.Test(t)
19 23
20- t.Cleanup(func() { mock.AssertExpectations(t) })24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
21 30
22 return mock31 return mock
23 }32 }
M internal/adapters/statefile/proc_tree_mock_test.go +10 -1
@@ -14,10 +14,19 @@ func newMockprocTree(t interface {
1414 mock.TestingT
1515 Cleanup(func())
1616 }) *mockprocTree {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
1721 mock := &mockprocTree{}
1822 mock.Mock.Test(t)
1923
20- t.Cleanup(func() { mock.AssertExpectations(t) })
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
2130
2231 return mock
2332 }
@@ -14,10 +14,19 @@ func newMockprocTree(t interface {
14 mock.TestingT14 mock.TestingT
15 Cleanup(func())15 Cleanup(func())
16 }) *mockprocTree {16 }) *mockprocTree {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
17 mock := &mockprocTree{}21 mock := &mockprocTree{}
18 mock.Mock.Test(t)22 mock.Mock.Test(t)
19 23
20- t.Cleanup(func() { mock.AssertExpectations(t) })24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
21 30
22 return mock31 return mock
23 }32 }
M internal/adapters/vulkan/renderer.go +31 -21
@@ -390,42 +390,52 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
390390 uploads = append(uploads, upload{rect: rect, offset: used, pixels: content.Pixels, stride: content.Stride, opaque: content.Opaque, dst: dst, src: src})
391391 used += rect.Dx() * rect.Dy() * 4
392392 }
393- // place draws content with its top-left at (x, y) logical, clipped to w×h logical.
394- place := func(content *ports.SurfaceContent, x, y, w, h int) {
393+ // drawSurface draws one surface buffer with its origin at (x, y)
394+ // logical, clipped to clip (logical).
395+ drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle) {
395396 lw, lh := content.LogicalW, content.LogicalH
396397 if lw <= 0 || lh <= 0 {
397398 lw, lh = content.Width, content.Height
398399 }
399- if content.Empty() || lw <= 0 || lh <= 0 {
400+ if content.Pixels == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
400401 return
401402 }
402403 if content.DMABuf == nil && (content.Stride < content.Width*4 || len(content.Pixels) < (content.Height-1)*content.Stride+content.Width*4) {
403404 return
404405 }
405- vw, vh := min(lw, w), min(lh, h)
406- if vw <= 0 || vh <= 0 {
407- return
408- }
409- // Buffer pixels per logical pixel on each axis.
410- src := image.Rect(0, 0, vw*content.Width/lw, vh*content.Height/lh)
411- dst := physRect(x, y, vw, vh)
406+ full := physRect(x, y, lw, lh)
412407 // A buffer drawn at the physical size (fractional-scale clients round
413408 // w*scale, we round per edge) is copied 1:1, never resampled for 1px.
414409 near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
415- if near(src.Dx(), dst.Dx()) && near(src.Dy(), dst.Dy()) {
416- dst.Max = dst.Min.Add(src.Size())
417- // Clip to the body: the buffer may be 1px wider than the slot.
418- clip := physRect(x, y, w, h)
419- if dst.Max.X > clip.Max.X {
420- src.Max.X -= dst.Max.X - clip.Max.X
421- dst.Max.X = clip.Max.X
410+ if near(full.Dx(), content.Width) && near(full.Dy(), content.Height) {
411+ full.Max = full.Min.Add(image.Pt(content.Width, content.Height))
412+ }
413+ dst := full.Intersect(physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy()))
414+ if dst.Empty() {
415+ return
416+ }
417+ // The visible part of dst in buffer pixels.
418+ bx := func(v int) int { return (v - full.Min.X) * content.Width / full.Dx() }
419+ by := func(v int) int { return (v - full.Min.Y) * content.Height / full.Dy() }
420+ src := image.Rect(bx(dst.Min.X), by(dst.Min.Y), bx(dst.Max.X), by(dst.Max.Y))
421+ addContent(dst, src, content)
422+ }
423+ // place draws a surface tree with its window geometry at (x, y)
424+ // logical, clipped to w×h logical: client shadows fall outside.
425+ place := func(content *ports.SurfaceContent, x, y, w, h int) {
426+ clip := image.Rect(x, y, x+w, y+h)
427+ ox, oy := x-content.Geometry.X, y-content.Geometry.Y
428+ for i := range content.Children {
429+ if ch := &content.Children[i]; ch.Below {
430+ drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
422431 }
423- if dst.Max.Y > clip.Max.Y {
424- src.Max.Y -= dst.Max.Y - clip.Max.Y
425- dst.Max.Y = clip.Max.Y
432+ }
433+ drawSurface(content, ox, oy, clip)
434+ for i := range content.Children {
435+ if ch := &content.Children[i]; !ch.Below {
436+ drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
426437 }
427438 }
428- addContent(dst, src, content)
429439 }
430440 fullscreen := false
431441 for _, w := range s.Windows {
@@ -390,42 +390,52 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
390 uploads = append(uploads, upload{rect: rect, offset: used, pixels: content.Pixels, stride: content.Stride, opaque: content.Opaque, dst: dst, src: src})390 uploads = append(uploads, upload{rect: rect, offset: used, pixels: content.Pixels, stride: content.Stride, opaque: content.Opaque, dst: dst, src: src})
391 used += rect.Dx() * rect.Dy() * 4391 used += rect.Dx() * rect.Dy() * 4
392 }392 }
393- // place draws content with its top-left at (x, y) logical, clipped to w×h logical.393+ // drawSurface draws one surface buffer with its origin at (x, y)
394- place := func(content *ports.SurfaceContent, x, y, w, h int) {394+ // logical, clipped to clip (logical).
395+ drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle) {
395 lw, lh := content.LogicalW, content.LogicalH396 lw, lh := content.LogicalW, content.LogicalH
396 if lw <= 0 || lh <= 0 {397 if lw <= 0 || lh <= 0 {
397 lw, lh = content.Width, content.Height398 lw, lh = content.Width, content.Height
398 }399 }
399- if content.Empty() || lw <= 0 || lh <= 0 {400+ if content.Pixels == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
400 return401 return
401 }402 }
402 if content.DMABuf == nil && (content.Stride < content.Width*4 || len(content.Pixels) < (content.Height-1)*content.Stride+content.Width*4) {403 if content.DMABuf == nil && (content.Stride < content.Width*4 || len(content.Pixels) < (content.Height-1)*content.Stride+content.Width*4) {
403 return404 return
404 }405 }
405- vw, vh := min(lw, w), min(lh, h)406+ full := physRect(x, y, lw, lh)
406- if vw <= 0 || vh <= 0 {
407- return
408- }
409- // Buffer pixels per logical pixel on each axis.
410- src := image.Rect(0, 0, vw*content.Width/lw, vh*content.Height/lh)
411- dst := physRect(x, y, vw, vh)
412 // A buffer drawn at the physical size (fractional-scale clients round407 // A buffer drawn at the physical size (fractional-scale clients round
413 // w*scale, we round per edge) is copied 1:1, never resampled for 1px.408 // w*scale, we round per edge) is copied 1:1, never resampled for 1px.
414 near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }409 near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
415- if near(src.Dx(), dst.Dx()) && near(src.Dy(), dst.Dy()) {410+ if near(full.Dx(), content.Width) && near(full.Dy(), content.Height) {
416- dst.Max = dst.Min.Add(src.Size())411+ full.Max = full.Min.Add(image.Pt(content.Width, content.Height))
417- // Clip to the body: the buffer may be 1px wider than the slot.412+ }
418- clip := physRect(x, y, w, h)413+ dst := full.Intersect(physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy()))
419- if dst.Max.X > clip.Max.X {414+ if dst.Empty() {
420- src.Max.X -= dst.Max.X - clip.Max.X415+ return
421- dst.Max.X = clip.Max.X416+ }
417+ // The visible part of dst in buffer pixels.
418+ bx := func(v int) int { return (v - full.Min.X) * content.Width / full.Dx() }
419+ by := func(v int) int { return (v - full.Min.Y) * content.Height / full.Dy() }
420+ src := image.Rect(bx(dst.Min.X), by(dst.Min.Y), bx(dst.Max.X), by(dst.Max.Y))
421+ addContent(dst, src, content)
422+ }
423+ // place draws a surface tree with its window geometry at (x, y)
424+ // logical, clipped to w×h logical: client shadows fall outside.
425+ place := func(content *ports.SurfaceContent, x, y, w, h int) {
426+ clip := image.Rect(x, y, x+w, y+h)
427+ ox, oy := x-content.Geometry.X, y-content.Geometry.Y
428+ for i := range content.Children {
429+ if ch := &content.Children[i]; ch.Below {
430+ drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
422 }431 }
423- if dst.Max.Y > clip.Max.Y {432+ }
424- src.Max.Y -= dst.Max.Y - clip.Max.Y433+ drawSurface(content, ox, oy, clip)
425- dst.Max.Y = clip.Max.Y434+ for i := range content.Children {
435+ if ch := &content.Children[i]; !ch.Below {
436+ drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
426 }437 }
427 }438 }
428- addContent(dst, src, content)
429 }439 }
430 fullscreen := false440 fullscreen := false
431 for _, w := range s.Windows {441 for _, w := range s.Windows {
M internal/adapters/vulkan/renderer_test.go +56 -0
@@ -247,3 +247,59 @@ func TestRendererScale(t *testing.T) {
247247 t.Errorf("clipped column = %v", got)
248248 }
249249 }
250+
251+// solidContent is a w×h B8G8R8A8 buffer of one color.
252+func solidContent(w, h int, c color.RGBA) ports.SurfaceContent {
253+ px := make([]byte, w*h*4)
254+ for i := 0; i < len(px); i += 4 {
255+ px[i], px[i+1], px[i+2], px[i+3] = c.B, c.G, c.R, 255
256+ }
257+ return ports.SurfaceContent{Width: w, Height: h, Stride: w * 4, Pixels: px}
258+}
259+
260+// The window geometry lands on the window rect and the client shadow
261+// around it is clipped; subsurfaces draw from the root origin, above or
262+// below it.
263+func TestRendererGeometryAndSubsurfaces(t *testing.T) {
264+ r, err := New(64, 48)
265+ if err != nil {
266+ t.Skipf("Vulkan unavailable: %v", err)
267+ }
268+ defer r.Close()
269+ red, green, blue := color.RGBA{255, 0, 0, 255}, color.RGBA{0, 255, 0, 255}, color.RGBA{0, 0, 255, 255}
270+ bg := color.RGBA{16, 32, 48, 255}
271+ scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}, Borderless: true}}}
272+ // A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow.
273+ root := solidContent(30, 20, red)
274+ root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10}
275+ // Green covers the window's top-left 4×4 corner; blue sits below the
276+ // root, hidden by it.
277+ root.Children = []ports.Subsurface{
278+ {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(30, 20, blue)},
279+ {X: 5, Y: 5, SurfaceContent: solidContent(4, 4, green)},
280+ }
281+ if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
282+ t.Fatal(err)
283+ }
284+ px := r.Pixels()
285+ for _, c := range []struct {
286+ x, y int
287+ want color.RGBA
288+ }{
289+ {10, 10, green}, {13, 13, green}, {14, 14, red}, {29, 19, red},
290+ // The shadow and the child reaching past it are clipped.
291+ {9, 10, bg}, {30, 19, bg}, {29, 20, bg},
292+ } {
293+ if got := px.At(c.x, c.y); got != c.want {
294+ t.Errorf("At(%d,%d)=%v want %v", c.x, c.y, got, c.want)
295+ }
296+ }
297+ // A root with no buffer of its own still shows its children.
298+ only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(20, 10, blue)}}}
299+ if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
300+ t.Fatal(err)
301+ }
302+ if got := r.Pixels().At(20, 15); got != blue {
303+ t.Errorf("child only: %v", got)
304+ }
305+}
@@ -247,3 +247,59 @@ func TestRendererScale(t *testing.T) {
247 t.Errorf("clipped column = %v", got)247 t.Errorf("clipped column = %v", got)
248 }248 }
249 }249 }
250+
251+// solidContent is a w×h B8G8R8A8 buffer of one color.
252+func solidContent(w, h int, c color.RGBA) ports.SurfaceContent {
253+ px := make([]byte, w*h*4)
254+ for i := 0; i < len(px); i += 4 {
255+ px[i], px[i+1], px[i+2], px[i+3] = c.B, c.G, c.R, 255
256+ }
257+ return ports.SurfaceContent{Width: w, Height: h, Stride: w * 4, Pixels: px}
258+}
259+
260+// The window geometry lands on the window rect and the client shadow
261+// around it is clipped; subsurfaces draw from the root origin, above or
262+// below it.
263+func TestRendererGeometryAndSubsurfaces(t *testing.T) {
264+ r, err := New(64, 48)
265+ if err != nil {
266+ t.Skipf("Vulkan unavailable: %v", err)
267+ }
268+ defer r.Close()
269+ red, green, blue := color.RGBA{255, 0, 0, 255}, color.RGBA{0, 255, 0, 255}, color.RGBA{0, 0, 255, 255}
270+ bg := color.RGBA{16, 32, 48, 255}
271+ scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}, Borderless: true}}}
272+ // A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow.
273+ root := solidContent(30, 20, red)
274+ root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10}
275+ // Green covers the window's top-left 4×4 corner; blue sits below the
276+ // root, hidden by it.
277+ root.Children = []ports.Subsurface{
278+ {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(30, 20, blue)},
279+ {X: 5, Y: 5, SurfaceContent: solidContent(4, 4, green)},
280+ }
281+ if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
282+ t.Fatal(err)
283+ }
284+ px := r.Pixels()
285+ for _, c := range []struct {
286+ x, y int
287+ want color.RGBA
288+ }{
289+ {10, 10, green}, {13, 13, green}, {14, 14, red}, {29, 19, red},
290+ // The shadow and the child reaching past it are clipped.
291+ {9, 10, bg}, {30, 19, bg}, {29, 20, bg},
292+ } {
293+ if got := px.At(c.x, c.y); got != c.want {
294+ t.Errorf("At(%d,%d)=%v want %v", c.x, c.y, got, c.want)
295+ }
296+ }
297+ // A root with no buffer of its own still shows its children.
298+ only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(20, 10, blue)}}}
299+ if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
300+ t.Fatal(err)
301+ }
302+ if got := r.Pixels().At(20, 15); got != blue {
303+ t.Errorf("child only: %v", got)
304+ }
305+}
M internal/adapters/wayland/decoration.go +31 -1
@@ -1,6 +1,8 @@
11 package wayland
22
33 import (
4+ "github.com/bnema/purego-libwayland/protocol/kdedecoration"
5+ "github.com/bnema/purego-libwayland/protocol/wayland"
46 "github.com/bnema/purego-libwayland/protocol/xdgdecoration"
57 "github.com/bnema/purego-libwayland/protocol/xdgshell"
68 "github.com/bnema/purego-libwayland/server"
@@ -11,9 +13,37 @@ import (
1113 type decorationManager struct{ s *Server }
1214
1315 func registerDecoration(d *server.Display, s *Server) error {
14- return xdgdecoration.NewZxdgDecorationManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
16+ err := xdgdecoration.NewZxdgDecorationManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
1517 _, _ = xdgdecoration.NewZxdgDecorationManagerV1(c, int32(v), id, decorationManager{s})
1618 })
19+ if err != nil {
20+ return err
21+ }
22+ // GTK only speaks KDE's older protocol: without it GTK4 draws its own
23+ // title bar.
24+ return kdedecoration.NewOrgKdeKwinServerDecorationManagerGlobal(d, 1, func(c server.Client, v, id uint32) {
25+ m, err := kdedecoration.NewOrgKdeKwinServerDecorationManager(c, int32(v), id, kdeManager{})
26+ if err == nil {
27+ m.SendDefaultMode(uint32(kdedecoration.OrgKdeKwinServerDecorationManagerModeServer))
28+ }
29+ })
30+}
31+
32+type kdeManager struct{}
33+
34+func (kdeManager) Create(r *kdedecoration.OrgKdeKwinServerDecorationManager, id uint32, _ *wayland.Surface) {
35+ if d, err := kdedecoration.NewOrgKdeKwinServerDecoration(r.Client(), r.Version(), id, kdeDecoration{}); err == nil {
36+ d.SendMode(uint32(kdedecoration.OrgKdeKwinServerDecorationModeServer))
37+ }
38+}
39+
40+type kdeDecoration struct{}
41+
42+func (kdeDecoration) Release(*kdedecoration.OrgKdeKwinServerDecoration) {}
43+
44+// Any requested mode is answered with server-side, like xdg-decoration.
45+func (kdeDecoration) RequestMode(r *kdedecoration.OrgKdeKwinServerDecoration, _ uint32) {
46+ r.SendMode(uint32(kdedecoration.OrgKdeKwinServerDecorationModeServer))
1747 }
1848
1949 func (decorationManager) Destroy(*xdgdecoration.ZxdgDecorationManagerV1) {}
@@ -1,6 +1,8 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "github.com/bnema/purego-libwayland/protocol/kdedecoration"
5+ "github.com/bnema/purego-libwayland/protocol/wayland"
4 "github.com/bnema/purego-libwayland/protocol/xdgdecoration"6 "github.com/bnema/purego-libwayland/protocol/xdgdecoration"
5 "github.com/bnema/purego-libwayland/protocol/xdgshell"7 "github.com/bnema/purego-libwayland/protocol/xdgshell"
6 "github.com/bnema/purego-libwayland/server"8 "github.com/bnema/purego-libwayland/server"
@@ -11,9 +13,37 @@ import (
11 type decorationManager struct{ s *Server }13 type decorationManager struct{ s *Server }
12 14
13 func registerDecoration(d *server.Display, s *Server) error {15 func registerDecoration(d *server.Display, s *Server) error {
14- return xdgdecoration.NewZxdgDecorationManagerV1Global(d, 1, func(c server.Client, v, id uint32) {16+ err := xdgdecoration.NewZxdgDecorationManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
15 _, _ = xdgdecoration.NewZxdgDecorationManagerV1(c, int32(v), id, decorationManager{s})17 _, _ = xdgdecoration.NewZxdgDecorationManagerV1(c, int32(v), id, decorationManager{s})
16 })18 })
19+ if err != nil {
20+ return err
21+ }
22+ // GTK only speaks KDE's older protocol: without it GTK4 draws its own
23+ // title bar.
24+ return kdedecoration.NewOrgKdeKwinServerDecorationManagerGlobal(d, 1, func(c server.Client, v, id uint32) {
25+ m, err := kdedecoration.NewOrgKdeKwinServerDecorationManager(c, int32(v), id, kdeManager{})
26+ if err == nil {
27+ m.SendDefaultMode(uint32(kdedecoration.OrgKdeKwinServerDecorationManagerModeServer))
28+ }
29+ })
30+}
31+
32+type kdeManager struct{}
33+
34+func (kdeManager) Create(r *kdedecoration.OrgKdeKwinServerDecorationManager, id uint32, _ *wayland.Surface) {
35+ if d, err := kdedecoration.NewOrgKdeKwinServerDecoration(r.Client(), r.Version(), id, kdeDecoration{}); err == nil {
36+ d.SendMode(uint32(kdedecoration.OrgKdeKwinServerDecorationModeServer))
37+ }
38+}
39+
40+type kdeDecoration struct{}
41+
42+func (kdeDecoration) Release(*kdedecoration.OrgKdeKwinServerDecoration) {}
43+
44+// Any requested mode is answered with server-side, like xdg-decoration.
45+func (kdeDecoration) RequestMode(r *kdedecoration.OrgKdeKwinServerDecoration, _ uint32) {
46+ r.SendMode(uint32(kdedecoration.OrgKdeKwinServerDecorationModeServer))
17 }47 }
18 48
19 func (decorationManager) Destroy(*xdgdecoration.ZxdgDecorationManagerV1) {}49 func (decorationManager) Destroy(*xdgdecoration.ZxdgDecorationManagerV1) {}
M internal/adapters/wayland/globals.go +106 -14
@@ -82,29 +82,120 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par
8282 if w == nil || parent == nil {
8383 return
8484 }
85- state := c.server.surfaces[w.Resource]
86- if state == nil {
85+ state, up := c.server.surfaces[w.Resource], c.server.surfaces[parent.Resource]
86+ if state == nil || up == nil {
8787 return
8888 }
89- if state.kind != roleNone {
89+ if state.kind != roleNone && state.kind != roleSubsurface || state.sub.role != nil {
9090 r.PostError(uint32(wayland.SubcompositorErrorBadSurface), "surface already has role")
9191 return
9292 }
93- if sub, err := wayland.NewSubsurface(r.Client(), 1, id, subsurface{state}); err == nil {
94- state.kind = roleSubsurface
95- state.role = func(bool) {}
96- sub.OnDestroy = func() { state.role = nil }
93+ // A surface cannot be its own ancestor.
94+ for p := up; p != nil; p = p.sub.parent {
95+ if p == state {
96+ r.PostError(uint32(wayland.SubcompositorErrorBadParent), "parent is a descendant")
97+ return
98+ }
99+ }
100+ sub, err := wayland.NewSubsurface(r.Client(), 1, id, subsurface{state})
101+ if err != nil {
102+ return
103+ }
104+ state.kind = roleSubsurface
105+ state.role = func(bool) {}
106+ state.sub = subState{role: sub, parent: up, children: state.sub.children}
107+ up.sub.children = append(up.sub.children, state)
108+ // The child follows its root window's output and scale.
109+ state.sendScale()
110+ sub.OnDestroy = func() {
111+ if state.sub.role != sub {
112+ return
113+ }
114+ state.sub.role = nil
115+ state.role = nil
116+ state.detach()
97117 }
98118 }
99119
120+// Subsurfaces are drawn as their own commits arrive (desynchronized): the
121+// synchronized mode's cached state is not implemented.
100122 type subsurface struct{ surface *surface }
101123
102-func (subsurface) Destroy(*wayland.Subsurface) {}
103-func (subsurface) SetPosition(*wayland.Subsurface, int32, int32) {}
104-func (subsurface) PlaceAbove(*wayland.Subsurface, *wayland.Surface) {}
105-func (subsurface) PlaceBelow(*wayland.Subsurface, *wayland.Surface) {}
106-func (subsurface) SetSync(*wayland.Subsurface) {}
107-func (subsurface) SetDesync(*wayland.Subsurface) {}
124+func (subsurface) Destroy(*wayland.Subsurface) {}
125+func (s subsurface) SetPosition(_ *wayland.Subsurface, x, y int32) {
126+ s.surface.sub.pendX, s.surface.sub.pendY, s.surface.sub.moved = int(x), int(y), true
127+}
128+func (s subsurface) PlaceAbove(r *wayland.Subsurface, sibling *wayland.Surface) {
129+ s.restack(r, sibling, true)
130+}
131+func (s subsurface) PlaceBelow(r *wayland.Subsurface, sibling *wayland.Surface) {
132+ s.restack(r, sibling, false)
133+}
134+func (subsurface) SetSync(*wayland.Subsurface) {}
135+func (subsurface) SetDesync(*wayland.Subsurface) {}
136+
137+// restack moves the surface next to a sibling or its parent. The order
138+// applies at once rather than on the parent commit.
139+func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, above bool) {
140+ me := s.surface
141+ p := me.sub.parent
142+ if p == nil || sibling == nil {
143+ return
144+ }
145+ other := me.server.surfaces[sibling.Resource]
146+ if other == me || other == nil || other != p && other.sub.parent != p {
147+ r.PostError(uint32(wayland.SubsurfaceErrorBadSurface), "not a sibling or the parent")
148+ return
149+ }
150+ list := p.sub.children
151+ for i, ch := range list {
152+ if ch == me {
153+ list = append(list[:i:i], list[i+1:]...)
154+ break
155+ }
156+ }
157+ if other == p {
158+ // Relative to the parent: just above it or just below it.
159+ me.sub.below = !above
160+ if above {
161+ list = append([]*surface{me}, list...)
162+ } else {
163+ list = append(list, me)
164+ }
165+ // Keep below children first so the flattening stays ordered.
166+ sortBelowFirst(list)
167+ } else {
168+ me.sub.below = other.sub.below
169+ at := len(list)
170+ for i, ch := range list {
171+ if ch == other {
172+ at = i
173+ if above {
174+ at++
175+ }
176+ break
177+ }
178+ }
179+ list = append(list[:at:at], append([]*surface{me}, list[at:]...)...)
180+ }
181+ p.sub.children = list
182+ me.redraw()
183+}
184+
185+// sortBelowFirst moves children below their parent ahead of those above,
186+// keeping each group's order.
187+func sortBelowFirst(list []*surface) {
188+ below := make([]*surface, 0, len(list))
189+ above := make([]*surface, 0, len(list))
190+ for _, ch := range list {
191+ if ch.sub.below {
192+ below = append(below, ch)
193+ } else {
194+ above = append(above, ch)
195+ }
196+ }
197+ copy(list, append(below, above...))
198+}
108199
109200 type region struct{}
110201
@@ -268,7 +359,8 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
268359 }
269360 if w := s.windows[s.pointerFocus]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() {
270361 s.serial++
271- p.SendEnter(s.serial, w.xdg.surfaceResource(), server.FixedFromFloat(s.pointerX), server.FixedFromFloat(s.pointerY))
362+ x, y := w.surfacePoint(s.pointerX, s.pointerY)
363+ p.SendEnter(s.serial, w.xdg.surfaceResource(), server.FixedFromFloat(x), server.FixedFromFloat(y))
272364 pointerFrame(p)
273365 }
274366 }
@@ -82,29 +82,120 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par
82 if w == nil || parent == nil {82 if w == nil || parent == nil {
83 return83 return
84 }84 }
85- state := c.server.surfaces[w.Resource]85+ state, up := c.server.surfaces[w.Resource], c.server.surfaces[parent.Resource]
86- if state == nil {86+ if state == nil || up == nil {
87 return87 return
88 }88 }
89- if state.kind != roleNone {89+ if state.kind != roleNone && state.kind != roleSubsurface || state.sub.role != nil {
90 r.PostError(uint32(wayland.SubcompositorErrorBadSurface), "surface already has role")90 r.PostError(uint32(wayland.SubcompositorErrorBadSurface), "surface already has role")
91 return91 return
92 }92 }
93- if sub, err := wayland.NewSubsurface(r.Client(), 1, id, subsurface{state}); err == nil {93+ // A surface cannot be its own ancestor.
94- state.kind = roleSubsurface94+ for p := up; p != nil; p = p.sub.parent {
95- state.role = func(bool) {}95+ if p == state {
96- sub.OnDestroy = func() { state.role = nil }96+ r.PostError(uint32(wayland.SubcompositorErrorBadParent), "parent is a descendant")
97+ return
98+ }
99+ }
100+ sub, err := wayland.NewSubsurface(r.Client(), 1, id, subsurface{state})
101+ if err != nil {
102+ return
103+ }
104+ state.kind = roleSubsurface
105+ state.role = func(bool) {}
106+ state.sub = subState{role: sub, parent: up, children: state.sub.children}
107+ up.sub.children = append(up.sub.children, state)
108+ // The child follows its root window's output and scale.
109+ state.sendScale()
110+ sub.OnDestroy = func() {
111+ if state.sub.role != sub {
112+ return
113+ }
114+ state.sub.role = nil
115+ state.role = nil
116+ state.detach()
97 }117 }
98 }118 }
99 119
120+// Subsurfaces are drawn as their own commits arrive (desynchronized): the
121+// synchronized mode's cached state is not implemented.
100 type subsurface struct{ surface *surface }122 type subsurface struct{ surface *surface }
101 123
102-func (subsurface) Destroy(*wayland.Subsurface) {}124+func (subsurface) Destroy(*wayland.Subsurface) {}
103-func (subsurface) SetPosition(*wayland.Subsurface, int32, int32) {}125+func (s subsurface) SetPosition(_ *wayland.Subsurface, x, y int32) {
104-func (subsurface) PlaceAbove(*wayland.Subsurface, *wayland.Surface) {}126+ s.surface.sub.pendX, s.surface.sub.pendY, s.surface.sub.moved = int(x), int(y), true
105-func (subsurface) PlaceBelow(*wayland.Subsurface, *wayland.Surface) {}127+}
106-func (subsurface) SetSync(*wayland.Subsurface) {}128+func (s subsurface) PlaceAbove(r *wayland.Subsurface, sibling *wayland.Surface) {
107-func (subsurface) SetDesync(*wayland.Subsurface) {}129+ s.restack(r, sibling, true)
130+}
131+func (s subsurface) PlaceBelow(r *wayland.Subsurface, sibling *wayland.Surface) {
132+ s.restack(r, sibling, false)
133+}
134+func (subsurface) SetSync(*wayland.Subsurface) {}
135+func (subsurface) SetDesync(*wayland.Subsurface) {}
136+
137+// restack moves the surface next to a sibling or its parent. The order
138+// applies at once rather than on the parent commit.
139+func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, above bool) {
140+ me := s.surface
141+ p := me.sub.parent
142+ if p == nil || sibling == nil {
143+ return
144+ }
145+ other := me.server.surfaces[sibling.Resource]
146+ if other == me || other == nil || other != p && other.sub.parent != p {
147+ r.PostError(uint32(wayland.SubsurfaceErrorBadSurface), "not a sibling or the parent")
148+ return
149+ }
150+ list := p.sub.children
151+ for i, ch := range list {
152+ if ch == me {
153+ list = append(list[:i:i], list[i+1:]...)
154+ break
155+ }
156+ }
157+ if other == p {
158+ // Relative to the parent: just above it or just below it.
159+ me.sub.below = !above
160+ if above {
161+ list = append([]*surface{me}, list...)
162+ } else {
163+ list = append(list, me)
164+ }
165+ // Keep below children first so the flattening stays ordered.
166+ sortBelowFirst(list)
167+ } else {
168+ me.sub.below = other.sub.below
169+ at := len(list)
170+ for i, ch := range list {
171+ if ch == other {
172+ at = i
173+ if above {
174+ at++
175+ }
176+ break
177+ }
178+ }
179+ list = append(list[:at:at], append([]*surface{me}, list[at:]...)...)
180+ }
181+ p.sub.children = list
182+ me.redraw()
183+}
184+
185+// sortBelowFirst moves children below their parent ahead of those above,
186+// keeping each group's order.
187+func sortBelowFirst(list []*surface) {
188+ below := make([]*surface, 0, len(list))
189+ above := make([]*surface, 0, len(list))
190+ for _, ch := range list {
191+ if ch.sub.below {
192+ below = append(below, ch)
193+ } else {
194+ above = append(above, ch)
195+ }
196+ }
197+ copy(list, append(below, above...))
198+}
108 199
109 type region struct{}200 type region struct{}
110 201
@@ -268,7 +359,8 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
268 }359 }
269 if w := s.windows[s.pointerFocus]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() {360 if w := s.windows[s.pointerFocus]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() {
270 s.serial++361 s.serial++
271- p.SendEnter(s.serial, w.xdg.surfaceResource(), server.FixedFromFloat(s.pointerX), server.FixedFromFloat(s.pointerY))362+ x, y := w.surfacePoint(s.pointerX, s.pointerY)
363+ p.SendEnter(s.serial, w.xdg.surfaceResource(), server.FixedFromFloat(x), server.FixedFromFloat(y))
272 pointerFrame(p)364 pointerFrame(p)
273 }365 }
274 }366 }
M internal/adapters/wayland/proc_env_mock_test.go +10 -1
@@ -14,10 +14,19 @@ func newMockprocEnv(t interface {
1414 mock.TestingT
1515 Cleanup(func())
1616 }) *mockprocEnv {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
1721 mock := &mockprocEnv{}
1822 mock.Mock.Test(t)
1923
20- t.Cleanup(func() { mock.AssertExpectations(t) })
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
2130
2231 return mock
2332 }
@@ -14,10 +14,19 @@ func newMockprocEnv(t interface {
14 mock.TestingT14 mock.TestingT
15 Cleanup(func())15 Cleanup(func())
16 }) *mockprocEnv {16 }) *mockprocEnv {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
17 mock := &mockprocEnv{}21 mock := &mockprocEnv{}
18 mock.Mock.Test(t)22 mock.Mock.Test(t)
19 23
20- t.Cleanup(func() { mock.AssertExpectations(t) })24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
21 30
22 return mock31 return mock
23 }32 }
M internal/adapters/wayland/scale.go +10 -0
@@ -16,6 +16,8 @@ import (
1616 // outputOfSurface is the output a surface is on: its window's configured
1717 // output, its layer output, or the focused one.
1818 func (s *Server) outputOfSurface(surf *surface) *output {
19+ // Subsurfaces are on their root's output.
20+ surf = surf.root()
1921 name := ""
2022 switch {
2123 case surf.xdg != nil && surf.xdg.window != nil:
@@ -26,6 +28,14 @@ func (s *Server) outputOfSurface(surf *surface) *output {
2628 return s.outputByName(name)
2729 }
2830
31+// sendTreeScale sends the scale to a surface and its subsurfaces.
32+func (surf *surface) sendTreeScale() {
33+ surf.sendScale()
34+ for _, ch := range surf.sub.children {
35+ ch.sendTreeScale()
36+ }
37+}
38+
2939 // sendScale tells the surface about its output: enter/leave, the integer
3040 // buffer scale (wl_surface v6+) and the fractional scale.
3141 func (surf *surface) sendScale() {
@@ -16,6 +16,8 @@ import (
16 // outputOfSurface is the output a surface is on: its window's configured16 // outputOfSurface is the output a surface is on: its window's configured
17 // output, its layer output, or the focused one.17 // output, its layer output, or the focused one.
18 func (s *Server) outputOfSurface(surf *surface) *output {18 func (s *Server) outputOfSurface(surf *surface) *output {
19+ // Subsurfaces are on their root's output.
20+ surf = surf.root()
19 name := ""21 name := ""
20 switch {22 switch {
21 case surf.xdg != nil && surf.xdg.window != nil:23 case surf.xdg != nil && surf.xdg.window != nil:
@@ -26,6 +28,14 @@ func (s *Server) outputOfSurface(surf *surface) *output {
26 return s.outputByName(name)28 return s.outputByName(name)
27 }29 }
28 30
31+// sendTreeScale sends the scale to a surface and its subsurfaces.
32+func (surf *surface) sendTreeScale() {
33+ surf.sendScale()
34+ for _, ch := range surf.sub.children {
35+ ch.sendTreeScale()
36+ }
37+}
38+
29 // sendScale tells the surface about its output: enter/leave, the integer39 // sendScale tells the surface about its output: enter/leave, the integer
30 // buffer scale (wl_surface v6+) and the fractional scale.40 // buffer scale (wl_surface v6+) and the fractional scale.
31 func (surf *surface) sendScale() {41 func (surf *surface) sendScale() {
M internal/adapters/wayland/server.go +6 -4
@@ -345,9 +345,10 @@ func (s *Server) apply(cmd ports.ClientCommand) {
345345 case ports.PointerFocus:
346346 s.changePointerFocus(c.ID, c.X, c.Y)
347347 case ports.PointerMotionTo:
348- if c.ID == s.pointerFocus {
349- for _, p := range s.windowPointers(s.windows[c.ID]) {
350- p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
348+ if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus {
349+ x, y := w.surfacePoint(c.X, c.Y)
350+ for _, p := range s.windowPointers(w) {
351+ p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))
351352 pointerFrame(p)
352353 }
353354 }
@@ -409,7 +410,7 @@ func (s *Server) apply(cmd ports.ClientCommand) {
409410 w.last, w.hasLast = c, true
410411 w.sendConfigure()
411412 if surf := w.xdg.surface; surf != nil && c.Output != "" {
412- surf.sendScale()
413+ surf.sendTreeScale()
413414 }
414415 case ports.SetOutputs:
415416 s.setOutputs(c)
@@ -585,6 +586,7 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
585586 if w := s.windows[id]; w != nil {
586587 if surface := w.xdg.surfaceResource(); surface != nil && surface.Resource.Alive() {
587588 s.pointerFocus, s.pointerX, s.pointerY = id, x, y
589+ x, y := w.surfacePoint(x, y)
588590 for _, p := range s.windowPointers(w) {
589591 s.serial++
590592 p.SendEnter(s.serial, surface, server.FixedFromFloat(x), server.FixedFromFloat(y))
@@ -345,9 +345,10 @@ func (s *Server) apply(cmd ports.ClientCommand) {
345 case ports.PointerFocus:345 case ports.PointerFocus:
346 s.changePointerFocus(c.ID, c.X, c.Y)346 s.changePointerFocus(c.ID, c.X, c.Y)
347 case ports.PointerMotionTo:347 case ports.PointerMotionTo:
348- if c.ID == s.pointerFocus {348+ if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus {
349- for _, p := range s.windowPointers(s.windows[c.ID]) {349+ x, y := w.surfacePoint(c.X, c.Y)
350- p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))350+ for _, p := range s.windowPointers(w) {
351+ p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))
351 pointerFrame(p)352 pointerFrame(p)
352 }353 }
353 }354 }
@@ -409,7 +410,7 @@ func (s *Server) apply(cmd ports.ClientCommand) {
409 w.last, w.hasLast = c, true410 w.last, w.hasLast = c, true
410 w.sendConfigure()411 w.sendConfigure()
411 if surf := w.xdg.surface; surf != nil && c.Output != "" {412 if surf := w.xdg.surface; surf != nil && c.Output != "" {
412- surf.sendScale()413+ surf.sendTreeScale()
413 }414 }
414 case ports.SetOutputs:415 case ports.SetOutputs:
415 s.setOutputs(c)416 s.setOutputs(c)
@@ -585,6 +586,7 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
585 if w := s.windows[id]; w != nil {586 if w := s.windows[id]; w != nil {
586 if surface := w.xdg.surfaceResource(); surface != nil && surface.Resource.Alive() {587 if surface := w.xdg.surfaceResource(); surface != nil && surface.Resource.Alive() {
587 s.pointerFocus, s.pointerX, s.pointerY = id, x, y588 s.pointerFocus, s.pointerX, s.pointerY = id, x, y
589+ x, y := w.surfacePoint(x, y)
588 for _, p := range s.windowPointers(w) {590 for _, p := range s.windowPointers(w) {
589 s.serial++591 s.serial++
590 p.SendEnter(s.serial, surface, server.FixedFromFloat(x), server.FixedFromFloat(y))592 p.SendEnter(s.serial, surface, server.FixedFromFloat(x), server.FixedFromFloat(y))
A internal/adapters/wayland/subsurface_test.go +150 -0
@@ -0,0 +1,150 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/wayland"
9+ "github.com/bnema/purego-libwayland/protocol/xdgshell"
10+ "golang.org/x/sys/unix"
11+)
12+
13+// A toplevel with a subsurface (how Firefox draws with the GPU) sends one
14+// content: its own buffer, the child at its committed position and the
15+// window geometry.
16+func TestSubsurfaceContent(t *testing.T) {
17+ s, events, contents, dir := dmabufServer(t, ports.DMABufSupport{})
18+ c := protocolClient(t, s, dir)
19+ comp := bindProtocol(t, c, "wl_compositor")
20+ subc := bindProtocol(t, c, "wl_subcompositor")
21+ wm := bindProtocol(t, c, "xdg_wm_base")
22+ shm := bindProtocol(t, c, "wl_shm")
23+ registerProtocol(t, c, shm)
24+ fd, err := unix.MemfdCreate("subsurface", 0)
25+ if err != nil {
26+ t.Fatal(err)
27+ }
28+ defer unix.Close(fd)
29+ if err := unix.Ftruncate(fd, 4*4*4); err != nil {
30+ t.Fatal(err)
31+ }
32+ pool, big, small := c.AllocateID(), c.AllocateID(), c.AllocateID()
33+ registerProtocol(t, c, pool)
34+ registerProtocol(t, c, big)
35+ registerProtocol(t, c, small)
36+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(64)); err != nil {
37+ t.Fatal(err)
38+ }
39+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, big, int32(0), int32(4), int32(4), int32(16), uint32(0))
40+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, small, int32(0), int32(2), int32(2), int32(16), uint32(0))
41+
42+ root, child := c.AllocateID(), c.AllocateID()
43+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, root)
44+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
45+ registerProtocol(t, c, root)
46+ registerProtocol(t, c, child)
47+ sub := c.AllocateID()
48+ registerProtocol(t, c, sub)
49+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
50+ xdg := c.AllocateID()
51+ requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, root)
52+ serials := make(chan uint32, 4)
53+ xp := &configureProxy{serial: serials}
54+ xp.SetID(xdg)
55+ c.Context().Register(xp)
56+ top := c.AllocateID()
57+ registerProtocol(t, c, top)
58+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
59+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
60+ if err := c.Roundtrip(); err != nil {
61+ t.Fatal(err)
62+ }
63+ select {
64+ case serial := <-serials:
65+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
66+ case <-time.After(2 * time.Second):
67+ t.Fatal("configure timeout")
68+ }
69+ // Child first: nothing to draw until the root maps.
70+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(1), int32(2))
71+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, small, int32(0), int32(0))
72+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
73+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestSetWindowGeometry, int32(1), int32(1), int32(2), int32(2))
74+ requestProtocol(t, c, root, wayland.SurfaceRequestAttach, big, int32(0), int32(0))
75+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
76+ if err := c.Roundtrip(); err != nil {
77+ t.Fatal(err)
78+ }
79+ w := mapped(t, events, 2*time.Second)
80+ var got ports.SurfaceContent
81+ deadline := time.After(2 * time.Second)
82+ for got.ID == 0 {
83+ select {
84+ case got = <-contents:
85+ case <-deadline:
86+ t.Fatal("no content")
87+ }
88+ }
89+ if got.ID != w.ID || got.Width != 4 || got.Geometry != (ports.Rect{X: 1, Y: 1, W: 2, H: 2}) {
90+ t.Fatalf("root %+v", got)
91+ }
92+ if len(got.Children) != 1 || got.Children[0].X != 1 || got.Children[0].Y != 2 || got.Children[0].Width != 2 || got.Children[0].Below {
93+ t.Fatalf("children %+v", got.Children)
94+ }
95+ // Destroying the subsurface redraws the root without it.
96+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy)
97+ if err := c.Roundtrip(); err != nil {
98+ t.Fatal(err)
99+ }
100+ deadline = time.After(2 * time.Second)
101+ for {
102+ select {
103+ case got = <-contents:
104+ if len(got.Children) == 0 {
105+ return
106+ }
107+ case <-deadline:
108+ t.Fatalf("child kept: %+v", got.Children)
109+ }
110+ }
111+}
112+
113+// A surface cannot become a subsurface of its own child.
114+func TestSubsurfaceLoop(t *testing.T) {
115+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{})
116+ c := protocolClient(t, s, dir)
117+ comp := bindProtocol(t, c, "wl_compositor")
118+ subc := bindProtocol(t, c, "wl_subcompositor")
119+ a, b := c.AllocateID(), c.AllocateID()
120+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, a)
121+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, b)
122+ s1 := c.AllocateID()
123+ registerProtocol(t, c, s1)
124+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s1, b, a)
125+ s2 := c.AllocateID()
126+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s2, a, b)
127+ expectProtocolError(t, c, subc, uint32(wayland.SubcompositorErrorBadParent))
128+}
129+
130+// A wl_subsurface outlives its parent: the surface keeps its role, so a
131+// second get_subsurface is bad_surface.
132+func TestSubsurfaceRoleOutlivesParent(t *testing.T) {
133+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{})
134+ c := protocolClient(t, s, dir)
135+ comp := bindProtocol(t, c, "wl_compositor")
136+ subc := bindProtocol(t, c, "wl_subcompositor")
137+ a, b := c.AllocateID(), c.AllocateID()
138+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, a)
139+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, b)
140+ registerProtocol(t, c, a)
141+ s1 := c.AllocateID()
142+ registerProtocol(t, c, s1)
143+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s1, b, a)
144+ requestProtocol(t, c, a, wayland.SurfaceRequestDestroy)
145+ p := c.AllocateID()
146+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, p)
147+ s2 := c.AllocateID()
148+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s2, b, p)
149+ expectProtocolError(t, c, subc, uint32(wayland.SubcompositorErrorBadSurface))
150+}
@@ -0,0 +1,150 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/wayland"
9+ "github.com/bnema/purego-libwayland/protocol/xdgshell"
10+ "golang.org/x/sys/unix"
11+)
12+
13+// A toplevel with a subsurface (how Firefox draws with the GPU) sends one
14+// content: its own buffer, the child at its committed position and the
15+// window geometry.
16+func TestSubsurfaceContent(t *testing.T) {
17+ s, events, contents, dir := dmabufServer(t, ports.DMABufSupport{})
18+ c := protocolClient(t, s, dir)
19+ comp := bindProtocol(t, c, "wl_compositor")
20+ subc := bindProtocol(t, c, "wl_subcompositor")
21+ wm := bindProtocol(t, c, "xdg_wm_base")
22+ shm := bindProtocol(t, c, "wl_shm")
23+ registerProtocol(t, c, shm)
24+ fd, err := unix.MemfdCreate("subsurface", 0)
25+ if err != nil {
26+ t.Fatal(err)
27+ }
28+ defer unix.Close(fd)
29+ if err := unix.Ftruncate(fd, 4*4*4); err != nil {
30+ t.Fatal(err)
31+ }
32+ pool, big, small := c.AllocateID(), c.AllocateID(), c.AllocateID()
33+ registerProtocol(t, c, pool)
34+ registerProtocol(t, c, big)
35+ registerProtocol(t, c, small)
36+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(64)); err != nil {
37+ t.Fatal(err)
38+ }
39+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, big, int32(0), int32(4), int32(4), int32(16), uint32(0))
40+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, small, int32(0), int32(2), int32(2), int32(16), uint32(0))
41+
42+ root, child := c.AllocateID(), c.AllocateID()
43+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, root)
44+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
45+ registerProtocol(t, c, root)
46+ registerProtocol(t, c, child)
47+ sub := c.AllocateID()
48+ registerProtocol(t, c, sub)
49+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
50+ xdg := c.AllocateID()
51+ requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, root)
52+ serials := make(chan uint32, 4)
53+ xp := &configureProxy{serial: serials}
54+ xp.SetID(xdg)
55+ c.Context().Register(xp)
56+ top := c.AllocateID()
57+ registerProtocol(t, c, top)
58+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
59+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
60+ if err := c.Roundtrip(); err != nil {
61+ t.Fatal(err)
62+ }
63+ select {
64+ case serial := <-serials:
65+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
66+ case <-time.After(2 * time.Second):
67+ t.Fatal("configure timeout")
68+ }
69+ // Child first: nothing to draw until the root maps.
70+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(1), int32(2))
71+ requestProtocol(t, c, child, wayland.SurfaceRequestAttach, small, int32(0), int32(0))
72+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
73+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestSetWindowGeometry, int32(1), int32(1), int32(2), int32(2))
74+ requestProtocol(t, c, root, wayland.SurfaceRequestAttach, big, int32(0), int32(0))
75+ requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
76+ if err := c.Roundtrip(); err != nil {
77+ t.Fatal(err)
78+ }
79+ w := mapped(t, events, 2*time.Second)
80+ var got ports.SurfaceContent
81+ deadline := time.After(2 * time.Second)
82+ for got.ID == 0 {
83+ select {
84+ case got = <-contents:
85+ case <-deadline:
86+ t.Fatal("no content")
87+ }
88+ }
89+ if got.ID != w.ID || got.Width != 4 || got.Geometry != (ports.Rect{X: 1, Y: 1, W: 2, H: 2}) {
90+ t.Fatalf("root %+v", got)
91+ }
92+ if len(got.Children) != 1 || got.Children[0].X != 1 || got.Children[0].Y != 2 || got.Children[0].Width != 2 || got.Children[0].Below {
93+ t.Fatalf("children %+v", got.Children)
94+ }
95+ // Destroying the subsurface redraws the root without it.
96+ requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy)
97+ if err := c.Roundtrip(); err != nil {
98+ t.Fatal(err)
99+ }
100+ deadline = time.After(2 * time.Second)
101+ for {
102+ select {
103+ case got = <-contents:
104+ if len(got.Children) == 0 {
105+ return
106+ }
107+ case <-deadline:
108+ t.Fatalf("child kept: %+v", got.Children)
109+ }
110+ }
111+}
112+
113+// A surface cannot become a subsurface of its own child.
114+func TestSubsurfaceLoop(t *testing.T) {
115+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{})
116+ c := protocolClient(t, s, dir)
117+ comp := bindProtocol(t, c, "wl_compositor")
118+ subc := bindProtocol(t, c, "wl_subcompositor")
119+ a, b := c.AllocateID(), c.AllocateID()
120+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, a)
121+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, b)
122+ s1 := c.AllocateID()
123+ registerProtocol(t, c, s1)
124+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s1, b, a)
125+ s2 := c.AllocateID()
126+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s2, a, b)
127+ expectProtocolError(t, c, subc, uint32(wayland.SubcompositorErrorBadParent))
128+}
129+
130+// A wl_subsurface outlives its parent: the surface keeps its role, so a
131+// second get_subsurface is bad_surface.
132+func TestSubsurfaceRoleOutlivesParent(t *testing.T) {
133+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{})
134+ c := protocolClient(t, s, dir)
135+ comp := bindProtocol(t, c, "wl_compositor")
136+ subc := bindProtocol(t, c, "wl_subcompositor")
137+ a, b := c.AllocateID(), c.AllocateID()
138+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, a)
139+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, b)
140+ registerProtocol(t, c, a)
141+ s1 := c.AllocateID()
142+ registerProtocol(t, c, s1)
143+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s1, b, a)
144+ requestProtocol(t, c, a, wayland.SurfaceRequestDestroy)
145+ p := c.AllocateID()
146+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, p)
147+ s2 := c.AllocateID()
148+ requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, s2, b, p)
149+ expectProtocolError(t, c, subc, uint32(wayland.SubcompositorErrorBadSurface))
150+}
M internal/adapters/wayland/surface.go +125 -16
@@ -35,10 +35,109 @@ type surface struct {
3535 // on is the output the surface entered; scale is the last scale sent.
3636 on *output
3737 scale float64
38+ // content is the last content built from this surface's own buffer;
39+ // has is false while no buffer is attached.
40+ content ports.SurfaceContent
41+ has bool
42+ sub subState
43+}
44+
45+// subState is the subsurface tree: parent is set on subsurfaces, children
46+// are ordered bottom to top, below marks children under their parent.
47+type subState struct {
48+ // role is the live wl_subsurface, nil when the surface has none.
49+ role *wayland.Subsurface
50+ parent *surface
51+ children []*surface
52+ x, y int // committed position, from the parent origin
53+ pendX, pendY int
54+ moved bool // a position waits for the parent commit
55+ below bool
56+}
57+
58+// root is the top of the surface's subsurface tree.
59+func (s *surface) root() *surface {
60+ for s.sub.parent != nil {
61+ s = s.sub.parent
62+ }
63+ return s
64+}
65+
66+// windowID is the mapped window or layer the surface draws, or 0.
67+func (s *surface) windowID() ports.WindowID {
68+ if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped {
69+ return s.xdg.window.id
70+ }
71+ if s.layer != nil && s.layer.mapped {
72+ return s.layer.id
73+ }
74+ return 0
75+}
76+
77+// tree is the root's content with its subsurfaces flattened.
78+func (s *surface) tree(id ports.WindowID) ports.SurfaceContent {
79+ c := s.content
80+ c.ID = id
81+ c.Children = nil
82+ for _, ch := range s.sub.children {
83+ ch.appendTree(&c.Children, ch.sub.x, ch.sub.y, ch.sub.below)
84+ }
85+ if s.xdg != nil {
86+ c.Geometry = s.xdg.geometry
87+ }
88+ return c
89+}
90+
91+// appendTree flattens a subsurface at (x, y) from the root: its children
92+// below it, itself, then its children above.
93+func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) {
94+ for _, ch := range s.sub.children {
95+ if ch.sub.below {
96+ ch.appendTree(out, x+ch.sub.x, y+ch.sub.y, below)
97+ }
98+ }
99+ if s.has {
100+ *out = append(*out, ports.Subsurface{X: x, Y: y, Below: below, SurfaceContent: s.content})
101+ }
102+ for _, ch := range s.sub.children {
103+ if !ch.sub.below {
104+ ch.appendTree(out, x+ch.sub.x, y+ch.sub.y, below)
105+ }
106+ }
107+}
108+
109+// redraw sends the tree of a mapped root to the outputs.
110+func (s *surface) redraw() {
111+ r := s.root()
112+ if id := r.windowID(); id != 0 && s.server.channels.Contents != nil {
113+ s.server.emitContent(r.tree(id))
114+ }
115+}
116+
117+// detach removes a subsurface from its parent and redraws the parent.
118+func (s *surface) detach() {
119+ p := s.sub.parent
120+ if p == nil {
121+ return
122+ }
123+ for i, ch := range p.sub.children {
124+ if ch == s {
125+ p.sub.children = append(p.sub.children[:i:i], p.sub.children[i+1:]...)
126+ break
127+ }
128+ }
129+ s.sub.parent = nil
130+ p.redraw()
131+ s.sendScale()
38132 }
39133
40134 func (s *surface) Destroy(*wayland.Surface) {
41135 s.destroyed = true
136+ s.detach()
137+ for _, ch := range s.sub.children {
138+ ch.sub.parent = nil
139+ }
140+ s.sub.children = nil
42141 for _, cb := range s.callbacks {
43142 cb.Destroy()
44143 }
@@ -90,36 +189,46 @@ func (s *surface) Commit(*wayland.Surface) {
90189 if s.role != nil {
91190 s.role(s.current != nil)
92191 }
93- id := ports.WindowID(0)
94- if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped {
95- id = s.xdg.window.id
96- }
97- if s.layer != nil && s.layer.mapped {
98- id = s.layer.id
99- }
100- if fresh && id != 0 && s.server.channels.Contents != nil {
101- b := s.current
102- if state, ok := s.server.buffers[b.Resource]; ok {
103- if c, ok := state.content(id); ok {
192+ if fresh {
193+ if state, ok := s.server.buffers[s.current.Resource]; ok {
194+ if c, ok := state.content(0); ok {
104195 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)
105- if c.Width != s.lastW || c.Height != s.lastH {
196+ if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) {
106197 s.lastW, s.lastH = c.Width, c.Height
107- s.server.log.Info().Uint64("id", uint64(id)).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")
198+ s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")
108199 }
109- s.server.emitContent(c)
200+ s.content, s.has = c, true
110201 } else if shm, ok := state.(*buffer); ok {
111202 shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
112203 }
113204 // wl_shm pixels were copied: the client may reuse the buffer.
114205 // A dmabuf is read in place until the next buffer replaces it.
115206 if _, gpu := state.(*dmabufBuffer); !gpu {
116- if b.Resource.Alive() {
117- b.SendRelease()
207+ if s.current.Resource.Alive() {
208+ s.current.SendRelease()
118209 }
119210 s.released = true
120211 }
121212 }
122213 }
214+ if s.current == nil {
215+ s.content, s.has = ports.SurfaceContent{}, false
216+ }
217+ // Subsurface positions apply on the parent commit.
218+ moved := false
219+ for _, ch := range s.sub.children {
220+ if ch.sub.moved {
221+ ch.sub.x, ch.sub.y, ch.sub.moved = ch.sub.pendX, ch.sub.pendY, false
222+ moved = true
223+ }
224+ }
225+ geometry := false
226+ if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry {
227+ s.xdg.geometry, geometry = s.xdg.pendingGeometry, true
228+ }
229+ if fresh || moved || geometry || s.sub.parent != nil {
230+ s.redraw()
231+ }
123232 }
124233 func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {}
125234 func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
@@ -35,10 +35,109 @@ type surface struct {
35 // on is the output the surface entered; scale is the last scale sent.35 // on is the output the surface entered; scale is the last scale sent.
36 on *output36 on *output
37 scale float6437 scale float64
38+ // content is the last content built from this surface's own buffer;
39+ // has is false while no buffer is attached.
40+ content ports.SurfaceContent
41+ has bool
42+ sub subState
43+}
44+
45+// subState is the subsurface tree: parent is set on subsurfaces, children
46+// are ordered bottom to top, below marks children under their parent.
47+type subState struct {
48+ // role is the live wl_subsurface, nil when the surface has none.
49+ role *wayland.Subsurface
50+ parent *surface
51+ children []*surface
52+ x, y int // committed position, from the parent origin
53+ pendX, pendY int
54+ moved bool // a position waits for the parent commit
55+ below bool
56+}
57+
58+// root is the top of the surface's subsurface tree.
59+func (s *surface) root() *surface {
60+ for s.sub.parent != nil {
61+ s = s.sub.parent
62+ }
63+ return s
64+}
65+
66+// windowID is the mapped window or layer the surface draws, or 0.
67+func (s *surface) windowID() ports.WindowID {
68+ if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped {
69+ return s.xdg.window.id
70+ }
71+ if s.layer != nil && s.layer.mapped {
72+ return s.layer.id
73+ }
74+ return 0
75+}
76+
77+// tree is the root's content with its subsurfaces flattened.
78+func (s *surface) tree(id ports.WindowID) ports.SurfaceContent {
79+ c := s.content
80+ c.ID = id
81+ c.Children = nil
82+ for _, ch := range s.sub.children {
83+ ch.appendTree(&c.Children, ch.sub.x, ch.sub.y, ch.sub.below)
84+ }
85+ if s.xdg != nil {
86+ c.Geometry = s.xdg.geometry
87+ }
88+ return c
89+}
90+
91+// appendTree flattens a subsurface at (x, y) from the root: its children
92+// below it, itself, then its children above.
93+func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) {
94+ for _, ch := range s.sub.children {
95+ if ch.sub.below {
96+ ch.appendTree(out, x+ch.sub.x, y+ch.sub.y, below)
97+ }
98+ }
99+ if s.has {
100+ *out = append(*out, ports.Subsurface{X: x, Y: y, Below: below, SurfaceContent: s.content})
101+ }
102+ for _, ch := range s.sub.children {
103+ if !ch.sub.below {
104+ ch.appendTree(out, x+ch.sub.x, y+ch.sub.y, below)
105+ }
106+ }
107+}
108+
109+// redraw sends the tree of a mapped root to the outputs.
110+func (s *surface) redraw() {
111+ r := s.root()
112+ if id := r.windowID(); id != 0 && s.server.channels.Contents != nil {
113+ s.server.emitContent(r.tree(id))
114+ }
115+}
116+
117+// detach removes a subsurface from its parent and redraws the parent.
118+func (s *surface) detach() {
119+ p := s.sub.parent
120+ if p == nil {
121+ return
122+ }
123+ for i, ch := range p.sub.children {
124+ if ch == s {
125+ p.sub.children = append(p.sub.children[:i:i], p.sub.children[i+1:]...)
126+ break
127+ }
128+ }
129+ s.sub.parent = nil
130+ p.redraw()
131+ s.sendScale()
38 }132 }
39 133
40 func (s *surface) Destroy(*wayland.Surface) {134 func (s *surface) Destroy(*wayland.Surface) {
41 s.destroyed = true135 s.destroyed = true
136+ s.detach()
137+ for _, ch := range s.sub.children {
138+ ch.sub.parent = nil
139+ }
140+ s.sub.children = nil
42 for _, cb := range s.callbacks {141 for _, cb := range s.callbacks {
43 cb.Destroy()142 cb.Destroy()
44 }143 }
@@ -90,36 +189,46 @@ func (s *surface) Commit(*wayland.Surface) {
90 if s.role != nil {189 if s.role != nil {
91 s.role(s.current != nil)190 s.role(s.current != nil)
92 }191 }
93- id := ports.WindowID(0)192+ if fresh {
94- if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped {193+ if state, ok := s.server.buffers[s.current.Resource]; ok {
95- id = s.xdg.window.id194+ if c, ok := state.content(0); ok {
96- }
97- if s.layer != nil && s.layer.mapped {
98- id = s.layer.id
99- }
100- if fresh && id != 0 && s.server.channels.Contents != nil {
101- b := s.current
102- if state, ok := s.server.buffers[b.Resource]; ok {
103- if c, ok := state.content(id); ok {
104 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)195 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)
105- if c.Width != s.lastW || c.Height != s.lastH {196+ if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) {
106 s.lastW, s.lastH = c.Width, c.Height197 s.lastW, s.lastH = c.Width, c.Height
107- s.server.log.Info().Uint64("id", uint64(id)).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")198+ s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")
108 }199 }
109- s.server.emitContent(c)200+ s.content, s.has = c, true
110 } else if shm, ok := state.(*buffer); ok {201 } else if shm, ok := state.(*buffer); ok {
111 shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")202 shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
112 }203 }
113 // wl_shm pixels were copied: the client may reuse the buffer.204 // wl_shm pixels were copied: the client may reuse the buffer.
114 // A dmabuf is read in place until the next buffer replaces it.205 // A dmabuf is read in place until the next buffer replaces it.
115 if _, gpu := state.(*dmabufBuffer); !gpu {206 if _, gpu := state.(*dmabufBuffer); !gpu {
116- if b.Resource.Alive() {207+ if s.current.Resource.Alive() {
117- b.SendRelease()208+ s.current.SendRelease()
118 }209 }
119 s.released = true210 s.released = true
120 }211 }
121 }212 }
122 }213 }
214+ if s.current == nil {
215+ s.content, s.has = ports.SurfaceContent{}, false
216+ }
217+ // Subsurface positions apply on the parent commit.
218+ moved := false
219+ for _, ch := range s.sub.children {
220+ if ch.sub.moved {
221+ ch.sub.x, ch.sub.y, ch.sub.moved = ch.sub.pendX, ch.sub.pendY, false
222+ moved = true
223+ }
224+ }
225+ geometry := false
226+ if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry {
227+ s.xdg.geometry, geometry = s.xdg.pendingGeometry, true
228+ }
229+ if fresh || moved || geometry || s.sub.parent != nil {
230+ s.redraw()
231+ }
123 }232 }
124 func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {}233 func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {}
125 func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}234 func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
M internal/adapters/wayland/xdg.go +20 -3
@@ -64,6 +64,9 @@ type xdgSurface struct {
6464 window *window
6565 popup *xdgshell.Popup
6666 serials []uint32
67+ // geometry is the committed window geometry: the visible window inside
68+ // client shadows. Empty means the whole surface.
69+ geometry, pendingGeometry ports.Rect
6770 }
6871
6972 func (x *xdgSurface) Destroy(r *xdgshell.Surface) {
@@ -167,6 +170,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
167170 x.configured = false
168171 x.acked = false
169172 x.serials = nil
173+ x.geometry, x.pendingGeometry = ports.Rect{}, ports.Rect{}
170174 }
171175 }
172176 func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) {
@@ -181,13 +185,19 @@ func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surfac
181185 }
182186 }
183187
184-// SetWindowGeometry is logged only: placement uses the full buffer for now.
185-func (x *xdgSurface) SetWindowGeometry(_ *xdgshell.Surface, gx, gy, gw, gh int32) {
188+// SetWindowGeometry sets the visible window inside the surface; it
189+// applies on the next commit.
190+func (x *xdgSurface) SetWindowGeometry(r *xdgshell.Surface, gx, gy, gw, gh int32) {
191+ if gw <= 0 || gh <= 0 {
192+ r.PostError(uint32(xdgshell.SurfaceErrorInvalidSize), "invalid window geometry")
193+ return
194+ }
195+ x.pendingGeometry = ports.Rect{X: int(gx), Y: int(gy), W: int(gw), H: int(gh)}
186196 id := uint64(0)
187197 if x.window != nil {
188198 id = uint64(x.window.id)
189199 }
190- x.server.log.Info().Uint64("id", id).Int32("x", gx).Int32("y", gy).Int32("w", gw).Int32("h", gh).Msg("window geometry")
200+ x.server.log.Debug().Uint64("id", id).Int32("x", gx).Int32("y", gy).Int32("w", gw).Int32("h", gh).Msg("window geometry")
191201 }
192202 func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) {
193203 for i, issued := range x.serials {
@@ -239,6 +249,13 @@ func (popup) Destroy(*xdgshell.Popup) {}
239249 func (popup) Grab(*xdgshell.Popup, *wayland.Seat, uint32) {}
240250 func (popup) Reposition(*xdgshell.Popup, *xdgshell.Positioner, uint32) {}
241251
252+// surfacePoint turns window coordinates (core's, from the geometry origin)
253+// into surface coordinates.
254+func (w *window) surfacePoint(x, y float64) (float64, float64) {
255+ g := w.xdg.geometry
256+ return x + float64(g.X), y + float64(g.Y)
257+}
258+
242259 func (x *xdgSurface) surfaceResource() *wayland.Surface {
243260 for resource, state := range x.server.surfaces {
244261 if state == x.surface && resource.Alive() {
@@ -64,6 +64,9 @@ type xdgSurface struct {
64 window *window64 window *window
65 popup *xdgshell.Popup65 popup *xdgshell.Popup
66 serials []uint3266 serials []uint32
67+ // geometry is the committed window geometry: the visible window inside
68+ // client shadows. Empty means the whole surface.
69+ geometry, pendingGeometry ports.Rect
67 }70 }
68 71
69 func (x *xdgSurface) Destroy(r *xdgshell.Surface) {72 func (x *xdgSurface) Destroy(r *xdgshell.Surface) {
@@ -167,6 +170,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
167 x.configured = false170 x.configured = false
168 x.acked = false171 x.acked = false
169 x.serials = nil172 x.serials = nil
173+ x.geometry, x.pendingGeometry = ports.Rect{}, ports.Rect{}
170 }174 }
171 }175 }
172 func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) {176 func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) {
@@ -181,13 +185,19 @@ func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surfac
181 }185 }
182 }186 }
183 187
184-// SetWindowGeometry is logged only: placement uses the full buffer for now.188+// SetWindowGeometry sets the visible window inside the surface; it
185-func (x *xdgSurface) SetWindowGeometry(_ *xdgshell.Surface, gx, gy, gw, gh int32) {189+// applies on the next commit.
190+func (x *xdgSurface) SetWindowGeometry(r *xdgshell.Surface, gx, gy, gw, gh int32) {
191+ if gw <= 0 || gh <= 0 {
192+ r.PostError(uint32(xdgshell.SurfaceErrorInvalidSize), "invalid window geometry")
193+ return
194+ }
195+ x.pendingGeometry = ports.Rect{X: int(gx), Y: int(gy), W: int(gw), H: int(gh)}
186 id := uint64(0)196 id := uint64(0)
187 if x.window != nil {197 if x.window != nil {
188 id = uint64(x.window.id)198 id = uint64(x.window.id)
189 }199 }
190- x.server.log.Info().Uint64("id", id).Int32("x", gx).Int32("y", gy).Int32("w", gw).Int32("h", gh).Msg("window geometry")200+ x.server.log.Debug().Uint64("id", id).Int32("x", gx).Int32("y", gy).Int32("w", gw).Int32("h", gh).Msg("window geometry")
191 }201 }
192 func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) {202 func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) {
193 for i, issued := range x.serials {203 for i, issued := range x.serials {
@@ -239,6 +249,13 @@ func (popup) Destroy(*xdgshell.Popup) {}
239 func (popup) Grab(*xdgshell.Popup, *wayland.Seat, uint32) {}249 func (popup) Grab(*xdgshell.Popup, *wayland.Seat, uint32) {}
240 func (popup) Reposition(*xdgshell.Popup, *xdgshell.Positioner, uint32) {}250 func (popup) Reposition(*xdgshell.Popup, *xdgshell.Positioner, uint32) {}
241 251
252+// surfacePoint turns window coordinates (core's, from the geometry origin)
253+// into surface coordinates.
254+func (w *window) surfacePoint(x, y float64) (float64, float64) {
255+ g := w.xdg.geometry
256+ return x + float64(g.X), y + float64(g.Y)
257+}
258+
242 func (x *xdgSurface) surfaceResource() *wayland.Surface {259 func (x *xdgSurface) surfaceResource() *wayland.Surface {
243 for resource, state := range x.server.surfaces {260 for resource, state := range x.server.surfaces {
244 if state == x.surface && resource.Alive() {261 if state == x.surface && resource.Alive() {
M internal/mocks/ports/devices_mock.go +20 -2
@@ -17,10 +17,19 @@ func NewMockRenderer(t interface {
1717 mock.TestingT
1818 Cleanup(func())
1919 }) *MockRenderer {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
2024 mock := &MockRenderer{}
2125 mock.Mock.Test(t)
2226
23- t.Cleanup(func() { mock.AssertExpectations(t) })
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
2433
2534 return mock
2635 }
@@ -226,10 +235,19 @@ func NewMockSeat(t interface {
226235 mock.TestingT
227236 Cleanup(func())
228237 }) *MockSeat {
238+ if helper, ok := t.(interface{ Helper() }); ok {
239+ helper.Helper()
240+ }
241+
229242 mock := &MockSeat{}
230243 mock.Mock.Test(t)
231244
232- t.Cleanup(func() { mock.AssertExpectations(t) })
245+ t.Cleanup(func() {
246+ if helper, ok := t.(interface{ Helper() }); ok {
247+ helper.Helper()
248+ }
249+ mock.AssertExpectations(t)
250+ })
233251
234252 return mock
235253 }
@@ -17,10 +17,19 @@ func NewMockRenderer(t interface {
17 mock.TestingT17 mock.TestingT
18 Cleanup(func())18 Cleanup(func())
19 }) *MockRenderer {19 }) *MockRenderer {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
20 mock := &MockRenderer{}24 mock := &MockRenderer{}
21 mock.Mock.Test(t)25 mock.Mock.Test(t)
22 26
23- t.Cleanup(func() { mock.AssertExpectations(t) })27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
24 33
25 return mock34 return mock
26 }35 }
@@ -226,10 +235,19 @@ func NewMockSeat(t interface {
226 mock.TestingT235 mock.TestingT
227 Cleanup(func())236 Cleanup(func())
228 }) *MockSeat {237 }) *MockSeat {
238+ if helper, ok := t.(interface{ Helper() }); ok {
239+ helper.Helper()
240+ }
241+
229 mock := &MockSeat{}242 mock := &MockSeat{}
230 mock.Mock.Test(t)243 mock.Mock.Test(t)
231 244
232- t.Cleanup(func() { mock.AssertExpectations(t) })245+ t.Cleanup(func() {
246+ if helper, ok := t.(interface{ Helper() }); ok {
247+ helper.Helper()
248+ }
249+ mock.AssertExpectations(t)
250+ })
233 251
234 return mock252 return mock
235 }253 }
M internal/ports/messages.go +18 -1
@@ -346,6 +346,8 @@ type SceneLayer struct {
346346 // GPU buffer the renderer reads in place. Receivers never modify either.
347347 // Empty means the window has no content. LogicalW and LogicalH are the
348348 // surface size in logical pixels (buffer scale and viewport applied).
349+// Subsurfaces come in Children; Geometry is the part of the surface that
350+// is the window (xdg window geometry), the rest being client shadows.
349351 type SurfaceContent struct {
350352 ID WindowID
351353 Width, Height int
@@ -354,10 +356,25 @@ type SurfaceContent struct {
354356 Opaque bool // x formats: ignore the alpha byte
355357 Pixels []byte
356358 DMABuf *DMABuf
359+ // Children are the subsurfaces, bottom to top, flattened.
360+ Children []Subsurface
361+ // Geometry is in logical pixels from the surface origin; empty means
362+ // the whole surface.
363+ Geometry Rect
364+}
365+
366+// Subsurface is a child surface at X, Y logical pixels from the root
367+// surface origin. Below children are drawn under the root surface.
368+type Subsurface struct {
369+ X, Y int
370+ Below bool
371+ SurfaceContent
357372 }
358373
359374 // Empty reports whether the content has nothing to draw.
360-func (c SurfaceContent) Empty() bool { return c.Pixels == nil && c.DMABuf == nil }
375+func (c SurfaceContent) Empty() bool {
376+ return c.Pixels == nil && c.DMABuf == nil && len(c.Children) == 0
377+}
361378
362379 // DMABuf is a client GPU buffer (linux-dmabuf). Its files stay open while
363380 // the client's buffer exists; a renderer that keeps it must duplicate the
@@ -346,6 +346,8 @@ type SceneLayer struct {
346 // GPU buffer the renderer reads in place. Receivers never modify either.346 // GPU buffer the renderer reads in place. Receivers never modify either.
347 // Empty means the window has no content. LogicalW and LogicalH are the347 // Empty means the window has no content. LogicalW and LogicalH are the
348 // surface size in logical pixels (buffer scale and viewport applied).348 // surface size in logical pixels (buffer scale and viewport applied).
349+// Subsurfaces come in Children; Geometry is the part of the surface that
350+// is the window (xdg window geometry), the rest being client shadows.
349 type SurfaceContent struct {351 type SurfaceContent struct {
350 ID WindowID352 ID WindowID
351 Width, Height int353 Width, Height int
@@ -354,10 +356,25 @@ type SurfaceContent struct {
354 Opaque bool // x formats: ignore the alpha byte356 Opaque bool // x formats: ignore the alpha byte
355 Pixels []byte357 Pixels []byte
356 DMABuf *DMABuf358 DMABuf *DMABuf
359+ // Children are the subsurfaces, bottom to top, flattened.
360+ Children []Subsurface
361+ // Geometry is in logical pixels from the surface origin; empty means
362+ // the whole surface.
363+ Geometry Rect
364+}
365+
366+// Subsurface is a child surface at X, Y logical pixels from the root
367+// surface origin. Below children are drawn under the root surface.
368+type Subsurface struct {
369+ X, Y int
370+ Below bool
371+ SurfaceContent
357 }372 }
358 373
359 // Empty reports whether the content has nothing to draw.374 // Empty reports whether the content has nothing to draw.
360-func (c SurfaceContent) Empty() bool { return c.Pixels == nil && c.DMABuf == nil }375+func (c SurfaceContent) Empty() bool {
376+ return c.Pixels == nil && c.DMABuf == nil && len(c.Children) == 0
377+}
361 378
362 // DMABuf is a client GPU buffer (linux-dmabuf). Its files stay open while379 // DMABuf is a client GPU buffer (linux-dmabuf). Its files stay open while
363 // the client's buffer exists; a renderer that keeps it must duplicate the380 // the client's buffer exists; a renderer that keeps it must duplicate the

Keyboard shortcuts

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

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

Branches 23

Tags 10

↑↓ moveenter openesc close