@react-x11/components 0.3.0 → 0.4.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 +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- 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/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 +143 -0
- package/dist/tabs/index.d.ts.map +1 -0
- package/dist/tabs/index.js +602 -0
- package/dist/tabs/index.js.map +1 -0
- 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/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/package.json +11 -1
- package/src/index.ts +44 -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/tabs/hx.ts +31 -0
- package/src/tabs/index.ts +850 -0
- package/src/terminal/vt/index.ts +4 -2
- package/src/terminal/vt/pty.ts +243 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// The React bridge: one small component per live QML object.
|
|
2
|
+
//
|
|
3
|
+
// Each QmlNode subscribes to exactly one instance (useSyncExternalStore on
|
|
4
|
+
// the instance's version counter), so a scalar property change re-renders
|
|
5
|
+
// one element's worth of tree and React reconciliation is reserved for
|
|
6
|
+
// structural change (Repeater rebuilds). The QML side never reaches around
|
|
7
|
+
// React: every pixel on screen is a `<box>`/`<text>`/`<image>` committed
|
|
8
|
+
// through the ordinary renderer.
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
forwardRef,
|
|
12
|
+
useEffect,
|
|
13
|
+
useImperativeHandle,
|
|
14
|
+
useRef,
|
|
15
|
+
useSyncExternalStore,
|
|
16
|
+
} from 'react';
|
|
17
|
+
import type { ComponentType, ReactElement, ReactNode, Ref } from 'react';
|
|
18
|
+
import type { DrawnNode } from 'react-x11';
|
|
19
|
+
import type { Style } from 'react-x11/style';
|
|
20
|
+
import { warn } from './globals.js';
|
|
21
|
+
|
|
22
|
+
import { parseQml } from './parse.js';
|
|
23
|
+
import {
|
|
24
|
+
Context,
|
|
25
|
+
QmlInstance,
|
|
26
|
+
instantiateDocument,
|
|
27
|
+
migrateUserState,
|
|
28
|
+
registerQmlModule,
|
|
29
|
+
type InstantiateResult,
|
|
30
|
+
type QmlFacade,
|
|
31
|
+
type QmlPropertyDef,
|
|
32
|
+
} from './objects.js';
|
|
33
|
+
import { flushBindings } from './slots.js';
|
|
34
|
+
import { afterLayout, cancelAfterLayout } from '../internal/timers.js';
|
|
35
|
+
import type { QmlResolver } from './resolver.js';
|
|
36
|
+
|
|
37
|
+
export function useQmlVersion(inst: QmlInstance): number {
|
|
38
|
+
return useSyncExternalStore(
|
|
39
|
+
(cb) => inst.subscribe(cb),
|
|
40
|
+
() => inst.version,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function QmlNode({ inst }: { inst: QmlInstance }): ReactNode {
|
|
45
|
+
useQmlVersion(inst);
|
|
46
|
+
if (inst.destroyed) return null;
|
|
47
|
+
const View = inst.typeInfo.view;
|
|
48
|
+
if (!View) return null;
|
|
49
|
+
return <View inst={inst} />;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function renderQmlChildren(inst: QmlInstance): ReactNode[] {
|
|
53
|
+
return inst.visualChildren().map((c) => <QmlNode key={c.uid} inst={c} />);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface QmlViewHandle {
|
|
57
|
+
/** The root object, as expressions see it: live properties, callable
|
|
58
|
+
* signals and methods. */
|
|
59
|
+
root: QmlFacade;
|
|
60
|
+
/** An object by its document id, or null. */
|
|
61
|
+
id(name: string): QmlFacade | null;
|
|
62
|
+
instance: QmlInstance;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface QmlViewProps {
|
|
66
|
+
/** QML source text. When it changes the tree is rebuilt; interactive
|
|
67
|
+
* state (assigned property values) is carried across by id — the
|
|
68
|
+
* hot-reload story. Pass `hotReload={false}` for a cold swap. */
|
|
69
|
+
source: string;
|
|
70
|
+
/** Shown in errors; pass the file path when the source came from one. */
|
|
71
|
+
file?: string;
|
|
72
|
+
/** Context properties — names visible to every binding in the document.
|
|
73
|
+
* Updated values ride ordinary commits. */
|
|
74
|
+
context?: Record<string, unknown>;
|
|
75
|
+
/** Subscriptions to the root object's signals, by signal name. */
|
|
76
|
+
onSignal?: Record<string, (...args: unknown[]) => void>;
|
|
77
|
+
hotReload?: boolean;
|
|
78
|
+
/** Enables `.qml`-file resolution — the implicit same-directory import
|
|
79
|
+
* and quoted-path imports. `createFileResolver(dir)` is the standard
|
|
80
|
+
* filesystem one; any object with the `QmlResolver` shape works
|
|
81
|
+
* (resolver.ts). Fixed for the lifetime of the view. */
|
|
82
|
+
resolver?: QmlResolver;
|
|
83
|
+
/** Rebuild (with the same state migration a `source` change gets) when
|
|
84
|
+
* this value changes — how a watcher of *sibling* `.qml` files triggers
|
|
85
|
+
* a hot reload the unchanged root source cannot. */
|
|
86
|
+
reloadToken?: unknown;
|
|
87
|
+
style?: Style | Style[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface Mounted {
|
|
91
|
+
source: string;
|
|
92
|
+
token: unknown;
|
|
93
|
+
res: InstantiateResult;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A QML document as a react-x11 element tree.
|
|
98
|
+
*
|
|
99
|
+
* The wrapper box takes the root object's declared size when it has one;
|
|
100
|
+
* otherwise it is sized by the app (the `style` prop) and feeds its
|
|
101
|
+
* laid-out size back into the root's implicit size, so `width`/`height`
|
|
102
|
+
* bindings inside the document see the real number — the "root fills the
|
|
103
|
+
* window" behavior.
|
|
104
|
+
*/
|
|
105
|
+
export const QmlView = forwardRef(function QmlView(
|
|
106
|
+
{
|
|
107
|
+
source,
|
|
108
|
+
file,
|
|
109
|
+
context,
|
|
110
|
+
onSignal,
|
|
111
|
+
hotReload = true,
|
|
112
|
+
resolver,
|
|
113
|
+
reloadToken,
|
|
114
|
+
style,
|
|
115
|
+
}: QmlViewProps,
|
|
116
|
+
ref: Ref<QmlViewHandle>,
|
|
117
|
+
): ReactElement {
|
|
118
|
+
const state = useRef<Mounted | null>(null);
|
|
119
|
+
if (
|
|
120
|
+
!state.current ||
|
|
121
|
+
state.current.source !== source ||
|
|
122
|
+
!Object.is(state.current.token, reloadToken)
|
|
123
|
+
) {
|
|
124
|
+
const doc = parseQml(source, { fileName: file ?? '<inline>' });
|
|
125
|
+
const next = instantiateDocument(doc, {
|
|
126
|
+
extras: { ...context },
|
|
127
|
+
resolver: resolver ?? null,
|
|
128
|
+
});
|
|
129
|
+
if (state.current) {
|
|
130
|
+
if (hotReload) migrateUserState(state.current.res.context, next.context);
|
|
131
|
+
state.current.res.root.destroy();
|
|
132
|
+
}
|
|
133
|
+
state.current = { source, token: reloadToken, res: next };
|
|
134
|
+
}
|
|
135
|
+
const { res } = state.current;
|
|
136
|
+
const root = res.root;
|
|
137
|
+
|
|
138
|
+
useQmlVersion(root); // the wrapper box tracks the root's size
|
|
139
|
+
|
|
140
|
+
// Context-property updates ride ordinary commits.
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!context) return;
|
|
143
|
+
for (const [k, v] of Object.entries(context)) {
|
|
144
|
+
const slot = res.context.extras.get(k);
|
|
145
|
+
if (slot && !Object.is(slot.peek(), v)) slot.assign(v);
|
|
146
|
+
}
|
|
147
|
+
flushBindings();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!onSignal) return;
|
|
152
|
+
const offs = Object.entries(onSignal).map(([name, fn]) =>
|
|
153
|
+
root.onSignal(name, fn),
|
|
154
|
+
);
|
|
155
|
+
return () => offs.forEach((off) => off());
|
|
156
|
+
}, [root, onSignal]);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const mounted = state.current;
|
|
160
|
+
return () => {
|
|
161
|
+
mounted?.res.root.destroy();
|
|
162
|
+
if (state.current === mounted) state.current = null;
|
|
163
|
+
};
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
useImperativeHandle(
|
|
167
|
+
ref,
|
|
168
|
+
() => ({
|
|
169
|
+
root: root.facade,
|
|
170
|
+
id: (name: string) => res.context.ids.get(name)?.facade ?? null,
|
|
171
|
+
instance: root,
|
|
172
|
+
}),
|
|
173
|
+
[root, res],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Root-size feedback. `afterLayout` (not a bare effect): react-x11 lays
|
|
177
|
+
// out on the frame flush, so an effect body reads the geometry of the
|
|
178
|
+
// previous pass — src/internal/timers.ts documents the shape.
|
|
179
|
+
const wrapRef = useRef<DrawnNode>(null);
|
|
180
|
+
const rootW = root.slots.get('width');
|
|
181
|
+
const rootH = root.slots.get('height');
|
|
182
|
+
const sized = !!rootW && !rootW.isDefault;
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (sized) return;
|
|
185
|
+
const tick = afterLayout(() => {
|
|
186
|
+
const abs = wrapRef.current?.abs;
|
|
187
|
+
if (!abs || root.destroyed) return;
|
|
188
|
+
if (abs.width) root.slots.get('implicitWidth')?.assign(abs.width);
|
|
189
|
+
if (abs.height) root.slots.get('implicitHeight')?.assign(abs.height);
|
|
190
|
+
flushBindings();
|
|
191
|
+
});
|
|
192
|
+
return () => cancelAfterLayout(tick);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<box
|
|
197
|
+
ref={wrapRef}
|
|
198
|
+
style={[
|
|
199
|
+
{ position: 'relative' },
|
|
200
|
+
sized && rootH
|
|
201
|
+
? { width: num(rootW.peek()), height: num(rootH.peek()) }
|
|
202
|
+
: false,
|
|
203
|
+
style,
|
|
204
|
+
]}
|
|
205
|
+
>
|
|
206
|
+
<QmlNode inst={root} />
|
|
207
|
+
</box>
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const num = (v: unknown): number =>
|
|
212
|
+
typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
213
|
+
|
|
214
|
+
export interface RegisterReactComponentOptions {
|
|
215
|
+
properties?: Record<string, QmlPropertyDef>;
|
|
216
|
+
signals?: Record<string, string[]>;
|
|
217
|
+
/** Reshape when prop names don't line up one-to-one. */
|
|
218
|
+
toProps?: (
|
|
219
|
+
props: Record<string, unknown>,
|
|
220
|
+
inst: QmlInstance,
|
|
221
|
+
) => Record<string, unknown>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Register a React component as a QML type — the "extend in code" seam.
|
|
226
|
+
*
|
|
227
|
+
* registerReactComponent('Shop.Widgets', 'PriceTag', PriceTag, {
|
|
228
|
+
* properties: { amount: { default: 0 } },
|
|
229
|
+
* signals: { activated: [] },
|
|
230
|
+
* });
|
|
231
|
+
*
|
|
232
|
+
* Declared properties arrive as props (live: a binding update re-renders
|
|
233
|
+
* the component); declared signals arrive as `on<Signal>` callback props
|
|
234
|
+
* that emit back into QML.
|
|
235
|
+
*/
|
|
236
|
+
export function registerReactComponent(
|
|
237
|
+
moduleName: string,
|
|
238
|
+
typeName: string,
|
|
239
|
+
Component: ComponentType<Record<string, unknown>>,
|
|
240
|
+
{
|
|
241
|
+
properties = {},
|
|
242
|
+
signals = {},
|
|
243
|
+
toProps,
|
|
244
|
+
}: RegisterReactComponentOptions = {},
|
|
245
|
+
): void {
|
|
246
|
+
const View = ({ inst }: { inst: QmlInstance }): ReactElement => {
|
|
247
|
+
const props: Record<string, unknown> = {};
|
|
248
|
+
for (const name of Object.keys(properties)) {
|
|
249
|
+
props[name] = inst.slots.get(name)?.peek();
|
|
250
|
+
}
|
|
251
|
+
for (const name of Object.keys(signals)) {
|
|
252
|
+
props[`on${name[0].toUpperCase()}${name.slice(1)}`] = (
|
|
253
|
+
...args: unknown[]
|
|
254
|
+
) => inst.emit(name, ...args);
|
|
255
|
+
}
|
|
256
|
+
const shaped = toProps ? toProps(props, inst) : props;
|
|
257
|
+
const kids = renderQmlChildren(inst);
|
|
258
|
+
return (
|
|
259
|
+
<box style={geometryStyle(inst)}>
|
|
260
|
+
<Component {...shaped}>
|
|
261
|
+
{kids.length ? kids : (shaped.children as ReactNode)}
|
|
262
|
+
</Component>
|
|
263
|
+
</box>
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
registerQmlModule(moduleName, {
|
|
267
|
+
types: {
|
|
268
|
+
[typeName]: { extends: 'Item', properties, signals, view: View },
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Shared with qtquick.tsx (kept here so registerReactComponent has no
|
|
274
|
+
// dependency on the QtQuick module being loaded).
|
|
275
|
+
export function geometryStyle(
|
|
276
|
+
inst: QmlInstance,
|
|
277
|
+
{ contentSized = false } = {},
|
|
278
|
+
): Style {
|
|
279
|
+
// Inside a RowLayout/ColumnLayout the item is a flex item: yoga decides
|
|
280
|
+
// its geometry from the Layout.* attached properties, and the container
|
|
281
|
+
// reflects the answers back into x/y/width/height afterwards.
|
|
282
|
+
if (inst.parentInst?.typeInfo.managesChildLayout) {
|
|
283
|
+
return layoutChildStyle(inst);
|
|
284
|
+
}
|
|
285
|
+
const s = inst.slots;
|
|
286
|
+
const style: Style = {
|
|
287
|
+
position: 'absolute',
|
|
288
|
+
left: num(s.get('x')?.peek()),
|
|
289
|
+
top: num(s.get('y')?.peek()),
|
|
290
|
+
};
|
|
291
|
+
const w = s.get('width');
|
|
292
|
+
const h = s.get('height');
|
|
293
|
+
if (w && !(contentSized && w.isDefault)) style.width = num(w.peek());
|
|
294
|
+
if (h && !(contentSized && h.isDefault)) style.height = num(h.peek());
|
|
295
|
+
if (s.get('visible')?.peek() === false) style.display = 'none';
|
|
296
|
+
if (s.get('clip')?.peek() === true) style.overflow = 'hidden';
|
|
297
|
+
const z = s.get('z')?.peek();
|
|
298
|
+
if (typeof z === 'number' && z) style.zIndex = z;
|
|
299
|
+
warnUnrenderable(inst);
|
|
300
|
+
return style;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// --- QtQuick.Layouts: the flex-item half -----------------------------------
|
|
304
|
+
|
|
305
|
+
// Qt.AlignmentFlag values (the subset Layout.alignment uses).
|
|
306
|
+
const ALIGN = {
|
|
307
|
+
left: 1,
|
|
308
|
+
right: 2,
|
|
309
|
+
hcenter: 4,
|
|
310
|
+
top: 32,
|
|
311
|
+
bottom: 64,
|
|
312
|
+
vcenter: 128,
|
|
313
|
+
} as const;
|
|
314
|
+
|
|
315
|
+
const layoutNumOf = (v: unknown): number | null =>
|
|
316
|
+
typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* An item's size hint on one axis, in Qt's order: `Layout.preferredWidth`,
|
|
320
|
+
* then an author-set `width` (friendlier than Qt, which ignores it — ours
|
|
321
|
+
* reads as a preferred size), then `implicitWidth`. `read` decides
|
|
322
|
+
* tracking: `.get` inside the container's implicit-size bindings, `.peek`
|
|
323
|
+
* during render.
|
|
324
|
+
*/
|
|
325
|
+
export function preferredSpan(
|
|
326
|
+
inst: QmlInstance,
|
|
327
|
+
horizontal: boolean,
|
|
328
|
+
read: (slot: NonNullable<ReturnType<QmlInstance['slots']['get']>>) => unknown,
|
|
329
|
+
filling = false,
|
|
330
|
+
): number | null {
|
|
331
|
+
const pref = inst.slots.get(
|
|
332
|
+
horizontal ? 'Layout.preferredWidth' : 'Layout.preferredHeight',
|
|
333
|
+
);
|
|
334
|
+
if (pref) {
|
|
335
|
+
const v = layoutNumOf(read(pref));
|
|
336
|
+
if (v !== null) return v;
|
|
337
|
+
}
|
|
338
|
+
if (!filling) {
|
|
339
|
+
const own = inst.slots.get(horizontal ? 'width' : 'height');
|
|
340
|
+
if (own && !own.isDefault) {
|
|
341
|
+
const v = layoutNumOf(read(own));
|
|
342
|
+
if (v !== null && v > 0) return v;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const implicit = inst.slots.get(
|
|
346
|
+
horizontal ? 'implicitWidth' : 'implicitHeight',
|
|
347
|
+
);
|
|
348
|
+
if (implicit) {
|
|
349
|
+
const v = layoutNumOf(read(implicit));
|
|
350
|
+
if (v !== null && v > 0) return v;
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function layoutChildStyle(inst: QmlInstance): Style {
|
|
356
|
+
const parent = inst.parentInst;
|
|
357
|
+
const horizontal = parent?.typeInfo.name === 'RowLayout';
|
|
358
|
+
const style: Style = {};
|
|
359
|
+
const la = (name: string): unknown =>
|
|
360
|
+
inst.slots.get(`Layout.${name}`)?.peek();
|
|
361
|
+
const peek = (slot: { peek(): unknown }): unknown => slot.peek();
|
|
362
|
+
|
|
363
|
+
const fillMain = la(horizontal ? 'fillWidth' : 'fillHeight') === true;
|
|
364
|
+
const fillCross = la(horizontal ? 'fillHeight' : 'fillWidth') === true;
|
|
365
|
+
const stretchRaw = la(
|
|
366
|
+
horizontal ? 'horizontalStretchFactor' : 'verticalStretchFactor',
|
|
367
|
+
);
|
|
368
|
+
const stretch = layoutNumOf(stretchRaw) ?? 0;
|
|
369
|
+
|
|
370
|
+
// Main axis: preferred size is the flex basis; `fillWidth` grows.
|
|
371
|
+
const basis = preferredSpan(inst, horizontal, peek, fillMain);
|
|
372
|
+
if (fillMain) {
|
|
373
|
+
style.flexGrow = stretch > 0 ? stretch : 1;
|
|
374
|
+
style.flexBasis = basis ?? 0;
|
|
375
|
+
} else {
|
|
376
|
+
style.flexGrow = 0;
|
|
377
|
+
style.flexShrink = 0;
|
|
378
|
+
if (basis !== null) {
|
|
379
|
+
if (horizontal) style.width = basis;
|
|
380
|
+
else style.height = basis;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Cross axis: fill stretches; otherwise the preferred size holds and
|
|
385
|
+
// `Layout.alignment` places the item (Qt's defaults: vertically centered
|
|
386
|
+
// in a row, left in a column).
|
|
387
|
+
const crossPref = preferredSpan(inst, !horizontal, peek, fillCross);
|
|
388
|
+
if (fillCross) {
|
|
389
|
+
style.alignSelf = 'stretch';
|
|
390
|
+
} else {
|
|
391
|
+
if (crossPref !== null) {
|
|
392
|
+
if (horizontal) style.height = crossPref;
|
|
393
|
+
else style.width = crossPref;
|
|
394
|
+
}
|
|
395
|
+
const alignment = layoutNumOf(la('alignment')) ?? 0;
|
|
396
|
+
const startFlag = horizontal ? ALIGN.top : ALIGN.left;
|
|
397
|
+
const endFlag = horizontal ? ALIGN.bottom : ALIGN.right;
|
|
398
|
+
const centerFlag = horizontal ? ALIGN.vcenter : ALIGN.hcenter;
|
|
399
|
+
style.alignSelf =
|
|
400
|
+
alignment & startFlag
|
|
401
|
+
? 'flex-start'
|
|
402
|
+
: alignment & endFlag
|
|
403
|
+
? 'flex-end'
|
|
404
|
+
: alignment & centerFlag
|
|
405
|
+
? 'center'
|
|
406
|
+
: horizontal
|
|
407
|
+
? 'center'
|
|
408
|
+
: 'flex-start';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const minW = layoutNumOf(la('minimumWidth'));
|
|
412
|
+
if (minW !== null) style.minWidth = minW;
|
|
413
|
+
const minH = layoutNumOf(la('minimumHeight'));
|
|
414
|
+
if (minH !== null) style.minHeight = minH;
|
|
415
|
+
const maxW = layoutNumOf(la('maximumWidth'));
|
|
416
|
+
if (maxW !== null) style.maxWidth = maxW;
|
|
417
|
+
const maxH = layoutNumOf(la('maximumHeight'));
|
|
418
|
+
if (maxH !== null) style.maxHeight = maxH;
|
|
419
|
+
|
|
420
|
+
const margins = layoutNumOf(la('margins')) ?? 0;
|
|
421
|
+
const side = (name: string): number => layoutNumOf(la(name)) ?? margins;
|
|
422
|
+
if (margins || la('leftMargin') !== undefined) {
|
|
423
|
+
style.marginLeft = side('leftMargin');
|
|
424
|
+
}
|
|
425
|
+
if (margins || la('rightMargin') !== undefined)
|
|
426
|
+
style.marginRight = side('rightMargin');
|
|
427
|
+
if (margins || la('topMargin') !== undefined)
|
|
428
|
+
style.marginTop = side('topMargin');
|
|
429
|
+
if (margins || la('bottomMargin') !== undefined)
|
|
430
|
+
style.marginBottom = side('bottomMargin');
|
|
431
|
+
|
|
432
|
+
if (inst.slots.get('visible')?.peek() === false) style.display = 'none';
|
|
433
|
+
if (inst.slots.get('clip')?.peek() === true) style.overflow = 'hidden';
|
|
434
|
+
warnUnrenderable(inst);
|
|
435
|
+
return style;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const INERT: Record<string, number> = { opacity: 1, rotation: 0, scale: 1 };
|
|
439
|
+
const warned = new Set<string>();
|
|
440
|
+
function warnUnrenderable(inst: QmlInstance): void {
|
|
441
|
+
for (const name of Object.keys(INERT)) {
|
|
442
|
+
const v = inst.slots.get(name)?.peek();
|
|
443
|
+
if (v !== undefined && v !== INERT[name] && !warned.has(name)) {
|
|
444
|
+
warned.add(name);
|
|
445
|
+
warn(
|
|
446
|
+
`QML: '${name}' is declared but react-x11 cannot render it yet ` +
|
|
447
|
+
`(needs offscreen composition); the value is ignored.`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Re-exported here so the family's public surface sits in one place.
|
|
454
|
+
export { Context, QmlInstance };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// The resolver seam: how `.qml` files find each other.
|
|
2
|
+
//
|
|
3
|
+
// The engine never touches a filesystem — it asks a `QmlResolver` for
|
|
4
|
+
// component sources and treats directory strings as opaque tokens only the
|
|
5
|
+
// resolver interprets. That keeps the engine host-neutral (the headless
|
|
6
|
+
// tests hand it plain objects) and makes "where do components come from"
|
|
7
|
+
// an app decision: a directory on disk, a bundle, an HTTP cache, a test
|
|
8
|
+
// fixture.
|
|
9
|
+
//
|
|
10
|
+
// Three ways a type name resolves, in Qt's precedence order:
|
|
11
|
+
// 1. the *implicit import* — `<Name>.qml` beside the document (no import
|
|
12
|
+
// line at all, and a local `Button.qml` shadows a module's `Button`);
|
|
13
|
+
// 2. quoted-path imports — `import "./widgets"` brings in that
|
|
14
|
+
// directory's components;
|
|
15
|
+
// 3. registered modules — `import QtQuick 2.15` and anything an app adds
|
|
16
|
+
// with `registerQmlModule`.
|
|
17
|
+
// The first two exist only when a resolver is provided.
|
|
18
|
+
|
|
19
|
+
export interface QmlResolver {
|
|
20
|
+
/** Where the root document lives — the starting directory for its
|
|
21
|
+
* implicit import. Opaque to the engine. */
|
|
22
|
+
rootDir: string;
|
|
23
|
+
/** The source of `<name>.qml` in `dir`, or null when there is no such
|
|
24
|
+
* component. Called often (every type name is probed here first, in
|
|
25
|
+
* Qt's shadowing order) but cached by the engine per instantiation —
|
|
26
|
+
* a plain `existsSync` + `readFileSync` implementation is fine. */
|
|
27
|
+
load(dir: string, name: string): { source: string; fileName: string } | null;
|
|
28
|
+
/** A quoted relative import (`"./widgets"`, `"../shared"`) resolved
|
|
29
|
+
* against a document's directory. */
|
|
30
|
+
join(dir: string, relative: string): string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** How a document was loaded — carried on `QmlDocument` so the types it
|
|
34
|
+
* names can resolve relative to it. */
|
|
35
|
+
export interface DocLoad {
|
|
36
|
+
resolver: QmlResolver;
|
|
37
|
+
dir: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- the standard filesystem resolver --------------------------------------
|
|
41
|
+
|
|
42
|
+
interface FsModule {
|
|
43
|
+
existsSync(path: string): boolean;
|
|
44
|
+
readFileSync(path: string, encoding: 'utf8'): string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface PathModule {
|
|
48
|
+
resolve(...parts: string[]): string;
|
|
49
|
+
join(...parts: string[]): string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load a node builtin without naming it to the compiler — `src/` compiles
|
|
54
|
+
* with `types: []` on purpose, and a literal `import('node:fs')` would
|
|
55
|
+
* need `@types/node` in the build program (the `embed/host.ts` pattern).
|
|
56
|
+
*/
|
|
57
|
+
const builtins = new Map<string, Promise<unknown>>();
|
|
58
|
+
function builtin<T>(name: string): Promise<T> {
|
|
59
|
+
let mod = builtins.get(name);
|
|
60
|
+
if (!mod) {
|
|
61
|
+
const specifier = name;
|
|
62
|
+
mod = import(/* @vite-ignore */ specifier);
|
|
63
|
+
builtins.set(name, mod);
|
|
64
|
+
}
|
|
65
|
+
return mod as Promise<T>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The standard filesystem resolver — pass the directory your root document
|
|
70
|
+
* lives in:
|
|
71
|
+
*
|
|
72
|
+
* const resolver = await createFileResolver(dirname(qmlPath));
|
|
73
|
+
* <QmlView source={source} file={qmlPath} resolver={resolver} />
|
|
74
|
+
*
|
|
75
|
+
* Async because the node builtins load through a dynamic import (the build
|
|
76
|
+
* has no node types); the resolver it returns is fully synchronous. It
|
|
77
|
+
* reads through on every engine request — no content cache — so a changed
|
|
78
|
+
* sibling file is picked up on the next root reload; the engine's own
|
|
79
|
+
* per-instantiation cache keeps a 1000-delegate ListView from re-reading
|
|
80
|
+
* its component 1000 times.
|
|
81
|
+
*/
|
|
82
|
+
export async function createFileResolver(
|
|
83
|
+
baseDir: string,
|
|
84
|
+
): Promise<QmlResolver> {
|
|
85
|
+
const fs = await builtin<FsModule>('node:fs');
|
|
86
|
+
const path = await builtin<PathModule>('node:path');
|
|
87
|
+
const rootDir = path.resolve(baseDir);
|
|
88
|
+
return {
|
|
89
|
+
rootDir,
|
|
90
|
+
load(dir, name) {
|
|
91
|
+
const file = path.join(dir, `${name}.qml`);
|
|
92
|
+
if (!fs.existsSync(file)) return null;
|
|
93
|
+
return { source: fs.readFileSync(file, 'utf8'), fileName: file };
|
|
94
|
+
},
|
|
95
|
+
join(dir, relative) {
|
|
96
|
+
return path.resolve(dir, relative);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|