detached b5d00e8

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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) {
909909 if c.security.Protected {
910910 return c.lockHit(x, y)
911911 }
912- o, ok := c.layout().At(x, y)
913- if !ok {
912+ i := c.screenAt(x, y)
913+ if i < 0 {
914914 return 0, 0, 0
915915 }
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)
918918 // Layer popups are over everything; window popups are over the windows
919919 // only, under the top and overlay layers (as drawn).
920920 if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
@@ -1191,7 +1191,7 @@ func (c *Core) Run(ctx context.Context) error {
11911191 if c.layerChanged {
11921192 c.setLayers(c.allLayers())
11931193 }
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)
11951195 }
11961196 if c.workspaceVisible(ctx, false) != nil {
11971197 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, 0914 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 windows918 // 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 nil1197 return nil
M internal/core/drag.go +9 -8
@@ -269,13 +269,13 @@ func (c *Core) abortDrag() {
269269
270270 // dropAt is the drop target under the global point x, y.
271271 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 {
274274 return dropTarget{}
275275 }
276- sc := c.screens[c.screenIndex(o.Info.Name)]
276+ sc := c.screens[i]
277277 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
279279 w := m.Current()
280280 if m.ov.open || w.pinned() || !m.frameHas(float64(lx), float64(ly)) {
281281 return dropTarget{}
@@ -514,15 +514,16 @@ func (c *Core) drop(id WindowID, t dropTarget) {
514514 // tile drag moves the pointer against its usable edge. It reports whether
515515 // the view moved.
516516 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 {
519519 return false
520520 }
521- w := c.screens[c.screenIndex(o.Info.Name)].mon.Current()
521+ sc := c.screens[i]
522+ w := sc.mon.Current()
522523 if w.Overflow == OverflowFixed || len(w.Columns) == 0 || w.pinned() {
523524 return false
524525 }
525- lx := int(c.cursorX) - o.X
526+ lx := int(c.cursorX) - sc.x
526527 dir := 0
527528 switch {
528529 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.mon277 m := sc.mon
278- lx, ly := int(x)-o.X, int(y)-o.Y278+ 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 whether514 // 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 false519 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 false524 return false
524 }525 }
525- lx := int(c.cursorX) - o.X526+ lx := int(c.cursorX) - sc.x
526 dir := 0527 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 {
2121 c.motionMsec = v.TimeMsec
2222 // A locked pointer stays still; relative motion still flows.
2323 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))
2525 }
2626 // The focused screen follows the pointer, so new windows
2727 // and launchers open where the user is.
2828 // Only a pointer entering another output switches: keyboard
2929 // moves to another screen stick until then. Not mid-drag of a
3030 // 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
3536 if err := c.publish(ctx); err != nil {
3637 return err
3738 }
@@ -21,17 +21,18 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
21 c.motionMsec = v.TimeMsec21 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 windows26 // 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: keyboard28 // Only a pointer entering another output switches: keyboard
29 // moves to another screen stick until then. Not mid-drag of a29 // 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.Name32+ 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 err37 return err
37 }38 }
M internal/core/overview.go +3 -4
@@ -835,16 +835,15 @@ func (m *Monitor) overviewAt(x, y float64) WindowID {
835835 // overviewClick closes the overview on the preview under the pointer, on
836836 // the output it shows on. picked is false when no preview is there.
837837 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 {
840840 return false, nil
841841 }
842- i := c.screenIndex(o.Info.Name)
843842 sc := c.screens[i]
844843 if !sc.mon.ov.open {
845844 return false, nil
846845 }
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))
848847 if id == 0 {
849848 return false, nil
850849 }
@@ -835,16 +835,15 @@ func (m *Monitor) overviewAt(x, y float64) WindowID {
835 // overviewClick closes the overview on the preview under the pointer, on835 // 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, nil840 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, nil844 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, nil848 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 {
2929
3030 func (s *screen) name() string { return s.info.Name }
3131
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 {
3337 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}
3544 }
3645
3746 // setScale resizes the logical output; layer placement follows.
3847 func (s *screen) setScale(v float64) {
3948 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))
4151 s.arrange()
4252 }
4353
@@ -63,8 +73,8 @@ func (c *Core) order() {
6373 c.focusScreen = slices.Index(c.screens, focused)
6474 items := make([]placeItem, len(c.screens))
6575 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
6878 if cfg, ok := c.outputConfig(s.name()); ok {
6979 items[i].pos, items[i].anchor = cfg.Pos, cfg.Anchor
7080 }
@@ -107,6 +117,39 @@ func (c *Core) screenIndex(name string) int {
107117 return slices.IndexFunc(c.screens, func(s *screen) bool { return s.name() == name })
108118 }
109119
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+
110153 // screenOf returns the screen and workspace holding the window.
111154 func (c *Core) screenOf(id WindowID) (*screen, *Workspace) {
112155 for _, s := range c.screens {
@@ -429,11 +472,11 @@ func (c *Core) neighbor(d direction) int {
429472 }
430473 // span is the extent of s along the move (lo, hi) and across it.
431474 span := func(s *screen) (lo, hi, clo, chi int) {
432- o := s.mon.Output()
475+ r := s.rect()
433476 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
435478 }
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
437480 }
438481 lo, hi, clo, chi := span(c.cur())
439482 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.H77+ 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.Anchor79 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.W477+ 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.H479+ 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 {
166166 }
167167
168168 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)
174170 if i < 0 {
175171 return 0, 0, 0
176172 }
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)
179176 }
180177 return 0, 0, 0
181178 }
@@ -253,7 +250,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
253250 }
254251 case ports.PointerMotion:
255252 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)
257254 id, x, y := c.lockHit(c.cursorX, c.cursorY)
258255 if id != c.pointer {
259256 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, 0171 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, 0177 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.TimeMsec252 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 = id256 c.pointer = id

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