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,1044 @@
|
|
|
1
|
+
import { O as displayWidthAnsi, rt as wrapText } from "./ansi-yC4RyBNY.mjs";
|
|
2
|
+
import { createLogger } from "loggily";
|
|
3
|
+
//#region packages/ag/src/focus-queries.ts
|
|
4
|
+
/** Check if a node has the focusable prop set to true (or truthy). */
|
|
5
|
+
function isFocusable(node) {
|
|
6
|
+
if (node.hidden) return false;
|
|
7
|
+
const props = node.props;
|
|
8
|
+
return Boolean(props.focusable) && props.display !== "none";
|
|
9
|
+
}
|
|
10
|
+
/** Check if a node creates a focus scope (isolated Tab cycle). */
|
|
11
|
+
function isFocusScope(node) {
|
|
12
|
+
const props = node.props;
|
|
13
|
+
return Boolean(props.focusScope);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Walk up from node to nearest ancestor (or self) with focusable prop.
|
|
17
|
+
* Useful for mouse clicks — find the focusable target from a deep text node.
|
|
18
|
+
*/
|
|
19
|
+
function findFocusableAncestor(node) {
|
|
20
|
+
let current = node;
|
|
21
|
+
while (current) {
|
|
22
|
+
if (isFocusable(current)) return current;
|
|
23
|
+
current = current.parent;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* DFS traversal of focusable nodes in tab order, optionally scoped.
|
|
29
|
+
*
|
|
30
|
+
* When scope is provided, only nodes within that scope subtree are included.
|
|
31
|
+
* If a focusScope node is encountered during traversal, its children are
|
|
32
|
+
* skipped (they belong to a different scope), unless that scope IS the
|
|
33
|
+
* provided scope node.
|
|
34
|
+
*/
|
|
35
|
+
function getTabOrder(root, scope) {
|
|
36
|
+
const result = [];
|
|
37
|
+
const walkRoot = scope ?? root;
|
|
38
|
+
function walk(node) {
|
|
39
|
+
if (node.hidden) return;
|
|
40
|
+
if (node.props.display === "none") return;
|
|
41
|
+
if (node !== walkRoot && isFocusScope(node)) {
|
|
42
|
+
if (isFocusable(node)) result.push(node);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (isFocusable(node)) result.push(node);
|
|
46
|
+
for (const child of node.children) walk(child);
|
|
47
|
+
}
|
|
48
|
+
walk(walkRoot);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Walk up from a node to find the nearest ancestor (or self) with focusScope prop.
|
|
53
|
+
* Returns the testID of the enclosing scope, or null if none found.
|
|
54
|
+
*/
|
|
55
|
+
function findEnclosingScope(node) {
|
|
56
|
+
let current = node;
|
|
57
|
+
while (current) {
|
|
58
|
+
if (isFocusScope(current)) {
|
|
59
|
+
const props = current.props;
|
|
60
|
+
return typeof props.testID === "string" ? props.testID : null;
|
|
61
|
+
}
|
|
62
|
+
current = current.parent;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Find a node by testID in the subtree rooted at root.
|
|
68
|
+
* DFS, returns the first match.
|
|
69
|
+
*/
|
|
70
|
+
function findByTestID(root, testID) {
|
|
71
|
+
if (root.props.testID === testID) return root;
|
|
72
|
+
for (const child of root.children) {
|
|
73
|
+
const found = findByTestID(child, testID);
|
|
74
|
+
if (found) return found;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compute center point of a Rect.
|
|
80
|
+
*/
|
|
81
|
+
function rectCenter(rect) {
|
|
82
|
+
return {
|
|
83
|
+
cx: rect.x + rect.width / 2,
|
|
84
|
+
cy: rect.y + rect.height / 2
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if a candidate point falls within a 45-degree cone from source
|
|
89
|
+
* in the given direction (tvOS-style spatial navigation).
|
|
90
|
+
*
|
|
91
|
+
* The cone extends from the center of the source rect in the specified
|
|
92
|
+
* direction with a 45-degree half-angle (90-degree total aperture).
|
|
93
|
+
*/
|
|
94
|
+
function isInCone(source, candidate, direction) {
|
|
95
|
+
const dx = candidate.cx - source.cx;
|
|
96
|
+
const dy = candidate.cy - source.cy;
|
|
97
|
+
switch (direction) {
|
|
98
|
+
case "up":
|
|
99
|
+
if (dy >= 0) return false;
|
|
100
|
+
return Math.abs(dx) <= Math.abs(dy);
|
|
101
|
+
case "down":
|
|
102
|
+
if (dy <= 0) return false;
|
|
103
|
+
return Math.abs(dx) <= Math.abs(dy);
|
|
104
|
+
case "left":
|
|
105
|
+
if (dx >= 0) return false;
|
|
106
|
+
return Math.abs(dy) <= Math.abs(dx);
|
|
107
|
+
case "right":
|
|
108
|
+
if (dx <= 0) return false;
|
|
109
|
+
return Math.abs(dy) <= Math.abs(dx);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Euclidean distance between two points.
|
|
114
|
+
*/
|
|
115
|
+
function distance(a, b) {
|
|
116
|
+
const dx = a.cx - b.cx;
|
|
117
|
+
const dy = a.cy - b.cy;
|
|
118
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Find the nearest focusable candidate in a given direction using
|
|
122
|
+
* 45-degree cone heuristic (tvOS-style spatial navigation).
|
|
123
|
+
*
|
|
124
|
+
* From the center of the source rect, draw a cone in the target direction.
|
|
125
|
+
* Filter candidates whose center falls within the cone. Pick the closest
|
|
126
|
+
* by Euclidean distance.
|
|
127
|
+
*
|
|
128
|
+
* @param from - The currently focused node
|
|
129
|
+
* @param direction - Direction to search
|
|
130
|
+
* @param candidates - All focusable nodes to consider
|
|
131
|
+
* @param layoutFn - Function to get screen rect for a node (null if not laid out)
|
|
132
|
+
*/
|
|
133
|
+
function findSpatialTarget(from, direction, candidates, layoutFn) {
|
|
134
|
+
const sourceRect = layoutFn(from);
|
|
135
|
+
if (!sourceRect) return null;
|
|
136
|
+
const source = rectCenter(sourceRect);
|
|
137
|
+
let best = null;
|
|
138
|
+
let bestDist = Infinity;
|
|
139
|
+
for (const candidate of candidates) {
|
|
140
|
+
if (candidate === from) continue;
|
|
141
|
+
const candidateRect = layoutFn(candidate);
|
|
142
|
+
if (!candidateRect) continue;
|
|
143
|
+
const target = rectCenter(candidateRect);
|
|
144
|
+
if (!isInCone(source, target, direction)) continue;
|
|
145
|
+
const dist = distance(source, target);
|
|
146
|
+
if (dist < bestDist) {
|
|
147
|
+
bestDist = dist;
|
|
148
|
+
best = candidate;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return best;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if a node has an explicit nextFocus{Direction} override prop.
|
|
155
|
+
*
|
|
156
|
+
* These props allow components to declare explicit focus targets for
|
|
157
|
+
* spatial navigation, overriding the cone heuristic.
|
|
158
|
+
*
|
|
159
|
+
* @param node - The node to check
|
|
160
|
+
* @param direction - Direction string: "up", "down", "left", "right"
|
|
161
|
+
* @returns The testID of the explicit target, or null
|
|
162
|
+
*/
|
|
163
|
+
function getExplicitFocusLink(node, direction) {
|
|
164
|
+
const value = node.props[`nextFocus${direction.charAt(0).toUpperCase()}${direction.slice(1)}`];
|
|
165
|
+
return typeof value === "string" ? value : null;
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region packages/ag/src/interactive-signals.ts
|
|
169
|
+
/**
|
|
170
|
+
* Ensure a node has an InteractiveState object, creating one if needed.
|
|
171
|
+
* Returns the (possibly newly created) state.
|
|
172
|
+
*/
|
|
173
|
+
function ensureInteractiveState(node) {
|
|
174
|
+
if (!node.interactiveState) node.interactiveState = {
|
|
175
|
+
hovered: false,
|
|
176
|
+
armed: false,
|
|
177
|
+
selected: false,
|
|
178
|
+
focused: false,
|
|
179
|
+
dropTarget: false
|
|
180
|
+
};
|
|
181
|
+
return node.interactiveState;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Set the hovered state. Returns true if the value changed.
|
|
185
|
+
*/
|
|
186
|
+
function setHovered(node, value) {
|
|
187
|
+
const state = ensureInteractiveState(node);
|
|
188
|
+
if (state.hovered === value) return false;
|
|
189
|
+
state.hovered = value;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Set the armed state (pointer-down, awaiting click). Returns true if the value changed.
|
|
194
|
+
*/
|
|
195
|
+
function setArmed(node, value) {
|
|
196
|
+
const state = ensureInteractiveState(node);
|
|
197
|
+
if (state.armed === value) return false;
|
|
198
|
+
state.armed = value;
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Set the focused state. Returns true if the value changed.
|
|
203
|
+
*/
|
|
204
|
+
function setFocused(node, value) {
|
|
205
|
+
const state = ensureInteractiveState(node);
|
|
206
|
+
if (state.focused === value) return false;
|
|
207
|
+
state.focused = value;
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region packages/ag/src/tree-utils.ts
|
|
212
|
+
/**
|
|
213
|
+
* Collect the ancestor path from target to root (inclusive).
|
|
214
|
+
*/
|
|
215
|
+
function getAncestorPath(node) {
|
|
216
|
+
const path = [];
|
|
217
|
+
let current = node;
|
|
218
|
+
while (current) {
|
|
219
|
+
path.push(current);
|
|
220
|
+
current = current.parent;
|
|
221
|
+
}
|
|
222
|
+
return path;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Check if a point is inside a rect.
|
|
226
|
+
*/
|
|
227
|
+
function pointInRect(x, y, rect) {
|
|
228
|
+
return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region packages/ag-term/src/mouse-events.ts
|
|
232
|
+
/**
|
|
233
|
+
* DOM-level Mouse Events for silvery
|
|
234
|
+
*
|
|
235
|
+
* Provides React DOM-compatible mouse event infrastructure:
|
|
236
|
+
* - SilveryMouseEvent / SilveryWheelEvent synthetic event objects
|
|
237
|
+
* - Tree-based hit testing using scrollRect (replaces manual HitRegistry)
|
|
238
|
+
* - Event dispatch with bubbling (target → root, stopPropagation support)
|
|
239
|
+
* - Double-click detection (300ms / 2-cell threshold)
|
|
240
|
+
* - mouseenter/mouseleave tracking (no bubble, like DOM spec)
|
|
241
|
+
*/
|
|
242
|
+
const mouseLog = createLogger("silvery:mouse");
|
|
243
|
+
/**
|
|
244
|
+
* Create a synthetic mouse event.
|
|
245
|
+
*
|
|
246
|
+
* Modifier keys are merged from two sources:
|
|
247
|
+
* - SGR mouse protocol: reports Ctrl, Alt/Meta, Shift (reliable)
|
|
248
|
+
* - Keyboard tracking: reports Super/Cmd, Hyper, CapsLock, NumLock (via Kitty protocol)
|
|
249
|
+
*
|
|
250
|
+
* `metaKey` = keyboard-tracked Super (Cmd on macOS). SGR "meta" maps to `altKey`.
|
|
251
|
+
*/
|
|
252
|
+
function createMouseEvent(type, x, y, target, parsed, keyboardMods) {
|
|
253
|
+
let propagationStopped = false;
|
|
254
|
+
let defaultPrevented = false;
|
|
255
|
+
const metaKey = keyboardMods?.super ?? false;
|
|
256
|
+
if (type === "click" || type === "mousedown") mouseLog.debug?.(`createMouseEvent(${type}) metaKey=${metaKey} keyboardMods.super=${keyboardMods?.super}`);
|
|
257
|
+
else if (type === "wheel") {
|
|
258
|
+
const targetId = target.props.id ?? "";
|
|
259
|
+
mouseLog.debug?.(`createMouseEvent(wheel) x=${x} y=${y} delta=${parsed.delta ?? 0} target=${target.type}#${targetId}`);
|
|
260
|
+
} else if (type === "mouseup") mouseLog.debug?.(`createMouseEvent(mouseup) x=${x} y=${y} button=${parsed.button}`);
|
|
261
|
+
return {
|
|
262
|
+
type,
|
|
263
|
+
x,
|
|
264
|
+
y,
|
|
265
|
+
clientX: parsed.clientX,
|
|
266
|
+
clientY: parsed.clientY,
|
|
267
|
+
button: parsed.button,
|
|
268
|
+
altKey: parsed.meta,
|
|
269
|
+
ctrlKey: parsed.ctrl,
|
|
270
|
+
metaKey,
|
|
271
|
+
shiftKey: parsed.shift,
|
|
272
|
+
timeStamp: parsed.receivedAt ?? performance.now(),
|
|
273
|
+
inputBatchId: parsed.inputBatchId,
|
|
274
|
+
target,
|
|
275
|
+
currentTarget: target,
|
|
276
|
+
nativeEvent: parsed,
|
|
277
|
+
get propagationStopped() {
|
|
278
|
+
return propagationStopped;
|
|
279
|
+
},
|
|
280
|
+
get defaultPrevented() {
|
|
281
|
+
return defaultPrevented;
|
|
282
|
+
},
|
|
283
|
+
stopPropagation() {
|
|
284
|
+
propagationStopped = true;
|
|
285
|
+
},
|
|
286
|
+
preventDefault() {
|
|
287
|
+
defaultPrevented = true;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Create a synthetic wheel event.
|
|
293
|
+
*/
|
|
294
|
+
function createWheelEvent(x, y, target, parsed, keyboardMods) {
|
|
295
|
+
const base = createMouseEvent("wheel", x, y, target, parsed, keyboardMods);
|
|
296
|
+
base.deltaY = parsed.delta ?? 0;
|
|
297
|
+
base.deltaX = 0;
|
|
298
|
+
return base;
|
|
299
|
+
}
|
|
300
|
+
/** Position property on a Box that takes the node out of normal flow. */
|
|
301
|
+
function isAbsolutePositioned(node) {
|
|
302
|
+
return node.props.position === "absolute";
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Geometry-based hit test for absolute-positioned descendants.
|
|
306
|
+
*
|
|
307
|
+
* Walks the whole subtree rooted at `node` in tree order. For each
|
|
308
|
+
* absolute-positioned descendant whose scrollRect contains (x, y), recurse
|
|
309
|
+
* into it as a standalone hit-test (which finds the deepest in-flow child
|
|
310
|
+
* under that absolute) and track the latest-in-tree hit — that one paints
|
|
311
|
+
* on top (third pass in render order uses natural child order, so later =
|
|
312
|
+
* higher z).
|
|
313
|
+
*
|
|
314
|
+
* Respects pointerEvents="none" on the absolute root and its ancestors,
|
|
315
|
+
* and overflow:hidden/scroll clipping on ancestors up to `node`.
|
|
316
|
+
*
|
|
317
|
+
* Returns null if no absolute descendant covers (x, y).
|
|
318
|
+
*/
|
|
319
|
+
function hitTestAbsoluteDescendants(node, x, y, ancestorClipRect) {
|
|
320
|
+
let result = null;
|
|
321
|
+
for (const child of node.children) {
|
|
322
|
+
const cp = child.props;
|
|
323
|
+
if (cp.pointerEvents === "none") continue;
|
|
324
|
+
let childClip = ancestorClipRect;
|
|
325
|
+
if (cp.overflow === "hidden" || cp.overflow === "scroll") {
|
|
326
|
+
const cr = child.scrollRect;
|
|
327
|
+
if (cr) childClip = childClip ? intersectRect(childClip, cr) : cr;
|
|
328
|
+
}
|
|
329
|
+
if (isAbsolutePositioned(child) && child.scrollRect) {
|
|
330
|
+
if (!(ancestorClipRect && !pointInRect(x, y, ancestorClipRect)) && pointInRect(x, y, child.scrollRect)) {
|
|
331
|
+
const hit = hitTestAbsoluteDescendants(child, x, y, null) ?? hitTestInFlow(child, x, y);
|
|
332
|
+
if (hit) result = hit;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const deeper = hitTestAbsoluteDescendants(child, x, y, childClip);
|
|
336
|
+
if (deeper) result = deeper;
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
/** Compute the intersection of two rects; returns a zero-size rect if disjoint. */
|
|
341
|
+
function intersectRect(a, b) {
|
|
342
|
+
const x1 = Math.max(a.x, b.x);
|
|
343
|
+
const y1 = Math.max(a.y, b.y);
|
|
344
|
+
const x2 = Math.min(a.x + a.width, b.x + b.width);
|
|
345
|
+
const y2 = Math.min(a.y + a.height, b.y + b.height);
|
|
346
|
+
return {
|
|
347
|
+
x: x1,
|
|
348
|
+
y: y1,
|
|
349
|
+
width: Math.max(0, x2 - x1),
|
|
350
|
+
height: Math.max(0, y2 - y1)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* In-flow (non-absolute) DFS hit test. Used by both `hitTest` after the
|
|
355
|
+
* absolute pass, and by `hitTestAbsoluteDescendants` when recursing into a
|
|
356
|
+
* matched absolute to find the deepest in-flow descendant under it.
|
|
357
|
+
*
|
|
358
|
+
* Skips absolute children — they're handled by the absolute pass at the
|
|
359
|
+
* entry point (`hitTest`).
|
|
360
|
+
*/
|
|
361
|
+
function hitTestInFlow(node, x, y) {
|
|
362
|
+
const rect = node.scrollRect;
|
|
363
|
+
if (!rect) return null;
|
|
364
|
+
if (!pointInRect(x, y, rect)) return null;
|
|
365
|
+
const props = node.props;
|
|
366
|
+
if (props.pointerEvents === "none") return null;
|
|
367
|
+
const clips = props.overflow === "hidden" || props.overflow === "scroll";
|
|
368
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
369
|
+
const child = node.children[i];
|
|
370
|
+
if (isAbsolutePositioned(child)) continue;
|
|
371
|
+
if (clips) {
|
|
372
|
+
if (child.scrollRect && !pointInRect(x, y, rect)) continue;
|
|
373
|
+
}
|
|
374
|
+
const hit = hitTestInFlow(child, x, y);
|
|
375
|
+
if (hit) return hit;
|
|
376
|
+
}
|
|
377
|
+
if (node.type === "silvery-text") for (let i = node.children.length - 1; i >= 0; i--) {
|
|
378
|
+
const child = node.children[i];
|
|
379
|
+
if (child.inlineRects) {
|
|
380
|
+
for (const inlineRect of child.inlineRects) if (pointInRect(x, y, inlineRect)) return child;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return node;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Tree-based hit test: find the deepest node whose scrollRect contains (x, y).
|
|
387
|
+
*
|
|
388
|
+
* Uses reverse child order (last sibling wins = highest z-order, like DOM).
|
|
389
|
+
* Respects overflow:hidden clipping and pointerEvents="none".
|
|
390
|
+
*
|
|
391
|
+
* ### Absolute-positioned nodes escape parent bounds
|
|
392
|
+
*
|
|
393
|
+
* Absolute descendants participate in hit-testing by GEOMETRY, not by
|
|
394
|
+
* tree order / parent rect containment. An absolute child can be placed
|
|
395
|
+
* outside its parent's bounding rect (e.g., a popover anchored near a
|
|
396
|
+
* viewport edge); it still occupies screen cells at its own geometry and
|
|
397
|
+
* must be hittable.
|
|
398
|
+
*
|
|
399
|
+
* The hit test runs an "absolute pass" first that walks the whole subtree
|
|
400
|
+
* for absolute descendants and returns the latest-in-tree hit (matching
|
|
401
|
+
* the three-pass render order where absolute children paint on top of
|
|
402
|
+
* normal + sticky content). If no absolute descendant covers the point,
|
|
403
|
+
* it falls through to standard in-flow DFS.
|
|
404
|
+
*
|
|
405
|
+
* A recursive sub-call (via `hitTest(absolute, ...)`) would re-run the
|
|
406
|
+
* absolute pass on that absolute's subtree — which is correct: nested
|
|
407
|
+
* absolutes also need geometry-based hit testing.
|
|
408
|
+
*/
|
|
409
|
+
function hitTest(node, x, y) {
|
|
410
|
+
const absHit = hitTestAbsoluteDescendants(node, x, y, null);
|
|
411
|
+
if (absHit) return absHit;
|
|
412
|
+
return hitTestInFlow(node, x, y);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Resolve the effective userSelect value for a node.
|
|
416
|
+
* "auto" inherits from parent; root defaults to "text".
|
|
417
|
+
*/
|
|
418
|
+
function resolveUserSelect(node) {
|
|
419
|
+
let current = node;
|
|
420
|
+
while (current) {
|
|
421
|
+
const value = current.props.userSelect;
|
|
422
|
+
if (value === "none" || value === "text" || value === "contain") return value;
|
|
423
|
+
current = current.parent;
|
|
424
|
+
}
|
|
425
|
+
return "text";
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Selection hit test: find the deepest node whose text is selectable at (x, y).
|
|
429
|
+
*
|
|
430
|
+
* Unlike pointer hitTest, this:
|
|
431
|
+
* - Ignores pointerEvents (a node with pointerEvents="none" can still be selectable)
|
|
432
|
+
* - Respects userSelect (a node with userSelect="none" is not a selection target)
|
|
433
|
+
*/
|
|
434
|
+
function selectionHitTest(node, x, y) {
|
|
435
|
+
return selectionHitTestInner(node, x, y, true);
|
|
436
|
+
}
|
|
437
|
+
function selectionCellFromPoint(x, y) {
|
|
438
|
+
return {
|
|
439
|
+
col: Math.max(0, Math.floor(x)),
|
|
440
|
+
row: Math.max(0, Math.floor(y))
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Find the nearest selectable rendered-text cell inside a container rect.
|
|
445
|
+
*
|
|
446
|
+
* This is the terminal-target equivalent of the browser's
|
|
447
|
+
* caretPositionFromPoint behavior for text-containing boxes: a mousedown in
|
|
448
|
+
* padding or interior whitespace can still start text selection by snapping to
|
|
449
|
+
* nearby rendered text, while genuinely text-free containers return null.
|
|
450
|
+
*/
|
|
451
|
+
function nearestSelectableCellFromPoint(buffer, rect, x, y) {
|
|
452
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
453
|
+
const left = Math.max(0, Math.floor(rect.x));
|
|
454
|
+
const right = Math.min(buffer.width - 1, Math.ceil(rect.x + rect.width) - 1);
|
|
455
|
+
const top = Math.max(0, Math.floor(rect.y));
|
|
456
|
+
const bottom = Math.min(buffer.height - 1, Math.ceil(rect.y + rect.height) - 1);
|
|
457
|
+
if (left > right || top > bottom) return null;
|
|
458
|
+
const pointerCol = Math.max(0, Math.floor(x));
|
|
459
|
+
const pointerRow = Math.max(0, Math.floor(y));
|
|
460
|
+
const rows = [];
|
|
461
|
+
for (let row = top; row <= bottom; row++) {
|
|
462
|
+
let first = -1;
|
|
463
|
+
let last = -1;
|
|
464
|
+
let nearest = -1;
|
|
465
|
+
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
466
|
+
for (let col = left; col <= right; col++) {
|
|
467
|
+
if (!buffer.isCellSelectable(col, row)) continue;
|
|
468
|
+
if (first === -1) first = col;
|
|
469
|
+
last = col;
|
|
470
|
+
const distance = Math.abs(col - pointerCol);
|
|
471
|
+
if (distance < nearestDistance) {
|
|
472
|
+
nearest = col;
|
|
473
|
+
nearestDistance = distance;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (first !== -1) rows.push({
|
|
477
|
+
row,
|
|
478
|
+
first,
|
|
479
|
+
last,
|
|
480
|
+
nearest
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
if (rows.length === 0) return null;
|
|
484
|
+
const firstRow = rows[0];
|
|
485
|
+
const lastRow = rows[rows.length - 1];
|
|
486
|
+
const sameRow = rows.find((candidate) => candidate.row === pointerRow);
|
|
487
|
+
if (sameRow) {
|
|
488
|
+
if (pointerCol < sameRow.first) return {
|
|
489
|
+
col: sameRow.first,
|
|
490
|
+
row: sameRow.row
|
|
491
|
+
};
|
|
492
|
+
if (pointerCol > sameRow.last) return {
|
|
493
|
+
col: Math.min(right, sameRow.last + 1),
|
|
494
|
+
row: sameRow.row
|
|
495
|
+
};
|
|
496
|
+
return {
|
|
497
|
+
col: sameRow.nearest,
|
|
498
|
+
row: sameRow.row
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (pointerRow < firstRow.row) return {
|
|
502
|
+
col: firstRow.first,
|
|
503
|
+
row: firstRow.row
|
|
504
|
+
};
|
|
505
|
+
if (pointerRow > lastRow.row) return {
|
|
506
|
+
col: lastRow.last,
|
|
507
|
+
row: lastRow.row
|
|
508
|
+
};
|
|
509
|
+
for (let i = 0; i < rows.length; i++) {
|
|
510
|
+
const candidate = rows[i];
|
|
511
|
+
if (candidate.row > pointerRow) return {
|
|
512
|
+
col: candidate.first,
|
|
513
|
+
row: candidate.row
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
col: lastRow.last,
|
|
518
|
+
row: lastRow.row
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Resolve the semantic selection anchor for a terminal pointer position.
|
|
523
|
+
*
|
|
524
|
+
* This is the single owner for mousedown selection semantics:
|
|
525
|
+
* - exact selectable glyph / empty rendered line hits
|
|
526
|
+
* - `userSelect="none"` pointer targets that block document selection
|
|
527
|
+
* - nearest rendered text-cell fallback from blank padding inside text containers
|
|
528
|
+
* - `userSelect="contain"` / document boundary discovery
|
|
529
|
+
* - Shift/raw buffer selection that bypasses document scopes
|
|
530
|
+
*/
|
|
531
|
+
function resolveSelectionAnchorFromPoint(options) {
|
|
532
|
+
const { root, buffer, x, y } = options;
|
|
533
|
+
const forceBufferSelection = options.forceBufferSelection === true;
|
|
534
|
+
const downCell = selectionCellFromPoint(x, y);
|
|
535
|
+
let anchorCell = downCell;
|
|
536
|
+
if (!root) return forceBufferSelection ? {
|
|
537
|
+
node: null,
|
|
538
|
+
cell: anchorCell,
|
|
539
|
+
downCell,
|
|
540
|
+
boundaries: [],
|
|
541
|
+
forceBufferSelection
|
|
542
|
+
} : null;
|
|
543
|
+
const pointerTarget = hitTest(root, x, y);
|
|
544
|
+
const pointerBlocksSelection = pointerTarget !== null && resolveUserSelect(pointerTarget) === "none";
|
|
545
|
+
let selectedNode = !pointerBlocksSelection ? selectionHitTest(root, x, y) : null;
|
|
546
|
+
if (selectedNode === null && !forceBufferSelection && !pointerBlocksSelection && pointerTarget !== null && buffer) {
|
|
547
|
+
let current = pointerTarget;
|
|
548
|
+
while (current && selectedNode === null) {
|
|
549
|
+
const rect = current.scrollRect;
|
|
550
|
+
const nearest = rect ? nearestSelectableCellFromPoint(buffer, rect, x, y) : null;
|
|
551
|
+
if (!nearest) {
|
|
552
|
+
current = current.parent;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const nearestHit = selectionHitTest(root, nearest.col, nearest.row);
|
|
556
|
+
if (nearestHit) {
|
|
557
|
+
anchorCell = nearest;
|
|
558
|
+
selectedNode = nearestHit;
|
|
559
|
+
}
|
|
560
|
+
current = current.parent;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (selectedNode === null && !forceBufferSelection) return null;
|
|
564
|
+
return {
|
|
565
|
+
node: selectedNode,
|
|
566
|
+
cell: anchorCell,
|
|
567
|
+
downCell,
|
|
568
|
+
boundaries: selectedNode ? findSelectionBoundaries(selectedNode) : [],
|
|
569
|
+
forceBufferSelection
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function selectionHitTestInner(node, x, y, allowRowFallback) {
|
|
573
|
+
const rect = node.scrollRect;
|
|
574
|
+
if (!rect) return null;
|
|
575
|
+
if (!pointInRect(x, y, rect)) return null;
|
|
576
|
+
const props = node.props;
|
|
577
|
+
if (resolveUserSelect(node) === "none") return null;
|
|
578
|
+
const clips = props.overflow === "hidden" || props.overflow === "scroll";
|
|
579
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
580
|
+
const child = node.children[i];
|
|
581
|
+
const childRect = child.scrollRect;
|
|
582
|
+
if (clips) {
|
|
583
|
+
if (childRect && !pointInRect(x, y, rect)) continue;
|
|
584
|
+
}
|
|
585
|
+
if (childRect && pointInRect(x, y, childRect)) {
|
|
586
|
+
if (resolveUserSelect(child) === "none") return null;
|
|
587
|
+
const hit = selectionHitTestInner(child, x, y, false);
|
|
588
|
+
if (hit) return hit;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const hit = selectionHitTestInner(child, x, y, false);
|
|
592
|
+
if (hit) return hit;
|
|
593
|
+
}
|
|
594
|
+
if (node.type === "silvery-text") for (let i = node.children.length - 1; i >= 0; i--) {
|
|
595
|
+
const child = node.children[i];
|
|
596
|
+
if (child.inlineRects) {
|
|
597
|
+
for (const inlineRect of child.inlineRects) if (pointInRect(x, y, inlineRect)) return child;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (node.type === "silvery-text") return pointHitsRenderedTextRow(node, y) ? node : null;
|
|
601
|
+
return allowRowFallback ? findTextNodeOnRow(node, y) : null;
|
|
602
|
+
}
|
|
603
|
+
function nodeSelectionScope(node) {
|
|
604
|
+
const rect = node.scrollRect;
|
|
605
|
+
if (!rect) return null;
|
|
606
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
607
|
+
if (node.type === "silvery-text") {
|
|
608
|
+
const textBounds = renderedTextBounds(node);
|
|
609
|
+
if (textBounds === null) return null;
|
|
610
|
+
return {
|
|
611
|
+
top: rect.y,
|
|
612
|
+
bottom: rect.y + textBounds.height - 1,
|
|
613
|
+
left: rect.x,
|
|
614
|
+
right: rect.x + textBounds.width - 1
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
top: rect.y,
|
|
619
|
+
bottom: rect.y + rect.height - 1,
|
|
620
|
+
left: rect.x,
|
|
621
|
+
right: rect.x + rect.width - 1
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function collectText(node) {
|
|
625
|
+
if (node.type === "silvery-text" && node.textContent !== void 0) return node.textContent;
|
|
626
|
+
let out = "";
|
|
627
|
+
for (const child of node.children) out += collectText(child);
|
|
628
|
+
return out;
|
|
629
|
+
}
|
|
630
|
+
function renderedTextLines(node) {
|
|
631
|
+
const rect = node.scrollRect;
|
|
632
|
+
if (!rect || rect.width <= 0) return [];
|
|
633
|
+
const text = collectText(node);
|
|
634
|
+
if (text.length === 0) return [];
|
|
635
|
+
const wrap = node.props.wrap;
|
|
636
|
+
return wrap !== false && wrap !== "truncate" && wrap !== "truncate-end" && wrap !== "clip" ? wrapText(text, rect.width, true, false) : text.split("\n");
|
|
637
|
+
}
|
|
638
|
+
function renderedTextBounds(node) {
|
|
639
|
+
const rect = node.scrollRect;
|
|
640
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return null;
|
|
641
|
+
const lines = renderedTextLines(node);
|
|
642
|
+
if (lines.length === 0) return null;
|
|
643
|
+
let maxWidth = 0;
|
|
644
|
+
for (const line of lines) maxWidth = Math.max(maxWidth, Math.min(rect.width, displayWidthAnsi(line)));
|
|
645
|
+
if (maxWidth <= 0) return null;
|
|
646
|
+
return {
|
|
647
|
+
width: maxWidth,
|
|
648
|
+
height: Math.min(rect.height, lines.length)
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function pointHitsRenderedTextRow(node, y) {
|
|
652
|
+
const rect = node.scrollRect;
|
|
653
|
+
if (!rect) return false;
|
|
654
|
+
const row = y - rect.y;
|
|
655
|
+
if (row < 0 || row >= rect.height) return false;
|
|
656
|
+
return renderedTextLines(node)[row] !== void 0;
|
|
657
|
+
}
|
|
658
|
+
function findTextNodeOnRow(node, y) {
|
|
659
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
660
|
+
const child = node.children[i];
|
|
661
|
+
const hit = findTextNodeOnRow(child, y);
|
|
662
|
+
if (hit) return hit;
|
|
663
|
+
}
|
|
664
|
+
return node.type === "silvery-text" && pointHitsRenderedTextRow(node, y) ? node : null;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Return the selectable document-ancestor chain for a node, nearest first.
|
|
668
|
+
*
|
|
669
|
+
* This is the DOM-like selection path: ordinary selectable nodes create
|
|
670
|
+
* semantic selection regions, while `userSelect="contain"` marks a CSS-style
|
|
671
|
+
* hard containment boundary that selection must not escape.
|
|
672
|
+
*/
|
|
673
|
+
function findSelectionBoundaries(node) {
|
|
674
|
+
const boundaries = [];
|
|
675
|
+
let current = node;
|
|
676
|
+
while (current) {
|
|
677
|
+
if (resolveUserSelect(current) !== "none") {
|
|
678
|
+
const scope = nodeSelectionScope(current);
|
|
679
|
+
if (scope) {
|
|
680
|
+
const props = current.props;
|
|
681
|
+
boundaries.push({
|
|
682
|
+
node: current,
|
|
683
|
+
scope,
|
|
684
|
+
hardContain: props.userSelect === "contain"
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
current = current.parent;
|
|
689
|
+
}
|
|
690
|
+
return boundaries;
|
|
691
|
+
}
|
|
692
|
+
/** Map event type to the handler prop name */
|
|
693
|
+
const EVENT_HANDLER_MAP = {
|
|
694
|
+
click: "onClick",
|
|
695
|
+
dblclick: "onDoubleClick",
|
|
696
|
+
tripleclick: "onTripleClick",
|
|
697
|
+
mousedown: "onMouseDown",
|
|
698
|
+
mouseup: "onMouseUp",
|
|
699
|
+
mousemove: "onMouseMove",
|
|
700
|
+
mouseenter: "onMouseEnter",
|
|
701
|
+
mouseleave: "onMouseLeave",
|
|
702
|
+
wheel: "onWheel"
|
|
703
|
+
};
|
|
704
|
+
/**
|
|
705
|
+
* Dispatch a mouse event through the render tree with DOM-style bubbling.
|
|
706
|
+
*
|
|
707
|
+
* Bubbles from target → root, calling the appropriate handler on each node.
|
|
708
|
+
* stopPropagation() halts bubbling. mouseenter/mouseleave do NOT bubble (DOM spec).
|
|
709
|
+
*/
|
|
710
|
+
function dispatchMouseEvent(event) {
|
|
711
|
+
const handlerProp = EVENT_HANDLER_MAP[event.type];
|
|
712
|
+
if (!handlerProp) return;
|
|
713
|
+
if (event.type === "mouseenter" || event.type === "mouseleave") {
|
|
714
|
+
const handler = event.target.props[handlerProp];
|
|
715
|
+
if (handler) {
|
|
716
|
+
const mutableEvent = event;
|
|
717
|
+
mutableEvent.currentTarget = event.target;
|
|
718
|
+
handler(event);
|
|
719
|
+
}
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const path = getAncestorPath(event.target);
|
|
723
|
+
for (const node of path) {
|
|
724
|
+
if (event.propagationStopped) break;
|
|
725
|
+
const handler = node.props[handlerProp];
|
|
726
|
+
if (handler) {
|
|
727
|
+
const mutableEvent = event;
|
|
728
|
+
mutableEvent.currentTarget = node;
|
|
729
|
+
handler(event);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function createClickCountState() {
|
|
734
|
+
return {
|
|
735
|
+
lastClickTime: 0,
|
|
736
|
+
lastClickX: -999,
|
|
737
|
+
lastClickY: -999,
|
|
738
|
+
lastClickButton: -1,
|
|
739
|
+
count: 0
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
/** @deprecated Use `createClickCountState()` instead. */
|
|
743
|
+
const createDoubleClickState = createClickCountState;
|
|
744
|
+
const MULTI_CLICK_TIME_MS = 300;
|
|
745
|
+
const MULTI_CLICK_DISTANCE = 2;
|
|
746
|
+
/**
|
|
747
|
+
* Determine the consecutive-click count for the current click.
|
|
748
|
+
*
|
|
749
|
+
* Returns 1 for a fresh click, 2 for a double-click, 3 for a triple-click.
|
|
750
|
+
* Subsequent clicks restart the chain at 1.
|
|
751
|
+
*
|
|
752
|
+
* Updates `state` so the next call sees the right history.
|
|
753
|
+
*/
|
|
754
|
+
function checkClickCount(state, x, y, button, now = Date.now()) {
|
|
755
|
+
const timeDelta = now - state.lastClickTime;
|
|
756
|
+
const dx = Math.abs(x - state.lastClickX);
|
|
757
|
+
const dy = Math.abs(y - state.lastClickY);
|
|
758
|
+
const inChain = button === state.lastClickButton && timeDelta <= MULTI_CLICK_TIME_MS && dx <= MULTI_CLICK_DISTANCE && dy <= MULTI_CLICK_DISTANCE;
|
|
759
|
+
let count;
|
|
760
|
+
if (!inChain || state.count >= 3) count = 1;
|
|
761
|
+
else if (state.count === 1) count = 2;
|
|
762
|
+
else count = 3;
|
|
763
|
+
state.lastClickTime = now;
|
|
764
|
+
state.lastClickX = x;
|
|
765
|
+
state.lastClickY = y;
|
|
766
|
+
state.lastClickButton = button;
|
|
767
|
+
state.count = count;
|
|
768
|
+
return count;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Check if a click qualifies as a double-click. Backwards-compatible
|
|
772
|
+
* wrapper around `checkClickCount`.
|
|
773
|
+
*
|
|
774
|
+
* @deprecated Use `checkClickCount` and inspect the returned count
|
|
775
|
+
* (`=== 2` for dblclick, `=== 3` for tripleclick).
|
|
776
|
+
*/
|
|
777
|
+
function checkDoubleClick(state, x, y, button, now = Date.now()) {
|
|
778
|
+
return checkClickCount(state, x, y, button, now) === 2;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Compute mouseenter/mouseleave transitions between two ancestor paths.
|
|
782
|
+
*
|
|
783
|
+
* Returns { entered, left } — arrays of nodes that were entered or left.
|
|
784
|
+
* Mirrors the DOM spec: fire mouseleave on nodes in prevPath not in nextPath,
|
|
785
|
+
* and mouseenter on nodes in nextPath not in prevPath.
|
|
786
|
+
*/
|
|
787
|
+
function computeEnterLeave(prevPath, nextPath) {
|
|
788
|
+
const prevSet = new Set(prevPath);
|
|
789
|
+
const nextSet = new Set(nextPath);
|
|
790
|
+
return {
|
|
791
|
+
entered: nextPath.filter((n) => !prevSet.has(n)),
|
|
792
|
+
left: prevPath.filter((n) => !nextSet.has(n))
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function createMouseEventProcessor(options) {
|
|
796
|
+
return {
|
|
797
|
+
doubleClick: createDoubleClickState(),
|
|
798
|
+
hoverPath: [],
|
|
799
|
+
mouseDownTarget: null,
|
|
800
|
+
mouseCaptureTarget: null,
|
|
801
|
+
outsideCaptureReleaseTimer: null,
|
|
802
|
+
outsideCaptureReleaseMouse: null,
|
|
803
|
+
focusManager: options?.focusManager,
|
|
804
|
+
keyboardModifiers: {
|
|
805
|
+
super: false,
|
|
806
|
+
hyper: false,
|
|
807
|
+
capsLock: false,
|
|
808
|
+
numLock: false
|
|
809
|
+
},
|
|
810
|
+
lastClickPrevented: false,
|
|
811
|
+
lastPointer: null
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function findMouseCaptureTarget(node) {
|
|
815
|
+
let current = node;
|
|
816
|
+
while (current) {
|
|
817
|
+
if (current.props.mouseCapture === true) return current;
|
|
818
|
+
current = current.parent;
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
const MOUSE_CAPTURE_OUTSIDE_GRACE_MS = 2e3;
|
|
823
|
+
function mouseUpParsed(parsed) {
|
|
824
|
+
return parsed.action === "up" ? parsed : {
|
|
825
|
+
...parsed,
|
|
826
|
+
action: "up"
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Update keyboard modifier state from a parsed key event.
|
|
831
|
+
* Call this for every keyboard event so mouse events can include accurate modifiers.
|
|
832
|
+
*/
|
|
833
|
+
function updateKeyboardModifiers(state, key) {
|
|
834
|
+
const isRelease = key.eventType === "release";
|
|
835
|
+
const prevSuper = state.keyboardModifiers.super;
|
|
836
|
+
if (key.super !== void 0) state.keyboardModifiers.super = isRelease ? false : key.super;
|
|
837
|
+
if (key.hyper !== void 0) state.keyboardModifiers.hyper = isRelease ? false : key.hyper;
|
|
838
|
+
if (key.capsLock !== void 0) state.keyboardModifiers.capsLock = key.capsLock;
|
|
839
|
+
if (key.numLock !== void 0) state.keyboardModifiers.numLock = key.numLock;
|
|
840
|
+
if (state.keyboardModifiers.super !== prevSuper) mouseLog.debug?.(`keyboardModifiers.super: ${prevSuper} → ${state.keyboardModifiers.super} (key.super=${key.super}, eventType=${key.eventType})`);
|
|
841
|
+
}
|
|
842
|
+
function releaseMousePress(state, parsed) {
|
|
843
|
+
let defaultPrevented = false;
|
|
844
|
+
const dispatchTarget = state.mouseCaptureTarget;
|
|
845
|
+
const releaseParsed = mouseUpParsed(parsed);
|
|
846
|
+
cancelOutsideCaptureRelease(state);
|
|
847
|
+
if (state.mouseDownTarget) setArmed(state.mouseDownTarget, false);
|
|
848
|
+
if (dispatchTarget) {
|
|
849
|
+
const event = createMouseEvent("mouseup", releaseParsed.x, releaseParsed.y, dispatchTarget, releaseParsed, state.keyboardModifiers);
|
|
850
|
+
dispatchMouseEvent(event);
|
|
851
|
+
defaultPrevented = event.defaultPrevented;
|
|
852
|
+
}
|
|
853
|
+
state.lastClickPrevented = false;
|
|
854
|
+
state.mouseDownTarget = null;
|
|
855
|
+
state.mouseCaptureTarget = null;
|
|
856
|
+
return defaultPrevented;
|
|
857
|
+
}
|
|
858
|
+
function cancelOutsideCaptureRelease(state) {
|
|
859
|
+
if (state.outsideCaptureReleaseTimer !== null) clearTimeout(state.outsideCaptureReleaseTimer);
|
|
860
|
+
state.outsideCaptureReleaseTimer = null;
|
|
861
|
+
state.outsideCaptureReleaseMouse = null;
|
|
862
|
+
}
|
|
863
|
+
function scheduleOutsideCaptureRelease(state, parsed) {
|
|
864
|
+
state.outsideCaptureReleaseMouse = parsed;
|
|
865
|
+
if (state.outsideCaptureReleaseTimer !== null) return;
|
|
866
|
+
state.outsideCaptureReleaseTimer = setTimeout(() => {
|
|
867
|
+
const outsideMouse = state.outsideCaptureReleaseMouse ?? parsed;
|
|
868
|
+
releaseMousePress(state, outsideMouse);
|
|
869
|
+
clearHoverPath(state, outsideMouse);
|
|
870
|
+
}, MOUSE_CAPTURE_OUTSIDE_GRACE_MS);
|
|
871
|
+
}
|
|
872
|
+
function clearHoverPath(state, parsed) {
|
|
873
|
+
for (const node of state.hoverPath.slice().reverse()) {
|
|
874
|
+
setHovered(node, false);
|
|
875
|
+
dispatchMouseEvent(createMouseEvent("mouseleave", parsed.x, parsed.y, node, parsed, state.keyboardModifiers));
|
|
876
|
+
}
|
|
877
|
+
state.hoverPath = [];
|
|
878
|
+
state.lastPointer = null;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Re-resolve the hover path at the last known pointer coordinates and
|
|
882
|
+
* dispatch enter/leave for any nodes that changed. Use after a layout
|
|
883
|
+
* change that didn't come from a mouse event — most importantly:
|
|
884
|
+
*
|
|
885
|
+
* - Wheel scrolls (content shifts under a stationary cursor)
|
|
886
|
+
* - Async content arrival (transcript leaf appended, list re-flowed)
|
|
887
|
+
* - Programmatic re-layout (resize, theme switch)
|
|
888
|
+
*
|
|
889
|
+
* Without this, hover bg / hover-armed popovers stick to whatever AgNode
|
|
890
|
+
* was under the cursor when the last mouse event fired, even after the
|
|
891
|
+
* tree under that coordinate changed. Symptoms: persistent hover bg on
|
|
892
|
+
* rows that have scrolled out from under the pointer; popover targets
|
|
893
|
+
* arming on rows the cursor isn't over anymore.
|
|
894
|
+
*
|
|
895
|
+
* Idempotent — when nothing changed, no events fire and `state.hoverPath`
|
|
896
|
+
* stays identity-equal. Safe to call every render commit.
|
|
897
|
+
*
|
|
898
|
+
* Bead: @km/code/sticky-hover-residue.
|
|
899
|
+
*/
|
|
900
|
+
function refreshHoverPath(state, root) {
|
|
901
|
+
if (state.lastPointer === null) return;
|
|
902
|
+
if (state.mouseCaptureTarget) return;
|
|
903
|
+
const { x, y } = state.lastPointer;
|
|
904
|
+
const target = hitTest(root, x, y);
|
|
905
|
+
const newPath = target ? getAncestorPath(target) : [];
|
|
906
|
+
const { entered, left } = computeEnterLeave(state.hoverPath, newPath);
|
|
907
|
+
if (entered.length === 0 && left.length === 0) return;
|
|
908
|
+
const synthetic = {
|
|
909
|
+
x,
|
|
910
|
+
y,
|
|
911
|
+
button: 0,
|
|
912
|
+
action: "move",
|
|
913
|
+
coordinateMode: "cell",
|
|
914
|
+
shift: false,
|
|
915
|
+
meta: false,
|
|
916
|
+
ctrl: false
|
|
917
|
+
};
|
|
918
|
+
for (const node of left) {
|
|
919
|
+
setHovered(node, false);
|
|
920
|
+
dispatchMouseEvent(createMouseEvent("mouseleave", x, y, node, synthetic, state.keyboardModifiers));
|
|
921
|
+
}
|
|
922
|
+
for (const node of entered.reverse()) {
|
|
923
|
+
setHovered(node, true);
|
|
924
|
+
dispatchMouseEvent(createMouseEvent("mouseenter", x, y, node, synthetic, state.keyboardModifiers));
|
|
925
|
+
}
|
|
926
|
+
state.hoverPath = newPath;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Process a raw ParsedMouse event and dispatch DOM-level events on the render tree.
|
|
930
|
+
*
|
|
931
|
+
* Call this for every SGR mouse event received. It handles:
|
|
932
|
+
* - mousedown / mouseup
|
|
933
|
+
* - click (on mouseup if same target as mousedown)
|
|
934
|
+
* - dblclick (based on timing)
|
|
935
|
+
* - mousemove + mouseenter/mouseleave
|
|
936
|
+
* - wheel
|
|
937
|
+
*/
|
|
938
|
+
function processMouseEvent(state, parsed, root) {
|
|
939
|
+
const { x, y, action } = parsed;
|
|
940
|
+
state.lastPointer = {
|
|
941
|
+
x,
|
|
942
|
+
y
|
|
943
|
+
};
|
|
944
|
+
const target = hitTest(root, x, y);
|
|
945
|
+
if (action === "move") {
|
|
946
|
+
const nodeType = target?.type ?? "null";
|
|
947
|
+
const nodeId = target ? target.props.id ?? "" : "";
|
|
948
|
+
let enterAncestor = "";
|
|
949
|
+
if (target) {
|
|
950
|
+
let n = target;
|
|
951
|
+
while (n) {
|
|
952
|
+
if ("onMouseEnter" in n.props) {
|
|
953
|
+
enterAncestor = `${n.type}#${n.props.id ?? ""}`;
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
n = n.parent;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const newPath = target ? getAncestorPath(target) : [];
|
|
960
|
+
const { entered } = computeEnterLeave(state.hoverPath, newPath);
|
|
961
|
+
mouseLog.debug?.(`move x=${x} y=${y} target=${nodeType}#${nodeId} enterAncestor=${enterAncestor || "none"} entered=${entered.length} prevPath=${state.hoverPath.length}`);
|
|
962
|
+
}
|
|
963
|
+
let defaultPrevented = false;
|
|
964
|
+
if (target) cancelOutsideCaptureRelease(state);
|
|
965
|
+
if (!target) {
|
|
966
|
+
if (action === "move") {
|
|
967
|
+
if (state.mouseCaptureTarget) scheduleOutsideCaptureRelease(state, parsed);
|
|
968
|
+
else defaultPrevented = releaseMousePress(state, parsed);
|
|
969
|
+
clearHoverPath(state, parsed);
|
|
970
|
+
return defaultPrevented;
|
|
971
|
+
}
|
|
972
|
+
if (action === "up") defaultPrevented = releaseMousePress(state, parsed);
|
|
973
|
+
return defaultPrevented;
|
|
974
|
+
}
|
|
975
|
+
if (action === "down") {
|
|
976
|
+
state.mouseDownTarget = target;
|
|
977
|
+
state.mouseCaptureTarget = findMouseCaptureTarget(target);
|
|
978
|
+
setArmed(target, true);
|
|
979
|
+
if (state.focusManager) {
|
|
980
|
+
const focusable = findFocusableAncestor(target);
|
|
981
|
+
if (focusable) state.focusManager.focus(focusable, "mouse");
|
|
982
|
+
}
|
|
983
|
+
const event = createMouseEvent("mousedown", x, y, target, parsed, state.keyboardModifiers);
|
|
984
|
+
dispatchMouseEvent(event);
|
|
985
|
+
if (event.defaultPrevented) defaultPrevented = true;
|
|
986
|
+
} else if (action === "up") {
|
|
987
|
+
const dispatchTarget = state.mouseCaptureTarget ?? target;
|
|
988
|
+
if (state.mouseDownTarget) setArmed(state.mouseDownTarget, false);
|
|
989
|
+
state.lastClickPrevented = false;
|
|
990
|
+
dispatchMouseEvent(createMouseEvent("mouseup", x, y, dispatchTarget, parsed, state.keyboardModifiers));
|
|
991
|
+
if (state.mouseDownTarget) {
|
|
992
|
+
const count = checkClickCount(state.doubleClick, x, y, parsed.button);
|
|
993
|
+
const clickEvent = createMouseEvent("click", x, y, dispatchTarget, parsed, state.keyboardModifiers);
|
|
994
|
+
clickEvent.detail = count;
|
|
995
|
+
dispatchMouseEvent(clickEvent);
|
|
996
|
+
if (clickEvent.defaultPrevented) {
|
|
997
|
+
defaultPrevented = true;
|
|
998
|
+
state.lastClickPrevented = true;
|
|
999
|
+
}
|
|
1000
|
+
if (count >= 2) {
|
|
1001
|
+
const dblEvent = createMouseEvent("dblclick", x, y, dispatchTarget, parsed, state.keyboardModifiers);
|
|
1002
|
+
dblEvent.detail = 2;
|
|
1003
|
+
dispatchMouseEvent(dblEvent);
|
|
1004
|
+
if (dblEvent.defaultPrevented) {
|
|
1005
|
+
defaultPrevented = true;
|
|
1006
|
+
state.lastClickPrevented = true;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (count === 3) {
|
|
1010
|
+
const tripleEvent = createMouseEvent("tripleclick", x, y, dispatchTarget, parsed, state.keyboardModifiers);
|
|
1011
|
+
tripleEvent.detail = 3;
|
|
1012
|
+
dispatchMouseEvent(tripleEvent);
|
|
1013
|
+
if (tripleEvent.defaultPrevented) {
|
|
1014
|
+
defaultPrevented = true;
|
|
1015
|
+
state.lastClickPrevented = true;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
state.mouseDownTarget = null;
|
|
1020
|
+
state.mouseCaptureTarget = null;
|
|
1021
|
+
} else if (action === "move") {
|
|
1022
|
+
dispatchMouseEvent(createMouseEvent("mousemove", x, y, state.mouseCaptureTarget ?? target, parsed, state.keyboardModifiers));
|
|
1023
|
+
const newPath = getAncestorPath(target);
|
|
1024
|
+
const { entered, left } = computeEnterLeave(state.hoverPath, newPath);
|
|
1025
|
+
for (const node of left) {
|
|
1026
|
+
setHovered(node, false);
|
|
1027
|
+
dispatchMouseEvent(createMouseEvent("mouseleave", x, y, node, parsed, state.keyboardModifiers));
|
|
1028
|
+
}
|
|
1029
|
+
for (const node of entered.reverse()) {
|
|
1030
|
+
setHovered(node, true);
|
|
1031
|
+
dispatchMouseEvent(createMouseEvent("mouseenter", x, y, node, parsed, state.keyboardModifiers));
|
|
1032
|
+
}
|
|
1033
|
+
state.hoverPath = newPath;
|
|
1034
|
+
} else if (action === "wheel") {
|
|
1035
|
+
const event = createWheelEvent(x, y, target, parsed, state.keyboardModifiers);
|
|
1036
|
+
dispatchMouseEvent(event);
|
|
1037
|
+
if (event.defaultPrevented) defaultPrevented = true;
|
|
1038
|
+
}
|
|
1039
|
+
return defaultPrevented;
|
|
1040
|
+
}
|
|
1041
|
+
//#endregion
|
|
1042
|
+
export { findSpatialTarget as C, findFocusableAncestor as S, getTabOrder as T, getAncestorPath as _, createDoubleClickState as a, findByTestID as b, createWheelEvent as c, hitTest as d, processMouseEvent as f, updateKeyboardModifiers as g, selectionHitTest as h, createClickCountState as i, dispatchMouseEvent as l, resolveSelectionAnchorFromPoint as m, checkDoubleClick as n, createMouseEvent as o, refreshHoverPath as p, computeEnterLeave as r, createMouseEventProcessor as s, checkClickCount as t, findSelectionBoundaries as u, setArmed as v, getExplicitFocusLink as w, findEnclosingScope as x, setFocused as y };
|
|
1043
|
+
|
|
1044
|
+
//# sourceMappingURL=mouse-events-Dki3ISIp.mjs.map
|