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