lecodes-cli 0.7.0 → 0.7.2
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/README.md +3 -3
- package/dist/index.js +256 -244
- package/package.json +4 -4
- package/runtime/sdk/compile/bundler.ts +4 -2
- package/runtime/sdk/compile/compileProject.ts +6 -1
- 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 +15 -7
- 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 +33 -5
- 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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// A host-registered platform view (map, QR scanner, camera preview, …) — the plugin surface of
|
|
2
|
+
// the Presentable model (docs/navigation-presentable-plan.md). The host registers a *capability*
|
|
3
|
+
// (`engine.registerView("map") { params, channel -> MapView(...) }`); when and with what to show
|
|
4
|
+
// it is app code. Dual-natured like every Presentable: a fullscreen destination
|
|
5
|
+
// (`Router.push(map)` / `map.open()`) AND an embeddable layout node (wire type `"native"` — place
|
|
6
|
+
// it among a screen's children, sized by its styles). One instance lives in one place at a time;
|
|
7
|
+
// pushing a mounted instance fullscreen is promotion (the native view reparents, state intact).
|
|
8
|
+
//
|
|
9
|
+
// Plugins ship a typed TS wrapper over the raw `call`/`on` channel — see QRScanner for the
|
|
10
|
+
// wrapper convention.
|
|
11
|
+
|
|
12
|
+
import { _channelCall, _channelEmit, _channelOff, _channelOn, type ChannelListeners } from "../runtime/channel"
|
|
13
|
+
import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
|
|
14
|
+
import { Element, type AnimateStyle, type BaseStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type Style } from "./UINode"
|
|
15
|
+
|
|
16
|
+
export type UINativeViewStyle = ElementStyle & DrawableStyle
|
|
17
|
+
|
|
18
|
+
export interface NativeView {
|
|
19
|
+
readonly type: "native",
|
|
20
|
+
/** The registered view kind this instance resolves to (the `registerView` name). */
|
|
21
|
+
readonly viewName: string,
|
|
22
|
+
/** Creation params, passed to the host factory (JSON-serializable). */
|
|
23
|
+
readonly params: any,
|
|
24
|
+
style: Style<this, UINativeViewStyle>,
|
|
25
|
+
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
26
|
+
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
27
|
+
|
|
28
|
+
/** Show fullscreen as the current destination (see Presentable). */
|
|
29
|
+
open(options?: PresentOptions): void,
|
|
30
|
+
close(): void,
|
|
31
|
+
|
|
32
|
+
onOpen(callback: () => void): this
|
|
33
|
+
onClose(callback: () => void): this
|
|
34
|
+
onBackPressed(callback: () => void): this
|
|
35
|
+
|
|
36
|
+
/** Invoke a method on the native view (`map.call("setCenter", [lng, lat])`). Args and the
|
|
37
|
+
* result are JSON-serialized. Rejects if the host has no such view/method. */
|
|
38
|
+
call(method: string, ...args: any[]): Promise<any>
|
|
39
|
+
/** Subscribe to an event the native view emits (`map.on("markerTap", cb)`). */
|
|
40
|
+
on(event: string, callback: (data?: any) => void): this
|
|
41
|
+
off(event: string, callback: (data?: any) => void): this
|
|
42
|
+
|
|
43
|
+
onLayout(onLayout: OnLayoutCallback): this
|
|
44
|
+
setClass(name: string, enabled: boolean): this
|
|
45
|
+
toggleClass(name: string): this
|
|
46
|
+
hasClass(name: string): boolean
|
|
47
|
+
bindClass(name: string, fn: () => boolean): this
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let nextViewId = 1
|
|
51
|
+
|
|
52
|
+
/** @internal Base for typed plugin wrappers (QRScanner, CameraView — src/plugins/): subclass it,
|
|
53
|
+
* pin the viewName/params, and layer typed methods over the raw `call`/`on` channel. Not a
|
|
54
|
+
* public global — apps use the `NativeView(name)` factory below. */
|
|
55
|
+
export class NativeViewElement extends Element<"native"> implements Presentable {
|
|
56
|
+
readonly viewName: string
|
|
57
|
+
readonly params: any
|
|
58
|
+
/** @internal stable per-instance handle for the viewCall channel + promotion identity. */
|
|
59
|
+
readonly _viewId: number
|
|
60
|
+
/** @internal event listeners by event name — the host invokes these (vlist pcl/psl convention)
|
|
61
|
+
* or calls _emitViewEvent. */
|
|
62
|
+
readonly nvl: ChannelListeners = {}
|
|
63
|
+
readonly ol: (() => void)[] = []
|
|
64
|
+
readonly cl: (() => void)[] = []
|
|
65
|
+
_backButtonCallback?: () => void
|
|
66
|
+
private _vd?: object
|
|
67
|
+
|
|
68
|
+
constructor(viewName: string, params: any) {
|
|
69
|
+
super("native", {})
|
|
70
|
+
this.viewName = viewName
|
|
71
|
+
this.params = params
|
|
72
|
+
this._viewId = nextViewId++
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @internal stable wire descriptor; hosts hand it back via router onChange (`_p` = this). */
|
|
76
|
+
_viewDesc(): object {
|
|
77
|
+
return this._vd ??= { type: "native", viewName: this.viewName, params: this.params, viewId: this._viewId, _p: this }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
open(options?: PresentOptions): void {
|
|
81
|
+
if (!_navSupported()) {
|
|
82
|
+
throw new Error(`NativeView("${this.viewName}") is not supported on this host (requires _creatorUI.openView)`)
|
|
83
|
+
}
|
|
84
|
+
_bumpNavEpoch()
|
|
85
|
+
_creatorUI.openView!(this._viewDesc(), options?.transition ?? "none")
|
|
86
|
+
_setCurrent(this)
|
|
87
|
+
}
|
|
88
|
+
close(): void {
|
|
89
|
+
_bumpNavEpoch()
|
|
90
|
+
if (_navSupported()) _creatorUI.closeView!()
|
|
91
|
+
if (Presentable.current === this) _setCurrent(null)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onOpen(callback: () => void): this {
|
|
95
|
+
this.ol.push(callback)
|
|
96
|
+
return this
|
|
97
|
+
}
|
|
98
|
+
onClose(callback: () => void): this {
|
|
99
|
+
this.cl.push(callback)
|
|
100
|
+
return this
|
|
101
|
+
}
|
|
102
|
+
onBackPressed(callback: () => void): this {
|
|
103
|
+
this._backButtonCallback = callback
|
|
104
|
+
return this
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
call(method: string, ...args: any[]): Promise<any> {
|
|
108
|
+
return _channelCall(
|
|
109
|
+
typeof _creatorUI !== "undefined" ? _creatorUI.viewCall : undefined,
|
|
110
|
+
`NativeView("${this.viewName}") is not supported on this host (requires _creatorUI.viewCall)`,
|
|
111
|
+
this._viewId, method, args)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
on(event: string, callback: (data?: any) => void): this {
|
|
115
|
+
_channelOn(this.nvl, event, callback)
|
|
116
|
+
return this
|
|
117
|
+
}
|
|
118
|
+
off(event: string, callback: (data?: any) => void): this {
|
|
119
|
+
_channelOff(this.nvl, event, callback)
|
|
120
|
+
return this
|
|
121
|
+
}
|
|
122
|
+
/** @internal host entry: deliver a native event (payload as a JSON string, or omitted). */
|
|
123
|
+
_emitViewEvent(event: string, dataJson?: string): void {
|
|
124
|
+
_channelEmit(this.nvl, event, dataJson)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create an instance of a host-registered platform view. `NativeView.isSupported(name)` reports
|
|
130
|
+
* whether this host registered a factory for it. Style via the chained `.style()`.
|
|
131
|
+
*/
|
|
132
|
+
// PURE IIFE (not a plain function + expando assignment) so an app that never uses NativeView
|
|
133
|
+
// tree-shakes it away — top-level property assignments read as side effects to the bundler.
|
|
134
|
+
export const NativeView: {
|
|
135
|
+
(name: string, params?: object): NativeView
|
|
136
|
+
/** Whether this host registered a `registerView` factory under `name`. */
|
|
137
|
+
isSupported(name: string): boolean
|
|
138
|
+
} = /*#__PURE__*/ (() => {
|
|
139
|
+
const factory = (name: string, params: object = {}): NativeView =>
|
|
140
|
+
new NativeViewElement(name, params) as unknown as NativeView
|
|
141
|
+
factory.isSupported = (name: string): boolean =>
|
|
142
|
+
typeof _creatorUI !== "undefined" && typeof _creatorUI.isViewSupported === "function" && _creatorUI.isViewSupported(name)
|
|
143
|
+
return factory
|
|
144
|
+
})()
|
package/runtime/sdk/ui/UI.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Renderer-side contract + per-renderer status (DOM / canvas-ui / desktop) for every component
|
|
2
2
|
// below is tracked in packages/canvas-ui/COMPONENTS.md.
|
|
3
3
|
export { UIButton, type UIButtonStyle } from './UIButton'
|
|
4
|
-
export { UIScreen, type UIScreenStyle } from './UIScreen'
|
|
4
|
+
export { UIScreen, type UIScreenStyle, type UIScrollableScreen } from './UIScreen'
|
|
5
5
|
export { UISpacer, type UISpacerStyle } from './UISpacer'
|
|
6
6
|
export { UIText, type UITextStyle } from './UIText'
|
|
7
7
|
export { registerFont } from './fonts'
|
|
@@ -18,6 +18,10 @@ export { UIScreenHost, type UIScreenHostStyle } from './UIScreenHost'
|
|
|
18
18
|
export { UIVirtualizedList } from './UIVirtualizedList'
|
|
19
19
|
|
|
20
20
|
export { Router } from './router'
|
|
21
|
+
export { NativeView, type UINativeViewStyle } from './NativeView'
|
|
22
|
+
// Presentable is both the interface and its runtime companion (Presentable.current).
|
|
23
|
+
export { Presentable } from './presentable'
|
|
24
|
+
export type { PresentOptions, Transition, TransitionName, TransitionSpec, TransitionTransform } from './presentable'
|
|
21
25
|
|
|
22
26
|
// Internal: memoized map for reactive children — the chisel `reactiveUi` pass rewrites
|
|
23
27
|
// `list.map(fn)` inside children bindings to `__uiMap(list, fn, slot)` calls.
|
|
@@ -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,38 @@ 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. Class state INHERITS down the tree (CSS-`.dark`-on-body style): a
|
|
249
|
+
// class set on a node also activates same-name `$` blocks on all descendants within the same
|
|
250
|
+
// root — see docs/style-class-cascade-plan.md. `$pressed`/`$focused` are reserved: hosts toggle
|
|
251
|
+
// them on press/focus (they beat other classes, lose to onPressed/onFocused), so children can
|
|
252
|
+
// react to an ancestor's press. Instantiate with the node's own style `T` (see StyleFn below) — a
|
|
253
|
+
// class block accepts everything the node's `.style()` does (e.g. `color` on text, layout on any
|
|
254
|
+
// node), exactly like `onLandscape`/`onPortrait`. Narrowing it (it once was `DrawableStyle &
|
|
255
|
+
// BaseStyle`) wrongly rejected valid props such as `$active: { color }`. Guarded by
|
|
256
|
+
// tests/ui-types.test.ts.
|
|
249
257
|
export type ClassStyles<T> = { [key: `$${string}`]: T & { duration?: number, delay?: number } }
|
|
250
258
|
|
|
251
|
-
|
|
252
|
-
|
|
259
|
+
// NB: do NOT annotate `this: R` here. `this` is a parameter position, so it makes R contravariant,
|
|
260
|
+
// which makes every node type INVARIANT in its own type params (and in `this`). That is what made
|
|
261
|
+
// `UIScreen<false>` unassignable to `UIScreen<boolean>` / `HostedScreen`, and would block any
|
|
262
|
+
// UINode subtype from being assignable to its base. Runtime `this` is already fixed by the
|
|
263
|
+
// `styleFunction.bind(this)` in the `style` getter — the annotation bought nothing and cost variance.
|
|
264
|
+
// R stays only in the (covariant) return, so `.style()` still returns the concrete node type for
|
|
265
|
+
// chaining. Guarded by the type tests in tests/ui-types.test-d.ts.
|
|
266
|
+
// Orientation state blocks — override props applied only in landscape / portrait (like a `$`-class,
|
|
267
|
+
// keyed by device orientation instead of setClass). Same full-`T` rule as class blocks.
|
|
268
|
+
export type OrientationStyles<T> = {
|
|
269
|
+
onLandscape?: T,
|
|
270
|
+
onPortrait?: T,
|
|
271
|
+
/** @deprecated Misspelling of `onPortrait` (no second "r"). Kept so existing projects and published
|
|
272
|
+
* bundles keep working — hosts still honour it — but new code should use `onPortrait`. */
|
|
273
|
+
onPortait?: T,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export type StyleFn <R, T extends object> = ((style: Reactive<T> & OrientationStyles<T> & ClassStyles<T>) => R)
|
|
277
|
+
export type Style <R, T extends object> = StyleFn<R,T> & T & OrientationStyles<T> & ClassStyles<T>
|
|
253
278
|
|
|
254
|
-
export type AnimateStyle<R, T extends object> = ((
|
|
279
|
+
export type AnimateStyle<R, T extends object> = ((style: T & { duration?: number, delay?: number, layer?: number, commit?: boolean }) => R)
|
|
255
280
|
|
|
256
281
|
export type AppearStyle <R, T> = (arg: { from: T, duration?: number, delay?: number }) => R
|
|
257
282
|
export type DisappearStyle <R, T> = (arg: { to: T, duration?: number, delay?: number }) => R
|
|
@@ -321,7 +346,10 @@ export class Element<T extends string> {
|
|
|
321
346
|
* re-assign _id on every open). */
|
|
322
347
|
protected _classes?: Set<string>
|
|
323
348
|
|
|
324
|
-
/** Activate/deactivate a `$`-class declared in .style() (accepts the name with or without `$`).
|
|
349
|
+
/** Activate/deactivate a `$`-class declared in .style() (accepts the name with or without `$`).
|
|
350
|
+
* The class cascades: it is also active on every descendant (their same-name `$` blocks light
|
|
351
|
+
* up), until the subtree's root — hosted screens/widgets don't inherit. `hasClass`/`toggleClass`
|
|
352
|
+
* reflect only THIS node's own classes; there is no opt-out below an active ancestor. */
|
|
325
353
|
setClass(name: string, enabled: boolean): this {
|
|
326
354
|
if (name[0] === "$") name = name.slice(1)
|
|
327
355
|
if (enabled) {
|
|
@@ -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)
|