@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.mjs CHANGED
@@ -727,10 +727,16 @@ function hitTestLayerCandidatesUI(x, y, candidates, documentWidth, documentHeigh
727
727
  return null;
728
728
  }
729
729
 
730
+ // src/gestures/selectionTap.ts
731
+ function selectedLayerTapId(hit, wasSelectedAtStart) {
732
+ if (!hit || hit.locked || !wasSelectedAtStart) return null;
733
+ return hit.id;
734
+ }
735
+
730
736
  // src/gestures/LayerGestureSurface.tsx
731
737
  import { jsx as jsx8 } from "react/jsx-runtime";
732
- function LayerGestureSurface({ onSnap }) {
733
- const { document, dispatch, profile } = useEditor5();
738
+ function LayerGestureSurface({ onSnap, onSelectedLayerTap }) {
739
+ const { document, dispatch, profile, selection } = useEditor5();
734
740
  const store = useEditorStore2();
735
741
  const metrics = useCanvasMetrics4();
736
742
  const { setPreview, setLayerDragActive, setOverDeleteTarget, setSnap } = useInteractionState();
@@ -821,10 +827,11 @@ function LayerGestureSurface({ onSnap }) {
821
827
  document.size.height
822
828
  );
823
829
  }, [document.size.width, document.size.height]);
824
- const latest = useRef7({ document, metrics, store, dispatch, onSnap, profile });
825
- latest.current = { document, metrics, store, dispatch, onSnap, profile };
830
+ const latest = useRef7({ document, metrics, store, dispatch, onSnap, profile, selection, onSelectedLayerTap });
831
+ latest.current = { document, metrics, store, dispatch, onSnap, profile, selection, onSelectedLayerTap };
826
832
  const grabbedId = useRef7(null);
827
833
  const grabbedFrom = useRef7(null);
834
+ const tapWasSelected = useRef7(false);
828
835
  const snapState = useRef7(NO_SNAP2);
829
836
  const onGrabLayer = useCallback3((id) => {
830
837
  const { document: doc, store: s } = latest.current;
@@ -837,9 +844,23 @@ function LayerGestureSurface({ onSnap }) {
837
844
  setPreview({ layerId: hit.id, transform: previewTransform });
838
845
  setLayerDragActive(true);
839
846
  }, [setPreview, setLayerDragActive, previewTransform]);
847
+ const snapshotTapAt = useCallback3((xDp, yDp) => {
848
+ const { document: doc, metrics: m, store: s, selection: selected } = latest.current;
849
+ if (!m) return;
850
+ const layerInteraction = latest.current.profile.interaction?.layers;
851
+ const hit = hitTestDocument(
852
+ doc,
853
+ { x: xDp / m.width, y: yDp / m.height },
854
+ s.registry,
855
+ (layer, index) => isDirectManipulationLayer(layer, index, layerInteraction)
856
+ );
857
+ tapWasSelected.current = Boolean(hit && selected.includes(hit.id));
858
+ }, []);
840
859
  const onSelectAt = useCallback3((xDp, yDp) => {
841
860
  const { document: doc, metrics: m, store: s } = latest.current;
842
861
  if (!m) return;
862
+ const wasSelected = tapWasSelected.current;
863
+ tapWasSelected.current = false;
843
864
  const layerInteraction = latest.current.profile.interaction?.layers;
844
865
  const hit = hitTestDocument(
845
866
  doc,
@@ -848,6 +869,8 @@ function LayerGestureSurface({ onSnap }) {
848
869
  (layer, index) => isDirectManipulationLayer(layer, index, layerInteraction)
849
870
  );
850
871
  s.select(hit && !hit.locked ? [hit.id] : []);
872
+ const editId = selectedLayerTapId(hit ?? null, wasSelected);
873
+ if (editId) latest.current.onSelectedLayerTap?.(editId);
851
874
  }, []);
852
875
  const onSnapChanged = useCallback3(
853
876
  (centerX, centerY, rotation) => {
@@ -986,6 +1009,8 @@ function LayerGestureSurface({ onSnap }) {
986
1009
  const pan = Gesture4.Pan().minPointers(1).maxPointers(2).onTouchesDown((e) => {
987
1010
  "worklet";
988
1011
  livePointers.value = e.allTouches.length;
1012
+ const touch = e.changedTouches[0] ?? e.allTouches[0];
1013
+ if (touch) runOnJS4(snapshotTapAt)(touch.x, touch.y);
989
1014
  }).onTouchesUp((e) => {
990
1015
  "worklet";
991
1016
  livePointers.value = Math.max(0, e.allTouches.length - e.changedTouches.length);
@@ -1076,6 +1101,7 @@ function LayerGestureSurface({ onSnap }) {
1076
1101
  deleteTarget,
1077
1102
  aspect,
1078
1103
  onGrabLayer,
1104
+ snapshotTapAt,
1079
1105
  onSelectAt,
1080
1106
  onSnapChanged,
1081
1107
  onHoverChanged,
@@ -1769,8 +1795,9 @@ function Slider({
1769
1795
  const valueRef = useRef9(value);
1770
1796
  valueRef.current = value;
1771
1797
  const onLayout = (e) => {
1772
- setWidth(e.nativeEvent.layout.width);
1773
- widthRef.current = e.nativeEvent.layout.width;
1798
+ const next = e.nativeEvent.layout.width;
1799
+ widthRef.current = next;
1800
+ setWidth((previous) => previous === next ? previous : next);
1774
1801
  };
1775
1802
  const handlers = useMemo7(() => {
1776
1803
  const begin = () => {
@@ -3564,7 +3591,8 @@ function fitCanvasRect(container, document) {
3564
3591
  };
3565
3592
  }
3566
3593
  function composerInputBox(request) {
3567
- const canvas = fitCanvasRect(request.container, request.document);
3594
+ const provided = request.canvas;
3595
+ const canvas = provided && provided.width > 0 && provided.height > 0 ? provided : fitCanvasRect(request.container, request.document);
3568
3596
  const fontSize = Math.max(1, request.fontSize * canvas.width);
3569
3597
  const lineHeight = Math.max(1, fontSize * request.lineHeight);
3570
3598
  const scale = request.document.width > 0 ? canvas.width / request.document.width : 0;
@@ -3588,6 +3616,35 @@ function composerInputBox(request) {
3588
3616
  lineHeight
3589
3617
  };
3590
3618
  }
3619
+ function sameCaretLine(previous, next) {
3620
+ return previous === next || previous !== null && next !== null && previous.x === next.x && previous.y === next.y && previous.width === next.width && previous.height === next.height;
3621
+ }
3622
+ var CARET_WIDTH = 2;
3623
+ function composerInputOpacity(platform) {
3624
+ return platform === "android" ? 0 : 1;
3625
+ }
3626
+ function composerCaretBox(box, line) {
3627
+ if (!line || !Number.isFinite(line.x) || !Number.isFinite(line.y) || !Number.isFinite(line.width) || !Number.isFinite(line.height)) {
3628
+ return null;
3629
+ }
3630
+ const height = Math.max(1, line.height || box.lineHeight);
3631
+ return {
3632
+ left: box.left + line.x + line.width - CARET_WIDTH / 2,
3633
+ top: box.top + line.y,
3634
+ width: CARET_WIDTH,
3635
+ height
3636
+ };
3637
+ }
3638
+
3639
+ // src/composer/tapGesture.ts
3640
+ var TAP_SLOP = 8;
3641
+ function pressMoved(start, end, slop = TAP_SLOP) {
3642
+ if (!start || !end) return false;
3643
+ if (!Number.isFinite(start.x) || !Number.isFinite(start.y) || !Number.isFinite(end.x) || !Number.isFinite(end.y)) {
3644
+ return false;
3645
+ }
3646
+ return Math.hypot(end.x - start.x, end.y - start.y) > Math.max(0, slop);
3647
+ }
3591
3648
 
3592
3649
  // src/composer/useTextSession.ts
3593
3650
  import {
@@ -3730,7 +3787,7 @@ var SHADOWS = [
3730
3787
  { color: "#000000", blur: 0.012, dx: 2e-3, dy: 4e-3 },
3731
3788
  { color: "#000000", blur: 2e-3, dx: 6e-3, dy: 8e-3 }
3732
3789
  ];
3733
- function TextComposer({ session, insets, onDone, onCancel }) {
3790
+ function TextComposer({ session, insets, onDone, onCancel, canvas }) {
3734
3791
  const theme = useTheme();
3735
3792
  const store = useEditorStore5();
3736
3793
  const { document } = useEditor22();
@@ -3740,6 +3797,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3740
3797
  const [selectionRange, setSelectionRange] = useState10(
3741
3798
  void 0
3742
3799
  );
3800
+ const [focused, setFocused] = useState10(false);
3801
+ const [caretLine, setCaretLine] = useState10(null);
3743
3802
  const data = session.data;
3744
3803
  const span = session.span;
3745
3804
  const layerId = session.layer?.id;
@@ -3749,7 +3808,12 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3749
3808
  setMeasured(null);
3750
3809
  return;
3751
3810
  }
3752
- const refresh = () => setMeasured(renderer.textMeasures.get(layerId) ?? null);
3811
+ const refresh = () => {
3812
+ const next = renderer.textMeasures.get(layerId) ?? null;
3813
+ setMeasured(
3814
+ (previous) => previous && next && previous.width === next.width && previous.height === next.height ? previous : next
3815
+ );
3816
+ };
3753
3817
  const unsubscribe = renderer.textMeasures.subscribe(refresh);
3754
3818
  refresh();
3755
3819
  return unsubscribe;
@@ -3762,7 +3826,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3762
3826
  lineHeight: span?.lineHeight ?? 1.2,
3763
3827
  centerY: session.layer?.transform.y ?? 0.42,
3764
3828
  measured,
3765
- lines: session.value.split("\n").length
3829
+ lines: session.value.split("\n").length,
3830
+ ...canvas ? { canvas } : {}
3766
3831
  }),
3767
3832
  [
3768
3833
  container,
@@ -3771,20 +3836,41 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3771
3836
  span?.lineHeight,
3772
3837
  session.layer?.transform.y,
3773
3838
  session.value,
3774
- measured
3839
+ measured,
3840
+ canvas
3775
3841
  ]
3776
3842
  );
3777
3843
  const fonts = useMemo11(() => fontChoices(store.registry.fonts), [store.registry.fonts]);
3778
3844
  useEffect8(() => {
3779
- const handle = setTimeout(() => input.current?.focus(), 50);
3845
+ const handle = setTimeout(() => {
3846
+ input.current?.focus();
3847
+ if (session.kind === "existing" && session.value.length > 0) {
3848
+ const end = session.value.length;
3849
+ setSelectionRange({ start: end, end });
3850
+ input.current?.setNativeProps({ selection: { start: end, end } });
3851
+ }
3852
+ }, 50);
3780
3853
  return () => clearTimeout(handle);
3781
3854
  }, []);
3782
3855
  const onSelectionChange = useCallback6(
3783
3856
  (event) => {
3784
- setSelectionRange(event.nativeEvent.selection);
3857
+ const next = event.nativeEvent.selection;
3858
+ setSelectionRange(
3859
+ (previous) => previous && previous.start === next.start && previous.end === next.end ? previous : next
3860
+ );
3785
3861
  },
3786
3862
  []
3787
3863
  );
3864
+ const caretAt = selectionRange?.end ?? session.value.length;
3865
+ const caretSource = session.value.slice(0, Math.max(0, caretAt));
3866
+ const onCaretLayout = useCallback6((event) => {
3867
+ const lines = event.nativeEvent.lines;
3868
+ const last = lines[lines.length - 1];
3869
+ const next = last ? { x: last.x, y: last.y, width: last.width, height: last.height } : null;
3870
+ setCaretLine((previous) => sameCaretLine(previous, next) ? previous : next);
3871
+ }, []);
3872
+ const caret = composerCaretBox(layout, caretLine);
3873
+ const letterSpacing = span?.letterSpacing && span.fontSize > 0 ? span.letterSpacing * layout.fontSize / span.fontSize : void 0;
3788
3874
  const keepFocus = useCallback6(
3789
3875
  (apply) => {
3790
3876
  apply();
@@ -3845,6 +3931,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3845
3931
  });
3846
3932
  return () => subscription.remove();
3847
3933
  }, [onCancel, insets.keyboardVisible]);
3934
+ const tapStart = useRef13(null);
3848
3935
  const controlRow = {
3849
3936
  flexDirection: "row",
3850
3937
  alignItems: "center",
@@ -3869,6 +3956,54 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3869
3956
  style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0, backgroundColor: "#00000055" }
3870
3957
  }
3871
3958
  ),
3959
+ /* @__PURE__ */ jsx27(
3960
+ Pressable15,
3961
+ {
3962
+ accessible: true,
3963
+ collapsable: false,
3964
+ accessibilityRole: "button",
3965
+ accessibilityLabel: "Finish editing text",
3966
+ onAccessibilityTap: onDone,
3967
+ onPressIn: (event) => {
3968
+ tapStart.current = {
3969
+ x: event.nativeEvent.pageX,
3970
+ y: event.nativeEvent.pageY
3971
+ };
3972
+ },
3973
+ onPress: (event) => {
3974
+ const start = tapStart.current;
3975
+ const end = { x: event.nativeEvent.pageX, y: event.nativeEvent.pageY };
3976
+ if (!pressMoved(start, end)) onDone();
3977
+ },
3978
+ style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }
3979
+ }
3980
+ ),
3981
+ /* @__PURE__ */ jsx27(
3982
+ Text18,
3983
+ {
3984
+ accessible: false,
3985
+ importantForAccessibility: "no-hide-descendants",
3986
+ pointerEvents: "none",
3987
+ onTextLayout: onCaretLayout,
3988
+ style: {
3989
+ position: "absolute",
3990
+ left: layout.left,
3991
+ width: layout.width,
3992
+ top: layout.top,
3993
+ height: layout.height,
3994
+ opacity: 0,
3995
+ fontSize: layout.fontSize,
3996
+ lineHeight: layout.lineHeight,
3997
+ fontFamily: span?.fontFamily,
3998
+ fontWeight: span?.fontWeight ? `${span.fontWeight}` : void 0,
3999
+ letterSpacing,
4000
+ textAlign: data?.align ?? "center",
4001
+ padding: 0
4002
+ },
4003
+ children: `${caretSource}\u200B`
4004
+ },
4005
+ `${caretSource}:${caretAt}`
4006
+ ),
3872
4007
  /* @__PURE__ */ jsx27(
3873
4008
  TextInput,
3874
4009
  {
@@ -3876,6 +4011,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3876
4011
  value: session.value,
3877
4012
  onChangeText: session.setValue,
3878
4013
  onSelectionChange,
4014
+ onFocus: () => setFocused(true),
4015
+ onBlur: () => setFocused(false),
3879
4016
  multiline: true,
3880
4017
  submitBehavior: "newline",
3881
4018
  autoCorrect: false,
@@ -3895,6 +4032,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3895
4032
  // rgba rather than the 'transparent' keyword because the keyword is
3896
4033
  // not reliably honoured for TextInput colour on Android.
3897
4034
  color: "rgba(0,0,0,0)",
4035
+ opacity: composerInputOpacity(Platform.OS),
3898
4036
  fontSize: layout.fontSize,
3899
4037
  lineHeight: layout.lineHeight,
3900
4038
  textAlign: data?.align ?? "center",
@@ -3905,6 +4043,22 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3905
4043
  cursorColor: theme.colors.accent
3906
4044
  }
3907
4045
  ),
4046
+ focused && caret ? /* @__PURE__ */ jsx27(
4047
+ View18,
4048
+ {
4049
+ pointerEvents: "none",
4050
+ accessible: false,
4051
+ style: {
4052
+ position: "absolute",
4053
+ left: caret.left,
4054
+ top: caret.top,
4055
+ width: caret.width,
4056
+ height: caret.height,
4057
+ borderRadius: caret.width / 2,
4058
+ backgroundColor: theme.colors.accent
4059
+ }
4060
+ }
4061
+ ) : null,
3908
4062
  /* @__PURE__ */ jsxs18(
3909
4063
  View18,
3910
4064
  {
@@ -4104,7 +4258,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
4104
4258
  }
4105
4259
  function TextComposerHost({
4106
4260
  Composer = TextComposer,
4107
- safeArea
4261
+ safeArea,
4262
+ canvas
4108
4263
  }) {
4109
4264
  const { setActiveTool } = useEditor22();
4110
4265
  const session = useTextSession();
@@ -4152,7 +4307,8 @@ function TextComposerHost({
4152
4307
  session,
4153
4308
  insets,
4154
4309
  onDone: () => finish(true),
4155
- onCancel: () => finish(false)
4310
+ onCancel: () => finish(false),
4311
+ ...canvas ? { canvas } : {}
4156
4312
  }
4157
4313
  );
4158
4314
  }