@weasel-js/core 1.9.3 → 1.9.4
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/CHANGELOG.md +356 -0
- package/README.md +2 -2
- package/dist/WeaselProvider.js +5 -5
- package/dist/affordances/composeAffordanceLayer.js +10 -20
- package/dist/affordances/composeAffordanceLayer.js.map +1 -1
- package/dist/affordances/cornerResize.js +25 -17
- package/dist/affordances/cornerResize.js.map +1 -1
- package/dist/affordances/hitAffordanceRegions.js +27 -20
- package/dist/affordances/hitAffordanceRegions.js.map +1 -1
- package/dist/affordances/rotationHandle.js +18 -18
- package/dist/affordances/rotationHandle.js.map +1 -1
- package/dist/animation/behaviors/momentum.js +93 -55
- package/dist/animation/behaviors/momentum.js.map +1 -1
- package/dist/animation/loop.js +10 -12
- package/dist/animation/loop.js.map +1 -1
- package/dist/animation/reflow.js +8 -8
- package/dist/animation/reflow.js.map +1 -1
- package/dist/animation/stagger.js +64 -56
- package/dist/animation/stagger.js.map +1 -1
- package/dist/animation/timeline/booking.js +62 -54
- package/dist/animation/timeline/booking.js.map +1 -1
- package/dist/animation/timeline/createTimeline.js +85 -76
- package/dist/animation/timeline/createTimeline.js.map +1 -1
- package/dist/animation/timeline/sampleTrack.js +13 -5
- package/dist/animation/timeline/sampleTrack.js.map +1 -1
- package/dist/animation/useAnimator.js +45 -35
- package/dist/animation/useAnimator.js.map +1 -1
- package/dist/animation/wrappers/animateLifecycle.js +31 -24
- package/dist/animation/wrappers/animateLifecycle.js.map +1 -1
- package/dist/animation/wrappers/animateOnSetPose.js +28 -26
- package/dist/animation/wrappers/animateOnSetPose.js.map +1 -1
- package/dist/animation/wrappers/instantPose.js +13 -0
- package/dist/animation/wrappers/instantPose.js.map +1 -0
- package/dist/{poseDescriptor-D-OH-tu5.d.ts → autoPoseDescriptor-Ct7yqivU.d.ts} +28 -3
- package/dist/barrel/chrome.js +16 -0
- package/dist/barrel/input.js +19 -0
- package/dist/barrel/interactions.js +42 -0
- package/dist/barrel/paint.js +18 -0
- package/dist/barrel/paths.js +19 -0
- package/dist/barrel/renderer.js +20 -0
- package/dist/barrel/routing.js +51 -0
- package/dist/barrel/text.js +18 -0
- package/dist/barrel/viewport.js +52 -0
- package/dist/builtins-DPcdghtB.d.ts +1223 -0
- package/dist/canvas/Canvas.js +363 -448
- package/dist/canvas/Canvas.js.map +1 -1
- package/dist/canvas/CanvasView.js +164 -165
- package/dist/canvas/CanvasView.js.map +1 -1
- package/dist/canvas/CursorCoordsHud.js +37 -32
- package/dist/canvas/CursorCoordsHud.js.map +1 -1
- package/dist/canvas/DrawCanvas.js +49 -43
- package/dist/canvas/DrawCanvas.js.map +1 -1
- package/dist/canvas/MinimapCanvas.js +26 -25
- package/dist/canvas/MinimapCanvas.js.map +1 -1
- package/dist/canvas/NodeShape.js +59 -377
- package/dist/canvas/NodeShape.js.map +1 -1
- package/dist/canvas/PickHud.js +50 -47
- package/dist/canvas/PickHud.js.map +1 -1
- package/dist/canvas/SceneCanvas/GestureDispatcherMounter.js +167 -0
- package/dist/canvas/SceneCanvas/GestureDispatcherMounter.js.map +1 -0
- package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +13 -13
- package/dist/canvas/SceneCanvas/StandardActionsRegistrar.js +67 -0
- package/dist/canvas/SceneCanvas/StandardActionsRegistrar.js.map +1 -0
- package/dist/canvas/SceneCanvas/ToolKeybindingsMounter.js +16 -0
- package/dist/canvas/SceneCanvas/ToolKeybindingsMounter.js.map +1 -0
- package/dist/canvas/SceneCanvas/coordTrace.js +10 -0
- package/dist/canvas/SceneCanvas/coordTrace.js.map +1 -0
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js +112 -70
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js.map +1 -1
- package/dist/canvas/SceneCanvas/defaultNodeRouting.js +9 -8
- package/dist/canvas/SceneCanvas/defaultNodeRouting.js.map +1 -1
- package/dist/canvas/SceneCanvas/features.js +1 -0
- package/dist/canvas/SceneCanvas/features.js.map +1 -1
- package/dist/canvas/SceneCanvas/isDev.js +12 -0
- package/dist/canvas/SceneCanvas/isDev.js.map +1 -0
- package/dist/canvas/SceneCanvas/poseGeometry.js +29 -28
- package/dist/canvas/SceneCanvas/poseGeometry.js.map +1 -1
- package/dist/canvas/SceneCanvas/poseRegistrars.js +13 -0
- package/dist/canvas/SceneCanvas/poseRegistrars.js.map +1 -0
- package/dist/canvas/SceneCanvas/props.js +38 -0
- package/dist/canvas/SceneCanvas/props.js.map +1 -0
- package/dist/canvas/SceneCanvas/useBuiltinShapeTools.js +21 -62
- package/dist/canvas/SceneCanvas/useBuiltinShapeTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useDispatcherOverlayLayer.js.map +1 -1
- package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js +46 -48
- package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js +33 -33
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +534 -788
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SceneViewCanvas.js +45 -43
- package/dist/canvas/SceneViewCanvas.js.map +1 -1
- package/dist/canvas/buildSceneLayer.js +91 -0
- package/dist/canvas/buildSceneLayer.js.map +1 -0
- package/dist/canvas/buildSceneTree.js +16 -19
- package/dist/canvas/buildSceneTree.js.map +1 -1
- package/dist/canvas/canvasRenderer.js +68 -52
- package/dist/canvas/canvasRenderer.js.map +1 -1
- package/dist/canvas/deps/changes.js +13 -0
- package/dist/canvas/deps/changes.js.map +1 -0
- package/dist/canvas/deps/editAnchors.js +28 -28
- package/dist/canvas/deps/insert.js +119 -91
- package/dist/canvas/deps/insert.js.map +1 -1
- package/dist/canvas/deps/slice.js +46 -45
- package/dist/canvas/deps/slice.js.map +1 -1
- package/dist/canvas/deps/textEdit.js +10 -9
- package/dist/canvas/deps/textEdit.js.map +1 -1
- package/dist/canvas/derivedPath.js +46 -23
- package/dist/canvas/derivedPath.js.map +1 -1
- package/dist/canvas/getNodeAtPoint.js +7 -7
- package/dist/canvas/getNodeAtPoint.js.map +1 -1
- package/dist/canvas/hud.module.js +1 -1
- package/dist/canvas/hud.module.js.map +1 -1
- package/dist/canvas/kitRequirements.js +2 -2
- package/dist/canvas/kitRequirements.js.map +1 -1
- package/dist/canvas/layerOrder.js +13 -6
- package/dist/canvas/layerOrder.js.map +1 -1
- package/dist/canvas/minimapMath.js +12 -10
- package/dist/canvas/minimapMath.js.map +1 -1
- package/dist/canvas/nodeShapes/derivedPainter.js +34 -0
- package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -0
- package/dist/canvas/nodeShapes/imagePainter.js +87 -0
- package/dist/canvas/nodeShapes/imagePainter.js.map +1 -0
- package/dist/canvas/nodeShapes/memo.js +9 -0
- package/dist/canvas/nodeShapes/memo.js.map +1 -0
- package/dist/canvas/nodeShapes/nodePaint.js +57 -0
- package/dist/canvas/nodeShapes/nodePaint.js.map +1 -0
- package/dist/canvas/nodeShapes/pathPainter.js +38 -0
- package/dist/canvas/nodeShapes/pathPainter.js.map +1 -0
- package/dist/canvas/nodeShapes/poseBox.js +20 -0
- package/dist/canvas/nodeShapes/poseBox.js.map +1 -0
- package/dist/canvas/nodeShapes/rectFallbackPainter.js +38 -0
- package/dist/canvas/nodeShapes/rectFallbackPainter.js.map +1 -0
- package/dist/canvas/nodeShapes/shapePainter.js +72 -0
- package/dist/canvas/nodeShapes/shapePainter.js.map +1 -0
- package/dist/canvas/nodeShapes/textPainter.js +55 -0
- package/dist/canvas/nodeShapes/textPainter.js.map +1 -0
- package/dist/canvas/overlayRoleStrokes.js.map +1 -1
- package/dist/canvas/paintCull.js +10 -11
- package/dist/canvas/paintCull.js.map +1 -1
- package/dist/canvas/pickWalk.js +52 -63
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/poseRotation.js +11 -23
- package/dist/canvas/poseRotation.js.map +1 -1
- package/dist/canvas/renderDebugSnapshot.js +5 -5
- package/dist/canvas/renderSceneToPixels.js +3 -1
- package/dist/canvas/renderSceneToPixels.js.map +1 -1
- package/dist/canvas/sceneAdapter.js +66 -148
- package/dist/canvas/sceneAdapter.js.map +1 -1
- package/dist/canvas/sceneClipboard.js +112 -0
- package/dist/canvas/sceneClipboard.js.map +1 -0
- package/dist/canvas/sceneSlotWiring.js +4 -4
- package/dist/canvas/sceneSlotWiring.js.map +1 -1
- package/dist/canvas/sceneViewRender.js +39 -40
- package/dist/canvas/sceneViewRender.js.map +1 -1
- package/dist/canvas/useCanvasRenderer.js +14 -6
- package/dist/canvas/useCanvasRenderer.js.map +1 -1
- package/dist/canvas/useCanvasView.js +58 -0
- package/dist/canvas/useCanvasView.js.map +1 -0
- package/dist/canvas/useFrameLoop.js +35 -26
- package/dist/canvas/useFrameLoop.js.map +1 -1
- package/dist/canvas/useSceneLayerList.js +21 -0
- package/dist/canvas/useSceneLayerList.js.map +1 -0
- package/dist/clipboard.d.ts +10 -6
- package/dist/clone.d.ts +4 -3
- package/dist/core/adapters/arrayAdapter.js +48 -39
- package/dist/core/adapters/arrayAdapter.js.map +1 -1
- package/dist/core/device/index.js +5 -4
- package/dist/core/device/useDeviceProfile.js +38 -44
- package/dist/core/device/useDeviceProfile.js.map +1 -1
- package/dist/core/device/watchDevicePixelRatio.js +17 -0
- package/dist/core/device/watchDevicePixelRatio.js.map +1 -0
- package/dist/core/fillInPoseFrame.js.map +1 -1
- package/dist/core/geometry/poseDescriptor.js +21 -19
- package/dist/core/geometry/poseDescriptor.js.map +1 -1
- package/dist/core/geometry/poseFrame.js +44 -0
- package/dist/core/geometry/poseFrame.js.map +1 -0
- package/dist/core/geometry/poseRotation.js.map +1 -1
- package/dist/core/geometry/regionHit.js +9 -10
- package/dist/core/geometry/regionHit.js.map +1 -1
- package/dist/core/geometry/unionBounds.js +31 -29
- package/dist/core/geometry/unionBounds.js.map +1 -1
- package/dist/core/isPlainObject.js +2 -11
- package/dist/core/ops/arrange.js +1 -0
- package/dist/core/ops/arrange.js.map +1 -1
- package/dist/core/ops/create.js +10 -5
- package/dist/core/ops/create.js.map +1 -1
- package/dist/core/ops/delete.js +12 -24
- package/dist/core/ops/delete.js.map +1 -1
- package/dist/core/ops/reorder/index.js +43 -40
- package/dist/core/ops/reorder/index.js.map +1 -1
- package/dist/core/ops/reorder/setChildOrder.js +34 -0
- package/dist/core/ops/reorder/setChildOrder.js.map +1 -0
- package/dist/core/paintKinds.js +5 -5
- package/dist/core/paintKinds.js.map +1 -1
- package/dist/core/scene/NodeProperties.js.map +1 -1
- package/dist/core/scene/appliedLog.js +37 -0
- package/dist/core/scene/appliedLog.js.map +1 -0
- package/dist/core/scene/arrivalWindow.js +101 -0
- package/dist/core/scene/arrivalWindow.js.map +1 -0
- package/dist/core/scene/derivedPath.js.map +1 -1
- package/dist/core/scene/historyFacade.js +84 -0
- package/dist/core/scene/historyFacade.js.map +1 -0
- package/dist/core/scene/kitOps.js +173 -0
- package/dist/core/scene/kitOps.js.map +1 -0
- package/dist/core/scene/layerStack.js +38 -0
- package/dist/core/scene/layerStack.js.map +1 -0
- package/dist/core/scene/lockGuard.js +37 -0
- package/dist/core/scene/lockGuard.js.map +1 -0
- package/dist/core/scene/nodeFnFields.js +5 -6
- package/dist/core/scene/nodeFnFields.js.map +1 -1
- package/dist/core/scene/nodeMemo.js +45 -19
- package/dist/core/scene/nodeMemo.js.map +1 -1
- package/dist/core/scene/nodeStore.js +126 -0
- package/dist/core/scene/nodeStore.js.map +1 -0
- package/dist/core/scene/pickability.js +6 -0
- package/dist/core/scene/pickability.js.map +1 -0
- package/dist/core/scene/poseFeed.js +38 -38
- package/dist/core/scene/poseFeed.js.map +1 -1
- package/dist/core/scene/poseOverrides.js +34 -38
- package/dist/core/scene/poseOverrides.js.map +1 -1
- package/dist/core/scene/renderOrder.js +84 -0
- package/dist/core/scene/renderOrder.js.map +1 -0
- package/dist/core/scene/scene.js +289 -866
- package/dist/core/scene/scene.js.map +1 -1
- package/dist/core/scene/serialization.js +68 -0
- package/dist/core/scene/serialization.js.map +1 -0
- package/dist/core/scene/treeOrder.js +13 -0
- package/dist/core/scene/treeOrder.js.map +1 -0
- package/dist/core/scene/types.js.map +1 -1
- package/dist/core/viewport/meanScale.js +1 -1
- package/dist/core/viewport/meanScale.js.map +1 -1
- package/dist/core/viewport/view.js +12 -12
- package/dist/core/viewport/view.js.map +1 -1
- package/dist/core/viewport/zoomBounds.js +10 -8
- package/dist/core/viewport/zoomBounds.js.map +1 -1
- package/dist/{cornerHandles-BCrDp_mH.d.ts → cornerHandles-CtCb8a3G.d.ts} +1 -1
- package/dist/debug/createDebugOverlayLayer.js +14 -14
- package/dist/debug/createDebugOverlayLayer.js.map +1 -1
- package/dist/debug/flag.js +2 -41
- package/dist/features/chrome-caps/useHoverTracking.js +22 -17
- package/dist/features/chrome-caps/useHoverTracking.js.map +1 -1
- package/dist/features/focus/gateLayer.js +3 -4
- package/dist/features/focus/gateLayer.js.map +1 -1
- package/dist/features/images/imageCache.js +19 -9
- package/dist/features/images/imageCache.js.map +1 -1
- package/dist/features/ingestion/contentHandlers.js +14 -11
- package/dist/features/ingestion/contentHandlers.js.map +1 -1
- package/dist/features/ingestion/index.js +1 -1
- package/dist/features/ingestion/registerKitHandlers.js +7 -7
- package/dist/features/ingestion/weaselJsonHandler.js +17 -14
- package/dist/features/ingestion/weaselJsonHandler.js.map +1 -1
- package/dist/features/meshPaint/lazyKind.js +201 -35
- package/dist/features/meshPaint/lazyKind.js.map +1 -1
- package/dist/features/meshPaint/meshPaint.js +82 -79
- package/dist/features/meshPaint/meshPaint.js.map +1 -1
- package/dist/features/minimap/createMinimapContribution.js +42 -42
- package/dist/features/minimap/layers.js +44 -62
- package/dist/features/minimap/layers.js.map +1 -1
- package/dist/features/parallax/createParallaxPlane.js +10 -14
- package/dist/features/parallax/createParallaxPlane.js.map +1 -1
- package/dist/features/paths/anchorEdits.js +19 -8
- package/dist/features/paths/anchorEdits.js.map +1 -1
- package/dist/features/paths/anchors.js +31 -26
- package/dist/features/paths/anchors.js.map +1 -1
- package/dist/features/paths/booleans.js +2 -2
- package/dist/features/paths/index.js +13 -11
- package/dist/features/paths/markerCommands.js +37 -36
- package/dist/features/paths/markerCommands.js.map +1 -1
- package/dist/features/paths/pathInWorld.js +19 -19
- package/dist/features/paths/pathInWorld.js.map +1 -1
- package/dist/features/paths/pathLayer.js.map +1 -1
- package/dist/features/paths/strokeToPath.js +40 -0
- package/dist/features/paths/strokeToPath.js.map +1 -0
- package/dist/features/paths/tessellate/stroke.js.map +1 -1
- package/dist/features/patterns/commandPaint.js +18 -0
- package/dist/features/patterns/commandPaint.js.map +1 -0
- package/dist/features/patterns/index.js +5 -17
- package/dist/features/patterns/index.js.map +1 -1
- package/dist/features/patterns/rasterizeTile.js +17 -0
- package/dist/features/patterns/rasterizeTile.js.map +1 -0
- package/dist/features/patterns/resolveSpec.js +52 -44
- package/dist/features/patterns/resolveSpec.js.map +1 -1
- package/dist/features/poseRun/usePoseRun.js +8 -3
- package/dist/features/poseRun/usePoseRun.js.map +1 -1
- package/dist/features/selection/SelectionContext.js +5 -0
- package/dist/features/selection/SelectionContext.js.map +1 -1
- package/dist/features/selection/overlay.js +18 -18
- package/dist/features/selection/overlay.js.map +1 -1
- package/dist/features/text/domOffsets.js +82 -0
- package/dist/features/text/domOffsets.js.map +1 -0
- package/dist/features/text/domRuns.js +103 -131
- package/dist/features/text/domRuns.js.map +1 -1
- package/dist/features/text/hitTest.js +5 -2
- package/dist/features/text/hitTest.js.map +1 -1
- package/dist/features/text/isTextData.js +8 -0
- package/dist/features/text/isTextData.js.map +1 -0
- package/dist/features/text/overlayDirection.js +15 -0
- package/dist/features/text/overlayDirection.js.map +1 -0
- package/dist/features/text/runs/rangeStyle.js +32 -34
- package/dist/features/text/runs/rangeStyle.js.map +1 -1
- package/dist/features/text/textCommand.js +34 -33
- package/dist/features/text/textCommand.js.map +1 -1
- package/dist/features/text/textToPath.js +40 -40
- package/dist/features/text/textToPath.js.map +1 -1
- package/dist/features/text/useSceneTextEdit.js +76 -82
- package/dist/features/text/useSceneTextEdit.js.map +1 -1
- package/dist/features/text/useTextEdit.js +184 -160
- package/dist/features/text/useTextEdit.js.map +1 -1
- package/dist/features/tiling/createTiledLayer.js +24 -22
- package/dist/features/tiling/createTiledLayer.js.map +1 -1
- package/dist/index.d.ts +11033 -7749
- package/dist/index.js +316 -309
- package/dist/input/keyState.js +42 -43
- package/dist/input/keyState.js.map +1 -1
- package/dist/input/useKeyModifiers.js +11 -0
- package/dist/input/useKeyModifiers.js.map +1 -0
- package/dist/interactions/actions/align/align.js +8 -8
- package/dist/interactions/actions/clipboard/clipboardOps.js +34 -32
- package/dist/interactions/actions/clipboard/clipboardOps.js.map +1 -1
- package/dist/interactions/actions/clipboard/materializePaste.js +10 -7
- package/dist/interactions/actions/clipboard/materializePaste.js.map +1 -1
- package/dist/interactions/actions/clipboard/wireFormat.js.map +1 -1
- package/dist/interactions/actions/clone/behaviors/cloneByAltDrag.js.map +1 -1
- package/dist/interactions/actions/defaultCommitAdapter.js +20 -18
- package/dist/interactions/actions/defaultCommitAdapter.js.map +1 -1
- package/dist/interactions/actions/defaults/align.js +9 -10
- package/dist/interactions/actions/defaults/align.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorEditing.js +59 -81
- package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
- package/dist/interactions/actions/defaults/areaSelect.js +35 -38
- package/dist/interactions/actions/defaults/areaSelect.js.map +1 -1
- package/dist/interactions/actions/defaults/clipboard.js +26 -10
- package/dist/interactions/actions/defaults/clipboard.js.map +1 -1
- package/dist/interactions/actions/defaults/clone.js +44 -60
- package/dist/interactions/actions/defaults/clone.js.map +1 -1
- package/dist/interactions/actions/defaults/copySubtree.js +40 -0
- package/dist/interactions/actions/defaults/copySubtree.js.map +1 -0
- package/dist/interactions/actions/defaults/distribute.js +14 -13
- package/dist/interactions/actions/defaults/distribute.js.map +1 -1
- package/dist/interactions/actions/defaults/duplicate.js +17 -57
- package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
- package/dist/interactions/actions/defaults/editAnchors.js +18 -18
- package/dist/interactions/actions/defaults/enterTextEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/flip.js +52 -46
- package/dist/interactions/actions/defaults/flip.js.map +1 -1
- package/dist/interactions/actions/defaults/group.js +43 -48
- package/dist/interactions/actions/defaults/group.js.map +1 -1
- package/dist/interactions/actions/defaults/ingest.js +35 -33
- package/dist/interactions/actions/defaults/ingest.js.map +1 -1
- package/dist/interactions/actions/defaults/insert.js +50 -51
- package/dist/interactions/actions/defaults/insert.js.map +1 -1
- package/dist/interactions/actions/defaults/insertPathAnchor.js +7 -7
- package/dist/interactions/actions/defaults/lassoSelect.js +67 -70
- package/dist/interactions/actions/defaults/lassoSelect.js.map +1 -1
- package/dist/interactions/actions/defaults/move.js +267 -249
- package/dist/interactions/actions/defaults/move.js.map +1 -1
- package/dist/interactions/actions/defaults/nudge.js +40 -68
- package/dist/interactions/actions/defaults/nudge.js.map +1 -1
- package/dist/interactions/actions/defaults/nudgeSteps.js +37 -0
- package/dist/interactions/actions/defaults/nudgeSteps.js.map +1 -0
- package/dist/interactions/actions/defaults/reorder.js +2 -0
- package/dist/interactions/actions/defaults/reorder.js.map +1 -1
- package/dist/interactions/actions/defaults/resize.js +104 -98
- package/dist/interactions/actions/defaults/resize.js.map +1 -1
- package/dist/interactions/actions/defaults/rotate.js +111 -110
- package/dist/interactions/actions/defaults/rotate.js.map +1 -1
- package/dist/interactions/actions/defaults/setFill.js +7 -7
- package/dist/interactions/actions/defaults/setFillOpacity.js +4 -4
- package/dist/interactions/actions/defaults/setStroke.js +12 -12
- package/dist/interactions/actions/defaults/setStrokeOpacity.js +6 -6
- package/dist/interactions/actions/defaults/sliceOps.js +19 -19
- package/dist/interactions/actions/defaults/viewportDragPan.js +37 -26
- package/dist/interactions/actions/defaults/viewportDragPan.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/geometry.js +28 -23
- package/dist/interactions/actions/edit-anchors/geometry.js.map +1 -1
- package/dist/interactions/actions/flip/helpers.js +30 -17
- package/dist/interactions/actions/flip/helpers.js.map +1 -1
- package/dist/interactions/actions/hitSelection.js +8 -0
- package/dist/interactions/actions/hitSelection.js.map +1 -0
- package/dist/interactions/actions/lasso-select/behaviors/selectFromLasso.js +14 -18
- package/dist/interactions/actions/lasso-select/behaviors/selectFromLasso.js.map +1 -1
- package/dist/interactions/actions/move/behaviors/snapBackOrDelete.js +5 -5
- package/dist/interactions/actions/move/behaviors/snapBackOrDelete.js.map +1 -1
- package/dist/interactions/actions/move/behaviors/snapToContainer.js +32 -21
- package/dist/interactions/actions/move/behaviors/snapToContainer.js.map +1 -1
- package/dist/interactions/actions/move/gestureAdapter.js +32 -15
- package/dist/interactions/actions/move/gestureAdapter.js.map +1 -1
- package/dist/interactions/actions/move/landedDescendants.js +33 -0
- package/dist/interactions/actions/move/landedDescendants.js.map +1 -0
- package/dist/interactions/actions/outlines/createOutlines.js +18 -18
- package/dist/interactions/actions/poseDescriptorDep.js +16 -4
- package/dist/interactions/actions/poseDescriptorDep.js.map +1 -1
- package/dist/interactions/actions/poseFrame.js +11 -6
- package/dist/interactions/actions/poseFrame.js.map +1 -1
- package/dist/interactions/actions/previewOverrides.js +5 -5
- package/dist/interactions/actions/previewOverrides.js.map +1 -1
- package/dist/interactions/actions/selectionSubtree.js +69 -0
- package/dist/interactions/actions/selectionSubtree.js.map +1 -0
- package/dist/interactions/actions/useStandardActions.js +66 -62
- package/dist/interactions/actions/useStandardActions.js.map +1 -1
- package/dist/interactions/gestures/dragGesture/dragGesture.js +4 -7
- package/dist/interactions/gestures/dragGesture/dragGesture.js.map +1 -1
- package/dist/interactions/gestures/dragGesture/dragPointOf.js +23 -0
- package/dist/interactions/gestures/dragGesture/dragPointOf.js.map +1 -0
- package/dist/interactions/gestures/handleDrag/handleDrag.js +21 -26
- package/dist/interactions/gestures/handleDrag/handleDrag.js.map +1 -1
- package/dist/interactions/gestures/pointerDrag/pointerDrag.js +8 -6
- package/dist/interactions/gestures/pointerDrag/pointerDrag.js.map +1 -1
- package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js +8 -4
- package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js.map +1 -1
- package/dist/{math-Drrw4ewZ.d.ts → math-J-ACHyEg.d.ts} +1 -2
- package/dist/math.d.ts +2 -3
- package/dist/move.d.ts +12 -11
- package/dist/{originProjection-DEklpDc_.d.ts → originProjection-gHnOup4e.d.ts} +1 -1
- package/dist/{registerProgram-BMZRDgrh.d.ts → registerProgram-T9S_PfAX.d.ts} +18 -7
- package/dist/renderer/DrawCommand.js.map +1 -1
- package/dist/renderer/WeaselRenderer.js +75 -73
- package/dist/renderer/WeaselRenderer.js.map +1 -1
- package/dist/renderer/cache/GLImageCache.js +15 -5
- package/dist/renderer/cache/GLImageCache.js.map +1 -1
- package/dist/renderer/cache/GLMeshCache.js +23 -23
- package/dist/renderer/cache/GLMeshCache.js.map +1 -1
- package/dist/renderer/cache/GLTextureCache.js +18 -1
- package/dist/renderer/cache/GLTextureCache.js.map +1 -1
- package/dist/renderer/cache/cache.js.map +1 -1
- package/dist/renderer/cache/markerCommandCache.js.map +1 -1
- package/dist/renderer/cache/strokeMeshCache.js.map +1 -1
- package/dist/renderer/cullDrawCommands.js +11 -11
- package/dist/renderer/draw/batch.js +133 -0
- package/dist/renderer/draw/batch.js.map +1 -0
- package/dist/renderer/draw/clip.js +89 -0
- package/dist/renderer/draw/clip.js.map +1 -0
- package/dist/renderer/draw/customShader.js +230 -0
- package/dist/renderer/draw/customShader.js.map +1 -0
- package/dist/renderer/draw/drawTriangles.js +8 -0
- package/dist/renderer/draw/drawTriangles.js.map +1 -0
- package/dist/renderer/draw/effects.js +28 -0
- package/dist/renderer/draw/effects.js.map +1 -0
- package/dist/renderer/draw/flushBatch.js +35 -0
- package/dist/renderer/draw/flushBatch.js.map +1 -0
- package/dist/renderer/draw/gradient.js +90 -0
- package/dist/renderer/draw/gradient.js.map +1 -0
- package/dist/renderer/draw/image.js +38 -0
- package/dist/renderer/draw/image.js.map +1 -0
- package/dist/renderer/draw/mesh.js +16 -0
- package/dist/renderer/draw/mesh.js.map +1 -0
- package/dist/renderer/draw/paintBind.js +70 -0
- package/dist/renderer/draw/paintBind.js.map +1 -0
- package/dist/renderer/draw/pathPaint.js +142 -0
- package/dist/renderer/draw/pathPaint.js.map +1 -0
- package/dist/renderer/draw/scratchPass.js +83 -0
- package/dist/renderer/draw/scratchPass.js.map +1 -0
- package/dist/renderer/draw/stencil.js +28 -0
- package/dist/renderer/draw/stencil.js.map +1 -0
- package/dist/renderer/draw/text.js +167 -0
- package/dist/renderer/draw/text.js.map +1 -0
- package/dist/renderer/draw/uniforms.js +77 -0
- package/dist/renderer/draw/uniforms.js.map +1 -0
- package/dist/renderer/draw.js +60 -1116
- package/dist/renderer/draw.js.map +1 -1
- package/dist/renderer/effects/EffectTargets.js +3 -0
- package/dist/renderer/effects/EffectTargets.js.map +1 -1
- package/dist/renderer/effects/composite.js +7 -5
- package/dist/renderer/effects/composite.js.map +1 -1
- package/dist/renderer/index.js +18 -17
- package/dist/renderer/math/color.js +63 -83
- package/dist/renderer/math/color.js.map +1 -1
- package/dist/renderer/math/mat3.js +15 -11
- package/dist/renderer/math/mat3.js.map +1 -1
- package/dist/renderer/math/viewToMat3.js +1 -1
- package/dist/renderer/math/viewToMat3.js.map +1 -1
- package/dist/renderer/shaders/ShaderProgram.js +13 -13
- package/dist/renderer/shaders/ShaderProgram.js.map +1 -1
- package/dist/renderer/shaders/glyphPaint.js +3 -3
- package/dist/renderer/shaders/glyphPaint.js.map +1 -1
- package/dist/renderer/shaders/gradFill.js +2 -1
- package/dist/renderer/shaders/gradFill.js.map +1 -1
- package/dist/renderer/shaders/pathFill.js +2 -1
- package/dist/renderer/shaders/pathFill.js.map +1 -1
- package/dist/renderer/shaders/patternFill.js +2 -1
- package/dist/renderer/shaders/patternFill.js.map +1 -1
- package/dist/renderer/shaders/registerProgram.js.map +1 -1
- package/dist/renderer/strokeLengths.js.map +1 -1
- package/dist/renderer/textures/registerTexture.js +25 -5
- package/dist/renderer/textures/registerTexture.js.map +1 -1
- package/dist/renderer.d.ts +16 -15
- package/dist/renderer.js +17 -16
- package/dist/resize.d.ts +5 -6
- package/dist/resize.js +5 -5
- package/dist/style.css +1 -1
- package/dist/test-hook/install.js +12 -6
- package/dist/test-hook/install.js.map +1 -1
- package/dist/{test-seams-CuIxe0xY.d.ts → test-seams-DM2B87-g.d.ts} +4 -4
- package/dist/test-seams.d.ts +2 -2
- package/dist/tools/builtin/ellipse/useEllipseTool.js +1 -0
- package/dist/tools/builtin/ellipse/useEllipseTool.js.map +1 -1
- package/dist/tools/builtin/hand/useHandTool.js +1 -0
- package/dist/tools/builtin/hand/useHandTool.js.map +1 -1
- package/dist/tools/builtin/lasso/useLassoTool.js +11 -11
- package/dist/tools/builtin/lasso/useLassoTool.js.map +1 -1
- package/dist/tools/builtin/line/useLineTool.js +1 -0
- package/dist/tools/builtin/line/useLineTool.js.map +1 -1
- package/dist/tools/builtin/pen/usePenTool.js +153 -103
- package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
- package/dist/tools/builtin/pencil/usePencilTool.js +1 -0
- package/dist/tools/builtin/pencil/usePencilTool.js.map +1 -1
- package/dist/tools/builtin/polygon/usePolygonTool.js +13 -8
- package/dist/tools/builtin/polygon/usePolygonTool.js.map +1 -1
- package/dist/tools/builtin/rect/useRectTool.js +1 -0
- package/dist/tools/builtin/rect/useRectTool.js.map +1 -1
- package/dist/tools/builtin/select/selectionContributions.js +24 -23
- package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
- package/dist/tools/builtin/select/useSelectTool.js +51 -48
- package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
- package/dist/tools/builtin/shared/pointerFollower.js +17 -0
- package/dist/tools/builtin/shared/pointerFollower.js.map +1 -0
- package/dist/tools/builtin/slice/useSliceTool.js +49 -54
- package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
- package/dist/tools/builtin/star/useStarTool.js +22 -16
- package/dist/tools/builtin/star/useStarTool.js.map +1 -1
- package/dist/tools/builtin/text/useTextTool.js +1 -0
- package/dist/tools/builtin/text/useTextTool.js.map +1 -1
- package/dist/tools/useKeybindings.js +14 -30
- package/dist/tools/useKeybindings.js.map +1 -1
- package/dist/{types-B_9g6hMM.d.ts → types-B4iJRHC3.d.ts} +5 -1
- package/dist/{types-A37hYkeR.d.ts → types-k-5Oqh-U.d.ts} +31 -7
- package/dist/util/color.js +3 -3
- package/dist/util/color.js.map +1 -1
- package/dist/util/paint.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +20 -18
- package/dist/autoPoseDescriptor-vCdt55xm.d.ts +0 -27
- package/dist/builtins-C-VgUlWI.d.ts +0 -4514
- package/dist/core/isPlainObject.js.map +0 -1
- package/dist/debug/flag.js.map +0 -1
- package/dist/interactions/actions/defaults/freshNodeId.js +0 -10
- package/dist/interactions/actions/defaults/freshNodeId.js.map +0 -1
- package/dist/renderer/shaders/imageFill.js +0 -14
- package/dist/renderer/shaders/imageFill.js.map +0 -1
- package/dist/tools/prefs.js +0 -62
- package/dist/tools/prefs.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTextEdit.js","names":["STYLE_SHORTCUTS","UNSHIFT","toggleForKey","raw","key","s","patchForToggle","toggle","current","on","rangeWeight","nodeStyle","nodeHasFlag","styleAtCaret","at","styleAtRange","runs","spliceRunAt","out","pos","placed","r","end","k","run","applyStyleToRange","restoreOverlayFocus","active","overlay","a","charOffsetToDomPosition","range","b","sel","placeCaretBetweenRuns","acc","index","i","offset","span","NO_PENDING","mergePending","next","prev","superseded","supersededKey","patch","value","readSelectionOffsets","start","domPositionToCharOffset","writeRunsPreservingSelection","inTextEntry","runsToDom","base","trimCaretHolder","last","trimmed","head","sameSelection","TEXT_EDIT_FONT_HOLD","useTextEdit","optsRef","useLatest","opts","editingId","setEditingId","useState","editingIdRef","overlayRef","useRef","baseRef","resolveTextStyle","tickRef","frameLoop","useVisibleRaf","initialCaretRef","selection","setSelection","rangeStyle","setRangeStyle","pendingStyle","setPendingStyle","pendingRef","pendingAtRef","armPending","useCallback","empty","dropPending","selectionRef","publishRange","collapsed","domToRuns","syncSelection","clearSelection","cancelEdit","commit","id","priorRuns","setRuns","hadRuns","runsToPlainText","runsCarryStyling","startEdit","isEditing","applyStyleToSelection","setStyle","unboldPatch","unsetFlag","setFlagOverRange","rest","toggleStyle","patchRangeStyle","useEffect","container","style","getStyle","getPaint","overlayClass","OVERLAY_SEQ","initialRuns","getText","appendOverlayText","applyOverlayStyle","hold","holdForFaces","styleEl","installSelectionStyle","clipBox","createClipBox","place","clip","held","release","holdTimer","initial","placeCaretAt","onKeyDown","onBlur","isChrome","onBeforeInput","ie","pending","after","textNode","resplitSmallCaps","normalizeSmallCaps","onInput","onPointerDownOutside","target","cssFontFamilyLoading","numericWeight","p","remaining","walker","root","node","len","el","fontFamily","cssFontFamily","fontString","isCanvasFont","edge","resolveAlign","decorations","SMALL_CAPS_SCALE_PROPERTY","smallCapsScaleFor","decls","ALIGN_ANCHOR","overlayTop","slack","verticalAlignOffset","pose","contentHeight","drop","baselineDrop","canvas","layoutTextPose","css","cssBaseline","CSS_BASELINES","parent","fontWeight","fontStyle","fontSize","lineHeight","textRendering","cached","line","mark","box","scale","baseline","placeOverlay","anchor","script","scriptMetricsFor","aligned","transforms"],"sources":["../../../src/features/text/useTextEdit.ts"],"sourcesContent":["/**\n * In-place text editing via a contenteditable overlay positioned over the\n * text node's screen-space pose. Enter / blur commit; Shift+Enter inserts a\n * newline; Escape cancels. The hook owns the DOM lifecycle of the overlay\n * element — caller just provides a container, the lookup hooks, and a\n * `setText` callback (which is responsible for op-batching/undo).\n *\n * The text layer should hide the node it's currently editing\n * (`isHidden: (n) => n.id === editingId`) so the overlay isn't drawn twice.\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { useVisibleRaf } from '../../scheduling/useVisibleRaf';\nimport type { ResolvedTextStyle, TextStyle } from '@weasel-js/text';\nimport {\n fontString, numericWeight, resolveAlign, resolveTextStyle, scriptMetricsFor, smallCapsScaleFor,\n} from '@weasel-js/text';\nimport { cssFontFamily, cssFontFamilyLoading, isCanvasFont } from '@weasel-js/font';\nimport type { TextPaint, TextVerticalAlign } from '@weasel-js/text';\nimport { layoutTextPose, verticalAlignOffset } from '@weasel-js/text';\nimport type { StyledRun } from '@weasel-js/text';\nimport { runsToPlainText } from '@weasel-js/text';\nimport {\n runsToDom, domToRuns, appendOverlayText, charOffsetToDomPosition, domPositionToCharOffset, normalizeSmallCaps,\n SMALL_CAPS_SCALE_PROPERTY, type OverlayRunBase,\n} from './domRuns';\nimport { applyStyleToRange, patchRangeStyle, runsCarryStyling, styleAtRange, supersededKey } from './runs/rangeStyle';\nimport { nodeHasFlag, setFlagOverRange, unboldPatch, type FlagKey } from './runs/flagRange';\nimport { rangeWeight } from './runs/effectiveRangeStyle';\nimport type { RangeStyle, RunStylePatch } from './runs/rangeStyle';\n\n/**\n * A styling a shortcut or a control toggles. The additive booleans, plus the\n * two `script` values — which are exclusive rather than additive, so they\n * toggle against one enum rather than each owning a boolean.\n */\nexport type StyleToggle = FlagKey | 'super' | 'sub';\n\n/**\n * The shortcut each styling answers to. Strikethrough takes Cmd+Shift+X\n * (Docs' binding) and MUST require shift: bare Cmd+X is cut, and swallowing\n * it here would break cutting text mid-edit. Superscript takes Word's\n * Cmd+Shift+=; subscript takes Cmd+Shift+- rather than Word's Cmd+-, because\n * the unshifted pair is browser zoom, which a page cannot cancel.\n * `overline` has no shortcut — no editor has established one — and is\n * reachable only from a control.\n *\n * Matching is on the *unshifted* character (`shiftedKey`): a US layout\n * reports Cmd+Shift+= as `'+'` and Cmd+Shift+- as `'_'`, so keying the table\n * on what the legend says needs that mapping to survive the shift.\n *\n * Underline has to be intercepted, not merely supported. Left alone, the\n * browser runs its own `formatUnderline` and `domToRuns`' `<u>` flattening\n * makes that *look* like it worked while bypassing `patchForToggle`\n * entirely — no toggle-off, no mixed-range \"turn the whole selection on\"\n * rule, no pending style for a collapsed caret. The flattening stays\n * regardless, since it is what lets pasted decoration survive, but it is\n * defense in depth and was never the mechanism.\n */\nconst STYLE_SHORTCUTS: ReadonlyArray<{ key: string; shift: boolean; toggle: StyleToggle }> = [\n { key: 'b', shift: false, toggle: 'bold' },\n { key: 'i', shift: false, toggle: 'italic' },\n { key: 'u', shift: false, toggle: 'underline' },\n { key: 'x', shift: true, toggle: 'strikethrough' },\n { key: '=', shift: true, toggle: 'super' },\n { key: '-', shift: true, toggle: 'sub' },\n];\n\n/** The characters shift produces from the keys the table names. */\nconst UNSHIFT: Readonly<Record<string, string>> = { '+': '=', _: '-' };\n\n/** The styling `e` toggles, or null when it isn't a styling shortcut. */\nfunction toggleForKey(e: KeyboardEvent): StyleToggle | null {\n if (!e.metaKey && !e.ctrlKey) return null;\n const raw = e.key.toLowerCase();\n const key = UNSHIFT[raw] ?? raw;\n return STYLE_SHORTCUTS.find((s) => s.key === key && s.shift === e.shiftKey)?.toggle ?? null;\n}\n\n/**\n * The patch that toggles `toggle` against the styling in `current`: if it is\n * already carried, clear it; otherwise set it — so a mixed range turns fully\n * on, as in every other text editor. `script` clears to `undefined` rather\n * than to `false`, since the off state of an enum is its absence.\n *\n * `nodeStyle` is read as well as `current`, and has to be: a node flag\n * renders on every run whether or not the runs carry it, so a toggle that\n * consulted the runs alone would read Cmd+B inside a `fontWeight: 700` node\n * as \"not bold\" and *add* bold rather than clearing it. `script` has no node\n * level to consult — by design; see `StyledRun.script`.\n */\nfunction patchForToggle(\n current: RangeStyle,\n toggle: StyleToggle,\n nodeStyle: TextStyle,\n): RunStylePatch {\n if (toggle === 'super' || toggle === 'sub') {\n return { script: current.script === toggle ? undefined : toggle };\n }\n const on = toggle === 'bold'\n ? rangeWeight(current, nodeStyle).bold === true\n : current[toggle] === true || nodeHasFlag(nodeStyle, toggle);\n return { [toggle]: !on };\n}\n\n/**\n * The styling at a collapsed caret: the run to its left, or — at the very\n * start of the text — the run to its right. This is what the next typed\n * character inherits, so it is what a character bar should be showing.\n */\nfunction styleAtCaret(runs: readonly StyledRun[], at: number): RangeStyle {\n return at > 0 ? styleAtRange(runs, at - 1, at) : styleAtRange(runs, 0, 1);\n}\n\n/**\n * Splice `run` into `runs` at character offset `at`, splitting whichever run\n * spans it. The result is normalized (empty runs dropped, identical\n * neighbours merged) by the same algebra every other write goes through.\n */\nfunction spliceRunAt(runs: readonly StyledRun[], at: number, run: StyledRun): StyledRun[] {\n const out: StyledRun[] = [];\n let pos = 0;\n let placed = false;\n for (const r of runs) {\n const end = pos + r.text.length;\n if (!placed && at <= end) {\n const k = at - pos;\n if (k > 0) out.push({ ...r, text: r.text.slice(0, k) });\n out.push(run);\n if (k < r.text.length) out.push({ ...r, text: r.text.slice(k) });\n placed = true;\n } else {\n out.push({ ...r });\n }\n pos = end;\n }\n if (!placed) out.push(run);\n // An empty patch over an empty range: this is the array-wide normalization\n // pass, not a styling.\n return applyStyleToRange(out, 0, 0, {});\n}\n\n/**\n * Return focus to the overlay after a styling arrived from editor chrome.\n *\n * A control that took focus in order to be clicked still holds it when the\n * patch lands, so the next keystroke goes to that button — and in an app that\n * binds bare letters to tools, \"click Superscript, then type the 2\" activates\n * the Text tool and pans the canvas instead of typing a 2. Restoring the\n * caret is what makes a styling control usable mid-edit at all.\n *\n * Two things are left holding focus deliberately: a text entry in the chrome\n * (the user is still typing a font size — stealing that is worse than the\n * problem) and anything inside an open dialog, which is where a color\n * picker's live `onChange` fires from.\n */\nfunction restoreOverlayFocus(overlay: HTMLElement, range: TextEditSelection): void {\n const active = document.activeElement;\n if (active === overlay || overlay.contains(active)) return;\n if (\n active instanceof HTMLInputElement\n || active instanceof HTMLTextAreaElement\n || (active instanceof HTMLElement && active.isContentEditable)\n || (active instanceof Element && active.closest('[role=\"dialog\"]') !== null)\n ) return;\n overlay.focus();\n const a = charOffsetToDomPosition(overlay, range.start);\n const b = charOffsetToDomPosition(overlay, range.end);\n const sel = window.getSelection();\n if (!a || !b || !sel) return;\n const r = document.createRange();\n r.setStart(a.node, a.offset);\n r.setEnd(b.node, b.offset);\n sel.removeAllRanges();\n sel.addRange(r);\n}\n\n/**\n * Put the caret *between* two run spans when `offset` falls on the boundary\n * of one, rather than at the same offset inside the left span's text node.\n *\n * The two positions are the same character offset and browsers report them\n * identically, but they type differently: a caret inside a span has the next\n * character absorbed by that run, and one between spans starts a fresh one.\n * That distinction is the whole point after a pending style has just been\n * committed to its own run — otherwise the character after a superscript is\n * superscript too. Leaves the selection alone when the offset falls mid-run,\n * where there is no boundary to prefer.\n */\nfunction placeCaretBetweenRuns(\n overlay: HTMLElement,\n runs: readonly StyledRun[],\n offset: number,\n): void {\n let acc = 0;\n let index = -1;\n for (let i = 0; i < runs.length; i++) {\n acc += runs[i].text.length;\n if (acc === offset) { index = i; break; }\n if (acc > offset) return;\n }\n const span = index >= 0 ? overlay.children[index] : null;\n const sel = window.getSelection();\n if (!span || !sel) return;\n const range = document.createRange();\n range.setStartAfter(span);\n range.collapse(true);\n sel.removeAllRanges();\n sel.addRange(range);\n}\n\n/** No pending style. One frozen object so re-publishing an already-empty\n * pending style doesn't re-render every consumer. */\nconst NO_PENDING: RunStylePatch = Object.freeze({});\n\n/**\n * Merge `patch` into a pending style. A key set to `false` or `undefined` is\n * deleted rather than stored — the same canonical form `patchRun` keeps, so\n * the pending style and a stored run agree on what \"off\" looks like, and\n * bold and a weight supersede each other here as they do there.\n */\nfunction mergePending(prev: RunStylePatch, patch: RunStylePatch): RunStylePatch {\n const next: Record<string, unknown> = { ...prev };\n const superseded = supersededKey(patch);\n if (superseded !== undefined) delete next[superseded];\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined || value === false) delete next[key];\n else next[key] = value;\n }\n return next as RunStylePatch;\n}\n\n/**\n * The caret's character range within the text being edited. Half-open\n * `[start, end)` over the concatenated run text, normalized so `start <= end`\n * regardless of which way the user dragged. `start === end` is a collapsed\n * caret — a real position, not the absence of one, which is why the hook\n * reports `null` rather than a zero-width range when there is no caret.\n */\nexport interface TextEditSelection {\n start: number;\n end: number;\n}\n\n/**\n * Character offsets of the current DOM selection within `overlay`, or `null`\n * when there is no selection or it lies outside the overlay. The one\n * DOM→offset conversion in the hook; everything that needs offsets goes\n * through here.\n */\nfunction readSelectionOffsets(overlay: HTMLElement): TextEditSelection | null {\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return null;\n const range = sel.getRangeAt(0);\n if (!overlay.contains(range.startContainer) || !overlay.contains(range.endContainer)) {\n return null;\n }\n // `Range` boundary points are already in document order, so this is the\n // normalization for a backwards drag — anchor/focus are not consulted.\n const start = domPositionToCharOffset(overlay, range.startContainer, range.startOffset);\n const end = domPositionToCharOffset(overlay, range.endContainer, range.endOffset);\n return start <= end ? { start, end } : { start: end, end: start };\n}\n\n/**\n * Replace the overlay's contents with `runs` and put the caret back on\n * `[start, end)`. Rewriting the DOM destroys the selection, and losing it\n * after every styling change would mean re-selecting the word to apply a\n * second style — so this is the only way styling writes back.\n *\n * The caret is **not** restored while focus sits in a text-entry control.\n * Browsers route editing commands by selection rather than by focus, so a\n * document selection put back inside the contenteditable while a bar's\n * number field has focus means the very Enter that committed that field also\n * runs its `insertParagraph` default over the restored range — replacing the\n * text that was just styled with a line break. (Observed in Chrome.)\n *\n * Focus on a *button* is the opposite case and must still restore: toggling\n * bold, then italic, then underline is one flow over one selection, and\n * clicking a toolbar button does take focus. Buttons turn keystrokes into\n * clicks, not into editing commands, so the range is safe there.\n *\n * The hook remembers the range as character offsets either way, so a skipped\n * restore costs the highlight, never the target.\n */\nfunction writeRunsPreservingSelection(\n overlay: HTMLElement,\n runs: readonly StyledRun[],\n start: number,\n end: number,\n base: OverlayRunBase,\n): void {\n const active = document.activeElement;\n const inTextEntry =\n active !== overlay &&\n !overlay.contains(active) &&\n (active instanceof HTMLInputElement ||\n active instanceof HTMLTextAreaElement ||\n (active instanceof HTMLElement && active.isContentEditable));\n runsToDom(runs, overlay, base);\n if (inTextEntry) return;\n const a = charOffsetToDomPosition(overlay, start);\n const b = charOffsetToDomPosition(overlay, end);\n const sel = window.getSelection();\n if (!a || !b || !sel) return;\n const range = document.createRange();\n range.setStart(a.node, a.offset);\n range.setEnd(b.node, b.offset);\n sel.removeAllRanges();\n sel.addRange(range);\n}\n\n/**\n * Drop the one trailing newline a contenteditable keeps so the caret has\n * somewhere to sit on the last line. `domToRuns` maps the `<br>` to a\n * literal `'\\n'`, which the edit never typed. Exactly one, so a newline\n * the user actually typed survives — the holder is never doubled.\n *\n * An empty run left behind is dropped: it carries no text and would otherwise\n * defeat `runsCarryStyling`'s \"did this edit produce styling\" question by\n * existing.\n */\nfunction trimCaretHolder(runs: StyledRun[]): StyledRun[] {\n const last = runs[runs.length - 1];\n if (last === undefined || !last.text.endsWith('\\n')) return runs;\n const trimmed = { ...last, text: last.text.slice(0, -1) };\n const head = runs.slice(0, -1);\n return trimmed.text === '' ? head : [...head, trimmed];\n}\n\nfunction sameSelection(a: TextEditSelection | null, b: TextEditSelection | null): boolean {\n if (a === null || b === null) return a === b;\n return a.start === b.start && a.end === b.end;\n}\n\n/** Screen-space pose passed to `useTextEdit` so the overlay can be placed and sized in CSS pixels. */\nexport interface TextEditScreenPose {\n /** Top-left in CSS pixels relative to `container`. Always screen pixels,\n * including when `zoom` is set — the scale is anchored at this point, not\n * translated by it. */\n x: number;\n y: number;\n /** Pre-scale — CSS pixels, or world units when `zoom` is set. */\n width: number;\n height: number;\n /** Pre-scale font size: `style.fontSize * zoom` when `zoom` is omitted, the\n * world-unit `style.fontSize` when it is set. */\n fontSize: number;\n /** Effective on-screen line height multiplier (defaults to style.lineHeight). */\n lineHeight?: number;\n /**\n * Where the text sits within `height`, as the canvas places it. `'center'`\n * and `'bottom'` size the overlay to its own content and offset it by the\n * slack, re-measured as typing changes the line count. Default `'top'`.\n */\n verticalAlign?: TextVerticalAlign;\n /**\n * CSS scale applied to the overlay (`transform: scale(zoom)`, anchored at\n * its top-left). Every other size on this pose, and every typographic\n * metric the hook writes, is then **pre-scale** — pass world units and the\n * transform does the world→screen conversion.\n *\n * This is the only way run-level typography can be correct at a zoom other\n * than 1. `runsToDom` emits run `fontSize` / `letterSpacing` in world units\n * and `domToRuns` reads them straight back; threading a scale through the\n * writer and its inverse through the reader would go lossy on fractional\n * zooms. Scaling the whole overlay instead leaves that serializer pure and\n * scales node-level and run-level values by the same factor for free.\n *\n * Omit it (the default, `1`) and the pose is plain screen pixels — the hook\n * then infers the world→screen factor from `fontSize / style.fontSize` to\n * scale node-level `letterSpacing`, and run-level overrides are left\n * unscaled. Correct only at zoom 1.\n */\n zoom?: number;\n}\n\n/** A box in CSS pixels relative to `useTextEdit`'s `container`. */\nexport interface TextEditClipRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** Options for `useTextEdit`. */\nexport interface UseTextEditOptions {\n /** Element the overlay is appended to. Must be `position: relative`/absolute. */\n container: HTMLElement | null;\n /** Read the current text for `id`. */\n getText: (id: string) => string;\n /** Read style for `id` (used for font setup on the overlay). */\n getStyle: (id: string) => TextStyle | undefined;\n /**\n * Read the node's paint for `id` — its `data.fill` / `data.stroke`. The\n * overlay paints its text and caret from the fill, so omitting this\n * renders every node's editor in the default black however the node\n * itself is painted.\n */\n getPaint?: (id: string) => TextPaint | undefined;\n /**\n * Write style back for `id`. Optional; needed only to turn a flag **off**\n * inside a node whose own `TextStyle` sets it. Run flags are additive, so\n * that edit is expressed by lowering the node flag and raising it on the\n * runs outside the selection — which needs somewhere to put the new node\n * style. Without this, such a toggle is declined and nothing changes, as\n * before. See `runs/flagRange`.\n */\n setStyle?: (id: string, style: TextStyle) => void;\n /** Read screen-space pose for `id`. Called per frame while editing. */\n getScreenPose: (id: string) => TextEditScreenPose | null;\n /**\n * The box, in CSS pixels relative to `container`, the overlay is visible\n * within — where the canvas clips the glyphs the overlay stands in for.\n * Re-read every frame. Omit it, or return `null`, to clip nothing.\n *\n * The clip cannot scroll, so a caret moving past its edge leaves the page\n * where it is; a scrollable container would drag the canvas after the caret.\n */\n getClipRect?: () => TextEditClipRect | null;\n /** Commit text. Caller wraps in op/undo. */\n setText: (id: string, text: string) => void;\n /**\n * Optional: read the current rich-text runs for `id`. When provided AND\n * returns a non-empty array, the overlay renders the runs as styled\n * `<span data-run>` elements; otherwise the overlay is seeded with plain\n * text via `getText`.\n */\n getRuns?: (id: string) => readonly StyledRun[] | undefined;\n /**\n * Optional: commit rich-text runs back to the node. When omitted, only\n * `setText` is called with the plain-text form on commit. When provided,\n * commit calls both `setText` (with `runsToPlainText(runs)`) and\n * `setRuns(id, runs)` — but only when runs are actually in play: the node\n * already had some, or the edit produced styling. A plain-text edit of a\n * plain-text node still calls `setText` alone, so a node that has never\n * been styled doesn't grow a single-run `runs` array just for being edited.\n */\n setRuns?: (id: string, runs: StyledRun[]) => void;\n /**\n * Is `el` part of the editor's own chrome — a character-options bar, a\n * color popover, anything whose whole purpose is to style the text being\n * edited? Focus moving into one does not end the edit.\n *\n * Without this, the controls the feature exists for are exactly what\n * destroys it: clicking a size field blurs the overlay, blur commits, and\n * the caret the control was about to act on is gone. Toggle buttons can\n * dodge it by `preventDefault()`-ing their own mousedown, but a field the\n * user has to type into cannot.\n *\n * Chrome focus does not disturb the reported `selection` either — see\n * `UseTextEditReturn.selection`.\n */\n isEditorChrome?: (el: Element) => boolean;\n /** What Escape does to the edit: `'cancel'` (the default) drops it,\n * `'commit'` keeps the text as Enter would. */\n escape?: 'cancel' | 'commit';\n /**\n * How long, in ms, an edit opening waits for the overlay's face before\n * showing the text in the fallback. Defaults to {@link TEXT_EDIT_FONT_HOLD}.\n *\n * The overlay stands in for canvas text the host has hidden, so the wait\n * shows nothing at all; past it, the fallback shows and reflows to the real\n * face when that lands. A face already loaded — the usual case, since\n * `@weasel-js/font` builds it as soon as its bytes arrive — shows at once.\n * `0` never waits.\n */\n fontHold?: number;\n}\n\n/**\n * The default {@link UseTextEditOptions.fontHold}: at 100ms a blank edit\n * still reads as an instant response, and it is the block period CSS gives\n * `font-display: fallback` for the same trade.\n */\nexport const TEXT_EDIT_FONT_HOLD = 100;\n\n/** Options for `useTextEdit().startEdit`. */\nexport interface StartEditOptions {\n /**\n * Where to place the caret on edit start. Number = caret offset (0..text.length);\n * `'all'` selects the whole text (default — preserves the prior behavior).\n */\n caret?: number | 'all';\n}\n\n/** Return shape of `useTextEdit`. */\nexport interface UseTextEditReturn {\n editingId: string | null;\n startEdit: (id: string, opts?: StartEditOptions) => void;\n cancelEdit: () => void;\n commit: () => void;\n isEditing: (id: string) => boolean;\n /**\n * The caret's character range, or `null` when nothing is being edited. A\n * collapsed caret reports `{ start: n, end: n }`, so `null` and \"caret at\n * n\" stay distinguishable — a styling written at a collapsed caret arms\n * {@link UseTextEditReturn.pendingStyle} instead of restyling a range.\n *\n * Follows the DOM selection, which browsers (and jsdom) report from a task\n * rather than synchronously; anything this hook writes itself updates it\n * synchronously. A DOM selection that leaves the overlay does **not** clear\n * it: that is what happens when the user clicks a styling control, and\n * reporting `null` there would read as \"collapsed caret\" and send the\n * control's patch to the node instead of to the range. It is cleared on\n * `startEdit` and when the edit ends.\n */\n selection: TextEditSelection | null;\n /**\n * The styling shared by every run in `selection` — a concrete value where\n * the range agrees, `MIXED` where it doesn't. `null` exactly when\n * `selection` is `null`. A collapsed caret reports the styling *at* the\n * caret (the run to its left, or the run to its right at offset 0), which\n * is what the next typed character inherits.\n *\n * This does not include {@link UseTextEditReturn.pendingStyle}. A control\n * showing what the next character will look like wants both, merged in\n * that order; one showing what is already written wants only this.\n */\n rangeStyle: RangeStyle | null;\n /**\n * Styling armed at a collapsed caret, applied to the next character typed\n * and then dropped. `{}` when nothing is armed — which is always the case\n * while `selection` covers a real range, since a range is styled directly.\n *\n * Moving the caret abandons it, as in any other editor.\n */\n pendingStyle: RunStylePatch;\n /**\n * Write `patch` over `selection`. A no-op with no active edit or an empty\n * patch. Over a real range this restyles the runs under it; at a collapsed\n * caret it merges into `pendingStyle` instead, so the styling lands on\n * what gets typed next rather than on text the user didn't select.\n *\n * Lowering a flag the *node* sets is neither of those — a run cannot say\n * \"not italic\" — so it rewrites instead: the node flag is cleared and raised\n * on every run outside the range. Bold is spared that: a run's own\n * `fontWeight` can say \"regular\", so it is written over the range.\n *\n * The caret survives, so a second style can be applied without\n * re-selecting, and `rangeStyle` reflects the write before this returns.\n */\n applyStyleToSelection: (patch: RunStylePatch) => void;\n /**\n * Toggle one styling over `selection` — set it if the range doesn't\n * uniformly carry it, clear it if it does. The shape a B / I / x² control\n * or a keyboard shortcut wants, and what this hook's own shortcuts call.\n */\n toggleStyle: (toggle: StyleToggle) => void;\n}\n\n/** In-place text editing via a contenteditable overlay positioned over the text node's screen-space pose. */\nexport function useTextEdit(\n opts: UseTextEditOptions,\n): UseTextEditReturn {\n const optsRef = useLatest(opts);\n\n const [editingId, setEditingId] = useState<string | null>(null);\n // `applyStyleToSelection` needs the node being edited to reach its\n // `TextStyle`, and is called from event handlers rather than from render.\n const editingIdRef = useLatest(editingId);\n const overlayRef = useRef<HTMLDivElement | null>(null);\n /** The editing node's resolved style, which run spans inheriting small caps\n * are split by whenever the runs are rewritten. */\n const baseRef = useRef<OverlayRunBase>(resolveTextStyle(undefined));\n // The overlay-follow loop is built inside the editing effect; the gate's\n // frame callback reaches it through this ref.\n const tickRef = useRef<() => void>(() => {});\n const frameLoop = useVisibleRaf(() => { tickRef.current(); });\n const initialCaretRef = useRef<number | 'all'>('all');\n const [selection, setSelection] = useState<TextEditSelection | null>(null);\n const [rangeStyle, setRangeStyle] = useState<RangeStyle | null>(null);\n const [pendingStyle, setPendingStyle] = useState<RunStylePatch>(NO_PENDING);\n /** The pending style, read synchronously by `beforeinput` — which fires\n * well before React has re-rendered with the state above. */\n const pendingRef = useRef<RunStylePatch>(NO_PENDING);\n /** The caret offset the pending style was armed at, or `null` when nothing\n * is armed. Publishing any other range abandons it. */\n const pendingAtRef = useRef<number | null>(null);\n\n const armPending = useCallback((next: RunStylePatch, at: number) => {\n const empty = Object.keys(next).length === 0;\n pendingRef.current = empty ? NO_PENDING : next;\n pendingAtRef.current = empty ? null : at;\n setPendingStyle(pendingRef.current);\n }, []);\n\n const dropPending = useCallback(() => {\n if (pendingAtRef.current === null) return;\n pendingRef.current = NO_PENDING;\n pendingAtRef.current = null;\n setPendingStyle(NO_PENDING);\n }, []);\n\n /**\n * The published range, mirrored in a ref. `applyStyleToSelection` reads it\n * synchronously, and it is the fallback when the DOM selection has moved\n * into editing chrome. Cleared with the state, never separately.\n */\n const selectionRef = useRef<TextEditSelection | null>(null);\n\n /**\n * Recompute the published caret range and its styling from the overlay.\n * Called from the `selectionchange` listener and directly after anything\n * the hook writes, so a control reading `rangeStyle` right after a patch\n * sees the new value rather than flickering back to the old one.\n *\n * A selection that is not inside the overlay leaves the published range\n * alone rather than clearing it. That state means focus moved into a\n * control — which is precisely when the range matters most, and reporting\n * `null` would read to a consumer as \"collapsed caret\" and send its patch\n * to the wrong target. The range is cleared where it is actually over: on\n * `startEdit` and on teardown.\n */\n /** Publish `range` and the styling the overlay currently carries across it. */\n const publishRange = useCallback((overlay: HTMLElement, range: TextEditSelection) => {\n const collapsed = range.start === range.end;\n // A pending style belongs to the caret position it was armed at. Any\n // other range means the caret moved, which abandons it.\n if (pendingAtRef.current !== null\n && !(collapsed && range.start === pendingAtRef.current)) {\n dropPending();\n }\n selectionRef.current = range;\n setSelection((prev) => (sameSelection(prev, range) ? prev : range));\n const runs = domToRuns(overlay);\n setRangeStyle(collapsed\n ? styleAtCaret(runs, range.start)\n : styleAtRange(runs, range.start, range.end));\n }, [dropPending]);\n\n const syncSelection = useCallback(() => {\n const overlay = overlayRef.current;\n if (!overlay) return;\n const next = readSelectionOffsets(overlay);\n if (next === null) return;\n publishRange(overlay, next);\n }, [publishRange]);\n\n /** Drop the published range — the edit session it described is over. */\n const clearSelection = useCallback(() => {\n selectionRef.current = null;\n setSelection(null);\n setRangeStyle(null);\n dropPending();\n }, [dropPending]);\n\n const cancelEdit = useCallback(() => {\n setEditingId(null);\n }, []);\n\n const commit = useCallback(() => {\n const id = editingId;\n const overlay = overlayRef.current;\n if (id == null || !overlay) {\n setEditingId(null);\n return;\n }\n // What decides this is whether *runs* are in play at the end of the\n // edit — not whether the overlay was seeded from them. Mirroring the\n // init-time guard (`getRuns` returned something non-empty) was the old\n // rule, and it silently dropped every styling a freshly created text\n // node acquired: no prior runs, so commit took the plain-text branch and\n // `domToRuns`' output went in the bin. So two ways in:\n //\n // - the node already had runs — they have to be rewritten even when the\n // edit ended up unstyled, or stale styling survives a Cmd-B that\n // turned it off; or\n // - the edit produced styling, whatever the node started as.\n //\n // A plain-text edit of a plain-text node satisfies neither and commits\n // text alone, so a node that has no `runs` doesn't grow a single-run\n // array just for being edited.\n const priorRuns = optsRef.current.getRuns?.(id);\n const setRuns = optsRef.current.setRuns;\n const runs = trimCaretHolder(domToRuns(overlay));\n const hadRuns = priorRuns != null && priorRuns.length > 0;\n // Read from the DOM, not `innerText`: a browser applies `text-transform`\n // to what that reports, and a break stand-in reads back as its `\\n`.\n optsRef.current.setText(id, runsToPlainText(runs));\n if (setRuns && (hadRuns || runsCarryStyling(runs))) setRuns(id, runs);\n setEditingId(null);\n }, [editingId, optsRef]);\n\n const startEdit = useCallback((id: string, opts?: StartEditOptions) => {\n initialCaretRef.current = opts?.caret ?? 'all';\n setEditingId(id);\n }, []);\n\n const isEditing = useCallback((id: string) => editingId === id, [editingId]);\n\n const applyStyleToSelection = useCallback((patch: RunStylePatch) => {\n const overlay = overlayRef.current;\n if (!overlay) return;\n // An empty patch would still round-trip the runs through\n // `applyStyleToRange`, whose normalization applies to the whole array and\n // not just the patched span. Nothing asked for that, so don't do it.\n if (Object.keys(patch).length === 0) return;\n // Fall back to the last range that WAS in the overlay: the caller may be\n // a control that took focus (and with it the DOM selection) to be\n // clicked. `writeRunsPreservingSelection` puts the range back afterwards,\n // so a second styling can follow without re-selecting either way.\n const range = readSelectionOffsets(overlay) ?? selectionRef.current;\n if (!range) return;\n\n // Lowering a flag the node itself sets is not expressible additively —\n // the run algebra can only add — so it takes the rewrite in\n // `setFlagOverRange`: clear the node flag, raise it outside the range.\n // This is the one styling that reaches existing text from a collapsed\n // caret, and has to: \"stop being bold from here on\" is otherwise\n // unsayable in a bold node.\n const id = editingIdRef.current;\n const setStyle = optsRef.current.setStyle;\n const runs = domToRuns(overlay);\n if (patch.bold === false && !('fontWeight' in patch) && id !== null) {\n patch = { ...patch, ...unboldPatch(optsRef.current.getStyle(id) ?? {}) };\n }\n const unsetFlag = id !== null && setStyle\n ? (Object.keys(patch) as FlagKey[]).find((key) =>\n key !== 'bold'\n && patch[key] === false\n && nodeHasFlag(optsRef.current.getStyle(id) ?? {}, key))\n : undefined;\n if (unsetFlag !== undefined && id !== null && setStyle) {\n const nodeStyle = optsRef.current.getStyle(id) ?? {};\n const r = setFlagOverRange(runs, nodeStyle, range.start, range.end, unsetFlag, false);\n setStyle(id, r.style);\n writeRunsPreservingSelection(overlay, r.runs, range.start, range.end, baseRef.current);\n publishRange(overlay, range);\n // The rewrite raised the flag on every existing run, so a caret sitting\n // at the end of one is now *inside* a flagged span and the next\n // character would be absorbed by it. Arming the `false` keeps it out:\n // it is not a storable run value, but it is enough to make\n // `beforeinput` open a fresh span, which `runsToDom` then emits bare.\n if (range.start === range.end) {\n armPending({ ...pendingRef.current, [unsetFlag]: false }, range.start);\n }\n restoreOverlayFocus(overlay, range);\n // Whatever else the patch carried still applies on its own terms.\n const rest = { ...patch };\n delete rest[unsetFlag];\n if (Object.keys(rest).length > 0) applyStyleToSelection(rest);\n return;\n }\n\n // A collapsed caret has no run to restyle, so the styling is armed for\n // the next character instead of silently doing nothing.\n if (range.start === range.end) {\n armPending(mergePending(pendingRef.current, patch), range.start);\n restoreOverlayFocus(overlay, range);\n return;\n }\n\n const next = applyStyleToRange(runs, range.start, range.end, patch);\n writeRunsPreservingSelection(overlay, next, range.start, range.end, baseRef.current);\n restoreOverlayFocus(overlay, range);\n // Republish from the range we just styled, not from the DOM selection:\n // when the patch came from chrome the selection is in that control, and\n // re-reading it would leave a consumer showing the pre-patch styling.\n publishRange(overlay, range);\n }, [publishRange, armPending, editingIdRef, optsRef]);\n\n const toggleStyle = useCallback((toggle: StyleToggle) => {\n const overlay = overlayRef.current;\n if (!overlay) return;\n const range = readSelectionOffsets(overlay) ?? selectionRef.current;\n if (!range) return;\n const runs = domToRuns(overlay);\n const id = editingIdRef.current;\n const nodeStyle = (id !== null ? optsRef.current.getStyle(id) : undefined) ?? {};\n // At a collapsed caret the styling in play is what the caret sits in plus\n // whatever is already armed, so pressing Cmd+B twice disarms rather than\n // arming a second time.\n const current = range.start === range.end\n ? patchRangeStyle(styleAtCaret(runs, range.start), pendingRef.current)\n : styleAtRange(runs, range.start, range.end);\n applyStyleToSelection(patchForToggle(current, toggle, nodeStyle));\n }, [applyStyleToSelection, editingIdRef, optsRef]);\n\n useEffect(() => {\n if (editingId == null) return;\n const { container, getText, getStyle, getPaint } = optsRef.current;\n if (!container) return;\n\n const style = resolveTextStyle(getStyle(editingId), getPaint?.(editingId));\n const overlay = document.createElement('div');\n const overlayClass = `weasel-text-edit-${++OVERLAY_SEQ}`;\n overlay.classList.add(overlayClass);\n overlay.setAttribute('contenteditable', 'true');\n overlay.spellcheck = false;\n baseRef.current = style;\n const initialRuns = optsRef.current.getRuns?.(editingId);\n if (initialRuns && initialRuns.length > 0) {\n runsToDom(initialRuns, overlay, style);\n } else if (style.fontVariantCaps === 'small-caps') {\n // Small caps needs its lowercase in pieces, which only a run span holds.\n runsToDom([{ text: getText(editingId) }], overlay, style);\n } else {\n // Not `innerText`, which turns each newline into a `<br>` the caret\n // walkers cannot count and U+2028 into a space.\n appendOverlayText(overlay, getText(editingId));\n }\n applyOverlayStyle(overlay, style);\n const hold = holdForFaces(style, initialRuns ?? []);\n if (hold) overlay.style.opacity = '0';\n const styleEl = installSelectionStyle(overlayClass, style);\n const clipBox = createClipBox();\n clipBox.appendChild(overlay);\n container.appendChild(clipBox);\n overlayRef.current = overlay;\n\n const place = () => {\n const clip = optsRef.current.getClipRect?.() ?? null;\n placeOverlay(overlay, clipBox, optsRef.current.getScreenPose(editingId), clip, style);\n };\n place();\n // Measured again on release: the first `place` ran in the fallback face.\n let held = hold !== null;\n const release = () => {\n if (!held) return;\n held = false;\n clearTimeout(holdTimer);\n place();\n overlay.style.opacity = '';\n };\n const holdTimer = held ? setTimeout(release, optsRef.current.fontHold ?? TEXT_EDIT_FONT_HOLD) : undefined;\n void hold?.then(release);\n\n const range = document.createRange();\n const initial = initialCaretRef.current;\n if (initial === 'all') {\n range.selectNodeContents(overlay);\n } else {\n placeCaretAt(overlay, range, initial);\n }\n // Focus first, then place the caret: focusing an editable host is itself\n // a selection-moving act (jsdom collapses to the start; browsers vary),\n // so a range set beforehand is not reliably the one the user ends up with.\n overlay.focus();\n const sel = window.getSelection();\n sel?.removeAllRanges();\n sel?.addRange(range);\n // The caret exists now, so publish it rather than waiting for the\n // `selectionchange` task — a control mounted alongside the editor would\n // otherwise render one frame with no selection.\n syncSelection();\n\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n commit();\n return;\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n if (optsRef.current.escape === 'commit') commit();\n else cancelEdit();\n return;\n }\n const toggle = toggleForKey(e);\n if (toggle) {\n e.preventDefault();\n toggleStyle(toggle);\n }\n };\n const onBlur = (e: FocusEvent) => {\n const next = e.relatedTarget;\n const isChrome = optsRef.current.isEditorChrome;\n if (isChrome && next instanceof Element && isChrome(next)) return;\n commit();\n };\n\n const onBeforeInput = (ie: InputEvent) => {\n if (ie.inputType !== 'insertText' || !ie.data) return;\n const pending = pendingRef.current;\n if (Object.keys(pending).length > 0) {\n // Rebuild from the runs rather than splicing a span into the DOM.\n // `insertNode` at a caret sitting inside a run span nests the new\n // span *within* it, and `domToRuns` reads styling down the ancestor\n // chain — so a pending style that has to escape the surrounding run\n // (the `false` armed after a node-flag rewrite) would inherit exactly\n // the flag it exists to shed. Going through the algebra also gets\n // every run field for free, with no CSS written here.\n const at = readSelectionOffsets(overlay) ?? selectionRef.current;\n if (!at || at.start !== at.end) return;\n ie.preventDefault();\n const next = spliceRunAt(domToRuns(overlay), at.start, {\n ...pending, text: ie.data,\n } as StyledRun);\n const after = at.start + ie.data.length;\n writeRunsPreservingSelection(overlay, next, after, after, baseRef.current);\n placeCaretBetweenRuns(overlay, next, after);\n dropPending();\n publishRange(overlay, { start: after, end: after });\n return;\n }\n ie.preventDefault();\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return;\n const range = sel.getRangeAt(0);\n range.deleteContents();\n {\n // No pending style — insert the character as a plain text node at the\n // caret position so the surrounding run's span absorbs it.\n const textNode = document.createTextNode(ie.data);\n range.insertNode(textNode);\n const after = document.createRange();\n after.setStartAfter(textNode);\n after.collapse(true);\n sel.removeAllRanges();\n sel.addRange(after);\n }\n resplitSmallCaps();\n };\n\n /** Put small-caps letters back in the pieces their case belongs in after\n * an edit, keeping the caret. Never mid-composition: rewriting the DOM\n * under an IME ends the composition. */\n const resplitSmallCaps = (e?: Event) => {\n if (e instanceof InputEvent && e.isComposing) return;\n const at = readSelectionOffsets(overlay);\n if (!normalizeSmallCaps(overlay) || !at) return;\n const a = charOffsetToDomPosition(overlay, at.start);\n const b = charOffsetToDomPosition(overlay, at.end);\n const sel = window.getSelection();\n if (!a || !b || !sel) return;\n const r = document.createRange();\n r.setStart(a.node, a.offset);\n r.setEnd(b.node, b.offset);\n sel.removeAllRanges();\n sel.addRange(r);\n };\n const onInput = (e: Event) => {\n resplitSmallCaps(e);\n place();\n };\n\n // Blur can't be the only way out. Once focus has moved into editing\n // chrome the overlay is no longer focused, so nothing blurs when the user\n // clicks away and the edit would never end. A pointerdown that lands\n // outside both the overlay and the chrome is that click. Capture phase so\n // it runs ahead of whatever the click was aimed at (a canvas gesture, a\n // different tool) rather than after it.\n const onPointerDownOutside = (e: Event) => {\n const target = e.target;\n if (!(target instanceof Node) || overlay.contains(target)) return;\n const isChrome = optsRef.current.isEditorChrome;\n if (isChrome && target instanceof Element && isChrome(target)) return;\n commit();\n };\n\n overlay.addEventListener('keydown', onKeyDown);\n overlay.addEventListener('blur', onBlur);\n // Typing can change the line count; re-placing here rather than on the\n // next frame keeps a center/bottom-aligned overlay from jumping a frame late.\n overlay.addEventListener('input', onInput);\n overlay.addEventListener('beforeinput', onBeforeInput);\n document.addEventListener('pointerdown', onPointerDownOutside, true);\n // `selectionchange` only exists on the document — there is no per-element\n // event for it — so this listens globally and `readSelectionOffsets`\n // filters to selections inside the overlay.\n document.addEventListener('selectionchange', syncSelection);\n\n // Follows the node's screen pose every frame — behind the visibility gate,\n // so an overlay left open in a background tab costs nothing.\n tickRef.current = () => {\n place();\n frameLoop.request();\n };\n frameLoop.request();\n\n return () => {\n held = false;\n clearTimeout(holdTimer);\n frameLoop.cancel();\n tickRef.current = () => {};\n overlay.removeEventListener('keydown', onKeyDown);\n overlay.removeEventListener('blur', onBlur);\n overlay.removeEventListener('input', onInput);\n overlay.removeEventListener('beforeinput', onBeforeInput);\n document.removeEventListener('pointerdown', onPointerDownOutside, true);\n document.removeEventListener('selectionchange', syncSelection);\n clipBox.remove();\n styleEl?.remove();\n overlayRef.current = null;\n clearSelection();\n };\n }, [editingId, optsRef, commit, cancelEdit, syncSelection, clearSelection, frameLoop,\n toggleStyle, dropPending, publishRange]);\n\n return {\n editingId, startEdit, cancelEdit, commit, isEditing,\n selection, rangeStyle, pendingStyle, applyStyleToSelection, toggleStyle,\n };\n}\n\nlet OVERLAY_SEQ = 0;\n\n/**\n * The loads the overlay's faces are still waiting on — the node's and each\n * run's that names its own family — or `null` when every one is in hand.\n */\nfunction holdForFaces(style: ResolvedTextStyle, runs: readonly StyledRun[]): Promise<unknown> | null {\n const pending = [\n cssFontFamilyLoading(style.fontFamily, {\n weight: numericWeight(style.fontWeight), style: style.fontStyle,\n }),\n ...runs.filter((r) => r.fontFamily != null).map((r) => cssFontFamilyLoading(r.fontFamily!, {\n weight: r.fontWeight ?? (r.bold ? 700 : undefined),\n style: r.italic ? 'italic' : undefined,\n })),\n ].filter((p) => p !== null);\n return pending.length === 0 ? null : Promise.all(pending);\n}\n\n/**\n * Place a collapsed caret `offset` characters into the overlay's text,\n * walking its text nodes. Out-of-range offsets clamp to the end.\n */\nfunction placeCaretAt(root: HTMLElement, range: Range, offset: number): void {\n let remaining = offset;\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);\n let node: Text | null = walker.nextNode() as Text | null;\n while (node) {\n const len = node.data.length;\n if (remaining <= len) {\n range.setStart(node, remaining);\n range.setEnd(node, remaining);\n return;\n }\n remaining -= len;\n node = walker.nextNode() as Text | null;\n }\n // Fallback: collapse at end of the root.\n range.selectNodeContents(root);\n range.collapse(false);\n}\n\n/** The overlay's positioning parent. Passes pointer input through everywhere\n * but the overlay itself, which spans no more than the canvas it covers. */\nfunction createClipBox(): HTMLDivElement {\n const el = document.createElement('div');\n el.style.position = 'absolute';\n el.style.margin = '0';\n el.style.padding = '0';\n el.style.border = '0';\n el.style.pointerEvents = 'none';\n return el;\n}\n\nfunction applyOverlayStyle(el: HTMLDivElement, style: ResolvedTextStyle): void {\n el.style.position = 'absolute';\n el.style.pointerEvents = 'auto';\n el.style.boxSizing = 'border-box';\n el.style.margin = '0';\n el.style.padding = '0';\n el.style.border = '0';\n el.style.outline = 'none';\n el.style.background = 'transparent';\n // Unfilled text still edits in black: the overlay has to be legible, and\n // there is no CSS spelling of \"no fill\" that leaves the glyphs visible.\n el.style.color = style.fill !== null && 'color' in style.fill ? style.fill.color : '#000';\n el.style.caretColor = style.caretColor;\n // The canvas's face, not the browser's reading of the family name.\n const fontFamily = cssFontFamily(style.fontFamily, {\n weight: numericWeight(style.fontWeight), style: style.fontStyle,\n });\n el.style.font = fontString({ ...style, fontFamily });\n // Linux Chromium otherwise rounds every advance to a whole pixel at DPR 1, while\n // the canvas lays out a face's own fractional ones: 'Small Caps' at 24px ran 3px\n // long. A canvas font is measured by the browser's default rendering, so keeps it.\n el.style.textRendering = isCanvasFont(style.fontFamily) ? '' : 'geometricPrecision';\n el.style.lineHeight = String(style.lineHeight);\n // The overlay sets no `direction`, so a justified paragraph's last line is\n // pinned to the edge the canvas puts it at rather than left to `start`.\n const edge = resolveAlign(style.align, style.direction);\n el.style.textAlign = style.align === 'justify' ? 'justify' : edge;\n el.style.setProperty('text-align-last', style.align === 'justify' ? edge : '');\n // Node-level decoration, so the overlay looks like the canvas the moment\n // editing starts. Runs are additive over the node style (a run can't un-set\n // a flag), which is exactly how CSS decoration propagates to descendants —\n // a run span adds its own decoration but can't remove this one.\n const decorations: string[] = [];\n if (style.underline) decorations.push('underline');\n if (style.strikethrough) decorations.push('line-through');\n if (style.overline) decorations.push('overline');\n el.style.textDecoration = decorations.length > 0 ? decorations.join(' ') : 'none';\n el.style.textTransform = style.textTransform;\n // The CSS spelling for what a browser reads; the scale for the pieces\n // `runsToDom` splits inheriting runs into, so they match the canvas.\n el.style.fontVariant = style.fontVariantCaps;\n el.style.setProperty(\n SMALL_CAPS_SCALE_PROPERTY,\n style.fontVariantCaps === 'small-caps'\n ? String(smallCapsScaleFor(style.fontFamily, numericWeight(style.fontWeight), style.fontStyle))\n : '',\n );\n // Line breaks follow the node's declared `wrap`, as the canvas's do. Layout\n // breaks only between words, never inside one.\n el.style.whiteSpace = style.wrap ? 'pre-wrap' : 'pre';\n el.style.overflowWrap = 'normal';\n el.style.wordBreak = 'normal';\n}\n\n/**\n * Inject a `<style>` element scoped to `overlayClass` so the edit overlay's\n * `::selection` matches the configured colors. Returns the style element so\n * the effect cleanup can remove it. Returns `null` when no selection\n * theming was requested.\n */\nfunction installSelectionStyle(\n overlayClass: string,\n style: ResolvedTextStyle,\n): HTMLStyleElement | null {\n if (style.selectionBackground == null && style.selectionColor == null) return null;\n const el = document.createElement('style');\n const decls: string[] = [];\n if (style.selectionBackground != null) {\n decls.push(`background: ${style.selectionBackground};`);\n }\n if (style.selectionColor != null) {\n decls.push(`color: ${style.selectionColor};`);\n }\n el.textContent = `.${overlayClass}::selection { ${decls.join(' ')} }`;\n document.head.appendChild(el);\n return el;\n}\n\nconst ALIGN_ANCHOR = { left: 0, center: 0.5, right: 1 } as const;\n\n/**\n * The overlay's `top`, in container pixels, for content `contentHeight` tall\n * (pre-scale, like the rest of `pose`): the pose's top, shifted by the same\n * `verticalAlign` slack the canvas applies, plus `drop` — how far below the\n * CSS line's baseline the canvas puts its own, from {@link baselineDrop}.\n * @internal\n */\nexport function overlayTop(pose: TextEditScreenPose, contentHeight: number, drop = 0): number {\n const slack = verticalAlignOffset(pose.verticalAlign, pose.height, contentHeight);\n return pose.y + (slack + drop) * (pose.zoom ?? 1);\n}\n\n/**\n * The canvas and CSS both hang a line's baseline half the leading plus one\n * ascent below the line top, from the same ascent rule, but each engine\n * rounds the font's metrics and that leading its own way, and a face that\n * states no ascent and descent hangs from its ascent alone. So the difference\n * is read from both sides rather than predicted: the canvas's from its own layout, the\n * overlay's from a zero-height inline box on a hidden line set exactly as the\n * overlay is. Pre-scale; `0` when either side has nothing to measure.\n * @internal\n */\nexport function baselineDrop(el: HTMLElement, style: ResolvedTextStyle, pose: TextEditScreenPose, lineHeight: number): number {\n const canvas = layoutTextPose({\n x: 0, y: 0, width: pose.width, height: pose.height, text: ' ',\n style: {\n fontFamily: style.fontFamily, fontWeight: style.fontWeight, fontStyle: style.fontStyle,\n fontSize: pose.fontSize, lineHeight, script: style.script,\n },\n }).laid.lines[0]?.baselineY ?? 0;\n const css = cssBaseline(el);\n return canvas > 0 && css > 0 ? canvas - css : 0;\n}\n\nconst CSS_BASELINES = new Map<string, number>();\n\nfunction cssBaseline(el: HTMLElement): number {\n const parent = el.parentElement;\n if (!parent) return 0;\n const { fontFamily, fontWeight, fontStyle, fontSize, lineHeight, textRendering } = el.style;\n const key = [fontFamily, fontWeight, fontStyle, fontSize, lineHeight, textRendering].join('|');\n const cached = CSS_BASELINES.get(key);\n if (cached !== undefined) return cached;\n const line = document.createElement('div');\n Object.assign(line.style, {\n position: 'absolute', left: '0', top: '0', width: '100px', margin: '0', padding: '0', border: '0',\n visibility: 'hidden', whiteSpace: 'pre', fontFamily, fontWeight, fontStyle, fontSize, lineHeight, textRendering,\n });\n const mark = document.createElement('span');\n Object.assign(mark.style, { display: 'inline-block', width: '0', height: '0', verticalAlign: 'baseline' });\n line.appendChild(mark);\n parent.appendChild(line);\n const box = line.getBoundingClientRect();\n // Rect over layout width undoes any transform on an ancestor.\n const scale = box.width / 100;\n const baseline = scale > 0 ? (mark.getBoundingClientRect().top - box.top) / scale : 0;\n line.remove();\n // A web font still loading is measured as its fallback; ask again later.\n if (baseline > 0 && document.fonts?.status !== 'loading') {\n if (CSS_BASELINES.size >= 64) CSS_BASELINES.clear();\n CSS_BASELINES.set(key, baseline);\n }\n return baseline;\n}\n\nfunction placeOverlay(\n el: HTMLDivElement,\n clipBox: HTMLDivElement,\n pose: TextEditScreenPose | null,\n clip: TextEditClipRect | null,\n style: ResolvedTextStyle,\n): void {\n if (!pose) {\n el.style.display = 'none';\n return;\n }\n // `clip` rather than `hidden`: a `hidden` box is still scrollable, and the\n // browser scrolls it to keep the caret in view.\n clipBox.style.overflow = clip ? 'clip' : 'visible';\n clipBox.style.left = `${clip?.x ?? 0}px`;\n clipBox.style.top = `${clip?.y ?? 0}px`;\n clipBox.style.width = `${clip?.width ?? 0}px`;\n clipBox.style.height = `${clip?.height ?? 0}px`;\n el.style.display = '';\n // An unwrapped line can be wider than its box, and the canvas aligns it\n // about the box's left edge, center or right edge regardless. The overlay\n // sizes to its text and grows from that same anchor: `left` puts the anchor\n // there and a trailing translate pulls the box back by that fraction of its\n // own width.\n const anchor = style.wrap ? 0 : ALIGN_ANCHOR[resolveAlign(style.align, style.direction)];\n el.style.left = `${pose.x + pose.width * (pose.zoom ?? 1) * anchor - (clip?.x ?? 0)}px`;\n el.style.width = style.wrap ? `${pose.width}px` : 'max-content';\n el.style.minWidth = style.wrap ? '' : `${pose.width}px`;\n // A node-level script draws the glyphs smaller and raised on the line the\n // plain text would hold, so the line height is pinned in pixels of the\n // unscripted size rather than left to scale with the font. The rise is\n // added to `top` with the baseline correction.\n const script = style.script\n ? scriptMetricsFor(style.fontFamily, numericWeight(style.fontWeight), style.fontStyle)[style.script]\n : undefined;\n const lineHeight = pose.lineHeight ?? style.lineHeight;\n el.style.fontSize = `${pose.fontSize * (script?.size ?? 1)}px`;\n el.style.lineHeight = script ? `${pose.fontSize * lineHeight}px` : String(lineHeight);\n // A top-aligned overlay fills its box, so a click anywhere in it lands in\n // the editor. Any other alignment needs the content's own height, which a\n // min-height would mask; `offsetHeight` is pre-transform, in pose units.\n const aligned = pose.verticalAlign !== undefined && pose.verticalAlign !== 'top';\n el.style.minHeight = aligned ? '' : `${pose.height}px`;\n const drop = baselineDrop(el, style, pose, lineHeight) - (script?.shift ?? 0) * pose.fontSize;\n el.style.top = `${overlayTop(pose, aligned ? el.offsetHeight : pose.height, drop) - (clip?.y ?? 0)}px`;\n // A declared `zoom` means every size on the pose is pre-scale and the\n // overlay carries the view scale as a transform. Anchored at the top-left so\n // `left`/`top` stay screen pixels. Written unconditionally (`none` at zoom\n // 1) because the overlay element outlives an edit session and would\n // otherwise keep a stale scale.\n el.style.transformOrigin = '0 0';\n const transforms: string[] = [];\n if (pose.zoom !== undefined && pose.zoom !== 1) transforms.push(`scale(${pose.zoom})`);\n if (anchor !== 0) transforms.push(`translateX(${-anchor * 100}%)`);\n el.style.transform = transforms.length > 0 ? transforms.join(' ') : 'none';\n // `letter-spacing` is not part of the CSS `font` shorthand, so\n // `applyOverlayStyle`'s `el.style.font` never carries it — it needs its own\n // assignment. It also lives here rather than there because without a\n // transform it is the one world-unit typography value that has to be\n // re-scaled as the view zooms.\n //\n // Under a transform there is nothing to re-scale: the world value is the\n // value, and run-level overrides — which `runsToDom` writes in world units\n // — scale by the same factor. Without one, `pose.fontSize` is documented as\n // `style.fontSize * zoom`, so their ratio is the world→screen factor; run\n // overrides then stay unscaled, which is why that path is only correct at\n // zoom 1. (CSS inheritance makes a run span's own declaration *replace* this\n // value rather than add to it, in both paths.)\n const scale =\n pose.zoom !== undefined ? 1 : style.fontSize > 0 ? pose.fontSize / style.fontSize : 1;\n el.style.letterSpacing = `${style.letterSpacing * scale}px`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useTextEdit.js","names":["STYLE_SHORTCUTS","UNSHIFT","toggleForKey","raw","key","s","patchForToggle","toggle","current","on","rangeWeight","nodeStyle","nodeHasFlag","styleAtCaret","at","styleAtRange","runs","spliceRunAt","out","pos","placed","r","end","k","run","applyStyleToRange","restoreOverlayFocus","active","overlay","a","charOffsetToDomPosition","range","b","sel","placeCaretBetweenRuns","acc","index","i","offset","span","NO_PENDING","mergePending","next","prev","superseded","supersededKey","patch","value","readSelectionOffsets","start","domPositionToCharOffset","writeRunsPreservingSelection","inTextEntry","runsToDom","base","trimCaretHolder","last","trimmed","head","sameSelection","TEXT_EDIT_FONT_HOLD","useTextEdit","optsRef","useLatest","opts","editingId","setEditingId","useState","editingIdRef","overlayRef","useRef","baseRef","resolveTextStyle","setNodeStyle","nodeStyleRef","holdNodeStyle","useCallback","style","styleOf","id","tickRef","frameLoop","useVisibleRaf","initialCaretRef","openedRef","selection","setSelection","rangeStyle","setRangeStyle","pendingStyle","setPendingStyle","pendingRef","pendingAtRef","armPending","empty","dropPending","selectionRef","publishRange","collapsed","domToRuns","syncSelection","clearSelection","cancelEdit","commit","commitAll","priorRuns","getRuns","read","runsInPlay","runsCarryStyling","text","runsToPlainText","setStyle","setText","setRuns","startEdit","isEditing","applyStyleToSelection","canWriteStyle","unboldPatch","unsetFlag","setFlagOverRange","resolved","applyOverlayStyle","rest","toggleStyle","patchRangeStyle","useEffect","container","getStyle","getPaint","overlayClass","OVERLAY_SEQ","initialRuns","getText","appendOverlayText","hold","holdForFaces","styleEl","installSelectionStyle","clipBox","createClipBox","place","clip","held","release","holdTimer","initial","placeCaretAt","onKeyDown","onBlur","isChrome","onBeforeInput","ie","pending","after","textNode","resplitSmallCaps","normalizeSmallCaps","onInput","onPointerDownOutside","target","cssFontFamilyLoading","numericWeight","p","remaining","walker","root","node","len","el","fontFamily","cssFontFamily","fontString","isCanvasFont","applyOverlayDirection","decorations","SMALL_CAPS_SCALE_PROPERTY","smallCapsScaleFor","decls","ALIGN_ANCHOR","overlayTop","slack","verticalAlignOffset","pose","contentHeight","drop","baselineDrop","canvas","layoutTextPose","css","cssBaseline","CSS_BASELINES","parent","fontWeight","fontStyle","fontSize","lineHeight","textRendering","cached","line","mark","box","scale","baseline","placeOverlay","anchor","overlayAlignEdge","script","scriptMetricsFor","aligned","transforms"],"sources":["../../../src/features/text/useTextEdit.ts"],"sourcesContent":["/**\n * In-place text editing via a contenteditable overlay positioned over the\n * text node's screen-space pose. Enter / blur commit; Shift+Enter inserts a\n * newline; Escape cancels. The hook owns the DOM lifecycle of the overlay\n * element — caller just provides a container, the lookup hooks, and a\n * `setText` callback (which is responsible for op-batching/undo).\n *\n * The text layer should hide the node it's currently editing\n * (`isHidden: (n) => n.id === editingId`) so the overlay isn't drawn twice.\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { useVisibleRaf } from '../../scheduling/useVisibleRaf';\nimport type { ResolvedTextStyle, TextStyle } from '@weasel-js/text';\nimport {\n fontString, numericWeight, resolveTextStyle, scriptMetricsFor, smallCapsScaleFor,\n} from '@weasel-js/text';\nimport { cssFontFamily, cssFontFamilyLoading, isCanvasFont } from '@weasel-js/font';\nimport type { TextPaint, TextVerticalAlign } from '@weasel-js/text';\nimport { layoutTextPose, verticalAlignOffset } from '@weasel-js/text';\nimport type { StyledRun } from '@weasel-js/text';\nimport { runsToPlainText } from '@weasel-js/text';\nimport {\n runsToDom, domToRuns, appendOverlayText, charOffsetToDomPosition, domPositionToCharOffset, normalizeSmallCaps,\n SMALL_CAPS_SCALE_PROPERTY, type OverlayRunBase,\n} from './domRuns';\nimport { applyStyleToRange, patchRangeStyle, runsCarryStyling, styleAtRange, supersededKey } from './runs/rangeStyle';\nimport { nodeHasFlag, setFlagOverRange, unboldPatch, type FlagKey } from './runs/flagRange';\nimport { rangeWeight } from './runs/effectiveRangeStyle';\nimport type { RangeStyle, RunStylePatch } from './runs/rangeStyle';\nimport { applyOverlayDirection, overlayAlignEdge } from './overlayDirection';\n\n/**\n * A styling a shortcut or a control toggles. The additive booleans, plus the\n * two `script` values — which are exclusive rather than additive, so they\n * toggle against one enum rather than each owning a boolean.\n */\nexport type StyleToggle = FlagKey | 'super' | 'sub';\n\n/**\n * The shortcut each styling answers to. Strikethrough takes Cmd+Shift+X\n * (Docs' binding) and MUST require shift: bare Cmd+X is cut, and swallowing\n * it here would break cutting text mid-edit. Superscript takes Word's\n * Cmd+Shift+=; subscript takes Cmd+Shift+- rather than Word's Cmd+-, because\n * the unshifted pair is browser zoom, which a page cannot cancel.\n * `overline` has no shortcut — no editor has established one — and is\n * reachable only from a control.\n *\n * Matching is on the *unshifted* character (`shiftedKey`): a US layout\n * reports Cmd+Shift+= as `'+'` and Cmd+Shift+- as `'_'`, so keying the table\n * on what the legend says needs that mapping to survive the shift.\n *\n * Underline has to be intercepted, not merely supported. Left alone, the\n * browser runs its own `formatUnderline` and `domToRuns`' `<u>` flattening\n * makes that *look* like it worked while bypassing `patchForToggle`\n * entirely — no toggle-off, no mixed-range \"turn the whole selection on\"\n * rule, no pending style for a collapsed caret. The flattening stays\n * regardless, since it is what lets pasted decoration survive, but it is\n * defense in depth and was never the mechanism.\n */\nconst STYLE_SHORTCUTS: ReadonlyArray<{ key: string; shift: boolean; toggle: StyleToggle }> = [\n { key: 'b', shift: false, toggle: 'bold' },\n { key: 'i', shift: false, toggle: 'italic' },\n { key: 'u', shift: false, toggle: 'underline' },\n { key: 'x', shift: true, toggle: 'strikethrough' },\n { key: '=', shift: true, toggle: 'super' },\n { key: '-', shift: true, toggle: 'sub' },\n];\n\n/** The characters shift produces from the keys the table names. */\nconst UNSHIFT: Readonly<Record<string, string>> = { '+': '=', _: '-' };\n\n/** The styling `e` toggles, or null when it isn't a styling shortcut. */\nfunction toggleForKey(e: KeyboardEvent): StyleToggle | null {\n if (!e.metaKey && !e.ctrlKey) return null;\n const raw = e.key.toLowerCase();\n const key = UNSHIFT[raw] ?? raw;\n return STYLE_SHORTCUTS.find((s) => s.key === key && s.shift === e.shiftKey)?.toggle ?? null;\n}\n\n/**\n * The patch that toggles `toggle` against the styling in `current`: if it is\n * already carried, clear it; otherwise set it — so a mixed range turns fully\n * on, as in every other text editor. `script` clears to `undefined` rather\n * than to `false`, since the off state of an enum is its absence.\n *\n * `nodeStyle` is read as well as `current`, and has to be: a node flag\n * renders on every run whether or not the runs carry it, so a toggle that\n * consulted the runs alone would read Cmd+B inside a `fontWeight: 700` node\n * as \"not bold\" and *add* bold rather than clearing it. `script` has no node\n * level to consult — by design; see `StyledRun.script`.\n */\nfunction patchForToggle(\n current: RangeStyle,\n toggle: StyleToggle,\n nodeStyle: TextStyle,\n): RunStylePatch {\n if (toggle === 'super' || toggle === 'sub') {\n return { script: current.script === toggle ? undefined : toggle };\n }\n const on = toggle === 'bold'\n ? rangeWeight(current, nodeStyle).bold === true\n : current[toggle] === true || nodeHasFlag(nodeStyle, toggle);\n return { [toggle]: !on };\n}\n\n/**\n * The styling at a collapsed caret: the run to its left, or — at the very\n * start of the text — the run to its right. This is what the next typed\n * character inherits, so it is what a character bar should be showing.\n */\nfunction styleAtCaret(runs: readonly StyledRun[], at: number): RangeStyle {\n return at > 0 ? styleAtRange(runs, at - 1, at) : styleAtRange(runs, 0, 1);\n}\n\n/**\n * Splice `run` into `runs` at character offset `at`, splitting whichever run\n * spans it. The result is normalized (empty runs dropped, identical\n * neighbours merged) by the same algebra every other write goes through.\n */\nfunction spliceRunAt(runs: readonly StyledRun[], at: number, run: StyledRun): StyledRun[] {\n const out: StyledRun[] = [];\n let pos = 0;\n let placed = false;\n for (const r of runs) {\n const end = pos + r.text.length;\n if (!placed && at <= end) {\n const k = at - pos;\n if (k > 0) out.push({ ...r, text: r.text.slice(0, k) });\n out.push(run);\n if (k < r.text.length) out.push({ ...r, text: r.text.slice(k) });\n placed = true;\n } else {\n out.push({ ...r });\n }\n pos = end;\n }\n if (!placed) out.push(run);\n // An empty patch over an empty range: this is the array-wide normalization\n // pass, not a styling.\n return applyStyleToRange(out, 0, 0, {});\n}\n\n/**\n * Return focus to the overlay after a styling arrived from editor chrome.\n *\n * A control that took focus in order to be clicked still holds it when the\n * patch lands, so the next keystroke goes to that button — and in an app that\n * binds bare letters to tools, \"click Superscript, then type the 2\" activates\n * the Text tool and pans the canvas instead of typing a 2. Restoring the\n * caret is what makes a styling control usable mid-edit at all.\n *\n * Two things are left holding focus deliberately: a text entry in the chrome\n * (the user is still typing a font size — stealing that is worse than the\n * problem) and anything inside an open dialog, which is where a color\n * picker's live `onChange` fires from.\n */\nfunction restoreOverlayFocus(overlay: HTMLElement, range: TextEditSelection): void {\n const active = document.activeElement;\n if (active === overlay || overlay.contains(active)) return;\n if (\n active instanceof HTMLInputElement\n || active instanceof HTMLTextAreaElement\n || (active instanceof HTMLElement && active.isContentEditable)\n || (active instanceof Element && active.closest('[role=\"dialog\"]') !== null)\n ) return;\n overlay.focus();\n const a = charOffsetToDomPosition(overlay, range.start);\n const b = charOffsetToDomPosition(overlay, range.end);\n const sel = window.getSelection();\n if (!a || !b || !sel) return;\n const r = document.createRange();\n r.setStart(a.node, a.offset);\n r.setEnd(b.node, b.offset);\n sel.removeAllRanges();\n sel.addRange(r);\n}\n\n/**\n * Put the caret *between* two run spans when `offset` falls on the boundary\n * of one, rather than at the same offset inside the left span's text node.\n *\n * The two positions are the same character offset and browsers report them\n * identically, but they type differently: a caret inside a span has the next\n * character absorbed by that run, and one between spans starts a fresh one.\n * That distinction is the whole point after a pending style has just been\n * committed to its own run — otherwise the character after a superscript is\n * superscript too. Leaves the selection alone when the offset falls mid-run,\n * where there is no boundary to prefer.\n */\nfunction placeCaretBetweenRuns(\n overlay: HTMLElement,\n runs: readonly StyledRun[],\n offset: number,\n): void {\n let acc = 0;\n let index = -1;\n for (let i = 0; i < runs.length; i++) {\n acc += runs[i].text.length;\n if (acc === offset) { index = i; break; }\n if (acc > offset) return;\n }\n const span = index >= 0 ? overlay.children[index] : null;\n const sel = window.getSelection();\n if (!span || !sel) return;\n const range = document.createRange();\n range.setStartAfter(span);\n range.collapse(true);\n sel.removeAllRanges();\n sel.addRange(range);\n}\n\n/** No pending style. One frozen object so re-publishing an already-empty\n * pending style doesn't re-render every consumer. */\nconst NO_PENDING: RunStylePatch = Object.freeze({});\n\n/**\n * Merge `patch` into a pending style. A key set to `false` or `undefined` is\n * deleted rather than stored — the same canonical form `patchRun` keeps, so\n * the pending style and a stored run agree on what \"off\" looks like, and\n * bold and a weight supersede each other here as they do there.\n */\nfunction mergePending(prev: RunStylePatch, patch: RunStylePatch): RunStylePatch {\n const next: Record<string, unknown> = { ...prev };\n const superseded = supersededKey(patch);\n if (superseded !== undefined) delete next[superseded];\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined || value === false) delete next[key];\n else next[key] = value;\n }\n return next as RunStylePatch;\n}\n\n/**\n * The caret's character range within the text being edited. Half-open\n * `[start, end)` over the concatenated run text, normalized so `start <= end`\n * regardless of which way the user dragged. `start === end` is a collapsed\n * caret — a real position, not the absence of one, which is why the hook\n * reports `null` rather than a zero-width range when there is no caret.\n */\nexport interface TextEditSelection {\n start: number;\n end: number;\n}\n\n/**\n * Character offsets of the current DOM selection within `overlay`, or `null`\n * when there is no selection or it lies outside the overlay. The one\n * DOM→offset conversion in the hook; everything that needs offsets goes\n * through here.\n */\nfunction readSelectionOffsets(overlay: HTMLElement): TextEditSelection | null {\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return null;\n const range = sel.getRangeAt(0);\n if (!overlay.contains(range.startContainer) || !overlay.contains(range.endContainer)) {\n return null;\n }\n // `Range` boundary points are already in document order, so this is the\n // normalization for a backwards drag — anchor/focus are not consulted.\n const start = domPositionToCharOffset(overlay, range.startContainer, range.startOffset);\n const end = domPositionToCharOffset(overlay, range.endContainer, range.endOffset);\n return start <= end ? { start, end } : { start: end, end: start };\n}\n\n/**\n * Replace the overlay's contents with `runs` and put the caret back on\n * `[start, end)`. Rewriting the DOM destroys the selection, and losing it\n * after every styling change would mean re-selecting the word to apply a\n * second style — so this is the only way styling writes back.\n *\n * The caret is **not** restored while focus sits in a text-entry control.\n * Browsers route editing commands by selection rather than by focus, so a\n * document selection put back inside the contenteditable while a bar's\n * number field has focus means the very Enter that committed that field also\n * runs its `insertParagraph` default over the restored range — replacing the\n * text that was just styled with a line break. (Observed in Chrome.)\n *\n * Focus on a *button* is the opposite case and must still restore: toggling\n * bold, then italic, then underline is one flow over one selection, and\n * clicking a toolbar button does take focus. Buttons turn keystrokes into\n * clicks, not into editing commands, so the range is safe there.\n *\n * The hook remembers the range as character offsets either way, so a skipped\n * restore costs the highlight, never the target.\n */\nfunction writeRunsPreservingSelection(\n overlay: HTMLElement,\n runs: readonly StyledRun[],\n start: number,\n end: number,\n base: OverlayRunBase,\n): void {\n const active = document.activeElement;\n const inTextEntry =\n active !== overlay &&\n !overlay.contains(active) &&\n (active instanceof HTMLInputElement ||\n active instanceof HTMLTextAreaElement ||\n (active instanceof HTMLElement && active.isContentEditable));\n runsToDom(runs, overlay, base);\n if (inTextEntry) return;\n const a = charOffsetToDomPosition(overlay, start);\n const b = charOffsetToDomPosition(overlay, end);\n const sel = window.getSelection();\n if (!a || !b || !sel) return;\n const range = document.createRange();\n range.setStart(a.node, a.offset);\n range.setEnd(b.node, b.offset);\n sel.removeAllRanges();\n sel.addRange(range);\n}\n\n/**\n * Drop the one trailing newline a contenteditable keeps so the caret has\n * somewhere to sit on the last line. `domToRuns` maps the `<br>` to a\n * literal `'\\n'`, which the edit never typed. Exactly one, so a newline\n * the user actually typed survives — the holder is never doubled.\n *\n * An empty run left behind is dropped: it carries no text and would otherwise\n * defeat `runsCarryStyling`'s \"did this edit produce styling\" question by\n * existing.\n */\nfunction trimCaretHolder(runs: StyledRun[]): StyledRun[] {\n const last = runs[runs.length - 1];\n if (last === undefined || !last.text.endsWith('\\n')) return runs;\n const trimmed = { ...last, text: last.text.slice(0, -1) };\n const head = runs.slice(0, -1);\n return trimmed.text === '' ? head : [...head, trimmed];\n}\n\nfunction sameSelection(a: TextEditSelection | null, b: TextEditSelection | null): boolean {\n if (a === null || b === null) return a === b;\n return a.start === b.start && a.end === b.end;\n}\n\n/** Screen-space pose passed to `useTextEdit` so the overlay can be placed and sized in CSS pixels. */\nexport interface TextEditScreenPose {\n /** Top-left in CSS pixels relative to `container`. Always screen pixels,\n * including when `zoom` is set — the scale is anchored at this point, not\n * translated by it. */\n x: number;\n y: number;\n /** Pre-scale — CSS pixels, or world units when `zoom` is set. */\n width: number;\n height: number;\n /** Pre-scale font size: `style.fontSize * zoom` when `zoom` is omitted, the\n * world-unit `style.fontSize` when it is set. */\n fontSize: number;\n /** Effective on-screen line height multiplier (defaults to style.lineHeight). */\n lineHeight?: number;\n /**\n * Where the text sits within `height`, as the canvas places it. `'center'`\n * and `'bottom'` size the overlay to its own content and offset it by the\n * slack, re-measured as typing changes the line count. Default `'top'`.\n */\n verticalAlign?: TextVerticalAlign;\n /**\n * CSS scale applied to the overlay (`transform: scale(zoom)`, anchored at\n * its top-left). Every other size on this pose, and every typographic\n * metric the hook writes, is then **pre-scale** — pass world units and the\n * transform does the world→screen conversion.\n *\n * This is the only way run-level typography can be correct at a zoom other\n * than 1. `runsToDom` emits run `fontSize` / `letterSpacing` in world units\n * and `domToRuns` reads them straight back; threading a scale through the\n * writer and its inverse through the reader would go lossy on fractional\n * zooms. Scaling the whole overlay instead leaves that serializer pure and\n * scales node-level and run-level values by the same factor for free.\n *\n * Omit it (the default, `1`) and the pose is plain screen pixels — the hook\n * then infers the world→screen factor from `fontSize / style.fontSize` to\n * scale node-level `letterSpacing`, and run-level overrides are left\n * unscaled. Correct only at zoom 1.\n */\n zoom?: number;\n}\n\n/** A box in CSS pixels relative to `useTextEdit`'s `container`. */\nexport interface TextEditClipRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** What one edit session changed, as `UseTextEditOptions.commit` receives it. */\nexport interface TextEditCommit {\n text: string;\n runs?: StyledRun[];\n style?: TextStyle;\n}\n\n/** Options for `useTextEdit`. */\nexport interface UseTextEditOptions {\n /** Element the overlay is appended to. Must be `position: relative`/absolute. */\n container: HTMLElement | null;\n /** Read the current text for `id`. */\n getText: (id: string) => string;\n /** Read style for `id` (used for font setup on the overlay). */\n getStyle: (id: string) => TextStyle | undefined;\n /**\n * Read the node's paint for `id` — its `data.fill` / `data.stroke`. The\n * overlay paints its text and caret from the fill, so omitting this\n * renders every node's editor in the default black however the node\n * itself is painted.\n */\n getPaint?: (id: string) => TextPaint | undefined;\n /**\n * Write style back for `id`. Optional; needed only to turn a flag **off**\n * inside a node whose own `TextStyle` sets it. Run flags are additive, so\n * that edit is expressed by lowering the node flag and raising it on the\n * runs outside the selection — which needs somewhere to put the new node\n * style. Without this or `commit`, such a toggle is declined and nothing\n * changes. See `runs/flagRange`.\n *\n * Called when the edit commits, never during it: the session holds the\n * rewritten style until then (see `UseTextEditReturn.nodeStyle`), so a\n * canceled edit writes nothing.\n */\n setStyle?: (id: string, style: TextStyle) => void;\n /** Read screen-space pose for `id`. Called per frame while editing. */\n getScreenPose: (id: string) => TextEditScreenPose | null;\n /**\n * The box, in CSS pixels relative to `container`, the overlay is visible\n * within — where the canvas clips the glyphs the overlay stands in for.\n * Re-read every frame. Omit it, or return `null`, to clip nothing.\n *\n * The clip cannot scroll, so a caret moving past its edge leaves the page\n * where it is; a scrollable container would drag the canvas after the caret.\n */\n getClipRect?: () => TextEditClipRect | null;\n /** Commit text. Caller wraps in op/undo. Required unless `commit` is given. */\n setText?: (id: string, text: string) => void;\n /**\n * Take the whole edit in one call, so the caller can make it one write and\n * one undo step. When given, it replaces `setText`, `setRuns`, and\n * `setStyle`, none of which is called. `runs` is present when runs are in\n * play (see `setRuns`), `style` when the edit rewrote the node's own style\n * (see `setStyle`).\n */\n commit?: (id: string, edit: TextEditCommit) => void;\n /**\n * Optional: read the current rich-text runs for `id`. When provided AND\n * returns a non-empty array, the overlay renders the runs as styled\n * `<span data-run>` elements; otherwise the overlay is seeded with plain\n * text via `getText`.\n */\n getRuns?: (id: string) => readonly StyledRun[] | undefined;\n /**\n * Optional: commit rich-text runs back to the node. When omitted, only\n * `setText` is called with the plain-text form on commit. When provided,\n * commit calls both `setText` (with `runsToPlainText(runs)`) and\n * `setRuns(id, runs)` — but only when runs are actually in play: the node\n * already had some, or the edit produced styling. A plain-text edit of a\n * plain-text node still calls `setText` alone, so a node that has never\n * been styled doesn't grow a single-run `runs` array just for being edited.\n */\n setRuns?: (id: string, runs: StyledRun[]) => void;\n /**\n * Is `el` part of the editor's own chrome — a character-options bar, a\n * color popover, anything whose whole purpose is to style the text being\n * edited? Focus moving into one does not end the edit.\n *\n * Without this, the controls the feature exists for are exactly what\n * destroys it: clicking a size field blurs the overlay, blur commits, and\n * the caret the control was about to act on is gone. Toggle buttons can\n * dodge it by `preventDefault()`-ing their own mousedown, but a field the\n * user has to type into cannot.\n *\n * Chrome focus does not disturb the reported `selection` either — see\n * `UseTextEditReturn.selection`.\n */\n isEditorChrome?: (el: Element) => boolean;\n /** What Escape does to the edit: `'cancel'` (the default) drops it,\n * `'commit'` keeps the text as Enter would. */\n escape?: 'cancel' | 'commit';\n /**\n * How long, in ms, an edit opening waits for the overlay's face before\n * showing the text in the fallback. Defaults to {@link TEXT_EDIT_FONT_HOLD}.\n *\n * The overlay stands in for canvas text the host has hidden, so the wait\n * shows nothing at all; past it, the fallback shows and reflows to the real\n * face when that lands. A face already loaded — the usual case, since\n * `@weasel-js/font` builds it as soon as its bytes arrive — shows at once.\n * `0` never waits.\n */\n fontHold?: number;\n}\n\n/**\n * The default {@link UseTextEditOptions.fontHold}: at 100ms a blank edit\n * still reads as an instant response, and it is the block period CSS gives\n * `font-display: fallback` for the same trade.\n */\nexport const TEXT_EDIT_FONT_HOLD = 100;\n\n/** Options for `useTextEdit().startEdit`. */\nexport interface StartEditOptions {\n /**\n * Where to place the caret on edit start. Number = caret offset (0..text.length);\n * `'all'` selects the whole text (default — preserves the prior behavior).\n */\n caret?: number | 'all';\n}\n\n/** Return shape of `useTextEdit`. */\nexport interface UseTextEditReturn {\n editingId: string | null;\n startEdit: (id: string, opts?: StartEditOptions) => void;\n cancelEdit: () => void;\n /** End the edit and write it. An edit that leaves the text, the runs, and\n * the node's style as it found them writes nothing. */\n commit: () => void;\n isEditing: (id: string) => boolean;\n /**\n * The node-wide style this edit has rewritten and not yet written back, or\n * `null` when it has rewritten none (the node's own style stands) or\n * nothing is being edited. Lowering a flag the node sets rewrites it; the\n * write waits for the commit. A control showing the node's style during an\n * edit reads this first.\n */\n nodeStyle: TextStyle | null;\n /**\n * The caret's character range, or `null` when nothing is being edited. A\n * collapsed caret reports `{ start: n, end: n }`, so `null` and \"caret at\n * n\" stay distinguishable — a styling written at a collapsed caret arms\n * {@link UseTextEditReturn.pendingStyle} instead of restyling a range.\n *\n * Follows the DOM selection, which browsers (and jsdom) report from a task\n * rather than synchronously; anything this hook writes itself updates it\n * synchronously. A DOM selection that leaves the overlay does **not** clear\n * it: that is what happens when the user clicks a styling control, and\n * reporting `null` there would read as \"collapsed caret\" and send the\n * control's patch to the node instead of to the range. It is cleared on\n * `startEdit` and when the edit ends.\n */\n selection: TextEditSelection | null;\n /**\n * The styling shared by every run in `selection` — a concrete value where\n * the range agrees, `MIXED` where it doesn't. `null` exactly when\n * `selection` is `null`. A collapsed caret reports the styling *at* the\n * caret (the run to its left, or the run to its right at offset 0), which\n * is what the next typed character inherits.\n *\n * This does not include {@link UseTextEditReturn.pendingStyle}. A control\n * showing what the next character will look like wants both, merged in\n * that order; one showing what is already written wants only this.\n */\n rangeStyle: RangeStyle | null;\n /**\n * Styling armed at a collapsed caret, applied to the next character typed\n * and then dropped. `{}` when nothing is armed — which is always the case\n * while `selection` covers a real range, since a range is styled directly.\n *\n * Moving the caret abandons it, as in any other editor.\n */\n pendingStyle: RunStylePatch;\n /**\n * Write `patch` over `selection`. A no-op with no active edit or an empty\n * patch. Over a real range this restyles the runs under it; at a collapsed\n * caret it merges into `pendingStyle` instead, so the styling lands on\n * what gets typed next rather than on text the user didn't select.\n *\n * Lowering a flag the *node* sets is neither of those — a run cannot say\n * \"not italic\" — so it rewrites instead: the node flag is cleared and raised\n * on every run outside the range. Bold is spared that: a run's own\n * `fontWeight` can say \"regular\", so it is written over the range.\n *\n * The caret survives, so a second style can be applied without\n * re-selecting, and `rangeStyle` reflects the write before this returns.\n */\n applyStyleToSelection: (patch: RunStylePatch) => void;\n /**\n * Toggle one styling over `selection` — set it if the range doesn't\n * uniformly carry it, clear it if it does. The shape a B / I / x² control\n * or a keyboard shortcut wants, and what this hook's own shortcuts call.\n */\n toggleStyle: (toggle: StyleToggle) => void;\n}\n\n/** In-place text editing via a contenteditable overlay positioned over the text node's screen-space pose. */\nexport function useTextEdit(\n opts: UseTextEditOptions,\n): UseTextEditReturn {\n const optsRef = useLatest(opts);\n\n const [editingId, setEditingId] = useState<string | null>(null);\n // `applyStyleToSelection` needs the node being edited to reach its\n // `TextStyle`, and is called from event handlers rather than from render.\n const editingIdRef = useLatest(editingId);\n const overlayRef = useRef<HTMLDivElement | null>(null);\n /** The editing node's resolved style, which run spans inheriting small caps\n * are split by whenever the runs are rewritten. */\n const baseRef = useRef<OverlayRunBase>(resolveTextStyle(undefined));\n const [nodeStyle, setNodeStyle] = useState<TextStyle | null>(null);\n /** `nodeStyle`, read synchronously by the handlers that rewrite and commit it. */\n const nodeStyleRef = useRef<TextStyle | null>(null);\n const holdNodeStyle = useCallback((style: TextStyle | null) => {\n nodeStyleRef.current = style;\n setNodeStyle(style);\n }, []);\n const styleOf = useCallback(\n (id: string): TextStyle | undefined => nodeStyleRef.current ?? optsRef.current.getStyle(id),\n [optsRef],\n );\n // The overlay-follow loop is built inside the editing effect; the gate's\n // frame callback reaches it through this ref.\n const tickRef = useRef<() => void>(() => {});\n const frameLoop = useVisibleRaf(() => { tickRef.current(); });\n const initialCaretRef = useRef<number | 'all'>('all');\n /** What the overlay read back as when the edit opened, before any input. */\n const openedRef = useRef<string | null>(null);\n const [selection, setSelection] = useState<TextEditSelection | null>(null);\n const [rangeStyle, setRangeStyle] = useState<RangeStyle | null>(null);\n const [pendingStyle, setPendingStyle] = useState<RunStylePatch>(NO_PENDING);\n /** The pending style, read synchronously by `beforeinput` — which fires\n * well before React has re-rendered with the state above. */\n const pendingRef = useRef<RunStylePatch>(NO_PENDING);\n /** The caret offset the pending style was armed at, or `null` when nothing\n * is armed. Publishing any other range abandons it. */\n const pendingAtRef = useRef<number | null>(null);\n\n const armPending = useCallback((next: RunStylePatch, at: number) => {\n const empty = Object.keys(next).length === 0;\n pendingRef.current = empty ? NO_PENDING : next;\n pendingAtRef.current = empty ? null : at;\n setPendingStyle(pendingRef.current);\n }, []);\n\n const dropPending = useCallback(() => {\n if (pendingAtRef.current === null) return;\n pendingRef.current = NO_PENDING;\n pendingAtRef.current = null;\n setPendingStyle(NO_PENDING);\n }, []);\n\n /**\n * The published range, mirrored in a ref. `applyStyleToSelection` reads it\n * synchronously, and it is the fallback when the DOM selection has moved\n * into editing chrome. Cleared with the state, never separately.\n */\n const selectionRef = useRef<TextEditSelection | null>(null);\n\n /**\n * Recompute the published caret range and its styling from the overlay.\n * Called from the `selectionchange` listener and directly after anything\n * the hook writes, so a control reading `rangeStyle` right after a patch\n * sees the new value rather than flickering back to the old one.\n *\n * A selection that is not inside the overlay leaves the published range\n * alone rather than clearing it. That state means focus moved into a\n * control — which is precisely when the range matters most, and reporting\n * `null` would read to a consumer as \"collapsed caret\" and send its patch\n * to the wrong target. The range is cleared where it is actually over: on\n * `startEdit` and on teardown.\n */\n /** Publish `range` and the styling the overlay currently carries across it. */\n const publishRange = useCallback((overlay: HTMLElement, range: TextEditSelection) => {\n const collapsed = range.start === range.end;\n // A pending style belongs to the caret position it was armed at. Any\n // other range means the caret moved, which abandons it.\n if (pendingAtRef.current !== null\n && !(collapsed && range.start === pendingAtRef.current)) {\n dropPending();\n }\n selectionRef.current = range;\n setSelection((prev) => (sameSelection(prev, range) ? prev : range));\n const runs = domToRuns(overlay);\n setRangeStyle(collapsed\n ? styleAtCaret(runs, range.start)\n : styleAtRange(runs, range.start, range.end));\n }, [dropPending]);\n\n const syncSelection = useCallback(() => {\n const overlay = overlayRef.current;\n if (!overlay) return;\n const next = readSelectionOffsets(overlay);\n if (next === null) return;\n publishRange(overlay, next);\n }, [publishRange]);\n\n /** Drop the published range — the edit session it described is over. */\n const clearSelection = useCallback(() => {\n selectionRef.current = null;\n setSelection(null);\n setRangeStyle(null);\n dropPending();\n }, [dropPending]);\n\n const cancelEdit = useCallback(() => {\n setEditingId(null);\n }, []);\n\n const commit = useCallback(() => {\n const id = editingId;\n const overlay = overlayRef.current;\n if (id == null || !overlay) {\n setEditingId(null);\n return;\n }\n // What decides this is whether *runs* are in play at the end of the\n // edit — not whether the overlay was seeded from them. Mirroring the\n // init-time guard (`getRuns` returned something non-empty) was the old\n // rule, and it silently dropped every styling a freshly created text\n // node acquired: no prior runs, so commit took the plain-text branch and\n // `domToRuns`' output went in the bin. So two ways in:\n //\n // - the node already had runs — they have to be rewritten even when the\n // edit ended up unstyled, or stale styling survives a Cmd-B that\n // turned it off; or\n // - the edit produced styling, whatever the node started as.\n //\n // A plain-text edit of a plain-text node satisfies neither and commits\n // text alone, so a node that has no `runs` doesn't grow a single-run\n // array just for being edited.\n const { getRuns, setRuns, setText, setStyle, commit: commitAll } = optsRef.current;\n const priorRuns = getRuns?.(id);\n const read = domToRuns(overlay);\n const style = nodeStyleRef.current;\n // Compared untrimmed: a seeded trailing newline reads back the same at\n // both ends, where the trimmed form would differ from the node's text.\n if (!style && JSON.stringify(read) === openedRef.current) {\n setEditingId(null);\n return;\n }\n const runs = trimCaretHolder(read);\n const hadRuns = priorRuns != null && priorRuns.length > 0;\n const runsInPlay = hadRuns || runsCarryStyling(runs);\n // Read from the DOM, not `innerText`: a browser applies `text-transform`\n // to what that reports, and a break stand-in reads back as its `\\n`.\n const text = runsToPlainText(runs);\n holdNodeStyle(null);\n if (commitAll) {\n commitAll(id, { text, ...(runsInPlay ? { runs } : {}), ...(style ? { style } : {}) });\n } else {\n if (style) setStyle?.(id, style);\n setText?.(id, text);\n if (setRuns && runsInPlay) setRuns(id, runs);\n }\n setEditingId(null);\n }, [editingId, optsRef, holdNodeStyle]);\n\n const startEdit = useCallback((id: string, opts?: StartEditOptions) => {\n initialCaretRef.current = opts?.caret ?? 'all';\n setEditingId(id);\n }, []);\n\n const isEditing = useCallback((id: string) => editingId === id, [editingId]);\n\n const applyStyleToSelection = useCallback((patch: RunStylePatch) => {\n const overlay = overlayRef.current;\n if (!overlay) return;\n // An empty patch would still round-trip the runs through\n // `applyStyleToRange`, whose normalization applies to the whole array and\n // not just the patched span. Nothing asked for that, so don't do it.\n if (Object.keys(patch).length === 0) return;\n // Fall back to the last range that WAS in the overlay: the caller may be\n // a control that took focus (and with it the DOM selection) to be\n // clicked. `writeRunsPreservingSelection` puts the range back afterwards,\n // so a second styling can follow without re-selecting either way.\n const range = readSelectionOffsets(overlay) ?? selectionRef.current;\n if (!range) return;\n\n // Lowering a flag the node itself sets is not expressible additively —\n // the run algebra can only add — so it takes the rewrite in\n // `setFlagOverRange`: clear the node flag, raise it outside the range.\n // This is the one styling that reaches existing text from a collapsed\n // caret, and has to: \"stop being bold from here on\" is otherwise\n // unsayable in a bold node.\n const id = editingIdRef.current;\n const canWriteStyle = optsRef.current.setStyle !== undefined || optsRef.current.commit !== undefined;\n const runs = domToRuns(overlay);\n if (patch.bold === false && !('fontWeight' in patch) && id !== null) {\n patch = { ...patch, ...unboldPatch(styleOf(id) ?? {}) };\n }\n const unsetFlag = id !== null && canWriteStyle\n ? (Object.keys(patch) as FlagKey[]).find((key) =>\n key !== 'bold'\n && patch[key] === false\n && nodeHasFlag(styleOf(id) ?? {}, key))\n : undefined;\n if (unsetFlag !== undefined && id !== null) {\n const r = setFlagOverRange(runs, styleOf(id) ?? {}, range.start, range.end, unsetFlag, false);\n holdNodeStyle(r.style);\n // The root's own decoration and slant reach every run by inheritance,\n // the range just cleared included.\n const resolved = resolveTextStyle(r.style, optsRef.current.getPaint?.(id));\n baseRef.current = resolved;\n applyOverlayStyle(overlay, resolved);\n writeRunsPreservingSelection(overlay, r.runs, range.start, range.end, resolved);\n publishRange(overlay, range);\n // The rewrite raised the flag on every existing run, so a caret sitting\n // at the end of one is now *inside* a flagged span and the next\n // character would be absorbed by it. Arming the `false` keeps it out:\n // it is not a storable run value, but it is enough to make\n // `beforeinput` open a fresh span, which `runsToDom` then emits bare.\n if (range.start === range.end) {\n armPending({ ...pendingRef.current, [unsetFlag]: false }, range.start);\n }\n restoreOverlayFocus(overlay, range);\n // Whatever else the patch carried still applies on its own terms.\n const rest = { ...patch };\n delete rest[unsetFlag];\n if (Object.keys(rest).length > 0) applyStyleToSelection(rest);\n return;\n }\n\n // A collapsed caret has no run to restyle, so the styling is armed for\n // the next character instead of silently doing nothing.\n if (range.start === range.end) {\n armPending(mergePending(pendingRef.current, patch), range.start);\n restoreOverlayFocus(overlay, range);\n return;\n }\n\n const next = applyStyleToRange(runs, range.start, range.end, patch);\n writeRunsPreservingSelection(overlay, next, range.start, range.end, baseRef.current);\n restoreOverlayFocus(overlay, range);\n // Republish from the range we just styled, not from the DOM selection:\n // when the patch came from chrome the selection is in that control, and\n // re-reading it would leave a consumer showing the pre-patch styling.\n publishRange(overlay, range);\n }, [publishRange, armPending, editingIdRef, optsRef, styleOf, holdNodeStyle]);\n\n const toggleStyle = useCallback((toggle: StyleToggle) => {\n const overlay = overlayRef.current;\n if (!overlay) return;\n const range = readSelectionOffsets(overlay) ?? selectionRef.current;\n if (!range) return;\n const runs = domToRuns(overlay);\n const id = editingIdRef.current;\n const nodeStyle = (id !== null ? styleOf(id) : undefined) ?? {};\n // At a collapsed caret the styling in play is what the caret sits in plus\n // whatever is already armed, so pressing Cmd+B twice disarms rather than\n // arming a second time.\n const current = range.start === range.end\n ? patchRangeStyle(styleAtCaret(runs, range.start), pendingRef.current)\n : styleAtRange(runs, range.start, range.end);\n applyStyleToSelection(patchForToggle(current, toggle, nodeStyle));\n }, [applyStyleToSelection, editingIdRef, styleOf]);\n\n useEffect(() => {\n if (editingId == null) return;\n const { container, getText, getStyle, getPaint } = optsRef.current;\n if (!container) return;\n holdNodeStyle(null);\n\n const style = resolveTextStyle(getStyle(editingId), getPaint?.(editingId));\n const overlay = document.createElement('div');\n const overlayClass = `weasel-text-edit-${++OVERLAY_SEQ}`;\n overlay.classList.add(overlayClass);\n overlay.setAttribute('contenteditable', 'true');\n overlay.spellcheck = false;\n baseRef.current = style;\n const initialRuns = optsRef.current.getRuns?.(editingId);\n if (initialRuns && initialRuns.length > 0) {\n runsToDom(initialRuns, overlay, style);\n } else if (style.fontVariantCaps === 'small-caps') {\n // Small caps needs its lowercase in pieces, which only a run span holds.\n runsToDom([{ text: getText(editingId) }], overlay, style);\n } else {\n // Not `innerText`, which turns each newline into a `<br>` the caret\n // walkers cannot count and U+2028 into a space.\n appendOverlayText(overlay, getText(editingId));\n }\n openedRef.current = JSON.stringify(domToRuns(overlay));\n applyOverlayStyle(overlay, style);\n const hold = holdForFaces(style, initialRuns ?? []);\n if (hold) overlay.style.opacity = '0';\n const styleEl = installSelectionStyle(overlayClass, style);\n const clipBox = createClipBox();\n clipBox.appendChild(overlay);\n container.appendChild(clipBox);\n overlayRef.current = overlay;\n\n const place = () => {\n const clip = optsRef.current.getClipRect?.() ?? null;\n placeOverlay(overlay, clipBox, optsRef.current.getScreenPose(editingId), clip, style);\n };\n place();\n // Measured again on release: the first `place` ran in the fallback face.\n let held = hold !== null;\n const release = () => {\n if (!held) return;\n held = false;\n clearTimeout(holdTimer);\n place();\n overlay.style.opacity = '';\n };\n const holdTimer = held ? setTimeout(release, optsRef.current.fontHold ?? TEXT_EDIT_FONT_HOLD) : undefined;\n void hold?.then(release);\n\n const range = document.createRange();\n const initial = initialCaretRef.current;\n if (initial === 'all') {\n range.selectNodeContents(overlay);\n } else {\n placeCaretAt(overlay, range, initial);\n }\n // Focus first, then place the caret: focusing an editable host is itself\n // a selection-moving act (jsdom collapses to the start; browsers vary),\n // so a range set beforehand is not reliably the one the user ends up with.\n overlay.focus();\n const sel = window.getSelection();\n sel?.removeAllRanges();\n sel?.addRange(range);\n // The caret exists now, so publish it rather than waiting for the\n // `selectionchange` task — a control mounted alongside the editor would\n // otherwise render one frame with no selection.\n syncSelection();\n\n const onKeyDown = (e: KeyboardEvent) => {\n // The key belongs to the IME: Enter confirms a candidate, Escape dismisses\n // them. Safari reports it after `compositionend`, as keyCode 229.\n if (e.isComposing || e.keyCode === 229) return;\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n commit();\n return;\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n if (optsRef.current.escape === 'commit') commit();\n else cancelEdit();\n return;\n }\n const toggle = toggleForKey(e);\n if (toggle) {\n e.preventDefault();\n toggleStyle(toggle);\n }\n };\n const onBlur = (e: FocusEvent) => {\n const next = e.relatedTarget;\n const isChrome = optsRef.current.isEditorChrome;\n if (isChrome && next instanceof Element && isChrome(next)) return;\n commit();\n };\n\n const onBeforeInput = (ie: InputEvent) => {\n if (ie.inputType !== 'insertText' || !ie.data) return;\n const pending = pendingRef.current;\n if (Object.keys(pending).length > 0) {\n // Rebuild from the runs rather than splicing a span into the DOM.\n // `insertNode` at a caret sitting inside a run span nests the new\n // span *within* it, and `domToRuns` reads styling down the ancestor\n // chain — so a pending style that has to escape the surrounding run\n // (the `false` armed after a node-flag rewrite) would inherit exactly\n // the flag it exists to shed. Going through the algebra also gets\n // every run field for free, with no CSS written here.\n const at = readSelectionOffsets(overlay) ?? selectionRef.current;\n if (!at || at.start !== at.end) return;\n ie.preventDefault();\n const next = spliceRunAt(domToRuns(overlay), at.start, {\n ...pending, text: ie.data,\n } as StyledRun);\n const after = at.start + ie.data.length;\n writeRunsPreservingSelection(overlay, next, after, after, baseRef.current);\n placeCaretBetweenRuns(overlay, next, after);\n dropPending();\n publishRange(overlay, { start: after, end: after });\n return;\n }\n ie.preventDefault();\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return;\n const range = sel.getRangeAt(0);\n range.deleteContents();\n {\n // No pending style — insert the character as a plain text node at the\n // caret position so the surrounding run's span absorbs it.\n const textNode = document.createTextNode(ie.data);\n range.insertNode(textNode);\n const after = document.createRange();\n after.setStartAfter(textNode);\n after.collapse(true);\n sel.removeAllRanges();\n sel.addRange(after);\n }\n resplitSmallCaps();\n };\n\n /** Put small-caps letters back in the pieces their case belongs in after\n * an edit, keeping the caret. Never mid-composition: rewriting the DOM\n * under an IME ends the composition. */\n const resplitSmallCaps = (e?: Event) => {\n if (e instanceof InputEvent && e.isComposing) return;\n const at = readSelectionOffsets(overlay);\n if (!normalizeSmallCaps(overlay) || !at) return;\n const a = charOffsetToDomPosition(overlay, at.start);\n const b = charOffsetToDomPosition(overlay, at.end);\n const sel = window.getSelection();\n if (!a || !b || !sel) return;\n const r = document.createRange();\n r.setStart(a.node, a.offset);\n r.setEnd(b.node, b.offset);\n sel.removeAllRanges();\n sel.addRange(r);\n };\n const onInput = (e: Event) => {\n resplitSmallCaps(e);\n place();\n };\n\n // Blur can't be the only way out. Once focus has moved into editing\n // chrome the overlay is no longer focused, so nothing blurs when the user\n // clicks away and the edit would never end. A pointerdown that lands\n // outside both the overlay and the chrome is that click. Capture phase so\n // it runs ahead of whatever the click was aimed at (a canvas gesture, a\n // different tool) rather than after it.\n const onPointerDownOutside = (e: Event) => {\n const target = e.target;\n if (!(target instanceof Node) || overlay.contains(target)) return;\n const isChrome = optsRef.current.isEditorChrome;\n if (isChrome && target instanceof Element && isChrome(target)) return;\n commit();\n };\n\n overlay.addEventListener('keydown', onKeyDown);\n overlay.addEventListener('blur', onBlur);\n // Typing can change the line count; re-placing here rather than on the\n // next frame keeps a center/bottom-aligned overlay from jumping a frame late.\n overlay.addEventListener('input', onInput);\n overlay.addEventListener('beforeinput', onBeforeInput);\n document.addEventListener('pointerdown', onPointerDownOutside, true);\n // `selectionchange` only exists on the document — there is no per-element\n // event for it — so this listens globally and `readSelectionOffsets`\n // filters to selections inside the overlay.\n document.addEventListener('selectionchange', syncSelection);\n\n // Follows the node's screen pose every frame — behind the visibility gate,\n // so an overlay left open in a background tab costs nothing.\n tickRef.current = () => {\n place();\n frameLoop.request();\n };\n frameLoop.request();\n\n return () => {\n held = false;\n clearTimeout(holdTimer);\n frameLoop.cancel();\n tickRef.current = () => {};\n overlay.removeEventListener('keydown', onKeyDown);\n overlay.removeEventListener('blur', onBlur);\n overlay.removeEventListener('input', onInput);\n overlay.removeEventListener('beforeinput', onBeforeInput);\n document.removeEventListener('pointerdown', onPointerDownOutside, true);\n document.removeEventListener('selectionchange', syncSelection);\n clipBox.remove();\n styleEl?.remove();\n overlayRef.current = null;\n holdNodeStyle(null);\n clearSelection();\n };\n }, [editingId, opts.container, optsRef, commit, cancelEdit, syncSelection, clearSelection,\n frameLoop, toggleStyle, dropPending, publishRange, holdNodeStyle]);\n\n return {\n editingId, startEdit, cancelEdit, commit, isEditing, nodeStyle,\n selection, rangeStyle, pendingStyle, applyStyleToSelection, toggleStyle,\n };\n}\n\nlet OVERLAY_SEQ = 0;\n\n/**\n * The loads the overlay's faces are still waiting on — the node's and each\n * run's that names its own family — or `null` when every one is in hand.\n */\nfunction holdForFaces(style: ResolvedTextStyle, runs: readonly StyledRun[]): Promise<unknown> | null {\n const pending = [\n cssFontFamilyLoading(style.fontFamily, {\n weight: numericWeight(style.fontWeight), style: style.fontStyle,\n }),\n ...runs.filter((r) => r.fontFamily != null).map((r) => cssFontFamilyLoading(r.fontFamily!, {\n weight: r.fontWeight ?? (r.bold ? 700 : undefined),\n style: r.italic ? 'italic' : undefined,\n })),\n ].filter((p) => p !== null);\n return pending.length === 0 ? null : Promise.all(pending);\n}\n\n/**\n * Place a collapsed caret `offset` characters into the overlay's text,\n * walking its text nodes. Out-of-range offsets clamp to the end.\n */\nfunction placeCaretAt(root: HTMLElement, range: Range, offset: number): void {\n let remaining = offset;\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);\n let node: Text | null = walker.nextNode() as Text | null;\n while (node) {\n const len = node.data.length;\n if (remaining <= len) {\n range.setStart(node, remaining);\n range.setEnd(node, remaining);\n return;\n }\n remaining -= len;\n node = walker.nextNode() as Text | null;\n }\n // Fallback: collapse at end of the root.\n range.selectNodeContents(root);\n range.collapse(false);\n}\n\n/** The overlay's positioning parent. Passes pointer input through everywhere\n * but the overlay itself, which spans no more than the canvas it covers. */\nfunction createClipBox(): HTMLDivElement {\n const el = document.createElement('div');\n el.style.position = 'absolute';\n el.style.margin = '0';\n el.style.padding = '0';\n el.style.border = '0';\n el.style.pointerEvents = 'none';\n return el;\n}\n\nfunction applyOverlayStyle(el: HTMLDivElement, style: ResolvedTextStyle): void {\n el.style.position = 'absolute';\n el.style.pointerEvents = 'auto';\n el.style.boxSizing = 'border-box';\n el.style.margin = '0';\n el.style.padding = '0';\n el.style.border = '0';\n el.style.outline = 'none';\n el.style.background = 'transparent';\n // Unfilled text still edits in black: the overlay has to be legible, and\n // there is no CSS spelling of \"no fill\" that leaves the glyphs visible.\n el.style.color = style.fill !== null && 'color' in style.fill ? style.fill.color : '#000';\n el.style.caretColor = style.caretColor;\n // The canvas's face, not the browser's reading of the family name.\n const fontFamily = cssFontFamily(style.fontFamily, {\n weight: numericWeight(style.fontWeight), style: style.fontStyle,\n });\n el.style.font = fontString({ ...style, fontFamily });\n // Linux Chromium otherwise rounds every advance to a whole pixel at DPR 1, while\n // the canvas lays out a face's own fractional ones: 'Small Caps' at 24px ran 3px\n // long. A canvas font is measured by the browser's default rendering, so keeps it.\n el.style.textRendering = isCanvasFont(style.fontFamily) ? '' : 'geometricPrecision';\n el.style.lineHeight = String(style.lineHeight);\n applyOverlayDirection(el, style);\n // Node-level decoration, so the overlay looks like the canvas the moment\n // editing starts. Runs are additive over the node style (a run can't un-set\n // a flag), which is exactly how CSS decoration propagates to descendants —\n // a run span adds its own decoration but can't remove this one.\n const decorations: string[] = [];\n if (style.underline) decorations.push('underline');\n if (style.strikethrough) decorations.push('line-through');\n if (style.overline) decorations.push('overline');\n el.style.textDecoration = decorations.length > 0 ? decorations.join(' ') : 'none';\n el.style.textTransform = style.textTransform;\n // The CSS spelling for what a browser reads; the scale for the pieces\n // `runsToDom` splits inheriting runs into, so they match the canvas.\n el.style.fontVariant = style.fontVariantCaps;\n el.style.setProperty(\n SMALL_CAPS_SCALE_PROPERTY,\n style.fontVariantCaps === 'small-caps'\n ? String(smallCapsScaleFor(style.fontFamily, numericWeight(style.fontWeight), style.fontStyle))\n : '',\n );\n // Line breaks follow the node's declared `wrap`, as the canvas's do. Layout\n // breaks only between words, never inside one.\n el.style.whiteSpace = style.wrap ? 'pre-wrap' : 'pre';\n el.style.overflowWrap = 'normal';\n el.style.wordBreak = 'normal';\n}\n\n/**\n * Inject a `<style>` element scoped to `overlayClass` so the edit overlay's\n * `::selection` matches the configured colors. Returns the style element so\n * the effect cleanup can remove it. Returns `null` when no selection\n * theming was requested.\n */\nfunction installSelectionStyle(\n overlayClass: string,\n style: ResolvedTextStyle,\n): HTMLStyleElement | null {\n if (style.selectionBackground == null && style.selectionColor == null) return null;\n const el = document.createElement('style');\n const decls: string[] = [];\n if (style.selectionBackground != null) {\n decls.push(`background: ${style.selectionBackground};`);\n }\n if (style.selectionColor != null) {\n decls.push(`color: ${style.selectionColor};`);\n }\n el.textContent = `.${overlayClass}::selection { ${decls.join(' ')} }`;\n document.head.appendChild(el);\n return el;\n}\n\nconst ALIGN_ANCHOR = { left: 0, center: 0.5, right: 1 } as const;\n\n/**\n * The overlay's `top`, in container pixels, for content `contentHeight` tall\n * (pre-scale, like the rest of `pose`): the pose's top, shifted by the same\n * `verticalAlign` slack the canvas applies, plus `drop` — how far below the\n * CSS line's baseline the canvas puts its own, from {@link baselineDrop}.\n * @internal\n */\nexport function overlayTop(pose: TextEditScreenPose, contentHeight: number, drop = 0): number {\n const slack = verticalAlignOffset(pose.verticalAlign, pose.height, contentHeight);\n return pose.y + (slack + drop) * (pose.zoom ?? 1);\n}\n\n/**\n * The canvas and CSS both hang a line's baseline half the leading plus one\n * ascent below the line top, from the same ascent rule, but each engine\n * rounds the font's metrics and that leading its own way, and a face that\n * states no ascent and descent hangs from its ascent alone. So the difference\n * is read from both sides rather than predicted: the canvas's from its own layout, the\n * overlay's from a zero-height inline box on a hidden line set exactly as the\n * overlay is. Pre-scale; `0` when either side has nothing to measure.\n * @internal\n */\nexport function baselineDrop(el: HTMLElement, style: ResolvedTextStyle, pose: TextEditScreenPose, lineHeight: number): number {\n const canvas = layoutTextPose({\n x: 0, y: 0, width: pose.width, height: pose.height, text: ' ',\n style: {\n fontFamily: style.fontFamily, fontWeight: style.fontWeight, fontStyle: style.fontStyle,\n fontSize: pose.fontSize, lineHeight, script: style.script,\n },\n }).laid.lines[0]?.baselineY ?? 0;\n const css = cssBaseline(el);\n return canvas > 0 && css > 0 ? canvas - css : 0;\n}\n\nconst CSS_BASELINES = new Map<string, number>();\n\nfunction cssBaseline(el: HTMLElement): number {\n const parent = el.parentElement;\n if (!parent) return 0;\n const { fontFamily, fontWeight, fontStyle, fontSize, lineHeight, textRendering } = el.style;\n const key = [fontFamily, fontWeight, fontStyle, fontSize, lineHeight, textRendering].join('|');\n const cached = CSS_BASELINES.get(key);\n if (cached !== undefined) return cached;\n const line = document.createElement('div');\n Object.assign(line.style, {\n position: 'absolute', left: '0', top: '0', width: '100px', margin: '0', padding: '0', border: '0',\n visibility: 'hidden', whiteSpace: 'pre', fontFamily, fontWeight, fontStyle, fontSize, lineHeight, textRendering,\n });\n const mark = document.createElement('span');\n Object.assign(mark.style, { display: 'inline-block', width: '0', height: '0', verticalAlign: 'baseline' });\n line.appendChild(mark);\n parent.appendChild(line);\n const box = line.getBoundingClientRect();\n // Rect over layout width undoes any transform on an ancestor.\n const scale = box.width / 100;\n const baseline = scale > 0 ? (mark.getBoundingClientRect().top - box.top) / scale : 0;\n line.remove();\n // A web font still loading is measured as its fallback; ask again later.\n if (baseline > 0 && document.fonts?.status !== 'loading') {\n if (CSS_BASELINES.size >= 64) CSS_BASELINES.clear();\n CSS_BASELINES.set(key, baseline);\n }\n return baseline;\n}\n\nfunction placeOverlay(\n el: HTMLDivElement,\n clipBox: HTMLDivElement,\n pose: TextEditScreenPose | null,\n clip: TextEditClipRect | null,\n style: ResolvedTextStyle,\n): void {\n if (!pose) {\n el.style.display = 'none';\n return;\n }\n // `clip` rather than `hidden`: a `hidden` box is still scrollable, and the\n // browser scrolls it to keep the caret in view.\n clipBox.style.overflow = clip ? 'clip' : 'visible';\n clipBox.style.left = `${clip?.x ?? 0}px`;\n clipBox.style.top = `${clip?.y ?? 0}px`;\n clipBox.style.width = `${clip?.width ?? 0}px`;\n clipBox.style.height = `${clip?.height ?? 0}px`;\n el.style.display = '';\n // An unwrapped line can be wider than its box, and the canvas aligns it\n // about the box's left edge, center or right edge regardless. The overlay\n // sizes to its text and grows from that same anchor: `left` puts the anchor\n // there and a trailing translate pulls the box back by that fraction of its\n // own width.\n const anchor = style.wrap ? 0 : ALIGN_ANCHOR[overlayAlignEdge(el, style)];\n el.style.left = `${pose.x + pose.width * (pose.zoom ?? 1) * anchor - (clip?.x ?? 0)}px`;\n el.style.width = style.wrap ? `${pose.width}px` : 'max-content';\n el.style.minWidth = style.wrap ? '' : `${pose.width}px`;\n // A node-level script draws the glyphs smaller and raised on the line the\n // plain text would hold, so the line height is pinned in pixels of the\n // unscripted size rather than left to scale with the font. The rise is\n // added to `top` with the baseline correction.\n const script = style.script\n ? scriptMetricsFor(style.fontFamily, numericWeight(style.fontWeight), style.fontStyle)[style.script]\n : undefined;\n const lineHeight = pose.lineHeight ?? style.lineHeight;\n el.style.fontSize = `${pose.fontSize * (script?.size ?? 1)}px`;\n el.style.lineHeight = script ? `${pose.fontSize * lineHeight}px` : String(lineHeight);\n // A top-aligned overlay fills its box, so a click anywhere in it lands in\n // the editor. Any other alignment needs the content's own height, which a\n // min-height would mask; `offsetHeight` is pre-transform, in pose units.\n const aligned = pose.verticalAlign !== undefined && pose.verticalAlign !== 'top';\n el.style.minHeight = aligned ? '' : `${pose.height}px`;\n const drop = baselineDrop(el, style, pose, lineHeight) - (script?.shift ?? 0) * pose.fontSize;\n el.style.top = `${overlayTop(pose, aligned ? el.offsetHeight : pose.height, drop) - (clip?.y ?? 0)}px`;\n // A declared `zoom` means every size on the pose is pre-scale and the\n // overlay carries the view scale as a transform. Anchored at the top-left so\n // `left`/`top` stay screen pixels. Written unconditionally (`none` at zoom\n // 1) because the overlay element outlives an edit session and would\n // otherwise keep a stale scale.\n el.style.transformOrigin = '0 0';\n const transforms: string[] = [];\n if (pose.zoom !== undefined && pose.zoom !== 1) transforms.push(`scale(${pose.zoom})`);\n if (anchor !== 0) transforms.push(`translateX(${-anchor * 100}%)`);\n el.style.transform = transforms.length > 0 ? transforms.join(' ') : 'none';\n // `letter-spacing` is not part of the CSS `font` shorthand, so\n // `applyOverlayStyle`'s `el.style.font` never carries it — it needs its own\n // assignment. It also lives here rather than there because without a\n // transform it is the one world-unit typography value that has to be\n // re-scaled as the view zooms.\n //\n // Under a transform there is nothing to re-scale: the world value is the\n // value, and run-level overrides — which `runsToDom` writes in world units\n // — scale by the same factor. Without one, `pose.fontSize` is documented as\n // `style.fontSize * zoom`, so their ratio is the world→screen factor; run\n // overrides then stay unscaled, which is why that path is only correct at\n // zoom 1. (CSS inheritance makes a run span's own declaration *replace* this\n // value rather than add to it, in both paths.)\n const scale =\n pose.zoom !== undefined ? 1 : style.fontSize > 0 ? pose.fontSize / style.fontSize : 1;\n el.style.letterSpacing = `${style.letterSpacing * scale}px`;\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { subscribeToSources as e } from "../../core/layers/render.js";
|
|
2
2
|
import { tiledProject as t } from "./tiledProject.js";
|
|
3
3
|
//#region src/features/tiling/createTiledLayer.ts
|
|
4
|
-
|
|
4
|
+
var n = 4096;
|
|
5
|
+
function r(e, t) {
|
|
5
6
|
return e === void 0 ? {
|
|
6
7
|
x: t,
|
|
7
8
|
y: t
|
|
@@ -13,8 +14,8 @@ function n(e, t) {
|
|
|
13
14
|
y: e.y ?? t
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
|
-
function
|
|
17
|
-
return new
|
|
17
|
+
function i(e, t) {
|
|
18
|
+
return new Float64Array([
|
|
18
19
|
1,
|
|
19
20
|
0,
|
|
20
21
|
0,
|
|
@@ -26,38 +27,39 @@ function r(e, t) {
|
|
|
26
27
|
1
|
|
27
28
|
]);
|
|
28
29
|
}
|
|
29
|
-
function
|
|
30
|
-
let { id:
|
|
30
|
+
function a(a) {
|
|
31
|
+
let { id: o, label: s, source: c, period: l, bleed: u } = a;
|
|
31
32
|
return {
|
|
32
|
-
id:
|
|
33
|
-
label:
|
|
33
|
+
id: o,
|
|
34
|
+
label: s,
|
|
34
35
|
space: "world",
|
|
35
|
-
subscribe: e(
|
|
36
|
-
draw: (e,
|
|
37
|
-
let
|
|
38
|
-
for (let t of
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
subscribe: e(c),
|
|
37
|
+
draw: (e, a, o) => {
|
|
38
|
+
let s = r(typeof l == "function" ? l(a, o) : l, Infinity), d = r(u, 0), f = o.width / a.scale.x, p = o.height / a.scale.y, m = t(a.x, a.x + f, s.x, d.x), h = t(a.y, a.y + p, s.y, d.y), g = [];
|
|
39
|
+
for (let t of c) (t.space ?? "world") === "screen" && g.push(...t.draw(e, a, o));
|
|
40
|
+
let _ = c.filter((e) => (e.space ?? "world") !== "screen");
|
|
41
|
+
if (!((m.to - m.from + 1) * (h.to - h.from + 1) <= n)) return g;
|
|
42
|
+
for (let t = m.from; t <= m.to; t++) for (let n = h.from; n <= h.to; n++) {
|
|
43
|
+
let r = Number.isFinite(s.x) ? t * s.x : 0, c = Number.isFinite(s.y) ? n * s.y : 0, l = {
|
|
44
|
+
...a,
|
|
45
|
+
x: a.x - r,
|
|
46
|
+
y: a.y - c
|
|
45
47
|
}, u = [];
|
|
46
|
-
for (let t of
|
|
47
|
-
u.length !== 0 &&
|
|
48
|
+
for (let t of _) u.push(...t.draw(e, l, o));
|
|
49
|
+
u.length !== 0 && g.push(r === 0 && c === 0 ? {
|
|
48
50
|
kind: "group",
|
|
49
51
|
children: u
|
|
50
52
|
} : {
|
|
51
53
|
kind: "group",
|
|
52
|
-
transform: r
|
|
54
|
+
transform: i(r, c),
|
|
53
55
|
children: u
|
|
54
56
|
});
|
|
55
57
|
}
|
|
56
|
-
return
|
|
58
|
+
return g;
|
|
57
59
|
}
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
//#endregion
|
|
61
|
-
export {
|
|
63
|
+
export { a as createTiledLayer };
|
|
62
64
|
|
|
63
65
|
//# sourceMappingURL=createTiledLayer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createTiledLayer.js","names":["axes","v","fallback","translate","tx","ty","createTiledLayer","opts","subscribeToSources","source","p","period","view","dims","b","bleed","spanX","spanY","kx","tiledProject","ky","out","layer","data","tiles","i","j","shifted","children"],"sources":["../../../src/features/tiling/createTiledLayer.ts"],"sourcesContent":["import type { DrawCommand } from '../../renderer';\nimport { subscribeToSources, type Dims, type RenderLayer } from '../../core/layers/render';\nimport type { View } from '../../core/viewport/view';\nimport { tiledProject } from './tiledProject';\n\n/**\n * How far apart the copies sit, in world units. A bare number wraps the x\n * axis only — the side-scrolling backdrop this is usually reached for.\n * `{ x, y }` wraps both; naming one key wraps that axis alone.\n *\n * The period names the axes, so there is no separate `axis` option: two ways\n * to say which axes wrap is one way for them to disagree.\n */\nexport type TilePeriod = number | { x?: number; y?: number };\n\n/** How far the content reaches outside its own cell, in world units. */\nexport type TileBleed = number | { x?: number; y?: number };\n\n/** Options for {@link createTiledLayer}. */\nexport interface CreateTiledLayerOpts<TData> {\n id: string;\n label: string;\n /** Layers redrawn once per visible copy. */\n source: RenderLayer<TData>[];\n /**\n * The lattice. A function is read per draw, so the period can follow the\n * camera or the canvas — \"one screen wide\" is a period, not a constant.\n */\n period: TilePeriod | ((view: View, dims: Dims) => TilePeriod);\n /**\n * How far the source's content reaches outside the cell `[0, period)`, in\n * world units. Content that overhangs by more than it declares pops in at\n * the edge of the view instead of scrolling in. Default 0.\n */\n bleed?: TileBleed;\n}\n\ninterface Axes { x: number; y: number }\n\nfunction axes(v: TilePeriod | TileBleed | undefined, fallback: number): Axes {\n if (v === undefined) return { x: fallback, y: fallback };\n if (typeof v === 'number') return { x: v, y: fallback };\n return { x: v.x ?? fallback, y: v.y ?? fallback };\n}\n\n/** Column-major, the layout `Mat3` documents. */\nfunction translate(tx: number, ty: number):
|
|
1
|
+
{"version":3,"file":"createTiledLayer.js","names":["MAX_COPIES","axes","v","fallback","translate","tx","ty","createTiledLayer","opts","subscribeToSources","source","p","period","view","dims","b","bleed","spanX","spanY","kx","tiledProject","ky","out","layer","data","tiles","i","j","shifted","children"],"sources":["../../../src/features/tiling/createTiledLayer.ts"],"sourcesContent":["import type { DrawCommand } from '../../renderer';\nimport { subscribeToSources, type Dims, type RenderLayer } from '../../core/layers/render';\nimport type { View } from '../../core/viewport/view';\nimport { tiledProject } from './tiledProject';\n\n/**\n * How far apart the copies sit, in world units. A bare number wraps the x\n * axis only — the side-scrolling backdrop this is usually reached for.\n * `{ x, y }` wraps both; naming one key wraps that axis alone.\n *\n * The period names the axes, so there is no separate `axis` option: two ways\n * to say which axes wrap is one way for them to disagree.\n */\nexport type TilePeriod = number | { x?: number; y?: number };\n\n/** How far the content reaches outside its own cell, in world units. */\nexport type TileBleed = number | { x?: number; y?: number };\n\n/** Options for {@link createTiledLayer}. */\nexport interface CreateTiledLayerOpts<TData> {\n id: string;\n label: string;\n /** Layers redrawn once per visible copy. */\n source: RenderLayer<TData>[];\n /**\n * The lattice. A function is read per draw, so the period can follow the\n * camera or the canvas — \"one screen wide\" is a period, not a constant.\n */\n period: TilePeriod | ((view: View, dims: Dims) => TilePeriod);\n /**\n * How far the source's content reaches outside the cell `[0, period)`, in\n * world units. Content that overhangs by more than it declares pops in at\n * the edge of the view instead of scrolling in. Default 0.\n */\n bleed?: TileBleed;\n}\n\ninterface Axes { x: number; y: number }\n\n/** Copies one draw will make. Past this the view is zoomed so far out that a\n * cell covers a pixel or two, and each copy still costs a full source draw. */\nconst MAX_COPIES = 4096;\n\nfunction axes(v: TilePeriod | TileBleed | undefined, fallback: number): Axes {\n if (v === undefined) return { x: fallback, y: fallback };\n if (typeof v === 'number') return { x: v, y: fallback };\n return { x: v.x ?? fallback, y: v.y ?? fallback };\n}\n\n/** Column-major, the layout `Mat3` documents. */\nfunction translate(tx: number, ty: number): Float64Array {\n return new Float64Array([1, 0, 0, 0, 1, 0, tx, ty, 1]);\n}\n\n/**\n * Wrap source RenderLayers in a periodic lattice: the content is authored\n * once, in the cell `[0, period)`, and every copy of it the view can see is\n * drawn. Panning in either direction keeps producing copies, so the content\n * loops seamlessly with no state and no wrap-around bookkeeping.\n *\n * Each copy draws the source through a view shifted by `-k * period`, so a\n * source that culls against the view it is handed culls per copy and never\n * learns it is being tiled. The copy's commands are then translated by\n * `+k * period` in world space.\n *\n * The lattice is per layer, not per shape. Content on two different periods\n * is two tiled layers stacked, which keeps each source ignorant of the other\n * — a per-shape period would move the lattice into the source and make the\n * wrapper unable to cull.\n *\n * The plane is `space: 'world'`, which is what lets it be a `source` of\n * `createParallaxLayer`: the parallax plane hands it the derived inner view,\n * the lattice resolves against that, and the copies ride the plane.\n *\n * **Zoomed out until a draw would take more than a few thousand copies, the\n * lattice draws none.** A cell is then a pixel or two across.\n *\n * **A `space: 'screen'` source is drawn once, untiled** — it ignores the view\n * by definition, so every copy would land on top of the last. Same constraint\n * `createParallaxLayer` carries.\n */\nexport function createTiledLayer<TData>(\n opts: CreateTiledLayerOpts<TData>,\n): RenderLayer<TData> {\n const { id, label, source, period, bleed } = opts;\n return {\n id,\n label,\n space: 'world',\n subscribe: subscribeToSources(source),\n draw: (data, view, dims) => {\n const p = axes(typeof period === 'function' ? period(view, dims) : period, Infinity);\n const b = axes(bleed, 0);\n const spanX = dims.width / view.scale.x;\n const spanY = dims.height / view.scale.y;\n const kx = tiledProject(view.x, view.x + spanX, p.x, b.x);\n const ky = tiledProject(view.y, view.y + spanY, p.y, b.y);\n\n const out: DrawCommand[] = [];\n for (const layer of source) {\n if ((layer.space ?? 'world') === 'screen') out.push(...layer.draw(data, view, dims));\n }\n const tiles = source.filter((layer) => (layer.space ?? 'world') !== 'screen');\n // Written so a non-finite span, which a zero view scale gives, fails it too.\n if (!((kx.to - kx.from + 1) * (ky.to - ky.from + 1) <= MAX_COPIES)) return out;\n\n for (let i = kx.from; i <= kx.to; i++) {\n for (let j = ky.from; j <= ky.to; j++) {\n const tx = Number.isFinite(p.x) ? i * p.x : 0;\n const ty = Number.isFinite(p.y) ? j * p.y : 0;\n const shifted: View = { ...view, x: view.x - tx, y: view.y - ty };\n const children: DrawCommand[] = [];\n for (const layer of tiles) children.push(...layer.draw(data, shifted, dims));\n if (children.length === 0) continue;\n out.push(tx === 0 && ty === 0\n ? { kind: 'group', children }\n : { kind: 'group', transform: translate(tx, ty), children });\n }\n }\n return out;\n },\n };\n}\n"],"mappings":";;;AAyCA,IAAMA,IAAa;AAEnB,SAASC,EAAK,GAAuC,GAAwB;CAG3E,OAFIC,MAAM,KAAA,IAAkB;EAAE,GAAGC;EAAU,GAAGA;CAAS,IACnD,OAAOD,KAAM,WAAiB;EAAE,GAAGA;EAAG,GAAGC;CAAS,IAC/C;EAAE,GAAGD,EAAE,KAAKC;EAAU,GAAGD,EAAE,KAAKC;CAAS;AAClD;AAGA,SAASC,EAAU,GAAY,GAA0B;CACvD,OAAO,IAAI,aAAa;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAGC;EAAIC;EAAI;CAAC,CAAC;AACvD;AA6BA,SAAgBC,EACd,GACoB;CACpB,IAAM,EAAE,OAAI,UAAO,WAAQ,WAAQ,aAAUC;CAC7C,OAAO;EACL;EACA;EACA,OAAO;EACP,WAAWC,EAAmBC,CAAM;EACpC,OAAO,GAAM,GAAM,MAAS;GAC1B,IAAMC,IAAIV,EAAK,OAAOW,KAAW,aAAaA,EAAOC,GAAMC,CAAI,IAAIF,GAAQ,QAAQ,GAC7EG,IAAId,EAAKe,GAAO,CAAC,GACjBC,IAAQH,EAAK,QAAQD,EAAK,MAAM,GAChCK,IAAQJ,EAAK,SAASD,EAAK,MAAM,GACjCM,IAAKC,EAAaP,EAAK,GAAGA,EAAK,IAAII,GAAON,EAAE,GAAGI,EAAE,CAAC,GAClDM,IAAKD,EAAaP,EAAK,GAAGA,EAAK,IAAIK,GAAOP,EAAE,GAAGI,EAAE,CAAC,GAElDO,IAAqB,CAAC;GAC5B,KAAK,IAAMC,KAASb,GAClB,CAAKa,EAAM,SAAS,aAAa,YAAUD,EAAI,KAAK,GAAGC,EAAM,KAAKC,GAAMX,GAAMC,CAAI,CAAC;GAErF,IAAMW,IAAQf,EAAO,QAAQ,OAAWa,EAAM,SAAS,aAAa,QAAQ;GAE5E,IAAI,GAAGJ,EAAG,KAAKA,EAAG,OAAO,MAAME,EAAG,KAAKA,EAAG,OAAO,MAAMrB,IAAa,OAAOsB;GAE3E,KAAK,IAAII,IAAIP,EAAG,MAAMO,KAAKP,EAAG,IAAI,KAChC,KAAK,IAAIQ,IAAIN,EAAG,MAAMM,KAAKN,EAAG,IAAI,KAAK;IACrC,IAAMhB,IAAK,OAAO,SAASM,EAAE,CAAC,IAAIe,IAAIf,EAAE,IAAI,GACtCL,IAAK,OAAO,SAASK,EAAE,CAAC,IAAIgB,IAAIhB,EAAE,IAAI,GACtCiB,IAAgB;KAAE,GAAGf;KAAM,GAAGA,EAAK,IAAIR;KAAI,GAAGQ,EAAK,IAAIP;IAAG,GAC1DuB,IAA0B,CAAC;IACjC,KAAK,IAAMN,KAASE,GAAO,EAAS,KAAK,GAAGF,EAAM,KAAKC,GAAMI,GAASd,CAAI,CAAC;IAC3E,AAAIe,EAAS,WAAW,KACxBP,EAAI,KAAKjB,MAAO,KAAKC,MAAO,IACxB;KAAE,MAAM;KAAS;IAAS,IAC1B;KAAE,MAAM;KAAS,WAAWF,EAAUC,GAAIC,CAAE;KAAG;IAAS,CAAC;GAC/D;GAEF,OAAOgB;EACT;CACF;AACF"}
|