feat(input): place the cursor on rotated outputs
- commit
- 7d665d68416865f1eae33c46c128a7f17f0b0a92
- author
- brice · · 2026-10-03 18:44 UTC
- parent
- 0868cc3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +50 -4
M internal/adapters/headlessinput/input.go +2 -1
| @@ -161,7 +161,8 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||
| 161 | 161 | y, ey := strconv.ParseFloat(fields[2], 64) | |
| 162 | 162 | if ex == nil && ey == nil && !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0) { | |
| 163 | 163 | if o, ok := layout.At(x, y); ok && moveCursor != nil { | |
| 164 | - | moveCursor(o.Info.Name, (x-float64(o.X))*o.Scale, (y-float64(o.Y))*o.Scale) | |
| 164 | + | tx, ty := o.ToTarget(x, y) | |
| 165 | + | moveCursor(o.Info.Name, tx, ty) | |
| 165 | 166 | } | |
| 166 | 167 | if err := sendPointer(ports.PointerMotion{X: x, Y: y, TimeMsec: now}); err != nil { | |
| 167 | 168 | return err | |
| @@ -161,7 +161,8 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||||
| 161 | y, ey := strconv.ParseFloat(fields[2], 64) | 161 | y, ey := strconv.ParseFloat(fields[2], 64) | ||
| 162 | if ex == nil && ey == nil && !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0) { | 162 | if ex == nil && ey == nil && !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0) { | ||
| 163 | if o, ok := layout.At(x, y); ok && moveCursor != nil { | 163 | if o, ok := layout.At(x, y); ok && moveCursor != nil { | ||
| 164 | - | moveCursor(o.Info.Name, (x-float64(o.X))*o.Scale, (y-float64(o.Y))*o.Scale) | 164 | + | tx, ty := o.ToTarget(x, y) |
| 165 | + | moveCursor(o.Info.Name, tx, ty) | |||
| 165 | } | 166 | } | ||
| 166 | if err := sendPointer(ports.PointerMotion{X: x, Y: y, TimeMsec: now}); err != nil { | 167 | if err := sendPointer(ports.PointerMotion{X: x, Y: y, TimeMsec: now}); err != nil { | ||
| 167 | return err | 168 | return err | ||
M internal/adapters/headlessinput/input_test.go +30 -0
| @@ -105,3 +105,33 @@ func TestPointerScript(t *testing.T) { | |||
| 105 | 105 | t.Fatal(u) | |
| 106 | 106 | } | |
| 107 | 107 | } | |
| 108 | + | ||
| 109 | + | // The cursor is placed in target pixels: after the output transform. | |
| 110 | + | func TestMoveCursorTarget(t *testing.T) { | |
| 111 | + | km, err := xkb.New(xkb.RMLVO{Layout: "us"}) | |
| 112 | + | if err != nil { | |
| 113 | + | t.Skip(err) | |
| 114 | + | } | |
| 115 | + | layouts := make(chan ports.Layout) | |
| 116 | + | script := make(chan string) | |
| 117 | + | input := make(chan ports.InputEvent, 4) | |
| 118 | + | type pos struct { | |
| 119 | + | out string | |
| 120 | + | x, y float64 | |
| 121 | + | } | |
| 122 | + | moved := make(chan pos, 1) | |
| 123 | + | done := make(chan error, 1) | |
| 124 | + | go func() { | |
| 125 | + | done <- Run(context.Background(), km, nil, script, input, layouts, func(o string, x, y float64) { moved <- pos{o, x, y} }, logging.For(context.Background(), "input")) | |
| 126 | + | }() | |
| 127 | + | // Unbuffered: the layout is taken before the move is read. | |
| 128 | + | layouts <- ports.Layout{{Info: ports.OutputInfo{Name: "A", Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}} | |
| 129 | + | script <- "move 10 20" | |
| 130 | + | if got, want := <-moved, (pos{"A", 20, 90}); got != want { | |
| 131 | + | t.Fatalf("got %+v want %+v", got, want) | |
| 132 | + | } | |
| 133 | + | close(script) | |
| 134 | + | if err := <-done; err != nil { | |
| 135 | + | t.Fatal(err) | |
| 136 | + | } | |
| 137 | + | } | |
| @@ -105,3 +105,33 @@ func TestPointerScript(t *testing.T) { | |||||
| 105 | t.Fatal(u) | 105 | t.Fatal(u) | ||
| 106 | } | 106 | } | ||
| 107 | } | 107 | } | ||
| 108 | + | ||||
| 109 | + | // The cursor is placed in target pixels: after the output transform. | |||
| 110 | + | func TestMoveCursorTarget(t *testing.T) { | |||
| 111 | + | km, err := xkb.New(xkb.RMLVO{Layout: "us"}) | |||
| 112 | + | if err != nil { | |||
| 113 | + | t.Skip(err) | |||
| 114 | + | } | |||
| 115 | + | layouts := make(chan ports.Layout) | |||
| 116 | + | script := make(chan string) | |||
| 117 | + | input := make(chan ports.InputEvent, 4) | |||
| 118 | + | type pos struct { | |||
| 119 | + | out string | |||
| 120 | + | x, y float64 | |||
| 121 | + | } | |||
| 122 | + | moved := make(chan pos, 1) | |||
| 123 | + | done := make(chan error, 1) | |||
| 124 | + | go func() { | |||
| 125 | + | done <- Run(context.Background(), km, nil, script, input, layouts, func(o string, x, y float64) { moved <- pos{o, x, y} }, logging.For(context.Background(), "input")) | |||
| 126 | + | }() | |||
| 127 | + | // Unbuffered: the layout is taken before the move is read. | |||
| 128 | + | layouts <- ports.Layout{{Info: ports.OutputInfo{Name: "A", Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}} | |||
| 129 | + | script <- "move 10 20" | |||
| 130 | + | if got, want := <-moved, (pos{"A", 20, 90}); got != want { | |||
| 131 | + | t.Fatalf("got %+v want %+v", got, want) | |||
| 132 | + | } | |||
| 133 | + | close(script) | |||
| 134 | + | if err := <-done; err != nil { | |||
| 135 | + | t.Fatal(err) | |||
| 136 | + | } | |||
| 137 | + | } | |||
M internal/adapters/libinput/libinput.go +4 -2
| @@ -775,10 +775,12 @@ func (p *pointer) bounds() ports.Rect { | |||
| 775 | 775 | return r | |
| 776 | 776 | } | |
| 777 | 777 | ||
| 778 | - | // moved reports the physical position on the output under the pointer; | |
| 778 | + | // moved reports the target position (physical pixels after the output | |
| 779 | + | // transform) on the output under the pointer; | |
| 779 | 780 | // motion tells the device moved it. | |
| 780 | 781 | func (p *pointer) moved(move func(output string, x, y float64, motion bool), motion bool) { | |
| 781 | 782 | if o, ok := p.layout.At(p.x, p.y); ok && move != nil { | |
| 782 | - | move(o.Info.Name, (p.x-float64(o.X))*o.Scale, (p.y-float64(o.Y))*o.Scale, motion) | |
| 783 | + | tx, ty := o.ToTarget(p.x, p.y) | |
| 784 | + | move(o.Info.Name, tx, ty, motion) | |
| 783 | 785 | } | |
| 784 | 786 | } | |
| @@ -775,10 +775,12 @@ func (p *pointer) bounds() ports.Rect { | |||||
| 775 | return r | 775 | return r | ||
| 776 | } | 776 | } | ||
| 777 | 777 | ||||
| 778 | - | // moved reports the physical position on the output under the pointer; | 778 | + | // moved reports the target position (physical pixels after the output |
| 779 | + | // transform) on the output under the pointer; | |||
| 779 | // motion tells the device moved it. | 780 | // motion tells the device moved it. | ||
| 780 | func (p *pointer) moved(move func(output string, x, y float64, motion bool), motion bool) { | 781 | func (p *pointer) moved(move func(output string, x, y float64, motion bool), motion bool) { | ||
| 781 | if o, ok := p.layout.At(p.x, p.y); ok && move != nil { | 782 | if o, ok := p.layout.At(p.x, p.y); ok && move != nil { | ||
| 782 | - | move(o.Info.Name, (p.x-float64(o.X))*o.Scale, (p.y-float64(o.Y))*o.Scale, motion) | 783 | + | tx, ty := o.ToTarget(p.x, p.y) |
| 784 | + | move(o.Info.Name, tx, ty, motion) | |||
| 783 | } | 785 | } | ||
| 784 | } | 786 | } | ||
M internal/adapters/libinput/libinput_test.go +12 -0
| @@ -337,3 +337,15 @@ func TestKeyboardProducerQuarantinesBeforeHotkeysAndLogging(t *testing.T) { | |||
| 337 | 337 | t.Fatalf("quarantined release logged: %s", &buf) | |
| 338 | 338 | } | |
| 339 | 339 | } | |
| 340 | + | ||
| 341 | + | // The cursor is placed in target pixels: after the output transform. | |
| 342 | + | func TestPointerMovedTransform(t *testing.T) { | |
| 343 | + | p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A", Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}}) | |
| 344 | + | p.set(10, 20) | |
| 345 | + | var out string | |
| 346 | + | var px, py float64 | |
| 347 | + | p.moved(func(o string, x, y float64, _ bool) { out, px, py = o, x, y }, true) | |
| 348 | + | if out != "A" || px != 20 || py != 90 { | |
| 349 | + | t.Fatal(out, px, py) | |
| 350 | + | } | |
| 351 | + | } | |
| @@ -337,3 +337,15 @@ func TestKeyboardProducerQuarantinesBeforeHotkeysAndLogging(t *testing.T) { | |||||
| 337 | t.Fatalf("quarantined release logged: %s", &buf) | 337 | t.Fatalf("quarantined release logged: %s", &buf) | ||
| 338 | } | 338 | } | ||
| 339 | } | 339 | } | ||
| 340 | + | ||||
| 341 | + | // The cursor is placed in target pixels: after the output transform. | |||
| 342 | + | func TestPointerMovedTransform(t *testing.T) { | |||
| 343 | + | p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A", Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}}) | |||
| 344 | + | p.set(10, 20) | |||
| 345 | + | var out string | |||
| 346 | + | var px, py float64 | |||
| 347 | + | p.moved(func(o string, x, y float64, _ bool) { out, px, py = o, x, y }, true) | |||
| 348 | + | if out != "A" || px != 20 || py != 90 { | |||
| 349 | + | t.Fatal(out, px, py) | |||
| 350 | + | } | |||
| 351 | + | } | |||
M internal/app/outputs.go +2 -1
| @@ -414,7 +414,8 @@ func (c *cursors) set(output string, cur cursor) { | |||
| 414 | 414 | } | |
| 415 | 415 | } | |
| 416 | 416 | ||
| 417 | - | // move places the cursor on output at physical (x, y). motion is false | |
| 417 | + | // move places the cursor on output at target pixels (x, y): physical, after | |
| 418 | + | // the output transform. motion is false | |
| 418 | 419 | // when the pointer is only placed again (layout or constraint change): it | |
| 419 | 420 | // neither shows an idle-hidden cursor nor delays the hide. | |
| 420 | 421 | func (c *cursors) move(output string, x, y float64, motion bool) { | |
| @@ -414,7 +414,8 @@ func (c *cursors) set(output string, cur cursor) { | |||||
| 414 | } | 414 | } | ||
| 415 | } | 415 | } | ||
| 416 | 416 | ||||
| 417 | - | // move places the cursor on output at physical (x, y). motion is false | 417 | + | // move places the cursor on output at target pixels (x, y): physical, after |
| 418 | + | // the output transform. motion is false | |||
| 418 | // when the pointer is only placed again (layout or constraint change): it | 419 | // when the pointer is only placed again (layout or constraint change): it | ||
| 419 | // neither shows an idle-hidden cursor nor delays the hide. | 420 | // neither shows an idle-hidden cursor nor delays the hide. | ||
| 420 | func (c *cursors) move(output string, x, y float64, motion bool) { | 421 | func (c *cursors) move(output string, x, y float64, motion bool) { | ||