lecodes-cli 0.6.4 → 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/README.md +1 -0
- package/dist/index.js +920 -636
- package/package.json +9 -4
- package/runtime/scene-harness.json +1 -0
- package/runtime/sdk/compile/assetMacro.ts +4 -3
- package/runtime/sdk/compile/bundler.ts +35 -3
- package/runtime/sdk/compile/compileProject.ts +14 -2
- package/runtime/sdk/compile/libraryImports.ts +47 -0
- package/runtime/sdk/compile/sceneEditor.ts +16 -4
- package/runtime/sdk/core/Aspect.ts +37 -0
- package/runtime/sdk/core/InspectorUI.ts +212 -0
- package/runtime/sdk/core/fields.ts +15 -3
- package/runtime/sdk/g2/Scene2D.ts +51 -5
- package/runtime/sdk/gl/Model.ts +9 -0
- package/runtime/sdk/gl/Node.ts +4 -1
- package/runtime/sdk/gl/Scene.ts +76 -11
- package/runtime/sdk/gl/scenarios.ts +349 -0
- package/runtime/sdk/inject.ts +28 -8
- package/runtime/sdk/kit/UITabs.ts +105 -0
- 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/scene/defineScene.ts +651 -17
- package/runtime/sdk/scene/editorPlugins.ts +86 -0
- package/runtime/sdk/ui/NativeView.ts +144 -0
- package/runtime/sdk/ui/UI.ts +6 -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 +86 -29
- package/runtime/sdk/ui/UIScreenHost.ts +213 -0
- 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,86 @@
|
|
|
1
|
+
// Editor plugins: `registerEditorWindow` / `registerEditorTool` (docs/scene-editor-plan.md,
|
|
2
|
+
// phase 11). Registrations live in `*.editor.ts` files — compiled and executed ONLY in
|
|
3
|
+
// scene-editor bundles (phase 10), so none of this reaches shipped app.js (nothing references it
|
|
4
|
+
// there, and method-granular DCE drops it).
|
|
5
|
+
//
|
|
6
|
+
// The scene-editor harness (packages/projects/src/scene-editor) reads the registry through the
|
|
7
|
+
// injected `__editorPlugins` global — the harness, the scene, and the editor files compile into
|
|
8
|
+
// ONE bundle, so they share this module instance. Windows and tools describe their UI through the
|
|
9
|
+
// same immediate-mode `InspectorUI` protocol custom inspector cards use (one widget vocabulary,
|
|
10
|
+
// three mount points), and mutate the scene ONLY through the `editor` doc API — every plugin write
|
|
11
|
+
// lands in the scene document (undoable, diffable), never in live engine state.
|
|
12
|
+
|
|
13
|
+
import type { InspectorUI } from "../core/InspectorUI"
|
|
14
|
+
|
|
15
|
+
/** A geometry hit under the viewport pointer, handed to tools. Physics builds raycast the scene's
|
|
16
|
+
* meshes (precise); otherwise (and on misses) the hit falls back to the ground plane (y = 0). */
|
|
17
|
+
export type EditorRayHit = {
|
|
18
|
+
point: [number, number, number]
|
|
19
|
+
normal: [number, number, number]
|
|
20
|
+
/** The scene-file node the hit belongs to — null for ground-plane fallback hits. */
|
|
21
|
+
node: string | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The editor scripting API handed to windows and tools. Doc-op methods write the scene DOCUMENT
|
|
26
|
+
* through the editor's normal commit path (undo, file, live patching all included); they return
|
|
27
|
+
* false / no-op when the document can't take the edit (duplicate name, unknown node).
|
|
28
|
+
*/
|
|
29
|
+
export type EditorApi = {
|
|
30
|
+
/** The currently selected node name (or `model::part` key), null when nothing is selected. */
|
|
31
|
+
readonly selection: string | null
|
|
32
|
+
select(name: string | null): void
|
|
33
|
+
/** The scene document's nodes (name + source kind: mesh / model / light / group). */
|
|
34
|
+
nodes(): { name: string, kind: string }[]
|
|
35
|
+
/** First unused "base", "base2", "base3", … node name. */
|
|
36
|
+
uniqueName(base: string): string
|
|
37
|
+
/** Add a node from plain def data (scene-file grammar; a string `model` value means an asset
|
|
38
|
+
* path). One undo step unless grouped by `transact`. */
|
|
39
|
+
addNode(name: string, def: Record<string, unknown>): boolean
|
|
40
|
+
/** Write one def prop (transforms apply live; anything else patches/re-runs the node). */
|
|
41
|
+
setProp(name: string, key: string, value: unknown): boolean
|
|
42
|
+
removeNode(name: string): void
|
|
43
|
+
/** Duplicate a node; returns the copy's name (null when it can't). */
|
|
44
|
+
duplicate(name: string): string | null
|
|
45
|
+
/** Raycast the scene under a viewport pixel (same hit rules as tool clicks). */
|
|
46
|
+
raycast(screenX: number, screenY: number): EditorRayHit | null
|
|
47
|
+
/** Group every doc edit inside `fn` into ONE undo step. */
|
|
48
|
+
transact(fn: () => void): void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type EditorWindowFn = (ui: InspectorUI, editor: EditorApi) => void
|
|
52
|
+
|
|
53
|
+
export type EditorToolHooks = {
|
|
54
|
+
/** CSS cursor for the viewport while this tool is active (default "crosshair"). */
|
|
55
|
+
cursor?: string
|
|
56
|
+
/** A short glyph (one character / emoji) for the tool's toolbar button — tools without one all
|
|
57
|
+
* share the generic wand icon, so any editor with two or more tools should set it. */
|
|
58
|
+
icon?: string
|
|
59
|
+
/** A viewport click while the tool is active — `hit` is the raycast result under the pointer.
|
|
60
|
+
* Selection clicks and the transform gizmo are suspended while a tool is active. */
|
|
61
|
+
onViewportClick?(hit: EditorRayHit, editor: EditorApi): void
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @internal The registry the scene-editor harness reads (windows/tools in registration order). */
|
|
65
|
+
export const __editorPlugins = {
|
|
66
|
+
windows: [] as { title: string, render: EditorWindowFn }[],
|
|
67
|
+
tools: [] as { name: string, hooks: EditorToolHooks }[],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Register an editor panel — a collapsible overlay docked over the viewport (the inspector rail
|
|
71
|
+
* stays selection-scoped). A window that emits `ui.toolButton` for a tool becomes that tool's
|
|
72
|
+
* settings panel: activating the tool expands and highlights it. `render` re-runs immediate-mode
|
|
73
|
+
* on every interaction — same protocol as `static inspector` cards. */
|
|
74
|
+
export const registerEditorWindow = (title: string, render: EditorWindowFn): void => {
|
|
75
|
+
const existing = __editorPlugins.windows.find((w) => w.title === title)
|
|
76
|
+
if (existing) existing.render = render
|
|
77
|
+
else __editorPlugins.windows.push({ title, render })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Register a viewport tool — a toolbar entry beside move/rotate/scale (activate it there or via
|
|
81
|
+
* `ui.toolButton(label, name)`). While active, viewport clicks arrive as raycast hits. */
|
|
82
|
+
export const registerEditorTool = (name: string, hooks: EditorToolHooks): void => {
|
|
83
|
+
const existing = __editorPlugins.tools.find((t) => t.name === name)
|
|
84
|
+
if (existing) existing.hooks = hooks
|
|
85
|
+
else __editorPlugins.tools.push({ name, hooks })
|
|
86
|
+
}
|
|
@@ -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'
|
|
@@ -13,10 +13,15 @@ export { UIWidget, type UIWidgetStyle } from './UIWidget'
|
|
|
13
13
|
|
|
14
14
|
export { UIRow, UIColumn, UIBox, type UIContainerStyle } from './UIContainer'
|
|
15
15
|
export { UIScrollable, type UIScrollableStyle } from './UIScrollable'
|
|
16
|
+
export { UIScreenHost, type UIScreenHostStyle } from './UIScreenHost'
|
|
16
17
|
|
|
17
18
|
export { UIVirtualizedList } from './UIVirtualizedList'
|
|
18
19
|
|
|
19
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'
|
|
20
25
|
|
|
21
26
|
// Internal: memoized map for reactive children — the chisel `reactiveUi` pass rewrites
|
|
22
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,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,39 +1,56 @@
|
|
|
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
|
|
36
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Keep this screen's built native tree alive when a UIScreenHost detaches it (replace / remove /
|
|
45
|
+
* pop): instead of being destroyed, its views + layout + live state (scroll position, input text)
|
|
46
|
+
* are retained, so re-hosting the SAME screen object restores it in place rather than rebuilding.
|
|
47
|
+
* `onClose`/`onOpen` still fire on detach/re-attach. You own the lifetime — a kept tree is freed
|
|
48
|
+
* only by `dispose()` or when the project unloads. (Native-only today; web rebuilds.)
|
|
49
|
+
*/
|
|
50
|
+
keepAlive(enabled?: boolean): this
|
|
51
|
+
/** Free a `keepAlive()` screen's retained tree now. No-op unless it's currently detached-and-kept. */
|
|
52
|
+
dispose(): this
|
|
53
|
+
|
|
37
54
|
onLayout(onLayout: OnLayoutCallback): this
|
|
38
55
|
|
|
39
56
|
setClass(name: string, enabled: boolean): this
|
|
@@ -41,16 +58,46 @@ export interface UIScreen <Scrollable extends boolean = false> {
|
|
|
41
58
|
hasClass(name: string): boolean
|
|
42
59
|
bindClass(name: string, fn: () => boolean): this
|
|
43
60
|
|
|
44
|
-
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,
|
|
45
79
|
}
|
|
46
80
|
|
|
47
81
|
export class ScreenElement extends ContainerElement<"screen"> {
|
|
48
|
-
scrollable = false
|
|
49
|
-
open(): void {
|
|
50
|
-
|
|
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)
|
|
51
92
|
}
|
|
52
93
|
close(): void {
|
|
53
|
-
|
|
94
|
+
_bumpNavEpoch()
|
|
95
|
+
if (_navSupported()) {
|
|
96
|
+
_creatorUI.closeView!()
|
|
97
|
+
} else {
|
|
98
|
+
_creatorUI.closeScreen()
|
|
99
|
+
}
|
|
100
|
+
if (Presentable.current === (this as any)) _setCurrent(null)
|
|
54
101
|
}
|
|
55
102
|
protected ol: (() => void)[] = []
|
|
56
103
|
onOpen(callback: () => void): this {
|
|
@@ -62,6 +109,18 @@ export class ScreenElement extends ContainerElement<"screen"> {
|
|
|
62
109
|
this.cl.push(callback)
|
|
63
110
|
return this
|
|
64
111
|
}
|
|
112
|
+
// Read by the host at detach time (a plain flag on the node, like _interactive).
|
|
113
|
+
_keepAlive = false
|
|
114
|
+
keepAlive(enabled = true): this {
|
|
115
|
+
this._keepAlive = enabled
|
|
116
|
+
return this
|
|
117
|
+
}
|
|
118
|
+
dispose(): this {
|
|
119
|
+
// Rides the generic command channel (no dedicated bridge). No-op while _id is 0 (never mounted)
|
|
120
|
+
// or when the tree isn't currently detached-and-kept — the host ignores it then.
|
|
121
|
+
if (this._id !== 0) _creatorUI.command(this, "disposeKeepAlive")
|
|
122
|
+
return this
|
|
123
|
+
}
|
|
65
124
|
readonly touchStartListeners: any[] = []
|
|
66
125
|
_interactive = false
|
|
67
126
|
onTouchStart (callback: any): this {
|
|
@@ -80,8 +139,8 @@ export class ScreenElement extends ContainerElement<"screen"> {
|
|
|
80
139
|
}
|
|
81
140
|
return null
|
|
82
141
|
}
|
|
83
|
-
makeScrollable():
|
|
84
|
-
this.scrollable = true
|
|
142
|
+
makeScrollable(): UIScrollableScreen {
|
|
143
|
+
this.scrollable = true
|
|
85
144
|
|
|
86
145
|
Object.assign(this, {
|
|
87
146
|
sl: [] as any[],
|
|
@@ -100,24 +159,22 @@ export class ScreenElement extends ContainerElement<"screen"> {
|
|
|
100
159
|
}
|
|
101
160
|
})
|
|
102
161
|
|
|
103
|
-
return this as any as
|
|
162
|
+
return this as any as UIScrollableScreen
|
|
104
163
|
}
|
|
105
164
|
_backButtonCallback?: () => void
|
|
106
165
|
onBackPressed(callback: any): this {
|
|
107
166
|
this._backButtonCallback = callback
|
|
108
167
|
return this
|
|
109
168
|
}
|
|
169
|
+
// Set by the onRefresh method that makeScrollable() grafts on; read by the host at mount.
|
|
110
170
|
declare _refreshCallback: any
|
|
111
|
-
declare onRefresh: never
|
|
112
|
-
declare onScroll: never
|
|
113
|
-
declare onOverscroll: never
|
|
114
171
|
}
|
|
115
172
|
|
|
116
|
-
export function UIScreen(): UIScreen
|
|
117
|
-
export function UIScreen(children: UINodeChild[] | ChildrenFn): UIScreen
|
|
118
|
-
export function UIScreen(style: UIScreenStyle): UIScreen
|
|
119
|
-
export function UIScreen(style: UIScreenStyle, children: UINodeChild[]): UIScreen
|
|
120
|
-
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 {
|
|
121
178
|
if (args.length === 0) {
|
|
122
179
|
return new ScreenElement("screen", {}, [])
|
|
123
180
|
} else if (args.length === 1) {
|