react-native-gtkx 0.3.0-alpha.1 → 0.4.0-alpha.2

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 (151) hide show
  1. package/README.md +1 -1
  2. package/dist/adw/index.d.ts +3 -2
  3. package/dist/adw/index.js +21 -4
  4. package/dist/adw/index.js.map +1 -1
  5. package/dist/adw/widgets.generated.d.ts +1 -1
  6. package/dist/adw/widgets.generated.js +2 -2
  7. package/dist/adw/widgets.generated.js.map +1 -1
  8. package/dist/animated/create-animated.d.ts +2 -0
  9. package/dist/animated/create-animated.js +2 -0
  10. package/dist/animated/create-animated.js.map +1 -1
  11. package/dist/animated/event.d.ts +15 -0
  12. package/dist/animated/event.js +80 -0
  13. package/dist/animated/event.js.map +1 -0
  14. package/dist/animated/index.d.ts +4 -0
  15. package/dist/animated/index.js +8 -0
  16. package/dist/animated/index.js.map +1 -1
  17. package/dist/animated/spring.d.ts +10 -0
  18. package/dist/animated/spring.js +20 -6
  19. package/dist/animated/spring.js.map +1 -1
  20. package/dist/animated/timing.d.ts +10 -0
  21. package/dist/animated/timing.js +21 -13
  22. package/dist/animated/timing.js.map +1 -1
  23. package/dist/apis/host.gtkx.js +70 -11
  24. package/dist/apis/host.gtkx.js.map +1 -1
  25. package/dist/common/navigation-stack.d.ts +6 -3
  26. package/dist/common/navigation-stack.js +8 -1
  27. package/dist/common/navigation-stack.js.map +1 -1
  28. package/dist/common/widget.js +2 -11
  29. package/dist/common/widget.js.map +1 -1
  30. package/dist/components/animated.d.ts +1 -0
  31. package/dist/components/animated.js +28 -4
  32. package/dist/components/animated.js.map +1 -1
  33. package/dist/components/app-registry.js +32 -8
  34. package/dist/components/app-registry.js.map +1 -1
  35. package/dist/components/request-animation-frame.d.ts +33 -0
  36. package/dist/components/request-animation-frame.js +83 -0
  37. package/dist/components/request-animation-frame.js.map +1 -0
  38. package/dist/components/scroll-view.js +60 -2
  39. package/dist/components/scroll-view.js.map +1 -1
  40. package/dist/components/virtualized-list.js +1 -4
  41. package/dist/components/virtualized-list.js.map +1 -1
  42. package/dist/dnd/autoscroll.d.ts +44 -0
  43. package/dist/dnd/autoscroll.js +176 -0
  44. package/dist/dnd/autoscroll.js.map +1 -0
  45. package/dist/dnd/grid-order.d.ts +45 -0
  46. package/dist/dnd/grid-order.js +151 -0
  47. package/dist/dnd/grid-order.js.map +1 -0
  48. package/dist/dnd/grid.d.ts +44 -0
  49. package/dist/dnd/grid.js +397 -0
  50. package/dist/dnd/grid.js.map +1 -0
  51. package/dist/dnd/gtk-controllers.d.ts +16 -2
  52. package/dist/dnd/gtk-controllers.js +23 -3
  53. package/dist/dnd/gtk-controllers.js.map +1 -1
  54. package/dist/dnd/index.d.ts +4 -2
  55. package/dist/dnd/index.js +9 -2
  56. package/dist/dnd/index.js.map +1 -1
  57. package/dist/dnd/order-state.d.ts +14 -0
  58. package/dist/dnd/order-state.js +55 -0
  59. package/dist/dnd/order-state.js.map +1 -0
  60. package/dist/dnd/order.d.ts +19 -0
  61. package/dist/dnd/order.js +19 -0
  62. package/dist/dnd/order.js.map +1 -1
  63. package/dist/dnd/sortable.d.ts +29 -4
  64. package/dist/dnd/sortable.js +308 -115
  65. package/dist/dnd/sortable.js.map +1 -1
  66. package/dist/dnd/types.d.ts +236 -14
  67. package/dist/dnd/types.js +30 -0
  68. package/dist/dnd/types.js.map +1 -1
  69. package/dist/gesture-handler-compat/attach-context.d.ts +29 -0
  70. package/dist/gesture-handler-compat/attach-context.js +90 -0
  71. package/dist/gesture-handler-compat/attach-context.js.map +1 -0
  72. package/dist/gesture-handler-compat/detector-runtime.d.ts +14 -3
  73. package/dist/gesture-handler-compat/detector-runtime.js +15 -5
  74. package/dist/gesture-handler-compat/detector-runtime.js.map +1 -1
  75. package/dist/gesture-handler-compat/detector.js +57 -2
  76. package/dist/gesture-handler-compat/detector.js.map +1 -1
  77. package/dist/gesture-handler-compat/gesture-state-manager.d.ts +5 -0
  78. package/dist/gesture-handler-compat/gesture-state-manager.js +77 -0
  79. package/dist/gesture-handler-compat/gesture-state-manager.js.map +1 -0
  80. package/dist/gesture-handler-compat/index.d.ts +2 -2
  81. package/dist/gesture-handler-compat/index.js +38 -33
  82. package/dist/gesture-handler-compat/index.js.map +1 -1
  83. package/dist/gesture-handler-compat/recognizer.d.ts +12 -1
  84. package/dist/gesture-handler-compat/recognizer.js +23 -9
  85. package/dist/gesture-handler-compat/recognizer.js.map +1 -1
  86. package/dist/gesture-handler-compat/tag-registry.d.ts +14 -0
  87. package/dist/gesture-handler-compat/tag-registry.js +19 -0
  88. package/dist/gesture-handler-compat/tag-registry.js.map +1 -0
  89. package/dist/globals/index.d.ts +47 -0
  90. package/dist/globals/index.js +181 -0
  91. package/dist/globals/index.js.map +1 -0
  92. package/dist/globals/install.d.ts +1 -0
  93. package/dist/globals/install.js +14 -0
  94. package/dist/globals/install.js.map +1 -0
  95. package/dist/gtkx/bridge/adw-namespace.d.ts +2 -0
  96. package/dist/gtkx/bridge/adw-namespace.js +26 -0
  97. package/dist/gtkx/bridge/adw-namespace.js.map +1 -0
  98. package/dist/gtkx/bridge/adw.d.ts +23 -0
  99. package/dist/gtkx/bridge/adw.js +93 -0
  100. package/dist/gtkx/bridge/adw.js.map +1 -0
  101. package/dist/gtkx/bridge/color-scheme-parse.d.ts +11 -0
  102. package/dist/gtkx/bridge/color-scheme-parse.js +42 -0
  103. package/dist/gtkx/bridge/color-scheme-parse.js.map +1 -0
  104. package/dist/gtkx/bridge/color-scheme-portal.d.ts +24 -0
  105. package/dist/gtkx/bridge/color-scheme-portal.js +172 -0
  106. package/dist/gtkx/bridge/color-scheme-portal.js.map +1 -0
  107. package/dist/gtkx/bridge/core.d.ts +25 -0
  108. package/dist/gtkx/bridge/core.js +69 -0
  109. package/dist/gtkx/bridge/core.js.map +1 -0
  110. package/dist/gtkx/bridge/drag-layer.d.ts +27 -0
  111. package/dist/gtkx/bridge/drag-layer.js +177 -0
  112. package/dist/gtkx/bridge/drag-layer.js.map +1 -0
  113. package/dist/gtkx/bridge/index.d.ts +1 -27
  114. package/dist/gtkx/bridge/index.js +12 -71
  115. package/dist/gtkx/bridge/index.js.map +1 -1
  116. package/dist/gtkx/bridge/widgets.generated.adw.d.ts +1 -0
  117. package/dist/gtkx/bridge/widgets.generated.adw.js +16 -0
  118. package/dist/gtkx/bridge/widgets.generated.adw.js.map +1 -0
  119. package/dist/gtkx/bridge/widgets.generated.d.ts +0 -1
  120. package/dist/gtkx/bridge/widgets.generated.js +3 -3
  121. package/dist/gtkx/bridge/widgets.generated.js.map +1 -1
  122. package/dist/index.js +20 -0
  123. package/dist/index.js.map +1 -1
  124. package/dist/mcp/data/generated.d.ts +41 -20
  125. package/dist/mcp/data/generated.js +46 -20
  126. package/dist/mcp/data/generated.js.map +1 -1
  127. package/dist/metro/index.d.ts +1 -0
  128. package/dist/metro/index.js +12 -0
  129. package/dist/metro/index.js.map +1 -1
  130. package/dist/navigation/sidebar.js +2 -45
  131. package/dist/navigation/sidebar.js.map +1 -1
  132. package/dist/reanimated-compat/animatable-value.d.ts +158 -0
  133. package/dist/reanimated-compat/animatable-value.js +316 -0
  134. package/dist/reanimated-compat/animatable-value.js.map +1 -0
  135. package/dist/reanimated-compat/animation.d.ts +34 -13
  136. package/dist/reanimated-compat/animation.js +97 -25
  137. package/dist/reanimated-compat/animation.js.map +1 -1
  138. package/dist/reanimated-compat/mutable.js +6 -6
  139. package/dist/reanimated-compat/mutable.js.map +1 -1
  140. package/dist/reanimated-compat/updater-animations.js +32 -12
  141. package/dist/reanimated-compat/updater-animations.js.map +1 -1
  142. package/dist/runner/host-dev.js +5 -1
  143. package/dist/runner/host-dev.js.map +1 -1
  144. package/dist/runner/host.js +9 -1
  145. package/dist/runner/host.js.map +1 -1
  146. package/dist/vite/index.js +130 -0
  147. package/dist/vite/index.js.map +1 -1
  148. package/package.json +1 -1
  149. package/dist/gtkx/bridge/theme.d.ts +0 -4
  150. package/dist/gtkx/bridge/theme.js +0 -5
  151. package/dist/gtkx/bridge/theme.js.map +0 -1
@@ -1,75 +1,60 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- // `Sortable`, `SortableItem`, `useSortable` and `useSortableList` the
3
- // drag-to-reorder list, which is the shape most apps actually reach for.
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // `Sortable`, `SortableItem`, `useSortable`, `useSortableList` and their
3
+ // horizontal counterparts (`useHorizontalSortable`, `useHorizontalSortableList`)
4
+ // — the drag-to-reorder list, which is the shape most apps actually reach
5
+ // for, in either direction.
4
6
  //
5
7
  // The reorder is LIVE: crossing another row moves the dragged row into its
6
8
  // place immediately, so the list rearranges under the drag icon. That is
7
9
  // upstream's behaviour too — its `onMove` fires as rows cross, not at the
8
10
  // end — minus the spring, because here the rows are laid out by Yoga rather
9
- // than transformed.
11
+ // than transformed. The mechanism does not care which axis the list scrolls
12
+ // along, which is why `SortableDirection.Horizontal` is a render-time branch
13
+ // here (a horizontal `ScrollView`, `leftBound`/`autoScrollHorizontalDirection`
14
+ // plumbing) rather than a second implementation.
15
+ //
16
+ // WHAT DECIDES A CROSSING, and why it changed: this used to be GDK
17
+ // hit-testing the raw pointer against a neighbour row's full rect (a row's
18
+ // own `onEnter`) — grab-point DEPENDENT, unlike upstream's own item-rect
19
+ // reasoning (`docs/research/dnd-collision-feel.md`). It is now the dragged
20
+ // row's own TRACKED position, upstream's shape without upstream's asymmetry:
21
+ // `useEdgeAutoscroll`'s `GtkDropControllerMotion` (already watching every
22
+ // motion event for edge-autoscroll) also reports each one here via
23
+ // `onDragMotion`, and `handleDragMotion` below tracks `fromIndex * slotSize +
24
+ // (pointer delta since the drag began)`, resolving the row it lands on by
25
+ // ROUNDING rather than upstream's own floor — the dragged row's CENTRE
26
+ // against a slot's centre, not its top-left corner against the slot's origin
27
+ // — see `order.ts`'s `resolveTrackedIndex` for the arithmetic. The origin
28
+ // that delta is measured from is `DragSourceControllers`'s `onGrab`,
29
+ // converted to the list's own container coordinates with `computePointIn`,
30
+ // NEVER the first motion sample: a fast drag's first sample arrives already
31
+ // displaced past GDK's own drag-start threshold, undercounting travel.
32
+ // `SortableItem`'s own `onEnter` no longer drives a reorder at all — only
33
+ // `onMotion` still does, for the public `onDragging`/`onDraggingHorizontal`
34
+ // callbacks, unrelated to this decision.
10
35
  //
11
36
  // The component owns the order, exactly as upstream requires ("do NOT update
12
37
  // external state in `onMove`"). An app reads the settled order from
13
38
  // `onDrop`'s `allPositions`.
14
39
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
40
+ import { widgetForHandle } from "../components/measure";
15
41
  import { ScrollView } from "../components/scroll-view";
16
42
  import { View } from "../components/view";
43
+ import { computePointIn } from "../gtkx/bridge/index";
44
+ import { useEdgeAutoscroll } from "./autoscroll";
17
45
  import { DraggableContext, DraggableHandle } from "./draggable";
18
46
  import { DragSourceControllers, DropTargetControllers } from "./gtk-controllers";
19
- import { listToObject } from "./order";
20
- import { nextDraggableId } from "./payload";
21
- import { ScrollDirection, SortableDirection, } from "./types";
47
+ import { listToObject, resolveTrackedIndex } from "./order";
48
+ import { keyOf, useOrder } from "./order-state";
49
+ import { HorizontalScrollDirection, ScrollDirection, SortableDirection, } from "./types";
22
50
  // Upstream exports these from the same entry point as the components, so
23
51
  // they are re-exported here rather than moved; the implementations live in
24
52
  // ./order, which imports nothing from GTK.
25
53
  export { clamp, listToObject, objectMove } from "./order";
26
54
  const SortableContext = createContext(null);
27
- const keyOf = (item, index, extractor) => extractor?.(item, index) ?? item.id;
28
- /** The order state, shared by `useSortableList` and `Sortable`. */
29
- const useOrder = (data, itemKeyExtractor) => {
30
- // Lazy initial state, not a ref: stable for the list's lifetime, and a ref
31
- // may not be read during render.
32
- const [scope] = useState(() => `sortable-${nextDraggableId()}`);
33
- const incoming = useMemo(() => data.map((item, index) => keyOf(item, index, itemKeyExtractor)), [data, itemKeyExtractor]);
34
- const [order, setOrder] = useState(incoming);
35
- // The list owns the ORDER; the app owns the SET. Adding or removing an item
36
- // has to reach the order without discarding a reorder the user already
37
- // made — so new ids go to the end, departed ids drop out, and a pure
38
- // reorder of `data` by the app is ignored (upstream's contract too: the
39
- // component owns the order).
40
- //
41
- // Adjusted DURING RENDER rather than in an effect. React documents this as
42
- // the way to derive state from changed props, and it matters here: an
43
- // effect would paint one frame in the stale order every time the app
44
- // appends an item.
45
- const signature = incoming.join(" ");
46
- const [seenSignature, setSeenSignature] = useState(signature);
47
- if (seenSignature !== signature) {
48
- setSeenSignature(signature);
49
- const known = new Set(incoming);
50
- const next = [
51
- ...order.filter((id) => known.has(id)),
52
- ...incoming.filter((id) => !order.includes(id)),
53
- ];
54
- if (next.length !== order.length ||
55
- next.some((id, index) => id !== order[index])) {
56
- setOrder(next);
57
- }
58
- }
59
- const items = useMemo(() => {
60
- const byId = new Map();
61
- data.forEach((item, index) => {
62
- byId.set(keyOf(item, index, itemKeyExtractor), item);
63
- });
64
- return order
65
- .map((id) => byId.get(id))
66
- .filter((item) => item !== undefined);
67
- }, [order, data, itemKeyExtractor]);
68
- return { scope, order, setOrder, items };
69
- };
70
55
  /** The `SharedValue`-shaped boxes upstream hands to `renderItem`. Real,
71
56
  * readable and writable here — just not animated. See `SharedValueLike`. */
72
- const useSharedBoxes = (order) => {
57
+ const useSharedBoxes = (order, direction, autoScrollDirection) => {
73
58
  // Rebuilt when the order changes rather than mutated in place. Upstream's
74
59
  // box keeps its identity and changes its `.value`; here the identity tracks
75
60
  // the value, which is the closest thing available without a mutable cell
@@ -79,28 +64,59 @@ const useSharedBoxes = (order) => {
79
64
  const positions = useMemo(() => ({
80
65
  value: listToObject(order),
81
66
  }), [order]);
82
- // These three this platform never writes: there is no UI-thread scroll
83
- // position, no autoscroll during a drag (see docs/research/drag-and-drop.md)
84
- // and no measured row heights, because Yoga lays rows out at their natural
85
- // height. Stable, so forwarding them is free.
67
+ // Two of these three this platform never writes: there is no UI-thread
68
+ // scroll position and no measured row heights, because Yoga lays rows out
69
+ // at their natural height. `autoScrollDirection` IS written now see
70
+ // `autoscroll.tsx` which is why it comes in as a parameter rather than
71
+ // living in this idle bag.
86
72
  const idle = useMemo(() => ({
87
73
  scrollY: { value: 0 },
88
- autoScroll: { value: ScrollDirection.None },
74
+ scrollX: { value: 0 },
89
75
  itemHeights: { value: {} },
90
76
  }), []);
91
77
  const itemsCount = order.length;
92
78
  return useMemo(() => ({
93
79
  positions,
94
80
  ...idle,
95
- plumbing: () => ({
96
- positions,
97
- lowerBound: idle.scrollY,
98
- autoScrollDirection: idle.autoScroll,
99
- itemHeights: idle.itemHeights,
100
- itemsCount,
101
- isDynamicHeight: false,
102
- }),
103
- }), [positions, idle, itemsCount]);
81
+ plumbing: (itemWidth, gap, paddingHorizontal) => direction === SortableDirection.Horizontal
82
+ ? {
83
+ positions,
84
+ leftBound: idle.scrollX,
85
+ autoScrollHorizontalDirection: autoScrollDirection,
86
+ itemsCount,
87
+ itemWidth,
88
+ gap,
89
+ paddingHorizontal,
90
+ }
91
+ : {
92
+ positions,
93
+ lowerBound: idle.scrollY,
94
+ autoScrollDirection: autoScrollDirection,
95
+ itemHeights: idle.itemHeights,
96
+ itemsCount,
97
+ },
98
+ }), [positions, idle, itemsCount, direction, autoScrollDirection]);
99
+ };
100
+ const noneDirectionFor = (direction) => direction === SortableDirection.Horizontal
101
+ ? HorizontalScrollDirection.None
102
+ : ScrollDirection.None;
103
+ const directionForDelta = (direction) => (dx, dy) => {
104
+ if (direction === SortableDirection.Horizontal) {
105
+ if (dx < 0) {
106
+ return HorizontalScrollDirection.Left;
107
+ }
108
+ if (dx > 0) {
109
+ return HorizontalScrollDirection.Right;
110
+ }
111
+ return HorizontalScrollDirection.None;
112
+ }
113
+ if (dy < 0) {
114
+ return ScrollDirection.Up;
115
+ }
116
+ if (dy > 0) {
117
+ return ScrollDirection.Down;
118
+ }
119
+ return ScrollDirection.None;
104
120
  };
105
121
  /**
106
122
  * Owns the order of a sortable list, for an app rendering its own container.
@@ -111,16 +127,21 @@ const useSharedBoxes = (order) => {
111
127
  * `SortableItem` outside one throws rather than quietly not dragging. This
112
128
  * hook is for reading the order and the `SharedValueLike` boxes, which is
113
129
  * what most callers of it actually want.
130
+ *
131
+ * No autoscroll wiring: this hook builds no `ScrollView` of its own to drive
132
+ * one, hence the no-op `handleScroll`/`handleScrollEnd` below, unchanged from
133
+ * before. `Sortable` is where the edge autoscroll in `autoscroll.tsx` lives.
114
134
  */
115
135
  export const useSortableList = (options) => {
116
136
  const { order, items } = useOrder(options.data, options.itemKeyExtractor);
117
- const boxes = useSharedBoxes(order);
137
+ const idleAutoScroll = useMemo(() => ({ value: ScrollDirection.None }), []);
138
+ const boxes = useSharedBoxes(order, SortableDirection.Vertical, idleAutoScroll);
118
139
  const dropProviderRef = useRef(null);
119
140
  const noop = useCallback(() => { }, []);
120
141
  return {
121
142
  positions: boxes.positions,
122
143
  scrollY: boxes.scrollY,
123
- autoScroll: boxes.autoScroll,
144
+ autoScroll: idleAutoScroll,
124
145
  itemHeights: boxes.itemHeights,
125
146
  dropProviderRef,
126
147
  handleScroll: noop,
@@ -128,7 +149,53 @@ export const useSortableList = (options) => {
128
149
  contentHeight: 0,
129
150
  isDynamicHeight: false,
130
151
  items,
131
- getItemProps: boxes.plumbing,
152
+ getItemProps: () => boxes.plumbing(),
153
+ };
154
+ };
155
+ /**
156
+ * The horizontal counterpart of {@link useSortableList} — its own hook
157
+ * upstream, so its own hook here, even though the order state underneath
158
+ * (`order-state.ts`) is identical.
159
+ */
160
+ export const useHorizontalSortableList = (options) => {
161
+ const { order, items } = useOrder(options.data, options.itemKeyExtractor);
162
+ const idleAutoScroll = useMemo(() => ({
163
+ value: HorizontalScrollDirection.None,
164
+ }), []);
165
+ const boxes = useSharedBoxes(order, SortableDirection.Horizontal, idleAutoScroll);
166
+ const dropProviderRef = useRef(null);
167
+ const noop = useCallback(() => { }, []);
168
+ const { itemWidth, gap = 0, paddingHorizontal = 0, itemKeyExtractor, } = options;
169
+ const getItemProps = useCallback((item, index) => {
170
+ const plumbing = boxes.plumbing(itemWidth, gap, paddingHorizontal);
171
+ return {
172
+ id: keyOf(item, index, itemKeyExtractor),
173
+ positions: plumbing.positions,
174
+ leftBound: plumbing.leftBound,
175
+ autoScrollDirection: idleAutoScroll,
176
+ itemsCount: plumbing.itemsCount,
177
+ itemWidth: itemWidth ?? 0,
178
+ gap,
179
+ paddingHorizontal,
180
+ };
181
+ }, [
182
+ boxes,
183
+ itemWidth,
184
+ gap,
185
+ paddingHorizontal,
186
+ itemKeyExtractor,
187
+ idleAutoScroll,
188
+ ]);
189
+ return {
190
+ positions: boxes.positions,
191
+ scrollX: boxes.scrollX,
192
+ autoScroll: idleAutoScroll,
193
+ dropProviderRef,
194
+ handleScroll: noop,
195
+ handleScrollEnd: noop,
196
+ contentWidth: 0,
197
+ items,
198
+ getItemProps,
132
199
  };
133
200
  };
134
201
  /**
@@ -153,10 +220,14 @@ export const useSortableList = (options) => {
153
220
  *
154
221
  * The rows carry a real GTK drag icon — a picture of the row itself, lifted
155
222
  * at the point it was grabbed — and the list rearranges live underneath it.
223
+ * Near either end of the visible list, the drag keeps the `ScrollView`
224
+ * scrolling toward it for as long as it stays there (`autoscroll.tsx`).
156
225
  */
157
- export const Sortable = ({ data, renderItem, itemKeyExtractor, style, contentContainerStyle, testID, direction = SortableDirection.Vertical, onMove, onDragStart, onDrop, onDragging, }) => {
226
+ export const Sortable = ({ data, renderItem, itemKeyExtractor, style, contentContainerStyle, testID, direction = SortableDirection.Vertical, itemWidth, gap, paddingHorizontal, onMove, onDragStart, onDrop, onDragging, onDraggingHorizontal, }) => {
158
227
  const { scope, order, setOrder, items } = useOrder(data, itemKeyExtractor);
159
- const boxes = useSharedBoxes(order);
228
+ const isHorizontal = direction === SortableDirection.Horizontal;
229
+ const containerRef = useRef(null);
230
+ const scrollViewRef = useRef(null);
160
231
  // The order as it was when the drag began, so a cancelled drag puts the
161
232
  // list back rather than leaving it wherever the pointer happened to be.
162
233
  const beforeDrag = useRef(null);
@@ -167,10 +238,6 @@ export const Sortable = ({ data, renderItem, itemKeyExtractor, style, contentCon
167
238
  useEffect(() => {
168
239
  orderRef.current = order;
169
240
  }, [order]);
170
- const beginDrag = useCallback((draggedId) => {
171
- beforeDrag.current = orderRef.current;
172
- onDragStart?.(draggedId, orderRef.current.indexOf(draggedId));
173
- }, [onDragStart]);
174
241
  const moveOnto = useCallback((draggedId, targetId) => {
175
242
  setOrder((current) => {
176
243
  const from = current.indexOf(draggedId);
@@ -185,7 +252,71 @@ export const Sortable = ({ data, renderItem, itemKeyExtractor, style, contentCon
185
252
  return next;
186
253
  });
187
254
  }, [setOrder, onMove]);
255
+ // The dragged row's own tracked position, and the slot it last resolved
256
+ // to — null whenever no drag from this list is in flight, or `beginDrag`
257
+ // could not establish an origin (see below). A ref, not state: written
258
+ // from a GTK motion callback outside React's commit cycle, exactly like
259
+ // `beforeDrag`/`orderRef` above.
260
+ const tracking = useRef(null);
261
+ const handleDragMotion = useCallback((x, y) => {
262
+ const state = tracking.current;
263
+ if (!state) {
264
+ return;
265
+ }
266
+ const trackedPosition = (isHorizontal ? x : y) + state.base;
267
+ const resolvedIndex = resolveTrackedIndex(trackedPosition, state.slotSize, orderRef.current.length);
268
+ if (resolvedIndex === state.lastResolvedIndex) {
269
+ return;
270
+ }
271
+ state.lastResolvedIndex = resolvedIndex;
272
+ const targetId = orderRef.current[resolvedIndex];
273
+ if (targetId !== undefined && targetId !== state.draggedId) {
274
+ moveOnto(state.draggedId, targetId);
275
+ }
276
+ }, [isHorizontal, moveOnto]);
277
+ const autoscroll = useEdgeAutoscroll({
278
+ containerRef,
279
+ scrollViewRef,
280
+ axes: isHorizontal ? "horizontal" : "vertical",
281
+ none: noneDirectionFor(direction),
282
+ directionFor: directionForDelta(direction),
283
+ onDragMotion: handleDragMotion,
284
+ });
285
+ const boxes = useSharedBoxes(order, direction, autoscroll.direction);
286
+ const beginDrag = useCallback((draggedId, grabWidget, grabX, grabY) => {
287
+ beforeDrag.current = orderRef.current;
288
+ autoscroll.setActive(true);
289
+ const fromIndex = orderRef.current.indexOf(draggedId);
290
+ onDragStart?.(draggedId, fromIndex);
291
+ tracking.current = null;
292
+ const container = widgetForHandle(containerRef.current);
293
+ if (grabWidget && container && fromIndex !== -1) {
294
+ const origin = computePointIn(grabWidget, container, grabX, grabY);
295
+ if (origin) {
296
+ // The row's own real size, measured rather than a hint: rows are
297
+ // Yoga-natural-height (docs/api.md), so there is no `itemHeight`
298
+ // prop to trust — the dragged row's own allocation, at the moment
299
+ // it was grabbed, is what the tracked position's slots are sized
300
+ // to. `gap` (a real Yoga gap on the content container either
301
+ // direction) folds into the slot period the same way upstream's
302
+ // own `itemHeight` would if it accounted for one.
303
+ const rawSize = isHorizontal
304
+ ? grabWidget.getWidth()
305
+ : grabWidget.getHeight();
306
+ const slotSize = rawSize + (gap ?? 0);
307
+ const origin1D = isHorizontal ? origin.x : origin.y;
308
+ tracking.current = {
309
+ draggedId,
310
+ base: fromIndex * slotSize - origin1D,
311
+ slotSize,
312
+ lastResolvedIndex: fromIndex,
313
+ };
314
+ }
315
+ }
316
+ }, [onDragStart, autoscroll, isHorizontal, gap]);
188
317
  const endDrag = useCallback((draggedId, cancelled) => {
318
+ autoscroll.setActive(false);
319
+ tracking.current = null;
189
320
  const restore = beforeDrag.current;
190
321
  beforeDrag.current = null;
191
322
  if (cancelled) {
@@ -196,37 +327,59 @@ export const Sortable = ({ data, renderItem, itemKeyExtractor, style, contentCon
196
327
  }
197
328
  const settled = orderRef.current;
198
329
  onDrop?.(draggedId, settled.indexOf(draggedId), listToObject(settled));
199
- }, [setOrder, onDrop]);
200
- const contextValue = useMemo(() => ({ scope, moveOnto, beginDrag, endDrag, onDragging }), [scope, moveOnto, beginDrag, endDrag, onDragging]);
201
- if (direction === SortableDirection.Horizontal) {
202
- // Loud rather than silent: a horizontal list that lays out vertically is
203
- // a bug report waiting to happen. See docs/research/drag-and-drop.md for
204
- // why the horizontal surface is deferred rather than approximated.
205
- throw new Error("react-native-gtkx/dnd: SortableDirection.Horizontal is not implemented on Linux");
206
- }
207
- return (_jsx(SortableContext.Provider, { value: contextValue, children: _jsx(ScrollView, { style: style,
208
- // No `alignItems: "stretch"` override here any more. It used to be
209
- // needed because this platform's ScrollView content container
210
- // defaulted to `flex-start`, which shrank every row to its intrinsic
211
- // width and collapsed the `flex: 1` text column inside it. That
212
- // default was a parity bug — RN's content container is a plain
213
- // `View`, whose default is `stretch` — and it is fixed at the source
214
- // now (components/scroll-view.tsx), so a `Sortable` gets full-width
215
- // rows the same way any other RN list does.
216
- contentContainerStyle: contentContainerStyle, testID: testID, children: items.map((item, index) => renderItem({
217
- item,
218
- index,
219
- id: keyOf(item, index, itemKeyExtractor),
220
- direction,
221
- ...boxes.plumbing(),
222
- })) }) }));
330
+ }, [setOrder, onDrop, autoscroll]);
331
+ const contextValue = useMemo(() => ({
332
+ scope,
333
+ direction,
334
+ moveOnto,
335
+ beginDrag,
336
+ endDrag,
337
+ onDragging,
338
+ onDraggingHorizontal,
339
+ }), [
340
+ scope,
341
+ direction,
342
+ moveOnto,
343
+ beginDrag,
344
+ endDrag,
345
+ onDragging,
346
+ onDraggingHorizontal,
347
+ ]);
348
+ return (_jsx(SortableContext.Provider, { value: contextValue, children: _jsxs(View, { ref: containerRef, style: { flex: 1 }, children: [autoscroll.controllers, _jsx(ScrollView, { ref: scrollViewRef, horizontal: isHorizontal, style: style,
349
+ // No `alignItems: "stretch"` override here any more. It used to be
350
+ // needed because this platform's ScrollView content container
351
+ // defaulted to `flex-start`, which shrank every row to its intrinsic
352
+ // width and collapsed the `flex: 1` text column inside it. That
353
+ // default was a parity bug — RN's content container is a plain
354
+ // `View`, whose default `alignItems` is `stretch` — and it is fixed
355
+ // at the source now (components/scroll-view.tsx), so a `Sortable`
356
+ // gets full-width rows the same way any other RN list does.
357
+ //
358
+ // An app that wants the old behaviour writes it, exactly as it
359
+ // would on iOS and Android: `contentContainerStyle={{ alignItems:
360
+ // "flex-start" }}`.
361
+ contentContainerStyle: [
362
+ { gap, paddingHorizontal },
363
+ contentContainerStyle,
364
+ ], testID: testID, children: items.map((item, index) => renderItem({
365
+ item,
366
+ index,
367
+ id: keyOf(item, index, itemKeyExtractor),
368
+ direction,
369
+ ...boxes.plumbing(itemWidth, gap, paddingHorizontal),
370
+ })) })] }) }));
223
371
  };
224
372
  /**
225
373
  * The reorder half of the module, as a hook, for a row that owns its own
226
- * view. Render `children` inside that view.
374
+ * view.
375
+ *
376
+ * Shared by both directions: which coordinate reaches `onDragging`/
377
+ * `onDraggingHorizontal` (and which of the two fires at all) comes from the
378
+ * enclosing `Sortable`'s own `direction`, read through context — the reorder
379
+ * itself (cross into another cell's drop target) never looks at an axis.
227
380
  */
228
381
  export const useSortable = (options) => {
229
- const { id, onDragging } = options;
382
+ const { id, onDragging, onDraggingHorizontal } = options;
230
383
  const list = useContext(SortableContext);
231
384
  if (list === null) {
232
385
  // Loudly, because a silent no-op is the worst failure mode this repo has
@@ -249,27 +402,49 @@ export const useSortable = (options) => {
249
402
  // eslint-disable-next-line react-hooks/set-state-in-effect
250
403
  useEffect(() => setMounted(true), []);
251
404
  // Same list only — and INCLUDING this row itself. Refusing the self-drop
252
- // looks right and is wrong: because the reorder happens live on `::enter`,
253
- // by the time the pointer settles it is over the DRAGGED row, so a row that
254
- // refuses its own payload leaves GDK with no target, cancels the drag, and
255
- // the list snaps back to where it started. Accepting is also the honest
256
- // reading: dropping a row on its own current position means "leave it
257
- // here", which is a completed drag, not an abandoned one.
405
+ // looks right and is wrong: the reorder tracking (`Sortable`'s own
406
+ // `handleDragMotion`) can settle the dragged row back onto its OWN slot,
407
+ // so by the time the pointer releases it may well be over the DRAGGED row.
408
+ // A row that refuses its own payload leaves GDK with no target, cancels
409
+ // the drag, and the list snaps back to where it started. Accepting is also
410
+ // the honest reading: dropping a row on its own current position means
411
+ // "leave it here", which is a completed drag, not an abandoned one.
258
412
  const accepts = useCallback((payload) => payload.scope === list.scope, [list]);
259
- const dragControllers = (_jsx(DragSourceControllers, { payload: { scope: list.scope, id }, onDragBegin: () => {
413
+ // `DragSourceControllers`'s own grab point (widget-local), captured here
414
+ // and forwarded to `beginDrag` — see the module comment for why this, and
415
+ // never the first motion sample, is the reorder tracking's origin.
416
+ const grab = useRef({
417
+ widget: null,
418
+ x: 0,
419
+ y: 0,
420
+ });
421
+ const dragControllers = (_jsx(DragSourceControllers, { payload: { scope: list.scope, id }, onGrab: (x, y, widget) => {
422
+ grab.current = { widget, x, y };
423
+ }, onDragBegin: () => {
260
424
  setIsMoving(true);
261
- list.beginDrag(id);
425
+ list.beginDrag(id, grab.current.widget, grab.current.x, grab.current.y);
262
426
  }, onDragEnd: (dropped) => {
263
427
  setIsMoving(false);
264
428
  list.endDrag(id, !dropped);
265
429
  } }));
266
430
  const dropControllers = (_jsx(DropTargetControllers, { accepts: accepts,
267
- // The reorder happens HERE, not on drop: crossing this row is what
268
- // moves the dragged row into its place, so the list rearranges under
269
- // the drag icon the way upstream's animated gaps do.
270
- onEnter: (payload) => list.moveOnto(payload.id, id), onMotion: (payload, _x, y) => {
271
- list.onDragging?.(payload.id, id, y);
272
- onDragging?.(payload.id, id, y);
431
+ // No `onEnter` any more: crossing this row's drop target used to be
432
+ // what moved the dragged row into its place. That decision is now
433
+ // `Sortable`'s own `handleDragMotion`, driven by the list's shared
434
+ // motion controller rather than each row's own see the module
435
+ // comment. This target still has to exist and accept, or GDK would
436
+ // refuse the drop and snap the drag back; `onMotion` below still
437
+ // reports the public `onDragging`/`onDraggingHorizontal` callbacks,
438
+ // unrelated to the reorder decision.
439
+ onMotion: (payload, x, y) => {
440
+ if (list.direction === SortableDirection.Horizontal) {
441
+ list.onDraggingHorizontal?.(payload.id, id, x);
442
+ onDraggingHorizontal?.(payload.id, id, x);
443
+ }
444
+ else {
445
+ list.onDragging?.(payload.id, id, y);
446
+ onDragging?.(payload.id, id, y);
447
+ }
273
448
  },
274
449
  // Nothing left to do: the order is already right, and `drag-end` on the
275
450
  // source is what settles it.
@@ -283,9 +458,26 @@ export const useSortable = (options) => {
283
458
  children: (_jsxs(_Fragment, { children: [mounted && !hasHandle ? dragControllers : null, dropControllers] })),
284
459
  };
285
460
  };
461
+ /**
462
+ * The horizontal counterpart of {@link useSortable} — upstream's own separate
463
+ * hook, kept separate here too, though it is the same implementation under a
464
+ * shape matching `UseHorizontalSortableOptions`. Must be inside a
465
+ * `<Sortable direction="horizontal">`, same as `useSortable` must be inside a
466
+ * `Sortable`.
467
+ */
468
+ export const useHorizontalSortable = (options) => useSortable({
469
+ id: options.id,
470
+ data: options.data,
471
+ positions: options.positions,
472
+ itemsCount: options.itemsCount,
473
+ onMove: options.onMove,
474
+ onDragStart: options.onDragStart,
475
+ onDrop: options.onDrop,
476
+ onDraggingHorizontal: options.onDragging,
477
+ });
286
478
  /** One row of a {@link Sortable}. Both a drag source and a drop target: the
287
479
  * row you drag onto is the position the dragged row takes. */
288
- export const SortableItem = ({ id, data, children, style, animatedStyle, testID, onMove, onDragStart, onDrop, onDragging, }) => {
480
+ export const SortableItem = ({ id, data, children, style, animatedStyle, testID, onMove, onDragStart, onDrop, onDragging, onDraggingHorizontal, }) => {
289
481
  const sortable = useSortable({
290
482
  id,
291
483
  data,
@@ -293,6 +485,7 @@ export const SortableItem = ({ id, data, children, style, animatedStyle, testID,
293
485
  onDragStart,
294
486
  onDrop,
295
487
  onDragging,
488
+ onDraggingHorizontal,
296
489
  });
297
490
  // `SortableItem.Handle` is `Draggable.Handle`: the same context carries the
298
491
  // drag source to whichever view claims it, so one implementation serves