@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 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.0
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 import_skia10 = require("@pictor-sdk/skia");
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
- () => renderer.textMeasures.subscribe(() => setMeasureVersion((v) => v + 1)),
815
- [renderer]
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 pan = import_react_native_gesture_handler4.Gesture.Pan().minPointers(1).maxPointers(2).onBegin((e) => {
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
- endIfIdle();
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 layout = (0, import_react21.useMemo)(() => {
3670
- const canvas = fitCanvasRect(container, document.size);
3671
- const fontSize = (span?.fontSize ?? 0.08) * canvas.width;
3672
- const centreY = canvas.top + (session.layer?.transform.y ?? 0.42) * canvas.height;
3673
- const height = canvas.height;
3674
- return {
3675
- fontSize: Math.max(1, fontSize),
3676
- lineHeight: Math.max(1, fontSize * (span?.lineHeight ?? 1.2)),
3677
- left: canvas.left,
3678
- width: Math.max(1, canvas.width),
3679
- top: Math.max(0, centreY - height / 2),
3680
- height: Math.max(1, height)
3681
- };
3682
- }, [container, document.size, span?.fontSize, span?.lineHeight, session.layer?.transform.y]);
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: 0,
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)(() => composerInsets(frame), [frame]);
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
- import_skia10.EditorCanvas,
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 import_skia11 = require("@pictor-sdk/skia");
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, import_skia11.useCanvasMetrics)();
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);