embedded-react 0.1.0
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/LICENSE +201 -0
- package/NOTICE +58 -0
- package/README.md +224 -0
- package/aot/compile.mjs +3066 -0
- package/aot/screenshot-smoke.mjs +110 -0
- package/aot/style-map.mjs +248 -0
- package/assets/bake-font.mjs +190 -0
- package/assets/bake-image.mjs +50 -0
- package/assets/build-builtin-font.mjs +51 -0
- package/assets/emit-c.mjs +187 -0
- package/assets/emit-container.mjs +121 -0
- package/assets/emit-pack.mjs +128 -0
- package/assets/index.mjs +72 -0
- package/assets/rasterize.mjs +169 -0
- package/build.mjs +136 -0
- package/pack-container.mjs +161 -0
- package/package.json +79 -0
- package/persist-transform.mjs +106 -0
- package/src/embedded-react/Animated.js +352 -0
- package/src/embedded-react/AppRegistry.js +49 -0
- package/src/embedded-react/Easing.js +39 -0
- package/src/embedded-react/LayoutAnimation.js +45 -0
- package/src/embedded-react/Platform.js +26 -0
- package/src/embedded-react/StyleSheet.js +36 -0
- package/src/embedded-react/components.js +44 -0
- package/src/embedded-react/imperative.js +68 -0
- package/src/embedded-react/index.js +52 -0
- package/src/embedded-react/layout-anim-config.js +91 -0
- package/src/embedded-react/split-style.js +58 -0
- package/src/embedded-react/svg-ops.js +564 -0
- package/src/embedded-react/usePersistentState.js +69 -0
- package/src/host-config.js +196 -0
- package/src/native-ui.js +24 -0
- package/src/props.js +183 -0
- package/src/renderer.js +57 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Animated — the React Native analog, backed by the engine's native-driver value system
|
|
18
|
+
// (er_anim_value_*). An Animated.Value is a handle to an engine-side float; binding it to a node
|
|
19
|
+
// prop (via Animated.View) lets the engine advance the animation each frame with NO per-frame JS.
|
|
20
|
+
import { createElement, useRef, useEffect } from 'react';
|
|
21
|
+
import { NativeUI } from '../native-ui.js';
|
|
22
|
+
import { splitAnimatedStyle } from './split-style.js';
|
|
23
|
+
|
|
24
|
+
/** A standalone animatable value bound to an engine-side float. */
|
|
25
|
+
export class AnimatedValue {
|
|
26
|
+
constructor(initial = 0) {
|
|
27
|
+
this.__animated = true;
|
|
28
|
+
this._handle = NativeUI.animValueCreate(initial);
|
|
29
|
+
this._value = initial;
|
|
30
|
+
this._destroyed = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setValue(v) {
|
|
34
|
+
this._value = v;
|
|
35
|
+
NativeUI.animValueSet(this._handle, v);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Releases the engine-side value slot. The engine's value pool is fixed-size (unlike RN, where the
|
|
40
|
+
* JS value is simply garbage-collected), so a value tied to a mounting/unmounting component must be
|
|
41
|
+
* freed explicitly — see useAnimatedValue. Idempotent; the value must not be used after destroy().
|
|
42
|
+
*/
|
|
43
|
+
destroy() {
|
|
44
|
+
if (this._destroyed) return;
|
|
45
|
+
this._destroyed = true;
|
|
46
|
+
NativeUI.animValueDestroy(this._handle);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
__getValue() {
|
|
50
|
+
return NativeUI.animValueGet(this._handle);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
__bind(node, prop) {
|
|
54
|
+
// The engine applies the current value on bind and ignores duplicate (node, prop) pairs, so
|
|
55
|
+
// this is safe to call on every render.
|
|
56
|
+
NativeUI.animValueBind(this._handle, node, prop);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interpolate(config) {
|
|
60
|
+
return new AnimatedInterpolation(this, config);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A value derived from a parent Animated.Value through a piecewise-linear interpolation.
|
|
66
|
+
* config = { inputRange, outputRange, extrapolate?, extrapolateLeft?, extrapolateRight? }; the
|
|
67
|
+
* extrapolate tokens ('extend' | 'clamp' | 'identity') control behavior outside the input range.
|
|
68
|
+
*/
|
|
69
|
+
export class AnimatedInterpolation {
|
|
70
|
+
constructor(parent, config) {
|
|
71
|
+
this.__animated = true;
|
|
72
|
+
this._parent = parent;
|
|
73
|
+
this._config = config;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
__bind(node, prop) {
|
|
77
|
+
NativeUI.animValueBindInterpolated(this._parent._handle, node, prop, this._config);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interpolate(config) {
|
|
81
|
+
// Chained interpolation isn't supported by the engine; re-map against the same parent.
|
|
82
|
+
return new AnimatedInterpolation(this._parent, config);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Wraps fn so it runs at most once, regardless of how many times it is invoked. */
|
|
87
|
+
function once(fn) {
|
|
88
|
+
let called = false;
|
|
89
|
+
return (arg) => {
|
|
90
|
+
if (called) return;
|
|
91
|
+
called = true;
|
|
92
|
+
if (fn) fn(arg);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Wraps a NativeUI animation call in an object with start()/stop() (RN's animation handle shape).
|
|
98
|
+
*
|
|
99
|
+
* start(onComplete) wires the engine's completion callback through the bridge: onComplete is called
|
|
100
|
+
* with `{ finished }` when the animation ends naturally (finished: true) or is interrupted by a new
|
|
101
|
+
* animation / stop() (finished: false). `_value` is exposed so Animated.loop can reset it between
|
|
102
|
+
* iterations.
|
|
103
|
+
*/
|
|
104
|
+
function makeAnimation(value, toValue, config) {
|
|
105
|
+
let handle = 0;
|
|
106
|
+
return {
|
|
107
|
+
_value: value,
|
|
108
|
+
start(onComplete) {
|
|
109
|
+
// Always pass a wrapper so we can null out the (possibly recycled) handle on completion.
|
|
110
|
+
const cb = (finished) => {
|
|
111
|
+
handle = 0;
|
|
112
|
+
if (onComplete) onComplete({ finished: !!finished });
|
|
113
|
+
};
|
|
114
|
+
handle = NativeUI.animValueAnimate(value._handle, toValue, config, cb);
|
|
115
|
+
},
|
|
116
|
+
stop() {
|
|
117
|
+
if (handle) NativeUI.animStop(handle);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function timing(value, config = {}) {
|
|
123
|
+
const { toValue = 0, useNativeDriver, ...rest } = config;
|
|
124
|
+
return makeAnimation(value, toValue, { type: 'timing', ...rest });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function spring(value, config = {}) {
|
|
128
|
+
const { toValue = 0, useNativeDriver, ...rest } = config;
|
|
129
|
+
return makeAnimation(value, toValue, { type: 'spring', ...rest });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function decay(value, config = {}) {
|
|
133
|
+
const { useNativeDriver, ...rest } = config;
|
|
134
|
+
// Decay coasts from its velocity; the engine ignores the target, so pass the current value.
|
|
135
|
+
return makeAnimation(value, value.__getValue(), { type: 'decay', ...rest });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Composition -------------------------------------------------------------------------------
|
|
139
|
+
// These are pure JS over each child animation's start()/stop() + the §2 timer globals. The engine
|
|
140
|
+
// deactivates a finished animation before firing its completion callback, so chaining the next
|
|
141
|
+
// animation from inside a completion handler is safe (no per-frame JS — each child still runs in C).
|
|
142
|
+
|
|
143
|
+
/** Runs animations one after another; each starts when the previous finishes. */
|
|
144
|
+
export function sequence(animations) {
|
|
145
|
+
let current = 0;
|
|
146
|
+
let stopped = false;
|
|
147
|
+
return {
|
|
148
|
+
start(onComplete) {
|
|
149
|
+
const done = once(onComplete);
|
|
150
|
+
const next = (result) => {
|
|
151
|
+
if (stopped || !result || result.finished === false) {
|
|
152
|
+
done({ finished: false });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (current >= animations.length) {
|
|
156
|
+
done({ finished: true });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
animations[current++].start(next);
|
|
160
|
+
};
|
|
161
|
+
next({ finished: true }); // kick off the first entry
|
|
162
|
+
},
|
|
163
|
+
stop() {
|
|
164
|
+
stopped = true;
|
|
165
|
+
if (current > 0 && current <= animations.length) animations[current - 1].stop();
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Runs animations simultaneously; completes when all finish. stopTogether (default true). */
|
|
171
|
+
export function parallel(animations, config) {
|
|
172
|
+
const stopTogether = !config || config.stopTogether !== false;
|
|
173
|
+
let stopped = false;
|
|
174
|
+
return {
|
|
175
|
+
start(onComplete) {
|
|
176
|
+
const done = once(onComplete);
|
|
177
|
+
const total = animations.length;
|
|
178
|
+
if (total === 0) {
|
|
179
|
+
done({ finished: true });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
let doneCount = 0;
|
|
183
|
+
let anyUnfinished = false;
|
|
184
|
+
const onChild = (result) => {
|
|
185
|
+
doneCount++;
|
|
186
|
+
if (!result || result.finished === false) {
|
|
187
|
+
anyUnfinished = true;
|
|
188
|
+
if (stopTogether && !stopped) {
|
|
189
|
+
stopped = true;
|
|
190
|
+
for (const a of animations) a.stop();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (doneCount >= total) done({ finished: !anyUnfinished });
|
|
194
|
+
};
|
|
195
|
+
for (const a of animations) a.start(onChild);
|
|
196
|
+
},
|
|
197
|
+
stop() {
|
|
198
|
+
stopped = true;
|
|
199
|
+
for (const a of animations) a.stop();
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Like parallel, but each animation starts `delay` ms after the previous one (uses setTimeout). */
|
|
205
|
+
export function stagger(delay, animations) {
|
|
206
|
+
let stopped = false;
|
|
207
|
+
const timers = [];
|
|
208
|
+
return {
|
|
209
|
+
start(onComplete) {
|
|
210
|
+
const done = once(onComplete);
|
|
211
|
+
const total = animations.length;
|
|
212
|
+
if (total === 0) {
|
|
213
|
+
done({ finished: true });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
let doneCount = 0;
|
|
217
|
+
let anyUnfinished = false;
|
|
218
|
+
const onChild = (result) => {
|
|
219
|
+
doneCount++;
|
|
220
|
+
if (!result || result.finished === false) anyUnfinished = true;
|
|
221
|
+
if (doneCount >= total) done({ finished: !anyUnfinished });
|
|
222
|
+
};
|
|
223
|
+
animations.forEach((a, i) => {
|
|
224
|
+
if (i === 0) {
|
|
225
|
+
a.start(onChild);
|
|
226
|
+
} else {
|
|
227
|
+
timers.push(
|
|
228
|
+
setTimeout(() => {
|
|
229
|
+
if (!stopped) a.start(onChild);
|
|
230
|
+
}, delay * i),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
stop() {
|
|
236
|
+
stopped = true;
|
|
237
|
+
for (const t of timers) clearTimeout(t);
|
|
238
|
+
for (const a of animations) a.stop();
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** An animation that simply waits `time` ms — useful as a spacer inside sequence (uses setTimeout). */
|
|
244
|
+
export function delay(time) {
|
|
245
|
+
let timer = 0;
|
|
246
|
+
return {
|
|
247
|
+
start(onComplete) {
|
|
248
|
+
const done = once(onComplete);
|
|
249
|
+
timer = setTimeout(() => {
|
|
250
|
+
timer = 0;
|
|
251
|
+
done({ finished: true });
|
|
252
|
+
}, time);
|
|
253
|
+
},
|
|
254
|
+
stop() {
|
|
255
|
+
if (timer) {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
timer = 0;
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Repeats an animation. iterations: -1 (default) loops forever; otherwise that many times.
|
|
265
|
+
* By default the underlying value is reset to its loop-start position before each iteration
|
|
266
|
+
* (resetBeforeIteration), matching RN — so a timing 0→1 repeats 0→1 rather than ping-ponging.
|
|
267
|
+
*/
|
|
268
|
+
export function loop(animation, config) {
|
|
269
|
+
const iterations = config && Number.isInteger(config.iterations) ? config.iterations : -1;
|
|
270
|
+
const resetBeforeIteration = !config || config.resetBeforeIteration !== false;
|
|
271
|
+
let stopped = false;
|
|
272
|
+
return {
|
|
273
|
+
_value: animation._value,
|
|
274
|
+
start(onComplete) {
|
|
275
|
+
const done = once(onComplete);
|
|
276
|
+
const startValue = resetBeforeIteration && animation._value ? animation._value.__getValue() : null;
|
|
277
|
+
let count = 0;
|
|
278
|
+
const run = (result) => {
|
|
279
|
+
if (stopped || !result || result.finished === false) {
|
|
280
|
+
done({ finished: false });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (iterations >= 0 && count >= iterations) {
|
|
284
|
+
done({ finished: true });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
count++;
|
|
288
|
+
if (resetBeforeIteration && animation._value && startValue != null && count > 1) {
|
|
289
|
+
animation._value.setValue(startValue);
|
|
290
|
+
}
|
|
291
|
+
animation.start(run);
|
|
292
|
+
};
|
|
293
|
+
run({ finished: true });
|
|
294
|
+
},
|
|
295
|
+
stop() {
|
|
296
|
+
stopped = true;
|
|
297
|
+
animation.stop();
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Returns a stable Animated.Value that persists across renders (the analog of
|
|
304
|
+
* `useRef(new Animated.Value(initial)).current`), and frees the engine-side value slot when the
|
|
305
|
+
* component unmounts. The unmount cleanup matters here in a way it doesn't in React Native: the
|
|
306
|
+
* engine's value pool is fixed-size, so a value owned by a mounting/unmounting component (e.g., a list
|
|
307
|
+
* row or a tab screen) would otherwise leak a slot on every unmount.
|
|
308
|
+
*/
|
|
309
|
+
export function useAnimatedValue(initial = 0) {
|
|
310
|
+
const ref = useRef(null);
|
|
311
|
+
if (ref.current == null) {
|
|
312
|
+
ref.current = new AnimatedValue(initial);
|
|
313
|
+
}
|
|
314
|
+
// Empty deps → the cleanup runs only on unmount. `initial` is read once on the first render (matching
|
|
315
|
+
// useRef semantics); later changes don't recreate the value, as in React Native.
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
const value = ref.current;
|
|
318
|
+
return () => value.destroy();
|
|
319
|
+
}, []);
|
|
320
|
+
return ref.current;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Wraps a host component so animated values in its `style` are bound to the node (native driver).
|
|
325
|
+
*/
|
|
326
|
+
export function createAnimatedComponent(Component) {
|
|
327
|
+
return function AnimatedComponent(props) {
|
|
328
|
+
const { style, ...rest } = props;
|
|
329
|
+
const { staticStyle, bindings } = splitAnimatedStyle(style);
|
|
330
|
+
const ref = (node) => {
|
|
331
|
+
if (node == null) return; // unmount
|
|
332
|
+
for (const b of bindings) b.value.__bind(node, b.prop);
|
|
333
|
+
};
|
|
334
|
+
return createElement(Component, { ...rest, style: staticStyle, ref });
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export const Animated = {
|
|
339
|
+
Value: AnimatedValue,
|
|
340
|
+
View: createAnimatedComponent('View'),
|
|
341
|
+
Text: createAnimatedComponent('Text'),
|
|
342
|
+
Image: createAnimatedComponent('Image'),
|
|
343
|
+
timing,
|
|
344
|
+
spring,
|
|
345
|
+
decay,
|
|
346
|
+
sequence,
|
|
347
|
+
parallel,
|
|
348
|
+
stagger,
|
|
349
|
+
delay,
|
|
350
|
+
loop,
|
|
351
|
+
createAnimatedComponent,
|
|
352
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// AppRegistry — the RN entry point. An app registers a root component; the runtime mounts it into
|
|
18
|
+
// a screen-sized container.
|
|
19
|
+
//
|
|
20
|
+
// In React Native the native side calls runApplication when the activity starts. Our host model is
|
|
21
|
+
// "running the bundle IS starting the app", so registerComponent mounts immediately into a root
|
|
22
|
+
// sized from the host-injected `screen` global. (A host-driven runApplication can be split out
|
|
23
|
+
// later when the C host owns app lifecycle.)
|
|
24
|
+
import { createElement } from 'react';
|
|
25
|
+
import { createRoot } from '../renderer.js';
|
|
26
|
+
|
|
27
|
+
let registered = null;
|
|
28
|
+
|
|
29
|
+
export const AppRegistry = {
|
|
30
|
+
/**
|
|
31
|
+
* Registers a root component and mounts it. `componentProvider` is a zero-arg function returning
|
|
32
|
+
* the component (RN signature), so the component module isn't evaluated until registration.
|
|
33
|
+
*/
|
|
34
|
+
registerComponent(appKey, componentProvider) {
|
|
35
|
+
registered = { appKey, Component: componentProvider() };
|
|
36
|
+
AppRegistry.runApplication(appKey);
|
|
37
|
+
return appKey;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Mounts the registered root component into a fresh screen-sized container.
|
|
42
|
+
*/
|
|
43
|
+
runApplication(appKey) {
|
|
44
|
+
if (!registered) return;
|
|
45
|
+
if (appKey && appKey !== registered.appKey) return;
|
|
46
|
+
const root = createRoot({ width: screen.width, height: screen.height });
|
|
47
|
+
root.render(createElement(registered.Component));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Easing tokens. The engine implements a fixed set of easing curves (ERAnimEasing), so unlike RN's
|
|
18
|
+
// composable Easing functions these are string tokens the bridge maps to the engine enum. `bezier`
|
|
19
|
+
// returns a control-point object the bridge reads as a custom cubic-bezier curve.
|
|
20
|
+
export const Easing = {
|
|
21
|
+
linear: 'linear',
|
|
22
|
+
ease: 'ease',
|
|
23
|
+
easeIn: 'easeIn',
|
|
24
|
+
easeOut: 'easeOut',
|
|
25
|
+
easeInOut: 'easeInOut',
|
|
26
|
+
quadIn: 'quadIn',
|
|
27
|
+
quadOut: 'quadOut',
|
|
28
|
+
quadInOut: 'quadInOut',
|
|
29
|
+
cubicIn: 'cubicIn',
|
|
30
|
+
cubicOut: 'cubicOut',
|
|
31
|
+
cubicInOut: 'cubicInOut',
|
|
32
|
+
bounceOut: 'bounceOut',
|
|
33
|
+
elasticOut: 'elasticOut',
|
|
34
|
+
|
|
35
|
+
/** Custom cubic-bezier curve via control points. */
|
|
36
|
+
bezier(x1, y1, x2, y2) {
|
|
37
|
+
return { x1, y1, x2, y2 };
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// LayoutAnimation — the React Native analog. Calling configureNext(...) before a state update that
|
|
18
|
+
// changes layout makes the engine tween every node whose computed rect moved (instead of snapping)
|
|
19
|
+
// on the next commit. Backed by the engine's er_layout_anim_configure_next; the tween advances in C
|
|
20
|
+
// each frame (er_layout_anim_tick), so there's no per-frame JS.
|
|
21
|
+
import { NativeUI } from '../native-ui.js';
|
|
22
|
+
import { Types, Properties, Presets, toEngineConfig, create } from './layout-anim-config.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Arms a layout animation for the next commit. Call right before the setState that changes layout.
|
|
26
|
+
* onAnimationDidEnd is approximated with a timer (the engine config carries no completion hook).
|
|
27
|
+
*/
|
|
28
|
+
export function configureNext(config, onAnimationDidEnd) {
|
|
29
|
+
NativeUI.configureNextLayoutAnimation(toEngineConfig(config));
|
|
30
|
+
if (typeof onAnimationDidEnd === 'function') {
|
|
31
|
+
const ms = config && typeof config.duration === 'number' ? config.duration : 300;
|
|
32
|
+
setTimeout(onAnimationDidEnd, ms);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const LayoutAnimation = {
|
|
37
|
+
configureNext,
|
|
38
|
+
create,
|
|
39
|
+
Types,
|
|
40
|
+
Properties,
|
|
41
|
+
Presets,
|
|
42
|
+
easeInEaseOut: (onDone) => configureNext(Presets.easeInEaseOut, onDone),
|
|
43
|
+
linear: (onDone) => configureNext(Presets.linear, onDone),
|
|
44
|
+
spring: (onDone) => configureNext(Presets.spring, onDone),
|
|
45
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Platform — minimal RN analog. OS is "embedded"; select() picks the embedded entry (falling back
|
|
18
|
+
// to default) so apps can write Platform.select({ embedded: ..., default: ... }).
|
|
19
|
+
export const Platform = {
|
|
20
|
+
OS: 'embedded',
|
|
21
|
+
select(specifics) {
|
|
22
|
+
if (specifics == null) return undefined;
|
|
23
|
+
if ('embedded' in specifics) return specifics.embedded;
|
|
24
|
+
return specifics.default;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// StyleSheet — the React Native analog. `create` is an identity pass-through (styles are plain
|
|
18
|
+
// objects the host config flattens at setProps time); `flatten` collapses nested arrays/objects.
|
|
19
|
+
export const StyleSheet = {
|
|
20
|
+
create(styles) {
|
|
21
|
+
return styles;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
flatten(style) {
|
|
25
|
+
if (!style) return {};
|
|
26
|
+
if (Array.isArray(style)) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const s of style) Object.assign(out, StyleSheet.flatten(s));
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
return style;
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
hairlineWidth: 1,
|
|
35
|
+
absoluteFill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
|
|
36
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Host components. Each is the string tag the reconciler passes to NativeUI.createNode(), which
|
|
18
|
+
// maps it onto an ERNodeType. JSX `<View/>` → createElement('View', ...) → createInstance('View').
|
|
19
|
+
export const View = 'View';
|
|
20
|
+
export const Text = 'Text';
|
|
21
|
+
export const Image = 'Image';
|
|
22
|
+
export const ScrollView = 'ScrollView';
|
|
23
|
+
export const FlatList = 'FlatList';
|
|
24
|
+
export const Pressable = 'Pressable';
|
|
25
|
+
// TouchableOpacity is RN's press-with-opacity wrapper; for now it maps to the same Pressable node.
|
|
26
|
+
export const TouchableOpacity = 'Pressable';
|
|
27
|
+
export const TextInput = 'TextInput';
|
|
28
|
+
export const Switch = 'Switch';
|
|
29
|
+
export const ActivityIndicator = 'ActivityIndicator';
|
|
30
|
+
export const Modal = 'Modal';
|
|
31
|
+
|
|
32
|
+
// SVG surface. <Svg> is the only host node (→ ER_NODE_VECTOR); the shape tags below are descriptive
|
|
33
|
+
// children that the host config flattens into the Svg node's op-tape (they are never mounted on their
|
|
34
|
+
// own — like raw text inside <Text>). Use them only inside an <Svg>.
|
|
35
|
+
export const Svg = 'Svg';
|
|
36
|
+
export const Path = 'Path';
|
|
37
|
+
export const Circle = 'Circle';
|
|
38
|
+
export const Ellipse = 'Ellipse';
|
|
39
|
+
export const Rect = 'Rect';
|
|
40
|
+
export const Line = 'Line';
|
|
41
|
+
export const G = 'G';
|
|
42
|
+
// Arc convenience primitive (not a standard SVG element): a circular arc given a center, radius, and
|
|
43
|
+
// start/end angles in DEGREES clockwise from 12 o'clock. Flattens to a native arc op — cheap to update.
|
|
44
|
+
export const Arc = 'Arc';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Imperative escape hatch for high-frequency interactive updates.
|
|
18
|
+
//
|
|
19
|
+
// Going through React on every pointer move is too slow on some devices — the
|
|
20
|
+
// reconcile + flatten dominate. For continuous gestures (e.g., dragging a dial), grab a node handle via
|
|
21
|
+
// a ref and push updates directly here: it skips React entirely and, for vectors, skips the d-string
|
|
22
|
+
// parser too. Commit back to React state when the gesture ends so the declarative tree re-syncs.
|
|
23
|
+
import { NativeUI } from '../native-ui.js';
|
|
24
|
+
import { shapesToVector } from './svg-ops.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Imperatively sets an <Svg> node's geometry from primitive shape descriptors (see shapesToVector).
|
|
28
|
+
* @param {number} handle The node handle from a ref on an <Svg/>.
|
|
29
|
+
* @param {Array} shapes Flat array of shape descriptors ({arc}/{circle}/{line}/{rect}/{path} + paint).
|
|
30
|
+
* @param {Array} [dirtyRect] Optional node-local [x, y, w, h] bounding the change; when given, only that
|
|
31
|
+
* region is repainted and flushed (a big win for small updates on a large
|
|
32
|
+
* vector node). Omit to repaint the whole node box.
|
|
33
|
+
*/
|
|
34
|
+
export function updateVector(handle, shapes, dirtyRect) {
|
|
35
|
+
if (handle == null) return;
|
|
36
|
+
const { ops, paints } = shapesToVector(shapes);
|
|
37
|
+
NativeUI.setVectorOps(handle, ops, paints, dirtyRect);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Imperatively sets a <Text> node's text content WITHOUT disturbing its style (uses a single inherited
|
|
42
|
+
* span, so color/size/etc. from the node's props are preserved). A later React render reverts cleanly.
|
|
43
|
+
* @param {number} handle The node handle from a ref on a <Text/>.
|
|
44
|
+
* @param {string} text New text content.
|
|
45
|
+
*/
|
|
46
|
+
export function updateText(handle, text) {
|
|
47
|
+
if (handle == null) return;
|
|
48
|
+
NativeUI.setTextSpans(handle, [{ text: String(text) }]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Customises the on-screen software keyboard's appearance and/or layout. Only effective when the engine was
|
|
53
|
+
* built with the keyboard enabled (ERUI_ONSCREEN_KEYBOARD=1); a no-op otherwise. The config is a plain
|
|
54
|
+
* object of colours/sizes plus an optional `layers` array; omit `layers` to keep the built-in QWERTY:
|
|
55
|
+
*
|
|
56
|
+
* setKeyboardConfig({ keyColor: '#fff', labelColor: '#111', fontSize: 20,
|
|
57
|
+
* layers: [ [ [ { char: 'a' }, ... ], // a row
|
|
58
|
+
* [ { label: 'shift', layer: 1, highlight: true }, ... ] ], // a row with a layer-switch key
|
|
59
|
+
* ... ] }); // more layers
|
|
60
|
+
*
|
|
61
|
+
* Key shapes: { char } types it; { char: ' ', span } a space bar; { label, layer } switches layer;
|
|
62
|
+
* { label, backspace } / { label, done }; optional `span` (grid columns) and `highlight` (lit on its layer).
|
|
63
|
+
* Pass nothing/null to restore the default.
|
|
64
|
+
* @param {object} [config] Keyboard config, or null for the built-in default.
|
|
65
|
+
*/
|
|
66
|
+
export function setKeyboardConfig(config) {
|
|
67
|
+
NativeUI.setKeyboardConfig(config || null);
|
|
68
|
+
}
|