@react-x11/components 0.10.0 → 0.12.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/dist/code/index.d.ts.map +1 -1
- package/dist/code/index.js +25 -24
- package/dist/code/index.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/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/codeblock/index.d.ts +13 -0
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +37 -1
- package/dist/codeblock/index.js.map +1 -1
- package/dist/flow/node.d.ts +14 -6
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +82 -64
- package/dist/flow/node.js.map +1 -1
- package/dist/html/controls.d.ts +26 -7
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +28 -3
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +32 -10
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +89 -31
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +19 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +86 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +11 -0
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +114 -4
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +45 -5
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +363 -14
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.js +22 -5
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +10 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +27 -2
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/css/vars.d.ts +5 -0
- package/dist/html/css/vars.d.ts.map +1 -1
- package/dist/html/css/vars.js +2 -2
- package/dist/html/css/vars.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +35 -2
- package/dist/html/dom.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +38 -1
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +11 -2
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +306 -25
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +61 -6
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +65 -16
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +2 -2
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +8 -3
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/floats.d.ts +4 -0
- package/dist/html/layout/floats.d.ts.map +1 -1
- package/dist/html/layout/floats.js +26 -0
- package/dist/html/layout/floats.js.map +1 -1
- package/dist/html/layout/inline.d.ts +29 -2
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +1454 -125
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +33 -0
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +608 -86
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +53 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +746 -214
- package/dist/html/paint.js.map +1 -1
- package/dist/html/surfaces.d.ts +7 -2
- package/dist/html/surfaces.d.ts.map +1 -1
- package/dist/html/surfaces.js +16 -0
- package/dist/html/surfaces.js.map +1 -1
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +27 -20
- package/dist/html/svg.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/window.d.ts +22 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +35 -8
- package/dist/internal/window.js.map +1 -1
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +98 -26
- package/dist/markdown/index.js.map +1 -1
- package/dist/rich-text-editor/index.d.ts.map +1 -1
- package/dist/rich-text-editor/index.js +3 -8
- package/dist/rich-text-editor/index.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/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/richtext/node.d.ts +7 -0
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/terminal-output/index.d.ts.map +1 -1
- package/dist/terminal-output/index.js +100 -25
- 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/package.json +5 -3
- package/src/code/index.ts +34 -32
- package/src/code-language/index.ts +1 -1
- package/src/code-language/runs.ts +111 -1
- package/src/codeblock/index.ts +48 -1
- package/src/flow/node.ts +80 -64
- package/src/html/controls.ts +50 -9
- package/src/html/css/cascade.ts +134 -43
- package/src/html/css/color.ts +87 -0
- package/src/html/css/parse.ts +107 -4
- package/src/html/css/style.ts +386 -23
- package/src/html/css/ua.ts +22 -5
- package/src/html/css/values.ts +31 -3
- package/src/html/css/vars.ts +2 -2
- package/src/html/dom.ts +38 -2
- package/src/html/index.ts +39 -1
- package/src/html/layout/block.ts +360 -22
- package/src/html/layout/boxes.ts +98 -23
- package/src/html/layout/cache.ts +8 -3
- package/src/html/layout/floats.ts +24 -0
- package/src/html/layout/inline.ts +1690 -119
- package/src/html/node.ts +648 -101
- package/src/html/paint.ts +883 -283
- package/src/html/surfaces.ts +21 -1
- package/src/html/svg.ts +30 -19
- package/src/internal/codelines.ts +200 -0
- package/src/internal/window.ts +36 -8
- package/src/markdown/index.ts +145 -26
- package/src/rich-text-editor/index.ts +3 -11
- package/src/rich-text-editor/markdown.ts +36 -1
- package/src/rich-text-editor/replace.ts +85 -0
- package/src/richtext/node.ts +7 -0
- package/src/terminal-output/index.ts +127 -34
- package/src/terminal-output/runs.ts +16 -2
package/dist/html/node.js
CHANGED
|
@@ -43,17 +43,17 @@ import { attr, HtmlSource, imageUrlOf, isElement, tagOf } from './dom.js';
|
|
|
43
43
|
import { Cascade } from './css/cascade.js';
|
|
44
44
|
import { mediaMatches, parseStylesheet } from './css/parse.js';
|
|
45
45
|
import { uaStylesheet } from './css/ua.js';
|
|
46
|
-
import { buildBoxes, CONTENT_IMAGES } from './layout/boxes.js';
|
|
47
|
-
import { layoutDocument } from './layout/block.js';
|
|
46
|
+
import { BOX_RAISES, buildBoxes, CONTENT_IMAGES, CUT_BLOCKS, GENERATED_FROM, INLINE_OFFSETS, LINE_BOX_RAISES, SHIFTED_LINES, TEXT_SHIFTS, } from './layout/boxes.js';
|
|
47
|
+
import { layoutDocument, retranslate, transformed } from './layout/block.js';
|
|
48
48
|
import { TextLayoutCache } from './layout/cache.js';
|
|
49
49
|
import { shapingSafe } from './layout/shaping.js';
|
|
50
|
-
import { SurfaceCache } from './surfaces.js';
|
|
51
|
-
import { inlineDecoration, runFor } from './layout/inline.js';
|
|
50
|
+
import { SurfaceCache, newSurface } from './surfaces.js';
|
|
51
|
+
import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
|
|
52
52
|
// Through the inline module rather than a second cache: the offsets table for
|
|
53
53
|
// a layout is built once, on the first selection that needs it.
|
|
54
54
|
import { documentOffsetOf, layoutOffsetOf, layoutOffsets as layoutOffsetsOf, } from './layout/inline.js';
|
|
55
55
|
import { lineBands as bandsFor } from '../richtext/runs.js';
|
|
56
|
-
import { clipsOverflow, containingBlockOf, hasRect, holds, holdsAbsolute, paintDocument, queryChildIndex, } from './paint.js';
|
|
56
|
+
import { clipsOverflow, computePaintBounds, containingBlockOf, FIXED_BOXES, fixedToViewport, forgetDecoratedAncestors, hasRect, hoistNegative, holds, holdsAbsolute, paintDocument, queryChildIndex, stackLayers, } from './paint.js';
|
|
57
57
|
import { controlRectsOf, measureControl } from './controls.js';
|
|
58
58
|
import { ResourceStore } from './resources.js';
|
|
59
59
|
import { WebFonts } from './fonts.js';
|
|
@@ -118,6 +118,8 @@ export class HtmlViewNode extends Node {
|
|
|
118
118
|
_sheetsRead = null;
|
|
119
119
|
/** Blurred shadows, drawn once each. */
|
|
120
120
|
_shadowCache = null;
|
|
121
|
+
/** Whether the backend was found to have no offscreen surface. */
|
|
122
|
+
_noSurface = false;
|
|
121
123
|
_cascade = null;
|
|
122
124
|
_tree = null;
|
|
123
125
|
/** The faces `_warmFaces` has asked the font matcher for, and of which
|
|
@@ -315,14 +317,16 @@ export class HtmlViewNode extends Node {
|
|
|
315
317
|
*/
|
|
316
318
|
_requestBackgrounds(tree) {
|
|
317
319
|
for (const box of tree.backgrounds) {
|
|
318
|
-
|
|
320
|
+
// a pseudo-element's are its element's
|
|
321
|
+
const element = box.el ?? GENERATED_FROM.get(box);
|
|
322
|
+
if (!element)
|
|
319
323
|
continue;
|
|
320
324
|
// each layer's, where there is more than one
|
|
321
325
|
for (const url of box.style.backgroundImages ?? [
|
|
322
326
|
box.style.backgroundImage,
|
|
323
327
|
]) {
|
|
324
328
|
if (typeof url === 'string') {
|
|
325
|
-
this._resources.request({ url, kind: 'image', element
|
|
329
|
+
this._resources.request({ url, kind: 'image', element });
|
|
326
330
|
}
|
|
327
331
|
}
|
|
328
332
|
const border = box.style.borderImage.source;
|
|
@@ -330,9 +334,15 @@ export class HtmlViewNode extends Node {
|
|
|
330
334
|
this._resources.request({
|
|
331
335
|
url: border,
|
|
332
336
|
kind: 'image',
|
|
333
|
-
element
|
|
337
|
+
element,
|
|
334
338
|
});
|
|
335
339
|
}
|
|
340
|
+
// and each mask layer's, which is an image as a background's is
|
|
341
|
+
for (const url of box.style.mask.images) {
|
|
342
|
+
if (typeof url === 'string') {
|
|
343
|
+
this._resources.request({ url, kind: 'image', element });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
336
346
|
}
|
|
337
347
|
// and every image generated content names, which is only known there
|
|
338
348
|
// too
|
|
@@ -463,7 +473,7 @@ export class HtmlViewNode extends Node {
|
|
|
463
473
|
order += sheet.rules.length + 1;
|
|
464
474
|
sheets.push(sheet);
|
|
465
475
|
}
|
|
466
|
-
this._cascade = new Cascade(sheets, look, width, this._viewportHeight(), this._scale, fonts ? (
|
|
476
|
+
this._cascade = new Cascade(sheets, look, width, this._viewportHeight(), this._scale, fonts ? (face) => xHeightOf(fonts, face) : null, fonts ? (face) => zeroWidthOf(fonts, face) : null, fonts ? (face) => normalLineOf(fonts, face) : null, faces.length ? this._webFonts : null);
|
|
467
477
|
this._sheetsRead = {
|
|
468
478
|
look,
|
|
469
479
|
scale: this._scale,
|
|
@@ -548,6 +558,58 @@ export class HtmlViewNode extends Node {
|
|
|
548
558
|
}
|
|
549
559
|
return true;
|
|
550
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* The viewport the document is seen through, in window coordinates: the
|
|
563
|
+
* content box of the pane that scrolls the element, where one does —
|
|
564
|
+
* what `position: fixed` and a fixed background are placed against, as
|
|
565
|
+
* a browser places them against its page area. Null where nothing
|
|
566
|
+
* scrolls it, and the element is its own.
|
|
567
|
+
*/
|
|
568
|
+
_viewport() {
|
|
569
|
+
for (let node = this.parent; node; node = node.parent) {
|
|
570
|
+
// `Scrollable`'s, and not every node's: asked the way core asks it
|
|
571
|
+
const scroller = node;
|
|
572
|
+
if (!scroller.isScroller?.())
|
|
573
|
+
continue;
|
|
574
|
+
const box = node.contentBox();
|
|
575
|
+
return box.width > 0 && box.height > 0 ? box : null;
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* What the document draws fixed to the viewport of the pane that scrolls
|
|
581
|
+
* it, for that pane's scroll blit to repaint rather than copy with the
|
|
582
|
+
* text (react-x11's `viewportFixedRects`): the whole viewport where a
|
|
583
|
+
* fixed background shows through it, which makes a scroll a repaint, and
|
|
584
|
+
* where each fixed box is drawn. Null for a document with neither, and
|
|
585
|
+
* where nothing scrolls the element.
|
|
586
|
+
*/
|
|
587
|
+
viewportFixedRects() {
|
|
588
|
+
const tree = this._tree;
|
|
589
|
+
const viewport = tree && this._viewport();
|
|
590
|
+
if (!tree || !viewport)
|
|
591
|
+
return null;
|
|
592
|
+
if (hasFixedBackground(tree))
|
|
593
|
+
return [viewport];
|
|
594
|
+
const boxes = FIXED_BOXES.get(tree);
|
|
595
|
+
if (!boxes)
|
|
596
|
+
return null;
|
|
597
|
+
// laid out against the viewport at the document's top, and drawn at the
|
|
598
|
+
// viewport (`atViewport`)
|
|
599
|
+
const root = tree.root;
|
|
600
|
+
const out = [];
|
|
601
|
+
for (const box of boxes) {
|
|
602
|
+
if (!(box.boundsWidth > 0 && box.boundsHeight > 0))
|
|
603
|
+
continue;
|
|
604
|
+
out.push({
|
|
605
|
+
x: viewport.x + box.boundsX - root.x,
|
|
606
|
+
y: viewport.y + box.boundsY - root.y,
|
|
607
|
+
width: box.boundsWidth,
|
|
608
|
+
height: box.boundsHeight,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
return out.length ? out : null;
|
|
612
|
+
}
|
|
551
613
|
_viewportHeight() {
|
|
552
614
|
// The viewport a `vh` resolves against is the one the document is seen
|
|
553
615
|
// through, not the document — a document taller than it does not make
|
|
@@ -1002,13 +1064,52 @@ export class HtmlViewNode extends Node {
|
|
|
1002
1064
|
}
|
|
1003
1065
|
else {
|
|
1004
1066
|
const bands = [];
|
|
1005
|
-
|
|
1006
|
-
|
|
1067
|
+
const fonts = this._layouts?.fonts ?? null;
|
|
1068
|
+
// an inline box broken around the blocks in it is its pieces, a box
|
|
1069
|
+
// of the element's each (CSS 2.1 9.2.1.1), and its fragments are
|
|
1070
|
+
// theirs; and on the lines between them it has a fragment around each
|
|
1071
|
+
// block, across the box the block is in and as tall as its border
|
|
1072
|
+
// box, as a browser's has — Blink's block-in-inline, Gecko's split
|
|
1073
|
+
// inline's anonymous block. A list item made inline around a block
|
|
1074
|
+
// link measured as the empty edge after it
|
|
1075
|
+
for (const piece of piecesOf(tree.root, box)) {
|
|
1076
|
+
inlineBands(tree.root, piece, fonts, bands);
|
|
1077
|
+
}
|
|
1078
|
+
for (const block of (box.cut ? CUT_BLOCKS.get(box) : null) ?? []) {
|
|
1079
|
+
const around = block.parent;
|
|
1080
|
+
if (!around)
|
|
1081
|
+
continue;
|
|
1082
|
+
bands.push({
|
|
1083
|
+
x: around.contentX,
|
|
1084
|
+
y: block.y,
|
|
1085
|
+
width: around.contentWidth,
|
|
1086
|
+
height: block.height,
|
|
1087
|
+
});
|
|
1007
1088
|
}
|
|
1008
|
-
if (box.kind === 'inline')
|
|
1009
|
-
fragmentReach(box, bands);
|
|
1010
1089
|
for (const band of bands)
|
|
1011
1090
|
rect = rect ? unionRect(rect, band) : band;
|
|
1091
|
+
if (!rect) {
|
|
1092
|
+
// on no line, where its block has none: text set at no size takes
|
|
1093
|
+
// no room and gets no box, and CSS puts it on lines of no height
|
|
1094
|
+
// at the block's top (9.4.2), as a browser measures it — nothing,
|
|
1095
|
+
// where the block's content starts. Where the text after it
|
|
1096
|
+
// starts was the next text that took room, elsewhere in the
|
|
1097
|
+
// document: the Zen Garden's 170 hides a heading at `font-size:
|
|
1098
|
+
// 0`, and an `<abbr>` in it measured as the end of the paragraph
|
|
1099
|
+
// above it
|
|
1100
|
+
let block = box.parent;
|
|
1101
|
+
while (block && block.kind === 'inline')
|
|
1102
|
+
block = block.parent;
|
|
1103
|
+
if (block && !block.lines?.length) {
|
|
1104
|
+
const rtl = block.style.direction === 'rtl';
|
|
1105
|
+
rect = {
|
|
1106
|
+
x: rtl ? block.contentX + block.contentWidth : block.contentX,
|
|
1107
|
+
y: block.contentY,
|
|
1108
|
+
width: 0,
|
|
1109
|
+
height: 0,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1012
1113
|
if (!rect) {
|
|
1013
1114
|
// an empty subtree's range is `[0, 0)`, wherever it stands: where
|
|
1014
1115
|
// its text would be is where the text after it starts
|
|
@@ -1032,7 +1133,20 @@ export class HtmlViewNode extends Node {
|
|
|
1032
1133
|
if (!tree)
|
|
1033
1134
|
return null;
|
|
1034
1135
|
const local = this._toDocument(x, y);
|
|
1035
|
-
return deepestAt(tree, local.x, local.y);
|
|
1136
|
+
return deepestAt(tree, local.x, local.y, undefined, this._fixedShift());
|
|
1137
|
+
}
|
|
1138
|
+
/** How far a box fixed to the viewport is drawn from where it was laid
|
|
1139
|
+
* out, in the document's pixels: the scroll of the pane the element is
|
|
1140
|
+
* in (`atViewport`). Null where nothing scrolls it. */
|
|
1141
|
+
_fixedShift() {
|
|
1142
|
+
const tree = this._tree;
|
|
1143
|
+
const viewport = tree && FIXED_BOXES.has(tree) && this._viewport();
|
|
1144
|
+
if (!tree || !viewport)
|
|
1145
|
+
return null;
|
|
1146
|
+
return {
|
|
1147
|
+
x: viewport.x - this.abs.x - tree.root.x,
|
|
1148
|
+
y: viewport.y - this.abs.y - tree.root.y,
|
|
1149
|
+
};
|
|
1036
1150
|
}
|
|
1037
1151
|
/**
|
|
1038
1152
|
* The cursor for a point of this element's, in device pixels: what core
|
|
@@ -1049,7 +1163,7 @@ export class HtmlViewNode extends Node {
|
|
|
1049
1163
|
if (!tree)
|
|
1050
1164
|
return null;
|
|
1051
1165
|
const hit = { text: false };
|
|
1052
|
-
const el = deepestAt(tree, x - this.abs.x, y - this.abs.y, hit);
|
|
1166
|
+
const el = deepestAt(tree, x - this.abs.x, y - this.abs.y, hit, this._fixedShift());
|
|
1053
1167
|
const cursor = el ? tree.styles.get(el)?.style.cursor : null;
|
|
1054
1168
|
// a keyword; a `url()` this cannot load falls back, as its list would
|
|
1055
1169
|
if (cursor && cursor !== 'auto' && /^[a-z-]+$/.test(cursor))
|
|
@@ -1091,10 +1205,8 @@ export class HtmlViewNode extends Node {
|
|
|
1091
1205
|
}
|
|
1092
1206
|
/** The hovered chain moved: restyle where it did, or the document. */
|
|
1093
1207
|
_restyleHover(was, now) {
|
|
1094
|
-
|
|
1095
|
-
if (
|
|
1096
|
-
this.invalidate(false, this, 'props');
|
|
1097
|
-
else if (done === false)
|
|
1208
|
+
// a restyle in place names what it repainted (`_hoverInPlace`)
|
|
1209
|
+
if (this._hoverInPlace(was, now) === false)
|
|
1098
1210
|
this._invalidate(Stale.Boxes);
|
|
1099
1211
|
}
|
|
1100
1212
|
/**
|
|
@@ -1189,6 +1301,11 @@ export class HtmlViewNode extends Node {
|
|
|
1189
1301
|
const fresh = new Map();
|
|
1190
1302
|
const changed = new Map();
|
|
1191
1303
|
let refused = false;
|
|
1304
|
+
// what changed beyond ink (`hoverChange`): how far some box's ink
|
|
1305
|
+
// reaches, the order its layer paints in, where one is
|
|
1306
|
+
let reaches = false;
|
|
1307
|
+
let reorders = false;
|
|
1308
|
+
const moving = new Set();
|
|
1192
1309
|
const styleOf = (el) => {
|
|
1193
1310
|
const kept = tree.styles.get(el);
|
|
1194
1311
|
if (!kept)
|
|
@@ -1208,11 +1325,18 @@ export class HtmlViewNode extends Node {
|
|
|
1208
1325
|
}
|
|
1209
1326
|
const style = cascade.styleFor(el, parentStyle, kept.inFlex);
|
|
1210
1327
|
fresh.set(el, style);
|
|
1211
|
-
const diff =
|
|
1328
|
+
const diff = hoverChange(kept.style, style);
|
|
1212
1329
|
if (diff === false)
|
|
1213
1330
|
refused = true;
|
|
1214
|
-
else if (diff)
|
|
1331
|
+
else if (diff) {
|
|
1215
1332
|
changed.set(el, style);
|
|
1333
|
+
if (diff.reach)
|
|
1334
|
+
reaches = true;
|
|
1335
|
+
if (diff.order)
|
|
1336
|
+
reorders = true;
|
|
1337
|
+
if (diff.move)
|
|
1338
|
+
moving.add(el);
|
|
1339
|
+
}
|
|
1216
1340
|
return style;
|
|
1217
1341
|
};
|
|
1218
1342
|
for (const el of reach) {
|
|
@@ -1246,10 +1370,14 @@ export class HtmlViewNode extends Node {
|
|
|
1246
1370
|
continue;
|
|
1247
1371
|
const kept = tree.styles.get(owner);
|
|
1248
1372
|
// an anonymous box, a pseudo-element, a marker, a control: a style
|
|
1249
|
-
// derived from the element's, or drawn from it somewhere else
|
|
1373
|
+
// derived from the element's, or drawn from it somewhere else — a
|
|
1374
|
+
// widget mounted beside the document takes its look from it. An
|
|
1375
|
+
// image, a drawing or a rule is drawn here, from its box, like any.
|
|
1250
1376
|
if (!box.el || box.style !== kept.style)
|
|
1251
1377
|
return false;
|
|
1252
|
-
if (box.marker || box.replaced
|
|
1378
|
+
if (box.marker || WIDGETS.has(box.replaced))
|
|
1379
|
+
return false;
|
|
1380
|
+
if (moving.has(owner) && !movable(box, kept.style, style))
|
|
1253
1381
|
return false;
|
|
1254
1382
|
restyled.push([box, style]);
|
|
1255
1383
|
if (box.kind === 'inline') {
|
|
@@ -1286,11 +1414,29 @@ export class HtmlViewNode extends Node {
|
|
|
1286
1414
|
}
|
|
1287
1415
|
}
|
|
1288
1416
|
}
|
|
1417
|
+
// what each box drew before, to be repainted with what it draws after
|
|
1418
|
+
const inks = [];
|
|
1419
|
+
for (const [box] of restyled) {
|
|
1420
|
+
const ink = inkOf(box);
|
|
1421
|
+
if (ink)
|
|
1422
|
+
inks.push(ink);
|
|
1423
|
+
}
|
|
1289
1424
|
// and only now, all of it
|
|
1290
|
-
|
|
1425
|
+
const moved = [];
|
|
1426
|
+
for (const [box, style] of restyled) {
|
|
1427
|
+
const was = box.style;
|
|
1291
1428
|
box.style = style;
|
|
1292
|
-
|
|
1429
|
+
if (moving.has(box.el))
|
|
1430
|
+
moved.push([box, was]);
|
|
1431
|
+
}
|
|
1432
|
+
for (const [box, decoration] of redecorated) {
|
|
1433
|
+
// what is inside it keeps which boxes around it paint, and this is
|
|
1434
|
+
// one more or one fewer of them
|
|
1435
|
+
if ((decoration === null) !== (box.decoration === null)) {
|
|
1436
|
+
forgetDecoratedAncestors(box);
|
|
1437
|
+
}
|
|
1293
1438
|
box.decoration = decoration;
|
|
1439
|
+
}
|
|
1294
1440
|
for (const text of texts) {
|
|
1295
1441
|
const next = relaid.get(text.layout);
|
|
1296
1442
|
if (next)
|
|
@@ -1299,8 +1445,72 @@ export class HtmlViewNode extends Node {
|
|
|
1299
1445
|
for (const [el, style] of changed) {
|
|
1300
1446
|
tree.styles.set(el, { style, inFlex: tree.styles.get(el).inFlex });
|
|
1301
1447
|
}
|
|
1448
|
+
// A box that moved moves what is in it, as layout moved it the first
|
|
1449
|
+
// time (`applyRelativeOffsets`), and a later layout moves it the same
|
|
1450
|
+
// way; where it reached the document's end, before or after, the
|
|
1451
|
+
// document's height may have changed with it, which only a layout says.
|
|
1452
|
+
let relayout = false;
|
|
1453
|
+
for (const [box, was] of moved) {
|
|
1454
|
+
const before = box.boundsY + box.boundsHeight;
|
|
1455
|
+
retranslate(box, was);
|
|
1456
|
+
tree.relative = true;
|
|
1457
|
+
if (before >= this._documentHeight)
|
|
1458
|
+
relayout = true;
|
|
1459
|
+
}
|
|
1460
|
+
// how far the boxes' ink reaches, and the order their layers paint in,
|
|
1461
|
+
// as layout leaves them (`layoutDocument`)
|
|
1462
|
+
if (reaches || reorders || moved.length) {
|
|
1463
|
+
computePaintBounds(tree.root, tree.movedInline);
|
|
1464
|
+
if (tree.negative)
|
|
1465
|
+
hoistNegative(tree.root);
|
|
1466
|
+
stackLayers(tree.root);
|
|
1467
|
+
}
|
|
1468
|
+
for (const [box] of moved) {
|
|
1469
|
+
if (box.boundsY + box.boundsHeight >= this._documentHeight) {
|
|
1470
|
+
relayout = true;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (moved.length)
|
|
1474
|
+
this._reportControls();
|
|
1475
|
+
if (relayout) {
|
|
1476
|
+
this._invalidate(Stale.Layout);
|
|
1477
|
+
return 'painted';
|
|
1478
|
+
}
|
|
1479
|
+
for (const [box] of restyled) {
|
|
1480
|
+
const ink = inkOf(box);
|
|
1481
|
+
if (ink)
|
|
1482
|
+
inks.push(ink);
|
|
1483
|
+
}
|
|
1484
|
+
this._repaintInk(inks);
|
|
1302
1485
|
return 'painted';
|
|
1303
1486
|
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Repaint the document where `inks` are, in its own coordinates: the ink
|
|
1489
|
+
* of the boxes a hover restyled, before and after. Past a handful of rects
|
|
1490
|
+
* the damage is one around them all, which is what core would make of
|
|
1491
|
+
* them anyway.
|
|
1492
|
+
*/
|
|
1493
|
+
_repaintInk(inks) {
|
|
1494
|
+
if (!inks.length)
|
|
1495
|
+
return;
|
|
1496
|
+
const x = this.abs.x;
|
|
1497
|
+
const y = this.abs.y;
|
|
1498
|
+
const place = (r) => ({
|
|
1499
|
+
x: Math.floor(x + r.x),
|
|
1500
|
+
y: Math.floor(y + r.y),
|
|
1501
|
+
width: Math.ceil(r.width) + 1,
|
|
1502
|
+
height: Math.ceil(r.height) + 1,
|
|
1503
|
+
});
|
|
1504
|
+
if (inks.length > 6) {
|
|
1505
|
+
let all = inks[0];
|
|
1506
|
+
for (const r of inks)
|
|
1507
|
+
all = unionRect(all, r);
|
|
1508
|
+
this.invalidate(false, place(all), 'props');
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
for (const r of inks)
|
|
1512
|
+
this.invalidate(false, place(r), 'props');
|
|
1513
|
+
}
|
|
1304
1514
|
/** The document, for an application that wants to read or change it. */
|
|
1305
1515
|
get document() {
|
|
1306
1516
|
return this._source.document;
|
|
@@ -1372,6 +1582,16 @@ export class HtmlViewNode extends Node {
|
|
|
1372
1582
|
this._fail(error, this._laidOutWidth);
|
|
1373
1583
|
}
|
|
1374
1584
|
}
|
|
1585
|
+
/** A surface for the painter to draw a masked element on, asked of the
|
|
1586
|
+
* backend until it is found to have none. */
|
|
1587
|
+
_surface(width, height) {
|
|
1588
|
+
if (this._noSurface)
|
|
1589
|
+
return null;
|
|
1590
|
+
const surface = newSurface(this.app, width, height);
|
|
1591
|
+
if (!surface)
|
|
1592
|
+
this._noSurface = true;
|
|
1593
|
+
return surface;
|
|
1594
|
+
}
|
|
1375
1595
|
_paint(ctx, tree, range, damage) {
|
|
1376
1596
|
paintDocument(ctx, tree, {
|
|
1377
1597
|
originX: this.abs.x,
|
|
@@ -1379,6 +1599,7 @@ export class HtmlViewNode extends Node {
|
|
|
1379
1599
|
// the root's background covers the whole element, not only the
|
|
1380
1600
|
// document: an element grown past its content is canvas too
|
|
1381
1601
|
canvas: this.abs,
|
|
1602
|
+
viewport: this._viewport() ?? undefined,
|
|
1382
1603
|
scale: this._scale,
|
|
1383
1604
|
damage,
|
|
1384
1605
|
selection: range
|
|
@@ -1394,11 +1615,34 @@ export class HtmlViewNode extends Node {
|
|
|
1394
1615
|
const size = image ? this._resources.imageSize(url) : null;
|
|
1395
1616
|
return image && size ? { image, ...size } : null;
|
|
1396
1617
|
},
|
|
1618
|
+
surface: (width, height) => this._surface(width, height),
|
|
1397
1619
|
cached: (key, width, height, draw) => (this._shadowCache ??= new SurfaceCache(this.app)).get(key, width, height, draw),
|
|
1398
1620
|
});
|
|
1399
1621
|
}
|
|
1400
1622
|
}
|
|
1401
1623
|
const EMPTY_SET = new Set();
|
|
1624
|
+
/** Whether a tree has a background fixed to the viewport: an image a box
|
|
1625
|
+
* names (`tree.backgrounds`), or the canvas's image or gradient — the
|
|
1626
|
+
* root's, or the body's it paints, which the root box stands in for where
|
|
1627
|
+
* the markup has neither element. */
|
|
1628
|
+
function hasFixedBackground(tree) {
|
|
1629
|
+
for (const box of tree.backgrounds) {
|
|
1630
|
+
if (box.style.backgroundAttachment === 'fixed')
|
|
1631
|
+
return true;
|
|
1632
|
+
}
|
|
1633
|
+
const root = tree.root;
|
|
1634
|
+
const body = root.children.find((child) => child.el?.name === 'body');
|
|
1635
|
+
for (const box of body ? [root, body] : [root]) {
|
|
1636
|
+
const style = box.style;
|
|
1637
|
+
if (style.backgroundAttachment === 'fixed' &&
|
|
1638
|
+
(style.backgroundImage ||
|
|
1639
|
+
style.backgroundImages ||
|
|
1640
|
+
style.backgroundGradient)) {
|
|
1641
|
+
return true;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return false;
|
|
1645
|
+
}
|
|
1402
1646
|
/** The first box an element made, depth first. */
|
|
1403
1647
|
function boxFor(root, element) {
|
|
1404
1648
|
const stack = [root];
|
|
@@ -1412,6 +1656,83 @@ function boxFor(root, element) {
|
|
|
1412
1656
|
}
|
|
1413
1657
|
return null;
|
|
1414
1658
|
}
|
|
1659
|
+
/**
|
|
1660
|
+
* An inline element's boxes: the one it has, or where a block in it broke
|
|
1661
|
+
* it (`breakAround`), each piece, in document order.
|
|
1662
|
+
*/
|
|
1663
|
+
function piecesOf(root, first) {
|
|
1664
|
+
// the first piece is cut at its end where there are more
|
|
1665
|
+
if (first.kind !== 'inline' || !(first.cut & 2))
|
|
1666
|
+
return [first];
|
|
1667
|
+
const pieces = [];
|
|
1668
|
+
const stack = [root];
|
|
1669
|
+
while (stack.length) {
|
|
1670
|
+
const box = stack.pop();
|
|
1671
|
+
if (box.el === first.el && box.kind === 'inline')
|
|
1672
|
+
pieces.push(box);
|
|
1673
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1674
|
+
stack.push(box.children[i]);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return pieces;
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* The bands of one inline box's fragments, or a text's: on the lines of
|
|
1681
|
+
* the block it is laid out in, and no text of a float or a positioned box
|
|
1682
|
+
* inside it — on lines of their own — is one of them (CSSOM View 6.1): a
|
|
1683
|
+
* list item made inline around an absolute link measured as the link.
|
|
1684
|
+
* Each across its text, the inline-blocks and inline boxes' edges it holds
|
|
1685
|
+
* and its own padding and border where it starts or ends there, and as
|
|
1686
|
+
* tall as its border box.
|
|
1687
|
+
*/
|
|
1688
|
+
function inlineBands(root, box, fonts, out) {
|
|
1689
|
+
// its own, apart from any other piece's: `fragmentHeights` makes each of
|
|
1690
|
+
// the bands it is handed as tall as this box on that band's line
|
|
1691
|
+
const bands = [];
|
|
1692
|
+
let block = box.parent;
|
|
1693
|
+
while (block && !block.lines)
|
|
1694
|
+
block = block.parent;
|
|
1695
|
+
if (box.subtreeTextEnd > box.subtreeTextStart) {
|
|
1696
|
+
if (box.kind === 'inline' && block?.lines) {
|
|
1697
|
+
lineBands(block.lines, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands, true);
|
|
1698
|
+
}
|
|
1699
|
+
else {
|
|
1700
|
+
collectBands(root, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (box.kind === 'inline') {
|
|
1704
|
+
fragmentReach(box, bands);
|
|
1705
|
+
if (fonts)
|
|
1706
|
+
fragmentHeights(box, bands, fonts);
|
|
1707
|
+
// where `position: relative` on it or an inline box around it moved
|
|
1708
|
+
// it, all of it (CSS 2.1 9.4.3), as `getBoundingClientRect` has it
|
|
1709
|
+
// (CSSOM View 6.1): the bands are where the lines put it, and a link
|
|
1710
|
+
// moved 120px down measured on its line, as wide as from where it was
|
|
1711
|
+
// laid out to where it was drawn
|
|
1712
|
+
const { x, y } = relativeOffsetOf(box);
|
|
1713
|
+
if (x || y) {
|
|
1714
|
+
for (const band of bands) {
|
|
1715
|
+
band.x += x;
|
|
1716
|
+
band.y += y;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
out.push(...bands);
|
|
1721
|
+
}
|
|
1722
|
+
/** How far an inline box and the inline boxes around it moved it by their
|
|
1723
|
+
* relative offsets (`INLINE_OFFSETS`). */
|
|
1724
|
+
function relativeOffsetOf(box) {
|
|
1725
|
+
let x = 0;
|
|
1726
|
+
let y = 0;
|
|
1727
|
+
for (let at = box; at?.kind === 'inline'; at = at.parent) {
|
|
1728
|
+
const offset = INLINE_OFFSETS.get(at);
|
|
1729
|
+
if (offset) {
|
|
1730
|
+
x += offset.x;
|
|
1731
|
+
y += offset.y;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return { x, y };
|
|
1735
|
+
}
|
|
1415
1736
|
/** Where the document's text goes on after a box with none: the start of
|
|
1416
1737
|
* the next text in document order, or the end of the last before it. */
|
|
1417
1738
|
function textAfter(root, target) {
|
|
@@ -1472,6 +1793,7 @@ function sameRects(a, b) {
|
|
|
1472
1793
|
p.y !== q.y ||
|
|
1473
1794
|
p.width !== q.width ||
|
|
1474
1795
|
p.height !== q.height ||
|
|
1796
|
+
p.opacity !== q.opacity ||
|
|
1475
1797
|
!sameBare(p.bare, q.bare)) {
|
|
1476
1798
|
return false;
|
|
1477
1799
|
}
|
|
@@ -1487,7 +1809,8 @@ function sameBare(a, b) {
|
|
|
1487
1809
|
a.height === b.height &&
|
|
1488
1810
|
a.color === b.color &&
|
|
1489
1811
|
a.fontFamily === b.fontFamily &&
|
|
1490
|
-
a.fontSize === b.fontSize
|
|
1812
|
+
a.fontSize === b.fontSize &&
|
|
1813
|
+
a.chevron === b.chevron);
|
|
1491
1814
|
}
|
|
1492
1815
|
// --- walks over the laid-out tree -------------------------------------------
|
|
1493
1816
|
/** Distance from a coordinate to an interval, zero inside it. */
|
|
@@ -1717,50 +2040,127 @@ function fragmentReach(box, out) {
|
|
|
1717
2040
|
}
|
|
1718
2041
|
}
|
|
1719
2042
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
2043
|
+
/**
|
|
2044
|
+
* An inline element's bands made as tall as its fragments' border boxes,
|
|
2045
|
+
* as CSSOM View takes an inline box's client rects (6.1): its font's
|
|
2046
|
+
* content area about its own baseline, with the padding and the border
|
|
2047
|
+
* above and below it, which take no room on the line (CSS 2.1 10.6.1) and
|
|
2048
|
+
* which paint draws the background over (`paintInlineBoxes`). A band is a
|
|
2049
|
+
* line's height across until then: a link padded 10px below measured as
|
|
2050
|
+
* tall as its line and no taller.
|
|
2051
|
+
*/
|
|
2052
|
+
function fragmentHeights(box, bands, fonts) {
|
|
2053
|
+
let block = box.parent;
|
|
2054
|
+
while (block && !block.lines)
|
|
2055
|
+
block = block.parent;
|
|
2056
|
+
if (!block?.lines?.length)
|
|
1724
2057
|
return;
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
2058
|
+
const face = faceExtentOf(fonts, box.style);
|
|
2059
|
+
const byTop = new Map();
|
|
2060
|
+
for (const line of block.lines)
|
|
2061
|
+
byTop.set(line.y, line);
|
|
2062
|
+
for (let i = 0; i < bands.length; i += 1) {
|
|
2063
|
+
const band = bands[i];
|
|
2064
|
+
const line = byTop.get(band.y);
|
|
2065
|
+
if (!line)
|
|
2066
|
+
continue;
|
|
2067
|
+
// on its own baseline, which `vertical-align` may raise off the line's
|
|
2068
|
+
const raise = SHIFTED_LINES.has(line)
|
|
2069
|
+
? (LINE_BOX_RAISES.get(line)?.get(box) ?? BOX_RAISES.get(box) ?? 0)
|
|
2070
|
+
: 0;
|
|
2071
|
+
const baseline = line.y + line.baseline - raise;
|
|
2072
|
+
const top = baseline - face.ascent - box.padTop - box.borderTop;
|
|
2073
|
+
const bottom = baseline + face.descent + box.padBottom + box.borderBottom;
|
|
2074
|
+
bands[i] = { x: band.x, y: top, width: band.width, height: bottom - top };
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
/** The bands of a text range on one block's own lines. */
|
|
2078
|
+
function lineBands(lines, from, to, dx, dy, out,
|
|
2079
|
+
/** Where the lines put the text, not where a relative offset moved it
|
|
2080
|
+
* (`TEXT_SHIFTS`): what an inline box's own offset is added to. */
|
|
2081
|
+
laidOut = false) {
|
|
2082
|
+
// First line whose text can reach `from`, by binary search over the
|
|
2083
|
+
// sorted text starts; stop at the first line past `to`.
|
|
2084
|
+
let lo = 0;
|
|
2085
|
+
let hi = lines.length;
|
|
2086
|
+
while (lo < hi) {
|
|
2087
|
+
const mid = (lo + hi) >> 1;
|
|
2088
|
+
if (lines[mid].textEnd > from)
|
|
2089
|
+
hi = mid;
|
|
2090
|
+
else
|
|
2091
|
+
lo = mid + 1;
|
|
2092
|
+
}
|
|
2093
|
+
for (let i = lo; i < lines.length; i += 1) {
|
|
2094
|
+
const line = lines[i];
|
|
2095
|
+
if (line.textStart >= to)
|
|
2096
|
+
break;
|
|
2097
|
+
if (line.textEnd > from && line.textStart < to) {
|
|
2098
|
+
for (const text of line.texts) {
|
|
2099
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
2100
|
+
if (!natural)
|
|
2101
|
+
continue;
|
|
2102
|
+
const a = Math.max(from, text.textStart);
|
|
2103
|
+
const b = Math.min(to, text.textEnd);
|
|
2104
|
+
if (b <= a)
|
|
2105
|
+
continue;
|
|
2106
|
+
const offsets = layoutOffsetsOf(text.layout);
|
|
2107
|
+
const moved = laidOut ? (TEXT_SHIFTS.get(text)?.x ?? 0) : 0;
|
|
2108
|
+
// The spaces a line ends on are removed (CSS Text 3, 4.1.2), and
|
|
2109
|
+
// take no room on it: the engine sets them past its line's content,
|
|
2110
|
+
// where a caret after them goes, and a band that took them in made
|
|
2111
|
+
// a link that ends a line a space wider than Chrome's. So the text
|
|
2112
|
+
// a line ends with is measured no further than its content.
|
|
2113
|
+
const ends = endsLine(line, text, natural);
|
|
2114
|
+
for (const band of bandsFor(text.layout, natural, offsets, layoutOffsetOf(text, a), layoutOffsetOf(text, b, true))) {
|
|
2115
|
+
let left = band.x;
|
|
2116
|
+
let right = band.x + band.width;
|
|
2117
|
+
if (ends) {
|
|
2118
|
+
left = Math.max(left, natural.x);
|
|
2119
|
+
right = Math.max(left, Math.min(right, natural.x + natural.width));
|
|
1759
2120
|
}
|
|
2121
|
+
out.push({
|
|
2122
|
+
x: dx + left + text.drawX - moved,
|
|
2123
|
+
y: dy + line.y,
|
|
2124
|
+
width: right - left,
|
|
2125
|
+
height: line.height,
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
// the spaces `pre-wrap` keeps that the line ends on, which have no
|
|
2129
|
+
// run: the engine hung them past the line (`LineText.hung`)
|
|
2130
|
+
for (const space of text.hung ?? []) {
|
|
2131
|
+
const at = documentOffsetOf(text, space.at);
|
|
2132
|
+
if (at < a || at >= b)
|
|
2133
|
+
continue;
|
|
2134
|
+
out.push({
|
|
2135
|
+
x: dx + space.x + text.drawX - moved,
|
|
2136
|
+
y: dy + line.y,
|
|
2137
|
+
width: space.width,
|
|
2138
|
+
height: line.height,
|
|
2139
|
+
});
|
|
1760
2140
|
}
|
|
1761
2141
|
}
|
|
1762
2142
|
}
|
|
1763
2143
|
}
|
|
2144
|
+
}
|
|
2145
|
+
/** Whether a line's text is where the line ends: its last, with nothing
|
|
2146
|
+
* placed after it — an inline-block or an image past its content. */
|
|
2147
|
+
function endsLine(line, text, natural) {
|
|
2148
|
+
if (line.texts[line.texts.length - 1] !== text)
|
|
2149
|
+
return false;
|
|
2150
|
+
const right = text.drawX + natural.x + natural.width;
|
|
2151
|
+
for (const placed of line.atomics) {
|
|
2152
|
+
if (placed.x >= right - 0.01)
|
|
2153
|
+
return false;
|
|
2154
|
+
}
|
|
2155
|
+
return true;
|
|
2156
|
+
}
|
|
2157
|
+
function collectBands(box, from, to, dx, dy, out) {
|
|
2158
|
+
if (box.subtreeTextEnd <= box.subtreeTextStart)
|
|
2159
|
+
return;
|
|
2160
|
+
if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to)
|
|
2161
|
+
return;
|
|
2162
|
+
if (box.lines)
|
|
2163
|
+
lineBands(box.lines, from, to, dx, dy, out);
|
|
1764
2164
|
// Atomics are ordinary children, reached below.
|
|
1765
2165
|
for (const child of box.children) {
|
|
1766
2166
|
if (child.kind === 'text' || child.kind === 'break')
|
|
@@ -1779,7 +2179,11 @@ const HIT_POSITIONED = 3;
|
|
|
1779
2179
|
function deepestAt(tree, x, y,
|
|
1780
2180
|
/** Told whether what was found was found under text: a run's, rather
|
|
1781
2181
|
* than a box's. */
|
|
1782
|
-
hit
|
|
2182
|
+
hit,
|
|
2183
|
+
/** How far a box fixed to the viewport is drawn from where it was laid
|
|
2184
|
+
* out (`HtmlViewNode._fixedShift`): the point is taken back by it in
|
|
2185
|
+
* there, as paint moved the box by it (`atViewport`). */
|
|
2186
|
+
fixedShift = null) {
|
|
1783
2187
|
const box = tree.root;
|
|
1784
2188
|
let found = box.el;
|
|
1785
2189
|
let foundKey = [];
|
|
@@ -1820,7 +2224,25 @@ hit) {
|
|
|
1820
2224
|
// Zen Garden's archive links are absolute `<li>`s in an `overflow:
|
|
1821
2225
|
// hidden` list with no height of its own, and not one of them could be
|
|
1822
2226
|
// hovered or pressed.
|
|
2227
|
+
let atViewport = false;
|
|
1823
2228
|
const enter = (child, context, clipped) => {
|
|
2229
|
+
if (!fixedShift || atViewport || !fixedToViewport(child)) {
|
|
2230
|
+
enterAt(child, context, clipped);
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
x -= fixedShift.x;
|
|
2234
|
+
y -= fixedShift.y;
|
|
2235
|
+
atViewport = true;
|
|
2236
|
+
try {
|
|
2237
|
+
enterAt(child, context, clipped);
|
|
2238
|
+
}
|
|
2239
|
+
finally {
|
|
2240
|
+
x += fixedShift.x;
|
|
2241
|
+
y += fixedShift.y;
|
|
2242
|
+
atViewport = false;
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
const enterAt = (child, context, clipped) => {
|
|
1824
2246
|
const style = child.style;
|
|
1825
2247
|
if (style.position !== 'static' ||
|
|
1826
2248
|
(child.parent?.kind === 'flex' && typeof style.zIndex === 'number')) {
|
|
@@ -1978,21 +2400,112 @@ const FACE_FIELDS = [
|
|
|
1978
2400
|
'letterSpacing',
|
|
1979
2401
|
'features',
|
|
1980
2402
|
];
|
|
1981
|
-
/**
|
|
1982
|
-
*
|
|
1983
|
-
|
|
1984
|
-
|
|
2403
|
+
/** The replaced boxes that are controls (`BoxTree.controls`, `controls.ts`):
|
|
2404
|
+
* a widget mounted beside the document, or a button whose press it reports. */
|
|
2405
|
+
const WIDGETS = new Set([
|
|
2406
|
+
'input',
|
|
2407
|
+
'textarea',
|
|
2408
|
+
'select',
|
|
2409
|
+
'button',
|
|
2410
|
+
'checkbox',
|
|
2411
|
+
'radio',
|
|
2412
|
+
]);
|
|
2413
|
+
/** What only moves how far a box's ink reaches: a shadow, and an outline's
|
|
2414
|
+
* size, which take no room (CSS Backgrounds 3, 7.1; CSS UI 4, 3). */
|
|
2415
|
+
const INK_REACH = new Set([
|
|
2416
|
+
'boxShadow',
|
|
2417
|
+
'outlineStyle',
|
|
2418
|
+
'outlineWidth',
|
|
2419
|
+
'outlineOffset',
|
|
2420
|
+
]);
|
|
2421
|
+
/**
|
|
2422
|
+
* How two styles of one element differ, for a restyle in place: null where
|
|
2423
|
+
* they do not, a `HoverChange` where every difference is ink, how far ink
|
|
2424
|
+
* reaches, a `z-index` that stays a stacking context's, or a translation —
|
|
2425
|
+
* none of which moves anything but the box and what it holds (CSS
|
|
2426
|
+
* Transforms 1: a transform does not affect layout) — and false where
|
|
2427
|
+
* anything else differs.
|
|
2428
|
+
*/
|
|
2429
|
+
function hoverChange(was, now) {
|
|
1985
2430
|
const a = was;
|
|
1986
2431
|
const b = now;
|
|
1987
|
-
let
|
|
2432
|
+
let change = null;
|
|
1988
2433
|
for (const key in b) {
|
|
1989
2434
|
if (key === 'custom' || sameValue(a[key], b[key]))
|
|
1990
2435
|
continue;
|
|
1991
|
-
|
|
2436
|
+
change ??= { reach: false, order: false, move: false };
|
|
2437
|
+
if (PAINT_ONLY.has(key))
|
|
2438
|
+
continue;
|
|
2439
|
+
if (INK_REACH.has(key)) {
|
|
2440
|
+
change.reach = true;
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
if (key === 'zIndex') {
|
|
2444
|
+
// one stacking context either way, with nothing hoisted below the
|
|
2445
|
+
// flow (`hoistNegative`): only its place among its layers changes
|
|
2446
|
+
const z0 = a[key];
|
|
2447
|
+
const z1 = b[key];
|
|
2448
|
+
if (typeof z0 !== 'number' || typeof z1 !== 'number')
|
|
2449
|
+
return false;
|
|
2450
|
+
if (z0 < 0 || z1 < 0)
|
|
2451
|
+
return false;
|
|
2452
|
+
change.order = true;
|
|
2453
|
+
continue;
|
|
2454
|
+
}
|
|
2455
|
+
if (key === 'transformTranslate' || key === 'translate') {
|
|
2456
|
+
change.move = true;
|
|
2457
|
+
continue;
|
|
2458
|
+
}
|
|
2459
|
+
return false;
|
|
2460
|
+
}
|
|
2461
|
+
return change;
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* Whether a box can take a new translation where it is: a box of its own
|
|
2465
|
+
* rather than an inline one, whose text is what moves; and the same
|
|
2466
|
+
* containing block and stacking context for what is in it, which a
|
|
2467
|
+
* transform makes a box (CSS Transforms 1, 2) — so either transformed
|
|
2468
|
+
* before and after, or already positioned with a `z-index`, and holding
|
|
2469
|
+
* nothing fixed, which only a transform takes in.
|
|
2470
|
+
*/
|
|
2471
|
+
function movable(box, was, now) {
|
|
2472
|
+
if (box.kind === 'inline')
|
|
2473
|
+
return false;
|
|
2474
|
+
if (transformed(was) === transformed(now))
|
|
2475
|
+
return true;
|
|
2476
|
+
if (was.position === 'static')
|
|
2477
|
+
return false;
|
|
2478
|
+
if (typeof was.zIndex !== 'number' || typeof now.zIndex !== 'number') {
|
|
2479
|
+
return false;
|
|
2480
|
+
}
|
|
2481
|
+
const stack = [...box.children];
|
|
2482
|
+
while (stack.length) {
|
|
2483
|
+
const at = stack.pop();
|
|
2484
|
+
if (at.style.position === 'fixed')
|
|
1992
2485
|
return false;
|
|
1993
|
-
|
|
2486
|
+
for (const child of at.children)
|
|
2487
|
+
stack.push(child);
|
|
1994
2488
|
}
|
|
1995
|
-
return
|
|
2489
|
+
return true;
|
|
2490
|
+
}
|
|
2491
|
+
/** Where a box draws, in document coordinates: its ink bounds, or for an
|
|
2492
|
+
* inline box or a run of text, which are drawn on their block's lines and
|
|
2493
|
+
* have none of their own, the block's. */
|
|
2494
|
+
function inkOf(box) {
|
|
2495
|
+
let at = box;
|
|
2496
|
+
while (at && (at.kind === 'text' || at.kind === 'break' || !hasRect(at))) {
|
|
2497
|
+
at = at.parent;
|
|
2498
|
+
}
|
|
2499
|
+
if (!at || at.boundsY === Infinity)
|
|
2500
|
+
return null;
|
|
2501
|
+
if (!(at.boundsWidth > 0 && at.boundsHeight > 0))
|
|
2502
|
+
return null;
|
|
2503
|
+
return {
|
|
2504
|
+
x: at.boundsX,
|
|
2505
|
+
y: at.boundsY,
|
|
2506
|
+
width: at.boundsWidth,
|
|
2507
|
+
height: at.boundsHeight,
|
|
2508
|
+
};
|
|
1996
2509
|
}
|
|
1997
2510
|
/** Structural equality for a computed value: a number, a string, or the
|
|
1998
2511
|
* plain objects and arrays a length or a shadow list is. */
|
|
@@ -2138,33 +2651,42 @@ function ownerOf(boxes, index) {
|
|
|
2138
2651
|
const box = boxes[lo];
|
|
2139
2652
|
return box && index >= box.textStart && index < box.textEnd ? box.el : null;
|
|
2140
2653
|
}
|
|
2141
|
-
/** The advance of a
|
|
2142
|
-
*
|
|
2143
|
-
function zeroWidthOf(fonts,
|
|
2654
|
+
/** The advance of a face's "0", laid out: null where the engine cannot
|
|
2655
|
+
* lay it out. */
|
|
2656
|
+
function zeroWidthOf(fonts, face) {
|
|
2657
|
+
const { family, size, weight } = face;
|
|
2658
|
+
// a run is upright or slanted, as `inline.ts` sets one
|
|
2659
|
+
const style = face.style === 'normal' ? 'normal' : 'italic';
|
|
2144
2660
|
try {
|
|
2145
|
-
|
|
2146
|
-
|
|
2661
|
+
const run = { family, size, weight, style };
|
|
2662
|
+
return fonts.layout([{ text: '0', ...run }], run, {}).width;
|
|
2147
2663
|
}
|
|
2148
2664
|
catch {
|
|
2149
2665
|
return null;
|
|
2150
2666
|
}
|
|
2151
2667
|
}
|
|
2152
|
-
/** A
|
|
2153
|
-
*
|
|
2154
|
-
function normalLineOf(fonts,
|
|
2668
|
+
/** A face's own line height, as the engine reports it: null where it
|
|
2669
|
+
* cannot say. */
|
|
2670
|
+
function normalLineOf(fonts, face) {
|
|
2671
|
+
const { family, size, weight, style } = face;
|
|
2155
2672
|
try {
|
|
2156
|
-
const line = fonts
|
|
2673
|
+
const line = fonts
|
|
2674
|
+
.match(family, { size, weight, style })
|
|
2675
|
+
.metrics(size).lineHeight;
|
|
2157
2676
|
return line > 0 ? line : null;
|
|
2158
2677
|
}
|
|
2159
2678
|
catch {
|
|
2160
2679
|
return null;
|
|
2161
2680
|
}
|
|
2162
2681
|
}
|
|
2163
|
-
/** A
|
|
2164
|
-
*
|
|
2165
|
-
function xHeightOf(fonts,
|
|
2682
|
+
/** A face's x-height, as the engine reports it: null where the face
|
|
2683
|
+
* states none, or the engine does not say. */
|
|
2684
|
+
function xHeightOf(fonts, face) {
|
|
2685
|
+
const { family, size, weight, style } = face;
|
|
2166
2686
|
try {
|
|
2167
|
-
const metrics = fonts
|
|
2687
|
+
const metrics = fonts
|
|
2688
|
+
.match(family, { size, weight, style })
|
|
2689
|
+
.metrics(size);
|
|
2168
2690
|
const x = metrics.xHeight;
|
|
2169
2691
|
// NaN where the face states none, which is a number
|
|
2170
2692
|
return typeof x === 'number' && x > 0 ? x : null;
|