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.
Files changed (84) hide show
  1. package/README.md +22 -1
  2. package/frontends/wayland/README.md +50 -21
  3. package/frontends/wayland/index.js +14 -0
  4. package/frontends/wayland/input.js +24 -5
  5. package/frontends/wayland/urlbar.js +9 -5
  6. package/lib/audio/shared-connection.js +31 -0
  7. package/lib/cdp-server.js +15 -2
  8. package/lib/headless-shell.js +7 -4
  9. package/lib/paint/display-list.js +2 -1
  10. package/lib/qml/README.md +456 -0
  11. package/lib/qml/builtins.js +76 -5
  12. package/lib/qml/canvas.js +76 -0
  13. package/lib/qml/document.js +47 -24
  14. package/lib/qml/fonts.js +69 -0
  15. package/lib/qml/host.js +28 -0
  16. package/lib/qml/loader.js +7 -3
  17. package/lib/qml/models.js +1 -1
  18. package/lib/qml/reactive.js +26 -3
  19. package/lib/qml/runtime.js +263 -41
  20. package/lib/qml/shader/fragment.js +83 -0
  21. package/lib/qml/shader/qsb.js +27 -0
  22. package/lib/qml/shader/renderer.js +39 -0
  23. package/lib/qml/shader/runtime.js +61 -0
  24. package/lib/qml/shapes.js +9 -1
  25. package/lib/qml/shortcuts.js +65 -0
  26. package/lib/qml/textures.js +4 -1
  27. package/lib/qml/window-constraints.js +16 -0
  28. package/lib/renderer/bindings.js +5 -1
  29. package/lib/renderer/canvas-2d.js +16 -0
  30. package/lib/renderer/document-selection.js +19 -0
  31. package/lib/renderer/host.js +14 -11
  32. package/lib/renderer/main.js +2 -0
  33. package/lib/renderer/page-realm.js +4 -9
  34. package/lib/renderer/page-renderer.js +273 -35
  35. package/lib/renderer/pointer-move.js +5 -3
  36. package/lib/renderer/render-interval.js +9 -0
  37. package/lib/style/computed-style.js +8 -2
  38. package/package.json +1 -1
  39. package/test/audio-connection-lifetime.test.js +46 -0
  40. package/test/canvas-paint.test.js +8 -0
  41. package/test/computed-style.test.js +12 -0
  42. package/test/document-selection.test.js +27 -0
  43. package/test/double-click.test.js +85 -0
  44. package/test/fixtures/canvas-damage.js +40 -0
  45. package/test/fixtures/fake-renderer.js +1 -0
  46. package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
  47. package/test/fixtures/omastorm-helpers/check.mjs +56 -0
  48. package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
  49. package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
  50. package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
  51. package/test/fixtures/pointer-paint-batching.js +23 -0
  52. package/test/fixtures/qml-fonts/main.qml +10 -0
  53. package/test/fixtures/qml-locale/check.mjs +6 -0
  54. package/test/fixtures/qml-locale/main.qml +8 -0
  55. package/test/fixtures/qml-shader/grab.qml +14 -0
  56. package/test/fixtures/qml-shader/gradient.frag +5 -0
  57. package/test/fixtures/qml-shader/main.qml +14 -0
  58. package/test/fixtures/qml-shader/texture-grab.qml +25 -0
  59. package/test/fixtures/qml-shader/texture-size.frag +5 -0
  60. package/test/fixtures/qml-shader-check.mjs +24 -0
  61. package/test/fixtures/title-no-paint.js +20 -0
  62. package/test/manual-double-click.html +21 -0
  63. package/test/manual-mouse-buttons.html +18 -0
  64. package/test/manual-omastorm-engine.mjs +19 -0
  65. package/test/manual-omastorm-shaders.mjs +28 -0
  66. package/test/manual-omastorm-software.mjs +25 -0
  67. package/test/manual-pointer-scroll.html +43 -0
  68. package/test/pointer-drag.test.js +104 -0
  69. package/test/qml-canvas.test.js +21 -0
  70. package/test/qml-connections-timing.test.js +22 -0
  71. package/test/qml-controls-window.test.js +182 -0
  72. package/test/qml-fonts.test.js +31 -0
  73. package/test/qml-grid-multiline.test.js +48 -0
  74. package/test/qml-locale.test.js +45 -0
  75. package/test/qml-nonvisual-visible.test.js +19 -0
  76. package/test/qml-shader-runtime.test.js +6 -0
  77. package/test/qml-shortcuts.test.js +39 -0
  78. package/test/qml-socket.test.js +60 -0
  79. package/test/qml-software-shader.test.js +55 -0
  80. package/test/qml-window-update.test.js +36 -0
  81. package/test/render-interval.test.js +33 -0
  82. package/test/title-no-paint.test.js +6 -0
  83. package/test/wayland-scroll.test.js +40 -0
  84. package/test/wayland-urlbar.test.js +23 -0
@@ -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
- value = factory(parent, properties);
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
- propertyHandlers.push(() => {
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
- listen(areaTarget, 'click', (event) => {
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) instance.dispose();
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.itemAt = (index) => instances[index]?.value ?? null;
1238
+ if (type === 'Instantiator') v.objectAt = index => instances[index]?.value ?? null;
1239
+ else v.itemAt = (index) => instances[index]?.value ?? null;
1196
1240
  effect(() => {
1197
- const model = v.model ?? [];
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
- listen(target, 'click', (event) => {
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 propertyHandlers) install();
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
- let windowValue = type === 'FloatingWindow' ? value : parent;
1866
- while (windowValue && owners.get(windowValue)?.type !== 'FloatingWindow')
1867
- windowValue = parents.get(windowValue);
1868
- value.Window = { window: windowValue ?? null };
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
- frame = requestAnimationFrame(tick);
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
- if (frame === undefined) frame = requestAnimationFrame(tick);
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
- maxCross = Math.max(maxCross, size + c[other]);
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
- natural + (control ? 22 : value.leftPadding + value.rightPadding);
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
- count * lineBox +
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
  }