@pictor-sdk/ui 0.3.0 → 0.4.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/dist/index.d.mts +29 -6
- package/dist/index.d.ts +29 -6
- package/dist/index.js +171 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +171 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/components/Slider.tsx +3 -2
- package/src/composer/TextComposer.tsx +180 -9
- package/src/composer/keyboardLayout.ts +94 -1
- package/src/composer/tapGesture.ts +31 -0
- package/src/gestures/LayerGestureSurface.tsx +40 -4
- package/src/gestures/selectionTap.ts +22 -0
package/dist/index.d.mts
CHANGED
|
@@ -87,6 +87,14 @@ interface ComposerInsets {
|
|
|
87
87
|
/** Animation duration to match the platform's own, clamped to something sane. */
|
|
88
88
|
duration: number;
|
|
89
89
|
}
|
|
90
|
+
interface CanvasRect {
|
|
91
|
+
/** Size of the fitted canvas inside the container. */
|
|
92
|
+
width: number;
|
|
93
|
+
height: number;
|
|
94
|
+
/** Offset of the canvas inside the container, which centres it. */
|
|
95
|
+
left: number;
|
|
96
|
+
top: number;
|
|
97
|
+
}
|
|
90
98
|
|
|
91
99
|
/**
|
|
92
100
|
* The state and commands behind a text-editing session.
|
|
@@ -167,6 +175,11 @@ interface TextComposerProps {
|
|
|
167
175
|
onDone(): void;
|
|
168
176
|
/** Abandon, restoring the document. */
|
|
169
177
|
onCancel(): void;
|
|
178
|
+
/**
|
|
179
|
+
* The canvas rectangle in this composer's coordinates, when the host draws a
|
|
180
|
+
* cover canvas. Omitted, the composer assumes a centred contain fit.
|
|
181
|
+
*/
|
|
182
|
+
canvas?: CanvasRect;
|
|
170
183
|
}
|
|
171
184
|
/**
|
|
172
185
|
* The full-screen Story text composer.
|
|
@@ -176,20 +189,22 @@ interface TextComposerProps {
|
|
|
176
189
|
* `renderDocument` — so what is on screen while composing is what exports.
|
|
177
190
|
*
|
|
178
191
|
* The `TextInput` itself is invisible and stretched over the canvas: it owns
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
192
|
+
* focus and the IME, while the *visible* text is the real Skia layer
|
|
193
|
+
* underneath. On Android the platform paints composing glyphs black regardless
|
|
194
|
+
* of the input's colour, so the input is fully hidden and a caret is drawn at
|
|
195
|
+
* the measured selection instead. That is what keeps preview and export
|
|
196
|
+
* identical without giving up text editing.
|
|
182
197
|
*
|
|
183
198
|
* @experimental
|
|
184
199
|
*/
|
|
185
|
-
declare function TextComposer({ session, insets, onDone, onCancel }: TextComposerProps): React__default.JSX.Element;
|
|
200
|
+
declare function TextComposer({ session, insets, onDone, onCancel, canvas }: TextComposerProps): React__default.JSX.Element;
|
|
186
201
|
/**
|
|
187
202
|
* Mounts a text session and the composer for it, and keeps the keyboard inset
|
|
188
203
|
* up to date. This is what `<Editor>` renders while the text tool is active.
|
|
189
204
|
*
|
|
190
205
|
* @experimental
|
|
191
206
|
*/
|
|
192
|
-
declare function TextComposerHost({ Composer, safeArea, }: {
|
|
207
|
+
declare function TextComposerHost({ Composer, safeArea, canvas, }: {
|
|
193
208
|
/** Replace the default composer without replacing the canvas or editor. */
|
|
194
209
|
Composer?: React__default.ComponentType<TextComposerProps>;
|
|
195
210
|
/**
|
|
@@ -203,6 +218,8 @@ declare function TextComposerHost({ Composer, safeArea, }: {
|
|
|
203
218
|
top?: number;
|
|
204
219
|
bottom?: number;
|
|
205
220
|
};
|
|
221
|
+
/** The canvas rectangle, for a host that draws a cover canvas. */
|
|
222
|
+
canvas?: CanvasRect;
|
|
206
223
|
}): React__default.JSX.Element;
|
|
207
224
|
|
|
208
225
|
/** What the top bar slot receives: the transaction actions and undo state. */
|
|
@@ -605,6 +622,12 @@ declare function EditorGestures({ onSnap }: EditorGesturesProps): React__default
|
|
|
605
622
|
interface LayerGestureSurfaceProps {
|
|
606
623
|
/** Fired once per snap engagement — hook up haptics here if you want them. */
|
|
607
624
|
onSnap?: (event: GestureSnapEvent) => void;
|
|
625
|
+
/**
|
|
626
|
+
* Fired when a tap lands on a layer that was already selected when the
|
|
627
|
+
* gesture began. Hosts use this for a second-tap action such as opening the
|
|
628
|
+
* text composer; the SDK itself stays unaware of what a layer means.
|
|
629
|
+
*/
|
|
630
|
+
onSelectedLayerTap?: (layerId: string) => void;
|
|
608
631
|
}
|
|
609
632
|
/**
|
|
610
633
|
* Direct manipulation of overlay layers: grab the top-most layer under the
|
|
@@ -622,7 +645,7 @@ interface LayerGestureSurfaceProps {
|
|
|
622
645
|
* - **One history entry.** The document is written exactly once, when the
|
|
623
646
|
* gesture settles, and not at all if nothing actually moved.
|
|
624
647
|
*/
|
|
625
|
-
declare function LayerGestureSurface({ onSnap }: LayerGestureSurfaceProps): React__default.JSX.Element;
|
|
648
|
+
declare function LayerGestureSurface({ onSnap, onSelectedLayerTap }: LayerGestureSurfaceProps): React__default.JSX.Element;
|
|
626
649
|
|
|
627
650
|
/** The centre and rotation guides shown while a layer snaps. */
|
|
628
651
|
interface InteractionGuidesProps {
|
package/dist/index.d.ts
CHANGED
|
@@ -87,6 +87,14 @@ interface ComposerInsets {
|
|
|
87
87
|
/** Animation duration to match the platform's own, clamped to something sane. */
|
|
88
88
|
duration: number;
|
|
89
89
|
}
|
|
90
|
+
interface CanvasRect {
|
|
91
|
+
/** Size of the fitted canvas inside the container. */
|
|
92
|
+
width: number;
|
|
93
|
+
height: number;
|
|
94
|
+
/** Offset of the canvas inside the container, which centres it. */
|
|
95
|
+
left: number;
|
|
96
|
+
top: number;
|
|
97
|
+
}
|
|
90
98
|
|
|
91
99
|
/**
|
|
92
100
|
* The state and commands behind a text-editing session.
|
|
@@ -167,6 +175,11 @@ interface TextComposerProps {
|
|
|
167
175
|
onDone(): void;
|
|
168
176
|
/** Abandon, restoring the document. */
|
|
169
177
|
onCancel(): void;
|
|
178
|
+
/**
|
|
179
|
+
* The canvas rectangle in this composer's coordinates, when the host draws a
|
|
180
|
+
* cover canvas. Omitted, the composer assumes a centred contain fit.
|
|
181
|
+
*/
|
|
182
|
+
canvas?: CanvasRect;
|
|
170
183
|
}
|
|
171
184
|
/**
|
|
172
185
|
* The full-screen Story text composer.
|
|
@@ -176,20 +189,22 @@ interface TextComposerProps {
|
|
|
176
189
|
* `renderDocument` — so what is on screen while composing is what exports.
|
|
177
190
|
*
|
|
178
191
|
* The `TextInput` itself is invisible and stretched over the canvas: it owns
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
192
|
+
* focus and the IME, while the *visible* text is the real Skia layer
|
|
193
|
+
* underneath. On Android the platform paints composing glyphs black regardless
|
|
194
|
+
* of the input's colour, so the input is fully hidden and a caret is drawn at
|
|
195
|
+
* the measured selection instead. That is what keeps preview and export
|
|
196
|
+
* identical without giving up text editing.
|
|
182
197
|
*
|
|
183
198
|
* @experimental
|
|
184
199
|
*/
|
|
185
|
-
declare function TextComposer({ session, insets, onDone, onCancel }: TextComposerProps): React__default.JSX.Element;
|
|
200
|
+
declare function TextComposer({ session, insets, onDone, onCancel, canvas }: TextComposerProps): React__default.JSX.Element;
|
|
186
201
|
/**
|
|
187
202
|
* Mounts a text session and the composer for it, and keeps the keyboard inset
|
|
188
203
|
* up to date. This is what `<Editor>` renders while the text tool is active.
|
|
189
204
|
*
|
|
190
205
|
* @experimental
|
|
191
206
|
*/
|
|
192
|
-
declare function TextComposerHost({ Composer, safeArea, }: {
|
|
207
|
+
declare function TextComposerHost({ Composer, safeArea, canvas, }: {
|
|
193
208
|
/** Replace the default composer without replacing the canvas or editor. */
|
|
194
209
|
Composer?: React__default.ComponentType<TextComposerProps>;
|
|
195
210
|
/**
|
|
@@ -203,6 +218,8 @@ declare function TextComposerHost({ Composer, safeArea, }: {
|
|
|
203
218
|
top?: number;
|
|
204
219
|
bottom?: number;
|
|
205
220
|
};
|
|
221
|
+
/** The canvas rectangle, for a host that draws a cover canvas. */
|
|
222
|
+
canvas?: CanvasRect;
|
|
206
223
|
}): React__default.JSX.Element;
|
|
207
224
|
|
|
208
225
|
/** What the top bar slot receives: the transaction actions and undo state. */
|
|
@@ -605,6 +622,12 @@ declare function EditorGestures({ onSnap }: EditorGesturesProps): React__default
|
|
|
605
622
|
interface LayerGestureSurfaceProps {
|
|
606
623
|
/** Fired once per snap engagement — hook up haptics here if you want them. */
|
|
607
624
|
onSnap?: (event: GestureSnapEvent) => void;
|
|
625
|
+
/**
|
|
626
|
+
* Fired when a tap lands on a layer that was already selected when the
|
|
627
|
+
* gesture began. Hosts use this for a second-tap action such as opening the
|
|
628
|
+
* text composer; the SDK itself stays unaware of what a layer means.
|
|
629
|
+
*/
|
|
630
|
+
onSelectedLayerTap?: (layerId: string) => void;
|
|
608
631
|
}
|
|
609
632
|
/**
|
|
610
633
|
* Direct manipulation of overlay layers: grab the top-most layer under the
|
|
@@ -622,7 +645,7 @@ interface LayerGestureSurfaceProps {
|
|
|
622
645
|
* - **One history entry.** The document is written exactly once, when the
|
|
623
646
|
* gesture settles, and not at all if nothing actually moved.
|
|
624
647
|
*/
|
|
625
|
-
declare function LayerGestureSurface({ onSnap }: LayerGestureSurfaceProps): React__default.JSX.Element;
|
|
648
|
+
declare function LayerGestureSurface({ onSnap, onSelectedLayerTap }: LayerGestureSurfaceProps): React__default.JSX.Element;
|
|
626
649
|
|
|
627
650
|
/** The centre and rotation guides shown while a layer snaps. */
|
|
628
651
|
interface InteractionGuidesProps {
|
package/dist/index.js
CHANGED
|
@@ -793,10 +793,16 @@ function hitTestLayerCandidatesUI(x, y, candidates, documentWidth, documentHeigh
|
|
|
793
793
|
return null;
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
+
// src/gestures/selectionTap.ts
|
|
797
|
+
function selectedLayerTapId(hit, wasSelectedAtStart) {
|
|
798
|
+
if (!hit || hit.locked || !wasSelectedAtStart) return null;
|
|
799
|
+
return hit.id;
|
|
800
|
+
}
|
|
801
|
+
|
|
796
802
|
// src/gestures/LayerGestureSurface.tsx
|
|
797
803
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
798
|
-
function LayerGestureSurface({ onSnap }) {
|
|
799
|
-
const { document, dispatch, profile } = (0, import_core8.useEditor)();
|
|
804
|
+
function LayerGestureSurface({ onSnap, onSelectedLayerTap }) {
|
|
805
|
+
const { document, dispatch, profile, selection } = (0, import_core8.useEditor)();
|
|
800
806
|
const store = (0, import_core8.useEditorStore)();
|
|
801
807
|
const metrics = (0, import_skia4.useCanvasMetrics)();
|
|
802
808
|
const { setPreview, setLayerDragActive, setOverDeleteTarget, setSnap } = useInteractionState();
|
|
@@ -887,10 +893,11 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
887
893
|
document.size.height
|
|
888
894
|
);
|
|
889
895
|
}, [document.size.width, document.size.height]);
|
|
890
|
-
const latest = (0, import_react9.useRef)({ document, metrics, store, dispatch, onSnap, profile });
|
|
891
|
-
latest.current = { document, metrics, store, dispatch, onSnap, profile };
|
|
896
|
+
const latest = (0, import_react9.useRef)({ document, metrics, store, dispatch, onSnap, profile, selection, onSelectedLayerTap });
|
|
897
|
+
latest.current = { document, metrics, store, dispatch, onSnap, profile, selection, onSelectedLayerTap };
|
|
892
898
|
const grabbedId = (0, import_react9.useRef)(null);
|
|
893
899
|
const grabbedFrom = (0, import_react9.useRef)(null);
|
|
900
|
+
const tapWasSelected = (0, import_react9.useRef)(false);
|
|
894
901
|
const snapState = (0, import_react9.useRef)(import_core8.NO_SNAP);
|
|
895
902
|
const onGrabLayer = (0, import_react9.useCallback)((id) => {
|
|
896
903
|
const { document: doc, store: s } = latest.current;
|
|
@@ -903,9 +910,23 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
903
910
|
setPreview({ layerId: hit.id, transform: previewTransform });
|
|
904
911
|
setLayerDragActive(true);
|
|
905
912
|
}, [setPreview, setLayerDragActive, previewTransform]);
|
|
913
|
+
const snapshotTapAt = (0, import_react9.useCallback)((xDp, yDp) => {
|
|
914
|
+
const { document: doc, metrics: m, store: s, selection: selected } = latest.current;
|
|
915
|
+
if (!m) return;
|
|
916
|
+
const layerInteraction = latest.current.profile.interaction?.layers;
|
|
917
|
+
const hit = (0, import_core8.hitTestDocument)(
|
|
918
|
+
doc,
|
|
919
|
+
{ x: xDp / m.width, y: yDp / m.height },
|
|
920
|
+
s.registry,
|
|
921
|
+
(layer, index) => (0, import_core8.isDirectManipulationLayer)(layer, index, layerInteraction)
|
|
922
|
+
);
|
|
923
|
+
tapWasSelected.current = Boolean(hit && selected.includes(hit.id));
|
|
924
|
+
}, []);
|
|
906
925
|
const onSelectAt = (0, import_react9.useCallback)((xDp, yDp) => {
|
|
907
926
|
const { document: doc, metrics: m, store: s } = latest.current;
|
|
908
927
|
if (!m) return;
|
|
928
|
+
const wasSelected = tapWasSelected.current;
|
|
929
|
+
tapWasSelected.current = false;
|
|
909
930
|
const layerInteraction = latest.current.profile.interaction?.layers;
|
|
910
931
|
const hit = (0, import_core8.hitTestDocument)(
|
|
911
932
|
doc,
|
|
@@ -914,6 +935,8 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
914
935
|
(layer, index) => (0, import_core8.isDirectManipulationLayer)(layer, index, layerInteraction)
|
|
915
936
|
);
|
|
916
937
|
s.select(hit && !hit.locked ? [hit.id] : []);
|
|
938
|
+
const editId = selectedLayerTapId(hit ?? null, wasSelected);
|
|
939
|
+
if (editId) latest.current.onSelectedLayerTap?.(editId);
|
|
917
940
|
}, []);
|
|
918
941
|
const onSnapChanged = (0, import_react9.useCallback)(
|
|
919
942
|
(centerX, centerY, rotation) => {
|
|
@@ -1052,6 +1075,8 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
1052
1075
|
const pan = import_react_native_gesture_handler4.Gesture.Pan().minPointers(1).maxPointers(2).onTouchesDown((e) => {
|
|
1053
1076
|
"worklet";
|
|
1054
1077
|
livePointers.value = e.allTouches.length;
|
|
1078
|
+
const touch = e.changedTouches[0] ?? e.allTouches[0];
|
|
1079
|
+
if (touch) (0, import_react_native_worklets4.runOnJS)(snapshotTapAt)(touch.x, touch.y);
|
|
1055
1080
|
}).onTouchesUp((e) => {
|
|
1056
1081
|
"worklet";
|
|
1057
1082
|
livePointers.value = Math.max(0, e.allTouches.length - e.changedTouches.length);
|
|
@@ -1142,6 +1167,7 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
1142
1167
|
deleteTarget,
|
|
1143
1168
|
aspect,
|
|
1144
1169
|
onGrabLayer,
|
|
1170
|
+
snapshotTapAt,
|
|
1145
1171
|
onSelectAt,
|
|
1146
1172
|
onSnapChanged,
|
|
1147
1173
|
onHoverChanged,
|
|
@@ -1819,8 +1845,9 @@ function Slider({
|
|
|
1819
1845
|
const valueRef = (0, import_react12.useRef)(value);
|
|
1820
1846
|
valueRef.current = value;
|
|
1821
1847
|
const onLayout = (e) => {
|
|
1822
|
-
|
|
1823
|
-
widthRef.current =
|
|
1848
|
+
const next = e.nativeEvent.layout.width;
|
|
1849
|
+
widthRef.current = next;
|
|
1850
|
+
setWidth((previous) => previous === next ? previous : next);
|
|
1824
1851
|
};
|
|
1825
1852
|
const handlers = (0, import_react12.useMemo)(() => {
|
|
1826
1853
|
const begin = () => {
|
|
@@ -3562,7 +3589,8 @@ function fitCanvasRect(container, document) {
|
|
|
3562
3589
|
};
|
|
3563
3590
|
}
|
|
3564
3591
|
function composerInputBox(request) {
|
|
3565
|
-
const
|
|
3592
|
+
const provided = request.canvas;
|
|
3593
|
+
const canvas = provided && provided.width > 0 && provided.height > 0 ? provided : fitCanvasRect(request.container, request.document);
|
|
3566
3594
|
const fontSize = Math.max(1, request.fontSize * canvas.width);
|
|
3567
3595
|
const lineHeight = Math.max(1, fontSize * request.lineHeight);
|
|
3568
3596
|
const scale = request.document.width > 0 ? canvas.width / request.document.width : 0;
|
|
@@ -3586,6 +3614,35 @@ function composerInputBox(request) {
|
|
|
3586
3614
|
lineHeight
|
|
3587
3615
|
};
|
|
3588
3616
|
}
|
|
3617
|
+
function sameCaretLine(previous, next) {
|
|
3618
|
+
return previous === next || previous !== null && next !== null && previous.x === next.x && previous.y === next.y && previous.width === next.width && previous.height === next.height;
|
|
3619
|
+
}
|
|
3620
|
+
var CARET_WIDTH = 2;
|
|
3621
|
+
function composerInputOpacity(platform) {
|
|
3622
|
+
return platform === "android" ? 0 : 1;
|
|
3623
|
+
}
|
|
3624
|
+
function composerCaretBox(box, line) {
|
|
3625
|
+
if (!line || !Number.isFinite(line.x) || !Number.isFinite(line.y) || !Number.isFinite(line.width) || !Number.isFinite(line.height)) {
|
|
3626
|
+
return null;
|
|
3627
|
+
}
|
|
3628
|
+
const height = Math.max(1, line.height || box.lineHeight);
|
|
3629
|
+
return {
|
|
3630
|
+
left: box.left + line.x + line.width - CARET_WIDTH / 2,
|
|
3631
|
+
top: box.top + line.y,
|
|
3632
|
+
width: CARET_WIDTH,
|
|
3633
|
+
height
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
// src/composer/tapGesture.ts
|
|
3638
|
+
var TAP_SLOP = 8;
|
|
3639
|
+
function pressMoved(start, end, slop = TAP_SLOP) {
|
|
3640
|
+
if (!start || !end) return false;
|
|
3641
|
+
if (!Number.isFinite(start.x) || !Number.isFinite(start.y) || !Number.isFinite(end.x) || !Number.isFinite(end.y)) {
|
|
3642
|
+
return false;
|
|
3643
|
+
}
|
|
3644
|
+
return Math.hypot(end.x - start.x, end.y - start.y) > Math.max(0, slop);
|
|
3645
|
+
}
|
|
3589
3646
|
|
|
3590
3647
|
// src/composer/useTextSession.ts
|
|
3591
3648
|
var import_core26 = require("@pictor-sdk/core");
|
|
@@ -3717,7 +3774,7 @@ var SHADOWS = [
|
|
|
3717
3774
|
{ color: "#000000", blur: 0.012, dx: 2e-3, dy: 4e-3 },
|
|
3718
3775
|
{ color: "#000000", blur: 2e-3, dx: 6e-3, dy: 8e-3 }
|
|
3719
3776
|
];
|
|
3720
|
-
function TextComposer({ session, insets, onDone, onCancel }) {
|
|
3777
|
+
function TextComposer({ session, insets, onDone, onCancel, canvas }) {
|
|
3721
3778
|
const theme = useTheme();
|
|
3722
3779
|
const store = (0, import_core27.useEditorStore)();
|
|
3723
3780
|
const { document } = (0, import_core27.useEditor)();
|
|
@@ -3727,6 +3784,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3727
3784
|
const [selectionRange, setSelectionRange] = (0, import_react21.useState)(
|
|
3728
3785
|
void 0
|
|
3729
3786
|
);
|
|
3787
|
+
const [focused, setFocused] = (0, import_react21.useState)(false);
|
|
3788
|
+
const [caretLine, setCaretLine] = (0, import_react21.useState)(null);
|
|
3730
3789
|
const data = session.data;
|
|
3731
3790
|
const span = session.span;
|
|
3732
3791
|
const layerId = session.layer?.id;
|
|
@@ -3736,7 +3795,12 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3736
3795
|
setMeasured(null);
|
|
3737
3796
|
return;
|
|
3738
3797
|
}
|
|
3739
|
-
const refresh = () =>
|
|
3798
|
+
const refresh = () => {
|
|
3799
|
+
const next = renderer.textMeasures.get(layerId) ?? null;
|
|
3800
|
+
setMeasured(
|
|
3801
|
+
(previous) => previous && next && previous.width === next.width && previous.height === next.height ? previous : next
|
|
3802
|
+
);
|
|
3803
|
+
};
|
|
3740
3804
|
const unsubscribe = renderer.textMeasures.subscribe(refresh);
|
|
3741
3805
|
refresh();
|
|
3742
3806
|
return unsubscribe;
|
|
@@ -3749,7 +3813,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3749
3813
|
lineHeight: span?.lineHeight ?? 1.2,
|
|
3750
3814
|
centerY: session.layer?.transform.y ?? 0.42,
|
|
3751
3815
|
measured,
|
|
3752
|
-
lines: session.value.split("\n").length
|
|
3816
|
+
lines: session.value.split("\n").length,
|
|
3817
|
+
...canvas ? { canvas } : {}
|
|
3753
3818
|
}),
|
|
3754
3819
|
[
|
|
3755
3820
|
container,
|
|
@@ -3758,20 +3823,41 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3758
3823
|
span?.lineHeight,
|
|
3759
3824
|
session.layer?.transform.y,
|
|
3760
3825
|
session.value,
|
|
3761
|
-
measured
|
|
3826
|
+
measured,
|
|
3827
|
+
canvas
|
|
3762
3828
|
]
|
|
3763
3829
|
);
|
|
3764
3830
|
const fonts = (0, import_react21.useMemo)(() => (0, import_core27.fontChoices)(store.registry.fonts), [store.registry.fonts]);
|
|
3765
3831
|
(0, import_react21.useEffect)(() => {
|
|
3766
|
-
const handle = setTimeout(() =>
|
|
3832
|
+
const handle = setTimeout(() => {
|
|
3833
|
+
input.current?.focus();
|
|
3834
|
+
if (session.kind === "existing" && session.value.length > 0) {
|
|
3835
|
+
const end = session.value.length;
|
|
3836
|
+
setSelectionRange({ start: end, end });
|
|
3837
|
+
input.current?.setNativeProps({ selection: { start: end, end } });
|
|
3838
|
+
}
|
|
3839
|
+
}, 50);
|
|
3767
3840
|
return () => clearTimeout(handle);
|
|
3768
3841
|
}, []);
|
|
3769
3842
|
const onSelectionChange = (0, import_react21.useCallback)(
|
|
3770
3843
|
(event) => {
|
|
3771
|
-
|
|
3844
|
+
const next = event.nativeEvent.selection;
|
|
3845
|
+
setSelectionRange(
|
|
3846
|
+
(previous) => previous && previous.start === next.start && previous.end === next.end ? previous : next
|
|
3847
|
+
);
|
|
3772
3848
|
},
|
|
3773
3849
|
[]
|
|
3774
3850
|
);
|
|
3851
|
+
const caretAt = selectionRange?.end ?? session.value.length;
|
|
3852
|
+
const caretSource = session.value.slice(0, Math.max(0, caretAt));
|
|
3853
|
+
const onCaretLayout = (0, import_react21.useCallback)((event) => {
|
|
3854
|
+
const lines = event.nativeEvent.lines;
|
|
3855
|
+
const last = lines[lines.length - 1];
|
|
3856
|
+
const next = last ? { x: last.x, y: last.y, width: last.width, height: last.height } : null;
|
|
3857
|
+
setCaretLine((previous) => sameCaretLine(previous, next) ? previous : next);
|
|
3858
|
+
}, []);
|
|
3859
|
+
const caret = composerCaretBox(layout, caretLine);
|
|
3860
|
+
const letterSpacing = span?.letterSpacing && span.fontSize > 0 ? span.letterSpacing * layout.fontSize / span.fontSize : void 0;
|
|
3775
3861
|
const keepFocus = (0, import_react21.useCallback)(
|
|
3776
3862
|
(apply) => {
|
|
3777
3863
|
apply();
|
|
@@ -3832,6 +3918,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3832
3918
|
});
|
|
3833
3919
|
return () => subscription.remove();
|
|
3834
3920
|
}, [onCancel, insets.keyboardVisible]);
|
|
3921
|
+
const tapStart = (0, import_react21.useRef)(null);
|
|
3835
3922
|
const controlRow = {
|
|
3836
3923
|
flexDirection: "row",
|
|
3837
3924
|
alignItems: "center",
|
|
@@ -3856,6 +3943,54 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3856
3943
|
style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0, backgroundColor: "#00000055" }
|
|
3857
3944
|
}
|
|
3858
3945
|
),
|
|
3946
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
3947
|
+
import_react_native24.Pressable,
|
|
3948
|
+
{
|
|
3949
|
+
accessible: true,
|
|
3950
|
+
collapsable: false,
|
|
3951
|
+
accessibilityRole: "button",
|
|
3952
|
+
accessibilityLabel: "Finish editing text",
|
|
3953
|
+
onAccessibilityTap: onDone,
|
|
3954
|
+
onPressIn: (event) => {
|
|
3955
|
+
tapStart.current = {
|
|
3956
|
+
x: event.nativeEvent.pageX,
|
|
3957
|
+
y: event.nativeEvent.pageY
|
|
3958
|
+
};
|
|
3959
|
+
},
|
|
3960
|
+
onPress: (event) => {
|
|
3961
|
+
const start = tapStart.current;
|
|
3962
|
+
const end = { x: event.nativeEvent.pageX, y: event.nativeEvent.pageY };
|
|
3963
|
+
if (!pressMoved(start, end)) onDone();
|
|
3964
|
+
},
|
|
3965
|
+
style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }
|
|
3966
|
+
}
|
|
3967
|
+
),
|
|
3968
|
+
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
3969
|
+
import_react_native24.Text,
|
|
3970
|
+
{
|
|
3971
|
+
accessible: false,
|
|
3972
|
+
importantForAccessibility: "no-hide-descendants",
|
|
3973
|
+
pointerEvents: "none",
|
|
3974
|
+
onTextLayout: onCaretLayout,
|
|
3975
|
+
style: {
|
|
3976
|
+
position: "absolute",
|
|
3977
|
+
left: layout.left,
|
|
3978
|
+
width: layout.width,
|
|
3979
|
+
top: layout.top,
|
|
3980
|
+
height: layout.height,
|
|
3981
|
+
opacity: 0,
|
|
3982
|
+
fontSize: layout.fontSize,
|
|
3983
|
+
lineHeight: layout.lineHeight,
|
|
3984
|
+
fontFamily: span?.fontFamily,
|
|
3985
|
+
fontWeight: span?.fontWeight ? `${span.fontWeight}` : void 0,
|
|
3986
|
+
letterSpacing,
|
|
3987
|
+
textAlign: data?.align ?? "center",
|
|
3988
|
+
padding: 0
|
|
3989
|
+
},
|
|
3990
|
+
children: `${caretSource}\u200B`
|
|
3991
|
+
},
|
|
3992
|
+
`${caretSource}:${caretAt}`
|
|
3993
|
+
),
|
|
3859
3994
|
/* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
3860
3995
|
import_react_native24.TextInput,
|
|
3861
3996
|
{
|
|
@@ -3863,6 +3998,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3863
3998
|
value: session.value,
|
|
3864
3999
|
onChangeText: session.setValue,
|
|
3865
4000
|
onSelectionChange,
|
|
4001
|
+
onFocus: () => setFocused(true),
|
|
4002
|
+
onBlur: () => setFocused(false),
|
|
3866
4003
|
multiline: true,
|
|
3867
4004
|
submitBehavior: "newline",
|
|
3868
4005
|
autoCorrect: false,
|
|
@@ -3882,6 +4019,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3882
4019
|
// rgba rather than the 'transparent' keyword because the keyword is
|
|
3883
4020
|
// not reliably honoured for TextInput colour on Android.
|
|
3884
4021
|
color: "rgba(0,0,0,0)",
|
|
4022
|
+
opacity: composerInputOpacity(import_react_native24.Platform.OS),
|
|
3885
4023
|
fontSize: layout.fontSize,
|
|
3886
4024
|
lineHeight: layout.lineHeight,
|
|
3887
4025
|
textAlign: data?.align ?? "center",
|
|
@@ -3892,6 +4030,22 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3892
4030
|
cursorColor: theme.colors.accent
|
|
3893
4031
|
}
|
|
3894
4032
|
),
|
|
4033
|
+
focused && caret ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
|
|
4034
|
+
import_react_native24.View,
|
|
4035
|
+
{
|
|
4036
|
+
pointerEvents: "none",
|
|
4037
|
+
accessible: false,
|
|
4038
|
+
style: {
|
|
4039
|
+
position: "absolute",
|
|
4040
|
+
left: caret.left,
|
|
4041
|
+
top: caret.top,
|
|
4042
|
+
width: caret.width,
|
|
4043
|
+
height: caret.height,
|
|
4044
|
+
borderRadius: caret.width / 2,
|
|
4045
|
+
backgroundColor: theme.colors.accent
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
) : null,
|
|
3895
4049
|
/* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
|
|
3896
4050
|
import_react_native24.View,
|
|
3897
4051
|
{
|
|
@@ -4091,7 +4245,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
4091
4245
|
}
|
|
4092
4246
|
function TextComposerHost({
|
|
4093
4247
|
Composer = TextComposer,
|
|
4094
|
-
safeArea
|
|
4248
|
+
safeArea,
|
|
4249
|
+
canvas
|
|
4095
4250
|
}) {
|
|
4096
4251
|
const { setActiveTool } = (0, import_core27.useEditor)();
|
|
4097
4252
|
const session = useTextSession();
|
|
@@ -4139,7 +4294,8 @@ function TextComposerHost({
|
|
|
4139
4294
|
session,
|
|
4140
4295
|
insets,
|
|
4141
4296
|
onDone: () => finish(true),
|
|
4142
|
-
onCancel: () => finish(false)
|
|
4297
|
+
onCancel: () => finish(false),
|
|
4298
|
+
...canvas ? { canvas } : {}
|
|
4143
4299
|
}
|
|
4144
4300
|
);
|
|
4145
4301
|
}
|