Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ module.name_mapper='^react-native/src/fb_internal/\(.*\)$' -> '<PROJECT_ROOT>/pa
# `src/fb_internal/*` stay importable.
module.name_mapper='^react-native/\(Libraries\|src/private\|src/types\)/.*$' -> 'react-native-deep-import-is-not-part-of-the-public-api'
module.name_mapper='^@react-native/dev-middleware$' -> '<PROJECT_ROOT>/packages/dev-middleware'
module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\|xml\|ktx\|heic\|heif\)$' -> '<PROJECT_ROOT>/packages/react-native/Libraries/Image/RelativeImageStub'
module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\|xml\|ktx\|heic\|ttf\|heif\)$' -> '<PROJECT_ROOT>/packages/react-native/Libraries/Image/RelativeImageStub'

module.system.haste.module_ref_prefix=m#

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@babel/preset-env": "^7.25.3",
"@babel/preset-flow": "^7.24.7",
"@electron/packager": "^20.0.0",
"@expo-google-fonts/noto-color-emoji": "0.4.7",
"@expo/spawn-async": "^1.7.2",
"@jest/create-cache-key-function": "^29.7.0",
"@jest/test-sequencer": "^29.7.0",
Expand All @@ -82,6 +83,7 @@
"connect": "^3.6.5",
"debug": "^4.4.0",
"deep-equal": "1.1.1",
"dejavu-fonts-ttf": "2.37.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-babel": "^5.3.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native/Libraries/Image/ImageTypes.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {HostInstance} from '../..';
import type {RootTag} from '../Types/RootTagTypes';
import type {ResolvedAssetSource} from './AssetSourceResolver';
import type {ImageProps as ImagePropsType} from './ImageProps';
import type {ImageSource} from './ImageSource';
import typeof resolveAssetSource from './resolveAssetSource';

import * as React from 'react';

Expand Down Expand Up @@ -59,7 +59,7 @@ type ImageComponentStaticsIOS = Readonly<{
/**
* @see https://reactnative.dev/docs/image#resolveassetsource
*/
resolveAssetSource(source: ImageSource): ?ImageResolvedAssetSource,
resolveAssetSource: resolveAssetSource,
}>;

type ImageComponentStaticsAndroid = Readonly<{
Expand Down
20 changes: 13 additions & 7 deletions packages/react-native/ReactNativeApi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<eda44383e13efcfb4f814115c4b44547>>
* @generated SignedSource<<fcf142a69b68104edc8bfe851de32d6f>>
*
* This file was generated by scripts/js-api/build-types/index.js.
*/
Expand Down Expand Up @@ -75,6 +75,7 @@ declare const $$index: {
}
declare const $$NativeDeviceInfo: typeof NativeModule_default
declare const $$ReactFabric: typeof ReactFabric_default
declare const $$resolveAssetSource: typeof resolveAssetSource_default
declare const $$ScrollViewContext: typeof ScrollViewContext_default
declare const absoluteFill: AbsoluteFillStyle
declare const AccessibilityInfo: typeof AccessibilityInfo_default
Expand Down Expand Up @@ -917,6 +918,7 @@ declare type $$flattenStyle = typeof $$flattenStyle
declare type $$index = typeof $$index
declare type $$NativeDeviceInfo = typeof $$NativeDeviceInfo
declare type $$ReactFabric = typeof $$ReactFabric
declare type $$resolveAssetSource = typeof $$resolveAssetSource
declare type $$ScrollViewContext = typeof $$ScrollViewContext
declare type absoluteFill = typeof absoluteFill
declare type AbsoluteFillStyle = {
Expand Down Expand Up @@ -2437,6 +2439,7 @@ declare type ImageComponentStaticsAndroid = Readonly<
}
>
declare type ImageComponentStaticsIOS = {
readonly resolveAssetSource: resolveAssetSource
getSize(uri: string): Promise<ImageSize>
getSize(
uri: string,
Expand Down Expand Up @@ -2466,7 +2469,6 @@ declare type ImageComponentStaticsIOS = {
queryCache(urls: Array<string>): Promise<{
[url: string]: "disk" | "disk/memory" | "memory"
}>
resolveAssetSource(source: ImageSource): ImageResolvedAssetSource | undefined
}
declare type ImageErrorEvent = NativeSyntheticEvent<
Readonly<ImageErrorEventData>
Expand Down Expand Up @@ -4116,6 +4118,10 @@ declare type RequiredVirtualizedSectionListProps<
sections: ReadonlyArray<SectionListData<ItemT, SectionT>>
}
declare type requireNativeComponent = typeof requireNativeComponent
declare type resolveAssetSource = typeof $$resolveAssetSource
declare function resolveAssetSource_default(
source: ImageSource | null | undefined,
): null | ResolvedAssetSource | undefined
declare type ResolvedAssetSource = {
readonly height: number | undefined
readonly scale: number
Expand Down Expand Up @@ -5791,7 +5797,7 @@ export {
AlertOptions, // 8a116d2a
AlertType, // 5ab91217
AndroidKeyboardEvent, // e03becc8
Animated, // 33fe7464
Animated, // adba9272
AppConfig, // 35c0ca70
AppRegistry, // edec2a30
AppState, // 12012be5
Expand Down Expand Up @@ -5856,10 +5862,10 @@ export {
IEventEmitter, // fbef6131
IOSKeyboardEvent, // e67bfe3a
IgnorePattern, // ec6f6ece
Image, // a24a5777
ImageBackground, // 1fbbacef
ImageBackgroundInstance, // 9c900a60
ImageBackgroundProps, // b078f0ac
Image, // 404e5c9b
ImageBackground, // d2afc8de
ImageBackgroundInstance, // 2e9ed91b
ImageBackgroundProps, // d49fc98c
ImageErrorEvent, // b6227714
ImageInstance, // df709524
ImageLoadEvent, // 31709c38
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ interface Spec extends TurboModule {
surfaceId: RootTag,
config: RenderFormatOptions,
) => string;
registerAsset(uri: string, filePath: string): void;
registerFont(filePath: string): void;
reportTestSuiteResultsJSON: (results: string) => void;
createShadowNodeReferenceCounter(
shadowNode: unknown /* ShadowNode */,
Expand Down
5 changes: 5 additions & 0 deletions private/react-native-fantom/__docs__/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ values. If another root is still alive and the new metrics differ from the
current global dimensions, Fantom applies the new values and emits a warning.
Call `root.destroy()` when a root is no longer needed.

Each root also uses its `fontScale` for native text and text-input measurement,
including line heights and wrapping. `allowFontScaling={false}` opts a component
out, and `maxFontSizeMultiplier` caps its scale. Existing roots retain their own
native font scale when a new root changes the global display metrics.

For a full API reference, please see the [inline documentation](../src/index.js)
defined for the methods in the `@react-native/fantom` [module](../src/index.js).

Expand Down
78 changes: 78 additions & 0 deletions private/react-native-fantom/__docs__/rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Rendering

## Text measurement

Fantom always measures intrinsic `Text` and `TextInput` sizes with Skia's CPU
font metrics. Layout and screenshot painting share the same text layout code,
including fragment styles, constrained-width wrapping, inline attachments, input
values and placeholders. `maximumNumberOfLines` limits the measured box; clipped
attachments retain their entries in Fabric's attachment mapping.

A root's `fontScale` defaults to `1` and is passed to native layout. Text and
inputs respect `allowFontScaling` and `maxFontSizeMultiplier`; line heights
scale with the font size. `devicePixelRatio` determines raster resolution,
rather than the logical text size.

### Fonts and assets

Fantom registers DejaVu Sans regular, bold, italic and bold italic faces from
[`dejavu-fonts-ttf`](https://www.npmjs.com/package/dejavu-fonts-ttf), plus
[`@expo-google-fonts/noto-color-emoji`](https://www.npmjs.com/package/@expo-google-fonts/noto-color-emoji).
Both are pinned Fantom dependencies installed by the repository's normal Yarn
setup. Their font licenses are compatible with React Native distribution; the
packages retain the Bitstream Vera/DejaVu and SIL Open Font License notices.
Registration runs once during Fantom's default environment setup. Tests that
configure React Native directly call `setUpFantomEnvironment()` themselves. No
host font installation or Expo runtime is required.

Metro preserves the source files of bundled assets in Fantom's asset metadata.
The shared resolver selects the asset variant for the root's pixel ratio and
registers its native file path. Ordinary `require()` sources therefore work
without custom path mappings. Keep a fixture alongside the test that uses it.

Default color emoji selection follows Unicode 16.0 `Emoji_Presentation`.
Explicit text and emoji variation selectors override that default. Selectors and
keycap sequences stay with their bases during fallback and wrapping; HarfBuzz
shapes color emoji sequences for both measurement and painting. General script
shaping, text decorations and ellipsis are not implemented.

### Public native build

`yarn workspace @react-native/fantom build` uses Gradle to prepare pinned Skia,
FreeType, libpng, zlib, HarfBuzz and BoringSSL sources. CMake builds HarfBuzz
and BoringSSL as static libraries, and builds Skia with GN and the Android SDK's
Ninja. BoringSSL provides SHA-256 for screenshot pixel hashes; the existing
Boost dependency has SHA-1 and MD5 but no SHA-256 implementation. GN bootstrap
uses Python 3. The usual JDK, Android SDK, C/C++ compiler and Python build tools
are required; graphics, crypto and font libraries come from these prepared
sources or bundled assets.

The native Gradle task tracks C++ sources so edits rebuild the tester.
Dependency versions and download locations are defined in
[`build.gradle.kts`](../build.gradle.kts). Native integration is configured in
[`tester/CMakeLists.txt`](../tester/CMakeLists.txt) and its third-party
subdirectories.

### Consistent raster configuration

Tests share snapshot baselines across Linux and macOS. Different font metrics
can change line wrapping and the placement of other views; different raster
rounding can change pixel hashes even when the images look alike. Keeping both
consistent makes a failing snapshot useful evidence of a rendering change,
independent of the machine running the test.

Both native builds use CPU Skia, FreeType and the same bundled fonts. Host font
managers and platform shaping backends are disabled. Text gamma correction is
configured identically in both builds.

Fantom disables the legacy shader context and lower precision raster pipeline,
forces the raster pipeline blitter, and selects scalar high precision stages.
Floating-point contraction is disabled with `-ffp-contract=off`. This avoids ARM
and x86 optimized stages producing different rounded channel values when
compositing the same pixels, including color emoji. These settings apply only to
Fantom's Skia library.

The scalar selector is an optional `SK_RASTER_PIPELINE_FORCE_SCALAR` definition
in the pinned Skia source. Gradle patches its prepared source copy, and CMake
validates the selector without modifying the source. Keep the internal target's
flags and the public GN flags aligned when changing Skia.
113 changes: 113 additions & 0 deletions private/react-native-fantom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,114 @@ val prepareNlohmannJson by
into("$thirdParty/nlohmann_json")
}

val boringsslVersion = "603b54d3d338efde4b84ece7be9aa410e44b1417"
val boringsslArchive = File(downloadsDir, "boringssl-$boringsslVersion.tar.gz")
val downloadBoringSSL by
tasks.registering(Download::class) {
dependsOn(createNativeDepsDirectories)
src("https://github.com/google/boringssl/archive/$boringsslVersion.tar.gz")
onlyIfModified(true)
overwrite(false)
retries(5)
quiet(true)
dest(boringsslArchive)
}
val prepareBoringSSL by
tasks.registering(Copy::class) {
dependsOn(downloadBoringSSL)
from(tarTree(boringsslArchive))
eachFile { path = path.substringAfter("/") }
includeEmptyDirs = false
into("$thirdParty/boringssl")
}

// Match the CPU Skia revision used by the native tester in Buck.
val skiaVersion = "3f8fc145ba678f3bd47af50f8fb51bcde474ef9c"

fun downloadSkiaSource(name: String, url: String, destination: File) =
tasks.register<Download>("downloadSkia${name}") {
dependsOn(createNativeDepsDirectories)
src(url)
onlyIfModified(true)
overwrite(false)
retries(5)
quiet(true)
dest(destination)
}

val skiaArchive = File(downloadsDir, "skia-$skiaVersion.tar.gz")
val downloadSkia =
downloadSkiaSource(
"Core",
"https://github.com/google/skia/archive/$skiaVersion.tar.gz",
skiaArchive,
)
val prepareSkiaSource by
tasks.registering(Copy::class) {
dependsOn(downloadSkia)
from(tarTree(skiaArchive))
from("tester/third-party/skia/CMakeLists.txt")
eachFile { path = path.substringAfter("/") }
filesMatching("**/SkRasterPipeline_opts.h") {
filter { line ->
line.replace(
"#if !defined(__clang__)",
"#if defined(SK_RASTER_PIPELINE_FORCE_SCALAR) || !defined(__clang__)",
)
}
}
filesMatching("**/SkBlitter_Sprite.cpp") {
filter { line ->
line
.replace(
" if (gSkForceRasterPipelineBlitter) {",
"#if !defined(SK_FORCE_RASTER_PIPELINE_BLITTER)\n if (gSkForceRasterPipelineBlitter) {",
)
.replace(
" if (!blitter && !paint.getMaskFilter()) {",
"#endif\n if (!blitter && !paint.getMaskFilter()) {",
)
}
}
includeEmptyDirs = false
into("$thirdParty/skia")
}

// GN builds these dependencies into libskia, including FreeType's color PNG glyph support.
val skiaDependencies =
mapOf(
"freetype" to "https://github.com/freetype/freetype/archive/refs/tags/VER-2-14-2.tar.gz",
"harfbuzz" to
"https://chromium.googlesource.com/external/github.com/harfbuzz/harfbuzz.git/+archive/8d1b000a3edc90c12267b836b4ef3f81c0e53edc.tar.gz",
"libpng" to "https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.55.tar.gz",
"zlib" to
"https://chromium.googlesource.com/chromium/src/third_party/zlib/+archive/c876c8f87101c5a75f6014b0f832499afeb65b73.tar.gz",
)
val prepareSkiaDependencies =
skiaDependencies.map { (name, url) ->
val archive = File(downloadsDir, "skia-$name-${url.substringAfterLast('/')}")
val download = downloadSkiaSource(name.replaceFirstChar { it.uppercase() }, url, archive)
tasks.register<Copy>("prepareSkia${name.replaceFirstChar { it.uppercase() }}") {
dependsOn(download, prepareSkiaSource)
from(tarTree(archive))
if (name == "freetype" || name == "libpng") {
eachFile { path = path.substringAfter("/") }
}
if (name == "zlib") {
filesMatching("zutil.h") {
filter { line ->
line.replace(
"defined(TARGET_OS_MAC)",
"(defined(TARGET_OS_MAC) && !defined(__APPLE__))",
)
}
}
}
includeEmptyDirs = false
into("$thirdParty/skia/third_party/externals/$name")
}
}

var codegenSrcDir = File("$reactAndroidBuildDir/generated/source/codegen/jni")
var codegenOutDir = File("$buildDir/codegen")
val prepareRNCodegen by
Expand Down Expand Up @@ -166,6 +274,9 @@ val prepareNative3pDependencies by
prepareGflags,
prepareNlohmannJson,
prepareFolly,
prepareBoringSSL,
prepareSkiaSource,
*prepareSkiaDependencies.toTypedArray(),
":packages:react-native:ReactAndroid:prepareBoost",
":packages:react-native:ReactAndroid:prepareDoubleConversion",
":packages:react-native:ReactAndroid:prepareFastFloat",
Expand Down Expand Up @@ -197,6 +308,7 @@ val configureFantomTester by
"-DCMAKE_BUILD_TYPE=Debug",
"-DFANTOM_CODEGEN_DIR=$buildDir/codegen",
"-DFANTOM_THIRD_PARTY_DIR=$buildDir/third-party",
"-DFANTOM_NINJA=$cmakePath/bin/ninja",
"-DREACT_ANDROID_DIR=$reactAndroidDir",
"-DREACT_COMMON_DIR=$reactNativeDir/ReactCommon",
"-DREACT_CXX_PLATFORM_DIR=$reactNativeDir/ReactCxxPlatform",
Expand All @@ -215,6 +327,7 @@ val buildFantomTester by
tasks.registering(CustomExecTask::class) {
dependsOn(configureFantomTester)
workingDir(testerDir)
inputs.dir(testerDir)
inputs.files(testerBuildOutputFileTree)
commandLine(
cmakeBinaryPath,
Expand Down
19 changes: 19 additions & 0 deletions private/react-native-fantom/config/assetPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

/*::
import type {AssetData} from 'metro/private/Assets';
*/

module.exports = function assetPlugin /*:: <T extends AssetData> */(
asset /*: T */,
) /*: {...T, fantomFiles: Array<string>, ...} */ {
return {...asset, fantomFiles: asset.files};
};
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ module.exports = {
const cacheKeyParts = [
MetroBabelTransformer.getCacheKey?.() ?? '',
fs.readFileSync(__filename),
fs.readFileSync(require.resolve('./assetPlugin')),
fs.readFileSync(
require.resolve('./babel-plugins/inject-debugger-statements-in-tests'),
),
Expand Down
1 change: 1 addition & 0 deletions private/react-native-fantom/config/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const config /*: InputConfigT */ = {
: null,
},
transformer: {
assetPlugins: [require.resolve('./assetPlugin')],
// We need to wrap the default transformer so we can run it from source
// using babel-register.
babelTransformerPath: path.resolve(__dirname, 'metro-babel-transformer.js'),
Expand Down
Loading
Loading