silvery 0.19.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/Text-Lq0dmj8-.mjs +239 -0
- package/dist/Text-Lq0dmj8-.mjs.map +1 -0
- package/dist/UPNG-Bo33r8rA.mjs +3 -0
- package/dist/UPNG-DosRPdF4.mjs +5075 -0
- package/dist/UPNG-DosRPdF4.mjs.map +1 -0
- package/dist/__vite-browser-external-2447137e-D_JM6skp.mjs +6 -0
- package/dist/__vite-browser-external-2447137e-D_JM6skp.mjs.map +1 -0
- package/dist/{animation-Cn64yepo.mjs → animation-ZMN2_XKv.mjs} +2 -2
- package/dist/animation-ZMN2_XKv.mjs.map +1 -0
- package/dist/{ansi-Cc33mW54.d.mts → ansi-2Xn0yatP.d.mts} +1 -1
- package/dist/{ansi-Cc33mW54.d.mts.map → ansi-2Xn0yatP.d.mts.map} +1 -1
- package/dist/{ansi-CLOitHKx.mjs → ansi-D1KQMAbf.mjs} +1 -1
- package/dist/{ansi-CLOitHKx.mjs.map → ansi-D1KQMAbf.mjs.map} +1 -1
- package/dist/ansi-yC4RyBNY.mjs +22441 -0
- package/dist/ansi-yC4RyBNY.mjs.map +1 -0
- package/dist/apng-CR08rIaH.mjs +58 -0
- package/dist/apng-CR08rIaH.mjs.map +1 -0
- package/dist/apng-DaHfVaVI.mjs +3 -0
- package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
- package/dist/assets/skia.darwin-arm64-DQs5sT6N.node +0 -0
- package/dist/backend-B-WYLUib.mjs +13396 -0
- package/dist/backend-B-WYLUib.mjs.map +1 -0
- package/dist/backends-CUtan80W.mjs +3 -0
- package/dist/backends-DIVYzKqd.mjs +1083 -0
- package/dist/backends-DIVYzKqd.mjs.map +1 -0
- package/dist/bound-term-0sPrrzH1.d.mts +4640 -0
- package/dist/bound-term-0sPrrzH1.d.mts.map +1 -0
- package/dist/canvas-1v7dPT-_.mjs +3 -0
- package/dist/canvas-CSuPOMNt.mjs +1442 -0
- package/dist/canvas-CSuPOMNt.mjs.map +1 -0
- package/dist/{chunk-Vs_PY4HZ.mjs → chunk-BSw8zbkd.mjs} +1 -1
- package/dist/cli-dvo0r2fs.mjs +4 -0
- package/dist/compare-CQodSH4G.mjs +376 -0
- package/dist/compare-CQodSH4G.mjs.map +1 -0
- package/dist/compare-DHlcxEYA.mjs +3 -0
- package/dist/context-BU5LkkIy.mjs.map +1 -1
- package/dist/devtools-CJdt5H0X.mjs +2 -0
- package/dist/{devtools-DxkSLXDA.mjs → devtools-DcQjgyjL.mjs} +5 -4
- package/dist/{devtools-DxkSLXDA.mjs.map → devtools-DcQjgyjL.mjs.map} +1 -1
- package/dist/easing-BI-ASGMO.d.mts +24 -0
- package/dist/easing-BI-ASGMO.d.mts.map +1 -0
- package/dist/{eta-Bb3RH3wh.mjs → eta-CJlGH06n.mjs} +1 -1
- package/dist/{eta-Bb3RH3wh.mjs.map → eta-CJlGH06n.mjs.map} +1 -1
- package/dist/flexily-zero-adapter-C3Vj0fPt.mjs +306 -0
- package/dist/flexily-zero-adapter-C3Vj0fPt.mjs.map +1 -0
- package/dist/{flexily-zero-adapter-CMxXhdOL.mjs → flexily-zero-adapter-C4lW_Ov5.mjs} +1 -1
- package/dist/fonts-BFmhXDv7.mjs +88 -0
- package/dist/fonts-BFmhXDv7.mjs.map +1 -0
- package/dist/gif-C_AjaT9d.mjs +188 -0
- package/dist/gif-C_AjaT9d.mjs.map +1 -0
- package/dist/gif-DaC4XrxA.mjs +3 -0
- package/dist/gifenc-BOUT-KFB.mjs +730 -0
- package/dist/gifenc-BOUT-KFB.mjs.map +1 -0
- package/dist/image-C2Birh2x.mjs +1252 -0
- package/dist/image-C2Birh2x.mjs.map +1 -0
- package/dist/index-BUMxS65f.d.mts +453 -0
- package/dist/index-BUMxS65f.d.mts.map +1 -0
- package/dist/{index-D3saHouR.d.mts → index-CSQf13CI.d.mts} +1057 -1133
- package/dist/index-CSQf13CI.d.mts.map +1 -0
- package/dist/{index-BXslOebb.d.mts → index-Cl9KKjQ_.d.mts} +4919 -3921
- package/dist/index-Cl9KKjQ_.d.mts.map +1 -0
- package/dist/index-XbNrPhWl.d.mts +336 -0
- package/dist/index-XbNrPhWl.d.mts.map +1 -0
- package/dist/index.d.mts +8 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +14 -12
- package/dist/index.mjs.map +1 -1
- package/dist/key-mapping-CS-YD_cD.mjs +132 -0
- package/dist/key-mapping-CS-YD_cD.mjs.map +1 -0
- package/dist/key-mapping-Yn-Jgrij.mjs +3 -0
- package/dist/{layout-engine-B6Cdz1yZ.mjs → layout-engine-C07LEXWT.mjs} +1 -1
- package/dist/layout-engine-C2px0RJE.mjs +67 -0
- package/dist/layout-engine-C2px0RJE.mjs.map +1 -0
- package/dist/layout-signals-Cnw6xk8Q.mjs +988 -0
- package/dist/layout-signals-Cnw6xk8Q.mjs.map +1 -0
- package/dist/mouse-events-Dki3ISIp.mjs +1044 -0
- package/dist/mouse-events-Dki3ISIp.mjs.map +1 -0
- package/dist/{multi-progress-Bq9Oi_WI.mjs → multi-progress-CIRjrzma.mjs} +3 -3
- package/dist/{multi-progress-Bq9Oi_WI.mjs.map → multi-progress-CIRjrzma.mjs.map} +1 -1
- package/dist/{multi-progress-DAQC7eap.d.mts → multi-progress-DHZ2xUT2.d.mts} +2 -2
- package/dist/{multi-progress-DAQC7eap.d.mts.map → multi-progress-DHZ2xUT2.d.mts.map} +1 -1
- package/dist/{node-BeWlnCPY.mjs → node-CjM5Rt-M.mjs} +4 -4
- package/dist/node-CjM5Rt-M.mjs.map +1 -0
- package/dist/playwright-D5YiZcNS.mjs +76397 -0
- package/dist/playwright-D5YiZcNS.mjs.map +1 -0
- package/dist/png-codec-Dp84742B.mjs +36 -0
- package/dist/png-codec-Dp84742B.mjs.map +1 -0
- package/dist/png-codec-QwOtJ8Zs.mjs +3 -0
- package/dist/progress-DB_Xo071.mjs +675 -0
- package/dist/progress-DB_Xo071.mjs.map +1 -0
- package/dist/{progress-bar-CXE5Qfkd.mjs → progress-bar-oJwq22CR.mjs} +4 -4
- package/dist/{progress-bar-CXE5Qfkd.mjs.map → progress-bar-oJwq22CR.mjs.map} +1 -1
- package/dist/rasterizer-BRXrDdWx.mjs +3 -0
- package/dist/rasterizer-CpEhJvdR.mjs +296 -0
- package/dist/rasterizer-CpEhJvdR.mjs.map +1 -0
- package/dist/reconciler-DldIJB93.mjs +2083 -0
- package/dist/reconciler-DldIJB93.mjs.map +1 -0
- package/dist/{render-string-CDCeYkS3.mjs → render-string-BcoCpjCB.mjs} +1 -1
- package/dist/{render-string-Darrg7ku.mjs → render-string-DkQacASz.mjs} +2707 -549
- package/dist/render-string-DkQacASz.mjs.map +1 -0
- package/dist/resvg-js-DkOndZI3.mjs +203 -0
- package/dist/resvg-js-DkOndZI3.mjs.map +1 -0
- package/dist/runtime.d.mts +3 -2
- package/dist/runtime.mjs +3 -3
- package/dist/schemes-JjNp4aSl.mjs +2611 -0
- package/dist/schemes-JjNp4aSl.mjs.map +1 -0
- package/dist/{spinner-CGo34vyR.d.mts → spinner-CZINHpkV.d.mts} +2 -2
- package/dist/{spinner-CGo34vyR.d.mts.map → spinner-CZINHpkV.d.mts.map} +1 -1
- package/dist/{spinner-CeOmcuw_.mjs → spinner-D9lrHr8s.mjs} +7 -7
- package/dist/spinner-D9lrHr8s.mjs.map +1 -0
- package/dist/src-5w9QR6_8.mjs +1071 -0
- package/dist/src-5w9QR6_8.mjs.map +1 -0
- package/dist/src-BNTToU7l.mjs +4387 -0
- package/dist/src-BNTToU7l.mjs.map +1 -0
- package/dist/{src-CF-6UN01.mjs → src-BR4xNwdG.mjs} +10436 -2622
- package/dist/src-BR4xNwdG.mjs.map +1 -0
- package/dist/{types-Bk2yw9Qj.mjs → src-DKp-_OFG.mjs} +34 -94
- package/dist/src-DKp-_OFG.mjs.map +1 -0
- package/dist/src-bt8wSrfJ.mjs +258 -0
- package/dist/src-bt8wSrfJ.mjs.map +1 -0
- package/dist/src-e33Y6kNJ.mjs +3 -0
- package/dist/src-iDwu25UD.mjs +1814 -0
- package/dist/src-iDwu25UD.mjs.map +1 -0
- package/dist/steps-Bp2uNqnn.d.mts +202 -0
- package/dist/steps-Bp2uNqnn.d.mts.map +1 -0
- package/dist/svg-15lZZzxq.mjs +486 -0
- package/dist/svg-15lZZzxq.mjs.map +1 -0
- package/dist/svg-Cz0UXcDj.mjs +255 -0
- package/dist/svg-Cz0UXcDj.mjs.map +1 -0
- package/dist/svg-DY72a4HK.mjs +3 -0
- package/dist/svg-g1D6ErwR.d.mts +82 -0
- package/dist/svg-g1D6ErwR.d.mts.map +1 -0
- package/dist/term.d.mts +3 -0
- package/dist/term.mjs +9 -0
- package/dist/term.mjs.map +1 -0
- package/dist/theme.d.mts +95 -2
- package/dist/theme.d.mts.map +1 -0
- package/dist/theme.mjs +9 -3
- package/dist/theme.mjs.map +1 -0
- package/dist/{types-BH_v3iMT.d.mts → types-kt_fKR37.d.mts} +2 -15
- package/dist/types-kt_fKR37.d.mts.map +1 -0
- package/dist/ui/animation.d.mts +2 -1
- package/dist/ui/animation.mjs +1 -1
- package/dist/ui/ansi.d.mts +1 -1
- package/dist/ui/ansi.mjs +1 -1
- package/dist/ui/cli.d.mts +3 -3
- package/dist/ui/cli.mjs +5 -5
- package/dist/ui/display.d.mts +1 -1
- package/dist/ui/image.d.mts +2 -2
- package/dist/ui/image.mjs +2 -2
- package/dist/ui/input.d.mts +1 -1
- package/dist/ui/input.mjs +4 -2
- package/dist/ui/input.mjs.map +1 -1
- package/dist/ui/progress.d.mts +5 -249
- package/dist/ui/progress.mjs +5 -858
- package/dist/ui/react.d.mts +1 -1
- package/dist/ui/react.mjs +2 -2
- package/dist/ui/recording-chrome-react.d.mts +21 -0
- package/dist/ui/recording-chrome-react.d.mts.map +1 -0
- package/dist/ui/recording-chrome-react.mjs +105 -0
- package/dist/ui/recording-chrome-react.mjs.map +1 -0
- package/dist/ui/recording-chrome.d.mts +2 -0
- package/dist/ui/recording-chrome.mjs +2 -0
- package/dist/ui/utils.mjs +1 -1
- package/dist/ui/wrappers.d.mts +3 -3
- package/dist/ui/wrappers.mjs +2 -2
- package/dist/ui.d.mts +7 -6
- package/dist/ui.mjs +8 -7
- package/dist/{useLatest-Bg2x4bfP.d.mts → useLatest-DRDDVwjh.d.mts} +5 -25
- package/dist/useLatest-DRDDVwjh.d.mts.map +1 -0
- package/dist/{with-text-input-CRfoiFFG.d.mts → with-text-input-YeohVLeo.d.mts} +4 -55
- package/dist/with-text-input-YeohVLeo.d.mts.map +1 -0
- package/dist/wrapper-C70ATkVv.mjs +3527 -0
- package/dist/wrapper-C70ATkVv.mjs.map +1 -0
- package/dist/{wrappers-UTADQkSY.mjs → wrappers-BCUYITrY.mjs} +5 -157
- package/dist/wrappers-BCUYITrY.mjs.map +1 -0
- package/dist/{yoga-adapter-8oRGRw8V.mjs → yoga-adapter-BnZX1PAY.mjs} +28 -2
- package/dist/yoga-adapter-BnZX1PAY.mjs.map +1 -0
- package/dist/yoga-adapter-DxgsQ_gg.mjs +2 -0
- package/dist/zipBundle-3nqeDRtm.mjs +3 -0
- package/dist/zipBundle-VNAYFmqJ.mjs +2003 -0
- package/dist/zipBundle-VNAYFmqJ.mjs.map +1 -0
- package/package.json +20 -9
- package/dist/animation-Cn64yepo.mjs.map +0 -1
- package/dist/cli-BKp0YtBD.mjs +0 -4
- package/dist/devtools-9QY4teqI.mjs +0 -2
- package/dist/flexily-zero-adapter-BlQa46nr.mjs +0 -3385
- package/dist/flexily-zero-adapter-BlQa46nr.mjs.map +0 -1
- package/dist/image-CTII5QWI.mjs +0 -477
- package/dist/image-CTII5QWI.mjs.map +0 -1
- package/dist/index-BXslOebb.d.mts.map +0 -1
- package/dist/index-BnA7mNpo.d.mts +0 -175
- package/dist/index-BnA7mNpo.d.mts.map +0 -1
- package/dist/index-D3saHouR.d.mts.map +0 -1
- package/dist/layout-engine-ClUgv6jB.mjs +0 -50
- package/dist/layout-engine-ClUgv6jB.mjs.map +0 -1
- package/dist/node-BeWlnCPY.mjs.map +0 -1
- package/dist/reconciler-Cwgm8hRR.mjs +0 -8459
- package/dist/reconciler-Cwgm8hRR.mjs.map +0 -1
- package/dist/render-string-Darrg7ku.mjs.map +0 -1
- package/dist/spinner-CeOmcuw_.mjs.map +0 -1
- package/dist/src-B5GjfG7g.mjs +0 -4305
- package/dist/src-B5GjfG7g.mjs.map +0 -1
- package/dist/src-CChwjk0Z.mjs +0 -738
- package/dist/src-CChwjk0Z.mjs.map +0 -1
- package/dist/src-CF-6UN01.mjs.map +0 -1
- package/dist/src-NCKb8kE5.mjs +0 -2660
- package/dist/src-NCKb8kE5.mjs.map +0 -1
- package/dist/types-BH_v3iMT.d.mts.map +0 -1
- package/dist/types-Bk2yw9Qj.mjs.map +0 -1
- package/dist/ui/progress.d.mts.map +0 -1
- package/dist/ui/progress.mjs.map +0 -1
- package/dist/useLatest-Bg2x4bfP.d.mts.map +0 -1
- package/dist/with-text-input-CRfoiFFG.d.mts.map +0 -1
- package/dist/wrappers-UTADQkSY.mjs.map +0 -1
- package/dist/yoga-adapter-8oRGRw8V.mjs.map +0 -1
- package/dist/yoga-adapter-D_CcxSt5.mjs +0 -2
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
import { i as signal } from "./src-DKp-_OFG.mjs";
|
|
2
|
+
//#region packages/ag/src/types.ts
|
|
3
|
+
/**
|
|
4
|
+
* Check if two rects are equal (same position and size).
|
|
5
|
+
*/
|
|
6
|
+
function rectEqual$1(a, b) {
|
|
7
|
+
if (a === b) return true;
|
|
8
|
+
if (!a || !b) return false;
|
|
9
|
+
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region packages/ag/src/wrap-measurer.ts
|
|
13
|
+
let _measurer = null;
|
|
14
|
+
/**
|
|
15
|
+
* Register the active wrap measurer. Pass `null` to clear (test teardown,
|
|
16
|
+
* or a Term disposing its runtime).
|
|
17
|
+
*
|
|
18
|
+
* Idempotent: setting the same reference twice is a no-op. Setting a new
|
|
19
|
+
* reference replaces the previous one — there's no stack. v1 assumes a
|
|
20
|
+
* single Term-per-process consumer; multi-Term setups need a different
|
|
21
|
+
* dispatch (see file header).
|
|
22
|
+
*/
|
|
23
|
+
function setWrapMeasurer(m) {
|
|
24
|
+
_measurer = m;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read the active wrap measurer, or `null` if none is registered.
|
|
28
|
+
*
|
|
29
|
+
* Geometry helpers (`computeSelectionFragments` is the v1 consumer) call
|
|
30
|
+
* this at compute-time — not at module-load — so the registration order
|
|
31
|
+
* doesn't matter. The fragment helper falls back to `\n`-split when this
|
|
32
|
+
* returns null.
|
|
33
|
+
*/
|
|
34
|
+
function getWrapMeasurer() {
|
|
35
|
+
return _measurer;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region packages/ag/src/place-floating.ts
|
|
39
|
+
function splitPlacement(placement) {
|
|
40
|
+
const dashIdx = placement.indexOf("-");
|
|
41
|
+
return {
|
|
42
|
+
side: placement.slice(0, dashIdx),
|
|
43
|
+
align: placement.slice(dashIdx + 1)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function oppositePlacement(placement) {
|
|
47
|
+
const { side, align } = splitPlacement(placement);
|
|
48
|
+
return `${{
|
|
49
|
+
top: "bottom",
|
|
50
|
+
bottom: "top",
|
|
51
|
+
left: "right",
|
|
52
|
+
right: "left"
|
|
53
|
+
}[side]}-${align}`;
|
|
54
|
+
}
|
|
55
|
+
function rectFitsWithin(rect, boundary) {
|
|
56
|
+
return rect.x >= boundary.x && rect.y >= boundary.y && rect.x + rect.width <= boundary.x + boundary.width && rect.y + rect.height <= boundary.y + boundary.height;
|
|
57
|
+
}
|
|
58
|
+
function clampAxis(value, min, max) {
|
|
59
|
+
if (max < min) return min;
|
|
60
|
+
return Math.min(max, Math.max(min, value));
|
|
61
|
+
}
|
|
62
|
+
function shiftIntoBoundary(rect, boundary) {
|
|
63
|
+
return {
|
|
64
|
+
x: clampAxis(rect.x, boundary.x, boundary.x + boundary.width - rect.width),
|
|
65
|
+
y: clampAxis(rect.y, boundary.y, boundary.y + boundary.height - rect.height),
|
|
66
|
+
width: rect.width,
|
|
67
|
+
height: rect.height
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function rectEqual(a, b) {
|
|
71
|
+
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
72
|
+
}
|
|
73
|
+
function sideOverflow(rect, boundary, side) {
|
|
74
|
+
switch (side) {
|
|
75
|
+
case "top": return Math.max(0, boundary.y - rect.y);
|
|
76
|
+
case "bottom": return Math.max(0, rect.y + rect.height - (boundary.y + boundary.height));
|
|
77
|
+
case "left": return Math.max(0, boundary.x - rect.x);
|
|
78
|
+
case "right": return Math.max(0, rect.x + rect.width - (boundary.x + boundary.width));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Compute the absolute rect at which a floating decoration should be painted
|
|
83
|
+
* relative to its anchor.
|
|
84
|
+
*
|
|
85
|
+
* Inputs:
|
|
86
|
+
* - `anchor`: the anchor's rect (typically `findAnchor(root, id)` →
|
|
87
|
+
* `contentRect`). Origin in the same absolute terminal cell space as
|
|
88
|
+
* other rect signals.
|
|
89
|
+
* - `target.{width, height}`: intrinsic size of the floating decoration.
|
|
90
|
+
* Both must be `>= 0` — this function does not enforce minimums; callers
|
|
91
|
+
* pass through what their renderer asked for.
|
|
92
|
+
* - `placement`: 12-placement vocabulary. See module docstring for the
|
|
93
|
+
* visual reference.
|
|
94
|
+
*
|
|
95
|
+
* Output: `Rect` in the same coordinate space as `anchor`. Width/height equal
|
|
96
|
+
* `target.width`/`target.height` exactly (no clamping, no shifting).
|
|
97
|
+
*
|
|
98
|
+
* **Pure**: no allocation other than the result rect, no I/O, no logging.
|
|
99
|
+
* Suitable for property tests and SILVERY_STRICT cross-checks.
|
|
100
|
+
*/
|
|
101
|
+
function placeFloating(anchor, target, placement, options = {}) {
|
|
102
|
+
const { x: ax, y: ay, width: aw, height: ah } = anchor;
|
|
103
|
+
const tw = target.width;
|
|
104
|
+
const th = target.height;
|
|
105
|
+
const { side, align } = splitPlacement(placement);
|
|
106
|
+
let x = 0;
|
|
107
|
+
let y = 0;
|
|
108
|
+
if (side === "top" || side === "bottom") {
|
|
109
|
+
y = side === "top" ? ay - th : ay + ah;
|
|
110
|
+
if (align === "start") x = ax;
|
|
111
|
+
else if (align === "end") x = ax + aw - tw;
|
|
112
|
+
else x = ax + Math.round((aw - tw) / 2);
|
|
113
|
+
} else {
|
|
114
|
+
x = side === "left" ? ax - tw : ax + aw;
|
|
115
|
+
if (align === "start") y = ay;
|
|
116
|
+
else if (align === "end") y = ay + ah - th;
|
|
117
|
+
else y = ay + Math.round((ah - th) / 2);
|
|
118
|
+
}
|
|
119
|
+
const offset = options.offset ?? 0;
|
|
120
|
+
const alignOffset = options.alignOffset ?? 0;
|
|
121
|
+
if (side === "top") y -= offset;
|
|
122
|
+
else if (side === "bottom") y += offset;
|
|
123
|
+
else if (side === "left") x -= offset;
|
|
124
|
+
else x += offset;
|
|
125
|
+
if (side === "top" || side === "bottom") x += alignOffset;
|
|
126
|
+
else y += alignOffset;
|
|
127
|
+
return {
|
|
128
|
+
x,
|
|
129
|
+
y,
|
|
130
|
+
width: tw,
|
|
131
|
+
height: th
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Resolve a floating rect with optional viewport collision handling.
|
|
136
|
+
*
|
|
137
|
+
* This is the collision-aware peer of `placeFloating`. The fixed-placement
|
|
138
|
+
* helper remains intentionally simple and deterministic; this function adds
|
|
139
|
+
* the behavior needed by declarative popovers/tooltips: gap offsets, alignment
|
|
140
|
+
* nudges, side flipping, viewport shifting, and hide-on-overflow.
|
|
141
|
+
*/
|
|
142
|
+
function resolveFloatingPlacement(anchor, target, placement, options = {}) {
|
|
143
|
+
const boundary = options.boundary ?? null;
|
|
144
|
+
const strategy = options.collisionStrategy ?? "none";
|
|
145
|
+
const requested = placeFloating(anchor, target, placement, options);
|
|
146
|
+
if (!boundary || strategy === "none") return {
|
|
147
|
+
rect: requested,
|
|
148
|
+
placement,
|
|
149
|
+
flipped: false,
|
|
150
|
+
shifted: false
|
|
151
|
+
};
|
|
152
|
+
if (strategy === "hide") return rectFitsWithin(requested, boundary) ? {
|
|
153
|
+
rect: requested,
|
|
154
|
+
placement,
|
|
155
|
+
flipped: false,
|
|
156
|
+
shifted: false
|
|
157
|
+
} : null;
|
|
158
|
+
let finalPlacement = placement;
|
|
159
|
+
let rect = requested;
|
|
160
|
+
let flipped = false;
|
|
161
|
+
if (strategy === "flip" || strategy === "flip-then-shift") {
|
|
162
|
+
const side = splitPlacement(placement).side;
|
|
163
|
+
const requestedSideOverflow = sideOverflow(requested, boundary, side);
|
|
164
|
+
if (requestedSideOverflow > 0) {
|
|
165
|
+
const candidatePlacement = oppositePlacement(placement);
|
|
166
|
+
const candidate = placeFloating(anchor, target, candidatePlacement, options);
|
|
167
|
+
const candidateSide = splitPlacement(candidatePlacement).side;
|
|
168
|
+
if (sideOverflow(candidate, boundary, candidateSide) < requestedSideOverflow) {
|
|
169
|
+
finalPlacement = candidatePlacement;
|
|
170
|
+
rect = candidate;
|
|
171
|
+
flipped = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
let shifted = false;
|
|
176
|
+
if (strategy === "shift" || strategy === "flip-then-shift") {
|
|
177
|
+
const shiftedRect = shiftIntoBoundary(rect, boundary);
|
|
178
|
+
shifted = !rectEqual(shiftedRect, rect);
|
|
179
|
+
rect = shiftedRect;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
rect,
|
|
183
|
+
placement: finalPlacement,
|
|
184
|
+
flipped,
|
|
185
|
+
shifted
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region packages/ag/src/layout-signals.ts
|
|
190
|
+
/**
|
|
191
|
+
* withLayoutSignals — reactive signal layer for AgNode layout outputs.
|
|
192
|
+
*
|
|
193
|
+
* Composable plugin that wraps an AgNode with reactive signals for layout
|
|
194
|
+
* rects, text content, and focus state. Engine-agnostic — works with
|
|
195
|
+
* Flexily, Yoga, or any future layout engine.
|
|
196
|
+
*
|
|
197
|
+
* Signals are WeakMap-backed and lazily created. Nodes without subscribers
|
|
198
|
+
* pay zero cost. After layout completes, the pipeline calls `syncSignals()`
|
|
199
|
+
* to propagate imperative state into signals.
|
|
200
|
+
*
|
|
201
|
+
* ## Usage
|
|
202
|
+
*
|
|
203
|
+
* ```ts
|
|
204
|
+
* import { getLayoutSignals, syncSignals } from "@silvery/ag/layout-signals"
|
|
205
|
+
*
|
|
206
|
+
* // Get (or create) signals for a node
|
|
207
|
+
* const signals = getLayoutSignals(node)
|
|
208
|
+
* signals.boxRect() // read current rect
|
|
209
|
+
* signals.textContent() // read current text
|
|
210
|
+
*
|
|
211
|
+
* // After layout/reconciler mutations, sync imperative → reactive
|
|
212
|
+
* syncSignals(node)
|
|
213
|
+
* ```
|
|
214
|
+
*
|
|
215
|
+
* ## Three-layer stack
|
|
216
|
+
*
|
|
217
|
+
* Layer 0: alien-signals (signal, computed, effect)
|
|
218
|
+
* Layer 1: getLayoutSignals() — this module (@silvery/ag, framework-agnostic)
|
|
219
|
+
* Layer 2: useSignal(signal) — @silvery/ag-react (React bridge)
|
|
220
|
+
* Layer 3: useBoxRect(), useAgNode() — semantic convenience hooks
|
|
221
|
+
*/
|
|
222
|
+
const permanentlyObservedLayoutSignals = /* @__PURE__ */ new WeakMap();
|
|
223
|
+
const observedLayoutSignals = /* @__PURE__ */ new WeakMap();
|
|
224
|
+
const retainedLayoutSignals = /* @__PURE__ */ new WeakMap();
|
|
225
|
+
function addObservedLayoutSignal(node, key) {
|
|
226
|
+
let observed = observedLayoutSignals.get(node);
|
|
227
|
+
if (!observed) {
|
|
228
|
+
observed = /* @__PURE__ */ new Set();
|
|
229
|
+
observedLayoutSignals.set(node, observed);
|
|
230
|
+
}
|
|
231
|
+
observed.add(key);
|
|
232
|
+
}
|
|
233
|
+
function markObservedLayoutSignal(node, key) {
|
|
234
|
+
addObservedLayoutSignal(node, key);
|
|
235
|
+
let permanent = permanentlyObservedLayoutSignals.get(node);
|
|
236
|
+
if (!permanent) {
|
|
237
|
+
permanent = /* @__PURE__ */ new Set();
|
|
238
|
+
permanentlyObservedLayoutSignals.set(node, permanent);
|
|
239
|
+
}
|
|
240
|
+
permanent.add(key);
|
|
241
|
+
}
|
|
242
|
+
function observeLayoutSignal(node, key) {
|
|
243
|
+
addObservedLayoutSignal(node, key);
|
|
244
|
+
let retained = retainedLayoutSignals.get(node);
|
|
245
|
+
if (!retained) {
|
|
246
|
+
retained = /* @__PURE__ */ new Map();
|
|
247
|
+
retainedLayoutSignals.set(node, retained);
|
|
248
|
+
}
|
|
249
|
+
retained.set(key, (retained.get(key) ?? 0) + 1);
|
|
250
|
+
let released = false;
|
|
251
|
+
return () => {
|
|
252
|
+
if (released) return;
|
|
253
|
+
released = true;
|
|
254
|
+
const current = retainedLayoutSignals.get(node);
|
|
255
|
+
if (!current) return;
|
|
256
|
+
const next = (current.get(key) ?? 0) - 1;
|
|
257
|
+
if (next > 0) {
|
|
258
|
+
current.set(key, next);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
current.delete(key);
|
|
262
|
+
if (current.size === 0) retainedLayoutSignals.delete(node);
|
|
263
|
+
if (!permanentlyObservedLayoutSignals.get(node)?.has(key)) observedLayoutSignals.get(node)?.delete(key);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function cursorRectEqual(a, b) {
|
|
267
|
+
if (a === b) return true;
|
|
268
|
+
if (!a || !b) return false;
|
|
269
|
+
return a.x === b.x && a.y === b.y && a.visible === b.visible && a.shape === b.shape;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Per-field equality on a list of rects. Used to skip selection-fragment
|
|
273
|
+
* signal writes when nothing changed — mirrors the rect-tuple equality
|
|
274
|
+
* pattern used for boxRect/scrollRect/screenRect/contentRect/cursorRect.
|
|
275
|
+
*
|
|
276
|
+
* Reference equality is checked first (the common no-op path); only when
|
|
277
|
+
* lengths match do we walk the entries. An empty array is the canonical
|
|
278
|
+
* "no fragments" state — the equality path treats `[]` and `[]` as equal
|
|
279
|
+
* by length-zero, so collapsed/no-selection nodes don't churn the signal.
|
|
280
|
+
*/
|
|
281
|
+
function selectionFragmentsEqual(a, b) {
|
|
282
|
+
if (a === b) return true;
|
|
283
|
+
if (a.length !== b.length) return false;
|
|
284
|
+
for (let i = 0; i < a.length; i++) if (!rectEqual$1(a[i] ?? null, b[i] ?? null)) return false;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Stable empty-rects sentinel — `selectionFragments` defaults to this when a
|
|
289
|
+
* node has no `selectionIntent` declared. Reusing the same array reference
|
|
290
|
+
* means subscribers see reference-stable "no selection" frames and can skip
|
|
291
|
+
* downstream re-computation. The array is frozen so accidental mutation
|
|
292
|
+
* never corrupts the sentinel.
|
|
293
|
+
*/
|
|
294
|
+
const EMPTY_FRAGMENTS = Object.freeze([]);
|
|
295
|
+
/**
|
|
296
|
+
* Stable empty-decorations sentinel — `decorationRects` defaults to this when
|
|
297
|
+
* a node has no `decorations` BoxProp. Same reference-stability story as
|
|
298
|
+
* `EMPTY_FRAGMENTS`.
|
|
299
|
+
*/
|
|
300
|
+
const EMPTY_DECORATION_RECTS = Object.freeze([]);
|
|
301
|
+
const signalMap = /* @__PURE__ */ new WeakMap();
|
|
302
|
+
/**
|
|
303
|
+
* Get or create layout signals for a node.
|
|
304
|
+
*
|
|
305
|
+
* Lazily created on first access. Automatically garbage-collected
|
|
306
|
+
* when the node is removed from the tree (WeakMap semantics).
|
|
307
|
+
*/
|
|
308
|
+
function getLayoutSignals(node) {
|
|
309
|
+
let s = signalMap.get(node);
|
|
310
|
+
if (!s) {
|
|
311
|
+
s = {
|
|
312
|
+
boxRect: signal(node.boxRect),
|
|
313
|
+
scrollRect: signal(node.scrollRect),
|
|
314
|
+
screenRect: signal(node.screenRect),
|
|
315
|
+
boxRectCommitted: signal(node.boxRect),
|
|
316
|
+
scrollRectCommitted: signal(node.scrollRect),
|
|
317
|
+
screenRectCommitted: signal(node.screenRect),
|
|
318
|
+
contentRect: signal(computeContentRect(node)),
|
|
319
|
+
cursorRect: signal(computeCursorRect(node)),
|
|
320
|
+
focusedNodeId: signal(computeFocusedNodeId(node)),
|
|
321
|
+
selectionFragments: signal(computeSelectionFragments(node)),
|
|
322
|
+
scrollState: signal(snapshotScrollState(node)),
|
|
323
|
+
anchorRect: signal(computeAnchorRect(node)),
|
|
324
|
+
decorationRects: signal(EMPTY_DECORATION_RECTS),
|
|
325
|
+
textContent: signal(node.textContent),
|
|
326
|
+
focused: signal(node.interactiveState?.focused ?? false)
|
|
327
|
+
};
|
|
328
|
+
signalMap.set(node, s);
|
|
329
|
+
}
|
|
330
|
+
return s;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Compute the content-box rect for a node — `scrollRect` minus border and
|
|
334
|
+
* padding (CSS content area in absolute terminal coordinates).
|
|
335
|
+
*
|
|
336
|
+
* Returns null when `scrollRect` is not yet populated (pre-layout) or when
|
|
337
|
+
* border + padding would shrink the area to zero/negative width or height
|
|
338
|
+
* (clipped/empty content area).
|
|
339
|
+
*
|
|
340
|
+
* The math is the canonical "border + padding" calculation that the layout
|
|
341
|
+
* engine uses internally. Lifted here so consumers (cursor positioning,
|
|
342
|
+
* popover anchors, selection overlays) read one signal instead of re-deriving
|
|
343
|
+
* the offsets at every call site.
|
|
344
|
+
*/
|
|
345
|
+
function computeContentRect(node) {
|
|
346
|
+
const props = node.props;
|
|
347
|
+
const scroll = node.scrollRect;
|
|
348
|
+
if (!scroll) return null;
|
|
349
|
+
const padLeft = props?.paddingLeft ?? props?.paddingX ?? props?.padding ?? 0;
|
|
350
|
+
const padRight = props?.paddingRight ?? props?.paddingX ?? props?.padding ?? 0;
|
|
351
|
+
const padTop = props?.paddingTop ?? props?.paddingY ?? props?.padding ?? 0;
|
|
352
|
+
const padBottom = props?.paddingBottom ?? props?.paddingY ?? props?.padding ?? 0;
|
|
353
|
+
const borderLeft = props?.borderStyle ? 1 : 0;
|
|
354
|
+
const borderRight = props?.borderStyle ? 1 : 0;
|
|
355
|
+
const borderTop = props?.borderStyle ? 1 : 0;
|
|
356
|
+
const borderBottom = props?.borderStyle ? 1 : 0;
|
|
357
|
+
const x = scroll.x + borderLeft + padLeft;
|
|
358
|
+
const y = scroll.y + borderTop + padTop;
|
|
359
|
+
const width = scroll.width - borderLeft - borderRight - padLeft - padRight;
|
|
360
|
+
const height = scroll.height - borderTop - borderBottom - padTop - padBottom;
|
|
361
|
+
if (width <= 0 || height <= 0) return null;
|
|
362
|
+
return {
|
|
363
|
+
x,
|
|
364
|
+
y,
|
|
365
|
+
width,
|
|
366
|
+
height
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Compute the absolute caret rect for a node based on its `cursorOffset`
|
|
371
|
+
* prop and current `contentRect`. Caret coordinates are content-area-relative
|
|
372
|
+
* (inside border + padding), so this delegates to `computeContentRect` for
|
|
373
|
+
* the origin instead of redoing the border/padding math here.
|
|
374
|
+
*
|
|
375
|
+
* Returns null when:
|
|
376
|
+
* - the node has no `cursorOffset` prop, OR
|
|
377
|
+
* - `scrollRect` is not yet populated (pre-layout), OR
|
|
378
|
+
* - the content box collapsed to zero/negative size (no place to draw).
|
|
379
|
+
*
|
|
380
|
+
* `computeContentRect` keeps cursor positioning and overlay anchoring on the
|
|
381
|
+
* same origin — Phase 4 / overlay-anchor consumers read `contentRect`
|
|
382
|
+
* directly and won't drift from where the caret lands. See bead
|
|
383
|
+
* `km-silvery.cursor-invariants` invariant 3.
|
|
384
|
+
*/
|
|
385
|
+
function computeCursorRect(node) {
|
|
386
|
+
const offset = node.props?.cursorOffset;
|
|
387
|
+
if (!offset) return null;
|
|
388
|
+
const content = computeContentRect(node);
|
|
389
|
+
if (!content) return null;
|
|
390
|
+
return {
|
|
391
|
+
x: content.x + offset.col,
|
|
392
|
+
y: content.y + offset.row,
|
|
393
|
+
visible: offset.visible !== false,
|
|
394
|
+
shape: offset.shape
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Compute the focused-node id for a node based on its `focused` BoxProp.
|
|
399
|
+
*
|
|
400
|
+
* Returns the node's `id` (preferred) or `testID` when `props.focused === true`,
|
|
401
|
+
* else `null`. This is the per-node value carried in
|
|
402
|
+
* `LayoutSignals.focusedNodeId` — the tree-walk lookup
|
|
403
|
+
* `findActiveFocusedNodeId(root)` picks the deepest non-null among all
|
|
404
|
+
* declarers (Phase 4a precedence rule).
|
|
405
|
+
*
|
|
406
|
+
* Identity priority: `id` > `testID`. Apps that want stable focus identity
|
|
407
|
+
* should set one of those props alongside `focused={true}`. When neither is
|
|
408
|
+
* set but `focused === true`, an opaque sentinel (`"__focused__"`) is
|
|
409
|
+
* returned so the signal is still observable as "something is focused" —
|
|
410
|
+
* downstream consumers should not depend on the sentinel value beyond
|
|
411
|
+
* non-null/null.
|
|
412
|
+
*/
|
|
413
|
+
function computeFocusedNodeId(node) {
|
|
414
|
+
const props = node.props;
|
|
415
|
+
if (!props?.focused) return null;
|
|
416
|
+
if (typeof props.id === "string" && props.id.length > 0) return props.id;
|
|
417
|
+
if (typeof props.testID === "string" && props.testID.length > 0) return props.testID;
|
|
418
|
+
return "__focused__";
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Resolve the `anchorRef` BoxProp into a string id. Accepts the shorthand
|
|
422
|
+
* `anchorRef="my-id"` and the structured `anchorRef={{ id: "my-id" }}` form.
|
|
423
|
+
*
|
|
424
|
+
* Returns `null` when no anchorRef is present, or when the prop is malformed
|
|
425
|
+
* (empty id string). Apps that need stable anchor identity should always pass
|
|
426
|
+
* a non-empty string.
|
|
427
|
+
*/
|
|
428
|
+
function resolveAnchorId(node) {
|
|
429
|
+
const ref = node.props?.anchorRef;
|
|
430
|
+
if (!ref) return null;
|
|
431
|
+
if (typeof ref === "string") return ref.length > 0 ? ref : null;
|
|
432
|
+
const ar = ref;
|
|
433
|
+
if (typeof ar.id === "string" && ar.id.length > 0) return ar.id;
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Compute the anchor rect for a Box that declares `anchorRef`. The registered
|
|
438
|
+
* rect is the Box's `contentRect` — the inner area inside border + padding
|
|
439
|
+
* — which is the canonical origin for placement math. Edge-specific rects
|
|
440
|
+
* (top/bottom/left/right) are derived by `placeFloating` at consumption time
|
|
441
|
+
* rather than baked into the registry.
|
|
442
|
+
*
|
|
443
|
+
* Returns `null` when:
|
|
444
|
+
* - the node has no `anchorRef` BoxProp (or it's empty), OR
|
|
445
|
+
* - `contentRect` is unavailable (pre-layout / clipped to zero size).
|
|
446
|
+
*
|
|
447
|
+
* Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
|
|
448
|
+
*/
|
|
449
|
+
function computeAnchorRect(node) {
|
|
450
|
+
if (resolveAnchorId(node) === null) return null;
|
|
451
|
+
return computeContentRect(node);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Walk the tree and find the rect for an anchor by id. Returns `null` when
|
|
455
|
+
* no Box declares `anchorRef` with a matching id, or when the matching Box's
|
|
456
|
+
* `contentRect` is unavailable this frame (pre-layout / clipped).
|
|
457
|
+
*
|
|
458
|
+
* **Optional `edge` parameter** — when supplied, returns a 1-cell-thick rect
|
|
459
|
+
* along the requested edge of the anchor (`top`, `bottom`, `left`, `right`).
|
|
460
|
+
* Convenience for callers that want to draw against a specific edge without
|
|
461
|
+
* threading the full content rect through `placeFloating`. Without `edge`,
|
|
462
|
+
* returns the full content rect.
|
|
463
|
+
*
|
|
464
|
+
* **Walk order**: post-order (deepest-first). If two anchors share an id —
|
|
465
|
+
* the contract says they shouldn't, but the substrate doesn't enforce
|
|
466
|
+
* uniqueness — the deeper / later-rendered one wins. This matches the
|
|
467
|
+
* deepest-wins precedence used by cursor and focus walks.
|
|
468
|
+
*
|
|
469
|
+
* Per-node cost: one `props.anchorRef` check + one signal lookup (or one
|
|
470
|
+
* direct compute when no signal is allocated). Trees with no anchors return
|
|
471
|
+
* `null` after a single traversal.
|
|
472
|
+
*
|
|
473
|
+
* Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
|
|
474
|
+
*/
|
|
475
|
+
function findAnchor(root, id, edge) {
|
|
476
|
+
let result = null;
|
|
477
|
+
function walk(node) {
|
|
478
|
+
for (const child of node.children) walk(child);
|
|
479
|
+
if (resolveAnchorId(node) !== id) return;
|
|
480
|
+
const s = signalMap.get(node);
|
|
481
|
+
const rect = s ? s.anchorRect() : computeAnchorRect(node);
|
|
482
|
+
if (rect) result = rect;
|
|
483
|
+
}
|
|
484
|
+
walk(root);
|
|
485
|
+
if (result === null || edge === void 0) return result;
|
|
486
|
+
const r = result;
|
|
487
|
+
switch (edge) {
|
|
488
|
+
case "top": return {
|
|
489
|
+
x: r.x,
|
|
490
|
+
y: r.y,
|
|
491
|
+
width: r.width,
|
|
492
|
+
height: 1
|
|
493
|
+
};
|
|
494
|
+
case "bottom": return {
|
|
495
|
+
x: r.x,
|
|
496
|
+
y: r.y + Math.max(0, r.height - 1),
|
|
497
|
+
width: r.width,
|
|
498
|
+
height: 1
|
|
499
|
+
};
|
|
500
|
+
case "left": return {
|
|
501
|
+
x: r.x,
|
|
502
|
+
y: r.y,
|
|
503
|
+
width: 1,
|
|
504
|
+
height: r.height
|
|
505
|
+
};
|
|
506
|
+
case "right": return {
|
|
507
|
+
x: r.x + Math.max(0, r.width - 1),
|
|
508
|
+
y: r.y,
|
|
509
|
+
width: 1,
|
|
510
|
+
height: r.height
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Compute the resolved decoration rects for a node based on its `decorations`
|
|
516
|
+
* BoxProp. Each entry produces one `DecorationRect` whose `rects` may be empty
|
|
517
|
+
* when an anchor lookup fails.
|
|
518
|
+
*
|
|
519
|
+
* **Behavior by kind**:
|
|
520
|
+
* - `popover` / `tooltip`: requires `anchorId` + `placement` + `size`. The
|
|
521
|
+
* anchor rect is looked up via `findAnchor(root, anchorId)`; if found and
|
|
522
|
+
* all required fields are present, `placeFloating` produces the placed
|
|
523
|
+
* rect. Missing anchor or missing required fields → empty rect list.
|
|
524
|
+
* - `highlight`: the `rect` field, if present, is translated from
|
|
525
|
+
* content-relative coordinates into absolute terminal coordinates by
|
|
526
|
+
* adding the owning Box's `contentRect.{x, y}`. Missing rect or no
|
|
527
|
+
* contentRect → empty rect list.
|
|
528
|
+
*
|
|
529
|
+
* **Per-frame**: this runs in the layout-phase notify pass, so anchor rects
|
|
530
|
+
* are populated for the same frame. Anchors declared deeper in the tree
|
|
531
|
+
* resolve correctly because the function takes the root tree as input rather
|
|
532
|
+
* than relying on a separately-built map.
|
|
533
|
+
*
|
|
534
|
+
* Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
|
|
535
|
+
*/
|
|
536
|
+
function computeDecorationRects(node, root) {
|
|
537
|
+
const decos = node.props?.decorations;
|
|
538
|
+
if (!decos || decos.length === 0) return EMPTY_DECORATION_RECTS;
|
|
539
|
+
const out = [];
|
|
540
|
+
const content = computeContentRect(node);
|
|
541
|
+
for (const d of decos) if (d.kind === "popover" || d.kind === "tooltip") {
|
|
542
|
+
if (!d.anchorId || !d.placement || !d.size) {
|
|
543
|
+
out.push({
|
|
544
|
+
kind: d.kind,
|
|
545
|
+
id: d.id,
|
|
546
|
+
rects: []
|
|
547
|
+
});
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const anchor = findAnchor(root, d.anchorId);
|
|
551
|
+
if (!anchor) {
|
|
552
|
+
out.push({
|
|
553
|
+
kind: d.kind,
|
|
554
|
+
id: d.id,
|
|
555
|
+
rects: []
|
|
556
|
+
});
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const placed = resolveFloatingPlacement(anchor, d.size, d.placement, {
|
|
560
|
+
offset: d.offset,
|
|
561
|
+
alignOffset: d.alignOffset,
|
|
562
|
+
collisionStrategy: d.collisionStrategy,
|
|
563
|
+
boundary: root.boxRect
|
|
564
|
+
});
|
|
565
|
+
out.push({
|
|
566
|
+
kind: d.kind,
|
|
567
|
+
id: d.id,
|
|
568
|
+
rects: placed ? [placed.rect] : []
|
|
569
|
+
});
|
|
570
|
+
} else if (d.kind === "highlight") {
|
|
571
|
+
if (!d.rect || !content) {
|
|
572
|
+
out.push({
|
|
573
|
+
kind: d.kind,
|
|
574
|
+
id: d.id,
|
|
575
|
+
rects: []
|
|
576
|
+
});
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
out.push({
|
|
580
|
+
kind: d.kind,
|
|
581
|
+
id: d.id,
|
|
582
|
+
rects: [{
|
|
583
|
+
x: content.x + d.rect.x,
|
|
584
|
+
y: content.y + d.rect.y,
|
|
585
|
+
width: d.rect.width,
|
|
586
|
+
height: d.rect.height
|
|
587
|
+
}]
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
return out.length === 0 ? EMPTY_DECORATION_RECTS : out;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Per-field equality on a list of `DecorationRect`. Used to skip
|
|
594
|
+
* `decorationRects` signal writes when nothing changed — mirrors the
|
|
595
|
+
* `selectionFragmentsEqual` pattern.
|
|
596
|
+
*/
|
|
597
|
+
function decorationRectsEqual(a, b) {
|
|
598
|
+
if (a === b) return true;
|
|
599
|
+
if (a.length !== b.length) return false;
|
|
600
|
+
for (let i = 0; i < a.length; i++) {
|
|
601
|
+
const ai = a[i];
|
|
602
|
+
const bi = b[i];
|
|
603
|
+
if (ai.kind !== bi.kind) return false;
|
|
604
|
+
if (ai.id !== bi.id) return false;
|
|
605
|
+
if (ai.rects.length !== bi.rects.length) return false;
|
|
606
|
+
for (let j = 0; j < ai.rects.length; j++) if (!rectEqual$1(ai.rects[j] ?? null, bi.rects[j] ?? null)) return false;
|
|
607
|
+
}
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Collect the textual content of a selection-declaring Box.
|
|
612
|
+
*
|
|
613
|
+
* The selection-fragment math operates on the rendered text content of the
|
|
614
|
+
* owning Box — `selectionIntent.{from,to}` are character offsets into this
|
|
615
|
+
* string. For Box nodes, the canonical content is the concatenation of
|
|
616
|
+
* descendant `silvery-text` nodes' `textContent` (in tree order), with `\n`
|
|
617
|
+
* separators between adjacent Text/Box children that introduce visual line
|
|
618
|
+
* breaks.
|
|
619
|
+
*
|
|
620
|
+
* v1 behaviour (kept intentionally minimal):
|
|
621
|
+
* - A Box with `silvery-text` children: concatenates `textContent` strings
|
|
622
|
+
* from those children. Two adjacent text children produce one logical
|
|
623
|
+
* line; if you want a line break, embed `\n` in the text.
|
|
624
|
+
* - A Box with mixed children: same — only `silvery-text` descendants
|
|
625
|
+
* contribute. Nested Box children don't add line breaks (they're treated
|
|
626
|
+
* as transparent for content purposes).
|
|
627
|
+
* - A `silvery-text` node directly carrying the prop: its own `textContent`
|
|
628
|
+
* is the content.
|
|
629
|
+
*
|
|
630
|
+
* This keeps the v1 model honest: declare `selectionIntent` on a Box (or
|
|
631
|
+
* Text) whose text content is the source of truth for the selection. Apps
|
|
632
|
+
* that want per-line semantics can split the selection across multiple
|
|
633
|
+
* intent declarations.
|
|
634
|
+
*/
|
|
635
|
+
function collectSelectionText(node) {
|
|
636
|
+
if (node.type === "silvery-text") return node.textContent ?? "";
|
|
637
|
+
let out = "";
|
|
638
|
+
const stack = [node];
|
|
639
|
+
while (stack.length) {
|
|
640
|
+
const cur = stack.pop();
|
|
641
|
+
for (let i = cur.children.length - 1; i >= 0; i--) {
|
|
642
|
+
const child = cur.children[i];
|
|
643
|
+
if (child) stack.push(child);
|
|
644
|
+
}
|
|
645
|
+
if (cur === node) continue;
|
|
646
|
+
if (cur.type === "silvery-text" && cur.textContent !== void 0) out += cur.textContent;
|
|
647
|
+
}
|
|
648
|
+
return out;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Compute the geometric fragments for a node's `selectionIntent` — the list
|
|
652
|
+
* of rectangles (one per visual line spanned) that the selection renderer
|
|
653
|
+
* should paint with highlight bg this frame.
|
|
654
|
+
*
|
|
655
|
+
* Returns:
|
|
656
|
+
* - `[]` when the node has no `selectionIntent` prop, or when the intent is
|
|
657
|
+
* collapsed (`from === to`), or when the content rect is unavailable
|
|
658
|
+
* (pre-layout / clipped to zero size).
|
|
659
|
+
* - `[Rect]` for a single-visual-line selection.
|
|
660
|
+
* - `[Rect, Rect, ...]` for multi-line selections (split per visual line).
|
|
661
|
+
*
|
|
662
|
+
* **Geometry** (mirrors text-editor / ProseMirror conventions):
|
|
663
|
+
* - First line: from `(content.x + fromCol, content.y + fromLine)` to the
|
|
664
|
+
* end of the line. If single-line, runs to `toCol`.
|
|
665
|
+
* - Middle lines: full content-rect width, one row each.
|
|
666
|
+
* - Last line: from `(content.x, content.y + toLine)` to `toCol` chars.
|
|
667
|
+
*
|
|
668
|
+
* Coordinates are absolute terminal cells, matching `cursorRect`'s
|
|
669
|
+
* coordinate space. Width is in cells (one rect per visual line).
|
|
670
|
+
*
|
|
671
|
+
* **Soft-wrap awareness (Option B)**: when a wrap measurer is registered
|
|
672
|
+
* via `setWrapMeasurer({ wrapText })` AND the content rect width is known,
|
|
673
|
+
* this function splits on the measurer's per-visual-line slices — a
|
|
674
|
+
* 60-char paragraph wrapped at width 20 produces 3 fragments rather than
|
|
675
|
+
* one wide rectangle. The terminal runtime (`@silvery/ag-term`) registers
|
|
676
|
+
* its grapheme-aware `wrapText` at startup; pure `@silvery/ag` consumers
|
|
677
|
+
* (no terminal) fall back to `\n`-only splitting which preserves the
|
|
678
|
+
* pre-Option-B behavior bit-for-bit. See `wrap-measurer.ts` for the
|
|
679
|
+
* registry contract. Closes Phase 4b deferred wrap-spanning (bead
|
|
680
|
+
* `km-silvery.softwrap-selection-fragments`).
|
|
681
|
+
*/
|
|
682
|
+
function computeSelectionFragments(node) {
|
|
683
|
+
const intent = node.props?.selectionIntent;
|
|
684
|
+
if (!intent) return EMPTY_FRAGMENTS;
|
|
685
|
+
if (intent.from >= intent.to) return EMPTY_FRAGMENTS;
|
|
686
|
+
const content = computeContentRect(node);
|
|
687
|
+
if (!content) return EMPTY_FRAGMENTS;
|
|
688
|
+
const text = collectSelectionText(node);
|
|
689
|
+
if (text.length === 0) return EMPTY_FRAGMENTS;
|
|
690
|
+
const measurer = getWrapMeasurer();
|
|
691
|
+
const visualLines = measurer !== null && content.width > 0 ? buildVisualLinesWithMeasurer(text, content.width, measurer.wrapText) : buildVisualLinesNewlineOnly(text);
|
|
692
|
+
const fragments = [];
|
|
693
|
+
for (let i = 0; i < visualLines.length; i++) {
|
|
694
|
+
const line = visualLines[i];
|
|
695
|
+
if (line.endOffset <= intent.from) continue;
|
|
696
|
+
if (line.startOffset >= intent.to) break;
|
|
697
|
+
const localFrom = Math.max(0, intent.from - line.startOffset);
|
|
698
|
+
const localTo = Math.min(line.text.length, intent.to - line.startOffset);
|
|
699
|
+
const width = Math.max(0, localTo - localFrom);
|
|
700
|
+
if (width === 0) continue;
|
|
701
|
+
fragments.push({
|
|
702
|
+
x: content.x + localFrom,
|
|
703
|
+
y: content.y + i,
|
|
704
|
+
width,
|
|
705
|
+
height: 1
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
return fragments.length === 0 ? EMPTY_FRAGMENTS : fragments;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Walk paragraphs (split on `\n`) through the registered wrap measurer to
|
|
712
|
+
* produce per-visual-line slices. When a paragraph fits within the width
|
|
713
|
+
* unchanged, the measurer returns `[]` — we synthesize a single-slice
|
|
714
|
+
* passthrough so the downstream loop sees uniform input.
|
|
715
|
+
*
|
|
716
|
+
* Maintains the invariant that visual-line offsets are monotone and cover
|
|
717
|
+
* the full input (including the `\n` terminator counted as a zero-width
|
|
718
|
+
* boundary so cross-paragraph selections stay aligned).
|
|
719
|
+
*/
|
|
720
|
+
function buildVisualLinesWithMeasurer(text, width, wrapText) {
|
|
721
|
+
const out = [];
|
|
722
|
+
let paraStart = 0;
|
|
723
|
+
for (let i = 0; i <= text.length; i++) {
|
|
724
|
+
const isEnd = i === text.length;
|
|
725
|
+
const isNewline = !isEnd && text.charCodeAt(i) === 10;
|
|
726
|
+
if (!isEnd && !isNewline) continue;
|
|
727
|
+
const para = text.slice(paraStart, i);
|
|
728
|
+
const slices = wrapText(para, width);
|
|
729
|
+
if (slices.length === 0) out.push({
|
|
730
|
+
text: para,
|
|
731
|
+
startOffset: paraStart,
|
|
732
|
+
endOffset: paraStart + para.length
|
|
733
|
+
});
|
|
734
|
+
else for (const slice of slices) out.push({
|
|
735
|
+
text: slice.text,
|
|
736
|
+
startOffset: paraStart + slice.startOffset,
|
|
737
|
+
endOffset: paraStart + slice.endOffset
|
|
738
|
+
});
|
|
739
|
+
paraStart = i + 1;
|
|
740
|
+
}
|
|
741
|
+
return out;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Fallback: split on `\n` only. Preserves pre-Option-B geometry exactly so
|
|
745
|
+
* unit tests that exercise the framework-only layer (no terminal Term
|
|
746
|
+
* registered) keep passing without changes.
|
|
747
|
+
*
|
|
748
|
+
* The `endOffset` of each line is the position of the `\n` (or `text.length`
|
|
749
|
+
* for the trailing line) — this matches the convention used by
|
|
750
|
+
* `buildVisualLinesWithMeasurer`, where the newline is a zero-width
|
|
751
|
+
* paragraph boundary rather than a visual line of its own.
|
|
752
|
+
*/
|
|
753
|
+
function buildVisualLinesNewlineOnly(text) {
|
|
754
|
+
const out = [];
|
|
755
|
+
let lineStart = 0;
|
|
756
|
+
for (let i = 0; i <= text.length; i++) if (i === text.length || text.charCodeAt(i) === 10) {
|
|
757
|
+
out.push({
|
|
758
|
+
text: text.slice(lineStart, i),
|
|
759
|
+
startOffset: lineStart,
|
|
760
|
+
endOffset: i
|
|
761
|
+
});
|
|
762
|
+
lineStart = i + 1;
|
|
763
|
+
}
|
|
764
|
+
return out;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Project AgNode.scrollState → ScrollStateSnapshot (the subset the virtualizer
|
|
768
|
+
* needs). Returns null if the node has no scroll state yet (non-scroll
|
|
769
|
+
* containers or fresh scroll containers pre-layout).
|
|
770
|
+
*
|
|
771
|
+
* Keeping this projection tight means callers can compare snapshots by
|
|
772
|
+
* per-field equality without pulling the mutable underlying object into
|
|
773
|
+
* consumer code.
|
|
774
|
+
*/
|
|
775
|
+
function snapshotScrollState(node) {
|
|
776
|
+
const ss = node.scrollState;
|
|
777
|
+
if (!ss) return null;
|
|
778
|
+
return {
|
|
779
|
+
offset: ss.offset,
|
|
780
|
+
contentHeight: ss.contentHeight,
|
|
781
|
+
viewportHeight: ss.viewportHeight,
|
|
782
|
+
firstVisibleChild: ss.firstVisibleChild,
|
|
783
|
+
lastVisibleChild: ss.lastVisibleChild,
|
|
784
|
+
hiddenAbove: ss.hiddenAbove,
|
|
785
|
+
hiddenBelow: ss.hiddenBelow
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
/** Per-field equality check for ScrollStateSnapshot (skips allocation). */
|
|
789
|
+
function scrollStateEqual(a, b) {
|
|
790
|
+
if (a === b) return true;
|
|
791
|
+
if (!a || !b) return false;
|
|
792
|
+
return a.offset === b.offset && a.contentHeight === b.contentHeight && a.viewportHeight === b.viewportHeight && a.firstVisibleChild === b.firstVisibleChild && a.lastVisibleChild === b.lastVisibleChild && a.hiddenAbove === b.hiddenAbove && a.hiddenBelow === b.hiddenBelow;
|
|
793
|
+
}
|
|
794
|
+
/** Check whether a node has signals allocated (for testing). */
|
|
795
|
+
function hasLayoutSignals(node) {
|
|
796
|
+
return signalMap.has(node);
|
|
797
|
+
}
|
|
798
|
+
function hasObservedLayoutSignal(node, key) {
|
|
799
|
+
return observedLayoutSignals.get(node)?.has(key) ?? false;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Sync all rect signals from the node's current values.
|
|
803
|
+
*
|
|
804
|
+
* Called from notifyLayoutSubscribers after layout + scroll + sticky
|
|
805
|
+
* phases complete. Only syncs nodes that have signals allocated.
|
|
806
|
+
* Reference-equality check prevents unnecessary downstream updates.
|
|
807
|
+
*/
|
|
808
|
+
function syncRectSignals(node) {
|
|
809
|
+
const props = node.props ?? void 0;
|
|
810
|
+
const hasCursorOffset = !!props?.cursorOffset;
|
|
811
|
+
const hasFocused = !!props?.focused;
|
|
812
|
+
const hasSelectionIntent = !!props?.selectionIntent;
|
|
813
|
+
const hasAnchorRef = !!props?.anchorRef;
|
|
814
|
+
const hasDecorations = !!(props?.decorations && props.decorations.length > 0);
|
|
815
|
+
const s = hasCursorOffset || hasFocused || hasSelectionIntent || hasAnchorRef || hasDecorations ? getLayoutSignals(node) : signalMap.get(node);
|
|
816
|
+
if (!s) return;
|
|
817
|
+
if (node.boxRect !== s.boxRect()) s.boxRect(node.boxRect);
|
|
818
|
+
if (node.scrollRect !== s.scrollRect()) s.scrollRect(node.scrollRect);
|
|
819
|
+
if (node.screenRect !== s.screenRect()) s.screenRect(node.screenRect);
|
|
820
|
+
const nextContentRect = computeContentRect(node);
|
|
821
|
+
if (!rectEqual$1(nextContentRect, s.contentRect())) s.contentRect(nextContentRect);
|
|
822
|
+
const nextCursorRect = computeCursorRect(node);
|
|
823
|
+
if (!cursorRectEqual(nextCursorRect, s.cursorRect())) s.cursorRect(nextCursorRect);
|
|
824
|
+
const nextFocusedId = computeFocusedNodeId(node);
|
|
825
|
+
if (nextFocusedId !== s.focusedNodeId()) s.focusedNodeId(nextFocusedId);
|
|
826
|
+
const nextFragments = computeSelectionFragments(node);
|
|
827
|
+
if (!selectionFragmentsEqual(nextFragments, s.selectionFragments())) s.selectionFragments(nextFragments);
|
|
828
|
+
const nextAnchorRect = computeAnchorRect(node);
|
|
829
|
+
if (!rectEqual$1(nextAnchorRect, s.anchorRect())) s.anchorRect(nextAnchorRect);
|
|
830
|
+
const nextScrollState = snapshotScrollState(node);
|
|
831
|
+
if (!scrollStateEqual(nextScrollState, s.scrollState())) s.scrollState(nextScrollState);
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Second-pass sync for `decorationRects` — must run AFTER `syncRectSignals`
|
|
835
|
+
* has populated every anchor rect this frame, because decoration resolution
|
|
836
|
+
* calls `findAnchor(root, id)` and needs the freshest anchor rects.
|
|
837
|
+
*
|
|
838
|
+
* Walks the tree, recomputes per-node decoration rects, and writes the signal
|
|
839
|
+
* only when the result differs (per-field equality via `decorationRectsEqual`).
|
|
840
|
+
*
|
|
841
|
+
* Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
|
|
842
|
+
*
|
|
843
|
+
* Per-node cost: one `props.decorations` length check (zero-allocation
|
|
844
|
+
* short-circuit) + one signal lookup + one decoration recompute when present.
|
|
845
|
+
* Trees without decorations pay only the prop check at every node.
|
|
846
|
+
*/
|
|
847
|
+
function syncDecorationRects(root) {
|
|
848
|
+
function walk(node) {
|
|
849
|
+
const props = node.props;
|
|
850
|
+
if (!!(props?.decorations && props.decorations.length > 0)) {
|
|
851
|
+
const s = getLayoutSignals(node);
|
|
852
|
+
const next = computeDecorationRects(node, root);
|
|
853
|
+
if (!decorationRectsEqual(next, s.decorationRects())) s.decorationRects(next);
|
|
854
|
+
} else {
|
|
855
|
+
const s = signalMap.get(node);
|
|
856
|
+
if (s && s.decorationRects().length > 0) s.decorationRects(EMPTY_DECORATION_RECTS);
|
|
857
|
+
}
|
|
858
|
+
for (const child of node.children) walk(child);
|
|
859
|
+
}
|
|
860
|
+
walk(root);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Promote the in-flight rect signals (`boxRect` / `scrollRect` / `screenRect`)
|
|
864
|
+
* to their committed counterparts (`boxRectCommitted` / etc.). Reactive
|
|
865
|
+
* `useBoxRect()` / `useScrollRect()` / `useScreenRect()` consumers subscribe
|
|
866
|
+
* to the committed signals — calling this advances them by one frame.
|
|
867
|
+
*
|
|
868
|
+
* Called by the runtime ONCE per event-batch commit, after the convergence
|
|
869
|
+
* loop has fully drained. Within a single batch, multiple convergence passes
|
|
870
|
+
* may write the in-flight signals (callback-form observers fire each time),
|
|
871
|
+
* but the committed signals advance only here. That's what lets a render
|
|
872
|
+
* which both READS `useBoxRect()` and WRITES a layout-affecting prop converge
|
|
873
|
+
* in one pass: the read returns the same value across every pass in the
|
|
874
|
+
* batch, so the write is idempotent.
|
|
875
|
+
*
|
|
876
|
+
* Reference equality on the underlying alien-signal write means a no-op
|
|
877
|
+
* commit (same rect as last frame) does not fire any subscribers — steady
|
|
878
|
+
* state pays no cost.
|
|
879
|
+
*
|
|
880
|
+
* The walker visits only nodes that already have allocated `LayoutSignals`
|
|
881
|
+
* (i.e. nodes with at least one consumer); a tree with no rect subscribers
|
|
882
|
+
* pays only the WeakMap probe per node.
|
|
883
|
+
*
|
|
884
|
+
* See bead `@km/silvery/use-deferred-box-rect-and-post-commit-observers`.
|
|
885
|
+
*/
|
|
886
|
+
function commitLayoutSnapshot(root) {
|
|
887
|
+
function walk(node) {
|
|
888
|
+
const s = signalMap.get(node);
|
|
889
|
+
if (s) {
|
|
890
|
+
const nextBox = s.boxRect();
|
|
891
|
+
if (!rectEqual$1(nextBox, s.boxRectCommitted())) s.boxRectCommitted(nextBox);
|
|
892
|
+
const nextScroll = s.scrollRect();
|
|
893
|
+
if (!rectEqual$1(nextScroll, s.scrollRectCommitted())) s.scrollRectCommitted(nextScroll);
|
|
894
|
+
const nextScreen = s.screenRect();
|
|
895
|
+
if (!rectEqual$1(nextScreen, s.screenRectCommitted())) s.screenRectCommitted(nextScreen);
|
|
896
|
+
}
|
|
897
|
+
for (const child of node.children) walk(child);
|
|
898
|
+
}
|
|
899
|
+
walk(root);
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Walk the tree and find the active caret rect — the caret to render this
|
|
903
|
+
* frame, applying the precedence + clipping rules locked by bead
|
|
904
|
+
* `km-silvery.cursor-invariants`. Returns null when no caret should be
|
|
905
|
+
* shown.
|
|
906
|
+
*
|
|
907
|
+
* **Precedence (invariant 1)**:
|
|
908
|
+
* 1. **Focused-editable wins**: a Box with `cursorOffset.visible !== false`
|
|
909
|
+
* AND `interactiveState.focused === true`. If multiple focused-editables
|
|
910
|
+
* exist (rare — typically one input is focused at a time), the deepest
|
|
911
|
+
* one in paint order wins.
|
|
912
|
+
* 2. **Otherwise deepest-in-paint-order**: if no node is focused-editable,
|
|
913
|
+
* fall back to the deepest visible declarer (post-order tree walk).
|
|
914
|
+
* This covers Ink-compat consumers and `useCursor` callers that don't
|
|
915
|
+
* participate in the focus tree.
|
|
916
|
+
* 3. **Otherwise null**: no visible caret declared anywhere.
|
|
917
|
+
*
|
|
918
|
+
* **Clipping (invariant 4)**: at each scroll/clip ancestor (a Box with
|
|
919
|
+
* `overflow="scroll"` / `"hidden"` / `overflowY="hidden"`), the caret's
|
|
920
|
+
* position is checked against the ancestor's `scrollRect`. If the caret
|
|
921
|
+
* falls outside the visible region, the caret is treated as not-present.
|
|
922
|
+
* Default behavior is **hide** (no caret ANSI emitted) — never clamp. A
|
|
923
|
+
* caret rect at the exact clip edge is treated as visible.
|
|
924
|
+
*
|
|
925
|
+
* Visited in tree order (depth-first, post-order). Per-node cost is one
|
|
926
|
+
* `props.cursorOffset` check + one signal lookup; trees without any caret
|
|
927
|
+
* declarer return null after a single traversal.
|
|
928
|
+
*/
|
|
929
|
+
function findActiveCursorRect(root) {
|
|
930
|
+
let focusedResult = null;
|
|
931
|
+
let fallbackResult = null;
|
|
932
|
+
const clipStack = [];
|
|
933
|
+
function isClipped(rect) {
|
|
934
|
+
for (let i = clipStack.length - 1; i >= 0; i--) {
|
|
935
|
+
const clip = clipStack[i];
|
|
936
|
+
if (!clip) continue;
|
|
937
|
+
if (rect.x < clip.x || rect.y < clip.y || rect.x >= clip.x + clip.width || rect.y >= clip.y + clip.height) return true;
|
|
938
|
+
}
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
function isClipAncestor(node) {
|
|
942
|
+
const props = node.props;
|
|
943
|
+
if (!props) return false;
|
|
944
|
+
if (props.overflow === "scroll" || props.overflow === "hidden") return true;
|
|
945
|
+
if (props.overflowY === "hidden") return true;
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
function walk(node) {
|
|
949
|
+
const isClip = isClipAncestor(node);
|
|
950
|
+
if (isClip) clipStack.push(node.scrollRect ?? null);
|
|
951
|
+
for (const child of node.children) walk(child);
|
|
952
|
+
if (node.props?.cursorOffset) {
|
|
953
|
+
const s = signalMap.get(node);
|
|
954
|
+
const rect = s ? s.cursorRect() : computeCursorRect(node);
|
|
955
|
+
if (rect && rect.visible && !isClipped(rect)) {
|
|
956
|
+
fallbackResult = rect;
|
|
957
|
+
if (node.interactiveState?.focused) focusedResult = rect;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (isClip) clipStack.pop();
|
|
961
|
+
}
|
|
962
|
+
walk(root);
|
|
963
|
+
return focusedResult ?? fallbackResult;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Sync textContent signal from the node's current value.
|
|
967
|
+
*
|
|
968
|
+
* Called from commitTextUpdate in the reconciler.
|
|
969
|
+
*/
|
|
970
|
+
function syncTextContentSignal(node) {
|
|
971
|
+
const s = signalMap.get(node);
|
|
972
|
+
if (!s) return;
|
|
973
|
+
if (node.textContent !== s.textContent()) s.textContent(node.textContent);
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Sync focused signal for a node.
|
|
977
|
+
*
|
|
978
|
+
* Called from FocusManager when focus changes.
|
|
979
|
+
*/
|
|
980
|
+
function syncFocusedSignal(node, focused) {
|
|
981
|
+
const s = signalMap.get(node);
|
|
982
|
+
if (!s) return;
|
|
983
|
+
if (focused !== s.focused()) s.focused(focused);
|
|
984
|
+
}
|
|
985
|
+
//#endregion
|
|
986
|
+
export { hasObservedLayoutSignal as a, syncDecorationRects as c, syncTextContentSignal as d, getWrapMeasurer as f, hasLayoutSignals as i, syncFocusedSignal as l, rectEqual$1 as m, findActiveCursorRect as n, markObservedLayoutSignal as o, setWrapMeasurer as p, getLayoutSignals as r, observeLayoutSignal as s, commitLayoutSnapshot as t, syncRectSignals as u };
|
|
987
|
+
|
|
988
|
+
//# sourceMappingURL=layout-signals-Cnw6xk8Q.mjs.map
|