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
@@ -236,9 +236,9 @@ function blockChildren(node, flags) {
236
236
  children.push(buildBox(child, inheritedFlags(child, childFlags)));
237
237
  for (const open of openInlines) items.push({ kind: "open", node: open, style: open.style ?? null });
238
238
  } else if (child.type === "image") {
239
- items.push({ kind: "image", node: child });
239
+ items.push({ kind: "image", node: child, wrap:childFlags.wrap });
240
240
  } else if (isAtomic(child)) {
241
- items.push({ kind: "atomic", node: child, box: buildBox(child, inheritedFlags(child, childFlags)) });
241
+ items.push({ kind: "atomic", node: child, wrap:childFlags.wrap, box: buildBox(child, inheritedFlags(child, childFlags)) });
242
242
  } else {
243
243
  items.push({ kind: "open", node: child, style: child.style ?? null });
244
244
  openInlines.push(child);
@@ -2117,11 +2117,13 @@ function buildParagraph(ctx, ifc) {
2117
2117
  let value = String(item.text).replace(/\r\n?/g, "\n");
2118
2118
  if (item.preserve) {
2119
2119
  const pieces = value.split("\n");
2120
+ let sourceStart=0;
2120
2121
  pieces.forEach((piece, index) => {
2121
2122
  if (index > 0) forced.set(text.length, (forced.get(text.length) ?? 0) + 1);
2122
2123
  if (piece) {
2123
- pushRun({ kind: "text", nodeId: item.node.id, style: item.style, collapsible: false, wrap: item.wrap }, piece);
2124
+ pushRun({ kind: "text", nodeId: item.node.id, style: item.style, collapsible: false, wrap: item.wrap, sourceStart }, piece);
2124
2125
  }
2126
+ sourceStart+=piece.length+1;
2125
2127
  });
2126
2128
  afterSpace = false;
2127
2129
  } else {
@@ -2137,7 +2139,7 @@ function buildParagraph(ctx, ifc) {
2137
2139
  forced.set(text.length, (forced.get(text.length) ?? 0) + 1);
2138
2140
  afterSpace = true;
2139
2141
  } else if (item.kind === "image" || item.kind === "atomic") {
2140
- pushRun({ kind: item.kind, nodeId: item.node.id, style: item.node.style ?? null, node: item.node, box: item.box, wrap: true }, "");
2142
+ pushRun({ kind: item.kind, nodeId: item.node.id, style: item.node.style ?? null, node: item.node, box: item.box, wrap: item.wrap!==false }, "");
2141
2143
  afterSpace = false;
2142
2144
  }
2143
2145
  }
@@ -2158,7 +2160,7 @@ function buildParagraph(ctx, ifc) {
2158
2160
  const end = index + segment.length;
2159
2161
  if (!run) continue;
2160
2162
  if (run.collapsible && run.wrap && segment === " ") opportunities.add(end);
2161
- else if (run.kind === "image" || run.kind === "atomic") {
2163
+ else if (run.wrap && (run.kind === "image" || run.kind === "atomic")) {
2162
2164
  opportunities.add(index);
2163
2165
  opportunities.add(end);
2164
2166
  } else if (run.wrap && run.kind === "text" && (Bun.stringWidth(segment) > 1 || (!run.collapsible && segment === " "))) {
@@ -2404,7 +2406,7 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
2404
2406
  ascent = Math.max(ascent, atomAscent);
2405
2407
  descent = Math.max(descent, atom.marginTop + atom.height + atom.marginBottom - atomAscent);
2406
2408
  } else {
2407
- segment.metrics = textMetrics(ctx, run.style);
2409
+ segment.metrics = textMetrics(ctx, run.style, paragraph.text.slice(segment.start,segment.end));
2408
2410
  ascent = Math.max(ascent, segment.metrics.ascent);
2409
2411
  descent = Math.max(descent, segment.metrics.descent);
2410
2412
  }
@@ -2461,6 +2463,7 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
2461
2463
  const common = {
2462
2464
  nodeId: run.nodeId,
2463
2465
  text: run.kind === "text" ? paragraph.text.slice(segment.start, segment.end) : "",
2466
+ sourceStart:(run.sourceStart??0)+segment.start-run.start,
2464
2467
  start: segment.start - line.start,
2465
2468
  end: segment.end - line.start,
2466
2469
  style: run.style ?? style,
@@ -2598,8 +2601,8 @@ function measureAtomic(ctx, box, availableWidth) {
2598
2601
  };
2599
2602
  }
2600
2603
 
2601
- function textMetrics(ctx, style) {
2602
- const font = ctx.metrics(style);
2604
+ function textMetrics(ctx, style, text = null) {
2605
+ const font = ctx.metrics(style,text);
2603
2606
  // `line-height: normal` (null) uses the font's own content height.
2604
2607
  const lineHeight = ctx.lineHeight ?? (style ? style.lineHeight ?? font.height : ctx.fallbackLineHeight);
2605
2608
  const halfLeading = (lineHeight - font.height) / 2;
@@ -2668,7 +2671,12 @@ function cachedMetrics(fontMetrics) {
2668
2671
  }
2669
2672
  return metrics;
2670
2673
  };
2671
- return (style) => {
2674
+ return (style,text=null) => {
2675
+ // Fallback fonts can be taller than the Latin strut. Use the same glyphs
2676
+ // measured for this run rather than assuming every character uses "Hg".
2677
+ if(text && /[^\x00-\x7f]/.test(text) && typeof fontMetrics==='function') {
2678
+ const measured=fontMetrics(style,text);if(measured?.height>0)return measured;
2679
+ }
2672
2680
  if (style === null || typeof style !== "object") return metricsFor(style);
2673
2681
  let metrics = byStyle.get(style);
2674
2682
  if (!metrics) byStyle.set(style, metrics = metricsFor(style));
@@ -193,6 +193,9 @@ export function buildDisplayList(layout, tree = null, resources = null, backgrou
193
193
  rect: effect.transform ? transformRect(effect.transform, hitRect) : hitRect,
194
194
  clipRect: effect.clipRect,
195
195
  fixed: effect.fixed,
196
+ // Preserve text geometry for form-control caret hit testing, including
197
+ // whitespace runs which intentionally have no painted glyphs.
198
+ ...(tree?.nodesById.get(run.nodeId)?.pseudo==='value'?{textRun:run,transform:effect.transform}:{}),
196
199
  });
197
200
  reach(effect, hitRect);
198
201
  paintRun(fragment, run);
@@ -234,6 +237,7 @@ export function buildDisplayList(layout, tree = null, resources = null, backgrou
234
237
  op: "drawText",
235
238
  nodeId: run.nodeId,
236
239
  text: run.text,
240
+ textWidth: run.width,
237
241
  x: run.x,
238
242
  y,
239
243
  font: {
@@ -411,7 +415,7 @@ function paintEffects(layout, tree, scrollOffsetOf = null) {
411
415
  radii = insetRadii(radii, style.borderWidths ?? [0, 0, 0, 0]);
412
416
  }
413
417
  clips.push([node, clip, radii]);
414
- if (isScroller(style)) {
418
+ if (isScroller(style) && boxes.has(node.id)) {
415
419
  if (!scrolledBy.size) outOfFlow = positionedOut;
416
420
  scrolledBy.add(node);
417
421
  }
@@ -15,10 +15,10 @@
15
15
  * modify them, including while an asynchronous image encoder is using them.
16
16
  * @returns {{ pixels: Uint8Array, width: number, height: number }}
17
17
  */
18
- export function cropViewport(tile, scrollX, scrollY, viewportWidth, viewportHeight) {
18
+ export function cropViewport(tile, scrollX, scrollY, viewportWidth, viewportHeight, exact = false) {
19
19
  const scale = tile.scale;
20
- const width = Math.max(1, Math.round(Math.floor(viewportWidth) * scale));
21
- const height = Math.max(1, Math.round(Math.floor(viewportHeight) * scale));
20
+ const width = Math.max(1, Math.round((exact ? viewportWidth : Math.floor(viewportWidth)) * scale));
21
+ const height = Math.max(1, Math.round((exact ? viewportHeight : Math.floor(viewportHeight)) * scale));
22
22
  const offsetX = Math.round((scrollX - tile.x) * scale);
23
23
  const offsetY = Math.round((scrollY - tile.y) * scale);
24
24
  // Raster tiles are normally viewport-wide. A full-width crop is already
@@ -115,3 +115,35 @@ export function backgroundBytes(color) {
115
115
  if (!match) return [255, 255, 255, 255];
116
116
  return [Number(match[1]), Number(match[2]), Number(match[3]), Math.round(Number(match[4] ?? 1) * 255)];
117
117
  }
118
+
119
+ /** Raw screenshot payload, owned independently of a recyclable raster tile. */
120
+ export function rawFrame(pixels, width, height, format) {
121
+ if (!['rgba', 'bgra'].includes(format) || width * height > 16777216) throw Error('Invalid raw screenshot');
122
+ const data = new Uint8Array(pixels);
123
+ if (format === 'bgra') {
124
+ for (let i=0; i<data.length; i+=4) { const red=data[i]; data[i]=data[i+2]; data[i+2]=red; }
125
+ }
126
+ return {format, width, height, pixels:data};
127
+ }
128
+
129
+ // CDP clip coordinates are document CSS pixels, not viewport-relative pixels.
130
+ export function captureViewport(tile, x, y, width, height, clip) {
131
+ if (!clip) return cropViewport(tile,x,y,width,height);
132
+ if (![clip.x,clip.y,clip.width,clip.height,clip.scale].every(Number.isFinite)
133
+ || clip.width<=0 || clip.height<=0 || clip.scale<=0 || clip.scale>8)
134
+ throw Error('Invalid screenshot clip');
135
+ const w=Math.max(1,Math.round(clip.width*tile.scale*clip.scale));
136
+ const h=Math.max(1,Math.round(clip.height*tile.scale*clip.scale));
137
+ const sourceW=Math.max(1,Math.round(clip.width*tile.scale));
138
+ const sourceH=Math.max(1,Math.round(clip.height*tile.scale));
139
+ if(Math.max(w,h,sourceW,sourceH)>8192 || w*h>16777216 || sourceW*sourceH>16777216)
140
+ throw Error('Invalid screenshot clip size');
141
+ const frame=cropViewport(tile,clip.x,clip.y,clip.width,clip.height,true);
142
+ if(clip.scale===1)return frame;
143
+ const pixels=new Uint8Array(w*h*4);
144
+ for(let row=0;row<h;row++)for(let col=0;col<w;col++) {
145
+ const from=(Math.min(frame.height-1,Math.floor(row/clip.scale))*frame.width+Math.min(frame.width-1,Math.floor(col/clip.scale)))*4;
146
+ pixels.set(frame.pixels.subarray(from,from+4),(row*w+col)*4);
147
+ }
148
+ return {pixels,width:w,height:h};
149
+ }
@@ -0,0 +1,47 @@
1
+ import {mkdtempSync,writeFileSync,readdirSync,statSync,unlinkSync,rmSync,realpathSync} from 'node:fs';
2
+ import {join} from 'node:path';
3
+ import {tmpdir} from 'node:os';
4
+
5
+ // The producer owns the directory; Kitty t=t consumers unlink individual frames.
6
+ // A worker borrows a directory owned by its parent, so terminating the worker
7
+ // cannot skip cleanup. No pixel data is encoded into the CDP response.
8
+ export class RawFrames {
9
+ constructor({directory,ownsDirectory=true}={}) {
10
+ this.externalDirectory=directory;this.ownsDirectory=ownsDirectory;
11
+ this.sequence=0;this.closed=false;
12
+ this.onExit=()=>this.close();
13
+ }
14
+ get directory() {
15
+ if(this.closed)throw Error('Raw frame store is closed');
16
+ if(!this.path){
17
+ this.path=this.externalDirectory ? this.externalDirectory() : realpathSync(mkdtempSync(join(tmpdir(),'tty-graphics-protocol-browser-')));
18
+ if(!this.path)throw Error('Missing raw frame directory');
19
+ if(this.ownsDirectory)process.once('exit',this.onExit);
20
+ }
21
+ return this.path;
22
+ }
23
+ write(bytes,width,height,format) {
24
+ if (!['rgba','bgra'].includes(format) || !Number.isInteger(width) || !Number.isInteger(height)
25
+ || width<1 || height<1 || width*height>16777216 || !(bytes instanceof Uint8Array)
26
+ || bytes.byteLength!==width*height*4) throw Error('Invalid raw frame');
27
+ const directory=this.directory;
28
+ let count=0,total=0;
29
+ for(const name of readdirSync(directory)){
30
+ const path=join(directory,name);
31
+ try{
32
+ const stat=statSync(path);
33
+ if(Date.now()-stat.mtimeMs>60000){unlinkSync(path);continue;}
34
+ count++;total+=stat.size;
35
+ }catch(error){if(error.code!=='ENOENT')throw error;}
36
+ }
37
+ if(count>=16||total+bytes.byteLength>128*1024*1024)throw Error('Raw frame consumer stalled');
38
+ const path=join(directory,`${++this.sequence}.${format}`);
39
+ writeFileSync(path,bytes,{flag:'wx',mode:0o600});
40
+ return JSON.stringify({version:1,transport:'file',path,width,height,stride:width*4,format,bytes:bytes.byteLength});
41
+ }
42
+ close(){
43
+ if(this.closed)return;this.closed=true;
44
+ process.removeListener('exit',this.onExit);
45
+ if(this.ownsDirectory&&this.path)rmSync(this.path,{recursive:true,force:true});
46
+ }
47
+ }