@react-x11/components 0.2.1 → 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/internal/heights.d.ts +26 -1
- package/dist/internal/heights.d.ts.map +1 -1
- package/dist/internal/heights.js +57 -6
- package/dist/internal/heights.js.map +1 -1
- package/dist/internal/scroll.d.ts +11 -0
- package/dist/internal/scroll.d.ts.map +1 -1
- package/dist/internal/scroll.js +43 -3
- package/dist/internal/scroll.js.map +1 -1
- package/dist/internal/timers.d.ts +8 -0
- package/dist/internal/timers.d.ts.map +1 -1
- package/dist/internal/timers.js +10 -0
- package/dist/internal/timers.js.map +1 -1
- package/dist/internal/window.d.ts +161 -0
- package/dist/internal/window.d.ts.map +1 -0
- package/dist/internal/window.js +417 -0
- package/dist/internal/window.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/table/index.d.ts +73 -2
- package/dist/table/index.d.ts.map +1 -1
- package/dist/table/index.js +405 -161
- package/dist/table/index.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 +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/dist/tree/index.d.ts +65 -5
- package/dist/tree/index.d.ts.map +1 -1
- package/dist/tree/index.js +446 -189
- package/dist/tree/index.js.map +1 -1
- package/package.json +11 -1
- package/src/index.ts +44 -0
- package/src/internal/heights.ts +57 -6
- package/src/internal/scroll.ts +59 -3
- package/src/internal/timers.ts +13 -0
- package/src/internal/window.ts +570 -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/table/index.ts +599 -178
- 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
- package/src/tree/index.ts +687 -260
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Pointer and keyboard: MouseArea, the `Keys` attached handlers, `focus`/
|
|
2
|
+
// `activeFocus`, and the two text editors over core's `<textinput>` /
|
|
3
|
+
// `<textarea>`.
|
|
4
|
+
|
|
5
|
+
import { useEffect } from 'react';
|
|
6
|
+
import type { ReactElement } from 'react';
|
|
7
|
+
import type { Style } from 'react-x11/style';
|
|
8
|
+
import {
|
|
9
|
+
XK_BACKSPACE,
|
|
10
|
+
XK_DELETE,
|
|
11
|
+
XK_DOWN,
|
|
12
|
+
XK_ESCAPE,
|
|
13
|
+
XK_KP_ENTER,
|
|
14
|
+
XK_LEFT,
|
|
15
|
+
XK_RETURN,
|
|
16
|
+
XK_RIGHT,
|
|
17
|
+
XK_SPACE,
|
|
18
|
+
XK_TAB,
|
|
19
|
+
XK_UP,
|
|
20
|
+
} from 'react-x11/keysyms';
|
|
21
|
+
|
|
22
|
+
import { QmlInstance, type QmlTypeDef } from './objects.js';
|
|
23
|
+
import { renderQmlChildren, geometryStyle } from './react.js';
|
|
24
|
+
import {
|
|
25
|
+
applyMotion,
|
|
26
|
+
captureNode,
|
|
27
|
+
hostNode,
|
|
28
|
+
num,
|
|
29
|
+
qmlColor,
|
|
30
|
+
str,
|
|
31
|
+
} from './view-utils.js';
|
|
32
|
+
|
|
33
|
+
// --- Keys + focus ----------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
interface KeyEventLike {
|
|
36
|
+
keysym?: number;
|
|
37
|
+
key?: string;
|
|
38
|
+
shiftKey?: boolean;
|
|
39
|
+
ctrlKey?: boolean;
|
|
40
|
+
altKey?: boolean;
|
|
41
|
+
metaKey?: boolean;
|
|
42
|
+
stopPropagation?(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface PointerEventLike {
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
button?: number;
|
|
49
|
+
detail?: number;
|
|
50
|
+
shiftKey?: boolean;
|
|
51
|
+
ctrlKey?: boolean;
|
|
52
|
+
altKey?: boolean;
|
|
53
|
+
stopPropagation?(): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface WheelEventLike extends PointerEventLike {
|
|
57
|
+
deltaX?: number;
|
|
58
|
+
deltaY?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Qt's modifier flags, so `mouse.modifiers & Qt.ShiftModifier` reads as
|
|
62
|
+
// ported code expects.
|
|
63
|
+
const SHIFT = 0x02000000;
|
|
64
|
+
const CTRL = 0x04000000;
|
|
65
|
+
const ALT = 0x08000000;
|
|
66
|
+
const META = 0x10000000;
|
|
67
|
+
|
|
68
|
+
const modifiersOf = (e: {
|
|
69
|
+
shiftKey?: boolean;
|
|
70
|
+
ctrlKey?: boolean;
|
|
71
|
+
altKey?: boolean;
|
|
72
|
+
metaKey?: boolean;
|
|
73
|
+
}): number =>
|
|
74
|
+
(e.shiftKey ? SHIFT : 0) |
|
|
75
|
+
(e.ctrlKey ? CTRL : 0) |
|
|
76
|
+
(e.altKey ? ALT : 0) |
|
|
77
|
+
(e.metaKey ? META : 0);
|
|
78
|
+
|
|
79
|
+
const NAMED_KEYS: Record<number, string> = {
|
|
80
|
+
[XK_RETURN]: 'Return',
|
|
81
|
+
[XK_KP_ENTER]: 'Enter',
|
|
82
|
+
[XK_ESCAPE]: 'Escape',
|
|
83
|
+
[XK_UP]: 'Up',
|
|
84
|
+
[XK_DOWN]: 'Down',
|
|
85
|
+
[XK_LEFT]: 'Left',
|
|
86
|
+
[XK_RIGHT]: 'Right',
|
|
87
|
+
[XK_TAB]: 'Tab',
|
|
88
|
+
[XK_SPACE]: 'Space',
|
|
89
|
+
[XK_DELETE]: 'Delete',
|
|
90
|
+
[XK_BACKSPACE]: 'Back',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function dispatchKeys(inst: QmlInstance, e: KeyEventLike): void {
|
|
94
|
+
const ev = {
|
|
95
|
+
key: e.keysym ?? 0,
|
|
96
|
+
text: e.key ?? '',
|
|
97
|
+
modifiers: modifiersOf(e),
|
|
98
|
+
accepted: false,
|
|
99
|
+
};
|
|
100
|
+
const named = NAMED_KEYS[ev.key];
|
|
101
|
+
if (named) inst.attachedHandlers.get(`Keys.on${named}Pressed`)?.(ev);
|
|
102
|
+
if (!ev.accepted) inst.attachedHandlers.get('Keys.onPressed')?.(ev);
|
|
103
|
+
if (ev.accepted) e.stopPropagation?.();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The focus/keyboard props an Item earns from `focus: true`, `Keys.*`
|
|
108
|
+
* handlers, or being told to be focusable. `activeFocus` follows the real
|
|
109
|
+
* focus events.
|
|
110
|
+
*/
|
|
111
|
+
export function keyFocusProps(inst: QmlInstance): Record<string, unknown> {
|
|
112
|
+
const wantsFocus = inst.slots.get('focus')?.peek() === true;
|
|
113
|
+
let hasKeys = false;
|
|
114
|
+
for (const k of inst.attachedHandlers.keys()) {
|
|
115
|
+
if (k.startsWith('Keys.')) {
|
|
116
|
+
hasKeys = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!wantsFocus && !hasKeys) return {};
|
|
121
|
+
const props: Record<string, unknown> = {
|
|
122
|
+
focusable: true,
|
|
123
|
+
onFocus: () => inst.slots.get('activeFocus')?.assign(true),
|
|
124
|
+
onBlur: () => inst.slots.get('activeFocus')?.assign(false),
|
|
125
|
+
};
|
|
126
|
+
if (wantsFocus) props.autoFocus = true;
|
|
127
|
+
if (hasKeys) props.onKeyDown = (e: KeyEventLike) => dispatchKeys(inst, e);
|
|
128
|
+
return props;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- MouseArea -------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
function MouseAreaView({ inst }: { inst: QmlInstance }): ReactElement {
|
|
134
|
+
const enabled = inst.slot('enabled').peek() !== false;
|
|
135
|
+
const hoverEnabled = inst.slot('hoverEnabled').peek() === true;
|
|
136
|
+
const style = geometryStyle(inst);
|
|
137
|
+
applyMotion(inst, style);
|
|
138
|
+
|
|
139
|
+
const mouse = (e: PointerEventLike) => {
|
|
140
|
+
const abs = hostNode(inst)?.abs;
|
|
141
|
+
return {
|
|
142
|
+
x: e.x - (abs?.x ?? 0),
|
|
143
|
+
y: e.y - (abs?.y ?? 0),
|
|
144
|
+
button: e.button,
|
|
145
|
+
modifiers: modifiersOf(e),
|
|
146
|
+
accepted: true,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const handlers: Record<string, unknown> = {};
|
|
151
|
+
if (enabled) {
|
|
152
|
+
handlers.onClick = (e: PointerEventLike) => {
|
|
153
|
+
e.stopPropagation?.();
|
|
154
|
+
inst.emit('clicked', mouse(e));
|
|
155
|
+
if (e.detail === 2) inst.emit('doubleClicked', mouse(e));
|
|
156
|
+
};
|
|
157
|
+
handlers.onMouseDown = (e: PointerEventLike) => {
|
|
158
|
+
e.stopPropagation?.();
|
|
159
|
+
inst.slot('pressed').assign(true);
|
|
160
|
+
inst.emit('pressed', mouse(e));
|
|
161
|
+
};
|
|
162
|
+
handlers.onMouseUp = (e: PointerEventLike) => {
|
|
163
|
+
e.stopPropagation?.();
|
|
164
|
+
inst.slot('pressed').assign(false);
|
|
165
|
+
inst.emit('released', mouse(e));
|
|
166
|
+
};
|
|
167
|
+
handlers.onMouseMove = (e: PointerEventLike) => {
|
|
168
|
+
inst.emit('positionChanged', mouse(e));
|
|
169
|
+
};
|
|
170
|
+
handlers.onWheel = (e: WheelEventLike) => {
|
|
171
|
+
inst.emit('wheel', {
|
|
172
|
+
...mouse(e),
|
|
173
|
+
angleDelta: { x: e.deltaX ?? 0, y: e.deltaY ?? 0 },
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
if (hoverEnabled) {
|
|
177
|
+
handlers.onMouseEnter = () => {
|
|
178
|
+
inst.slot('containsMouse').assign(true);
|
|
179
|
+
inst.emit('entered');
|
|
180
|
+
};
|
|
181
|
+
handlers.onMouseLeave = () => {
|
|
182
|
+
inst.slot('containsMouse').assign(false);
|
|
183
|
+
inst.emit('exited');
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<box
|
|
190
|
+
ref={captureNode(inst)}
|
|
191
|
+
style={style}
|
|
192
|
+
{...handlers}
|
|
193
|
+
{...keyFocusProps(inst)}
|
|
194
|
+
>
|
|
195
|
+
{renderQmlChildren(inst)}
|
|
196
|
+
</box>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- TextInput / TextEdit --------------------------------------------------
|
|
201
|
+
|
|
202
|
+
/** The font/colour subset Text and the editors share. */
|
|
203
|
+
export function applyFontStyle(inst: QmlInstance, style: Style): void {
|
|
204
|
+
const color = inst.slots.get('color')?.peek();
|
|
205
|
+
if (color !== undefined) style.color = qmlColor(color);
|
|
206
|
+
const px = num(inst.slots.get('font.pixelSize')?.peek());
|
|
207
|
+
const pt = num(inst.slots.get('font.pointSize')?.peek());
|
|
208
|
+
if (px) style.fontSize = px;
|
|
209
|
+
else if (pt) style.fontSize = Math.round((pt * 96) / 72); // CSS's 1pt
|
|
210
|
+
const family = inst.slots.get('font.family')?.peek();
|
|
211
|
+
if (family) style.fontFamily = String(family);
|
|
212
|
+
if (inst.slots.get('font.bold')?.peek()) style.fontWeight = 700;
|
|
213
|
+
else {
|
|
214
|
+
const weight = num(inst.slots.get('font.weight')?.peek());
|
|
215
|
+
if (weight) style.fontWeight = weight;
|
|
216
|
+
}
|
|
217
|
+
if (inst.slots.get('font.italic')?.peek()) style.fontStyle = 'italic';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function useImplicitLineHeight(inst: QmlInstance, text: string): void {
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
const node = hostNode(inst) as {
|
|
223
|
+
app?: { fonts?: { layout(t: string, s: unknown): { height: number } } };
|
|
224
|
+
resolvedTextStyle?(): unknown;
|
|
225
|
+
} | null;
|
|
226
|
+
if (!node?.app?.fonts || !node.resolvedTextStyle) return;
|
|
227
|
+
try {
|
|
228
|
+
const probe = node.app.fonts.layout(
|
|
229
|
+
text || 'Mg',
|
|
230
|
+
node.resolvedTextStyle(),
|
|
231
|
+
);
|
|
232
|
+
inst.slots.get('implicitHeight')?.assign(Math.ceil(probe.height) + 8);
|
|
233
|
+
} catch {
|
|
234
|
+
// No fonts loaded (headless without the fonts option): leave 0.
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function editorView(tag: 'textinput' | 'textarea') {
|
|
240
|
+
return function EditorView({ inst }: { inst: QmlInstance }): ReactElement {
|
|
241
|
+
const style = geometryStyle(inst);
|
|
242
|
+
applyFontStyle(inst, style);
|
|
243
|
+
applyMotion(inst, style);
|
|
244
|
+
const text = str(inst.slot('text').peek());
|
|
245
|
+
useImplicitLineHeight(inst, text);
|
|
246
|
+
const maxLen = num(inst.slots.get('maximumLength')?.peek());
|
|
247
|
+
const props: Record<string, unknown> = {
|
|
248
|
+
ref: captureNode(inst),
|
|
249
|
+
style,
|
|
250
|
+
value: text,
|
|
251
|
+
onChange: (ev: { value: string }) => inst.slot('text').set(ev.value),
|
|
252
|
+
...keyFocusProps(inst),
|
|
253
|
+
};
|
|
254
|
+
if (tag === 'textinput') {
|
|
255
|
+
props.onSubmit = () => inst.emit('accepted');
|
|
256
|
+
if (maxLen > 0 && maxLen < 32767) props.maxLength = maxLen;
|
|
257
|
+
return <textinput {...props} />;
|
|
258
|
+
}
|
|
259
|
+
return <textarea {...props} />;
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- the type definitions --------------------------------------------------
|
|
264
|
+
|
|
265
|
+
export const interactionTypes: Record<string, QmlTypeDef> = {
|
|
266
|
+
MouseArea: {
|
|
267
|
+
extends: 'Item',
|
|
268
|
+
view: MouseAreaView,
|
|
269
|
+
properties: {
|
|
270
|
+
pressed: { default: false },
|
|
271
|
+
containsMouse: { default: false },
|
|
272
|
+
hoverEnabled: { default: false },
|
|
273
|
+
},
|
|
274
|
+
signals: {
|
|
275
|
+
clicked: ['mouse'],
|
|
276
|
+
doubleClicked: ['mouse'],
|
|
277
|
+
pressed: ['mouse'],
|
|
278
|
+
released: ['mouse'],
|
|
279
|
+
positionChanged: ['mouse'],
|
|
280
|
+
entered: [],
|
|
281
|
+
exited: [],
|
|
282
|
+
wheel: ['wheel'],
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
TextInput: {
|
|
287
|
+
extends: 'Item',
|
|
288
|
+
view: editorView('textinput'),
|
|
289
|
+
properties: {
|
|
290
|
+
text: { default: '' },
|
|
291
|
+
color: { default: undefined },
|
|
292
|
+
maximumLength: { default: 32767 },
|
|
293
|
+
'font.pixelSize': { default: 0 },
|
|
294
|
+
'font.pointSize': { default: 0 },
|
|
295
|
+
'font.family': { default: '' },
|
|
296
|
+
'font.bold': { default: false },
|
|
297
|
+
'font.italic': { default: false },
|
|
298
|
+
'font.weight': { default: 0 },
|
|
299
|
+
},
|
|
300
|
+
signals: { accepted: [], editingFinished: [] },
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
TextEdit: {
|
|
304
|
+
extends: 'Item',
|
|
305
|
+
view: editorView('textarea'),
|
|
306
|
+
properties: {
|
|
307
|
+
text: { default: '' },
|
|
308
|
+
color: { default: undefined },
|
|
309
|
+
'font.pixelSize': { default: 0 },
|
|
310
|
+
'font.pointSize': { default: 0 },
|
|
311
|
+
'font.family': { default: '' },
|
|
312
|
+
'font.bold': { default: false },
|
|
313
|
+
'font.italic': { default: false },
|
|
314
|
+
'font.weight': { default: 0 },
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
};
|
package/src/qml/ir.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The intermediate representation both halves share: the parser produces it,
|
|
2
|
+
// the runtime instantiates it, and the future compiler will serialize it.
|
|
3
|
+
// Everything is plain data — no methods, no cycles — so a document parses
|
|
4
|
+
// once and instantiates many times (a Repeater's delegate is one ObjectIR,
|
|
5
|
+
// however many rows exist).
|
|
6
|
+
|
|
7
|
+
export interface QmlImport {
|
|
8
|
+
/** `import QtQuick 2.15` — a registry module name. */
|
|
9
|
+
module?: string;
|
|
10
|
+
/** `import "./widgets"` — a path import (resolved by the host app). */
|
|
11
|
+
path?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
alias?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ObjectIR {
|
|
17
|
+
type: string;
|
|
18
|
+
id: string | null;
|
|
19
|
+
members: MemberIR[];
|
|
20
|
+
loc: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ValueIR =
|
|
24
|
+
/** A JavaScript expression, compiled lazily by the runtime. */
|
|
25
|
+
| { kind: 'expr'; src: string; loc: number }
|
|
26
|
+
/** `prop: { …statements }` — a script binding (still reactive). */
|
|
27
|
+
| { kind: 'block'; src: string; loc: number }
|
|
28
|
+
/** `delegate: Component { … }`, `background: Rectangle { … }`. */
|
|
29
|
+
| { kind: 'object'; object: ObjectIR }
|
|
30
|
+
/** `states: [State { … }, …]` — an array of object templates. */
|
|
31
|
+
| { kind: 'array'; items: ValueIR[] };
|
|
32
|
+
|
|
33
|
+
export interface AliasTargetIR {
|
|
34
|
+
kind: 'alias-target';
|
|
35
|
+
path: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type MemberIR =
|
|
39
|
+
| {
|
|
40
|
+
kind: 'property';
|
|
41
|
+
name: string;
|
|
42
|
+
propType: string;
|
|
43
|
+
readonly: boolean;
|
|
44
|
+
default: boolean;
|
|
45
|
+
required: boolean;
|
|
46
|
+
value: ValueIR | AliasTargetIR | null;
|
|
47
|
+
loc: number;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
kind: 'signal';
|
|
51
|
+
name: string;
|
|
52
|
+
params: Array<{ type: string; name: string }>;
|
|
53
|
+
loc: number;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
kind: 'function';
|
|
57
|
+
name: string;
|
|
58
|
+
params: string[];
|
|
59
|
+
body: string;
|
|
60
|
+
loc: number;
|
|
61
|
+
}
|
|
62
|
+
| { kind: 'binding'; path: string[]; value: ValueIR; loc: number }
|
|
63
|
+
| { kind: 'group'; bindings: BindingMemberIR[]; loc: number }
|
|
64
|
+
| { kind: 'object'; object: ObjectIR; loc: number }
|
|
65
|
+
| { kind: 'value-source'; object: ObjectIR; on: string[]; loc: number }
|
|
66
|
+
| { kind: 'id'; id: string; loc: number }
|
|
67
|
+
| { kind: 'inline-component'; name: string; object: ObjectIR; loc: number };
|
|
68
|
+
|
|
69
|
+
export type BindingMemberIR = Extract<MemberIR, { kind: 'binding' }>;
|
|
70
|
+
|
|
71
|
+
export interface QmlDocument {
|
|
72
|
+
fileName: string;
|
|
73
|
+
imports: QmlImport[];
|
|
74
|
+
pragmas: string[];
|
|
75
|
+
root: ObjectIR;
|
|
76
|
+
/** How this document was loaded — set by the engine when a resolver is
|
|
77
|
+
* in play, so the type names it uses can resolve `.qml` files relative
|
|
78
|
+
* to it (resolver.ts). */
|
|
79
|
+
load?: import('./resolver.js').DocLoad;
|
|
80
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// QtQuick.Layouts — RowLayout and ColumnLayout, lowered onto yoga.
|
|
2
|
+
//
|
|
3
|
+
// This is react-x11's home turf: the container renders a flex `<box>` and
|
|
4
|
+
// every child renders as a flex item styled from its `Layout.*` attached
|
|
5
|
+
// properties (react.tsx, `layoutChildStyle`) — `fillWidth` is `flexGrow`,
|
|
6
|
+
// `preferredWidth` is the basis, `alignment` is `alignSelf`, margins are
|
|
7
|
+
// margins. Yoga decides the geometry; the renderer never re-implements it.
|
|
8
|
+
//
|
|
9
|
+
// What QML still needs from that arrangement is *readable geometry*:
|
|
10
|
+
// `item.x` and `item.width` are ordinary properties other bindings consume.
|
|
11
|
+
// After each layout pass the container reads its children's laid-out rects
|
|
12
|
+
// and `reflect()`s them into the x/y/width/height slots — a write that
|
|
13
|
+
// disturbs no binding and no flag, so the implicit-width default keeps
|
|
14
|
+
// meaning "content-sized" and an author `width:` keeps reading as a size
|
|
15
|
+
// hint. Expressions see yoga's answer; yoga sees the hints.
|
|
16
|
+
//
|
|
17
|
+
// GridLayout is not here yet — yoga has no grid, and a half-true one would
|
|
18
|
+
// be worse than the clear unknown-type error; RowLayout, ColumnLayout and
|
|
19
|
+
// the full Layout.* set cover the linked reference page.
|
|
20
|
+
|
|
21
|
+
import { useEffect } from 'react';
|
|
22
|
+
import type { ReactElement } from 'react';
|
|
23
|
+
|
|
24
|
+
import { QmlInstance, type QmlTypeDef } from './objects.js';
|
|
25
|
+
import { renderQmlChildren, geometryStyle, preferredSpan } from './react.js';
|
|
26
|
+
import {
|
|
27
|
+
applyMotion,
|
|
28
|
+
captureNode,
|
|
29
|
+
hostNode,
|
|
30
|
+
initItem,
|
|
31
|
+
num,
|
|
32
|
+
} from './view-utils.js';
|
|
33
|
+
import {
|
|
34
|
+
afterLayout,
|
|
35
|
+
cancelAfterLayout,
|
|
36
|
+
type LayoutTick,
|
|
37
|
+
} from '../internal/timers.js';
|
|
38
|
+
|
|
39
|
+
function isRow(inst: QmlInstance): boolean {
|
|
40
|
+
return inst.typeInfo.name === 'RowLayout';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Reflect yoga's answers into the managed children's geometry slots. */
|
|
44
|
+
function feedbackLayout(inst: QmlInstance): void {
|
|
45
|
+
if (inst.destroyed) return;
|
|
46
|
+
const containerAbs = hostNode(inst)?.abs;
|
|
47
|
+
if (!containerAbs) return;
|
|
48
|
+
for (const child of inst.visualChildren()) {
|
|
49
|
+
const abs = hostNode(child)?.abs;
|
|
50
|
+
if (!abs) continue; // a custom view without captureNode: skip quietly
|
|
51
|
+
child.slots.get('x')?.reflect(abs.x - containerAbs.x);
|
|
52
|
+
child.slots.get('y')?.reflect(abs.y - containerAbs.y);
|
|
53
|
+
child.slots.get('width')?.reflect(abs.width);
|
|
54
|
+
child.slots.get('height')?.reflect(abs.height);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function LayoutView({ inst }: { inst: QmlInstance }): ReactElement {
|
|
59
|
+
const horizontal = isRow(inst);
|
|
60
|
+
const style = geometryStyle(inst);
|
|
61
|
+
style.display = 'flex';
|
|
62
|
+
style.flexDirection = horizontal ? 'row' : 'column';
|
|
63
|
+
style.gap = num(inst.slot('spacing').peek());
|
|
64
|
+
applyMotion(inst, style);
|
|
65
|
+
|
|
66
|
+
// Feed geometry back after the layout flush — and again whenever any
|
|
67
|
+
// child re-renders (its size hints may have moved yoga). A reflect that
|
|
68
|
+
// changes nothing notifies nobody, so this settles.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
let tick: LayoutTick = null;
|
|
71
|
+
const schedule = (): void => {
|
|
72
|
+
cancelAfterLayout(tick);
|
|
73
|
+
tick = afterLayout(() => feedbackLayout(inst));
|
|
74
|
+
};
|
|
75
|
+
schedule();
|
|
76
|
+
const offs = inst.visualChildren().map((c) => c.subscribe(schedule));
|
|
77
|
+
return () => {
|
|
78
|
+
cancelAfterLayout(tick);
|
|
79
|
+
offs.forEach((off) => off());
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<box ref={captureNode(inst)} style={style}>
|
|
85
|
+
{renderQmlChildren(inst)}
|
|
86
|
+
</box>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The container's own size hint: sum of children's preferred spans on the
|
|
91
|
+
* main axis (plus spacing), max on the cross — tracked bindings, so a
|
|
92
|
+
* child's hint change resizes a content-sized layout. */
|
|
93
|
+
function layoutImplicit(inst: QmlInstance): void {
|
|
94
|
+
const horizontal = isRow(inst);
|
|
95
|
+
const tracked = (slot: { get(): unknown }): unknown => slot.get();
|
|
96
|
+
const mainExtent = (): number => {
|
|
97
|
+
let sum = 0;
|
|
98
|
+
let count = 0;
|
|
99
|
+
const spacing = num(inst.slot('spacing').get());
|
|
100
|
+
for (const child of inst.visualChildren()) {
|
|
101
|
+
if (child.slot('visible').get() === false) continue;
|
|
102
|
+
sum += preferredSpan(child, horizontal, tracked) ?? 0;
|
|
103
|
+
count++;
|
|
104
|
+
}
|
|
105
|
+
return sum + spacing * Math.max(0, count - 1);
|
|
106
|
+
};
|
|
107
|
+
const crossExtent = (): number => {
|
|
108
|
+
let max = 0;
|
|
109
|
+
for (const child of inst.visualChildren()) {
|
|
110
|
+
if (child.slot('visible').get() === false) continue;
|
|
111
|
+
max = Math.max(max, preferredSpan(child, !horizontal, tracked) ?? 0);
|
|
112
|
+
}
|
|
113
|
+
return max;
|
|
114
|
+
};
|
|
115
|
+
inst
|
|
116
|
+
.slot(horizontal ? 'implicitWidth' : 'implicitHeight')
|
|
117
|
+
.setBinding(mainExtent);
|
|
118
|
+
inst
|
|
119
|
+
.slot(horizontal ? 'implicitHeight' : 'implicitWidth')
|
|
120
|
+
.setBinding(crossExtent);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function layoutInit(inst: QmlInstance): void {
|
|
124
|
+
initItem(inst);
|
|
125
|
+
layoutImplicit(inst);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const layoutContainer: QmlTypeDef = {
|
|
129
|
+
extends: 'Item',
|
|
130
|
+
managesChildLayout: true,
|
|
131
|
+
view: LayoutView,
|
|
132
|
+
// Qt's RowLayout/ColumnLayout default spacing.
|
|
133
|
+
properties: { spacing: { default: 5 } },
|
|
134
|
+
init: layoutInit,
|
|
135
|
+
onStructure: layoutImplicit, // a Repeater spliced new children in
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const layoutTypes: Record<string, QmlTypeDef> = {
|
|
139
|
+
RowLayout: { ...layoutContainer },
|
|
140
|
+
ColumnLayout: { ...layoutContainer },
|
|
141
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// ListModel / ListElement, and the one helper every model consumer shares.
|
|
2
|
+
//
|
|
3
|
+
// A ListModel is rows of plain objects. Inline `ListElement` children seed
|
|
4
|
+
// it; the mutation methods (`append`, `remove`, `set`, …) are facade
|
|
5
|
+
// methods, and every mutation bumps a hidden revision slot the consumers
|
|
6
|
+
// (Repeater, ListView) watch — a coarse rebuild, which is the honest P1
|
|
7
|
+
// answer; incremental delegate reuse is DelegateModel's job, later.
|
|
8
|
+
|
|
9
|
+
import { QmlInstance, instanceOf, type QmlTypeDef } from './objects.js';
|
|
10
|
+
import { Slot } from './slots.js';
|
|
11
|
+
|
|
12
|
+
type Row = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
const rowsOf = (m: QmlInstance): Row[] => m.state.rows as Row[];
|
|
15
|
+
|
|
16
|
+
function mutate(m: QmlInstance, fn: (rows: Row[]) => void): void {
|
|
17
|
+
fn(rowsOf(m));
|
|
18
|
+
m.slot('count').assign(rowsOf(m).length);
|
|
19
|
+
m.slot('__rev').assign((m.slot('__rev').peek() as number) + 1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const modelTypes: Record<string, QmlTypeDef> = {
|
|
23
|
+
ListElement: {
|
|
24
|
+
nonVisual: true,
|
|
25
|
+
dynamicProperties: true,
|
|
26
|
+
properties: {},
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
ListModel: {
|
|
30
|
+
nonVisual: true,
|
|
31
|
+
properties: { count: { default: 0 }, __rev: { default: 0 } },
|
|
32
|
+
init(m) {
|
|
33
|
+
const rows: Row[] = [];
|
|
34
|
+
for (const el of m.children) {
|
|
35
|
+
if (el.typeInfo.name !== 'ListElement') continue;
|
|
36
|
+
const row: Row = {};
|
|
37
|
+
for (const [name, slot] of el.slots) row[name] = slot.peek();
|
|
38
|
+
rows.push(row);
|
|
39
|
+
}
|
|
40
|
+
m.state.rows = rows;
|
|
41
|
+
m.slot('count').assign(rows.length);
|
|
42
|
+
m.methods.set('get', (i) => rowsOf(m)[Number(i)]);
|
|
43
|
+
m.methods.set('append', (row) => mutate(m, (r) => r.push(row as Row)));
|
|
44
|
+
m.methods.set('insert', (i, row) =>
|
|
45
|
+
mutate(m, (r) => r.splice(Number(i), 0, row as Row)),
|
|
46
|
+
);
|
|
47
|
+
m.methods.set('remove', (i, n) =>
|
|
48
|
+
mutate(m, (r) => r.splice(Number(i), n === undefined ? 1 : Number(n))),
|
|
49
|
+
);
|
|
50
|
+
m.methods.set('set', (i, row) =>
|
|
51
|
+
mutate(m, (r) => {
|
|
52
|
+
r[Number(i)] = { ...r[Number(i)], ...(row as Row) };
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
m.methods.set('setProperty', (i, name, value) =>
|
|
56
|
+
mutate(m, (r) => {
|
|
57
|
+
r[Number(i)] = { ...r[Number(i)], [String(name)]: value };
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
m.methods.set('clear', () => mutate(m, (r) => (r.length = 0)));
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export interface ResolvedModel {
|
|
66
|
+
rows: unknown[];
|
|
67
|
+
/** Present for a ListModel: the revision slot a consumer watches. */
|
|
68
|
+
revSlot: Slot | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** number | array | ListModel → rows a Repeater/ListView can walk. */
|
|
72
|
+
export function resolveModel(value: unknown): ResolvedModel {
|
|
73
|
+
const inst = instanceOf(value);
|
|
74
|
+
if (inst && inst.typeInfo.name === 'ListModel') {
|
|
75
|
+
return { rows: rowsOf(inst), revSlot: inst.slot('__rev') };
|
|
76
|
+
}
|
|
77
|
+
if (typeof value === 'number') {
|
|
78
|
+
return {
|
|
79
|
+
rows: Array.from({ length: Math.max(0, Math.floor(value)) }, (_, i) => i),
|
|
80
|
+
revSlot: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(value)) return { rows: value, revSlot: null };
|
|
84
|
+
return { rows: [], revSlot: null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The context properties a delegate row sees: index, modelData, and — for
|
|
88
|
+
* object rows — each role by name, the way QML exposes ListModel roles. */
|
|
89
|
+
export function delegateExtras(
|
|
90
|
+
rows: unknown[],
|
|
91
|
+
index: number,
|
|
92
|
+
): Record<string, unknown> {
|
|
93
|
+
const data = rows[index];
|
|
94
|
+
const extras: Record<string, unknown> = { index, modelData: data };
|
|
95
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
96
|
+
for (const [k, v] of Object.entries(data as Row)) {
|
|
97
|
+
if (!(k in extras)) extras[k] = v;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return extras;
|
|
101
|
+
}
|