detached aba8c9f

fix(core): center layers between opposing anchors

commit
aba8c9ff1c4ebebc1a73758e903225813f57ea5e
author
brice · · 2026-09-23 16:17 UTC
parent
d8ac0f6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +27 -8
M internal/core/layers.go +23 -8
@@ -20,18 +20,33 @@ func arrangeLayers(outW, outH int, layers []ports.LayerSurface) (placed []ports.
2020 bounds = full
2121 }
2222 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 {
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:
2533 x = bounds.X + left
26- } else if s.Anchor&ports.AnchorRight != 0 {
27- x = bounds.X + bounds.W - s.Width - right
34+ case ports.AnchorRight:
35+ x = bounds.X + bounds.W - w - right
2836 }
29- if s.Anchor&ports.AnchorTop != 0 {
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:
3045 y = bounds.Y + top
31- } else if s.Anchor&ports.AnchorBottom != 0 {
32- y = bounds.Y + bounds.H - s.Height - bottom
46+ case ports.AnchorBottom:
47+ y = bounds.Y + bounds.H - h - bottom
3348 }
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}})
49+ placed = append(placed, ports.SceneLayer{ID: s.ID, Layer: s.Layer, Rect: ports.Rect{X: x, Y: y, W: w, H: h}})
3550 if s.ExclusiveZone <= 0 {
3651 continue
3752 }
@@ -20,18 +20,33 @@ func arrangeLayers(outW, outH int, layers []ports.LayerSurface) (placed []ports.
20 bounds = full20 bounds = full
21 }21 }
22 top, right, bottom, left := int(s.Margin[0]), int(s.Margin[1]), int(s.Margin[2]), int(s.Margin[3])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)/223+ w, h := s.Width, s.Height
24- if s.Anchor&ports.AnchorLeft != 0 {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:
25 x = bounds.X + left33 x = bounds.X + left
26- } else if s.Anchor&ports.AnchorRight != 0 {34+ case ports.AnchorRight:
27- x = bounds.X + bounds.W - s.Width - right35+ x = bounds.X + bounds.W - w - right
28 }36 }
29- if s.Anchor&ports.AnchorTop != 0 {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:
30 y = bounds.Y + top45 y = bounds.Y + top
31- } else if s.Anchor&ports.AnchorBottom != 0 {46+ case ports.AnchorBottom:
32- y = bounds.Y + bounds.H - s.Height - bottom47+ y = bounds.Y + bounds.H - h - bottom
33 }48 }
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}})49+ placed = append(placed, ports.SceneLayer{ID: s.ID, Layer: s.Layer, Rect: ports.Rect{X: x, Y: y, W: w, H: h}})
35 if s.ExclusiveZone <= 0 {50 if s.ExclusiveZone <= 0 {
36 continue51 continue
37 }52 }
M internal/core/layers_test.go +4 -0
@@ -20,6 +20,10 @@ func TestArrangeLayers(t *testing.T) {
2020 {"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}},
2121 {"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}},
2222 {"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}},
2327 {"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}},
2428 {"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}},
2529 }
@@ -20,6 +20,10 @@ func TestArrangeLayers(t *testing.T) {
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}},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}},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}},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}},
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}},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}},
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}},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}},
25 }29 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 26

Tags 10

↑↓ moveenter openesc close