@pictor-sdk/ui 0.2.2 → 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 = () => {
@@ -2888,6 +2915,13 @@ function fitImageToLayoutCell(cell, documentSize, intrinsic, options = {}) {
2888
2915
  function cellToTransformPatch(cell) {
2889
2916
  return { x: cell.x, y: cell.y, scale: cell.scale, rotation: 0 };
2890
2917
  }
2918
+ function layoutCellBox(cell, documentSize) {
2919
+ "worklet";
2920
+ return {
2921
+ width: cell.width * documentSize.width,
2922
+ height: cell.height * documentSize.height
2923
+ };
2924
+ }
2891
2925
 
2892
2926
  // src/panels/LayoutPanel.tsx
2893
2927
  import { jsx as jsx19 } from "react/jsx-runtime";
@@ -3557,7 +3591,8 @@ function fitCanvasRect(container, document) {
3557
3591
  };
3558
3592
  }
3559
3593
  function composerInputBox(request) {
3560
- 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);
3561
3596
  const fontSize = Math.max(1, request.fontSize * canvas.width);
3562
3597
  const lineHeight = Math.max(1, fontSize * request.lineHeight);
3563
3598
  const scale = request.document.width > 0 ? canvas.width / request.document.width : 0;
@@ -3581,6 +3616,35 @@ function composerInputBox(request) {
3581
3616
  lineHeight
3582
3617
  };
3583
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
+ }
3584
3648
 
3585
3649
  // src/composer/useTextSession.ts
3586
3650
  import {
@@ -3723,7 +3787,7 @@ var SHADOWS = [
3723
3787
  { color: "#000000", blur: 0.012, dx: 2e-3, dy: 4e-3 },
3724
3788
  { color: "#000000", blur: 2e-3, dx: 6e-3, dy: 8e-3 }
3725
3789
  ];
3726
- function TextComposer({ session, insets, onDone, onCancel }) {
3790
+ function TextComposer({ session, insets, onDone, onCancel, canvas }) {
3727
3791
  const theme = useTheme();
3728
3792
  const store = useEditorStore5();
3729
3793
  const { document } = useEditor22();
@@ -3733,6 +3797,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3733
3797
  const [selectionRange, setSelectionRange] = useState10(
3734
3798
  void 0
3735
3799
  );
3800
+ const [focused, setFocused] = useState10(false);
3801
+ const [caretLine, setCaretLine] = useState10(null);
3736
3802
  const data = session.data;
3737
3803
  const span = session.span;
3738
3804
  const layerId = session.layer?.id;
@@ -3742,7 +3808,12 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3742
3808
  setMeasured(null);
3743
3809
  return;
3744
3810
  }
3745
- 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
+ };
3746
3817
  const unsubscribe = renderer.textMeasures.subscribe(refresh);
3747
3818
  refresh();
3748
3819
  return unsubscribe;
@@ -3755,7 +3826,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3755
3826
  lineHeight: span?.lineHeight ?? 1.2,
3756
3827
  centerY: session.layer?.transform.y ?? 0.42,
3757
3828
  measured,
3758
- lines: session.value.split("\n").length
3829
+ lines: session.value.split("\n").length,
3830
+ ...canvas ? { canvas } : {}
3759
3831
  }),
3760
3832
  [
3761
3833
  container,
@@ -3764,20 +3836,41 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3764
3836
  span?.lineHeight,
3765
3837
  session.layer?.transform.y,
3766
3838
  session.value,
3767
- measured
3839
+ measured,
3840
+ canvas
3768
3841
  ]
3769
3842
  );
3770
3843
  const fonts = useMemo11(() => fontChoices(store.registry.fonts), [store.registry.fonts]);
3771
3844
  useEffect8(() => {
3772
- 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);
3773
3853
  return () => clearTimeout(handle);
3774
3854
  }, []);
3775
3855
  const onSelectionChange = useCallback6(
3776
3856
  (event) => {
3777
- 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
+ );
3778
3861
  },
3779
3862
  []
3780
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;
3781
3874
  const keepFocus = useCallback6(
3782
3875
  (apply) => {
3783
3876
  apply();
@@ -3838,6 +3931,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3838
3931
  });
3839
3932
  return () => subscription.remove();
3840
3933
  }, [onCancel, insets.keyboardVisible]);
3934
+ const tapStart = useRef13(null);
3841
3935
  const controlRow = {
3842
3936
  flexDirection: "row",
3843
3937
  alignItems: "center",
@@ -3862,6 +3956,54 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3862
3956
  style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0, backgroundColor: "#00000055" }
3863
3957
  }
3864
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
+ ),
3865
4007
  /* @__PURE__ */ jsx27(
3866
4008
  TextInput,
3867
4009
  {
@@ -3869,6 +4011,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3869
4011
  value: session.value,
3870
4012
  onChangeText: session.setValue,
3871
4013
  onSelectionChange,
4014
+ onFocus: () => setFocused(true),
4015
+ onBlur: () => setFocused(false),
3872
4016
  multiline: true,
3873
4017
  submitBehavior: "newline",
3874
4018
  autoCorrect: false,
@@ -3888,6 +4032,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3888
4032
  // rgba rather than the 'transparent' keyword because the keyword is
3889
4033
  // not reliably honoured for TextInput colour on Android.
3890
4034
  color: "rgba(0,0,0,0)",
4035
+ opacity: composerInputOpacity(Platform.OS),
3891
4036
  fontSize: layout.fontSize,
3892
4037
  lineHeight: layout.lineHeight,
3893
4038
  textAlign: data?.align ?? "center",
@@ -3898,6 +4043,22 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3898
4043
  cursorColor: theme.colors.accent
3899
4044
  }
3900
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,
3901
4062
  /* @__PURE__ */ jsxs18(
3902
4063
  View18,
3903
4064
  {
@@ -4097,7 +4258,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
4097
4258
  }
4098
4259
  function TextComposerHost({
4099
4260
  Composer = TextComposer,
4100
- safeArea
4261
+ safeArea,
4262
+ canvas
4101
4263
  }) {
4102
4264
  const { setActiveTool } = useEditor22();
4103
4265
  const session = useTextSession();
@@ -4145,7 +4307,8 @@ function TextComposerHost({
4145
4307
  session,
4146
4308
  insets,
4147
4309
  onDone: () => finish(true),
4148
- onCancel: () => finish(false)
4310
+ onCancel: () => finish(false),
4311
+ ...canvas ? { canvas } : {}
4149
4312
  }
4150
4313
  );
4151
4314
  }
@@ -4741,6 +4904,7 @@ export {
4741
4904
  defaultTheme,
4742
4905
  fitImageToLayoutCell,
4743
4906
  layoutCellBounds,
4907
+ layoutCellBox,
4744
4908
  layoutCells,
4745
4909
  mergeSlots,
4746
4910
  mergeTheme,