@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 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");
@@ -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 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) => {
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
- endIfIdle();
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 layout = (0, import_react21.useMemo)(() => {
3672
- const canvas = fitCanvasRect(container, document.size);
3673
- const fontSize = (span?.fontSize ?? 0.08) * canvas.width;
3674
- const centreY = canvas.top + (session.layer?.transform.y ?? 0.42) * canvas.height;
3675
- const height = canvas.height;
3676
- return {
3677
- fontSize: Math.max(1, fontSize),
3678
- lineHeight: Math.max(1, fontSize * (span?.lineHeight ?? 1.2)),
3679
- left: canvas.left,
3680
- width: Math.max(1, canvas.width),
3681
- top: Math.max(0, centreY - height / 2),
3682
- height: Math.max(1, height)
3683
- };
3684
- }, [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
+ );
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: 0,
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)(() => 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
+ );
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
- import_skia10.EditorCanvas,
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 import_skia11 = require("@pictor-sdk/skia");
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, import_skia11.useCanvasMetrics)();
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);