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.
Files changed (340) hide show
  1. package/README.md +5 -2
  2. package/dist/aliases/index.d.ts +84 -0
  3. package/dist/aliases/index.js +208 -0
  4. package/dist/aliases/index.js.map +1 -0
  5. package/dist/animated/index.d.ts +2 -0
  6. package/dist/animated/index.js +5 -0
  7. package/dist/animated/index.js.map +1 -1
  8. package/dist/animated/types.d.ts +1 -0
  9. package/dist/animated/types.js.map +1 -1
  10. package/dist/animated/value-animation.js +17 -4
  11. package/dist/animated/value-animation.js.map +1 -1
  12. package/dist/apis/host.gtkx.js +9 -12
  13. package/dist/apis/host.gtkx.js.map +1 -1
  14. package/dist/apis/index.d.ts +4 -2
  15. package/dist/apis/index.js +2 -0
  16. package/dist/apis/index.js.map +1 -1
  17. package/dist/apis/keyboard.d.ts +60 -0
  18. package/dist/apis/keyboard.js +63 -0
  19. package/dist/apis/keyboard.js.map +1 -0
  20. package/dist/apis/log-box.d.ts +15 -0
  21. package/dist/apis/log-box.js +46 -0
  22. package/dist/apis/log-box.js.map +1 -0
  23. package/dist/apis/platform.d.ts +18 -1
  24. package/dist/apis/platform.js.map +1 -1
  25. package/dist/common/index.d.ts +0 -1
  26. package/dist/common/index.js +24 -1
  27. package/dist/common/index.js.map +1 -1
  28. package/dist/components/animated.d.ts +70 -6
  29. package/dist/components/animated.js +559 -46
  30. package/dist/components/animated.js.map +1 -1
  31. package/dist/components/driven-size.d.ts +27 -0
  32. package/dist/components/driven-size.js +85 -0
  33. package/dist/components/driven-size.js.map +1 -0
  34. package/dist/components/find-node-handle.d.ts +11 -0
  35. package/dist/components/find-node-handle.js +53 -0
  36. package/dist/components/find-node-handle.js.map +1 -0
  37. package/dist/components/flat-list.d.ts +3 -2
  38. package/dist/components/flat-list.js.map +1 -1
  39. package/dist/components/frame-scheduler.d.ts +16 -0
  40. package/dist/components/frame-scheduler.js +37 -0
  41. package/dist/components/frame-scheduler.js.map +1 -0
  42. package/dist/components/image.d.ts +6 -1
  43. package/dist/components/image.js +15 -4
  44. package/dist/components/image.js.map +1 -1
  45. package/dist/components/index.d.ts +7 -5
  46. package/dist/components/index.js +5 -3
  47. package/dist/components/index.js.map +1 -1
  48. package/dist/components/measure.d.ts +51 -1
  49. package/dist/components/measure.js +101 -2
  50. package/dist/components/measure.js.map +1 -1
  51. package/dist/components/pressable.d.ts +61 -1
  52. package/dist/components/pressable.js +151 -4
  53. package/dist/components/pressable.js.map +1 -1
  54. package/dist/components/rect-store.d.ts +58 -0
  55. package/dist/components/rect-store.js +124 -3
  56. package/dist/components/rect-store.js.map +1 -1
  57. package/dist/components/root.js +6 -1
  58. package/dist/components/root.js.map +1 -1
  59. package/dist/components/scroll-phase.d.ts +35 -0
  60. package/dist/components/scroll-phase.js +26 -0
  61. package/dist/components/scroll-phase.js.map +1 -0
  62. package/dist/components/scroll-view.d.ts +39 -3
  63. package/dist/components/scroll-view.js +378 -43
  64. package/dist/components/scroll-view.js.map +1 -1
  65. package/dist/components/text-input.js +9 -25
  66. package/dist/components/text-input.js.map +1 -1
  67. package/dist/components/text.d.ts +6 -2
  68. package/dist/components/text.js +43 -8
  69. package/dist/components/text.js.map +1 -1
  70. package/dist/components/use-focus.d.ts +27 -0
  71. package/dist/components/use-focus.js +116 -0
  72. package/dist/components/use-focus.js.map +1 -0
  73. package/dist/components/use-layout-child.d.ts +13 -1
  74. package/dist/components/use-layout-child.js +188 -5
  75. package/dist/components/use-layout-child.js.map +1 -1
  76. package/dist/components/view.d.ts +5 -2
  77. package/dist/components/view.js +18 -13
  78. package/dist/components/view.js.map +1 -1
  79. package/dist/components/virtualized-list.d.ts +45 -5
  80. package/dist/components/virtualized-list.js +90 -24
  81. package/dist/components/virtualized-list.js.map +1 -1
  82. package/dist/components/wheel-scroll-session.d.ts +7 -0
  83. package/dist/components/wheel-scroll-session.js +53 -0
  84. package/dist/components/wheel-scroll-session.js.map +1 -0
  85. package/dist/components/widget-retention.d.ts +35 -0
  86. package/dist/components/widget-retention.js +169 -0
  87. package/dist/components/widget-retention.js.map +1 -0
  88. package/dist/contracts.d.ts +4 -0
  89. package/dist/contracts.js.map +1 -1
  90. package/dist/dnd/context.d.ts +69 -0
  91. package/dist/dnd/context.js +149 -0
  92. package/dist/dnd/context.js.map +1 -0
  93. package/dist/dnd/draggable.d.ts +59 -0
  94. package/dist/dnd/draggable.js +149 -0
  95. package/dist/dnd/draggable.js.map +1 -0
  96. package/dist/dnd/droppable.d.ts +24 -0
  97. package/dist/dnd/droppable.js +74 -0
  98. package/dist/dnd/droppable.js.map +1 -0
  99. package/dist/dnd/gtk-controllers.d.ts +39 -0
  100. package/dist/dnd/gtk-controllers.js +110 -0
  101. package/dist/dnd/gtk-controllers.js.map +1 -0
  102. package/dist/dnd/index.d.ts +6 -0
  103. package/dist/dnd/index.js +31 -0
  104. package/dist/dnd/index.js.map +1 -0
  105. package/dist/dnd/order.d.ts +7 -0
  106. package/dist/dnd/order.js +20 -0
  107. package/dist/dnd/order.js.map +1 -0
  108. package/dist/dnd/payload.d.ts +14 -0
  109. package/dist/dnd/payload.js +41 -0
  110. package/dist/dnd/payload.js.map +1 -0
  111. package/dist/dnd/sortable.d.ts +49 -0
  112. package/dist/dnd/sortable.js +307 -0
  113. package/dist/dnd/sortable.js.map +1 -0
  114. package/dist/dnd/types.d.ts +290 -0
  115. package/dist/dnd/types.js +25 -0
  116. package/dist/dnd/types.js.map +1 -0
  117. package/dist/gesture-handler-compat/builder.d.ts +251 -0
  118. package/dist/gesture-handler-compat/builder.js +513 -0
  119. package/dist/gesture-handler-compat/builder.js.map +1 -0
  120. package/dist/gesture-handler-compat/composition.d.ts +23 -0
  121. package/dist/gesture-handler-compat/composition.js +111 -0
  122. package/dist/gesture-handler-compat/composition.js.map +1 -0
  123. package/dist/gesture-handler-compat/deciders.d.ts +3 -0
  124. package/dist/gesture-handler-compat/deciders.js +47 -0
  125. package/dist/gesture-handler-compat/deciders.js.map +1 -0
  126. package/dist/gesture-handler-compat/detector-runtime.d.ts +44 -0
  127. package/dist/gesture-handler-compat/detector-runtime.js +374 -0
  128. package/dist/gesture-handler-compat/detector-runtime.js.map +1 -0
  129. package/dist/gesture-handler-compat/detector.d.ts +22 -0
  130. package/dist/gesture-handler-compat/detector.js +104 -0
  131. package/dist/gesture-handler-compat/detector.js.map +1 -0
  132. package/dist/gesture-handler-compat/fling.d.ts +24 -0
  133. package/dist/gesture-handler-compat/fling.js +123 -0
  134. package/dist/gesture-handler-compat/fling.js.map +1 -0
  135. package/dist/gesture-handler-compat/force-touch.d.ts +13 -0
  136. package/dist/gesture-handler-compat/force-touch.js +40 -0
  137. package/dist/gesture-handler-compat/force-touch.js.map +1 -0
  138. package/dist/gesture-handler-compat/hooks.d.ts +157 -0
  139. package/dist/gesture-handler-compat/hooks.js +268 -0
  140. package/dist/gesture-handler-compat/hooks.js.map +1 -0
  141. package/dist/gesture-handler-compat/hover.d.ts +2 -0
  142. package/dist/gesture-handler-compat/hover.js +22 -0
  143. package/dist/gesture-handler-compat/hover.js.map +1 -0
  144. package/dist/gesture-handler-compat/index.d.ts +98 -0
  145. package/dist/gesture-handler-compat/index.js +255 -0
  146. package/dist/gesture-handler-compat/index.js.map +1 -0
  147. package/dist/gesture-handler-compat/long-press.d.ts +6 -0
  148. package/dist/gesture-handler-compat/long-press.js +21 -0
  149. package/dist/gesture-handler-compat/long-press.js.map +1 -0
  150. package/dist/gesture-handler-compat/manual.d.ts +2 -0
  151. package/dist/gesture-handler-compat/manual.js +18 -0
  152. package/dist/gesture-handler-compat/manual.js.map +1 -0
  153. package/dist/gesture-handler-compat/native.d.ts +7 -0
  154. package/dist/gesture-handler-compat/native.js +34 -0
  155. package/dist/gesture-handler-compat/native.js.map +1 -0
  156. package/dist/gesture-handler-compat/orchestrator.d.ts +77 -0
  157. package/dist/gesture-handler-compat/orchestrator.js +284 -0
  158. package/dist/gesture-handler-compat/orchestrator.js.map +1 -0
  159. package/dist/gesture-handler-compat/pan.d.ts +23 -0
  160. package/dist/gesture-handler-compat/pan.js +116 -0
  161. package/dist/gesture-handler-compat/pan.js.map +1 -0
  162. package/dist/gesture-handler-compat/recognizer.d.ts +268 -0
  163. package/dist/gesture-handler-compat/recognizer.js +1056 -0
  164. package/dist/gesture-handler-compat/recognizer.js.map +1 -0
  165. package/dist/gesture-handler-compat/relations.d.ts +43 -0
  166. package/dist/gesture-handler-compat/relations.js +128 -0
  167. package/dist/gesture-handler-compat/relations.js.map +1 -0
  168. package/dist/gesture-handler-compat/tap.d.ts +6 -0
  169. package/dist/gesture-handler-compat/tap.js +29 -0
  170. package/dist/gesture-handler-compat/tap.js.map +1 -0
  171. package/dist/gesture-handler-compat/touchpad.d.ts +41 -0
  172. package/dist/gesture-handler-compat/touchpad.js +64 -0
  173. package/dist/gesture-handler-compat/touchpad.js.map +1 -0
  174. package/dist/gesture-handler-compat/types.d.ts +441 -0
  175. package/dist/gesture-handler-compat/types.js +167 -0
  176. package/dist/gesture-handler-compat/types.js.map +1 -0
  177. package/dist/gtk/controllers.d.ts +39 -0
  178. package/dist/gtk/controllers.js +92 -0
  179. package/dist/gtk/controllers.js.map +1 -0
  180. package/dist/gtk/index.d.ts +2 -1
  181. package/dist/gtk/index.js +7 -1
  182. package/dist/gtk/index.js.map +1 -1
  183. package/dist/gtkx/bridge/geometry.js +6 -4
  184. package/dist/gtkx/bridge/geometry.js.map +1 -1
  185. package/dist/gtkx/bridge/index.d.ts +5 -3
  186. package/dist/gtkx/bridge/index.js +6 -3
  187. package/dist/gtkx/bridge/index.js.map +1 -1
  188. package/dist/gtkx/bridge/slot-portal.js.map +1 -1
  189. package/dist/gtkx/bridge/use-signal.js +1 -1
  190. package/dist/gtkx/bridge/use-signal.js.map +1 -1
  191. package/dist/gtkx/bridge/view-box.d.ts +56 -0
  192. package/dist/gtkx/bridge/view-box.js +394 -9
  193. package/dist/gtkx/bridge/view-box.js.map +1 -1
  194. package/dist/gtkx/bridge/widget-css.d.ts +17 -0
  195. package/dist/gtkx/bridge/widget-css.js +72 -0
  196. package/dist/gtkx/bridge/widget-css.js.map +1 -0
  197. package/dist/index.d.ts +3 -3
  198. package/dist/index.js +2 -2
  199. package/dist/index.js.map +1 -1
  200. package/dist/layout/driven-size.d.ts +12 -0
  201. package/dist/layout/driven-size.js +70 -0
  202. package/dist/layout/driven-size.js.map +1 -0
  203. package/dist/layout/engine.d.ts +17 -1
  204. package/dist/layout/engine.js +3 -20
  205. package/dist/layout/engine.js.map +1 -1
  206. package/dist/layout/node.d.ts +13 -0
  207. package/dist/layout/node.js +14 -0
  208. package/dist/layout/node.js.map +1 -1
  209. package/dist/layout/yoga.d.ts +3 -2
  210. package/dist/layout/yoga.js +5 -2
  211. package/dist/layout/yoga.js.map +1 -1
  212. package/dist/mcp/data/generated.d.ts +257 -60
  213. package/dist/mcp/data/generated.js +306 -62
  214. package/dist/mcp/data/generated.js.map +1 -1
  215. package/dist/metro/index.d.ts +10 -0
  216. package/dist/metro/index.js +21 -12
  217. package/dist/metro/index.js.map +1 -1
  218. package/dist/navigation/sidebar.d.ts +16 -0
  219. package/dist/navigation/sidebar.js +65 -1
  220. package/dist/navigation/sidebar.js.map +1 -1
  221. package/dist/reanimated-compat/animated-ref.d.ts +32 -0
  222. package/dist/reanimated-compat/animated-ref.js +57 -0
  223. package/dist/reanimated-compat/animated-ref.js.map +1 -0
  224. package/dist/reanimated-compat/animation.d.ts +177 -0
  225. package/dist/reanimated-compat/animation.js +540 -0
  226. package/dist/reanimated-compat/animation.js.map +1 -0
  227. package/dist/reanimated-compat/color.d.ts +28 -0
  228. package/dist/reanimated-compat/color.js +249 -0
  229. package/dist/reanimated-compat/color.js.map +1 -0
  230. package/dist/reanimated-compat/decay.d.ts +39 -0
  231. package/dist/reanimated-compat/decay.js +96 -0
  232. package/dist/reanimated-compat/decay.js.map +1 -0
  233. package/dist/reanimated-compat/easing.d.ts +30 -0
  234. package/dist/reanimated-compat/easing.js +76 -0
  235. package/dist/reanimated-compat/easing.js.map +1 -0
  236. package/dist/reanimated-compat/hooks.d.ts +14 -0
  237. package/dist/reanimated-compat/hooks.js +217 -0
  238. package/dist/reanimated-compat/hooks.js.map +1 -0
  239. package/dist/reanimated-compat/index.d.ts +258 -0
  240. package/dist/reanimated-compat/index.js +464 -0
  241. package/dist/reanimated-compat/index.js.map +1 -0
  242. package/dist/reanimated-compat/interpolation.d.ts +18 -0
  243. package/dist/reanimated-compat/interpolation.js +122 -0
  244. package/dist/reanimated-compat/interpolation.js.map +1 -0
  245. package/dist/reanimated-compat/layout-animation-config.d.ts +43 -0
  246. package/dist/reanimated-compat/layout-animation-config.js +78 -0
  247. package/dist/reanimated-compat/layout-animation-config.js.map +1 -0
  248. package/dist/reanimated-compat/layout-animation-presets.d.ts +117 -0
  249. package/dist/reanimated-compat/layout-animation-presets.js +498 -0
  250. package/dist/reanimated-compat/layout-animation-presets.js.map +1 -0
  251. package/dist/reanimated-compat/layout-animation-runtime.d.ts +21 -0
  252. package/dist/reanimated-compat/layout-animation-runtime.js +311 -0
  253. package/dist/reanimated-compat/layout-animation-runtime.js.map +1 -0
  254. package/dist/reanimated-compat/layout-animation-view.d.ts +27 -0
  255. package/dist/reanimated-compat/layout-animation-view.js +266 -0
  256. package/dist/reanimated-compat/layout-animation-view.js.map +1 -0
  257. package/dist/reanimated-compat/layout-animation.d.ts +171 -0
  258. package/dist/reanimated-compat/layout-animation.js +469 -0
  259. package/dist/reanimated-compat/layout-animation.js.map +1 -0
  260. package/dist/reanimated-compat/layout-transitions.d.ts +83 -0
  261. package/dist/reanimated-compat/layout-transitions.js +350 -0
  262. package/dist/reanimated-compat/layout-transitions.js.map +1 -0
  263. package/dist/reanimated-compat/mutable.d.ts +29 -0
  264. package/dist/reanimated-compat/mutable.js +139 -0
  265. package/dist/reanimated-compat/mutable.js.map +1 -0
  266. package/dist/reanimated-compat/props.d.ts +18 -0
  267. package/dist/reanimated-compat/props.js +111 -0
  268. package/dist/reanimated-compat/props.js.map +1 -0
  269. package/dist/reanimated-compat/scroll-handler.d.ts +119 -0
  270. package/dist/reanimated-compat/scroll-handler.js +274 -0
  271. package/dist/reanimated-compat/scroll-handler.js.map +1 -0
  272. package/dist/reanimated-compat/scroll-offset.d.ts +12 -0
  273. package/dist/reanimated-compat/scroll-offset.js +104 -0
  274. package/dist/reanimated-compat/scroll-offset.js.map +1 -0
  275. package/dist/reanimated-compat/style.d.ts +90 -0
  276. package/dist/reanimated-compat/style.js +452 -0
  277. package/dist/reanimated-compat/style.js.map +1 -0
  278. package/dist/reanimated-compat/test-timers.d.ts +28 -0
  279. package/dist/reanimated-compat/test-timers.js +136 -0
  280. package/dist/reanimated-compat/test-timers.js.map +1 -0
  281. package/dist/reanimated-compat/threads.d.ts +13 -0
  282. package/dist/reanimated-compat/threads.js +41 -0
  283. package/dist/reanimated-compat/threads.js.map +1 -0
  284. package/dist/reanimated-compat/tracking.d.ts +41 -0
  285. package/dist/reanimated-compat/tracking.js +106 -0
  286. package/dist/reanimated-compat/tracking.js.map +1 -0
  287. package/dist/reanimated-compat/updater-animations.d.ts +36 -0
  288. package/dist/reanimated-compat/updater-animations.js +273 -0
  289. package/dist/reanimated-compat/updater-animations.js.map +1 -0
  290. package/dist/responder/system.d.ts +80 -7
  291. package/dist/responder/system.js +143 -23
  292. package/dist/responder/system.js.map +1 -1
  293. package/dist/responder/use-responder.d.ts +11 -0
  294. package/dist/responder/use-responder.js +225 -6
  295. package/dist/responder/use-responder.js.map +1 -1
  296. package/dist/runner/host-dev.js +1 -1
  297. package/dist/runner/host-dev.js.map +1 -1
  298. package/dist/runner/host.js +1 -1
  299. package/dist/runner/host.js.map +1 -1
  300. package/dist/runner/index.js +3 -3
  301. package/dist/runner/index.js.map +1 -1
  302. package/dist/sea/gtkx-config-module.js +1 -1
  303. package/dist/sea/gtkx-config-module.js.map +1 -1
  304. package/dist/style/absolute-insets.d.ts +26 -0
  305. package/dist/style/absolute-insets.js +113 -0
  306. package/dist/style/absolute-insets.js.map +1 -0
  307. package/dist/style/animated-size.d.ts +25 -0
  308. package/dist/style/animated-size.js +225 -0
  309. package/dist/style/animated-size.js.map +1 -0
  310. package/dist/style/colors.d.ts +17 -0
  311. package/dist/style/colors.js +25 -6
  312. package/dist/style/colors.js.map +1 -1
  313. package/dist/style/imperative-css.d.ts +21 -0
  314. package/dist/style/imperative-css.js +63 -0
  315. package/dist/style/imperative-css.js.map +1 -0
  316. package/dist/style/index.d.ts +2 -1
  317. package/dist/style/index.js +2 -1
  318. package/dist/style/index.js.map +1 -1
  319. package/dist/style/split-style.js +5 -0
  320. package/dist/style/split-style.js.map +1 -1
  321. package/dist/testing/index.js.map +1 -1
  322. package/dist/unsupported-export.d.ts +8 -0
  323. package/dist/unsupported-export.js +66 -0
  324. package/dist/unsupported-export.js.map +1 -0
  325. package/dist/vite/index.d.ts +21 -8
  326. package/dist/vite/index.js +127 -82
  327. package/dist/vite/index.js.map +1 -1
  328. package/dist/vitest/index.d.ts +2 -2
  329. package/dist/vitest/index.js.map +1 -1
  330. package/dist/worklets-compat/index.d.ts +4 -0
  331. package/dist/worklets-compat/index.js +28 -0
  332. package/dist/worklets-compat/index.js.map +1 -0
  333. package/dist/worklets-compat/surface.d.ts +110 -0
  334. package/dist/worklets-compat/surface.js +153 -0
  335. package/dist/worklets-compat/surface.js.map +1 -0
  336. package/package.json +25 -9
  337. package/types.d.ts +9 -0
  338. package/dist/common/list.d.ts +0 -82
  339. package/dist/common/list.js +0 -166
  340. package/dist/common/list.js.map +0 -1
@@ -1,11 +1,15 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Children, createContext, forwardRef, useContext, useImperativeHandle, useLayoutEffect, useRef, useState, } from "react";
3
3
  import { splitStyle, StyleSheet } from "../style/index";
4
- import { Gtk, GtkBox, GtkScrolledWindow, queueAllocate, queueResize, useSignal, } from "../gtkx/bridge/index";
4
+ import { Gdk, Gtk, GtkBox, GtkScrolledWindow, queueAllocate, queueResize, useSignal, } from "../gtkx/bridge/index";
5
5
  import { ensurePerfReporter, perfAddTime, perfCount, perfEnabled, perfFrameTick, perfNow, } from "../perf";
6
+ import { useResponder } from "../responder/use-responder";
6
7
  import { HostNodeContext } from "./host-node";
8
+ import { createMeasureHandle, registerHandleWidget, } from "./measure";
7
9
  import { deferDuringAllocate, setStoredOffset } from "./rect-store";
10
+ import { scrollPhaseSink } from "./scroll-phase";
8
11
  import { useLayoutChild, useRnContainer, } from "./use-layout-child";
12
+ import { createWheelScrollSession } from "./wheel-scroll-session";
9
13
  export const StickyRegistryContext = createContext(null);
10
14
  // A wrapper around a sticky child: a plain container that reports its layout
11
15
  // and widget into the registry, so the scroll handler can translate/reorder
@@ -42,7 +46,7 @@ export const StickySlot = ({ stickyKey, style, top, onLayout, children, }) => {
42
46
  // node inside the same engine: the content's RnGtkxLayout measures the engine
43
47
  // content size (which grows past the viewport — that IS the scroll range;
44
48
  // overflow: scroll on the outer node) and the scrolled window pans it.
45
- export const ScrollView = forwardRef(({ style, contentContainerStyle, horizontal = false, stickyHeaderIndices, onScroll, onContentSizeChange, onLayout, children, testID, }, handleRef) => {
49
+ export const ScrollView = forwardRef(({ style, contentContainerStyle, horizontal = false, stickyHeaderIndices, onScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd, onContentSizeChange, onLayout, children, testID, ...responderProps }, handleRef) => {
46
50
  const outerRef = useRef(null);
47
51
  const contentRef = useRef(null);
48
52
  const [scrolled, setScrolled] = useState(null);
@@ -50,11 +54,39 @@ export const ScrollView = forwardRef(({ style, contentContainerStyle, horizontal
50
54
  // Yoga unconstrains a scroll node's MAIN axis — align it with the
51
55
  // scroll direction or a horizontal list's content clamps to the
52
56
  // viewport width (hadjustment upper == page).
57
+ //
58
+ // `flexGrow: 1, flexShrink: 1` is RN's own base style for the scroller
59
+ // (`styles.baseVertical` / `baseHorizontal` in ScrollView.js, applied as
60
+ // `StyleSheet.compose(baseStyle, this.props.style)` — hence base first,
61
+ // the app's style wins). The shrink half is what turns a scrollable with
62
+ // NO style of its own into a viewport: a flex item defaults to
63
+ // flexShrink 0 on RN's Yoga config, so without this the scroller sizes
64
+ // to its content, outgrows a bounded parent and its scroll range stays
65
+ // empty. It cost a whole investigation once: `@gorhom/bottom-sheet`
66
+ // hands its list down unstyled, so the sheet's scroll lock had nothing
67
+ // to lock — the list never received a scroll event because it had never
68
+ // become a viewport.
69
+ //
70
+ // Composing base-first does NOT protect an explicit `height`: grow and
71
+ // height are different properties, so nothing overrides anything — the
72
+ // height is the flex BASIS and grow expands from it, and a scroller with
73
+ // `height: 200` in a 400px column lays out at 400. RN does the same
74
+ // (same base, same order, same node, same Yoga resolution), so this is
75
+ // parity and not a bug; docs/api.md says so where people will look.
76
+ // Bound the PARENT to bound the viewport.
53
77
  style: [
78
+ { flexGrow: 1, flexShrink: 1 },
54
79
  style,
55
80
  { overflow: "scroll", flexDirection: horizontal ? "row" : "column" },
56
81
  ],
57
82
  onLayout,
83
+ // RN semantics: a ScrollView CLIPS its content. Containers paint-
84
+ // overflow by default, so without this the scrolled-away content (e.g.
85
+ // the original of a pinned sticky header) keeps drawing past the scroll
86
+ // area's edges. The `overflow: "scroll"` above would now clip the
87
+ // viewport on its own, but a viewport that clips only while nobody
88
+ // overrides the style is not a contract.
89
+ alwaysClips: true,
58
90
  });
59
91
  const [contentNode] = useState(() => host.engine.createNode());
60
92
  // Sticky positions are computed INSIDE the same allocation pass that
@@ -68,13 +100,6 @@ export const ScrollView = forwardRef(({ style, contentContainerStyle, horizontal
68
100
  }
69
101
  },
70
102
  });
71
- // RN semantics: a ScrollView CLIPS its content. Branch B made containers
72
- // paint-overflow by default, so without this the scrolled-away content
73
- // (e.g. the original of a pinned sticky header) keeps drawing past the
74
- // scroll area's edges.
75
- useLayoutEffect(() => {
76
- outerRef.current?.setOverflow(Gtk.Overflow.HIDDEN);
77
- }, []);
78
103
  useLayoutEffect(() => {
79
104
  outerNode.insertChild(contentNode, 0);
80
105
  contentNode.setCommit(() => {
@@ -103,39 +128,74 @@ export const ScrollView = forwardRef(({ style, contentContainerStyle, horizontal
103
128
  useLayoutEffect(() => {
104
129
  contentNode.setStyle({
105
130
  flexDirection: horizontal ? "row" : "column",
106
- alignItems: "flex-start",
131
+ // RN's content container is a plain `View`, and a `View`'s default
132
+ // `alignItems` is `stretch` — so a vertical ScrollView's children are
133
+ // as wide as the viewport unless they say otherwise. This used to be
134
+ // `flex-start`, which made every child shrink to its intrinsic width
135
+ // and any `flex: 1` inside it collapse to zero.
136
+ //
137
+ // It was found twice. First by `Sortable`, whose rows rendered as
138
+ // bare drag handles with no text (docs/research/drag-and-drop.md
139
+ // recorded it as a probable ScrollView parity bug and did not act on
140
+ // it, because changing a shared default under every example was not
141
+ // that epic's business). Then by porting
142
+ // `react-native-reanimated-dnd`'s example app, where it broke
143
+ // seventeen screens at once — at which point "a ported app changes
144
+ // nothing in its source" was simply false, and the default was the
145
+ // thing that was wrong.
146
+ //
147
+ // An app that wants the old behaviour writes it, exactly as it would
148
+ // on iOS and Android: `contentContainerStyle={{ alignItems:
149
+ // "flex-start" }}`.
150
+ alignItems: "stretch",
107
151
  ...contentLayout,
108
152
  });
109
153
  // eslint-disable-next-line react-hooks/exhaustive-deps
110
154
  }, [contentNode, contentKey]);
111
- useImperativeHandle(handleRef, () => ({
112
- scrollTo({ x, y }) {
113
- const widget = outerRef.current;
114
- if (!widget) {
115
- return;
116
- }
117
- if (y !== undefined) {
118
- widget.getVadjustment()?.setValue(y);
119
- }
120
- if (x !== undefined) {
121
- widget.getHadjustment()?.setValue(x);
122
- }
123
- },
124
- scrollToEnd() {
125
- const widget = outerRef.current;
126
- const adjustment = horizontal
127
- ? widget?.getHadjustment()
128
- : widget?.getVadjustment();
129
- if (adjustment) {
130
- adjustment.setValue(adjustment.getUpper() - adjustment.getPageSize());
131
- }
132
- },
133
- }));
155
+ // On the SCROLLED WINDOW, not on the content box: that is the widget the
156
+ // handle stands for, the widget a `GestureDetector` measures its gesture's
157
+ // view against, and the widget whose own kinetics a `Gesture.Native()`
158
+ // over this scrollable is talking about.
159
+ useResponder(outerRef, responderProps);
160
+ useImperativeHandle(handleRef, () => {
161
+ const handle = {
162
+ ...createMeasureHandle(outerRef, outerNode),
163
+ scrollTo({ x, y }) {
164
+ const widget = outerRef.current;
165
+ if (!widget) {
166
+ return;
167
+ }
168
+ if (y !== undefined) {
169
+ widget.getVadjustment()?.setValue(y);
170
+ }
171
+ if (x !== undefined) {
172
+ widget.getHadjustment()?.setValue(x);
173
+ }
174
+ },
175
+ scrollToEnd() {
176
+ const widget = outerRef.current;
177
+ const adjustment = horizontal
178
+ ? widget?.getHadjustment()
179
+ : widget?.getVadjustment();
180
+ if (adjustment) {
181
+ adjustment.setValue(adjustment.getUpper() - adjustment.getPageSize());
182
+ }
183
+ },
184
+ };
185
+ // Spreading built a NEW object, and the widget lookup is keyed by
186
+ // handle identity — without this the composed handle is a stranger to
187
+ // measure.ts.
188
+ registerHandleWidget(handle, outerRef);
189
+ return handle;
190
+ });
134
191
  // --- sticky headers -------------------------------------------------
135
192
  // The RN model, faithfully: the REAL child is translated (no duplicate,
136
- // state preserved). RN gives the pinned cell a zIndex; GTK's z-order is
137
- // sibling paint order, so the active slot is reordered to be the LAST
138
- // content child while pinned and restored afterwards. Per-frame movement
193
+ // state preserved). RN gives the pinned cell a zIndex; this reorders the
194
+ // active slot to be the LAST content child while pinned and restores it
195
+ // afterwards, which predates `zIndex` working (gtkx/bridge/view-box.ts)
196
+ // and is kept because it is exact: the slots here are the ScrollView's own
197
+ // and their Yoga order is rebuilt from the same list, so the reorder that
198
+ // would be a hazard for arbitrary app children is not one for these. Per-frame movement
139
199
  // goes through the rect-store offset fast path — zero React work while
140
200
  // scrolling.
141
201
  const stickySet = stickyHeaderIndices ?? [];
@@ -232,18 +292,20 @@ export const ScrollView = forwardRef(({ style, contentContainerStyle, horizontal
232
292
  }
233
293
  }
234
294
  };
235
- const emitScroll = () => {
236
- if (!onScroll) {
237
- return;
238
- }
295
+ // The one place the payload is read off the widget, shared by `onScroll`
296
+ // and by all four phases — RN hands the same `ScrollEvent` to every one
297
+ // of them, and reading it twice would be two sets of FFI calls for one
298
+ // truth. Null when there is nothing honest to report yet (no widget, or
299
+ // a content size the engine has not committed).
300
+ const readScrollEvent = () => {
239
301
  const widget = outerRef.current;
240
302
  const contentRect = contentNode.getRect();
241
303
  if (!widget || !contentRect) {
242
- return;
304
+ return null;
243
305
  }
244
306
  const hadjustment = widget.getHadjustment();
245
307
  const vadjustment = widget.getVadjustment();
246
- onScroll({
308
+ return {
247
309
  nativeEvent: {
248
310
  contentOffset: {
249
311
  x: hadjustment?.getValue() ?? 0,
@@ -255,7 +317,16 @@ export const ScrollView = forwardRef(({ style, contentContainerStyle, horizontal
255
317
  height: vadjustment?.getPageSize() ?? 0,
256
318
  },
257
319
  },
258
- });
320
+ };
321
+ };
322
+ const emitScroll = () => {
323
+ if (!onScroll) {
324
+ return;
325
+ }
326
+ const event = readScrollEvent();
327
+ if (event) {
328
+ onScroll(event);
329
+ }
259
330
  };
260
331
  const adjustment = horizontal
261
332
  ? (scrolled?.getHadjustment() ?? null)
@@ -273,6 +344,270 @@ export const ScrollView = forwardRef(({ style, contentContainerStyle, horizontal
273
344
  emitScroll();
274
345
  perfAddTime("scroll.js", perfNow() - start);
275
346
  };
347
+ // --- the four scroll phases -----------------------------------------
348
+ //
349
+ // What GTK actually reports, measured on 4.22.4 under a real pointer
350
+ // (docs/research/scroll-phases.md, and the trace is in
351
+ // tests/gtk/components/scroll-phases.gtk.test.tsx):
352
+ //
353
+ // - a mouse WHEEL emits `::scroll` per detent and NOTHING around it.
354
+ // GTK gives us no sequence, so while a consumer asks for begin/end we
355
+ // synthesize a desktop scroll SESSION: begin before the first detent
356
+ // mutates the adjustment, end after a measured idle window, and no
357
+ // momentum. This is an intentional desktop extension — RN has no
358
+ // wheel — and is what lets a phase-aware consumer capture the offset
359
+ // it is about to constrain instead of discovering it one event late.
360
+ // - a touchpad GLIDE emits `::scroll-begin`, a stream of `::scroll`,
361
+ // `::scroll-end` and `::decelerate` — a real sequence with a real
362
+ // beginning and end — and the scrolled window's own kinetic
363
+ // animation then carries the adjustment on for another second or so.
364
+ // All four phases exist, and all four are reported.
365
+ //
366
+ // Drag maps onto the scroll SEQUENCE rather than onto a finger on the
367
+ // content: `::scroll-begin` is "the user started driving this scroller",
368
+ // which is what a consumer of `onScrollBeginDrag` acts on and is as close
369
+ // as a device that never touches the content can get. That is the one
370
+ // approximation here, and it is named in docs/api.md.
371
+ //
372
+ // Momentum is read off the ADJUSTMENT, not off `::decelerate`. The
373
+ // controller emits `decelerate` at every `scroll-end` — it reports the
374
+ // velocity it measured, not a decision to coast — while the scrolled
375
+ // window decides for itself whether that velocity is worth animating. RN
376
+ // fires `onMomentumScrollBegin` only when the view actually keeps
377
+ // moving, so the honest source is the movement itself.
378
+ const phaseSink = scrollPhaseSink(onScroll);
379
+ // `wants()` rather than the sink's mere presence: every Reanimated scroll
380
+ // handler carries one, and most of them ask for nothing but `onScroll`.
381
+ // Asking is what keeps the common case free.
382
+ const wantsPhases = Boolean(onScrollBeginDrag ||
383
+ onScrollEndDrag ||
384
+ onMomentumScrollBegin ||
385
+ onMomentumScrollEnd ||
386
+ phaseSink?.wants());
387
+ // GTK brackets touchpad gestures for us. A wheel has only detents, so the
388
+ // begin/end extension below needs to see those detents — but only when a
389
+ // consumer asks for either end of the session. A momentum-only handler
390
+ // still pays no JS call per wheel event, because a wheel never coasts.
391
+ const wantsWheelSessions = Boolean(onScrollBeginDrag ||
392
+ onScrollEndDrag ||
393
+ phaseSink?.wants("beginDrag") ||
394
+ phaseSink?.wants("endDrag"));
395
+ // Read per event rather than captured, exactly as `useAnimatedScrollHandler`
396
+ // does it: a re-render that changed a handler must be picked up without
397
+ // tearing the GTK controller down and building a new one.
398
+ const phaseHandlers = useRef({
399
+ onScrollBeginDrag,
400
+ onScrollEndDrag,
401
+ onMomentumScrollBegin,
402
+ onMomentumScrollEnd,
403
+ phaseSink,
404
+ readScrollEvent,
405
+ });
406
+ phaseHandlers.current = {
407
+ onScrollBeginDrag,
408
+ onScrollEndDrag,
409
+ onMomentumScrollBegin,
410
+ onMomentumScrollEnd,
411
+ phaseSink,
412
+ readScrollEvent,
413
+ };
414
+ useLayoutEffect(() => {
415
+ // The whole cost story is this early return. No phase handler means no
416
+ // controller, no signal, no tick callback and no bookkeeping — the
417
+ // widget is byte-for-byte the one this component built before the
418
+ // phases existed.
419
+ if (!wantsPhases) {
420
+ return;
421
+ }
422
+ const widget = scrolled;
423
+ if (!widget) {
424
+ return;
425
+ }
426
+ const emitPhase = (phase) => {
427
+ const handlers = phaseHandlers.current;
428
+ const event = handlers.readScrollEvent();
429
+ if (!event) {
430
+ return;
431
+ }
432
+ if (phase === "beginDrag") {
433
+ handlers.onScrollBeginDrag?.(event);
434
+ }
435
+ else if (phase === "endDrag") {
436
+ handlers.onScrollEndDrag?.(event);
437
+ }
438
+ else if (phase === "momentumBegin") {
439
+ handlers.onMomentumScrollBegin?.(event);
440
+ }
441
+ else {
442
+ handlers.onMomentumScrollEnd?.(event);
443
+ }
444
+ handlers.phaseSink?.deliver(phase, event);
445
+ };
446
+ const axis = horizontal
447
+ ? widget.getHadjustment()
448
+ : widget.getVadjustment();
449
+ // Momentum is watched on the ADJUSTMENT, with two timers and no tick
450
+ // callback. A frame-polling watcher was the first shape and it was
451
+ // wrong twice over: it burns a callback per frame for the length of
452
+ // every coast, and it counts FRAMES, which is not a unit — under the
453
+ // headless compositor the frame clock free-runs at ~106 ticks per
454
+ // millisecond (measured, docs/research/scroll-phases.md), so a
455
+ // four-frame grace expired in microseconds. Wall time means the same
456
+ // thing everywhere.
457
+ //
458
+ // Milliseconds to wait after `::scroll-end` for the scrolled window's
459
+ // kinetic animation to take over before concluding it never will.
460
+ // Measured handoff: `::scroll-end` at 1921.9 ms, first inertial value
461
+ // change at 1927.5 ms — 5.6 ms. 120 is slack, not a guess.
462
+ const handoffMs = 120;
463
+ // Milliseconds of a motionless adjustment that end the momentum. GTK's
464
+ // deceleration steps the value every frame right to the end (16.7 ms
465
+ // apart in the trace), so 60 ms is three and a half missed steps.
466
+ const restMs = 60;
467
+ let watching = false;
468
+ let coasting = false;
469
+ let lastMoveAt = 0;
470
+ let handoffTimer = null;
471
+ let restTimer = null;
472
+ const clearTimers = () => {
473
+ if (handoffTimer !== null) {
474
+ clearTimeout(handoffTimer);
475
+ handoffTimer = null;
476
+ }
477
+ if (restTimer !== null) {
478
+ clearTimeout(restTimer);
479
+ restTimer = null;
480
+ }
481
+ };
482
+ // Re-armed for the REMAINING idle time rather than reset on every step,
483
+ // so the number of timers a coast costs is bounded by its duration
484
+ // (one per `restMs`) and not by how many times the adjustment moved.
485
+ // The difference is not academic: under the headless compositor's
486
+ // free-running frame clock a single coast steps the value ~150,000
487
+ // times, and a clear-and-rearm per step is 150,000 timers.
488
+ const checkRest = () => {
489
+ const idle = perfNow() - lastMoveAt;
490
+ if (idle < restMs) {
491
+ restTimer = setTimeout(checkRest, restMs - idle);
492
+ return;
493
+ }
494
+ restTimer = null;
495
+ stopMomentumWatch();
496
+ emitPhase("momentumEnd");
497
+ };
498
+ const onCoastStep = () => {
499
+ lastMoveAt = perfNow();
500
+ if (coasting) {
501
+ return;
502
+ }
503
+ coasting = true;
504
+ if (handoffTimer !== null) {
505
+ clearTimeout(handoffTimer);
506
+ handoffTimer = null;
507
+ }
508
+ emitPhase("momentumBegin");
509
+ restTimer = setTimeout(checkRest, restMs);
510
+ };
511
+ const stopMomentumWatch = () => {
512
+ clearTimers();
513
+ if (watching) {
514
+ axis?.off("value-changed", onCoastStep);
515
+ watching = false;
516
+ }
517
+ coasting = false;
518
+ };
519
+ const watchMomentum = () => {
520
+ if (watching) {
521
+ return;
522
+ }
523
+ watching = true;
524
+ axis?.on("value-changed", onCoastStep);
525
+ handoffTimer = setTimeout(() => {
526
+ handoffTimer = null;
527
+ if (!coasting) {
528
+ // The scroller came to rest with the drag. No inertia, so no
529
+ // momentum pair — which is RN's behaviour too.
530
+ stopMomentumWatch();
531
+ }
532
+ }, handoffMs);
533
+ };
534
+ const controller = Gtk.EventControllerScroll.new(
535
+ // An axis flag is REQUIRED, and not for the deltas: measured, a
536
+ // controller created with `KINETIC` alone emits nothing at all — not
537
+ // even `::scroll-begin`. `::scroll` is therefore emitted. It stays
538
+ // unconnected unless somebody asks for begin/end: only then does a
539
+ // wheel detent enter JS to maintain the desktop session below; a
540
+ // momentum-only or untracked scroller keeps the old empty C signal
541
+ // path.
542
+ Gtk.EventControllerScrollFlags.BOTH_AXES |
543
+ Gtk.EventControllerScrollFlags.KINETIC);
544
+ // CAPTURE, so the phase is reported before the scrolled window's own
545
+ // controller acts on the same event and moves the adjustment. Nothing
546
+ // here handles the event, so propagation is untouched either way.
547
+ controller.setPropagationPhase(Gtk.PropagationPhase.CAPTURE);
548
+ const beginDrag = () => {
549
+ // A new drag during a coast: RN ends the momentum before beginning
550
+ // the drag, so a consumer never sees two live phases at once.
551
+ if (coasting) {
552
+ stopMomentumWatch();
553
+ emitPhase("momentumEnd");
554
+ }
555
+ else {
556
+ stopMomentumWatch();
557
+ }
558
+ emitPhase("beginDrag");
559
+ };
560
+ // A wheel gives GTK no begin/end signals. Group its detents into one
561
+ // user-driven session, ending after an idle interval longer than the
562
+ // ordinary detent spacing measured in docs/research/scroll-phases.md
563
+ // (20–33 ms in the trace). The callback runs in CAPTURE, so beginDrag
564
+ // sees the PRE-scroll offset before GtkScrolledWindow mutates its
565
+ // adjustment and emits the onScroll callback that consumes the context.
566
+ const wheelSession = createWheelScrollSession(beginDrag, () => emitPhase("endDrag"));
567
+ // A touchpad brackets itself, so its sequence owns the session while it
568
+ // runs. Two live sessions at once would hand a consumer a second
569
+ // `beginDrag` before the first `endDrag` — which is exactly the state
570
+ // gorhom's lock reads to decide where to pin its list.
571
+ let nativeSequence = false;
572
+ const onScrollBegin = () => {
573
+ nativeSequence = true;
574
+ // A wheel burst may still be waiting out its idle timer.
575
+ wheelSession.finish();
576
+ beginDrag();
577
+ };
578
+ const onScrollEnd = () => {
579
+ nativeSequence = false;
580
+ emitPhase("endDrag");
581
+ watchMomentum();
582
+ };
583
+ const onScroll = () => {
584
+ if (!wantsWheelSessions ||
585
+ nativeSequence ||
586
+ controller.getUnit() !== Gdk.ScrollUnit.WHEEL) {
587
+ return false;
588
+ }
589
+ wheelSession.detent();
590
+ // Observe only. GtkScrolledWindow still receives and applies the
591
+ // detent, which is what makes this phase seam transparent.
592
+ return false;
593
+ };
594
+ controller.on("scroll-begin", onScrollBegin);
595
+ controller.on("scroll-end", onScrollEnd);
596
+ if (wantsWheelSessions) {
597
+ controller.on("scroll", onScroll);
598
+ }
599
+ widget.addController(controller);
600
+ return () => {
601
+ controller.off("scroll-begin", onScrollBegin);
602
+ controller.off("scroll-end", onScrollEnd);
603
+ if (wantsWheelSessions) {
604
+ controller.off("scroll", onScroll);
605
+ }
606
+ wheelSession.dispose();
607
+ widget.removeController(controller);
608
+ stopMomentumWatch();
609
+ };
610
+ }, [scrolled, wantsPhases, wantsWheelSessions, horizontal]);
276
611
  // Content-size reports dedupe on the engine rect: "changed" also fires
277
612
  // for pure viewport (page-size) changes, which RN does not report.
278
613
  const lastContentSize = useRef({ width: -1, height: -1 });