refactor(core): document overlap reach limit and parse output relations without a map
- commit
- f23fedf30b1f82f58d437e22e9ea468701adc357
- author
- brice · · 2026-10-03 14:22 UTC
- parent
- ac43558
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +32 -16
M internal/adapters/config/config.go +14 -8
| @@ -234,17 +234,23 @@ func Load(path string) (ports.Config, []Warning, error) { | |||
| 234 | 234 | // real layout but small enough that placement arithmetic cannot overflow. | |
| 235 | 235 | const maxOutputOffset = 65536 | |
| 236 | 236 | ||
| 237 | + | // outputRelations lists the relation suffixes, tried in order. | |
| 238 | + | var outputRelations = [...]struct { | |
| 239 | + | suffix string | |
| 240 | + | rel ports.OutputRelation | |
| 241 | + | }{ | |
| 242 | + | {".right-of", ports.RelationRightOf}, | |
| 243 | + | {".left-of", ports.RelationLeftOf}, | |
| 244 | + | {".above", ports.RelationAbove}, | |
| 245 | + | {".below", ports.RelationBelow}, | |
| 246 | + | } | |
| 247 | + | ||
| 237 | 248 | // outputRelation splits "<output>.right-of" (left-of, above, below) into the | |
| 238 | 249 | // relation and the output name. | |
| 239 | 250 | func outputRelation(name string) (ports.OutputRelation, string, bool) { | |
| 240 | - | for suffix, rel := range map[string]ports.OutputRelation{ | |
| 241 | - | ".right-of": ports.RelationRightOf, | |
| 242 | - | ".left-of": ports.RelationLeftOf, | |
| 243 | - | ".above": ports.RelationAbove, | |
| 244 | - | ".below": ports.RelationBelow, | |
| 245 | - | } { | |
| 246 | - | if base, ok := strings.CutSuffix(name, suffix); ok { | |
| 247 | - | return rel, base, true | |
| 251 | + | for _, r := range outputRelations { | |
| 252 | + | if base, ok := strings.CutSuffix(name, r.suffix); ok { | |
| 253 | + | return r.rel, base, true | |
| 248 | 254 | } | |
| 249 | 255 | } | |
| 250 | 256 | return ports.RelationNone, "", false | |
| @@ -234,17 +234,23 @@ func Load(path string) (ports.Config, []Warning, error) { | |||||
| 234 | // real layout but small enough that placement arithmetic cannot overflow. | 234 | // real layout but small enough that placement arithmetic cannot overflow. | ||
| 235 | const maxOutputOffset = 65536 | 235 | const maxOutputOffset = 65536 | ||
| 236 | 236 | ||||
| 237 | + | // outputRelations lists the relation suffixes, tried in order. | |||
| 238 | + | var outputRelations = [...]struct { | |||
| 239 | + | suffix string | |||
| 240 | + | rel ports.OutputRelation | |||
| 241 | + | }{ | |||
| 242 | + | {".right-of", ports.RelationRightOf}, | |||
| 243 | + | {".left-of", ports.RelationLeftOf}, | |||
| 244 | + | {".above", ports.RelationAbove}, | |||
| 245 | + | {".below", ports.RelationBelow}, | |||
| 246 | + | } | |||
| 247 | + | ||||
| 237 | // outputRelation splits "<output>.right-of" (left-of, above, below) into the | 248 | // outputRelation splits "<output>.right-of" (left-of, above, below) into the | ||
| 238 | // relation and the output name. | 249 | // relation and the output name. | ||
| 239 | func outputRelation(name string) (ports.OutputRelation, string, bool) { | 250 | func outputRelation(name string) (ports.OutputRelation, string, bool) { | ||
| 240 | - | for suffix, rel := range map[string]ports.OutputRelation{ | 251 | + | for _, r := range outputRelations { |
| 241 | - | ".right-of": ports.RelationRightOf, | 252 | + | if base, ok := strings.CutSuffix(name, r.suffix); ok { |
| 242 | - | ".left-of": ports.RelationLeftOf, | 253 | + | return r.rel, base, true |
| 243 | - | ".above": ports.RelationAbove, | |||
| 244 | - | ".below": ports.RelationBelow, | |||
| 245 | - | } { | |||
| 246 | - | if base, ok := strings.CutSuffix(name, suffix); ok { | |||
| 247 | - | return rel, base, true | |||
| 248 | } | 254 | } | ||
| 249 | } | 255 | } | ||
| 250 | return ports.RelationNone, "", false | 256 | return ports.RelationNone, "", false | ||
M internal/core/neighbor_test.go +5 -0
| @@ -83,6 +83,11 @@ func TestNeighbor(t *testing.T) { | |||
| 83 | 83 | // R is fully left of Y and wins over the closer, overlapping X. | |
| 84 | 84 | {"Y", [4]string{"R", "", "", "X"}}, | |
| 85 | 85 | }}, | |
| 86 | + | {"same-height outputs anchored to the same reference: one is unreachable by direction", []tile{{"R", 0, 0, 100, 100}, {"Y", 100, 0, 300, 100}, {"X", 100, 0, 100, 100}}, []step{ | |
| 87 | + | {"R", [4]string{"", "Y", "", ""}}, | |
| 88 | + | {"Y", [4]string{"R", "", "", ""}}, | |
| 89 | + | {"X", [4]string{"R", "Y", "", ""}}, | |
| 90 | + | }}, | |
| 86 | 91 | {"strict candidate beats a closer overlapping one", []tile{{"A", 0, 0, 100, 100}, {"near", 50, 0, 100, 100}, {"far", 300, 0, 100, 100}}, []step{ | |
| 87 | 92 | {"A", [4]string{"", "far", "", ""}}, | |
| 88 | 93 | {"near", [4]string{"A", "far", "", ""}}, | |
| @@ -83,6 +83,11 @@ func TestNeighbor(t *testing.T) { | |||||
| 83 | // R is fully left of Y and wins over the closer, overlapping X. | 83 | // R is fully left of Y and wins over the closer, overlapping X. | ||
| 84 | {"Y", [4]string{"R", "", "", "X"}}, | 84 | {"Y", [4]string{"R", "", "", "X"}}, | ||
| 85 | }}, | 85 | }}, | ||
| 86 | + | {"same-height outputs anchored to the same reference: one is unreachable by direction", []tile{{"R", 0, 0, 100, 100}, {"Y", 100, 0, 300, 100}, {"X", 100, 0, 100, 100}}, []step{ | |||
| 87 | + | {"R", [4]string{"", "Y", "", ""}}, | |||
| 88 | + | {"Y", [4]string{"R", "", "", ""}}, | |||
| 89 | + | {"X", [4]string{"R", "Y", "", ""}}, | |||
| 90 | + | }}, | |||
| 86 | {"strict candidate beats a closer overlapping one", []tile{{"A", 0, 0, 100, 100}, {"near", 50, 0, 100, 100}, {"far", 300, 0, 100, 100}}, []step{ | 91 | {"strict candidate beats a closer overlapping one", []tile{{"A", 0, 0, 100, 100}, {"near", 50, 0, 100, 100}, {"far", 300, 0, 100, 100}}, []step{ | ||
| 87 | {"A", [4]string{"", "far", "", ""}}, | 92 | {"A", [4]string{"", "far", "", ""}}, | ||
| 88 | {"near", [4]string{"A", "far", "", ""}}, | 93 | {"near", [4]string{"A", "far", "", ""}}, | ||
M internal/core/screens.go +8 -6
| @@ -415,12 +415,14 @@ const ( | |||
| 415 | 415 | // direction d, or -1 at the edge. Candidates lie fully beyond the focused | |
| 416 | 416 | // screen's edge; one sharing part of that edge wins over a diagonal one, then | |
| 417 | 417 | // the smallest gap, the largest shared edge, the closest centers and the | |
| 418 | - | // lowest index decide. When none lies fully beyond, a second pass handles | |
| 419 | - | // overlapping outputs (for instance two anchored to the same reference): it | |
| 420 | - | // takes the screens whose rectangle intersects the focused one and whose | |
| 421 | - | // center is strictly beyond the focused center, ranked the same way with a | |
| 422 | - | // gap of 0, so they stay reachable. Screens with identical centers are not | |
| 423 | - | // reachable by direction. There is no wrap. | |
| 418 | + | // lowest index decide. Overlapping outputs (for instance two anchored to the | |
| 419 | + | // same reference) are reached only when no screen lies fully beyond the edge | |
| 420 | + | // in that direction: a second pass then takes the screens whose rectangle | |
| 421 | + | // intersects the focused one and whose center is strictly beyond the focused | |
| 422 | + | // center, ranked the same way with a gap of 0. A screen that does have a | |
| 423 | + | // strict candidate never falls back, so an overlapping one can stay out of | |
| 424 | + | // reach. Screens with the same center on the axis are unreachable by direction | |
| 425 | + | // (the pointer still reaches them). There is no wrap. | |
| 424 | 426 | func (c *Core) neighbor(d direction) int { | |
| 425 | 427 | if d < dirLeft || d > dirDown { | |
| 426 | 428 | return -1 | |
| @@ -415,12 +415,14 @@ const ( | |||||
| 415 | // direction d, or -1 at the edge. Candidates lie fully beyond the focused | 415 | // direction d, or -1 at the edge. Candidates lie fully beyond the focused | ||
| 416 | // screen's edge; one sharing part of that edge wins over a diagonal one, then | 416 | // screen's edge; one sharing part of that edge wins over a diagonal one, then | ||
| 417 | // the smallest gap, the largest shared edge, the closest centers and the | 417 | // the smallest gap, the largest shared edge, the closest centers and the | ||
| 418 | - | // lowest index decide. When none lies fully beyond, a second pass handles | 418 | + | // lowest index decide. Overlapping outputs (for instance two anchored to the |
| 419 | - | // overlapping outputs (for instance two anchored to the same reference): it | 419 | + | // same reference) are reached only when no screen lies fully beyond the edge |
| 420 | - | // takes the screens whose rectangle intersects the focused one and whose | 420 | + | // in that direction: a second pass then takes the screens whose rectangle |
| 421 | - | // center is strictly beyond the focused center, ranked the same way with a | 421 | + | // intersects the focused one and whose center is strictly beyond the focused |
| 422 | - | // gap of 0, so they stay reachable. Screens with identical centers are not | 422 | + | // center, ranked the same way with a gap of 0. A screen that does have a |
| 423 | - | // reachable by direction. There is no wrap. | 423 | + | // strict candidate never falls back, so an overlapping one can stay out of |
| 424 | + | // reach. Screens with the same center on the axis are unreachable by direction | |||
| 425 | + | // (the pointer still reaches them). There is no wrap. | |||
| 424 | func (c *Core) neighbor(d direction) int { | 426 | func (c *Core) neighbor(d direction) int { | ||
| 425 | if d < dirLeft || d > dirDown { | 427 | if d < dirLeft || d > dirDown { | ||
| 426 | return -1 | 428 | return -1 | ||
M internal/core/screens_test.go +5 -2
| @@ -1398,8 +1398,11 @@ func TestPoweredOffScreenStaysNeighbor(t *testing.T) { | |||
| 1398 | 1398 | current := focusedOutput(r, lastOutputs(t, r.commands).Focused) | |
| 1399 | 1399 | r.client <- ports.OutputPower{Output: "DP-2", On: false} | |
| 1400 | 1400 | _ = receive(t, r.scenes) | |
| 1401 | - | if out := lastOutputs(t, r.commands); current != "DP-1" || !slices.Equal(out.Off, []string{"DP-2"}) { | |
| 1402 | - | t.Fatalf("focused %s, off %v", current, out.Off) | |
| 1401 | + | if current != "DP-1" { | |
| 1402 | + | t.Fatalf("focused %s, want DP-1", current) | |
| 1403 | + | } | |
| 1404 | + | if out := lastOutputs(t, r.commands); !slices.Equal(out.Off, []string{"DP-2"}) { | |
| 1405 | + | t.Fatalf("off %v, want [DP-2]", out.Off) | |
| 1403 | 1406 | } | |
| 1404 | 1407 | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 1405 | 1408 | if got := focusedOutput(r, current); got != "DP-2" { | |
| @@ -1398,8 +1398,11 @@ func TestPoweredOffScreenStaysNeighbor(t *testing.T) { | |||||
| 1398 | current := focusedOutput(r, lastOutputs(t, r.commands).Focused) | 1398 | current := focusedOutput(r, lastOutputs(t, r.commands).Focused) | ||
| 1399 | r.client <- ports.OutputPower{Output: "DP-2", On: false} | 1399 | r.client <- ports.OutputPower{Output: "DP-2", On: false} | ||
| 1400 | _ = receive(t, r.scenes) | 1400 | _ = receive(t, r.scenes) | ||
| 1401 | - | if out := lastOutputs(t, r.commands); current != "DP-1" || !slices.Equal(out.Off, []string{"DP-2"}) { | 1401 | + | if current != "DP-1" { |
| 1402 | - | t.Fatalf("focused %s, off %v", current, out.Off) | 1402 | + | t.Fatalf("focused %s, want DP-1", current) |
| 1403 | + | } | |||
| 1404 | + | if out := lastOutputs(t, r.commands); !slices.Equal(out.Off, []string{"DP-2"}) { | |||
| 1405 | + | t.Fatalf("off %v, want [DP-2]", out.Off) | |||
| 1403 | } | 1406 | } | ||
| 1404 | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | 1407 | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | ||
| 1405 | if got := focusedOutput(r, current); got != "DP-2" { | 1408 | if got := focusedOutput(r, current); got != "DP-2" { | ||