@react-native-gpui/screens 0.0.0-stage → 4.28.0-alpha.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.
@@ -0,0 +1,193 @@
1
+ //! The animation modes a stack accepts, and the poses each one corresponds to.
2
+ //!
3
+ //! Core owns the timing and the paint; this file is the module's own statement
4
+ //! of what "slide from the right" and "fade" mean as movement between two
5
+ //! poses, so core never learns a navigation name.
6
+ use react_native_gpui::extensions::{Pose, Transition};
7
+ use std::time::Duration;
8
+
9
+ /// A stack animation, as `stackAnimation` names it.
10
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
11
+ pub enum TransitionMode {
12
+ Fade,
13
+ SlideFromRight,
14
+ SlideFromLeft,
15
+ }
16
+
17
+ impl TransitionMode {
18
+ /// The mode the reverse of this one uses, so a pop plays the entry mirrored.
19
+ #[must_use]
20
+ pub fn reverse(self) -> Self {
21
+ match self {
22
+ Self::SlideFromRight => Self::SlideFromLeft,
23
+ Self::SlideFromLeft => Self::SlideFromRight,
24
+ Self::Fade => Self::Fade,
25
+ }
26
+ }
27
+
28
+ /// How long this mode runs.
29
+ pub fn duration(self) -> Duration {
30
+ match self {
31
+ Self::Fade => Duration::from_millis(150),
32
+ _ => Duration::from_millis(240),
33
+ }
34
+ }
35
+ }
36
+
37
+ /// The mode a `stackAnimation` string asks for. Unknown values fade, which is
38
+ /// what the platform's own default is.
39
+ pub fn transition_mode(animation: &str) -> Option<TransitionMode> {
40
+ match animation {
41
+ "none" => None,
42
+ "slide_from_right" | "simple_push" => Some(TransitionMode::SlideFromRight),
43
+ "slide_from_left" => Some(TransitionMode::SlideFromLeft),
44
+ "default" | "" | "fade" => Some(TransitionMode::Fade),
45
+ _ => Some(TransitionMode::Fade),
46
+ }
47
+ }
48
+
49
+ /// Whether entering or leaving.
50
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
51
+ pub enum Role {
52
+ Enter,
53
+ Exit,
54
+ }
55
+
56
+ /// The poses a mode and role correspond to.
57
+ ///
58
+ /// Entering slides from off-screen to rest; leaving slides from rest to
59
+ /// off-screen; a fade keeps the node still while its opacity moves. The offset
60
+ /// is a fraction of the node's own width, so `distance` is how far it travels.
61
+ pub fn poses(mode: TransitionMode, role: Role, distance: f32) -> (Pose, Pose) {
62
+ let rest = Pose {
63
+ offset_x: 0.0,
64
+ opacity: 1.0,
65
+ };
66
+ let transparent = Pose {
67
+ offset_x: 0.0,
68
+ opacity: 0.0,
69
+ };
70
+ let off_screen = |sign: f32| Pose {
71
+ offset_x: sign * distance,
72
+ opacity: 1.0,
73
+ };
74
+ match (mode, role) {
75
+ (TransitionMode::Fade, Role::Enter) => (transparent, rest),
76
+ (TransitionMode::Fade, Role::Exit) => (rest, transparent),
77
+ (TransitionMode::SlideFromRight, Role::Enter) => (off_screen(1.0), rest),
78
+ (TransitionMode::SlideFromLeft, Role::Enter) => (off_screen(-1.0), rest),
79
+ (TransitionMode::SlideFromRight, Role::Exit) => (rest, off_screen(-1.0)),
80
+ (TransitionMode::SlideFromLeft, Role::Exit) => (rest, off_screen(1.0)),
81
+ }
82
+ }
83
+
84
+ /// The transition a mode and role describe.
85
+ pub fn transition(mode: TransitionMode, role: Role, distance: f32) -> Transition {
86
+ let (from, to) = poses(mode, role, distance);
87
+ Transition {
88
+ from,
89
+ to,
90
+ duration: mode.duration(),
91
+ }
92
+ }
93
+
94
+ /// The card underneath a push parallaxes a short way rather than leaving.
95
+ pub const PARALLAX_DISTANCE: f32 = 0.12;
96
+
97
+ #[cfg(test)]
98
+ mod tests {
99
+ use super::*;
100
+
101
+ #[test]
102
+ fn maps_supported_animations_and_falls_back_to_fade() {
103
+ assert_eq!(transition_mode("none"), None);
104
+ assert_eq!(transition_mode("fade"), Some(TransitionMode::Fade));
105
+ assert_eq!(transition_mode("default"), Some(TransitionMode::Fade));
106
+ assert_eq!(transition_mode(""), Some(TransitionMode::Fade));
107
+ assert_eq!(
108
+ transition_mode("slide_from_right"),
109
+ Some(TransitionMode::SlideFromRight)
110
+ );
111
+ assert_eq!(
112
+ transition_mode("simple_push"),
113
+ Some(TransitionMode::SlideFromRight)
114
+ );
115
+ assert_eq!(
116
+ transition_mode("slide_from_left"),
117
+ Some(TransitionMode::SlideFromLeft)
118
+ );
119
+ assert_eq!(transition_mode("unsupported"), Some(TransitionMode::Fade));
120
+ }
121
+
122
+ #[test]
123
+ fn reversing_a_slide_swaps_its_direction() {
124
+ assert_eq!(
125
+ TransitionMode::SlideFromRight.reverse(),
126
+ TransitionMode::SlideFromLeft
127
+ );
128
+ assert_eq!(
129
+ TransitionMode::SlideFromLeft.reverse(),
130
+ TransitionMode::SlideFromRight
131
+ );
132
+ assert_eq!(TransitionMode::Fade.reverse(), TransitionMode::Fade);
133
+ }
134
+
135
+ #[test]
136
+ fn the_poses_reproduce_the_original_slide_and_fade_curves() {
137
+ for (mode, role, distance, expected) in [
138
+ (
139
+ TransitionMode::SlideFromRight,
140
+ Role::Enter,
141
+ 1.0,
142
+ [1.0, 0.5, 0.0],
143
+ ),
144
+ (
145
+ TransitionMode::SlideFromLeft,
146
+ Role::Enter,
147
+ 1.0,
148
+ [-1.0, -0.5, 0.0],
149
+ ),
150
+ (
151
+ TransitionMode::SlideFromRight,
152
+ Role::Exit,
153
+ 1.0,
154
+ [0.0, -0.5, -1.0],
155
+ ),
156
+ (
157
+ TransitionMode::SlideFromLeft,
158
+ Role::Exit,
159
+ 1.0,
160
+ [0.0, 0.5, 1.0],
161
+ ),
162
+ (
163
+ TransitionMode::SlideFromRight,
164
+ Role::Exit,
165
+ PARALLAX_DISTANCE,
166
+ [0.0, -0.06, -0.12],
167
+ ),
168
+ ] {
169
+ let transition = transition(mode, role, distance);
170
+ for (progress, expected_offset) in
171
+ [(0.0, expected[0]), (0.5, expected[1]), (1.0, expected[2])]
172
+ {
173
+ let pose = transition.pose_at(progress);
174
+ assert!(
175
+ (pose.offset_x - expected_offset).abs() < 0.001,
176
+ "{mode:?} {role:?} at {progress}: {} != {expected_offset}",
177
+ pose.offset_x
178
+ );
179
+ }
180
+ }
181
+ }
182
+
183
+ #[test]
184
+ fn a_fade_moves_opacity_and_nothing_else() {
185
+ let transition = transition(TransitionMode::Fade, Role::Enter, 1.0);
186
+ for (progress, expected) in [(0.0, 0.0), (0.5, 0.5), (1.0, 1.0)] {
187
+ let pose = transition.pose_at(progress);
188
+ assert!((pose.opacity - expected).abs() < 0.001);
189
+ assert!(pose.offset_x.abs() < 0.001);
190
+ }
191
+ assert_eq!(transition.duration, Duration::from_millis(150));
192
+ }
193
+ }
@@ -0,0 +1,39 @@
1
+ //! Components react-native-screens ships that this platform does not implement.
2
+ //!
3
+ //! They are named here so the module can say so once, instead of core carrying a
4
+ //! list of a library's components.
5
+ /// Whether this component is one the platform cannot render.
6
+ pub fn unsupported_component(name: &str) -> bool {
7
+ matches!(
8
+ name,
9
+ "RNSSearchBar"
10
+ | "RNSBottomTabs"
11
+ | "RNSBottomTabsScreen"
12
+ | "RNSScreenFooter"
13
+ | "RNSSplitView"
14
+ | "RNSSplitViewScreen"
15
+ ) || name.starts_with("RNSSplitView")
16
+ }
17
+
18
+ #[cfg(test)]
19
+ mod tests {
20
+ use super::*;
21
+
22
+ #[test]
23
+ fn identifies_the_components_the_platform_cannot_render() {
24
+ for name in [
25
+ "RNSSearchBar",
26
+ "RNSBottomTabs",
27
+ "RNSBottomTabsScreen",
28
+ "RNSScreenFooter",
29
+ "RNSSplitView",
30
+ "RNSSplitViewScreen",
31
+ ] {
32
+ assert!(unsupported_component(name), "{name} should be unsupported");
33
+ }
34
+ assert!(unsupported_component("RNSSplitViewSomethingElse"));
35
+ for name in ["RNSScreen", "RNSScreenStack", "View", "RNSScreenContainer"] {
36
+ assert!(!unsupported_component(name), "{name} should be supported");
37
+ }
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@react-native-gpui/screens",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "4.28.0-alpha.0",
4
+ "description": "GPUI platform integration for react-native-screens",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/streetwriters/react-native-gpui.git",
9
+ "directory": "modules/screens"
10
+ },
11
+ "homepage": "https://streetwriters.github.io/react-native-gpui/",
12
+ "bugs": {
13
+ "url": "https://github.com/streetwriters/react-native-gpui/issues"
14
+ },
15
+ "files": [
16
+ "desktop",
17
+ "react-native.config.js",
18
+ "src",
19
+ "CHANGELOG.md",
20
+ "!**/*.test.js",
21
+ "!**/*.test.mjs",
22
+ "!**/*.test.ts",
23
+ "!**/*.test.cjs"
24
+ ],
25
+ "peerDependencies": {
26
+ "react-native": "0.87.x",
27
+ "react-native-screens": "4.28.x"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "tag": "latest"
32
+ }
33
+ }
@@ -0,0 +1,36 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ desktop: {
5
+ cmake: 'desktop',
6
+ crate: 'rn-gpui-screens',
7
+ componentCodegenPackage: 'react-native-screens',
8
+ componentDescriptors: [
9
+ 'RNSScreenComponentDescriptor',
10
+ 'RNSScreenContainerComponentDescriptor',
11
+ 'RNSScreenContentWrapperComponentDescriptor',
12
+ 'RNSScreenNavigationContainerComponentDescriptor',
13
+ 'RNSScreenStackComponentDescriptor',
14
+ 'RNSScreenStackHeaderConfigComponentDescriptor',
15
+ 'RNSScreenStackHeaderSubviewComponentDescriptor',
16
+ 'RNSModalScreenComponentDescriptor',
17
+ 'RNSFullWindowOverlayComponentDescriptor',
18
+ ],
19
+ componentHeaders: [
20
+ 'react/renderer/components/rnscreens/ComponentDescriptors.h',
21
+ 'react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h',
22
+ 'react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h',
23
+ 'react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewComponentDescriptor.h',
24
+ 'react/renderer/components/rnscreens/RNSModalScreenComponentDescriptor.h',
25
+ 'react/renderer/components/rnscreens/RNSFullWindowOverlayComponentDescriptor.h',
26
+ ],
27
+ // Metro: replace these files of installed packages for gpui.
28
+ sourceOverrides: {
29
+ 'react-native-screens/src/core.ts': './src/core.desktop.ts',
30
+ 'react-native-screens/src/components/FullWindowOverlay.tsx':
31
+ './src/FullWindowOverlay.desktop.tsx',
32
+ },
33
+ },
34
+ },
35
+ },
36
+ };
@@ -0,0 +1,19 @@
1
+ import React, { type PropsWithChildren } from 'react';
2
+ import { StyleSheet, useWindowDimensions } from 'react-native';
3
+ import FullWindowOverlayNativeComponent from 'react-native-screens/src/fabric/FullWindowOverlayNativeComponent';
4
+ import type { NativeProps } from 'react-native-screens/src/fabric/FullWindowOverlayNativeComponent';
5
+
6
+ export default function FullWindowOverlay({
7
+ children,
8
+ ...props
9
+ }: PropsWithChildren<NativeProps>) {
10
+ const { width, height } = useWindowDimensions();
11
+ return (
12
+ <FullWindowOverlayNativeComponent
13
+ {...props}
14
+ style={[StyleSheet.absoluteFill, { width, height }, props.style]}
15
+ >
16
+ {children}
17
+ </FullWindowOverlayNativeComponent>
18
+ );
19
+ }
@@ -0,0 +1,28 @@
1
+ import './customDirectEventTypes.desktop';
2
+
3
+ // GPUI always ships the native screens implementation.
4
+ export const isNativePlatformSupported = true;
5
+
6
+ let ENABLE_SCREENS = isNativePlatformSupported;
7
+
8
+ export function enableScreens(shouldEnableScreens = true) {
9
+ ENABLE_SCREENS = shouldEnableScreens;
10
+ }
11
+
12
+ let ENABLE_FREEZE = false;
13
+
14
+ export function enableFreeze(shouldEnableReactFreeze = true) {
15
+ if (!isNativePlatformSupported) {
16
+ return;
17
+ }
18
+
19
+ ENABLE_FREEZE = shouldEnableReactFreeze;
20
+ }
21
+
22
+ export function screensEnabled() {
23
+ return ENABLE_SCREENS;
24
+ }
25
+
26
+ export function freezeEnabled() {
27
+ return ENABLE_FREEZE;
28
+ }
@@ -0,0 +1,16 @@
1
+ // The native stack header dispatches this upstream screen event to notify
2
+ // React Navigation that the user requested a native back action.
3
+ import { customDirectEventTypes } from 'react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry';
4
+
5
+ customDirectEventTypes.topHeaderBackButtonClicked = {
6
+ registrationName: 'onHeaderBackButtonClicked',
7
+ };
8
+
9
+ customDirectEventTypes.topWillAppear = { registrationName: 'onWillAppear' };
10
+ customDirectEventTypes.topWillDisappear = {
11
+ registrationName: 'onWillDisappear',
12
+ };
13
+ customDirectEventTypes.topAppear = { registrationName: 'onAppear' };
14
+ customDirectEventTypes.topDisappear = { registrationName: 'onDisappear' };
15
+
16
+ export { customDirectEventTypes };