@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.
@@ -0,0 +1,11 @@
1
+ export { UI_ALIGN, UI_ALIGN_SELF, UI_ANCHOR_STATES, UI_DIRECTIONS, UI_DISPLAYS, UI_FIT, UI_JUSTIFY, UI_OVERFLOW, UI_POSITIONS, UI_SCALES, UI_STATES, UI_TEXT_ALIGN, UiAnchor, UiAnchorArrow, UiButton, UiChanged, UiClick, UiDefaults, type UiDefaultsValue, type UiEntityEvent, UiHover, UiImage, UiInteraction, UiLayout, UiNode, type UiNodeValue, UiRoot, UiSlider, UiStyle, type UiStyleValue, UiText, UiTextInput, type UiTextValue, UiToggle, } from './components.js';
2
+ export { Align, Dir, FlexTree, Justify, L, LENGTHS, layoutTree, type Measure } from './flex.js';
3
+ export { clickUi, focusUi, hitTest, interactUi, UiPointer, UiPointerState } from './interaction.js';
4
+ export { LengthUnit, parseLength, type UiLength, uiLength } from './length.js';
5
+ export { describeUi, resolveUiEntity, uiMethods, uiPath } from './methods.js';
6
+ export { uiPlugin } from './plugin.js';
7
+ export { describeUiRender, prepareUi, QUAD_FLOATS, UiRenderer, UiRenderStore, uiLayoutOverlay, } from './render.js';
8
+ export { UI_SHADERS } from './shaders.js';
9
+ export { ResolvedStyle, resolveStyle, State } from './style.js';
10
+ export { THEME_FIELDS, THEME_STATES, type ThemeStyle, UiThemeAsset, UiThemeAssetType, UiThemeImporter, UiThemeSchema, UiThemeStore, UiThemes, } from './theme.js';
11
+ export { AnchorState, Kind, layoutUi, UiRootState, UiState, UiStore } from './tree.js';
package/dist/index.js ADDED
@@ -0,0 +1,133 @@
1
+ import {
2
+ UI_ALIGN,
3
+ UI_ALIGN_SELF,
4
+ UI_ANCHOR_STATES,
5
+ UI_DIRECTIONS,
6
+ UI_DISPLAYS,
7
+ UI_FIT,
8
+ UI_JUSTIFY,
9
+ UI_OVERFLOW,
10
+ UI_POSITIONS,
11
+ UI_SCALES,
12
+ UI_STATES,
13
+ UI_TEXT_ALIGN,
14
+ UiAnchor,
15
+ UiAnchorArrow,
16
+ UiButton,
17
+ UiChanged,
18
+ UiClick,
19
+ UiDefaults,
20
+ UiHover,
21
+ UiImage,
22
+ UiInteraction,
23
+ UiLayout,
24
+ UiNode,
25
+ UiRoot,
26
+ UiSlider,
27
+ UiStyle,
28
+ UiText,
29
+ UiTextInput,
30
+ UiToggle
31
+ } from "./components.js";
32
+ import { Align, Dir, FlexTree, Justify, L, LENGTHS, layoutTree } from "./flex.js";
33
+ import { clickUi, focusUi, hitTest, interactUi, UiPointer, UiPointerState } from "./interaction.js";
34
+ import { LengthUnit, parseLength, uiLength } from "./length.js";
35
+ import { describeUi, resolveUiEntity, uiMethods, uiPath } from "./methods.js";
36
+ import { uiPlugin } from "./plugin.js";
37
+ import {
38
+ describeUiRender,
39
+ prepareUi,
40
+ QUAD_FLOATS,
41
+ UiRenderer,
42
+ UiRenderStore,
43
+ uiLayoutOverlay
44
+ } from "./render.js";
45
+ import { UI_SHADERS } from "./shaders.js";
46
+ import { ResolvedStyle, resolveStyle, State } from "./style.js";
47
+ import {
48
+ THEME_FIELDS,
49
+ THEME_STATES,
50
+ UiThemeAsset,
51
+ UiThemeAssetType,
52
+ UiThemeImporter,
53
+ UiThemeSchema,
54
+ UiThemeStore,
55
+ UiThemes
56
+ } from "./theme.js";
57
+ import { AnchorState, Kind, layoutUi, UiRootState, UiState, UiStore } from "./tree.js";
58
+ export {
59
+ Align,
60
+ AnchorState,
61
+ Dir,
62
+ FlexTree,
63
+ Justify,
64
+ Kind,
65
+ L,
66
+ LENGTHS,
67
+ LengthUnit,
68
+ QUAD_FLOATS,
69
+ ResolvedStyle,
70
+ State,
71
+ THEME_FIELDS,
72
+ THEME_STATES,
73
+ UI_ALIGN,
74
+ UI_ALIGN_SELF,
75
+ UI_ANCHOR_STATES,
76
+ UI_DIRECTIONS,
77
+ UI_DISPLAYS,
78
+ UI_FIT,
79
+ UI_JUSTIFY,
80
+ UI_OVERFLOW,
81
+ UI_POSITIONS,
82
+ UI_SCALES,
83
+ UI_SHADERS,
84
+ UI_STATES,
85
+ UI_TEXT_ALIGN,
86
+ UiAnchor,
87
+ UiAnchorArrow,
88
+ UiButton,
89
+ UiChanged,
90
+ UiClick,
91
+ UiDefaults,
92
+ UiHover,
93
+ UiImage,
94
+ UiInteraction,
95
+ UiLayout,
96
+ UiNode,
97
+ UiPointer,
98
+ UiPointerState,
99
+ UiRenderStore,
100
+ UiRenderer,
101
+ UiRoot,
102
+ UiRootState,
103
+ UiSlider,
104
+ UiState,
105
+ UiStore,
106
+ UiStyle,
107
+ UiText,
108
+ UiTextInput,
109
+ UiThemeAsset,
110
+ UiThemeAssetType,
111
+ UiThemeImporter,
112
+ UiThemeSchema,
113
+ UiThemeStore,
114
+ UiThemes,
115
+ UiToggle,
116
+ clickUi,
117
+ describeUi,
118
+ describeUiRender,
119
+ focusUi,
120
+ hitTest,
121
+ interactUi,
122
+ layoutTree,
123
+ layoutUi,
124
+ parseLength,
125
+ prepareUi,
126
+ resolveStyle,
127
+ resolveUiEntity,
128
+ uiLayoutOverlay,
129
+ uiLength,
130
+ uiMethods,
131
+ uiPath,
132
+ uiPlugin
133
+ };
@@ -0,0 +1,44 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import { type UiRootState, type UiStore } from './tree.js';
3
+ /** Pointer and focus state of the UI. */
4
+ export declare class UiPointerState {
5
+ /** The interactive node under the pointer (null: none). */
6
+ hovered: Entity | null;
7
+ /** The node a press started on, until release. */
8
+ pressed: Entity | null;
9
+ focused: Entity | null;
10
+ /** The pointer is over UI that takes it (a node with a background, an image, or a widget). */
11
+ overUi: boolean;
12
+ /** A press that started on the world: UI doesn't take the pointer until it's released. */
13
+ worldPress: boolean;
14
+ /** Pointer in screen pixels, and whether a pointer (mouse or touch) is present. */
15
+ x: number;
16
+ y: number;
17
+ touch: number;
18
+ /** Clicks and changes this frame (for describe). */
19
+ clicks: number;
20
+ }
21
+ export declare const UiPointer: import("@aethervtt/shard-core").ResourceDef<UiPointerState>;
22
+ /** The topmost node taking the pointer at (x, y) screen pixels in a root, or -1. */
23
+ export declare function hitNode(r: UiRootState, sx: number, sy: number): number;
24
+ declare const hitResult: {
25
+ root: UiRootState | undefined;
26
+ node: number;
27
+ };
28
+ /** The topmost UI node taking the pointer at a screen pixel, over every root (top root first). */
29
+ export declare function hitTest(store: UiStore, sx: number, sy: number, camera?: Entity): typeof hitResult;
30
+ /**
31
+ * Clicks a button or toggle by entity, as a real click would (UiClick, a toggle flips), without
32
+ * pointer coordinates. For tests, tools, and `ui.click`.
33
+ */
34
+ export declare function clickUi(world: World, entity: Entity): void;
35
+ /** Gives keyboard and gamepad focus to a widget (null clears it). */
36
+ export declare function focusUi(world: World, entity: Entity | null): void;
37
+ /**
38
+ * Pointer, keyboard, and gamepad input for UI, between device updates and action maps: hit tests
39
+ * last frame's layout, updates hover and press, clicks, drags sliders, scrolls, moves focus, and
40
+ * edits text. While the pointer is over UI (or pressing a widget), action maps don't see mouse
41
+ * buttons or the wheel; while a node has focus the input context is `ui`.
42
+ */
43
+ export declare const interactUi: import("@aethervtt/shard-core").SystemDef<undefined>;
44
+ export {};
@@ -0,0 +1,416 @@
1
+ import {
2
+ defineResource,
3
+ defineSystem,
4
+ ShardError
5
+ } from "@aethervtt/shard-core";
6
+ import { Gamepads, InputContext, Keyboard, Mouse, Touches } from "@aethervtt/shard-input";
7
+ import {
8
+ UiChanged,
9
+ UiClick,
10
+ UiHover,
11
+ UiInteraction,
12
+ UiNode,
13
+ UiSlider,
14
+ UiTextInput,
15
+ UiToggle
16
+ } from "./components.js";
17
+ import { Kind, UiState } from "./tree.js";
18
+ const STATE_NONE = 0;
19
+ const STATE_HOVERED = 1;
20
+ const STATE_PRESSED = 2;
21
+ const STATE_DISABLED = 3;
22
+ class UiPointerState {
23
+ /** The interactive node under the pointer (null: none). */
24
+ hovered = null;
25
+ /** The node a press started on, until release. */
26
+ pressed = null;
27
+ focused = null;
28
+ /** The pointer is over UI that takes it (a node with a background, an image, or a widget). */
29
+ overUi = false;
30
+ /** A press that started on the world: UI doesn't take the pointer until it's released. */
31
+ worldPress = false;
32
+ /** Pointer in screen pixels, and whether a pointer (mouse or touch) is present. */
33
+ x = -1;
34
+ y = -1;
35
+ touch = -1;
36
+ /** Clicks and changes this frame (for describe). */
37
+ clicks = 0;
38
+ }
39
+ const UiPointer = defineResource("ui/UiPointer", {
40
+ description: "What the pointer is over, what it pressed, and which node has focus.",
41
+ init: () => new UiPointerState()
42
+ });
43
+ function hitNode(r, sx, sy) {
44
+ const x = sx / r.factor;
45
+ const y = sy / r.factor;
46
+ for (let p = r.count - 1; p >= 0; p--) {
47
+ const i = r.paint[p];
48
+ if (!r.visible[i] || !r.solid[i]) continue;
49
+ if (x < r.x[i] || y < r.y[i] || x >= r.x[i] + r.w[i] || y >= r.y[i] + r.h[i]) continue;
50
+ const o = i * 4;
51
+ if (x < r.clip[o] || y < r.clip[o + 1] || x >= r.clip[o + 2] || y >= r.clip[o + 3]) continue;
52
+ return i;
53
+ }
54
+ return -1;
55
+ }
56
+ function widgetOf(r, i) {
57
+ for (let n = i; n >= 0; n = r.parent[n]) if (r.kind[n] !== Kind.None) return n;
58
+ return -1;
59
+ }
60
+ const hitResult = { root: void 0, node: -1 };
61
+ function hitTest(store, sx, sy, camera) {
62
+ hitResult.root = void 0;
63
+ hitResult.node = -1;
64
+ for (let k = store.roots.length - 1; k >= 0; k--) {
65
+ const r = store.roots[k];
66
+ if (camera !== void 0 && r.camera !== camera) continue;
67
+ const i = hitNode(r, sx, sy);
68
+ if (i >= 0) {
69
+ hitResult.root = r;
70
+ hitResult.node = i;
71
+ return hitResult;
72
+ }
73
+ }
74
+ return hitResult;
75
+ }
76
+ function nodeOf(store, e) {
77
+ const r = store.rootOf.get(e);
78
+ const i = store.indexOf.get(e);
79
+ return r && i !== void 0 ? { r, i } : void 0;
80
+ }
81
+ function activate(world, e, kind) {
82
+ const pointer = world.resource(UiPointer);
83
+ if (kind === Kind.Toggle) {
84
+ const table = world.entityTable(e);
85
+ const row = world.entityRow(e);
86
+ const on = table.column(UiToggle, "on");
87
+ on[row] = on[row] ? 0 : 1;
88
+ table.markChanged(UiToggle, row);
89
+ world.send(UiChanged, { entity: e });
90
+ }
91
+ if (kind === Kind.Button || kind === Kind.Toggle) {
92
+ world.send(UiClick, { entity: e });
93
+ pointer.clicks++;
94
+ }
95
+ }
96
+ function clickUi(world, entity) {
97
+ const store = world.resource(UiState);
98
+ const at = nodeOf(store, entity);
99
+ const kind = at ? at.r.kind[at.i] : Kind.None;
100
+ if (!at || kind !== Kind.Button && kind !== Kind.Toggle) {
101
+ throw new ShardError(
102
+ "ui/not-clickable",
103
+ `Entity ${entity} isn't a UiButton or UiToggle in a UI tree`,
104
+ {
105
+ hint: "ui.click takes the path of a node with ui/UiButton or ui/UiToggle (see ui.describe)."
106
+ }
107
+ );
108
+ }
109
+ if (at.r.disabled[at.i]) {
110
+ throw new ShardError("ui/disabled", `Entity ${entity} is disabled`, {
111
+ hint: "Set disabled to false on its UiButton or UiToggle first."
112
+ });
113
+ }
114
+ if (!at.r.visible[at.i]) {
115
+ throw new ShardError("ui/hidden", `Entity ${entity} isn't visible`, {
116
+ hint: "A node with display none, under a hidden anchor, or outside a root can't be clicked."
117
+ });
118
+ }
119
+ activate(world, entity, kind);
120
+ }
121
+ function focusUi(world, entity) {
122
+ const pointer = world.resource(UiPointer);
123
+ if (entity !== null) {
124
+ const at = nodeOf(world.resource(UiState), entity);
125
+ if (!at || at.r.kind[at.i] === Kind.None) {
126
+ throw new ShardError("ui/not-focusable", `Entity ${entity} isn't a widget in a UI tree`, {
127
+ hint: "Focus takes a node with UiButton, UiToggle, UiSlider, or UiTextInput."
128
+ });
129
+ }
130
+ }
131
+ pointer.focused = entity;
132
+ }
133
+ function sliderValue(world, e, fraction) {
134
+ const table = world.entityTable(e);
135
+ const row = world.entityRow(e);
136
+ const min = table.column(UiSlider, "min")[row];
137
+ const max = table.column(UiSlider, "max")[row];
138
+ const step = table.column(UiSlider, "step")[row];
139
+ let v = min + Math.min(1, Math.max(0, fraction)) * (max - min);
140
+ if (step > 0) v = min + Math.round((v - min) / step) * step;
141
+ const lo = Math.min(min, max);
142
+ const hi = Math.max(min, max);
143
+ v = Math.min(hi, Math.max(lo, v));
144
+ const value = table.column(UiSlider, "value");
145
+ if (Math.fround(v) === value[row]) return false;
146
+ value[row] = v;
147
+ table.markChanged(UiSlider, row);
148
+ world.send(UiChanged, { entity: e });
149
+ return true;
150
+ }
151
+ function stepSlider(world, e, dir) {
152
+ const table = world.entityTable(e);
153
+ const row = world.entityRow(e);
154
+ const min = table.column(UiSlider, "min")[row];
155
+ const max = table.column(UiSlider, "max")[row];
156
+ const step = table.column(UiSlider, "step")[row];
157
+ const span = max - min;
158
+ const value = table.column(UiSlider, "value")[row];
159
+ const delta = step > 0 ? step : span / 10;
160
+ if (span !== 0) sliderValue(world, e, (value + dir * delta - min) / span);
161
+ }
162
+ function editText(world, e, typed) {
163
+ const table = world.entityTable(e);
164
+ const row = world.entityRow(e);
165
+ const values = table.column(UiTextInput, "value");
166
+ const maxLength = table.column(UiTextInput, "maxLength")[row];
167
+ let v = values[row] ?? "";
168
+ const before = v;
169
+ for (const ch of typed) {
170
+ if (ch === "\b") v = Array.from(v).slice(0, -1).join("");
171
+ else if (ch >= " " && (maxLength === 0 || Array.from(v).length < maxLength)) v += ch;
172
+ }
173
+ if (v === before) return;
174
+ values[row] = v;
175
+ table.markChanged(UiTextInput, row);
176
+ world.send(UiChanged, { entity: e });
177
+ }
178
+ function focusables(store, out) {
179
+ let n = 0;
180
+ for (const r of store.roots) {
181
+ for (let i = 0; i < r.count; i++) {
182
+ if (r.kind[i] === Kind.None || r.disabled[i] || !r.visible[i]) continue;
183
+ const slot = out[n];
184
+ if (slot) {
185
+ slot.r = r;
186
+ slot.i = i;
187
+ } else out.push({ r, i });
188
+ n++;
189
+ }
190
+ }
191
+ return n;
192
+ }
193
+ function nearest(list, n, from, dx, dy) {
194
+ const f = from.r.factor;
195
+ const cx = (from.r.x[from.i] + from.r.w[from.i] / 2) * f;
196
+ const cy = (from.r.y[from.i] + from.r.h[from.i] / 2) * f;
197
+ let best;
198
+ let bestScore = Number.POSITIVE_INFINITY;
199
+ for (let k = 0; k < n; k++) {
200
+ const c = list[k];
201
+ if (c.r === from.r && c.i === from.i) continue;
202
+ const g = c.r.factor;
203
+ const x = (c.r.x[c.i] + c.r.w[c.i] / 2) * g - cx;
204
+ const y = (c.r.y[c.i] + c.r.h[c.i] / 2) * g - cy;
205
+ const along = x * dx + y * dy;
206
+ if (along <= 0.5) continue;
207
+ const across = Math.abs(dx !== 0 ? y : x);
208
+ const score = along + 2 * across;
209
+ if (score < bestScore) {
210
+ bestScore = score;
211
+ best = c;
212
+ }
213
+ }
214
+ return best;
215
+ }
216
+ const focusList = [];
217
+ function setInteraction(world, e, state, focused) {
218
+ if (e === null || !world.isAlive(e) || !world.has(e, UiInteraction)) return;
219
+ const table = world.entityTable(e);
220
+ const row = world.entityRow(e);
221
+ const s = table.column(UiInteraction, "state");
222
+ const f = table.column(UiInteraction, "focused");
223
+ if (s[row] === state && f[row] !== 0 === focused) return;
224
+ s[row] = state;
225
+ f[row] = focused ? 1 : 0;
226
+ table.markChanged(UiInteraction, row);
227
+ }
228
+ function stateOf(store, p, e) {
229
+ const at = nodeOf(store, e);
230
+ if (at?.r.disabled[at.i]) return STATE_DISABLED;
231
+ if (p.pressed === e && p.hovered === e) return STATE_PRESSED;
232
+ if (p.hovered === e || p.pressed === e) return STATE_HOVERED;
233
+ return STATE_NONE;
234
+ }
235
+ const interactUi = defineSystem({
236
+ name: "ui/interact",
237
+ description: "Hover, press, click, drag, scroll, focus navigation, and text editing for UI.",
238
+ run: (_, world) => {
239
+ const store = world.tryResource(UiState);
240
+ const p = world.tryResource(UiPointer);
241
+ const mouse = world.tryResource(Mouse);
242
+ if (!store || !p || !mouse) return;
243
+ const keyboard = world.resource(Keyboard);
244
+ const touches = world.resource(Touches);
245
+ const pads = world.resource(Gamepads);
246
+ p.clicks = 0;
247
+ let px = mouse.position[0];
248
+ let py = mouse.position[1];
249
+ let down = mouse.justPressed("left");
250
+ let up = mouse.justReleased("left");
251
+ let held = mouse.pressed("left");
252
+ let startedTouch = -1;
253
+ if (touches.started.length > 0) {
254
+ startedTouch = touches.started[0];
255
+ p.touch = startedTouch;
256
+ down = true;
257
+ }
258
+ if (p.touch >= 0) {
259
+ const t = touches.active.get(p.touch);
260
+ if (t) {
261
+ px = t.position[0];
262
+ py = t.position[1];
263
+ held = true;
264
+ } else {
265
+ up = true;
266
+ held = false;
267
+ p.touch = -1;
268
+ }
269
+ }
270
+ p.x = px;
271
+ p.y = py;
272
+ const hit = hitTest(store, px, py);
273
+ const overUi = hit.node >= 0;
274
+ p.overUi = overUi;
275
+ let widget = null;
276
+ if (hit.root) {
277
+ const w = widgetOf(hit.root, hit.node);
278
+ if (w >= 0 && !hit.root.disabled[w]) widget = hit.root.entities[w];
279
+ }
280
+ const prevHover = p.hovered;
281
+ if (widget !== prevHover) {
282
+ if (prevHover !== null && world.isAlive(prevHover))
283
+ world.send(UiHover, { entity: prevHover, hovered: false });
284
+ if (widget !== null) world.send(UiHover, { entity: widget, hovered: true });
285
+ p.hovered = widget;
286
+ }
287
+ if (down) {
288
+ if (overUi) {
289
+ p.pressed = widget;
290
+ p.worldPress = false;
291
+ if (startedTouch >= 0) touches.captured.add(startedTouch);
292
+ const at = widget !== null ? nodeOf(store, widget) : void 0;
293
+ const kind2 = at ? at.r.kind[at.i] : Kind.None;
294
+ p.focused = kind2 === Kind.TextInput ? widget : null;
295
+ } else {
296
+ p.worldPress = true;
297
+ p.focused = null;
298
+ }
299
+ }
300
+ if (p.pressed !== null) {
301
+ const at = nodeOf(store, p.pressed);
302
+ if (at && at.r.kind[at.i] === Kind.Slider && held) {
303
+ const f = at.r.factor;
304
+ sliderValue(world, p.pressed, (px / f - at.r.x[at.i]) / Math.max(1e-6, at.r.w[at.i]));
305
+ }
306
+ }
307
+ if (up) {
308
+ const pressed = p.pressed;
309
+ if (pressed !== null && pressed === widget) {
310
+ const at = nodeOf(store, pressed);
311
+ if (at) activate(world, pressed, at.r.kind[at.i]);
312
+ }
313
+ p.pressed = null;
314
+ p.worldPress = false;
315
+ } else if (!held && p.touch < 0) {
316
+ p.pressed = null;
317
+ p.worldPress = false;
318
+ }
319
+ mouse.captured = p.pressed !== null || overUi && !p.worldPress;
320
+ if (overUi && hit.root && (mouse.wheel[0] !== 0 || mouse.wheel[1] !== 0)) {
321
+ const r = hit.root;
322
+ for (let n2 = hit.node; n2 >= 0; n2 = r.parent[n2]) {
323
+ if (r.overflow[n2] !== 2) continue;
324
+ const e = r.entities[n2];
325
+ const table = world.entityTable(e);
326
+ const row = world.entityRow(e);
327
+ const scroll = table.column(UiNode, "scroll");
328
+ const maxX = Math.max(0, r.contentW[n2] - r.tree.w[n2]);
329
+ const maxY = Math.max(0, r.contentH[n2] - r.tree.h[n2]);
330
+ const sx = Math.min(maxX, Math.max(0, r.scrollX[n2] + mouse.wheel[0] / r.factor));
331
+ const sy = Math.min(maxY, Math.max(0, r.scrollY[n2] + mouse.wheel[1] / r.factor));
332
+ if (sx !== scroll[row * 2] || sy !== scroll[row * 2 + 1]) {
333
+ scroll[row * 2] = sx;
334
+ scroll[row * 2 + 1] = sy;
335
+ table.markChanged(UiNode, row);
336
+ }
337
+ break;
338
+ }
339
+ }
340
+ if (p.focused !== null && !(world.isAlive(p.focused) && nodeOf(store, p.focused)))
341
+ p.focused = null;
342
+ const n = focusables(store, focusList);
343
+ const shift = keyboard.pressed("ShiftLeft") || keyboard.pressed("ShiftRight");
344
+ if (keyboard.justPressed("Tab") && n > 0) {
345
+ let at = -1;
346
+ for (let k = 0; k < n; k++) {
347
+ const c2 = focusList[k];
348
+ if (c2.r.entities[c2.i] === p.focused) at = k;
349
+ }
350
+ const next = at < 0 ? shift ? n - 1 : 0 : (at + (shift ? n - 1 : 1)) % n;
351
+ const c = focusList[next];
352
+ p.focused = c.r.entities[c.i];
353
+ }
354
+ const focusedAt = p.focused !== null ? nodeOf(store, p.focused) : void 0;
355
+ let navX = 0;
356
+ let navY = 0;
357
+ let activatePressed = false;
358
+ let back = keyboard.justPressed("Escape");
359
+ const kind = focusedAt ? focusedAt.r.kind[focusedAt.i] : Kind.None;
360
+ const typing = kind === Kind.TextInput;
361
+ if (focusedAt) {
362
+ if (!typing) {
363
+ if (keyboard.justPressed("ArrowLeft")) navX = -1;
364
+ if (keyboard.justPressed("ArrowRight")) navX = 1;
365
+ }
366
+ if (keyboard.justPressed("ArrowUp")) navY = -1;
367
+ if (keyboard.justPressed("ArrowDown")) navY = 1;
368
+ activatePressed = keyboard.justPressed("Enter") || keyboard.justPressed("NumpadEnter") || !typing && keyboard.justPressed("Space");
369
+ }
370
+ for (let k = 0; k < 4; k++) {
371
+ const pad = pads.get(k);
372
+ if (!pad) continue;
373
+ if (focusedAt) {
374
+ if (pad.justPressed("DpadLeft")) navX = -1;
375
+ if (pad.justPressed("DpadRight")) navX = 1;
376
+ if (pad.justPressed("DpadUp")) navY = -1;
377
+ if (pad.justPressed("DpadDown")) navY = 1;
378
+ if (pad.justPressed("South")) activatePressed = true;
379
+ }
380
+ if (pad.justPressed("East")) back = true;
381
+ }
382
+ if (focusedAt && p.focused !== null) {
383
+ if (kind === Kind.Slider && navX !== 0) {
384
+ stepSlider(world, p.focused, navX);
385
+ navX = 0;
386
+ }
387
+ if (navX !== 0 || navY !== 0) {
388
+ const next = nearest(focusList, n, focusedAt, navX !== 0 ? navX : 0, navX !== 0 ? 0 : navY);
389
+ if (next) p.focused = next.r.entities[next.i];
390
+ } else if (activatePressed) {
391
+ if (typing) p.focused = null;
392
+ else activate(world, p.focused, kind);
393
+ }
394
+ if (typing && p.focused !== null && keyboard.typed !== "")
395
+ editText(world, p.focused, keyboard.typed);
396
+ if (back) p.focused = null;
397
+ }
398
+ world.resource(InputContext).active = p.focused !== null ? "ui" : "game";
399
+ for (const r of store.roots) {
400
+ for (let i = 0; i < r.count; i++) {
401
+ if (r.kind[i] === Kind.None) continue;
402
+ const e = r.entities[i];
403
+ setInteraction(world, e, stateOf(store, p, e), p.focused === e);
404
+ }
405
+ }
406
+ }
407
+ });
408
+ export {
409
+ UiPointer,
410
+ UiPointerState,
411
+ clickUi,
412
+ focusUi,
413
+ hitNode,
414
+ hitTest,
415
+ interactUi
416
+ };
@@ -0,0 +1,17 @@
1
+ import { type FieldOptions, type FieldType } from '@aethervtt/shard-core';
2
+ /** A size or offset: pixels (a number), a percent of the parent (`"50%"`), or `"auto"`. */
3
+ export type UiLength = number | `${number}%` | 'auto';
4
+ /** Unit codes in the second float of a length column. */
5
+ export declare const LengthUnit: {
6
+ readonly Px: 0;
7
+ readonly Percent: 1;
8
+ readonly Auto: 2;
9
+ };
10
+ /** Parses a length into [value, unit], or undefined when it isn't one. */
11
+ export declare function parseLength(json: unknown, out?: [number, number]): [number, number] | undefined;
12
+ /**
13
+ * A length field: one f32 column with two floats per row (value, unit), so layout reads it from a
14
+ * TypedArray. JSON takes a number (pixels), `"120"`, `"120px"`, `"50%"`, or `"auto"`; reads give
15
+ * a number for pixels and a string otherwise.
16
+ */
17
+ export declare function uiLength(options?: FieldOptions<UiLength>): FieldType<UiLength, 'f32'>;