feat(core): lay out rotated outputs in their transformed size
- commit
- 343f5a5b2f8a417040e98cb1309cbf618b65d171
- author
- brice · · 2026-10-03 18:40 UTC
- parent
- c1a5a02
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +135 -17
M internal/core/core.go +2 -2
| @@ -282,7 +282,7 @@ func (c *Core) apply(cfg ports.Config) error { | |||
| 282 | 282 | for _, s := range c.screens { | |
| 283 | 283 | c.settings(s.mon) | |
| 284 | 284 | } | |
| 285 | - | c.applyConfigScales() | |
| 285 | + | c.applyOutputConfig() | |
| 286 | 286 | return nil | |
| 287 | 287 | } | |
| 288 | 288 | ||
| @@ -549,7 +549,7 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 549 | 549 | if frame != (Rect{W: o.W, H: o.H}) { | |
| 550 | 550 | clip = frame | |
| 551 | 551 | } | |
| 552 | - | scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: shownLayers(sc)} | |
| 552 | + | scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: shownLayers(sc)} | |
| 553 | 553 | layout := sc.mon.Layout() | |
| 554 | 554 | var real map[WindowID]Placement | |
| 555 | 555 | if sc.mon.ov.open { | |
| @@ -282,7 +282,7 @@ func (c *Core) apply(cfg ports.Config) error { | |||||
| 282 | for _, s := range c.screens { | 282 | for _, s := range c.screens { | ||
| 283 | c.settings(s.mon) | 283 | c.settings(s.mon) | ||
| 284 | } | 284 | } | ||
| 285 | - | c.applyConfigScales() | 285 | + | c.applyOutputConfig() |
| 286 | return nil | 286 | return nil | ||
| 287 | } | 287 | } | ||
| 288 | 288 | ||||
| @@ -549,7 +549,7 @@ func (c *Core) publish(ctx context.Context) error { | |||||
| 549 | if frame != (Rect{W: o.W, H: o.H}) { | 549 | if frame != (Rect{W: o.W, H: o.H}) { | ||
| 550 | clip = frame | 550 | clip = frame | ||
| 551 | } | 551 | } | ||
| 552 | - | scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: shownLayers(sc)} | 552 | + | scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: shownLayers(sc)} |
| 553 | layout := sc.mon.Layout() | 553 | layout := sc.mon.Layout() | ||
| 554 | var real map[WindowID]Placement | 554 | var real map[WindowID]Placement | ||
| 555 | if sc.mon.ov.open { | 555 | if sc.mon.ov.open { | ||
M internal/core/screens.go +38 -14
| @@ -13,11 +13,14 @@ type screen struct { | |||
| 13 | 13 | info ports.OutputInfo | |
| 14 | 14 | mon *Monitor | |
| 15 | 15 | scale, cfgScale float64 | |
| 16 | - | primary bool | |
| 17 | - | x, y int // logical origin in the global layout | |
| 18 | - | off bool // turned off by a client (output power management) | |
| 19 | - | layers []ports.LayerSurface | |
| 20 | - | placed []ports.SceneLayer | |
| 16 | + | // transform is how the physical target holds the screen's logical | |
| 17 | + | // output (output.<name>.transform); 90/270 swap width and height. | |
| 18 | + | transform ports.BufferTransform | |
| 19 | + | primary bool | |
| 20 | + | x, y int // logical origin in the global layout | |
| 21 | + | off bool // turned off by a client (output power management) | |
| 22 | + | layers []ports.LayerSurface | |
| 23 | + | placed []ports.SceneLayer | |
| 21 | 24 | // capture holds the attached layers of the capture exclusion: shown over | |
| 22 | 25 | // a fullscreen window (capture.go). | |
| 23 | 26 | capture map[WindowID]bool | |
| @@ -29,8 +32,14 @@ type screen struct { | |||
| 29 | 32 | ||
| 30 | 33 | func (s *screen) name() string { return s.info.Name } | |
| 31 | 34 | ||
| 32 | - | // modeSize is the physical mode size in the screen's logical orientation. | |
| 33 | - | func (s *screen) modeSize() (w, h int) { return s.info.Width, s.info.Height } | |
| 35 | + | // modeSize is the physical mode size in the screen's logical orientation: | |
| 36 | + | // width and height swap for a 90/270 transform. | |
| 37 | + | func (s *screen) modeSize() (w, h int) { | |
| 38 | + | if s.transform.Rotated() { | |
| 39 | + | return s.info.Height, s.info.Width | |
| 40 | + | } | |
| 41 | + | return s.info.Width, s.info.Height | |
| 42 | + | } | |
| 34 | 43 | ||
| 35 | 44 | // rect is the screen's place in the global logical layout. | |
| 36 | 45 | func (s *screen) rect() Rect { | |
| @@ -40,7 +49,7 @@ func (s *screen) rect() Rect { | |||
| 40 | 49 | ||
| 41 | 50 | func (s *screen) placement() ports.OutputPlacement { | |
| 42 | 51 | r := s.rect() | |
| 43 | - | return ports.OutputPlacement{Info: s.info, X: r.X, Y: r.Y, Width: r.W, Height: r.H, Scale: s.scale, Primary: s.primary} | |
| 52 | + | return ports.OutputPlacement{Info: s.info, X: r.X, Y: r.Y, Width: r.W, Height: r.H, Scale: s.scale, Transform: s.transform, Primary: s.primary} | |
| 44 | 53 | } | |
| 45 | 54 | ||
| 46 | 55 | // setScale resizes the logical output; layer placement follows. | |
| @@ -197,14 +206,27 @@ func (c *Core) configScale(name string) float64 { | |||
| 197 | 206 | return SnapScale(v) | |
| 198 | 207 | } | |
| 199 | 208 | ||
| 200 | - | // applyConfigScales picks up output.<name>.scale when it changed in the | |
| 201 | - | // config, replacing any live scale-up/scale-down adjustment. | |
| 202 | - | func (c *Core) applyConfigScales() { | |
| 209 | + | // configTransform is output.<name>.transform, else 0. | |
| 210 | + | func (c *Core) configTransform(name string) ports.BufferTransform { | |
| 211 | + | o, _ := c.outputConfig(name) | |
| 212 | + | return o.Transform | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | // applyOutputConfig picks up output.<name>.scale and .transform when they | |
| 216 | + | // changed in the config. A new scale replaces any live scale-up/scale-down | |
| 217 | + | // adjustment; a transform alone keeps it. | |
| 218 | + | func (c *Core) applyOutputConfig() { | |
| 203 | 219 | for _, s := range c.screens { | |
| 204 | - | if v := c.configScale(s.name()); v != s.cfgScale { | |
| 205 | - | s.cfgScale = v | |
| 206 | - | s.setScale(v) | |
| 220 | + | v, t := c.configScale(s.name()), c.configTransform(s.name()) | |
| 221 | + | if v == s.cfgScale && t == s.transform { | |
| 222 | + | continue | |
| 223 | + | } | |
| 224 | + | scale := s.scale | |
| 225 | + | if v != s.cfgScale { | |
| 226 | + | scale = v | |
| 207 | 227 | } | |
| 228 | + | s.cfgScale, s.transform = v, t | |
| 229 | + | s.setScale(scale) | |
| 208 | 230 | } | |
| 209 | 231 | c.order() | |
| 210 | 232 | } | |
| @@ -221,6 +243,7 @@ func (c *Core) addScreen(info ports.OutputInfo) { | |||
| 221 | 243 | s.mon.Key = info.Key() | |
| 222 | 244 | c.settings(s.mon) | |
| 223 | 245 | } | |
| 246 | + | s.transform = c.configTransform(info.Name) | |
| 224 | 247 | s.setScale(s.scale) | |
| 225 | 248 | c.order() | |
| 226 | 249 | return | |
| @@ -238,6 +261,7 @@ func (c *Core) addScreen(info ports.OutputInfo) { | |||
| 238 | 261 | s.mon.Name, s.mon.Key = info.Name, info.Key() | |
| 239 | 262 | c.settings(s.mon) | |
| 240 | 263 | s.cfgScale = c.configScale(info.Name) | |
| 264 | + | s.transform = c.configTransform(info.Name) | |
| 241 | 265 | s.setScale(s.cfgScale) | |
| 242 | 266 | s.primary = c.isPrimary(info.Name) | |
| 243 | 267 | c.order() | |
| @@ -13,11 +13,14 @@ type screen struct { | |||||
| 13 | info ports.OutputInfo | 13 | info ports.OutputInfo | ||
| 14 | mon *Monitor | 14 | mon *Monitor | ||
| 15 | scale, cfgScale float64 | 15 | scale, cfgScale float64 | ||
| 16 | - | primary bool | 16 | + | // transform is how the physical target holds the screen's logical |
| 17 | - | x, y int // logical origin in the global layout | 17 | + | // output (output.<name>.transform); 90/270 swap width and height. |
| 18 | - | off bool // turned off by a client (output power management) | 18 | + | transform ports.BufferTransform |
| 19 | - | layers []ports.LayerSurface | 19 | + | primary bool |
| 20 | - | placed []ports.SceneLayer | 20 | + | x, y int // logical origin in the global layout |
| 21 | + | off bool // turned off by a client (output power management) | |||
| 22 | + | layers []ports.LayerSurface | |||
| 23 | + | placed []ports.SceneLayer | |||
| 21 | // capture holds the attached layers of the capture exclusion: shown over | 24 | // capture holds the attached layers of the capture exclusion: shown over | ||
| 22 | // a fullscreen window (capture.go). | 25 | // a fullscreen window (capture.go). | ||
| 23 | capture map[WindowID]bool | 26 | capture map[WindowID]bool | ||
| @@ -29,8 +32,14 @@ type screen struct { | |||||
| 29 | 32 | ||||
| 30 | func (s *screen) name() string { return s.info.Name } | 33 | func (s *screen) name() string { return s.info.Name } | ||
| 31 | 34 | ||||
| 32 | - | // modeSize is the physical mode size in the screen's logical orientation. | 35 | + | // modeSize is the physical mode size in the screen's logical orientation: |
| 33 | - | func (s *screen) modeSize() (w, h int) { return s.info.Width, s.info.Height } | 36 | + | // width and height swap for a 90/270 transform. |
| 37 | + | func (s *screen) modeSize() (w, h int) { | |||
| 38 | + | if s.transform.Rotated() { | |||
| 39 | + | return s.info.Height, s.info.Width | |||
| 40 | + | } | |||
| 41 | + | return s.info.Width, s.info.Height | |||
| 42 | + | } | |||
| 34 | 43 | ||||
| 35 | // rect is the screen's place in the global logical layout. | 44 | // rect is the screen's place in the global logical layout. | ||
| 36 | func (s *screen) rect() Rect { | 45 | func (s *screen) rect() Rect { | ||
| @@ -40,7 +49,7 @@ func (s *screen) rect() Rect { | |||||
| 40 | 49 | ||||
| 41 | func (s *screen) placement() ports.OutputPlacement { | 50 | func (s *screen) placement() ports.OutputPlacement { | ||
| 42 | r := s.rect() | 51 | r := s.rect() | ||
| 43 | - | return ports.OutputPlacement{Info: s.info, X: r.X, Y: r.Y, Width: r.W, Height: r.H, Scale: s.scale, Primary: s.primary} | 52 | + | return ports.OutputPlacement{Info: s.info, X: r.X, Y: r.Y, Width: r.W, Height: r.H, Scale: s.scale, Transform: s.transform, Primary: s.primary} |
| 44 | } | 53 | } | ||
| 45 | 54 | ||||
| 46 | // setScale resizes the logical output; layer placement follows. | 55 | // setScale resizes the logical output; layer placement follows. | ||
| @@ -197,14 +206,27 @@ func (c *Core) configScale(name string) float64 { | |||||
| 197 | return SnapScale(v) | 206 | return SnapScale(v) | ||
| 198 | } | 207 | } | ||
| 199 | 208 | ||||
| 200 | - | // applyConfigScales picks up output.<name>.scale when it changed in the | 209 | + | // configTransform is output.<name>.transform, else 0. |
| 201 | - | // config, replacing any live scale-up/scale-down adjustment. | 210 | + | func (c *Core) configTransform(name string) ports.BufferTransform { |
| 202 | - | func (c *Core) applyConfigScales() { | 211 | + | o, _ := c.outputConfig(name) |
| 212 | + | return o.Transform | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | // applyOutputConfig picks up output.<name>.scale and .transform when they | |||
| 216 | + | // changed in the config. A new scale replaces any live scale-up/scale-down | |||
| 217 | + | // adjustment; a transform alone keeps it. | |||
| 218 | + | func (c *Core) applyOutputConfig() { | |||
| 203 | for _, s := range c.screens { | 219 | for _, s := range c.screens { | ||
| 204 | - | if v := c.configScale(s.name()); v != s.cfgScale { | 220 | + | v, t := c.configScale(s.name()), c.configTransform(s.name()) |
| 205 | - | s.cfgScale = v | 221 | + | if v == s.cfgScale && t == s.transform { |
| 206 | - | s.setScale(v) | 222 | + | continue |
| 223 | + | } | |||
| 224 | + | scale := s.scale | |||
| 225 | + | if v != s.cfgScale { | |||
| 226 | + | scale = v | |||
| 207 | } | 227 | } | ||
| 228 | + | s.cfgScale, s.transform = v, t | |||
| 229 | + | s.setScale(scale) | |||
| 208 | } | 230 | } | ||
| 209 | c.order() | 231 | c.order() | ||
| 210 | } | 232 | } | ||
| @@ -221,6 +243,7 @@ func (c *Core) addScreen(info ports.OutputInfo) { | |||||
| 221 | s.mon.Key = info.Key() | 243 | s.mon.Key = info.Key() | ||
| 222 | c.settings(s.mon) | 244 | c.settings(s.mon) | ||
| 223 | } | 245 | } | ||
| 246 | + | s.transform = c.configTransform(info.Name) | |||
| 224 | s.setScale(s.scale) | 247 | s.setScale(s.scale) | ||
| 225 | c.order() | 248 | c.order() | ||
| 226 | return | 249 | return | ||
| @@ -238,6 +261,7 @@ func (c *Core) addScreen(info ports.OutputInfo) { | |||||
| 238 | s.mon.Name, s.mon.Key = info.Name, info.Key() | 261 | s.mon.Name, s.mon.Key = info.Name, info.Key() | ||
| 239 | c.settings(s.mon) | 262 | c.settings(s.mon) | ||
| 240 | s.cfgScale = c.configScale(info.Name) | 263 | s.cfgScale = c.configScale(info.Name) | ||
| 264 | + | s.transform = c.configTransform(info.Name) | |||
| 241 | s.setScale(s.cfgScale) | 265 | s.setScale(s.cfgScale) | ||
| 242 | s.primary = c.isPrimary(info.Name) | 266 | s.primary = c.isPrimary(info.Name) | ||
| 243 | c.order() | 267 | c.order() | ||
M internal/core/screens_test.go +94 -0
| @@ -1409,3 +1409,97 @@ func TestPoweredOffScreenStaysNeighbor(t *testing.T) { | |||
| 1409 | 1409 | t.Fatalf("focus-monitor-right: %s, want the powered-off DP-2", got) | |
| 1410 | 1410 | } | |
| 1411 | 1411 | } | |
| 1412 | + | ||
| 1413 | + | func TestRotatedOutputLayout(t *testing.T) { | |
| 1414 | + | r := startMulti(t, func(c *ports.Config) { | |
| 1415 | + | c.Outputs = []ports.OutputConfig{{Name: left.Name, Transform: 1}} | |
| 1416 | + | }, left, right) | |
| 1417 | + | out := lastOutputs(t, r.commands) | |
| 1418 | + | if len(out.Outputs) != 2 { | |
| 1419 | + | t.Fatalf("%+v", out.Outputs) | |
| 1420 | + | } | |
| 1421 | + | if o := out.Outputs[0]; o.Info.Name != "DP-1" || o.Width != 100 || o.Height != 200 || o.Transform != 1 { | |
| 1422 | + | t.Fatalf("DP-1: %+v", o) | |
| 1423 | + | } | |
| 1424 | + | if o := out.Outputs[1]; o.Info.Name != "DP-2" || o.X != 100 || o.Width != 400 || o.Height != 200 || o.Transform != 0 { | |
| 1425 | + | t.Fatalf("DP-2: %+v", o) | |
| 1426 | + | } | |
| 1427 | + | r.mapWindow(t, 1) | |
| 1428 | + | set := r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 1429 | + | for _, s := range set { | |
| 1430 | + | switch s.Output { | |
| 1431 | + | case "DP-1": | |
| 1432 | + | if s.OutputWidth != 100 || s.OutputHeight != 200 || s.Transform != 1 { | |
| 1433 | + | t.Fatalf("DP-1 scene: %dx%d t=%d", s.OutputWidth, s.OutputHeight, s.Transform) | |
| 1434 | + | } | |
| 1435 | + | case "DP-2": | |
| 1436 | + | if s.OutputWidth != 400 || s.OutputHeight != 200 || s.Transform != 0 { | |
| 1437 | + | t.Fatalf("DP-2 scene: %dx%d t=%d", s.OutputWidth, s.OutputHeight, s.Transform) | |
| 1438 | + | } | |
| 1439 | + | } | |
| 1440 | + | } | |
| 1441 | + | } | |
| 1442 | + | ||
| 1443 | + | func TestRotatedOutputNeighbor(t *testing.T) { | |
| 1444 | + | r := startMulti(t, func(c *ports.Config) { | |
| 1445 | + | c.Outputs = []ports.OutputConfig{ | |
| 1446 | + | {Name: "DP-1", Transform: 1}, | |
| 1447 | + | {Name: "DP-2", Anchor: ports.OutputAnchor{Relation: ports.RelationBelow, To: "DP-1"}}, | |
| 1448 | + | } | |
| 1449 | + | }, left, right) | |
| 1450 | + | out := lastOutputs(t, r.commands) | |
| 1451 | + | if o := out.Outputs[1]; o.Info.Name != "DP-2" || o.Y != 200 { | |
| 1452 | + | t.Fatalf("DP-2: %+v", o) | |
| 1453 | + | } | |
| 1454 | + | r.mapWindow(t, 1) | |
| 1455 | + | r.key(t, "Down", ports.ModAlt|ports.ModCtrl) | |
| 1456 | + | if got := lastOutputs(t, r.commands).Focused; got != "DP-2" { | |
| 1457 | + | t.Fatalf("focus-monitor-down: %s", got) | |
| 1458 | + | } | |
| 1459 | + | } | |
| 1460 | + | ||
| 1461 | + | func TestTransformReload(t *testing.T) { | |
| 1462 | + | r := startMulti(t, func(c *ports.Config) { | |
| 1463 | + | c.Binds["Alt+equal"] = "scale-up" | |
| 1464 | + | c.Outputs = []ports.OutputConfig{{Name: "DP-1"}, {Name: "DP-2"}} | |
| 1465 | + | }, left, right) | |
| 1466 | + | // Scale DP-2 (focus follows the key to it) with the bind first. | |
| 1467 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 1468 | + | set := r.key(t, "equal", ports.ModAlt) | |
| 1469 | + | scale := set[1].Scale | |
| 1470 | + | if scale == 1 { | |
| 1471 | + | t.Fatal("scale-up did not change the scale") | |
| 1472 | + | } | |
| 1473 | + | if out := lastOutputs(t, r.commands); out.Outputs[0].Width != 200 || out.Outputs[1].X != 200 { | |
| 1474 | + | t.Fatalf("%+v", out.Outputs) | |
| 1475 | + | } | |
| 1476 | + | ||
| 1477 | + | r.cfg.Outputs[0].Transform = 1 | |
| 1478 | + | r.reload <- ports.ConfigChanged{Config: r.cfg} | |
| 1479 | + | set = receive(t, r.scenes) | |
| 1480 | + | out := lastOutputs(t, r.commands) | |
| 1481 | + | if o := out.Outputs[0]; o.Width != 100 || o.Height != 200 || o.Transform != 1 { | |
| 1482 | + | t.Fatalf("DP-1: %+v", o) | |
| 1483 | + | } | |
| 1484 | + | if o := out.Outputs[1]; o.X != 100 { | |
| 1485 | + | t.Fatalf("DP-2 not re-placed: %+v", o) | |
| 1486 | + | } | |
| 1487 | + | if set[1].Scale != scale || out.Outputs[1].Scale != scale { | |
| 1488 | + | t.Fatalf("scale %v changed to %v by a transform reload", scale, set[1].Scale) | |
| 1489 | + | } | |
| 1490 | + | ||
| 1491 | + | // A transform change on the scaled output itself keeps its live scale. | |
| 1492 | + | r.cfg.Outputs[1].Transform = 1 | |
| 1493 | + | r.reload <- ports.ConfigChanged{Config: r.cfg} | |
| 1494 | + | set = receive(t, r.scenes) | |
| 1495 | + | if set[1].Scale != scale || set[1].Transform != 1 { | |
| 1496 | + | t.Fatalf("scale %v transform %d", set[1].Scale, set[1].Transform) | |
| 1497 | + | } | |
| 1498 | + | // Back to normal. | |
| 1499 | + | r.cfg.Outputs[0].Transform = 0 | |
| 1500 | + | r.reload <- ports.ConfigChanged{Config: r.cfg} | |
| 1501 | + | _ = receive(t, r.scenes) | |
| 1502 | + | if o := lastOutputs(t, r.commands).Outputs[0]; o.Width != 200 || o.Transform != 0 { | |
| 1503 | + | t.Fatalf("DP-1: %+v", o) | |
| 1504 | + | } | |
| 1505 | + | } | |
| @@ -1409,3 +1409,97 @@ func TestPoweredOffScreenStaysNeighbor(t *testing.T) { | |||||
| 1409 | t.Fatalf("focus-monitor-right: %s, want the powered-off DP-2", got) | 1409 | t.Fatalf("focus-monitor-right: %s, want the powered-off DP-2", got) | ||
| 1410 | } | 1410 | } | ||
| 1411 | } | 1411 | } | ||
| 1412 | + | ||||
| 1413 | + | func TestRotatedOutputLayout(t *testing.T) { | |||
| 1414 | + | r := startMulti(t, func(c *ports.Config) { | |||
| 1415 | + | c.Outputs = []ports.OutputConfig{{Name: left.Name, Transform: 1}} | |||
| 1416 | + | }, left, right) | |||
| 1417 | + | out := lastOutputs(t, r.commands) | |||
| 1418 | + | if len(out.Outputs) != 2 { | |||
| 1419 | + | t.Fatalf("%+v", out.Outputs) | |||
| 1420 | + | } | |||
| 1421 | + | if o := out.Outputs[0]; o.Info.Name != "DP-1" || o.Width != 100 || o.Height != 200 || o.Transform != 1 { | |||
| 1422 | + | t.Fatalf("DP-1: %+v", o) | |||
| 1423 | + | } | |||
| 1424 | + | if o := out.Outputs[1]; o.Info.Name != "DP-2" || o.X != 100 || o.Width != 400 || o.Height != 200 || o.Transform != 0 { | |||
| 1425 | + | t.Fatalf("DP-2: %+v", o) | |||
| 1426 | + | } | |||
| 1427 | + | r.mapWindow(t, 1) | |||
| 1428 | + | set := r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |||
| 1429 | + | for _, s := range set { | |||
| 1430 | + | switch s.Output { | |||
| 1431 | + | case "DP-1": | |||
| 1432 | + | if s.OutputWidth != 100 || s.OutputHeight != 200 || s.Transform != 1 { | |||
| 1433 | + | t.Fatalf("DP-1 scene: %dx%d t=%d", s.OutputWidth, s.OutputHeight, s.Transform) | |||
| 1434 | + | } | |||
| 1435 | + | case "DP-2": | |||
| 1436 | + | if s.OutputWidth != 400 || s.OutputHeight != 200 || s.Transform != 0 { | |||
| 1437 | + | t.Fatalf("DP-2 scene: %dx%d t=%d", s.OutputWidth, s.OutputHeight, s.Transform) | |||
| 1438 | + | } | |||
| 1439 | + | } | |||
| 1440 | + | } | |||
| 1441 | + | } | |||
| 1442 | + | ||||
| 1443 | + | func TestRotatedOutputNeighbor(t *testing.T) { | |||
| 1444 | + | r := startMulti(t, func(c *ports.Config) { | |||
| 1445 | + | c.Outputs = []ports.OutputConfig{ | |||
| 1446 | + | {Name: "DP-1", Transform: 1}, | |||
| 1447 | + | {Name: "DP-2", Anchor: ports.OutputAnchor{Relation: ports.RelationBelow, To: "DP-1"}}, | |||
| 1448 | + | } | |||
| 1449 | + | }, left, right) | |||
| 1450 | + | out := lastOutputs(t, r.commands) | |||
| 1451 | + | if o := out.Outputs[1]; o.Info.Name != "DP-2" || o.Y != 200 { | |||
| 1452 | + | t.Fatalf("DP-2: %+v", o) | |||
| 1453 | + | } | |||
| 1454 | + | r.mapWindow(t, 1) | |||
| 1455 | + | r.key(t, "Down", ports.ModAlt|ports.ModCtrl) | |||
| 1456 | + | if got := lastOutputs(t, r.commands).Focused; got != "DP-2" { | |||
| 1457 | + | t.Fatalf("focus-monitor-down: %s", got) | |||
| 1458 | + | } | |||
| 1459 | + | } | |||
| 1460 | + | ||||
| 1461 | + | func TestTransformReload(t *testing.T) { | |||
| 1462 | + | r := startMulti(t, func(c *ports.Config) { | |||
| 1463 | + | c.Binds["Alt+equal"] = "scale-up" | |||
| 1464 | + | c.Outputs = []ports.OutputConfig{{Name: "DP-1"}, {Name: "DP-2"}} | |||
| 1465 | + | }, left, right) | |||
| 1466 | + | // Scale DP-2 (focus follows the key to it) with the bind first. | |||
| 1467 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |||
| 1468 | + | set := r.key(t, "equal", ports.ModAlt) | |||
| 1469 | + | scale := set[1].Scale | |||
| 1470 | + | if scale == 1 { | |||
| 1471 | + | t.Fatal("scale-up did not change the scale") | |||
| 1472 | + | } | |||
| 1473 | + | if out := lastOutputs(t, r.commands); out.Outputs[0].Width != 200 || out.Outputs[1].X != 200 { | |||
| 1474 | + | t.Fatalf("%+v", out.Outputs) | |||
| 1475 | + | } | |||
| 1476 | + | ||||
| 1477 | + | r.cfg.Outputs[0].Transform = 1 | |||
| 1478 | + | r.reload <- ports.ConfigChanged{Config: r.cfg} | |||
| 1479 | + | set = receive(t, r.scenes) | |||
| 1480 | + | out := lastOutputs(t, r.commands) | |||
| 1481 | + | if o := out.Outputs[0]; o.Width != 100 || o.Height != 200 || o.Transform != 1 { | |||
| 1482 | + | t.Fatalf("DP-1: %+v", o) | |||
| 1483 | + | } | |||
| 1484 | + | if o := out.Outputs[1]; o.X != 100 { | |||
| 1485 | + | t.Fatalf("DP-2 not re-placed: %+v", o) | |||
| 1486 | + | } | |||
| 1487 | + | if set[1].Scale != scale || out.Outputs[1].Scale != scale { | |||
| 1488 | + | t.Fatalf("scale %v changed to %v by a transform reload", scale, set[1].Scale) | |||
| 1489 | + | } | |||
| 1490 | + | ||||
| 1491 | + | // A transform change on the scaled output itself keeps its live scale. | |||
| 1492 | + | r.cfg.Outputs[1].Transform = 1 | |||
| 1493 | + | r.reload <- ports.ConfigChanged{Config: r.cfg} | |||
| 1494 | + | set = receive(t, r.scenes) | |||
| 1495 | + | if set[1].Scale != scale || set[1].Transform != 1 { | |||
| 1496 | + | t.Fatalf("scale %v transform %d", set[1].Scale, set[1].Transform) | |||
| 1497 | + | } | |||
| 1498 | + | // Back to normal. | |||
| 1499 | + | r.cfg.Outputs[0].Transform = 0 | |||
| 1500 | + | r.reload <- ports.ConfigChanged{Config: r.cfg} | |||
| 1501 | + | _ = receive(t, r.scenes) | |||
| 1502 | + | if o := lastOutputs(t, r.commands).Outputs[0]; o.Width != 200 || o.Transform != 0 { | |||
| 1503 | + | t.Fatalf("DP-1: %+v", o) | |||
| 1504 | + | } | |||
| 1505 | + | } | |||
M internal/core/security.go +1 -1
| @@ -186,7 +186,7 @@ func (c *Core) publishProtected(ctx context.Context) error { | |||
| 186 | 186 | for _, sc := range c.screens { | |
| 187 | 187 | c.seq++ | |
| 188 | 188 | o := sc.mon.Output() | |
| 189 | - | scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Off: sc.off, Background: "#000000"} | |
| 189 | + | scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: "#000000"} | |
| 190 | 190 | if s, ok := c.lockSurface(sc); ok { | |
| 191 | 191 | scene.Windows = []ports.SceneWindow{{ID: s.ID, Rect: Rect{W: o.W, H: o.H}, Fullscreen: true, Focused: s.ID == focus}} | |
| 192 | 192 | } | |
| @@ -186,7 +186,7 @@ func (c *Core) publishProtected(ctx context.Context) error { | |||||
| 186 | for _, sc := range c.screens { | 186 | for _, sc := range c.screens { | ||
| 187 | c.seq++ | 187 | c.seq++ | ||
| 188 | o := sc.mon.Output() | 188 | o := sc.mon.Output() | ||
| 189 | - | scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Off: sc.off, Background: "#000000"} | 189 | + | scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: "#000000"} |
| 190 | if s, ok := c.lockSurface(sc); ok { | 190 | if s, ok := c.lockSurface(sc); ok { | ||
| 191 | scene.Windows = []ports.SceneWindow{{ID: s.ID, Rect: Rect{W: o.W, H: o.H}, Fullscreen: true, Focused: s.ID == focus}} | 191 | scene.Windows = []ports.SceneWindow{{ID: s.ID, Rect: Rect{W: o.W, H: o.H}, Fullscreen: true, Focused: s.ID == focus}} | ||
| 192 | } | 192 | } | ||