lecodes-sdk 2.0.8 → 2.0.9
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 -0
- package/dist/types/g2/Scene2D.d.ts +4 -0
- package/dist/types/gl/Light.d.ts +36 -1
- package/dist/types/gl/Scene.d.ts +4 -0
- package/dist/types/inject.d.ts +1 -1
- package/dist/types/ui/NativeView.d.ts +3 -0
- package/dist/types/ui/UI.d.ts +1 -0
- package/dist/types/ui/UILayer.d.ts +25 -0
- package/dist/types/ui/UIModal.d.ts +20 -16
- package/dist/types/ui/UIPopover.d.ts +5 -3
- package/dist/types/ui/UIVideo.d.ts +3 -0
- package/dist/types/ui/UIWidget.d.ts +25 -24
- package/dist/types/ui/presentable.d.ts +10 -0
- package/dist/types/ui/transitions.d.ts +2 -1
- package/dist/types/ui/tree.d.ts +1 -0
- 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 +3 -3
- package/prompts/3d-scene.md +1 -1
- package/prompts/dist/3d-app.md +4 -4
- package/src/bridges/gl.d.ts +5 -0
- package/src/bridges/tree.d.ts +24 -12
- package/src/chisel.ts +1 -1
- package/src/g2/Scene2D.ts +10 -0
- package/src/gl/Light.ts +78 -2
- package/src/gl/Material.ts +3 -0
- package/src/gl/Scene.ts +11 -1
- package/src/inject.ts +1 -0
- package/src/ui/NativeView.ts +11 -0
- package/src/ui/UI.ts +1 -0
- package/src/ui/UIBottomSheet.ts +8 -7
- package/src/ui/UILayer.ts +88 -0
- package/src/ui/UIModal.ts +59 -43
- package/src/ui/UINode.ts +9 -1
- package/src/ui/UIPopover.ts +23 -9
- package/src/ui/UIVideo.ts +11 -0
- package/src/ui/UIWidget.ts +70 -44
- package/src/ui/presentable.ts +11 -1
- package/src/ui/transitions.ts +36 -7
- package/src/ui/tree.ts +4 -0
- package/src/version.ts +1 -1
- package/tests/helpers/fakeTree.ts +7 -3
package/package.json
CHANGED
|
@@ -66,10 +66,10 @@ export class Spinner extends Aspect<'spinner'> {
|
|
|
66
66
|
// ===== UI OVER AN OPEN SCENE =====
|
|
67
67
|
|
|
68
68
|
// Exactly one Presentable is visible at a time: UIScreen(...).open() REPLACES the scene. A HUD is
|
|
69
|
-
// a UIWidget
|
|
69
|
+
// a UIWidget that is the scene's CONTENT — it shows and hides together with it:
|
|
70
70
|
const hud = UIWidget(UIText('Score: 0').style({ color: 'white', fontSize: 24 }))
|
|
71
71
|
.style({ top: 'max(safe-top, 16px)', left: 16 })
|
|
72
|
-
|
|
72
|
+
scene.setContent([hud])
|
|
73
73
|
|
|
74
74
|
// ===== SCENE FILE MISTAKES — DO NOT DO THESE =====
|
|
75
75
|
|
|
@@ -107,7 +107,7 @@ const { scene, nodes } = await mainScene.open()
|
|
|
107
107
|
|
|
108
108
|
let taps = 0
|
|
109
109
|
const label = UIText('Tap the crate').style({ color: 'white', fontSize: 20, fontWeight: 700 })
|
|
110
|
-
UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 })
|
|
110
|
+
scene.setContent([UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 })])
|
|
111
111
|
|
|
112
112
|
nodes.crate.addEventListener('click', () => { label.text = `Taps: ${++taps}` })
|
|
113
113
|
</file>
|
package/prompts/3d-scene.md
CHANGED
|
@@ -114,6 +114,6 @@ const hud = UIWidget(
|
|
|
114
114
|
scoreLabel = UIText('Score: 0').style({ color: 'white', fontSize: 24, fontWeight: 700 }),
|
|
115
115
|
).style({ top: 'max(safe-top, 16px)', right: 16 })
|
|
116
116
|
|
|
117
|
+
scene.setContent([hud])
|
|
117
118
|
scene.open()
|
|
118
|
-
hud.attachTo(scene).show()
|
|
119
119
|
</file>
|
package/prompts/dist/3d-app.md
CHANGED
|
@@ -775,8 +775,8 @@ const hud = UIWidget(
|
|
|
775
775
|
scoreLabel = UIText('Score: 0').style({ color: 'white', fontSize: 24, fontWeight: 700 }),
|
|
776
776
|
).style({ top: 'max(safe-top, 16px)', right: 16 })
|
|
777
777
|
|
|
778
|
+
scene.setContent([hud])
|
|
778
779
|
scene.open()
|
|
779
|
-
hud.attachTo(scene).show()
|
|
780
780
|
</file>
|
|
781
781
|
|
|
782
782
|
## Scene files (.scene.ts)
|
|
@@ -847,10 +847,10 @@ export class Spinner extends Aspect<'spinner'> {
|
|
|
847
847
|
// ===== UI OVER AN OPEN SCENE =====
|
|
848
848
|
|
|
849
849
|
// Exactly one Presentable is visible at a time: UIScreen(...).open() REPLACES the scene. A HUD is
|
|
850
|
-
// a UIWidget
|
|
850
|
+
// a UIWidget that is the scene's CONTENT — it shows and hides together with it:
|
|
851
851
|
const hud = UIWidget(UIText('Score: 0').style({ color: 'white', fontSize: 24 }))
|
|
852
852
|
.style({ top: 'max(safe-top, 16px)', left: 16 })
|
|
853
|
-
|
|
853
|
+
scene.setContent([hud])
|
|
854
854
|
|
|
855
855
|
// ===== SCENE FILE MISTAKES — DO NOT DO THESE =====
|
|
856
856
|
|
|
@@ -888,7 +888,7 @@ const { scene, nodes } = await mainScene.open()
|
|
|
888
888
|
|
|
889
889
|
let taps = 0
|
|
890
890
|
const label = UIText('Tap the crate').style({ color: 'white', fontSize: 20, fontWeight: 700 })
|
|
891
|
-
UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 })
|
|
891
|
+
scene.setContent([UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 })])
|
|
892
892
|
|
|
893
893
|
nodes.crate.addEventListener('click', () => { label.text = `Taps: ${++taps}` })
|
|
894
894
|
</file>
|
package/src/bridges/gl.d.ts
CHANGED
|
@@ -95,6 +95,11 @@ declare global {
|
|
|
95
95
|
/** Punctual light. `intensity` is luminous POWER in lumens; `falloff` is the metres of
|
|
96
96
|
* influence (filament's own default is 1 m, i.e. invisible, so it is always passed). */
|
|
97
97
|
createPointLight?(entityId: u32, intensity: f64, color: u32, falloff: f64, castShadows: boolean): void
|
|
98
|
+
/** Spot light: the point light's lumens and falloff in a cone along the entity's local -Z. `inner` / `outer` are
|
|
99
|
+
* HALF-angles in radians (outer ≤ π/2); the cone does not change the brightness. Real time only (the bake skips it). */
|
|
100
|
+
createSpotLight?(entityId: u32, intensity: f64, color: u32, falloff: f64, inner: f32, outer: f32, castShadows: boolean): void
|
|
101
|
+
/** A spot light's cone, live — half-angles in radians, as createSpotLight. */
|
|
102
|
+
setLightCone?(entityId: u32, inner: f32, outer: f32): void
|
|
98
103
|
/** The lightmap bake takes this point light as a RECTANGLE of `width` x `height` metres (an area light: a ceiling
|
|
99
104
|
* panel) in the light's local XZ plane, emitting along its local -Y with the same lumens; 0, 0 = a point again. */
|
|
100
105
|
setLightBakeArea?(entityId: u32, width: f32, height: f32): void
|
package/src/bridges/tree.d.ts
CHANGED
|
@@ -112,10 +112,20 @@ declare global {
|
|
|
112
112
|
isViewSupported(name: string): boolean
|
|
113
113
|
viewVersion(name: string): i32
|
|
114
114
|
viewCall(viewId: i32, method: string, args: value, onComplete: (result?: value) => void, onReject: (message: string) => void): void
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
// ---- widgets (docs/plans/widgets-plan.md): a widget is a CHILD — of a screen root, of a
|
|
116
|
+
// destination's content layer, of the app layer — never a root of its own; `insert` /
|
|
117
|
+
// `remove` place it, `display` shows it, the runtime lays it out in the outermost presented
|
|
118
|
+
// destination's box (the portal) and mounts it under that root for the host. ------------
|
|
119
|
+
// The content LAYER of a destination: a "layer" root (a pass-through root laid out at the frame,
|
|
120
|
+
// shown while the destination is presented) whose widget children are the surface's UI — a HUD
|
|
121
|
+
// over a scene, a card over a map. `kind` TREE_DEST_NONE (id 0) = the APP layer, above every
|
|
122
|
+
// destination, always shown. 0 takes the layer away.
|
|
123
|
+
setLayer(kind: u8, id: f64, layer: TreeNodeId): void
|
|
124
|
+
// The pose a widget child enters from and leaves to: a transition id (defineTransition) whose
|
|
125
|
+
// ALONE roles the runtime plays as tracks on the widget when its resolved `display` flips —
|
|
126
|
+
// `none → flex` from the enter pose to rest, `flex → none` from rest to the exit pose, `none`
|
|
127
|
+
// taking effect when the tracks land. TRANSITION_NONE = no pose (the flip is instant).
|
|
128
|
+
setWidgetPose(id: TreeNodeId, transition: i32): void
|
|
119
129
|
|
|
120
130
|
// ---- pager (a "pager" node's children are its pages; TAB-MAJOR order, `lens` = per-tab stack
|
|
121
131
|
// lengths, `index` = the selected tab). A change of the current tab's stack plays a
|
|
@@ -251,11 +261,16 @@ export interface HostUI {
|
|
|
251
261
|
* call's arguments, valid for the call. Settle exactly one of the two callbacks through the
|
|
252
262
|
* dispatch bridge — `onComplete` with the result as ONE value, or with none. */
|
|
253
263
|
viewCall?(viewId: i32, method: string, args: value, onComplete: (result?: value) => void, onReject: (message: string) => void): void
|
|
254
|
-
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
|
|
258
|
-
|
|
264
|
+
/** A LAYER root is shown: a pass-through root the runtime lays out at the frame, above every
|
|
265
|
+
* destination (the app layer) or over the presented surface (a scene's / a native view's / a
|
|
266
|
+
* video's content layer). Its own box is never hit — only its children are — so a touch outside
|
|
267
|
+
* them reaches what is under the layer; it is painted after the destination, layers in open
|
|
268
|
+
* order. Its children are widgets (the runtime inserts them with insertNode, as under a screen
|
|
269
|
+
* root): a widget child with an overlay layer (`overlayColor`, any color — "transparent" too)
|
|
270
|
+
* intercepts every touch outside its own box within its root and reports it as the overlay tap. */
|
|
271
|
+
layerOpen(node: nodeid): void
|
|
272
|
+
/** The layer root is hidden (its view stays; releaseNode says when it is gone). */
|
|
273
|
+
layerClose(node: nodeid): void
|
|
259
274
|
/** Load a font face for `fontFamily` (weight 100–900, style 0 normal / 2 italic) from `url`; settle
|
|
260
275
|
* one of the callbacks through the dispatch bridge. */
|
|
261
276
|
registerFont(url: string, fontFamily: string, weight: i32, style: i32, onComplete: () => void, onReject: (err: string) => void): void
|
|
@@ -289,9 +304,6 @@ export interface HostUI {
|
|
|
289
304
|
* a child (covered by a push, a keepAlive page); a page that is released goes through
|
|
290
305
|
* removeNode right after. */
|
|
291
306
|
pagerDropPage?(node: nodeid, page: nodeid): void
|
|
292
|
-
/** A pager page root (`page`) was mounted under the pager or released (an attachTo widget names the
|
|
293
|
-
* page node as its owner). */
|
|
294
|
-
pagerPageMounted?(page: nodeid, mounted: boolean): void
|
|
295
307
|
/** Optional: the host may PLAY a UI tween track itself — a platform animation interpolating on its
|
|
296
308
|
* own clock (Core Animation on iOS) instead of one style write per runtime frame. `samples` holds
|
|
297
309
|
* the track evaluated at even points of its length, easing included, `lanes` floats each: 1 for a
|
package/src/chisel.ts
CHANGED
|
@@ -129,7 +129,7 @@ export const chiselVocab: ChiselVocab = {
|
|
|
129
129
|
"onSelect", "onChange", "onFocus", "onBlur", "onOverlayTap", "onDetent",
|
|
130
130
|
"onEndReached", "onStartReached",
|
|
131
131
|
"append", "insert", "remove", "setContent", "keepAlive", "url",
|
|
132
|
-
"
|
|
132
|
+
"detents", "dismissible", "transition", "setSourceRect",
|
|
133
133
|
"push", "pop", "popToRoot", "replace", "select",
|
|
134
134
|
// UITabs / UIVirtualizedList / the native views (NativeView, CameraView, MapView, QRScanner)
|
|
135
135
|
"badge", "setData", "update", "prepend", "removeByKey",
|
package/src/g2/Scene2D.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { _installAspectFrames, _installTimeScale, _attachSystem, _detachSystem,
|
|
|
6
6
|
import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "../ui/presentable"
|
|
7
7
|
import { _transitionId, TRANSITION_NONE } from "../ui/transitions"
|
|
8
8
|
import { TREE_DEST_SCENE2D, ensureEmitter, registerDest } from "../ui/tree"
|
|
9
|
+
import { _setSurfaceContent, type LayerElement, type UIWidgetContent } from "../ui/UILayer"
|
|
9
10
|
import { cx, cy, type Vec2Like } from "../math/vec"
|
|
10
11
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
11
12
|
import { Camera2D } from "./Camera2D"
|
|
@@ -188,6 +189,15 @@ export class Scene2D implements Presentable {
|
|
|
188
189
|
registerDest(TREE_DEST_SCENE2D, this.id, this)
|
|
189
190
|
return [TREE_DEST_SCENE2D, this.id, "", ""]
|
|
190
191
|
}
|
|
192
|
+
/** @internal The scene's content layer (ui/UILayer.ts), made by the first setContent. */
|
|
193
|
+
_contentLayer?: LayerElement
|
|
194
|
+
/** The UI over the scene — widgets (a HUD, a dialog), laid out over the scene while it is
|
|
195
|
+
* presented; touches outside a widget's box reach the scene. */
|
|
196
|
+
setContent(content: UIWidgetContent): this {
|
|
197
|
+
ensureEmitter()
|
|
198
|
+
_setSurfaceContent(this, content)
|
|
199
|
+
return this
|
|
200
|
+
}
|
|
191
201
|
/** @internal presentation lifecycle from the runtime */
|
|
192
202
|
_emitOpen(): void { for (const cb of this.ol.slice()) cb() }
|
|
193
203
|
/** @internal */
|
package/src/gl/Light.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Lights. A directional sun (the engine's primary light)
|
|
1
|
+
// Lights. A directional sun (the engine's primary light), punctual point lights and spot lights. Add one to a
|
|
2
2
|
// scene like any node; `intensity` and `color` stay writable so a light can be animated.
|
|
3
3
|
|
|
4
4
|
import { Color, type ColorInput } from "../core/color"
|
|
@@ -80,6 +80,24 @@ export type PointOptions = {
|
|
|
80
80
|
bakeArea?: readonly [number, number]
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
export type SpotOptions = {
|
|
84
|
+
/**
|
|
85
|
+
* Luminous power in lumens, as a point light's. The cone does not change the brightness: widen it and the
|
|
86
|
+
* same lumens light a larger patch at the same level. On its axis a spot is 4x brighter than a point light of
|
|
87
|
+
* the same lumens (its light is not spread over the whole sphere). Default 1000.
|
|
88
|
+
*/
|
|
89
|
+
intensity?: number
|
|
90
|
+
color?: ColorInput
|
|
91
|
+
/** Metres of influence along the cone, the performance knob as for a point light. Default 10. */
|
|
92
|
+
range?: number
|
|
93
|
+
/** The cone's full angle in degrees, up to 180: past it the light contributes nothing. Default 45. */
|
|
94
|
+
angle?: number
|
|
95
|
+
/** The full angle in degrees of the cone's fully lit core; the light fades from it out to `angle`. Default 0.75 x `angle`. */
|
|
96
|
+
innerAngle?: number
|
|
97
|
+
/** A spot's shadow is one 2D shadow map (a point light's is a cubemap); off by default. */
|
|
98
|
+
castShadows?: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
export class Light extends Node {
|
|
84
102
|
private _intensity = 0
|
|
85
103
|
|
|
@@ -159,7 +177,65 @@ export class Light extends Node {
|
|
|
159
177
|
/** Whether this host can create point lights at all. */
|
|
160
178
|
static get supportsPoint(): boolean { return _creator.createPointLight !== undefined }
|
|
161
179
|
|
|
162
|
-
/**
|
|
180
|
+
/** @internal a spot's cone, full angles in degrees (the engine takes half-angles in radians). */
|
|
181
|
+
_angle = 0
|
|
182
|
+
_innerAngle = 0
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A spot light — a flashlight, a headlight, a stage light. It shines along the node's forward (-Z): position
|
|
186
|
+
* it like any node and aim it with `lookAt`. Real time only: the lightmap bake takes no spot lights, so a spot
|
|
187
|
+
* lights the baked statics live too.
|
|
188
|
+
*
|
|
189
|
+
* Feature-detected like `Light.point`: on a host without it the node exists and lights nothing
|
|
190
|
+
* (`Light.supportsSpot`).
|
|
191
|
+
*/
|
|
192
|
+
static spot(options: SpotOptions = {}): Light {
|
|
193
|
+
const light = new Light()
|
|
194
|
+
light._intensity = options.intensity ?? 1000
|
|
195
|
+
light._color = options.color ?? 0xffffff
|
|
196
|
+
light._baked = false
|
|
197
|
+
light._angle = options.angle ?? 45
|
|
198
|
+
light._innerAngle = options.innerAngle ?? light._angle * 0.75
|
|
199
|
+
const [ inner, outer ] = light._cone()
|
|
200
|
+
_creator.createSpotLight?.(
|
|
201
|
+
light.id,
|
|
202
|
+
light._intensity,
|
|
203
|
+
Color.toPackedRgb(light._color),
|
|
204
|
+
options.range ?? 10,
|
|
205
|
+
inner, outer,
|
|
206
|
+
options.castShadows ?? false,
|
|
207
|
+
)
|
|
208
|
+
// the statics of a lightmapped level are on Filament channel 1 alone (see Lightmap)
|
|
209
|
+
_creator.setLightChannel?.(light.id, 1, true)
|
|
210
|
+
return light
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Whether this host can create spot lights. */
|
|
214
|
+
static get supportsSpot(): boolean { return _creator.createSpotLight !== undefined }
|
|
215
|
+
|
|
216
|
+
/** @internal the cone as the engine takes it: half-angles in radians, outer in (0, 90°], inner in [0, outer]. */
|
|
217
|
+
_cone(): [number, number] {
|
|
218
|
+
const rad = Math.PI / 360
|
|
219
|
+
const outer = Math.min(Math.max(this._angle, 0.1), 180) * rad
|
|
220
|
+
const inner = Math.min(Math.max(this._innerAngle, 0), this._angle) * rad
|
|
221
|
+
return [ Math.min(inner, outer), outer ]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** A spot's full cone angle in degrees — live (a flashlight's focus). */
|
|
225
|
+
get angle(): number { return this._angle }
|
|
226
|
+
set angle(value: number) {
|
|
227
|
+
this._innerAngle *= this._angle > 0 ? value / this._angle : 0
|
|
228
|
+
this._angle = value
|
|
229
|
+
_creator.setLightCone?.(this.id, ...this._cone())
|
|
230
|
+
}
|
|
231
|
+
/** A spot's fully lit core, full angle in degrees — live. Setting `angle` scales it along. */
|
|
232
|
+
get innerAngle(): number { return this._innerAngle }
|
|
233
|
+
set innerAngle(value: number) {
|
|
234
|
+
this._innerAngle = value
|
|
235
|
+
_creator.setLightCone?.(this.id, ...this._cone())
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Live intensity (sun: lux, point / spot: lumens) — animate a flash without rebuilding the light. */
|
|
163
239
|
get intensity(): number { return this._intensity }
|
|
164
240
|
set intensity(value: number) {
|
|
165
241
|
this._intensity = value
|
package/src/gl/Material.ts
CHANGED
|
@@ -187,6 +187,9 @@ export class Material {
|
|
|
187
187
|
} else if ("_h" in (shader as any)) {
|
|
188
188
|
this.shader = shader as FetchResponse
|
|
189
189
|
this._h = _creator.createMaterial((shader as any)._h.id) // owned: dropped and unused → freed
|
|
190
|
+
// 0xFFFFFFFF = the engine refused the package: no shader in it for the running backend (a
|
|
191
|
+
// phone on Vulkan handed the mobile `_opengl` artifact — the host's log names what is missing).
|
|
192
|
+
if (this._h.id === 0xFFFFFFFF) throw new Error("Material: the engine refused the shader (no variant for its backend — see the host log)")
|
|
190
193
|
} else {
|
|
191
194
|
this.shader = "unknown"
|
|
192
195
|
this._h = shader as Handle // an instance the runtime already wrapped for us
|
package/src/gl/Scene.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { _installAspectFrames, _installTimeScale, _attachSystem, _detachSystem,
|
|
|
10
10
|
import { _bumpNavEpoch, _navEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "../ui/presentable"
|
|
11
11
|
import { _transitionId, TRANSITION_NONE } from "../ui/transitions"
|
|
12
12
|
import { TREE_DEST_SCENE3D, TREE_FLAG_BACK, ensureEmitter, registerDest, unregisterDest } from "../ui/tree"
|
|
13
|
+
import { _setSurfaceContent, type LayerElement, type UIWidgetContent } from "../ui/UILayer"
|
|
13
14
|
import { device } from "../runtime/device"
|
|
14
15
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
15
16
|
import type { FetchResponse } from "../runtime/fetch"
|
|
@@ -478,10 +479,19 @@ export class Scene implements Presentable {
|
|
|
478
479
|
|
|
479
480
|
/** @internal The destination tuple (ui/presentable.ts). */
|
|
480
481
|
_dest(): DestTuple {
|
|
481
|
-
// Addressed by the router /
|
|
482
|
+
// Addressed by the router / its content layer: make sure the (kind, id) resolves back to us.
|
|
482
483
|
registerDest(TREE_DEST_SCENE3D, this._id, this)
|
|
483
484
|
return [TREE_DEST_SCENE3D, this._id, "", ""]
|
|
484
485
|
}
|
|
486
|
+
/** @internal The scene's content layer (ui/UILayer.ts), made by the first setContent. */
|
|
487
|
+
_contentLayer?: LayerElement
|
|
488
|
+
/** The UI over the scene — widgets (a HUD, a dialog), laid out over the scene while it is
|
|
489
|
+
* presented; touches outside a widget's box reach the scene. `scene.setContent([hud])`. */
|
|
490
|
+
setContent(content: UIWidgetContent): this {
|
|
491
|
+
ensureEmitter()
|
|
492
|
+
_setSurfaceContent(this, content)
|
|
493
|
+
return this
|
|
494
|
+
}
|
|
485
495
|
/** @internal presentation lifecycle from the runtime */
|
|
486
496
|
_emitOpen(): void { for (const cb of this.ol.slice()) cb() }
|
|
487
497
|
/** @internal */
|
package/src/inject.ts
CHANGED
|
@@ -102,6 +102,7 @@ export {
|
|
|
102
102
|
UITabs, type UITabDef,
|
|
103
103
|
defineTabs, type TabDef, type TabsHandle,
|
|
104
104
|
UIWidget, type UIWidgetStyle,
|
|
105
|
+
UIOverlay, type UIWidgetContent,
|
|
105
106
|
UIModal, type UIModalStyle,
|
|
106
107
|
UIBottomSheet, type UIBottomSheetStyle,
|
|
107
108
|
UIPopover, type UIPopoverStyle,
|
package/src/ui/NativeView.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// wrapper convention.
|
|
11
11
|
|
|
12
12
|
import { _channelCall, _channelEmit, _channelOff, _channelOn, type ChannelListeners } from "../runtime/channel"
|
|
13
|
+
import { _setSurfaceContent, type LayerElement, type UIWidgetContent } from "./UILayer"
|
|
13
14
|
import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "./presentable"
|
|
14
15
|
import { _transitionId, TRANSITION_NONE } from "./transitions"
|
|
15
16
|
import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type UIElementBase } from "./UINode"
|
|
@@ -45,6 +46,8 @@ export interface NativeView extends UIElementBase<UINativeViewStyle, DrawableSty
|
|
|
45
46
|
/** Subscribe to an event the native view emits (`map.on("markerTap", cb)`). */
|
|
46
47
|
on(event: string, callback: (data?: any) => void): this
|
|
47
48
|
off(event: string, callback: (data?: any) => void): this
|
|
49
|
+
/** The UI over the view when presented: widgets over it (`Presentable.setContent`). */
|
|
50
|
+
setContent(content: UIWidgetContent): this
|
|
48
51
|
/** @internal The destination tuple (Presentable). */
|
|
49
52
|
_dest(): DestTuple
|
|
50
53
|
}
|
|
@@ -85,6 +88,14 @@ export class NativeViewElement extends Element<"native"> implements Presentable
|
|
|
85
88
|
|
|
86
89
|
/** @internal */
|
|
87
90
|
_dest(): DestTuple { return [TREE_DEST_NATIVE, this._viewId, this.viewName, this._paramsJson] }
|
|
91
|
+
/** @internal The view's content layer (ui/UILayer.ts), made by the first setContent. */
|
|
92
|
+
_contentLayer?: LayerElement
|
|
93
|
+
/** The UI over the view when it is presented full screen — widgets (a header, a card, a sheet
|
|
94
|
+
* over a map), laid out over it; touches outside a widget's box reach the view. */
|
|
95
|
+
setContent(content: UIWidgetContent): this {
|
|
96
|
+
_setSurfaceContent(this, content)
|
|
97
|
+
return this
|
|
98
|
+
}
|
|
88
99
|
|
|
89
100
|
open(options?: PresentOptions): void {
|
|
90
101
|
if (!tree().isViewSupported(this.viewName)) {
|
package/src/ui/UI.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { UIVideo, type UIVideoStyle } from './UIVideo'
|
|
|
10
10
|
export { UIInput, UITextArea, type UIInputStyle } from './UIInput'
|
|
11
11
|
|
|
12
12
|
export { UIWidget, type UIWidgetStyle } from './UIWidget'
|
|
13
|
+
export { UIOverlay, type UIWidgetContent } from './UILayer'
|
|
13
14
|
export { UIModal, type UIModalStyle } from './UIModal'
|
|
14
15
|
export { UIBottomSheet, type UIBottomSheetStyle } from './UIBottomSheet'
|
|
15
16
|
export { UIPopover, type UIPopoverStyle, type UIPopoverAnchor } from './UIPopover'
|
package/src/ui/UIBottomSheet.ts
CHANGED
|
@@ -67,6 +67,7 @@ export class BottomSheetElement extends ModalElement {
|
|
|
67
67
|
// The inherited show/hide pose animates ONLY the scrim: sheet geometry rides the
|
|
68
68
|
// sheetDetent style key, never the pose system.
|
|
69
69
|
this._transition = { duration: SHEET_ANIM_MS }
|
|
70
|
+
this._syncPose()
|
|
70
71
|
this._addFlags(TREE_FLAG_DETENT)
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -97,7 +98,7 @@ export class BottomSheetElement extends ModalElement {
|
|
|
97
98
|
setDetent(index: number): void {
|
|
98
99
|
const clamped = Math.max(0, Math.min(Math.floor(index), this._detents.length - 1))
|
|
99
100
|
this._detent = clamped
|
|
100
|
-
if (!this.
|
|
101
|
+
if (!this._shown) return // applied by the next show()
|
|
101
102
|
this.style({ sheetDetent: clamped } as UIBottomSheetStyle)
|
|
102
103
|
for (const callback of this._detentListeners) callback(clamped)
|
|
103
104
|
}
|
|
@@ -107,20 +108,20 @@ export class BottomSheetElement extends ModalElement {
|
|
|
107
108
|
return this
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
show(): void {
|
|
111
|
-
if (this.
|
|
111
|
+
override show(): void {
|
|
112
|
+
if (this._shown) return
|
|
112
113
|
// Before the mount, so the built node carries the target and hosts play the entrance to it.
|
|
113
114
|
this.style({ sheetDetent: this._detent } as UIBottomSheetStyle)
|
|
114
115
|
super.show()
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
hide(): void {
|
|
118
|
-
if (!this.
|
|
119
|
-
this.style({ sheetDetent: -1 } as UIBottomSheetStyle) // hosts slide out;
|
|
118
|
+
override hide(): void {
|
|
119
|
+
if (!this._shown) return
|
|
120
|
+
this.style({ sheetDetent: -1 } as UIBottomSheetStyle) // hosts slide out; the hide follows
|
|
120
121
|
super.hide()
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
dismissible(enabled: boolean): this {
|
|
124
|
+
override dismissible(enabled: boolean): this {
|
|
124
125
|
super.dismissible(enabled)
|
|
125
126
|
this.style({ sheetDismissible: enabled ? 1 : 0 } as UIBottomSheetStyle)
|
|
126
127
|
return this
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The LAYER roots widgets live in when they belong to no screen (docs/plans/widgets-plan.md):
|
|
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.
|
|
9
|
+
//
|
|
10
|
+
// 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`.
|
|
13
|
+
import { ContainerElement, type UINodeChild, type ChildrenFn } from "./UINode"
|
|
14
|
+
import type { UIWidget } from "./UIWidget"
|
|
15
|
+
import type { DestTuple } from "./presentable"
|
|
16
|
+
import { TREE_DEST_NONE, pin, tree } from "./tree"
|
|
17
|
+
|
|
18
|
+
export type UIWidgetContent = (UIWidget | null | undefined | false)[] | (() => (UIWidget | null | undefined | false)[])
|
|
19
|
+
|
|
20
|
+
/** @internal A layer root: a container whose children are widgets. */
|
|
21
|
+
export class LayerElement extends ContainerElement<"layer"> {
|
|
22
|
+
constructor() {
|
|
23
|
+
super("layer", {}, [])
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @internal A surface destination that can hold its content layer. */
|
|
28
|
+
export interface LayerOwner {
|
|
29
|
+
_contentLayer?: LayerElement
|
|
30
|
+
_dest(): DestTuple
|
|
31
|
+
}
|
|
32
|
+
|
|
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. */
|
|
35
|
+
export const _destLayer = (owner: LayerOwner): LayerElement => {
|
|
36
|
+
if (!owner._contentLayer) {
|
|
37
|
+
const layer = new LayerElement()
|
|
38
|
+
const [kind, id] = owner._dest()
|
|
39
|
+
tree().setLayer(kind, id, layer._h.id)
|
|
40
|
+
pin(layer) // the runtime holds it (its owner's content) — findable by id while it does
|
|
41
|
+
owner._contentLayer = layer
|
|
42
|
+
}
|
|
43
|
+
return owner._contentLayer
|
|
44
|
+
}
|
|
45
|
+
|
|
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)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- the app layer ------------------------------------------------------------------------------------
|
|
52
|
+
let appLayer: LayerElement | null = null
|
|
53
|
+
|
|
54
|
+
const layer = (): LayerElement => {
|
|
55
|
+
if (!appLayer) {
|
|
56
|
+
appLayer = new LayerElement()
|
|
57
|
+
tree().setLayer(TREE_DEST_NONE, 0, appLayer._h.id)
|
|
58
|
+
pin(appLayer)
|
|
59
|
+
}
|
|
60
|
+
return appLayer
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @internal Tests: a new fake runtime has no app layer of the previous one. */
|
|
64
|
+
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
|
+
})
|