diff --git a/core/internal/screenshot/region.go b/core/internal/screenshot/region.go index 9ccf78dcc..e992a9fb5 100644 --- a/core/internal/screenshot/region.go +++ b/core/internal/screenshot/region.go @@ -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() } diff --git a/core/internal/screenshot/region_input.go b/core/internal/screenshot/region_input.go index ca776a53b..4aec305b2 100644 --- a/core/internal/screenshot/region_input.go +++ b/core/internal/screenshot/region_input.go @@ -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 diff --git a/core/internal/screenshot/region_touch_test.go b/core/internal/screenshot/region_touch_test.go new file mode 100644 index 000000000..b7d91db44 --- /dev/null +++ b/core/internal/screenshot/region_touch_test.go @@ -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") + } +}