lecodes-viewer 0.19.0
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 +5 -0
- package/dist-embed/VERSION.json +5 -0
- package/dist-embed/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
- package/dist-embed/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/dist-embed/assets/basis-C64VHDVD.js +1 -0
- package/dist-embed/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/dist-embed/assets/createViewer2d-D12Wy3VF.js +1 -0
- package/dist-embed/assets/createViewerLite-CP-WHGLg.js +707 -0
- package/dist-embed/assets/creator-gl-BF2CQzeI.wasm +0 -0
- package/dist-embed/assets/creator-gl.js-B_0GrQRe.symbols +5379 -0
- package/dist-embed/assets/creator-ui-BX5Fvnbt.js +1 -0
- package/dist-embed/assets/creator-ui-CzKHLH2Q.wasm +0 -0
- package/dist-embed/assets/creator2d-BtQ-1_B0.wasm +0 -0
- package/dist-embed/assets/draco-BiISTFcR.js +118 -0
- package/dist-embed/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/dist-embed/assets/host-CWKB1WDd.js +162 -0
- package/dist-embed/assets/index-CIK8PnUA.js +2 -0
- package/dist-embed/assets/lit-DUE9rhY9.filamat +0 -0
- package/dist-embed/assets/loadCreatorUiNode-UAdhjRTd.js +1 -0
- package/dist-embed/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
- package/dist-embed/assets/particles-DxYDmI1O.filamat +0 -0
- package/dist-embed/assets/particles-quad-DwYo9yCK.filamat +0 -0
- package/dist-embed/assets/shadow-C9xJpqlY.filamat +0 -0
- package/dist-embed/assets/uberarchive-uXjRYa9p.bin +0 -0
- package/dist-embed/assets/unlit-pe4tFY5b.filamat +0 -0
- package/dist-embed/assets/unlit-transparent-D9OyaOBR.filamat +0 -0
- package/dist-embed/assets/video-eedvhMvS.filamat +0 -0
- package/dist-embed/assets/worker-Caf-yYEI.js +2 -0
- package/dist-embed/embed.html +48 -0
- package/dist-embed/embed.js +187 -0
- package/embed.html +48 -0
- package/package.json +35 -0
- package/src/assets/banner.png +0 -0
- package/src/assets/image2.png +0 -0
- package/src/assets/landing.mp4 +0 -0
- package/src/assets/neutral_ibl128.ktx +0 -0
- package/src/assets/neutral_ibl256.ktx +0 -0
- package/src/assets/output.mp4 +0 -0
- package/src/assets/output2.mp4 +0 -0
- package/src/assets/texture.png +0 -0
- package/src/assets/vue.svg +1 -0
- package/src/canvas/createCanvasHost.ts +2 -0
- package/src/createViewer.ts +103 -0
- package/src/embed.ts +515 -0
- package/src/host.ts +428 -0
- package/src/lite-test.ts +160 -0
- package/src/main.ts +77 -0
- package/src/sourcemapError.ts +2 -0
- package/src/ui/CuiProps.gen.ts +103 -0
- package/src/ui/UIScreen.ts +486 -0
- package/src/ui/createBareUI.ts +32 -0
- package/src/ui/createMeasureSystem.ts +220 -0
- package/src/ui/createNodeElement.ts +77 -0
- package/src/ui/createPagerSystem.ts +345 -0
- package/src/ui/createTouchSystem.ts +2 -0
- package/src/ui/createUI.ts +806 -0
- package/src/ui/creator-ui.js +14 -0
- package/src/ui/creator-ui.wasm +0 -0
- package/src/ui/defaultStyle.css +176 -0
- package/src/ui/layers.ts +9 -0
- package/src/ui/mountCanvasUI.ts +213 -0
- package/src/ui/nativeViewSystem.ts +91 -0
- package/src/ui/presentPlayer.ts +156 -0
- package/src/ui/props-apply.ts +403 -0
- package/src/ui/router.ts +2 -0
- package/src/ui/safeAreaSpec.ts +3 -0
- package/src/ui/shells.ts +37 -0
- package/src/ui/toasts.ts +2 -0
- package/src/ui/transitions.ts +250 -0
- package/src/ui/ui.d.ts +78 -0
- package/src/ui/vListSystem.ts +212 -0
- package/src/ui/viewScale.ts +2 -0
- package/src/utils/animate.ts +2 -0
- package/src/utils/createUtils.ts +2 -0
- package/src/utils/fetch.ts +3 -0
- package/src/utils/fonts.ts +2 -0
- package/src/utils/globals.ts +2 -0
- package/src/utils/imageNode.ts +260 -0
- package/src/utils/openFilePicker.ts +2 -0
- package/src/utils/svg.ts +2 -0
- package/src/utils/videoNode.ts +2 -0
- package/src/utils/ws.ts +2 -0
- package/src/viewer/createViewer.ts +993 -0
- package/src/viewer/creator-gl.js +14 -0
- package/src/viewer/creator-gl.js.symbols +5379 -0
- package/src/viewer/creator-gl.wasm +0 -0
- package/src/viewer/lit.filamat +0 -0
- package/src/viewer/mediaplayer.ts +3 -0
- package/src/viewer/particles-quad.filamat +0 -0
- package/src/viewer/particles.filamat +0 -0
- package/src/viewer/physics.ts +140 -0
- package/src/viewer/shadow.filamat +0 -0
- package/src/viewer/uberarchive.bin +0 -0
- package/src/viewer/unlit-transparent.filamat +0 -0
- package/src/viewer/unlit.filamat +0 -0
- package/src/viewer/validateGlb.ts +262 -0
- package/src/viewer/video.filamat +0 -0
- package/src/viewer/wasmCrashSymbolizer.ts +133 -0
- package/src/viewer2d/createViewer2d.ts +438 -0
- package/src/viewer2d/creator2d.js +14 -0
- package/src/viewer2d/creator2d.wasm +0 -0
- package/src/viewer2d/sprite2d.ts +80 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import createModule from './creator-ui'
|
|
3
|
+
import wasmLib from './creator-ui.wasm?url'
|
|
4
|
+
import { getUrl } from "../utils/fetch"
|
|
5
|
+
import { createToastSystem } from "./toasts"
|
|
6
|
+
import { createTouchSystem } from "./createTouchSystem"
|
|
7
|
+
import { installKeyboardDismissGuard } from "lecodes-viewer-lite/src/ui/keyboardDismissGuard"
|
|
8
|
+
import { installTouchDefaultsGuard } from "lecodes-viewer-lite/src/ui/touchDefaultsGuard"
|
|
9
|
+
import { createTransitionSystem } from "./transitions"
|
|
10
|
+
import { createMeasureSystem, cssFontFamily, setDefaultFontFamily } from "./createMeasureSystem"
|
|
11
|
+
import { registerUIProps } from "./props-apply"
|
|
12
|
+
import defaultStyleRaw from './defaultStyle.css?raw'
|
|
13
|
+
import { UIScreen } from './UIScreen'
|
|
14
|
+
import { Router } from './router'
|
|
15
|
+
import { convertSvgToDataUri, isSvgSrc } from '../utils/svg'
|
|
16
|
+
import { createVListSystem } from './vListSystem'
|
|
17
|
+
import { createPagerSystem } from './createPagerSystem'
|
|
18
|
+
import { createNativeViewSystem } from './nativeViewSystem'
|
|
19
|
+
import { createPresentPlayer } from './presentPlayer'
|
|
20
|
+
import { getVideoElement } from '../utils/videoNode'
|
|
21
|
+
import { DEFAULT_SAFE_AREA, orientedSafeArea, type SafeAreaSpec } from './safeAreaSpec'
|
|
22
|
+
const LAYER_DEFAULT = 256
|
|
23
|
+
// creator-ui setSafeAreaBars bitmask, in the setSafePaddings arg order.
|
|
24
|
+
const EDGE_BIT = { top: 1, right: 2, bottom: 4, left: 8 } as const
|
|
25
|
+
|
|
26
|
+
export const createUI = async (root: HTMLElement, canvasHost?: { surfaceToDataUrl(surfaceId: number): string | null }) => {
|
|
27
|
+
|
|
28
|
+
const wasmBinary = await fetch(wasmLib).then(resp => resp.arrayBuffer())
|
|
29
|
+
// const nodeMap = new Map<number, HTMLElement>()
|
|
30
|
+
// const weakMap = new WeakMap<HTMLElement, number>()
|
|
31
|
+
|
|
32
|
+
// const nodeOnLayoutEventsMap = new Map<number, ((layout: any) => void)[]>()
|
|
33
|
+
|
|
34
|
+
const module = await createModule({ wasmBinary })
|
|
35
|
+
|
|
36
|
+
const styleBlock = document.createElement("style")
|
|
37
|
+
styleBlock.innerHTML = defaultStyleRaw
|
|
38
|
+
root.append(styleBlock)
|
|
39
|
+
|
|
40
|
+
// Screens (positive ids) and widgets (negative ids) live in id-keyed Maps so that a
|
|
41
|
+
// closed screen/widget can be reclaimed. Ids come from monotonic counters — they stay
|
|
42
|
+
// stable and are never reused, preserving the C++ NodeContext.screenId contract.
|
|
43
|
+
const screens = new Map<number, UIScreen>()
|
|
44
|
+
let nextScreenId = 0
|
|
45
|
+
const widgets = new Map<number, UIScreen>()
|
|
46
|
+
let widgetsId = 1
|
|
47
|
+
|
|
48
|
+
const getNode = (screenId: number, nodeId: number) =>
|
|
49
|
+
(screenId < 0 ? widgets : screens).get(screenId)?.nodeMap.get(nodeId)
|
|
50
|
+
|
|
51
|
+
const getScreen = (screenId: number) =>
|
|
52
|
+
(screenId < 0 ? widgets : screens).get(screenId)
|
|
53
|
+
|
|
54
|
+
// Single teardown hook: every permanent remove() leaves the registry, making the
|
|
55
|
+
// screen/widget GC-eligible. Router history uses removeFromHTML() (not remove()), so
|
|
56
|
+
// history screens stay registered and restorable.
|
|
57
|
+
const deregister = (screenId: number) => {
|
|
58
|
+
if (screenId < 0) widgets.delete(screenId)
|
|
59
|
+
else screens.delete(screenId)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const measureSystem = createMeasureSystem(getNode)
|
|
63
|
+
const func = module.addFunction(measureSystem.measure, "jiififi")
|
|
64
|
+
module._setMeasureFunc(func)
|
|
65
|
+
|
|
66
|
+
const vlistSystem = createVListSystem(module)
|
|
67
|
+
// Assigned once `newScreen` exists (the screen host / pager build each page as its own layout
|
|
68
|
+
// root). Referenced only from runtime paths (openScreen/newScreen/showWidget), which run after
|
|
69
|
+
// createUI returns.
|
|
70
|
+
let pagerSystem: ReturnType<typeof createPagerSystem>
|
|
71
|
+
|
|
72
|
+
let currentScreen: UIScreen | null = null
|
|
73
|
+
|
|
74
|
+
const addStringHandler = (key: string, callback: (screenId: number, nodePtr: number, str: number) => void) => {
|
|
75
|
+
const maxLen = (key as string).length + 1;
|
|
76
|
+
const keyPtr = module._malloc(maxLen);
|
|
77
|
+
module.stringToUTF8(key, keyPtr, maxLen);
|
|
78
|
+
|
|
79
|
+
const func = module.addFunction(callback, "viii")
|
|
80
|
+
|
|
81
|
+
module._addStringHandler(keyPtr, func);
|
|
82
|
+
module._free(keyPtr)
|
|
83
|
+
}
|
|
84
|
+
const addFloatHandler = (key: string, callback: (screenId: number, nodePtr: number, val: number) => void) => {
|
|
85
|
+
const maxLen = key.length + 1;
|
|
86
|
+
const valuePtr = module._malloc(maxLen);
|
|
87
|
+
module.stringToUTF8(key, valuePtr, maxLen);
|
|
88
|
+
const func = module.addFunction(callback, "viif")
|
|
89
|
+
module._addFloatHandler(valuePtr, func)
|
|
90
|
+
module._free(valuePtr)
|
|
91
|
+
}
|
|
92
|
+
const addUIntHandler = (key: string, callback: (screenId: number, nodePtr: number, val: number) => void) => {
|
|
93
|
+
const maxLen = key.length + 1;
|
|
94
|
+
const valuePtr = module._malloc(maxLen);
|
|
95
|
+
module.stringToUTF8(key, valuePtr, maxLen);
|
|
96
|
+
const func = module.addFunction(callback, "viii")
|
|
97
|
+
module._addUIntHandler(valuePtr, func)
|
|
98
|
+
module._free(valuePtr)
|
|
99
|
+
}
|
|
100
|
+
const addResetHandler = (key: string, callback: (screenId: number, nodePtr: number) => void) => {
|
|
101
|
+
const maxLen = key.length + 1;
|
|
102
|
+
const valuePtr = module._malloc(maxLen);
|
|
103
|
+
module.stringToUTF8(key, valuePtr, maxLen);
|
|
104
|
+
const func = module.addFunction(callback, "vii")
|
|
105
|
+
module._addResetHandler(valuePtr, func)
|
|
106
|
+
module._free(valuePtr)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const addMatrixHandler = (callback: (screenId: number, nodePtr: number, matrixPtr: number) => void) => {
|
|
110
|
+
const func = module.addFunction(callback, "viii")
|
|
111
|
+
module._setMatrixHandler(func)
|
|
112
|
+
}
|
|
113
|
+
const addTransformOriginHandler = (callback: (screenId: number, nodePtr: number, x: number, y: number, xPercent: number, yPercent: number) => void) => {
|
|
114
|
+
const func = module.addFunction(callback, "viiffii")
|
|
115
|
+
module._setTransformOriginHandler(func)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Property application (every set + reset dispatch from the core) lives in props-apply.ts,
|
|
119
|
+
// enum-exhaustive against the generated registry surface (CuiProps.gen.ts).
|
|
120
|
+
registerUIProps({ module, root, getNode, getScreen, addStringHandler, addFloatHandler, addUIntHandler, addResetHandler, addMatrixHandler, addTransformOriginHandler })
|
|
121
|
+
|
|
122
|
+
// Style keys are a tiny, fixed set; intern their WASM pointers once instead of
|
|
123
|
+
// mallocing/freeing on every applyStyle call. The cache lives for the module
|
|
124
|
+
// lifetime by design (not a leak).
|
|
125
|
+
const keyPtrCache = new Map<string, number>()
|
|
126
|
+
const internKey = (key: string) => {
|
|
127
|
+
let ptr = keyPtrCache.get(key)!
|
|
128
|
+
if (ptr === undefined) {
|
|
129
|
+
const maxLen = key.length + 1
|
|
130
|
+
ptr = module._malloc(maxLen)
|
|
131
|
+
module.stringToUTF8(key, ptr, maxLen)
|
|
132
|
+
keyPtrCache.set(key, ptr)
|
|
133
|
+
}
|
|
134
|
+
return ptr
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const applyStyle = (layer: number, yogaId: number, key: string, value: number | string | boolean) => {
|
|
138
|
+
const keyPtr = internKey(key)
|
|
139
|
+
|
|
140
|
+
// A reactive/conditional style can resolve to undefined (e.g. `active ? "#fff" : undefined`).
|
|
141
|
+
// Treat it as a reset, exactly like an explicit null — otherwise `typeof undefined` falls into
|
|
142
|
+
// the string `default` branch below and lengthBytesUTF8(undefined) throws. Matches viewer-lite's
|
|
143
|
+
// applyStyleValue (createLiteUI.ts), which clears the prop for null/undefined alike.
|
|
144
|
+
// Layer-scoped (_resetPropExt, not _resetProp): a null inside a `$class`/orientation block
|
|
145
|
+
// must land in THAT layer — reset semantics only while it's active — never erase the base
|
|
146
|
+
// entry. The core keeps erase semantics when `layer` is the default (256).
|
|
147
|
+
if (value === undefined) {
|
|
148
|
+
module._resetPropExt(layer, yogaId, keyPtr)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
switch(typeof value) {
|
|
153
|
+
case "number":
|
|
154
|
+
module._setFloatExt(layer, yogaId, keyPtr, value)
|
|
155
|
+
break
|
|
156
|
+
case "boolean":
|
|
157
|
+
module._setFloatExt(layer, yogaId, keyPtr, value? 1: 0)
|
|
158
|
+
break
|
|
159
|
+
case "object":
|
|
160
|
+
if (value === null) {
|
|
161
|
+
module._resetPropExt(layer, yogaId, keyPtr)
|
|
162
|
+
} else if ("_id" in value) {
|
|
163
|
+
applyStyle(layer, yogaId, key, getUrl((value as any)._id))
|
|
164
|
+
} else if ("svg" in value) {
|
|
165
|
+
applyStyle(layer, yogaId, key, convertSvgToDataUri((value as any).svg))
|
|
166
|
+
} else {
|
|
167
|
+
const layer = module._getLayer(yogaId, keyPtr)
|
|
168
|
+
transitionSystem.applyStyleTransition(yogaId, value, layer)
|
|
169
|
+
}
|
|
170
|
+
break
|
|
171
|
+
default:
|
|
172
|
+
const maxLen = module.lengthBytesUTF8(value) + 1;
|
|
173
|
+
const valuePtr = module._malloc(maxLen);
|
|
174
|
+
module.stringToUTF8(value, valuePtr, maxLen);
|
|
175
|
+
|
|
176
|
+
module._setStringExt(layer, yogaId, keyPtr, valuePtr)
|
|
177
|
+
module._free(valuePtr)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Layout size of the root. Unlike getBoundingClientRect, client sizes ignore CSS transforms,
|
|
182
|
+
// so a host's appear animation (e.g. the editor simulator's scale/translate pop) can't poison
|
|
183
|
+
// a screen/widget laid out while it plays.
|
|
184
|
+
const rootSize = (): [number, number] => [root.clientWidth, root.clientHeight]
|
|
185
|
+
|
|
186
|
+
// ==== destinations (docs/navigation-presentable-plan.md — phase 2, web reference host) ========
|
|
187
|
+
// Exactly one destination (screen / scene / native view / fullscreen video) is visible at a
|
|
188
|
+
// time. The scene canvas is no longer an always-on bottom layer: presenting a scene shows its
|
|
189
|
+
// canvas and activates the engine; covering it closes the engine (rendering pauses) and hides
|
|
190
|
+
// the canvas. Screens stay position:absolute siblings, so during transitions they naturally
|
|
191
|
+
// paint above the in-flow canvas (see presentPlayer's static rule).
|
|
192
|
+
|
|
193
|
+
type SceneHooks = { el: HTMLElement, open(sceneId: number): void, close(): void }
|
|
194
|
+
let sceneHost: { scene3d?: SceneHooks, scene2d?: SceneHooks } = {}
|
|
195
|
+
// Wired by host.ts once the engines exist (same late-wiring seam as _setCreator).
|
|
196
|
+
const _setSceneHost = (hooks: { scene3d?: SceneHooks, scene2d?: SceneHooks }) => { sceneHost = hooks }
|
|
197
|
+
|
|
198
|
+
const nativeViews = createNativeViewSystem()
|
|
199
|
+
const presentPlayer = createPresentPlayer(root)
|
|
200
|
+
|
|
201
|
+
type Dest =
|
|
202
|
+
| { kind: "screen", node: any, screen: UIScreen }
|
|
203
|
+
| { kind: "scene3d", desc: any }
|
|
204
|
+
| { kind: "scene2d", desc: any }
|
|
205
|
+
| { kind: "native", desc: any, cell?: HTMLElement }
|
|
206
|
+
| { kind: "videoView", desc: any, wrapper?: HTMLElement, restore?: { parent: HTMLElement | null, next: Node | null, cssText: string } }
|
|
207
|
+
|
|
208
|
+
let current: Dest | null = null
|
|
209
|
+
|
|
210
|
+
const newScreen = (node: any) => {
|
|
211
|
+
const screen = new UIScreen(module, touchSystem, applyStyle, classLayer, vlistSystem, pagerSystem, deregister, canvasHost, nativeViews)
|
|
212
|
+
const id = nextScreenId++
|
|
213
|
+
screens.set(id, screen)
|
|
214
|
+
screen.init(id, node)
|
|
215
|
+
return screen
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Built here (not at the top) because each page is hosted by its own `newScreen` layout root.
|
|
219
|
+
pagerSystem = createPagerSystem(module, { newScreen })
|
|
220
|
+
|
|
221
|
+
const makeDest = (view: any): Dest => {
|
|
222
|
+
switch (view?.type) {
|
|
223
|
+
case "scene3d":
|
|
224
|
+
case "scene2d": return { kind: view.type, desc: view }
|
|
225
|
+
case "native": return { kind: "native", desc: view }
|
|
226
|
+
case "videoView": return { kind: "videoView", desc: view }
|
|
227
|
+
default: return { kind: "screen", node: view, screen: newScreen(view) }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Presentation lifecycle arrays: screens carry ol/cl on the node; descriptors on their `_p`.
|
|
232
|
+
const fireLifecycle = (dest: Dest, key: "ol" | "cl") => {
|
|
233
|
+
const owner = dest.kind === "screen" ? dest.node : dest.desc._p
|
|
234
|
+
for (const callback of owner?.[key] ?? []) callback()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Attached widgets (widget.attachTo(owner)) show only while their owner is the current
|
|
238
|
+
// destination. Owner identity: the screen node object itself, or the scene/native descriptor.
|
|
239
|
+
const ownerIsCurrent = (ownerDesc: any) =>
|
|
240
|
+
current !== null && (current.kind === "screen" ? current.node === ownerDesc : current.desc === ownerDesc)
|
|
241
|
+
const updateAttachedWidgets = () => {
|
|
242
|
+
for (const widget of widgets.values()) {
|
|
243
|
+
if (widget.ownerDesc === undefined) continue
|
|
244
|
+
const visible = ownerIsCurrent(widget.ownerDesc)
|
|
245
|
+
widget.root.style.display = visible ? "" : "none"
|
|
246
|
+
if (widget.overlayElement) widget.overlayElement.style.display = visible ? "" : "none"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Mounts a destination and returns the element the transition moves (scenes are static — the
|
|
251
|
+
// canvas never animates, mirroring the Android SurfaceView rule; screens animate over it).
|
|
252
|
+
const mountDest = (dest: Dest): { el: HTMLElement, static?: boolean } | undefined => {
|
|
253
|
+
if (dest.kind === "screen") {
|
|
254
|
+
root.appendChild(dest.screen.root)
|
|
255
|
+
dest.screen.applyLayout(cachedWidth, cachedHeight)
|
|
256
|
+
fireLifecycle(dest, "ol")
|
|
257
|
+
dest.screen.setPagersActive(true) // open embedded pages (after the screen's onOpen)
|
|
258
|
+
return { el: dest.screen.root }
|
|
259
|
+
}
|
|
260
|
+
if (dest.kind === "scene3d" || dest.kind === "scene2d") {
|
|
261
|
+
const hooks = sceneHost[dest.kind]
|
|
262
|
+
if (!hooks) {
|
|
263
|
+
console.error(`[le.codes] openView: this host has no ${dest.kind === "scene3d" ? "3D" : "2D"} engine`)
|
|
264
|
+
return undefined
|
|
265
|
+
}
|
|
266
|
+
hooks.open(dest.desc.sceneId) // the host activates the engine — the SDK no longer does
|
|
267
|
+
hooks.el.style.display = "block"
|
|
268
|
+
fireLifecycle(dest, "ol")
|
|
269
|
+
return { el: hooks.el, static: true }
|
|
270
|
+
}
|
|
271
|
+
if (dest.kind === "native") {
|
|
272
|
+
const cell = dest.cell ??= document.createElement("div")
|
|
273
|
+
cell.style.cssText = "position:absolute;inset:0;overflow:hidden"
|
|
274
|
+
nativeViews.mountInto(dest.desc._p, cell)
|
|
275
|
+
root.appendChild(cell)
|
|
276
|
+
fireLifecycle(dest, "ol")
|
|
277
|
+
return { el: cell }
|
|
278
|
+
}
|
|
279
|
+
// videoView — fullscreen promotion of the player's own <video> element: reparent it into a
|
|
280
|
+
// fullscreen wrapper (playback continues) and put it back on dismiss.
|
|
281
|
+
const videoEl = getVideoElement(dest.desc.node)
|
|
282
|
+
dest.restore = { parent: videoEl.parentElement, next: videoEl.nextSibling, cssText: videoEl.style.cssText }
|
|
283
|
+
const wrapper = dest.wrapper ??= document.createElement("div")
|
|
284
|
+
wrapper.style.cssText = "position:absolute;inset:0;background:#000"
|
|
285
|
+
videoEl.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:contain"
|
|
286
|
+
wrapper.appendChild(videoEl)
|
|
287
|
+
root.appendChild(wrapper)
|
|
288
|
+
fireLifecycle(dest, "ol")
|
|
289
|
+
return { el: wrapper }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const destElement = (dest: Dest): { el: HTMLElement, static?: boolean } | undefined => {
|
|
293
|
+
if (dest.kind === "screen") return { el: dest.screen.root }
|
|
294
|
+
if (dest.kind === "scene3d" || dest.kind === "scene2d") {
|
|
295
|
+
const hooks = sceneHost[dest.kind]
|
|
296
|
+
return hooks ? { el: hooks.el, static: true } : undefined
|
|
297
|
+
}
|
|
298
|
+
if (dest.kind === "native") return dest.cell ? { el: dest.cell } : undefined
|
|
299
|
+
return dest.wrapper ? { el: dest.wrapper } : undefined
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Physical dismount, after the transition. `free` releases a screen for good unless the router
|
|
303
|
+
// still holds it in history (then it only detaches, staying restorable).
|
|
304
|
+
const unmountDest = (dest: Dest, free: boolean) => {
|
|
305
|
+
if (dest.kind === "screen") {
|
|
306
|
+
if (free && !router.stack.includes(dest)) dest.screen.remove()
|
|
307
|
+
else dest.screen.removeFromHTML()
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
if (dest.kind === "scene3d" || dest.kind === "scene2d") {
|
|
311
|
+
const hooks = sceneHost[dest.kind]
|
|
312
|
+
if (hooks) { hooks.close(); hooks.el.style.display = "none" } // rendering pauses while covered
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
if (dest.kind === "native") {
|
|
316
|
+
dest.cell?.remove() // the live instance survives (keeps its state) — only the mount point goes
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
const videoEl = getVideoElement(dest.desc.node)
|
|
320
|
+
if (dest.restore) {
|
|
321
|
+
videoEl.style.cssText = dest.restore.cssText
|
|
322
|
+
if (dest.restore.parent) dest.restore.parent.insertBefore(videoEl, dest.restore.next)
|
|
323
|
+
else videoEl.remove()
|
|
324
|
+
}
|
|
325
|
+
dest.wrapper?.remove()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Router history leaving for good (replace/pop/re-init). If the dest is mid-exit-animation,
|
|
329
|
+
// defer the free to the transition's onDone.
|
|
330
|
+
let animatingPrev: Dest | null = null
|
|
331
|
+
let animatingPrevFree = false
|
|
332
|
+
const freeDest = (dest: Dest) => {
|
|
333
|
+
if (dest === animatingPrev) { animatingPrevFree = true; return }
|
|
334
|
+
if (dest.kind === "screen") dest.screen.remove()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const show = (next: Dest | null, transition: string | object | null | undefined, freePrev: boolean) => {
|
|
338
|
+
presentPlayer.finish() // snap any in-flight transition (dismounts its outgoing side)
|
|
339
|
+
const prev = current
|
|
340
|
+
if (prev === next) return
|
|
341
|
+
if (prev) fireLifecycle(prev, "cl") // onClose fires at transition start (native parity)
|
|
342
|
+
|
|
343
|
+
current = next
|
|
344
|
+
currentScreen = next?.kind === "screen" ? next.screen : null
|
|
345
|
+
const incoming = next ? mountDest(next) : undefined
|
|
346
|
+
updateAttachedWidgets()
|
|
347
|
+
|
|
348
|
+
if (!prev) return
|
|
349
|
+
animatingPrev = prev
|
|
350
|
+
animatingPrevFree = false
|
|
351
|
+
presentPlayer.play({
|
|
352
|
+
incoming,
|
|
353
|
+
outgoing: destElement(prev),
|
|
354
|
+
transition,
|
|
355
|
+
onDone: () => {
|
|
356
|
+
const wantFree = freePrev || animatingPrevFree
|
|
357
|
+
animatingPrev = null
|
|
358
|
+
unmountDest(prev, wantFree)
|
|
359
|
+
},
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---- the bridge surface ----------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
const openView = (view: any, transition?: string | object) => {
|
|
366
|
+
if (router.show) routerHide()
|
|
367
|
+
const [ width, height ] = rootSize()
|
|
368
|
+
_onResize(width, height)
|
|
369
|
+
show(makeDest(view), transition ?? "none", true)
|
|
370
|
+
}
|
|
371
|
+
const closeView = () => {
|
|
372
|
+
show(null, "none", true)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Legacy aliases — previously compiled bundles keep running, with the new one-destination
|
|
376
|
+
// semantics (an openScreen over a scene now replaces it instead of overlaying it).
|
|
377
|
+
const openScreen = (screenObj: any) => openView(screenObj, "none")
|
|
378
|
+
|
|
379
|
+
let onRouterChange: (view: any) => void
|
|
380
|
+
// The shared Router class doesn't thread a transition through onScreen — stash it around the call.
|
|
381
|
+
let pendingTransition: string | object | null = null
|
|
382
|
+
const router = new Router<Dest>((dest) => {
|
|
383
|
+
const transition = pendingTransition
|
|
384
|
+
pendingTransition = null
|
|
385
|
+
show(dest, transition, false) // router history detaches, never frees (freeDest handles that)
|
|
386
|
+
onRouterChange?.(dest.kind === "screen" ? dest.node : dest.desc)
|
|
387
|
+
}, freeDest)
|
|
388
|
+
|
|
389
|
+
const routerOpen = (homePage: any, onChange: (view: any) => void) => {
|
|
390
|
+
if (current) {
|
|
391
|
+
// A current router destination is freed by the stack loop below (detach only here to avoid
|
|
392
|
+
// a double free); anything else is closed outright.
|
|
393
|
+
fireLifecycle(current, "cl")
|
|
394
|
+
unmountDest(current, !router.stack.includes(current))
|
|
395
|
+
current = null
|
|
396
|
+
currentScreen = null
|
|
397
|
+
}
|
|
398
|
+
// Re-initializing the router: free any leftover history from a previous session.
|
|
399
|
+
for (const dest of router.stack) freeDest(dest)
|
|
400
|
+
router.stack = []
|
|
401
|
+
|
|
402
|
+
const [ width, height ] = rootSize()
|
|
403
|
+
_onResize(width, height)
|
|
404
|
+
|
|
405
|
+
onRouterChange = onChange
|
|
406
|
+
router.init(makeDest(homePage))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const routerPush = (page: any, transition?: string | object) => {
|
|
410
|
+
pendingTransition = transition ?? "push"
|
|
411
|
+
router.push(makeDest(page))
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const routerReplace = (page: any, transition?: string | object) => {
|
|
415
|
+
pendingTransition = transition ?? "fade"
|
|
416
|
+
router.replace(makeDest(page))
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const routerPop = (to: number, transition?: string | object) => {
|
|
420
|
+
pendingTransition = transition ?? "pop"
|
|
421
|
+
router.pop(to)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const routerHide = () => {
|
|
425
|
+
router.hide()
|
|
426
|
+
if (current && router.stack.includes(current)) {
|
|
427
|
+
presentPlayer.finish()
|
|
428
|
+
fireLifecycle(current, "cl")
|
|
429
|
+
const detached = current
|
|
430
|
+
current = null
|
|
431
|
+
currentScreen = null
|
|
432
|
+
unmountDest(detached, false)
|
|
433
|
+
updateAttachedWidgets()
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const routerRestore = () => {
|
|
438
|
+
router.restore()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const getTextValue = (nodeId: number): string => {
|
|
442
|
+
// Resolve the owning screen by id — an input can live inside a hosted page (its own layout
|
|
443
|
+
// root), not just the active screen.
|
|
444
|
+
const owner = getScreen(module._getScreenId(nodeId)) ?? currentScreen
|
|
445
|
+
const element = owner?.nodeMap.get(nodeId) as (HTMLInputElement | HTMLTextAreaElement)
|
|
446
|
+
if (!element) return ""
|
|
447
|
+
return element.value
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Absolute device-space rect [left, top, width, height] backing el.getBoundingClientRect().
|
|
451
|
+
// Read from the DOM rather than the yoga tree so scroll offsets, vlist windows, and in-flight
|
|
452
|
+
// transition transforms are all included for free; normalized against the app root's own rect
|
|
453
|
+
// (and its scale, when the editor preview shrinks the device frame) so values come back in
|
|
454
|
+
// logical px — the same space widget top/left position in.
|
|
455
|
+
const getBoundingClientRect = (nodeId: number): [number, number, number, number] | null => {
|
|
456
|
+
const owner = getScreen(module._getScreenId(nodeId)) ?? currentScreen
|
|
457
|
+
const element = owner?.nodeMap.get(nodeId)
|
|
458
|
+
if (!element || !element.isConnected) return null
|
|
459
|
+
const rect = element.getBoundingClientRect()
|
|
460
|
+
// an all-zero DOMRect means display:none — a screen parked in router history
|
|
461
|
+
if (rect.width === 0 && rect.height === 0 && rect.left === 0 && rect.top === 0) return null
|
|
462
|
+
const appRect = root.getBoundingClientRect()
|
|
463
|
+
const [displayWidth] = getDisplaySize()
|
|
464
|
+
const scale = displayWidth > 0 && appRect.width > 0 ? appRect.width / displayWidth : 1
|
|
465
|
+
return [
|
|
466
|
+
(rect.left - appRect.left) / scale, (rect.top - appRect.top) / scale,
|
|
467
|
+
rect.width / scale, rect.height / scale,
|
|
468
|
+
]
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const closeScreen = () => closeView()
|
|
472
|
+
|
|
473
|
+
const updateNode = (nodeId: number, prop: string, value: string | number) => {
|
|
474
|
+
const screenId = module._getScreenId(nodeId)
|
|
475
|
+
const screen = getScreen(screenId)
|
|
476
|
+
if (!screen) return
|
|
477
|
+
const element = screen.nodeMap.get(nodeId)
|
|
478
|
+
if (!element) return
|
|
479
|
+
|
|
480
|
+
switch (prop) {
|
|
481
|
+
case "text":
|
|
482
|
+
element.textContent = value as string
|
|
483
|
+
module._markNodeDirty(nodeId)
|
|
484
|
+
screen.recalc()
|
|
485
|
+
break
|
|
486
|
+
case "value":
|
|
487
|
+
(element as HTMLInputElement).value = value as string
|
|
488
|
+
// Auto-grow: a content-measured textarea re-measures on programmatic value sets too.
|
|
489
|
+
if (element instanceof HTMLTextAreaElement && module._nodeHasMeasure(nodeId)) {
|
|
490
|
+
module._markNodeDirty(nodeId)
|
|
491
|
+
screen.recalc()
|
|
492
|
+
}
|
|
493
|
+
break
|
|
494
|
+
case "src":
|
|
495
|
+
screen.updateImageSrc(element, value as any)
|
|
496
|
+
break
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Render only a sub-rectangle of an image's source texture — the atlas / spritesheet
|
|
501
|
+
// primitive. Coords are texture pixels (= CSS px on web, no density scaling). The crop is
|
|
502
|
+
// applied by swapping the <img> for a background-image <div> (see setImageSourceRect).
|
|
503
|
+
const setSourceRect = (nodeId: number, x: number, y: number, w: number, h: number) => {
|
|
504
|
+
getScreen(module._getScreenId(nodeId))?.setSourceRect(nodeId, x, y, w, h)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Canvas.update() → repaint every live UIImage(canvas) node showing this baked surface with the
|
|
508
|
+
// re-rasterized pixels (draw-only, no relayout). The surface can back nodes on any screen/widget
|
|
509
|
+
// — including ones parked in router history — so sweep them all.
|
|
510
|
+
const refreshCanvasSurface = (surfaceId: number) => {
|
|
511
|
+
for (const screen of screens.values()) screen.refreshCanvasSurface(surfaceId)
|
|
512
|
+
for (const widget of widgets.values()) widget.refreshCanvasSurface(surfaceId)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Fonts the user code registered, tracked so _destroy can remove them from the (process-wide)
|
|
516
|
+
// document.fonts set instead of letting them accumulate across runs.
|
|
517
|
+
const registeredFonts: FontFace[] = []
|
|
518
|
+
|
|
519
|
+
// Every cached yoga text measure goes stale when a face lands after layout (or the default
|
|
520
|
+
// font retargets): dirty the measure leaves everywhere — screens in history included — and
|
|
521
|
+
// lay the live surfaces out again.
|
|
522
|
+
const dirtyAllTextMeasures = () => {
|
|
523
|
+
for (const collection of [screens, widgets]) {
|
|
524
|
+
for (const screen of collection.values()) {
|
|
525
|
+
for (const id of screen.nodeMap.keys()) {
|
|
526
|
+
if (module._nodeHasMeasure(id)) module._markNodeDirty(id)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const relayoutAll = () => {
|
|
532
|
+
if (!cachedWidth || !cachedHeight) return
|
|
533
|
+
currentScreen?.applyLayout(cachedWidth, cachedHeight)
|
|
534
|
+
for (const widget of widgets.values()) widget.applyLayout(cachedWidth, cachedHeight)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const registerFont = (fontFamily: string, url: string, options: { weight?: number, style?: string }, onComplete: () => void, onReject: () => void) => {
|
|
538
|
+
const font = new FontFace(fontFamily, `url(${url})`, options as any)
|
|
539
|
+
// Added before load resolves: DOM text swaps the moment the bytes land (font-display: swap
|
|
540
|
+
// semantics), whether or not anything awaits the legacy completion callback.
|
|
541
|
+
document.fonts.add(font)
|
|
542
|
+
registeredFonts.push(font)
|
|
543
|
+
font.load().then(() => {
|
|
544
|
+
// Late arrival is the fallback path (boot faces are prefetched via the `// fonts:` header):
|
|
545
|
+
// re-measure + re-layout so yoga agrees with the swapped-in glyphs.
|
|
546
|
+
dirtyAllTextMeasures()
|
|
547
|
+
relayoutAll()
|
|
548
|
+
onComplete()
|
|
549
|
+
})
|
|
550
|
+
.catch(onReject)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const updateStyle = (nodeId: number, prop: string, value: string | number) => {
|
|
554
|
+
applyStyle(LAYER_DEFAULT, nodeId, prop, value)
|
|
555
|
+
// Recalc the node's own screen — not necessarily the active one (the node may live
|
|
556
|
+
// on a screen that's currently in history).
|
|
557
|
+
getScreen(module._getScreenId(nodeId))?.recalc()
|
|
558
|
+
}
|
|
559
|
+
const mergeStyle = (nodeId: number, style: any) => {
|
|
560
|
+
for (let [ key, value ] of Object.entries(style)) {
|
|
561
|
+
applyStyle(LAYER_DEFAULT, nodeId, key, value as string | number)
|
|
562
|
+
}
|
|
563
|
+
getScreen(module._getScreenId(nodeId))?.recalc()
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Resolve a user style class ("$"+name) to its layer id (allocated app-global by getLayer on
|
|
567
|
+
// first sight). Reuses the interned-key pointer cache. The declaration path needs no changes —
|
|
568
|
+
// a `$checked: {...}` block already flows through applyStyle → getLayer → applyStyleTransition.
|
|
569
|
+
const classLayer = (nodeId: number, name: string) => module._getLayer(nodeId, internKey("$" + name))
|
|
570
|
+
|
|
571
|
+
// Class toggling lives in creator-ui (setNodeClass): name→layer conversion + the DESCENDANT
|
|
572
|
+
// CASCADE — a class set here is active on the whole subtree (docs/style-class-cascade-plan.md).
|
|
573
|
+
const setClass = (nodeId: number, name: string, enabled: boolean) => {
|
|
574
|
+
module._setNodeClass(nodeId, internKey(name), enabled)
|
|
575
|
+
getScreen(module._getScreenId(nodeId))?.recalc()
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const getOverlay = (yogaId: number) => getScreen(module._getScreenId(yogaId))?.overlayElement
|
|
579
|
+
const transitionSystem = createTransitionSystem(module, styleBlock, getNode, applyStyle, getOverlay)
|
|
580
|
+
|
|
581
|
+
const toast = createToastSystem(root)
|
|
582
|
+
|
|
583
|
+
let onResizeCallback: (width: number, height: number) => void = () => {}
|
|
584
|
+
let cachedWidth: number
|
|
585
|
+
let cachedHeight: number
|
|
586
|
+
// Simulated safe insets: the neutral web defaults, unless a device-emulating embedder (the
|
|
587
|
+
// editor Simulator's iOS/Android shells) pins a spec via _setSafeAreaSpec.
|
|
588
|
+
let safeAreaSpec: SafeAreaSpec = DEFAULT_SAFE_AREA
|
|
589
|
+
const _onResize = (width: number, height: number) => {
|
|
590
|
+
if (width === 0 || height === 0) return
|
|
591
|
+
|
|
592
|
+
// Bars first — the paddings call is what re-resolves the dynamic safe-*/comfort-* styles.
|
|
593
|
+
const { insets, bars } = orientedSafeArea(safeAreaSpec, width > height)
|
|
594
|
+
module._setSafeAreaBars(bars.reduce((mask, edge) => mask | EDGE_BIT[edge], 0))
|
|
595
|
+
module._setSafePaddings(insets[0], insets[1], insets[2], insets[3])
|
|
596
|
+
module._setConditionValue(width, height)
|
|
597
|
+
if (currentScreen) {
|
|
598
|
+
currentScreen?.applyLayout(width, height)
|
|
599
|
+
}
|
|
600
|
+
// Widgets are their own layout roots — without this they'd keep the dimensions they were
|
|
601
|
+
// opened with forever (the "gap after opening during the simulator's pop animation" bug).
|
|
602
|
+
for (const widget of widgets.values()) {
|
|
603
|
+
widget.applyLayout(width, height)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
cachedWidth = width
|
|
607
|
+
cachedHeight = height
|
|
608
|
+
|
|
609
|
+
onResizeCallback(width, height)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const registerResizeEvent = (callback: (width: number, height: number) => void) => {
|
|
613
|
+
onResizeCallback = callback
|
|
614
|
+
}
|
|
615
|
+
// Current display size (device.width / device.height) — the last size seen by _onResize.
|
|
616
|
+
const getDisplaySize = (): [number, number] => [cachedWidth ?? 0, cachedHeight ?? 0]
|
|
617
|
+
|
|
618
|
+
// Host-config: pin the safe-area insets/bars (null restores the web defaults); re-applies
|
|
619
|
+
// through _onResize when the host is already sized.
|
|
620
|
+
const setSafeAreaSpec = (spec: SafeAreaSpec | null) => {
|
|
621
|
+
safeAreaSpec = spec ?? DEFAULT_SAFE_AREA
|
|
622
|
+
if (cachedWidth && cachedHeight) _onResize(cachedWidth, cachedHeight)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// _creatorUI.setTheme (docs/ui-theme-plan.md): merge app theme values into the core table,
|
|
626
|
+
// then re-resolve every live root's dynamic styles (var() refs, comfort-* tokens). Numbers are
|
|
627
|
+
// lengths → "px" suffix (the core's bare-number string branches mean percent); null removes.
|
|
628
|
+
// Colors/strings re-dispatch through the registered handlers synchronously; layout props mark
|
|
629
|
+
// yoga dirty, so the visible roots relayout the same way a resize does (covered screens
|
|
630
|
+
// relayout when shown).
|
|
631
|
+
const setTheme = (values: Record<string, string | number | null>) => {
|
|
632
|
+
for (const key of Object.keys(values)) {
|
|
633
|
+
const value = key === "fontFamily" && typeof values[key] === "string"
|
|
634
|
+
? cssFontFamily(values[key] as string)
|
|
635
|
+
: values[key]
|
|
636
|
+
const str = value == null ? null : typeof value === "number" ? `${value}px` : value
|
|
637
|
+
const keyLen = key.length + 1
|
|
638
|
+
const keyPtr = module._malloc(keyLen)
|
|
639
|
+
module.stringToUTF8(key, keyPtr, keyLen)
|
|
640
|
+
let valPtr = 0
|
|
641
|
+
if (str !== null) {
|
|
642
|
+
const valLen = module.lengthBytesUTF8(str) + 1
|
|
643
|
+
valPtr = module._malloc(valLen)
|
|
644
|
+
module.stringToUTF8(str, valPtr, valLen)
|
|
645
|
+
}
|
|
646
|
+
module._setThemeValue(keyPtr, valPtr)
|
|
647
|
+
if (valPtr) module._free(valPtr)
|
|
648
|
+
module._free(keyPtr)
|
|
649
|
+
// Mirror as a CSS custom property on the app root: `var(--x)` values the DOM renders and
|
|
650
|
+
// the theme-sourced defaults (.screen color/font-family in defaultStyle.css) re-resolve
|
|
651
|
+
// natively. Env names never reach here (the SDK strips them; the core ignores them too).
|
|
652
|
+
if (str === null) root.style.removeProperty(`--${key}`)
|
|
653
|
+
else root.style.setProperty(`--${key}`, str)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
let relayout = module._applyTheme()
|
|
657
|
+
|
|
658
|
+
// System default: the `fontFamily` theme key retargets the default text font. The DOM side
|
|
659
|
+
// follows via CSS above — measurement must agree, and every text node's cached yoga measure
|
|
660
|
+
// is stale, so dirty the measure leaves everywhere (screens in history included).
|
|
661
|
+
if ("fontFamily" in values) {
|
|
662
|
+
setDefaultFontFamily(values.fontFamily == null ? null : String(values.fontFamily))
|
|
663
|
+
dirtyAllTextMeasures()
|
|
664
|
+
relayout = true
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (relayout && cachedWidth && cachedHeight) {
|
|
668
|
+
currentScreen?.applyLayout(cachedWidth, cachedHeight)
|
|
669
|
+
for (const widget of widgets.values()) {
|
|
670
|
+
widget.applyLayout(cachedWidth, cachedHeight)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const touchSystem = createTouchSystem(root)
|
|
676
|
+
touchSystem.onNodeStateChanged((nodeId, pressed) => {
|
|
677
|
+
// Flips the built-in onPressed layer AND the cascading `$pressed` class (creator-ui pairs them).
|
|
678
|
+
module._setNodePressed(nodeId, pressed)
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
// Touch: native defaults (focus, scroll) stay on editables/scrollers; gesture surfaces (canvas,
|
|
682
|
+
// `.interactive`) keep the touch for the app — see viewer-lite's touchDefaultsGuard. Root-level
|
|
683
|
+
// touch-action drops double-tap/pinch zoom on the app surface without touching native panning.
|
|
684
|
+
const savedTouchAction = root.style.touchAction
|
|
685
|
+
root.style.touchAction = "pan-x pan-y"
|
|
686
|
+
const removeTouchGuard = installTouchDefaultsGuard(root, { scroller: ".scrollable,.vlist,.pager" })
|
|
687
|
+
root.addEventListener("pointerdown", touchSystem.onSceneTouchDown)
|
|
688
|
+
root.addEventListener("pointerup", touchSystem.onSceneTouchEnd)
|
|
689
|
+
root.addEventListener("pointercancel", touchSystem.onSceneTouchCancel)
|
|
690
|
+
const removeDismissGuard = installKeyboardDismissGuard(root)
|
|
691
|
+
|
|
692
|
+
const _destroy = () => {
|
|
693
|
+
removeDismissGuard()
|
|
694
|
+
removeTouchGuard()
|
|
695
|
+
root.style.touchAction = savedTouchAction
|
|
696
|
+
root.removeEventListener("pointerdown", touchSystem.onSceneTouchDown)
|
|
697
|
+
root.removeEventListener("pointerup", touchSystem.onSceneTouchEnd)
|
|
698
|
+
root.removeEventListener("pointercancel", touchSystem.onSceneTouchCancel)
|
|
699
|
+
touchSystem._destroy()
|
|
700
|
+
nativeViews.destroy()
|
|
701
|
+
for (const font of registeredFonts) document.fonts.delete(font)
|
|
702
|
+
registeredFonts.length = 0
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const showWidget = (widgetJS: any, ownerDesc?: any) => {
|
|
706
|
+
for (let w of widgets.values()) {
|
|
707
|
+
if (w.node === widgetJS) return
|
|
708
|
+
}
|
|
709
|
+
const screen = new UIScreen(module, touchSystem, applyStyle, classLayer, vlistSystem, pagerSystem, deregister, canvasHost, nativeViews)
|
|
710
|
+
screen.isWidget = true
|
|
711
|
+
screen.ownerDesc = ownerDesc // attached widget: visible only while the owner is presented
|
|
712
|
+
|
|
713
|
+
widgets.set(-widgetsId, screen)
|
|
714
|
+
screen.init(-widgetsId, widgetJS)
|
|
715
|
+
widgetsId++
|
|
716
|
+
|
|
717
|
+
const [ width, height ] = rootSize()
|
|
718
|
+
screen.applyLayout(width, height)
|
|
719
|
+
|
|
720
|
+
screen.root.classList.add("widget")
|
|
721
|
+
// A widget with a root-level touch listener catches taps on its whole surface (pointer-events:
|
|
722
|
+
// all); without one it stays pointer-transparent so taps fall through to the scene beneath — same
|
|
723
|
+
// rule as `.screen` (createNodeElement). Non-interactive children re-enable events themselves.
|
|
724
|
+
if (widgetJS.touchStartListeners?.length > 0) screen.root.classList.add("interactive")
|
|
725
|
+
root.appendChild(screen.root)
|
|
726
|
+
screen.setPagersActive(true) // a pager inside a widget opens its current page
|
|
727
|
+
if (ownerDesc !== undefined) updateAttachedWidgets()
|
|
728
|
+
}
|
|
729
|
+
const hideWidget = (widgetJS: any) => {
|
|
730
|
+
if (!widgetJS._id) return
|
|
731
|
+
const screenId = module._getScreenId(widgetJS._id)
|
|
732
|
+
if (screenId >= 0) return
|
|
733
|
+
const widget = getScreen(screenId)
|
|
734
|
+
widget?.remove() // self-deregisters via the deregister hook
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const insertNode = (index: number, nodeId: number, node: any) => {
|
|
738
|
+
const screenId = module._getScreenId(nodeId)
|
|
739
|
+
const screen = getScreen(screenId)
|
|
740
|
+
screen?.insertNode(index, nodeId, node)
|
|
741
|
+
}
|
|
742
|
+
const removeNode = (nodeId: number, node: any) => {
|
|
743
|
+
const screenId = module._getScreenId(nodeId)
|
|
744
|
+
const screen = getScreen(screenId)
|
|
745
|
+
screen?.removeNode(nodeId, node)
|
|
746
|
+
}
|
|
747
|
+
const setContent = (nodeId: number, oldChildren: any[], newChildren: any[]) => {
|
|
748
|
+
const screenId = module._getScreenId(nodeId)
|
|
749
|
+
const screen = getScreen(screenId)
|
|
750
|
+
screen?.setContent(nodeId, oldChildren, newChildren)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
openScreen,
|
|
755
|
+
closeScreen,
|
|
756
|
+
routerOpen,
|
|
757
|
+
routerPush,
|
|
758
|
+
routerReplace,
|
|
759
|
+
routerPop,
|
|
760
|
+
routerHide,
|
|
761
|
+
routerRestore,
|
|
762
|
+
// Presentable navigation (docs/navigation-presentable-plan.md) — the SDK feature-detects these.
|
|
763
|
+
openView,
|
|
764
|
+
closeView,
|
|
765
|
+
isViewSupported: nativeViews.isViewSupported,
|
|
766
|
+
viewCall: nativeViews.viewCall,
|
|
767
|
+
// Host-side plugin registration (the embedding app calls host._creatorUI._registerView).
|
|
768
|
+
_registerView: nativeViews.registerView,
|
|
769
|
+
_setSceneHost,
|
|
770
|
+
updateNode,
|
|
771
|
+
setSourceRect,
|
|
772
|
+
refreshCanvasSurface,
|
|
773
|
+
updateStyle,
|
|
774
|
+
mergeStyle,
|
|
775
|
+
setClass,
|
|
776
|
+
registerFont,
|
|
777
|
+
// Browser host: CDN faces should come as woff2 (the font() macro's generated code reads this).
|
|
778
|
+
fontFormat: "woff2",
|
|
779
|
+
toast,
|
|
780
|
+
registerTouchStartEvent: touchSystem.registerTouchStartEvent,
|
|
781
|
+
registerTouchEndEvent: touchSystem.registerTouchEndEvent,
|
|
782
|
+
isButtonPressed: touchSystem.isButtonPressed,
|
|
783
|
+
registerResizeEvent,
|
|
784
|
+
getDisplaySize,
|
|
785
|
+
getTextValue,
|
|
786
|
+
getBoundingClientRect,
|
|
787
|
+
insertNode,
|
|
788
|
+
removeNode,
|
|
789
|
+
setContent,
|
|
790
|
+
animateTo: transitionSystem.animateTo,
|
|
791
|
+
animateFrom: transitionSystem.animateFrom,
|
|
792
|
+
showWidget,
|
|
793
|
+
hideWidget,
|
|
794
|
+
_onResize,
|
|
795
|
+
_destroy,
|
|
796
|
+
_setCreator: touchSystem.setCreator,
|
|
797
|
+
_setSafeAreaSpec: setSafeAreaSpec,
|
|
798
|
+
setTheme,
|
|
799
|
+
...vlistSystem.api,
|
|
800
|
+
// Generic command channel — pager nodes route to their system, everything else to vlist.
|
|
801
|
+
command: (jsNode: any, name: string, ...args: any[]) => {
|
|
802
|
+
if (jsNode?.type === "pager") return pagerSystem.command(jsNode, name, ...args)
|
|
803
|
+
return vlistSystem.api.command(jsNode, name, ...args)
|
|
804
|
+
},
|
|
805
|
+
}
|
|
806
|
+
}
|