pixifact 0.4.0 → 0.5.0

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 (41) hide show
  1. package/dist/compiler/builtinScenes.d.ts +0 -5
  2. package/dist/compiler/builtinScenes.js +0 -26
  3. package/dist/compiler/commands/index.js +8 -5
  4. package/dist/compiler/compileScenes.d.ts +3 -1
  5. package/dist/compiler/compileScenes.js +32 -2
  6. package/dist/compiler/index.d.ts +0 -1
  7. package/dist/compiler/index.js +0 -1
  8. package/dist/compiler/pixiNodeSchema.d.ts +4 -3
  9. package/dist/compiler/pixiNodeSchema.js +60 -6
  10. package/dist/compiler/sceneValidation.d.ts +3 -1
  11. package/dist/compiler/sceneValidation.js +105 -6
  12. package/dist/compiler/spec.d.ts +18 -7
  13. package/dist/compiler/spec.js +3 -1
  14. package/dist/compiler/templateParser.js +19 -0
  15. package/dist/compiler/templateSerializer.js +7 -1
  16. package/dist/compiler/typescriptCompiler.js +66 -8
  17. package/dist/compiler-node/builtinSceneInterfaces.d.ts +6 -0
  18. package/dist/compiler-node/builtinSceneInterfaces.js +27 -0
  19. package/dist/compiler-node/index.d.ts +2 -0
  20. package/dist/compiler-node/index.js +2 -0
  21. package/dist/{compiler → compiler-node}/scriptInterfaceExtractor.d.ts +1 -1
  22. package/dist/{compiler → compiler-node}/scriptInterfaceExtractor.js +157 -25
  23. package/dist/runtime/BitmapLabel.d.ts +9 -0
  24. package/dist/runtime/BitmapLabel.js +7 -0
  25. package/dist/runtime/Label.d.ts +9 -0
  26. package/dist/runtime/Label.js +7 -0
  27. package/dist/runtime/ScrollContainer.d.ts +2 -0
  28. package/dist/runtime/ScrollContainer.js +12 -2
  29. package/dist/runtime/TextBoxControl.d.ts +48 -0
  30. package/dist/runtime/TextBoxControl.js +162 -0
  31. package/dist/runtime/index.d.ts +2 -0
  32. package/dist/runtime/index.js +2 -0
  33. package/dist/runtime/runtimeBehavior.d.ts +3 -0
  34. package/dist/runtime/runtimeBehavior.js +11 -0
  35. package/dist/scene/decorators.d.ts +2 -1
  36. package/dist/scene/decorators.js +12 -7
  37. package/dist/scene/index.d.ts +1 -0
  38. package/dist/scene/index.js +1 -0
  39. package/dist/scene/sceneBindingRuntime.d.ts +10 -0
  40. package/dist/scene/sceneBindingRuntime.js +124 -0
  41. package/package.json +1 -1
@@ -79,18 +79,24 @@ function serializeAttributes(attributes) {
79
79
  }
80
80
  function flattenAttributes(attributes) {
81
81
  return attributes.flatMap(([name, value]) => {
82
- if (!value || typeof value !== 'object') {
82
+ if (!value || typeof value !== 'object' || isBindingValue(value)) {
83
83
  return [[name, value]];
84
84
  }
85
85
  return Object.entries(value).map(([field, fieldValue]) => [`${name}.${field}`, fieldValue]);
86
86
  });
87
87
  }
88
88
  function formatAttributeValue(name, value) {
89
+ if (isBindingValue(value)) {
90
+ return `{${value.path.join('.')}}`;
91
+ }
89
92
  if (typeof value === 'number' && colorPropNames.has(name)) {
90
93
  return `#${value.toString(16).padStart(6, '0').slice(-6)}`;
91
94
  }
92
95
  return String(value);
93
96
  }
97
+ function isBindingValue(value) {
98
+ return typeof value === 'object' && value.kind === 'binding';
99
+ }
94
100
  function escapeAttribute(value) {
95
101
  return value
96
102
  .replaceAll('&', '&')
@@ -1,14 +1,18 @@
1
- import { pixiSceneDisplayProps, pixiSceneGraphicsProps, pixiSceneLayoutProps, pixiSceneRectProps, pixiSceneSpriteLikeProps, pixiSceneTextStyleProps, pixiSceneTransformProps, } from './pixiNodeSchema.js';
1
+ import { pixiSceneDisplayProps, pixiSceneGraphicsProps, pixiSceneLabelProps, pixiSceneLayoutProps, pixiSceneRectProps, pixiSceneSpriteLikeProps, pixiSceneTextStyleProps, pixiSceneTransformProps, } from './pixiNodeSchema.js';
2
2
  import { compilerSceneNodeLocator } from './commands/index.js';
3
3
  const transformProps = new Set(pixiSceneTransformProps);
4
4
  const layoutProps = new Set(pixiSceneLayoutProps);
5
5
  const pixiProps = new Set(pixiSceneDisplayProps);
6
6
  const spriteProps = new Set(pixiSceneSpriteLikeProps);
7
7
  const graphicsProps = new Set(pixiSceneGraphicsProps);
8
+ const labelProps = new Set(pixiSceneLabelProps);
8
9
  const rectProps = new Set(pixiSceneRectProps);
9
10
  const textStyleProps = new Set(pixiSceneTextStyleProps);
10
- const runtimeNodeTypes = new Set(['Group', 'GridContainer', 'HBoxContainer', 'ScrollContainer', 'VBoxContainer', 'Rect', 'Image', 'NineImage', 'TileImage']);
11
+ const runtimeNodeTypes = new Set(['Group', 'GridContainer', 'HBoxContainer', 'ScrollContainer', 'VBoxContainer', 'Label', 'BitmapLabel', 'Rect', 'Image', 'NineImage', 'TileImage']);
11
12
  const runtimeNodeProps = new Set(['columns', 'gap', 'gapX', 'gapY', 'alignX', 'alignY', 'justify', 'direction', 'scrollX', 'scrollY']);
13
+ function isSceneBindingValue(value) {
14
+ return !!value && typeof value === 'object' && value.kind === 'binding';
15
+ }
12
16
  export function compileSceneTemplateToTs(template, options = {}) {
13
17
  const context = new CompileContext(template, options);
14
18
  return context.compile();
@@ -228,7 +232,8 @@ class CompileContext {
228
232
  const variable = node.id || this.#anonymousName(node.type);
229
233
  const locator = compilerSceneNodeLocator(node, path);
230
234
  const constructorName = this.#sceneConstructorName(node);
231
- this.#lines.push(` const ${variable} = new ${constructorName}();`);
235
+ const constructorArgs = this.#sceneInstanceConstructorArgs(variable, node);
236
+ this.#lines.push(` const ${variable} = new ${constructorName}(${constructorArgs});`);
232
237
  this.#applyPixiProps(variable, node.props, true, node);
233
238
  for (const [name, action] of Object.entries(node.events)) {
234
239
  this.#runtimeImports.add('connectSceneEvent');
@@ -253,7 +258,8 @@ class CompileContext {
253
258
  const variable = node.id || this.#anonymousName(node.type);
254
259
  const locator = compilerSceneNodeLocator(node, path);
255
260
  const constructorName = this.#sceneConstructorName(node);
256
- this.#lines.push(` const ${variable} = new ${constructorName}();`);
261
+ const constructorArgs = this.#sceneInstanceConstructorArgs(variable, node);
262
+ this.#lines.push(` const ${variable} = new ${constructorName}(${constructorArgs});`);
257
263
  this.#applyPixiProps(variable, node.props, true, node);
258
264
  for (const [name, action] of Object.entries(node.events)) {
259
265
  this.#runtimeImports.add('connectSceneEvent');
@@ -282,7 +288,7 @@ class CompileContext {
282
288
  }
283
289
  #constructPixiNode(node) {
284
290
  if (node.type === 'Text' || node.type === 'BitmapText' || node.type === 'HTMLText') {
285
- const text = this.#stringValue(node.props.text ?? '');
291
+ const text = this.#stringValue(isSceneBindingValue(node.props.text) ? '' : node.props.text ?? '');
286
292
  const style = this.#styleObject(node.props);
287
293
  return `new ${node.type}({ text: ${text}${style ? `, style: ${style}` : ''} })`;
288
294
  }
@@ -308,6 +314,31 @@ class CompileContext {
308
314
  #sceneConstructorName(node) {
309
315
  return this.options.sceneClassAliases?.[node.scene] ?? node.type;
310
316
  }
317
+ #sceneInstanceConstructorArgs(variable, node) {
318
+ const sceneInterface = this.options.sceneInterfaces?.[node.scene];
319
+ if (!sceneInterface) {
320
+ return '';
321
+ }
322
+ const entries = [];
323
+ for (const [key, value] of Object.entries(node.props)) {
324
+ const contract = sceneInterface.props[key];
325
+ if (!contract) {
326
+ continue;
327
+ }
328
+ if (isSceneBindingValue(value)) {
329
+ this.#runtimeImports.add('readSceneBindingValue');
330
+ entries.push(`${key}: readSceneBindingValue(root, ${JSON.stringify(value)})`);
331
+ continue;
332
+ }
333
+ if (contract.type === 'struct' && value && typeof value === 'object') {
334
+ const structVariable = this.#createStructValue(variable, key, value, contract);
335
+ entries.push(`${key}: ${structVariable}`);
336
+ continue;
337
+ }
338
+ entries.push(`${key}: ${this.#value(value)}`);
339
+ }
340
+ return entries.length > 0 ? `{ ${entries.join(', ')} }` : '';
341
+ }
311
342
  #sceneStructImports(sceneImport) {
312
343
  const scene = this.#sceneImportScene(sceneImport);
313
344
  if (!scene) {
@@ -378,6 +409,18 @@ class CompileContext {
378
409
  return contract.sourceScene ?? instanceScene;
379
410
  }
380
411
  #applyPixiProps(variable, props, instance = false, sceneInstance, pixiType) {
412
+ const publicProps = new Set(Object.keys(sceneInstance ? this.options.sceneInterfaces?.[sceneInstance.scene]?.props ?? {} : {}));
413
+ const bindingEntries = Object.entries(props).filter((entry) => (isSceneBindingValue(entry[1]) && !publicProps.has(entry[0])));
414
+ props = Object.fromEntries(Object.entries(props).filter(([, value]) => !isSceneBindingValue(value)));
415
+ if (sceneInstance) {
416
+ props = Object.fromEntries(Object.entries(props).filter(([key]) => !publicProps.has(key)));
417
+ for (const [key, value] of Object.entries(sceneInstance.props)) {
418
+ if (publicProps.has(key) && isSceneBindingValue(value)) {
419
+ this.#runtimeImports.add('bindSceneNodeProp');
420
+ this.#lines.push(` bindSceneNodeProp(root, ${JSON.stringify(value)}, ${variable}, ${JSON.stringify(key)});`);
421
+ }
422
+ }
423
+ }
381
424
  const x = props.x;
382
425
  const y = props.y;
383
426
  if (x !== undefined || y !== undefined) {
@@ -413,6 +456,10 @@ class CompileContext {
413
456
  this.#drawGraphics(variable, props);
414
457
  }
415
458
  for (const [key, value] of Object.entries(props)) {
459
+ if (!instance && (pixiType === 'Label' || pixiType === 'BitmapLabel') && labelProps.has(key)) {
460
+ this.#lines.push(` ${variable}.${key} = ${this.#value(value)};`);
461
+ continue;
462
+ }
416
463
  if (!instance && runtimeNodeProps.has(key)) {
417
464
  this.#lines.push(` ${variable}.${key} = ${this.#value(value)};`);
418
465
  continue;
@@ -436,7 +483,7 @@ class CompileContext {
436
483
  if (instance && layoutProps.has(key)) {
437
484
  continue;
438
485
  }
439
- if (instance && value && typeof value === 'object') {
486
+ if (instance && value && typeof value === 'object' && !isSceneBindingValue(value)) {
440
487
  this.#applyStructProp(variable, key, value, sceneInstance);
441
488
  continue;
442
489
  }
@@ -444,6 +491,10 @@ class CompileContext {
444
491
  this.#lines.push(` ${variable}.${key} = ${this.#value(value)};`);
445
492
  }
446
493
  }
494
+ for (const [key, binding] of bindingEntries) {
495
+ this.#runtimeImports.add('bindSceneNodeProp');
496
+ this.#lines.push(` bindSceneNodeProp(root, ${JSON.stringify(binding)}, ${variable}, ${JSON.stringify(key)});`);
497
+ }
447
498
  }
448
499
  #applyRootProps() {
449
500
  const props = { ...this.template.props };
@@ -474,12 +525,16 @@ class CompileContext {
474
525
  this.#lines.push(` ${variable}.${key} = ${this.#value(value)};`);
475
526
  return;
476
527
  }
528
+ const structVariable = this.#createStructValue(variable, key, value, contract);
529
+ this.#lines.push(` ${variable}.${key} = ${structVariable};`);
530
+ }
531
+ #createStructValue(variable, key, value, contract) {
477
532
  const structVariable = `${variable}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
478
533
  this.#lines.push(` const ${structVariable} = new ${contract.struct}();`);
479
534
  for (const [field, fieldValue] of Object.entries(value)) {
480
535
  this.#lines.push(` ${structVariable}.${field} = ${this.#value(fieldValue)};`);
481
536
  }
482
- this.#lines.push(` ${variable}.${key} = ${structVariable};`);
537
+ return structVariable;
483
538
  }
484
539
  #sceneInstanceStructContract(sceneInstance, key) {
485
540
  const contract = sceneInstance ? this.options.sceneInterfaces?.[sceneInstance.scene]?.props[key] : undefined;
@@ -550,7 +605,7 @@ class CompileContext {
550
605
  const entries = [];
551
606
  for (const key of pixiSceneTextStyleProps) {
552
607
  const value = props[key];
553
- if (value !== undefined) {
608
+ if (value !== undefined && !isSceneBindingValue(value)) {
554
609
  entries.push(`${key}: ${key === 'fontWeight' ? JSON.stringify(String(value)) : this.#value(value)}`);
555
610
  }
556
611
  }
@@ -608,6 +663,9 @@ class CompileContext {
608
663
  if (typeof value === 'string') {
609
664
  return JSON.stringify(value);
610
665
  }
666
+ if (isSceneBindingValue(value)) {
667
+ throw new Error(`Scene binding ${value.path.join('.')} must be compiled as a binding target.`);
668
+ }
611
669
  if (value && typeof value === 'object') {
612
670
  return `{ ${Object.entries(value).map(([key, fieldValue]) => `${key}: ${this.#scalarValue(fieldValue)}`).join(', ')} }`;
613
671
  }
@@ -0,0 +1,6 @@
1
+ import type { SceneTemplateInterface } from '../compiler/spec.js';
2
+ import { type BuiltinSceneScriptSources } from '../compiler/builtinScenes.js';
3
+ export declare function builtinSceneInterface(assetId: string, scriptSources: BuiltinSceneScriptSources): SceneTemplateInterface;
4
+ export declare function builtinSceneInterfaces(scriptSources: BuiltinSceneScriptSources): {
5
+ [k: string]: SceneTemplateInterface;
6
+ };
@@ -0,0 +1,27 @@
1
+ import { builtinSceneAssetId, builtinSceneAssetIds, builtinSceneNameFromAssetId, builtinSceneNames, } from '../compiler/builtinScenes.js';
2
+ import { extractSceneScriptInterfaces } from './scriptInterfaceExtractor.js';
3
+ export function builtinSceneInterface(assetId, scriptSources) {
4
+ return builtinSceneInterfaceDescriptors(scriptSources)[assetId].interface;
5
+ }
6
+ export function builtinSceneInterfaces(scriptSources) {
7
+ const descriptors = builtinSceneInterfaceDescriptors(scriptSources);
8
+ return Object.fromEntries(builtinSceneAssetIds().map((assetId) => [assetId, descriptors[assetId].interface]));
9
+ }
10
+ function builtinSceneInterfaceDescriptors(scriptSources) {
11
+ const descriptors = extractSceneScriptInterfaces(builtinSceneNames.map((name) => ({
12
+ scene: builtinSceneAssetId(name),
13
+ fileName: `${name}.ts`,
14
+ source: scriptSources[name],
15
+ })));
16
+ for (const assetId of builtinSceneAssetIds()) {
17
+ const name = builtinSceneNameFromAssetId(assetId);
18
+ const descriptor = descriptors[assetId];
19
+ if (!descriptor) {
20
+ throw new Error(`Built-in Scene "${assetId}" is missing a @scene script.`);
21
+ }
22
+ if (descriptor.className !== name) {
23
+ throw new Error(`Built-in Scene "${assetId}" name must match @scene class "${descriptor.className}".`);
24
+ }
25
+ }
26
+ return descriptors;
27
+ }
@@ -1,3 +1,5 @@
1
1
  export * from '../compiler/compileScenes.js';
2
2
  export * from './builtinSceneAssets.js';
3
+ export * from './builtinSceneInterfaces.js';
4
+ export * from './scriptInterfaceExtractor.js';
3
5
  export * from './vite.js';
@@ -1,3 +1,5 @@
1
1
  export * from '../compiler/compileScenes.js';
2
2
  export * from './builtinSceneAssets.js';
3
+ export * from './builtinSceneInterfaces.js';
4
+ export * from './scriptInterfaceExtractor.js';
3
5
  export * from './vite.js';
@@ -1,4 +1,4 @@
1
- import type { SceneScriptInterface } from './spec.js';
1
+ import type { SceneScriptInterface } from '../compiler/spec.js';
2
2
  export interface ExtractSceneScriptInterfaceOptions {
3
3
  scene: string;
4
4
  }
@@ -15,6 +15,7 @@ function extractSceneScriptClasses(source, fileName, options) {
15
15
  const sourceId = normalizeScriptSourceId(fileName);
16
16
  const imports = collectNamedImports(sourceFile, sourceId);
17
17
  const structClasses = collectStructClasses(sourceFile);
18
+ const variantDeclarations = collectVariantDeclarations(sourceFile);
18
19
  const classes = [];
19
20
  for (const statement of sourceFile.statements) {
20
21
  if (!ts.isClassDeclaration(statement)) {
@@ -42,7 +43,8 @@ function extractSceneScriptClasses(source, fileName, options) {
42
43
  }
43
44
  const prop = memberDecoratorOptions(member, 'prop');
44
45
  if (prop) {
45
- props[name] = propContract(prop, structClasses);
46
+ assertStaticPropMember(member, name);
47
+ props[name] = propContract(prop, member, structClasses, variantDeclarations);
46
48
  }
47
49
  const event = memberDecoratorOptions(member, 'event');
48
50
  if (event) {
@@ -318,6 +320,88 @@ function memberName(name) {
318
320
  }
319
321
  return undefined;
320
322
  }
323
+ function collectVariantDeclarations(sourceFile) {
324
+ const variants = new Map();
325
+ for (const statement of sourceFile.statements) {
326
+ if (!ts.isVariableStatement(statement)) {
327
+ continue;
328
+ }
329
+ for (const declaration of statement.declarationList.declarations) {
330
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
331
+ continue;
332
+ }
333
+ const initializer = unwrapExpression(declaration.initializer);
334
+ if (!ts.isCallExpression(initializer)
335
+ || !ts.isIdentifier(initializer.expression)
336
+ || initializer.expression.text !== 'defineVariants') {
337
+ continue;
338
+ }
339
+ if (initializer.arguments.length !== 1) {
340
+ throw new Error('defineVariants accepts exactly one argument.');
341
+ }
342
+ variants.set(declaration.name.text, variantObjectValue(initializer.arguments[0], 'defineVariants argument'));
343
+ }
344
+ }
345
+ return variants;
346
+ }
347
+ function variantObjectValue(expression, label) {
348
+ const object = unwrapExpression(expression);
349
+ if (!ts.isObjectLiteralExpression(object)) {
350
+ throw new Error(`${label} must be an object literal.`);
351
+ }
352
+ const variants = {};
353
+ let expectedFields;
354
+ for (const property of object.properties) {
355
+ if (!ts.isPropertyAssignment(property)) {
356
+ throw new Error(`${label} only supports property assignments.`);
357
+ }
358
+ const name = memberName(property.name);
359
+ if (!name) {
360
+ throw new Error(`${label} only supports literal case names.`);
361
+ }
362
+ const caseExpression = unwrapExpression(property.initializer);
363
+ if (!ts.isObjectLiteralExpression(caseExpression)) {
364
+ throw new Error(`${label}.${name} must be an object literal.`);
365
+ }
366
+ const fields = {};
367
+ for (const field of caseExpression.properties) {
368
+ if (!ts.isPropertyAssignment(field)) {
369
+ throw new Error(`${label}.${name} only supports property assignments.`);
370
+ }
371
+ const fieldName = memberName(field.name);
372
+ if (!fieldName) {
373
+ throw new Error(`${label}.${name} only supports literal field names.`);
374
+ }
375
+ fields[fieldName] = literalValue(field.initializer, `${label}.${name}.${fieldName}`);
376
+ }
377
+ const fieldTypes = Object.fromEntries(Object.entries(fields).map(([field, value]) => [field, typeof value]));
378
+ if (!expectedFields) {
379
+ expectedFields = fieldTypes;
380
+ }
381
+ else if (!sameFieldTypes(expectedFields, fieldTypes)) {
382
+ throw new Error(`defineVariants case "${name}" must contain the same fields and field types as the first case.`);
383
+ }
384
+ variants[name] = fields;
385
+ }
386
+ if (Object.keys(variants).length === 0) {
387
+ throw new Error('defineVariants requires at least one case.');
388
+ }
389
+ return variants;
390
+ }
391
+ function sameFieldTypes(left, right) {
392
+ const leftEntries = Object.entries(left);
393
+ return leftEntries.length === Object.keys(right).length
394
+ && leftEntries.every(([name, type]) => right[name] === type);
395
+ }
396
+ function unwrapExpression(expression) {
397
+ if (ts.isAsExpression(expression)
398
+ || ts.isTypeAssertionExpression(expression)
399
+ || ts.isSatisfiesExpression(expression)
400
+ || ts.isParenthesizedExpression(expression)) {
401
+ return unwrapExpression(expression.expression);
402
+ }
403
+ return expression;
404
+ }
321
405
  function collectStructClasses(sourceFile) {
322
406
  const classes = new Map();
323
407
  for (const statement of sourceFile.statements) {
@@ -355,65 +439,113 @@ function structFields(node) {
355
439
  }
356
440
  return fields;
357
441
  }
358
- function propContract(prop, structClasses) {
359
- const type = prop.type;
360
- const defaultValue = prop.default;
361
- if (type === 'string' || type === 'number' || type === 'boolean') {
362
- throw new Error('@prop type must be String, Number, Boolean, or a struct class.');
442
+ function assertStaticPropMember(member, name) {
443
+ const isDeclareProperty = ts.isPropertyDeclaration(member)
444
+ && member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DeclareKeyword)
445
+ && !member.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.AccessorKeyword)
446
+ && !member.initializer;
447
+ if (!isDeclareProperty) {
448
+ throw new Error(`@prop "${name}" must decorate a declare property without an initializer.`);
449
+ }
450
+ if (!member.type) {
451
+ throw new Error(`@prop "${name}" requires an explicit TypeScript property type.`);
452
+ }
453
+ }
454
+ function propContract(prop, member, structClasses, variantDeclarations) {
455
+ if (!ts.isPropertyDeclaration(member) || !member.type) {
456
+ throw new Error('@prop requires a typed declare property.');
363
457
  }
364
- if (!isIdentifierValue(type)) {
365
- throw new Error('@prop type must be String, Number, Boolean, or a struct class.');
458
+ if (prop.type !== undefined) {
459
+ throw new Error('@prop type is inferred from the TypeScript property declaration; remove the type option.');
460
+ }
461
+ const defaultValue = prop.default;
462
+ const variantsReference = prop.variants;
463
+ if (variantsReference !== undefined) {
464
+ if (!isIdentifierValue(variantsReference)) {
465
+ throw new Error('@prop variants must reference a local defineVariants declaration.');
466
+ }
467
+ const variants = variantDeclarations.get(variantsReference.text);
468
+ if (!variants) {
469
+ throw new Error(`@prop variants "${variantsReference.text}" must reference a local defineVariants declaration.`);
470
+ }
471
+ if (typeof defaultValue !== 'string' || !variants[defaultValue]) {
472
+ throw new Error('@prop Variant default must name an existing defineVariants case.');
473
+ }
474
+ assertVariantPropertyType(member.type, variantsReference.text);
475
+ return {
476
+ type: 'variant',
477
+ default: defaultValue,
478
+ variants,
479
+ };
366
480
  }
367
- const primitiveType = primitiveConstructorType(type.text);
481
+ const primitiveType = primitivePropertyType(member.type);
368
482
  if (primitiveType) {
369
483
  if (defaultValue !== undefined && (!isSceneTemplateScalarValue(defaultValue) || sceneTemplateScalarType(defaultValue) !== primitiveType)) {
370
- throw new Error(`@prop default for ${type.text} must be a ${primitiveType} literal.`);
484
+ throw new Error(`@prop default must be a ${primitiveType} literal.`);
371
485
  }
372
486
  return {
373
487
  type: primitiveType,
374
488
  ...(defaultValue !== undefined ? { default: defaultValue } : {}),
375
489
  };
376
490
  }
377
- const structClass = structClasses.get(type.text);
491
+ const structName = typeReferenceName(member.type);
492
+ if (!structName) {
493
+ throw new Error('@prop type must be string, number, boolean, a local struct class, or keyof a local defineVariants declaration.');
494
+ }
495
+ const structClass = structClasses.get(structName);
378
496
  if (!structClass) {
379
- throw new Error(`Struct prop type ${type.text} was not found.`);
497
+ throw new Error(`Struct prop type ${structName} was not found.`);
380
498
  }
381
499
  if (!structClass.exported) {
382
- throw new Error(`Struct prop type ${type.text} must be exported.`);
500
+ throw new Error(`Struct prop type ${structName} must be exported.`);
383
501
  }
384
502
  if (defaultValue !== undefined) {
385
503
  throw new Error('@prop default is only supported for primitive props.');
386
504
  }
387
505
  const struct = structContractInfo(structClass.node);
388
506
  if (struct.hasRequiredConstructorParameters) {
389
- throw new Error(`Struct prop type ${type.text} must be constructable with no required parameters.`);
507
+ throw new Error(`Struct prop type ${structName} must be constructable with no required parameters.`);
390
508
  }
391
509
  return {
392
510
  type: 'struct',
393
- struct: type.text,
511
+ struct: structName,
394
512
  fields: struct.fields,
395
513
  };
396
514
  }
397
- function structContractInfo(node) {
398
- return {
399
- fields: structFields(node),
400
- hasRequiredConstructorParameters: node.members.some((member) => ts.isConstructorDeclaration(member) && member.parameters.some((parameter) => !parameter.questionToken && !parameter.initializer)),
401
- };
515
+ function assertVariantPropertyType(type, variantsName) {
516
+ if (!ts.isTypeOperatorNode(type)
517
+ || type.operator !== ts.SyntaxKind.KeyOfKeyword
518
+ || !ts.isTypeQueryNode(type.type)
519
+ || !ts.isIdentifier(type.type.exprName)
520
+ || type.type.exprName.text !== variantsName) {
521
+ throw new Error(`@prop Variant type must be "keyof typeof ${variantsName}".`);
522
+ }
402
523
  }
403
- function primitiveConstructorType(name) {
404
- if (name === 'String') {
524
+ function primitivePropertyType(type) {
525
+ if (type.kind === ts.SyntaxKind.StringKeyword) {
405
526
  return 'string';
406
527
  }
407
- if (name === 'Number') {
528
+ if (type.kind === ts.SyntaxKind.NumberKeyword) {
408
529
  return 'number';
409
530
  }
410
- if (name === 'Boolean') {
531
+ if (type.kind === ts.SyntaxKind.BooleanKeyword) {
411
532
  return 'boolean';
412
533
  }
413
534
  return undefined;
414
535
  }
536
+ function typeReferenceName(type) {
537
+ return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)
538
+ ? type.typeName.text
539
+ : undefined;
540
+ }
541
+ function structContractInfo(node) {
542
+ return {
543
+ fields: structFields(node),
544
+ hasRequiredConstructorParameters: node.members.some((member) => ts.isConstructorDeclaration(member) && member.parameters.some((parameter) => !parameter.questionToken && !parameter.initializer)),
545
+ };
546
+ }
415
547
  function isIdentifierValue(value) {
416
- return ts.isIdentifier(value);
548
+ return typeof value === 'object' && ts.isIdentifier(value);
417
549
  }
418
550
  function isSceneTemplateScalarValue(value) {
419
551
  return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
@@ -0,0 +1,9 @@
1
+ import { BitmapText } from 'pixi.js';
2
+ import { TextBoxControl, type TextBoxAlign, type TextBoxFontWeight, type TextBoxOptions, type TextBoxOverflow } from './TextBoxControl.js';
3
+ export type BitmapLabelAlign = TextBoxAlign;
4
+ export type BitmapLabelOverflow = TextBoxOverflow;
5
+ export type BitmapLabelFontWeight = TextBoxFontWeight;
6
+ export type BitmapLabelOptions = TextBoxOptions;
7
+ export declare class BitmapLabel extends TextBoxControl<BitmapText> {
8
+ constructor(options?: BitmapLabelOptions);
9
+ }
@@ -0,0 +1,7 @@
1
+ import { BitmapText } from 'pixi.js';
2
+ import { TextBoxControl, } from './TextBoxControl.js';
3
+ export class BitmapLabel extends TextBoxControl {
4
+ constructor(options = {}) {
5
+ super(options, (textOptions) => new BitmapText(textOptions));
6
+ }
7
+ }
@@ -0,0 +1,9 @@
1
+ import { Text } from 'pixi.js';
2
+ import { TextBoxControl, type TextBoxAlign, type TextBoxFontWeight, type TextBoxOptions, type TextBoxOverflow } from './TextBoxControl.js';
3
+ export type LabelAlign = TextBoxAlign;
4
+ export type LabelOverflow = TextBoxOverflow;
5
+ export type LabelFontWeight = TextBoxFontWeight;
6
+ export type LabelOptions = TextBoxOptions;
7
+ export declare class Label extends TextBoxControl<Text> {
8
+ constructor(options?: LabelOptions);
9
+ }
@@ -0,0 +1,7 @@
1
+ import { Text } from 'pixi.js';
2
+ import { TextBoxControl, } from './TextBoxControl.js';
3
+ export class Label extends TextBoxControl {
4
+ constructor(options = {}) {
5
+ super(options, (textOptions) => new Text(textOptions));
6
+ }
7
+ }
@@ -1,6 +1,7 @@
1
1
  import { Container, type ContainerChild, type DestroyOptions } from 'pixi.js';
2
2
  import { Control } from './Control.js';
3
3
  import type { GroupOptions } from './Group.js';
4
+ import { runtimeBehavior } from './runtimeBehavior.js';
4
5
  export type ScrollDirection = 'vertical' | 'horizontal' | 'both';
5
6
  export type ScrollContainerOptions = GroupOptions & {
6
7
  direction?: ScrollDirection;
@@ -11,6 +12,7 @@ export declare class ScrollContainer extends Control {
11
12
  #private;
12
13
  readonly contentLayer: Container<ContainerChild>;
13
14
  constructor(options?: ScrollContainerOptions);
15
+ [runtimeBehavior](): void;
14
16
  get direction(): ScrollDirection;
15
17
  set direction(value: ScrollDirection);
16
18
  get scrollX(): number;
@@ -1,5 +1,6 @@
1
1
  import { Container, Graphics, Ticker, } from 'pixi.js';
2
2
  import { Control } from './Control.js';
3
+ import { runtimeBehavior } from './runtimeBehavior.js';
3
4
  const elasticResistance = 0.5;
4
5
  const inertiaFriction = 0.92;
5
6
  const springStrength = 0.18;
@@ -26,19 +27,21 @@ export class ScrollContainer extends Control {
26
27
  this.#direction = direction ?? this.#direction;
27
28
  this.#targetScrollX = scrollX ?? this.#targetScrollX;
28
29
  this.#targetScrollY = scrollY ?? this.#targetScrollY;
29
- this.eventMode = 'static';
30
30
  this.contentLayer.mask = this.#mask;
31
31
  this.contentLayer.on('childAdded', this.#handleContentChildAdded, this);
32
32
  this.contentLayer.on('childRemoved', this.#handleContentChildRemoved, this);
33
33
  super.addChild(this.contentLayer);
34
34
  super.addChild(this.#mask);
35
+ this.layout();
36
+ }
37
+ [runtimeBehavior]() {
38
+ this.eventMode = 'static';
35
39
  this.on('wheel', this.#handleWheel, this);
36
40
  this.on('pointerdown', this.#handlePointerDown, this);
37
41
  this.on('globalpointermove', this.#handleGlobalPointerMove, this);
38
42
  this.on('pointerup', this.#handlePointerUp, this);
39
43
  this.on('pointerupoutside', this.#handlePointerUp, this);
40
44
  this.on('pointercancel', this.#handlePointerUp, this);
41
- this.layout();
42
45
  }
43
46
  get direction() {
44
47
  return this.#direction;
@@ -116,6 +119,13 @@ export class ScrollContainer extends Control {
116
119
  destroy(options) {
117
120
  this.#destroying = true;
118
121
  this.#stopMomentum();
122
+ this.#drag = undefined;
123
+ this.off('wheel', this.#handleWheel, this);
124
+ this.off('pointerdown', this.#handlePointerDown, this);
125
+ this.off('globalpointermove', this.#handleGlobalPointerMove, this);
126
+ this.off('pointerup', this.#handlePointerUp, this);
127
+ this.off('pointerupoutside', this.#handlePointerUp, this);
128
+ this.off('pointercancel', this.#handlePointerUp, this);
119
129
  this.contentLayer.off('childAdded', this.#handleContentChildAdded, this);
120
130
  this.contentLayer.off('childRemoved', this.#handleContentChildRemoved, this);
121
131
  for (const child of this.#watchedContent) {
@@ -0,0 +1,48 @@
1
+ import { BitmapText, Text, type TextOptions } from 'pixi.js';
2
+ import { Control } from './Control.js';
3
+ import type { GroupOptions } from './Group.js';
4
+ export type TextBoxAlign = 'start' | 'center' | 'end';
5
+ export type TextBoxOverflow = 'visible' | 'clip';
6
+ export type TextBoxFontWeight = 400 | 500 | 600 | 700 | '400' | '500' | '600' | '700' | 'bold';
7
+ export type TextBoxOptions = GroupOptions & {
8
+ text?: string;
9
+ fontFamily?: string;
10
+ fontSize?: number;
11
+ fontWeight?: TextBoxFontWeight;
12
+ fill?: number;
13
+ lineHeight?: number;
14
+ letterSpacing?: number;
15
+ wordWrap?: boolean;
16
+ alignX?: TextBoxAlign;
17
+ alignY?: TextBoxAlign;
18
+ overflow?: TextBoxOverflow;
19
+ };
20
+ type TextBoxRenderer = Text | BitmapText;
21
+ export declare class TextBoxControl<T extends TextBoxRenderer> extends Control {
22
+ #private;
23
+ constructor(options: TextBoxOptions, createTextNode: (options: TextOptions) => T);
24
+ get text(): string;
25
+ set text(value: string);
26
+ get fontFamily(): string;
27
+ set fontFamily(value: string);
28
+ get fontSize(): number;
29
+ set fontSize(value: number);
30
+ get fontWeight(): TextBoxFontWeight;
31
+ set fontWeight(value: TextBoxFontWeight);
32
+ get fill(): number;
33
+ set fill(value: number);
34
+ get lineHeight(): number;
35
+ set lineHeight(value: number);
36
+ get letterSpacing(): number;
37
+ set letterSpacing(value: number);
38
+ get wordWrap(): boolean;
39
+ set wordWrap(value: boolean);
40
+ get alignX(): TextBoxAlign;
41
+ set alignX(value: TextBoxAlign);
42
+ get alignY(): TextBoxAlign;
43
+ set alignY(value: TextBoxAlign);
44
+ get overflow(): TextBoxOverflow;
45
+ set overflow(value: TextBoxOverflow);
46
+ layout(): void;
47
+ }
48
+ export {};