@simpludify/core 0.1.2 → 0.1.4
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 +5 -3
- package/dist/foundation/version.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +116 -14
- package/dist/index.js.map +1 -1
- package/dist/interaction/actions.d.ts +5 -0
- package/dist/interaction/input-source.d.ts +3 -0
- package/dist/interaction/input.d.ts +9 -0
- package/dist/ui/canvas-control.d.ts +1 -1
- package/dist/ui/canvas-editor.d.ts +1 -0
- package/dist/ui/canvas-node.d.ts +17 -1
- package/dist/ui/canvas-text-input.d.ts +4 -0
- package/dist/ui/canvas-ui.d.ts +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# @simpludify/core
|
|
2
2
|
|
|
3
|
-
Experimental v0.1.
|
|
3
|
+
Experimental v0.1.4 of Simpludify, a code-first TypeScript 2D engine. APIs may change between 0.x releases. This package contains host-independent Scene/Actor/Component, collision, geometry, tilemaps, tween/animation, input and Canvas UI models. It has no browser or Node runtime dependencies.
|
|
4
4
|
|
|
5
|
-
Registry installation (check availability with `npm view @simpludify/core@0.1.
|
|
5
|
+
Registry installation (check availability with `npm view @simpludify/core@0.1.4 version`):
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npm install @simpludify/core@0.1.
|
|
8
|
+
npm install @simpludify/core@0.1.4
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
```js
|
|
@@ -19,3 +19,5 @@ engine.dispose();
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
ESM only, ES2022, Node >=22.12 for headless usage. No CommonJS or renderer is included. For browser presentation install `@simpludify/web`, which depends on this package. JavaScript source maps embed the original sources; declarations are included. [Repository and API](https://github.com/oz-coden/Simpludify) · [Documentation](https://github.oz-coden.com/Simpludify/). MIT.
|
|
22
|
+
|
|
23
|
+
Registry availability is recorded in the repository release report; a source version alone does not establish publication.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Source workspace version; checked against package.json by the build validator. */
|
|
2
|
-
export declare const ENGINE_VERSION = "0.1.
|
|
2
|
+
export declare const ENGINE_VERSION = "0.1.4";
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export * from "./graphics/sprite-sheet.js";
|
|
|
20
20
|
export * from "./graphics/tile-geometry.js";
|
|
21
21
|
export type { ActionFrame, ActionSchema } from "./interaction/action-frame.js";
|
|
22
22
|
export * from "./interaction/actions.js";
|
|
23
|
+
export type { InputAdapter } from "./interaction/input.js";
|
|
23
24
|
export { InputSystem, KeyboardSource } from "./interaction/input.js";
|
|
24
25
|
export * from "./interaction/key-code.js";
|
|
25
26
|
export * from "./interaction/pointer-routing.js";
|
package/dist/index.js
CHANGED
|
@@ -1583,7 +1583,7 @@ var RandomManager = class {
|
|
|
1583
1583
|
};
|
|
1584
1584
|
// packages/core/src/foundation/version.ts
|
|
1585
1585
|
/** Source workspace version; checked against package.json by the build validator. */
|
|
1586
|
-
var ENGINE_VERSION = "0.1.
|
|
1586
|
+
var ENGINE_VERSION = "0.1.4";
|
|
1587
1587
|
// packages/core/src/scene/actor.ts
|
|
1588
1588
|
var inputPhase$1 = ComponentPhase.Input;
|
|
1589
1589
|
var updatePhase$1 = ComponentPhase.Update;
|
|
@@ -4452,16 +4452,27 @@ function defineActions(definitions) {
|
|
|
4452
4452
|
/** @internal Shared lifetime for logical and device contributions. */
|
|
4453
4453
|
var OwnedInputSource = class {
|
|
4454
4454
|
dead = false;
|
|
4455
|
+
accepting = true;
|
|
4455
4456
|
removeOwner;
|
|
4456
4457
|
get disposed() {
|
|
4457
4458
|
return this.dead;
|
|
4458
4459
|
}
|
|
4460
|
+
get enabled() {
|
|
4461
|
+
return this.accepting;
|
|
4462
|
+
}
|
|
4463
|
+
set enabled(value) {
|
|
4464
|
+
if (this.dead) throw new EngineError("OWNER_DISPOSED", "Input source has been disposed");
|
|
4465
|
+
if (typeof value !== "boolean") throw new EngineError("INVALID_VALUE", "Source enabled must be boolean");
|
|
4466
|
+
if (value === this.accepting) return;
|
|
4467
|
+
this.accepting = value;
|
|
4468
|
+
if (!value) this.release(false);
|
|
4469
|
+
}
|
|
4459
4470
|
/** @internal */ own(owner) {
|
|
4460
4471
|
if (owner.disposed) throw new EngineError("OWNER_DISPOSED", "Input owner has been disposed");
|
|
4461
4472
|
this.removeOwner = owner.onDispose(() => this.dispose());
|
|
4462
4473
|
}
|
|
4463
4474
|
writable(neutral) {
|
|
4464
|
-
if (!this.dead) return
|
|
4475
|
+
if (!this.dead) return this.accepting;
|
|
4465
4476
|
if (neutral) return false;
|
|
4466
4477
|
throw new EngineError("OWNER_DISPOSED", "Input source has been disposed");
|
|
4467
4478
|
}
|
|
@@ -4509,13 +4520,17 @@ var ActionController = class {
|
|
|
4509
4520
|
entries = /* @__PURE__ */ new Map();
|
|
4510
4521
|
sources = /* @__PURE__ */ new Map();
|
|
4511
4522
|
bus = new EventBus();
|
|
4523
|
+
keyboardAllowed = true;
|
|
4524
|
+
blockedKeys = /* @__PURE__ */ new Set();
|
|
4512
4525
|
pending = [];
|
|
4513
4526
|
transitions = [];
|
|
4514
4527
|
constructor(actions, options) {
|
|
4515
4528
|
this.actions = actions;
|
|
4516
4529
|
this.options = options;
|
|
4530
|
+
if (options.keyboard !== void 0 && typeof options.keyboard !== "boolean") throw new EngineError("INVALID_VALUE", "Keyboard enabled must be boolean");
|
|
4531
|
+
this.keyboardAllowed = options.keyboard ?? true;
|
|
4517
4532
|
for (const action of Object.values(actions)) {
|
|
4518
|
-
const binding = checkedBinding(action, options.bindings
|
|
4533
|
+
const binding = checkedBinding(action, options.bindings && Object.hasOwn(options.bindings, action.name) ? options.bindings[action.name] ?? {} : {});
|
|
4519
4534
|
const value = neutral(action);
|
|
4520
4535
|
this.entries.set(action, {
|
|
4521
4536
|
value,
|
|
@@ -4543,6 +4558,20 @@ var ActionController = class {
|
|
|
4543
4558
|
get active() {
|
|
4544
4559
|
return !this.disposed && this.options.scene.state === SceneState.Active && this.options.scene.inputOwner === this.options.input;
|
|
4545
4560
|
}
|
|
4561
|
+
get keyboardEnabled() {
|
|
4562
|
+
return this.keyboardAllowed;
|
|
4563
|
+
}
|
|
4564
|
+
set keyboardEnabled(value) {
|
|
4565
|
+
if (this.disposed) throw new EngineError("OWNER_DISPOSED", "Action controller has been disposed");
|
|
4566
|
+
if (typeof value !== "boolean") throw new EngineError("INVALID_VALUE", "Keyboard enabled must be boolean");
|
|
4567
|
+
if (value === this.keyboardAllowed) return;
|
|
4568
|
+
this.keyboardAllowed = value;
|
|
4569
|
+
this.blockedKeys.clear();
|
|
4570
|
+
if (value) {
|
|
4571
|
+
for (const entry of this.entries.values()) for (const list of keyLists(entry.binding)) for (const key of list.keys) if (this.options.input.keyDown(key)) this.blockedKeys.add(key);
|
|
4572
|
+
}
|
|
4573
|
+
for (const action of this.entries.keys()) this.recompute(action);
|
|
4574
|
+
}
|
|
4546
4575
|
get sourceCount() {
|
|
4547
4576
|
return this.sources.size;
|
|
4548
4577
|
}
|
|
@@ -4641,8 +4670,9 @@ var ActionController = class {
|
|
|
4641
4670
|
if (!this.disposed) for (const action of actions) this.recompute(action);
|
|
4642
4671
|
}
|
|
4643
4672
|
keyboard(action) {
|
|
4673
|
+
if (!this.keyboardAllowed) return neutral(action);
|
|
4644
4674
|
const binding = this.entry(action).binding;
|
|
4645
|
-
const held = (keys) => keys?.some((k) => this.options.input.keyDown(k)) ?? false;
|
|
4675
|
+
const held = (keys) => keys?.some((k) => !this.blockedKeys.has(k) && this.options.input.keyDown(k)) ?? false;
|
|
4646
4676
|
const axis = (b) => Number(held(b?.positive)) - Number(held(b?.negative));
|
|
4647
4677
|
if (action.kind === "button") return held(binding.keys);
|
|
4648
4678
|
if (action.kind === "axis1d") return axis(binding);
|
|
@@ -4696,10 +4726,11 @@ var ActionController = class {
|
|
|
4696
4726
|
entry.value = value;
|
|
4697
4727
|
}
|
|
4698
4728
|
/** @internal */ keyChanged() {
|
|
4729
|
+
for (const key of this.blockedKeys) if (!this.options.input.keyDown(key)) this.blockedKeys.delete(key);
|
|
4699
4730
|
if (this.active || !this.disposed && this.options.scene.inputOwner === this.options.input && (this.options.scene.state === SceneState.Preparing || this.options.scene.state === SceneState.Ready)) for (const action of this.entries.keys()) this.recompute(action);
|
|
4700
4731
|
}
|
|
4701
4732
|
/** @internal */ accepts(code) {
|
|
4702
|
-
return this.active && [...this.entries.values()].some((e) => keyLists(e.binding).some((list) => list.keys.includes(code)));
|
|
4733
|
+
return this.active && this.keyboardAllowed && [...this.entries.values()].some((e) => keyLists(e.binding).some((list) => list.keys.includes(code)));
|
|
4703
4734
|
}
|
|
4704
4735
|
/** @internal */ capture(override) {
|
|
4705
4736
|
if (!this.active) return;
|
|
@@ -4741,6 +4772,7 @@ var ActionController = class {
|
|
|
4741
4772
|
}
|
|
4742
4773
|
/** Clears held contributions and unconsumed edges; no synthetic button release. */
|
|
4743
4774
|
reset() {
|
|
4775
|
+
this.blockedKeys.clear();
|
|
4744
4776
|
for (const values of this.sources.values()) values.clear();
|
|
4745
4777
|
this.pending = [];
|
|
4746
4778
|
for (const [action, entry] of this.entries) entry.value = neutral(action);
|
|
@@ -4840,6 +4872,7 @@ var KeyboardSource = class extends OwnedInputSource {
|
|
|
4840
4872
|
/** Engine-owned routing and update boundary. No raw game-state/alternate movement API. */
|
|
4841
4873
|
var InputSystem = class {
|
|
4842
4874
|
actions = /* @__PURE__ */ new Set();
|
|
4875
|
+
adapters = /* @__PURE__ */ new Set();
|
|
4843
4876
|
keyboards = /* @__PURE__ */ new Map();
|
|
4844
4877
|
dead = false;
|
|
4845
4878
|
playback = false;
|
|
@@ -4856,6 +4889,14 @@ var InputSystem = class {
|
|
|
4856
4889
|
this.actions.delete(actions);
|
|
4857
4890
|
};
|
|
4858
4891
|
}
|
|
4892
|
+
/** Host integration boundary; ordinary game input uses ActionSource instead. */
|
|
4893
|
+
addAdapter(adapter) {
|
|
4894
|
+
if (this.dead) throw new EngineError("OWNER_DISPOSED", "Input has been disposed");
|
|
4895
|
+
this.adapters.add(adapter);
|
|
4896
|
+
return () => {
|
|
4897
|
+
this.adapters.delete(adapter);
|
|
4898
|
+
};
|
|
4899
|
+
}
|
|
4859
4900
|
acceptsKey(code) {
|
|
4860
4901
|
if (this.playback) return false;
|
|
4861
4902
|
for (const actions of this.actions) if (actions.accepts(code)) return true;
|
|
@@ -4871,6 +4912,9 @@ var InputSystem = class {
|
|
|
4871
4912
|
}
|
|
4872
4913
|
/** @internal Freeze every controller before invoking any callbacks. */
|
|
4873
4914
|
beginStep(captured) {
|
|
4915
|
+
if (!this.playback && this.adapters.size) {
|
|
4916
|
+
for (const adapter of [...this.adapters]) if (this.adapters.has(adapter)) adapter.sample();
|
|
4917
|
+
}
|
|
4874
4918
|
if (!this.actions.size) {
|
|
4875
4919
|
captured?.();
|
|
4876
4920
|
return;
|
|
@@ -4945,7 +4989,9 @@ var InputSystem = class {
|
|
|
4945
4989
|
}
|
|
4946
4990
|
clear() {
|
|
4947
4991
|
for (const keys of this.keyboards.values()) keys.clear();
|
|
4948
|
-
|
|
4992
|
+
cleanupAll([...[...this.actions].map((actions) => () => actions.reset()), ...[...this.adapters].map((adapter) => () => {
|
|
4993
|
+
if (this.adapters.has(adapter)) adapter.reset();
|
|
4994
|
+
})], "Input reset failed");
|
|
4949
4995
|
}
|
|
4950
4996
|
/** Device diagnostics only; game state is ActionController.snapshot(). */
|
|
4951
4997
|
snapshot() {
|
|
@@ -4957,8 +5003,13 @@ var InputSystem = class {
|
|
|
4957
5003
|
if (this.dead) return;
|
|
4958
5004
|
this.dead = true;
|
|
4959
5005
|
try {
|
|
4960
|
-
cleanupAll([
|
|
5006
|
+
cleanupAll([
|
|
5007
|
+
...[...this.adapters].map((a) => () => a.dispose()),
|
|
5008
|
+
...[...this.actions].map((a) => () => a.dispose()),
|
|
5009
|
+
...[...this.keyboards.keys()].map((s) => () => s.dispose())
|
|
5010
|
+
], "Input cleanup failed");
|
|
4961
5011
|
} finally {
|
|
5012
|
+
this.adapters.clear();
|
|
4962
5013
|
this.actions.clear();
|
|
4963
5014
|
this.keyboards.clear();
|
|
4964
5015
|
}
|
|
@@ -6269,12 +6320,12 @@ var ReplayPlayer = class {
|
|
|
6269
6320
|
this.engine = engine;
|
|
6270
6321
|
this.data = parseReplayData(data);
|
|
6271
6322
|
this.bound = bindings(engine, options.controllers);
|
|
6272
|
-
if (this.data.engineVersion !== "0.1.
|
|
6323
|
+
if (this.data.engineVersion !== "0.1.4" || engine.random.seed !== this.data.random.seed) throw new EngineError("REPLAY_INCOMPATIBLE", "Engine version or initial seed does not match Replay");
|
|
6273
6324
|
if (!compatible(this.bound.map(([id, c]) => ({
|
|
6274
6325
|
id,
|
|
6275
6326
|
actions: c.schema()
|
|
6276
6327
|
})), this.data.controllers)) throw new EngineError("REPLAY_INCOMPATIBLE", "Controller IDs or Action names/kinds/normalization do not match Replay");
|
|
6277
|
-
for (const [key, value] of Object.entries(options.expectedMetadata ?? {})) if (!compatible(copyJson(value), this.data.metadata[key])) throw new EngineError("REPLAY_INCOMPATIBLE", `Metadata mismatch: ${key}`);
|
|
6328
|
+
for (const [key, value] of Object.entries(options.expectedMetadata ?? {})) if (!Object.hasOwn(this.data.metadata, key) || !compatible(copyJson(value), this.data.metadata[key])) throw new EngineError("REPLAY_INCOMPATIBLE", `Metadata mismatch: ${key}`);
|
|
6278
6329
|
this.detach = engine.attachExecution(this);
|
|
6279
6330
|
try {
|
|
6280
6331
|
engine.input.lockPlayback();
|
|
@@ -6515,7 +6566,7 @@ function checkedStyle(value) {
|
|
|
6515
6566
|
"highlight"
|
|
6516
6567
|
]
|
|
6517
6568
|
};
|
|
6518
|
-
if (typeof v !== "string" || (choices
|
|
6569
|
+
if (typeof v !== "string" || (Object.hasOwn(choices, key) ? !choices[key]?.includes(v) : !validFontFamily(v))) throw new EngineError("INVALID_UI", `Invalid UI ${key}`);
|
|
6519
6570
|
}
|
|
6520
6571
|
}
|
|
6521
6572
|
return Object.freeze(result);
|
|
@@ -6639,11 +6690,22 @@ var CanvasNode = class {
|
|
|
6639
6690
|
}
|
|
6640
6691
|
click;
|
|
6641
6692
|
hold;
|
|
6693
|
+
/** @internal Text semantics supplied by CanvasLabel. */
|
|
6694
|
+
labelSemantics;
|
|
6642
6695
|
/** @internal Prefer CanvasPanel/Label/Button/HoldButton. */
|
|
6643
6696
|
constructor(kind, options = {}) {
|
|
6644
6697
|
this.kind = kind;
|
|
6645
6698
|
this.captionValue = options.text ?? "";
|
|
6646
6699
|
if (typeof this.captionValue !== "string") throw new EngineError("INVALID_UI", "UI text must be a string");
|
|
6700
|
+
if (options.label) {
|
|
6701
|
+
const { role, level } = options.label;
|
|
6702
|
+
if (kind !== "label" || role !== void 0 && ![
|
|
6703
|
+
"heading",
|
|
6704
|
+
"status",
|
|
6705
|
+
"alert"
|
|
6706
|
+
].includes(role) || (role === "heading" ? typeof level !== "number" || !Number.isInteger(level) || level < 1 || level > 6 : level !== void 0)) throw new EngineError("INVALID_UI", "Invalid label semantics");
|
|
6707
|
+
this.labelSemantics = Object.freeze({ ...options.label });
|
|
6708
|
+
}
|
|
6647
6709
|
this.config = checkedStyle(options.style ?? {});
|
|
6648
6710
|
this.click = options.click;
|
|
6649
6711
|
this.hold = options.hold;
|
|
@@ -6889,9 +6951,17 @@ function CanvasColumn(children, style = {}) {
|
|
|
6889
6951
|
direction: "column"
|
|
6890
6952
|
});
|
|
6891
6953
|
}
|
|
6892
|
-
function CanvasLabel(
|
|
6954
|
+
function CanvasLabel(content, style = {}) {
|
|
6955
|
+
if (typeof content === "string") return new CanvasNode("label", {
|
|
6956
|
+
text: content,
|
|
6957
|
+
style
|
|
6958
|
+
});
|
|
6959
|
+
if (!content || typeof content !== "object") throw new EngineError("INVALID_UI", "UI label must be text or options");
|
|
6960
|
+
const { text, ...label } = content;
|
|
6961
|
+
if (typeof text !== "string") throw new EngineError("INVALID_UI", "UI text must be a string");
|
|
6893
6962
|
return new CanvasNode("label", {
|
|
6894
6963
|
text,
|
|
6964
|
+
label,
|
|
6895
6965
|
style
|
|
6896
6966
|
});
|
|
6897
6967
|
}
|
|
@@ -6928,6 +6998,10 @@ function CanvasScroll(children = [], style = {}) {
|
|
|
6928
6998
|
clip: true
|
|
6929
6999
|
});
|
|
6930
7000
|
}
|
|
7001
|
+
function validFontFamily(value) {
|
|
7002
|
+
const syntax = value.replace(/"(?:\\[^\r\n]|[^"\\])*"|'(?:\\[^\r\n]|[^'\\])*'/g, "family").replace(/\\(?:[0-9a-f]{1,6}[ \t]?|[^\r\n])/gi, "x");
|
|
7003
|
+
return value.trim().length > 0 && !/[\r\n\0]/.test(value) && !/[;{}<>\\"']|url\s*\(/i.test(syntax);
|
|
7004
|
+
}
|
|
6931
7005
|
// packages/core/src/ui/canvas-input.ts
|
|
6932
7006
|
var boolean = (value) => {
|
|
6933
7007
|
if (typeof value !== "boolean") throw new EngineError("INVALID_UI", "Control flag must be boolean");
|
|
@@ -7361,6 +7435,9 @@ var CanvasTextEditorNode = class extends CanvasInputNode {
|
|
|
7361
7435
|
this.buffer = snapshot(initialText);
|
|
7362
7436
|
this.onDispose(() => this.session?.cancel());
|
|
7363
7437
|
}
|
|
7438
|
+
get inputMode() {
|
|
7439
|
+
return this.kind === "number-input" ? "decimal" : "text";
|
|
7440
|
+
}
|
|
7364
7441
|
get limit() {}
|
|
7365
7442
|
get draft() {
|
|
7366
7443
|
return this.buffer;
|
|
@@ -7386,7 +7463,7 @@ var CanvasTextEditorNode = class extends CanvasInputNode {
|
|
|
7386
7463
|
text: this.editing ? this.buffer.text : this.format(this.value),
|
|
7387
7464
|
placeholder: this.hint,
|
|
7388
7465
|
maxLength: this.limit,
|
|
7389
|
-
inputMode: this.
|
|
7466
|
+
inputMode: this.inputMode,
|
|
7390
7467
|
readOnly: this.readOnly
|
|
7391
7468
|
});
|
|
7392
7469
|
}
|
|
@@ -7669,7 +7746,16 @@ function validText(value, limit) {
|
|
|
7669
7746
|
var CanvasTextInputNode = class extends CanvasTextEditorNode {
|
|
7670
7747
|
constraints;
|
|
7671
7748
|
constructor(options = {}, style = {}) {
|
|
7672
|
-
const
|
|
7749
|
+
const inputMode = options.inputMode ?? "text";
|
|
7750
|
+
if (![
|
|
7751
|
+
"text",
|
|
7752
|
+
"numeric",
|
|
7753
|
+
"decimal"
|
|
7754
|
+
].includes(inputMode)) throw new EngineError("INVALID_UI", "Invalid text keyboard hint");
|
|
7755
|
+
const constraints = {
|
|
7756
|
+
maxLength: lengthLimit(options.maxLength),
|
|
7757
|
+
inputMode
|
|
7758
|
+
}, initial = validText(options.value ?? "", constraints.maxLength);
|
|
7673
7759
|
super("text-input", options.label ?? "Text", initial, (value) => validText(value, constraints.maxLength), options.readOnly ?? false, options.placeholder ?? "", initial, style);
|
|
7674
7760
|
this.constraints = constraints;
|
|
7675
7761
|
}
|
|
@@ -7680,6 +7766,9 @@ var CanvasTextInputNode = class extends CanvasTextEditorNode {
|
|
|
7680
7766
|
if (this.maxLength !== void 0 && text.length > this.maxLength) return;
|
|
7681
7767
|
return { value: text };
|
|
7682
7768
|
}
|
|
7769
|
+
get inputMode() {
|
|
7770
|
+
return this.constraints.inputMode;
|
|
7771
|
+
}
|
|
7683
7772
|
get limit() {
|
|
7684
7773
|
return this.maxLength;
|
|
7685
7774
|
}
|
|
@@ -7759,6 +7848,7 @@ var CanvasUI = class {
|
|
|
7759
7848
|
focusNode;
|
|
7760
7849
|
factor;
|
|
7761
7850
|
dirty = true;
|
|
7851
|
+
measure;
|
|
7762
7852
|
projectionDirty = true;
|
|
7763
7853
|
baseBounds = /* @__PURE__ */ new WeakMap();
|
|
7764
7854
|
moving = /* @__PURE__ */ new Set();
|
|
@@ -7841,8 +7931,12 @@ var CanvasUI = class {
|
|
|
7841
7931
|
/** Logical focus; Hosts mirror it to native controls where available. */
|
|
7842
7932
|
focus(node) {
|
|
7843
7933
|
if (this.disposed) throw new EngineError("OWNER_DISPOSED", "UI has been disposed");
|
|
7934
|
+
this.syncFocusLayout();
|
|
7844
7935
|
if (node && !this.canFocus(node)) throw new EngineError("UI_OWNERSHIP", "Focus requires a reachable interactive node in this UI");
|
|
7845
|
-
if (this.focusNode === node)
|
|
7936
|
+
if (this.focusNode === node) {
|
|
7937
|
+
if (node) this.reveal(node);
|
|
7938
|
+
return;
|
|
7939
|
+
}
|
|
7846
7940
|
const previous = this.focusNode;
|
|
7847
7941
|
this.focusNode = node;
|
|
7848
7942
|
if (previous) {
|
|
@@ -7858,6 +7952,7 @@ var CanvasUI = class {
|
|
|
7858
7952
|
}
|
|
7859
7953
|
/** Ordered traversal, including offscreen controls. Undefined leaves focus at the boundary. */
|
|
7860
7954
|
focusNext(reverse = false) {
|
|
7955
|
+
this.syncFocusLayout();
|
|
7861
7956
|
const order = [];
|
|
7862
7957
|
this.walk((node) => {
|
|
7863
7958
|
if (this.canFocus(node)) order.push(node);
|
|
@@ -7867,6 +7962,10 @@ var CanvasUI = class {
|
|
|
7867
7962
|
if (node) this.focus(node);
|
|
7868
7963
|
return node;
|
|
7869
7964
|
}
|
|
7965
|
+
/** Reuse the Host's last metrics after a dynamic form/list mutation. */
|
|
7966
|
+
syncFocusLayout() {
|
|
7967
|
+
if (this.measure && (this.dirty || this.projectionDirty)) this.layout(this.width, this.height, this.measure);
|
|
7968
|
+
}
|
|
7870
7969
|
/** Project scroll reveal before testing clips; static clipped controls must not trap Tab. */
|
|
7871
7970
|
canFocus(node) {
|
|
7872
7971
|
if (node.ui !== this || !node.available || !node.interactive || node.style.pointerEvents !== "auto") return false;
|
|
@@ -7948,6 +8047,7 @@ var CanvasUI = class {
|
|
|
7948
8047
|
}
|
|
7949
8048
|
}
|
|
7950
8049
|
/** @internal */ invalidate(layout = false) {
|
|
8050
|
+
if (this.disposed) return;
|
|
7951
8051
|
if (this.focusNode && (this.focusNode.ui !== this || !this.focusNode.available)) this.focus();
|
|
7952
8052
|
for (const node of this.moving) if (node.ui !== this || !node.available || !node.style.inertia) {
|
|
7953
8053
|
this.moving.delete(node);
|
|
@@ -7959,6 +8059,7 @@ var CanvasUI = class {
|
|
|
7959
8059
|
/** @internal Rendering supplies exact text metrics; unchanged frames do no layout. */
|
|
7960
8060
|
layout(width, height, measure) {
|
|
7961
8061
|
if (this.disposed) return false;
|
|
8062
|
+
this.measure = measure;
|
|
7962
8063
|
const geometry = this.dirty || this.width !== width || this.height !== height;
|
|
7963
8064
|
if (!geometry && !this.projectionDirty) return false;
|
|
7964
8065
|
if (geometry) {
|
|
@@ -8112,6 +8213,7 @@ var CanvasUI = class {
|
|
|
8112
8213
|
this.removeScene = void 0;
|
|
8113
8214
|
this.nodes = 0;
|
|
8114
8215
|
this.errors = () => {};
|
|
8216
|
+
this.measure = void 0;
|
|
8115
8217
|
}
|
|
8116
8218
|
], "Canvas UI cleanup failed");
|
|
8117
8219
|
}
|