@pictor-sdk/ui 0.2.0 → 0.2.2
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/README.md +1 -1
- package/dist/index.d.mts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +105 -28
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +101 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/composer/TextComposer.tsx +64 -22
- package/src/composer/keyboardLayout.ts +83 -0
- package/src/gestures/LayerGestureSurface.tsx +61 -13
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ photo and video editor SDK for React Native. Public APIs are pre-1.0 and experim
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
pnpm add @pictor-sdk/ui@0.2.
|
|
11
|
+
pnpm add @pictor-sdk/ui@0.2.1
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Install the package's declared peer dependencies in your app. Keep React, React Native,
|
package/dist/index.d.mts
CHANGED
|
@@ -71,6 +71,15 @@ declare const ThemeContext: React.Context<EditorTheme>;
|
|
|
71
71
|
declare function useTheme(): EditorTheme;
|
|
72
72
|
|
|
73
73
|
interface ComposerInsets {
|
|
74
|
+
/**
|
|
75
|
+
* Distance from the top of the screen to place the Cancel/Done bar.
|
|
76
|
+
*
|
|
77
|
+
* The composer cannot work this out for itself: reading the safe area needs
|
|
78
|
+
* `react-native-safe-area-context`, and this package takes no dependency the
|
|
79
|
+
* host has not already chosen. So the host supplies it, and a host that does
|
|
80
|
+
* not gets 0 — which is what every release before this one drew.
|
|
81
|
+
*/
|
|
82
|
+
top: number;
|
|
74
83
|
/** Distance from the bottom of the screen to place the control bar. */
|
|
75
84
|
bottom: number;
|
|
76
85
|
/** True while the keyboard occupies screen space. */
|
|
@@ -180,9 +189,20 @@ declare function TextComposer({ session, insets, onDone, onCancel }: TextCompose
|
|
|
180
189
|
*
|
|
181
190
|
* @experimental
|
|
182
191
|
*/
|
|
183
|
-
declare function TextComposerHost({ Composer, }: {
|
|
192
|
+
declare function TextComposerHost({ Composer, safeArea, }: {
|
|
184
193
|
/** Replace the default composer without replacing the canvas or editor. */
|
|
185
194
|
Composer?: React__default.ComponentType<TextComposerProps>;
|
|
195
|
+
/**
|
|
196
|
+
* The app's safe-area insets, in dp.
|
|
197
|
+
*
|
|
198
|
+
* Supplied by the host because reading them needs a dependency this package
|
|
199
|
+
* does not take. Omit it and the composer draws exactly where it always did:
|
|
200
|
+
* its bar at the very top, which on a device with a notch means under one.
|
|
201
|
+
*/
|
|
202
|
+
safeArea?: {
|
|
203
|
+
top?: number;
|
|
204
|
+
bottom?: number;
|
|
205
|
+
};
|
|
186
206
|
}): React__default.JSX.Element;
|
|
187
207
|
|
|
188
208
|
/** What the top bar slot receives: the transaction actions and undo state. */
|
package/dist/index.d.ts
CHANGED
|
@@ -71,6 +71,15 @@ declare const ThemeContext: React.Context<EditorTheme>;
|
|
|
71
71
|
declare function useTheme(): EditorTheme;
|
|
72
72
|
|
|
73
73
|
interface ComposerInsets {
|
|
74
|
+
/**
|
|
75
|
+
* Distance from the top of the screen to place the Cancel/Done bar.
|
|
76
|
+
*
|
|
77
|
+
* The composer cannot work this out for itself: reading the safe area needs
|
|
78
|
+
* `react-native-safe-area-context`, and this package takes no dependency the
|
|
79
|
+
* host has not already chosen. So the host supplies it, and a host that does
|
|
80
|
+
* not gets 0 — which is what every release before this one drew.
|
|
81
|
+
*/
|
|
82
|
+
top: number;
|
|
74
83
|
/** Distance from the bottom of the screen to place the control bar. */
|
|
75
84
|
bottom: number;
|
|
76
85
|
/** True while the keyboard occupies screen space. */
|
|
@@ -180,9 +189,20 @@ declare function TextComposer({ session, insets, onDone, onCancel }: TextCompose
|
|
|
180
189
|
*
|
|
181
190
|
* @experimental
|
|
182
191
|
*/
|
|
183
|
-
declare function TextComposerHost({ Composer, }: {
|
|
192
|
+
declare function TextComposerHost({ Composer, safeArea, }: {
|
|
184
193
|
/** Replace the default composer without replacing the canvas or editor. */
|
|
185
194
|
Composer?: React__default.ComponentType<TextComposerProps>;
|
|
195
|
+
/**
|
|
196
|
+
* The app's safe-area insets, in dp.
|
|
197
|
+
*
|
|
198
|
+
* Supplied by the host because reading them needs a dependency this package
|
|
199
|
+
* does not take. Omit it and the composer draws exactly where it always did:
|
|
200
|
+
* its bar at the very top, which on a device with a notch means under one.
|
|
201
|
+
*/
|
|
202
|
+
safeArea?: {
|
|
203
|
+
top?: number;
|
|
204
|
+
bottom?: number;
|
|
205
|
+
};
|
|
186
206
|
}): React__default.JSX.Element;
|
|
187
207
|
|
|
188
208
|
/** What the top bar slot receives: the transaction actions and undo state. */
|
package/dist/index.js
CHANGED
|
@@ -95,7 +95,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
95
95
|
|
|
96
96
|
// src/Editor.tsx
|
|
97
97
|
var import_core30 = require("@pictor-sdk/core");
|
|
98
|
-
var
|
|
98
|
+
var import_skia11 = require("@pictor-sdk/skia");
|
|
99
99
|
var import_react22 = __toESM(require("react"));
|
|
100
100
|
var import_react_native27 = require("react-native");
|
|
101
101
|
var import_react_native_gesture_handler8 = require("react-native-gesture-handler");
|
|
@@ -810,10 +810,12 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
810
810
|
);
|
|
811
811
|
const aspect = document.size.height / document.size.width;
|
|
812
812
|
const [measureVersion, setMeasureVersion] = (0, import_react9.useState)(0);
|
|
813
|
-
(0, import_react9.useEffect)(
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
813
|
+
(0, import_react9.useEffect)(() => {
|
|
814
|
+
const refresh = () => setMeasureVersion((v) => v + 1);
|
|
815
|
+
const unsubscribe = renderer.textMeasures.subscribe(refresh);
|
|
816
|
+
refresh();
|
|
817
|
+
return unsubscribe;
|
|
818
|
+
}, [renderer]);
|
|
817
819
|
const hitCandidates = (0, import_react9.useMemo)(
|
|
818
820
|
() => document.layers.map((layer, index) => ({ layer, index })).filter(
|
|
819
821
|
({ layer, index }) => !layer.hidden && !layer.locked && (0, import_core8.isDirectManipulationLayer)(layer, index, profile.interaction?.layers)
|
|
@@ -866,6 +868,7 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
866
868
|
const focalY = (0, import_react_native_reanimated4.useSharedValue)(0.5);
|
|
867
869
|
const pointers = (0, import_react_native_reanimated4.useSharedValue)(0);
|
|
868
870
|
const active = (0, import_react_native_reanimated4.useSharedValue)(false);
|
|
871
|
+
const livePointers = (0, import_react_native_reanimated4.useSharedValue)(0);
|
|
869
872
|
const snapCX = (0, import_react_native_reanimated4.useSharedValue)(false);
|
|
870
873
|
const snapCY = (0, import_react_native_reanimated4.useSharedValue)(false);
|
|
871
874
|
const snapR = (0, import_react_native_reanimated4.useSharedValue)(false);
|
|
@@ -1040,7 +1043,23 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
1040
1043
|
deleted
|
|
1041
1044
|
);
|
|
1042
1045
|
};
|
|
1043
|
-
const
|
|
1046
|
+
const releaseIfEmpty = () => {
|
|
1047
|
+
"worklet";
|
|
1048
|
+
if (livePointers.value > 0) return;
|
|
1049
|
+
endIfIdle();
|
|
1050
|
+
};
|
|
1051
|
+
const pan = import_react_native_gesture_handler4.Gesture.Pan().minPointers(1).maxPointers(2).onTouchesDown((e) => {
|
|
1052
|
+
"worklet";
|
|
1053
|
+
livePointers.value = e.allTouches.length;
|
|
1054
|
+
}).onTouchesUp((e) => {
|
|
1055
|
+
"worklet";
|
|
1056
|
+
livePointers.value = Math.max(0, e.allTouches.length - e.changedTouches.length);
|
|
1057
|
+
releaseIfEmpty();
|
|
1058
|
+
}).onTouchesCancelled(() => {
|
|
1059
|
+
"worklet";
|
|
1060
|
+
livePointers.value = 0;
|
|
1061
|
+
releaseIfEmpty();
|
|
1062
|
+
}).onBegin((e) => {
|
|
1044
1063
|
"worklet";
|
|
1045
1064
|
if (active.value) return;
|
|
1046
1065
|
rawPanX.value = 0;
|
|
@@ -1087,7 +1106,7 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
1087
1106
|
recompute(false);
|
|
1088
1107
|
}).onFinalize(() => {
|
|
1089
1108
|
"worklet";
|
|
1090
|
-
|
|
1109
|
+
releaseIfEmpty();
|
|
1091
1110
|
});
|
|
1092
1111
|
const pinch = import_react_native_gesture_handler4.Gesture.Pinch().onUpdate((e) => {
|
|
1093
1112
|
"worklet";
|
|
@@ -1096,6 +1115,9 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
1096
1115
|
focalY.value = e.focalY / metricsHeight;
|
|
1097
1116
|
syncPointers(e.numberOfPointers);
|
|
1098
1117
|
recompute(false);
|
|
1118
|
+
}).onFinalize(() => {
|
|
1119
|
+
"worklet";
|
|
1120
|
+
releaseIfEmpty();
|
|
1099
1121
|
});
|
|
1100
1122
|
const rotate = import_react_native_gesture_handler4.Gesture.Rotation().enabled(config.rotationEnabled).onUpdate((e) => {
|
|
1101
1123
|
"worklet";
|
|
@@ -1104,6 +1126,9 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
1104
1126
|
focalY.value = e.anchorY / metricsHeight;
|
|
1105
1127
|
syncPointers(e.numberOfPointers);
|
|
1106
1128
|
recompute(false);
|
|
1129
|
+
}).onFinalize(() => {
|
|
1130
|
+
"worklet";
|
|
1131
|
+
releaseIfEmpty();
|
|
1107
1132
|
});
|
|
1108
1133
|
const tap = import_react_native_gesture_handler4.Gesture.Tap().onEnd((e) => {
|
|
1109
1134
|
"worklet";
|
|
@@ -3494,17 +3519,21 @@ function ToolPanelHost() {
|
|
|
3494
3519
|
var import_core27 = require("@pictor-sdk/core");
|
|
3495
3520
|
var import_react21 = require("react");
|
|
3496
3521
|
var import_react_native24 = require("react-native");
|
|
3522
|
+
var import_skia10 = require("@pictor-sdk/skia");
|
|
3497
3523
|
|
|
3498
3524
|
// src/composer/keyboardLayout.ts
|
|
3499
3525
|
var HIDDEN_KEYBOARD = { height: 0 };
|
|
3500
3526
|
var MAX_DURATION = 400;
|
|
3501
3527
|
var FALLBACK_DURATION = 180;
|
|
3502
|
-
function composerInsets(frame, safeAreaBottom = 0) {
|
|
3528
|
+
function composerInsets(frame, safeAreaBottom = 0, safeAreaTop = 0) {
|
|
3503
3529
|
const height = Number.isFinite(frame.height) ? Math.max(0, frame.height) : 0;
|
|
3504
3530
|
const visible = height > 0;
|
|
3505
3531
|
const raw = frame.duration;
|
|
3506
3532
|
const duration = raw === void 0 || !Number.isFinite(raw) || raw <= 0 ? FALLBACK_DURATION : Math.min(MAX_DURATION, raw);
|
|
3507
3533
|
return {
|
|
3534
|
+
// The keyboard never covers the top, so this one is the safe area and
|
|
3535
|
+
// nothing else.
|
|
3536
|
+
top: Number.isFinite(safeAreaTop) ? Math.max(0, safeAreaTop) : 0,
|
|
3508
3537
|
bottom: visible ? height : Math.max(0, safeAreaBottom),
|
|
3509
3538
|
keyboardVisible: visible,
|
|
3510
3539
|
duration
|
|
@@ -3524,6 +3553,31 @@ function fitCanvasRect(container, document) {
|
|
|
3524
3553
|
top: (container.height - height) / 2
|
|
3525
3554
|
};
|
|
3526
3555
|
}
|
|
3556
|
+
function composerInputBox(request) {
|
|
3557
|
+
const canvas = fitCanvasRect(request.container, request.document);
|
|
3558
|
+
const fontSize = Math.max(1, request.fontSize * canvas.width);
|
|
3559
|
+
const lineHeight = Math.max(1, fontSize * request.lineHeight);
|
|
3560
|
+
const scale = request.document.width > 0 ? canvas.width / request.document.width : 0;
|
|
3561
|
+
const measuredHeight = request.measured && request.measured.height > 0 ? request.measured.height * scale : 0;
|
|
3562
|
+
const estimated = lineHeight * Math.max(1, request.lines);
|
|
3563
|
+
const height = Math.min(
|
|
3564
|
+
Math.max(lineHeight, measuredHeight > 0 ? measuredHeight : estimated),
|
|
3565
|
+
Math.max(lineHeight, canvas.height)
|
|
3566
|
+
);
|
|
3567
|
+
const centre = canvas.top + request.centerY * canvas.height;
|
|
3568
|
+
const top = Math.min(
|
|
3569
|
+
Math.max(0, centre - height / 2),
|
|
3570
|
+
Math.max(0, request.container.height - height)
|
|
3571
|
+
);
|
|
3572
|
+
return {
|
|
3573
|
+
left: canvas.left,
|
|
3574
|
+
width: Math.max(1, canvas.width),
|
|
3575
|
+
top,
|
|
3576
|
+
height: Math.max(1, height),
|
|
3577
|
+
fontSize,
|
|
3578
|
+
lineHeight
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3527
3581
|
|
|
3528
3582
|
// src/composer/useTextSession.ts
|
|
3529
3583
|
var import_core26 = require("@pictor-sdk/core");
|
|
@@ -3660,26 +3714,45 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3660
3714
|
const store = (0, import_core27.useEditorStore)();
|
|
3661
3715
|
const { document } = (0, import_core27.useEditor)();
|
|
3662
3716
|
const [container, setContainer] = (0, import_react21.useState)({ width: 0, height: 0 });
|
|
3717
|
+
const renderer = (0, import_skia10.useRenderer)();
|
|
3663
3718
|
const input = (0, import_react21.useRef)(null);
|
|
3664
3719
|
const [selectionRange, setSelectionRange] = (0, import_react21.useState)(
|
|
3665
3720
|
void 0
|
|
3666
3721
|
);
|
|
3667
3722
|
const data = session.data;
|
|
3668
3723
|
const span = session.span;
|
|
3669
|
-
const
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3724
|
+
const layerId = session.layer?.id;
|
|
3725
|
+
const [measured, setMeasured] = (0, import_react21.useState)(null);
|
|
3726
|
+
(0, import_react21.useEffect)(() => {
|
|
3727
|
+
if (!layerId) {
|
|
3728
|
+
setMeasured(null);
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
const refresh = () => setMeasured(renderer.textMeasures.get(layerId) ?? null);
|
|
3732
|
+
const unsubscribe = renderer.textMeasures.subscribe(refresh);
|
|
3733
|
+
refresh();
|
|
3734
|
+
return unsubscribe;
|
|
3735
|
+
}, [renderer, layerId]);
|
|
3736
|
+
const layout = (0, import_react21.useMemo)(
|
|
3737
|
+
() => composerInputBox({
|
|
3738
|
+
container,
|
|
3739
|
+
document: document.size,
|
|
3740
|
+
fontSize: span?.fontSize ?? 0.08,
|
|
3741
|
+
lineHeight: span?.lineHeight ?? 1.2,
|
|
3742
|
+
centerY: session.layer?.transform.y ?? 0.42,
|
|
3743
|
+
measured,
|
|
3744
|
+
lines: session.value.split("\n").length
|
|
3745
|
+
}),
|
|
3746
|
+
[
|
|
3747
|
+
container,
|
|
3748
|
+
document.size,
|
|
3749
|
+
span?.fontSize,
|
|
3750
|
+
span?.lineHeight,
|
|
3751
|
+
session.layer?.transform.y,
|
|
3752
|
+
session.value,
|
|
3753
|
+
measured
|
|
3754
|
+
]
|
|
3755
|
+
);
|
|
3683
3756
|
const fonts = (0, import_react21.useMemo)(() => (0, import_core27.fontChoices)(store.registry.fonts), [store.registry.fonts]);
|
|
3684
3757
|
(0, import_react21.useEffect)(() => {
|
|
3685
3758
|
const handle = setTimeout(() => input.current?.focus(), 50);
|
|
@@ -3818,7 +3891,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3818
3891
|
position: "absolute",
|
|
3819
3892
|
left: 0,
|
|
3820
3893
|
right: 0,
|
|
3821
|
-
top:
|
|
3894
|
+
top: insets.top,
|
|
3822
3895
|
height: 56,
|
|
3823
3896
|
flexDirection: "row",
|
|
3824
3897
|
alignItems: "center",
|
|
@@ -4009,7 +4082,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
4009
4082
|
);
|
|
4010
4083
|
}
|
|
4011
4084
|
function TextComposerHost({
|
|
4012
|
-
Composer = TextComposer
|
|
4085
|
+
Composer = TextComposer,
|
|
4086
|
+
safeArea
|
|
4013
4087
|
}) {
|
|
4014
4088
|
const { setActiveTool } = (0, import_core27.useEditor)();
|
|
4015
4089
|
const session = useTextSession();
|
|
@@ -4038,7 +4112,10 @@ function TextComposerHost({
|
|
|
4038
4112
|
hide.remove();
|
|
4039
4113
|
};
|
|
4040
4114
|
}, []);
|
|
4041
|
-
const insets = (0, import_react21.useMemo)(
|
|
4115
|
+
const insets = (0, import_react21.useMemo)(
|
|
4116
|
+
() => composerInsets(frame, safeArea?.bottom ?? 0, safeArea?.top ?? 0),
|
|
4117
|
+
[frame, safeArea?.bottom, safeArea?.top]
|
|
4118
|
+
);
|
|
4042
4119
|
const finish = (0, import_react21.useCallback)(
|
|
4043
4120
|
(commit) => {
|
|
4044
4121
|
if (commit) session.commit();
|
|
@@ -4163,7 +4240,7 @@ function CanvasStage({ children, onError }) {
|
|
|
4163
4240
|
const { previews } = useInteractionState();
|
|
4164
4241
|
const surface = resolveGestureSurface(activeTool);
|
|
4165
4242
|
return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
|
|
4166
|
-
|
|
4243
|
+
import_skia11.EditorCanvas,
|
|
4167
4244
|
{
|
|
4168
4245
|
style: { flex: 1 },
|
|
4169
4246
|
onError,
|
|
@@ -4283,7 +4360,7 @@ function Editor({
|
|
|
4283
4360
|
|
|
4284
4361
|
// src/gestures/EditorGestures.tsx
|
|
4285
4362
|
var import_core32 = require("@pictor-sdk/core");
|
|
4286
|
-
var
|
|
4363
|
+
var import_skia12 = require("@pictor-sdk/skia");
|
|
4287
4364
|
var import_react23 = require("react");
|
|
4288
4365
|
var import_react_native28 = require("react-native");
|
|
4289
4366
|
var import_react_native_gesture_handler9 = require("react-native-gesture-handler");
|
|
@@ -4391,7 +4468,7 @@ function createGestureController(store, getMetrics, options = {}) {
|
|
|
4391
4468
|
var import_jsx_runtime31 = require("react/jsx-runtime");
|
|
4392
4469
|
function EditorGestures({ onSnap }) {
|
|
4393
4470
|
const store = (0, import_core32.useEditorStore)();
|
|
4394
|
-
const metrics = (0,
|
|
4471
|
+
const metrics = (0, import_skia12.useCanvasMetrics)();
|
|
4395
4472
|
const metricsRef = (0, import_react23.useRef)(metrics);
|
|
4396
4473
|
metricsRef.current = metrics;
|
|
4397
4474
|
const onSnapRef = (0, import_react23.useRef)(onSnap);
|