Files
gui/plans/rngh-press-conditional.md
zeekayandHanzo Dev 69c6e860d7 transplant onto tamagui v2.6.0, brand as a re-runnable transform
This repo could never sync. There was NO merge-base with tamagui — 372 commits
here against 15,791 there, unrelated histories — because it began as a
copy-paste rebrand with a flattened layout. Every "sync" since was a hand
re-application that drifted, which is why the tree sat on 7.3.1 while upstream
shipped fixes we then rediscovered by hand.

This commit's PARENT IS TAMAGUI. A future sync is `git merge tama/main` then
`node scripts/rebrand.mjs` — a real merge, because the history is now shared.

174 packages: 166 upstream, our 8 restored (chrome, shell, telemetry, recipes,
get, font-geist-{sans,mono}, @hanzo/booker), bento-get gained. 170/170 build.

Our patches to files UPSTREAM ALSO OWNS are carried, which the package-level
delta missed and which was where the value actually was:
  - useMedia's server snapshot (`initState ?? getMedia()`). Upstream still
    returns undefined there, so every Tamagui Dialog kills a prerender — the
    exact crash hanzo.app spent a day on.
  - data-slot on web host nodes, absent upstream entirely.
Both applied ONTO upstream's newer files (it rewrote useMedia +150/-63 and
createComponent +160/-55), marked `PATCH (ours, carried across the transplant)`.

Seven traps are encoded in scripts/rebrand.mjs, each found by a gate:
skip itself and lockfiles; special-case the double-branded names in BOTH scoped
and bare form; map path strings, not just paths; drop a level for the flattened
group; check disk rather than a stale index.

Rust build artifacts are NOT committed. An earlier draft of this commit swept
apps/desktop/src-tauri/target/debug in via `git add -A` — 3,859 files including
a 272MB .a, over GitHub's 100MB cap, so the push could never have landed. They
are gitignored now.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-07-30 18:25:06 -07:00

4.7 KiB

Plan: Conditional RNGH Press Handling

Goal

Use RNGH for press events when @hanzogui/native/setup-gesture-handler has been called, otherwise fall back to RN's usePressability. Avoid re-parenting by tracking hasEverHadPressEvents in stateRef.

Key Points

  1. runOnJS(true) is required on Tap/LongPress gestures for React callbacks to work
  2. Gesture.Exclusive(longPress, tap) ensures only one fires based on timing
  3. Inline require usePressability only when RNGH is not enabled (avoids RN internal dep when not needed)
  4. Track hasEverHadPressEvents in stateRef to maintain consistent tree structure

Changes

1. pkgs/core/web/src/types.tsx

Add to GuiComponentStateRef (around line 535):

export type GuiComponentStateRef = {
  // ... existing fields ...
  hasEverHadPressEvents?: boolean // <-- add this
}

2. pkgs/core/web/src/eventHandling.native.ts

import React from 'react'
import { composeEventHandlers } from '@hanzogui/helpers'
import { getGestureHandler } from '@hanzogui/native'
import type { GuiComponentStateRef } from './types'

// web events not used on native
export function getWebEvents() {
  return {}
}

const dontComposePressabilityKeys: Record<string, boolean> = {
  onBlur: true,
  onFocus: true,
}

export function usePressHandling(
  events: any,
  viewProps: any,
  stateRef: { current: GuiComponentStateRef }
) {
  const hasPressEvents =
    events?.onPress || events?.onPressIn || events?.onPressOut || events?.onLongPress

  const gh = getGestureHandler()

  // track if we ever had press events to avoid re-parenting
  if (hasPressEvents) {
    stateRef.current.hasEverHadPressEvents = true
  }

  if (gh.isEnabled && hasPressEvents) {
    // RNGH path - return gesture for wrapping
    return gh.createPressGesture({
      onPressIn: events.onPressIn,
      onPressOut: events.onPressOut,
      onPress: events.onPress,
      onLongPress: events.onLongPress,
      delayLongPress: events.delayLongPress,
      hitSlop: viewProps.hitSlop,
    })
  }

  if (hasPressEvents) {
    // fallback - inline require usePressability only when needed
    const usePressability =
      require('react-native/Libraries/Pressability/usePressability').default
    const pressability = usePressability(events)

    if (pressability) {
      if (viewProps.hitSlop) {
        events.hitSlop = viewProps.hitSlop
      }
      for (const key in pressability) {
        const og = viewProps[key]
        const val = pressability[key]
        viewProps[key] =
          og && !dontComposePressabilityKeys[key] ? composeEventHandlers(og, val) : val
      }
    }
  }

  return null
}

export function wrapWithGestureDetector(
  content: any,
  gesture: any,
  stateRef: { current: GuiComponentStateRef }
) {
  const gh = getGestureHandler()
  const { GestureDetector, Gesture } = gh.state

  // avoid re-parenting: if we ever had press events, always wrap
  const shouldWrap = stateRef.current.hasEverHadPressEvents

  if (!GestureDetector || !shouldWrap) {
    return content
  }

  // use actual gesture or no-op Manual gesture to maintain tree structure
  const gestureToUse = gesture || Gesture?.Manual()

  if (!gestureToUse) {
    return content
  }

  return React.createElement(GestureDetector, { gesture: gestureToUse }, content)
}

How It Works

  1. User Setup (optional):

    // app entry
    import '@hanzogui/native/setup-gesture-handler'
    import { GestureHandlerRootView } from 'react-native-gesture-handler'
    
  2. At Runtime:

    • usePressHandling checks getGestureHandler().isEnabled
    • If RNGH enabled: creates press gesture via createPressGesture(), returns it
    • If RNGH not enabled: inline requires usePressability, mutates viewProps, returns null
  3. Wrapping:

    • wrapWithGestureDetector checks hasEverHadPressEvents
    • If component ever had press events, wraps with GestureDetector
    • Uses actual gesture or Gesture.Manual() no-op to maintain tree structure
    • If never had press events, returns content unwrapped

Gesture Callback Flow

User Action Callback RNGH Event
Touch down onPressIn Tap's onBegin
Quick release (< 500ms) onPress + onPressOut Tap's onEnd + onFinalize
Hold 500ms+ onLongPress + onPressOut LongPress's onStart + onFinalize

Testing

  1. Run existing Detox tests with RNGH enabled
  2. Run PressStyleNative.noRngh.test.ts to verify fallback path
  3. Test press style changes work with both paths