@aethervtt/shard-ui 0.0.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/components.d.ts +144 -0
- package/dist/components.js +355 -0
- package/dist/flex.d.ts +107 -0
- package/dist/flex.js +751 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +133 -0
- package/dist/interaction.d.ts +44 -0
- package/dist/interaction.js +416 -0
- package/dist/length.d.ts +17 -0
- package/dist/length.js +92 -0
- package/dist/methods.d.ts +52 -0
- package/dist/methods.js +187 -0
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.js +40 -0
- package/dist/render.d.ts +76 -0
- package/dist/render.js +715 -0
- package/dist/shaders.d.ts +2 -0
- package/dist/shaders.js +124 -0
- package/dist/style.d.ts +45 -0
- package/dist/style.js +111 -0
- package/dist/testing.d.ts +20 -0
- package/dist/testing.js +68 -0
- package/dist/theme.d.ts +30 -0
- package/dist/theme.js +172 -0
- package/dist/tree.d.ts +172 -0
- package/dist/tree.js +1109 -0
- package/package.json +42 -0
package/dist/length.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ShardError
|
|
3
|
+
} from "@aethervtt/shard-core";
|
|
4
|
+
const LengthUnit = { Px: 0, Percent: 1, Auto: 2 };
|
|
5
|
+
const PATTERN = /^\s*(-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)\s*(px|%)?\s*$/i;
|
|
6
|
+
function parseLength(json, out = [0, 0]) {
|
|
7
|
+
if (typeof json === "number") {
|
|
8
|
+
if (!Number.isFinite(json)) return void 0;
|
|
9
|
+
out[0] = json;
|
|
10
|
+
out[1] = LengthUnit.Px;
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
if (typeof json !== "string") return void 0;
|
|
14
|
+
if (json.trim() === "auto") {
|
|
15
|
+
out[0] = 0;
|
|
16
|
+
out[1] = LengthUnit.Auto;
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
const m = PATTERN.exec(json);
|
|
20
|
+
if (!m) return void 0;
|
|
21
|
+
out[0] = Number(m[1]);
|
|
22
|
+
out[1] = m[2] === "%" ? LengthUnit.Percent : LengthUnit.Px;
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function invalid(path, got) {
|
|
26
|
+
return new ShardError(
|
|
27
|
+
"ui/invalid-length",
|
|
28
|
+
`${JSON.stringify(got)} at ${path || "/"} isn't a length`,
|
|
29
|
+
{
|
|
30
|
+
path,
|
|
31
|
+
hint: 'Use pixels (120 or "120"), a percent of the parent ("50%"), or "auto".'
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
function toValue(value, unit) {
|
|
36
|
+
if (unit === LengthUnit.Auto) return "auto";
|
|
37
|
+
const v = Number.isInteger(value) ? value : Number(value.toPrecision(7));
|
|
38
|
+
return unit === LengthUnit.Percent ? `${v}%` : v;
|
|
39
|
+
}
|
|
40
|
+
const scratch = [0, 0];
|
|
41
|
+
function uiLength(options = {}) {
|
|
42
|
+
const fallback = options.default ?? "auto";
|
|
43
|
+
const field = {
|
|
44
|
+
kind: "length",
|
|
45
|
+
storage: "f32",
|
|
46
|
+
stride: 2,
|
|
47
|
+
options,
|
|
48
|
+
defaultValue: () => fallback,
|
|
49
|
+
read: (c, r) => {
|
|
50
|
+
const col = c;
|
|
51
|
+
return toValue(col[r * 2], col[r * 2 + 1]);
|
|
52
|
+
},
|
|
53
|
+
write: (c, r, v) => {
|
|
54
|
+
const parsed = parseLength(v, scratch);
|
|
55
|
+
if (!parsed) throw invalid("", v);
|
|
56
|
+
const col = c;
|
|
57
|
+
col[r * 2] = parsed[0];
|
|
58
|
+
col[r * 2 + 1] = parsed[1];
|
|
59
|
+
},
|
|
60
|
+
validate(json, path, errors) {
|
|
61
|
+
if (!parseLength(json, scratch)) errors.push(invalid(path, json));
|
|
62
|
+
},
|
|
63
|
+
toJson: (v) => {
|
|
64
|
+
const parsed = parseLength(v, scratch);
|
|
65
|
+
return parsed ? toValue(parsed[0], parsed[1]) : "auto";
|
|
66
|
+
},
|
|
67
|
+
fromJson: (json) => {
|
|
68
|
+
const parsed = parseLength(json, scratch);
|
|
69
|
+
return parsed ? toValue(parsed[0], parsed[1]) : "auto";
|
|
70
|
+
},
|
|
71
|
+
jsonSchema: () => {
|
|
72
|
+
const schema = {
|
|
73
|
+
anyOf: [
|
|
74
|
+
{ type: "number" },
|
|
75
|
+
{ type: "string", pattern: "^\\s*-?(\\d+\\.?\\d*|\\.\\d+)\\s*(px|%)?\\s*$" },
|
|
76
|
+
{ const: "auto" }
|
|
77
|
+
],
|
|
78
|
+
default: fallback
|
|
79
|
+
};
|
|
80
|
+
if (options.description) schema.description = options.description;
|
|
81
|
+
if (options.readonly) schema.readOnly = true;
|
|
82
|
+
schema["x-unit"] = "px or %";
|
|
83
|
+
return schema;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
return field;
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
LengthUnit,
|
|
90
|
+
parseLength,
|
|
91
|
+
uiLength
|
|
92
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { AppMethod } from '@aethervtt/shard-runtime';
|
|
3
|
+
/** An entity's scene path, if it came from a scene. */
|
|
4
|
+
export declare function uiPath(world: World, entity: Entity): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* A UI node by id or scene path. A path may also be a suffix after "/" ("play" finds
|
|
7
|
+
* "hud/menu/play") when only one node matches.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveUiEntity(world: World, ref: unknown): Entity;
|
|
10
|
+
interface DescribedNode {
|
|
11
|
+
entity: Entity;
|
|
12
|
+
path: string | null;
|
|
13
|
+
rect: [number, number, number, number];
|
|
14
|
+
visible: boolean;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** Hooks other parts of the plugin add to describe (the renderer's upload counts). */
|
|
18
|
+
export declare const describeExtras: Map<string, (world: World) => unknown>;
|
|
19
|
+
/** Every UI tree as data: rects, text, widget state, focus, anchors, and this frame's work. */
|
|
20
|
+
export declare function describeUi(world: World, root?: Entity): {
|
|
21
|
+
roots: {
|
|
22
|
+
entity: number;
|
|
23
|
+
path: string | null;
|
|
24
|
+
size: number[];
|
|
25
|
+
scale: number;
|
|
26
|
+
viewport: number[];
|
|
27
|
+
camera: number | null;
|
|
28
|
+
nodes: number;
|
|
29
|
+
laidOutAt: number;
|
|
30
|
+
tree: DescribedNode;
|
|
31
|
+
}[];
|
|
32
|
+
focus: string | number | null;
|
|
33
|
+
hovered: string | number | null;
|
|
34
|
+
pointer: {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
overUi: boolean;
|
|
38
|
+
};
|
|
39
|
+
frame: {
|
|
40
|
+
layouts: number;
|
|
41
|
+
nodesLaidOut: number;
|
|
42
|
+
repositions: number;
|
|
43
|
+
flexCalls: number;
|
|
44
|
+
} & Record<string, unknown>;
|
|
45
|
+
problems: {
|
|
46
|
+
code: string;
|
|
47
|
+
entity: number;
|
|
48
|
+
path: string | null;
|
|
49
|
+
}[];
|
|
50
|
+
};
|
|
51
|
+
export declare const uiMethods: AppMethod[];
|
|
52
|
+
export {};
|
package/dist/methods.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineSchema,
|
|
3
|
+
findComponent,
|
|
4
|
+
ShardError,
|
|
5
|
+
t
|
|
6
|
+
} from "@aethervtt/shard-core";
|
|
7
|
+
import { Localized } from "@aethervtt/shard-text";
|
|
8
|
+
import {
|
|
9
|
+
UI_ANCHOR_STATES,
|
|
10
|
+
UI_STATES,
|
|
11
|
+
UiInteraction,
|
|
12
|
+
UiNode,
|
|
13
|
+
UiSlider,
|
|
14
|
+
UiText,
|
|
15
|
+
UiTextInput,
|
|
16
|
+
UiToggle
|
|
17
|
+
} from "./components.js";
|
|
18
|
+
import { clickUi, focusUi, UiPointer } from "./interaction.js";
|
|
19
|
+
import { ResolvedStyle, resolveStyle } from "./style.js";
|
|
20
|
+
import { Kind, textColumns, UiState } from "./tree.js";
|
|
21
|
+
const round = (x) => Math.round(x * 100) / 100;
|
|
22
|
+
const KINDS = ["", "button", "toggle", "slider", "text-input"];
|
|
23
|
+
function uiPath(world, entity) {
|
|
24
|
+
const member = findComponent("scene/SceneMember");
|
|
25
|
+
if (!member || !world.isAlive(entity)) return null;
|
|
26
|
+
return world.tryGet(entity, member)?.path || null;
|
|
27
|
+
}
|
|
28
|
+
function resolveUiEntity(world, ref) {
|
|
29
|
+
const store = world.resource(UiState);
|
|
30
|
+
if (typeof ref === "number" && world.isAlive(ref) && store.rootOf.has(ref)) return ref;
|
|
31
|
+
if (typeof ref === "string" && ref !== "") {
|
|
32
|
+
let exact;
|
|
33
|
+
const suffix = [];
|
|
34
|
+
for (const e of store.rootOf.keys()) {
|
|
35
|
+
const path = uiPath(world, e);
|
|
36
|
+
if (!path) continue;
|
|
37
|
+
if (path === ref || path.endsWith(`:${ref}`)) exact = e;
|
|
38
|
+
else if (path.endsWith(`/${ref}`)) suffix.push(e);
|
|
39
|
+
}
|
|
40
|
+
if (exact !== void 0) return exact;
|
|
41
|
+
if (suffix.length === 1) return suffix[0];
|
|
42
|
+
if (suffix.length > 1) {
|
|
43
|
+
throw new ShardError("ui/ambiguous-path", `"${ref}" matches ${suffix.length} UI nodes`, {
|
|
44
|
+
hint: `Use the full path: ${suffix.map((e) => uiPath(world, e)).join(", ")}.`
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new ShardError("ui/unknown-node", `No UI node ${JSON.stringify(ref)}`, {
|
|
49
|
+
hint: "Pass an entity id or a scene path of a node under a UiRoot (ui.describe lists them)."
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const describeStyle = new ResolvedStyle();
|
|
53
|
+
function describeNode(world, r, i, focused) {
|
|
54
|
+
const e = r.entities[i];
|
|
55
|
+
const table = world.entityTable(e);
|
|
56
|
+
const row = world.entityRow(e);
|
|
57
|
+
const out = {
|
|
58
|
+
entity: e,
|
|
59
|
+
path: uiPath(world, e),
|
|
60
|
+
rect: [round(r.x[i]), round(r.y[i]), round(r.w[i]), round(r.h[i])],
|
|
61
|
+
visible: r.visible[i] !== 0
|
|
62
|
+
};
|
|
63
|
+
const style = table.column(UiNode, "style")[row] ?? "";
|
|
64
|
+
if (style) {
|
|
65
|
+
out.style = style;
|
|
66
|
+
if (r.theme && !r.theme.styles.has(style))
|
|
67
|
+
out.problem = `ui/unknown-style: "${style}" isn't in the theme`;
|
|
68
|
+
}
|
|
69
|
+
const text = r.texts[i];
|
|
70
|
+
if (text) {
|
|
71
|
+
out.text = table.has(UiTextInput) ? table.column(UiTextInput, "value")[row] : table.has(Localized) ? table.column(Localized, "value")[row] : table.column(UiText, "text")[row];
|
|
72
|
+
const key = table.column(UiText, "key")[row];
|
|
73
|
+
if (key) {
|
|
74
|
+
out.key = key;
|
|
75
|
+
if (!table.has(Localized)) out.problem = `locale/missing-key: no string for "${key}"`;
|
|
76
|
+
}
|
|
77
|
+
resolveStyle(describeStyle, textColumns(table), row, r.theme, style, 0);
|
|
78
|
+
out.textSize = round(describeStyle.size);
|
|
79
|
+
if (!text.font) out.problem = "no font: set UiText.font, a theme font, or UiDefaults.font";
|
|
80
|
+
else if (text.layout.missing > 0) out.missingGlyphs = text.layout.missing;
|
|
81
|
+
}
|
|
82
|
+
const kind = r.kind[i];
|
|
83
|
+
if (kind !== Kind.None) {
|
|
84
|
+
out.widget = KINDS[kind];
|
|
85
|
+
const s = table.has(UiInteraction) ? table.column(UiInteraction, "state")[row] : 0;
|
|
86
|
+
out.state = UI_STATES[s];
|
|
87
|
+
if (focused === e) out.focused = true;
|
|
88
|
+
if (r.disabled[i]) out.disabled = true;
|
|
89
|
+
if (kind === Kind.Toggle) out.on = table.column(UiToggle, "on")[row] !== 0;
|
|
90
|
+
if (kind === Kind.Slider) out.value = round(table.column(UiSlider, "value")[row]);
|
|
91
|
+
}
|
|
92
|
+
if (r.anchorKind[i] === 1) {
|
|
93
|
+
out.anchor = UI_ANCHOR_STATES[r.anchorState[i]];
|
|
94
|
+
out.distance = round(r.distance[i]);
|
|
95
|
+
if (r.anchorScale[i] !== 1) out.scale = round(r.anchorScale[i]);
|
|
96
|
+
}
|
|
97
|
+
if (r.anchorKind[i] === 2) out.arrowAngle = round(r.angle[i]);
|
|
98
|
+
if (r.overflow[i] === 2) {
|
|
99
|
+
out.scroll = [round(r.scrollX[i]), round(r.scrollY[i])];
|
|
100
|
+
out.content = [round(r.contentW[i]), round(r.contentH[i])];
|
|
101
|
+
}
|
|
102
|
+
const children = [];
|
|
103
|
+
for (let c = i + 1; c < r.end[i]; c = r.end[c])
|
|
104
|
+
children.push(describeNode(world, r, c, focused));
|
|
105
|
+
if (children.length > 0) out.children = children;
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
const describeExtras = /* @__PURE__ */ new Map();
|
|
109
|
+
function describeUi(world, root) {
|
|
110
|
+
const store = world.resource(UiState);
|
|
111
|
+
const pointer = world.resource(UiPointer);
|
|
112
|
+
const roots = store.roots.filter((r) => root === void 0 || r.entity === root).map((r) => ({
|
|
113
|
+
entity: r.entity,
|
|
114
|
+
path: uiPath(world, r.entity),
|
|
115
|
+
size: [round(r.width), round(r.height)],
|
|
116
|
+
scale: round(r.factor),
|
|
117
|
+
viewport: [r.viewportW, r.viewportH],
|
|
118
|
+
camera: r.camera,
|
|
119
|
+
nodes: r.count,
|
|
120
|
+
laidOutAt: r.laidOutAt,
|
|
121
|
+
tree: describeNode(world, r, 0, pointer.focused)
|
|
122
|
+
}));
|
|
123
|
+
const extras = {};
|
|
124
|
+
for (const [name, fn] of describeExtras) extras[name] = fn(world);
|
|
125
|
+
return {
|
|
126
|
+
roots,
|
|
127
|
+
focus: pointer.focused === null ? null : uiPath(world, pointer.focused) ?? pointer.focused,
|
|
128
|
+
hovered: pointer.hovered === null ? null : uiPath(world, pointer.hovered) ?? pointer.hovered,
|
|
129
|
+
pointer: { x: round(pointer.x), y: round(pointer.y), overUi: pointer.overUi },
|
|
130
|
+
frame: {
|
|
131
|
+
layouts: store.layouts,
|
|
132
|
+
nodesLaidOut: store.nodesLaidOut,
|
|
133
|
+
repositions: store.repositions,
|
|
134
|
+
flexCalls: store.flexCalls,
|
|
135
|
+
...extras
|
|
136
|
+
},
|
|
137
|
+
problems: [...store.orphans].map((e) => ({
|
|
138
|
+
code: "ui/no-root",
|
|
139
|
+
entity: e,
|
|
140
|
+
path: uiPath(world, e)
|
|
141
|
+
}))
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const uiMethods = [
|
|
145
|
+
{
|
|
146
|
+
name: "ui.describe",
|
|
147
|
+
description: "Every UI tree (or one root) as data: each node's path, rect [x, y, width, height] in the root's pixels, visibility, text and size, style, widget kind and state (hovered, pressed, disabled, focused, on, value), anchor state (on-screen, clamped, hidden) and distance, scroll; the focused and hovered nodes; and this frame's layout and upload counts (0 on a frame where nothing changed).",
|
|
148
|
+
params: defineSchema("ui/DescribeParams", {
|
|
149
|
+
root: t.json({ description: "A UiRoot entity id or path (default: all)." })
|
|
150
|
+
}),
|
|
151
|
+
handler: ({ world }, p) => describeUi(
|
|
152
|
+
world,
|
|
153
|
+
p.root === void 0 || p.root === null ? void 0 : resolveUiEntity(world, p.root)
|
|
154
|
+
)
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "ui.click",
|
|
158
|
+
description: 'Clicks a UiButton or UiToggle by entity id or path (a unique suffix like "play" works): sends ui/UiClick and flips a toggle, as a real click does, with no pixel coordinates. Takes effect now; step a frame for systems to react.',
|
|
159
|
+
params: defineSchema("ui/ClickParams", {
|
|
160
|
+
entity: t.json({ description: "Entity id or scene path of the button." })
|
|
161
|
+
}),
|
|
162
|
+
handler: ({ world }, p) => {
|
|
163
|
+
const e = resolveUiEntity(world, p.entity);
|
|
164
|
+
clickUi(world, e);
|
|
165
|
+
return { clicked: e, path: uiPath(world, e) };
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "ui.focus",
|
|
170
|
+
description: "Gives keyboard and gamepad focus to a widget by id or path (null clears it). While a node has focus, action maps in the game context are paused.",
|
|
171
|
+
params: defineSchema("ui/FocusParams", {
|
|
172
|
+
entity: t.json({ description: "Entity id or scene path, or null." })
|
|
173
|
+
}),
|
|
174
|
+
handler: ({ world }, p) => {
|
|
175
|
+
const e = p.entity === null || p.entity === void 0 ? null : resolveUiEntity(world, p.entity);
|
|
176
|
+
focusUi(world, e);
|
|
177
|
+
return { focused: e, path: e === null ? null : uiPath(world, e) };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
];
|
|
181
|
+
export {
|
|
182
|
+
describeExtras,
|
|
183
|
+
describeUi,
|
|
184
|
+
resolveUiEntity,
|
|
185
|
+
uiMethods,
|
|
186
|
+
uiPath
|
|
187
|
+
};
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retained UI: UiNode trees under UiRoots laid out by flexbox after transforms, drawn over the
|
|
3
|
+
* finished image, and driven by the pointer, keyboard, and gamepad. Without a renderer (headless)
|
|
4
|
+
* everything but drawing runs, so layout and clicks are testable.
|
|
5
|
+
*/
|
|
6
|
+
export declare const uiPlugin: import("@aethervtt/shard-runtime").Plugin;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { First, PostUpdate } from "@aethervtt/shard-core";
|
|
2
|
+
import { updateActions, updateInput } from "@aethervtt/shard-input";
|
|
3
|
+
import { definePlugin } from "@aethervtt/shard-runtime";
|
|
4
|
+
import { Fonts, installLocalization, localizeComponent, localizeText } from "@aethervtt/shard-text";
|
|
5
|
+
import { TransformSystems } from "@aethervtt/shard-transform";
|
|
6
|
+
import * as componentsModule from "./components.js";
|
|
7
|
+
import { UiDefaults, UiText } from "./components.js";
|
|
8
|
+
import * as interactionModule from "./interaction.js";
|
|
9
|
+
import { interactUi, UiPointer } from "./interaction.js";
|
|
10
|
+
import { uiMethods } from "./methods.js";
|
|
11
|
+
import * as renderModule from "./render.js";
|
|
12
|
+
import { installUiRenderer } from "./render.js";
|
|
13
|
+
import * as themeModule from "./theme.js";
|
|
14
|
+
import { UiThemes } from "./theme.js";
|
|
15
|
+
import * as treeModule from "./tree.js";
|
|
16
|
+
import { layoutUi, observeUiStructure, UiState } from "./tree.js";
|
|
17
|
+
const uiPlugin = definePlugin({
|
|
18
|
+
name: "ui",
|
|
19
|
+
provides: [componentsModule, interactionModule, renderModule, themeModule, treeModule],
|
|
20
|
+
dependencies: ["core/transform"],
|
|
21
|
+
build(app) {
|
|
22
|
+
const w = app.world;
|
|
23
|
+
w.initResource(UiState);
|
|
24
|
+
w.initResource(UiPointer);
|
|
25
|
+
w.initResource(UiThemes);
|
|
26
|
+
w.initResource(UiDefaults);
|
|
27
|
+
w.initResource(Fonts);
|
|
28
|
+
observeUiStructure(w);
|
|
29
|
+
installLocalization(app);
|
|
30
|
+
localizeComponent(w, UiText);
|
|
31
|
+
app.addSystems(First, interactUi.after(updateInput).before(updateActions)).addSystems(PostUpdate, layoutUi.after(TransformSystems, localizeText));
|
|
32
|
+
app.addMethod(...uiMethods);
|
|
33
|
+
},
|
|
34
|
+
ready(app) {
|
|
35
|
+
installUiRenderer(app);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
export {
|
|
39
|
+
uiPlugin
|
|
40
|
+
};
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
|
|
3
|
+
import { type NodeDescriptor } from '@aethervtt/shard-render';
|
|
4
|
+
import type { App } from '@aethervtt/shard-runtime';
|
|
5
|
+
import type { FontPage } from '@aethervtt/shard-text';
|
|
6
|
+
import { type Texture } from '@aethervtt/shard-texture';
|
|
7
|
+
import { type UiRootState } from './tree.js';
|
|
8
|
+
/** Floats per quad record: rect, uv, color, border color, radius, clip, border, kind, range, angle. */
|
|
9
|
+
export declare const QUAD_FLOATS = 28;
|
|
10
|
+
/** Consecutive quads drawn with one texture (boxes join any run). */
|
|
11
|
+
interface UiDraw {
|
|
12
|
+
camera: Entity | null;
|
|
13
|
+
first: number;
|
|
14
|
+
count: number;
|
|
15
|
+
/** A texture or a font page; null while only boxes are in the run. */
|
|
16
|
+
source: Texture | FontPage | null;
|
|
17
|
+
page: boolean;
|
|
18
|
+
/** Sample the sRGB view (color images) or the linear one (glyphs, data). */
|
|
19
|
+
srgb: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Every UI root's quads in one storage buffer, in draw order (roots by order, nodes in paint
|
|
23
|
+
* order). Rebuilt only on frames where a root's layout or look changed; otherwise nothing uploads.
|
|
24
|
+
*/
|
|
25
|
+
export declare class UiRenderStore {
|
|
26
|
+
records: Float32Array<ArrayBuffer>;
|
|
27
|
+
recordU32: Uint32Array<ArrayBuffer>;
|
|
28
|
+
count: number;
|
|
29
|
+
draws: UiDraw[];
|
|
30
|
+
drawCount: number;
|
|
31
|
+
readonly buffer: GpuBuffer;
|
|
32
|
+
/** Root visual versions the records were built from. */
|
|
33
|
+
private built;
|
|
34
|
+
overlay: boolean;
|
|
35
|
+
/** This frame's rebuilds and bytes uploaded, and the frame total. */
|
|
36
|
+
rebuilds: number;
|
|
37
|
+
uploadedBytes: number;
|
|
38
|
+
totalUploads: number;
|
|
39
|
+
constructor(gpu: GpuContext);
|
|
40
|
+
/** Whether any root changed since the records were built. */
|
|
41
|
+
stale(roots: readonly UiRootState[], overlay: boolean): boolean;
|
|
42
|
+
markBuilt(roots: readonly UiRootState[], overlay: boolean): void;
|
|
43
|
+
push(): number;
|
|
44
|
+
}
|
|
45
|
+
export declare const UiRenderer: import("@aethervtt/shard-core").ResourceDef<UiRenderStore>;
|
|
46
|
+
export declare const uiLayoutOverlay: import("@aethervtt/shard-render").OverlayDef;
|
|
47
|
+
/** Rebuilds the quad records when any root changed, and uploads them. */
|
|
48
|
+
export declare const prepareUi: import("@aethervtt/shard-core").SystemDef<undefined>;
|
|
49
|
+
interface UiCaches {
|
|
50
|
+
generation: number;
|
|
51
|
+
layouts?: {
|
|
52
|
+
view: GPUBindGroupLayout;
|
|
53
|
+
texture: GPUBindGroupLayout;
|
|
54
|
+
};
|
|
55
|
+
pipelines: Map<string, GPURenderPipeline>;
|
|
56
|
+
groups: Map<string, {
|
|
57
|
+
key: string;
|
|
58
|
+
group: GPUBindGroup;
|
|
59
|
+
}>;
|
|
60
|
+
views: Map<string, GpuBuffer>;
|
|
61
|
+
sampler?: GPUSampler;
|
|
62
|
+
white?: GPUTextureView;
|
|
63
|
+
}
|
|
64
|
+
export declare const UiCachesResource: import("@aethervtt/shard-core").ResourceDef<UiCaches>;
|
|
65
|
+
/** UI: over the finished image, after screen sprites and screen text. */
|
|
66
|
+
export declare function uiNode(world: World): NodeDescriptor;
|
|
67
|
+
/** The ui section of `render.describe`, and the upload counts ui.describe reports. */
|
|
68
|
+
export declare function describeUiRender(world: World): {
|
|
69
|
+
quads: number;
|
|
70
|
+
drawCalls: number;
|
|
71
|
+
rebuilds: number;
|
|
72
|
+
uploadedBytes: number;
|
|
73
|
+
} | undefined;
|
|
74
|
+
/** Adds drawing when a renderer is present: the pass, the prepare system, and the pick blocker. */
|
|
75
|
+
export declare function installUiRenderer(app: App): void;
|
|
76
|
+
export {};
|