detached 5a832c8

feat(xwm): add Space mapping layout coordinates to X coordinates

commit
5a832c8ffbfe55c6a1c9f2d80dffd2d8fa95b8fb
author
brice · · 2026-10-22 15:07 UTC
committer
brice · · 2026-10-22 17:54 UTC
parent
58a7346
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +279 -1
M Makefile +1 -1
@@ -70,7 +70,7 @@ adapter-imports:
7070 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
7171 # Fast, mandatory guards for tiled content publications and SHM copies.
7272 perf-check:
73- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations)$$' -count=1
73+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations)$$' -count=1
7474 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1
7575 check: vet test arch fakes-check log-check perf-check
7676
@@ -70,7 +70,7 @@ adapter-imports:
70 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }70 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
71 # Fast, mandatory guards for tiled content publications and SHM copies.71 # Fast, mandatory guards for tiled content publications and SHM copies.
72 perf-check:72 perf-check:
73- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations)$$' -count=173+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations)$$' -count=1
74 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=174 CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1
75 check: vet test arch fakes-check log-check perf-check75 check: vet test arch fakes-check log-check perf-check
76 76
A internal/adapters/wayland/xwm/space.go +167 -0
@@ -0,0 +1,167 @@
1+package xwm
2+
3+import (
4+ "math"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+const maxRegions = 16
10+
11+// Region is one output's part of the layout and where it is in X space.
12+// X = X.Origin + (layout - Layout.Origin) * Scale. Scale is 1 in v1.
13+type Region struct {
14+ Layout, X ports.Rect // logical layout rect; X rect in X pixels
15+ Scale float64 // 1 in v1; reserved for native resolution
16+}
17+
18+// Space maps layout coordinates (which may be negative) to X coordinates
19+// (non-negative: the layout top-left is X (0,0)). Fixed storage, no allocs.
20+type Space struct {
21+ regions [maxRegions]Region
22+ n int
23+ dropped int // outputs beyond maxRegions in the last Set
24+ ox, oy int // layout point that is X (0,0)
25+ w, h int // X screen size: the extents
26+ gen uint32 // bumped on every change
27+}
28+
29+// Gen returns the change counter.
30+func (s *Space) Gen() uint32 { return s.gen }
31+
32+// Dropped is how many outputs of the last Set did not fit.
33+func (s *Space) Dropped() int { return s.dropped }
34+
35+// Set recomputes the mapping and reports whether it changed.
36+func (s *Space) Set(l ports.Layout) bool {
37+ var next Space
38+ n := len(l)
39+ if n > maxRegions {
40+ next.dropped = n - maxRegions
41+ n = maxRegions
42+ }
43+ next.n = n
44+ var maxX, maxY int
45+ for i := 0; i < n; i++ {
46+ o := l[i]
47+ if i == 0 || o.X < next.ox {
48+ next.ox = o.X
49+ }
50+ if i == 0 || o.Y < next.oy {
51+ next.oy = o.Y
52+ }
53+ if i == 0 || o.X+o.Width > maxX {
54+ maxX = o.X + o.Width
55+ }
56+ if i == 0 || o.Y+o.Height > maxY {
57+ maxY = o.Y + o.Height
58+ }
59+ }
60+ for i := 0; i < n; i++ {
61+ o := l[i]
62+ next.regions[i] = Region{
63+ Layout: ports.Rect{X: o.X, Y: o.Y, W: o.Width, H: o.Height},
64+ X: ports.Rect{X: o.X - next.ox, Y: o.Y - next.oy, W: o.Width, H: o.Height},
65+ Scale: 1,
66+ }
67+ }
68+ next.w, next.h = maxX-next.ox, maxY-next.oy
69+ if next.regions == s.regions && next.n == s.n && next.ox == s.ox && next.oy == s.oy &&
70+ next.w == s.w && next.h == s.h {
71+ s.dropped = next.dropped
72+ return false
73+ }
74+ next.gen = s.gen + 1
75+ *s = next
76+ return true
77+}
78+
79+// Size is the X screen size.
80+func (s *Space) Size() (w, h int) { return s.w, s.h }
81+
82+// nearest returns the index of the region containing the layout point, or
83+// the closest one by squared distance when it is in a hole. -1 when empty.
84+func (s *Space) nearest(lx, ly int) int {
85+ best, bestD := -1, 0
86+ for i := 0; i < s.n; i++ {
87+ r := s.regions[i].Layout
88+ dx := axisGap(lx, r.X, r.X+r.W)
89+ dy := axisGap(ly, r.Y, r.Y+r.H)
90+ d := dx*dx + dy*dy
91+ if best < 0 || d < bestD {
92+ best, bestD = i, d
93+ }
94+ }
95+ return best
96+}
97+
98+// nearestX is nearest in X space.
99+func (s *Space) nearestX(x, y int) int {
100+ best, bestD := -1, 0
101+ for i := 0; i < s.n; i++ {
102+ r := s.regions[i].X
103+ dx := axisGap(x, r.X, r.X+r.W)
104+ dy := axisGap(y, r.Y, r.Y+r.H)
105+ d := dx*dx + dy*dy
106+ if best < 0 || d < bestD {
107+ best, bestD = i, d
108+ }
109+ }
110+ return best
111+}
112+
113+// axisGap is the distance from v to [lo, hi): 0 inside.
114+func axisGap(v, lo, hi int) int {
115+ if v < lo {
116+ return lo - v
117+ }
118+ if v >= hi {
119+ return v - hi + 1
120+ }
121+ return 0
122+}
123+
124+// ToX converts a layout point to X, through the nearest region.
125+func (s *Space) ToX(lx, ly int) (x, y int) {
126+ i := s.nearest(lx, ly)
127+ if i < 0 {
128+ return lx, ly
129+ }
130+ r := &s.regions[i]
131+ return r.X.X + scaled(lx-r.Layout.X, r.Scale), r.X.Y + scaled(ly-r.Layout.Y, r.Scale)
132+}
133+
134+// ToLayout converts an X point to layout coordinates.
135+func (s *Space) ToLayout(x, y int) (lx, ly int) {
136+ i := s.nearestX(x, y)
137+ if i < 0 {
138+ return x, y
139+ }
140+ r := &s.regions[i]
141+ return r.Layout.X + unscaled(x-r.X.X, r.Scale), r.Layout.Y + unscaled(y-r.X.Y, r.Scale)
142+}
143+
144+// RegionAtLayout returns the region containing the layout point.
145+func (s *Space) RegionAtLayout(lx, ly int) (Region, bool) {
146+ for i := 0; i < s.n; i++ {
147+ r := s.regions[i].Layout
148+ if lx >= r.X && lx < r.X+r.W && ly >= r.Y && ly < r.Y+r.H {
149+ return s.regions[i], true
150+ }
151+ }
152+ return Region{}, false
153+}
154+
155+func scaled(v int, sc float64) int {
156+ if sc == 1 || sc == 0 {
157+ return v
158+ }
159+ return int(math.Round(float64(v) * sc))
160+}
161+
162+func unscaled(v int, sc float64) int {
163+ if sc == 1 || sc == 0 {
164+ return v
165+ }
166+ return int(math.Round(float64(v) / sc))
167+}
@@ -0,0 +1,167 @@
1+package xwm
2+
3+import (
4+ "math"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+const maxRegions = 16
10+
11+// Region is one output's part of the layout and where it is in X space.
12+// X = X.Origin + (layout - Layout.Origin) * Scale. Scale is 1 in v1.
13+type Region struct {
14+ Layout, X ports.Rect // logical layout rect; X rect in X pixels
15+ Scale float64 // 1 in v1; reserved for native resolution
16+}
17+
18+// Space maps layout coordinates (which may be negative) to X coordinates
19+// (non-negative: the layout top-left is X (0,0)). Fixed storage, no allocs.
20+type Space struct {
21+ regions [maxRegions]Region
22+ n int
23+ dropped int // outputs beyond maxRegions in the last Set
24+ ox, oy int // layout point that is X (0,0)
25+ w, h int // X screen size: the extents
26+ gen uint32 // bumped on every change
27+}
28+
29+// Gen returns the change counter.
30+func (s *Space) Gen() uint32 { return s.gen }
31+
32+// Dropped is how many outputs of the last Set did not fit.
33+func (s *Space) Dropped() int { return s.dropped }
34+
35+// Set recomputes the mapping and reports whether it changed.
36+func (s *Space) Set(l ports.Layout) bool {
37+ var next Space
38+ n := len(l)
39+ if n > maxRegions {
40+ next.dropped = n - maxRegions
41+ n = maxRegions
42+ }
43+ next.n = n
44+ var maxX, maxY int
45+ for i := 0; i < n; i++ {
46+ o := l[i]
47+ if i == 0 || o.X < next.ox {
48+ next.ox = o.X
49+ }
50+ if i == 0 || o.Y < next.oy {
51+ next.oy = o.Y
52+ }
53+ if i == 0 || o.X+o.Width > maxX {
54+ maxX = o.X + o.Width
55+ }
56+ if i == 0 || o.Y+o.Height > maxY {
57+ maxY = o.Y + o.Height
58+ }
59+ }
60+ for i := 0; i < n; i++ {
61+ o := l[i]
62+ next.regions[i] = Region{
63+ Layout: ports.Rect{X: o.X, Y: o.Y, W: o.Width, H: o.Height},
64+ X: ports.Rect{X: o.X - next.ox, Y: o.Y - next.oy, W: o.Width, H: o.Height},
65+ Scale: 1,
66+ }
67+ }
68+ next.w, next.h = maxX-next.ox, maxY-next.oy
69+ if next.regions == s.regions && next.n == s.n && next.ox == s.ox && next.oy == s.oy &&
70+ next.w == s.w && next.h == s.h {
71+ s.dropped = next.dropped
72+ return false
73+ }
74+ next.gen = s.gen + 1
75+ *s = next
76+ return true
77+}
78+
79+// Size is the X screen size.
80+func (s *Space) Size() (w, h int) { return s.w, s.h }
81+
82+// nearest returns the index of the region containing the layout point, or
83+// the closest one by squared distance when it is in a hole. -1 when empty.
84+func (s *Space) nearest(lx, ly int) int {
85+ best, bestD := -1, 0
86+ for i := 0; i < s.n; i++ {
87+ r := s.regions[i].Layout
88+ dx := axisGap(lx, r.X, r.X+r.W)
89+ dy := axisGap(ly, r.Y, r.Y+r.H)
90+ d := dx*dx + dy*dy
91+ if best < 0 || d < bestD {
92+ best, bestD = i, d
93+ }
94+ }
95+ return best
96+}
97+
98+// nearestX is nearest in X space.
99+func (s *Space) nearestX(x, y int) int {
100+ best, bestD := -1, 0
101+ for i := 0; i < s.n; i++ {
102+ r := s.regions[i].X
103+ dx := axisGap(x, r.X, r.X+r.W)
104+ dy := axisGap(y, r.Y, r.Y+r.H)
105+ d := dx*dx + dy*dy
106+ if best < 0 || d < bestD {
107+ best, bestD = i, d
108+ }
109+ }
110+ return best
111+}
112+
113+// axisGap is the distance from v to [lo, hi): 0 inside.
114+func axisGap(v, lo, hi int) int {
115+ if v < lo {
116+ return lo - v
117+ }
118+ if v >= hi {
119+ return v - hi + 1
120+ }
121+ return 0
122+}
123+
124+// ToX converts a layout point to X, through the nearest region.
125+func (s *Space) ToX(lx, ly int) (x, y int) {
126+ i := s.nearest(lx, ly)
127+ if i < 0 {
128+ return lx, ly
129+ }
130+ r := &s.regions[i]
131+ return r.X.X + scaled(lx-r.Layout.X, r.Scale), r.X.Y + scaled(ly-r.Layout.Y, r.Scale)
132+}
133+
134+// ToLayout converts an X point to layout coordinates.
135+func (s *Space) ToLayout(x, y int) (lx, ly int) {
136+ i := s.nearestX(x, y)
137+ if i < 0 {
138+ return x, y
139+ }
140+ r := &s.regions[i]
141+ return r.Layout.X + unscaled(x-r.X.X, r.Scale), r.Layout.Y + unscaled(y-r.X.Y, r.Scale)
142+}
143+
144+// RegionAtLayout returns the region containing the layout point.
145+func (s *Space) RegionAtLayout(lx, ly int) (Region, bool) {
146+ for i := 0; i < s.n; i++ {
147+ r := s.regions[i].Layout
148+ if lx >= r.X && lx < r.X+r.W && ly >= r.Y && ly < r.Y+r.H {
149+ return s.regions[i], true
150+ }
151+ }
152+ return Region{}, false
153+}
154+
155+func scaled(v int, sc float64) int {
156+ if sc == 1 || sc == 0 {
157+ return v
158+ }
159+ return int(math.Round(float64(v) * sc))
160+}
161+
162+func unscaled(v int, sc float64) int {
163+ if sc == 1 || sc == 0 {
164+ return v
165+ }
166+ return int(math.Round(float64(v) / sc))
167+}
A internal/adapters/wayland/xwm/space_test.go +111 -0
@@ -0,0 +1,111 @@
1+package xwm
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+func out(x, y, w, h int) ports.OutputPlacement {
10+ return ports.OutputPlacement{X: x, Y: y, Width: w, Height: h, Scale: 1}
11+}
12+
13+func TestSpaceLayouts(t *testing.T) {
14+ tests := []struct {
15+ name string
16+ l ports.Layout
17+ w, h int
18+ lx, ly, x, y int
19+ }{
20+ {"single", ports.Layout{out(0, 0, 1920, 1080)}, 1920, 1080, 10, 20, 10, 20},
21+ {"side by side", ports.Layout{out(0, 0, 1920, 1080), out(1920, 0, 1280, 1024)}, 3200, 1080, 2000, 5, 2000, 5},
22+ {"left-of negative origin", ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)}, 2560, 1080, -630, 20, 10, 20},
23+ {"above", ports.Layout{out(0, -500, 800, 500), out(0, 0, 800, 600)}, 800, 1100, 3, -400, 3, 100},
24+ {"below", ports.Layout{out(0, 0, 800, 600), out(0, 600, 800, 400)}, 800, 1000, 3, 700, 3, 700},
25+ {"hole nearest", ports.Layout{out(0, 0, 100, 100), out(300, 0, 100, 100)}, 400, 100, 120, 10, 120, 10},
26+ }
27+ for _, tc := range tests {
28+ t.Run(tc.name, func(t *testing.T) {
29+ var s Space
30+ if !s.Set(tc.l) {
31+ t.Fatal("first Set not changed")
32+ }
33+ if w, h := s.Size(); w != tc.w || h != tc.h {
34+ t.Fatalf("size %dx%d, want %dx%d", w, h, tc.w, tc.h)
35+ }
36+ x, y := s.ToX(tc.lx, tc.ly)
37+ if x != tc.x || y != tc.y {
38+ t.Fatalf("ToX = %d,%d want %d,%d", x, y, tc.x, tc.y)
39+ }
40+ if lx, ly := s.ToLayout(x, y); lx != tc.lx || ly != tc.ly {
41+ t.Fatalf("round trip = %d,%d want %d,%d", lx, ly, tc.lx, tc.ly)
42+ }
43+ if s.Set(tc.l) {
44+ t.Fatal("identical Set reported changed")
45+ }
46+ })
47+ }
48+}
49+
50+func TestSpaceHoleAndRegion(t *testing.T) {
51+ var s Space
52+ s.Set(ports.Layout{out(0, 0, 100, 100), out(300, 0, 100, 100)})
53+ if _, ok := s.RegionAtLayout(200, 50); ok {
54+ t.Fatal("hole must have no region")
55+ }
56+ r, ok := s.RegionAtLayout(350, 50)
57+ if !ok || r.X.X != 300 {
58+ t.Fatalf("region = %+v %v", r, ok)
59+ }
60+}
61+
62+func TestSpaceGenAndEmpty(t *testing.T) {
63+ var s Space
64+ if x, y := s.ToX(5, 6); x != 5 || y != 6 {
65+ t.Fatalf("empty ToX = %d,%d", x, y)
66+ }
67+ s.Set(ports.Layout{out(0, 0, 10, 10)})
68+ g := s.Gen()
69+ s.Set(ports.Layout{out(0, 0, 10, 10)})
70+ if s.Gen() != g {
71+ t.Fatal("gen bumped without change")
72+ }
73+ s.Set(ports.Layout{out(5, 0, 10, 10)})
74+ if s.Gen() == g {
75+ t.Fatal("gen not bumped")
76+ }
77+}
78+
79+func TestSpaceSeventeenOutputs(t *testing.T) {
80+ l := make(ports.Layout, 17)
81+ for i := range l {
82+ l[i] = out(i*100, 0, 100, 100)
83+ }
84+ var s Space
85+ s.Set(l)
86+ if s.Dropped() != 1 {
87+ t.Fatalf("dropped = %d", s.Dropped())
88+ }
89+ if w, _ := s.Size(); w != 1600 {
90+ t.Fatalf("width = %d", w)
91+ }
92+ if x, _ := s.ToX(1550, 5); x != 1550 {
93+ t.Fatalf("ToX = %d", x)
94+ }
95+}
96+
97+func TestSpaceAllocations(t *testing.T) {
98+ l := ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)}
99+ var s Space
100+ s.Set(l)
101+ n := testing.AllocsPerRun(100, func() {
102+ s.Set(l)
103+ x, y := s.ToX(-600, 10)
104+ x, y = s.ToLayout(x, y)
105+ _, _ = s.RegionAtLayout(x, y)
106+ _, _ = s.Size()
107+ })
108+ if n != 0 {
109+ t.Fatalf("allocs = %v", n)
110+ }
111+}
@@ -0,0 +1,111 @@
1+package xwm
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+func out(x, y, w, h int) ports.OutputPlacement {
10+ return ports.OutputPlacement{X: x, Y: y, Width: w, Height: h, Scale: 1}
11+}
12+
13+func TestSpaceLayouts(t *testing.T) {
14+ tests := []struct {
15+ name string
16+ l ports.Layout
17+ w, h int
18+ lx, ly, x, y int
19+ }{
20+ {"single", ports.Layout{out(0, 0, 1920, 1080)}, 1920, 1080, 10, 20, 10, 20},
21+ {"side by side", ports.Layout{out(0, 0, 1920, 1080), out(1920, 0, 1280, 1024)}, 3200, 1080, 2000, 5, 2000, 5},
22+ {"left-of negative origin", ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)}, 2560, 1080, -630, 20, 10, 20},
23+ {"above", ports.Layout{out(0, -500, 800, 500), out(0, 0, 800, 600)}, 800, 1100, 3, -400, 3, 100},
24+ {"below", ports.Layout{out(0, 0, 800, 600), out(0, 600, 800, 400)}, 800, 1000, 3, 700, 3, 700},
25+ {"hole nearest", ports.Layout{out(0, 0, 100, 100), out(300, 0, 100, 100)}, 400, 100, 120, 10, 120, 10},
26+ }
27+ for _, tc := range tests {
28+ t.Run(tc.name, func(t *testing.T) {
29+ var s Space
30+ if !s.Set(tc.l) {
31+ t.Fatal("first Set not changed")
32+ }
33+ if w, h := s.Size(); w != tc.w || h != tc.h {
34+ t.Fatalf("size %dx%d, want %dx%d", w, h, tc.w, tc.h)
35+ }
36+ x, y := s.ToX(tc.lx, tc.ly)
37+ if x != tc.x || y != tc.y {
38+ t.Fatalf("ToX = %d,%d want %d,%d", x, y, tc.x, tc.y)
39+ }
40+ if lx, ly := s.ToLayout(x, y); lx != tc.lx || ly != tc.ly {
41+ t.Fatalf("round trip = %d,%d want %d,%d", lx, ly, tc.lx, tc.ly)
42+ }
43+ if s.Set(tc.l) {
44+ t.Fatal("identical Set reported changed")
45+ }
46+ })
47+ }
48+}
49+
50+func TestSpaceHoleAndRegion(t *testing.T) {
51+ var s Space
52+ s.Set(ports.Layout{out(0, 0, 100, 100), out(300, 0, 100, 100)})
53+ if _, ok := s.RegionAtLayout(200, 50); ok {
54+ t.Fatal("hole must have no region")
55+ }
56+ r, ok := s.RegionAtLayout(350, 50)
57+ if !ok || r.X.X != 300 {
58+ t.Fatalf("region = %+v %v", r, ok)
59+ }
60+}
61+
62+func TestSpaceGenAndEmpty(t *testing.T) {
63+ var s Space
64+ if x, y := s.ToX(5, 6); x != 5 || y != 6 {
65+ t.Fatalf("empty ToX = %d,%d", x, y)
66+ }
67+ s.Set(ports.Layout{out(0, 0, 10, 10)})
68+ g := s.Gen()
69+ s.Set(ports.Layout{out(0, 0, 10, 10)})
70+ if s.Gen() != g {
71+ t.Fatal("gen bumped without change")
72+ }
73+ s.Set(ports.Layout{out(5, 0, 10, 10)})
74+ if s.Gen() == g {
75+ t.Fatal("gen not bumped")
76+ }
77+}
78+
79+func TestSpaceSeventeenOutputs(t *testing.T) {
80+ l := make(ports.Layout, 17)
81+ for i := range l {
82+ l[i] = out(i*100, 0, 100, 100)
83+ }
84+ var s Space
85+ s.Set(l)
86+ if s.Dropped() != 1 {
87+ t.Fatalf("dropped = %d", s.Dropped())
88+ }
89+ if w, _ := s.Size(); w != 1600 {
90+ t.Fatalf("width = %d", w)
91+ }
92+ if x, _ := s.ToX(1550, 5); x != 1550 {
93+ t.Fatalf("ToX = %d", x)
94+ }
95+}
96+
97+func TestSpaceAllocations(t *testing.T) {
98+ l := ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)}
99+ var s Space
100+ s.Set(l)
101+ n := testing.AllocsPerRun(100, func() {
102+ s.Set(l)
103+ x, y := s.ToX(-600, 10)
104+ x, y = s.ToLayout(x, y)
105+ _, _ = s.RegionAtLayout(x, y)
106+ _, _ = s.Size()
107+ })
108+ if n != 0 {
109+ t.Fatalf("allocs = %v", n)
110+ }
111+}

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 25

Tags 10

↑↓ moveenter openesc close