lecodes-cli 0.2.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 (48) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +883 -507
  3. package/package.json +9 -4
  4. package/runtime/sdk/canvas/Canvas.ts +270 -0
  5. package/runtime/sdk/compile/aspectMacro.ts +42 -0
  6. package/runtime/sdk/compile/assetMacro.ts +44 -0
  7. package/runtime/sdk/compile/bundler.ts +173 -0
  8. package/runtime/sdk/compile/compileProject.ts +84 -0
  9. package/runtime/sdk/compile/header.ts +46 -0
  10. package/runtime/sdk/compile/index.ts +12 -0
  11. package/runtime/sdk/compile/sourcemap.ts +25 -0
  12. package/runtime/sdk/core/Aspect.ts +82 -26
  13. package/runtime/sdk/core/signals.ts +277 -0
  14. package/runtime/sdk/g2/CharacterController2D.ts +5 -4
  15. package/runtime/sdk/g2/Node2D.ts +1 -1
  16. package/runtime/sdk/g2/Physics2D.ts +22 -12
  17. package/runtime/sdk/g2/Scene2D.ts +30 -0
  18. package/runtime/sdk/g2/{Shape.ts → Shape2D.ts} +10 -10
  19. package/runtime/sdk/g2/Sprite.ts +16 -6
  20. package/runtime/sdk/g2/Texture2D.ts +12 -2
  21. package/runtime/sdk/g2/{Trigger.ts → Trigger2D.ts} +9 -9
  22. package/runtime/sdk/gl/CharacterController.ts +101 -0
  23. package/runtime/sdk/gl/Material.ts +10 -3
  24. package/runtime/sdk/gl/Node.ts +11 -2
  25. package/runtime/sdk/gl/Physics.ts +65 -41
  26. package/runtime/sdk/gl/Scene.ts +18 -0
  27. package/runtime/sdk/gl/Shape.ts +98 -0
  28. package/runtime/sdk/gl/Texture.ts +13 -2
  29. package/runtime/sdk/gl/Trigger.ts +46 -0
  30. package/runtime/sdk/gl/physicsEvents.ts +20 -0
  31. package/runtime/sdk/inject.ts +14 -3
  32. package/runtime/sdk/runtime/camera.ts +31 -0
  33. package/runtime/sdk/runtime/device.ts +26 -0
  34. package/runtime/sdk/runtime/share.ts +9 -0
  35. package/runtime/sdk/ui/UI.ts +5 -1
  36. package/runtime/sdk/ui/UIButton.ts +11 -5
  37. package/runtime/sdk/ui/UIContainer.ts +28 -13
  38. package/runtime/sdk/ui/UIImage.ts +31 -11
  39. package/runtime/sdk/ui/UIInput.ts +10 -0
  40. package/runtime/sdk/ui/UINode.ts +206 -8
  41. package/runtime/sdk/ui/UIScreen.ts +17 -7
  42. package/runtime/sdk/ui/UIScrollable.ts +10 -5
  43. package/runtime/sdk/ui/UISpacer.ts +4 -0
  44. package/runtime/sdk/ui/UIText.ts +21 -5
  45. package/runtime/sdk/ui/UIVideo.ts +5 -0
  46. package/runtime/sdk/ui/UIVirtualizedList.ts +5 -0
  47. package/runtime/sdk/ui/UIWidget.ts +10 -5
  48. package/runtime/sdk/gl/Collider.ts +0 -29
@@ -1,4 +1,5 @@
1
1
  import type { FetchResponse } from "../runtime/fetch"
2
+ import { createBinding, disposeBinding } from "../core/signals"
2
3
 
3
4
  export interface UINode {
4
5
  readonly type: string
@@ -159,12 +160,39 @@ export type TextStyle = {
159
160
  letterSpacing?: UIValue
160
161
  }
161
162
 
163
+ // Pulls function-valued props out of `style` (mutating it — they must never reach the host bridge)
164
+ // and returns them. A static value under the same key overrides (disposes) an existing binding.
165
+ function extractBindings(el: any, style: any): [string, () => any][] | null {
166
+ let fns: [string, () => any][] | null = null
167
+ for (const key of Object.keys(style)) {
168
+ const value = style[key]
169
+ if (typeof value === "function") {
170
+ (fns ??= []).push([key, value])
171
+ delete style[key]
172
+ } else if (el._fx !== undefined) {
173
+ disposeBinding(el, key)
174
+ }
175
+ }
176
+ return fns
177
+ }
178
+
179
+ // One reactive binding per style key: re-runs when a signal the function read changes, writing
180
+ // through the style proxy (so a mounted node hits _creatorUI.updateStyle, an unmounted one just
181
+ // keeps _style current for the next mount).
182
+ function applyBindings(el: any, fns: [string, () => any][]) {
183
+ for (const [ key, fn ] of fns) {
184
+ createBinding(el, key, () => { el.style[key] = fn() })
185
+ }
186
+ }
187
+
162
188
  // Не надо переписывать эту функцию 10 раз, обрати внимание, что у тебя тут возвращается this
163
189
  function styleFunction(this: any, newStyle: any) {
190
+ const fns = extractBindings(this, newStyle)
164
191
  Object.assign(this._style, newStyle)
165
192
  if (this._id !== 0) {
166
193
  _creatorUI.mergeStyle(this._id, newStyle)
167
194
  }
195
+ if (fns) applyBindings(this, fns)
168
196
  return this
169
197
  }
170
198
 
@@ -176,6 +204,14 @@ const handler: ProxyHandler<any> = {
176
204
  return target._el._style[key]
177
205
  },
178
206
  set(target, key: string, value: string | number) {
207
+ if (typeof value === "function") {
208
+ // Reactive binding: `el.style.prop = () => sig.value`. Note a later plain write does NOT
209
+ // dispose the binding (this is the hot path) — override statically via `.style({ prop })`.
210
+ const el = target._el
211
+ const fn = value as unknown as () => any
212
+ createBinding(el, key, () => { el.style[key] = fn() })
213
+ return true
214
+ }
179
215
  if (target._el._id !== 0) {
180
216
  _creatorUI.updateStyle(target._el._id, key, value)
181
217
  }
@@ -194,8 +230,18 @@ function Style() {
194
230
 
195
231
  }
196
232
 
197
- export type StyleFn <R, T extends object> = ((this: R, style: T & { onLandscape?: T, onPortait?: T }) => R)
198
- export type Style <R, T extends object> = StyleFn<R,T> & T & { onLandscape?: T, onPortait?: T }
233
+ // Reactive style input: every primitive-valued prop also accepts a `() => value` binding that
234
+ // re-applies when a signal it read changes. Nested state blocks (onPressed, onLandscape, …) stay
235
+ // static — bindings are extracted at the top level only.
236
+ export type Reactive<T> = { [K in keyof T]: NonNullable<T[K]> extends object ? T[K] : T[K] | (() => T[K]) }
237
+
238
+ // User-defined style classes: any `$`-prefixed key in .style() declares a state block (like
239
+ // onPressed, but with any name), toggled from code via setClass/toggleClass. `duration`/`delay`
240
+ // make the swap transition.
241
+ export type ClassStyles<T> = { [key: `$${string}`]: T & { duration?: number, delay?: number } }
242
+
243
+ export type StyleFn <R, T extends object> = ((this: R, style: Reactive<T> & { onLandscape?: T, onPortait?: T } & ClassStyles<DrawableStyle & BaseStyle>) => R)
244
+ export type Style <R, T extends object> = StyleFn<R,T> & T & { onLandscape?: T, onPortait?: T } & ClassStyles<DrawableStyle & BaseStyle>
199
245
 
200
246
  export type AnimateStyle<R, T extends object> = ((this: R, style: T & { duration?: number, delay?: number, layer?: number, commit?: boolean }) => R)
201
247
 
@@ -216,6 +262,9 @@ export class Element<T extends string> {
216
262
  protected _styleProxy: any
217
263
  protected _appear: any
218
264
  protected _disappear: any
265
+ /** @internal Reactive bindings owned by this element (style key / "#text" → effect). The element
266
+ * is the effect's only strong owner, so discarding a subtree lets its bindings collect with it. */
267
+ _fx?: Map<string, any>
219
268
 
220
269
  constructor(type: T, style: any) {
221
270
  this.type = type
@@ -224,6 +273,16 @@ export class Element<T extends string> {
224
273
  delete style.name
225
274
  }
226
275
  this._style = style
276
+ if (style) {
277
+ const fns = extractBindings(this, style)
278
+ if (fns) applyBindings(this, fns) // sync first run — _style holds concrete values pre-mount
279
+ }
280
+ // Creation-site capture for the editor preview's element inspector: the host sets the flag
281
+ // before running the bundle, and the stack (mapped through the bundle's source map) points at
282
+ // the user code that created this element. Off everywhere else — zero cost when unset.
283
+ if ((globalThis as any).__lecodesCaptureSites) {
284
+ (this as any)._site = new Error("lecodes-site")
285
+ }
227
286
  }
228
287
 
229
288
  get style(): any {
@@ -237,18 +296,53 @@ export class Element<T extends string> {
237
296
  return proxy
238
297
  }
239
298
 
240
- animateTo(style: any): this{
299
+ animateTo(style: any): this{
241
300
  _creatorUI.animateTo(this._id, style)
242
301
  if (style.commit !== false) {
243
302
  Object.assign(this._style, style)
244
303
  }
245
304
  return this
246
305
  }
247
- animateFrom(style: any): this{
306
+ animateFrom(style: any): this{
248
307
  _creatorUI.animateFrom(this._id, style)
249
308
  return this
250
309
  }
251
310
 
311
+ /** @internal Active style-class names (stored without the `$`). Kept on the element — not just in
312
+ * the layout engine — so hosts re-apply them when a detached node is re-mounted (screens
313
+ * re-assign _id on every open). */
314
+ protected _classes?: Set<string>
315
+
316
+ /** Activate/deactivate a `$`-class declared in .style() (accepts the name with or without `$`). */
317
+ setClass(name: string, enabled: boolean): this {
318
+ if (name[0] === "$") name = name.slice(1)
319
+ if (enabled) {
320
+ (this._classes ??= new Set()).add(name)
321
+ } else {
322
+ this._classes?.delete(name)
323
+ }
324
+ if (this._id !== 0) {
325
+ _creatorUI.setClass(this._id, name, enabled)
326
+ }
327
+ return this
328
+ }
329
+
330
+ toggleClass(name: string): this {
331
+ return this.setClass(name, !this.hasClass(name))
332
+ }
333
+
334
+ hasClass(name: string): boolean {
335
+ return this._classes?.has(name[0] === "$" ? name.slice(1) : name) ?? false
336
+ }
337
+
338
+ /** Reactive class binding: keeps the `$`-class active exactly while `fn()` is true — re-runs
339
+ * when a signal `fn` read changes. Re-binding the same name replaces the previous binding. */
340
+ bindClass(name: string, fn: () => boolean): this {
341
+ if (name[0] === "$") name = name.slice(1)
342
+ createBinding(this, "#class:" + name, () => { this.setClass(name, !!fn()) })
343
+ return this
344
+ }
345
+
252
346
  protected ll?: OnLayoutCallback[]
253
347
  onLayout(onLayout: OnLayoutCallback): this {
254
348
  if (this.ll) {
@@ -260,14 +354,113 @@ export class Element<T extends string> {
260
354
  }
261
355
  }
262
356
 
357
+ // ---- reactive children --------------------------------------------------------------------------
358
+
359
+ export type ChildrenFn = () => UINodeChild[]
360
+
361
+ // The container whose children binding is currently executing. __uiMap uses it as the cache owner;
362
+ // nested bindings save/restore it, so each level caches on its own element.
363
+ let currentMapOwner: any = null
364
+
365
+ /**
366
+ * Compiler-emitted memoized map for reactive children. The chisel `reactiveUi` pass rewrites
367
+ * `list.map(fn)` inside a children binding to `__uiMap(list, fn, "<file>:<offset>")`, so unchanged
368
+ * items keep their element identity across re-runs (the reconciler then leaves them mounted).
369
+ * Cache: per (binding owner, call-site slot), keyed by item reference; duplicate items consume
370
+ * cached nodes in order. Outside a children binding, for non-array inputs, or for non-element
371
+ * results it behaves exactly like Array.prototype.map — a wrong rewrite can't change behavior.
372
+ */
373
+ export function __uiMap(list: any, render: (item: any, index: number) => any, slot: string): any[] {
374
+ if (currentMapOwner === null || !Array.isArray(list)) {
375
+ return list?.map(render) ?? []
376
+ }
377
+ const slots: Map<string, Map<any, any[]>> = currentMapOwner._mapCache ??= new Map()
378
+ const prev = slots.get(slot)
379
+ const next = new Map<any, any[]>()
380
+ const out: any[] = []
381
+ for (let i = 0; i < list.length; i++) {
382
+ const item = list[i]
383
+ const stack = prev?.get(item)
384
+ let node = stack !== undefined && stack.length > 0 ? stack.shift() : undefined
385
+ if (node === undefined) {
386
+ node = render(item, i)
387
+ if (!(node instanceof Element)) {
388
+ out.push(node) // not a UI element — plain map semantics, never cached
389
+ continue
390
+ }
391
+ }
392
+ let arr = next.get(item)
393
+ if (arr === undefined) next.set(item, arr = [])
394
+ arr.push(node)
395
+ out.push(node)
396
+ }
397
+ // entries left in `prev` (items gone from the list) are dropped here; their nodes and bindings
398
+ // become unreachable and collect
399
+ slots.set(slot, next)
400
+ return out
401
+ }
402
+
263
403
  export class ContainerElement<T extends string> extends Element<T> {
264
404
  children: any[] = []
265
405
 
266
- constructor(type: T, style: any, children: any[]) {
406
+ constructor(type: T, style: any, children: any[] | ChildrenFn) {
267
407
  super(type, style)
268
- this.children = children
408
+ if (typeof children === "function") {
409
+ this._bindChildren(children)
410
+ } else {
411
+ this.children = children
412
+ }
269
413
  }
270
-
414
+
415
+ /** @internal Reactive children: re-runs when a signal read inside `fn` changes and reconciles
416
+ * the result against the mounted children with minimal insert/remove ops. */
417
+ _bindChildren(fn: ChildrenFn) {
418
+ createBinding(this, "#children", () => {
419
+ const prevOwner = currentMapOwner
420
+ currentMapOwner = this
421
+ let next: UINodeChild[]
422
+ try {
423
+ next = fn()
424
+ } finally {
425
+ currentMapOwner = prevOwner
426
+ }
427
+ this._reconcile(Array.isArray(next) ? next : [])
428
+ })
429
+ }
430
+
431
+ /** @internal Identity diff of the current children against `next`: departed nodes are removed,
432
+ * new ones inserted at their position, kept nodes stay mounted (so their host state survives).
433
+ * A moved node is remove+insert — it remounts. Falsy entries are compacted away. */
434
+ _reconcile(next: UINodeChild[]) {
435
+ const target: any[] = []
436
+ for (const c of next) if (c) target.push(c)
437
+
438
+ if (this._id === 0) {
439
+ this.children.length = 0
440
+ this.children.push(...target)
441
+ return
442
+ }
443
+
444
+ // compact holes left by a previous static children array (falsy entries were never mounted)
445
+ for (let i = this.children.length - 1; i >= 0; i--) {
446
+ if (!this.children[i]) this.children.splice(i, 1)
447
+ }
448
+
449
+ const targetSet = new Set(target)
450
+ const departed = this.children.filter(c => !targetSet.has(c))
451
+ if (departed.length > 0) this.remove(...departed)
452
+
453
+ // this.children now mirrors the mounted order; walk the target and fix mismatches in place
454
+ for (let i = 0; i < target.length; i++) {
455
+ if (this.children[i] === target[i]) continue
456
+ const node = target[i]
457
+ if (this.children.includes(node, i + 1)) {
458
+ this.remove(node) // moved forward — remount at its new position
459
+ }
460
+ this.insert(i, node)
461
+ }
462
+ }
463
+
271
464
  append(...nodes: UINodeChild[]): this {
272
465
  if (this._id === 0) {
273
466
  this.children.push(...nodes)
@@ -331,7 +524,12 @@ export class ContainerElement<T extends string> extends Element<T> {
331
524
  return this
332
525
  }
333
526
 
334
- setContent(children: UINodeChild[]): this {
527
+ setContent(children: UINodeChild[] | ChildrenFn): this {
528
+ if (typeof children === "function") {
529
+ this._bindChildren(children) // createBinding replaces a previous "#children" binding
530
+ return this
531
+ }
532
+ disposeBinding(this, "#children") // static content overrides a reactive binding
335
533
  if (this._id !== 0) {
336
534
  _creatorUI.setContent(this._id, this.children, children)
337
535
  }
@@ -1,9 +1,14 @@
1
1
  import { TouchStartEvent } from "../runtime/touch"
2
- import { ContainerElement, type AnimateStyle, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type PaddingStyle, type ScrollStyle, type Style, type UINodeChild, } from "./UINode"
2
+ import { ContainerElement, type AnimateStyle, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type PaddingStyle, type ScrollStyle, type Style, type UINodeChild, type ChildrenFn, } from "./UINode"
3
3
 
4
4
  export type UIScreenStyle = ContainerStyle & DrawableStyle & PaddingStyle & ScrollStyle & BaseStyle & ScrollStyle & { refreshControlColor?: Color }
5
5
 
6
- export interface UIScreen <Scrollable extends boolean = boolean> {
6
+ // Default to `false`: a bare `UIScreen` means the non-scrollable screen the `UIScreen(...)`
7
+ // constructor returns, so `const s: UIScreen = UIScreen([...])` type-checks. The type is invariant
8
+ // in `Scrollable` (the fluent `style()` puts `this` in a contravariant position), so a `boolean`
9
+ // default wouldn't accept the constructor's `UIScreen<false>`. Opt into scrolling via
10
+ // `.makeScrollable(): UIScreen<true>`; write `UIScreen<boolean>` explicitly when you need the union.
11
+ export interface UIScreen <Scrollable extends boolean = false> {
7
12
  readonly type: "screen",
8
13
  scrollable: Scrollable,
9
14
  style: Style<this, UIScreenStyle>,
@@ -16,7 +21,7 @@ export interface UIScreen <Scrollable extends boolean = boolean> {
16
21
  append(...nodes: UINodeChild[]): this
17
22
  insert(index: number, ...nodes: UINodeChild[]): this
18
23
  remove(...nodes: UINodeChild[]): this
19
- setContent(nodes: UINodeChild[]): this
24
+ setContent(nodes: UINodeChild[] | ChildrenFn): this
20
25
 
21
26
  readonly children: UINodeChild[],
22
27
 
@@ -31,7 +36,12 @@ export interface UIScreen <Scrollable extends boolean = boolean> {
31
36
 
32
37
  onLayout(onLayout: OnLayoutCallback): this
33
38
 
34
- onTouchStart(callback: (ev: TouchStartEvent<UIScreen>) => void): this,
39
+ setClass(name: string, enabled: boolean): this
40
+ toggleClass(name: string): this
41
+ hasClass(name: string): boolean
42
+ bindClass(name: string, fn: () => boolean): this
43
+
44
+ onTouchStart(callback: (ev: TouchStartEvent<UIScreen<boolean>>) => void): this,
35
45
  }
36
46
 
37
47
  export class ScreenElement extends ContainerElement<"screen"> {
@@ -104,14 +114,14 @@ export class ScreenElement extends ContainerElement<"screen"> {
104
114
  }
105
115
 
106
116
  export function UIScreen(): UIScreen<false>;
107
- export function UIScreen(children: UINodeChild[]): UIScreen<false>;
117
+ export function UIScreen(children: UINodeChild[] | ChildrenFn): UIScreen<false>;
108
118
  export function UIScreen(style: UIScreenStyle): UIScreen<false>;
109
119
  export function UIScreen(style: UIScreenStyle, children: UINodeChild[]): UIScreen<false>;
110
- export function UIScreen(...args: [] | [UINodeChild[] | UIScreenStyle] | [UIScreenStyle, children: UINodeChild[]]): UIScreen<false> {
120
+ export function UIScreen(...args: [] | [UINodeChild[] | ChildrenFn | UIScreenStyle] | [UIScreenStyle, children: UINodeChild[] | ChildrenFn]): UIScreen<false> {
111
121
  if (args.length === 0) {
112
122
  return new ScreenElement("screen", {}, [])
113
123
  } else if (args.length === 1) {
114
- if (Array.isArray(args[0])) {
124
+ if (Array.isArray(args[0]) || typeof args[0] === "function") {
115
125
  return new ScreenElement("screen", {}, args[0])
116
126
  } else {
117
127
  return new ScreenElement("screen", args[0], [])
@@ -1,5 +1,5 @@
1
1
  import type { UIContainerStyle } from "./UIContainer"
2
- import { type Style, type AnimateStyle, type UINodeChild, type OnLayoutCallback, ContainerElement } from "./UINode"
2
+ import { type Style, type AnimateStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, ContainerElement } from "./UINode"
3
3
 
4
4
  export type ScrollStyle = { showScrollbar?: boolean, overscrollMode?: "none" | "absorb" | "default" }
5
5
 
@@ -14,10 +14,15 @@ export interface UIScrollable {
14
14
  append(...nodes: UINodeChild[]): UIScrollable
15
15
  insert(index: number, ...nodes: UINodeChild[]): UIScrollable
16
16
  remove(...nodes: UINodeChild[]): UIScrollable
17
- setContent(nodes: UINodeChild[]): this
17
+ setContent(nodes: UINodeChild[] | ChildrenFn): this
18
18
 
19
19
  onLayout(onLayout: OnLayoutCallback): this
20
20
 
21
+ setClass(name: string, enabled: boolean): this
22
+ toggleClass(name: string): this
23
+ hasClass(name: string): boolean
24
+ bindClass(name: string, fn: () => boolean): this
25
+
21
26
  onScroll(callback: (scrollPosition: number) => void): UIScrollable,
22
27
  onScrollRelease(callback: () => void): UIScrollable,
23
28
  onOverscroll(callback: (delta: number) => void): UIScrollable
@@ -52,14 +57,14 @@ class ScrollableElement extends ContainerElement<"scrollable"> {
52
57
  }
53
58
 
54
59
  export function UIScrollable(): UIScrollable
55
- export function UIScrollable(children: UINodeChild[]): UIScrollable
60
+ export function UIScrollable(children: UINodeChild[] | ChildrenFn): UIScrollable
56
61
  export function UIScrollable(style: UIScrollableStyle): UIScrollable
57
62
  export function UIScrollable(style: UIScrollableStyle, children: UINodeChild[]): UIScrollable
58
- export function UIScrollable(...args: [] | [UINodeChild[] | UIScrollableStyle] | [UIScrollableStyle, children: UINodeChild[]]): UIScrollable {
63
+ export function UIScrollable(...args: [] | [UINodeChild[] | ChildrenFn | UIScrollableStyle] | [UIScrollableStyle, children: UINodeChild[] | ChildrenFn]): UIScrollable {
59
64
  if (args.length === 0) {
60
65
  return new ScrollableElement("scrollable", {}, [])
61
66
  } else if (args.length === 1) {
62
- if (Array.isArray(args[0])) {
67
+ if (Array.isArray(args[0]) || typeof args[0] === "function") {
63
68
  return new ScrollableElement("scrollable", {}, args[0])
64
69
  } else {
65
70
  return new ScrollableElement("scrollable", args[0], [])
@@ -6,6 +6,10 @@ export interface UISpacer {
6
6
  readonly type: "spacer",
7
7
  style: Style<this, UISpacerStyle>,
8
8
 
9
+ setClass(name: string, enabled: boolean): this
10
+ toggleClass(name: string): this
11
+ hasClass(name: string): boolean
12
+ bindClass(name: string, fn: () => boolean): this
9
13
  }
10
14
  export function UISpacer(): UISpacer
11
15
  export function UISpacer(style: UISpacerStyle): UISpacer
@@ -1,3 +1,4 @@
1
+ import { createBinding } from "../core/signals"
1
2
  import { Element, type AnimateStyle, type BaseStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type Style, type TextStyle } from "./UINode"
2
3
 
3
4
  export type UITextStyle = ElementStyle & TextStyle
@@ -10,13 +11,28 @@ export interface UIText {
10
11
  animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle & TextStyle>
11
12
 
12
13
  onLayout(onLayout: OnLayoutCallback): this
14
+
15
+ setClass(name: string, enabled: boolean): this
16
+ toggleClass(name: string): this
17
+ hasClass(name: string): boolean
18
+ bindClass(name: string, fn: () => boolean): this
13
19
  }
14
20
 
15
21
  export class TextElement extends Element<"text"> {
16
22
  private _text: string
17
- constructor(style: any, text: string) {
23
+ constructor(style: any, text: string | (() => string)) {
18
24
  super("text", style)
19
- this._text = text
25
+ if (typeof text === "function") {
26
+ // Reactive content: re-runs when a signal `text()` read changes; the setter routes to
27
+ // _creatorUI.updateNode when mounted and just keeps _text current when not.
28
+ this._text = ""
29
+ createBinding(this, "#text", () => {
30
+ const value = text()
31
+ if (value !== this._text) this.text = value
32
+ })
33
+ } else {
34
+ this._text = text
35
+ }
20
36
  }
21
37
  get text(): string {
22
38
  return this._text
@@ -29,9 +45,9 @@ export class TextElement extends Element<"text"> {
29
45
  }
30
46
  }
31
47
 
32
- export function UIText(text: string): UIText;
33
- export function UIText(style: UITextStyle, text: string): UIText;
34
- export function UIText(...args: [string] | [UITextStyle, string]): UIText {
48
+ export function UIText(text: string | (() => string)): UIText;
49
+ export function UIText(style: UITextStyle, text: string | (() => string)): UIText;
50
+ export function UIText(...args: [string | (() => string)] | [UITextStyle, string | (() => string)]): UIText {
35
51
  if (args.length === 1) {
36
52
  return new TextElement({}, args[0])
37
53
  } else {
@@ -11,6 +11,11 @@ export interface UIVideo {
11
11
  animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
12
12
 
13
13
  onLayout(onLayout: OnLayoutCallback): this
14
+
15
+ setClass(name: string, enabled: boolean): this
16
+ toggleClass(name: string): this
17
+ hasClass(name: string): boolean
18
+ bindClass(name: string, fn: () => boolean): this
14
19
  }
15
20
 
16
21
  class VideoElement extends Element<"video"> {
@@ -45,6 +45,11 @@ export interface UIVirtualizedList<T = unknown> {
45
45
  /** Порог от верхнего края — подгрузка истории в чате */
46
46
  onStartReached(thresholdPx: number, callback: () => void): this
47
47
  onLayout(onLayout: OnLayoutCallback): this
48
+
49
+ setClass(name: string, enabled: boolean): this
50
+ toggleClass(name: string): this
51
+ hasClass(name: string): boolean
52
+ bindClass(name: string, fn: () => boolean): this
48
53
  }
49
54
 
50
55
  export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedList<T> {
@@ -1,5 +1,5 @@
1
1
  import { TouchStartEvent } from "../runtime/touch"
2
- import { ContainerElement, type AnimateStyle, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type PaddingStyle, type PositionStyle, type Style, type UINodeChild } from "./UINode"
2
+ import { ContainerElement, type AnimateStyle, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type PaddingStyle, type PositionStyle, type Style, type UINodeChild, type ChildrenFn } from "./UINode"
3
3
 
4
4
 
5
5
  export type UIWidgetStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle & PositionStyle & {
@@ -23,7 +23,7 @@ export interface UIWidget {
23
23
  append(...nodes: UINodeChild[]): UIWidget
24
24
  insert(index: number, ...nodes: UINodeChild[]): UIWidget
25
25
  remove(...nodes: UINodeChild[]): UIWidget
26
- setContent(nodes: UINodeChild[]): this
26
+ setContent(nodes: UINodeChild[] | ChildrenFn): this
27
27
 
28
28
  readonly children: UINodeChild[],
29
29
 
@@ -31,6 +31,11 @@ export interface UIWidget {
31
31
  onTouchStart(callback: (ev: TouchStartEvent<UIWidget>) => void): this,
32
32
  onOverlayTap(callback: () => void): this,
33
33
  onBackPressed(callback: () => void): this
34
+
35
+ setClass(name: string, enabled: boolean): this
36
+ toggleClass(name: string): this
37
+ hasClass(name: string): boolean
38
+ bindClass(name: string, fn: () => boolean): this
34
39
  }
35
40
 
36
41
 
@@ -79,14 +84,14 @@ export class WidgetElement extends ContainerElement<"widget"> {
79
84
 
80
85
 
81
86
  export function UIWidget(): UIWidget;
82
- export function UIWidget(children: UINodeChild[]): UIWidget;
87
+ export function UIWidget(children: UINodeChild[] | ChildrenFn): UIWidget;
83
88
  export function UIWidget(style: UIWidgetStyle): UIWidget;
84
89
  export function UIWidget(style: UIWidgetStyle, children: UINodeChild[]): UIWidget;
85
- export function UIWidget(...args: [] | [UINodeChild[] | UIWidgetStyle] | [UIWidgetStyle, children: UINodeChild[]]): UIWidget {
90
+ export function UIWidget(...args: [] | [UINodeChild[] | ChildrenFn | UIWidgetStyle] | [UIWidgetStyle, children: UINodeChild[] | ChildrenFn]): UIWidget {
86
91
  if (args.length === 0) {
87
92
  return new WidgetElement("widget", {}, [])
88
93
  } else if (args.length === 1) {
89
- if (Array.isArray(args[0])) {
94
+ if (Array.isArray(args[0]) || typeof args[0] === "function") {
90
95
  return new WidgetElement("widget", {}, args[0])
91
96
  } else {
92
97
  return new WidgetElement("widget", args[0], [])
@@ -1,29 +0,0 @@
1
- // A 3D collider for click/touch hit-testing, as an aspect. Derives a box from the mesh by default.
2
- // (3D is the default engine, so this is the unsuffixed Collider; the 2D one is Collider2D.)
3
- // node.aspect(Collider, { shape: 'box' })
4
- // node.aspect(Collider, { shape: 'box', center: [0,1,0], size: [2,2,2] })
5
-
6
- import { Aspect } from "../core/Aspect"
7
- import { cx, cy, cz, type Vec3Like } from "../math/vec"
8
- import type { Node } from "./Node"
9
-
10
- export class Collider extends Aspect<"collider", Node> {
11
- static readonly aspect = "collider"
12
- shape: "box" | "sphere" = "box"
13
- /** Explicit box: center + full size. */
14
- center?: Vec3Like
15
- size?: Vec3Like
16
- /** Explicit sphere: center + radius. */
17
- radius?: number
18
-
19
- onAttach(): void {
20
- const id = this.node.id
21
- if (this.shape === "box" && this.center && this.size) {
22
- _creator.setColliderBox(id, cx(this.center), cy(this.center), cz(this.center), cx(this.size), cy(this.size), cz(this.size))
23
- } else if (this.shape === "sphere" && this.center && this.radius !== undefined) {
24
- _creator.setColliderSphere(id, cx(this.center), cy(this.center), cz(this.center), this.radius)
25
- } else {
26
- _creator.setColliderFromMesh(id, id, this.shape === "sphere" ? 1 : 0)
27
- }
28
- }
29
- }