canvasengine 2.0.0-beta.45 → 2.0.0-beta.47

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.
Files changed (79) hide show
  1. package/dist/components/Container.d.ts +86 -0
  2. package/dist/components/Container.d.ts.map +1 -0
  3. package/dist/components/DOMContainer.d.ts +98 -0
  4. package/dist/components/DOMContainer.d.ts.map +1 -0
  5. package/dist/components/DOMElement.d.ts +16 -5
  6. package/dist/components/DOMElement.d.ts.map +1 -1
  7. package/dist/components/DOMSprite.d.ts +108 -0
  8. package/dist/components/DOMSprite.d.ts.map +1 -0
  9. package/dist/components/DisplayObject.d.ts +94 -0
  10. package/dist/components/DisplayObject.d.ts.map +1 -0
  11. package/dist/components/FocusContainer.d.ts +129 -0
  12. package/dist/components/FocusContainer.d.ts.map +1 -0
  13. package/dist/components/Mesh.d.ts +208 -0
  14. package/dist/components/Mesh.d.ts.map +1 -0
  15. package/dist/components/Sprite.d.ts +242 -0
  16. package/dist/components/Sprite.d.ts.map +1 -0
  17. package/dist/components/Viewport.d.ts +121 -0
  18. package/dist/components/Viewport.d.ts.map +1 -0
  19. package/dist/components/index.d.ts +2 -1
  20. package/dist/components/index.d.ts.map +1 -1
  21. package/dist/directives/Controls.d.ts +4 -4
  22. package/dist/directives/Controls.d.ts.map +1 -1
  23. package/dist/directives/ControlsBase.d.ts +1 -0
  24. package/dist/directives/ControlsBase.d.ts.map +1 -1
  25. package/dist/directives/FocusNavigation.d.ts +4 -22
  26. package/dist/directives/FocusNavigation.d.ts.map +1 -1
  27. package/dist/directives/KeyboardControls.d.ts +1 -0
  28. package/dist/directives/KeyboardControls.d.ts.map +1 -1
  29. package/dist/directives/Scheduler.d.ts.map +1 -1
  30. package/dist/directives/Shake.d.ts +1 -0
  31. package/dist/directives/Shake.d.ts.map +1 -1
  32. package/dist/engine/FocusManager.d.ts +10 -9
  33. package/dist/engine/FocusManager.d.ts.map +1 -1
  34. package/dist/engine/bootstrap.d.ts +1 -0
  35. package/dist/engine/bootstrap.d.ts.map +1 -1
  36. package/dist/engine/directive.d.ts +1 -1
  37. package/dist/engine/directive.d.ts.map +1 -1
  38. package/dist/engine/reactive.d.ts.map +1 -1
  39. package/dist/hooks/useFocus.d.ts.map +1 -1
  40. package/dist/index-DaGekQUW.js +2218 -0
  41. package/dist/index-DaGekQUW.js.map +1 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.global.js +3 -3
  45. package/dist/index.global.js.map +1 -1
  46. package/dist/index.js +11868 -88
  47. package/dist/index.js.map +1 -1
  48. package/dist/utils/tabindex.d.ts +16 -0
  49. package/dist/utils/tabindex.d.ts.map +1 -0
  50. package/package.json +1 -1
  51. package/src/components/DOMContainer.ts +186 -1
  52. package/src/components/DOMElement.ts +164 -37
  53. package/src/components/DOMSprite.ts +759 -0
  54. package/src/components/DisplayObject.ts +33 -7
  55. package/src/components/FocusContainer.ts +22 -26
  56. package/src/components/Sprite.ts +12 -3
  57. package/src/components/Text.ts +1 -1
  58. package/src/components/Viewport.ts +5 -5
  59. package/src/components/index.ts +2 -1
  60. package/src/directives/Controls.ts +5 -5
  61. package/src/directives/ControlsBase.ts +1 -0
  62. package/src/directives/FocusNavigation.ts +8 -146
  63. package/src/directives/KeyboardControls.ts +11 -2
  64. package/src/directives/Scheduler.ts +12 -4
  65. package/src/directives/Shake.ts +9 -6
  66. package/src/engine/FocusManager.ts +44 -29
  67. package/src/engine/bootstrap.ts +5 -2
  68. package/src/engine/directive.ts +2 -2
  69. package/src/engine/reactive.ts +84 -12
  70. package/src/hooks/useFocus.ts +2 -5
  71. package/src/index.ts +2 -1
  72. package/src/types/pixi-cull.d.ts +7 -0
  73. package/src/utils/tabindex.ts +70 -0
  74. package/testing/index.ts +31 -3
  75. package/tsconfig.json +3 -2
  76. package/dist/DebugRenderer-CSxse9YI.js +0 -172
  77. package/dist/DebugRenderer-CSxse9YI.js.map +0 -1
  78. package/dist/index-DH2ZMhYm.js +0 -13276
  79. package/dist/index-DH2ZMhYm.js.map +0 -1
@@ -9,7 +9,7 @@ import type {
9
9
  TransformOrigin,
10
10
  } from "./types/DisplayObject";
11
11
  import { signal } from "@signe/reactive";
12
- import { BlurFilter, ObservablePoint } from "pixi.js";
12
+ import { BlurFilter, ObservablePoint, type Point, type Rectangle } from "pixi.js";
13
13
  import * as FILTERS from "pixi-filters";
14
14
  import { isPercent } from "../utils/functions";
15
15
  import { BehaviorSubject, filter, Subject } from "rxjs";
@@ -20,9 +20,11 @@ export interface ComponentInstance extends PixiMixins.ContainerOptions {
20
20
  onInit?(props: Props): void;
21
21
  onUpdate?(props: Props): void;
22
22
  onDestroy?(parent: Element, afterDestroy: () => void): void;
23
- onMount?(context: Element, index?: number): void;
23
+ onMount?(context: Element<any>, index?: number): void;
24
24
  setWidth(width: number): void;
25
25
  setHeight(height: number): void;
26
+ getLocalBounds?(): Rectangle;
27
+ getGlobalPosition?(): Point;
26
28
  }
27
29
 
28
30
  export const EVENTS = [
@@ -121,16 +123,18 @@ export function DisplayObject(extendClass) {
121
123
  // Store computed layout box dimensions
122
124
  #computedLayoutBox: { width?: number; height?: number } | null = null;
123
125
  // Store reference to element for freeze checking
124
- #element: Element<DisplayObject> | null = null;
126
+ #element: Element<any> | null = null;
125
127
 
126
128
  /**
127
129
  * Get the element reference for freeze checking
128
130
  * @returns The element reference or null
129
131
  */
130
- protected getElement(): Element<DisplayObject> | null {
132
+ getElement(): Element<any> | null {
131
133
  return this.#element;
132
134
  }
133
135
 
136
+ onLayoutComputed(_event: any) {}
137
+
134
138
  get deltaRatio() {
135
139
  return this.#canvasContext?.scheduler?.tick.value.deltaRatio;
136
140
  }
@@ -141,6 +145,10 @@ export function DisplayObject(extendClass) {
141
145
  }
142
146
 
143
147
  onInit(props: Props) {
148
+ // Ensure layout setter from @pixi/layout is used when available.
149
+ if (Object.prototype.hasOwnProperty.call(this, "layout")) {
150
+ delete (this as any).layout;
151
+ }
144
152
  this._id = props.id;
145
153
  for (let event of EVENTS) {
146
154
  if (props[event] && !this.overrideProps.includes(event)) {
@@ -190,12 +198,29 @@ export function DisplayObject(extendClass) {
190
198
  this.subjectInit.next(this);
191
199
  }
192
200
 
193
- async onMount(element: Element<DisplayObject>, index?: number) {
201
+ async onMount(element: Element<any>, index?: number) {
194
202
  if (this.destroyed) return
195
203
  this.#element = element;
196
204
  this.#canvasContext = element.props.context;
197
205
  if (element.parent) {
198
- const instance = element.parent.componentInstance as DisplayObject;
206
+ let parentElement = element.parent;
207
+ let instance = parentElement.componentInstance as DisplayObject;
208
+ if (typeof (instance as any)?.addChild !== "function") {
209
+ let search = parentElement.parent;
210
+ while (search && typeof (search.componentInstance as any)?.addChild !== "function") {
211
+ search = search.parent;
212
+ }
213
+ if (search && typeof (search.componentInstance as any)?.addChild === "function") {
214
+ parentElement = search;
215
+ instance = parentElement.componentInstance as DisplayObject;
216
+ } else {
217
+ console.warn("DisplayObject mount skipped: parent has no addChild", {
218
+ child: element.tag,
219
+ parent: element.parent?.tag,
220
+ });
221
+ return;
222
+ }
223
+ }
199
224
  if (instance.isFlex && !this.layout && !this.disableLayout) {
200
225
  try {
201
226
  this.layout = {};
@@ -203,7 +228,7 @@ export function DisplayObject(extendClass) {
203
228
  console.warn('Failed to set layout:', error);
204
229
  }
205
230
  }
206
- if (index === undefined) {
231
+ if (index === undefined || parentElement !== element.parent || typeof (instance as any)?.addChildAt !== "function") {
207
232
  instance.addChild(this);
208
233
  } else {
209
234
  instance.addChildAt(this, index);
@@ -219,6 +244,7 @@ export function DisplayObject(extendClass) {
219
244
  height: event.computedLayout.height,
220
245
  };
221
246
  }
247
+ this.onLayoutComputed(event);
222
248
  };
223
249
  this.on('layout', layoutHandler);
224
250
  this.#registeredEvents.set('layout', layoutHandler);
@@ -1,12 +1,9 @@
1
- import { Container as PixiContainer } from "pixi.js";
2
1
  import { createComponent, registerComponent, type Element } from "../engine/reactive";
3
2
  import { applyDirective } from "../engine/directive";
4
- import { ComponentInstance, DisplayObject } from "./DisplayObject";
5
- import { ComponentFunction, h } from "../engine/signal";
3
+ import { ComponentFunction } from "../engine/signal";
6
4
  import { DisplayObjectProps } from "./types/DisplayObject";
7
- import { Container } from "./Container";
8
5
  import { focusManager, ScrollOptions } from "../engine/FocusManager";
9
- import { signal, Signal, isSignal } from "@signe/reactive";
6
+ import { signal, Signal, WritableSignal, WritableObjectSignal, isSignal } from "@signe/reactive";
10
7
  import { CanvasViewport } from "./Viewport";
11
8
  import { Controls } from "../directives/ControlsBase";
12
9
  // Import FocusNavigation directive to ensure it's registered
@@ -27,7 +24,9 @@ export interface FocusContainerProps extends DisplayObjectProps {
27
24
  onFocusChange?: (index: number, element: Element | null) => void;
28
25
  autoScroll?: boolean | ScrollOptions;
29
26
  viewport?: CanvasViewport;
30
- throttle?: number;
27
+ context?: {
28
+ viewport?: CanvasViewport;
29
+ };
31
30
  }
32
31
 
33
32
  /**
@@ -67,10 +66,10 @@ export interface FocusContainerProps extends DisplayObjectProps {
67
66
  * </Viewport>
68
67
  * ```
69
68
  */
70
- export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
69
+ export class CanvasFocusContainer {
71
70
  private containerId: string = '';
72
- private currentIndexSignal: Signal<number | null> | null = null;
73
- private focusedElementSignal: Signal<Element | null> | null = null;
71
+ private currentIndexSignal: WritableSignal<number | null> | null = null;
72
+ private focusedElementSignal: WritableSignal<Element | null> | WritableObjectSignal<Element | null> | null = null;
74
73
  private registeredFocusables: Set<number> = new Set();
75
74
 
76
75
  /**
@@ -79,14 +78,12 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
79
78
  * @param props - Component properties
80
79
  */
81
80
  onInit(props: FocusContainerProps) {
82
- super.onInit(props);
83
-
84
81
  // Generate unique container ID
85
82
  this.containerId = `focus-container-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
86
83
 
87
84
  // Create signals for current index and focused element
88
85
  const currentIndex = signal<number | null>(null);
89
- const focusedElement = signal<Element | null>(null);
86
+ const focusedElement = signal<Element | null>(null) as WritableSignal<Element | null> | WritableObjectSignal<Element | null>;
90
87
 
91
88
  this.currentIndexSignal = currentIndex;
92
89
  this.focusedElementSignal = focusedElement;
@@ -101,8 +98,7 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
101
98
  focusedElement,
102
99
  onFocusChange: props.onFocusChange,
103
100
  autoScroll: props.autoScroll,
104
- viewport,
105
- throttle: props.throttle ?? 150
101
+ viewport
106
102
  });
107
103
  }
108
104
 
@@ -112,8 +108,6 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
112
108
  * @param element - The element being mounted
113
109
  */
114
110
  async onMount(element: Element<CanvasFocusContainer>): Promise<void> {
115
- await super.onMount(element, undefined);
116
-
117
111
  // Update container with element reference for freeze checking
118
112
  focusManager.updateContainer(this.containerId, { element });
119
113
 
@@ -154,7 +148,7 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
154
148
  // element.effectSubscriptions.push(subscription);
155
149
  // }
156
150
 
157
- focusManager.setTabindex(this.containerId, element.propObservables.tabindex);
151
+ focusManager.setTabindex(this.containerId, element.propObservables?.tabindex as any);
158
152
 
159
153
  // Register all focusable children initially
160
154
  // Use setTimeout to ensure children are mounted
@@ -169,15 +163,12 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
169
163
  * @param props - Updated properties
170
164
  */
171
165
  onUpdate(props: FocusContainerProps) {
172
- super.onUpdate(props);
173
-
174
166
  // Update viewport if changed
175
167
  const viewport = props.viewport || (props.context?.viewport as CanvasViewport | undefined);
176
168
  focusManager.updateContainer(this.containerId, {
177
169
  viewport,
178
170
  autoScroll: props.autoScroll,
179
- onFocusChange: props.onFocusChange,
180
- throttle: props.throttle ?? 150
171
+ onFocusChange: props.onFocusChange
181
172
  });
182
173
  }
183
174
 
@@ -196,8 +187,9 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
196
187
 
197
188
  // Unregister container
198
189
  focusManager.unregisterContainer(this.containerId);
199
-
200
- await super.onDestroy(parent, afterDestroy);
190
+ if (afterDestroy) {
191
+ afterDestroy();
192
+ }
201
193
  }
202
194
 
203
195
  /**
@@ -248,6 +240,9 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
248
240
 
249
241
  const processChild = (child: Element) => {
250
242
  if (!child || !child.componentInstance) return;
243
+ if ((child.tag === "Navigation" || child.tag === "FocusContainer") && child !== (element as any)) {
244
+ return;
245
+ }
251
246
 
252
247
  // Check for tabindex in props
253
248
  let tabindex: number | undefined = undefined;
@@ -272,7 +267,7 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
272
267
  }
273
268
  }
274
269
 
275
- // Recursively process children
270
+ // Recursively process children unless we hit another FocusContainer
276
271
  if (child.props && child.props.children) {
277
272
  if (Array.isArray(child.props.children)) {
278
273
  processChildren(child.props.children);
@@ -358,7 +353,7 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
358
353
 
359
354
  export interface CanvasFocusContainer extends DisplayObjectProps { }
360
355
 
361
- registerComponent("FocusContainer", CanvasFocusContainer);
356
+ registerComponent("Navigation", CanvasFocusContainer);
362
357
 
363
358
  /**
364
359
  * FocusContainer component function
@@ -367,6 +362,7 @@ registerComponent("FocusContainer", CanvasFocusContainer);
367
362
  * @returns FocusContainer element
368
363
  */
369
364
  export const FocusContainer: ComponentFunction<FocusContainerProps> = (props) => {
370
- return createComponent("FocusContainer", props);
365
+ return createComponent("Navigation", props);
371
366
  };
372
367
 
368
+ export const Navigation = FocusContainer;
@@ -299,7 +299,7 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
299
299
  }
300
300
  }
301
301
 
302
- async onMount(params: Element<CanvasSprite>) {
302
+ async onMount(params: Element<any>) {
303
303
  // Set #element manually for freeze checking before calling super.onMount
304
304
  // We need to set it early so update() can check freeze state
305
305
  (this as any)['#element'] = params;
@@ -323,6 +323,13 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
323
323
  this.spritesheet = resolvedDefinition.value ?? resolvedDefinition;
324
324
  await this.createAnimations();
325
325
  }
326
+ if (sheet?.params) {
327
+ this.sheetParams = sheet.params;
328
+ }
329
+ if (sheet?.playing && this.has(sheet.playing)) {
330
+ this.sheetCurrentAnimation = sheet.playing;
331
+ this.play(this.sheetCurrentAnimation, [this.sheetParams]);
332
+ }
326
333
  if (sheet.params) {
327
334
  for (let key in propObservables?.sheet["params"]) {
328
335
  const value = propObservables?.sheet["params"][key] as Signal;
@@ -330,11 +337,13 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
330
337
  this.subscriptionSheet.push(
331
338
  value.observable.subscribe((value) => {
332
339
  if (this.animations.size == 0) return;
333
- this.play(this.sheetCurrentAnimation, [{ [key]: value }]);
340
+ if (!this.has(this.sheetCurrentAnimation)) return;
341
+ this.play(this.sheetCurrentAnimation, [{ ...this.sheetParams, [key]: value }]);
334
342
  })
335
343
  );
336
344
  } else {
337
- this.play(this.sheetCurrentAnimation, [{ [key]: value }]);
345
+ if (!this.has(this.sheetCurrentAnimation)) continue;
346
+ this.play(this.sheetCurrentAnimation, [{ ...this.sheetParams, [key]: value }]);
338
347
  }
339
348
  }
340
349
  }
@@ -48,7 +48,7 @@ class CanvasText extends DisplayObject(PixiText) {
48
48
  * @param {Element<CanvasText>} element - The element being mounted with parent and props.
49
49
  * @param {number} [index] - The index of the component among its siblings.
50
50
  */
51
- async onMount(element: Element<CanvasText>, index?: number): Promise<void> {
51
+ async onMount(element: Element<any>, index?: number): Promise<void> {
52
52
  const { props } = element;
53
53
  await super.onMount(element, index);
54
54
  const tick: Signal = props.context.tick;
@@ -3,7 +3,7 @@ import { Subscription } from 'rxjs';
3
3
  import { createComponent, registerComponent, Element, Props } from '../engine/reactive';
4
4
  import { DisplayObject, ComponentInstance } from './DisplayObject';
5
5
  import { effect, Signal } from '@signe/reactive';
6
- import { Graphics, Container } from 'pixi.js';
6
+ import { Graphics, Container, ContainerChild, IRenderLayer } from 'pixi.js';
7
7
 
8
8
  const EVENTS = [
9
9
  'bounce-x-end',
@@ -73,8 +73,8 @@ export class CanvasViewport extends DisplayObject(Container) {
73
73
  return this.viewport.addChild(...children)
74
74
  }
75
75
 
76
- addChildAt<U extends any>(child: U, index: number): U {
77
- return this.viewport.addChildAt(child, index)
76
+ addChildAt<T extends ContainerChild | IRenderLayer>(child: T, index: number): T {
77
+ return this.viewport.addChildAt(child, index) as T
78
78
  }
79
79
 
80
80
  onInit(props) {
@@ -90,7 +90,7 @@ export class CanvasViewport extends DisplayObject(Container) {
90
90
  * @param {Element<CanvasViewport>} element - The element being mounted. Its `props` property (of type ViewportProps) contains component properties and context.
91
91
  * @param {number} [index] - The index of the component among its siblings.
92
92
  */
93
- async onMount(element: Element<CanvasViewport>, index?: number): Promise<void> {
93
+ async onMount(element: Element<any>, index?: number): Promise<void> {
94
94
  element.props.context.viewport = this.viewport
95
95
  await super.onMount(element, index);
96
96
  const { props } = element;
@@ -211,4 +211,4 @@ registerComponent('Viewport', CanvasViewport)
211
211
 
212
212
  export function Viewport(props: ViewportProps) {
213
213
  return createComponent('Viewport', props);
214
- }
214
+ }
@@ -13,6 +13,7 @@ export { NineSliceSprite } from './NineSliceSprite'
13
13
  export { type ComponentInstance } from './DisplayObject'
14
14
  export { DOMContainer } from './DOMContainer'
15
15
  export { DOMElement } from './DOMElement'
16
+ export { DOMSprite } from './DOMSprite'
16
17
  export { Button, ButtonState, type ButtonProps, type ButtonStyle } from './Button'
17
18
  export { Joystick, type JoystickSettings } from './Joystick'
18
- export { FocusContainer, type FocusContainerProps } from './FocusContainer'
19
+ export { FocusContainer, Navigation, type FocusContainerProps } from './FocusContainer'
@@ -36,7 +36,7 @@ export class ControlsDirective extends Directive {
36
36
  * Initialize the controls directive
37
37
  * Sets up keyboard, gamepad, and joystick controls if available
38
38
  */
39
- onInit(element: Element) {
39
+ onInit(element: Element<any>) {
40
40
  this.element = element;
41
41
  const value = element.props.controls?.value ?? element.props.controls;
42
42
  if (!value) return;
@@ -71,7 +71,7 @@ export class ControlsDirective extends Directive {
71
71
  // Subscribe to freeze prop if it's a signal
72
72
  const freezeProp = element.propObservables?.freeze ?? element.props?.freeze;
73
73
  if (isSignal(freezeProp)) {
74
- this.freezeSubscription = (freezeProp as Signal<boolean>).observable.subscribe((isFrozen) => {
74
+ this.freezeSubscription = ((freezeProp as Signal<boolean>).observable as any).subscribe((isFrozen) => {
75
75
  if (isFrozen) {
76
76
  this.stopInputs();
77
77
  } else {
@@ -84,13 +84,13 @@ export class ControlsDirective extends Directive {
84
84
  /**
85
85
  * Mount hook (no specific action needed)
86
86
  */
87
- onMount(element: Element) { }
87
+ onMount(element: Element<any>) { }
88
88
 
89
89
  /**
90
90
  * Update controls configuration
91
91
  * Updates both keyboard and gamepad controls
92
92
  */
93
- onUpdate(props: any, element: Element) {
93
+ onUpdate(props: any, element: Element<any>) {
94
94
  const value = props.controls?.value ?? props.controls;
95
95
  if (value) {
96
96
  if (this.keyboardControls) {
@@ -115,7 +115,7 @@ export class ControlsDirective extends Directive {
115
115
  /**
116
116
  * Cleanup and destroy all control systems
117
117
  */
118
- onDestroy(element: Element) {
118
+ onDestroy(element: Element<any>) {
119
119
  if (this.freezeSubscription) {
120
120
  this.freezeSubscription.unsubscribe();
121
121
  this.freezeSubscription = null;
@@ -5,6 +5,7 @@ export interface ControlOptions {
5
5
  bind: string | string[];
6
6
  keyUp?: Function;
7
7
  keyDown?: Function;
8
+ throttle?: number;
8
9
  delay?: number | {
9
10
  duration: number;
10
11
  otherControls?: (string)[];
@@ -1,19 +1,18 @@
1
1
  import { Directive, registerDirective, applyDirective } from "../engine/directive";
2
2
  import { type Element } from "../engine/reactive";
3
- import { focusManager } from "../engine/FocusManager";
4
3
  import { ControlsDirective } from "./Controls";
5
4
  import { Controls } from "./ControlsBase";
6
5
  import { isSignal, Signal } from "@signe/reactive";
7
6
  import { CanvasFocusContainer } from "../components/FocusContainer";
8
7
 
9
8
  /**
10
- * FocusNavigation directive for automatic focus navigation via Controls
9
+ * FocusNavigation directive for wiring Controls with FocusContainer
11
10
  *
12
- * This directive integrates with the Controls system to automatically navigate
13
- * between focusable elements using keyboard arrows or gamepad input.
11
+ * This directive integrates with the Controls system and lets external
12
+ * control handlers update the FocusContainer tabindex signal.
14
13
  *
15
14
  * The directive is automatically applied when a FocusContainer has a `controls` prop.
16
- * It wraps the existing Controls configuration to add focus navigation behavior.
15
+ * It keeps the Controls directive in sync with the provided controls config.
17
16
  *
18
17
  * @example
19
18
  * ```typescript
@@ -27,9 +26,7 @@ import { CanvasFocusContainer } from "../components/FocusContainer";
27
26
  export class FocusNavigationDirective extends Directive {
28
27
  private element: Element<CanvasFocusContainer> | null = null;
29
28
  private controlsDirective: ControlsDirective | null = null;
30
- private containerId: string = '';
31
29
  private controlsSubscription: any = null;
32
- private originalControls: Controls | null = null;
33
30
 
34
31
  /**
35
32
  * Initialize the focus navigation directive
@@ -38,130 +35,6 @@ export class FocusNavigationDirective extends Directive {
38
35
  */
39
36
  onInit(element: Element<CanvasFocusContainer>) {
40
37
  this.element = element;
41
-
42
- // Get container ID from component instance
43
- const instance = element.componentInstance as CanvasFocusContainer;
44
- if (instance && typeof instance.getContainerId === 'function') {
45
- this.containerId = instance.getContainerId();
46
- }
47
-
48
- // Get controls from props
49
- const controlsProp = element.props.controls;
50
- if (!controlsProp) return;
51
-
52
- // Get controls value (handle signals)
53
- const controlsValue = isSignal(controlsProp) ? controlsProp() : controlsProp;
54
- if (!controlsValue) return;
55
-
56
- this.originalControls = controlsValue;
57
-
58
- // Note: Controls directive will be created/initialized in onMount
59
- // We'll set up navigation controls there
60
- }
61
-
62
- /**
63
- * Set up navigation controls
64
- *
65
- * @param controls - Controls configuration
66
- */
67
- private setupNavigationControls(controls: Controls) {
68
- if (!this.controlsDirective) {
69
- console.warn('FocusNavigation: Controls directive not found, cannot set up navigation');
70
- return;
71
- }
72
- controls = (controls.value ?? controls) as Controls;
73
- // Create navigation controls by wrapping existing ones
74
- const navigationControls: Controls = {
75
- ...controls,
76
- // Override or add navigation controls
77
- up: {
78
- ...controls.up,
79
- repeat: controls.up?.repeat ?? true,
80
- bind: controls.up?.bind ?? ['up', 'top_left', 'top_right'],
81
- keyDown: (boundKey?: any) => {
82
- // Navigate up/previous first
83
- this.navigate('previous');
84
- // Call original handler if exists
85
- controls.up?.keyDown?.(boundKey);
86
- }
87
- },
88
- down: {
89
- ...controls.down,
90
- repeat: controls.down?.repeat ?? true,
91
- bind: controls.down?.bind ?? ['down', 'bottom_left', 'bottom_right'],
92
- keyDown: (boundKey?: any) => {
93
- // Navigate down/next first
94
- this.navigate('next');
95
- // Call original handler if exists
96
- controls.down?.keyDown?.(boundKey);
97
- }
98
- },
99
- left: {
100
- ...controls.left,
101
- repeat: controls.left?.repeat ?? true,
102
- bind: controls.left?.bind ?? 'left',
103
- keyDown: (boundKey?: any) => {
104
- // Navigate previous (for horizontal lists)
105
- this.navigate('previous');
106
- // Call original handler if exists
107
- controls.left?.keyDown?.(boundKey);
108
- }
109
- },
110
- right: {
111
- ...controls.right,
112
- repeat: controls.right?.repeat ?? true,
113
- bind: controls.right?.bind ?? 'right',
114
- keyDown: (boundKey?: any) => {
115
- // Navigate next (for horizontal lists)
116
- this.navigate('next');
117
- // Call original handler if exists
118
- controls.right?.keyDown?.(boundKey);
119
- }
120
- },
121
- action: {
122
- ...controls.action,
123
- bind: controls.action?.bind ?? ['space', 'enter'],
124
- keyDown: (boundKey?: any) => {
125
- // Trigger action on focused element (e.g., click)
126
- this.triggerAction();
127
- // Call original handler if exists
128
- controls.action?.keyDown?.(boundKey);
129
- }
130
- }
131
- };
132
-
133
- // Update controls directive with navigation controls
134
- this.controlsDirective.onUpdate({ controls: navigationControls }, this.element!);
135
- }
136
-
137
- /**
138
- * Navigate to next or previous focusable element
139
- *
140
- * @param direction - Navigation direction
141
- */
142
- private navigate(direction: 'next' | 'previous') {
143
- if (!this.containerId) return;
144
- focusManager.navigate(this.containerId, direction);
145
- }
146
-
147
- /**
148
- * Trigger action on currently focused element
149
- */
150
- private triggerAction() {
151
- if (!this.containerId) return;
152
-
153
- const focusedElementSignal = focusManager.getFocusedElementSignal(this.containerId);
154
- if (!focusedElementSignal) return;
155
-
156
- const focusedElement = focusedElementSignal();
157
- if (!focusedElement) return;
158
-
159
- // Try to trigger click/pointertap event on focused element
160
- const instance = focusedElement.componentInstance;
161
- if (instance && typeof instance.emit === 'function') {
162
- // Emit pointertap event (equivalent to click)
163
- instance.emit('pointertap', { target: instance });
164
- }
165
38
  }
166
39
 
167
40
  /**
@@ -170,12 +43,6 @@ export class FocusNavigationDirective extends Directive {
170
43
  * @param element - FocusContainer element
171
44
  */
172
45
  onMount(element: Element<CanvasFocusContainer>) {
173
- // Get container ID again (should be available now)
174
- const instance = element.componentInstance as CanvasFocusContainer;
175
- if (instance && typeof instance.getContainerId === 'function') {
176
- this.containerId = instance.getContainerId();
177
- }
178
-
179
46
  // Get or create Controls directive
180
47
  this.controlsDirective = element.directives?.controls as ControlsDirective;
181
48
 
@@ -196,17 +63,15 @@ export class FocusNavigationDirective extends Directive {
196
63
  if (controlsProp) {
197
64
  const controlsValue = isSignal(controlsProp) ? controlsProp() : controlsProp;
198
65
  if (controlsValue) {
199
- this.originalControls = controlsValue;
200
- this.setupNavigationControls(controlsValue);
66
+ this.controlsDirective?.onUpdate({ controls: controlsValue }, element);
201
67
  }
202
68
  }
203
69
 
204
70
  // Handle controls prop updates if it's a signal
205
71
  if (isSignal(controlsProp)) {
206
- this.controlsSubscription = (controlsProp as Signal<Controls>).observable.subscribe((controls) => {
72
+ this.controlsSubscription = ((controlsProp as Signal<Controls>).observable as any).subscribe((controls) => {
207
73
  if (controls) {
208
- this.originalControls = controls;
209
- this.setupNavigationControls(controls);
74
+ this.controlsDirective?.onUpdate({ controls }, element);
210
75
  }
211
76
  });
212
77
  }
@@ -223,8 +88,7 @@ export class FocusNavigationDirective extends Directive {
223
88
  if (props.controls !== undefined) {
224
89
  const controlsValue = isSignal(props.controls) ? props.controls() : props.controls;
225
90
  if (controlsValue) {
226
- this.originalControls = controlsValue;
227
- this.setupNavigationControls(controlsValue);
91
+ this.controlsDirective?.onUpdate({ controls: controlsValue }, element);
228
92
  }
229
93
  }
230
94
  }
@@ -243,9 +107,7 @@ export class FocusNavigationDirective extends Directive {
243
107
 
244
108
  this.element = null;
245
109
  this.controlsDirective = null;
246
- this.originalControls = null;
247
110
  }
248
111
  }
249
112
 
250
113
  registerDirective('focusNavigation', FocusNavigationDirective);
251
-
@@ -351,6 +351,7 @@ export class KeyboardControls extends ControlsBase {
351
351
  } | null
352
352
  } = {}
353
353
  private lastKeyPressed: number | null = null
354
+ private lastActionTimes: Record<string, number> = {}
354
355
  private directionState: {
355
356
  up: boolean,
356
357
  down: boolean,
@@ -419,8 +420,16 @@ export class KeyboardControls extends ControlsBase {
419
420
  if (!boundKey) {
420
421
  return;
421
422
  }
422
- const { repeat, keyDown } = boundKey.options;
423
+ const { repeat, keyDown, throttle } = boundKey.options;
423
424
  if ((repeat || count == 0)) {
425
+ if (typeof throttle === "number") {
426
+ const now = Date.now();
427
+ const lastTime = this.lastActionTimes[boundKey.actionName] ?? 0;
428
+ if (now - lastTime < throttle) {
429
+ return;
430
+ }
431
+ this.lastActionTimes[boundKey.actionName] = now;
432
+ }
424
433
  let parameters = boundKey.parameters;
425
434
  if (typeof parameters === "function") {
426
435
  parameters = parameters();
@@ -565,4 +574,4 @@ export class KeyboardControls extends ControlsBase {
565
574
  this.keyState = {}
566
575
  }
567
576
 
568
- }
577
+ }
@@ -30,9 +30,17 @@ export class Scheduler extends Directive {
30
30
  onUpdate(props: any) { }
31
31
 
32
32
  nextTick(timestamp: number) {
33
- this.lastTimestamp = this.lastTimestamp || this.timestamp // first
34
- this.deltaTime = Utils.preciseNow() - this.timestamp
35
- this.timestamp = timestamp
33
+ const now = (typeof timestamp === "number" && timestamp > 0)
34
+ ? timestamp
35
+ : Utils.preciseNow()
36
+ if (this.lastTimestamp === 0) {
37
+ this.lastTimestamp = now
38
+ this.deltaTime = 0
39
+ } else {
40
+ this.deltaTime = now - this.lastTimestamp
41
+ this.lastTimestamp = now
42
+ }
43
+ this.timestamp = now
36
44
  this.tick.set({
37
45
  timestamp: this.timestamp,
38
46
  deltaTime: this.deltaTime,
@@ -98,4 +106,4 @@ export class Scheduler extends Directive {
98
106
  }
99
107
  }
100
108
 
101
- registerDirective('tick', Scheduler)
109
+ registerDirective('tick', Scheduler)