Skip to content

Commit be7180b

Browse files
rubennortemeta-codesync[bot]
authored andcommitted
Set global Dimensions when creating Fantom roots (#59350)
Summary: Pull Request resolved: #59350 Set global `Dimensions` from each new Fantom root's viewport size, pixel ratio and font scale. Restore the latest surviving root when a root is destroyed, use the root defaults before creating a root and after destroying the last one, and warn when active roots have conflicting metrics. Pass each root's `fontScale` to native layout. Changelog: [Internal] Reviewed By: christophpurrer Differential Revision: D126645992 fbshipit-source-id: e1ab5bcb85fa2f3bf8d7cfd5a44ddbd9c8941cfe
1 parent 4bba13d commit be7180b

9 files changed

Lines changed: 277 additions & 1 deletion

File tree

‎packages/react-native/src/private/testing/fantom/specs/NativeFantom.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ interface Spec extends TurboModule {
7878
viewportWidth: number,
7979
viewportHeight: number,
8080
devicePixelRatio: number,
81+
fontScale: number,
8182
viewportOffsetX?: number,
8283
viewportOffsetY?: number,
8384
) => RootTag;

‎private/react-native-fantom/__docs__/README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ describe('My feature', () => {
9292
});
9393
```
9494

95+
Creating a root sets global `Dimensions` for both `window` and `screen` from its
96+
`viewportWidth`, `viewportHeight`, `devicePixelRatio`, and optional `fontScale`
97+
(default `1`). `useWindowDimensions()` and `PixelRatio` report the new root's
98+
values. If another root is still alive with different display metrics, Fantom
99+
applies the new values and emits a warning. Call `root.destroy()` when a root is
100+
no longer needed.
101+
95102
For a full API reference, please see the [inline documentation](../src/index.js)
96103
defined for the methods in the `@react-native/fantom` [module](../src/index.js).
97104

‎private/react-native-fantom/src/__tests__/Fantom-itest.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ describe('Fantom', () => {
320320
viewportOffsetY: 102,
321321
},
322322
);
323+
rootWithDefaults.destroy();
324+
rootWithCustomWidthAndHeight.destroy();
323325
});
324326

325327
it('throws when trying to render a root outside of a task', () => {
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
/* global console: writable */
12+
13+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
14+
15+
import type {Root, RootConfig} from '@react-native/fantom';
16+
17+
import * as Fantom from '@react-native/fantom';
18+
import * as React from 'react';
19+
import {Dimensions, PixelRatio, View, useWindowDimensions} from 'react-native';
20+
21+
let roots: Array<Root>;
22+
let originalConsole: typeof console;
23+
let warn: JestMockFn<Array<unknown>, void>;
24+
25+
beforeEach(() => {
26+
roots = [];
27+
originalConsole = console;
28+
warn = jest.fn();
29+
console = Object.create(originalConsole, {warn: {value: warn}});
30+
});
31+
32+
afterEach(() => {
33+
for (const root of roots) {
34+
Fantom.runTask(() => root.render(<></>));
35+
root.destroy();
36+
}
37+
console = originalConsole;
38+
});
39+
40+
function createRoot(config?: RootConfig): Root {
41+
const root = Fantom.createRoot(config);
42+
roots.push(root);
43+
return root;
44+
}
45+
46+
component WindowMetrics() {
47+
const {width, height, scale, fontScale} = useWindowDimensions();
48+
return <View testID={`${width}x${height}@${scale}:${fontScale}`} />;
49+
}
50+
51+
it('uses root defaults before any root is created', () => {
52+
for (const dimension of ['window', 'screen']) {
53+
const metrics = Dimensions.get(dimension);
54+
expect(metrics.width).toBe(390);
55+
expect(metrics.height).toBe(844);
56+
expect(metrics.scale).toBe(3);
57+
expect(metrics.fontScale).toBe(1);
58+
}
59+
expect(PixelRatio.get()).toBe(3);
60+
expect(PixelRatio.getFontScale()).toBe(1);
61+
});
62+
63+
it('sets default window and screen metrics', () => {
64+
createRoot();
65+
for (const dimension of ['window', 'screen']) {
66+
const metrics = Dimensions.get(dimension);
67+
expect(metrics.width).toBe(390);
68+
expect(metrics.height).toBe(844);
69+
expect(metrics.scale).toBe(3);
70+
expect(metrics.fontScale).toBe(1);
71+
}
72+
});
73+
74+
it('sets custom viewport, pixel ratio, and font scale', () => {
75+
createRoot({
76+
viewportWidth: 640,
77+
viewportHeight: 480,
78+
devicePixelRatio: 2,
79+
fontScale: 1.5,
80+
});
81+
for (const dimension of ['window', 'screen']) {
82+
const metrics = Dimensions.get(dimension);
83+
expect(metrics.width).toBe(640);
84+
expect(metrics.height).toBe(480);
85+
expect(metrics.scale).toBe(2);
86+
expect(metrics.fontScale).toBe(1.5);
87+
}
88+
expect(PixelRatio.get()).toBe(2);
89+
expect(PixelRatio.getFontScale()).toBe(1.5);
90+
});
91+
92+
it('does not warn for matching metrics, including different viewport offsets', () => {
93+
createRoot();
94+
createRoot({viewportOffsetX: 20, viewportOffsetY: 90});
95+
expect(warn).not.toHaveBeenCalled();
96+
});
97+
98+
const conflictingConfigs: Array<[string, RootConfig]> = [
99+
['width', {viewportWidth: 402}],
100+
['height', {viewportHeight: 874}],
101+
['pixel ratio', {devicePixelRatio: 2}],
102+
['font scale', {fontScale: 1.5}],
103+
];
104+
105+
for (const [name, config] of conflictingConfigs) {
106+
it(`warns and applies new metrics when an active root has a different ${name}`, () => {
107+
createRoot();
108+
createRoot(config);
109+
expect(warn).toHaveBeenCalledTimes(1);
110+
expect(warn.mock.calls[0][0]).toContain('another active root');
111+
const metrics = Dimensions.get('window');
112+
expect(metrics.width).toBe(config.viewportWidth ?? 390);
113+
expect(metrics.height).toBe(config.viewportHeight ?? 844);
114+
expect(metrics.scale).toBe(config.devicePixelRatio ?? 3);
115+
expect(metrics.fontScale).toBe(config.fontScale ?? 1);
116+
});
117+
}
118+
119+
it('checks all active roots, including one that differs from the latest root', () => {
120+
createRoot();
121+
createRoot({viewportWidth: 640});
122+
createRoot({viewportWidth: 640});
123+
expect(warn).toHaveBeenCalledTimes(2);
124+
expect(Dimensions.get('window').width).toBe(640);
125+
});
126+
127+
it('does not warn for a destroyed root', () => {
128+
const first = createRoot();
129+
first.destroy();
130+
roots.shift();
131+
createRoot({viewportWidth: 640});
132+
expect(warn).not.toHaveBeenCalled();
133+
});
134+
135+
it('restores the latest surviving root and then defaults when all roots are destroyed', () => {
136+
const first = createRoot();
137+
const second = createRoot({
138+
viewportWidth: 640,
139+
viewportHeight: 480,
140+
devicePixelRatio: 2,
141+
fontScale: 1.5,
142+
});
143+
const third = createRoot({viewportWidth: 900});
144+
third.destroy();
145+
roots.pop();
146+
for (const dimension of ['window', 'screen']) {
147+
const metrics = Dimensions.get(dimension);
148+
expect(metrics.width).toBe(640);
149+
expect(metrics.height).toBe(480);
150+
expect(metrics.scale).toBe(2);
151+
expect(metrics.fontScale).toBe(1.5);
152+
}
153+
first.destroy();
154+
roots.shift();
155+
for (const dimension of ['window', 'screen']) {
156+
const metrics = Dimensions.get(dimension);
157+
expect(metrics.width).toBe(640);
158+
expect(metrics.height).toBe(480);
159+
expect(metrics.scale).toBe(2);
160+
expect(metrics.fontScale).toBe(1.5);
161+
}
162+
second.destroy();
163+
roots.pop();
164+
for (const dimension of ['window', 'screen']) {
165+
const metrics = Dimensions.get(dimension);
166+
expect(metrics.width).toBe(390);
167+
expect(metrics.height).toBe(844);
168+
expect(metrics.scale).toBe(3);
169+
expect(metrics.fontScale).toBe(1);
170+
}
171+
expect(PixelRatio.get()).toBe(3);
172+
expect(PixelRatio.getFontScale()).toBe(1);
173+
});
174+
175+
it('notifies mounted components when another root changes the metrics', () => {
176+
const first = createRoot();
177+
Fantom.runTask(() => first.render(<WindowMetrics />));
178+
expect(first.getRenderedOutput({props: ['testID']}).toJSX()).toEqual(
179+
<rn-view testID="390x844@3:1" />,
180+
);
181+
const second = createRoot({
182+
viewportWidth: 640,
183+
viewportHeight: 480,
184+
devicePixelRatio: 2,
185+
fontScale: 1.5,
186+
});
187+
Fantom.runTask(() => second.render(<WindowMetrics />));
188+
expect(warn).toHaveBeenCalledTimes(1);
189+
for (const root of [first, second]) {
190+
expect(root.getRenderedOutput({props: ['testID']}).toJSX()).toEqual(
191+
<rn-view testID="640x480@2:1.5" />,
192+
);
193+
}
194+
Fantom.runTask(() => second.render(<></>));
195+
second.destroy();
196+
roots.pop();
197+
Fantom.runWorkLoop();
198+
expect(first.getRenderedOutput({props: ['testID']}).toJSX()).toEqual(
199+
<rn-view testID="390x844@3:1" />,
200+
);
201+
});

‎private/react-native-fantom/src/index.js‎

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @format
99
*/
1010

11+
import type {DisplayMetrics} from '../../../packages/react-native/Libraries/Utilities/Dimensions';
1112
import type {HostPlatform} from './Constants';
1213
import type {
1314
FantomRenderedOutput,
@@ -22,7 +23,7 @@ import NativeFantom, {
2223
import * as Benchmark from './Benchmark';
2324
import {getConstants} from './Constants';
2425
import getFantomRenderedOutput from './getFantomRenderedOutput';
25-
import {LogBox} from 'react-native';
26+
import {Dimensions, LogBox} from 'react-native';
2627
import {
2728
getInternalInstanceHandleFromPublicInstance,
2829
getNodeFromPublicInstance,
@@ -43,6 +44,7 @@ export type RootConfig = {
4344
viewportWidth?: number,
4445
viewportHeight?: number,
4546
devicePixelRatio?: number,
47+
fontScale?: number,
4648
viewportOffsetX?: number,
4749
viewportOffsetY?: number,
4850
};
@@ -63,6 +65,52 @@ export function getHostPlatform(): HostPlatform {
6365
const DEFAULT_VIEWPORT_WIDTH = 390;
6466
const DEFAULT_VIEWPORT_HEIGHT = 844;
6567
const DEFAULT_DEVICE_PIXEL_RATIO = 3;
68+
const DEFAULT_FONT_SCALE = 1;
69+
const activeRootDisplayMetrics = new Map<RootTag, DisplayMetrics>();
70+
71+
function setDefaultDisplayMetrics(): void {
72+
const displayMetrics = {
73+
width: DEFAULT_VIEWPORT_WIDTH,
74+
height: DEFAULT_VIEWPORT_HEIGHT,
75+
scale: DEFAULT_DEVICE_PIXEL_RATIO,
76+
fontScale: DEFAULT_FONT_SCALE,
77+
};
78+
Dimensions.set({window: displayMetrics, screen: displayMetrics});
79+
}
80+
81+
setDefaultDisplayMetrics();
82+
83+
function pushDimensions(
84+
surfaceId: RootTag,
85+
displayMetrics: DisplayMetrics,
86+
): void {
87+
activeRootDisplayMetrics.set(surfaceId, displayMetrics);
88+
Dimensions.set({window: displayMetrics, screen: displayMetrics});
89+
for (const metrics of activeRootDisplayMetrics.values()) {
90+
if (
91+
metrics.width !== displayMetrics.width ||
92+
metrics.height !== displayMetrics.height ||
93+
metrics.scale !== displayMetrics.scale ||
94+
metrics.fontScale !== displayMetrics.fontScale
95+
) {
96+
console.warn(
97+
'Fantom.createRoot() updated global Dimensions while another active root has different display metrics. Dimensions and PixelRatio now reflect the new root.',
98+
{existingRoot: metrics, newRoot: displayMetrics},
99+
);
100+
break;
101+
}
102+
}
103+
}
104+
105+
function popDimensions(surfaceId: RootTag): void {
106+
activeRootDisplayMetrics.delete(surfaceId);
107+
const displayMetrics = Array.from(activeRootDisplayMetrics.values()).pop();
108+
if (displayMetrics != null) {
109+
Dimensions.set({window: displayMetrics, screen: displayMetrics});
110+
} else {
111+
setDefaultDisplayMetrics();
112+
}
113+
}
66114

67115
class Root {
68116
#surfaceId: RootTag;
@@ -71,23 +119,33 @@ class Root {
71119
#viewportOffsetX: number;
72120
#viewportOffsetY: number;
73121
#devicePixelRatio: number;
122+
#fontScale: number;
74123
#document: ?Document;
75124

76125
constructor(config?: RootConfig) {
77126
this.#viewportWidth = config?.viewportWidth ?? DEFAULT_VIEWPORT_WIDTH;
78127
this.#viewportHeight = config?.viewportHeight ?? DEFAULT_VIEWPORT_HEIGHT;
79128
this.#devicePixelRatio =
80129
config?.devicePixelRatio ?? DEFAULT_DEVICE_PIXEL_RATIO;
130+
this.#fontScale = config?.fontScale ?? DEFAULT_FONT_SCALE;
81131
this.#viewportOffsetX = config?.viewportOffsetX ?? 0;
82132
this.#viewportOffsetY = config?.viewportOffsetY ?? 0;
83133

84134
this.#surfaceId = NativeFantom.startSurface(
85135
this.#viewportWidth,
86136
this.#viewportHeight,
87137
this.#devicePixelRatio,
138+
this.#fontScale,
88139
this.#viewportOffsetX,
89140
this.#viewportOffsetY,
90141
);
142+
143+
pushDimensions(this.#surfaceId, {
144+
width: this.#viewportWidth,
145+
height: this.#viewportHeight,
146+
scale: this.#devicePixelRatio,
147+
fontScale: this.#fontScale,
148+
});
91149
}
92150

93151
// $FlowExpectedError[unsafe-getters-setters]
@@ -128,6 +186,7 @@ class Root {
128186
destroy() {
129187
// TODO: check for leaks.
130188
NativeFantom.stopSurface(this.#surfaceId);
189+
popDimensions(this.#surfaceId);
131190
NativeFantom.flushMessageQueue();
132191
this.#document = null;
133192
}

‎private/react-native-fantom/tester/src/NativeFantom.cpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ SurfaceId NativeFantom::startSurface(
3636
double viewportWidth,
3737
double viewportHeight,
3838
double devicePixelRatio,
39+
double fontScale,
3940
double viewportOffsetX,
4041
double viewportOffsetY) {
4142
SurfaceId surfaceId = nextSurfaceId_;
@@ -46,6 +47,7 @@ SurfaceId NativeFantom::startSurface(
4647
static_cast<float>(viewportHeight),
4748
surfaceId,
4849
static_cast<float>(devicePixelRatio),
50+
static_cast<float>(fontScale),
4951
static_cast<float>(viewportOffsetX),
5052
static_cast<float>(viewportOffsetY));
5153
return surfaceId;

‎private/react-native-fantom/tester/src/NativeFantom.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class NativeFantom : public NativeFantomCxxSpec<NativeFantom> {
8282
double viewportWidth,
8383
double viewportHeight,
8484
double devicePixelRatio,
85+
double fontScale,
8586
double viewportOffsetX,
8687
double viewportOffsetY);
8788

‎private/react-native-fantom/tester/src/TesterAppDelegate.cpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ void TesterAppDelegate::startSurface(
270270
float heightDp,
271271
SurfaceId surfaceId,
272272
float pointScaleFactor,
273+
float fontScale,
273274
float offsetX,
274275
float offsetY) {
275276
Size extentsDp{static_cast<Float>(widthDp), static_cast<Float>(heightDp)};
@@ -281,6 +282,7 @@ void TesterAppDelegate::startSurface(
281282

282283
LayoutContext layoutContext{
283284
.pointScaleFactor = pointScaleFactor,
285+
.fontSizeMultiplier = fontScale,
284286
.viewportOffset = {.x = offsetX, .y = offsetY},
285287
};
286288

0 commit comments

Comments
 (0)