buninu-browser 0.2.4 → 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 (249) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +102 -2
  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 +57 -0
  30. package/lib/document-types.js +11 -0
  31. package/lib/headless-shell.js +78 -13
  32. package/lib/layout/text-layout.js +17 -9
  33. package/lib/paint/display-list.js +5 -1
  34. package/lib/paint/frame.js +35 -3
  35. package/lib/paint/raw-frames.js +47 -0
  36. package/lib/qml/README.md +971 -0
  37. package/lib/qml/builtins.js +700 -0
  38. package/lib/qml/canvas-dom.js +115 -0
  39. package/lib/qml/document.js +1176 -0
  40. package/lib/qml/format.js +9 -0
  41. package/lib/qml/host.js +324 -0
  42. package/lib/qml/images.js +199 -0
  43. package/lib/qml/launcher.js +132 -0
  44. package/lib/qml/loader.js +517 -0
  45. package/lib/qml/media-renderer.js +49 -0
  46. package/lib/qml/media.js +86 -0
  47. package/lib/qml/models.js +622 -0
  48. package/lib/qml/parser.js +381 -0
  49. package/lib/qml/qs +5 -0
  50. package/lib/qml/qscasty +3 -0
  51. package/lib/qml/qsheadless +3 -0
  52. package/lib/qml/qswayland +3 -0
  53. package/lib/qml/reactive.js +159 -0
  54. package/lib/qml/runtime.js +3381 -0
  55. package/lib/qml/shapes.js +243 -0
  56. package/lib/qml/text.js +109 -0
  57. package/lib/qml/textures.js +123 -0
  58. package/lib/qml/wayland-clipboard.js +32 -0
  59. package/lib/qml/worker-thread.js +70 -0
  60. package/lib/render-tree/index.js +16 -1
  61. package/lib/renderer/bindings.js +11 -0
  62. package/lib/renderer/canvas-2d.js +13 -0
  63. package/lib/renderer/compositor-channel.js +103 -0
  64. package/lib/renderer/compositor.js +25 -33
  65. package/lib/renderer/flags.js +1 -1
  66. package/lib/renderer/host.js +128 -92
  67. package/lib/renderer/main.js +38 -4
  68. package/lib/renderer/page-realm.js +32 -1
  69. package/lib/renderer/page-renderer.js +250 -21
  70. package/lib/renderer/text-input-paint.js +54 -0
  71. package/lib/renderer/text-input.js +57 -0
  72. package/lib/style/computed-style.js +2 -2
  73. package/package.json +1 -1
  74. package/test/clipboard.test.js +54 -0
  75. package/test/fixtures/browser-raw-demo.html +36 -0
  76. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  77. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  78. package/test/fixtures/flea-helpers/bin/bat +3 -0
  79. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  80. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  81. package/test/fixtures/flea-helpers/bin/glow +3 -0
  82. package/test/fixtures/flea-helpers/bin/qs +3 -0
  83. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  84. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  85. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  86. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  87. package/test/fixtures/flea-helpers/check.mjs +36 -0
  88. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  89. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  90. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  91. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  92. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  93. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  94. package/test/fixtures/qml/flea/LICENSE +21 -0
  95. package/test/fixtures/qml/flea/README.md +25 -0
  96. package/test/fixtures/qml/flea/Theme.qml +8 -0
  97. package/test/fixtures/qml/flea/demo.qml +24 -0
  98. package/test/fixtures/qml/flea/qmldir +3 -0
  99. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  100. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  101. package/test/fixtures/qml/flea-theme/README.md +70 -0
  102. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  103. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  104. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  105. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  106. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  107. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  108. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  109. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  110. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  111. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  112. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  113. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  114. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  115. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  116. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  117. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  118. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  119. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  120. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  121. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  122. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  123. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  124. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  125. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  126. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  127. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  128. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  129. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  130. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  131. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  132. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  133. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  134. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  135. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  136. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  137. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  138. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  139. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  140. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  141. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  142. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  143. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  144. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  145. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  146. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  147. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  148. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  167. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  168. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  169. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  170. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  171. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  172. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  173. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  174. package/test/fixtures/qml/hello.qml +26 -0
  175. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  176. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  177. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  178. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  179. package/test/fixtures/qml/imports/logic/base.js +2 -0
  180. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  181. package/test/fixtures/qml/imports/logic/math.js +3 -0
  182. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  183. package/test/fixtures/qml/imports/main.qml +21 -0
  184. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  185. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  186. package/test/fixtures/qml/multiply.qml +61 -0
  187. package/test/fixtures/qml-canvas-dom.js +90 -0
  188. package/test/fixtures/qml-media-audio.js +39 -0
  189. package/test/fixtures/raw-capture-benchmark.json +76 -0
  190. package/test/helpers/flea-sandbox.js +48 -0
  191. package/test/helpers/quickshell-fixture.js +125 -0
  192. package/test/html-clipboard-wayland.test.js +47 -0
  193. package/test/index.test.js +13 -0
  194. package/test/manual-casty-without-qml.js +87 -0
  195. package/test/manual-flea-application.js +49 -0
  196. package/test/manual-flea-binary.js +62 -0
  197. package/test/manual-flea-qml.js +9 -0
  198. package/test/manual-flea-settings.js +40 -0
  199. package/test/manual-flea-theme.js +40 -0
  200. package/test/manual-qml-removal.js +46 -0
  201. package/test/manual-without-qml.js +45 -0
  202. package/test/qml-canvas-dom.test.js +37 -0
  203. package/test/qml-clipboard-bridge.test.js +25 -0
  204. package/test/qml-clipboard-wayland.test.js +40 -0
  205. package/test/qml-connections-timing.test.js +73 -0
  206. package/test/qml-dynamic.test.js +401 -0
  207. package/test/qml-flea-app.test.js +146 -0
  208. package/test/qml-flea-backend.test.js +78 -0
  209. package/test/qml-flea-columns-backend.test.js +38 -0
  210. package/test/qml-flea-compile.test.js +78 -0
  211. package/test/qml-flea-launcher.test.js +88 -0
  212. package/test/qml-flea-markdown-backend.test.js +31 -0
  213. package/test/qml-flea-media.test.js +28 -0
  214. package/test/qml-flea-navigation-backend.test.js +35 -0
  215. package/test/qml-flea-navigation-timing.test.js +147 -0
  216. package/test/qml-flea-preview-backend.test.js +53 -0
  217. package/test/qml-flea-previews.test.js +163 -0
  218. package/test/qml-flea-rows.test.js +101 -0
  219. package/test/qml-flea-textures.test.js +59 -0
  220. package/test/qml-flea-view-switch.test.js +33 -0
  221. package/test/qml-flea-wayland.test.js +68 -0
  222. package/test/qml-flea.test.js +34 -0
  223. package/test/qml-host.test.js +238 -0
  224. package/test/qml-imports.test.js +102 -0
  225. package/test/qml-inherited-handlers.test.js +51 -0
  226. package/test/qml-input-selection.test.js +66 -0
  227. package/test/qml-interactions.test.js +309 -0
  228. package/test/qml-language.test.js +167 -0
  229. package/test/qml-launcher.test.js +54 -0
  230. package/test/qml-layout-text.test.js +205 -0
  231. package/test/qml-media.test.js +63 -0
  232. package/test/qml-persisted-delegates.test.js +45 -0
  233. package/test/qml-quickshell-controls.test.js +65 -0
  234. package/test/qml-quickshell-settings.test.js +62 -0
  235. package/test/qml-repeater.test.js +72 -0
  236. package/test/qml-style-updates.test.js +124 -0
  237. package/test/qml-textures.test.js +66 -0
  238. package/test/qml-views.test.js +332 -0
  239. package/test/qml-worker.test.js +69 -0
  240. package/test/qml.test.js +83 -0
  241. package/test/raw-capture.test.js +152 -0
  242. package/test/renderer-host.test.js +11 -0
  243. package/test/text-input-pointer.test.js +31 -0
  244. package/test/text-input-scroll.test.js +29 -0
  245. package/test/text-input.test.js +47 -0
  246. package/test/wayland-frontend.test.js +83 -0
  247. package/test/wayland-urlbar.test.js +38 -0
  248. package/test/window-resize.test.js +36 -0
  249. package/tools/bench-raw-capture.js +32 -0
@@ -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,22 @@ 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
+ },
1607
+ clipboardWrite: async text => {
1608
+ try {
1609
+ const url = new URL(document.URL);
1610
+ const secure = url.protocol === 'https:' || (url.protocol === 'http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname));
1611
+ if (!secure || !hooks.clipboardWrite) throw Error('Clipboard unavailable');
1612
+ await hooks.clipboardWrite(text);
1613
+ }
1614
+ catch { throw new DOMException('Clipboard write was not allowed or failed', 'NotAllowedError'); }
1615
+ },
1593
1616
  sendBeacon: (url, data) => {
1594
1617
  network.fetch(url, { method: "POST", body: data, mode: "no-cors", credentials: "include", keepalive: true })
1595
1618
  .catch(() => {});
@@ -2672,7 +2695,7 @@ function navigatorPlatform(userAgent) {
2672
2695
  * global exposes for them. Desktop Chromium lists five PDF viewer plugins
2673
2696
  * and their two MIME types whatever is installed (HTML §8.9.1.6).
2674
2697
  */
2675
- function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile, viewport, performance, storages, sendBeacon }) {
2698
+ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile, viewport, performance, storages, sendBeacon, clipboardWrite, clipboardRead }) {
2676
2699
  const EventTargetPrototype = eventTargets.EventTarget.prototype;
2677
2700
  // An instance an event target's listeners can be added to.
2678
2701
  const eventTarget = (prototype) => {
@@ -2750,6 +2773,13 @@ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile
2750
2773
  }, targetOptions);
2751
2774
  const NavigatorUAData = userAgentDataMembers ? define("NavigatorUAData", userAgentDataMembers) : null;
2752
2775
  const languages = Object.freeze(["en-US"]);
2776
+ const Clipboard = define('Clipboard', {
2777
+ readText() { return clipboardRead(); },
2778
+ writeText(text) {
2779
+ if (arguments.length===0) return Promise.reject(new TypeError('writeText requires text'));
2780
+ return clipboardWrite(String(text));
2781
+ },
2782
+ }, targetOptions);
2753
2783
  const Navigator = define("Navigator", {
2754
2784
  userAgent,
2755
2785
  appCodeName: "Mozilla",
@@ -2775,6 +2805,7 @@ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile
2775
2805
  ...(NavigatorUAData ? { userAgentData: NavigatorUAData.instance } : {}),
2776
2806
  connection: NetworkInformation.instance,
2777
2807
  permissions: Permissions.instance,
2808
+ clipboard: Clipboard.instance,
2778
2809
  sendBeacon(url, data = null) { return sendBeacon(url, data); },
2779
2810
  javaEnabled() { return false; },
2780
2811
  getGamepads() { return [null, null, null, null]; },
@@ -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";
@@ -42,7 +54,7 @@ import { absolutizeStyleSheetUrls, extractFontFaces, matchSelectors, mediaQueryM
42
54
  import { compileSelector, matchesCompiled } from "../happy-dom/selector-matcher.js";
43
55
  import { elementFeatures, sheetTraits } from "../style/selector-traits.js";
44
56
  import { buildDisplayList, visibleItems } from "../paint/display-list.js";
45
- import { backgroundBytes, cropViewport, encodeImageBase64 } from "../paint/frame.js";
57
+ import { backgroundBytes, rawFrame, captureViewport, cropViewport, encodeImageBase64 } from "../paint/frame.js";
46
58
  import { FrameCaptureCache } from "../paint/frame-cache.js";
47
59
  import { RasterReadback } from "../paint/raster-readback.js";
48
60
  import { insetRadii, roundedRect } from "../paint/rounded-rect.js";
@@ -227,6 +239,10 @@ 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")),
245
+ clipboardWrite = () => Promise.reject(Error("Clipboard unavailable")),
230
246
  webSockets = null,
231
247
  audio = null,
232
248
  setCookie = () => {},
@@ -504,7 +520,7 @@ function buildPageRenderer({
504
520
  const paragraph = makeParagraph(text, computedStyle);
505
521
  paragraph.layout(100_000);
506
522
  // getLongestLine() drops trailing white space, which line breaking needs.
507
- width = paragraph.getMaxIntrinsicWidth();
523
+ width = {width:paragraph.getMaxIntrinsicWidth(),ascent:paragraph.getAlphabeticBaseline(),height:paragraph.getHeight()};
508
524
  paragraph.delete();
509
525
  if (++measuredCount > MAX_MEASURED_TEXTS) {
510
526
  measuredWidths.clear();
@@ -513,7 +529,7 @@ function buildPageRenderer({
513
529
  }
514
530
  widths.set(text, width);
515
531
  }
516
- return width;
532
+ return width.width;
517
533
  }
518
534
 
519
535
  // Computed styles are immutable and shared, so each one's key is computed once.
@@ -533,8 +549,12 @@ function buildPageRenderer({
533
549
  }
534
550
 
535
551
  const fontMetricsCache = new Map();
536
- function fontMetrics(computedStyle = null) {
552
+ function fontMetrics(computedStyle = null, text = null) {
537
553
  const key = fontKeyOf(computedStyle);
554
+ if(text) {
555
+ measureText(text,computedStyle);
556
+ return measuredWidths.get(key).get(text);
557
+ }
538
558
  let metrics = fontMetricsCache.get(key);
539
559
  if (!metrics) {
540
560
  const paragraph = makeParagraph("Hg", computedStyle);
@@ -787,18 +807,22 @@ function buildPageRenderer({
787
807
  frameTimer = hostSetTimeout(() => {
788
808
  frameTimer = null;
789
809
  refresh();
790
- // Leave one rebuild-sized idle window after an expensive frame. Without
791
- // it, a large document can spend all its time producing back-to-back
792
- // frames while scripts are still making related mutations.
793
- }, Math.max(50, lastRebuildMs * 2));
810
+ // Coalesce ordinary mutations for 16ms. Expensive style/layout rebuilds
811
+ // retain a longer idle window so related script mutations can settle
812
+ // instead of continuously rebuilding a large document.
813
+ }, Math.max(16, lastRebuildMs * 2));
794
814
  }
795
815
 
796
816
  /** The viewport as an encoded image (base64), cut from the latest tile. */
797
- async function viewportPng({ format = "png", quality = 80, optimizeForSpeed = false } = {}) {
817
+ async function viewportPng({ format = "png", quality = 80, optimizeForSpeed = false, clip } = {}) {
798
818
  if (!lastTile) return "";
799
- const key = `${tileSerial}:${viewport.x}:${viewport.y}:${page.width}:${page.height}:${format}:${quality}:${optimizeForSpeed}`;
819
+ if (format === 'rgba' || format === 'bgra') {
820
+ const {pixels,width,height} = captureViewport(lastTile, viewport.x, viewport.y, page.width, page.height,clip);
821
+ return rawFrame(pixels,width,height,format);
822
+ }
823
+ const key = `${tileSerial}:${viewport.x}:${viewport.y}:${page.width}:${page.height}:${format}:${quality}:${optimizeForSpeed}:${JSON.stringify(clip)}`;
800
824
  return captures.get(key, () => {
801
- const { pixels, width, height } = cropViewport(lastTile, viewport.x, viewport.y, page.width, page.height);
825
+ const { pixels, width, height } = captureViewport(lastTile, viewport.x, viewport.y, page.width, page.height,clip);
802
826
  return encodeImageBase64(pixels, width, height, { format, quality, optimizeForSpeed });
803
827
  });
804
828
  }
@@ -1008,7 +1032,7 @@ function buildPageRenderer({
1008
1032
  } else if (item.op === "drawSvg") {
1009
1033
  drawSvg(item);
1010
1034
  } else if (item.op === "drawText") {
1011
- 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)));
1012
1036
  try {
1013
1037
  if (item.gradient) {
1014
1038
  // background-clip:text: draw glyphs, then keep the gradient only where they are.
@@ -1378,6 +1402,14 @@ function buildPageRenderer({
1378
1402
  const traits = selectorTraitsFor(sheets);
1379
1403
  const records = styleChanges.records;
1380
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
+
1381
1413
  const roots = styleChanges.roots;
1382
1414
  // Explicit roots mean that state-dependent selectors may need restyling;
1383
1415
  // they do not by themselves change render-tree content. DOM mutation
@@ -1544,12 +1576,16 @@ function buildPageRenderer({
1544
1576
 
1545
1577
  function replaceParsedDocument(parsed, styleSheets = [], imageResources = emptyImageResources(), linkSheets = null) {
1546
1578
  if (page.document) loadImageResource.cancelDocument(page.document);
1579
+ resetInputCaret();
1547
1580
  rasterReadback.dispose();
1548
1581
  clearPaintParagraphs();
1549
1582
  resetDocumentFonts();
1550
1583
  disposeFrames();
1551
1584
  closePiano();
1552
1585
  disposeVideos();
1586
+ // qml handle begin: dispose application resources with their document
1587
+ page.document?.__qmlHost?.dispose();
1588
+ // qml handle end
1553
1589
  page.realm?.dispose();
1554
1590
  page.realm = null;
1555
1591
  page.window?.happyDOM?.close?.();
@@ -1792,8 +1828,23 @@ function buildPageRenderer({
1792
1828
  trace(`${url}: ${name} ${Math.round(now - phaseStart)}ms`);
1793
1829
  phaseStart = now;
1794
1830
  } : () => {};
1795
- const isHtml = contentType.toLowerCase().includes("html");
1796
- 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);
1797
1848
  phase("parse");
1798
1849
  // Stylesheets block rendering and scripts; images do not (only the
1799
1850
  // window load event waits for them, see whenImagesLoaded): one slow image
@@ -1853,6 +1904,33 @@ function buildPageRenderer({
1853
1904
  mode: page.scripting,
1854
1905
  hooks: realmHooks(generation),
1855
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
+
1856
1934
  await page.realm.runDocumentScripts();
1857
1935
  if (!current()) return;
1858
1936
  phase("scripts");
@@ -1952,7 +2030,9 @@ function buildPageRenderer({
1952
2030
  /** Brings style, layout and raster up to date; returns whether it repainted. */
1953
2031
  function refresh() {
1954
2032
  if (disposedRenderer) return false;
2033
+ syncInputCaret();
1955
2034
  if (page.dirty) rebuildRenderTree();
2035
+ if(followCaret) { followCaret=false; revealInputCaret(); }
1956
2036
  if (!page.needsPaint) return false;
1957
2037
  render();
1958
2038
  return true;
@@ -1973,7 +2053,111 @@ function buildPageRenderer({
1973
2053
  if (!prevented && element.tagName === "DETAILS") element.open = !element.open;
1974
2054
  }
1975
2055
 
1976
- 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') {
1977
2161
  const element = page.document.activeElement;
1978
2162
  if (!element || !["INPUT", "TEXTAREA"].includes(element.tagName) || element.disabled || element.readOnly) return;
1979
2163
  if (element.tagName === "INPUT" && !["text", "search", "url", "email", "tel", "password"].includes(element.type)) return;
@@ -1988,14 +2172,14 @@ function buildPageRenderer({
1988
2172
  if (page.realm) {
1989
2173
  const prevented = page.realm.dispatch(element, new page.window.InputEvent("beforeinput", {
1990
2174
  bubbles: true, cancelable: true, data: backspace ? null : text,
1991
- inputType: backspace ? "deleteContentBackward" : "insertText",
2175
+ inputType: backspace ? "deleteContentBackward" : inputType,
1992
2176
  }));
1993
2177
  if (prevented) return;
1994
2178
  }
1995
2179
  element.value = value.slice(0, start) + text + value.slice(end);
1996
2180
  markStyleDirty(element); // :placeholder-shown, :valid / :invalid
1997
2181
  element.setSelectionRange?.(start + text.length, start + text.length);
1998
- 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 }));
1999
2183
  page.dirty = true;
2000
2184
  }
2001
2185
 
@@ -2059,6 +2243,20 @@ function buildPageRenderer({
2059
2243
  canvasImage(element) {
2060
2244
  if (!element) return null;
2061
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
+
2062
2260
  const resource = page.resources.get(element);
2063
2261
  if (!resource?.image) return null;
2064
2262
  const url = resource.url ?? element.src ?? "";
@@ -2191,6 +2389,8 @@ function buildPageRenderer({
2191
2389
  page.cookie = mergeDocumentCookie(page.cookie, String(value));
2192
2390
  setCookie(String(value));
2193
2391
  },
2392
+ clipboardRead,
2393
+ clipboardWrite,
2194
2394
  webSockets,
2195
2395
  pageFetch(request) {
2196
2396
  const networkPending = pageFetch(request);
@@ -2437,6 +2637,7 @@ function buildPageRenderer({
2437
2637
  * visualViewport.scale.
2438
2638
  */
2439
2639
  resize(width, height, deviceScaleFactor = 1, pageScale = 1) {
2640
+ const resized = width !== page.width || height !== page.height;
2440
2641
  page.pageScale = Number.isFinite(pageScale) && pageScale > 0 ? pageScale : 1;
2441
2642
  page.width = width;
2442
2643
  page.height = height;
@@ -2445,6 +2646,10 @@ function buildPageRenderer({
2445
2646
  : 1;
2446
2647
  viewport.resize(width, height);
2447
2648
  rebuildRenderTree();
2649
+ // Listeners must observe the new viewport and layout. A repeated metrics
2650
+ // request (or DPR-only change) does not resize the CSS viewport.
2651
+ if (resized && page.realm)
2652
+ page.realm.dispatch(page.window, new page.window.Event("resize"));
2448
2653
  render();
2449
2654
  },
2450
2655
  screenshot(options) {
@@ -2549,7 +2754,8 @@ function buildPageRenderer({
2549
2754
  };
2550
2755
  const prevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keydown", init));
2551
2756
  let activated = false;
2552
- 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) {
2553
2759
  if ((key === "Enter" || key === " ") && isDisclosureControl(target)) {
2554
2760
  activated = true;
2555
2761
  if (key === "Enter") activateDisclosure(target);
@@ -2568,7 +2774,7 @@ function buildPageRenderer({
2568
2774
  }
2569
2775
  const upPrevented = page.realm.dispatch(target, new page.window.KeyboardEvent("keyup", init));
2570
2776
  if (activated && key === " " && !upPrevented) activateDisclosure(target);
2571
- if (refresh() || prevented || activated) return { x: viewport.x, y: viewport.y };
2777
+ if (refresh() || prevented || activated || inputHandled) return { x: viewport.x, y: viewport.y };
2572
2778
  }
2573
2779
  const pageStep = Math.max(31, viewport.height - 31);
2574
2780
  const delta = {
@@ -2655,7 +2861,7 @@ function buildPageRenderer({
2655
2861
  // and a search form's forwarded field takes the click on the form.
2656
2862
  const hitNode = page.displayList ? renderNodeAtPoint(page.renderTree, page.displayList, x, y, viewport) : null;
2657
2863
  const precise = hitNode?.domNode?.nodeType === 1 ? hitNode.domNode : hitNode?.domNode?.parentElement;
2658
- const target = forwarded ? region.element : precise ?? region?.element ?? null;
2864
+ const target = forwarded ? region.element : inputOwner.get(precise) ?? precise ?? region?.element ?? null;
2659
2865
  const generatedDisclosure = hitNode?.pseudo === "summary" && target?.tagName === "DETAILS";
2660
2866
  if (!target) {
2661
2867
  focusedFrame = null;
@@ -2685,7 +2891,11 @@ function buildPageRenderer({
2685
2891
  // The press focuses the focusable element it lands in, unless mousedown was canceled.
2686
2892
  const focus = () => {
2687
2893
  const focusable = generatedDisclosure ? target : target.closest?.("input, textarea, select, button, summary, [tabindex]");
2688
- 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
+ }
2689
2899
  };
2690
2900
  if (!page.realm) focus();
2691
2901
  else {
@@ -2800,12 +3010,16 @@ function buildPageRenderer({
2800
3010
  },
2801
3011
  dispose() {
2802
3012
  if (disposedRenderer) return;
3013
+ resetInputCaret();
2803
3014
  disposedRenderer = true;
2804
3015
  loadGeneration++;
2805
3016
  if (page.document) loadImageResource.cancelDocument(page.document);
2806
3017
  hostClearTimeout(prepaintTimer);
2807
3018
  hostClearTimeout(frameTimer);
2808
3019
  disposeFrames();
3020
+ // qml handle begin: renderer shutdown closes application resources
3021
+ page.document?.__qmlHost?.dispose();
3022
+ // qml handle end
2809
3023
  page.realm?.dispose();
2810
3024
  closePiano();
2811
3025
  disposeVideos();
@@ -2969,6 +3183,10 @@ function buildPageRenderer({
2969
3183
  requestDownload,
2970
3184
  reportError,
2971
3185
  pageFetch,
3186
+ clipboardRead,
3187
+ clipboardWrite,
3188
+ clipboardReadInput,
3189
+ clipboardWriteInput,
2972
3190
  webSockets,
2973
3191
  setCookie,
2974
3192
  consoleMessage,
@@ -3129,6 +3347,12 @@ function buildPageRenderer({
3129
3347
  if (resource) page.resources.set(node.domNode, resource);
3130
3348
  else page.resources.delete(node.domNode);
3131
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
+
3132
3356
  return buildDisplayList(page.layout, page.renderTree, page.resources, backgroundResource, scrollOffsetOf);
3133
3357
  }
3134
3358
 
@@ -4399,6 +4623,11 @@ function buildPageRenderer({
4399
4623
  sound.history.shift();
4400
4624
  }
4401
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
+
4402
4631
  audio.write(sound.id, pcm);
4403
4632
  sent += pcm.length;
4404
4633
  }
@@ -0,0 +1,54 @@
1
+ import { computeElementStyle } from '../style/computed-style.js';
2
+ export const inputPresentation = new WeakMap();
3
+ export const inputOwner = new WeakMap();
4
+
5
+ const spans = new WeakMap();
6
+ const carets = new WeakMap();
7
+ export const inputCaretNode = element => carets.get(element);
8
+
9
+ // Form controls share native selection and caret presentation across frontends.
10
+ export function textInputSelection(element, text, style, make) {
11
+ const options=inputPresentation.get(element)||{};
12
+ const focused=element.ownerDocument.activeElement===element;
13
+ const value=String(element.value??'');
14
+ const position=at=>element.type==='password'?[...value.slice(0,at)].length:at;
15
+ const start=position(element.selectionStart??0),end=position(element.selectionEnd??0);
16
+ const raw=(focused||options.persistent)&&end>start ? {start,end,background:options.background||'#3874f2',color:options.selectedColor||'white'}:null;
17
+ const caretAt = (at, selected=false) => {
18
+ let span=carets.get(element);
19
+ if(!span){span=element.ownerDocument.createElement('span');carets.set(element,span);inputOwner.set(span,element);}
20
+ const height=options.height||parseFloat(style.fontSize)||16;
21
+ const color=options.color||style.color;
22
+ const on=!selected&&options.on!==false&&options.visible!==false;
23
+ span.style.cssText=`display:inline-block;width:0;height:${height}px;border-left:1px solid ${on?color:'transparent'};margin-right:-1px;vertical-align:text-bottom;pointer-events:none`;
24
+ return make('caret','inline-block',{tagName:'SPAN',style:computeElementStyle(span,style),children:Object.freeze([])},span);
25
+ };
26
+ if (!raw) {
27
+ if(!focused||element.disabled||options.visible===false)return null;
28
+ const node=(part,text)=>make(part,'text',{text,style,valueStart:part==='after'?start:0,pseudo:'value',children:Object.freeze([])},element);
29
+ return [node('before',text.slice(0,start)),caretAt(start),node('after',text.slice(start))];
30
+ }
31
+ const { background, color } = raw;
32
+ const from = Math.max(0, Math.min(text.length, start)),
33
+ to = Math.max(from, Math.min(text.length, end));
34
+ if (from === to) return null;
35
+ const textNode = (part, text, textStyle = style, domNode = element) => make(part, 'text', {
36
+ text, style: textStyle, valueStart:part==='selected'?from:part==='after'?to:0, pseudo: 'value', children: Object.freeze([]),
37
+ }, domNode);
38
+ let span = spans.get(element);
39
+ if (!span) { span = element.ownerDocument.createElement('span'); spans.set(element, span); inputOwner.set(span,element); }
40
+ span.style.pointerEvents = 'none';
41
+ span.style.backgroundColor = background;
42
+ span.style.color = color;
43
+ const selectedStyle = computeElementStyle(span, style);
44
+ const children = [];
45
+ if (from) children.push(textNode('before', text.slice(0, from)));
46
+ if(focused&&element.selectionDirection==='backward')children.push(caretAt(from,true));
47
+ children.push(make('selection', 'inline', {
48
+ tagName: 'SPAN', style: selectedStyle,
49
+ children: Object.freeze([textNode('selected', text.slice(from, to), selectedStyle, span)]),
50
+ }, span));
51
+ if(focused&&element.selectionDirection!=='backward')children.push(caretAt(to,true));
52
+ if (to < text.length) children.push(textNode('after', text.slice(to)));
53
+ return children;
54
+ }
@@ -0,0 +1,57 @@
1
+ // Shared editing defaults for HTML and QML's real input/textarea elements.
2
+ export function isTextInput(element) {
3
+ return !!element && !element.disabled && (element.tagName==='TEXTAREA' ||
4
+ element.tagName==='INPUT' && ['text','search','url','email','tel','password'].includes(element.type));
5
+ }
6
+ export const inputRange = element => ({text:String(element.value),start:element.selectionStart??0,end:element.selectionEnd??0});
7
+ export function selectInput(element,start,end=start) {
8
+ element.setSelectionRange(Math.min(start,end),Math.max(start,end),end<start?'backward':'forward');
9
+ }
10
+ const columns=new WeakMap();
11
+ export function moveInputSelection(element,event) {
12
+ if(!isTextInput(element)||event.altKey)return false;
13
+ const {text,start,end}=inputRange(element),ctrl=event.ctrlKey||event.metaKey,key=event.key;
14
+ if(ctrl&&key.toLowerCase()==='a'){selectInput(element,0,text.length);return true;}
15
+ if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Home','End'].includes(key))return false;
16
+ let at=element.selectionDirection==='backward'?start:end;
17
+ const anchor=at===start?end:start,multi=element.tagName==='TEXTAREA';
18
+ const lineStart=text.lastIndexOf('\n',at-1)+1;
19
+ if(key==='ArrowUp'||key==='ArrowDown') {
20
+ if(!multi)return false;
21
+ const previous=columns.get(element),column=previous?.at===at?previous.column:at-lineStart;
22
+ if(key==='ArrowUp') {
23
+ const stop=lineStart-1;at=stop<0?0:Math.min(text.lastIndexOf('\n',stop-1)+1+column,stop);
24
+ } else {
25
+ const next=text.indexOf('\n',at),stop=next<0?-1:text.indexOf('\n',next+1);
26
+ at=next<0?text.length:Math.min(next+1+column,stop<0?text.length:stop);
27
+ }
28
+ columns.set(element,{at,column});
29
+ } else {
30
+ columns.delete(element);
31
+ if(key==='Home')at=multi&&!ctrl?lineStart:0;
32
+ else if(key==='End'){const stop=text.indexOf('\n',at);at=multi&&!ctrl&&stop>=0?stop:text.length;}
33
+ else if(!event.shiftKey&&start!==end)at=key==='ArrowLeft'?start:end;
34
+ else if(key==='ArrowLeft') {
35
+ at-=[...text.slice(0,at)].pop()?.length??0;
36
+ if(ctrl){while(at>0&&/\s/u.test(text[at]))at--;while(at>0&&/[\p{L}\p{N}_]/u.test(text[at-1]))at--;}
37
+ } else {
38
+ at+=text.codePointAt(at)>0xffff?2:at<text.length?1:0;
39
+ if(ctrl){while(at<text.length&&/[\p{L}\p{N}_]/u.test(text[at]))at++;while(at<text.length&&/\s/u.test(text[at]))at++;}
40
+ }
41
+ }
42
+ selectInput(element,event.shiftKey?anchor:at,at);return true;
43
+ }
44
+ export async function inputClipboard(element,operation,{read,write,replace}) {
45
+ if(!isTextInput(element)||operation!=='copy'&&element.readOnly)return;
46
+ const range=inputRange(element);
47
+ if(operation==='copy'||operation==='cut') {
48
+ if(element.type==='password'||range.start===range.end)return;
49
+ await write(range.text.slice(range.start,range.end));
50
+ if(operation==='copy')return;
51
+ }
52
+ let text=operation==='paste'?await read():'';
53
+ if(String(element.value)!==range.text||element.selectionStart!==range.start||element.selectionEnd!==range.end||element.readOnly||!element.isConnected)return;
54
+ if(element.tagName!=='TEXTAREA')text=text.replace(/[\r\n]+/g,'');
55
+ if(element.maxLength>=0)text=text.slice(0,Math.max(0,element.maxLength-(range.text.length-range.end+range.start)));
56
+ replace(text,operation==='paste'?'insertFromPaste':'deleteByCut');
57
+ }