buninu-browser 0.3.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 (296) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +40 -4
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +152 -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 +130 -0
  20. package/frontends/wayland/input.js +75 -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 +162 -0
  26. package/index.js +28 -1
  27. package/lib/audio/shared-connection.js +31 -0
  28. package/lib/cdp-server.js +15 -2
  29. package/lib/clipboard-command.js +10 -0
  30. package/lib/clipboard-powershell.js +22 -0
  31. package/lib/clipboard.js +37 -2
  32. package/lib/document-types.js +11 -0
  33. package/lib/headless-shell.js +54 -12
  34. package/lib/layout/text-layout.js +17 -9
  35. package/lib/paint/display-list.js +6 -1
  36. package/lib/qml/README.md +1427 -0
  37. package/lib/qml/builtins.js +771 -0
  38. package/lib/qml/canvas-dom.js +115 -0
  39. package/lib/qml/canvas.js +76 -0
  40. package/lib/qml/document.js +1199 -0
  41. package/lib/qml/fonts.js +69 -0
  42. package/lib/qml/format.js +9 -0
  43. package/lib/qml/host.js +352 -0
  44. package/lib/qml/images.js +199 -0
  45. package/lib/qml/launcher.js +132 -0
  46. package/lib/qml/loader.js +521 -0
  47. package/lib/qml/media-renderer.js +49 -0
  48. package/lib/qml/media.js +86 -0
  49. package/lib/qml/models.js +622 -0
  50. package/lib/qml/parser.js +381 -0
  51. package/lib/qml/qs +5 -0
  52. package/lib/qml/qscasty +3 -0
  53. package/lib/qml/qsheadless +3 -0
  54. package/lib/qml/qswayland +3 -0
  55. package/lib/qml/reactive.js +182 -0
  56. package/lib/qml/runtime.js +3603 -0
  57. package/lib/qml/shader/fragment.js +83 -0
  58. package/lib/qml/shader/qsb.js +27 -0
  59. package/lib/qml/shader/renderer.js +39 -0
  60. package/lib/qml/shader/runtime.js +61 -0
  61. package/lib/qml/shapes.js +251 -0
  62. package/lib/qml/shortcuts.js +65 -0
  63. package/lib/qml/text.js +109 -0
  64. package/lib/qml/textures.js +126 -0
  65. package/lib/qml/wayland-clipboard.js +32 -0
  66. package/lib/qml/window-constraints.js +16 -0
  67. package/lib/qml/worker-thread.js +70 -0
  68. package/lib/render-tree/index.js +16 -1
  69. package/lib/renderer/bindings.js +16 -1
  70. package/lib/renderer/canvas-2d.js +29 -0
  71. package/lib/renderer/document-selection.js +19 -0
  72. package/lib/renderer/host.js +45 -17
  73. package/lib/renderer/main.js +21 -6
  74. package/lib/renderer/page-realm.js +20 -10
  75. package/lib/renderer/page-renderer.js +497 -42
  76. package/lib/renderer/pointer-move.js +5 -3
  77. package/lib/renderer/render-interval.js +9 -0
  78. package/lib/renderer/text-input-paint.js +54 -0
  79. package/lib/renderer/text-input.js +57 -0
  80. package/lib/style/computed-style.js +10 -4
  81. package/package.json +1 -1
  82. package/test/audio-connection-lifetime.test.js +46 -0
  83. package/test/canvas-paint.test.js +8 -0
  84. package/test/clipboard.test.js +19 -0
  85. package/test/computed-style.test.js +12 -0
  86. package/test/document-selection.test.js +27 -0
  87. package/test/double-click.test.js +85 -0
  88. package/test/fixtures/canvas-damage.js +40 -0
  89. package/test/fixtures/fake-renderer.js +1 -0
  90. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  91. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  92. package/test/fixtures/flea-helpers/bin/bat +3 -0
  93. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  94. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  95. package/test/fixtures/flea-helpers/bin/glow +3 -0
  96. package/test/fixtures/flea-helpers/bin/qs +3 -0
  97. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  98. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  99. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  100. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  101. package/test/fixtures/flea-helpers/check.mjs +36 -0
  102. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  103. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  104. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  105. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  106. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  107. package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
  108. package/test/fixtures/omastorm-helpers/check.mjs +56 -0
  109. package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
  110. package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
  111. package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
  112. package/test/fixtures/pointer-paint-batching.js +23 -0
  113. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  114. package/test/fixtures/qml/flea/LICENSE +21 -0
  115. package/test/fixtures/qml/flea/README.md +25 -0
  116. package/test/fixtures/qml/flea/Theme.qml +8 -0
  117. package/test/fixtures/qml/flea/demo.qml +24 -0
  118. package/test/fixtures/qml/flea/qmldir +3 -0
  119. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  120. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  121. package/test/fixtures/qml/flea-theme/README.md +70 -0
  122. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  123. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  124. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  125. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  126. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  127. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  128. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  129. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  130. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  131. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  132. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  133. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  134. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  135. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  136. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  137. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  138. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  139. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  140. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  141. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  142. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  143. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  144. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  145. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  146. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  147. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  148. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  167. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  168. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  169. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  170. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  171. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  172. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  173. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  174. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  175. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  176. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  177. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  178. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  179. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  180. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  181. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  182. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  183. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  184. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  185. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  186. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  187. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  188. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  189. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  190. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  191. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  192. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  193. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  194. package/test/fixtures/qml/hello.qml +26 -0
  195. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  196. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  197. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  198. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  199. package/test/fixtures/qml/imports/logic/base.js +2 -0
  200. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  201. package/test/fixtures/qml/imports/logic/math.js +3 -0
  202. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  203. package/test/fixtures/qml/imports/main.qml +21 -0
  204. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  205. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  206. package/test/fixtures/qml/multiply.qml +61 -0
  207. package/test/fixtures/qml-canvas-dom.js +90 -0
  208. package/test/fixtures/qml-fonts/main.qml +10 -0
  209. package/test/fixtures/qml-locale/check.mjs +6 -0
  210. package/test/fixtures/qml-locale/main.qml +8 -0
  211. package/test/fixtures/qml-media-audio.js +39 -0
  212. package/test/fixtures/qml-shader/grab.qml +14 -0
  213. package/test/fixtures/qml-shader/gradient.frag +5 -0
  214. package/test/fixtures/qml-shader/main.qml +14 -0
  215. package/test/fixtures/qml-shader/texture-grab.qml +25 -0
  216. package/test/fixtures/qml-shader/texture-size.frag +5 -0
  217. package/test/fixtures/qml-shader-check.mjs +24 -0
  218. package/test/fixtures/title-no-paint.js +20 -0
  219. package/test/helpers/flea-sandbox.js +48 -0
  220. package/test/helpers/quickshell-fixture.js +125 -0
  221. package/test/html-clipboard-wayland.test.js +47 -0
  222. package/test/index.test.js +13 -0
  223. package/test/manual-casty-without-qml.js +87 -0
  224. package/test/manual-double-click.html +21 -0
  225. package/test/manual-flea-application.js +49 -0
  226. package/test/manual-flea-binary.js +62 -0
  227. package/test/manual-flea-qml.js +9 -0
  228. package/test/manual-flea-settings.js +40 -0
  229. package/test/manual-flea-theme.js +40 -0
  230. package/test/manual-mouse-buttons.html +18 -0
  231. package/test/manual-omastorm-engine.mjs +19 -0
  232. package/test/manual-omastorm-shaders.mjs +28 -0
  233. package/test/manual-omastorm-software.mjs +25 -0
  234. package/test/manual-pointer-scroll.html +43 -0
  235. package/test/manual-qml-removal.js +46 -0
  236. package/test/manual-without-qml.js +45 -0
  237. package/test/pointer-drag.test.js +104 -0
  238. package/test/qml-canvas-dom.test.js +37 -0
  239. package/test/qml-canvas.test.js +21 -0
  240. package/test/qml-clipboard-bridge.test.js +25 -0
  241. package/test/qml-clipboard-wayland.test.js +40 -0
  242. package/test/qml-connections-timing.test.js +95 -0
  243. package/test/qml-controls-window.test.js +182 -0
  244. package/test/qml-dynamic.test.js +401 -0
  245. package/test/qml-flea-app.test.js +146 -0
  246. package/test/qml-flea-backend.test.js +78 -0
  247. package/test/qml-flea-columns-backend.test.js +38 -0
  248. package/test/qml-flea-compile.test.js +78 -0
  249. package/test/qml-flea-launcher.test.js +88 -0
  250. package/test/qml-flea-markdown-backend.test.js +31 -0
  251. package/test/qml-flea-media.test.js +28 -0
  252. package/test/qml-flea-navigation-backend.test.js +35 -0
  253. package/test/qml-flea-navigation-timing.test.js +147 -0
  254. package/test/qml-flea-preview-backend.test.js +53 -0
  255. package/test/qml-flea-previews.test.js +163 -0
  256. package/test/qml-flea-rows.test.js +101 -0
  257. package/test/qml-flea-textures.test.js +59 -0
  258. package/test/qml-flea-view-switch.test.js +33 -0
  259. package/test/qml-flea-wayland.test.js +68 -0
  260. package/test/qml-flea.test.js +34 -0
  261. package/test/qml-fonts.test.js +31 -0
  262. package/test/qml-grid-multiline.test.js +48 -0
  263. package/test/qml-host.test.js +238 -0
  264. package/test/qml-imports.test.js +102 -0
  265. package/test/qml-inherited-handlers.test.js +51 -0
  266. package/test/qml-input-selection.test.js +66 -0
  267. package/test/qml-interactions.test.js +309 -0
  268. package/test/qml-language.test.js +167 -0
  269. package/test/qml-launcher.test.js +54 -0
  270. package/test/qml-layout-text.test.js +205 -0
  271. package/test/qml-locale.test.js +45 -0
  272. package/test/qml-media.test.js +63 -0
  273. package/test/qml-nonvisual-visible.test.js +19 -0
  274. package/test/qml-persisted-delegates.test.js +45 -0
  275. package/test/qml-quickshell-controls.test.js +65 -0
  276. package/test/qml-quickshell-settings.test.js +62 -0
  277. package/test/qml-repeater.test.js +72 -0
  278. package/test/qml-shader-runtime.test.js +6 -0
  279. package/test/qml-shortcuts.test.js +39 -0
  280. package/test/qml-socket.test.js +60 -0
  281. package/test/qml-software-shader.test.js +55 -0
  282. package/test/qml-style-updates.test.js +124 -0
  283. package/test/qml-textures.test.js +66 -0
  284. package/test/qml-views.test.js +332 -0
  285. package/test/qml-window-update.test.js +36 -0
  286. package/test/qml-worker.test.js +69 -0
  287. package/test/qml.test.js +83 -0
  288. package/test/render-interval.test.js +33 -0
  289. package/test/renderer-host.test.js +11 -0
  290. package/test/text-input-pointer.test.js +31 -0
  291. package/test/text-input-scroll.test.js +29 -0
  292. package/test/text-input.test.js +47 -0
  293. package/test/title-no-paint.test.js +6 -0
  294. package/test/wayland-frontend.test.js +83 -0
  295. package/test/wayland-scroll.test.js +40 -0
  296. package/test/wayland-urlbar.test.js +61 -0
@@ -22,10 +22,28 @@
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 software shader canvas bridge
33
+ import {paintQmlCanvas} from './canvas-2d.js';
34
+ import {createShaderRenderer} from '../qml/shader/renderer.js';
35
+ // qml handle end
36
+
37
+ // qml handle begin: optional document frontend
38
+ import {createQmlMediaController,applyQmlMediaGain} from '../qml/media-renderer.js';
39
+ import {INTERFACES as qmlBindings} from "./bindings.js";
40
+ import { enableQmlDomImages, hasQmlDomImages, captureQmlDomImage, filterQmlTextureSources, qmlTextureVisibilityChanged } from "../qml/canvas-dom.js";
41
+ import {loadQmlFonts} from '../qml/fonts.js';
42
+ import {createQmlHost} from "../qml/host.js";
43
+ import { loadQmlDocument } from "../qml/document.js";
44
+ import { isQmlDocument } from "../qml/format.js";
45
+ // qml handle end
46
+
29
47
  import { parseHTMLDocument } from "../happy-dom/parser.js";
30
48
  import { LayoutEngine } from "../layout/text-layout.js";
31
49
  import { ScrollViewport } from "../layout/scroll-viewport.js";
@@ -227,6 +245,9 @@ function buildPageRenderer({
227
245
  requestDownload = () => {},
228
246
  reportError = () => {},
229
247
  pageFetch = () => Promise.reject(new TypeError("network access is not available")),
248
+ clipboardReadInput = () => clipboardRead(),
249
+ clipboardWriteInput = text => clipboardWrite(text),
250
+ clipboardRead = () => Promise.reject(Error("Clipboard unavailable")),
230
251
  clipboardWrite = () => Promise.reject(Error("Clipboard unavailable")),
231
252
  webSockets = null,
232
253
  audio = null,
@@ -242,6 +263,7 @@ function buildPageRenderer({
242
263
  mobile = false,
243
264
  verifyStyle = false,
244
265
  fullStyle = false,
266
+ renderMinInterval = 16,
245
267
  // An iframe's document (see syncFrames): frames go to the embedding page,
246
268
  // which needs only the viewport, so nothing is painted ahead.
247
269
  embedded = false,
@@ -252,6 +274,10 @@ function buildPageRenderer({
252
274
  CanvasKit, loadedFamilies, fontCollection, fontFamilies, sansSerifFamily, monospaceFamily, monospaceBoldFamily,
253
275
  genericFamilies, builtinTypefaces,
254
276
  }) {
277
+ // qml handle begin: preserve ordinary page fonts across QML navigation
278
+ const standardFonts = {CanvasKit, loadedFamilies, fontCollection, fontFamilies, builtinTypefaces};
279
+ // qml handle end
280
+
255
281
  let documentFonts = null;
256
282
  let fontSheets = null;
257
283
  let fontEnvironment = "";
@@ -315,6 +341,10 @@ function buildPageRenderer({
315
341
  let framePaintTimer = null;
316
342
  // The frame keyboard input goes to, after a click into it.
317
343
  let focusedFrame = null;
344
+ let pointerButtons = 0, suppressPointerMouse = false;
345
+ const pointerPresses = new Map();
346
+ let selectionDrag = null;
347
+ let selectionVersion = 0;
318
348
  const viewport = new ScrollViewport(800, 600);
319
349
  const page = {
320
350
  url: "about:blank",
@@ -505,7 +535,7 @@ function buildPageRenderer({
505
535
  const paragraph = makeParagraph(text, computedStyle);
506
536
  paragraph.layout(100_000);
507
537
  // getLongestLine() drops trailing white space, which line breaking needs.
508
- width = paragraph.getMaxIntrinsicWidth();
538
+ width = {width:paragraph.getMaxIntrinsicWidth(),ascent:paragraph.getAlphabeticBaseline(),height:paragraph.getHeight()};
509
539
  paragraph.delete();
510
540
  if (++measuredCount > MAX_MEASURED_TEXTS) {
511
541
  measuredWidths.clear();
@@ -514,7 +544,7 @@ function buildPageRenderer({
514
544
  }
515
545
  widths.set(text, width);
516
546
  }
517
- return width;
547
+ return width.width;
518
548
  }
519
549
 
520
550
  // Computed styles are immutable and shared, so each one's key is computed once.
@@ -534,8 +564,12 @@ function buildPageRenderer({
534
564
  }
535
565
 
536
566
  const fontMetricsCache = new Map();
537
- function fontMetrics(computedStyle = null) {
567
+ function fontMetrics(computedStyle = null, text = null) {
538
568
  const key = fontKeyOf(computedStyle);
569
+ if(text) {
570
+ measureText(text,computedStyle);
571
+ return measuredWidths.get(key).get(text);
572
+ }
539
573
  let metrics = fontMetricsCache.get(key);
540
574
  if (!metrics) {
541
575
  const paragraph = makeParagraph("Hg", computedStyle);
@@ -570,6 +604,7 @@ function buildPageRenderer({
570
604
  let rasterSurface = null;
571
605
  let rasterSurfaceWidth = 0;
572
606
  let rasterSurfaceHeight = 0;
607
+ let rasterRegion = null;
573
608
  const rasterReadback = new RasterReadback(CanvasKit, { shared: Boolean(onFrame), allocate: allocateFramePixels });
574
609
  // A scroll or unrelated DOM update does not change text shaping. Keep a
575
610
  // bounded set of visible paragraphs keyed by their complete shaping input.
@@ -637,7 +672,8 @@ function buildPageRenderer({
637
672
  const hints = page.hintsVisible
638
673
  ? page.hints.map((hint) => `${hint.label}@${hint.x + hint.scrollX},${hint.y + hint.scrollY}`).join(";")
639
674
  : "";
640
- return hints;
675
+ const selection=page.window?.getSelection();
676
+ return hints + selectionVersion + (selection?.rangeCount ? `:${selection.anchorOffset}:${selection.focusOffset}:${selection.toString()}` : "");
641
677
  }
642
678
 
643
679
  function sameRasterInput() {
@@ -682,6 +718,36 @@ function buildPageRenderer({
682
718
  schedulePrepaint();
683
719
  }
684
720
 
721
+ /** Conservative canvas-only damage, in document CSS pixels. */
722
+ function canvasDamage(region) {
723
+ const old = rasterized.list, next = page.displayList;
724
+ // Reuse pixels only when geometry and every non-canvas input are stable.
725
+ // Transformed/fixed canvases conservatively take the full-paint path.
726
+ if (!rasterSurface || !Bun.deepEquals(rasterRegion, region)
727
+ || rasterized.scale !== page.deviceScaleFactor || rasterized.background !== page.backgroundColor
728
+ || rasterized.overlay !== overlayKey() || rasterized.frames !== framesVersion
729
+ || rasterized.scrollX !== viewport.x || rasterized.scrollY !== viewport.y
730
+ || !old || !next || old.images.length !== next.images.length
731
+ || !Bun.deepEquals(old.items, next.items)
732
+ || next.items.some(item => item.op === 'drawVideo')) return null;
733
+ let damage = null;
734
+ for (let i = 0; i < next.images.length; i++) {
735
+ if (old.images[i] === next.images[i]) continue;
736
+ const uses = next.items.filter(item => item.image === i);
737
+ if (!uses.length) return null;
738
+ for (const item of uses) {
739
+ if (item.op !== 'drawImage' || item.fixed || item.transform
740
+ || page.renderTree.nodesById.get(item.nodeId)?.domNode?.tagName !== 'CANVAS') return null;
741
+ const r = item.rect;
742
+ if (![r.x, r.y, r.width, r.height].every(Number.isFinite)) return null;
743
+ const left = r.x - 1, top = r.y - 1, right = r.x + r.width + 1, bottom = r.y + r.height + 1;
744
+ damage = damage ? {x:Math.min(damage.x,left),y:Math.min(damage.y,top),right:Math.max(damage.right,right),bottom:Math.max(damage.bottom,bottom)}
745
+ : {x:left,y:top,right,bottom};
746
+ }
747
+ }
748
+ return damage;
749
+ }
750
+
685
751
  /** Rasterizes a page rectangle (CSS pixels) into RGBA pixels. */
686
752
  function rasterTile(region) {
687
753
  // Prepaint can run before a queued canvas/image repaint. Refresh native
@@ -693,6 +759,8 @@ function buildPageRenderer({
693
759
  const scale = page.deviceScaleFactor;
694
760
  const pixelWidth = Math.max(1, Math.round(region.width * scale));
695
761
  const pixelHeight = Math.max(1, Math.round(region.height * scale));
762
+ const damage = canvasDamage(region);
763
+ if (damage) trace?.(`${page.url}: canvas damage ${damage.right-damage.x}x${damage.bottom-damage.y}`);
696
764
  if (!rasterSurface || rasterSurfaceWidth !== pixelWidth || rasterSurfaceHeight !== pixelHeight) {
697
765
  // MakeSurface owns a malloc-backed pixel buffer; delete() alone leaks it.
698
766
  rasterSurface?.dispose();
@@ -703,9 +771,19 @@ function buildPageRenderer({
703
771
  const surface = rasterSurface;
704
772
  if (!surface) throw new Error(`Unable to create a ${pixelWidth}x${pixelHeight} CanvasKit surface`);
705
773
  const canvas = surface.getCanvas();
774
+ canvas.save();
775
+ if (damage) canvas.clipRect(CanvasKit.LTRBRect(
776
+ Math.floor((damage.x-region.x)*scale), Math.floor((damage.y-region.y)*scale),
777
+ Math.ceil((damage.right-region.x)*scale), Math.ceil((damage.bottom-region.y)*scale),
778
+ ), CanvasKit.ClipOp.Intersect, false);
779
+ // Clear and replay all layers inside the damage, including backgrounds
780
+ // and foreground occluders. Transparent canvas pixels must not leave trails.
706
781
  canvas.clear(canvasColor(CanvasKit, page.backgroundColor));
707
- paintDisplayList(canvas, page.displayList, region, scale);
782
+ paintDisplayList(canvas, page.displayList, region, scale, null, damage ? {
783
+ x:damage.x, y:damage.y, width:damage.right-damage.x, height:damage.bottom-damage.y,
784
+ } : region);
708
785
  drawHints(canvas, region);
786
+ canvas.restore();
709
787
  surface.flush();
710
788
  const image = surface.makeImageSnapshot();
711
789
  let pixels;
@@ -713,6 +791,7 @@ function buildPageRenderer({
713
791
  finally { image.delete(); }
714
792
  const videoLayer = makeVideoLayer(canvas, surface, region, scale, pixelWidth, pixelHeight);
715
793
  page.needsPaint = false;
794
+ rasterRegion = {...region};
716
795
  rasterized = {
717
796
  list: page.displayList, overlay: overlayKey(), background: page.backgroundColor, scale: page.deviceScaleFactor,
718
797
  scrollX: viewport.x, scrollY: viewport.y, frames: framesVersion,
@@ -788,10 +867,10 @@ function buildPageRenderer({
788
867
  frameTimer = hostSetTimeout(() => {
789
868
  frameTimer = null;
790
869
  refresh();
791
- // Coalesce ordinary mutations for 16ms. Expensive style/layout rebuilds
792
- // retain a longer idle window so related script mutations can settle
870
+ // Coalesce ordinary mutations for the configured minimum interval.
871
+ // Expensive style/layout rebuilds retain a longer idle window so related script mutations can settle
793
872
  // instead of continuously rebuilding a large document.
794
- }, Math.max(16, lastRebuildMs * 2));
873
+ }, Math.max(renderMinInterval, lastRebuildMs * 2));
795
874
  }
796
875
 
797
876
  /** The viewport as an encoded image (base64), cut from the latest tile. */
@@ -811,7 +890,7 @@ function buildPageRenderer({
811
890
  /** Rasterizes display items intersecting the viewport onto a CanvasKit canvas. */
812
891
  const paintFailures = new Set();
813
892
 
814
- function paintDisplayList(canvas, displayList, view, scale, videoOverride = null) {
893
+ function paintDisplayList(canvas, displayList, view, scale, videoOverride = null, dirtyView = view) {
815
894
  if (!displayList) return;
816
895
  const pixel = (value) => Math.round(value * scale);
817
896
  const deviceRect = (rect) => CanvasKit.XYWHRect(
@@ -838,7 +917,7 @@ function buildPageRenderer({
838
917
  if (rounded) canvas.drawRRect(rounded, paint);
839
918
  else canvas.drawRect(deviceRect(rect), paint);
840
919
  };
841
- for (const item of visibleItems(displayList, view, viewport)) {
920
+ for (const item of visibleItems(displayList, dirtyView, viewport)) {
842
921
  const saved = item.fixed || item.clipRect || item.opacity < 1 || item.transform ? canvas.save() : null;
843
922
  try {
844
923
  if (item.fixed) canvas.translate(viewport.x * scale, viewport.y * scale);
@@ -1013,7 +1092,7 @@ function buildPageRenderer({
1013
1092
  } else if (item.op === "drawSvg") {
1014
1093
  drawSvg(item);
1015
1094
  } else if (item.op === "drawText") {
1016
- const entry = paragraphForPaint(item, scale, pixel(Math.max(1, view.width + view.x - item.x)));
1095
+ const entry = paragraphForPaint(item, scale, pixel(Math.max(1, item.textWidth + 1)));
1017
1096
  try {
1018
1097
  if (item.gradient) {
1019
1098
  // background-clip:text: draw glyphs, then keep the gradient only where they are.
@@ -1027,6 +1106,17 @@ function buildPageRenderer({
1027
1106
  } else {
1028
1107
  canvas.drawParagraph(entry.paragraph, pixel(item.x - view.x), pixel(item.y - view.y));
1029
1108
  }
1109
+ const selected=selectedTextRange(item);
1110
+ if(selected){
1111
+ const boxes=entry.paragraph.getRectsForRange(selected.from,selected.to,CanvasKit.RectHeightStyle.Max,CanvasKit.RectWidthStyle.Tight);
1112
+ const white=paragraphForPaint({...item,color:'rgba(255, 255, 255, 1)',gradient:null},scale,pixel(Math.max(1,item.textWidth+1)));
1113
+ const paint=new CanvasKit.Paint();paint.setColor(canvasColor(CanvasKit,'rgba(56, 116, 242, 1)'));
1114
+ try{for(const box of boxes){
1115
+ const r=box.rect,ox=pixel(item.x-view.x),oy=pixel(item.y-view.y);
1116
+ const rect=CanvasKit.LTRBRect(r[0]+ox,r[1]+oy,r[2]+ox,r[3]+oy);
1117
+ canvas.save();canvas.clipRect(rect,CanvasKit.ClipOp.Intersect,false);canvas.drawRect(rect,paint);canvas.drawParagraph(white.paragraph,ox,oy);canvas.restore();
1118
+ }}finally{paint.delete();if(!white.retained)white.paragraph.delete();}
1119
+ }
1030
1120
  } finally {
1031
1121
  if (!entry.retained) entry.paragraph.delete();
1032
1122
  }
@@ -1383,6 +1473,14 @@ function buildPageRenderer({
1383
1473
  const traits = selectorTraitsFor(sheets);
1384
1474
  const records = styleChanges.records;
1385
1475
  if (styleChanges.observer) records.push(...styleChanges.observer.takeRecords());
1476
+
1477
+ // qml handle begin: QML visual attributes invalidate retained rendering
1478
+ if (hasQmlDomImages(page.document) && qmlTextureVisibilityChanged(records)) {
1479
+ page.resourcePaintDirty = true;
1480
+ page.needsPaint = true;
1481
+ }
1482
+ // qml handle end
1483
+
1386
1484
  const roots = styleChanges.roots;
1387
1485
  // Explicit roots mean that state-dependent selectors may need restyling;
1388
1486
  // they do not by themselves change render-tree content. DOM mutation
@@ -1549,18 +1647,23 @@ function buildPageRenderer({
1549
1647
 
1550
1648
  function replaceParsedDocument(parsed, styleSheets = [], imageResources = emptyImageResources(), linkSheets = null) {
1551
1649
  if (page.document) loadImageResource.cancelDocument(page.document);
1650
+ resetInputCaret();
1552
1651
  rasterReadback.dispose();
1553
1652
  clearPaintParagraphs();
1554
1653
  resetDocumentFonts();
1555
1654
  disposeFrames();
1556
1655
  closePiano();
1557
1656
  disposeVideos();
1657
+ // qml handle begin: dispose application resources with their document
1658
+ page.document?.__qmlHost?.dispose();
1659
+ // qml handle end
1558
1660
  page.realm?.dispose();
1559
1661
  page.realm = null;
1560
1662
  page.window?.happyDOM?.close?.();
1561
1663
  for (const image of page.decodedImages) image.delete();
1562
1664
  disposeBackgrounds();
1563
1665
  page.window = parsed.window;
1666
+ pointerButtons = 0; suppressPointerMouse = false; pointerPresses.clear(); selectionDrag=null;
1564
1667
  page.document = parsed.document;
1565
1668
  observeDocumentCanvases(parsed.document, (node) => {
1566
1669
  if (disposedRenderer || node.ownerDocument !== page.document || !node.isConnected) return;
@@ -1762,6 +1865,16 @@ function buildPageRenderer({
1762
1865
  onLifecycle?.("DOMContentLoaded", generation);
1763
1866
  onLifecycle?.("load", generation);
1764
1867
  };
1868
+ // qml handle begin: optional local Qt font directory, never inherited by HTML
1869
+ const selectedFonts = page.scripting === "host" && url.startsWith("file:") && isQmlDocument(url, contentType)
1870
+ ? await loadQmlFonts(standardFonts, message => reportError(new Error(message))) : standardFonts;
1871
+ if (!current()) return;
1872
+ if (fontCollection !== selectedFonts.fontCollection) {
1873
+ ({loadedFamilies, fontCollection, fontFamilies, builtinTypefaces} = selectedFonts);
1874
+ clearFontCaches();
1875
+ }
1876
+ // qml handle end
1877
+
1765
1878
  page.url = url;
1766
1879
  page.cookie = typeof cookie === "string" ? cookie : "";
1767
1880
  if (error) {
@@ -1797,8 +1910,23 @@ function buildPageRenderer({
1797
1910
  trace(`${url}: ${name} ${Math.round(now - phaseStart)}ms`);
1798
1911
  phaseStart = now;
1799
1912
  } : () => {};
1800
- const isHtml = contentType.toLowerCase().includes("html");
1801
- const parsed = parseHTMLDocument(isHtml ? source : "<body></body>", url, referrer);
1913
+
1914
+ let isHtml = contentType.toLowerCase().includes("html");
1915
+ let parsed;
1916
+
1917
+ // qml handle begin: supply DOM before the default HTML parser
1918
+ if (isQmlDocument(url, contentType)) {
1919
+ const qmlHost = page.scripting === "host" && new URL(url).protocol === "file:" ? createQmlHost(url) : null;
1920
+ parsed = await loadQmlDocument(source, url, {scripting: page.scripting !== "off", fetchResource, importRoots:qmlHost?.importRoots});
1921
+ parsed.document.__qmlHost = qmlHost;
1922
+ if (qmlHost) enableQmlDomImages(parsed.document);
1923
+ if (!current()) {qmlHost?.dispose();return;}
1924
+ // Reuse the existing stylesheet/script/lifecycle pipeline for QML DOM.
1925
+ isHtml = true;
1926
+ }
1927
+ // qml handle end
1928
+
1929
+ parsed ??= parseHTMLDocument(isHtml ? source : "<body></body>", url, referrer);
1802
1930
  phase("parse");
1803
1931
  // Stylesheets block rendering and scripts; images do not (only the
1804
1932
  // window load event waits for them, see whenImagesLoaded): one slow image
@@ -1858,6 +1986,43 @@ function buildPageRenderer({
1858
1986
  mode: page.scripting,
1859
1987
  hooks: realmHooks(generation),
1860
1988
  });
1989
+ // qml handle begin: only a local QML host realm receives this capability
1990
+ if (page.document.__qmlHost) page.realm.pageGlobal.__qmlHost = page.document.__qmlHost;
1991
+ // qml handle end
1992
+
1993
+ // qml handle begin: QML methods/properties adapt shared media and text controls
1994
+ if(page.document.__qmlHost) {
1995
+ const host=page.document.__qmlHost;
1996
+ host.media=element=>{
1997
+ element=qmlBindings.Node.unwrap(element)??element;
1998
+ if(element?.ownerDocument!==page.document||!['VIDEO','AUDIO'].includes(element.tagName))throw Error('Invalid QML media element');
1999
+ return createQmlMediaController(element,{
2000
+ valid:e=>e.ownerDocument===page.document,state:e=>page.media.get(e),
2001
+ close:closeMedia,forget:e=>{page.media.delete(e);page.resources.delete(e);},ensure:mediaStateFor,
2002
+ load:loadMedia,seek:seekMedia,play:playMedia,pause:pauseMedia,
2003
+ snapshot:mediaState,error:mediaError,gains:qmlMediaGains,
2004
+ });
2005
+ };
2006
+
2007
+ host.inputClipboard=(element,operation)=>clipboardForInput(qmlBindings.Node.unwrap(element)??element,operation,{read:()=>host.clipboardRead(),write:text=>host.clipboardWrite(text)});
2008
+ host.inputPresentation=(element,options)=>{
2009
+ element=qmlBindings.Node.unwrap(element)??element;
2010
+ if(element?.ownerDocument!==page.document)return;
2011
+ inputPresentation.set(element,Object.assign(inputPresentation.get(element)||{},options));invalidateInput();
2012
+ };
2013
+ }
2014
+ // qml handle end
2015
+
2016
+ // qml handle begin: local QML fragment shader capability
2017
+ if (page.document.__qmlHost) {
2018
+ const host=page.document.__qmlHost;
2019
+ host.shader=createShaderRenderer({kit:CanvasKit,unwrap:e=>qmlBindings.Node.unwrap(e),
2020
+ valid:e=>e?.ownerDocument===page.document,
2021
+ resource:e=>e.tagName==='CANVAS'?canvasResource(e):page.resources.get(e),
2022
+ paint:paintQmlCanvas,track:fn=>host.trackResource(fn)});
2023
+ }
2024
+ // qml handle end
2025
+
1861
2026
  await page.realm.runDocumentScripts();
1862
2027
  if (!current()) return;
1863
2028
  phase("scripts");
@@ -1957,7 +2122,9 @@ function buildPageRenderer({
1957
2122
  /** Brings style, layout and raster up to date; returns whether it repainted. */
1958
2123
  function refresh() {
1959
2124
  if (disposedRenderer) return false;
2125
+ syncInputCaret();
1960
2126
  if (page.dirty) rebuildRenderTree();
2127
+ if(followCaret) { followCaret=false; revealInputCaret(); }
1961
2128
  if (!page.needsPaint) return false;
1962
2129
  render();
1963
2130
  return true;
@@ -1978,7 +2145,159 @@ function buildPageRenderer({
1978
2145
  if (!prevented && element.tagName === "DETAILS") element.open = !element.open;
1979
2146
  }
1980
2147
 
1981
- function editFocusedText(text, backspace = false) {
2148
+ // qml handle begin: optional per-player PCM gain
2149
+ const qmlMediaGains=new WeakMap();
2150
+ // qml handle end
2151
+
2152
+ let caretElement, caretKey, caretTimer, followCaret=false;
2153
+ function resetInputCaret() {
2154
+ hostClearInterval(caretTimer);caretTimer=null;caretElement=null;caretKey=null;
2155
+ }
2156
+ function invalidateInput() {
2157
+ page.resourceLayoutDirty=true;page.dirty=true;page.needsPaint=true;scheduleFrame();
2158
+ }
2159
+ function syncInputCaret() {
2160
+ const element=page.document?.activeElement;
2161
+ const valid=isTextInput(element)&&element.isConnected;
2162
+ const key=valid?`${element.selectionStart}:${element.selectionEnd}:${element.value}:${inputPresentation.get(element)?.visible}`:'';
2163
+ if(element===caretElement&&key===caretKey)return;
2164
+ resetInputCaret();caretElement=element;caretKey=key;followCaret=true;
2165
+ if(!valid)return;
2166
+ const options=inputPresentation.get(element)||{};options.on=true;inputPresentation.set(element,options);
2167
+ page.resourceLayoutDirty=true;page.dirty=true;page.needsPaint=true;
2168
+ if(element.selectionStart!==element.selectionEnd||options.visible===false)return;
2169
+ caretTimer=hostSetInterval(()=>{
2170
+ if(page.document?.activeElement!==element||!element.isConnected){resetInputCaret();invalidateInput();return;}
2171
+ options.on=!options.on;invalidateInput();
2172
+ },500);
2173
+ caretTimer.unref?.();
2174
+ }
2175
+ function revealInputCaret() {
2176
+ const element=page.document?.activeElement;
2177
+ if(!isTextInput(element))return;
2178
+ const position=element.selectionDirection==='backward'?element.selectionStart:element.selectionEnd;
2179
+ if(position===0){setElementScroll(element,0,0);return;}
2180
+ const caret=layoutBoxOf(inputCaretNode(element)),field=layoutBoxOf(element),geometry=scrollerGeometry(element);
2181
+ if(!caret||!field||!geometry)return;
2182
+ const [top,, ,left]=field.node.style.borderWidths||[0,0,0,0];
2183
+ const padding=field.node.style.padding||[0,0,0,0];
2184
+ const x=caret.box.x-field.box.x-left,y=caret.box.y-field.box.y-top;
2185
+ const px=Number(padding[3])||0,py=Number(padding[0])||0;
2186
+ let sx=geometry.x,sy=geometry.y;
2187
+ if(x<sx+px)sx=x-px;
2188
+ else if(x+1>sx+geometry.clientWidth-px)sx=x+1-geometry.clientWidth+px;
2189
+ if(y<sy+py)sy=y-py;
2190
+ else if(y+caret.box.height>sy+geometry.clientHeight-py)sy=y+caret.box.height-geometry.clientHeight+py;
2191
+ setElementScroll(element,sx,sy);
2192
+ }
2193
+
2194
+ // Hit the same laid-out/shaped runs that paint the control. This is only
2195
+ // performed on a pointer press, never on the animation/capture path.
2196
+ function inputPositionAtPoint(element,x,y) {
2197
+ if(!String(element.value??''))return 0;
2198
+ let best=null;
2199
+ for(const hit of page.displayList?.hits??[]) {
2200
+ const run=hit.textRun;if(!run)continue;
2201
+ const node=page.renderTree.nodesById.get(hit.nodeId);
2202
+ if((inputOwner.get(node?.domNode)??node?.domNode)!==element)continue;
2203
+ let px=x+(hit.fixed?0:viewport.x),py=y+(hit.fixed?0:viewport.y);
2204
+ if(hit.transform){
2205
+ const {a,d,e,f}=hit.transform;if(!a||!d)continue;
2206
+ px=(px-e)/a;py=(py-f)/d;
2207
+ }
2208
+ const top=run.y, height=parseFloat(run.style.lineHeight)||parseFloat(run.style.fontSize)||16;
2209
+ const dy=Math.max(top-py,py-top-height,0),dx=Math.max(run.x-px,px-run.x-run.width,0);
2210
+ if(!best||dy<best.dy||dy===best.dy&&dx<best.dx)best={run,node,px,dy,dx};
2211
+ }
2212
+ if(!best)return 0;
2213
+ const {run,node,px}=best;
2214
+ const paragraph=makeParagraph(run.text,run.style);let at;
2215
+ try{
2216
+ paragraph.layout(Math.max(1,run.width+1));
2217
+ at=paragraph.getGlyphPositionAtCoordinate(px-run.x,0).pos;
2218
+ }finally{paragraph.delete();}
2219
+ let position=(node.valueStart??0)+(run.sourceStart??0)+at;
2220
+ if(element.type==='password')position=[...String(element.value)].slice(0,position).join('').length;
2221
+ return Math.max(0,Math.min(String(element.value).length,position));
2222
+ }
2223
+
2224
+ function selectableText(node){
2225
+ for(let el=node.parentElement;el;el=el.parentElement){
2226
+ const value=styleEngine.get(el)?.userSelect;
2227
+ if(value==='none')return false;
2228
+ if(value==='text'||value==='all')return true;
2229
+ }
2230
+ return true;
2231
+ }
2232
+ function documentTextPoint(x,y,target=null) {
2233
+ let best=null;
2234
+ for(const hit of page.displayList?.hits??[]){
2235
+ const run=hit.textRun,node=page.renderTree.nodesById.get(hit.nodeId)?.domNode;
2236
+ if(!run||node?.nodeType!==3||!node.isConnected||!selectableText(node))continue;
2237
+ if(target && !target.contains(node))continue;
2238
+ if(node.parentElement?.closest('button,input,textarea,select,canvas,script,style'))continue;
2239
+ let px=x+(hit.fixed?0:viewport.x),py=y+(hit.fixed?0:viewport.y);
2240
+ if(hit.transform){const {a,d,e,f}=hit.transform;if(!a||!d)continue;px=(px-e)/a;py=(py-f)/d;}
2241
+ if(hit.clipRect && (px<hit.clipRect.x||py<hit.clipRect.y||px>hit.clipRect.x+hit.clipRect.width||py>hit.clipRect.y+hit.clipRect.height))continue;
2242
+ const height=parseFloat(run.style.lineHeight)||parseFloat(run.style.fontSize)||16;
2243
+ const dy=Math.max(run.y-py,py-run.y-height,0),dx=Math.max(run.x-px,px-run.x-run.width,0);
2244
+ if(!best||dy<best.dy||dy===best.dy&&dx<best.dx)best={run,node,px,dy,dx};
2245
+ }
2246
+ if(!best)return null;
2247
+ const {run,node,px}=best,p=makeParagraph(run.text,run.style);
2248
+ try{p.layout(Math.max(1,run.width+1));return {node,offset:Math.max(0,Math.min(node.textContent.length,(run.sourceStart??0)+p.getGlyphPositionAtCoordinate(px-run.x,0).pos))};}
2249
+ finally{p.delete();}
2250
+ }
2251
+ function extendDrag(x,y){
2252
+ if(selectionDrag.element){
2253
+ const el=selectionDrag.element;if(!el.isConnected||page.document.activeElement!==el){selectionDrag=null;return;}
2254
+ selectInput(el,selectionDrag.anchor,inputPositionAtPoint(el,x,y));syncInputCaret();invalidateInput();
2255
+ }else{
2256
+ const point=documentTextPoint(x,y);if(!point||!selectionDrag.node.isConnected)return;
2257
+ page.window.getSelection().setBaseAndExtent(selectionDrag.node,selectionDrag.offset,point.node,point.offset);
2258
+ selectionVersion++;
2259
+ page.needsPaint=true;
2260
+ }
2261
+ }
2262
+ function selectedTextRange(item){
2263
+ const sel=page.window.getSelection(),node=page.renderTree.nodesById.get(item.nodeId)?.domNode;
2264
+ if(!sel?.rangeCount||sel.isCollapsed||node?.nodeType!==3||!node.isConnected||!selectableText(node))return null;
2265
+ const range=sel.getRangeAt(0);if(!range.intersectsNode(node))return null;
2266
+ const base=item.sourceStart??0;
2267
+ const from=Math.max(0,(range.startContainer===node?range.startOffset:0)-base);
2268
+ const to=Math.min(item.text.length,(range.endContainer===node?range.endOffset:node.textContent.length)-base);
2269
+ return to>from?{from,to}:null;
2270
+ }
2271
+
2272
+ function clipboardForInput(element,operation,backend={read:clipboardReadInput,write:clipboardWriteInput}) {
2273
+ return inputClipboard(element,operation,{...backend,replace:(text,type)=>{
2274
+ if(page.document.activeElement!==element)return;
2275
+ editFocusedText(text,false,type);syncInputCaret();invalidateInput();
2276
+ }});
2277
+ }
2278
+ function inputDefault(element,event) {
2279
+ if(!isTextInput(element)||event.altKey)return false;
2280
+ if(moveInputSelection(element,event)){syncInputCaret();invalidateInput();return true;}
2281
+ if((event.ctrlKey||event.metaKey)&&['c','x','v'].includes(event.key.toLowerCase())) {
2282
+ let backend;
2283
+
2284
+ // qml handle begin: local applications choose their frontend clipboard transport
2285
+ const host=page.document.__qmlHost;
2286
+ if(host)backend={read:()=>host.clipboardRead(),write:text=>host.clipboardWrite(text)};
2287
+ // qml handle end
2288
+
2289
+ clipboardForInput(element,{c:'copy',x:'cut',v:'paste'}[event.key.toLowerCase()],backend).catch(reportError);
2290
+ return true;
2291
+ }
2292
+ if(event.key==='Delete'&&!element.readOnly) {
2293
+ const at=element.selectionStart;
2294
+ if(at===element.selectionEnd)selectInput(element,at,at+(String(element.value).codePointAt(at)>0xffff?2:1));
2295
+ editFocusedText('',false,'deleteContentForward');invalidateInput();return true;
2296
+ }
2297
+ return false;
2298
+ }
2299
+
2300
+ function editFocusedText(text, backspace = false, inputType = 'insertText') {
1982
2301
  const element = page.document.activeElement;
1983
2302
  if (!element || !["INPUT", "TEXTAREA"].includes(element.tagName) || element.disabled || element.readOnly) return;
1984
2303
  if (element.tagName === "INPUT" && !["text", "search", "url", "email", "tel", "password"].includes(element.type)) return;
@@ -1993,14 +2312,14 @@ function buildPageRenderer({
1993
2312
  if (page.realm) {
1994
2313
  const prevented = page.realm.dispatch(element, new page.window.InputEvent("beforeinput", {
1995
2314
  bubbles: true, cancelable: true, data: backspace ? null : text,
1996
- inputType: backspace ? "deleteContentBackward" : "insertText",
2315
+ inputType: backspace ? "deleteContentBackward" : inputType,
1997
2316
  }));
1998
2317
  if (prevented) return;
1999
2318
  }
2000
2319
  element.value = value.slice(0, start) + text + value.slice(end);
2001
2320
  markStyleDirty(element); // :placeholder-shown, :valid / :invalid
2002
2321
  element.setSelectionRange?.(start + text.length, start + text.length);
2003
- if (page.realm) page.realm.dispatch(element, new page.window.Event("input", { bubbles: true }));
2322
+ if (page.realm) page.realm.dispatch(element, new page.window.InputEvent("input", { bubbles: true, inputType:backspace?"deleteContentBackward":inputType, data:backspace?null:text }));
2004
2323
  page.dirty = true;
2005
2324
  }
2006
2325
 
@@ -2064,6 +2383,20 @@ function buildPageRenderer({
2064
2383
  canvasImage(element) {
2065
2384
  if (!element) return null;
2066
2385
  if (element.tagName === "CANVAS") return canvasResource(element);
2386
+
2387
+ // qml handle begin: local QML may draw a DOM subtree into a canvas
2388
+ if (hasQmlDomImages(page.document) && !["IMG", "VIDEO", "AUDIO", "IMAGE"].includes(element.tagName)) {
2389
+ if (page.dirty) rebuildRenderTree();
2390
+ return captureQmlDomImage(element, {
2391
+ document: page.document, tree: page.renderTree, layout: page.layout,
2392
+ resources: page.resources, backgroundResource, scrollOffsetOf,
2393
+ kit: CanvasKit, paint: paintDisplayList, canvasResource,
2394
+ isOriginClean: resource => resource?.originClean ?? Boolean(resource?.url &&
2395
+ (resource.url.startsWith("data:") || originOf(resource.url) === originOf(page.url))),
2396
+ });
2397
+ }
2398
+ // qml handle end
2399
+
2067
2400
  const resource = page.resources.get(element);
2068
2401
  if (!resource?.image) return null;
2069
2402
  const url = resource.url ?? element.src ?? "";
@@ -2105,6 +2438,7 @@ function buildPageRenderer({
2105
2438
  }
2106
2439
  },
2107
2440
  whenImagesLoaded: () => Promise.all([page.imagesLoaded, documentFonts.ready(), ...[...page.backgrounds.values()].map((entry) => entry.loaded)]),
2441
+ selectionChanged(){selectionVersion++;page.needsPaint=true;scheduleFrame();},
2108
2442
  afterTask() {},
2109
2443
  boundsOf(element) {
2110
2444
  if (page.dirty) rebuildRenderTree("script read geometry");
@@ -2196,6 +2530,7 @@ function buildPageRenderer({
2196
2530
  page.cookie = mergeDocumentCookie(page.cookie, String(value));
2197
2531
  setCookie(String(value));
2198
2532
  },
2533
+ clipboardRead,
2199
2534
  clipboardWrite,
2200
2535
  webSockets,
2201
2536
  pageFetch(request) {
@@ -2533,9 +2868,9 @@ function buildPageRenderer({
2533
2868
  schedulePrepaint();
2534
2869
  },
2535
2870
  title() {
2536
- refresh();
2537
- // Live value: scripts may have set document.title without touching layout.
2538
- return page.document?.title || page.title;
2871
+ // DOM title changes are synchronous and do not require layout or paint.
2872
+ // An empty document title is also a live value, not a cached-title fallback.
2873
+ return page.document?.title ?? page.title;
2539
2874
  },
2540
2875
  scroll(deltaX, deltaY) {
2541
2876
  if (viewport.scrollBy(deltaX, deltaY)) render();
@@ -2560,7 +2895,11 @@ function buildPageRenderer({
2560
2895
  };
2561
2896
  const prevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keydown", init));
2562
2897
  let activated = false;
2563
- if (!prevented && !init.ctrlKey && !init.metaKey && !init.altKey) {
2898
+ const inputHandled=!prevented&&inputDefault(target,init);
2899
+ if(!prevented&&!inputHandled&&(init.ctrlKey||init.metaKey)&&init.key.toLowerCase()==='c'){
2900
+ const text=page.window.getSelection().toString();if(text)clipboardWriteInput(text).catch(reportError);
2901
+ }
2902
+ if (!inputHandled && !prevented && !init.ctrlKey && !init.metaKey && !init.altKey) {
2564
2903
  if ((key === "Enter" || key === " ") && isDisclosureControl(target)) {
2565
2904
  activated = true;
2566
2905
  if (key === "Enter") activateDisclosure(target);
@@ -2579,7 +2918,7 @@ function buildPageRenderer({
2579
2918
  }
2580
2919
  const upPrevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keyup", init));
2581
2920
  if (activated && key === " " && !upPrevented) activateDisclosure(target);
2582
- if (refresh() || prevented || activated) return { x: viewport.x, y: viewport.y };
2921
+ if (refresh() || prevented || activated || inputHandled) return { x: viewport.x, y: viewport.y };
2583
2922
  }
2584
2923
  const pageStep = Math.max(31, viewport.height - 31);
2585
2924
  const delta = {
@@ -2629,18 +2968,29 @@ function buildPageRenderer({
2629
2968
  return wheel(Number(x), Number(y), Number(deltaX) || 0, Number(deltaY) || 0, options);
2630
2969
  },
2631
2970
  /** Move the pointer in viewport coordinates, including boundary events. */
2632
- move(x, y) {
2633
- refresh();
2634
- const target = elementAtPoint(page.renderTree, page.displayList, Number(x), Number(y), viewport);
2635
- dispatchPointerMove(page.window, page.realm, target, Number(x), Number(y));
2636
- refresh();
2971
+ move(x, y, options = {}) {
2972
+ // Hit testing needs current geometry, not a rasterized frame. Dispatch
2973
+ // every motion (drawing needs every point), but let the frame scheduler
2974
+ // combine their paints instead of rasterizing the whole page per event.
2975
+ if (page.dirty) rebuildRenderTree("pointer hit test");
2976
+ const target = elementAtPoint(page.renderTree, page.displayList, Number(x), Number(y), viewport) ?? (pointerButtons ? page.document.documentElement : null);
2977
+ const inside = target?.tagName === 'IFRAME' ? frameAt(target, x, y) : null;
2978
+ if (inside) inside.frame.renderer.move(inside.x, inside.y, options);
2979
+ dispatchPointerMove(page.window, page.realm, target, Number(x), Number(y), {...options, buttons: pointerButtons, suppressMouse:suppressPointerMouse});
2980
+ if(selectionDrag && (pointerButtons&1)) extendDrag(x,y);
2981
+ if (page.dirty || page.needsPaint) scheduleFrame();
2637
2982
  },
2638
2983
  /**
2639
2984
  * Clicks at a viewport point: the page receives a click event first, and
2640
2985
  * unless it calls preventDefault() a link under the point is returned for
2641
2986
  * the controller to navigate to.
2642
2987
  */
2643
- click(x, y) {
2988
+ pointerButton(x, y, button, state, options = {}) {
2989
+ if (button < 272 || button > 274) return null;
2990
+ return this.click(x, y, {...options, button: button === 273 ? 'right' : button === 274 ? 'middle' : 'left', phase: state ? 'down' : 'up'});
2991
+ },
2992
+ click(x, y, options = {}) {
2993
+ const detail = Number.isInteger(options.clickCount) && options.clickCount > 0 ? options.clickCount : 1;
2644
2994
  refresh();
2645
2995
  let region = hitTest(page.renderTree, page.layout, x, y, viewport, viewport);
2646
2996
  let forwarded = false;
@@ -2666,7 +3016,7 @@ function buildPageRenderer({
2666
3016
  // and a search form's forwarded field takes the click on the form.
2667
3017
  const hitNode = page.displayList ? renderNodeAtPoint(page.renderTree, page.displayList, x, y, viewport) : null;
2668
3018
  const precise = hitNode?.domNode?.nodeType === 1 ? hitNode.domNode : hitNode?.domNode?.parentElement;
2669
- const target = forwarded ? region.element : precise ?? region?.element ?? null;
3019
+ const target = options.activationTarget ?? (forwarded ? region.element : inputOwner.get(precise) ?? precise ?? region?.element ?? (options.phase ? page.document.documentElement : null));
2670
3020
  const generatedDisclosure = hitNode?.pseudo === "summary" && target?.tagName === "DETAILS";
2671
3021
  if (!target) {
2672
3022
  focusedFrame = null;
@@ -2679,11 +3029,97 @@ function buildPageRenderer({
2679
3029
  const inFrame = target.tagName === "IFRAME" ? frameAt(target, x, y) : null;
2680
3030
  focusedFrame = inFrame?.frame ?? null;
2681
3031
  if (inFrame) {
2682
- const followed = inFrame.frame.renderer.click(inFrame.x, inFrame.y);
3032
+ const followed = inFrame.frame.renderer.click(inFrame.x, inFrame.y, options);
2683
3033
  const href = typeof followed === "string" ? followed : followed?.fragmentFrom ? null : followed?.url;
2684
3034
  if (href && !followed?.download) void loadFrame(inFrame.frame, href);
2685
3035
  return null;
2686
3036
  }
3037
+ const mouseButton = options.button === 'right' || options.button === 2 ? 2
3038
+ : options.button === 'middle' || options.button === 1 ? 1 : 0;
3039
+ // The press focuses the focusable element it lands in, unless mousedown was canceled.
3040
+ const focus = () => {
3041
+ selectionDrag=null;
3042
+ const focusable = generatedDisclosure ? target : target.closest?.("input, textarea, select, button, summary, [tabindex]");
3043
+ if (focusable && !focusable.disabled) {
3044
+ const position=isTextInput(focusable)&&!forwarded?inputPositionAtPoint(focusable,x,y):null;
3045
+ focusable.focus();
3046
+ if(position!==null){selectInput(focusable,position);syncInputCaret();invalidateInput();if(options.phase==='down')selectionDrag={element:focusable,anchor:position};}
3047
+ } else if(mouseButton===0){
3048
+ const point=documentTextPoint(x,y,target);
3049
+ if(point)page.document.activeElement?.blur?.();
3050
+ selectionVersion++;page.window.getSelection().removeAllRanges();
3051
+ if(point){page.window.getSelection().collapse(point.node,point.offset);if(options.phase==='down')selectionDrag=point;}
3052
+ page.needsPaint=true;
3053
+ }
3054
+ };
3055
+ const modifiers = options.modifiers ?? 0;
3056
+ const mouseInit = { bubbles:true, cancelable:true, composed:true,
3057
+ clientX:Number(x), clientY:Number(y), button:mouseButton, detail,
3058
+ altKey:!!(modifiers&1), ctrlKey:!!(modifiers&2),
3059
+ metaKey:!!(modifiers&4), shiftKey:!!(modifiers&8) };
3060
+ if (options.phase) {
3061
+ const mask = mouseButton === 0 ? 1 : mouseButton === 1 ? 4 : 2;
3062
+ const before = pointerButtons;
3063
+ const pressed = pointerPresses.get(mouseButton);
3064
+ pointerButtons = options.phase === 'down' ? before | mask : before & ~mask;
3065
+ const Pointer = page.window.PointerEvent ?? page.window.MouseEvent;
3066
+ const pointer = {...mouseInit, detail:0, buttons:pointerButtons,
3067
+ pointerId:1, pointerType:'mouse', isPrimary:true};
3068
+ if (options.phase === 'down') {
3069
+ const record = {target, document:page.document, suppressMouse:false};
3070
+ pointerPresses.set(mouseButton, record);
3071
+ if (page.realm) {
3072
+ if (before === 0) suppressPointerMouse = page.realm.dispatch(target,new Pointer('pointerdown',pointer));
3073
+ record.suppressMouse = suppressPointerMouse;
3074
+ if (before !== 0) page.realm.dispatch(target,new Pointer('pointermove',pointer));
3075
+ if (!record.suppressMouse) {
3076
+ const prevented = page.realm.dispatch(target,new page.window.MouseEvent('mousedown',{...mouseInit,buttons:pointerButtons}));
3077
+ if (mouseButton === 0 && !prevented) focus();
3078
+ }
3079
+ if (mouseButton === 2) page.realm.dispatch(target,new Pointer('contextmenu',pointer));
3080
+ } else if (mouseButton === 0) focus();
3081
+ refresh();
3082
+ return null;
3083
+ }
3084
+ let selectedByDrag=false;
3085
+ if(mouseButton===0){
3086
+ if(selectionDrag){extendDrag(x,y);selectedByDrag=!selectionDrag?.element&&!page.window.getSelection().isCollapsed;}
3087
+ selectionDrag=null;
3088
+ }
3089
+ pointerPresses.delete(mouseButton);
3090
+ if (page.realm) {
3091
+ page.realm.dispatch(target,new Pointer(pointerButtons ? 'pointermove' : 'pointerup',pointer));
3092
+ if (!suppressPointerMouse) page.realm.dispatch(target,new page.window.MouseEvent('mouseup',{...mouseInit,buttons:pointerButtons}));
3093
+ }
3094
+ if (!pointerButtons) suppressPointerMouse = false;
3095
+ // Click/auxclick targets the common ancestor of the press and release.
3096
+ // Removed press targets and releases without a press do not activate.
3097
+ let activation = target;
3098
+ if (!pressed || pressed.document !== page.document || !pressed.target.isConnected) activation = null;
3099
+ while (activation && !activation.contains(pressed.target)) activation = activation.parentElement;
3100
+ refresh();
3101
+ return activation ? this.click(x,y,{...options,phase:undefined,activationOnly:true,activationTarget:activation,selectionDragged:selectedByDrag}) : null;
3102
+ }
3103
+ if (mouseButton !== 0) {
3104
+ // Non-primary clicks reach DOM handlers without primary activation
3105
+ // (navigation, form submission, controls, text-caret placement).
3106
+ if (page.realm) {
3107
+ const init = mouseInit;
3108
+ const Pointer = page.window.PointerEvent ?? page.window.MouseEvent;
3109
+ const pointer = {...init, detail:0, pointerId:1, pointerType:'mouse', isPrimary:true};
3110
+ const buttons = mouseButton === 2 ? 2 : 4;
3111
+ if (!options.activationOnly) {
3112
+ page.realm.dispatch(target,new Pointer('pointerdown',{...pointer,buttons}));
3113
+ page.realm.dispatch(target,new page.window.MouseEvent('mousedown',{...init,buttons}));
3114
+ if(mouseButton===2)page.realm.dispatch(target,new Pointer('contextmenu',{...pointer,buttons}));
3115
+ page.realm.dispatch(target,new Pointer('pointerup',{...pointer,buttons:0}));
3116
+ page.realm.dispatch(target,new page.window.MouseEvent('mouseup',{...init,buttons:0}));
3117
+ }
3118
+ page.realm.dispatch(target,new Pointer('auxclick',{...pointer,detail,buttons:pointerButtons}));
3119
+ refresh();
3120
+ }
3121
+ return null;
3122
+ }
2687
3123
  if (page.url === "about:audio") {
2688
3124
  const key = target.closest?.("[data-piano-note]");
2689
3125
  const note = Number(key?.getAttribute("data-piano-note"));
@@ -2693,27 +3129,27 @@ function buildPageRenderer({
2693
3129
  return null;
2694
3130
  }
2695
3131
  }
2696
- // The press focuses the focusable element it lands in, unless mousedown was canceled.
2697
- const focus = () => {
2698
- const focusable = generatedDisclosure ? target : target.closest?.("input, textarea, select, button, summary, [tabindex]");
2699
- if (focusable && !focusable.disabled) focusable.focus();
2700
- };
2701
- if (!page.realm) focus();
3132
+ if (!page.realm) { if (!options.activationOnly) focus(); }
2702
3133
  else {
2703
3134
  const controlStates = controlStatesBeforeActivation();
2704
3135
  // A press and release, then the click (UI Events §3.5).
2705
- const init = { bubbles: true, cancelable: true, composed: true, clientX: Number(x), clientY: Number(y), button: 0 };
3136
+ const init = { ...mouseInit, buttons: pointerButtons };
2706
3137
  const PointerEvent = page.window.PointerEvent ?? page.window.MouseEvent;
2707
- const pointer = { ...init, pointerId: 1, pointerType: "mouse", isPrimary: true };
2708
- page.realm.dispatch(target, new PointerEvent("pointerdown", { ...pointer, buttons: 1 }));
2709
- if (!page.realm.dispatch(target, new page.window.MouseEvent("mousedown", { ...init, buttons: 1 }))) focus();
2710
- page.realm.dispatch(target, new PointerEvent("pointerup", pointer));
2711
- page.realm.dispatch(target, new page.window.MouseEvent("mouseup", init));
2712
- const prevented = page.realm.dispatch(target, new page.window.MouseEvent("click", { ...init, detail: 1 }));
3138
+ const pointer = { ...init, detail: 0, pointerId: 1, pointerType: "mouse", isPrimary: true };
3139
+ if (!options.activationOnly) {
3140
+ page.realm.dispatch(target, new PointerEvent("pointerdown", { ...pointer, buttons: 1 }));
3141
+ if (!page.realm.dispatch(target, new page.window.MouseEvent("mousedown", { ...init, buttons: 1 }))) focus();
3142
+ page.realm.dispatch(target, new PointerEvent("pointerup", pointer));
3143
+ page.realm.dispatch(target, new page.window.MouseEvent("mouseup", init));
3144
+ }
3145
+ const prevented = page.realm.dispatch(target, new page.window.MouseEvent("click", init));
3146
+ // dblclick follows the second click even when its default action was cancelled.
3147
+ if (detail === 2) page.realm.dispatch(target, new page.window.MouseEvent("dblclick", init));
2713
3148
  invalidateActivatedControls(controlStates);
2714
3149
  refresh();
2715
3150
  if (prevented) return null;
2716
3151
  }
3152
+ if (options.selectionDragged) return null;
2717
3153
  if (generatedDisclosure) {
2718
3154
  target.open = !target.open;
2719
3155
  refresh();
@@ -2811,12 +3247,16 @@ function buildPageRenderer({
2811
3247
  },
2812
3248
  dispose() {
2813
3249
  if (disposedRenderer) return;
3250
+ resetInputCaret();
2814
3251
  disposedRenderer = true;
2815
3252
  loadGeneration++;
2816
3253
  if (page.document) loadImageResource.cancelDocument(page.document);
2817
3254
  hostClearTimeout(prepaintTimer);
2818
3255
  hostClearTimeout(frameTimer);
2819
3256
  disposeFrames();
3257
+ // qml handle begin: renderer shutdown closes application resources
3258
+ page.document?.__qmlHost?.dispose();
3259
+ // qml handle end
2820
3260
  page.realm?.dispose();
2821
3261
  closePiano();
2822
3262
  disposeVideos();
@@ -2973,6 +3413,7 @@ function buildPageRenderer({
2973
3413
  // Made at once, as is its initial about:blank document (HTML §4.8.5): a
2974
3414
  // script can use its contentWindow as soon as the iframe is inserted.
2975
3415
  const renderer = buildPageRenderer({
3416
+ renderMinInterval,
2976
3417
  fetchResource: rawFetchResource,
2977
3418
  requestNavigation: (url, downloadName) => {
2978
3419
  if (downloadName === null) void loadFrame(frame, url);
@@ -2980,7 +3421,10 @@ function buildPageRenderer({
2980
3421
  requestDownload,
2981
3422
  reportError,
2982
3423
  pageFetch,
3424
+ clipboardRead,
2983
3425
  clipboardWrite,
3426
+ clipboardReadInput,
3427
+ clipboardWriteInput,
2984
3428
  webSockets,
2985
3429
  setCookie,
2986
3430
  consoleMessage,
@@ -3141,6 +3585,12 @@ function buildPageRenderer({
3141
3585
  if (resource) page.resources.set(node.domNode, resource);
3142
3586
  else page.resources.delete(node.domNode);
3143
3587
  }
3588
+
3589
+ // qml handle begin: texture sources keep layout/input but paint through their texture
3590
+ if (hasQmlDomImages(page.document)) return filterQmlTextureSources(
3591
+ buildDisplayList(page.layout, page.renderTree, page.resources, backgroundResource, scrollOffsetOf), page.renderTree);
3592
+ // qml handle end
3593
+
3144
3594
  return buildDisplayList(page.layout, page.renderTree, page.resources, backgroundResource, scrollOffsetOf);
3145
3595
  }
3146
3596
 
@@ -4411,6 +4861,11 @@ function buildPageRenderer({
4411
4861
  sound.history.shift();
4412
4862
  }
4413
4863
  const pcm = floatToS16(taken);
4864
+
4865
+ // qml handle begin: apply QML AudioOutput gain to the output chunk only
4866
+ applyQmlMediaGain(pcm,qmlMediaGains.get(state.element));
4867
+ // qml handle end
4868
+
4414
4869
  audio.write(sound.id, pcm);
4415
4870
  sent += pcm.length;
4416
4871
  }