lecodes-sdk 1.2.0 → 2.0.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 (260) hide show
  1. package/README.md +5 -2
  2. package/dist/editor.d.ts +12 -0
  3. package/dist/global.d.ts +4 -8
  4. package/dist/host.d.ts +2 -3
  5. package/dist/types/animate/tween/Animation.d.ts +0 -3
  6. package/dist/types/animate/tween/animateValue.d.ts +4 -2
  7. package/dist/types/animate/tween/easing.d.ts +8 -0
  8. package/dist/types/animate/tween/spec.d.ts +15 -7
  9. package/dist/types/audio/audio.d.ts +2 -1
  10. package/dist/types/canvas/Canvas.d.ts +40 -108
  11. package/dist/types/canvas/gen/cssColor.d.ts +17 -0
  12. package/dist/types/canvas/gen/recorder.d.ts +118 -0
  13. package/dist/types/canvas/gen/spec.d.ts +144 -0
  14. package/dist/types/core/color.d.ts +3 -1
  15. package/dist/types/core/pins.d.ts +18 -0
  16. package/dist/types/g2/Node2D.d.ts +5 -8
  17. package/dist/types/g2/Scene2D.d.ts +6 -2
  18. package/dist/types/gl/Foliage.d.ts +30 -7
  19. package/dist/types/gl/Light.d.ts +8 -0
  20. package/dist/types/gl/Lightmap.d.ts +13 -2
  21. package/dist/types/gl/Material.d.ts +17 -3
  22. package/dist/types/gl/Model.d.ts +6 -2
  23. package/dist/types/gl/Node.d.ts +3 -6
  24. package/dist/types/gl/Scene.d.ts +41 -13
  25. package/dist/types/gl/Texture.d.ts +1 -1
  26. package/dist/types/gl/animation/Locomotion.d.ts +8 -1
  27. package/dist/types/inject.d.ts +11 -11
  28. package/dist/types/inject.editor.d.ts +1 -0
  29. package/dist/types/net/core.d.ts +7 -0
  30. package/dist/types/plugin.d.ts +76 -0
  31. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  32. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  33. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  34. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  35. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  36. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  37. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  38. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  39. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  40. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  41. package/dist/types/runtime/app.d.ts +9 -2
  42. package/dist/types/runtime/fetch.d.ts +2 -0
  43. package/dist/types/runtime/input.d.ts +1 -1
  44. package/dist/types/runtime/media.d.ts +6 -10
  45. package/dist/types/runtime/misc.d.ts +4 -1
  46. package/dist/types/runtime/net.d.ts +3 -2
  47. package/dist/types/runtime/touch.d.ts +32 -0
  48. package/dist/types/scene/defineScene.d.ts +43 -2
  49. package/dist/types/scene/editor.d.ts +52 -0
  50. package/dist/types/scene/gizmos.d.ts +7 -4
  51. package/dist/types/ui/NativeView.d.ts +6 -4
  52. package/dist/types/ui/UI.d.ts +1 -1
  53. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  54. package/dist/types/ui/UIButton.d.ts +12 -14
  55. package/dist/types/ui/UIContainer.d.ts +0 -6
  56. package/dist/types/ui/UIImage.d.ts +1 -4
  57. package/dist/types/ui/UIInput.d.ts +8 -24
  58. package/dist/types/ui/UIModal.d.ts +0 -2
  59. package/dist/types/ui/UINode.d.ts +70 -56
  60. package/dist/types/ui/UIPager.d.ts +28 -27
  61. package/dist/types/ui/UIPopover.d.ts +0 -2
  62. package/dist/types/ui/UIScreen.d.ts +12 -17
  63. package/dist/types/ui/UIScrollable.d.ts +1 -4
  64. package/dist/types/ui/UIText.d.ts +0 -2
  65. package/dist/types/ui/UIVideo.d.ts +3 -5
  66. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  67. package/dist/types/ui/UIWidget.d.ts +10 -9
  68. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  69. package/dist/types/ui/presentable.d.ts +46 -32
  70. package/dist/types/ui/router.d.ts +18 -7
  71. package/dist/types/ui/styleColor.d.ts +1 -0
  72. package/dist/types/ui/transitions.d.ts +18 -0
  73. package/dist/types/ui/tree.d.ts +75 -0
  74. package/dist/types/version.d.ts +10 -0
  75. package/dist/types.json +1 -1
  76. package/package.json +12 -3
  77. package/prompts/2d.md +2 -6
  78. package/prompts/3d.md +1 -5
  79. package/prompts/README.md +1 -1
  80. package/prompts/canvas.md +9 -8
  81. package/prompts/compose.ts +1 -1
  82. package/prompts/core.md +3 -3
  83. package/prompts/design.md +1 -1
  84. package/prompts/dist/2d-game.md +473 -239
  85. package/prompts/dist/3d-app.md +553 -205
  86. package/prompts/dist/ar-app.md +435 -202
  87. package/prompts/dist/design.md +113 -95
  88. package/prompts/dist/ui-app.md +386 -170
  89. package/prompts/ui-design.md +2 -3
  90. package/prompts/ui.md +45 -37
  91. package/src/animate/tween/Animation.ts +34 -150
  92. package/src/animate/tween/Timeline.ts +175 -175
  93. package/src/animate/tween/animateValue.ts +6 -3
  94. package/src/animate/tween/easing.ts +10 -3
  95. package/src/animate/tween/spec.ts +41 -15
  96. package/src/audio/Sound.ts +3 -3
  97. package/src/audio/audio.ts +2 -1
  98. package/src/bridges/2d.d.ts +317 -0
  99. package/src/bridges/app.d.ts +91 -0
  100. package/src/bridges/audio.d.ts +97 -0
  101. package/src/bridges/canvas.d.ts +79 -0
  102. package/src/bridges/device.d.ts +72 -0
  103. package/src/bridges/fetch.d.ts +80 -0
  104. package/src/bridges/files.d.ts +70 -0
  105. package/src/bridges/gl.d.ts +1133 -0
  106. package/src/bridges/input.d.ts +72 -0
  107. package/src/bridges/media.d.ts +55 -0
  108. package/src/bridges/nav.d.ts +71 -0
  109. package/src/bridges/net.d.ts +52 -0
  110. package/src/bridges/service.d.ts +52 -0
  111. package/src/bridges/socket.d.ts +31 -0
  112. package/src/bridges/storage.d.ts +33 -0
  113. package/src/bridges/tree.d.ts +301 -0
  114. package/src/bridges/types.d.ts +49 -0
  115. package/src/canvas/Canvas.ts +114 -159
  116. package/src/canvas/gen/cssColor.ts +224 -0
  117. package/src/canvas/gen/recorder.ts +212 -0
  118. package/src/canvas/gen/spec.ts +201 -0
  119. package/src/chisel.ts +193 -0
  120. package/src/compile/assetMacro.ts +1 -1
  121. package/src/compile/bundler.ts +11 -2
  122. package/src/compile/compileProject.ts +43 -4
  123. package/src/compile/fontMacro.ts +3 -4
  124. package/src/compile/header.ts +26 -5
  125. package/src/compile/index.ts +3 -1
  126. package/src/compile/liteMaterial.ts +1 -1
  127. package/src/compile/sceneEditor.ts +11 -26
  128. package/src/core/color.ts +73 -30
  129. package/src/core/pins.ts +51 -0
  130. package/src/core/signals.ts +8 -1
  131. package/src/g2/CharacterController2D.ts +3 -3
  132. package/src/g2/Node2D.ts +57 -39
  133. package/src/g2/Physics2D.ts +2 -2
  134. package/src/g2/Scene2D.ts +35 -23
  135. package/src/g2/Texture2D.ts +1 -1
  136. package/src/g2/loop.ts +4 -4
  137. package/src/gl/CameraPlace.ts +52 -52
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +10 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Mesh.ts +120 -120
  144. package/src/gl/Model.ts +21 -16
  145. package/src/gl/Node.ts +91 -24
  146. package/src/gl/Particles.ts +1 -1
  147. package/src/gl/Scene.ts +100 -44
  148. package/src/gl/Texture.ts +8 -7
  149. package/src/gl/animation/AnimationClip.ts +1 -1
  150. package/src/gl/animation/DynamicBone.ts +482 -482
  151. package/src/gl/animation/Locomotion.ts +8 -3
  152. package/src/gl/nav/NavMesh.ts +3 -4
  153. package/src/gl/physics/Physics.ts +2 -2
  154. package/src/gl/physics/physicsEvents.ts +3 -3
  155. package/src/gl/scenarios.ts +291 -291
  156. package/src/gl/terrain/Terrain.ts +4 -5
  157. package/src/gl/touch.ts +14 -15
  158. package/src/host.d.ts +2 -3
  159. package/src/inject.editor.ts +7 -0
  160. package/src/inject.ts +13 -16
  161. package/src/net/core.ts +6 -5
  162. package/src/net/index.ts +1 -1
  163. package/src/net/replication.ts +1 -1
  164. package/src/plugin.ts +191 -0
  165. package/src/plugins/gen/camera/contract.d.ts +27 -0
  166. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  167. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  168. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  169. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  170. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  171. package/src/plugins/gen/map/contract.d.ts +144 -0
  172. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  173. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  174. package/src/plugins/gen/push/contract.d.ts +61 -0
  175. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  176. package/src/plugins/gen/push/sdk/push.ts +105 -0
  177. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  178. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  179. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  180. package/src/plugins/permission.ts +5 -4
  181. package/src/runtime/app.ts +20 -9
  182. package/src/runtime/appEvents.ts +5 -4
  183. package/src/runtime/channel.ts +18 -15
  184. package/src/runtime/clipboard.ts +4 -3
  185. package/src/runtime/datetime.ts +2 -1
  186. package/src/runtime/device.ts +17 -15
  187. package/src/runtime/fetch.ts +30 -20
  188. package/src/runtime/files.ts +16 -15
  189. package/src/runtime/input.ts +12 -10
  190. package/src/runtime/media.ts +50 -46
  191. package/src/runtime/misc.ts +7 -3
  192. package/src/runtime/net.ts +8 -7
  193. package/src/runtime/rpc.ts +1 -3
  194. package/src/runtime/service.ts +19 -14
  195. package/src/runtime/share.ts +4 -3
  196. package/src/runtime/storage.ts +6 -4
  197. package/src/runtime/touch.ts +32 -0
  198. package/src/scene/defineScene.ts +61 -365
  199. package/src/scene/editor.ts +408 -0
  200. package/src/scene/editorPlugins.ts +3 -3
  201. package/src/scene/gizmos.ts +15 -9
  202. package/src/server/db/marci/query.ts +1 -1
  203. package/src/server/host.ts +1 -1
  204. package/src/server/runtime.ts +1 -1
  205. package/src/ui/NativeView.ts +72 -25
  206. package/src/ui/UI.ts +3 -3
  207. package/src/ui/UIBottomSheet.ts +16 -17
  208. package/src/ui/UIButton.ts +54 -16
  209. package/src/ui/UIContainer.ts +0 -6
  210. package/src/ui/UIImage.ts +34 -30
  211. package/src/ui/UIInput.ts +29 -37
  212. package/src/ui/UIModal.ts +1 -3
  213. package/src/ui/UINode.ts +347 -297
  214. package/src/ui/UIPager.ts +93 -78
  215. package/src/ui/UIPopover.ts +0 -2
  216. package/src/ui/UIScreen.ts +41 -36
  217. package/src/ui/UIScrollable.ts +19 -13
  218. package/src/ui/UISpacer.ts +1 -1
  219. package/src/ui/UITabs.ts +8 -6
  220. package/src/ui/UIText.ts +7 -17
  221. package/src/ui/UIVideo.ts +30 -27
  222. package/src/ui/UIVirtualizedList.ts +58 -59
  223. package/src/ui/UIWidget.ts +38 -18
  224. package/src/ui/colorKeys.gen.ts +37 -0
  225. package/src/ui/fonts.ts +2 -2
  226. package/src/ui/presentable.ts +59 -42
  227. package/src/ui/router.ts +52 -35
  228. package/src/ui/styleColor.ts +56 -0
  229. package/src/ui/theme.ts +7 -6
  230. package/src/ui/transitions.ts +249 -0
  231. package/src/ui/tree.ts +346 -0
  232. package/src/version.ts +24 -0
  233. package/tests/helpers/engineWorld.ts +11 -0
  234. package/tests/helpers/fakeTree.ts +353 -0
  235. package/tests/helpers/hostStubs.ts +31 -0
  236. package/tests/helpers/index.ts +14 -0
  237. package/tests/helpers/memoryMarci.ts +124 -0
  238. package/tests/helpers/phases.ts +23 -0
  239. package/tests/helpers/preload.ts +18 -0
  240. package/tests/helpers/stubApp.ts +2 -0
  241. package/tests/helpers/stubDevice.ts +2 -0
  242. package/tests/helpers/stubFetch.ts +2 -0
  243. package/tests/helpers/stubInput.ts +2 -0
  244. package/dist/inject.js +0 -4629
  245. package/dist/types/core/registry.d.ts +0 -7
  246. package/dist/types/plugins/camera.d.ts +0 -25
  247. package/dist/types/plugins/push.d.ts +0 -46
  248. package/src/bridges.d.ts +0 -1769
  249. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  250. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  251. package/src/compile/__tests__/assetName.test.ts +0 -55
  252. package/src/compile/__tests__/compile.test.ts +0 -310
  253. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  254. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  255. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  256. package/src/core/__tests__/stateMachine.test.ts +0 -132
  257. package/src/core/registry.ts +0 -23
  258. package/src/plugins/camera.ts +0 -81
  259. package/src/plugins/push.ts +0 -132
  260. package/src/plugins/qr.ts +0 -73
package/src/ui/UIText.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createBinding } from "../core/signals"
2
2
  import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type TextStyle, type UIElementBase } from "./UINode"
3
+ import { tree } from "./tree"
3
4
 
4
5
  export type UITextStyle = ElementStyle & TextStyle & DrawableStyle
5
6
 
@@ -11,19 +12,17 @@ export interface UIText extends UIElementBase<UITextStyle, DrawableStyle & BaseS
11
12
  }
12
13
 
13
14
  export class TextElement extends Element<"text"> {
14
- private _text: string
15
+ private _text = ""
15
16
  constructor(style: any, text: string | (() => string)) {
16
17
  super("text", style)
17
18
  if (typeof text === "function") {
18
- // Reactive content: re-runs when a signal `text()` read changes; the setter routes to
19
- // _creatorUI.updateNode when mounted and just keeps _text current when not.
20
- this._text = ""
19
+ // Reactive content: re-runs when a signal `text()` read changes.
21
20
  createBinding(this, "#text", () => {
22
21
  const value = text()
23
22
  if (value !== this._text) this.text = value
24
23
  })
25
24
  } else {
26
- this._text = text
25
+ this.text = text
27
26
  }
28
27
  }
29
28
  get text(): string {
@@ -31,21 +30,12 @@ export class TextElement extends Element<"text"> {
31
30
  }
32
31
  set text(value) {
33
32
  this._text = value
34
- if (this._id !== 0) {
35
- _creatorUI.updateNode(this._id, "text", value)
36
- }
33
+ if (this._h.id !== 0) tree().setString(this._h.id, "text", value)
37
34
  }
38
35
  }
39
36
 
40
37
  /** Create a text label — a string, or `() => string` for reactive text that re-renders when a
41
38
  * signal it read changes. */
42
- export function UIText(text: string | (() => string)): UIText;
43
- /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
44
- export function UIText(style: UITextStyle, text: string | (() => string)): UIText;
45
- export function UIText(...args: [string | (() => string)] | [UITextStyle, string | (() => string)]): UIText {
46
- if (args.length === 1) {
47
- return new TextElement({}, args[0])
48
- } else {
49
- return new TextElement(args[0], args[1])
50
- }
39
+ export function UIText(text: string | (() => string)): UIText {
40
+ return new TextElement({}, text)
51
41
  }
package/src/ui/UIVideo.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { VideoPlayer } from "../runtime/media"
2
- import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
2
+ import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "./presentable"
3
+ import { _transitionId, TRANSITION_NONE } from "./transitions"
3
4
  import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type UIElementBase } from "./UINode"
5
+ import { TREE_DEST_VIDEO, TREE_FLAG_BACK, pin, tree } from "./tree"
4
6
 
5
7
  export type UIVideoStyle = ElementStyle & DrawableStyle & { objectFit?: "cover" | "contain" | "fill" }
6
8
 
@@ -15,22 +17,23 @@ export interface UIVideo extends UIElementBase<UIVideoStyle, DrawableStyle & Bas
15
17
  * the SAME native player moves fullscreen — playback uninterrupted; a pop moves it back. */
16
18
  open(options?: PresentOptions): void
17
19
  /** Dismiss if this is the visible destination. */
18
- close(): void
20
+ close(options?: DismissOptions): void
19
21
  /** Became the visible destination. */
20
22
  onOpen(callback: () => void): this
21
23
  /** Stopped being the visible destination. */
22
24
  onClose(callback: () => void): this
23
25
  /** Hardware/system back while current. */
24
- onBackPressed(callback: () => void): this
26
+ onBack(callback: () => void): this
27
+ /** @internal The destination tuple (Presentable). */
28
+ _dest(): DestTuple
25
29
  }
26
30
 
27
31
  class VideoElement extends Element<"video"> implements Presentable {
28
- private _playerId: number
29
32
  private _player: VideoPlayer
30
33
  constructor(style: any, player: VideoPlayer) {
31
34
  super("video", style)
32
35
  this._player = player
33
- this._playerId = (player as any)._id
36
+ tree().setInt(this._h.id, "playerId", (player as any)._h.id)
34
37
  }
35
38
 
36
39
  get player() {
@@ -40,25 +43,21 @@ class VideoElement extends Element<"video"> implements Presentable {
40
43
  // ---- Presentable (docs/navigation-presentable-plan.md) ----
41
44
  readonly ol: (() => void)[] = []
42
45
  readonly cl: (() => void)[] = []
43
- _backButtonCallback?: () => void
44
- private _vd?: object
46
+ private _backButtonCallback?: () => void
45
47
 
46
- /** @internal wire descriptor — type "videoView" (distinct from the "video" node type). */
47
- _viewDesc(): object {
48
- return this._vd ??= { type: "videoView", node: this, _p: this }
49
- }
48
+ /** @internal */
49
+ _dest(): DestTuple { return [TREE_DEST_VIDEO, this._h.id, "", ""] }
50
50
 
51
51
  open(options?: PresentOptions): void {
52
- if (!_navSupported()) {
53
- throw new Error("Fullscreen video is not supported on this host (requires _creatorUI.openView)")
54
- }
52
+ if (this._h.id === 0) return
55
53
  _bumpNavEpoch()
56
- _creatorUI.openView!(this._viewDesc(), options?.transition ?? "none")
54
+ pin(this)
55
+ tree().open(TREE_DEST_VIDEO, this._h.id, "", "", _transitionId(options?.transition, TRANSITION_NONE))
57
56
  _setCurrent(this)
58
57
  }
59
- close(): void {
58
+ close(options?: DismissOptions): void {
60
59
  _bumpNavEpoch()
61
- if (_navSupported()) _creatorUI.closeView!()
60
+ tree().close(TREE_DEST_VIDEO, this._h.id, _transitionId(options?.transition, TRANSITION_NONE))
62
61
  if (Presentable.current === this) _setCurrent(null)
63
62
  }
64
63
  onOpen(callback: () => void): this {
@@ -69,20 +68,24 @@ class VideoElement extends Element<"video"> implements Presentable {
69
68
  this.cl.push(callback)
70
69
  return this
71
70
  }
72
- onBackPressed(callback: () => void): this {
71
+ onBack(callback: () => void): this {
73
72
  this._backButtonCallback = callback
73
+ this._addFlags(TREE_FLAG_BACK)
74
74
  return this
75
75
  }
76
+ /** @internal */
77
+ _emitOpen(): void { for (const cb of this.ol.slice()) cb() }
78
+ /** @internal */
79
+ _emitClose(): void { for (const cb of this.cl.slice()) cb() }
80
+ /** @internal */
81
+ _emitBack(): boolean {
82
+ if (!this._backButtonCallback) return false
83
+ this._backButtonCallback()
84
+ return true
85
+ }
76
86
  }
77
87
 
78
88
  /** Create a video node showing `player`'s output. */
79
- export function UIVideo(player: VideoPlayer): UIVideo
80
- /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
81
- export function UIVideo(style: UIVideoStyle, player: VideoPlayer): UIVideo
82
- export function UIVideo(...args: [VideoPlayer] | [UIVideoStyle, VideoPlayer]): UIVideo {
83
- if (args.length === 1) {
84
- return new VideoElement({}, args[0])
85
- } else {
86
- return new VideoElement(args[0], args[1])
87
- }
89
+ export function UIVideo(player: VideoPlayer): UIVideo {
90
+ return new VideoElement({}, player)
88
91
  }
@@ -1,4 +1,5 @@
1
1
  import { Element, type Color, type DrawableStyle, type ElementStyle, type UIElementBase, type UINodeChild } from "./UINode"
2
+ import { TREE_FLAG_REFRESH, TREE_FLAG_SCROLL, TREE_FLAG_VLIST_END, TREE_FLAG_VLIST_START, pin, tree, unpin } from "./tree"
2
3
 
3
4
  export type UIVirtualizedListStyle = DrawableStyle & ElementStyle & {
4
5
  /** Tint of the native pull-to-refresh spinner (pairs with `onRefresh`). */
@@ -30,11 +31,11 @@ export interface VListConfig<T> {
30
31
  export interface UIVirtualizedList<T = unknown> extends UIElementBase<UIVirtualizedListStyle> {
31
32
  readonly type: "vlist"
32
33
 
33
- /** Replace the data wholesale — vanished keys are unmounted by the host's window sync. */
34
+ /** Replace the data wholesale — vanished keys are unmounted by the window sync. */
34
35
  setData(items: T[]): this
35
36
  /** Append items to the end (auto-scrolls when `inverted` and resting at the bottom). */
36
37
  append(...items: T[]): this
37
- /** Insert at the start WITHOUT a scroll jump (the host compensates the offset). */
38
+ /** Insert at the start WITHOUT a scroll jump (the runtime compensates the offset). */
38
39
  prepend(...items: T[]): this
39
40
  /** Update items with the same keys: mounted ones re-render; the cached height persists until
40
41
  * re-measured. */
@@ -56,37 +57,31 @@ export interface UIVirtualizedList<T = unknown> extends UIElementBase<UIVirtuali
56
57
 
57
58
  /** Scroll position changed (px from the start edge). */
58
59
  onScroll(callback: (scrollPosition: number) => void): this
59
- /** Pull-to-refresh: the spinner stays until the returned promise settles. Attach BEFORE the
60
- * node mounts (hosts read it at creation). Native hosts only — no-op on web. */
60
+ /** Pull-to-refresh: the spinner stays until the returned promise settles. */
61
61
  onRefresh(callback: () => void | Promise<void>): this
62
62
  /** Fires within `thresholdPx` of the content's bottom edge — once, re-armed after leaving the
63
- * edge (host-side latch). Infinite-scroll loading. */
63
+ * edge (a runtime-side latch). Infinite-scroll loading. */
64
64
  onEndReached(thresholdPx: number, callback: () => void): this
65
65
  /** Same, for the top edge — chat history loading. */
66
66
  onStartReached(thresholdPx: number, callback: () => void): this
67
67
  }
68
68
 
69
69
  export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedList<T> {
70
+ private readonly sl: ((scrollPosition: number) => void)[] = [] // scroll
71
+ private readonly erl: (() => void)[] = [] // endReached
72
+ private readonly strl: (() => void)[] = [] // startReached
73
+ private _refreshCallback?: () => void | Promise<void>
70
74
 
71
- // --- read by native side in createUINode (copyProperty / copyNodePtr) ---
72
- _overscan: number
73
- _inverted: boolean
74
- _keys: string[] = []
75
- _estimates: number[] = []
76
- _endThreshold = 0
77
- _startThreshold = 0
78
-
79
- // --- списки слушателей, нативка забирает через copyPropertyListPtr ---
80
- protected sl: ((scrollPosition: number) => void)[] = [] // scroll
81
- protected erl: (() => void)[] = [] // endReached
82
- protected strl: (() => void)[] = [] // startReached
83
-
84
- // --- приватное состояние JS-стороны ---
85
75
  private items = new Map<string, T>()
86
- private mounted = new Map<string, UINodeChild>()
76
+ private mounted = new Map<string, any>()
87
77
  private keyOf: (item: T) => string
88
78
  private renderItem: (item: T) => UINodeChild
89
79
  private estimate: (item: T) => number
80
+ private readonly _overscan: number
81
+ private readonly _inverted: boolean
82
+ private _endThreshold = 0
83
+ private _startThreshold = 0
84
+ private _inited = false
90
85
 
91
86
  constructor(config: VListConfig<T>) {
92
87
  // flexShrink: 1 default (SDK-owned, same as UIScrollable): an explicit height/flexBasis
@@ -102,8 +97,12 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
102
97
  this._inverted = config.inverted ?? false
103
98
  }
104
99
 
105
- private get isAttached(): boolean {
106
- return !!this._id && this._id !== 0
100
+ // The runtime's list state is created on the first data write — after the app had its chance to
101
+ // set the edge thresholds (onEndReached / onStartReached come before setData in practice).
102
+ private _ensure(): void {
103
+ if (this._inited || this._h.id === 0) return
104
+ this._inited = true
105
+ tree().vlistInit(this._h.id, this._overscan, this._inverted, this._startThreshold, this._endThreshold)
107
106
  }
108
107
 
109
108
  private register(items: T[]): [string[], number[]] {
@@ -123,14 +122,12 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
123
122
  const prevMounted = [...this.mounted.keys()]
124
123
  this.items.clear()
125
124
  const [keys, estimates] = this.register(items)
126
- this._keys = keys
127
- this._estimates = estimates
128
- if (this.isAttached) {
129
- _creatorUI.vlistSetKeys(this, keys, estimates)
130
-
125
+ this._ensure()
126
+ if (this._h.id !== 0) {
127
+ tree().vlistSetKeys(this._h.id, keys, estimates)
131
128
  for (const k of prevMounted) {
132
129
  if (this.mounted.has(k) && this.items.get(k) !== prevItems.get(k)) {
133
- _creatorUI.vlistInvalidate(this, k)
130
+ tree().vlistInvalidate(this._h.id, k)
134
131
  }
135
132
  }
136
133
  }
@@ -139,21 +136,15 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
139
136
 
140
137
  append(...items: T[]): this {
141
138
  const [keys, estimates] = this.register(items)
142
- this._keys.push(...keys)
143
- this._estimates.push(...estimates)
144
- if (this.isAttached) {
145
- _creatorUI.vlistInsertKeys(this, -1, keys, estimates)
146
- }
139
+ this._ensure()
140
+ if (this._h.id !== 0) tree().vlistInsertKeys(this._h.id, -1, keys, estimates)
147
141
  return this
148
142
  }
149
143
 
150
144
  prepend(...items: T[]): this {
151
145
  const [keys, estimates] = this.register(items)
152
- this._keys.unshift(...keys)
153
- this._estimates.unshift(...estimates)
154
- if (this.isAttached) {
155
- _creatorUI.vlistInsertKeys(this, 0, keys, estimates)
156
- }
146
+ this._ensure()
147
+ if (this._h.id !== 0) tree().vlistInsertKeys(this._h.id, 0, keys, estimates)
157
148
  return this
158
149
  }
159
150
 
@@ -162,74 +153,82 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
162
153
  const k = this.keyOf(item)
163
154
  if (!this.items.has(k)) continue
164
155
  this.items.set(k, item)
165
- if (this.isAttached && this.mounted.has(k)) {
166
- _creatorUI.vlistInvalidate(this, k)
167
- }
156
+ if (this._h.id !== 0 && this.mounted.has(k)) tree().vlistInvalidate(this._h.id, k)
168
157
  }
169
158
  return this
170
159
  }
171
160
 
172
161
  removeByKey(...keys: string[]): this {
162
+ const gone: string[] = []
173
163
  for (const k of keys) {
174
164
  if (!this.items.delete(k)) continue
175
- const i = this._keys.indexOf(k)
176
- if (i >= 0) { this._keys.splice(i, 1); this._estimates.splice(i, 1) }
177
- }
178
- if (this.isAttached) {
179
- _creatorUI.vlistRemoveKeys(this, keys)
165
+ gone.push(k)
180
166
  }
167
+ if (this._h.id !== 0 && gone.length > 0) tree().vlistRemoveKeys(this._h.id, gone)
181
168
  return this
182
169
  }
183
170
 
184
171
  get itemCount(): number { return this.items.size }
185
172
  getItem(key: string): T | undefined { return this.items.get(key) }
186
173
 
187
- // --- команды: офсеты и позиции знает только нативка (префикс-суммы) ---
174
+ // --- commands: offsets and positions are the runtime's (prefix sums) ---
188
175
  scrollTo(offset: number, animated = true): void {
189
- _creatorUI.command(this, "scrollTo", offset, animated)
176
+ if (this._h.id !== 0) tree().vlistScrollTo(this._h.id, offset, animated)
190
177
  }
191
178
  scrollToKey(key: string, animated = true): void {
192
- _creatorUI.command(this, "scrollToKey", key, animated)
179
+ if (this._h.id !== 0) tree().vlistScrollToKey(this._h.id, key, animated)
193
180
  }
194
181
  scrollToEnd(animated = true): void {
195
- _creatorUI.command(this, "scrollToEnd", animated)
182
+ if (this._h.id !== 0) tree().vlistScrollToEnd(this._h.id, animated)
196
183
  }
197
184
 
198
185
  onScroll(callback: (scrollPosition: number) => void): this {
199
186
  this.sl.push(callback)
187
+ this._addFlags(TREE_FLAG_SCROLL)
200
188
  return this
201
189
  }
202
- // Читается нативкой при маунте (тот же контракт, что у UIScrollable).
203
- declare _refreshCallback: any
204
190
  onRefresh(callback: () => void | Promise<void>): this {
205
191
  this._refreshCallback = callback
192
+ this._addFlags(TREE_FLAG_REFRESH)
206
193
  return this
207
194
  }
208
195
  onEndReached(thresholdPx: number, callback: () => void): this {
209
196
  this._endThreshold = thresholdPx
210
197
  this.erl.push(callback)
198
+ this._addFlags(TREE_FLAG_VLIST_END)
211
199
  return this
212
200
  }
213
201
  onStartReached(thresholdPx: number, callback: () => void): this {
214
202
  this._startThreshold = thresholdPx
215
203
  this.strl.push(callback)
204
+ this._addFlags(TREE_FLAG_VLIST_START)
216
205
  return this
217
206
  }
218
207
 
219
- /** @internal The one host→JS callback (via jsPtr, like button). Fires ONLY when the window
220
- * crossed a boundary — not per scroll frame. Order: unmounts first (the host already tore the
221
- * subtrees down and zeroed _id; only JS refs are cleaned here), then mounts. */
208
+ /** @internal */
209
+ _emitScroll(position: number): void { for (const cb of this.sl.slice()) cb(position) }
210
+ /** @internal */
211
+ _emitRefresh(): unknown { return this._refreshCallback?.() }
212
+ /** @internal */
213
+ _emitEdge(end: boolean): void { for (const cb of (end ? this.erl : this.strl).slice()) cb() }
214
+
215
+ /** @internal The runtime's window sync (synchronous). Order: unmounts first (the runtime already
216
+ * detached those subtrees — they are plain roots again and go with their elements), then mounts:
217
+ * each rendered subtree is handed to the runtime under its key on this same stack. */
222
218
  _syncWindow(unmountKeys: string[], mountKeys: string[]): void {
223
219
  for (const k of unmountKeys) {
220
+ const node = this.mounted.get(k)
224
221
  this.mounted.delete(k)
222
+ if (node) unpin(node._h.id)
225
223
  }
226
224
  for (const k of mountKeys) {
227
225
  const item = this.items.get(k)
228
226
  if (item === undefined) continue
229
- const node = this.renderItem(item)
227
+ const node: any = this.renderItem(item)
228
+ if (!node || node._h.id === 0) continue
230
229
  this.mounted.set(k, node)
231
- // createUINode в detached yoga root + измерение (width, NaN) + кэш высоты
232
- _creatorUI.vlistMount(this, k, node)
230
+ pin(node)
231
+ tree().vlistMount(this._h.id, k, node._h.id)
233
232
  }
234
233
  }
235
234
  }
@@ -238,4 +237,4 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
238
237
  * then feed it via `setData`/`append`/`prepend`. Give it a size (`flexGrow: 1` as a screen body). */
239
238
  export function UIVirtualizedList<T>(config: VListConfig<T>): UIVirtualizedList<T> {
240
239
  return new VListElement<T>(config)
241
- }
240
+ }
@@ -1,6 +1,7 @@
1
1
  import { TouchStartEvent } from "../runtime/touch"
2
- import { _descOf, _navSupported, Presentable } from "./presentable"
2
+ import { Presentable } from "./presentable"
3
3
  import { ContainerElement, buildUI, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type PaddingStyle, type PositionStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn } from "./UINode"
4
+ import { TREE_DEST_NONE, TREE_FLAG_BACK, TREE_FLAG_INTERACTIVE, TREE_FLAG_OVERLAY_TAP, pin, tree, unpin } from "./tree"
4
5
 
5
6
 
6
7
  export type UIWidgetStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle & PositionStyle & {
@@ -23,8 +24,7 @@ export interface UIWidget extends UIContainerBase<UIWidgetStyle, DrawableStyle &
23
24
  * (= back to a global overlay). Set it while the widget is hidden. The widget is always its own
24
25
  * overlay root mounted into the owner's page view — it never becomes a child of the owner, so it
25
26
  * stays topmost and its updates never trigger the owner's relayout. Works with a `UIScreen` that
26
- * is a page inside a `UIPager`: the widget mounts into that tab and rides its swipe. (Web shows
27
- * and hides it with the owner but does not yet ride the transition.)
27
+ * is a page inside a `UIPager`: the widget mounts into that tab and rides its swipe.
28
28
  */
29
29
  attachTo(owner: Presentable | null): this,
30
30
 
@@ -37,13 +37,16 @@ export interface UIWidget extends UIContainerBase<UIWidgetStyle, DrawableStyle &
37
37
  hide(): void,
38
38
 
39
39
  /** Mounted right now (`show()` ran, `hide()` hasn't). */
40
- readonly isShow: boolean,
40
+ readonly isShown: boolean,
41
41
 
42
42
  /** Touch began on the widget; `ev.track(...)` takes over the rest of the gesture. */
43
43
  onTouchStart(callback: (ev: TouchStartEvent<UIWidget>) => void): this,
44
44
  /** Tap on the overlay layer (requires an `overlayColor`). */
45
45
  onOverlayTap(callback: () => void): this,
46
46
  /** Hardware/system back while the widget is shown. */
47
+ onBack(callback: () => void): this
48
+ /** @deprecated Renamed `onBack` (2026-09-26). An alias for the projects compiled before the
49
+ * rename; removed with the release that recompiles them. */
47
50
  onBackPressed(callback: () => void): this
48
51
  }
49
52
 
@@ -51,6 +54,7 @@ export interface UIWidget extends UIContainerBase<UIWidgetStyle, DrawableStyle &
51
54
  export class WidgetElement extends ContainerElement<"widget"> {
52
55
  scrollable = false as const
53
56
  _owner?: Presentable
57
+ private _shown = false
54
58
  constructor(type: "widget", style: UIWidgetStyle, children: UINodeChild[] | ChildrenFn) {
55
59
  // Widgets are absolutely positioned in device coordinates — in the constructor (not the
56
60
  // factory) so subclasses like ModalElement inherit it.
@@ -65,27 +69,33 @@ export class WidgetElement extends ContainerElement<"widget"> {
65
69
  // content) — never a child of the owner, so it stays topmost, its updates never relayout the
66
70
  // owner, and it doesn't pollute the owner's children. `attachTo(owner)` just tells the host
67
71
  // which page view to mount that root into; ownerless widgets are global overlays.
68
- if (this._owner && _navSupported()) {
69
- _creatorUI.showWidget(this, _descOf(this._owner))
72
+ if (this._h.id === 0 || this._shown) return
73
+ this._shown = true
74
+ pin(this)
75
+ if (this._owner) {
76
+ const [kind, id] = this._owner._dest()
77
+ tree().showWidget(this._h.id, kind, id)
70
78
  } else {
71
- // Unattached (or legacy host): global overlay above navigation.
72
- _creatorUI.showWidget(this)
79
+ tree().showWidget(this._h.id, TREE_DEST_NONE, 0)
73
80
  }
74
81
  }
75
82
  hide(): void {
76
- _creatorUI.hideWidget(this)
83
+ if (this._h.id === 0 || !this._shown) return
84
+ this._shown = false
85
+ tree().hideWidget(this._h.id)
86
+ unpin(this._h.id)
77
87
  }
78
- get isShow(): boolean {
79
- return this._id !== 0
88
+ get isShown(): boolean {
89
+ return this._shown
80
90
  }
81
91
 
82
- _interactive = false
83
92
  readonly touchStartListeners: any[] = []
84
93
  onTouchStart (callback: any): this {
85
- this._interactive = true
86
94
  this.touchStartListeners.push(callback)
95
+ this._addFlags(TREE_FLAG_INTERACTIVE)
87
96
  return this
88
97
  }
98
+ /** @internal */
89
99
  _emitTouchStart(pointerId: number, clientX: number, clientY: number): any {
90
100
  const touchEvent = new TouchStartEvent(clientX, clientY, pointerId)
91
101
  touchEvent.target = this
@@ -100,14 +110,26 @@ export class WidgetElement extends ContainerElement<"widget"> {
100
110
  readonly otl: (() => void)[] = []
101
111
  onOverlayTap(callback: () => void): this {
102
112
  this.otl.push(callback)
113
+ this._addFlags(TREE_FLAG_OVERLAY_TAP)
103
114
  return this
104
115
  }
116
+ /** @internal */
117
+ _emitOverlayTap(): void { for (const cb of this.otl.slice()) cb() }
105
118
 
106
- _backButtonCallback?: () => void
107
- onBackPressed(callback: any): this {
119
+ private _backButtonCallback?: () => void
120
+ onBack(callback: any): this {
108
121
  this._backButtonCallback = callback
122
+ this._addFlags(TREE_FLAG_BACK)
109
123
  return this
110
124
  }
125
+ /** @deprecated see the interface */
126
+ onBackPressed(callback: any): this { return this.onBack(callback) }
127
+ /** @internal the runtime's back chain: true = handled. */
128
+ _emitBack(): boolean {
129
+ if (!this._backButtonCallback) return false
130
+ this._backButtonCallback()
131
+ return true
132
+ }
111
133
  }
112
134
 
113
135
 
@@ -120,8 +142,6 @@ export const __UIWidget = (children: UINodeChild[] | ChildrenFn): UIWidget => ma
120
142
  * (`top`/`left`/`bottom`/`right`), then `show()`/`hide()`. */
121
143
  export function UIWidget(...children: UIChildArg[]): UIWidget;
122
144
  export function UIWidget(children: UINodeChild[] | ChildrenFn): UIWidget;
123
- /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
124
- export function UIWidget(style: UIWidgetStyle, children?: UINodeChild[] | ChildrenFn): UIWidget;
125
145
  export function UIWidget(...args: any[]): UIWidget {
126
146
  return buildUI(args, null, makeWidget)
127
- }
147
+ }
@@ -0,0 +1,37 @@
1
+ // AUTO-GENERATED from engines/ui/properties.mjs — DO NOT EDIT. Regenerate: bun run gen:ui-props
2
+ //
3
+ // The UI style keys that carry a color (engines/ui/properties.mjs). The SDK sends colors to the
4
+ // core as '#rrggbbaa' strings only: the UI core rejects a NUMBER on a color property (a warning,
5
+ // the write dropped) and parses strings with AnyCanvas's CSS parser (the SDK's twin: canvas/gen).
6
+
7
+ /** The keys whose value IS a color — every color property with its aliases: normalize the value
8
+ * to '#rrggbbaa'. Pass-throughs: null / undefined (a reset), any string containing "var(" (the
9
+ * core resolves theme vars), rippleColor's "default" (a core keyword). */
10
+ export const COLOR_KEYS: ReadonlySet<string> = new Set([
11
+ "color",
12
+ "backgroundColor",
13
+ "bgColor",
14
+ "borderColor",
15
+ "borderTopColor",
16
+ "borderRightColor",
17
+ "borderBottomColor",
18
+ "borderLeftColor",
19
+ "placeholderColor",
20
+ "refreshControlColor",
21
+ "tintColor",
22
+ "rippleColor",
23
+ "overlayColor",
24
+ ])
25
+
26
+ /** The shorthands that expand into a color property. A color is only ONE of their value forms:
27
+ * never normalize the value whole — "2px solid red", a gradient or an image path go to the core
28
+ * raw (its shorthand parsers read the colors inside with the same grammar), a number on
29
+ * `border*` is a WIDTH; only a number / color array on `background` is a color. */
30
+ export const COLOR_SHORTHAND_KEYS: ReadonlySet<string> = new Set([
31
+ "background",
32
+ "border",
33
+ "borderTop",
34
+ "borderRight",
35
+ "borderBottom",
36
+ "borderLeft",
37
+ ])
package/src/ui/fonts.ts CHANGED
@@ -8,6 +8,6 @@ type FontOptions = {
8
8
  * dynamic escape hatch. */
9
9
  export async function registerFont (fontFamily: string, url: string, options: FontOptions = {}): Promise<void> {
10
10
  return new Promise<void>((res, rej) => {
11
- _creatorUI.registerFont(fontFamily, url, options, res, rej)
11
+ _creatorTree.registerFont(fontFamily, url, options.weight ?? 400, options.style === "italic" ? 2 : 0, res, rej)
12
12
  })
13
- }
13
+ }