react-native-gtkx 0.2.0-alpha.2 → 0.3.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/aliases/index.d.ts +84 -0
- package/dist/aliases/index.js +208 -0
- package/dist/aliases/index.js.map +1 -0
- package/dist/animated/index.d.ts +2 -0
- package/dist/animated/index.js +5 -0
- package/dist/animated/index.js.map +1 -1
- package/dist/animated/types.d.ts +1 -0
- package/dist/animated/types.js.map +1 -1
- package/dist/animated/value-animation.js +17 -4
- package/dist/animated/value-animation.js.map +1 -1
- package/dist/apis/host.gtkx.js +9 -12
- package/dist/apis/host.gtkx.js.map +1 -1
- package/dist/apis/index.d.ts +4 -2
- package/dist/apis/index.js +2 -0
- package/dist/apis/index.js.map +1 -1
- package/dist/apis/keyboard.d.ts +60 -0
- package/dist/apis/keyboard.js +63 -0
- package/dist/apis/keyboard.js.map +1 -0
- package/dist/apis/log-box.d.ts +15 -0
- package/dist/apis/log-box.js +46 -0
- package/dist/apis/log-box.js.map +1 -0
- package/dist/apis/platform.d.ts +18 -1
- package/dist/apis/platform.js.map +1 -1
- package/dist/common/index.d.ts +0 -1
- package/dist/common/index.js +24 -1
- package/dist/common/index.js.map +1 -1
- package/dist/components/animated.d.ts +70 -6
- package/dist/components/animated.js +559 -46
- package/dist/components/animated.js.map +1 -1
- package/dist/components/driven-size.d.ts +27 -0
- package/dist/components/driven-size.js +85 -0
- package/dist/components/driven-size.js.map +1 -0
- package/dist/components/find-node-handle.d.ts +11 -0
- package/dist/components/find-node-handle.js +53 -0
- package/dist/components/find-node-handle.js.map +1 -0
- package/dist/components/flat-list.d.ts +3 -2
- package/dist/components/flat-list.js.map +1 -1
- package/dist/components/frame-scheduler.d.ts +16 -0
- package/dist/components/frame-scheduler.js +37 -0
- package/dist/components/frame-scheduler.js.map +1 -0
- package/dist/components/image.d.ts +6 -1
- package/dist/components/image.js +15 -4
- package/dist/components/image.js.map +1 -1
- package/dist/components/index.d.ts +7 -5
- package/dist/components/index.js +5 -3
- package/dist/components/index.js.map +1 -1
- package/dist/components/measure.d.ts +51 -1
- package/dist/components/measure.js +101 -2
- package/dist/components/measure.js.map +1 -1
- package/dist/components/pressable.d.ts +61 -1
- package/dist/components/pressable.js +151 -4
- package/dist/components/pressable.js.map +1 -1
- package/dist/components/rect-store.d.ts +58 -0
- package/dist/components/rect-store.js +124 -3
- package/dist/components/rect-store.js.map +1 -1
- package/dist/components/root.js +6 -1
- package/dist/components/root.js.map +1 -1
- package/dist/components/scroll-phase.d.ts +35 -0
- package/dist/components/scroll-phase.js +26 -0
- package/dist/components/scroll-phase.js.map +1 -0
- package/dist/components/scroll-view.d.ts +39 -3
- package/dist/components/scroll-view.js +378 -43
- package/dist/components/scroll-view.js.map +1 -1
- package/dist/components/text-input.js +9 -25
- package/dist/components/text-input.js.map +1 -1
- package/dist/components/text.d.ts +6 -2
- package/dist/components/text.js +43 -8
- package/dist/components/text.js.map +1 -1
- package/dist/components/use-focus.d.ts +27 -0
- package/dist/components/use-focus.js +116 -0
- package/dist/components/use-focus.js.map +1 -0
- package/dist/components/use-layout-child.d.ts +13 -1
- package/dist/components/use-layout-child.js +188 -5
- package/dist/components/use-layout-child.js.map +1 -1
- package/dist/components/view.d.ts +5 -2
- package/dist/components/view.js +18 -13
- package/dist/components/view.js.map +1 -1
- package/dist/components/virtualized-list.d.ts +45 -5
- package/dist/components/virtualized-list.js +90 -24
- package/dist/components/virtualized-list.js.map +1 -1
- package/dist/components/wheel-scroll-session.d.ts +7 -0
- package/dist/components/wheel-scroll-session.js +53 -0
- package/dist/components/wheel-scroll-session.js.map +1 -0
- package/dist/components/widget-retention.d.ts +35 -0
- package/dist/components/widget-retention.js +169 -0
- package/dist/components/widget-retention.js.map +1 -0
- package/dist/contracts.d.ts +4 -0
- package/dist/contracts.js.map +1 -1
- package/dist/dnd/context.d.ts +69 -0
- package/dist/dnd/context.js +149 -0
- package/dist/dnd/context.js.map +1 -0
- package/dist/dnd/draggable.d.ts +59 -0
- package/dist/dnd/draggable.js +149 -0
- package/dist/dnd/draggable.js.map +1 -0
- package/dist/dnd/droppable.d.ts +24 -0
- package/dist/dnd/droppable.js +74 -0
- package/dist/dnd/droppable.js.map +1 -0
- package/dist/dnd/gtk-controllers.d.ts +39 -0
- package/dist/dnd/gtk-controllers.js +110 -0
- package/dist/dnd/gtk-controllers.js.map +1 -0
- package/dist/dnd/index.d.ts +6 -0
- package/dist/dnd/index.js +31 -0
- package/dist/dnd/index.js.map +1 -0
- package/dist/dnd/order.d.ts +7 -0
- package/dist/dnd/order.js +20 -0
- package/dist/dnd/order.js.map +1 -0
- package/dist/dnd/payload.d.ts +14 -0
- package/dist/dnd/payload.js +41 -0
- package/dist/dnd/payload.js.map +1 -0
- package/dist/dnd/sortable.d.ts +49 -0
- package/dist/dnd/sortable.js +307 -0
- package/dist/dnd/sortable.js.map +1 -0
- package/dist/dnd/types.d.ts +290 -0
- package/dist/dnd/types.js +25 -0
- package/dist/dnd/types.js.map +1 -0
- package/dist/gesture-handler-compat/builder.d.ts +251 -0
- package/dist/gesture-handler-compat/builder.js +513 -0
- package/dist/gesture-handler-compat/builder.js.map +1 -0
- package/dist/gesture-handler-compat/composition.d.ts +23 -0
- package/dist/gesture-handler-compat/composition.js +111 -0
- package/dist/gesture-handler-compat/composition.js.map +1 -0
- package/dist/gesture-handler-compat/deciders.d.ts +3 -0
- package/dist/gesture-handler-compat/deciders.js +47 -0
- package/dist/gesture-handler-compat/deciders.js.map +1 -0
- package/dist/gesture-handler-compat/detector-runtime.d.ts +44 -0
- package/dist/gesture-handler-compat/detector-runtime.js +374 -0
- package/dist/gesture-handler-compat/detector-runtime.js.map +1 -0
- package/dist/gesture-handler-compat/detector.d.ts +22 -0
- package/dist/gesture-handler-compat/detector.js +104 -0
- package/dist/gesture-handler-compat/detector.js.map +1 -0
- package/dist/gesture-handler-compat/fling.d.ts +24 -0
- package/dist/gesture-handler-compat/fling.js +123 -0
- package/dist/gesture-handler-compat/fling.js.map +1 -0
- package/dist/gesture-handler-compat/force-touch.d.ts +13 -0
- package/dist/gesture-handler-compat/force-touch.js +40 -0
- package/dist/gesture-handler-compat/force-touch.js.map +1 -0
- package/dist/gesture-handler-compat/hooks.d.ts +157 -0
- package/dist/gesture-handler-compat/hooks.js +268 -0
- package/dist/gesture-handler-compat/hooks.js.map +1 -0
- package/dist/gesture-handler-compat/hover.d.ts +2 -0
- package/dist/gesture-handler-compat/hover.js +22 -0
- package/dist/gesture-handler-compat/hover.js.map +1 -0
- package/dist/gesture-handler-compat/index.d.ts +98 -0
- package/dist/gesture-handler-compat/index.js +255 -0
- package/dist/gesture-handler-compat/index.js.map +1 -0
- package/dist/gesture-handler-compat/long-press.d.ts +6 -0
- package/dist/gesture-handler-compat/long-press.js +21 -0
- package/dist/gesture-handler-compat/long-press.js.map +1 -0
- package/dist/gesture-handler-compat/manual.d.ts +2 -0
- package/dist/gesture-handler-compat/manual.js +18 -0
- package/dist/gesture-handler-compat/manual.js.map +1 -0
- package/dist/gesture-handler-compat/native.d.ts +7 -0
- package/dist/gesture-handler-compat/native.js +34 -0
- package/dist/gesture-handler-compat/native.js.map +1 -0
- package/dist/gesture-handler-compat/orchestrator.d.ts +77 -0
- package/dist/gesture-handler-compat/orchestrator.js +284 -0
- package/dist/gesture-handler-compat/orchestrator.js.map +1 -0
- package/dist/gesture-handler-compat/pan.d.ts +23 -0
- package/dist/gesture-handler-compat/pan.js +116 -0
- package/dist/gesture-handler-compat/pan.js.map +1 -0
- package/dist/gesture-handler-compat/recognizer.d.ts +268 -0
- package/dist/gesture-handler-compat/recognizer.js +1056 -0
- package/dist/gesture-handler-compat/recognizer.js.map +1 -0
- package/dist/gesture-handler-compat/relations.d.ts +43 -0
- package/dist/gesture-handler-compat/relations.js +128 -0
- package/dist/gesture-handler-compat/relations.js.map +1 -0
- package/dist/gesture-handler-compat/tap.d.ts +6 -0
- package/dist/gesture-handler-compat/tap.js +29 -0
- package/dist/gesture-handler-compat/tap.js.map +1 -0
- package/dist/gesture-handler-compat/touchpad.d.ts +41 -0
- package/dist/gesture-handler-compat/touchpad.js +64 -0
- package/dist/gesture-handler-compat/touchpad.js.map +1 -0
- package/dist/gesture-handler-compat/types.d.ts +441 -0
- package/dist/gesture-handler-compat/types.js +167 -0
- package/dist/gesture-handler-compat/types.js.map +1 -0
- package/dist/gtk/controllers.d.ts +39 -0
- package/dist/gtk/controllers.js +92 -0
- package/dist/gtk/controllers.js.map +1 -0
- package/dist/gtk/index.d.ts +2 -1
- package/dist/gtk/index.js +7 -1
- package/dist/gtk/index.js.map +1 -1
- package/dist/gtkx/bridge/geometry.js +6 -4
- package/dist/gtkx/bridge/geometry.js.map +1 -1
- package/dist/gtkx/bridge/index.d.ts +5 -3
- package/dist/gtkx/bridge/index.js +6 -3
- package/dist/gtkx/bridge/index.js.map +1 -1
- package/dist/gtkx/bridge/slot-portal.js.map +1 -1
- package/dist/gtkx/bridge/use-signal.js +1 -1
- package/dist/gtkx/bridge/use-signal.js.map +1 -1
- package/dist/gtkx/bridge/view-box.d.ts +56 -0
- package/dist/gtkx/bridge/view-box.js +394 -9
- package/dist/gtkx/bridge/view-box.js.map +1 -1
- package/dist/gtkx/bridge/widget-css.d.ts +17 -0
- package/dist/gtkx/bridge/widget-css.js +72 -0
- package/dist/gtkx/bridge/widget-css.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/layout/driven-size.d.ts +12 -0
- package/dist/layout/driven-size.js +70 -0
- package/dist/layout/driven-size.js.map +1 -0
- package/dist/layout/engine.d.ts +17 -1
- package/dist/layout/engine.js +3 -20
- package/dist/layout/engine.js.map +1 -1
- package/dist/layout/node.d.ts +13 -0
- package/dist/layout/node.js +14 -0
- package/dist/layout/node.js.map +1 -1
- package/dist/layout/yoga.d.ts +3 -2
- package/dist/layout/yoga.js +5 -2
- package/dist/layout/yoga.js.map +1 -1
- package/dist/mcp/data/generated.d.ts +257 -60
- package/dist/mcp/data/generated.js +306 -62
- package/dist/mcp/data/generated.js.map +1 -1
- package/dist/metro/index.d.ts +10 -0
- package/dist/metro/index.js +21 -12
- package/dist/metro/index.js.map +1 -1
- package/dist/navigation/sidebar.d.ts +16 -0
- package/dist/navigation/sidebar.js +65 -1
- package/dist/navigation/sidebar.js.map +1 -1
- package/dist/reanimated-compat/animated-ref.d.ts +32 -0
- package/dist/reanimated-compat/animated-ref.js +57 -0
- package/dist/reanimated-compat/animated-ref.js.map +1 -0
- package/dist/reanimated-compat/animation.d.ts +177 -0
- package/dist/reanimated-compat/animation.js +540 -0
- package/dist/reanimated-compat/animation.js.map +1 -0
- package/dist/reanimated-compat/color.d.ts +28 -0
- package/dist/reanimated-compat/color.js +249 -0
- package/dist/reanimated-compat/color.js.map +1 -0
- package/dist/reanimated-compat/decay.d.ts +39 -0
- package/dist/reanimated-compat/decay.js +96 -0
- package/dist/reanimated-compat/decay.js.map +1 -0
- package/dist/reanimated-compat/easing.d.ts +30 -0
- package/dist/reanimated-compat/easing.js +76 -0
- package/dist/reanimated-compat/easing.js.map +1 -0
- package/dist/reanimated-compat/hooks.d.ts +14 -0
- package/dist/reanimated-compat/hooks.js +217 -0
- package/dist/reanimated-compat/hooks.js.map +1 -0
- package/dist/reanimated-compat/index.d.ts +258 -0
- package/dist/reanimated-compat/index.js +464 -0
- package/dist/reanimated-compat/index.js.map +1 -0
- package/dist/reanimated-compat/interpolation.d.ts +18 -0
- package/dist/reanimated-compat/interpolation.js +122 -0
- package/dist/reanimated-compat/interpolation.js.map +1 -0
- package/dist/reanimated-compat/layout-animation-config.d.ts +43 -0
- package/dist/reanimated-compat/layout-animation-config.js +78 -0
- package/dist/reanimated-compat/layout-animation-config.js.map +1 -0
- package/dist/reanimated-compat/layout-animation-presets.d.ts +117 -0
- package/dist/reanimated-compat/layout-animation-presets.js +498 -0
- package/dist/reanimated-compat/layout-animation-presets.js.map +1 -0
- package/dist/reanimated-compat/layout-animation-runtime.d.ts +21 -0
- package/dist/reanimated-compat/layout-animation-runtime.js +311 -0
- package/dist/reanimated-compat/layout-animation-runtime.js.map +1 -0
- package/dist/reanimated-compat/layout-animation-view.d.ts +27 -0
- package/dist/reanimated-compat/layout-animation-view.js +266 -0
- package/dist/reanimated-compat/layout-animation-view.js.map +1 -0
- package/dist/reanimated-compat/layout-animation.d.ts +171 -0
- package/dist/reanimated-compat/layout-animation.js +469 -0
- package/dist/reanimated-compat/layout-animation.js.map +1 -0
- package/dist/reanimated-compat/layout-transitions.d.ts +83 -0
- package/dist/reanimated-compat/layout-transitions.js +350 -0
- package/dist/reanimated-compat/layout-transitions.js.map +1 -0
- package/dist/reanimated-compat/mutable.d.ts +29 -0
- package/dist/reanimated-compat/mutable.js +139 -0
- package/dist/reanimated-compat/mutable.js.map +1 -0
- package/dist/reanimated-compat/props.d.ts +18 -0
- package/dist/reanimated-compat/props.js +111 -0
- package/dist/reanimated-compat/props.js.map +1 -0
- package/dist/reanimated-compat/scroll-handler.d.ts +119 -0
- package/dist/reanimated-compat/scroll-handler.js +274 -0
- package/dist/reanimated-compat/scroll-handler.js.map +1 -0
- package/dist/reanimated-compat/scroll-offset.d.ts +12 -0
- package/dist/reanimated-compat/scroll-offset.js +104 -0
- package/dist/reanimated-compat/scroll-offset.js.map +1 -0
- package/dist/reanimated-compat/style.d.ts +90 -0
- package/dist/reanimated-compat/style.js +452 -0
- package/dist/reanimated-compat/style.js.map +1 -0
- package/dist/reanimated-compat/test-timers.d.ts +28 -0
- package/dist/reanimated-compat/test-timers.js +136 -0
- package/dist/reanimated-compat/test-timers.js.map +1 -0
- package/dist/reanimated-compat/threads.d.ts +13 -0
- package/dist/reanimated-compat/threads.js +41 -0
- package/dist/reanimated-compat/threads.js.map +1 -0
- package/dist/reanimated-compat/tracking.d.ts +41 -0
- package/dist/reanimated-compat/tracking.js +106 -0
- package/dist/reanimated-compat/tracking.js.map +1 -0
- package/dist/reanimated-compat/updater-animations.d.ts +36 -0
- package/dist/reanimated-compat/updater-animations.js +273 -0
- package/dist/reanimated-compat/updater-animations.js.map +1 -0
- package/dist/responder/system.d.ts +80 -7
- package/dist/responder/system.js +143 -23
- package/dist/responder/system.js.map +1 -1
- package/dist/responder/use-responder.d.ts +11 -0
- package/dist/responder/use-responder.js +225 -6
- package/dist/responder/use-responder.js.map +1 -1
- package/dist/runner/host-dev.js +1 -1
- package/dist/runner/host-dev.js.map +1 -1
- package/dist/runner/host.js +1 -1
- package/dist/runner/host.js.map +1 -1
- package/dist/runner/index.js +3 -3
- package/dist/runner/index.js.map +1 -1
- package/dist/sea/gtkx-config-module.js +1 -1
- package/dist/sea/gtkx-config-module.js.map +1 -1
- package/dist/style/absolute-insets.d.ts +26 -0
- package/dist/style/absolute-insets.js +113 -0
- package/dist/style/absolute-insets.js.map +1 -0
- package/dist/style/animated-size.d.ts +25 -0
- package/dist/style/animated-size.js +225 -0
- package/dist/style/animated-size.js.map +1 -0
- package/dist/style/colors.d.ts +17 -0
- package/dist/style/colors.js +25 -6
- package/dist/style/colors.js.map +1 -1
- package/dist/style/imperative-css.d.ts +21 -0
- package/dist/style/imperative-css.js +63 -0
- package/dist/style/imperative-css.js.map +1 -0
- package/dist/style/index.d.ts +2 -1
- package/dist/style/index.js +2 -1
- package/dist/style/index.js.map +1 -1
- package/dist/style/split-style.js +5 -0
- package/dist/style/split-style.js.map +1 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/unsupported-export.d.ts +8 -0
- package/dist/unsupported-export.js +66 -0
- package/dist/unsupported-export.js.map +1 -0
- package/dist/vite/index.d.ts +21 -8
- package/dist/vite/index.js +127 -82
- package/dist/vite/index.js.map +1 -1
- package/dist/vitest/index.d.ts +2 -2
- package/dist/vitest/index.js.map +1 -1
- package/dist/worklets-compat/index.d.ts +4 -0
- package/dist/worklets-compat/index.js +28 -0
- package/dist/worklets-compat/index.js.map +1 -0
- package/dist/worklets-compat/surface.d.ts +110 -0
- package/dist/worklets-compat/surface.js +153 -0
- package/dist/worklets-compat/surface.js.map +1 -0
- package/package.json +25 -9
- package/types.d.ts +9 -0
- package/dist/common/list.d.ts +0 -82
- package/dist/common/list.js +0 -166
- package/dist/common/list.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GENERATED FILE — do not edit by hand.
|
|
2
2
|
// Produced by scripts/generate-mcp-data.mjs from docs/api.md,
|
|
3
|
-
// docs/platform-layer.md, docs/gtkx-
|
|
4
|
-
// docs/research/navigation-extensibility.md and
|
|
3
|
+
// docs/platform-layer.md, docs/gtkx-rc4-notes.md, docs/getting-started.md,
|
|
4
|
+
// docs/gestures.md, docs/research/navigation-extensibility.md and
|
|
5
5
|
// scripts/widget-surface/classification.json.
|
|
6
6
|
//
|
|
7
7
|
// Regenerate after touching any of those:
|
|
@@ -13,36 +13,36 @@ export const PORTABLE_COMPONENTS = [
|
|
|
13
13
|
name: "View",
|
|
14
14
|
subpath: "react-native",
|
|
15
15
|
gtkImplementation: "GtkBox (RnGtkxViewBox)",
|
|
16
|
-
supported: "`style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here",
|
|
17
|
-
differences: "
|
|
16
|
+
supported: "`style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), `focusable` + `onFocus`/`onBlur` (RN has `focusable` on View for Android/Windows; react-native-web and react-native-windows both have the callbacks — off by default, as in RN), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props ([guide](gestures.md)) (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here",
|
|
17
|
+
differences: "Responder negotiation is RN's in full — capture-then-bubble, transfer to an ancestor mid-gesture through `onResponderTerminationRequest`/`onResponderReject`, and `onResponderTerminate`. The lock is one per process as in RN; the negotiation PATH stops at the layout root, so native GTK widgets between or above views take no part. Single-pointer only: a mouse is one fabricated touch, `touches` never exceeds one. Terminations differ from RN's, because GTK decides most of them before JS is told: a context menu (a second mouse button), a native widget or a `Controllers` `GtkDragSource` taking the sequence, and text selection all arrive as a cancelled gesture and terminate **without** consulting `onResponderTerminationRequest` — GTK's `CLAIMED` is irrevocable, so there is nothing an answer could change. Window blur terminates unconditionally, as it does in react-native-web. An enclosing `ScrollView` scrolling under the gesture is the one termination the holder may refuse. `overflow: \"hidden\"` (and `\"scroll\"`, which clips identically — a `View` is not made scrollable by a style on this platform any more than it is in RN) clips both the paint AND the picking of the children, including transformed ones and children driven out by an animated absolute `top`/`left`: GTK pushes a clip node over the container's CSS padding box before it snapshots them, and `gtk_widget_pick()` refuses the same box, so what you cannot see you cannot click. `borderRadius` shapes that clip — a rounded container clips its children to the rounded corners. A container never clips its OWN background, border, shadow or outline, only its children's",
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
name: "Text",
|
|
21
21
|
subpath: "react-native",
|
|
22
22
|
gtkImplementation: "GtkLabel (Pango)",
|
|
23
|
-
supported: "wrap, `numberOfLines` (ellipsize END), `textAlign`, font styles, `onLayout`, `testID`",
|
|
23
|
+
supported: "wrap, `numberOfLines` (ellipsize END), `textAlign`, font styles, `onLayout`, `testID`, ref: `measure`/`measureInWindow`/`measureLayout` (`TextHandle` — RN gives every host component the geometry methods, so a label no longer has to be wrapped in a `View` to be measurable)",
|
|
24
24
|
differences: "nested `Text` elements are concatenated without per-span styles; text is always ellipsizable (shrinkable in narrow windows)",
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
name: "Image",
|
|
28
28
|
subpath: "react-native",
|
|
29
29
|
gtkImplementation: "GtkPicture",
|
|
30
|
-
supported: '`source={{uri}}`/string — local paths, file:// and **http(s)** (Node fetch → disk cache keyed by URL, in-flight de-duplication), `resizeMode` cover/contain/stretch/center, `onLoad`/`onError`; **`.svg` files load like any other image** — `Gdk.Texture.newFromFilename` rasterizes them via librsvg, no extra code needed (for building vector graphics from state instead of a file, see the "Svg" section below — a separate import, not part of this table)',
|
|
30
|
+
supported: '`source={{uri}}`/string — local paths, file:// and **http(s)** (Node fetch → disk cache keyed by URL, in-flight de-duplication), `resizeMode` cover/contain/stretch/center, `onLoad`/`onError`; **`.svg` files load like any other image** — `Gdk.Texture.newFromFilename` rasterizes them via librsvg, no extra code needed (for building vector graphics from state instead of a file, see the "Svg" section below — a separate import, not part of this table); ref: `measure`/`measureInWindow`/`measureLayout` (`ImageHandle`)',
|
|
31
31
|
differences: "no synchronous size from remote images (style sets the size, as in RN); cache is not size-limited yet",
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
name: "TextInput",
|
|
35
35
|
subpath: "react-native",
|
|
36
36
|
gtkImplementation: "GtkEntry / GtkTextView",
|
|
37
|
-
supported: "controlled/uncontrolled (`value`/`defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus`/`onBlur`, `placeholder` (own dim overlay in multiline — GtkTextView has none), `secureTextEntry`, `editable`, `keyboardType`, `multiline`, `clearButtonMode` (GtkEntry's built-in clear icon; RN ships this on iOS only) (real GtkTextView: word wrap, internal scroll, Enter inserts a newline and never fires onSubmitEditing — RN semantics)",
|
|
37
|
+
supported: "controlled/uncontrolled (`value`/`defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus`/`onBlur`, `placeholder` (own dim overlay in multiline — GtkTextView has none), `secureTextEntry`, `editable`, `keyboardType`, `multiline`, `clearButtonMode` (GtkEntry's built-in clear icon; RN ships this on iOS only), the visual half of `style` (background, border, radius — it used to be computed and dropped, so a styled TextInput silently kept the theme's own frame) (real GtkTextView: word wrap, internal scroll, Enter inserts a newline and never fires onSubmitEditing — RN semantics)",
|
|
38
38
|
differences: "multiline needs a height in the style (as RN recommends)",
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
name: "Pressable",
|
|
42
42
|
subpath: "react-native",
|
|
43
|
-
gtkImplementation: "
|
|
44
|
-
supported: "`onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered}
|
|
45
|
-
differences: "—
|
|
43
|
+
gtkImplementation: "GtkBox (RnGtkxViewBox) + GestureClick/Motion",
|
|
44
|
+
supported: "`onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `onFocus`/`onBlur`, `focusable`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered, focused}` (react-native-web's own state shape); **keyboard-operable**: `focusable` defaults to true when `onPress` is set (react-native-web's rule), which puts the view in GTK's focus chain so Tab and the arrow keys reach it, and Enter/Space fire `onPress` as they do on web and Android; the `PressEvent` payload is RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, monotonic `timestamp`, single-element `touches`/`changedTouches` — a desktop pointer is one fabricated touch). `hitSlop` and `pressRetentionOffset`, each a number or per-edge; the press rect defaults to RN's own `{top: 20, left: 20, right: 20, bottom: 30}` around the hit rect, and a release outside it is a cancel rather than a press",
|
|
45
|
+
differences: '`hitSlop` cannot escape an ancestor that clips — a `ScrollView` viewport, or any view with `overflow: "hidden"` — because GTK stops picking at the clip, which is the limit RN documents on Android for the same reason. Hover fires from touch as well as from a mouse: react-native-web filters that out, and here a GTK crossing event carries no device to filter on; GTK also sends a matching leave when a touch sequence ends, so the stuck phantom hover the filter exists for does not arise, and GTK\'s own `:hover` behaves the same way (docs/research/gestures.md)',
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
name: "TouchableOpacity",
|
|
@@ -51,19 +51,33 @@ export const PORTABLE_COMPONENTS = [
|
|
|
51
51
|
supported: "`activeOpacity`",
|
|
52
52
|
differences: "—",
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
name: "TouchableHighlight",
|
|
56
|
+
subpath: "react-native",
|
|
57
|
+
gtkImplementation: "on top of Pressable",
|
|
58
|
+
supported: "`underlayColor` (RN default `black`), `activeOpacity`, `onShowUnderlay`/`onHideUnderlay`",
|
|
59
|
+
differences: "RN renders a separate underlay VIEW behind the child and dims the child onto it; here the highlight is the view's own `backgroundColor` while pressed, because an extra box would change flex layout and what `measureLayout` is relative to — the same reason `GestureDetector` and `createAnimatedComponent` add none. Give the child a translucent background for RN's exact blend.",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "TouchableWithoutFeedback",
|
|
63
|
+
subpath: "react-native",
|
|
64
|
+
gtkImplementation: "on top of Pressable",
|
|
65
|
+
supported: "the `Pressable` press/hover/focus props, with no visual reaction",
|
|
66
|
+
differences: "RN clones its single child instead of rendering a box, which its own docs call a mistake kept for compatibility; this renders the `Pressable` box. Prefer `Pressable`, as RN's docs say.",
|
|
67
|
+
},
|
|
54
68
|
{
|
|
55
69
|
name: "ScrollView",
|
|
56
70
|
subpath: "react-native",
|
|
57
71
|
gtkImplementation: "GtkScrolledWindow",
|
|
58
|
-
supported: "vertical/`horizontal`, `contentContainerStyle`, `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` (`ScrollViewHandle`)",
|
|
59
|
-
differences: "`animated` in scrollTo is ignored",
|
|
72
|
+
supported: "vertical/`horizontal`, `contentContainerStyle` (RN's default: the content container is a plain `View`, so `alignItems` is `stretch` — children fill the cross axis unless they say otherwise), `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onScrollBeginDrag`/`onScrollEndDrag`/`onMomentumScrollBegin`/`onMomentumScrollEnd` (the four phases, each carrying the same `ScrollEvent` — see the Differences column), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` **plus the geometry methods** `measure`/`measureInWindow`/`measureLayout` (`ScrollViewHandle`). A view inside it that takes the responder suspends the scroller's own gestures for the rest of the interaction — RN's `setIsJSResponder`, so a child pan is reachable inside a scrolling list",
|
|
73
|
+
differences: "`animated` in scrollTo is ignored. **The scroll phases are input-device-aware**: GTK gives a mouse wheel isolated detents, so this platform groups a burst into one desktop begin/end session (120 ms idle boundary) and reports no momentum; a touchpad glide reports all four from its native GTK sequence, and the content really does keep moving after the fingers leave. RN has no wheel, so the wheel session is an explicit desktop extension rather than a parity claim. `onScrollBeginDrag`/`onScrollEndDrag` map onto the user-driven scroll SESSION (GTK's `::scroll-begin`/`::scroll-end` for touchpad, the grouped burst for wheel) rather than onto a finger on the content, which is the one approximation: a touchpad never touches the content, so \"the user started driving this scroller\" is the closest true statement. The momentum pair is read off the adjustment actually moving after the sequence ended, not off `::decelerate` (which fires at every lift, velocity or none), so a glide that ends dead reports the drag pair and no momentum pair — as RN does. **None of it is installed until a handler is attached**: no controller, no signal, no timer, and 6.93 µs per scroll event with all four attached against 7.17 µs with none, which is inside the noise; the GTK-controller residual is 0.31 µs per event while any phase handler is attached. A begin/end consumer also makes each wheel detent enter the 0.235 µs session state machine; momentum-only and untracked scrollers do not. Traces and numbers: [research/scroll-phases.md](research/scroll-phases.md). Scroll arbitration is **touch-only and unverified end to end**: all four gestures `GtkScrolledWindow` installs are touch-only, so under a mouse a child pan never competes with scrolling at all, and no touch can be injected on the test rig (wlroots has no virtual-touch protocol) — every link of the mechanism is tested, the finger is not. Two known edges on touch: a view that claims on a MOVE rather than on press can lose the first ~8 px to the scroller, which `CLAIMED` makes irrevocable (iOS has the same artefact); and the mouse wheel is deliberately left alone, so scrolling with a wheel during a gesture terminates the responder rather than being suppressed. **The scroller carries RN's own base style, `flexGrow: 1, flexShrink: 1`** — `styles.baseVertical`/`baseHorizontal`, composed UNDER the app's `style` exactly as RN's `StyleSheet.compose(baseStyle, this.props.style)` composes it, on the same node the `style` prop lands on. `FlatList`, `SectionList` and `VirtualizedList` inherit it. It is what makes a scrollable with NO style of its own a viewport rather than a box grown to its content: libraries hand their list down unstyled and let the host size it (`@gorhom/bottom-sheet` is the one that found this — its scroll lock had nothing to lock, because a list that never became a viewport never emitted a scroll event). **The surprise is the other consequence**: an explicit main-axis `height` on the scroller is only its flex BASIS, so in a taller flex parent grow expands past it — `<ScrollView style={{ height: 200 }}>` inside a 400px column lays out at 400, not 200. That is parity, not a deviation, and it was checked rather than assumed: RN 0.86.2 composes the same two properties under the same `style` prop on the same Yoga node, and in Yoga a definite main-axis length becomes the flex BASIS (`computeFlexBasisForChild`) before `resolveFlexibleLength` hands out the free space — neither function consults any config. The one Yoga setting the two platforms differ on is errata (RN runs the tree at `YGErrataAll`, this platform at Yoga's default `None`) and it is inert here: the only flex-relevant bit, `StretchFlexBasis`, is reached only when the CONTAINER's main size is indefinite, and a bounded parent's is not. `useWebDefaults` is false on both, which is exactly why the `flexShrink: 1` half is needed at all. To bound the viewport, bound the PARENT (`<View style={{ height: 200 }}><FlatList /></View>` — what an RN app writes) or cancel the base with `flexGrow: 0`",
|
|
60
74
|
},
|
|
61
75
|
{
|
|
62
76
|
name: "FlatList",
|
|
63
77
|
subpath: "react-native",
|
|
64
78
|
gtkImplementation: "windowed core on ScrollView",
|
|
65
|
-
supported: "virtualization (`estimatedItemSize` or `getItemLayout`, **`windowSize`/`initialNumToRender` — the primary scroll-performance knobs**, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`), `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`, `ListHeader/Footer/EmptyComponent`, `onEndReached(-Threshold)`, `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`), `inverted` (RN chat semantics: opens at `data[0]`, stays pinned on prepend), `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`, ref: `scrollToIndex`/`scrollToItem`/`scrollToOffset` + ScrollView
|
|
66
|
-
differences: "1000 rows mount windowed in ~120 ms (v1 full mount was 879 ms); `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure and a wider window means fewer mount+reflow bursts per scrolled pixel (measured: −21% churn, late frames 10/s → 7.7/s); rows beyond the visible ones are mounted `maxToRenderPerBatch` (10) at a time every `updateCellsBatchingPeriod` (50) ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one; no pull gesture — `onRefresh` must be app-triggered; an inverted list shorter than its viewport anchors to the top, not the bottom",
|
|
79
|
+
supported: "virtualization (`estimatedItemSize` or `getItemLayout`, **`windowSize`/`initialNumToRender` — the primary scroll-performance knobs**, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`), `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`, `CellRendererComponent` (RN's per-cell wrapper — the list still hands it the cell's absolute `style` and the `onLayout` that measures it, and both must be applied; this is what `react-native-draggable-flatlist` builds its whole design on), `ListHeader/Footer/EmptyComponent`, `onEndReached(-Threshold)`, `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`), `inverted` (RN chat semantics: opens at `data[0]`, stays pinned on prepend), `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`, ref: `scrollToIndex`/`scrollToItem`/`scrollToOffset` + `scrollTo`/`scrollToEnd` (`FlatListHandle`) — the SCROLL half of a ScrollView ref, not the geometry half: a windowed list is a composite over a ScrollView and owns no widget of its own, so a `measure()` here would have to pick some inner widget and pretend it was the list. Measure the `ScrollView` or a cell",
|
|
80
|
+
differences: "1000 rows mount windowed in ~120 ms (v1 full mount was 879 ms); `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure and a wider window means fewer mount+reflow bursts per scrolled pixel (measured: −21% churn, late frames 10/s → 7.7/s); rows beyond the visible ones are mounted `maxToRenderPerBatch` (10) at a time every `updateCellsBatchingPeriod` (50) ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one; no pull gesture — `onRefresh` must be app-triggered; an inverted list shorter than its viewport anchors to the top, not the bottom; `CellRendererComponent` is not applied to a STICKY cell (`stickyHeaderIndices`) — pinning reorders the cell's real GTK widget, so the sticky container has to BE the cell",
|
|
67
81
|
},
|
|
68
82
|
{
|
|
69
83
|
name: "SectionList",
|
|
@@ -72,6 +86,13 @@ export const PORTABLE_COMPONENTS = [
|
|
|
72
86
|
supported: "`sections`, `renderSectionHeader`, sticky section headers by default (`stickySectionHeadersEnabled`)",
|
|
73
87
|
differences: "viewability props are not exposed (section-aware ViewTokens pending)",
|
|
74
88
|
},
|
|
89
|
+
{
|
|
90
|
+
name: "VirtualizedList",
|
|
91
|
+
subpath: "react-native",
|
|
92
|
+
gtkImplementation: "the same windowed core",
|
|
93
|
+
supported: "RN's data-source shape over the list `FlatList` already sits on: `data` is OPAQUE and read only through `getItemCount(data)` and `getItem(data, index)`, both honoured LAZILY (only the rows the window mounts are ever asked for). Everything else is `FlatList`'s row above, `CellRendererComponent` included",
|
|
94
|
+
differences: "The accessors are OPTIONAL here and required upstream — one component serves both shapes, which is why `FlatList` needs no separate implementation. `scrollToItem` scans the source through `getItem` (upstream scans too; an opaque source has no index to ask). Every difference in the `FlatList` row applies unchanged",
|
|
95
|
+
},
|
|
75
96
|
{
|
|
76
97
|
name: "Switch",
|
|
77
98
|
subpath: "react-native",
|
|
@@ -97,7 +118,7 @@ export const PORTABLE_COMPONENTS = [
|
|
|
97
118
|
name: "Animated.View",
|
|
98
119
|
subpath: "react-native",
|
|
99
120
|
gtkImplementation: "direct widget calls",
|
|
100
|
-
supported: "`opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange)
|
|
121
|
+
supported: "`opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange); `top`/`left`/`right`/`bottom` too when the node's own `position` is `\"absolute\"`, which is what makes `Animated.ValueXY`'s `getLayout()` work; and `width`/`height` where the change is confined to the node that owns it; plus the same responder and touch props `View` takes — this is where an idiomatic `PanResponder` drag lands; plus `pointerEvents` and `animatedProps`, because Reanimated's `Animated.View` is `createAnimatedComponent(View)` and every View prop reaches it there",
|
|
101
122
|
differences: "`rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`)",
|
|
102
123
|
},
|
|
103
124
|
{
|
|
@@ -227,11 +248,29 @@ export const PORTABLE_APIS = [
|
|
|
227
248
|
supported: '`addEventListener("hardwareBackPress")`, `exitApp`',
|
|
228
249
|
differences: "no hardware back key on desktop — subscriptions are honored but nothing fires them yet",
|
|
229
250
|
},
|
|
251
|
+
{
|
|
252
|
+
name: "findNodeHandle",
|
|
253
|
+
subpath: "react-native",
|
|
254
|
+
supported: "a stable integer per mounted widget, resolvable back to it; accepted by `measureLayout` as its first argument, alongside a handle object. Takes what RN takes — a component handle, a node handle (returned unchanged), `null`/`undefined`. A windowed list resolves to the `ScrollView` it renders, as RN's `FlatList` resolves through to its own scroll view",
|
|
255
|
+
differences: "The tag identifies the WIDGET, not the ref: two refs onto one view report the same number and a re-render that rebuilt the handle object does not change it. It reaches nothing native — there is no `UIManager` to hand a tag to — so it is worth exactly what this platform can resolve it to: `measureLayout`, and identity. `null` for anything that is not a mounted host view (RN's answer too)",
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "Keyboard",
|
|
259
|
+
subpath: "react-native",
|
|
260
|
+
supported: "`addListener` (honoured, never fires), `removeAllListeners`, `dismiss`, `isVisible` (always false), `metrics` (always undefined), `scheduleLayoutAnimation`",
|
|
261
|
+
differences: "This is the SOFTWARE keyboard, and a desktop has none: every event it carries describes a panel occluding the app, so none can fire. Subscriptions are real and `remove()` pairs with them (a fake subscription would turn an unmount into a crash) — the same shape as `BackHandler` above, and what react-native-windows inherits from RN core, whose emitter is only ever fed on iOS and Android. **`dismiss()` is a no-op and deliberately not RN's**: RN blurs the focused input as the only way to retract the keyboard, and doing that here would let a library's gesture steal focus from a form",
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "LogBox",
|
|
265
|
+
subpath: "react-native",
|
|
266
|
+
supported: "`ignoreLogs`, `ignoreAllLogs`, `install`, `uninstall` — accepted and ignored",
|
|
267
|
+
differences: "RN's LogBox is a full-screen dev OVERLAY, and `ignoreLogs` has never filtered the console — it keeps a warning out of the yellow box. There is no overlay here, so the console output after the call is already the console output RN would have had, and nothing observable is lost. Called by `react-native-draggable-flatlist` on every `NestableDraggableFlatList` render, to silence a nesting warning this platform does not emit either",
|
|
268
|
+
},
|
|
230
269
|
{
|
|
231
270
|
name: "PanResponder",
|
|
232
271
|
subpath: "react-native",
|
|
233
272
|
supported: "`create(config)` -> `panHandlers` (spread onto a `View`), full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) — **react-native's own file, vendored unmodified** (MIT, `Libraries/Interaction/PanResponder.js`), running on our reproduction of RN's `touchHistory` store",
|
|
234
|
-
differences: "multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`'s return value is not consumed yet",
|
|
273
|
+
differences: "multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`'s return value is not consumed yet. `onPanResponderTerminationRequest` is asked when an ancestor tries to take the gesture and when an enclosing `ScrollView` scrolls; every other termination is GTK's decision and arrives as `onPanResponderTerminate` unasked (see `View`)",
|
|
235
274
|
},
|
|
236
275
|
{
|
|
237
276
|
name: "Animated",
|
|
@@ -718,22 +757,37 @@ export const DOC_CHUNKS = [
|
|
|
718
757
|
{
|
|
719
758
|
doc: "docs/api.md",
|
|
720
759
|
heading: "Components",
|
|
721
|
-
text: "| Export | GTK implementation | Supported | Differences from RN |\n| ------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `View` | GtkBox (RnGtkxViewBox) | `style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here | nesting another pointerEvents inside a box-only view is not supported; responder negotiation is capture-then-bubble as in RN, but there is **no transfer yet**: once a view is granted, `onResponderTerminationRequest`/`onResponderReject` are never called and a second view cannot steal (GTK's `CLAIMED` is irrevocable, so a transfer could not be enforced against native widgets — see docs/research/gestures.md). The lock is one per process as in RN; the negotiation PATH stops at the layout root, so native GTK widgets between or above views take no part. Single-pointer only: a mouse is one fabricated touch, `touches` never exceeds one |\n| `Text` | GtkLabel (Pango) | wrap, `numberOfLines` (ellipsize END), `textAlign`, font styles, `onLayout`, `testID` | nested `Text` elements are concatenated without per-span styles; text is always ellipsizable (shrinkable in narrow windows) |\n| `Image` | GtkPicture | `source={{uri}}`/string — local paths, file:// and **http(s)** (Node fetch → disk cache keyed by URL, in-flight de-duplication), `resizeMode` cover/contain/stretch/center, `onLoad`/`onError`; **`.svg` files load like any other image** — `Gdk.Texture.newFromFilename` rasterizes them via librsvg, no extra code needed (for building vector graphics from state instead of a file, see the \"Svg\" section below — a separate import, not part of this table) | no synchronous size from remote images (style sets the size, as in RN); cache is not size-limited yet |\n| `TextInput` | GtkEntry / GtkTextView | controlled/uncontrolled (`value`/`defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus`/`onBlur`, `placeholder` (own dim overlay in multiline — GtkTextView has none), `secureTextEntry`, `editable`, `keyboardType`, `multiline`, `clearButtonMode` (GtkEntry's built-in clear icon; RN ships this on iOS only) (real GtkTextView: word wrap, internal scroll, Enter inserts a newline and never fires onSubmitEditing — RN semantics) | multiline needs a height in the style (as RN recommends) |\n| `Pressable` | GtkFixed + GestureClick/Motion | `onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered}`; the `PressEvent` payload is RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, monotonic `timestamp`, single-element `touches`/`changedTouches` — a desktop pointer is one fabricated touch) | — |\n| `TouchableOpacity` | on top of Pressable | `activeOpacity` | — |\n| `ScrollView` | GtkScrolledWindow | vertical/`horizontal`, `contentContainerStyle`, `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` (`ScrollViewHandle`) | `animated` in scrollTo is ignored |\n| `FlatList` | windowed core on ScrollView | virtualization (`estimatedItemSize` or `getItemLayout`, **`windowSize`/`initialNumToRender` — the primary scroll-performance knobs**, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`), `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`, `ListHeader/Footer/EmptyComponent`, `onEndReached(-Threshold)`, `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`), `inverted` (RN chat semantics: opens at `data[0]`, stays pinned on prepend), `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`, ref: `scrollToIndex`/`scrollToItem`/`scrollToOffset` + ScrollView methods (`FlatListHandle`) | 1000 rows mount windowed in ~120 ms (v1 full mount was 879 ms); `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure and a wider window means fewer mount+reflow bursts per scrolled pixel (measured: −21% churn, late frames 10/s → 7.7/s); rows beyond the visible ones are mounted `maxToRenderPerBatch` (10) at a time every `updateCellsBatchingPeriod` (50) ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one; no pull gesture — `onRefresh` must be app-triggered; an inverted list shorter than its viewport anchors to the top, not the bottom |\n| `SectionList` | on top of FlatList | `sections`, `renderSectionHeader`, sticky section headers by default (`stickySectionHeadersEnabled`) | viewability props are not exposed (section-aware ViewTokens pending) |\n| `Switch` | GtkSwitch | `value`/`onValueChange`, `disabled` | sized by the GTK theme, not iOS metrics |\n| `ActivityIndicator` | GtkSpinner | `animating`, `size` (small/large/number) | no `color` yet |\n| `Modal` | modal GtkWindow (portal) | `visible`, `onRequestClose` (Escape/close button), `title`, `width`/`height`; independently resizable with relayout | desktop semantics: a separate window, not an overlay; `transparent`/`animationType` are no-ops |\n| `Animated.View` | direct widget calls | `opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange), plus the same responder and touch props `View` takes — this is where an idiomatic `PanResponder` drag lands | `rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`) |\n| `SafeAreaView` | = View | — | no notches on desktop |\n| `StatusBar` | null | — | no status bar |\n| `Root` | internal root | `width`/`height` | extension: required by the test harness |\n| `NestedRoot` | internal root | — | extension: a Yoga root inside any GTK container slot (navigation pages, custom containers); the slot allocation is the viewport |\n| `IntrinsicRoot` | internal root | — | extension: a content-sized Yoga root for chrome slots (HeaderBar start/end) — reports its content size to GTK |",
|
|
760
|
+
text: "| Export | GTK implementation | Supported | Differences from RN |\n| -------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `View` | GtkBox (RnGtkxViewBox) | `style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), `focusable` + `onFocus`/`onBlur` (RN has `focusable` on View for Android/Windows; react-native-web and react-native-windows both have the callbacks — off by default, as in RN), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props ([guide](gestures.md)) (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here | Responder negotiation is RN's in full — capture-then-bubble, transfer to an ancestor mid-gesture through `onResponderTerminationRequest`/`onResponderReject`, and `onResponderTerminate`. The lock is one per process as in RN; the negotiation PATH stops at the layout root, so native GTK widgets between or above views take no part. Single-pointer only: a mouse is one fabricated touch, `touches` never exceeds one. Terminations differ from RN's, because GTK decides most of them before JS is told: a context menu (a second mouse button), a native widget or a `Controllers` `GtkDragSource` taking the sequence, and text selection all arrive as a cancelled gesture and terminate **without** consulting `onResponderTerminationRequest` — GTK's `CLAIMED` is irrevocable, so there is nothing an answer could change. Window blur terminates unconditionally, as it does in react-native-web. An enclosing `ScrollView` scrolling under the gesture is the one termination the holder may refuse. `overflow: \"hidden\"` (and `\"scroll\"`, which clips identically — a `View` is not made scrollable by a style on this platform any more than it is in RN) clips both the paint AND the picking of the children, including transformed ones and children driven out by an animated absolute `top`/`left`: GTK pushes a clip node over the container's CSS padding box before it snapshots them, and `gtk_widget_pick()` refuses the same box, so what you cannot see you cannot click. `borderRadius` shapes that clip — a rounded container clips its children to the rounded corners. A container never clips its OWN background, border, shadow or outline, only its children's |\n| `Text` | GtkLabel (Pango) | wrap, `numberOfLines` (ellipsize END), `textAlign`, font styles, `onLayout`, `testID`, ref: `measure`/`measureInWindow`/`measureLayout` (`TextHandle` — RN gives every host component the geometry methods, so a label no longer has to be wrapped in a `View` to be measurable) | nested `Text` elements are concatenated without per-span styles; text is always ellipsizable (shrinkable in narrow windows) |\n| `Image` | GtkPicture | `source={{uri}}`/string — local paths, file:// and **http(s)** (Node fetch → disk cache keyed by URL, in-flight de-duplication), `resizeMode` cover/contain/stretch/center, `onLoad`/`onError`; **`.svg` files load like any other image** — `Gdk.Texture.newFromFilename` rasterizes them via librsvg, no extra code needed (for building vector graphics from state instead of a file, see the \"Svg\" section below — a separate import, not part of this table); ref: `measure`/`measureInWindow`/`measureLayout` (`ImageHandle`) | no synchronous size from remote images (style sets the size, as in RN); cache is not size-limited yet |\n| `TextInput` | GtkEntry / GtkTextView | controlled/uncontrolled (`value`/`defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus`/`onBlur`, `placeholder` (own dim overlay in multiline — GtkTextView has none), `secureTextEntry`, `editable`, `keyboardType`, `multiline`, `clearButtonMode` (GtkEntry's built-in clear icon; RN ships this on iOS only), the visual half of `style` (background, border, radius — it used to be computed and dropped, so a styled TextInput silently kept the theme's own frame) (real GtkTextView: word wrap, internal scroll, Enter inserts a newline and never fires onSubmitEditing — RN semantics) | multiline needs a height in the style (as RN recommends) |\n| `Pressable` | GtkBox (RnGtkxViewBox) + GestureClick/Motion | `onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `onFocus`/`onBlur`, `focusable`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered, focused}` (react-native-web's own state shape); **keyboard-operable**: `focusable` defaults to true when `onPress` is set (react-native-web's rule), which puts the view in GTK's focus chain so Tab and the arrow keys reach it, and Enter/Space fire `onPress` as they do on web and Android; the `PressEvent` payload is RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, monotonic `timestamp`, single-element `touches`/`changedTouches` — a desktop pointer is one fabricated touch). `hitSlop` and `pressRetentionOffset`, each a number or per-edge; the press rect defaults to RN's own `{top: 20, left: 20, right: 20, bottom: 30}` around the hit rect, and a release outside it is a cancel rather than a press | `hitSlop` cannot escape an ancestor that clips — a `ScrollView` viewport, or any view with `overflow: \"hidden\"` — because GTK stops picking at the clip, which is the limit RN documents on Android for the same reason. Hover fires from touch as well as from a mouse: react-native-web filters that out, and here a GTK crossing event carries no device to filter on; GTK also sends a matching leave when a touch sequence ends, so the stuck phantom hover the filter exists for does not arise, and GTK's own `:hover` behaves the same way (docs/research/gestures.md) |\n| `TouchableOpacity` | on top of Pressable | `activeOpacity` | — |\n| `TouchableHighlight` | on top of Pressable | `underlayColor` (RN default `black`), `activeOpacity`, `onShowUnderlay`/`onHideUnderlay` | RN renders a separate underlay VIEW behind the child and dims the child onto it; here the highlight is the view's own `backgroundColor` while pressed, because an extra box would change flex layout and what `measureLayout` is relative to — the same reason `GestureDetector` and `createAnimatedComponent` add none. Give the child a translucent background for RN's exact blend. |\n| `TouchableWithoutFeedback` | on top of Pressable | the `Pressable` press/hover/focus props, with no visual reaction | RN clones its single child instead of rendering a box, which its own docs call a mistake kept for compatibility; this renders the `Pressable` box. Prefer `Pressable`, as RN's docs say. |\n| `ScrollView` | GtkScrolledWindow | vertical/`horizontal`, `contentContainerStyle` (RN's default: the content container is a plain `View`, so `alignItems` is `stretch` — children fill the cross axis unless they say otherwise), `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onScrollBeginDrag`/`onScrollEndDrag`/`onMomentumScrollBegin`/`onMomentumScrollEnd` (the four phases, each carrying the same `ScrollEvent` — see the Differences column), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` **plus the geometry methods** `measure`/`measureInWindow`/`measureLayout` (`ScrollViewHandle`). A view inside it that takes the responder suspends the scroller's own gestures for the rest of the interaction — RN's `setIsJSResponder`, so a child pan is reachable inside a scrolling list | `animated` in scrollTo is ignored. **The scroll phases are input-device-aware**: GTK gives a mouse wheel isolated detents, so this platform groups a burst into one desktop begin/end session (120 ms idle boundary) and reports no momentum; a touchpad glide reports all four from its native GTK sequence, and the content really does keep moving after the fingers leave. RN has no wheel, so the wheel session is an explicit desktop extension rather than a parity claim. `onScrollBeginDrag`/`onScrollEndDrag` map onto the user-driven scroll SESSION (GTK's `::scroll-begin`/`::scroll-end` for touchpad, the grouped burst for wheel) rather than onto a finger on the content, which is the one approximation: a touchpad never touches the content, so \"the user started driving this scroller\" is the closest true statement. The momentum pair is read off the adjustment actually moving after the sequence ended, not off `::decelerate` (which fires at every lift, velocity or none), so a glide that ends dead reports the drag pair and no momentum pair — as RN does. **None of it is installed until a handler is attached**: no controller, no signal, no timer, and 6.93 µs per scroll event with all four attached against 7.17 µs with none, which is inside the noise; the GTK-controller residual is 0.31 µs per event while any phase handler is attached. A begin/end consumer also makes each wheel detent enter the 0.235 µs session state machine; momentum-only and untracked scrollers do not. Traces and numbers: [research/scroll-phases.md](research/scroll-phases.md). Scroll arbitration is **touch-only and unverified end to end**: all four gestures `GtkScrolledWindow` installs are touch-only, so under a mouse a child pan never competes with scrolling at all, and no touch can be injected on the test rig (wlroots has no virtual-touch protocol) — every link of the mechanism is tested, the finger is not. Two known edges on touch: a view that claims on a MOVE rather than on press can lose the first ~8 px to the scroller, which `CLAIMED` makes irrevocable (iOS has the same artefact); and the mouse wheel is deliberately left alone, so scrolling with a wheel during a gesture terminates the responder rather than being suppressed. **The scroller carries RN's own base style, `flexGrow: 1, flexShrink: 1`** — `styles.baseVertical`/`baseHorizontal`, composed UNDER the app's `style` exactly as RN's `StyleSheet.compose(baseStyle, this.props.style)` composes it, on the same node the `style` prop lands on. `FlatList`, `SectionList` and `VirtualizedList` inherit it. It is what makes a scrollable with NO style of its own a viewport rather than a box grown to its content: libraries hand their list down unstyled and let the host size it (`@gorhom/bottom-sheet` is the one that found this — its scroll lock had nothing to lock, because a list that never became a viewport never emitted a scroll event). **The surprise is the other consequence**: an explicit main-axis `height` on the scroller is only its flex BASIS, so in a taller flex parent grow expands past it — `<ScrollView style={{ height: 200 }}>` inside a 400px column lays out at 400, not 200. That is parity, not a deviation, and it was checked rather than assumed: RN 0.86.2 composes the same two properties under the same `style` prop on the same Yoga node, and in Yoga a definite main-axis length becomes the flex BASIS (`computeFlexBasisForChild`) before `resolveFlexibleLength` hands out the free space — neither function consults any config. The one Yoga setting the two platforms differ on is errata (RN runs the tree at `YGErrataAll`, this platform at Yoga's default `None`) and it is inert here: the only flex-relevant bit, `StretchFlexBasis`, is reached only when the CONTAINER's main size is indefinite, and a bounded parent's is not. `useWebDefaults` is false on both, which is exactly why the `flexShrink: 1` half is needed at all. To bound the viewport, bound the PARENT (`<View style={{ height: 200 }}><FlatList /></View>` — what an RN app writes) or cancel the base with `flexGrow: 0` |\n| `FlatList` | windowed core on ScrollView | virtualization (`estimatedItemSize` or `getItemLayout`, **`windowSize`/`initialNumToRender` — the primary scroll-performance knobs**, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`), `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`, `CellRendererComponent` (RN's per-cell wrapper — the list still hands it the cell's absolute `style` and the `onLayout` that measures it, and both must be applied; this is what `react-native-draggable-flatlist` builds its whole design on), `ListHeader/Footer/EmptyComponent`, `onEndReached(-Threshold)`, `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`), `inverted` (RN chat semantics: opens at `data[0]`, stays pinned on prepend), `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`, ref: `scrollToIndex`/`scrollToItem`/`scrollToOffset` + `scrollTo`/`scrollToEnd` (`FlatListHandle`) — the SCROLL half of a ScrollView ref, not the geometry half: a windowed list is a composite over a ScrollView and owns no widget of its own, so a `measure()` here would have to pick some inner widget and pretend it was the list. Measure the `ScrollView` or a cell | 1000 rows mount windowed in ~120 ms (v1 full mount was 879 ms); `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure and a wider window means fewer mount+reflow bursts per scrolled pixel (measured: −21% churn, late frames 10/s → 7.7/s); rows beyond the visible ones are mounted `maxToRenderPerBatch` (10) at a time every `updateCellsBatchingPeriod` (50) ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one; no pull gesture — `onRefresh` must be app-triggered; an inverted list shorter than its viewport anchors to the top, not the bottom; `CellRendererComponent` is not applied to a STICKY cell (`stickyHeaderIndices`) — pinning reorders the cell's real GTK widget, so the sticky container has to BE the cell |\n| `SectionList` | on top of FlatList | `sections`, `renderSectionHeader`, sticky section headers by default (`stickySectionHeadersEnabled`) | viewability props are not exposed (section-aware ViewTokens pending) |\n| `VirtualizedList` | the same windowed core | RN's data-source shape over the list `FlatList` already sits on: `data` is OPAQUE and read only through `getItemCount(data)` and `getItem(data, index)`, both honoured LAZILY (only the rows the window mounts are ever asked for). Everything else is `FlatList`'s row above, `CellRendererComponent` included | The accessors are OPTIONAL here and required upstream — one component serves both shapes, which is why `FlatList` needs no separate implementation. `scrollToItem` scans the source through `getItem` (upstream scans too; an opaque source has no index to ask). Every difference in the `FlatList` row applies unchanged |\n| `Switch` | GtkSwitch | `value`/`onValueChange`, `disabled` | sized by the GTK theme, not iOS metrics |\n| `ActivityIndicator` | GtkSpinner | `animating`, `size` (small/large/number) | no `color` yet |\n| `Modal` | modal GtkWindow (portal) | `visible`, `onRequestClose` (Escape/close button), `title`, `width`/`height`; independently resizable with relayout | desktop semantics: a separate window, not an overlay; `transparent`/`animationType` are no-ops |\n| `Animated.View` | direct widget calls | `opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange); `top`/`left`/`right`/`bottom` too when the node's own `position` is `\"absolute\"`, which is what makes `Animated.ValueXY`'s `getLayout()` work; and `width`/`height` where the change is confined to the node that owns it; plus the same responder and touch props `View` takes — this is where an idiomatic `PanResponder` drag lands; plus `pointerEvents` and `animatedProps`, because Reanimated's `Animated.View` is `createAnimatedComponent(View)` and every View prop reaches it there | `rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`) |\n| `SafeAreaView` | = View | — | no notches on desktop |\n| `StatusBar` | null | — | no status bar |\n| `Root` | internal root | `width`/`height` | extension: required by the test harness |\n| `NestedRoot` | internal root | — | extension: a Yoga root inside any GTK container slot (navigation pages, custom containers); the slot allocation is the viewport |\n| `IntrinsicRoot` | internal root | — | extension: a content-sized Yoga root for chrome slots (HeaderBar start/end) — reports its content size to GTK |",
|
|
722
761
|
},
|
|
723
762
|
{
|
|
724
763
|
doc: "docs/api.md",
|
|
725
764
|
heading: "API modules",
|
|
726
|
-
text: '| Export | Supported | Differences |\n| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `StyleSheet` | `create`, `flatten`, `compose`, `absoluteFill(Object)`, `hairlineWidth` | — |\n| `PlatformColor` | Adwaita variables: `PlatformColor("accent-bg-color")` → `var(--...)`, `@named` | names are Adwaita, not iOS/Android |\n| `AppRegistry` | `registerComponent`, `runApplication(appKey, {title,width,height,initialProps,chrome,actionAccels,breakpoints,applicationActions,windowActions,windowControllers})`, `getAppKeys` | desktop window parameters; `chrome: "content"` uses an AdwApplicationWindow with no window titlebar — the app\'s HeaderBars (navigation) become the chrome. `actionAccels` binds accelerators to action names on the `GtkApplication`; `breakpoints` reaches `AdwApplicationWindow`\'s own prop and only does anything under `chrome: "content"` (a dev warning fires otherwise). **`applicationActions`/`windowActions`/`windowControllers` are deprecated** — reach for [`<ApplicationActions>`/`<WindowActions>`/`<WindowControllers>`](platform-layer.md#actions-and-shortcuts-declared-in-the-app-tree) instead; they still work unchanged |\n| `Platform` | `OS: "linux"`, `Version` (GTK), `select` (linux → native → default), `isTV`, `isTesting` | — |\n| `Dimensions` | `get("window"/"screen")`, `addEventListener("change")` | main window only (transient windows are ignored) |\n| `useWindowDimensions` | reactive main-window dimensions | — |\n| `Appearance` | `getColorScheme`, `setColorScheme` (AdwStyleManager), `addChangeListener` | — |\n| `useColorScheme` | reactive theme | — |\n| `AppState` | `currentState` active/background, `addEventListener` | driven by the window\'s `is-active` |\n| `Alert` | `alert(title, message, buttons, options)` → Adw.AlertDialog | `cancel`/`destructive`/`isPreferred` styles |\n| `Linking` | `openURL`, `canOpenURL` (http/https/mailto/file), `getInitialURL` (null), `addEventListener("url")` | system launcher; no deep-link delivery on desktop yet — "url" subscriptions never fire |\n| `InteractionManager` | `runAfterInteractions(task?)` (cancellable, then-able), `createInteractionHandle`/`clearInteractionHandle`, `addListener` | navigation transitions register interactions, so screen work deferred with `runAfterInteractions` waits for the push/pop slide |\n| `DevSettings` | `addMenuItem(title, handler)` (entries in the Dev Menu — Ctrl+Shift+D in `run-linux --dev`, the react-native-windows shortcut), `reload(reason?)` | silent no-ops in release builds, like RN |\n| `I18nManager` | `isRTL` (live: GTK\'s read of the locale text direction), `doLeftAndRightSwapInRTL`, `getConstants` | `allowRTL`/`forceRTL`/`swapLeftAndRightInRTL` are accepted no-ops (mobile persistence has no desktop store) |\n| `BackHandler` | `addEventListener("hardwareBackPress")`, `exitApp` | no hardware back key on desktop — subscriptions are honored but nothing fires them yet |\n| `PanResponder` | `create(config)` -> `panHandlers` (spread onto a `View`), full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) — **react-native\'s own file, vendored unmodified** (MIT, `Libraries/Interaction/PanResponder.js`), running on our reproduction of RN\'s `touchHistory` store | multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`\'s return value is not consumed yet |\n| `Animated` | `Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and deg/rad strings, clamp/extend/identity), `ValueXY` (`setValue`/`setOffset`/`flattenOffset`/`extractOffset`, `getLayout`, `getTranslateTransform`) — the value a `PanResponder` drag writes to | `useNativeDriver` is ignored (with a warning); the direct path is native-speed anyway; `Animated.event` is not implemented — write the value directly (`pan.setValue({x: g.dx, y: g.dy})`), which is what it would do |\n| `Easing` | linear/ease/quad/cubic/in/out/inOut/bezier | — |\n| `version` | package version | extension |\n\nStyles (which keys go where and what is unsupported) — [style system table](../packages/react-native-gtkx/src/style/README.md). Includes `boxShadow` (RN 0.76) and `outlineColor`/`outlineOffset`/`outlineStyle`/`outlineWidth` (RN 0.77): both are what Adwaita\'s own theme uses for the `.card`/`.boxed-list` frame and for every focus ring, so they are the difference between a React Native style approximating the platform look and reproducing it — see [research/react-native-first-showcase.md](research/react-native-first-showcase.md).',
|
|
765
|
+
text: '| Export | Supported | Differences |\n| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `StyleSheet` | `create`, `flatten`, `compose`, `absoluteFill(Object)`, `hairlineWidth` | — |\n| `PlatformColor` | Adwaita variables: `PlatformColor("accent-bg-color")` → `var(--...)`, `@named` | names are Adwaita, not iOS/Android |\n| `AppRegistry` | `registerComponent`, `runApplication(appKey, {title,width,height,initialProps,chrome,actionAccels,breakpoints,applicationActions,windowActions,windowControllers})`, `getAppKeys` | desktop window parameters; `chrome: "content"` uses an AdwApplicationWindow with no window titlebar — the app\'s HeaderBars (navigation) become the chrome. `actionAccels` binds accelerators to action names on the `GtkApplication`; `breakpoints` reaches `AdwApplicationWindow`\'s own prop and only does anything under `chrome: "content"` (a dev warning fires otherwise). **`applicationActions`/`windowActions`/`windowControllers` are deprecated** — reach for [`<ApplicationActions>`/`<WindowActions>`/`<WindowControllers>`](platform-layer.md#actions-and-shortcuts-declared-in-the-app-tree) instead; they still work unchanged |\n| `Platform` | `OS: "linux"`, `Version` (GTK), `select` (linux → native → default), `isTV`, `isTesting` | — |\n| `Dimensions` | `get("window"/"screen")`, `addEventListener("change")` | main window only (transient windows are ignored) |\n| `useWindowDimensions` | reactive main-window dimensions | — |\n| `Appearance` | `getColorScheme`, `setColorScheme` (AdwStyleManager), `addChangeListener` | — |\n| `useColorScheme` | reactive theme | — |\n| `AppState` | `currentState` active/background, `addEventListener` | driven by the window\'s `is-active` |\n| `Alert` | `alert(title, message, buttons, options)` → Adw.AlertDialog | `cancel`/`destructive`/`isPreferred` styles |\n| `Linking` | `openURL`, `canOpenURL` (http/https/mailto/file), `getInitialURL` (null), `addEventListener("url")` | system launcher; no deep-link delivery on desktop yet — "url" subscriptions never fire |\n| `InteractionManager` | `runAfterInteractions(task?)` (cancellable, then-able), `createInteractionHandle`/`clearInteractionHandle`, `addListener` | navigation transitions register interactions, so screen work deferred with `runAfterInteractions` waits for the push/pop slide |\n| `DevSettings` | `addMenuItem(title, handler)` (entries in the Dev Menu — Ctrl+Shift+D in `run-linux --dev`, the react-native-windows shortcut), `reload(reason?)` | silent no-ops in release builds, like RN |\n| `I18nManager` | `isRTL` (live: GTK\'s read of the locale text direction), `doLeftAndRightSwapInRTL`, `getConstants` | `allowRTL`/`forceRTL`/`swapLeftAndRightInRTL` are accepted no-ops (mobile persistence has no desktop store) |\n| `BackHandler` | `addEventListener("hardwareBackPress")`, `exitApp` | no hardware back key on desktop — subscriptions are honored but nothing fires them yet |\n| `findNodeHandle` | a stable integer per mounted widget, resolvable back to it; accepted by `measureLayout` as its first argument, alongside a handle object. Takes what RN takes — a component handle, a node handle (returned unchanged), `null`/`undefined`. A windowed list resolves to the `ScrollView` it renders, as RN\'s `FlatList` resolves through to its own scroll view | The tag identifies the WIDGET, not the ref: two refs onto one view report the same number and a re-render that rebuilt the handle object does not change it. It reaches nothing native — there is no `UIManager` to hand a tag to — so it is worth exactly what this platform can resolve it to: `measureLayout`, and identity. `null` for anything that is not a mounted host view (RN\'s answer too) |\n| `Keyboard` | `addListener` (honoured, never fires), `removeAllListeners`, `dismiss`, `isVisible` (always false), `metrics` (always undefined), `scheduleLayoutAnimation` | This is the SOFTWARE keyboard, and a desktop has none: every event it carries describes a panel occluding the app, so none can fire. Subscriptions are real and `remove()` pairs with them (a fake subscription would turn an unmount into a crash) — the same shape as `BackHandler` above, and what react-native-windows inherits from RN core, whose emitter is only ever fed on iOS and Android. **`dismiss()` is a no-op and deliberately not RN\'s**: RN blurs the focused input as the only way to retract the keyboard, and doing that here would let a library\'s gesture steal focus from a form |\n| `LogBox` | `ignoreLogs`, `ignoreAllLogs`, `install`, `uninstall` — accepted and ignored | RN\'s LogBox is a full-screen dev OVERLAY, and `ignoreLogs` has never filtered the console — it keeps a warning out of the yellow box. There is no overlay here, so the console output after the call is already the console output RN would have had, and nothing observable is lost. Called by `react-native-draggable-flatlist` on every `NestableDraggableFlatList` render, to silence a nesting warning this platform does not emit either |\n| `PanResponder` | `create(config)` -> `panHandlers` (spread onto a `View`), full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) — **react-native\'s own file, vendored unmodified** (MIT, `Libraries/Interaction/PanResponder.js`), running on our reproduction of RN\'s `touchHistory` store | multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`\'s return value is not consumed yet. `onPanResponderTerminationRequest` is asked when an ancestor tries to take the gesture and when an enclosing `ScrollView` scrolls; every other termination is GTK\'s decision and arrives as `onPanResponderTerminate` unasked (see `View`) |\n| `Animated` | `Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and deg/rad strings, clamp/extend/identity), `ValueXY` (`setValue`/`setOffset`/`flattenOffset`/`extractOffset`, `getLayout`, `getTranslateTransform`) — the value a `PanResponder` drag writes to | `useNativeDriver` is ignored (with a warning); the direct path is native-speed anyway; `Animated.event` is not implemented — write the value directly (`pan.setValue({x: g.dx, y: g.dy})`), which is what it would do |\n| `Easing` | linear/ease/quad/cubic/in/out/inOut/bezier | — |\n| `version` | package version | extension |\n\n`StyleProp<T>` defaults its type argument, and `ViewStyle`/`TextStyle`/`ImageStyle` are exported as aliases of the one flat style bag this platform has — so `StyleProp<ViewStyle>`, the way ordinary React Native code writes it, compiles here unchanged. `Platform.OS` is typed as the full `PlatformOSType` union (plus `"linux"`), not the `"linux"` literal: comparing it against another platform is a runtime question, and RN\'s own types let that compile everywhere.\n\nStyles (which keys go where and what is unsupported) — [style system table](../packages/react-native-gtkx/src/style/README.md). Includes `boxShadow` (RN 0.76) and `outlineColor`/`outlineOffset`/`outlineStyle`/`outlineWidth` (RN 0.77): both are what Adwaita\'s own theme uses for the `.card`/`.boxed-list` frame and for every focus ring, so they are the difference between a React Native style approximating the platform look and reproducing it — see [research/react-native-first-showcase.md](research/react-native-first-showcase.md).',
|
|
727
766
|
},
|
|
728
767
|
{
|
|
729
768
|
doc: "docs/api.md",
|
|
730
769
|
heading: "Key differences from React Native (summary)",
|
|
731
|
-
text: "1. **Desktop, not mobile**: `Modal` is a real window; `runApplication` accepts a title and dimensions; gestures are mouse-driven (hover works, no touch gestures);\n2. **Node.js runtime**: all of npm/Node is available (fs, sqlite, napi) — \"native modules\" are written as regular Node modules; RN libraries with iOS/Android code do not work;\n3. **Layout is exactly RN's**: every container runs a custom GtkLayoutManager that obeys only the Yoga engine — GTK widget minimums never leak into the layout, windows shrink freely, and `Dimensions.get(\"window\")` reports the app viewport (the window's content area under the headerbar, like RN's app window);\n4. **Text**: the ellipsis is opt-in via `numberOfLines`, exactly like RN; plain text wraps naturally and an unbreakable word wider than its box clips to it (text
|
|
770
|
+
text: "1. **Desktop, not mobile**: `Modal` is a real window; `runApplication` accepts a title and dimensions; gestures are mouse-driven (hover works, no touch gestures);\n2. **Node.js runtime**: all of npm/Node is available (fs, sqlite, napi) — \"native modules\" are written as regular Node modules; RN libraries with iOS/Android code do not work;\n3. **Layout is exactly RN's**: every container runs a custom GtkLayoutManager that obeys only the Yoga engine — GTK widget minimums never leak into the layout, windows shrink freely, and `Dimensions.get(\"window\")` reports the app viewport (the window's content area under the headerbar, like RN's app window);\n4. **Text**: the ellipsis is opt-in via `numberOfLines`, exactly like RN; plain text wraps naturally and an unbreakable word wider than its box clips to it (a text leaf always clips; a container paint-overflows until its style says otherwise — see `overflow` below);\n5. **transform** is paint-only, like RN: `translateX/Y`, `scale`, `scaleX`, `scaleY` and `rotate`/`rotateZ` apply to any component's style (not just `Animated.View`), the array composes left to right as in RN and CSS, and the origin is the view's centre. A transformed child honestly draws past its container over siblings (later siblings stay on top unless a `zIndex` says otherwise — see 10 below) without moving any ancestor, and GTK routes input through the transform, so a rotated view is clickable in its rotated shape — unless the container asks to clip, and `overflow: \"hidden\"` on it cuts the transformed child off at the edge exactly as it cuts off an untransformed one. Rotation and scale reach the widget as the `GskTransform` of its allocation (`docs/research/transforms.md`); 3D (`rotateX`/`rotateY`/`perspective`), `skewX`/`skewY`, `matrix` and `transformOrigin` are not supported;\n6. **Animations never auto-stop**: the desktop \"reduce animations\" hint is not applied automatically (GTK-side animations are kept on to match `Animated`, which runs on its own timers) — honoring reduced motion stays an app-level opt-in, as in RN;\n7. **Lists are windowed like RN's**: FlatList/SectionList mount only the rows around the viewport (prefix-sum offsets, `estimatedItemSize` refined by real measurements or exact `getItemLayout`); sticky headers translate the REAL widget (no duplicate) and `inverted` follows the RN chat contract — `contentOffset` counts from the end where `data[0]` renders. The one RefreshControl compromise: desktop has no pull gesture, so `refreshing`/`onRefresh` are API-compatible but the trigger is app chrome (a button/shortcut);\n8. The package ships compiled (`dist/`: ESM + `.d.ts` alongside, sources embedded in the maps); consumers — Metro (`react-native-gtkx/metro` preset) and vite (preset) — both consume the built output. Requires Node ≥ 24 (the gtkx runtime floor; the run-linux host also relies on `module.registerHooks`).\n9. **`zIndex` orders paint AND picking, per sibling group.** GTK4 has no z-order property, so the container widget does it: it allocates its children in Yoga's order and _snapshots_ them in `zIndex` order, and a widget covered by a higher-painting sibling declines `gtk_widget_pick()` so input lands where the pixels are. Layout is untouched — only the paint pass sorts. RN's rules, checked rather than assumed and each pinned by a test: it applies whatever `position` is (CSS needs a non-`static` `position`; RN does not, and neither does this); equal values keep document order (the sort is stable); `undefined` is `0` and negatives are legal and paint below silent siblings; and it is **per sibling group only** — it creates no stacking context that escapes the parent, so a child cannot paint above its parent's sibling. That last rule is the one that decides what you write: to lift a dragged chip over a drop-zone row, put the `zIndex` on the chip's ROW, exactly as on iOS and Android. Animated (`Animated.View`, `useAnimatedStyle`) on the same terms as `opacity` — one widget write, no Yoga pass. **One divergence**: `contains()` is GTK's only per-point hook and it is consulted after a widget's children, so an _interactive native leaf_ inside a covered sibling — a `TextInput`, a `Switch`, a `ScrollView` viewport, a raw GTK widget in a slot — still takes the press even where a raised view covers it. `Text` and `Image` do not (they have no press prop here, so while something is raised they are excluded from picking and the press reaches their nearest `View`), which is also why a `pointerEvents: \"box-none\"` View whose only child is a `Text` falls through to what is behind it while a sibling in that container is raised. Measurements, the probe, the mutation check and the real-pointer proof: [research/z-index.md](research/z-index.md).\n\n10. **Pre-commit hooks regenerate derived data**: editing this file (or the other generator inputs) and forgetting to run `scripts/generate-mcp-data.mjs` no longer fails CI — the pre-commit hook regenerates `packages/react-native-gtkx/src/mcp/data/generated.ts` and stages it for you.",
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
doc: "docs/api.md",
|
|
774
|
+
heading: "Package aliases",
|
|
775
|
+
text: "Both presets — `withLinuxPlatform` (Metro) and `reactNativeGtkx` (vite) —\nrewrite six package names during resolution, from one table\n(`packages/react-native-gtkx/src/aliases/index.ts`) that both of them read. A\nname is matched **exactly or with a `/` after it**, and the tail is\ntransplanted onto the target: `react-native-svg/lib/x` becomes\n`react-native-gtkx/svg/lib/x`, while `react-native-svg-icons` is left alone.\n\n| Package | Resolves to | Why |\n| ------------------------------------------------------------------------------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `react-native` | `react-native-gtkx` | **The platform.** Not a substitution and not configurable — the out-of-tree `npmPackageName` declaration alone does not alias imports for a bundle. |\n| [`react-native-svg`](#react-native-svg-compatibility-react-native-gtkxsvg) | `react-native-gtkx/svg` | The real package is a native module. |\n| [`react-native-reanimated`](#react-native-reanimated-react-native-gtkxreanimated) | `react-native-gtkx/reanimated` | The real package needs a worklet runtime and a Babel plugin. |\n| [`react-native-worklets`](#react-native-worklets-react-native-gtkxworklets) | `react-native-gtkx/worklets` | Where Reanimated 4 moved that runtime. Libraries pull `scheduleOnRN`/`scheduleOnUI` out of it at module scope, so an unaliased name fails at import. |\n| [`react-native-gesture-handler`](#react-native-gesture-handler-react-native-gtkxgesture-handler) | `react-native-gtkx/gesture-handler` | A shim, not a port: it implements `GestureHandlerRootView` and makes every other export throw where it is used. |\n| [`react-native-reanimated-dnd`](#drag-and-drop-react-native-gtkxdnd) | `react-native-gtkx/dnd` | A mirror of its API on GTK drag-and-drop. **The one that is a real choice** — see below. |",
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
doc: "docs/api.md",
|
|
779
|
+
heading: "The one that is a real choice",
|
|
780
|
+
text: "Five of the six substitute an implementation that cannot run here **at all**.\n`react-native-reanimated-dnd` stopped being one of those: the real 2.0.0 runs\non top of this platform's Reanimated, worklets and gesture-handler surfaces,\ndragged by a real pointer (the gallery's Upstream drop zones and Upstream\nsortables sections). So\nthere is a genuine trade:\n\n- **`react-native-gtkx/dnd` (default)** — GDK carries a\n `Gtk.WidgetPaintable` of the dragged view above every window, with the\n theme's own drag cursors, hit testing against the real widget tree and\n drops into _other applications_. The dragged view itself never moves.\n- **the real `react-native-reanimated-dnd`** — `dragAxis`, `dragBoundsRef`,\n `dropAlignment`, `collisionAlgorithm` and the rest of upstream's prop\n surface, and the view moves under the pointer. No drag icon, no\n cross-application drop, and the drag is confined to the app window.\n\nEverything else in [Differences from\n`react-native-reanimated-dnd`](#differences-from-react-native-reanimated-dnd)\napplies to the mirror; the real package has upstream's behaviour by\ndefinition.",
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
doc: "docs/api.md",
|
|
784
|
+
heading: "Configuring the package aliases",
|
|
785
|
+
text: 'Both presets take an `aliases` option: **deltas keyed by package name**, not a\nreplacement list. Anything you do not mention keeps its default, which is the\npoint — a list you have to re-state in full is a list that can silently lose\nan entry, and `ssr.noExternal` losing three of these six names is what put the\nreal `react-native-gesture-handler` into a Linux app.\n\n```ts\n// vite.config.ts\nimport { reactNativeGtkx } from "react-native-gtkx/vite"\n\nexport default defineConfig({\n plugins: [\n reactNativeGtkx({\n aliases: {\n // false — drop one of ours, so the real package loads\n "react-native-reanimated-dnd": false,\n // string — exact name or subpath, tail transplanted\n "my-pkg": "my-pkg/linux",\n // { pattern, replace } — for the rare case where the subpath\n // layouts differ\n "weird-pkg": { pattern: /^weird-pkg\\/lib\\/(.+)$/, replace: "impl/$1" },\n },\n }),\n ],\n})\n```\n\n```ts\n// metro.config.ts — the same object, the same semantics\nexport default withLinuxPlatform(getDefaultConfig(__dirname), {\n aliases: { "react-native-reanimated-dnd": false },\n})\n```\n\nPrefer the string form. It is anchored to the package name by construction,\nwhich is not a nicety: `react-native-reanimated-dnd` is a lookalike of\n`react-native-reanimated`, and `react-native-worklets-core` is a real,\nunrelated package (VisionCamera\'s) that looks like `react-native-worklets` —\na loose prefix rewrite sends either onto a subpath that does not exist. Reach\nfor `{ pattern, replace }` only when a package\'s subpath layout does not match\nits target\'s.\n\nBecause the rules are data rather than functions, the preset validates them\nwhen your config loads, and says what is wrong:\n\n- **an unknown key with `false`** — the aliases that exist are named, so a\n typo cannot silently do nothing;\n- **an overlapping pattern** — "your pattern also matches\n `react-native-reanimated-dnd`, which is declared separately". Two rules\n claiming one specifier would make resolution order-dependent;\n- **an unanchored pattern, or one with the `g`/`y` flag** — the first matches\n inside longer specifiers, the second carries a `lastIndex` between calls;\n- **a target that is not a module specifier** — a relative or absolute path,\n or one ending in `/`;\n- **`react-native`** — it cannot be dropped or retargeted, and the message\n says why: it is the platform, not one of the substituted packages.\n\nOn the vite path the option also drives `ssr.noExternal`, which is derived\nfrom the table rather than written out beside it. Every name in the table\nstays inside vite\'s pipeline — including the ones you turn off, deliberately:\nan un-aliased package still imports `react-native` at module scope, and that\nimport only reaches the platform alias if Node never gets the package first.',
|
|
732
786
|
},
|
|
733
787
|
{
|
|
734
788
|
doc: "docs/api.md",
|
|
735
789
|
heading: "Navigation (`react-native-gtkx/navigation`)",
|
|
736
|
-
text: 'A [react-navigation](https://reactnavigation.org) stack navigator backed by\n`Adw.NavigationView` — native Adwaita page transitions, the HeaderBar back\nbutton and back gestures stay in sync with react-navigation state (the\nreact-native-windows / native-stack model). Requires the optional peer\n`@react-navigation/native` (v8).\n\n`@react-navigation/native@8` itself peers on `react-native: "*"` (unlike\n`@react-navigation/core@8`, which has no react-native peer at all). If your\napp has no `react-native` package anywhere in its tree — a vite+gtkx app\nwith no Metro side, exactly what `examples/gallery` demonstrates —\n`npm install` will print an unmet-peer-dependency warning for it. This is\nharmless: react-native-gtkx never imports anything from the `react-native`\npackage, so nothing actually needs it at runtime; the warning is npm being\nstrict about a peer range upstream declared loosely (`"*"` — any version\nsatisfies it, npm just wants the package present at all).\n\n```tsx\nimport { NavigationContainer } from "@react-navigation/native"\nimport { createStackNavigator } from "react-native-gtkx/navigation"\n\n// Run the app with chrome: "content" — the navigator\'s HeaderBars ARE the\n// window chrome (the default system chrome would add a second titlebar):\n// AppRegistry.runApplication(name, { ..., chrome: "content" })\n\nconst Stack = createStackNavigator()\n\nconst App = () => (\n <NavigationContainer>\n <Stack.Navigator>\n <Stack.Screen\n name="Home"\n component={HomeScreen}\n />\n <Stack.Screen\n name="Details"\n component={DetailsScreen}\n options={{ title: "Details page" }}\n />\n </Stack.Navigator>\n </NavigationContainer>\n)\n```\n\n- Screen `options`: `title` (HeaderBar title, defaults to the route name),\n `headerShown` (default true).\n- `createSidebarNavigator` — the desktop drawer equivalent on\n `Adw.NavigationSplitView`: a persistent native sidebar (`AdwActionRow`\n per screen, in a GtkListBox with Adwaita `navigation-sidebar` styling)\n selects between parallel screens (TabRouter semantics). Navigator prop\n `sidebarTitle`; screen `options`: `title`, `icon` (Adwaita symbolic icon\n name for the row\'s prefix), `color` (a CSS color for a colored-dot\n prefix instead of `icon` — the two are mutually exclusive per row,\n `color` wins if both are set), `count` (a badge suffix, hidden when 0 or\n unset). Run the app with `chrome: "content"` so the split view\'s\n HeaderBars are the window chrome (`examples/gallery` is built on it).\n Navigator prop `headerButtons` packs declarative native buttons into the\n content HeaderBar end (`{id, icon, tooltip, onPress}`, `icon` is an\n Adwaita symbolic name) — the gallery\'s color-scheme toggle uses it.\n Navigator prop `collapseWidth` (sp): below this width the split view\n collapses to the sidebar or the content pane alone, through a native\n `Adw.Breakpoint` wrapping the view in an `AdwBreakpointBin` — NOT a\n `useWindowDimensions` conditional (see docs/platform-layer.md, "Two ways\n to react to size"); the property flip happens inside GTK\'s own\n allocation pass, costing no React render for the resize itself. Unset by\n default — no `AdwBreakpointBin` is mounted at all, so existing consumers\n see no behavior change. Any route becoming active while collapsed\n reveals content (`AdwNavigationSplitView.showContent`, a plain native\n property write, not React state) — a row click OR a programmatic\n `navigate()`/`jumpTo()`; the native back button that then appears\n reverses it. Re-selecting the same, already-active row after that also\n reveals content again — GTK\'s `row-selected` does not refire for a\n re-click with no selection change, so this is driven by `row-activated`\n (fires on every click) in addition. The reverse direction — the split\n view\'s own back button, Escape or back gesture hiding content again — is\n observed too: it fires a `sidebarShown` event\n (`navigation.addListener("sidebarShown", …)`) on the currently active\n route, the same event-map protocol `createStackNavigator`\'s\n `transitionStart`/`transitionEnd` use. Nothing in react-navigation state\n changes when this fires — TabRouter has no "closed" concept, the same\n route stays focused, only the pane did — so it exists purely for an app\n that wants to react (`examples/tasks-nav`\'s `ContentScreen` resets its\n own in-screen "open task" state on it). Never fired for content being\n revealed (that direction is already an ordinary state change) or when\n `collapseWidth` is unset. Resizing back above `collapseWidth` and then\n back below it again does NOT reset `showContent` or the selection —\n confirmed empirically, not assumed — both simply persist across the\n round trip, the same size-class behavior a mobile master-detail app\n relies on; see docs/research/navigation-extensibility.md for the\n evidence.\n- **Which rung to reach for.** Three ways to put content in the sidebar,\n cheapest first — the same ladder react-navigation\'s own `tabBarIcon` →\n `drawerLabel` → `drawerContent` climbs: (1) `title`/`icon`/`color`/`count`\n above — the convenience; composes an `AdwActionRow`. (2) `sidebarRow`\n (screen option, below) — draw one row yourself; the navigator keeps the\n list and everything attached to it (selection, click → `jumpTo`, staying\n in step with navigation state, the collapsed reveal). (3) `sidebarContent`\n (navigator prop, below) — draw the whole pane, routing surface included.\n The reason rungs 2 and 3 exist at all, plainly: **`AdwActionRow` carries\n Adwaita\'s OWN row metrics, not a default this package picked** — measured\n at roughly 104px per row (with a prefix and/or count laid out) against\n ~40px for a plain title-only row — and nothing passed to\n `title`/`icon`/`color`/`count` changes that height. A screen on rung 1\n has no lever for it; wanting a different height or density means climbing\n to `sidebarRow` or `sidebarContent` instead.\n- Sidebar navigator props `minWidth` / `minHeight` (px, default 360×294 —\n GNOME\'s own adaptive floor): the narrowest size this navigator\'s UI\n supports, applied to the `AdwBreakpointBin` that `collapseWidth` mounts.\n Ignored when `collapseWidth` is unset, since no bin exists then. Adwaita\n cannot measure a breakpoint bin — what it contains changes with the\n breakpoints — so the bin reports a minimum of ZERO and warns that\n `width-request`/`height-request` must be set. Under `chrome: "content"`\n the bin is the window\'s own child, so that zero IS the window\'s floor:\n the window resizes straight past what the pane inside can draw, and\n Adwaita clips the pane instead of adapting it ("AdwNavigationSplitView\n exceeds AdwBreakpointBin width: requested 469 px, 360 px available" in\n the journal, felt as a list running off the right edge with its trailing\n controls cut away). An app whose content HeaderBar needs more than the\n default must raise it — measure the pane rather than guessing: a\n segmented control as `headerTitle` costs ~110px on its own and, unlike a\n title label, cannot ellipsize. `examples/tasks-nav` passes `480` for\n exactly that reason; the value stays below its `collapseWidth`, so the\n collapsed layout is still fully reachable.\n- Sidebar screen options `headerLeft` / `headerRight` / `headerTitle`:\n `() => ReactNode` — the content HeaderBar\'s own start/end/title, per\n screen, on top of the one navigator-wide default. This is what lets one\n screen\'s header change shape with ITS OWN selection (a filter toggle\n group for a list, a back button plus star/trash for an open item):\n call `navigation.setOptions({ headerLeft, headerRight, headerTitle })`\n from inside the screen, in an effect keyed on whatever local state\n decides its shape — no stack involved, and no new navigator API beyond\n the options themselves (`useNavigationBuilder` already re-resolves\n descriptor options on every `setOptions` call). `headerTitle` replaces\n the HeaderBar\'s title widget outright (unset, the page\'s own title\n shows automatically, as before). A screen\'s own `headerButtons`\n (`HeaderButton[]`, same shape as the navigator prop) replaces the\n navigator-level default entirely for that screen. **Caveat, found\n while testing this**: `setOptions` MERGES into the previously resolved\n options rather than replacing them — a call that omits `headerRight`\n does not clear a `headerRight` a PREVIOUS call set, it leaves it in\n place. A screen that flips between shapes must give every one of these\n four keys an explicit value (`undefined` counts as a real overwrite; an\n absent key does not) on every call, not just the ones currently in use.\n- Sidebar screen option `sidebarRow`: `() => ReactNode` — draw the row\n yourself instead of letting `title`/`icon`/`color`/`count` compose one.\n Those four are a convenience, not the ceiling: they build an\n `AdwActionRow`, which brings Adwaita\'s own row metrics with it, so an app\n wanting a different shape, density or height had nothing to reach for.\n Return anything a `GtkListBoxRow` can hold — React Native content, GTK\n widgets, a differently-configured Adwaita row. The navigator keeps owning\n row BEHAVIOUR (selection, click → `jumpTo`, staying in step with\n navigation state, the collapsed reveal), so a custom row cannot drift out\n of sync with the router; only what is drawn changes. A screen that passes\n none of `icon`/`color`/`count` gets a compact `GtkListBoxRow` + label\n automatically — `AdwActionRow`\'s height is right when there IS a prefix\n and a count to lay out and pure cost when there is not. The next rung up\n is `sidebarContent`, below, for replacing the whole pane rather than one\n row.\n- Sidebar navigator prop `sidebarContent`:\n `(props: SidebarContentProps) => ReactNode` — replaces the ENTIRE sidebar\n pane\'s body, for a sidebar that needs sections, a search field, a footer,\n or anything a flat list of rows cannot express. The sidebar\'s children\n stop being "one row per screen": you draw what you like, and navigation\n is just the `jumpTo` you were handed. `SidebarContentProps` carries\n `routes` (key, name, resolved options, title, `focused`), `focusedIndex`\n and `jumpTo(name)` — use those rather than dispatching yourself, so\n selection cannot drift from navigation state. The pane\'s AdwHeaderBar and\n `sidebarTitle` still belong to the navigator: this is the body under it,\n not the chrome. Mounted as React Native content (a layout root filling\n the pane); a sidebar built from GTK widgets wraps its own tree in\n `WidgetContent`, the same escape hatch `contentLayout: "widget"` is for a\n screen body. Reach for `sidebarRow` (above) first if you only want a\n different ROW — it keeps the navigator\'s list and everything attached to\n it; this one hands over the whole pane, routing included. A sidebar with\n a search field above the list and a footer below it, still driven by the\n navigator\'s own routing:\n\n ```tsx\n <Sidebar.Navigator\n sidebarContent={({ routes, focusedIndex, jumpTo }) => (\n <View style={{ flex: 1 }}>\n <SearchField onSubmit={filterRoutes} />\n <ScrollView style={{ flex: 1 }}>\n {routes.map((route, index) => (\n <Pressable\n key={route.key}\n onPress={() => jumpTo(route.name)}\n >\n <Text\n style={{\n padding: 8,\n fontWeight: index === focusedIndex ? "700" : "400",\n }}\n >\n {route.title}\n </Text>\n </Pressable>\n ))}\n </ScrollView>\n <StorageUsageFooter />\n </View>\n )}\n >\n <Sidebar.Screen\n name="Inbox"\n component={InboxScreen}\n />\n <Sidebar.Screen\n name="Trash"\n component={TrashScreen}\n />\n </Sidebar.Navigator>\n ```\n\n `route.title` is already resolved (`options.title`, falling back to the\n route name) — no need to read `options.title` yourself. `jumpTo` reveals\n the content pane when collapsed, same as a native row click; the\n navigator, not this callback, decides that.\n\n- Sidebar navigator props `sidebarHeaderLeft` / `sidebarHeaderRight` /\n `sidebarHeaderTitle`: `() => ReactNode` — the SIDEBAR pane\'s own\n AdwHeaderBar start/end/title, the exact counterparts of the content\n header\'s `headerLeft`/`headerRight`/`headerTitle`. Until these existed the\n sidebar header was a hard-coded `<AdwHeaderBar />` and `sidebarTitle` (a\n plain string) was the only thing an app could set on it at all, so a\n sidebar\'s own "new item" action — where GNOME puts it, next to the pane\n title — had nowhere to go and ended up on the content header instead\n (`examples/tasks-nav` shipped with two indistinguishable `+` buttons for\n exactly this reason). `sidebarHeaderTitle` replaces the title widget the\n same way a screen\'s `headerTitle` does; unset, `sidebarTitle` renders as\n before. Content is mounted through the same `HeaderSlotContent` root the\n content header uses, so React Native content lays out as a horizontal,\n content-hugging cluster flush with natively packed buttons — do not\n hand-roll an `IntrinsicContent` here, a bare Yoga root defaults to\n `column` and pushes the window controls onto a second row. These are\n navigator PROPS rather than screen options on purpose: there is one\n sidebar pane shared by every screen, so its chrome sits at the level\n `sidebarTitle`/`sidebarContent` already do, and the `sidebar` prefix marks\n which header a name refers to. There is deliberately no\n `sidebarHeaderButtons` convenience mirroring `headerButtons` — arbitrary\n content is the primitive, and a one-button call site reads no better as a\n `{id, icon, tooltip, onPress}` record than as the `GtkButton` it already\n is; add it only if a real call site is worse without it.\n\n- Sidebar screen option `contentLayout`: `"react-native"` (default) or\n `"widget"` — what the screen\'s body IS. The default mounts it in a Yoga\n layout root that fills the pane, so `<View style={{ flex: 1 }}>` behaves\n the way it does anywhere else. `"widget"` packs the body into the page\n directly, with no layout root in between, for a screen whose body is a\n GTK widget tree (a `GtkScrolledWindow` around an `AdwClamp` around a\n `.boxed-list` `GtkListBox`, say): GTK\'s own sizing — `vexpand`, a list\'s\n natural height — then applies normally. **Under the default a widget tree\n collapses instead**, and quietly: every widget becomes a single Yoga LEAF\n measured for its own natural size, so a container renders its first child,\n drops the rest, and reports the ~1px it can shrink to, with no error\n anywhere. `examples/tasks-nav` is built this way. Mixing is per screen,\n not per subtree — a `"widget"` screen that wants React Native content\n somewhere inside it wraps that part in `SlotContent` itself.\n- Stack screen options `headerLeft` / `headerRight`: `() => ReactNode` —\n real RN content in the HeaderBar (inputs included), hosted by an\n intrinsic-size root; `headerButtons` render after `headerRight`\n (hn-app\'s header search filter is the demo).\n- Stack screen option `gestureEnabled: false` disables the native back\n button, Escape and the back gesture for that screen (the page\'s\n Adwaita `can-pop`); a programmatic `goBack` still pops. `usePreventRemove`\n works through the same mechanism — a prevented route reports\n `can-pop: false`, so no native pop can race react-navigation state; the\n route pops once the app lifts the guard (e.g. after its own\n confirmation dialog).\n- Stack screen option `animation` maps onto `Adw.NavigationView`\'s\n `animate-transitions` — GTK has exactly one transition style, not a\n choice of styles like iOS/Android, so the option collapses to a\n boolean: `"none"` turns transitions off, any other value (including\n native-stack\'s own style names, e.g. `"slide_from_bottom"`, `"fade"`)\n turns them on, with the standard Adwaita transition rather than the\n one asked for. Requesting a specific type still animates — it is not\n silently treated as `"none"` — and warns once in development.\n `animate-transitions` is a property of the whole view, not a per-page\n one, so there is no per-screen granularity to offer: the value used is\n read from whichever screen is currently on top of the visible stack,\n recomputed on every navigation. Setting it once via `screenOptions`\n (the same value for every screen) is the reliable way to use this —\n the per-screen case only matters if different screens genuinely\n disagree, and even then only the active one\'s value is observed.\n Interactive swipe-back gestures always animate regardless of this\n setting — Adwaita\'s own behavior, not overridable here.\n- The factories are typed: `createStackNavigator<ParamList>()` gives\n typed `Screen` configs and `StackScreenProps<ParamList, Route>` for\n screen components (`SidebarScreenProps` likewise).\n- The stack navigator emits `transitionStart` / `transitionEnd` on a\n screen\'s `navigation` object, matching `@react-navigation/stack` and\n `@react-navigation/native-stack` exactly: `{ data: { closing: boolean } }`,\n `closing: false` for the screen being pushed in, `closing: true` for the\n screen being popped out. A screen that stays mounted without actually\n entering or leaving (e.g. the screen underneath a push) gets neither\n event, same as upstream. Two things worth knowing before relying on\n timing:\n - **`transitionEnd` is tied to `AdwNavigationPage`\'s own `shown`/`hidden`\n signals** — contrary to an earlier version of this page, Adwaita DOES\n expose a transition-finished signal (four of them, in fact: `showing`,\n `shown`, `hiding`, `hidden`, all per-page). `transitionEnd` on the\n entering screen fires on that screen\'s `shown`; on the leaving screen\n it fires on `hidden`. `transitionDuration` (default 400 ms) is a\n fallback only, used when a page\'s own signal never arrives — a\n signal-less environment, or a page skipped entirely by a multi-hop\n pop (popping past an intermediate screen never fires anything on it,\n since it was never the one actually on screen during the transition).\n When transitions are not animated, the real signals still fire —\n immediately — so `transitionEnd` is not delayed by the fallback\n window either.\n - **Native pops do not fire these events at all today.** A user-driven\n pop (the Adwaita back button, Escape, the back gesture) is handled by\n the widget itself before this package\'s code is told about it, so\n there is nothing to hook a `transitionStart` into. Only\n programmatic navigation (`navigate`, `goBack`, `dispatch`, …) fires\n `transitionStart`/`transitionEnd`.\n- The sidebar navigator emits `sidebarShown` (`{ data: undefined }`) on a\n screen\'s `navigation` object — the collapsed-mode counterpart of a native\n pop, and the one case where a native, user-driven interaction (the split\n view\'s own back button, Escape, the back gesture) DOES get an event: the\n widget-level property that changes (`showContent`) has no\n react-navigation state behind it at all, so there is no state change for\n an app to observe any other way. Fired on the active route only when\n `showContent` goes from shown back to hidden, and only while\n `collapseWidth` is set; never fired for content being revealed (that\n already shows up as an ordinary focused-route change).\n- The rest of the react-navigation surface — `useNavigation`, `useRoute`,\n `useFocusEffect`, `useIsFocused`, `useNavigationContainerRef`,\n `CommonActions`, `StackActions`, `usePreventRemove`, `NavigationContainer`\n and everything else — comes from `@react-navigation/native` directly, not\n from this package. **Breaking change**: earlier versions re-exported a\n subset of these names from `react-native-gtkx/navigation`; the re-export\n was removed because it was never complete (anything beyond the subset\n still required importing from `@react-navigation/native`, so it was one\n more place to look rather than a convenience). This package\'s navigation\n entry point now exports exactly its own surface: `createStackNavigator`,\n `createSidebarNavigator`, and the option/prop types around them.\n- Each screen mounts its own layout root inside the page: the page\'s\n content allocation is that screen\'s viewport.\n- Differences from `@react-navigation/native-stack`: `headerRight`/custom\n header widgets are not supported yet; deep-link "url" events never fire\n on desktop (see `Linking`).',
|
|
790
|
+
text: 'A [react-navigation](https://reactnavigation.org) stack navigator backed by\n`Adw.NavigationView` — native Adwaita page transitions, the HeaderBar back\nbutton and back gestures stay in sync with react-navigation state (the\nreact-native-windows / native-stack model). Requires the optional peer\n`@react-navigation/native` (v8).\n\n`@react-navigation/native@8` itself peers on `react-native: "*"` (unlike\n`@react-navigation/core@8`, which has no react-native peer at all). If your\napp has no `react-native` package anywhere in its tree — a vite+gtkx app\nwith no Metro side, exactly what `examples/gallery` demonstrates —\n`npm install` will print an unmet-peer-dependency warning for it. This is\nharmless: react-native-gtkx never imports anything from the `react-native`\npackage, so nothing actually needs it at runtime; the warning is npm being\nstrict about a peer range upstream declared loosely (`"*"` — any version\nsatisfies it, npm just wants the package present at all).\n\n```tsx\nimport { NavigationContainer } from "@react-navigation/native"\nimport { createStackNavigator } from "react-native-gtkx/navigation"\n\n// Run the app with chrome: "content" — the navigator\'s HeaderBars ARE the\n// window chrome (the default system chrome would add a second titlebar):\n// AppRegistry.runApplication(name, { ..., chrome: "content" })\n\nconst Stack = createStackNavigator()\n\nconst App = () => (\n <NavigationContainer>\n <Stack.Navigator>\n <Stack.Screen\n name="Home"\n component={HomeScreen}\n />\n <Stack.Screen\n name="Details"\n component={DetailsScreen}\n options={{ title: "Details page" }}\n />\n </Stack.Navigator>\n </NavigationContainer>\n)\n```\n\n- Screen `options`: `title` (HeaderBar title, defaults to the route name),\n `headerShown` (default true).\n- `createSidebarNavigator` — the desktop drawer equivalent on\n `Adw.NavigationSplitView`: a persistent native sidebar (`AdwActionRow`\n per screen, in a GtkListBox with Adwaita `navigation-sidebar` styling)\n selects between parallel screens (TabRouter semantics). Navigator prop\n `sidebarTitle`; screen `options`: `title`, `icon` (Adwaita symbolic icon\n name for the row\'s prefix), `color` (a CSS color for a colored-dot\n prefix instead of `icon` — the two are mutually exclusive per row,\n `color` wins if both are set), `count` (a badge suffix, hidden when 0 or\n unset). Run the app with `chrome: "content"` so the split view\'s\n HeaderBars are the window chrome (`examples/gallery` is built on it).\n Navigator prop `headerButtons` packs declarative native buttons into the\n content HeaderBar end (`{id, icon, tooltip, onPress}`, `icon` is an\n Adwaita symbolic name) — the gallery\'s color-scheme toggle uses it.\n Navigator prop `collapseWidth` (sp): below this width the split view\n collapses to the sidebar or the content pane alone, through a native\n `Adw.Breakpoint` wrapping the view in an `AdwBreakpointBin` — NOT a\n `useWindowDimensions` conditional (see docs/platform-layer.md, "Two ways\n to react to size"); the property flip happens inside GTK\'s own\n allocation pass, costing no React render for the resize itself. Unset by\n default — no `AdwBreakpointBin` is mounted at all, so existing consumers\n see no behavior change. Any route becoming active while collapsed\n reveals content (`AdwNavigationSplitView.showContent`, a plain native\n property write, not React state) — a row click OR a programmatic\n `navigate()`/`jumpTo()`; the native back button that then appears\n reverses it. Re-selecting the same, already-active row after that also\n reveals content again — GTK\'s `row-selected` does not refire for a\n re-click with no selection change, so this is driven by `row-activated`\n (fires on every click) in addition. The reverse direction — the split\n view\'s own back button, Escape or back gesture hiding content again — is\n observed too: it fires a `sidebarShown` event\n (`navigation.addListener("sidebarShown", …)`) on the currently active\n route, the same event-map protocol `createStackNavigator`\'s\n `transitionStart`/`transitionEnd` use. Nothing in react-navigation state\n changes when this fires — TabRouter has no "closed" concept, the same\n route stays focused, only the pane did — so it exists purely for an app\n that wants to react (`examples/tasks-nav`\'s `ContentScreen` resets its\n own in-screen "open task" state on it). Never fired for content being\n revealed (that direction is already an ordinary state change) or when\n `collapseWidth` is unset. Resizing back above `collapseWidth` and then\n back below it again does NOT reset `showContent` or the selection —\n confirmed empirically, not assumed — both simply persist across the\n round trip, the same size-class behavior a mobile master-detail app\n relies on; see docs/research/navigation-extensibility.md for the\n evidence.\n- **Which rung to reach for.** Three ways to put content in the sidebar,\n cheapest first — the same ladder react-navigation\'s own `tabBarIcon` →\n `drawerLabel` → `drawerContent` climbs: (1) `title`/`icon`/`color`/`count`\n above — the convenience; composes an `AdwActionRow`. (2) `sidebarRow`\n (screen option, below) — draw one row yourself; the navigator keeps the\n list and everything attached to it (selection, click → `jumpTo`, staying\n in step with navigation state, the collapsed reveal). (3) `sidebarContent`\n (navigator prop, below) — draw the whole pane, routing surface included.\n The reason rungs 2 and 3 exist at all, plainly: **`AdwActionRow` carries\n Adwaita\'s OWN row metrics, not a default this package picked** — measured\n at roughly 104px per row (with a prefix and/or count laid out) against\n ~40px for a plain title-only row — and nothing passed to\n `title`/`icon`/`color`/`count` changes that height. A screen on rung 1\n has no lever for it; wanting a different height or density means climbing\n to `sidebarRow` or `sidebarContent` instead.\n- Sidebar navigator props `minWidth` / `minHeight` (px, default 360×294 —\n GNOME\'s own adaptive floor): the narrowest size this navigator\'s UI\n supports, applied to the `AdwBreakpointBin` that `collapseWidth` mounts.\n Ignored when `collapseWidth` is unset, since no bin exists then. Adwaita\n cannot measure a breakpoint bin — what it contains changes with the\n breakpoints — so the bin reports a minimum of ZERO and warns that\n `width-request`/`height-request` must be set. Under `chrome: "content"`\n the bin is the window\'s own child, so that zero IS the window\'s floor:\n the window resizes straight past what the pane inside can draw, and\n Adwaita clips the pane instead of adapting it ("AdwNavigationSplitView\n exceeds AdwBreakpointBin width: requested 469 px, 360 px available" in\n the journal, felt as a list running off the right edge with its trailing\n controls cut away). An app whose content HeaderBar needs more than the\n default must raise it — measure the pane rather than guessing: a\n segmented control as `headerTitle` costs ~110px on its own and, unlike a\n title label, cannot ellipsize. `examples/tasks-nav` passes `480` for\n exactly that reason; the value stays below its `collapseWidth`, so the\n collapsed layout is still fully reachable.\n- Sidebar screen options `headerLeft` / `headerRight` / `headerTitle`:\n `() => ReactNode` — the content HeaderBar\'s own start/end/title, per\n screen, on top of the one navigator-wide default. This is what lets one\n screen\'s header change shape with ITS OWN selection (a filter toggle\n group for a list, a back button plus star/trash for an open item):\n call `navigation.setOptions({ headerLeft, headerRight, headerTitle })`\n from inside the screen, in an effect keyed on whatever local state\n decides its shape — no stack involved, and no new navigator API beyond\n the options themselves (`useNavigationBuilder` already re-resolves\n descriptor options on every `setOptions` call). `headerTitle` replaces\n the HeaderBar\'s title widget outright (unset, the page\'s own title\n shows automatically, as before). A screen\'s own `headerButtons`\n (`HeaderButton[]`, same shape as the navigator prop) replaces the\n navigator-level default entirely for that screen. **Caveat, found\n while testing this**: `setOptions` MERGES into the previously resolved\n options rather than replacing them — a call that omits `headerRight`\n does not clear a `headerRight` a PREVIOUS call set, it leaves it in\n place. A screen that flips between shapes must give every one of these\n four keys an explicit value (`undefined` counts as a real overwrite; an\n absent key does not) on every call, not just the ones currently in use.\n- Sidebar screen option `group`: `string` — the section this row belongs to.\n Consecutive screens sharing a `group` get one Adwaita section header above\n the first of them, attached with `GtkListBox.set_header_func` — the\n mechanism GNOME\'s own sidebars use. A header attached this way is a\n DECORATION owned by the row below it, not a row: it is outside the list\'s\n selection model and outside its focus chain, so the arrow keys and Tab walk\n straight past it and assistive technology never announces a row that cannot\n be activated. A header faked as a non-selectable `GtkListBoxRow` gets none\n of that. Grouping follows ROW ORDER, so screens in one group are declared\n together and a group name reappearing after a gap starts a second header\n rather than reordering anything; leave it unset on every screen (the\n default) and the list is flat. `examples/gallery` groups its sections into\n React Native / gtkx / Modules.\n- Sidebar screen option `sidebarRow`: `() => ReactNode` — draw the row\n yourself instead of letting `title`/`icon`/`color`/`count` compose one.\n Those four are a convenience, not the ceiling: they build an\n `AdwActionRow`, which brings Adwaita\'s own row metrics with it, so an app\n wanting a different shape, density or height had nothing to reach for.\n Return anything a `GtkListBoxRow` can hold — React Native content, GTK\n widgets, a differently-configured Adwaita row. The navigator keeps owning\n row BEHAVIOUR (selection, click → `jumpTo`, staying in step with\n navigation state, the collapsed reveal), so a custom row cannot drift out\n of sync with the router; only what is drawn changes. A screen that passes\n none of `icon`/`color`/`count` gets a compact `GtkListBoxRow` + label\n automatically — `AdwActionRow`\'s height is right when there IS a prefix\n and a count to lay out and pure cost when there is not. The next rung up\n is `sidebarContent`, below, for replacing the whole pane rather than one\n row.\n- Sidebar navigator prop `sidebarContent`:\n `(props: SidebarContentProps) => ReactNode` — replaces the ENTIRE sidebar\n pane\'s body, for a sidebar that needs sections, a search field, a footer,\n or anything a flat list of rows cannot express. The sidebar\'s children\n stop being "one row per screen": you draw what you like, and navigation\n is just the `jumpTo` you were handed. `SidebarContentProps` carries\n `routes` (key, name, resolved options, title, `focused`), `focusedIndex`\n and `jumpTo(name)` — use those rather than dispatching yourself, so\n selection cannot drift from navigation state. The pane\'s AdwHeaderBar and\n `sidebarTitle` still belong to the navigator: this is the body under it,\n not the chrome. Mounted as React Native content (a layout root filling\n the pane); a sidebar built from GTK widgets wraps its own tree in\n `WidgetContent`, the same escape hatch `contentLayout: "widget"` is for a\n screen body. Reach for `sidebarRow` (above) first if you only want a\n different ROW — it keeps the navigator\'s list and everything attached to\n it; this one hands over the whole pane, routing included. A sidebar with\n a search field above the list and a footer below it, still driven by the\n navigator\'s own routing:\n\n ```tsx\n <Sidebar.Navigator\n sidebarContent={({ routes, focusedIndex, jumpTo }) => (\n <View style={{ flex: 1 }}>\n <SearchField onSubmit={filterRoutes} />\n <ScrollView style={{ flex: 1 }}>\n {routes.map((route, index) => (\n <Pressable\n key={route.key}\n onPress={() => jumpTo(route.name)}\n >\n <Text\n style={{\n padding: 8,\n fontWeight: index === focusedIndex ? "700" : "400",\n }}\n >\n {route.title}\n </Text>\n </Pressable>\n ))}\n </ScrollView>\n <StorageUsageFooter />\n </View>\n )}\n >\n <Sidebar.Screen\n name="Inbox"\n component={InboxScreen}\n />\n <Sidebar.Screen\n name="Trash"\n component={TrashScreen}\n />\n </Sidebar.Navigator>\n ```\n\n `route.title` is already resolved (`options.title`, falling back to the\n route name) — no need to read `options.title` yourself. `jumpTo` reveals\n the content pane when collapsed, same as a native row click; the\n navigator, not this callback, decides that.\n\n- Sidebar navigator props `sidebarHeaderLeft` / `sidebarHeaderRight` /\n `sidebarHeaderTitle`: `() => ReactNode` — the SIDEBAR pane\'s own\n AdwHeaderBar start/end/title, the exact counterparts of the content\n header\'s `headerLeft`/`headerRight`/`headerTitle`. Until these existed the\n sidebar header was a hard-coded `<AdwHeaderBar />` and `sidebarTitle` (a\n plain string) was the only thing an app could set on it at all, so a\n sidebar\'s own "new item" action — where GNOME puts it, next to the pane\n title — had nowhere to go and ended up on the content header instead\n (`examples/tasks-nav` shipped with two indistinguishable `+` buttons for\n exactly this reason). `sidebarHeaderTitle` replaces the title widget the\n same way a screen\'s `headerTitle` does; unset, `sidebarTitle` renders as\n before. Content is mounted through the same `HeaderSlotContent` root the\n content header uses, so React Native content lays out as a horizontal,\n content-hugging cluster flush with natively packed buttons — do not\n hand-roll an `IntrinsicContent` here, a bare Yoga root defaults to\n `column` and pushes the window controls onto a second row. These are\n navigator PROPS rather than screen options on purpose: there is one\n sidebar pane shared by every screen, so its chrome sits at the level\n `sidebarTitle`/`sidebarContent` already do, and the `sidebar` prefix marks\n which header a name refers to. There is deliberately no\n `sidebarHeaderButtons` convenience mirroring `headerButtons` — arbitrary\n content is the primitive, and a one-button call site reads no better as a\n `{id, icon, tooltip, onPress}` record than as the `GtkButton` it already\n is; add it only if a real call site is worse without it.\n\n- Sidebar screen option `contentLayout`: `"react-native"` (default) or\n `"widget"` — what the screen\'s body IS. The default mounts it in a Yoga\n layout root that fills the pane, so `<View style={{ flex: 1 }}>` behaves\n the way it does anywhere else. `"widget"` packs the body into the page\n directly, with no layout root in between, for a screen whose body is a\n GTK widget tree (a `GtkScrolledWindow` around an `AdwClamp` around a\n `.boxed-list` `GtkListBox`, say): GTK\'s own sizing — `vexpand`, a list\'s\n natural height — then applies normally. **Under the default a widget tree\n collapses instead**, and quietly: every widget becomes a single Yoga LEAF\n measured for its own natural size, so a container renders its first child,\n drops the rest, and reports the ~1px it can shrink to, with no error\n anywhere. `examples/tasks-nav` is built this way. Mixing is per screen,\n not per subtree — a `"widget"` screen that wants React Native content\n somewhere inside it wraps that part in `SlotContent` itself.\n- Stack screen options `headerLeft` / `headerRight`: `() => ReactNode` —\n real RN content in the HeaderBar (inputs included), hosted by an\n intrinsic-size root; `headerButtons` render after `headerRight`\n (hn-app\'s header search filter is the demo).\n- Stack screen option `gestureEnabled: false` disables the native back\n button, Escape and the back gesture for that screen (the page\'s\n Adwaita `can-pop`); a programmatic `goBack` still pops. `usePreventRemove`\n works through the same mechanism — a prevented route reports\n `can-pop: false`, so no native pop can race react-navigation state; the\n route pops once the app lifts the guard (e.g. after its own\n confirmation dialog).\n- Stack screen option `animation` maps onto `Adw.NavigationView`\'s\n `animate-transitions` — GTK has exactly one transition style, not a\n choice of styles like iOS/Android, so the option collapses to a\n boolean: `"none"` turns transitions off, any other value (including\n native-stack\'s own style names, e.g. `"slide_from_bottom"`, `"fade"`)\n turns them on, with the standard Adwaita transition rather than the\n one asked for. Requesting a specific type still animates — it is not\n silently treated as `"none"` — and warns once in development.\n `animate-transitions` is a property of the whole view, not a per-page\n one, so there is no per-screen granularity to offer: the value used is\n read from whichever screen is currently on top of the visible stack,\n recomputed on every navigation. Setting it once via `screenOptions`\n (the same value for every screen) is the reliable way to use this —\n the per-screen case only matters if different screens genuinely\n disagree, and even then only the active one\'s value is observed.\n Interactive swipe-back gestures always animate regardless of this\n setting — Adwaita\'s own behavior, not overridable here.\n- The factories are typed: `createStackNavigator<ParamList>()` gives\n typed `Screen` configs and `StackScreenProps<ParamList, Route>` for\n screen components (`SidebarScreenProps` likewise).\n- The stack navigator emits `transitionStart` / `transitionEnd` on a\n screen\'s `navigation` object, matching `@react-navigation/stack` and\n `@react-navigation/native-stack` exactly: `{ data: { closing: boolean } }`,\n `closing: false` for the screen being pushed in, `closing: true` for the\n screen being popped out. A screen that stays mounted without actually\n entering or leaving (e.g. the screen underneath a push) gets neither\n event, same as upstream. Two things worth knowing before relying on\n timing:\n - **`transitionEnd` is tied to `AdwNavigationPage`\'s own `shown`/`hidden`\n signals** — contrary to an earlier version of this page, Adwaita DOES\n expose a transition-finished signal (four of them, in fact: `showing`,\n `shown`, `hiding`, `hidden`, all per-page). `transitionEnd` on the\n entering screen fires on that screen\'s `shown`; on the leaving screen\n it fires on `hidden`. `transitionDuration` (default 400 ms) is a\n fallback only, used when a page\'s own signal never arrives — a\n signal-less environment, or a page skipped entirely by a multi-hop\n pop (popping past an intermediate screen never fires anything on it,\n since it was never the one actually on screen during the transition).\n When transitions are not animated, the real signals still fire —\n immediately — so `transitionEnd` is not delayed by the fallback\n window either.\n - **Native pops do not fire these events at all today.** A user-driven\n pop (the Adwaita back button, Escape, the back gesture) is handled by\n the widget itself before this package\'s code is told about it, so\n there is nothing to hook a `transitionStart` into. Only\n programmatic navigation (`navigate`, `goBack`, `dispatch`, …) fires\n `transitionStart`/`transitionEnd`.\n- The sidebar navigator emits `sidebarShown` (`{ data: undefined }`) on a\n screen\'s `navigation` object — the collapsed-mode counterpart of a native\n pop, and the one case where a native, user-driven interaction (the split\n view\'s own back button, Escape, the back gesture) DOES get an event: the\n widget-level property that changes (`showContent`) has no\n react-navigation state behind it at all, so there is no state change for\n an app to observe any other way. Fired on the active route only when\n `showContent` goes from shown back to hidden, and only while\n `collapseWidth` is set; never fired for content being revealed (that\n already shows up as an ordinary focused-route change).\n- The rest of the react-navigation surface — `useNavigation`, `useRoute`,\n `useFocusEffect`, `useIsFocused`, `useNavigationContainerRef`,\n `CommonActions`, `StackActions`, `usePreventRemove`, `NavigationContainer`\n and everything else — comes from `@react-navigation/native` directly, not\n from this package. **Breaking change**: earlier versions re-exported a\n subset of these names from `react-native-gtkx/navigation`; the re-export\n was removed because it was never complete (anything beyond the subset\n still required importing from `@react-navigation/native`, so it was one\n more place to look rather than a convenience). This package\'s navigation\n entry point now exports exactly its own surface: `createStackNavigator`,\n `createSidebarNavigator`, and the option/prop types around them.\n- Each screen mounts its own layout root inside the page: the page\'s\n content allocation is that screen\'s viewport.\n- Differences from `@react-navigation/native-stack`: `headerRight`/custom\n header widgets are not supported yet; deep-link "url" events never fire\n on desktop (see `Linking`).',
|
|
737
791
|
},
|
|
738
792
|
{
|
|
739
793
|
doc: "docs/api.md",
|
|
@@ -745,6 +799,116 @@ export const DOC_CHUNKS = [
|
|
|
745
799
|
heading: "`react-native-svg` compatibility (`react-native-gtkx/svg`)",
|
|
746
800
|
text: '`react-native-gtkx/svg` re-exports the same set in `react-native-svg`\'s\nshape (`Svg` as both the default and a named export). The `react-native-gtkx/\nmetro` and `react-native-gtkx/vite` presets alias the bare `react-native-svg`\npackage name to it automatically, the same way they alias `react-native`\nitself — so portable code that imports from `react-native-svg` runs\nunmodified:\n\n```tsx\nimport Svg, { Circle, Path } from "react-native-svg"\n```\n\nApps using neither preset can point their own bundler alias at\n`react-native-gtkx/svg` by hand. `react-native-svg` itself is never a\ndependency of this package and does not need to be installed — the alias\nworks whether or not the real package is present.',
|
|
747
801
|
},
|
|
802
|
+
{
|
|
803
|
+
doc: "docs/api.md",
|
|
804
|
+
heading: "Drag and drop (`react-native-gtkx/dnd`)",
|
|
805
|
+
text: "A mirror of [`react-native-reanimated-dnd`](https://github.com/entropyconquers/react-native-reanimated-dnd)'s\nAPI, implemented on `GtkDragSource`/`GtkDropTarget`. Both presets alias the\nbare `react-native-reanimated-dnd` package name onto it, exactly as they do\n`react-native-svg` — so an app that already does drag-and-drop keeps its\nsource:\n\n```tsx\nimport { Draggable, Droppable, DropProvider } from \"react-native-reanimated-dnd\"\n```\n\n**Why a mirror and not the library.** Reanimated 4, `react-native-worklets`\nand `react-native-gesture-handler` are imported at module scope in twelve of\nits files, its sort algorithm lives inside a `useAnimatedReaction` worklet and\nits row layout inside a `useAnimatedStyle`, and its public types are written\nin `SharedValue<T>`. Full evidence in\n[research/drag-and-drop.md](research/drag-and-drop.md).\n\n**And it is the one alias that is a real choice.** Once those three surfaces\nexisted the real library ran on top of them, so an app can take upstream's own\nimplementation instead with\n`aliases: { \"react-native-reanimated-dnd\": false }` — what that trades away\nand what it buys is in [Package aliases](#the-one-that-is-a-real-choice).\n\n**A ported app changes nothing in its source.** `<GestureHandlerRootView>` —\nthe one non-drag-and-drop import such an app has, because upstream's quick\nstart puts it at the root — is covered too: `react-native-gesture-handler` is\naliased to [`react-native-gtkx/gesture-handler`](#react-native-gesture-handler-react-native-gtkxgesture-handler),\na shim that implements that root faithfully and makes every other RNGH export\nthrow where it is used. `examples/reanimated-dnd` is upstream's own example\napp, ported: its README lists every line the port had to change, and none of\nthem is a drag-and-drop call.",
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
doc: "docs/api.md",
|
|
809
|
+
heading: "Which one to reach for",
|
|
810
|
+
text: "There is one drag-and-drop API. Three sentences cover every case:\n\n- **Porting an app that already uses `react-native-reanimated-dnd`** — change\n nothing. Both presets alias the package name; the imports stay as they are.\n- **Writing a new app** — import from `react-native-gtkx/dnd`. Same names,\n same props, so the code also reads correctly to anyone who knows the\n library, and the file can move to a shared location later.\n- **Reordering by row id rather than by array index** — a `Droppable` around\n a `Draggable` per row, inside one `DropProvider`. `Sortable` owns an array\n and reports positions, which is the right shape when the component owns the\n order; when a store owns it, filters it and sorts it, the id-keyed pair is\n the one that fits. `examples/tasks-nav/src/components/task-row.tsx` is a\n worked example.\n\n`List`/`ListRow` used to offer a second, id-keyed reorder of their own. They\nare gone from `react-native-gtkx/common` entirely — they were the Adwaita\nlist _appearance_ written in React Native, and that is an app's business (see\n[platform-layer.md](platform-layer.md#listlistrowlistseparator-were-here-and-are-not-any-more)).\nNothing about `List` has anything to do with dragging any more.\n\n| Export | Notes |\n| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `DropProvider` | Scopes a set of draggables and droppables. Renders a `View` (upstream renders a fragment) because `onDragging` needs a widget. `ref` gives `getDroppedItems()` and `requestPositionUpdate()`. |\n| `Draggable`, `Draggable.Handle`, `useDraggable` | The drag source. With a handle, the `GtkDragSource` attaches to the **handle's** widget, so the rest of the item stays pressable. |\n| `Droppable`, `useDroppable` | The drop target. `capacity` is enforced in GDK's `::accept`, so a full zone shows the no-drop cursor. |\n| `Sortable`, `SortableItem`, `SortableItem.Handle`, `useSortable`, `useSortableList` | Drag-to-reorder. The component owns the order (upstream's contract); read the settled one from `onDrop`'s `allPositions`. |\n| `DraggableState`, `ScrollDirection`, `SortableDirection`, `HorizontalScrollDirection` | The enums, unchanged. |\n| `listToObject`, `objectMove`, `clamp` | The utilities, as plain functions rather than worklets. |\n| `SharedValueLike<T>` | What `SharedValue<T>` degrades to: `{ value: T }` without the worklet crossing. Reads and writes work; they just do not animate. |",
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
doc: "docs/api.md",
|
|
814
|
+
heading: "Differences from `react-native-reanimated-dnd`",
|
|
815
|
+
text: "The dragged view never moves — GDK carries a `Gtk.WidgetPaintable` of it\nabove every window, with the theme's own cursors and hit testing against the\nreal widget tree, including widgets React Native never created. Everything\nbelow follows from that one fact.\n\n| Prop | Behaviour here |\n| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `preDragDelay` | Accepted, ignored. GDK's `gtk-dnd-drag-threshold` already separates a tap from a drag. |\n| `collisionAlgorithm` | Accepted, ignored. GDK hit-tests the pointer; `\"center\"` is the closest of the three. |\n| `requestPositionUpdate()` | No-op. Nothing caches a slot rectangle, because GDK re-hit-tests every motion. |\n| `onLayoutUpdateComplete` | Accepted, ignored — there is no layout pass to complete. |\n| `itemHeight`, `estimatedItemHeight`, `enableDynamicHeights`, `useFlatList`, `containerHeight` | Accepted, ignored. Yoga lays rows out at their natural height, and there is no autoscroll for `containerHeight` to feed. |\n| `dragAxis`, `dragBoundsRef`, `animationFunction` | **Unsupported.** All three describe where the dragged view goes, and it never went anywhere. Kept in the type so a file shared with iOS and Android still compiles. |\n| `dropAlignment`, `dropOffset` | **Unsupported**, same reason. |\n| `positions`, `lowerBound`, `autoScrollDirection`, `itemHeights` | Real `{ value }` boxes (`SharedValueLike`), not `SharedValue`. Forwarding them with `{...rest}` works, reads work, writes do not animate. |\n| `SortableGrid`, `SortableGridItem`, `useGridSortable*`, `useHorizontalSortable*`, `SortableDirection.Horizontal` | **Not implemented.** Importing them fails at build time; passing `Horizontal` throws. |\n| Autoscroll near a container edge during a drag | Not implemented. |\n| Sortable list height | Rows are in flow layout, so the list is as tall as its rows — not `itemsCount × itemHeight`. |",
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
doc: "docs/api.md",
|
|
819
|
+
heading: "`react-native-gesture-handler` (`react-native-gtkx/gesture-handler`)",
|
|
820
|
+
text: "**Not a port of RNGH.** The semantics are reimplemented over this platform's\nown responder system, the same way `react-native-gtkx/reanimated` and\n`react-native-gtkx/dnd` are — upstream's implementation is the blueprint, not\na dependency. Two of the four reasons\n[research/gestures.md](research/gestures.md) originally gave for refusing RNGH\nexpired when Reanimated shipped; the other two (no `exports` map on its\n`src/web/`, and a react-native-windows precedent that has been a literal\n`// NO-OP` since 2.8.0) stand, which is why nothing is vendored.\n[research/gesture-detector.md](research/gesture-detector.md) has the\nmeasurements the design rests on.\n\nBoth presets alias the package name onto this subpath, so a ported app changes\nnothing in its source.\n\n```tsx\nimport { Gesture, GestureDetector } from \"react-native-gesture-handler\"\n\nconst offset = useSharedValue(0)\nconst start = useSharedValue(0)\n\nconst pan = Gesture.Pan()\n .activeOffsetY([-10, 10])\n // Capture where the view already is. `translationY` is measured from where\n // THIS gesture activated, so it starts at zero on every new grab — writing\n // `offset.value = event.translationY` instead would throw away everything\n // the view had accumulated and snap it back toward its origin the second\n // time you grab it.\n .onStart(() => {\n start.value = offset.value\n })\n .onUpdate((event) => {\n offset.value = start.value + event.translationY\n })\n\n;<GestureDetector gesture={pan}>\n <Animated.View style={[styles.card, animatedStyle]} />\n</GestureDetector>\n```\n\n`Tap` and `LongPress` are the same state machine with different predicates —\none recognizer, one event stream, one grant channel:\n\n```tsx\nconst doubleTap = Gesture.Tap()\n .numberOfTaps(2)\n // The tap-vs-drag rule: a press that travels further than this is a drag,\n // and stops being a tap.\n .maxDistance(10)\n .onStart(() => setZoomed((on) => !on))\n\nconst hold = Gesture.LongPress()\n .minDuration(400)\n // Fires with the pointer standing still — a long press activates on its\n // timer, not on the next movement.\n .onStart((event) => openMenuAfter(event.duration))\n```\n\n| Export | Behaviour |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `GestureHandlerRootView` | **Implemented, faithfully.** A `View` with `style ?? { flex: 1 }` — note that an explicit `style` _replaces_ the default rather than merging with it, which is what upstream does in all three of its implementations. Its other job, marking the subtree as gesture-arbitrating, is already this platform's: the responder system's lock is global, so there is nothing to scope. |\n| `GestureDetector` | **Implemented, and it adds no widget.** It renders its single child unchanged and reaches that child's widget through the handle the child already exposes, the same seam `createAnimatedComponent` uses. Its recognizer's responder props are merged into the child's, so a child with its own `onTouchStart` keeps working. `userSelect`, `touchAction` and `enableContextMenu` are Web-only upstream and are accepted and ignored. |\n| `Gesture.Pan()`, `Gesture.Tap()`, `Gesture.LongPress()`, `Gesture.Native()` | **Implemented**, all four over one state machine — the same event stream, the same grant channel, different predicates. See the config tables below. |\n| `Gesture.Pinch()`, `Gesture.Rotation()` | **Implemented, and they need a TOUCHPAD.** The same state machine, fed by `GtkGestureZoom`/`GtkGestureRotate` instead of by the pointer — see [the touchpad gestures](#gesturepinch-and-gesturerotation--the-two-that-need-a-touchpad) below. A mouse cannot produce either: with no touchpad attached they simply never begin. |\n| `usePanGesture()`, `useTapGesture()`, `useLongPressGesture()`, `useNativeGesture()`, `usePinchGesture()`, `useRotationGesture()`, `useFlingGesture()`, `useManualGesture()`, `useHoverGesture()` | **Implemented**, over the same recognizers. Nine hooks and ten recognizers, which is upstream's own count rather than a gap: `src/v3/hooks/gestures/` has nine directories and no `forceTouch`, `SingleGesture` omits ForceTouch from its union, and `useForceTouchGesture` exists nowhere in 3.1.0 — so `Gesture.ForceTouch()` is the whole API upstream offers for it. Upstream deprecated all twelve `Gesture.*` statics in 3.1.0 in favour of hooks, and its hook renamed the callbacks: `onStart` → `onActivate`, `onEnd` → `onDeactivate`, `onTouchesCancelled` → `onTouchesCancel`, no `onChange`, and `canceled` on the ending event instead of a second `success` argument. Both spellings are honoured as written. |\n| `Gesture.Race()`, `Gesture.Simultaneous()`, `Gesture.Exclusive()` | **Implemented as list-builders** over the three relation maps, with no mechanism of their own — see [the relations](#cross-gesture-relations) below. One `GestureDetector` may hold a composition, which mounts several recognizers on the one child and still adds no widget. |\n| `useCompetingGestures()`, `useSimultaneousGestures()`, `useExclusiveGestures()` | **Implemented**, the hook spelling of the same three, over the same lists. `useCompetingGestures` is `Gesture.Race()` under upstream's better name. |\n| `Gesture.Fling()`, `Gesture.Manual()`, `Gesture.Hover()`, `Gesture.ForceTouch()` | **Implemented**, and each was refused until now for a different reason — see [the last four](#the-last-four-fling-manual-hover-and-force-touch). `Fling` and `Manual` were reachable and unwritten. `Hover` was refused on a judgement about the test rig that was simply wrong, and is now the most fully verified of the four. `ForceTouch` needs a pressure-reporting **stylus**: it is driven by `GtkGestureStylus`, which is stylus-only, so a mouse produces no events for it at all. |\n| `Directions`, `HoverEffect`, `MouseButton`, `PointerType` | **Implemented**, as the plain enums they are upstream, with every value pinned by a test. `Directions` is required rather than merely harmless — `Gesture.Fling().direction()` takes those bits. `PointerType` became meaningful with `ForceTouch`, the first kind whose events are honestly not a mouse. `HoverEffect` and `MouseButton` are **inert**, exactly as they are off their platforms upstream, and are exported because the knobs that take them (`.effect()`, `.mouseButton()`) are already accepted-and-inert: a knob that takes a number while refusing the constant naming that number is incoherent. |\n| `State` | **Implemented**, as the plain enum it is upstream: `UNDETERMINED` 0, `FAILED` 1, `BEGAN` 2, `CANCELLED` 3, `ACTIVE` 4, `END` 5. Every payload carries a faithful `state`, and two of the libraries this targets compare it by value, so all six numbers are pinned by a test against 3.1.0 — a silently different one would go on compiling and quietly answer false. |\n| `ScrollView`, `FlatList`, `TextInput`, `Switch`, `Pressable` | **Implemented as the platform's own components, by identity.** Upstream builds each with `createNativeWrapper(RN.X, { disallowInterruption: true, shouldCancelWhenOutside: false })` — an RN component with a `NativeViewGestureHandler` attached, so its arbitration knows about the native scrolling underneath. Here the responder system IS that arbitration, every one of these already speaks it, and `Gesture.Native()` is how a gesture is declared over one explicitly — so the wrapper has nothing to add and the re-export is the component itself. They are here because they are RENDERED: two of the three measured consumers hand `FlatList`/`ScrollView` to `Animated.createAnimatedComponent()` at module scope. |\n| `TouchableOpacity`, `TouchableHighlight`, `TouchableWithoutFeedback` | **Implemented as the platform's own**, same reasoning. Out of scope by preference and unavoidable in fact: `@gorhom/bottom-sheet` re-exports all three from its own public entry as `BottomSheetTouchable` on every platform except iOS, so it is upstream's export rather than an app's choice. |\n| everything else | **Throws**, naming the symbol — and every remaining refusal now carries its reason rather than being a bare stub. Three groups: the RNGH **1.x component API** (`PanGestureHandler` and the eight other `*GestureHandler` components, `legacy_createNativeWrapper`), which is a second public surface over the same recognizers that upstream deprecated before it deprecated the builder, and which none of the four target libraries still uses; the **button family** (`RawButton`, `BaseButton`, `RectButton`, `BorderlessButton`, plus `TouchableNativeFeedback`, the deprecated `Touchable` mixin and `RefreshControl`), which is not RN components with a handler attached but RNGH's own native button views, with an Android ripple, `rippleColor`/`rippleRadius` and an `activeOpacity` applied by a widget this platform does not have; and the **tag registry** (`GestureStateManager`, `VirtualGestureDetector`, `InterceptingGestureDetector`), which needs a process-wide handler-tag lookup this platform deliberately does not keep — identity here is the mounted detector. The twelve `Legacy*` aliases inherit whichever of those applies. See [what stays refused, and why](#what-stays-refused-and-why). |",
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
doc: "docs/api.md",
|
|
824
|
+
heading: "`Gesture.Pan()` — the config surface",
|
|
825
|
+
text: "| Method | Behaviour |\n| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `activeOffsetX` / `activeOffsetY` / `failOffsetX` / `failOffsetY` | **Implemented.** A single number is DIRECTIONAL, by its sign — `activeOffsetX(20)` bounds the positive side only. Failure is tested before activation, and with strict comparisons where activation uses non-strict ones, so a translation exactly on a bound activates. |\n| `minDistance`, `minVelocity`, `minVelocityX`, `minVelocityY`, `minPointers`, `maxPointers` | **Implemented.** `minDistance` defaults to 10 unless an `activeOffset*` or `minVelocity*` is set, in which case those are the criteria and distance stops applying. |\n| `activateAfterLongPress` | **Implemented, and it activates on the timer** rather than on the next pointer movement — see the responder-model extension in [research/gestures.md](research/gestures.md). `0` means no hold at all, as upstream (both of its implementations guard on `> 0`). |\n| `enabled`, `shouldCancelWhenOutside`, `manualActivation` | **Implemented.** |\n| `hitSlop` | **Implemented**, in RNGH's gesture spelling rather than RN's `View` one: it can SHRINK the area (negative values), and `{ left: 0, width: 32 }` anchors a strip to one edge. |\n| the callbacks | **Implemented**: `onBegin`, `onStart`, `onUpdate`, `onChange`, `onEnd`, `onFinalize`, `onTouchesDown`, `onTouchesMove`, `onTouchesUp`, `onTouchesCancelled`. |\n| `runOnJS` | **Accepted, and does nothing** — correctly. It asks for the JS runtime; there is exactly one runtime here, so every callback already runs where it is asking. |\n| `averageTouches`, `enableTrackpadTwoFingerGesture`, `cancelsTouchesInView`, `activeCursor`, `mouseButton`, `withTestId` | **Accepted, inert** — each is platform-specific upstream too, and inert off its platform there. |\n| `simultaneousWithExternalGesture`, `requireExternalGestureToFail`, `blocksExternalGesture` | **Implemented** — see [cross-gesture relations](#cross-gesture-relations). |\n\nThe common configuration and the callbacks above are shared by all three\nrecognizers, minus `onUpdate` and `onChange`: upstream puts those on\n`ContinousBaseGesture`, which `Tap` and `LongPress` do not extend. A discrete\ngesture has no travel to report and the methods are not offered.",
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
doc: "docs/api.md",
|
|
829
|
+
heading: "Cross-gesture relations",
|
|
830
|
+
text: "Three relations, three maps keyed by handler tag, and the composers are sugar\nover them. That is upstream's shape and it is reproduced because it is the\nright one — 159 lines of list-building over three primitives.\n\n| Relation | Means |\n| ------------------------------------------------------------------- | -------------------------------------- |\n| `requireExternalGestureToFail(other)` — hook: `requireToFail` | this gesture waits for `other` to fail |\n| `simultaneousWithExternalGesture(other)` — hook: `simultaneousWith` | both may be ACTIVE at once |\n| `blocksExternalGesture(other)` — hook: `block` | `other` waits for **this** one |\n\n```tsx\nconst scroll = Gesture.Pan().activeOffsetX([-10, 10]).failOffsetY([-25, 25])\n\nconst sheet = Gesture.Pan()\n .activeOffsetY([-10, 10])\n // Held in BEGAN — taking nothing, claiming nothing — until `scroll` fails.\n .requireExternalGestureToFail(scroll)\n```\n\nA relation names the other gesture with the gesture **object**, a\n`withRef()` handle to it, or a raw handler tag, exactly as upstream's\n`GestureRef` does. Memoize the gesture you point AT (`useMemo`, a ref, or a\ncontext value): both spellings rebuild their object every render, and a\nrelation written against a stale object of a gesture that has since been\nrebuilt cannot be resolved. Upstream has the same constraint and the same\nadvice.\n\n**Two locks, at two levels, and they are deliberately not merged.** The\nresponder lock keeps its one job — this interaction belongs to React Native,\none holder, one irrevocable `CLAIMED` on the source. Gesture arbitration is a\nsecond, JS-only registry that never talks to GTK, so every relation resolves\nbefore anything is claimed. The consequences are observable:\n\n- **`Simultaneous` really means two ACTIVE gestures**, each getting its own\n `onStart`/`onUpdate`/`onEnd` for the same pointer — and there is still\n exactly ONE responder while that happens, claimed once. The gesture that did\n not win the lock is driven from the touch props, which fire regardless of\n responder status; the holder reads `onResponderMove`.\n- **Mutual exclusion is the default.** Without a relation the first gesture to\n activate cancels every other gesture watching the same interaction. A\n gesture that is already ACTIVE, or parked waiting for another, is cancelled\n by nothing except an active `Gesture.Native()` — upstream's rule, and the\n reason `Native` is special rather than just another recognizer.\n- **`END` and `FAILED` are not the same release.** A gesture waiting on\n another is released when that one FAILS or is CANCELLED, and **cancelled**\n when it ENDS: the thing it was deferring to actually happened, so its turn\n never comes.\n\n`Race` adds no relation at all, because racing is what happens anyway;\n`Simultaneous` is a pairwise fill of the second map; `Exclusive` is a chain\nfill of the first, where every group waits for all the groups before it. A\nnested `Exclusive` inside a `Simultaneous` stays exclusive.\n\n#### Relations across `Root`s\n\nThe responder lock is one per process, but the negotiation PATH is whatever\nGTK widget chain the interaction arrives on, and `NestedRoot`/`IntrinsicRoot`\nput native widgets both above and below RN views. The arbitration registry is\nalso process-wide and has **no tree knowledge at all** — it is keyed by\nhandler tag. What makes that safe is when a gesture enters it: **on the press,\nnot on mount.**\n\nSo:\n\n- **Two `Root`s that nest** — an island mounted inside another island's view —\n are one GTK widget chain, so both gestures are on one interaction path and\n every relation behaves exactly as it does inside a single `Root`. Native\n widgets in between take no part in the negotiation and do not break the\n chain.\n- **Two `Root`s that are disjoint** — separate windows, or sibling islands —\n can never have both gestures live in one interaction: there is one pointer\n and one session. A relation between them is expressible, resolves to a real\n handler tag, and simply never has an occasion to apply. It is not an error\n and it does not warn.\n- **`requireExternalGestureToFail` across disjoint `Root`s does not\n deadlock.** Parking only ever happens against a gesture that is live in the\n interaction under way, so a gesture in another `Root` is never waited for.\n Recording on mount instead would have made exactly this a permanent hang.\n\nThe same reasoning covers two gestures in one `Root` that the pointer cannot\nreach together: siblings never see each other's interaction, so a relation\nbetween siblings is inert for the same reason.",
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
doc: "docs/api.md",
|
|
834
|
+
heading: "`Gesture.Tap()` — the config surface",
|
|
835
|
+
text: "`Tap` activates on the **release**, not on the press, which is what leaves the\ninteraction available to anything else watching the same pointer while a tap is\nstill being decided. It never holds the responder until the instant it wins.\n\n| Method | Behaviour |\n| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `numberOfTaps` | **Implemented.** Between the taps the gesture stays `BEGAN` and holds nothing; `onBegin` fires once for the whole sequence, which is upstream's shape. |\n| `maxDuration` | **Implemented**, default 500ms, re-armed on every press of a sequence. A press held past it fails on the timer, with the pointer still down. |\n| `maxDelay` | **Implemented**, default 500ms — how long the next tap may take to arrive before the sequence gives up. |\n| `maxDistance` | **Implemented.** A radius from the press, not a per-axis limit. This is the tap-vs-drag rule, and it is what lets a press that turns into a drag stop being a tap. **There is no default**, which is upstream's own behaviour: all three of its distance limits start at an \"unset\" sentinel, so an unconfigured tap accepts any travel that stays inside the view. |\n| `maxDeltaX`, `maxDeltaY` | **Implemented**, per axis, and independent of `maxDistance`. |\n| `minPointers` | **Implemented**, checked against the most pointers the interaction ever had at once. Above 1 it never activates — see the differences below. |\n| `shouldCancelWhenOutside` | **On by default**, set from the constructor exactly as upstream's `TapGesture` does. A press that wanders off the view is not a tap on it. Note that upstream's own `useTapGesture` forgets this and its builder does not; both spellings agree here. |",
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
doc: "docs/api.md",
|
|
839
|
+
heading: "`Gesture.LongPress()` — the config surface",
|
|
840
|
+
text: "`LongPress` activates on a **timer**, with the pointer standing still — which\nworks only because of the out-of-event grant channel described in\n[research/gestures.md](research/gestures.md). Waiting for the next pointer move\nwould mean waiting forever for a press-and-hold.\n\n| Method | Behaviour |\n| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `minDuration` | **Implemented**, default 500ms. `minDuration(0)` activates on the next tick rather than synchronously inside the press, which upstream does; nothing observable here depends on the difference. |\n| `maxDistance` | **Implemented**, default 10, and measured **from the press** for the whole gesture rather than re-based at activation — upstream's `startX`/`startY` are set on pointer-down and never moved. Travelling past it before the press matures **fails** the gesture; travelling past it after **cancels** it, so `onEnd`/`onFinalize` report `false`. |\n| `numberOfPointers` | **Implemented**, and above 1 it never activates — see the differences below. |\n| `shouldCancelWhenOutside` | **On by default**, as upstream sets it in both spellings. |\n| `event.duration` | **Implemented.** Milliseconds since the press, which is the point of the gesture. Upstream carries it on `LongPress` alone; here every payload has it, because there is one payload type. |\n\n**Differences from `react-native-gesture-handler`.** There is one pointer and\n`pointerType` is always `MOUSE`: the responder system fabricates one touch per\npointer, and wlroots offers no virtual-touch protocol, so `minPointers(2)`,\n`numberOfPointers(2)` and every other multi-touch configuration is unreachable\nrather than merely untested — those gestures simply never activate, which is\nthe honest outcome rather than a silently single-finger one.\n`Pinch` and `Rotation` are the exception, and they are implemented — they take\ntheir numbers from a touchpad rather than from the pointer, which is the one\ninput this platform has that carries more than one contact point. See below.",
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
doc: "docs/api.md",
|
|
844
|
+
heading: "`Gesture.Pinch()` and `Gesture.Rotation()` — the two that need a touchpad",
|
|
845
|
+
text: "**Implemented, and they are the only two gestures here that a mouse cannot\nproduce.** Everything else on this page runs off the pointer. These two do not,\nbecause a pinch is not a pointer event in any sense: it is a conclusion\nlibinput draws from two fingers moving on a device it has classified as a\ntouchpad, delivered to the app as `zwp_pointer_gestures_v1` and turned by GDK\ninto a `GDK_TOUCHPAD_PINCH`.\n\nThat turns out to be a better path than upstream has. RNGH's own single-runtime\nimplementation runs a `ScaleGestureDetector` over two tracked pointers and needs\ntwo real touches; only its `Pan` has a trackpad path at all, and that one is a\nwheel-event heuristic. GTK's `gtk_gesture_zoom_filter_event` lets the touchpad\nevent straight through at two fingers and reads\n`gdk_touchpad_event_get_pinch_scale()` off it, so the scale and the angle\narrive first-class rather than being reconstructed from positions.\n\n**Recognition and arbitration are unchanged.** These are ordinary participants\nin the same registry: the same state machine, the same callbacks, the same\n`tryActivate`, the same three relation maps, the same broadcast cancel. Only the\nraw numbers come from GTK, because they are what the pointer stream physically\nlacks. There is no second arbitration path — `Gesture.Simultaneous(pinch,\nrotation)` behaves exactly as `Gesture.Simultaneous(pan, tap)` does, and a\n`Pinch` and a `Rotation` written without a relation race and cancel each other.\n\n```tsx\nconst scale = useSharedValue(1)\nconst angle = useSharedValue(0)\n\n// A photo viewer: both gestures live at once, which is what the relation buys.\nconst pinch = Gesture.Pinch().onUpdate((event) => {\n scale.value = event.scale // 1 at the start, cumulative, >1 for a spread\n})\nconst rotation = Gesture.Rotation().onUpdate((event) => {\n angle.value = event.rotation // radians since the start, positive clockwise\n})\n\n;<GestureDetector gesture={Gesture.Simultaneous(pinch, rotation)}>\n <Animated.View style={animatedStyle}>{/* ... */}</Animated.View>\n</GestureDetector>\n```\n\n| Field / method | Behaviour |\n| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `event.scale` | **Implemented, cumulative and multiplicative**, 1 at the start of the gesture — upstream's own meaning. Not re-based when the gesture activates, which is where it differs from `Pan`'s translation: upstream's `resetProgress()` resets `scale` only while the handler is not yet ACTIVE, and GTK measures from its own recognition point for the same reason. |\n| `event.scaleChange` | **Implemented** as a RATIO — upstream's `changeEventCalculator` divides for `Pinch` where it subtracts for `Rotation`, because scale composes by multiplication. On the first update it is the `scale` itself, also upstream's. |\n| `event.rotation` | **Implemented**, radians since the start of the gesture, **positive clockwise** — upstream's convention, and also libinput's and GDK's, so nothing is negated on the way through. |\n| `event.rotationChange` | **Implemented** as a difference in radians. |\n| `event.focalX` / `focalY`, `event.anchorX` / `anchorY` | **Implemented**, in the gesture VIEW's coordinates — the space upstream's `absoluteToLocal` puts them in. They come from `gtk_gesture_get_bounding_box_center()`, whose coordinates are already relative to the widget the controller is on. `absoluteX`/`absoluteY` carry the same point in window coordinates. |\n| `event.velocity` | **Implemented per SECOND** — scale-per-second for `Pinch`, radians-per-second for `Rotation`. This is a deliberate divergence; see below. |\n| activation | `Rotation` activates at **5° of accumulated rotation**, which is upstream's `ROTATION_RECOGNITION_THRESHOLD` exactly. `Pinch` activates at **5% of scale change**, which is not upstream's number and cannot be; see below. |\n| `enabled`, `hitSlop`, `manualActivation`, `runOnJS`, `withTestId`, the three relations | **Implemented**, as for every other kind. `hitSlop` is tested against the focal point. |\n| `shouldCancelWhenOutside` | **Off by default**, as upstream sets it from `PinchGestureHandler.init` / `RotationGestureHandler.init` — a pinch is not addressed to a point the way a tap is, so a focal point that drifts off the view mid-gesture does not cancel it. |\n| the callbacks | **Implemented**: `onBegin`, `onStart`, `onUpdate`, `onChange`, `onEnd`, `onFinalize`. Both gestures are CONTINUOUS upstream, so they have `onUpdate`/`onChange`. |\n| the `onTouches*` callbacks | **Accepted, and never fire.** There is no touch sequence behind a touchpad gesture to report — no pointer goes down. Upstream's do not fire on a trackpad either. |\n| pinch-specific / rotation-specific config | **There is none, upstream included.** `PinchGesture` and `RotationGesture` add zero builder methods over `ContinousBaseGesture` in 3.1.0, and v3's `PinchGestureNativeProperties` is literally `Record<string, never>`. |\n\n**Two deliberate divergences, both named.**\n\n`velocity` is **per second**, and upstream's web path computes neither of these\nthat way. `PinchGestureHandler` divides the scale delta by a millisecond\n`timeDelta` and never by 1000, so its number is a thousand times smaller than\nthe \"points per second\" its own documentation promises; Android's equivalent\nuses `timeDeltaSeconds` and agrees with the documentation.\n`RotationGestureDetector.timeDelta` is worse — it returns\n`currentTime + previousTime`, an addition rather than a subtraction, which makes\nthe denominator roughly twice a page-lifetime timestamp and the result not a\nvelocity at all. There is no single upstream number to reproduce here, so the\ndocumented unit wins. This is the same call this module already makes about a\nplain-number `hitSlop`, which upstream's web path silently ignores and its\nnative paths normalise: where web contradicts both the documentation and\nupstream's own native path, follow the documentation and say so.\n\n`Pinch` activates at **5% of scale change**, where upstream activates after two\nstages of pixels — `ScaleGestureDetector` reports nothing until the span between\nthe two touches has changed by more than 30px, and `PinchGestureHandler` then\nactivates after a further 15px from wherever that opened. Both are arithmetic\nover two touch POSITIONS, and a touchpad pinch has none: libinput hands the\ncompositor a ratio and GDK hands GTK a ratio, so there is no span in pixels\nanywhere in the chain to measure 45 of. What makes a small threshold the right\nrestatement rather than a weaker one is where upstream's sits in the pipeline:\nupstream's is the FIRST decision that a pinch is happening at all, while here\nlibinput has already made that decision — it will not emit a pinch until it has\nclassified the two fingers' motion as one rather than as a two-finger scroll.\nMeasured with a virtual touchpad, the first scale GTK reports after `begin` is\nalready about 1.09, so this gate is a second and smaller one.\n\n**How this was verified, since no test in the suite can.** A touchpad gesture\nneeds a compositor with a libinput backend, and the headless one each vitest\nworker runs against has none (`WLR_BACKENDS=headless`,\n`WLR_LIBINPUT_NO_DEVICES=1`) — measured, and it delivers nothing. So the chain\nbelow the GTK controller is measured by probe 6 in\n[research/gesture-detector.md](research/gesture-detector.md): a virtual\nmultitouch touchpad on `/dev/uinput`\n(`packages/react-native-gtkx/tests/gtk/support/virtual-touchpad.ts`, the\ntechnique libinput's own litest suite uses), real libinput classification, the\ndesktop session's real compositor, real GDK, and both a raw `GtkGestureZoom` and\nthe shipped `Gesture.Pinch()` at the far end.\n`packages/react-native-gtkx/tests/gtk/gesture-handler/touchpad-gestures.gtk.test.tsx`\ncovers the chain above it against real controllers on real widgets, and\n`tests/unit/gesture-handler/touchpad.test.ts` covers the semantics.",
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
doc: "docs/api.md",
|
|
849
|
+
heading: "`Gesture.Native()` — the config surface",
|
|
850
|
+
text: "`Native` stands for the widget UNDERNEATH the detector rather than for\nanything React Native is doing, which makes its one platform-specific rule the\nmost important thing about it: **it never takes the responder.** Taking it is\nwhat makes this platform declare `CLAIMED` on the GTK sequence and call\n`setKineticScrolling(false)` on every enclosing `GtkScrolledWindow` — RN's\n`setIsJSResponder`, which exists to stop a native scroller stealing a JS drag.\nA gesture whose whole meaning is \"the native scroller is handling this\" cannot\nbe the thing that switches the native scroller off. So it reports, and yields;\na GTK test drives a real wheel and a real drag over one and asserts the\nscroller stays live throughout.\n\n| Method | Behaviour |\n| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| activation | **Implemented**, on upstream's own rule: `BEGAN` on press, `ACTIVE` once the pointer has travelled its `DEFAULT_TOUCH_SLOP` of 15px, which is where a native scrollable would have started scrolling. A lift before that fails rather than ends. |\n| `shouldActivateOnStart` | **Implemented.** Takes the gesture on the press itself, which is upstream's shape for a native view that is a button rather than a scrollable. |\n| `disallowInterruption`, `yieldsToContinuousGestures` | **Recorded, and read by nothing yet.** Both are statements about ARBITRATION, and the registry that arbitrates is the orchestrator's. Refusing them would refuse `@gorhom/bottom-sheet`'s own configuration for knobs whose only effect is on a relation it also states explicitly. |\n| the callbacks | **Implemented**, all of them, and `Native` is CONTINUOUS upstream so it reports `onUpdate`/`onChange` travel like `Pan` does. They arrive from the touch props rather than from `onResponderMove`, because those fire regardless of responder status and this gesture never holds it. |\n| `shouldCancelWhenOutside` | **On by default**, as upstream's `NativeViewGestureHandler.init` sets it. |\n| a sequence taken away mid-drag | **Reported as a cancellation** — `onEnd`/`onFinalize` with `success: false`. See the note below; this is the one place on the platform where telling a theft from an ending needed new machinery. |",
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
doc: "docs/api.md",
|
|
854
|
+
heading: "The last four: fling, manual, hover and force touch",
|
|
855
|
+
text: "**All four ship, and the interesting part is that each was refused for a\ndifferent reason — only one of which turned out to be about the platform.**\nThe recon that opened this work grouped them together and its own note said two\nof them were \"reachable today and just unwritten\". Re-examined one at a time:\n\n| Recognizer | Why it was refused | What re-examining it found |\n| ---------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `Gesture.Fling()` | unwritten | Nothing was blocking it. It is a velocity predicate and a direction predicate over the machine every other kind runs on. |\n| `Gesture.Manual()` | unwritten | The smallest of the four: two constant predicates. The work was making `GestureStateManager`'s four methods real transitions rather than two transitions and two deferred flags. |\n| `Gesture.Hover()` | \"no input to run on\" | **Wrong, and inherited rather than measured.** A hover needs no button — it needs `motion_absolute` and nothing else, which is the one request the injection harness has always had. `Pressable` has shipped hover on the same GTK controller since long before this epic. It is now the most fully verified of the four. |\n| `Gesture.ForceTouch()` | needs pressure, which nothing here reports | **True of every ordinary input, and not true of the rig.** No Wayland pointer protocol carries pressure — but the tablet protocol does, and a stylus is a kernel object. See below. |\n\n#### `Gesture.Fling()` — velocity, not distance\n\nThe thing to get right, and the thing a naive test does not catch: a fling is\nnot \"the pointer travelled 200px to the right\", because a slow drag travels\nexactly as far. Upstream guards it twice and both are reproduced.\n\n| Method / rule | Behaviour |\n| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `direction` | **Implemented**, as the bitmask it is: `Directions.LEFT \\| Directions.RIGHT` accepts either. Defaults to `Directions.RIGHT`. Setting two axis bits also opens the DIAGONAL between them, with a wider cone — `UP \\| RIGHT` accepts a 45° flick that neither `UP` nor `RIGHT` accepts alone. |\n| the cones | Upstream's: 30° around each axis (±15°) and 60° around each diagonal (±30°), which tile the circle exactly. |\n| `minVelocity` | **700 units per second**, upstream's `DEFAULT_MIN_VELOCITY`, compared strictly. Not configurable upstream and not here. |\n| the deadline | **800ms** from the press, upstream's `DEFAULT_MAX_DURATION_MS`. A press that has not flung by then FAILS, whatever it is doing. |\n| `numberOfPointers` | **Implemented, and compared for EQUALITY** against the most pointers the interaction ever had — so a two-finger fling is honestly unreachable on a one-pointer platform rather than silently single-finger, the same shape `LongPress` has. |\n| when it decides | **On every move, not on the release.** A fling activates the instant it is fast enough and pointed the right way, with the button still down; the release is only the last chance. |\n| the progression | BEGAN → ACTIVE → END in one synchronous breath, with **no `onUpdate` ever** — upstream overrides `activate()` to call `end()`. `Fling` is therefore discrete, and neither spelling offers `onUpdate`. |\n\n**One documented difference from upstream, and it is the velocity itself.**\nUpstream fits a second-degree least-squares polynomial over up to 20 samples\ninside a 300ms horizon (`VelocityTracker`) and takes the linear coefficient.\nThis platform's `velocityX`/`velocityY` are the last inter-event delta — which\nis what `Pan().minVelocity()` has always used here and what every payload\nreports. `Fling` reads the same number its own event carries rather than a\nsecond, better one nothing else can see. The consequence is that this fling is\nmore sensitive to a single long frame than upstream's; the deadline and the\ncone are unaffected.\n\n#### `Gesture.Manual()` — the app owns the state machine\n\nNo configuration of its own, in either spelling, which is upstream's shape\n(`ManualGesture` adds zero builder methods; v3's `ManualGestureProperties` is\n`Record<string, never>`). It begins on the press and then decides nothing: the\n`GestureStateManager` handed to `onTouchesDown`/`onTouchesMove`/`onTouchesUp`/\n`onTouchesCancel` is the whole API.\n\n| Method | Behaviour |\n| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `.begin()` | UNDETERMINED → BEGAN. |\n| `.activate()` | BEGAN → ACTIVE, **through the ordinary arbitration**. It is a request, not a decision: it can come back parked behind `requireExternalGestureToFail`, or cancelled. Forced past `manualActivation`, as upstream's web state manager forces it. |\n| `.end()` | BEGAN or ACTIVE → END, successfully. |\n| `.fail()` | BEGAN or ACTIVE → FAILED. |\n\n**One deliberate deviation, forced by the platform.** Upstream's Manual does not\nend when the pointers lift — its documentation says so explicitly. Half of that\nis reproduced exactly: a Manual still BEGAN when the pointer comes up **stays\nBEGAN**, holding nothing. The other half is not reachable. A gesture that is\nACTIVE here is holding an _interaction_ — the responder lock, the GTK sequence,\nthe suspended scrollers — and that interaction ends when the button does.\nStaying ACTIVE past it would mean holding a lock that no longer exists,\nreceiving no further events of any kind, and never reporting an ending at all.\nSo an ACTIVE Manual ends with the interaction, successfully. `onTouchesUp` fires\nfirst and carries the state manager, so an app that wants a different ending has\nthe event to write it in.\n\n#### `Gesture.Hover()` — the refusal that did not survive contact\n\nDriven by `GtkEventControllerMotion` — the same controller `Pressable` uses for\nits `hovered` state — through the same channel `Pinch` and `Rotation` arrive on.\nIt goes straight to ACTIVE on the crossing with no threshold at all (upstream's\n`begin(); activate();` on one event), reports `x`/`y` in the gesture view's own\ncoordinates while the pointer moves inside, and ENDs — not cancels — when the\npointer leaves.\n\n**It never takes the responder**, for the same structural reason the touchpad\ngestures do not: the responder lock is a lock over an interaction, an\ninteraction starts with a press, and a hover has none. There is no session to\ntake and no GTK sequence to claim. A hover therefore cannot exclude a press.\n\n| Method | Behaviour |\n| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- |\n| `.effect()` / `hoverEffect` | **Accepted, inert.** iOS's own pointer effect; nothing in upstream's web handler branches on it either. |\n| `hitSlop`, `enabled` | **Implemented**, including the shrinking (negative) form of `hitSlop`. |\n| the callbacks | **Implemented**; `Hover` is continuous, so `onUpdate`/`onChange` report travel and `changeX`/`changeY` carry the delta. |\n| `mouseButton` | Inert for this kind on both platforms — upstream's hover entry points never consult a button. |\n\n**One thing to know before putting a hover next to something.** Mutual exclusion\nis upstream's default and is reproduced, and a hover activates whenever the\npointer crosses in — so a hover entering while a pan on another view is still\nBEGAN will cancel that pan. Upstream behaves the same way and works around it\nper-use: its own `Pressable` sets `manualActivation` on the hover recognizer\nprecisely to stop it blocking a `Gesture.Native()`. Declare\n`simultaneousWithExternalGesture` (or `Gesture.Simultaneous()`) between a hover\nand anything sharing its screen. Inventing an exemption here would be a second\narbitration rule upstream does not have.\n\n#### `Gesture.ForceTouch()` — pressure, and exactly how far it is verified\n\n**Upstream does not implement this off iOS at all**, so there is no web\nbehaviour to restate and the semantics below come from the documented contract:\nthere is no `src/web/handlers/ForceTouchGestureHandler.ts`, the web `Gestures`\nregistry has nine entries and this is not one of them, the legacy component\nresolves to a `ForceTouchFallback` that warns once and renders its children\nunchanged, and there is no v3 hook.\n\n| Method | Behaviour |\n| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `minForce` | **Implemented**, defaulting to upstream's documented **0.2**. Non-strict at the bound, like every other activation threshold here. Note that upstream's 0.2 is a doc comment rather than a constant — no JavaScript in 3.1.0 assigns it, because the real default lives in iOS code. |\n| `maxForce` | **Implemented** as a ceiling that FAILS the gesture before activation and CANCELS it after — the shape `LongPress`'s `maxDistance` has. Unset means no ceiling, which is upstream's shape too. |\n| `feedbackOnActivation` | **Accepted, inert.** There is no haptic device on this platform. |\n| `force`, `forceChange` | On every payload. `forceChange` is a **difference** (upstream's calculator subtracts, where the one for `Pinch` divides); on the first update it is the force itself. |\n| `pointerType` | **`STYLUS`**, and this is the only kind that does not say `MOUSE`. A pressure reading can only have come from a tablet tool. |\n\n**Where it comes from, and what a mouse does.** `GtkGestureStylus`, whose\n`down`/`motion`/`up` signals carry `get_axis(GDK_AXIS_PRESSURE)` already\nnormalised to `[0, 1]` — which is upstream's documented range, so nothing is\nrescaled. The controller is left at GTK's default `stylus-only`, so **a mouse\nproduces no events for it whatsoever**. That is deliberate and it is what keeps\na `ForceTouch` from quietly activating at pressure 0 on a machine with no\ntablet; a GTK test injects a real mouse press and drag over one and asserts that\nnothing fires.\n\n**How far it IS verified, measured rather than asserted.** A uinput virtual pen\ntablet (`tests/gtk/support/virtual-stylus.{py,ts}`, built from libinput's own\nlitest Wacom descriptor) drives the shipped recognizer through the whole real\nchain — kernel, evdev, libinput, compositor, GDK, `GtkGestureStylus` — under\n`spike/gesture-detector/run-stylus.sh`. It reports `begin=1 start=1 updates=13\nend=1 success=true`, activation at force **0.2298** against a `minForce` of 0.2,\nmonotonically rising forces to **1.000**, `pointerType: STYLUS`, a `maxForce`\ncancellation at **0.5196** against a ceiling of 0.5 with no update after it, and\nzero callbacks on a card the pen never touched. What it cannot do is run inside\nthe vitest suite, whose compositor is started with `WLR_BACKENDS=headless\nWLR_LIBINPUT_NO_DEVICES=1` and enumerates no input devices at all — the same\nsplit `Pinch` and `Rotation` already live with, and a property of the\ncompositor rather than of the gesture.",
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
doc: "docs/api.md",
|
|
859
|
+
heading: "What stays refused, and why",
|
|
860
|
+
text: "The refusals that remain are decisions rather than gaps, and each now carries\nits reason in the source as well as here. Re-checked against the four target\nlibraries rather than assumed: sweeping the shipped sources of\n`@gorhom/bottom-sheet` 5.2.14, `react-native-draggable-flatlist` 4.0.3,\n`react-native-drawer-layout` 4.2.9 and `react-native-reanimated-dnd` 2.0.0 for\nevery symbol still refused finds exactly one hit — `RefreshControl` in\n`@gorhom/bottom-sheet` — and it is not this package's: it comes from\n`react-native`, as a type in one file and as a value only in a `.android.tsx`\nsibling, which Metro on this platform never resolves. **Nothing reaches for a\nbutton, a legacy handler component, or any `Legacy*` alias.** The `Touchable`\nsubset shipped earlier remains the only thing upstream's own exports forced.\n\n| Refused | Why |\n| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| the nine `*GestureHandler` components and `legacy_createNativeWrapper` | The RNGH **1.x component API**, deprecated upstream years before the builder was, and migrated off by all four target libraries. Reimplementing it means a second public surface over the same recognizers, with its own `onGestureEvent`/`onHandlerStateChange` shape and its own prop plumbing, for zero measured consumers. `createNativeWrapper` specifically has nothing to add: the responder system IS the arbitration it registers with, every component here already speaks it, and `Gesture.Native()` is how a gesture is declared over one explicitly. |\n| `RawButton`, `BaseButton`, `RectButton`, `BorderlessButton` | Not RN components with a handler attached — RNGH's own **native button views**, in Java and Objective-C, with an Android ripple, `rippleColor`/`rippleRadius`, `borderless` drawable selection and an `activeOpacity` applied by the native view. There is no GTK widget with those semantics and no way to fake the ripple, so any implementation would be a `Pressable` wearing another name. |\n| `TouchableNativeFeedback`, `Touchable`, `RefreshControl` | Android's ripple by another name; RN's deprecated mixin; and pull-to-refresh, which needs a scroll gesture this `ScrollView` does not expose and a spinner widget this platform does not have. |\n| `GestureStateManager`, `VirtualGestureDetector` | Upstream's standalone `create(tag)` factory looks a mounted handler up by tag in a global `NodeManager`. **The manager an app actually uses is implemented** — it is the one handed into `onTouches*`, and it is what drives `Gesture.Manual()`. What is missing is the process-wide tag→handler registry, and its absence is deliberate: identity here is the mounted detector, and relations resolve an app's gesture object to a tag lazily so that nothing has to be looked up in a global map. Upstream deprecates this export in favour of the hook API in the same breath. |\n| `InterceptingGestureDetector` | 3.1.0's new experimental detector, which intercepts events destined for views BELOW it. On this platform that would mean claiming a GTK sequence before deciding — and `CLAIMED` is irrevocable here, so \"intercept, look, maybe give it back\" is not expressible. |\n| `GestureDetectorType` | A TYPE upstream, not a value. Type positions never reach this module at all (the alias is a bundler alias; `tsc` resolves the real package's types from node_modules), so a runtime value under that name could only be read by code that has already gone wrong. |\n| the twelve `Legacy*` aliases | Each is 3.x's escape hatch back to the 2.x implementation of a component whose 3.x spelling is either implemented here already or refused above with its own reason. Where the modern name works, the alias would be a second name carrying a promise — \"this behaves like 2.x did\" — that this platform cannot keep, never having implemented 2.x to differ from. `LegacyDrawerLayoutAndroid` is refused twice over: React Native itself does not ship `DrawerLayoutAndroid` off Android, and `@react-navigation/drawer` reaches for `react-native-drawer-layout`, which runs here. |",
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
doc: "docs/api.md",
|
|
864
|
+
heading: "A native ancestor stealing the sequence is no longer a clean release",
|
|
865
|
+
text: "The correction [research/gestures.md](research/gestures.md) records, now\nimplemented. GTK's claim propagation is asymmetric: a claim by a **descendant**\ncancels the ancestor's gesture (`::cancel` then `::end`), while a claim by an\n**ancestor** DENIES the descendant and then ends it with an ordinary\n`drag-end` — the same signal a finger lifting produces. This platform mapped\n`drag-end` to `onResponderRelease` and `onTouchEnd`, so a native widget above\nyou stealing a drag arrived in JS as a **clean, successful ending** at whatever\nposition the theft happened at.\n\n`responder/use-responder.ts` now watches `::sequence-state-changed` for the\n`->DENIED` transition, which is the only thing that separates the two, and\nroutes a denied `drag-end` to the cancel path. So a stolen interaction reaches\n`onResponderTerminate` and `onTouchCancel`, and every recognizer built on them\nreports it as a cancellation.\n\nNothing depended on this before, and the reason is measured rather than\nassumed: a view that takes the responder on PRESS makes the platform claim on\nits own gesture, and a claim by the descendant cancels every ancestor's gesture\noutright — so an ancestor never gets a second chance to steal. It becomes\nload-bearing with `Gesture.Native()`, which is the first recognizer that\ndeliberately never claims.",
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
doc: "docs/api.md",
|
|
869
|
+
heading: "The two libraries this surface was measured against RUN",
|
|
870
|
+
text: "**Measured by building them and then driving them, not by reading them.** The\nprobe app is committed at `spike/core-exports` — the real published packages\nunder the real `gtkx build`, with the presets' aliases in place, and a real\n`zwlr_virtual_pointer_v1` aimed at coordinates taken from `measureInWindow`\non the running tree. `bash spike/core-exports/run-headless.sh` in the VM\nrebuilds it, drives it and prints:\n\n```\n[core-exports] PASS the window fills the output, so window coordinates are output coordinates — columns rect = 0,0 1024x708\n[core-exports] draggable onDragBegin\n[core-exports] draggable order=b,c,a,d,e\n[core-exports] PASS the dragged row changed place — row-a y 40 -> 160\n[core-exports] PASS CONTROL: a plain ScrollView scrolls under the same injected wheel — sv row-one y 40 -> -215\n[core-exports] PASS an unstyled scrollable in a bounded parent is a viewport and scrolls — unstyled row-one y 406 -> 278\n[core-exports] PASS the sheet's own scrollable receives scroll events at all — sheet list onScroll calls = 162\n[core-exports] PASS COLLAPSED: the sheet holds its list at the top under a real scroll — sheet row-one y 559 -> 559\n[core-exports] PASS the sheet moved up under the drag — handle y 531 -> 212\n[core-exports] PASS EXTENDED: the lock releases and the same wheel scrolls the sheet's list — sheet row-one y 240 -> -84\n[core-exports] PASS NEGATIVE CONTROL: the zone the pointer never visited saw nothing — control touch events = 0\n[core-exports] DONE all checks passed\n```\n\nThat method is the point, and it corrected this section four times running.\nTwice the blockers were predicted from sources and both lists were wrong; the\nthird time the list was right about the BUILD and could not have known what\nthe first render would hit; the fourth time the probe overturned a diagnosis\nthis file had already written down as settled — the sheet's list was blamed on\nthe driven-size carve-out, and the animated height it names was never a number\nat all. Nothing below was reasoned about — each entry is\nan error the toolchain produced, in the order it produced it.\n\n**`react-native-draggable-flatlist` 4.0.3.** Stopped at BUILD on\n`react-native` rather than on `react-native-gesture-handler`:\n[`findNodeHandle`](#api-modules) and [`LogBox`](#api-modules)\n(`components/CellRendererComponent`, `components/NestableDraggableFlatList`),\nthen [`useAnimatedScrollHandler`](#implemented)\n(`components/DraggableFlatList`, `components/NestableScrollContainer`). Past\nthe build it hit three more walls that only a running app can show:\n\n1. `__DEV__` was not defined on the vite path at all — RN's own dev flag,\n which the Metro path gets from the app's stock preset and nothing supplied\n here. The preset defines it from vite's mode now;\n2. `CellRendererComponent`, which the windowed list did not accept. That prop\n is the library's whole design (the cell is what translates, and what\n provides the \"am I the active row\" context), so `ScaleDecorator` threw\n `useIsActive must be called from within CellProvider!` on first render;\n3. `useDerivedValue(() => withSpring(…))` — a documented Reanimated pattern\n and the shape of `hooks/useOnCellActiveAnimation`. The first evaluation of\n an updater has nothing to animate FROM, so upstream collapses every\n builder to its target for that one run; this platform seeded the shared\n value with the animation OBJECT instead, and the second evaluation failed\n with \"an animation can only be assigned to a shared value holding a\n number\".\n\n**`@gorhom/bottom-sheet` 5.2.14.** Stopped at BUILD on the same surface plus\ntwo: `findNodeHandle`, `LogBox`, [`Keyboard`](#api-modules) and\n[`VirtualizedList`](#components) (`hooks/useGestureEventsHandlersDefault`,\n`hooks/useScrollableSetter`, `utilities/findNodeHandle`,\n`components/bottomSheetScrollable`). Everything it takes from THIS surface\nresolved already — `Gesture.Native()`, `State`, the `Touchable` family it\nre-exports as `BottomSheetTouchable`, `TextInput`, and the cross-gesture\nrelations its pan chains configure. Its one running wall was\n`findNodeHandle` answering `null` for a list: it identifies its scrollable by\nnode handle (`hooks/useScrollable`), and a composite that resolved to nothing\nleft the sheet warning `Couldn't find the scrollable node handle id!` with no\nscrollable bound. A windowed list resolves to the `ScrollView` it renders\nnow, which is what RN does for a `FlatList` too.\n\n**`react-native-reanimated-dnd` 2.0.0 — never loads, by design; and it RUNS\nwhen it does.** Both presets alias the package name onto\n[`react-native-gtkx/dnd`](#drag-and-drop-react-native-gtkxdnd), which mirrors\nits API on GTK's own drag-and-drop, so an app never resolves the real package\nunless it asks to. The gallery's \"Upstream drop zones\" and \"Upstream\nsortables\" sections ask — `aliases:\n{ \"react-native-reanimated-dnd\": false }` (see [Package\naliases](#configuring-the-package-aliases)) — and installs it for real:\n`Draggable`, `Droppable`, `DropProvider` and `Sortable` all work on this\nsurface, dragged by a real pointer. What that took, and the two things that\nstill differ, are in\n[research/upstream-libraries.md](research/upstream-libraries.md).\n\n#### What the probe does NOT prove\n\n- **Anything about touch.** The pointer is a mouse, as everything on this rig\n is; a drag that a finger would arbitrate differently against a scroller is\n the same gap `research/gestures.md` records.\n- **The rest of either library's surface.** One draggable list and one sheet\n with a scrollable in it were driven. `BottomSheetModal`, the backdrop, the\n footer, `NestableScrollContainer`, horizontal lists and `enableDynamicSizing`\n edge cases are built but not driven.\n- **That every accepted-and-ignored call is harmless in every configuration.**\n `Keyboard`'s events never firing is right on a desktop and is still a\n behaviour difference: a sheet with `keyboardBehavior` configured has nothing\n to react to, and its keyboard state stays `UNDETERMINED`.\n\nThe throws are still the point where something IS missing. A\n`PanGestureHandler` that quietly rendered its children without gestures is\nexactly the trap [research/gestures.md](research/gestures.md) records\n`Animated.View` falling into — compiled, ran, did nothing. The stand-ins fail\non call, on render and on property access, while still answering the\nintrospection React and `console.log` do first, so the message that surfaces\nis the precise one — and `createAnimatedComponent` is the honest limit of\nthat: it reads only `displayName` and `name`, both on the allowlist, so a\nstand-in it is handed binds without complaint and cannot fail until it is\nrendered. That is why the earlier claim that RNGH's `FlatList`/`ScrollView`\nre-exports stopped `draggable-flatlist` at import was wrong.\n\nA symbol this shim does not list at all fails earlier still, at bundle time,\nwith the bundler's own \"no export named X\".",
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
doc: "docs/api.md",
|
|
874
|
+
heading: "`react-native-reanimated` (`react-native-gtkx/reanimated`)",
|
|
875
|
+
text: "Reanimated's **semantics**, reimplemented on a platform that has none of its\narchitecture — because it needs none of it. Both presets alias the bare\npackage name onto this subpath, so an app keeps its source:\n\n```tsx\nimport Animated, {\n useAnimatedStyle,\n useSharedValue,\n withTiming,\n} from \"react-native-reanimated\"\n```\n\n**Why the architecture is gone.** Reanimated exists to cross a thread\nboundary. Here GTK's main loop _is_ the JS thread — a widget call is a\nsynchronous C call on the same stack — so a worklet is an ordinary function,\n`measure()` is synchronous, and a shared value is an observable box. That is\nnot this project's reinterpretation: upstream ships the flattened version\nitself, selects it with `SHOULD_BE_USE_WEB`, and routes react-native-windows\n(no DOM, no second runtime) down it. Full evidence, and what it costs, in\n[research/reanimated.md](research/reanimated.md).\n\n**Why a reimplementation and not the library.** ~35,700 lines of `src/`, 21\nDOM-bound files, and a `Platform.OS` gate that does not know about `linux`;\nrunning it would mean maintaining a fork of a fast-moving dependency. The web\npath is the blueprint — every behaviour here was read off it — and its pure\nparts (`interpolate`, `Easing`, the spring config maths) are ported.\n\n**The Babel plugin is not needed, and not assumed.** Its output is an ordinary\nlexical closure with metadata and no injected runtime import, so `'worklet'`\nis an inert directive. This platform never runs Babel (vite/rolldown; the\nMetro path uses the app's own stock preset), while an app that also targets\niOS or Android keeps the plugin for those builds — so both configurations\nwork. Dependency tracking here is **dynamic**: a mapper subscribes to the\nshared values it actually reads, which is more precise than a static\n`__closure` scan (a conditional read is tracked correctly) and needs no build\nstep. `dependencies` arrays are accepted and control only when a mapper is\nrebuilt.",
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
doc: "docs/api.md",
|
|
879
|
+
heading: "The boundary: what can be animated",
|
|
880
|
+
text: "This is the honest limit of the surface, and it is not a runtime limit. What\nthis platform can write to a mounted widget without a React render:\n\n| Property | Reached through | How it reaches GTK |\n| ------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `opacity` | `useAnimatedStyle` | `gtk_widget_set_opacity`, straight from the animation frame. |\n| `transform` (`translateX/Y`, `scale`, `scaleX/Y`, `rotate`/`rotateZ`) | `useAnimatedStyle` | The rect store plus one queued allocation, applied as a `GskTransform`. No 3D, no skew, no `matrix` — the same list the static `transform` style takes. |\n| `top`, `left`, `right`, `bottom` — **only** on a node whose own `position` is `\"absolute\"` | `useAnimatedStyle` | Turned into a translate from the position the committed layout gave it: the same rect store, the same queued allocation, 1.99 µs. |\n| `width`, `height` — **only** where the change is confined to the node that owns it | `useAnimatedStyle` | The node's own subtree re-laid-out pinned to the driven value, into the rect store as an override — 7.1 µs for a leaf, 21.7 µs with wrapped text, the same at five siblings and at three hundred. |\n| `backgroundColor`, `color`, `borderColor` (and per side), `outlineColor` | `useAnimatedStyle` | A `GtkCssProvider` private to that widget, reloaded in place — 11.2 µs per frame, flat in the size of the tree. |\n| The numeric SVG props (`r`, `cx`, `strokeWidth`, `strokeDashoffset` and the rest of the geometry and paint numbers) | `useAnimatedProps` | The shape's own descriptor plus `queueDraw` — the SVG components subscribe to an animated node themselves, so nothing new writes. |\n\nColours deliberately do **not** go through the memoised class registry the\nstatic styles use. That registry keys on the generated CSS text, so a colour\ndriven through it would mint a class per frame into one process-wide\nstylesheet that GTK re-parses whole and that is never pruned — measured at\n0.8 ms for the first frame and 6.8 ms by the six-hundredth, still climbing.\nThe private provider has no cache and no document, so nothing about the\nstatic path — including its memoisation — changes. Every animated component\ngets this, not just `Animated.View`: the write path is a hook over \"a widget\nand its parent\", so `Animated.Text` and anything through\n`createAnimatedComponent` animate colours on the same terms.\n\n**The remaining layout properties are refused, and it is a decision rather\nthan a gap.** `flex`, `flexBasis`, every `margin*`/`padding*`, `gap` and the\n`min*`/`max*` family need a Yoga pass plus the commit walk that follows it,\nand that cost is proportional to the CONTAINER rather than to the animated\nvalue: 52 µs for a five-child container, 133 µs at sixty, 496 µs at three\nhundred, per frame. A transform is 1.5 µs at all three, and a colour 11.2 µs.\nA `useAnimatedStyle` that changes one of them warns once for that property,\nsays it is a layout property and why, and names the transform to use instead.\nThe value is still applied on the next React render rather than dropped — and\nwhen the value comes from an animation the updater returned\n(`height: withTiming(320)`), that render is produced for you: when the\nanimation reaches its target, and at most once per 100 ms while it is on its\nway, because otherwise there would not be one. Ten renders a second at the very\nmost, never one per frame: in a full run of `spike/core-exports`'s bottom\nsheet, 294 animation frames on refused properties produced 42 renders. The\ncadence is not decoration — with the settle alone, `@gorhom/bottom-sheet`\nmounting held its content mask at 96 px of a 954 px target for 1.38 s (its\ntarget is derived from the sheet's own position, so the opening spring re-aims\nit every frame and it never settles), and the list inside mounted none of its\nrows for all of it\n([research/animated-size.md §10](research/animated-size.md)).\n\nIt is a cost argument and only a cost argument. Two things that used to be\nsaid here were re-measured and are not true:\n[research/animated-size.md](research/animated-size.md) found that GTK\nre-measuring every ancestor after the resize adds nothing at any tree size,\nand that a size write cannot resize the window — the RN root reports a zero\nsize request, so the toplevel never re-negotiates. (An `IntrinsicRoot` mounted\ndirectly in GTK chrome does change the window's request, and is the one place\nthe hazard is real.)\n\n**`scaleX`/`scaleY` are an approximation for `width`/`height`, not a\nreplacement**, and the warning says which — it is the sentence a refused size\nstill gets. Measured on a 100×60 box widened to 260: a scale grows about the\nview's CENTRE, so the box moves as it grows (x 500 → 420, where the width\nchange kept x at 500), and it scales the CONTENT with the box instead of\nre-laying it out — the label inside kept its three-line 45 px layout and was\ndrawn stretched, where the width change re-wrapped it to one line of 15 px.\nReach for a scale when the content can take being stretched (a plain box, an\nimage). `translateX`/`translateY` for the insets are exact and carry no such\ncaveat.\n\nFull measurements in [research/animated-size.md](research/animated-size.md),\nnext to the original table in\n[research/animated-colors.md](research/animated-colors.md).\n\n#### The first exception: insets on an absolutely positioned node\n\n`top`, `left`, `right` and `bottom` **are** driven at frame rate, on a node\nwhose own `position` is `\"absolute\"`. Such a node is out of flow, so moving it\nchanges nothing but where it is drawn — which makes an inset exactly a\ntranslation from the position the committed layout gave it, and lets it run on\nthe transform path with no Yoga pass at all. It is the shape the whole\nsortable-list ecosystem is built on:\n\n```tsx\nconst style = useAnimatedStyle(() => ({\n position: \"absolute\",\n left: 0,\n right: 0,\n top: top.value, // driven — 1.99 µs, flat in the size of the list\n}))\n```\n\nFour things are worth knowing about it.\n\n- **It composes with your own transform**, it does not replace it. The derived\n translate is applied outermost, so it moves the already-rotated,\n already-scaled box by the distance the layout asked for — a `top: 100` under\n `scale: 2` moves the box 100 px, not 200.\n- **`right` and `bottom` invert**, because they measure inward from the far\n edge: a larger value moves the node towards the origin.\n- **An axis anchored by BOTH edges is still refused**, because it is no longer\n a translation. `left: 0, right: 0` with no `width` derives the width from\n both edges, so animating `left` there resizes the node; and with a definite\n `width` Yoga honours `left` and ignores `right` entirely, so animating\n `right` would invent motion a real layout pass would not produce. Both cases\n warn in their own words and say which configuration would work. (The\n sortable shape above is fine: its horizontal axis has two edges but neither\n of them animates, and its vertical axis has only `top`.)\n- **`measure()` reports the committed layout, not the translated position** —\n see below.\n- **`position` may live in a sibling style entry.** The usual spelling —\n `style={[styles.row, useAnimatedStyle(() => ({ top: y.value }))]}` — works:\n the decision is taken against the flattened style, not against the updater's\n object alone.\n\nMeasurements, including the hit-testing probe under real pointer injection and\nthe per-configuration table, are in\n[research/absolute-insets.md](research/absolute-insets.md).\n\n#### The second exception: a size that is confined to the node that owns it\n\n`width` and `height` **are** driven at frame rate where the change stops at\nthe node — the node's own subtree is re-laid-out pinned to the driven value,\nthe result goes into the rect store as an override, and one queued allocation\nputs it on screen. Nothing above the node is visited and nothing is written\ninto Yoga, so the cost is the size of the NODE rather than of its container:\n\n| children in the container | driven `width`, leaf | with wrapped text | the naive write | a transform |\n| ------------------------- | -------------------- | ----------------- | --------------- | ----------- |\n| 5 | 7.1 µs | 22.1 µs | 52.1 µs | 1.6 µs |\n| 60 | 6.9 µs | 21.8 µs | 133.1 µs | 1.5 µs |\n| 300 | 7.1 µs | 21.7 µs | 496.4 µs | 1.5 µs |\n\n```tsx\n// A progress bar, a disclosure panel, a sliding drawer — all the same shape.\nconst style = useAnimatedStyle(() => ({ width: width.value }))\n\n<View style={{ width: 400, height: 700 }}>\n {/* the container's width is its own, so nothing this box does can move it */}\n <Animated.View style={[{ height: 60 }, style]}>\n <Text>re-wraps as the box grows, which a scaleX does not</Text>\n </Animated.View>\n <View style={{ height: 20 }} />\n</View>\n```\n\n**This is a real layout, not a stretch.** The content inside is re-laid-out at\nthe new size: text re-wraps, a flex row inside redistributes, a stretched\nchild follows. That is the difference from `scaleX`, and it is why this is a\nYoga pass at all rather than one store write.\n\nThe precondition — measured, and refused where it does not hold:\n\n- **the axis is the container's CROSS axis.** A `width` in a column, a\n `height` in a row. A main-axis size pushes every following sibling along,\n which is the layout pass the whole refusal is about.\n- **the container's size on that axis does not come from its children.** A\n definite or percentage size, a `flex` from its own parent, or `stretch` on\n its parent's cross axis — the rule climbs until it finds one. A\n content-sized container would grow with the node.\n- **the node's OTHER axis does not come from its content.** A box with\n `height: auto` around wrapping text gets taller as it gets narrower, and\n everything after it moves.\n- **the node's resolved cross-axis alignment is `flex-start` or `stretch`.**\n `center` and `flex-end` move the node's own origin as it grows.\n- **no `aspectRatio`, and no `min`/`max` on that axis.** The first ties the\n other axis to this one; the second clamps the driven value, so the box\n silently stops following the animation.\n- **the container does not wrap**, which would re-size the node's line and\n move every line after it.\n- an **absolutely positioned** node qualifies too, on either axis, as long as\n the axis' START edge (`left`, `top`) is anchored — it then grows from an\n origin that does not move, and being out of flow it touches nothing at all.\n- **not under an `IntrinsicRoot`.** That root reports its Yoga content size to\n GTK, so a size below it feeds the window's own size request — and the driven\n value deliberately never goes into Yoga, so the island would keep its old\n request and the node would draw outside it. This is the one root shape where\n the original \"it can resize the window\" worry was real.\n\nEverything outside that keeps the refusal, and the warning names which of\nthose it was.\n\nThree more things are worth knowing.\n\n- **The container's `flexDirection` and `alignItems` are usually not in the\n updater's object** — `style={[styles.bar, useAnimatedStyle(() => ({ width:\nw.value }))]}` is the ordinary spelling. The decision is taken against the\n layout tree, so it sees the real answer either way.\n- **The driven size survives an unrelated engine flush.** It is kept as an\n override next to the animated offset rather than written over the committed\n rect, so a window resize — or any other reason the engine re-commits the\n tree mid-animation — cannot drop a frame of it.\n- **`measure()` reports the committed layout, not the driven size**, exactly\n as it does for a transform and for an animated inset. The node's Yoga size\n did not change; it catches up on the next React render.\n\nMeasurements, the hit-testing probe under real pointer injection, and the\nper-configuration comparison against a full layout pass are in\n[research/animated-size.md](research/animated-size.md).\n\n**`measure()` on a node moved this way reports the LAYOUT rect.** The node's\nYoga `top` did not change; only its allocated and painted position did. So\n`x`/`y`/`width`/`height` are the committed layout — untranslated — while\n`pageX`/`pageY` go through GTK's transform chain and report where the node is\nactually drawn. `measureInWindow` and `measureLayout` follow `pageX`/`pageY`.\nThis is a real difference from reading the geometry back on mobile, and it is\nthe same split an explicit `translateY` has always had here.\n\n**`zIndex` is driven, animated or not** — see item 10 of\n[the differences summary](#key-differences-from-react-native-summary) for what\nit means here and where it diverges. It is one widget\nwrite, no Yoga pass and no CSS, so it costs what `opacity` costs; the shape\n`useSortable` produces every frame (`{ position: \"absolute\", left: 0, right: 0,\ntop: top.value, zIndex: moving ? 1 : 0 }`) drives both `top` and `zIndex` and\nwarns about neither.\n\nEverything else — borders, radii, shadows — still reaches GTK as a CSS class\ncomputed during render. It is not dropped silently either: the property is\nnamed in a one-per-session warning and its latest value is applied on the\nnext React render — and when the value moves inside an animation, that render\nis produced for it (see above). `useAnimatedProps` has the same rule with the\nsame warning: a numeric prop is driven, anything else is named and lands on the\nnext render.\n\n#### An animation returned from the updater: where it starts, and both directions of a toggle\n\n`useAnimatedStyle(() => ({ height: withSpring(open.value ? 320 : 0) }))` is how\nReanimated's own documentation writes an animation, and it runs here on the\nplatform's one frame scheduler. Three rules decide what a given mapper run\ndoes with it, and all three are upstream's — read out of `styleUpdater` and\n`prepareAnimation` in `react-native-reanimated`'s `hook/useAnimatedStyle.ts`,\nnot inferred:\n\n- **A key animating for the first time is seeded at its target**, not animated\n to it. There is nothing to animate from. (Upstream arrives at the same place\n from the other side: its starting point for a key absent from the previous\n result is `undefined`, which leaves the animation's own `current` — the\n target — standing.)\n- **A key whose previous updater result held a plain NUMBER animates from that\n number.** So the common \"snap shut, open smoothly\" shape works, and both of\n these run 100 → 200 and 0 → 1 over the animation's duration rather than\n jumping:\n\n ```tsx\n useAnimatedStyle(() => ({ height: open.value ? withTiming(200) : 100 }))\n useAnimatedStyle(() => ({ opacity: visible.value ? withTiming(1) : 0 }))\n ```\n\n Upstream keeps the whole previous result and its starting-point branch for\n this case is one line: _\"previously it was a plain value, just set it as\n starting point\"_. This platform used to drop the key while it held the number\n and re-seed it when it next held an animation, so the seed went straight to\n the target and the animation did not play at all — silently, with the resting\n size correct\n ([research/animated-size.md §11](research/animated-size.md)).\n\n- **A later run producing the SAME animation does not restart it**, compared by\n target and shape rather than by object identity, because a mapper re-runs\n many times a second and every run builds a fresh descriptor.\n\n**The reverse direction is not the mirror image.** When a plain number replaces\na running animation — the `open.value = 0` half of the shapes above — the\nanimation is **cancelled and the number lands at once**; it does not ease back,\nand no settle is reported. That is upstream's behaviour too (it deletes the\nanimation and pushes the plain value in the same mapper run), so it is matched\nrather than symmetrised. On a driven property (`opacity`, `transform`, a\ncolour, an inset, a confined size) the number reaches the widget on that frame;\non a refused one the snap is a React render, and it is produced for you — a\nsnap that waited for the cadence or for a settle that will never come would\nleave the animation's old value on screen, which is the same silent failure\nthe cadence exists to prevent.\n\nA percentage or a colour string in the previous result is not a starting point\na numeric driver can use, so those fall back to the seed.",
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
doc: "docs/api.md",
|
|
884
|
+
heading: "Implemented",
|
|
885
|
+
text: "| Export | Behaviour |\n| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `useSharedValue`, `makeMutable`, `isSharedValue`, `cancelAnimation` | Full. A shared value is also a platform animated node, so it can be handed to `Animated.View`'s style directly as well as through `useAnimatedStyle`. |\n| `useAnimatedStyle` | Full for `opacity`, `transform`, colours, the insets of an absolutely positioned node and a `width`/`height` whose change is confined to that node — see the boundary above. An animation RETURNED from the updater (`() => ({ opacity: withTiming(1) })`) runs here as it does upstream, on the platform's one frame scheduler; a key animating for the first time is seeded at its target rather than animated to it, a key whose previous updater result held a plain NUMBER animates from that number (`height: open ? withTiming(200) : 100` runs 100 → 200, upstream's rule), a plain number replacing a running animation cancels it and snaps rather than easing back (also upstream's, and not the mirror image of the previous rule), and a later run producing the same animation does not restart it. A style whose _shape_ changes between runs costs exactly one React render and rebinds; a running animation costs none, except on a REFUSED property, which is published through React when its animation settles and at most once per 100 ms while it runs — ten renders a second at the very most, against sixty frames. |\n| `useAnimatedProps` | Numeric props, driven straight into the component that takes them — in practice the SVG shapes, which already accept `number \\| AnimatedNode` on every geometry and paint number. Same lifecycle as `useAnimatedStyle`, down to the one render a shape change costs. |\n| `useDerivedValue`, `useAnimatedReaction`, `startMapper`, `stopMapper` | Full. Mappers are torn down on unmount. |\n| `withTiming`, `withSpring`, `withSequence`, `withRepeat`, `withDelay` | Full for numeric values, on upstream's defaults (timing 300 ms / `inOut(quad)`, spring `GentleSpringConfig`), driven by the platform's own frame scheduler. Assigned to a shared value or returned from a `useAnimatedStyle`/`useAnimatedProps` updater, both. One divergence: re-aiming a running animation keeps its current value but takes the new descriptor's velocity, where upstream also carries the previous animation's velocity across — a target that moves every frame is therefore slightly more damped here. |\n| `withDecay`, `withClamp` | Full, including `velocity`, `deceleration`, `velocityFactor`, `clamp` and `rubberBandEffect` — upstream's own step function, ported. `withDecay` is what an inertial fling rides on: released with a velocity, it coasts, decelerates and stops with no target. `withClamp` runs its inner animation un-truncated and clips what reaches the value, which is upstream's distinction and is observable on an overshooting spring. |\n| `interpolate`, `clamp`, `Extrapolation`, `Extrapolate`, `Easing` | Full, including per-edge extrapolation and `Easing.bezier`'s factory shape. |\n| `interpolateColor`, `convertToRGBA`, `isColor`, `rgbaArrayToRGBAColor` | Full for `'RGB'` (upstream's 2.2 gamma) and `'HSV'` (upstream's hue-wrap correction), including its `transparent` handling. `'LAB'` throws — see the differences table. |\n| `PlatformColor` | The platform's own: theme colours by name, resolved by GTK against the live Adwaita palette. Can be animated _between_ on a shared value; cannot be interpolated _through_. |\n| `useAnimatedScrollHandler` | `onScroll` in full, on a path that was already there: `ScrollView`'s `emitScroll` runs from a `GtkAdjustment::value-changed` handler — a C callback on the loop this JS is on — so a handler that writes a shared value gets Reanimated's promise (no React render per scroll) without any event machinery. Hand the result to a scrollable's `onScroll`; the handler receives Reanimated's FLATTENED event (`event.contentOffset.y`, not `event.nativeEvent`) carrying the three measurements a `GtkScrolledWindow` can report, plus one context object shared by every call. `onBeginDrag`/`onEndDrag`/`onMomentumBegin`/`onMomentumEnd` are called: a wheel burst gets one synthetic begin/end pair and no momentum, while a touchpad glide gets all four from its native sequence. The wheel pair is a documented desktop extension because RN has no wheel. Routed through the ONE `onScroll` prop, as on mobile — `@gorhom/bottom-sheet` passes no phase prop at all — and sharing the single context object with `onScroll`, which is what its scroll lock is built on. `contentInset`, `velocity` and `zoomScale` are absent from the event rather than invented as zeros. See the `ScrollView` row and [research/scroll-phases.md](research/scroll-phases.md) |\n| `scrollTo` | `scrollTo(ref, x, y, animated)` on the scrollable an `useAnimatedRef` points at — the write half of the hook above, and the same reasoning: this IS the thread that owns the widget, so it calls the scrollable's own imperative `scrollTo` synchronously. Upstream's argument order rather than RN's options object, so library call sites are unchanged; `animated` is ignored, as it is on `ScrollView`. A ref pointing at nothing (or at something without a scroll API) is ignored rather than throwing, as upstream's is. |\n| `useScrollOffset`, `useScrollViewOffset` | A shared value holding a scrollable's current offset, updated from the adjustment's own `value-changed` — no `onScroll` prop needed and no React render. Takes upstream's second argument (write into a shared value you already own) and upstream's own axis rule (`x` when there is a horizontal offset, `y` otherwise). Point it at a `ScrollView`, a `FlatList` (which resolves through to the `ScrollView` it renders, as `findNodeHandle` does) or an `Animated.ScrollView`; a ref on anything else warns once and the value stays 0. Costs 5.15 µs per scroll event while tracking and nothing at all while not — it connects on mount and disconnects on unmount. |\n| `useHandler` | Full. `doDependenciesDiffer` is always **false**, and that is a statement rather than a stub: upstream needs it because a worklet is a by-value snapshot that goes stale, and here a handler is an ordinary closure read out of a ref at call time. `useWeb` is true, for the reason the whole surface is on upstream's web path. |\n| `useEvent` | **Scroll event names only** — `onScroll`, `onScrollBeginDrag`, `onScrollEndDrag`, `onMomentumScrollBegin`, `onMomentumScrollEnd`. The value it returns goes on a scrollable's `onScroll`, which IS the subscription here; that is the same object `useAnimatedScrollHandler` returns, so a hand-built handler and the stock one behave identically. Any other event name throws where it is asked for, naming itself: there is no native event registry to subscribe an arbitrary name against, and a subscription that could never fire is the failure mode this package refuses everywhere else. `rebuild` is accepted and ignored, for the same reason `doDependenciesDiffer` is false. `.workletEventHandler` throws — it registers a native view TAG, and there is neither. |\n| `useAnimatedKeyboard`, `KeyboardState` | Real shared values, **honoured and never updated** — the same shape and the same reason as RN's `Keyboard`: every number the hook reports describes a software panel taking screen space from the app, and a desktop has none. `height` is 0 because the keyboard occupies nothing and `state` is `CLOSED` because it is — deliberately not `UNKNOWN`, which upstream seeds only until the native side reports and here would be false. A `useAnimatedStyle` reading them subscribes, computes and settles once, so an app written for three platforms keeps one source and gets the right answer here. |\n| `useAnimatedRef`, `measure` | Full, and callable from anywhere — there is no worklet to be inside of. Returns `null` before the first committed layout, which is RN's own contract. |\n| `runOnUI`, `runOnJS`, `scheduleOnUI`, `scheduleOnRN` | Deferred, not inlined — see below. |\n| `Animated.View` | The platform's own, unchanged. Takes a `ref` giving `measure`/`measureInWindow`/`measureLayout`. |\n| `Animated.Text`, `Animated.Image`, `Animated.ScrollView` | `createAnimatedComponent` over the platform's own components — no subclass and no special case. All three forward the `ref` through, so `useAnimatedRef` + `measure()` works on them. |\n| `createAnimatedComponent` | **Adds no widget to the tree.** It renders the wrapped component itself and reaches its widget through the ref that component already exposes, so the GTK output is what the unwrapped component produces. Wrap anything that takes a `ref` giving the geometry methods; anything else gets a named warning rather than a silent no-op. |\n| `entering`, `exiting`, `layout` | On every animated component, not only `Animated.View` — see the layout-animation section below. `exiting` keeps the widget on screen after React has removed it. |\n| `FadeIn`, `FadeOut`, `LinearTransition`, `Layout`, `Keyframe` | Upstream's fluent surface (`.duration()`, `.delay()`, `.easing()`, `.springify()` and the spring parameters, `.rotate()`, `.withInitialValues()`, `.withCallback()`), usable as the class or as an instance. `Layout` is upstream's own deprecated alias of `LinearTransition`. |\n| The preset catalogue: `Fade*`, `Bounce*`, `Pinwheel*`, `Roll*`, `Rotate*`, `Slide*`, `Stretch*`, `Zoom*` | 60 of upstream's 76, on upstream's own parameters, sharing one builder over a table. The 16 that are refused are the twelve `Flip*` (`perspective` + `rotateX`/`rotateY` — a 3D rotation, where this platform folds a transform array into one 2D affine matrix) and the four `LightSpeed*` (`skewX`, a deliberate cut across the whole transform surface). Both throw by name. |\n| `CurvedTransition`, `FadingTransition`, `JumpingTransition`, `SequencedTransition`, `EntryExitTransition` | The four `layout` transitions beside `LinearTransition`, plus the one that composes an entering and an exiting builder into a single layout animation. Same properties, same paint-only position (see the differences table). |\n| `LayoutAnimationConfig`, `enableLayoutAnimations` | `<LayoutAnimationConfig skipEntering skipExiting>` suppresses the animations of the subtree below it, and adds no widget. `enableLayoutAnimations` warns and does nothing, which is exactly what it does upstream — it is deprecated there and its allow-list is gone. |\n| `withReanimatedTimer`, `advanceAnimationByTime`, `advanceAnimationByFrame` | Real, and not an emulation: the frame driver every animation here runs on is this platform's own, so a test takes it and steps it. `withReanimatedTimer` also accepts an async body, which a `@gtkx/testing` test needs. `getAnimatedStyle` and `setUpTests` are refused — see the differences table. |\n| `BaseAnimationBuilder`, `ComplexAnimationBuilder` | One class under both names — upstream splits the plain chain from the spring parameters, this platform does not — so a library subclassing either keeps working. |\n| `GentleSpringConfig` and the other seven spring presets, `ReduceMotion`, `ReanimatedLogLevel`, `isSharedValue` | Plain data, mirrored exactly. |\n| `isConfigured`, `isReanimated3`, `makeShareableCloneRecursive`, `isWorkletFunction`, `configureReanimatedLogger` | Present. Cloning is identity (nothing leaves the runtime it was made in); `configureReanimatedLogger` is accepted and does nothing, because there is no second logger to configure. |\n\nAnimating an SVG shape is the case `useAnimatedProps` exists for, and it\nreads exactly as it does on mobile:\n\n```tsx\nimport { Circle, Svg } from \"react-native-gtkx/svg\"\nimport Animated, {\n useAnimatedProps,\n useSharedValue,\n withTiming,\n} from \"react-native-reanimated\"\n\nconst AnimatedCircle = Animated.createAnimatedComponent(Circle)\n\nconst Pulse = () => {\n const r = useSharedValue(10)\n const animatedProps = useAnimatedProps(() => ({ r: r.value }))\n return (\n <Svg\n width={100}\n height={100}\n onLayout={() => (r.value = withTiming(40))}\n >\n <AnimatedCircle\n cx={50}\n cy={50}\n fill=\"green\"\n animatedProps={animatedProps}\n />\n </Svg>\n )\n}\n```\n\n#### Writing a shared value: `.value` or `.set()`\n\nBoth spellings are upstream's and both work here: `sharedValue.value = x`, or\n`sharedValue.get()` / `sharedValue.set(x)` (which also takes an updater,\n`count.set((current) => current + 1)`). They differ only under lint. The\nReact Compiler — [on by default on the vite path](getting-started.md#the-react-compiler-is-on-by-default-vite-path) —\ntreats anything a hook returns as frozen, so `react-hooks/immutability`\nreports **every** assignment to `.value`, including the ones inside a\ncallback or an effect that are perfectly legitimate. `.get()`/`.set()` is\nwhat upstream added for exactly this case and lints clean everywhere. Prefer\nit in new code; `.value` keeps working, and a ported app does not have to be\nrewritten.",
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
doc: "docs/api.md",
|
|
889
|
+
heading: "Layout animations, and the one primitive they needed",
|
|
890
|
+
text: "```tsx\n<Animated.View\n entering={FadeIn.duration(300)}\n exiting={FadeOut}\n layout={LinearTransition.springify()}\n/>\n```\n\nAll three work on every animated component — `Animated.View`,\n`Animated.Text`, `Animated.Image`, `Animated.ScrollView` and anything through\n`createAnimatedComponent` — because they are added by wrapping rather than by\nsubclassing, and the wrapper adds no widget to the tree any more than\n`createAnimatedComponent` does.\n\n`entering` and `layout` needed nothing new. `entering` writes the builder's\ninitial values in the commit that mounts the widget (so it is never drawn\nun-faded, not even for a frame) and animates from there. `layout` watches for\nthe layout engine committing a **different rect** for that child and walks it\nfrom where it was to where the engine put it.\n\n**`layout` animates the position, and applies the size.** Upstream's\n`LinearTransition` animates `originX`/`originY`/`width`/`height`; all four are\nstill produced here, and the origins are honoured as a **translation** — the\nsame paint-only write a `transform` uses, composed with whatever transform the\nstyle already has, so a row that scales while the list reorders does both. A\nsize change lands immediately instead, for the reason the boundary section\nabove already gives: animating a size means a Yoga pass per frame whose cost\nis the tree's rather than the animated value's.\n\n**`exiting` is the one that needed a new primitive**, and it is the reason\nthis slice exists. An exit animation has to keep drawing a widget React has\nalready reconciled away, and React's deletion is neither asynchronous nor\nnegotiable: in one synchronous commit it runs the unmounting subtree's\ncleanups and unparents its topmost widget. So the platform grew a **widget\nretention** primitive, generalised from the one\n`react-native-gtkx/adw`'s `NavigationStack` already used for pages — hold what\nis leaving, drop it on the real end signal, and arm a timer in case that\nsignal never comes:\n\n- The widget is put back into the same container, **at the end of the child\n list**, so it draws over the siblings closing the gap rather than under\n them.\n- Its Yoga node leaves the shadow tree immediately, so an exiting view does\n **not** hold its space — the row below it moves up at once, and the fade\n happens over the top.\n- Every container in the retained subtree keeps its layout manager until the\n animation ends, so the exiting view's own children stay exactly where the\n engine put them.\n- **A fallback timer always runs**, armed from the animation's declared\n length. Whichever arrives first — the animation's end or the timer — drops\n the widget, so a spring that never settles, a frame source that dies, or an\n animation that was never started cannot leak a widget that is still\n parented, drawn and hit-testable.\n\n`exiting` is skipped when the component's own container is unmounting in the\nsame commit: there is no container left to hold the widget, and an exit\nanimation inside a disappearing parent is not one anybody sees.",
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
doc: "docs/api.md",
|
|
894
|
+
heading: "Differences from `react-native-reanimated`",
|
|
895
|
+
text: "| Behaviour | Here |\n| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Animatable properties | `opacity`, `transform` and colours — see the boundary table. Layout properties are refused with the transform to use instead; anything else warns once by name. Both land on the next render. |\n| Animated values | Numbers only. `withTiming(\"#ff0000\")` throws rather than animating nothing: animate a number and map it with `interpolateColor`, which is what upstream's own examples do. |\n| `interpolateColor` colour spaces | `'RGB'` and `'HSV'`. `'LAB'` throws by name — upstream's is a vendored slice of culori fed 0-255 channels where culori documents 0-1, so matching it would mean matching the scaling. |\n| `interpolateColor` inputs | Colour strings only, and not `PlatformColor` — a theme colour has no value until GTK resolves it against the live theme, so it has nothing to blend. Both cases throw and say which one happened. |\n| `processColor` | Throws. It returns RN's packed AARRGGBB integer, whose only consumer is a native module; a colour's destination here is a GTK stylesheet, which takes strings. |\n| `runOnUI` / `runOnJS` | Schedule rather than run inline, and return `void`, as upstream. A UI hop is a **task**, an RN hop a microtask — so a UI hop is still the later of the two, but it does not wait for a frame the way upstream's _web_ build does (`requestAnimationFrame` stands in there for a UI runtime the web has not got; React Native's real one does not wait either). Waiting cost a `scheduleOnUI(measure)`/`scheduleOnRN(use it)` round trip a whole frame, which is longer than the gap between two GTK pointer events — see `docs/research/dnd-hover-flicker.md`. |\n| `SharedValue.addListener` | Accepts upstream's `(listenerID, listener)` **and** this platform's animated-node `(callback) => id`. Both callers are real, and supporting only one fails silently. |\n| Worklet closure capture | Live lexical capture, not the plugin's by-value snapshot. Only observable for a worklet closing over a reassigned plain `let`, which is already a bug on mobile. |\n| `withSpring` rest condition | Upstream stops on remaining energy relative to initial energy; the platform's solver stops on displacement and speed thresholds, derived here from the same energy budget. The stopping point differs by well under a pixel. |\n| `withDecay` config validation | Throws at the `withDecay()` call rather than on the animation's first frame. Same errors (`clamp` shape, `velocityFactor > 0`, `rubberBandEffect` needing a `clamp`), one line earlier. |\n| `ReduceMotion`, `useReducedMotion` | The enum is mirrored and every value behaves as `Never`; `useReducedMotion()` is always false. GNOME's `gtk-enable-animations` is not read yet. |\n| `reanimatedVersion` | The upstream version this surface mirrors, not a claim to be that package. |\n| `LinearTransition` size changes | The position animates (as a translation); a width or height change lands immediately. A layout transition animates a rect the ENGINE produced, so it has no style to pin a subtree to — the carve-out `useAnimatedStyle` has for a driven `width`/`height` does not reach it. |\n| Layout-animation properties | `opacity`, `transform` and position. `width`/`height` are applied rather than driven (above); anything else a builder asks for is named once, by property, in a warning. |\n| Builder methods | `.restDisplacementThreshold()` and `.restSpeedThreshold()` are accepted and ignored — this platform's spring derives its rest condition from the same energy budget instead (see the row above). `.reduceMotion()` is accepted and ignored for the reason `useReducedMotion()` is always false. |\n| `entering` / `exiting` ownership | A layout animation owns `opacity` and `transform` for as long as it runs, so a `useAnimatedStyle` driving the same property on the same view during a fade is two writers on one slot. Upstream has the same rule. |\n| The layout-animation catalog | 60 of the 76 presets, all five `*Transition` builders, `Keyframe` and `LayoutAnimationConfig`. `Flip*` needs a 3D rotation and `LightSpeed*` a skew; both throw by name. `rotate` is carried as degrees rather than upstream's `'90deg'`/`'5rad'` strings — a numeric animation cannot carry a unit, and the matrix that reaches GTK is identical. A builder's own `.rotate()` and a `.withInitialValues()` angle still take either spelling. |\n| `CurvedTransition` size easings | `.easingWidth()` and `.easingHeight()` are accepted and ignored, for the reason in the `LinearTransition` row above: a size change lands immediately rather than being driven, so there is no curve to apply to it. The two position easings are honoured. |\n| `SharedTransition` | Throws. It needs three things that do not exist here: a `sharedTransitionTag` prop, an overlay above the navigation stack, and a retention that REPARENTS the leaving widget rather than holding it in place. Upstream's own web path does not implement it either. |\n| `getAnimatedStyle`, `setUpTests` | Throw. Upstream returns the style object its updater produced, which exists on mobile because its Jest path mirrors it onto the component; here a style is taken apart at bind time — opacity to the widget, colours to a private CSS provider, the whole `transform` array folded into one matrix — so there is no such object to return. Assert the widget instead (`getOpacity()`, `computeBounds()`), which is what every GTK test in this repo does and is strictly stronger. |",
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
doc: "docs/api.md",
|
|
899
|
+
heading: "Not implemented — throws, naming itself",
|
|
900
|
+
text: "`Animated.FlatList`; sixteen of the seventy-six preset layout-animation\nbuilders — the twelve `FlipIn*`/`FlipOut*` (a 3D rotation with a perspective)\nand the four `LightSpeed*` (a skew), both of which need a transform this\nplatform's 2D matrix has no room for;\n`processColor` and `DynamicColorIOS`; `useComposedEventHandler`,\n`useFrameCallback`, `useTimestamp`; sensors, screen and shared-element\ntransitions; Reanimated 4's CSS animations (`css.create`, `css.keyframes`);\n`defineAnimation`; `createWorkletRuntime` and `runOnRuntime` (see the worklets\nsection below); `getAnimatedStyle` and `setUpTests` (the other three test\nhelpers are implemented).\n\n**Why `Animated.FlatList` is a decision and not an omission.** It is the one\nanimated component that is refused, because it is a _composite_ rather than a\nhost component: `FlatList` renders the windowed core, which renders a\n`ScrollView`, which is the only thing in that chain that owns a widget — and\n`FlatListHandle` is a scroll API by contract, so there is no handle to read a\nwidget back out of. Giving it one would mean publishing the scrolled window\nthrough two layers whose job is to hide it. Upstream's `Animated.FlatList`\nmostly exists so `onScroll` can be an `Animated.event` /\n`useAnimatedScrollHandler` — and the second of those IS implemented here, so\na plain `FlatList` already takes one on its `onScroll` and needs no animated\nwrapper for it. Put the animated style on an `Animated.View` around the list,\nor use `Animated.ScrollView` when the list does not need virtualization.\n\nThe throw is the point, and it is the same discipline as the RNGH shim: a\n`BounceIn` that mounted without bouncing is the trap\n[research/gestures.md](research/gestures.md) records `Animated.View` falling\ninto — compiled, ran, did nothing. The stand-ins fail on call, on render and\non property access (`BounceIn.duration(300)`, `css.create`), while still\nanswering the introspection React and `console.log` do first. A symbol not\nlisted at all fails earlier still, at bundle time.\n\n**`@gorhom/bottom-sheet`'s scroll lock runs**, and it took two fixes a layer\nbelow this surface to get there. Both halves of the lock were always here —\nthe `onScroll`/`onBeginDrag`/`onEndDrag`/`onMomentumEnd` handlers it registers,\nand the `scrollTo` they call to pin the list — and driving the sheet with a\nreal pointer showed it never executing, because the sheet's scrollable emitted\nno scroll event at all.\n\nThe first cause was the scrollable's own style, and the base style above is it:\n`<FlatList />` carrying no style of its own inside a BOUNDED parent now scrolls\n— `spike/core-exports` measures it beside its controls, row one moving 406 →\n278 where it used to report 170 → 170.\n\nThe second was that gorhom's parent was not bounded, and the diagnosis on file\nfor that one was **wrong**, which is worth keeping because it was wrong in an\ninstructive way. gorhom bounds the list with an animated `height`\n(`contentMaskContainerAnimatedStyle` in `BottomSheetContent`), and the blame\nwent to the driven-size carve-out: a size that lives as a rect-store override\nYoga never sees cannot bound a child. The probe said otherwise — the height\nnever became a **number**. `useAnimatedStyle` did not run animations returned\nfrom its updater, so the property arrived as a spring descriptor and the\ndriven-size path was never even asked. It runs them now, and a `height` this\nplatform refuses to drive at frame rate lands in Yoga through a React render\nwhen its animation settles and at most once per 100 ms while it runs — 42\nrenders against 294 animation frames in a measured run. The settle on its own\nshipped first and was not enough: gorhom's mask target is derived from the\nsheet's own position, so the opening spring re-aims it every frame and it never\nsettles, and the sheet stood empty for 1.38 s\n([research/animated-size.md §9 and §10](research/animated-size.md)).\n`spike/core-exports` reaches 0 FAILED: the list receives scroll events, the\nlock holds it at the top while the sheet is collapsed and releases when it is\nextended.\n\n**`@gorhom/bottom-sheet` and `react-native-draggable-flatlist` both run now**,\nand this surface is one of the three they needed: the other two are\n[`react-native-gesture-handler`](#react-native-gesture-handler-react-native-gtkxgesture-handler)\nand four `react-native` core exports. What each of them was actually stopped\nby — and how that was established — is in\n[the section that measured it](#the-two-libraries-this-surface-was-measured-against-run).",
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
doc: "docs/api.md",
|
|
904
|
+
heading: "`react-native-worklets` (`react-native-gtkx/worklets`)",
|
|
905
|
+
text: "Reanimated 4 moved the worklet surface out of Reanimated and into its own\npackage, and libraries import it under that name. Aliasing\n`react-native-reanimated` alone therefore left the import wall standing one\npackage over — and it is an **import**-time wall, not a runtime one:\n`react-native-reanimated-dnd` 2.0.0 pulls `scheduleOnRN` and `scheduleOnUI`\nout of `react-native-worklets` at module scope in five of its hooks\n(`useDraggable`, `useDroppable`, `useSortable`, `useHorizontalSortable`,\n`useGridSortable`) with no `try { require } catch` anywhere, so the module\nfails to load rather than failing where the function is used. Both presets\nalias the package name onto this subpath, so an app keeps its source.\n\nTwo measurements worth keeping, taken against the published packages rather\nthan their docs:\n\n- **`react-native-reanimated-dnd` 2.0.0 imports exactly two symbols from it**\n — `scheduleOnRN` and `scheduleOnUI`, both implemented here.\n- **`@gorhom/bottom-sheet` 5.2.14 imports none.** It reaches `runOnJS` and\n `runOnUI` through `react-native-reanimated`, and does not depend on\n `react-native-worklets` at all. What blocks it is measured in the\n `react-native-gesture-handler` section, and it is not this package.\n `react-native-gesture-handler` 3.1.0 does use this package\n (`scheduleOnUI`), but behind a `try { require } catch`, so it never had this\n failure mode.\n\nThe thread functions here and the ones `react-native-gtkx/reanimated` exports\nare the **same instance**, not two copies: jobs queued through either package\nname land in one batch, in order, exactly as upstream, where Reanimated\nre-exports them from this package.",
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
doc: "docs/api.md",
|
|
909
|
+
heading: "The boundary, and who drew it",
|
|
910
|
+
text: "What is implemented and what refuses is decided by **upstream's own\nnon-native build** — the `.ts` files it ships next to its `.native.ts` ones,\nwhich are what react-native-windows and the web run. Where that build\ncomputes something, so does this; where it throws, this refuses by name. That\nis the only boundary here with a source of truth, and it draws itself in the\nright place: a worklet runtime is a **second JS runtime**, and this platform\nhas one thread. Measured against `react-native-worklets` 0.11.3.\n\nOne thing that build does is deliberately **not** copied, and it is a timing\nrather than a boundary: its UI hop waits for a `requestAnimationFrame`. That\nis the web standing in for a UI runtime it has not got, and React Native — the\ncontract here — posts to a real thread that picks the job up without waiting\nfor a frame. See `docs/research/dnd-hover-flicker.md` for what the wait broke.\n\n| Export | Behaviour |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `runOnUI`, `scheduleOnUI`, `runOnJS`, `scheduleOnRN` | Deferred, not inlined, and returning `void` — the same functions `react-native-gtkx/reanimated` exports, so see that section's differences table. |\n| `runOnUIAsync` | Resolves with the worklet's return value when the UI hop runs it. The one thread API that hands anything back, because a promise crosses the deferral the others impose. |\n| `isWorkletFunction` | Upstream's `__workletHash` check. This platform never runs the Babel plugin, so nothing is a worklet and nothing needs to be — `'worklet'` is an inert string. |\n| `makeShareableCloneRecursive`, `createSerializable`, `makeShareable`, `makeShareableCloneOnUIRecursive`, `isSerializableRef`, `isShareableRef` | Identity, as upstream's own non-native serializer: a value never leaves the runtime it was made in, so there is nothing to clone. |\n| `serializableMappingCache`, `shareableMappingCache`, `registerCustomSerializable`, `callMicrotasks` | No-ops, as upstream. |\n| `isShareable`, `isSynchronizable` | Upstream's structural checks, ported unchanged. |\n| `RuntimeKind`, `getRuntimeKind`, `isRNRuntime`, `isUIRuntime`, `isWorkerRuntime`, `isWorkletRuntime`, `UIRuntimeId` | Answer for the one runtime there is: `ReactNative`. Upstream's non-native path reports the same, because its initializer sets that kind and nothing ever changes it. |\n| `getStaticFeatureFlag`, `getDynamicFeatureFlag`, `setDynamicFeatureFlag`, `isBundleModeEnabled`, `toggleSlowAnimationsOnUIRuntime` | `false` and no-ops. These gate upstream's native experiments and its Babel bundle mode, none of which exist here. |\n| `createWorkletRuntime`, `runOnRuntime`, `runOnRuntimeSync`/`Async`(`WithId`), `scheduleOnRuntime`(`WithId`), `getUIRuntimeHolder`, `getUISchedulerHolder` | **Throw**, naming themselves. A second runtime is structural, and upstream's own `runtimes.ts` throws for every one of these on any single-runtime build. |\n| `runOnUISync`, `executeOnUIRuntimeSync` | **Throw.** Both are \"run it over there and give me the answer now\". Deferring instead would be worse than refusing: the caller wants the return value, and a deferred call has none. |\n| `createShareable`, `createSynchronizable` | **Throw** — memory shared between runtimes. |\n| `WorkletsModule` | **Throws**, naming itself. The one deliberate deviation from the mirror rule: upstream's non-native build exports it as `null`, which fails naming nothing. |\n\nA symbol not listed at all fails earlier still, at bundle time, with the\nbundler's own \"no export named X\".",
|
|
911
|
+
},
|
|
748
912
|
{
|
|
749
913
|
doc: "docs/platform-layer.md",
|
|
750
914
|
heading: "Why you would reach for it",
|
|
@@ -758,12 +922,12 @@ export const DOC_CHUNKS = [
|
|
|
758
922
|
{
|
|
759
923
|
doc: "docs/platform-layer.md",
|
|
760
924
|
heading: "React Native content inside GTK slots",
|
|
761
|
-
text: "| Export | Sizing | Use for |\n| ------------------ | ---------------------------- | ------------------------------------------------ |\n| `SlotContent` | fills the slot | a page body, a pane, a dialog body |\n| `IntrinsicContent` | sized by its own Yoga layout | an AdwHeaderBar slot, a toolbar area, a list row |\n\n`createSidebarNavigator`'s `sidebarRow` screen option (docs/api.md) wraps\nits content in exactly `IntrinsicContent` for this reason — a row is sized\nby what it holds, not stretched to fill the list.\n\n**Every content area inside a widget needs one of them.** A widget hands out\nrectangles two ways: as ordinary CHILDREN (a content area) and as SLOTS —\nproperties that take a widget, `topBar={…}`, `titleWidget={…}`, `sheet={…}`.\nWhich way a given area arrives is gtkx's business and moves between releases\n(rc.3 took the `content`/`child` props off single-child widgets and made that\ncontent a child), and it has never had anything to do with layout. Both are\nGTK's territory: the layout root is cleared on the way in, so a widget lands\nbare (what `WidgetContent` does by hand) and React Native content has to bring\nits own root.\n\n```tsx\n<AdwBottomSheet\n style={{ flex: 1 }}\n sheet={\n <IntrinsicContent>\n <View style={{ padding: 20, gap: 10 }}>…</View>\n </IntrinsicContent>\n }\n bottomBar={\n <IntrinsicContent>\n <View style={{ flexDirection: \"row\", gap: 8 }}>…</View>\n </IntrinsicContent>\n }\n>\n {/* the content area — a child under rc.3, and just as much a boundary */}\n <SlotContent>\n <View style={{ flex: 1, justifyContent: \"center\" }}>…</View>\n </SlotContent>\n</AdwBottomSheet>\n```\n\nForget the wrapper and you get an error naming the widget and where the\ncontent landed, not a wrong-looking window: without a root, content inside a\nwidget would join the ENCLOSING Yoga tree — laid out against the window's\nviewport while GTK hands it the widget's own rectangle.\
|
|
925
|
+
text: "| Export | Sizing | Use for |\n| ------------------ | ---------------------------- | ------------------------------------------------ |\n| `SlotContent` | fills the slot | a page body, a pane, a dialog body |\n| `IntrinsicContent` | sized by its own Yoga layout | an AdwHeaderBar slot, a toolbar area, a list row |\n\n`createSidebarNavigator`'s `sidebarRow` screen option (docs/api.md) wraps\nits content in exactly `IntrinsicContent` for this reason — a row is sized\nby what it holds, not stretched to fill the list.\n\n**Every content area inside a widget needs one of them.** A widget hands out\nrectangles two ways: as ordinary CHILDREN (a content area) and as SLOTS —\nproperties that take a widget, `topBar={…}`, `titleWidget={…}`, `sheet={…}`.\nWhich way a given area arrives is gtkx's business and moves between releases\n(rc.3 took the `content`/`child` props off single-child widgets and made that\ncontent a child), and it has never had anything to do with layout. Both are\nGTK's territory: the layout root is cleared on the way in, so a widget lands\nbare (what `WidgetContent` does by hand) and React Native content has to bring\nits own root.\n\n```tsx\n<AdwBottomSheet\n style={{ flex: 1 }}\n sheet={\n <IntrinsicContent>\n <View style={{ padding: 20, gap: 10 }}>…</View>\n </IntrinsicContent>\n }\n bottomBar={\n <IntrinsicContent>\n <View style={{ flexDirection: \"row\", gap: 8 }}>…</View>\n </IntrinsicContent>\n }\n>\n {/* the content area — a child under rc.3, and just as much a boundary */}\n <SlotContent>\n <View style={{ flex: 1, justifyContent: \"center\" }}>…</View>\n </SlotContent>\n</AdwBottomSheet>\n```\n\nForget the wrapper and you get an error naming the widget and where the\ncontent landed, not a wrong-looking window: without a root, content inside a\nwidget would join the ENCLOSING Yoga tree — laid out against the window's\nviewport while GTK hands it the widget's own rectangle.\nThe gallery's \"Widget hosting\" section is that whole story in one screen.\n\nWhich of the two is yours to choose, and the platform deliberately does not\nguess: `AdwBottomSheet` alone FILLS in its content child but HUGS in both\n`sheet` and `bottomBar`. One widget, three content areas, two answers, with\nnothing in the name or the GIR type to tell them apart — the answer lives in\nthe widget's own layout code. Swapping them is visible immediately:\n`SlotContent` in a bottom bar collapses it to nothing (a filling root reports\na zero minimum, so a size-to-content area is told \"nothing\"), and\n`IntrinsicContent` around a content area leaves `flex: 1` with no viewport to\nfill.\n\nNote the two independent sizes here. `style={{ flex: 1 }}` on the widget is\nthe WIDGET's size in the surrounding React Native layout (a wrapped widget is\na Yoga leaf at its natural size until the style says otherwise); the wrapper\ninside each content area is the CONTENT's size inside the rectangle that\nwidget then hands out.",
|
|
762
926
|
},
|
|
763
927
|
{
|
|
764
928
|
doc: "docs/platform-layer.md",
|
|
765
929
|
heading: "Adwaita chrome, written in React Native",
|
|
766
|
-
text: "The opposite direction from everything above: these create no widget an app\ncould not have created itself, because what they encode is a **look**, not a\nbehaviour
|
|
930
|
+
text: "The opposite direction from everything above: these create no widget an app\ncould not have created itself, because what they encode is a **look**, not a\nbehaviour.\n\n| Export | What it is |\n| ------ | -------------------------------------------- |\n| `Icon` | a **named** icon from the desktop icon theme |\n\n`Icon` is not `Image`: RN's `Image` takes a file path or URI, because on iOS\nand Android an icon is a bundled asset. Here it is a _name_ resolved against\nthe current icon theme at paint time, which recolours itself with the label\ncolour and follows the user's theme — nothing in `Image`'s contract can\nexpress that. The shape is the one RN apps already use\n(`<Icon name size />`), with the desktop icon theme behind it instead of a\nbundled font.\n\n#### `List`/`ListRow`/`ListSeparator` were here, and are not any more\n\nThey were Adwaita's `.boxed-list` re-implemented in React Native — the frame,\nthe separators, the corner radii, both tints — plus an id-keyed\ndrag-to-reorder. Both halves are gone, for two separate reasons, and the\nreasoning is here rather than in a changelog because it is the general rule\nfor what belongs in this subpath.\n\n**The components.** The argument for shipping them was that a screen shared\nwith iOS and Android could not import `react-native-gtkx/adw`. That argument\ndoes not survive contact with the resolver: **`react-native-gtkx/common` does\nnot resolve on iOS or Android either.** Either import needs a `.linux.tsx`\nsplit or a `Platform` check, so `List` bought a shared screen nothing over\n`AdwActionRow` — while costing a hand-maintained copy of libadwaita's own\nmetrics that drifts every time libadwaita moves. And the widget is better\nwhere it works: GTK's real keynav, focus and accessibility, with the numbers\ncoming from the system theme instead of from our source. They had exactly one\nconsumer in this repo.\n\nSo:\n\n- **want a native list** → `AdwActionRow`, `AdwEntryRow` and friends from\n [`react-native-gtkx/adw`](api.md), in a `GtkListBox` with\n `cssClasses={[\"boxed-list\"]}` — see the gallery's \"Adwaita stack\" section;\n- **want that look written in React Native** → copy\n [`examples/tasks-nav/src/components/list.tsx`](../examples/tasks-nav/src/components/list.tsx).\n It is 200 lines of `View`, `Pressable`, `Text` and `StyleSheet` with the\n measurements in comments, and it is meant to be copied.\n\nWhat survives from the change that introduced them (#47) is the part that\nmattered, and it is still platform surface: **`boxShadow`, `outline*` and\n`textDecorationLine` in the style layer**. Those are what make an\nAdwaita-looking list expressible in `StyleSheet` at all — the frame is a\nthree-part `box-shadow` rather than a border, and the focus ring is an\n`outline`, which takes no layout space. The finding was the style props, not\nthe components built on them.\n\n**The reorder.** `onReorder`/`reorderId` was a second, id-keyed entry point\ninto the same module `Draggable` and `Sortable` come from — two ways to drag,\none of them shaped like nothing an RN developer had seen. There is now one:\n[`react-native-gtkx/dnd`](api.md#drag-and-drop-react-native-gtkxdnd), which\nmirrors `react-native-reanimated-dnd`. An id-keyed reorder is a `Droppable`\naround a `Draggable` per row inside one `DropProvider`;\n`examples/tasks-nav/src/components/task-row.tsx` is that, and says in a\ncomment what it costs against the two lines it replaced.",
|
|
767
931
|
},
|
|
768
932
|
{
|
|
769
933
|
doc: "docs/platform-layer.md",
|
|
@@ -790,6 +954,11 @@ export const DOC_CHUNKS = [
|
|
|
790
954
|
heading: "Actions and shortcuts declared in the app tree",
|
|
791
955
|
text: '`WindowActions`, `ApplicationActions` and `WindowControllers` register their\nchildren on the window (or the application) **from wherever they are written\nin the app tree**. They render nothing where they sit — they are portals in\nReact\'s own sense: the children stay part of the tree at that position, with\nthe context, state and effects they would have there, while the registration\nlands on the window.\n\n```tsx\nimport { GSimpleAction, WindowActions } from "react-native-gtkx/gtk"\n\nconst NewTaskAction = () => {\n const { addTask } = useStore() // an ordinary React context store\n return (\n <WindowActions>\n <GSimpleAction\n name="new"\n onActivate={() => addTask()}\n />\n </WindowActions>\n )\n}\n```\n\nThat is `win.new` — what a HeaderBar button\'s `actionName`, a `GMenu` item\nand an `actionAccels` entry all target. `ApplicationActions` is the same\ncomponent against the application\'s action map (`app.*`), and the two are\nNOT interchangeable: a `Gio.Notification`\'s action button can only ever\nactivate an application action, and an application action outlives any one\nwindow. Which prefix you get is decided by which component you write.\n`WindowControllers` takes `Gtk.EventController` children — a\n`GtkShortcutController` with `scope={Gtk.ShortcutScope.GLOBAL}` is the whole\nreason it exists.\n\n**Reach for these, not for `runApplication`\'s `applicationActions` /\n`windowActions` / `windowControllers` options.** Those options build their\nchildren as props of the window `AppRegistry` creates, which makes them\nSIBLINGS of the app tree: no provider inside the app is above them, so an\naction declared there cannot read a React context — `examples/tasks-nav` had\nto rewrite its store as a module-level external store before Ctrl+N could\nsee any app state at all. The options still work and are not going away\nwithout notice, but they are deprecated, and everything they can express the\ncomponents can express better:\n\n- **context works**, because the declaration is a descendant of its provider;\n- **registration is dynamic** — the action is added when the declaring\n component mounts and removed when it unmounts, so one screen can own its\n own actions instead of the process owning all of them for its lifetime;\n- **it composes** — two unrelated subtrees each declare their own without\n meeting in a single options object.\n\n`actionAccels` is NOT deprecated and stays a `runApplication` option: it is a\nflat name→keys table with no children and nothing to read from context, and\nit is deliberately process-wide (an accelerator naming an action that is not\nregistered right now simply does nothing). A shortcut that should come and\ngo with a screen is a `GtkShortcutController` inside `<WindowControllers>`.\n\n**Two components, not one, on purpose.** Actions land on the window as a\n`Gio.ActionMap` (`addAction`/`removeAction`, keyed by NAME); controllers land\non it as a `Gtk.Widget` (`addController`/`removeController`, keyed by the\ncontroller object). Different children, different GObject interfaces, and —\nsee below — different duplicate semantics. One component sorting its children\nby type would fail silently on a wrong child; two fail at the type level.\n\n**A duplicated action name goes to the FIRST declaration**, and a second one\nis ignored with a development warning naming it. This is not a coin toss\nbetween first and last. `Gio.ActionMap` is name-keyed at both ends:\n`addAction` silently replaces a same-named action, and `removeAction` takes a\nname, not the action object. Under "last wins", the first of the two\ndeclarations to unmount would remove whatever currently answers to that\nname — leaving the other one mounted but dead. First-wins is the only order\nin which release always precedes acquire: the loser never registers, and when\nthe winner unmounts (removing its own action, correctly) the claim passes to\nthe next declaration still mounted, which registers in a later commit. If you\nwant a screen to override a shortcut, give it its own name, or move the\ndeclaration somewhere both screens can reach.\n\n**Inside a `Modal`, the enclosing window is the modal\'s own window**, so\nactions and controllers declared there belong to it and go away with it —\nusually what a dialog wants, and worth knowing when it is not. Under\n`chrome: "content"` and inside the navigators nothing changes: the window is\nstill the one `AppRegistry` built, the navigators own widgets inside it and\nnot its action map, and a `HeaderBar` button in a page resolves `win.*`\nthrough the widget hierarchy up to that same window. One consequence worth\nknowing: react-navigation keeps a popped screen mounted until its exit\ntransition ends, so a screen\'s actions outlive the pop by the length of the\nanimation.',
|
|
792
956
|
},
|
|
957
|
+
{
|
|
958
|
+
doc: "docs/platform-layer.md",
|
|
959
|
+
heading: "`Controllers` — a GTK event controller on a React Native component",
|
|
960
|
+
text: "The same idea one level down. `<Controllers>` attaches its children to the\nwidget of the **enclosing React Native component** — `View`, `Pressable`,\n`ScrollView`, `Animated.View`, any of them:\n\n```tsx\n<Pressable onPress={open}>\n <Controllers>\n <GtkDragSource\n actions={Gdk.DragAction.MOVE}\n onPrepare={(x, y, self) =>\n Gdk.ContentProvider.newForValue(\n GObject.buildValue(GObject.TYPE_STRING, (v) => v.setString(id)),\n )\n }\n />\n </Controllers>\n <Text>{title}</Text>\n</Pressable>\n```\n\n**Why it exists.** A `Pressable`'s `ref` is a `ViewHandle` —\n`measure`/`measureInWindow`/`measureLayout` — and that is correct: React\nNative's contract says nothing about widgets, and an app reaching through a\nref to a `Gtk.Widget` would pin every internal of this platform as public\nAPI. But GTK carries behaviour no style and no RN prop expresses,\ndrag-and-drop above all, and before this there was simply no way to reach it\nfrom a row written in React Native. `examples/tasks-nav`'s rows could not be\nrewritten because of it (see\n[the showcase research](research/react-native-first-showcase.md)).\n\n**Why a component here and not a `controllers` prop on `View`.** A prop\nwould have been three lines, and it would sit on the two components an app\nshares with iOS and Android, imported from the _portable_ entry point: the\nfile would still compile everywhere, the prop would be ignored off Linux,\nand the feature would vanish with no diagnostic. On this platform **the\nimport is the signal** — `react-native-gtkx/gtk` is the line an app knows it\nis crossing. An element is also something an RN developer already knows how\nto put behind a `Platform.OS` check or a `.linux.tsx` split, and its absence\nis visible in the tree.\n\nTwo properties follow from it being a portal, and both matter:\n\n- **it composes with context** — the handler that reorders a list is written\n where that list's state already is;\n- **it is lifecycle-bound** — attached on mount, removed on unmount, so a\n screen's controllers leave with the screen.\n\nOne caveat, stated rather than hidden: the controllers attach **one commit\nafter mount**. React attaches host refs bottom-up, so the enclosing view's\nwidget does not exist yet when a child's layout effects run. For an event\ncontroller this is unobservable — no pointer reaches a widget in its first\nframe — and it is the same delay `WindowActions` has. It does mean a test\nthat aims a synthetic pointer at a fresh tree has to let one commit land.\n\nInside a GTK widget's own slot there is no enclosing React Native component\nand nothing is attached; pass `controllers={…}` to the widget itself there,\nwhich is the prop this substitutes for.\n\nFor drag-and-drop specifically there is a whole module above this:\n[`react-native-gtkx/dnd`](api.md#drag-and-drop-react-native-gtkxdnd) mirrors\n`react-native-reanimated-dnd`'s API (`Draggable`, `Droppable`,\n`DropProvider`, `Sortable`) on these two controllers, and both bundler\npresets alias the package name onto it — and `react-native-gesture-handler`\nonto a shim that keeps `GestureHandlerRootView` working — so a ported app\nkeeps its source unchanged.\nAll of it is written on top of `Controllers`, not around it — which is the\nproperty that makes this subpath worth having.",
|
|
961
|
+
},
|
|
793
962
|
{
|
|
794
963
|
doc: "docs/platform-layer.md",
|
|
795
964
|
heading: "GSettings",
|
|
@@ -808,7 +977,7 @@ export const DOC_CHUNKS = [
|
|
|
808
977
|
{
|
|
809
978
|
doc: "docs/platform-layer.md",
|
|
810
979
|
heading: "Navigation without a router",
|
|
811
|
-
text: 'The stack is an array of tags. Change the array, the widget animates.\n\n```tsx\nimport { useState } from "react"\nimport { Pressable, Text, View } from "react-native"\nimport { AdwHeaderBar, AdwToolbarView } from "react-native-gtkx/adw"\nimport {\n NavigationStack,\n NavigationStackPage,\n SlotContent,\n} from "react-native-gtkx/common"\n\nconst App = () => {\n const [stack, setStack] = useState(["home"])\n\n return (\n <NavigationStack\n stack={stack}\n // The Adwaita back button, Escape, the back gesture and the\n // back-history menu all arrive here. Follow them in your own state.\n onPopped={(tag) => setStack((s) => s.filter((entry) => entry !== tag))}\n >\n <NavigationStackPage\n tag="home"\n title="Home"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <Pressable onPress={() => setStack((s) => [...s, "detail"])}>\n <Text>Open detail</Text>\n </Pressable>\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n\n <NavigationStackPage\n tag="detail"\n title="Detail"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <View />\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n </NavigationStack>\n )\n}\n```\n\nA runnable version is
|
|
980
|
+
text: 'The stack is an array of tags. Change the array, the widget animates.\n\n```tsx\nimport { useState } from "react"\nimport { Pressable, Text, View } from "react-native"\nimport { AdwHeaderBar, AdwToolbarView } from "react-native-gtkx/adw"\nimport {\n NavigationStack,\n NavigationStackPage,\n SlotContent,\n} from "react-native-gtkx/common"\n\nconst App = () => {\n const [stack, setStack] = useState(["home"])\n\n return (\n <NavigationStack\n stack={stack}\n // The Adwaita back button, Escape, the back gesture and the\n // back-history menu all arrive here. Follow them in your own state.\n onPopped={(tag) => setStack((s) => s.filter((entry) => entry !== tag))}\n >\n <NavigationStackPage\n tag="home"\n title="Home"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <Pressable onPress={() => setStack((s) => [...s, "detail"])}>\n <Text>Open detail</Text>\n </Pressable>\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n\n <NavigationStackPage\n tag="detail"\n title="Detail"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <View />\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n </NavigationStack>\n )\n}\n```\n\nA runnable version is the gallery\'s "Adwaita stack" section — three levels\ndeep, with React Native content in the header bar and a raw `GtkButton` beside\nit. Note that it wraps the `NavigationStack` in a `Widget`: the component\nrenders a RAW `Adw.NavigationView`, which has no Yoga node of its own, so\nnesting it inside a React Native layout root needs the measured leaf `Widget`\nprovides. As an app\'s root, where GTK allocates it directly, that is not\nneeded.',
|
|
812
981
|
},
|
|
813
982
|
{
|
|
814
983
|
doc: "docs/platform-layer.md",
|
|
@@ -880,6 +1049,11 @@ export const DOC_CHUNKS = [
|
|
|
880
1049
|
heading: "Metro or vite?",
|
|
881
1050
|
text: "- **Adding Linux to an existing RN app** (ios/android + Metro): the\n section above — standard RN toolchain end to end,\n `run-linux --dev` for Fast Refresh.\n- **Linux-first project**: the template with the vite preset\n (`react-native-gtkx/vite`; `gtkx dev` gives Fast Refresh, builds are\n single-file bundles). Both paths consume the same published package.",
|
|
882
1051
|
},
|
|
1052
|
+
{
|
|
1053
|
+
doc: "docs/getting-started.md",
|
|
1054
|
+
heading: "The React Compiler is on by default (vite path)",
|
|
1055
|
+
text: '`gtkx dev` and `gtkx build` run the\n[React Compiler](https://react.dev/learn/react-compiler) over every source\nfile in the project — never `node_modules`. Nothing switches it on; it is on\nunless `gtkx.config.ts` switches it off:\n\n```ts\nexport default defineConfig({\n libraries: ["Gtk-4.0", "Adw-1"],\n applicationId: "com.example.myapp",\n reactCompiler: false,\n})\n```\n\nLeaving the option out and setting it to `true` mean the same thing — only an\nexplicit `false` disables it. The Metro path (`run-linux` / `build-linux`)\nkeeps the app\'s stock Babel preset and does not run the compiler at all.\n\n**If a ported app misbehaves in a way that smells like stale rendering, set\n`reactCompiler: false` and see whether the symptom goes away.** One line, and\nit tells you which half of the system to debug — worth knowing about up\nfront, because the symptom does not look like a compiler.\n\nThe expensive version of this: a component that reads mutable module-level\nstate during render has that read memoised. A render counter built on\n`readCounter("loop")` — a function taking no reactive input — is computed\nonce, and the JSX built from it is reused forever. The component re-renders\nfourteen times and shows the mount value every time. It looks exactly like a\nbroken counter, and it is not: it is a working counter behind a cached\nrender.\n\nOn React Native the compiler is opt-in, so an app that does not follow the\n[Rules of React](https://react.dev/reference/rules) still works. Here it is\non, so the same violations become visible misbehaviour — and on a platform\nwhere everything else is new too, that reads as _our_ bug. Flipping\n`reactCompiler` to `false` settles it. Once it is settled the fix is the\nRules-of-React one (move the read into state, a ref or a hook), not leaving\nthe compiler off.\n\nReanimated shared values have their own spelling for the same reason:\n`sharedValue.value = x` and `sharedValue.set(x)` both work here, but only\n`.get()`/`.set()` passes compiler-aware lint — see\n[api.md](api.md#writing-a-shared-value-value-or-set).',
|
|
1056
|
+
},
|
|
883
1057
|
{
|
|
884
1058
|
doc: "docs/getting-started.md",
|
|
885
1059
|
heading: "Shipping an app",
|
|
@@ -893,12 +1067,12 @@ export const DOC_CHUNKS = [
|
|
|
893
1067
|
{
|
|
894
1068
|
doc: "docs/getting-started.md",
|
|
895
1069
|
heading: "Examples in the repository",
|
|
896
|
-
text: "- `examples/profile` — a static layout; the same source also builds with react-native-web (`examples/profile-web`);\n- `examples/
|
|
1070
|
+
text: "- `examples/profile` — a static layout; the same source also builds with react-native-web (`examples/profile-web`);\n- `examples/gallery` — the entire v1 surface, one capability per sidebar section: views, text and layout; inputs, buttons and toggles; lists, media and SVG; Animated, interpolate and transforms; the three Reanimated sections; the three gesture sections; drag-and-drop, real upstream libraries, widget hosting, the Adwaita stack, Modal and the imperative APIs;\n- `examples/rn-app` — a cli-init React Native app with ios + android + linux;\n- `examples/hn-app` — a Hacker News reader on the Metro path: live API data over Node fetch, state-based two-screen navigation, a lazily loaded comment tree.",
|
|
897
1071
|
},
|
|
898
1072
|
{
|
|
899
1073
|
doc: "docs/getting-started.md",
|
|
900
1074
|
heading: "Tests",
|
|
901
|
-
text: 'Unit logic is plain vitest — no special setup, runs anywhere. Component\ntests render real GTK widgets under a headless Wayland compositor, and\nreact-native-gtkx ships the whole recipe as two subpaths so a consumer app\ndoes not have to rediscover it:\n\n- `react-native-gtkx/vitest` — `reactNativeGtkxTest()`, a ready Vitest\n project config: the headless-compositor plugin, the `react-native` alias\n and Metro-style platform extensions, an inline-deps default for RN\n libraries that import `react-native` themselves (`@react-navigation`),\n and the React act-environment setup;\n- `react-native-gtkx/testing` — re-exports `@gtkx/testing`\'s\n render/screen/userEvent/fireEvent surface (already RN-shaped: `getByText`\n finds a `Text`, `userEvent.click` walks up to a `Pressable`\'s gesture\n controller — no wrapper needed) plus `renderHookWithWindow`, for hooks\n that read the active window (`useWindowDimensions` and similar) —\n `renderHook` alone mounts into a windowless container.\n\nMinimal `vitest.config.ts`:\n\n```ts\nimport { reactNativeGtkxTest } from "react-native-gtkx/vitest"\nimport { defineConfig } from "vitest/config"\n\nexport default defineConfig(reactNativeGtkxTest())\n```\n\nThe default test glob is `**/*.gtk.test.{ts,tsx}`; override `include` (and\n`name`, `headless`, `platform`, `inlineDeps`, `setupFiles`,\n`fileParallelism`) through `reactNativeGtkxTest`\'s options. For a project\nthat also has portable unit tests, use the result as one entry of\n`test.projects` instead of the whole config — `vitest.config.ts` at this\nrepo\'s root is the reference (`process.platform === "linux"` guards the\ngtk project so `npm test` still works on a non-Linux dev machine, running\nonly the unit project there).\n\n```tsx\nimport { Root } from "react-native"\nimport { render, screen } from "react-native-gtkx/testing"\nimport { expect, it } from "vitest"\nimport { App } from "../src/App"\n\nit("renders the greeting", async () => {\n // react-native-gtkx components need a layout root — AppRegistry.runApplication()\n // in the real app, <Root> in a test.\n await render(\n <Root\n width={800}\n height={600}\n >\n <App />\n </Root>,\n )\n expect(screen.getByText("Hello, GNOME!")).toBeTruthy()\n})\n```\n\nRequirements: a headless Wayland compositor and D-Bus on PATH — the same\nsystem packages CI installs, `sway xwayland dbus` (Ubuntu:\n`apt install sway xwayland dbus`). A missing compositor fails a test run\nwith a readable error (`Cannot find the "sway" executable on PATH`) rather\nthan hanging. `gtkx codegen` must already have generated the project\'s\n`@gtkx/gi` bindings before the first test run — a bare `vitest run` does\nnot trigger codegen itself, unlike `gtkx dev`/`gtkx build`; the template\'s\nown `package.json` wires this as a `pretest` script.\n\n`packages/react-native-gtkx/tests/gtk/` is this repo\'s own suite, built on\nthe same `@gtkx/testing` surface directly (it tests source, not the\npublished package) — a good place to see more query and `userEvent`\npatterns in context. Query roles with `Gtk.AccessibleRole` enums (see\ndocs/gtkx-
|
|
1075
|
+
text: 'Unit logic is plain vitest — no special setup, runs anywhere. Component\ntests render real GTK widgets under a headless Wayland compositor, and\nreact-native-gtkx ships the whole recipe as two subpaths so a consumer app\ndoes not have to rediscover it:\n\n- `react-native-gtkx/vitest` — `reactNativeGtkxTest()`, a ready Vitest\n project config: the headless-compositor plugin, the `react-native` alias\n and Metro-style platform extensions, an inline-deps default for RN\n libraries that import `react-native` themselves (`@react-navigation`),\n and the React act-environment setup;\n- `react-native-gtkx/testing` — re-exports `@gtkx/testing`\'s\n render/screen/userEvent/fireEvent surface (already RN-shaped: `getByText`\n finds a `Text`, `userEvent.click` walks up to a `Pressable`\'s gesture\n controller — no wrapper needed) plus `renderHookWithWindow`, for hooks\n that read the active window (`useWindowDimensions` and similar) —\n `renderHook` alone mounts into a windowless container.\n\nMinimal `vitest.config.ts`:\n\n```ts\nimport { reactNativeGtkxTest } from "react-native-gtkx/vitest"\nimport { defineConfig } from "vitest/config"\n\nexport default defineConfig(reactNativeGtkxTest())\n```\n\nThe default test glob is `**/*.gtk.test.{ts,tsx}`; override `include` (and\n`name`, `headless`, `platform`, `inlineDeps`, `setupFiles`,\n`fileParallelism`) through `reactNativeGtkxTest`\'s options. For a project\nthat also has portable unit tests, use the result as one entry of\n`test.projects` instead of the whole config — `vitest.config.ts` at this\nrepo\'s root is the reference (`process.platform === "linux"` guards the\ngtk project so `npm test` still works on a non-Linux dev machine, running\nonly the unit project there).\n\n```tsx\nimport { Root } from "react-native"\nimport { render, screen } from "react-native-gtkx/testing"\nimport { expect, it } from "vitest"\nimport { App } from "../src/App"\n\nit("renders the greeting", async () => {\n // react-native-gtkx components need a layout root — AppRegistry.runApplication()\n // in the real app, <Root> in a test.\n await render(\n <Root\n width={800}\n height={600}\n >\n <App />\n </Root>,\n )\n expect(screen.getByText("Hello, GNOME!")).toBeTruthy()\n})\n```\n\nRequirements: a headless Wayland compositor and D-Bus on PATH — the same\nsystem packages CI installs, `sway xwayland dbus` (Ubuntu:\n`apt install sway xwayland dbus`). A missing compositor fails a test run\nwith a readable error (`Cannot find the "sway" executable on PATH`) rather\nthan hanging. `gtkx codegen` must already have generated the project\'s\n`@gtkx/gi` bindings before the first test run — a bare `vitest run` does\nnot trigger codegen itself, unlike `gtkx dev`/`gtkx build`; the template\'s\nown `package.json` wires this as a `pretest` script.\n\n`packages/react-native-gtkx/tests/gtk/` is this repo\'s own suite, built on\nthe same `@gtkx/testing` surface directly (it tests source, not the\npublished package) — a good place to see more query and `userEvent`\npatterns in context. Query roles with `Gtk.AccessibleRole` enums (see\ndocs/gtkx-rc4-notes.md for the live workarounds still baked into that\nrecipe).',
|
|
902
1076
|
},
|
|
903
1077
|
{
|
|
904
1078
|
doc: "docs/getting-started.md",
|
|
@@ -908,42 +1082,57 @@ export const DOC_CHUNKS = [
|
|
|
908
1082
|
{
|
|
909
1083
|
doc: "docs/getting-started.md",
|
|
910
1084
|
heading: "Next steps",
|
|
911
|
-
text: "- [docs/api.md](api.md) — the entire v1 surface and differences from RN;\n- [CONTRIBUTING.md](../CONTRIBUTING.md) — developing the library itself (from macOS — via the UTM VM);\n- [docs/gtkx-
|
|
1085
|
+
text: "- [docs/api.md](api.md) — the entire v1 surface and differences from RN;\n- [CONTRIBUTING.md](../CONTRIBUTING.md) — developing the library itself (from macOS — via the UTM VM);\n- [docs/gtkx-rc4-notes.md](gtkx-rc4-notes.md) — the gtkx rc.4 baseline: workarounds, what it fixed, quirks that stay.",
|
|
912
1086
|
},
|
|
913
1087
|
{
|
|
914
|
-
doc: "docs/gtkx-
|
|
1088
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
915
1089
|
heading: "Live workarounds",
|
|
916
|
-
text: "| Name | What rc.
|
|
1090
|
+
text: "| Name | What rc.4 does | Our workaround | Removal condition |\n| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| `use-signal-stale-handler` | `useSignal` still routes the handler through React's `useEffectEvent`; `react-reconciler@0.33.0` only refreshes it in `commitBeforeMutationEffects` for `case 0` (FunctionComponent) — `case 11` (ForwardRef) and `case 15` (SimpleMemoComponent) fall through unrefreshed, so any `useEffectEvent` in a `memo`/`forwardRef` component is pinned to its mount closure forever (our `ScrollView` is a `forwardRef` with the `useSignal` calls inside it) — a fetch-fed FlatList empties itself on the first scroll. **rc.4 did not bump React and did not change the path** — see below | `gtkx/bridge/use-signal.ts` re-pins the latest handler (insertion effect) and hands gtkx a stable wrapper; the bridge exports that hook, not gtkx's | A stable React 19.3 (React fixed the refresh on the 19.3 line) |\n| `runtime-dedupe` | Two bundled copies of the gtkx runtime still double-init GLib and abort; nothing guards against it. Reproduced on rc.4: `g_log_set_writer_func() called multiple times`, SIGABRT | `src/vite/index.ts` puts `resolve.dedupe` over `@gtkx/*` + `react` (+ `@react-navigation/*` for its context) into the preset every app inherits | Idempotent runtime init upstream, or an error that names the duplicate |\n| `prop-portal` | `createPortal(children, container, key?)` is byte-for-byte the rc.3 signature and can still only target a container's DEFAULT slot (\"children\"). Every other slot an object exposes declaratively — a window's `Gio.ActionMap` (`actions`), a widget's `controllers`, an `AdwApplicationWindow`'s `breakpoints` — is reached only by passing an element-valued PROP, which the reconciler routes through an internal `\"gtkx:prop\"` element. rc.4 exports that element from neither `@gtkx/react` nor `/internal`, and its `exports` map now refuses the deep path outright | `gtkx/bridge/slot-portal.ts` restates the `\"gtkx:prop\"` element name and wraps it in `createSlotPortal(children, target, slot)` — the one line that would move if gtkx renames it. `WindowActions`/`ApplicationActions`/`WindowControllers` are built on it | gtkx exports a slot-aware portal (or the prop element) from a public entry point |\n| `renderhook-no-window` | `renderHook` still mounts into a bare `Gtk.Box` — rc.4's `render-hook.js` is byte-identical to rc.3's, which was byte-identical to rc.2's — so window-dependent APIs have no toplevel to read | Hook tests create a window with `render()` first (`tests/gtk/apis/dimensions.test.tsx`); packaged for consumers as `renderHookWithWindow` (`react-native-gtkx/testing`) | `renderHook` mounts into the same harness window `render` uses |",
|
|
917
1091
|
},
|
|
918
1092
|
{
|
|
919
|
-
doc: "docs/gtkx-
|
|
1093
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
920
1094
|
heading: "`use-signal-stale-handler` is a decision upstream made, not an oversight",
|
|
921
|
-
text: "We shipped the fix as a PR (gtkx-org/gtkx#469) and it was **closed unmerged\non purpose**. @eugeniodepalo: _\"closing this in favour of waiting for\nupstream… Since React fixes this properly on 19.3 for all fiber tags, I'd\nrather take the version bump than carry a workaround I'd revert.\"_ So this\nrow does not move on any gtkx release — only a stable React 19.3 retires it,\nand the hazard is wider than `useSignal`: any hook built on `useEffectEvent`\ninherits it until then.",
|
|
1095
|
+
text: "We shipped the fix as a PR (gtkx-org/gtkx#469) and it was **closed unmerged\non purpose**. @eugeniodepalo: _\"closing this in favour of waiting for\nupstream… Since React fixes this properly on 19.3 for all fiber tags, I'd\nrather take the version bump than carry a workaround I'd revert.\"_ So this\nrow does not move on any gtkx release — only a stable React 19.3 retires it,\nand the hazard is wider than `useSignal`: any hook built on `useEffectEvent`\ninherits it until then.\n\nrc.4 was checked against that condition anyway, because the condition names a\nReact version and a release could satisfy it by bumping one: it does not.\n`@gtkx/react@1.0.0-rc.4` peers `react: ^19.2` and depends on\n`react-reconciler: ^0.33.0` — character-identical to rc.3 — and React's own\n`latest` is still 19.2.8, with 19.3.0 published only as canaries. rc.4 did\ntouch `useSignal`, but only to rename its options (`after`/`immediate` →\n`isAfter`/`isImmediate`); the body still calls `useEffectEvent`, and its\ndoc comment still says React fixes this on the 19.3 line.",
|
|
922
1096
|
},
|
|
923
1097
|
{
|
|
924
|
-
doc: "docs/gtkx-
|
|
925
|
-
heading: "
|
|
926
|
-
text:
|
|
1098
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
1099
|
+
heading: "How each was checked against rc.4",
|
|
1100
|
+
text: 'The rule is that a changelog entry is a claim and the removal condition is\nthe test, so each row was re-run on the real runtime rather than read about.\n\n- **`use-signal-stale-handler`** — `tests/gtk/bridge/use-signal-upstream.gtk.test.tsx`\n calls gtkx\'s own hook directly on a `memo` component; it is an `it.fails`\n guard that starts passing the day the defect is gone. On rc.4 it still\n fails, and it is the "1 expected fail" the whole suite reports.\n- **`runtime-dedupe`** — the first two attempts at a probe both said "no\n abort", and both were wrong, which is worth recording: Node caches a native\n addon by the resolved path of the `.node` FILE, so a second copy of the thin\n `@gtkx/native` JS wrapper shares one addon instance and one Rust static, and\n a second `init()` on it returns normally — on rc.3 exactly as on rc.4. The\n failure needs two DISTINCT `.node` files in one process (an app with its own\n `@gtkx/native-linux-*-gnu` plus a nested one under the library), each\n carrying its own `glib::log::WRITER_FUNC`. Built that way, rc.4 dies:\n `gtkx: GLib-ERROR: g_log_set_writer_func() called multiple times`, exit 134,\n core dumped. Neither half of the condition is met — the init is not\n idempotent, and while the error names the symbol it does not name the\n duplicate package, which is the part that would make it debuggable.\n- **`prop-portal`** — enumerated the real module objects on the runtime rather\n than reading the `.d.ts`. `@gtkx/react` exports exactly `createPortal`,\n `createRoot`, `quit`, `rootElement`, `useApplication`, `useBindSetting`,\n `useParentWindow`, `useProperty`, `useSetting`, `useSignal`;\n `@gtkx/react/internal` exports `applyWrite`, `createApplicationComponent`,\n `createElementComponent`, `createReconcilerRoot`, `createWindowComponent`,\n `getAccessibleMetadata`, `isRootElement`, `setReconcilerErrorHandler`,\n `useMergedRef`. No value in either is `"gtkx:prop"`, and there is no\n slot-aware portal. rc.4 also made the fallback worse rather than better:\n importing `@gtkx/react/dist/components/element.js` now fails with _"not\n exported under the conditions [node, development, import]"_, so restating\n the literal is the only route left. The literal itself did not move —\n `const Prop = "gtkx:prop"` is unchanged in rc.4\'s `components/element.tsx`\n (only a doc comment above it was deleted), which the passing\n `WindowActions`/`WindowControllers`/breakpoint suites confirm functionally.\n- **`renderhook-no-window`** — `RenderHookOptions` still carries only\n `wrapper` and `initialProps`, no `container`, and on the runtime\n `renderHook` took the toplevel count from 0 to 0 while `render` took it\n from 0 to 1 in the same file.',
|
|
927
1101
|
},
|
|
928
1102
|
{
|
|
929
|
-
doc: "docs/gtkx-
|
|
1103
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
1104
|
+
heading: "What rc.4 renamed under us",
|
|
1105
|
+
text: "rc.4 is a naming-convention sweep. Nothing below changed behaviour, but each\none is a compile error or a silent runtime miss for a consumer of the RC.\n\n- **`@gtkx/react` moved the settings types off its public entry point.**\n `SettingsSchema`, `SettingsSchemaKeys` and `SettingValue` are now exported\n from `/internal` only, while the hooks they type (`useSetting`,\n `useBindSetting`) stay public — so an app that wants to name the type of a\n setting has no supported import for it. The bridge re-exports them from\n `/internal`; the ask to put them back is in\n [docs/upstream-gtkx.md](upstream-gtkx.md). (`MenuItem` and `VflConstraints`\n left the public entry too; nothing here used them.)\n- **`@gtkx/codegen`'s `runCodegen` result renamed `regenerated` →\n `isRegenerated`** (`src/runner/index.ts`), and the package dropped its\n `./gi` and `./jsx` subpath exports in favour of a new `./internal`.\n- **`@gtkx/vitest` renamed `GtkxPluginOptions` → `PluginOptions`**\n (`src/vitest/index.ts`).\n- **The element config renamed `lazy` → `isLazy` and `omitProps` →\n `omittedProps`.** This is the one with no compiler behind it: our three\n hosts synthesize `virtual:gtkx-config` as SOURCE TEXT\n (`src/runner/host.ts`, `src/runner/host-dev.ts`,\n `src/sea/gtkx-config-module.ts`), so a stale key typechecks perfectly and\n simply stops marking elements lazy at runtime. Caught by diffing rc.4's own\n `renderConfigModule` against ours and proven by the headless `run-linux`\n proof, not by a gate.\n- **`virtual:gtkx-config`'s metadata constants** went `SIGNALS` → `signals`,\n `CONSTRUCT_PROPS` → `constructProps`, `CONSTRUCT_ONLY_PROPS` →\n `constructOnlyProps`, `DEFAULT_PROPS` → `defaultProps`. Free for us: all\n three hosts re-export the module wholesale\n (`export * from \"@gtkx/jsx/metadata\"`) rather than naming its members.\n- **`@gtkx/testing` renamed `GtkxElementError` → `ElementError`,\n `render`'s `animations`/`reactStrictMode` → `areAnimationsEnabled`/\n `isReactStrictMode`, and `prettyWidget`'s `highlight` → `shouldHighlight`.**\n We use none of them, but a consumer's test suite will.\n- **`defineBehavior`'s `createContext` → `initialize`**, and\n `@gtkx/utils` dropped its `./function` subpath. Neither reaches us.",
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
1109
|
+
heading: "Fixed in rc.3 (history, one line each)",
|
|
1110
|
+
text: '- **`gsk-colorstop-boxed-write`** — constructing a `Gsk.ColorStop` threw in\n the native addon, so SVG gradients had zero constructible stops and painted\n nothing. **Fixed upstream by us** (gtkx-org/gtkx#473, closing #472): a\n record field write converts through `toNative` now.\n- **`graphene-rect-nested-boxed-props`** — the same native bug reached through\n `new Graphene.Rect({ origin, size })`; same upstream fix, so `svg-node.ts`\n uses the plain constructor again.\n- **The codegen freshness lie** — rc.2\'s `@gtkx/cli` could report "bindings up\n to date" over a store `npm install` had pruned. Fixed upstream in\n gtkx-org/gtkx#470 (also ours); separately `src/runner` calls the\n programmatic `@gtkx/codegen` API rather than the CLI.',
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
930
1114
|
heading: "Fixed in rc.2 (history, one line each)",
|
|
931
1115
|
text: "- **`vitest-compositor`** — rc.1 defaulted the headless display to weston;\n rc.2's default IS sway, so `vitest.config.ts` calls the plugin with no\n arguments.\n- **`no-virtual-seat`** — rc.1 had no input seat under sway, so windows never\n activated and `userEvent` was impossible; rc.2 starts a virtual seat.\n- **`fixed-layout-child`** — rc.1's declarative `<GtkFixedLayoutChild>`\n created a detached object; moot since containers moved to our own\n `RnGtkxLayout` manager and GtkFixed left the codebase.\n- **`controllers-as-children`** — rc.1 silently ignored controllers passed as\n JSX; rc.2 has a `controllers` slot on `GtkWidget`. Pressable and TextInput\n still attach theirs imperatively on purpose — a choice now, not a\n workaround.",
|
|
932
1116
|
},
|
|
933
1117
|
{
|
|
934
|
-
doc: "docs/gtkx-
|
|
935
|
-
heading: "Behaviour rc.
|
|
936
|
-
text: "
|
|
1118
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
1119
|
+
heading: "Behaviour rc.4 changed under us",
|
|
1120
|
+
text: "Nothing measurable. The suite is **166 files, 1601 passed + 1 expected fail**\non rc.4 — identical to main's own CI run on rc.3 (251c353), file for file and\ntest for test. The renames above are the whole of the release as far as this\nrepo can observe it: the reconciler's commit-time signal handling, the\nharness window, the accessibility tree and the codegen output all behave as\nthey did on rc.3, and the regenerated bindings typecheck clean.\n\nTwo things worth knowing before debugging something odd on rc.4:\n\n- **A first codegen after a version bump is slow enough to look like a hang.**\n The store fingerprint includes the app's own config, so each example\n regenerates once on top of the root's run — ~45 s for the gallery on the\n VM. `scripts/gtkx-dev-headless.ts` sleeps 25 s before its first shot and\n will report `FAST-REFRESH-FAIL` on a cold store; run `npx gtkx codegen` in\n the example first.\n- **`gtkx dev` still binds vite's HMR websocket on the fixed port 24678**, and\n the CLI exposes no way to move it. A second `gtkx dev` anywhere on the\n machine logs `WebSocket server error: Port 24678 is already in use` and the\n edit never reaches the app, while the supervisor still prints \"Fast Refresh\n complete\" — so the log marker alone is not proof the refresh applied.\n Verified identical on rc.3, so this is not new, but it makes the dev-path\n proof unreliable when another app is running.",
|
|
937
1121
|
},
|
|
938
1122
|
{
|
|
939
|
-
doc: "docs/gtkx-
|
|
1123
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
1124
|
+
heading: "Behaviour rc.3 changed under us (still true)",
|
|
1125
|
+
text: "- **Blockable signals are no longer suppressed for a whole React commit** —\n rc.3 wraps each framework write individually, so an emission the framework\n did not cause (one raised from a `useLayoutEffect`, or aimed at another\n `createRoot` tree) reaches its handler. Our navigators lean on this.\n- **`render`'s harness window is undecorated**, so role queries see only what\n the test rendered.\n- **A widget with `accessibleLabelledBy` reports the relation as its\n accessible name**, ahead of its own text — the precedence ARIA defines.\n- **`toHaveTextContent` no longer falls back to the accessible name**;\n **`toHaveDisplayValue` throws** on a widget without one; **checked state is\n tri-state**.\n- **Records are constructible only when their bytes can be copied.** Neither\n `Gsk.ColorStop` nor `Graphene.Rect` is caught by it.\n- **Single-child widgets have no `content`/`child` props** (pass the widget as\n a child instead).",
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
940
1129
|
heading: "Non-workarounds (quirks that stay)",
|
|
941
|
-
text: '- 64-bit FFI values arrive as BigInt → `toNumber()` at the boundary\n (`gtkx/bridge/measure.ts`);\n- signal names are kebab-case ("value-changed"); signals do not pass the\n emitter (get the widget from a ref);\n- role queries in tests use the `Gtk.AccessibleRole` enum, not strings;\n- `npm install` prunes the codegen store (`node_modules/.gtkx` is not in the\n lockfile) → run `npm run codegen` after installing — npm behavior, not gtkx;\n- measuring unmapped widgets yields 0 (offscreen Label probes are the\n exception) → re-measure on the `map` signal + re-commit measured leaves on\n every flush (`layout/node.ts`);\n- mixed-session setups only: running an app on a bare compositor (headless\n sway) while `XDG_RUNTIME_DIR` points at a full GNOME session can segfault in\n a GTK signal handler when the GNOME settings portal pushes updates into the\n app (`g_cclosure_marshal_VOID__OBJECTv` via the FFI emit path); cutting\n `DBUS_SESSION_BUS_ADDRESS` avoids it, which is why the headless scripts do.\n Normal desktop and container runs are unaffected. The
|
|
1130
|
+
text: '- 64-bit FFI values arrive as BigInt → `toNumber()` at the boundary\n (`gtkx/bridge/measure.ts`);\n- signal names are kebab-case ("value-changed"); signals do not pass the\n emitter (get the widget from a ref);\n- role queries in tests use the `Gtk.AccessibleRole` enum, not strings;\n- `npm install` prunes the codegen store (`node_modules/.gtkx` is not in the\n lockfile) → run `npm run codegen` after installing — npm behavior, not gtkx;\n- measuring unmapped widgets yields 0 (offscreen Label probes are the\n exception) → re-measure on the `map` signal + re-commit measured leaves on\n every flush (`layout/node.ts`);\n- mixed-session setups only: running an app on a bare compositor (headless\n sway) while `XDG_RUNTIME_DIR` points at a full GNOME session can segfault in\n a GTK signal handler when the GNOME settings portal pushes updates into the\n app (`g_cclosure_marshal_VOID__OBJECTv` via the FFI emit path); cutting\n `DBUS_SESSION_BUS_ADDRESS` avoids it, which is why the headless scripts do.\n Normal desktop and container runs are unaffected. The portal-push crash\n needs a live settings change to trigger and stays on the list unconfirmed.',
|
|
942
1131
|
},
|
|
943
1132
|
{
|
|
944
|
-
doc: "docs/gtkx-
|
|
1133
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
945
1134
|
heading: "Procedure when the next release ships",
|
|
946
|
-
text: "1. Update the `@gtkx/*` pins (root, spike, examples, template), then\n `npm install && rm -rf node_modules/.gtkx && npm run codegen`;\n2. Run everything on Linux: `npm run typecheck && npm test`, `build:dist`,\n `check:package`, plus the headless example proofs;\n3. Walk the live-workaround table: for each row check the removal condition,\n delete the tag and the row together when it is met, and move the entry into\n the history section above — **with a probe that proves the fix on the real\n runtime**, not just the release notes claiming it;\n4. Re-tag whatever survives (`
|
|
1135
|
+
text: "1. Update the `@gtkx/*` pins (root, spike, examples, template), then\n `npm install && rm -rf node_modules/.gtkx && npm run codegen`;\n2. Run everything on Linux: `npm run typecheck && npm test`, `build:dist`,\n `check:package`, plus the headless example proofs;\n3. Walk the live-workaround table: for each row check the removal condition,\n delete the tag and the row together when it is met, and move the entry into\n the history section above — **with a probe that proves the fix on the real\n runtime**, not just the release notes claiming it. And make the probe\n reproduce the ORIGINAL failure first: two of the three `runtime-dedupe`\n probes written for rc.4 reported a fix that was not there, because they\n were not actually building the duplicate;\n4. Re-tag whatever survives (`RC4-WORKAROUND` → the new release), rename this\n file to match the new pin, and update `docs/upstream-gtkx.md` if an ask was\n answered.",
|
|
947
1136
|
},
|
|
948
1137
|
{
|
|
949
1138
|
doc: "docs/research/navigation-extensibility.md",
|
|
@@ -971,70 +1160,110 @@ export const DOC_CHUNKS = [
|
|
|
971
1160
|
text: "No other desktop React Native platform has native navigation integration:\n\n- **react-native-screens** lists Windows support, but it is a thin\n old-architecture module; native-stack on modern react-native-windows\n fails, because screens has no new-architecture Windows implementation and\n RNW 0.82 removed the old one. Microsoft's own react-native-gallery falls\n back to the JS drawer.\n- **react-native-macos** is not supported by react-native-screens at all —\n AppKit has no navigation-stack primitive to bind to.\n\nOur path — a real `Adw.NavigationView` driven from a custom navigator, with\nreact-navigation state as the source of truth and native pops reported back\ninto it — is structurally the iOS native-stack / `UINavigationController`\nmodel, which neither desktop platform reached. GTK's advantage is that the\nprimitive exists at all: back button, Escape, back gesture, history menu and\ntransitions ship with the widget.\n\nThe trade: a JS stack can render anything into its fake header, while our\nchrome is real and had to be opened up deliberately — which is what the\nintrinsic-size root does.\n\nSources: react-native-screens README and discussions #1575 / #2541, RNW\ndiscussions #14273 / issue #4152 / new-architecture docs / 0.82 release\npost, microsoft/react-native-gallery, reactnavigation.org native-stack docs.",
|
|
972
1161
|
},
|
|
973
1162
|
{
|
|
974
|
-
doc: "docs/
|
|
975
|
-
heading: "
|
|
976
|
-
text: "`
|
|
1163
|
+
doc: "docs/gestures.md",
|
|
1164
|
+
heading: "Three layers, in the order to reach for them",
|
|
1165
|
+
text: "**1. `Pressable`** — taps, long presses, hover and keyboard activation. It is\nnot built on the responder system, it takes no negotiation, and it is what\nalmost every interaction actually needs.\n\n```tsx\n<Pressable\n onPress={open}\n onLongPress={showMenu}\n hitSlop={8}\n style={({ pressed, hovered, focused }) => [\n styles.row,\n hovered && styles.rowHovered,\n pressed && styles.rowPressed,\n focused && styles.rowFocused,\n ]}\n>\n <Text>Open</Text>\n</Pressable>\n```\n\n`hitSlop` widens the target without changing the layout; `pressRetentionOffset`\nsets how far the pointer may drift after pressing and still activate on\nrelease (RN's default rect, `{top: 20, left: 20, right: 20, bottom: 30}`, is\nalready generous). A release outside that rect is a cancel — dragging off a\ncontrol to change your mind works the way it does everywhere else.\n\n**2. The responder system and `PanResponder`** — drags, pans, swipes, and\nanything that needs to decide _which_ view owns an interaction.\n\n```tsx\nconst pan = useRef(new Animated.ValueXY()).current\nconst responder = useRef(\n PanResponder.create({\n // Claim on press, or wait for movement — the choice matters, see\n // \"Claiming on press versus on move\" below.\n onMoveShouldSetPanResponder: (_event, gesture) => Math.abs(gesture.dx) > 8,\n onPanResponderMove: (_event, gesture) => {\n pan.setValue({ x: gesture.dx, y: gesture.dy })\n },\n onPanResponderRelease: () => {\n // The continuing-drag idiom: fold where it ended into the offset so\n // the next gesture's dx starts from zero instead of snapping back.\n pan.extractOffset()\n pan.setValue({ x: 0, y: 0 })\n },\n }),\n).current\n\nreturn (\n <Animated.View\n {...responder.panHandlers}\n style={{ transform: pan.getTranslateTransform() }}\n />\n)\n```\n\nResponder and touch props go on `View` and `Animated.View`. Spreading\n`panHandlers` onto anything else compiles and does nothing, which is worth\nknowing because the idiomatic drag target is `Animated.View`.\n\n`PanResponder` here is react-native's own file, vendored unmodified, running\nagainst a reproduction of RN's `touchHistory` store — so `dx`/`dy`/`vx`/`vy`\nand the clustered-touch maths are upstream's, not a reimplementation.\n\n**3. GTK event controllers** — anything GTK does that React Native has no\nword for. Drag-and-drop with real drag icons and content negotiation, zoom\nand rotate gestures, keyboard shortcut controllers. This is a Linux-only\nescape hatch and the import says so:\n\n```tsx\nimport { Controllers, GtkDragSource } from \"react-native-gtkx/gtk\"\n\n;<Pressable onPress={open}>\n <Controllers>\n <GtkDragSource\n actions={Gdk.DragAction.MOVE}\n onPrepare={prepare}\n />\n </Controllers>\n <Text>{task.title}</Text>\n</Pressable>\n```\n\nSee [the platform layer](platform-layer.md). For drag-and-drop of any\nshape, including a reorderable list,\n[`react-native-gtkx/dnd`](api.md#drag-and-drop-react-native-gtkxdnd) already\nwraps this.",
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
doc: "docs/gestures.md",
|
|
1169
|
+
heading: "How the negotiation works",
|
|
1170
|
+
text: "One interaction lock for the whole process, as in RN. On a press, the\n`*ShouldSetResponderCapture` handlers run from the root down and the\n`*ShouldSetResponder` handlers from the target up; the first to return `true`\nwins. While the pointer is down the lock can move **upwards** — an ancestor\nthat returns `true` from `onMoveShouldSetResponder` takes it, provided the\ncurrent holder does not refuse with `onResponderTerminationRequest`. A\ndescendant can never take it from an ancestor.\n\nThe negotiation path stops at the layout root. A React Native tree here can\nbe an island inside a native GTK widget tree, so native widgets above or\nbetween views simply take no part.",
|
|
977
1171
|
},
|
|
978
1172
|
{
|
|
979
|
-
doc: "docs/
|
|
980
|
-
heading: "
|
|
981
|
-
text: "
|
|
1173
|
+
doc: "docs/gestures.md",
|
|
1174
|
+
heading: "Claiming on press versus on move",
|
|
1175
|
+
text: "Claiming on press (`onStartShouldSetPanResponder`) tells GTK the interaction\nis yours before anything else can compete for it. Claiming on movement is the\nmore common shape and is slightly weaker: inside a scrolling list on a\ntouchscreen, the scroller can take the first few pixels before your threshold\nis reached. Claim on press when the view is unambiguously a drag handle.",
|
|
982
1176
|
},
|
|
983
1177
|
{
|
|
984
|
-
doc: "docs/
|
|
985
|
-
heading: "
|
|
986
|
-
text:
|
|
1178
|
+
doc: "docs/gestures.md",
|
|
1179
|
+
heading: "Where this differs from React Native",
|
|
1180
|
+
text: "The full reasoning for each is in\n[docs/research/gestures.md](research/gestures.md); the short version:\n\n**Terminations mostly cannot be refused.** In React Native and\nreact-native-web, `onResponderTerminationRequest` is consulted for a context\nmenu, an ancestor scroll and a selection change. Here it is consulted in\nexactly two situations — a transfer to an ancestor, and an enclosing\n`ScrollView` scrolling. Everything else (a second mouse button, a native\nwidget or a `GtkDragSource` taking the sequence, text selection) reaches JS\nonly _after_ GTK has already taken the interaction away, and GTK's claim\ncannot be given back. Those arrive as `onResponderTerminate` with no question\nasked. Window blur also terminates unconditionally, which is RN's behaviour\ntoo.\n\n**One pointer.** A mouse is one fabricated touch; `touches` never has more\nthan one entry, and multi-finger `gestureState` is single-touch. Pinch and\nrotate are not available through the portable API — use `GtkGestureZoom` /\n`GtkGestureRotate` through `Controllers` if you need them on Linux.\n\n**ScrollView arbitration is touch-only.** All four gestures\n`GtkScrolledWindow` runs internally are touch-only, so under a mouse a child\npan never competes with scrolling at all. On touch, a view that takes the\nresponder suspends the enclosing scroller for the rest of the interaction\n(RN's `setIsJSResponder`). Scrolling with a **wheel** during a gesture is not\nsuppressed — it terminates the responder instead, which is react-native-web's\nrule for an ancestor scroll.\n\n**Hover fires from touch.** react-native-web filters hover events that come\nfrom a finger; GTK crossing events carry no device to filter on, GTK sends a\nmatching leave when a touch ends so no phantom hover sticks, and GTK's own\n`:hover` behaves the same way. Filtering here would make `Pressable` the odd\nwidget out in its own window.\n\n**`hitSlop` stops at a clip.** GTK stops picking at a clipping ancestor, so\nslop cannot escape a `ScrollView` viewport, or any view whose style says\n`overflow: \"hidden\"` — the same limit RN documents on Android.\n\n**No `Animated.event`.** Write the value directly\n(`pan.setValue({x: gesture.dx, y: gesture.dy})`), which is what it would do.",
|
|
987
1181
|
},
|
|
988
1182
|
{
|
|
989
|
-
doc: "docs/
|
|
990
|
-
heading: "
|
|
991
|
-
text: "`
|
|
1183
|
+
doc: "docs/gestures.md",
|
|
1184
|
+
heading: "Porting an app",
|
|
1185
|
+
text: "Both `react-native-reanimated` and `react-native-gesture-handler` are aliased\nonto reimplementations by both presets, so their imports resolve and their\n`Pan` code runs unedited. What is implemented of RNGH is\n`GestureHandlerRootView`, `GestureDetector`, `State`, `Pan`, `Tap`,\n`LongPress` and `Native` in both spellings (`Gesture.Pan()` and\n`usePanGesture()`, and so on), the `Race`/`Simultaneous`/`Exclusive` composers\nand the cross-gesture relations (`simultaneousWithExternalGesture`,\n`requireExternalGestureToFail`, `blocksExternalGesture` — arbitrated in a\nsecond, JS-only registry over the responder lock, because the lock has one\nholder by design and simultaneity is a set), and the components it re-exports\nfrom `react-native` — `ScrollView`, `FlatList`, `TextInput`, `Switch`,\n`Pressable` and the three `Touchable`s. See\n[the API reference](api.md#react-native-gesture-handler-react-native-gtkxgesture-handler)\nfor the tables, and the gallery's \"Gesture detector\" and \"Gesture\nrelations\" sections for the shapes running.\n\nWhat is not implemented throws where it is used, naming itself, rather than\nsilently doing nothing:\n\n- **`Pinch` and `Rotation`** — GTK feeds touchpad gestures properly, and\n nothing in this project's test rig can produce one, so they wait for a\n machine that can;\n- **`Fling`, `Hover`, `Manual`, `ForceTouch`**, the legacy `*GestureHandler`\n components and the button family (`RectButton` and friends — RNGH's own\n native button views, not RN components with a handler attached).\n\n**`react-native-draggable-flatlist` 4.0.3 and `@gorhom/bottom-sheet` 5.2.14\nboth run**, and neither was stopped by this surface in the end: what they\nneeded was four `react-native` core exports (`findNodeHandle`, `LogBox`,\n`Keyboard`, `VirtualizedList`) and Reanimated's `useAnimatedScrollHandler`,\nall of which ship now. That was established by BUILDING them and then driving\nthem with a real pointer rather than by reading their imports — twice a list\nof blockers derived from sources turned out to be wrong. The probe app is\n`spike/core-exports`; the API reference has the per-library detail and what\nthe probe does not prove.\n\nWhat to do instead, where something is still missing:\n\n- a **drag** — `PanResponder` plus `Animated.ValueXY`, as above. Portable,\n and it is what most RNGH usage in the wild amounts to;\n- **drag and drop between zones, or a sortable list** —\n [`react-native-gtkx/dnd`](api.md#drag-and-drop-react-native-gtkxdnd)\n mirrors `react-native-reanimated-dnd`'s API on GTK's own drag-and-drop,\n and both presets alias that package name onto it;\n- **swipeable rows** — by hand today: `PanResponder` for the gesture, plus\n either `Animated` or\n [`react-native-gtkx/reanimated`](api.md#react-native-reanimated-react-native-gtkxreanimated)\n for the motion. A **bottom sheet** no longer needs the hand-rolled version:\n `@gorhom/bottom-sheet` runs (see above), and `AdwBottomSheet` is the native\n one a Linux-first app would reach for instead.\n\n`examples/gallery`'s Gestures section is a working reference written entirely\nin portable `react-native`, with no platform-layer import in it at all.",
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
1189
|
+
heading: "RC4-WORKAROUND(use-signal-stale-handler)",
|
|
1190
|
+
text: "`useSignal` still routes the handler through React's `useEffectEvent`; `react-reconciler@0.33.0` only refreshes it in `commitBeforeMutationEffects` for `case 0` (FunctionComponent) — `case 11` (ForwardRef) and `case 15` (SimpleMemoComponent) fall through unrefreshed, so any `useEffectEvent` in a `memo`/`forwardRef` component is pinned to its mount closure forever (our `ScrollView` is a `forwardRef` with the `useSignal` calls inside it) — a fetch-fed FlatList empties itself on the first scroll. **rc.4 did not bump React and did not change the path** — see below — our workaround: `gtkx/bridge/use-signal.ts` re-pins the latest handler (insertion effect) and hands gtkx a stable wrapper; the bridge exports that hook, not gtkx's — removed when: A stable React 19.3 (React fixed the refresh on the 19.3 line)",
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
1194
|
+
heading: "RC4-WORKAROUND(runtime-dedupe)",
|
|
1195
|
+
text: "Two bundled copies of the gtkx runtime still double-init GLib and abort; nothing guards against it. Reproduced on rc.4: `g_log_set_writer_func() called multiple times`, SIGABRT — our workaround: `src/vite/index.ts` puts `resolve.dedupe` over `@gtkx/*` + `react` (+ `@react-navigation/*` for its context) into the preset every app inherits — removed when: Idempotent runtime init upstream, or an error that names the duplicate",
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
1199
|
+
heading: "RC4-WORKAROUND(prop-portal)",
|
|
1200
|
+
text: '`createPortal(children, container, key?)` is byte-for-byte the rc.3 signature and can still only target a container\'s DEFAULT slot ("children"). Every other slot an object exposes declaratively — a window\'s `Gio.ActionMap` (`actions`), a widget\'s `controllers`, an `AdwApplicationWindow`\'s `breakpoints` — is reached only by passing an element-valued PROP, which the reconciler routes through an internal `"gtkx:prop"` element. rc.4 exports that element from neither `@gtkx/react` nor `/internal`, and its `exports` map now refuses the deep path outright — our workaround: `gtkx/bridge/slot-portal.ts` restates the `"gtkx:prop"` element name and wraps it in `createSlotPortal(children, target, slot)` — the one line that would move if gtkx renames it. `WindowActions`/`ApplicationActions`/`WindowControllers` are built on it — removed when: gtkx exports a slot-aware portal (or the prop element) from a public entry point',
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
doc: "docs/gtkx-rc4-notes.md",
|
|
1204
|
+
heading: "RC4-WORKAROUND(renderhook-no-window)",
|
|
1205
|
+
text: "`renderHook` still mounts into a bare `Gtk.Box` — rc.4's `render-hook.js` is byte-identical to rc.3's, which was byte-identical to rc.2's — so window-dependent APIs have no toplevel to read — our workaround: Hook tests create a window with `render()` first (`tests/gtk/apis/dimensions.test.tsx`); packaged for consumers as `renderHookWithWindow` (`react-native-gtkx/testing`) — removed when: `renderHook` mounts into the same harness window `render` uses",
|
|
992
1206
|
},
|
|
993
1207
|
{
|
|
994
1208
|
doc: "docs/api.md",
|
|
995
1209
|
heading: "View",
|
|
996
|
-
text: "View — GTK implementation: GtkBox (RnGtkxViewBox). Supported: `style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here. Differences from RN:
|
|
1210
|
+
text: "View — GTK implementation: GtkBox (RnGtkxViewBox). Supported: `style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), `focusable` + `onFocus`/`onBlur` (RN has `focusable` on View for Android/Windows; react-native-web and react-native-windows both have the callbacks — off by default, as in RN), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props ([guide](gestures.md)) (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here. Differences from RN: Responder negotiation is RN's in full — capture-then-bubble, transfer to an ancestor mid-gesture through `onResponderTerminationRequest`/`onResponderReject`, and `onResponderTerminate`. The lock is one per process as in RN; the negotiation PATH stops at the layout root, so native GTK widgets between or above views take no part. Single-pointer only: a mouse is one fabricated touch, `touches` never exceeds one. Terminations differ from RN's, because GTK decides most of them before JS is told: a context menu (a second mouse button), a native widget or a `Controllers` `GtkDragSource` taking the sequence, and text selection all arrive as a cancelled gesture and terminate **without** consulting `onResponderTerminationRequest` — GTK's `CLAIMED` is irrevocable, so there is nothing an answer could change. Window blur terminates unconditionally, as it does in react-native-web. An enclosing `ScrollView` scrolling under the gesture is the one termination the holder may refuse. `overflow: \"hidden\"` (and `\"scroll\"`, which clips identically — a `View` is not made scrollable by a style on this platform any more than it is in RN) clips both the paint AND the picking of the children, including transformed ones and children driven out by an animated absolute `top`/`left`: GTK pushes a clip node over the container's CSS padding box before it snapshots them, and `gtk_widget_pick()` refuses the same box, so what you cannot see you cannot click. `borderRadius` shapes that clip — a rounded container clips its children to the rounded corners. A container never clips its OWN background, border, shadow or outline, only its children's",
|
|
997
1211
|
},
|
|
998
1212
|
{
|
|
999
1213
|
doc: "docs/api.md",
|
|
1000
1214
|
heading: "Text",
|
|
1001
|
-
text: "Text — GTK implementation: GtkLabel (Pango). Supported: wrap, `numberOfLines` (ellipsize END), `textAlign`, font styles, `onLayout`, `testID
|
|
1215
|
+
text: "Text — GTK implementation: GtkLabel (Pango). Supported: wrap, `numberOfLines` (ellipsize END), `textAlign`, font styles, `onLayout`, `testID`, ref: `measure`/`measureInWindow`/`measureLayout` (`TextHandle` — RN gives every host component the geometry methods, so a label no longer has to be wrapped in a `View` to be measurable). Differences from RN: nested `Text` elements are concatenated without per-span styles; text is always ellipsizable (shrinkable in narrow windows)",
|
|
1002
1216
|
},
|
|
1003
1217
|
{
|
|
1004
1218
|
doc: "docs/api.md",
|
|
1005
1219
|
heading: "Image",
|
|
1006
|
-
text: 'Image — GTK implementation: GtkPicture. Supported: `source={{uri}}`/string — local paths, file:// and **http(s)** (Node fetch → disk cache keyed by URL, in-flight de-duplication), `resizeMode` cover/contain/stretch/center, `onLoad`/`onError`; **`.svg` files load like any other image** — `Gdk.Texture.newFromFilename` rasterizes them via librsvg, no extra code needed (for building vector graphics from state instead of a file, see the "Svg" section below — a separate import, not part of this table). Differences from RN: no synchronous size from remote images (style sets the size, as in RN); cache is not size-limited yet',
|
|
1220
|
+
text: 'Image — GTK implementation: GtkPicture. Supported: `source={{uri}}`/string — local paths, file:// and **http(s)** (Node fetch → disk cache keyed by URL, in-flight de-duplication), `resizeMode` cover/contain/stretch/center, `onLoad`/`onError`; **`.svg` files load like any other image** — `Gdk.Texture.newFromFilename` rasterizes them via librsvg, no extra code needed (for building vector graphics from state instead of a file, see the "Svg" section below — a separate import, not part of this table); ref: `measure`/`measureInWindow`/`measureLayout` (`ImageHandle`). Differences from RN: no synchronous size from remote images (style sets the size, as in RN); cache is not size-limited yet',
|
|
1007
1221
|
},
|
|
1008
1222
|
{
|
|
1009
1223
|
doc: "docs/api.md",
|
|
1010
1224
|
heading: "TextInput",
|
|
1011
|
-
text: "TextInput — GTK implementation: GtkEntry / GtkTextView. Supported: controlled/uncontrolled (`value`/`defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus`/`onBlur`, `placeholder` (own dim overlay in multiline — GtkTextView has none), `secureTextEntry`, `editable`, `keyboardType`, `multiline`, `clearButtonMode` (GtkEntry's built-in clear icon; RN ships this on iOS only) (real GtkTextView: word wrap, internal scroll, Enter inserts a newline and never fires onSubmitEditing — RN semantics). Differences from RN: multiline needs a height in the style (as RN recommends)",
|
|
1225
|
+
text: "TextInput — GTK implementation: GtkEntry / GtkTextView. Supported: controlled/uncontrolled (`value`/`defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus`/`onBlur`, `placeholder` (own dim overlay in multiline — GtkTextView has none), `secureTextEntry`, `editable`, `keyboardType`, `multiline`, `clearButtonMode` (GtkEntry's built-in clear icon; RN ships this on iOS only), the visual half of `style` (background, border, radius — it used to be computed and dropped, so a styled TextInput silently kept the theme's own frame) (real GtkTextView: word wrap, internal scroll, Enter inserts a newline and never fires onSubmitEditing — RN semantics). Differences from RN: multiline needs a height in the style (as RN recommends)",
|
|
1012
1226
|
},
|
|
1013
1227
|
{
|
|
1014
1228
|
doc: "docs/api.md",
|
|
1015
1229
|
heading: "Pressable",
|
|
1016
|
-
text: "Pressable — GTK implementation:
|
|
1230
|
+
text: "Pressable — GTK implementation: GtkBox (RnGtkxViewBox) + GestureClick/Motion. Supported: `onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `onFocus`/`onBlur`, `focusable`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered, focused}` (react-native-web's own state shape); **keyboard-operable**: `focusable` defaults to true when `onPress` is set (react-native-web's rule), which puts the view in GTK's focus chain so Tab and the arrow keys reach it, and Enter/Space fire `onPress` as they do on web and Android; the `PressEvent` payload is RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, monotonic `timestamp`, single-element `touches`/`changedTouches` — a desktop pointer is one fabricated touch). `hitSlop` and `pressRetentionOffset`, each a number or per-edge; the press rect defaults to RN's own `{top: 20, left: 20, right: 20, bottom: 30}` around the hit rect, and a release outside it is a cancel rather than a press. Differences from RN: `hitSlop` cannot escape an ancestor that clips — a `ScrollView` viewport, or any view with `overflow: \"hidden\"` — because GTK stops picking at the clip, which is the limit RN documents on Android for the same reason. Hover fires from touch as well as from a mouse: react-native-web filters that out, and here a GTK crossing event carries no device to filter on; GTK also sends a matching leave when a touch sequence ends, so the stuck phantom hover the filter exists for does not arise, and GTK's own `:hover` behaves the same way (docs/research/gestures.md)",
|
|
1017
1231
|
},
|
|
1018
1232
|
{
|
|
1019
1233
|
doc: "docs/api.md",
|
|
1020
1234
|
heading: "TouchableOpacity",
|
|
1021
1235
|
text: "TouchableOpacity — GTK implementation: on top of Pressable. Supported: `activeOpacity`. Differences from RN: —",
|
|
1022
1236
|
},
|
|
1237
|
+
{
|
|
1238
|
+
doc: "docs/api.md",
|
|
1239
|
+
heading: "TouchableHighlight",
|
|
1240
|
+
text: "TouchableHighlight — GTK implementation: on top of Pressable. Supported: `underlayColor` (RN default `black`), `activeOpacity`, `onShowUnderlay`/`onHideUnderlay`. Differences from RN: RN renders a separate underlay VIEW behind the child and dims the child onto it; here the highlight is the view's own `backgroundColor` while pressed, because an extra box would change flex layout and what `measureLayout` is relative to — the same reason `GestureDetector` and `createAnimatedComponent` add none. Give the child a translucent background for RN's exact blend.",
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
doc: "docs/api.md",
|
|
1244
|
+
heading: "TouchableWithoutFeedback",
|
|
1245
|
+
text: "TouchableWithoutFeedback — GTK implementation: on top of Pressable. Supported: the `Pressable` press/hover/focus props, with no visual reaction. Differences from RN: RN clones its single child instead of rendering a box, which its own docs call a mistake kept for compatibility; this renders the `Pressable` box. Prefer `Pressable`, as RN's docs say.",
|
|
1246
|
+
},
|
|
1023
1247
|
{
|
|
1024
1248
|
doc: "docs/api.md",
|
|
1025
1249
|
heading: "ScrollView",
|
|
1026
|
-
text: "ScrollView — GTK implementation: GtkScrolledWindow. Supported: vertical/`horizontal`, `contentContainerStyle`, `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` (`ScrollViewHandle`). Differences from RN: `animated` in scrollTo is ignored",
|
|
1250
|
+
text: "ScrollView — GTK implementation: GtkScrolledWindow. Supported: vertical/`horizontal`, `contentContainerStyle` (RN's default: the content container is a plain `View`, so `alignItems` is `stretch` — children fill the cross axis unless they say otherwise), `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onScrollBeginDrag`/`onScrollEndDrag`/`onMomentumScrollBegin`/`onMomentumScrollEnd` (the four phases, each carrying the same `ScrollEvent` — see the Differences column), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` **plus the geometry methods** `measure`/`measureInWindow`/`measureLayout` (`ScrollViewHandle`). A view inside it that takes the responder suspends the scroller's own gestures for the rest of the interaction — RN's `setIsJSResponder`, so a child pan is reachable inside a scrolling list. Differences from RN: `animated` in scrollTo is ignored. **The scroll phases are input-device-aware**: GTK gives a mouse wheel isolated detents, so this platform groups a burst into one desktop begin/end session (120 ms idle boundary) and reports no momentum; a touchpad glide reports all four from its native GTK sequence, and the content really does keep moving after the fingers leave. RN has no wheel, so the wheel session is an explicit desktop extension rather than a parity claim. `onScrollBeginDrag`/`onScrollEndDrag` map onto the user-driven scroll SESSION (GTK's `::scroll-begin`/`::scroll-end` for touchpad, the grouped burst for wheel) rather than onto a finger on the content, which is the one approximation: a touchpad never touches the content, so \"the user started driving this scroller\" is the closest true statement. The momentum pair is read off the adjustment actually moving after the sequence ended, not off `::decelerate` (which fires at every lift, velocity or none), so a glide that ends dead reports the drag pair and no momentum pair — as RN does. **None of it is installed until a handler is attached**: no controller, no signal, no timer, and 6.93 µs per scroll event with all four attached against 7.17 µs with none, which is inside the noise; the GTK-controller residual is 0.31 µs per event while any phase handler is attached. A begin/end consumer also makes each wheel detent enter the 0.235 µs session state machine; momentum-only and untracked scrollers do not. Traces and numbers: [research/scroll-phases.md](research/scroll-phases.md). Scroll arbitration is **touch-only and unverified end to end**: all four gestures `GtkScrolledWindow` installs are touch-only, so under a mouse a child pan never competes with scrolling at all, and no touch can be injected on the test rig (wlroots has no virtual-touch protocol) — every link of the mechanism is tested, the finger is not. Two known edges on touch: a view that claims on a MOVE rather than on press can lose the first ~8 px to the scroller, which `CLAIMED` makes irrevocable (iOS has the same artefact); and the mouse wheel is deliberately left alone, so scrolling with a wheel during a gesture terminates the responder rather than being suppressed. **The scroller carries RN's own base style, `flexGrow: 1, flexShrink: 1`** — `styles.baseVertical`/`baseHorizontal`, composed UNDER the app's `style` exactly as RN's `StyleSheet.compose(baseStyle, this.props.style)` composes it, on the same node the `style` prop lands on. `FlatList`, `SectionList` and `VirtualizedList` inherit it. It is what makes a scrollable with NO style of its own a viewport rather than a box grown to its content: libraries hand their list down unstyled and let the host size it (`@gorhom/bottom-sheet` is the one that found this — its scroll lock had nothing to lock, because a list that never became a viewport never emitted a scroll event). **The surprise is the other consequence**: an explicit main-axis `height` on the scroller is only its flex BASIS, so in a taller flex parent grow expands past it — `<ScrollView style={{ height: 200 }}>` inside a 400px column lays out at 400, not 200. That is parity, not a deviation, and it was checked rather than assumed: RN 0.86.2 composes the same two properties under the same `style` prop on the same Yoga node, and in Yoga a definite main-axis length becomes the flex BASIS (`computeFlexBasisForChild`) before `resolveFlexibleLength` hands out the free space — neither function consults any config. The one Yoga setting the two platforms differ on is errata (RN runs the tree at `YGErrataAll`, this platform at Yoga's default `None`) and it is inert here: the only flex-relevant bit, `StretchFlexBasis`, is reached only when the CONTAINER's main size is indefinite, and a bounded parent's is not. `useWebDefaults` is false on both, which is exactly why the `flexShrink: 1` half is needed at all. To bound the viewport, bound the PARENT (`<View style={{ height: 200 }}><FlatList /></View>` — what an RN app writes) or cancel the base with `flexGrow: 0`",
|
|
1027
1251
|
},
|
|
1028
1252
|
{
|
|
1029
1253
|
doc: "docs/api.md",
|
|
1030
1254
|
heading: "FlatList",
|
|
1031
|
-
text: "FlatList — GTK implementation: windowed core on ScrollView. Supported: virtualization (`estimatedItemSize` or `getItemLayout`, **`windowSize`/`initialNumToRender` — the primary scroll-performance knobs**, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`), `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`, `ListHeader/Footer/EmptyComponent`, `onEndReached(-Threshold)`, `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`), `inverted` (RN chat semantics: opens at `data[0]`, stays pinned on prepend), `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`, ref: `scrollToIndex`/`scrollToItem`/`scrollToOffset` +
|
|
1255
|
+
text: "FlatList — GTK implementation: windowed core on ScrollView. Supported: virtualization (`estimatedItemSize` or `getItemLayout`, **`windowSize`/`initialNumToRender` — the primary scroll-performance knobs**, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`), `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`, `CellRendererComponent` (RN's per-cell wrapper — the list still hands it the cell's absolute `style` and the `onLayout` that measures it, and both must be applied; this is what `react-native-draggable-flatlist` builds its whole design on), `ListHeader/Footer/EmptyComponent`, `onEndReached(-Threshold)`, `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`), `inverted` (RN chat semantics: opens at `data[0]`, stays pinned on prepend), `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`, ref: `scrollToIndex`/`scrollToItem`/`scrollToOffset` + `scrollTo`/`scrollToEnd` (`FlatListHandle`) — the SCROLL half of a ScrollView ref, not the geometry half: a windowed list is a composite over a ScrollView and owns no widget of its own, so a `measure()` here would have to pick some inner widget and pretend it was the list. Measure the `ScrollView` or a cell. Differences from RN: 1000 rows mount windowed in ~120 ms (v1 full mount was 879 ms); `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure and a wider window means fewer mount+reflow bursts per scrolled pixel (measured: −21% churn, late frames 10/s → 7.7/s); rows beyond the visible ones are mounted `maxToRenderPerBatch` (10) at a time every `updateCellsBatchingPeriod` (50) ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one; no pull gesture — `onRefresh` must be app-triggered; an inverted list shorter than its viewport anchors to the top, not the bottom; `CellRendererComponent` is not applied to a STICKY cell (`stickyHeaderIndices`) — pinning reorders the cell's real GTK widget, so the sticky container has to BE the cell",
|
|
1032
1256
|
},
|
|
1033
1257
|
{
|
|
1034
1258
|
doc: "docs/api.md",
|
|
1035
1259
|
heading: "SectionList",
|
|
1036
1260
|
text: "SectionList — GTK implementation: on top of FlatList. Supported: `sections`, `renderSectionHeader`, sticky section headers by default (`stickySectionHeadersEnabled`). Differences from RN: viewability props are not exposed (section-aware ViewTokens pending)",
|
|
1037
1261
|
},
|
|
1262
|
+
{
|
|
1263
|
+
doc: "docs/api.md",
|
|
1264
|
+
heading: "VirtualizedList",
|
|
1265
|
+
text: "VirtualizedList — GTK implementation: the same windowed core. Supported: RN's data-source shape over the list `FlatList` already sits on: `data` is OPAQUE and read only through `getItemCount(data)` and `getItem(data, index)`, both honoured LAZILY (only the rows the window mounts are ever asked for). Everything else is `FlatList`'s row above, `CellRendererComponent` included. Differences from RN: The accessors are OPTIONAL here and required upstream — one component serves both shapes, which is why `FlatList` needs no separate implementation. `scrollToItem` scans the source through `getItem` (upstream scans too; an opaque source has no index to ask). Every difference in the `FlatList` row applies unchanged",
|
|
1266
|
+
},
|
|
1038
1267
|
{
|
|
1039
1268
|
doc: "docs/api.md",
|
|
1040
1269
|
heading: "Switch",
|
|
@@ -1053,7 +1282,7 @@ export const DOC_CHUNKS = [
|
|
|
1053
1282
|
{
|
|
1054
1283
|
doc: "docs/api.md",
|
|
1055
1284
|
heading: "Animated.View",
|
|
1056
|
-
text: "Animated.View — GTK implementation: direct widget calls. Supported: `opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange)
|
|
1285
|
+
text: "Animated.View — GTK implementation: direct widget calls. Supported: `opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange); `top`/`left`/`right`/`bottom` too when the node's own `position` is `\"absolute\"`, which is what makes `Animated.ValueXY`'s `getLayout()` work; and `width`/`height` where the change is confined to the node that owns it; plus the same responder and touch props `View` takes — this is where an idiomatic `PanResponder` drag lands; plus `pointerEvents` and `animatedProps`, because Reanimated's `Animated.View` is `createAnimatedComponent(View)` and every View prop reaches it there. Differences from RN: `rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`)",
|
|
1057
1286
|
},
|
|
1058
1287
|
{
|
|
1059
1288
|
doc: "docs/api.md",
|
|
@@ -1155,10 +1384,25 @@ export const DOC_CHUNKS = [
|
|
|
1155
1384
|
heading: "BackHandler",
|
|
1156
1385
|
text: 'BackHandler — Supported: `addEventListener("hardwareBackPress")`, `exitApp`. Differences: no hardware back key on desktop — subscriptions are honored but nothing fires them yet',
|
|
1157
1386
|
},
|
|
1387
|
+
{
|
|
1388
|
+
doc: "docs/api.md",
|
|
1389
|
+
heading: "findNodeHandle",
|
|
1390
|
+
text: "findNodeHandle — Supported: a stable integer per mounted widget, resolvable back to it; accepted by `measureLayout` as its first argument, alongside a handle object. Takes what RN takes — a component handle, a node handle (returned unchanged), `null`/`undefined`. A windowed list resolves to the `ScrollView` it renders, as RN's `FlatList` resolves through to its own scroll view. Differences: The tag identifies the WIDGET, not the ref: two refs onto one view report the same number and a re-render that rebuilt the handle object does not change it. It reaches nothing native — there is no `UIManager` to hand a tag to — so it is worth exactly what this platform can resolve it to: `measureLayout`, and identity. `null` for anything that is not a mounted host view (RN's answer too)",
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
doc: "docs/api.md",
|
|
1394
|
+
heading: "Keyboard",
|
|
1395
|
+
text: "Keyboard — Supported: `addListener` (honoured, never fires), `removeAllListeners`, `dismiss`, `isVisible` (always false), `metrics` (always undefined), `scheduleLayoutAnimation`. Differences: This is the SOFTWARE keyboard, and a desktop has none: every event it carries describes a panel occluding the app, so none can fire. Subscriptions are real and `remove()` pairs with them (a fake subscription would turn an unmount into a crash) — the same shape as `BackHandler` above, and what react-native-windows inherits from RN core, whose emitter is only ever fed on iOS and Android. **`dismiss()` is a no-op and deliberately not RN's**: RN blurs the focused input as the only way to retract the keyboard, and doing that here would let a library's gesture steal focus from a form",
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
doc: "docs/api.md",
|
|
1399
|
+
heading: "LogBox",
|
|
1400
|
+
text: "LogBox — Supported: `ignoreLogs`, `ignoreAllLogs`, `install`, `uninstall` — accepted and ignored. Differences: RN's LogBox is a full-screen dev OVERLAY, and `ignoreLogs` has never filtered the console — it keeps a warning out of the yellow box. There is no overlay here, so the console output after the call is already the console output RN would have had, and nothing observable is lost. Called by `react-native-draggable-flatlist` on every `NestableDraggableFlatList` render, to silence a nesting warning this platform does not emit either",
|
|
1401
|
+
},
|
|
1158
1402
|
{
|
|
1159
1403
|
doc: "docs/api.md",
|
|
1160
1404
|
heading: "PanResponder",
|
|
1161
|
-
text: "PanResponder — Supported: `create(config)` -> `panHandlers` (spread onto a `View`), full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) — **react-native's own file, vendored unmodified** (MIT, `Libraries/Interaction/PanResponder.js`), running on our reproduction of RN's `touchHistory` store. Differences: multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`'s return value is not consumed yet",
|
|
1405
|
+
text: "PanResponder — Supported: `create(config)` -> `panHandlers` (spread onto a `View`), full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) — **react-native's own file, vendored unmodified** (MIT, `Libraries/Interaction/PanResponder.js`), running on our reproduction of RN's `touchHistory` store. Differences: multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`'s return value is not consumed yet. `onPanResponderTerminationRequest` is asked when an ancestor tries to take the gesture and when an enclosing `ScrollView` scrolls; every other termination is GTK's decision and arrives as `onPanResponderTerminate` unasked (see `View`)",
|
|
1162
1406
|
},
|
|
1163
1407
|
{
|
|
1164
1408
|
doc: "docs/api.md",
|