@pictor-sdk/ui 0.2.1 → 0.3.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 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. */
@@ -807,6 +827,22 @@ interface LayoutImageFit {
807
827
  declare function fitImageToLayoutCell(cell: Rect, documentSize: Size, intrinsic: Size, options?: LayoutCropOptions): LayoutImageFit;
808
828
  /** The transform patch that moves a layer into a cell, leaving its other fields alone. */
809
829
  declare function cellToTransformPatch(cell: LayoutCell): Partial<Transform>;
830
+ /**
831
+ * The box a photo draws into when it fills one cell, in document pixels.
832
+ *
833
+ * The cell fixes the box: `fitImageToLayoutCell` picks a crop whose aspect
834
+ * matches the cell, so zooming and panning inside it move the source window
835
+ * and leave the box alone. That is what makes a live crop expressible as a
836
+ * source-space override at all, and it is why a cell's bounds cannot drift
837
+ * while a gesture is running.
838
+ *
839
+ * Paired with `imageSourceTransform` from `@pictor-sdk/skia`, this is
840
+ * everything a host needs to draw an uncommitted crop from a worklet, using
841
+ * the renderer's own arithmetic rather than a second copy of it.
842
+ *
843
+ * @experimental
844
+ */
845
+ declare function layoutCellBox(cell: Rect, documentSize: Size): Size;
810
846
 
811
847
  /** The active brush: kind, colour and width. Width is a fraction of document width. */
812
848
  interface BrushConfig {
@@ -862,4 +898,4 @@ declare function UiStateProvider({ children }: {
862
898
  /** Read and write one key of the ephemeral UI state. Throws outside an `<Editor>`. */
863
899
  declare function useUiState<T>(key: string, fallback: T): [T, (value: T) => void];
864
900
 
865
- export { type AddPhotoController, type AddPhotoControllerOptions, AddPhotoPanel, AdjustPanel, type BrushConfig, CanvasStage, type CanvasStageProps, CropGestureSurface, type CropGestureSurfaceProps, CropGrid, CropPanel, DEFAULT_BRUSH, type DrawController, DrawPanel, DrawingSurface, Editor, EditorGestures, type EditorGesturesProps, type EditorProps, type EditorSlots, type EditorTheme, FilterStrip, type GestureController, type GestureControllerOptions, type GestureMetrics, type GestureSnapEvent, type GestureSurfaceKind, type HostCallbacks, HostCallbacksProvider, InteractionGuides, type InteractionGuidesProps, InteractionProvider, InvalidImageResultError, LayerGestureSurface, type LayerGestureSurfaceProps, LayersPanel, type LayoutCell, type LayoutCropOptions, type LayoutImageFit, LayoutPanel, type LayoutPreset, MediaPanel, type PartialTheme, STRAIGHTEN_LIMIT, Slider, type SliderProps, type StickerAsset, StickerPanel, StoryMediaGestureSurface, type StoryMediaGestureSurfaceProps, StraightenRuler, type StraightenRulerProps, TextComposer, TextComposerHost, type TextComposerProps, TextPanel, type TextSessionController, ThemeContext, ToolPanelHost, ToolRail, TopBar, type TopBarProps, UiStateProvider, type UiStateStore, type UseTextSessionOptions, canvasSizeForAspect, cellToTransformPatch, centeredCropForAspect, createAddPhotoController, createDrawController, createGestureController, createUiStateStore, defaultTheme, fitImageToLayoutCell, layoutCellBounds, layoutCells, mergeSlots, mergeTheme, radiansToStraighten, resolveGestureSurface, resolvePanel, straightenToRadians, useBaseImageLayer, useBaseMediaLayer, useHostCallbacks, useInteractionState, useTargetImageLayer, useTextSession, useTheme, useUiState };
901
+ export { type AddPhotoController, type AddPhotoControllerOptions, AddPhotoPanel, AdjustPanel, type BrushConfig, CanvasStage, type CanvasStageProps, CropGestureSurface, type CropGestureSurfaceProps, CropGrid, CropPanel, DEFAULT_BRUSH, type DrawController, DrawPanel, DrawingSurface, Editor, EditorGestures, type EditorGesturesProps, type EditorProps, type EditorSlots, type EditorTheme, FilterStrip, type GestureController, type GestureControllerOptions, type GestureMetrics, type GestureSnapEvent, type GestureSurfaceKind, type HostCallbacks, HostCallbacksProvider, InteractionGuides, type InteractionGuidesProps, InteractionProvider, InvalidImageResultError, LayerGestureSurface, type LayerGestureSurfaceProps, LayersPanel, type LayoutCell, type LayoutCropOptions, type LayoutImageFit, LayoutPanel, type LayoutPreset, MediaPanel, type PartialTheme, STRAIGHTEN_LIMIT, Slider, type SliderProps, type StickerAsset, StickerPanel, StoryMediaGestureSurface, type StoryMediaGestureSurfaceProps, StraightenRuler, type StraightenRulerProps, TextComposer, TextComposerHost, type TextComposerProps, TextPanel, type TextSessionController, ThemeContext, ToolPanelHost, ToolRail, TopBar, type TopBarProps, UiStateProvider, type UiStateStore, type UseTextSessionOptions, canvasSizeForAspect, cellToTransformPatch, centeredCropForAspect, createAddPhotoController, createDrawController, createGestureController, createUiStateStore, defaultTheme, fitImageToLayoutCell, layoutCellBounds, layoutCellBox, layoutCells, mergeSlots, mergeTheme, radiansToStraighten, resolveGestureSurface, resolvePanel, straightenToRadians, useBaseImageLayer, useBaseMediaLayer, useHostCallbacks, useInteractionState, useTargetImageLayer, useTextSession, useTheme, useUiState };
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. */
@@ -807,6 +827,22 @@ interface LayoutImageFit {
807
827
  declare function fitImageToLayoutCell(cell: Rect, documentSize: Size, intrinsic: Size, options?: LayoutCropOptions): LayoutImageFit;
808
828
  /** The transform patch that moves a layer into a cell, leaving its other fields alone. */
809
829
  declare function cellToTransformPatch(cell: LayoutCell): Partial<Transform>;
830
+ /**
831
+ * The box a photo draws into when it fills one cell, in document pixels.
832
+ *
833
+ * The cell fixes the box: `fitImageToLayoutCell` picks a crop whose aspect
834
+ * matches the cell, so zooming and panning inside it move the source window
835
+ * and leave the box alone. That is what makes a live crop expressible as a
836
+ * source-space override at all, and it is why a cell's bounds cannot drift
837
+ * while a gesture is running.
838
+ *
839
+ * Paired with `imageSourceTransform` from `@pictor-sdk/skia`, this is
840
+ * everything a host needs to draw an uncommitted crop from a worklet, using
841
+ * the renderer's own arithmetic rather than a second copy of it.
842
+ *
843
+ * @experimental
844
+ */
845
+ declare function layoutCellBox(cell: Rect, documentSize: Size): Size;
810
846
 
811
847
  /** The active brush: kind, colour and width. Width is a fraction of document width. */
812
848
  interface BrushConfig {
@@ -862,4 +898,4 @@ declare function UiStateProvider({ children }: {
862
898
  /** Read and write one key of the ephemeral UI state. Throws outside an `<Editor>`. */
863
899
  declare function useUiState<T>(key: string, fallback: T): [T, (value: T) => void];
864
900
 
865
- export { type AddPhotoController, type AddPhotoControllerOptions, AddPhotoPanel, AdjustPanel, type BrushConfig, CanvasStage, type CanvasStageProps, CropGestureSurface, type CropGestureSurfaceProps, CropGrid, CropPanel, DEFAULT_BRUSH, type DrawController, DrawPanel, DrawingSurface, Editor, EditorGestures, type EditorGesturesProps, type EditorProps, type EditorSlots, type EditorTheme, FilterStrip, type GestureController, type GestureControllerOptions, type GestureMetrics, type GestureSnapEvent, type GestureSurfaceKind, type HostCallbacks, HostCallbacksProvider, InteractionGuides, type InteractionGuidesProps, InteractionProvider, InvalidImageResultError, LayerGestureSurface, type LayerGestureSurfaceProps, LayersPanel, type LayoutCell, type LayoutCropOptions, type LayoutImageFit, LayoutPanel, type LayoutPreset, MediaPanel, type PartialTheme, STRAIGHTEN_LIMIT, Slider, type SliderProps, type StickerAsset, StickerPanel, StoryMediaGestureSurface, type StoryMediaGestureSurfaceProps, StraightenRuler, type StraightenRulerProps, TextComposer, TextComposerHost, type TextComposerProps, TextPanel, type TextSessionController, ThemeContext, ToolPanelHost, ToolRail, TopBar, type TopBarProps, UiStateProvider, type UiStateStore, type UseTextSessionOptions, canvasSizeForAspect, cellToTransformPatch, centeredCropForAspect, createAddPhotoController, createDrawController, createGestureController, createUiStateStore, defaultTheme, fitImageToLayoutCell, layoutCellBounds, layoutCells, mergeSlots, mergeTheme, radiansToStraighten, resolveGestureSurface, resolvePanel, straightenToRadians, useBaseImageLayer, useBaseMediaLayer, useHostCallbacks, useInteractionState, useTargetImageLayer, useTextSession, useTheme, useUiState };
901
+ export { type AddPhotoController, type AddPhotoControllerOptions, AddPhotoPanel, AdjustPanel, type BrushConfig, CanvasStage, type CanvasStageProps, CropGestureSurface, type CropGestureSurfaceProps, CropGrid, CropPanel, DEFAULT_BRUSH, type DrawController, DrawPanel, DrawingSurface, Editor, EditorGestures, type EditorGesturesProps, type EditorProps, type EditorSlots, type EditorTheme, FilterStrip, type GestureController, type GestureControllerOptions, type GestureMetrics, type GestureSnapEvent, type GestureSurfaceKind, type HostCallbacks, HostCallbacksProvider, InteractionGuides, type InteractionGuidesProps, InteractionProvider, InvalidImageResultError, LayerGestureSurface, type LayerGestureSurfaceProps, LayersPanel, type LayoutCell, type LayoutCropOptions, type LayoutImageFit, LayoutPanel, type LayoutPreset, MediaPanel, type PartialTheme, STRAIGHTEN_LIMIT, Slider, type SliderProps, type StickerAsset, StickerPanel, StoryMediaGestureSurface, type StoryMediaGestureSurfaceProps, StraightenRuler, type StraightenRulerProps, TextComposer, TextComposerHost, type TextComposerProps, TextPanel, type TextSessionController, ThemeContext, ToolPanelHost, ToolRail, TopBar, type TopBarProps, UiStateProvider, type UiStateStore, type UseTextSessionOptions, canvasSizeForAspect, cellToTransformPatch, centeredCropForAspect, createAddPhotoController, createDrawController, createGestureController, createUiStateStore, defaultTheme, fitImageToLayoutCell, layoutCellBounds, layoutCellBox, layoutCells, mergeSlots, mergeTheme, radiansToStraighten, resolveGestureSurface, resolvePanel, straightenToRadians, useBaseImageLayer, useBaseMediaLayer, useHostCallbacks, useInteractionState, useTargetImageLayer, useTextSession, useTheme, useUiState };
package/dist/index.js CHANGED
@@ -75,6 +75,7 @@ __export(index_exports, {
75
75
  defaultTheme: () => defaultTheme,
76
76
  fitImageToLayoutCell: () => fitImageToLayoutCell,
77
77
  layoutCellBounds: () => layoutCellBounds,
78
+ layoutCellBox: () => layoutCellBox,
78
79
  layoutCells: () => layoutCells,
79
80
  mergeSlots: () => mergeSlots,
80
81
  mergeTheme: () => mergeTheme,
@@ -95,7 +96,7 @@ module.exports = __toCommonJS(index_exports);
95
96
 
96
97
  // src/Editor.tsx
97
98
  var import_core30 = require("@pictor-sdk/core");
98
- var import_skia10 = require("@pictor-sdk/skia");
99
+ var import_skia11 = require("@pictor-sdk/skia");
99
100
  var import_react22 = __toESM(require("react"));
100
101
  var import_react_native27 = require("react-native");
101
102
  var import_react_native_gesture_handler8 = require("react-native-gesture-handler");
@@ -868,6 +869,7 @@ function LayerGestureSurface({ onSnap }) {
868
869
  const focalY = (0, import_react_native_reanimated4.useSharedValue)(0.5);
869
870
  const pointers = (0, import_react_native_reanimated4.useSharedValue)(0);
870
871
  const active = (0, import_react_native_reanimated4.useSharedValue)(false);
872
+ const livePointers = (0, import_react_native_reanimated4.useSharedValue)(0);
871
873
  const snapCX = (0, import_react_native_reanimated4.useSharedValue)(false);
872
874
  const snapCY = (0, import_react_native_reanimated4.useSharedValue)(false);
873
875
  const snapR = (0, import_react_native_reanimated4.useSharedValue)(false);
@@ -1042,7 +1044,23 @@ function LayerGestureSurface({ onSnap }) {
1042
1044
  deleted
1043
1045
  );
1044
1046
  };
1045
- const pan = import_react_native_gesture_handler4.Gesture.Pan().minPointers(1).maxPointers(2).onBegin((e) => {
1047
+ const releaseIfEmpty = () => {
1048
+ "worklet";
1049
+ if (livePointers.value > 0) return;
1050
+ endIfIdle();
1051
+ };
1052
+ const pan = import_react_native_gesture_handler4.Gesture.Pan().minPointers(1).maxPointers(2).onTouchesDown((e) => {
1053
+ "worklet";
1054
+ livePointers.value = e.allTouches.length;
1055
+ }).onTouchesUp((e) => {
1056
+ "worklet";
1057
+ livePointers.value = Math.max(0, e.allTouches.length - e.changedTouches.length);
1058
+ releaseIfEmpty();
1059
+ }).onTouchesCancelled(() => {
1060
+ "worklet";
1061
+ livePointers.value = 0;
1062
+ releaseIfEmpty();
1063
+ }).onBegin((e) => {
1046
1064
  "worklet";
1047
1065
  if (active.value) return;
1048
1066
  rawPanX.value = 0;
@@ -1089,7 +1107,7 @@ function LayerGestureSurface({ onSnap }) {
1089
1107
  recompute(false);
1090
1108
  }).onFinalize(() => {
1091
1109
  "worklet";
1092
- endIfIdle();
1110
+ releaseIfEmpty();
1093
1111
  });
1094
1112
  const pinch = import_react_native_gesture_handler4.Gesture.Pinch().onUpdate((e) => {
1095
1113
  "worklet";
@@ -1098,6 +1116,9 @@ function LayerGestureSurface({ onSnap }) {
1098
1116
  focalY.value = e.focalY / metricsHeight;
1099
1117
  syncPointers(e.numberOfPointers);
1100
1118
  recompute(false);
1119
+ }).onFinalize(() => {
1120
+ "worklet";
1121
+ releaseIfEmpty();
1101
1122
  });
1102
1123
  const rotate = import_react_native_gesture_handler4.Gesture.Rotation().enabled(config.rotationEnabled).onUpdate((e) => {
1103
1124
  "worklet";
@@ -1106,6 +1127,9 @@ function LayerGestureSurface({ onSnap }) {
1106
1127
  focalY.value = e.anchorY / metricsHeight;
1107
1128
  syncPointers(e.numberOfPointers);
1108
1129
  recompute(false);
1130
+ }).onFinalize(() => {
1131
+ "worklet";
1132
+ releaseIfEmpty();
1109
1133
  });
1110
1134
  const tap = import_react_native_gesture_handler4.Gesture.Tap().onEnd((e) => {
1111
1135
  "worklet";
@@ -2901,6 +2925,13 @@ function fitImageToLayoutCell(cell, documentSize, intrinsic, options = {}) {
2901
2925
  function cellToTransformPatch(cell) {
2902
2926
  return { x: cell.x, y: cell.y, scale: cell.scale, rotation: 0 };
2903
2927
  }
2928
+ function layoutCellBox(cell, documentSize) {
2929
+ "worklet";
2930
+ return {
2931
+ width: cell.width * documentSize.width,
2932
+ height: cell.height * documentSize.height
2933
+ };
2934
+ }
2904
2935
 
2905
2936
  // src/panels/LayoutPanel.tsx
2906
2937
  var import_jsx_runtime19 = require("react/jsx-runtime");
@@ -3496,17 +3527,21 @@ function ToolPanelHost() {
3496
3527
  var import_core27 = require("@pictor-sdk/core");
3497
3528
  var import_react21 = require("react");
3498
3529
  var import_react_native24 = require("react-native");
3530
+ var import_skia10 = require("@pictor-sdk/skia");
3499
3531
 
3500
3532
  // src/composer/keyboardLayout.ts
3501
3533
  var HIDDEN_KEYBOARD = { height: 0 };
3502
3534
  var MAX_DURATION = 400;
3503
3535
  var FALLBACK_DURATION = 180;
3504
- function composerInsets(frame, safeAreaBottom = 0) {
3536
+ function composerInsets(frame, safeAreaBottom = 0, safeAreaTop = 0) {
3505
3537
  const height = Number.isFinite(frame.height) ? Math.max(0, frame.height) : 0;
3506
3538
  const visible = height > 0;
3507
3539
  const raw = frame.duration;
3508
3540
  const duration = raw === void 0 || !Number.isFinite(raw) || raw <= 0 ? FALLBACK_DURATION : Math.min(MAX_DURATION, raw);
3509
3541
  return {
3542
+ // The keyboard never covers the top, so this one is the safe area and
3543
+ // nothing else.
3544
+ top: Number.isFinite(safeAreaTop) ? Math.max(0, safeAreaTop) : 0,
3510
3545
  bottom: visible ? height : Math.max(0, safeAreaBottom),
3511
3546
  keyboardVisible: visible,
3512
3547
  duration
@@ -3526,6 +3561,31 @@ function fitCanvasRect(container, document) {
3526
3561
  top: (container.height - height) / 2
3527
3562
  };
3528
3563
  }
3564
+ function composerInputBox(request) {
3565
+ const canvas = fitCanvasRect(request.container, request.document);
3566
+ const fontSize = Math.max(1, request.fontSize * canvas.width);
3567
+ const lineHeight = Math.max(1, fontSize * request.lineHeight);
3568
+ const scale = request.document.width > 0 ? canvas.width / request.document.width : 0;
3569
+ const measuredHeight = request.measured && request.measured.height > 0 ? request.measured.height * scale : 0;
3570
+ const estimated = lineHeight * Math.max(1, request.lines);
3571
+ const height = Math.min(
3572
+ Math.max(lineHeight, measuredHeight > 0 ? measuredHeight : estimated),
3573
+ Math.max(lineHeight, canvas.height)
3574
+ );
3575
+ const centre = canvas.top + request.centerY * canvas.height;
3576
+ const top = Math.min(
3577
+ Math.max(0, centre - height / 2),
3578
+ Math.max(0, request.container.height - height)
3579
+ );
3580
+ return {
3581
+ left: canvas.left,
3582
+ width: Math.max(1, canvas.width),
3583
+ top,
3584
+ height: Math.max(1, height),
3585
+ fontSize,
3586
+ lineHeight
3587
+ };
3588
+ }
3529
3589
 
3530
3590
  // src/composer/useTextSession.ts
3531
3591
  var import_core26 = require("@pictor-sdk/core");
@@ -3662,26 +3722,45 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3662
3722
  const store = (0, import_core27.useEditorStore)();
3663
3723
  const { document } = (0, import_core27.useEditor)();
3664
3724
  const [container, setContainer] = (0, import_react21.useState)({ width: 0, height: 0 });
3725
+ const renderer = (0, import_skia10.useRenderer)();
3665
3726
  const input = (0, import_react21.useRef)(null);
3666
3727
  const [selectionRange, setSelectionRange] = (0, import_react21.useState)(
3667
3728
  void 0
3668
3729
  );
3669
3730
  const data = session.data;
3670
3731
  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]);
3732
+ const layerId = session.layer?.id;
3733
+ const [measured, setMeasured] = (0, import_react21.useState)(null);
3734
+ (0, import_react21.useEffect)(() => {
3735
+ if (!layerId) {
3736
+ setMeasured(null);
3737
+ return;
3738
+ }
3739
+ const refresh = () => setMeasured(renderer.textMeasures.get(layerId) ?? null);
3740
+ const unsubscribe = renderer.textMeasures.subscribe(refresh);
3741
+ refresh();
3742
+ return unsubscribe;
3743
+ }, [renderer, layerId]);
3744
+ const layout = (0, import_react21.useMemo)(
3745
+ () => composerInputBox({
3746
+ container,
3747
+ document: document.size,
3748
+ fontSize: span?.fontSize ?? 0.08,
3749
+ lineHeight: span?.lineHeight ?? 1.2,
3750
+ centerY: session.layer?.transform.y ?? 0.42,
3751
+ measured,
3752
+ lines: session.value.split("\n").length
3753
+ }),
3754
+ [
3755
+ container,
3756
+ document.size,
3757
+ span?.fontSize,
3758
+ span?.lineHeight,
3759
+ session.layer?.transform.y,
3760
+ session.value,
3761
+ measured
3762
+ ]
3763
+ );
3685
3764
  const fonts = (0, import_react21.useMemo)(() => (0, import_core27.fontChoices)(store.registry.fonts), [store.registry.fonts]);
3686
3765
  (0, import_react21.useEffect)(() => {
3687
3766
  const handle = setTimeout(() => input.current?.focus(), 50);
@@ -3820,7 +3899,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3820
3899
  position: "absolute",
3821
3900
  left: 0,
3822
3901
  right: 0,
3823
- top: 0,
3902
+ top: insets.top,
3824
3903
  height: 56,
3825
3904
  flexDirection: "row",
3826
3905
  alignItems: "center",
@@ -4011,7 +4090,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
4011
4090
  );
4012
4091
  }
4013
4092
  function TextComposerHost({
4014
- Composer = TextComposer
4093
+ Composer = TextComposer,
4094
+ safeArea
4015
4095
  }) {
4016
4096
  const { setActiveTool } = (0, import_core27.useEditor)();
4017
4097
  const session = useTextSession();
@@ -4040,7 +4120,10 @@ function TextComposerHost({
4040
4120
  hide.remove();
4041
4121
  };
4042
4122
  }, []);
4043
- const insets = (0, import_react21.useMemo)(() => composerInsets(frame), [frame]);
4123
+ const insets = (0, import_react21.useMemo)(
4124
+ () => composerInsets(frame, safeArea?.bottom ?? 0, safeArea?.top ?? 0),
4125
+ [frame, safeArea?.bottom, safeArea?.top]
4126
+ );
4044
4127
  const finish = (0, import_react21.useCallback)(
4045
4128
  (commit) => {
4046
4129
  if (commit) session.commit();
@@ -4165,7 +4248,7 @@ function CanvasStage({ children, onError }) {
4165
4248
  const { previews } = useInteractionState();
4166
4249
  const surface = resolveGestureSurface(activeTool);
4167
4250
  return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
4168
- import_skia10.EditorCanvas,
4251
+ import_skia11.EditorCanvas,
4169
4252
  {
4170
4253
  style: { flex: 1 },
4171
4254
  onError,
@@ -4285,7 +4368,7 @@ function Editor({
4285
4368
 
4286
4369
  // src/gestures/EditorGestures.tsx
4287
4370
  var import_core32 = require("@pictor-sdk/core");
4288
- var import_skia11 = require("@pictor-sdk/skia");
4371
+ var import_skia12 = require("@pictor-sdk/skia");
4289
4372
  var import_react23 = require("react");
4290
4373
  var import_react_native28 = require("react-native");
4291
4374
  var import_react_native_gesture_handler9 = require("react-native-gesture-handler");
@@ -4393,7 +4476,7 @@ function createGestureController(store, getMetrics, options = {}) {
4393
4476
  var import_jsx_runtime31 = require("react/jsx-runtime");
4394
4477
  function EditorGestures({ onSnap }) {
4395
4478
  const store = (0, import_core32.useEditorStore)();
4396
- const metrics = (0, import_skia11.useCanvasMetrics)();
4479
+ const metrics = (0, import_skia12.useCanvasMetrics)();
4397
4480
  const metricsRef = (0, import_react23.useRef)(metrics);
4398
4481
  metricsRef.current = metrics;
4399
4482
  const onSnapRef = (0, import_react23.useRef)(onSnap);
@@ -4645,6 +4728,7 @@ function TextPanel() {
4645
4728
  defaultTheme,
4646
4729
  fitImageToLayoutCell,
4647
4730
  layoutCellBounds,
4731
+ layoutCellBox,
4648
4732
  layoutCells,
4649
4733
  mergeSlots,
4650
4734
  mergeTheme,