lecodes-sdk 2.2.4 → 2.2.5

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/src/ui/UILayer.ts CHANGED
@@ -1,37 +1,106 @@
1
- // The LAYER roots widgets live in when they belong to no screen (docs/plans/widgets-plan.md):
1
+ // The WIDGETS of a destination (docs/plans/widgets-plan.md): one face on every destination —
2
2
  //
3
- // · the APP layer (`UIOverlay`): above every destination, always shown — the home of what belongs
4
- // to the app rather than to a place in it: a global loader, a mini-player, a toast with actions.
5
- // `widget.show()` on a widget that has no parent appends it here first.
6
- // · a SURFACE's content layer: a `Scene`, a `Scene2D`, a `NativeView` or a `UIVideo` cannot take
7
- // children, so its widgets (a HUD, a card over a map) go in a layer of its own, shown over it
8
- // while it is presented — `scene.setContent([hud])` fills it.
3
+ // screen.widgets(dialog, sheet) scene.widgets(hud) map.widgets(header, card)
4
+ // x.widgets.append(w) / x.widgets.remove(w) / x.widgets.list
5
+ //
6
+ // A screen keeps its widgets in the tree as the TAIL of its children (the runtime's portal link:
7
+ // never a yoga child, hoisted to the outermost presented root); `children` of a screen reads its
8
+ // content only, `widgets.list` the tail. A surface — a `Scene`, a `Scene2D`, a `NativeView`, a
9
+ // `UIVideo` — cannot take children, so its widgets live in a LAYER root of their own, bound to the
10
+ // destination (`_creatorTree.setLayer`) and shown over it while it is presented. The APP layer is
11
+ // the same kind of root above every destination, always shown: where a widget with no place lands
12
+ // on `show()` (a global loader, a dialog opened from anywhere) — it has no name of its own.
9
13
  //
10
14
  // A layer is a "layer" node of the tree: a pass-through root the runtime lays out at the frame and
11
- // the host paints after the destination; its own box is never hit, only its widgets are. The
12
- // runtime learns which destination a layer belongs to through `_creatorTree.setLayer`.
15
+ // the host paints after the destination; its own box is never hit, only its widgets are.
13
16
  import { ContainerElement, type UINodeChild, type ChildrenFn } from "./UINode"
14
17
  import type { UIWidget } from "./UIWidget"
15
18
  import type { DestTuple } from "./presentable"
16
19
  import { TREE_DEST_NONE, pin, tree } from "./tree"
17
20
 
18
- export type UIWidgetContent = (UIWidget | null | undefined | false)[] | (() => (UIWidget | null | undefined | false)[])
21
+ /** A widget slot: a widget, or a falsy value (skipped). */
22
+ export type UIWidgetChild = UIWidget | null | undefined | false
23
+ /** The widgets of a destination as `widgets()` takes them: a list, or a function for reactive
24
+ * content (re-run when a signal read inside it changes, reconciled against the shown ones). */
25
+ export type UIWidgetContent = UIWidgetChild[] | (() => UIWidgetChild[])
26
+
27
+ /**
28
+ * The widgets of a destination — a dialog, a sheet, a HUD: what floats over it, laid out and
29
+ * painted in its box above everything else. Call it to set them (`screen.widgets(dialog)`, same
30
+ * argument forms as `UIColumn`: widgets, arrays of them, or ONE function for reactive content);
31
+ * `append` / `remove` manage them one at a time; `list` reads them. A widget shows with its
32
+ * destination unless its style says `display: "none"`; `show()` / `hide()` flip that, and
33
+ * `show()` puts it on top of the others.
34
+ *
35
+ * ```ts
36
+ * const home = UIScreen(content).widgets(dialog, sheet)
37
+ * scene.widgets(hud)
38
+ * scene.widgets(() => [hud, paused() && pauseMenu])
39
+ * home.widgets.append(toast); home.widgets.remove(toast)
40
+ * home.widgets.list // [dialog, sheet] — never in `children`
41
+ * ```
42
+ */
43
+ export interface WidgetSet<Owner> {
44
+ (...widgets: (UIWidgetChild | UIWidgetChild[])[]): Owner
45
+ (widgets: UIWidgetContent): Owner
46
+ /** Add widgets at the end (on top). */
47
+ append(...widgets: UIWidgetChild[]): void
48
+ /** Take widgets out (they stay valid while you hold them; a dropped one is freed). */
49
+ remove(...widgets: UIWidgetChild[]): void
50
+ /** The widgets, bottom to top — a snapshot read from the runtime. */
51
+ readonly list: UIWidget[]
52
+ }
53
+
54
+ /** @internal What a widget set drives: a screen (its widget tail) or a surface's layer. */
55
+ export interface WidgetHost {
56
+ _setWidgets(content: UIWidgetContent): void
57
+ _appendWidgets(widgets: UINodeChild[]): void
58
+ _removeWidgets(widgets: UINodeChild[]): void
59
+ _widgetList(): UIWidget[]
60
+ }
61
+
62
+ /** @internal `widgets(...)`'s arguments as content: one function → reactive, one array → the
63
+ * list, otherwise the rest arguments with arrays among them flattened (as the UI factories). */
64
+ export const _widgetContent = (args: any[]): UIWidgetContent => {
65
+ if (args.length === 1 && (typeof args[0] === "function" || Array.isArray(args[0]))) return args[0]
66
+ const flat: UIWidgetChild[] = []
67
+ for (const a of args) {
68
+ if (Array.isArray(a)) for (const w of a) flat.push(w)
69
+ else flat.push(a)
70
+ }
71
+ return flat
72
+ }
73
+
74
+ /** @internal The widget set of `owner` over `host()` (resolved at every call: a surface's layer
75
+ * is made on first use). */
76
+ export const _makeWidgetSet = <O>(owner: O, host: () => WidgetHost): WidgetSet<O> => {
77
+ const set = ((...args: any[]) => { host()._setWidgets(_widgetContent(args)); return owner }) as WidgetSet<O>
78
+ set.append = (...widgets) => { host()._appendWidgets(widgets as UINodeChild[]) }
79
+ set.remove = (...widgets) => { host()._removeWidgets(widgets as UINodeChild[]) }
80
+ Object.defineProperty(set, "list", { get: () => host()._widgetList() })
81
+ return set
82
+ }
19
83
 
20
84
  /** @internal A layer root: a container whose children are widgets. */
21
- export class LayerElement extends ContainerElement<"layer"> {
85
+ export class LayerElement extends ContainerElement<"layer"> implements WidgetHost {
22
86
  constructor() {
23
87
  super("layer", {}, [])
24
88
  }
89
+ _setWidgets(content: UIWidgetContent): void { this.setContent(content as UINodeChild[] | ChildrenFn) }
90
+ _appendWidgets(widgets: UINodeChild[]): void { this.append(...widgets) }
91
+ _removeWidgets(widgets: UINodeChild[]): void { this.remove(...widgets) }
92
+ _widgetList(): UIWidget[] { return this.children as UIWidget[] }
25
93
  }
26
94
 
27
- /** @internal A surface destination that can hold its content layer. */
95
+ /** @internal A surface destination that holds its widgets in a layer of its own. */
28
96
  export interface LayerOwner {
29
97
  _contentLayer?: LayerElement
98
+ _widgetSet?: WidgetSet<any>
30
99
  _dest(): DestTuple
31
100
  }
32
101
 
33
- /** @internal The content layer of a surface destination, made on first use and bound to the
34
- * destination (`setLayer(kind, id, layer)`); the runtime shows it while the surface is presented. */
102
+ /** @internal The layer of a surface destination, made on first use and bound to the destination
103
+ * (`setLayer(kind, id, layer)`); the runtime shows it while the surface is presented. */
35
104
  export const _destLayer = (owner: LayerOwner): LayerElement => {
36
105
  if (!owner._contentLayer) {
37
106
  const layer = new LayerElement()
@@ -43,15 +112,18 @@ export const _destLayer = (owner: LayerOwner): LayerElement => {
43
112
  return owner._contentLayer
44
113
  }
45
114
 
46
- /** @internal `setContent` of a surface: its widgets, as a list or a reactive function. */
47
- export const _setSurfaceContent = (owner: LayerOwner, content: UIWidgetContent): void => {
48
- _destLayer(owner).setContent(content as UINodeChild[] | ChildrenFn)
115
+ /** @internal The `widgets` of a surface: one set per owner, over its layer. */
116
+ export const _surfaceWidgets = <O extends LayerOwner>(owner: O): WidgetSet<O> => {
117
+ if (!owner._widgetSet) owner._widgetSet = _makeWidgetSet(owner, () => _destLayer(owner))
118
+ return owner._widgetSet as WidgetSet<O>
49
119
  }
50
120
 
51
121
  // ---- the app layer ------------------------------------------------------------------------------------
52
122
  let appLayer: LayerElement | null = null
53
123
 
54
- const layer = (): LayerElement => {
124
+ /** @internal The app layer, made on first use: above every destination, always shown — where a
125
+ * widget with no parent lands on `show()` (UIWidget), where a popover with no anchor goes. */
126
+ export const _appLayer = (): LayerElement => {
55
127
  if (!appLayer) {
56
128
  appLayer = new LayerElement()
57
129
  tree().setLayer(TREE_DEST_NONE, 0, appLayer._h.id)
@@ -62,27 +134,3 @@ const layer = (): LayerElement => {
62
134
 
63
135
  /** @internal Tests: a new fake runtime has no app layer of the previous one. */
64
136
  export const _resetOverlay = (): void => { appLayer = null }
65
-
66
- /**
67
- * The app layer: the widgets above EVERY destination, always shown — a global loader, a mini-player
68
- * that follows the user across screens, a toast with actions. Widgets that belong to a place go in
69
- * that place instead: a dialog among its screen's children (`UIScreen(..., dialog)`), a HUD in
70
- * `scene.setContent([hud])`. A widget shown with `show()` while it has no parent lands here.
71
- *
72
- * ```ts
73
- * UIOverlay.append(miniPlayer)
74
- * miniPlayer.hide() // display: none — still here, ready for show()
75
- * ```
76
- */
77
- export const UIOverlay = /*#__PURE__*/ Object.freeze({
78
- /** Add widgets at the end (on top). */
79
- append(...widgets: (UIWidget | null | undefined | false)[]): void { layer().append(...(widgets as UINodeChild[])) },
80
- /** Insert widgets at `index` (0 = the bottom of the layer). */
81
- insert(index: number, ...widgets: (UIWidget | null | undefined | false)[]): void { layer().insert(index, ...(widgets as UINodeChild[])) },
82
- /** Take widgets out of the layer (they stay valid; a dropped one is freed). */
83
- remove(...widgets: (UIWidget | null | undefined | false)[]): void { layer().remove(...(widgets as UINodeChild[])) },
84
- /** Replace the layer's widgets — a list, or a function for reactive content. */
85
- setContent(content: UIWidgetContent): void { layer().setContent(content as UINodeChild[] | ChildrenFn) },
86
- /** The layer's widgets, bottom to top (a snapshot read from the runtime). */
87
- get children(): UIWidget[] { return appLayer ? (appLayer.children as UIWidget[]) : [] },
88
- })
package/src/ui/UINode.ts CHANGED
@@ -798,11 +798,11 @@ export function buildUI<R>(args: any[], defaults: any, make: (style: any, childr
798
798
  const acceptChild = (parent: { type: string }, node: any): boolean => {
799
799
  if (!node) return false
800
800
  if (node instanceof Element) {
801
- // A widget belongs to a WINDOW, never to a box (docs/plans/widgets-plan.md decision 9): a
802
- // child of a screen root, a surface's content or the app layer only. The runtime refuses it
803
- // too; this says what to write instead.
804
- if (node.type === "widget" && parent.type !== "screen" && parent.type !== "layer") {
805
- throw new Error(`${parent.type}: a widget cannot be a child of a ${parent.type}. A widget (UIWidget / UIModal / UIBottomSheet / UIPopover) belongs to the window: put it among the screen's children — UIScreen(content, dialog) — or in scene.setContent([...]) / UIOverlay. A floating element of a box is an absolute child: UIBox(...).style({ position: "absolute", ... }), not a widget.`)
801
+ // A widget is never a CHILD (docs/plans/widgets-plan.md decision 9): it is one of the
802
+ // `widgets` of a screen or a surface — a layer's children are widgets, and only a widget set
803
+ // puts one there (acceptWidget). This says what to write instead.
804
+ if (node.type === "widget" && parent.type !== "layer") {
805
+ throw new Error(`${parent.type}: a widget (UIWidget / UIModal / UIBottomSheet / UIPopover) is not a child. It belongs to the widgets of a screen or a surface: UIScreen(content).widgets(dialog), scene.widgets(hud). A floating element of a box is an absolute child: UIBox(...).style({ position: "absolute", ... }), not a widget.`)
806
806
  }
807
807
  return true
808
808
  }
@@ -817,6 +817,16 @@ const acceptChild = (parent: { type: string }, node: any): boolean => {
817
817
  return false
818
818
  }
819
819
 
820
+ /** A widget slot of `widgets(...)` takes a widget (falsy = conditional, skipped silently);
821
+ * anything else is reported and skipped. */
822
+ export const acceptWidget = (owner: { type: string }, node: any): boolean => {
823
+ if (!node) return false
824
+ if (node instanceof Element && node.type === "widget") return true
825
+ const what = node instanceof Element ? `a ${node.type}` : typeof node === "object" ? (node.constructor?.name || "object") : typeof node
826
+ console.error(`${owner.type}.widgets: not a widget (${what}) — skipped. widgets() takes UIWidget / UIModal / UIBottomSheet / UIPopover; content goes among the children`)
827
+ return false
828
+ }
829
+
820
830
  export class ContainerElement<T extends string> extends Element<T> {
821
831
  constructor(type: T, style: any, children: any[] | ChildrenFn) {
822
832
  super(type, style)
@@ -855,13 +865,19 @@ export class ContainerElement<T extends string> extends Element<T> {
855
865
  _reconcile(next: UINodeChild[]) {
856
866
  const target: any[] = []
857
867
  for (const c of next) if (acceptChild(this, c)) target.push(c)
858
- if (this._h.id === 0) return
868
+ this._reconcileList(target, () => this.children, (i, node) => this._insertAt(i, node))
869
+ }
859
870
 
860
- const current: any[] = this.children
871
+ /** @internal The diff of `_reconcile` over any list of this node's children: `current` reads
872
+ * the mounted ones (a screen's content, or its widget tail), `insertAt` mounts one at an index
873
+ * of that list. */
874
+ _reconcileList(target: any[], current: () => any[], insertAt: (index: number, node: any) => void) {
875
+ if (this._h.id === 0) return
876
+ const mounted: any[] = current()
861
877
  const targetSet = new Set(target)
862
- const departed = current.filter(c => !targetSet.has(c))
878
+ const departed = mounted.filter(c => !targetSet.has(c))
863
879
  if (departed.length > 0) this.remove(...departed)
864
- const cur: any[] = departed.length > 0 ? this.children : current
880
+ const cur: any[] = departed.length > 0 ? current() : mounted
865
881
 
866
882
  // `cur` mirrors the mounted order; walk the target and fix mismatches in place
867
883
  for (let i = 0; i < target.length; i++) {
@@ -872,12 +888,15 @@ export class ContainerElement<T extends string> extends Element<T> {
872
888
  this.remove(node) // moved forward — remount at its new position
873
889
  cur.splice(j, 1)
874
890
  }
875
- this._insertAt(i, node)
891
+ insertAt(i, node)
876
892
  cur.splice(i, 0, node)
877
893
  }
878
894
  }
879
895
 
880
- private _insertAt(index: number, node: any): void {
896
+ /** @internal Mount `node` at `index` of the runtime's child list (a screen: its content, then
897
+ * its widgets — tree.cpp treeInsert clamps a content index into the content, a widget's into
898
+ * the tail). */
899
+ _insertAt(index: number, node: any): void {
881
900
  if (this._h.id === 0 || !node || node._h.id === 0) return
882
901
  tree().insert(this._h.id, node._h.id, index)
883
902
  pin(node)
@@ -58,7 +58,7 @@ export class PopoverElement extends ModalElement {
58
58
  }
59
59
  // A menu belongs where it opened: the anchor's root — a screen, a page, a surface's layer.
60
60
  const root = anchorRoot(anchor)
61
- if (root && this.parent !== root) root.append(this)
61
+ if (root && this.parent !== root) root._appendWidgets([this])
62
62
  } else {
63
63
  this._anchorRect = undefined // unanchored show: the user's own style owns the position
64
64
  }
@@ -92,13 +92,13 @@ export class PopoverElement extends ModalElement {
92
92
  // The root an anchor element sits in, when it is a place a widget can belong to: a screen or a
93
93
  // layer that is ATTACHED (presented, in the router, a pager's page, a shown layer — the pins find
94
94
  // it by id); null for a point, a loose subtree, or a screen that is not on any stack.
95
- const anchorRoot = (anchor: UIPopoverAnchor): { append(...nodes: any[]): unknown, type: string } | null => {
95
+ const anchorRoot = (anchor: UIPopoverAnchor): { _appendWidgets(nodes: any[]): void, type: string } | null => {
96
96
  const h = (anchor as any)._h
97
97
  if (!h || typeof h.id !== "number" || h.id === 0) return null
98
98
  let id = h.id as number
99
99
  for (let up = tree().parent(id); up !== 0; up = tree().parent(id)) id = up
100
100
  const el = byId(id) as any
101
- return el && typeof el.append === "function" && (el.type === "screen" || el.type === "layer") ? el : null
101
+ return el && typeof el._appendWidgets === "function" && (el.type === "screen" || el.type === "layer") ? el : null
102
102
  }
103
103
 
104
104
  const makePopover = (style: any, children: any) => new PopoverElement(style, children)
@@ -1,7 +1,10 @@
1
1
  import { TouchStartEvent } from "../runtime/touch"
2
2
  import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "./presentable"
3
3
  import { _transitionId, TRANSITION_NONE } from "./transitions"
4
- import { ContainerElement, buildUI, type BaseStyle, type ContainerStyle, type DrawableStyle, type PaddingStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, } from "./UINode"
4
+ import { createBinding, disposeBinding } from "../core/signals"
5
+ import { ContainerElement, acceptWidget, buildUI, type BaseStyle, type ContainerStyle, type DrawableStyle, type PaddingStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, } from "./UINode"
6
+ import { _makeWidgetSet, type UIWidgetContent, type WidgetHost, type WidgetSet } from "./UILayer"
7
+ import type { UIWidget } from "./UIWidget"
5
8
  import { TREE_DEST_SCREEN, TREE_FLAG_BACK, TREE_FLAG_INTERACTIVE, TREE_FLAG_KEEP_ALIVE, pin, tree } from "./tree"
6
9
  import { autofocusIn } from "./autofocus"
7
10
 
@@ -57,16 +60,61 @@ export interface UIScreen extends UIContainerBase<UIScreenStyle, DrawableStyle &
57
60
 
58
61
  /** Touch began on the screen; `ev.track(...)` takes over the rest of the gesture. */
59
62
  onTouchStart(callback: (ev: TouchStartEvent<UIScreen>) => void): this,
63
+
64
+ /** The screen's widgets — a dialog, a sheet, a toast: `UIScreen(content).widgets(dialog)` sets
65
+ * them, `widgets.append` / `.remove` manage them, `widgets.list` reads them. Never among
66
+ * `children`: that is the content. A widget is laid out and painted over the whole screen (on a
67
+ * pager page: over the pager too) and shows with it unless its style says `display: "none"`. */
68
+ readonly widgets: WidgetSet<this>,
60
69
  /** @internal The destination tuple (Presentable). */
61
70
  _dest(): DestTuple
62
71
  }
63
72
 
64
- export class ScreenElement extends ContainerElement<"screen"> {
73
+ export class ScreenElement extends ContainerElement<"screen"> implements WidgetHost {
65
74
  // The per-type default: a screen's background is the theme's `bg` (no theme = none, the host's).
66
75
  constructor(type: "screen", style: any, children: any) {
67
76
  super(type, { bgColor: "var(--bg)", ...style }, children)
68
77
  }
69
78
 
79
+ // ---- content vs widgets: ONE list in the runtime (the content, then the widget tail), two faces here
80
+ /** The content — the children read from the runtime without the widget tail. */
81
+ override get children(): any[] {
82
+ return super.children.filter((c: any) => c.type !== "widget")
83
+ }
84
+ private _widgetSet?: WidgetSet<this>
85
+ get widgets(): WidgetSet<this> {
86
+ if (!this._widgetSet) this._widgetSet = _makeWidgetSet(this, () => this)
87
+ return this._widgetSet
88
+ }
89
+ /** @internal */
90
+ _widgetList(): UIWidget[] {
91
+ return super.children.filter((c: any) => c.type === "widget") as UIWidget[]
92
+ }
93
+ /** @internal The widget tail; an index past every child lands at its end (tree.cpp). */
94
+ _appendWidgets(widgets: UINodeChild[]): void {
95
+ if (this._h.id === 0) return
96
+ for (const w of widgets) if (acceptWidget(this, w)) this._insertAt(tree().children(this._h.id).length, w)
97
+ }
98
+ /** @internal */
99
+ _removeWidgets(widgets: UINodeChild[]): void { this.remove(...widgets) }
100
+ /** @internal `widgets(...)`: the list replaced, or a reactive function bound as the content's is. */
101
+ _setWidgets(content: UIWidgetContent): void {
102
+ if (typeof content === "function") {
103
+ createBinding(this, "#widgets", () => {
104
+ const next = content()
105
+ const target: any[] = []
106
+ for (const w of (Array.isArray(next) ? next : [])) if (acceptWidget(this, w)) target.push(w)
107
+ // a widget's index is counted from the first widget: offset by the content (treeInsert)
108
+ this._reconcileList(target, () => this._widgetList(), (i, node) => this._insertAt(this.children.length + i, node))
109
+ })
110
+ return
111
+ }
112
+ disposeBinding(this, "#widgets")
113
+ const current = this._widgetList()
114
+ if (current.length > 0) this.remove(...current)
115
+ this._appendWidgets(content)
116
+ }
117
+
70
118
  /** @internal */
71
119
  _dest(): DestTuple { return [TREE_DEST_SCREEN, this._h.id, "", ""] }
72
120
 
package/src/ui/UIVideo.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { VideoPlayer } from "../runtime/media"
2
- import { _setSurfaceContent, type LayerElement, type UIWidgetContent } from "./UILayer"
2
+ import { _surfaceWidgets, type LayerElement, type WidgetSet } from "./UILayer"
3
3
  import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "./presentable"
4
4
  import { _transitionId, TRANSITION_NONE } from "./transitions"
5
5
  import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type UIElementBase } from "./UINode"
@@ -25,8 +25,8 @@ export interface UIVideo extends UIElementBase<UIVideoStyle, DrawableStyle & Bas
25
25
  onClose(callback: () => void): this
26
26
  /** Hardware/system back while current. */
27
27
  onBack(callback: () => void): this
28
- /** The UI over the video when presented: widgets over it (`Presentable.setContent`). */
29
- setContent(content: UIWidgetContent): this
28
+ /** The widgets over the video when it is presented (`Presentable.widgets`): `video.widgets(controls)`. */
29
+ readonly widgets: WidgetSet<this>
30
30
  /** @internal The destination tuple (Presentable). */
31
31
  _dest(): DestTuple
32
32
  }
@@ -50,13 +50,15 @@ class VideoElement extends Element<"video"> implements Presentable {
50
50
 
51
51
  /** @internal */
52
52
  _dest(): DestTuple { return [TREE_DEST_VIDEO, this._h.id, "", ""] }
53
- /** @internal The video's content layer (ui/UILayer.ts), made by the first setContent. */
53
+ /** @internal The video's widget layer (ui/UILayer.ts), made on the first use of `widgets`. */
54
54
  _contentLayer?: LayerElement
55
- /** The UI over the video when it is presented full screen — widgets (controls, a title),
56
- * laid out over it; touches outside a widget's box reach the video. */
57
- setContent(content: UIWidgetContent): this {
58
- _setSurfaceContent(this, content)
59
- return this
55
+ /** @internal */
56
+ _widgetSet?: WidgetSet<any>
57
+ /** The widgets over the video while it is presented — a HUD, a dialog, a sheet: `video.widgets(hud)`
58
+ * sets them, `widgets.append` / `.remove` manage them, `widgets.list` reads them. Laid out
59
+ * over it; touches outside a widget's box reach the video. */
60
+ get widgets(): WidgetSet<this> {
61
+ return _surfaceWidgets(this)
60
62
  }
61
63
 
62
64
  open(options?: PresentOptions): void {
@@ -1,7 +1,8 @@
1
1
  import { TouchStartEvent } from "../runtime/touch"
2
2
  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"
3
- import { UIOverlay } from "./UILayer"
4
- import { TREE_FLAG_BACK, TREE_FLAG_INTERACTIVE, TREE_FLAG_OVERLAY_TAP, tree } from "./tree"
3
+ import { _appLayer } from "./UILayer"
4
+ import type { Presentable } from "./presentable"
5
+ import { TREE_FLAG_BACK, TREE_FLAG_INTERACTIVE, TREE_FLAG_OVERLAY_TAP, tree, unpin } from "./tree"
5
6
 
6
7
 
7
8
  export type UIWidgetStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle & PositionStyle & {
@@ -14,13 +15,13 @@ export type UIWidgetStyle = ContainerStyle & DrawableStyle & PaddingStyle & Base
14
15
  }
15
16
 
16
17
 
17
- /** A floating overlay that belongs to a WINDOW, not to a box (docs/plans/widgets-plan.md): a dialog,
18
- * a sheet, a HUD, a mini-player. A widget is a CHILD of the place it belongs to — a `UIScreen`
19
- * (among its children), a surface's content (`scene.setContent([hud])`) or the app layer
20
- * (`UIOverlay`) — and is laid out and painted in the box of the outermost presented destination
21
- * that contains it: absolutely positioned in that box, above everything else in it. A widget on a
22
- * tab page covers the tab bar and rides the shell's push and pop. A widget inside a `UIColumn` or
23
- * any other box is an error: a floating element of a box is an absolute child, not a widget.
18
+ /** A floating overlay that belongs to a DESTINATION, not to a box (docs/plans/widgets-plan.md): a
19
+ * dialog, a sheet, a HUD, a mini-player. A widget is one of the `widgets` of the place it belongs
20
+ * to — `UIScreen(content).widgets(dialog)`, `scene.widgets(hud)` — and is laid out and painted in
21
+ * the box of the outermost presented destination that contains it: absolutely positioned in that
22
+ * box, above everything else in it. A widget on a tab page covers the tab bar and rides the
23
+ * shell's push and pop. A widget among the children of a box is an error: a floating element of
24
+ * a box is an absolute child, not a widget.
24
25
  *
25
26
  * It shows with its place unless its style says `display: "none"`; `show()` / `hide()` flip that,
26
27
  * and so does a state block (`$class: { display: "flex" }`). `UIModal` / `UIPopover` /
@@ -28,13 +29,12 @@ export type UIWidgetStyle = ContainerStyle & DrawableStyle & PaddingStyle & Base
28
29
  export interface UIWidget extends UIContainerBase<UIWidgetStyle, DrawableStyle & BaseStyle & { overlayColor?: Color | null }> {
29
30
  readonly type: "widget",
30
31
 
31
- /** Show the widget: `display: "flex"`, on top of its root — it moves to the end of its parent's
32
- * children, so a dialog opened later covers an earlier one and a HUD declared after it. A widget
33
- * with no parent is appended to the app layer first (`UIOverlay`) — a global overlay above every
34
- * destination, up until `hide()`. */
32
+ /** Show the widget: `display: "flex"`, on top of its root — it moves to the end of its
33
+ * destination's widgets, so a dialog opened later covers an earlier one and a HUD declared after
34
+ * it. A widget that belongs nowhere goes ABOVE EVERY destination — a global loader, a dialog
35
+ * opened from anywhere — and stays there, hidden by `hide()`, until it is given a place. */
35
36
  show(): void,
36
- /** Hide the widget: `display: "none"`. It stays where it is (its place, the app layer), ready
37
- * for the next `show()`. */
37
+ /** Hide the widget: `display: "none"`. It stays where it is, ready for the next `show()`. */
38
38
  hide(): void,
39
39
 
40
40
  /** Shown right now by its own display (`show()` ran, `hide()` hasn't, no state block hides
@@ -42,6 +42,14 @@ export interface UIWidget extends UIContainerBase<UIWidgetStyle, DrawableStyle &
42
42
  * visibility, not the widget's. */
43
43
  readonly isShown: boolean,
44
44
 
45
+ /** @deprecated A widget is one of the `widgets` of the place it belongs to (2026-10-07): write
46
+ * `UIScreen(content).widgets(widget)` / `scene.widgets(widget)` instead. This alias is
47
+ * `owner.widgets.append(widget)` with the widget hidden, so the `show()` that follows flips it as
48
+ * on any placed widget; `attachTo(null)` takes it out, and the next `show()` puts it above every
49
+ * destination. For the projects compiled before the change; removed with the release that
50
+ * recompiles them. */
51
+ attachTo(owner: Presentable | null): this,
52
+
45
53
  /** Touch began on the widget; `ev.track(...)` takes over the rest of the gesture. */
46
54
  onTouchStart(callback: (ev: TouchStartEvent<UIWidget>) => void): this,
47
55
  /** Tap on the overlay layer (requires an `overlayColor`). */
@@ -74,7 +82,7 @@ export class WidgetElement extends ContainerElement<"widget"> {
74
82
  // is a display flip wherever the widget is, so its pose plays (a modal's fade) exactly as on
75
83
  // a placed one. Appended shown, it would merely appear: a widget shows with its place, no pose.
76
84
  this.style({ display: "none" } as any)
77
- UIOverlay.append(this as any) // pins it: _pinned reads the display
85
+ _appLayer().append(this as any) // pins it: _pinned reads the display
78
86
  } else {
79
87
  // Shown = on top of its root: a show() moves the widget to the TAIL of its parent's children
80
88
  // (child order is the one ordering rule), so a modal opened later covers an earlier one and
@@ -97,6 +105,21 @@ export class WidgetElement extends ContainerElement<"widget"> {
97
105
  get isShown(): boolean {
98
106
  return this._shown
99
107
  }
108
+ /** @deprecated see the interface */
109
+ attachTo(owner: Presentable | null): this {
110
+ if (this._h.id === 0) return this
111
+ const parentId = tree().parent(this._h.id)
112
+ if (parentId !== 0) { // one parent: a re-attach moves it
113
+ tree().remove(parentId, this._h.id)
114
+ unpin(this._h.id) // as a node's remove(): shown nowhere
115
+ }
116
+ if (owner) {
117
+ // Hidden first: a widget shows with its place, and the old contract showed it on show() only.
118
+ this.style({ display: "none" } as any)
119
+ owner.widgets.append(this as any)
120
+ }
121
+ return this
122
+ }
100
123
  /** @internal The intent changed (the app's call, or the runtime's report of a cascade flip):
101
124
  * subclasses hook the change (UIModal's onOpen / onClose). */
102
125
  _setShown(shown: boolean): void {
@@ -163,8 +186,8 @@ const makeWidget = (style: any, children: any) => new WidgetElement("widget", st
163
186
  /** @internal Compiler fast path (chisel `flatten_ui` raw lowering) — see UIContainer. */
164
187
  export const __UIWidget = (children: UINodeChild[] | ChildrenFn): UIWidget => makeWidget({}, children)
165
188
 
166
- /** Create a widget (same argument forms as `UIColumn`). Put it where it belongs — among a screen's
167
- * children, in `scene.setContent([...])`, or `UIOverlay` — position it with absolute-style props
189
+ /** Create a widget (same argument forms as `UIColumn`). Give it to the destination it belongs to —
190
+ * `UIScreen(content).widgets(w)`, `scene.widgets(w)` — position it with absolute-style props
168
191
  * (`top`/`left`/`bottom`/`right`), then `show()`/`hide()`. */
169
192
  export function UIWidget(...children: UIChildArg[]): UIWidget;
170
193
  export function UIWidget(children: UINodeChild[] | ChildrenFn): UIWidget;
@@ -2,7 +2,7 @@
2
2
  // Anything that can occupy the app's main slot implements it: UIScreen, Scene (+ARScene),
3
3
  // Scene2D, NativeView, UIVideo. Exactly one Presentable is visible at a time — a screen no
4
4
  // longer overlays a scene, it replaces it (with a transition); UI over a scene goes through
5
- // `scene.setContent([hud])` — a widget is the destination's content (docs/plans/widgets-plan.md).
5
+ // `scene.widgets(hud)` — a widget belongs to a destination (docs/plans/widgets-plan.md).
6
6
  //
7
7
  // A destination is addressed by (kind, id): a screen / video by its node id, a scene by its
8
8
  // scene id, a native view by its viewId (`_dest()`). The runtime reports the lifecycle back through
@@ -12,7 +12,7 @@
12
12
  // element code into engine-only bundles.
13
13
 
14
14
  import type { EasingInput } from "../animate/tween/easing"
15
- import type { UIWidget } from "./UIWidget"
15
+ import type { WidgetSet } from "./UILayer"
16
16
 
17
17
  /** The built-in transitions (ui/transitions.ts holds what each one is). `push` / `pop` are the
18
18
  * stacked-navigation pair — the incoming screen slides over the other one, which drifts and dims;
@@ -101,12 +101,12 @@ export interface Presentable {
101
101
  onOpen(callback: () => void): this
102
102
  onClose(callback: () => void): this
103
103
  onBack(callback: () => void): this
104
- /** The destination's widgets (docs/plans/widgets-plan.md): a dialog, a HUD, a sheet declared as
105
- * CONTENT of the place they belong to. A `UIScreen` takes them among its children; a surface — a
106
- * `Scene`, a `Scene2D`, a `NativeView`, a `UIVideo` — takes widgets alone, laid out over it while
107
- * it is presented (touches outside a widget's box reach the surface). A widget shows with its
108
- * destination unless its style says `display: "none"`; `show()` / `hide()` flip that. */
109
- setContent(content: (UIWidget | null | undefined | false)[] | (() => (UIWidget | null | undefined | false)[])): this
104
+ /** The destination's widgets (docs/plans/widgets-plan.md): a dialog, a HUD, a sheet — what floats
105
+ * over it. `x.widgets(a, b)` sets them, `x.widgets.append` / `.remove` manage them, `x.widgets.list`
106
+ * reads them; the same on a `UIScreen` (apart from its `children`), a `Scene`, a `Scene2D`, a
107
+ * `NativeView`, a `UIVideo`. A widget shows with its destination unless its style says
108
+ * `display: "none"`; `show()` / `hide()` flip that. */
109
+ readonly widgets: WidgetSet<this>
110
110
  /** @internal Async work that must finish BEFORE this destination replaces the current one
111
111
  * (camera permission, warm render, session launch — ARScene). The previous destination —
112
112
  * typically a loading screen — stays visible while it runs; `open()` and `Router.push/replace`
package/src/version.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // same manifest). Its MAJOR is the bundle ↔ runtime contract: a bundle's `// sdk:` header line
5
5
  // and a host's embedded version must agree on the major for the host to run the bundle; minor
6
6
  // and patch never gate anything (features are detected, never versioned).
7
- export const SDK_VERSION = "2.2.4"
7
+ export const SDK_VERSION = "2.2.5"
8
8
 
9
9
  /** The major of a semver string, or null when it is not one. */
10
10
  export const sdkMajor = (version: string | null | undefined): number | null => {
@@ -87,7 +87,12 @@ export class FakeTree {
87
87
  if (!p || !c) return
88
88
  if (c.parent !== 0) return
89
89
  c.parent = parent
90
- const i = Math.max(0, Math.min(index, p.children.length))
90
+ // tree.cpp treeInsert: a widget is the TAIL of a screen's children (its index counted past the
91
+ // content, clamped into the tail); a content index past the content lands before the widgets.
92
+ const own = p.type === "layer" ? 0 : p.children.filter((x) => this.nodes.get(x)?.type !== "widget").length
93
+ const i = c.type === "widget"
94
+ ? own + Math.max(0, Math.min(index < own ? 0 : index - own, p.children.length - own))
95
+ : Math.max(0, Math.min(index, own))
91
96
  p.children.splice(i, 0, child)
92
97
  }
93
98
  remove = (parent: number, child: number): void => {