@react-x11/components 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/charts/node.d.ts +11 -1
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +44 -14
- 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 +27 -24
- 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/index.d.ts +1 -1
- package/dist/code-language/index.d.ts.map +1 -1
- package/dist/code-language/index.js +1 -1
- package/dist/code-language/index.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/runs.d.ts +31 -0
- package/dist/code-language/runs.d.ts.map +1 -1
- package/dist/code-language/runs.js +105 -0
- package/dist/code-language/runs.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 +26 -2
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +57 -4
- package/dist/codeblock/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 +262 -6
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +1373 -67
- 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 +501 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3729 -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 +65 -16
- 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 +3077 -219
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +367 -17
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +1742 -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 +154 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +3598 -175
- 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 +169 -4
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +1429 -137
- 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 +3635 -107
- 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 +442 -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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/codelines.d.ts +42 -0
- package/dist/internal/codelines.d.ts.map +1 -0
- package/dist/internal/codelines.js +170 -0
- package/dist/internal/codelines.js.map +1 -0
- 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/internal/markdown/ast.d.ts +11 -0
- package/dist/internal/markdown/ast.d.ts.map +1 -1
- package/dist/internal/markdown/parse.d.ts.map +1 -1
- package/dist/internal/markdown/parse.js +109 -8
- package/dist/internal/markdown/parse.js.map +1 -1
- 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/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 +6 -1
- package/dist/maps/anchors.d.ts.map +1 -1
- package/dist/maps/anchors.js +260 -1
- package/dist/maps/anchors.js.map +1 -1
- package/dist/maps/controller.d.ts +57 -0
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +97 -18
- package/dist/maps/controller.js.map +1 -1
- package/dist/maps/gl/buckets.d.ts +5 -1
- package/dist/maps/gl/buckets.d.ts.map +1 -1
- package/dist/maps/gl/buckets.js +5 -2
- package/dist/maps/gl/buckets.js.map +1 -1
- package/dist/maps/gl/build-worker.d.ts +2 -0
- package/dist/maps/gl/build-worker.d.ts.map +1 -1
- package/dist/maps/gl/build-worker.js +1 -1
- package/dist/maps/gl/build-worker.js.map +1 -1
- package/dist/maps/gl/cover.d.ts +23 -4
- package/dist/maps/gl/cover.d.ts.map +1 -1
- package/dist/maps/gl/cover.js +61 -48
- package/dist/maps/gl/cover.js.map +1 -1
- package/dist/maps/gl/placement.d.ts +21 -8
- package/dist/maps/gl/placement.d.ts.map +1 -1
- package/dist/maps/gl/placement.js +133 -58
- package/dist/maps/gl/placement.js.map +1 -1
- package/dist/maps/gl/renderer.d.ts +36 -4
- package/dist/maps/gl/renderer.d.ts.map +1 -1
- package/dist/maps/gl/renderer.js +81 -24
- package/dist/maps/gl/renderer.js.map +1 -1
- package/dist/maps/gl/shaders.d.ts +18 -17
- package/dist/maps/gl/shaders.d.ts.map +1 -1
- package/dist/maps/gl/shaders.js +34 -32
- package/dist/maps/gl/shaders.js.map +1 -1
- package/dist/maps/gl/store.d.ts +10 -0
- package/dist/maps/gl/store.d.ts.map +1 -1
- package/dist/maps/gl/store.js +47 -4
- package/dist/maps/gl/store.js.map +1 -1
- package/dist/maps/gl/text.d.ts +153 -24
- package/dist/maps/gl/text.d.ts.map +1 -1
- package/dist/maps/gl/text.js +379 -63
- package/dist/maps/gl/text.js.map +1 -1
- package/dist/maps/gl/view.d.ts +293 -2
- package/dist/maps/gl/view.d.ts.map +1 -1
- package/dist/maps/gl/view.js +317 -82
- package/dist/maps/gl/view.js.map +1 -1
- 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 +2 -0
- package/dist/maps/index.d.ts.map +1 -1
- package/dist/maps/index.js +2 -0
- package/dist/maps/index.js.map +1 -1
- package/dist/maps/labels.d.ts +56 -4
- package/dist/maps/labels.d.ts.map +1 -1
- package/dist/maps/labels.js +213 -46
- package/dist/maps/labels.js.map +1 -1
- package/dist/maps/node.d.ts +17 -0
- package/dist/maps/node.d.ts.map +1 -1
- package/dist/maps/node.js +64 -30
- package/dist/maps/node.js.map +1 -1
- package/dist/maps/proj.d.ts +48 -0
- package/dist/maps/proj.d.ts.map +1 -1
- package/dist/maps/proj.js +73 -0
- package/dist/maps/proj.js.map +1 -1
- package/dist/maps/sources.d.ts +5 -3
- package/dist/maps/sources.d.ts.map +1 -1
- package/dist/maps/sources.js +5 -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/tiles.d.ts +31 -11
- package/dist/maps/tiles.d.ts.map +1 -1
- package/dist/maps/tiles.js +61 -27
- package/dist/maps/tiles.js.map +1 -1
- package/dist/maps/types.d.ts +28 -4
- package/dist/maps/types.d.ts.map +1 -1
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +209 -68
- package/dist/markdown/index.js.map +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/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/index.d.ts +0 -11
- package/dist/rich-text-editor/index.d.ts.map +1 -1
- package/dist/rich-text-editor/index.js +38 -9
- package/dist/rich-text-editor/index.js.map +1 -1
- package/dist/rich-text-editor/keymap.d.ts.map +1 -1
- package/dist/rich-text-editor/keymap.js +2 -1
- package/dist/rich-text-editor/keymap.js.map +1 -1
- package/dist/rich-text-editor/keys.d.ts +17 -6
- package/dist/rich-text-editor/keys.d.ts.map +1 -1
- package/dist/rich-text-editor/keys.js +135 -26
- package/dist/rich-text-editor/keys.js.map +1 -1
- package/dist/rich-text-editor/markdown.d.ts.map +1 -1
- package/dist/rich-text-editor/markdown.js +30 -1
- package/dist/rich-text-editor/markdown.js.map +1 -1
- 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/render.d.ts.map +1 -1
- package/dist/rich-text-editor/render.js +28 -10
- package/dist/rich-text-editor/render.js.map +1 -1
- package/dist/rich-text-editor/replace.d.ts +13 -0
- package/dist/rich-text-editor/replace.d.ts.map +1 -0
- package/dist/rich-text-editor/replace.js +64 -0
- package/dist/rich-text-editor/replace.js.map +1 -0
- package/dist/rich-text-editor/toolbar.d.ts.map +1 -1
- package/dist/rich-text-editor/toolbar.js +1 -1
- package/dist/rich-text-editor/toolbar.js.map +1 -1
- package/dist/rich-text-editor/virtual.d.ts.map +1 -1
- package/dist/rich-text-editor/virtual.js +17 -0
- package/dist/rich-text-editor/virtual.js.map +1 -1
- package/dist/richtext/node.d.ts +46 -2
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +187 -4
- 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/index.d.ts.map +1 -1
- package/dist/terminal/index.js +4 -0
- 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 +103 -23
- package/dist/terminal-output/index.js.map +1 -1
- package/dist/terminal-output/runs.d.ts +4 -1
- package/dist/terminal-output/runs.d.ts.map +1 -1
- package/dist/terminal-output/runs.js +5 -1
- package/dist/terminal-output/runs.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 +42 -33
- package/src/charts/node.ts +62 -26
- package/src/charts/render.ts +120 -30
- package/src/code/index.ts +36 -32
- 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/index.ts +1 -1
- 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/runs.ts +111 -1
- package/src/code-language/stream.ts +257 -17
- package/src/code-language/types.ts +5 -4
- package/src/codeblock/index.ts +68 -4
- 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 +1481 -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 +4131 -223
- package/src/html/css/ua.ts +66 -17
- 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 +3560 -208
- package/src/html/layout/boxes.ts +2141 -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 +4187 -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 +1585 -156
- package/src/html/paint.ts +4582 -267
- package/src/html/resources.ts +161 -50
- package/src/html/surfaces.ts +81 -0
- package/src/html/svg.ts +504 -0
- package/src/html/url.ts +86 -0
- package/src/index.ts +2 -0
- package/src/internal/codelines.ts +200 -0
- package/src/internal/collate.ts +12 -0
- package/src/internal/markdown/ast.ts +11 -0
- package/src/internal/markdown/parse.ts +154 -11
- package/src/internal/prewarm.ts +45 -0
- 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 +307 -1
- package/src/maps/controller.ts +116 -31
- package/src/maps/gl/buckets.ts +5 -1
- package/src/maps/gl/build-worker.ts +13 -2
- package/src/maps/gl/cover.ts +79 -44
- package/src/maps/gl/placement.ts +221 -69
- package/src/maps/gl/renderer.ts +104 -20
- package/src/maps/gl/shaders.ts +35 -32
- package/src/maps/gl/store.ts +66 -17
- package/src/maps/gl/text.ts +444 -71
- package/src/maps/gl/view.ts +361 -96
- package/src/maps/icons.ts +183 -0
- package/src/maps/index.ts +4 -0
- package/src/maps/labels.ts +269 -49
- package/src/maps/node.ts +82 -49
- package/src/maps/proj.ts +87 -0
- package/src/maps/sources.ts +5 -3
- package/src/maps/style.ts +32 -0
- package/src/maps/styles.ts +215 -4
- package/src/maps/tiles.ts +64 -31
- package/src/maps/types.ts +28 -4
- package/src/markdown/index.ts +293 -92
- package/src/markdown/spans.ts +5 -0
- 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/index.ts +37 -12
- package/src/rich-text-editor/keymap.ts +1 -1
- package/src/rich-text-editor/keys.ts +135 -26
- package/src/rich-text-editor/markdown.ts +36 -1
- package/src/rich-text-editor/marks.ts +101 -0
- package/src/rich-text-editor/render.ts +49 -12
- package/src/rich-text-editor/replace.ts +85 -0
- package/src/rich-text-editor/toolbar.ts +1 -1
- package/src/rich-text-editor/virtual.ts +25 -0
- package/src/richtext/node.ts +235 -14
- package/src/richtext/runs.ts +23 -12
- package/src/table/index.ts +29 -0
- package/src/table/rows.ts +18 -8
- package/src/terminal/index.ts +4 -0
- package/src/terminal/vt/pty.ts +16 -2
- package/src/terminal-output/index.ts +129 -31
- package/src/terminal-output/runs.ts +16 -2
- package/src/three/canvas.ts +102 -8
- package/src/three/events.ts +49 -9
- package/src/tree/index.ts +19 -0
package/src/html/node.ts
CHANGED
|
@@ -48,27 +48,61 @@ import type { Style } from 'react-x11/style';
|
|
|
48
48
|
import type { Element } from 'domhandler';
|
|
49
49
|
|
|
50
50
|
import { codePointAtOffset, codeUnitOffsets } from '../internal/text.js';
|
|
51
|
-
import { attr, HtmlSource, isElement, tagOf } from './dom.js';
|
|
51
|
+
import { attr, HtmlSource, imageUrlOf, isElement, tagOf } from './dom.js';
|
|
52
52
|
import type { Document } from './dom.js';
|
|
53
53
|
import { Cascade } from './css/cascade.js';
|
|
54
|
-
import {
|
|
54
|
+
import type { MetricFace } from './css/cascade.js';
|
|
55
|
+
import { mediaMatches, parseStylesheet } from './css/parse.js';
|
|
55
56
|
import type { Stylesheet } from './css/parse.js';
|
|
56
57
|
import { uaStylesheet } from './css/ua.js';
|
|
57
|
-
import type { RootLook } from './css/style.js';
|
|
58
|
-
import {
|
|
59
|
-
|
|
58
|
+
import type { ComputedStyle, RootLook } from './css/style.js';
|
|
59
|
+
import {
|
|
60
|
+
BOX_RAISES,
|
|
61
|
+
buildBoxes,
|
|
62
|
+
CONTENT_IMAGES,
|
|
63
|
+
LINE_BOX_RAISES,
|
|
64
|
+
SHIFTED_LINES,
|
|
65
|
+
} from './layout/boxes.js';
|
|
66
|
+
import type {
|
|
67
|
+
Box,
|
|
68
|
+
BoxTree,
|
|
69
|
+
LineBox,
|
|
70
|
+
LineText,
|
|
71
|
+
ReplacedKind,
|
|
72
|
+
TextLayoutLike,
|
|
73
|
+
} from './layout/boxes.js';
|
|
60
74
|
import { layoutDocument } from './layout/block.js';
|
|
75
|
+
import { TextLayoutCache } from './layout/cache.js';
|
|
76
|
+
import { shapingSafe } from './layout/shaping.js';
|
|
77
|
+
import { SurfaceCache } from './surfaces.js';
|
|
78
|
+
import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
|
|
61
79
|
import type { FontsLike } from './layout/inline.js';
|
|
80
|
+
import type { TextRun } from '../richtext/index.js';
|
|
62
81
|
// Through the inline module rather than a second cache: the offsets table for
|
|
63
82
|
// a layout is built once, on the first selection that needs it.
|
|
64
|
-
import {
|
|
83
|
+
import {
|
|
84
|
+
documentOffsetOf,
|
|
85
|
+
layoutOffsetOf,
|
|
86
|
+
layoutOffsets as layoutOffsetsOf,
|
|
87
|
+
} from './layout/inline.js';
|
|
65
88
|
import { lineBands as bandsFor } from '../richtext/runs.js';
|
|
66
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
clipsOverflow,
|
|
91
|
+
containingBlockOf,
|
|
92
|
+
hasRect,
|
|
93
|
+
holds,
|
|
94
|
+
holdsAbsolute,
|
|
95
|
+
paintDocument,
|
|
96
|
+
queryChildIndex,
|
|
97
|
+
} from './paint.js';
|
|
67
98
|
import type { PaintContext } from './paint.js';
|
|
68
99
|
import { controlRectsOf, measureControl } from './controls.js';
|
|
69
|
-
import type { ControlRect } from './controls.js';
|
|
100
|
+
import type { BareField, ControlRect } from './controls.js';
|
|
70
101
|
import { ResourceStore } from './resources.js';
|
|
71
102
|
import type { ResourceRequest, ResourceResult } from './resources.js';
|
|
103
|
+
import { WebFonts } from './fonts.js';
|
|
104
|
+
import type { DeclaredFace } from './fonts.js';
|
|
105
|
+
import { resolveUrl, UrlResolver } from './url.js';
|
|
72
106
|
|
|
73
107
|
/** The element name — registration key, `node.kind` and JSX tag alike. */
|
|
74
108
|
export const ELEMENT = 'htmlview';
|
|
@@ -95,6 +129,12 @@ export interface HtmlViewProps {
|
|
|
95
129
|
complete?: boolean;
|
|
96
130
|
/** Author stylesheets applied after the document's own. */
|
|
97
131
|
stylesheet?: string | string[];
|
|
132
|
+
/** The encoding the host decoded `source` from: what a stylesheet handed
|
|
133
|
+
* over as bytes falls back to. */
|
|
134
|
+
charset?: string;
|
|
135
|
+
/** The URL the document came from, which its relative URLs resolve
|
|
136
|
+
* against — see `url.ts`. */
|
|
137
|
+
baseUrl?: string | null;
|
|
98
138
|
look: RootLook;
|
|
99
139
|
selectionColor?: string;
|
|
100
140
|
onResource?: (
|
|
@@ -118,11 +158,48 @@ export function registerHtmlView(): void {
|
|
|
118
158
|
// `source` and `look` are this element's own vocabulary and neither is a
|
|
119
159
|
// style name today; declaring them keeps the DEV flat-style-prop
|
|
120
160
|
// assertion honest if core's vocabulary grows underneath us.
|
|
121
|
-
semanticNames: [
|
|
161
|
+
semanticNames: [
|
|
162
|
+
'source',
|
|
163
|
+
'look',
|
|
164
|
+
'stylesheet',
|
|
165
|
+
'charset',
|
|
166
|
+
'complete',
|
|
167
|
+
'baseUrl',
|
|
168
|
+
],
|
|
122
169
|
childrenAllowed: false,
|
|
123
170
|
});
|
|
124
171
|
}
|
|
125
172
|
|
|
173
|
+
/** How many widths' sizes a document keeps (`HtmlViewNode._sizeAt`): the
|
|
174
|
+
* two a frame of a resize asks, and a little slack for a drag that turns
|
|
175
|
+
* back. */
|
|
176
|
+
const SIZES_KEPT = 4;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A size in the whole pixels core lays out in, rounded up — less the float
|
|
180
|
+
* noise a sum of Yoga's single-precision positions carries: a page exactly
|
|
181
|
+
* `100vh` tall came to 737.0000076 under a 737-pixel viewport, and a
|
|
182
|
+
* rounding that took it for 738 scrolled it by a pixel under a scrollbar.
|
|
183
|
+
*/
|
|
184
|
+
function wholePixels(n: number): number {
|
|
185
|
+
return Math.max(0, Math.ceil(n - 1 / 64));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** How deep `@import`s are followed: an import in an import in an import is
|
|
189
|
+
* a stylesheet; sixteen of them is a loop that changes its URL each time. */
|
|
190
|
+
const MAX_IMPORT_DEPTH = 16;
|
|
191
|
+
|
|
192
|
+
/** A scheme at the start: an absolute URL, a base that means something. */
|
|
193
|
+
const ABSOLUTE_URL = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
194
|
+
|
|
195
|
+
/** What warming a document's faces needs of the fonts: ntk's
|
|
196
|
+
* `FontManager#prewarm`, which names faces from the release that added
|
|
197
|
+
* them (8.14) and warms the family's four before it. An engine without it
|
|
198
|
+
* has nothing to look up. */
|
|
199
|
+
interface WarmingFonts {
|
|
200
|
+
prewarm?(family: string, faces?: { weight: number; style: string }[]): void;
|
|
201
|
+
}
|
|
202
|
+
|
|
126
203
|
/** What changed, and therefore how far back up the pipeline to go. */
|
|
127
204
|
const enum Stale {
|
|
128
205
|
Nothing = 0,
|
|
@@ -134,16 +211,43 @@ const enum Stale {
|
|
|
134
211
|
|
|
135
212
|
export class HtmlViewNode extends Node {
|
|
136
213
|
private _source = new HtmlSource();
|
|
214
|
+
/** Where the document's relative URLs resolve: its `<base href>` against
|
|
215
|
+
* the `baseUrl` prop, or the prop, or nowhere (`url.ts`). */
|
|
216
|
+
private _urls = new UrlResolver();
|
|
137
217
|
private _resources: ResourceStore;
|
|
218
|
+
/** The families the document's `@font-face` rules declare (`fonts.ts`). */
|
|
219
|
+
private _webFonts: WebFonts;
|
|
220
|
+
/** The sheets the last restyle read, and the cascade built from them. */
|
|
221
|
+
private _sheetsRead: SheetsRead | null = null;
|
|
222
|
+
/** Blurred shadows, drawn once each. */
|
|
223
|
+
private _shadowCache: SurfaceCache | null = null;
|
|
138
224
|
private _cascade: Cascade | null = null;
|
|
139
225
|
private _tree: BoxTree | null = null;
|
|
226
|
+
/** The faces `_warmFaces` has asked the font matcher for, and of which
|
|
227
|
+
* fonts. */
|
|
228
|
+
private _warmedFaces = new Set<string>();
|
|
229
|
+
private _warmedFor: unknown = null;
|
|
140
230
|
private _stale: Stale = Stale.Everything;
|
|
141
231
|
/** The width the document was last laid out at. Not `_laidOutAt`: core's
|
|
142
232
|
* `Node` has a method of that name, which a field would hide. */
|
|
143
233
|
private _laidOutWidth = -1;
|
|
144
234
|
private _mediaBand = -1;
|
|
235
|
+
/** The viewport the box tree's styles were computed at, for the ones
|
|
236
|
+
* that read it (`vw`, `vh`). */
|
|
237
|
+
private _styledWidth = -1;
|
|
238
|
+
private _styledHeight = -1;
|
|
145
239
|
private _documentHeight = 0;
|
|
146
240
|
private _documentWidth = 0;
|
|
241
|
+
/** The viewport height the document was last laid out under, and whether
|
|
242
|
+
* that layout read it (`LayoutResult.readsViewportHeight`). */
|
|
243
|
+
private _laidOutUnder = -1;
|
|
244
|
+
private _layoutReadsViewport = false;
|
|
245
|
+
/** Whether core is asking for this element's size right now — inside
|
|
246
|
+
* its layout pass, where a node must not ask to be measured again. */
|
|
247
|
+
private _measuring = false;
|
|
248
|
+
/** What the document came to at the widths it was last laid out at, while
|
|
249
|
+
* nothing a layout reads has changed (`_sizeAt`). */
|
|
250
|
+
private _sizes = new Map<number, { width: number; height: number }>();
|
|
147
251
|
private _textPoints: number[] | null = null;
|
|
148
252
|
/** Whether code points and code units are the same index — true unless
|
|
149
253
|
* the text carries surrogate pairs. null until checked. */
|
|
@@ -155,14 +259,32 @@ export class HtmlViewNode extends Node {
|
|
|
155
259
|
|
|
156
260
|
constructor(props: Record<string, unknown>, app: NtkApp) {
|
|
157
261
|
super(ELEMENT, props, app);
|
|
262
|
+
const ask = (request: ResourceRequest) =>
|
|
263
|
+
this._props().onResource?.(request) ?? null;
|
|
158
264
|
this._resources = new ResourceStore(
|
|
159
|
-
|
|
265
|
+
ask,
|
|
266
|
+
(what) => {
|
|
267
|
+
// A late stylesheet is a new cascade: its rules, what it imports,
|
|
268
|
+
// the faces it declares. A late image changes intrinsic sizes, so
|
|
269
|
+
// the box tree is what has to be rebuilt — not merely repainted.
|
|
270
|
+
// Either arriving after first paint is the ordinary case, not an
|
|
271
|
+
// error path: a host on a network answers every request that way.
|
|
272
|
+
this._invalidate(what === 'stylesheet' ? Stale.Style : Stale.Boxes);
|
|
273
|
+
},
|
|
274
|
+
this._urls,
|
|
275
|
+
);
|
|
276
|
+
this._webFonts = new WebFonts(
|
|
277
|
+
app,
|
|
278
|
+
ask,
|
|
160
279
|
() => {
|
|
161
|
-
// A
|
|
162
|
-
//
|
|
163
|
-
//
|
|
280
|
+
// A face arrived: the family lists change, so the styles do, and
|
|
281
|
+
// every text layout kept from before was set in the face it
|
|
282
|
+
// replaces — under the same list, where a family of the document's
|
|
283
|
+
// had loaded one weight and now has another.
|
|
284
|
+
this._layouts = null;
|
|
164
285
|
this._invalidate(Stale.Boxes);
|
|
165
286
|
},
|
|
287
|
+
'sans-serif',
|
|
166
288
|
);
|
|
167
289
|
this._read();
|
|
168
290
|
}
|
|
@@ -256,9 +378,24 @@ export class HtmlViewNode extends Node {
|
|
|
256
378
|
props.onDocument?.(this._source.document);
|
|
257
379
|
this._reportedDomRevision = props.domRevision ?? 0;
|
|
258
380
|
}
|
|
381
|
+
this._updateBase();
|
|
259
382
|
this._sweep();
|
|
260
383
|
}
|
|
261
384
|
|
|
385
|
+
/**
|
|
386
|
+
* The document's base URL: its first `<base href>`, resolved against the
|
|
387
|
+
* `baseUrl` prop, or the prop — and only an absolute one, since a relative
|
|
388
|
+
* base resolves nothing. Everything resolved against the old one is
|
|
389
|
+
* stale when it moves, the sheets' URLs with it, so the cascade is.
|
|
390
|
+
*/
|
|
391
|
+
private _updateBase(): void {
|
|
392
|
+
const given = this._props().baseUrl || null;
|
|
393
|
+
const href = this._source.facts().base;
|
|
394
|
+
let base = href ? resolveUrl(href, given) : given;
|
|
395
|
+
if (base && !ABSOLUTE_URL.test(base)) base = null;
|
|
396
|
+
if (this._urls.setBase(base)) this._invalidate(Stale.Style);
|
|
397
|
+
}
|
|
398
|
+
|
|
262
399
|
/**
|
|
263
400
|
* Hand the document's own declarations to the host: every `<script>` once,
|
|
264
401
|
* and every resource that has not been asked for.
|
|
@@ -286,7 +423,7 @@ export class HtmlViewNode extends Node {
|
|
|
286
423
|
}
|
|
287
424
|
for (const el of facts.resources) {
|
|
288
425
|
const tag = tagOf(el);
|
|
289
|
-
const url = tag === 'link' ? attr(el, 'href') :
|
|
426
|
+
const url = tag === 'link' ? attr(el, 'href') : imageUrlOf(el);
|
|
290
427
|
if (!url) continue;
|
|
291
428
|
this._resources.request({
|
|
292
429
|
url,
|
|
@@ -296,49 +433,190 @@ export class HtmlViewNode extends Node {
|
|
|
296
433
|
}
|
|
297
434
|
}
|
|
298
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Ask for every `background-image` the styles name. Known only once the
|
|
438
|
+
* cascade has run, where an `<img>` is known from the markup; the store
|
|
439
|
+
* asks once a URL, so a tree built again asks nothing new.
|
|
440
|
+
*/
|
|
441
|
+
private _requestBackgrounds(tree: BoxTree): void {
|
|
442
|
+
for (const box of tree.backgrounds) {
|
|
443
|
+
if (!box.el) continue;
|
|
444
|
+
// each layer's, where there is more than one
|
|
445
|
+
for (const url of box.style.backgroundImages ?? [
|
|
446
|
+
box.style.backgroundImage,
|
|
447
|
+
]) {
|
|
448
|
+
if (typeof url === 'string') {
|
|
449
|
+
this._resources.request({ url, kind: 'image', element: box.el });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const border = box.style.borderImage.source;
|
|
453
|
+
if (typeof border === 'string') {
|
|
454
|
+
this._resources.request({
|
|
455
|
+
url: border,
|
|
456
|
+
kind: 'image',
|
|
457
|
+
element: box.el,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// and every image generated content names, which is only known there
|
|
462
|
+
// too
|
|
463
|
+
for (const { url, element } of tree.contentImages) {
|
|
464
|
+
this._resources.request({ url, kind: 'image', element });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Whether an image generated content names arrived as it was asked for,
|
|
469
|
+
* the tree built without its size: a host that answers at once is told
|
|
470
|
+
* nothing later, where an `<img>` is asked for before the build. */
|
|
471
|
+
private _contentImagesArrived(tree: BoxTree): boolean {
|
|
472
|
+
for (const { url, sized } of tree.contentImages) {
|
|
473
|
+
if (!sized && this._resources.imageSize(url)) return true;
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Ask the font matcher for the faces the document's text is set in, as
|
|
480
|
+
* soon as its boxes say which and while its layout is still ahead. A face
|
|
481
|
+
* nothing had warmed was a synchronous fc-match inside that layout: the
|
|
482
|
+
* benchmark report's `th`, at 600, waited 38 ms before its first paint.
|
|
483
|
+
* One child answers every face a call names, off the event loop (ntk's
|
|
484
|
+
* `FontManager#prewarm`), and a layout reaching a face still on its way
|
|
485
|
+
* takes the answer rather than asking again. Each face is asked once, by
|
|
486
|
+
* the family, weight and style the layout hands the engine.
|
|
487
|
+
*/
|
|
488
|
+
private _warmFaces(tree: BoxTree): void {
|
|
489
|
+
const fonts = this._fonts() as WarmingFonts | null;
|
|
490
|
+
if (typeof fonts?.prewarm !== 'function') return;
|
|
491
|
+
if (this._warmedFor !== fonts) {
|
|
492
|
+
this._warmedFor = fonts;
|
|
493
|
+
this._warmedFaces.clear();
|
|
494
|
+
}
|
|
495
|
+
let faces: Map<string, { weight: number; style: string }[]> | null = null;
|
|
496
|
+
for (const style of tree.textStyles) {
|
|
497
|
+
const key = `${style.fontFamily}|${style.fontWeight}|${style.fontStyle}`;
|
|
498
|
+
if (this._warmedFaces.has(key)) continue;
|
|
499
|
+
this._warmedFaces.add(key);
|
|
500
|
+
faces ??= new Map();
|
|
501
|
+
let named = faces.get(style.fontFamily);
|
|
502
|
+
if (!named) faces.set(style.fontFamily, (named = []));
|
|
503
|
+
named.push({ weight: style.fontWeight, style: style.fontStyle });
|
|
504
|
+
}
|
|
505
|
+
if (faces)
|
|
506
|
+
for (const [family, named] of faces) fonts.prewarm(family, named);
|
|
507
|
+
}
|
|
508
|
+
|
|
299
509
|
/** Rebuild the cascade — the document's sheets plus the host's. */
|
|
300
510
|
private _restyle(width: number): void {
|
|
301
511
|
const props = this._props();
|
|
302
512
|
const look = this._deviceLook();
|
|
303
|
-
const
|
|
304
|
-
|
|
513
|
+
const documentBase = this._urls.base;
|
|
514
|
+
// What the sheets are read from, in order: a `<style>`'s text or a
|
|
515
|
+
// fetched `<link>`'s, and after them the host's — each with the URL its
|
|
516
|
+
// own relative URLs resolve against: the document's for a `<style>`,
|
|
517
|
+
// the sheet's own for a `<link>`.
|
|
518
|
+
const read: SheetText[] = [];
|
|
305
519
|
for (const ref of this._source.facts().sheets) {
|
|
306
|
-
|
|
520
|
+
// A sheet handed over as bytes that names no encoding of its own is in
|
|
521
|
+
// its referrer's: a `<link charset>`, then the document's (CSS 2.1
|
|
522
|
+
// 4.4), and an import is in the encoding of the sheet importing it.
|
|
523
|
+
const linked =
|
|
307
524
|
ref.kind === 'inline'
|
|
308
|
-
?
|
|
309
|
-
: this._resources.
|
|
525
|
+
? null
|
|
526
|
+
: this._resources.stylesheet(ref.href, [
|
|
527
|
+
attr(ref.element, 'charset') || undefined,
|
|
528
|
+
props.charset,
|
|
529
|
+
]);
|
|
530
|
+
const text = ref.kind === 'inline' ? ref.text : linked?.text;
|
|
310
531
|
if (!text) continue;
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
url,
|
|
318
|
-
kind: 'stylesheet',
|
|
319
|
-
element: ref.element,
|
|
320
|
-
});
|
|
321
|
-
const imported = this._resources.stylesheetText(url);
|
|
322
|
-
if (imported) {
|
|
323
|
-
const parsed = parseStylesheet(imported, order);
|
|
324
|
-
order += parsed.rules.length + 1;
|
|
325
|
-
sheets.push(parsed);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
sheets.push(sheet);
|
|
532
|
+
const encoding = linked ? linked.encoding : props.charset;
|
|
533
|
+
const base =
|
|
534
|
+
ref.kind === 'inline'
|
|
535
|
+
? documentBase
|
|
536
|
+
: this._resources.sheetBase(ref.href);
|
|
537
|
+
read.push({ text, encoding, element: ref.element, base });
|
|
329
538
|
}
|
|
330
539
|
const extra = props.stylesheet;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
540
|
+
const extras = Array.isArray(extra) ? extra : extra ? [extra] : [];
|
|
541
|
+
const fonts = this._fonts();
|
|
542
|
+
// Sheets that read as they did last time are the same rules: an append
|
|
543
|
+
// to a streamed document, or a width across a `@media` breakpoint,
|
|
544
|
+
// restyles the elements without parsing a framework's stylesheet again
|
|
545
|
+
// or indexing its thousand rules again.
|
|
546
|
+
const kept = this._sheetsRead;
|
|
547
|
+
let faces: DeclaredFace[];
|
|
548
|
+
if (
|
|
549
|
+
kept &&
|
|
550
|
+
kept.look === look &&
|
|
551
|
+
kept.scale === this._scale &&
|
|
552
|
+
kept.fonts === fonts &&
|
|
553
|
+
this._sameSheets(kept, read, extras)
|
|
554
|
+
) {
|
|
555
|
+
this._cascade = kept.cascade;
|
|
556
|
+
kept.cascade.viewportWidth = width;
|
|
557
|
+
kept.cascade.viewportHeight = this._viewportHeight();
|
|
558
|
+
faces = kept.faces;
|
|
559
|
+
} else {
|
|
560
|
+
const sheets: Stylesheet[] = [uaStylesheet(look)];
|
|
561
|
+
const imports: ImportRead[][] = [];
|
|
562
|
+
faces = [];
|
|
563
|
+
let order = 0;
|
|
564
|
+
const layers = new Map<string, number>();
|
|
565
|
+
// A sheet takes its place in the cascade's order once everything it
|
|
566
|
+
// imports has taken theirs: `@import` is a resource like any other,
|
|
567
|
+
// and an imported sheet's rules sit *before* the importing sheet's
|
|
568
|
+
// (CSS 2.1 6.4.1), or an imported rule would win a tie against the
|
|
569
|
+
// sheet importing it.
|
|
570
|
+
const place = (sheet: Stylesheet, element: Element): void => {
|
|
571
|
+
for (const rule of sheet.rules) rule.order = order++;
|
|
572
|
+
order += 1;
|
|
573
|
+
sheets.push(sheet);
|
|
574
|
+
for (const rule of sheet.fontFaces) faces.push({ rule, element });
|
|
575
|
+
};
|
|
576
|
+
for (const { text, encoding, element, base } of read) {
|
|
577
|
+
const sheet = parseStylesheet(text, 0, layers, base);
|
|
578
|
+
const seen: ImportRead[] = [];
|
|
579
|
+
this._placeImports(sheet, encoding, element, layers, seen, place);
|
|
580
|
+
imports.push(seen);
|
|
581
|
+
place(sheet, element);
|
|
582
|
+
}
|
|
583
|
+
for (const text of extras) {
|
|
584
|
+
const sheet = parseStylesheet(text, order, layers);
|
|
585
|
+
order += sheet.rules.length + 1;
|
|
586
|
+
sheets.push(sheet);
|
|
587
|
+
}
|
|
588
|
+
this._cascade = new Cascade(
|
|
589
|
+
sheets,
|
|
590
|
+
look,
|
|
591
|
+
width,
|
|
592
|
+
this._viewportHeight(),
|
|
593
|
+
this._scale,
|
|
594
|
+
fonts ? (face) => xHeightOf(fonts, face) : null,
|
|
595
|
+
fonts ? (face) => zeroWidthOf(fonts, face) : null,
|
|
596
|
+
fonts ? (face) => normalLineOf(fonts, face) : null,
|
|
597
|
+
faces.length ? this._webFonts : null,
|
|
598
|
+
);
|
|
599
|
+
this._sheetsRead = {
|
|
600
|
+
look,
|
|
601
|
+
scale: this._scale,
|
|
602
|
+
fonts,
|
|
603
|
+
texts: read.map((r) => r.text),
|
|
604
|
+
encodings: read.map((r) => r.encoding),
|
|
605
|
+
bases: read.map((r) => r.base),
|
|
606
|
+
imports,
|
|
607
|
+
extras,
|
|
608
|
+
faces,
|
|
609
|
+
cascade: this._cascade,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
// The faces this width and scheme declare: a `@font-face` may sit in a
|
|
613
|
+
// `@media` block like any rule.
|
|
614
|
+
const cssWidth = width / this._scale;
|
|
615
|
+
this._webFonts.setFallback(look.fontFamily);
|
|
616
|
+
this._webFonts.setFaces(
|
|
617
|
+
faces.filter((f) =>
|
|
618
|
+
mediaMatches(f.rule.media, cssWidth, look.colorScheme),
|
|
619
|
+
),
|
|
342
620
|
);
|
|
343
621
|
this._cascade.setPointer({
|
|
344
622
|
hovered: new Set(this._hovered),
|
|
@@ -347,10 +625,101 @@ export class HtmlViewNode extends Node {
|
|
|
347
625
|
this._mediaBand = this._cascade.mediaBand(width);
|
|
348
626
|
}
|
|
349
627
|
|
|
628
|
+
/**
|
|
629
|
+
* Place what a sheet imports, and what each of those imports, depth
|
|
630
|
+
* first — asking for each through the seam, as a parse of the sheet has
|
|
631
|
+
* to. `seen` records every import read, in order, which is what tells the
|
|
632
|
+
* next restyle whether the sheets read the same (`_sameSheets`). A sheet
|
|
633
|
+
* that imports itself, or one of the sheets importing it, is read once.
|
|
634
|
+
*/
|
|
635
|
+
private _placeImports(
|
|
636
|
+
sheet: Stylesheet,
|
|
637
|
+
encoding: string | undefined,
|
|
638
|
+
element: Element,
|
|
639
|
+
layers: Map<string, number>,
|
|
640
|
+
seen: ImportRead[],
|
|
641
|
+
place: (sheet: Stylesheet, element: Element) => void,
|
|
642
|
+
depth = 0,
|
|
643
|
+
chain: Set<string> = new Set(),
|
|
644
|
+
): void {
|
|
645
|
+
for (const url of sheet.imports) {
|
|
646
|
+
this._resources.request({ url, kind: 'stylesheet', element });
|
|
647
|
+
const fetched = this._resources.stylesheet(url, [encoding]);
|
|
648
|
+
const base = this._resources.sheetBase(url);
|
|
649
|
+
seen.push({ url, text: fetched?.text ?? null, base });
|
|
650
|
+
const key = base ?? url;
|
|
651
|
+
if (!fetched || depth >= MAX_IMPORT_DEPTH || chain.has(key)) continue;
|
|
652
|
+
const imported = parseStylesheet(fetched.text, 0, layers, base);
|
|
653
|
+
chain.add(key);
|
|
654
|
+
this._placeImports(
|
|
655
|
+
imported,
|
|
656
|
+
fetched.encoding,
|
|
657
|
+
element,
|
|
658
|
+
layers,
|
|
659
|
+
seen,
|
|
660
|
+
place,
|
|
661
|
+
depth + 1,
|
|
662
|
+
chain,
|
|
663
|
+
);
|
|
664
|
+
chain.delete(key);
|
|
665
|
+
place(imported, element);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Whether the sheets read the same as the ones `kept` was built from,
|
|
670
|
+
* down to what each imports — asking for the imports again, as a parse
|
|
671
|
+
* of the sheet would have. */
|
|
672
|
+
private _sameSheets(
|
|
673
|
+
kept: SheetsRead,
|
|
674
|
+
read: SheetText[],
|
|
675
|
+
extras: string[],
|
|
676
|
+
): boolean {
|
|
677
|
+
if (read.length !== kept.texts.length) return false;
|
|
678
|
+
if (extras.length !== kept.extras.length) return false;
|
|
679
|
+
for (let i = 0; i < extras.length; i += 1) {
|
|
680
|
+
if (extras[i] !== kept.extras[i]) return false;
|
|
681
|
+
}
|
|
682
|
+
for (let i = 0; i < read.length; i += 1) {
|
|
683
|
+
const r = read[i];
|
|
684
|
+
if (
|
|
685
|
+
r.text !== kept.texts[i] ||
|
|
686
|
+
r.encoding !== kept.encodings[i] ||
|
|
687
|
+
r.base !== kept.bases[i]
|
|
688
|
+
) {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
// The encodings an import falls back to are its importer's, which
|
|
692
|
+
// only a parse knows; the text an import decoded to under them is what
|
|
693
|
+
// was kept, and bytes that decode differently now are a new sheet.
|
|
694
|
+
for (const { url, text, base } of kept.imports[i]) {
|
|
695
|
+
this._resources.request({
|
|
696
|
+
url,
|
|
697
|
+
kind: 'stylesheet',
|
|
698
|
+
element: r.element,
|
|
699
|
+
});
|
|
700
|
+
const fetched = this._resources.stylesheet(url, [r.encoding]);
|
|
701
|
+
if ((fetched?.text ?? null) !== text) return false;
|
|
702
|
+
if (this._resources.sheetBase(url) !== base) return false;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
|
|
350
708
|
private _viewportHeight(): number {
|
|
351
|
-
// The viewport a `vh` resolves against is the
|
|
352
|
-
// document
|
|
353
|
-
// taller with it.
|
|
709
|
+
// The viewport a `vh` resolves against is the one the document is seen
|
|
710
|
+
// through, not the document — a document taller than it does not make
|
|
711
|
+
// `100vh` taller with it. That is the box that scrolls the element,
|
|
712
|
+
// where one does: a browser's page area, below its tab strip and its
|
|
713
|
+
// toolbar, where `100vh` measured by the window put a page's footer the
|
|
714
|
+
// toolbar's height below the fold. The window, where nothing scrolls it.
|
|
715
|
+
for (let node = this.parent; node; node = node.parent) {
|
|
716
|
+
// `Scrollable`'s, and not every node's: asked the way core asks it
|
|
717
|
+
const scroller = node as { isScroller?: () => boolean };
|
|
718
|
+
if (!scroller.isScroller?.()) continue;
|
|
719
|
+
const height = node.contentBox().height;
|
|
720
|
+
if (height > 0) return height;
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
354
723
|
const root = this.root;
|
|
355
724
|
const height = root?.abs?.height;
|
|
356
725
|
return height && height > 0 ? height : 600;
|
|
@@ -358,20 +727,101 @@ export class HtmlViewNode extends Node {
|
|
|
358
727
|
|
|
359
728
|
private _fonts(): FontsLike | null {
|
|
360
729
|
const fonts = (this.app as { fonts?: FontsLike } | null)?.fonts;
|
|
361
|
-
|
|
730
|
+
// a face the engine cannot shape from costs its characters, not the
|
|
731
|
+
// document (`shaping.ts`)
|
|
732
|
+
return fonts ? shapingSafe(fonts) : null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** The text layouts the last pass made, for this one to reuse. */
|
|
736
|
+
private _layouts: TextLayoutCache | null = null;
|
|
737
|
+
|
|
738
|
+
/** The fonts a layout pass lays text out with: the engine's, through the
|
|
739
|
+
* layouts the pass before made (`TextLayoutCache`). */
|
|
740
|
+
private _layoutFonts(): FontsLike | null {
|
|
741
|
+
const fonts = this._fonts();
|
|
742
|
+
if (!fonts) return null;
|
|
743
|
+
if (this._layouts?.engine !== fonts)
|
|
744
|
+
this._layouts = new TextLayoutCache(fonts);
|
|
745
|
+
this._layouts.begin();
|
|
746
|
+
return this._layouts.fonts;
|
|
362
747
|
}
|
|
363
748
|
|
|
364
|
-
/**
|
|
749
|
+
/**
|
|
750
|
+
* Bring the pipeline up to date for a width — or, where that throws, leave
|
|
751
|
+
* the document blank. From a paint a throw is the application's end, for
|
|
752
|
+
* a document it did not write: a stack a thousand nested elements run
|
|
753
|
+
* down, or a limit of the text engine's or the server's that one more
|
|
754
|
+
* document finds. Reported once for each thing that went wrong, and tried
|
|
755
|
+
* again only when something changes, or the width does.
|
|
756
|
+
*/
|
|
365
757
|
private _prepare(width: number): void {
|
|
366
758
|
const target = Math.max(1, Math.floor(width));
|
|
759
|
+
if (this._failedAt === target && this._stale === Stale.Nothing) return;
|
|
760
|
+
try {
|
|
761
|
+
this._update(target);
|
|
762
|
+
this._failedAt = -1;
|
|
763
|
+
} catch (error) {
|
|
764
|
+
this._fail(error, target);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** The width the pipeline threw at, until something changes (`_prepare`). */
|
|
769
|
+
private _failedAt = -1;
|
|
770
|
+
private _failures: Set<string> | null = null;
|
|
771
|
+
|
|
772
|
+
private _fail(error: unknown, width: number): void {
|
|
773
|
+
this._tree = null;
|
|
774
|
+
this._stale = Stale.Nothing;
|
|
775
|
+
this._failedAt = width;
|
|
776
|
+
this._laidOutWidth = -1;
|
|
777
|
+
this._documentWidth = 0;
|
|
778
|
+
this._documentHeight = 0;
|
|
779
|
+
const message = String((error as { stack?: unknown })?.stack ?? error);
|
|
780
|
+
this._failures ??= new Set();
|
|
781
|
+
if (this._failures.has(message)) return;
|
|
782
|
+
this._failures.add(message);
|
|
783
|
+
const g = globalThis as {
|
|
784
|
+
process?: { env?: Record<string, string | undefined> };
|
|
785
|
+
console?: { error(message: string): void };
|
|
786
|
+
};
|
|
787
|
+
if (g.process?.env?.NODE_ENV === 'production') return;
|
|
788
|
+
g.console?.error(
|
|
789
|
+
'@react-x11/components: <Html> could not lay out or paint its ' +
|
|
790
|
+
`document, and leaves it blank.\n${message}`,
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private _update(target: number): void {
|
|
367
795
|
const props = this._props();
|
|
368
796
|
if ((props.domRevision ?? 0) !== this._reportedDomRevision) {
|
|
369
797
|
this._reportedDomRevision = props.domRevision ?? 0;
|
|
370
798
|
this._source.touch();
|
|
799
|
+
this._updateBase();
|
|
371
800
|
this._sweep();
|
|
372
801
|
if (this._stale < Stale.Style) this._stale = Stale.Style;
|
|
373
802
|
}
|
|
374
|
-
|
|
803
|
+
// A document that reads its viewport's height — a `vh`, a percentage
|
|
804
|
+
// height on the root, a box placed against the initial containing block
|
|
805
|
+
// — is laid out again when that height moves. One that reads none, which
|
|
806
|
+
// is most, goes on skipping layout when the window only grew taller.
|
|
807
|
+
const viewport = this._viewportHeight();
|
|
808
|
+
const viewportMoved =
|
|
809
|
+
viewport !== this._laidOutUnder && this._readsViewportHeight();
|
|
810
|
+
// the sizes other widths came to were read from what just changed
|
|
811
|
+
if (this._stale !== Stale.Nothing || viewportMoved) this._sizes.clear();
|
|
812
|
+
if (
|
|
813
|
+
this._stale === Stale.Nothing &&
|
|
814
|
+
this._laidOutWidth === target &&
|
|
815
|
+
!viewportMoved
|
|
816
|
+
) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
// Nothing but the viewport: the size core laid this element out at came
|
|
820
|
+
// from a layout under the old one, and nothing else will tell it so.
|
|
821
|
+
const onlyViewport =
|
|
822
|
+
this._stale === Stale.Nothing && this._laidOutWidth === target;
|
|
823
|
+
const wasWidth = this._documentWidth;
|
|
824
|
+
const wasHeight = this._documentHeight;
|
|
375
825
|
|
|
376
826
|
if (this._stale >= Stale.Style || !this._cascade) {
|
|
377
827
|
this._restyle(target);
|
|
@@ -387,17 +837,43 @@ export class HtmlViewNode extends Node {
|
|
|
387
837
|
const cascade = this._cascade;
|
|
388
838
|
if (!cascade) return;
|
|
389
839
|
cascade.viewportWidth = target;
|
|
390
|
-
cascade.viewportHeight =
|
|
840
|
+
cascade.viewportHeight = viewport;
|
|
841
|
+
// A `vw` or a `vh` is a number by the time a style holds it, so the
|
|
842
|
+
// styles computed for another viewport are wrong for this one: built
|
|
843
|
+
// again, where some style reads the side that moved. A document that
|
|
844
|
+
// reads neither — most — goes on skipping the cascade on a resize.
|
|
845
|
+
if (
|
|
846
|
+
this._stale < Stale.Boxes &&
|
|
847
|
+
((cascade.readsViewportWidth && this._styledWidth !== target) ||
|
|
848
|
+
(cascade.readsViewportHeight && this._styledHeight !== viewport))
|
|
849
|
+
) {
|
|
850
|
+
this._stale = Stale.Boxes;
|
|
851
|
+
}
|
|
391
852
|
|
|
392
853
|
if (this._stale >= Stale.Boxes || !this._tree) {
|
|
393
854
|
const look = this._deviceLook();
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
855
|
+
const build = () =>
|
|
856
|
+
buildBoxes(this._source.document, {
|
|
857
|
+
cascade,
|
|
858
|
+
scale: this._scale,
|
|
859
|
+
imageSize: (el) => this._resources.imageSize(imageUrlOf(el) ?? ''),
|
|
860
|
+
urlSize: (url) => this._resources.imageSize(url),
|
|
861
|
+
controlSize: (el, kind, style) =>
|
|
862
|
+
measureControl(el, kind, style, this._fonts(), look),
|
|
863
|
+
});
|
|
864
|
+
this._tree = build();
|
|
865
|
+
this._styledWidth = target;
|
|
866
|
+
this._styledHeight = viewport;
|
|
867
|
+
this._requestBackgrounds(this._tree);
|
|
868
|
+
let again = this._contentImagesArrived(this._tree);
|
|
869
|
+
// the faces the styles just asked for, of the families the document
|
|
870
|
+
// loads itself — which, answered at once, change the styles asking
|
|
871
|
+
if (this._webFonts.request(this._tree.text)) {
|
|
872
|
+
this._layouts = null;
|
|
873
|
+
again = true;
|
|
874
|
+
}
|
|
875
|
+
if (again) this._tree = build();
|
|
876
|
+
this._warmFaces(this._tree);
|
|
401
877
|
this._textPoints = null;
|
|
402
878
|
this._pointsAreUnits = null;
|
|
403
879
|
this._laidOutWidth = -1;
|
|
@@ -405,20 +881,47 @@ export class HtmlViewNode extends Node {
|
|
|
405
881
|
|
|
406
882
|
if (
|
|
407
883
|
this._tree &&
|
|
408
|
-
(this._laidOutWidth !== target ||
|
|
884
|
+
(this._laidOutWidth !== target ||
|
|
885
|
+
viewportMoved ||
|
|
886
|
+
this._stale >= Stale.Layout)
|
|
409
887
|
) {
|
|
410
888
|
const result = layoutDocument(
|
|
411
889
|
this._tree,
|
|
412
|
-
this.
|
|
890
|
+
this._layoutFonts(),
|
|
413
891
|
target,
|
|
414
|
-
|
|
892
|
+
viewport,
|
|
415
893
|
);
|
|
416
894
|
this._documentWidth = result.width;
|
|
417
895
|
this._documentHeight = result.height;
|
|
418
896
|
this._laidOutWidth = target;
|
|
897
|
+
this._laidOutUnder = viewport;
|
|
898
|
+
this._layoutReadsViewport = result.readsViewportHeight;
|
|
899
|
+
this._sizes.delete(target);
|
|
900
|
+
this._sizes.set(target, { width: result.width, height: result.height });
|
|
901
|
+
if (this._sizes.size > SIZES_KEPT) {
|
|
902
|
+
this._sizes.delete(this._sizes.keys().next().value!);
|
|
903
|
+
}
|
|
419
904
|
this._reportControls();
|
|
420
905
|
}
|
|
421
906
|
this._stale = Stale.Nothing;
|
|
907
|
+
// Read after core's layout pass — the viewport is the box around this
|
|
908
|
+
// one, whose new height only that pass decides — so it is a paint that
|
|
909
|
+
// finds it moved, and the next frame that measures this element again.
|
|
910
|
+
// Inside a measure the answer already is the new size.
|
|
911
|
+
if (
|
|
912
|
+
onlyViewport &&
|
|
913
|
+
!this._measuring &&
|
|
914
|
+
(this._documentWidth !== wasWidth || this._documentHeight !== wasHeight)
|
|
915
|
+
) {
|
|
916
|
+
this.invalidateMeasure('content');
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Whether the document as laid out reads its viewport's height. */
|
|
921
|
+
private _readsViewportHeight(): boolean {
|
|
922
|
+
return (
|
|
923
|
+
this._layoutReadsViewport || this._cascade?.readsViewportHeight === true
|
|
924
|
+
);
|
|
422
925
|
}
|
|
423
926
|
|
|
424
927
|
private _reportControls(): void {
|
|
@@ -437,6 +940,16 @@ export class HtmlViewNode extends Node {
|
|
|
437
940
|
y: r.y / s,
|
|
438
941
|
width: r.width / s,
|
|
439
942
|
height: r.height / s,
|
|
943
|
+
...(r.bare && {
|
|
944
|
+
bare: {
|
|
945
|
+
...r.bare,
|
|
946
|
+
x: r.bare.x / s,
|
|
947
|
+
y: r.bare.y / s,
|
|
948
|
+
width: r.bare.width / s,
|
|
949
|
+
height: r.bare.height / s,
|
|
950
|
+
fontSize: r.bare.fontSize / s,
|
|
951
|
+
},
|
|
952
|
+
}),
|
|
440
953
|
},
|
|
441
954
|
);
|
|
442
955
|
if (sameRects(rects, this._controls)) return;
|
|
@@ -457,13 +970,52 @@ export class HtmlViewNode extends Node {
|
|
|
457
970
|
*/
|
|
458
971
|
override measureContent({ width }: MeasureConstraints): MeasuredSize {
|
|
459
972
|
const offered = Number.isFinite(width) ? width : 800;
|
|
460
|
-
this.
|
|
973
|
+
this._measuring = true;
|
|
974
|
+
let size: { width: number; height: number };
|
|
975
|
+
try {
|
|
976
|
+
size = this._sizeAt(offered);
|
|
977
|
+
} finally {
|
|
978
|
+
this._measuring = false;
|
|
979
|
+
}
|
|
980
|
+
// core takes finite numbers only, and throws on any other from the
|
|
981
|
+
// layout — a document of lengths no clamp foresaw is none too tall
|
|
982
|
+
const finite = (n: number): number => (Number.isFinite(n) ? n : 0);
|
|
461
983
|
return {
|
|
462
|
-
width:
|
|
463
|
-
height:
|
|
984
|
+
width: wholePixels(Math.min(finite(size.width), offered)),
|
|
985
|
+
height: wholePixels(finite(size.height)),
|
|
464
986
|
};
|
|
465
987
|
}
|
|
466
988
|
|
|
989
|
+
/**
|
|
990
|
+
* The size the document comes to at `width`.
|
|
991
|
+
*
|
|
992
|
+
* Core asks a leaf for its height at the width it was measured at as well
|
|
993
|
+
* as at the one it has now (`probeHeightFloors`), to learn whether a
|
|
994
|
+
* relayout changed what the leaf needs — so every frame of a resize asks
|
|
995
|
+
* two widths, and the boxes hold one. Laying the document out at the old
|
|
996
|
+
* width to answer, and then at the new one again for the pass after, was
|
|
997
|
+
* three whole passes a frame. The size a width came to is kept instead,
|
|
998
|
+
* and answers until anything a layout reads changes: the source, the
|
|
999
|
+
* styles, a resource, the viewport height. Nothing but a size comes from
|
|
1000
|
+
* it — paint and the text accessors read the boxes, which are only ever
|
|
1001
|
+
* laid out for real (`_prepare`).
|
|
1002
|
+
*/
|
|
1003
|
+
private _sizeAt(width: number): { width: number; height: number } {
|
|
1004
|
+
const target = Math.max(1, Math.floor(width));
|
|
1005
|
+
if (
|
|
1006
|
+
target !== this._laidOutWidth &&
|
|
1007
|
+
this._stale === Stale.Nothing &&
|
|
1008
|
+
(this._props().domRevision ?? 0) === this._reportedDomRevision &&
|
|
1009
|
+
(!this._readsViewportHeight() ||
|
|
1010
|
+
this._viewportHeight() === this._laidOutUnder)
|
|
1011
|
+
) {
|
|
1012
|
+
const known = this._sizes.get(target);
|
|
1013
|
+
if (known) return known;
|
|
1014
|
+
}
|
|
1015
|
+
this._prepare(width);
|
|
1016
|
+
return { width: this._documentWidth, height: this._documentHeight };
|
|
1017
|
+
}
|
|
1018
|
+
|
|
467
1019
|
override applyProps(
|
|
468
1020
|
nextProps: Record<string, unknown>,
|
|
469
1021
|
prevProps: Record<string, unknown>,
|
|
@@ -471,19 +1023,31 @@ export class HtmlViewNode extends Node {
|
|
|
471
1023
|
super.applyProps(nextProps, prevProps);
|
|
472
1024
|
const next = nextProps as unknown as HtmlViewProps;
|
|
473
1025
|
const prev = prevProps as unknown as HtmlViewProps;
|
|
474
|
-
if (
|
|
1026
|
+
if (
|
|
1027
|
+
next.look !== prev.look ||
|
|
1028
|
+
next.stylesheet !== prev.stylesheet ||
|
|
1029
|
+
next.charset !== prev.charset
|
|
1030
|
+
) {
|
|
475
1031
|
this._invalidate(Stale.Style);
|
|
476
1032
|
}
|
|
477
1033
|
if (next.source !== prev.source || next.complete !== prev.complete) {
|
|
478
1034
|
this._read();
|
|
479
1035
|
} else if ((next.domRevision ?? 0) !== (prev.domRevision ?? 0)) {
|
|
480
1036
|
this._invalidate(Stale.Style);
|
|
1037
|
+
} else if ((next.baseUrl ?? null) !== (prev.baseUrl ?? null)) {
|
|
1038
|
+
// the same document somewhere else: every URL in it is another one
|
|
1039
|
+
this._updateBase();
|
|
1040
|
+
this._sweep();
|
|
481
1041
|
}
|
|
482
1042
|
}
|
|
483
1043
|
|
|
484
1044
|
override destroySubtree(): void {
|
|
485
1045
|
this._resources.destroy();
|
|
1046
|
+
this._webFonts.destroy();
|
|
486
1047
|
this._source.destroy();
|
|
1048
|
+
this._shadowCache?.destroy();
|
|
1049
|
+
this._shadowCache = null;
|
|
1050
|
+
this._sheetsRead = null;
|
|
487
1051
|
this._tree = null;
|
|
488
1052
|
this._cascade = null;
|
|
489
1053
|
super.destroySubtree();
|
|
@@ -586,26 +1150,129 @@ export class HtmlViewNode extends Node {
|
|
|
586
1150
|
// handler hand over — and are the one place the two units meet on the way
|
|
587
1151
|
// in. `_toDocument` multiplies.
|
|
588
1152
|
|
|
589
|
-
/** The link under a logical window point, if any
|
|
590
|
-
*
|
|
591
|
-
* #291, so following a
|
|
1153
|
+
/** The link under a logical window point, if any — resolved against the
|
|
1154
|
+
* document's base where it has one. Not part of the selection seam: core
|
|
1155
|
+
* deliberately left hover and `cursorAt` out of #291, so following a
|
|
1156
|
+
* link stays this package's. */
|
|
592
1157
|
hrefAtPoint(x: number, y: number): string | null {
|
|
593
1158
|
const el = this.elementAtPoint(x, y);
|
|
594
1159
|
let node: Element | null = el;
|
|
595
1160
|
while (node) {
|
|
596
1161
|
const href = attr(node, 'href');
|
|
597
|
-
if (href && (tagOf(node) === 'a' || tagOf(node) === 'area'))
|
|
1162
|
+
if (href && (tagOf(node) === 'a' || tagOf(node) === 'area')) {
|
|
1163
|
+
return this._urls.resolve(href);
|
|
1164
|
+
}
|
|
598
1165
|
node = isElement(node.parent) ? node.parent : null;
|
|
599
1166
|
}
|
|
600
1167
|
return null;
|
|
601
1168
|
}
|
|
602
1169
|
|
|
1170
|
+
/** The URL the document's relative URLs resolve against, or null where
|
|
1171
|
+
* it has none (`_updateBase`). */
|
|
1172
|
+
get documentBase(): string | null {
|
|
1173
|
+
return this._urls.base;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Where an element is, in document coordinates: logical pixels from this
|
|
1178
|
+
* element's top left, the space the scroll offset of a box around it is
|
|
1179
|
+
* in — so scrolling to a fragment is `scrollTo({ y: rect.y })`. A block's
|
|
1180
|
+
* border box. An inline element's fragments, from its first line to its
|
|
1181
|
+
* last, each across its text, the inline-blocks and inline boxes' edges
|
|
1182
|
+
* it holds, and its own padding and border where it starts or ends — its
|
|
1183
|
+
* border box across, as `getBoundingClientRect` is (CSSOM View 6.1) — and
|
|
1184
|
+
* as tall as its lines; or where its text would start when it has none,
|
|
1185
|
+
* as `<a name>` has none. Null for an element with no box — `display:
|
|
1186
|
+
* none`, or not in the document.
|
|
1187
|
+
*/
|
|
1188
|
+
elementRect(element: Element): Rect | null {
|
|
1189
|
+
this._prepare(this.abs.width || 1);
|
|
1190
|
+
const tree = this._tree;
|
|
1191
|
+
if (!tree) return null;
|
|
1192
|
+
const box = boxFor(tree.root, element);
|
|
1193
|
+
if (!box) return null;
|
|
1194
|
+
let rect: Rect | null = null;
|
|
1195
|
+
if (box.kind !== 'inline' && box.kind !== 'text') {
|
|
1196
|
+
rect = { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
1197
|
+
} else {
|
|
1198
|
+
const bands: Rect[] = [];
|
|
1199
|
+
// an inline box's fragments are on the lines of the block it is laid
|
|
1200
|
+
// out in, and no text of a float or a positioned box inside it — on
|
|
1201
|
+
// lines of their own — is one of them (CSSOM View 6.1): a list item
|
|
1202
|
+
// made inline around an absolute link measured as the link
|
|
1203
|
+
let block: Box | null = box.parent;
|
|
1204
|
+
while (block && !block.lines) block = block.parent;
|
|
1205
|
+
if (box.subtreeTextEnd > box.subtreeTextStart) {
|
|
1206
|
+
if (box.kind === 'inline' && block?.lines) {
|
|
1207
|
+
lineBands(
|
|
1208
|
+
block.lines,
|
|
1209
|
+
box.subtreeTextStart,
|
|
1210
|
+
box.subtreeTextEnd,
|
|
1211
|
+
0,
|
|
1212
|
+
0,
|
|
1213
|
+
bands,
|
|
1214
|
+
);
|
|
1215
|
+
} else {
|
|
1216
|
+
collectBands(
|
|
1217
|
+
tree.root,
|
|
1218
|
+
box.subtreeTextStart,
|
|
1219
|
+
box.subtreeTextEnd,
|
|
1220
|
+
0,
|
|
1221
|
+
0,
|
|
1222
|
+
bands,
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
if (box.kind === 'inline') {
|
|
1227
|
+
fragmentReach(box, bands);
|
|
1228
|
+
const fonts = this._layouts?.fonts;
|
|
1229
|
+
if (fonts) fragmentHeights(box, bands, fonts);
|
|
1230
|
+
}
|
|
1231
|
+
for (const band of bands) rect = rect ? unionRect(rect, band) : band;
|
|
1232
|
+
if (!rect) {
|
|
1233
|
+
// an empty subtree's range is `[0, 0)`, wherever it stands: where
|
|
1234
|
+
// its text would be is where the text after it starts
|
|
1235
|
+
const caret = caretAt(tree.root, textAfter(tree.root, box));
|
|
1236
|
+
if (!caret) return null;
|
|
1237
|
+
rect = { x: caret.x, y: caret.y, width: 0, height: caret.height };
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const s = this._scale;
|
|
1241
|
+
return {
|
|
1242
|
+
x: rect.x / s,
|
|
1243
|
+
y: rect.y / s,
|
|
1244
|
+
width: rect.width / s,
|
|
1245
|
+
height: rect.height / s,
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
603
1249
|
/** The deepest element whose box contains a logical window point. */
|
|
604
1250
|
elementAtPoint(x: number, y: number): Element | null {
|
|
605
1251
|
const tree = this._tree;
|
|
606
1252
|
if (!tree) return null;
|
|
607
1253
|
const local = this._toDocument(x, y);
|
|
608
|
-
return deepestAt(tree
|
|
1254
|
+
return deepestAt(tree, local.x, local.y);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* The cursor for a point of this element's, in device pixels: what core
|
|
1259
|
+
* asks a drawn element as the pointer moves over it (`cursorAt`,
|
|
1260
|
+
* react-x11#757). The `cursor` the document's styles give what is under
|
|
1261
|
+
* it — a link's `pointer`, the user-agent sheet's — and where they say
|
|
1262
|
+
* nothing, text's I-beam over text and the arrow elsewhere, as a browser
|
|
1263
|
+
* shows them. The arrow is named rather than left to null: null lets
|
|
1264
|
+
* core fall through to the element's `defaultCursor`, which is the
|
|
1265
|
+
* I-beam on a selectable surface, and every `<Html>` is one.
|
|
1266
|
+
*/
|
|
1267
|
+
override cursorAt(x: number, y: number): string | null {
|
|
1268
|
+
const tree = this._tree;
|
|
1269
|
+
if (!tree) return null;
|
|
1270
|
+
const hit = { text: false };
|
|
1271
|
+
const el = deepestAt(tree, x - this.abs.x, y - this.abs.y, hit);
|
|
1272
|
+
const cursor = el ? tree.styles.get(el)?.style.cursor : null;
|
|
1273
|
+
// a keyword; a `url()` this cannot load falls back, as its list would
|
|
1274
|
+
if (cursor && cursor !== 'auto' && /^[a-z-]+$/.test(cursor)) return cursor;
|
|
1275
|
+
return hit.text ? 'text' : 'default';
|
|
609
1276
|
}
|
|
610
1277
|
|
|
611
1278
|
/**
|
|
@@ -624,20 +1291,207 @@ export class HtmlViewNode extends Node {
|
|
|
624
1291
|
node = isElement(node.parent) ? node.parent : null;
|
|
625
1292
|
}
|
|
626
1293
|
if (sameChain(chain, this._hovered)) return false;
|
|
1294
|
+
const was = this._hovered;
|
|
627
1295
|
this._hovered = chain;
|
|
628
1296
|
cascade.setPointer({ hovered: new Set(chain), active: EMPTY_SET });
|
|
629
|
-
this.
|
|
1297
|
+
this._restyleHover(was, chain);
|
|
630
1298
|
return true;
|
|
631
1299
|
}
|
|
632
1300
|
|
|
633
1301
|
clearHover(): boolean {
|
|
634
1302
|
if (!this._hovered.length) return false;
|
|
1303
|
+
const was = this._hovered;
|
|
635
1304
|
this._hovered = [];
|
|
636
1305
|
this._cascade?.setPointer({ hovered: new Set(), active: EMPTY_SET });
|
|
637
|
-
this.
|
|
1306
|
+
this._restyleHover(was, []);
|
|
638
1307
|
return true;
|
|
639
1308
|
}
|
|
640
1309
|
|
|
1310
|
+
/** The hovered chain moved: restyle where it did, or the document. */
|
|
1311
|
+
private _restyleHover(was: readonly Element[], now: readonly Element[]) {
|
|
1312
|
+
const done = this._hoverInPlace(was, now);
|
|
1313
|
+
if (done === 'painted') this.invalidate(false, this, 'props');
|
|
1314
|
+
else if (done === false) this._invalidate(Stale.Boxes);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* A pointer move restyled where it happened, when all it changed is ink.
|
|
1319
|
+
*
|
|
1320
|
+
* A move to another element is a change to the cascade, and the pipeline
|
|
1321
|
+
* answers one by building every box again and laying the document out:
|
|
1322
|
+
* 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
|
|
1323
|
+
* link the pointer crossed. But the rules a page writes for `:hover`
|
|
1324
|
+
* nearly all change a colour, an underline, a background or a border's
|
|
1325
|
+
* colour — 77 of that article's 79 — and those move nothing. So:
|
|
1326
|
+
*
|
|
1327
|
+
* - Only an element whose hover state flipped and that a compound testing
|
|
1328
|
+
* the pointer could match can change (`Cascade.hoverTouches`), with its
|
|
1329
|
+
* subtree, and its later siblings where a sibling combinator follows.
|
|
1330
|
+
* A move between two paragraphs under `a:hover` touches nothing.
|
|
1331
|
+
* - Those are styled again from their parents. Where anything but ink
|
|
1332
|
+
* differs (`PAINT_ONLY`), or a box is not the element's own — an
|
|
1333
|
+
* anonymous or a pseudo-element box, a marker, a control — this is
|
|
1334
|
+
* not the move to take.
|
|
1335
|
+
* - The boxes take their new styles, and each layout of their text is
|
|
1336
|
+
* made again from the runs it was made from (`TextLayoutCache.inputsOf`)
|
|
1337
|
+
* with the new ink (`runFor`), where its geometry comes out the same.
|
|
1338
|
+
*
|
|
1339
|
+
* Everything is checked before anything is changed. 'none' where nothing
|
|
1340
|
+
* could change, 'painted' where it was restyled here, false where the
|
|
1341
|
+
* document has to be built again.
|
|
1342
|
+
*/
|
|
1343
|
+
private _hoverInPlace(
|
|
1344
|
+
was: readonly Element[],
|
|
1345
|
+
now: readonly Element[],
|
|
1346
|
+
): 'none' | 'painted' | false {
|
|
1347
|
+
const cascade = this._cascade;
|
|
1348
|
+
const tree = this._tree;
|
|
1349
|
+
const layouts = this._layouts;
|
|
1350
|
+
if (!cascade || !tree || !layouts || !cascade.hoverLocal) return false;
|
|
1351
|
+
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0) return false;
|
|
1352
|
+
|
|
1353
|
+
const before = new Set(was);
|
|
1354
|
+
const after = new Set(now);
|
|
1355
|
+
const roots: Element[] = [];
|
|
1356
|
+
// and the elements a `:has()` testing the pointer may flip, which
|
|
1357
|
+
// are around the ones that changed; the chains are ancestor chains,
|
|
1358
|
+
// so the deepest that changed on each side is where to look from
|
|
1359
|
+
const flipped = (el: Element, other: Set<Element>): void => {
|
|
1360
|
+
if (other.has(el)) return;
|
|
1361
|
+
if (cascade.hoverTouches(el)) roots.push(el);
|
|
1362
|
+
};
|
|
1363
|
+
for (const el of was) flipped(el, after);
|
|
1364
|
+
for (const el of now) flipped(el, before);
|
|
1365
|
+
const deepest = (chain: readonly Element[], other: Set<Element>) => {
|
|
1366
|
+
const el = chain[0];
|
|
1367
|
+
if (el && !other.has(el)) cascade.hoverAnchors(el, roots);
|
|
1368
|
+
};
|
|
1369
|
+
deepest(was, after);
|
|
1370
|
+
deepest(now, before);
|
|
1371
|
+
if (!roots.length) return 'none';
|
|
1372
|
+
|
|
1373
|
+
// what may restyle: the roots' subtrees, which inherit from them and a
|
|
1374
|
+
// descendant combinator reaches, and their later siblings' too where a
|
|
1375
|
+
// sibling combinator follows a compound that tests the pointer
|
|
1376
|
+
const reach = new Set<Element>();
|
|
1377
|
+
const collect = (el: Element): boolean => {
|
|
1378
|
+
const stack: Element[] = [el];
|
|
1379
|
+
while (stack.length) {
|
|
1380
|
+
const at = stack.pop()!;
|
|
1381
|
+
if (reach.has(at)) continue;
|
|
1382
|
+
reach.add(at);
|
|
1383
|
+
if (reach.size > HOVER_RESTYLE_LIMIT) return false;
|
|
1384
|
+
for (const child of at.children)
|
|
1385
|
+
if (isElement(child)) stack.push(child);
|
|
1386
|
+
}
|
|
1387
|
+
return true;
|
|
1388
|
+
};
|
|
1389
|
+
for (const root of roots) {
|
|
1390
|
+
if (!collect(root)) return false;
|
|
1391
|
+
if (!cascade.hoverSiblings) continue;
|
|
1392
|
+
for (let s = root.nextSibling; s; s = s.nextSibling) {
|
|
1393
|
+
if (isElement(s) && !collect(s)) return false;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// styled again from their parents, a parent first
|
|
1398
|
+
const fresh = new Map<Element, ComputedStyle>();
|
|
1399
|
+
const changed = new Map<Element, ComputedStyle>();
|
|
1400
|
+
let refused = false;
|
|
1401
|
+
const styleOf = (el: Element): ComputedStyle | null => {
|
|
1402
|
+
const kept = tree.styles.get(el);
|
|
1403
|
+
if (!kept) return null;
|
|
1404
|
+
if (!reach.has(el)) return kept.style;
|
|
1405
|
+
const done = fresh.get(el);
|
|
1406
|
+
if (done) return done;
|
|
1407
|
+
// an element at the top of the document is styled from the box above
|
|
1408
|
+
// it all, as the builder styled it (`BoxBuilder.run`)
|
|
1409
|
+
const parent = isElement(el.parent) ? el.parent : null;
|
|
1410
|
+
const parentStyle = parent ? styleOf(parent) : tree.root.style;
|
|
1411
|
+
if (!parentStyle) {
|
|
1412
|
+
refused = true;
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
const style = cascade.styleFor(el, parentStyle, kept.inFlex);
|
|
1416
|
+
fresh.set(el, style);
|
|
1417
|
+
const diff = inkOnly(kept.style, style);
|
|
1418
|
+
if (diff === false) refused = true;
|
|
1419
|
+
else if (diff) changed.set(el, style);
|
|
1420
|
+
return style;
|
|
1421
|
+
};
|
|
1422
|
+
for (const el of reach) {
|
|
1423
|
+
styleOf(el);
|
|
1424
|
+
if (refused) return false;
|
|
1425
|
+
}
|
|
1426
|
+
if (!changed.size) return 'none';
|
|
1427
|
+
for (const el of changed.keys()) {
|
|
1428
|
+
const tag = tagOf(el);
|
|
1429
|
+
// their backgrounds are the canvas's
|
|
1430
|
+
if (tag === 'html' || tag === 'body') return false;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// the boxes: each changed element's own, and nothing that takes its
|
|
1434
|
+
// style from one without being it
|
|
1435
|
+
const fonts = layouts.fonts;
|
|
1436
|
+
const restyled: [Box, ComputedStyle][] = [];
|
|
1437
|
+
const redecorated: [Box, Box['decoration']][] = [];
|
|
1438
|
+
const walk: Box[] = [tree.root];
|
|
1439
|
+
while (walk.length) {
|
|
1440
|
+
const box = walk.pop()!;
|
|
1441
|
+
for (const child of box.children) walk.push(child);
|
|
1442
|
+
const owner = box.el ?? nearestElement(box);
|
|
1443
|
+
if (!owner) continue;
|
|
1444
|
+
const style = changed.get(owner);
|
|
1445
|
+
if (!style) continue;
|
|
1446
|
+
const kept = tree.styles.get(owner)!;
|
|
1447
|
+
// an anonymous box, a pseudo-element, a marker, a control: a style
|
|
1448
|
+
// derived from the element's, or drawn from it somewhere else
|
|
1449
|
+
if (!box.el || box.style !== kept.style) return false;
|
|
1450
|
+
if (box.marker || box.replaced !== 'none') return false;
|
|
1451
|
+
restyled.push([box, style]);
|
|
1452
|
+
if (box.kind === 'inline') {
|
|
1453
|
+
const decoration = inlineDecoration(fonts, box, style);
|
|
1454
|
+
if ((decoration === null) !== (box.decoration === null)) {
|
|
1455
|
+
// a rounded box with a background is laid out with its edges
|
|
1456
|
+
if (style.borderRadius.some((r) => r !== 0)) return false;
|
|
1457
|
+
}
|
|
1458
|
+
if (decoration !== box.decoration) redecorated.push([box, decoration]);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// the text: each layout holding a run of a changed element's, made
|
|
1463
|
+
// again with its new ink, where it comes out the same shape
|
|
1464
|
+
const relaid = new Map<TextLayoutLike, TextLayoutLike | null>();
|
|
1465
|
+
const texts: LineText[] = [];
|
|
1466
|
+
walk.push(tree.root);
|
|
1467
|
+
while (walk.length) {
|
|
1468
|
+
const box = walk.pop()!;
|
|
1469
|
+
for (const child of box.children) walk.push(child);
|
|
1470
|
+
if (!box.lines) continue;
|
|
1471
|
+
for (const line of box.lines) {
|
|
1472
|
+
for (const text of line.texts) {
|
|
1473
|
+
texts.push(text);
|
|
1474
|
+
if (relaid.has(text.layout)) continue;
|
|
1475
|
+
const next = reinked(text, layouts, changed, tree.styles);
|
|
1476
|
+
if (next === false) return false;
|
|
1477
|
+
relaid.set(text.layout, next);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// and only now, all of it
|
|
1483
|
+
for (const [box, style] of restyled) box.style = style;
|
|
1484
|
+
for (const [box, decoration] of redecorated) box.decoration = decoration;
|
|
1485
|
+
for (const text of texts) {
|
|
1486
|
+
const next = relaid.get(text.layout);
|
|
1487
|
+
if (next) text.layout = next;
|
|
1488
|
+
}
|
|
1489
|
+
for (const [el, style] of changed) {
|
|
1490
|
+
tree.styles.set(el, { style, inFlex: tree.styles.get(el)!.inFlex });
|
|
1491
|
+
}
|
|
1492
|
+
return 'painted';
|
|
1493
|
+
}
|
|
1494
|
+
|
|
641
1495
|
/** The document, for an application that wants to read or change it. */
|
|
642
1496
|
get document(): Document {
|
|
643
1497
|
return this._source.document;
|
|
@@ -652,6 +1506,7 @@ export class HtmlViewNode extends Node {
|
|
|
652
1506
|
*/
|
|
653
1507
|
touchDocument(): void {
|
|
654
1508
|
this._source.touch();
|
|
1509
|
+
this._updateBase();
|
|
655
1510
|
this._sweep();
|
|
656
1511
|
this._invalidate(Stale.Style);
|
|
657
1512
|
}
|
|
@@ -708,23 +1563,133 @@ export class HtmlViewNode extends Node {
|
|
|
708
1563
|
damage = { x: 0, y: 0, width: window.width, height: window.height };
|
|
709
1564
|
}
|
|
710
1565
|
}
|
|
1566
|
+
try {
|
|
1567
|
+
this._paint(ctx, tree, range, damage);
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
this._fail(error, this._laidOutWidth);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
private _paint(
|
|
1574
|
+
ctx: Context2D,
|
|
1575
|
+
tree: BoxTree,
|
|
1576
|
+
range: { start: number; end: number } | null,
|
|
1577
|
+
damage: { x: number; y: number; width: number; height: number } | null,
|
|
1578
|
+
): void {
|
|
711
1579
|
paintDocument(ctx as PaintContext, tree, {
|
|
712
1580
|
originX: this.abs.x,
|
|
713
1581
|
originY: this.abs.y,
|
|
1582
|
+
// the root's background covers the whole element, not only the
|
|
1583
|
+
// document: an element grown past its content is canvas too
|
|
1584
|
+
canvas: this.abs,
|
|
714
1585
|
scale: this._scale,
|
|
715
1586
|
damage,
|
|
716
1587
|
selection: range
|
|
717
1588
|
? { start: this._toUnits(range.start), end: this._toUnits(range.end) }
|
|
718
1589
|
: null,
|
|
719
1590
|
selectionColor: this.selectionColor,
|
|
720
|
-
imageFor: (box) =>
|
|
721
|
-
box.el ?
|
|
1591
|
+
imageFor: (box) => {
|
|
1592
|
+
const url = box.el ? imageUrlOf(box.el) : CONTENT_IMAGES.get(box);
|
|
1593
|
+
return url === undefined ? null : this._resources.image(url);
|
|
1594
|
+
},
|
|
1595
|
+
backgroundImageFor: (url) => {
|
|
1596
|
+
const image = this._resources.image(url);
|
|
1597
|
+
const size = image ? this._resources.imageSize(url) : null;
|
|
1598
|
+
return image && size ? { image, ...size } : null;
|
|
1599
|
+
},
|
|
1600
|
+
cached: (key, width, height, draw) =>
|
|
1601
|
+
(this._shadowCache ??= new SurfaceCache(this.app)).get(
|
|
1602
|
+
key,
|
|
1603
|
+
width,
|
|
1604
|
+
height,
|
|
1605
|
+
draw as (ctx: unknown) => void,
|
|
1606
|
+
),
|
|
722
1607
|
});
|
|
723
1608
|
}
|
|
724
1609
|
}
|
|
725
1610
|
|
|
726
1611
|
const EMPTY_SET: ReadonlySet<Element> = new Set();
|
|
727
1612
|
|
|
1613
|
+
/** A sheet as the restyle reads it: its text, the encoding it was decoded
|
|
1614
|
+
* from, the element that brought it, and what its URLs resolve against. */
|
|
1615
|
+
interface SheetText {
|
|
1616
|
+
text: string;
|
|
1617
|
+
encoding?: string;
|
|
1618
|
+
element: Element;
|
|
1619
|
+
base: string | null;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/** One `@import` a sheet read, and what it read as. */
|
|
1623
|
+
interface ImportRead {
|
|
1624
|
+
url: string;
|
|
1625
|
+
text: string | null;
|
|
1626
|
+
base: string | null;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/** What a cascade was built from, to tell whether the next would be the
|
|
1630
|
+
* same one: the look, scale and fonts, each sheet's text, encoding and
|
|
1631
|
+
* base, the texts of everything each imports, and the host's own — and
|
|
1632
|
+
* the faces all of them declare. */
|
|
1633
|
+
interface SheetsRead {
|
|
1634
|
+
look: RootLook;
|
|
1635
|
+
scale: number;
|
|
1636
|
+
fonts: unknown;
|
|
1637
|
+
texts: string[];
|
|
1638
|
+
encodings: (string | undefined)[];
|
|
1639
|
+
bases: (string | null)[];
|
|
1640
|
+
imports: ImportRead[][];
|
|
1641
|
+
extras: string[];
|
|
1642
|
+
faces: DeclaredFace[];
|
|
1643
|
+
cascade: Cascade;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/** The first box an element made, depth first. */
|
|
1647
|
+
function boxFor(root: Box, element: Element): Box | null {
|
|
1648
|
+
const stack: Box[] = [root];
|
|
1649
|
+
while (stack.length) {
|
|
1650
|
+
const box = stack.pop()!;
|
|
1651
|
+
if (box.el === element) return box;
|
|
1652
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1653
|
+
stack.push(box.children[i]);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return null;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/** Where the document's text goes on after a box with none: the start of
|
|
1660
|
+
* the next text in document order, or the end of the last before it. */
|
|
1661
|
+
function textAfter(root: Box, target: Box): number {
|
|
1662
|
+
const stack: Box[] = [root];
|
|
1663
|
+
let passed = false;
|
|
1664
|
+
let before = 0;
|
|
1665
|
+
while (stack.length) {
|
|
1666
|
+
const box = stack.pop()!;
|
|
1667
|
+
if (box === target) {
|
|
1668
|
+
passed = true;
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
if (box.kind === 'text' && box.textEnd > box.textStart) {
|
|
1672
|
+
if (passed) return box.textStart;
|
|
1673
|
+
before = box.textEnd;
|
|
1674
|
+
}
|
|
1675
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1676
|
+
stack.push(box.children[i]);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return before;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function unionRect(a: Rect, b: Rect): Rect {
|
|
1683
|
+
const x = Math.min(a.x, b.x);
|
|
1684
|
+
const y = Math.min(a.y, b.y);
|
|
1685
|
+
return {
|
|
1686
|
+
x,
|
|
1687
|
+
y,
|
|
1688
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
1689
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y,
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
|
|
728
1693
|
function textOf(el: Element): string {
|
|
729
1694
|
let out = '';
|
|
730
1695
|
for (const child of el.children) {
|
|
@@ -749,7 +1714,8 @@ function sameRects(a: ControlRect[], b: ControlRect[]): boolean {
|
|
|
749
1714
|
p.x !== q.x ||
|
|
750
1715
|
p.y !== q.y ||
|
|
751
1716
|
p.width !== q.width ||
|
|
752
|
-
p.height !== q.height
|
|
1717
|
+
p.height !== q.height ||
|
|
1718
|
+
!sameBare(p.bare, q.bare)
|
|
753
1719
|
) {
|
|
754
1720
|
return false;
|
|
755
1721
|
}
|
|
@@ -757,6 +1723,19 @@ function sameRects(a: ControlRect[], b: ControlRect[]): boolean {
|
|
|
757
1723
|
return true;
|
|
758
1724
|
}
|
|
759
1725
|
|
|
1726
|
+
function sameBare(a?: BareField, b?: BareField): boolean {
|
|
1727
|
+
if (!a || !b) return a === b;
|
|
1728
|
+
return (
|
|
1729
|
+
a.x === b.x &&
|
|
1730
|
+
a.y === b.y &&
|
|
1731
|
+
a.width === b.width &&
|
|
1732
|
+
a.height === b.height &&
|
|
1733
|
+
a.color === b.color &&
|
|
1734
|
+
a.fontFamily === b.fontFamily &&
|
|
1735
|
+
a.fontSize === b.fontSize
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
760
1739
|
// --- walks over the laid-out tree -------------------------------------------
|
|
761
1740
|
|
|
762
1741
|
/** Distance from a coordinate to an interval, zero inside it. */
|
|
@@ -797,7 +1776,7 @@ function nearestText(box: Box, x: number, y: number): number | null {
|
|
|
797
1776
|
const units = offsets.length
|
|
798
1777
|
? offsets[Math.max(0, Math.min(local, offsets.length - 1))]
|
|
799
1778
|
: local;
|
|
800
|
-
best = text.textStart
|
|
1779
|
+
best = Math.max(text.textStart, documentOffsetOf(text, units));
|
|
801
1780
|
}
|
|
802
1781
|
};
|
|
803
1782
|
|
|
@@ -908,7 +1887,7 @@ function caretAt(
|
|
|
908
1887
|
for (const text of line.texts) {
|
|
909
1888
|
if (units < text.textStart || units > text.textEnd) continue;
|
|
910
1889
|
const offsets = layoutOffsetsOf(text.layout);
|
|
911
|
-
const layoutUnits = text
|
|
1890
|
+
const layoutUnits = layoutOffsetOf(text, units);
|
|
912
1891
|
const caret = text.layout.caretPosition(
|
|
913
1892
|
codePointAtOffset(offsets, layoutUnits),
|
|
914
1893
|
);
|
|
@@ -930,56 +1909,152 @@ function caretAt(
|
|
|
930
1909
|
/** Every band a document range covers, in window coordinates. The subtree
|
|
931
1910
|
* text ranges prune the walk to the boxes the range actually crosses, so a
|
|
932
1911
|
* drag's small range costs its own paragraphs; only Ctrl+A pays for all. */
|
|
933
|
-
|
|
934
|
-
|
|
1912
|
+
/**
|
|
1913
|
+
* What an inline box reaches on each line besides its text, as `out`
|
|
1914
|
+
* rectangles as tall as the line: its own padding and border where it
|
|
1915
|
+
* starts or ends there — its edge less its margin, which is outside it —
|
|
1916
|
+
* and the inline-blocks and inline boxes' edges inside it, whole. The rest
|
|
1917
|
+
* of its border box across, which its text alone missed: a padded link was
|
|
1918
|
+
* as wide as its words (`elementRect`).
|
|
1919
|
+
*/
|
|
1920
|
+
function fragmentReach(box: Box, out: Rect[]): void {
|
|
1921
|
+
let block = box.parent;
|
|
1922
|
+
while (block && !block.lines) block = block.parent;
|
|
1923
|
+
if (!block?.lines) return;
|
|
1924
|
+
const inside = (b: Box | null): boolean => {
|
|
1925
|
+
for (let at = b; at && at !== block; at = at.parent) {
|
|
1926
|
+
if (at === box) return true;
|
|
1927
|
+
}
|
|
1928
|
+
return false;
|
|
1929
|
+
};
|
|
1930
|
+
const rtl = box.style.direction === 'rtl';
|
|
1931
|
+
for (const line of block.lines) {
|
|
1932
|
+
let left = Infinity;
|
|
1933
|
+
let right = -Infinity;
|
|
1934
|
+
for (const edge of line.edges ?? []) {
|
|
1935
|
+
if (edge.box === box) {
|
|
1936
|
+
// the element's `direction` says which side its start is on, as
|
|
1937
|
+
// paint reads it (`paintInlineBoxes`)
|
|
1938
|
+
const onLeft = (edge.side === 'start') !== rtl;
|
|
1939
|
+
left = Math.min(left, onLeft ? edge.x + box.marginLeft : edge.x);
|
|
1940
|
+
right = Math.max(
|
|
1941
|
+
right,
|
|
1942
|
+
onLeft ? edge.x + edge.width : edge.x + edge.width - box.marginRight,
|
|
1943
|
+
);
|
|
1944
|
+
} else if (inside(edge.box)) {
|
|
1945
|
+
left = Math.min(left, edge.x);
|
|
1946
|
+
right = Math.max(right, edge.x + edge.width);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
for (const placed of line.atomics) {
|
|
1950
|
+
if (!inside(placed.box)) continue;
|
|
1951
|
+
left = Math.min(left, placed.x - placed.box.marginLeft);
|
|
1952
|
+
right = Math.max(
|
|
1953
|
+
right,
|
|
1954
|
+
placed.x + placed.box.width + placed.box.marginRight,
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
if (right > left) {
|
|
1958
|
+
out.push({
|
|
1959
|
+
x: left,
|
|
1960
|
+
y: line.y,
|
|
1961
|
+
width: right - left,
|
|
1962
|
+
height: line.height,
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/**
|
|
1969
|
+
* An inline element's bands made as tall as its fragments' border boxes,
|
|
1970
|
+
* as CSSOM View takes an inline box's client rects (6.1): its font's
|
|
1971
|
+
* content area about its own baseline, with the padding and the border
|
|
1972
|
+
* above and below it, which take no room on the line (CSS 2.1 10.6.1) and
|
|
1973
|
+
* which paint draws the background over (`paintInlineBoxes`). A band is a
|
|
1974
|
+
* line's height across until then: a link padded 10px below measured as
|
|
1975
|
+
* tall as its line and no taller.
|
|
1976
|
+
*/
|
|
1977
|
+
function fragmentHeights(box: Box, bands: Rect[], fonts: FontsLike): void {
|
|
1978
|
+
let block = box.parent;
|
|
1979
|
+
while (block && !block.lines) block = block.parent;
|
|
1980
|
+
if (!block?.lines?.length) return;
|
|
1981
|
+
const face = faceExtentOf(fonts, box.style);
|
|
1982
|
+
const byTop = new Map<number, LineBox>();
|
|
1983
|
+
for (const line of block.lines) byTop.set(line.y, line);
|
|
1984
|
+
for (let i = 0; i < bands.length; i += 1) {
|
|
1985
|
+
const band = bands[i];
|
|
1986
|
+
const line = byTop.get(band.y);
|
|
1987
|
+
if (!line) continue;
|
|
1988
|
+
// on its own baseline, which `vertical-align` may raise off the line's
|
|
1989
|
+
const raise = SHIFTED_LINES.has(line)
|
|
1990
|
+
? (LINE_BOX_RAISES.get(line)?.get(box) ?? BOX_RAISES.get(box) ?? 0)
|
|
1991
|
+
: 0;
|
|
1992
|
+
const baseline = line.y + line.baseline - raise;
|
|
1993
|
+
const top = baseline - face.ascent - box.padTop - box.borderTop;
|
|
1994
|
+
const bottom = baseline + face.descent + box.padBottom + box.borderBottom;
|
|
1995
|
+
bands[i] = { x: band.x, y: top, width: band.width, height: bottom - top };
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/** The bands of a text range on one block's own lines. */
|
|
2000
|
+
function lineBands(
|
|
2001
|
+
lines: readonly LineBox[],
|
|
935
2002
|
from: number,
|
|
936
2003
|
to: number,
|
|
937
2004
|
dx: number,
|
|
938
2005
|
dy: number,
|
|
939
2006
|
out: Rect[],
|
|
940
2007
|
): void {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
y: dy + line.y,
|
|
975
|
-
width: band.width,
|
|
976
|
-
height: line.height,
|
|
977
|
-
});
|
|
978
|
-
}
|
|
2008
|
+
// First line whose text can reach `from`, by binary search over the
|
|
2009
|
+
// sorted text starts; stop at the first line past `to`.
|
|
2010
|
+
let lo = 0;
|
|
2011
|
+
let hi = lines.length;
|
|
2012
|
+
while (lo < hi) {
|
|
2013
|
+
const mid = (lo + hi) >> 1;
|
|
2014
|
+
if (lines[mid].textEnd > from) hi = mid;
|
|
2015
|
+
else lo = mid + 1;
|
|
2016
|
+
}
|
|
2017
|
+
for (let i = lo; i < lines.length; i += 1) {
|
|
2018
|
+
const line = lines[i];
|
|
2019
|
+
if (line.textStart >= to) break;
|
|
2020
|
+
if (line.textEnd > from && line.textStart < to) {
|
|
2021
|
+
for (const text of line.texts) {
|
|
2022
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
2023
|
+
if (!natural) continue;
|
|
2024
|
+
const a = Math.max(from, text.textStart);
|
|
2025
|
+
const b = Math.min(to, text.textEnd);
|
|
2026
|
+
if (b <= a) continue;
|
|
2027
|
+
const offsets = layoutOffsetsOf(text.layout);
|
|
2028
|
+
for (const band of bandsFor(
|
|
2029
|
+
text.layout,
|
|
2030
|
+
natural,
|
|
2031
|
+
offsets,
|
|
2032
|
+
layoutOffsetOf(text, a),
|
|
2033
|
+
layoutOffsetOf(text, b, true),
|
|
2034
|
+
)) {
|
|
2035
|
+
out.push({
|
|
2036
|
+
x: dx + band.x + text.drawX,
|
|
2037
|
+
y: dy + line.y,
|
|
2038
|
+
width: band.width,
|
|
2039
|
+
height: line.height,
|
|
2040
|
+
});
|
|
979
2041
|
}
|
|
980
2042
|
}
|
|
981
2043
|
}
|
|
982
2044
|
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function collectBands(
|
|
2048
|
+
box: Box,
|
|
2049
|
+
from: number,
|
|
2050
|
+
to: number,
|
|
2051
|
+
dx: number,
|
|
2052
|
+
dy: number,
|
|
2053
|
+
out: Rect[],
|
|
2054
|
+
): void {
|
|
2055
|
+
if (box.subtreeTextEnd <= box.subtreeTextStart) return;
|
|
2056
|
+
if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to) return;
|
|
2057
|
+
if (box.lines) lineBands(box.lines, from, to, dx, dy, out);
|
|
983
2058
|
// Atomics are ordinary children, reached below.
|
|
984
2059
|
for (const child of box.children) {
|
|
985
2060
|
if (child.kind === 'text' || child.kind === 'break') continue;
|
|
@@ -987,10 +2062,118 @@ function collectBands(
|
|
|
987
2062
|
}
|
|
988
2063
|
}
|
|
989
2064
|
|
|
2065
|
+
/** The layers of a hit test, in the order CSS paints them within a
|
|
2066
|
+
* context (`deepestAt`). */
|
|
2067
|
+
const HIT_NEGATIVE = -1;
|
|
2068
|
+
const HIT_BLOCK = 0;
|
|
2069
|
+
const HIT_FLOAT = 1;
|
|
2070
|
+
const HIT_INLINE = 2;
|
|
2071
|
+
const HIT_POSITIONED = 3;
|
|
2072
|
+
|
|
990
2073
|
/** The deepest element box containing a document-space point. */
|
|
991
|
-
function deepestAt(
|
|
2074
|
+
function deepestAt(
|
|
2075
|
+
tree: BoxTree,
|
|
2076
|
+
x: number,
|
|
2077
|
+
y: number,
|
|
2078
|
+
/** Told whether what was found was found under text: a run's, rather
|
|
2079
|
+
* than a box's. */
|
|
2080
|
+
hit?: { text: boolean },
|
|
2081
|
+
): Element | null {
|
|
2082
|
+
const box = tree.root;
|
|
992
2083
|
let found: Element | null = box.el;
|
|
993
|
-
|
|
2084
|
+
let foundKey: readonly number[] = [];
|
|
2085
|
+
let viaText = false;
|
|
2086
|
+
// What is under a point is what was painted there last (CSS 2.1 Appendix
|
|
2087
|
+
// E): within a context, an in-flow block's own box, then a float over it,
|
|
2088
|
+
// then a line's text and atomics, then a positioned box — and a float, an
|
|
2089
|
+
// atomic or a positioned box paints its own content whole in its turn,
|
|
2090
|
+
// those layers again inside it. So a hit carries the layers down to it,
|
|
2091
|
+
// and takes the place of the one before where those come after them, or
|
|
2092
|
+
// tie: an infobox floated out of one section hangs over the next, whose
|
|
2093
|
+
// own box took every link in it, and a skin that puts the article in a
|
|
2094
|
+
// `position: relative` box put all of it in one layer. Positioned boxes
|
|
2095
|
+
// are painted in `z-index` order and then the document's (`byZIndex`),
|
|
2096
|
+
// the negative ones under the flow: the Zen Garden's `›` has
|
|
2097
|
+
// `z-index: 3` over the bar the "View All Designs" link fills after it.
|
|
2098
|
+
const take = (el: Element, key: readonly number[], text: boolean) => {
|
|
2099
|
+
if (compareKeys(key, foundKey) < 0) return;
|
|
2100
|
+
found = el;
|
|
2101
|
+
foundKey = key;
|
|
2102
|
+
viaText = text;
|
|
2103
|
+
};
|
|
2104
|
+
// A box is walked into wherever it draws — its reach, overflow and all
|
|
2105
|
+
// (`computePaintBounds`) — and named only where its own rectangle is. A
|
|
2106
|
+
// page's `html, body { height: 100% }` is one viewport tall and its
|
|
2107
|
+
// article overflows it, and a walk that went no further than a box's own
|
|
2108
|
+
// rectangle found nothing below the first screen: once Wikipedia was
|
|
2109
|
+
// scrolled, no link lit up. One whose reach is not known yet — before its
|
|
2110
|
+
// first paint — is walked into at its rectangle, as before.
|
|
2111
|
+
//
|
|
2112
|
+
// A box that clips what overflows it hides what it holds past its edge —
|
|
2113
|
+
// but not the positioned boxes whose containing block is outside it (CSS
|
|
2114
|
+
// 2.1 11.1.1), which paint puts off until its clip ends
|
|
2115
|
+
// (`paintPositioned`). So past the edge the walk goes on only into a box
|
|
2116
|
+
// that holds positioned ones, carrying the clips the point is outside
|
|
2117
|
+
// of, and names nothing until a positioned box escapes them all: the
|
|
2118
|
+
// Zen Garden's archive links are absolute `<li>`s in an `overflow:
|
|
2119
|
+
// hidden` list with no height of its own, and not one of them could be
|
|
2120
|
+
// hovered or pressed.
|
|
2121
|
+
const enter = (
|
|
2122
|
+
child: Box,
|
|
2123
|
+
context: readonly number[],
|
|
2124
|
+
clipped: readonly Box[],
|
|
2125
|
+
): void => {
|
|
2126
|
+
const style = child.style;
|
|
2127
|
+
if (
|
|
2128
|
+
style.position !== 'static' ||
|
|
2129
|
+
(child.parent?.kind === 'flex' && typeof style.zIndex === 'number')
|
|
2130
|
+
) {
|
|
2131
|
+
// a flex item with a `z-index` is layered unpositioned (`layered`)
|
|
2132
|
+
const z = style.zIndex === 'auto' ? 0 : style.zIndex;
|
|
2133
|
+
context = [...context, z < 0 ? HIT_NEGATIVE : HIT_POSITIONED, z];
|
|
2134
|
+
} else if (style.float !== 'none') context = [...context, HIT_FLOAT];
|
|
2135
|
+
if (clipped.length !== 0 && child.outOfFlow) {
|
|
2136
|
+
const containing = containingBlockOf(child);
|
|
2137
|
+
clipped = containing
|
|
2138
|
+
? clipped.filter((clip) => holds(clip, containing))
|
|
2139
|
+
: [];
|
|
2140
|
+
}
|
|
2141
|
+
const inside =
|
|
2142
|
+
x >= child.x &&
|
|
2143
|
+
x < child.x + child.width &&
|
|
2144
|
+
y >= child.y &&
|
|
2145
|
+
y < child.y + child.height;
|
|
2146
|
+
const own = inside && hasRect(child);
|
|
2147
|
+
if (!own) {
|
|
2148
|
+
const reach = child.boundsY;
|
|
2149
|
+
const known =
|
|
2150
|
+
Number.isFinite(reach) &&
|
|
2151
|
+
(child.boundsWidth > 0 || child.boundsHeight > 0);
|
|
2152
|
+
if (!known) {
|
|
2153
|
+
if (!inside) return;
|
|
2154
|
+
} else if (
|
|
2155
|
+
x < child.boundsX ||
|
|
2156
|
+
x >= child.boundsX + child.boundsWidth ||
|
|
2157
|
+
y < reach ||
|
|
2158
|
+
y >= reach + child.boundsHeight
|
|
2159
|
+
) {
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
if (clipsOverflow(child)) {
|
|
2163
|
+
if (!holdsAbsolute(child)) return;
|
|
2164
|
+
clipped = [...clipped, child];
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
if (own && child.el && clipped.length === 0) {
|
|
2168
|
+
take(child.el, [...context, HIT_BLOCK], false);
|
|
2169
|
+
}
|
|
2170
|
+
visit(child, context, clipped);
|
|
2171
|
+
};
|
|
2172
|
+
const visit = (
|
|
2173
|
+
node: Box,
|
|
2174
|
+
context: readonly number[],
|
|
2175
|
+
clipped: readonly Box[],
|
|
2176
|
+
): void => {
|
|
994
2177
|
// The paint index answers a point query too — the wide level of a flat
|
|
995
2178
|
// document is the root's child list, and a hit test that walked all of
|
|
996
2179
|
// it would run per pointer move once hover is in the picture.
|
|
@@ -999,55 +2182,37 @@ function deepestAt(box: Box, x: number, y: number): Element | null {
|
|
|
999
2182
|
: node.children;
|
|
1000
2183
|
for (const child of candidates) {
|
|
1001
2184
|
if (child.kind === 'text' || child.kind === 'break') continue;
|
|
1002
|
-
|
|
1003
|
-
x >= child.x &&
|
|
1004
|
-
x < child.x + child.width &&
|
|
1005
|
-
y >= child.y &&
|
|
1006
|
-
y < child.y + child.height
|
|
1007
|
-
) {
|
|
1008
|
-
if (child.el) found = child.el;
|
|
1009
|
-
visit(child);
|
|
1010
|
-
}
|
|
2185
|
+
enter(child, context, clipped);
|
|
1011
2186
|
}
|
|
1012
2187
|
if (node.paintIndex && node.positionedPaint) {
|
|
1013
2188
|
for (const child of node.positionedPaint) {
|
|
1014
|
-
|
|
1015
|
-
x >= child.x &&
|
|
1016
|
-
x < child.x + child.width &&
|
|
1017
|
-
y >= child.y &&
|
|
1018
|
-
y < child.y + child.height
|
|
1019
|
-
) {
|
|
1020
|
-
if (child.el) found = child.el;
|
|
1021
|
-
visit(child);
|
|
1022
|
-
}
|
|
2189
|
+
enter(child, context, clipped);
|
|
1023
2190
|
}
|
|
1024
2191
|
}
|
|
1025
2192
|
if (node.lines) {
|
|
1026
2193
|
for (const line of node.lines) {
|
|
1027
2194
|
for (const placed of line.atomics) {
|
|
1028
|
-
|
|
1029
|
-
x >= placed.box.x &&
|
|
1030
|
-
x < placed.box.x + placed.box.width &&
|
|
1031
|
-
y >= placed.box.y &&
|
|
1032
|
-
y < placed.box.y + placed.box.height
|
|
1033
|
-
) {
|
|
1034
|
-
if (placed.box.el) found = placed.box.el;
|
|
1035
|
-
visit(placed.box);
|
|
1036
|
-
}
|
|
2195
|
+
enter(placed.box, [...context, HIT_INLINE], clipped);
|
|
1037
2196
|
}
|
|
1038
2197
|
// An inline box has no box of its own — its extent is the runs on
|
|
1039
2198
|
// this line — so the element under a point inside a paragraph is
|
|
1040
|
-
// found from the run rather than from a rectangle
|
|
1041
|
-
|
|
2199
|
+
// found from the run rather than from a rectangle: from where its
|
|
2200
|
+
// text sits in the document, whose text boxes know their element.
|
|
2201
|
+
// Not from the run itself, whose layout may be one an earlier parse
|
|
2202
|
+
// made (`TextLayoutCache`), and which an engine may hand back with
|
|
2203
|
+
// nothing on it but its extent.
|
|
2204
|
+
if (clipped.length === 0 && y >= line.y && y < line.y + line.height) {
|
|
1042
2205
|
for (const text of line.texts) {
|
|
1043
2206
|
const natural = text.layout.lines[text.layoutLine];
|
|
1044
2207
|
if (!natural) continue;
|
|
1045
2208
|
for (const run of natural.runs) {
|
|
1046
2209
|
const left = text.drawX + natural.x + run.x;
|
|
1047
2210
|
if (x >= left && x < left + run.width) {
|
|
1048
|
-
const owner = (
|
|
1049
|
-
|
|
1050
|
-
|
|
2211
|
+
const owner = ownerOf(
|
|
2212
|
+
tree.textBoxes,
|
|
2213
|
+
text.spans.documentAt(run.start),
|
|
2214
|
+
);
|
|
2215
|
+
if (owner) take(owner, [...context, HIT_INLINE], true);
|
|
1051
2216
|
}
|
|
1052
2217
|
}
|
|
1053
2218
|
}
|
|
@@ -1055,8 +2220,272 @@ function deepestAt(box: Box, x: number, y: number): Element | null {
|
|
|
1055
2220
|
}
|
|
1056
2221
|
}
|
|
1057
2222
|
};
|
|
1058
|
-
visit(box);
|
|
2223
|
+
visit(box, [], []);
|
|
2224
|
+
if (hit) hit.text = viaText;
|
|
1059
2225
|
return found;
|
|
1060
2226
|
}
|
|
1061
2227
|
|
|
2228
|
+
/** Paint order between two hits' layers, outermost first: negative where
|
|
2229
|
+
* `a` was painted under `b`. */
|
|
2230
|
+
function compareKeys(a: readonly number[], b: readonly number[]): number {
|
|
2231
|
+
const n = Math.min(a.length, b.length);
|
|
2232
|
+
for (let i = 0; i < n; i += 1) if (a[i] !== b[i]) return a[i] - b[i];
|
|
2233
|
+
return a.length - b.length;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// --- a pointer move restyled where it happened (`_hoverInPlace`) -----------
|
|
2237
|
+
|
|
2238
|
+
/** How many elements a pointer move restyles in place before the document
|
|
2239
|
+
* is built again instead: a link's subtree is a handful, and a compound
|
|
2240
|
+
* that matches a container reaches everything in it. */
|
|
2241
|
+
const HOVER_RESTYLE_LIMIT = 300;
|
|
2242
|
+
|
|
2243
|
+
/**
|
|
2244
|
+
* The computed properties a pointer move may change in place: ink, which
|
|
2245
|
+
* moves nothing. A background colour is drawn inside the box it colours
|
|
2246
|
+
* and a border's colour on the border it has; `box-shadow`, `text-shadow`
|
|
2247
|
+
* and an outline's width reach past it, where the paint index has already
|
|
2248
|
+
* looked, and a background image has to be fetched.
|
|
2249
|
+
*/
|
|
2250
|
+
const PAINT_ONLY = new Set([
|
|
2251
|
+
'color',
|
|
2252
|
+
'textFillColor',
|
|
2253
|
+
'cursor',
|
|
2254
|
+
'backgroundColor',
|
|
2255
|
+
'borderTopColor',
|
|
2256
|
+
'borderRightColor',
|
|
2257
|
+
'borderBottomColor',
|
|
2258
|
+
'borderLeftColor',
|
|
2259
|
+
'outlineColor',
|
|
2260
|
+
'textDecorationLine',
|
|
2261
|
+
'textDecorationColor',
|
|
2262
|
+
'textDecorationStyle',
|
|
2263
|
+
'textDecorationThickness',
|
|
2264
|
+
'textUnderlineOffset',
|
|
2265
|
+
'underline',
|
|
2266
|
+
'underlineStyle',
|
|
2267
|
+
'underlineThickness',
|
|
2268
|
+
'underlineOffset',
|
|
2269
|
+
'lineThrough',
|
|
2270
|
+
]);
|
|
2271
|
+
|
|
2272
|
+
/** The fields of a run its ink is: what `runFor` takes from `PAINT_ONLY`. */
|
|
2273
|
+
const INK_FIELDS = [
|
|
2274
|
+
'color',
|
|
2275
|
+
'underline',
|
|
2276
|
+
'underlineStyle',
|
|
2277
|
+
'underlineOffset',
|
|
2278
|
+
'underlineThickness',
|
|
2279
|
+
'strike',
|
|
2280
|
+
] as const;
|
|
2281
|
+
|
|
2282
|
+
/** The fields of a run its shape is, which ink never changes. */
|
|
2283
|
+
const FACE_FIELDS = [
|
|
2284
|
+
'family',
|
|
2285
|
+
'size',
|
|
2286
|
+
'weight',
|
|
2287
|
+
'style',
|
|
2288
|
+
'letterSpacing',
|
|
2289
|
+
'features',
|
|
2290
|
+
] as const;
|
|
2291
|
+
|
|
2292
|
+
/** Whether two styles differ in ink alone: true where they do, null where
|
|
2293
|
+
* they do not differ, false where something else does. Custom properties
|
|
2294
|
+
* are read through the properties that use them. */
|
|
2295
|
+
function inkOnly(was: ComputedStyle, now: ComputedStyle): boolean | null {
|
|
2296
|
+
const a = was as unknown as Record<string, unknown>;
|
|
2297
|
+
const b = now as unknown as Record<string, unknown>;
|
|
2298
|
+
let ink = false;
|
|
2299
|
+
for (const key in b) {
|
|
2300
|
+
if (key === 'custom' || sameValue(a[key], b[key])) continue;
|
|
2301
|
+
if (!PAINT_ONLY.has(key)) return false;
|
|
2302
|
+
ink = true;
|
|
2303
|
+
}
|
|
2304
|
+
return ink ? true : null;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/** Structural equality for a computed value: a number, a string, or the
|
|
2308
|
+
* plain objects and arrays a length or a shadow list is. */
|
|
2309
|
+
function sameValue(x: unknown, y: unknown): boolean {
|
|
2310
|
+
if (x === y) return true;
|
|
2311
|
+
if (typeof x === 'number' && typeof y === 'number') {
|
|
2312
|
+
return Number.isNaN(x) && Number.isNaN(y);
|
|
2313
|
+
}
|
|
2314
|
+
if (!x || !y || typeof x !== 'object' || typeof y !== 'object') return false;
|
|
2315
|
+
if (Array.isArray(x) !== Array.isArray(y)) return false;
|
|
2316
|
+
const a = x as Record<string, unknown>;
|
|
2317
|
+
const b = y as Record<string, unknown>;
|
|
2318
|
+
const keys = Object.keys(a);
|
|
2319
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
2320
|
+
for (const key of keys) if (!sameValue(a[key], b[key])) return false;
|
|
2321
|
+
return true;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
/** The element an anonymous box takes its style from: its nearest
|
|
2325
|
+
* ancestor's that has one. */
|
|
2326
|
+
function nearestElement(box: Box): Element | null {
|
|
2327
|
+
for (let at = box.parent; at; at = at.parent) if (at.el) return at.el;
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
/**
|
|
2332
|
+
* `text`'s layout made again with the ink of the elements a pointer move
|
|
2333
|
+
* restyled: from the runs it was made from, each of a changed element's
|
|
2334
|
+
* given the ink its new style makes (`runFor`) and nothing else. Null where
|
|
2335
|
+
* none of its runs is theirs; false where that cannot be told — a run this
|
|
2336
|
+
* cannot place in the document, one some other pass inked (a first line's),
|
|
2337
|
+
* or a layout that comes out another shape.
|
|
2338
|
+
*/
|
|
2339
|
+
function reinked(
|
|
2340
|
+
text: LineText,
|
|
2341
|
+
layouts: TextLayoutCache,
|
|
2342
|
+
changed: ReadonlyMap<Element, ComputedStyle>,
|
|
2343
|
+
styles: BoxTree['styles'],
|
|
2344
|
+
): TextLayoutLike | null | false {
|
|
2345
|
+
const spans = text.spans;
|
|
2346
|
+
const ownerAt = (offset: number): Element | null =>
|
|
2347
|
+
spans.boxAt ? (spans.boxAt(offset)?.el ?? null) : null;
|
|
2348
|
+
const inputs = layouts.inputsOf(text.layout);
|
|
2349
|
+
if (!inputs || !spans.boxAt) {
|
|
2350
|
+
// not one this could make again: none of its text may be theirs
|
|
2351
|
+
for (const line of text.layout.lines) {
|
|
2352
|
+
for (const run of line.runs) {
|
|
2353
|
+
const owner = spans.boxAt ? ownerAt(run.start) : null;
|
|
2354
|
+
if (!spans.boxAt || (owner && changed.has(owner))) return false;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
return null;
|
|
2358
|
+
}
|
|
2359
|
+
let runs: TextRun[] | null = null;
|
|
2360
|
+
let offset = 0;
|
|
2361
|
+
const content = inputs.content;
|
|
2362
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
2363
|
+
const run = content[i];
|
|
2364
|
+
const length = run.text.length;
|
|
2365
|
+
const owner = length ? ownerAt(offset) : null;
|
|
2366
|
+
const style = owner ? changed.get(owner) : undefined;
|
|
2367
|
+
if (owner && style) {
|
|
2368
|
+
if (ownerAt(offset + length - 1) !== owner) return false;
|
|
2369
|
+
const was = runFor(run.text, styles.get(owner)!.style);
|
|
2370
|
+
const now = runFor(run.text, style);
|
|
2371
|
+
for (const f of FACE_FIELDS) if (!sameValue(was[f], now[f])) return false;
|
|
2372
|
+
let next: Record<string, unknown> | null = null;
|
|
2373
|
+
for (const f of INK_FIELDS) {
|
|
2374
|
+
// a run that is not what its style made — a first line's colour
|
|
2375
|
+
if (!sameValue(run[f], was[f])) return false;
|
|
2376
|
+
if (sameValue(was[f], now[f])) continue;
|
|
2377
|
+
next ??= { ...run };
|
|
2378
|
+
if (now[f] === undefined) delete next[f];
|
|
2379
|
+
else next[f] = now[f];
|
|
2380
|
+
}
|
|
2381
|
+
if (next) {
|
|
2382
|
+
runs ??= content.slice();
|
|
2383
|
+
runs[i] = next as unknown as TextRun;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
offset += length;
|
|
2387
|
+
}
|
|
2388
|
+
if (!runs) return null;
|
|
2389
|
+
const layout = layouts.fonts.layout(runs, inputs.style, inputs.options);
|
|
2390
|
+
return sameShape(text.layout, layout) ? layout : false;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
/** Whether two layouts put the same text in the same places: ink aside,
|
|
2394
|
+
* the same layout. */
|
|
2395
|
+
function sameShape(a: TextLayoutLike, b: TextLayoutLike): boolean {
|
|
2396
|
+
if (a.width !== b.width || a.height !== b.height) return false;
|
|
2397
|
+
if (a.lines.length !== b.lines.length) return false;
|
|
2398
|
+
for (let i = 0; i < a.lines.length; i += 1) {
|
|
2399
|
+
const x = a.lines[i];
|
|
2400
|
+
const y = b.lines[i];
|
|
2401
|
+
if (
|
|
2402
|
+
x.x !== y.x ||
|
|
2403
|
+
x.y !== y.y ||
|
|
2404
|
+
x.width !== y.width ||
|
|
2405
|
+
x.height !== y.height ||
|
|
2406
|
+
x.baseline !== y.baseline ||
|
|
2407
|
+
x.ascent !== y.ascent ||
|
|
2408
|
+
x.descent !== y.descent ||
|
|
2409
|
+
x.start !== y.start ||
|
|
2410
|
+
x.end !== y.end ||
|
|
2411
|
+
x.runs.length !== y.runs.length
|
|
2412
|
+
) {
|
|
2413
|
+
return false;
|
|
2414
|
+
}
|
|
2415
|
+
for (let j = 0; j < x.runs.length; j += 1) {
|
|
2416
|
+
const r = x.runs[j];
|
|
2417
|
+
const t = y.runs[j];
|
|
2418
|
+
if (
|
|
2419
|
+
r.x !== t.x ||
|
|
2420
|
+
r.width !== t.width ||
|
|
2421
|
+
r.start !== t.start ||
|
|
2422
|
+
r.end !== t.end
|
|
2423
|
+
) {
|
|
2424
|
+
return false;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
return true;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
/** The element whose text holds a document index: the text box around it,
|
|
2432
|
+
* found by bisecting the boxes in document order. */
|
|
2433
|
+
function ownerOf(boxes: readonly Box[], index: number): Element | null {
|
|
2434
|
+
let lo = 0;
|
|
2435
|
+
let hi = boxes.length - 1;
|
|
2436
|
+
while (lo < hi) {
|
|
2437
|
+
const mid = (lo + hi + 1) >> 1;
|
|
2438
|
+
if (boxes[mid].textStart <= index) lo = mid;
|
|
2439
|
+
else hi = mid - 1;
|
|
2440
|
+
}
|
|
2441
|
+
const box = boxes[lo];
|
|
2442
|
+
return box && index >= box.textStart && index < box.textEnd ? box.el : null;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
1062
2445
|
export type { ControlRect, ReplacedKind, ResourceRequest, ResourceResult };
|
|
2446
|
+
|
|
2447
|
+
/** The advance of a face's "0", laid out: null where the engine cannot
|
|
2448
|
+
* lay it out. */
|
|
2449
|
+
function zeroWidthOf(fonts: FontsLike, face: MetricFace): number | null {
|
|
2450
|
+
const { family, size, weight } = face;
|
|
2451
|
+
// a run is upright or slanted, as `inline.ts` sets one
|
|
2452
|
+
const style = face.style === 'normal' ? 'normal' : 'italic';
|
|
2453
|
+
try {
|
|
2454
|
+
const run = { family, size, weight, style } as const;
|
|
2455
|
+
return fonts.layout([{ text: '0', ...run }], run, {}).width;
|
|
2456
|
+
} catch {
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
/** A face's own line height, as the engine reports it: null where it
|
|
2462
|
+
* cannot say. */
|
|
2463
|
+
function normalLineOf(fonts: FontsLike, face: MetricFace): number | null {
|
|
2464
|
+
const { family, size, weight, style } = face;
|
|
2465
|
+
try {
|
|
2466
|
+
const line = fonts
|
|
2467
|
+
.match(family, { size, weight, style })
|
|
2468
|
+
.metrics(size).lineHeight;
|
|
2469
|
+
return line > 0 ? line : null;
|
|
2470
|
+
} catch {
|
|
2471
|
+
return null;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
/** A face's x-height, as the engine reports it: null where the face
|
|
2476
|
+
* states none, or the engine does not say. */
|
|
2477
|
+
function xHeightOf(fonts: FontsLike, face: MetricFace): number | null {
|
|
2478
|
+
const { family, size, weight, style } = face;
|
|
2479
|
+
try {
|
|
2480
|
+
const metrics = fonts
|
|
2481
|
+
.match(family, { size, weight, style })
|
|
2482
|
+
.metrics(size) as {
|
|
2483
|
+
xHeight?: number | null;
|
|
2484
|
+
};
|
|
2485
|
+
const x = metrics.xHeight;
|
|
2486
|
+
// NaN where the face states none, which is a number
|
|
2487
|
+
return typeof x === 'number' && x > 0 ? x : null;
|
|
2488
|
+
} catch {
|
|
2489
|
+
return null;
|
|
2490
|
+
}
|
|
2491
|
+
}
|