lecodes-cli 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +889 -507
  3. package/package.json +9 -4
  4. package/runtime/sdk/canvas/Canvas.ts +105 -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 +96 -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 +2 -2
  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 +12 -0
  18. package/runtime/sdk/g2/{Shape.ts → Shape2D.ts} +10 -10
  19. package/runtime/sdk/g2/Sprite.ts +7 -2
  20. package/runtime/sdk/g2/Texture2D.ts +4 -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/Node.ts +11 -2
  24. package/runtime/sdk/gl/Physics.ts +65 -41
  25. package/runtime/sdk/gl/Scene.ts +18 -0
  26. package/runtime/sdk/gl/Shape.ts +98 -0
  27. package/runtime/sdk/gl/Texture.ts +2 -2
  28. package/runtime/sdk/gl/Trigger.ts +46 -0
  29. package/runtime/sdk/gl/physicsEvents.ts +20 -0
  30. package/runtime/sdk/inject.ts +12 -5
  31. package/runtime/sdk/runtime/camera.ts +31 -0
  32. package/runtime/sdk/runtime/device.ts +26 -0
  33. package/runtime/sdk/runtime/share.ts +9 -0
  34. package/runtime/sdk/ui/UI.ts +5 -1
  35. package/runtime/sdk/ui/UIButton.ts +11 -5
  36. package/runtime/sdk/ui/UIContainer.ts +28 -13
  37. package/runtime/sdk/ui/UIImage.ts +5 -0
  38. package/runtime/sdk/ui/UIInput.ts +10 -0
  39. package/runtime/sdk/ui/UINode.ts +206 -8
  40. package/runtime/sdk/ui/UIScreen.ts +10 -5
  41. package/runtime/sdk/ui/UIScrollable.ts +10 -5
  42. package/runtime/sdk/ui/UISpacer.ts +4 -0
  43. package/runtime/sdk/ui/UIText.ts +21 -5
  44. package/runtime/sdk/ui/UIVideo.ts +5 -0
  45. package/runtime/sdk/ui/UIVirtualizedList.ts +5 -0
  46. package/runtime/sdk/ui/UIWidget.ts +10 -5
  47. package/runtime/sdk/canvas/Label.ts +0 -57
  48. package/runtime/sdk/gl/Collider.ts +0 -29
@@ -1,4 +1,4 @@
1
- import { ContainerElement, type AnimateStyle, type AppearStyle, type ContainerStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type Style, type StyleFn, type UINode, type UINodeChild } from "./UINode"
1
+ import { ContainerElement, type AnimateStyle, type AppearStyle, type ChildrenFn, type ContainerStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type Style, type StyleFn, type UINode, type UINodeChild } from "./UINode"
2
2
 
3
3
  export type UIContainerStyle = ElementStyle & DrawableStyle & ContainerStyle
4
4
 
@@ -11,21 +11,26 @@ export interface UIColumn {
11
11
  append(...nodes: UINodeChild[]): UIColumn
12
12
  insert(index: number, ...nodes: UINodeChild[]): UIColumn
13
13
  remove(...nodes: UINodeChild[]): UIColumn
14
- setContent(nodes: UINodeChild[]): this
14
+ setContent(nodes: UINodeChild[] | ChildrenFn): this
15
15
 
16
16
  onLayout(onLayout: OnLayoutCallback): this
17
17
 
18
+ setClass(name: string, enabled: boolean): this
19
+ toggleClass(name: string): this
20
+ hasClass(name: string): boolean
21
+ bindClass(name: string, fn: () => boolean): this
22
+
18
23
  readonly children: UINodeChild[]
19
24
  }
20
25
  export function UIColumn(): UIColumn
21
- export function UIColumn(children: UINodeChild[]): UIColumn
26
+ export function UIColumn(children: UINodeChild[] | ChildrenFn): UIColumn
22
27
  export function UIColumn(style: UIContainerStyle): UIColumn
23
28
  export function UIColumn(style: UIContainerStyle, children: UINodeChild[]): UIColumn
24
- export function UIColumn(...args: [] | [UINodeChild[] | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[]]): UIColumn {
29
+ export function UIColumn(...args: [] | [UINodeChild[] | ChildrenFn | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[] | ChildrenFn]): UIColumn {
25
30
  if (args.length === 0) {
26
31
  return new ContainerElement("column", {}, [])
27
32
  } else if (args.length === 1) {
28
- if (Array.isArray(args[0])) {
33
+ if (Array.isArray(args[0]) || typeof args[0] === "function") {
29
34
  return new ContainerElement("column", {}, args[0])
30
35
  } else {
31
36
  return new ContainerElement("column", args[0], [])
@@ -44,20 +49,25 @@ export interface UIRow {
44
49
  append(...nodes: UINodeChild[]): UIRow
45
50
  insert(index: number, ...nodes: UINodeChild[]): UIRow
46
51
  remove(...nodes: UINodeChild[]): UIRow
47
- setContent(nodes: UINodeChild[]): this
52
+ setContent(nodes: UINodeChild[] | ChildrenFn): this
48
53
 
49
54
  onLayout(onLayout: OnLayoutCallback): this
50
55
 
56
+ setClass(name: string, enabled: boolean): this
57
+ toggleClass(name: string): this
58
+ hasClass(name: string): boolean
59
+ bindClass(name: string, fn: () => boolean): this
60
+
51
61
  readonly children: UINodeChild[],
52
62
  }
53
- export function UIRow(children: UINodeChild[]): UIRow
63
+ export function UIRow(children: UINodeChild[] | ChildrenFn): UIRow
54
64
  export function UIRow(style: UIContainerStyle): UIRow
55
65
  export function UIRow(style: UIContainerStyle, children: UINodeChild[]): UIRow
56
- export function UIRow(...args: [] | [UINodeChild[] | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[]]): UIRow {
66
+ export function UIRow(...args: [] | [UINodeChild[] | ChildrenFn | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[] | ChildrenFn]): UIRow {
57
67
  if (args.length === 0) {
58
68
  return new ContainerElement("row", {}, [])
59
69
  } else if (args.length === 1) {
60
- if (Array.isArray(args[0])) {
70
+ if (Array.isArray(args[0]) || typeof args[0] === "function") {
61
71
  return new ContainerElement("row", {}, args[0])
62
72
  } else {
63
73
  return new ContainerElement("row", args[0], [])
@@ -76,20 +86,25 @@ export interface UIBox {
76
86
  append(...nodes: UINodeChild[]): this
77
87
  insert(index: number, ...nodes: UINodeChild[]): this
78
88
  remove(...nodes: UINodeChild[]): this
79
- setContent(nodes: UINodeChild[]): this
89
+ setContent(nodes: UINodeChild[] | ChildrenFn): this
80
90
 
81
91
  onLayout(onLayout: OnLayoutCallback): this
82
92
 
93
+ setClass(name: string, enabled: boolean): this
94
+ toggleClass(name: string): this
95
+ hasClass(name: string): boolean
96
+ bindClass(name: string, fn: () => boolean): this
97
+
83
98
  readonly children: UINodeChild[],
84
99
  }
85
- export function UIBox(children: UINodeChild[]): UIBox
100
+ export function UIBox(children: UINodeChild[] | ChildrenFn): UIBox
86
101
  export function UIBox(style: UIContainerStyle): UIBox
87
102
  export function UIBox(style: UIContainerStyle, children: UINodeChild[]): UIBox
88
- export function UIBox(...args: [] | [UINodeChild[] | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[]]): UIBox {
103
+ export function UIBox(...args: [] | [UINodeChild[] | ChildrenFn | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[] | ChildrenFn]): UIBox {
89
104
  if (args.length === 0) {
90
105
  return new ContainerElement("box", {}, [])
91
106
  } else if (args.length === 1) {
92
- if (Array.isArray(args[0])) {
107
+ if (Array.isArray(args[0]) || typeof args[0] === "function") {
93
108
  return new ContainerElement("box", {}, args[0])
94
109
  } else {
95
110
  return new ContainerElement("box", args[0], [])
@@ -35,6 +35,11 @@ export interface UIImage {
35
35
  setSourceRect(x: number, y: number, w: number, h: number): this
36
36
 
37
37
  onLayout(onLayout: OnLayoutCallback): this
38
+
39
+ setClass(name: string, enabled: boolean): this
40
+ toggleClass(name: string): this
41
+ hasClass(name: string): boolean
42
+ bindClass(name: string, fn: () => boolean): this
38
43
  }
39
44
 
40
45
  class ImageElement extends Element<"image"> {
@@ -16,6 +16,11 @@ export interface UIInput {
16
16
  onFocus(callback: () => void): UIInput
17
17
  onBlur(callback: () => void): UIInput
18
18
  onLayout(onLayout: OnLayoutCallback): this
19
+
20
+ setClass(name: string, enabled: boolean): this
21
+ toggleClass(name: string): this
22
+ hasClass(name: string): boolean
23
+ bindClass(name: string, fn: () => boolean): this
19
24
  }
20
25
 
21
26
  export interface UITextArea {
@@ -33,6 +38,11 @@ export interface UITextArea {
33
38
  onChange(callback: (value: string) => void): UITextArea
34
39
  onFocus(callback: () => void): UITextArea
35
40
  onBlur(callback: () => void): UITextArea
41
+
42
+ setClass(name: string, enabled: boolean): this
43
+ toggleClass(name: string): this
44
+ hasClass(name: string): boolean
45
+ bindClass(name: string, fn: () => boolean): this
36
46
  }
37
47
 
38
48
  class InputElement<T extends "input" | "textarea"> extends Element<T> {
@@ -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,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 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
 
@@ -21,7 +21,7 @@ export interface UIScreen <Scrollable extends boolean = false> {
21
21
  append(...nodes: UINodeChild[]): this
22
22
  insert(index: number, ...nodes: UINodeChild[]): this
23
23
  remove(...nodes: UINodeChild[]): this
24
- setContent(nodes: UINodeChild[]): this
24
+ setContent(nodes: UINodeChild[] | ChildrenFn): this
25
25
 
26
26
  readonly children: UINodeChild[],
27
27
 
@@ -36,6 +36,11 @@ export interface UIScreen <Scrollable extends boolean = false> {
36
36
 
37
37
  onLayout(onLayout: OnLayoutCallback): this
38
38
 
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
+
39
44
  onTouchStart(callback: (ev: TouchStartEvent<UIScreen<boolean>>) => void): this,
40
45
  }
41
46
 
@@ -109,14 +114,14 @@ export class ScreenElement extends ContainerElement<"screen"> {
109
114
  }
110
115
 
111
116
  export function UIScreen(): UIScreen<false>;
112
- export function UIScreen(children: UINodeChild[]): UIScreen<false>;
117
+ export function UIScreen(children: UINodeChild[] | ChildrenFn): UIScreen<false>;
113
118
  export function UIScreen(style: UIScreenStyle): UIScreen<false>;
114
119
  export function UIScreen(style: UIScreenStyle, children: UINodeChild[]): UIScreen<false>;
115
- 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> {
116
121
  if (args.length === 0) {
117
122
  return new ScreenElement("screen", {}, [])
118
123
  } else if (args.length === 1) {
119
- if (Array.isArray(args[0])) {
124
+ if (Array.isArray(args[0]) || typeof args[0] === "function") {
120
125
  return new ScreenElement("screen", {}, args[0])
121
126
  } else {
122
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], [])