lecodes-sdk 2.2.4 → 2.2.6
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/dist/global.d.ts +2 -1
- package/dist/types/g2/Scene2D.d.ts +5 -4
- package/dist/types/gl/Scene.d.ts +5 -4
- package/dist/types/inject.d.ts +1 -1
- package/dist/types/ui/NativeView.d.ts +3 -3
- package/dist/types/ui/UI.d.ts +1 -1
- package/dist/types/ui/UILayer.d.ts +25 -18
- package/dist/types/ui/UINode.d.ts +13 -1
- package/dist/types/ui/UIScreen.d.ts +11 -1
- package/dist/types/ui/UIVideo.d.ts +3 -3
- package/dist/types/ui/UIWidget.d.ts +24 -15
- package/dist/types/ui/presentable.d.ts +7 -7
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/3d-scene-files.md +2 -2
- package/prompts/3d-scene.md +1 -1
- package/prompts/core.md +1 -1
- package/prompts/ui-design.md +6 -6
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/chisel.ts +1 -1
- package/src/compile/assetMacro.ts +81 -13
- package/src/compile/resourceSlots.ts +45 -0
- package/src/g2/Scene2D.ts +9 -7
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Light.ts +257 -257
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Scene.ts +9 -7
- package/src/gl/animation/DynamicBone.ts +482 -482
- package/src/gl/scenarios.ts +291 -291
- package/src/inject.ts +1 -1
- package/src/ui/NativeView.ts +11 -9
- package/src/ui/UI.ts +1 -1
- package/src/ui/UILayer.ts +90 -42
- package/src/ui/UINode.ts +39 -12
- package/src/ui/UIPopover.ts +3 -3
- package/src/ui/UIScreen.ts +50 -2
- package/src/ui/UIVideo.ts +11 -9
- package/src/ui/UIWidget.ts +41 -18
- package/src/ui/presentable.ts +8 -8
- package/src/version.ts +1 -1
- package/tests/helpers/fakeTree.ts +6 -1
- package/prompts/dist/2d-game.md +0 -1567
- package/prompts/dist/3d-app.md +0 -1709
- package/prompts/dist/ar-app.md +0 -1585
- package/prompts/dist/concept.md +0 -49
- package/prompts/dist/design.md +0 -521
- package/prompts/dist/ui-app.md +0 -1175
package/src/ui/UILayer.ts
CHANGED
|
@@ -1,37 +1,106 @@
|
|
|
1
|
-
// The
|
|
1
|
+
// The WIDGETS of a destination (docs/plans/widgets-plan.md): one face on every destination —
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
34
|
-
*
|
|
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 `
|
|
47
|
-
export const
|
|
48
|
-
|
|
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
|
-
|
|
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
|
@@ -195,7 +195,15 @@ export type TextStyle = {
|
|
|
195
195
|
/** Cap the text at N lines; the box is measured to the clamped height. 0 / omitted = unbounded. */
|
|
196
196
|
lineClamp?: number,
|
|
197
197
|
/** How a clamped line ends. Only meaningful together with `lineClamp`. Default "ellipsis". */
|
|
198
|
-
textOverflow?: "ellipsis" | "clip"
|
|
198
|
+
textOverflow?: "ellipsis" | "clip",
|
|
199
|
+
/**
|
|
200
|
+
* How a paragraph breaks into lines (CSS `text-wrap`; a `\n` stays a hard break in every mode).
|
|
201
|
+
* "wrap" (default) = greedy; "nowrap" = never at a space — the box is measured to the whole line and
|
|
202
|
+
* a line wider than the box it gets is ended per `textOverflow`; "balance" = the lines are made even
|
|
203
|
+
* (the narrowest width that keeps the line count, paragraphs of 2–10 lines); "pretty" = no single
|
|
204
|
+
* word alone on the last line. Inherited.
|
|
205
|
+
*/
|
|
206
|
+
textWrap?: "wrap" | "nowrap" | "balance" | "pretty"
|
|
199
207
|
}
|
|
200
208
|
|
|
201
209
|
/** Reactive style input: every primitive-valued prop also accepts a `() => value` binding that
|
|
@@ -798,11 +806,11 @@ export function buildUI<R>(args: any[], defaults: any, make: (style: any, childr
|
|
|
798
806
|
const acceptChild = (parent: { type: string }, node: any): boolean => {
|
|
799
807
|
if (!node) return false
|
|
800
808
|
if (node instanceof Element) {
|
|
801
|
-
// A widget
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
if (node.type === "widget" && parent.type !== "
|
|
805
|
-
throw new Error(`${parent.type}: a widget
|
|
809
|
+
// A widget is never a CHILD (docs/plans/widgets-plan.md decision 9): it is one of the
|
|
810
|
+
// `widgets` of a screen or a surface — a layer's children are widgets, and only a widget set
|
|
811
|
+
// puts one there (acceptWidget). This says what to write instead.
|
|
812
|
+
if (node.type === "widget" && parent.type !== "layer") {
|
|
813
|
+
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
814
|
}
|
|
807
815
|
return true
|
|
808
816
|
}
|
|
@@ -817,6 +825,16 @@ const acceptChild = (parent: { type: string }, node: any): boolean => {
|
|
|
817
825
|
return false
|
|
818
826
|
}
|
|
819
827
|
|
|
828
|
+
/** A widget slot of `widgets(...)` takes a widget (falsy = conditional, skipped silently);
|
|
829
|
+
* anything else is reported and skipped. */
|
|
830
|
+
export const acceptWidget = (owner: { type: string }, node: any): boolean => {
|
|
831
|
+
if (!node) return false
|
|
832
|
+
if (node instanceof Element && node.type === "widget") return true
|
|
833
|
+
const what = node instanceof Element ? `a ${node.type}` : typeof node === "object" ? (node.constructor?.name || "object") : typeof node
|
|
834
|
+
console.error(`${owner.type}.widgets: not a widget (${what}) — skipped. widgets() takes UIWidget / UIModal / UIBottomSheet / UIPopover; content goes among the children`)
|
|
835
|
+
return false
|
|
836
|
+
}
|
|
837
|
+
|
|
820
838
|
export class ContainerElement<T extends string> extends Element<T> {
|
|
821
839
|
constructor(type: T, style: any, children: any[] | ChildrenFn) {
|
|
822
840
|
super(type, style)
|
|
@@ -855,13 +873,19 @@ export class ContainerElement<T extends string> extends Element<T> {
|
|
|
855
873
|
_reconcile(next: UINodeChild[]) {
|
|
856
874
|
const target: any[] = []
|
|
857
875
|
for (const c of next) if (acceptChild(this, c)) target.push(c)
|
|
858
|
-
|
|
876
|
+
this._reconcileList(target, () => this.children, (i, node) => this._insertAt(i, node))
|
|
877
|
+
}
|
|
859
878
|
|
|
860
|
-
|
|
879
|
+
/** @internal The diff of `_reconcile` over any list of this node's children: `current` reads
|
|
880
|
+
* the mounted ones (a screen's content, or its widget tail), `insertAt` mounts one at an index
|
|
881
|
+
* of that list. */
|
|
882
|
+
_reconcileList(target: any[], current: () => any[], insertAt: (index: number, node: any) => void) {
|
|
883
|
+
if (this._h.id === 0) return
|
|
884
|
+
const mounted: any[] = current()
|
|
861
885
|
const targetSet = new Set(target)
|
|
862
|
-
const departed =
|
|
886
|
+
const departed = mounted.filter(c => !targetSet.has(c))
|
|
863
887
|
if (departed.length > 0) this.remove(...departed)
|
|
864
|
-
const cur: any[] = departed.length > 0 ?
|
|
888
|
+
const cur: any[] = departed.length > 0 ? current() : mounted
|
|
865
889
|
|
|
866
890
|
// `cur` mirrors the mounted order; walk the target and fix mismatches in place
|
|
867
891
|
for (let i = 0; i < target.length; i++) {
|
|
@@ -872,12 +896,15 @@ export class ContainerElement<T extends string> extends Element<T> {
|
|
|
872
896
|
this.remove(node) // moved forward — remount at its new position
|
|
873
897
|
cur.splice(j, 1)
|
|
874
898
|
}
|
|
875
|
-
|
|
899
|
+
insertAt(i, node)
|
|
876
900
|
cur.splice(i, 0, node)
|
|
877
901
|
}
|
|
878
902
|
}
|
|
879
903
|
|
|
880
|
-
|
|
904
|
+
/** @internal Mount `node` at `index` of the runtime's child list (a screen: its content, then
|
|
905
|
+
* its widgets — tree.cpp treeInsert clamps a content index into the content, a widget's into
|
|
906
|
+
* the tail). */
|
|
907
|
+
_insertAt(index: number, node: any): void {
|
|
881
908
|
if (this._h.id === 0 || !node || node._h.id === 0) return
|
|
882
909
|
tree().insert(this._h.id, node._h.id, index)
|
|
883
910
|
pin(node)
|
package/src/ui/UIPopover.ts
CHANGED
|
@@ -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.
|
|
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): {
|
|
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.
|
|
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)
|
package/src/ui/UIScreen.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
|
29
|
-
|
|
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
|
|
53
|
+
/** @internal The video's widget layer (ui/UILayer.ts), made on the first use of `widgets`. */
|
|
54
54
|
_contentLayer?: LayerElement
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 {
|
package/src/ui/UIWidget.ts
CHANGED
|
@@ -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 {
|
|
4
|
-
import {
|
|
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
|
|
18
|
-
* a sheet, a HUD, a mini-player. A widget is
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
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
|
-
|
|
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`).
|
|
167
|
-
*
|
|
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;
|
package/src/ui/presentable.ts
CHANGED
|
@@ -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.
|
|
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 {
|
|
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
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
|
|
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.
|
|
7
|
+
export const SDK_VERSION = "2.2.6"
|
|
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
|
-
|
|
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 => {
|