fix(wayland): enforce xdg-shell configure, role and destruction rules
- commit
- 78a0c3a97c85ac334dc2024e5f37afedd73cc9ef
- author
- brice · · 2026-09-23 12:28 UTC
- parent
- a7ac56a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +199 -10
M go.mod +1 -0
| @@ -5,6 +5,7 @@ go 1.27 | |||
| 5 | 5 | require ( | |
| 6 | 6 | github.com/BurntSushi/toml v1.5.0 | |
| 7 | 7 | github.com/bnema/purego-libwayland v0.0.0 | |
| 8 | + | github.com/bnema/wlturbo v0.2.0 | |
| 8 | 9 | github.com/bnema/zerowrap v1.4.1 | |
| 9 | 10 | github.com/rs/zerolog v1.35.1 | |
| 10 | 11 | golang.org/x/sys v0.48.0 | |
| @@ -5,6 +5,7 @@ go 1.27 | |||||
| 5 | require ( | 5 | require ( | ||
| 6 | github.com/BurntSushi/toml v1.5.0 | 6 | github.com/BurntSushi/toml v1.5.0 | ||
| 7 | github.com/bnema/purego-libwayland v0.0.0 | 7 | github.com/bnema/purego-libwayland v0.0.0 | ||
| 8 | + | github.com/bnema/wlturbo v0.2.0 | |||
| 8 | github.com/bnema/zerowrap v1.4.1 | 9 | github.com/bnema/zerowrap v1.4.1 | ||
| 9 | github.com/rs/zerolog v1.35.1 | 10 | github.com/rs/zerolog v1.35.1 | ||
| 10 | golang.org/x/sys v0.48.0 | 11 | golang.org/x/sys v0.48.0 | ||
M go.sum +2 -0 generated file: collapsed
M internal/adapters/wayland/globals.go +2 -1
| @@ -93,11 +93,12 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par | |||
| 93 | 93 | if state == nil { | |
| 94 | 94 | return | |
| 95 | 95 | } | |
| 96 | - | if state.role != nil { | |
| 96 | + | if state.kind != roleNone { | |
| 97 | 97 | r.PostError(uint32(wayland.SubcompositorErrorBadSurface), "surface already has role") | |
| 98 | 98 | return | |
| 99 | 99 | } | |
| 100 | 100 | if sub, err := wayland.NewSubsurface(r.Client(), 1, id, subsurface{state}); err == nil { | |
| 101 | + | state.kind = roleSubsurface | |
| 101 | 102 | state.role = func(bool) {} | |
| 102 | 103 | sub.OnDestroy = func() { state.role = nil } | |
| 103 | 104 | } | |
| @@ -93,11 +93,12 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par | |||||
| 93 | if state == nil { | 93 | if state == nil { | ||
| 94 | return | 94 | return | ||
| 95 | } | 95 | } | ||
| 96 | - | if state.role != nil { | 96 | + | if state.kind != roleNone { |
| 97 | r.PostError(uint32(wayland.SubcompositorErrorBadSurface), "surface already has role") | 97 | r.PostError(uint32(wayland.SubcompositorErrorBadSurface), "surface already has role") | ||
| 98 | return | 98 | return | ||
| 99 | } | 99 | } | ||
| 100 | if sub, err := wayland.NewSubsurface(r.Client(), 1, id, subsurface{state}); err == nil { | 100 | if sub, err := wayland.NewSubsurface(r.Client(), 1, id, subsurface{state}); err == nil { | ||
| 101 | + | state.kind = roleSubsurface | |||
| 101 | state.role = func(bool) {} | 102 | state.role = func(bool) {} | ||
| 102 | sub.OnDestroy = func() { state.role = nil } | 103 | sub.OnDestroy = func() { state.role = nil } | ||
| 103 | } | 104 | } | ||
A internal/adapters/wayland/protocol_test.go +140 -0
| @@ -0,0 +1,140 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "path/filepath" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 10 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 11 | + | "github.com/bnema/wlturbo" | |
| 12 | + | "golang.org/x/sys/unix" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | func protocolClient(t *testing.T, s *Server, dir string) *wlturbo.Display { | |
| 16 | + | t.Helper() | |
| 17 | + | c, err := wlturbo.Connect(filepath.Join(dir, s.SocketName())) | |
| 18 | + | if err != nil { | |
| 19 | + | t.Fatal(err) | |
| 20 | + | } | |
| 21 | + | t.Cleanup(func() { _ = c.Close() }) | |
| 22 | + | if err := c.Roundtrip(); err != nil { | |
| 23 | + | t.Fatal(err) | |
| 24 | + | } | |
| 25 | + | return c | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func bindProtocol(t *testing.T, c *wlturbo.Display, iface string) uint32 { | |
| 29 | + | t.Helper() | |
| 30 | + | g, ok := c.Registry().FindGlobal(iface) | |
| 31 | + | if !ok { | |
| 32 | + | t.Fatalf("missing %s", iface) | |
| 33 | + | } | |
| 34 | + | id, err := c.Registry().BindID(g.Name, g.Interface, 1) | |
| 35 | + | if err != nil { | |
| 36 | + | t.Fatal(err) | |
| 37 | + | } | |
| 38 | + | return id | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | func requestProtocol(t *testing.T, c *wlturbo.Display, id uint32, op uint32, args ...any) { | |
| 42 | + | t.Helper() | |
| 43 | + | if err := c.SendRequest(id, uint16(op), args...); err != nil { | |
| 44 | + | t.Fatal(err) | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func expectProtocolError(t *testing.T, c *wlturbo.Display, object, code uint32) { | |
| 49 | + | t.Helper() | |
| 50 | + | done := make(chan error, 1) | |
| 51 | + | go func() { done <- c.Roundtrip() }() | |
| 52 | + | select { | |
| 53 | + | case err := <-done: | |
| 54 | + | var displayErr *wlturbo.DisplayError | |
| 55 | + | if !errors.As(err, &displayErr) { | |
| 56 | + | t.Fatalf("expected display error, got %v", err) | |
| 57 | + | } | |
| 58 | + | if displayErr.ObjectID != object || displayErr.Code != code { | |
| 59 | + | t.Fatalf("protocol error = %v, want object=%d code=%d", displayErr, object, code) | |
| 60 | + | } | |
| 61 | + | case <-time.After(2 * time.Second): | |
| 62 | + | _ = c.Close() | |
| 63 | + | t.Fatal("protocol error timeout") | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | type protocolProxy struct{ wlturbo.BaseProxy } | |
| 68 | + | ||
| 69 | + | func (*protocolProxy) Dispatch(*wlturbo.Event) {} | |
| 70 | + | ||
| 71 | + | func registerProtocol(t *testing.T, c *wlturbo.Display, id uint32) { | |
| 72 | + | t.Helper() | |
| 73 | + | p := &protocolProxy{} | |
| 74 | + | p.SetID(id) | |
| 75 | + | c.Context().Register(p) | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func TestXDGProtocolErrors(t *testing.T) { | |
| 79 | + | for _, scenario := range []string{"bogus ack", "buffer before ack", "subsurface role", "defunct toplevel"} { | |
| 80 | + | t.Run(scenario, func(t *testing.T) { | |
| 81 | + | s, _, _, dir := lifecycleServer(t) | |
| 82 | + | c := protocolClient(t, s, dir) | |
| 83 | + | compositor := bindProtocol(t, c, "wl_compositor") | |
| 84 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |
| 85 | + | surface := c.AllocateID() | |
| 86 | + | requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, surface) | |
| 87 | + | registerProtocol(t, c, surface) | |
| 88 | + | if scenario == "subsurface role" { | |
| 89 | + | parent := c.AllocateID() | |
| 90 | + | requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, parent) | |
| 91 | + | registerProtocol(t, c, parent) | |
| 92 | + | subcomp := bindProtocol(t, c, "wl_subcompositor") | |
| 93 | + | requestProtocol(t, c, subcomp, wayland.SubcompositorRequestGetSubsurface, c.AllocateID(), surface, parent) | |
| 94 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, c.AllocateID(), surface) | |
| 95 | + | expectProtocolError(t, c, wm, uint32(xdgshell.WmBaseErrorRole)) | |
| 96 | + | return | |
| 97 | + | } | |
| 98 | + | xdg := c.AllocateID() | |
| 99 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surface) | |
| 100 | + | top := c.AllocateID() | |
| 101 | + | registerProtocol(t, c, xdg) | |
| 102 | + | registerProtocol(t, c, top) | |
| 103 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |
| 104 | + | switch scenario { | |
| 105 | + | case "bogus ack": | |
| 106 | + | requestProtocol(t, c, surface, wayland.SurfaceRequestCommit) | |
| 107 | + | if err := c.Roundtrip(); err != nil { | |
| 108 | + | t.Fatal(err) | |
| 109 | + | } | |
| 110 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, uint32(0)) | |
| 111 | + | expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorInvalidSerial)) | |
| 112 | + | case "buffer before ack": | |
| 113 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 114 | + | fd, err := unix.MemfdCreate("protocol-buffer", 0) | |
| 115 | + | if err != nil { | |
| 116 | + | t.Fatal(err) | |
| 117 | + | } | |
| 118 | + | defer unix.Close(fd) | |
| 119 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |
| 120 | + | t.Fatal(err) | |
| 121 | + | } | |
| 122 | + | pool := c.AllocateID() | |
| 123 | + | registerProtocol(t, c, shm) | |
| 124 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, uint32(0), int32(4)); err != nil { | |
| 125 | + | t.Fatal(err) | |
| 126 | + | } | |
| 127 | + | buffer := c.AllocateID() | |
| 128 | + | registerProtocol(t, c, pool) | |
| 129 | + | registerProtocol(t, c, buffer) | |
| 130 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |
| 131 | + | requestProtocol(t, c, surface, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |
| 132 | + | requestProtocol(t, c, surface, wayland.SurfaceRequestCommit) | |
| 133 | + | expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorUnconfiguredBuffer)) | |
| 134 | + | case "defunct toplevel": | |
| 135 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestDestroy) | |
| 136 | + | expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorDefunctRoleObject)) | |
| 137 | + | } | |
| 138 | + | }) | |
| 139 | + | } | |
| 140 | + | } | |
| @@ -0,0 +1,140 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "path/filepath" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |||
| 11 | + | "github.com/bnema/wlturbo" | |||
| 12 | + | "golang.org/x/sys/unix" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | func protocolClient(t *testing.T, s *Server, dir string) *wlturbo.Display { | |||
| 16 | + | t.Helper() | |||
| 17 | + | c, err := wlturbo.Connect(filepath.Join(dir, s.SocketName())) | |||
| 18 | + | if err != nil { | |||
| 19 | + | t.Fatal(err) | |||
| 20 | + | } | |||
| 21 | + | t.Cleanup(func() { _ = c.Close() }) | |||
| 22 | + | if err := c.Roundtrip(); err != nil { | |||
| 23 | + | t.Fatal(err) | |||
| 24 | + | } | |||
| 25 | + | return c | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | func bindProtocol(t *testing.T, c *wlturbo.Display, iface string) uint32 { | |||
| 29 | + | t.Helper() | |||
| 30 | + | g, ok := c.Registry().FindGlobal(iface) | |||
| 31 | + | if !ok { | |||
| 32 | + | t.Fatalf("missing %s", iface) | |||
| 33 | + | } | |||
| 34 | + | id, err := c.Registry().BindID(g.Name, g.Interface, 1) | |||
| 35 | + | if err != nil { | |||
| 36 | + | t.Fatal(err) | |||
| 37 | + | } | |||
| 38 | + | return id | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func requestProtocol(t *testing.T, c *wlturbo.Display, id uint32, op uint32, args ...any) { | |||
| 42 | + | t.Helper() | |||
| 43 | + | if err := c.SendRequest(id, uint16(op), args...); err != nil { | |||
| 44 | + | t.Fatal(err) | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func expectProtocolError(t *testing.T, c *wlturbo.Display, object, code uint32) { | |||
| 49 | + | t.Helper() | |||
| 50 | + | done := make(chan error, 1) | |||
| 51 | + | go func() { done <- c.Roundtrip() }() | |||
| 52 | + | select { | |||
| 53 | + | case err := <-done: | |||
| 54 | + | var displayErr *wlturbo.DisplayError | |||
| 55 | + | if !errors.As(err, &displayErr) { | |||
| 56 | + | t.Fatalf("expected display error, got %v", err) | |||
| 57 | + | } | |||
| 58 | + | if displayErr.ObjectID != object || displayErr.Code != code { | |||
| 59 | + | t.Fatalf("protocol error = %v, want object=%d code=%d", displayErr, object, code) | |||
| 60 | + | } | |||
| 61 | + | case <-time.After(2 * time.Second): | |||
| 62 | + | _ = c.Close() | |||
| 63 | + | t.Fatal("protocol error timeout") | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | type protocolProxy struct{ wlturbo.BaseProxy } | |||
| 68 | + | ||||
| 69 | + | func (*protocolProxy) Dispatch(*wlturbo.Event) {} | |||
| 70 | + | ||||
| 71 | + | func registerProtocol(t *testing.T, c *wlturbo.Display, id uint32) { | |||
| 72 | + | t.Helper() | |||
| 73 | + | p := &protocolProxy{} | |||
| 74 | + | p.SetID(id) | |||
| 75 | + | c.Context().Register(p) | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func TestXDGProtocolErrors(t *testing.T) { | |||
| 79 | + | for _, scenario := range []string{"bogus ack", "buffer before ack", "subsurface role", "defunct toplevel"} { | |||
| 80 | + | t.Run(scenario, func(t *testing.T) { | |||
| 81 | + | s, _, _, dir := lifecycleServer(t) | |||
| 82 | + | c := protocolClient(t, s, dir) | |||
| 83 | + | compositor := bindProtocol(t, c, "wl_compositor") | |||
| 84 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |||
| 85 | + | surface := c.AllocateID() | |||
| 86 | + | requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, surface) | |||
| 87 | + | registerProtocol(t, c, surface) | |||
| 88 | + | if scenario == "subsurface role" { | |||
| 89 | + | parent := c.AllocateID() | |||
| 90 | + | requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, parent) | |||
| 91 | + | registerProtocol(t, c, parent) | |||
| 92 | + | subcomp := bindProtocol(t, c, "wl_subcompositor") | |||
| 93 | + | requestProtocol(t, c, subcomp, wayland.SubcompositorRequestGetSubsurface, c.AllocateID(), surface, parent) | |||
| 94 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, c.AllocateID(), surface) | |||
| 95 | + | expectProtocolError(t, c, wm, uint32(xdgshell.WmBaseErrorRole)) | |||
| 96 | + | return | |||
| 97 | + | } | |||
| 98 | + | xdg := c.AllocateID() | |||
| 99 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surface) | |||
| 100 | + | top := c.AllocateID() | |||
| 101 | + | registerProtocol(t, c, xdg) | |||
| 102 | + | registerProtocol(t, c, top) | |||
| 103 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |||
| 104 | + | switch scenario { | |||
| 105 | + | case "bogus ack": | |||
| 106 | + | requestProtocol(t, c, surface, wayland.SurfaceRequestCommit) | |||
| 107 | + | if err := c.Roundtrip(); err != nil { | |||
| 108 | + | t.Fatal(err) | |||
| 109 | + | } | |||
| 110 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, uint32(0)) | |||
| 111 | + | expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorInvalidSerial)) | |||
| 112 | + | case "buffer before ack": | |||
| 113 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 114 | + | fd, err := unix.MemfdCreate("protocol-buffer", 0) | |||
| 115 | + | if err != nil { | |||
| 116 | + | t.Fatal(err) | |||
| 117 | + | } | |||
| 118 | + | defer unix.Close(fd) | |||
| 119 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |||
| 120 | + | t.Fatal(err) | |||
| 121 | + | } | |||
| 122 | + | pool := c.AllocateID() | |||
| 123 | + | registerProtocol(t, c, shm) | |||
| 124 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, uint32(0), int32(4)); err != nil { | |||
| 125 | + | t.Fatal(err) | |||
| 126 | + | } | |||
| 127 | + | buffer := c.AllocateID() | |||
| 128 | + | registerProtocol(t, c, pool) | |||
| 129 | + | registerProtocol(t, c, buffer) | |||
| 130 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |||
| 131 | + | requestProtocol(t, c, surface, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |||
| 132 | + | requestProtocol(t, c, surface, wayland.SurfaceRequestCommit) | |||
| 133 | + | expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorUnconfiguredBuffer)) | |||
| 134 | + | case "defunct toplevel": | |||
| 135 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestDestroy) | |||
| 136 | + | expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorDefunctRoleObject)) | |||
| 137 | + | } | |||
| 138 | + | }) | |||
| 139 | + | } | |||
| 140 | + | } | |||
M internal/adapters/wayland/server.go +1 -0
| @@ -209,6 +209,7 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 209 | 209 | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states) | |
| 210 | 210 | s.serial++ | |
| 211 | 211 | w.xdg.resource.SendConfigure(s.serial) | |
| 212 | + | w.xdg.serials = append(w.xdg.serials, s.serial) | |
| 212 | 213 | case ports.CloseWindow: | |
| 213 | 214 | w := s.windows[c.ID] | |
| 214 | 215 | if w == nil || !w.toplevel.Resource.Alive() { | |
| @@ -209,6 +209,7 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 209 | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states) | 209 | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states) | ||
| 210 | s.serial++ | 210 | s.serial++ | ||
| 211 | w.xdg.resource.SendConfigure(s.serial) | 211 | w.xdg.resource.SendConfigure(s.serial) | ||
| 212 | + | w.xdg.serials = append(w.xdg.serials, s.serial) | |||
| 212 | case ports.CloseWindow: | 213 | case ports.CloseWindow: | ||
| 213 | w := s.windows[c.ID] | 214 | w := s.windows[c.ID] | ||
| 214 | if w == nil || !w.toplevel.Resource.Alive() { | 215 | if w == nil || !w.toplevel.Resource.Alive() { | ||
M internal/adapters/wayland/surface.go +22 -1
| @@ -1,8 +1,21 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | - | import "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 5 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | type roleKind uint8 | |
| 9 | + | ||
| 10 | + | const ( | |
| 11 | + | roleNone roleKind = iota | |
| 12 | + | roleXDG | |
| 13 | + | roleSubsurface | |
| 14 | + | ) | |
| 4 | 15 | ||
| 5 | 16 | type surface struct { | |
| 17 | + | kind roleKind | |
| 18 | + | xdg *xdgSurface | |
| 6 | 19 | server *Server | |
| 7 | 20 | current, pending *wayland.Buffer | |
| 8 | 21 | attached bool | |
| @@ -13,7 +26,11 @@ type surface struct { | |||
| 13 | 26 | ||
| 14 | 27 | func (s *surface) Destroy(*wayland.Surface) { | |
| 15 | 28 | s.destroyed = true | |
| 29 | + | for _, cb := range s.callbacks { | |
| 30 | + | cb.Destroy() | |
| 31 | + | } | |
| 16 | 32 | s.callbacks = nil | |
| 33 | + | s.current, s.pending = nil, nil | |
| 17 | 34 | if s.role != nil { | |
| 18 | 35 | s.role(false) | |
| 19 | 36 | } | |
| @@ -29,6 +46,10 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) { | |||
| 29 | 46 | } | |
| 30 | 47 | } | |
| 31 | 48 | func (s *surface) Commit(*wayland.Surface) { | |
| 49 | + | if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked { | |
| 50 | + | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | |
| 51 | + | return | |
| 52 | + | } | |
| 32 | 53 | if s.attached { | |
| 33 | 54 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | |
| 34 | 55 | s.current.SendRelease() | |
| @@ -1,8 +1,21 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | - | import "github.com/bnema/purego-libwayland/protocol/wayland" | 3 | + | import ( |
| 4 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 5 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | type roleKind uint8 | |||
| 9 | + | ||||
| 10 | + | const ( | |||
| 11 | + | roleNone roleKind = iota | |||
| 12 | + | roleXDG | |||
| 13 | + | roleSubsurface | |||
| 14 | + | ) | |||
| 4 | 15 | ||||
| 5 | type surface struct { | 16 | type surface struct { | ||
| 17 | + | kind roleKind | |||
| 18 | + | xdg *xdgSurface | |||
| 6 | server *Server | 19 | server *Server | ||
| 7 | current, pending *wayland.Buffer | 20 | current, pending *wayland.Buffer | ||
| 8 | attached bool | 21 | attached bool | ||
| @@ -13,7 +26,11 @@ type surface struct { | |||||
| 13 | 26 | ||||
| 14 | func (s *surface) Destroy(*wayland.Surface) { | 27 | func (s *surface) Destroy(*wayland.Surface) { | ||
| 15 | s.destroyed = true | 28 | s.destroyed = true | ||
| 29 | + | for _, cb := range s.callbacks { | |||
| 30 | + | cb.Destroy() | |||
| 31 | + | } | |||
| 16 | s.callbacks = nil | 32 | s.callbacks = nil | ||
| 33 | + | s.current, s.pending = nil, nil | |||
| 17 | if s.role != nil { | 34 | if s.role != nil { | ||
| 18 | s.role(false) | 35 | s.role(false) | ||
| 19 | } | 36 | } | ||
| @@ -29,6 +46,10 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) { | |||||
| 29 | } | 46 | } | ||
| 30 | } | 47 | } | ||
| 31 | func (s *surface) Commit(*wayland.Surface) { | 48 | func (s *surface) Commit(*wayland.Surface) { | ||
| 49 | + | if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked { | |||
| 50 | + | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 32 | if s.attached { | 53 | if s.attached { | ||
| 33 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | 54 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | ||
| 34 | s.current.SendRelease() | 55 | s.current.SendRelease() | ||
M internal/adapters/wayland/xdg.go +31 -8
| @@ -26,14 +26,17 @@ func (m wm) GetXdgSurface(r *xdgshell.WmBase, id uint32, w *wayland.Surface) { | |||
| 26 | 26 | if state == nil { | |
| 27 | 27 | return | |
| 28 | 28 | } | |
| 29 | - | if state.role != nil { | |
| 30 | - | r.PostError(0, "surface already has role") | |
| 29 | + | if state.kind != roleNone && state.kind != roleXDG || state.xdg != nil { | |
| 30 | + | r.PostError(uint32(xdgshell.WmBaseErrorRole), "surface already has role") | |
| 31 | 31 | return | |
| 32 | 32 | } | |
| 33 | 33 | x := &xdgSurface{server: m.server, surface: state} | |
| 34 | 34 | resource, err := xdgshell.NewSurface(r.Client(), r.Version(), id, x) | |
| 35 | 35 | if err == nil { | |
| 36 | 36 | x.resource = resource | |
| 37 | + | state.kind = roleXDG | |
| 38 | + | state.xdg = x | |
| 39 | + | resource.OnDestroy = func() { state.xdg = nil; state.role = nil } | |
| 37 | 40 | } | |
| 38 | 41 | ||
| 39 | 42 | } | |
| @@ -58,10 +61,14 @@ type xdgSurface struct { | |||
| 58 | 61 | configured bool | |
| 59 | 62 | acked bool | |
| 60 | 63 | window *window | |
| 61 | - | serial uint32 | |
| 64 | + | serials []uint32 | |
| 62 | 65 | } | |
| 63 | 66 | ||
| 64 | - | func (x *xdgSurface) Destroy(*xdgshell.Surface) {} | |
| 67 | + | func (x *xdgSurface) Destroy(r *xdgshell.Surface) { | |
| 68 | + | if x.window != nil { | |
| 69 | + | r.PostError(uint32(xdgshell.SurfaceErrorDefunctRoleObject), "toplevel still exists") | |
| 70 | + | } | |
| 71 | + | } | |
| 65 | 72 | ||
| 66 | 73 | type window struct { | |
| 67 | 74 | id ports.WindowID | |
| @@ -78,6 +85,7 @@ func (w *window) unmap() { | |||
| 78 | 85 | w.mapped = false | |
| 79 | 86 | w.xdg.configured = false | |
| 80 | 87 | w.xdg.acked = false | |
| 88 | + | w.xdg.serials = nil | |
| 81 | 89 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | |
| 82 | 90 | } | |
| 83 | 91 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |
| @@ -104,6 +112,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||
| 104 | 112 | x.server.serial++ | |
| 105 | 113 | t.SendConfigure(0, 0, nil) | |
| 106 | 114 | r.SendConfigure(x.server.serial) | |
| 115 | + | x.serials = append(x.serials, x.server.serial) | |
| 107 | 116 | } else if buffer && !w.mapped && x.acked { | |
| 108 | 117 | w.mapped = true | |
| 109 | 118 | x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID}) | |
| @@ -112,7 +121,15 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||
| 112 | 121 | w.unmap() | |
| 113 | 122 | } | |
| 114 | 123 | } | |
| 115 | - | t.OnDestroy = func() { w.unmap(); delete(x.server.windows, w.id); x.window = nil; x.surface.role = nil } | |
| 124 | + | t.OnDestroy = func() { | |
| 125 | + | w.unmap() | |
| 126 | + | delete(x.server.windows, w.id) | |
| 127 | + | x.window = nil | |
| 128 | + | x.surface.role = nil | |
| 129 | + | x.configured = false | |
| 130 | + | x.acked = false | |
| 131 | + | x.serials = nil | |
| 132 | + | } | |
| 116 | 133 | } | |
| 117 | 134 | func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) { | |
| 118 | 135 | if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil { | |
| @@ -120,9 +137,15 @@ func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surfac | |||
| 120 | 137 | } | |
| 121 | 138 | } | |
| 122 | 139 | func (x *xdgSurface) SetWindowGeometry(*xdgshell.Surface, int32, int32, int32, int32) {} | |
| 123 | - | func (x *xdgSurface) AckConfigure(_ *xdgshell.Surface, serial uint32) { | |
| 124 | - | x.serial = serial | |
| 125 | - | x.acked = true | |
| 140 | + | func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) { | |
| 141 | + | for i, issued := range x.serials { | |
| 142 | + | if issued == serial { | |
| 143 | + | x.serials = x.serials[i+1:] | |
| 144 | + | x.acked = true | |
| 145 | + | return | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | r.PostError(uint32(xdgshell.SurfaceErrorInvalidSerial), "unknown configure serial") | |
| 126 | 149 | } | |
| 127 | 150 | ||
| 128 | 151 | type top struct{ w *window } | |
| @@ -26,14 +26,17 @@ func (m wm) GetXdgSurface(r *xdgshell.WmBase, id uint32, w *wayland.Surface) { | |||||
| 26 | if state == nil { | 26 | if state == nil { | ||
| 27 | return | 27 | return | ||
| 28 | } | 28 | } | ||
| 29 | - | if state.role != nil { | 29 | + | if state.kind != roleNone && state.kind != roleXDG || state.xdg != nil { |
| 30 | - | r.PostError(0, "surface already has role") | 30 | + | r.PostError(uint32(xdgshell.WmBaseErrorRole), "surface already has role") |
| 31 | return | 31 | return | ||
| 32 | } | 32 | } | ||
| 33 | x := &xdgSurface{server: m.server, surface: state} | 33 | x := &xdgSurface{server: m.server, surface: state} | ||
| 34 | resource, err := xdgshell.NewSurface(r.Client(), r.Version(), id, x) | 34 | resource, err := xdgshell.NewSurface(r.Client(), r.Version(), id, x) | ||
| 35 | if err == nil { | 35 | if err == nil { | ||
| 36 | x.resource = resource | 36 | x.resource = resource | ||
| 37 | + | state.kind = roleXDG | |||
| 38 | + | state.xdg = x | |||
| 39 | + | resource.OnDestroy = func() { state.xdg = nil; state.role = nil } | |||
| 37 | } | 40 | } | ||
| 38 | 41 | ||||
| 39 | } | 42 | } | ||
| @@ -58,10 +61,14 @@ type xdgSurface struct { | |||||
| 58 | configured bool | 61 | configured bool | ||
| 59 | acked bool | 62 | acked bool | ||
| 60 | window *window | 63 | window *window | ||
| 61 | - | serial uint32 | 64 | + | serials []uint32 |
| 62 | } | 65 | } | ||
| 63 | 66 | ||||
| 64 | - | func (x *xdgSurface) Destroy(*xdgshell.Surface) {} | 67 | + | func (x *xdgSurface) Destroy(r *xdgshell.Surface) { |
| 68 | + | if x.window != nil { | |||
| 69 | + | r.PostError(uint32(xdgshell.SurfaceErrorDefunctRoleObject), "toplevel still exists") | |||
| 70 | + | } | |||
| 71 | + | } | |||
| 65 | 72 | ||||
| 66 | type window struct { | 73 | type window struct { | ||
| 67 | id ports.WindowID | 74 | id ports.WindowID | ||
| @@ -78,6 +85,7 @@ func (w *window) unmap() { | |||||
| 78 | w.mapped = false | 85 | w.mapped = false | ||
| 79 | w.xdg.configured = false | 86 | w.xdg.configured = false | ||
| 80 | w.xdg.acked = false | 87 | w.xdg.acked = false | ||
| 88 | + | w.xdg.serials = nil | |||
| 81 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | 89 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | ||
| 82 | } | 90 | } | ||
| 83 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | 91 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | ||
| @@ -104,6 +112,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||||
| 104 | x.server.serial++ | 112 | x.server.serial++ | ||
| 105 | t.SendConfigure(0, 0, nil) | 113 | t.SendConfigure(0, 0, nil) | ||
| 106 | r.SendConfigure(x.server.serial) | 114 | r.SendConfigure(x.server.serial) | ||
| 115 | + | x.serials = append(x.serials, x.server.serial) | |||
| 107 | } else if buffer && !w.mapped && x.acked { | 116 | } else if buffer && !w.mapped && x.acked { | ||
| 108 | w.mapped = true | 117 | w.mapped = true | ||
| 109 | x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID}) | 118 | x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID}) | ||
| @@ -112,7 +121,15 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |||||
| 112 | w.unmap() | 121 | w.unmap() | ||
| 113 | } | 122 | } | ||
| 114 | } | 123 | } | ||
| 115 | - | t.OnDestroy = func() { w.unmap(); delete(x.server.windows, w.id); x.window = nil; x.surface.role = nil } | 124 | + | t.OnDestroy = func() { |
| 125 | + | w.unmap() | |||
| 126 | + | delete(x.server.windows, w.id) | |||
| 127 | + | x.window = nil | |||
| 128 | + | x.surface.role = nil | |||
| 129 | + | x.configured = false | |||
| 130 | + | x.acked = false | |||
| 131 | + | x.serials = nil | |||
| 132 | + | } | |||
| 116 | } | 133 | } | ||
| 117 | func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) { | 134 | func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) { | ||
| 118 | if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil { | 135 | if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil { | ||
| @@ -120,9 +137,15 @@ func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surfac | |||||
| 120 | } | 137 | } | ||
| 121 | } | 138 | } | ||
| 122 | func (x *xdgSurface) SetWindowGeometry(*xdgshell.Surface, int32, int32, int32, int32) {} | 139 | func (x *xdgSurface) SetWindowGeometry(*xdgshell.Surface, int32, int32, int32, int32) {} | ||
| 123 | - | func (x *xdgSurface) AckConfigure(_ *xdgshell.Surface, serial uint32) { | 140 | + | func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) { |
| 124 | - | x.serial = serial | 141 | + | for i, issued := range x.serials { |
| 125 | - | x.acked = true | 142 | + | if issued == serial { |
| 143 | + | x.serials = x.serials[i+1:] | |||
| 144 | + | x.acked = true | |||
| 145 | + | return | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | r.PostError(uint32(xdgshell.SurfaceErrorInvalidSerial), "unknown configure serial") | |||
| 126 | } | 149 | } | ||
| 127 | 150 | ||||
| 128 | type top struct{ w *window } | 151 | type top struct{ w *window } | ||