refactor(core): single screen rect and allocation-free pointer hit tests
- commit
- b5d00e8be1b79ae0c940cef3c26fdbe1b6b0218e
- author
- brice · · 2026-10-03 18:37 UTC
- parent
- ff7108c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +128 -38
M internal/core/core.go +5 -5
| @@ -909,12 +909,12 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 909 | 909 | if c.security.Protected { | |
| 910 | 910 | return c.lockHit(x, y) | |
| 911 | 911 | } | |
| 912 | - | o, ok := c.layout().At(x, y) | |
| 913 | - | if !ok { | |
| 912 | + | i := c.screenAt(x, y) | |
| 913 | + | if i < 0 { | |
| 914 | 914 | return 0, 0, 0 | |
| 915 | 915 | } | |
| 916 | - | lx, ly := x-float64(o.X), y-float64(o.Y) | |
| 917 | - | sc := c.screens[c.screenIndex(o.Info.Name)] | |
| 916 | + | sc := c.screens[i] | |
| 917 | + | lx, ly := x-float64(sc.x), y-float64(sc.y) | |
| 918 | 918 | // Layer popups are over everything; window popups are over the windows | |
| 919 | 919 | // only, under the top and overlay layers (as drawn). | |
| 920 | 920 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | |
| @@ -1191,7 +1191,7 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 1191 | 1191 | if c.layerChanged { | |
| 1192 | 1192 | c.setLayers(c.allLayers()) | |
| 1193 | 1193 | } | |
| 1194 | - | c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, c.cursorX, c.cursorY) | |
| 1194 | + | c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY) | |
| 1195 | 1195 | } | |
| 1196 | 1196 | if c.workspaceVisible(ctx, false) != nil { | |
| 1197 | 1197 | return nil | |
| @@ -909,12 +909,12 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 909 | if c.security.Protected { | 909 | if c.security.Protected { | ||
| 910 | return c.lockHit(x, y) | 910 | return c.lockHit(x, y) | ||
| 911 | } | 911 | } | ||
| 912 | - | o, ok := c.layout().At(x, y) | 912 | + | i := c.screenAt(x, y) |
| 913 | - | if !ok { | 913 | + | if i < 0 { |
| 914 | return 0, 0, 0 | 914 | return 0, 0, 0 | ||
| 915 | } | 915 | } | ||
| 916 | - | lx, ly := x-float64(o.X), y-float64(o.Y) | 916 | + | sc := c.screens[i] |
| 917 | - | sc := c.screens[c.screenIndex(o.Info.Name)] | 917 | + | lx, ly := x-float64(sc.x), y-float64(sc.y) |
| 918 | // Layer popups are over everything; window popups are over the windows | 918 | // Layer popups are over everything; window popups are over the windows | ||
| 919 | // only, under the top and overlay layers (as drawn). | 919 | // only, under the top and overlay layers (as drawn). | ||
| 920 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | 920 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | ||
| @@ -1191,7 +1191,7 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 1191 | if c.layerChanged { | 1191 | if c.layerChanged { | ||
| 1192 | c.setLayers(c.allLayers()) | 1192 | c.setLayers(c.allLayers()) | ||
| 1193 | } | 1193 | } | ||
| 1194 | - | c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, c.cursorX, c.cursorY) | 1194 | + | c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY) |
| 1195 | } | 1195 | } | ||
| 1196 | if c.workspaceVisible(ctx, false) != nil { | 1196 | if c.workspaceVisible(ctx, false) != nil { | ||
| 1197 | return nil | 1197 | return nil | ||
M internal/core/drag.go +9 -8
| @@ -269,13 +269,13 @@ func (c *Core) abortDrag() { | |||
| 269 | 269 | ||
| 270 | 270 | // dropAt is the drop target under the global point x, y. | |
| 271 | 271 | func (c *Core) dropAt(id WindowID, x, y float64) dropTarget { | |
| 272 | - | o, ok := c.layout().At(x, y) | |
| 273 | - | if !ok { | |
| 272 | + | i := c.screenAt(x, y) | |
| 273 | + | if i < 0 { | |
| 274 | 274 | return dropTarget{} | |
| 275 | 275 | } | |
| 276 | - | sc := c.screens[c.screenIndex(o.Info.Name)] | |
| 276 | + | sc := c.screens[i] | |
| 277 | 277 | m := sc.mon | |
| 278 | - | lx, ly := int(x)-o.X, int(y)-o.Y | |
| 278 | + | lx, ly := int(x)-sc.x, int(y)-sc.y | |
| 279 | 279 | w := m.Current() | |
| 280 | 280 | if m.ov.open || w.pinned() || !m.frameHas(float64(lx), float64(ly)) { | |
| 281 | 281 | return dropTarget{} | |
| @@ -514,15 +514,16 @@ func (c *Core) drop(id WindowID, t dropTarget) { | |||
| 514 | 514 | // tile drag moves the pointer against its usable edge. It reports whether | |
| 515 | 515 | // the view moved. | |
| 516 | 516 | func (c *Core) edgeScroll(timeMsec uint32) bool { | |
| 517 | - | o, ok := c.layout().At(c.cursorX, c.cursorY) | |
| 518 | - | if !ok || timeMsec-c.drag.scrolled < edgeScrollMsec && c.drag.scrolled != 0 { | |
| 517 | + | i := c.screenAt(c.cursorX, c.cursorY) | |
| 518 | + | if i < 0 || timeMsec-c.drag.scrolled < edgeScrollMsec && c.drag.scrolled != 0 { | |
| 519 | 519 | return false | |
| 520 | 520 | } | |
| 521 | - | w := c.screens[c.screenIndex(o.Info.Name)].mon.Current() | |
| 521 | + | sc := c.screens[i] | |
| 522 | + | w := sc.mon.Current() | |
| 522 | 523 | if w.Overflow == OverflowFixed || len(w.Columns) == 0 || w.pinned() { | |
| 523 | 524 | return false | |
| 524 | 525 | } | |
| 525 | - | lx := int(c.cursorX) - o.X | |
| 526 | + | lx := int(c.cursorX) - sc.x | |
| 526 | 527 | dir := 0 | |
| 527 | 528 | switch { | |
| 528 | 529 | case lx < w.Usable.X+edgeScroll: | |
| @@ -269,13 +269,13 @@ func (c *Core) abortDrag() { | |||||
| 269 | 269 | ||||
| 270 | // dropAt is the drop target under the global point x, y. | 270 | // dropAt is the drop target under the global point x, y. | ||
| 271 | func (c *Core) dropAt(id WindowID, x, y float64) dropTarget { | 271 | func (c *Core) dropAt(id WindowID, x, y float64) dropTarget { | ||
| 272 | - | o, ok := c.layout().At(x, y) | 272 | + | i := c.screenAt(x, y) |
| 273 | - | if !ok { | 273 | + | if i < 0 { |
| 274 | return dropTarget{} | 274 | return dropTarget{} | ||
| 275 | } | 275 | } | ||
| 276 | - | sc := c.screens[c.screenIndex(o.Info.Name)] | 276 | + | sc := c.screens[i] |
| 277 | m := sc.mon | 277 | m := sc.mon | ||
| 278 | - | lx, ly := int(x)-o.X, int(y)-o.Y | 278 | + | lx, ly := int(x)-sc.x, int(y)-sc.y |
| 279 | w := m.Current() | 279 | w := m.Current() | ||
| 280 | if m.ov.open || w.pinned() || !m.frameHas(float64(lx), float64(ly)) { | 280 | if m.ov.open || w.pinned() || !m.frameHas(float64(lx), float64(ly)) { | ||
| 281 | return dropTarget{} | 281 | return dropTarget{} | ||
| @@ -514,15 +514,16 @@ func (c *Core) drop(id WindowID, t dropTarget) { | |||||
| 514 | // tile drag moves the pointer against its usable edge. It reports whether | 514 | // tile drag moves the pointer against its usable edge. It reports whether | ||
| 515 | // the view moved. | 515 | // the view moved. | ||
| 516 | func (c *Core) edgeScroll(timeMsec uint32) bool { | 516 | func (c *Core) edgeScroll(timeMsec uint32) bool { | ||
| 517 | - | o, ok := c.layout().At(c.cursorX, c.cursorY) | 517 | + | i := c.screenAt(c.cursorX, c.cursorY) |
| 518 | - | if !ok || timeMsec-c.drag.scrolled < edgeScrollMsec && c.drag.scrolled != 0 { | 518 | + | if i < 0 || timeMsec-c.drag.scrolled < edgeScrollMsec && c.drag.scrolled != 0 { |
| 519 | return false | 519 | return false | ||
| 520 | } | 520 | } | ||
| 521 | - | w := c.screens[c.screenIndex(o.Info.Name)].mon.Current() | 521 | + | sc := c.screens[i] |
| 522 | + | w := sc.mon.Current() | |||
| 522 | if w.Overflow == OverflowFixed || len(w.Columns) == 0 || w.pinned() { | 523 | if w.Overflow == OverflowFixed || len(w.Columns) == 0 || w.pinned() { | ||
| 523 | return false | 524 | return false | ||
| 524 | } | 525 | } | ||
| 525 | - | lx := int(c.cursorX) - o.X | 526 | + | lx := int(c.cursorX) - sc.x |
| 526 | dir := 0 | 527 | dir := 0 | ||
| 527 | switch { | 528 | switch { | ||
| 528 | case lx < w.Usable.X+edgeScroll: | 529 | case lx < w.Usable.X+edgeScroll: | ||
M internal/core/input.go +6 -5
| @@ -21,17 +21,18 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 21 | 21 | c.motionMsec = v.TimeMsec | |
| 22 | 22 | // A locked pointer stays still; relative motion still flows. | |
| 23 | 23 | if c.constraint.Mode != ports.ConstraintLock { | |
| 24 | - | c.cursorX, c.cursorY = c.constraint.Clamp(c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y)) | |
| 24 | + | c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y)) | |
| 25 | 25 | } | |
| 26 | 26 | // The focused screen follows the pointer, so new windows | |
| 27 | 27 | // and launchers open where the user is. | |
| 28 | 28 | // Only a pointer entering another output switches: keyboard | |
| 29 | 29 | // moves to another screen stick until then. Not mid-drag of a | |
| 30 | 30 | // client; a window drag (drag.go) follows the pointer. | |
| 31 | - | if o, ok := c.layout().At(c.cursorX, c.cursorY); ok && o.Info.Name != c.pointerOutput && c.grab == 0 { | |
| 32 | - | c.pointerOutput = o.Info.Name | |
| 33 | - | if o.Info.Name != c.cur().name() { | |
| 34 | - | c.focusScreen = c.screenIndex(o.Info.Name) | |
| 31 | + | if i := c.screenAt(c.cursorX, c.cursorY); i >= 0 && c.screens[i].name() != c.pointerOutput && c.grab == 0 { | |
| 32 | + | name := c.screens[i].name() | |
| 33 | + | c.pointerOutput = name | |
| 34 | + | if name != c.cur().name() { | |
| 35 | + | c.focusScreen = i | |
| 35 | 36 | if err := c.publish(ctx); err != nil { | |
| 36 | 37 | return err | |
| 37 | 38 | } | |
| @@ -21,17 +21,18 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 21 | c.motionMsec = v.TimeMsec | 21 | c.motionMsec = v.TimeMsec | ||
| 22 | // A locked pointer stays still; relative motion still flows. | 22 | // A locked pointer stays still; relative motion still flows. | ||
| 23 | if c.constraint.Mode != ports.ConstraintLock { | 23 | if c.constraint.Mode != ports.ConstraintLock { | ||
| 24 | - | c.cursorX, c.cursorY = c.constraint.Clamp(c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y)) | 24 | + | c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y)) |
| 25 | } | 25 | } | ||
| 26 | // The focused screen follows the pointer, so new windows | 26 | // The focused screen follows the pointer, so new windows | ||
| 27 | // and launchers open where the user is. | 27 | // and launchers open where the user is. | ||
| 28 | // Only a pointer entering another output switches: keyboard | 28 | // Only a pointer entering another output switches: keyboard | ||
| 29 | // moves to another screen stick until then. Not mid-drag of a | 29 | // moves to another screen stick until then. Not mid-drag of a | ||
| 30 | // client; a window drag (drag.go) follows the pointer. | 30 | // client; a window drag (drag.go) follows the pointer. | ||
| 31 | - | if o, ok := c.layout().At(c.cursorX, c.cursorY); ok && o.Info.Name != c.pointerOutput && c.grab == 0 { | 31 | + | if i := c.screenAt(c.cursorX, c.cursorY); i >= 0 && c.screens[i].name() != c.pointerOutput && c.grab == 0 { |
| 32 | - | c.pointerOutput = o.Info.Name | 32 | + | name := c.screens[i].name() |
| 33 | - | if o.Info.Name != c.cur().name() { | 33 | + | c.pointerOutput = name |
| 34 | - | c.focusScreen = c.screenIndex(o.Info.Name) | 34 | + | if name != c.cur().name() { |
| 35 | + | c.focusScreen = i | |||
| 35 | if err := c.publish(ctx); err != nil { | 36 | if err := c.publish(ctx); err != nil { | ||
| 36 | return err | 37 | return err | ||
| 37 | } | 38 | } | ||
M internal/core/overview.go +3 -4
| @@ -835,16 +835,15 @@ func (m *Monitor) overviewAt(x, y float64) WindowID { | |||
| 835 | 835 | // overviewClick closes the overview on the preview under the pointer, on | |
| 836 | 836 | // the output it shows on. picked is false when no preview is there. | |
| 837 | 837 | func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) { | |
| 838 | - | o, ok := c.layout().At(c.cursorX, c.cursorY) | |
| 839 | - | if !ok { | |
| 838 | + | i := c.screenAt(c.cursorX, c.cursorY) | |
| 839 | + | if i < 0 { | |
| 840 | 840 | return false, nil | |
| 841 | 841 | } | |
| 842 | - | i := c.screenIndex(o.Info.Name) | |
| 843 | 842 | sc := c.screens[i] | |
| 844 | 843 | if !sc.mon.ov.open { | |
| 845 | 844 | return false, nil | |
| 846 | 845 | } | |
| 847 | - | id := sc.mon.overviewAt(c.cursorX-float64(o.X), c.cursorY-float64(o.Y)) | |
| 846 | + | id := sc.mon.overviewAt(c.cursorX-float64(sc.x), c.cursorY-float64(sc.y)) | |
| 848 | 847 | if id == 0 { | |
| 849 | 848 | return false, nil | |
| 850 | 849 | } | |
| @@ -835,16 +835,15 @@ func (m *Monitor) overviewAt(x, y float64) WindowID { | |||||
| 835 | // overviewClick closes the overview on the preview under the pointer, on | 835 | // overviewClick closes the overview on the preview under the pointer, on | ||
| 836 | // the output it shows on. picked is false when no preview is there. | 836 | // the output it shows on. picked is false when no preview is there. | ||
| 837 | func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) { | 837 | func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) { | ||
| 838 | - | o, ok := c.layout().At(c.cursorX, c.cursorY) | 838 | + | i := c.screenAt(c.cursorX, c.cursorY) |
| 839 | - | if !ok { | 839 | + | if i < 0 { |
| 840 | return false, nil | 840 | return false, nil | ||
| 841 | } | 841 | } | ||
| 842 | - | i := c.screenIndex(o.Info.Name) | |||
| 843 | sc := c.screens[i] | 842 | sc := c.screens[i] | ||
| 844 | if !sc.mon.ov.open { | 843 | if !sc.mon.ov.open { | ||
| 845 | return false, nil | 844 | return false, nil | ||
| 846 | } | 845 | } | ||
| 847 | - | id := sc.mon.overviewAt(c.cursorX-float64(o.X), c.cursorY-float64(o.Y)) | 846 | + | id := sc.mon.overviewAt(c.cursorX-float64(sc.x), c.cursorY-float64(sc.y)) |
| 848 | if id == 0 { | 847 | if id == 0 { | ||
| 849 | return false, nil | 848 | return false, nil | ||
| 850 | } | 849 | } | ||
A internal/core/screen_geometry_test.go +49 -0
| @@ -0,0 +1,49 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/assert" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func geometryTiles() []tile { | |
| 10 | + | return []tile{{"A", 0, 0, 200, 100}, {"B", 200, 0, 400, 200}} | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | func TestScreenAt(t *testing.T) { | |
| 14 | + | c := layoutCore(0, geometryTiles()...) | |
| 15 | + | for _, tc := range []struct { | |
| 16 | + | x, y float64 | |
| 17 | + | want int | |
| 18 | + | }{ | |
| 19 | + | {199, 50, 0}, {200, 50, 1}, {250, 150, 1}, {100, 150, -1}, {-1, 0, -1}, | |
| 20 | + | } { | |
| 21 | + | assert.Equal(t, tc.want, c.screenAt(tc.x, tc.y), "(%v,%v)", tc.x, tc.y) | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | p := layoutCore(0, tile{"", 0, 0, 200, 100}) | |
| 25 | + | assert.Equal(t, -1, p.screenAt(10, 10), "a placeholder is never returned") | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func TestClampPointer(t *testing.T) { | |
| 29 | + | c := layoutCore(0, geometryTiles()...) | |
| 30 | + | x, y := c.clampPointer(0, 0, 100, 50) | |
| 31 | + | assert.Equal(t, [2]float64{100, 50}, [2]float64{x, y}) | |
| 32 | + | x, y = c.clampPointer(100, 50, 100, 150) | |
| 33 | + | assert.Equal(t, [2]float64{100, 99}, [2]float64{x, y}) | |
| 34 | + | x, y = c.clampPointer(-5, -5, -10, -10) | |
| 35 | + | assert.Equal(t, [2]float64{0, 0}, [2]float64{x, y}) | |
| 36 | + | ||
| 37 | + | none := layoutCore(0, tile{"", 0, 0, 200, 100}) | |
| 38 | + | x, y = none.clampPointer(0, 0, 500, 500) | |
| 39 | + | assert.Equal(t, [2]float64{500, 500}, [2]float64{x, y}) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func TestScreenAtAllocations(t *testing.T) { | |
| 43 | + | c := layoutCore(0, geometryTiles()...) | |
| 44 | + | n := testing.AllocsPerRun(100, func() { | |
| 45 | + | c.screenAt(250, 150) | |
| 46 | + | c.clampPointer(100, 50, 100, 150) | |
| 47 | + | }) | |
| 48 | + | assert.Zero(t, n) | |
| 49 | + | } | |
| @@ -0,0 +1,49 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/assert" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func geometryTiles() []tile { | |||
| 10 | + | return []tile{{"A", 0, 0, 200, 100}, {"B", 200, 0, 400, 200}} | |||
| 11 | + | } | |||
| 12 | + | ||||
| 13 | + | func TestScreenAt(t *testing.T) { | |||
| 14 | + | c := layoutCore(0, geometryTiles()...) | |||
| 15 | + | for _, tc := range []struct { | |||
| 16 | + | x, y float64 | |||
| 17 | + | want int | |||
| 18 | + | }{ | |||
| 19 | + | {199, 50, 0}, {200, 50, 1}, {250, 150, 1}, {100, 150, -1}, {-1, 0, -1}, | |||
| 20 | + | } { | |||
| 21 | + | assert.Equal(t, tc.want, c.screenAt(tc.x, tc.y), "(%v,%v)", tc.x, tc.y) | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | p := layoutCore(0, tile{"", 0, 0, 200, 100}) | |||
| 25 | + | assert.Equal(t, -1, p.screenAt(10, 10), "a placeholder is never returned") | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | func TestClampPointer(t *testing.T) { | |||
| 29 | + | c := layoutCore(0, geometryTiles()...) | |||
| 30 | + | x, y := c.clampPointer(0, 0, 100, 50) | |||
| 31 | + | assert.Equal(t, [2]float64{100, 50}, [2]float64{x, y}) | |||
| 32 | + | x, y = c.clampPointer(100, 50, 100, 150) | |||
| 33 | + | assert.Equal(t, [2]float64{100, 99}, [2]float64{x, y}) | |||
| 34 | + | x, y = c.clampPointer(-5, -5, -10, -10) | |||
| 35 | + | assert.Equal(t, [2]float64{0, 0}, [2]float64{x, y}) | |||
| 36 | + | ||||
| 37 | + | none := layoutCore(0, tile{"", 0, 0, 200, 100}) | |||
| 38 | + | x, y = none.clampPointer(0, 0, 500, 500) | |||
| 39 | + | assert.Equal(t, [2]float64{500, 500}, [2]float64{x, y}) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func TestScreenAtAllocations(t *testing.T) { | |||
| 43 | + | c := layoutCore(0, geometryTiles()...) | |||
| 44 | + | n := testing.AllocsPerRun(100, func() { | |||
| 45 | + | c.screenAt(250, 150) | |||
| 46 | + | c.clampPointer(100, 50, 100, 150) | |||
| 47 | + | }) | |||
| 48 | + | assert.Zero(t, n) | |||
| 49 | + | } | |||
M internal/core/screens.go +51 -8
| @@ -29,15 +29,25 @@ type screen struct { | |||
| 29 | 29 | ||
| 30 | 30 | func (s *screen) name() string { return s.info.Name } | |
| 31 | 31 | ||
| 32 | - | func (s *screen) placement() ports.OutputPlacement { | |
| 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 } | |
| 34 | + | ||
| 35 | + | // rect is the screen's place in the global logical layout. | |
| 36 | + | func (s *screen) rect() Rect { | |
| 33 | 37 | o := s.mon.Output() | |
| 34 | - | return ports.OutputPlacement{Info: s.info, X: s.x, Y: s.y, Width: o.W, Height: o.H, Scale: s.scale, Primary: s.primary} | |
| 38 | + | return Rect{X: s.x, Y: s.y, W: o.W, H: o.H} | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | func (s *screen) placement() ports.OutputPlacement { | |
| 42 | + | 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} | |
| 35 | 44 | } | |
| 36 | 45 | ||
| 37 | 46 | // setScale resizes the logical output; layer placement follows. | |
| 38 | 47 | func (s *screen) setScale(v float64) { | |
| 39 | 48 | s.scale = SnapScale(v) | |
| 40 | - | s.mon.SetOutput(logical(s.info.Width, s.scale), logical(s.info.Height, s.scale)) | |
| 49 | + | w, h := s.modeSize() | |
| 50 | + | s.mon.SetOutput(logical(w, s.scale), logical(h, s.scale)) | |
| 41 | 51 | s.arrange() | |
| 42 | 52 | } | |
| 43 | 53 | ||
| @@ -63,8 +73,8 @@ func (c *Core) order() { | |||
| 63 | 73 | c.focusScreen = slices.Index(c.screens, focused) | |
| 64 | 74 | items := make([]placeItem, len(c.screens)) | |
| 65 | 75 | for i, s := range c.screens { | |
| 66 | - | o := s.mon.Output() | |
| 67 | - | items[i].name, items[i].w, items[i].h = s.name(), o.W, o.H | |
| 76 | + | r := s.rect() | |
| 77 | + | items[i].name, items[i].w, items[i].h = s.name(), r.W, r.H | |
| 68 | 78 | if cfg, ok := c.outputConfig(s.name()); ok { | |
| 69 | 79 | items[i].pos, items[i].anchor = cfg.Pos, cfg.Anchor | |
| 70 | 80 | } | |
| @@ -107,6 +117,39 @@ func (c *Core) screenIndex(name string) int { | |||
| 107 | 117 | return slices.IndexFunc(c.screens, func(s *screen) bool { return s.name() == name }) | |
| 108 | 118 | } | |
| 109 | 119 | ||
| 120 | + | // screenAt is the index of the first connected screen containing the global | |
| 121 | + | // point, or -1. It does not allocate. | |
| 122 | + | func (c *Core) screenAt(x, y float64) int { | |
| 123 | + | for i, s := range c.screens { | |
| 124 | + | if s.name() == "" { | |
| 125 | + | continue | |
| 126 | + | } | |
| 127 | + | r := s.rect() | |
| 128 | + | if x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) { | |
| 129 | + | return i | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | return -1 | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // clampPointer keeps the pointer on a screen: a point outside every screen | |
| 136 | + | // stays on the screen under (fromX, fromY), or the first one, clamped to its | |
| 137 | + | // edges. | |
| 138 | + | func (c *Core) clampPointer(fromX, fromY, x, y float64) (float64, float64) { | |
| 139 | + | i := c.screenAt(x, y) | |
| 140 | + | if i >= 0 { | |
| 141 | + | return x, y | |
| 142 | + | } | |
| 143 | + | if i = c.screenAt(fromX, fromY); i < 0 { | |
| 144 | + | i = slices.IndexFunc(c.screens, func(s *screen) bool { return s.name() != "" }) | |
| 145 | + | if i < 0 { | |
| 146 | + | return x, y | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | r := c.screens[i].rect() | |
| 150 | + | return min(max(x, float64(r.X)), float64(r.X+r.W)-1), min(max(y, float64(r.Y)), float64(r.Y+r.H)-1) | |
| 151 | + | } | |
| 152 | + | ||
| 110 | 153 | // screenOf returns the screen and workspace holding the window. | |
| 111 | 154 | func (c *Core) screenOf(id WindowID) (*screen, *Workspace) { | |
| 112 | 155 | for _, s := range c.screens { | |
| @@ -429,11 +472,11 @@ func (c *Core) neighbor(d direction) int { | |||
| 429 | 472 | } | |
| 430 | 473 | // span is the extent of s along the move (lo, hi) and across it. | |
| 431 | 474 | span := func(s *screen) (lo, hi, clo, chi int) { | |
| 432 | - | o := s.mon.Output() | |
| 475 | + | r := s.rect() | |
| 433 | 476 | if d == dirUp || d == dirDown { | |
| 434 | - | return s.y, s.y + o.H, s.x, s.x + o.W | |
| 477 | + | return r.Y, r.Y + r.H, r.X, r.X + r.W | |
| 435 | 478 | } | |
| 436 | - | return s.x, s.x + o.W, s.y, s.y + o.H | |
| 479 | + | return r.X, r.X + r.W, r.Y, r.Y + r.H | |
| 437 | 480 | } | |
| 438 | 481 | lo, hi, clo, chi := span(c.cur()) | |
| 439 | 482 | pick := func(fallback bool) int { | |
| @@ -29,15 +29,25 @@ type screen struct { | |||||
| 29 | 29 | ||||
| 30 | func (s *screen) name() string { return s.info.Name } | 30 | func (s *screen) name() string { return s.info.Name } | ||
| 31 | 31 | ||||
| 32 | - | func (s *screen) placement() ports.OutputPlacement { | 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 } | |||
| 34 | + | ||||
| 35 | + | // rect is the screen's place in the global logical layout. | |||
| 36 | + | func (s *screen) rect() Rect { | |||
| 33 | o := s.mon.Output() | 37 | o := s.mon.Output() | ||
| 34 | - | return ports.OutputPlacement{Info: s.info, X: s.x, Y: s.y, Width: o.W, Height: o.H, Scale: s.scale, Primary: s.primary} | 38 | + | return Rect{X: s.x, Y: s.y, W: o.W, H: o.H} |
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func (s *screen) placement() ports.OutputPlacement { | |||
| 42 | + | 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} | |||
| 35 | } | 44 | } | ||
| 36 | 45 | ||||
| 37 | // setScale resizes the logical output; layer placement follows. | 46 | // setScale resizes the logical output; layer placement follows. | ||
| 38 | func (s *screen) setScale(v float64) { | 47 | func (s *screen) setScale(v float64) { | ||
| 39 | s.scale = SnapScale(v) | 48 | s.scale = SnapScale(v) | ||
| 40 | - | s.mon.SetOutput(logical(s.info.Width, s.scale), logical(s.info.Height, s.scale)) | 49 | + | w, h := s.modeSize() |
| 50 | + | s.mon.SetOutput(logical(w, s.scale), logical(h, s.scale)) | |||
| 41 | s.arrange() | 51 | s.arrange() | ||
| 42 | } | 52 | } | ||
| 43 | 53 | ||||
| @@ -63,8 +73,8 @@ func (c *Core) order() { | |||||
| 63 | c.focusScreen = slices.Index(c.screens, focused) | 73 | c.focusScreen = slices.Index(c.screens, focused) | ||
| 64 | items := make([]placeItem, len(c.screens)) | 74 | items := make([]placeItem, len(c.screens)) | ||
| 65 | for i, s := range c.screens { | 75 | for i, s := range c.screens { | ||
| 66 | - | o := s.mon.Output() | 76 | + | r := s.rect() |
| 67 | - | items[i].name, items[i].w, items[i].h = s.name(), o.W, o.H | 77 | + | items[i].name, items[i].w, items[i].h = s.name(), r.W, r.H |
| 68 | if cfg, ok := c.outputConfig(s.name()); ok { | 78 | if cfg, ok := c.outputConfig(s.name()); ok { | ||
| 69 | items[i].pos, items[i].anchor = cfg.Pos, cfg.Anchor | 79 | items[i].pos, items[i].anchor = cfg.Pos, cfg.Anchor | ||
| 70 | } | 80 | } | ||
| @@ -107,6 +117,39 @@ func (c *Core) screenIndex(name string) int { | |||||
| 107 | return slices.IndexFunc(c.screens, func(s *screen) bool { return s.name() == name }) | 117 | return slices.IndexFunc(c.screens, func(s *screen) bool { return s.name() == name }) | ||
| 108 | } | 118 | } | ||
| 109 | 119 | ||||
| 120 | + | // screenAt is the index of the first connected screen containing the global | |||
| 121 | + | // point, or -1. It does not allocate. | |||
| 122 | + | func (c *Core) screenAt(x, y float64) int { | |||
| 123 | + | for i, s := range c.screens { | |||
| 124 | + | if s.name() == "" { | |||
| 125 | + | continue | |||
| 126 | + | } | |||
| 127 | + | r := s.rect() | |||
| 128 | + | if x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) { | |||
| 129 | + | return i | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | return -1 | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | // clampPointer keeps the pointer on a screen: a point outside every screen | |||
| 136 | + | // stays on the screen under (fromX, fromY), or the first one, clamped to its | |||
| 137 | + | // edges. | |||
| 138 | + | func (c *Core) clampPointer(fromX, fromY, x, y float64) (float64, float64) { | |||
| 139 | + | i := c.screenAt(x, y) | |||
| 140 | + | if i >= 0 { | |||
| 141 | + | return x, y | |||
| 142 | + | } | |||
| 143 | + | if i = c.screenAt(fromX, fromY); i < 0 { | |||
| 144 | + | i = slices.IndexFunc(c.screens, func(s *screen) bool { return s.name() != "" }) | |||
| 145 | + | if i < 0 { | |||
| 146 | + | return x, y | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | r := c.screens[i].rect() | |||
| 150 | + | return min(max(x, float64(r.X)), float64(r.X+r.W)-1), min(max(y, float64(r.Y)), float64(r.Y+r.H)-1) | |||
| 151 | + | } | |||
| 152 | + | ||||
| 110 | // screenOf returns the screen and workspace holding the window. | 153 | // screenOf returns the screen and workspace holding the window. | ||
| 111 | func (c *Core) screenOf(id WindowID) (*screen, *Workspace) { | 154 | func (c *Core) screenOf(id WindowID) (*screen, *Workspace) { | ||
| 112 | for _, s := range c.screens { | 155 | for _, s := range c.screens { | ||
| @@ -429,11 +472,11 @@ func (c *Core) neighbor(d direction) int { | |||||
| 429 | } | 472 | } | ||
| 430 | // span is the extent of s along the move (lo, hi) and across it. | 473 | // span is the extent of s along the move (lo, hi) and across it. | ||
| 431 | span := func(s *screen) (lo, hi, clo, chi int) { | 474 | span := func(s *screen) (lo, hi, clo, chi int) { | ||
| 432 | - | o := s.mon.Output() | 475 | + | r := s.rect() |
| 433 | if d == dirUp || d == dirDown { | 476 | if d == dirUp || d == dirDown { | ||
| 434 | - | return s.y, s.y + o.H, s.x, s.x + o.W | 477 | + | return r.Y, r.Y + r.H, r.X, r.X + r.W |
| 435 | } | 478 | } | ||
| 436 | - | return s.x, s.x + o.W, s.y, s.y + o.H | 479 | + | return r.X, r.X + r.W, r.Y, r.Y + r.H |
| 437 | } | 480 | } | ||
| 438 | lo, hi, clo, chi := span(c.cur()) | 481 | lo, hi, clo, chi := span(c.cur()) | ||
| 439 | pick := func(fallback bool) int { | 482 | pick := func(fallback bool) int { | ||
M internal/core/security.go +5 -8
| @@ -166,16 +166,13 @@ func (c *Core) lockKeyboardFocus() WindowID { | |||
| 166 | 166 | } | |
| 167 | 167 | ||
| 168 | 168 | func (c *Core) lockHit(x, y float64) (WindowID, float64, float64) { | |
| 169 | - | o, ok := c.layout().At(x, y) | |
| 170 | - | if !ok { | |
| 171 | - | return 0, 0, 0 | |
| 172 | - | } | |
| 173 | - | i := c.screenIndex(o.Info.Name) | |
| 169 | + | i := c.screenAt(x, y) | |
| 174 | 170 | if i < 0 { | |
| 175 | 171 | return 0, 0, 0 | |
| 176 | 172 | } | |
| 177 | - | if s, ok := c.lockSurface(c.screens[i]); ok { | |
| 178 | - | return s.ID, x - float64(o.X), y - float64(o.Y) | |
| 173 | + | sc := c.screens[i] | |
| 174 | + | if s, ok := c.lockSurface(sc); ok { | |
| 175 | + | return s.ID, x - float64(sc.x), y - float64(sc.y) | |
| 179 | 176 | } | |
| 180 | 177 | return 0, 0, 0 | |
| 181 | 178 | } | |
| @@ -253,7 +250,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 253 | 250 | } | |
| 254 | 251 | case ports.PointerMotion: | |
| 255 | 252 | c.motionMsec = v.TimeMsec | |
| 256 | - | c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y) | |
| 253 | + | c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y) | |
| 257 | 254 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | |
| 258 | 255 | if id != c.pointer { | |
| 259 | 256 | c.pointer = id | |
| @@ -166,16 +166,13 @@ func (c *Core) lockKeyboardFocus() WindowID { | |||||
| 166 | } | 166 | } | ||
| 167 | 167 | ||||
| 168 | func (c *Core) lockHit(x, y float64) (WindowID, float64, float64) { | 168 | func (c *Core) lockHit(x, y float64) (WindowID, float64, float64) { | ||
| 169 | - | o, ok := c.layout().At(x, y) | 169 | + | i := c.screenAt(x, y) |
| 170 | - | if !ok { | |||
| 171 | - | return 0, 0, 0 | |||
| 172 | - | } | |||
| 173 | - | i := c.screenIndex(o.Info.Name) | |||
| 174 | if i < 0 { | 170 | if i < 0 { | ||
| 175 | return 0, 0, 0 | 171 | return 0, 0, 0 | ||
| 176 | } | 172 | } | ||
| 177 | - | if s, ok := c.lockSurface(c.screens[i]); ok { | 173 | + | sc := c.screens[i] |
| 178 | - | return s.ID, x - float64(o.X), y - float64(o.Y) | 174 | + | if s, ok := c.lockSurface(sc); ok { |
| 175 | + | return s.ID, x - float64(sc.x), y - float64(sc.y) | |||
| 179 | } | 176 | } | ||
| 180 | return 0, 0, 0 | 177 | return 0, 0, 0 | ||
| 181 | } | 178 | } | ||
| @@ -253,7 +250,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 253 | } | 250 | } | ||
| 254 | case ports.PointerMotion: | 251 | case ports.PointerMotion: | ||
| 255 | c.motionMsec = v.TimeMsec | 252 | c.motionMsec = v.TimeMsec | ||
| 256 | - | c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y) | 253 | + | c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y) |
| 257 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | 254 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | ||
| 258 | if id != c.pointer { | 255 | if id != c.pointer { | ||
| 259 | c.pointer = id | 256 | c.pointer = id | ||