@react-x11/components 0.3.0 → 0.5.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 +131 -4
- package/dist/charts/components.d.ts +3 -1
- package/dist/charts/components.d.ts.map +1 -1
- package/dist/charts/components.js +8 -4
- package/dist/charts/components.js.map +1 -1
- package/dist/charts/node.d.ts +13 -6
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +75 -24
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +4 -0
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +11 -6
- package/dist/charts/render.js.map +1 -1
- package/dist/code-editor/node.d.ts +47 -16
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +121 -51
- package/dist/code-editor/node.js.map +1 -1
- package/dist/flow/draw.d.ts +19 -1
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +64 -35
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +24 -8
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/model.d.ts +4 -2
- package/dist/flow/model.d.ts.map +1 -1
- package/dist/flow/model.js +4 -2
- package/dist/flow/model.js.map +1 -1
- package/dist/flow/node.d.ts +40 -9
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +142 -53
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/types.d.ts +46 -21
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/formula/index.d.ts +4 -2
- package/dist/formula/index.d.ts.map +1 -1
- package/dist/formula/index.js.map +1 -1
- package/dist/formula/node.d.ts +5 -1
- package/dist/formula/node.d.ts.map +1 -1
- package/dist/formula/node.js +19 -3
- package/dist/formula/node.js.map +1 -1
- package/dist/html/controls.d.ts +4 -1
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +11 -5
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +18 -8
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +15 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +41 -16
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +3 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3 -1
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/values.d.ts +14 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +11 -8
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +1 -0
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +12 -5
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +7 -4
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +22 -1
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +22 -8
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +97 -15
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +3 -0
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +2 -2
- package/dist/html/paint.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/maps/clip.d.ts +43 -0
- package/dist/maps/clip.d.ts.map +1 -0
- package/dist/maps/clip.js +171 -0
- package/dist/maps/clip.js.map +1 -0
- package/dist/maps/gzip.d.ts +15 -0
- package/dist/maps/gzip.d.ts.map +1 -0
- package/dist/maps/gzip.js +56 -0
- package/dist/maps/gzip.js.map +1 -0
- package/dist/maps/index.d.ts +55 -0
- package/dist/maps/index.d.ts.map +1 -0
- package/dist/maps/index.js +141 -0
- package/dist/maps/index.js.map +1 -0
- package/dist/maps/labels.d.ts +123 -0
- package/dist/maps/labels.d.ts.map +1 -0
- package/dist/maps/labels.js +323 -0
- package/dist/maps/labels.js.map +1 -0
- package/dist/maps/mvt.d.ts +244 -0
- package/dist/maps/mvt.d.ts.map +1 -0
- package/dist/maps/mvt.js +820 -0
- package/dist/maps/mvt.js.map +1 -0
- package/dist/maps/node.d.ts +278 -0
- package/dist/maps/node.d.ts.map +1 -0
- package/dist/maps/node.js +1459 -0
- package/dist/maps/node.js.map +1 -0
- package/dist/maps/overlay.d.ts +157 -0
- package/dist/maps/overlay.d.ts.map +1 -0
- package/dist/maps/overlay.js +472 -0
- package/dist/maps/overlay.js.map +1 -0
- package/dist/maps/paint.d.ts +226 -0
- package/dist/maps/paint.d.ts.map +1 -0
- package/dist/maps/paint.js +705 -0
- package/dist/maps/paint.js.map +1 -0
- package/dist/maps/proj.d.ts +281 -0
- package/dist/maps/proj.d.ts.map +1 -0
- package/dist/maps/proj.js +451 -0
- package/dist/maps/proj.js.map +1 -0
- package/dist/maps/sources.d.ts +256 -0
- package/dist/maps/sources.d.ts.map +1 -0
- package/dist/maps/sources.js +210 -0
- package/dist/maps/sources.js.map +1 -0
- package/dist/maps/style.d.ts +141 -0
- package/dist/maps/style.d.ts.map +1 -0
- package/dist/maps/style.js +147 -0
- package/dist/maps/style.js.map +1 -0
- package/dist/maps/styles.d.ts +82 -0
- package/dist/maps/styles.d.ts.map +1 -0
- package/dist/maps/styles.js +898 -0
- package/dist/maps/styles.js.map +1 -0
- package/dist/maps/tiles.d.ts +283 -0
- package/dist/maps/tiles.d.ts.map +1 -0
- package/dist/maps/tiles.js +469 -0
- package/dist/maps/tiles.js.map +1 -0
- package/dist/maps/types.d.ts +244 -0
- package/dist/maps/types.d.ts.map +1 -0
- package/dist/maps/types.js +2 -0
- package/dist/maps/types.js.map +1 -0
- package/dist/qml/containers.d.ts +3 -0
- package/dist/qml/containers.d.ts.map +1 -0
- package/dist/qml/containers.js +331 -0
- package/dist/qml/containers.js.map +1 -0
- package/dist/qml/controls.d.ts +12 -0
- package/dist/qml/controls.d.ts.map +1 -0
- package/dist/qml/controls.js +21 -0
- package/dist/qml/controls.js.map +1 -0
- package/dist/qml/globals.d.ts +6 -0
- package/dist/qml/globals.d.ts.map +1 -0
- package/dist/qml/globals.js +25 -0
- package/dist/qml/globals.js.map +1 -0
- package/dist/qml/index.d.ts +15 -0
- package/dist/qml/index.d.ts.map +1 -0
- package/dist/qml/index.js +25 -0
- package/dist/qml/index.js.map +1 -0
- package/dist/qml/interaction.d.ts +12 -0
- package/dist/qml/interaction.d.ts.map +1 -0
- package/dist/qml/interaction.js +246 -0
- package/dist/qml/interaction.js.map +1 -0
- package/dist/qml/ir.d.ts +106 -0
- package/dist/qml/ir.d.ts.map +1 -0
- package/dist/qml/ir.js +7 -0
- package/dist/qml/ir.js.map +1 -0
- package/dist/qml/layouts.d.ts +3 -0
- package/dist/qml/layouts.d.ts.map +1 -0
- package/dist/qml/layouts.js +122 -0
- package/dist/qml/layouts.js.map +1 -0
- package/dist/qml/models.d.ts +14 -0
- package/dist/qml/models.d.ts.map +1 -0
- package/dist/qml/models.js +80 -0
- package/dist/qml/models.js.map +1 -0
- package/dist/qml/objects.d.ts +228 -0
- package/dist/qml/objects.d.ts.map +1 -0
- package/dist/qml/objects.js +1103 -0
- package/dist/qml/objects.js.map +1 -0
- package/dist/qml/parse.d.ts +10 -0
- package/dist/qml/parse.d.ts.map +1 -0
- package/dist/qml/parse.js +607 -0
- package/dist/qml/parse.js.map +1 -0
- package/dist/qml/qtquick.d.ts +7 -0
- package/dist/qml/qtquick.d.ts.map +1 -0
- package/dist/qml/qtquick.js +347 -0
- package/dist/qml/qtquick.js.map +1 -0
- package/dist/qml/react.d.ts +83 -0
- package/dist/qml/react.d.ts.map +1 -0
- package/dist/qml/react.js +323 -0
- package/dist/qml/react.js.map +1 -0
- package/dist/qml/resolver.d.ts +38 -0
- package/dist/qml/resolver.d.ts.map +1 -0
- package/dist/qml/resolver.js +64 -0
- package/dist/qml/resolver.js.map +1 -0
- package/dist/qml/slots.d.ts +65 -0
- package/dist/qml/slots.d.ts.map +1 -0
- package/dist/qml/slots.js +230 -0
- package/dist/qml/slots.js.map +1 -0
- package/dist/qml/states.d.ts +5 -0
- package/dist/qml/states.d.ts.map +1 -0
- package/dist/qml/states.js +276 -0
- package/dist/qml/states.js.map +1 -0
- package/dist/qml/view-utils.d.ts +42 -0
- package/dist/qml/view-utils.d.ts.map +1 -0
- package/dist/qml/view-utils.js +121 -0
- package/dist/qml/view-utils.js.map +1 -0
- package/dist/reorder/hx.d.ts +18 -0
- package/dist/reorder/hx.d.ts.map +1 -0
- package/dist/reorder/hx.js +10 -0
- package/dist/reorder/hx.js.map +1 -0
- package/dist/reorder/index.d.ts +314 -0
- package/dist/reorder/index.d.ts.map +1 -0
- package/dist/reorder/index.js +1539 -0
- package/dist/reorder/index.js.map +1 -0
- package/dist/reorder/model.d.ts +107 -0
- package/dist/reorder/model.d.ts.map +1 -0
- package/dist/reorder/model.js +162 -0
- package/dist/reorder/model.js.map +1 -0
- package/dist/richtext/node.d.ts +15 -5
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +40 -11
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +35 -13
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +83 -27
- package/dist/richtext/runs.js.map +1 -1
- package/dist/tabs/hx.d.ts +18 -0
- package/dist/tabs/hx.d.ts.map +1 -0
- package/dist/tabs/hx.js +10 -0
- package/dist/tabs/hx.js.map +1 -0
- package/dist/tabs/index.d.ts +160 -0
- package/dist/tabs/index.d.ts.map +1 -0
- package/dist/tabs/index.js +1265 -0
- package/dist/tabs/index.js.map +1 -0
- package/dist/terminal/vt/fonts.d.ts +45 -15
- package/dist/terminal/vt/fonts.d.ts.map +1 -1
- package/dist/terminal/vt/fonts.js +89 -19
- package/dist/terminal/vt/fonts.js.map +1 -1
- package/dist/terminal/vt/index.d.ts +1 -1
- package/dist/terminal/vt/index.d.ts.map +1 -1
- package/dist/terminal/vt/index.js +3 -3
- package/dist/terminal/vt/index.js.map +1 -1
- package/dist/terminal/vt/node.d.ts +42 -0
- package/dist/terminal/vt/node.d.ts.map +1 -1
- package/dist/terminal/vt/node.js +95 -11
- package/dist/terminal/vt/node.js.map +1 -1
- package/dist/terminal/vt/pty.d.ts +16 -0
- package/dist/terminal/vt/pty.d.ts.map +1 -1
- package/dist/terminal/vt/pty.js +186 -0
- package/dist/terminal/vt/pty.js.map +1 -1
- package/dist/terminal/vt/renderer.d.ts.map +1 -1
- package/dist/terminal/vt/renderer.js +13 -3
- package/dist/terminal/vt/renderer.js.map +1 -1
- package/package.json +23 -3
- package/src/charts/components.ts +14 -7
- package/src/charts/node.ts +81 -26
- package/src/charts/render.ts +16 -6
- package/src/code-editor/node.ts +147 -53
- package/src/flow/draw.ts +104 -35
- package/src/flow/index.ts +33 -13
- package/src/flow/model.ts +4 -2
- package/src/flow/node.ts +158 -59
- package/src/flow/types.ts +46 -21
- package/src/formula/index.ts +4 -2
- package/src/formula/node.ts +24 -5
- package/src/html/controls.ts +4 -1
- package/src/html/css/cascade.ts +18 -7
- package/src/html/css/parse.ts +46 -21
- package/src/html/css/style.ts +6 -1
- package/src/html/css/values.ts +25 -9
- package/src/html/index.ts +1 -0
- package/src/html/layout/boxes.ts +19 -9
- package/src/html/layout/inline.ts +23 -1
- package/src/html/node.ts +110 -17
- package/src/html/paint.ts +17 -2
- package/src/index.ts +142 -0
- package/src/maps/clip.ts +184 -0
- package/src/maps/gzip.ts +66 -0
- package/src/maps/index.ts +316 -0
- package/src/maps/labels.ts +442 -0
- package/src/maps/mvt.ts +857 -0
- package/src/maps/node.ts +1819 -0
- package/src/maps/overlay.ts +639 -0
- package/src/maps/paint.ts +978 -0
- package/src/maps/proj.ts +641 -0
- package/src/maps/sources.ts +425 -0
- package/src/maps/style.ts +309 -0
- package/src/maps/styles.ts +1040 -0
- package/src/maps/tiles.ts +676 -0
- package/src/maps/types.ts +260 -0
- package/src/qml/containers.tsx +380 -0
- package/src/qml/controls.tsx +47 -0
- package/src/qml/globals.ts +33 -0
- package/src/qml/index.ts +67 -0
- package/src/qml/interaction.tsx +317 -0
- package/src/qml/ir.ts +80 -0
- package/src/qml/layouts.tsx +141 -0
- package/src/qml/models.ts +101 -0
- package/src/qml/objects.ts +1431 -0
- package/src/qml/parse.ts +624 -0
- package/src/qml/qtquick.tsx +413 -0
- package/src/qml/react.tsx +454 -0
- package/src/qml/resolver.ts +99 -0
- package/src/qml/slots.ts +249 -0
- package/src/qml/states.ts +318 -0
- package/src/qml/view-utils.ts +140 -0
- package/src/reorder/hx.ts +31 -0
- package/src/reorder/index.ts +2223 -0
- package/src/reorder/model.ts +232 -0
- package/src/richtext/node.ts +49 -14
- package/src/richtext/runs.ts +115 -30
- package/src/tabs/hx.ts +31 -0
- package/src/tabs/index.ts +1686 -0
- package/src/terminal/vt/fonts.ts +128 -36
- package/src/terminal/vt/index.ts +4 -2
- package/src/terminal/vt/node.ts +105 -12
- package/src/terminal/vt/pty.ts +243 -0
- package/src/terminal/vt/renderer.ts +13 -3
|
@@ -0,0 +1,1431 @@
|
|
|
1
|
+
// The QML object model: documents → live instances wired into the slot
|
|
2
|
+
// graph.
|
|
3
|
+
//
|
|
4
|
+
// Scope rules follow Qt's documented order (ids outrank the scope object's
|
|
5
|
+
// own properties; the component root is consulted before context
|
|
6
|
+
// properties; unresolved names fall through `with` to the JS globals, which
|
|
7
|
+
// is how Math, JSON and console work without us lifting a finger).
|
|
8
|
+
//
|
|
9
|
+
// Expressions compile once per document via `new Function` and are shared
|
|
10
|
+
// by every instance (a Repeater with 1000 delegates compiles each binding
|
|
11
|
+
// once).
|
|
12
|
+
//
|
|
13
|
+
// This module is deliberately renderer-free: no react-x11, and React only
|
|
14
|
+
// as a type (the `view` field a registered type may carry). It is the half
|
|
15
|
+
// that a future compiler shares with the interpreter.
|
|
16
|
+
|
|
17
|
+
import type { ComponentType } from 'react';
|
|
18
|
+
import type {
|
|
19
|
+
AliasTargetIR,
|
|
20
|
+
MemberIR,
|
|
21
|
+
ObjectIR,
|
|
22
|
+
QmlDocument,
|
|
23
|
+
ValueIR,
|
|
24
|
+
} from './ir.js';
|
|
25
|
+
import type { DocLoad, QmlResolver } from './resolver.js';
|
|
26
|
+
import { parseQml } from './parse.js';
|
|
27
|
+
import { Slot, flushBindings, type BindingFn } from './slots.js';
|
|
28
|
+
import { warn } from './globals.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A captured object template (`delegate:`, `sourceComponent:`, a `states:`
|
|
32
|
+
* entry) together with where it was written — its document (for compiling
|
|
33
|
+
* and imports) and its context (what its instances' names resolve
|
|
34
|
+
* through). A delegate handed to a file-backed ListView from the use site
|
|
35
|
+
* must keep resolving use-site names; carrying the pair is what makes
|
|
36
|
+
* that true.
|
|
37
|
+
*/
|
|
38
|
+
export interface TemplateRef {
|
|
39
|
+
object: ObjectIR;
|
|
40
|
+
doc: QmlDocument;
|
|
41
|
+
context: Context;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The use-site half of a file-backed component instance: `MyBackdrop
|
|
45
|
+
* { width: 5; … }` — members evaluated in the *site's* scope, applied on
|
|
46
|
+
* the component document's root instance. */
|
|
47
|
+
interface SiteRef {
|
|
48
|
+
ir: ObjectIR;
|
|
49
|
+
doc: QmlDocument;
|
|
50
|
+
ctx: Context;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What one member-application pass evaluates against. A plain instance
|
|
54
|
+
* has one env (its own document and context); a file-backed instance has
|
|
55
|
+
* two — the component document's for its internals, the use site's for
|
|
56
|
+
* the site members. */
|
|
57
|
+
interface EvalEnv {
|
|
58
|
+
doc: QmlDocument;
|
|
59
|
+
ctx: Context;
|
|
60
|
+
scope: object;
|
|
61
|
+
facade: QmlFacade;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* How expressions see an object: live property access, callable signals
|
|
66
|
+
* and methods. Deliberately `any`-valued — a facade is QML's dynamic edge,
|
|
67
|
+
* and `view.id('meter').width = 40` reading naturally is the point.
|
|
68
|
+
*/
|
|
69
|
+
// eslint does not see TypeScript here (AGENTS.md, "Linting"); the explicit
|
|
70
|
+
// `any` is a deliberate choice for this one boundary type.
|
|
71
|
+
export type QmlFacade = Record<string, any>;
|
|
72
|
+
|
|
73
|
+
export interface QmlPropertyDef {
|
|
74
|
+
default?: unknown;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ValueSourceHookup {
|
|
78
|
+
targetSlot: Slot;
|
|
79
|
+
target: QmlInstance;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What `registerQmlModule` takes, per type. */
|
|
83
|
+
export interface QmlTypeDef {
|
|
84
|
+
extends?: string;
|
|
85
|
+
nonVisual?: boolean;
|
|
86
|
+
/** Object children become templates under this key (Repeater's
|
|
87
|
+
* `delegate`, Loader's `sourceComponent`) instead of instantiating. */
|
|
88
|
+
capture?: string;
|
|
89
|
+
/** Bindings to undeclared names create slots (`PropertyChanges`,
|
|
90
|
+
* `ListElement`) instead of failing. */
|
|
91
|
+
dynamicProperties?: boolean;
|
|
92
|
+
/** `on<Signal>` members are collected, not attached — the type wires
|
|
93
|
+
* them itself (`Connections`). */
|
|
94
|
+
deferHandlers?: boolean;
|
|
95
|
+
/** RowLayout/ColumnLayout: children render as flex items (yoga decides
|
|
96
|
+
* geometry, read back into their slots) instead of absolutely. */
|
|
97
|
+
managesChildLayout?: boolean;
|
|
98
|
+
properties?: Record<string, QmlPropertyDef>;
|
|
99
|
+
signals?: Record<string, string[]>;
|
|
100
|
+
enums?: Record<string, number>;
|
|
101
|
+
init?: (inst: QmlInstance, hookup?: ValueSourceHookup) => void;
|
|
102
|
+
dispose?: (inst: QmlInstance) => void;
|
|
103
|
+
/** The instance's effective children changed (Repeater splice). */
|
|
104
|
+
onStructure?: (inst: QmlInstance) => void;
|
|
105
|
+
view?: ComponentType<{ inst: QmlInstance }>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface QmlTypeInfo {
|
|
109
|
+
name: string;
|
|
110
|
+
base: QmlTypeInfo | null;
|
|
111
|
+
nonVisual: boolean;
|
|
112
|
+
capture: string | null;
|
|
113
|
+
dynamicProperties: boolean;
|
|
114
|
+
deferHandlers: boolean;
|
|
115
|
+
managesChildLayout: boolean;
|
|
116
|
+
properties: Record<string, QmlPropertyDef>;
|
|
117
|
+
signals: Record<string, string[]>;
|
|
118
|
+
enums: Record<string, number>;
|
|
119
|
+
init: ((inst: QmlInstance, hookup?: ValueSourceHookup) => void) | null;
|
|
120
|
+
dispose: ((inst: QmlInstance) => void) | null;
|
|
121
|
+
onStructure: ((inst: QmlInstance) => void) | null;
|
|
122
|
+
view: ComponentType<{ inst: QmlInstance }> | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- type registry ---------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
const modules = new Map<string, { types: Map<string, QmlTypeInfo> }>();
|
|
128
|
+
|
|
129
|
+
export function registerQmlModule(
|
|
130
|
+
name: string,
|
|
131
|
+
{ types }: { version?: string; types: Record<string, QmlTypeDef> },
|
|
132
|
+
): void {
|
|
133
|
+
const mod = modules.get(name) ?? { types: new Map<string, QmlTypeInfo>() };
|
|
134
|
+
modules.set(name, mod); // visible before resolution: types extend siblings
|
|
135
|
+
for (const [typeName, def] of Object.entries(types)) {
|
|
136
|
+
mod.types.set(typeName, resolveTypeInfo(typeName, def));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveTypeInfo(name: string, def: QmlTypeDef): QmlTypeInfo {
|
|
141
|
+
let base: QmlTypeInfo | null = null;
|
|
142
|
+
if (def.extends) {
|
|
143
|
+
for (const mod of modules.values()) {
|
|
144
|
+
const hit = mod.types.get(def.extends);
|
|
145
|
+
if (hit) base = hit;
|
|
146
|
+
}
|
|
147
|
+
if (!base)
|
|
148
|
+
throw new Error(`QML type ${name} extends unknown ${def.extends}`);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
name,
|
|
152
|
+
base,
|
|
153
|
+
properties: { ...base?.properties, ...def.properties },
|
|
154
|
+
signals: { ...base?.signals, ...def.signals },
|
|
155
|
+
enums: { ...base?.enums, ...def.enums },
|
|
156
|
+
nonVisual: def.nonVisual ?? base?.nonVisual ?? false,
|
|
157
|
+
capture: def.capture ?? base?.capture ?? null,
|
|
158
|
+
dynamicProperties:
|
|
159
|
+
def.dynamicProperties ?? base?.dynamicProperties ?? false,
|
|
160
|
+
deferHandlers: def.deferHandlers ?? base?.deferHandlers ?? false,
|
|
161
|
+
managesChildLayout:
|
|
162
|
+
def.managesChildLayout ?? base?.managesChildLayout ?? false,
|
|
163
|
+
init: def.init ?? base?.init ?? null,
|
|
164
|
+
dispose: def.dispose ?? base?.dispose ?? null,
|
|
165
|
+
onStructure: def.onStructure ?? base?.onStructure ?? null,
|
|
166
|
+
view: def.view ?? base?.view ?? null,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function lookupType(doc: QmlDocument, typeName: string): QmlTypeInfo {
|
|
171
|
+
// `alias.Type` from `import … as alias`
|
|
172
|
+
let name = typeName;
|
|
173
|
+
let onlyModule: string | null = null;
|
|
174
|
+
const dot = typeName.indexOf('.');
|
|
175
|
+
if (dot !== -1) {
|
|
176
|
+
const alias = typeName.slice(0, dot);
|
|
177
|
+
const imp = doc.imports.find((i) => i.alias === alias);
|
|
178
|
+
if (imp?.module) {
|
|
179
|
+
onlyModule = imp.module;
|
|
180
|
+
name = typeName.slice(dot + 1);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const imp of doc.imports) {
|
|
184
|
+
if (!imp.module) continue;
|
|
185
|
+
if (onlyModule && imp.module !== onlyModule) continue;
|
|
186
|
+
const info = modules.get(imp.module)?.types.get(name);
|
|
187
|
+
if (info) return info;
|
|
188
|
+
}
|
|
189
|
+
const known = [...modules.keys()].join(', ');
|
|
190
|
+
const dirNote = doc.load
|
|
191
|
+
? ` No <${typeName}>.qml was found beside the document or under its ` +
|
|
192
|
+
`quoted imports either.`
|
|
193
|
+
: ` Pass a resolver (createFileResolver) to also load <Name>.qml files.`;
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Unknown QML type '${typeName}' in ${doc.fileName}. Imported modules ` +
|
|
196
|
+
`were searched; registered modules: ${known}. Register your own with ` +
|
|
197
|
+
`registerQmlModule(module, { types }).` +
|
|
198
|
+
dirNote,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- file-backed components (the resolver seam) ----------------------------
|
|
203
|
+
|
|
204
|
+
/** A type name that resolved to a `.qml` document rather than a registry
|
|
205
|
+
* entry — instantiated by composition in createTree. */
|
|
206
|
+
interface FileComponent {
|
|
207
|
+
fileDoc: QmlDocument;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface FileCacheEntry {
|
|
211
|
+
doc: QmlDocument | null; // null = negative: no such component here
|
|
212
|
+
source: string | null;
|
|
213
|
+
gen: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Cached per (dir, name). Entries revalidate once per root instantiation
|
|
217
|
+
// (the generation bump below): a 1000-delegate ListView reads its component
|
|
218
|
+
// file once, and a hot reload of the root re-reads sources so edited
|
|
219
|
+
// siblings are picked up.
|
|
220
|
+
const fileCache = new Map<string, FileCacheEntry>();
|
|
221
|
+
let loadGeneration = 0;
|
|
222
|
+
|
|
223
|
+
function loadFileComponent(
|
|
224
|
+
load: DocLoad,
|
|
225
|
+
dir: string,
|
|
226
|
+
name: string,
|
|
227
|
+
): QmlDocument | null {
|
|
228
|
+
const key = `${dir}\u0000${name}`;
|
|
229
|
+
const cached = fileCache.get(key);
|
|
230
|
+
if (cached && cached.gen === loadGeneration) return cached.doc;
|
|
231
|
+
const hit = load.resolver.load(dir, name);
|
|
232
|
+
if (!hit) {
|
|
233
|
+
fileCache.set(key, { doc: null, source: null, gen: loadGeneration });
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
if (cached && cached.doc && cached.source === hit.source) {
|
|
237
|
+
cached.gen = loadGeneration; // unchanged: keep the parsed document
|
|
238
|
+
return cached.doc;
|
|
239
|
+
}
|
|
240
|
+
const compDoc = parseQml(hit.source, { fileName: hit.fileName });
|
|
241
|
+
compDoc.load = { resolver: load.resolver, dir };
|
|
242
|
+
fileCache.set(key, { doc: compDoc, source: hit.source, gen: loadGeneration });
|
|
243
|
+
return compDoc;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Qt's precedence: the implicit same-directory import first (a local
|
|
247
|
+
* `Button.qml` shadows a module's `Button`), then quoted-path imports,
|
|
248
|
+
* then registered modules. */
|
|
249
|
+
function resolveTypeRef(
|
|
250
|
+
doc: QmlDocument,
|
|
251
|
+
typeName: string,
|
|
252
|
+
): QmlTypeInfo | FileComponent {
|
|
253
|
+
const load = doc.load;
|
|
254
|
+
if (load && !typeName.includes('.')) {
|
|
255
|
+
const implicit = loadFileComponent(load, load.dir, typeName);
|
|
256
|
+
if (implicit) return { fileDoc: implicit };
|
|
257
|
+
for (const imp of doc.imports) {
|
|
258
|
+
if (!imp.path) continue;
|
|
259
|
+
const dir = load.resolver.join(load.dir, imp.path);
|
|
260
|
+
const fromPath = loadFileComponent(load, dir, typeName);
|
|
261
|
+
if (fromPath) return { fileDoc: fromPath };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return lookupType(doc, typeName);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** `Loader { source: "widgets/Meter.qml" }` — a relative path resolved
|
|
268
|
+
* against the document that names it. Null without a resolver or when the
|
|
269
|
+
* file is absent. */
|
|
270
|
+
export function resolveComponentByPath(
|
|
271
|
+
fromDoc: QmlDocument,
|
|
272
|
+
path: string,
|
|
273
|
+
): QmlDocument | null {
|
|
274
|
+
const load = fromDoc.load;
|
|
275
|
+
if (!load || !/\.qml$/i.test(path)) return null;
|
|
276
|
+
const parts = path.replace(/\.qml$/i, '').split('/');
|
|
277
|
+
const name = parts.pop();
|
|
278
|
+
if (!name) return null;
|
|
279
|
+
const dir = parts.length
|
|
280
|
+
? load.resolver.join(load.dir, parts.join('/'))
|
|
281
|
+
: load.dir;
|
|
282
|
+
return loadFileComponent(load, dir, name);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Instantiation-time cycle guard: A.qml using B.qml using A.qml would
|
|
286
|
+
// recurse forever in createTree; the parse-level cache cannot see it
|
|
287
|
+
// because each document parses fine on its own.
|
|
288
|
+
const instantiating: QmlDocument[] = [];
|
|
289
|
+
|
|
290
|
+
function topContextOf(ctx: Context): Context {
|
|
291
|
+
let top = ctx;
|
|
292
|
+
while (top.parent) top = top.parent;
|
|
293
|
+
return top;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// --- expression compilation ------------------------------------------------
|
|
297
|
+
|
|
298
|
+
type CompiledExpr = (this: QmlFacade, scope: object) => unknown;
|
|
299
|
+
type HandlerFactory = (scope: object) => (...args: unknown[]) => unknown;
|
|
300
|
+
|
|
301
|
+
const compiledCache = new WeakMap<object, unknown>();
|
|
302
|
+
|
|
303
|
+
function compileExpr(
|
|
304
|
+
node: Extract<ValueIR, { kind: 'expr' }>,
|
|
305
|
+
doc: QmlDocument,
|
|
306
|
+
): CompiledExpr {
|
|
307
|
+
let fn = compiledCache.get(node) as CompiledExpr | undefined;
|
|
308
|
+
if (!fn) {
|
|
309
|
+
try {
|
|
310
|
+
fn = new Function(
|
|
311
|
+
'$scope',
|
|
312
|
+
`with($scope){ return (${node.src}\n) }`,
|
|
313
|
+
) as CompiledExpr;
|
|
314
|
+
} catch (e) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`QML: bad expression in ${doc.fileName}: \`${node.src}\` (${message(e)})`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
compiledCache.set(node, fn);
|
|
320
|
+
}
|
|
321
|
+
return fn;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function compileHandler(
|
|
325
|
+
node: { kind: string; src: string },
|
|
326
|
+
params: string[],
|
|
327
|
+
doc: QmlDocument,
|
|
328
|
+
): HandlerFactory {
|
|
329
|
+
let fn = compiledCache.get(node) as HandlerFactory | undefined;
|
|
330
|
+
if (!fn) {
|
|
331
|
+
const body = node.kind === 'block' ? node.src : `return (${node.src}\n)`;
|
|
332
|
+
try {
|
|
333
|
+
fn = new Function(
|
|
334
|
+
'$scope',
|
|
335
|
+
`with($scope){ return function(${params.join(', ')}){ ${body} \n} }`,
|
|
336
|
+
) as HandlerFactory;
|
|
337
|
+
} catch (e) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`QML: bad handler in ${doc.fileName}: \`${node.src}\` (${message(e)})`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
compiledCache.set(node, fn);
|
|
343
|
+
}
|
|
344
|
+
return fn;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const message = (e: unknown): string =>
|
|
348
|
+
e instanceof Error ? e.message : String(e);
|
|
349
|
+
|
|
350
|
+
// --- contexts --------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
export class Context {
|
|
353
|
+
parent: Context | null;
|
|
354
|
+
doc: QmlDocument;
|
|
355
|
+
ids = new Map<string, QmlInstance>();
|
|
356
|
+
extras = new Map<string, Slot>(); // context properties
|
|
357
|
+
root: QmlInstance | null = null;
|
|
358
|
+
|
|
359
|
+
constructor(parent: Context | null, doc: QmlDocument) {
|
|
360
|
+
this.parent = parent;
|
|
361
|
+
this.doc = doc;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
lookupId(name: string): QmlInstance | null {
|
|
365
|
+
for (let c: Context | null = this; c; c = c.parent) {
|
|
366
|
+
const inst = c.ids.get(name);
|
|
367
|
+
if (inst) return inst;
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
lookupExtra(name: string): Slot | null {
|
|
373
|
+
for (let c: Context | null = this; c; c = c.parent) {
|
|
374
|
+
const slot = c.extras.get(name);
|
|
375
|
+
if (slot) return slot;
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const QtBindingMark = Symbol('Qt.binding');
|
|
382
|
+
|
|
383
|
+
export interface QtNamespace {
|
|
384
|
+
[key: string]: unknown;
|
|
385
|
+
rgba(r: number, g: number, b: number, a?: number): string;
|
|
386
|
+
hsla(h: number, s: number, l: number, a?: number): string;
|
|
387
|
+
point(x: number, y: number): { x: number; y: number };
|
|
388
|
+
size(width: number, height: number): { width: number; height: number };
|
|
389
|
+
rect(
|
|
390
|
+
x: number,
|
|
391
|
+
y: number,
|
|
392
|
+
width: number,
|
|
393
|
+
height: number,
|
|
394
|
+
): { x: number; y: number; width: number; height: number };
|
|
395
|
+
binding(fn: () => unknown): unknown;
|
|
396
|
+
qsTr(s: string): string;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export const Qt: QtNamespace = {
|
|
400
|
+
rgba: (r, g, b, a = 1) =>
|
|
401
|
+
`rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a})`,
|
|
402
|
+
hsla: (h, s, l, a = 1) =>
|
|
403
|
+
`hsla(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%, ${a})`,
|
|
404
|
+
point: (x, y) => ({ x, y }),
|
|
405
|
+
size: (width, height) => ({ width, height }),
|
|
406
|
+
rect: (x, y, width, height) => ({ x, y, width, height }),
|
|
407
|
+
binding: (fn) => ({ [QtBindingMark]: fn }),
|
|
408
|
+
qsTr: (s) => s,
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// Parameterized by (ctx, doc) rather than reading the instance's own: a
|
|
412
|
+
// file-backed instance evaluates its use-site members in the *site's*
|
|
413
|
+
// scope — same object, different name resolution.
|
|
414
|
+
function makeScopeProxy(
|
|
415
|
+
inst: QmlInstance,
|
|
416
|
+
ctx: Context,
|
|
417
|
+
doc: QmlDocument,
|
|
418
|
+
): object {
|
|
419
|
+
const scopeHas = (name: string): boolean => {
|
|
420
|
+
if (name === 'Qt' || name === 'parent' || name === 'this') return true;
|
|
421
|
+
if (ctx.lookupId(name)) return true;
|
|
422
|
+
if (inst.resolveMember(name) !== undefined) return true;
|
|
423
|
+
const root = ctx.root;
|
|
424
|
+
if (root && root !== inst && root.resolveMember(name) !== undefined)
|
|
425
|
+
return true;
|
|
426
|
+
if (ctx.lookupExtra(name)) return true;
|
|
427
|
+
if (enumHolder(doc, name)) return true;
|
|
428
|
+
return false; // fall through `with` to the real JS globals
|
|
429
|
+
};
|
|
430
|
+
return new Proxy(Object.create(null) as object, {
|
|
431
|
+
has: (_, name) => (typeof name === 'string' ? scopeHas(name) : false),
|
|
432
|
+
get: (_, name) => {
|
|
433
|
+
if (typeof name !== 'string') return undefined;
|
|
434
|
+
if (name === 'Qt') return Qt;
|
|
435
|
+
if (name === 'parent') return inst.parentInst?.facade ?? null;
|
|
436
|
+
const idHit = ctx.lookupId(name);
|
|
437
|
+
if (idHit) return idHit.facade;
|
|
438
|
+
let v = inst.getMember(name);
|
|
439
|
+
if (v !== undefined) return v;
|
|
440
|
+
const root = ctx.root;
|
|
441
|
+
if (root && root !== inst) {
|
|
442
|
+
v = root.getMember(name);
|
|
443
|
+
if (v !== undefined) return v;
|
|
444
|
+
}
|
|
445
|
+
const extra = ctx.lookupExtra(name);
|
|
446
|
+
if (extra) return extra.get();
|
|
447
|
+
const holder = enumHolder(doc, name);
|
|
448
|
+
if (holder) return holder;
|
|
449
|
+
return undefined;
|
|
450
|
+
},
|
|
451
|
+
set: (_, name, value) => {
|
|
452
|
+
if (typeof name !== 'string') return false;
|
|
453
|
+
// Assignment in handlers: prefer the object that owns the property.
|
|
454
|
+
if (inst.slots.has(name)) {
|
|
455
|
+
inst.setMember(name, value);
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
const root = ctx.root;
|
|
459
|
+
if (root && root !== inst && root.slots.has(name)) {
|
|
460
|
+
root.setMember(name, value);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
throw new Error(`QML: cannot assign unknown name '${name}'`);
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** `Text.AlignHCenter` — uppercase names resolve to a type's enums.
|
|
469
|
+
* Registry-only on purpose: this runs for every capitalized name any
|
|
470
|
+
* expression mentions, and must never cost a resolver probe. */
|
|
471
|
+
function enumHolder(
|
|
472
|
+
doc: QmlDocument,
|
|
473
|
+
name: string,
|
|
474
|
+
): Record<string, number> | null {
|
|
475
|
+
if (!/^[A-Z]/.test(name)) return null;
|
|
476
|
+
try {
|
|
477
|
+
const info = lookupType(doc, name);
|
|
478
|
+
return Object.keys(info.enums).length ? info.enums : null;
|
|
479
|
+
} catch {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// --- instances -------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
let nextInstanceId = 1;
|
|
487
|
+
|
|
488
|
+
export interface AnchorLine {
|
|
489
|
+
inst: QmlInstance;
|
|
490
|
+
edge: string;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export class QmlInstance {
|
|
494
|
+
readonly uid: number;
|
|
495
|
+
readonly typeInfo: QmlTypeInfo;
|
|
496
|
+
readonly doc: QmlDocument;
|
|
497
|
+
readonly context: Context;
|
|
498
|
+
parentInst: QmlInstance | null;
|
|
499
|
+
id: string | null = null;
|
|
500
|
+
ir: ObjectIR | null = null;
|
|
501
|
+
slots = new Map<string, Slot>();
|
|
502
|
+
methods = new Map<string, (...args: unknown[]) => unknown>();
|
|
503
|
+
signalParams = new Map<string, string[]>();
|
|
504
|
+
signalSubs = new Map<string, Set<(...args: unknown[]) => void>>();
|
|
505
|
+
/** Effective visual children — Repeater splices instances in here. */
|
|
506
|
+
children: QmlInstance[] = [];
|
|
507
|
+
/** Captured object templates (`delegate`, `sourceComponent`, …). */
|
|
508
|
+
templates = new Map<string, TemplateRef>();
|
|
509
|
+
/** For a file-backed component instance: the use-site members, applied
|
|
510
|
+
* in the site's own scope during activation. */
|
|
511
|
+
site: SiteRef | null = null;
|
|
512
|
+
valueSources: QmlInstance[] = [];
|
|
513
|
+
/** `Keys.onPressed` and friends: attached handlers by dotted name. */
|
|
514
|
+
attachedHandlers = new Map<string, (...args: unknown[]) => unknown>();
|
|
515
|
+
/** `Connections`: raw `onX` members, wired by the type's init. */
|
|
516
|
+
deferredHandlers: Array<{ name: string; value: ValueIR }> = [];
|
|
517
|
+
onDestruction: Array<() => void> = [];
|
|
518
|
+
anchorLines = new Map<string, AnchorLine>();
|
|
519
|
+
version = 0;
|
|
520
|
+
structVersion = 0;
|
|
521
|
+
listeners = new Set<() => void>();
|
|
522
|
+
destroyed = false;
|
|
523
|
+
/** Per-type scratch: timers, behaviors, repeater items, host node… */
|
|
524
|
+
state: Record<string, unknown> = {};
|
|
525
|
+
scope: object;
|
|
526
|
+
facade: QmlFacade;
|
|
527
|
+
_activated = false;
|
|
528
|
+
|
|
529
|
+
constructor(
|
|
530
|
+
typeInfo: QmlTypeInfo,
|
|
531
|
+
doc: QmlDocument,
|
|
532
|
+
context: Context,
|
|
533
|
+
parentInst: QmlInstance | null,
|
|
534
|
+
) {
|
|
535
|
+
this.uid = nextInstanceId++;
|
|
536
|
+
this.typeInfo = typeInfo;
|
|
537
|
+
this.doc = doc;
|
|
538
|
+
this.context = context;
|
|
539
|
+
this.parentInst = parentInst;
|
|
540
|
+
this.scope = makeScopeProxy(this, context, doc);
|
|
541
|
+
this.facade = makeFacade(this);
|
|
542
|
+
for (const [name, decl] of Object.entries(typeInfo.properties)) {
|
|
543
|
+
this.addSlot(name, decl.default);
|
|
544
|
+
}
|
|
545
|
+
for (const [name, params] of Object.entries(typeInfo.signals)) {
|
|
546
|
+
this.signalParams.set(name, params);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
addSlot(name: string, value: unknown): Slot {
|
|
551
|
+
const slot = new Slot(this, name, value);
|
|
552
|
+
this.slots.set(name, slot);
|
|
553
|
+
return slot;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
slot(name: string): Slot {
|
|
557
|
+
const s = this.slots.get(name);
|
|
558
|
+
if (!s)
|
|
559
|
+
throw new Error(
|
|
560
|
+
`QML: ${this.typeInfo.name} has no property '${name}' (${this.doc.fileName})`,
|
|
561
|
+
);
|
|
562
|
+
return s;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** undefined ⇒ not a member (so scope lookup continues up the chain). */
|
|
566
|
+
resolveMember(name: string): unknown {
|
|
567
|
+
if (this.slots.has(name)) return this.slots.get(name);
|
|
568
|
+
if (this.methods.has(name)) return this.methods.get(name);
|
|
569
|
+
if (this.signalParams.has(name)) return name;
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
getMember(name: string): unknown {
|
|
574
|
+
const slot = this.slots.get(name);
|
|
575
|
+
if (slot) {
|
|
576
|
+
const v = slot.get();
|
|
577
|
+
return v === undefined ? null : v; // undefined would break the walk
|
|
578
|
+
}
|
|
579
|
+
if (this.methods.has(name)) return this.methods.get(name);
|
|
580
|
+
if (this.signalParams.has(name)) {
|
|
581
|
+
return (...args: unknown[]) => this.emit(name, ...args);
|
|
582
|
+
}
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
setMember(name: string, value: unknown): void {
|
|
587
|
+
if (value && typeof value === 'object' && QtBindingMark in value) {
|
|
588
|
+
const fn = (value as Record<symbol, () => unknown>)[QtBindingMark];
|
|
589
|
+
this.slot(name).setBinding(() => fn.call(this.facade));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
this.slot(name).set(value);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
emit(name: string, ...args: unknown[]): void {
|
|
596
|
+
const subs = this.signalSubs.get(name);
|
|
597
|
+
if (subs) for (const fn of [...subs]) fn(...args);
|
|
598
|
+
flushBindings();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
onSignal(name: string, fn: (...args: unknown[]) => void): () => void {
|
|
602
|
+
if (!this.signalParams.has(name) && !this.slots.has(stripChanged(name)))
|
|
603
|
+
throw new Error(`QML: ${this.typeInfo.name} has no signal '${name}'`);
|
|
604
|
+
let set = this.signalSubs.get(name);
|
|
605
|
+
if (!set) this.signalSubs.set(name, (set = new Set()));
|
|
606
|
+
set.add(fn);
|
|
607
|
+
return () => set.delete(fn);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
anchorLine(edge: string): AnchorLine {
|
|
611
|
+
let line = this.anchorLines.get(edge);
|
|
612
|
+
if (!line) this.anchorLines.set(edge, (line = { inst: this, edge }));
|
|
613
|
+
return line;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** React bridge: any slot change bumps the version; one subscription
|
|
617
|
+
* per object. */
|
|
618
|
+
_changed(): void {
|
|
619
|
+
this.version++;
|
|
620
|
+
for (const l of [...this.listeners]) l();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
_structChanged(): void {
|
|
624
|
+
this.structVersion++;
|
|
625
|
+
this.typeInfo.onStructure?.(this); // positioners re-pin children
|
|
626
|
+
this._changed();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
subscribe(fn: () => void): () => void {
|
|
630
|
+
this.listeners.add(fn);
|
|
631
|
+
return () => this.listeners.delete(fn);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
visualChildren(): QmlInstance[] {
|
|
635
|
+
return this.children.filter((c) => !c.typeInfo.nonVisual);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
destroy(): void {
|
|
639
|
+
if (this.destroyed) return;
|
|
640
|
+
this.destroyed = true;
|
|
641
|
+
for (const fn of this.onDestruction) fn();
|
|
642
|
+
for (const child of this.children) child.destroy();
|
|
643
|
+
for (const src of this.valueSources) src.destroy();
|
|
644
|
+
this.typeInfo.dispose?.(this);
|
|
645
|
+
for (const slot of this.slots.values()) slot.destroy();
|
|
646
|
+
this.listeners.clear();
|
|
647
|
+
this.signalSubs.clear();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const stripChanged = (n: string): string =>
|
|
652
|
+
/^on[A-Z].*Changed$/.test(n)
|
|
653
|
+
? n.slice(2, -7)[0].toLowerCase() + n.slice(3, -7)
|
|
654
|
+
: '';
|
|
655
|
+
|
|
656
|
+
// --- facade ----------------------------------------------------------------
|
|
657
|
+
|
|
658
|
+
const FACADE = Symbol('qml-instance');
|
|
659
|
+
|
|
660
|
+
/** The instance behind a facade, or null for anything else. */
|
|
661
|
+
export const instanceOf = (facade: unknown): QmlInstance | null =>
|
|
662
|
+
facade && typeof facade === 'object'
|
|
663
|
+
? (((facade as Record<symbol, unknown>)[FACADE] as QmlInstance | null) ??
|
|
664
|
+
null)
|
|
665
|
+
: null;
|
|
666
|
+
|
|
667
|
+
const ANCHOR_EDGES = new Set([
|
|
668
|
+
'left',
|
|
669
|
+
'right',
|
|
670
|
+
'top',
|
|
671
|
+
'bottom',
|
|
672
|
+
'horizontalCenter',
|
|
673
|
+
'verticalCenter',
|
|
674
|
+
]);
|
|
675
|
+
|
|
676
|
+
function makeFacade(inst: QmlInstance): QmlFacade {
|
|
677
|
+
return new Proxy(Object.create(null) as QmlFacade, {
|
|
678
|
+
has: (_, name) =>
|
|
679
|
+
typeof name === 'string' &&
|
|
680
|
+
(name === 'parent' ||
|
|
681
|
+
ANCHOR_EDGES.has(name) ||
|
|
682
|
+
inst.resolveMember(name) !== undefined),
|
|
683
|
+
get: (_, name) => {
|
|
684
|
+
if (name === FACADE) return inst;
|
|
685
|
+
if (typeof name !== 'string') return undefined;
|
|
686
|
+
if (name === 'parent') return inst.parentInst?.facade ?? null;
|
|
687
|
+
if (ANCHOR_EDGES.has(name) && !inst.slots.has(name))
|
|
688
|
+
return inst.anchorLine(name);
|
|
689
|
+
return inst.getMember(name);
|
|
690
|
+
},
|
|
691
|
+
set: (_, name, value) => {
|
|
692
|
+
if (typeof name !== 'string') return false;
|
|
693
|
+
inst.setMember(name, value);
|
|
694
|
+
return true;
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// --- instantiation ---------------------------------------------------------
|
|
700
|
+
|
|
701
|
+
let activating = false;
|
|
702
|
+
|
|
703
|
+
export interface InstantiateResult {
|
|
704
|
+
root: QmlInstance;
|
|
705
|
+
context: Context;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export function instantiateDocument(
|
|
709
|
+
doc: QmlDocument,
|
|
710
|
+
{
|
|
711
|
+
context = null,
|
|
712
|
+
extras = null,
|
|
713
|
+
resolver = null,
|
|
714
|
+
}: {
|
|
715
|
+
context?: Context | null;
|
|
716
|
+
extras?: Record<string, unknown> | null;
|
|
717
|
+
/** Enables `.qml`-file resolution: the implicit same-directory import
|
|
718
|
+
* and quoted-path imports (resolver.ts). */
|
|
719
|
+
resolver?: QmlResolver | null;
|
|
720
|
+
} = {},
|
|
721
|
+
): InstantiateResult {
|
|
722
|
+
if (resolver) {
|
|
723
|
+
doc.load = { resolver, dir: resolver.rootDir };
|
|
724
|
+
loadGeneration++; // revalidate cached component sources on each mount
|
|
725
|
+
}
|
|
726
|
+
const ctx = new Context(context, doc);
|
|
727
|
+
if (extras) {
|
|
728
|
+
for (const [k, v] of Object.entries(extras)) {
|
|
729
|
+
ctx.extras.set(k, new Slot(null, k, v));
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const completedQueue: Array<() => void> = [];
|
|
733
|
+
const root = createTree(doc.root, doc, ctx, null);
|
|
734
|
+
ctx.root = root;
|
|
735
|
+
const wasActivating = activating;
|
|
736
|
+
activating = true;
|
|
737
|
+
try {
|
|
738
|
+
activate(root, completedQueue);
|
|
739
|
+
flushBindings();
|
|
740
|
+
} finally {
|
|
741
|
+
activating = wasActivating;
|
|
742
|
+
}
|
|
743
|
+
retryFailedBindings(root);
|
|
744
|
+
for (const fn of completedQueue) fn(); // leaf-first: queued on the way up
|
|
745
|
+
flushBindings();
|
|
746
|
+
return { root, context: ctx };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* A delegate template instantiated into a child context (Repeater, Loader,
|
|
751
|
+
* ListView). `extras` become context properties (index, modelData, roles);
|
|
752
|
+
* ids inside the template scope to the child context while names from the
|
|
753
|
+
* enclosing document keep resolving through the chain.
|
|
754
|
+
*/
|
|
755
|
+
export function instantiateTemplate(
|
|
756
|
+
tpl: TemplateRef,
|
|
757
|
+
parentInst: QmlInstance | null,
|
|
758
|
+
extras: Record<string, unknown> | null = null,
|
|
759
|
+
): { inst: QmlInstance; context: Context } {
|
|
760
|
+
const ctx = new Context(tpl.context, tpl.doc);
|
|
761
|
+
ctx.root = tpl.context.root;
|
|
762
|
+
if (extras) {
|
|
763
|
+
for (const [k, v] of Object.entries(extras)) {
|
|
764
|
+
ctx.extras.set(k, new Slot(null, k, v));
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const completedQueue: Array<() => void> = [];
|
|
768
|
+
const inst = createTree(tpl.object, tpl.doc, ctx, parentInst);
|
|
769
|
+
const wasActivating = activating;
|
|
770
|
+
activating = true;
|
|
771
|
+
try {
|
|
772
|
+
activate(inst, completedQueue);
|
|
773
|
+
flushBindings();
|
|
774
|
+
} finally {
|
|
775
|
+
activating = wasActivating;
|
|
776
|
+
}
|
|
777
|
+
retryFailedBindings(inst);
|
|
778
|
+
for (const fn of completedQueue) fn();
|
|
779
|
+
return { inst, context: ctx };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Instantiate a whole component document (Loader's `source` path). Its
|
|
784
|
+
* names resolve within its own file; only the top-level context — where
|
|
785
|
+
* `QmlView`'s context properties live — is shared through.
|
|
786
|
+
*/
|
|
787
|
+
export function instantiateComponentDoc(
|
|
788
|
+
compDoc: QmlDocument,
|
|
789
|
+
nearContext: Context,
|
|
790
|
+
parentInst: QmlInstance | null,
|
|
791
|
+
): { inst: QmlInstance; context: Context } {
|
|
792
|
+
const ctx = new Context(topContextOf(nearContext), compDoc);
|
|
793
|
+
const completedQueue: Array<() => void> = [];
|
|
794
|
+
const inst = createTree(compDoc.root, compDoc, ctx, parentInst);
|
|
795
|
+
ctx.root = inst;
|
|
796
|
+
const wasActivating = activating;
|
|
797
|
+
activating = true;
|
|
798
|
+
try {
|
|
799
|
+
activate(inst, completedQueue);
|
|
800
|
+
flushBindings();
|
|
801
|
+
} finally {
|
|
802
|
+
activating = wasActivating;
|
|
803
|
+
}
|
|
804
|
+
retryFailedBindings(inst);
|
|
805
|
+
for (const fn of completedQueue) fn();
|
|
806
|
+
return { inst, context: ctx };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Hot reload: carry interactive state (slots assigned at runtime —
|
|
811
|
+
* toggles, counters, typed text) from the old tree onto the new one,
|
|
812
|
+
* matched by id + property name. Everything else re-derives from the
|
|
813
|
+
* edited source.
|
|
814
|
+
*/
|
|
815
|
+
export function migrateUserState(
|
|
816
|
+
oldContext: Context,
|
|
817
|
+
newContext: Context,
|
|
818
|
+
): number {
|
|
819
|
+
let carried = 0;
|
|
820
|
+
for (const [id, oldInst] of oldContext.ids) {
|
|
821
|
+
const newInst = newContext.ids.get(id);
|
|
822
|
+
if (!newInst) continue;
|
|
823
|
+
for (const [name, oldSlot] of oldInst.slots) {
|
|
824
|
+
if (!oldSlot.userSet) continue;
|
|
825
|
+
const newSlot = newInst.slots.get(name);
|
|
826
|
+
if (!newSlot) continue;
|
|
827
|
+
newSlot.set(oldSlot.peek());
|
|
828
|
+
carried++;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
flushBindings();
|
|
832
|
+
return carried;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** A binding still failing after the graph settled deserves its warning —
|
|
836
|
+
* and a broken dependency read may have kept it from subscribing to
|
|
837
|
+
* anything that would re-run it, so run it once more by hand. */
|
|
838
|
+
function retryFailedBindings(inst: QmlInstance): void {
|
|
839
|
+
for (const child of inst.children) retryFailedBindings(child);
|
|
840
|
+
for (const slot of inst.slots.values()) {
|
|
841
|
+
if (slot.hasBinding() && slot.lastError) slot._reevaluate();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function createTree(
|
|
846
|
+
objIr: ObjectIR,
|
|
847
|
+
doc: QmlDocument,
|
|
848
|
+
ctx: Context,
|
|
849
|
+
parentInst: QmlInstance | null,
|
|
850
|
+
): QmlInstance {
|
|
851
|
+
const resolved = resolveTypeRef(doc, objIr.type);
|
|
852
|
+
if ('fileDoc' in resolved) {
|
|
853
|
+
// A file-backed component: instantiate its document's root in its own
|
|
854
|
+
// context (ids and root-props private to the file; only the top-level
|
|
855
|
+
// context — QmlView's context properties — is shared), then lay the
|
|
856
|
+
// use-site members over it. Site bindings evaluate in the *site's*
|
|
857
|
+
// scope during activation; instantiator bindings win, as in Qt.
|
|
858
|
+
const compDoc = resolved.fileDoc;
|
|
859
|
+
if (instantiating.includes(compDoc)) {
|
|
860
|
+
const chain = [...instantiating.map((d) => d.fileName), compDoc.fileName];
|
|
861
|
+
throw new Error(
|
|
862
|
+
`QML: circular component reference: ${chain.join(' -> ')}`,
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
instantiating.push(compDoc);
|
|
866
|
+
let inst: QmlInstance;
|
|
867
|
+
try {
|
|
868
|
+
const compCtx = new Context(topContextOf(ctx), compDoc);
|
|
869
|
+
inst = createTree(compDoc.root, compDoc, compCtx, parentInst);
|
|
870
|
+
compCtx.root = inst;
|
|
871
|
+
} finally {
|
|
872
|
+
instantiating.pop();
|
|
873
|
+
}
|
|
874
|
+
if (objIr.id) {
|
|
875
|
+
inst.id = objIr.id;
|
|
876
|
+
ctx.ids.set(objIr.id, inst);
|
|
877
|
+
}
|
|
878
|
+
inst.site = { ir: objIr, doc, ctx };
|
|
879
|
+
collectStructural(inst, objIr, doc, ctx);
|
|
880
|
+
return inst;
|
|
881
|
+
}
|
|
882
|
+
const typeInfo = resolved;
|
|
883
|
+
const inst = new QmlInstance(typeInfo, doc, ctx, parentInst);
|
|
884
|
+
inst.ir = objIr;
|
|
885
|
+
// Qt's default: an unset width tracks implicitWidth (`widthValid`). Any
|
|
886
|
+
// later binding or assignment replaces this — binding-breaking gives the
|
|
887
|
+
// exact semantics with no bookkeeping.
|
|
888
|
+
if (inst.slots.has('width') && inst.slots.has('implicitWidth')) {
|
|
889
|
+
inst
|
|
890
|
+
.slot('width')
|
|
891
|
+
.setBinding(() => inst.slot('implicitWidth').get(), { isDefault: true });
|
|
892
|
+
inst
|
|
893
|
+
.slot('height')
|
|
894
|
+
.setBinding(() => inst.slot('implicitHeight').get(), { isDefault: true });
|
|
895
|
+
}
|
|
896
|
+
if (objIr.id) {
|
|
897
|
+
inst.id = objIr.id;
|
|
898
|
+
ctx.ids.set(objIr.id, inst);
|
|
899
|
+
}
|
|
900
|
+
collectStructural(inst, objIr, doc, ctx);
|
|
901
|
+
return inst;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** The structural half of a member list: declarations, children, captured
|
|
905
|
+
* templates. Runs once for a plain instance; a file-backed instance runs
|
|
906
|
+
* it twice — the component document's pass happened inside its own
|
|
907
|
+
* createTree, and the use site's runs here with the site's doc and
|
|
908
|
+
* context, so site children and site delegates resolve site names. */
|
|
909
|
+
function collectStructural(
|
|
910
|
+
inst: QmlInstance,
|
|
911
|
+
objIr: ObjectIR,
|
|
912
|
+
doc: QmlDocument,
|
|
913
|
+
ctx: Context,
|
|
914
|
+
): void {
|
|
915
|
+
const capture = inst.typeInfo.capture;
|
|
916
|
+
for (const m of objIr.members) {
|
|
917
|
+
if (m.kind === 'property') {
|
|
918
|
+
if (m.propType !== 'alias' && !inst.slots.has(m.name)) {
|
|
919
|
+
inst.addSlot(m.name, defaultForPropType(m.propType));
|
|
920
|
+
}
|
|
921
|
+
} else if (m.kind === 'signal') {
|
|
922
|
+
inst.signalParams.set(
|
|
923
|
+
m.name,
|
|
924
|
+
m.params.map((p) => p.name),
|
|
925
|
+
);
|
|
926
|
+
} else if (m.kind === 'object') {
|
|
927
|
+
if (capture) {
|
|
928
|
+
if (!inst.templates.has(capture))
|
|
929
|
+
inst.templates.set(capture, {
|
|
930
|
+
object: m.object,
|
|
931
|
+
doc,
|
|
932
|
+
context: ctx,
|
|
933
|
+
});
|
|
934
|
+
} else {
|
|
935
|
+
inst.children.push(createTree(m.object, doc, ctx, inst));
|
|
936
|
+
}
|
|
937
|
+
} else if (m.kind === 'binding' && m.value.kind === 'object') {
|
|
938
|
+
const name = m.path.join('.');
|
|
939
|
+
if (name === capture || m.value.object.type === 'Component') {
|
|
940
|
+
// `delegate: Text { … }`, `sourceComponent: Component { … }` —
|
|
941
|
+
// component-typed properties stay templates, instantiated on demand.
|
|
942
|
+
inst.templates.set(name, {
|
|
943
|
+
object: unwrapComponent(m.value.object),
|
|
944
|
+
doc,
|
|
945
|
+
context: ctx,
|
|
946
|
+
});
|
|
947
|
+
} else {
|
|
948
|
+
// `model: ListModel { … }` — an object rvalue instantiates, and the
|
|
949
|
+
// property is assigned the reference (QML's rule).
|
|
950
|
+
const child = createTree(m.value.object, doc, ctx, inst);
|
|
951
|
+
inst.children.push(child);
|
|
952
|
+
(
|
|
953
|
+
(inst.state.objectAssigns ??= []) as Array<[string, QmlInstance]>
|
|
954
|
+
).push([name, child]);
|
|
955
|
+
}
|
|
956
|
+
} else if (m.kind === 'binding' && m.value.kind === 'array') {
|
|
957
|
+
// `states: [State { … }]` — templates too, under the property name.
|
|
958
|
+
const objects = m.value.items
|
|
959
|
+
.filter(
|
|
960
|
+
(it): it is Extract<ValueIR, { kind: 'object' }> =>
|
|
961
|
+
it.kind === 'object',
|
|
962
|
+
)
|
|
963
|
+
.map((it) => it.object);
|
|
964
|
+
if (objects.length === m.value.items.length && objects.length > 0) {
|
|
965
|
+
inst.state[`templates:${m.path.join('.')}`] = objects.map(
|
|
966
|
+
(object): TemplateRef => ({ object, doc, context: ctx }),
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
} else if (m.kind === 'inline-component') {
|
|
970
|
+
throw new Error(
|
|
971
|
+
`QML: inline components (component ${m.name}: …) are not there ` +
|
|
972
|
+
`yet; put the component in its own <Name>.qml next to this ` +
|
|
973
|
+
`document (resolved via the resolver seam), or define it in ` +
|
|
974
|
+
`JavaScript with registerQmlModule.`,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function defaultForPropType(propType: string): unknown {
|
|
981
|
+
if (propType === 'int' || propType === 'real' || propType === 'double')
|
|
982
|
+
return 0;
|
|
983
|
+
if (propType === 'bool') return false;
|
|
984
|
+
if (propType === 'string') return '';
|
|
985
|
+
if (propType === 'list') return [];
|
|
986
|
+
return undefined;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const unwrapComponent = (obj: ObjectIR): ObjectIR => {
|
|
990
|
+
if (obj.type !== 'Component') return obj;
|
|
991
|
+
const child = obj.members.find(
|
|
992
|
+
(m): m is Extract<MemberIR, { kind: 'object' }> => m.kind === 'object',
|
|
993
|
+
);
|
|
994
|
+
if (!child) throw new Error('QML: Component {} needs exactly one child');
|
|
995
|
+
return child.object;
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
const ownEnv = (inst: QmlInstance): EvalEnv => ({
|
|
999
|
+
doc: inst.doc,
|
|
1000
|
+
ctx: inst.context,
|
|
1001
|
+
scope: inst.scope,
|
|
1002
|
+
facade: inst.facade,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
function activate(inst: QmlInstance, completedQueue: Array<() => void>): void {
|
|
1006
|
+
if (inst._activated) return; // Repeater splices siblings mid-walk
|
|
1007
|
+
inst._activated = true;
|
|
1008
|
+
const objIr = inst.ir;
|
|
1009
|
+
if (!objIr) return;
|
|
1010
|
+
for (const child of [...inst.children]) activate(child, completedQueue);
|
|
1011
|
+
|
|
1012
|
+
applyMembers(inst, objIr.members, ownEnv(inst), completedQueue);
|
|
1013
|
+
if (inst.site) {
|
|
1014
|
+
const site = inst.site;
|
|
1015
|
+
applyMembers(
|
|
1016
|
+
inst,
|
|
1017
|
+
site.ir.members,
|
|
1018
|
+
{
|
|
1019
|
+
doc: site.doc,
|
|
1020
|
+
ctx: site.ctx,
|
|
1021
|
+
scope: makeScopeProxy(inst, site.ctx, site.doc),
|
|
1022
|
+
facade: inst.facade,
|
|
1023
|
+
},
|
|
1024
|
+
completedQueue,
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Object rvalues (`model: ListModel { … }`): the instance exists since
|
|
1029
|
+
// createTree; hand the property its reference before the type's init
|
|
1030
|
+
// reads it.
|
|
1031
|
+
const objectAssigns = inst.state.objectAssigns as
|
|
1032
|
+
Array<[string, QmlInstance]> | undefined;
|
|
1033
|
+
if (objectAssigns) {
|
|
1034
|
+
for (const [name, child] of objectAssigns) {
|
|
1035
|
+
const slot =
|
|
1036
|
+
inst.slots.get(name) ??
|
|
1037
|
+
(inst.typeInfo.dynamicProperties
|
|
1038
|
+
? inst.addSlot(name, undefined)
|
|
1039
|
+
: inst.slot(name));
|
|
1040
|
+
slot.assign(child.facade);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
installAnchors(inst);
|
|
1045
|
+
inst.typeInfo.init?.(inst);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function applyMembers(
|
|
1049
|
+
inst: QmlInstance,
|
|
1050
|
+
members: MemberIR[],
|
|
1051
|
+
env: EvalEnv,
|
|
1052
|
+
completedQueue: Array<() => void>,
|
|
1053
|
+
): void {
|
|
1054
|
+
const applyBinding = (path: string[], value: ValueIR): void => {
|
|
1055
|
+
if (path.length === 1 && /^on[A-Z]/.test(path[0])) {
|
|
1056
|
+
if (inst.typeInfo.deferHandlers) {
|
|
1057
|
+
inst.deferredHandlers.push({ name: path[0], value });
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
attachHandler(inst, path[0], value, env);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (/^[A-Z]/.test(path[0]) && path.length > 1) {
|
|
1064
|
+
attachAttached(inst, path, value, env, completedQueue);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
const name = path.join('.'); // grouped props live as dotted slot names
|
|
1068
|
+
if (value.kind === 'object') return; // captured as a template already
|
|
1069
|
+
if (value.kind === 'array') return; // captured as templates already
|
|
1070
|
+
let slot = inst.slots.get(name);
|
|
1071
|
+
if (!slot) {
|
|
1072
|
+
if (inst.typeInfo.dynamicProperties) slot = inst.addSlot(name, undefined);
|
|
1073
|
+
else slot = inst.slot(name); // throws with the readable message
|
|
1074
|
+
}
|
|
1075
|
+
if (value.kind === 'block') {
|
|
1076
|
+
// `width: { if (…) return a; return b }` — a script binding; QML
|
|
1077
|
+
// re-evaluates these like any binding when dependencies change.
|
|
1078
|
+
const h = compileHandler(value, [], env.doc)(env.scope);
|
|
1079
|
+
installGuardedBinding(inst, slot, value, env.doc, name, () =>
|
|
1080
|
+
h.call(env.facade),
|
|
1081
|
+
);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (isLiteral(value.src)) {
|
|
1085
|
+
slot.assign(evalLiteral(value.src));
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const fn = compileExpr(value, env.doc);
|
|
1089
|
+
installGuardedBinding(inst, slot, value, env.doc, name, () =>
|
|
1090
|
+
fn.call(env.facade, env.scope),
|
|
1091
|
+
);
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
for (const m of members) {
|
|
1095
|
+
if (m.kind === 'property' && m.value) {
|
|
1096
|
+
if (m.propType === 'alias') {
|
|
1097
|
+
installAlias(inst, m.name, m.value as AliasTargetIR, env);
|
|
1098
|
+
} else {
|
|
1099
|
+
applyBinding([m.name], m.value as ValueIR);
|
|
1100
|
+
}
|
|
1101
|
+
} else if (m.kind === 'binding') {
|
|
1102
|
+
applyBinding(m.path, m.value);
|
|
1103
|
+
} else if (m.kind === 'group') {
|
|
1104
|
+
for (const b of m.bindings) applyBinding(b.path, b.value);
|
|
1105
|
+
} else if (m.kind === 'function') {
|
|
1106
|
+
const factory = compileHandler(
|
|
1107
|
+
{ kind: 'block', src: m.body },
|
|
1108
|
+
m.params,
|
|
1109
|
+
env.doc,
|
|
1110
|
+
);
|
|
1111
|
+
const f = factory(env.scope);
|
|
1112
|
+
inst.methods.set(m.name, (...args: unknown[]) =>
|
|
1113
|
+
f.call(env.facade, ...args),
|
|
1114
|
+
);
|
|
1115
|
+
} else if (m.kind === 'value-source') {
|
|
1116
|
+
installValueSource(inst, m, env);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function installGuardedBinding(
|
|
1122
|
+
inst: QmlInstance,
|
|
1123
|
+
slot: Slot,
|
|
1124
|
+
value: ValueIR,
|
|
1125
|
+
doc: QmlDocument,
|
|
1126
|
+
name: string,
|
|
1127
|
+
evalFn: () => unknown,
|
|
1128
|
+
): void {
|
|
1129
|
+
slot.setBinding(() => {
|
|
1130
|
+
try {
|
|
1131
|
+
const v = evalFn();
|
|
1132
|
+
slot.lastError = null;
|
|
1133
|
+
return v;
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
slot.lastError = e;
|
|
1136
|
+
// First-pass evaluation order is unspecified (a child's binding can
|
|
1137
|
+
// read a root property whose own binding has not run yet) — exactly
|
|
1138
|
+
// as in Qt. Stay quiet during activation; instantiateDocument
|
|
1139
|
+
// re-evaluates whatever is still broken once the graph settles, and
|
|
1140
|
+
// *that* failure warns.
|
|
1141
|
+
if (!activating) reportBindingError(doc, value, name, e);
|
|
1142
|
+
return slot.peek();
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function isLiteral(src: string): boolean {
|
|
1148
|
+
return /^(-?\d+(\.\d+)?|true|false|null|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(
|
|
1149
|
+
src.trim(),
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function evalLiteral(src: string): unknown {
|
|
1154
|
+
const t = src.trim();
|
|
1155
|
+
if (t === 'true') return true;
|
|
1156
|
+
if (t === 'false') return false;
|
|
1157
|
+
if (t === 'null') return null;
|
|
1158
|
+
if (t[0] === '"' || t[0] === "'")
|
|
1159
|
+
return t.slice(1, -1).replace(/\\(.)/g, '$1');
|
|
1160
|
+
return Number(t);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const reportedBindingErrors = new Set<string>();
|
|
1164
|
+
function reportBindingError(
|
|
1165
|
+
doc: QmlDocument,
|
|
1166
|
+
value: ValueIR,
|
|
1167
|
+
name: string,
|
|
1168
|
+
e: unknown,
|
|
1169
|
+
): void {
|
|
1170
|
+
const key = `${doc.fileName}:${name}`;
|
|
1171
|
+
if (reportedBindingErrors.has(key)) return;
|
|
1172
|
+
reportedBindingErrors.add(key);
|
|
1173
|
+
const src = 'src' in value ? value.src : '';
|
|
1174
|
+
warn(
|
|
1175
|
+
`QML: binding for '${name}' in ${doc.fileName} threw: ${message(e)}\n \`${src}\``,
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// `onFoo: expr-or-block` — but also the modern `onFoo: (args) => { … }`,
|
|
1180
|
+
// where the RHS evaluates to the function to invoke with the signal's args.
|
|
1181
|
+
const FN_VALUED = /^\s*(function\b|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/;
|
|
1182
|
+
|
|
1183
|
+
function makeHandlerFn(
|
|
1184
|
+
inst: QmlInstance,
|
|
1185
|
+
value: ValueIR,
|
|
1186
|
+
params: string[],
|
|
1187
|
+
env: EvalEnv,
|
|
1188
|
+
): (...args: unknown[]) => unknown {
|
|
1189
|
+
if (value.kind === 'expr' && FN_VALUED.test(value.src)) {
|
|
1190
|
+
const arrow = compileExpr(value, env.doc).call(env.facade, env.scope) as (
|
|
1191
|
+
...args: unknown[]
|
|
1192
|
+
) => unknown;
|
|
1193
|
+
return (...args) => arrow.apply(env.facade, args);
|
|
1194
|
+
}
|
|
1195
|
+
if (value.kind !== 'expr' && value.kind !== 'block')
|
|
1196
|
+
throw new Error(`QML: a handler cannot be an object (${env.doc.fileName})`);
|
|
1197
|
+
const f = compileHandler(value, params, env.doc)(env.scope);
|
|
1198
|
+
return (...args) => f.call(env.facade, ...args);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/** Compile a stored handler in an instance's scope — how `Connections`
|
|
1202
|
+
* wires its deferred `onX` members to another object's signals. */
|
|
1203
|
+
export function makeHandlerFor(
|
|
1204
|
+
inst: QmlInstance,
|
|
1205
|
+
value: ValueIR,
|
|
1206
|
+
params: string[],
|
|
1207
|
+
): (...args: unknown[]) => unknown {
|
|
1208
|
+
return makeHandlerFn(inst, value, params, ownEnv(inst));
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function attachHandler(
|
|
1212
|
+
inst: QmlInstance,
|
|
1213
|
+
onName: string,
|
|
1214
|
+
value: ValueIR,
|
|
1215
|
+
env: EvalEnv,
|
|
1216
|
+
): void {
|
|
1217
|
+
const base = onName[2].toLowerCase() + onName.slice(3);
|
|
1218
|
+
// Property-change handler?
|
|
1219
|
+
const changed = stripChanged(onName);
|
|
1220
|
+
if (changed && inst.slots.has(changed)) {
|
|
1221
|
+
const f = makeHandlerFn(inst, value, [], env);
|
|
1222
|
+
inst.slots.get(changed)?.watch(() => f());
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const params = inst.signalParams.get(base);
|
|
1226
|
+
if (!params)
|
|
1227
|
+
throw new Error(
|
|
1228
|
+
`QML: '${onName}' in ${env.doc.fileName} matches no signal or property on ${inst.typeInfo.name}`,
|
|
1229
|
+
);
|
|
1230
|
+
inst.onSignal(base, makeHandlerFn(inst, value, params, env));
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function attachAttached(
|
|
1234
|
+
inst: QmlInstance,
|
|
1235
|
+
path: string[],
|
|
1236
|
+
value: ValueIR,
|
|
1237
|
+
env: EvalEnv,
|
|
1238
|
+
completedQueue: Array<() => void>,
|
|
1239
|
+
): void {
|
|
1240
|
+
const ns = path[0];
|
|
1241
|
+
const name = path.slice(1).join('.');
|
|
1242
|
+
if (ns === 'Component' && name === 'onCompleted') {
|
|
1243
|
+
const f = makeHandlerFn(inst, value, [], env);
|
|
1244
|
+
completedQueue.push(() => f());
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (ns === 'Component' && name === 'onDestruction') {
|
|
1248
|
+
const f = makeHandlerFn(inst, value, [], env);
|
|
1249
|
+
inst.onDestruction.push(() => f());
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (/^on[A-Z]/.test(name)) {
|
|
1253
|
+
// `Keys.onPressed`, `Keys.onReturnPressed`: stored; the view wires
|
|
1254
|
+
// them to the focus/key system (qtquick.tsx).
|
|
1255
|
+
const params = ['event'];
|
|
1256
|
+
inst.attachedHandlers.set(
|
|
1257
|
+
`${ns}.${name}`,
|
|
1258
|
+
makeHandlerFn(inst, value, params, env),
|
|
1259
|
+
);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
// `Layout.fillWidth` and friends — attached values, namespaced slots.
|
|
1263
|
+
const slotName = `${ns}.${name}`;
|
|
1264
|
+
const slot = inst.slots.get(slotName) ?? inst.addSlot(slotName, undefined);
|
|
1265
|
+
if (value.kind === 'expr' && isLiteral(value.src)) {
|
|
1266
|
+
slot.assign(evalLiteral(value.src));
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (value.kind === 'expr') {
|
|
1270
|
+
const fn = compileExpr(value, env.doc);
|
|
1271
|
+
installGuardedBinding(inst, slot, value, env.doc, slotName, () =>
|
|
1272
|
+
fn.call(env.facade, env.scope),
|
|
1273
|
+
);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (value.kind === 'block') {
|
|
1277
|
+
const h = compileHandler(value, [], env.doc)(env.scope);
|
|
1278
|
+
installGuardedBinding(inst, slot, value, env.doc, slotName, () =>
|
|
1279
|
+
h.call(env.facade),
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function installAlias(
|
|
1285
|
+
inst: QmlInstance,
|
|
1286
|
+
name: string,
|
|
1287
|
+
target: AliasTargetIR,
|
|
1288
|
+
env: EvalEnv,
|
|
1289
|
+
): void {
|
|
1290
|
+
const [idName, ...rest] = target.path;
|
|
1291
|
+
const targetInst = env.ctx.lookupId(idName);
|
|
1292
|
+
if (!targetInst)
|
|
1293
|
+
throw new Error(`QML: alias '${name}' targets unknown id '${idName}'`);
|
|
1294
|
+
if (!rest.length)
|
|
1295
|
+
throw new Error(`QML: alias '${name}' must target id.property`);
|
|
1296
|
+
const slot = targetInst.slot(rest.join('.'));
|
|
1297
|
+
inst.slots.set(name, slot); // the same Slot: reads and writes forward
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function installValueSource(
|
|
1301
|
+
inst: QmlInstance,
|
|
1302
|
+
m: Extract<MemberIR, { kind: 'value-source' }>,
|
|
1303
|
+
env: EvalEnv,
|
|
1304
|
+
): void {
|
|
1305
|
+
const srcType = lookupType(env.doc, m.object.type);
|
|
1306
|
+
const propName = m.on.join('.');
|
|
1307
|
+
const slot = inst.slot(propName);
|
|
1308
|
+
const srcInst = createTree(m.object, env.doc, env.ctx, inst);
|
|
1309
|
+
activate(srcInst, []);
|
|
1310
|
+
inst.valueSources.push(srcInst);
|
|
1311
|
+
if (!srcType.init)
|
|
1312
|
+
throw new Error(`QML: ${m.object.type} cannot be a value source here`);
|
|
1313
|
+
srcType.init(srcInst, { targetSlot: slot, target: inst });
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// --- anchors ---------------------------------------------------------------
|
|
1317
|
+
|
|
1318
|
+
const px = (v: unknown): number =>
|
|
1319
|
+
typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
1320
|
+
|
|
1321
|
+
function lineCoord(line: AnchorLine, forInst: QmlInstance): number {
|
|
1322
|
+
const { inst, edge } = line;
|
|
1323
|
+
const horizontal =
|
|
1324
|
+
edge === 'left' || edge === 'right' || edge === 'horizontalCenter';
|
|
1325
|
+
if (inst === forInst.parentInst) {
|
|
1326
|
+
const size = px(inst.slot(horizontal ? 'width' : 'height').get());
|
|
1327
|
+
if (edge === 'left' || edge === 'top') return 0;
|
|
1328
|
+
if (edge === 'right' || edge === 'bottom') return size;
|
|
1329
|
+
return size / 2;
|
|
1330
|
+
}
|
|
1331
|
+
// Sibling: the same coordinate space (both relative to the parent).
|
|
1332
|
+
const pos = px(inst.slot(horizontal ? 'x' : 'y').get());
|
|
1333
|
+
const size = px(inst.slot(horizontal ? 'width' : 'height').get());
|
|
1334
|
+
if (edge === 'left' || edge === 'top') return pos;
|
|
1335
|
+
if (edge === 'right' || edge === 'bottom') return pos + size;
|
|
1336
|
+
return pos + size / 2;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const isAnchorLine = (v: unknown): v is AnchorLine =>
|
|
1340
|
+
!!v &&
|
|
1341
|
+
typeof v === 'object' &&
|
|
1342
|
+
'inst' in v &&
|
|
1343
|
+
'edge' in v &&
|
|
1344
|
+
(v as AnchorLine).inst instanceof QmlInstance;
|
|
1345
|
+
|
|
1346
|
+
const ANCHOR_CONFIG_NAMES = [
|
|
1347
|
+
'fill',
|
|
1348
|
+
'centerIn',
|
|
1349
|
+
'left',
|
|
1350
|
+
'right',
|
|
1351
|
+
'top',
|
|
1352
|
+
'bottom',
|
|
1353
|
+
'horizontalCenter',
|
|
1354
|
+
'verticalCenter',
|
|
1355
|
+
] as const;
|
|
1356
|
+
|
|
1357
|
+
function installAnchors(inst: QmlInstance): void {
|
|
1358
|
+
if (!inst.slots.has('anchors.fill')) return; // non-Item types
|
|
1359
|
+
const a = (n: string): unknown => inst.slots.get(`anchors.${n}`)?.get();
|
|
1360
|
+
const margin = (side: string): number => {
|
|
1361
|
+
const m = a(`${side}Margin`);
|
|
1362
|
+
return px(m !== undefined && m !== null ? m : a('margins'));
|
|
1363
|
+
};
|
|
1364
|
+
const line = (v: unknown): AnchorLine | null => (isAnchorLine(v) ? v : null);
|
|
1365
|
+
|
|
1366
|
+
const xSlot = inst.slot('x');
|
|
1367
|
+
const ySlot = inst.slot('y');
|
|
1368
|
+
const wSlot = inst.slot('width');
|
|
1369
|
+
const hSlot = inst.slot('height');
|
|
1370
|
+
|
|
1371
|
+
const install = (): void => {
|
|
1372
|
+
const fill = instanceOf(a('fill'));
|
|
1373
|
+
const centerIn = instanceOf(a('centerIn'));
|
|
1374
|
+
const L = fill ? fill.anchorLine('left') : line(a('left'));
|
|
1375
|
+
const R = fill ? fill.anchorLine('right') : line(a('right'));
|
|
1376
|
+
const T = fill ? fill.anchorLine('top') : line(a('top'));
|
|
1377
|
+
const B = fill ? fill.anchorLine('bottom') : line(a('bottom'));
|
|
1378
|
+
const HC = centerIn
|
|
1379
|
+
? centerIn.anchorLine('horizontalCenter')
|
|
1380
|
+
: line(a('horizontalCenter'));
|
|
1381
|
+
const VC = centerIn
|
|
1382
|
+
? centerIn.anchorLine('verticalCenter')
|
|
1383
|
+
: line(a('verticalCenter'));
|
|
1384
|
+
|
|
1385
|
+
if (L && R) {
|
|
1386
|
+
wSlot.setBinding(
|
|
1387
|
+
() =>
|
|
1388
|
+
lineCoord(R, inst) -
|
|
1389
|
+
margin('right') -
|
|
1390
|
+
(lineCoord(L, inst) + margin('left')),
|
|
1391
|
+
);
|
|
1392
|
+
xSlot.setBinding(() => lineCoord(L, inst) + margin('left'));
|
|
1393
|
+
} else if (L) {
|
|
1394
|
+
xSlot.setBinding(() => lineCoord(L, inst) + margin('left'));
|
|
1395
|
+
} else if (R) {
|
|
1396
|
+
xSlot.setBinding(
|
|
1397
|
+
() => lineCoord(R, inst) - margin('right') - px(wSlot.get()),
|
|
1398
|
+
);
|
|
1399
|
+
} else if (HC) {
|
|
1400
|
+
xSlot.setBinding(() => lineCoord(HC, inst) - px(wSlot.get()) / 2);
|
|
1401
|
+
}
|
|
1402
|
+
if (T && B) {
|
|
1403
|
+
hSlot.setBinding(
|
|
1404
|
+
() =>
|
|
1405
|
+
lineCoord(B, inst) -
|
|
1406
|
+
margin('bottom') -
|
|
1407
|
+
(lineCoord(T, inst) + margin('top')),
|
|
1408
|
+
);
|
|
1409
|
+
ySlot.setBinding(() => lineCoord(T, inst) + margin('top'));
|
|
1410
|
+
} else if (T) {
|
|
1411
|
+
ySlot.setBinding(() => lineCoord(T, inst) + margin('top'));
|
|
1412
|
+
} else if (B) {
|
|
1413
|
+
ySlot.setBinding(
|
|
1414
|
+
() => lineCoord(B, inst) - margin('bottom') - px(hSlot.get()),
|
|
1415
|
+
);
|
|
1416
|
+
} else if (VC) {
|
|
1417
|
+
ySlot.setBinding(() => lineCoord(VC, inst) - px(hSlot.get()) / 2);
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1421
|
+
const configured = ANCHOR_CONFIG_NAMES.some((n) => {
|
|
1422
|
+
const s = inst.slots.get(`anchors.${n}`);
|
|
1423
|
+
return s && (s.hasBinding() || s.peek() !== undefined);
|
|
1424
|
+
});
|
|
1425
|
+
// Re-anchoring at runtime re-installs the geometry bindings.
|
|
1426
|
+
for (const n of ANCHOR_CONFIG_NAMES)
|
|
1427
|
+
inst.slots.get(`anchors.${n}`)?.watch(install);
|
|
1428
|
+
if (configured) install();
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
export type { BindingFn };
|