buninu-browser 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +19 -4
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +123 -0
  5. package/frontends/wayland/casty/LICENSE +21 -0
  6. package/frontends/wayland/casty/bookmarks.js +32 -0
  7. package/frontends/wayland/casty/click.js +96 -0
  8. package/frontends/wayland/casty/commands.js +61 -0
  9. package/frontends/wayland/casty/config.js +45 -0
  10. package/frontends/wayland/casty/download-directory.js +28 -0
  11. package/frontends/wayland/casty/download.js +49 -0
  12. package/frontends/wayland/casty/hints.js +334 -0
  13. package/frontends/wayland/casty/keys.js +56 -0
  14. package/frontends/wayland/casty/page-commands.js +80 -0
  15. package/frontends/wayland/casty/prompt-width.js +148 -0
  16. package/frontends/wayland/casty/urlbar.js +606 -0
  17. package/frontends/wayland/client.js +88 -0
  18. package/frontends/wayland/clipboard.js +80 -0
  19. package/frontends/wayland/index.js +116 -0
  20. package/frontends/wayland/input.js +56 -0
  21. package/frontends/wayland/protocol.js +55 -0
  22. package/frontends/wayland/protocols/wayland.xml +3248 -0
  23. package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
  24. package/frontends/wayland/transport.js +120 -0
  25. package/frontends/wayland/urlbar.js +158 -0
  26. package/index.js +28 -1
  27. package/lib/clipboard-command.js +10 -0
  28. package/lib/clipboard-powershell.js +22 -0
  29. package/lib/clipboard.js +37 -2
  30. package/lib/document-types.js +11 -0
  31. package/lib/headless-shell.js +47 -8
  32. package/lib/layout/text-layout.js +17 -9
  33. package/lib/paint/display-list.js +5 -1
  34. package/lib/qml/README.md +971 -0
  35. package/lib/qml/builtins.js +700 -0
  36. package/lib/qml/canvas-dom.js +115 -0
  37. package/lib/qml/document.js +1176 -0
  38. package/lib/qml/format.js +9 -0
  39. package/lib/qml/host.js +324 -0
  40. package/lib/qml/images.js +199 -0
  41. package/lib/qml/launcher.js +132 -0
  42. package/lib/qml/loader.js +517 -0
  43. package/lib/qml/media-renderer.js +49 -0
  44. package/lib/qml/media.js +86 -0
  45. package/lib/qml/models.js +622 -0
  46. package/lib/qml/parser.js +381 -0
  47. package/lib/qml/qs +5 -0
  48. package/lib/qml/qscasty +3 -0
  49. package/lib/qml/qsheadless +3 -0
  50. package/lib/qml/qswayland +3 -0
  51. package/lib/qml/reactive.js +159 -0
  52. package/lib/qml/runtime.js +3381 -0
  53. package/lib/qml/shapes.js +243 -0
  54. package/lib/qml/text.js +109 -0
  55. package/lib/qml/textures.js +123 -0
  56. package/lib/qml/wayland-clipboard.js +32 -0
  57. package/lib/qml/worker-thread.js +70 -0
  58. package/lib/render-tree/index.js +16 -1
  59. package/lib/renderer/bindings.js +11 -0
  60. package/lib/renderer/canvas-2d.js +13 -0
  61. package/lib/renderer/host.js +31 -6
  62. package/lib/renderer/main.js +19 -6
  63. package/lib/renderer/page-realm.js +16 -1
  64. package/lib/renderer/page-renderer.js +230 -13
  65. package/lib/renderer/text-input-paint.js +54 -0
  66. package/lib/renderer/text-input.js +57 -0
  67. package/lib/style/computed-style.js +2 -2
  68. package/package.json +1 -1
  69. package/test/clipboard.test.js +19 -0
  70. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  71. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  72. package/test/fixtures/flea-helpers/bin/bat +3 -0
  73. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  74. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  75. package/test/fixtures/flea-helpers/bin/glow +3 -0
  76. package/test/fixtures/flea-helpers/bin/qs +3 -0
  77. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  78. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  79. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  80. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  81. package/test/fixtures/flea-helpers/check.mjs +36 -0
  82. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  83. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  84. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  85. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  86. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  87. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  88. package/test/fixtures/qml/flea/LICENSE +21 -0
  89. package/test/fixtures/qml/flea/README.md +25 -0
  90. package/test/fixtures/qml/flea/Theme.qml +8 -0
  91. package/test/fixtures/qml/flea/demo.qml +24 -0
  92. package/test/fixtures/qml/flea/qmldir +3 -0
  93. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  94. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  95. package/test/fixtures/qml/flea-theme/README.md +70 -0
  96. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  97. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  98. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  99. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  100. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  101. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  102. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  103. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  104. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  105. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  106. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  107. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  108. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  109. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  110. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  111. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  112. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  113. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  114. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  115. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  116. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  117. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  118. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  119. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  120. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  121. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  122. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  123. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  124. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  125. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  126. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  127. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  128. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  129. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  130. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  131. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  132. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  133. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  134. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  135. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  136. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  137. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  138. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  139. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  140. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  141. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  142. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  143. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  144. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  145. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  146. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  147. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  148. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  167. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  168. package/test/fixtures/qml/hello.qml +26 -0
  169. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  170. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  171. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  172. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  173. package/test/fixtures/qml/imports/logic/base.js +2 -0
  174. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  175. package/test/fixtures/qml/imports/logic/math.js +3 -0
  176. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  177. package/test/fixtures/qml/imports/main.qml +21 -0
  178. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  179. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  180. package/test/fixtures/qml/multiply.qml +61 -0
  181. package/test/fixtures/qml-canvas-dom.js +90 -0
  182. package/test/fixtures/qml-media-audio.js +39 -0
  183. package/test/helpers/flea-sandbox.js +48 -0
  184. package/test/helpers/quickshell-fixture.js +125 -0
  185. package/test/html-clipboard-wayland.test.js +47 -0
  186. package/test/index.test.js +13 -0
  187. package/test/manual-casty-without-qml.js +87 -0
  188. package/test/manual-flea-application.js +49 -0
  189. package/test/manual-flea-binary.js +62 -0
  190. package/test/manual-flea-qml.js +9 -0
  191. package/test/manual-flea-settings.js +40 -0
  192. package/test/manual-flea-theme.js +40 -0
  193. package/test/manual-qml-removal.js +46 -0
  194. package/test/manual-without-qml.js +45 -0
  195. package/test/qml-canvas-dom.test.js +37 -0
  196. package/test/qml-clipboard-bridge.test.js +25 -0
  197. package/test/qml-clipboard-wayland.test.js +40 -0
  198. package/test/qml-connections-timing.test.js +73 -0
  199. package/test/qml-dynamic.test.js +401 -0
  200. package/test/qml-flea-app.test.js +146 -0
  201. package/test/qml-flea-backend.test.js +78 -0
  202. package/test/qml-flea-columns-backend.test.js +38 -0
  203. package/test/qml-flea-compile.test.js +78 -0
  204. package/test/qml-flea-launcher.test.js +88 -0
  205. package/test/qml-flea-markdown-backend.test.js +31 -0
  206. package/test/qml-flea-media.test.js +28 -0
  207. package/test/qml-flea-navigation-backend.test.js +35 -0
  208. package/test/qml-flea-navigation-timing.test.js +147 -0
  209. package/test/qml-flea-preview-backend.test.js +53 -0
  210. package/test/qml-flea-previews.test.js +163 -0
  211. package/test/qml-flea-rows.test.js +101 -0
  212. package/test/qml-flea-textures.test.js +59 -0
  213. package/test/qml-flea-view-switch.test.js +33 -0
  214. package/test/qml-flea-wayland.test.js +68 -0
  215. package/test/qml-flea.test.js +34 -0
  216. package/test/qml-host.test.js +238 -0
  217. package/test/qml-imports.test.js +102 -0
  218. package/test/qml-inherited-handlers.test.js +51 -0
  219. package/test/qml-input-selection.test.js +66 -0
  220. package/test/qml-interactions.test.js +309 -0
  221. package/test/qml-language.test.js +167 -0
  222. package/test/qml-launcher.test.js +54 -0
  223. package/test/qml-layout-text.test.js +205 -0
  224. package/test/qml-media.test.js +63 -0
  225. package/test/qml-persisted-delegates.test.js +45 -0
  226. package/test/qml-quickshell-controls.test.js +65 -0
  227. package/test/qml-quickshell-settings.test.js +62 -0
  228. package/test/qml-repeater.test.js +72 -0
  229. package/test/qml-style-updates.test.js +124 -0
  230. package/test/qml-textures.test.js +66 -0
  231. package/test/qml-views.test.js +332 -0
  232. package/test/qml-worker.test.js +69 -0
  233. package/test/qml.test.js +83 -0
  234. package/test/renderer-host.test.js +11 -0
  235. package/test/text-input-pointer.test.js +31 -0
  236. package/test/text-input-scroll.test.js +29 -0
  237. package/test/text-input.test.js +47 -0
  238. package/test/wayland-frontend.test.js +83 -0
  239. package/test/wayland-urlbar.test.js +38 -0
@@ -1,4 +1,4 @@
1
- import {writeClipboard} from '../clipboard.js';
1
+ import {writeClipboard, readClipboard} from '../clipboard.js';
2
2
  /**
3
3
  * Controller-side handle for a renderer process.
4
4
  *
@@ -55,11 +55,13 @@ const LIFECYCLE_NAMES = new Set(["DOMContentLoaded", "load"]);
55
55
  const DEFAULT_ENTRY = Bun.fileURLToPath(new URL("./main.js", import.meta.url));
56
56
 
57
57
  export class RendererHost {
58
+ #clipboardRead = readClipboard;
58
59
  #clipboardWrite = writeClipboard;
59
60
  #clipboardUntil = 0;
60
61
  #clipboardPending = false;
61
62
  #rawFrames = null;
62
63
  #startEpoch = 0;
64
+ #closed = false;
63
65
  #rejectStartup = null;
64
66
  #entry;
65
67
  #args;
@@ -114,8 +116,10 @@ export class RendererHost {
114
116
  network = new NetworkService({ fetch: fetchImpl }), onFrame = null, onLifecycle = null,
115
117
  // A dedicated loopback socket works independently of the main thread on all platforms.
116
118
  compositor = entry === DEFAULT_ENTRY,
119
+ clipboardRead = readClipboard,
117
120
  clipboardWrite = writeClipboard,
118
121
  } = {}) {
122
+ this.#clipboardRead = clipboardRead;
119
123
  this.#clipboardWrite = clipboardWrite;
120
124
  this.#onFrame = onFrame;
121
125
  this.#onLifecycle = onLifecycle;
@@ -131,6 +135,7 @@ export class RendererHost {
131
135
 
132
136
  /** Starts the renderer if it is not running and waits until it is ready. */
133
137
  start() {
138
+ if (this.#closed) return Promise.reject(Error("Renderer is closed"));
134
139
  if (this.#ready) return this.#ready;
135
140
  let resolveReady;
136
141
  let rejectReady;
@@ -149,13 +154,19 @@ export class RendererHost {
149
154
  ()=>this.#closeChannel(channel));
150
155
  if (epoch !== this.#startEpoch) { channel?.transport.close(); throw Error('Renderer startup cancelled'); }
151
156
  this.#channel = channel;
157
+ // No inherited environment: nothing from the controller leaks in.
158
+ // Windows programs need SystemRoot to load system libraries.
159
+ let env = process.platform === "win32" && process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {};
160
+
161
+ // qml handle begin: applications need HOME/PATH/runtime paths, web pages do not
162
+ if (this.#args.includes("--buninu-qml-host")) env = { ...process.env };
163
+ // qml handle end
164
+
152
165
  const child = Bun.spawn({
153
166
  cmd: [process.execPath, this.#entry, ...this.#args,
154
167
  ...(logging ? [RENDERER_LOG_FLAG] : []), ...(compositor ? [COMPOSITOR_FLAG, `--compositor-endpoint=${JSON.stringify(channel.transport.endpoint)}`] : []),
155
168
  ...(process.env.BUNINU_VERIFY_STYLE ? [VERIFY_STYLE_FLAG] : []), ...(process.env.BUNINU_FULL_STYLE ? [FULL_STYLE_FLAG] : [])],
156
- // No inherited environment: nothing from the controller leaks in.
157
- // Windows programs need SystemRoot to load system libraries.
158
- env: process.platform === "win32" && process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {},
169
+ env,
159
170
  // With BUNINU_LOG, output goes through the controller into the log file.
160
171
  stdio: ["ignore", logging ? "pipe" : "inherit", logging ? "pipe" : "inherit"],
161
172
  serialization: "advanced",
@@ -297,6 +308,7 @@ export class RendererHost {
297
308
  }
298
309
 
299
310
  close() {
311
+ this.#closed = true;
300
312
  this.#startEpoch++;
301
313
  this.#rejectStartup?.(Error("Renderer closed during startup")); this.#rejectStartup=null;
302
314
  this.#rawFrames?.close(); this.#rawFrames=null;
@@ -313,6 +325,7 @@ export class RendererHost {
313
325
  }
314
326
 
315
327
  async #call(method, params, timeout = this.#timeout) {
328
+ if (this.#closed) throw Error("Renderer is closed");
316
329
  if (!this.#child) await this.#restart();
317
330
  await this.start();
318
331
  const child = this.#child;
@@ -365,11 +378,23 @@ export class RendererHost {
365
378
  pending.reject(new Error(`renderer: ${String(message.error).split("\n")[0] || "unknown error"}`));
366
379
  }
367
380
  else pending.resolve(message.result);
368
- } else if (message.type === 'clipboard-write' && Number.isInteger(message.id)) {
381
+ } else if (['clipboard-read','clipboard-read-input'].includes(message.type) && Number.isInteger(message.id)) {
382
+ const reply = (text, error) => {if(this.#child===child) child.send({type:'clipboard-result',id:message.id,text,error});};
383
+ let secure=false;
384
+ try {const url=new URL(this.#documentUrl); secure=url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname));} catch {}
385
+ if ((!secure && !message.type.endsWith('-input')) || Date.now()>this.#clipboardUntil || this.#clipboardPending) {
386
+ reply(undefined,'Clipboard read requires a recent user action in a secure document'); return;
387
+ }
388
+ this.#clipboardUntil=0; this.#clipboardPending=true;
389
+ Promise.resolve().then(()=>this.#clipboardRead()).then(text=>{
390
+ if(typeof text!=='string'||Buffer.byteLength(text)>1024*1024)throw Error('Clipboard text exceeds limit');
391
+ reply(text);
392
+ }).catch(()=>reply(undefined,'Unable to read system clipboard')).finally(()=>{this.#clipboardPending=false;});
393
+ } else if (['clipboard-write','clipboard-write-input'].includes(message.type) && Number.isInteger(message.id)) {
369
394
  const reply = error => { if(this.#child===child)child.send({type:'clipboard-result',id:message.id,...(error?{error}: {})}); };
370
395
  let secure=false;
371
396
  try { const url=new URL(this.#documentUrl); secure=url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname)); } catch {}
372
- if (!secure || Date.now()>this.#clipboardUntil || this.#clipboardPending
397
+ if ((!secure && !message.type.endsWith('-input')) || Date.now()>this.#clipboardUntil || this.#clipboardPending
373
398
  || typeof message.text!=='string' || Buffer.byteLength(message.text)>1024*1024) {
374
399
  reply('Clipboard write requires a recent user action in a secure document'); return;
375
400
  }
@@ -241,12 +241,13 @@ async function startScriptRewriter() {
241
241
 
242
242
  let nextClipboardId=1;
243
243
  const clipboardWrites=new Map();
244
- function clipboardWrite(text) {
244
+ function clipboardRead() { return clipboardWrite(undefined, 'clipboard-read'); }
245
+ function clipboardWrite(text, type = 'clipboard-write') {
245
246
  const id=nextClipboardId++;
246
247
  return new Promise((resolve,reject)=>{
247
248
  const timer=setTimeout(()=>{clipboardWrites.delete(id);reject(Error('Clipboard timed out'));},5000);
248
249
  clipboardWrites.set(id,{resolve,reject,timer});
249
- process.send({type:'clipboard-write',id,text});
250
+ process.send({type,id,text});
250
251
  });
251
252
  }
252
253
 
@@ -256,7 +257,10 @@ const renderer = await createPageRenderer({
256
257
  requestNavigation: (url, download = null, fragmentFrom = null, post = null) => process.send({ type: "navigate", url, download, fragmentFrom, post }),
257
258
  requestDownload: ({ url, name, data }) => process.send({ type: "download-data", url, name, data }),
258
259
  pageFetch,
260
+ clipboardRead,
259
261
  clipboardWrite,
262
+ clipboardReadInput:()=>clipboardWrite(undefined,'clipboard-read-input'),
263
+ clipboardWriteInput:text=>clipboardWrite(text,'clipboard-write-input'),
260
264
  webSockets,
261
265
  audio,
262
266
  setCookie: (value) => process.send({ type: "cookie", value }),
@@ -281,11 +285,20 @@ const renderer = await createPageRenderer({
281
285
  videoDecoder: wasmpeg,
282
286
  });
283
287
  await warmUp();
284
- const allowHostJs = process.argv.includes(DANGEROUSLY_ALLOW_HOST_JS);
288
+ let allowHostJs = process.argv.includes(DANGEROUSLY_ALLOW_HOST_JS);
289
+ let hostReason = `disabled by ${DANGEROUSLY_ALLOW_HOST_JS}`;
290
+
291
+ // qml handle begin: controller-selected local QML application process
292
+ if (process.argv.includes("--buninu-qml-host")) {
293
+ allowHostJs = true;
294
+ hostReason = "local QML application renderer";
295
+ }
296
+ // qml handle end
297
+
285
298
  const seccomp = allowHostJs
286
- ? { installed: false, reason: `disabled by ${DANGEROUSLY_ALLOW_HOST_JS}` }
299
+ ? { installed: false, reason: hostReason }
287
300
  : installSeccomp();
288
- const ses = allowHostJs ? { installed: false, reason: `disabled by ${DANGEROUSLY_ALLOW_HOST_JS}` } : lockdownRealm();
301
+ const ses = allowHostJs ? { installed: false, reason: hostReason } : lockdownRealm();
289
302
  renderer.setScripting(allowHostJs ? "host" : ses.installed ? "ses" : "off");
290
303
 
291
304
  /** Hardens the JavaScript realm so page scripts can run in SES compartments. */
@@ -340,7 +353,7 @@ process.on("message", async (message) => {
340
353
  if(message?.type==='clipboard-result') {
341
354
  const pending=clipboardWrites.get(message.id);if(!pending)return;
342
355
  clipboardWrites.delete(message.id);clearTimeout(pending.timer);
343
- if(message.error)pending.reject(Error(String(message.error)));else pending.resolve();
356
+ if(message.error)pending.reject(Error(String(message.error)));else pending.resolve(message.text);
344
357
  return;
345
358
  }
346
359
 
@@ -192,6 +192,13 @@ const INTERFACE_MEMBERS = {
192
192
  alt: ["HTMLImageElement", "HTMLAreaElement", "HTMLInputElement"],
193
193
  placeholder: ["HTMLInputElement", "HTMLTextAreaElement"],
194
194
  readOnly: ["HTMLInputElement", "HTMLTextAreaElement"],
195
+ maxLength: ["HTMLInputElement", "HTMLTextAreaElement"],
196
+ selectionStart: ["HTMLInputElement", "HTMLTextAreaElement"],
197
+ selectionEnd: ["HTMLInputElement", "HTMLTextAreaElement"],
198
+ selectionDirection: ["HTMLInputElement", "HTMLTextAreaElement"],
199
+ setSelectionRange: ["HTMLInputElement", "HTMLTextAreaElement"],
200
+ setRangeText: ["HTMLInputElement", "HTMLTextAreaElement"],
201
+ select: ["HTMLInputElement", "HTMLTextAreaElement"],
195
202
  required: ["HTMLInputElement", "HTMLTextAreaElement", "HTMLSelectElement"],
196
203
  min: ["HTMLInputElement", "HTMLMeterElement"], max: ["HTMLInputElement", "HTMLMeterElement", "HTMLProgressElement"],
197
204
  step: ["HTMLInputElement"],
@@ -1590,6 +1597,13 @@ export function createPageRealm({ window, document, mode, hooks }) {
1590
1597
  const platform = platformObjects({
1591
1598
  eventTargets, userAgent, userAgentDataMembers: chromeVersion ? userAgentDataMembers : null, mobile: mobileUserAgent, viewport: hooks.viewport,
1592
1599
  performance: userTiming.performance, storages: { local: hostStorage.get(origin), session: new Map() },
1600
+ clipboardRead: async () => {
1601
+ try {
1602
+ const url=new URL(document.URL);
1603
+ if (!(url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname))) || !hooks.clipboardRead) throw Error('Clipboard unavailable');
1604
+ return await hooks.clipboardRead();
1605
+ } catch {throw new DOMException('Clipboard read was not allowed or failed','NotAllowedError');}
1606
+ },
1593
1607
  clipboardWrite: async text => {
1594
1608
  try {
1595
1609
  const url = new URL(document.URL);
@@ -2681,7 +2695,7 @@ function navigatorPlatform(userAgent) {
2681
2695
  * global exposes for them. Desktop Chromium lists five PDF viewer plugins
2682
2696
  * and their two MIME types whatever is installed (HTML §8.9.1.6).
2683
2697
  */
2684
- function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile, viewport, performance, storages, sendBeacon, clipboardWrite }) {
2698
+ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile, viewport, performance, storages, sendBeacon, clipboardWrite, clipboardRead }) {
2685
2699
  const EventTargetPrototype = eventTargets.EventTarget.prototype;
2686
2700
  // An instance an event target's listeners can be added to.
2687
2701
  const eventTarget = (prototype) => {
@@ -2760,6 +2774,7 @@ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile
2760
2774
  const NavigatorUAData = userAgentDataMembers ? define("NavigatorUAData", userAgentDataMembers) : null;
2761
2775
  const languages = Object.freeze(["en-US"]);
2762
2776
  const Clipboard = define('Clipboard', {
2777
+ readText() { return clipboardRead(); },
2763
2778
  writeText(text) {
2764
2779
  if (arguments.length===0) return Promise.reject(new TypeError('writeText requires text'));
2765
2780
  return clipboardWrite(String(text));
@@ -22,10 +22,22 @@
22
22
  * then scripts run. `loadDocument` resolves at the first paint; `whenLoaded`
23
23
  * after the load event.
24
24
  */
25
+ import {isTextInput, moveInputSelection, inputClipboard, selectInput} from './text-input.js';
26
+ import {inputPresentation, inputOwner, inputCaretNode} from './text-input-paint.js';
25
27
  import CanvasKitInit from "../../usr/lib/canvaskit/canvaskit.js";
26
28
  import { setCanvasKit, canvasResource, observeDocumentCanvases } from "./canvas-2d.js";
27
29
  import { createCanvasTextRenderer, parseCanvasFont } from "./canvas-text.js";
28
30
  import { DESKTOP_USER_AGENT } from "../user-agent.js";
31
+
32
+ // qml handle begin: optional document frontend
33
+ import {createQmlMediaController,applyQmlMediaGain} from '../qml/media-renderer.js';
34
+ import {INTERFACES as qmlBindings} from "./bindings.js";
35
+ import { enableQmlDomImages, hasQmlDomImages, captureQmlDomImage, filterQmlTextureSources, qmlTextureVisibilityChanged } from "../qml/canvas-dom.js";
36
+ import {createQmlHost} from "../qml/host.js";
37
+ import { loadQmlDocument } from "../qml/document.js";
38
+ import { isQmlDocument } from "../qml/format.js";
39
+ // qml handle end
40
+
29
41
  import { parseHTMLDocument } from "../happy-dom/parser.js";
30
42
  import { LayoutEngine } from "../layout/text-layout.js";
31
43
  import { ScrollViewport } from "../layout/scroll-viewport.js";
@@ -227,6 +239,9 @@ function buildPageRenderer({
227
239
  requestDownload = () => {},
228
240
  reportError = () => {},
229
241
  pageFetch = () => Promise.reject(new TypeError("network access is not available")),
242
+ clipboardReadInput = () => clipboardRead(),
243
+ clipboardWriteInput = text => clipboardWrite(text),
244
+ clipboardRead = () => Promise.reject(Error("Clipboard unavailable")),
230
245
  clipboardWrite = () => Promise.reject(Error("Clipboard unavailable")),
231
246
  webSockets = null,
232
247
  audio = null,
@@ -505,7 +520,7 @@ function buildPageRenderer({
505
520
  const paragraph = makeParagraph(text, computedStyle);
506
521
  paragraph.layout(100_000);
507
522
  // getLongestLine() drops trailing white space, which line breaking needs.
508
- width = paragraph.getMaxIntrinsicWidth();
523
+ width = {width:paragraph.getMaxIntrinsicWidth(),ascent:paragraph.getAlphabeticBaseline(),height:paragraph.getHeight()};
509
524
  paragraph.delete();
510
525
  if (++measuredCount > MAX_MEASURED_TEXTS) {
511
526
  measuredWidths.clear();
@@ -514,7 +529,7 @@ function buildPageRenderer({
514
529
  }
515
530
  widths.set(text, width);
516
531
  }
517
- return width;
532
+ return width.width;
518
533
  }
519
534
 
520
535
  // Computed styles are immutable and shared, so each one's key is computed once.
@@ -534,8 +549,12 @@ function buildPageRenderer({
534
549
  }
535
550
 
536
551
  const fontMetricsCache = new Map();
537
- function fontMetrics(computedStyle = null) {
552
+ function fontMetrics(computedStyle = null, text = null) {
538
553
  const key = fontKeyOf(computedStyle);
554
+ if(text) {
555
+ measureText(text,computedStyle);
556
+ return measuredWidths.get(key).get(text);
557
+ }
539
558
  let metrics = fontMetricsCache.get(key);
540
559
  if (!metrics) {
541
560
  const paragraph = makeParagraph("Hg", computedStyle);
@@ -1013,7 +1032,7 @@ function buildPageRenderer({
1013
1032
  } else if (item.op === "drawSvg") {
1014
1033
  drawSvg(item);
1015
1034
  } else if (item.op === "drawText") {
1016
- const entry = paragraphForPaint(item, scale, pixel(Math.max(1, view.width + view.x - item.x)));
1035
+ const entry = paragraphForPaint(item, scale, pixel(Math.max(1, item.textWidth + 1)));
1017
1036
  try {
1018
1037
  if (item.gradient) {
1019
1038
  // background-clip:text: draw glyphs, then keep the gradient only where they are.
@@ -1383,6 +1402,14 @@ function buildPageRenderer({
1383
1402
  const traits = selectorTraitsFor(sheets);
1384
1403
  const records = styleChanges.records;
1385
1404
  if (styleChanges.observer) records.push(...styleChanges.observer.takeRecords());
1405
+
1406
+ // qml handle begin: QML visual attributes invalidate retained rendering
1407
+ if (hasQmlDomImages(page.document) && qmlTextureVisibilityChanged(records)) {
1408
+ page.resourcePaintDirty = true;
1409
+ page.needsPaint = true;
1410
+ }
1411
+ // qml handle end
1412
+
1386
1413
  const roots = styleChanges.roots;
1387
1414
  // Explicit roots mean that state-dependent selectors may need restyling;
1388
1415
  // they do not by themselves change render-tree content. DOM mutation
@@ -1549,12 +1576,16 @@ function buildPageRenderer({
1549
1576
 
1550
1577
  function replaceParsedDocument(parsed, styleSheets = [], imageResources = emptyImageResources(), linkSheets = null) {
1551
1578
  if (page.document) loadImageResource.cancelDocument(page.document);
1579
+ resetInputCaret();
1552
1580
  rasterReadback.dispose();
1553
1581
  clearPaintParagraphs();
1554
1582
  resetDocumentFonts();
1555
1583
  disposeFrames();
1556
1584
  closePiano();
1557
1585
  disposeVideos();
1586
+ // qml handle begin: dispose application resources with their document
1587
+ page.document?.__qmlHost?.dispose();
1588
+ // qml handle end
1558
1589
  page.realm?.dispose();
1559
1590
  page.realm = null;
1560
1591
  page.window?.happyDOM?.close?.();
@@ -1797,8 +1828,23 @@ function buildPageRenderer({
1797
1828
  trace(`${url}: ${name} ${Math.round(now - phaseStart)}ms`);
1798
1829
  phaseStart = now;
1799
1830
  } : () => {};
1800
- const isHtml = contentType.toLowerCase().includes("html");
1801
- const parsed = parseHTMLDocument(isHtml ? source : "<body></body>", url, referrer);
1831
+
1832
+ let isHtml = contentType.toLowerCase().includes("html");
1833
+ let parsed;
1834
+
1835
+ // qml handle begin: supply DOM before the default HTML parser
1836
+ if (isQmlDocument(url, contentType)) {
1837
+ const qmlHost = page.scripting === "host" && new URL(url).protocol === "file:" ? createQmlHost(url) : null;
1838
+ parsed = await loadQmlDocument(source, url, {scripting: page.scripting !== "off", fetchResource, importRoots:qmlHost?.importRoots});
1839
+ parsed.document.__qmlHost = qmlHost;
1840
+ if (qmlHost) enableQmlDomImages(parsed.document);
1841
+ if (!current()) {qmlHost?.dispose();return;}
1842
+ // Reuse the existing stylesheet/script/lifecycle pipeline for QML DOM.
1843
+ isHtml = true;
1844
+ }
1845
+ // qml handle end
1846
+
1847
+ parsed ??= parseHTMLDocument(isHtml ? source : "<body></body>", url, referrer);
1802
1848
  phase("parse");
1803
1849
  // Stylesheets block rendering and scripts; images do not (only the
1804
1850
  // window load event waits for them, see whenImagesLoaded): one slow image
@@ -1858,6 +1904,33 @@ function buildPageRenderer({
1858
1904
  mode: page.scripting,
1859
1905
  hooks: realmHooks(generation),
1860
1906
  });
1907
+ // qml handle begin: only a local QML host realm receives this capability
1908
+ if (page.document.__qmlHost) page.realm.pageGlobal.__qmlHost = page.document.__qmlHost;
1909
+ // qml handle end
1910
+
1911
+ // qml handle begin: QML methods/properties adapt shared media and text controls
1912
+ if(page.document.__qmlHost) {
1913
+ const host=page.document.__qmlHost;
1914
+ host.media=element=>{
1915
+ element=qmlBindings.Node.unwrap(element)??element;
1916
+ if(element?.ownerDocument!==page.document||!['VIDEO','AUDIO'].includes(element.tagName))throw Error('Invalid QML media element');
1917
+ return createQmlMediaController(element,{
1918
+ valid:e=>e.ownerDocument===page.document,state:e=>page.media.get(e),
1919
+ close:closeMedia,forget:e=>{page.media.delete(e);page.resources.delete(e);},ensure:mediaStateFor,
1920
+ load:loadMedia,seek:seekMedia,play:playMedia,pause:pauseMedia,
1921
+ snapshot:mediaState,error:mediaError,gains:qmlMediaGains,
1922
+ });
1923
+ };
1924
+
1925
+ host.inputClipboard=(element,operation)=>clipboardForInput(qmlBindings.Node.unwrap(element)??element,operation,{read:()=>host.clipboardRead(),write:text=>host.clipboardWrite(text)});
1926
+ host.inputPresentation=(element,options)=>{
1927
+ element=qmlBindings.Node.unwrap(element)??element;
1928
+ if(element?.ownerDocument!==page.document)return;
1929
+ inputPresentation.set(element,Object.assign(inputPresentation.get(element)||{},options));invalidateInput();
1930
+ };
1931
+ }
1932
+ // qml handle end
1933
+
1861
1934
  await page.realm.runDocumentScripts();
1862
1935
  if (!current()) return;
1863
1936
  phase("scripts");
@@ -1957,7 +2030,9 @@ function buildPageRenderer({
1957
2030
  /** Brings style, layout and raster up to date; returns whether it repainted. */
1958
2031
  function refresh() {
1959
2032
  if (disposedRenderer) return false;
2033
+ syncInputCaret();
1960
2034
  if (page.dirty) rebuildRenderTree();
2035
+ if(followCaret) { followCaret=false; revealInputCaret(); }
1961
2036
  if (!page.needsPaint) return false;
1962
2037
  render();
1963
2038
  return true;
@@ -1978,7 +2053,111 @@ function buildPageRenderer({
1978
2053
  if (!prevented && element.tagName === "DETAILS") element.open = !element.open;
1979
2054
  }
1980
2055
 
1981
- function editFocusedText(text, backspace = false) {
2056
+ // qml handle begin: optional per-player PCM gain
2057
+ const qmlMediaGains=new WeakMap();
2058
+ // qml handle end
2059
+
2060
+ let caretElement, caretKey, caretTimer, followCaret=false;
2061
+ function resetInputCaret() {
2062
+ hostClearInterval(caretTimer);caretTimer=null;caretElement=null;caretKey=null;
2063
+ }
2064
+ function invalidateInput() {
2065
+ page.resourceLayoutDirty=true;page.dirty=true;page.needsPaint=true;scheduleFrame();
2066
+ }
2067
+ function syncInputCaret() {
2068
+ const element=page.document?.activeElement;
2069
+ const valid=isTextInput(element)&&element.isConnected;
2070
+ const key=valid?`${element.selectionStart}:${element.selectionEnd}:${element.value}:${inputPresentation.get(element)?.visible}`:'';
2071
+ if(element===caretElement&&key===caretKey)return;
2072
+ resetInputCaret();caretElement=element;caretKey=key;followCaret=true;
2073
+ if(!valid)return;
2074
+ const options=inputPresentation.get(element)||{};options.on=true;inputPresentation.set(element,options);
2075
+ page.resourceLayoutDirty=true;page.dirty=true;page.needsPaint=true;
2076
+ if(element.selectionStart!==element.selectionEnd||options.visible===false)return;
2077
+ caretTimer=hostSetInterval(()=>{
2078
+ if(page.document?.activeElement!==element||!element.isConnected){resetInputCaret();invalidateInput();return;}
2079
+ options.on=!options.on;invalidateInput();
2080
+ },500);
2081
+ caretTimer.unref?.();
2082
+ }
2083
+ function revealInputCaret() {
2084
+ const element=page.document?.activeElement;
2085
+ if(!isTextInput(element))return;
2086
+ const position=element.selectionDirection==='backward'?element.selectionStart:element.selectionEnd;
2087
+ if(position===0){setElementScroll(element,0,0);return;}
2088
+ const caret=layoutBoxOf(inputCaretNode(element)),field=layoutBoxOf(element),geometry=scrollerGeometry(element);
2089
+ if(!caret||!field||!geometry)return;
2090
+ const [top,, ,left]=field.node.style.borderWidths||[0,0,0,0];
2091
+ const padding=field.node.style.padding||[0,0,0,0];
2092
+ const x=caret.box.x-field.box.x-left,y=caret.box.y-field.box.y-top;
2093
+ const px=Number(padding[3])||0,py=Number(padding[0])||0;
2094
+ let sx=geometry.x,sy=geometry.y;
2095
+ if(x<sx+px)sx=x-px;
2096
+ else if(x+1>sx+geometry.clientWidth-px)sx=x+1-geometry.clientWidth+px;
2097
+ if(y<sy+py)sy=y-py;
2098
+ else if(y+caret.box.height>sy+geometry.clientHeight-py)sy=y+caret.box.height-geometry.clientHeight+py;
2099
+ setElementScroll(element,sx,sy);
2100
+ }
2101
+
2102
+ // Hit the same laid-out/shaped runs that paint the control. This is only
2103
+ // performed on a pointer press, never on the animation/capture path.
2104
+ function inputPositionAtPoint(element,x,y) {
2105
+ if(!String(element.value??''))return 0;
2106
+ let best=null;
2107
+ for(const hit of page.displayList?.hits??[]) {
2108
+ const run=hit.textRun;if(!run)continue;
2109
+ const node=page.renderTree.nodesById.get(hit.nodeId);
2110
+ if((inputOwner.get(node?.domNode)??node?.domNode)!==element)continue;
2111
+ let px=x+(hit.fixed?0:viewport.x),py=y+(hit.fixed?0:viewport.y);
2112
+ if(hit.transform){
2113
+ const {a,d,e,f}=hit.transform;if(!a||!d)continue;
2114
+ px=(px-e)/a;py=(py-f)/d;
2115
+ }
2116
+ const top=run.y, height=parseFloat(run.style.lineHeight)||parseFloat(run.style.fontSize)||16;
2117
+ const dy=Math.max(top-py,py-top-height,0),dx=Math.max(run.x-px,px-run.x-run.width,0);
2118
+ if(!best||dy<best.dy||dy===best.dy&&dx<best.dx)best={run,node,px,dy,dx};
2119
+ }
2120
+ if(!best)return 0;
2121
+ const {run,node,px}=best;
2122
+ const paragraph=makeParagraph(run.text,run.style);let at;
2123
+ try{
2124
+ paragraph.layout(Math.max(1,run.width+1));
2125
+ at=paragraph.getGlyphPositionAtCoordinate(px-run.x,0).pos;
2126
+ }finally{paragraph.delete();}
2127
+ let position=(node.valueStart??0)+(run.sourceStart??0)+at;
2128
+ if(element.type==='password')position=[...String(element.value)].slice(0,position).join('').length;
2129
+ return Math.max(0,Math.min(String(element.value).length,position));
2130
+ }
2131
+
2132
+ function clipboardForInput(element,operation,backend={read:clipboardReadInput,write:clipboardWriteInput}) {
2133
+ return inputClipboard(element,operation,{...backend,replace:(text,type)=>{
2134
+ if(page.document.activeElement!==element)return;
2135
+ editFocusedText(text,false,type);syncInputCaret();invalidateInput();
2136
+ }});
2137
+ }
2138
+ function inputDefault(element,event) {
2139
+ if(!isTextInput(element)||event.altKey)return false;
2140
+ if(moveInputSelection(element,event)){syncInputCaret();invalidateInput();return true;}
2141
+ if((event.ctrlKey||event.metaKey)&&['c','x','v'].includes(event.key.toLowerCase())) {
2142
+ let backend;
2143
+
2144
+ // qml handle begin: local applications choose their frontend clipboard transport
2145
+ const host=page.document.__qmlHost;
2146
+ if(host)backend={read:()=>host.clipboardRead(),write:text=>host.clipboardWrite(text)};
2147
+ // qml handle end
2148
+
2149
+ clipboardForInput(element,{c:'copy',x:'cut',v:'paste'}[event.key.toLowerCase()],backend).catch(reportError);
2150
+ return true;
2151
+ }
2152
+ if(event.key==='Delete'&&!element.readOnly) {
2153
+ const at=element.selectionStart;
2154
+ if(at===element.selectionEnd)selectInput(element,at,at+(String(element.value).codePointAt(at)>0xffff?2:1));
2155
+ editFocusedText('',false,'deleteContentForward');invalidateInput();return true;
2156
+ }
2157
+ return false;
2158
+ }
2159
+
2160
+ function editFocusedText(text, backspace = false, inputType = 'insertText') {
1982
2161
  const element = page.document.activeElement;
1983
2162
  if (!element || !["INPUT", "TEXTAREA"].includes(element.tagName) || element.disabled || element.readOnly) return;
1984
2163
  if (element.tagName === "INPUT" && !["text", "search", "url", "email", "tel", "password"].includes(element.type)) return;
@@ -1993,14 +2172,14 @@ function buildPageRenderer({
1993
2172
  if (page.realm) {
1994
2173
  const prevented = page.realm.dispatch(element, new page.window.InputEvent("beforeinput", {
1995
2174
  bubbles: true, cancelable: true, data: backspace ? null : text,
1996
- inputType: backspace ? "deleteContentBackward" : "insertText",
2175
+ inputType: backspace ? "deleteContentBackward" : inputType,
1997
2176
  }));
1998
2177
  if (prevented) return;
1999
2178
  }
2000
2179
  element.value = value.slice(0, start) + text + value.slice(end);
2001
2180
  markStyleDirty(element); // :placeholder-shown, :valid / :invalid
2002
2181
  element.setSelectionRange?.(start + text.length, start + text.length);
2003
- if (page.realm) page.realm.dispatch(element, new page.window.Event("input", { bubbles: true }));
2182
+ if (page.realm) page.realm.dispatch(element, new page.window.InputEvent("input", { bubbles: true, inputType:backspace?"deleteContentBackward":inputType, data:backspace?null:text }));
2004
2183
  page.dirty = true;
2005
2184
  }
2006
2185
 
@@ -2064,6 +2243,20 @@ function buildPageRenderer({
2064
2243
  canvasImage(element) {
2065
2244
  if (!element) return null;
2066
2245
  if (element.tagName === "CANVAS") return canvasResource(element);
2246
+
2247
+ // qml handle begin: local QML may draw a DOM subtree into a canvas
2248
+ if (hasQmlDomImages(page.document) && !["IMG", "VIDEO", "AUDIO", "IMAGE"].includes(element.tagName)) {
2249
+ if (page.dirty) rebuildRenderTree();
2250
+ return captureQmlDomImage(element, {
2251
+ document: page.document, tree: page.renderTree, layout: page.layout,
2252
+ resources: page.resources, backgroundResource, scrollOffsetOf,
2253
+ kit: CanvasKit, paint: paintDisplayList, canvasResource,
2254
+ isOriginClean: resource => resource?.originClean ?? Boolean(resource?.url &&
2255
+ (resource.url.startsWith("data:") || originOf(resource.url) === originOf(page.url))),
2256
+ });
2257
+ }
2258
+ // qml handle end
2259
+
2067
2260
  const resource = page.resources.get(element);
2068
2261
  if (!resource?.image) return null;
2069
2262
  const url = resource.url ?? element.src ?? "";
@@ -2196,6 +2389,7 @@ function buildPageRenderer({
2196
2389
  page.cookie = mergeDocumentCookie(page.cookie, String(value));
2197
2390
  setCookie(String(value));
2198
2391
  },
2392
+ clipboardRead,
2199
2393
  clipboardWrite,
2200
2394
  webSockets,
2201
2395
  pageFetch(request) {
@@ -2560,7 +2754,8 @@ function buildPageRenderer({
2560
2754
  };
2561
2755
  const prevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keydown", init));
2562
2756
  let activated = false;
2563
- if (!prevented && !init.ctrlKey && !init.metaKey && !init.altKey) {
2757
+ const inputHandled=!prevented&&inputDefault(target,init);
2758
+ if (!inputHandled && !prevented && !init.ctrlKey && !init.metaKey && !init.altKey) {
2564
2759
  if ((key === "Enter" || key === " ") && isDisclosureControl(target)) {
2565
2760
  activated = true;
2566
2761
  if (key === "Enter") activateDisclosure(target);
@@ -2579,7 +2774,7 @@ function buildPageRenderer({
2579
2774
  }
2580
2775
  const upPrevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keyup", init));
2581
2776
  if (activated && key === " " && !upPrevented) activateDisclosure(target);
2582
- if (refresh() || prevented || activated) return { x: viewport.x, y: viewport.y };
2777
+ if (refresh() || prevented || activated || inputHandled) return { x: viewport.x, y: viewport.y };
2583
2778
  }
2584
2779
  const pageStep = Math.max(31, viewport.height - 31);
2585
2780
  const delta = {
@@ -2666,7 +2861,7 @@ function buildPageRenderer({
2666
2861
  // and a search form's forwarded field takes the click on the form.
2667
2862
  const hitNode = page.displayList ? renderNodeAtPoint(page.renderTree, page.displayList, x, y, viewport) : null;
2668
2863
  const precise = hitNode?.domNode?.nodeType === 1 ? hitNode.domNode : hitNode?.domNode?.parentElement;
2669
- const target = forwarded ? region.element : precise ?? region?.element ?? null;
2864
+ const target = forwarded ? region.element : inputOwner.get(precise) ?? precise ?? region?.element ?? null;
2670
2865
  const generatedDisclosure = hitNode?.pseudo === "summary" && target?.tagName === "DETAILS";
2671
2866
  if (!target) {
2672
2867
  focusedFrame = null;
@@ -2696,7 +2891,11 @@ function buildPageRenderer({
2696
2891
  // The press focuses the focusable element it lands in, unless mousedown was canceled.
2697
2892
  const focus = () => {
2698
2893
  const focusable = generatedDisclosure ? target : target.closest?.("input, textarea, select, button, summary, [tabindex]");
2699
- if (focusable && !focusable.disabled) focusable.focus();
2894
+ if (focusable && !focusable.disabled) {
2895
+ const position=isTextInput(focusable)&&!forwarded?inputPositionAtPoint(focusable,x,y):null;
2896
+ focusable.focus();
2897
+ if(position!==null){selectInput(focusable,position);syncInputCaret();invalidateInput();}
2898
+ }
2700
2899
  };
2701
2900
  if (!page.realm) focus();
2702
2901
  else {
@@ -2811,12 +3010,16 @@ function buildPageRenderer({
2811
3010
  },
2812
3011
  dispose() {
2813
3012
  if (disposedRenderer) return;
3013
+ resetInputCaret();
2814
3014
  disposedRenderer = true;
2815
3015
  loadGeneration++;
2816
3016
  if (page.document) loadImageResource.cancelDocument(page.document);
2817
3017
  hostClearTimeout(prepaintTimer);
2818
3018
  hostClearTimeout(frameTimer);
2819
3019
  disposeFrames();
3020
+ // qml handle begin: renderer shutdown closes application resources
3021
+ page.document?.__qmlHost?.dispose();
3022
+ // qml handle end
2820
3023
  page.realm?.dispose();
2821
3024
  closePiano();
2822
3025
  disposeVideos();
@@ -2980,7 +3183,10 @@ function buildPageRenderer({
2980
3183
  requestDownload,
2981
3184
  reportError,
2982
3185
  pageFetch,
3186
+ clipboardRead,
2983
3187
  clipboardWrite,
3188
+ clipboardReadInput,
3189
+ clipboardWriteInput,
2984
3190
  webSockets,
2985
3191
  setCookie,
2986
3192
  consoleMessage,
@@ -3141,6 +3347,12 @@ function buildPageRenderer({
3141
3347
  if (resource) page.resources.set(node.domNode, resource);
3142
3348
  else page.resources.delete(node.domNode);
3143
3349
  }
3350
+
3351
+ // qml handle begin: texture sources keep layout/input but paint through their texture
3352
+ if (hasQmlDomImages(page.document)) return filterQmlTextureSources(
3353
+ buildDisplayList(page.layout, page.renderTree, page.resources, backgroundResource, scrollOffsetOf), page.renderTree);
3354
+ // qml handle end
3355
+
3144
3356
  return buildDisplayList(page.layout, page.renderTree, page.resources, backgroundResource, scrollOffsetOf);
3145
3357
  }
3146
3358
 
@@ -4411,6 +4623,11 @@ function buildPageRenderer({
4411
4623
  sound.history.shift();
4412
4624
  }
4413
4625
  const pcm = floatToS16(taken);
4626
+
4627
+ // qml handle begin: apply QML AudioOutput gain to the output chunk only
4628
+ applyQmlMediaGain(pcm,qmlMediaGains.get(state.element));
4629
+ // qml handle end
4630
+
4414
4631
  audio.write(sound.id, pcm);
4415
4632
  sent += pcm.length;
4416
4633
  }