@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.mjs CHANGED
@@ -803,6 +803,7 @@ function LayerGestureSurface({ onSnap }) {
803
803
  const focalY = useSharedValue3(0.5);
804
804
  const pointers = useSharedValue3(0);
805
805
  const active = useSharedValue3(false);
806
+ const livePointers = useSharedValue3(0);
806
807
  const snapCX = useSharedValue3(false);
807
808
  const snapCY = useSharedValue3(false);
808
809
  const snapR = useSharedValue3(false);
@@ -977,7 +978,23 @@ function LayerGestureSurface({ onSnap }) {
977
978
  deleted
978
979
  );
979
980
  };
980
- const pan = Gesture4.Pan().minPointers(1).maxPointers(2).onBegin((e) => {
981
+ const releaseIfEmpty = () => {
982
+ "worklet";
983
+ if (livePointers.value > 0) return;
984
+ endIfIdle();
985
+ };
986
+ const pan = Gesture4.Pan().minPointers(1).maxPointers(2).onTouchesDown((e) => {
987
+ "worklet";
988
+ livePointers.value = e.allTouches.length;
989
+ }).onTouchesUp((e) => {
990
+ "worklet";
991
+ livePointers.value = Math.max(0, e.allTouches.length - e.changedTouches.length);
992
+ releaseIfEmpty();
993
+ }).onTouchesCancelled(() => {
994
+ "worklet";
995
+ livePointers.value = 0;
996
+ releaseIfEmpty();
997
+ }).onBegin((e) => {
981
998
  "worklet";
982
999
  if (active.value) return;
983
1000
  rawPanX.value = 0;
@@ -1024,7 +1041,7 @@ function LayerGestureSurface({ onSnap }) {
1024
1041
  recompute(false);
1025
1042
  }).onFinalize(() => {
1026
1043
  "worklet";
1027
- endIfIdle();
1044
+ releaseIfEmpty();
1028
1045
  });
1029
1046
  const pinch = Gesture4.Pinch().onUpdate((e) => {
1030
1047
  "worklet";
@@ -1033,6 +1050,9 @@ function LayerGestureSurface({ onSnap }) {
1033
1050
  focalY.value = e.focalY / metricsHeight;
1034
1051
  syncPointers(e.numberOfPointers);
1035
1052
  recompute(false);
1053
+ }).onFinalize(() => {
1054
+ "worklet";
1055
+ releaseIfEmpty();
1036
1056
  });
1037
1057
  const rotate = Gesture4.Rotation().enabled(config.rotationEnabled).onUpdate((e) => {
1038
1058
  "worklet";
@@ -1041,6 +1061,9 @@ function LayerGestureSurface({ onSnap }) {
1041
1061
  focalY.value = e.anchorY / metricsHeight;
1042
1062
  syncPointers(e.numberOfPointers);
1043
1063
  recompute(false);
1064
+ }).onFinalize(() => {
1065
+ "worklet";
1066
+ releaseIfEmpty();
1044
1067
  });
1045
1068
  const tap = Gesture4.Tap().onEnd((e) => {
1046
1069
  "worklet";
@@ -2865,6 +2888,13 @@ function fitImageToLayoutCell(cell, documentSize, intrinsic, options = {}) {
2865
2888
  function cellToTransformPatch(cell) {
2866
2889
  return { x: cell.x, y: cell.y, scale: cell.scale, rotation: 0 };
2867
2890
  }
2891
+ function layoutCellBox(cell, documentSize) {
2892
+ "worklet";
2893
+ return {
2894
+ width: cell.width * documentSize.width,
2895
+ height: cell.height * documentSize.height
2896
+ };
2897
+ }
2868
2898
 
2869
2899
  // src/panels/LayoutPanel.tsx
2870
2900
  import { jsx as jsx19 } from "react/jsx-runtime";
@@ -3499,17 +3529,21 @@ import {
3499
3529
  TextInput,
3500
3530
  View as View18
3501
3531
  } from "react-native";
3532
+ import { useRenderer as useRenderer4 } from "@pictor-sdk/skia";
3502
3533
 
3503
3534
  // src/composer/keyboardLayout.ts
3504
3535
  var HIDDEN_KEYBOARD = { height: 0 };
3505
3536
  var MAX_DURATION = 400;
3506
3537
  var FALLBACK_DURATION = 180;
3507
- function composerInsets(frame, safeAreaBottom = 0) {
3538
+ function composerInsets(frame, safeAreaBottom = 0, safeAreaTop = 0) {
3508
3539
  const height = Number.isFinite(frame.height) ? Math.max(0, frame.height) : 0;
3509
3540
  const visible = height > 0;
3510
3541
  const raw = frame.duration;
3511
3542
  const duration = raw === void 0 || !Number.isFinite(raw) || raw <= 0 ? FALLBACK_DURATION : Math.min(MAX_DURATION, raw);
3512
3543
  return {
3544
+ // The keyboard never covers the top, so this one is the safe area and
3545
+ // nothing else.
3546
+ top: Number.isFinite(safeAreaTop) ? Math.max(0, safeAreaTop) : 0,
3513
3547
  bottom: visible ? height : Math.max(0, safeAreaBottom),
3514
3548
  keyboardVisible: visible,
3515
3549
  duration
@@ -3529,6 +3563,31 @@ function fitCanvasRect(container, document) {
3529
3563
  top: (container.height - height) / 2
3530
3564
  };
3531
3565
  }
3566
+ function composerInputBox(request) {
3567
+ const canvas = fitCanvasRect(request.container, request.document);
3568
+ const fontSize = Math.max(1, request.fontSize * canvas.width);
3569
+ const lineHeight = Math.max(1, fontSize * request.lineHeight);
3570
+ const scale = request.document.width > 0 ? canvas.width / request.document.width : 0;
3571
+ const measuredHeight = request.measured && request.measured.height > 0 ? request.measured.height * scale : 0;
3572
+ const estimated = lineHeight * Math.max(1, request.lines);
3573
+ const height = Math.min(
3574
+ Math.max(lineHeight, measuredHeight > 0 ? measuredHeight : estimated),
3575
+ Math.max(lineHeight, canvas.height)
3576
+ );
3577
+ const centre = canvas.top + request.centerY * canvas.height;
3578
+ const top = Math.min(
3579
+ Math.max(0, centre - height / 2),
3580
+ Math.max(0, request.container.height - height)
3581
+ );
3582
+ return {
3583
+ left: canvas.left,
3584
+ width: Math.max(1, canvas.width),
3585
+ top,
3586
+ height: Math.max(1, height),
3587
+ fontSize,
3588
+ lineHeight
3589
+ };
3590
+ }
3532
3591
 
3533
3592
  // src/composer/useTextSession.ts
3534
3593
  import {
@@ -3676,26 +3735,45 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3676
3735
  const store = useEditorStore5();
3677
3736
  const { document } = useEditor22();
3678
3737
  const [container, setContainer] = useState10({ width: 0, height: 0 });
3738
+ const renderer = useRenderer4();
3679
3739
  const input = useRef13(null);
3680
3740
  const [selectionRange, setSelectionRange] = useState10(
3681
3741
  void 0
3682
3742
  );
3683
3743
  const data = session.data;
3684
3744
  const span = session.span;
3685
- const layout = useMemo11(() => {
3686
- const canvas = fitCanvasRect(container, document.size);
3687
- const fontSize = (span?.fontSize ?? 0.08) * canvas.width;
3688
- const centreY = canvas.top + (session.layer?.transform.y ?? 0.42) * canvas.height;
3689
- const height = canvas.height;
3690
- return {
3691
- fontSize: Math.max(1, fontSize),
3692
- lineHeight: Math.max(1, fontSize * (span?.lineHeight ?? 1.2)),
3693
- left: canvas.left,
3694
- width: Math.max(1, canvas.width),
3695
- top: Math.max(0, centreY - height / 2),
3696
- height: Math.max(1, height)
3697
- };
3698
- }, [container, document.size, span?.fontSize, span?.lineHeight, session.layer?.transform.y]);
3745
+ const layerId = session.layer?.id;
3746
+ const [measured, setMeasured] = useState10(null);
3747
+ useEffect8(() => {
3748
+ if (!layerId) {
3749
+ setMeasured(null);
3750
+ return;
3751
+ }
3752
+ const refresh = () => setMeasured(renderer.textMeasures.get(layerId) ?? null);
3753
+ const unsubscribe = renderer.textMeasures.subscribe(refresh);
3754
+ refresh();
3755
+ return unsubscribe;
3756
+ }, [renderer, layerId]);
3757
+ const layout = useMemo11(
3758
+ () => composerInputBox({
3759
+ container,
3760
+ document: document.size,
3761
+ fontSize: span?.fontSize ?? 0.08,
3762
+ lineHeight: span?.lineHeight ?? 1.2,
3763
+ centerY: session.layer?.transform.y ?? 0.42,
3764
+ measured,
3765
+ lines: session.value.split("\n").length
3766
+ }),
3767
+ [
3768
+ container,
3769
+ document.size,
3770
+ span?.fontSize,
3771
+ span?.lineHeight,
3772
+ session.layer?.transform.y,
3773
+ session.value,
3774
+ measured
3775
+ ]
3776
+ );
3699
3777
  const fonts = useMemo11(() => fontChoices(store.registry.fonts), [store.registry.fonts]);
3700
3778
  useEffect8(() => {
3701
3779
  const handle = setTimeout(() => input.current?.focus(), 50);
@@ -3834,7 +3912,7 @@ function TextComposer({ session, insets, onDone, onCancel }) {
3834
3912
  position: "absolute",
3835
3913
  left: 0,
3836
3914
  right: 0,
3837
- top: 0,
3915
+ top: insets.top,
3838
3916
  height: 56,
3839
3917
  flexDirection: "row",
3840
3918
  alignItems: "center",
@@ -4025,7 +4103,8 @@ function TextComposer({ session, insets, onDone, onCancel }) {
4025
4103
  );
4026
4104
  }
4027
4105
  function TextComposerHost({
4028
- Composer = TextComposer
4106
+ Composer = TextComposer,
4107
+ safeArea
4029
4108
  }) {
4030
4109
  const { setActiveTool } = useEditor22();
4031
4110
  const session = useTextSession();
@@ -4054,7 +4133,10 @@ function TextComposerHost({
4054
4133
  hide.remove();
4055
4134
  };
4056
4135
  }, []);
4057
- const insets = useMemo11(() => composerInsets(frame), [frame]);
4136
+ const insets = useMemo11(
4137
+ () => composerInsets(frame, safeArea?.bottom ?? 0, safeArea?.top ?? 0),
4138
+ [frame, safeArea?.bottom, safeArea?.top]
4139
+ );
4058
4140
  const finish = useCallback6(
4059
4141
  (commit) => {
4060
4142
  if (commit) session.commit();
@@ -4666,6 +4748,7 @@ export {
4666
4748
  defaultTheme,
4667
4749
  fitImageToLayoutCell,
4668
4750
  layoutCellBounds,
4751
+ layoutCellBox,
4669
4752
  layoutCells,
4670
4753
  mergeSlots,
4671
4754
  mergeTheme,