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,159 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { NodeKind, Prop, UiTree } from "../tree";
|
|
3
|
+
import { Align, Justify, computeLayout } from "../layout";
|
|
4
|
+
import { measureText } from "../font";
|
|
5
|
+
|
|
6
|
+
function boxNode(
|
|
7
|
+
tree: UiTree,
|
|
8
|
+
parent: number,
|
|
9
|
+
props: Partial<Record<Prop, number>> = {},
|
|
10
|
+
): number {
|
|
11
|
+
const id = tree.createNode(NodeKind.Box);
|
|
12
|
+
for (const [prop, value] of Object.entries(props)) {
|
|
13
|
+
tree.setProp(id, Number(prop) as Prop, value as number);
|
|
14
|
+
}
|
|
15
|
+
tree.append(parent, id);
|
|
16
|
+
return id;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("layout", () => {
|
|
20
|
+
test("column stacks children with gap and padding", () => {
|
|
21
|
+
const tree = new UiTree();
|
|
22
|
+
tree.setProp(tree.root, Prop.Dir, 1);
|
|
23
|
+
tree.setProp(tree.root, Prop.Pad, 10);
|
|
24
|
+
tree.setProp(tree.root, Prop.Gap, 5);
|
|
25
|
+
const a = boxNode(tree, tree.root, {
|
|
26
|
+
[Prop.Width]: 50,
|
|
27
|
+
[Prop.Height]: 20,
|
|
28
|
+
});
|
|
29
|
+
const b = boxNode(tree, tree.root, {
|
|
30
|
+
[Prop.Width]: 50,
|
|
31
|
+
[Prop.Height]: 30,
|
|
32
|
+
});
|
|
33
|
+
computeLayout(tree, 200, 200);
|
|
34
|
+
expect([tree.get(a).x, tree.get(a).y]).toEqual([10, 10]);
|
|
35
|
+
expect([tree.get(b).x, tree.get(b).y]).toEqual([10, 35]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("row grow distributes leftover space by weight", () => {
|
|
39
|
+
const tree = new UiTree();
|
|
40
|
+
const fixed = boxNode(tree, tree.root, {
|
|
41
|
+
[Prop.Width]: 100,
|
|
42
|
+
[Prop.Height]: 10,
|
|
43
|
+
});
|
|
44
|
+
const growA = boxNode(tree, tree.root, {
|
|
45
|
+
[Prop.Grow]: 1,
|
|
46
|
+
[Prop.Height]: 10,
|
|
47
|
+
});
|
|
48
|
+
const growB = boxNode(tree, tree.root, {
|
|
49
|
+
[Prop.Grow]: 3,
|
|
50
|
+
[Prop.Height]: 10,
|
|
51
|
+
});
|
|
52
|
+
computeLayout(tree, 500, 100);
|
|
53
|
+
expect(tree.get(fixed).w).toBe(100);
|
|
54
|
+
expect(tree.get(growA).w).toBe(100);
|
|
55
|
+
expect(tree.get(growB).w).toBe(300);
|
|
56
|
+
expect(tree.get(growB).x).toBe(200);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("justify center offsets children along the main axis", () => {
|
|
60
|
+
const tree = new UiTree();
|
|
61
|
+
tree.setProp(tree.root, Prop.Justify, Justify.Center);
|
|
62
|
+
const child = boxNode(tree, tree.root, {
|
|
63
|
+
[Prop.Width]: 100,
|
|
64
|
+
[Prop.Height]: 50,
|
|
65
|
+
});
|
|
66
|
+
computeLayout(tree, 300, 100);
|
|
67
|
+
expect(tree.get(child).x).toBe(100);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("justify space-between spreads children", () => {
|
|
71
|
+
const tree = new UiTree();
|
|
72
|
+
tree.setProp(tree.root, Prop.Justify, Justify.SpaceBetween);
|
|
73
|
+
const a = boxNode(tree, tree.root, {
|
|
74
|
+
[Prop.Width]: 50,
|
|
75
|
+
[Prop.Height]: 10,
|
|
76
|
+
});
|
|
77
|
+
const b = boxNode(tree, tree.root, {
|
|
78
|
+
[Prop.Width]: 50,
|
|
79
|
+
[Prop.Height]: 10,
|
|
80
|
+
});
|
|
81
|
+
computeLayout(tree, 300, 100);
|
|
82
|
+
expect(tree.get(a).x).toBe(0);
|
|
83
|
+
expect(tree.get(b).x).toBe(250);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("align stretch fills the cross axis inside padding", () => {
|
|
87
|
+
const tree = new UiTree();
|
|
88
|
+
tree.setProp(tree.root, Prop.Align, Align.Stretch);
|
|
89
|
+
tree.setProp(tree.root, Prop.Pad, 8);
|
|
90
|
+
const child = boxNode(tree, tree.root, {
|
|
91
|
+
[Prop.Width]: 40,
|
|
92
|
+
});
|
|
93
|
+
computeLayout(tree, 200, 100);
|
|
94
|
+
expect(tree.get(child).h).toBe(84);
|
|
95
|
+
expect(tree.get(child).y).toBe(8);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("align center positions on the cross axis", () => {
|
|
99
|
+
const tree = new UiTree();
|
|
100
|
+
tree.setProp(tree.root, Prop.Align, Align.Center);
|
|
101
|
+
const child = boxNode(tree, tree.root, {
|
|
102
|
+
[Prop.Width]: 40,
|
|
103
|
+
[Prop.Height]: 40,
|
|
104
|
+
});
|
|
105
|
+
computeLayout(tree, 200, 100);
|
|
106
|
+
expect(tree.get(child).y).toBe(30);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("auto box wraps its content plus padding (align start)", () => {
|
|
110
|
+
const tree = new UiTree();
|
|
111
|
+
tree.setProp(tree.root, Prop.Align, Align.Start);
|
|
112
|
+
const wrapper = boxNode(tree, tree.root, { [Prop.Pad]: 6 });
|
|
113
|
+
boxNode(tree, wrapper, { [Prop.Width]: 30, [Prop.Height]: 20 });
|
|
114
|
+
computeLayout(tree, 400, 400);
|
|
115
|
+
expect(tree.get(wrapper).w).toBe(42);
|
|
116
|
+
expect(tree.get(wrapper).h).toBe(32);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("children stretch on the cross axis by default (flexbox rule)", () => {
|
|
120
|
+
const tree = new UiTree();
|
|
121
|
+
// Row: auto-height box stretches to the container's height...
|
|
122
|
+
const auto = boxNode(tree, tree.root, { [Prop.Width]: 40 });
|
|
123
|
+
// ...a fixed cross size wins over stretch...
|
|
124
|
+
const fixed = boxNode(tree, tree.root, {
|
|
125
|
+
[Prop.Width]: 40,
|
|
126
|
+
[Prop.Height]: 25,
|
|
127
|
+
});
|
|
128
|
+
// ...and text keeps its intrinsic size.
|
|
129
|
+
const label = tree.createTextNode("hi");
|
|
130
|
+
tree.append(tree.root, label);
|
|
131
|
+
computeLayout(tree, 400, 300);
|
|
132
|
+
expect(tree.get(auto).h).toBe(300);
|
|
133
|
+
expect(tree.get(fixed).h).toBe(25);
|
|
134
|
+
expect(tree.get(label).h).toBe(14);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("text nodes measure from the bitmap font", () => {
|
|
138
|
+
const tree = new UiTree();
|
|
139
|
+
const label = tree.createTextNode("hello");
|
|
140
|
+
tree.setProp(label, Prop.FontSize, 14);
|
|
141
|
+
tree.append(tree.root, label);
|
|
142
|
+
computeLayout(tree, 400, 400);
|
|
143
|
+
const expected = measureText("hello", 14);
|
|
144
|
+
expect(tree.get(label).w).toBeCloseTo(expected.w);
|
|
145
|
+
expect(tree.get(label).h).toBe(14);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("anchor nodes occupy layout space", () => {
|
|
149
|
+
const tree = new UiTree();
|
|
150
|
+
tree.setProp(tree.root, Prop.Dir, 1);
|
|
151
|
+
boxNode(tree, tree.root, { [Prop.Height]: 40, [Prop.Width]: 40 });
|
|
152
|
+
const anchor = tree.createNode(NodeKind.Anchor);
|
|
153
|
+
tree.setProp(anchor, Prop.Grow, 1);
|
|
154
|
+
tree.append(tree.root, anchor);
|
|
155
|
+
computeLayout(tree, 300, 200);
|
|
156
|
+
expect(tree.get(anchor).y).toBe(40);
|
|
157
|
+
expect(tree.get(anchor).h).toBe(160);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { NodeKind, Prop, UiTree } from "../tree";
|
|
3
|
+
import { computeLayout } from "../layout";
|
|
4
|
+
import { FLOATS_PER_INSTANCE, paint, parseColor } from "../paint";
|
|
5
|
+
import { glyphRuns } from "../font";
|
|
6
|
+
|
|
7
|
+
function instance(data: Float32Array, i: number): number[] {
|
|
8
|
+
return Array.from(data.subarray(i * FLOATS_PER_INSTANCE, (i + 1) * FLOATS_PER_INSTANCE));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("parseColor", () => {
|
|
12
|
+
test("parses #rrggbb with implicit opaque alpha", () => {
|
|
13
|
+
expect(parseColor("#ff8000")).toBe(0xff8000ff);
|
|
14
|
+
});
|
|
15
|
+
test("parses #rgb shorthand", () => {
|
|
16
|
+
expect(parseColor("#fff")).toBe(0xffffffff);
|
|
17
|
+
});
|
|
18
|
+
test("parses #rrggbbaa", () => {
|
|
19
|
+
expect(parseColor("#11223344")).toBe(0x11223344);
|
|
20
|
+
});
|
|
21
|
+
test("passes numbers through", () => {
|
|
22
|
+
expect(parseColor(0x12345678)).toBe(0x12345678);
|
|
23
|
+
});
|
|
24
|
+
test("rejects malformed colors", () => {
|
|
25
|
+
expect(() => parseColor("#12345")).toThrow();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("paint", () => {
|
|
30
|
+
test("boxes with background emit one rounded rect", () => {
|
|
31
|
+
const tree = new UiTree();
|
|
32
|
+
const box = tree.createNode(NodeKind.Box);
|
|
33
|
+
tree.setProp(box, Prop.Width, 100);
|
|
34
|
+
tree.setProp(box, Prop.Height, 40);
|
|
35
|
+
tree.setProp(box, Prop.Bg, parseColor("#ff0000"));
|
|
36
|
+
tree.setProp(box, Prop.Radius, 6);
|
|
37
|
+
tree.append(tree.root, box);
|
|
38
|
+
computeLayout(tree, 200, 200);
|
|
39
|
+
const buffer = paint(tree);
|
|
40
|
+
expect(buffer.count).toBe(1);
|
|
41
|
+
const [x, y, w, h, r, g, b, a, radius] = instance(buffer.data, 0);
|
|
42
|
+
expect([x, y, w, h]).toEqual([0, 0, 100, 40]);
|
|
43
|
+
expect([r, g, b, a]).toEqual([1, 0, 0, 1]);
|
|
44
|
+
expect(radius).toBe(6);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("transparent boxes emit nothing", () => {
|
|
48
|
+
const tree = new UiTree();
|
|
49
|
+
const box = tree.createNode(NodeKind.Box);
|
|
50
|
+
tree.setProp(box, Prop.Width, 10);
|
|
51
|
+
tree.setProp(box, Prop.Height, 10);
|
|
52
|
+
tree.append(tree.root, box);
|
|
53
|
+
computeLayout(tree, 100, 100);
|
|
54
|
+
expect(paint(tree).count).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("border emits outer border rect then inset background", () => {
|
|
58
|
+
const tree = new UiTree();
|
|
59
|
+
const box = tree.createNode(NodeKind.Box);
|
|
60
|
+
tree.setProp(box, Prop.Width, 50);
|
|
61
|
+
tree.setProp(box, Prop.Height, 30);
|
|
62
|
+
tree.setProp(box, Prop.Bg, parseColor("#000000"));
|
|
63
|
+
tree.setProp(box, Prop.BorderWidth, 2);
|
|
64
|
+
tree.setProp(box, Prop.BorderColor, parseColor("#00ff00"));
|
|
65
|
+
tree.setProp(box, Prop.Radius, 5);
|
|
66
|
+
tree.append(tree.root, box);
|
|
67
|
+
computeLayout(tree, 100, 100);
|
|
68
|
+
const buffer = paint(tree);
|
|
69
|
+
expect(buffer.count).toBe(2);
|
|
70
|
+
const border = instance(buffer.data, 0);
|
|
71
|
+
const bg = instance(buffer.data, 1);
|
|
72
|
+
expect(border.slice(0, 4)).toEqual([0, 0, 50, 30]);
|
|
73
|
+
expect(bg.slice(0, 4)).toEqual([2, 2, 46, 26]);
|
|
74
|
+
expect(bg[8]).toBe(3); // radius shrinks by border width
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("parents paint before children (painter's order)", () => {
|
|
78
|
+
const tree = new UiTree();
|
|
79
|
+
const parent = tree.createNode(NodeKind.Box);
|
|
80
|
+
tree.setProp(parent, Prop.Width, 100);
|
|
81
|
+
tree.setProp(parent, Prop.Height, 100);
|
|
82
|
+
tree.setProp(parent, Prop.Bg, parseColor("#111111"));
|
|
83
|
+
tree.append(tree.root, parent);
|
|
84
|
+
const child = tree.createNode(NodeKind.Box);
|
|
85
|
+
tree.setProp(child, Prop.Width, 10);
|
|
86
|
+
tree.setProp(child, Prop.Height, 10);
|
|
87
|
+
tree.setProp(child, Prop.Bg, parseColor("#eeeeee"));
|
|
88
|
+
tree.append(parent, child);
|
|
89
|
+
computeLayout(tree, 100, 100);
|
|
90
|
+
const buffer = paint(tree);
|
|
91
|
+
expect(buffer.count).toBe(2);
|
|
92
|
+
expect(instance(buffer.data, 0)[2]).toBe(100); // parent first
|
|
93
|
+
expect(instance(buffer.data, 1)[2]).toBe(10);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("text emits one instance per glyph run", () => {
|
|
97
|
+
const tree = new UiTree();
|
|
98
|
+
const label = tree.createTextNode("II");
|
|
99
|
+
tree.setProp(label, Prop.FontSize, 14);
|
|
100
|
+
tree.append(tree.root, label);
|
|
101
|
+
computeLayout(tree, 100, 100);
|
|
102
|
+
const runsPerGlyph = glyphRuns("I").length;
|
|
103
|
+
expect(paint(tree).count).toBe(runsPerGlyph * 2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("anchor nodes paint nothing", () => {
|
|
107
|
+
const tree = new UiTree();
|
|
108
|
+
const anchor = tree.createNode(NodeKind.Anchor);
|
|
109
|
+
tree.setProp(anchor, Prop.Width, 50);
|
|
110
|
+
tree.setProp(anchor, Prop.Height, 50);
|
|
111
|
+
tree.append(tree.root, anchor);
|
|
112
|
+
computeLayout(tree, 100, 100);
|
|
113
|
+
expect(paint(tree).count).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
});
|