merge: layer-shell
13 files · +822 -5
M internal/adapters/headless/output_test.go +34 -0
| @@ -141,3 +141,37 @@ func TestWritePNGAtomic(t *testing.T) { | |||
| 141 | 141 | t.Fatalf("unexpected files: %v", entries) | |
| 142 | 142 | } | |
| 143 | 143 | } | |
| 144 | + | ||
| 145 | + | func TestRunPassesLayerContent(t *testing.T) { | |
| 146 | + | scenes := make(chan ports.Scene, 1) | |
| 147 | + | contents := make(chan ports.SurfaceContent, 1) | |
| 148 | + | layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}} | |
| 149 | + | scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}} | |
| 150 | + | contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}} | |
| 151 | + | r := new(fakeRenderer) | |
| 152 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 153 | + | done := make(chan error, 1) | |
| 154 | + | go func() { | |
| 155 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, contents) | |
| 156 | + | }() | |
| 157 | + | deadline := time.After(3 * time.Second) | |
| 158 | + | for { | |
| 159 | + | r.mu.Lock() | |
| 160 | + | passed := len(r.contents) > 0 && len(r.frames[0].Layers) == 1 && r.contents[0][42].Pixels != nil | |
| 161 | + | r.mu.Unlock() | |
| 162 | + | if passed { | |
| 163 | + | break | |
| 164 | + | } | |
| 165 | + | select { | |
| 166 | + | case <-deadline: | |
| 167 | + | cancel() | |
| 168 | + | <-done | |
| 169 | + | t.Fatal("layer content not passed to renderer") | |
| 170 | + | case <-time.After(10 * time.Millisecond): | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | cancel() | |
| 174 | + | if err := <-done; err != nil { | |
| 175 | + | t.Fatal(err) | |
| 176 | + | } | |
| 177 | + | } | |
| @@ -141,3 +141,37 @@ func TestWritePNGAtomic(t *testing.T) { | |||||
| 141 | t.Fatalf("unexpected files: %v", entries) | 141 | t.Fatalf("unexpected files: %v", entries) | ||
| 142 | } | 142 | } | ||
| 143 | } | 143 | } | ||
| 144 | + | ||||
| 145 | + | func TestRunPassesLayerContent(t *testing.T) { | |||
| 146 | + | scenes := make(chan ports.Scene, 1) | |||
| 147 | + | contents := make(chan ports.SurfaceContent, 1) | |||
| 148 | + | layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}} | |||
| 149 | + | scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}} | |||
| 150 | + | contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}} | |||
| 151 | + | r := new(fakeRenderer) | |||
| 152 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 153 | + | done := make(chan error, 1) | |||
| 154 | + | go func() { | |||
| 155 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, contents) | |||
| 156 | + | }() | |||
| 157 | + | deadline := time.After(3 * time.Second) | |||
| 158 | + | for { | |||
| 159 | + | r.mu.Lock() | |||
| 160 | + | passed := len(r.contents) > 0 && len(r.frames[0].Layers) == 1 && r.contents[0][42].Pixels != nil | |||
| 161 | + | r.mu.Unlock() | |||
| 162 | + | if passed { | |||
| 163 | + | break | |||
| 164 | + | } | |||
| 165 | + | select { | |||
| 166 | + | case <-deadline: | |||
| 167 | + | cancel() | |||
| 168 | + | <-done | |||
| 169 | + | t.Fatal("layer content not passed to renderer") | |||
| 170 | + | case <-time.After(10 * time.Millisecond): | |||
| 171 | + | } | |||
| 172 | + | } | |||
| 173 | + | cancel() | |||
| 174 | + | if err := <-done; err != nil { | |||
| 175 | + | t.Fatal(err) | |||
| 176 | + | } | |||
| 177 | + | } | |||
M internal/adapters/vulkan/renderer.go +22 -0
| @@ -336,6 +336,27 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 336 | 336 | uploads = append(uploads, u) | |
| 337 | 337 | used += n | |
| 338 | 338 | } | |
| 339 | + | fullscreen := false | |
| 340 | + | for _, w := range s.Windows { | |
| 341 | + | fullscreen = fullscreen || (!w.Hidden && w.Fullscreen) | |
| 342 | + | } | |
| 343 | + | addLayers := func(afterWindows bool) { | |
| 344 | + | for _, layer := range s.Layers { | |
| 345 | + | if (layer.Layer >= ports.LayerTop) != afterWindows || | |
| 346 | + | (fullscreen && (layer.Layer == ports.LayerBottom || layer.Layer == ports.LayerTop)) || | |
| 347 | + | layer.Rect.W <= 0 || layer.Rect.H <= 0 { | |
| 348 | + | continue | |
| 349 | + | } | |
| 350 | + | content := contents[layer.ID] | |
| 351 | + | width, height := min(content.Width, layer.Rect.W), min(content.Height, layer.Rect.H) | |
| 352 | + | if content.Pixels == nil || width <= 0 || height <= 0 || content.Stride < width*4 || len(content.Pixels) < (height-1)*content.Stride+width*4 { | |
| 353 | + | continue | |
| 354 | + | } | |
| 355 | + | x, y := layer.Rect.X, layer.Rect.Y | |
| 356 | + | add(image.Rect(x, y, x+width, y+height), [3]uint8{}, &content, image.Pt(x, y)) | |
| 357 | + | } | |
| 358 | + | } | |
| 359 | + | addLayers(false) | |
| 339 | 360 | for _, w := range s.Windows { | |
| 340 | 361 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |
| 341 | 362 | continue | |
| @@ -358,6 +379,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 358 | 379 | } | |
| 359 | 380 | } | |
| 360 | 381 | } | |
| 382 | + | addLayers(true) | |
| 361 | 383 | if err := r.ensureStaging(used); err != nil { | |
| 362 | 384 | return err | |
| 363 | 385 | } | |
| @@ -336,6 +336,27 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 336 | uploads = append(uploads, u) | 336 | uploads = append(uploads, u) | ||
| 337 | used += n | 337 | used += n | ||
| 338 | } | 338 | } | ||
| 339 | + | fullscreen := false | |||
| 340 | + | for _, w := range s.Windows { | |||
| 341 | + | fullscreen = fullscreen || (!w.Hidden && w.Fullscreen) | |||
| 342 | + | } | |||
| 343 | + | addLayers := func(afterWindows bool) { | |||
| 344 | + | for _, layer := range s.Layers { | |||
| 345 | + | if (layer.Layer >= ports.LayerTop) != afterWindows || | |||
| 346 | + | (fullscreen && (layer.Layer == ports.LayerBottom || layer.Layer == ports.LayerTop)) || | |||
| 347 | + | layer.Rect.W <= 0 || layer.Rect.H <= 0 { | |||
| 348 | + | continue | |||
| 349 | + | } | |||
| 350 | + | content := contents[layer.ID] | |||
| 351 | + | width, height := min(content.Width, layer.Rect.W), min(content.Height, layer.Rect.H) | |||
| 352 | + | if content.Pixels == nil || width <= 0 || height <= 0 || content.Stride < width*4 || len(content.Pixels) < (height-1)*content.Stride+width*4 { | |||
| 353 | + | continue | |||
| 354 | + | } | |||
| 355 | + | x, y := layer.Rect.X, layer.Rect.Y | |||
| 356 | + | add(image.Rect(x, y, x+width, y+height), [3]uint8{}, &content, image.Pt(x, y)) | |||
| 357 | + | } | |||
| 358 | + | } | |||
| 359 | + | addLayers(false) | |||
| 339 | for _, w := range s.Windows { | 360 | for _, w := range s.Windows { | ||
| 340 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | 361 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | ||
| 341 | continue | 362 | continue | ||
| @@ -358,6 +379,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 358 | } | 379 | } | ||
| 359 | } | 380 | } | ||
| 360 | } | 381 | } | ||
| 382 | + | addLayers(true) | |||
| 361 | if err := r.ensureStaging(used); err != nil { | 383 | if err := r.ensureStaging(used); err != nil { | ||
| 362 | return err | 384 | return err | ||
| 363 | } | 385 | } | ||
M internal/adapters/vulkan/renderer_test.go +38 -0
| @@ -145,3 +145,41 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||
| 145 | 145 | } | |
| 146 | 146 | } | |
| 147 | 147 | } | |
| 148 | + | ||
| 149 | + | func TestRendererLayers(t *testing.T) { | |
| 150 | + | r, err := New(64, 48) | |
| 151 | + | if err != nil { | |
| 152 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 153 | + | } | |
| 154 | + | defer r.Close() | |
| 155 | + | red := make([]byte, 64*8*4) | |
| 156 | + | for i := 0; i < len(red); i += 4 { | |
| 157 | + | copy(red[i:i+4], []byte{0, 0, 255, 0}) | |
| 158 | + | } | |
| 159 | + | contents := map[ports.WindowID]ports.SurfaceContent{2: {ID: 2, Width: 64, Height: 8, Stride: 64 * 4, Opaque: true, Pixels: red}} | |
| 160 | + | scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerTop, Rect: ports.Rect{W: 64, H: 8}}}} | |
| 161 | + | wc := windowColor(1) | |
| 162 | + | window := color.RGBA{wc[0], wc[1], wc[2], 255} | |
| 163 | + | check := func(want color.RGBA) { | |
| 164 | + | t.Helper() | |
| 165 | + | if err := r.Render(scene, contents); err != nil { | |
| 166 | + | t.Fatal(err) | |
| 167 | + | } | |
| 168 | + | if got := r.Pixels().RGBAAt(5, 2); got != want { | |
| 169 | + | t.Errorf("pixel = %v, want %v", got, want) | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | check(color.RGBA{255, 0, 0, 255}) | |
| 173 | + | scene.Layers[0].Layer = ports.LayerBottom | |
| 174 | + | check(window) | |
| 175 | + | scene.Windows[0].Fullscreen = true | |
| 176 | + | scene.Layers[0].Layer = ports.LayerTop | |
| 177 | + | check(window) | |
| 178 | + | scene.Layers[0].Layer = ports.LayerOverlay | |
| 179 | + | check(color.RGBA{255, 0, 0, 255}) | |
| 180 | + | scene.Windows[0].Hidden = true | |
| 181 | + | scene.Layers[0].Layer = ports.LayerBottom | |
| 182 | + | check(color.RGBA{255, 0, 0, 255}) | |
| 183 | + | delete(contents, 2) | |
| 184 | + | check(color.RGBA{16, 32, 48, 255}) | |
| 185 | + | } | |
| @@ -145,3 +145,41 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||||
| 145 | } | 145 | } | ||
| 146 | } | 146 | } | ||
| 147 | } | 147 | } | ||
| 148 | + | ||||
| 149 | + | func TestRendererLayers(t *testing.T) { | |||
| 150 | + | r, err := New(64, 48) | |||
| 151 | + | if err != nil { | |||
| 152 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 153 | + | } | |||
| 154 | + | defer r.Close() | |||
| 155 | + | red := make([]byte, 64*8*4) | |||
| 156 | + | for i := 0; i < len(red); i += 4 { | |||
| 157 | + | copy(red[i:i+4], []byte{0, 0, 255, 0}) | |||
| 158 | + | } | |||
| 159 | + | contents := map[ports.WindowID]ports.SurfaceContent{2: {ID: 2, Width: 64, Height: 8, Stride: 64 * 4, Opaque: true, Pixels: red}} | |||
| 160 | + | scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerTop, Rect: ports.Rect{W: 64, H: 8}}}} | |||
| 161 | + | wc := windowColor(1) | |||
| 162 | + | window := color.RGBA{wc[0], wc[1], wc[2], 255} | |||
| 163 | + | check := func(want color.RGBA) { | |||
| 164 | + | t.Helper() | |||
| 165 | + | if err := r.Render(scene, contents); err != nil { | |||
| 166 | + | t.Fatal(err) | |||
| 167 | + | } | |||
| 168 | + | if got := r.Pixels().RGBAAt(5, 2); got != want { | |||
| 169 | + | t.Errorf("pixel = %v, want %v", got, want) | |||
| 170 | + | } | |||
| 171 | + | } | |||
| 172 | + | check(color.RGBA{255, 0, 0, 255}) | |||
| 173 | + | scene.Layers[0].Layer = ports.LayerBottom | |||
| 174 | + | check(window) | |||
| 175 | + | scene.Windows[0].Fullscreen = true | |||
| 176 | + | scene.Layers[0].Layer = ports.LayerTop | |||
| 177 | + | check(window) | |||
| 178 | + | scene.Layers[0].Layer = ports.LayerOverlay | |||
| 179 | + | check(color.RGBA{255, 0, 0, 255}) | |||
| 180 | + | scene.Windows[0].Hidden = true | |||
| 181 | + | scene.Layers[0].Layer = ports.LayerBottom | |||
| 182 | + | check(color.RGBA{255, 0, 0, 255}) | |||
| 183 | + | delete(contents, 2) | |||
| 184 | + | check(color.RGBA{16, 32, 48, 255}) | |||
| 185 | + | } | |||
M internal/adapters/wayland/globals.go +2 -0
| @@ -11,6 +11,8 @@ import ( | |||
| 11 | 11 | func registerGlobals(d *server.Display, o Options, s *Server) error { | |
| 12 | 12 | for _, register := range []func() error{ | |
| 13 | 13 | func() error { return registerXDG(d, s) }, | |
| 14 | + | func() error { return registerLayer(d, o, s) }, | |
| 15 | + | func() error { return registerXDGOutput(d, o) }, | |
| 14 | 16 | func() error { | |
| 15 | 17 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | |
| 16 | 18 | }, | |
| @@ -11,6 +11,8 @@ import ( | |||||
| 11 | func registerGlobals(d *server.Display, o Options, s *Server) error { | 11 | func registerGlobals(d *server.Display, o Options, s *Server) error { | ||
| 12 | for _, register := range []func() error{ | 12 | for _, register := range []func() error{ | ||
| 13 | func() error { return registerXDG(d, s) }, | 13 | func() error { return registerXDG(d, s) }, | ||
| 14 | + | func() error { return registerLayer(d, o, s) }, | |||
| 15 | + | func() error { return registerXDGOutput(d, o) }, | |||
| 14 | func() error { | 16 | func() error { | ||
| 15 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | 17 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | ||
| 16 | }, | 18 | }, | ||
A internal/adapters/wayland/layer.go +195 -0
| @@ -0,0 +1,195 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |
| 5 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 6 | + | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |
| 7 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 8 | + | "github.com/bnema/purego-libwayland/server" | |
| 9 | + | "sort" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | type layerShell struct { | |
| 13 | + | server *Server | |
| 14 | + | width, height uint32 | |
| 15 | + | } | |
| 16 | + | type layerState struct { | |
| 17 | + | width, height, anchor uint32 | |
| 18 | + | zone int32 | |
| 19 | + | margin [4]int32 | |
| 20 | + | keyboard uint32 | |
| 21 | + | layer ports.Layer | |
| 22 | + | } | |
| 23 | + | type layerSurface struct { | |
| 24 | + | resource *wlrlayershell.ZwlrLayerSurfaceV1 | |
| 25 | + | shell layerShell | |
| 26 | + | surface *surface | |
| 27 | + | id ports.WindowID | |
| 28 | + | namespace string | |
| 29 | + | pending, current layerState | |
| 30 | + | configured, acked, mapped bool | |
| 31 | + | serials []uint32 | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func registerLayer(d *server.Display, o Options, s *Server) error { | |
| 35 | + | return wlrlayershell.NewZwlrLayerShellV1Global(d, 4, func(c server.Client, v, id uint32) { | |
| 36 | + | _, _ = wlrlayershell.NewZwlrLayerShellV1(c, int32(v), id, layerShell{s, uint32(o.OutputWidth), uint32(o.OutputHeight)}) | |
| 37 | + | }) | |
| 38 | + | } | |
| 39 | + | func (h layerShell) Destroy(*wlrlayershell.ZwlrLayerShellV1) {} | |
| 40 | + | func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32, w *wayland.Surface, _ *wayland.Output, layer uint32, namespace string) { | |
| 41 | + | if w == nil || h.server.surfaces[w.Resource] == nil { | |
| 42 | + | return | |
| 43 | + | } | |
| 44 | + | state := h.server.surfaces[w.Resource] | |
| 45 | + | if state.kind != roleNone || (state.attached && state.pending != nil) || state.current != nil { | |
| 46 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed") | |
| 47 | + | return | |
| 48 | + | } | |
| 49 | + | if layer > 3 { | |
| 50 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorInvalidLayer), "invalid layer") | |
| 51 | + | return | |
| 52 | + | } | |
| 53 | + | l := &layerSurface{shell: h, surface: state, id: h.server.nextWindow, namespace: namespace} | |
| 54 | + | l.pending.layer = ports.Layer(layer) | |
| 55 | + | resource, err := wlrlayershell.NewZwlrLayerSurfaceV1(r.Client(), r.Version(), id, l) | |
| 56 | + | if err != nil { | |
| 57 | + | return | |
| 58 | + | } | |
| 59 | + | l.resource = resource | |
| 60 | + | h.server.nextWindow++ | |
| 61 | + | state.kind = roleLayer | |
| 62 | + | state.layer = l | |
| 63 | + | state.role = l.commit | |
| 64 | + | h.server.layers[l.id] = l | |
| 65 | + | resource.OnDestroy = func() { | |
| 66 | + | l.unmap() | |
| 67 | + | delete(h.server.layers, l.id) | |
| 68 | + | state.layer = nil | |
| 69 | + | state.role = nil | |
| 70 | + | state.current, state.pending = nil, nil | |
| 71 | + | state.attached = false | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | func (l *layerSurface) SetSize(_ *wlrlayershell.ZwlrLayerSurfaceV1, w, h uint32) { | |
| 75 | + | l.pending.width, l.pending.height = w, h | |
| 76 | + | } | |
| 77 | + | func (l *layerSurface) SetAnchor(r *wlrlayershell.ZwlrLayerSurfaceV1, a uint32) { | |
| 78 | + | if a > 15 { | |
| 79 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidAnchor), "invalid anchor") | |
| 80 | + | return | |
| 81 | + | } | |
| 82 | + | l.pending.anchor = a | |
| 83 | + | } | |
| 84 | + | func (l *layerSurface) SetExclusiveZone(_ *wlrlayershell.ZwlrLayerSurfaceV1, z int32) { | |
| 85 | + | l.pending.zone = z | |
| 86 | + | } | |
| 87 | + | func (l *layerSurface) SetMargin(_ *wlrlayershell.ZwlrLayerSurfaceV1, t, r, b, left int32) { | |
| 88 | + | l.pending.margin = [4]int32{t, r, b, left} | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // Keyboard interactivity is deferred; layer-shell is capped at v4. | |
| 92 | + | func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) { | |
| 93 | + | if k > 2 { | |
| 94 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity") | |
| 95 | + | return | |
| 96 | + | } | |
| 97 | + | l.pending.keyboard = k | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // Layer popups are deferred; layer-shell is capped at v4. | |
| 101 | + | func (*layerSurface) GetPopup(*wlrlayershell.ZwlrLayerSurfaceV1, *xdgshell.Popup) {} | |
| 102 | + | func (l *layerSurface) AckConfigure(r *wlrlayershell.ZwlrLayerSurfaceV1, serial uint32) { | |
| 103 | + | for i, v := range l.serials { | |
| 104 | + | if v == serial { | |
| 105 | + | l.serials = l.serials[i+1:] | |
| 106 | + | l.acked = true | |
| 107 | + | return | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "unknown configure serial") | |
| 111 | + | } | |
| 112 | + | func (*layerSurface) Destroy(*wlrlayershell.ZwlrLayerSurfaceV1) {} | |
| 113 | + | func (l *layerSurface) SetLayer(r *wlrlayershell.ZwlrLayerSurfaceV1, v uint32) { | |
| 114 | + | if v > 3 { | |
| 115 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "invalid layer") | |
| 116 | + | return | |
| 117 | + | } | |
| 118 | + | l.pending.layer = ports.Layer(v) | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // Exclusive edge is not implemented; layer-shell is capped at v4. | |
| 122 | + | func (*layerSurface) SetExclusiveEdge(*wlrlayershell.ZwlrLayerSurfaceV1, uint32) {} | |
| 123 | + | func (l *layerSurface) unmap() { | |
| 124 | + | if !l.mapped { | |
| 125 | + | return | |
| 126 | + | } | |
| 127 | + | l.mapped = false | |
| 128 | + | l.configured = false | |
| 129 | + | l.acked = false | |
| 130 | + | l.serials = nil | |
| 131 | + | l.shell.server.layerChanged() | |
| 132 | + | l.shell.server.emitContent(ports.SurfaceContent{ID: l.id}) | |
| 133 | + | } | |
| 134 | + | func (s *Server) layerChanged() { | |
| 135 | + | list := make([]ports.LayerSurface, 0) | |
| 136 | + | for _, l := range s.layers { | |
| 137 | + | if !l.mapped { | |
| 138 | + | continue | |
| 139 | + | } | |
| 140 | + | v := ports.LayerSurface{ID: l.id, Layer: l.current.layer, Anchor: l.current.anchor, ExclusiveZone: l.current.zone, Margin: l.current.margin, Namespace: l.namespace} | |
| 141 | + | if l.surface.current != nil { | |
| 142 | + | if b := s.buffers[l.surface.current.Resource]; b != nil { | |
| 143 | + | v.Width, v.Height = b.width, b.height | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | list = append(list, v) | |
| 147 | + | } | |
| 148 | + | sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID }) | |
| 149 | + | s.emit(ports.LayerChanged{Layers: list}) | |
| 150 | + | } | |
| 151 | + | func (l *layerSurface) commit(buffer bool) { | |
| 152 | + | if l.surface.destroyed { | |
| 153 | + | l.unmap() | |
| 154 | + | delete(l.shell.server.layers, l.id) | |
| 155 | + | return | |
| 156 | + | } | |
| 157 | + | p := l.pending | |
| 158 | + | if (p.width == 0 && p.anchor&(ports.AnchorLeft|ports.AnchorRight) != ports.AnchorLeft|ports.AnchorRight) || (p.height == 0 && p.anchor&(ports.AnchorTop|ports.AnchorBottom) != ports.AnchorTop|ports.AnchorBottom) { | |
| 159 | + | l.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSize), "zero size requires opposing anchors") | |
| 160 | + | return | |
| 161 | + | } | |
| 162 | + | changed := p != l.current | |
| 163 | + | sizeChanged := p.width != l.current.width || p.height != l.current.height | |
| 164 | + | l.current = p | |
| 165 | + | if !l.configured || sizeChanged { | |
| 166 | + | w, h := p.width, p.height | |
| 167 | + | if w == 0 { | |
| 168 | + | w = l.shell.width | |
| 169 | + | } | |
| 170 | + | if h == 0 { | |
| 171 | + | h = l.shell.height | |
| 172 | + | } | |
| 173 | + | l.shell.server.serial++ | |
| 174 | + | l.resource.SendConfigure(l.shell.server.serial, w, h) | |
| 175 | + | l.serials = append(l.serials, l.shell.server.serial) | |
| 176 | + | l.configured = true | |
| 177 | + | ||
| 178 | + | } | |
| 179 | + | if !buffer { | |
| 180 | + | if l.mapped { | |
| 181 | + | l.unmap() | |
| 182 | + | l.configured = false | |
| 183 | + | l.acked = false | |
| 184 | + | l.serials = nil | |
| 185 | + | } | |
| 186 | + | return | |
| 187 | + | } | |
| 188 | + | if l.acked { | |
| 189 | + | wasMapped := l.mapped | |
| 190 | + | l.mapped = true | |
| 191 | + | if changed || !wasMapped { | |
| 192 | + | l.shell.server.layerChanged() | |
| 193 | + | } | |
| 194 | + | } | |
| 195 | + | } | |
| @@ -0,0 +1,195 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 5 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 6 | + | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |||
| 7 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | "sort" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | type layerShell struct { | |||
| 13 | + | server *Server | |||
| 14 | + | width, height uint32 | |||
| 15 | + | } | |||
| 16 | + | type layerState struct { | |||
| 17 | + | width, height, anchor uint32 | |||
| 18 | + | zone int32 | |||
| 19 | + | margin [4]int32 | |||
| 20 | + | keyboard uint32 | |||
| 21 | + | layer ports.Layer | |||
| 22 | + | } | |||
| 23 | + | type layerSurface struct { | |||
| 24 | + | resource *wlrlayershell.ZwlrLayerSurfaceV1 | |||
| 25 | + | shell layerShell | |||
| 26 | + | surface *surface | |||
| 27 | + | id ports.WindowID | |||
| 28 | + | namespace string | |||
| 29 | + | pending, current layerState | |||
| 30 | + | configured, acked, mapped bool | |||
| 31 | + | serials []uint32 | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func registerLayer(d *server.Display, o Options, s *Server) error { | |||
| 35 | + | return wlrlayershell.NewZwlrLayerShellV1Global(d, 4, func(c server.Client, v, id uint32) { | |||
| 36 | + | _, _ = wlrlayershell.NewZwlrLayerShellV1(c, int32(v), id, layerShell{s, uint32(o.OutputWidth), uint32(o.OutputHeight)}) | |||
| 37 | + | }) | |||
| 38 | + | } | |||
| 39 | + | func (h layerShell) Destroy(*wlrlayershell.ZwlrLayerShellV1) {} | |||
| 40 | + | func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32, w *wayland.Surface, _ *wayland.Output, layer uint32, namespace string) { | |||
| 41 | + | if w == nil || h.server.surfaces[w.Resource] == nil { | |||
| 42 | + | return | |||
| 43 | + | } | |||
| 44 | + | state := h.server.surfaces[w.Resource] | |||
| 45 | + | if state.kind != roleNone || (state.attached && state.pending != nil) || state.current != nil { | |||
| 46 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed") | |||
| 47 | + | return | |||
| 48 | + | } | |||
| 49 | + | if layer > 3 { | |||
| 50 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorInvalidLayer), "invalid layer") | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 53 | + | l := &layerSurface{shell: h, surface: state, id: h.server.nextWindow, namespace: namespace} | |||
| 54 | + | l.pending.layer = ports.Layer(layer) | |||
| 55 | + | resource, err := wlrlayershell.NewZwlrLayerSurfaceV1(r.Client(), r.Version(), id, l) | |||
| 56 | + | if err != nil { | |||
| 57 | + | return | |||
| 58 | + | } | |||
| 59 | + | l.resource = resource | |||
| 60 | + | h.server.nextWindow++ | |||
| 61 | + | state.kind = roleLayer | |||
| 62 | + | state.layer = l | |||
| 63 | + | state.role = l.commit | |||
| 64 | + | h.server.layers[l.id] = l | |||
| 65 | + | resource.OnDestroy = func() { | |||
| 66 | + | l.unmap() | |||
| 67 | + | delete(h.server.layers, l.id) | |||
| 68 | + | state.layer = nil | |||
| 69 | + | state.role = nil | |||
| 70 | + | state.current, state.pending = nil, nil | |||
| 71 | + | state.attached = false | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | func (l *layerSurface) SetSize(_ *wlrlayershell.ZwlrLayerSurfaceV1, w, h uint32) { | |||
| 75 | + | l.pending.width, l.pending.height = w, h | |||
| 76 | + | } | |||
| 77 | + | func (l *layerSurface) SetAnchor(r *wlrlayershell.ZwlrLayerSurfaceV1, a uint32) { | |||
| 78 | + | if a > 15 { | |||
| 79 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidAnchor), "invalid anchor") | |||
| 80 | + | return | |||
| 81 | + | } | |||
| 82 | + | l.pending.anchor = a | |||
| 83 | + | } | |||
| 84 | + | func (l *layerSurface) SetExclusiveZone(_ *wlrlayershell.ZwlrLayerSurfaceV1, z int32) { | |||
| 85 | + | l.pending.zone = z | |||
| 86 | + | } | |||
| 87 | + | func (l *layerSurface) SetMargin(_ *wlrlayershell.ZwlrLayerSurfaceV1, t, r, b, left int32) { | |||
| 88 | + | l.pending.margin = [4]int32{t, r, b, left} | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | // Keyboard interactivity is deferred; layer-shell is capped at v4. | |||
| 92 | + | func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) { | |||
| 93 | + | if k > 2 { | |||
| 94 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity") | |||
| 95 | + | return | |||
| 96 | + | } | |||
| 97 | + | l.pending.keyboard = k | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // Layer popups are deferred; layer-shell is capped at v4. | |||
| 101 | + | func (*layerSurface) GetPopup(*wlrlayershell.ZwlrLayerSurfaceV1, *xdgshell.Popup) {} | |||
| 102 | + | func (l *layerSurface) AckConfigure(r *wlrlayershell.ZwlrLayerSurfaceV1, serial uint32) { | |||
| 103 | + | for i, v := range l.serials { | |||
| 104 | + | if v == serial { | |||
| 105 | + | l.serials = l.serials[i+1:] | |||
| 106 | + | l.acked = true | |||
| 107 | + | return | |||
| 108 | + | } | |||
| 109 | + | } | |||
| 110 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "unknown configure serial") | |||
| 111 | + | } | |||
| 112 | + | func (*layerSurface) Destroy(*wlrlayershell.ZwlrLayerSurfaceV1) {} | |||
| 113 | + | func (l *layerSurface) SetLayer(r *wlrlayershell.ZwlrLayerSurfaceV1, v uint32) { | |||
| 114 | + | if v > 3 { | |||
| 115 | + | r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "invalid layer") | |||
| 116 | + | return | |||
| 117 | + | } | |||
| 118 | + | l.pending.layer = ports.Layer(v) | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | // Exclusive edge is not implemented; layer-shell is capped at v4. | |||
| 122 | + | func (*layerSurface) SetExclusiveEdge(*wlrlayershell.ZwlrLayerSurfaceV1, uint32) {} | |||
| 123 | + | func (l *layerSurface) unmap() { | |||
| 124 | + | if !l.mapped { | |||
| 125 | + | return | |||
| 126 | + | } | |||
| 127 | + | l.mapped = false | |||
| 128 | + | l.configured = false | |||
| 129 | + | l.acked = false | |||
| 130 | + | l.serials = nil | |||
| 131 | + | l.shell.server.layerChanged() | |||
| 132 | + | l.shell.server.emitContent(ports.SurfaceContent{ID: l.id}) | |||
| 133 | + | } | |||
| 134 | + | func (s *Server) layerChanged() { | |||
| 135 | + | list := make([]ports.LayerSurface, 0) | |||
| 136 | + | for _, l := range s.layers { | |||
| 137 | + | if !l.mapped { | |||
| 138 | + | continue | |||
| 139 | + | } | |||
| 140 | + | v := ports.LayerSurface{ID: l.id, Layer: l.current.layer, Anchor: l.current.anchor, ExclusiveZone: l.current.zone, Margin: l.current.margin, Namespace: l.namespace} | |||
| 141 | + | if l.surface.current != nil { | |||
| 142 | + | if b := s.buffers[l.surface.current.Resource]; b != nil { | |||
| 143 | + | v.Width, v.Height = b.width, b.height | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | list = append(list, v) | |||
| 147 | + | } | |||
| 148 | + | sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID }) | |||
| 149 | + | s.emit(ports.LayerChanged{Layers: list}) | |||
| 150 | + | } | |||
| 151 | + | func (l *layerSurface) commit(buffer bool) { | |||
| 152 | + | if l.surface.destroyed { | |||
| 153 | + | l.unmap() | |||
| 154 | + | delete(l.shell.server.layers, l.id) | |||
| 155 | + | return | |||
| 156 | + | } | |||
| 157 | + | p := l.pending | |||
| 158 | + | if (p.width == 0 && p.anchor&(ports.AnchorLeft|ports.AnchorRight) != ports.AnchorLeft|ports.AnchorRight) || (p.height == 0 && p.anchor&(ports.AnchorTop|ports.AnchorBottom) != ports.AnchorTop|ports.AnchorBottom) { | |||
| 159 | + | l.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSize), "zero size requires opposing anchors") | |||
| 160 | + | return | |||
| 161 | + | } | |||
| 162 | + | changed := p != l.current | |||
| 163 | + | sizeChanged := p.width != l.current.width || p.height != l.current.height | |||
| 164 | + | l.current = p | |||
| 165 | + | if !l.configured || sizeChanged { | |||
| 166 | + | w, h := p.width, p.height | |||
| 167 | + | if w == 0 { | |||
| 168 | + | w = l.shell.width | |||
| 169 | + | } | |||
| 170 | + | if h == 0 { | |||
| 171 | + | h = l.shell.height | |||
| 172 | + | } | |||
| 173 | + | l.shell.server.serial++ | |||
| 174 | + | l.resource.SendConfigure(l.shell.server.serial, w, h) | |||
| 175 | + | l.serials = append(l.serials, l.shell.server.serial) | |||
| 176 | + | l.configured = true | |||
| 177 | + | ||||
| 178 | + | } | |||
| 179 | + | if !buffer { | |||
| 180 | + | if l.mapped { | |||
| 181 | + | l.unmap() | |||
| 182 | + | l.configured = false | |||
| 183 | + | l.acked = false | |||
| 184 | + | l.serials = nil | |||
| 185 | + | } | |||
| 186 | + | return | |||
| 187 | + | } | |||
| 188 | + | if l.acked { | |||
| 189 | + | wasMapped := l.mapped | |||
| 190 | + | l.mapped = true | |||
| 191 | + | if changed || !wasMapped { | |||
| 192 | + | l.shell.server.layerChanged() | |||
| 193 | + | } | |||
| 194 | + | } | |||
| 195 | + | } | |||
A internal/adapters/wayland/layer_test.go +224 -0
| @@ -0,0 +1,224 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "github.com/bnema/nefertty/internal/ports" | |
| 6 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 7 | + | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |
| 8 | + | "github.com/bnema/wlturbo" | |
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | "os" | |
| 11 | + | "os/exec" | |
| 12 | + | "testing" | |
| 13 | + | "time" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | type layerProxy struct { | |
| 17 | + | wlturbo.BaseProxy | |
| 18 | + | configured chan [3]uint32 | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func (p *layerProxy) Dispatch(e *wlturbo.Event) { | |
| 22 | + | if e.Opcode == uint16(wlrlayershell.ZwlrLayerSurfaceV1EventConfigure) { | |
| 23 | + | p.configured <- [3]uint32{e.Uint32(), e.Uint32(), e.Uint32()} | |
| 24 | + | } | |
| 25 | + | } | |
| 26 | + | func layerProtocol(t *testing.T, c *wlturbo.Display) (uint32, uint32) { | |
| 27 | + | t.Helper() | |
| 28 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 29 | + | shell := bindProtocol(t, c, "zwlr_layer_shell_v1") | |
| 30 | + | surf := c.AllocateID() | |
| 31 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |
| 32 | + | registerProtocol(t, c, surf) | |
| 33 | + | layer := c.AllocateID() | |
| 34 | + | requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, layer, surf, uint32(0), uint32(ports.LayerTop), "test") | |
| 35 | + | return surf, layer | |
| 36 | + | } | |
| 37 | + | func TestLayerInvalidSize(t *testing.T) { | |
| 38 | + | s, _, _, dir := lifecycleServer(t) | |
| 39 | + | c := protocolClient(t, s, dir) | |
| 40 | + | surf, layer := layerProtocol(t, c) | |
| 41 | + | registerProtocol(t, c, layer) | |
| 42 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(0), uint32(30)) | |
| 43 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetAnchor, uint32(ports.AnchorTop)) | |
| 44 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 45 | + | expectProtocolError(t, c, layer, uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSize)) | |
| 46 | + | } | |
| 47 | + | func TestLayerLifecycle(t *testing.T) { | |
| 48 | + | s, events, _, dir := lifecycleServer(t) | |
| 49 | + | c := protocolClient(t, s, dir) | |
| 50 | + | surf, layer := layerProtocol(t, c) | |
| 51 | + | proxy := &layerProxy{configured: make(chan [3]uint32, 2)} | |
| 52 | + | proxy.SetID(layer) | |
| 53 | + | c.Context().Register(proxy) | |
| 54 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetAnchor, uint32(13)) | |
| 55 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(0), uint32(30)) | |
| 56 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetExclusiveZone, int32(30)) | |
| 57 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 58 | + | if err := c.Roundtrip(); err != nil { | |
| 59 | + | t.Fatal(err) | |
| 60 | + | } | |
| 61 | + | var config [3]uint32 | |
| 62 | + | select { | |
| 63 | + | case config = <-proxy.configured: | |
| 64 | + | case <-time.After(2 * time.Second): | |
| 65 | + | t.Fatal("configure timeout") | |
| 66 | + | } | |
| 67 | + | if config[1] != 1920 || config[2] != 30 { | |
| 68 | + | t.Fatalf("configure: %v", config) | |
| 69 | + | } | |
| 70 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestAckConfigure, config[0]) | |
| 71 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 72 | + | registerProtocol(t, c, shm) | |
| 73 | + | fd, err := unix.MemfdCreate("layer-buffer", 0) | |
| 74 | + | if err != nil { | |
| 75 | + | t.Fatal(err) | |
| 76 | + | } | |
| 77 | + | defer unix.Close(fd) | |
| 78 | + | if err = unix.Ftruncate(fd, 1920*30*4); err != nil { | |
| 79 | + | t.Fatal(err) | |
| 80 | + | } | |
| 81 | + | pool, buf := c.AllocateID(), c.AllocateID() | |
| 82 | + | registerProtocol(t, c, pool) | |
| 83 | + | registerProtocol(t, c, buf) | |
| 84 | + | if err = c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(1920*30*4)); err != nil { | |
| 85 | + | t.Fatal(err) | |
| 86 | + | } | |
| 87 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1920), int32(30), int32(1920*4), uint32(wayland.ShmFormatArgb8888)) | |
| 88 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 89 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 90 | + | if err = c.Roundtrip(); err != nil { | |
| 91 | + | t.Fatal(err) | |
| 92 | + | } | |
| 93 | + | select { | |
| 94 | + | case ev := <-events: | |
| 95 | + | v, ok := ev.(ports.LayerChanged) | |
| 96 | + | if !ok || len(v.Layers) != 1 || v.Layers[0].Anchor != 13 || v.Layers[0].ExclusiveZone != 30 || v.Layers[0].Width != 1920 || v.Layers[0].Height != 30 { | |
| 97 | + | t.Fatalf("mapped: %#v", ev) | |
| 98 | + | } | |
| 99 | + | case <-time.After(2 * time.Second): | |
| 100 | + | t.Fatal("map timeout") | |
| 101 | + | } | |
| 102 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy) | |
| 103 | + | if err = c.Roundtrip(); err != nil { | |
| 104 | + | t.Fatal(err) | |
| 105 | + | } | |
| 106 | + | select { | |
| 107 | + | case ev := <-events: | |
| 108 | + | v, ok := ev.(ports.LayerChanged) | |
| 109 | + | if !ok || len(v.Layers) != 0 { | |
| 110 | + | t.Fatalf("destroy: %#v", ev) | |
| 111 | + | } | |
| 112 | + | case <-time.After(2 * time.Second): | |
| 113 | + | t.Fatal("destroy timeout") | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | func TestWaybarLayer(t *testing.T) { | |
| 117 | + | tool, err := exec.LookPath("waybar") | |
| 118 | + | if err != nil { | |
| 119 | + | t.Skip("waybar missing") | |
| 120 | + | } | |
| 121 | + | s, events, _, dir := keyboardServer(t) | |
| 122 | + | home := t.TempDir() | |
| 123 | + | config := home + "/config.json" | |
| 124 | + | style := home + "/style.css" | |
| 125 | + | if err = os.WriteFile(config, []byte(`{"layer":"top","position":"top","height":30,"modules-left":["clock"]}`), 0600); err != nil { | |
| 126 | + | t.Fatal(err) | |
| 127 | + | } | |
| 128 | + | if err = os.WriteFile(style, nil, 0600); err != nil { | |
| 129 | + | t.Fatal(err) | |
| 130 | + | } | |
| 131 | + | _, done, output := lifecycleClient(t, tool, s, dir, []string{"HOME=" + home}, "-c", config, "-s", style) | |
| 132 | + | select { | |
| 133 | + | case ev := <-events: | |
| 134 | + | v, ok := ev.(ports.LayerChanged) | |
| 135 | + | if !ok || len(v.Layers) != 1 || v.Layers[0].Height != 30 { | |
| 136 | + | t.Fatalf("waybar event: %#v stderr: %s", ev, output.String()) | |
| 137 | + | } | |
| 138 | + | case err := <-done: | |
| 139 | + | t.Fatalf("waybar exited: %v stderr: %s", err, output.String()) | |
| 140 | + | case <-time.After(10 * time.Second): | |
| 141 | + | t.Fatalf("waybar timeout stderr: %s", output.String()) | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | func TestLayerAlreadyConstructed(t *testing.T) { | |
| 146 | + | for _, scenario := range []string{"role"} { | |
| 147 | + | t.Run(scenario, func(t *testing.T) { | |
| 148 | + | s, _, _, dir := lifecycleServer(t) | |
| 149 | + | c := protocolClient(t, s, dir) | |
| 150 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 151 | + | shell := bindProtocol(t, c, "zwlr_layer_shell_v1") | |
| 152 | + | surf := c.AllocateID() | |
| 153 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |
| 154 | + | registerProtocol(t, c, surf) | |
| 155 | + | switch scenario { | |
| 156 | + | case "role": | |
| 157 | + | sub := bindProtocol(t, c, "wl_subcompositor") | |
| 158 | + | parent := c.AllocateID() | |
| 159 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, parent) | |
| 160 | + | registerProtocol(t, c, parent) | |
| 161 | + | id := c.AllocateID() | |
| 162 | + | requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, id, surf, parent) | |
| 163 | + | } | |
| 164 | + | id := c.AllocateID() | |
| 165 | + | requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, id, surf, uint32(0), uint32(ports.LayerTop), "test") | |
| 166 | + | expectProtocolError(t, c, shell, uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed)) | |
| 167 | + | }) | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | type doneProxy struct { | |
| 172 | + | wlturbo.BaseProxy | |
| 173 | + | done int | |
| 174 | + | opcode uint16 | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | func (p *doneProxy) Dispatch(e *wlturbo.Event) { | |
| 178 | + | if e.Opcode == p.opcode { | |
| 179 | + | p.done++ | |
| 180 | + | } | |
| 181 | + | } | |
| 182 | + | func TestXDGOutputDone(t *testing.T) { | |
| 183 | + | for _, version := range []uint32{1, 2} { | |
| 184 | + | t.Run(fmt.Sprintf("wl_output_v%d", version), func(t *testing.T) { | |
| 185 | + | s, _, _, dir := lifecycleServer(t) | |
| 186 | + | c := protocolClient(t, s, dir) | |
| 187 | + | global, ok := c.Registry().FindGlobal("wl_output") | |
| 188 | + | if !ok { | |
| 189 | + | t.Fatal("missing wl_output") | |
| 190 | + | } | |
| 191 | + | output, err := c.Registry().BindID(global.Name, global.Interface, version) | |
| 192 | + | if err != nil { | |
| 193 | + | t.Fatal(err) | |
| 194 | + | } | |
| 195 | + | op := &doneProxy{opcode: uint16(wayland.OutputEventDone)} | |
| 196 | + | op.SetID(output) | |
| 197 | + | c.Context().Register(op) | |
| 198 | + | global, ok = c.Registry().FindGlobal("zxdg_output_manager_v1") | |
| 199 | + | if !ok { | |
| 200 | + | t.Fatal("missing xdg output manager") | |
| 201 | + | } | |
| 202 | + | manager, err := c.Registry().BindID(global.Name, global.Interface, 3) | |
| 203 | + | if err != nil { | |
| 204 | + | t.Fatal(err) | |
| 205 | + | } | |
| 206 | + | registerProtocol(t, c, manager) | |
| 207 | + | if err := c.Roundtrip(); err != nil { | |
| 208 | + | t.Fatal(err) | |
| 209 | + | } | |
| 210 | + | op.done = 0 | |
| 211 | + | xdg := c.AllocateID() | |
| 212 | + | xp := &doneProxy{opcode: 2} | |
| 213 | + | xp.SetID(xdg) | |
| 214 | + | c.Context().Register(xp) | |
| 215 | + | requestProtocol(t, c, manager, 1, xdg, output) | |
| 216 | + | if err := c.Roundtrip(); err != nil { | |
| 217 | + | t.Fatal(err) | |
| 218 | + | } | |
| 219 | + | if version == 1 && (xp.done != 1 || op.done != 0) || version == 2 && (xp.done != 0 || op.done != 1) { | |
| 220 | + | t.Fatalf("wl_output v%d: xdg done=%d, wl_output done=%d", version, xp.done, op.done) | |
| 221 | + | } | |
| 222 | + | }) | |
| 223 | + | } | |
| 224 | + | } | |
| @@ -0,0 +1,224 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 6 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 7 | + | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |||
| 8 | + | "github.com/bnema/wlturbo" | |||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | "os" | |||
| 11 | + | "os/exec" | |||
| 12 | + | "testing" | |||
| 13 | + | "time" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | type layerProxy struct { | |||
| 17 | + | wlturbo.BaseProxy | |||
| 18 | + | configured chan [3]uint32 | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func (p *layerProxy) Dispatch(e *wlturbo.Event) { | |||
| 22 | + | if e.Opcode == uint16(wlrlayershell.ZwlrLayerSurfaceV1EventConfigure) { | |||
| 23 | + | p.configured <- [3]uint32{e.Uint32(), e.Uint32(), e.Uint32()} | |||
| 24 | + | } | |||
| 25 | + | } | |||
| 26 | + | func layerProtocol(t *testing.T, c *wlturbo.Display) (uint32, uint32) { | |||
| 27 | + | t.Helper() | |||
| 28 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 29 | + | shell := bindProtocol(t, c, "zwlr_layer_shell_v1") | |||
| 30 | + | surf := c.AllocateID() | |||
| 31 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 32 | + | registerProtocol(t, c, surf) | |||
| 33 | + | layer := c.AllocateID() | |||
| 34 | + | requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, layer, surf, uint32(0), uint32(ports.LayerTop), "test") | |||
| 35 | + | return surf, layer | |||
| 36 | + | } | |||
| 37 | + | func TestLayerInvalidSize(t *testing.T) { | |||
| 38 | + | s, _, _, dir := lifecycleServer(t) | |||
| 39 | + | c := protocolClient(t, s, dir) | |||
| 40 | + | surf, layer := layerProtocol(t, c) | |||
| 41 | + | registerProtocol(t, c, layer) | |||
| 42 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(0), uint32(30)) | |||
| 43 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetAnchor, uint32(ports.AnchorTop)) | |||
| 44 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 45 | + | expectProtocolError(t, c, layer, uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSize)) | |||
| 46 | + | } | |||
| 47 | + | func TestLayerLifecycle(t *testing.T) { | |||
| 48 | + | s, events, _, dir := lifecycleServer(t) | |||
| 49 | + | c := protocolClient(t, s, dir) | |||
| 50 | + | surf, layer := layerProtocol(t, c) | |||
| 51 | + | proxy := &layerProxy{configured: make(chan [3]uint32, 2)} | |||
| 52 | + | proxy.SetID(layer) | |||
| 53 | + | c.Context().Register(proxy) | |||
| 54 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetAnchor, uint32(13)) | |||
| 55 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(0), uint32(30)) | |||
| 56 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetExclusiveZone, int32(30)) | |||
| 57 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 58 | + | if err := c.Roundtrip(); err != nil { | |||
| 59 | + | t.Fatal(err) | |||
| 60 | + | } | |||
| 61 | + | var config [3]uint32 | |||
| 62 | + | select { | |||
| 63 | + | case config = <-proxy.configured: | |||
| 64 | + | case <-time.After(2 * time.Second): | |||
| 65 | + | t.Fatal("configure timeout") | |||
| 66 | + | } | |||
| 67 | + | if config[1] != 1920 || config[2] != 30 { | |||
| 68 | + | t.Fatalf("configure: %v", config) | |||
| 69 | + | } | |||
| 70 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestAckConfigure, config[0]) | |||
| 71 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 72 | + | registerProtocol(t, c, shm) | |||
| 73 | + | fd, err := unix.MemfdCreate("layer-buffer", 0) | |||
| 74 | + | if err != nil { | |||
| 75 | + | t.Fatal(err) | |||
| 76 | + | } | |||
| 77 | + | defer unix.Close(fd) | |||
| 78 | + | if err = unix.Ftruncate(fd, 1920*30*4); err != nil { | |||
| 79 | + | t.Fatal(err) | |||
| 80 | + | } | |||
| 81 | + | pool, buf := c.AllocateID(), c.AllocateID() | |||
| 82 | + | registerProtocol(t, c, pool) | |||
| 83 | + | registerProtocol(t, c, buf) | |||
| 84 | + | if err = c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(1920*30*4)); err != nil { | |||
| 85 | + | t.Fatal(err) | |||
| 86 | + | } | |||
| 87 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1920), int32(30), int32(1920*4), uint32(wayland.ShmFormatArgb8888)) | |||
| 88 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 89 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 90 | + | if err = c.Roundtrip(); err != nil { | |||
| 91 | + | t.Fatal(err) | |||
| 92 | + | } | |||
| 93 | + | select { | |||
| 94 | + | case ev := <-events: | |||
| 95 | + | v, ok := ev.(ports.LayerChanged) | |||
| 96 | + | if !ok || len(v.Layers) != 1 || v.Layers[0].Anchor != 13 || v.Layers[0].ExclusiveZone != 30 || v.Layers[0].Width != 1920 || v.Layers[0].Height != 30 { | |||
| 97 | + | t.Fatalf("mapped: %#v", ev) | |||
| 98 | + | } | |||
| 99 | + | case <-time.After(2 * time.Second): | |||
| 100 | + | t.Fatal("map timeout") | |||
| 101 | + | } | |||
| 102 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy) | |||
| 103 | + | if err = c.Roundtrip(); err != nil { | |||
| 104 | + | t.Fatal(err) | |||
| 105 | + | } | |||
| 106 | + | select { | |||
| 107 | + | case ev := <-events: | |||
| 108 | + | v, ok := ev.(ports.LayerChanged) | |||
| 109 | + | if !ok || len(v.Layers) != 0 { | |||
| 110 | + | t.Fatalf("destroy: %#v", ev) | |||
| 111 | + | } | |||
| 112 | + | case <-time.After(2 * time.Second): | |||
| 113 | + | t.Fatal("destroy timeout") | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | func TestWaybarLayer(t *testing.T) { | |||
| 117 | + | tool, err := exec.LookPath("waybar") | |||
| 118 | + | if err != nil { | |||
| 119 | + | t.Skip("waybar missing") | |||
| 120 | + | } | |||
| 121 | + | s, events, _, dir := keyboardServer(t) | |||
| 122 | + | home := t.TempDir() | |||
| 123 | + | config := home + "/config.json" | |||
| 124 | + | style := home + "/style.css" | |||
| 125 | + | if err = os.WriteFile(config, []byte(`{"layer":"top","position":"top","height":30,"modules-left":["clock"]}`), 0600); err != nil { | |||
| 126 | + | t.Fatal(err) | |||
| 127 | + | } | |||
| 128 | + | if err = os.WriteFile(style, nil, 0600); err != nil { | |||
| 129 | + | t.Fatal(err) | |||
| 130 | + | } | |||
| 131 | + | _, done, output := lifecycleClient(t, tool, s, dir, []string{"HOME=" + home}, "-c", config, "-s", style) | |||
| 132 | + | select { | |||
| 133 | + | case ev := <-events: | |||
| 134 | + | v, ok := ev.(ports.LayerChanged) | |||
| 135 | + | if !ok || len(v.Layers) != 1 || v.Layers[0].Height != 30 { | |||
| 136 | + | t.Fatalf("waybar event: %#v stderr: %s", ev, output.String()) | |||
| 137 | + | } | |||
| 138 | + | case err := <-done: | |||
| 139 | + | t.Fatalf("waybar exited: %v stderr: %s", err, output.String()) | |||
| 140 | + | case <-time.After(10 * time.Second): | |||
| 141 | + | t.Fatalf("waybar timeout stderr: %s", output.String()) | |||
| 142 | + | } | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | func TestLayerAlreadyConstructed(t *testing.T) { | |||
| 146 | + | for _, scenario := range []string{"role"} { | |||
| 147 | + | t.Run(scenario, func(t *testing.T) { | |||
| 148 | + | s, _, _, dir := lifecycleServer(t) | |||
| 149 | + | c := protocolClient(t, s, dir) | |||
| 150 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 151 | + | shell := bindProtocol(t, c, "zwlr_layer_shell_v1") | |||
| 152 | + | surf := c.AllocateID() | |||
| 153 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 154 | + | registerProtocol(t, c, surf) | |||
| 155 | + | switch scenario { | |||
| 156 | + | case "role": | |||
| 157 | + | sub := bindProtocol(t, c, "wl_subcompositor") | |||
| 158 | + | parent := c.AllocateID() | |||
| 159 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, parent) | |||
| 160 | + | registerProtocol(t, c, parent) | |||
| 161 | + | id := c.AllocateID() | |||
| 162 | + | requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, id, surf, parent) | |||
| 163 | + | } | |||
| 164 | + | id := c.AllocateID() | |||
| 165 | + | requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, id, surf, uint32(0), uint32(ports.LayerTop), "test") | |||
| 166 | + | expectProtocolError(t, c, shell, uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed)) | |||
| 167 | + | }) | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | type doneProxy struct { | |||
| 172 | + | wlturbo.BaseProxy | |||
| 173 | + | done int | |||
| 174 | + | opcode uint16 | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | func (p *doneProxy) Dispatch(e *wlturbo.Event) { | |||
| 178 | + | if e.Opcode == p.opcode { | |||
| 179 | + | p.done++ | |||
| 180 | + | } | |||
| 181 | + | } | |||
| 182 | + | func TestXDGOutputDone(t *testing.T) { | |||
| 183 | + | for _, version := range []uint32{1, 2} { | |||
| 184 | + | t.Run(fmt.Sprintf("wl_output_v%d", version), func(t *testing.T) { | |||
| 185 | + | s, _, _, dir := lifecycleServer(t) | |||
| 186 | + | c := protocolClient(t, s, dir) | |||
| 187 | + | global, ok := c.Registry().FindGlobal("wl_output") | |||
| 188 | + | if !ok { | |||
| 189 | + | t.Fatal("missing wl_output") | |||
| 190 | + | } | |||
| 191 | + | output, err := c.Registry().BindID(global.Name, global.Interface, version) | |||
| 192 | + | if err != nil { | |||
| 193 | + | t.Fatal(err) | |||
| 194 | + | } | |||
| 195 | + | op := &doneProxy{opcode: uint16(wayland.OutputEventDone)} | |||
| 196 | + | op.SetID(output) | |||
| 197 | + | c.Context().Register(op) | |||
| 198 | + | global, ok = c.Registry().FindGlobal("zxdg_output_manager_v1") | |||
| 199 | + | if !ok { | |||
| 200 | + | t.Fatal("missing xdg output manager") | |||
| 201 | + | } | |||
| 202 | + | manager, err := c.Registry().BindID(global.Name, global.Interface, 3) | |||
| 203 | + | if err != nil { | |||
| 204 | + | t.Fatal(err) | |||
| 205 | + | } | |||
| 206 | + | registerProtocol(t, c, manager) | |||
| 207 | + | if err := c.Roundtrip(); err != nil { | |||
| 208 | + | t.Fatal(err) | |||
| 209 | + | } | |||
| 210 | + | op.done = 0 | |||
| 211 | + | xdg := c.AllocateID() | |||
| 212 | + | xp := &doneProxy{opcode: 2} | |||
| 213 | + | xp.SetID(xdg) | |||
| 214 | + | c.Context().Register(xp) | |||
| 215 | + | requestProtocol(t, c, manager, 1, xdg, output) | |||
| 216 | + | if err := c.Roundtrip(); err != nil { | |||
| 217 | + | t.Fatal(err) | |||
| 218 | + | } | |||
| 219 | + | if version == 1 && (xp.done != 1 || op.done != 0) || version == 2 && (xp.done != 0 || op.done != 1) { | |||
| 220 | + | t.Fatalf("wl_output v%d: xdg done=%d, wl_output done=%d", version, xp.done, op.done) | |||
| 221 | + | } | |||
| 222 | + | }) | |||
| 223 | + | } | |||
| 224 | + | } | |||
M internal/adapters/wayland/server.go +2 -1
| @@ -43,6 +43,7 @@ type Server struct { | |||
| 43 | 43 | serial uint32 | |
| 44 | 44 | ctx context.Context | |
| 45 | 45 | windows map[ports.WindowID]*window | |
| 46 | + | layers map[ports.WindowID]*layerSurface | |
| 46 | 47 | nextWindow ports.WindowID | |
| 47 | 48 | focused ports.WindowID | |
| 48 | 49 | pointerFocus ports.WindowID | |
| @@ -91,7 +92,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 91 | 92 | d.Close() | |
| 92 | 93 | return nil, err | |
| 93 | 94 | } | |
| 94 | - | s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | |
| 95 | + | s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | |
| 95 | 96 | if opts.Keymap != "" { | |
| 96 | 97 | if uint64(len(opts.Keymap))+1 > uint64(^uint32(0)) { | |
| 97 | 98 | cleanup() | |
| @@ -43,6 +43,7 @@ type Server struct { | |||||
| 43 | serial uint32 | 43 | serial uint32 | ||
| 44 | ctx context.Context | 44 | ctx context.Context | ||
| 45 | windows map[ports.WindowID]*window | 45 | windows map[ports.WindowID]*window | ||
| 46 | + | layers map[ports.WindowID]*layerSurface | |||
| 46 | nextWindow ports.WindowID | 47 | nextWindow ports.WindowID | ||
| 47 | focused ports.WindowID | 48 | focused ports.WindowID | ||
| 48 | pointerFocus ports.WindowID | 49 | pointerFocus ports.WindowID | ||
| @@ -91,7 +92,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 91 | d.Close() | 92 | d.Close() | ||
| 92 | return nil, err | 93 | return nil, err | ||
| 93 | } | 94 | } | ||
| 94 | - | s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | 95 | + | s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} |
| 95 | if opts.Keymap != "" { | 96 | if opts.Keymap != "" { | ||
| 96 | if uint64(len(opts.Keymap))+1 > uint64(^uint32(0)) { | 97 | if uint64(len(opts.Keymap))+1 > uint64(^uint32(0)) { | ||
| 97 | cleanup() | 98 | cleanup() | ||
M internal/adapters/wayland/surface.go +17 -2
| @@ -1,7 +1,9 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |
| 4 | 5 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 6 | + | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |
| 5 | 7 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 6 | 8 | ) | |
| 7 | 9 | ||
| @@ -11,11 +13,13 @@ const ( | |||
| 11 | 13 | roleNone roleKind = iota | |
| 12 | 14 | roleXDG | |
| 13 | 15 | roleSubsurface | |
| 16 | + | roleLayer | |
| 14 | 17 | ) | |
| 15 | 18 | ||
| 16 | 19 | type surface struct { | |
| 17 | 20 | kind roleKind | |
| 18 | 21 | xdg *xdgSurface | |
| 22 | + | layer *layerSurface | |
| 19 | 23 | server *Server | |
| 20 | 24 | current, pending *wayland.Buffer | |
| 21 | 25 | attached bool | |
| @@ -48,6 +52,10 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) { | |||
| 48 | 52 | } | |
| 49 | 53 | func (s *surface) Commit(*wayland.Surface) { | |
| 50 | 54 | fresh := s.attached && s.pending != nil | |
| 55 | + | if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked { | |
| 56 | + | s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack") | |
| 57 | + | return | |
| 58 | + | } | |
| 51 | 59 | if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked { | |
| 52 | 60 | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | |
| 53 | 61 | return | |
| @@ -68,10 +76,17 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 68 | 76 | if s.role != nil { | |
| 69 | 77 | s.role(s.current != nil) | |
| 70 | 78 | } | |
| 71 | - | if fresh && s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped && s.server.channels.Contents != nil { | |
| 79 | + | id := ports.WindowID(0) | |
| 80 | + | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | |
| 81 | + | id = s.xdg.window.id | |
| 82 | + | } | |
| 83 | + | if s.layer != nil && s.layer.mapped { | |
| 84 | + | id = s.layer.id | |
| 85 | + | } | |
| 86 | + | if fresh && id != 0 && s.server.channels.Contents != nil { | |
| 72 | 87 | b := s.current | |
| 73 | 88 | if state, ok := s.server.buffers[b.Resource]; ok { | |
| 74 | - | if c, ok := state.content(s.xdg.window.id); ok { | |
| 89 | + | if c, ok := state.content(id); ok { | |
| 75 | 90 | s.server.emitContent(c) | |
| 76 | 91 | } else { | |
| 77 | 92 | state.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated") | |
| @@ -1,7 +1,9 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 4 | "github.com/bnema/purego-libwayland/protocol/wayland" | 5 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 6 | + | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |||
| 5 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | 7 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | ||
| 6 | ) | 8 | ) | ||
| 7 | 9 | ||||
| @@ -11,11 +13,13 @@ const ( | |||||
| 11 | roleNone roleKind = iota | 13 | roleNone roleKind = iota | ||
| 12 | roleXDG | 14 | roleXDG | ||
| 13 | roleSubsurface | 15 | roleSubsurface | ||
| 16 | + | roleLayer | |||
| 14 | ) | 17 | ) | ||
| 15 | 18 | ||||
| 16 | type surface struct { | 19 | type surface struct { | ||
| 17 | kind roleKind | 20 | kind roleKind | ||
| 18 | xdg *xdgSurface | 21 | xdg *xdgSurface | ||
| 22 | + | layer *layerSurface | |||
| 19 | server *Server | 23 | server *Server | ||
| 20 | current, pending *wayland.Buffer | 24 | current, pending *wayland.Buffer | ||
| 21 | attached bool | 25 | attached bool | ||
| @@ -48,6 +52,10 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) { | |||||
| 48 | } | 52 | } | ||
| 49 | func (s *surface) Commit(*wayland.Surface) { | 53 | func (s *surface) Commit(*wayland.Surface) { | ||
| 50 | fresh := s.attached && s.pending != nil | 54 | fresh := s.attached && s.pending != nil | ||
| 55 | + | if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked { | |||
| 56 | + | s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack") | |||
| 57 | + | return | |||
| 58 | + | } | |||
| 51 | if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked { | 59 | if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked { | ||
| 52 | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | 60 | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | ||
| 53 | return | 61 | return | ||
| @@ -68,10 +76,17 @@ func (s *surface) Commit(*wayland.Surface) { | |||||
| 68 | if s.role != nil { | 76 | if s.role != nil { | ||
| 69 | s.role(s.current != nil) | 77 | s.role(s.current != nil) | ||
| 70 | } | 78 | } | ||
| 71 | - | if fresh && s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped && s.server.channels.Contents != nil { | 79 | + | id := ports.WindowID(0) |
| 80 | + | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | |||
| 81 | + | id = s.xdg.window.id | |||
| 82 | + | } | |||
| 83 | + | if s.layer != nil && s.layer.mapped { | |||
| 84 | + | id = s.layer.id | |||
| 85 | + | } | |||
| 86 | + | if fresh && id != 0 && s.server.channels.Contents != nil { | |||
| 72 | b := s.current | 87 | b := s.current | ||
| 73 | if state, ok := s.server.buffers[b.Resource]; ok { | 88 | if state, ok := s.server.buffers[b.Resource]; ok { | ||
| 74 | - | if c, ok := state.content(s.xdg.window.id); ok { | 89 | + | if c, ok := state.content(id); ok { |
| 75 | s.server.emitContent(c) | 90 | s.server.emitContent(c) | ||
| 76 | } else { | 91 | } else { | ||
| 77 | state.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated") | 92 | state.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated") | ||
A internal/adapters/wayland/xdgoutput.go +37 -0
| @@ -0,0 +1,37 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 5 | + | "github.com/bnema/purego-libwayland/protocol/xdgoutput" | |
| 6 | + | "github.com/bnema/purego-libwayland/server" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | type xdgOutputManager struct{ width, height int32 } | |
| 10 | + | ||
| 11 | + | func registerXDGOutput(d *server.Display, o Options) error { | |
| 12 | + | return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) { | |
| 13 | + | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{int32(o.OutputWidth), int32(o.OutputHeight)}) | |
| 14 | + | }) | |
| 15 | + | } | |
| 16 | + | func (xdgOutputManager) Destroy(*xdgoutput.ZxdgOutputManagerV1) {} | |
| 17 | + | func (m xdgOutputManager) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, output *wayland.Output) { | |
| 18 | + | x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{}) | |
| 19 | + | if err != nil { | |
| 20 | + | return | |
| 21 | + | } | |
| 22 | + | x.SendLogicalPosition(0, 0) | |
| 23 | + | x.SendLogicalSize(m.width, m.height) | |
| 24 | + | if r.Version() >= 2 { | |
| 25 | + | x.SendName("HEADLESS-1") | |
| 26 | + | x.SendDescription("NeferTTY headless output") | |
| 27 | + | } | |
| 28 | + | if r.Version() < 3 || output == nil || output.Version() < 2 { | |
| 29 | + | x.SendDone() | |
| 30 | + | } else { | |
| 31 | + | output.SendDone() | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | type xdgOutput struct{} | |
| 36 | + | ||
| 37 | + | func (xdgOutput) Destroy(*xdgoutput.ZxdgOutputV1) {} | |
| @@ -0,0 +1,37 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 5 | + | "github.com/bnema/purego-libwayland/protocol/xdgoutput" | |||
| 6 | + | "github.com/bnema/purego-libwayland/server" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | type xdgOutputManager struct{ width, height int32 } | |||
| 10 | + | ||||
| 11 | + | func registerXDGOutput(d *server.Display, o Options) error { | |||
| 12 | + | return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) { | |||
| 13 | + | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{int32(o.OutputWidth), int32(o.OutputHeight)}) | |||
| 14 | + | }) | |||
| 15 | + | } | |||
| 16 | + | func (xdgOutputManager) Destroy(*xdgoutput.ZxdgOutputManagerV1) {} | |||
| 17 | + | func (m xdgOutputManager) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, output *wayland.Output) { | |||
| 18 | + | x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{}) | |||
| 19 | + | if err != nil { | |||
| 20 | + | return | |||
| 21 | + | } | |||
| 22 | + | x.SendLogicalPosition(0, 0) | |||
| 23 | + | x.SendLogicalSize(m.width, m.height) | |||
| 24 | + | if r.Version() >= 2 { | |||
| 25 | + | x.SendName("HEADLESS-1") | |||
| 26 | + | x.SendDescription("NeferTTY headless output") | |||
| 27 | + | } | |||
| 28 | + | if r.Version() < 3 || output == nil || output.Version() < 2 { | |||
| 29 | + | x.SendDone() | |||
| 30 | + | } else { | |||
| 31 | + | output.SendDone() | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | type xdgOutput struct{} | |||
| 36 | + | ||||
| 37 | + | func (xdgOutput) Destroy(*xdgoutput.ZxdgOutputV1) {} | |||
M internal/core/core.go +19 -2
| @@ -40,6 +40,9 @@ type Core struct { | |||
| 40 | 40 | buttons map[uint32]bool | |
| 41 | 41 | cursorX, cursorY float64 | |
| 42 | 42 | seq uint64 | |
| 43 | + | layers []ports.LayerSurface | |
| 44 | + | placed []ports.SceneLayer | |
| 45 | + | layerChanged bool | |
| 43 | 46 | } | |
| 44 | 47 | ||
| 45 | 48 | func keyName(s string) string { | |
| @@ -144,7 +147,7 @@ func (c *Core) command(ctx context.Context, v ports.ClientCommand) error { | |||
| 144 | 147 | } | |
| 145 | 148 | func (c *Core) publish(ctx context.Context) error { | |
| 146 | 149 | c.seq++ | |
| 147 | - | scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Windows: make([]ports.SceneWindow, 0)} | |
| 150 | + | scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), c.placed...)} | |
| 148 | 151 | alive := map[WindowID]bool{} | |
| 149 | 152 | focus, _ := c.ws.Focused() | |
| 150 | 153 | for _, p := range c.ws.Layout() { | |
| @@ -205,6 +208,12 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 205 | 208 | continue | |
| 206 | 209 | } | |
| 207 | 210 | switch v := ev.(type) { | |
| 211 | + | case ports.LayerChanged: | |
| 212 | + | c.layers = append([]ports.LayerSurface(nil), v.Layers...) | |
| 213 | + | c.layerChanged = true | |
| 214 | + | var usable ports.Rect | |
| 215 | + | c.placed, usable = arrangeLayers(c.ws.Output.W, c.ws.Output.H, c.layers) | |
| 216 | + | c.ws.SetUsable(usable) | |
| 208 | 217 | case ports.WindowMapped: | |
| 209 | 218 | c.ws.AddWindow(v.ID) | |
| 210 | 219 | case ports.WindowUnmapped: | |
| @@ -229,8 +238,16 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 229 | 238 | switch v := ev.(type) { | |
| 230 | 239 | case ports.OutputMode: | |
| 231 | 240 | c.ws.SetOutput(v.Width, v.Height) | |
| 241 | + | if c.layerChanged { | |
| 242 | + | var usable ports.Rect | |
| 243 | + | c.placed, usable = arrangeLayers(c.ws.Output.W, c.ws.Output.H, c.layers) | |
| 244 | + | c.ws.SetUsable(usable) | |
| 245 | + | } | |
| 232 | 246 | case ports.OutputUsable: | |
| 233 | - | c.ws.SetUsable(v.Rect) | |
| 247 | + | // Once layer state arrives, its exclusive zones take precedence. | |
| 248 | + | if !c.layerChanged { | |
| 249 | + | c.ws.SetUsable(v.Rect) | |
| 250 | + | } | |
| 234 | 251 | } | |
| 235 | 252 | if err := c.publish(ctx); err != nil { | |
| 236 | 253 | return nil | |
| @@ -40,6 +40,9 @@ type Core struct { | |||||
| 40 | buttons map[uint32]bool | 40 | buttons map[uint32]bool | ||
| 41 | cursorX, cursorY float64 | 41 | cursorX, cursorY float64 | ||
| 42 | seq uint64 | 42 | seq uint64 | ||
| 43 | + | layers []ports.LayerSurface | |||
| 44 | + | placed []ports.SceneLayer | |||
| 45 | + | layerChanged bool | |||
| 43 | } | 46 | } | ||
| 44 | 47 | ||||
| 45 | func keyName(s string) string { | 48 | func keyName(s string) string { | ||
| @@ -144,7 +147,7 @@ func (c *Core) command(ctx context.Context, v ports.ClientCommand) error { | |||||
| 144 | } | 147 | } | ||
| 145 | func (c *Core) publish(ctx context.Context) error { | 148 | func (c *Core) publish(ctx context.Context) error { | ||
| 146 | c.seq++ | 149 | c.seq++ | ||
| 147 | - | scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Windows: make([]ports.SceneWindow, 0)} | 150 | + | scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), c.placed...)} |
| 148 | alive := map[WindowID]bool{} | 151 | alive := map[WindowID]bool{} | ||
| 149 | focus, _ := c.ws.Focused() | 152 | focus, _ := c.ws.Focused() | ||
| 150 | for _, p := range c.ws.Layout() { | 153 | for _, p := range c.ws.Layout() { | ||
| @@ -205,6 +208,12 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 205 | continue | 208 | continue | ||
| 206 | } | 209 | } | ||
| 207 | switch v := ev.(type) { | 210 | switch v := ev.(type) { | ||
| 211 | + | case ports.LayerChanged: | |||
| 212 | + | c.layers = append([]ports.LayerSurface(nil), v.Layers...) | |||
| 213 | + | c.layerChanged = true | |||
| 214 | + | var usable ports.Rect | |||
| 215 | + | c.placed, usable = arrangeLayers(c.ws.Output.W, c.ws.Output.H, c.layers) | |||
| 216 | + | c.ws.SetUsable(usable) | |||
| 208 | case ports.WindowMapped: | 217 | case ports.WindowMapped: | ||
| 209 | c.ws.AddWindow(v.ID) | 218 | c.ws.AddWindow(v.ID) | ||
| 210 | case ports.WindowUnmapped: | 219 | case ports.WindowUnmapped: | ||
| @@ -229,8 +238,16 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 229 | switch v := ev.(type) { | 238 | switch v := ev.(type) { | ||
| 230 | case ports.OutputMode: | 239 | case ports.OutputMode: | ||
| 231 | c.ws.SetOutput(v.Width, v.Height) | 240 | c.ws.SetOutput(v.Width, v.Height) | ||
| 241 | + | if c.layerChanged { | |||
| 242 | + | var usable ports.Rect | |||
| 243 | + | c.placed, usable = arrangeLayers(c.ws.Output.W, c.ws.Output.H, c.layers) | |||
| 244 | + | c.ws.SetUsable(usable) | |||
| 245 | + | } | |||
| 232 | case ports.OutputUsable: | 246 | case ports.OutputUsable: | ||
| 233 | - | c.ws.SetUsable(v.Rect) | 247 | + | // Once layer state arrives, its exclusive zones take precedence. |
| 248 | + | if !c.layerChanged { | |||
| 249 | + | c.ws.SetUsable(v.Rect) | |||
| 250 | + | } | |||
| 234 | } | 251 | } | ||
| 235 | if err := c.publish(ctx); err != nil { | 252 | if err := c.publish(ctx); err != nil { | ||
| 236 | return nil | 253 | return nil | ||
A internal/core/layers.go +75 -0
| @@ -0,0 +1,75 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sort" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // arrangeLayers places exclusive surfaces first, then sorts the resulting scene by layer. | |
| 10 | + | func arrangeLayers(outW, outH int, layers []ports.LayerSurface) (placed []ports.SceneLayer, usable ports.Rect) { | |
| 11 | + | full := ports.Rect{W: max(outW, 0), H: max(outH, 0)} | |
| 12 | + | usable = full | |
| 13 | + | for pass := 0; pass < 2; pass++ { | |
| 14 | + | for _, s := range layers { | |
| 15 | + | if (s.ExclusiveZone > 0) != (pass == 0) { | |
| 16 | + | continue | |
| 17 | + | } | |
| 18 | + | bounds := usable | |
| 19 | + | if s.ExclusiveZone == -1 { | |
| 20 | + | bounds = full | |
| 21 | + | } | |
| 22 | + | top, right, bottom, left := int(s.Margin[0]), int(s.Margin[1]), int(s.Margin[2]), int(s.Margin[3]) | |
| 23 | + | w, h := s.Width, s.Height | |
| 24 | + | x, y := bounds.X+(bounds.W-w)/2, bounds.Y+(bounds.H-h)/2 | |
| 25 | + | switch s.Anchor & (ports.AnchorLeft | ports.AnchorRight) { | |
| 26 | + | case ports.AnchorLeft | ports.AnchorRight: | |
| 27 | + | span := bounds.W - left - right | |
| 28 | + | if w == 0 { | |
| 29 | + | w = span | |
| 30 | + | } | |
| 31 | + | x = bounds.X + left + (span-w)/2 | |
| 32 | + | case ports.AnchorLeft: | |
| 33 | + | x = bounds.X + left | |
| 34 | + | case ports.AnchorRight: | |
| 35 | + | x = bounds.X + bounds.W - w - right | |
| 36 | + | } | |
| 37 | + | switch s.Anchor & (ports.AnchorTop | ports.AnchorBottom) { | |
| 38 | + | case ports.AnchorTop | ports.AnchorBottom: | |
| 39 | + | span := bounds.H - top - bottom | |
| 40 | + | if h == 0 { | |
| 41 | + | h = span | |
| 42 | + | } | |
| 43 | + | y = bounds.Y + top + (span-h)/2 | |
| 44 | + | case ports.AnchorTop: | |
| 45 | + | y = bounds.Y + top | |
| 46 | + | case ports.AnchorBottom: | |
| 47 | + | y = bounds.Y + bounds.H - h - bottom | |
| 48 | + | } | |
| 49 | + | placed = append(placed, ports.SceneLayer{ID: s.ID, Layer: s.Layer, Rect: ports.Rect{X: x, Y: y, W: w, H: h}}) | |
| 50 | + | if s.ExclusiveZone <= 0 { | |
| 51 | + | continue | |
| 52 | + | } | |
| 53 | + | a := s.Anchor | |
| 54 | + | z := int(s.ExclusiveZone) | |
| 55 | + | switch { | |
| 56 | + | case a&ports.AnchorTop != 0 && a&ports.AnchorBottom == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight): | |
| 57 | + | z = min(max(z+top, 0), usable.H) | |
| 58 | + | usable.Y += z | |
| 59 | + | usable.H -= z | |
| 60 | + | case a&ports.AnchorBottom != 0 && a&ports.AnchorTop == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight): | |
| 61 | + | z = min(max(z+bottom, 0), usable.H) | |
| 62 | + | usable.H -= z | |
| 63 | + | case a&ports.AnchorLeft != 0 && a&ports.AnchorRight == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom): | |
| 64 | + | z = min(max(z+left, 0), usable.W) | |
| 65 | + | usable.X += z | |
| 66 | + | usable.W -= z | |
| 67 | + | case a&ports.AnchorRight != 0 && a&ports.AnchorLeft == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom): | |
| 68 | + | z = min(max(z+right, 0), usable.W) | |
| 69 | + | usable.W -= z | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | sort.SliceStable(placed, func(i, j int) bool { return placed[i].Layer < placed[j].Layer }) | |
| 74 | + | return | |
| 75 | + | } | |
| @@ -0,0 +1,75 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "sort" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // arrangeLayers places exclusive surfaces first, then sorts the resulting scene by layer. | |||
| 10 | + | func arrangeLayers(outW, outH int, layers []ports.LayerSurface) (placed []ports.SceneLayer, usable ports.Rect) { | |||
| 11 | + | full := ports.Rect{W: max(outW, 0), H: max(outH, 0)} | |||
| 12 | + | usable = full | |||
| 13 | + | for pass := 0; pass < 2; pass++ { | |||
| 14 | + | for _, s := range layers { | |||
| 15 | + | if (s.ExclusiveZone > 0) != (pass == 0) { | |||
| 16 | + | continue | |||
| 17 | + | } | |||
| 18 | + | bounds := usable | |||
| 19 | + | if s.ExclusiveZone == -1 { | |||
| 20 | + | bounds = full | |||
| 21 | + | } | |||
| 22 | + | top, right, bottom, left := int(s.Margin[0]), int(s.Margin[1]), int(s.Margin[2]), int(s.Margin[3]) | |||
| 23 | + | w, h := s.Width, s.Height | |||
| 24 | + | x, y := bounds.X+(bounds.W-w)/2, bounds.Y+(bounds.H-h)/2 | |||
| 25 | + | switch s.Anchor & (ports.AnchorLeft | ports.AnchorRight) { | |||
| 26 | + | case ports.AnchorLeft | ports.AnchorRight: | |||
| 27 | + | span := bounds.W - left - right | |||
| 28 | + | if w == 0 { | |||
| 29 | + | w = span | |||
| 30 | + | } | |||
| 31 | + | x = bounds.X + left + (span-w)/2 | |||
| 32 | + | case ports.AnchorLeft: | |||
| 33 | + | x = bounds.X + left | |||
| 34 | + | case ports.AnchorRight: | |||
| 35 | + | x = bounds.X + bounds.W - w - right | |||
| 36 | + | } | |||
| 37 | + | switch s.Anchor & (ports.AnchorTop | ports.AnchorBottom) { | |||
| 38 | + | case ports.AnchorTop | ports.AnchorBottom: | |||
| 39 | + | span := bounds.H - top - bottom | |||
| 40 | + | if h == 0 { | |||
| 41 | + | h = span | |||
| 42 | + | } | |||
| 43 | + | y = bounds.Y + top + (span-h)/2 | |||
| 44 | + | case ports.AnchorTop: | |||
| 45 | + | y = bounds.Y + top | |||
| 46 | + | case ports.AnchorBottom: | |||
| 47 | + | y = bounds.Y + bounds.H - h - bottom | |||
| 48 | + | } | |||
| 49 | + | placed = append(placed, ports.SceneLayer{ID: s.ID, Layer: s.Layer, Rect: ports.Rect{X: x, Y: y, W: w, H: h}}) | |||
| 50 | + | if s.ExclusiveZone <= 0 { | |||
| 51 | + | continue | |||
| 52 | + | } | |||
| 53 | + | a := s.Anchor | |||
| 54 | + | z := int(s.ExclusiveZone) | |||
| 55 | + | switch { | |||
| 56 | + | case a&ports.AnchorTop != 0 && a&ports.AnchorBottom == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight): | |||
| 57 | + | z = min(max(z+top, 0), usable.H) | |||
| 58 | + | usable.Y += z | |||
| 59 | + | usable.H -= z | |||
| 60 | + | case a&ports.AnchorBottom != 0 && a&ports.AnchorTop == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight): | |||
| 61 | + | z = min(max(z+bottom, 0), usable.H) | |||
| 62 | + | usable.H -= z | |||
| 63 | + | case a&ports.AnchorLeft != 0 && a&ports.AnchorRight == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom): | |||
| 64 | + | z = min(max(z+left, 0), usable.W) | |||
| 65 | + | usable.X += z | |||
| 66 | + | usable.W -= z | |||
| 67 | + | case a&ports.AnchorRight != 0 && a&ports.AnchorLeft == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom): | |||
| 68 | + | z = min(max(z+right, 0), usable.W) | |||
| 69 | + | usable.W -= z | |||
| 70 | + | } | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | sort.SliceStable(placed, func(i, j int) bool { return placed[i].Layer < placed[j].Layer }) | |||
| 74 | + | return | |||
| 75 | + | } | |||
A internal/core/layers_test.go +110 -0
| @@ -0,0 +1,110 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/nefertty/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestArrangeLayers(t *testing.T) { | |
| 12 | + | bar := ports.LayerSurface{ID: 1, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30} | |
| 13 | + | tests := []struct { | |
| 14 | + | name string | |
| 15 | + | layers []ports.LayerSurface | |
| 16 | + | rects []ports.Rect | |
| 17 | + | usable ports.Rect | |
| 18 | + | }{ | |
| 19 | + | {"top", []ports.LayerSurface{bar}, []ports.Rect{{W: 1920, H: 30}}, ports.Rect{Y: 30, W: 1920, H: 1050}}, | |
| 20 | + | {"bottom margin", []ports.LayerSurface{{Anchor: ports.AnchorBottom, Width: 100, Height: 20, ExclusiveZone: 20, Margin: [4]int32{0, 0, 5, 0}}}, []ports.Rect{{X: 910, Y: 1055, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1055}}, | |
| 21 | + | {"left dock", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorTop | ports.AnchorBottom, Width: 40, Height: 1080, ExclusiveZone: 40}}, []ports.Rect{{W: 40, H: 1080}}, ports.Rect{X: 40, W: 1880, H: 1080}}, | |
| 22 | + | {"center", []ports.LayerSurface{{Width: 100, Height: 40, Layer: ports.LayerOverlay}}, []ports.Rect{{X: 910, Y: 520, W: 100, H: 40}}, ports.Rect{W: 1920, H: 1080}}, | |
| 23 | + | {"opposing horizontal anchors with margins", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorRight, Width: 100, Height: 20, Margin: [4]int32{0, 30, 0, 10}}}, []ports.Rect{{X: 900, Y: 530, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}}, | |
| 24 | + | {"top only", []ports.LayerSurface{{Anchor: ports.AnchorTop, Width: 100, Height: 20, Margin: [4]int32{7, 0, 0, 0}}}, []ports.Rect{{X: 910, Y: 7, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}}, | |
| 25 | + | {"full span between margins", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorRight | ports.AnchorTop, Height: 20, Margin: [4]int32{7, 30, 0, 10}}}, []ports.Rect{{X: 10, Y: 7, W: 1880, H: 20}}, ports.Rect{W: 1920, H: 1080}}, | |
| 26 | + | {"opposing vertical anchors with margins", []ports.LayerSurface{{Anchor: ports.AnchorTop | ports.AnchorBottom, Width: 100, Height: 20, Margin: [4]int32{10, 0, 30, 0}}}, []ports.Rect{{X: 910, Y: 520, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}}, | |
| 27 | + | {"minus one", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: ports.AnchorTop, Width: 100, Height: 20, ExclusiveZone: -1}}, []ports.Rect{{W: 1920, H: 30}, {X: 910, W: 100, H: 20}}, ports.Rect{Y: 30, W: 1920, H: 1050}}, | |
| 28 | + | {"stack", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: bar.Anchor, Width: 1920, Height: 20, ExclusiveZone: 20}}, []ports.Rect{{W: 1920, H: 30}, {Y: 30, W: 1920, H: 20}}, ports.Rect{Y: 50, W: 1920, H: 1030}}, | |
| 29 | + | } | |
| 30 | + | for _, tt := range tests { | |
| 31 | + | t.Run(tt.name, func(t *testing.T) { | |
| 32 | + | got, u := arrangeLayers(1920, 1080, tt.layers) | |
| 33 | + | if u != tt.usable || len(got) != len(tt.rects) { | |
| 34 | + | t.Fatalf("got %v %v", got, u) | |
| 35 | + | } | |
| 36 | + | for i, r := range tt.rects { | |
| 37 | + | if got[i].Rect != r { | |
| 38 | + | t.Fatalf("rect %d: %v want %v", i, got[i].Rect, r) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | }) | |
| 42 | + | } | |
| 43 | + | layers := []ports.LayerSurface{{ID: 1, Layer: ports.LayerOverlay}, {ID: 2, Layer: ports.LayerBottom}, {ID: 3, Layer: ports.LayerBackground}, {ID: 4, Layer: ports.LayerTop}, {ID: 5, Layer: ports.LayerBottom}} | |
| 44 | + | got, _ := arrangeLayers(100, 100, layers) | |
| 45 | + | for i, id := range []ports.WindowID{3, 2, 5, 4, 1} { | |
| 46 | + | if got[i].ID != id { | |
| 47 | + | t.Fatalf("order: %v", got) | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func TestLayerChangedChannels(t *testing.T) { | |
| 53 | + | client := make(chan ports.ClientEvent) | |
| 54 | + | output := make(chan ports.OutputEvent) | |
| 55 | + | commands := make(chan ports.ClientCommand, 16) | |
| 56 | + | scenes := make(chan ports.Scene, 1) | |
| 57 | + | cfg := ports.Config{} | |
| 58 | + | cfg.Keyboard.CmdKey = "super" | |
| 59 | + | cfg.Layout.DefaultColumnWidth = "1/2" | |
| 60 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}) | |
| 61 | + | if err != nil { | |
| 62 | + | t.Fatal(err) | |
| 63 | + | } | |
| 64 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 65 | + | defer cancel() | |
| 66 | + | done := make(chan struct{}) | |
| 67 | + | go func() { defer close(done); _ = c.Run(ctx) }() | |
| 68 | + | recv := func() ports.Scene { | |
| 69 | + | t.Helper() | |
| 70 | + | select { | |
| 71 | + | case s := <-scenes: | |
| 72 | + | return s | |
| 73 | + | case <-time.After(time.Second): | |
| 74 | + | t.Fatal("scene timeout") | |
| 75 | + | return ports.Scene{} | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | output <- ports.OutputMode{Width: 1920, Height: 1080} | |
| 79 | + | recv() | |
| 80 | + | client <- ports.WindowMapped{ID: 1} | |
| 81 | + | before := recv() | |
| 82 | + | for len(commands) > 0 { | |
| 83 | + | <-commands | |
| 84 | + | } | |
| 85 | + | bar := ports.LayerSurface{ID: 2, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30} | |
| 86 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |
| 87 | + | scene := recv() | |
| 88 | + | if len(scene.Layers) != 1 || scene.Layers[0].Rect != (ports.Rect{W: 1920, H: 30}) || len(scene.Windows) != 1 || scene.Windows[0].Rect.Y < 30 { | |
| 89 | + | t.Fatalf("scene: %v", scene) | |
| 90 | + | } | |
| 91 | + | if scene.Windows[0].Rect.H != before.Windows[0].Rect.H-30 { | |
| 92 | + | t.Fatalf("height: %v vs %v", scene.Windows, before.Windows) | |
| 93 | + | } | |
| 94 | + | select { | |
| 95 | + | case v := <-commands: | |
| 96 | + | cfg, ok := v.(ports.ConfigureWindow) | |
| 97 | + | if !ok || cfg.ID != 1 || cfg.Height != scene.Windows[0].Rect.H { | |
| 98 | + | t.Fatalf("configure: %v", v) | |
| 99 | + | } | |
| 100 | + | case <-time.After(time.Second): | |
| 101 | + | t.Fatal("configure timeout") | |
| 102 | + | } | |
| 103 | + | scene.Layers[0].Rect.Y = 99 | |
| 104 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |
| 105 | + | if next := recv(); next.Layers[0].Rect.Y != 0 { | |
| 106 | + | t.Fatalf("scene alias: %v", next) | |
| 107 | + | } | |
| 108 | + | cancel() | |
| 109 | + | <-done | |
| 110 | + | } | |
| @@ -0,0 +1,110 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestArrangeLayers(t *testing.T) { | |||
| 12 | + | bar := ports.LayerSurface{ID: 1, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30} | |||
| 13 | + | tests := []struct { | |||
| 14 | + | name string | |||
| 15 | + | layers []ports.LayerSurface | |||
| 16 | + | rects []ports.Rect | |||
| 17 | + | usable ports.Rect | |||
| 18 | + | }{ | |||
| 19 | + | {"top", []ports.LayerSurface{bar}, []ports.Rect{{W: 1920, H: 30}}, ports.Rect{Y: 30, W: 1920, H: 1050}}, | |||
| 20 | + | {"bottom margin", []ports.LayerSurface{{Anchor: ports.AnchorBottom, Width: 100, Height: 20, ExclusiveZone: 20, Margin: [4]int32{0, 0, 5, 0}}}, []ports.Rect{{X: 910, Y: 1055, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1055}}, | |||
| 21 | + | {"left dock", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorTop | ports.AnchorBottom, Width: 40, Height: 1080, ExclusiveZone: 40}}, []ports.Rect{{W: 40, H: 1080}}, ports.Rect{X: 40, W: 1880, H: 1080}}, | |||
| 22 | + | {"center", []ports.LayerSurface{{Width: 100, Height: 40, Layer: ports.LayerOverlay}}, []ports.Rect{{X: 910, Y: 520, W: 100, H: 40}}, ports.Rect{W: 1920, H: 1080}}, | |||
| 23 | + | {"opposing horizontal anchors with margins", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorRight, Width: 100, Height: 20, Margin: [4]int32{0, 30, 0, 10}}}, []ports.Rect{{X: 900, Y: 530, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}}, | |||
| 24 | + | {"top only", []ports.LayerSurface{{Anchor: ports.AnchorTop, Width: 100, Height: 20, Margin: [4]int32{7, 0, 0, 0}}}, []ports.Rect{{X: 910, Y: 7, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}}, | |||
| 25 | + | {"full span between margins", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorRight | ports.AnchorTop, Height: 20, Margin: [4]int32{7, 30, 0, 10}}}, []ports.Rect{{X: 10, Y: 7, W: 1880, H: 20}}, ports.Rect{W: 1920, H: 1080}}, | |||
| 26 | + | {"opposing vertical anchors with margins", []ports.LayerSurface{{Anchor: ports.AnchorTop | ports.AnchorBottom, Width: 100, Height: 20, Margin: [4]int32{10, 0, 30, 0}}}, []ports.Rect{{X: 910, Y: 520, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}}, | |||
| 27 | + | {"minus one", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: ports.AnchorTop, Width: 100, Height: 20, ExclusiveZone: -1}}, []ports.Rect{{W: 1920, H: 30}, {X: 910, W: 100, H: 20}}, ports.Rect{Y: 30, W: 1920, H: 1050}}, | |||
| 28 | + | {"stack", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: bar.Anchor, Width: 1920, Height: 20, ExclusiveZone: 20}}, []ports.Rect{{W: 1920, H: 30}, {Y: 30, W: 1920, H: 20}}, ports.Rect{Y: 50, W: 1920, H: 1030}}, | |||
| 29 | + | } | |||
| 30 | + | for _, tt := range tests { | |||
| 31 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 32 | + | got, u := arrangeLayers(1920, 1080, tt.layers) | |||
| 33 | + | if u != tt.usable || len(got) != len(tt.rects) { | |||
| 34 | + | t.Fatalf("got %v %v", got, u) | |||
| 35 | + | } | |||
| 36 | + | for i, r := range tt.rects { | |||
| 37 | + | if got[i].Rect != r { | |||
| 38 | + | t.Fatalf("rect %d: %v want %v", i, got[i].Rect, r) | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | }) | |||
| 42 | + | } | |||
| 43 | + | layers := []ports.LayerSurface{{ID: 1, Layer: ports.LayerOverlay}, {ID: 2, Layer: ports.LayerBottom}, {ID: 3, Layer: ports.LayerBackground}, {ID: 4, Layer: ports.LayerTop}, {ID: 5, Layer: ports.LayerBottom}} | |||
| 44 | + | got, _ := arrangeLayers(100, 100, layers) | |||
| 45 | + | for i, id := range []ports.WindowID{3, 2, 5, 4, 1} { | |||
| 46 | + | if got[i].ID != id { | |||
| 47 | + | t.Fatalf("order: %v", got) | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func TestLayerChangedChannels(t *testing.T) { | |||
| 53 | + | client := make(chan ports.ClientEvent) | |||
| 54 | + | output := make(chan ports.OutputEvent) | |||
| 55 | + | commands := make(chan ports.ClientCommand, 16) | |||
| 56 | + | scenes := make(chan ports.Scene, 1) | |||
| 57 | + | cfg := ports.Config{} | |||
| 58 | + | cfg.Keyboard.CmdKey = "super" | |||
| 59 | + | cfg.Layout.DefaultColumnWidth = "1/2" | |||
| 60 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}) | |||
| 61 | + | if err != nil { | |||
| 62 | + | t.Fatal(err) | |||
| 63 | + | } | |||
| 64 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 65 | + | defer cancel() | |||
| 66 | + | done := make(chan struct{}) | |||
| 67 | + | go func() { defer close(done); _ = c.Run(ctx) }() | |||
| 68 | + | recv := func() ports.Scene { | |||
| 69 | + | t.Helper() | |||
| 70 | + | select { | |||
| 71 | + | case s := <-scenes: | |||
| 72 | + | return s | |||
| 73 | + | case <-time.After(time.Second): | |||
| 74 | + | t.Fatal("scene timeout") | |||
| 75 | + | return ports.Scene{} | |||
| 76 | + | } | |||
| 77 | + | } | |||
| 78 | + | output <- ports.OutputMode{Width: 1920, Height: 1080} | |||
| 79 | + | recv() | |||
| 80 | + | client <- ports.WindowMapped{ID: 1} | |||
| 81 | + | before := recv() | |||
| 82 | + | for len(commands) > 0 { | |||
| 83 | + | <-commands | |||
| 84 | + | } | |||
| 85 | + | bar := ports.LayerSurface{ID: 2, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30} | |||
| 86 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |||
| 87 | + | scene := recv() | |||
| 88 | + | if len(scene.Layers) != 1 || scene.Layers[0].Rect != (ports.Rect{W: 1920, H: 30}) || len(scene.Windows) != 1 || scene.Windows[0].Rect.Y < 30 { | |||
| 89 | + | t.Fatalf("scene: %v", scene) | |||
| 90 | + | } | |||
| 91 | + | if scene.Windows[0].Rect.H != before.Windows[0].Rect.H-30 { | |||
| 92 | + | t.Fatalf("height: %v vs %v", scene.Windows, before.Windows) | |||
| 93 | + | } | |||
| 94 | + | select { | |||
| 95 | + | case v := <-commands: | |||
| 96 | + | cfg, ok := v.(ports.ConfigureWindow) | |||
| 97 | + | if !ok || cfg.ID != 1 || cfg.Height != scene.Windows[0].Rect.H { | |||
| 98 | + | t.Fatalf("configure: %v", v) | |||
| 99 | + | } | |||
| 100 | + | case <-time.After(time.Second): | |||
| 101 | + | t.Fatal("configure timeout") | |||
| 102 | + | } | |||
| 103 | + | scene.Layers[0].Rect.Y = 99 | |||
| 104 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |||
| 105 | + | if next := recv(); next.Layers[0].Rect.Y != 0 { | |||
| 106 | + | t.Fatalf("scene alias: %v", next) | |||
| 107 | + | } | |||
| 108 | + | cancel() | |||
| 109 | + | <-done | |||
| 110 | + | } | |||
M internal/ports/messages.go +47 -0
| @@ -24,6 +24,43 @@ type WindowFullscreenRequest struct { | |||
| 24 | 24 | ||
| 25 | 25 | func (WindowFullscreenRequest) clientEvent() {} | |
| 26 | 26 | ||
| 27 | + | // Layer is a wlr-layer-shell stacking layer. | |
| 28 | + | type Layer uint32 | |
| 29 | + | ||
| 30 | + | const ( | |
| 31 | + | LayerBackground Layer = iota | |
| 32 | + | LayerBottom | |
| 33 | + | LayerTop | |
| 34 | + | LayerOverlay | |
| 35 | + | ) | |
| 36 | + | ||
| 37 | + | // Anchor edges, as in zwlr_layer_surface_v1.anchor. | |
| 38 | + | const ( | |
| 39 | + | AnchorTop uint32 = 1 | |
| 40 | + | AnchorBottom uint32 = 2 | |
| 41 | + | AnchorLeft uint32 = 4 | |
| 42 | + | AnchorRight uint32 = 8 | |
| 43 | + | ) | |
| 44 | + | ||
| 45 | + | // LayerSurface is the committed state of one mapped layer surface. Width and | |
| 46 | + | // Height are the committed buffer size. Margin is top, right, bottom, left. | |
| 47 | + | // IDs share the WindowID space with windows and never collide. | |
| 48 | + | type LayerSurface struct { | |
| 49 | + | ID WindowID | |
| 50 | + | Layer Layer | |
| 51 | + | Anchor uint32 | |
| 52 | + | ExclusiveZone int32 | |
| 53 | + | Margin [4]int32 | |
| 54 | + | Width, Height int | |
| 55 | + | Namespace string | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // LayerChanged carries wayland → core the full list of mapped layer surfaces, | |
| 59 | + | // sent whenever a layer surface maps, unmaps or commits new layer state. | |
| 60 | + | type LayerChanged struct{ Layers []LayerSurface } | |
| 61 | + | ||
| 62 | + | func (LayerChanged) clientEvent() {} | |
| 63 | + | ||
| 27 | 64 | // Mods carries input → core modifier flags. | |
| 28 | 65 | type Mods uint8 | |
| 29 | 66 | ||
| @@ -151,6 +188,16 @@ type Scene struct { | |||
| 151 | 188 | OutputWidth, OutputHeight int | |
| 152 | 189 | Background string | |
| 153 | 190 | Windows []SceneWindow | |
| 191 | + | // Layers are drawn in slice order: background and bottom before windows, | |
| 192 | + | // top and overlay after. A fullscreen window covers bottom and top. | |
| 193 | + | Layers []SceneLayer | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | // SceneLayer carries core → renderer layer surface placement. | |
| 197 | + | type SceneLayer struct { | |
| 198 | + | ID WindowID | |
| 199 | + | Layer Layer | |
| 200 | + | Rect Rect | |
| 154 | 201 | } | |
| 155 | 202 | ||
| 156 | 203 | // SurfaceContent carries wayland → output the latest committed pixels of a | |
| @@ -24,6 +24,43 @@ type WindowFullscreenRequest struct { | |||||
| 24 | 24 | ||||
| 25 | func (WindowFullscreenRequest) clientEvent() {} | 25 | func (WindowFullscreenRequest) clientEvent() {} | ||
| 26 | 26 | ||||
| 27 | + | // Layer is a wlr-layer-shell stacking layer. | |||
| 28 | + | type Layer uint32 | |||
| 29 | + | ||||
| 30 | + | const ( | |||
| 31 | + | LayerBackground Layer = iota | |||
| 32 | + | LayerBottom | |||
| 33 | + | LayerTop | |||
| 34 | + | LayerOverlay | |||
| 35 | + | ) | |||
| 36 | + | ||||
| 37 | + | // Anchor edges, as in zwlr_layer_surface_v1.anchor. | |||
| 38 | + | const ( | |||
| 39 | + | AnchorTop uint32 = 1 | |||
| 40 | + | AnchorBottom uint32 = 2 | |||
| 41 | + | AnchorLeft uint32 = 4 | |||
| 42 | + | AnchorRight uint32 = 8 | |||
| 43 | + | ) | |||
| 44 | + | ||||
| 45 | + | // LayerSurface is the committed state of one mapped layer surface. Width and | |||
| 46 | + | // Height are the committed buffer size. Margin is top, right, bottom, left. | |||
| 47 | + | // IDs share the WindowID space with windows and never collide. | |||
| 48 | + | type LayerSurface struct { | |||
| 49 | + | ID WindowID | |||
| 50 | + | Layer Layer | |||
| 51 | + | Anchor uint32 | |||
| 52 | + | ExclusiveZone int32 | |||
| 53 | + | Margin [4]int32 | |||
| 54 | + | Width, Height int | |||
| 55 | + | Namespace string | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // LayerChanged carries wayland → core the full list of mapped layer surfaces, | |||
| 59 | + | // sent whenever a layer surface maps, unmaps or commits new layer state. | |||
| 60 | + | type LayerChanged struct{ Layers []LayerSurface } | |||
| 61 | + | ||||
| 62 | + | func (LayerChanged) clientEvent() {} | |||
| 63 | + | ||||
| 27 | // Mods carries input → core modifier flags. | 64 | // Mods carries input → core modifier flags. | ||
| 28 | type Mods uint8 | 65 | type Mods uint8 | ||
| 29 | 66 | ||||
| @@ -151,6 +188,16 @@ type Scene struct { | |||||
| 151 | OutputWidth, OutputHeight int | 188 | OutputWidth, OutputHeight int | ||
| 152 | Background string | 189 | Background string | ||
| 153 | Windows []SceneWindow | 190 | Windows []SceneWindow | ||
| 191 | + | // Layers are drawn in slice order: background and bottom before windows, | |||
| 192 | + | // top and overlay after. A fullscreen window covers bottom and top. | |||
| 193 | + | Layers []SceneLayer | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | // SceneLayer carries core → renderer layer surface placement. | |||
| 197 | + | type SceneLayer struct { | |||
| 198 | + | ID WindowID | |||
| 199 | + | Layer Layer | |||
| 200 | + | Rect Rect | |||
| 154 | } | 201 | } | ||
| 155 | 202 | ||||
| 156 | // SurfaceContent carries wayland → output the latest committed pixels of a | 203 | // SurfaceContent carries wayland → output the latest committed pixels of a | ||