API

nsh/gesture

Pan responders are drag gestures on top of the gesture responder system: the element claims the touches of a gesture, then hears them alone with the gesture state, which tracks the touches’ centroid, the distance moved and the velocity for you. Rust code claims them with .claim_touches(TouchClaim::…) and hears them with .on_responder_* listeners whose ResponderEvent carries a PanGesture, all in nsh_core::ui::prelude; TSX imports PanResponder from nsh/gesture, whose onPanResponder… callbacks receive the touch event and the gesture state.

use nsh_core::ui::prelude::*; // TouchClaim, ClaimRequest, ResponderEvent, PanGesture

#PanResponder.create

const PanResponder: { create(config: PanResponderCallbacks): PanResponderInstance };

interface PanResponderInstance {
  panHandlers: TouchProps;
}

Creates a pan responder. Spread its panHandlers on the view that should receive the gesture. In Rust, the view claims the touches and listens itself:

use nsh_core::ui::prelude::*;

#[component]
pub fn App() -> impl IntoElement {
    let pos = signal((0.0_f32, 0.0_f32));
    let start = stored((0.0_f32, 0.0_f32));
    view().flex_1().child(
        view()
            .size(px(64.0))
            .rounded(px(32.0))
            .bg(rgb(0x6366f1))
            .style_with(move |s| {
                let (x, y) = pos.get();
                s.translate_x(px(x)).translate_y(px(y))
            })
            .claim_touches(TouchClaim::on_start())
            .on_responder_grant(move |_: &ResponderEvent| {
                start.replace(pos.get_untracked());
            })
            .on_responder_move(move |e: &ResponderEvent| {
                let (x, y) = start.get();
                pos.set((x + e.gesture.delta.x.0, y + e.gesture.delta.y.0));
            })
            .on_responder_release(move |e: &ResponderEvent| {
                log::info!("released at {:.2} pt/ms", e.gesture.vx);
            }),
    )
}

#Callbacks

Every callback receives the touch event (GestureResponderEvent, from nsh) and the gesture state. In Rust, a claim decides with .ask(|request: &ClaimRequest| …), calling request.claim() to take the touches (without ask it takes them always), and each listener gets a ResponderEvent whose gesture is the state.

type PanHandler<R = void> = (e: GestureResponderEvent, gestureState: PanResponderGestureState) => R;
CallbackRustReturnsCalled when
onStartShouldSetPanResponder.claim_touches(TouchClaim::on_start())booleanA finger goes down: return true to become the responder. Asked from the touched view outwards.
onStartShouldSetPanResponderCaptureTouchClaim::on_start().capture()booleanThe same, asked from the outside in before the bubbling one (a parent taking the gesture from its children).
onMoveShouldSetPanResponderTouchClaim::on_move().ask(…)booleanA finger moves: return true to become the responder (e.g. only once Math.abs(g.dx) > 10).
onMoveShouldSetPanResponderCaptureTouchClaim::on_move().capture()booleanThe same, capture phase.
onPanResponderGrant.on_responder_grant(…)This view is the responder now: the gesture starts.
onPanResponderReject.on_responder_reject(…)Another view is the responder and would not let go.
onPanResponderStart.on_responder_start(…)A finger went down while responding.
onPanResponderMove.on_responder_move(…)The fingers moved.
onPanResponderEnd.on_responder_end(…)A finger lifted while responding.
onPanResponderRelease.on_responder_release(…)The last finger lifted: the gesture ended.
onPanResponderTerminate.on_responder_terminate(…)Another view (or a native gesture, like a scroll view) took the touches.
onPanResponderTerminationRequest.keep_touches(true) keeps thembooleanAnother view wants the touches: return false to keep them (default true).
onShouldBlockNativeResponderTouchClaim::block_native(…)booleanWhether to keep native scroll views from taking the touches while responding (default true).

#PanResponderGestureState

interface PanResponderGestureState {
  stateID: number;
  moveX: number;               // latest centroid of the touches (surface points)
  moveY: number;
  x0: number;                  // the centroid when the responder was granted
  y0: number;
  dx: number;                  // distance moved since the grant
  dy: number;
  vx: number;                  // velocity, points per millisecond
  vy: number;
  numberActiveTouches: number;
}

Rust: PanGesture, with points as Point<Points>.

FieldRustDescription
stateIDIdentifies the gesture.
moveX, moveYpositionLatest position of the touches’ centroid, in surface points.
x0, y0originWhere the centroid was when the responder was granted.
dx, dydeltaDistance moved since the grant.
vx, vyvx, vyVelocity in points per millisecond.
numberActiveTouchesactive_touchesFingers down.

#Inside scroll views

While a pan responder is the responder, native scroll views around it do not scroll (onShouldBlockNativeResponder defaults to true). To let a horizontal swipe coexist with vertical scrolling, become the responder only on horizontal moves:

view()
    .claim_touches(TouchClaim::on_move().ask(|request: &ClaimRequest| {
        let d = request.event.gesture.delta;
        if d.x.0.abs() > 10.0 && d.x.0.abs() > d.y.0.abs() {
            request.claim();
        }
    }))
    .on_responder_move(move |e: &ResponderEvent| offset.set(e.gesture.delta.x.0))
    .on_responder_release(move |e: &ResponderEvent| {
        if e.gesture.delta.x.0.abs() > 120.0 { remove() } else { offset.set(0.0) }
    })
    .on_responder_terminate(move |_: &ResponderEvent| offset.set(0.0))

For a pan the platform recognizes, TouchClaim::on_pan(PanActivation::horizontal(10.0, 10.0)) decides on the main thread as the fingers move. The underlying responder props (onStartShouldSetResponder, onResponderGrant, onTouchStart, …) can also be used directly on any element; see Elements.