detached 18b66fe

feat(wayland): draw subsurfaces, honour window geometry, offer KDE server decoration

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

Keyboard shortcuts

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

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

Branches 25

Tags 10

↑↓ moveenter openesc close