feat(core): arrange layer surfaces and exclusive zones
- commit
- 567a2850f8e9619d62105bd2f238098754de92f7
- author
- brice · · 2026-09-23 16:08 UTC
- parent
- ffa1efa
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +185 -2
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 +60 -0
| @@ -0,0 +1,60 @@ | |||
| 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 | + | x, y := bounds.X+(bounds.W-s.Width)/2, bounds.Y+(bounds.H-s.Height)/2 | |
| 24 | + | if s.Anchor&ports.AnchorLeft != 0 { | |
| 25 | + | x = bounds.X + left | |
| 26 | + | } else if s.Anchor&ports.AnchorRight != 0 { | |
| 27 | + | x = bounds.X + bounds.W - s.Width - right | |
| 28 | + | } | |
| 29 | + | if s.Anchor&ports.AnchorTop != 0 { | |
| 30 | + | y = bounds.Y + top | |
| 31 | + | } else if s.Anchor&ports.AnchorBottom != 0 { | |
| 32 | + | y = bounds.Y + bounds.H - s.Height - bottom | |
| 33 | + | } | |
| 34 | + | placed = append(placed, ports.SceneLayer{ID: s.ID, Layer: s.Layer, Rect: ports.Rect{X: x, Y: y, W: s.Width, H: s.Height}}) | |
| 35 | + | if s.ExclusiveZone <= 0 { | |
| 36 | + | continue | |
| 37 | + | } | |
| 38 | + | a := s.Anchor | |
| 39 | + | z := int(s.ExclusiveZone) | |
| 40 | + | switch { | |
| 41 | + | case a&ports.AnchorTop != 0 && a&ports.AnchorBottom == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight): | |
| 42 | + | z = min(max(z+top, 0), usable.H) | |
| 43 | + | usable.Y += z | |
| 44 | + | usable.H -= z | |
| 45 | + | case a&ports.AnchorBottom != 0 && a&ports.AnchorTop == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight): | |
| 46 | + | z = min(max(z+bottom, 0), usable.H) | |
| 47 | + | usable.H -= z | |
| 48 | + | case a&ports.AnchorLeft != 0 && a&ports.AnchorRight == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom): | |
| 49 | + | z = min(max(z+left, 0), usable.W) | |
| 50 | + | usable.X += z | |
| 51 | + | usable.W -= z | |
| 52 | + | case a&ports.AnchorRight != 0 && a&ports.AnchorLeft == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom): | |
| 53 | + | z = min(max(z+right, 0), usable.W) | |
| 54 | + | usable.W -= z | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | sort.SliceStable(placed, func(i, j int) bool { return placed[i].Layer < placed[j].Layer }) | |
| 59 | + | return | |
| 60 | + | } | |
| @@ -0,0 +1,60 @@ | |||||
| 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 | + | x, y := bounds.X+(bounds.W-s.Width)/2, bounds.Y+(bounds.H-s.Height)/2 | |||
| 24 | + | if s.Anchor&ports.AnchorLeft != 0 { | |||
| 25 | + | x = bounds.X + left | |||
| 26 | + | } else if s.Anchor&ports.AnchorRight != 0 { | |||
| 27 | + | x = bounds.X + bounds.W - s.Width - right | |||
| 28 | + | } | |||
| 29 | + | if s.Anchor&ports.AnchorTop != 0 { | |||
| 30 | + | y = bounds.Y + top | |||
| 31 | + | } else if s.Anchor&ports.AnchorBottom != 0 { | |||
| 32 | + | y = bounds.Y + bounds.H - s.Height - bottom | |||
| 33 | + | } | |||
| 34 | + | placed = append(placed, ports.SceneLayer{ID: s.ID, Layer: s.Layer, Rect: ports.Rect{X: x, Y: y, W: s.Width, H: s.Height}}) | |||
| 35 | + | if s.ExclusiveZone <= 0 { | |||
| 36 | + | continue | |||
| 37 | + | } | |||
| 38 | + | a := s.Anchor | |||
| 39 | + | z := int(s.ExclusiveZone) | |||
| 40 | + | switch { | |||
| 41 | + | case a&ports.AnchorTop != 0 && a&ports.AnchorBottom == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight): | |||
| 42 | + | z = min(max(z+top, 0), usable.H) | |||
| 43 | + | usable.Y += z | |||
| 44 | + | usable.H -= z | |||
| 45 | + | case a&ports.AnchorBottom != 0 && a&ports.AnchorTop == 0 && (a&(ports.AnchorLeft|ports.AnchorRight) == 0 || a&(ports.AnchorLeft|ports.AnchorRight) == ports.AnchorLeft|ports.AnchorRight): | |||
| 46 | + | z = min(max(z+bottom, 0), usable.H) | |||
| 47 | + | usable.H -= z | |||
| 48 | + | case a&ports.AnchorLeft != 0 && a&ports.AnchorRight == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom): | |||
| 49 | + | z = min(max(z+left, 0), usable.W) | |||
| 50 | + | usable.X += z | |||
| 51 | + | usable.W -= z | |||
| 52 | + | case a&ports.AnchorRight != 0 && a&ports.AnchorLeft == 0 && (a&(ports.AnchorTop|ports.AnchorBottom) == 0 || a&(ports.AnchorTop|ports.AnchorBottom) == ports.AnchorTop|ports.AnchorBottom): | |||
| 53 | + | z = min(max(z+right, 0), usable.W) | |||
| 54 | + | usable.W -= z | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | } | |||
| 58 | + | sort.SliceStable(placed, func(i, j int) bool { return placed[i].Layer < placed[j].Layer }) | |||
| 59 | + | return | |||
| 60 | + | } | |||
A internal/core/layers_test.go +106 -0
| @@ -0,0 +1,106 @@ | |||
| 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 | + | {"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}}, | |
| 24 | + | {"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}}, | |
| 25 | + | } | |
| 26 | + | for _, tt := range tests { | |
| 27 | + | t.Run(tt.name, func(t *testing.T) { | |
| 28 | + | got, u := arrangeLayers(1920, 1080, tt.layers) | |
| 29 | + | if u != tt.usable || len(got) != len(tt.rects) { | |
| 30 | + | t.Fatalf("got %v %v", got, u) | |
| 31 | + | } | |
| 32 | + | for i, r := range tt.rects { | |
| 33 | + | if got[i].Rect != r { | |
| 34 | + | t.Fatalf("rect %d: %v want %v", i, got[i].Rect, r) | |
| 35 | + | } | |
| 36 | + | } | |
| 37 | + | }) | |
| 38 | + | } | |
| 39 | + | 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}} | |
| 40 | + | got, _ := arrangeLayers(100, 100, layers) | |
| 41 | + | for i, id := range []ports.WindowID{3, 2, 5, 4, 1} { | |
| 42 | + | if got[i].ID != id { | |
| 43 | + | t.Fatalf("order: %v", got) | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func TestLayerChangedChannels(t *testing.T) { | |
| 49 | + | client := make(chan ports.ClientEvent) | |
| 50 | + | output := make(chan ports.OutputEvent) | |
| 51 | + | commands := make(chan ports.ClientCommand, 16) | |
| 52 | + | scenes := make(chan ports.Scene, 1) | |
| 53 | + | cfg := ports.Config{} | |
| 54 | + | cfg.Keyboard.CmdKey = "super" | |
| 55 | + | cfg.Layout.DefaultColumnWidth = "1/2" | |
| 56 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}) | |
| 57 | + | if err != nil { | |
| 58 | + | t.Fatal(err) | |
| 59 | + | } | |
| 60 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 61 | + | defer cancel() | |
| 62 | + | done := make(chan struct{}) | |
| 63 | + | go func() { defer close(done); _ = c.Run(ctx) }() | |
| 64 | + | recv := func() ports.Scene { | |
| 65 | + | t.Helper() | |
| 66 | + | select { | |
| 67 | + | case s := <-scenes: | |
| 68 | + | return s | |
| 69 | + | case <-time.After(time.Second): | |
| 70 | + | t.Fatal("scene timeout") | |
| 71 | + | return ports.Scene{} | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | output <- ports.OutputMode{Width: 1920, Height: 1080} | |
| 75 | + | recv() | |
| 76 | + | client <- ports.WindowMapped{ID: 1} | |
| 77 | + | before := recv() | |
| 78 | + | for len(commands) > 0 { | |
| 79 | + | <-commands | |
| 80 | + | } | |
| 81 | + | bar := ports.LayerSurface{ID: 2, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30} | |
| 82 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |
| 83 | + | scene := recv() | |
| 84 | + | 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 { | |
| 85 | + | t.Fatalf("scene: %v", scene) | |
| 86 | + | } | |
| 87 | + | if scene.Windows[0].Rect.H != before.Windows[0].Rect.H-30 { | |
| 88 | + | t.Fatalf("height: %v vs %v", scene.Windows, before.Windows) | |
| 89 | + | } | |
| 90 | + | select { | |
| 91 | + | case v := <-commands: | |
| 92 | + | cfg, ok := v.(ports.ConfigureWindow) | |
| 93 | + | if !ok || cfg.ID != 1 || cfg.Height != scene.Windows[0].Rect.H { | |
| 94 | + | t.Fatalf("configure: %v", v) | |
| 95 | + | } | |
| 96 | + | case <-time.After(time.Second): | |
| 97 | + | t.Fatal("configure timeout") | |
| 98 | + | } | |
| 99 | + | scene.Layers[0].Rect.Y = 99 | |
| 100 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |
| 101 | + | if next := recv(); next.Layers[0].Rect.Y != 0 { | |
| 102 | + | t.Fatalf("scene alias: %v", next) | |
| 103 | + | } | |
| 104 | + | cancel() | |
| 105 | + | <-done | |
| 106 | + | } | |
| @@ -0,0 +1,106 @@ | |||||
| 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 | + | {"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}}, | |||
| 24 | + | {"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}}, | |||
| 25 | + | } | |||
| 26 | + | for _, tt := range tests { | |||
| 27 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 28 | + | got, u := arrangeLayers(1920, 1080, tt.layers) | |||
| 29 | + | if u != tt.usable || len(got) != len(tt.rects) { | |||
| 30 | + | t.Fatalf("got %v %v", got, u) | |||
| 31 | + | } | |||
| 32 | + | for i, r := range tt.rects { | |||
| 33 | + | if got[i].Rect != r { | |||
| 34 | + | t.Fatalf("rect %d: %v want %v", i, got[i].Rect, r) | |||
| 35 | + | } | |||
| 36 | + | } | |||
| 37 | + | }) | |||
| 38 | + | } | |||
| 39 | + | 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}} | |||
| 40 | + | got, _ := arrangeLayers(100, 100, layers) | |||
| 41 | + | for i, id := range []ports.WindowID{3, 2, 5, 4, 1} { | |||
| 42 | + | if got[i].ID != id { | |||
| 43 | + | t.Fatalf("order: %v", got) | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func TestLayerChangedChannels(t *testing.T) { | |||
| 49 | + | client := make(chan ports.ClientEvent) | |||
| 50 | + | output := make(chan ports.OutputEvent) | |||
| 51 | + | commands := make(chan ports.ClientCommand, 16) | |||
| 52 | + | scenes := make(chan ports.Scene, 1) | |||
| 53 | + | cfg := ports.Config{} | |||
| 54 | + | cfg.Keyboard.CmdKey = "super" | |||
| 55 | + | cfg.Layout.DefaultColumnWidth = "1/2" | |||
| 56 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}) | |||
| 57 | + | if err != nil { | |||
| 58 | + | t.Fatal(err) | |||
| 59 | + | } | |||
| 60 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 61 | + | defer cancel() | |||
| 62 | + | done := make(chan struct{}) | |||
| 63 | + | go func() { defer close(done); _ = c.Run(ctx) }() | |||
| 64 | + | recv := func() ports.Scene { | |||
| 65 | + | t.Helper() | |||
| 66 | + | select { | |||
| 67 | + | case s := <-scenes: | |||
| 68 | + | return s | |||
| 69 | + | case <-time.After(time.Second): | |||
| 70 | + | t.Fatal("scene timeout") | |||
| 71 | + | return ports.Scene{} | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | output <- ports.OutputMode{Width: 1920, Height: 1080} | |||
| 75 | + | recv() | |||
| 76 | + | client <- ports.WindowMapped{ID: 1} | |||
| 77 | + | before := recv() | |||
| 78 | + | for len(commands) > 0 { | |||
| 79 | + | <-commands | |||
| 80 | + | } | |||
| 81 | + | bar := ports.LayerSurface{ID: 2, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30} | |||
| 82 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |||
| 83 | + | scene := recv() | |||
| 84 | + | 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 { | |||
| 85 | + | t.Fatalf("scene: %v", scene) | |||
| 86 | + | } | |||
| 87 | + | if scene.Windows[0].Rect.H != before.Windows[0].Rect.H-30 { | |||
| 88 | + | t.Fatalf("height: %v vs %v", scene.Windows, before.Windows) | |||
| 89 | + | } | |||
| 90 | + | select { | |||
| 91 | + | case v := <-commands: | |||
| 92 | + | cfg, ok := v.(ports.ConfigureWindow) | |||
| 93 | + | if !ok || cfg.ID != 1 || cfg.Height != scene.Windows[0].Rect.H { | |||
| 94 | + | t.Fatalf("configure: %v", v) | |||
| 95 | + | } | |||
| 96 | + | case <-time.After(time.Second): | |||
| 97 | + | t.Fatal("configure timeout") | |||
| 98 | + | } | |||
| 99 | + | scene.Layers[0].Rect.Y = 99 | |||
| 100 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |||
| 101 | + | if next := recv(); next.Layers[0].Rect.Y != 0 { | |||
| 102 | + | t.Fatalf("scene alias: %v", next) | |||
| 103 | + | } | |||
| 104 | + | cancel() | |||
| 105 | + | <-done | |||
| 106 | + | } | |||