lecodes-cli 0.7.0 → 0.7.1
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/index.js +3 -3
- package/package.json +3 -3
- package/runtime/sdk/g2/Scene2D.ts +51 -5
- package/runtime/sdk/gl/Model.ts +9 -0
- package/runtime/sdk/gl/Scene.ts +76 -11
- package/runtime/sdk/inject.ts +14 -6
- package/runtime/sdk/plugins/camera.ts +81 -0
- package/runtime/sdk/plugins/geolocation.ts +123 -0
- package/runtime/sdk/plugins/permission.ts +7 -0
- package/runtime/sdk/plugins/qr.ts +61 -24
- package/runtime/sdk/runtime/app.ts +37 -0
- package/runtime/sdk/runtime/appEvents.ts +29 -0
- package/runtime/sdk/runtime/channel.ts +50 -0
- package/runtime/sdk/runtime/clipboard.ts +20 -0
- package/runtime/sdk/runtime/datetime.ts +2 -4
- package/runtime/sdk/runtime/device.ts +137 -4
- package/runtime/sdk/runtime/misc.ts +4 -0
- package/runtime/sdk/runtime/service.ts +82 -0
- package/runtime/sdk/runtime/touch.ts +26 -0
- package/runtime/sdk/ui/NativeView.ts +144 -0
- package/runtime/sdk/ui/UI.ts +5 -1
- package/runtime/sdk/ui/UIButton.ts +26 -3
- package/runtime/sdk/ui/UIInput.ts +2 -2
- package/runtime/sdk/ui/UINode.ts +24 -4
- package/runtime/sdk/ui/UIScreen.ts +63 -29
- package/runtime/sdk/ui/UIScreenHost.ts +3 -2
- package/runtime/sdk/ui/UIText.ts +1 -1
- package/runtime/sdk/ui/UIVideo.ts +48 -2
- package/runtime/sdk/ui/UIWidget.ts +34 -1
- package/runtime/sdk/ui/presentable.ts +116 -0
- package/runtime/sdk/ui/router.ts +73 -29
- package/runtime/sdk-types.json +1 -1
- package/runtime/sdk/runtime/camera.ts +0 -31
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
2
|
-
import type { ClickEvent as ClickEventType, TouchStartEvent as TouchStartEventType } from "../runtime/touch"
|
|
1
|
+
import { ClickEvent, TouchStartEvent, LongPressEvent } from "../runtime/touch"
|
|
2
|
+
import type { ClickEvent as ClickEventType, TouchStartEvent as TouchStartEventType, LongPressEvent as LongPressEventType } from "../runtime/touch"
|
|
3
3
|
import { type ElementStyle, type ContainerStyle, type Style, ContainerElement, type DrawableStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BaseStyle, type AnimateStyle, type Color } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { rippleColor?: Color | "default" }
|
|
@@ -7,12 +7,16 @@ export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { ri
|
|
|
7
7
|
export interface UIButton {
|
|
8
8
|
readonly type: "button",
|
|
9
9
|
|
|
10
|
-
style: Style<this, UIButtonStyle & { onPressed?:
|
|
10
|
+
style: Style<this, UIButtonStyle & { onPressed?: UIButtonStyle & { duration?: number } }>,
|
|
11
11
|
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>,
|
|
12
12
|
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
13
13
|
|
|
14
14
|
onClick(callback: (ev: ClickEventType<UIButton>) => void): this,
|
|
15
15
|
onTouchStart(callback: (ev: TouchStartEventType<UIButton>) => void): this,
|
|
16
|
+
/** Fired when a finger is held on the button past the long-press threshold. The event can
|
|
17
|
+
* `ev.track(...)` the rest of the gesture (like `onTouchStart`), so a hold can flow straight into
|
|
18
|
+
* dragging the element. A handled long-press suppresses the click that would follow the release. */
|
|
19
|
+
onLongPress(callback: (ev: LongPressEventType<UIButton>) => void): this,
|
|
16
20
|
isPressed(): boolean,
|
|
17
21
|
|
|
18
22
|
append(...nodes: UINodeChild[]): this
|
|
@@ -35,6 +39,7 @@ class ButtonElement extends ContainerElement<"button"> {
|
|
|
35
39
|
|
|
36
40
|
readonly clickListeners: any[] = []
|
|
37
41
|
readonly touchStartListeners: any[] = []
|
|
42
|
+
readonly longPressListeners: any[] = []
|
|
38
43
|
|
|
39
44
|
onClick (callback: any): this {
|
|
40
45
|
this.clickListeners.push(callback)
|
|
@@ -65,6 +70,24 @@ class ButtonElement extends ContainerElement<"button"> {
|
|
|
65
70
|
return null
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
onLongPress (callback: any): this {
|
|
74
|
+
this.longPressListeners.push(callback)
|
|
75
|
+
return this
|
|
76
|
+
}
|
|
77
|
+
// Fired by the host when the finger is held past the long-press threshold. Returns null when
|
|
78
|
+
// nothing is listening — the host then lets the trailing click through as usual. Otherwise it
|
|
79
|
+
// returns an object whose presence tells the host to swallow that click, carrying the optional
|
|
80
|
+
// drag handler a listener registered via `ev.track(...)` for the host to drive the gesture.
|
|
81
|
+
_emitLongPress(pointerId: number, clientX: number, clientY: number): any {
|
|
82
|
+
if (this.longPressListeners.length === 0) return null
|
|
83
|
+
const longPressEvent = new LongPressEvent(clientX, clientY, pointerId)
|
|
84
|
+
longPressEvent.target = this
|
|
85
|
+
for (let callback of this.longPressListeners) {
|
|
86
|
+
callback(longPressEvent)
|
|
87
|
+
}
|
|
88
|
+
return { track: (longPressEvent as any)._trackHandler ?? null }
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
isPressed(): boolean {
|
|
69
92
|
if (!this._id) return false
|
|
70
93
|
return _creatorUI.isButtonPressed(this._id)
|
|
@@ -8,7 +8,7 @@ export interface UIInput {
|
|
|
8
8
|
|
|
9
9
|
value: string
|
|
10
10
|
|
|
11
|
-
style: Style<this, UIInputStyle & { onFocused?:
|
|
11
|
+
style: Style<this, UIInputStyle & { onFocused?: UIInputStyle & { duration?: number } }>,
|
|
12
12
|
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
13
13
|
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
14
14
|
|
|
@@ -28,7 +28,7 @@ export interface UITextArea {
|
|
|
28
28
|
|
|
29
29
|
value: string
|
|
30
30
|
|
|
31
|
-
style: Style<this, UIInputStyle & { onFocused?:
|
|
31
|
+
style: Style<this, UIInputStyle & { onFocused?: UIInputStyle & { duration?: number } }>,
|
|
32
32
|
// styleV: StyleFn<this, UIInputStyle>,
|
|
33
33
|
// styleH: StyleFn<this, UIInputStyle>,
|
|
34
34
|
|
package/runtime/sdk/ui/UINode.ts
CHANGED
|
@@ -245,13 +245,33 @@ export type Reactive<T> = { [K in keyof T]: NonNullable<T[K]> extends object ? T
|
|
|
245
245
|
|
|
246
246
|
// User-defined style classes: any `$`-prefixed key in .style() declares a state block (like
|
|
247
247
|
// onPressed, but with any name), toggled from code via setClass/toggleClass. `duration`/`delay`
|
|
248
|
-
// make the swap transition.
|
|
248
|
+
// make the swap transition. Instantiate with the node's own style `T` (see StyleFn below) — a class
|
|
249
|
+
// block accepts everything the node's `.style()` does (e.g. `color` on text, layout on any node),
|
|
250
|
+
// exactly like `onLandscape`/`onPortrait`. Narrowing it (it once was `DrawableStyle & BaseStyle`)
|
|
251
|
+
// wrongly rejected valid props such as `$active: { color }`. Guarded by tests/ui-types.test.ts.
|
|
249
252
|
export type ClassStyles<T> = { [key: `$${string}`]: T & { duration?: number, delay?: number } }
|
|
250
253
|
|
|
251
|
-
|
|
252
|
-
|
|
254
|
+
// NB: do NOT annotate `this: R` here. `this` is a parameter position, so it makes R contravariant,
|
|
255
|
+
// which makes every node type INVARIANT in its own type params (and in `this`). That is what made
|
|
256
|
+
// `UIScreen<false>` unassignable to `UIScreen<boolean>` / `HostedScreen`, and would block any
|
|
257
|
+
// UINode subtype from being assignable to its base. Runtime `this` is already fixed by the
|
|
258
|
+
// `styleFunction.bind(this)` in the `style` getter — the annotation bought nothing and cost variance.
|
|
259
|
+
// R stays only in the (covariant) return, so `.style()` still returns the concrete node type for
|
|
260
|
+
// chaining. Guarded by the type tests in tests/ui-types.test-d.ts.
|
|
261
|
+
// Orientation state blocks — override props applied only in landscape / portrait (like a `$`-class,
|
|
262
|
+
// keyed by device orientation instead of setClass). Same full-`T` rule as class blocks.
|
|
263
|
+
export type OrientationStyles<T> = {
|
|
264
|
+
onLandscape?: T,
|
|
265
|
+
onPortrait?: T,
|
|
266
|
+
/** @deprecated Misspelling of `onPortrait` (no second "r"). Kept so existing projects and published
|
|
267
|
+
* bundles keep working — hosts still honour it — but new code should use `onPortrait`. */
|
|
268
|
+
onPortait?: T,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type StyleFn <R, T extends object> = ((style: Reactive<T> & OrientationStyles<T> & ClassStyles<T>) => R)
|
|
272
|
+
export type Style <R, T extends object> = StyleFn<R,T> & T & OrientationStyles<T> & ClassStyles<T>
|
|
253
273
|
|
|
254
|
-
export type AnimateStyle<R, T extends object> = ((
|
|
274
|
+
export type AnimateStyle<R, T extends object> = ((style: T & { duration?: number, delay?: number, layer?: number, commit?: boolean }) => R)
|
|
255
275
|
|
|
256
276
|
export type AppearStyle <R, T> = (arg: { from: T, duration?: number, delay?: number }) => R
|
|
257
277
|
export type DisappearStyle <R, T> = (arg: { to: T, duration?: number, delay?: number }) => R
|
|
@@ -1,35 +1,41 @@
|
|
|
1
1
|
import { TouchStartEvent } from "../runtime/touch"
|
|
2
|
+
import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
|
|
2
3
|
import { ContainerElement, type AnimateStyle, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type PaddingStyle, type ScrollStyle, type Style, type UINodeChild, type ChildrenFn, } from "./UINode"
|
|
3
4
|
|
|
4
5
|
export type UIScreenStyle = ContainerStyle & DrawableStyle & PaddingStyle & ScrollStyle & BaseStyle & ScrollStyle & { refreshControlColor?: Color }
|
|
5
6
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
7
|
+
// A screen — the root the router/host mounts, sized to its slot. Two flavours share one contract: a
|
|
8
|
+
// plain `UIScreen`, and a `UIScrollableScreen` (a screen that also scrolls). `makeScrollable()`
|
|
9
|
+
// upgrades a plain screen to the scrollable one, where `onScroll`/`onOverscroll`/`onRefresh` live —
|
|
10
|
+
// they mean nothing without scrolling. `UIScrollableScreen extends UIScreen`, so anywhere a
|
|
11
|
+
// `UIScreen` / `HostedScreen` is accepted, a scrollable one works too.
|
|
12
|
+
//
|
|
13
|
+
// This is a subtype split, NOT a `UIScreen<Scrollable>` generic: a generic can't work while `style`
|
|
14
|
+
// is `Style<this,…>`, because `this` is then contravariant and the type becomes invariant in its own
|
|
15
|
+
// params — that was the old `UIScreen<false>`-not-assignable-to-`HostedScreen` bug. See the StyleFn
|
|
16
|
+
// note in UINode.ts; the contract is pinned by tests/ui-types.test.ts.
|
|
17
|
+
export interface UIScreen {
|
|
12
18
|
readonly type: "screen",
|
|
13
|
-
scrollable:
|
|
19
|
+
readonly scrollable: boolean,
|
|
14
20
|
style: Style<this, UIScreenStyle>,
|
|
15
21
|
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
16
22
|
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
/** Show this screen as the current destination (replaces whatever is visible — a screen, a
|
|
25
|
+
* scene, a native view — suspending an active Router until `Router.restore()`). */
|
|
26
|
+
open(options?: PresentOptions): void,
|
|
19
27
|
close(): void
|
|
20
28
|
|
|
21
29
|
append(...nodes: UINodeChild[]): this
|
|
22
30
|
insert(index: number, ...nodes: UINodeChild[]): this
|
|
23
31
|
remove(...nodes: UINodeChild[]): this
|
|
24
32
|
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
25
|
-
|
|
33
|
+
|
|
26
34
|
readonly children: UINodeChild[],
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
onOverscroll: Scrollable extends true? (callback: (scrollPosition: number) => void) => void: never,
|
|
36
|
+
/** Upgrade this screen into a scroll view, unlocking `onScroll`/`onOverscroll`/`onRefresh`. */
|
|
37
|
+
makeScrollable(): UIScrollableScreen,
|
|
31
38
|
onBackPressed(callback: () => void): this
|
|
32
|
-
onRefresh: Scrollable extends true? ((callback: () => void | Promise<void>) => this): never,
|
|
33
39
|
|
|
34
40
|
onOpen(callback: () => void): this
|
|
35
41
|
onClose(callback: () => void): this
|
|
@@ -52,16 +58,46 @@ export interface UIScreen <Scrollable extends boolean = false> {
|
|
|
52
58
|
hasClass(name: string): boolean
|
|
53
59
|
bindClass(name: string, fn: () => boolean): this
|
|
54
60
|
|
|
55
|
-
onTouchStart(callback: (ev: TouchStartEvent<UIScreen
|
|
61
|
+
onTouchStart(callback: (ev: TouchStartEvent<UIScreen>) => void): this,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A `UIScreen` in scroll mode — what `makeScrollable()` returns. Adds the scroll lifecycle that a
|
|
66
|
+
* plain screen doesn't carry. A subtype of `UIScreen`, so it's accepted anywhere a screen /
|
|
67
|
+
* `HostedScreen` is (router pages, `UIScreenHost`, `UITabs`).
|
|
68
|
+
*/
|
|
69
|
+
export interface UIScrollableScreen extends UIScreen {
|
|
70
|
+
readonly scrollable: true,
|
|
71
|
+
/** Fires as the content scrolls; the offset is logical px from the start edge. Chainable. */
|
|
72
|
+
onScroll(callback: (scrollPosition: number) => void): this,
|
|
73
|
+
/** Fires while the user drags past an edge (rubber-band). Chainable. */
|
|
74
|
+
onOverscroll(callback: (scrollPosition: number) => void): this,
|
|
75
|
+
/** Pull-to-refresh: the spinner stays until the returned promise resolves. Chainable. */
|
|
76
|
+
onRefresh(callback: () => void | Promise<void>): this,
|
|
77
|
+
/** Already scrollable — returns itself. */
|
|
78
|
+
makeScrollable(): UIScrollableScreen,
|
|
56
79
|
}
|
|
57
80
|
|
|
58
81
|
export class ScreenElement extends ContainerElement<"screen"> {
|
|
59
|
-
scrollable = false
|
|
60
|
-
open(): void {
|
|
61
|
-
|
|
82
|
+
scrollable = false
|
|
83
|
+
open(options?: PresentOptions): void {
|
|
84
|
+
_bumpNavEpoch()
|
|
85
|
+
if (_navSupported()) {
|
|
86
|
+
_creatorUI.openView!(this, options?.transition ?? "none")
|
|
87
|
+
} else {
|
|
88
|
+
// Legacy host: no destination navigation — transition is dropped.
|
|
89
|
+
_creatorUI.openScreen(this)
|
|
90
|
+
}
|
|
91
|
+
_setCurrent(this as any)
|
|
62
92
|
}
|
|
63
93
|
close(): void {
|
|
64
|
-
|
|
94
|
+
_bumpNavEpoch()
|
|
95
|
+
if (_navSupported()) {
|
|
96
|
+
_creatorUI.closeView!()
|
|
97
|
+
} else {
|
|
98
|
+
_creatorUI.closeScreen()
|
|
99
|
+
}
|
|
100
|
+
if (Presentable.current === (this as any)) _setCurrent(null)
|
|
65
101
|
}
|
|
66
102
|
protected ol: (() => void)[] = []
|
|
67
103
|
onOpen(callback: () => void): this {
|
|
@@ -103,8 +139,8 @@ export class ScreenElement extends ContainerElement<"screen"> {
|
|
|
103
139
|
}
|
|
104
140
|
return null
|
|
105
141
|
}
|
|
106
|
-
makeScrollable():
|
|
107
|
-
this.scrollable = true
|
|
142
|
+
makeScrollable(): UIScrollableScreen {
|
|
143
|
+
this.scrollable = true
|
|
108
144
|
|
|
109
145
|
Object.assign(this, {
|
|
110
146
|
sl: [] as any[],
|
|
@@ -123,24 +159,22 @@ export class ScreenElement extends ContainerElement<"screen"> {
|
|
|
123
159
|
}
|
|
124
160
|
})
|
|
125
161
|
|
|
126
|
-
return this as any as
|
|
162
|
+
return this as any as UIScrollableScreen
|
|
127
163
|
}
|
|
128
164
|
_backButtonCallback?: () => void
|
|
129
165
|
onBackPressed(callback: any): this {
|
|
130
166
|
this._backButtonCallback = callback
|
|
131
167
|
return this
|
|
132
168
|
}
|
|
169
|
+
// Set by the onRefresh method that makeScrollable() grafts on; read by the host at mount.
|
|
133
170
|
declare _refreshCallback: any
|
|
134
|
-
declare onRefresh: never
|
|
135
|
-
declare onScroll: never
|
|
136
|
-
declare onOverscroll: never
|
|
137
171
|
}
|
|
138
172
|
|
|
139
|
-
export function UIScreen(): UIScreen
|
|
140
|
-
export function UIScreen(children: UINodeChild[] | ChildrenFn): UIScreen
|
|
141
|
-
export function UIScreen(style: UIScreenStyle): UIScreen
|
|
142
|
-
export function UIScreen(style: UIScreenStyle, children: UINodeChild[]): UIScreen
|
|
143
|
-
export function UIScreen(...args: [] | [UINodeChild[] | ChildrenFn | UIScreenStyle] | [UIScreenStyle, children: UINodeChild[] | ChildrenFn]): UIScreen
|
|
173
|
+
export function UIScreen(): UIScreen;
|
|
174
|
+
export function UIScreen(children: UINodeChild[] | ChildrenFn): UIScreen;
|
|
175
|
+
export function UIScreen(style: UIScreenStyle): UIScreen;
|
|
176
|
+
export function UIScreen(style: UIScreenStyle, children: UINodeChild[]): UIScreen;
|
|
177
|
+
export function UIScreen(...args: [] | [UINodeChild[] | ChildrenFn | UIScreenStyle] | [UIScreenStyle, children: UINodeChild[] | ChildrenFn]): UIScreen {
|
|
144
178
|
if (args.length === 0) {
|
|
145
179
|
return new ScreenElement("screen", {}, [])
|
|
146
180
|
} else if (args.length === 1) {
|
|
@@ -12,8 +12,9 @@ import type { UIScreen } from "./UIScreen"
|
|
|
12
12
|
// implements exactly one page kind: mount the screen root in the slot, lay it out to the slot size.
|
|
13
13
|
// To host a plain element, wrap it: UIScreenHost(UIScreen([el])).
|
|
14
14
|
|
|
15
|
-
/** A hosted page: any UIScreen
|
|
16
|
-
|
|
15
|
+
/** A hosted page: any UIScreen (a `UIScrollableScreen` is a subtype, so it's accepted too). Sizing
|
|
16
|
+
* styles on it are ignored — screens are roots sized by their host. */
|
|
17
|
+
export type HostedScreen = UIScreen
|
|
17
18
|
/** Reactive pages — ChildrenFn narrowed to screens. */
|
|
18
19
|
export type ScreensFn = () => (HostedScreen | null | undefined | false)[]
|
|
19
20
|
|
package/runtime/sdk/ui/UIText.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createBinding } from "../core/signals"
|
|
2
2
|
import { Element, type AnimateStyle, type BaseStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type Style, type TextStyle } from "./UINode"
|
|
3
3
|
|
|
4
|
-
export type UITextStyle = ElementStyle & TextStyle
|
|
4
|
+
export type UITextStyle = ElementStyle & TextStyle & DrawableStyle
|
|
5
5
|
|
|
6
6
|
export interface UIText {
|
|
7
7
|
readonly type: "text",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { VideoPlayer } from "../runtime/media"
|
|
2
|
+
import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
|
|
2
3
|
import { Element, type AnimateStyle, type AppearStyle, type BaseStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type Style, type StyleFn } from "./UINode"
|
|
3
4
|
|
|
4
5
|
export type UIVideoStyle = ElementStyle & DrawableStyle & { objectFit?: "cover" | "contain" | "fill" }
|
|
@@ -10,6 +11,14 @@ export interface UIVideo {
|
|
|
10
11
|
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
11
12
|
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
12
13
|
|
|
14
|
+
/** Show fullscreen as the current destination. Promotion: if this video is mounted in a screen,
|
|
15
|
+
* the SAME native player moves fullscreen — playback uninterrupted; a pop moves it back. */
|
|
16
|
+
open(options?: PresentOptions): void
|
|
17
|
+
close(): void
|
|
18
|
+
onOpen(callback: () => void): this
|
|
19
|
+
onClose(callback: () => void): this
|
|
20
|
+
onBackPressed(callback: () => void): this
|
|
21
|
+
|
|
13
22
|
onLayout(onLayout: OnLayoutCallback): this
|
|
14
23
|
|
|
15
24
|
setClass(name: string, enabled: boolean): this
|
|
@@ -18,7 +27,7 @@ export interface UIVideo {
|
|
|
18
27
|
bindClass(name: string, fn: () => boolean): this
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
class VideoElement extends Element<"video"> {
|
|
30
|
+
class VideoElement extends Element<"video"> implements Presentable {
|
|
22
31
|
private _playerId: number
|
|
23
32
|
private _player: VideoPlayer
|
|
24
33
|
constructor(style: any, player: VideoPlayer) {
|
|
@@ -30,6 +39,43 @@ class VideoElement extends Element<"video"> {
|
|
|
30
39
|
get player() {
|
|
31
40
|
return this._player
|
|
32
41
|
}
|
|
42
|
+
|
|
43
|
+
// ---- Presentable (docs/navigation-presentable-plan.md) ----
|
|
44
|
+
readonly ol: (() => void)[] = []
|
|
45
|
+
readonly cl: (() => void)[] = []
|
|
46
|
+
_backButtonCallback?: () => void
|
|
47
|
+
private _vd?: object
|
|
48
|
+
|
|
49
|
+
/** @internal wire descriptor — type "videoView" (distinct from the "video" node type). */
|
|
50
|
+
_viewDesc(): object {
|
|
51
|
+
return this._vd ??= { type: "videoView", node: this, _p: this }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
open(options?: PresentOptions): void {
|
|
55
|
+
if (!_navSupported()) {
|
|
56
|
+
throw new Error("Fullscreen video is not supported on this host (requires _creatorUI.openView)")
|
|
57
|
+
}
|
|
58
|
+
_bumpNavEpoch()
|
|
59
|
+
_creatorUI.openView!(this._viewDesc(), options?.transition ?? "none")
|
|
60
|
+
_setCurrent(this)
|
|
61
|
+
}
|
|
62
|
+
close(): void {
|
|
63
|
+
_bumpNavEpoch()
|
|
64
|
+
if (_navSupported()) _creatorUI.closeView!()
|
|
65
|
+
if (Presentable.current === this) _setCurrent(null)
|
|
66
|
+
}
|
|
67
|
+
onOpen(callback: () => void): this {
|
|
68
|
+
this.ol.push(callback)
|
|
69
|
+
return this
|
|
70
|
+
}
|
|
71
|
+
onClose(callback: () => void): this {
|
|
72
|
+
this.cl.push(callback)
|
|
73
|
+
return this
|
|
74
|
+
}
|
|
75
|
+
onBackPressed(callback: () => void): this {
|
|
76
|
+
this._backButtonCallback = callback
|
|
77
|
+
return this
|
|
78
|
+
}
|
|
33
79
|
}
|
|
34
80
|
|
|
35
81
|
export function UIVideo(player: VideoPlayer): UIVideo
|
|
@@ -40,4 +86,4 @@ export function UIVideo(...args: [VideoPlayer] | [UIVideoStyle, VideoPlayer]): U
|
|
|
40
86
|
} else {
|
|
41
87
|
return new VideoElement(args[0], args[1])
|
|
42
88
|
}
|
|
43
|
-
}
|
|
89
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TouchStartEvent } from "../runtime/touch"
|
|
2
|
+
import { _descOf, _navSupported, Presentable } from "./presentable"
|
|
2
3
|
import { ContainerElement, type AnimateStyle, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type PaddingStyle, type PositionStyle, type Style, type UINodeChild, type ChildrenFn } from "./UINode"
|
|
3
4
|
|
|
4
5
|
|
|
@@ -15,7 +16,23 @@ export interface UIWidget {
|
|
|
15
16
|
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle & { overlayColor?: Color | null }>
|
|
16
17
|
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle & { overlayColor?: Color | null }>
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Pre-bind this widget to a destination (a `Scene`, a `UIScreen`, …) that is not current yet —
|
|
21
|
+
* e.g. HUD for an `ARScene` built before opening it, so it's in place from the first frame.
|
|
22
|
+
* `show()` binds to the *current* destination automatically, so this is only needed ahead of
|
|
23
|
+
* time (or to target a non-current owner). Sticky: an explicit owner wins over the automatic
|
|
24
|
+
* one until `attachTo(null)` (= back to automatic). Set it while the widget is hidden.
|
|
25
|
+
*/
|
|
26
|
+
attachTo(owner: Presentable | null): this,
|
|
27
|
+
|
|
28
|
+
/** Show the widget, attached to the current destination (or the `attachTo` owner): it mounts
|
|
29
|
+
* inside that page, shows/hides with it and rides its transition — a dialog opened on a screen
|
|
30
|
+
* disappears when the user navigates away. For an overlay that survives navigation use
|
|
31
|
+
* `showDetached()`. */
|
|
18
32
|
show(): void,
|
|
33
|
+
/** Show the widget as a global overlay above every destination — it stays visible across
|
|
34
|
+
* navigation (mini-player, global loader). */
|
|
35
|
+
showDetached(): void,
|
|
19
36
|
hide(): void,
|
|
20
37
|
|
|
21
38
|
readonly isShow: boolean,
|
|
@@ -41,8 +58,24 @@ export interface UIWidget {
|
|
|
41
58
|
|
|
42
59
|
export class WidgetElement extends ContainerElement<"widget"> {
|
|
43
60
|
scrollable = false as const
|
|
61
|
+
_owner?: Presentable
|
|
62
|
+
attachTo(owner: Presentable | null): this {
|
|
63
|
+
this._owner = owner ?? undefined
|
|
64
|
+
return this
|
|
65
|
+
}
|
|
44
66
|
show(): void {
|
|
45
|
-
|
|
67
|
+
// Explicit attachTo() wins; otherwise bind to whatever is on screen right now — re-showing
|
|
68
|
+
// the same widget from another page re-attaches it there.
|
|
69
|
+
const owner = this._owner ?? Presentable.current
|
|
70
|
+
if (owner && _navSupported()) {
|
|
71
|
+
_creatorUI.showWidget(this, _descOf(owner))
|
|
72
|
+
} else {
|
|
73
|
+
// Legacy host (or nothing presented yet): global overlay.
|
|
74
|
+
_creatorUI.showWidget(this)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
showDetached(): void {
|
|
78
|
+
_creatorUI.showWidget(this)
|
|
46
79
|
}
|
|
47
80
|
hide(): void {
|
|
48
81
|
_creatorUI.hideWidget(this)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The Presentable contract — the one navigation model (docs/navigation-presentable-plan.md).
|
|
2
|
+
// Anything that can occupy the app's main slot implements it: UIScreen, Scene (+ARScene),
|
|
3
|
+
// Scene2D, NativeView, UIVideo. Exactly one Presentable is visible at a time — a screen no
|
|
4
|
+
// longer overlays a scene, it replaces it (with a transition); UI over a scene goes through
|
|
5
|
+
// `UIWidget.attachTo(scene)`.
|
|
6
|
+
//
|
|
7
|
+
// This module is dependency-free on purpose: gl/ and g2/ import it, so it must not pull any UI
|
|
8
|
+
// element code into engine-only bundles.
|
|
9
|
+
|
|
10
|
+
/** Built-in transition catalog. `push`/`pop` are the stacked-navigation pair (slide + parallax +
|
|
11
|
+
* dim + edge shadow); the rest are simple two-view moves. */
|
|
12
|
+
export type TransitionName =
|
|
13
|
+
| "push" | "pop"
|
|
14
|
+
| "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom"
|
|
15
|
+
| "zoom" | "zoom-in" | "zoom-out" | "fade" | "none"
|
|
16
|
+
|
|
17
|
+
/** Start/end pose of one side of a custom transition. `x`/`y` are logical px, or a percentage of
|
|
18
|
+
* the destination's size (e.g. `"100%"` = one full width to the right). */
|
|
19
|
+
export type TransitionTransform = {
|
|
20
|
+
x?: number | `${number}%`
|
|
21
|
+
y?: number | `${number}%`
|
|
22
|
+
scale?: number
|
|
23
|
+
opacity?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A custom transition: declarative poses for the incoming/outgoing destination, played natively.
|
|
28
|
+
* Serializes onto each platform's existing transition machinery — there is no per-frame JS.
|
|
29
|
+
* Fields omitted keep the identity pose. Interactive back-swipe stays system-native (push/pop
|
|
30
|
+
* only); a spec applies to non-interactive transitions.
|
|
31
|
+
*/
|
|
32
|
+
export interface TransitionSpec {
|
|
33
|
+
/** Duration in ms (default 300). */
|
|
34
|
+
duration?: number
|
|
35
|
+
/** cubic-bezier control points (default ease-out). */
|
|
36
|
+
easing?: [number, number, number, number]
|
|
37
|
+
/** The entering destination animates FROM this pose to identity. */
|
|
38
|
+
incoming?: { from?: TransitionTransform }
|
|
39
|
+
/** The leaving destination animates from identity TO this pose. */
|
|
40
|
+
outgoing?: { to?: TransitionTransform }
|
|
41
|
+
/** Scrim alpha (0..1) painted over whichever destination is underneath. */
|
|
42
|
+
dim?: number
|
|
43
|
+
/** Which side stacks on top during the animation (default "incoming"). On Android, transitions
|
|
44
|
+
* involving a scene always animate the screen side on top — the scene view itself never moves. */
|
|
45
|
+
onTop?: "incoming" | "outgoing"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type Transition = TransitionName | TransitionSpec
|
|
49
|
+
|
|
50
|
+
export type PresentOptions = {
|
|
51
|
+
/** Transition to play while this destination replaces the current one (default "none" for a
|
|
52
|
+
* direct open(); the Router applies its own defaults — push/fade/pop). */
|
|
53
|
+
transition?: Transition
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Anything that can be shown as the app's current destination: a `UIScreen`, a `Scene` /
|
|
58
|
+
* `ARScene`, a `Scene2D`, a `NativeView`, or a `UIVideo`. One Presentable is visible at a time;
|
|
59
|
+
* open it directly (`p.open()` — replaces the current destination, suspending an active Router
|
|
60
|
+
* until `Router.restore()`) or navigate with `Router.push/replace/pop`.
|
|
61
|
+
*
|
|
62
|
+
* `onOpen`/`onClose` are the presentation lifecycle (fired when the destination becomes / stops
|
|
63
|
+
* being the visible one — including router pushes covering it and pops revealing it).
|
|
64
|
+
*/
|
|
65
|
+
export interface Presentable {
|
|
66
|
+
// Return type covers the implementers: UIScreen → void, Scene2D → this, ARScene → Promise.
|
|
67
|
+
open(options?: PresentOptions): void | this | Promise<void>
|
|
68
|
+
close(): void
|
|
69
|
+
onOpen(callback: () => void): this
|
|
70
|
+
onClose(callback: () => void): this
|
|
71
|
+
onBackPressed(callback: () => void): this
|
|
72
|
+
/** @internal Async work that must finish BEFORE this destination replaces the current one
|
|
73
|
+
* (camera permission, warm render, session launch — ARScene). The previous destination —
|
|
74
|
+
* typically a loading screen — stays visible while it runs; `open()` and `Router.push/replace`
|
|
75
|
+
* await it and skip the swap entirely if it rejects (permission denied, or superseded by
|
|
76
|
+
* another navigation that happened while preparing). */
|
|
77
|
+
_prepare?(): Promise<void>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The destination visible right now. Set optimistically by every successful open()/router
|
|
81
|
+
// navigation, cleared by close(), and corrected by router onChange (hosts hand the wire desc
|
|
82
|
+
// back on every stack change, including back gestures — `_p` resolves it to the SDK instance).
|
|
83
|
+
let current: Presentable | null = null
|
|
84
|
+
/** @internal Call whenever the visible destination changes. */
|
|
85
|
+
export const _setCurrent = (p: Presentable | null): void => { current = p }
|
|
86
|
+
|
|
87
|
+
/** Runtime companion of the `Presentable` interface (declaration merging): the navigation
|
|
88
|
+
* state that isn't tied to the Router. */
|
|
89
|
+
export const Presentable = /*#__PURE__*/ Object.freeze({
|
|
90
|
+
/** The destination visible right now (a `UIScreen`, `Scene`, `Scene2D`, `NativeView` or
|
|
91
|
+
* `UIVideo`), or `null` before the first open. Distinct from `Router.current` — that is the
|
|
92
|
+
* top of the router stack, which stays meaningful while the router is suspended by a direct
|
|
93
|
+
* `open()`; this is what is actually on screen. `UIWidget.show()` attaches to it. */
|
|
94
|
+
get current(): Presentable | null { return current },
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Monotonic navigation epoch: every destination change (open/close/router nav) bumps it. Async
|
|
98
|
+
// preparers capture the epoch before their awaits and reject as "superseded" if it moved — so a
|
|
99
|
+
// slow AR prepare can't steal the screen from a navigation the user made in the meantime.
|
|
100
|
+
let navEpoch = 0
|
|
101
|
+
/** @internal */
|
|
102
|
+
export const _navEpoch = (): number => navEpoch
|
|
103
|
+
/** @internal Call on every action that changes the visible destination. */
|
|
104
|
+
export const _bumpNavEpoch = (): void => { navEpoch++ }
|
|
105
|
+
|
|
106
|
+
/** @internal Does this host implement the Presentable navigation surface? The SDK falls back to
|
|
107
|
+
* the legacy openScreen/openScene bridges when it doesn't (screens keep working; scene/native
|
|
108
|
+
* destinations in the Router then throw). */
|
|
109
|
+
export const _navSupported = (): boolean =>
|
|
110
|
+
typeof _creatorUI !== "undefined" && typeof _creatorUI.openView === "function"
|
|
111
|
+
|
|
112
|
+
/** @internal The wire descriptor for a Presentable. Screens (and anything without `_viewDesc`)
|
|
113
|
+
* travel as themselves; scenes/native views/video provide a stable descriptor object carrying
|
|
114
|
+
* `_p` (the SDK instance) so hosts can hand it back through router onChange / lifecycle. */
|
|
115
|
+
export const _descOf = (p: Presentable): object =>
|
|
116
|
+
typeof (p as any)._viewDesc === "function" ? (p as any)._viewDesc() : (p as object)
|