@pictor-sdk/ui 0.3.0 → 0.4.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
@@ -87,6 +87,14 @@ interface ComposerInsets {
87
87
  /** Animation duration to match the platform's own, clamped to something sane. */
88
88
  duration: number;
89
89
  }
90
+ interface CanvasRect {
91
+ /** Size of the fitted canvas inside the container. */
92
+ width: number;
93
+ height: number;
94
+ /** Offset of the canvas inside the container, which centres it. */
95
+ left: number;
96
+ top: number;
97
+ }
90
98
 
91
99
  /**
92
100
  * The state and commands behind a text-editing session.
@@ -167,6 +175,11 @@ interface TextComposerProps {
167
175
  onDone(): void;
168
176
  /** Abandon, restoring the document. */
169
177
  onCancel(): void;
178
+ /**
179
+ * The canvas rectangle in this composer's coordinates, when the host draws a
180
+ * cover canvas. Omitted, the composer assumes a centred contain fit.
181
+ */
182
+ canvas?: CanvasRect;
170
183
  }
171
184
  /**
172
185
  * The full-screen Story text composer.
@@ -176,20 +189,22 @@ interface TextComposerProps {
176
189
  * `renderDocument` — so what is on screen while composing is what exports.
177
190
  *
178
191
  * The `TextInput` itself is invisible and stretched over the canvas: it owns
179
- * the caret, the platform selection handles and the IME, while the *visible*
180
- * text is the real Skia layer underneath. That is what keeps preview and export
181
- * identical without giving up native text editing.
192
+ * focus and the IME, while the *visible* text is the real Skia layer
193
+ * underneath. On Android the platform paints composing glyphs black regardless
194
+ * of the input's colour, so the input is fully hidden and a caret is drawn at
195
+ * the measured selection instead. That is what keeps preview and export
196
+ * identical without giving up text editing.
182
197
  *
183
198
  * @experimental
184
199
  */
185
- declare function TextComposer({ session, insets, onDone, onCancel }: TextComposerProps): React__default.JSX.Element;
200
+ declare function TextComposer({ session, insets, onDone, onCancel, canvas }: TextComposerProps): React__default.JSX.Element;
186
201
  /**
187
202
  * Mounts a text session and the composer for it, and keeps the keyboard inset
188
203
  * up to date. This is what `<Editor>` renders while the text tool is active.
189
204
  *
190
205
  * @experimental
191
206
  */
192
- declare function TextComposerHost({ Composer, safeArea, }: {
207
+ declare function TextComposerHost({ Composer, safeArea, canvas, }: {
193
208
  /** Replace the default composer without replacing the canvas or editor. */
194
209
  Composer?: React__default.ComponentType<TextComposerProps>;
195
210
  /**
@@ -203,6 +218,8 @@ declare function TextComposerHost({ Composer, safeArea, }: {
203
218
  top?: number;
204
219
  bottom?: number;
205
220
  };
221
+ /** The canvas rectangle, for a host that draws a cover canvas. */
222
+ canvas?: CanvasRect;
206
223
  }): React__default.JSX.Element;
207
224
 
208
225
  /** What the top bar slot receives: the transaction actions and undo state. */
@@ -605,6 +622,12 @@ declare function EditorGestures({ onSnap }: EditorGesturesProps): React__default
605
622
  interface LayerGestureSurfaceProps {
606
623
  /** Fired once per snap engagement — hook up haptics here if you want them. */
607
624
  onSnap?: (event: GestureSnapEvent) => void;
625
+ /**
626
+ * Fired when a tap lands on a layer that was already selected when the
627
+ * gesture began. Hosts use this for a second-tap action such as opening the
628
+ * text composer; the SDK itself stays unaware of what a layer means.
629
+ */
630
+ onSelectedLayerTap?: (layerId: string) => void;
608
631
  }
609
632
  /**
610
633
  * Direct manipulation of overlay layers: grab the top-most layer under the
@@ -622,7 +645,7 @@ interface LayerGestureSurfaceProps {
622
645
  * - **One history entry.** The document is written exactly once, when the
623
646
  * gesture settles, and not at all if nothing actually moved.
624
647
  */
625
- declare function LayerGestureSurface({ onSnap }: LayerGestureSurfaceProps): React__default.JSX.Element;
648
+ declare function LayerGestureSurface({ onSnap, onSelectedLayerTap }: LayerGestureSurfaceProps): React__default.JSX.Element;
626
649
 
627
650
  /** The centre and rotation guides shown while a layer snaps. */
628
651
  interface InteractionGuidesProps {
package/dist/index.d.ts CHANGED
@@ -87,6 +87,14 @@ interface ComposerInsets {
87
87
  /** Animation duration to match the platform's own, clamped to something sane. */
88
88
  duration: number;
89
89
  }
90
+ interface CanvasRect {
91
+ /** Size of the fitted canvas inside the container. */
92
+ width: number;
93
+ height: number;
94
+ /** Offset of the canvas inside the container, which centres it. */
95
+ left: number;
96
+ top: number;
97
+ }
90
98
 
91
99
  /**
92
100
  * The state and commands behind a text-editing session.
@@ -167,6 +175,11 @@ interface TextComposerProps {
167
175
  onDone(): void;
168
176
  /** Abandon, restoring the document. */
169
177
  onCancel(): void;
178
+ /**
179
+ * The canvas rectangle in this composer's coordinates, when the host draws a
180
+ * cover canvas. Omitted, the composer assumes a centred contain fit.
181
+ */
182
+ canvas?: CanvasRect;
170
183
  }
171
184
  /**
172
185
  * The full-screen Story text composer.
@@ -176,20 +189,22 @@ interface TextComposerProps {
176
189
  * `renderDocument` — so what is on screen while composing is what exports.
177
190
  *
178
191
  * The `TextInput` itself is invisible and stretched over the canvas: it owns
179
- * the caret, the platform selection handles and the IME, while the *visible*
180
- * text is the real Skia layer underneath. That is what keeps preview and export
181
- * identical without giving up native text editing.
192
+ * focus and the IME, while the *visible* text is the real Skia layer
193
+ * underneath. On Android the platform paints composing glyphs black regardless
194
+ * of the input's colour, so the input is fully hidden and a caret is drawn at
195
+ * the measured selection instead. That is what keeps preview and export
196
+ * identical without giving up text editing.
182
197
  *
183
198
  * @experimental
184
199
  */
185
- declare function TextComposer({ session, insets, onDone, onCancel }: TextComposerProps): React__default.JSX.Element;
200
+ declare function TextComposer({ session, insets, onDone, onCancel, canvas }: TextComposerProps): React__default.JSX.Element;
186
201
  /**
187
202
  * Mounts a text session and the composer for it, and keeps the keyboard inset
188
203
  * up to date. This is what `<Editor>` renders while the text tool is active.
189
204
  *
190
205
  * @experimental
191
206
  */
192
- declare function TextComposerHost({ Composer, safeArea, }: {
207
+ declare function TextComposerHost({ Composer, safeArea, canvas, }: {
193
208
  /** Replace the default composer without replacing the canvas or editor. */
194
209
  Composer?: React__default.ComponentType<TextComposerProps>;
195
210
  /**
@@ -203,6 +218,8 @@ declare function TextComposerHost({ Composer, safeArea, }: {
203
218
  top?: number;
204
219
  bottom?: number;
205
220
  };
221
+ /** The canvas rectangle, for a host that draws a cover canvas. */
222
+ canvas?: CanvasRect;
206
223
  }): React__default.JSX.Element;
207
224
 
208
225
  /** What the top bar slot receives: the transaction actions and undo state. */
@@ -605,6 +622,12 @@ declare function EditorGestures({ onSnap }: EditorGesturesProps): React__default
605
622
  interface LayerGestureSurfaceProps {
606
623
  /** Fired once per snap engagement — hook up haptics here if you want them. */
607
624
  onSnap?: (event: GestureSnapEvent) => void;
625
+ /**
626
+ * Fired when a tap lands on a layer that was already selected when the
627
+ * gesture began. Hosts use this for a second-tap action such as opening the
628
+ * text composer; the SDK itself stays unaware of what a layer means.
629
+ */
630
+ onSelectedLayerTap?: (layerId: string) => void;
608
631
  }
609
632
  /**
610
633
  * Direct manipulation of overlay layers: grab the top-most layer under the
@@ -622,7 +645,7 @@ interface LayerGestureSurfaceProps {
622
645
  * - **One history entry.** The document is written exactly once, when the
623
646
  * gesture settles, and not at all if nothing actually moved.
624
647
  */
625
- declare function LayerGestureSurface({ onSnap }: LayerGestureSurfaceProps): React__default.JSX.Element;
648
+ declare function LayerGestureSurface({ onSnap, onSelectedLayerTap }: LayerGestureSurfaceProps): React__default.JSX.Element;
626
649
 
627
650
  /** The centre and rotation guides shown while a layer snaps. */
628
651
  interface InteractionGuidesProps {
package/dist/index.js CHANGED
@@ -793,10 +793,16 @@ function hitTestLayerCandidatesUI(x, y, candidates, documentWidth, documentHeigh
793
793
  return null;
794
794
  }
795
795
 
796
+ // src/gestures/selectionTap.ts
797
+ function selectedLayerTapId(hit, wasSelectedAtStart) {
798
+ if (!hit || hit.locked || !wasSelectedAtStart) return null;
799
+ return hit.id;
800
+ }
801
+
796
802
  // src/gestures/LayerGestureSurface.tsx
797
803
  var import_jsx_runtime8 = require("react/jsx-runtime");
798
- function LayerGestureSurface({ onSnap }) {
799
- const { document, dispatch, profile } = (0, import_core8.useEditor)();
804
+ function LayerGestureSurface({ onSnap, onSelectedLayerTap }) {
805
+ const { document, dispatch, profile, selection } = (0, import_core8.useEditor)();
800
806
  const store = (0, import_core8.useEditorStore)();
801
807
  const metrics = (0, import_skia4.useCanvasMetrics)();
802
808
  const { setPreview, setLayerDragActive, setOverDeleteTarget, setSnap } = useInteractionState();
@@ -887,10 +893,11 @@ function LayerGestureSurface({ onSnap }) {
887
893
  document.size.height
888
894
  );
889
895
  }, [document.size.width, document.size.height]);
890
- const latest = (0, import_react9.useRef)({ document, metrics, store, dispatch, onSnap, profile });
891
- latest.current = { document, metrics, store, dispatch, onSnap, profile };
896
+ const latest = (0, import_react9.useRef)({ document, metrics, store, dispatch, onSnap, profile, selection, onSelectedLayerTap });
897
+ latest.current = { document, metrics, store, dispatch, onSnap, profile, selection, onSelectedLayerTap };
892
898
  const grabbedId = (0, import_react9.useRef)(null);
893
899
  const grabbedFrom = (0, import_react9.useRef)(null);
900
+ const tapWasSelected = (0, import_react9.useRef)(false);
894
901
  const snapState = (0, import_react9.useRef)(import_core8.NO_SNAP);
895
902
  const onGrabLayer = (0, import_react9.useCallback)((id) => {
896
903
  const { document: doc, store: s } = latest.current;
@@ -903,9 +910,23 @@ function LayerGestureSurface({ onSnap }) {
903
910
  setPreview({ layerId: hit.id, transform: previewTransform });
904
911
  setLayerDragActive(true);
905
912
  }, [setPreview, setLayerDragActive, previewTransform]);
913
+ const snapshotTapAt = (0, import_react9.useCallback)((xDp, yDp) => {
914
+ const { document: doc, metrics: m, store: s, selection: selected } = latest.current;
915
+ if (!m) return;
916
+ const layerInteraction = latest.current.profile.interaction?.layers;
917
+ const hit = (0, import_core8.hitTestDocument)(
918
+ doc,
919
+ { x: xDp / m.width, y: yDp / m.height },
920
+ s.registry,
921
+ (layer, index) => (0, import_core8.isDirectManipulationLayer)(layer, index, layerInteraction)
922
+ );
923
+ tapWasSelected.current = Boolean(hit && selected.includes(hit.id));
924
+ }, []);
906
925
  const onSelectAt = (0, import_react9.useCallback)((xDp, yDp) => {
907
926
  const { document: doc, metrics: m, store: s } = latest.current;
908
927
  if (!m) return;
928
+ const wasSelected = tapWasSelected.current;
929
+ tapWasSelected.current = false;
909
930
  const layerInteraction = latest.current.profile.interaction?.layers;
910
931
  const hit = (0, import_core8.hitTestDocument)(
911
932
  doc,
@@ -914,6 +935,8 @@ function LayerGestureSurface({ onSnap }) {
914
935
  (layer, index) => (0, import_core8.isDirectManipulationLayer)(layer, index, layerInteraction)
915
936
  );
916
937
  s.select(hit && !hit.locked ? [hit.id] : []);
938
+ const editId = selectedLayerTapId(hit ?? null, wasSelected);
939
+ if (editId) latest.current.onSelectedLayerTap?.(editId);
917
940
  }, []);
918
941
  const onSnapChanged = (0, import_react9.useCallback)(
919
942
  (centerX, centerY, rotation) => {
@@ -1052,6 +1075,8 @@ function LayerGestureSurface({ onSnap }) {
1052
1075
  const pan = import_react_native_gesture_handler4.Gesture.Pan().minPointers(1).maxPointers(2).onTouchesDown((e) => {
1053
1076
  "worklet";
1054
1077
  livePointers.value = e.allTouches.length;
1078
+ const touch = e.changedTouches[0] ?? e.allTouches[0];
1079
+ if (touch) (0, import_react_native_worklets4.runOnJS)(snapshotTapAt)(touch.x, touch.y);
1055
1080
  }).onTouchesUp((e) => {
1056
1081
  "worklet";
1057
1082
  livePointers.value = Math.max(0, e.allTouches.length - e.changedTouches.length);
@@ -1142,6 +1167,7 @@ function LayerGestureSurface({ onSnap }) {
1142
1167
  deleteTarget,
1143
1168
  aspect,
1144
1169
  onGrabLayer,
1170
+ snapshotTapAt,
1145
1171
  onSelectAt,
1146
1172
  onSnapChanged,
1147
1173
  onHoverChanged,
@@ -1819,8 +1845,9 @@ function Slider({
1819
1845
  const valueRef = (0, import_react12.useRef)(value);
1820
1846
  valueRef.current = value;
1821
1847
  const onLayout = (e) => {
1822
- setWidth(e.nativeEvent.layout.width);
1823
- widthRef.current = e.nativeEvent.layout.width;
1848
+ const next = e.nativeEvent.layout.width;
1849
+ widthRef.current = next;
1850
+ setWidth((previous) => previous === next ? previous : next);
1824
1851
  };
1825
1852
  const handlers = (0, import_react12.useMemo)(() => {
1826
1853
  const begin = () => {
@@ -3562,7 +3589,8 @@ function fitCanvasRect(container, document) {
3562
3589
  };
3563
3590
  }
3564
3591
  function composerInputBox(request) {
3565
- const canvas = fitCanvasRect(request.container, request.document);
3592
+ const provided = request.canvas;
3593
+ const canvas = provided && provided.width > 0 && provided.height > 0 ? provided : fitCanvasRect(request.container, request.document);
3566
3594
  const fontSize = Math.max(1, request.fontSize * canvas.width);
3567
3595
  const lineHeight = Math.max(1, fontSize * request.lineHeight);
3568
3596
  const scale = request.document.width > 0 ? canvas.width / request.document.width : 0;
@@ -3586,6 +3614,35 @@ function composerInputBox(request) {
3586
3614
  lineHeight
3587
3615
  };
3588
3616
  }
3617
+ function sameCaretLine(previous, next) {
3618
+ return previous === next || previous !== null && next !== null && previous.x === next.x && previous.y === next.y && previous.width === next.width && previous.height === next.height;
3619
+ }
3620
+ var CARET_WIDTH = 2;
3621
+ function composerInputOpacity(platform) {
3622
+ return platform === "android" ? 0 : 1;
3623
+ }
3624
+ function composerCaretBox(box, line) {
3625
+ if (!line || !Number.isFinite(line.x) || !Number.isFinite(line.y) || !Number.isFinite(line.width) || !Number.isFinite(line.height)) {
3626
+ return null;
3627
+ }
3628
+ const height = Math.max(1, line.height || box.lineHeight);
3629
+ return {
3630
+ left: box.left + line.x + line.width - CARET_WIDTH / 2,
3631
+ top: box.top + line.y,
3632
+ width: CARET_WIDTH,
3633
+ height
3634
+ };
3635
+ }
3636
+
3637
+ // src/composer/tapGesture.ts
3638
+ var TAP_SLOP = 8;
3639
+ function pressMoved(start, end, slop = TAP_SLOP) {
3640
+ if (!start || !end) return false;
3641
+ if (!Number.isFinite(start.x) || !Number.isFinite(start.y) || !Number.isFinite(end.x) || !Number.isFinite(end.y)) {
3642
+ return false;
3643
+ }
3644
+ return Math.hypot(end.x - start.x, end.y - start.y) > Math.max(0, slop);
3645
+ }
3589
3646
 
3590
3647
  // src/composer/useTextSession.ts
3591
3648
  var import_core26 = require("@pictor-sdk/core");
@@ -3717,7 +3774,7 @@ var SHADOWS = [
3717
3774
  { color: "#000000", blur: 0.012, dx: 2e-3, dy: 4e-3 },
3718
3775
  { color: "#000000", blur: 2e-3, dx: 6e-3, dy: 8e-3 }
3719
3776
  ];
3720
- function TextComposer({ session, insets, onDone, onCancel }) {
3777
+ function TextComposer({ session, insets, onDone, onCancel, canvas }) {
3721
3778
  const theme = useTheme();
3722
3779
  const store = (0, import_core27.useEditorStore)();
3723
3780
  const { document } = (0, import_core27.useEditor)();
@@ -3727,6 +3784,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3727
3784
  const [selectionRange, setSelectionRange] = (0, import_react21.useState)(
3728
3785
  void 0
3729
3786
  );
3787
+ const [focused, setFocused] = (0, import_react21.useState)(false);
3788
+ const [caretLine, setCaretLine] = (0, import_react21.useState)(null);
3730
3789
  const data = session.data;
3731
3790
  const span = session.span;
3732
3791
  const layerId = session.layer?.id;
@@ -3736,7 +3795,12 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3736
3795
  setMeasured(null);
3737
3796
  return;
3738
3797
  }
3739
- const refresh = () => setMeasured(renderer.textMeasures.get(layerId) ?? null);
3798
+ const refresh = () => {
3799
+ const next = renderer.textMeasures.get(layerId) ?? null;
3800
+ setMeasured(
3801
+ (previous) => previous && next && previous.width === next.width && previous.height === next.height ? previous : next
3802
+ );
3803
+ };
3740
3804
  const unsubscribe = renderer.textMeasures.subscribe(refresh);
3741
3805
  refresh();
3742
3806
  return unsubscribe;
@@ -3749,7 +3813,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3749
3813
  lineHeight: span?.lineHeight ?? 1.2,
3750
3814
  centerY: session.layer?.transform.y ?? 0.42,
3751
3815
  measured,
3752
- lines: session.value.split("\n").length
3816
+ lines: session.value.split("\n").length,
3817
+ ...canvas ? { canvas } : {}
3753
3818
  }),
3754
3819
  [
3755
3820
  container,
@@ -3758,20 +3823,41 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3758
3823
  span?.lineHeight,
3759
3824
  session.layer?.transform.y,
3760
3825
  session.value,
3761
- measured
3826
+ measured,
3827
+ canvas
3762
3828
  ]
3763
3829
  );
3764
3830
  const fonts = (0, import_react21.useMemo)(() => (0, import_core27.fontChoices)(store.registry.fonts), [store.registry.fonts]);
3765
3831
  (0, import_react21.useEffect)(() => {
3766
- const handle = setTimeout(() => input.current?.focus(), 50);
3832
+ const handle = setTimeout(() => {
3833
+ input.current?.focus();
3834
+ if (session.kind === "existing" && session.value.length > 0) {
3835
+ const end = session.value.length;
3836
+ setSelectionRange({ start: end, end });
3837
+ input.current?.setNativeProps({ selection: { start: end, end } });
3838
+ }
3839
+ }, 50);
3767
3840
  return () => clearTimeout(handle);
3768
3841
  }, []);
3769
3842
  const onSelectionChange = (0, import_react21.useCallback)(
3770
3843
  (event) => {
3771
- setSelectionRange(event.nativeEvent.selection);
3844
+ const next = event.nativeEvent.selection;
3845
+ setSelectionRange(
3846
+ (previous) => previous && previous.start === next.start && previous.end === next.end ? previous : next
3847
+ );
3772
3848
  },
3773
3849
  []
3774
3850
  );
3851
+ const caretAt = selectionRange?.end ?? session.value.length;
3852
+ const caretSource = session.value.slice(0, Math.max(0, caretAt));
3853
+ const onCaretLayout = (0, import_react21.useCallback)((event) => {
3854
+ const lines = event.nativeEvent.lines;
3855
+ const last = lines[lines.length - 1];
3856
+ const next = last ? { x: last.x, y: last.y, width: last.width, height: last.height } : null;
3857
+ setCaretLine((previous) => sameCaretLine(previous, next) ? previous : next);
3858
+ }, []);
3859
+ const caret = composerCaretBox(layout, caretLine);
3860
+ const letterSpacing = span?.letterSpacing && span.fontSize > 0 ? span.letterSpacing * layout.fontSize / span.fontSize : void 0;
3775
3861
  const keepFocus = (0, import_react21.useCallback)(
3776
3862
  (apply) => {
3777
3863
  apply();
@@ -3832,6 +3918,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3832
3918
  });
3833
3919
  return () => subscription.remove();
3834
3920
  }, [onCancel, insets.keyboardVisible]);
3921
+ const tapStart = (0, import_react21.useRef)(null);
3835
3922
  const controlRow = {
3836
3923
  flexDirection: "row",
3837
3924
  alignItems: "center",
@@ -3856,6 +3943,54 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3856
3943
  style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0, backgroundColor: "#00000055" }
3857
3944
  }
3858
3945
  ),
3946
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3947
+ import_react_native24.Pressable,
3948
+ {
3949
+ accessible: true,
3950
+ collapsable: false,
3951
+ accessibilityRole: "button",
3952
+ accessibilityLabel: "Finish editing text",
3953
+ onAccessibilityTap: onDone,
3954
+ onPressIn: (event) => {
3955
+ tapStart.current = {
3956
+ x: event.nativeEvent.pageX,
3957
+ y: event.nativeEvent.pageY
3958
+ };
3959
+ },
3960
+ onPress: (event) => {
3961
+ const start = tapStart.current;
3962
+ const end = { x: event.nativeEvent.pageX, y: event.nativeEvent.pageY };
3963
+ if (!pressMoved(start, end)) onDone();
3964
+ },
3965
+ style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }
3966
+ }
3967
+ ),
3968
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3969
+ import_react_native24.Text,
3970
+ {
3971
+ accessible: false,
3972
+ importantForAccessibility: "no-hide-descendants",
3973
+ pointerEvents: "none",
3974
+ onTextLayout: onCaretLayout,
3975
+ style: {
3976
+ position: "absolute",
3977
+ left: layout.left,
3978
+ width: layout.width,
3979
+ top: layout.top,
3980
+ height: layout.height,
3981
+ opacity: 0,
3982
+ fontSize: layout.fontSize,
3983
+ lineHeight: layout.lineHeight,
3984
+ fontFamily: span?.fontFamily,
3985
+ fontWeight: span?.fontWeight ? `${span.fontWeight}` : void 0,
3986
+ letterSpacing,
3987
+ textAlign: data?.align ?? "center",
3988
+ padding: 0
3989
+ },
3990
+ children: `${caretSource}\u200B`
3991
+ },
3992
+ `${caretSource}:${caretAt}`
3993
+ ),
3859
3994
  /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3860
3995
  import_react_native24.TextInput,
3861
3996
  {
@@ -3863,6 +3998,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3863
3998
  value: session.value,
3864
3999
  onChangeText: session.setValue,
3865
4000
  onSelectionChange,
4001
+ onFocus: () => setFocused(true),
4002
+ onBlur: () => setFocused(false),
3866
4003
  multiline: true,
3867
4004
  submitBehavior: "newline",
3868
4005
  autoCorrect: false,
@@ -3882,6 +4019,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3882
4019
  // rgba rather than the 'transparent' keyword because the keyword is
3883
4020
  // not reliably honoured for TextInput colour on Android.
3884
4021
  color: "rgba(0,0,0,0)",
4022
+ opacity: composerInputOpacity(import_react_native24.Platform.OS),
3885
4023
  fontSize: layout.fontSize,
3886
4024
  lineHeight: layout.lineHeight,
3887
4025
  textAlign: data?.align ?? "center",
@@ -3892,6 +4030,22 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3892
4030
  cursorColor: theme.colors.accent
3893
4031
  }
3894
4032
  ),
4033
+ focused && caret ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
4034
+ import_react_native24.View,
4035
+ {
4036
+ pointerEvents: "none",
4037
+ accessible: false,
4038
+ style: {
4039
+ position: "absolute",
4040
+ left: caret.left,
4041
+ top: caret.top,
4042
+ width: caret.width,
4043
+ height: caret.height,
4044
+ borderRadius: caret.width / 2,
4045
+ backgroundColor: theme.colors.accent
4046
+ }
4047
+ }
4048
+ ) : null,
3895
4049
  /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3896
4050
  import_react_native24.View,
3897
4051
  {
@@ -4091,7 +4245,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
4091
4245
  }
4092
4246
  function TextComposerHost({
4093
4247
  Composer = TextComposer,
4094
- safeArea
4248
+ safeArea,
4249
+ canvas
4095
4250
  }) {
4096
4251
  const { setActiveTool } = (0, import_core27.useEditor)();
4097
4252
  const session = useTextSession();
@@ -4139,7 +4294,8 @@ function TextComposerHost({
4139
4294
  session,
4140
4295
  insets,
4141
4296
  onDone: () => finish(true),
4142
- onCancel: () => finish(false)
4297
+ onCancel: () => finish(false),
4298
+ ...canvas ? { canvas } : {}
4143
4299
  }
4144
4300
  );
4145
4301
  }