@react-native-motion-kit/text-motion 0.0.0-stage → 0.1.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.
- package/CHANGELOG.md +14 -0
- package/LICENSE +20 -0
- package/README.ko.md +104 -0
- package/README.md +103 -2
- package/lib/module/accessibility/index.js +4 -0
- package/lib/module/accessibility/index.js.map +1 -0
- package/lib/module/accessibility/policies.js +39 -0
- package/lib/module/accessibility/policies.js.map +1 -0
- package/lib/module/controls/brand.js +4 -0
- package/lib/module/controls/brand.js.map +1 -0
- package/lib/module/controls/controls.js +28 -0
- package/lib/module/controls/controls.js.map +1 -0
- package/lib/module/controls/descriptors.js +55 -0
- package/lib/module/controls/descriptors.js.map +1 -0
- package/lib/module/controls/index.js +4 -0
- package/lib/module/controls/index.js.map +1 -0
- package/lib/module/effects/compose.js +48 -0
- package/lib/module/effects/compose.js.map +1 -0
- package/lib/module/effects/fade.js +15 -0
- package/lib/module/effects/fade.js.map +1 -0
- package/lib/module/effects/index.js +12 -0
- package/lib/module/effects/index.js.map +1 -0
- package/lib/module/effects/lineReveal.js +17 -0
- package/lib/module/effects/lineReveal.js.map +1 -0
- package/lib/module/effects/pulse.js +14 -0
- package/lib/module/effects/pulse.js.map +1 -0
- package/lib/module/effects/rise.js +14 -0
- package/lib/module/effects/rise.js.map +1 -0
- package/lib/module/effects/scale.js +15 -0
- package/lib/module/effects/scale.js.map +1 -0
- package/lib/module/effects/shake.js +14 -0
- package/lib/module/effects/shake.js.map +1 -0
- package/lib/module/effects/slide.js +15 -0
- package/lib/module/effects/slide.js.map +1 -0
- package/lib/module/effects/typewriter.js +19 -0
- package/lib/module/effects/typewriter.js.map +1 -0
- package/lib/module/effects/validation.js +10 -0
- package/lib/module/effects/validation.js.map +1 -0
- package/lib/module/index.js +10 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/presets/index.js +35 -0
- package/lib/module/presets/index.js.map +1 -0
- package/lib/module/recipe/component.js +60 -0
- package/lib/module/recipe/component.js.map +1 -0
- package/lib/module/recipe/defineTextMotion.js +9 -0
- package/lib/module/recipe/defineTextMotion.js.map +1 -0
- package/lib/module/recipe/descriptors.js +60 -0
- package/lib/module/recipe/descriptors.js.map +1 -0
- package/lib/module/recipe/index.js +5 -0
- package/lib/module/recipe/index.js.map +1 -0
- package/lib/module/recipe/recipe.js +163 -0
- package/lib/module/recipe/recipe.js.map +1 -0
- package/lib/module/renderers/index.js +5 -0
- package/lib/module/renderers/index.js.map +1 -0
- package/lib/module/renderers/nativeText.js +494 -0
- package/lib/module/renderers/nativeText.js.map +1 -0
- package/lib/module/renderers/nativeTextControls.js +96 -0
- package/lib/module/renderers/nativeTextControls.js.map +1 -0
- package/lib/module/renderers/nativeTextLineLayout.js +290 -0
- package/lib/module/renderers/nativeTextLineLayout.js.map +1 -0
- package/lib/module/renderers/nativeTextPlayback.js +91 -0
- package/lib/module/renderers/nativeTextPlayback.js.map +1 -0
- package/lib/module/renderers/overlayText.js +543 -0
- package/lib/module/renderers/overlayText.js.map +1 -0
- package/lib/module/renderers/overlayTextLayout.js +182 -0
- package/lib/module/renderers/overlayTextLayout.js.map +1 -0
- package/lib/module/renderers/rendererMotion.js +264 -0
- package/lib/module/renderers/rendererMotion.js.map +1 -0
- package/lib/module/split/createToken.js +19 -0
- package/lib/module/split/createToken.js.map +1 -0
- package/lib/module/split/custom.js +57 -0
- package/lib/module/split/custom.js.map +1 -0
- package/lib/module/split/graphemes.js +25 -0
- package/lib/module/split/graphemes.js.map +1 -0
- package/lib/module/split/index.js +7 -0
- package/lib/module/split/index.js.map +1 -0
- package/lib/module/split/lines.js +43 -0
- package/lib/module/split/lines.js.map +1 -0
- package/lib/module/split/segmenter.js +74 -0
- package/lib/module/split/segmenter.js.map +1 -0
- package/lib/module/split/words.js +28 -0
- package/lib/module/split/words.js.map +1 -0
- package/lib/module/timeline/index.js +7 -0
- package/lib/module/timeline/index.js.map +1 -0
- package/lib/module/timeline/parallel.js +21 -0
- package/lib/module/timeline/parallel.js.map +1 -0
- package/lib/module/timeline/sequence.js +18 -0
- package/lib/module/timeline/sequence.js.map +1 -0
- package/lib/module/timeline/stagger.js +43 -0
- package/lib/module/timeline/stagger.js.map +1 -0
- package/lib/module/timeline/validation.js +24 -0
- package/lib/module/timeline/validation.js.map +1 -0
- package/lib/module/timeline/wave.js +25 -0
- package/lib/module/timeline/wave.js.map +1 -0
- package/lib/module/types/index.js +4 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/types/recipe.js +4 -0
- package/lib/module/types/recipe.js.map +1 -0
- package/lib/module/types/renderer.js +27 -0
- package/lib/module/types/renderer.js.map +1 -0
- package/lib/module/types/token.js +2 -0
- package/lib/module/types/token.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/accessibility/index.d.ts +2 -0
- package/lib/typescript/src/accessibility/index.d.ts.map +1 -0
- package/lib/typescript/src/accessibility/policies.d.ts +13 -0
- package/lib/typescript/src/accessibility/policies.d.ts.map +1 -0
- package/lib/typescript/src/controls/brand.d.ts +2 -0
- package/lib/typescript/src/controls/brand.d.ts.map +1 -0
- package/lib/typescript/src/controls/controls.d.ts +28 -0
- package/lib/typescript/src/controls/controls.d.ts.map +1 -0
- package/lib/typescript/src/controls/descriptors.d.ts +14 -0
- package/lib/typescript/src/controls/descriptors.d.ts.map +1 -0
- package/lib/typescript/src/controls/index.d.ts +3 -0
- package/lib/typescript/src/controls/index.d.ts.map +1 -0
- package/lib/typescript/src/effects/compose.d.ts +11 -0
- package/lib/typescript/src/effects/compose.d.ts.map +1 -0
- package/lib/typescript/src/effects/fade.d.ts +10 -0
- package/lib/typescript/src/effects/fade.d.ts.map +1 -0
- package/lib/typescript/src/effects/index.d.ts +10 -0
- package/lib/typescript/src/effects/index.d.ts.map +1 -0
- package/lib/typescript/src/effects/lineReveal.d.ts +11 -0
- package/lib/typescript/src/effects/lineReveal.d.ts.map +1 -0
- package/lib/typescript/src/effects/pulse.d.ts +8 -0
- package/lib/typescript/src/effects/pulse.d.ts.map +1 -0
- package/lib/typescript/src/effects/rise.d.ts +8 -0
- package/lib/typescript/src/effects/rise.d.ts.map +1 -0
- package/lib/typescript/src/effects/scale.d.ts +10 -0
- package/lib/typescript/src/effects/scale.d.ts.map +1 -0
- package/lib/typescript/src/effects/shake.d.ts +8 -0
- package/lib/typescript/src/effects/shake.d.ts.map +1 -0
- package/lib/typescript/src/effects/slide.d.ts +10 -0
- package/lib/typescript/src/effects/slide.d.ts.map +1 -0
- package/lib/typescript/src/effects/typewriter.d.ts +13 -0
- package/lib/typescript/src/effects/typewriter.d.ts.map +1 -0
- package/lib/typescript/src/effects/validation.d.ts +2 -0
- package/lib/typescript/src/effects/validation.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +15 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/presets/index.d.ts +10 -0
- package/lib/typescript/src/presets/index.d.ts.map +1 -0
- package/lib/typescript/src/recipe/component.d.ts +3 -0
- package/lib/typescript/src/recipe/component.d.ts.map +1 -0
- package/lib/typescript/src/recipe/defineTextMotion.d.ts +4 -0
- package/lib/typescript/src/recipe/defineTextMotion.d.ts.map +1 -0
- package/lib/typescript/src/recipe/descriptors.d.ts +15 -0
- package/lib/typescript/src/recipe/descriptors.d.ts.map +1 -0
- package/lib/typescript/src/recipe/index.d.ts +4 -0
- package/lib/typescript/src/recipe/index.d.ts.map +1 -0
- package/lib/typescript/src/recipe/recipe.d.ts +67 -0
- package/lib/typescript/src/recipe/recipe.d.ts.map +1 -0
- package/lib/typescript/src/renderers/index.d.ts +3 -0
- package/lib/typescript/src/renderers/index.d.ts.map +1 -0
- package/lib/typescript/src/renderers/nativeText.d.ts +13 -0
- package/lib/typescript/src/renderers/nativeText.d.ts.map +1 -0
- package/lib/typescript/src/renderers/nativeTextControls.d.ts +16 -0
- package/lib/typescript/src/renderers/nativeTextControls.d.ts.map +1 -0
- package/lib/typescript/src/renderers/nativeTextLineLayout.d.ts +84 -0
- package/lib/typescript/src/renderers/nativeTextLineLayout.d.ts.map +1 -0
- package/lib/typescript/src/renderers/nativeTextPlayback.d.ts +21 -0
- package/lib/typescript/src/renderers/nativeTextPlayback.d.ts.map +1 -0
- package/lib/typescript/src/renderers/overlayText.d.ts +13 -0
- package/lib/typescript/src/renderers/overlayText.d.ts.map +1 -0
- package/lib/typescript/src/renderers/overlayTextLayout.d.ts +36 -0
- package/lib/typescript/src/renderers/overlayTextLayout.d.ts.map +1 -0
- package/lib/typescript/src/renderers/rendererMotion.d.ts +53 -0
- package/lib/typescript/src/renderers/rendererMotion.d.ts.map +1 -0
- package/lib/typescript/src/split/createToken.d.ts +11 -0
- package/lib/typescript/src/split/createToken.d.ts.map +1 -0
- package/lib/typescript/src/split/custom.d.ts +17 -0
- package/lib/typescript/src/split/custom.d.ts.map +1 -0
- package/lib/typescript/src/split/graphemes.d.ts +4 -0
- package/lib/typescript/src/split/graphemes.d.ts.map +1 -0
- package/lib/typescript/src/split/index.d.ts +5 -0
- package/lib/typescript/src/split/index.d.ts.map +1 -0
- package/lib/typescript/src/split/lines.d.ts +4 -0
- package/lib/typescript/src/split/lines.d.ts.map +1 -0
- package/lib/typescript/src/split/segmenter.d.ts +18 -0
- package/lib/typescript/src/split/segmenter.d.ts.map +1 -0
- package/lib/typescript/src/split/words.d.ts +4 -0
- package/lib/typescript/src/split/words.d.ts.map +1 -0
- package/lib/typescript/src/timeline/index.d.ts +5 -0
- package/lib/typescript/src/timeline/index.d.ts.map +1 -0
- package/lib/typescript/src/timeline/parallel.d.ts +4 -0
- package/lib/typescript/src/timeline/parallel.d.ts.map +1 -0
- package/lib/typescript/src/timeline/sequence.d.ts +4 -0
- package/lib/typescript/src/timeline/sequence.d.ts.map +1 -0
- package/lib/typescript/src/timeline/stagger.d.ts +11 -0
- package/lib/typescript/src/timeline/stagger.d.ts.map +1 -0
- package/lib/typescript/src/timeline/validation.d.ts +4 -0
- package/lib/typescript/src/timeline/validation.d.ts.map +1 -0
- package/lib/typescript/src/timeline/wave.d.ts +11 -0
- package/lib/typescript/src/timeline/wave.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +6 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -0
- package/lib/typescript/src/types/recipe.d.ts +123 -0
- package/lib/typescript/src/types/recipe.d.ts.map +1 -0
- package/lib/typescript/src/types/renderer.d.ts +51 -0
- package/lib/typescript/src/types/renderer.d.ts.map +1 -0
- package/lib/typescript/src/types/token.d.ts +35 -0
- package/lib/typescript/src/types/token.d.ts.map +1 -0
- package/package.json +117 -3
- package/src/accessibility/index.ts +7 -0
- package/src/accessibility/policies.ts +60 -0
- package/src/controls/brand.ts +3 -0
- package/src/controls/controls.ts +41 -0
- package/src/controls/descriptors.ts +83 -0
- package/src/controls/index.ts +2 -0
- package/src/effects/compose.ts +98 -0
- package/src/effects/fade.ts +22 -0
- package/src/effects/index.ts +9 -0
- package/src/effects/lineReveal.ts +28 -0
- package/src/effects/pulse.ts +19 -0
- package/src/effects/rise.ts +19 -0
- package/src/effects/scale.ts +22 -0
- package/src/effects/shake.ts +19 -0
- package/src/effects/slide.ts +22 -0
- package/src/effects/typewriter.ts +16 -0
- package/src/effects/validation.ts +9 -0
- package/src/index.tsx +46 -0
- package/src/presets/index.ts +42 -0
- package/src/recipe/component.tsx +88 -0
- package/src/recipe/defineTextMotion.ts +6 -0
- package/src/recipe/descriptors.ts +158 -0
- package/src/recipe/index.ts +3 -0
- package/src/recipe/recipe.ts +359 -0
- package/src/renderers/index.ts +2 -0
- package/src/renderers/nativeText.tsx +813 -0
- package/src/renderers/nativeTextControls.ts +131 -0
- package/src/renderers/nativeTextLineLayout.ts +495 -0
- package/src/renderers/nativeTextPlayback.ts +204 -0
- package/src/renderers/overlayText.tsx +945 -0
- package/src/renderers/overlayTextLayout.ts +312 -0
- package/src/renderers/rendererMotion.ts +483 -0
- package/src/split/createToken.ts +26 -0
- package/src/split/custom.ts +92 -0
- package/src/split/graphemes.ts +27 -0
- package/src/split/index.ts +9 -0
- package/src/split/lines.ts +35 -0
- package/src/split/segmenter.ts +111 -0
- package/src/split/words.ts +28 -0
- package/src/timeline/index.ts +4 -0
- package/src/timeline/parallel.ts +30 -0
- package/src/timeline/sequence.ts +29 -0
- package/src/timeline/stagger.ts +60 -0
- package/src/timeline/validation.ts +29 -0
- package/src/timeline/wave.ts +34 -0
- package/src/types/index.ts +34 -0
- package/src/types/recipe.ts +195 -0
- package/src/types/renderer.ts +91 -0
- package/src/types/token.ts +39 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import { cancelAnimation, type SharedValue } from 'react-native-reanimated';
|
|
3
|
+
|
|
4
|
+
import type { TextMotionControls } from '../controls';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
readTextMotionControlsDescriptor,
|
|
8
|
+
type TextMotionControlCommand,
|
|
9
|
+
type TextMotionControlCommandKind,
|
|
10
|
+
} from '../controls/descriptors';
|
|
11
|
+
import { clampTextMotionProgress, type TextMotionItemMotion } from './rendererMotion';
|
|
12
|
+
|
|
13
|
+
type NativeTextProgressAnimationFactory = (tokenMotion: TextMotionItemMotion) => number;
|
|
14
|
+
|
|
15
|
+
type NativeTextOwnedPlaybackCommandOptions = {
|
|
16
|
+
createProgressAnimation: NativeTextProgressAnimationFactory;
|
|
17
|
+
progress: SharedValue<number>;
|
|
18
|
+
renderFinalState: boolean;
|
|
19
|
+
tokenMotion: TextMotionItemMotion;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type NativeTextPlaybackCommandHandler = (options: NativeTextOwnedPlaybackCommandOptions) => void;
|
|
23
|
+
|
|
24
|
+
type NativeTextControlsPlaybackOptions = NativeTextOwnedPlaybackCommandOptions & {
|
|
25
|
+
controls?: TextMotionControls;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type NativeTextControlsPlaybackState = NativeTextOwnedPlaybackCommandOptions;
|
|
29
|
+
|
|
30
|
+
function resetOwnedProgress(progress: SharedValue<number>, renderFinalState: boolean) {
|
|
31
|
+
cancelAnimation(progress);
|
|
32
|
+
progress.value = renderFinalState ? 1 : 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stopOwnedProgress(progress: SharedValue<number>, renderFinalState: boolean) {
|
|
36
|
+
cancelAnimation(progress);
|
|
37
|
+
|
|
38
|
+
if (renderFinalState) {
|
|
39
|
+
progress.value = 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function replayOwnedProgress({
|
|
44
|
+
createProgressAnimation,
|
|
45
|
+
progress,
|
|
46
|
+
renderFinalState,
|
|
47
|
+
tokenMotion,
|
|
48
|
+
}: NativeTextOwnedPlaybackCommandOptions) {
|
|
49
|
+
resetOwnedProgress(progress, renderFinalState);
|
|
50
|
+
|
|
51
|
+
if (renderFinalState) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
progress.value = createProgressAnimation(tokenMotion);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function playOwnedProgress({
|
|
59
|
+
createProgressAnimation,
|
|
60
|
+
progress,
|
|
61
|
+
renderFinalState,
|
|
62
|
+
tokenMotion,
|
|
63
|
+
}: NativeTextOwnedPlaybackCommandOptions) {
|
|
64
|
+
stopOwnedProgress(progress, renderFinalState);
|
|
65
|
+
|
|
66
|
+
if (renderFinalState) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const currentProgress = clampTextMotionProgress(progress.value);
|
|
71
|
+
const delayMs = currentProgress <= 0 ? tokenMotion.delayMs : 0;
|
|
72
|
+
|
|
73
|
+
progress.value = createProgressAnimation({
|
|
74
|
+
...tokenMotion,
|
|
75
|
+
delayMs,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const playbackCommandHandlers = {
|
|
80
|
+
play: playOwnedProgress,
|
|
81
|
+
replay: replayOwnedProgress,
|
|
82
|
+
reset({ progress, renderFinalState }) {
|
|
83
|
+
resetOwnedProgress(progress, renderFinalState);
|
|
84
|
+
},
|
|
85
|
+
stop({ progress, renderFinalState }) {
|
|
86
|
+
stopOwnedProgress(progress, renderFinalState);
|
|
87
|
+
},
|
|
88
|
+
} as const satisfies Record<TextMotionControlCommandKind, NativeTextPlaybackCommandHandler>;
|
|
89
|
+
|
|
90
|
+
function applyNativeTextOwnedPlaybackCommand(
|
|
91
|
+
command: TextMotionControlCommand,
|
|
92
|
+
options: NativeTextOwnedPlaybackCommandOptions,
|
|
93
|
+
) {
|
|
94
|
+
playbackCommandHandlers[command.kind](options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function useNativeTextControlsPlayback({
|
|
98
|
+
controls,
|
|
99
|
+
createProgressAnimation,
|
|
100
|
+
progress,
|
|
101
|
+
renderFinalState,
|
|
102
|
+
tokenMotion,
|
|
103
|
+
}: NativeTextControlsPlaybackOptions) {
|
|
104
|
+
const playbackState = useRef<NativeTextControlsPlaybackState>({
|
|
105
|
+
createProgressAnimation,
|
|
106
|
+
progress,
|
|
107
|
+
renderFinalState,
|
|
108
|
+
tokenMotion,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
useLayoutEffect(() => {
|
|
112
|
+
playbackState.current = {
|
|
113
|
+
createProgressAnimation,
|
|
114
|
+
progress,
|
|
115
|
+
renderFinalState,
|
|
116
|
+
tokenMotion,
|
|
117
|
+
};
|
|
118
|
+
}, [createProgressAnimation, progress, renderFinalState, tokenMotion]);
|
|
119
|
+
|
|
120
|
+
useLayoutEffect(() => {
|
|
121
|
+
if (!controls) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const descriptor = readTextMotionControlsDescriptor(controls);
|
|
126
|
+
|
|
127
|
+
return descriptor.subscribe((command) => {
|
|
128
|
+
applyNativeTextOwnedPlaybackCommand(command, playbackState.current);
|
|
129
|
+
});
|
|
130
|
+
}, [controls]);
|
|
131
|
+
}
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { StyleSheet, type StyleProp, type TextStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import type { TextMotionSourceRange, TextMotionToken } from '../types';
|
|
4
|
+
|
|
5
|
+
const LAYOUT_TEXT_STYLE_KEYS = [
|
|
6
|
+
'fontFamily',
|
|
7
|
+
'fontSize',
|
|
8
|
+
'fontStyle',
|
|
9
|
+
'fontVariant',
|
|
10
|
+
'fontWeight',
|
|
11
|
+
'includeFontPadding',
|
|
12
|
+
'letterSpacing',
|
|
13
|
+
'lineHeight',
|
|
14
|
+
'textAlign',
|
|
15
|
+
'textTransform',
|
|
16
|
+
'writingDirection',
|
|
17
|
+
] as const satisfies readonly (keyof TextStyle)[];
|
|
18
|
+
|
|
19
|
+
const NATIVE_TEXT_LINE_LAYOUT_INVALIDATION_REASONS = [
|
|
20
|
+
'font-scale',
|
|
21
|
+
'initial',
|
|
22
|
+
'line-text',
|
|
23
|
+
'style',
|
|
24
|
+
'text',
|
|
25
|
+
'width',
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export type NativeTextLineLayoutFailureReason =
|
|
29
|
+
| 'empty-line-text-unsupported'
|
|
30
|
+
| 'line-group-text-mismatch'
|
|
31
|
+
| 'line-text-not-found'
|
|
32
|
+
| 'token-crosses-line'
|
|
33
|
+
| 'token-outside-lines';
|
|
34
|
+
|
|
35
|
+
export type NativeTextLineLayoutFailure = {
|
|
36
|
+
lineIndex?: number;
|
|
37
|
+
lineRange?: TextMotionSourceRange;
|
|
38
|
+
message: string;
|
|
39
|
+
reason: NativeTextLineLayoutFailureReason;
|
|
40
|
+
sourceRange?: TextMotionSourceRange;
|
|
41
|
+
tokenId?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type NativeTextLineLayoutResult<Value> =
|
|
45
|
+
| {
|
|
46
|
+
ok: true;
|
|
47
|
+
value: Value;
|
|
48
|
+
}
|
|
49
|
+
| ({
|
|
50
|
+
ok: false;
|
|
51
|
+
} & NativeTextLineLayoutFailure);
|
|
52
|
+
|
|
53
|
+
export type NativeTextRenderedLine = {
|
|
54
|
+
height: number;
|
|
55
|
+
index: number;
|
|
56
|
+
text: string;
|
|
57
|
+
width: number;
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type NativeTextLineRange = {
|
|
63
|
+
index: number;
|
|
64
|
+
sourceRange: TextMotionSourceRange;
|
|
65
|
+
text: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type NativeTextLineTokenGroup = {
|
|
69
|
+
line: NativeTextLineRange;
|
|
70
|
+
text: string;
|
|
71
|
+
tokens: readonly TextMotionToken[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type NativeTextLineLayoutCompatibilityReport =
|
|
75
|
+
| {
|
|
76
|
+
lineCount: number;
|
|
77
|
+
status: 'go';
|
|
78
|
+
tokenCount: number;
|
|
79
|
+
}
|
|
80
|
+
| {
|
|
81
|
+
actualText: string;
|
|
82
|
+
expectedText: string;
|
|
83
|
+
lineIndex: number;
|
|
84
|
+
reason: 'line-group-text-mismatch';
|
|
85
|
+
status: 'no-go';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type NativeTextLineLayoutInvalidationReason =
|
|
89
|
+
(typeof NATIVE_TEXT_LINE_LAYOUT_INVALIDATION_REASONS)[number];
|
|
90
|
+
|
|
91
|
+
export type NativeTextLineLayoutProbeSnapshot = {
|
|
92
|
+
acceptedMappingUpdates: number;
|
|
93
|
+
invalidationsByReason: Record<NativeTextLineLayoutInvalidationReason, number>;
|
|
94
|
+
lineCount: number;
|
|
95
|
+
mappingComputations: number;
|
|
96
|
+
onTextLayoutCalls: number;
|
|
97
|
+
rejectedIdenticalPayloads: number;
|
|
98
|
+
signature?: string;
|
|
99
|
+
tokenCount: number;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type NativeTextLineLayoutMeasurement = {
|
|
103
|
+
invalidationReason: NativeTextLineLayoutInvalidationReason;
|
|
104
|
+
lineCount: number;
|
|
105
|
+
signature: string;
|
|
106
|
+
tokenCount: number;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type NativeTextLineLayoutProbeUpdate = {
|
|
110
|
+
accepted: boolean;
|
|
111
|
+
snapshot: NativeTextLineLayoutProbeSnapshot;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type NativeTextLineLayoutSignatureInput = {
|
|
115
|
+
allowFontScaling?: boolean;
|
|
116
|
+
maxFontSizeMultiplier?: number | null;
|
|
117
|
+
measuredWidth: number;
|
|
118
|
+
renderedLines: readonly Pick<NativeTextRenderedLine, 'height' | 'text' | 'width' | 'x' | 'y'>[];
|
|
119
|
+
style?: StyleProp<TextStyle>;
|
|
120
|
+
text: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
type NativeTextLineLayoutStyleSnapshot = Partial<
|
|
124
|
+
Record<(typeof LAYOUT_TEXT_STYLE_KEYS)[number], unknown>
|
|
125
|
+
>;
|
|
126
|
+
|
|
127
|
+
type NativeTextLineRangeAccumulator =
|
|
128
|
+
| { cursor: number; lines: NativeTextLineRange[]; ok: true }
|
|
129
|
+
| ({ ok: false } & NativeTextLineLayoutFailure);
|
|
130
|
+
|
|
131
|
+
function createNativeTextLineLayoutFailure(
|
|
132
|
+
reason: NativeTextLineLayoutFailureReason,
|
|
133
|
+
message: string,
|
|
134
|
+
details: Omit<NativeTextLineLayoutFailure, 'message' | 'reason'> = {},
|
|
135
|
+
): NativeTextLineLayoutResult<never> {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
reason,
|
|
139
|
+
message,
|
|
140
|
+
...details,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createNativeTextLineRange(
|
|
145
|
+
text: string,
|
|
146
|
+
lineText: string,
|
|
147
|
+
lineIndex: number,
|
|
148
|
+
cursor: number,
|
|
149
|
+
): NativeTextLineLayoutResult<NativeTextLineRange> {
|
|
150
|
+
if (lineText.length === 0 && text.length === 0 && cursor === 0) {
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
value: {
|
|
154
|
+
index: lineIndex,
|
|
155
|
+
sourceRange: { end: 0, start: 0 },
|
|
156
|
+
text: lineText,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (lineText.length === 0) {
|
|
162
|
+
return createNativeTextLineLayoutFailure(
|
|
163
|
+
'empty-line-text-unsupported',
|
|
164
|
+
'nativeText line layout cannot map an empty rendered line to a stable source range yet.',
|
|
165
|
+
{ lineIndex, sourceRange: { end: cursor, start: cursor } },
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const start = text.indexOf(lineText, cursor);
|
|
170
|
+
|
|
171
|
+
if (start < 0) {
|
|
172
|
+
return createNativeTextLineLayoutFailure(
|
|
173
|
+
'line-text-not-found',
|
|
174
|
+
'nativeText line layout could not find the rendered line text after the previous source range.',
|
|
175
|
+
{ lineIndex, sourceRange: { end: cursor, start: cursor } },
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
value: {
|
|
182
|
+
index: lineIndex,
|
|
183
|
+
sourceRange: { end: start + lineText.length, start },
|
|
184
|
+
text: lineText,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function rangesIntersect(first: TextMotionSourceRange, second: TextMotionSourceRange): boolean {
|
|
190
|
+
return first.start < second.end && second.start < first.end;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function rangeContainsRange(outer: TextMotionSourceRange, inner: TextMotionSourceRange): boolean {
|
|
194
|
+
return inner.start >= outer.start && inner.end <= outer.end;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function findWhitespaceSeparatorLineIndex(
|
|
198
|
+
sourceRange: TextMotionSourceRange,
|
|
199
|
+
lineRanges: readonly NativeTextLineRange[],
|
|
200
|
+
): number | undefined {
|
|
201
|
+
const previousIndex = lineRanges.reduce<number | undefined>(
|
|
202
|
+
(match, lineRange, index) => (lineRange.sourceRange.end <= sourceRange.start ? index : match),
|
|
203
|
+
undefined,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (typeof previousIndex === 'number') {
|
|
207
|
+
return previousIndex;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const firstLine = lineRanges[0];
|
|
211
|
+
|
|
212
|
+
if (firstLine && sourceRange.end <= firstLine.sourceRange.start) {
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function findLineIndexForToken(
|
|
220
|
+
token: TextMotionToken,
|
|
221
|
+
lineRanges: readonly NativeTextLineRange[],
|
|
222
|
+
): NativeTextLineLayoutResult<number | undefined> {
|
|
223
|
+
if (token.sourceRange.start === token.sourceRange.end) {
|
|
224
|
+
return { ok: true, value: undefined };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const intersectingIndexes = lineRanges.flatMap((lineRange, index) =>
|
|
228
|
+
rangesIntersect(token.sourceRange, lineRange.sourceRange) ? [index] : [],
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (intersectingIndexes.length > 1) {
|
|
232
|
+
return createNativeTextLineLayoutFailure(
|
|
233
|
+
'token-crosses-line',
|
|
234
|
+
'nativeText line layout cannot silently assign one token to multiple rendered lines.',
|
|
235
|
+
{ sourceRange: token.sourceRange, tokenId: token.id },
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const lineIndex = intersectingIndexes[0];
|
|
240
|
+
|
|
241
|
+
if (typeof lineIndex === 'number') {
|
|
242
|
+
const lineRange = lineRanges[lineIndex];
|
|
243
|
+
|
|
244
|
+
if (lineRange && rangeContainsRange(lineRange.sourceRange, token.sourceRange)) {
|
|
245
|
+
return { ok: true, value: lineIndex };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return createNativeTextLineLayoutFailure(
|
|
249
|
+
'token-crosses-line',
|
|
250
|
+
'nativeText line layout found a partial token and requires deterministic handling before stable line support.',
|
|
251
|
+
{ lineIndex, sourceRange: token.sourceRange, tokenId: token.id },
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (token.text.trim().length === 0) {
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
value: findWhitespaceSeparatorLineIndex(token.sourceRange, lineRanges),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return createNativeTextLineLayoutFailure(
|
|
263
|
+
'token-outside-lines',
|
|
264
|
+
'nativeText line layout found a non-whitespace token outside all rendered line ranges.',
|
|
265
|
+
{ sourceRange: token.sourceRange, tokenId: token.id },
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function createStyleSnapshot(style: StyleProp<TextStyle>): NativeTextLineLayoutStyleSnapshot {
|
|
270
|
+
const flattenedStyle = StyleSheet.flatten(style) ?? {};
|
|
271
|
+
|
|
272
|
+
return LAYOUT_TEXT_STYLE_KEYS.reduce<NativeTextLineLayoutStyleSnapshot>((snapshot, key) => {
|
|
273
|
+
const value = flattenedStyle[key];
|
|
274
|
+
|
|
275
|
+
if (value === undefined) {
|
|
276
|
+
return snapshot;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
...snapshot,
|
|
281
|
+
[key]: value,
|
|
282
|
+
};
|
|
283
|
+
}, {});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function normalizeFiniteNumber(value: number): number | null {
|
|
287
|
+
return Number.isFinite(value) ? value : null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function createInitialInvalidationsByReason(): Record<
|
|
291
|
+
NativeTextLineLayoutInvalidationReason,
|
|
292
|
+
number
|
|
293
|
+
> {
|
|
294
|
+
return NATIVE_TEXT_LINE_LAYOUT_INVALIDATION_REASONS.reduce<
|
|
295
|
+
Record<NativeTextLineLayoutInvalidationReason, number>
|
|
296
|
+
>(
|
|
297
|
+
(invalidations, reason) => ({
|
|
298
|
+
...invalidations,
|
|
299
|
+
[reason]: 0,
|
|
300
|
+
}),
|
|
301
|
+
{
|
|
302
|
+
'font-scale': 0,
|
|
303
|
+
initial: 0,
|
|
304
|
+
'line-text': 0,
|
|
305
|
+
style: 0,
|
|
306
|
+
text: 0,
|
|
307
|
+
width: 0,
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function createNativeTextLineRanges(
|
|
313
|
+
text: string,
|
|
314
|
+
renderedLines: readonly Pick<NativeTextRenderedLine, 'text'>[],
|
|
315
|
+
): NativeTextLineLayoutResult<readonly NativeTextLineRange[]> {
|
|
316
|
+
const result: NativeTextLineRangeAccumulator = { cursor: 0, lines: [], ok: true };
|
|
317
|
+
|
|
318
|
+
// Keep this accumulator mutable so source mapping stays linear for stress probes.
|
|
319
|
+
for (const [lineIndex, renderedLine] of renderedLines.entries()) {
|
|
320
|
+
const lineRange = createNativeTextLineRange(text, renderedLine.text, lineIndex, result.cursor);
|
|
321
|
+
|
|
322
|
+
if (!lineRange.ok) {
|
|
323
|
+
return lineRange;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
result.cursor = lineRange.value.sourceRange.end;
|
|
327
|
+
result.lines.push(lineRange.value);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (text.length > 0 && result.lines.length === 0) {
|
|
331
|
+
return createNativeTextLineLayoutFailure(
|
|
332
|
+
'line-text-not-found',
|
|
333
|
+
'nativeText line layout needs rendered line data for non-empty text.',
|
|
334
|
+
{ sourceRange: { end: 0, start: 0 } },
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
ok: true,
|
|
340
|
+
value: result.lines,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function groupNativeTextTokensByLineRanges(
|
|
345
|
+
tokens: readonly TextMotionToken[],
|
|
346
|
+
lineRanges: readonly NativeTextLineRange[],
|
|
347
|
+
): NativeTextLineLayoutResult<readonly NativeTextLineTokenGroup[]> {
|
|
348
|
+
const groups: { line: NativeTextLineRange; tokens: TextMotionToken[] }[] = lineRanges.map(
|
|
349
|
+
(line) => ({
|
|
350
|
+
line,
|
|
351
|
+
tokens: [],
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
for (const token of tokens) {
|
|
356
|
+
const lineIndex = findLineIndexForToken(token, lineRanges);
|
|
357
|
+
|
|
358
|
+
if (!lineIndex.ok) {
|
|
359
|
+
return lineIndex;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (typeof lineIndex.value !== 'number') {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const group = groups[lineIndex.value];
|
|
367
|
+
|
|
368
|
+
if (!group) {
|
|
369
|
+
return createNativeTextLineLayoutFailure(
|
|
370
|
+
'token-outside-lines',
|
|
371
|
+
'nativeText line layout resolved a token to a missing line group.',
|
|
372
|
+
{ sourceRange: token.sourceRange, tokenId: token.id },
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
group.tokens.push(token);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
ok: true,
|
|
381
|
+
value: groups.map(({ line, tokens: groupTokens }) => ({
|
|
382
|
+
line,
|
|
383
|
+
text: groupTokens.map((token) => token.text).join(''),
|
|
384
|
+
tokens: groupTokens,
|
|
385
|
+
})),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function evaluateNativeTextLineLayoutCompatibility(
|
|
390
|
+
groups: readonly NativeTextLineTokenGroup[],
|
|
391
|
+
): NativeTextLineLayoutCompatibilityReport {
|
|
392
|
+
const mismatch = groups.find((group) => group.text !== group.line.text);
|
|
393
|
+
|
|
394
|
+
if (mismatch) {
|
|
395
|
+
return {
|
|
396
|
+
actualText: mismatch.text,
|
|
397
|
+
expectedText: mismatch.line.text,
|
|
398
|
+
lineIndex: mismatch.line.index,
|
|
399
|
+
reason: 'line-group-text-mismatch',
|
|
400
|
+
status: 'no-go',
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
lineCount: groups.length,
|
|
406
|
+
status: 'go',
|
|
407
|
+
tokenCount: groups.reduce((count, group) => count + group.tokens.length, 0),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function createNativeTextLineLayoutSignature({
|
|
412
|
+
allowFontScaling,
|
|
413
|
+
maxFontSizeMultiplier,
|
|
414
|
+
measuredWidth,
|
|
415
|
+
renderedLines,
|
|
416
|
+
style,
|
|
417
|
+
text,
|
|
418
|
+
}: NativeTextLineLayoutSignatureInput): string {
|
|
419
|
+
return JSON.stringify({
|
|
420
|
+
allowFontScaling,
|
|
421
|
+
maxFontSizeMultiplier,
|
|
422
|
+
measuredWidth: normalizeFiniteNumber(measuredWidth),
|
|
423
|
+
renderedLines: renderedLines.map((line) => ({
|
|
424
|
+
height: normalizeFiniteNumber(line.height),
|
|
425
|
+
text: line.text,
|
|
426
|
+
width: normalizeFiniteNumber(line.width),
|
|
427
|
+
x: normalizeFiniteNumber(line.x),
|
|
428
|
+
y: normalizeFiniteNumber(line.y),
|
|
429
|
+
})),
|
|
430
|
+
style: createStyleSnapshot(style),
|
|
431
|
+
text,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function createNativeTextLineLayoutProbeSnapshot(
|
|
436
|
+
initial?: Partial<
|
|
437
|
+
Pick<
|
|
438
|
+
NativeTextLineLayoutProbeSnapshot,
|
|
439
|
+
| 'acceptedMappingUpdates'
|
|
440
|
+
| 'lineCount'
|
|
441
|
+
| 'mappingComputations'
|
|
442
|
+
| 'onTextLayoutCalls'
|
|
443
|
+
| 'rejectedIdenticalPayloads'
|
|
444
|
+
| 'signature'
|
|
445
|
+
| 'tokenCount'
|
|
446
|
+
>
|
|
447
|
+
>,
|
|
448
|
+
): NativeTextLineLayoutProbeSnapshot {
|
|
449
|
+
return {
|
|
450
|
+
acceptedMappingUpdates: initial?.acceptedMappingUpdates ?? 0,
|
|
451
|
+
invalidationsByReason: createInitialInvalidationsByReason(),
|
|
452
|
+
lineCount: initial?.lineCount ?? 0,
|
|
453
|
+
mappingComputations: initial?.mappingComputations ?? 0,
|
|
454
|
+
onTextLayoutCalls: initial?.onTextLayoutCalls ?? 0,
|
|
455
|
+
rejectedIdenticalPayloads: initial?.rejectedIdenticalPayloads ?? 0,
|
|
456
|
+
signature: initial?.signature,
|
|
457
|
+
tokenCount: initial?.tokenCount ?? 0,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function recordNativeTextLineLayoutMeasurement(
|
|
462
|
+
snapshot: NativeTextLineLayoutProbeSnapshot,
|
|
463
|
+
measurement: NativeTextLineLayoutMeasurement,
|
|
464
|
+
): NativeTextLineLayoutProbeUpdate {
|
|
465
|
+
if (snapshot.signature === measurement.signature) {
|
|
466
|
+
return {
|
|
467
|
+
accepted: false,
|
|
468
|
+
snapshot: {
|
|
469
|
+
...snapshot,
|
|
470
|
+
lineCount: measurement.lineCount,
|
|
471
|
+
onTextLayoutCalls: snapshot.onTextLayoutCalls + 1,
|
|
472
|
+
rejectedIdenticalPayloads: snapshot.rejectedIdenticalPayloads + 1,
|
|
473
|
+
tokenCount: measurement.tokenCount,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
accepted: true,
|
|
480
|
+
snapshot: {
|
|
481
|
+
...snapshot,
|
|
482
|
+
acceptedMappingUpdates: snapshot.acceptedMappingUpdates + 1,
|
|
483
|
+
invalidationsByReason: {
|
|
484
|
+
...snapshot.invalidationsByReason,
|
|
485
|
+
[measurement.invalidationReason]:
|
|
486
|
+
snapshot.invalidationsByReason[measurement.invalidationReason] + 1,
|
|
487
|
+
},
|
|
488
|
+
lineCount: measurement.lineCount,
|
|
489
|
+
mappingComputations: snapshot.mappingComputations + 1,
|
|
490
|
+
onTextLayoutCalls: snapshot.onTextLayoutCalls + 1,
|
|
491
|
+
signature: measurement.signature,
|
|
492
|
+
tokenCount: measurement.tokenCount,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|