feat(screenshot): add touch & stylus support for region capture (#3353)

* feat(screenshot): add touch and stylus input support for region capture

* fix(screenshot): guard against finishSelection on touch up during scroll phase

port 1.6

(cherry picked from commit a9fce46fec)
This commit is contained in:
Huỳnh Thiện Lộc
2026-09-07 13:11:50 +00:00
committed by dms-ci[bot]
parent 710f20d085
commit 3f09d7b1c8
3 changed files with 301 additions and 0 deletions
+6
View File
@@ -108,6 +108,7 @@ type RegionSelector struct {
shm *client.Shm
seat *client.Seat
pointer *client.Pointer
touch *client.Touch
keyboard *client.Keyboard
keymap *keymap.Keymap
layerShell *wlr_layer_shell.ZwlrLayerShellV1
@@ -138,6 +139,8 @@ type RegionSelector struct {
selection SelectionState
pointerX float64
pointerY float64
hasTouchPoint bool
touchPointId int32
preSelect Region
showCapturedCursor bool
shiftHeld bool
@@ -1103,6 +1106,9 @@ func (r *RegionSelector) cleanup() {
if r.pointer != nil {
r.pointer.Release()
}
if r.touch != nil {
_ = r.touch.Release()
}
if r.keyboard != nil {
r.keyboard.Release()
}
+143
View File
@@ -19,6 +19,12 @@ func (r *RegionSelector) setupInput() {
r.setupPointerHandlers()
}
}
if e.Capabilities&uint32(client.SeatCapabilityTouch) != 0 && r.touch == nil {
if touch, err := r.seat.GetTouch(); err == nil {
r.touch = touch
r.setupTouchHandlers()
}
}
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && r.keyboard == nil {
if keyboard, err := r.seat.GetKeyboard(); err == nil {
r.keyboard = keyboard
@@ -146,6 +152,143 @@ func (r *RegionSelector) setupPointerHandlers() {
})
}
func (r *RegionSelector) setupTouchHandlers() {
r.touch.SetDownHandler(func(e client.TouchDownEvent) {
r.handleTouchDown(e.Surface, e.Id, e.X, e.Y)
})
r.touch.SetMotionHandler(func(e client.TouchMotionEvent) {
r.handleTouchMotion(e.Id, e.X, e.Y)
})
r.touch.SetUpHandler(func(e client.TouchUpEvent) {
r.handleTouchUp(e.Id)
})
r.touch.SetCancelHandler(func(e client.TouchCancelEvent) {
r.handleTouchCancel()
})
}
func (r *RegionSelector) handleTouchDown(surface *client.Surface, touchId int32, x, y float64) {
if r.hasTouchPoint {
return
}
r.activeSurface = nil
for _, os := range r.surfaces {
if os.wlSurface != nil && surface != nil && os.wlSurface.ID() == surface.ID() {
r.activeSurface = os
break
}
}
if r.activeSurface == nil {
return
}
r.hasTouchPoint = true
r.touchPointId = touchId
if r.phase == phaseScroll {
if r.activeSurface != r.selection.surface {
return
}
switch r.scrollBarHit(x, y) {
case "done", "preview":
r.finishScroll()
case "cancel":
r.cancelled = true
r.running = false
}
return
}
touchX := x + float64(r.activeSurface.output.x)
touchY := y + float64(r.activeSurface.output.y)
if r.ctrlHeld && r.selection.hasSelection {
if handle := r.resizeHandleAt(touchX, touchY); handle != handleNone {
if r.beginSelectionResize(handle, touchX, touchY) {
return
}
}
if r.beginSelectionMove(touchX, touchY) {
r.selection.dragging = true
return
}
}
r.preSelect = Region{}
r.movingSelection = false
r.resizingHandle = handleNone
r.selection.hasSelection = true
r.selection.dragging = true
r.selection.surface = r.activeSurface
r.selection.anchorX = touchX
r.selection.anchorY = touchY
r.selection.currentX = r.selection.anchorX
r.selection.currentY = r.selection.anchorY
for _, os := range r.surfaces {
r.redrawSurface(os)
}
}
func (r *RegionSelector) handleTouchMotion(touchId int32, x, y float64) {
if !r.hasTouchPoint || touchId != r.touchPointId || r.activeSurface == nil {
return
}
if r.phase == phaseScroll {
return
}
if !r.selection.dragging {
return
}
r.updateSelectionCurrent(r.activeSurface, x, y)
}
func (r *RegionSelector) handleTouchUp(touchId int32) {
if !r.hasTouchPoint || touchId != r.touchPointId {
return
}
r.hasTouchPoint = false
r.selection.dragging = false
r.movingSelection = false
r.resizingHandle = handleNone
if r.phase == phaseScroll {
return
}
for _, os := range r.surfaces {
r.redrawSurface(os)
}
if r.screenshoter != nil && r.screenshoter.config.NoConfirm && r.selection.hasSelection {
r.finishSelection()
}
}
func (r *RegionSelector) handleTouchCancel() {
if !r.hasTouchPoint {
return
}
r.hasTouchPoint = false
r.selection.dragging = false
r.movingSelection = false
r.resizingHandle = handleNone
for _, os := range r.surfaces {
r.redrawSurface(os)
}
}
func (r *RegionSelector) updateSelectionCurrent(os *OutputSurface, surfaceX, surfaceY float64) {
if os == nil || os.output == nil || !r.selection.dragging {
return
@@ -0,0 +1,152 @@
package screenshot
import (
"testing"
"github.com/AvengeMedia/dankgo/wayland/client"
)
func newTestSurface(id uint32) *client.Surface {
s := &client.Surface{}
s.SetID(id)
return s
}
func TestTouchInputSequence(t *testing.T) {
surface := newTestSurface(100)
os := &OutputSurface{
output: &WaylandOutput{
x: 0,
y: 0,
},
logicalW: 1920,
logicalH: 1080,
wlSurface: surface,
}
r := &RegionSelector{
running: true,
surfaces: []*OutputSurface{os},
}
// 1. Touch Down
r.handleTouchDown(surface, 1, 100, 200)
if !r.hasTouchPoint || r.touchPointId != 1 {
t.Fatalf("expected hasTouchPoint=true, touchPointId=1, got hasTouchPoint=%v, id=%d", r.hasTouchPoint, r.touchPointId)
}
if !r.selection.dragging || !r.selection.hasSelection {
t.Fatalf("expected dragging=true, hasSelection=true")
}
if r.selection.anchorX != 100 || r.selection.anchorY != 200 {
t.Fatalf("expected anchor (100, 200), got (%.1f, %.1f)", r.selection.anchorX, r.selection.anchorY)
}
// 2. Ignore secondary touch point while primary is active
surface2 := newTestSurface(200)
r.handleTouchDown(surface2, 2, 500, 600)
if r.touchPointId != 1 {
t.Fatalf("expected touchPointId to stay 1, got %d", r.touchPointId)
}
// 3. Touch Motion with matching id
r.handleTouchMotion(1, 400, 500)
if r.selection.currentX != 400 || r.selection.currentY != 500 {
t.Fatalf("expected current (400, 500), got (%.1f, %.1f)", r.selection.currentX, r.selection.currentY)
}
// 4. Touch Motion with mismatched id should be ignored
r.handleTouchMotion(2, 900, 900)
if r.selection.currentX != 400 || r.selection.currentY != 500 {
t.Fatalf("expected current to remain (400, 500), got (%.1f, %.1f)", r.selection.currentX, r.selection.currentY)
}
// 5. Touch Up with mismatched id should not release
r.handleTouchUp(2)
if !r.hasTouchPoint || !r.selection.dragging {
t.Fatalf("expected touch point to remain active on mismatched touch up")
}
// 6. Touch Up with matching id
r.handleTouchUp(1)
if r.hasTouchPoint || r.selection.dragging {
t.Fatalf("expected hasTouchPoint=false, dragging=false after touch up")
}
if !r.selection.hasSelection {
t.Fatalf("expected selection to be preserved after touch up")
}
}
func TestTouchCancel(t *testing.T) {
surface := newTestSurface(100)
os := &OutputSurface{
output: &WaylandOutput{
x: 0,
y: 0,
},
logicalW: 1920,
logicalH: 1080,
wlSurface: surface,
}
r := &RegionSelector{
running: true,
surfaces: []*OutputSurface{os},
}
r.handleTouchDown(surface, 5, 50, 60)
if !r.hasTouchPoint || !r.selection.dragging {
t.Fatalf("expected active touch point")
}
r.handleTouchCancel()
if r.hasTouchPoint || r.selection.dragging {
t.Fatalf("expected touch point and dragging to be cleared on cancel")
}
}
func TestTouchUpScrollPhase(t *testing.T) {
surface := newTestSurface(100)
os := &OutputSurface{
output: &WaylandOutput{
x: 0,
y: 0,
},
logicalW: 1920,
logicalH: 1080,
wlSurface: surface,
}
sc := &Screenshoter{
config: Config{NoConfirm: true},
}
r := &RegionSelector{
screenshoter: sc,
phase: phaseScroll,
running: true,
surfaces: []*OutputSurface{os},
selection: SelectionState{
hasSelection: true,
surface: os,
},
}
r.hasTouchPoint = true
r.touchPointId = 3
r.selection.dragging = true
r.handleTouchUp(3)
if r.hasTouchPoint || r.selection.dragging {
t.Fatalf("expected touch point and dragging cleared")
}
// In scroll phase with NoConfirm, handleTouchUp must not call finishSelection / change running state
if !r.running {
t.Fatalf("expected running to stay true, did not expect finishSelection side-effects")
}
}