@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/src/html/node.ts
CHANGED
|
@@ -51,23 +51,36 @@ import { codePointAtOffset, codeUnitOffsets } from '../internal/text.js';
|
|
|
51
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 type { MetricFace } from './css/cascade.js';
|
|
54
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
58
|
import type { ComputedStyle, RootLook } from './css/style.js';
|
|
58
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
BOX_RAISES,
|
|
61
|
+
buildBoxes,
|
|
62
|
+
CONTENT_IMAGES,
|
|
63
|
+
CUT_BLOCKS,
|
|
64
|
+
GENERATED_FROM,
|
|
65
|
+
INLINE_OFFSETS,
|
|
66
|
+
LINE_BOX_RAISES,
|
|
67
|
+
SHIFTED_LINES,
|
|
68
|
+
TEXT_SHIFTS,
|
|
69
|
+
} from './layout/boxes.js';
|
|
59
70
|
import type {
|
|
60
71
|
Box,
|
|
61
72
|
BoxTree,
|
|
73
|
+
LineBox,
|
|
62
74
|
LineText,
|
|
63
75
|
ReplacedKind,
|
|
64
76
|
TextLayoutLike,
|
|
65
77
|
} from './layout/boxes.js';
|
|
66
|
-
import { layoutDocument } from './layout/block.js';
|
|
78
|
+
import { layoutDocument, retranslate, transformed } from './layout/block.js';
|
|
67
79
|
import { TextLayoutCache } from './layout/cache.js';
|
|
68
80
|
import { shapingSafe } from './layout/shaping.js';
|
|
69
|
-
import { SurfaceCache } from './surfaces.js';
|
|
70
|
-
import {
|
|
81
|
+
import { SurfaceCache, newSurface } from './surfaces.js';
|
|
82
|
+
import type { SurfaceLike } from './surfaces.js';
|
|
83
|
+
import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
|
|
71
84
|
import type { FontsLike } from './layout/inline.js';
|
|
72
85
|
import type { TextRun } from '../richtext/index.js';
|
|
73
86
|
// Through the inline module rather than a second cache: the offsets table for
|
|
@@ -80,12 +93,18 @@ import {
|
|
|
80
93
|
import { lineBands as bandsFor } from '../richtext/runs.js';
|
|
81
94
|
import {
|
|
82
95
|
clipsOverflow,
|
|
96
|
+
computePaintBounds,
|
|
83
97
|
containingBlockOf,
|
|
98
|
+
FIXED_BOXES,
|
|
99
|
+
fixedToViewport,
|
|
100
|
+
forgetDecoratedAncestors,
|
|
84
101
|
hasRect,
|
|
102
|
+
hoistNegative,
|
|
85
103
|
holds,
|
|
86
104
|
holdsAbsolute,
|
|
87
105
|
paintDocument,
|
|
88
106
|
queryChildIndex,
|
|
107
|
+
stackLayers,
|
|
89
108
|
} from './paint.js';
|
|
90
109
|
import type { PaintContext } from './paint.js';
|
|
91
110
|
import { controlRectsOf, measureControl } from './controls.js';
|
|
@@ -213,6 +232,8 @@ export class HtmlViewNode extends Node {
|
|
|
213
232
|
private _sheetsRead: SheetsRead | null = null;
|
|
214
233
|
/** Blurred shadows, drawn once each. */
|
|
215
234
|
private _shadowCache: SurfaceCache | null = null;
|
|
235
|
+
/** Whether the backend was found to have no offscreen surface. */
|
|
236
|
+
private _noSurface = false;
|
|
216
237
|
private _cascade: Cascade | null = null;
|
|
217
238
|
private _tree: BoxTree | null = null;
|
|
218
239
|
/** The faces `_warmFaces` has asked the font matcher for, and of which
|
|
@@ -432,13 +453,15 @@ export class HtmlViewNode extends Node {
|
|
|
432
453
|
*/
|
|
433
454
|
private _requestBackgrounds(tree: BoxTree): void {
|
|
434
455
|
for (const box of tree.backgrounds) {
|
|
435
|
-
|
|
456
|
+
// a pseudo-element's are its element's
|
|
457
|
+
const element = box.el ?? GENERATED_FROM.get(box);
|
|
458
|
+
if (!element) continue;
|
|
436
459
|
// each layer's, where there is more than one
|
|
437
460
|
for (const url of box.style.backgroundImages ?? [
|
|
438
461
|
box.style.backgroundImage,
|
|
439
462
|
]) {
|
|
440
463
|
if (typeof url === 'string') {
|
|
441
|
-
this._resources.request({ url, kind: 'image', element
|
|
464
|
+
this._resources.request({ url, kind: 'image', element });
|
|
442
465
|
}
|
|
443
466
|
}
|
|
444
467
|
const border = box.style.borderImage.source;
|
|
@@ -446,9 +469,15 @@ export class HtmlViewNode extends Node {
|
|
|
446
469
|
this._resources.request({
|
|
447
470
|
url: border,
|
|
448
471
|
kind: 'image',
|
|
449
|
-
element
|
|
472
|
+
element,
|
|
450
473
|
});
|
|
451
474
|
}
|
|
475
|
+
// and each mask layer's, which is an image as a background's is
|
|
476
|
+
for (const url of box.style.mask.images) {
|
|
477
|
+
if (typeof url === 'string') {
|
|
478
|
+
this._resources.request({ url, kind: 'image', element });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
452
481
|
}
|
|
453
482
|
// and every image generated content names, which is only known there
|
|
454
483
|
// too
|
|
@@ -583,9 +612,9 @@ export class HtmlViewNode extends Node {
|
|
|
583
612
|
width,
|
|
584
613
|
this._viewportHeight(),
|
|
585
614
|
this._scale,
|
|
586
|
-
fonts ? (
|
|
587
|
-
fonts ? (
|
|
588
|
-
fonts ? (
|
|
615
|
+
fonts ? (face) => xHeightOf(fonts, face) : null,
|
|
616
|
+
fonts ? (face) => zeroWidthOf(fonts, face) : null,
|
|
617
|
+
fonts ? (face) => normalLineOf(fonts, face) : null,
|
|
589
618
|
faces.length ? this._webFonts : null,
|
|
590
619
|
);
|
|
591
620
|
this._sheetsRead = {
|
|
@@ -697,6 +726,55 @@ export class HtmlViewNode extends Node {
|
|
|
697
726
|
return true;
|
|
698
727
|
}
|
|
699
728
|
|
|
729
|
+
/**
|
|
730
|
+
* The viewport the document is seen through, in window coordinates: the
|
|
731
|
+
* content box of the pane that scrolls the element, where one does —
|
|
732
|
+
* what `position: fixed` and a fixed background are placed against, as
|
|
733
|
+
* a browser places them against its page area. Null where nothing
|
|
734
|
+
* scrolls it, and the element is its own.
|
|
735
|
+
*/
|
|
736
|
+
private _viewport(): Rect | null {
|
|
737
|
+
for (let node = this.parent; node; node = node.parent) {
|
|
738
|
+
// `Scrollable`'s, and not every node's: asked the way core asks it
|
|
739
|
+
const scroller = node as { isScroller?: () => boolean };
|
|
740
|
+
if (!scroller.isScroller?.()) continue;
|
|
741
|
+
const box = node.contentBox();
|
|
742
|
+
return box.width > 0 && box.height > 0 ? box : null;
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* What the document draws fixed to the viewport of the pane that scrolls
|
|
749
|
+
* it, for that pane's scroll blit to repaint rather than copy with the
|
|
750
|
+
* text (react-x11's `viewportFixedRects`): the whole viewport where a
|
|
751
|
+
* fixed background shows through it, which makes a scroll a repaint, and
|
|
752
|
+
* where each fixed box is drawn. Null for a document with neither, and
|
|
753
|
+
* where nothing scrolls the element.
|
|
754
|
+
*/
|
|
755
|
+
override viewportFixedRects(): Rect[] | null {
|
|
756
|
+
const tree = this._tree;
|
|
757
|
+
const viewport = tree && this._viewport();
|
|
758
|
+
if (!tree || !viewport) return null;
|
|
759
|
+
if (hasFixedBackground(tree)) return [viewport];
|
|
760
|
+
const boxes = FIXED_BOXES.get(tree);
|
|
761
|
+
if (!boxes) return null;
|
|
762
|
+
// laid out against the viewport at the document's top, and drawn at the
|
|
763
|
+
// viewport (`atViewport`)
|
|
764
|
+
const root = tree.root;
|
|
765
|
+
const out: Rect[] = [];
|
|
766
|
+
for (const box of boxes) {
|
|
767
|
+
if (!(box.boundsWidth > 0 && box.boundsHeight > 0)) continue;
|
|
768
|
+
out.push({
|
|
769
|
+
x: viewport.x + box.boundsX - root.x,
|
|
770
|
+
y: viewport.y + box.boundsY - root.y,
|
|
771
|
+
width: box.boundsWidth,
|
|
772
|
+
height: box.boundsHeight,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return out.length ? out : null;
|
|
776
|
+
}
|
|
777
|
+
|
|
700
778
|
private _viewportHeight(): number {
|
|
701
779
|
// The viewport a `vh` resolves against is the one the document is seen
|
|
702
780
|
// through, not the document — a document taller than it does not make
|
|
@@ -1188,18 +1266,49 @@ export class HtmlViewNode extends Node {
|
|
|
1188
1266
|
rect = { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
1189
1267
|
} else {
|
|
1190
1268
|
const bands: Rect[] = [];
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1269
|
+
const fonts = this._layouts?.fonts ?? null;
|
|
1270
|
+
// an inline box broken around the blocks in it is its pieces, a box
|
|
1271
|
+
// of the element's each (CSS 2.1 9.2.1.1), and its fragments are
|
|
1272
|
+
// theirs; and on the lines between them it has a fragment around each
|
|
1273
|
+
// block, across the box the block is in and as tall as its border
|
|
1274
|
+
// box, as a browser's has — Blink's block-in-inline, Gecko's split
|
|
1275
|
+
// inline's anonymous block. A list item made inline around a block
|
|
1276
|
+
// link measured as the empty edge after it
|
|
1277
|
+
for (const piece of piecesOf(tree.root, box)) {
|
|
1278
|
+
inlineBands(tree.root, piece, fonts, bands);
|
|
1279
|
+
}
|
|
1280
|
+
for (const block of (box.cut ? CUT_BLOCKS.get(box) : null) ?? []) {
|
|
1281
|
+
const around = block.parent;
|
|
1282
|
+
if (!around) continue;
|
|
1283
|
+
bands.push({
|
|
1284
|
+
x: around.contentX,
|
|
1285
|
+
y: block.y,
|
|
1286
|
+
width: around.contentWidth,
|
|
1287
|
+
height: block.height,
|
|
1288
|
+
});
|
|
1200
1289
|
}
|
|
1201
|
-
if (box.kind === 'inline') fragmentReach(box, bands);
|
|
1202
1290
|
for (const band of bands) rect = rect ? unionRect(rect, band) : band;
|
|
1291
|
+
if (!rect) {
|
|
1292
|
+
// on no line, where its block has none: text set at no size takes
|
|
1293
|
+
// no room and gets no box, and CSS puts it on lines of no height
|
|
1294
|
+
// at the block's top (9.4.2), as a browser measures it — nothing,
|
|
1295
|
+
// where the block's content starts. Where the text after it
|
|
1296
|
+
// starts was the next text that took room, elsewhere in the
|
|
1297
|
+
// document: the Zen Garden's 170 hides a heading at `font-size:
|
|
1298
|
+
// 0`, and an `<abbr>` in it measured as the end of the paragraph
|
|
1299
|
+
// above it
|
|
1300
|
+
let block = box.parent;
|
|
1301
|
+
while (block && block.kind === 'inline') block = block.parent;
|
|
1302
|
+
if (block && !block.lines?.length) {
|
|
1303
|
+
const rtl = block.style.direction === 'rtl';
|
|
1304
|
+
rect = {
|
|
1305
|
+
x: rtl ? block.contentX + block.contentWidth : block.contentX,
|
|
1306
|
+
y: block.contentY,
|
|
1307
|
+
width: 0,
|
|
1308
|
+
height: 0,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1203
1312
|
if (!rect) {
|
|
1204
1313
|
// an empty subtree's range is `[0, 0)`, wherever it stands: where
|
|
1205
1314
|
// its text would be is where the text after it starts
|
|
@@ -1222,7 +1331,20 @@ export class HtmlViewNode extends Node {
|
|
|
1222
1331
|
const tree = this._tree;
|
|
1223
1332
|
if (!tree) return null;
|
|
1224
1333
|
const local = this._toDocument(x, y);
|
|
1225
|
-
return deepestAt(tree, local.x, local.y);
|
|
1334
|
+
return deepestAt(tree, local.x, local.y, undefined, this._fixedShift());
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/** How far a box fixed to the viewport is drawn from where it was laid
|
|
1338
|
+
* out, in the document's pixels: the scroll of the pane the element is
|
|
1339
|
+
* in (`atViewport`). Null where nothing scrolls it. */
|
|
1340
|
+
private _fixedShift(): { x: number; y: number } | null {
|
|
1341
|
+
const tree = this._tree;
|
|
1342
|
+
const viewport = tree && FIXED_BOXES.has(tree) && this._viewport();
|
|
1343
|
+
if (!tree || !viewport) return null;
|
|
1344
|
+
return {
|
|
1345
|
+
x: viewport.x - this.abs.x - tree.root.x,
|
|
1346
|
+
y: viewport.y - this.abs.y - tree.root.y,
|
|
1347
|
+
};
|
|
1226
1348
|
}
|
|
1227
1349
|
|
|
1228
1350
|
/**
|
|
@@ -1239,7 +1361,13 @@ export class HtmlViewNode extends Node {
|
|
|
1239
1361
|
const tree = this._tree;
|
|
1240
1362
|
if (!tree) return null;
|
|
1241
1363
|
const hit = { text: false };
|
|
1242
|
-
const el = deepestAt(
|
|
1364
|
+
const el = deepestAt(
|
|
1365
|
+
tree,
|
|
1366
|
+
x - this.abs.x,
|
|
1367
|
+
y - this.abs.y,
|
|
1368
|
+
hit,
|
|
1369
|
+
this._fixedShift(),
|
|
1370
|
+
);
|
|
1243
1371
|
const cursor = el ? tree.styles.get(el)?.style.cursor : null;
|
|
1244
1372
|
// a keyword; a `url()` this cannot load falls back, as its list would
|
|
1245
1373
|
if (cursor && cursor !== 'auto' && /^[a-z-]+$/.test(cursor)) return cursor;
|
|
@@ -1280,9 +1408,8 @@ export class HtmlViewNode extends Node {
|
|
|
1280
1408
|
|
|
1281
1409
|
/** The hovered chain moved: restyle where it did, or the document. */
|
|
1282
1410
|
private _restyleHover(was: readonly Element[], now: readonly Element[]) {
|
|
1283
|
-
|
|
1284
|
-
if (
|
|
1285
|
-
else if (done === false) this._invalidate(Stale.Boxes);
|
|
1411
|
+
// a restyle in place names what it repainted (`_hoverInPlace`)
|
|
1412
|
+
if (this._hoverInPlace(was, now) === false) this._invalidate(Stale.Boxes);
|
|
1286
1413
|
}
|
|
1287
1414
|
|
|
1288
1415
|
/**
|
|
@@ -1369,6 +1496,11 @@ export class HtmlViewNode extends Node {
|
|
|
1369
1496
|
const fresh = new Map<Element, ComputedStyle>();
|
|
1370
1497
|
const changed = new Map<Element, ComputedStyle>();
|
|
1371
1498
|
let refused = false;
|
|
1499
|
+
// what changed beyond ink (`hoverChange`): how far some box's ink
|
|
1500
|
+
// reaches, the order its layer paints in, where one is
|
|
1501
|
+
let reaches = false;
|
|
1502
|
+
let reorders = false;
|
|
1503
|
+
const moving = new Set<Element>();
|
|
1372
1504
|
const styleOf = (el: Element): ComputedStyle | null => {
|
|
1373
1505
|
const kept = tree.styles.get(el);
|
|
1374
1506
|
if (!kept) return null;
|
|
@@ -1385,9 +1517,14 @@ export class HtmlViewNode extends Node {
|
|
|
1385
1517
|
}
|
|
1386
1518
|
const style = cascade.styleFor(el, parentStyle, kept.inFlex);
|
|
1387
1519
|
fresh.set(el, style);
|
|
1388
|
-
const diff =
|
|
1520
|
+
const diff = hoverChange(kept.style, style);
|
|
1389
1521
|
if (diff === false) refused = true;
|
|
1390
|
-
else if (diff)
|
|
1522
|
+
else if (diff) {
|
|
1523
|
+
changed.set(el, style);
|
|
1524
|
+
if (diff.reach) reaches = true;
|
|
1525
|
+
if (diff.order) reorders = true;
|
|
1526
|
+
if (diff.move) moving.add(el);
|
|
1527
|
+
}
|
|
1391
1528
|
return style;
|
|
1392
1529
|
};
|
|
1393
1530
|
for (const el of reach) {
|
|
@@ -1416,9 +1553,12 @@ export class HtmlViewNode extends Node {
|
|
|
1416
1553
|
if (!style) continue;
|
|
1417
1554
|
const kept = tree.styles.get(owner)!;
|
|
1418
1555
|
// an anonymous box, a pseudo-element, a marker, a control: a style
|
|
1419
|
-
// derived from the element's, or drawn from it somewhere else
|
|
1556
|
+
// derived from the element's, or drawn from it somewhere else — a
|
|
1557
|
+
// widget mounted beside the document takes its look from it. An
|
|
1558
|
+
// image, a drawing or a rule is drawn here, from its box, like any.
|
|
1420
1559
|
if (!box.el || box.style !== kept.style) return false;
|
|
1421
|
-
if (box.marker || box.replaced
|
|
1560
|
+
if (box.marker || WIDGETS.has(box.replaced)) return false;
|
|
1561
|
+
if (moving.has(owner) && !movable(box, kept.style, style)) return false;
|
|
1422
1562
|
restyled.push([box, style]);
|
|
1423
1563
|
if (box.kind === 'inline') {
|
|
1424
1564
|
const decoration = inlineDecoration(fonts, box, style);
|
|
@@ -1450,9 +1590,28 @@ export class HtmlViewNode extends Node {
|
|
|
1450
1590
|
}
|
|
1451
1591
|
}
|
|
1452
1592
|
|
|
1593
|
+
// what each box drew before, to be repainted with what it draws after
|
|
1594
|
+
const inks: Rect[] = [];
|
|
1595
|
+
for (const [box] of restyled) {
|
|
1596
|
+
const ink = inkOf(box);
|
|
1597
|
+
if (ink) inks.push(ink);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1453
1600
|
// and only now, all of it
|
|
1454
|
-
|
|
1455
|
-
for (const [box,
|
|
1601
|
+
const moved: [Box, ComputedStyle][] = [];
|
|
1602
|
+
for (const [box, style] of restyled) {
|
|
1603
|
+
const was = box.style;
|
|
1604
|
+
box.style = style;
|
|
1605
|
+
if (moving.has(box.el!)) moved.push([box, was]);
|
|
1606
|
+
}
|
|
1607
|
+
for (const [box, decoration] of redecorated) {
|
|
1608
|
+
// what is inside it keeps which boxes around it paint, and this is
|
|
1609
|
+
// one more or one fewer of them
|
|
1610
|
+
if ((decoration === null) !== (box.decoration === null)) {
|
|
1611
|
+
forgetDecoratedAncestors(box);
|
|
1612
|
+
}
|
|
1613
|
+
box.decoration = decoration;
|
|
1614
|
+
}
|
|
1456
1615
|
for (const text of texts) {
|
|
1457
1616
|
const next = relaid.get(text.layout);
|
|
1458
1617
|
if (next) text.layout = next;
|
|
@@ -1460,9 +1619,67 @@ export class HtmlViewNode extends Node {
|
|
|
1460
1619
|
for (const [el, style] of changed) {
|
|
1461
1620
|
tree.styles.set(el, { style, inFlex: tree.styles.get(el)!.inFlex });
|
|
1462
1621
|
}
|
|
1622
|
+
// A box that moved moves what is in it, as layout moved it the first
|
|
1623
|
+
// time (`applyRelativeOffsets`), and a later layout moves it the same
|
|
1624
|
+
// way; where it reached the document's end, before or after, the
|
|
1625
|
+
// document's height may have changed with it, which only a layout says.
|
|
1626
|
+
let relayout = false;
|
|
1627
|
+
for (const [box, was] of moved) {
|
|
1628
|
+
const before = box.boundsY + box.boundsHeight;
|
|
1629
|
+
retranslate(box, was);
|
|
1630
|
+
tree.relative = true;
|
|
1631
|
+
if (before >= this._documentHeight) relayout = true;
|
|
1632
|
+
}
|
|
1633
|
+
// how far the boxes' ink reaches, and the order their layers paint in,
|
|
1634
|
+
// as layout leaves them (`layoutDocument`)
|
|
1635
|
+
if (reaches || reorders || moved.length) {
|
|
1636
|
+
computePaintBounds(tree.root, tree.movedInline);
|
|
1637
|
+
if (tree.negative) hoistNegative(tree.root);
|
|
1638
|
+
stackLayers(tree.root);
|
|
1639
|
+
}
|
|
1640
|
+
for (const [box] of moved) {
|
|
1641
|
+
if (box.boundsY + box.boundsHeight >= this._documentHeight) {
|
|
1642
|
+
relayout = true;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (moved.length) this._reportControls();
|
|
1646
|
+
if (relayout) {
|
|
1647
|
+
this._invalidate(Stale.Layout);
|
|
1648
|
+
return 'painted';
|
|
1649
|
+
}
|
|
1650
|
+
for (const [box] of restyled) {
|
|
1651
|
+
const ink = inkOf(box);
|
|
1652
|
+
if (ink) inks.push(ink);
|
|
1653
|
+
}
|
|
1654
|
+
this._repaintInk(inks);
|
|
1463
1655
|
return 'painted';
|
|
1464
1656
|
}
|
|
1465
1657
|
|
|
1658
|
+
/**
|
|
1659
|
+
* Repaint the document where `inks` are, in its own coordinates: the ink
|
|
1660
|
+
* of the boxes a hover restyled, before and after. Past a handful of rects
|
|
1661
|
+
* the damage is one around them all, which is what core would make of
|
|
1662
|
+
* them anyway.
|
|
1663
|
+
*/
|
|
1664
|
+
private _repaintInk(inks: Rect[]): void {
|
|
1665
|
+
if (!inks.length) return;
|
|
1666
|
+
const x = this.abs.x;
|
|
1667
|
+
const y = this.abs.y;
|
|
1668
|
+
const place = (r: Rect): Rect => ({
|
|
1669
|
+
x: Math.floor(x + r.x),
|
|
1670
|
+
y: Math.floor(y + r.y),
|
|
1671
|
+
width: Math.ceil(r.width) + 1,
|
|
1672
|
+
height: Math.ceil(r.height) + 1,
|
|
1673
|
+
});
|
|
1674
|
+
if (inks.length > 6) {
|
|
1675
|
+
let all = inks[0];
|
|
1676
|
+
for (const r of inks) all = unionRect(all, r);
|
|
1677
|
+
this.invalidate(false, place(all), 'props');
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
for (const r of inks) this.invalidate(false, place(r), 'props');
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1466
1683
|
/** The document, for an application that wants to read or change it. */
|
|
1467
1684
|
get document(): Document {
|
|
1468
1685
|
return this._source.document;
|
|
@@ -1541,6 +1758,15 @@ export class HtmlViewNode extends Node {
|
|
|
1541
1758
|
}
|
|
1542
1759
|
}
|
|
1543
1760
|
|
|
1761
|
+
/** A surface for the painter to draw a masked element on, asked of the
|
|
1762
|
+
* backend until it is found to have none. */
|
|
1763
|
+
private _surface(width: number, height: number): SurfaceLike | null {
|
|
1764
|
+
if (this._noSurface) return null;
|
|
1765
|
+
const surface = newSurface(this.app, width, height);
|
|
1766
|
+
if (!surface) this._noSurface = true;
|
|
1767
|
+
return surface;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1544
1770
|
private _paint(
|
|
1545
1771
|
ctx: Context2D,
|
|
1546
1772
|
tree: BoxTree,
|
|
@@ -1553,6 +1779,7 @@ export class HtmlViewNode extends Node {
|
|
|
1553
1779
|
// the root's background covers the whole element, not only the
|
|
1554
1780
|
// document: an element grown past its content is canvas too
|
|
1555
1781
|
canvas: this.abs,
|
|
1782
|
+
viewport: this._viewport() ?? undefined,
|
|
1556
1783
|
scale: this._scale,
|
|
1557
1784
|
damage,
|
|
1558
1785
|
selection: range
|
|
@@ -1568,6 +1795,7 @@ export class HtmlViewNode extends Node {
|
|
|
1568
1795
|
const size = image ? this._resources.imageSize(url) : null;
|
|
1569
1796
|
return image && size ? { image, ...size } : null;
|
|
1570
1797
|
},
|
|
1798
|
+
surface: (width, height) => this._surface(width, height),
|
|
1571
1799
|
cached: (key, width, height, draw) =>
|
|
1572
1800
|
(this._shadowCache ??= new SurfaceCache(this.app)).get(
|
|
1573
1801
|
key,
|
|
@@ -1581,6 +1809,30 @@ export class HtmlViewNode extends Node {
|
|
|
1581
1809
|
|
|
1582
1810
|
const EMPTY_SET: ReadonlySet<Element> = new Set();
|
|
1583
1811
|
|
|
1812
|
+
/** Whether a tree has a background fixed to the viewport: an image a box
|
|
1813
|
+
* names (`tree.backgrounds`), or the canvas's image or gradient — the
|
|
1814
|
+
* root's, or the body's it paints, which the root box stands in for where
|
|
1815
|
+
* the markup has neither element. */
|
|
1816
|
+
function hasFixedBackground(tree: BoxTree): boolean {
|
|
1817
|
+
for (const box of tree.backgrounds) {
|
|
1818
|
+
if (box.style.backgroundAttachment === 'fixed') return true;
|
|
1819
|
+
}
|
|
1820
|
+
const root = tree.root;
|
|
1821
|
+
const body = root.children.find((child) => child.el?.name === 'body');
|
|
1822
|
+
for (const box of body ? [root, body] : [root]) {
|
|
1823
|
+
const style = box.style;
|
|
1824
|
+
if (
|
|
1825
|
+
style.backgroundAttachment === 'fixed' &&
|
|
1826
|
+
(style.backgroundImage ||
|
|
1827
|
+
style.backgroundImages ||
|
|
1828
|
+
style.backgroundGradient)
|
|
1829
|
+
) {
|
|
1830
|
+
return true;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return false;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1584
1836
|
/** A sheet as the restyle reads it: its text, the encoding it was decoded
|
|
1585
1837
|
* from, the element that brought it, and what its URLs resolve against. */
|
|
1586
1838
|
interface SheetText {
|
|
@@ -1627,6 +1879,94 @@ function boxFor(root: Box, element: Element): Box | null {
|
|
|
1627
1879
|
return null;
|
|
1628
1880
|
}
|
|
1629
1881
|
|
|
1882
|
+
/**
|
|
1883
|
+
* An inline element's boxes: the one it has, or where a block in it broke
|
|
1884
|
+
* it (`breakAround`), each piece, in document order.
|
|
1885
|
+
*/
|
|
1886
|
+
function piecesOf(root: Box, first: Box): Box[] {
|
|
1887
|
+
// the first piece is cut at its end where there are more
|
|
1888
|
+
if (first.kind !== 'inline' || !(first.cut & 2)) return [first];
|
|
1889
|
+
const pieces: Box[] = [];
|
|
1890
|
+
const stack: Box[] = [root];
|
|
1891
|
+
while (stack.length) {
|
|
1892
|
+
const box = stack.pop()!;
|
|
1893
|
+
if (box.el === first.el && box.kind === 'inline') pieces.push(box);
|
|
1894
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1895
|
+
stack.push(box.children[i]);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return pieces;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
* The bands of one inline box's fragments, or a text's: on the lines of
|
|
1903
|
+
* the block it is laid out in, and no text of a float or a positioned box
|
|
1904
|
+
* inside it — on lines of their own — is one of them (CSSOM View 6.1): a
|
|
1905
|
+
* list item made inline around an absolute link measured as the link.
|
|
1906
|
+
* Each across its text, the inline-blocks and inline boxes' edges it holds
|
|
1907
|
+
* and its own padding and border where it starts or ends there, and as
|
|
1908
|
+
* tall as its border box.
|
|
1909
|
+
*/
|
|
1910
|
+
function inlineBands(
|
|
1911
|
+
root: Box,
|
|
1912
|
+
box: Box,
|
|
1913
|
+
fonts: FontsLike | null,
|
|
1914
|
+
out: Rect[],
|
|
1915
|
+
): void {
|
|
1916
|
+
// its own, apart from any other piece's: `fragmentHeights` makes each of
|
|
1917
|
+
// the bands it is handed as tall as this box on that band's line
|
|
1918
|
+
const bands: Rect[] = [];
|
|
1919
|
+
let block: Box | null = box.parent;
|
|
1920
|
+
while (block && !block.lines) block = block.parent;
|
|
1921
|
+
if (box.subtreeTextEnd > box.subtreeTextStart) {
|
|
1922
|
+
if (box.kind === 'inline' && block?.lines) {
|
|
1923
|
+
lineBands(
|
|
1924
|
+
block.lines,
|
|
1925
|
+
box.subtreeTextStart,
|
|
1926
|
+
box.subtreeTextEnd,
|
|
1927
|
+
0,
|
|
1928
|
+
0,
|
|
1929
|
+
bands,
|
|
1930
|
+
true,
|
|
1931
|
+
);
|
|
1932
|
+
} else {
|
|
1933
|
+
collectBands(root, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (box.kind === 'inline') {
|
|
1937
|
+
fragmentReach(box, bands);
|
|
1938
|
+
if (fonts) fragmentHeights(box, bands, fonts);
|
|
1939
|
+
// where `position: relative` on it or an inline box around it moved
|
|
1940
|
+
// it, all of it (CSS 2.1 9.4.3), as `getBoundingClientRect` has it
|
|
1941
|
+
// (CSSOM View 6.1): the bands are where the lines put it, and a link
|
|
1942
|
+
// moved 120px down measured on its line, as wide as from where it was
|
|
1943
|
+
// laid out to where it was drawn
|
|
1944
|
+
const { x, y } = relativeOffsetOf(box);
|
|
1945
|
+
if (x || y) {
|
|
1946
|
+
for (const band of bands) {
|
|
1947
|
+
band.x += x;
|
|
1948
|
+
band.y += y;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
out.push(...bands);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
/** How far an inline box and the inline boxes around it moved it by their
|
|
1956
|
+
* relative offsets (`INLINE_OFFSETS`). */
|
|
1957
|
+
function relativeOffsetOf(box: Box): { x: number; y: number } {
|
|
1958
|
+
let x = 0;
|
|
1959
|
+
let y = 0;
|
|
1960
|
+
for (let at: Box | null = box; at?.kind === 'inline'; at = at.parent) {
|
|
1961
|
+
const offset = INLINE_OFFSETS.get(at);
|
|
1962
|
+
if (offset) {
|
|
1963
|
+
x += offset.x;
|
|
1964
|
+
y += offset.y;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return { x, y };
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1630
1970
|
/** Where the document's text goes on after a box with none: the start of
|
|
1631
1971
|
* the next text in document order, or the end of the last before it. */
|
|
1632
1972
|
function textAfter(root: Box, target: Box): number {
|
|
@@ -1686,6 +2026,7 @@ function sameRects(a: ControlRect[], b: ControlRect[]): boolean {
|
|
|
1686
2026
|
p.y !== q.y ||
|
|
1687
2027
|
p.width !== q.width ||
|
|
1688
2028
|
p.height !== q.height ||
|
|
2029
|
+
p.opacity !== q.opacity ||
|
|
1689
2030
|
!sameBare(p.bare, q.bare)
|
|
1690
2031
|
) {
|
|
1691
2032
|
return false;
|
|
@@ -1703,7 +2044,8 @@ function sameBare(a?: BareField, b?: BareField): boolean {
|
|
|
1703
2044
|
a.height === b.height &&
|
|
1704
2045
|
a.color === b.color &&
|
|
1705
2046
|
a.fontFamily === b.fontFamily &&
|
|
1706
|
-
a.fontSize === b.fontSize
|
|
2047
|
+
a.fontSize === b.fontSize &&
|
|
2048
|
+
a.chevron === b.chevron
|
|
1707
2049
|
);
|
|
1708
2050
|
}
|
|
1709
2051
|
|
|
@@ -1936,56 +2278,139 @@ function fragmentReach(box: Box, out: Rect[]): void {
|
|
|
1936
2278
|
}
|
|
1937
2279
|
}
|
|
1938
2280
|
|
|
1939
|
-
|
|
1940
|
-
|
|
2281
|
+
/**
|
|
2282
|
+
* An inline element's bands made as tall as its fragments' border boxes,
|
|
2283
|
+
* as CSSOM View takes an inline box's client rects (6.1): its font's
|
|
2284
|
+
* content area about its own baseline, with the padding and the border
|
|
2285
|
+
* above and below it, which take no room on the line (CSS 2.1 10.6.1) and
|
|
2286
|
+
* which paint draws the background over (`paintInlineBoxes`). A band is a
|
|
2287
|
+
* line's height across until then: a link padded 10px below measured as
|
|
2288
|
+
* tall as its line and no taller.
|
|
2289
|
+
*/
|
|
2290
|
+
function fragmentHeights(box: Box, bands: Rect[], fonts: FontsLike): void {
|
|
2291
|
+
let block = box.parent;
|
|
2292
|
+
while (block && !block.lines) block = block.parent;
|
|
2293
|
+
if (!block?.lines?.length) return;
|
|
2294
|
+
const face = faceExtentOf(fonts, box.style);
|
|
2295
|
+
const byTop = new Map<number, LineBox>();
|
|
2296
|
+
for (const line of block.lines) byTop.set(line.y, line);
|
|
2297
|
+
for (let i = 0; i < bands.length; i += 1) {
|
|
2298
|
+
const band = bands[i];
|
|
2299
|
+
const line = byTop.get(band.y);
|
|
2300
|
+
if (!line) continue;
|
|
2301
|
+
// on its own baseline, which `vertical-align` may raise off the line's
|
|
2302
|
+
const raise = SHIFTED_LINES.has(line)
|
|
2303
|
+
? (LINE_BOX_RAISES.get(line)?.get(box) ?? BOX_RAISES.get(box) ?? 0)
|
|
2304
|
+
: 0;
|
|
2305
|
+
const baseline = line.y + line.baseline - raise;
|
|
2306
|
+
const top = baseline - face.ascent - box.padTop - box.borderTop;
|
|
2307
|
+
const bottom = baseline + face.descent + box.padBottom + box.borderBottom;
|
|
2308
|
+
bands[i] = { x: band.x, y: top, width: band.width, height: bottom - top };
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
/** The bands of a text range on one block's own lines. */
|
|
2313
|
+
function lineBands(
|
|
2314
|
+
lines: readonly LineBox[],
|
|
1941
2315
|
from: number,
|
|
1942
2316
|
to: number,
|
|
1943
2317
|
dx: number,
|
|
1944
2318
|
dy: number,
|
|
1945
2319
|
out: Rect[],
|
|
2320
|
+
/** Where the lines put the text, not where a relative offset moved it
|
|
2321
|
+
* (`TEXT_SHIFTS`): what an inline box's own offset is added to. */
|
|
2322
|
+
laidOut = false,
|
|
1946
2323
|
): void {
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2324
|
+
// First line whose text can reach `from`, by binary search over the
|
|
2325
|
+
// sorted text starts; stop at the first line past `to`.
|
|
2326
|
+
let lo = 0;
|
|
2327
|
+
let hi = lines.length;
|
|
2328
|
+
while (lo < hi) {
|
|
2329
|
+
const mid = (lo + hi) >> 1;
|
|
2330
|
+
if (lines[mid].textEnd > from) hi = mid;
|
|
2331
|
+
else lo = mid + 1;
|
|
2332
|
+
}
|
|
2333
|
+
for (let i = lo; i < lines.length; i += 1) {
|
|
2334
|
+
const line = lines[i];
|
|
2335
|
+
if (line.textStart >= to) break;
|
|
2336
|
+
if (line.textEnd > from && line.textStart < to) {
|
|
2337
|
+
for (const text of line.texts) {
|
|
2338
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
2339
|
+
if (!natural) continue;
|
|
2340
|
+
const a = Math.max(from, text.textStart);
|
|
2341
|
+
const b = Math.min(to, text.textEnd);
|
|
2342
|
+
if (b <= a) continue;
|
|
2343
|
+
const offsets = layoutOffsetsOf(text.layout);
|
|
2344
|
+
const moved = laidOut ? (TEXT_SHIFTS.get(text)?.x ?? 0) : 0;
|
|
2345
|
+
// The spaces a line ends on are removed (CSS Text 3, 4.1.2), and
|
|
2346
|
+
// take no room on it: the engine sets them past its line's content,
|
|
2347
|
+
// where a caret after them goes, and a band that took them in made
|
|
2348
|
+
// a link that ends a line a space wider than Chrome's. So the text
|
|
2349
|
+
// a line ends with is measured no further than its content.
|
|
2350
|
+
const ends = endsLine(line, text, natural);
|
|
2351
|
+
for (const band of bandsFor(
|
|
2352
|
+
text.layout,
|
|
2353
|
+
natural,
|
|
2354
|
+
offsets,
|
|
2355
|
+
layoutOffsetOf(text, a),
|
|
2356
|
+
layoutOffsetOf(text, b, true),
|
|
2357
|
+
)) {
|
|
2358
|
+
let left = band.x;
|
|
2359
|
+
let right = band.x + band.width;
|
|
2360
|
+
if (ends) {
|
|
2361
|
+
left = Math.max(left, natural.x);
|
|
2362
|
+
right = Math.max(left, Math.min(right, natural.x + natural.width));
|
|
1984
2363
|
}
|
|
2364
|
+
out.push({
|
|
2365
|
+
x: dx + left + text.drawX - moved,
|
|
2366
|
+
y: dy + line.y,
|
|
2367
|
+
width: right - left,
|
|
2368
|
+
height: line.height,
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
// the spaces `pre-wrap` keeps that the line ends on, which have no
|
|
2372
|
+
// run: the engine hung them past the line (`LineText.hung`)
|
|
2373
|
+
for (const space of text.hung ?? []) {
|
|
2374
|
+
const at = documentOffsetOf(text, space.at);
|
|
2375
|
+
if (at < a || at >= b) continue;
|
|
2376
|
+
out.push({
|
|
2377
|
+
x: dx + space.x + text.drawX - moved,
|
|
2378
|
+
y: dy + line.y,
|
|
2379
|
+
width: space.width,
|
|
2380
|
+
height: line.height,
|
|
2381
|
+
});
|
|
1985
2382
|
}
|
|
1986
2383
|
}
|
|
1987
2384
|
}
|
|
1988
2385
|
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
/** Whether a line's text is where the line ends: its last, with nothing
|
|
2389
|
+
* placed after it — an inline-block or an image past its content. */
|
|
2390
|
+
function endsLine(
|
|
2391
|
+
line: LineBox,
|
|
2392
|
+
text: LineText,
|
|
2393
|
+
natural: { x: number; width: number },
|
|
2394
|
+
): boolean {
|
|
2395
|
+
if (line.texts[line.texts.length - 1] !== text) return false;
|
|
2396
|
+
const right = text.drawX + natural.x + natural.width;
|
|
2397
|
+
for (const placed of line.atomics) {
|
|
2398
|
+
if (placed.x >= right - 0.01) return false;
|
|
2399
|
+
}
|
|
2400
|
+
return true;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
function collectBands(
|
|
2404
|
+
box: Box,
|
|
2405
|
+
from: number,
|
|
2406
|
+
to: number,
|
|
2407
|
+
dx: number,
|
|
2408
|
+
dy: number,
|
|
2409
|
+
out: Rect[],
|
|
2410
|
+
): void {
|
|
2411
|
+
if (box.subtreeTextEnd <= box.subtreeTextStart) return;
|
|
2412
|
+
if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to) return;
|
|
2413
|
+
if (box.lines) lineBands(box.lines, from, to, dx, dy, out);
|
|
1989
2414
|
// Atomics are ordinary children, reached below.
|
|
1990
2415
|
for (const child of box.children) {
|
|
1991
2416
|
if (child.kind === 'text' || child.kind === 'break') continue;
|
|
@@ -2009,6 +2434,10 @@ function deepestAt(
|
|
|
2009
2434
|
/** Told whether what was found was found under text: a run's, rather
|
|
2010
2435
|
* than a box's. */
|
|
2011
2436
|
hit?: { text: boolean },
|
|
2437
|
+
/** How far a box fixed to the viewport is drawn from where it was laid
|
|
2438
|
+
* out (`HtmlViewNode._fixedShift`): the point is taken back by it in
|
|
2439
|
+
* there, as paint moved the box by it (`atViewport`). */
|
|
2440
|
+
fixedShift: { x: number; y: number } | null = null,
|
|
2012
2441
|
): Element | null {
|
|
2013
2442
|
const box = tree.root;
|
|
2014
2443
|
let found: Element | null = box.el;
|
|
@@ -2049,10 +2478,31 @@ function deepestAt(
|
|
|
2049
2478
|
// Zen Garden's archive links are absolute `<li>`s in an `overflow:
|
|
2050
2479
|
// hidden` list with no height of its own, and not one of them could be
|
|
2051
2480
|
// hovered or pressed.
|
|
2481
|
+
let atViewport = false;
|
|
2052
2482
|
const enter = (
|
|
2053
2483
|
child: Box,
|
|
2054
2484
|
context: readonly number[],
|
|
2055
2485
|
clipped: readonly Box[],
|
|
2486
|
+
): void => {
|
|
2487
|
+
if (!fixedShift || atViewport || !fixedToViewport(child)) {
|
|
2488
|
+
enterAt(child, context, clipped);
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
x -= fixedShift.x;
|
|
2492
|
+
y -= fixedShift.y;
|
|
2493
|
+
atViewport = true;
|
|
2494
|
+
try {
|
|
2495
|
+
enterAt(child, context, clipped);
|
|
2496
|
+
} finally {
|
|
2497
|
+
x += fixedShift.x;
|
|
2498
|
+
y += fixedShift.y;
|
|
2499
|
+
atViewport = false;
|
|
2500
|
+
}
|
|
2501
|
+
};
|
|
2502
|
+
const enterAt = (
|
|
2503
|
+
child: Box,
|
|
2504
|
+
context: readonly number[],
|
|
2505
|
+
clipped: readonly Box[],
|
|
2056
2506
|
): void => {
|
|
2057
2507
|
const style = child.style;
|
|
2058
2508
|
if (
|
|
@@ -2223,16 +2673,116 @@ const FACE_FIELDS = [
|
|
|
2223
2673
|
/** Whether two styles differ in ink alone: true where they do, null where
|
|
2224
2674
|
* they do not differ, false where something else does. Custom properties
|
|
2225
2675
|
* are read through the properties that use them. */
|
|
2226
|
-
|
|
2676
|
+
/** What a restyle in place changed beyond ink: how far a box's ink
|
|
2677
|
+
* reaches, the order its layer paints in, or where it is. */
|
|
2678
|
+
interface HoverChange {
|
|
2679
|
+
reach: boolean;
|
|
2680
|
+
order: boolean;
|
|
2681
|
+
move: boolean;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
/** The replaced boxes that are controls (`BoxTree.controls`, `controls.ts`):
|
|
2685
|
+
* a widget mounted beside the document, or a button whose press it reports. */
|
|
2686
|
+
const WIDGETS = new Set<ReplacedKind>([
|
|
2687
|
+
'input',
|
|
2688
|
+
'textarea',
|
|
2689
|
+
'select',
|
|
2690
|
+
'button',
|
|
2691
|
+
'checkbox',
|
|
2692
|
+
'radio',
|
|
2693
|
+
]);
|
|
2694
|
+
|
|
2695
|
+
/** What only moves how far a box's ink reaches: a shadow, and an outline's
|
|
2696
|
+
* size, which take no room (CSS Backgrounds 3, 7.1; CSS UI 4, 3). */
|
|
2697
|
+
const INK_REACH = new Set([
|
|
2698
|
+
'boxShadow',
|
|
2699
|
+
'outlineStyle',
|
|
2700
|
+
'outlineWidth',
|
|
2701
|
+
'outlineOffset',
|
|
2702
|
+
]);
|
|
2703
|
+
|
|
2704
|
+
/**
|
|
2705
|
+
* How two styles of one element differ, for a restyle in place: null where
|
|
2706
|
+
* they do not, a `HoverChange` where every difference is ink, how far ink
|
|
2707
|
+
* reaches, a `z-index` that stays a stacking context's, or a translation —
|
|
2708
|
+
* none of which moves anything but the box and what it holds (CSS
|
|
2709
|
+
* Transforms 1: a transform does not affect layout) — and false where
|
|
2710
|
+
* anything else differs.
|
|
2711
|
+
*/
|
|
2712
|
+
function hoverChange(
|
|
2713
|
+
was: ComputedStyle,
|
|
2714
|
+
now: ComputedStyle,
|
|
2715
|
+
): HoverChange | null | false {
|
|
2227
2716
|
const a = was as unknown as Record<string, unknown>;
|
|
2228
2717
|
const b = now as unknown as Record<string, unknown>;
|
|
2229
|
-
let
|
|
2718
|
+
let change: HoverChange | null = null;
|
|
2230
2719
|
for (const key in b) {
|
|
2231
2720
|
if (key === 'custom' || sameValue(a[key], b[key])) continue;
|
|
2232
|
-
|
|
2233
|
-
|
|
2721
|
+
change ??= { reach: false, order: false, move: false };
|
|
2722
|
+
if (PAINT_ONLY.has(key)) continue;
|
|
2723
|
+
if (INK_REACH.has(key)) {
|
|
2724
|
+
change.reach = true;
|
|
2725
|
+
continue;
|
|
2726
|
+
}
|
|
2727
|
+
if (key === 'zIndex') {
|
|
2728
|
+
// one stacking context either way, with nothing hoisted below the
|
|
2729
|
+
// flow (`hoistNegative`): only its place among its layers changes
|
|
2730
|
+
const z0 = a[key];
|
|
2731
|
+
const z1 = b[key];
|
|
2732
|
+
if (typeof z0 !== 'number' || typeof z1 !== 'number') return false;
|
|
2733
|
+
if (z0 < 0 || z1 < 0) return false;
|
|
2734
|
+
change.order = true;
|
|
2735
|
+
continue;
|
|
2736
|
+
}
|
|
2737
|
+
if (key === 'transformTranslate' || key === 'translate') {
|
|
2738
|
+
change.move = true;
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
return false;
|
|
2742
|
+
}
|
|
2743
|
+
return change;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
/**
|
|
2747
|
+
* Whether a box can take a new translation where it is: a box of its own
|
|
2748
|
+
* rather than an inline one, whose text is what moves; and the same
|
|
2749
|
+
* containing block and stacking context for what is in it, which a
|
|
2750
|
+
* transform makes a box (CSS Transforms 1, 2) — so either transformed
|
|
2751
|
+
* before and after, or already positioned with a `z-index`, and holding
|
|
2752
|
+
* nothing fixed, which only a transform takes in.
|
|
2753
|
+
*/
|
|
2754
|
+
function movable(box: Box, was: ComputedStyle, now: ComputedStyle): boolean {
|
|
2755
|
+
if (box.kind === 'inline') return false;
|
|
2756
|
+
if (transformed(was) === transformed(now)) return true;
|
|
2757
|
+
if (was.position === 'static') return false;
|
|
2758
|
+
if (typeof was.zIndex !== 'number' || typeof now.zIndex !== 'number') {
|
|
2759
|
+
return false;
|
|
2760
|
+
}
|
|
2761
|
+
const stack: Box[] = [...box.children];
|
|
2762
|
+
while (stack.length) {
|
|
2763
|
+
const at = stack.pop()!;
|
|
2764
|
+
if (at.style.position === 'fixed') return false;
|
|
2765
|
+
for (const child of at.children) stack.push(child);
|
|
2766
|
+
}
|
|
2767
|
+
return true;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
/** Where a box draws, in document coordinates: its ink bounds, or for an
|
|
2771
|
+
* inline box or a run of text, which are drawn on their block's lines and
|
|
2772
|
+
* have none of their own, the block's. */
|
|
2773
|
+
function inkOf(box: Box): Rect | null {
|
|
2774
|
+
let at: Box | null = box;
|
|
2775
|
+
while (at && (at.kind === 'text' || at.kind === 'break' || !hasRect(at))) {
|
|
2776
|
+
at = at.parent;
|
|
2234
2777
|
}
|
|
2235
|
-
|
|
2778
|
+
if (!at || at.boundsY === Infinity) return null;
|
|
2779
|
+
if (!(at.boundsWidth > 0 && at.boundsHeight > 0)) return null;
|
|
2780
|
+
return {
|
|
2781
|
+
x: at.boundsX,
|
|
2782
|
+
y: at.boundsY,
|
|
2783
|
+
width: at.boundsWidth,
|
|
2784
|
+
height: at.boundsHeight,
|
|
2785
|
+
};
|
|
2236
2786
|
}
|
|
2237
2787
|
|
|
2238
2788
|
/** Structural equality for a computed value: a number, a string, or the
|
|
@@ -2375,45 +2925,42 @@ function ownerOf(boxes: readonly Box[], index: number): Element | null {
|
|
|
2375
2925
|
|
|
2376
2926
|
export type { ControlRect, ReplacedKind, ResourceRequest, ResourceResult };
|
|
2377
2927
|
|
|
2378
|
-
/** The advance of a
|
|
2379
|
-
*
|
|
2380
|
-
function zeroWidthOf(
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
): number | null {
|
|
2928
|
+
/** The advance of a face's "0", laid out: null where the engine cannot
|
|
2929
|
+
* lay it out. */
|
|
2930
|
+
function zeroWidthOf(fonts: FontsLike, face: MetricFace): number | null {
|
|
2931
|
+
const { family, size, weight } = face;
|
|
2932
|
+
// a run is upright or slanted, as `inline.ts` sets one
|
|
2933
|
+
const style = face.style === 'normal' ? 'normal' : 'italic';
|
|
2385
2934
|
try {
|
|
2386
|
-
|
|
2387
|
-
|
|
2935
|
+
const run = { family, size, weight, style } as const;
|
|
2936
|
+
return fonts.layout([{ text: '0', ...run }], run, {}).width;
|
|
2388
2937
|
} catch {
|
|
2389
2938
|
return null;
|
|
2390
2939
|
}
|
|
2391
2940
|
}
|
|
2392
2941
|
|
|
2393
|
-
/** A
|
|
2394
|
-
*
|
|
2395
|
-
function normalLineOf(
|
|
2396
|
-
|
|
2397
|
-
family: string,
|
|
2398
|
-
size: number,
|
|
2399
|
-
): number | null {
|
|
2942
|
+
/** A face's own line height, as the engine reports it: null where it
|
|
2943
|
+
* cannot say. */
|
|
2944
|
+
function normalLineOf(fonts: FontsLike, face: MetricFace): number | null {
|
|
2945
|
+
const { family, size, weight, style } = face;
|
|
2400
2946
|
try {
|
|
2401
|
-
const line = fonts
|
|
2947
|
+
const line = fonts
|
|
2948
|
+
.match(family, { size, weight, style })
|
|
2949
|
+
.metrics(size).lineHeight;
|
|
2402
2950
|
return line > 0 ? line : null;
|
|
2403
2951
|
} catch {
|
|
2404
2952
|
return null;
|
|
2405
2953
|
}
|
|
2406
2954
|
}
|
|
2407
2955
|
|
|
2408
|
-
/** A
|
|
2409
|
-
*
|
|
2410
|
-
function xHeightOf(
|
|
2411
|
-
|
|
2412
|
-
family: string,
|
|
2413
|
-
size: number,
|
|
2414
|
-
): number | null {
|
|
2956
|
+
/** A face's x-height, as the engine reports it: null where the face
|
|
2957
|
+
* states none, or the engine does not say. */
|
|
2958
|
+
function xHeightOf(fonts: FontsLike, face: MetricFace): number | null {
|
|
2959
|
+
const { family, size, weight, style } = face;
|
|
2415
2960
|
try {
|
|
2416
|
-
const metrics = fonts
|
|
2961
|
+
const metrics = fonts
|
|
2962
|
+
.match(family, { size, weight, style })
|
|
2963
|
+
.metrics(size) as {
|
|
2417
2964
|
xHeight?: number | null;
|
|
2418
2965
|
};
|
|
2419
2966
|
const x = metrics.xHeight;
|