react-native-impeller-platform 0.0.0-bootstrap.0 → 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/README.md +164 -1
- package/package.json +47 -3
- package/react-native.config.js +24 -0
- package/src/bin/engine-path.js +12 -0
- package/src/cli/index.js +20 -0
- package/src/cli/init.js +112 -0
- package/src/cli/run.js +73 -0
- package/src/externaltexture/index.js +58 -0
- package/src/index.js +12 -0
- package/src/metro-shims/index.js +382 -0
- package/src/metro-shims/shims/Alert.impeller.js +89 -0
- package/src/metro-shims/shims/BaseViewConfig.impeller.js +81 -0
- package/src/metro-shims/shims/Button.impeller.js +100 -0
- package/src/metro-shims/shims/Image.impeller.js +97 -0
- package/src/metro-shims/shims/Platform.impeller-as-ios.js +44 -0
- package/src/metro-shims/shims/Platform.impeller.js +53 -0
- package/src/metro-shims/shims/ReactNativeFeatureFlags.impeller.js +40 -0
- package/src/metro-shims/shims/ReactNativeStyleAttributes.impeller.js +20 -0
- package/src/metro-shims/shims/TextInputState.impeller.js +100 -0
- package/src/metro-shims/shims/ViewConfigIgnore.impeller.js +55 -0
- package/src/metro-shims/shims/reanimated/clock.js +41 -0
- package/src/metro-shims/shims/reanimated/component.js +83 -0
- package/src/metro-shims/shims/reanimated/css.js +193 -0
- package/src/metro-shims/shims/reanimated/easing.js +47 -0
- package/src/metro-shims/shims/reanimated/hooks.js +191 -0
- package/src/metro-shims/shims/reanimated/interpolate.js +133 -0
- package/src/metro-shims/shims/reanimated/values.js +264 -0
- package/src/metro-shims/shims/reanimated.impeller.js +90 -0
- package/src/platformview/index.js +23 -0
- package/src/resolve.js +238 -0
- package/src/selftest.js +123 -0
- package/src/storage/index.js +161 -0
- package/src/viewshot/index.js +98 -0
- package/src/worldraster/index.js +75 -0
- package/templates/app/README.md.tmpl +18 -0
- package/templates/app/_gitignore +1 -0
- package/templates/app/impeller.config.json.tmpl +11 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// <Image> for the `impeller` platform (A19 / SC-30; replaces the parity item
|
|
3
|
+
// 12 placeholder View).
|
|
4
|
+
//
|
|
5
|
+
// WHY a shim: Libraries/Image/Image.js is a self re-export (`import Image from
|
|
6
|
+
// './Image'`), so on impeller -- no .impeller.js sibling -- it resolves to
|
|
7
|
+
// ITSELF and its default export is undefined. Image.ios.js cannot be used
|
|
8
|
+
// either: it getEnforcing's the ImageLoader TurboModule at module load.
|
|
9
|
+
//
|
|
10
|
+
// This is Image.ios.js's render path: it mounts the RCTImageView host
|
|
11
|
+
// component (Fabric "Image", painted by the engine) with `source` as an array,
|
|
12
|
+
// resizeMode (objectFit wins, default 'cover') and tintColor (prop or style).
|
|
13
|
+
// onLoadStart/onLoad/onError/onLoadEnd and defaultSource ride the native
|
|
14
|
+
// component (the engine fires them, SC-31). The statics are Image.ios.js's,
|
|
15
|
+
// over the engine's "ImageLoader" module.
|
|
16
|
+
const React = require('react');
|
|
17
|
+
const interop = (m) => (m && m.default) || m;
|
|
18
|
+
const flattenStyle = interop(require('react-native/Libraries/StyleSheet/flattenStyle'));
|
|
19
|
+
const StyleSheet = interop(require('react-native/Libraries/StyleSheet/StyleSheet'));
|
|
20
|
+
const ImageViewNativeComponent = interop(require('react-native/Libraries/Image/ImageViewNativeComponent'));
|
|
21
|
+
const {getImageSourcesFromImageProps} = require('react-native/Libraries/Image/ImageSourceUtils');
|
|
22
|
+
const {convertObjectFitToResizeMode} = require('react-native/Libraries/Image/ImageUtils');
|
|
23
|
+
|
|
24
|
+
const styles = StyleSheet.create({base: {overflow: 'hidden'}});
|
|
25
|
+
|
|
26
|
+
function Image({ref, ...props}) {
|
|
27
|
+
const source = getImageSourcesFromImageProps(props) || {
|
|
28
|
+
uri: undefined,
|
|
29
|
+
width: undefined,
|
|
30
|
+
height: undefined,
|
|
31
|
+
};
|
|
32
|
+
let style;
|
|
33
|
+
let sources;
|
|
34
|
+
if (Array.isArray(source)) {
|
|
35
|
+
style = [styles.base, props.style];
|
|
36
|
+
sources = source;
|
|
37
|
+
} else {
|
|
38
|
+
const width = source.width ?? props.width;
|
|
39
|
+
const height = source.height ?? props.height;
|
|
40
|
+
style = [{width, height}, styles.base, props.style];
|
|
41
|
+
sources = [source];
|
|
42
|
+
}
|
|
43
|
+
const flat = flattenStyle(style);
|
|
44
|
+
const objectFit = convertObjectFitToResizeMode(flat?.objectFit);
|
|
45
|
+
const resizeMode = objectFit || props.resizeMode || flat?.resizeMode || 'cover';
|
|
46
|
+
const tintColor = props.tintColor ?? flat?.tintColor;
|
|
47
|
+
if (props.children != null) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const {src, 'aria-hidden': ariaHidden, ...rest} = props;
|
|
53
|
+
const accessible = ariaHidden !== true && (props.alt !== undefined ? true : props.accessible);
|
|
54
|
+
return React.createElement(ImageViewNativeComponent, {
|
|
55
|
+
...rest,
|
|
56
|
+
ref,
|
|
57
|
+
accessible,
|
|
58
|
+
accessibilityLabel: props['aria-label'] ?? props.accessibilityLabel ?? props.alt,
|
|
59
|
+
style,
|
|
60
|
+
resizeMode,
|
|
61
|
+
tintColor,
|
|
62
|
+
source: sources,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
Image.displayName = 'Image';
|
|
66
|
+
|
|
67
|
+
const TurboModuleRegistry = require('react-native/Libraries/TurboModule/TurboModuleRegistry');
|
|
68
|
+
const loader = () => {
|
|
69
|
+
const m = TurboModuleRegistry.get('ImageLoader');
|
|
70
|
+
if (m == null) throw new Error('ImageLoader native module is not available');
|
|
71
|
+
return m;
|
|
72
|
+
};
|
|
73
|
+
const settle = (promise, uri, success, failure) => {
|
|
74
|
+
if (typeof success !== 'function') return promise;
|
|
75
|
+
promise
|
|
76
|
+
.then((sizes) => success(sizes.width, sizes.height))
|
|
77
|
+
.catch(failure || (() => console.warn('Failed to get size for image: ' + uri)));
|
|
78
|
+
return undefined;
|
|
79
|
+
};
|
|
80
|
+
Image.getSize = (uri, success, failure) =>
|
|
81
|
+
settle(
|
|
82
|
+
loader().getSize(uri).then(([width, height]) => ({width, height})),
|
|
83
|
+
uri,
|
|
84
|
+
success,
|
|
85
|
+
failure,
|
|
86
|
+
);
|
|
87
|
+
Image.getSizeWithHeaders = (uri, headers, success, failure) =>
|
|
88
|
+
settle(loader().getSizeWithHeaders(uri, headers), uri, success, failure);
|
|
89
|
+
Image.prefetch = (url) => loader().prefetchImage(url);
|
|
90
|
+
Image.prefetchWithMetadata = (url, queryRootName, rootTag) =>
|
|
91
|
+
loader().prefetchImageWithMetadata(url, queryRootName, rootTag || 0);
|
|
92
|
+
Image.queryCache = async (urls) => loader().queryCache(urls);
|
|
93
|
+
Image.resolveAssetSource = (source) =>
|
|
94
|
+
require('react-native/Libraries/Image/resolveAssetSource').default(source);
|
|
95
|
+
|
|
96
|
+
module.exports = Image;
|
|
97
|
+
module.exports.default = Image;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// ENG-01a. Platform variant handed ONLY to react-native files that branch
|
|
3
|
+
// `if (ios) ... else if (android) ...` with no else, and whose iOS arm is the
|
|
4
|
+
// desktop-shaped one.
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS FILE EXISTS AT ALL
|
|
7
|
+
// ---------------------------
|
|
8
|
+
// TextInput.js does not fail. It BRANCHES ITSELF OUT OF EXISTENCE, exactly the
|
|
9
|
+
// way Alert.js did (see Alert.impeller.js). Read the real file:
|
|
10
|
+
//
|
|
11
|
+
// line 555 let textInput = null;
|
|
12
|
+
// line 677 if (Platform.OS === 'ios') { textInput = <RCTSingleline.../> }
|
|
13
|
+
// line 727 else if (Platform.OS === 'android') { textInput = <AndroidTextInput/> }
|
|
14
|
+
// (no else)
|
|
15
|
+
// line 802 return <TextAncestorContext value={true}>{textInput}</TextAncestorContext>
|
|
16
|
+
//
|
|
17
|
+
// On `impeller` neither arm runs, so a perfectly good <TextInput> renders
|
|
18
|
+
// `null` inside a context provider. React mounts nothing, Fabric is never
|
|
19
|
+
// asked for a host component, and the log is clean. Registering a
|
|
20
|
+
// TextInput ComponentDescriptor in FabricHost.cpp would have changed NOTHING,
|
|
21
|
+
// because the JS never reaches C++ at all. That was the whole ENG-01 dispute.
|
|
22
|
+
//
|
|
23
|
+
// WHY NOT JUST SET Platform.OS = 'ios'
|
|
24
|
+
// ------------------------------------
|
|
25
|
+
// Because it is a lie the whole bundle would read. Platform.OS === 'impeller'
|
|
26
|
+
// is what selects our shims, our .impeller.js extensions, and what app code
|
|
27
|
+
// branches on. Flipping it globally to 'ios' would unpick every one of those.
|
|
28
|
+
// So the lie is scoped: this module is served ONLY to the specific RN files
|
|
29
|
+
// named in IOS_BRANCHED, by resolved-origin path, and every other importer of
|
|
30
|
+
// Platform still gets the honest 'impeller'.
|
|
31
|
+
//
|
|
32
|
+
// Everything except OS is delegated to the real shim, so constants/Version/
|
|
33
|
+
// select/isTesting cannot drift between the two copies.
|
|
34
|
+
const Platform = require('./Platform.impeller.js');
|
|
35
|
+
|
|
36
|
+
const PlatformAsIos = Object.create(Platform);
|
|
37
|
+
Object.defineProperty(PlatformAsIos, 'OS', {value: 'ios', enumerable: true});
|
|
38
|
+
// `select` must keep honouring 'impeller' first — an app that wrote
|
|
39
|
+
// Platform.select({impeller: x, ios: y}) means x, and this file is not a
|
|
40
|
+
// licence to hand it y. Only the OS string is forged.
|
|
41
|
+
PlatformAsIos.select = Platform.select.bind(Platform);
|
|
42
|
+
|
|
43
|
+
module.exports = PlatformAsIos;
|
|
44
|
+
module.exports.default = PlatformAsIos;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Platform shim for Platform.OS === 'impeller'.
|
|
3
|
+
//
|
|
4
|
+
// The literal below is the PACKAGING MARKER. tools/pack-and-consume.sh greps the
|
|
5
|
+
// produced Metro bundle for it: if the marker is absent, the shim never made it
|
|
6
|
+
// into the tarball or metro never resolved it, and the consumer path is broken --
|
|
7
|
+
// regardless of whether the build "succeeded".
|
|
8
|
+
const RN_IMPELLER_SHIM_MARKER = '__RN_IMPELLER_SHIM_V1__';
|
|
9
|
+
|
|
10
|
+
const Platform = {
|
|
11
|
+
__constants: null,
|
|
12
|
+
OS: 'impeller',
|
|
13
|
+
get Version() {
|
|
14
|
+
return this.constants.osVersion || 0;
|
|
15
|
+
},
|
|
16
|
+
// P4: actually READ the PlatformConstants TurboModule. Until now this
|
|
17
|
+
// returned `{}`, so `Platform.constants.reactNativeVersion` was undefined and
|
|
18
|
+
// `Platform.Version` was 0 — while the native module sat there answering
|
|
19
|
+
// correctly and nothing indicated the two were not connected.
|
|
20
|
+
//
|
|
21
|
+
// TurboModuleRegistry.get (not getEnforcing): if the module is somehow
|
|
22
|
+
// absent, a Platform that reports empty constants is far better than a
|
|
23
|
+
// bundle that dies at import over a version string.
|
|
24
|
+
get constants() {
|
|
25
|
+
if (this.__constants == null) {
|
|
26
|
+
const TurboModuleRegistry = require('react-native/Libraries/TurboModule/TurboModuleRegistry');
|
|
27
|
+
const NativePlatformConstants = TurboModuleRegistry.get('PlatformConstants');
|
|
28
|
+
this.__constants = NativePlatformConstants
|
|
29
|
+
? NativePlatformConstants.getConstants()
|
|
30
|
+
: {};
|
|
31
|
+
}
|
|
32
|
+
return this.__constants;
|
|
33
|
+
},
|
|
34
|
+
get isTesting() {
|
|
35
|
+
return this.constants.isTesting === true;
|
|
36
|
+
},
|
|
37
|
+
get isTV() {
|
|
38
|
+
return false;
|
|
39
|
+
},
|
|
40
|
+
get isVision() {
|
|
41
|
+
return false;
|
|
42
|
+
},
|
|
43
|
+
select(spec) {
|
|
44
|
+
if ('impeller' in spec) return spec.impeller;
|
|
45
|
+
if ('native' in spec) return spec.native;
|
|
46
|
+
return spec.default;
|
|
47
|
+
},
|
|
48
|
+
__marker: RN_IMPELLER_SHIM_MARKER,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = Platform;
|
|
52
|
+
module.exports.default = Platform;
|
|
53
|
+
module.exports.RN_IMPELLER_SHIM_MARKER = RN_IMPELLER_SHIM_MARKER;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// parity item 20 (moved here from HostApp's app-level metro-shims, ENG-09b).
|
|
3
|
+
// Redirect target for react-native/src/private/featureflags/
|
|
4
|
+
// ReactNativeFeatureFlags on platform 'impeller' only.
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS FILE EXISTS
|
|
7
|
+
// Upstream defines shouldPressibilityUseW3CPointerEventsForHover through
|
|
8
|
+
// createNativeFlagGetter, which reads NativeReactNativeFeatureFlagsCxx (a
|
|
9
|
+
// TurboModule this engine does not register) and falls back to its default,
|
|
10
|
+
// false, when the module is absent. An app's override() call only feeds the
|
|
11
|
+
// OTHER getter family (createJavaScriptFlagGetter's shared `overrides`), so
|
|
12
|
+
// overriding this flag was silently a no-op and Pressability never called
|
|
13
|
+
// onHoverIn. Measured in HostApp: a synthesised hover_enter on a Button logged
|
|
14
|
+
// {"hover":false}.
|
|
15
|
+
//
|
|
16
|
+
// The default is TRUE (SC-25): the engine delivers hover_enter/leave as W3C
|
|
17
|
+
// pointer events, so a stock app (NativeWind hover:, group-hover:) works with
|
|
18
|
+
// no override. An app can still override() it to false.
|
|
19
|
+
//
|
|
20
|
+
// Fix, scoped to this ONE flag: re-export every other flag from the real
|
|
21
|
+
// module untouched, and redefine this one through createJavaScriptFlagGetter,
|
|
22
|
+
// which reads the same `overrides` object override()/setOverrides install.
|
|
23
|
+
//
|
|
24
|
+
// The require() below reaches the REAL upstream file: the resolver exempts
|
|
25
|
+
// requests whose origin IS this shim, so it is not redirected back here.
|
|
26
|
+
const real = require('react-native/src/private/featureflags/ReactNativeFeatureFlags');
|
|
27
|
+
const {
|
|
28
|
+
createJavaScriptFlagGetter,
|
|
29
|
+
} = require('react-native/src/private/featureflags/ReactNativeFeatureFlagsBase');
|
|
30
|
+
|
|
31
|
+
const shim = Object.create(real);
|
|
32
|
+
Object.defineProperty(shim, 'shouldPressibilityUseW3CPointerEventsForHover', {
|
|
33
|
+
value: createJavaScriptFlagGetter(
|
|
34
|
+
'shouldPressibilityUseW3CPointerEventsForHover',
|
|
35
|
+
true,
|
|
36
|
+
),
|
|
37
|
+
enumerable: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
module.exports = shim;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// SC-18. RN sends style.fontFamily to Fabric untouched, and the C++ prop parser
|
|
3
|
+
// only accepts a string, so NativeWind's `font-mono` (an array) failed with
|
|
4
|
+
// "Error while converting prop 'fontFamily'". Re-export RN's own table with a
|
|
5
|
+
// fontFamily processor that joins arrays into one CSS stack; the engine picks
|
|
6
|
+
// the first family in the stack that resolves (engine/text/FontFamily.cpp).
|
|
7
|
+
// Required by explicit path from this file, which index.js does not redirect.
|
|
8
|
+
const real = require('react-native/Libraries/Components/View/ReactNativeStyleAttributes');
|
|
9
|
+
const attributes = real.default || real;
|
|
10
|
+
|
|
11
|
+
function processFontFamily(value) {
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return value.filter((v) => typeof v === 'string' && v.length > 0).join(', ');
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
attributes.fontFamily = {process: processFontFamily};
|
|
19
|
+
|
|
20
|
+
module.exports = real;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// parity item 7. TextInputState for the `impeller` platform.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS: RN's TextInputState.js sends the native focus/blur COMMAND
|
|
5
|
+
// only inside `if (Platform.OS === 'ios') ... else if (Platform.OS ===
|
|
6
|
+
// 'android')`, with no else. It reads Platform through a top-level
|
|
7
|
+
// `const Platform = require(...).default`, which Metro's inline-platform
|
|
8
|
+
// transform (metro-transform-plugins/src/utils/createInlinePlatformChecks.js)
|
|
9
|
+
// rewrites to the BUILD platform string ('impeller') before any resolver shim
|
|
10
|
+
// runs. Both branches are then dead code, the minifier deletes them, and
|
|
11
|
+
// TextInput ref.focus()/ref.blur() update JS bookkeeping and send nothing.
|
|
12
|
+
// The Platform-swap trick that fixes TextInput.js cannot reach this file
|
|
13
|
+
// (that file uses an ES import, which the inline pass does not match).
|
|
14
|
+
//
|
|
15
|
+
// NOT A DELEGATING WRAPPER: there is no .ios.js sibling to delegate to, and
|
|
16
|
+
// requiring the real file would redirect straight back here (SHIM_TARGETS
|
|
17
|
+
// matches the resolved path). So this is the upstream module with the
|
|
18
|
+
// platform branch fixed to the iOS commands, which target the same
|
|
19
|
+
// RCTSinglelineTextInputView that TextInput.js mounts on impeller. Every
|
|
20
|
+
// importer resolves here, so the focused-input bookkeeping stays single.
|
|
21
|
+
const {findNodeHandle} = require('react-native/Libraries/ReactNative/RendererProxy');
|
|
22
|
+
const {
|
|
23
|
+
Commands: iOSTextInputCommands,
|
|
24
|
+
} = require('react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent');
|
|
25
|
+
|
|
26
|
+
let currentlyFocusedInputRef = null;
|
|
27
|
+
const inputs = new Set();
|
|
28
|
+
|
|
29
|
+
function currentlyFocusedInput() {
|
|
30
|
+
return currentlyFocusedInputRef;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function currentlyFocusedField() {
|
|
34
|
+
return findNodeHandle(currentlyFocusedInputRef);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function focusInput(textField) {
|
|
38
|
+
if (currentlyFocusedInputRef !== textField && textField != null) {
|
|
39
|
+
currentlyFocusedInputRef = textField;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function blurInput(textField) {
|
|
44
|
+
if (currentlyFocusedInputRef === textField && textField != null) {
|
|
45
|
+
currentlyFocusedInputRef = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function focusField() {}
|
|
50
|
+
function blurField() {}
|
|
51
|
+
|
|
52
|
+
function focusTextInput(textField) {
|
|
53
|
+
if (typeof textField === 'number' || textField == null) return;
|
|
54
|
+
const fieldCanBeFocused =
|
|
55
|
+
currentlyFocusedInputRef !== textField &&
|
|
56
|
+
textField.currentProps?.editable !== false;
|
|
57
|
+
if (!fieldCanBeFocused) return;
|
|
58
|
+
focusInput(textField);
|
|
59
|
+
iOSTextInputCommands.focus(textField);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function blurTextInput(textField) {
|
|
63
|
+
if (typeof textField === 'number') return;
|
|
64
|
+
if (currentlyFocusedInputRef === textField && textField != null) {
|
|
65
|
+
blurInput(textField);
|
|
66
|
+
iOSTextInputCommands.blur(textField);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function registerInput(textField) {
|
|
71
|
+
if (typeof textField === 'number') return;
|
|
72
|
+
inputs.add(textField);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function unregisterInput(textField) {
|
|
76
|
+
if (typeof textField === 'number') return;
|
|
77
|
+
inputs.delete(textField);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isTextInput(textField) {
|
|
81
|
+
if (typeof textField === 'number') return false;
|
|
82
|
+
return inputs.has(textField);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const TextInputState = {
|
|
86
|
+
currentlyFocusedInput,
|
|
87
|
+
focusInput,
|
|
88
|
+
blurInput,
|
|
89
|
+
currentlyFocusedField,
|
|
90
|
+
focusField,
|
|
91
|
+
blurField,
|
|
92
|
+
focusTextInput,
|
|
93
|
+
blurTextInput,
|
|
94
|
+
registerInput,
|
|
95
|
+
unregisterInput,
|
|
96
|
+
isTextInput,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
module.exports = TextInputState;
|
|
100
|
+
module.exports.default = TextInputState;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// desktop-input-gap item 6. ConditionallyIgnoredEventHandlers for 'impeller'.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS: RN core's ViewConfigIgnore.js strips onClick/onPointerDown/
|
|
5
|
+
// onPointerUp/onPointerEnter/... out of BaseViewConfig's validAttributes
|
|
6
|
+
// unless Platform.OS === 'ios' (ConditionallyIgnoredEventHandlers returns the
|
|
7
|
+
// object UNCHANGED on ios, `undefined` on every other platform, including our
|
|
8
|
+
// own 'impeller'). A stripped validAttribute means React never forwards the
|
|
9
|
+
// onClick/onPointer* PROP into the native view config at all, so
|
|
10
|
+
// PointerEventsProcessor::shouldEmitPointerEvent reads an all-zero ViewEvents
|
|
11
|
+
// bitset and swallows the event before it reaches JS -- even though
|
|
12
|
+
// FabricHost dispatches a perfectly correct PointerEvent on the C++ side.
|
|
13
|
+
// NATIVE FIRES, JS NEVER HEARS. Measured 2026-09-30: a real --hover-after-frames
|
|
14
|
+
// run printed native `hover_enter`/`cursor=` lines while the fixture's JS
|
|
15
|
+
// onPointerEnter console.log never fired, with a clean exit and no error.
|
|
16
|
+
//
|
|
17
|
+
// NOT A DELEGATING WRAPPER. ViewConfigIgnore.js has no `.ios.js`/`.android.js`
|
|
18
|
+
// sibling, so BaseViewConfig.impeller.js's trick (require the sibling by its
|
|
19
|
+
// EXPLICIT platform extension, bypassing the redirect machinery) is
|
|
20
|
+
// unavailable here: requiring the bare specifier from inside this file would
|
|
21
|
+
// be redirected straight back to this same file by shimForResolvedPath
|
|
22
|
+
// (SHIM_TARGETS matches the RESOLVED path, not who asked), an infinite
|
|
23
|
+
// require cycle. So this reimplements the ~15 lines of real upstream logic
|
|
24
|
+
// instead of delegating to them; every caller on the impeller platform
|
|
25
|
+
// resolves to this one file, so the WeakSet stays globally consistent.
|
|
26
|
+
const ignoredViewConfigProps = new WeakSet();
|
|
27
|
+
|
|
28
|
+
function DynamicallyInjectedByGestureHandler(object) {
|
|
29
|
+
ignoredViewConfigProps.add(object);
|
|
30
|
+
return object;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// THE FIX: unconditionally honour onPointerDown/onPointerUp/onClick/... on
|
|
34
|
+
// the impeller platform, instead of upstream's `OS === 'ios' ? value :
|
|
35
|
+
// undefined`. See the file header for why stripping these is a native-fires/
|
|
36
|
+
// JS-never-hears bug, not a feature gate.
|
|
37
|
+
function ConditionallyIgnoredEventHandlers(value) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isIgnored(value) {
|
|
42
|
+
if (typeof value === 'object' && value != null) {
|
|
43
|
+
return ignoredViewConfigProps.has(value);
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
DynamicallyInjectedByGestureHandler,
|
|
50
|
+
ConditionallyIgnoredEventHandlers,
|
|
51
|
+
isIgnored,
|
|
52
|
+
};
|
|
53
|
+
module.exports.DynamicallyInjectedByGestureHandler = DynamicallyInjectedByGestureHandler;
|
|
54
|
+
module.exports.ConditionallyIgnoredEventHandlers = ConditionallyIgnoredEventHandlers;
|
|
55
|
+
module.exports.isIgnored = isIgnored;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// SC-26. The one JS-thread frame loop behind every animation in the
|
|
3
|
+
// reanimated stub. requestAnimationFrame on impeller is a setTimeout(0), so a
|
|
4
|
+
// rAF loop busy-spins the JS thread; frames are paced with setTimeout(16) and
|
|
5
|
+
// timed with performance.now(). The loop stops when nothing animates, so an
|
|
6
|
+
// idle app holds no timer (headless captures wait for JS idle).
|
|
7
|
+
const FRAME_MS = 16;
|
|
8
|
+
const subscribers = new Set();
|
|
9
|
+
let timer = null;
|
|
10
|
+
|
|
11
|
+
function now() {
|
|
12
|
+
return typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function tick() {
|
|
16
|
+
timer = null;
|
|
17
|
+
const t = now();
|
|
18
|
+
for (const fn of Array.from(subscribers)) {
|
|
19
|
+
let keep = false;
|
|
20
|
+
try {
|
|
21
|
+
keep = fn(t);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.error(e);
|
|
24
|
+
}
|
|
25
|
+
if (!keep) subscribers.delete(fn);
|
|
26
|
+
}
|
|
27
|
+
schedule();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function schedule() {
|
|
31
|
+
if (timer == null && subscribers.size > 0) timer = setTimeout(tick, FRAME_MS);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// fn(now) runs once per frame while it returns true.
|
|
35
|
+
function onFrame(fn) {
|
|
36
|
+
subscribers.add(fn);
|
|
37
|
+
schedule();
|
|
38
|
+
return () => subscribers.delete(fn);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {now, onFrame};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// SC-26. createAnimatedComponent for the stub: a wrapper that resolves
|
|
3
|
+
// useAnimatedStyle / useAnimatedProps handles and CSS animation/transition
|
|
4
|
+
// style props to plain values on every render, and re-renders itself on the
|
|
5
|
+
// clock while anything is moving.
|
|
6
|
+
const React = require('react');
|
|
7
|
+
const {StyleSheet} = require('react-native');
|
|
8
|
+
const {now, onFrame} = require('./clock');
|
|
9
|
+
const {HANDLE} = require('./hooks');
|
|
10
|
+
const css = require('./css');
|
|
11
|
+
|
|
12
|
+
// Replace every animated-style handle in a (nested) style with its current
|
|
13
|
+
// values, collecting the handles to subscribe to.
|
|
14
|
+
function resolveHandles(style, handles) {
|
|
15
|
+
if (Array.isArray(style)) return style.map((s) => resolveHandles(s, handles));
|
|
16
|
+
if (style && typeof style === 'object' && style[HANDLE]) {
|
|
17
|
+
handles.push(style[HANDLE]);
|
|
18
|
+
return style[HANDLE].current();
|
|
19
|
+
}
|
|
20
|
+
return style;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function useFrames(activeRef, force) {
|
|
24
|
+
const stop = React.useRef(null);
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
if (activeRef.current && !stop.current) {
|
|
27
|
+
stop.current = onFrame(() => {
|
|
28
|
+
if (!activeRef.current) {
|
|
29
|
+
stop.current = null;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
force();
|
|
33
|
+
return true;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
React.useEffect(() => () => stop.current && stop.current(), []);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const cache = new WeakMap();
|
|
41
|
+
|
|
42
|
+
function createAnimatedComponent(Component) {
|
|
43
|
+
if (Component == null || (typeof Component !== 'function' && typeof Component !== 'object')) return Component;
|
|
44
|
+
if (cache.has(Component)) return cache.get(Component);
|
|
45
|
+
function Animated(props, ref) {
|
|
46
|
+
const {style, animatedProps, entering, exiting, layout, ...rest} = props;
|
|
47
|
+
const [, force] = React.useReducer((x) => x + 1, 0);
|
|
48
|
+
const handles = [];
|
|
49
|
+
const resolved = resolveHandles(style, handles);
|
|
50
|
+
let extra = null;
|
|
51
|
+
if (animatedProps && animatedProps[HANDLE]) {
|
|
52
|
+
handles.push(animatedProps[HANDLE]);
|
|
53
|
+
extra = animatedProps[HANDLE].current();
|
|
54
|
+
}
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
const unsubs = handles.map((h) => h.subscribe(force));
|
|
57
|
+
return () => unsubs.forEach((u) => u());
|
|
58
|
+
});
|
|
59
|
+
const state = React.useRef(null);
|
|
60
|
+
if (!state.current) state.current = css.newState();
|
|
61
|
+
const activeRef = React.useRef(false);
|
|
62
|
+
const flat = StyleSheet.flatten(resolved) || {};
|
|
63
|
+
const parts = css.split(flat);
|
|
64
|
+
let finalStyle = style;
|
|
65
|
+
if (parts.css || state.current.last) {
|
|
66
|
+
const r = css.evaluate(state.current, parts.base, parts.css, now());
|
|
67
|
+
activeRef.current = r.active;
|
|
68
|
+
finalStyle = r.style;
|
|
69
|
+
} else if (handles.length) {
|
|
70
|
+
finalStyle = flat;
|
|
71
|
+
}
|
|
72
|
+
useFrames(activeRef, force);
|
|
73
|
+
return React.createElement(Component, {...rest, ...(extra || {}), style: finalStyle, ref});
|
|
74
|
+
}
|
|
75
|
+
const name = Component.displayName || Component.name || 'Component';
|
|
76
|
+
Animated.displayName = `AnimatedComponent(${name})`;
|
|
77
|
+
const Wrapped = React.forwardRef(Animated);
|
|
78
|
+
Wrapped.displayName = Animated.displayName;
|
|
79
|
+
cache.set(Component, Wrapped);
|
|
80
|
+
return Wrapped;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {createAnimatedComponent};
|