@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.
- package/README.md +105 -38
- package/dist/charts/node.d.ts +12 -2
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +50 -16
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +12 -3
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +94 -25
- package/dist/charts/render.js.map +1 -1
- package/dist/code/index.d.ts.map +1 -1
- package/dist/code/index.js +3 -1
- package/dist/code/index.js.map +1 -1
- package/dist/code-editor/completion.d.ts.map +1 -1
- package/dist/code-editor/completion.js +3 -2
- package/dist/code-editor/completion.js.map +1 -1
- package/dist/code-editor/index.d.ts.map +1 -1
- package/dist/code-editor/index.js +19 -0
- package/dist/code-editor/index.js.map +1 -1
- package/dist/code-editor/node.d.ts +177 -7
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +1329 -164
- package/dist/code-editor/node.js.map +1 -1
- package/dist/code-language/languages/glsl.d.ts.map +1 -1
- package/dist/code-language/languages/glsl.js +10 -2
- package/dist/code-language/languages/glsl.js.map +1 -1
- package/dist/code-language/languages/javascript.d.ts.map +1 -1
- package/dist/code-language/languages/javascript.js +18 -3
- package/dist/code-language/languages/javascript.js.map +1 -1
- package/dist/code-language/languages/json.d.ts.map +1 -1
- package/dist/code-language/languages/json.js +10 -2
- package/dist/code-language/languages/json.js.map +1 -1
- package/dist/code-language/languages/shell.d.ts.map +1 -1
- package/dist/code-language/languages/shell.js +10 -2
- package/dist/code-language/languages/shell.js.map +1 -1
- package/dist/code-language/languages/sql.d.ts.map +1 -1
- package/dist/code-language/languages/sql.js +20 -2
- package/dist/code-language/languages/sql.js.map +1 -1
- package/dist/code-language/stream.d.ts +22 -0
- package/dist/code-language/stream.d.ts.map +1 -1
- package/dist/code-language/stream.js +259 -9
- package/dist/code-language/stream.js.map +1 -1
- package/dist/code-language/types.d.ts +5 -4
- package/dist/code-language/types.d.ts.map +1 -1
- package/dist/codeblock/index.d.ts +13 -2
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +20 -3
- package/dist/codeblock/index.js.map +1 -1
- package/dist/embed/client.d.ts +48 -3
- package/dist/embed/client.d.ts.map +1 -1
- package/dist/embed/client.js +79 -5
- package/dist/embed/client.js.map +1 -1
- package/dist/embed/index.d.ts +1 -1
- package/dist/embed/index.d.ts.map +1 -1
- package/dist/embed/index.js +1 -1
- package/dist/embed/index.js.map +1 -1
- package/dist/flow/draw.d.ts +50 -0
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +132 -33
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/gl/color.d.ts +13 -0
- package/dist/flow/gl/color.d.ts.map +1 -0
- package/dist/flow/gl/color.js +82 -0
- package/dist/flow/gl/color.js.map +1 -0
- package/dist/flow/gl/index.d.ts +60 -0
- package/dist/flow/gl/index.d.ts.map +1 -0
- package/dist/flow/gl/index.js +283 -0
- package/dist/flow/gl/index.js.map +1 -0
- package/dist/flow/gl/pack.d.ts +133 -0
- package/dist/flow/gl/pack.d.ts.map +1 -0
- package/dist/flow/gl/pack.js +408 -0
- package/dist/flow/gl/pack.js.map +1 -0
- package/dist/flow/gl/renderer.d.ts +95 -0
- package/dist/flow/gl/renderer.d.ts.map +1 -0
- package/dist/flow/gl/renderer.js +459 -0
- package/dist/flow/gl/renderer.js.map +1 -0
- package/dist/flow/gl/shaders.d.ts +79 -0
- package/dist/flow/gl/shaders.d.ts.map +1 -0
- package/dist/flow/gl/shaders.js +346 -0
- package/dist/flow/gl/shaders.js.map +1 -0
- package/dist/flow/gl/text.d.ts +318 -0
- package/dist/flow/gl/text.d.ts.map +1 -0
- package/dist/flow/gl/text.js +1103 -0
- package/dist/flow/gl/text.js.map +1 -0
- package/dist/flow/index.d.ts +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +619 -64
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/node.d.ts +621 -72
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +2253 -1018
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/paint.d.ts +35 -0
- package/dist/flow/paint.d.ts.map +1 -0
- package/dist/flow/paint.js +393 -0
- package/dist/flow/paint.js.map +1 -0
- package/dist/flow/paths.d.ts +14 -0
- package/dist/flow/paths.d.ts.map +1 -1
- package/dist/flow/paths.js +45 -19
- package/dist/flow/paths.js.map +1 -1
- package/dist/flow/scene.d.ts +584 -0
- package/dist/flow/scene.d.ts.map +1 -0
- package/dist/flow/scene.js +1453 -0
- package/dist/flow/scene.js.map +1 -0
- package/dist/flow/types.d.ts +111 -1
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/html/controls.d.ts +28 -0
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +52 -2
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/calc.d.ts +14 -0
- package/dist/html/css/calc.d.ts.map +1 -0
- package/dist/html/css/calc.js +344 -0
- package/dist/html/css/calc.js.map +1 -0
- package/dist/html/css/cascade.d.ts +240 -6
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +1327 -66
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +23 -0
- package/dist/html/css/color.d.ts.map +1 -0
- package/dist/html/css/color.js +740 -0
- package/dist/html/css/color.js.map +1 -0
- package/dist/html/css/content.d.ts +92 -0
- package/dist/html/css/content.d.ts.map +1 -0
- package/dist/html/css/content.js +388 -0
- package/dist/html/css/content.js.map +1 -0
- package/dist/html/css/counter-styles.d.ts +76 -0
- package/dist/html/css/counter-styles.d.ts.map +1 -0
- package/dist/html/css/counter-styles.js +893 -0
- package/dist/html/css/counter-styles.js.map +1 -0
- package/dist/html/css/decode.d.ts +12 -0
- package/dist/html/css/decode.d.ts.map +1 -0
- package/dist/html/css/decode.js +116 -0
- package/dist/html/css/decode.js.map +1 -0
- package/dist/html/css/parse.d.ts +110 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +1473 -180
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +498 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3698 -206
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +57 -11
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +37 -3
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +122 -48
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/css/vars.d.ts +52 -0
- package/dist/html/css/vars.d.ts.map +1 -0
- package/dist/html/css/vars.js +358 -0
- package/dist/html/css/vars.js.map +1 -0
- package/dist/html/dom.d.ts +30 -4
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +278 -17
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +106 -0
- package/dist/html/fonts.d.ts.map +1 -0
- package/dist/html/fonts.js +565 -0
- package/dist/html/fonts.js.map +1 -0
- package/dist/html/gif.d.ts +15 -0
- package/dist/html/gif.d.ts.map +1 -0
- package/dist/html/gif.js +229 -0
- package/dist/html/gif.js.map +1 -0
- package/dist/html/index.d.ts +50 -11
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +115 -9
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +226 -3
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +2937 -197
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +340 -17
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +1714 -182
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +58 -0
- package/dist/html/layout/cache.d.ts.map +1 -0
- package/dist/html/layout/cache.js +224 -0
- package/dist/html/layout/cache.js.map +1 -0
- package/dist/html/layout/collapse.d.ts +45 -0
- package/dist/html/layout/collapse.d.ts.map +1 -0
- package/dist/html/layout/collapse.js +246 -0
- package/dist/html/layout/collapse.js.map +1 -0
- package/dist/html/layout/css-grid.d.ts +7 -0
- package/dist/html/layout/css-grid.d.ts.map +1 -0
- package/dist/html/layout/css-grid.js +900 -0
- package/dist/html/layout/css-grid.js.map +1 -0
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +1025 -89
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/floats.d.ts +45 -6
- package/dist/html/layout/floats.d.ts.map +1 -1
- package/dist/html/layout/floats.js +113 -14
- package/dist/html/layout/floats.js.map +1 -1
- package/dist/html/layout/grid-lines.d.ts +39 -0
- package/dist/html/layout/grid-lines.d.ts.map +1 -0
- package/dist/html/layout/grid-lines.js +119 -0
- package/dist/html/layout/grid-lines.js.map +1 -0
- package/dist/html/layout/grid.d.ts +53 -0
- package/dist/html/layout/grid.d.ts.map +1 -0
- package/dist/html/layout/grid.js +153 -0
- package/dist/html/layout/grid.js.map +1 -0
- package/dist/html/layout/inline.d.ts +151 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +3375 -174
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/shaping.d.ts +6 -0
- package/dist/html/layout/shaping.d.ts.map +1 -0
- package/dist/html/layout/shaping.js +100 -0
- package/dist/html/layout/shaping.js.map +1 -0
- package/dist/html/layout/table.d.ts +3 -0
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +681 -175
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/layout/tracks.d.ts +44 -0
- package/dist/html/layout/tracks.d.ts.map +1 -0
- package/dist/html/layout/tracks.js +349 -0
- package/dist/html/layout/tracks.js.map +1 -0
- package/dist/html/node.d.ts +172 -5
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +1336 -103
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +92 -3
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +3503 -141
- package/dist/html/paint.js.map +1 -1
- package/dist/html/resources.d.ts +60 -14
- package/dist/html/resources.d.ts.map +1 -1
- package/dist/html/resources.js +122 -36
- package/dist/html/resources.js.map +1 -1
- package/dist/html/surfaces.d.ts +23 -0
- package/dist/html/surfaces.d.ts.map +1 -0
- package/dist/html/surfaces.js +68 -0
- package/dist/html/surfaces.js.map +1 -0
- package/dist/html/svg.d.ts +80 -0
- package/dist/html/svg.d.ts.map +1 -0
- package/dist/html/svg.js +435 -0
- package/dist/html/svg.js.map +1 -0
- package/dist/html/url.d.ts +23 -0
- package/dist/html/url.d.ts.map +1 -0
- package/dist/html/url.js +80 -0
- package/dist/html/url.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/collate.d.ts +3 -0
- package/dist/internal/collate.d.ts.map +1 -0
- package/dist/internal/collate.js +11 -0
- package/dist/internal/collate.js.map +1 -0
- package/dist/{markdown → internal/markdown}/ast.d.ts +23 -0
- package/dist/internal/markdown/ast.d.ts.map +1 -0
- package/dist/internal/markdown/ast.js +22 -0
- package/dist/internal/markdown/ast.js.map +1 -0
- package/dist/internal/markdown/parse.d.ts.map +1 -0
- package/dist/{markdown → internal/markdown}/parse.js +221 -30
- package/dist/internal/markdown/parse.js.map +1 -0
- package/dist/internal/markdown/stringify.d.ts +4 -0
- package/dist/internal/markdown/stringify.d.ts.map +1 -0
- package/dist/internal/markdown/stringify.js +684 -0
- package/dist/internal/markdown/stringify.js.map +1 -0
- package/dist/internal/markdown/tags.d.ts.map +1 -0
- package/dist/internal/markdown/tags.js.map +1 -0
- package/dist/internal/prewarm.d.ts +18 -0
- package/dist/internal/prewarm.d.ts.map +1 -0
- package/dist/internal/prewarm.js +42 -0
- package/dist/internal/prewarm.js.map +1 -0
- package/dist/internal/scroll.d.ts +3 -2
- package/dist/internal/scroll.d.ts.map +1 -1
- package/dist/internal/scroll.js +1 -0
- package/dist/internal/scroll.js.map +1 -1
- package/dist/internal/sdf.d.ts +20 -0
- package/dist/internal/sdf.d.ts.map +1 -0
- package/dist/internal/sdf.js +216 -0
- package/dist/internal/sdf.js.map +1 -0
- package/dist/internal/splice.d.ts +12 -0
- package/dist/internal/splice.d.ts.map +1 -0
- package/dist/internal/splice.js +23 -0
- package/dist/internal/splice.js.map +1 -0
- package/dist/internal/window.d.ts +12 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +23 -6
- package/dist/internal/window.js.map +1 -1
- package/dist/maps/anchors.d.ts +103 -0
- package/dist/maps/anchors.d.ts.map +1 -0
- package/dist/maps/anchors.js +790 -0
- package/dist/maps/anchors.js.map +1 -0
- package/dist/maps/controller.d.ts +340 -0
- package/dist/maps/controller.d.ts.map +1 -0
- package/dist/maps/controller.js +822 -0
- package/dist/maps/controller.js.map +1 -0
- package/dist/maps/gl/buckets.d.ts +99 -0
- package/dist/maps/gl/buckets.d.ts.map +1 -0
- package/dist/maps/gl/buckets.js +365 -0
- package/dist/maps/gl/buckets.js.map +1 -0
- package/dist/maps/gl/build-worker.d.ts +15 -0
- package/dist/maps/gl/build-worker.d.ts.map +1 -0
- package/dist/maps/gl/build-worker.js +44 -0
- package/dist/maps/gl/build-worker.js.map +1 -0
- package/dist/maps/gl/color.d.ts +8 -0
- package/dist/maps/gl/color.d.ts.map +1 -0
- package/dist/maps/gl/color.js +57 -0
- package/dist/maps/gl/color.js.map +1 -0
- package/dist/maps/gl/cover.d.ts +70 -0
- package/dist/maps/gl/cover.d.ts.map +1 -0
- package/dist/maps/gl/cover.js +137 -0
- package/dist/maps/gl/cover.js.map +1 -0
- package/dist/maps/gl/index.d.ts +3 -0
- package/dist/maps/gl/index.d.ts.map +1 -0
- package/dist/maps/gl/index.js +20 -0
- package/dist/maps/gl/index.js.map +1 -0
- package/dist/maps/gl/markers.d.ts +28 -0
- package/dist/maps/gl/markers.d.ts.map +1 -0
- package/dist/maps/gl/markers.js +60 -0
- package/dist/maps/gl/markers.js.map +1 -0
- package/dist/maps/gl/overlays.d.ts +68 -0
- package/dist/maps/gl/overlays.d.ts.map +1 -0
- package/dist/maps/gl/overlays.js +300 -0
- package/dist/maps/gl/overlays.js.map +1 -0
- package/dist/maps/gl/pane.d.ts +12 -0
- package/dist/maps/gl/pane.d.ts.map +1 -0
- package/dist/maps/gl/pane.js +37 -0
- package/dist/maps/gl/pane.js.map +1 -0
- package/dist/maps/gl/placement.d.ts +124 -0
- package/dist/maps/gl/placement.d.ts.map +1 -0
- package/dist/maps/gl/placement.js +642 -0
- package/dist/maps/gl/placement.js.map +1 -0
- package/dist/maps/gl/renderer.d.ts +384 -0
- package/dist/maps/gl/renderer.d.ts.map +1 -0
- package/dist/maps/gl/renderer.js +1184 -0
- package/dist/maps/gl/renderer.js.map +1 -0
- package/dist/maps/gl/shaders.d.ts +121 -0
- package/dist/maps/gl/shaders.d.ts.map +1 -0
- package/dist/maps/gl/shaders.js +453 -0
- package/dist/maps/gl/shaders.js.map +1 -0
- package/dist/maps/gl/store.d.ts +125 -0
- package/dist/maps/gl/store.d.ts.map +1 -0
- package/dist/maps/gl/store.js +512 -0
- package/dist/maps/gl/store.js.map +1 -0
- package/dist/maps/gl/text.d.ts +283 -0
- package/dist/maps/gl/text.d.ts.map +1 -0
- package/dist/maps/gl/text.js +698 -0
- package/dist/maps/gl/text.js.map +1 -0
- package/dist/maps/gl/view.d.ts +345 -0
- package/dist/maps/gl/view.d.ts.map +1 -0
- package/dist/maps/gl/view.js +1209 -0
- package/dist/maps/gl/view.js.map +1 -0
- package/dist/maps/icons.d.ts +32 -0
- package/dist/maps/icons.d.ts.map +1 -0
- package/dist/maps/icons.js +151 -0
- package/dist/maps/icons.js.map +1 -0
- package/dist/maps/index.d.ts +3 -1
- package/dist/maps/index.d.ts.map +1 -1
- package/dist/maps/index.js +218 -52
- package/dist/maps/index.js.map +1 -1
- package/dist/maps/labels.d.ts +91 -18
- package/dist/maps/labels.d.ts.map +1 -1
- package/dist/maps/labels.js +376 -140
- package/dist/maps/labels.js.map +1 -1
- package/dist/maps/node.d.ts +179 -56
- package/dist/maps/node.d.ts.map +1 -1
- package/dist/maps/node.js +663 -537
- package/dist/maps/node.js.map +1 -1
- package/dist/maps/overlay.d.ts +72 -9
- package/dist/maps/overlay.d.ts.map +1 -1
- package/dist/maps/overlay.js +108 -28
- package/dist/maps/overlay.js.map +1 -1
- package/dist/maps/paint.d.ts +3 -0
- package/dist/maps/paint.d.ts.map +1 -1
- package/dist/maps/paint.js.map +1 -1
- package/dist/maps/proj.d.ts +62 -0
- package/dist/maps/proj.d.ts.map +1 -1
- package/dist/maps/proj.js +101 -0
- package/dist/maps/proj.js.map +1 -1
- package/dist/maps/renderer.d.ts +69 -0
- package/dist/maps/renderer.d.ts.map +1 -0
- package/dist/maps/renderer.js +66 -0
- package/dist/maps/renderer.js.map +1 -0
- package/dist/maps/sources.d.ts +30 -6
- package/dist/maps/sources.d.ts.map +1 -1
- package/dist/maps/sources.js +22 -3
- package/dist/maps/sources.js.map +1 -1
- package/dist/maps/style.d.ts +35 -0
- package/dist/maps/style.d.ts.map +1 -1
- package/dist/maps/style.js.map +1 -1
- package/dist/maps/styles.d.ts +12 -0
- package/dist/maps/styles.d.ts.map +1 -1
- package/dist/maps/styles.js +120 -6
- package/dist/maps/styles.js.map +1 -1
- package/dist/maps/theme.d.ts +19 -0
- package/dist/maps/theme.d.ts.map +1 -0
- package/dist/maps/theme.js +44 -0
- package/dist/maps/theme.js.map +1 -0
- package/dist/maps/tiles.d.ts +147 -27
- package/dist/maps/tiles.d.ts.map +1 -1
- package/dist/maps/tiles.js +221 -54
- package/dist/maps/tiles.js.map +1 -1
- package/dist/maps/types.d.ts +213 -21
- package/dist/maps/types.d.ts.map +1 -1
- package/dist/markdown/expressions.d.ts +1 -1
- package/dist/markdown/expressions.d.ts.map +1 -1
- package/dist/markdown/expressions.js.map +1 -1
- package/dist/markdown/index.d.ts +7 -6
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +121 -50
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/spans.d.ts +1 -1
- package/dist/markdown/spans.d.ts.map +1 -1
- package/dist/markdown/spans.js +6 -0
- package/dist/markdown/spans.js.map +1 -1
- package/dist/media-player/index.d.ts +11 -3
- package/dist/media-player/index.d.ts.map +1 -1
- package/dist/media-player/index.js +13 -2
- package/dist/media-player/index.js.map +1 -1
- package/dist/qml/containers.d.ts.map +1 -1
- package/dist/qml/containers.js +6 -1
- package/dist/qml/containers.js.map +1 -1
- package/dist/qml/interaction.d.ts.map +1 -1
- package/dist/qml/interaction.js +38 -5
- package/dist/qml/interaction.js.map +1 -1
- package/dist/qml/layouts.d.ts.map +1 -1
- package/dist/qml/layouts.js +21 -6
- package/dist/qml/layouts.js.map +1 -1
- package/dist/qml/qtquick.d.ts.map +1 -1
- package/dist/qml/qtquick.js +9 -2
- package/dist/qml/qtquick.js.map +1 -1
- package/dist/qml/react.d.ts.map +1 -1
- package/dist/qml/react.js +9 -7
- package/dist/qml/react.js.map +1 -1
- package/dist/reorder/clock.d.ts +7 -0
- package/dist/reorder/clock.d.ts.map +1 -0
- package/dist/reorder/clock.js +26 -0
- package/dist/reorder/clock.js.map +1 -0
- package/dist/reorder/index.d.ts +25 -3
- package/dist/reorder/index.d.ts.map +1 -1
- package/dist/reorder/index.js +64 -19
- package/dist/reorder/index.js.map +1 -1
- package/dist/rich-text-editor/clipboard.d.ts +23 -0
- package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
- package/dist/rich-text-editor/clipboard.js +307 -0
- package/dist/rich-text-editor/clipboard.js.map +1 -0
- package/dist/rich-text-editor/collab.d.ts +32 -0
- package/dist/rich-text-editor/collab.d.ts.map +1 -0
- package/dist/rich-text-editor/collab.js +55 -0
- package/dist/rich-text-editor/collab.js.map +1 -0
- package/dist/rich-text-editor/commands.d.ts +50 -0
- package/dist/rich-text-editor/commands.d.ts.map +1 -0
- package/dist/rich-text-editor/commands.js +376 -0
- package/dist/rich-text-editor/commands.js.map +1 -0
- package/dist/rich-text-editor/drag.d.ts +68 -0
- package/dist/rich-text-editor/drag.d.ts.map +1 -0
- package/dist/rich-text-editor/drag.js +148 -0
- package/dist/rich-text-editor/drag.js.map +1 -0
- package/dist/rich-text-editor/html.d.ts +145 -0
- package/dist/rich-text-editor/html.d.ts.map +1 -0
- package/dist/rich-text-editor/html.js +516 -0
- package/dist/rich-text-editor/html.js.map +1 -0
- package/dist/rich-text-editor/index.d.ts +252 -0
- package/dist/rich-text-editor/index.d.ts.map +1 -0
- package/dist/rich-text-editor/index.js +856 -0
- package/dist/rich-text-editor/index.js.map +1 -0
- package/dist/rich-text-editor/inline.d.ts +94 -0
- package/dist/rich-text-editor/inline.d.ts.map +1 -0
- package/dist/rich-text-editor/inline.js +252 -0
- package/dist/rich-text-editor/inline.js.map +1 -0
- package/dist/rich-text-editor/keymap.d.ts +71 -0
- package/dist/rich-text-editor/keymap.d.ts.map +1 -0
- package/dist/rich-text-editor/keymap.js +394 -0
- package/dist/rich-text-editor/keymap.js.map +1 -0
- package/dist/rich-text-editor/keys.d.ts +40 -0
- package/dist/rich-text-editor/keys.d.ts.map +1 -0
- package/dist/rich-text-editor/keys.js +202 -0
- package/dist/rich-text-editor/keys.js.map +1 -0
- package/dist/rich-text-editor/look.d.ts +71 -0
- package/dist/rich-text-editor/look.d.ts.map +1 -0
- package/dist/rich-text-editor/look.js +342 -0
- package/dist/rich-text-editor/look.js.map +1 -0
- package/dist/rich-text-editor/markdown.d.ts +18 -0
- package/dist/rich-text-editor/markdown.d.ts.map +1 -0
- package/dist/rich-text-editor/markdown.js +496 -0
- package/dist/rich-text-editor/markdown.js.map +1 -0
- package/dist/rich-text-editor/marks.d.ts +9 -0
- package/dist/rich-text-editor/marks.d.ts.map +1 -0
- package/dist/rich-text-editor/marks.js +83 -0
- package/dist/rich-text-editor/marks.js.map +1 -0
- package/dist/rich-text-editor/nodes.d.ts +135 -0
- package/dist/rich-text-editor/nodes.d.ts.map +1 -0
- package/dist/rich-text-editor/nodes.js +397 -0
- package/dist/rich-text-editor/nodes.js.map +1 -0
- package/dist/rich-text-editor/render.d.ts +68 -0
- package/dist/rich-text-editor/render.d.ts.map +1 -0
- package/dist/rich-text-editor/render.js +518 -0
- package/dist/rich-text-editor/render.js.map +1 -0
- package/dist/rich-text-editor/schema.d.ts +16 -0
- package/dist/rich-text-editor/schema.d.ts.map +1 -0
- package/dist/rich-text-editor/schema.js +386 -0
- package/dist/rich-text-editor/schema.js.map +1 -0
- package/dist/rich-text-editor/store.d.ts +11 -0
- package/dist/rich-text-editor/store.d.ts.map +1 -0
- package/dist/rich-text-editor/store.js +43 -0
- package/dist/rich-text-editor/store.js.map +1 -0
- package/dist/rich-text-editor/suggest.d.ts +127 -0
- package/dist/rich-text-editor/suggest.d.ts.map +1 -0
- package/dist/rich-text-editor/suggest.js +0 -0
- package/dist/rich-text-editor/suggest.js.map +1 -0
- package/dist/rich-text-editor/tables.d.ts +41 -0
- package/dist/rich-text-editor/tables.d.ts.map +1 -0
- package/dist/rich-text-editor/tables.js +204 -0
- package/dist/rich-text-editor/tables.js.map +1 -0
- package/dist/rich-text-editor/toolbar.d.ts +58 -0
- package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
- package/dist/rich-text-editor/toolbar.js +548 -0
- package/dist/rich-text-editor/toolbar.js.map +1 -0
- package/dist/rich-text-editor/view.d.ts +368 -0
- package/dist/rich-text-editor/view.d.ts.map +1 -0
- package/dist/rich-text-editor/view.js +1968 -0
- package/dist/rich-text-editor/view.js.map +1 -0
- package/dist/rich-text-editor/virtual.d.ts +54 -0
- package/dist/rich-text-editor/virtual.d.ts.map +1 -0
- package/dist/rich-text-editor/virtual.js +279 -0
- package/dist/rich-text-editor/virtual.js.map +1 -0
- package/dist/richtext/node.d.ts +71 -4
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +233 -28
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +13 -8
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +18 -8
- package/dist/richtext/runs.js.map +1 -1
- package/dist/table/index.d.ts.map +1 -1
- package/dist/table/index.js +23 -0
- package/dist/table/index.js.map +1 -1
- package/dist/table/rows.d.ts +3 -2
- package/dist/table/rows.d.ts.map +1 -1
- package/dist/table/rows.js +20 -6
- package/dist/table/rows.js.map +1 -1
- package/dist/terminal/backends.d.ts +4 -1
- package/dist/terminal/backends.d.ts.map +1 -1
- package/dist/terminal/backends.js.map +1 -1
- package/dist/terminal/index.d.ts +18 -7
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +32 -11
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/vt/pty.d.ts +13 -2
- package/dist/terminal/vt/pty.d.ts.map +1 -1
- package/dist/terminal/vt/pty.js +18 -3
- package/dist/terminal/vt/pty.js.map +1 -1
- package/dist/terminal-output/index.d.ts.map +1 -1
- package/dist/terminal-output/index.js +6 -1
- package/dist/terminal-output/index.js.map +1 -1
- package/dist/three/canvas.d.ts.map +1 -1
- package/dist/three/canvas.js +85 -7
- package/dist/three/canvas.js.map +1 -1
- package/dist/three/events.d.ts +22 -4
- package/dist/three/events.d.ts.map +1 -1
- package/dist/three/events.js +34 -4
- package/dist/three/events.js.map +1 -1
- package/dist/tree/index.d.ts.map +1 -1
- package/dist/tree/index.js +17 -0
- package/dist/tree/index.js.map +1 -1
- package/package.json +59 -31
- package/src/charts/node.ts +68 -28
- package/src/charts/render.ts +120 -30
- package/src/code/index.ts +3 -1
- package/src/code-editor/completion.ts +3 -2
- package/src/code-editor/index.ts +19 -0
- package/src/code-editor/node.ts +1574 -184
- package/src/code-language/languages/glsl.ts +12 -2
- package/src/code-language/languages/javascript.ts +21 -4
- package/src/code-language/languages/json.ts +12 -2
- package/src/code-language/languages/shell.ts +12 -2
- package/src/code-language/languages/sql.ts +22 -2
- package/src/code-language/stream.ts +257 -17
- package/src/code-language/types.ts +5 -4
- package/src/codeblock/index.ts +20 -3
- package/src/embed/client.ts +102 -8
- package/src/embed/index.ts +2 -0
- package/src/flow/draw.ts +167 -41
- package/src/flow/gl/color.ts +85 -0
- package/src/flow/gl/index.ts +342 -0
- package/src/flow/gl/pack.ts +585 -0
- package/src/flow/gl/renderer.ts +679 -0
- package/src/flow/gl/shaders.ts +371 -0
- package/src/flow/gl/text.ts +1295 -0
- package/src/flow/index.ts +794 -67
- package/src/flow/node.ts +2496 -1256
- package/src/flow/paint.ts +511 -0
- package/src/flow/paths.ts +80 -15
- package/src/flow/scene.ts +2199 -0
- package/src/flow/types.ts +113 -1
- package/src/html/controls.ts +73 -2
- package/src/html/css/calc.ts +385 -0
- package/src/html/css/cascade.ts +1403 -62
- package/src/html/css/color.ts +755 -0
- package/src/html/css/content.ts +417 -0
- package/src/html/css/counter-styles.ts +973 -0
- package/src/html/css/decode.ts +130 -0
- package/src/html/css/parse.ts +1514 -160
- package/src/html/css/style.ts +4102 -222
- package/src/html/css/ua.ts +58 -12
- package/src/html/css/values.ts +150 -50
- package/src/html/css/vars.ts +348 -0
- package/src/html/dom.ts +284 -17
- package/src/html/fonts.ts +641 -0
- package/src/html/gif.ts +233 -0
- package/src/html/index.ts +180 -20
- package/src/html/layout/block.ts +3456 -230
- package/src/html/layout/boxes.ts +2100 -190
- package/src/html/layout/cache.ts +289 -0
- package/src/html/layout/collapse.ts +317 -0
- package/src/html/layout/css-grid.ts +1063 -0
- package/src/html/layout/flex.ts +1115 -106
- package/src/html/layout/floats.ts +128 -14
- package/src/html/layout/grid-lines.ts +141 -0
- package/src/html/layout/grid.ts +181 -0
- package/src/html/layout/inline.ts +3943 -192
- package/src/html/layout/shaping.ts +140 -0
- package/src/html/layout/table.ts +696 -186
- package/src/html/layout/tracks.ts +437 -0
- package/src/html/node.ts +1486 -121
- package/src/html/paint.ts +4293 -169
- package/src/html/resources.ts +161 -50
- package/src/html/surfaces.ts +81 -0
- package/src/html/svg.ts +493 -0
- package/src/html/url.ts +86 -0
- package/src/index.ts +4 -0
- package/src/internal/collate.ts +12 -0
- package/src/{markdown → internal/markdown}/ast.ts +35 -7
- package/src/{markdown → internal/markdown}/parse.ts +288 -35
- package/src/internal/markdown/stringify.ts +772 -0
- package/src/internal/prewarm.ts +45 -0
- package/src/internal/scroll.ts +5 -3
- package/src/internal/sdf.ts +227 -0
- package/src/internal/splice.ts +25 -0
- package/src/internal/window.ts +36 -5
- package/src/maps/anchors.ts +913 -0
- package/src/maps/controller.ts +1084 -0
- package/src/maps/gl/buckets.ts +461 -0
- package/src/maps/gl/build-worker.ts +66 -0
- package/src/maps/gl/color.ts +62 -0
- package/src/maps/gl/cover.ts +205 -0
- package/src/maps/gl/index.ts +23 -0
- package/src/maps/gl/markers.ts +83 -0
- package/src/maps/gl/overlays.ts +379 -0
- package/src/maps/gl/pane.ts +42 -0
- package/src/maps/gl/placement.ts +968 -0
- package/src/maps/gl/renderer.ts +1660 -0
- package/src/maps/gl/shaders.ts +489 -0
- package/src/maps/gl/store.ts +615 -0
- package/src/maps/gl/text.ts +868 -0
- package/src/maps/gl/view.ts +1489 -0
- package/src/maps/icons.ts +183 -0
- package/src/maps/index.ts +274 -81
- package/src/maps/labels.ts +457 -144
- package/src/maps/node.ts +832 -665
- package/src/maps/overlay.ts +148 -43
- package/src/maps/paint.ts +3 -0
- package/src/maps/proj.ts +118 -0
- package/src/maps/renderer.ts +155 -0
- package/src/maps/sources.ts +43 -6
- package/src/maps/style.ts +32 -0
- package/src/maps/styles.ts +215 -4
- package/src/maps/theme.ts +51 -0
- package/src/maps/tiles.ts +253 -62
- package/src/maps/types.ts +213 -21
- package/src/markdown/expressions.ts +5 -1
- package/src/markdown/index.ts +163 -78
- package/src/markdown/spans.ts +6 -1
- package/src/media-player/index.ts +18 -4
- package/src/qml/containers.tsx +6 -1
- package/src/qml/interaction.tsx +42 -5
- package/src/qml/layouts.tsx +21 -6
- package/src/qml/qtquick.tsx +9 -2
- package/src/qml/react.tsx +7 -5
- package/src/reorder/clock.ts +27 -0
- package/src/reorder/index.ts +100 -27
- package/src/rich-text-editor/clipboard.ts +439 -0
- package/src/rich-text-editor/collab.ts +91 -0
- package/src/rich-text-editor/commands.ts +448 -0
- package/src/rich-text-editor/dom-globals.d.ts +23 -0
- package/src/rich-text-editor/drag.ts +211 -0
- package/src/rich-text-editor/html.ts +619 -0
- package/src/rich-text-editor/index.ts +1364 -0
- package/src/rich-text-editor/inline.ts +345 -0
- package/src/rich-text-editor/keymap.ts +510 -0
- package/src/rich-text-editor/keys.ts +228 -0
- package/src/rich-text-editor/look.ts +427 -0
- package/src/rich-text-editor/markdown.ts +653 -0
- package/src/rich-text-editor/marks.ts +101 -0
- package/src/rich-text-editor/nodes.ts +497 -0
- package/src/rich-text-editor/render.ts +837 -0
- package/src/rich-text-editor/schema.ts +431 -0
- package/src/rich-text-editor/store.ts +39 -0
- package/src/rich-text-editor/suggest.ts +597 -0
- package/src/rich-text-editor/tables.ts +272 -0
- package/src/rich-text-editor/toolbar.ts +724 -0
- package/src/rich-text-editor/view.ts +2287 -0
- package/src/rich-text-editor/virtual.ts +372 -0
- package/src/richtext/node.ts +294 -44
- package/src/richtext/runs.ts +23 -12
- package/src/table/index.ts +29 -0
- package/src/table/rows.ts +18 -8
- package/src/terminal/backends.ts +4 -1
- package/src/terminal/index.ts +47 -15
- package/src/terminal/vt/pty.ts +16 -2
- package/src/terminal-output/index.ts +6 -1
- package/src/three/canvas.ts +102 -8
- package/src/three/events.ts +49 -9
- package/src/tree/index.ts +19 -0
- package/dist/markdown/ast.d.ts.map +0 -1
- package/dist/markdown/ast.js +0 -17
- package/dist/markdown/ast.js.map +0 -1
- package/dist/markdown/parse.d.ts.map +0 -1
- package/dist/markdown/parse.js.map +0 -1
- package/dist/markdown/tags.d.ts.map +0 -1
- package/dist/markdown/tags.js.map +0 -1
- /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.js +0 -0
- /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
|
|
31
|
-
import {
|
|
32
|
-
import type {
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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,
|
|
98
|
-
y1 = Math.min(y1,
|
|
99
|
-
x2 = Math.max(x2,
|
|
100
|
-
y2 = Math.max(y2,
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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.
|
|
137
|
-
|
|
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'
|
|
148
|
-
|
|
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
|
-
|
|
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 (
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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 === '
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
|
306
|
-
if (
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
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.
|
|
323
|
-
ctx.
|
|
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
|
-
|
|
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
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
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:
|
|
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
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
const
|
|
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(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
marker.
|
|
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
|
|
458
|
-
const
|
|
459
|
-
const
|
|
460
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
604
|
-
const layoutTo = text
|
|
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,
|