electrobun 1.18.4-beta.18 → 1.18.4-beta.21
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 -0
- package/bin/electrobun.cjs +165 -0
- package/dist/api/browser/ui/__tests__/dom.test.ts +473 -0
- package/dist/api/browser/ui/__tests__/domStub.ts +218 -0
- package/dist/api/browser/ui/dom.ts +490 -0
- package/dist/api/browser/ui/index.ts +44 -0
- package/dist/api/browser/ui/jsx-dev-runtime.ts +16 -0
- package/dist/api/browser/ui/jsx-runtime.ts +56 -0
- package/dist/api/config/ElectrobunConfig.ts +33 -0
- package/dist/api/preload/.generated/compiled.ts +1 -1
- package/dist/api/preload/index.ts +2 -0
- package/dist/api/preload/uiTag.ts +45 -0
- package/dist/api/sdks/main/__tests__/utils-quit-exit-code.test.ts +44 -0
- package/dist/api/sdks/main/core/GpuWindow.ts +19 -0
- package/dist/api/sdks/main/core/Utils.ts +52 -4
- package/dist/api/sdks/main/core/WGPUView.ts +9 -0
- package/dist/api/sdks/main/entries/ui.ts +1 -0
- package/dist/api/sdks/main/proc/native.ts +187 -0
- package/dist/api/sdks/main/ui/__tests__/font.test.ts +49 -0
- package/dist/api/sdks/main/ui/__tests__/hit.test.ts +64 -0
- package/dist/api/sdks/main/ui/__tests__/jsx.test.ts +252 -0
- package/dist/api/sdks/main/ui/__tests__/layout.test.ts +159 -0
- package/dist/api/sdks/main/ui/__tests__/paint.test.ts +115 -0
- package/dist/api/sdks/main/ui/__tests__/reactive.test.ts +456 -0
- package/dist/api/sdks/main/ui/__tests__/scroll-focus-input.test.ts +298 -0
- package/dist/api/sdks/main/ui/__tests__/tree.test.ts +96 -0
- package/dist/api/sdks/main/ui/__tests__/ui.test.ts +170 -0
- package/dist/api/sdks/main/ui/elements.ts +135 -0
- package/dist/api/sdks/main/ui/font.ts +168 -0
- package/dist/api/sdks/main/ui/hit.ts +46 -0
- package/dist/api/sdks/main/ui/index.ts +71 -0
- package/dist/api/sdks/main/ui/input.ts +268 -0
- package/dist/api/sdks/main/ui/jsx-dev-runtime.ts +16 -0
- package/dist/api/sdks/main/ui/jsx-runtime.ts +136 -0
- package/dist/api/sdks/main/ui/keymap.ts +147 -0
- package/dist/api/sdks/main/ui/layout.ts +178 -0
- package/dist/api/sdks/main/ui/paint.ts +196 -0
- package/dist/api/sdks/main/ui/reactive.ts +4 -0
- package/dist/api/sdks/main/ui/renderer.ts +278 -0
- package/dist/api/sdks/main/ui/text.ts +175 -0
- package/dist/api/sdks/main/ui/textInput.ts +121 -0
- package/dist/api/sdks/main/ui/tree.ts +276 -0
- package/dist/api/sdks/main/ui/ui.ts +457 -0
- package/dist/api/sdks/main/ui/uiTagHost.ts +56 -0
- package/dist/api/sdks/main/ui/uiwindow.ts +330 -0
- package/dist/api/shared/build-dependencies.test.ts +1 -1
- package/dist/api/shared/build-dependencies.ts +4 -4
- package/dist/api/shared/linux-webkit-automation.test.ts +1 -1
- package/dist/api/shared/warren/jsx.ts +279 -0
- package/dist/api/shared/warren/reactive.ts +638 -0
- package/dist/api/shared/windows-unicode-ui.test.ts +4 -4
- package/dist/preload-full.js +35 -0
- package/dist/zig-sdk/electrobun.zig +197 -162
- package/{dash.config.ts → hutch.config.ts} +4 -3
- package/package.json +14 -2
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Embedded 5x7 bitmap font. Each glyph is 7 rows of 5 cells ('#' = on).
|
|
2
|
+
// Advance is 6 cells (5 + 1 spacing). Deliberately tiny: typography for the
|
|
3
|
+
// prototype only — shaping, system fonts, and accessibility come later.
|
|
4
|
+
|
|
5
|
+
export const GLYPH_W = 5;
|
|
6
|
+
export const GLYPH_H = 7;
|
|
7
|
+
export const GLYPH_ADVANCE = 6;
|
|
8
|
+
|
|
9
|
+
type GlyphRows = [string, string, string, string, string, string, string];
|
|
10
|
+
|
|
11
|
+
const G: Record<string, GlyphRows> = {
|
|
12
|
+
" ": [".....", ".....", ".....", ".....", ".....", ".....", "....."],
|
|
13
|
+
"!": ["..#..", "..#..", "..#..", "..#..", "..#..", ".....", "..#.."],
|
|
14
|
+
'"': [".#.#.", ".#.#.", ".....", ".....", ".....", ".....", "....."],
|
|
15
|
+
"#": [".#.#.", "#####", ".#.#.", ".#.#.", "#####", ".#.#.", "....."],
|
|
16
|
+
$: ["..#..", ".####", "#....", ".###.", "....#", "####.", "..#.."],
|
|
17
|
+
"%": ["##..#", "##..#", "...#.", "..#..", ".#...", "#..##", "#..##"],
|
|
18
|
+
"&": [".##..", "#..#.", "#.#..", ".#...", "#.#.#", "#..#.", ".##.#"],
|
|
19
|
+
"'": ["..#..", "..#..", ".....", ".....", ".....", ".....", "....."],
|
|
20
|
+
"(": ["...#.", "..#..", ".#...", ".#...", ".#...", "..#..", "...#."],
|
|
21
|
+
")": [".#...", "..#..", "...#.", "...#.", "...#.", "..#..", ".#..."],
|
|
22
|
+
"*": [".....", "..#..", "#.#.#", ".###.", "#.#.#", "..#..", "....."],
|
|
23
|
+
"+": [".....", "..#..", "..#..", "#####", "..#..", "..#..", "....."],
|
|
24
|
+
",": [".....", ".....", ".....", ".....", ".....", "..#..", ".#..."],
|
|
25
|
+
"-": [".....", ".....", ".....", "#####", ".....", ".....", "....."],
|
|
26
|
+
".": [".....", ".....", ".....", ".....", ".....", ".##..", ".##.."],
|
|
27
|
+
"/": ["....#", "....#", "...#.", "..#..", ".#...", "#....", "#...."],
|
|
28
|
+
"0": [".###.", "#...#", "#..##", "#.#.#", "##..#", "#...#", ".###."],
|
|
29
|
+
"1": ["..#..", ".##..", "..#..", "..#..", "..#..", "..#..", ".###."],
|
|
30
|
+
"2": [".###.", "#...#", "....#", "...#.", "..#..", ".#...", "#####"],
|
|
31
|
+
"3": [".###.", "#...#", "....#", "..##.", "....#", "#...#", ".###."],
|
|
32
|
+
"4": ["...#.", "..##.", ".#.#.", "#..#.", "#####", "...#.", "...#."],
|
|
33
|
+
"5": ["#####", "#....", "####.", "....#", "....#", "#...#", ".###."],
|
|
34
|
+
"6": [".###.", "#....", "#....", "####.", "#...#", "#...#", ".###."],
|
|
35
|
+
"7": ["#####", "....#", "...#.", "..#..", ".#...", ".#...", ".#..."],
|
|
36
|
+
"8": [".###.", "#...#", "#...#", ".###.", "#...#", "#...#", ".###."],
|
|
37
|
+
"9": [".###.", "#...#", "#...#", ".####", "....#", "....#", ".###."],
|
|
38
|
+
":": [".....", ".##..", ".##..", ".....", ".##..", ".##..", "....."],
|
|
39
|
+
";": [".....", ".##..", ".##..", ".....", ".##..", "..#..", ".#..."],
|
|
40
|
+
"<": ["...#.", "..#..", ".#...", "#....", ".#...", "..#..", "...#."],
|
|
41
|
+
"=": [".....", ".....", "#####", ".....", "#####", ".....", "....."],
|
|
42
|
+
">": [".#...", "..#..", "...#.", "....#", "...#.", "..#..", ".#..."],
|
|
43
|
+
"?": [".###.", "#...#", "....#", "...#.", "..#..", ".....", "..#.."],
|
|
44
|
+
"@": [".###.", "#...#", "#.###", "#.#.#", "#.###", "#....", ".###."],
|
|
45
|
+
A: [".###.", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"],
|
|
46
|
+
B: ["####.", "#...#", "#...#", "####.", "#...#", "#...#", "####."],
|
|
47
|
+
C: [".###.", "#...#", "#....", "#....", "#....", "#...#", ".###."],
|
|
48
|
+
D: ["####.", "#...#", "#...#", "#...#", "#...#", "#...#", "####."],
|
|
49
|
+
E: ["#####", "#....", "#....", "####.", "#....", "#....", "#####"],
|
|
50
|
+
F: ["#####", "#....", "#....", "####.", "#....", "#....", "#...."],
|
|
51
|
+
G: [".###.", "#...#", "#....", "#.###", "#...#", "#...#", ".###."],
|
|
52
|
+
H: ["#...#", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"],
|
|
53
|
+
I: [".###.", "..#..", "..#..", "..#..", "..#..", "..#..", ".###."],
|
|
54
|
+
J: ["..###", "...#.", "...#.", "...#.", "...#.", "#..#.", ".##.."],
|
|
55
|
+
K: ["#...#", "#..#.", "#.#..", "##...", "#.#..", "#..#.", "#...#"],
|
|
56
|
+
L: ["#....", "#....", "#....", "#....", "#....", "#....", "#####"],
|
|
57
|
+
M: ["#...#", "##.##", "#.#.#", "#.#.#", "#...#", "#...#", "#...#"],
|
|
58
|
+
N: ["#...#", "##..#", "#.#.#", "#..##", "#...#", "#...#", "#...#"],
|
|
59
|
+
O: [".###.", "#...#", "#...#", "#...#", "#...#", "#...#", ".###."],
|
|
60
|
+
P: ["####.", "#...#", "#...#", "####.", "#....", "#....", "#...."],
|
|
61
|
+
Q: [".###.", "#...#", "#...#", "#...#", "#.#.#", "#..#.", ".##.#"],
|
|
62
|
+
R: ["####.", "#...#", "#...#", "####.", "#.#..", "#..#.", "#...#"],
|
|
63
|
+
S: [".####", "#....", "#....", ".###.", "....#", "....#", "####."],
|
|
64
|
+
T: ["#####", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."],
|
|
65
|
+
U: ["#...#", "#...#", "#...#", "#...#", "#...#", "#...#", ".###."],
|
|
66
|
+
V: ["#...#", "#...#", "#...#", "#...#", "#...#", ".#.#.", "..#.."],
|
|
67
|
+
W: ["#...#", "#...#", "#...#", "#.#.#", "#.#.#", "##.##", "#...#"],
|
|
68
|
+
X: ["#...#", "#...#", ".#.#.", "..#..", ".#.#.", "#...#", "#...#"],
|
|
69
|
+
Y: ["#...#", "#...#", ".#.#.", "..#..", "..#..", "..#..", "..#.."],
|
|
70
|
+
Z: ["#####", "....#", "...#.", "..#..", ".#...", "#....", "#####"],
|
|
71
|
+
"[": [".###.", ".#...", ".#...", ".#...", ".#...", ".#...", ".###."],
|
|
72
|
+
"\\": ["#....", "#....", ".#...", "..#..", "...#.", "....#", "....#"],
|
|
73
|
+
"]": [".###.", "...#.", "...#.", "...#.", "...#.", "...#.", ".###."],
|
|
74
|
+
"^": ["..#..", ".#.#.", "#...#", ".....", ".....", ".....", "....."],
|
|
75
|
+
_: [".....", ".....", ".....", ".....", ".....", ".....", "#####"],
|
|
76
|
+
"`": [".#...", "..#..", ".....", ".....", ".....", ".....", "....."],
|
|
77
|
+
a: [".....", ".....", ".###.", "....#", ".####", "#...#", ".####"],
|
|
78
|
+
b: ["#....", "#....", "####.", "#...#", "#...#", "#...#", "####."],
|
|
79
|
+
c: [".....", ".....", ".###.", "#....", "#....", "#...#", ".###."],
|
|
80
|
+
d: ["....#", "....#", ".####", "#...#", "#...#", "#...#", ".####"],
|
|
81
|
+
e: [".....", ".....", ".###.", "#...#", "#####", "#....", ".###."],
|
|
82
|
+
f: ["..##.", ".#...", "####.", ".#...", ".#...", ".#...", ".#..."],
|
|
83
|
+
g: [".....", ".####", "#...#", "#...#", ".####", "....#", ".###."],
|
|
84
|
+
h: ["#....", "#....", "####.", "#...#", "#...#", "#...#", "#...#"],
|
|
85
|
+
i: ["..#..", ".....", ".##..", "..#..", "..#..", "..#..", ".###."],
|
|
86
|
+
j: ["...#.", ".....", "..##.", "...#.", "...#.", "#..#.", ".##.."],
|
|
87
|
+
k: ["#....", "#....", "#..#.", "#.#..", "##...", "#.#..", "#..#."],
|
|
88
|
+
l: [".##..", "..#..", "..#..", "..#..", "..#..", "..#..", ".###."],
|
|
89
|
+
m: [".....", ".....", "##.#.", "#.#.#", "#.#.#", "#.#.#", "#.#.#"],
|
|
90
|
+
n: [".....", ".....", "####.", "#...#", "#...#", "#...#", "#...#"],
|
|
91
|
+
o: [".....", ".....", ".###.", "#...#", "#...#", "#...#", ".###."],
|
|
92
|
+
p: [".....", "####.", "#...#", "#...#", "####.", "#....", "#...."],
|
|
93
|
+
q: [".....", ".####", "#...#", "#...#", ".####", "....#", "....#"],
|
|
94
|
+
r: [".....", ".....", "#.##.", "##...", "#....", "#....", "#...."],
|
|
95
|
+
s: [".....", ".....", ".####", "#....", ".###.", "....#", "####."],
|
|
96
|
+
t: [".#...", ".#...", "####.", ".#...", ".#...", ".#..#", "..##."],
|
|
97
|
+
u: [".....", ".....", "#...#", "#...#", "#...#", "#...#", ".####"],
|
|
98
|
+
v: [".....", ".....", "#...#", "#...#", "#...#", ".#.#.", "..#.."],
|
|
99
|
+
w: [".....", ".....", "#...#", "#.#.#", "#.#.#", "#.#.#", ".#.#."],
|
|
100
|
+
x: [".....", ".....", "#...#", ".#.#.", "..#..", ".#.#.", "#...#"],
|
|
101
|
+
y: [".....", "#...#", "#...#", "#...#", ".####", "....#", ".###."],
|
|
102
|
+
z: [".....", ".....", "#####", "...#.", "..#..", ".#...", "#####"],
|
|
103
|
+
"{": ["...##", "..#..", "..#..", ".#...", "..#..", "..#..", "...##"],
|
|
104
|
+
"|": ["..#..", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."],
|
|
105
|
+
"}": ["##...", "..#..", "..#..", "...#.", "..#..", "..#..", "##..."],
|
|
106
|
+
"~": [".....", ".....", ".#...", "#.#.#", "...#.", ".....", "....."],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const FALLBACK: GlyphRows = [
|
|
110
|
+
"#####",
|
|
111
|
+
"#...#",
|
|
112
|
+
"#...#",
|
|
113
|
+
"#...#",
|
|
114
|
+
"#...#",
|
|
115
|
+
"#...#",
|
|
116
|
+
"#####",
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
export function glyphRows(char: string): GlyphRows {
|
|
120
|
+
return G[char] ?? FALLBACK;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function hasGlyph(char: string): boolean {
|
|
124
|
+
return char in G;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function glyphNames(): string[] {
|
|
128
|
+
return Object.keys(G);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Horizontal runs of on-cells per glyph: [row, startCol, length][]. */
|
|
132
|
+
export function glyphRuns(char: string): Array<[number, number, number]> {
|
|
133
|
+
const cached = runCache.get(char);
|
|
134
|
+
if (cached) return cached;
|
|
135
|
+
const rows = glyphRows(char);
|
|
136
|
+
const runs: Array<[number, number, number]> = [];
|
|
137
|
+
for (let row = 0; row < GLYPH_H; row++) {
|
|
138
|
+
const line = rows[row]!;
|
|
139
|
+
let col = 0;
|
|
140
|
+
while (col < GLYPH_W) {
|
|
141
|
+
if (line[col] === "#") {
|
|
142
|
+
let len = 1;
|
|
143
|
+
while (col + len < GLYPH_W && line[col + len] === "#") len++;
|
|
144
|
+
runs.push([row, col, len]);
|
|
145
|
+
col += len;
|
|
146
|
+
} else {
|
|
147
|
+
col++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
runCache.set(char, runs);
|
|
152
|
+
return runs;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const runCache = new Map<string, Array<[number, number, number]>>();
|
|
156
|
+
|
|
157
|
+
/** Size of a single-line string at `size` px glyph height. */
|
|
158
|
+
export function measureText(
|
|
159
|
+
text: string,
|
|
160
|
+
size: number,
|
|
161
|
+
): { w: number; h: number } {
|
|
162
|
+
if (text.length === 0) return { w: 0, h: size };
|
|
163
|
+
const cell = size / GLYPH_H;
|
|
164
|
+
return {
|
|
165
|
+
w: (text.length * GLYPH_ADVANCE - 1) * cell,
|
|
166
|
+
h: size,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Hit testing over the laid-out tree. Later siblings render on top, so the
|
|
2
|
+
// deepest match in the last matching branch wins. Pure — testable without a
|
|
3
|
+
// GPU or window.
|
|
4
|
+
|
|
5
|
+
import { Prop, type UiTree } from "./tree";
|
|
6
|
+
|
|
7
|
+
function contains(tree: UiTree, id: number, x: number, y: number): boolean {
|
|
8
|
+
const node = tree.get(id);
|
|
9
|
+
return x >= node.x && y >= node.y && x < node.x + node.w && y < node.y + node.h;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Deepest node containing the point, regardless of hittability. */
|
|
13
|
+
function deepestAt(tree: UiTree, id: number, x: number, y: number): number {
|
|
14
|
+
if (!contains(tree, id, x, y)) return 0;
|
|
15
|
+
const children = tree.childrenOf(id);
|
|
16
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
17
|
+
const hit = deepestAt(tree, children[i]!, x, y);
|
|
18
|
+
if (hit !== 0) return hit;
|
|
19
|
+
}
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hittable nodes under the point, innermost first — the dispatch chain for
|
|
25
|
+
* bubbling. Empty when nothing hittable is under the point.
|
|
26
|
+
*/
|
|
27
|
+
export function hitChain(tree: UiTree, x: number, y: number): number[] {
|
|
28
|
+
const deepest = deepestAt(tree, tree.root, x, y);
|
|
29
|
+
const chain: number[] = [];
|
|
30
|
+
for (let id = deepest; id !== 0; id = tree.parentOf(id)) {
|
|
31
|
+
if (tree.getProp(id, Prop.Hittable) === 1) chain.push(id);
|
|
32
|
+
}
|
|
33
|
+
return chain;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Innermost scroll container (Overflow=1) under the point, or 0. */
|
|
37
|
+
export function scrollTargetAt(tree: UiTree, x: number, y: number): number {
|
|
38
|
+
for (
|
|
39
|
+
let id = deepestAt(tree, tree.root, x, y);
|
|
40
|
+
id !== 0;
|
|
41
|
+
id = tree.parentOf(id)
|
|
42
|
+
) {
|
|
43
|
+
if (tree.getProp(id, Prop.Overflow) === 1) return id;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Public surface of the prototype UI runtime.
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
batch,
|
|
5
|
+
cleanup,
|
|
6
|
+
createRoot,
|
|
7
|
+
inert,
|
|
8
|
+
isLive,
|
|
9
|
+
live,
|
|
10
|
+
memo,
|
|
11
|
+
setDevMode,
|
|
12
|
+
signal,
|
|
13
|
+
store,
|
|
14
|
+
type Accessor,
|
|
15
|
+
type LiveBinding,
|
|
16
|
+
type Reactive,
|
|
17
|
+
type Setter,
|
|
18
|
+
type StoreSetter,
|
|
19
|
+
} from "./reactive";
|
|
20
|
+
export { AUTO, NodeKind, Prop, UiTree } from "./tree";
|
|
21
|
+
export { Align, Justify, computeLayout } from "./layout";
|
|
22
|
+
export { FLOATS_PER_INSTANCE, paint, parseColor } from "./paint";
|
|
23
|
+
export { hitChain } from "./hit";
|
|
24
|
+
export { measureText } from "./font";
|
|
25
|
+
export {
|
|
26
|
+
createUiContext,
|
|
27
|
+
getUiContext,
|
|
28
|
+
onKey,
|
|
29
|
+
read,
|
|
30
|
+
ui,
|
|
31
|
+
withUiContext,
|
|
32
|
+
type AnchorRect,
|
|
33
|
+
type BoxProps,
|
|
34
|
+
type KeyEventInfo,
|
|
35
|
+
type PointerEventInfo,
|
|
36
|
+
type TextProps,
|
|
37
|
+
type UiContext,
|
|
38
|
+
} from "./ui";
|
|
39
|
+
export {
|
|
40
|
+
webview,
|
|
41
|
+
wgpuSurface,
|
|
42
|
+
type WebviewElementProps,
|
|
43
|
+
type WgpuSurfaceProps,
|
|
44
|
+
} from "./elements";
|
|
45
|
+
export { applyEditKey, charForKey, Key, Mod } from "./keymap";
|
|
46
|
+
export { textInput, type TextInputProps } from "./textInput";
|
|
47
|
+
export {
|
|
48
|
+
createUIView,
|
|
49
|
+
createUIWindow,
|
|
50
|
+
type UIView,
|
|
51
|
+
type UIWindow,
|
|
52
|
+
type UIWindowOptions,
|
|
53
|
+
} from "./uiwindow";
|
|
54
|
+
export { registerUIRoot, type UIRootRegistration } from "./uiTagHost";
|
|
55
|
+
export {
|
|
56
|
+
For,
|
|
57
|
+
Fragment,
|
|
58
|
+
Match,
|
|
59
|
+
Show,
|
|
60
|
+
Switch,
|
|
61
|
+
isUIElement,
|
|
62
|
+
jsx,
|
|
63
|
+
jsxs,
|
|
64
|
+
mountChild,
|
|
65
|
+
type ForProps,
|
|
66
|
+
type MatchProps,
|
|
67
|
+
type ShowProps,
|
|
68
|
+
type SwitchProps,
|
|
69
|
+
type UIChild,
|
|
70
|
+
type UIElement,
|
|
71
|
+
} from "./jsx-runtime";
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Input for the UI runtime. Preferred path: native pointer events from the
|
|
2
|
+
// render target's WGPUView (move/down/up/wheel/enter/exit with view-local,
|
|
3
|
+
// top-left coordinates) — no polling, no focus gating, wheel scrolling.
|
|
4
|
+
// Fallback path (older native binaries / platforms without the handler):
|
|
5
|
+
// poll the cursor and global button bitmask as before.
|
|
6
|
+
//
|
|
7
|
+
// Keyboard arrives via native window key events in both paths.
|
|
8
|
+
|
|
9
|
+
import { enableWGPUKeyEvents, enableWGPUPointerEvents, ffi, Screen } from "../proc/native";
|
|
10
|
+
import electrobunEventEmitter from "../events/eventEmitter";
|
|
11
|
+
import type { KeyEventInfo } from "./ui";
|
|
12
|
+
|
|
13
|
+
export interface InputSink {
|
|
14
|
+
/** Hittable chain under the point, innermost first. */
|
|
15
|
+
hitChain(x: number, y: number): number[];
|
|
16
|
+
dispatchPointer(
|
|
17
|
+
type: "click" | "down" | "up" | "enter" | "leave",
|
|
18
|
+
target: number,
|
|
19
|
+
x: number,
|
|
20
|
+
y: number,
|
|
21
|
+
): void;
|
|
22
|
+
dispatchWheel(x: number, y: number, dx: number, dy: number): void;
|
|
23
|
+
dispatchKey(e: KeyEventInfo): void;
|
|
24
|
+
/** True when pressing this node should drag the host window. */
|
|
25
|
+
isDragHandle(id: number): boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface InputDriver {
|
|
29
|
+
poll(): void;
|
|
30
|
+
dispose(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface DragState {
|
|
34
|
+
startX: number;
|
|
35
|
+
startY: number;
|
|
36
|
+
frameX: number;
|
|
37
|
+
frameY: number;
|
|
38
|
+
targetId: number;
|
|
39
|
+
moved: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function attachInput(
|
|
43
|
+
windowId: number,
|
|
44
|
+
viewId: number,
|
|
45
|
+
viewOffset: () => { x: number; y: number },
|
|
46
|
+
sink: InputSink,
|
|
47
|
+
): InputDriver {
|
|
48
|
+
let hoverId = 0;
|
|
49
|
+
let downId = 0;
|
|
50
|
+
let drag: DragState | null = null;
|
|
51
|
+
|
|
52
|
+
const syncHover = (x: number, y: number): number => {
|
|
53
|
+
const top = sink.hitChain(x, y)[0] ?? 0;
|
|
54
|
+
if (top !== hoverId) {
|
|
55
|
+
if (hoverId !== 0) sink.dispatchPointer("leave", hoverId, x, y);
|
|
56
|
+
if (top !== 0) sink.dispatchPointer("enter", top, x, y);
|
|
57
|
+
hoverId = top;
|
|
58
|
+
}
|
|
59
|
+
return top;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const clearHover = () => {
|
|
63
|
+
if (hoverId !== 0) {
|
|
64
|
+
sink.dispatchPointer("leave", hoverId, -1, -1);
|
|
65
|
+
hoverId = 0;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const beginDragIfHandle = (top: number) => {
|
|
70
|
+
if (top !== 0 && sink.isDragHandle(top)) {
|
|
71
|
+
const point = Screen.getCursorScreenPoint();
|
|
72
|
+
const frame = ffi.request.getWindowFrame({ winId: windowId });
|
|
73
|
+
drag = {
|
|
74
|
+
startX: point.x,
|
|
75
|
+
startY: point.y,
|
|
76
|
+
frameX: frame.x,
|
|
77
|
+
frameY: frame.y,
|
|
78
|
+
targetId: top,
|
|
79
|
+
moved: false,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const dragMove = () => {
|
|
85
|
+
if (!drag) return;
|
|
86
|
+
const point = Screen.getCursorScreenPoint();
|
|
87
|
+
const dx = point.x - drag.startX;
|
|
88
|
+
const dy = point.y - drag.startY;
|
|
89
|
+
if (Math.abs(dx) + Math.abs(dy) > 3) drag.moved = true;
|
|
90
|
+
if (drag.moved) {
|
|
91
|
+
ffi.request.setWindowPosition({
|
|
92
|
+
winId: windowId,
|
|
93
|
+
x: drag.frameX + dx,
|
|
94
|
+
y: drag.frameY + dy,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const endPress = (top: number, x: number, y: number) => {
|
|
100
|
+
if (drag) {
|
|
101
|
+
sink.dispatchPointer("up", drag.targetId, x, y);
|
|
102
|
+
if (!drag.moved) sink.dispatchPointer("click", drag.targetId, x, y);
|
|
103
|
+
drag = null;
|
|
104
|
+
downId = 0;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (top !== 0) sink.dispatchPointer("up", top, x, y);
|
|
108
|
+
if (downId !== 0 && downId !== top) {
|
|
109
|
+
// Released outside the pressed node: let it reset state.
|
|
110
|
+
sink.dispatchPointer("up", downId, x, y);
|
|
111
|
+
}
|
|
112
|
+
if (downId !== 0 && downId === top) {
|
|
113
|
+
sink.dispatchPointer("click", top, x, y);
|
|
114
|
+
}
|
|
115
|
+
downId = 0;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ---- Keyboard (both paths) ----
|
|
119
|
+
|
|
120
|
+
// Prefer view-level key events (they carry the layout's characters);
|
|
121
|
+
// fall back to window key events otherwise.
|
|
122
|
+
const hasNativeKeys = enableWGPUKeyEvents();
|
|
123
|
+
const onViewKey = (e: any) => {
|
|
124
|
+
if (!e?.isDown) return;
|
|
125
|
+
sink.dispatchKey({
|
|
126
|
+
keyCode: e.keyCode ?? 0,
|
|
127
|
+
modifiers: e.modifiers ?? 0,
|
|
128
|
+
isRepeat: Boolean(e.isRepeat),
|
|
129
|
+
chars: typeof e.chars === "string" ? e.chars : undefined,
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
const onKeyDown = (event: any) => {
|
|
133
|
+
const data = event?.data ?? {};
|
|
134
|
+
sink.dispatchKey({
|
|
135
|
+
keyCode: data.keyCode ?? 0,
|
|
136
|
+
modifiers: data.modifiers ?? 0,
|
|
137
|
+
isRepeat: Boolean(data.isRepeat),
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
if (hasNativeKeys) {
|
|
141
|
+
electrobunEventEmitter.on(`wgpu-key-${viewId}`, onViewKey);
|
|
142
|
+
} else {
|
|
143
|
+
electrobunEventEmitter.on(`keyDown-${windowId}`, onKeyDown);
|
|
144
|
+
}
|
|
145
|
+
const disposeKeys = () => {
|
|
146
|
+
if (hasNativeKeys) {
|
|
147
|
+
electrobunEventEmitter.off(`wgpu-key-${viewId}`, onViewKey);
|
|
148
|
+
} else {
|
|
149
|
+
electrobunEventEmitter.off(`keyDown-${windowId}`, onKeyDown);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// ---- Native pointer events path ----
|
|
154
|
+
|
|
155
|
+
if (enableWGPUPointerEvents()) {
|
|
156
|
+
const onPointer = (e: {
|
|
157
|
+
type: number;
|
|
158
|
+
x: number;
|
|
159
|
+
y: number;
|
|
160
|
+
buttonOrDx: number;
|
|
161
|
+
dy: number;
|
|
162
|
+
}) => {
|
|
163
|
+
const { x, y } = e;
|
|
164
|
+
switch (e.type) {
|
|
165
|
+
case 0: {
|
|
166
|
+
// move / drag-move
|
|
167
|
+
if (drag) {
|
|
168
|
+
dragMove();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
syncHover(x, y);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 1: {
|
|
175
|
+
// down (left button only participates in UI presses)
|
|
176
|
+
if (e.buttonOrDx !== 0) return;
|
|
177
|
+
const top = syncHover(x, y);
|
|
178
|
+
downId = top;
|
|
179
|
+
sink.dispatchPointer("down", top, x, y);
|
|
180
|
+
beginDragIfHandle(top);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case 2: {
|
|
184
|
+
if (e.buttonOrDx !== 0) return;
|
|
185
|
+
const top = drag ? drag.targetId : syncHover(x, y);
|
|
186
|
+
endPress(top, x, y);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case 3: {
|
|
190
|
+
sink.dispatchWheel(x, y, e.buttonOrDx, e.dy);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 5: {
|
|
194
|
+
clearHover();
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
electrobunEventEmitter.on(`wgpu-pointer-${viewId}`, onPointer);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
poll() {
|
|
203
|
+
// Event-driven: nothing to poll.
|
|
204
|
+
},
|
|
205
|
+
dispose() {
|
|
206
|
+
electrobunEventEmitter.off(`wgpu-pointer-${viewId}`, onPointer);
|
|
207
|
+
disposeKeys();
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---- Polling fallback ----
|
|
213
|
+
|
|
214
|
+
let focused = true;
|
|
215
|
+
let wasDown = false;
|
|
216
|
+
|
|
217
|
+
const onFocus = () => {
|
|
218
|
+
focused = true;
|
|
219
|
+
};
|
|
220
|
+
const onBlur = () => {
|
|
221
|
+
focused = false;
|
|
222
|
+
clearHover();
|
|
223
|
+
wasDown = false;
|
|
224
|
+
downId = 0;
|
|
225
|
+
};
|
|
226
|
+
electrobunEventEmitter.on(`focus-${windowId}`, onFocus);
|
|
227
|
+
electrobunEventEmitter.on(`blur-${windowId}`, onBlur);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
poll() {
|
|
231
|
+
const frame = ffi.request.getWindowFrame({ winId: windowId });
|
|
232
|
+
const offset = viewOffset();
|
|
233
|
+
const point = Screen.getCursorScreenPoint();
|
|
234
|
+
const x = point.x - frame.x - offset.x;
|
|
235
|
+
const y = point.y - frame.y - offset.y;
|
|
236
|
+
|
|
237
|
+
if (drag) {
|
|
238
|
+
const stillDown =
|
|
239
|
+
focused && (Number(Screen.getMouseButtons()) & 1) === 1;
|
|
240
|
+
if (stillDown) dragMove();
|
|
241
|
+
else endPress(drag.targetId, x, y);
|
|
242
|
+
wasDown = stillDown;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const top = syncHover(x, y);
|
|
247
|
+
|
|
248
|
+
// Gate button state on focus so clicks on other apps or windows
|
|
249
|
+
// stacked above ours don't register.
|
|
250
|
+
const isDown =
|
|
251
|
+
focused && (Number(Screen.getMouseButtons()) & 1) === 1;
|
|
252
|
+
if (isDown && !wasDown) {
|
|
253
|
+
downId = top;
|
|
254
|
+
// Dispatched even for target 0: a background press blurs focus.
|
|
255
|
+
sink.dispatchPointer("down", top, x, y);
|
|
256
|
+
beginDragIfHandle(top);
|
|
257
|
+
} else if (!isDown && wasDown) {
|
|
258
|
+
endPress(top, x, y);
|
|
259
|
+
}
|
|
260
|
+
wasDown = isDown;
|
|
261
|
+
},
|
|
262
|
+
dispose() {
|
|
263
|
+
electrobunEventEmitter.off(`focus-${windowId}`, onFocus);
|
|
264
|
+
electrobunEventEmitter.off(`blur-${windowId}`, onBlur);
|
|
265
|
+
disposeKeys();
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Dev-transform entrypoint: same runtime, jsxDEV signature.
|
|
2
|
+
import { Fragment, jsx, type UIElement } from "./jsx-runtime";
|
|
3
|
+
|
|
4
|
+
export { Fragment };
|
|
5
|
+
export type { JSX, UIChild, UIElement } from "./jsx-runtime";
|
|
6
|
+
|
|
7
|
+
export function jsxDEV(
|
|
8
|
+
type: Parameters<typeof jsx>[0],
|
|
9
|
+
props: Parameters<typeof jsx>[1],
|
|
10
|
+
key?: unknown,
|
|
11
|
+
_isStaticChildren?: boolean,
|
|
12
|
+
_source?: unknown,
|
|
13
|
+
_self?: unknown,
|
|
14
|
+
): UIElement {
|
|
15
|
+
return jsx(type, props, key);
|
|
16
|
+
}
|