detached dc119ed

feat(libinput): stream pinch, hold and any-finger swipes as input events

commit
dc119ed58cbfe68067de8efd4da94840b9001a2f
author
brice · · 2026-10-08 09:32 UTC
committer
brice · · 2026-10-08 09:59 UTC
parent
4fd770d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +281 -53
M internal/adapters/libinput/libinput.go +91 -26
@@ -33,6 +33,11 @@ const (
3333 evSwipeBegin = 800
3434 evSwipeUpdate = 801
3535 evSwipeEnd = 802
36+ evPinchBegin = 803
37+ evPinchUpdate = 804
38+ evPinchEnd = 805
39+ evHoldBegin = 806
40+ evHoldEnd = 807
3641 )
3742
3843 var (
@@ -76,6 +81,8 @@ var (
7681 gestureDX func(gev uintptr) float64
7782 gestureDY func(gev uintptr) float64
7883 gestureUsec func(gev uintptr) uint64
84+ gestureScale func(gev uintptr) float64
85+ gestureAngle func(gev uintptr) float64
7986 deviceRef func(dev uintptr) uintptr
8087 deviceUnref func(dev uintptr) uintptr
8188 hasNatural func(dev uintptr) int32
@@ -149,6 +156,8 @@ func load() error {
149156 reg(&gestureDX, "event_gesture_get_dx_unaccelerated")
150157 reg(&gestureDY, "event_gesture_get_dy_unaccelerated")
151158 reg(&gestureUsec, "event_gesture_get_time_usec")
159+ reg(&gestureScale, "event_gesture_get_scale")
160+ reg(&gestureAngle, "event_gesture_get_angle_delta")
152161 reg(&deviceRef, "device_ref")
153162 reg(&deviceUnref, "device_unref")
154163 reg(&hasNatural, "device_config_scroll_has_natural_scroll")
@@ -325,14 +334,14 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error
325334 }
326335
327336 // inputState is the device state Run owns: the device config, the devices
328-// it configures (referenced until removed) and the swipe in progress on
329-// each touchpad. dev applies the config to libinput; it is a zero-size
337+// it configures (referenced until removed) and the gesture in progress on
338+// the touchpads. dev applies the config to libinput; it is a zero-size
330339 // type, so storing it in the interface allocates nothing.
331340 type inputState struct {
332- cfg ports.InputDevicesConfig
333- dev deviceConfig
334- devices map[uintptr]bool
335- swipes swipes
341+ cfg ports.InputDevicesConfig
342+ dev deviceConfig
343+ devices map[uintptr]bool
344+ gestures gestures
336345 }
337346
338347 // deviceConfig is libinput's device configuration side: what a device is
@@ -478,33 +487,74 @@ func (s *inputState) release() {
478487 clear(s.devices)
479488 }
480489
481-// swipes streams one three- or four-finger swipe at a time to core, which
482-// follows
483-// the fingers. owner is the touchpad of the swipe in progress (0: none);
484-// another touchpad's swipe is ignored until it ends, so core never sees
485-// two streams interleaved.
486-type swipes struct{ owner uintptr }
490+// gestures streams one touchpad gesture at a time to core: a swipe of any
491+// finger count, a pinch or a hold. Core follows the fingers of a three- or
492+// four-finger swipe and hands the other gestures to the window under the
493+// pointer. owner is the touchpad of the gesture in progress (0: none) and
494+// kind its kind; another touchpad's gesture, or another kind's event, is
495+// ignored until it ends, so core never sees two streams interleaved.
496+type gestures struct {
497+ owner uintptr
498+ kind ports.GestureKind
499+}
487500
488-func (s *swipes) begin(dev uintptr, fingers int, at time.Duration) ports.InputEvent {
489- if (fingers != 3 && fingers != 4) || s.owner != 0 {
501+func (g *gestures) begin(dev uintptr, kind ports.GestureKind, fingers int, at time.Duration) ports.InputEvent {
502+ if dev == 0 || fingers <= 0 || g.owner != 0 {
490503 return nil
491504 }
492- s.owner = dev
505+ g.owner, g.kind = dev, kind
506+ switch kind {
507+ case ports.GesturePinch:
508+ return ports.PinchBegin{Fingers: fingers, Time: at}
509+ case ports.GestureHold:
510+ return ports.HoldBegin{Fingers: fingers, Time: at}
511+ }
493512 return ports.SwipeBegin{Fingers: fingers, Time: at}
494513 }
495514
496-func (s *swipes) update(dev uintptr, dx, dy float64, at time.Duration) ports.InputEvent {
497- if dev == 0 || s.owner != dev {
515+// running reports whether dev has a gesture of the given kind in progress.
516+func (g *gestures) running(dev uintptr, kind ports.GestureKind) bool {
517+ return dev != 0 && g.owner == dev && g.kind == kind
518+}
519+
520+func (g *gestures) swipeUpdate(dev uintptr, dx, dy float64, at time.Duration) ports.InputEvent {
521+ if !g.running(dev, ports.GestureSwipe) {
498522 return nil
499523 }
500524 return ports.SwipeUpdate{DX: dx, DY: dy, Time: at}
501525 }
502526
503-func (s *swipes) end(dev uintptr, cancelled bool, at time.Duration) ports.InputEvent {
504- if dev == 0 || s.owner != dev {
527+func (g *gestures) pinchUpdate(dev uintptr, dx, dy, scale, rotation float64, at time.Duration) ports.InputEvent {
528+ if !g.running(dev, ports.GesturePinch) {
529+ return nil
530+ }
531+ return ports.PinchUpdate{DX: dx, DY: dy, Scale: scale, Rotation: rotation, Time: at}
532+}
533+
534+// end finishes the gesture of the given kind on dev.
535+func (g *gestures) end(dev uintptr, kind ports.GestureKind, cancelled bool, at time.Duration) ports.InputEvent {
536+ if !g.running(dev, kind) {
505537 return nil
506538 }
507- s.owner = 0
539+ return g.finish(cancelled, at)
540+}
541+
542+// abort cancels whatever gesture dev has in progress (the device went).
543+func (g *gestures) abort(dev uintptr) ports.InputEvent {
544+ if dev == 0 || g.owner != dev {
545+ return nil
546+ }
547+ return g.finish(true, 0)
548+}
549+
550+func (g *gestures) finish(cancelled bool, at time.Duration) ports.InputEvent {
551+ g.owner = 0
552+ switch g.kind {
553+ case ports.GesturePinch:
554+ return ports.PinchEnd{Cancelled: cancelled, Time: at}
555+ case ports.GestureHold:
556+ return ports.HoldEnd{Cancelled: cancelled, Time: at}
557+ }
508558 return ports.SwipeEnd{Cancelled: cancelled, Time: at}
509559 }
510560
@@ -552,17 +602,32 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
552602 delete(in.devices, dev)
553603 deviceUnref(dev)
554604 }
555- // Core waits for the end of a swipe the device began.
556- return in.swipes.end(dev, true, 0), nil
557- case evSwipeBegin:
605+ // Core waits for the end of a gesture the device began.
606+ return in.gestures.abort(dev), nil
607+ case evSwipeBegin, evPinchBegin, evHoldBegin:
558608 ge := gestureEvent(ev)
559- return in.swipes.begin(eventDevice(ev), int(gestureFingers(ge)), usec(gestureUsec(ge))), nil
609+ kind := ports.GestureSwipe
610+ if eventType(ev) == evPinchBegin {
611+ kind = ports.GesturePinch
612+ } else if eventType(ev) == evHoldBegin {
613+ kind = ports.GestureHold
614+ }
615+ return in.gestures.begin(eventDevice(ev), kind, int(gestureFingers(ge)), usec(gestureUsec(ge))), nil
560616 case evSwipeUpdate:
561617 ge := gestureEvent(ev)
562- return in.swipes.update(eventDevice(ev), gestureDX(ge), gestureDY(ge), usec(gestureUsec(ge))), nil
618+ return in.gestures.swipeUpdate(eventDevice(ev), gestureDX(ge), gestureDY(ge), usec(gestureUsec(ge))), nil
619+ case evPinchUpdate:
620+ ge := gestureEvent(ev)
621+ return in.gestures.pinchUpdate(eventDevice(ev), gestureDX(ge), gestureDY(ge), gestureScale(ge), gestureAngle(ge), usec(gestureUsec(ge))), nil
563622 case evSwipeEnd:
564623 ge := gestureEvent(ev)
565- return in.swipes.end(eventDevice(ev), gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
624+ return in.gestures.end(eventDevice(ev), ports.GestureSwipe, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
625+ case evPinchEnd:
626+ ge := gestureEvent(ev)
627+ return in.gestures.end(eventDevice(ev), ports.GesturePinch, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
628+ case evHoldEnd:
629+ ge := gestureEvent(ev)
630+ return in.gestures.end(eventDevice(ev), ports.GestureHold, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
566631 case evKeyboardKey:
567632 k := keyboardEvent(ev)
568633 code, pressed := keyboardKey(k), keyboardState(k) == 1
@@ -33,6 +33,11 @@ const (
33 evSwipeBegin = 80033 evSwipeBegin = 800
34 evSwipeUpdate = 80134 evSwipeUpdate = 801
35 evSwipeEnd = 80235 evSwipeEnd = 802
36+ evPinchBegin = 803
37+ evPinchUpdate = 804
38+ evPinchEnd = 805
39+ evHoldBegin = 806
40+ evHoldEnd = 807
36 )41 )
37 42
38 var (43 var (
@@ -76,6 +81,8 @@ var (
76 gestureDX func(gev uintptr) float6481 gestureDX func(gev uintptr) float64
77 gestureDY func(gev uintptr) float6482 gestureDY func(gev uintptr) float64
78 gestureUsec func(gev uintptr) uint6483 gestureUsec func(gev uintptr) uint64
84+ gestureScale func(gev uintptr) float64
85+ gestureAngle func(gev uintptr) float64
79 deviceRef func(dev uintptr) uintptr86 deviceRef func(dev uintptr) uintptr
80 deviceUnref func(dev uintptr) uintptr87 deviceUnref func(dev uintptr) uintptr
81 hasNatural func(dev uintptr) int3288 hasNatural func(dev uintptr) int32
@@ -149,6 +156,8 @@ func load() error {
149 reg(&gestureDX, "event_gesture_get_dx_unaccelerated")156 reg(&gestureDX, "event_gesture_get_dx_unaccelerated")
150 reg(&gestureDY, "event_gesture_get_dy_unaccelerated")157 reg(&gestureDY, "event_gesture_get_dy_unaccelerated")
151 reg(&gestureUsec, "event_gesture_get_time_usec")158 reg(&gestureUsec, "event_gesture_get_time_usec")
159+ reg(&gestureScale, "event_gesture_get_scale")
160+ reg(&gestureAngle, "event_gesture_get_angle_delta")
152 reg(&deviceRef, "device_ref")161 reg(&deviceRef, "device_ref")
153 reg(&deviceUnref, "device_unref")162 reg(&deviceUnref, "device_unref")
154 reg(&hasNatural, "device_config_scroll_has_natural_scroll")163 reg(&hasNatural, "device_config_scroll_has_natural_scroll")
@@ -325,14 +334,14 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error
325 }334 }
326 335
327 // inputState is the device state Run owns: the device config, the devices336 // inputState is the device state Run owns: the device config, the devices
328-// it configures (referenced until removed) and the swipe in progress on337+// it configures (referenced until removed) and the gesture in progress on
329-// each touchpad. dev applies the config to libinput; it is a zero-size338+// the touchpads. dev applies the config to libinput; it is a zero-size
330 // type, so storing it in the interface allocates nothing.339 // type, so storing it in the interface allocates nothing.
331 type inputState struct {340 type inputState struct {
332- cfg ports.InputDevicesConfig341+ cfg ports.InputDevicesConfig
333- dev deviceConfig342+ dev deviceConfig
334- devices map[uintptr]bool343+ devices map[uintptr]bool
335- swipes swipes344+ gestures gestures
336 }345 }
337 346
338 // deviceConfig is libinput's device configuration side: what a device is347 // deviceConfig is libinput's device configuration side: what a device is
@@ -478,33 +487,74 @@ func (s *inputState) release() {
478 clear(s.devices)487 clear(s.devices)
479 }488 }
480 489
481-// swipes streams one three- or four-finger swipe at a time to core, which490+// gestures streams one touchpad gesture at a time to core: a swipe of any
482-// follows491+// finger count, a pinch or a hold. Core follows the fingers of a three- or
483-// the fingers. owner is the touchpad of the swipe in progress (0: none);492+// four-finger swipe and hands the other gestures to the window under the
484-// another touchpad's swipe is ignored until it ends, so core never sees493+// pointer. owner is the touchpad of the gesture in progress (0: none) and
485-// two streams interleaved.494+// kind its kind; another touchpad's gesture, or another kind's event, is
486-type swipes struct{ owner uintptr }495+// ignored until it ends, so core never sees two streams interleaved.
496+type gestures struct {
497+ owner uintptr
498+ kind ports.GestureKind
499+}
487 500
488-func (s *swipes) begin(dev uintptr, fingers int, at time.Duration) ports.InputEvent {501+func (g *gestures) begin(dev uintptr, kind ports.GestureKind, fingers int, at time.Duration) ports.InputEvent {
489- if (fingers != 3 && fingers != 4) || s.owner != 0 {502+ if dev == 0 || fingers <= 0 || g.owner != 0 {
490 return nil503 return nil
491 }504 }
492- s.owner = dev505+ g.owner, g.kind = dev, kind
506+ switch kind {
507+ case ports.GesturePinch:
508+ return ports.PinchBegin{Fingers: fingers, Time: at}
509+ case ports.GestureHold:
510+ return ports.HoldBegin{Fingers: fingers, Time: at}
511+ }
493 return ports.SwipeBegin{Fingers: fingers, Time: at}512 return ports.SwipeBegin{Fingers: fingers, Time: at}
494 }513 }
495 514
496-func (s *swipes) update(dev uintptr, dx, dy float64, at time.Duration) ports.InputEvent {515+// running reports whether dev has a gesture of the given kind in progress.
497- if dev == 0 || s.owner != dev {516+func (g *gestures) running(dev uintptr, kind ports.GestureKind) bool {
517+ return dev != 0 && g.owner == dev && g.kind == kind
518+}
519+
520+func (g *gestures) swipeUpdate(dev uintptr, dx, dy float64, at time.Duration) ports.InputEvent {
521+ if !g.running(dev, ports.GestureSwipe) {
498 return nil522 return nil
499 }523 }
500 return ports.SwipeUpdate{DX: dx, DY: dy, Time: at}524 return ports.SwipeUpdate{DX: dx, DY: dy, Time: at}
501 }525 }
502 526
503-func (s *swipes) end(dev uintptr, cancelled bool, at time.Duration) ports.InputEvent {527+func (g *gestures) pinchUpdate(dev uintptr, dx, dy, scale, rotation float64, at time.Duration) ports.InputEvent {
504- if dev == 0 || s.owner != dev {528+ if !g.running(dev, ports.GesturePinch) {
529+ return nil
530+ }
531+ return ports.PinchUpdate{DX: dx, DY: dy, Scale: scale, Rotation: rotation, Time: at}
532+}
533+
534+// end finishes the gesture of the given kind on dev.
535+func (g *gestures) end(dev uintptr, kind ports.GestureKind, cancelled bool, at time.Duration) ports.InputEvent {
536+ if !g.running(dev, kind) {
505 return nil537 return nil
506 }538 }
507- s.owner = 0539+ return g.finish(cancelled, at)
540+}
541+
542+// abort cancels whatever gesture dev has in progress (the device went).
543+func (g *gestures) abort(dev uintptr) ports.InputEvent {
544+ if dev == 0 || g.owner != dev {
545+ return nil
546+ }
547+ return g.finish(true, 0)
548+}
549+
550+func (g *gestures) finish(cancelled bool, at time.Duration) ports.InputEvent {
551+ g.owner = 0
552+ switch g.kind {
553+ case ports.GesturePinch:
554+ return ports.PinchEnd{Cancelled: cancelled, Time: at}
555+ case ports.GestureHold:
556+ return ports.HoldEnd{Cancelled: cancelled, Time: at}
557+ }
508 return ports.SwipeEnd{Cancelled: cancelled, Time: at}558 return ports.SwipeEnd{Cancelled: cancelled, Time: at}
509 }559 }
510 560
@@ -552,17 +602,32 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
552 delete(in.devices, dev)602 delete(in.devices, dev)
553 deviceUnref(dev)603 deviceUnref(dev)
554 }604 }
555- // Core waits for the end of a swipe the device began.605+ // Core waits for the end of a gesture the device began.
556- return in.swipes.end(dev, true, 0), nil606+ return in.gestures.abort(dev), nil
557- case evSwipeBegin:607+ case evSwipeBegin, evPinchBegin, evHoldBegin:
558 ge := gestureEvent(ev)608 ge := gestureEvent(ev)
559- return in.swipes.begin(eventDevice(ev), int(gestureFingers(ge)), usec(gestureUsec(ge))), nil609+ kind := ports.GestureSwipe
610+ if eventType(ev) == evPinchBegin {
611+ kind = ports.GesturePinch
612+ } else if eventType(ev) == evHoldBegin {
613+ kind = ports.GestureHold
614+ }
615+ return in.gestures.begin(eventDevice(ev), kind, int(gestureFingers(ge)), usec(gestureUsec(ge))), nil
560 case evSwipeUpdate:616 case evSwipeUpdate:
561 ge := gestureEvent(ev)617 ge := gestureEvent(ev)
562- return in.swipes.update(eventDevice(ev), gestureDX(ge), gestureDY(ge), usec(gestureUsec(ge))), nil618+ return in.gestures.swipeUpdate(eventDevice(ev), gestureDX(ge), gestureDY(ge), usec(gestureUsec(ge))), nil
619+ case evPinchUpdate:
620+ ge := gestureEvent(ev)
621+ return in.gestures.pinchUpdate(eventDevice(ev), gestureDX(ge), gestureDY(ge), gestureScale(ge), gestureAngle(ge), usec(gestureUsec(ge))), nil
563 case evSwipeEnd:622 case evSwipeEnd:
564 ge := gestureEvent(ev)623 ge := gestureEvent(ev)
565- return in.swipes.end(eventDevice(ev), gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil624+ return in.gestures.end(eventDevice(ev), ports.GestureSwipe, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
625+ case evPinchEnd:
626+ ge := gestureEvent(ev)
627+ return in.gestures.end(eventDevice(ev), ports.GesturePinch, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
628+ case evHoldEnd:
629+ ge := gestureEvent(ev)
630+ return in.gestures.end(eventDevice(ev), ports.GestureHold, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
566 case evKeyboardKey:631 case evKeyboardKey:
567 k := keyboardEvent(ev)632 k := keyboardEvent(ev)
568 code, pressed := keyboardKey(k), keyboardState(k) == 1633 code, pressed := keyboardKey(k), keyboardState(k) == 1
M internal/adapters/libinput/libinput_test.go +91 -25
@@ -42,61 +42,126 @@ func TestHotkey(t *testing.T) {
4242 }
4343 }
4444
45-func TestSwipesIgnoreSecondTouchpad(t *testing.T) {
46- var s swipes
45+func TestGesturesIgnoreSecondTouchpad(t *testing.T) {
46+ var g gestures
4747 const pad, other = 1, 2
48- s.begin(pad, 3, time.Second)
49- if ev := s.begin(other, 3, time.Second); ev != nil {
48+ g.begin(pad, ports.GestureSwipe, 3, time.Second)
49+ if ev := g.begin(other, ports.GestureSwipe, 3, time.Second); ev != nil {
5050 t.Fatalf("second touchpad began: %v", ev)
5151 }
52- if ev := s.update(other, 1, 1, time.Second); ev != nil {
52+ if ev := g.swipeUpdate(other, 1, 1, time.Second); ev != nil {
5353 t.Fatalf("second touchpad moved: %v", ev)
5454 }
55- if ev := s.end(other, false, time.Second); ev != nil {
55+ if ev := g.end(other, ports.GestureSwipe, false, time.Second); ev != nil {
5656 t.Fatalf("second touchpad ended the swipe: %v", ev)
5757 }
58- if ev := s.update(pad, 1, 1, 2*time.Second); ev == nil {
58+ if ev := g.swipeUpdate(pad, 1, 1, 2*time.Second); ev == nil {
5959 t.Fatal("first touchpad lost its swipe")
6060 }
61- s.end(pad, false, 3*time.Second)
62- if ev := s.begin(other, 3, 4*time.Second); ev == nil {
61+ g.end(pad, ports.GestureSwipe, false, 3*time.Second)
62+ if ev := g.begin(other, ports.GestureSwipe, 3, 4*time.Second); ev == nil {
6363 t.Fatal("second touchpad cannot swipe after the first ended")
6464 }
6565 }
6666
67-func TestSwipesStreamThreeFingers(t *testing.T) {
68- var s swipes
67+func TestGesturesStreamSwipe(t *testing.T) {
68+ var g gestures
6969 const pad, other = 1, 2
70- if ev := s.begin(pad, 2, time.Second); ev != nil {
71- t.Fatalf("two fingers: %v", ev)
70+ if ev := g.begin(pad, ports.GestureSwipe, 0, time.Second); ev != nil {
71+ t.Fatalf("no fingers: %v", ev)
7272 }
73- if ev := s.update(pad, 1, 1, time.Second); ev != nil {
73+ if ev := g.swipeUpdate(pad, 1, 1, time.Second); ev != nil {
7474 t.Fatalf("update without a swipe: %v", ev)
7575 }
76- if ev := s.begin(pad, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {
76+ if ev := g.begin(pad, ports.GestureSwipe, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {
7777 t.Fatalf("begin: %v", ev)
7878 }
79- if ev := s.update(pad, -3, 2, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, Time: 2 * time.Second}) {
79+ if ev := g.swipeUpdate(pad, -3, 2, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, Time: 2 * time.Second}) {
8080 t.Fatalf("update: %v", ev)
8181 }
82- if ev := s.end(other, false, 0); ev != nil {
82+ if ev := g.end(other, ports.GestureSwipe, false, 0); ev != nil {
8383 t.Fatalf("end on another device: %v", ev)
8484 }
85- if ev := s.end(pad, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {
85+ if ev := g.end(pad, ports.GestureSwipe, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {
8686 t.Fatalf("end: %v", ev)
8787 }
88- if ev := s.end(pad, false, 0); ev != nil {
88+ if ev := g.end(pad, ports.GestureSwipe, false, 0); ev != nil {
8989 t.Fatalf("second end: %v", ev)
9090 }
9191 }
9292
93-func TestSwipesStreamFourFingers(t *testing.T) {
94- var s swipes
95- if ev := s.begin(1, 4, time.Second); ev != (ports.SwipeBegin{Fingers: 4, Time: time.Second}) {
96- t.Fatalf("four fingers: %v", ev)
93+// Swipes of every finger count stream; core picks which ones it keeps.
94+func TestGesturesStreamAnyFingerCount(t *testing.T) {
95+ for _, fingers := range []int{2, 3, 4, 5} {
96+ var g gestures
97+ if ev := g.begin(1, ports.GestureSwipe, fingers, time.Second); ev != (ports.SwipeBegin{Fingers: fingers, Time: time.Second}) {
98+ t.Fatalf("%d fingers: %v", fingers, ev)
99+ }
100+ if ev := g.begin(1, ports.GestureSwipe, 5, time.Second); ev != nil {
101+ t.Fatalf("a second swipe began during a swipe: %v", ev)
102+ }
103+ }
104+}
105+
106+func TestGesturesStreamPinch(t *testing.T) {
107+ var g gestures
108+ const pad = 1
109+ if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != (ports.PinchBegin{Fingers: 2, Time: time.Second}) {
110+ t.Fatalf("begin: %v", ev)
111+ }
112+ if ev := g.swipeUpdate(pad, 1, 1, time.Second); ev != nil {
113+ t.Fatalf("swipe update during a pinch: %v", ev)
97114 }
98- if ev := s.begin(1, 5, time.Second); ev != nil {
99- t.Fatalf("five fingers during a swipe: %v", ev)
115+ if ev := g.end(pad, ports.GestureHold, false, time.Second); ev != nil {
116+ t.Fatalf("hold end during a pinch: %v", ev)
117+ }
118+ want := ports.PinchUpdate{DX: 1, DY: -2, Scale: 1.5, Rotation: -10, Time: 2 * time.Second}
119+ if ev := g.pinchUpdate(pad, 1, -2, 1.5, -10, 2*time.Second); ev != want {
120+ t.Fatalf("update: %v", ev)
121+ }
122+ if ev := g.end(pad, ports.GesturePinch, false, 3*time.Second); ev != (ports.PinchEnd{Time: 3 * time.Second}) {
123+ t.Fatalf("end: %v", ev)
124+ }
125+ if ev := g.pinchUpdate(pad, 1, 1, 1, 0, 4*time.Second); ev != nil {
126+ t.Fatalf("update after the end: %v", ev)
127+ }
128+}
129+
130+func TestGesturesStreamHold(t *testing.T) {
131+ var g gestures
132+ const pad = 1
133+ if ev := g.begin(pad, ports.GestureHold, 3, time.Second); ev != (ports.HoldBegin{Fingers: 3, Time: time.Second}) {
134+ t.Fatalf("begin: %v", ev)
135+ }
136+ if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != nil {
137+ t.Fatalf("pinch began during a hold: %v", ev)
138+ }
139+ if ev := g.end(pad, ports.GestureHold, true, 2*time.Second); ev != (ports.HoldEnd{Cancelled: true, Time: 2 * time.Second}) {
140+ t.Fatalf("end: %v", ev)
141+ }
142+}
143+
144+// A device that goes during a gesture ends it cancelled, whatever its kind.
145+func TestGesturesAbortOnRemoval(t *testing.T) {
146+ for kind, want := range map[ports.GestureKind]ports.InputEvent{
147+ ports.GestureSwipe: ports.SwipeEnd{Cancelled: true},
148+ ports.GesturePinch: ports.PinchEnd{Cancelled: true},
149+ ports.GestureHold: ports.HoldEnd{Cancelled: true},
150+ } {
151+ var g gestures
152+ g.begin(1, kind, 3, time.Second)
153+ if ev := g.abort(2); ev != nil {
154+ t.Fatalf("kind %d: another device aborted: %v", kind, ev)
155+ }
156+ if ev := g.abort(1); ev != want {
157+ t.Fatalf("kind %d: abort %v, want %v", kind, ev, want)
158+ }
159+ if ev := g.abort(1); ev != nil {
160+ t.Fatalf("kind %d: second abort %v", kind, ev)
161+ }
162+ if ev := g.begin(1, kind, 3, time.Second); ev == nil {
163+ t.Fatalf("kind %d: no gesture after the abort", kind)
164+ }
100165 }
101166 }
102167
@@ -262,6 +327,7 @@ func TestSecurityInputAllKindsProductionEpoch(t *testing.T) {
262327 ports.KeyEvent{Keysym: "secret", Pressed: true}, ports.PointerMotion{X: 1},
263328 ports.PointerButton{Button: 272}, ports.PointerAxis{},
264329 ports.SwipeBegin{}, ports.SwipeUpdate{}, ports.SwipeEnd{},
330+ ports.PinchBegin{}, ports.PinchUpdate{}, ports.PinchEnd{}, ports.HoldBegin{}, ports.HoldEnd{},
265331 }
266332 security.EXPECT().Snapshot().Return(old).Times(len(events))
267333 security.EXPECT().Snapshot().Return(newState).Once()
@@ -42,61 +42,126 @@ func TestHotkey(t *testing.T) {
42 }42 }
43 }43 }
44 44
45-func TestSwipesIgnoreSecondTouchpad(t *testing.T) {45+func TestGesturesIgnoreSecondTouchpad(t *testing.T) {
46- var s swipes46+ var g gestures
47 const pad, other = 1, 247 const pad, other = 1, 2
48- s.begin(pad, 3, time.Second)48+ g.begin(pad, ports.GestureSwipe, 3, time.Second)
49- if ev := s.begin(other, 3, time.Second); ev != nil {49+ if ev := g.begin(other, ports.GestureSwipe, 3, time.Second); ev != nil {
50 t.Fatalf("second touchpad began: %v", ev)50 t.Fatalf("second touchpad began: %v", ev)
51 }51 }
52- if ev := s.update(other, 1, 1, time.Second); ev != nil {52+ if ev := g.swipeUpdate(other, 1, 1, time.Second); ev != nil {
53 t.Fatalf("second touchpad moved: %v", ev)53 t.Fatalf("second touchpad moved: %v", ev)
54 }54 }
55- if ev := s.end(other, false, time.Second); ev != nil {55+ if ev := g.end(other, ports.GestureSwipe, false, time.Second); ev != nil {
56 t.Fatalf("second touchpad ended the swipe: %v", ev)56 t.Fatalf("second touchpad ended the swipe: %v", ev)
57 }57 }
58- if ev := s.update(pad, 1, 1, 2*time.Second); ev == nil {58+ if ev := g.swipeUpdate(pad, 1, 1, 2*time.Second); ev == nil {
59 t.Fatal("first touchpad lost its swipe")59 t.Fatal("first touchpad lost its swipe")
60 }60 }
61- s.end(pad, false, 3*time.Second)61+ g.end(pad, ports.GestureSwipe, false, 3*time.Second)
62- if ev := s.begin(other, 3, 4*time.Second); ev == nil {62+ if ev := g.begin(other, ports.GestureSwipe, 3, 4*time.Second); ev == nil {
63 t.Fatal("second touchpad cannot swipe after the first ended")63 t.Fatal("second touchpad cannot swipe after the first ended")
64 }64 }
65 }65 }
66 66
67-func TestSwipesStreamThreeFingers(t *testing.T) {67+func TestGesturesStreamSwipe(t *testing.T) {
68- var s swipes68+ var g gestures
69 const pad, other = 1, 269 const pad, other = 1, 2
70- if ev := s.begin(pad, 2, time.Second); ev != nil {70+ if ev := g.begin(pad, ports.GestureSwipe, 0, time.Second); ev != nil {
71- t.Fatalf("two fingers: %v", ev)71+ t.Fatalf("no fingers: %v", ev)
72 }72 }
73- if ev := s.update(pad, 1, 1, time.Second); ev != nil {73+ if ev := g.swipeUpdate(pad, 1, 1, time.Second); ev != nil {
74 t.Fatalf("update without a swipe: %v", ev)74 t.Fatalf("update without a swipe: %v", ev)
75 }75 }
76- if ev := s.begin(pad, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {76+ if ev := g.begin(pad, ports.GestureSwipe, 3, time.Second); ev != (ports.SwipeBegin{Fingers: 3, Time: time.Second}) {
77 t.Fatalf("begin: %v", ev)77 t.Fatalf("begin: %v", ev)
78 }78 }
79- if ev := s.update(pad, -3, 2, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, Time: 2 * time.Second}) {79+ if ev := g.swipeUpdate(pad, -3, 2, 2*time.Second); ev != (ports.SwipeUpdate{DX: -3, DY: 2, Time: 2 * time.Second}) {
80 t.Fatalf("update: %v", ev)80 t.Fatalf("update: %v", ev)
81 }81 }
82- if ev := s.end(other, false, 0); ev != nil {82+ if ev := g.end(other, ports.GestureSwipe, false, 0); ev != nil {
83 t.Fatalf("end on another device: %v", ev)83 t.Fatalf("end on another device: %v", ev)
84 }84 }
85- if ev := s.end(pad, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {85+ if ev := g.end(pad, ports.GestureSwipe, true, 3*time.Second); ev != (ports.SwipeEnd{Cancelled: true, Time: 3 * time.Second}) {
86 t.Fatalf("end: %v", ev)86 t.Fatalf("end: %v", ev)
87 }87 }
88- if ev := s.end(pad, false, 0); ev != nil {88+ if ev := g.end(pad, ports.GestureSwipe, false, 0); ev != nil {
89 t.Fatalf("second end: %v", ev)89 t.Fatalf("second end: %v", ev)
90 }90 }
91 }91 }
92 92
93-func TestSwipesStreamFourFingers(t *testing.T) {93+// Swipes of every finger count stream; core picks which ones it keeps.
94- var s swipes94+func TestGesturesStreamAnyFingerCount(t *testing.T) {
95- if ev := s.begin(1, 4, time.Second); ev != (ports.SwipeBegin{Fingers: 4, Time: time.Second}) {95+ for _, fingers := range []int{2, 3, 4, 5} {
96- t.Fatalf("four fingers: %v", ev)96+ var g gestures
97+ if ev := g.begin(1, ports.GestureSwipe, fingers, time.Second); ev != (ports.SwipeBegin{Fingers: fingers, Time: time.Second}) {
98+ t.Fatalf("%d fingers: %v", fingers, ev)
99+ }
100+ if ev := g.begin(1, ports.GestureSwipe, 5, time.Second); ev != nil {
101+ t.Fatalf("a second swipe began during a swipe: %v", ev)
102+ }
103+ }
104+}
105+
106+func TestGesturesStreamPinch(t *testing.T) {
107+ var g gestures
108+ const pad = 1
109+ if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != (ports.PinchBegin{Fingers: 2, Time: time.Second}) {
110+ t.Fatalf("begin: %v", ev)
111+ }
112+ if ev := g.swipeUpdate(pad, 1, 1, time.Second); ev != nil {
113+ t.Fatalf("swipe update during a pinch: %v", ev)
97 }114 }
98- if ev := s.begin(1, 5, time.Second); ev != nil {115+ if ev := g.end(pad, ports.GestureHold, false, time.Second); ev != nil {
99- t.Fatalf("five fingers during a swipe: %v", ev)116+ t.Fatalf("hold end during a pinch: %v", ev)
117+ }
118+ want := ports.PinchUpdate{DX: 1, DY: -2, Scale: 1.5, Rotation: -10, Time: 2 * time.Second}
119+ if ev := g.pinchUpdate(pad, 1, -2, 1.5, -10, 2*time.Second); ev != want {
120+ t.Fatalf("update: %v", ev)
121+ }
122+ if ev := g.end(pad, ports.GesturePinch, false, 3*time.Second); ev != (ports.PinchEnd{Time: 3 * time.Second}) {
123+ t.Fatalf("end: %v", ev)
124+ }
125+ if ev := g.pinchUpdate(pad, 1, 1, 1, 0, 4*time.Second); ev != nil {
126+ t.Fatalf("update after the end: %v", ev)
127+ }
128+}
129+
130+func TestGesturesStreamHold(t *testing.T) {
131+ var g gestures
132+ const pad = 1
133+ if ev := g.begin(pad, ports.GestureHold, 3, time.Second); ev != (ports.HoldBegin{Fingers: 3, Time: time.Second}) {
134+ t.Fatalf("begin: %v", ev)
135+ }
136+ if ev := g.begin(pad, ports.GesturePinch, 2, time.Second); ev != nil {
137+ t.Fatalf("pinch began during a hold: %v", ev)
138+ }
139+ if ev := g.end(pad, ports.GestureHold, true, 2*time.Second); ev != (ports.HoldEnd{Cancelled: true, Time: 2 * time.Second}) {
140+ t.Fatalf("end: %v", ev)
141+ }
142+}
143+
144+// A device that goes during a gesture ends it cancelled, whatever its kind.
145+func TestGesturesAbortOnRemoval(t *testing.T) {
146+ for kind, want := range map[ports.GestureKind]ports.InputEvent{
147+ ports.GestureSwipe: ports.SwipeEnd{Cancelled: true},
148+ ports.GesturePinch: ports.PinchEnd{Cancelled: true},
149+ ports.GestureHold: ports.HoldEnd{Cancelled: true},
150+ } {
151+ var g gestures
152+ g.begin(1, kind, 3, time.Second)
153+ if ev := g.abort(2); ev != nil {
154+ t.Fatalf("kind %d: another device aborted: %v", kind, ev)
155+ }
156+ if ev := g.abort(1); ev != want {
157+ t.Fatalf("kind %d: abort %v, want %v", kind, ev, want)
158+ }
159+ if ev := g.abort(1); ev != nil {
160+ t.Fatalf("kind %d: second abort %v", kind, ev)
161+ }
162+ if ev := g.begin(1, kind, 3, time.Second); ev == nil {
163+ t.Fatalf("kind %d: no gesture after the abort", kind)
164+ }
100 }165 }
101 }166 }
102 167
@@ -262,6 +327,7 @@ func TestSecurityInputAllKindsProductionEpoch(t *testing.T) {
262 ports.KeyEvent{Keysym: "secret", Pressed: true}, ports.PointerMotion{X: 1},327 ports.KeyEvent{Keysym: "secret", Pressed: true}, ports.PointerMotion{X: 1},
263 ports.PointerButton{Button: 272}, ports.PointerAxis{},328 ports.PointerButton{Button: 272}, ports.PointerAxis{},
264 ports.SwipeBegin{}, ports.SwipeUpdate{}, ports.SwipeEnd{},329 ports.SwipeBegin{}, ports.SwipeUpdate{}, ports.SwipeEnd{},
330+ ports.PinchBegin{}, ports.PinchUpdate{}, ports.PinchEnd{}, ports.HoldBegin{}, ports.HoldEnd{},
265 }331 }
266 security.EXPECT().Snapshot().Return(old).Times(len(events))332 security.EXPECT().Snapshot().Return(old).Times(len(events))
267 security.EXPECT().Snapshot().Return(newState).Once()333 security.EXPECT().Snapshot().Return(newState).Once()
M internal/ports/client_commands.go +49 -0
@@ -192,6 +192,55 @@ type PointerAxisTo struct {
192192
193193 func (PointerAxisTo) clientCommand() {}
194194
195+// GestureKind is a touchpad gesture a client can receive
196+// (zwp_pointer_gestures_v1).
197+type GestureKind uint8
198+
199+const (
200+ GestureSwipe GestureKind = iota
201+ GesturePinch
202+ GestureHold
203+)
204+
205+// GestureBeginTo starts a gesture on a window with the pointer focus.
206+// Wayland assigns the serial. Time is the device timestamp
207+// (CLOCK_MONOTONIC), like the updates and the end of the gesture.
208+type GestureBeginTo struct {
209+ ID WindowID
210+ Kind GestureKind
211+ Fingers int
212+ Time time.Duration
213+}
214+
215+func (GestureBeginTo) clientCommand() {}
216+
217+// GestureUpdateTo moves the gesture of a window. DX and DY are the
218+// centre's movement; Scale and Rotation are only for a pinch (see
219+// PinchUpdate). A hold has no updates.
220+type GestureUpdateTo struct {
221+ ID WindowID
222+ Kind GestureKind
223+ DX, DY float64
224+ Scale float64
225+ Rotation float64
226+ Time time.Duration
227+}
228+
229+func (GestureUpdateTo) clientCommand() {}
230+
231+// GestureEndTo ends the gesture of a window. Cancelled is set when the
232+// gesture did not complete: the pointer focus moved, the window went away
233+// or the device cancelled it. Ending a gesture that is not running does
234+// nothing.
235+type GestureEndTo struct {
236+ ID WindowID
237+ Kind GestureKind
238+ Cancelled bool
239+ Time time.Duration
240+}
241+
242+func (GestureEndTo) clientCommand() {}
243+
195244 // ForwardKey carries core → wayland unbound keys.
196245 type ForwardKey struct {
197246 ID WindowID
@@ -192,6 +192,55 @@ type PointerAxisTo struct {
192 192
193 func (PointerAxisTo) clientCommand() {}193 func (PointerAxisTo) clientCommand() {}
194 194
195+// GestureKind is a touchpad gesture a client can receive
196+// (zwp_pointer_gestures_v1).
197+type GestureKind uint8
198+
199+const (
200+ GestureSwipe GestureKind = iota
201+ GesturePinch
202+ GestureHold
203+)
204+
205+// GestureBeginTo starts a gesture on a window with the pointer focus.
206+// Wayland assigns the serial. Time is the device timestamp
207+// (CLOCK_MONOTONIC), like the updates and the end of the gesture.
208+type GestureBeginTo struct {
209+ ID WindowID
210+ Kind GestureKind
211+ Fingers int
212+ Time time.Duration
213+}
214+
215+func (GestureBeginTo) clientCommand() {}
216+
217+// GestureUpdateTo moves the gesture of a window. DX and DY are the
218+// centre's movement; Scale and Rotation are only for a pinch (see
219+// PinchUpdate). A hold has no updates.
220+type GestureUpdateTo struct {
221+ ID WindowID
222+ Kind GestureKind
223+ DX, DY float64
224+ Scale float64
225+ Rotation float64
226+ Time time.Duration
227+}
228+
229+func (GestureUpdateTo) clientCommand() {}
230+
231+// GestureEndTo ends the gesture of a window. Cancelled is set when the
232+// gesture did not complete: the pointer focus moved, the window went away
233+// or the device cancelled it. Ending a gesture that is not running does
234+// nothing.
235+type GestureEndTo struct {
236+ ID WindowID
237+ Kind GestureKind
238+ Cancelled bool
239+ Time time.Duration
240+}
241+
242+func (GestureEndTo) clientCommand() {}
243+
195 // ForwardKey carries core → wayland unbound keys.244 // ForwardKey carries core → wayland unbound keys.
196 type ForwardKey struct {245 type ForwardKey struct {
197 ID WindowID246 ID WindowID
M internal/ports/input_events.go +50 -2
@@ -88,8 +88,10 @@ type PointerAxis struct {
8888
8989 func (PointerAxis) inputEvent() {}
9090
91-// SwipeBegin starts a three- or four-finger touchpad swipe. Time is the
92-// device timestamp (CLOCK_MONOTONIC), shared by the updates and the end.
91+// SwipeBegin starts a touchpad swipe of any finger count; core keeps three
92+// and four fingers and forwards the others to the window under the
93+// pointer. Time is the device timestamp (CLOCK_MONOTONIC), shared by the
94+// updates and the end.
9395 type SwipeBegin struct {
9496 Fingers int
9597 Time time.Duration
@@ -112,3 +114,49 @@ type SwipeEnd struct {
112114 func (SwipeBegin) inputEvent() {}
113115 func (SwipeUpdate) inputEvent() {}
114116 func (SwipeEnd) inputEvent() {}
117+
118+// PinchBegin starts a touchpad pinch. Time is the device timestamp
119+// (CLOCK_MONOTONIC), shared by the updates and the end. A pinch always goes
120+// to the window under the pointer.
121+type PinchBegin struct {
122+ Fingers int
123+ Time time.Duration
124+}
125+
126+// PinchUpdate moves a pinch: the fingers' centre by the unaccelerated
127+// deltas DX, DY (touchpad units), Scale is the spread of the fingers
128+// relative to the begin (1.0 there) and Rotation the angle turned since the
129+// previous event, in degrees clockwise.
130+type PinchUpdate struct {
131+ DX, DY float64
132+ Scale float64
133+ Rotation float64
134+ Time time.Duration
135+}
136+
137+// PinchEnd lifts the fingers. Cancelled is set when libinput cancelled the
138+// pinch.
139+type PinchEnd struct {
140+ Cancelled bool
141+ Time time.Duration
142+}
143+
144+// HoldBegin starts a touchpad hold: fingers resting on the pad. Like a
145+// pinch it goes to the window under the pointer.
146+type HoldBegin struct {
147+ Fingers int
148+ Time time.Duration
149+}
150+
151+// HoldEnd lifts the fingers. Cancelled is set when the fingers moved or
152+// another finger landed, so the hold became something else.
153+type HoldEnd struct {
154+ Cancelled bool
155+ Time time.Duration
156+}
157+
158+func (PinchBegin) inputEvent() {}
159+func (PinchUpdate) inputEvent() {}
160+func (PinchEnd) inputEvent() {}
161+func (HoldBegin) inputEvent() {}
162+func (HoldEnd) inputEvent() {}
@@ -88,8 +88,10 @@ type PointerAxis struct {
88 88
89 func (PointerAxis) inputEvent() {}89 func (PointerAxis) inputEvent() {}
90 90
91-// SwipeBegin starts a three- or four-finger touchpad swipe. Time is the91+// SwipeBegin starts a touchpad swipe of any finger count; core keeps three
92-// device timestamp (CLOCK_MONOTONIC), shared by the updates and the end.92+// and four fingers and forwards the others to the window under the
93+// pointer. Time is the device timestamp (CLOCK_MONOTONIC), shared by the
94+// updates and the end.
93 type SwipeBegin struct {95 type SwipeBegin struct {
94 Fingers int96 Fingers int
95 Time time.Duration97 Time time.Duration
@@ -112,3 +114,49 @@ type SwipeEnd struct {
112 func (SwipeBegin) inputEvent() {}114 func (SwipeBegin) inputEvent() {}
113 func (SwipeUpdate) inputEvent() {}115 func (SwipeUpdate) inputEvent() {}
114 func (SwipeEnd) inputEvent() {}116 func (SwipeEnd) inputEvent() {}
117+
118+// PinchBegin starts a touchpad pinch. Time is the device timestamp
119+// (CLOCK_MONOTONIC), shared by the updates and the end. A pinch always goes
120+// to the window under the pointer.
121+type PinchBegin struct {
122+ Fingers int
123+ Time time.Duration
124+}
125+
126+// PinchUpdate moves a pinch: the fingers' centre by the unaccelerated
127+// deltas DX, DY (touchpad units), Scale is the spread of the fingers
128+// relative to the begin (1.0 there) and Rotation the angle turned since the
129+// previous event, in degrees clockwise.
130+type PinchUpdate struct {
131+ DX, DY float64
132+ Scale float64
133+ Rotation float64
134+ Time time.Duration
135+}
136+
137+// PinchEnd lifts the fingers. Cancelled is set when libinput cancelled the
138+// pinch.
139+type PinchEnd struct {
140+ Cancelled bool
141+ Time time.Duration
142+}
143+
144+// HoldBegin starts a touchpad hold: fingers resting on the pad. Like a
145+// pinch it goes to the window under the pointer.
146+type HoldBegin struct {
147+ Fingers int
148+ Time time.Duration
149+}
150+
151+// HoldEnd lifts the fingers. Cancelled is set when the fingers moved or
152+// another finger landed, so the hold became something else.
153+type HoldEnd struct {
154+ Cancelled bool
155+ Time time.Duration
156+}
157+
158+func (PinchBegin) inputEvent() {}
159+func (PinchUpdate) inputEvent() {}
160+func (PinchEnd) inputEvent() {}
161+func (HoldBegin) inputEvent() {}
162+func (HoldEnd) inputEvent() {}

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 23

Tags 10

↑↓ moveenter openesc close