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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +279 -1
M Makefile +1 -1
| @@ -70,7 +70,7 @@ adapter-imports: | |||
| 70 | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 71 | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 72 | 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=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 | |
| 74 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | |
| 75 | 75 | check: vet test arch fakes-check log-check perf-check | |
| 76 | 76 | ||
| @@ -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=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 |
| 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | ||
| 75 | check: vet test arch fakes-check log-check perf-check | 75 | 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 | + | } | |||