@pictor-sdk/ui 0.2.1 → 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/dist/index.d.mts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +99 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +95 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/composer/TextComposer.tsx +64 -22
- package/src/composer/keyboardLayout.ts +83 -0
- package/src/gestures/LayerGestureSurface.tsx +54 -9
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");
|
|
@@ -868,6 +868,7 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
868
868
|
const focalY = (0, import_react_native_reanimated4.useSharedValue)(0.5);
|
|
869
869
|
const pointers = (0, import_react_native_reanimated4.useSharedValue)(0);
|
|
870
870
|
const active = (0, import_react_native_reanimated4.useSharedValue)(false);
|
|
871
|
+
const livePointers = (0, import_react_native_reanimated4.useSharedValue)(0);
|
|
871
872
|
const snapCX = (0, import_react_native_reanimated4.useSharedValue)(false);
|
|
872
873
|
const snapCY = (0, import_react_native_reanimated4.useSharedValue)(false);
|
|
873
874
|
const snapR = (0, import_react_native_reanimated4.useSharedValue)(false);
|
|
@@ -1042,7 +1043,23 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
1042
1043
|
deleted
|
|
1043
1044
|
);
|
|
1044
1045
|
};
|
|
1045
|
-
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) => {
|
|
1046
1063
|
"worklet";
|
|
1047
1064
|
if (active.value) return;
|
|
1048
1065
|
rawPanX.value = 0;
|
|
@@ -1089,7 +1106,7 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
1089
1106
|
recompute(false);
|
|
1090
1107
|
}).onFinalize(() => {
|
|
1091
1108
|
"worklet";
|
|
1092
|
-
|
|
1109
|
+
releaseIfEmpty();
|
|
1093
1110
|
});
|
|
1094
1111
|
const pinch = import_react_native_gesture_handler4.Gesture.Pinch().onUpdate((e) => {
|
|
1095
1112
|
"worklet";
|
|
@@ -1098,6 +1115,9 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
1098
1115
|
focalY.value = e.focalY / metricsHeight;
|
|
1099
1116
|
syncPointers(e.numberOfPointers);
|
|
1100
1117
|
recompute(false);
|
|
1118
|
+
}).onFinalize(() => {
|
|
1119
|
+
"worklet";
|
|
1120
|
+
releaseIfEmpty();
|
|
1101
1121
|
});
|
|
1102
1122
|
const rotate = import_react_native_gesture_handler4.Gesture.Rotation().enabled(config.rotationEnabled).onUpdate((e) => {
|
|
1103
1123
|
"worklet";
|
|
@@ -1106,6 +1126,9 @@ function LayerGestureSurface({ onSnap }) {
|
|
|
1106
1126
|
focalY.value = e.anchorY / metricsHeight;
|
|
1107
1127
|
syncPointers(e.numberOfPointers);
|
|
1108
1128
|
recompute(false);
|
|
1129
|
+
}).onFinalize(() => {
|
|
1130
|
+
"worklet";
|
|
1131
|
+
releaseIfEmpty();
|
|
1109
1132
|
});
|
|
1110
1133
|
const tap = import_react_native_gesture_handler4.Gesture.Tap().onEnd((e) => {
|
|
1111
1134
|
"worklet";
|
|
@@ -3496,17 +3519,21 @@ function ToolPanelHost() {
|
|
|
3496
3519
|
var import_core27 = require("@pictor-sdk/core");
|
|
3497
3520
|
var import_react21 = require("react");
|
|
3498
3521
|
var import_react_native24 = require("react-native");
|
|
3522
|
+
var import_skia10 = require("@pictor-sdk/skia");
|
|
3499
3523
|
|
|
3500
3524
|
// src/composer/keyboardLayout.ts
|
|
3501
3525
|
var HIDDEN_KEYBOARD = { height: 0 };
|
|
3502
3526
|
var MAX_DURATION = 400;
|
|
3503
3527
|
var FALLBACK_DURATION = 180;
|
|
3504
|
-
function composerInsets(frame, safeAreaBottom = 0) {
|
|
3528
|
+
function composerInsets(frame, safeAreaBottom = 0, safeAreaTop = 0) {
|
|
3505
3529
|
const height = Number.isFinite(frame.height) ? Math.max(0, frame.height) : 0;
|
|
3506
3530
|
const visible = height > 0;
|
|
3507
3531
|
const raw = frame.duration;
|
|
3508
3532
|
const duration = raw === void 0 || !Number.isFinite(raw) || raw <= 0 ? FALLBACK_DURATION : Math.min(MAX_DURATION, raw);
|
|
3509
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,
|
|
3510
3537
|
bottom: visible ? height : Math.max(0, safeAreaBottom),
|
|
3511
3538
|
keyboardVisible: visible,
|
|
3512
3539
|
duration
|
|
@@ -3526,6 +3553,31 @@ function fitCanvasRect(container, document) {
|
|
|
3526
3553
|
top: (container.height - height) / 2
|
|
3527
3554
|
};
|
|
3528
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
|
+
}
|
|
3529
3581
|
|
|
3530
3582
|
// src/composer/useTextSession.ts
|
|
3531
3583
|
var import_core26 = require("@pictor-sdk/core");
|
|
@@ -3662,26 +3714,45 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3662
3714
|
const store = (0, import_core27.useEditorStore)();
|
|
3663
3715
|
const { document } = (0, import_core27.useEditor)();
|
|
3664
3716
|
const [container, setContainer] = (0, import_react21.useState)({ width: 0, height: 0 });
|
|
3717
|
+
const renderer = (0, import_skia10.useRenderer)();
|
|
3665
3718
|
const input = (0, import_react21.useRef)(null);
|
|
3666
3719
|
const [selectionRange, setSelectionRange] = (0, import_react21.useState)(
|
|
3667
3720
|
void 0
|
|
3668
3721
|
);
|
|
3669
3722
|
const data = session.data;
|
|
3670
3723
|
const span = session.span;
|
|
3671
|
-
const
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
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
|
+
);
|
|
3685
3756
|
const fonts = (0, import_react21.useMemo)(() => (0, import_core27.fontChoices)(store.registry.fonts), [store.registry.fonts]);
|
|
3686
3757
|
(0, import_react21.useEffect)(() => {
|
|
3687
3758
|
const handle = setTimeout(() => input.current?.focus(), 50);
|
|
@@ -3820,7 +3891,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
3820
3891
|
position: "absolute",
|
|
3821
3892
|
left: 0,
|
|
3822
3893
|
right: 0,
|
|
3823
|
-
top:
|
|
3894
|
+
top: insets.top,
|
|
3824
3895
|
height: 56,
|
|
3825
3896
|
flexDirection: "row",
|
|
3826
3897
|
alignItems: "center",
|
|
@@ -4011,7 +4082,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
|
|
|
4011
4082
|
);
|
|
4012
4083
|
}
|
|
4013
4084
|
function TextComposerHost({
|
|
4014
|
-
Composer = TextComposer
|
|
4085
|
+
Composer = TextComposer,
|
|
4086
|
+
safeArea
|
|
4015
4087
|
}) {
|
|
4016
4088
|
const { setActiveTool } = (0, import_core27.useEditor)();
|
|
4017
4089
|
const session = useTextSession();
|
|
@@ -4040,7 +4112,10 @@ function TextComposerHost({
|
|
|
4040
4112
|
hide.remove();
|
|
4041
4113
|
};
|
|
4042
4114
|
}, []);
|
|
4043
|
-
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
|
+
);
|
|
4044
4119
|
const finish = (0, import_react21.useCallback)(
|
|
4045
4120
|
(commit) => {
|
|
4046
4121
|
if (commit) session.commit();
|
|
@@ -4165,7 +4240,7 @@ function CanvasStage({ children, onError }) {
|
|
|
4165
4240
|
const { previews } = useInteractionState();
|
|
4166
4241
|
const surface = resolveGestureSurface(activeTool);
|
|
4167
4242
|
return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
|
|
4168
|
-
|
|
4243
|
+
import_skia11.EditorCanvas,
|
|
4169
4244
|
{
|
|
4170
4245
|
style: { flex: 1 },
|
|
4171
4246
|
onError,
|
|
@@ -4285,7 +4360,7 @@ function Editor({
|
|
|
4285
4360
|
|
|
4286
4361
|
// src/gestures/EditorGestures.tsx
|
|
4287
4362
|
var import_core32 = require("@pictor-sdk/core");
|
|
4288
|
-
var
|
|
4363
|
+
var import_skia12 = require("@pictor-sdk/skia");
|
|
4289
4364
|
var import_react23 = require("react");
|
|
4290
4365
|
var import_react_native28 = require("react-native");
|
|
4291
4366
|
var import_react_native_gesture_handler9 = require("react-native-gesture-handler");
|
|
@@ -4393,7 +4468,7 @@ function createGestureController(store, getMetrics, options = {}) {
|
|
|
4393
4468
|
var import_jsx_runtime31 = require("react/jsx-runtime");
|
|
4394
4469
|
function EditorGestures({ onSnap }) {
|
|
4395
4470
|
const store = (0, import_core32.useEditorStore)();
|
|
4396
|
-
const metrics = (0,
|
|
4471
|
+
const metrics = (0, import_skia12.useCanvasMetrics)();
|
|
4397
4472
|
const metricsRef = (0, import_react23.useRef)(metrics);
|
|
4398
4473
|
metricsRef.current = metrics;
|
|
4399
4474
|
const onSnapRef = (0, import_react23.useRef)(onSnap);
|