buninu-browser 0.4.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 +22 -1
- package/frontends/wayland/README.md +50 -21
- package/frontends/wayland/index.js +14 -0
- package/frontends/wayland/input.js +24 -5
- package/frontends/wayland/urlbar.js +9 -5
- package/lib/audio/shared-connection.js +31 -0
- package/lib/cdp-server.js +15 -2
- package/lib/headless-shell.js +7 -4
- package/lib/paint/display-list.js +2 -1
- package/lib/qml/README.md +456 -0
- package/lib/qml/builtins.js +76 -5
- package/lib/qml/canvas.js +76 -0
- package/lib/qml/document.js +47 -24
- package/lib/qml/fonts.js +69 -0
- package/lib/qml/host.js +28 -0
- package/lib/qml/loader.js +7 -3
- package/lib/qml/models.js +1 -1
- package/lib/qml/reactive.js +26 -3
- package/lib/qml/runtime.js +263 -41
- package/lib/qml/shader/fragment.js +83 -0
- package/lib/qml/shader/qsb.js +27 -0
- package/lib/qml/shader/renderer.js +39 -0
- package/lib/qml/shader/runtime.js +61 -0
- package/lib/qml/shapes.js +9 -1
- package/lib/qml/shortcuts.js +65 -0
- package/lib/qml/textures.js +4 -1
- package/lib/qml/window-constraints.js +16 -0
- package/lib/renderer/bindings.js +5 -1
- package/lib/renderer/canvas-2d.js +16 -0
- package/lib/renderer/document-selection.js +19 -0
- package/lib/renderer/host.js +14 -11
- package/lib/renderer/main.js +2 -0
- package/lib/renderer/page-realm.js +4 -9
- package/lib/renderer/page-renderer.js +273 -35
- package/lib/renderer/pointer-move.js +5 -3
- package/lib/renderer/render-interval.js +9 -0
- package/lib/style/computed-style.js +8 -2
- package/package.json +1 -1
- package/test/audio-connection-lifetime.test.js +46 -0
- package/test/canvas-paint.test.js +8 -0
- package/test/computed-style.test.js +12 -0
- package/test/document-selection.test.js +27 -0
- package/test/double-click.test.js +85 -0
- package/test/fixtures/canvas-damage.js +40 -0
- package/test/fixtures/fake-renderer.js +1 -0
- package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
- package/test/fixtures/omastorm-helpers/check.mjs +56 -0
- package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
- package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
- package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
- package/test/fixtures/pointer-paint-batching.js +23 -0
- package/test/fixtures/qml-fonts/main.qml +10 -0
- package/test/fixtures/qml-locale/check.mjs +6 -0
- package/test/fixtures/qml-locale/main.qml +8 -0
- package/test/fixtures/qml-shader/grab.qml +14 -0
- package/test/fixtures/qml-shader/gradient.frag +5 -0
- package/test/fixtures/qml-shader/main.qml +14 -0
- package/test/fixtures/qml-shader/texture-grab.qml +25 -0
- package/test/fixtures/qml-shader/texture-size.frag +5 -0
- package/test/fixtures/qml-shader-check.mjs +24 -0
- package/test/fixtures/title-no-paint.js +20 -0
- package/test/manual-double-click.html +21 -0
- package/test/manual-mouse-buttons.html +18 -0
- package/test/manual-omastorm-engine.mjs +19 -0
- package/test/manual-omastorm-shaders.mjs +28 -0
- package/test/manual-omastorm-software.mjs +25 -0
- package/test/manual-pointer-scroll.html +43 -0
- package/test/pointer-drag.test.js +104 -0
- package/test/qml-canvas.test.js +21 -0
- package/test/qml-connections-timing.test.js +22 -0
- package/test/qml-controls-window.test.js +182 -0
- package/test/qml-fonts.test.js +31 -0
- package/test/qml-grid-multiline.test.js +48 -0
- package/test/qml-locale.test.js +45 -0
- package/test/qml-nonvisual-visible.test.js +19 -0
- package/test/qml-shader-runtime.test.js +6 -0
- package/test/qml-shortcuts.test.js +39 -0
- package/test/qml-socket.test.js +60 -0
- package/test/qml-software-shader.test.js +55 -0
- package/test/qml-window-update.test.js +36 -0
- package/test/render-interval.test.js +33 -0
- package/test/title-no-paint.test.js +6 -0
- package/test/wayland-scroll.test.js +40 -0
- package/test/wayland-urlbar.test.js +23 -0
package/lib/qml/runtime.js
CHANGED
|
@@ -9,6 +9,9 @@ export function createQmlRuntime(
|
|
|
9
9
|
createQmlImages,
|
|
10
10
|
createQmlTextures,
|
|
11
11
|
createQmlMedia,
|
|
12
|
+
createQmlCanvas,
|
|
13
|
+
createQmlShortcuts,
|
|
14
|
+
createQmlShaderEffects,
|
|
12
15
|
) {
|
|
13
16
|
const { untrack } = reactive;
|
|
14
17
|
// Qt.binding may also replace an unbound/default property (e.g. Item.visible).
|
|
@@ -20,9 +23,10 @@ export function createQmlRuntime(
|
|
|
20
23
|
untrack(start);
|
|
21
24
|
return true;
|
|
22
25
|
});
|
|
26
|
+
const socketStates = new WeakMap();
|
|
23
27
|
let disposers = null;
|
|
24
|
-
const effect = (fn) => {
|
|
25
|
-
const stop = reactive.effect(fn);
|
|
28
|
+
const effect = (fn, options) => {
|
|
29
|
+
const stop = reactive.effect(fn, options);
|
|
26
30
|
disposers?.push(stop);
|
|
27
31
|
return stop;
|
|
28
32
|
};
|
|
@@ -82,12 +86,13 @@ export function createQmlRuntime(
|
|
|
82
86
|
'ShellRoot',
|
|
83
87
|
'Scale',
|
|
84
88
|
'Translate',
|
|
85
|
-
'Repeater',
|
|
89
|
+
'Shortcut', 'Instantiator', 'Repeater',
|
|
86
90
|
'TapHandler',
|
|
87
91
|
'HoverHandler',
|
|
88
92
|
'QtObject',
|
|
89
93
|
'Singleton',
|
|
90
94
|
'FileView',
|
|
95
|
+
'Socket',
|
|
91
96
|
'Process',
|
|
92
97
|
'Timer',
|
|
93
98
|
'Connections',
|
|
@@ -190,6 +195,24 @@ export function createQmlRuntime(
|
|
|
190
195
|
}[edge] ?? 0)
|
|
191
196
|
);
|
|
192
197
|
};
|
|
198
|
+
// Layout preferred size: explicit hint, then positive implicit size, then
|
|
199
|
+
// the initial item size. Never feed a layout-assigned stretched size back
|
|
200
|
+
// into its own preference (Qt also caches this last-resort size hint).
|
|
201
|
+
const layoutFallbackSizes = new WeakMap();
|
|
202
|
+
const layoutPreferred = (item, suffix) => {
|
|
203
|
+
const hint = item.Layout['preferred' + suffix];
|
|
204
|
+
if (hint >= 0) return hint;
|
|
205
|
+
const implicit = item['implicit' + suffix];
|
|
206
|
+
if (implicit > 0) return implicit;
|
|
207
|
+
let saved = layoutFallbackSizes.get(item);
|
|
208
|
+
if (!saved) layoutFallbackSizes.set(item, saved = state({}));
|
|
209
|
+
if (!(suffix in saved)) {
|
|
210
|
+
const initial = untrack(() => item[suffix === 'Width' ? 'width' : 'height']);
|
|
211
|
+
saved[suffix] = Math.max(0, Number(initial) || 0);
|
|
212
|
+
}
|
|
213
|
+
return saved[suffix] || 0;
|
|
214
|
+
};
|
|
215
|
+
|
|
193
216
|
const dimension = (v, key, topLevel = false) => {
|
|
194
217
|
const a = v.anchors,
|
|
195
218
|
start = key === 'width' ? 'left' : 'top',
|
|
@@ -202,7 +225,7 @@ export function createQmlRuntime(
|
|
|
202
225
|
margin(v, start) -
|
|
203
226
|
margin(v, end)
|
|
204
227
|
);
|
|
205
|
-
if (owners.get(v)?.type === 'FloatingWindow') return viewport[key];
|
|
228
|
+
if (owners.get(v)?.type === 'FloatingWindow') return Math.max(v.minimumSize?.[key]||0,Math.min(v.maximumSize?.[key]>0?v.maximumSize[key]:Infinity,viewport[key]));
|
|
206
229
|
return (
|
|
207
230
|
v[key === 'width' ? 'implicitWidth' : 'implicitHeight'] ??
|
|
208
231
|
(topLevel ? viewport[key] : 0)
|
|
@@ -507,7 +530,9 @@ export function createQmlRuntime(
|
|
|
507
530
|
disposeScope(stops);
|
|
508
531
|
};
|
|
509
532
|
try {
|
|
510
|
-
|
|
533
|
+
// Imperative construction must not subscribe the caller binding to all
|
|
534
|
+
// properties read while initializing the newly created object.
|
|
535
|
+
value = untrack(() => factory(parent, properties));
|
|
511
536
|
value.destroy = (delay = 0) =>
|
|
512
537
|
delay > 0 ? setTimeout(destroy, delay) : destroy();
|
|
513
538
|
} catch (error) {
|
|
@@ -639,6 +664,10 @@ export function createQmlRuntime(
|
|
|
639
664
|
// QML signal handlers execute with their declaring object as `this`,
|
|
640
665
|
// including handlers connected later or merged with inherited handlers.
|
|
641
666
|
fn = fn.bind(value);
|
|
667
|
+
if(name==='Window.onWindowChanged'){
|
|
668
|
+
initial.push(()=>effect(()=>{const window=value.Window.window;untrack(()=>fn(window));}));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
642
671
|
let map = handlers.get(value);
|
|
643
672
|
if (!map) handlers.set(value, (map = new Map()));
|
|
644
673
|
const previous = map.get(name);
|
|
@@ -651,8 +680,8 @@ export function createQmlRuntime(
|
|
|
651
680
|
};
|
|
652
681
|
const emit = (value, name, ...args) =>
|
|
653
682
|
untrack(() => handlers.get(value)?.get(name)?.(...args));
|
|
683
|
+
const pendingPropertyHandlers = new Map();
|
|
654
684
|
const start = () => {
|
|
655
|
-
const propertyHandlers = [];
|
|
656
685
|
if (!nodes.some((node) => started.has(node.value)))
|
|
657
686
|
host?.hyprEvents((event) => Hyprland.rawEvent(event));
|
|
658
687
|
let pending = initial.splice(0),
|
|
@@ -702,7 +731,7 @@ export function createQmlRuntime(
|
|
|
702
731
|
// Initial bindings can change a parent property before child built-in
|
|
703
732
|
// methods exist. Install change observers after this component's nodes
|
|
704
733
|
// have started, while retaining the pre-binding value for notification.
|
|
705
|
-
|
|
734
|
+
pendingPropertyHandlers.set(v, () => {
|
|
706
735
|
const previous = disposers;
|
|
707
736
|
disposers = node.scope;
|
|
708
737
|
try {
|
|
@@ -765,6 +794,17 @@ export function createQmlRuntime(
|
|
|
765
794
|
(event.ctrlKey ? Qt.ControlModifier : 0) |
|
|
766
795
|
(event.altKey ? Qt.AltModifier : 0) |
|
|
767
796
|
(event.metaKey ? Qt.MetaModifier : 0),
|
|
797
|
+
matches:standard=>{
|
|
798
|
+
const key=String(event.key).toLowerCase();
|
|
799
|
+
if(event.altKey)return false;
|
|
800
|
+
const command=event.ctrlKey||event.metaKey;
|
|
801
|
+
return standard===10 ? (command&&!event.shiftKey&&key==='v')||(event.shiftKey&&!command&&key==='insert')
|
|
802
|
+
: standard===9 ? command&&!event.shiftKey&&key==='c'
|
|
803
|
+
: standard===8 ? command&&!event.shiftKey&&key==='x'
|
|
804
|
+
: standard===26 ? command&&!event.shiftKey&&key==='a'
|
|
805
|
+
: standard===11 ? command&&!event.shiftKey&&key==='z'
|
|
806
|
+
: standard===12 ? command&&(key==='y'||event.shiftKey&&key==='z') : false;
|
|
807
|
+
},
|
|
768
808
|
isAutoRepeat: !!event.repeat,
|
|
769
809
|
accepted: !!callbacks?.has(specific),
|
|
770
810
|
};
|
|
@@ -877,7 +917,7 @@ export function createQmlRuntime(
|
|
|
877
917
|
finally { areaTarget.style.pointerEvents = old; }
|
|
878
918
|
}
|
|
879
919
|
});
|
|
880
|
-
for (const type of ['mouseup', 'click', 'dblclick'])
|
|
920
|
+
for (const type of ['mouseup', 'click', 'auxclick', 'dblclick'])
|
|
881
921
|
listen(areaTarget, type, event => { if (passTarget) forward(event, passTarget); });
|
|
882
922
|
listen(document, 'mousemove', (event) => {
|
|
883
923
|
if (v.pressed || v.hoverEnabled)
|
|
@@ -888,7 +928,7 @@ export function createQmlRuntime(
|
|
|
888
928
|
v.pressed = false;
|
|
889
929
|
emit(v, 'onReleased', update(event));
|
|
890
930
|
});
|
|
891
|
-
|
|
931
|
+
for (const type of ['click', 'auxclick']) listen(areaTarget, type, (event) => {
|
|
892
932
|
if (!passTarget && enabled(v) && v.acceptedButtons & button(event))
|
|
893
933
|
emit(v, 'onClicked', update(event));
|
|
894
934
|
});
|
|
@@ -1185,16 +1225,21 @@ export function createQmlRuntime(
|
|
|
1185
1225
|
}
|
|
1186
1226
|
});
|
|
1187
1227
|
}
|
|
1188
|
-
if (type === 'Repeater') {
|
|
1228
|
+
if (type === 'Repeater' || type === 'Instantiator') {
|
|
1189
1229
|
let instances = [];
|
|
1190
1230
|
const clear = () => {
|
|
1191
|
-
for (const instance of instances
|
|
1231
|
+
for (const [index, instance] of instances.entries()) {
|
|
1232
|
+
if (type === 'Instantiator') emit(v, 'onObjectRemoved', index, instance.value);
|
|
1233
|
+
instance.dispose();
|
|
1234
|
+
}
|
|
1192
1235
|
instances = [];
|
|
1193
1236
|
};
|
|
1194
1237
|
disposers?.push(clear);
|
|
1195
|
-
v.
|
|
1238
|
+
if (type === 'Instantiator') v.objectAt = index => instances[index]?.value ?? null;
|
|
1239
|
+
else v.itemAt = (index) => instances[index]?.value ?? null;
|
|
1196
1240
|
effect(() => {
|
|
1197
|
-
|
|
1241
|
+
if (type === 'Instantiator' && v.asynchronous) throw Error('Instantiator asynchronous creation is not implemented');
|
|
1242
|
+
const model = type === 'Instantiator' && !v.active ? [] : (v.model ?? []);
|
|
1198
1243
|
if (
|
|
1199
1244
|
typeof model === 'number' &&
|
|
1200
1245
|
(!Number.isFinite(model) || model < 0 || model > 1000)
|
|
@@ -1206,7 +1251,7 @@ export function createQmlRuntime(
|
|
|
1206
1251
|
{ length: Math.max(0, Math.floor(model)) },
|
|
1207
1252
|
(_, i) => i,
|
|
1208
1253
|
)
|
|
1209
|
-
: model;
|
|
1254
|
+
: models.rows(model) ?? model;
|
|
1210
1255
|
if (!Array.isArray(data) || data.length > 1000)
|
|
1211
1256
|
throw Error('Repeater supports arrays or counts up to 1000');
|
|
1212
1257
|
untrack(() => {
|
|
@@ -1236,6 +1281,10 @@ export function createQmlRuntime(
|
|
|
1236
1281
|
});
|
|
1237
1282
|
}
|
|
1238
1283
|
v.count = instances.length;
|
|
1284
|
+
if (type === 'Instantiator') {
|
|
1285
|
+
v.object = instances[0]?.value ?? null;
|
|
1286
|
+
for (const [index, instance] of instances.entries()) emit(v, 'onObjectAdded', index, instance.value);
|
|
1287
|
+
}
|
|
1239
1288
|
});
|
|
1240
1289
|
});
|
|
1241
1290
|
} else if (type === 'Loader') {
|
|
@@ -1377,7 +1426,7 @@ export function createQmlRuntime(
|
|
|
1377
1426
|
v.pressed = false;
|
|
1378
1427
|
}
|
|
1379
1428
|
});
|
|
1380
|
-
|
|
1429
|
+
for (const type of ['click', 'auxclick']) listen(target, type, (event) => {
|
|
1381
1430
|
if (
|
|
1382
1431
|
!v.enabled ||
|
|
1383
1432
|
!enabled(parents.get(v)) ||
|
|
@@ -1469,6 +1518,43 @@ export function createQmlRuntime(
|
|
|
1469
1518
|
unwatch = host.watch(path, () => emit(v, 'onFileChanged'));
|
|
1470
1519
|
}
|
|
1471
1520
|
});
|
|
1521
|
+
} else if (type === 'Socket') {
|
|
1522
|
+
const status = socketStates.get(v);
|
|
1523
|
+
let connection = null, generation = 0, pending = '';
|
|
1524
|
+
const stop = () => { generation++; connection?.(); connection = null; pending = ''; status.actual = false; };
|
|
1525
|
+
disposers?.push(stop);
|
|
1526
|
+
v.write = text => { if (status.actual) connection?.write(text); };
|
|
1527
|
+
v.flush = () => connection?.flush();
|
|
1528
|
+
effect(() => {
|
|
1529
|
+
const wanted = status.wanted, path = v.path;
|
|
1530
|
+
if (!wanted) { untrack(stop); return; }
|
|
1531
|
+
if (!path || connection) return;
|
|
1532
|
+
untrack(() => {
|
|
1533
|
+
const current = ++generation;
|
|
1534
|
+
connection = host.socket(path, {
|
|
1535
|
+
open() { if (current === generation) status.actual = true; },
|
|
1536
|
+
data(chunk) {
|
|
1537
|
+
if (current !== generation) return;
|
|
1538
|
+
pending += chunk;
|
|
1539
|
+
const parser = v.parser;
|
|
1540
|
+
if (!parser) { pending = ''; return; }
|
|
1541
|
+
const marker = String(parser.splitMarker ?? '\n');
|
|
1542
|
+
if (!marker) { const text = pending; pending = ''; emit(parser, 'onRead', text); return; }
|
|
1543
|
+
let at;
|
|
1544
|
+
while ((at = pending.indexOf(marker)) >= 0) {
|
|
1545
|
+
const text = pending.slice(0, at); pending = pending.slice(at + marker.length);
|
|
1546
|
+
emit(parser, 'onRead', text);
|
|
1547
|
+
if (current !== generation) return;
|
|
1548
|
+
}
|
|
1549
|
+
},
|
|
1550
|
+
error(error) { if (current === generation) emit(v, 'onError', error); },
|
|
1551
|
+
close() {
|
|
1552
|
+
if (current !== generation) return;
|
|
1553
|
+
connection = null; pending = ''; status.actual = false; status.wanted = false;
|
|
1554
|
+
},
|
|
1555
|
+
});
|
|
1556
|
+
});
|
|
1557
|
+
});
|
|
1472
1558
|
} else if (type === 'Process') {
|
|
1473
1559
|
let child = null,
|
|
1474
1560
|
generation = 0;
|
|
@@ -1646,7 +1732,10 @@ export function createQmlRuntime(
|
|
|
1646
1732
|
disposers = previousScope;
|
|
1647
1733
|
}
|
|
1648
1734
|
}
|
|
1649
|
-
for (const install of
|
|
1735
|
+
for (const [value, install] of [...pendingPropertyHandlers]) {
|
|
1736
|
+
pendingPropertyHandlers.delete(value);
|
|
1737
|
+
if (!destroyedNodes.has(value)) install();
|
|
1738
|
+
}
|
|
1650
1739
|
reactive.flush();
|
|
1651
1740
|
};
|
|
1652
1741
|
const cssColor = (v) =>
|
|
@@ -1682,6 +1771,8 @@ export function createQmlRuntime(
|
|
|
1682
1771
|
].includes(type)
|
|
1683
1772
|
? 'black'
|
|
1684
1773
|
: 'transparent',
|
|
1774
|
+
smooth:true,
|
|
1775
|
+
antialiasing:true, // Existing CSS/CanvasKit geometry is antialiased.
|
|
1685
1776
|
radius: 0,
|
|
1686
1777
|
opacity: 1,
|
|
1687
1778
|
visible: true,
|
|
@@ -1862,10 +1953,13 @@ export function createQmlRuntime(
|
|
|
1862
1953
|
b = elements.get(item)?.getBoundingClientRect() ?? { x: 0, y: 0 };
|
|
1863
1954
|
return { x: b.x - a.x + x, y: b.y - a.y + y };
|
|
1864
1955
|
};
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1956
|
+
value.Window=state({window:null});
|
|
1957
|
+
effect(()=>{
|
|
1958
|
+
structure.version;
|
|
1959
|
+
let windowValue=type==='FloatingWindow'?value:parents.get(value);
|
|
1960
|
+
while(windowValue&&owners.get(windowValue)?.type!=='FloatingWindow')windowValue=parents.get(windowValue);
|
|
1961
|
+
value.Window.window=windowValue??null;
|
|
1962
|
+
});
|
|
1869
1963
|
let viewValue = parent;
|
|
1870
1964
|
while (
|
|
1871
1965
|
viewValue &&
|
|
@@ -1879,36 +1973,54 @@ export function createQmlRuntime(
|
|
|
1879
1973
|
view: owners.get(viewValue)?.type === 'GridView' ? viewValue : null,
|
|
1880
1974
|
};
|
|
1881
1975
|
if (type === 'FloatingWindow') {
|
|
1976
|
+
value.minimumSize=state({width:0,height:0});value.maximumSize=state({width:0,height:0});
|
|
1977
|
+
value.sceneGraphInvalidated=signal();
|
|
1978
|
+
disposers?.push(()=>value.sceneGraphInvalidated());
|
|
1979
|
+
effect(()=>{element.setAttribute('data-qml-window-min',JSON.stringify([value.minimumSize.width,value.minimumSize.height]));element.setAttribute('data-qml-window-max',JSON.stringify([value.maximumSize.width,value.maximumSize.height]));});
|
|
1882
1980
|
value.sceneGraphError = signal();
|
|
1883
1981
|
value.afterAnimating = signal();
|
|
1884
1982
|
value.afterSynchronizing = signal();
|
|
1885
1983
|
value.frameSwapped = signal();
|
|
1886
1984
|
value.activeFocusItem = null;
|
|
1887
1985
|
let frame,
|
|
1888
|
-
hadFrame = false
|
|
1986
|
+
hadFrame = false,
|
|
1987
|
+
updateRequested = false,
|
|
1988
|
+
disposed = false;
|
|
1989
|
+
const schedule = () => {
|
|
1990
|
+
if (!disposed && frame === undefined) frame = requestAnimationFrame(tick);
|
|
1991
|
+
};
|
|
1992
|
+
value.update = () => {
|
|
1993
|
+
if (disposed) return;
|
|
1994
|
+
updateRequested = true;
|
|
1995
|
+
schedule();
|
|
1996
|
+
};
|
|
1889
1997
|
const tick = () => {
|
|
1890
1998
|
frame = undefined;
|
|
1999
|
+
if (disposed) return;
|
|
1891
2000
|
if (
|
|
2001
|
+
!updateRequested &&
|
|
1892
2002
|
!value.afterAnimating.listenerCount &&
|
|
1893
2003
|
!value.afterSynchronizing.listenerCount &&
|
|
1894
2004
|
!value.frameSwapped.listenerCount
|
|
1895
2005
|
)
|
|
1896
2006
|
return;
|
|
2007
|
+
updateRequested = false;
|
|
1897
2008
|
if (hadFrame) value.frameSwapped();
|
|
1898
2009
|
value.afterAnimating();
|
|
1899
2010
|
textures.flush();
|
|
1900
2011
|
value.afterSynchronizing();
|
|
1901
2012
|
hadFrame = true;
|
|
1902
|
-
|
|
2013
|
+
schedule();
|
|
1903
2014
|
};
|
|
1904
2015
|
for (const event of [value.afterAnimating, value.afterSynchronizing, value.frameSwapped]) {
|
|
1905
2016
|
const connect = event.connect;
|
|
1906
2017
|
event.connect = (fn) => {
|
|
1907
2018
|
connect(fn);
|
|
1908
|
-
|
|
2019
|
+
schedule();
|
|
1909
2020
|
};
|
|
1910
2021
|
}
|
|
1911
2022
|
disposers?.push(() => {
|
|
2023
|
+
disposed = true;
|
|
1912
2024
|
if (frame !== undefined) cancelAnimationFrame(frame);
|
|
1913
2025
|
});
|
|
1914
2026
|
|
|
@@ -2180,10 +2292,13 @@ export function createQmlRuntime(
|
|
|
2180
2292
|
rowSpacing: null,
|
|
2181
2293
|
columnSpacing: null,
|
|
2182
2294
|
});
|
|
2295
|
+
if (type === 'PathMultiline') value.paths = [];
|
|
2296
|
+
if (type === 'GridLayout') Object.assign(value, {columns:0,rows:0,columnSpacing:5,rowSpacing:5});
|
|
2183
2297
|
if (type === 'PathSvg') value.path = '';
|
|
2184
2298
|
if (type === 'LinearGradient')
|
|
2185
2299
|
Object.assign(value, { x1: 0, y1: 0, x2: 0, y2: 0 });
|
|
2186
2300
|
|
|
2301
|
+
if (type === 'Instantiator') Object.assign(value, {model:1,count:0,active:true,asynchronous:false,object:null,objectAt:()=>null});
|
|
2187
2302
|
if (type === 'Repeater')
|
|
2188
2303
|
Object.assign(value, { model: [], count: 0, itemAt: () => null });
|
|
2189
2304
|
if (type === 'MouseArea')
|
|
@@ -2274,6 +2389,12 @@ export function createQmlRuntime(
|
|
|
2274
2389
|
}
|
|
2275
2390
|
if (type === 'StdioCollector') Object.assign(value, { text: '', data: new ArrayBuffer(0) });
|
|
2276
2391
|
if (type === 'SplitParser') Object.assign(value, { splitMarker: '\n' });
|
|
2392
|
+
if (type === 'Socket') {
|
|
2393
|
+
const status = state({ wanted: false, actual: false });
|
|
2394
|
+
socketStates.set(value, status);
|
|
2395
|
+
Object.assign(value, { path: '', parser: null, error: signal() });
|
|
2396
|
+
alias(value, 'connected', () => status.actual, connected => { status.wanted = !!connected; }, false);
|
|
2397
|
+
}
|
|
2277
2398
|
if (type === 'Process')
|
|
2278
2399
|
Object.assign(value, {
|
|
2279
2400
|
running: false,
|
|
@@ -2369,9 +2490,31 @@ export function createQmlRuntime(
|
|
|
2369
2490
|
}
|
|
2370
2491
|
media.defaults(value,type);
|
|
2371
2492
|
textures.make(value, type, element);
|
|
2493
|
+
canvases.make(value, type, element);
|
|
2494
|
+
shaders.make(value,type,element);
|
|
2495
|
+
shortcuts.make(value, type);
|
|
2372
2496
|
shapes.make(value, type, element, parent);
|
|
2373
2497
|
models.make(value, type, element, parent);
|
|
2374
2498
|
if (nonvisual.has(type)) return value;
|
|
2499
|
+
const grabFrames=new Set();
|
|
2500
|
+
disposers?.push(()=>{for(const frame of grabFrames)cancelAnimationFrame(frame);grabFrames.clear();});
|
|
2501
|
+
value.grabToImage=(callback,size)=>{
|
|
2502
|
+
if(!host||typeof callback!=='function'||!element.isConnected)return false;
|
|
2503
|
+
const w=Math.ceil(size?.width??value.width),h=Math.ceil(size?.height??value.height);
|
|
2504
|
+
if(!(w>0&&h>0)||w>8192||h>8192||w*h>16777216)return false;
|
|
2505
|
+
const frame=requestAnimationFrame(()=>{
|
|
2506
|
+
grabFrames.delete(frame);
|
|
2507
|
+
if(!element.isConnected)return;
|
|
2508
|
+
const canvas=document.createElement('canvas');canvas.width=w;canvas.height=h;
|
|
2509
|
+
try{
|
|
2510
|
+
textures.flush();
|
|
2511
|
+
canvas.getContext('2d').drawImage(element,0,0,w,h);
|
|
2512
|
+
callback({url:canvas.toDataURL('image/png')});
|
|
2513
|
+
}finally{canvas.width=canvas.height=1;}
|
|
2514
|
+
});
|
|
2515
|
+
grabFrames.add(frame);
|
|
2516
|
+
return true;
|
|
2517
|
+
};
|
|
2375
2518
|
focusElements.set(element, value);
|
|
2376
2519
|
listen(element, 'focus', () => activateFocus(value));
|
|
2377
2520
|
listen(element, 'blur', (event) =>
|
|
@@ -2487,6 +2630,35 @@ export function createQmlRuntime(
|
|
|
2487
2630
|
value.implicitWidth = w + value.leftPadding + value.rightPadding;
|
|
2488
2631
|
value.implicitHeight = h + value.topPadding + value.bottomPadding;
|
|
2489
2632
|
});
|
|
2633
|
+
if (type === 'GridLayout')
|
|
2634
|
+
effect(() => {
|
|
2635
|
+
const items=childrenOf(value).filter(c=>c.visible!==false);
|
|
2636
|
+
const cols=Math.max(1, value.columns || (value.rows ? Math.ceil(items.length/value.rows) : items.length));
|
|
2637
|
+
const rows=Math.ceil(items.length/cols), widths=Array(cols).fill(0), heights=Array(rows).fill(0);
|
|
2638
|
+
const preferred=(c,axis)=>Math.max(c.Layout['minimum'+axis],Math.min(c.Layout['maximum'+axis],layoutPreferred(c,axis)));
|
|
2639
|
+
items.forEach((c,i)=>{widths[i%cols]=Math.max(widths[i%cols],preferred(c,'Width'));heights[Math.floor(i/cols)]=Math.max(heights[Math.floor(i/cols)],preferred(c,'Height'));});
|
|
2640
|
+
value.implicitWidth=widths.reduce((a,b)=>a+b,0)+Math.max(0,cols-1)*value.columnSpacing;
|
|
2641
|
+
value.implicitHeight=heights.reduce((a,b)=>a+b,0)+Math.max(0,rows-1)*value.rowSpacing;
|
|
2642
|
+
for(const [sizes,axis,available,gap] of [[widths,'Width',value.width,value.columnSpacing],[heights,'Height',value.height,value.rowSpacing]]){
|
|
2643
|
+
const tracks=new Set(items.flatMap((c,i)=>c.Layout['fill'+axis]?[axis==='Width'?i%cols:Math.floor(i/cols)]:[]));
|
|
2644
|
+
let extra=available-sizes.reduce((a,b)=>a+b,0)-Math.max(0,sizes.length-1)*gap;
|
|
2645
|
+
const minimum=sizes.map((_,index)=>Math.max(0,...items.filter((c,i)=>(axis==='Width'?i%cols:Math.floor(i/cols))===index).map(c=>c.Layout['minimum'+axis])));
|
|
2646
|
+
for(let pass=0;pass<sizes.length && Math.abs(extra)>0.01;pass++){
|
|
2647
|
+
const adjustable=[...tracks].filter(index=>extra>0||sizes[index]>minimum[index]);
|
|
2648
|
+
if(!adjustable.length)break;
|
|
2649
|
+
const share=extra/adjustable.length;let used=0;
|
|
2650
|
+
for(const index of adjustable){const next=Math.max(minimum[index],sizes[index]+share);used+=next-sizes[index];sizes[index]=next;}
|
|
2651
|
+
extra-=used;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
items.forEach((c,i)=>{
|
|
2655
|
+
const col=i%cols,row=Math.floor(i/cols),l=c.Layout;
|
|
2656
|
+
c.width=l.fillWidth?Math.max(l.minimumWidth,Math.min(l.maximumWidth,widths[col])):preferred(c,'Width');
|
|
2657
|
+
c.height=l.fillHeight?Math.max(l.minimumHeight,Math.min(l.maximumHeight,heights[row])):preferred(c,'Height');
|
|
2658
|
+
c.x=widths.slice(0,col).reduce((a,b)=>a+b+value.columnSpacing,0)+(l.alignment&2?widths[col]-c.width:l.alignment&4?(widths[col]-c.width)/2:0);
|
|
2659
|
+
c.y=heights.slice(0,row).reduce((a,b)=>a+b+value.rowSpacing,0)+(l.alignment&64?heights[row]-c.height:l.alignment&128?(heights[row]-c.height)/2:0);
|
|
2660
|
+
});
|
|
2661
|
+
});
|
|
2490
2662
|
if (type === 'RowLayout' || type === 'ColumnLayout')
|
|
2491
2663
|
effect(() => {
|
|
2492
2664
|
const horizontal = type === 'RowLayout',
|
|
@@ -2500,10 +2672,7 @@ export function createQmlRuntime(
|
|
|
2500
2672
|
gap = value.spacing;
|
|
2501
2673
|
const boxes = children.map((c) => {
|
|
2502
2674
|
const l = c.Layout;
|
|
2503
|
-
const preferred =
|
|
2504
|
-
l['preferred' + suffix] >= 0
|
|
2505
|
-
? l['preferred' + suffix]
|
|
2506
|
-
: c['implicit' + suffix];
|
|
2675
|
+
const preferred = layoutPreferred(c, suffix);
|
|
2507
2676
|
return {
|
|
2508
2677
|
c,
|
|
2509
2678
|
l,
|
|
@@ -2544,10 +2713,7 @@ export function createQmlRuntime(
|
|
|
2544
2713
|
);
|
|
2545
2714
|
for (const b of boxes) {
|
|
2546
2715
|
const { c, l } = b;
|
|
2547
|
-
const preferredCross =
|
|
2548
|
-
l['preferred' + opposite] >= 0
|
|
2549
|
-
? l['preferred' + opposite]
|
|
2550
|
-
: c['implicit' + opposite];
|
|
2716
|
+
const preferredCross = layoutPreferred(c, opposite);
|
|
2551
2717
|
const size = Math.max(
|
|
2552
2718
|
l['minimum' + opposite],
|
|
2553
2719
|
Math.min(
|
|
@@ -2568,7 +2734,10 @@ export function createQmlRuntime(
|
|
|
2568
2734
|
: alignment & (horizontal ? 128 : 4)
|
|
2569
2735
|
? (value[cross] - size) / 2
|
|
2570
2736
|
: 0;
|
|
2571
|
-
|
|
2737
|
+
// Intrinsic size must not depend on the space allocated by a parent
|
|
2738
|
+
// layout: that feeds stretched sizes back into sibling allocation.
|
|
2739
|
+
const intrinsic = Math.max(l['minimum' + opposite], Math.min(l['maximum' + opposite], preferredCross));
|
|
2740
|
+
maxCross = Math.max(maxCross, intrinsic + (horizontal && alignment & 256 ? baseline - c.baselineOffset : 0));
|
|
2572
2741
|
}
|
|
2573
2742
|
value['implicit' + suffix] = preferred;
|
|
2574
2743
|
value['implicit' + opposite] = maxCross;
|
|
@@ -2636,13 +2805,19 @@ export function createQmlRuntime(
|
|
|
2636
2805
|
value.lineHeightMode === 1
|
|
2637
2806
|
? value.lineHeight
|
|
2638
2807
|
: line * value.lineHeight;
|
|
2808
|
+
// A custom Control contentItem supplies its natural size. Its actual
|
|
2809
|
+
// width is assigned by the control, so reading width here would create
|
|
2810
|
+
// a layout cycle and collapse adjacent RowLayout items onto its text.
|
|
2811
|
+
const custom = type === 'Button' && value.contentItem;
|
|
2812
|
+
const padding = Math.max(0, Number(value.padding) || 0);
|
|
2639
2813
|
if (!explicit.includes('implicitWidth'))
|
|
2640
|
-
value.implicitWidth =
|
|
2641
|
-
|
|
2814
|
+
value.implicitWidth = custom
|
|
2815
|
+
? Math.max(value.background?.implicitWidth || 0, (custom.implicitWidth || 0) + padding * 2)
|
|
2816
|
+
: natural + (control ? 22 : value.leftPadding + value.rightPadding);
|
|
2642
2817
|
if (!explicit.includes('implicitHeight'))
|
|
2643
|
-
value.implicitHeight =
|
|
2644
|
-
|
|
2645
|
-
(control ? 18 : value.topPadding + value.bottomPadding);
|
|
2818
|
+
value.implicitHeight = custom
|
|
2819
|
+
? Math.max(value.background?.implicitHeight || 0, (custom.implicitHeight || 0) + padding * 2)
|
|
2820
|
+
: count * lineBox + (control ? 18 : value.topPadding + value.bottomPadding);
|
|
2646
2821
|
value.baselineOffset =
|
|
2647
2822
|
(metrics.fontBoundingBoxAscent || value.font.pixelSize * 0.8) +
|
|
2648
2823
|
value.topPadding;
|
|
@@ -2669,6 +2844,7 @@ export function createQmlRuntime(
|
|
|
2669
2844
|
autoScroll: true,
|
|
2670
2845
|
persistentSelection: false,
|
|
2671
2846
|
cursorVisible: true,
|
|
2847
|
+
cursorDelegate:null,
|
|
2672
2848
|
selectionColor: '#3874f2',
|
|
2673
2849
|
selectedTextColor: 'white',
|
|
2674
2850
|
});
|
|
@@ -2746,10 +2922,24 @@ export function createQmlRuntime(
|
|
|
2746
2922
|
)
|
|
2747
2923
|
element.selectionStart = element.selectionEnd = value.cursorPosition;
|
|
2748
2924
|
});
|
|
2925
|
+
let cursorItem=null,cursorFactory=null;
|
|
2926
|
+
effect(()=>{
|
|
2927
|
+
const factory=value.cursorDelegate;
|
|
2928
|
+
if(factory!==cursorFactory)untrack(()=>{
|
|
2929
|
+
cursorItem?.destroy();cursorItem=null;cursorFactory=factory;
|
|
2930
|
+
if(factory)cursorItem=factory.createObject(value);
|
|
2931
|
+
});
|
|
2932
|
+
if(cursorItem){
|
|
2933
|
+
const rect=value.cursorRectangle;
|
|
2934
|
+
cursorItem.x=rect.x+value.leftPadding;cursorItem.y=rect.y+value.topPadding;cursorItem.height=rect.height;
|
|
2935
|
+
cursorItem.visible=value.cursorVisible&&value.activeFocus;
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2938
|
+
disposers?.push(()=>cursorItem?.destroy());
|
|
2749
2939
|
effect(() => {
|
|
2750
2940
|
// Presentation options only; editing and blinking belong to HTML controls.
|
|
2751
2941
|
host?.inputPresentation?.(element, {
|
|
2752
|
-
persistent:value.persistentSelection, visible:value.cursorVisible,
|
|
2942
|
+
persistent:value.persistentSelection, visible:value.cursorVisible&&!value.cursorDelegate,
|
|
2753
2943
|
background:cssColor(value.selectionColor), selectedColor:cssColor(value.selectedTextColor),
|
|
2754
2944
|
color:cssColor(value.color), height:value.font.pixelSize,
|
|
2755
2945
|
});
|
|
@@ -2771,6 +2961,19 @@ export function createQmlRuntime(
|
|
|
2771
2961
|
});
|
|
2772
2962
|
|
|
2773
2963
|
}
|
|
2964
|
+
if(type==='Button'){
|
|
2965
|
+
Object.assign(value,{padding:8,contentItem:null,background:null,focusPolicy:3,hovered:false,pressed:false,down:false});
|
|
2966
|
+
listen(element,'mouseenter',()=>{value.hovered=true;});
|
|
2967
|
+
listen(element,'mouseleave',()=>{value.hovered=false;value.pressed=false;value.down=false;});
|
|
2968
|
+
listen(element,'mousedown',e=>{if(e.button===0&&enabled(value)){value.pressed=true;value.down=true;}if(value.focusPolicy===0)e.preventDefault();});
|
|
2969
|
+
listen(document,'mouseup',()=>{value.pressed=false;value.down=false;});
|
|
2970
|
+
effect(()=>{
|
|
2971
|
+
const pad=Math.max(0,Number(value.padding)||0),content=value.contentItem,bg=value.background;
|
|
2972
|
+
if(content){content.x=pad;content.y=pad;content.width=Math.max(0,value.width-pad*2);content.height=Math.max(0,value.height-pad*2);}
|
|
2973
|
+
if(bg){bg.x=bg.y=0;bg.width=value.width;bg.height=value.height;bg.z=-1;}
|
|
2974
|
+
value.activeFocusOnTab=!!(value.focusPolicy&1);
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2774
2977
|
if (type === 'Button')
|
|
2775
2978
|
element.addEventListener('keydown', (event) => {
|
|
2776
2979
|
if (
|
|
@@ -2889,7 +3092,7 @@ export function createQmlRuntime(
|
|
|
2889
3092
|
}
|
|
2890
3093
|
if (type === 'TextField' || type === 'Button') {
|
|
2891
3094
|
element.disabled = !value.enabled;
|
|
2892
|
-
s.padding = '8px 10px';
|
|
3095
|
+
s.padding = type==='Button'?Math.max(0,Number(value.padding)||0)+'px':'8px 10px';
|
|
2893
3096
|
s.backgroundColor = type === 'Button' ? '#bcebf0' : '#ffffff';
|
|
2894
3097
|
s.border = '1px solid #6a8791';
|
|
2895
3098
|
s.borderRadius = '5px';
|
|
@@ -2967,6 +3170,7 @@ export function createQmlRuntime(
|
|
|
2967
3170
|
}))
|
|
2968
3171
|
element.setAttribute(key, String(v));
|
|
2969
3172
|
}
|
|
3173
|
+
if (type === 'PathMultiline') element.setAttribute('d', shapes.multiline(value.paths));
|
|
2970
3174
|
if (type === 'PathSvg') element.setAttribute('d', value.path);
|
|
2971
3175
|
if (type === 'PathRectangle')
|
|
2972
3176
|
for (const [key, v] of Object.entries({
|
|
@@ -2993,6 +3197,10 @@ export function createQmlRuntime(
|
|
|
2993
3197
|
')';
|
|
2994
3198
|
}
|
|
2995
3199
|
|
|
3200
|
+
if(type==='Button'){
|
|
3201
|
+
if(value.background){s.backgroundColor='transparent';s.border='none';s.borderRadius='0';s.isolation='isolate';}
|
|
3202
|
+
if(textNode)textNode.style.display=value.contentItem?'none':'contents';
|
|
3203
|
+
}
|
|
2996
3204
|
const style = element.style;
|
|
2997
3205
|
// Reuse our input values (CSS getters may normalize colors/shorthands).
|
|
2998
3206
|
// If another QML helper or script changed inline styles, reconcile them.
|
|
@@ -3015,6 +3223,7 @@ export function createQmlRuntime(
|
|
|
3015
3223
|
// Bind through a setter supplied by generated code. A separate flag avoids
|
|
3016
3224
|
// treating a binding's own write as an imperative assignment.
|
|
3017
3225
|
const bind = (target, key, read, readonly = false, type = null) => {
|
|
3226
|
+
if (socketStates.has(target) && key === 'connected') return bindAlias(target, key, read);
|
|
3018
3227
|
if (key === 'parent' && owners.has(target)) {
|
|
3019
3228
|
initial.push(() => {
|
|
3020
3229
|
const previous = disposers;
|
|
@@ -3080,6 +3289,14 @@ export function createQmlRuntime(
|
|
|
3080
3289
|
try {
|
|
3081
3290
|
stop = effect(() => {
|
|
3082
3291
|
const value = read();
|
|
3292
|
+
// A parent's layout can run before this explicit size binding's
|
|
3293
|
+
// first evaluation. Replace that provisional zero with the declared
|
|
3294
|
+
// size, and invalidate the layout through reactive hint storage.
|
|
3295
|
+
if ((key === 'width' || key === 'height') && descriptions.get(target)?.properties.includes(key)) {
|
|
3296
|
+
let hint = layoutFallbackSizes.get(target);
|
|
3297
|
+
if (!hint) layoutFallbackSizes.set(target, hint = state({}));
|
|
3298
|
+
hint[key === 'width' ? 'Width' : 'Height'] = Math.max(0, Number(value) || 0);
|
|
3299
|
+
}
|
|
3083
3300
|
writing = true;
|
|
3084
3301
|
try {
|
|
3085
3302
|
target[key] = value;
|
|
@@ -3165,7 +3382,7 @@ export function createQmlRuntime(
|
|
|
3165
3382
|
});
|
|
3166
3383
|
};
|
|
3167
3384
|
const create = (type, index, parent, before, persistent = false) => {
|
|
3168
|
-
const svgTag = { ShapePath: 'g', PathSvg: 'path', PathRectangle: 'rect' }[
|
|
3385
|
+
const svgTag = { ShapePath: 'g', PathMultiline: 'path', PathSvg: 'path', PathRectangle: 'rect' }[
|
|
3169
3386
|
type
|
|
3170
3387
|
];
|
|
3171
3388
|
const element = svgTag
|
|
@@ -3223,6 +3440,9 @@ export function createQmlRuntime(
|
|
|
3223
3440
|
host,
|
|
3224
3441
|
cleanup: (fn) => disposers?.push(fn),
|
|
3225
3442
|
});
|
|
3443
|
+
const shortcuts=createQmlShortcuts({emit,activeWindow:()=>activeWindow,cleanup:fn=>disposers?.push(fn)});
|
|
3444
|
+
const shaders=createQmlShaderEffects({effect,untrack,elements,host,texture:v=>textures.image(v),cleanup:fn=>disposers?.push(fn),resolve:(v,url)=>new URL(String(url),descriptions.get(v)?.url||document.baseURI).href});
|
|
3445
|
+
const canvases=createQmlCanvas({effect,untrack,emit,cssColor,cleanup:fn=>disposers?.push(fn)});
|
|
3226
3446
|
const media=createQmlMedia({document,elements,effect,untrack,watchWrite,listen,host,emit,
|
|
3227
3447
|
cleanup:fn=>disposers?.push(fn),resolve:(v,url)=>new URL(String(url),descriptions.get(v)?.url||document.baseURI).href});
|
|
3228
3448
|
const models = createQmlModels({
|
|
@@ -3336,6 +3556,8 @@ export function createQmlRuntime(
|
|
|
3336
3556
|
get: () => {
|
|
3337
3557
|
if (changed) return changed;
|
|
3338
3558
|
changed = signal();
|
|
3559
|
+
// QML property notify signals are synchronous: connected handlers
|
|
3560
|
+
// can mirror state which the caller reads immediately afterward.
|
|
3339
3561
|
let previous = untrack(() => value[key]);
|
|
3340
3562
|
const previousScope = disposers;
|
|
3341
3563
|
disposers = owners.get(value)?.scope ?? null;
|
|
@@ -3346,7 +3568,7 @@ export function createQmlRuntime(
|
|
|
3346
3568
|
previous = next;
|
|
3347
3569
|
changed();
|
|
3348
3570
|
}
|
|
3349
|
-
});
|
|
3571
|
+
}, { sync: true });
|
|
3350
3572
|
} finally {
|
|
3351
3573
|
disposers = previousScope;
|
|
3352
3574
|
}
|