merge: browsers (subsurfaces, window geometry, KDE decoration, mockery v3.8.0)
15 files · +604 -66
M .github/workflows/ci.yml +1 -1
| @@ -9,7 +9,7 @@ jobs: | |||
| 9 | 9 | with: | |
| 10 | 10 | go-version: '1.27' | |
| 11 | 11 | - 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 | |
| 13 | 13 | - run: make check | |
| 14 | 14 | - run: make race | |
| 15 | 15 | - 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@latest | 11 | - 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 |
| 13 | - run: make check | 13 | - run: make check | ||
| 14 | - run: make race | 14 | - run: make race | ||
| 15 | - run: make mocks-check | 15 | - run: make mocks-check | ||
M internal/adapters/drm/cursor_plane_mock_test.go +10 -1
| @@ -14,10 +14,19 @@ func newMockcursorPlane(t interface { | |||
| 14 | 14 | mock.TestingT | |
| 15 | 15 | Cleanup(func()) | |
| 16 | 16 | }) *mockcursorPlane { | |
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | + | helper.Helper() | |
| 19 | + | } | |
| 20 | + | ||
| 17 | 21 | mock := &mockcursorPlane{} | |
| 18 | 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 | 31 | return mock | |
| 23 | 32 | } | |
| @@ -14,10 +14,19 @@ func newMockcursorPlane(t interface { | |||||
| 14 | mock.TestingT | 14 | 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 mock | 31 | return mock | ||
| 23 | } | 32 | } | ||
M internal/adapters/statefile/proc_tree_mock_test.go +10 -1
| @@ -14,10 +14,19 @@ func newMockprocTree(t interface { | |||
| 14 | 14 | mock.TestingT | |
| 15 | 15 | Cleanup(func()) | |
| 16 | 16 | }) *mockprocTree { | |
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | + | helper.Helper() | |
| 19 | + | } | |
| 20 | + | ||
| 17 | 21 | mock := &mockprocTree{} | |
| 18 | 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 | 31 | return mock | |
| 23 | 32 | } | |
| @@ -14,10 +14,19 @@ func newMockprocTree(t interface { | |||||
| 14 | mock.TestingT | 14 | 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 mock | 31 | 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 | |||
| 390 | 390 | uploads = append(uploads, upload{rect: rect, offset: used, pixels: content.Pixels, stride: content.Stride, opaque: content.Opaque, dst: dst, src: src}) | |
| 391 | 391 | 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. | |
| 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) { | |
| 395 | 396 | lw, lh := content.LogicalW, content.LogicalH | |
| 396 | 397 | if lw <= 0 || lh <= 0 { | |
| 397 | 398 | 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 | 401 | return | |
| 401 | 402 | } | |
| 402 | 403 | if content.DMABuf == nil && (content.Stride < content.Width*4 || len(content.Pixels) < (content.Height-1)*content.Stride+content.Width*4) { | |
| 403 | 404 | return | |
| 404 | 405 | } | |
| 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) | |
| 412 | 407 | // A buffer drawn at the physical size (fractional-scale clients round | |
| 413 | 408 | // w*scale, we round per edge) is copied 1:1, never resampled for 1px. | |
| 414 | 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()) { | |
| 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) | |
| 422 | 431 | } | |
| 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) | |
| 426 | 437 | } | |
| 427 | 438 | } | |
| 428 | - | addContent(dst, src, content) | |
| 429 | 439 | } | |
| 430 | 440 | fullscreen := false | |
| 431 | 441 | 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() * 4 | 391 | 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.LogicalH | 396 | lw, lh := content.LogicalW, content.LogicalH | ||
| 396 | if lw <= 0 || lh <= 0 { | 397 | if lw <= 0 || lh <= 0 { | ||
| 397 | lw, lh = content.Width, content.Height | 398 | 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 | return | 401 | 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 | return | 404 | 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 round | 407 | // 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.X | 415 | + | return |
| 421 | - | dst.Max.X = clip.Max.X | 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) | |||
| 422 | } | 431 | } | ||
| 423 | - | if dst.Max.Y > clip.Max.Y { | 432 | + | } |
| 424 | - | src.Max.Y -= dst.Max.Y - clip.Max.Y | 433 | + | drawSurface(content, ox, oy, clip) |
| 425 | - | dst.Max.Y = clip.Max.Y | 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) | |||
| 426 | } | 437 | } | ||
| 427 | } | 438 | } | ||
| 428 | - | addContent(dst, src, content) | |||
| 429 | } | 439 | } | ||
| 430 | fullscreen := false | 440 | 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) { | |||
| 247 | 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 | + | } | |
| @@ -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 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "github.com/bnema/purego-libwayland/protocol/kdedecoration" | |
| 5 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 4 | 6 | "github.com/bnema/purego-libwayland/protocol/xdgdecoration" | |
| 5 | 7 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 6 | 8 | "github.com/bnema/purego-libwayland/server" | |
| @@ -11,9 +13,37 @@ import ( | |||
| 11 | 13 | type decorationManager struct{ s *Server } | |
| 12 | 14 | ||
| 13 | 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 | 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 | 49 | func (decorationManager) Destroy(*xdgdecoration.ZxdgDecorationManagerV1) {} | |
| @@ -1,6 +1,8 @@ | |||||
| 1 | package wayland | 1 | 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 | |||
| 82 | 82 | if w == nil || parent == nil { | |
| 83 | 83 | return | |
| 84 | 84 | } | |
| 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 { | |
| 87 | 87 | return | |
| 88 | 88 | } | |
| 89 | - | if state.kind != roleNone { | |
| 89 | + | if state.kind != roleNone && state.kind != roleSubsurface || state.sub.role != nil { | |
| 90 | 90 | r.PostError(uint32(wayland.SubcompositorErrorBadSurface), "surface already has role") | |
| 91 | 91 | return | |
| 92 | 92 | } | |
| 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() | |
| 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 | 122 | type subsurface struct{ surface *surface } | |
| 101 | 123 | ||
| 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 | + | } | |
| 108 | 199 | ||
| 109 | 200 | type region struct{} | |
| 110 | 201 | ||
| @@ -268,7 +359,8 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) { | |||
| 268 | 359 | } | |
| 269 | 360 | if w := s.windows[s.pointerFocus]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() { | |
| 270 | 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 | 364 | pointerFrame(p) | |
| 273 | 365 | } | |
| 274 | 366 | } | |
| @@ -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 | return | 83 | 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 | return | 87 | 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 | return | 91 | 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 = roleSubsurface | 94 | + | 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 { | |||
| 14 | 14 | mock.TestingT | |
| 15 | 15 | Cleanup(func()) | |
| 16 | 16 | }) *mockprocEnv { | |
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | + | helper.Helper() | |
| 19 | + | } | |
| 20 | + | ||
| 17 | 21 | mock := &mockprocEnv{} | |
| 18 | 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 | 31 | return mock | |
| 23 | 32 | } | |
| @@ -14,10 +14,19 @@ func newMockprocEnv(t interface { | |||||
| 14 | mock.TestingT | 14 | 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 mock | 31 | return mock | ||
| 23 | } | 32 | } | ||
M internal/adapters/wayland/scale.go +10 -0
| @@ -16,6 +16,8 @@ import ( | |||
| 16 | 16 | // outputOfSurface is the output a surface is on: its window's configured | |
| 17 | 17 | // output, its layer output, or the focused one. | |
| 18 | 18 | func (s *Server) outputOfSurface(surf *surface) *output { | |
| 19 | + | // Subsurfaces are on their root's output. | |
| 20 | + | surf = surf.root() | |
| 19 | 21 | name := "" | |
| 20 | 22 | switch { | |
| 21 | 23 | case surf.xdg != nil && surf.xdg.window != nil: | |
| @@ -26,6 +28,14 @@ func (s *Server) outputOfSurface(surf *surface) *output { | |||
| 26 | 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 | 39 | // sendScale tells the surface about its output: enter/leave, the integer | |
| 30 | 40 | // buffer scale (wl_surface v6+) and the fractional scale. | |
| 31 | 41 | func (surf *surface) sendScale() { | |
| @@ -16,6 +16,8 @@ import ( | |||||
| 16 | // outputOfSurface is the output a surface is on: its window's configured | 16 | // 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 integer | 39 | // 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) { | |||
| 345 | 345 | case ports.PointerFocus: | |
| 346 | 346 | s.changePointerFocus(c.ID, c.X, c.Y) | |
| 347 | 347 | 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)) | |
| 351 | 352 | pointerFrame(p) | |
| 352 | 353 | } | |
| 353 | 354 | } | |
| @@ -409,7 +410,7 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 409 | 410 | w.last, w.hasLast = c, true | |
| 410 | 411 | w.sendConfigure() | |
| 411 | 412 | if surf := w.xdg.surface; surf != nil && c.Output != "" { | |
| 412 | - | surf.sendScale() | |
| 413 | + | surf.sendTreeScale() | |
| 413 | 414 | } | |
| 414 | 415 | case ports.SetOutputs: | |
| 415 | 416 | s.setOutputs(c) | |
| @@ -585,6 +586,7 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |||
| 585 | 586 | if w := s.windows[id]; w != nil { | |
| 586 | 587 | if surface := w.xdg.surfaceResource(); surface != nil && surface.Resource.Alive() { | |
| 587 | 588 | s.pointerFocus, s.pointerX, s.pointerY = id, x, y | |
| 589 | + | x, y := w.surfacePoint(x, y) | |
| 588 | 590 | for _, p := range s.windowPointers(w) { | |
| 589 | 591 | s.serial++ | |
| 590 | 592 | 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, true | 410 | 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, y | 588 | 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 { | |||
| 35 | 35 | // on is the output the surface entered; scale is the last scale sent. | |
| 36 | 36 | on *output | |
| 37 | 37 | 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 | 134 | func (s *surface) Destroy(*wayland.Surface) { | |
| 41 | 135 | 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 | 141 | for _, cb := range s.callbacks { | |
| 43 | 142 | cb.Destroy() | |
| 44 | 143 | } | |
| @@ -90,36 +189,46 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 90 | 189 | if s.role != nil { | |
| 91 | 190 | s.role(s.current != nil) | |
| 92 | 191 | } | |
| 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 { | |
| 104 | 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 | 197 | 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 | 201 | } else if shm, ok := state.(*buffer); ok { | |
| 111 | 202 | shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated") | |
| 112 | 203 | } | |
| 113 | 204 | // wl_shm pixels were copied: the client may reuse the buffer. | |
| 114 | 205 | // A dmabuf is read in place until the next buffer replaces it. | |
| 115 | 206 | if _, gpu := state.(*dmabufBuffer); !gpu { | |
| 116 | - | if b.Resource.Alive() { | |
| 117 | - | b.SendRelease() | |
| 207 | + | if s.current.Resource.Alive() { | |
| 208 | + | s.current.SendRelease() | |
| 118 | 209 | } | |
| 119 | 210 | 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 | 233 | func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {} | |
| 125 | 234 | 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 *output | 36 | on *output | ||
| 37 | scale float64 | 37 | 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 = true | 135 | 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.id | 194 | + | 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.Height | 197 | 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 = true | 210 | 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 { | |||
| 64 | 64 | window *window | |
| 65 | 65 | popup *xdgshell.Popup | |
| 66 | 66 | 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 | 72 | func (x *xdgSurface) Destroy(r *xdgshell.Surface) { | |
| @@ -167,6 +170,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||
| 167 | 170 | x.configured = false | |
| 168 | 171 | x.acked = false | |
| 169 | 172 | x.serials = nil | |
| 173 | + | x.geometry, x.pendingGeometry = ports.Rect{}, ports.Rect{} | |
| 170 | 174 | } | |
| 171 | 175 | } | |
| 172 | 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. | |
| 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)} | |
| 186 | 196 | id := uint64(0) | |
| 187 | 197 | if x.window != nil { | |
| 188 | 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 | 202 | func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) { | |
| 193 | 203 | for i, issued := range x.serials { | |
| @@ -239,6 +249,13 @@ func (popup) Destroy(*xdgshell.Popup) {} | |||
| 239 | 249 | func (popup) Grab(*xdgshell.Popup, *wayland.Seat, uint32) {} | |
| 240 | 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 | 259 | func (x *xdgSurface) surfaceResource() *wayland.Surface { | |
| 243 | 260 | for resource, state := range x.server.surfaces { | |
| 244 | 261 | if state == x.surface && resource.Alive() { | |
| @@ -64,6 +64,9 @@ type xdgSurface struct { | |||||
| 64 | window *window | 64 | window *window | ||
| 65 | popup *xdgshell.Popup | 65 | popup *xdgshell.Popup | ||
| 66 | serials []uint32 | 66 | 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 = false | 170 | x.configured = false | ||
| 168 | x.acked = false | 171 | x.acked = false | ||
| 169 | x.serials = nil | 172 | 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 { | |||
| 17 | 17 | mock.TestingT | |
| 18 | 18 | Cleanup(func()) | |
| 19 | 19 | }) *MockRenderer { | |
| 20 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 21 | + | helper.Helper() | |
| 22 | + | } | |
| 23 | + | ||
| 20 | 24 | mock := &MockRenderer{} | |
| 21 | 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 | 34 | return mock | |
| 26 | 35 | } | |
| @@ -226,10 +235,19 @@ func NewMockSeat(t interface { | |||
| 226 | 235 | mock.TestingT | |
| 227 | 236 | Cleanup(func()) | |
| 228 | 237 | }) *MockSeat { | |
| 238 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 239 | + | helper.Helper() | |
| 240 | + | } | |
| 241 | + | ||
| 229 | 242 | mock := &MockSeat{} | |
| 230 | 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 | 252 | return mock | |
| 235 | 253 | } | |
| @@ -17,10 +17,19 @@ func NewMockRenderer(t interface { | |||||
| 17 | mock.TestingT | 17 | 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 mock | 34 | return mock | ||
| 26 | } | 35 | } | ||
| @@ -226,10 +235,19 @@ func NewMockSeat(t interface { | |||||
| 226 | mock.TestingT | 235 | 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 mock | 252 | return mock | ||
| 235 | } | 253 | } | ||
M internal/ports/messages.go +18 -1
| @@ -346,6 +346,8 @@ type SceneLayer struct { | |||
| 346 | 346 | // GPU buffer the renderer reads in place. Receivers never modify either. | |
| 347 | 347 | // Empty means the window has no content. LogicalW and LogicalH are the | |
| 348 | 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 | 351 | type SurfaceContent struct { | |
| 350 | 352 | ID WindowID | |
| 351 | 353 | Width, Height int | |
| @@ -354,10 +356,25 @@ type SurfaceContent struct { | |||
| 354 | 356 | Opaque bool // x formats: ignore the alpha byte | |
| 355 | 357 | Pixels []byte | |
| 356 | 358 | 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 | 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 | 379 | // DMABuf is a client GPU buffer (linux-dmabuf). Its files stay open while | |
| 363 | 380 | // 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 the | 347 | // 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 WindowID | 352 | ID WindowID | ||
| 351 | Width, Height int | 353 | Width, Height int | ||
| @@ -354,10 +356,25 @@ type SurfaceContent struct { | |||||
| 354 | Opaque bool // x formats: ignore the alpha byte | 356 | Opaque bool // x formats: ignore the alpha byte | ||
| 355 | Pixels []byte | 357 | Pixels []byte | ||
| 356 | DMABuf *DMABuf | 358 | 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 while | 379 | // 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 the | 380 | // the client's buffer exists; a renderer that keeps it must duplicate the | ||