@sigx/lynx-runtime 0.4.0 → 0.4.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/dist/animated/animated-value.d.ts +2 -2
- package/dist/animated/animated-value.js +15 -0
- package/dist/animated/shared-value.d.ts +1 -1
- package/dist/animated/shared-value.js +94 -0
- package/dist/animated/use-animated-style.d.ts +3 -3
- package/dist/animated/use-animated-style.js +53 -0
- package/dist/animated-bridge.js +71 -0
- package/dist/bg-bridge.js +63 -0
- package/dist/event-registry.js +75 -0
- package/dist/flush.d.ts +1 -1
- package/dist/flush.js +8 -0
- package/dist/hmr.js +119 -39
- package/dist/index.d.ts +24 -22
- package/dist/index.js +37 -849
- package/dist/jsx.d.ts +29 -3
- package/dist/jsx.js +19 -0
- package/dist/main-thread-ref.js +134 -0
- package/dist/model-processor.js +76 -0
- package/dist/mt-hmr-bridge.js +125 -53
- package/dist/native/gesture-detector.d.ts +1 -1
- package/dist/native/gesture-detector.js +340 -0
- package/dist/native/index.d.ts +2 -2
- package/dist/native/index.js +1 -0
- package/dist/nodeOps.d.ts +1 -1
- package/dist/nodeOps.js +319 -0
- package/dist/op-queue.js +213 -0
- package/dist/render.d.ts +1 -1
- package/dist/render.js +125 -0
- package/dist/run-on-background.d.ts +1 -1
- package/dist/run-on-background.js +201 -0
- package/dist/shadow-element.js +91 -0
- package/dist/threading.d.ts +1 -1
- package/dist/threading.js +124 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +10 -0
- package/dist/use-element-layout.d.ts +72 -0
- package/dist/use-element-layout.js +40 -0
- package/package.json +10 -8
- package/dist/hmr.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mt-hmr-bridge.js.map +0 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native gesture detector — BG-side wrapper around Lynx's
|
|
3
|
+
* `__SetGestureDetector(dom, id, type, config, relationMap)` PAPI.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the contract from upstream `@lynx-js/react/runtime/lib/gesture/`:
|
|
6
|
+
* - GestureType enum values match `GestureTypeInner`.
|
|
7
|
+
* - relationMap keys: `waitFor`, `simultaneous`, `continueWith`.
|
|
8
|
+
* - COMPOSED gestures are walked client-side and each base is registered
|
|
9
|
+
* with `__SetGestureDetector` separately (the platform receives bases).
|
|
10
|
+
*
|
|
11
|
+
* Public surface:
|
|
12
|
+
* - `Gesture.Pan() / .Tap() / .LongPress() / ...` — chainable builders.
|
|
13
|
+
* - `Gesture.Race(...) / .Simultaneous(...) / .Exclusive(...)` — composers.
|
|
14
|
+
* - `useGestureDetector(elRef, gesture)` — attaches the gesture to the
|
|
15
|
+
* element pointed at by elRef. Op-emit is deferred to `onMounted` so the
|
|
16
|
+
* SET_MT_REF op (pushed during the first JSX render) is applied before
|
|
17
|
+
* the SET_GESTURE_DETECTOR op tries to resolve the workletRefMap entry.
|
|
18
|
+
*/
|
|
19
|
+
import { onMounted, onUnmounted } from '@sigx/runtime-core';
|
|
20
|
+
import { OP, pushOp, scheduleFlush } from '../op-queue.js';
|
|
21
|
+
import { registerWorkletCtx } from '../run-on-background.js';
|
|
22
|
+
import { sanitizeCaptured } from '../main-thread-ref.js';
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Gesture type enum
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
export const GestureType = {
|
|
27
|
+
COMPOSED: -1,
|
|
28
|
+
PAN: 0,
|
|
29
|
+
FLING: 1,
|
|
30
|
+
DEFAULT: 2,
|
|
31
|
+
TAP: 3,
|
|
32
|
+
LONGPRESS: 4,
|
|
33
|
+
ROTATION: 5,
|
|
34
|
+
PINCH: 6,
|
|
35
|
+
NATIVE: 7,
|
|
36
|
+
};
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Gesture id allocator (global counter — relations refer across components)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
let nextGestureId = 1;
|
|
41
|
+
export function resetGestureIdCounter() {
|
|
42
|
+
nextGestureId = 1;
|
|
43
|
+
}
|
|
44
|
+
function allocGestureId() {
|
|
45
|
+
return nextGestureId++;
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Builder base — uses polymorphic `this` so subclass-specific config setters
|
|
49
|
+
// (`PanBuilder.axis`, `LongPressBuilder.duration`, …) chain off shared
|
|
50
|
+
// callback / relation methods without losing the concrete type.
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
class GestureBuilderBase {
|
|
53
|
+
gesture;
|
|
54
|
+
constructor(type) {
|
|
55
|
+
this.gesture = {
|
|
56
|
+
__isSerialized: true,
|
|
57
|
+
type,
|
|
58
|
+
id: allocGestureId(),
|
|
59
|
+
callbacks: {},
|
|
60
|
+
waitFor: [],
|
|
61
|
+
simultaneousWith: [],
|
|
62
|
+
continueWith: [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
setConfigKey(key, value) {
|
|
66
|
+
if (!this.gesture.config)
|
|
67
|
+
this.gesture.config = {};
|
|
68
|
+
this.gesture.config[key] = value;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
onBegin(cb) {
|
|
72
|
+
this.gesture.callbacks['onBegin'] = cb;
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
onStart(cb) {
|
|
76
|
+
this.gesture.callbacks['onStart'] = cb;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
onUpdate(cb) {
|
|
80
|
+
this.gesture.callbacks['onUpdate'] = cb;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
onEnd(cb) {
|
|
84
|
+
this.gesture.callbacks['onEnd'] = cb;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
onFinalize(cb) {
|
|
88
|
+
this.gesture.callbacks['onFinalize'] = cb;
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
waitFor(...gestures) {
|
|
92
|
+
this.gesture.waitFor.push(...gestures);
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
simultaneousWith(...gestures) {
|
|
96
|
+
this.gesture.simultaneousWith.push(...gestures);
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
continueWith(...gestures) {
|
|
100
|
+
this.gesture.continueWith.push(...gestures);
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
build() {
|
|
104
|
+
return this.gesture;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Per-type builders
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
class PanBuilder extends GestureBuilderBase {
|
|
111
|
+
constructor() {
|
|
112
|
+
super(GestureType.PAN);
|
|
113
|
+
}
|
|
114
|
+
axis(a) {
|
|
115
|
+
return this.setConfigKey('axis', a);
|
|
116
|
+
}
|
|
117
|
+
minDistance(n) {
|
|
118
|
+
return this.setConfigKey('minDistance', n);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
class FlingBuilder extends GestureBuilderBase {
|
|
122
|
+
constructor() {
|
|
123
|
+
super(GestureType.FLING);
|
|
124
|
+
}
|
|
125
|
+
minVelocity(n) {
|
|
126
|
+
return this.setConfigKey('minVelocity', n);
|
|
127
|
+
}
|
|
128
|
+
direction(d) {
|
|
129
|
+
return this.setConfigKey('direction', d);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
class TapBuilder extends GestureBuilderBase {
|
|
133
|
+
constructor() {
|
|
134
|
+
super(GestureType.TAP);
|
|
135
|
+
}
|
|
136
|
+
numberOfTaps(n) {
|
|
137
|
+
return this.setConfigKey('numberOfTaps', n);
|
|
138
|
+
}
|
|
139
|
+
maxDistance(n) {
|
|
140
|
+
return this.setConfigKey('maxDistance', n);
|
|
141
|
+
}
|
|
142
|
+
maxDuration(ms) {
|
|
143
|
+
return this.setConfigKey('maxDuration', ms);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
class LongPressBuilder extends GestureBuilderBase {
|
|
147
|
+
constructor() {
|
|
148
|
+
super(GestureType.LONGPRESS);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Minimum hold duration in ms before the gesture activates and `onStart`
|
|
152
|
+
* fires. Native iOS handler (`LynxLongPressGestureHandler`) reads the
|
|
153
|
+
* `minDuration` config key — defaults to 500 ms if not set.
|
|
154
|
+
*/
|
|
155
|
+
minDuration(ms) {
|
|
156
|
+
return this.setConfigKey('minDuration', ms);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* @deprecated alias for `minDuration` kept for source compatibility. The
|
|
160
|
+
* native handler only honours `minDuration`; this method now writes both
|
|
161
|
+
* keys so older call sites keep working until they migrate.
|
|
162
|
+
*/
|
|
163
|
+
duration(ms) {
|
|
164
|
+
this.setConfigKey('duration', ms);
|
|
165
|
+
return this.setConfigKey('minDuration', ms);
|
|
166
|
+
}
|
|
167
|
+
maxDistance(n) {
|
|
168
|
+
return this.setConfigKey('maxDistance', n);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
class PinchBuilder extends GestureBuilderBase {
|
|
172
|
+
constructor() {
|
|
173
|
+
super(GestureType.PINCH);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
class RotationBuilder extends GestureBuilderBase {
|
|
177
|
+
constructor() {
|
|
178
|
+
super(GestureType.ROTATION);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
class NativeBuilder extends GestureBuilderBase {
|
|
182
|
+
constructor() {
|
|
183
|
+
super(GestureType.NATIVE);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Compose helpers
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
function asBaseGestures(g) {
|
|
190
|
+
const resolved = resolveGesture(g);
|
|
191
|
+
const out = [];
|
|
192
|
+
collectBases(resolved, out);
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
function collectBases(g, out) {
|
|
196
|
+
if (g.type === GestureType.COMPOSED) {
|
|
197
|
+
for (const sub of g.gestures)
|
|
198
|
+
collectBases(sub, out);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
out.push(g);
|
|
202
|
+
}
|
|
203
|
+
function resolveGesture(g) {
|
|
204
|
+
if (g && typeof g.build === 'function') {
|
|
205
|
+
if (g.__isSerialized !== true) {
|
|
206
|
+
return g.build();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return g;
|
|
210
|
+
}
|
|
211
|
+
function makeComposed(gestures) {
|
|
212
|
+
return {
|
|
213
|
+
__isSerialized: true,
|
|
214
|
+
type: GestureType.COMPOSED,
|
|
215
|
+
gestures,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Public Gesture namespace
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
export const Gesture = {
|
|
222
|
+
Pan: () => new PanBuilder(),
|
|
223
|
+
Fling: () => new FlingBuilder(),
|
|
224
|
+
Tap: () => new TapBuilder(),
|
|
225
|
+
LongPress: () => new LongPressBuilder(),
|
|
226
|
+
Pinch: () => new PinchBuilder(),
|
|
227
|
+
Rotation: () => new RotationBuilder(),
|
|
228
|
+
Native: () => new NativeBuilder(),
|
|
229
|
+
/**
|
|
230
|
+
* Race — first recognizer to claim wins. Sibling bases mutually waitFor
|
|
231
|
+
* each other so the platform's gesture arena resolves the priority.
|
|
232
|
+
*/
|
|
233
|
+
Race(...gs) {
|
|
234
|
+
const resolved = gs.map(resolveGesture);
|
|
235
|
+
const composed = makeComposed(resolved);
|
|
236
|
+
const bases = asBaseGestures(composed);
|
|
237
|
+
for (const a of bases) {
|
|
238
|
+
for (const b of bases) {
|
|
239
|
+
if (a !== b)
|
|
240
|
+
a.waitFor.push(b);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return composed;
|
|
244
|
+
},
|
|
245
|
+
/**
|
|
246
|
+
* Simultaneous — all recognizers can fire at once. Sibling bases declare
|
|
247
|
+
* mutual `simultaneousWith`.
|
|
248
|
+
*/
|
|
249
|
+
Simultaneous(...gs) {
|
|
250
|
+
const resolved = gs.map(resolveGesture);
|
|
251
|
+
const composed = makeComposed(resolved);
|
|
252
|
+
const bases = asBaseGestures(composed);
|
|
253
|
+
for (const a of bases) {
|
|
254
|
+
for (const b of bases) {
|
|
255
|
+
if (a !== b)
|
|
256
|
+
a.simultaneousWith.push(b);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return composed;
|
|
260
|
+
},
|
|
261
|
+
/**
|
|
262
|
+
* Exclusive — sequential. Later items waitFor all earlier items.
|
|
263
|
+
*/
|
|
264
|
+
Exclusive(...gs) {
|
|
265
|
+
const resolved = gs.map(resolveGesture);
|
|
266
|
+
const composed = makeComposed(resolved);
|
|
267
|
+
for (let i = 1; i < resolved.length; i++) {
|
|
268
|
+
const laterBases = [];
|
|
269
|
+
collectBases(resolved[i], laterBases);
|
|
270
|
+
const earlierBases = [];
|
|
271
|
+
for (let j = 0; j < i; j++)
|
|
272
|
+
collectBases(resolved[j], earlierBases);
|
|
273
|
+
for (const later of laterBases)
|
|
274
|
+
later.waitFor.push(...earlierBases);
|
|
275
|
+
}
|
|
276
|
+
return composed;
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
function appendUniqueBases(g, out, seen) {
|
|
280
|
+
if (g.type === GestureType.COMPOSED) {
|
|
281
|
+
for (const sub of g.gestures) {
|
|
282
|
+
appendUniqueBases(sub, out, seen);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const base = g;
|
|
287
|
+
if (seen.has(base.id))
|
|
288
|
+
return;
|
|
289
|
+
seen.add(base.id);
|
|
290
|
+
out.push(base);
|
|
291
|
+
}
|
|
292
|
+
function buildSerializedConfig(base) {
|
|
293
|
+
const callbacks = [];
|
|
294
|
+
for (const name in base.callbacks) {
|
|
295
|
+
const cb = base.callbacks[name];
|
|
296
|
+
// Preserve every field the SWC transform emits — the platform's gesture
|
|
297
|
+
// arena may rely on `_workletType: 'main-thread'` (and any other markers)
|
|
298
|
+
// being present at __SetGestureDetector time. We had previously stripped
|
|
299
|
+
// to `{_wkltId,_c,_jsFn}`; that lost `_workletType`, and the gesture
|
|
300
|
+
// arena silently ignored the registration on-device. Spread first, then
|
|
301
|
+
// sanitize `_c` for wire-safety (MainThreadRef instances → wire shape).
|
|
302
|
+
const wireCtx = { ...cb };
|
|
303
|
+
if (cb._c)
|
|
304
|
+
wireCtx._c = sanitizeCaptured(cb._c);
|
|
305
|
+
// Stamp _execId via registerWorkletCtx so runOnBackground inside the
|
|
306
|
+
// gesture callback can route MT→BG dispatches back through the same
|
|
307
|
+
// pipeline used by SET_WORKLET_EVENT.
|
|
308
|
+
registerWorkletCtx(wireCtx);
|
|
309
|
+
callbacks.push({ name, callback: wireCtx });
|
|
310
|
+
}
|
|
311
|
+
const out = { callbacks };
|
|
312
|
+
if (base.config)
|
|
313
|
+
out.config = base.config;
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
export function useGestureDetector(elRef, gesture) {
|
|
317
|
+
const resolved = resolveGesture(gesture);
|
|
318
|
+
const bases = [];
|
|
319
|
+
appendUniqueBases(resolved, bases, new Set());
|
|
320
|
+
if (bases.length === 0)
|
|
321
|
+
return;
|
|
322
|
+
onMounted(() => {
|
|
323
|
+
for (const base of bases) {
|
|
324
|
+
const config = buildSerializedConfig(base);
|
|
325
|
+
const relationMap = {
|
|
326
|
+
waitFor: base.waitFor.map((g) => g.id),
|
|
327
|
+
simultaneous: base.simultaneousWith.map((g) => g.id),
|
|
328
|
+
continueWith: base.continueWith.map((g) => g.id),
|
|
329
|
+
};
|
|
330
|
+
pushOp(OP.SET_GESTURE_DETECTOR, elRef._wvid, base.id, base.type, config, relationMap);
|
|
331
|
+
}
|
|
332
|
+
scheduleFlush();
|
|
333
|
+
});
|
|
334
|
+
onUnmounted(() => {
|
|
335
|
+
for (const base of bases) {
|
|
336
|
+
pushOp(OP.REMOVE_GESTURE_DETECTOR, elRef._wvid, base.id);
|
|
337
|
+
}
|
|
338
|
+
scheduleFlush();
|
|
339
|
+
});
|
|
340
|
+
}
|
package/dist/native/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Gesture, GestureType, useGestureDetector, resetGestureIdCounter, } from './gesture-detector';
|
|
2
|
-
export type { GestureTypeValue, GestureWorklet, GestureCallback, BaseGesture, ComposedGesture, AnyGesture, } from './gesture-detector';
|
|
1
|
+
export { Gesture, GestureType, useGestureDetector, resetGestureIdCounter, } from './gesture-detector.js';
|
|
2
|
+
export type { GestureTypeValue, GestureWorklet, GestureCallback, BaseGesture, ComposedGesture, AnyGesture, } from './gesture-detector.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Gesture, GestureType, useGestureDetector, resetGestureIdCounter, } from './gesture-detector.js';
|
package/dist/nodeOps.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* sigxPatchUpdate, where ops-apply.ts dispatches them to real PAPI calls.
|
|
8
8
|
*/
|
|
9
9
|
import type { RendererOptions } from '@sigx/runtime-core/internals';
|
|
10
|
-
import { ShadowElement } from './shadow-element';
|
|
10
|
+
import { ShadowElement } from './shadow-element.js';
|
|
11
11
|
export type LynxNode = ShadowElement;
|
|
12
12
|
export type LynxElement = ShadowElement;
|
|
13
13
|
export declare function resolveClass(el: ShadowElement): string;
|
package/dist/nodeOps.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { OP, pushOp, scheduleFlush } from './op-queue.js';
|
|
2
|
+
import { register, unregister } from './event-registry.js';
|
|
3
|
+
import { ShadowElement } from './shadow-element.js';
|
|
4
|
+
import { registerWorkletCtx } from './run-on-background.js';
|
|
5
|
+
function parseEventProp(key) {
|
|
6
|
+
// Main-thread event prefixes: main-thread-bind*, main-thread-catch*
|
|
7
|
+
if (key.startsWith('main-thread-bind')) {
|
|
8
|
+
return { type: 'bindEvent', name: key.slice('main-thread-bind'.length), mainThread: true };
|
|
9
|
+
}
|
|
10
|
+
if (key.startsWith('main-thread-catch')) {
|
|
11
|
+
return { type: 'catchEvent', name: key.slice('main-thread-catch'.length), mainThread: true };
|
|
12
|
+
}
|
|
13
|
+
// Alternative syntax: main-thread:bind*, main-thread:catch*
|
|
14
|
+
if (key.startsWith('main-thread:bind')) {
|
|
15
|
+
return { type: 'bindEvent', name: key.slice('main-thread:bind'.length), mainThread: true };
|
|
16
|
+
}
|
|
17
|
+
if (key.startsWith('main-thread:catch')) {
|
|
18
|
+
return { type: 'catchEvent', name: key.slice('main-thread:catch'.length), mainThread: true };
|
|
19
|
+
}
|
|
20
|
+
if (key.startsWith('global-bind')) {
|
|
21
|
+
return { type: 'bindGlobalEvent', name: key.slice('global-bind'.length) };
|
|
22
|
+
}
|
|
23
|
+
if (key.startsWith('global-catch')) {
|
|
24
|
+
return { type: 'catchGlobalEvent', name: key.slice('global-catch'.length) };
|
|
25
|
+
}
|
|
26
|
+
if (key.startsWith('catch')) {
|
|
27
|
+
return { type: 'catchEvent', name: key.slice('catch'.length) };
|
|
28
|
+
}
|
|
29
|
+
if (/^bind(?!ingx)/.test(key)) {
|
|
30
|
+
return { type: 'bindEvent', name: key.slice('bind'.length) };
|
|
31
|
+
}
|
|
32
|
+
if (/^on[A-Z]/.test(key)) {
|
|
33
|
+
// onTap → { type: 'bindEvent', name: 'tap' }
|
|
34
|
+
const name = key.slice(2, 3).toLowerCase() + key.slice(3);
|
|
35
|
+
return { type: 'bindEvent', name };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Worklet placeholder detection — @lynx-js/react/transform replaces
|
|
41
|
+
// 'main thread' functions with { _wkltId, _c? } placeholders in the BG bundle.
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
import { sanitizeCaptured } from './main-thread-ref.js';
|
|
44
|
+
function isWorkletPlaceholder(v) {
|
|
45
|
+
return typeof v === 'object' && v !== null && '_wkltId' in v;
|
|
46
|
+
}
|
|
47
|
+
// Track sent worklet ids per (elementId, propKey) to skip redundant ops on re-render.
|
|
48
|
+
const sentWorklets = new Map();
|
|
49
|
+
// Track the sign registered for each (element, propKey) so we can unregister
|
|
50
|
+
// on prop removal / update.
|
|
51
|
+
const elementEventSigns = new Map();
|
|
52
|
+
// Track which sign owns the native event slot per (elementId, 'eventType:eventName').
|
|
53
|
+
// Lynx only supports one __AddEvent per (element, eventType, eventName). When multiple
|
|
54
|
+
// props resolve to the same native event (e.g. main-thread-bindtap + bindtap both map
|
|
55
|
+
// to bindEvent:tap), we keep one sign in __AddEvent and dispatch to all handlers from
|
|
56
|
+
// a multi-handler wrapper in the BG registry.
|
|
57
|
+
const nativeEventSlots = new Map();
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Style normalisation — numeric values → 'Npx' (Lynx requires units)
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
const DIMENSIONLESS = new Set([
|
|
62
|
+
'flex',
|
|
63
|
+
'flexGrow',
|
|
64
|
+
'flexShrink',
|
|
65
|
+
'flexOrder',
|
|
66
|
+
'order',
|
|
67
|
+
'opacity',
|
|
68
|
+
'zIndex',
|
|
69
|
+
'aspectRatio',
|
|
70
|
+
'fontWeight',
|
|
71
|
+
'lineClamp',
|
|
72
|
+
]);
|
|
73
|
+
function normalizeStyle(style) {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const key of Object.keys(style)) {
|
|
76
|
+
const val = style[key];
|
|
77
|
+
if (typeof val === 'number' && !DIMENSIONLESS.has(key) && val !== 0) {
|
|
78
|
+
out[key] = `${val}px`;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
out[key] = val;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
function shallowEqual(a, b) {
|
|
87
|
+
const ka = Object.keys(a);
|
|
88
|
+
const kb = Object.keys(b);
|
|
89
|
+
if (ka.length !== kb.length)
|
|
90
|
+
return false;
|
|
91
|
+
for (const k of ka) {
|
|
92
|
+
if (a[k] !== b[k])
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Class resolution — merges user :class with transition classes
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
export function resolveClass(el) {
|
|
101
|
+
if (el._transitionClasses.size === 0)
|
|
102
|
+
return el._baseClass;
|
|
103
|
+
const parts = [];
|
|
104
|
+
if (el._baseClass)
|
|
105
|
+
parts.push(el._baseClass);
|
|
106
|
+
for (const cls of el._transitionClasses)
|
|
107
|
+
parts.push(cls);
|
|
108
|
+
return parts.join(' ');
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// RendererOptions implementation
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
export const nodeOps = {
|
|
114
|
+
createElement(type) {
|
|
115
|
+
const el = new ShadowElement(type);
|
|
116
|
+
pushOp(OP.CREATE, el.id, type);
|
|
117
|
+
scheduleFlush();
|
|
118
|
+
return el;
|
|
119
|
+
},
|
|
120
|
+
createText(text) {
|
|
121
|
+
const el = new ShadowElement('#text');
|
|
122
|
+
pushOp(OP.CREATE_TEXT, el.id);
|
|
123
|
+
if (text)
|
|
124
|
+
pushOp(OP.SET_TEXT, el.id, text);
|
|
125
|
+
scheduleFlush();
|
|
126
|
+
return el;
|
|
127
|
+
},
|
|
128
|
+
// Comment nodes are used as position anchors for conditionals / Fragment.
|
|
129
|
+
// Materialised as invisible placeholder elements on the Main Thread.
|
|
130
|
+
createComment(_text) {
|
|
131
|
+
const el = new ShadowElement('#comment');
|
|
132
|
+
pushOp(OP.CREATE, el.id, '__comment');
|
|
133
|
+
scheduleFlush();
|
|
134
|
+
return el;
|
|
135
|
+
},
|
|
136
|
+
setText(node, text) {
|
|
137
|
+
pushOp(OP.SET_TEXT, node.id, text);
|
|
138
|
+
scheduleFlush();
|
|
139
|
+
},
|
|
140
|
+
setElementText(el, text) {
|
|
141
|
+
// Remove all children from shadow tree
|
|
142
|
+
while (el.firstChild) {
|
|
143
|
+
const child = el.firstChild;
|
|
144
|
+
el.removeChild(child);
|
|
145
|
+
pushOp(OP.REMOVE, el.id, child.id);
|
|
146
|
+
}
|
|
147
|
+
// Set text content directly on the element
|
|
148
|
+
pushOp(OP.SET_TEXT, el.id, text);
|
|
149
|
+
scheduleFlush();
|
|
150
|
+
},
|
|
151
|
+
insert(child, parent, anchor) {
|
|
152
|
+
// Always update the shadow tree (the core renderer needs sync tree queries).
|
|
153
|
+
parent.insertBefore(child, anchor ?? null);
|
|
154
|
+
// Lynx's native <list> only accepts <list-item> children.
|
|
155
|
+
// Skip comment/text anchors to avoid NSInvalidArgumentException.
|
|
156
|
+
if (parent.type === 'list'
|
|
157
|
+
&& (child.type === '#comment' || child.type === '#text')) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// If the anchor is a comment node inside a <list>, walk forward to find
|
|
161
|
+
// the next real sibling so the MT __InsertElementBefore has a valid ref.
|
|
162
|
+
let resolvedAnchor = anchor ?? null;
|
|
163
|
+
if (parent.type === 'list') {
|
|
164
|
+
while (resolvedAnchor
|
|
165
|
+
&& (resolvedAnchor.type === '#comment'
|
|
166
|
+
|| resolvedAnchor.type === '#text')) {
|
|
167
|
+
resolvedAnchor = resolvedAnchor.next;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const anchorId = resolvedAnchor ? resolvedAnchor.id : -1;
|
|
171
|
+
pushOp(OP.INSERT, parent.id, child.id, anchorId);
|
|
172
|
+
scheduleFlush();
|
|
173
|
+
},
|
|
174
|
+
remove(child) {
|
|
175
|
+
if (child.parent) {
|
|
176
|
+
const parentId = child.parent.id;
|
|
177
|
+
child.parent.removeChild(child);
|
|
178
|
+
pushOp(OP.REMOVE, parentId, child.id);
|
|
179
|
+
scheduleFlush();
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
patchProp(el, key, _prevValue, nextValue) {
|
|
183
|
+
// Handle main-thread:ref — bind a MainThreadRef to this element
|
|
184
|
+
if (key === 'main-thread:ref') {
|
|
185
|
+
if (nextValue != null) {
|
|
186
|
+
const mtRef = nextValue;
|
|
187
|
+
pushOp(OP.SET_MT_REF, el.id, mtRef._wvid);
|
|
188
|
+
}
|
|
189
|
+
scheduleFlush();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const event = parseEventProp(key);
|
|
193
|
+
if (event) {
|
|
194
|
+
// Worklet placeholders ({ _wkltId, _c? }) emitted by @lynx-js/react/transform
|
|
195
|
+
// bypass the BG event-registry path entirely — the MT side dispatches.
|
|
196
|
+
if (event.mainThread && nextValue != null && isWorkletPlaceholder(nextValue)) {
|
|
197
|
+
let elWorklets = sentWorklets.get(el.id);
|
|
198
|
+
const prevId = elWorklets?.get(key);
|
|
199
|
+
if (prevId !== nextValue._wkltId) {
|
|
200
|
+
if (!elWorklets) {
|
|
201
|
+
elWorklets = new Map();
|
|
202
|
+
sentWorklets.set(el.id, elWorklets);
|
|
203
|
+
}
|
|
204
|
+
elWorklets.set(key, nextValue._wkltId);
|
|
205
|
+
const wireCtx = {
|
|
206
|
+
_wkltId: nextValue._wkltId,
|
|
207
|
+
};
|
|
208
|
+
if (nextValue._c)
|
|
209
|
+
wireCtx._c = sanitizeCaptured(nextValue._c);
|
|
210
|
+
if (nextValue._jsFn)
|
|
211
|
+
wireCtx._jsFn = nextValue._jsFn;
|
|
212
|
+
// Stamp _execId so MT can route runOnBackground dispatches back
|
|
213
|
+
// to the JsFnHandles inside _jsFn / _c.
|
|
214
|
+
registerWorkletCtx(wireCtx);
|
|
215
|
+
pushOp(OP.SET_WORKLET_EVENT, el.id, event.type, event.name, wireCtx);
|
|
216
|
+
scheduleFlush();
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
let propSigns = elementEventSigns.get(el.id);
|
|
221
|
+
const nativeKey = `${event.type}:${event.name}`;
|
|
222
|
+
if (nextValue != null) {
|
|
223
|
+
const handler = nextValue;
|
|
224
|
+
// Get or create the native event slot for this (element, eventType, eventName).
|
|
225
|
+
let elSlots = nativeEventSlots.get(el.id);
|
|
226
|
+
if (!elSlots) {
|
|
227
|
+
elSlots = new Map();
|
|
228
|
+
nativeEventSlots.set(el.id, elSlots);
|
|
229
|
+
}
|
|
230
|
+
let slot = elSlots.get(nativeKey);
|
|
231
|
+
if (!slot) {
|
|
232
|
+
// First handler for this native event — register with Lynx.
|
|
233
|
+
const sign = register((data) => {
|
|
234
|
+
// Dispatch to all handlers registered for this slot.
|
|
235
|
+
const s = elSlots.get(nativeKey);
|
|
236
|
+
if (s) {
|
|
237
|
+
for (const h of s.handlers.values())
|
|
238
|
+
h(data);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
slot = { sign, handlers: new Map() };
|
|
242
|
+
elSlots.set(nativeKey, slot);
|
|
243
|
+
pushOp(OP.SET_EVENT, el.id, event.type, event.name, sign);
|
|
244
|
+
}
|
|
245
|
+
// Add/update this prop's handler in the slot.
|
|
246
|
+
slot.handlers.set(key, handler);
|
|
247
|
+
// Track prop→sign for re-render updates.
|
|
248
|
+
if (!propSigns) {
|
|
249
|
+
propSigns = new Map();
|
|
250
|
+
elementEventSigns.set(el.id, propSigns);
|
|
251
|
+
}
|
|
252
|
+
propSigns.set(key, slot.sign);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// Handler removed.
|
|
256
|
+
const elSlots = nativeEventSlots.get(el.id);
|
|
257
|
+
const slot = elSlots?.get(nativeKey);
|
|
258
|
+
if (slot) {
|
|
259
|
+
slot.handlers.delete(key);
|
|
260
|
+
if (slot.handlers.size === 0) {
|
|
261
|
+
// No more handlers — unregister from Lynx.
|
|
262
|
+
unregister(slot.sign);
|
|
263
|
+
elSlots.delete(nativeKey);
|
|
264
|
+
pushOp(OP.REMOVE_EVENT, el.id, event.type, event.name);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
propSigns?.delete(key);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else if (key === 'style') {
|
|
271
|
+
const style = nextValue != null && typeof nextValue === 'object'
|
|
272
|
+
? normalizeStyle(nextValue)
|
|
273
|
+
: {};
|
|
274
|
+
const effective = el._vShowHidden ? { ...style, display: 'none' } : style;
|
|
275
|
+
// Skip SET_STYLE when structurally unchanged. JSX inline `style={{...}}`
|
|
276
|
+
// creates a fresh object every render but its keys/values typically don't
|
|
277
|
+
// change between renders. Re-emitting SET_STYLE would overwrite any MT
|
|
278
|
+
// worklet's setStyleProperties calls (pink/scale on tap, etc.) on every
|
|
279
|
+
// unrelated signal change. Shallow-equal previous _style suffices since
|
|
280
|
+
// sigx normalises everything to a flat string→string|number map.
|
|
281
|
+
const prev = el._style;
|
|
282
|
+
const sameStyle = prev != null && shallowEqual(prev, effective);
|
|
283
|
+
el._style = style;
|
|
284
|
+
if (!sameStyle) {
|
|
285
|
+
pushOp(OP.SET_STYLE, el.id, effective);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else if (key === 'class') {
|
|
289
|
+
el._baseClass = nextValue ?? '';
|
|
290
|
+
const finalClass = resolveClass(el);
|
|
291
|
+
pushOp(OP.SET_CLASS, el.id, finalClass);
|
|
292
|
+
}
|
|
293
|
+
else if (key === 'id') {
|
|
294
|
+
pushOp(OP.SET_ID, el.id, nextValue);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
pushOp(OP.SET_PROP, el.id, key, nextValue);
|
|
298
|
+
}
|
|
299
|
+
scheduleFlush();
|
|
300
|
+
},
|
|
301
|
+
parentNode(node) {
|
|
302
|
+
return node.parent;
|
|
303
|
+
},
|
|
304
|
+
nextSibling(node) {
|
|
305
|
+
return node.next;
|
|
306
|
+
},
|
|
307
|
+
cloneNode(node) {
|
|
308
|
+
// Lynx has no native clone — create a new shadow element of the same type
|
|
309
|
+
const el = new ShadowElement(node.type);
|
|
310
|
+
if (node.type === '#text') {
|
|
311
|
+
pushOp(OP.CREATE_TEXT, el.id);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
pushOp(OP.CREATE, el.id, node.type);
|
|
315
|
+
}
|
|
316
|
+
scheduleFlush();
|
|
317
|
+
return el;
|
|
318
|
+
},
|
|
319
|
+
};
|