react-native-impeller-platform 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +165 -2
  2. package/package.json +52 -4
  3. package/react-native.config.js +24 -0
  4. package/src/bin/engine-path.js +12 -0
  5. package/src/cli/index.js +20 -0
  6. package/src/cli/init.js +112 -0
  7. package/src/cli/run.js +73 -0
  8. package/src/externaltexture/index.js +58 -0
  9. package/src/index.js +12 -0
  10. package/src/metro-shims/index.js +382 -0
  11. package/src/metro-shims/shims/Alert.impeller.js +89 -0
  12. package/src/metro-shims/shims/BaseViewConfig.impeller.js +81 -0
  13. package/src/metro-shims/shims/Button.impeller.js +100 -0
  14. package/src/metro-shims/shims/Image.impeller.js +97 -0
  15. package/src/metro-shims/shims/Platform.impeller-as-ios.js +44 -0
  16. package/src/metro-shims/shims/Platform.impeller.js +53 -0
  17. package/src/metro-shims/shims/ReactNativeFeatureFlags.impeller.js +40 -0
  18. package/src/metro-shims/shims/ReactNativeStyleAttributes.impeller.js +20 -0
  19. package/src/metro-shims/shims/TextInputState.impeller.js +100 -0
  20. package/src/metro-shims/shims/ViewConfigIgnore.impeller.js +55 -0
  21. package/src/metro-shims/shims/reanimated/clock.js +41 -0
  22. package/src/metro-shims/shims/reanimated/component.js +83 -0
  23. package/src/metro-shims/shims/reanimated/css.js +193 -0
  24. package/src/metro-shims/shims/reanimated/easing.js +47 -0
  25. package/src/metro-shims/shims/reanimated/hooks.js +191 -0
  26. package/src/metro-shims/shims/reanimated/interpolate.js +133 -0
  27. package/src/metro-shims/shims/reanimated/values.js +264 -0
  28. package/src/metro-shims/shims/reanimated.impeller.js +90 -0
  29. package/src/notifications/index.d.ts +44 -0
  30. package/src/notifications/index.js +95 -0
  31. package/src/platformview/index.js +23 -0
  32. package/src/resolve.js +238 -0
  33. package/src/selftest.js +123 -0
  34. package/src/storage/index.js +161 -0
  35. package/src/viewshot/index.js +98 -0
  36. package/src/worldraster/index.js +75 -0
  37. package/templates/app/README.md.tmpl +18 -0
  38. package/templates/app/_gitignore +1 -0
  39. package/templates/app/impeller.config.json.tmpl +11 -0
@@ -0,0 +1,264 @@
1
+ 'use strict';
2
+ // SC-26. Shared values and the with* animations of the reanimated stub, run
3
+ // on the JS thread by clock.js. An animation is a plain object with
4
+ // start(from, now) and frame(now) -> done; assigning one to sv.value runs it.
5
+ const {now, onFrame} = require('./clock');
6
+ const {Easing, toEasing} = require('./easing');
7
+ const {mix} = require('./interpolate');
8
+
9
+ const ANIM = Symbol.for('rni.reanimated.animation');
10
+ const isAnimation = (v) => v != null && typeof v === 'object' && v[ANIM] === true;
11
+
12
+ // Shared values read while a tracker is set are recorded (useAnimatedStyle /
13
+ // useDerivedValue re-run when one of them changes).
14
+ let tracker = null;
15
+ function track(fn) {
16
+ const prev = tracker;
17
+ const seen = new Set();
18
+ tracker = seen;
19
+ try {
20
+ return {result: fn(), deps: seen};
21
+ } finally {
22
+ tracker = prev;
23
+ }
24
+ }
25
+
26
+ class SharedValue {
27
+ constructor(v) {
28
+ this._value = v;
29
+ this._anim = null;
30
+ this._listeners = new Map();
31
+ this._seq = 0;
32
+ }
33
+ get value() {
34
+ if (tracker) tracker.add(this);
35
+ return this._value;
36
+ }
37
+ set value(v) {
38
+ this._anim = null;
39
+ if (isAnimation(v)) {
40
+ const anim = v;
41
+ this._anim = anim;
42
+ anim.start(this._value, now());
43
+ onFrame((t) => {
44
+ if (this._anim !== anim) return false;
45
+ const done = anim.frame(t);
46
+ this._write(anim.current);
47
+ if (done) {
48
+ this._anim = null;
49
+ anim.finish(true);
50
+ }
51
+ return !done;
52
+ });
53
+ return;
54
+ }
55
+ this._write(v);
56
+ }
57
+ _write(v) {
58
+ this._value = v;
59
+ for (const fn of Array.from(this._listeners.values())) fn(v);
60
+ }
61
+ get() {
62
+ return this.value;
63
+ }
64
+ set(v) {
65
+ this.value = typeof v === 'function' ? v(this._value) : v;
66
+ }
67
+ modify(fn) {
68
+ if (fn) this.value = fn(this._value);
69
+ }
70
+ addListener(id, fn) {
71
+ this._listeners.set(id, fn);
72
+ }
73
+ removeListener(id) {
74
+ this._listeners.delete(id);
75
+ }
76
+ // internal: subscribe with an auto id
77
+ _subscribe(fn) {
78
+ const id = `__rni${this._seq++}`;
79
+ this._listeners.set(id, fn);
80
+ return () => this._listeners.delete(id);
81
+ }
82
+ _cancel() {
83
+ const a = this._anim;
84
+ this._anim = null;
85
+ if (a) a.finish(false);
86
+ }
87
+ }
88
+
89
+ const makeMutable = (v) => new SharedValue(v);
90
+ const cancelAnimation = (sv) => sv && sv._cancel && sv._cancel();
91
+
92
+ function animation(spec, callback) {
93
+ // defineProperties, not spread: withDelay forwards toValue through accessors.
94
+ const a = {
95
+ [ANIM]: true,
96
+ current: undefined,
97
+ finish(ok) {
98
+ if (callback) callback(ok, this.current);
99
+ },
100
+ };
101
+ return Object.defineProperties(a, Object.getOwnPropertyDescriptors(spec));
102
+ }
103
+
104
+ function withTiming(toValue, config, callback) {
105
+ const duration = config && config.duration != null ? config.duration : 300;
106
+ const ease = toEasing(config && config.easing, Easing.inOut(Easing.quad));
107
+ return animation({
108
+ toValue,
109
+ start(from, t) {
110
+ this.from = from;
111
+ this.t0 = t;
112
+ this.current = from;
113
+ },
114
+ frame(t) {
115
+ const p = duration <= 0 ? 1 : Math.min(1, (t - this.t0) / duration);
116
+ this.current = p >= 1 ? this.toValue : mix(this.from, this.toValue, ease(p));
117
+ return p >= 1;
118
+ },
119
+ }, callback);
120
+ }
121
+
122
+ // Damped harmonic oscillator, Reanimated 3's defaults, integrated in 1 ms
123
+ // steps so the result does not depend on the frame rate.
124
+ function withSpring(toValue, config, callback) {
125
+ const c = {mass: 1, stiffness: 100, damping: 10, velocity: 0, overshootClamping: false,
126
+ restDisplacementThreshold: 0.01, restSpeedThreshold: 2, ...(config || {})};
127
+ return animation({
128
+ toValue,
129
+ start(from, t) {
130
+ this.x = typeof from === 'number' ? from : toValue;
131
+ this.v = c.velocity;
132
+ this.t = t;
133
+ this.current = this.x;
134
+ },
135
+ frame(t) {
136
+ const to = this.toValue;
137
+ let steps = Math.min(Math.max(0, Math.round(t - this.t)), 1000);
138
+ this.t = t;
139
+ while (steps-- > 0) {
140
+ const a = (-c.stiffness * (this.x - to) - c.damping * this.v) / c.mass;
141
+ this.v += a * 0.001;
142
+ this.x += this.v * 0.001;
143
+ }
144
+ const overshot = c.overshootClamping && (this.x - to) * (this.current - to) < 0;
145
+ const rest = Math.abs(this.x - to) < c.restDisplacementThreshold && Math.abs(this.v) < c.restSpeedThreshold;
146
+ this.current = overshot || rest ? to : this.x;
147
+ return overshot || rest;
148
+ },
149
+ }, callback);
150
+ }
151
+
152
+ function withDecay(config, callback) {
153
+ const c = {velocity: 0, deceleration: 0.998, ...(config || {})};
154
+ return animation({
155
+ start(from, t) {
156
+ this.x = typeof from === 'number' ? from : 0;
157
+ this.v = c.velocity;
158
+ this.t = t;
159
+ this.current = this.x;
160
+ },
161
+ frame(t) {
162
+ const dt = Math.max(0, t - this.t);
163
+ this.t = t;
164
+ const k = Math.pow(c.deceleration, dt);
165
+ this.x += (this.v / 1000) * ((1 - k) / (1 - c.deceleration));
166
+ this.v *= k;
167
+ let done = Math.abs(this.v) < 0.5;
168
+ if (Array.isArray(c.clamp)) {
169
+ const [lo, hi] = c.clamp;
170
+ if (this.x <= lo || this.x >= hi) {
171
+ this.x = Math.min(Math.max(this.x, lo), hi);
172
+ done = true;
173
+ }
174
+ }
175
+ this.current = this.x;
176
+ return done;
177
+ },
178
+ }, callback);
179
+ }
180
+
181
+ const toAnim = (a) => (isAnimation(a) ? a : withTiming(a, {duration: 0}));
182
+
183
+ function withDelay(delayMs, inner) {
184
+ const a = toAnim(inner);
185
+ return animation({
186
+ get toValue() {
187
+ return a.toValue;
188
+ },
189
+ set toValue(v) {
190
+ a.toValue = v;
191
+ },
192
+ start(from, t) {
193
+ this.t0 = t;
194
+ this.from = from;
195
+ this.started = false;
196
+ this.current = from;
197
+ },
198
+ frame(t) {
199
+ if (t - this.t0 < delayMs) return false;
200
+ if (!this.started) {
201
+ this.started = true;
202
+ a.start(this.from, this.t0 + delayMs);
203
+ }
204
+ const done = a.frame(t);
205
+ this.current = a.current;
206
+ if (done) a.finish(true);
207
+ return done;
208
+ },
209
+ });
210
+ }
211
+
212
+ function withSequence(...inners) {
213
+ const list = inners.map(toAnim);
214
+ return animation({
215
+ start(from, t) {
216
+ this.i = 0;
217
+ this.current = from;
218
+ if (list.length) list[0].start(from, t);
219
+ },
220
+ frame(t) {
221
+ while (this.i < list.length) {
222
+ const a = list[this.i];
223
+ const done = a.frame(t);
224
+ this.current = a.current;
225
+ if (!done) return false;
226
+ a.finish(true);
227
+ this.i++;
228
+ if (this.i < list.length) list[this.i].start(this.current, t);
229
+ }
230
+ return true;
231
+ },
232
+ });
233
+ }
234
+
235
+ // numberOfReps <= 0 repeats forever; reverse swaps the inner animation's
236
+ // target with its start value on every other run.
237
+ function withRepeat(inner, numberOfReps = 2, reverse = false, callback) {
238
+ const a = toAnim(inner);
239
+ return animation({
240
+ start(from, t) {
241
+ this.n = 0;
242
+ this.origin = from;
243
+ this.target = a.toValue;
244
+ this.current = from;
245
+ a.start(from, t);
246
+ },
247
+ frame(t) {
248
+ const done = a.frame(t);
249
+ this.current = a.current;
250
+ if (!done) return false;
251
+ a.finish(true);
252
+ this.n++;
253
+ if (numberOfReps > 0 && this.n >= numberOfReps) return true;
254
+ if (reverse && 'toValue' in a) a.toValue = this.n % 2 ? this.origin : this.target;
255
+ a.start(reverse ? this.current : this.origin, t);
256
+ return false;
257
+ },
258
+ }, callback);
259
+ }
260
+
261
+ module.exports = {
262
+ SharedValue, makeMutable, isAnimation, track, cancelAnimation,
263
+ withTiming, withSpring, withDecay, withDelay, withSequence, withRepeat,
264
+ };
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+ // ENG-19 (moved from HostApp's app-level metro-shims/reanimated-stub.js).
3
+ // react-native-reanimated for the `impeller` platform.
4
+ //
5
+ // WHY: the engine vends no Reanimated / Worklets native module, so the real
6
+ // package cannot initialise, and an app that does not install it cannot even
7
+ // BUNDLE: react-native-css (NativeWind 5) require()s 'react-native-reanimated'
8
+ // from native/reanimated.js on every className render that carries a
9
+ // transition/animation, and Gluestack's motion parts import it at module top.
10
+ //
11
+ // WHAT IT DOES (SC-26): animated components are wrappers that re-render on a
12
+ // JS-thread clock, shared values and with* helpers really animate, and
13
+ // react-native-css transitions / animate-* move. Layout-animation presets stay
14
+ // chainable no-ops. Resolved only when the app has NOT
15
+ // installed react-native-reanimated itself (see metro-shims/index.js).
16
+ const RN = require('react-native');
17
+ const {createAnimatedComponent: animated} = require('./reanimated/component');
18
+ const hooks = require('./reanimated/hooks');
19
+ const values = require('./reanimated/values');
20
+ const {Easing: StubEasing, cubicBezier, cssSteps} = require('./reanimated/easing');
21
+ const {interpolate: interp} = require('./reanimated/interpolate');
22
+
23
+ // Chainable layout-animation builder: FadeIn.duration(200).delay(50) etc. all
24
+ // return the same inert object.
25
+ const preset = new Proxy(function () {}, {
26
+ get: (t, k) => (k === '__esModule' ? false : k === 'build' ? () => () => ({}) : () => preset),
27
+ apply: () => preset,
28
+ });
29
+
30
+ const Animated = {
31
+ View: animated(RN.View),
32
+ Text: animated(RN.Text),
33
+ Image: animated(RN.Image),
34
+ ScrollView: animated(RN.ScrollView),
35
+ FlatList: animated(RN.FlatList),
36
+ createAnimatedComponent: animated,
37
+ };
38
+
39
+ const sharedValue = (v) => ({value: v, get: () => v, set: () => {}, modify: () => {}});
40
+
41
+ module.exports = {
42
+ __esModule: true,
43
+ __impellerStub: true,
44
+ default: Animated,
45
+ createAnimatedComponent: animated,
46
+ // layout-animation presets Gluestack's parts reference
47
+ FadeIn: preset,
48
+ FadeOut: preset,
49
+ FadeInUp: preset,
50
+ FadeInDown: preset,
51
+ FadeOutUp: preset,
52
+ FadeOutDown: preset,
53
+ ZoomIn: preset,
54
+ ZoomOut: preset,
55
+ SlideInDown: preset,
56
+ SlideOutDown: preset,
57
+ SlideInUp: preset,
58
+ SlideOutUp: preset,
59
+ SlideInLeft: preset,
60
+ SlideOutLeft: preset,
61
+ SlideInRight: preset,
62
+ SlideOutRight: preset,
63
+ Layout: preset,
64
+ LinearTransition: preset,
65
+ // hooks and animation helpers (JS-thread implementation, see ./reanimated/)
66
+ useSharedValue: hooks.useSharedValue,
67
+ useDerivedValue: hooks.useDerivedValue,
68
+ useAnimatedStyle: hooks.useAnimatedStyle,
69
+ useAnimatedProps: hooks.useAnimatedProps,
70
+ useAnimatedRef: () => ({current: null}),
71
+ useAnimatedReaction: hooks.useAnimatedReaction,
72
+ useAnimatedScrollHandler: hooks.useAnimatedScrollHandler,
73
+ useReducedMotion: () => false,
74
+ withTiming: values.withTiming,
75
+ withSpring: values.withSpring,
76
+ withDecay: values.withDecay,
77
+ withDelay: values.withDelay,
78
+ withRepeat: values.withRepeat,
79
+ withSequence: values.withSequence,
80
+ cancelAnimation: values.cancelAnimation,
81
+ runOnJS: (f) => f,
82
+ runOnUI: (f) => f,
83
+ interpolate: interp,
84
+ Extrapolation: {CLAMP: 'clamp', EXTEND: 'extend', IDENTITY: 'identity'},
85
+ ReduceMotion: {System: 'system', Always: 'always', Never: 'never'},
86
+ Easing: StubEasing,
87
+ // CSS-animation timing helpers react-native-css reads
88
+ cubicBezier,
89
+ steps: cssSteps,
90
+ };
@@ -0,0 +1,44 @@
1
+ // Types for react-native-impeller-platform/src/notifications.
2
+
3
+ export type PermissionStatus = 'granted' | 'denied' | 'unavailable' | 'error';
4
+ export type ShowStatus = 'shown' | 'denied' | 'unavailable' | 'error';
5
+
6
+ export interface PermissionResult {
7
+ status: PermissionStatus;
8
+ /** Present when status is not 'granted'. */
9
+ reason?: string;
10
+ }
11
+
12
+ export interface ShowOptions<Data = unknown> {
13
+ title: string;
14
+ body?: string;
15
+ /** Showing an id that is already on screen replaces it. Generated when omitted. */
16
+ id?: string;
17
+ /** Any JSON-serialisable value. It comes back unchanged in the press event. */
18
+ data?: Data;
19
+ }
20
+
21
+ export interface ShowResult {
22
+ status: ShowStatus;
23
+ id: string;
24
+ reason?: string;
25
+ }
26
+
27
+ export interface PressEvent<Data = unknown> {
28
+ id: string;
29
+ /** 'default': the user clicked the notification. */
30
+ action: string;
31
+ data: Data | null;
32
+ }
33
+
34
+ export interface Subscription {
35
+ remove(): void;
36
+ }
37
+
38
+ export const PRESS_EVENT: 'rniNotificationPress';
39
+
40
+ export function requestPermission(): Promise<PermissionResult>;
41
+ export function show<Data = unknown>(options: ShowOptions<Data>): Promise<ShowResult>;
42
+ export function onPress<Data = unknown>(
43
+ listener: (event: PressEvent<Data>) => void,
44
+ ): Subscription;
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+ // Desktop notifications, the JS half of the engine's Notifications module.
3
+ //
4
+ // requestPermission() -> Promise<{status, reason?}>
5
+ // show({title, body, id, data}) -> Promise<{status, id, reason?}>
6
+ // onPress(listener) -> {remove()}; listener gets {id, data, action}
7
+ //
8
+ // The engine owns the OS call (freedesktop D-Bus on Linux,
9
+ // UNUserNotificationCenter on macOS, toast notifications on Windows). This
10
+ // file owns the shape: argument checks, the JSON encoding of `data`, and the
11
+ // subscription. `data` goes to native as a JSON string and comes back on press
12
+ // unchanged, so native never has to understand it.
13
+ //
14
+ // Nothing here throws for a missing capability. An engine without the module
15
+ // (built before it existed, or running on a platform with no backend) answers
16
+ // {status: 'unavailable', reason}, the same answer the engine gives when the
17
+ // OS service is missing. Bad arguments are programmer errors and reject.
18
+
19
+ const {DeviceEventEmitter, TurboModuleRegistry} = require('react-native');
20
+
21
+ // get, not getEnforcing: a missing module must fail this feature, not the
22
+ // whole bundle at import time.
23
+ const Native = TurboModuleRegistry.get('Notifications');
24
+
25
+ // Emitted by the engine (ReactHost.cpp) when the user clicks a notification
26
+ // this app showed.
27
+ const PRESS_EVENT = 'rniNotificationPress';
28
+
29
+ const MISSING_REASON =
30
+ 'this engine has no Notifications module (run `rn-impeller --list-modules` to check)';
31
+
32
+ let generated = 0;
33
+
34
+ async function requestPermission() {
35
+ if (Native == null) return {status: 'unavailable', reason: MISSING_REASON};
36
+ return Native.requestPermission();
37
+ }
38
+
39
+ function checkOptions(options) {
40
+ if (options == null || typeof options !== 'object') {
41
+ throw new TypeError('show() wants an options object {title, body, id, data}');
42
+ }
43
+ const {title, body = '', id} = options;
44
+ if (typeof title !== 'string' || title.length === 0) {
45
+ throw new TypeError('show(): title must be a non-empty string');
46
+ }
47
+ if (typeof body !== 'string') {
48
+ throw new TypeError('show(): body must be a string');
49
+ }
50
+ if (id != null && (typeof id !== 'string' || id.length === 0)) {
51
+ throw new TypeError('show(): id must be a non-empty string when given');
52
+ }
53
+ let dataJson;
54
+ try {
55
+ dataJson = JSON.stringify(options.data === undefined ? null : options.data);
56
+ } catch (error) {
57
+ throw new TypeError('show(): data must be JSON-serialisable: ' + error.message);
58
+ }
59
+ if (dataJson === undefined) {
60
+ // JSON.stringify returns undefined for a function or a symbol.
61
+ throw new TypeError('show(): data must be JSON-serialisable');
62
+ }
63
+ generated += 1;
64
+ return {title, body, id: id == null ? `rni-${Date.now()}-${generated}` : id, dataJson};
65
+ }
66
+
67
+ // Showing an id that is already on screen replaces that notification.
68
+ async function show(options) {
69
+ const {title, body, id, dataJson} = checkOptions(options);
70
+ if (Native == null) return {status: 'unavailable', id, reason: MISSING_REASON};
71
+ const result = await Native.show(title, body, id, dataJson);
72
+ return {...result, id};
73
+ }
74
+
75
+ function decodeData(raw) {
76
+ if (typeof raw !== 'string') return null;
77
+ try {
78
+ return JSON.parse(raw);
79
+ } catch (error) {
80
+ // Native echoes exactly what show() encoded, so this only happens for a
81
+ // payload that did not come from this file. Report it as no data.
82
+ return null;
83
+ }
84
+ }
85
+
86
+ function onPress(listener) {
87
+ if (typeof listener !== 'function') {
88
+ throw new TypeError('onPress() wants a function');
89
+ }
90
+ return DeviceEventEmitter.addListener(PRESS_EVENT, event => {
91
+ listener({id: event.id, action: event.action, data: decodeData(event.data)});
92
+ });
93
+ }
94
+
95
+ module.exports = {requestPermission, show, onPress, PRESS_EVENT};
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+ // FP-44 — platform views (Flutter's PlatformViewLayer + ExternalViewEmbedder).
3
+ //
4
+ // <ImpellerPlatformView color="#00aa00" style={...} /> is a native OS child view
5
+ // (an X11 child window on linux windowed GL) that the engine positions, clips
6
+ // and stacks to the view's box every frame; RN content drawn after it in paint
7
+ // order is composited above it. This first native view is a solid colour.
8
+ // Translate and rect clips are composited; scale/rotate/skew are not (the view
9
+ // is placed at the transformed bounds and the engine logs
10
+ // `platform_view_unsupported`). Hosts without an embedder (darwin, Windows,
11
+ // headless) paint the colour in place.
12
+
13
+ const {processColor} = require('react-native');
14
+ const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');
15
+
16
+ const ImpellerPlatformView = NativeComponentRegistry.get('ImpellerPlatformView', () => ({
17
+ uiViewClassName: 'ImpellerPlatformView',
18
+ bubblingEventTypes: {},
19
+ directEventTypes: {},
20
+ validAttributes: {color: {process: processColor}},
21
+ }));
22
+
23
+ module.exports = {ImpellerPlatformView};