@react-x11/components 0.8.0 → 0.10.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 (721) hide show
  1. package/README.md +105 -38
  2. package/dist/charts/node.d.ts +12 -2
  3. package/dist/charts/node.d.ts.map +1 -1
  4. package/dist/charts/node.js +50 -16
  5. package/dist/charts/node.js.map +1 -1
  6. package/dist/charts/render.d.ts +12 -3
  7. package/dist/charts/render.d.ts.map +1 -1
  8. package/dist/charts/render.js +94 -25
  9. package/dist/charts/render.js.map +1 -1
  10. package/dist/code/index.d.ts.map +1 -1
  11. package/dist/code/index.js +3 -1
  12. package/dist/code/index.js.map +1 -1
  13. package/dist/code-editor/completion.d.ts.map +1 -1
  14. package/dist/code-editor/completion.js +3 -2
  15. package/dist/code-editor/completion.js.map +1 -1
  16. package/dist/code-editor/index.d.ts.map +1 -1
  17. package/dist/code-editor/index.js +19 -0
  18. package/dist/code-editor/index.js.map +1 -1
  19. package/dist/code-editor/node.d.ts +177 -7
  20. package/dist/code-editor/node.d.ts.map +1 -1
  21. package/dist/code-editor/node.js +1329 -164
  22. package/dist/code-editor/node.js.map +1 -1
  23. package/dist/code-language/languages/glsl.d.ts.map +1 -1
  24. package/dist/code-language/languages/glsl.js +10 -2
  25. package/dist/code-language/languages/glsl.js.map +1 -1
  26. package/dist/code-language/languages/javascript.d.ts.map +1 -1
  27. package/dist/code-language/languages/javascript.js +18 -3
  28. package/dist/code-language/languages/javascript.js.map +1 -1
  29. package/dist/code-language/languages/json.d.ts.map +1 -1
  30. package/dist/code-language/languages/json.js +10 -2
  31. package/dist/code-language/languages/json.js.map +1 -1
  32. package/dist/code-language/languages/shell.d.ts.map +1 -1
  33. package/dist/code-language/languages/shell.js +10 -2
  34. package/dist/code-language/languages/shell.js.map +1 -1
  35. package/dist/code-language/languages/sql.d.ts.map +1 -1
  36. package/dist/code-language/languages/sql.js +20 -2
  37. package/dist/code-language/languages/sql.js.map +1 -1
  38. package/dist/code-language/stream.d.ts +22 -0
  39. package/dist/code-language/stream.d.ts.map +1 -1
  40. package/dist/code-language/stream.js +259 -9
  41. package/dist/code-language/stream.js.map +1 -1
  42. package/dist/code-language/types.d.ts +5 -4
  43. package/dist/code-language/types.d.ts.map +1 -1
  44. package/dist/codeblock/index.d.ts +13 -2
  45. package/dist/codeblock/index.d.ts.map +1 -1
  46. package/dist/codeblock/index.js +20 -3
  47. package/dist/codeblock/index.js.map +1 -1
  48. package/dist/embed/client.d.ts +48 -3
  49. package/dist/embed/client.d.ts.map +1 -1
  50. package/dist/embed/client.js +79 -5
  51. package/dist/embed/client.js.map +1 -1
  52. package/dist/embed/index.d.ts +1 -1
  53. package/dist/embed/index.d.ts.map +1 -1
  54. package/dist/embed/index.js +1 -1
  55. package/dist/embed/index.js.map +1 -1
  56. package/dist/flow/draw.d.ts +50 -0
  57. package/dist/flow/draw.d.ts.map +1 -1
  58. package/dist/flow/draw.js +132 -33
  59. package/dist/flow/draw.js.map +1 -1
  60. package/dist/flow/gl/color.d.ts +13 -0
  61. package/dist/flow/gl/color.d.ts.map +1 -0
  62. package/dist/flow/gl/color.js +82 -0
  63. package/dist/flow/gl/color.js.map +1 -0
  64. package/dist/flow/gl/index.d.ts +60 -0
  65. package/dist/flow/gl/index.d.ts.map +1 -0
  66. package/dist/flow/gl/index.js +283 -0
  67. package/dist/flow/gl/index.js.map +1 -0
  68. package/dist/flow/gl/pack.d.ts +133 -0
  69. package/dist/flow/gl/pack.d.ts.map +1 -0
  70. package/dist/flow/gl/pack.js +408 -0
  71. package/dist/flow/gl/pack.js.map +1 -0
  72. package/dist/flow/gl/renderer.d.ts +95 -0
  73. package/dist/flow/gl/renderer.d.ts.map +1 -0
  74. package/dist/flow/gl/renderer.js +459 -0
  75. package/dist/flow/gl/renderer.js.map +1 -0
  76. package/dist/flow/gl/shaders.d.ts +79 -0
  77. package/dist/flow/gl/shaders.d.ts.map +1 -0
  78. package/dist/flow/gl/shaders.js +346 -0
  79. package/dist/flow/gl/shaders.js.map +1 -0
  80. package/dist/flow/gl/text.d.ts +318 -0
  81. package/dist/flow/gl/text.d.ts.map +1 -0
  82. package/dist/flow/gl/text.js +1103 -0
  83. package/dist/flow/gl/text.js.map +1 -0
  84. package/dist/flow/index.d.ts +1 -1
  85. package/dist/flow/index.d.ts.map +1 -1
  86. package/dist/flow/index.js +619 -64
  87. package/dist/flow/index.js.map +1 -1
  88. package/dist/flow/node.d.ts +621 -72
  89. package/dist/flow/node.d.ts.map +1 -1
  90. package/dist/flow/node.js +2253 -1018
  91. package/dist/flow/node.js.map +1 -1
  92. package/dist/flow/paint.d.ts +35 -0
  93. package/dist/flow/paint.d.ts.map +1 -0
  94. package/dist/flow/paint.js +393 -0
  95. package/dist/flow/paint.js.map +1 -0
  96. package/dist/flow/paths.d.ts +14 -0
  97. package/dist/flow/paths.d.ts.map +1 -1
  98. package/dist/flow/paths.js +45 -19
  99. package/dist/flow/paths.js.map +1 -1
  100. package/dist/flow/scene.d.ts +584 -0
  101. package/dist/flow/scene.d.ts.map +1 -0
  102. package/dist/flow/scene.js +1453 -0
  103. package/dist/flow/scene.js.map +1 -0
  104. package/dist/flow/types.d.ts +111 -1
  105. package/dist/flow/types.d.ts.map +1 -1
  106. package/dist/html/controls.d.ts +28 -0
  107. package/dist/html/controls.d.ts.map +1 -1
  108. package/dist/html/controls.js +52 -2
  109. package/dist/html/controls.js.map +1 -1
  110. package/dist/html/css/calc.d.ts +14 -0
  111. package/dist/html/css/calc.d.ts.map +1 -0
  112. package/dist/html/css/calc.js +344 -0
  113. package/dist/html/css/calc.js.map +1 -0
  114. package/dist/html/css/cascade.d.ts +240 -6
  115. package/dist/html/css/cascade.d.ts.map +1 -1
  116. package/dist/html/css/cascade.js +1327 -66
  117. package/dist/html/css/cascade.js.map +1 -1
  118. package/dist/html/css/color.d.ts +23 -0
  119. package/dist/html/css/color.d.ts.map +1 -0
  120. package/dist/html/css/color.js +740 -0
  121. package/dist/html/css/color.js.map +1 -0
  122. package/dist/html/css/content.d.ts +92 -0
  123. package/dist/html/css/content.d.ts.map +1 -0
  124. package/dist/html/css/content.js +388 -0
  125. package/dist/html/css/content.js.map +1 -0
  126. package/dist/html/css/counter-styles.d.ts +76 -0
  127. package/dist/html/css/counter-styles.d.ts.map +1 -0
  128. package/dist/html/css/counter-styles.js +893 -0
  129. package/dist/html/css/counter-styles.js.map +1 -0
  130. package/dist/html/css/decode.d.ts +12 -0
  131. package/dist/html/css/decode.d.ts.map +1 -0
  132. package/dist/html/css/decode.js +116 -0
  133. package/dist/html/css/decode.js.map +1 -0
  134. package/dist/html/css/parse.d.ts +110 -10
  135. package/dist/html/css/parse.d.ts.map +1 -1
  136. package/dist/html/css/parse.js +1473 -180
  137. package/dist/html/css/parse.js.map +1 -1
  138. package/dist/html/css/style.d.ts +498 -15
  139. package/dist/html/css/style.d.ts.map +1 -1
  140. package/dist/html/css/style.js +3698 -206
  141. package/dist/html/css/style.js.map +1 -1
  142. package/dist/html/css/ua.d.ts.map +1 -1
  143. package/dist/html/css/ua.js +57 -11
  144. package/dist/html/css/ua.js.map +1 -1
  145. package/dist/html/css/values.d.ts +37 -3
  146. package/dist/html/css/values.d.ts.map +1 -1
  147. package/dist/html/css/values.js +122 -48
  148. package/dist/html/css/values.js.map +1 -1
  149. package/dist/html/css/vars.d.ts +52 -0
  150. package/dist/html/css/vars.d.ts.map +1 -0
  151. package/dist/html/css/vars.js +358 -0
  152. package/dist/html/css/vars.js.map +1 -0
  153. package/dist/html/dom.d.ts +30 -4
  154. package/dist/html/dom.d.ts.map +1 -1
  155. package/dist/html/dom.js +278 -17
  156. package/dist/html/dom.js.map +1 -1
  157. package/dist/html/fonts.d.ts +106 -0
  158. package/dist/html/fonts.d.ts.map +1 -0
  159. package/dist/html/fonts.js +565 -0
  160. package/dist/html/fonts.js.map +1 -0
  161. package/dist/html/gif.d.ts +15 -0
  162. package/dist/html/gif.d.ts.map +1 -0
  163. package/dist/html/gif.js +229 -0
  164. package/dist/html/gif.js.map +1 -0
  165. package/dist/html/index.d.ts +50 -11
  166. package/dist/html/index.d.ts.map +1 -1
  167. package/dist/html/index.js +115 -9
  168. package/dist/html/index.js.map +1 -1
  169. package/dist/html/layout/block.d.ts +226 -3
  170. package/dist/html/layout/block.d.ts.map +1 -1
  171. package/dist/html/layout/block.js +2937 -197
  172. package/dist/html/layout/block.js.map +1 -1
  173. package/dist/html/layout/boxes.d.ts +340 -17
  174. package/dist/html/layout/boxes.d.ts.map +1 -1
  175. package/dist/html/layout/boxes.js +1714 -182
  176. package/dist/html/layout/boxes.js.map +1 -1
  177. package/dist/html/layout/cache.d.ts +58 -0
  178. package/dist/html/layout/cache.d.ts.map +1 -0
  179. package/dist/html/layout/cache.js +224 -0
  180. package/dist/html/layout/cache.js.map +1 -0
  181. package/dist/html/layout/collapse.d.ts +45 -0
  182. package/dist/html/layout/collapse.d.ts.map +1 -0
  183. package/dist/html/layout/collapse.js +246 -0
  184. package/dist/html/layout/collapse.js.map +1 -0
  185. package/dist/html/layout/css-grid.d.ts +7 -0
  186. package/dist/html/layout/css-grid.d.ts.map +1 -0
  187. package/dist/html/layout/css-grid.js +900 -0
  188. package/dist/html/layout/css-grid.js.map +1 -0
  189. package/dist/html/layout/flex.d.ts.map +1 -1
  190. package/dist/html/layout/flex.js +1025 -89
  191. package/dist/html/layout/flex.js.map +1 -1
  192. package/dist/html/layout/floats.d.ts +45 -6
  193. package/dist/html/layout/floats.d.ts.map +1 -1
  194. package/dist/html/layout/floats.js +113 -14
  195. package/dist/html/layout/floats.js.map +1 -1
  196. package/dist/html/layout/grid-lines.d.ts +39 -0
  197. package/dist/html/layout/grid-lines.d.ts.map +1 -0
  198. package/dist/html/layout/grid-lines.js +119 -0
  199. package/dist/html/layout/grid-lines.js.map +1 -0
  200. package/dist/html/layout/grid.d.ts +53 -0
  201. package/dist/html/layout/grid.d.ts.map +1 -0
  202. package/dist/html/layout/grid.js +153 -0
  203. package/dist/html/layout/grid.js.map +1 -0
  204. package/dist/html/layout/inline.d.ts +151 -1
  205. package/dist/html/layout/inline.d.ts.map +1 -1
  206. package/dist/html/layout/inline.js +3375 -174
  207. package/dist/html/layout/inline.js.map +1 -1
  208. package/dist/html/layout/shaping.d.ts +6 -0
  209. package/dist/html/layout/shaping.d.ts.map +1 -0
  210. package/dist/html/layout/shaping.js +100 -0
  211. package/dist/html/layout/shaping.js.map +1 -0
  212. package/dist/html/layout/table.d.ts +3 -0
  213. package/dist/html/layout/table.d.ts.map +1 -1
  214. package/dist/html/layout/table.js +681 -175
  215. package/dist/html/layout/table.js.map +1 -1
  216. package/dist/html/layout/tracks.d.ts +44 -0
  217. package/dist/html/layout/tracks.d.ts.map +1 -0
  218. package/dist/html/layout/tracks.js +349 -0
  219. package/dist/html/layout/tracks.js.map +1 -0
  220. package/dist/html/node.d.ts +172 -5
  221. package/dist/html/node.d.ts.map +1 -1
  222. package/dist/html/node.js +1336 -103
  223. package/dist/html/node.js.map +1 -1
  224. package/dist/html/paint.d.ts +92 -3
  225. package/dist/html/paint.d.ts.map +1 -1
  226. package/dist/html/paint.js +3503 -141
  227. package/dist/html/paint.js.map +1 -1
  228. package/dist/html/resources.d.ts +60 -14
  229. package/dist/html/resources.d.ts.map +1 -1
  230. package/dist/html/resources.js +122 -36
  231. package/dist/html/resources.js.map +1 -1
  232. package/dist/html/surfaces.d.ts +23 -0
  233. package/dist/html/surfaces.d.ts.map +1 -0
  234. package/dist/html/surfaces.js +68 -0
  235. package/dist/html/surfaces.js.map +1 -0
  236. package/dist/html/svg.d.ts +80 -0
  237. package/dist/html/svg.d.ts.map +1 -0
  238. package/dist/html/svg.js +435 -0
  239. package/dist/html/svg.js.map +1 -0
  240. package/dist/html/url.d.ts +23 -0
  241. package/dist/html/url.d.ts.map +1 -0
  242. package/dist/html/url.js +80 -0
  243. package/dist/html/url.js.map +1 -0
  244. package/dist/index.d.ts +2 -2
  245. package/dist/index.d.ts.map +1 -1
  246. package/dist/index.js +1 -1
  247. package/dist/index.js.map +1 -1
  248. package/dist/internal/collate.d.ts +3 -0
  249. package/dist/internal/collate.d.ts.map +1 -0
  250. package/dist/internal/collate.js +11 -0
  251. package/dist/internal/collate.js.map +1 -0
  252. package/dist/{markdown → internal/markdown}/ast.d.ts +23 -0
  253. package/dist/internal/markdown/ast.d.ts.map +1 -0
  254. package/dist/internal/markdown/ast.js +22 -0
  255. package/dist/internal/markdown/ast.js.map +1 -0
  256. package/dist/internal/markdown/parse.d.ts.map +1 -0
  257. package/dist/{markdown → internal/markdown}/parse.js +221 -30
  258. package/dist/internal/markdown/parse.js.map +1 -0
  259. package/dist/internal/markdown/stringify.d.ts +4 -0
  260. package/dist/internal/markdown/stringify.d.ts.map +1 -0
  261. package/dist/internal/markdown/stringify.js +684 -0
  262. package/dist/internal/markdown/stringify.js.map +1 -0
  263. package/dist/internal/markdown/tags.d.ts.map +1 -0
  264. package/dist/internal/markdown/tags.js.map +1 -0
  265. package/dist/internal/prewarm.d.ts +18 -0
  266. package/dist/internal/prewarm.d.ts.map +1 -0
  267. package/dist/internal/prewarm.js +42 -0
  268. package/dist/internal/prewarm.js.map +1 -0
  269. package/dist/internal/scroll.d.ts +3 -2
  270. package/dist/internal/scroll.d.ts.map +1 -1
  271. package/dist/internal/scroll.js +1 -0
  272. package/dist/internal/scroll.js.map +1 -1
  273. package/dist/internal/sdf.d.ts +20 -0
  274. package/dist/internal/sdf.d.ts.map +1 -0
  275. package/dist/internal/sdf.js +216 -0
  276. package/dist/internal/sdf.js.map +1 -0
  277. package/dist/internal/splice.d.ts +12 -0
  278. package/dist/internal/splice.d.ts.map +1 -0
  279. package/dist/internal/splice.js +23 -0
  280. package/dist/internal/splice.js.map +1 -0
  281. package/dist/internal/window.d.ts +12 -0
  282. package/dist/internal/window.d.ts.map +1 -1
  283. package/dist/internal/window.js +23 -6
  284. package/dist/internal/window.js.map +1 -1
  285. package/dist/maps/anchors.d.ts +103 -0
  286. package/dist/maps/anchors.d.ts.map +1 -0
  287. package/dist/maps/anchors.js +790 -0
  288. package/dist/maps/anchors.js.map +1 -0
  289. package/dist/maps/controller.d.ts +340 -0
  290. package/dist/maps/controller.d.ts.map +1 -0
  291. package/dist/maps/controller.js +822 -0
  292. package/dist/maps/controller.js.map +1 -0
  293. package/dist/maps/gl/buckets.d.ts +99 -0
  294. package/dist/maps/gl/buckets.d.ts.map +1 -0
  295. package/dist/maps/gl/buckets.js +365 -0
  296. package/dist/maps/gl/buckets.js.map +1 -0
  297. package/dist/maps/gl/build-worker.d.ts +15 -0
  298. package/dist/maps/gl/build-worker.d.ts.map +1 -0
  299. package/dist/maps/gl/build-worker.js +44 -0
  300. package/dist/maps/gl/build-worker.js.map +1 -0
  301. package/dist/maps/gl/color.d.ts +8 -0
  302. package/dist/maps/gl/color.d.ts.map +1 -0
  303. package/dist/maps/gl/color.js +57 -0
  304. package/dist/maps/gl/color.js.map +1 -0
  305. package/dist/maps/gl/cover.d.ts +70 -0
  306. package/dist/maps/gl/cover.d.ts.map +1 -0
  307. package/dist/maps/gl/cover.js +137 -0
  308. package/dist/maps/gl/cover.js.map +1 -0
  309. package/dist/maps/gl/index.d.ts +3 -0
  310. package/dist/maps/gl/index.d.ts.map +1 -0
  311. package/dist/maps/gl/index.js +20 -0
  312. package/dist/maps/gl/index.js.map +1 -0
  313. package/dist/maps/gl/markers.d.ts +28 -0
  314. package/dist/maps/gl/markers.d.ts.map +1 -0
  315. package/dist/maps/gl/markers.js +60 -0
  316. package/dist/maps/gl/markers.js.map +1 -0
  317. package/dist/maps/gl/overlays.d.ts +68 -0
  318. package/dist/maps/gl/overlays.d.ts.map +1 -0
  319. package/dist/maps/gl/overlays.js +300 -0
  320. package/dist/maps/gl/overlays.js.map +1 -0
  321. package/dist/maps/gl/pane.d.ts +12 -0
  322. package/dist/maps/gl/pane.d.ts.map +1 -0
  323. package/dist/maps/gl/pane.js +37 -0
  324. package/dist/maps/gl/pane.js.map +1 -0
  325. package/dist/maps/gl/placement.d.ts +124 -0
  326. package/dist/maps/gl/placement.d.ts.map +1 -0
  327. package/dist/maps/gl/placement.js +642 -0
  328. package/dist/maps/gl/placement.js.map +1 -0
  329. package/dist/maps/gl/renderer.d.ts +384 -0
  330. package/dist/maps/gl/renderer.d.ts.map +1 -0
  331. package/dist/maps/gl/renderer.js +1184 -0
  332. package/dist/maps/gl/renderer.js.map +1 -0
  333. package/dist/maps/gl/shaders.d.ts +121 -0
  334. package/dist/maps/gl/shaders.d.ts.map +1 -0
  335. package/dist/maps/gl/shaders.js +453 -0
  336. package/dist/maps/gl/shaders.js.map +1 -0
  337. package/dist/maps/gl/store.d.ts +125 -0
  338. package/dist/maps/gl/store.d.ts.map +1 -0
  339. package/dist/maps/gl/store.js +512 -0
  340. package/dist/maps/gl/store.js.map +1 -0
  341. package/dist/maps/gl/text.d.ts +283 -0
  342. package/dist/maps/gl/text.d.ts.map +1 -0
  343. package/dist/maps/gl/text.js +698 -0
  344. package/dist/maps/gl/text.js.map +1 -0
  345. package/dist/maps/gl/view.d.ts +345 -0
  346. package/dist/maps/gl/view.d.ts.map +1 -0
  347. package/dist/maps/gl/view.js +1209 -0
  348. package/dist/maps/gl/view.js.map +1 -0
  349. package/dist/maps/icons.d.ts +32 -0
  350. package/dist/maps/icons.d.ts.map +1 -0
  351. package/dist/maps/icons.js +151 -0
  352. package/dist/maps/icons.js.map +1 -0
  353. package/dist/maps/index.d.ts +3 -1
  354. package/dist/maps/index.d.ts.map +1 -1
  355. package/dist/maps/index.js +218 -52
  356. package/dist/maps/index.js.map +1 -1
  357. package/dist/maps/labels.d.ts +91 -18
  358. package/dist/maps/labels.d.ts.map +1 -1
  359. package/dist/maps/labels.js +376 -140
  360. package/dist/maps/labels.js.map +1 -1
  361. package/dist/maps/node.d.ts +179 -56
  362. package/dist/maps/node.d.ts.map +1 -1
  363. package/dist/maps/node.js +663 -537
  364. package/dist/maps/node.js.map +1 -1
  365. package/dist/maps/overlay.d.ts +72 -9
  366. package/dist/maps/overlay.d.ts.map +1 -1
  367. package/dist/maps/overlay.js +108 -28
  368. package/dist/maps/overlay.js.map +1 -1
  369. package/dist/maps/paint.d.ts +3 -0
  370. package/dist/maps/paint.d.ts.map +1 -1
  371. package/dist/maps/paint.js.map +1 -1
  372. package/dist/maps/proj.d.ts +62 -0
  373. package/dist/maps/proj.d.ts.map +1 -1
  374. package/dist/maps/proj.js +101 -0
  375. package/dist/maps/proj.js.map +1 -1
  376. package/dist/maps/renderer.d.ts +69 -0
  377. package/dist/maps/renderer.d.ts.map +1 -0
  378. package/dist/maps/renderer.js +66 -0
  379. package/dist/maps/renderer.js.map +1 -0
  380. package/dist/maps/sources.d.ts +30 -6
  381. package/dist/maps/sources.d.ts.map +1 -1
  382. package/dist/maps/sources.js +22 -3
  383. package/dist/maps/sources.js.map +1 -1
  384. package/dist/maps/style.d.ts +35 -0
  385. package/dist/maps/style.d.ts.map +1 -1
  386. package/dist/maps/style.js.map +1 -1
  387. package/dist/maps/styles.d.ts +12 -0
  388. package/dist/maps/styles.d.ts.map +1 -1
  389. package/dist/maps/styles.js +120 -6
  390. package/dist/maps/styles.js.map +1 -1
  391. package/dist/maps/theme.d.ts +19 -0
  392. package/dist/maps/theme.d.ts.map +1 -0
  393. package/dist/maps/theme.js +44 -0
  394. package/dist/maps/theme.js.map +1 -0
  395. package/dist/maps/tiles.d.ts +147 -27
  396. package/dist/maps/tiles.d.ts.map +1 -1
  397. package/dist/maps/tiles.js +221 -54
  398. package/dist/maps/tiles.js.map +1 -1
  399. package/dist/maps/types.d.ts +213 -21
  400. package/dist/maps/types.d.ts.map +1 -1
  401. package/dist/markdown/expressions.d.ts +1 -1
  402. package/dist/markdown/expressions.d.ts.map +1 -1
  403. package/dist/markdown/expressions.js.map +1 -1
  404. package/dist/markdown/index.d.ts +7 -6
  405. package/dist/markdown/index.d.ts.map +1 -1
  406. package/dist/markdown/index.js +121 -50
  407. package/dist/markdown/index.js.map +1 -1
  408. package/dist/markdown/spans.d.ts +1 -1
  409. package/dist/markdown/spans.d.ts.map +1 -1
  410. package/dist/markdown/spans.js +6 -0
  411. package/dist/markdown/spans.js.map +1 -1
  412. package/dist/media-player/index.d.ts +11 -3
  413. package/dist/media-player/index.d.ts.map +1 -1
  414. package/dist/media-player/index.js +13 -2
  415. package/dist/media-player/index.js.map +1 -1
  416. package/dist/qml/containers.d.ts.map +1 -1
  417. package/dist/qml/containers.js +6 -1
  418. package/dist/qml/containers.js.map +1 -1
  419. package/dist/qml/interaction.d.ts.map +1 -1
  420. package/dist/qml/interaction.js +38 -5
  421. package/dist/qml/interaction.js.map +1 -1
  422. package/dist/qml/layouts.d.ts.map +1 -1
  423. package/dist/qml/layouts.js +21 -6
  424. package/dist/qml/layouts.js.map +1 -1
  425. package/dist/qml/qtquick.d.ts.map +1 -1
  426. package/dist/qml/qtquick.js +9 -2
  427. package/dist/qml/qtquick.js.map +1 -1
  428. package/dist/qml/react.d.ts.map +1 -1
  429. package/dist/qml/react.js +9 -7
  430. package/dist/qml/react.js.map +1 -1
  431. package/dist/reorder/clock.d.ts +7 -0
  432. package/dist/reorder/clock.d.ts.map +1 -0
  433. package/dist/reorder/clock.js +26 -0
  434. package/dist/reorder/clock.js.map +1 -0
  435. package/dist/reorder/index.d.ts +25 -3
  436. package/dist/reorder/index.d.ts.map +1 -1
  437. package/dist/reorder/index.js +64 -19
  438. package/dist/reorder/index.js.map +1 -1
  439. package/dist/rich-text-editor/clipboard.d.ts +23 -0
  440. package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
  441. package/dist/rich-text-editor/clipboard.js +307 -0
  442. package/dist/rich-text-editor/clipboard.js.map +1 -0
  443. package/dist/rich-text-editor/collab.d.ts +32 -0
  444. package/dist/rich-text-editor/collab.d.ts.map +1 -0
  445. package/dist/rich-text-editor/collab.js +55 -0
  446. package/dist/rich-text-editor/collab.js.map +1 -0
  447. package/dist/rich-text-editor/commands.d.ts +50 -0
  448. package/dist/rich-text-editor/commands.d.ts.map +1 -0
  449. package/dist/rich-text-editor/commands.js +376 -0
  450. package/dist/rich-text-editor/commands.js.map +1 -0
  451. package/dist/rich-text-editor/drag.d.ts +68 -0
  452. package/dist/rich-text-editor/drag.d.ts.map +1 -0
  453. package/dist/rich-text-editor/drag.js +148 -0
  454. package/dist/rich-text-editor/drag.js.map +1 -0
  455. package/dist/rich-text-editor/html.d.ts +145 -0
  456. package/dist/rich-text-editor/html.d.ts.map +1 -0
  457. package/dist/rich-text-editor/html.js +516 -0
  458. package/dist/rich-text-editor/html.js.map +1 -0
  459. package/dist/rich-text-editor/index.d.ts +252 -0
  460. package/dist/rich-text-editor/index.d.ts.map +1 -0
  461. package/dist/rich-text-editor/index.js +856 -0
  462. package/dist/rich-text-editor/index.js.map +1 -0
  463. package/dist/rich-text-editor/inline.d.ts +94 -0
  464. package/dist/rich-text-editor/inline.d.ts.map +1 -0
  465. package/dist/rich-text-editor/inline.js +252 -0
  466. package/dist/rich-text-editor/inline.js.map +1 -0
  467. package/dist/rich-text-editor/keymap.d.ts +71 -0
  468. package/dist/rich-text-editor/keymap.d.ts.map +1 -0
  469. package/dist/rich-text-editor/keymap.js +394 -0
  470. package/dist/rich-text-editor/keymap.js.map +1 -0
  471. package/dist/rich-text-editor/keys.d.ts +40 -0
  472. package/dist/rich-text-editor/keys.d.ts.map +1 -0
  473. package/dist/rich-text-editor/keys.js +202 -0
  474. package/dist/rich-text-editor/keys.js.map +1 -0
  475. package/dist/rich-text-editor/look.d.ts +71 -0
  476. package/dist/rich-text-editor/look.d.ts.map +1 -0
  477. package/dist/rich-text-editor/look.js +342 -0
  478. package/dist/rich-text-editor/look.js.map +1 -0
  479. package/dist/rich-text-editor/markdown.d.ts +18 -0
  480. package/dist/rich-text-editor/markdown.d.ts.map +1 -0
  481. package/dist/rich-text-editor/markdown.js +496 -0
  482. package/dist/rich-text-editor/markdown.js.map +1 -0
  483. package/dist/rich-text-editor/marks.d.ts +9 -0
  484. package/dist/rich-text-editor/marks.d.ts.map +1 -0
  485. package/dist/rich-text-editor/marks.js +83 -0
  486. package/dist/rich-text-editor/marks.js.map +1 -0
  487. package/dist/rich-text-editor/nodes.d.ts +135 -0
  488. package/dist/rich-text-editor/nodes.d.ts.map +1 -0
  489. package/dist/rich-text-editor/nodes.js +397 -0
  490. package/dist/rich-text-editor/nodes.js.map +1 -0
  491. package/dist/rich-text-editor/render.d.ts +68 -0
  492. package/dist/rich-text-editor/render.d.ts.map +1 -0
  493. package/dist/rich-text-editor/render.js +518 -0
  494. package/dist/rich-text-editor/render.js.map +1 -0
  495. package/dist/rich-text-editor/schema.d.ts +16 -0
  496. package/dist/rich-text-editor/schema.d.ts.map +1 -0
  497. package/dist/rich-text-editor/schema.js +386 -0
  498. package/dist/rich-text-editor/schema.js.map +1 -0
  499. package/dist/rich-text-editor/store.d.ts +11 -0
  500. package/dist/rich-text-editor/store.d.ts.map +1 -0
  501. package/dist/rich-text-editor/store.js +43 -0
  502. package/dist/rich-text-editor/store.js.map +1 -0
  503. package/dist/rich-text-editor/suggest.d.ts +127 -0
  504. package/dist/rich-text-editor/suggest.d.ts.map +1 -0
  505. package/dist/rich-text-editor/suggest.js +0 -0
  506. package/dist/rich-text-editor/suggest.js.map +1 -0
  507. package/dist/rich-text-editor/tables.d.ts +41 -0
  508. package/dist/rich-text-editor/tables.d.ts.map +1 -0
  509. package/dist/rich-text-editor/tables.js +204 -0
  510. package/dist/rich-text-editor/tables.js.map +1 -0
  511. package/dist/rich-text-editor/toolbar.d.ts +58 -0
  512. package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
  513. package/dist/rich-text-editor/toolbar.js +548 -0
  514. package/dist/rich-text-editor/toolbar.js.map +1 -0
  515. package/dist/rich-text-editor/view.d.ts +368 -0
  516. package/dist/rich-text-editor/view.d.ts.map +1 -0
  517. package/dist/rich-text-editor/view.js +1968 -0
  518. package/dist/rich-text-editor/view.js.map +1 -0
  519. package/dist/rich-text-editor/virtual.d.ts +54 -0
  520. package/dist/rich-text-editor/virtual.d.ts.map +1 -0
  521. package/dist/rich-text-editor/virtual.js +279 -0
  522. package/dist/rich-text-editor/virtual.js.map +1 -0
  523. package/dist/richtext/node.d.ts +71 -4
  524. package/dist/richtext/node.d.ts.map +1 -1
  525. package/dist/richtext/node.js +233 -28
  526. package/dist/richtext/node.js.map +1 -1
  527. package/dist/richtext/runs.d.ts +13 -8
  528. package/dist/richtext/runs.d.ts.map +1 -1
  529. package/dist/richtext/runs.js +18 -8
  530. package/dist/richtext/runs.js.map +1 -1
  531. package/dist/table/index.d.ts.map +1 -1
  532. package/dist/table/index.js +23 -0
  533. package/dist/table/index.js.map +1 -1
  534. package/dist/table/rows.d.ts +3 -2
  535. package/dist/table/rows.d.ts.map +1 -1
  536. package/dist/table/rows.js +20 -6
  537. package/dist/table/rows.js.map +1 -1
  538. package/dist/terminal/backends.d.ts +4 -1
  539. package/dist/terminal/backends.d.ts.map +1 -1
  540. package/dist/terminal/backends.js.map +1 -1
  541. package/dist/terminal/index.d.ts +18 -7
  542. package/dist/terminal/index.d.ts.map +1 -1
  543. package/dist/terminal/index.js +32 -11
  544. package/dist/terminal/index.js.map +1 -1
  545. package/dist/terminal/vt/pty.d.ts +13 -2
  546. package/dist/terminal/vt/pty.d.ts.map +1 -1
  547. package/dist/terminal/vt/pty.js +18 -3
  548. package/dist/terminal/vt/pty.js.map +1 -1
  549. package/dist/terminal-output/index.d.ts.map +1 -1
  550. package/dist/terminal-output/index.js +6 -1
  551. package/dist/terminal-output/index.js.map +1 -1
  552. package/dist/three/canvas.d.ts.map +1 -1
  553. package/dist/three/canvas.js +85 -7
  554. package/dist/three/canvas.js.map +1 -1
  555. package/dist/three/events.d.ts +22 -4
  556. package/dist/three/events.d.ts.map +1 -1
  557. package/dist/three/events.js +34 -4
  558. package/dist/three/events.js.map +1 -1
  559. package/dist/tree/index.d.ts.map +1 -1
  560. package/dist/tree/index.js +17 -0
  561. package/dist/tree/index.js.map +1 -1
  562. package/package.json +59 -31
  563. package/src/charts/node.ts +68 -28
  564. package/src/charts/render.ts +120 -30
  565. package/src/code/index.ts +3 -1
  566. package/src/code-editor/completion.ts +3 -2
  567. package/src/code-editor/index.ts +19 -0
  568. package/src/code-editor/node.ts +1574 -184
  569. package/src/code-language/languages/glsl.ts +12 -2
  570. package/src/code-language/languages/javascript.ts +21 -4
  571. package/src/code-language/languages/json.ts +12 -2
  572. package/src/code-language/languages/shell.ts +12 -2
  573. package/src/code-language/languages/sql.ts +22 -2
  574. package/src/code-language/stream.ts +257 -17
  575. package/src/code-language/types.ts +5 -4
  576. package/src/codeblock/index.ts +20 -3
  577. package/src/embed/client.ts +102 -8
  578. package/src/embed/index.ts +2 -0
  579. package/src/flow/draw.ts +167 -41
  580. package/src/flow/gl/color.ts +85 -0
  581. package/src/flow/gl/index.ts +342 -0
  582. package/src/flow/gl/pack.ts +585 -0
  583. package/src/flow/gl/renderer.ts +679 -0
  584. package/src/flow/gl/shaders.ts +371 -0
  585. package/src/flow/gl/text.ts +1295 -0
  586. package/src/flow/index.ts +794 -67
  587. package/src/flow/node.ts +2496 -1256
  588. package/src/flow/paint.ts +511 -0
  589. package/src/flow/paths.ts +80 -15
  590. package/src/flow/scene.ts +2199 -0
  591. package/src/flow/types.ts +113 -1
  592. package/src/html/controls.ts +73 -2
  593. package/src/html/css/calc.ts +385 -0
  594. package/src/html/css/cascade.ts +1403 -62
  595. package/src/html/css/color.ts +755 -0
  596. package/src/html/css/content.ts +417 -0
  597. package/src/html/css/counter-styles.ts +973 -0
  598. package/src/html/css/decode.ts +130 -0
  599. package/src/html/css/parse.ts +1514 -160
  600. package/src/html/css/style.ts +4102 -222
  601. package/src/html/css/ua.ts +58 -12
  602. package/src/html/css/values.ts +150 -50
  603. package/src/html/css/vars.ts +348 -0
  604. package/src/html/dom.ts +284 -17
  605. package/src/html/fonts.ts +641 -0
  606. package/src/html/gif.ts +233 -0
  607. package/src/html/index.ts +180 -20
  608. package/src/html/layout/block.ts +3456 -230
  609. package/src/html/layout/boxes.ts +2100 -190
  610. package/src/html/layout/cache.ts +289 -0
  611. package/src/html/layout/collapse.ts +317 -0
  612. package/src/html/layout/css-grid.ts +1063 -0
  613. package/src/html/layout/flex.ts +1115 -106
  614. package/src/html/layout/floats.ts +128 -14
  615. package/src/html/layout/grid-lines.ts +141 -0
  616. package/src/html/layout/grid.ts +181 -0
  617. package/src/html/layout/inline.ts +3943 -192
  618. package/src/html/layout/shaping.ts +140 -0
  619. package/src/html/layout/table.ts +696 -186
  620. package/src/html/layout/tracks.ts +437 -0
  621. package/src/html/node.ts +1486 -121
  622. package/src/html/paint.ts +4293 -169
  623. package/src/html/resources.ts +161 -50
  624. package/src/html/surfaces.ts +81 -0
  625. package/src/html/svg.ts +493 -0
  626. package/src/html/url.ts +86 -0
  627. package/src/index.ts +4 -0
  628. package/src/internal/collate.ts +12 -0
  629. package/src/{markdown → internal/markdown}/ast.ts +35 -7
  630. package/src/{markdown → internal/markdown}/parse.ts +288 -35
  631. package/src/internal/markdown/stringify.ts +772 -0
  632. package/src/internal/prewarm.ts +45 -0
  633. package/src/internal/scroll.ts +5 -3
  634. package/src/internal/sdf.ts +227 -0
  635. package/src/internal/splice.ts +25 -0
  636. package/src/internal/window.ts +36 -5
  637. package/src/maps/anchors.ts +913 -0
  638. package/src/maps/controller.ts +1084 -0
  639. package/src/maps/gl/buckets.ts +461 -0
  640. package/src/maps/gl/build-worker.ts +66 -0
  641. package/src/maps/gl/color.ts +62 -0
  642. package/src/maps/gl/cover.ts +205 -0
  643. package/src/maps/gl/index.ts +23 -0
  644. package/src/maps/gl/markers.ts +83 -0
  645. package/src/maps/gl/overlays.ts +379 -0
  646. package/src/maps/gl/pane.ts +42 -0
  647. package/src/maps/gl/placement.ts +968 -0
  648. package/src/maps/gl/renderer.ts +1660 -0
  649. package/src/maps/gl/shaders.ts +489 -0
  650. package/src/maps/gl/store.ts +615 -0
  651. package/src/maps/gl/text.ts +868 -0
  652. package/src/maps/gl/view.ts +1489 -0
  653. package/src/maps/icons.ts +183 -0
  654. package/src/maps/index.ts +274 -81
  655. package/src/maps/labels.ts +457 -144
  656. package/src/maps/node.ts +832 -665
  657. package/src/maps/overlay.ts +148 -43
  658. package/src/maps/paint.ts +3 -0
  659. package/src/maps/proj.ts +118 -0
  660. package/src/maps/renderer.ts +155 -0
  661. package/src/maps/sources.ts +43 -6
  662. package/src/maps/style.ts +32 -0
  663. package/src/maps/styles.ts +215 -4
  664. package/src/maps/theme.ts +51 -0
  665. package/src/maps/tiles.ts +253 -62
  666. package/src/maps/types.ts +213 -21
  667. package/src/markdown/expressions.ts +5 -1
  668. package/src/markdown/index.ts +163 -78
  669. package/src/markdown/spans.ts +6 -1
  670. package/src/media-player/index.ts +18 -4
  671. package/src/qml/containers.tsx +6 -1
  672. package/src/qml/interaction.tsx +42 -5
  673. package/src/qml/layouts.tsx +21 -6
  674. package/src/qml/qtquick.tsx +9 -2
  675. package/src/qml/react.tsx +7 -5
  676. package/src/reorder/clock.ts +27 -0
  677. package/src/reorder/index.ts +100 -27
  678. package/src/rich-text-editor/clipboard.ts +439 -0
  679. package/src/rich-text-editor/collab.ts +91 -0
  680. package/src/rich-text-editor/commands.ts +448 -0
  681. package/src/rich-text-editor/dom-globals.d.ts +23 -0
  682. package/src/rich-text-editor/drag.ts +211 -0
  683. package/src/rich-text-editor/html.ts +619 -0
  684. package/src/rich-text-editor/index.ts +1364 -0
  685. package/src/rich-text-editor/inline.ts +345 -0
  686. package/src/rich-text-editor/keymap.ts +510 -0
  687. package/src/rich-text-editor/keys.ts +228 -0
  688. package/src/rich-text-editor/look.ts +427 -0
  689. package/src/rich-text-editor/markdown.ts +653 -0
  690. package/src/rich-text-editor/marks.ts +101 -0
  691. package/src/rich-text-editor/nodes.ts +497 -0
  692. package/src/rich-text-editor/render.ts +837 -0
  693. package/src/rich-text-editor/schema.ts +431 -0
  694. package/src/rich-text-editor/store.ts +39 -0
  695. package/src/rich-text-editor/suggest.ts +597 -0
  696. package/src/rich-text-editor/tables.ts +272 -0
  697. package/src/rich-text-editor/toolbar.ts +724 -0
  698. package/src/rich-text-editor/view.ts +2287 -0
  699. package/src/rich-text-editor/virtual.ts +372 -0
  700. package/src/richtext/node.ts +294 -44
  701. package/src/richtext/runs.ts +23 -12
  702. package/src/table/index.ts +29 -0
  703. package/src/table/rows.ts +18 -8
  704. package/src/terminal/backends.ts +4 -1
  705. package/src/terminal/index.ts +47 -15
  706. package/src/terminal/vt/pty.ts +16 -2
  707. package/src/terminal-output/index.ts +6 -1
  708. package/src/three/canvas.ts +102 -8
  709. package/src/three/events.ts +49 -9
  710. package/src/tree/index.ts +19 -0
  711. package/dist/markdown/ast.d.ts.map +0 -1
  712. package/dist/markdown/ast.js +0 -17
  713. package/dist/markdown/ast.js.map +0 -1
  714. package/dist/markdown/parse.d.ts.map +0 -1
  715. package/dist/markdown/parse.js.map +0 -1
  716. package/dist/markdown/tags.d.ts.map +0 -1
  717. package/dist/markdown/tags.js.map +0 -1
  718. /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
  719. /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
  720. /package/dist/{markdown → internal/markdown}/tags.js +0 -0
  721. /package/src/{markdown → internal/markdown}/tags.ts +0 -0
@@ -20,63 +20,185 @@
20
20
  // back on the laid-out lines is exactly what that module already knows how to
21
21
  // draw.
22
22
  import { canFill, lineBands, paintRunBackgrounds, paintRunRules, } from '../richtext/runs.js';
23
- import { inkColor, isTransparent } from './css/values.js';
24
- import { Box } from './layout/boxes.js';
25
- import { layoutOffsets } from './layout/inline.js';
23
+ import { alphaOf, inkColor, isTransparent, resolve } from './css/values.js';
24
+ import { blend, borderShades } from './css/color.js';
25
+ import { CONTAIN_LAYOUT, CONTAIN_PAINT, CONTAIN_SIZE, copyStyle, scrolls, } from './css/style.js';
26
+ import { contained } from './layout/block.js';
27
+ import { BOX_RAISES, LINE_BOX_RAISES, Box, CLAMPED, CLIPPED_CELLS, COLLAPSED_CELLS, FADED_BLOCKS, PAINT_ORDER, INLINE_OFFSETS, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, TEXT_SHIFTS, } from './layout/boxes.js';
28
+ import { CLIPPED_TEXT, clipBoxOf, depthOf, inklessLayout, layoutOffsetOf, layoutOffsets, } from './layout/inline.js';
29
+ import { tableGrid } from './layout/grid.js';
30
+ import { SvgDrawing, inlineDrawing } from './svg.js';
26
31
  /**
27
32
  * The bounds of everything a box and its descendants draw, in document
28
- * coordinates. Computed once per layout; the paint pass reads it.
29
- */
30
- export function computePaintBounds(box) {
31
- let x1 = box.x;
32
- let y1 = box.y;
33
- let x2 = box.x + box.width;
34
- let y2 = box.y + box.height;
35
- if (box.lines) {
36
- for (const line of box.lines) {
33
+ * coordinates, into `boundsX`…, with the text `position: relative` moved off
34
+ * its lines where `moved` says there is some. Computed once per layout; the paint pass
35
+ * reads it. Returns how far down the box's content reaches, for the
36
+ * document's height — handed up rather than kept on every box, where
37
+ * writing it and reading it back cost this walk a fifth of its time.
38
+ */
39
+ export function computePaintBounds(box, moved = false) {
40
+ // A box with no rectangle of its own gives only what it holds: nothing,
41
+ // until a child with bounds is met.
42
+ const own = hasRect(box);
43
+ let x1 = own ? box.x : Infinity;
44
+ let y1 = own ? box.y : Infinity;
45
+ let x2 = own ? box.x + box.width : -Infinity;
46
+ let y2 = own ? box.y + box.height : -Infinity;
47
+ // How far down the content reaches, for the document's height: the
48
+ // border box, and every box and line under it, but not past a box that
49
+ // clips what it holds — where the scrollable overflow ends. Out-of-flow
50
+ // boxes are the layout's to count.
51
+ let bottom = y2;
52
+ // A shadow is ink past the box, and no overflow: a repaint of the strip
53
+ // under a card has to reach the card, and the document is no taller.
54
+ const shadows = own ? box.style.boxShadow : null;
55
+ if (shadows) {
56
+ for (const shadow of shadows) {
57
+ if (shadow.inset)
58
+ continue;
59
+ const reach = shadow.spread + shadowReach(shadow.blur);
60
+ x1 = Math.min(x1, box.x + shadow.x - reach);
61
+ y1 = Math.min(y1, box.y + shadow.y - reach);
62
+ x2 = Math.max(x2, box.x + box.width + shadow.x + reach);
63
+ y2 = Math.max(y2, box.y + box.height + shadow.y + reach);
64
+ }
65
+ }
66
+ // and so is an outline
67
+ if (own && box.style.outlineStyle !== 'none') {
68
+ const reach = Math.max(0, box.style.outlineOffset + box.style.outlineWidth);
69
+ x1 = Math.min(x1, box.x - reach);
70
+ y1 = Math.min(y1, box.y - reach);
71
+ x2 = Math.max(x2, box.x + box.width + reach);
72
+ y2 = Math.max(y2, box.y + box.height + reach);
73
+ }
74
+ const lines = box.lines;
75
+ if (lines) {
76
+ let tallest = 0;
77
+ for (const line of lines) {
37
78
  x1 = Math.min(x1, line.x);
38
79
  y1 = Math.min(y1, line.y);
39
80
  x2 = Math.max(x2, line.x + line.width);
40
81
  y2 = Math.max(y2, line.y + line.height);
41
- for (const placed of line.atomics) {
42
- const bounds = computePaintBounds(placed.box);
43
- x1 = Math.min(x1, bounds.x);
44
- y1 = Math.min(y1, bounds.y);
45
- x2 = Math.max(x2, bounds.x + bounds.width);
46
- y2 = Math.max(y2, bounds.y + bounds.height);
82
+ tallest = Math.max(tallest, line.height);
83
+ if (moved && SHIFTED_LINES.has(line)) {
84
+ // text `position: relative` moved off its line
85
+ for (const text of line.texts) {
86
+ const natural = text.layout.lines[text.layoutLine];
87
+ if (!natural)
88
+ continue;
89
+ const x = text.drawX + natural.x;
90
+ const y = text.drawY + natural.y;
91
+ x1 = Math.min(x1, x);
92
+ y1 = Math.min(y1, y);
93
+ x2 = Math.max(x2, x + natural.width);
94
+ y2 = Math.max(y2, y + natural.height);
95
+ }
47
96
  }
48
97
  }
98
+ box.maxLineHeight = tallest;
99
+ if (lines.length) {
100
+ const last = lines[lines.length - 1];
101
+ bottom = Math.max(bottom, last.y + last.height);
102
+ }
49
103
  }
50
- if (box.markerLayout) {
104
+ const marker = box.marker;
105
+ const drawn = marker?.image ?? marker?.layout;
106
+ if (marker && drawn) {
51
107
  // The marker hangs in the padding to the left of the content, so it is
52
108
  // outside the border box and has to widen the ink bounds or a repaint
53
109
  // clipped to a narrow strip drops it.
54
- x1 = Math.min(x1, box.markerX);
55
- y1 = Math.min(y1, box.markerY);
56
- x2 = Math.max(x2, box.markerX + box.markerLayout.width);
57
- y2 = Math.max(y2, box.markerY + box.markerLayout.height);
110
+ x1 = Math.min(x1, marker.x);
111
+ y1 = Math.min(y1, marker.y);
112
+ x2 = Math.max(x2, marker.x + drawn.width);
113
+ y2 = Math.max(y2, marker.y + drawn.height);
58
114
  }
59
115
  for (const child of box.children) {
60
116
  if (child.kind === 'text' || child.kind === 'break')
61
117
  continue;
62
- const bounds = computePaintBounds(child);
63
- x1 = Math.min(x1, bounds.x);
64
- y1 = Math.min(y1, bounds.y);
65
- x2 = Math.max(x2, bounds.x + bounds.width);
66
- y2 = Math.max(y2, bounds.y + bounds.height);
67
- }
68
- box.boundsX = x1;
69
- box.boundsY = y1;
70
- box.boundsWidth = x2 - x1;
71
- box.boundsHeight = y2 - y1;
72
- if (box.lines) {
73
- let tallest = 0;
74
- for (const line of box.lines)
75
- tallest = Math.max(tallest, line.height);
76
- box.maxLineHeight = tallest;
118
+ // past a clamp point: nothing drawn, and no overflow (CSS Overflow 4,
119
+ // 5.3.1)
120
+ if (CLAMPED.has(child)) {
121
+ child.boundsX = Infinity;
122
+ child.boundsY = Infinity;
123
+ child.boundsWidth = 0;
124
+ child.boundsHeight = 0;
125
+ continue;
126
+ }
127
+ const reach = computePaintBounds(child, moved);
128
+ if (!child.outOfFlow)
129
+ bottom = Math.max(bottom, reach);
130
+ if (child.boundsY === Infinity)
131
+ continue;
132
+ x1 = Math.min(x1, child.boundsX);
133
+ y1 = Math.min(y1, child.boundsY);
134
+ x2 = Math.max(x2, child.boundsX + child.boundsWidth);
135
+ y2 = Math.max(y2, child.boundsY + child.boundsHeight);
136
+ }
137
+ // An atomic on a line is the box's, or an inline box's in it, so the walk
138
+ // above has its bounds already: walked from its line as well, an
139
+ // inline-block in an inline-block was walked twice a level, and twenty of
140
+ // them took seventy milliseconds a layout.
141
+ if (lines) {
142
+ for (const line of lines) {
143
+ for (const placed of line.atomics) {
144
+ const atomic = placed.box;
145
+ if (atomic.boundsY === Infinity)
146
+ continue;
147
+ x1 = Math.min(x1, atomic.boundsX);
148
+ y1 = Math.min(y1, atomic.boundsY);
149
+ x2 = Math.max(x2, atomic.boundsX + atomic.boundsWidth);
150
+ y2 = Math.max(y2, atomic.boundsY + atomic.boundsHeight);
151
+ }
152
+ }
153
+ }
154
+ if (x1 === Infinity) {
155
+ // nothing to draw: no damage meets it, and no parent takes it in
156
+ box.boundsX = Infinity;
157
+ box.boundsY = Infinity;
158
+ box.boundsWidth = 0;
159
+ box.boundsHeight = 0;
160
+ }
161
+ else {
162
+ box.boundsX = x1;
163
+ box.boundsY = y1;
164
+ box.boundsWidth = x2 - x1;
165
+ box.boundsHeight = y2 - y1;
77
166
  }
78
167
  buildChildIndexes(box);
79
- return { x: x1, y: y1, width: box.boundsWidth, height: box.boundsHeight };
168
+ // A box with no rectangle reaches as far as what it holds, and no
169
+ // further: its `y` and `height` were never laid out, but moving a laid
170
+ // out subtree (`translate`) moves them with the rest, so they add up
171
+ // across passes. A flex item laid out at one width and then another had a
172
+ // link in it reach a document's height below its end.
173
+ if (!own)
174
+ return bottom;
175
+ // whether the box clips only matters where its content reaches past it,
176
+ // and its style is one more object a walk of every box would read
177
+ const end = box.y + box.height;
178
+ if (bottom <= end)
179
+ return end;
180
+ const style = box.style;
181
+ return box.parent &&
182
+ (style.overflowX !== 'visible' || style.overflowY !== 'visible')
183
+ ? end
184
+ : bottom;
185
+ }
186
+ /**
187
+ * Whether a box has a rectangle of its own to draw in. An inline box has
188
+ * none: it is drawn on its block's lines, which the block's bounds hold,
189
+ * and its `x`…`height` are never laid out. Nor has a table column, drawn in
190
+ * its cells. Taken for rectangles, their (0, 0, 0, 0) stretched every block
191
+ * with a link in it, and every table with a `<col>`, up to the top of the
192
+ * document — and a paint low in a long document went through every block
193
+ * above the viewport, as did a hit test during a selection drag.
194
+ */
195
+ export function hasRect(box) {
196
+ if (box.kind === 'inline')
197
+ return false;
198
+ if (box.kind !== 'block')
199
+ return true;
200
+ const display = box.style.display;
201
+ return display !== 'table-column' && display !== 'table-column-group';
80
202
  }
81
203
  /** Children lists past this size get the sorted viewport index; below it a
82
204
  * linear scan is cheaper than keeping one. */
@@ -86,14 +208,18 @@ function buildChildIndexes(box) {
86
208
  box.positionedPaint = null;
87
209
  let positioned = null;
88
210
  let paintable = 0;
211
+ let inner = false;
89
212
  for (const child of box.children) {
90
213
  if (child.kind === 'text' || child.kind === 'break')
91
214
  continue;
92
- if (child.outOfFlow)
215
+ if (child.holdsLayers)
216
+ inner = true;
217
+ if (layered(box, child))
93
218
  (positioned ??= []).push(child);
94
- else
219
+ else if (!onLine(box, child))
95
220
  paintable += 1;
96
221
  }
222
+ box.holdsLayers = positioned !== null || inner;
97
223
  if (positioned) {
98
224
  // Pre-sorted once per layout instead of filtered and sorted per paint.
99
225
  positioned.sort(byZIndex);
@@ -103,9 +229,10 @@ function buildChildIndexes(box) {
103
229
  return;
104
230
  const boxes = [];
105
231
  for (const child of box.children) {
106
- if (child.kind === 'text' || child.kind === 'break' || child.outOfFlow)
232
+ if (child.kind === 'text' || child.kind === 'break')
107
233
  continue;
108
- boxes.push(child);
234
+ if (!layered(box, child) && !onLine(box, child))
235
+ boxes.push(child);
109
236
  }
110
237
  const order = boxes.map((_, i) => i);
111
238
  order.sort((a, b) => boxes[a].boundsY - boxes[b].boundsY);
@@ -153,110 +280,2177 @@ export function paintDocument(ctx, tree, options) {
153
280
  if (!canFill(ctx))
154
281
  return;
155
282
  ctx.save();
156
- paintBox(ctx, tree.root, options);
283
+ const canvas = canvasBackground(tree.root);
284
+ if (canvas)
285
+ paintCanvas(ctx, canvas, tree.root, options);
286
+ paintBox(ctx, tree.root, {
287
+ ...options,
288
+ canvasSource: canvas?.source,
289
+ negative: tree.negative,
290
+ });
157
291
  ctx.restore();
158
292
  }
293
+ /**
294
+ * Whose background covers the canvas (CSS 2.1 14.2): the root element's,
295
+ * or — where `<html>` has neither a colour nor an image — the first
296
+ * `<body>`'s, which then paints no background of its own. A document with
297
+ * no `<html>` tag has the root box standing in for its root element, and a
298
+ * `<body>` in it is still the body: mail often starts at `<body style>`.
299
+ * `anchor` is the box the image is positioned against: the root element's,
300
+ * whichever box it came from.
301
+ */
302
+ function canvasBackground(root) {
303
+ const has = (b) => !isTransparent(b.style.backgroundColor) ||
304
+ !!b.style.backgroundImage ||
305
+ !!b.style.backgroundGradient ||
306
+ !!b.style.backgroundImages;
307
+ const top = childNamed(root, 'html') ?? root;
308
+ if (has(top))
309
+ return { source: top, anchor: top };
310
+ const body = childNamed(top, 'body');
311
+ // containment on either keeps the body's to the body (CSS Containment 2)
312
+ if (!body || top.style.contain || body.style.contain)
313
+ return null;
314
+ return has(body) ? { source: body, anchor: top } : null;
315
+ }
316
+ /** A box's child element of that name, or one inside the anonymous boxes
317
+ * it is wrapped in: a `<body>` in an `<html>` set `display: table` is in
318
+ * an anonymous row and cell. */
319
+ function childNamed(box, name) {
320
+ for (const child of box.children) {
321
+ if (child.el?.name === name)
322
+ return child;
323
+ if (!child.el) {
324
+ const found = childNamed(child, name);
325
+ if (found)
326
+ return found;
327
+ }
328
+ }
329
+ return null;
330
+ }
331
+ function paintCanvas(ctx, { source, anchor }, root, options) {
332
+ const whole = options.canvas ?? {
333
+ x: root.x + options.originX,
334
+ y: root.y + options.originY,
335
+ width: root.width,
336
+ height: root.height,
337
+ };
338
+ const area = clampRect(options, Math.round(whole.x), Math.round(whole.y), Math.ceil(whole.width), Math.ceil(whole.height));
339
+ if (!area)
340
+ return;
341
+ const layers = layersOf(source.style) ?? [source.style];
342
+ const visible = source.style.visibility === 'visible';
343
+ for (let i = layers.length - 1; i >= 0; i -= 1) {
344
+ const style = layers[i];
345
+ if (!isTransparent(style.backgroundColor) && visible) {
346
+ ctx.fillStyle = inkColor(style.backgroundColor, style.color);
347
+ ctx.fillRect(area.x, area.y, area.w, area.h);
348
+ }
349
+ if (style.backgroundGradient && ctx.createLinearGradient && visible) {
350
+ // sized by the root element's box and repeated down the canvas, as a
351
+ // browser does — the stripes a short page with a gradient on its
352
+ // body shows — or by the viewport, where it is fixed
353
+ const box = style.backgroundAttachment === 'fixed' && options.canvas
354
+ ? options.canvas
355
+ : originBox(anchor, options, style);
356
+ paintGradient(ctx, style, style.backgroundGradient, area, snapped(box.x, box.y, box.width, box.height));
357
+ }
358
+ if (style.backgroundImage) {
359
+ paintBackgroundImage(ctx, style, area, originBox(anchor, options, style), options);
360
+ }
361
+ }
362
+ }
363
+ const LAYERS = new WeakMap();
364
+ /**
365
+ * A style for each of a background's layers, top first, where it has more
366
+ * than one (CSS Backgrounds 3, 2.1); null where it has one, which the style
367
+ * itself is. Each is the box's own style with the layer's image, repeat,
368
+ * size, attachment and position in place of the first's — a property with
369
+ * fewer values than there are images taking them over again — and the
370
+ * colour on the bottom layer alone, which is painted first, under the
371
+ * others. Made once a style, so a repaint makes none.
372
+ */
373
+ function layersOf(style) {
374
+ const images = style.backgroundImages;
375
+ if (!images)
376
+ return null;
377
+ let layers = LAYERS.get(style);
378
+ if (layers)
379
+ return layers;
380
+ const nth = (list, first, i) => list ? list[i % list.length] : first;
381
+ layers = images.map((image, i) => {
382
+ const layer = Object.create(style);
383
+ layer.backgroundImages = null;
384
+ layer.backgroundImage = typeof image === 'string' ? image : null;
385
+ layer.backgroundGradient = typeof image === 'string' ? null : image;
386
+ layer.backgroundRepeat = nth(style.backgroundRepeats, style.backgroundRepeat, i);
387
+ layer.backgroundSize = nth(style.backgroundSizes, style.backgroundSize, i);
388
+ layer.backgroundAttachment = nth(style.backgroundAttachments, style.backgroundAttachment, i);
389
+ layer.backgroundClip = nth(style.backgroundClips, style.backgroundClip, i);
390
+ layer.backgroundOrigin = nth(style.backgroundOrigins, style.backgroundOrigin, i);
391
+ [layer.backgroundPositionX, layer.backgroundPositionY] = nth(style.backgroundPositions, [style.backgroundPositionX, style.backgroundPositionY], i);
392
+ if (i < images.length - 1)
393
+ layer.backgroundColor = null;
394
+ return layer;
395
+ });
396
+ LAYERS.set(style, layers);
397
+ return layers;
398
+ }
399
+ /**
400
+ * A box's background: its colour and each of its layers, bottom first.
401
+ * `paintBackground` draws a colour and a gradient, and `image`, where a
402
+ * caller draws images, a layer that is one.
403
+ */
404
+ function paintLayers(ctx, box, options, image) {
405
+ // a background painted through the text is painted with it
406
+ // (`paintClippedText`), and not as a box
407
+ if (box.style.backgroundClipText)
408
+ return;
409
+ const layers = layersOf(box.style);
410
+ if (!layers) {
411
+ paintBackground(ctx, box, options, box.style);
412
+ if (image && box.style.backgroundImage)
413
+ image(box.style);
414
+ return;
415
+ }
416
+ for (let i = layers.length - 1; i >= 0; i -= 1) {
417
+ paintBackground(ctx, box, options, layers[i]);
418
+ if (image && layers[i].backgroundImage)
419
+ image(layers[i]);
420
+ }
421
+ }
422
+ /** A box's padding box in window coordinates. */
423
+ function paddingBox(box, options) {
424
+ return {
425
+ x: box.x + box.borderLeft + options.originX,
426
+ y: frameY(box) + box.borderTop + options.originY,
427
+ width: box.width - box.borderLeft - box.borderRight,
428
+ height: frameHeight(box) - box.borderTop - box.borderBottom,
429
+ };
430
+ }
431
+ /** Where a layer of a box's background is placed, in window coordinates:
432
+ * the box its `background-origin` names (CSS Backgrounds 3, 3.8). */
433
+ function originBox(box, options, style) {
434
+ const [t, r, b, l] = edgeInsets(box, style.backgroundOrigin);
435
+ return {
436
+ x: box.x + l + options.originX,
437
+ y: frameY(box) + t + options.originY,
438
+ width: box.width - l - r,
439
+ height: frameHeight(box) - t - b,
440
+ };
441
+ }
442
+ /**
443
+ * Where a layer of a box's background is painted: the box its
444
+ * `background-clip` names (CSS Backgrounds 3, 3.7), each edge on the pixel
445
+ * it falls nearest, as browsers snap a box — boxes that meet share the
446
+ * column their edge is in, and a rule 1.33px wide is one pixel, not two —
447
+ * with the border box's corners less the widths between (5.3), where
448
+ * `rounded` asks for them. Null where it has no area.
449
+ */
450
+ function clipArea(box, options, style, rounded) {
451
+ const left = box.x + options.originX;
452
+ const top = frameY(box) + options.originY;
453
+ const right = left + box.width;
454
+ const bottom = top + frameHeight(box);
455
+ const [t, r, b, l] = edgeInsets(box, style.backgroundClip);
456
+ const x = Math.round(left + l);
457
+ const y = Math.round(top + t);
458
+ const w = Math.round(right - r) - x;
459
+ const h = Math.round(bottom - b) - y;
460
+ if (!(w > 0 && h > 0))
461
+ return null;
462
+ let corners = rounded
463
+ ? cornersOf(style, Math.round(right) - Math.round(left), Math.round(bottom) - Math.round(top))
464
+ : null;
465
+ if (corners && (t || r || b || l)) {
466
+ corners = spreadCorners(corners, box.width, frameHeight(box), -t, -r, -b, -l);
467
+ }
468
+ return { x, y, w, h, corners };
469
+ }
470
+ /** Where a box's background and border go: its border box, which for a
471
+ * table leaves out the captions around it (CSS 2.1 17.4). */
472
+ function frameY(box) {
473
+ return box.y + box.captionTop;
474
+ }
475
+ function frameHeight(box) {
476
+ return box.height - box.captionTop - box.captionBottom;
477
+ }
159
478
  function paintBox(ctx, box, options) {
160
- if (!intersects(box, options))
479
+ if (!intersects(box, options) ||
480
+ COLLAPSED_CELLS.has(box) ||
481
+ CLAMPED.has(box)) {
161
482
  return;
162
- const style = box.style;
163
- const visible = style.visibility === 'visible';
483
+ }
484
+ // An element under full opacity is painted whole in its place, as the
485
+ // group it is (it is a stacking context, CSS Color 4 3.2; `inFlow`): at
486
+ // 0 not at all — the control a page keeps invisible until its row is
487
+ // hovered — and between through the context's alpha. That multiplies
488
+ // each thing drawn rather than the group they make, so where two of its
489
+ // own boxes overlap the lower shows through the upper, as a browser's
490
+ // group does not let it.
491
+ const opacity = opacityOf(box);
492
+ if (opacity <= 0)
493
+ return;
494
+ const fade = opacity < 1 && typeof ctx.globalAlpha === 'number';
495
+ if (fade) {
496
+ ctx.save();
497
+ ctx.globalAlpha = ctx.globalAlpha * opacity;
498
+ }
499
+ // `clip` shows the part of an absolutely positioned box it names, its own
500
+ // background and borders among it (CSS 2.1 11.1.2)
501
+ const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
502
+ if (!clip || (clip.w > 0 && clip.h > 0)) {
503
+ const clipped = !!clip && pushClip(ctx, clip, null);
504
+ paintContent(ctx, box, options);
505
+ if (clipped)
506
+ ctx.restore();
507
+ }
508
+ if (fade)
509
+ ctx.restore();
510
+ }
511
+ function paintContent(ctx, box, options) {
512
+ const visible = box.style.visibility === 'visible';
513
+ if (visible)
514
+ paintOwnBackground(ctx, box, options);
515
+ // `content-visibility: hidden` skips what the box holds, its own
516
+ // background, borders and outline drawn (CSS Containment 2, 4)
517
+ if (box.style.contentVisibility === 'hidden' &&
518
+ box.kind !== 'replaced' &&
519
+ contained(box, CONTAIN_SIZE)) {
520
+ if (box.style.outlineStyle !== 'none')
521
+ paintOutline(ctx, box, options);
522
+ return;
523
+ }
524
+ // a stacking context's descendants with a negative `z-index`, over its
525
+ // background and under everything else in it (CSS 2.1 Appendix E)
526
+ const below = options.negative ? NEGATIVE.get(box) : undefined;
527
+ if (below) {
528
+ for (const child of below)
529
+ paintStacked(ctx, child, options);
530
+ }
164
531
  if (visible) {
165
- paintBackground(ctx, box, options);
166
- paintBorders(ctx, box, options);
167
- if (box.markerText)
168
- paintMarker(ctx, box, options);
532
+ if (box.marker)
533
+ paintMarker(ctx, box.marker, options);
169
534
  if (box.replaced === 'image')
170
535
  paintImage(ctx, box, options);
536
+ else if (box.replaced === 'svg')
537
+ paintSvg(ctx, box, options);
538
+ }
539
+ // A box that does not let its content overflow clips it to its padding
540
+ // box, rounded where the box is (CSS 2.1 11.1.1). Everything inside is
541
+ // clipped but a positioned box whose containing block is outside: that
542
+ // is painted once this clip is gone (`paintPositioned`).
543
+ let level = null;
544
+ const clips = clipsOverflow(box);
545
+ // a table's captions are outside the table box that clips, and painted
546
+ // before it does
547
+ if (clips && box.kind === 'table') {
548
+ for (const child of box.children) {
549
+ if (child.kind === 'table-caption')
550
+ paintBox(ctx, child, options);
551
+ }
552
+ }
553
+ if (clips) {
554
+ const { rect, radii } = clipEdge(box, options);
555
+ // Clipped to no area, nothing in the box shows but an absolute box
556
+ // whose containing block is outside it, and where it holds none what it
557
+ // holds is not painted at all: a menu at `max-height: 0`. Clipped to an
558
+ // empty rectangle instead, it was painted through a mask the size of
559
+ // the window, which is what an empty rectangle is to the context, built
560
+ // again at every restore — and a nest of them did that at every level.
561
+ if ((rect.w <= 0 || rect.h <= 0) && !holdsAbsolute(box)) {
562
+ if (box.style.outlineStyle !== 'none' && box.kind !== 'inline') {
563
+ paintOutline(ctx, box, options);
564
+ }
565
+ return;
566
+ }
567
+ if (pushClip(ctx, rect, radii)) {
568
+ level = { box, deferred: [] };
569
+ (options.clips ??= []).push(level);
570
+ }
571
+ }
572
+ // The flow this box holds, in CSS 2.1 Appendix E's order: the backgrounds
573
+ // and borders of its in-flow blocks, then its floats, then the lines of
574
+ // them all, then — where it is a stacking context — the positioned boxes
575
+ // in it (`stackLayers`). Painted a block at a time instead,
576
+ // a float was covered by the background of every block after it — the
577
+ // shaded paragraph beside a floated image hid the image — and a block's
578
+ // text by the next one's background where a negative margin overlapped
579
+ // them. A child that is no plain block of the flow — a table, a flex box,
580
+ // a box that clips, a replaced element — is painted whole in its place.
581
+ const floats = [];
582
+ paintFlowBackgrounds(ctx, box, options, floats);
583
+ for (const float of floats)
584
+ paintBox(ctx, float, options);
585
+ // a hidden box's text is drawn in no ink, so that a visible element's in
586
+ // it is drawn (`runFor`)
587
+ if (box.lines)
588
+ paintLines(ctx, box, options);
589
+ const outlines = [];
590
+ paintFlowLines(ctx, box, options, outlines);
591
+ // the outlines of the blocks in the flow over all of its lines, and under
592
+ // its positioned boxes, as browsers draw them (CSS 2.1 Appendix E, step
593
+ // 10 left the choice open): drawn after each block's own lines, an
594
+ // outline went under an inline-block the next block held
595
+ for (const child of outlines)
596
+ paintOutline(ctx, child, options);
597
+ if (box.collapsed && visible)
598
+ paintCollapsedBorders(ctx, box, options);
599
+ // `z-index: auto` and 0 in document order, then the positive ones
600
+ const stacked = STACKED.get(box);
601
+ if (stacked)
602
+ for (const child of stacked)
603
+ paintStacked(ctx, child, options);
604
+ if (level) {
605
+ ctx.restore();
606
+ options.clips.pop();
607
+ for (const child of level.deferred)
608
+ paintPositioned(ctx, child, options);
609
+ }
610
+ // over all of it, outside the box's own clip; an inline box's is drawn a
611
+ // fragment at a time, on its lines
612
+ if (box.style.outlineStyle !== 'none' && box.kind !== 'inline') {
613
+ paintOutline(ctx, box, options);
171
614
  }
172
- // In-flow and floated descendants first, then the inline content, then the
173
- // positioned ones — a flattening of CSS's painting order that is right for
174
- // everything short of a document that puts a negative z-index under its own
175
- // parent's background.
615
+ }
616
+ /** A box's own background, background image and borders, and a table's
617
+ * parts' backgrounds; a row or a row group paints none of its own. */
618
+ function paintOwnBackground(ctx, box, options) {
619
+ // A row or a row group paints nothing of its own: its background is
620
+ // painted in its cells' areas with the table's (`paintPartBackgrounds`),
621
+ // and its borders are the collapsed grid's or none (CSS 2.1 17.6.1)
622
+ if (box.kind === 'table-row' || box.kind === 'table-row-group')
623
+ return;
624
+ if (hidden(box))
625
+ return;
626
+ const style = box.style;
627
+ if (style.boxShadow)
628
+ paintShadows(ctx, box, options, false);
629
+ if (box !== options.canvasSource) {
630
+ paintLayers(ctx, box, options, (layer) => {
631
+ const painted = clipArea(box, options, layer, true);
632
+ const area = painted &&
633
+ clampRect(options, painted.x, painted.y, painted.w, painted.h);
634
+ if (area) {
635
+ paintBackgroundImage(ctx, layer, area, originBox(box, options, layer), options, painted.corners);
636
+ }
637
+ });
638
+ }
639
+ if (style.boxShadow)
640
+ paintShadows(ctx, box, options, true);
641
+ if (!box.bordersCollapsed && !paintBorderImage(ctx, box, options)) {
642
+ paintBorders(ctx, box, options);
643
+ }
644
+ if (box.kind === 'table')
645
+ paintPartBackgrounds(ctx, box, options);
646
+ }
647
+ /** How far past its shape a shadow's blur shows: three standard deviations,
648
+ * the blur being two (CSS Backgrounds 3, 7.1.1). */
649
+ function shadowReach(blur) {
650
+ return blur > 0 ? Math.ceil(blur * 1.5) : 0;
651
+ }
652
+ const SQUARE = { x: [0, 0, 0, 0], y: [0, 0, 0, 0] };
653
+ /** Corners shrunk by `by` — an inner shadow's spread — or grown where it
654
+ * is negative; a square corner stays square. */
655
+ function grownCorners(c, by) {
656
+ const grow = (r) => (r > 0 ? Math.max(0, r + by) : 0);
657
+ return {
658
+ x: [grow(c.x[0]), grow(c.x[1]), grow(c.x[2]), grow(c.x[3])],
659
+ y: [grow(c.y[0]), grow(c.y[1]), grow(c.y[2]), grow(c.y[3])],
660
+ };
661
+ }
662
+ /**
663
+ * One dimension of a corner's radius moved out by `by`, or in where it is
664
+ * negative (CSS Backgrounds 3, 4.2, the outset-adjusted border radius): a
665
+ * radius small beside the outset grows by less than it, and the less the
666
+ * rounder the corner already is — `coverage` is how much of the box's side
667
+ * its two corners take, 1 for an ellipse — so a small rounded corner on a
668
+ * big outset stays nearly square and a circle stays a circle.
669
+ */
670
+ function spreadRadius(radius, by, coverage) {
671
+ if (by <= 0)
672
+ return radius + by;
673
+ if (radius > by || coverage > 1)
674
+ return radius + by;
675
+ return radius + by * (1 - (1 - radius / by) ** 3 * (1 - coverage ** 3));
676
+ }
677
+ /**
678
+ * A box's shadows (CSS Backgrounds 3, 7.1), the outer ones under its
679
+ * background and the inset ones over it, back to front. A shadow with no
680
+ * blur is a shape of one colour, and one that only spreads, the ring
681
+ * Tailwind's `ring-*` draws a border with, is the band between two shapes.
682
+ * A blurred one is the context's shadow of a shape drawn clear of the
683
+ * window, so only the shadow lands: ntk bakes and caches the blur, and
684
+ * CoreGraphics draws it. An outer shadow is not drawn under its box, which
685
+ * the box's own opaque colour usually sees to; where it does not, the
686
+ * shadow is clipped out of the box, a clip the size of the window on X11.
687
+ * An inset one is clipped to the padding box.
688
+ */
689
+ function paintShadows(ctx, box, options, inset) {
690
+ const shadows = box.style.boxShadow;
691
+ if (!ctx.beginPath || !ctx.fill || !ctx.roundRect)
692
+ return;
693
+ const style = box.style;
694
+ const left = box.x + options.originX;
695
+ const top = frameY(box) + options.originY;
696
+ const rect = snapped(left, top, box.width, frameHeight(box));
697
+ if (!(rect.width > 0 && rect.height > 0))
698
+ return;
699
+ const corners = cornersOf(style, rect.width, rect.height) ?? SQUARE;
700
+ const canShadow = 'shadowBlur' in ctx;
701
+ if (!inset) {
702
+ const bg = style.backgroundColor;
703
+ const covered = !!bg && !isTransparent(bg) && alphaOf(inkColor(bg, style.color)) === 1;
704
+ for (let i = shadows.length - 1; i >= 0; i -= 1) {
705
+ const s = shadows[i];
706
+ if (s.inset || (s.blur > 0 && !canShadow))
707
+ continue;
708
+ const shape = {
709
+ x: rect.x + s.x - s.spread,
710
+ y: rect.y + s.y - s.spread,
711
+ width: rect.width + 2 * s.spread,
712
+ height: rect.height + 2 * s.spread,
713
+ };
714
+ if (!(shape.width > 0 && shape.height > 0))
715
+ continue;
716
+ const reach = shadowReach(s.blur);
717
+ if (!clampRect(options, shape.x - reach, shape.y - reach, shape.width + 2 * reach, shape.height + 2 * reach)) {
718
+ continue;
719
+ }
720
+ // Cut, as a background is (`clampRect`), to what the paint reaches
721
+ // and as far again as the blur does, so the cut edges cast nothing
722
+ // on it. A shadow down a box thousands of pixels tall went to the
723
+ // server whole, and its outline, sent in 16.16 fixed point, threw.
724
+ const within = reach + CLAMP_PAD;
725
+ const cut = clampAround(options, shape, within);
726
+ if (!cut)
727
+ continue;
728
+ Object.assign(shape, cut);
729
+ const own = clampAround(options, rect, within) ?? rect;
730
+ const around = spreadCorners(corners, rect.width, rect.height, s.spread, s.spread, s.spread, s.spread) ?? SQUARE;
731
+ const color = inkColor(s.color, style.color);
732
+ if (!(s.blur > 0) && !s.x && !s.y && s.spread >= 0 && !covered) {
733
+ // a ring about the box: the band between it and the spread
734
+ ctx.fillStyle = color;
735
+ fillRing(ctx, shape, around, own, corners);
736
+ continue;
737
+ }
738
+ if (!(s.blur > 0) && covered) {
739
+ // a hard shadow, under a box that hides what falls under it
740
+ fillShadow(ctx, s, color, (dx) => {
741
+ roundedRect(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
742
+ }, 0);
743
+ continue;
744
+ }
745
+ if (bakedOuter(ctx, options, s, color, shape, around, covered ? null : { rect: own, corners })) {
746
+ continue;
747
+ }
748
+ // what of the shadow falls under the box is not drawn
749
+ const clipped = !covered && ctx.clip && ctx.rect && ctx.save;
750
+ if (clipped) {
751
+ ctx.save();
752
+ ctx.beginPath();
753
+ ctx.rect(shape.x - reach - 1, shape.y - reach - 1, shape.width + 2 * reach + 2, shape.height + 2 * reach + 2);
754
+ roundedRect(ctx, own.x, own.y, own.width, own.height, corners, true, true);
755
+ ctx.clip();
756
+ }
757
+ fillShadow(ctx, s, color, (dx) => {
758
+ roundedRect(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
759
+ }, shape.x + shape.width + reach);
760
+ if (clipped)
761
+ ctx.restore();
762
+ }
763
+ return;
764
+ }
765
+ // the padding box, which an inset shadow falls inside
766
+ const pad = {
767
+ x: rect.x + box.borderLeft,
768
+ y: rect.y + box.borderTop,
769
+ width: rect.width - box.borderLeft - box.borderRight,
770
+ height: rect.height - box.borderTop - box.borderBottom,
771
+ };
772
+ if (!(pad.width > 0 && pad.height > 0))
773
+ return;
774
+ if (!clampRect(options, pad.x, pad.y, pad.width, pad.height))
775
+ return;
776
+ // cut as an outer shadow is, by as far as any of them reaches: what is
777
+ // cast from beside a cut edge falls short of what the paint reaches
778
+ let furthest = 0;
779
+ for (const s of shadows) {
780
+ if (s.inset) {
781
+ furthest = Math.max(furthest, shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1);
782
+ }
783
+ }
784
+ const padCut = clampAround(options, pad, furthest + CLAMP_PAD);
785
+ if (!padCut)
786
+ return;
787
+ Object.assign(pad, padCut);
788
+ const inner = insetCorners(corners, box.borderTop, box.borderRight, box.borderBottom, box.borderLeft);
789
+ for (let i = shadows.length - 1; i >= 0; i -= 1) {
790
+ const s = shadows[i];
791
+ if (!s.inset || (s.blur > 0 && !canShadow))
792
+ continue;
793
+ const color = inkColor(s.color, style.color);
794
+ // the hole the shadow is cast around, moved and shrunk by the spread
795
+ const hole = {
796
+ x: pad.x + s.x + s.spread,
797
+ y: pad.y + s.y + s.spread,
798
+ width: Math.max(0, pad.width - 2 * s.spread),
799
+ height: Math.max(0, pad.height - 2 * s.spread),
800
+ };
801
+ const within = grownCorners(inner, -s.spread);
802
+ if (!(s.blur > 0) && !s.x && !s.y && s.spread >= 0) {
803
+ // Tailwind's `ring-inset`: a band inside the padding edge
804
+ ctx.fillStyle = color;
805
+ fillRing(ctx, pad, inner, hole, within);
806
+ continue;
807
+ }
808
+ if (bakedInset(ctx, options, s, color, pad, inner, hole, within))
809
+ continue;
810
+ if (!ctx.clip || !ctx.save || !ctx.rect)
811
+ continue;
812
+ ctx.save();
813
+ ctx.beginPath();
814
+ roundedRect(ctx, pad.x, pad.y, pad.width, pad.height, inner);
815
+ ctx.clip();
816
+ // a frame around the hole, as wide as the blur and the offset reach
817
+ const reach = shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1;
818
+ const frame = {
819
+ x: pad.x - reach,
820
+ y: pad.y - reach,
821
+ width: pad.width + 2 * reach,
822
+ height: pad.height + 2 * reach,
823
+ };
824
+ fillShadow(ctx, s, color, (dx) => {
825
+ ctx.rect(frame.x - dx, frame.y, frame.width, frame.height);
826
+ if (hole.width > 0 && hole.height > 0) {
827
+ roundedRect(ctx, hole.x - dx, hole.y, hole.width, hole.height, within, true, true);
828
+ }
829
+ }, frame.x + frame.width + shadowReach(s.blur));
830
+ ctx.restore();
831
+ }
832
+ }
833
+ /** Corners as part of a key. */
834
+ function cornerKey(c) {
835
+ return `${c.x.join(',')}/${c.y.join(',')}`;
836
+ }
837
+ /**
838
+ * An outer shadow drawn once on a surface of its own and composited: the
839
+ * shape's shadow, and where `box` is given the box cut out of it — so a box
840
+ * that shows what is behind it shows no shadow there, with the clip on the
841
+ * small surface rather than the window. False where there is no surface.
842
+ */
843
+ function bakedOuter(ctx, options, s, color, shape, around, box) {
844
+ if (!options.cached || !ctx.drawImage)
845
+ return false;
846
+ const reach = shadowReach(s.blur) + 1;
847
+ const x0 = Math.floor(shape.x) - reach;
848
+ const y0 = Math.floor(shape.y) - reach;
849
+ const w = Math.ceil(shape.x + shape.width) + reach - x0;
850
+ const h = Math.ceil(shape.y + shape.height) + reach - y0;
851
+ const sx = shape.x - x0;
852
+ const sy = shape.y - y0;
853
+ const cut = box && {
854
+ x: box.rect.x - x0,
855
+ y: box.rect.y - y0,
856
+ width: box.rect.width,
857
+ height: box.rect.height,
858
+ };
859
+ const key = [
860
+ 'shadow',
861
+ w,
862
+ h,
863
+ sx,
864
+ sy,
865
+ shape.width,
866
+ shape.height,
867
+ cornerKey(around),
868
+ s.blur,
869
+ color,
870
+ cut
871
+ ? `${cut.x},${cut.y},${cut.width},${cut.height},${cornerKey(box.corners)}`
872
+ : '',
873
+ ].join('|');
874
+ const image = options.cached(key, w, h, (sctx) => {
875
+ if (cut) {
876
+ sctx.save();
877
+ sctx.beginPath();
878
+ sctx.rect(0, 0, w, h);
879
+ roundedRect(sctx, cut.x, cut.y, cut.width, cut.height, box.corners, true, true);
880
+ sctx.clip();
881
+ }
882
+ fillShadow(sctx, s, color, (dx) => {
883
+ roundedRect(sctx, sx - dx, sy, shape.width, shape.height, around);
884
+ }, sx + shape.width + reach);
885
+ if (cut)
886
+ sctx.restore();
887
+ });
888
+ if (!image)
889
+ return false;
890
+ ctx.drawImage(image, x0, y0);
891
+ return true;
892
+ }
893
+ /**
894
+ * An inset shadow drawn once on a surface the size of the padding box and
895
+ * composited: a frame around the hole, whose shadow falls inside, clipped
896
+ * to the padding box's corners on the surface. False where there is none.
897
+ */
898
+ function bakedInset(ctx, options, s, color, pad, inner, hole, within) {
899
+ if (!options.cached || !ctx.drawImage)
900
+ return false;
901
+ const w = Math.round(pad.width);
902
+ const h = Math.round(pad.height);
903
+ const hx = hole.x - pad.x;
904
+ const hy = hole.y - pad.y;
905
+ const reach = shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1;
906
+ const key = [
907
+ 'inset',
908
+ w,
909
+ h,
910
+ cornerKey(inner),
911
+ hx,
912
+ hy,
913
+ hole.width,
914
+ hole.height,
915
+ cornerKey(within),
916
+ s.blur,
917
+ color,
918
+ ].join('|');
919
+ const image = options.cached(key, w, h, (sctx) => {
920
+ sctx.save();
921
+ sctx.beginPath();
922
+ roundedRect(sctx, 0, 0, w, h, inner);
923
+ sctx.clip();
924
+ fillShadow(sctx, s, color, (dx) => {
925
+ sctx.rect(-reach - dx, -reach, w + 2 * reach, h + 2 * reach);
926
+ if (hole.width > 0 && hole.height > 0) {
927
+ roundedRect(sctx, hx - dx, hy, hole.width, hole.height, within, true, true);
928
+ }
929
+ }, w + reach + shadowReach(s.blur));
930
+ sctx.restore();
931
+ });
932
+ if (!image)
933
+ return false;
934
+ ctx.drawImage(image, pad.x, pad.y);
935
+ return true;
936
+ }
937
+ /**
938
+ * Fill a shadow's shape: in its colour where it has no blur, and otherwise
939
+ * as the context's shadow of the shape drawn `dx` to the left — clear of
940
+ * the window, its right edge at `right` before the move — with the shadow
941
+ * offset back by as much, so that only the shadow lands.
942
+ */
943
+ function fillShadow(ctx, s, color, shape, right) {
944
+ if (!(s.blur > 0)) {
945
+ ctx.fillStyle = color;
946
+ ctx.beginPath();
947
+ shape(0);
948
+ ctx.fill();
949
+ return;
950
+ }
951
+ const dx = Math.ceil(right) + 1;
952
+ ctx.save();
953
+ ctx.shadowColor = color;
954
+ ctx.shadowBlur = s.blur;
955
+ ctx.shadowOffsetX = dx;
956
+ ctx.shadowOffsetY = 0;
957
+ ctx.fillStyle = '#000000';
958
+ ctx.beginPath();
959
+ shape(dx);
960
+ ctx.fill();
961
+ ctx.restore();
962
+ }
963
+ /**
964
+ * The band between two rounded rectangles, the inner inside the outer, in
965
+ * the colour already set. Where a side has no band the two edges meet, and
966
+ * they have to be drawn one way to cancel: two `roundRect`s under the
967
+ * even-odd rule, or two paths of curves, the inside run backwards under
968
+ * the non-zero rule — ntk leaves a hairline along a curve drawn twice the
969
+ * same way, and an arc beside a curve never quite meets it.
970
+ */
971
+ function fillRing(ctx, outer, outerCorners, inner, innerCorners) {
972
+ const curves = canCurve(ctx) &&
973
+ (!circlesFit(outerCorners, outer.width, outer.height) ||
974
+ !circlesFit(innerCorners, inner.width, inner.height));
975
+ ctx.beginPath();
976
+ roundedRect(ctx, outer.x, outer.y, outer.width, outer.height, outerCorners, curves);
977
+ if (inner.width > 0 && inner.height > 0) {
978
+ roundedRect(ctx, inner.x, inner.y, inner.width, inner.height, innerCorners, curves, true);
979
+ }
980
+ ctx.fill(curves ? 'nonzero' : 'evenodd');
981
+ }
982
+ /** The children of a box a paint may reach, in document order: where the
983
+ * box keeps a viewport index, those whose ink meets the damage — which
984
+ * leaves its positioned children out, for `positionedPaint` to give. */
985
+ function paintedChildren(box, options) {
986
+ // a flex box's in the order it laid them out
987
+ const ordered = PAINT_ORDER.get(box);
988
+ if (ordered)
989
+ return ordered;
176
990
  const damage = options.damage;
177
- if (box.paintIndex && damage) {
178
- for (const child of queryChildIndex(box.paintIndex, damage.y - options.originY, damage.y + damage.height - options.originY)) {
179
- paintBox(ctx, child, options);
991
+ if (!box.paintIndex || !damage)
992
+ return box.children;
993
+ return queryChildIndex(box.paintIndex, damage.y - options.originY, damage.y + damage.height - options.originY);
994
+ }
995
+ /** Whether a child is a plain block of its parent's flow, whose background
996
+ * goes with the flow's and whose lines with its lines: an in-flow block
997
+ * that clips nothing, is fully opaque and is no stacking context holding
998
+ * a negative `z-index`, in a parent that is no flex box, where an item is
999
+ * painted whole (CSS Flexbox 5.4). */
1000
+ function inFlow(parent, child, options) {
1001
+ if (child.kind !== 'block' || parent.kind === 'flex')
1002
+ return false;
1003
+ if (options.negative && NEGATIVE.has(child))
1004
+ return false;
1005
+ const style = child.style;
1006
+ return (style.overflowX === 'visible' &&
1007
+ style.overflowY === 'visible' &&
1008
+ opacityOf(child) >= 1 &&
1009
+ // containment makes it a stacking context, painted whole
1010
+ !contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
1011
+ }
1012
+ /** How opaque a box is drawn: its own `opacity`, and the one it takes from
1013
+ * an inline box it broke in pieces (`FADED_BLOCKS`). */
1014
+ function opacityOf(box) {
1015
+ const taken = FADED_BLOCKS.get(box);
1016
+ return taken === undefined ? box.style.opacity : box.style.opacity * taken;
1017
+ }
1018
+ /** Whether a child is a flex box of its parent's flow — or a grid — that
1019
+ * clips nothing and is no stacking context holding a negative `z-index`:
1020
+ * its background and borders go with the flow's backgrounds, in the
1021
+ * document's order, and its items with the flow's lines, each painted
1022
+ * whole as an inline block is (CSS 2.1 Appendix E, CSS Flexbox 5.4).
1023
+ * Painted whole in its place, its background covered a block after it
1024
+ * that a negative margin drew up over it. */
1025
+ function flowFlex(parent, child, options) {
1026
+ if (child.kind !== 'flex' || parent.kind === 'flex')
1027
+ return false;
1028
+ if (options.negative && NEGATIVE.has(child))
1029
+ return false;
1030
+ const style = child.style;
1031
+ return (style.overflowX === 'visible' &&
1032
+ style.overflowY === 'visible' &&
1033
+ !(child.outOfFlow && style.clip) &&
1034
+ !contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
1035
+ }
1036
+ /**
1037
+ * The first pass over a box's flow: each plain block's background and
1038
+ * borders, in document order and at any depth, while the floats met on the
1039
+ * way are kept for their own pass, and the positioned boxes are left to
1040
+ * the stacking context that paints them (`stackLayers`). A child that is
1041
+ * no plain block is left for the last pass, where it is painted whole
1042
+ * among the lines: its text is text, over every block background, and it
1043
+ * stands beside the floats rather than under them.
1044
+ */
1045
+ function paintFlowBackgrounds(ctx, box, options, floats) {
1046
+ for (const child of paintedChildren(box, options)) {
1047
+ if (child.kind === 'text' || child.kind === 'break')
1048
+ continue;
1049
+ if (layered(box, child) || onLine(box, child))
1050
+ continue;
1051
+ if (CLAMPED.has(child))
1052
+ continue;
1053
+ if (child.isFloat) {
1054
+ floats.push(child);
1055
+ continue;
1056
+ }
1057
+ if (flowFlex(box, child, options)) {
1058
+ if (child.style.visibility === 'visible' && intersects(child, options)) {
1059
+ paintOwnBackground(ctx, child, options);
1060
+ }
1061
+ continue;
1062
+ }
1063
+ if (!inFlow(box, child, options) || !intersects(child, options))
1064
+ continue;
1065
+ if (child.style.visibility === 'visible') {
1066
+ paintOwnBackground(ctx, child, options);
180
1067
  }
1068
+ paintFlowBackgrounds(ctx, child, options, floats);
181
1069
  }
182
- else {
183
- for (const child of box.children) {
184
- if (child.kind === 'text' || child.kind === 'break')
1070
+ }
1071
+ /** The last pass over a box's flow: the plain blocks' markers and lines,
1072
+ * and the children painted whole, in document order and at any depth,
1073
+ * over the floats. */
1074
+ function paintFlowLines(ctx, box, options, outlines) {
1075
+ for (const child of paintedChildren(box, options)) {
1076
+ if (child.kind === 'text' || child.kind === 'break')
1077
+ continue;
1078
+ if (layered(box, child) || onLine(box, child) || child.isFloat)
1079
+ continue;
1080
+ if (CLAMPED.has(child))
1081
+ continue;
1082
+ if (flowFlex(box, child, options)) {
1083
+ if (!intersects(child, options))
185
1084
  continue;
186
- if (child.outOfFlow)
1085
+ // its items, each whole, in `order` — one `float` makes no float of
1086
+ for (const item of paintedChildren(child, options)) {
1087
+ if (item.kind === 'text' || item.kind === 'break')
1088
+ continue;
1089
+ if (layered(child, item) || CLAMPED.has(item))
1090
+ continue;
1091
+ paintBox(ctx, item, options);
1092
+ }
1093
+ if (child.style.outlineStyle !== 'none')
1094
+ outlines.push(child);
1095
+ continue;
1096
+ }
1097
+ if (!inFlow(box, child, options)) {
1098
+ // a clipping table's captions were painted before its clip
1099
+ if (child.kind === 'table-caption' && clipsOverflow(box))
187
1100
  continue;
188
1101
  paintBox(ctx, child, options);
1102
+ continue;
1103
+ }
1104
+ if (!intersects(child, options))
1105
+ continue;
1106
+ if (child.marker && child.style.visibility === 'visible') {
1107
+ paintMarker(ctx, child.marker, options);
1108
+ }
1109
+ if (child.lines)
1110
+ paintLines(ctx, child, options);
1111
+ paintFlowLines(ctx, child, options, outlines);
1112
+ // an inline box's is drawn a fragment at a time, on its lines
1113
+ if (child.style.outlineStyle !== 'none' && child.kind !== 'inline') {
1114
+ outlines.push(child);
189
1115
  }
190
1116
  }
191
- if (box.lines && visible)
192
- paintLines(ctx, box, options);
193
- if (box.positionedPaint) {
194
- for (const child of box.positionedPaint)
195
- paintBox(ctx, child, options);
1117
+ }
1118
+ /** Where a box that clips its overflow clips it: on the pixels its own
1119
+ * background covers, inside its borders. Rounded out to whole pixels
1120
+ * instead, a box at a fractional position showed a row of what it clips
1121
+ * beyond its background's edge. */
1122
+ function overflowClip(box, options) {
1123
+ const left = box.x + options.originX;
1124
+ const top = frameY(box) + options.originY;
1125
+ const x = Math.round(left);
1126
+ const y = Math.round(top);
1127
+ return {
1128
+ x: x + box.borderLeft,
1129
+ y: y + box.borderTop,
1130
+ w: Math.round(left + box.width) - x - box.borderLeft - box.borderRight,
1131
+ h: Math.round(top + frameHeight(box)) - y - box.borderTop - box.borderBottom,
1132
+ };
1133
+ }
1134
+ /**
1135
+ * The edge a box that clips cuts what it holds at: its padding box where it
1136
+ * scrolls, and for `overflow: clip` and paint containment its overflow clip
1137
+ * edge — the box `overflow-clip-margin` names moved out by its length, or
1138
+ * in where that is negative (CSS Overflow 4, 3.2). Its corners are the
1139
+ * padding box's moved out as a spread moves them, which is what browsers
1140
+ * draw where the text measures from the border edge. Along an axis it lets
1141
+ * overflow show, nothing is cut.
1142
+ */
1143
+ function clipEdge(box, options) {
1144
+ const style = box.style;
1145
+ const painted = contained(box, CONTAIN_PAINT);
1146
+ if (scrolls(style) ||
1147
+ (!painted && style.overflowX !== 'clip' && style.overflowY !== 'clip')) {
1148
+ return { rect: overflowClip(box, options), radii: innerRadii(box) };
1149
+ }
1150
+ // From the padding box, as a browser draws it: out to the box the
1151
+ // margin is from and then by the margin, its rounded corners moving out
1152
+ // with it as a spread moves them
1153
+ const margin = style.overflowClipMargin * (options.scale ?? 1);
1154
+ const bt = box.borderTop;
1155
+ const br = box.borderRight;
1156
+ const bb = box.borderBottom;
1157
+ const bl = box.borderLeft;
1158
+ const [it, ir, ib, il] = edgeInsets(box, style.overflowClipBox);
1159
+ const t = bt - it + margin;
1160
+ const r = br - ir + margin;
1161
+ const b = bb - ib + margin;
1162
+ const l = bl - il + margin;
1163
+ const left = box.x + options.originX + bl;
1164
+ const top = frameY(box) + options.originY + bt;
1165
+ const width = box.width - bl - br;
1166
+ const height = frameHeight(box) - bt - bb;
1167
+ let x = Math.round(left - l);
1168
+ let y = Math.round(top - t);
1169
+ let w = Math.round(left + width + r) - x;
1170
+ let h = Math.round(top + height + b) - y;
1171
+ const corners = cornersOf(style, box.width, frameHeight(box));
1172
+ let radii = corners
1173
+ ? spreadCorners(insetCorners(corners, bt, br, bb, bl), width, height, t, r, b, l)
1174
+ : null;
1175
+ const openX = !painted && style.overflowX === 'visible';
1176
+ const openY = !painted && style.overflowY === 'visible';
1177
+ if (openX || openY) {
1178
+ // as far as the damage reaches, or the coordinates a clip can carry
1179
+ const damage = options.damage;
1180
+ radii = null;
1181
+ if (openX) {
1182
+ x = damage ? damage.x - CLAMP_PAD : -COORD_LIMIT;
1183
+ w = damage ? damage.width + 2 * CLAMP_PAD : 2 * COORD_LIMIT;
1184
+ }
1185
+ if (openY) {
1186
+ y = damage ? damage.y - CLAMP_PAD : -COORD_LIMIT;
1187
+ h = damage ? damage.height + 2 * CLAMP_PAD : 2 * COORD_LIMIT;
1188
+ }
1189
+ }
1190
+ return { rect: { x, y, w, h }, radii };
1191
+ }
1192
+ /**
1193
+ * The corners of a box `width` by `height` moved out by each side's
1194
+ * distance, or in where it is negative (`spreadRadius`) — an outer
1195
+ * shadow's spread, an overflow clip edge; a square corner stays square,
1196
+ * and so does one that comes to nothing either way. Null where none is
1197
+ * left rounded.
1198
+ */
1199
+ function spreadCorners(c, width, height, top, right, bottom, left) {
1200
+ const x = [0, 0, 0, 0];
1201
+ const y = [0, 0, 0, 0];
1202
+ let rounded = false;
1203
+ for (let i = 0; i < 4; i += 1) {
1204
+ if (!(c.x[i] > 0 && c.y[i] > 0))
1205
+ continue;
1206
+ const coverage = 2 * Math.min(c.x[i] / width, c.y[i] / height);
1207
+ const rx = spreadRadius(c.x[i], i === 0 || i === 3 ? left : right, coverage);
1208
+ const ry = spreadRadius(c.y[i], i < 2 ? top : bottom, coverage);
1209
+ if (!(rx > 0 && ry > 0))
1210
+ continue;
1211
+ x[i] = rx;
1212
+ y[i] = ry;
1213
+ rounded = true;
1214
+ }
1215
+ return rounded ? { x, y } : null;
1216
+ }
1217
+ /** How far in from a box's border box the box a `<visual-box>` names is:
1218
+ * top, right, bottom and left. */
1219
+ function edgeInsets(box, which) {
1220
+ if (which === 'border-box')
1221
+ return NO_INSETS;
1222
+ const t = box.borderTop;
1223
+ const r = box.borderRight;
1224
+ const b = box.borderBottom;
1225
+ const l = box.borderLeft;
1226
+ if (which === 'padding-box')
1227
+ return [t, r, b, l];
1228
+ return [
1229
+ t + (box.padTop ?? 0),
1230
+ r + (box.padRight ?? 0),
1231
+ b + (box.padBottom ?? 0),
1232
+ l + (box.padLeft ?? 0),
1233
+ ];
1234
+ }
1235
+ const NO_INSETS = [0, 0, 0, 0];
1236
+ /**
1237
+ * A positioned box, painted under the clips of the boxes its containing
1238
+ * block is inside and no others (CSS 2.1 11.1.1): a clip it escapes puts
1239
+ * it off until that clip ends. A fixed box escapes them all.
1240
+ */
1241
+ function paintPositioned(ctx, box, options) {
1242
+ const clips = options.clips;
1243
+ if (clips?.length && box.outOfFlow) {
1244
+ let escaped = clips.length;
1245
+ const containing = containingBlockOf(box);
1246
+ if (containing) {
1247
+ escaped = 0;
1248
+ for (let i = clips.length - 1; i >= 0; i -= 1) {
1249
+ if (holds(clips[i].box, containing))
1250
+ break;
1251
+ escaped += 1;
1252
+ }
1253
+ }
1254
+ if (escaped > 0) {
1255
+ clips[clips.length - escaped].deferred.push(box);
1256
+ return;
1257
+ }
1258
+ }
1259
+ paintBox(ctx, box, options);
1260
+ }
1261
+ /**
1262
+ * The box an out-of-flow box is positioned in, whose clips are the ones it
1263
+ * is under (`paintPositioned`, and the hit test's `deepestAt`): its nearest
1264
+ * positioned or translated ancestor, or the root. Null for a fixed box,
1265
+ * which is under none.
1266
+ */
1267
+ export function containingBlockOf(box) {
1268
+ if (box.style.position === 'fixed')
1269
+ return null;
1270
+ let containing = box.parent;
1271
+ while (containing?.parent &&
1272
+ containing.style.position === 'static' &&
1273
+ !containing.style.translate &&
1274
+ !containing.style.transformTranslate) {
1275
+ containing = containing.parent;
196
1276
  }
1277
+ return containing;
1278
+ }
1279
+ /** Whether anything in a box is positioned out of the flow, which may be
1280
+ * outside the box's clip; kept for the tree's life. */
1281
+ export function holdsAbsolute(box) {
1282
+ let holds = HOLDS_ABSOLUTE.get(box);
1283
+ if (holds === undefined) {
1284
+ holds = false;
1285
+ for (const child of box.children) {
1286
+ if (child.outOfFlow || holdsAbsolute(child)) {
1287
+ holds = true;
1288
+ break;
1289
+ }
1290
+ }
1291
+ HOLDS_ABSOLUTE.set(box, holds);
1292
+ }
1293
+ return holds;
1294
+ }
1295
+ const HOLDS_ABSOLUTE = new WeakMap();
1296
+ /** Whether `inner` is `outer` or inside it. */
1297
+ export function holds(outer, inner) {
1298
+ for (let at = inner; at; at = at.parent)
1299
+ if (at === outer)
1300
+ return true;
1301
+ return false;
1302
+ }
1303
+ /**
1304
+ * Whether a box clips its content: `overflow` other than `visible`, on a
1305
+ * block container (CSS 2.1 11.1.1) — not a table, a row or a row group.
1306
+ * The root element's `overflow` is the viewport's, and so is the
1307
+ * `<body>`'s where the root's is `visible`, an `<html>` the markup left out
1308
+ * among them; the viewport here is the element, which clips anyway.
1309
+ */
1310
+ export function clipsOverflow(box) {
1311
+ if (CLIPPED_CELLS.has(box))
1312
+ return true;
1313
+ const style = box.style;
1314
+ if (style.overflowX === 'visible' &&
1315
+ style.overflowY === 'visible' &&
1316
+ // paint containment clips as `overflow: clip` does (CSS Containment
1317
+ // 2, 3.5)
1318
+ !contained(box, CONTAIN_PAINT)) {
1319
+ return false;
1320
+ }
1321
+ const parent = box.parent;
1322
+ if (!parent)
1323
+ return false;
1324
+ switch (box.kind) {
1325
+ case 'block':
1326
+ case 'table-cell':
1327
+ case 'table-caption':
1328
+ case 'flex':
1329
+ // the table box, its captions outside it (the CSS 2.1 errata, 11.1.1)
1330
+ case 'table':
1331
+ break;
1332
+ default:
1333
+ return false;
1334
+ }
1335
+ const name = box.el?.name;
1336
+ if (name === 'html')
1337
+ return false;
1338
+ if (name === 'body') {
1339
+ if (parent.el?.name !== 'html')
1340
+ return !!parent.parent;
1341
+ // where the root's `overflow` is `visible`, the body's is the
1342
+ // viewport's — but not where either has any containment (CSS
1343
+ // Containment 2), which keeps it the body's own
1344
+ return (parent.style.overflowX !== 'visible' ||
1345
+ parent.style.overflowY !== 'visible' ||
1346
+ !!parent.style.contain ||
1347
+ !!box.style.contain);
1348
+ }
1349
+ return true;
1350
+ }
1351
+ /** A box's `clip` region, in window coordinates. */
1352
+ function clipOf(box, options) {
1353
+ const clip = box.style.clip;
1354
+ const left = clip.left ?? 0;
1355
+ const top = clip.top ?? 0;
1356
+ const right = clip.right ?? box.width;
1357
+ const bottom = clip.bottom ?? box.height;
1358
+ const x = Math.round(box.x + options.originX + left);
1359
+ const y = Math.round(box.y + options.originY + top);
1360
+ return {
1361
+ x,
1362
+ y,
1363
+ w: Math.round(box.x + options.originX + right) - x,
1364
+ h: Math.round(box.y + options.originY + bottom) - y,
1365
+ };
1366
+ }
1367
+ /**
1368
+ * The radii of a box's padding edge — its border radii less the borders —
1369
+ * for a clip, or null where a rectangle clips the same: padding at least a
1370
+ * corner's radius on both of its sides keeps the content box clear of the
1371
+ * corner, so only content overflowing it both ways at once could tell them
1372
+ * apart. A rounded clip is a mask the size of the window on X11, and a code
1373
+ * block — rounded, padded, `overflow: hidden` — had one made for every
1374
+ * paint.
1375
+ */
1376
+ function innerRadii(box) {
1377
+ const corners = cornersOf(box.style, box.width, frameHeight(box));
1378
+ if (!corners)
1379
+ return null;
1380
+ const inner = insetCorners(corners, box.borderTop, box.borderRight, box.borderBottom, box.borderLeft);
1381
+ const { x, y } = inner;
1382
+ if (box.padLeft >= x[0] &&
1383
+ box.padTop >= y[0] &&
1384
+ box.padRight >= x[1] &&
1385
+ box.padTop >= y[1] &&
1386
+ box.padRight >= x[2] &&
1387
+ box.padBottom >= y[2] &&
1388
+ box.padLeft >= x[3] &&
1389
+ box.padBottom >= y[3]) {
1390
+ return null;
1391
+ }
1392
+ return inner;
1393
+ }
1394
+ /** The corners of a box's content edge, inside its borders and padding;
1395
+ * null where every one of them is square. */
1396
+ function contentCorners(box) {
1397
+ const corners = cornersOf(box.style, box.width, frameHeight(box));
1398
+ if (!corners)
1399
+ return null;
1400
+ const inner = insetCorners(corners, box.borderTop + box.padTop, box.borderRight + box.padRight, box.borderBottom + box.padBottom, box.borderLeft + box.padLeft);
1401
+ for (let i = 0; i < 4; i += 1) {
1402
+ if (inner.x[i] > 0 && inner.y[i] > 0)
1403
+ return inner;
1404
+ }
1405
+ return null;
1406
+ }
1407
+ /**
1408
+ * The radii of a box's corners in pixels (CSS Backgrounds 3, 5.1): a
1409
+ * percentage is of the box's width across and of its height down, a corner
1410
+ * with either radius nought is square, and where the two on a side would
1411
+ * overlap all eight are reduced together (5.5) — which is what makes `50%`
1412
+ * an ellipse and `calc(infinity * 1px)` a pill. Null where every corner is
1413
+ * square, which is almost every box and the first thing asked.
1414
+ */
1415
+ function cornersOf(style, w, h) {
1416
+ const across = style.borderRadius;
1417
+ const down = style.borderRadiusY;
1418
+ if (down === null &&
1419
+ across[0] === 0 &&
1420
+ across[1] === 0 &&
1421
+ across[2] === 0 &&
1422
+ across[3] === 0) {
1423
+ return null;
1424
+ }
1425
+ const vertical = down ?? across;
1426
+ const x = [0, 0, 0, 0];
1427
+ const y = [0, 0, 0, 0];
1428
+ let rounded = false;
1429
+ for (let i = 0; i < 4; i += 1) {
1430
+ const rx = resolve(across[i], w);
1431
+ const ry = resolve(vertical[i], h);
1432
+ if (!(rx > 0 && ry > 0))
1433
+ continue;
1434
+ x[i] = rx;
1435
+ y[i] = ry;
1436
+ rounded = true;
1437
+ }
1438
+ if (!rounded)
1439
+ return null;
1440
+ let f = 1;
1441
+ const fit = (side, sum) => {
1442
+ if (sum > side)
1443
+ f = Math.min(f, Math.max(0, side) / sum);
1444
+ };
1445
+ fit(w, x[0] + x[1]);
1446
+ fit(h, y[1] + y[2]);
1447
+ fit(w, x[2] + x[3]);
1448
+ fit(h, y[3] + y[0]);
1449
+ if (f < 1) {
1450
+ for (let i = 0; i < 4; i += 1) {
1451
+ x[i] *= f;
1452
+ y[i] *= f;
1453
+ }
1454
+ }
1455
+ return { x, y };
1456
+ }
1457
+ /** The corners of an edge inside a box's outer one — its padding edge,
1458
+ * inside its borders: each radius less the width of the border across it
1459
+ * (CSS Backgrounds 3, 5.2), and square where that leaves none. */
1460
+ function insetCorners(c, top, right, bottom, left) {
1461
+ const x = [
1462
+ Math.max(0, c.x[0] - left),
1463
+ Math.max(0, c.x[1] - right),
1464
+ Math.max(0, c.x[2] - right),
1465
+ Math.max(0, c.x[3] - left),
1466
+ ];
1467
+ const y = [
1468
+ Math.max(0, c.y[0] - top),
1469
+ Math.max(0, c.y[1] - top),
1470
+ Math.max(0, c.y[2] - bottom),
1471
+ Math.max(0, c.y[3] - bottom),
1472
+ ];
1473
+ return { x, y };
1474
+ }
1475
+ /** How far along a quarter ellipse's radius its Bézier handles reach. */
1476
+ const KAPPA = 0.5522847498307936;
1477
+ /**
1478
+ * Whether `roundRect` draws these corners as CSS has them: every one a
1479
+ * circle, and none more than half the shorter side — the Cocoa context's
1480
+ * clamps each corner to that on its own, where CSS reduces them together.
1481
+ */
1482
+ function circlesFit(c, w, h) {
1483
+ const [a, b, d, e] = c.x;
1484
+ const [p, q, r, t] = c.y;
1485
+ return (a === p &&
1486
+ b === q &&
1487
+ d === r &&
1488
+ e === t &&
1489
+ Math.max(a, b, d, e) <= Math.min(w, h) / 2);
1490
+ }
1491
+ /** Whether the context builds a path of curves, as both backends' do. */
1492
+ function canCurve(ctx) {
1493
+ return !!(ctx.moveTo && ctx.lineTo && ctx.bezierCurveTo && ctx.closePath);
1494
+ }
1495
+ /**
1496
+ * A rectangle with rounded corners, added to the path being built as a
1497
+ * subpath of its own: `roundRect` where its circles fit, which ntk fills
1498
+ * quickest, and four curves where they do not — an ellipse, which the
1499
+ * Cocoa context's `roundRect` cannot draw at all. `curves` asks for the
1500
+ * curves regardless, and `reverse` has them run anticlockwise, which is
1501
+ * how a hole is cut in the subpath around it.
1502
+ */
1503
+ function roundedRect(ctx, x, y, w, h, c, curves = !circlesFit(c, w, h), reverse = false) {
1504
+ const [a, b, d, e] = c.x;
1505
+ const [p, q, r, t] = c.y;
1506
+ if (!curves || !canCurve(ctx)) {
1507
+ ctx.roundRect(x, y, w, h, [
1508
+ Math.min(a, p),
1509
+ Math.min(b, q),
1510
+ Math.min(d, r),
1511
+ Math.min(e, t),
1512
+ ]);
1513
+ return;
1514
+ }
1515
+ const k = 1 - KAPPA;
1516
+ const right = x + w;
1517
+ const bottom = y + h;
1518
+ const moveTo = ctx.moveTo.bind(ctx);
1519
+ const lineTo = ctx.lineTo.bind(ctx);
1520
+ const curveTo = ctx.bezierCurveTo.bind(ctx);
1521
+ if (!reverse) {
1522
+ moveTo(x + a, y);
1523
+ lineTo(right - b, y);
1524
+ curveTo(right - b * k, y, right, y + q * k, right, y + q);
1525
+ lineTo(right, bottom - r);
1526
+ curveTo(right, bottom - r * k, right - d * k, bottom, right - d, bottom);
1527
+ lineTo(x + e, bottom);
1528
+ curveTo(x + e * k, bottom, x, bottom - t * k, x, bottom - t);
1529
+ lineTo(x, y + p);
1530
+ curveTo(x, y + p * k, x + a * k, y, x + a, y);
1531
+ }
1532
+ else {
1533
+ moveTo(x + a, y);
1534
+ curveTo(x + a * k, y, x, y + p * k, x, y + p);
1535
+ lineTo(x, bottom - t);
1536
+ curveTo(x, bottom - t * k, x + e * k, bottom, x + e, bottom);
1537
+ lineTo(right - d, bottom);
1538
+ curveTo(right - d * k, bottom, right, bottom - r * k, right, bottom - r);
1539
+ lineTo(right, y + q);
1540
+ curveTo(right, y + q * k, right - b * k, y, right - b, y);
1541
+ }
1542
+ ctx.closePath();
1543
+ }
1544
+ /** Clip what follows to a rectangle, rounded where `radii` are: false
1545
+ * where the context cannot clip, and nothing was pushed. */
1546
+ function pushClip(ctx, rect, radii) {
1547
+ if (!ctx.beginPath || !ctx.rect || !ctx.clip)
1548
+ return false;
1549
+ ctx.save();
1550
+ ctx.beginPath();
1551
+ const w = Math.max(0, rect.w);
1552
+ const h = Math.max(0, rect.h);
1553
+ if (radii && ctx.roundRect)
1554
+ roundedRect(ctx, rect.x, rect.y, w, h, radii);
1555
+ else
1556
+ ctx.rect(rect.x, rect.y, w, h);
1557
+ ctx.clip();
1558
+ return true;
1559
+ }
1560
+ /**
1561
+ * Whether a child is painted with the positioned boxes, after the flow
1562
+ * rather than in it: an absolutely positioned box, and a relatively
1563
+ * positioned block, which CSS paints among them in document order (CSS 2.1
1564
+ * Appendix E) — a relative box after an absolute one covers it. An inline
1565
+ * or an inline-block is painted by its line.
1566
+ */
1567
+ function layered(parent, child) {
1568
+ if (child.outOfFlow)
1569
+ return true;
1570
+ const style = child.style;
1571
+ // a flex item with a `z-index` is a stacking context, positioned or not
1572
+ // (CSS Flexbox 5.4), and a grid's
1573
+ if (parent.kind === 'flex' && typeof style.zIndex === 'number')
1574
+ return true;
1575
+ if (style.position !== 'relative' &&
1576
+ style.position !== 'sticky' &&
1577
+ !style.translate &&
1578
+ !style.transformTranslate) {
1579
+ return false;
1580
+ }
1581
+ return child.kind !== 'inline' && !onLine(parent, child);
1582
+ }
1583
+ /**
1584
+ * Whether a child is painted by its parent's lines rather than as a child:
1585
+ * an inline-block or an image in a line of text is placed on the line, and
1586
+ * `paintLines` paints it there. Painted as a child as well, its text was
1587
+ * drawn twice — darker at every antialiased edge — and a translucent
1588
+ * background had its alpha doubled.
1589
+ */
1590
+ function onLine(parent, child) {
1591
+ if (parent.lines === null && parent.kind !== 'inline')
1592
+ return false;
1593
+ if (child.isFloat || child.outOfFlow)
1594
+ return false;
1595
+ return (child.kind !== 'inline' && child.kind !== 'text' && child.kind !== 'break');
1596
+ }
1597
+ /**
1598
+ * Per stacking context — the root, and a positioned box with a `z-index` —
1599
+ * the positioned boxes it paints over its flow (CSS 2.1 9.9.1, Appendix E,
1600
+ * steps 8 and 9): every one in it that no stacking context inside it
1601
+ * paints, at any depth, in document order and then by `z-index`. Painted
1602
+ * by their parent after its flow, as they were, the ones in a box painted
1603
+ * whole — one that clips, a table, a flex box, a float, a positioned box
1604
+ * with no `z-index` — were painted with it, among the flow around it: a
1605
+ * box absolute in an `overflow: hidden` one went under a positioned box
1606
+ * before it, and a menu with a `z-index` in a positioned header under the
1607
+ * positioned content after the header.
1608
+ */
1609
+ const STACKED = new WeakMap();
1610
+ /** Between a positioned box and the stacking context that paints it, the
1611
+ * boxes whose clips it is under (`clipsFor`), outermost first. */
1612
+ const CLIPS_BETWEEN = new WeakMap();
1613
+ const NO_CLIPS = [];
1614
+ /** Gather each stacking context's positioned boxes (`STACKED`); once per
1615
+ * layout, after `hoistNegative`, whose boxes it leaves where they are. */
1616
+ export function stackLayers(root) {
1617
+ const list = [];
1618
+ if (root.holdsLayers)
1619
+ gatherLayers(root, root, list);
1620
+ settleLayers(root, list);
1621
+ }
1622
+ function gatherLayers(box, context, into) {
1623
+ for (const child of PAINT_ORDER.get(box) ?? box.children) {
1624
+ if (child.kind === 'text' || child.kind === 'break')
1625
+ continue;
1626
+ // a positioned box past a clamp point, or in a box that is
1627
+ if (CLAMPED.has(child))
1628
+ continue;
1629
+ // one below the flow is on its stacking context's list already
1630
+ const below = HOISTED.has(child);
1631
+ if (below || layered(box, child)) {
1632
+ const clips = clipsFor(child, context);
1633
+ if (clips.length)
1634
+ CLIPS_BETWEEN.set(child, clips);
1635
+ else
1636
+ CLIPS_BETWEEN.delete(child);
1637
+ if (!below)
1638
+ into.push(child);
1639
+ }
1640
+ if (below || stacksLayers(child)) {
1641
+ const own = [];
1642
+ if (child.holdsLayers)
1643
+ gatherLayers(child, child, own);
1644
+ settleLayers(child, own);
1645
+ }
1646
+ else if (child.holdsLayers) {
1647
+ gatherLayers(child, context, into);
1648
+ }
1649
+ }
1650
+ }
1651
+ function settleLayers(box, list) {
1652
+ if (!list.length) {
1653
+ STACKED.delete(box);
1654
+ return;
1655
+ }
1656
+ // a stable sort: `z-index: auto` and 0 in document order, then the
1657
+ // positive ones, each in document order
1658
+ list.sort(byZIndex);
1659
+ STACKED.set(box, list);
1660
+ }
1661
+ /**
1662
+ * Whether a box is a stacking context, which paints the positioned boxes in
1663
+ * it itself, and the ones below its flow: positioned with a `z-index`, or a
1664
+ * flex item with one; fixed or sticky with none (CSS Positioned Layout 3, as
1665
+ * browsers paint them — a fixed header's box set behind its content with
1666
+ * `z-index: -1` went behind the page); or under full opacity (CSS Color 4
1667
+ * 3.2), its own or the opacity it takes from an inline box it broke
1668
+ * (`FADED_BLOCKS`), which is painted as one group — faded with it, or at
1669
+ * `opacity: 0` not at all, where its root context drew a hover menu's
1670
+ * absolute children.
1671
+ */
1672
+ function stacksLayers(box) {
1673
+ const style = box.style;
1674
+ if (style.position === 'fixed' || style.position === 'sticky')
1675
+ return true;
1676
+ if (style.opacity < 1 || FADED_BLOCKS.has(box))
1677
+ return true;
1678
+ // layout and paint containment make one (CSS Containment 2, 3.3, 3.5)
1679
+ if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT))
1680
+ return true;
1681
+ if (typeof style.zIndex !== 'number')
1682
+ return false;
1683
+ return style.position !== 'static' || flexItem(box);
1684
+ }
1685
+ /** Whether a box is an item of a flex box — or a grid, to the box tree the
1686
+ * same — which a `z-index` makes a stacking context unpositioned. */
1687
+ function flexItem(box) {
1688
+ return !box.outOfFlow && box.parent?.kind === 'flex';
1689
+ }
1690
+ /**
1691
+ * The boxes between a positioned box and its stacking context whose clips
1692
+ * it is under: the ones that clip their overflow, or are cut to a `clip`,
1693
+ * at or above its containing block (CSS 2.1 11.1.1). An absolute box is
1694
+ * outside a box that clips where its containing block is, a fixed one
1695
+ * outside every one, and a relative one inside every one.
1696
+ */
1697
+ function clipsFor(box, context) {
1698
+ let from = box.parent;
1699
+ if (box.outOfFlow) {
1700
+ const fixed = box.style.position === 'fixed';
1701
+ while (from && from !== context) {
1702
+ const style = from.style;
1703
+ if (style.translate || style.transformTranslate)
1704
+ break;
1705
+ if (!fixed && style.position !== 'static')
1706
+ break;
1707
+ from = from.parent;
1708
+ }
1709
+ }
1710
+ let clips = null;
1711
+ for (let at = from; at && at !== context; at = at.parent) {
1712
+ if (clipsOverflow(at) || (at.outOfFlow && at.style.clip)) {
1713
+ (clips ??= []).push(at);
1714
+ }
1715
+ }
1716
+ return clips ? clips.reverse() : NO_CLIPS;
1717
+ }
1718
+ /**
1719
+ * A positioned box its stacking context paints, under the clips of the
1720
+ * boxes between them that hold it (`clipsFor`): inside them, where its
1721
+ * parent painted it, it was. A clip of no area leaves none of it to paint.
1722
+ */
1723
+ function paintStacked(ctx, box, options) {
1724
+ if (!intersects(box, options))
1725
+ return;
1726
+ const between = CLIPS_BETWEEN.get(box);
1727
+ if (!between) {
1728
+ paintPositioned(ctx, box, options);
1729
+ return;
1730
+ }
1731
+ let pushed = 0;
1732
+ let empty = false;
1733
+ for (const clipper of between) {
1734
+ if (clipper.outOfFlow && clipper.style.clip) {
1735
+ const rect = clipOf(clipper, options);
1736
+ if (rect.w <= 0 || rect.h <= 0)
1737
+ empty = true;
1738
+ else if (pushClip(ctx, rect, null))
1739
+ pushed += 1;
1740
+ }
1741
+ if (!empty && clipsOverflow(clipper)) {
1742
+ const { rect, radii } = clipEdge(clipper, options);
1743
+ if (rect.w <= 0 || rect.h <= 0)
1744
+ empty = true;
1745
+ else if (pushClip(ctx, rect, radii))
1746
+ pushed += 1;
1747
+ }
1748
+ if (empty)
1749
+ break;
1750
+ }
1751
+ if (!empty)
1752
+ paintPositioned(ctx, box, options);
1753
+ for (let i = 0; i < pushed; i += 1)
1754
+ ctx.restore();
1755
+ }
1756
+ /** Per stacking context, its descendants with a negative `z-index`, in
1757
+ * paint order; and every box so placed, which its parent's positioned
1758
+ * children then leave out. Kept beside the boxes: few documents have one. */
1759
+ const NEGATIVE = new WeakMap();
1760
+ const HOISTED = new WeakSet();
1761
+ /**
1762
+ * Give each stacking context — the root, and a positioned box with a
1763
+ * `z-index` — the positioned descendants with a negative `z-index` it paints
1764
+ * below its flow (CSS 2.1 9.9.1, Appendix E). They were painted with the
1765
+ * rest of the positioned boxes, over the flow, so a box set behind the page
1766
+ * with `z-index: -1` covered what it was meant to be under. Asked only of a
1767
+ * tree that has one (`BoxTree.negative`).
1768
+ */
1769
+ export function hoistNegative(root) {
1770
+ hoistFrom(root, true);
1771
+ }
1772
+ function hoistFrom(box, root) {
1773
+ let pending = null;
1774
+ for (const child of box.children) {
1775
+ if (child.kind === 'text' || child.kind === 'break')
1776
+ continue;
1777
+ // the root element is the root stacking context, over its own borders
1778
+ const up = hoistFrom(child, root && child.el?.name === 'html');
1779
+ if (up)
1780
+ (pending ??= []).push(...up);
1781
+ const z = child.style.zIndex;
1782
+ if (typeof z === 'number' &&
1783
+ z < 0 &&
1784
+ (child.style.position !== 'static' || flexItem(child))) {
1785
+ (pending ??= []).push(child);
1786
+ }
1787
+ }
1788
+ if (!root && !stacksLayers(box))
1789
+ return pending;
1790
+ if (pending) {
1791
+ pending.sort(byZIndex);
1792
+ NEGATIVE.set(box, pending);
1793
+ for (const b of pending)
1794
+ HOISTED.add(b);
1795
+ }
1796
+ else {
1797
+ NEGATIVE.delete(box);
1798
+ }
1799
+ return null;
197
1800
  }
198
1801
  function byZIndex(a, b) {
199
1802
  const az = a.style.zIndex === 'auto' ? 0 : a.style.zIndex;
200
1803
  const bz = b.style.zIndex === 'auto' ? 0 : b.style.zIndex;
201
1804
  return az - bz;
202
1805
  }
203
- /** Whether anything this box or its descendants draw is in the damage. */
204
- function intersects(box, options) {
205
- const damage = options.damage;
206
- if (!damage)
207
- return true;
208
- const x = box.boundsX + options.originX;
209
- const y = box.boundsY + options.originY;
210
- return (x < damage.x + damage.width &&
211
- x + box.boundsWidth > damage.x &&
212
- y < damage.y + damage.height &&
213
- y + box.boundsHeight > damage.y);
1806
+ /** Whether anything this box or its descendants draw is in the damage. */
1807
+ function intersects(box, options) {
1808
+ const damage = options.damage;
1809
+ if (!damage)
1810
+ return true;
1811
+ const x = box.boundsX + options.originX;
1812
+ const y = box.boundsY + options.originY;
1813
+ return (x < damage.x + damage.width &&
1814
+ x + box.boundsWidth > damage.x &&
1815
+ y < damage.y + damage.height &&
1816
+ y + box.boundsHeight > damage.y);
1817
+ }
1818
+ /**
1819
+ * A fill rectangle clamped to the neighbourhood of the damage.
1820
+ *
1821
+ * Load-bearing, not an optimization: the X protocol carries a fill's
1822
+ * position as Int16 and its size as Uint16, so the background of a box
1823
+ * hundreds of thousands of pixels tall *thrown at the server whole* dies in
1824
+ * the request encoder — the pixels beyond the viewport were never going to
1825
+ * exist, but the numbers still had to fit. The margin keeps rounded corners
1826
+ * and antialiasing outside the damage honest; with no damage at all the
1827
+ * Int16 envelope itself is the clamp.
1828
+ */
1829
+ const CLAMP_PAD = 64;
1830
+ /** `clampRect` with a margin of its own, as a `Rect`. */
1831
+ function clampAround(options, r, pad) {
1832
+ const damage = options.damage;
1833
+ const x1 = Math.max(r.x, damage ? damage.x - pad : -COORD_LIMIT);
1834
+ const y1 = Math.max(r.y, damage ? damage.y - pad : -COORD_LIMIT);
1835
+ const x2 = Math.min(r.x + r.width, damage ? damage.x + damage.width + pad : COORD_LIMIT);
1836
+ const y2 = Math.min(r.y + r.height, damage ? damage.y + damage.height + pad : COORD_LIMIT);
1837
+ if (x2 <= x1 || y2 <= y1)
1838
+ return null;
1839
+ return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };
1840
+ }
1841
+ function clampRect(options, x, y, w, h) {
1842
+ const damage = options.damage;
1843
+ const left = damage ? damage.x - CLAMP_PAD : -COORD_LIMIT;
1844
+ const top = damage ? damage.y - CLAMP_PAD : -COORD_LIMIT;
1845
+ const right = damage ? damage.x + damage.width + CLAMP_PAD : COORD_LIMIT;
1846
+ const bottom = damage ? damage.y + damage.height + CLAMP_PAD : COORD_LIMIT;
1847
+ const x1 = Math.max(x, left);
1848
+ const y1 = Math.max(y, top);
1849
+ const x2 = Math.min(x + w, right);
1850
+ const y2 = Math.min(y + h, bottom);
1851
+ if (x2 <= x1 || y2 <= y1)
1852
+ return null;
1853
+ return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
1854
+ }
1855
+ function paintBackground(ctx, box, options, style) {
1856
+ const color = style.backgroundColor;
1857
+ const gradient = style.backgroundGradient;
1858
+ const solid = !isTransparent(color);
1859
+ if (!solid && !gradient)
1860
+ return;
1861
+ const area = clipArea(box, options, style, !!(ctx.roundRect && ctx.fill && ctx.beginPath));
1862
+ const rect = area && clampRect(options, area.x, area.y, area.w, area.h);
1863
+ if (!rect)
1864
+ return;
1865
+ const rounded = area.corners;
1866
+ const fill = () => {
1867
+ if (rounded) {
1868
+ // The clamp can only have cut edges further than CLAMP_PAD outside
1869
+ // the damage, and a sane radius is smaller than that — so a corner
1870
+ // that survives the cut is whole, and a cut edge is offscreen.
1871
+ ctx.beginPath();
1872
+ roundedRect(ctx, rect.x, rect.y, rect.w, rect.h, rounded);
1873
+ ctx.fill();
1874
+ }
1875
+ else
1876
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
1877
+ };
1878
+ if (solid) {
1879
+ ctx.fillStyle = inkColor(color, style.color);
1880
+ fill();
1881
+ }
1882
+ if (gradient && ctx.createLinearGradient) {
1883
+ // over the colour, as the layer an image is: the size of the padding
1884
+ // box, which is where it starts, and repeated under the borders — or
1885
+ // the viewport's, where it is fixed; its line runs across the whole of
1886
+ // it, not the part this paint reaches
1887
+ const fixed = style.backgroundAttachment === 'fixed' && options.canvas;
1888
+ const origin = fixed || originBox(box, options, style);
1889
+ const at = snapped(origin.x, origin.y, origin.width, origin.height);
1890
+ if (rounded && style.backgroundSize !== 'auto' && ctx.clip) {
1891
+ // tiles of the size it was given, cut to the rounded shape
1892
+ ctx.save();
1893
+ ctx.beginPath();
1894
+ roundedRect(ctx, rect.x, rect.y, rect.w, rect.h, rounded);
1895
+ ctx.clip();
1896
+ paintGradient(ctx, style, gradient, rect, at);
1897
+ ctx.restore();
1898
+ }
1899
+ else if (rounded) {
1900
+ // one fill in the rounded shape; under the borders it carries on
1901
+ // with its end colours where a browser would show the next tile
1902
+ if (!(at.width > 0 && at.height > 0))
1903
+ return;
1904
+ ctx.fillStyle = linearGradient(ctx, gradient, at.x, at.y, at.width, at.height, style.color, rect);
1905
+ fill();
1906
+ }
1907
+ else
1908
+ paintGradient(ctx, style, gradient, rect, at);
1909
+ }
1910
+ }
1911
+ /** A rectangle with each edge on the pixel it falls nearest. */
1912
+ function snapped(x, y, w, h) {
1913
+ const left = Math.round(x);
1914
+ const top = Math.round(y);
1915
+ return {
1916
+ x: left,
1917
+ y: top,
1918
+ width: Math.round(x + w) - left,
1919
+ height: Math.round(y + h) - top,
1920
+ };
1921
+ }
1922
+ /** A gradient's own size: none, and no ratio. */
1923
+ const NO_SIZE = { width: null, height: null, ratio: 0 };
1924
+ /**
1925
+ * A gradient layer across `area`: an image the size `background-size`
1926
+ * gives it, the size of `at` where that is `auto` (CSS Backgrounds 3, 3.9 —
1927
+ * an image with no size of its own is the size of its positioning area),
1928
+ * placed by `background-position` and repeated as `background-repeat`
1929
+ * says. Each tile is its own fill, so a tile's colours are the first's.
1930
+ */
1931
+ function paintGradient(ctx, style, gradient, area, at) {
1932
+ // a gradient has no size of its own: the positioning area's, unless
1933
+ // `background-size` gives it one, and then `background-position` places
1934
+ // it in the area as it does an image
1935
+ const repeat = style.backgroundRepeat;
1936
+ const [w, h] = roundedTile(style.backgroundSize, repeat, sizedTile(style.backgroundSize, NO_SIZE, at, 1), at);
1937
+ if (!(w > 0 && h > 0))
1938
+ return;
1939
+ const x0 = w === at.width
1940
+ ? at.x
1941
+ : at.x + resolve(style.backgroundPositionX, at.width - w);
1942
+ const y0 = h === at.height
1943
+ ? at.y
1944
+ : at.y + resolve(style.backgroundPositionY, at.height - h);
1945
+ const across = tileRun(repeat[0], x0, w, area.x, area.x + area.w, at.x, at.width);
1946
+ const down = tileRun(repeat[1], y0, h, area.y, area.y + area.h, at.y, at.height);
1947
+ const { from: fromX, to: toX, step: stepX } = across;
1948
+ const { from: fromY, to: toY, step: stepY } = down;
1949
+ if (Math.ceil((toX - fromX) / stepX) * Math.ceil((toY - fromY) / stepY) >
1950
+ MAX_TILES) {
1951
+ // a sliver of a root repeated down a long canvas: the one tile, and
1952
+ // its end colours on past it
1953
+ ctx.fillStyle = linearGradient(ctx, gradient, x0, y0, w, h, style.color, area);
1954
+ ctx.fillRect(area.x, area.y, area.w, area.h);
1955
+ return;
1956
+ }
1957
+ for (let y = fromY; y < toY; y += stepY) {
1958
+ const top = Math.max(y, area.y);
1959
+ const bottom = Math.min(y + h, area.y + area.h);
1960
+ if (bottom <= top)
1961
+ continue;
1962
+ for (let x = fromX; x < toX; x += stepX) {
1963
+ const left = Math.max(x, area.x);
1964
+ const right = Math.min(x + w, area.x + area.w);
1965
+ if (right <= left)
1966
+ continue;
1967
+ const tile = { x: left, y: top, w: right - left, h: bottom - top };
1968
+ ctx.fillStyle = linearGradient(ctx, gradient, x, y, w, h, style.color, tile);
1969
+ ctx.fillRect(tile.x, tile.y, tile.w, tile.h);
1970
+ }
1971
+ }
1972
+ }
1973
+ /**
1974
+ * A `linear-gradient()` across a box (CSS Images 3, 3.1.1): its line
1975
+ * through the box's centre at its angle, as long as the box is across at
1976
+ * that angle, so its ends are the colours at the corners that line points
1977
+ * to; a corner's angle is the one whose perpendicular runs through the
1978
+ * other two corners. A stop without a position is spread evenly between
1979
+ * its neighbours', and none is before the one before it. A stop off the
1980
+ * line lengthens it, so the colours at its ends are the ones between.
1981
+ */
1982
+ function linearGradient(ctx, gradient, x, y, w, h, currentColor, visible = null) {
1983
+ let angle = gradient.angle;
1984
+ if (gradient.corner) {
1985
+ const turn = Math.atan2(h, w);
1986
+ angle =
1987
+ gradient.corner === 'top right'
1988
+ ? turn
1989
+ : gradient.corner === 'bottom right'
1990
+ ? Math.PI - turn
1991
+ : gradient.corner === 'bottom left'
1992
+ ? Math.PI + turn
1993
+ : 2 * Math.PI - turn;
1994
+ }
1995
+ const dx = Math.sin(angle);
1996
+ const dy = -Math.cos(angle);
1997
+ const length = Math.abs(w * dx) + Math.abs(h * dy);
1998
+ const stops = gradient.stops;
1999
+ const at = stops.map((stop) => stop.at === null
2000
+ ? null
2001
+ : length > 0
2002
+ ? resolve(stop.at, length) / length
2003
+ : 0);
2004
+ if (at[0] === null)
2005
+ at[0] = 0;
2006
+ if (at[at.length - 1] === null)
2007
+ at[at.length - 1] = 1;
2008
+ for (let i = 1; i < at.length; i += 1) {
2009
+ if (at[i] === null) {
2010
+ // spread the ones without a position between the ones with
2011
+ let j = i;
2012
+ while (at[j] === null)
2013
+ j += 1;
2014
+ const from = at[i - 1];
2015
+ const to = at[j];
2016
+ for (let k = i; k < j; k += 1) {
2017
+ at[k] = from + ((to - from) * (k - i + 1)) / (j - i + 1);
2018
+ }
2019
+ }
2020
+ at[i] = Math.max(at[i], at[i - 1]);
2021
+ }
2022
+ // the context's offsets are 0 to 1, so a line that stops reach past runs
2023
+ // from the first to the last, and the box sees the part of it between
2024
+ const from = Math.min(0, at[0]);
2025
+ const to = Math.max(1, at[at.length - 1]);
2026
+ const startX = x + w / 2 - (dx * length) / 2;
2027
+ const startY = y + h / 2 - (dy * length) / 2;
2028
+ const x0 = startX + dx * length * from;
2029
+ const y0 = startY + dy * length * from;
2030
+ const x1 = startX + dx * length * to;
2031
+ const y1 = startY + dy * length * to;
2032
+ const colors = stops.map((stop) => inkColor(stop.color, currentColor));
2033
+ if (visible &&
2034
+ length > 0 &&
2035
+ !(Math.abs(x0) < GRADIENT_REACH &&
2036
+ Math.abs(y0) < GRADIENT_REACH &&
2037
+ Math.abs(x1) < GRADIENT_REACH &&
2038
+ Math.abs(y1) < GRADIENT_REACH)) {
2039
+ return clippedGradient(ctx, at, colors, startX, startY, dx * length, dy * length, visible);
2040
+ }
2041
+ const g = ctx.createLinearGradient(x0, y0, x1, y1);
2042
+ for (let i = 0; i < stops.length; i += 1) {
2043
+ g.addColorStop((at[i] - from) / (to - from), colors[i]);
2044
+ }
2045
+ return g;
2046
+ }
2047
+ /**
2048
+ * How far from the origin a gradient's line may end. X RENDER takes the
2049
+ * ends in 16.16 fixed point, and a pair past ±32,767 pixels threw from the
2050
+ * paint: a gradient down a document's long wrapper, scrolled far enough,
2051
+ * or a stop at `calc(1px / 0)`.
2052
+ */
2053
+ const GRADIENT_REACH = 16384;
2054
+ /**
2055
+ * A gradient's line cut to the part `visible` sees — the colours the rest
2056
+ * of it holds are never drawn — and moved along its perpendicular to run
2057
+ * through `visible`, which a linear gradient's colours do not change
2058
+ * along. `at` is where each stop is on the line from (`sx`, `sy`) that
2059
+ * runs (`lx`, `ly`) for 1; a stop before or past the part is its colour
2060
+ * there instead.
2061
+ */
2062
+ function clippedGradient(ctx, at, colors, sx, sy, lx, ly, visible) {
2063
+ const along = (px, py) => ((px - sx) * lx + (py - sy) * ly) / (lx * lx + ly * ly);
2064
+ // and no further out than a context draws
2065
+ const left = Math.max(visible.x, -GRADIENT_REACH);
2066
+ const top = Math.max(visible.y, -GRADIENT_REACH);
2067
+ const right = Math.max(left, Math.min(visible.x + visible.w, GRADIENT_REACH));
2068
+ const bottom = Math.max(top, Math.min(visible.y + visible.h, GRADIENT_REACH));
2069
+ let t0 = Infinity;
2070
+ let t1 = -Infinity;
2071
+ for (const [px, py] of [
2072
+ [left, top],
2073
+ [right, top],
2074
+ [left, bottom],
2075
+ [right, bottom],
2076
+ ]) {
2077
+ const t = along(px, py);
2078
+ t0 = Math.min(t0, t);
2079
+ t1 = Math.max(t1, t);
2080
+ }
2081
+ if (!(t1 > t0))
2082
+ t1 = t0 + 1e-6;
2083
+ const cx = (left + right) / 2;
2084
+ const cy = (top + bottom) / 2;
2085
+ const tc = along(cx, cy);
2086
+ const g = ctx.createLinearGradient(cx + (t0 - tc) * lx, cy + (t0 - tc) * ly, cx + (t1 - tc) * lx, cy + (t1 - tc) * ly);
2087
+ // the colour at a place on the line: a stop's, or between two
2088
+ const colorAt = (t) => {
2089
+ if (!(t > at[0]))
2090
+ return colors[0];
2091
+ for (let i = 1; i < at.length; i += 1) {
2092
+ if (t > at[i])
2093
+ continue;
2094
+ const span = at[i] - at[i - 1];
2095
+ const f = span > 0 ? (t - at[i - 1]) / span : 1;
2096
+ return (blend(colors[i - 1], colors[i], f) ??
2097
+ (f < 0.5 ? colors[i - 1] : colors[i]));
2098
+ }
2099
+ return colors[colors.length - 1];
2100
+ };
2101
+ g.addColorStop(0, colorAt(t0));
2102
+ for (let i = 0; i < at.length; i += 1) {
2103
+ if (at[i] > t0 && at[i] < t1) {
2104
+ g.addColorStop((at[i] - t0) / (t1 - t0), colors[i]);
2105
+ }
2106
+ }
2107
+ g.addColorStop(1, colorAt(t1));
2108
+ return g;
2109
+ }
2110
+ /**
2111
+ * The backgrounds of a table's column groups, columns, row groups and rows
2112
+ * (CSS 2.1 17.5.1), in that order: over the table's own and under its
2113
+ * cells', in the area of each cell in them. So the spacing between the
2114
+ * cells shows the table, and a row group with no cells in it shows nothing,
2115
+ * as neither laid its background over the whole of its box. An image is
2116
+ * placed against the part's own box and seen through its cells. A column is
2117
+ * laid out nowhere, so its box is the one its cells make: from the first of
2118
+ * its columns to the last, and from the table's first row to its last. And
2119
+ * where borders are separate a part's edges are its cells' border edges
2120
+ * (17.5.1), so a row's box runs from its first cell to its last, without
2121
+ * the spacing either side of them that the row's laid-out box holds.
2122
+ */
2123
+ function paintPartBackgrounds(ctx, table, options) {
2124
+ const grid = tableGrid(table);
2125
+ const { cells } = grid;
2126
+ const layers = [
2127
+ [grid.columnGroups, columnOf],
2128
+ [grid.columnBoxes, columnOf],
2129
+ [grid.groups, (cell) => cell.row],
2130
+ [grid.rows, (cell) => cell.row],
2131
+ ];
2132
+ // each part's box, and the cells' extent across the table, found once
2133
+ const areas = new Map();
2134
+ let across = null;
2135
+ for (const [layer, indexOf] of layers) {
2136
+ if (!layer.some(paintsPart))
2137
+ continue;
2138
+ const columns = indexOf === columnOf;
2139
+ for (const cell of cells) {
2140
+ const part = layer[indexOf(cell)];
2141
+ if (!part || !paintsPart(part) || hidden(cell.box))
2142
+ continue;
2143
+ const box = cell.box;
2144
+ const frame = {
2145
+ x: box.x,
2146
+ y: box.y,
2147
+ width: box.width,
2148
+ height: box.height,
2149
+ captionTop: 0,
2150
+ captionBottom: 0,
2151
+ borderTop: 0,
2152
+ borderRight: 0,
2153
+ borderBottom: 0,
2154
+ borderLeft: 0,
2155
+ style: part.style,
2156
+ };
2157
+ paintLayers(ctx, frame, options, (style) => {
2158
+ const left = box.x + options.originX;
2159
+ const top = box.y + options.originY;
2160
+ const area = clampRect(options, Math.round(left), Math.round(top), Math.round(left + box.width) - Math.round(left), Math.round(top + box.height) - Math.round(top));
2161
+ if (area) {
2162
+ let at = areas.get(part);
2163
+ if (!at) {
2164
+ at = columns
2165
+ ? columnBox(part, layer, grid.rows, cells, options)
2166
+ : paddingBox(part, options);
2167
+ if (!columns && table.style.borderCollapse !== 'collapse') {
2168
+ across ??= columnBox(null, null, grid.rows, cells, options);
2169
+ at = { ...at, x: across.x, width: across.width };
2170
+ }
2171
+ areas.set(part, at);
2172
+ }
2173
+ paintBackgroundImage(ctx, style, area, at, options);
2174
+ }
2175
+ });
2176
+ }
2177
+ }
2178
+ }
2179
+ const columnOf = (cell) => cell.column;
2180
+ /**
2181
+ * A cell `empty-cells: hide` leaves undrawn: one with no content, where
2182
+ * borders are separate — no background of its own or of its row, its
2183
+ * column or their groups, and no borders (CSS 2.1 17.6.1.1). A cell holds
2184
+ * content when anything in its flow does, an empty element or a float
2185
+ * among it, but not white space its `white-space` collapses away.
2186
+ */
2187
+ function hidden(cell) {
2188
+ if (cell.kind !== 'table-cell' || cell.style.emptyCells !== 'hide') {
2189
+ return false;
2190
+ }
2191
+ if (cell.bordersCollapsed)
2192
+ return false;
2193
+ for (const child of cell.children) {
2194
+ if (child.outOfFlow)
2195
+ continue;
2196
+ if (child.kind !== 'text' || !BLANK_TEXT.test(child.text))
2197
+ return false;
2198
+ const ws = child.style.whiteSpace;
2199
+ if (ws === 'pre' || ws === 'pre-wrap')
2200
+ return false;
2201
+ if (ws === 'pre-line' && /[\n\r]/.test(child.text))
2202
+ return false;
2203
+ }
2204
+ return true;
2205
+ }
2206
+ const BLANK_TEXT = /^[ \t\n\r\f]*$/;
2207
+ /** Whether a table part has a background to paint: a colour, or an image. */
2208
+ function paintsPart(box) {
2209
+ if (box === null || box.style.visibility !== 'visible')
2210
+ return false;
2211
+ return (!isTransparent(box.style.backgroundColor) ||
2212
+ !!box.style.backgroundImage ||
2213
+ !!box.style.backgroundImages);
2214
+ }
2215
+ /** The box a column or a column group would have, in window coordinates:
2216
+ * across its columns, as its cells lay them out, and down the table's
2217
+ * rows — or, with no part, across all of them. */
2218
+ function columnBox(part, layer, rows, cells, options) {
2219
+ let first = 0;
2220
+ let last = 0;
2221
+ if (part && layer) {
2222
+ first = layer.indexOf(part);
2223
+ last = layer.lastIndexOf(part);
2224
+ }
2225
+ else {
2226
+ for (const cell of cells) {
2227
+ last = Math.max(last, cell.column + cell.colSpan - 1);
2228
+ }
2229
+ }
2230
+ let left = Infinity;
2231
+ let right = -Infinity;
2232
+ for (const cell of cells) {
2233
+ if (cell.column === first)
2234
+ left = Math.min(left, cell.box.x);
2235
+ if (cell.column + cell.colSpan - 1 === last) {
2236
+ right = Math.max(right, cell.box.x + cell.box.width);
2237
+ }
2238
+ }
2239
+ const top = rows.length ? rows[0].y : 0;
2240
+ const end = rows.length ? rows[rows.length - 1] : null;
2241
+ const bottom = end ? end.y + end.height : 0;
2242
+ if (!(right > left))
2243
+ return { x: 0, y: 0, width: 0, height: 0 };
2244
+ return {
2245
+ x: left + options.originX,
2246
+ y: top + options.originY,
2247
+ width: right - left,
2248
+ height: bottom - top,
2249
+ };
2250
+ }
2251
+ /** How many tiles a repeating background may draw one by one, where the
2252
+ * context has no pattern to fill with. Past it, the image draws once. */
2253
+ const MAX_TILES = 4096;
2254
+ /**
2255
+ * A `background-image` (CSS 2.1 14.2.1): positioned in `at`, the padding
2256
+ * box — or the viewport, the element, for `background-attachment: fixed` —
2257
+ * repeated across `area`, the border box within the damage, over the
2258
+ * colour and under the borders. Filled with a pattern where the context has
2259
+ * one, drawn a tile at a time where it does not.
2260
+ */
2261
+ function paintBackgroundImage(ctx, style, area, at, options, corners = null) {
2262
+ if (style.backgroundAttachment === 'fixed' && options.canvas) {
2263
+ at = options.canvas;
2264
+ }
2265
+ const url = style.backgroundImage;
2266
+ const loaded = url ? options.backgroundImageFor?.(url) : null;
2267
+ if (!loaded)
2268
+ return;
2269
+ const svg = loaded.image instanceof SvgDrawing ? loaded.image : null;
2270
+ if (!svg && !ctx.drawImage)
2271
+ return;
2272
+ // an image pixel is a CSS pixel, and the box is device
2273
+ const scale = options.scale ?? 1;
2274
+ const repeat = style.backgroundRepeat;
2275
+ const [iw, ih] = roundedTile(style.backgroundSize, repeat, sizedTile(style.backgroundSize, loaded, at, scale), at);
2276
+ if (!(iw > 0 && ih > 0))
2277
+ return;
2278
+ const offset = (len, extent, size) => resolve(len, extent - size);
2279
+ const x0 = Math.round(at.x + offset(style.backgroundPositionX, at.width, iw));
2280
+ const y0 = Math.round(at.y + offset(style.backgroundPositionY, at.height, ih));
2281
+ // the tiles that reach the area: from the first at or before its edge
2282
+ const across = tileRun(repeat[0], x0, iw, area.x, area.x + area.w, at.x, at.width);
2283
+ const down = tileRun(repeat[1], y0, ih, area.y, area.y + area.h, at.y, at.height);
2284
+ const { from: fromX, to: toX, step: stepX } = across;
2285
+ const { from: fromY, to: toY, step: stepY } = down;
2286
+ ctx.save();
2287
+ if (ctx.beginPath && ctx.rect && ctx.clip) {
2288
+ ctx.beginPath();
2289
+ // in the box's corners, as its colour is (CSS Backgrounds 3, 5.3)
2290
+ if (corners && ctx.roundRect) {
2291
+ roundedRect(ctx, area.x, area.y, area.w, area.h, corners);
2292
+ }
2293
+ else
2294
+ ctx.rect(area.x, area.y, area.w, area.h);
2295
+ ctx.clip();
2296
+ }
2297
+ const tiles = Math.ceil((toX - fromX) / stepX) * Math.ceil((toY - fromY) / stepY);
2298
+ if (svg) {
2299
+ // a drawing is drawn a tile at a time, at the size it was given
2300
+ if (tiles <= MAX_TILES) {
2301
+ for (let y = fromY; y < toY; y += stepY) {
2302
+ for (let x = fromX; x < toX; x += stepX)
2303
+ svg.draw(ctx, Math.round(x), Math.round(y), iw, ih, scale);
2304
+ }
2305
+ }
2306
+ else {
2307
+ svg.draw(ctx, x0, y0, iw, ih, scale);
2308
+ }
2309
+ }
2310
+ else if (tiles > 1 &&
2311
+ iw === loaded.width &&
2312
+ ih === loaded.height &&
2313
+ stepX === iw &&
2314
+ stepY === ih &&
2315
+ ctx.createPattern &&
2316
+ ctx.translate) {
2317
+ // a pattern tiles from the origin of the space it is filled in, and
2318
+ // draws the image at its own size: a tile of a device pixel an image
2319
+ // pixel, which is 1x and `background-size` leaving it be
2320
+ ctx.fillStyle = ctx.createPattern(loaded.image, 'repeat');
2321
+ ctx.translate(x0, y0);
2322
+ ctx.fillRect(fromX - x0, fromY - y0, toX - fromX, toY - fromY);
2323
+ }
2324
+ else if (tiles <= MAX_TILES) {
2325
+ // each tile's edges on the pixels they fall nearest, so tiles `space`
2326
+ // sets apart or `round` sizes to a fraction meet without a seam
2327
+ for (let y = fromY; y < toY; y += stepY) {
2328
+ const top = Math.round(y);
2329
+ const height = Math.round(y + ih) - top;
2330
+ for (let x = fromX; x < toX; x += stepX) {
2331
+ const left = Math.round(x);
2332
+ ctx.drawImage(loaded.image, left, top, Math.round(x + iw) - left, height);
2333
+ }
2334
+ }
2335
+ }
2336
+ else {
2337
+ ctx.drawImage(loaded.image, x0, y0, iw, ih);
2338
+ }
2339
+ ctx.restore();
2340
+ }
2341
+ /**
2342
+ * Where a background's tiles fall along one axis (CSS Backgrounds 3, 3.4):
2343
+ * from the first at or before the painting area's start, a step apart, to
2344
+ * its end. `space` fits as many whole tiles into the positioning area as it
2345
+ * holds, the first and last against its edges and the rest spread evenly
2346
+ * between — or one, where two do not fit, placed as `background-position`
2347
+ * says — and `round`'s tile is already the size that fits (`roundedTile`).
2348
+ */
2349
+ function tileRun(mode, start, size, areaStart, areaEnd, originStart, originSize) {
2350
+ let step = size;
2351
+ if (mode === 'space') {
2352
+ const fits = Math.floor(originSize / size + 1e-6);
2353
+ if (fits < 2)
2354
+ return { from: start, step, to: start + size };
2355
+ step = size + (originSize - fits * size) / (fits - 1);
2356
+ start = originStart;
2357
+ }
2358
+ else if (mode === 'no-repeat') {
2359
+ return { from: start, step, to: start + size };
2360
+ }
2361
+ return {
2362
+ from: start - Math.ceil((start - areaStart) / step) * step,
2363
+ step,
2364
+ to: areaEnd,
2365
+ };
2366
+ }
2367
+ /**
2368
+ * A tile that `round` fits a whole number of times into the positioning
2369
+ * area along the axes it rounds (CSS Backgrounds 3, 3.9): the nearest whole
2370
+ * number, and one at least. Rounded one way only, with the size `auto` the
2371
+ * other way, the tile keeps its ratio.
2372
+ */
2373
+ function roundedTile(size, [modeX, modeY], [w, h], at) {
2374
+ const acrossRounds = modeX === 'round' && w > 0 && at.width > 0;
2375
+ const downRounds = modeY === 'round' && h > 0 && at.height > 0;
2376
+ if (!acrossRounds && !downRounds)
2377
+ return [w, h];
2378
+ const w2 = acrossRounds
2379
+ ? at.width / Math.max(1, Math.round(at.width / w))
2380
+ : w;
2381
+ const h2 = downRounds
2382
+ ? at.height / Math.max(1, Math.round(at.height / h))
2383
+ : h;
2384
+ const autoW = size === 'auto' || (typeof size !== 'string' && size[0] === 'auto');
2385
+ const autoH = size === 'auto' || (typeof size !== 'string' && size[1] === 'auto');
2386
+ if (acrossRounds && !downRounds && autoH)
2387
+ return [w2, (h * w2) / w];
2388
+ if (downRounds && !acrossRounds && autoW)
2389
+ return [(w * h2) / h, h2];
2390
+ return [w2, h2];
214
2391
  }
215
2392
  /**
216
- * A fill rectangle clamped to the neighbourhood of the damage.
217
- *
218
- * Load-bearing, not an optimization: the X protocol carries a fill's
219
- * position as Int16 and its size as Uint16, so the background of a box
220
- * hundreds of thousands of pixels tall *thrown at the server whole* dies in
221
- * the request encoder — the pixels beyond the viewport were never going to
222
- * exist, but the numbers still had to fit. The margin keeps rounded corners
223
- * and antialiasing outside the damage honest; with no damage at all the
224
- * Int16 envelope itself is the clamp.
2393
+ * A background tile's size (CSS Backgrounds 3, 3.9, over CSS Images' sizing
2394
+ * of an object): `auto` is the image's own, as `tileSize` has it; `cover`
2395
+ * and `contain` scale it to fill the positioning area or to fit inside it,
2396
+ * keeping its ratio, and one with no ratio is the size of the area; and a
2397
+ * width or a height alone takes the other from the ratio, or else from the
2398
+ * image's own size in it, or else from the area. Percentages are of the
2399
+ * area.
225
2400
  */
226
- const CLAMP_PAD = 64;
227
- function clampRect(options, x, y, w, h) {
228
- const damage = options.damage;
229
- const left = damage ? damage.x - CLAMP_PAD : -COORD_LIMIT;
230
- const top = damage ? damage.y - CLAMP_PAD : -COORD_LIMIT;
231
- const right = damage ? damage.x + damage.width + CLAMP_PAD : COORD_LIMIT;
232
- const bottom = damage ? damage.y + damage.height + CLAMP_PAD : COORD_LIMIT;
233
- const x1 = Math.max(x, left);
234
- const y1 = Math.max(y, top);
235
- const x2 = Math.min(x + w, right);
236
- const y2 = Math.min(y + h, bottom);
237
- if (x2 <= x1 || y2 <= y1)
238
- return null;
239
- return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
2401
+ function sizedTile(size, image, area, scale) {
2402
+ if (size === 'auto')
2403
+ return tileSize(image, area, scale);
2404
+ const { ratio } = image;
2405
+ if (size === 'cover' || size === 'contain') {
2406
+ if (!(ratio > 0 && area.height > 0))
2407
+ return [area.width, area.height];
2408
+ const wider = area.width / area.height > ratio;
2409
+ return (size === 'cover') === wider
2410
+ ? [area.width, area.width / ratio]
2411
+ : [area.height * ratio, area.height];
2412
+ }
2413
+ const [sw, sh] = size;
2414
+ const w = sw === 'auto' ? null : resolve(sw, area.width);
2415
+ const h = sh === 'auto' ? null : resolve(sh, area.height);
2416
+ if (w !== null && h !== null)
2417
+ return [w, h];
2418
+ if (w !== null) {
2419
+ if (ratio > 0)
2420
+ return [w, w / ratio];
2421
+ return [w, image.height === null ? area.height : image.height * scale];
2422
+ }
2423
+ if (h !== null) {
2424
+ if (ratio > 0)
2425
+ return [h * ratio, h];
2426
+ return [image.width === null ? area.width : image.width * scale, h];
2427
+ }
2428
+ return tileSize(image, area, scale);
240
2429
  }
241
- function paintBackground(ctx, box, options) {
242
- const color = box.style.backgroundColor;
243
- if (isTransparent(color))
244
- return;
245
- const rect = clampRect(options, Math.round(box.x + options.originX), Math.round(box.y + options.originY), Math.ceil(box.width), Math.ceil(box.height));
246
- if (!rect)
247
- return;
248
- ctx.fillStyle = inkColor(color, box.style.color);
249
- const radii = box.style.borderRadius;
250
- if (radii.some((r) => r > 0) && ctx.roundRect && ctx.fill && ctx.beginPath) {
251
- // The clamp can only have cut edges further than CLAMP_PAD outside the
252
- // damage, and a sane radius is smaller than that — so a corner that
253
- // survives the cut is whole, and a cut edge is offscreen.
254
- ctx.beginPath();
255
- ctx.roundRect(rect.x, rect.y, rect.w, rect.h, radii.slice());
256
- ctx.fill();
257
- return;
2430
+ /**
2431
+ * A background image's size, in device pixels, where nothing sets it — CSS
2432
+ * 2.1 has no `background-size` — which is CSS Images' default sizing: an
2433
+ * image's own size where it has one; the dimension it lacks from its ratio,
2434
+ * or else from the positioning area; and one with a ratio alone as large as
2435
+ * fits in the area. An SVG may be any of these, and a raster image is the
2436
+ * first.
2437
+ */
2438
+ function tileSize(size, area, scale) {
2439
+ const { ratio } = size;
2440
+ const width = size.width === null ? null : size.width * scale;
2441
+ const height = size.height === null ? null : size.height * scale;
2442
+ if (width !== null && height !== null)
2443
+ return [width, height];
2444
+ if (width !== null)
2445
+ return [width, ratio > 0 ? width / ratio : area.height];
2446
+ if (height !== null)
2447
+ return [ratio > 0 ? height * ratio : area.width, height];
2448
+ if (ratio > 0) {
2449
+ return area.width / area.height > ratio
2450
+ ? [area.height * ratio, area.height]
2451
+ : [area.width, area.width / ratio];
258
2452
  }
259
- ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
2453
+ return [area.width, area.height];
260
2454
  }
261
2455
  /**
262
2456
  * Borders, as four rectangles.
@@ -268,14 +2462,74 @@ function paintBackground(ctx, box, options) {
268
2462
  * which is right whenever the two sides share a colour and close enough when
269
2463
  * they do not.
270
2464
  */
2465
+ /**
2466
+ * A box's outline (CSS 2.1 18.4, CSS UI 4 5): a border of its own width,
2467
+ * style and colour round the border box grown by `outline-offset` — which
2468
+ * a negative offset brings inside it, as Tailwind UI's `-outline-offset-1`
2469
+ * frames an image with a hairline over its edge — with the box's corners
2470
+ * grown along with it, and taking no room. Drawn over the box's content,
2471
+ * which is where a browser draws it; `auto` is drawn solid.
2472
+ */
2473
+ function paintOutline(ctx, box, options) {
2474
+ const s = box.style;
2475
+ const width = s.outlineWidth;
2476
+ if (!(width > 0) || s.visibility !== 'visible')
2477
+ return;
2478
+ const grow = s.outlineOffset + width;
2479
+ const height = frameHeight(box);
2480
+ const w = box.width + 2 * grow;
2481
+ const h = height + 2 * grow;
2482
+ if (!(w > 0 && h > 0))
2483
+ return;
2484
+ const corners = cornersOf(s, box.width, height);
2485
+ const kind = s.outlineStyle === 'auto' ? 'solid' : s.outlineStyle;
2486
+ const grown = (r) => (r > 0 ? Math.max(0, r + grow) : 0);
2487
+ const ring = copyStyle(s);
2488
+ ring.borderTopStyle = kind;
2489
+ ring.borderRightStyle = kind;
2490
+ ring.borderBottomStyle = kind;
2491
+ ring.borderLeftStyle = kind;
2492
+ ring.borderTopColor = s.outlineColor;
2493
+ ring.borderRightColor = s.outlineColor;
2494
+ ring.borderBottomColor = s.outlineColor;
2495
+ ring.borderLeftColor = s.outlineColor;
2496
+ ring.borderRadius = corners
2497
+ ? corners.x.map(grown)
2498
+ : [0, 0, 0, 0];
2499
+ ring.borderRadiusY = corners
2500
+ ? corners.y.map(grown)
2501
+ : null;
2502
+ paintBorders(ctx, {
2503
+ x: box.x - grow,
2504
+ y: frameY(box) - grow,
2505
+ width: w,
2506
+ height: h,
2507
+ captionTop: 0,
2508
+ captionBottom: 0,
2509
+ borderTop: width,
2510
+ borderRight: width,
2511
+ borderBottom: width,
2512
+ borderLeft: width,
2513
+ style: ring,
2514
+ }, options);
2515
+ }
271
2516
  function paintBorders(ctx, box, options) {
2517
+ // most boxes have none, and are asked on every paint
2518
+ if (!(box.borderTop || box.borderRight || box.borderBottom || box.borderLeft))
2519
+ return;
272
2520
  const s = box.style;
273
- const x = Math.round(box.x + options.originX);
274
- const y = Math.round(box.y + options.originY);
275
- const w = Math.ceil(box.width);
276
- const h = Math.ceil(box.height);
2521
+ const left = box.x + options.originX;
2522
+ const top = frameY(box) + options.originY;
2523
+ const x = Math.round(left);
2524
+ const y = Math.round(top);
2525
+ const w = Math.round(left + box.width) - x;
2526
+ const h = Math.round(top + frameHeight(box)) - y;
277
2527
  if (w <= 0 || h <= 0)
278
2528
  return;
2529
+ if (roundedRing(ctx, box, x, y, w, h, options))
2530
+ return;
2531
+ if (sculpted(s) && paintSculpted(ctx, box, x, y, w, h, options))
2532
+ return;
279
2533
  const edge = (ex, ey, ew, eh, style, color, horizontal) => {
280
2534
  const rect = clampRect(options, ex, ey, ew, eh);
281
2535
  if (!rect)
@@ -298,6 +2552,444 @@ function paintBorders(ctx, box, options) {
298
2552
  edge(x + w - box.borderRight, y + box.borderTop, box.borderRight, h - box.borderTop - box.borderBottom, s.borderRightStyle, s.borderRightColor, false);
299
2553
  }
300
2554
  }
2555
+ /**
2556
+ * A box's border image in place of its border's style (CSS Backgrounds 3,
2557
+ * 6.2): the image cut into nine by its slices, drawn over the border image
2558
+ * area — the border box grown by the outset — in the nine parts the widths
2559
+ * make. The corners are scaled into theirs, the edges scaled to their
2560
+ * sides' widths and repeated along them as `border-image-repeat` says, and
2561
+ * the middle drawn only where `fill` asks for it. False where there is no
2562
+ * image to draw, or none here yet, and the border is drawn as its style
2563
+ * says.
2564
+ */
2565
+ function paintBorderImage(ctx, box, options) {
2566
+ const spec = box.style.borderImage;
2567
+ const source = spec.source;
2568
+ if (!source || !ctx.drawImage)
2569
+ return false;
2570
+ const loaded = typeof source === 'string' ? options.backgroundImageFor?.(source) : null;
2571
+ if (typeof source === 'string' && !loaded)
2572
+ return false;
2573
+ const scale = options.scale ?? 1;
2574
+ const borders = [
2575
+ box.borderTop,
2576
+ box.borderRight,
2577
+ box.borderBottom,
2578
+ box.borderLeft,
2579
+ ];
2580
+ const outset = spec.outset.map((o, i) => typeof o === 'number' ? o : o.times * borders[i]);
2581
+ const left = box.x + options.originX - outset[3];
2582
+ const top = frameY(box) + options.originY - outset[0];
2583
+ const aw = box.width + outset[1] + outset[3];
2584
+ const ah = frameHeight(box) + outset[0] + outset[2];
2585
+ // the image's size, CSS Images' default sizing in the area: a drawing
2586
+ // with no size of its own is as large as it fits there, and a gradient,
2587
+ // which has none, is the area's
2588
+ const [cw, ch] = loaded
2589
+ ? tileSize(loaded, { x: 0, y: 0, width: aw, height: ah }, scale)
2590
+ : [aw, ah];
2591
+ const iw = cw / scale;
2592
+ const ih = ch / scale;
2593
+ if (!(iw > 0 && ih > 0))
2594
+ return false;
2595
+ const key = typeof source === 'string' ? source : gradientKey(source, box.style.color);
2596
+ // A drawing or a gradient is drawn once at that size and cut as a raster
2597
+ // is, so that each piece stretches its part of the one picture
2598
+ let image = loaded?.image;
2599
+ let unitX = 1;
2600
+ let unitY = 1;
2601
+ if (!loaded || image instanceof SvgDrawing) {
2602
+ const svg = image instanceof SvgDrawing ? image : null;
2603
+ const w = Math.max(1, Math.round(cw));
2604
+ const h = Math.max(1, Math.round(ch));
2605
+ const drawn = options.cached?.(`border-image|${key}|${w}x${h}`, w, h, (sctx) => {
2606
+ if (svg)
2607
+ svg.draw(sctx, 0, 0, w, h, scale);
2608
+ else if (typeof source !== 'string' && sctx.createLinearGradient) {
2609
+ sctx.fillStyle = linearGradient(sctx, source, 0, 0, w, h, box.style.color);
2610
+ sctx.fillRect(0, 0, w, h);
2611
+ }
2612
+ });
2613
+ if (!drawn)
2614
+ return false;
2615
+ image = drawn;
2616
+ unitX = w / iw;
2617
+ unitY = h / ih;
2618
+ }
2619
+ // the slices, image pixels in from each edge, and none past the image
2620
+ const slices = spec.slice.map((s, i) => Math.min(i % 2 ? iw : ih, Math.max(0, resolve(s, i % 2 ? iw : ih))));
2621
+ const widths = spec.width.map((w, i) => {
2622
+ if (w === 'auto')
2623
+ return slices[i] * scale;
2624
+ if (typeof w === 'number')
2625
+ return w;
2626
+ if ('times' in w)
2627
+ return w.times * borders[i];
2628
+ return resolve(w, i % 2 ? aw : ah);
2629
+ });
2630
+ // widths that overlap across the area are scaled down together
2631
+ const f = Math.min(aw / (widths[1] + widths[3]), ah / (widths[0] + widths[2]));
2632
+ if (f < 1)
2633
+ for (let i = 0; i < 4; i += 1)
2634
+ widths[i] *= f;
2635
+ const [st, sr, sb, sl] = slices;
2636
+ const [wt, wr, wb, wl] = widths;
2637
+ // Each column and row of the image, as from and to: the slices may
2638
+ // overlap, and each corner is still all of its own; the edges and the
2639
+ // middle between two that meet or cross are empty (6.2)
2640
+ const sx = [
2641
+ [0, sl],
2642
+ [sl, Math.max(sl, iw - sr)],
2643
+ [iw - sr, iw],
2644
+ ];
2645
+ const sy = [
2646
+ [0, st],
2647
+ [st, Math.max(st, ih - sb)],
2648
+ [ih - sb, ih],
2649
+ ];
2650
+ const dx = [
2651
+ Math.round(left),
2652
+ Math.round(left + wl),
2653
+ Math.round(left + aw - wr),
2654
+ Math.round(left + aw),
2655
+ ];
2656
+ const dy = [
2657
+ Math.round(top),
2658
+ Math.round(top + wt),
2659
+ Math.round(top + ah - wb),
2660
+ Math.round(top + ah),
2661
+ ];
2662
+ const sized = `${Math.round(cw)}x${Math.round(ch)}`;
2663
+ const part = (col, row, across, down) => {
2664
+ // the piece, whole pixels of the image
2665
+ const px = Math.round(sx[col][0] * unitX);
2666
+ const py = Math.round(sy[row][0] * unitY);
2667
+ const pw = Math.round(sx[col][1] * unitX) - px;
2668
+ const ph = Math.round(sy[row][1] * unitY) - py;
2669
+ const w = dx[col + 1] - dx[col];
2670
+ const h = dy[row + 1] - dy[row];
2671
+ if (!(pw > 0 && ph > 0 && w > 0 && h > 0))
2672
+ return;
2673
+ const [modeX, modeY] = spec.repeat;
2674
+ const runX = edgeRun(col === 1 ? modeX : 'stretch', dx[col], w, across);
2675
+ const runY = edgeRun(row === 1 ? modeY : 'stretch', dy[row], h, down);
2676
+ // A piece drawn at another size is filtered, and the filter reads past
2677
+ // the piece's edge into its neighbours in the image: the middle's
2678
+ // colour bled into every edge. Copied out to a surface of its own, a
2679
+ // piece is padded at its own edge, as a browser draws it.
2680
+ let source = image;
2681
+ let ox = px;
2682
+ let oy = py;
2683
+ const scaled = runX.some(([a, b, c0, c1]) => b - a !== (c1 - c0) * pw) ||
2684
+ runY.some(([a, b, c0, c1]) => b - a !== (c1 - c0) * ph);
2685
+ const piece = scaled &&
2686
+ options.cached?.(`border-image|${key}|${sized}|${px},${py},${pw},${ph}`, pw, ph, (sctx) => sctx.drawImage(image, px, py, pw, ph, 0, 0, pw, ph));
2687
+ if (piece) {
2688
+ source = piece;
2689
+ ox = 0;
2690
+ oy = 0;
2691
+ }
2692
+ for (const [x0, x1, cx0, cx1] of runX) {
2693
+ for (const [y0, y1, cy0, cy1] of runY) {
2694
+ ctx.drawImage(source, ox + cx0 * pw, oy + cy0 * ph, (cx1 - cx0) * pw, (cy1 - cy0) * ph, x0, y0, x1 - x0, y1 - y0);
2695
+ }
2696
+ }
2697
+ };
2698
+ // Along an edge, a tile is the slice scaled to the side's width; the
2699
+ // middle's is scaled as the top edge is across and the left edge is
2700
+ // down, or the bottom and the right, or not at all
2701
+ const factor = (d, s) => (s > 0 && d > 0 ? d / s : 0);
2702
+ const top0 = factor(dy[1] - dy[0], st) || factor(dy[3] - dy[2], sb) || scale;
2703
+ const left0 = factor(dx[1] - dx[0], sl) || factor(dx[3] - dx[2], sr) || scale;
2704
+ const middleW = (sx[1][1] - sx[1][0]) * top0;
2705
+ const middleH = (sy[1][1] - sy[1][0]) * left0;
2706
+ part(0, 0, 0, 0);
2707
+ part(2, 0, 0, 0);
2708
+ part(0, 2, 0, 0);
2709
+ part(2, 2, 0, 0);
2710
+ part(1, 0, (sx[1][1] - sx[1][0]) * factor(dy[1] - dy[0], st), 0);
2711
+ part(1, 2, (sx[1][1] - sx[1][0]) * factor(dy[3] - dy[2], sb), 0);
2712
+ part(0, 1, 0, (sy[1][1] - sy[1][0]) * factor(dx[1] - dx[0], sl));
2713
+ part(2, 1, 0, (sy[1][1] - sy[1][0]) * factor(dx[3] - dx[2], sr));
2714
+ if (spec.fill)
2715
+ part(1, 1, middleW, middleH);
2716
+ return true;
2717
+ }
2718
+ const GRADIENT_KEYS = new WeakMap();
2719
+ /** A gradient as a key for what is drawn of it: its angle and stops, and
2720
+ * the colour `currentColor` among them is. */
2721
+ function gradientKey(gradient, color) {
2722
+ let key = GRADIENT_KEYS.get(gradient);
2723
+ if (key === undefined) {
2724
+ key = JSON.stringify(gradient);
2725
+ GRADIENT_KEYS.set(gradient, key);
2726
+ }
2727
+ return `${key}|${color}`;
2728
+ }
2729
+ /**
2730
+ * The tiles of a border image part along one axis, each as where it is
2731
+ * drawn — its start and end, whole pixels — and the fraction of the slice
2732
+ * it shows, which is all of it but where the part cuts a tile. `stretch`
2733
+ * is one tile over the part; `repeat` tiles of `tile`'s size centred on it;
2734
+ * `round` as many as fit, the nearest whole number, sized to fill it; and
2735
+ * `space` as many whole ones as fit, the room left spread around them
2736
+ * (CSS Backgrounds 3, 6.5). A tile of no size is the part's.
2737
+ */
2738
+ function edgeRun(mode, start, length, tile) {
2739
+ if (mode === 'stretch' || !(tile > 0))
2740
+ return [[start, start + length, 0, 1]];
2741
+ const end = start + length;
2742
+ const tiles = [];
2743
+ let step = tile;
2744
+ let from;
2745
+ if (mode === 'round') {
2746
+ step = tile = length / Math.max(1, Math.round(length / tile));
2747
+ from = start;
2748
+ }
2749
+ else if (mode === 'space') {
2750
+ const fits = Math.floor(length / tile + 1e-6);
2751
+ if (!fits)
2752
+ return tiles;
2753
+ const gap = (length - fits * tile) / (fits + 1);
2754
+ step = tile + gap;
2755
+ from = start + gap;
2756
+ }
2757
+ else {
2758
+ // centred: the first tile at or before the start
2759
+ from = start + (length - tile) / 2;
2760
+ from -= Math.ceil((from - start) / tile) * tile;
2761
+ }
2762
+ for (let at = from; at < end - 1e-6 && tiles.length < MAX_TILES; at += step) {
2763
+ const a = Math.max(at, start);
2764
+ const b = Math.min(at + tile, end);
2765
+ const x0 = Math.round(a);
2766
+ const x1 = Math.round(b);
2767
+ if (x1 <= x0)
2768
+ continue;
2769
+ tiles.push([x0, x1, (a - at) / tile, (b - at) / tile]);
2770
+ }
2771
+ return tiles;
2772
+ }
2773
+ /**
2774
+ * A rounded box's border as the ring between its border edge and its
2775
+ * padding edge, each rounded, where every side that has a border has one
2776
+ * of the same colour and a solid rule: a card's or a button's. Drawn
2777
+ * straight, its corners were square over the background's rounded ones,
2778
+ * and an accent border down one side did not follow the corner. The inner
2779
+ * radius is the outer less the wider border at that corner (CSS
2780
+ * Backgrounds 3, 5.2). False where the border is not such a one, and it is
2781
+ * drawn a side at a time.
2782
+ */
2783
+ function roundedRing(ctx, box, x, y, w, h, options) {
2784
+ const s = box.style;
2785
+ if (!ctx.roundRect || !ctx.fill || !ctx.beginPath)
2786
+ return false;
2787
+ const corners = cornersOf(s, w, h);
2788
+ if (!corners)
2789
+ return false;
2790
+ const sides = [
2791
+ [box.borderTop, s.borderTopColor, s.borderTopStyle],
2792
+ [box.borderRight, s.borderRightColor, s.borderRightStyle],
2793
+ [box.borderBottom, s.borderBottomColor, s.borderBottomStyle],
2794
+ [box.borderLeft, s.borderLeftColor, s.borderLeftStyle],
2795
+ ];
2796
+ let color = null;
2797
+ for (const [width, ink, style] of sides) {
2798
+ if (!width)
2799
+ continue;
2800
+ if (style !== 'solid' || (color !== null && ink !== color))
2801
+ return false;
2802
+ color = ink;
2803
+ }
2804
+ if (color === null)
2805
+ return true;
2806
+ if (isTransparent(color))
2807
+ return true;
2808
+ const rect = clampRect(options, x, y, w, h);
2809
+ if (!rect)
2810
+ return true;
2811
+ const top = box.borderTop;
2812
+ const right = box.borderRight;
2813
+ const bottom = box.borderBottom;
2814
+ const left = box.borderLeft;
2815
+ ctx.fillStyle = inkColor(color, s.color);
2816
+ fillRing(ctx, { x: rect.x, y: rect.y, width: rect.w, height: rect.h }, corners, {
2817
+ x: rect.x + left,
2818
+ y: rect.y + top,
2819
+ width: Math.max(0, rect.w - left - right),
2820
+ height: Math.max(0, rect.h - top - bottom),
2821
+ }, insetCorners(corners, top, right, bottom, left));
2822
+ return true;
2823
+ }
2824
+ /** The border styles drawn in two shades, as though lit from the top left. */
2825
+ const SCULPTED = new Set(['groove', 'ridge', 'inset', 'outset']);
2826
+ function sculpted(s) {
2827
+ return (SCULPTED.has(s.borderTopStyle) ||
2828
+ SCULPTED.has(s.borderRightStyle) ||
2829
+ SCULPTED.has(s.borderBottomStyle) ||
2830
+ SCULPTED.has(s.borderLeftStyle));
2831
+ }
2832
+ /**
2833
+ * A border with a 3D style in it, a side at a time as a trapezoid whose
2834
+ * ends meet its neighbours' on the diagonal, so a corner is shared rather
2835
+ * than stacked (CSS 2.1 8.5.3). `inset` shades its top and left in the
2836
+ * colour's shadow and `outset` its bottom and right; `groove` is `inset`
2837
+ * outside `outset`, a band each, and `ridge` the other way round
2838
+ * (`borderShades`). A side of any other style takes its trapezoid in its
2839
+ * colour. The trapezoids are clamped to the painted area, which leaves a
2840
+ * cut end square and out of sight and keeps a long box's far corners out
2841
+ * of X's coordinates. False where the context cannot draw a path, and the
2842
+ * sides are drawn straight.
2843
+ */
2844
+ function paintSculpted(ctx, box, x, y, w, h, options) {
2845
+ if (!ctx.beginPath || !ctx.moveTo || !ctx.lineTo || !ctx.fill)
2846
+ return false;
2847
+ const area = clampRect(options, x, y, w, h);
2848
+ if (!area)
2849
+ return true;
2850
+ const s = box.style;
2851
+ const t = box.borderTop;
2852
+ const r = box.borderRight;
2853
+ const b = box.borderBottom;
2854
+ const l = box.borderLeft;
2855
+ const cx = (v) => Math.max(area.x - 1, Math.min(v, area.x + area.w + 1));
2856
+ const cy = (v) => Math.max(area.y - 1, Math.min(v, area.y + area.h + 1));
2857
+ /** The band between the edge `outer` of the way in and `inner`, for one
2858
+ * side: 0 is the border box, 1 the padding box. */
2859
+ const band = (side, outer, inner, color) => {
2860
+ const at = (k) => ({
2861
+ x0: x + l * k,
2862
+ y0: y + t * k,
2863
+ x1: x + w - r * k,
2864
+ y1: y + h - b * k,
2865
+ });
2866
+ const o = at(outer);
2867
+ const i = at(inner);
2868
+ const points = side === 0
2869
+ ? [o.x0, o.y0, o.x1, o.y0, i.x1, i.y0, i.x0, i.y0]
2870
+ : side === 1
2871
+ ? [o.x1, o.y0, o.x1, o.y1, i.x1, i.y1, i.x1, i.y0]
2872
+ : side === 2
2873
+ ? [o.x1, o.y1, o.x0, o.y1, i.x0, i.y1, i.x1, i.y1]
2874
+ : [o.x0, o.y1, o.x0, o.y0, i.x0, i.y0, i.x0, i.y1];
2875
+ ctx.fillStyle = color;
2876
+ ctx.beginPath();
2877
+ ctx.moveTo(cx(points[0]), cy(points[1]));
2878
+ for (let k = 2; k < 8; k += 2)
2879
+ ctx.lineTo(cx(points[k]), cy(points[k + 1]));
2880
+ ctx.closePath?.();
2881
+ ctx.fill();
2882
+ };
2883
+ const sides = [
2884
+ [t, s.borderTopStyle, s.borderTopColor],
2885
+ [r, s.borderRightStyle, s.borderRightColor],
2886
+ [b, s.borderBottomStyle, s.borderBottomColor],
2887
+ [l, s.borderLeftStyle, s.borderLeftColor],
2888
+ ];
2889
+ for (let side = 0; side < 4; side += 1) {
2890
+ const [width, style, raw] = sides[side];
2891
+ if (!(width > 0) || style === 'none' || style === 'hidden')
2892
+ continue;
2893
+ if (isTransparent(raw))
2894
+ continue;
2895
+ const color = inkColor(raw, s.color);
2896
+ if (!SCULPTED.has(style)) {
2897
+ band(side, 0, 1, color);
2898
+ continue;
2899
+ }
2900
+ const shades = borderShades(color) ?? { lit: color, shadowed: color };
2901
+ // the top and the left face the light
2902
+ const lit = side === 0 || side === 3;
2903
+ const shade = (sunk) => sunk === lit ? shades.shadowed : shades.lit;
2904
+ if (style === 'inset' || style === 'outset') {
2905
+ band(side, 0, 1, shade(style === 'inset'));
2906
+ }
2907
+ else {
2908
+ band(side, 0, 0.5, shade(style === 'groove'));
2909
+ band(side, 0.5, 1, shade(style !== 'groove'));
2910
+ }
2911
+ }
2912
+ return true;
2913
+ }
2914
+ /**
2915
+ * A table's collapsed borders: one per segment of its grid, centred on the
2916
+ * line, reaching across the borders it meets at either end. The winners
2917
+ * are painted last, so where two cross, the corner is the one that won
2918
+ * (CSS 2.1 17.6.2.1). Painted over the cells, as the table's borders are.
2919
+ */
2920
+ function paintCollapsedBorders(ctx, table, options) {
2921
+ const grid = table.collapsed;
2922
+ const { rows: R, columns: C, lineX, lineY, horizontal, vertical } = grid;
2923
+ if (lineX.length !== C + 1 || lineY.length !== R + 1)
2924
+ return;
2925
+ const ox = table.x + options.originX;
2926
+ const oy = table.y + options.originY;
2927
+ const widthOf = (b) => (b ? b.width : 0);
2928
+ const h = (line, c) => c < 0 || c >= C ? 0 : widthOf(horizontal[line * C + c]);
2929
+ const v = (r, line) => r < 0 || r >= R ? 0 : widthOf(vertical[r * (C + 1) + line]);
2930
+ const segments = [];
2931
+ // A border centred on a grid line starts half its width before it,
2932
+ // rounded there, on the page: a line at 12.5 carries a 25px border from
2933
+ // 0, where rounding the line first and taking a whole half off drew it
2934
+ // from 1, and a line 18.4 into a table at 50.4 carries a pixel's from
2935
+ // 69, where rounding the two apart drew it from 68
2936
+ const from = (origin, line, width) => Math.round(origin + line - width / 2);
2937
+ for (let line = 0; line <= R; line += 1) {
2938
+ for (let c = 0; c < C; c += 1) {
2939
+ const border = horizontal[line * C + c];
2940
+ if (!border)
2941
+ continue;
2942
+ const start = Math.max(v(line - 1, c), v(line, c));
2943
+ const end = Math.max(v(line - 1, c + 1), v(line, c + 1));
2944
+ const x = from(ox, lineX[c], start);
2945
+ segments.push({
2946
+ border,
2947
+ x,
2948
+ y: from(oy, lineY[line], border.width),
2949
+ w: from(ox, lineX[c + 1], end) + end - x,
2950
+ h: border.width,
2951
+ horizontal: true,
2952
+ row: line,
2953
+ column: c,
2954
+ });
2955
+ }
2956
+ }
2957
+ for (let r = 0; r < R; r += 1) {
2958
+ for (let line = 0; line <= C; line += 1) {
2959
+ const border = vertical[r * (C + 1) + line];
2960
+ if (!border)
2961
+ continue;
2962
+ const start = Math.max(h(r, line - 1), h(r, line));
2963
+ const end = Math.max(h(r + 1, line - 1), h(r + 1, line));
2964
+ const y = from(oy, lineY[r], start);
2965
+ segments.push({
2966
+ border,
2967
+ x: from(ox, lineX[line], border.width),
2968
+ y,
2969
+ w: border.width,
2970
+ h: from(oy, lineY[r + 1], end) + end - y,
2971
+ horizontal: false,
2972
+ row: r,
2973
+ column: line,
2974
+ });
2975
+ }
2976
+ }
2977
+ // The winners last, where segments cross — and between two that won
2978
+ // alike, the one further up and further left, as between two borders
2979
+ // on one segment (CSS 2.1 17.6.2.1): a corner four equal borders meet
2980
+ // at is the top-left cell's. Painted in the order they were found, the
2981
+ // segment below a corner took it.
2982
+ segments.sort((a, b) => a.border.rank - b.border.rank || b.row - a.row || b.column - a.column);
2983
+ for (const s of segments) {
2984
+ if (isTransparent(s.border.color))
2985
+ continue;
2986
+ const rect = clampRect(options, s.x, s.y, s.w, s.h);
2987
+ if (!rect)
2988
+ continue;
2989
+ ctx.fillStyle = s.border.color;
2990
+ fillEdge(ctx, rect, s.horizontal ? s.x : s.y, s.border.style, s.horizontal);
2991
+ }
2992
+ }
301
2993
  function fillEdge(ctx, rect, phaseOrigin, style, horizontal) {
302
2994
  const { x, y, w, h } = rect;
303
2995
  const length = horizontal ? w : h;
@@ -338,24 +3030,54 @@ function fillEdge(ctx, rect, phaseOrigin, style, horizontal) {
338
3030
  ctx.fillRect(x, y, w, h);
339
3031
  }
340
3032
  /** A list item's bullet or number, in the margin. */
341
- function paintMarker(ctx, box, options) {
342
- const marker = box.markerLayout;
343
- if (!marker)
3033
+ function paintMarker(ctx, marker, options) {
3034
+ const x = marker.x + options.originX;
3035
+ const y = marker.y + options.originY;
3036
+ if (marker.image) {
3037
+ // `list-style-image`'s, at its own size
3038
+ const { url, width, height } = marker.image;
3039
+ const loaded = options.backgroundImageFor?.(url);
3040
+ if (!loaded || !(width > 0 && height > 0))
3041
+ return;
3042
+ if (loaded.image instanceof SvgDrawing) {
3043
+ loaded.image.draw(ctx, x, y, width, height, options.scale ?? 1);
3044
+ }
3045
+ else
3046
+ ctx.drawImage?.(loaded.image, x, y, width, height);
344
3047
  return;
345
- const x = box.markerX + options.originX;
346
- const y = box.markerY + options.originY;
347
- marker.draw(ctx, x, y);
3048
+ }
3049
+ marker.layout?.draw(ctx, x, y);
348
3050
  }
349
3051
  function paintImage(ctx, box, options) {
350
3052
  const image = options.imageFor(box);
351
- const x = Math.round(box.contentX + options.originX);
352
- const y = Math.round(box.contentY + options.originY);
353
- const w = Math.ceil(box.contentWidth);
354
- const h = Math.ceil(box.contentHeight);
3053
+ const left = box.contentX + options.originX;
3054
+ const top = box.contentY + options.originY;
3055
+ const x = Math.round(left);
3056
+ const y = Math.round(top);
3057
+ const w = Math.round(left + box.contentWidth) - x;
3058
+ const h = Math.round(top + box.contentHeight) - y;
355
3059
  if (w <= 0 || h <= 0)
356
3060
  return;
357
- if (image && ctx.drawImage) {
358
- ctx.drawImage(image, x, y, w, h);
3061
+ if (image instanceof SvgDrawing || (image && ctx.drawImage)) {
3062
+ // on the pixel grid, as a background's tile is: a fraction of a pixel
3063
+ // is a blur, or a row of the image the next pixel's
3064
+ const at = fitted(box, x, y, w, h);
3065
+ at.x = Math.round(at.x);
3066
+ at.y = Math.round(at.y);
3067
+ // trimmed to the curve of its content edge (CSS Backgrounds 3, 5.3):
3068
+ // an avatar is a round photograph. A rounded clip is a mask the size
3069
+ // of the window on X11, so only a box that has corners pays for one;
3070
+ // an image `object-fit` puts past its box is cut by a rectangle
3071
+ const corners = contentCorners(box);
3072
+ const past = at.x < x || at.y < y || at.x + at.w > x + w || at.y + at.h > y + h;
3073
+ const clipped = (!!corners || past) && pushClip(ctx, { x, y, w, h }, corners);
3074
+ if (image instanceof SvgDrawing) {
3075
+ image.draw(ctx, at.x, at.y, at.w, at.h, options.scale ?? 1);
3076
+ }
3077
+ else
3078
+ ctx.drawImage(image, at.x, at.y, at.w, at.h);
3079
+ if (clipped)
3080
+ ctx.restore();
359
3081
  return;
360
3082
  }
361
3083
  // No image yet, or no image at all: a faint frame where it will be, so a
@@ -370,6 +3092,82 @@ function paintImage(ctx, box, options) {
370
3092
  ctx.fillRect(x + w - t, y, t, h);
371
3093
  }
372
3094
  }
3095
+ /**
3096
+ * Where an image is drawn in its content box (CSS Images 3, 5.5):
3097
+ * stretched to it, the `fill` everything is by default, or at its own
3098
+ * ratio — within it (`contain`), over the whole of it (`cover`, which
3099
+ * Tailwind's `object-cover` avatars and card images are), at its own size
3100
+ * (`none`), or the smaller of those two (`scale-down`) — and placed by
3101
+ * `object-position`, the middle unless it says otherwise.
3102
+ */
3103
+ function fitted(box, x, y, w, h) {
3104
+ const fit = box.style.objectFit;
3105
+ const own = box.intrinsic;
3106
+ if (!own)
3107
+ return { x, y, w, h };
3108
+ // what of its size it has of its own: an SVG may have a ratio from its
3109
+ // `viewBox` and neither side, or one side
3110
+ const hasWidth = !(own.missing & 1) && own.width > 0;
3111
+ const hasHeight = !(own.missing & 2) && own.height > 0;
3112
+ const ratio = own.ratio > 0
3113
+ ? own.ratio
3114
+ : hasWidth && hasHeight
3115
+ ? own.width / own.height
3116
+ : 0;
3117
+ // at its ratio within the box, and over the whole of it; the box where
3118
+ // it has none
3119
+ const within = !ratio
3120
+ ? [w, h]
3121
+ : w / h > ratio
3122
+ ? [h * ratio, h]
3123
+ : [w, w / ratio];
3124
+ const over = !ratio
3125
+ ? [w, h]
3126
+ : w / h > ratio
3127
+ ? [w, w / ratio]
3128
+ : [h * ratio, h];
3129
+ // its own size: a side it lacks from the other through its ratio, and
3130
+ // with neither side, within the box (CSS Images 3, 5.2)
3131
+ const natural = () => hasWidth && hasHeight
3132
+ ? [own.width, own.height]
3133
+ : hasWidth
3134
+ ? [own.width, ratio ? own.width / ratio : h]
3135
+ : hasHeight
3136
+ ? [ratio ? own.height * ratio : w, own.height]
3137
+ : within;
3138
+ let dw = w;
3139
+ let dh = h;
3140
+ if (fit === 'contain')
3141
+ [dw, dh] = within;
3142
+ else if (fit === 'cover')
3143
+ [dw, dh] = over;
3144
+ else if (fit === 'none')
3145
+ [dw, dh] = natural();
3146
+ else if (fit === 'scale-down') {
3147
+ const n = natural();
3148
+ [dw, dh] = n[0] * n[1] <= within[0] * within[1] ? n : within;
3149
+ }
3150
+ // `fill` is the box's size; placed, it has no room to move in by a
3151
+ // percentage, but it does by a length — `right 2px` puts it two pixels
3152
+ // in from the right, and cut there
3153
+ return {
3154
+ x: x + resolve(box.style.objectPositionX, w - dw),
3155
+ y: y + resolve(box.style.objectPositionY, h - dh),
3156
+ w: dw,
3157
+ h: dh,
3158
+ };
3159
+ }
3160
+ /** An inline `<svg>`, drawn in its content box. Its `currentColor` is the
3161
+ * box's `color`, as an icon's is the text's around it. */
3162
+ function paintSvg(ctx, box, options) {
3163
+ if (!box.el)
3164
+ return;
3165
+ const x = Math.round(box.contentX + options.originX);
3166
+ const y = Math.round(box.contentY + options.originY);
3167
+ const w = Math.round(box.contentX + options.originX + box.contentWidth) - x;
3168
+ const h = Math.round(box.contentY + options.originY + box.contentHeight) - y;
3169
+ inlineDrawing(box.el).draw(ctx, x, y, w, h, options.scale ?? 1, box.style.color);
3170
+ }
373
3171
  /**
374
3172
  * The X protocol carries glyph positions as Int16, so anything drawn past
375
3173
  * ±32767 window coordinates does not clip — it throws in the encoder. The
@@ -423,7 +3221,11 @@ function paintLines(ctx, box, options) {
423
3221
  }
424
3222
  if (!visible.length)
425
3223
  return;
3224
+ const bleeds = [];
426
3225
  for (const line of visible) {
3226
+ if (line.background)
3227
+ paintLineBackground(ctx, line, options);
3228
+ paintInlineBoxes(ctx, line, options, line === lines[0] ? null : bleeds);
427
3229
  for (const text of line.texts) {
428
3230
  const natural = text.layout.lines[text.layoutLine];
429
3231
  if (natural)
@@ -431,6 +3233,13 @@ function paintLines(ctx, box, options) {
431
3233
  }
432
3234
  paintSelection(ctx, line, options);
433
3235
  }
3236
+ if (SHADOWED_TEXT.has(box))
3237
+ paintTextShadows(ctx, visible, options);
3238
+ // an underline goes under the glyphs, a line through over them (CSS 2.1
3239
+ // Appendix E): a descender crosses its own underline
3240
+ paintRules(ctx, visible, dx, dy, options.scale ?? 1, 'under');
3241
+ if (CLIPPED_TEXT.has(box))
3242
+ paintClippedText(ctx, box, visible, options);
434
3243
  // One `draw` per layout: a paragraph is a single glyph composite, and
435
3244
  // drawing it once per line would be one X request per line for the same
436
3245
  // batch.
@@ -452,15 +3261,568 @@ function paintLines(ctx, box, options) {
452
3261
  text.layout.draw(ctx, text.drawX + dx, top);
453
3262
  }
454
3263
  }
3264
+ paintRules(ctx, visible, dx, dy, options.scale ?? 1, 'over');
3265
+ if (bleeds.length)
3266
+ paintBleeds(ctx, bleeds, options);
3267
+ // an atomic set below the flow with a negative `z-index` is its
3268
+ // stacking context's to paint, there (`hoistNegative`): painted by its
3269
+ // line as well, it came back over the box it was under
3270
+ const hoisted = options.negative;
455
3271
  for (const line of visible) {
3272
+ for (const placed of line.atomics) {
3273
+ if (hoisted && HOISTED.has(placed.box))
3274
+ continue;
3275
+ paintBox(ctx, placed.box, options);
3276
+ }
3277
+ }
3278
+ }
3279
+ /**
3280
+ * The part of each such fragment above its line, drawn again over the
3281
+ * text there. CSS 2.1 Appendix E paints a block's inline content a line at
3282
+ * a time, backgrounds before text, so a box on the second line with
3283
+ * padding enough to reach the first is drawn over the first line's text;
3284
+ * the ink here goes on in one batch after every line's backgrounds
3285
+ * (`paintLines`), which left that text over it.
3286
+ */
3287
+ function paintBleeds(ctx, bleeds, options) {
3288
+ if (!ctx.save || !ctx.restore || !ctx.beginPath || !ctx.rect || !ctx.clip)
3289
+ return;
3290
+ for (const { fragment, lineTop } of bleeds) {
3291
+ const left = Math.floor(fragment.x + options.originX) - 1;
3292
+ const top = Math.floor(fragment.y + options.originY) - 1;
3293
+ const bottom = Math.round(lineTop + options.originY);
3294
+ if (bottom <= top)
3295
+ continue;
3296
+ ctx.save();
3297
+ ctx.beginPath();
3298
+ ctx.rect(left, top, Math.ceil(fragment.width) + 2, bottom - top);
3299
+ ctx.clip();
3300
+ paintLayers(ctx, fragment, options);
3301
+ paintBorders(ctx, fragment, options);
3302
+ ctx.restore();
3303
+ }
3304
+ }
3305
+ /**
3306
+ * The backgrounds painted through text (`background-clip: text`, CSS
3307
+ * Backgrounds 4): Tailwind's `bg-clip-text text-transparent` over a
3308
+ * gradient, which is how a landing page colours its headline. The text is
3309
+ * laid out again with no ink of its own (`inklessLayout`) and drawn with
3310
+ * the box's background as the fill — a gradient, which both engines fill
3311
+ * glyphs with, or its colour — clipped to the runs that are the box's, so
3312
+ * the text around them keeps its own ink. The gradient spans the box's
3313
+ * padding box, or an inline box's own runs and its padding.
3314
+ */
3315
+ function paintClippedText(ctx, block, lines, options) {
3316
+ if (!ctx.save || !ctx.restore || !ctx.beginPath || !ctx.rect || !ctx.clip)
3317
+ return;
3318
+ const dx = options.originX;
3319
+ const dy = options.originY;
3320
+ // each box's runs, a layout at a time: one draw a layout, clipped to all
3321
+ // of them
3322
+ const parts = new Map();
3323
+ for (const line of lines) {
456
3324
  for (const text of line.texts) {
457
3325
  const natural = text.layout.lines[text.layoutLine];
458
- if (natural)
459
- paintRunRules(ctx, natural, text.drawX + dx, text.drawY + dy, options.scale ?? 1);
3326
+ if (!natural)
3327
+ continue;
3328
+ const x = text.drawX + dx;
3329
+ const y = text.drawY + dy;
3330
+ // a glyph may reach past its line box, where its line is tight
3331
+ const top = y + Math.min(natural.y, natural.baseline - (natural.ascent ?? 0)) - 2;
3332
+ const bottom = y +
3333
+ Math.max(natural.y + natural.height, natural.baseline + (natural.descent ?? 0)) +
3334
+ 2;
3335
+ for (const run of natural.runs) {
3336
+ const clip = clipBoxOf(text.spans.boxAt?.(run.start) ?? null, block);
3337
+ if (!clip)
3338
+ continue;
3339
+ let byLayout = parts.get(clip);
3340
+ if (!byLayout)
3341
+ parts.set(clip, (byLayout = new Map()));
3342
+ let part = byLayout.get(text.layout);
3343
+ if (!part) {
3344
+ part = { layout: text.layout, x, y, rects: [] };
3345
+ byLayout.set(text.layout, part);
3346
+ }
3347
+ part.rects.push({
3348
+ x: x + natural.x + run.x - 1,
3349
+ y: top,
3350
+ width: run.width + 2,
3351
+ height: bottom - top,
3352
+ });
3353
+ }
460
3354
  }
461
- for (const placed of line.atomics)
462
- paintBox(ctx, placed.box, options);
463
3355
  }
3356
+ for (const [clip, byLayout] of parts) {
3357
+ const list = [...byLayout.values()];
3358
+ const style = clip.style;
3359
+ let area;
3360
+ if (clip === block)
3361
+ area = paddingBox(block, options);
3362
+ else {
3363
+ // an inline box: its runs, and its padding round them
3364
+ let x0 = Infinity;
3365
+ let y0 = Infinity;
3366
+ let x1 = -Infinity;
3367
+ let y1 = -Infinity;
3368
+ for (const part of list) {
3369
+ for (const r of part.rects) {
3370
+ x0 = Math.min(x0, r.x);
3371
+ y0 = Math.min(y0, r.y);
3372
+ x1 = Math.max(x1, r.x + r.width);
3373
+ y1 = Math.max(y1, r.y + r.height);
3374
+ }
3375
+ }
3376
+ area = {
3377
+ x: x0 - clip.padLeft,
3378
+ y: y0 - clip.padTop,
3379
+ width: x1 - x0 + clip.padLeft + clip.padRight,
3380
+ height: y1 - y0 + clip.padTop + clip.padBottom,
3381
+ };
3382
+ }
3383
+ const gradient = style.backgroundGradient ??
3384
+ (style.backgroundImages?.find((image) => image !== null && typeof image !== 'string') ||
3385
+ null);
3386
+ let fill = null;
3387
+ if (gradient && ctx.createLinearGradient) {
3388
+ if (!(area.width > 0 && area.height > 0))
3389
+ continue;
3390
+ // the text it shows through: what the fill is drawn over
3391
+ let x0 = Infinity;
3392
+ let y0 = Infinity;
3393
+ let x1 = -Infinity;
3394
+ let y1 = -Infinity;
3395
+ for (const part of list) {
3396
+ for (const r of part.rects) {
3397
+ x0 = Math.min(x0, r.x);
3398
+ y0 = Math.min(y0, r.y);
3399
+ x1 = Math.max(x1, r.x + r.width);
3400
+ y1 = Math.max(y1, r.y + r.height);
3401
+ }
3402
+ }
3403
+ fill = linearGradient(ctx, gradient, area.x, area.y, area.width, area.height, style.color, x1 > x0 ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : null);
3404
+ }
3405
+ else if (!isTransparent(style.backgroundColor)) {
3406
+ fill = inkColor(style.backgroundColor, style.color);
3407
+ }
3408
+ if (fill === null)
3409
+ continue;
3410
+ for (const part of list) {
3411
+ const inkless = inklessLayout(part.layout);
3412
+ if (!inkless)
3413
+ continue;
3414
+ ctx.save();
3415
+ ctx.beginPath();
3416
+ for (const r of part.rects)
3417
+ ctx.rect(r.x, r.y, r.width, r.height);
3418
+ ctx.clip();
3419
+ ctx.fillStyle = fill;
3420
+ inkless.draw(ctx, part.x, part.y);
3421
+ ctx.restore();
3422
+ }
3423
+ }
3424
+ }
3425
+ /**
3426
+ * The shadows text casts (CSS Text Decoration 3, 4), under it and its
3427
+ * underline: a layout drawn again for each shadow, the last first, as
3428
+ * `fillShadow` draws a box's — clear of the window to the left, the
3429
+ * shadow offset back by as much — so that only the shadow lands. The
3430
+ * engines draw a layout's glyphs in its runs' own colours, and a copy in a
3431
+ * shadow's would be another layout. One whose runs do not all cast the
3432
+ * same shadows draws each stretch's clipped to it, a line at a time.
3433
+ */
3434
+ function paintTextShadows(ctx, lines, options) {
3435
+ if (!('shadowBlur' in ctx) || !ctx.save || !ctx.restore)
3436
+ return;
3437
+ const dx = options.originX;
3438
+ const dy = options.originY;
3439
+ const whole = new Set();
3440
+ for (const line of lines) {
3441
+ for (const text of line.texts) {
3442
+ const layout = text.layout;
3443
+ const top = text.drawY + dy;
3444
+ if (top < -COORD_LIMIT || top + layout.height > COORD_LIMIT)
3445
+ continue;
3446
+ const cast = castOf(text);
3447
+ if (cast === null)
3448
+ continue;
3449
+ const left = text.drawX + dx;
3450
+ if (cast !== MIXED) {
3451
+ // every run casts the same: the layout's once for each shadow
3452
+ if (whole.has(layout))
3453
+ continue;
3454
+ whole.add(layout);
3455
+ castShadows(ctx, text, left, top, cast, null);
3456
+ continue;
3457
+ }
3458
+ const natural = layout.lines[text.layoutLine];
3459
+ if (!natural || !text.spans.boxAt)
3460
+ continue;
3461
+ for (const [stretch, from, to] of stretchesOf(text, natural)) {
3462
+ castShadows(ctx, text, left, top, stretch, {
3463
+ x: left + natural.x + from,
3464
+ y: text.drawY + dy + natural.y,
3465
+ width: to - from,
3466
+ height: natural.height,
3467
+ });
3468
+ }
3469
+ }
3470
+ }
3471
+ }
3472
+ /** A layout whose runs do not all cast the same shadows. */
3473
+ const MIXED = { shadows: [], color: '' };
3474
+ /** What a layout's runs cast: null for none, the one cast of all of them,
3475
+ * or `MIXED`. Kept by the map of its runs to their boxes, which is its
3476
+ * paragraph's own — not by the layout, which another paragraph of the same
3477
+ * runs may share, casting other shadows or none, since a shadow is no
3478
+ * field of a run. */
3479
+ const CASTS = new WeakMap();
3480
+ function castOf(text) {
3481
+ const layout = text.layout;
3482
+ if (CASTS.has(text.spans))
3483
+ return CASTS.get(text.spans);
3484
+ let cast;
3485
+ const boxAt = text.spans.boxAt;
3486
+ for (const natural of layout.lines) {
3487
+ for (const run of natural.runs) {
3488
+ // a spacer, or a bidi control, is no one's text and draws nothing
3489
+ const style = boxAt?.call(text.spans, run.start)?.style;
3490
+ if (!style)
3491
+ continue;
3492
+ const shadows = style.textShadow;
3493
+ const next = shadows ? { shadows, color: style.color } : null;
3494
+ if (cast === undefined)
3495
+ cast = next;
3496
+ else if (!sameCast(cast, next))
3497
+ cast = MIXED;
3498
+ if (cast === MIXED)
3499
+ break;
3500
+ }
3501
+ if (cast === MIXED)
3502
+ break;
3503
+ }
3504
+ CASTS.set(text.spans, cast ?? null);
3505
+ return cast ?? null;
3506
+ }
3507
+ function sameCast(a, b) {
3508
+ return (a === b || (!!a && !!b && a.shadows === b.shadows && a.color === b.color));
3509
+ }
3510
+ /** The stretches of one line of a layout that cast shadows: each cast, and
3511
+ * where along the line its runs start and end. */
3512
+ function stretchesOf(text, natural) {
3513
+ const out = [];
3514
+ for (const run of natural.runs) {
3515
+ const style = text.spans.boxAt.call(text.spans, run.start)?.style;
3516
+ if (!style?.textShadow)
3517
+ continue;
3518
+ const cast = { shadows: style.textShadow, color: style.color };
3519
+ const last = out[out.length - 1];
3520
+ if (last && sameCast(last[0], cast) && Math.abs(last[2] - run.x) < 0.5) {
3521
+ last[2] = Math.max(last[2], run.x + run.width);
3522
+ }
3523
+ else
3524
+ out.push([cast, run.x, run.x + run.width]);
3525
+ }
3526
+ return out;
3527
+ }
3528
+ /** A layout's shadows, drawn where `left` and `top` put it, each clipped to
3529
+ * `stretch` where the layout's runs do not all cast them. */
3530
+ function castShadows(ctx, text, left, top, cast, stretch) {
3531
+ const layout = text.layout;
3532
+ // clear of the window: the far end of its furthest line, which a line
3533
+ // `text-align` moves in from the layout's left reaches past its width,
3534
+ // at the window's left
3535
+ let right = layout.width;
3536
+ for (const natural of layout.lines) {
3537
+ right = Math.max(right, natural.x + natural.width);
3538
+ }
3539
+ const shift = Math.ceil(left + right) + 1;
3540
+ for (let i = cast.shadows.length - 1; i >= 0; i -= 1) {
3541
+ const s = cast.shadows[i];
3542
+ ctx.save();
3543
+ if (stretch && ctx.beginPath && ctx.rect && ctx.clip) {
3544
+ // the stretch, as far as its shadow falls
3545
+ const reach = shadowReach(s.blur);
3546
+ ctx.beginPath();
3547
+ ctx.rect(stretch.x + Math.min(0, s.x) - reach, stretch.y + Math.min(0, s.y) - reach, stretch.width + Math.abs(s.x) + 2 * reach, stretch.height + Math.abs(s.y) + 2 * reach);
3548
+ ctx.clip();
3549
+ }
3550
+ ctx.shadowColor = inkColor(s.color, cast.color);
3551
+ // CoreGraphics casts no shadow with no blur at all: a hard one is one
3552
+ // blurred too little to see
3553
+ ctx.shadowBlur = s.blur > 0 ? s.blur : 0.01;
3554
+ ctx.shadowOffsetX = s.x + shift;
3555
+ ctx.shadowOffsetY = s.y;
3556
+ layout.draw(ctx, left - shift, top);
3557
+ ctx.restore();
3558
+ }
3559
+ }
3560
+ /** One pass of the lines' run rules, `under` or `over` their glyphs. */
3561
+ function paintRules(ctx, lines, dx, dy, scale, rules) {
3562
+ for (const line of lines) {
3563
+ const shifted = SHIFTED_LINES.has(line);
3564
+ for (const text of line.texts) {
3565
+ const natural = text.layout.lines[text.layoutLine];
3566
+ if (natural) {
3567
+ paintRunRules(ctx, natural, text.drawX + dx, text.drawY + dy + (shifted ? ruleDrop(text) : 0), scale, rules);
3568
+ }
3569
+ }
3570
+ }
3571
+ }
3572
+ /**
3573
+ * How far below a raised text its rules go: to the baseline of the element
3574
+ * that decorates it. `vertical-align` moves the text and not the lines an
3575
+ * element outside it draws through it, which stay on that element's
3576
+ * baseline (CSS Text Decoration 3, 2.1). `position: relative` moves both.
3577
+ */
3578
+ function ruleDrop(text) {
3579
+ const raise = TEXT_RAISES.get(text);
3580
+ if (!raise)
3581
+ return 0;
3582
+ const owner = text.spans.boxAt?.(text.layoutStart);
3583
+ for (let at = owner?.parent; at?.kind === 'inline'; at = at.parent) {
3584
+ if (at.style.textDecorationLine !== 'none') {
3585
+ return raise - (BOX_RAISES.get(at) ?? 0);
3586
+ }
3587
+ }
3588
+ return raise;
3589
+ }
3590
+ /**
3591
+ * The backgrounds and borders of the inline boxes on one line — a `<mark>`, a
3592
+ * padded `<span>`, a link set as a button — under the text, a fragment each:
3593
+ * from the ascent to the descent of the box's own face, with its vertical
3594
+ * padding and border beyond (CSS 2.1 10.6.1, 10.8.1, which is also why they
3595
+ * do not make the line taller), and across the box's text and atomics on
3596
+ * this line out to its padding and border where it starts or ends. Outer
3597
+ * boxes first, so a highlight shows behind a nested element's text too. A
3598
+ * run finds its element from its place in the document, as a click does.
3599
+ */
3600
+ /** A `::first-line` background: behind all the line's content, across it,
3601
+ * over its face's height from the baseline, as an inline box around the
3602
+ * line's content would be painted. */
3603
+ function paintLineBackground(ctx, line, options) {
3604
+ const background = line.background;
3605
+ let left = Infinity;
3606
+ let right = -Infinity;
3607
+ let baseline = line.y + line.baseline;
3608
+ const shifted = SHIFTED_LINES.has(line);
3609
+ for (const text of line.texts) {
3610
+ const natural = text.layout.lines[text.layoutLine];
3611
+ if (!natural)
3612
+ continue;
3613
+ // where the line has it, not where `position: relative` moved it or
3614
+ // `vertical-align` raised it
3615
+ const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
3616
+ const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
3617
+ const x = text.drawX - (shift?.x ?? 0) + natural.x;
3618
+ baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
3619
+ left = Math.min(left, x);
3620
+ right = Math.max(right, x + natural.width);
3621
+ }
3622
+ for (const placed of line.atomics) {
3623
+ left = Math.min(left, placed.x - placed.box.marginLeft);
3624
+ right = Math.max(right, placed.x + placed.box.width + placed.box.marginRight);
3625
+ }
3626
+ if (!(right > left))
3627
+ return;
3628
+ const top = baseline - background.ascent;
3629
+ paintLayers(ctx, {
3630
+ x: left,
3631
+ y: top,
3632
+ width: right - left,
3633
+ height: baseline + background.descent - top,
3634
+ captionTop: 0,
3635
+ captionBottom: 0,
3636
+ borderTop: 0,
3637
+ borderBottom: 0,
3638
+ borderLeft: 0,
3639
+ borderRight: 0,
3640
+ style: background.style,
3641
+ }, options);
3642
+ }
3643
+ function paintInlineBoxes(ctx, line, options,
3644
+ /** Where a fragment that reaches up over the lines before goes, to be
3645
+ * drawn again over their text; null for a block's first line. */
3646
+ bleeds = null) {
3647
+ let fragments = null;
3648
+ const widen = (box, left, right) => {
3649
+ fragments ??= new Map();
3650
+ const f = fragments.get(box);
3651
+ if (f) {
3652
+ f.left = Math.min(f.left, left);
3653
+ f.right = Math.max(f.right, right);
3654
+ }
3655
+ else {
3656
+ fragments.set(box, { left, right, start: false, end: false });
3657
+ }
3658
+ };
3659
+ // Every text on a line is drawn on one baseline (`finishLine`), and an
3660
+ // engine's is from the top of its layout rather than of the line. Taken
3661
+ // where the line has each text: a box `position: relative` moves is
3662
+ // moved by its own offset below, and the boxes around it are not.
3663
+ let baseline = line.y + line.baseline;
3664
+ const shifted = SHIFTED_LINES.has(line);
3665
+ for (const text of line.texts) {
3666
+ const natural = text.layout.lines[text.layoutLine];
3667
+ const boxAt = text.spans.boxAt;
3668
+ if (!natural || !boxAt)
3669
+ continue;
3670
+ const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
3671
+ const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
3672
+ baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
3673
+ const x = text.drawX - (shift?.x ?? 0) + natural.x;
3674
+ for (const run of natural.runs) {
3675
+ const owner = boxAt.call(text.spans, run.start);
3676
+ if (!owner)
3677
+ continue;
3678
+ // Within the line: a space a line ends on hangs past it, and CoreText
3679
+ // keeps it in the run it ends even where the line's width does not.
3680
+ const left = Math.max(x, x + run.x);
3681
+ const right = Math.min(x + natural.width, x + run.x + run.width);
3682
+ if (right <= left)
3683
+ continue;
3684
+ for (const box of decoratedAncestors(owner))
3685
+ widen(box, left, right);
3686
+ }
3687
+ }
3688
+ for (const placed of line.atomics) {
3689
+ const atomic = placed.box;
3690
+ for (const box of decoratedAncestors(atomic)) {
3691
+ widen(box, placed.x - atomic.marginLeft, placed.x + atomic.width + atomic.marginRight);
3692
+ }
3693
+ }
3694
+ for (const edge of line.edges ?? []) {
3695
+ const box = edge.box;
3696
+ if (!box.decoration)
3697
+ continue;
3698
+ // The element's `direction` says which side its start is on, whatever
3699
+ // its text reads as (CSS 2.1 8.6); the edge's margin is outside the box,
3700
+ // its border and padding inside.
3701
+ const onLeft = (edge.side === 'start') !== (box.style.direction === 'rtl');
3702
+ if (onLeft) {
3703
+ const left = edge.x + box.marginLeft;
3704
+ widen(box, left, left);
3705
+ }
3706
+ else {
3707
+ const right = edge.x + edge.width - box.marginRight;
3708
+ widen(box, right, right);
3709
+ }
3710
+ const f = fragments.get(box);
3711
+ if (edge.side === 'start')
3712
+ f.start = true;
3713
+ else
3714
+ f.end = true;
3715
+ }
3716
+ if (!fragments)
3717
+ return;
3718
+ const boxes = [...fragments.keys()].sort((a, b) => depthOf(a) - depthOf(b));
3719
+ for (const box of boxes) {
3720
+ if (box.style.visibility !== 'visible')
3721
+ continue;
3722
+ const f = fragments.get(box);
3723
+ const face = box.decoration;
3724
+ // on its own baseline, which `vertical-align` may raise off the line's
3725
+ const own = shifted
3726
+ ? baseline -
3727
+ (LINE_BOX_RAISES.get(line)?.get(box) ?? BOX_RAISES.get(box) ?? 0)
3728
+ : baseline;
3729
+ const top = own - face.ascent - box.padTop - box.borderTop;
3730
+ const bottom = own + face.descent + box.padBottom + box.borderBottom;
3731
+ // Sliced where the box goes on to another line: no border and no
3732
+ // rounded corner on a side it does not end on (`box-decoration-break:
3733
+ // slice`, CSS's default). Left and right swap for right-to-left text.
3734
+ const rtl = box.style.direction === 'rtl';
3735
+ const leftEnds = rtl ? f.end : f.start;
3736
+ const rightEnds = rtl ? f.start : f.end;
3737
+ const moved = shifted ? offsetOf(box) : null;
3738
+ const style = fragmentStyle(box.style, leftEnds, rightEnds);
3739
+ const fragment = {
3740
+ x: f.left + (moved?.x ?? 0),
3741
+ y: top + (moved?.y ?? 0),
3742
+ width: f.right - f.left,
3743
+ height: bottom - top,
3744
+ captionTop: 0,
3745
+ captionBottom: 0,
3746
+ borderTop: box.borderTop,
3747
+ borderBottom: box.borderBottom,
3748
+ borderLeft: leftEnds ? box.borderLeft : 0,
3749
+ borderRight: rightEnds ? box.borderRight : 0,
3750
+ padTop: box.padTop,
3751
+ padBottom: box.padBottom,
3752
+ padLeft: leftEnds ? box.padLeft : 0,
3753
+ padRight: rightEnds ? box.padRight : 0,
3754
+ style,
3755
+ };
3756
+ if (fragment.width <= 0)
3757
+ continue;
3758
+ paintLayers(ctx, fragment, options);
3759
+ paintBorders(ctx, fragment, options);
3760
+ if (box.style.outlineStyle !== 'none')
3761
+ paintOutline(ctx, fragment, options);
3762
+ if (bleeds && fragment.y < line.y - 0.5) {
3763
+ bleeds.push({ fragment, lineTop: line.y + (moved?.y ?? 0) });
3764
+ }
3765
+ }
3766
+ }
3767
+ /** An inline box's style on a line it does not both start and end on: no
3768
+ * rounded corner on a side it goes on from. Made once a style and a pair
3769
+ * of ends, so a repaint copies no style and the layers made of it are
3770
+ * found again. */
3771
+ const FRAGMENT_STYLES = new WeakMap();
3772
+ function fragmentStyle(style, leftEnds, rightEnds) {
3773
+ if (leftEnds && rightEnds)
3774
+ return style;
3775
+ let made = FRAGMENT_STYLES.get(style);
3776
+ if (!made) {
3777
+ made = [];
3778
+ FRAGMENT_STYLES.set(style, made);
3779
+ }
3780
+ const which = (leftEnds ? 1 : 0) | (rightEnds ? 2 : 0);
3781
+ let out = made[which];
3782
+ if (!out) {
3783
+ const ends = (radii) => [
3784
+ leftEnds ? radii[0] : 0,
3785
+ rightEnds ? radii[1] : 0,
3786
+ rightEnds ? radii[2] : 0,
3787
+ leftEnds ? radii[3] : 0,
3788
+ ];
3789
+ out = copyStyle(style);
3790
+ out.borderRadius = ends(style.borderRadius);
3791
+ out.borderRadiusY = style.borderRadiusY ? ends(style.borderRadiusY) : null;
3792
+ made[which] = out;
3793
+ }
3794
+ return out;
3795
+ }
3796
+ /** How far `position: relative` moved an inline box: its own offset, and
3797
+ * those of the inline boxes around it. */
3798
+ function offsetOf(box) {
3799
+ let x = 0;
3800
+ let y = 0;
3801
+ let moved = false;
3802
+ for (let at = box; at?.kind === 'inline'; at = at.parent) {
3803
+ const offset = INLINE_OFFSETS.get(at);
3804
+ if (!offset)
3805
+ continue;
3806
+ x += offset.x;
3807
+ y += offset.y;
3808
+ moved = true;
3809
+ }
3810
+ return moved ? { x, y } : null;
3811
+ }
3812
+ /** The inline boxes around a box, within its line's block, that paint a
3813
+ * background or a border — outermost last. */
3814
+ const DECORATED = new WeakMap();
3815
+ function decoratedAncestors(box) {
3816
+ let found = DECORATED.get(box);
3817
+ if (found)
3818
+ return found;
3819
+ found = [];
3820
+ for (let at = box.parent; at && at.kind === 'inline'; at = at.parent) {
3821
+ if (at.decoration)
3822
+ found.push(at);
3823
+ }
3824
+ DECORATED.set(box, found);
3825
+ return found;
464
3826
  }
465
3827
  /**
466
3828
  * The band the document selection covers on one line.
@@ -485,8 +3847,8 @@ function paintSelection(ctx, line, options) {
485
3847
  if (to <= from)
486
3848
  continue;
487
3849
  const offsets = layoutOffsets(text.layout);
488
- const layoutFrom = text.layoutStart + (from - text.textStart);
489
- const layoutTo = text.layoutStart + (to - text.textStart);
3850
+ const layoutFrom = layoutOffsetOf(text, from);
3851
+ const layoutTo = layoutOffsetOf(text, to, true);
490
3852
  for (const band of lineBands(text.layout, natural, offsets, layoutFrom, layoutTo)) {
491
3853
  ctx.fillRect(Math.round(band.x + text.drawX + options.originX), Math.round(line.y + options.originY), Math.ceil(band.width), Math.ceil(line.height));
492
3854
  }