lecodes-cli 0.3.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.
- package/README.md +3 -0
- package/dist/index.js +883 -507
- package/package.json +9 -4
- package/runtime/sdk/canvas/Canvas.ts +86 -0
- package/runtime/sdk/compile/aspectMacro.ts +42 -0
- package/runtime/sdk/compile/assetMacro.ts +44 -0
- package/runtime/sdk/compile/bundler.ts +173 -0
- package/runtime/sdk/compile/compileProject.ts +84 -0
- package/runtime/sdk/compile/header.ts +46 -0
- package/runtime/sdk/compile/index.ts +12 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/core/Aspect.ts +2 -2
- package/runtime/sdk/core/signals.ts +277 -0
- package/runtime/sdk/g2/CharacterController2D.ts +5 -4
- package/runtime/sdk/g2/Node2D.ts +1 -1
- package/runtime/sdk/g2/Physics2D.ts +22 -12
- package/runtime/sdk/g2/Scene2D.ts +12 -0
- package/runtime/sdk/g2/{Shape.ts → Shape2D.ts} +10 -10
- package/runtime/sdk/g2/Sprite.ts +7 -2
- package/runtime/sdk/g2/Texture2D.ts +4 -2
- package/runtime/sdk/g2/{Trigger.ts → Trigger2D.ts} +9 -9
- package/runtime/sdk/gl/CharacterController.ts +101 -0
- package/runtime/sdk/gl/Node.ts +11 -2
- package/runtime/sdk/gl/Physics.ts +65 -41
- package/runtime/sdk/gl/Scene.ts +18 -0
- package/runtime/sdk/gl/Shape.ts +98 -0
- package/runtime/sdk/gl/Texture.ts +2 -2
- package/runtime/sdk/gl/Trigger.ts +46 -0
- package/runtime/sdk/gl/physicsEvents.ts +20 -0
- package/runtime/sdk/inject.ts +12 -5
- package/runtime/sdk/runtime/camera.ts +31 -0
- package/runtime/sdk/runtime/device.ts +26 -0
- package/runtime/sdk/runtime/share.ts +9 -0
- package/runtime/sdk/ui/UI.ts +5 -1
- package/runtime/sdk/ui/UIButton.ts +11 -5
- package/runtime/sdk/ui/UIContainer.ts +28 -13
- package/runtime/sdk/ui/UIImage.ts +5 -0
- package/runtime/sdk/ui/UIInput.ts +10 -0
- package/runtime/sdk/ui/UINode.ts +206 -8
- package/runtime/sdk/ui/UIScreen.ts +10 -5
- package/runtime/sdk/ui/UIScrollable.ts +10 -5
- package/runtime/sdk/ui/UISpacer.ts +4 -0
- package/runtime/sdk/ui/UIText.ts +21 -5
- package/runtime/sdk/ui/UIVideo.ts +5 -0
- package/runtime/sdk/ui/UIVirtualizedList.ts +5 -0
- package/runtime/sdk/ui/UIWidget.ts +10 -5
- package/runtime/sdk/canvas/Label.ts +0 -57
- 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> {
|
package/runtime/sdk/ui/UINode.ts
CHANGED
|
@@ -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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
package/runtime/sdk/ui/UIText.ts
CHANGED
|
@@ -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
|
-
|
|
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], [])
|