@react-x11/components 0.8.0 → 0.10.0

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