@react-x11/components 0.11.0 → 0.13.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/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 +38 -10
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +43 -58
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +134 -20
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +538 -128
- 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 +40 -2
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +391 -13
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +52 -1
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +328 -11
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.d.ts +11 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +73 -7
- 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 +38 -3
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +25 -1
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +99 -6
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/form.d.ts +200 -0
- package/dist/html/form.d.ts.map +1 -0
- package/dist/html/form.js +801 -0
- package/dist/html/form.js.map +1 -0
- package/dist/html/index.d.ts +12 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +30 -264
- 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 +186 -19
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +65 -7
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +87 -22
- 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/flex.js +4 -1
- package/dist/html/layout/flex.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 +26 -2
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +1232 -115
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +120 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +993 -174
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +75 -2
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +836 -200
- 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/widgets.d.ts +29 -0
- package/dist/html/widgets.d.ts.map +1 -0
- package/dist/html/widgets.js +620 -0
- package/dist/html/widgets.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/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/richtext/node.d.ts +7 -0
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js.map +1 -1
- package/package.json +6 -4
- package/src/flow/node.ts +80 -64
- package/src/html/controls.ts +79 -58
- package/src/html/css/cascade.ts +610 -146
- package/src/html/css/color.ts +87 -0
- package/src/html/css/parse.ts +431 -13
- package/src/html/css/style.ts +367 -11
- package/src/html/css/ua.ts +76 -8
- package/src/html/css/values.ts +31 -3
- package/src/html/css/vars.ts +2 -2
- package/src/html/dom.ts +41 -3
- package/src/html/fonts.ts +116 -6
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -300
- package/src/html/layout/block.ts +231 -16
- package/src/html/layout/boxes.ts +133 -32
- package/src/html/layout/cache.ts +8 -3
- package/src/html/layout/flex.ts +4 -1
- package/src/html/layout/floats.ts +24 -0
- package/src/html/layout/inline.ts +1445 -108
- package/src/html/node.ts +1010 -166
- package/src/html/paint.ts +980 -270
- package/src/html/surfaces.ts +21 -1
- package/src/html/widgets.ts +735 -0
- package/src/index.ts +1 -0
- package/src/internal/window.ts +36 -8
- package/src/richtext/node.ts +7 -0
package/src/html/node.ts
CHANGED
|
@@ -50,8 +50,14 @@ import type { Element } from 'domhandler';
|
|
|
50
50
|
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
|
-
import {
|
|
54
|
-
|
|
53
|
+
import {
|
|
54
|
+
Cascade,
|
|
55
|
+
HOVER_FOLLOWED,
|
|
56
|
+
HOVER_PSEUDO_NONE,
|
|
57
|
+
HOVER_PSEUDO_OTHER,
|
|
58
|
+
HOVER_UNTOUCHED,
|
|
59
|
+
} from './css/cascade.js';
|
|
60
|
+
import type { HoverTouch, KeptStyles, MetricFace } from './css/cascade.js';
|
|
55
61
|
import { mediaMatches, parseStylesheet } from './css/parse.js';
|
|
56
62
|
import type { Stylesheet } from './css/parse.js';
|
|
57
63
|
import { uaStylesheet } from './css/ua.js';
|
|
@@ -60,8 +66,13 @@ import {
|
|
|
60
66
|
BOX_RAISES,
|
|
61
67
|
buildBoxes,
|
|
62
68
|
CONTENT_IMAGES,
|
|
69
|
+
CLEARED_FROM,
|
|
70
|
+
CUT_BLOCKS,
|
|
71
|
+
GENERATED_FROM,
|
|
72
|
+
INLINE_OFFSETS,
|
|
63
73
|
LINE_BOX_RAISES,
|
|
64
74
|
SHIFTED_LINES,
|
|
75
|
+
TEXT_SHIFTS,
|
|
65
76
|
} from './layout/boxes.js';
|
|
66
77
|
import type {
|
|
67
78
|
Box,
|
|
@@ -71,10 +82,11 @@ import type {
|
|
|
71
82
|
ReplacedKind,
|
|
72
83
|
TextLayoutLike,
|
|
73
84
|
} from './layout/boxes.js';
|
|
74
|
-
import { layoutDocument } from './layout/block.js';
|
|
85
|
+
import { layoutDocument, retranslate, transformed } from './layout/block.js';
|
|
75
86
|
import { TextLayoutCache } from './layout/cache.js';
|
|
76
87
|
import { shapingSafe } from './layout/shaping.js';
|
|
77
|
-
import { SurfaceCache } from './surfaces.js';
|
|
88
|
+
import { SurfaceCache, newSurface } from './surfaces.js';
|
|
89
|
+
import type { SurfaceLike } from './surfaces.js';
|
|
78
90
|
import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
|
|
79
91
|
import type { FontsLike } from './layout/inline.js';
|
|
80
92
|
import type { TextRun } from '../richtext/index.js';
|
|
@@ -88,12 +100,19 @@ import {
|
|
|
88
100
|
import { lineBands as bandsFor } from '../richtext/runs.js';
|
|
89
101
|
import {
|
|
90
102
|
clipsOverflow,
|
|
103
|
+
computePaintBounds,
|
|
91
104
|
containingBlockOf,
|
|
105
|
+
FIXED_BOXES,
|
|
106
|
+
fixedToViewport,
|
|
107
|
+
forgetDecoratedAncestors,
|
|
92
108
|
hasRect,
|
|
109
|
+
hoistNegative,
|
|
93
110
|
holds,
|
|
94
111
|
holdsAbsolute,
|
|
95
112
|
paintDocument,
|
|
96
113
|
queryChildIndex,
|
|
114
|
+
selectionRows,
|
|
115
|
+
stackLayers,
|
|
97
116
|
} from './paint.js';
|
|
98
117
|
import type { PaintContext } from './paint.js';
|
|
99
118
|
import { controlRectsOf, measureControl } from './controls.js';
|
|
@@ -221,6 +240,8 @@ export class HtmlViewNode extends Node {
|
|
|
221
240
|
private _sheetsRead: SheetsRead | null = null;
|
|
222
241
|
/** Blurred shadows, drawn once each. */
|
|
223
242
|
private _shadowCache: SurfaceCache | null = null;
|
|
243
|
+
/** Whether the backend was found to have no offscreen surface. */
|
|
244
|
+
private _noSurface = false;
|
|
224
245
|
private _cascade: Cascade | null = null;
|
|
225
246
|
private _tree: BoxTree | null = null;
|
|
226
247
|
/** The faces `_warmFaces` has asked the font matcher for, and of which
|
|
@@ -236,6 +257,8 @@ export class HtmlViewNode extends Node {
|
|
|
236
257
|
* that read it (`vw`, `vh`). */
|
|
237
258
|
private _styledWidth = -1;
|
|
238
259
|
private _styledHeight = -1;
|
|
260
|
+
/** The cascade the box tree's styles were computed by. */
|
|
261
|
+
private _styledWith: Cascade | null = null;
|
|
239
262
|
private _documentHeight = 0;
|
|
240
263
|
private _documentWidth = 0;
|
|
241
264
|
/** The viewport height the document was last laid out under, and whether
|
|
@@ -253,6 +276,20 @@ export class HtmlViewNode extends Node {
|
|
|
253
276
|
* the text carries surrogate pairs. null until checked. */
|
|
254
277
|
private _pointsAreUnits: boolean | null = null;
|
|
255
278
|
private _hovered: Element[] = [];
|
|
279
|
+
/** Where the pointer last moved to, in logical window pixels, while it
|
|
280
|
+
* is over the element (`defaultMouseMove`). */
|
|
281
|
+
private _pointerAt: { x: number; y: number } | null = null;
|
|
282
|
+
/** A hover held until the content under a still pointer stops moving:
|
|
283
|
+
* the timer, and when the content last moved (`_holdHover`). */
|
|
284
|
+
private _heldHover: unknown = null;
|
|
285
|
+
private _heldSince = 0;
|
|
286
|
+
/**
|
|
287
|
+
* The elements the build of the boxes that is due has to style again,
|
|
288
|
+
* where every change asking for it said which (`_invalidate`), and null
|
|
289
|
+
* where any element's style may have changed — a new cascade, a viewport
|
|
290
|
+
* a `vw` reads, a face that arrived.
|
|
291
|
+
*/
|
|
292
|
+
private _restyleOnly: Set<Element> | null = null;
|
|
256
293
|
private _scriptsSeen = new WeakSet<Element>();
|
|
257
294
|
private _controls: ControlRect[] = [];
|
|
258
295
|
private _reportedDomRevision = -1;
|
|
@@ -350,6 +387,7 @@ export class HtmlViewNode extends Node {
|
|
|
350
387
|
controlPadY: look.controlPadY * s,
|
|
351
388
|
controlBorder: look.controlBorder * s,
|
|
352
389
|
controlRadius: look.controlRadius * s,
|
|
390
|
+
controlFontSize: (look.controlFontSize ?? look.fontSize) * s,
|
|
353
391
|
};
|
|
354
392
|
this._deviceLookFor = look;
|
|
355
393
|
this._deviceLookAt = s;
|
|
@@ -359,7 +397,26 @@ export class HtmlViewNode extends Node {
|
|
|
359
397
|
|
|
360
398
|
// --- the pipeline ---------------------------------------------------------
|
|
361
399
|
|
|
362
|
-
|
|
400
|
+
/**
|
|
401
|
+
* `restyle` with `Stale.Boxes`: the elements whose styles the change
|
|
402
|
+
* could have touched, where that is all of it — a pointer move's reach
|
|
403
|
+
* (`_hoverReach`). The build it asks for keeps every other element's
|
|
404
|
+
* (`_restyleOnly`).
|
|
405
|
+
*/
|
|
406
|
+
private _invalidate(
|
|
407
|
+
stale: Stale,
|
|
408
|
+
restyle: ReadonlySet<Element> | null = null,
|
|
409
|
+
): void {
|
|
410
|
+
if (stale >= Stale.Boxes) {
|
|
411
|
+
const only = stale === Stale.Boxes ? restyle : null;
|
|
412
|
+
if (this._stale < Stale.Boxes) {
|
|
413
|
+
this._restyleOnly = only ? new Set(only) : null;
|
|
414
|
+
} else if (this._restyleOnly && only) {
|
|
415
|
+
for (const el of only) this._restyleOnly.add(el);
|
|
416
|
+
} else {
|
|
417
|
+
this._restyleOnly = null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
363
420
|
if (stale > this._stale) this._stale = stale;
|
|
364
421
|
if (stale >= Stale.Layout) this.invalidateMeasure('content');
|
|
365
422
|
this.invalidate(stale >= Stale.Layout, this, 'props');
|
|
@@ -440,13 +497,15 @@ export class HtmlViewNode extends Node {
|
|
|
440
497
|
*/
|
|
441
498
|
private _requestBackgrounds(tree: BoxTree): void {
|
|
442
499
|
for (const box of tree.backgrounds) {
|
|
443
|
-
|
|
500
|
+
// a pseudo-element's are its element's
|
|
501
|
+
const element = box.el ?? GENERATED_FROM.get(box);
|
|
502
|
+
if (!element) continue;
|
|
444
503
|
// each layer's, where there is more than one
|
|
445
504
|
for (const url of box.style.backgroundImages ?? [
|
|
446
505
|
box.style.backgroundImage,
|
|
447
506
|
]) {
|
|
448
507
|
if (typeof url === 'string') {
|
|
449
|
-
this._resources.request({ url, kind: 'image', element
|
|
508
|
+
this._resources.request({ url, kind: 'image', element });
|
|
450
509
|
}
|
|
451
510
|
}
|
|
452
511
|
const border = box.style.borderImage.source;
|
|
@@ -454,9 +513,15 @@ export class HtmlViewNode extends Node {
|
|
|
454
513
|
this._resources.request({
|
|
455
514
|
url: border,
|
|
456
515
|
kind: 'image',
|
|
457
|
-
element
|
|
516
|
+
element,
|
|
458
517
|
});
|
|
459
518
|
}
|
|
519
|
+
// and each mask layer's, which is an image as a background's is
|
|
520
|
+
for (const url of box.style.mask.images) {
|
|
521
|
+
if (typeof url === 'string') {
|
|
522
|
+
this._resources.request({ url, kind: 'image', element });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
460
525
|
}
|
|
461
526
|
// and every image generated content names, which is only known there
|
|
462
527
|
// too
|
|
@@ -557,7 +622,10 @@ export class HtmlViewNode extends Node {
|
|
|
557
622
|
kept.cascade.viewportHeight = this._viewportHeight();
|
|
558
623
|
faces = kept.faces;
|
|
559
624
|
} else {
|
|
560
|
-
|
|
625
|
+
// the UA sheet's `px` are CSS pixels, which the cascade scales like an
|
|
626
|
+
// author's, so it is written from the look in CSS pixels: from the
|
|
627
|
+
// device one, a control's size and a button's chrome were doubled at 2x
|
|
628
|
+
const sheets: Stylesheet[] = [uaStylesheet(props.look)];
|
|
561
629
|
const imports: ImportRead[][] = [];
|
|
562
630
|
faces = [];
|
|
563
631
|
let order = 0;
|
|
@@ -612,10 +680,17 @@ export class HtmlViewNode extends Node {
|
|
|
612
680
|
// The faces this width and scheme declare: a `@font-face` may sit in a
|
|
613
681
|
// `@media` block like any rule.
|
|
614
682
|
const cssWidth = width / this._scale;
|
|
683
|
+
const cssHeight = this._viewportHeight() / this._scale;
|
|
615
684
|
this._webFonts.setFallback(look.fontFamily);
|
|
616
685
|
this._webFonts.setFaces(
|
|
617
686
|
faces.filter((f) =>
|
|
618
|
-
mediaMatches(
|
|
687
|
+
mediaMatches(
|
|
688
|
+
f.rule.media,
|
|
689
|
+
cssWidth,
|
|
690
|
+
look.colorScheme,
|
|
691
|
+
cssHeight,
|
|
692
|
+
this._scale,
|
|
693
|
+
),
|
|
619
694
|
),
|
|
620
695
|
);
|
|
621
696
|
this._cascade.setPointer({
|
|
@@ -705,6 +780,55 @@ export class HtmlViewNode extends Node {
|
|
|
705
780
|
return true;
|
|
706
781
|
}
|
|
707
782
|
|
|
783
|
+
/**
|
|
784
|
+
* The viewport the document is seen through, in window coordinates: the
|
|
785
|
+
* content box of the pane that scrolls the element, where one does —
|
|
786
|
+
* what `position: fixed` and a fixed background are placed against, as
|
|
787
|
+
* a browser places them against its page area. Null where nothing
|
|
788
|
+
* scrolls it, and the element is its own.
|
|
789
|
+
*/
|
|
790
|
+
private _viewport(): Rect | null {
|
|
791
|
+
for (let node = this.parent; node; node = node.parent) {
|
|
792
|
+
// `Scrollable`'s, and not every node's: asked the way core asks it
|
|
793
|
+
const scroller = node as { isScroller?: () => boolean };
|
|
794
|
+
if (!scroller.isScroller?.()) continue;
|
|
795
|
+
const box = node.contentBox();
|
|
796
|
+
return box.width > 0 && box.height > 0 ? box : null;
|
|
797
|
+
}
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* What the document draws fixed to the viewport of the pane that scrolls
|
|
803
|
+
* it, for that pane's scroll blit to repaint rather than copy with the
|
|
804
|
+
* text (react-x11's `viewportFixedRects`): the whole viewport where a
|
|
805
|
+
* fixed background shows through it, which makes a scroll a repaint, and
|
|
806
|
+
* where each fixed box is drawn. Null for a document with neither, and
|
|
807
|
+
* where nothing scrolls the element.
|
|
808
|
+
*/
|
|
809
|
+
override viewportFixedRects(): Rect[] | null {
|
|
810
|
+
const tree = this._tree;
|
|
811
|
+
const viewport = tree && this._viewport();
|
|
812
|
+
if (!tree || !viewport) return null;
|
|
813
|
+
if (hasFixedBackground(tree)) return [viewport];
|
|
814
|
+
const boxes = FIXED_BOXES.get(tree);
|
|
815
|
+
if (!boxes) return null;
|
|
816
|
+
// laid out against the viewport at the document's top, and drawn at the
|
|
817
|
+
// viewport (`atViewport`)
|
|
818
|
+
const root = tree.root;
|
|
819
|
+
const out: Rect[] = [];
|
|
820
|
+
for (const box of boxes) {
|
|
821
|
+
if (!(box.boundsWidth > 0 && box.boundsHeight > 0)) continue;
|
|
822
|
+
out.push({
|
|
823
|
+
x: viewport.x + box.boundsX - root.x,
|
|
824
|
+
y: viewport.y + box.boundsY - root.y,
|
|
825
|
+
width: box.boundsWidth,
|
|
826
|
+
height: box.boundsHeight,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
return out.length ? out : null;
|
|
830
|
+
}
|
|
831
|
+
|
|
708
832
|
private _viewportHeight(): number {
|
|
709
833
|
// The viewport a `vh` resolves against is the one the document is seen
|
|
710
834
|
// through, not the document — a document taller than it does not make
|
|
@@ -823,15 +947,21 @@ export class HtmlViewNode extends Node {
|
|
|
823
947
|
const wasWidth = this._documentWidth;
|
|
824
948
|
const wasHeight = this._documentHeight;
|
|
825
949
|
|
|
950
|
+
// the styles a build may keep (`_restyleOnly`), unless something
|
|
951
|
+
// found below changes them all
|
|
952
|
+
let restyleOnly = this._stale === Stale.Boxes ? this._restyleOnly : null;
|
|
953
|
+
this._restyleOnly = null;
|
|
826
954
|
if (this._stale >= Stale.Style || !this._cascade) {
|
|
827
955
|
this._restyle(target);
|
|
828
956
|
this._stale = Math.max(this._stale, Stale.Boxes) as Stale;
|
|
957
|
+
restyleOnly = null;
|
|
829
958
|
} else if (this._cascade.mediaBand(target) !== this._mediaBand) {
|
|
830
959
|
// A resize that crossed a `@media` breakpoint is the one resize that
|
|
831
960
|
// does have to restyle. Knowing which resizes those are is why the
|
|
832
961
|
// breakpoints are collected at parse time.
|
|
833
962
|
this._restyle(target);
|
|
834
963
|
this._stale = Math.max(this._stale, Stale.Boxes) as Stale;
|
|
964
|
+
restyleOnly = null;
|
|
835
965
|
}
|
|
836
966
|
|
|
837
967
|
const cascade = this._cascade;
|
|
@@ -843,18 +973,27 @@ export class HtmlViewNode extends Node {
|
|
|
843
973
|
// again, where some style reads the side that moved. A document that
|
|
844
974
|
// reads neither — most — goes on skipping the cascade on a resize.
|
|
845
975
|
if (
|
|
846
|
-
|
|
847
|
-
(
|
|
848
|
-
(cascade.readsViewportHeight && this._styledHeight !== viewport))
|
|
976
|
+
(cascade.readsViewportWidth && this._styledWidth !== target) ||
|
|
977
|
+
(cascade.readsViewportHeight && this._styledHeight !== viewport)
|
|
849
978
|
) {
|
|
850
|
-
this._stale = Stale.Boxes;
|
|
979
|
+
if (this._stale < Stale.Boxes) this._stale = Stale.Boxes;
|
|
980
|
+
restyleOnly = null;
|
|
851
981
|
}
|
|
852
982
|
|
|
853
983
|
if (this._stale >= Stale.Boxes || !this._tree) {
|
|
854
984
|
const look = this._deviceLook();
|
|
985
|
+
// A build a pointer move asked for styles the elements the move
|
|
986
|
+
// reached, and takes every other element's from the tree it replaces
|
|
987
|
+
// (`Cascade.beginSharing`): the first build only, since a second is
|
|
988
|
+
// one the first found a reason for — a face, an image's size.
|
|
989
|
+
let kept: KeptStyles | null =
|
|
990
|
+
restyleOnly && this._tree && this._styledWith === cascade
|
|
991
|
+
? { styles: this._tree.styles, restyle: restyleOnly }
|
|
992
|
+
: null;
|
|
855
993
|
const build = () =>
|
|
856
994
|
buildBoxes(this._source.document, {
|
|
857
995
|
cascade,
|
|
996
|
+
kept,
|
|
858
997
|
scale: this._scale,
|
|
859
998
|
imageSize: (el) => this._resources.imageSize(imageUrlOf(el) ?? ''),
|
|
860
999
|
urlSize: (url) => this._resources.imageSize(url),
|
|
@@ -862,6 +1001,8 @@ export class HtmlViewNode extends Node {
|
|
|
862
1001
|
measureControl(el, kind, style, this._fonts(), look),
|
|
863
1002
|
});
|
|
864
1003
|
this._tree = build();
|
|
1004
|
+
kept = null;
|
|
1005
|
+
this._styledWith = cascade;
|
|
865
1006
|
this._styledWidth = target;
|
|
866
1007
|
this._styledHeight = viewport;
|
|
867
1008
|
this._requestBackgrounds(this._tree);
|
|
@@ -940,6 +1081,7 @@ export class HtmlViewNode extends Node {
|
|
|
940
1081
|
y: r.y / s,
|
|
941
1082
|
width: r.width / s,
|
|
942
1083
|
height: r.height / s,
|
|
1084
|
+
fontSize: r.fontSize / s,
|
|
943
1085
|
...(r.bare && {
|
|
944
1086
|
bare: {
|
|
945
1087
|
...r.bare,
|
|
@@ -1042,6 +1184,7 @@ export class HtmlViewNode extends Node {
|
|
|
1042
1184
|
}
|
|
1043
1185
|
|
|
1044
1186
|
override destroySubtree(): void {
|
|
1187
|
+
this._dropHeldHover();
|
|
1045
1188
|
this._resources.destroy();
|
|
1046
1189
|
this._webFonts.destroy();
|
|
1047
1190
|
this._source.destroy();
|
|
@@ -1139,7 +1282,8 @@ export class HtmlViewNode extends Node {
|
|
|
1139
1282
|
const to = this._toUnits(end);
|
|
1140
1283
|
if (to <= from) return [];
|
|
1141
1284
|
const out: Rect[] = [];
|
|
1142
|
-
|
|
1285
|
+
// the rows the highlight paints, over glyphs taller than their line
|
|
1286
|
+
collectBands(tree.root, from, to, this.abs.x, this.abs.y, out, true);
|
|
1143
1287
|
return out;
|
|
1144
1288
|
}
|
|
1145
1289
|
|
|
@@ -1196,39 +1340,54 @@ export class HtmlViewNode extends Node {
|
|
|
1196
1340
|
rect = { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
1197
1341
|
} else {
|
|
1198
1342
|
const bands: Rect[] = [];
|
|
1199
|
-
|
|
1200
|
-
//
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
box.subtreeTextStart,
|
|
1210
|
-
box.subtreeTextEnd,
|
|
1211
|
-
0,
|
|
1212
|
-
0,
|
|
1213
|
-
bands,
|
|
1214
|
-
);
|
|
1215
|
-
} else {
|
|
1216
|
-
collectBands(
|
|
1217
|
-
tree.root,
|
|
1218
|
-
box.subtreeTextStart,
|
|
1219
|
-
box.subtreeTextEnd,
|
|
1220
|
-
0,
|
|
1221
|
-
0,
|
|
1222
|
-
bands,
|
|
1223
|
-
);
|
|
1224
|
-
}
|
|
1343
|
+
const fonts = this._layouts?.fonts ?? null;
|
|
1344
|
+
// an inline box broken around the blocks in it is its pieces, a box
|
|
1345
|
+
// of the element's each (CSS 2.1 9.2.1.1), and its fragments are
|
|
1346
|
+
// theirs; and on the lines between them it has a fragment around each
|
|
1347
|
+
// block, across the box the block is in and as tall as its border
|
|
1348
|
+
// box, as a browser's has — Blink's block-in-inline, Gecko's split
|
|
1349
|
+
// inline's anonymous block. A list item made inline around a block
|
|
1350
|
+
// link measured as the empty edge after it
|
|
1351
|
+
for (const piece of piecesOf(tree.root, box)) {
|
|
1352
|
+
inlineBands(tree.root, piece, fonts, bands);
|
|
1225
1353
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1354
|
+
for (const block of (box.cut ? CUT_BLOCKS.get(box) : null) ?? []) {
|
|
1355
|
+
const around = block.parent;
|
|
1356
|
+
if (!around) continue;
|
|
1357
|
+
// from where clearance moved it down from, as Blink's line around
|
|
1358
|
+
// a block in an inline box starts there: the Zen Garden's 214
|
|
1359
|
+
// makes its content an inline box of floats and a footer that
|
|
1360
|
+
// clears them, and Chrome measures it from the floats' top
|
|
1361
|
+
const top = Math.min(block.y, CLEARED_FROM.get(block) ?? block.y);
|
|
1362
|
+
bands.push({
|
|
1363
|
+
x: around.contentX,
|
|
1364
|
+
y: top,
|
|
1365
|
+
width: around.contentWidth,
|
|
1366
|
+
height: block.y + block.height - top,
|
|
1367
|
+
});
|
|
1230
1368
|
}
|
|
1231
1369
|
for (const band of bands) rect = rect ? unionRect(rect, band) : band;
|
|
1370
|
+
if (!rect) {
|
|
1371
|
+
// on no line, where its block has none: text set at no size takes
|
|
1372
|
+
// no room and gets no box, and CSS puts it on lines of no height
|
|
1373
|
+
// at the block's top (9.4.2), as a browser measures it — nothing,
|
|
1374
|
+
// where the block's content starts. Where the text after it
|
|
1375
|
+
// starts was the next text that took room, elsewhere in the
|
|
1376
|
+
// document: the Zen Garden's 170 hides a heading at `font-size:
|
|
1377
|
+
// 0`, and an `<abbr>` in it measured as the end of the paragraph
|
|
1378
|
+
// above it
|
|
1379
|
+
let block = box.parent;
|
|
1380
|
+
while (block && block.kind === 'inline') block = block.parent;
|
|
1381
|
+
if (block && !block.lines?.length) {
|
|
1382
|
+
const rtl = block.style.direction === 'rtl';
|
|
1383
|
+
rect = {
|
|
1384
|
+
x: rtl ? block.contentX + block.contentWidth : block.contentX,
|
|
1385
|
+
y: block.contentY,
|
|
1386
|
+
width: 0,
|
|
1387
|
+
height: 0,
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1232
1391
|
if (!rect) {
|
|
1233
1392
|
// an empty subtree's range is `[0, 0)`, wherever it stands: where
|
|
1234
1393
|
// its text would be is where the text after it starts
|
|
@@ -1251,7 +1410,20 @@ export class HtmlViewNode extends Node {
|
|
|
1251
1410
|
const tree = this._tree;
|
|
1252
1411
|
if (!tree) return null;
|
|
1253
1412
|
const local = this._toDocument(x, y);
|
|
1254
|
-
return deepestAt(tree, local.x, local.y);
|
|
1413
|
+
return deepestAt(tree, local.x, local.y, undefined, this._fixedShift());
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/** How far a box fixed to the viewport is drawn from where it was laid
|
|
1417
|
+
* out, in the document's pixels: the scroll of the pane the element is
|
|
1418
|
+
* in (`atViewport`). Null where nothing scrolls it. */
|
|
1419
|
+
private _fixedShift(): { x: number; y: number } | null {
|
|
1420
|
+
const tree = this._tree;
|
|
1421
|
+
const viewport = tree && FIXED_BOXES.has(tree) && this._viewport();
|
|
1422
|
+
if (!tree || !viewport) return null;
|
|
1423
|
+
return {
|
|
1424
|
+
x: viewport.x - this.abs.x - tree.root.x,
|
|
1425
|
+
y: viewport.y - this.abs.y - tree.root.y,
|
|
1426
|
+
};
|
|
1255
1427
|
}
|
|
1256
1428
|
|
|
1257
1429
|
/**
|
|
@@ -1268,7 +1440,13 @@ export class HtmlViewNode extends Node {
|
|
|
1268
1440
|
const tree = this._tree;
|
|
1269
1441
|
if (!tree) return null;
|
|
1270
1442
|
const hit = { text: false };
|
|
1271
|
-
const el = deepestAt(
|
|
1443
|
+
const el = deepestAt(
|
|
1444
|
+
tree,
|
|
1445
|
+
x - this.abs.x,
|
|
1446
|
+
y - this.abs.y,
|
|
1447
|
+
hit,
|
|
1448
|
+
this._fixedShift(),
|
|
1449
|
+
);
|
|
1272
1450
|
const cursor = el ? tree.styles.get(el)?.style.cursor : null;
|
|
1273
1451
|
// a keyword; a `url()` this cannot load falls back, as its list would
|
|
1274
1452
|
if (cursor && cursor !== 'auto' && /^[a-z-]+$/.test(cursor)) return cursor;
|
|
@@ -1282,6 +1460,7 @@ export class HtmlViewNode extends Node {
|
|
|
1282
1460
|
* contains a `:hover` rule.
|
|
1283
1461
|
*/
|
|
1284
1462
|
setHover(x: number, y: number): boolean {
|
|
1463
|
+
this._dropHeldHover();
|
|
1285
1464
|
const cascade = this._cascade;
|
|
1286
1465
|
if (!cascade || !cascade.hoverSensitive) return false;
|
|
1287
1466
|
const chain: Element[] = [];
|
|
@@ -1299,6 +1478,7 @@ export class HtmlViewNode extends Node {
|
|
|
1299
1478
|
}
|
|
1300
1479
|
|
|
1301
1480
|
clearHover(): boolean {
|
|
1481
|
+
this._dropHeldHover();
|
|
1302
1482
|
if (!this._hovered.length) return false;
|
|
1303
1483
|
const was = this._hovered;
|
|
1304
1484
|
this._hovered = [];
|
|
@@ -1307,97 +1487,151 @@ export class HtmlViewNode extends Node {
|
|
|
1307
1487
|
return true;
|
|
1308
1488
|
}
|
|
1309
1489
|
|
|
1310
|
-
/**
|
|
1490
|
+
/**
|
|
1491
|
+
* The hovered chain moved: nothing, where no rule that tests the pointer
|
|
1492
|
+
* names an element it moved over; a restyle where they are, where all it
|
|
1493
|
+
* changed is ink (`_hoverInPlace`); and otherwise the boxes built again,
|
|
1494
|
+
* with the styles of every element the move did not reach kept.
|
|
1495
|
+
*/
|
|
1311
1496
|
private _restyleHover(was: readonly Element[], now: readonly Element[]) {
|
|
1312
|
-
const
|
|
1313
|
-
if (
|
|
1314
|
-
|
|
1497
|
+
const reach = this._hoverReach(was, now);
|
|
1498
|
+
if (reach !== null) {
|
|
1499
|
+
if (!reach.size) return;
|
|
1500
|
+
if (this._hoverInPlace(reach, was)) return;
|
|
1501
|
+
}
|
|
1502
|
+
this._invalidate(Stale.Boxes, reach);
|
|
1315
1503
|
}
|
|
1316
1504
|
|
|
1317
1505
|
/**
|
|
1318
|
-
*
|
|
1506
|
+
* The elements whose styles a move of the hovered chain can change, or
|
|
1507
|
+
* null where that cannot be said (`Cascade.hoverLocal`).
|
|
1319
1508
|
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
* - Only an element whose hover state flipped and that a compound testing
|
|
1328
|
-
* the pointer could match can change (`Cascade.hoverTouches`), with its
|
|
1329
|
-
* subtree, and its later siblings where a sibling combinator follows.
|
|
1330
|
-
* A move between two paragraphs under `a:hover` touches nothing.
|
|
1331
|
-
* - Those are styled again from their parents. Where anything but ink
|
|
1332
|
-
* differs (`PAINT_ONLY`), or a box is not the element's own — an
|
|
1333
|
-
* anonymous or a pseudo-element box, a marker, a control — this is
|
|
1334
|
-
* not the move to take.
|
|
1335
|
-
* - The boxes take their new styles, and each layout of their text is
|
|
1336
|
-
* made again from the runs it was made from (`TextLayoutCache.inputsOf`)
|
|
1337
|
-
* with the new ink (`runFor`), where its geometry comes out the same.
|
|
1338
|
-
*
|
|
1339
|
-
* Everything is checked before anything is changed. 'none' where nothing
|
|
1340
|
-
* could change, 'painted' where it was restyled here, false where the
|
|
1341
|
-
* document has to be built again.
|
|
1509
|
+
* Only an element whose hover state flipped and that a compound testing
|
|
1510
|
+
* the pointer could match can change (`Cascade.hoverTouches`), with its
|
|
1511
|
+
* subtree, and its later siblings' where a sibling combinator is in play;
|
|
1512
|
+
* and the elements a `:has()` testing the pointer may flip, which are
|
|
1513
|
+
* around the ones that changed. A move between two paragraphs under
|
|
1514
|
+
* `a:hover` reaches nothing.
|
|
1342
1515
|
*/
|
|
1343
|
-
private
|
|
1516
|
+
private _hoverReach(
|
|
1344
1517
|
was: readonly Element[],
|
|
1345
1518
|
now: readonly Element[],
|
|
1346
|
-
):
|
|
1519
|
+
): Set<Element> | null {
|
|
1347
1520
|
const cascade = this._cascade;
|
|
1348
|
-
|
|
1349
|
-
const layouts = this._layouts;
|
|
1350
|
-
if (!cascade || !tree || !layouts || !cascade.hoverLocal) return false;
|
|
1351
|
-
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0) return false;
|
|
1352
|
-
|
|
1521
|
+
if (!cascade || !cascade.hoverLocal) return null;
|
|
1353
1522
|
const before = new Set(was);
|
|
1354
1523
|
const after = new Set(now);
|
|
1355
|
-
const roots
|
|
1356
|
-
// and the elements a `:has()` testing the pointer may flip, which
|
|
1357
|
-
// are around the ones that changed; the chains are ancestor chains,
|
|
1358
|
-
// so the deepest that changed on each side is where to look from
|
|
1524
|
+
const roots = new Map<Element, HoverTouch>();
|
|
1359
1525
|
const flipped = (el: Element, other: Set<Element>): void => {
|
|
1360
1526
|
if (other.has(el)) return;
|
|
1361
|
-
|
|
1527
|
+
const touch = cascade.hoverTouches(el);
|
|
1528
|
+
if (touch > (roots.get(el) ?? HOVER_UNTOUCHED)) roots.set(el, touch);
|
|
1362
1529
|
};
|
|
1363
1530
|
for (const el of was) flipped(el, after);
|
|
1364
1531
|
for (const el of now) flipped(el, before);
|
|
1532
|
+
// the chains are ancestor chains, so the deepest that changed on each
|
|
1533
|
+
// side is where a `:has()` is looked for from
|
|
1365
1534
|
const deepest = (chain: readonly Element[], other: Set<Element>) => {
|
|
1366
1535
|
const el = chain[0];
|
|
1367
1536
|
if (el && !other.has(el)) cascade.hoverAnchors(el, roots);
|
|
1368
1537
|
};
|
|
1369
1538
|
deepest(was, after);
|
|
1370
1539
|
deepest(now, before);
|
|
1371
|
-
if (!roots.length) return 'none';
|
|
1372
1540
|
|
|
1373
|
-
//
|
|
1374
|
-
//
|
|
1375
|
-
//
|
|
1541
|
+
// the roots' subtrees, which inherit from them and a descendant
|
|
1542
|
+
// combinator reaches, and their later siblings' too where a sibling
|
|
1543
|
+
// combinator follows the compound that tests the pointer
|
|
1376
1544
|
const reach = new Set<Element>();
|
|
1377
|
-
const collect = (el: Element):
|
|
1545
|
+
const collect = (el: Element): void => {
|
|
1378
1546
|
const stack: Element[] = [el];
|
|
1379
1547
|
while (stack.length) {
|
|
1380
1548
|
const at = stack.pop()!;
|
|
1381
1549
|
if (reach.has(at)) continue;
|
|
1382
1550
|
reach.add(at);
|
|
1383
|
-
if (reach.size > HOVER_RESTYLE_LIMIT) return false;
|
|
1384
1551
|
for (const child of at.children)
|
|
1385
1552
|
if (isElement(child)) stack.push(child);
|
|
1386
1553
|
}
|
|
1387
|
-
return true;
|
|
1388
1554
|
};
|
|
1389
|
-
for (const root of roots) {
|
|
1390
|
-
|
|
1391
|
-
if (
|
|
1555
|
+
for (const [root, touch] of roots) {
|
|
1556
|
+
collect(root);
|
|
1557
|
+
if (touch !== HOVER_FOLLOWED) continue;
|
|
1392
1558
|
for (let s = root.nextSibling; s; s = s.nextSibling) {
|
|
1393
|
-
if (isElement(s)
|
|
1559
|
+
if (isElement(s)) collect(s);
|
|
1394
1560
|
}
|
|
1395
1561
|
}
|
|
1562
|
+
return reach;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* A pointer move restyled where it happened, when all it changed is ink.
|
|
1567
|
+
*
|
|
1568
|
+
* A move to another element is a change to the cascade, and the pipeline
|
|
1569
|
+
* answers one by building every box again and laying the document out:
|
|
1570
|
+
* 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
|
|
1571
|
+
* link the pointer crossed. But the rules a page writes for `:hover`
|
|
1572
|
+
* nearly all change a colour, an underline, a background or a border's
|
|
1573
|
+
* colour — 77 of that article's 79 — and those move nothing. So:
|
|
1574
|
+
*
|
|
1575
|
+
* - The elements the move reached (`_hoverReach`) are styled again from
|
|
1576
|
+
* their parents — the ones a rule testing the pointer answers
|
|
1577
|
+
* differently for, or whose parent's style changed; the rest, most of
|
|
1578
|
+
* a hovered card or table row, have the styles they had
|
|
1579
|
+
* (`Cascade.pointerChanged`). Where anything but ink differs
|
|
1580
|
+
* (`PAINT_ONLY`), this is not the move to take.
|
|
1581
|
+
* - The boxes take their new styles: an element's own, the text in it,
|
|
1582
|
+
* the anonymous boxes the fix-up made around what is in it, and its
|
|
1583
|
+
* `::before` and `::after`, styled again by their own rules. A box
|
|
1584
|
+
* whose style is derived some other way — a marker, a first letter, a
|
|
1585
|
+
* control — is not one to restyle here.
|
|
1586
|
+
* - Each layout of their text is made again from the runs it was made
|
|
1587
|
+
* from (`TextLayoutCache.inputsOf`) with the new ink (`runFor`), where
|
|
1588
|
+
* its geometry comes out the same.
|
|
1589
|
+
*
|
|
1590
|
+
* Everything is checked before anything is changed, and nothing is looked
|
|
1591
|
+
* at but the blocks the restyled elements are in (`_blocksOf`): a move
|
|
1592
|
+
* costs what it changed, in a document of any length. True where it was
|
|
1593
|
+
* restyled here, or had nothing to restyle; false where the boxes have to
|
|
1594
|
+
* be built again.
|
|
1595
|
+
*/
|
|
1596
|
+
private _hoverInPlace(
|
|
1597
|
+
reach: ReadonlySet<Element>,
|
|
1598
|
+
was: readonly Element[],
|
|
1599
|
+
): boolean {
|
|
1600
|
+
const cascade = this._cascade;
|
|
1601
|
+
const tree = this._tree;
|
|
1602
|
+
const layouts = this._layouts;
|
|
1603
|
+
if (!cascade || !tree || !layouts) return false;
|
|
1604
|
+
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0) return false;
|
|
1605
|
+
if (reach.size > HOVER_RESTYLE_LIMIT) return false;
|
|
1606
|
+
// a first line's, a first letter's and a marker's styles are layout's
|
|
1607
|
+
const pseudoRules = cascade.hoverPseudo;
|
|
1608
|
+
if (pseudoRules === HOVER_PSEUDO_OTHER) return false;
|
|
1609
|
+
|
|
1610
|
+
// whether the rules that test the pointer answer differently for an
|
|
1611
|
+
// element than they did before the move
|
|
1612
|
+
const before = { hovered: new Set(was), active: EMPTY_SET };
|
|
1613
|
+
const flips = new Map<Element, boolean>();
|
|
1614
|
+
const flipped = (el: Element): boolean => {
|
|
1615
|
+
let flip = flips.get(el);
|
|
1616
|
+
if (flip === undefined) {
|
|
1617
|
+
flip = cascade.pointerChanged(el, before);
|
|
1618
|
+
flips.set(el, flip);
|
|
1619
|
+
}
|
|
1620
|
+
return flip;
|
|
1621
|
+
};
|
|
1396
1622
|
|
|
1397
1623
|
// styled again from their parents, a parent first
|
|
1398
1624
|
const fresh = new Map<Element, ComputedStyle>();
|
|
1399
1625
|
const changed = new Map<Element, ComputedStyle>();
|
|
1626
|
+
// the ones whose ink is what it was: only their custom properties
|
|
1627
|
+
// changed, which the elements under them read
|
|
1628
|
+
const quiet = new Set<Element>();
|
|
1400
1629
|
let refused = false;
|
|
1630
|
+
// what changed beyond ink (`hoverChange`): how far some box's ink
|
|
1631
|
+
// reaches, the order its layer paints in, where one is
|
|
1632
|
+
let reaches = false;
|
|
1633
|
+
let reorders = false;
|
|
1634
|
+
const moving = new Set<Element>();
|
|
1401
1635
|
const styleOf = (el: Element): ComputedStyle | null => {
|
|
1402
1636
|
const kept = tree.styles.get(el);
|
|
1403
1637
|
if (!kept) return null;
|
|
@@ -1412,43 +1646,123 @@ export class HtmlViewNode extends Node {
|
|
|
1412
1646
|
refused = true;
|
|
1413
1647
|
return null;
|
|
1414
1648
|
}
|
|
1415
|
-
const
|
|
1649
|
+
const parentWas = parent ? tree.styles.get(parent)!.style : parentStyle;
|
|
1650
|
+
let style = kept.style;
|
|
1651
|
+
if (parentStyle !== parentWas || flipped(el)) {
|
|
1652
|
+
const made = cascade.styleFor(el, parentStyle, kept.inFlex);
|
|
1653
|
+
const diff = hoverChange(kept.style, made);
|
|
1654
|
+
if (diff === false) refused = true;
|
|
1655
|
+
else if (diff) {
|
|
1656
|
+
style = made;
|
|
1657
|
+
changed.set(el, made);
|
|
1658
|
+
if (!diff.ink) quiet.add(el);
|
|
1659
|
+
if (diff.reach) reaches = true;
|
|
1660
|
+
if (diff.order) reorders = true;
|
|
1661
|
+
if (diff.move) moving.add(el);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1416
1664
|
fresh.set(el, style);
|
|
1417
|
-
const diff = inkOnly(kept.style, style);
|
|
1418
|
-
if (diff === false) refused = true;
|
|
1419
|
-
else if (diff) changed.set(el, style);
|
|
1420
1665
|
return style;
|
|
1421
1666
|
};
|
|
1422
1667
|
for (const el of reach) {
|
|
1423
1668
|
styleOf(el);
|
|
1424
1669
|
if (refused) return false;
|
|
1425
1670
|
}
|
|
1426
|
-
if (!changed.size) return
|
|
1671
|
+
if (!changed.size && pseudoRules === HOVER_PSEUDO_NONE) return true;
|
|
1427
1672
|
for (const el of changed.keys()) {
|
|
1428
1673
|
const tag = tagOf(el);
|
|
1429
1674
|
// their backgrounds are the canvas's
|
|
1430
1675
|
if (tag === 'html' || tag === 'body') return false;
|
|
1431
1676
|
}
|
|
1432
1677
|
|
|
1433
|
-
//
|
|
1434
|
-
//
|
|
1678
|
+
// The boxes, each under its parent: what a box takes depends on what
|
|
1679
|
+
// the box it is in took.
|
|
1435
1680
|
const fonts = layouts.fonts;
|
|
1681
|
+
const blocks = this._blocksOf(
|
|
1682
|
+
tree,
|
|
1683
|
+
pseudoRules === HOVER_PSEUDO_NONE ? changed.keys() : reach,
|
|
1684
|
+
);
|
|
1436
1685
|
const restyled: [Box, ComputedStyle][] = [];
|
|
1686
|
+
const next = new Map<Box, ComputedStyle>();
|
|
1687
|
+
/** The restyled boxes whose ink changed, to repaint. */
|
|
1688
|
+
const inked = new Set<Box>();
|
|
1689
|
+
/** The `::before` and `::after` boxes found, by element: 1 and 2. */
|
|
1690
|
+
const generated = new Map<Element, number>();
|
|
1437
1691
|
const redecorated: [Box, Box['decoration']][] = [];
|
|
1438
|
-
const walk: Box[] =
|
|
1692
|
+
const walk: Box[] = blocks.slice();
|
|
1439
1693
|
while (walk.length) {
|
|
1440
1694
|
const box = walk.pop()!;
|
|
1441
1695
|
for (const child of box.children) walk.push(child);
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1696
|
+
const parent = box.parent;
|
|
1697
|
+
const above = parent ? next.get(parent) : undefined;
|
|
1698
|
+
let style: ComputedStyle | undefined;
|
|
1699
|
+
let ink = !!parent && inked.has(parent);
|
|
1700
|
+
if (box.kind === 'text' || box.kind === 'break') {
|
|
1701
|
+
// Text is set in the style of the box it is in — an element's, a
|
|
1702
|
+
// pseudo-element's — or of its element, where the fix-up put an
|
|
1703
|
+
// anonymous box around it.
|
|
1704
|
+
const el = box.el;
|
|
1705
|
+
if (parent && box.style === parent.style) style = above;
|
|
1706
|
+
else if (el && box.style === tree.styles.get(el)?.style) {
|
|
1707
|
+
style = changed.get(el);
|
|
1708
|
+
ink = !quiet.has(el);
|
|
1709
|
+
} else if (above || (el && changed.has(el))) return false;
|
|
1710
|
+
} else if (box.pseudo === 'before' || box.pseudo === 'after') {
|
|
1711
|
+
const from = GENERATED_FROM.get(box);
|
|
1712
|
+
if (!from) {
|
|
1713
|
+
// an inside marker, made of its item's style and its own rules
|
|
1714
|
+
if (above) return false;
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
if (!reach.has(from)) continue;
|
|
1718
|
+
generated.set(
|
|
1719
|
+
from,
|
|
1720
|
+
(generated.get(from) ?? 0) | (box.pseudo === 'before' ? 1 : 2),
|
|
1721
|
+
);
|
|
1722
|
+
const inherits = fresh.get(from);
|
|
1723
|
+
const kept = tree.styles.get(from)?.style;
|
|
1724
|
+
if (!inherits || !kept) return false;
|
|
1725
|
+
if (inherits === kept && !flipped(from)) continue;
|
|
1726
|
+
// styled by its own rules over its element's style, as the builder
|
|
1727
|
+
// styled it, in its element's box
|
|
1728
|
+
if (!parent || parent.style !== kept || box.marker) return false;
|
|
1729
|
+
const made = cascade.pseudoStyleFor(from, box.pseudo, inherits);
|
|
1730
|
+
if (!made || made.display === 'none') return false;
|
|
1731
|
+
const diff = hoverChange(box.style, made);
|
|
1732
|
+
if (diff === false) return false;
|
|
1733
|
+
if (!diff) continue;
|
|
1734
|
+
if (diff.reach || diff.order || diff.move) return false;
|
|
1735
|
+
style = made;
|
|
1736
|
+
ink = diff.ink;
|
|
1737
|
+
} else if (box.pseudo) {
|
|
1738
|
+
// a first letter, styled from the box its letter is in
|
|
1739
|
+
if (above) return false;
|
|
1740
|
+
} else if (box.el) {
|
|
1741
|
+
const el = box.el;
|
|
1742
|
+
style = changed.get(el);
|
|
1743
|
+
if (!style) continue;
|
|
1744
|
+
const kept = tree.styles.get(el)!.style;
|
|
1745
|
+
// a style derived from the element's, or drawn from it somewhere
|
|
1746
|
+
// else — a widget mounted beside the document takes its look from
|
|
1747
|
+
// it. An image, a drawing or a rule is drawn here, from its box,
|
|
1748
|
+
// like any.
|
|
1749
|
+
if (box.style !== kept) return false;
|
|
1750
|
+
if (box.marker || WIDGETS.has(box.replaced)) return false;
|
|
1751
|
+
if (moving.has(el) && !movable(box, kept, style)) return false;
|
|
1752
|
+
ink = !quiet.has(el);
|
|
1753
|
+
} else if (above) {
|
|
1754
|
+
// An anonymous box takes what its parent's style passes on
|
|
1755
|
+
// (`anonymousStyles`), so it takes that of the new one: the text of
|
|
1756
|
+
// a link made a flex row is in one, and takes the link's colour.
|
|
1757
|
+
if (GENERATED_FROM.has(box)) return false;
|
|
1758
|
+
const display = box.style.display;
|
|
1759
|
+
if (tree.anonymous(parent!.style, display) !== box.style) return false;
|
|
1760
|
+
style = tree.anonymous(above, display);
|
|
1761
|
+
}
|
|
1445
1762
|
if (!style) continue;
|
|
1446
|
-
|
|
1447
|
-
// an anonymous box, a pseudo-element, a marker, a control: a style
|
|
1448
|
-
// derived from the element's, or drawn from it somewhere else
|
|
1449
|
-
if (!box.el || box.style !== kept.style) return false;
|
|
1450
|
-
if (box.marker || box.replaced !== 'none') return false;
|
|
1763
|
+
next.set(box, style);
|
|
1451
1764
|
restyled.push([box, style]);
|
|
1765
|
+
if (ink) inked.add(box);
|
|
1452
1766
|
if (box.kind === 'inline') {
|
|
1453
1767
|
const decoration = inlineDecoration(fonts, box, style);
|
|
1454
1768
|
if ((decoration === null) !== (box.decoration === null)) {
|
|
@@ -1458,12 +1772,28 @@ export class HtmlViewNode extends Node {
|
|
|
1458
1772
|
if (decoration !== box.decoration) redecorated.push([box, decoration]);
|
|
1459
1773
|
}
|
|
1460
1774
|
}
|
|
1775
|
+
// a `::before` or an `::after` a rule that tests the pointer now gives
|
|
1776
|
+
// an element is a box to build
|
|
1777
|
+
if (pseudoRules !== HOVER_PSEUDO_NONE) {
|
|
1778
|
+
for (const el of reach) {
|
|
1779
|
+
if (!flipped(el)) continue;
|
|
1780
|
+
const style = fresh.get(el);
|
|
1781
|
+
if (!style) continue;
|
|
1782
|
+
const has = generated.get(el) ?? 0;
|
|
1783
|
+
for (const [which, bit] of GENERATED_BITS) {
|
|
1784
|
+
if (has & bit) continue;
|
|
1785
|
+
const made = cascade.pseudoStyleFor(el, which, style);
|
|
1786
|
+
if (made && made.display !== 'none') return false;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (!restyled.length) return true;
|
|
1461
1791
|
|
|
1462
|
-
// the text: each layout holding a run of a
|
|
1463
|
-
//
|
|
1792
|
+
// the text: each layout holding a run of a restyled box's, made again
|
|
1793
|
+
// with its new ink, where it comes out the same shape
|
|
1464
1794
|
const relaid = new Map<TextLayoutLike, TextLayoutLike | null>();
|
|
1465
1795
|
const texts: LineText[] = [];
|
|
1466
|
-
walk.push(
|
|
1796
|
+
if (inked.size) for (const block of blocks) walk.push(block);
|
|
1467
1797
|
while (walk.length) {
|
|
1468
1798
|
const box = walk.pop()!;
|
|
1469
1799
|
for (const child of box.children) walk.push(child);
|
|
@@ -1472,24 +1802,150 @@ export class HtmlViewNode extends Node {
|
|
|
1472
1802
|
for (const text of line.texts) {
|
|
1473
1803
|
texts.push(text);
|
|
1474
1804
|
if (relaid.has(text.layout)) continue;
|
|
1475
|
-
const
|
|
1476
|
-
if (
|
|
1477
|
-
relaid.set(text.layout,
|
|
1805
|
+
const again = reinked(text, layouts, next);
|
|
1806
|
+
if (again === false) return false;
|
|
1807
|
+
relaid.set(text.layout, again);
|
|
1478
1808
|
}
|
|
1479
1809
|
}
|
|
1480
1810
|
}
|
|
1481
1811
|
|
|
1812
|
+
// what each box drew before, to be repainted with what it draws after
|
|
1813
|
+
const inks: Rect[] = [];
|
|
1814
|
+
for (const box of inked) {
|
|
1815
|
+
const ink = inkOf(box);
|
|
1816
|
+
if (ink) inks.push(ink);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1482
1819
|
// and only now, all of it
|
|
1483
|
-
|
|
1484
|
-
for (const [box,
|
|
1820
|
+
const moved: [Box, ComputedStyle][] = [];
|
|
1821
|
+
for (const [box, style] of restyled) {
|
|
1822
|
+
const was = box.style;
|
|
1823
|
+
box.style = style;
|
|
1824
|
+
if (box.el && moving.has(box.el) && box.kind !== 'text') {
|
|
1825
|
+
moved.push([box, was]);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
for (const [box, decoration] of redecorated) {
|
|
1829
|
+
// what is inside it keeps which boxes around it paint, and this is
|
|
1830
|
+
// one more or one fewer of them
|
|
1831
|
+
if ((decoration === null) !== (box.decoration === null)) {
|
|
1832
|
+
forgetDecoratedAncestors(box);
|
|
1833
|
+
}
|
|
1834
|
+
box.decoration = decoration;
|
|
1835
|
+
}
|
|
1485
1836
|
for (const text of texts) {
|
|
1486
|
-
const
|
|
1487
|
-
if (
|
|
1837
|
+
const again = relaid.get(text.layout);
|
|
1838
|
+
if (again) text.layout = again;
|
|
1488
1839
|
}
|
|
1489
1840
|
for (const [el, style] of changed) {
|
|
1490
1841
|
tree.styles.set(el, { style, inFlex: tree.styles.get(el)!.inFlex });
|
|
1491
1842
|
}
|
|
1492
|
-
|
|
1843
|
+
// A box that moved moves what is in it, as layout moved it the first
|
|
1844
|
+
// time (`applyRelativeOffsets`), and a later layout moves it the same
|
|
1845
|
+
// way; where it reached the document's end, before or after, the
|
|
1846
|
+
// document's height may have changed with it, which only a layout says.
|
|
1847
|
+
let relayout = false;
|
|
1848
|
+
for (const [box, was] of moved) {
|
|
1849
|
+
const before = box.boundsY + box.boundsHeight;
|
|
1850
|
+
retranslate(box, was);
|
|
1851
|
+
tree.relative = true;
|
|
1852
|
+
if (before >= this._documentHeight) relayout = true;
|
|
1853
|
+
}
|
|
1854
|
+
// how far the boxes' ink reaches, and the order their layers paint in,
|
|
1855
|
+
// as layout leaves them (`layoutDocument`)
|
|
1856
|
+
if (reaches || reorders || moved.length) {
|
|
1857
|
+
computePaintBounds(tree.root, tree.movedInline);
|
|
1858
|
+
if (tree.negative) hoistNegative(tree.root);
|
|
1859
|
+
stackLayers(tree.root);
|
|
1860
|
+
}
|
|
1861
|
+
for (const [box] of moved) {
|
|
1862
|
+
if (box.boundsY + box.boundsHeight >= this._documentHeight) {
|
|
1863
|
+
relayout = true;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
if (moved.length) this._reportControls();
|
|
1867
|
+
if (relayout) {
|
|
1868
|
+
this._invalidate(Stale.Layout);
|
|
1869
|
+
return true;
|
|
1870
|
+
}
|
|
1871
|
+
for (const box of inked) {
|
|
1872
|
+
const ink = inkOf(box);
|
|
1873
|
+
if (ink) inks.push(ink);
|
|
1874
|
+
}
|
|
1875
|
+
this._repaintInk(inks);
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
/** An element's first box in document order, for the tree it was asked
|
|
1880
|
+
* of: made on the first move over a tree, which is one walk of it. */
|
|
1881
|
+
private _firstBoxes: { tree: BoxTree; of: Map<Element, Box> } | null = null;
|
|
1882
|
+
|
|
1883
|
+
/**
|
|
1884
|
+
* The blocks a restyle of `changed` has to look in: for each element, the
|
|
1885
|
+
* box its text is set in the lines of — the nearest box around its first
|
|
1886
|
+
* that is not an inline one — which holds every box that takes its style
|
|
1887
|
+
* from it, the pieces a block in it broke it into among them
|
|
1888
|
+
* (`breakAround`). One that is inside another is left to it.
|
|
1889
|
+
*/
|
|
1890
|
+
private _blocksOf(tree: BoxTree, changed: Iterable<Element>): Box[] {
|
|
1891
|
+
let index = this._firstBoxes;
|
|
1892
|
+
if (index?.tree !== tree) {
|
|
1893
|
+
const of = new Map<Element, Box>();
|
|
1894
|
+
const stack: Box[] = [tree.root];
|
|
1895
|
+
while (stack.length) {
|
|
1896
|
+
const box = stack.pop()!;
|
|
1897
|
+
if (box.el && !of.has(box.el)) of.set(box.el, box);
|
|
1898
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1899
|
+
stack.push(box.children[i]);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
index = this._firstBoxes = { tree, of };
|
|
1903
|
+
}
|
|
1904
|
+
const blocks = new Set<Box>();
|
|
1905
|
+
for (const el of changed) {
|
|
1906
|
+
let box: Box | null = index.of.get(el) ?? null;
|
|
1907
|
+
while (
|
|
1908
|
+
box?.parent &&
|
|
1909
|
+
(box.kind === 'inline' || box.kind === 'text' || box.kind === 'break')
|
|
1910
|
+
) {
|
|
1911
|
+
box = box.parent;
|
|
1912
|
+
}
|
|
1913
|
+
if (box) blocks.add(box);
|
|
1914
|
+
}
|
|
1915
|
+
const out: Box[] = [];
|
|
1916
|
+
for (const block of blocks) {
|
|
1917
|
+
let inside = false;
|
|
1918
|
+
for (let at = block.parent; at && !inside; at = at.parent) {
|
|
1919
|
+
inside = blocks.has(at);
|
|
1920
|
+
}
|
|
1921
|
+
if (!inside) out.push(block);
|
|
1922
|
+
}
|
|
1923
|
+
return out;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* Repaint the document where `inks` are, in its own coordinates: the ink
|
|
1928
|
+
* of the boxes a hover restyled, before and after. Past a handful of rects
|
|
1929
|
+
* the damage is one around them all, which is what core would make of
|
|
1930
|
+
* them anyway.
|
|
1931
|
+
*/
|
|
1932
|
+
private _repaintInk(inks: Rect[]): void {
|
|
1933
|
+
if (!inks.length) return;
|
|
1934
|
+
const x = this.abs.x;
|
|
1935
|
+
const y = this.abs.y;
|
|
1936
|
+
const place = (r: Rect): Rect => ({
|
|
1937
|
+
x: Math.floor(x + r.x),
|
|
1938
|
+
y: Math.floor(y + r.y),
|
|
1939
|
+
width: Math.ceil(r.width) + 1,
|
|
1940
|
+
height: Math.ceil(r.height) + 1,
|
|
1941
|
+
});
|
|
1942
|
+
if (inks.length > 6) {
|
|
1943
|
+
let all = inks[0];
|
|
1944
|
+
for (const r of inks) all = unionRect(all, r);
|
|
1945
|
+
this.invalidate(false, place(all), 'props');
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
for (const r of inks) this.invalidate(false, place(r), 'props');
|
|
1493
1949
|
}
|
|
1494
1950
|
|
|
1495
1951
|
/** The document, for an application that wants to read or change it. */
|
|
@@ -1527,20 +1983,65 @@ export class HtmlViewNode extends Node {
|
|
|
1527
1983
|
// `:hover` is wired at the element rather than through React: the cascade
|
|
1528
1984
|
// already knows whether any rule in the document tests it, `setHover`
|
|
1529
1985
|
// returns without work when none does, and a document that does use it
|
|
1530
|
-
// re-styles only when the hovered chain actually changed
|
|
1531
|
-
//
|
|
1532
|
-
//
|
|
1986
|
+
// re-styles only when the hovered chain actually changed, and then only
|
|
1987
|
+
// the elements the change reached (`_restyleHover`).
|
|
1988
|
+
//
|
|
1989
|
+
// **A hover waits for a scroll to stop.** Core asks again what is under a
|
|
1990
|
+
// pointer that stayed where it was after every frame that laid out —
|
|
1991
|
+
// hover follows content, react-x11#793 — and says so with a move to the
|
|
1992
|
+
// point the pointer is already at. While a document scrolls that is every
|
|
1993
|
+
// frame, each with something else under the pointer: a restyle a frame
|
|
1994
|
+
// at best, and where a hover moves something, the boxes built again a
|
|
1995
|
+
// frame, which took a page scrolled under a parked pointer from 52 frames
|
|
1996
|
+
// a second to 4. A browser holds its hover until the scroll is over, for
|
|
1997
|
+
// the same reason; so does this, for `HOVER_REST_MS` after the content
|
|
1998
|
+
// last moved. What was hovered stays hovered as it scrolls away, as it
|
|
1999
|
+
// does there. A move of the pointer itself is answered at once.
|
|
1533
2000
|
|
|
1534
2001
|
override defaultMouseMove(ev: X11MouseEvent): void {
|
|
1535
2002
|
super.defaultMouseMove?.(ev);
|
|
2003
|
+
const at = this._pointerAt;
|
|
2004
|
+
if (at !== null && at.x === ev.x && at.y === ev.y) {
|
|
2005
|
+
this._holdHover();
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
this._pointerAt = { x: ev.x, y: ev.y };
|
|
1536
2009
|
this.setHover(ev.x, ev.y);
|
|
1537
2010
|
}
|
|
1538
2011
|
|
|
1539
2012
|
override defaultMouseLeave(ev: X11MouseEvent): void {
|
|
1540
2013
|
super.defaultMouseLeave?.(ev);
|
|
2014
|
+
this._pointerAt = null;
|
|
1541
2015
|
this.clearHover();
|
|
1542
2016
|
}
|
|
1543
2017
|
|
|
2018
|
+
/** The content moved under the pointer: the hover is asked for once it
|
|
2019
|
+
* has been still for `HOVER_REST_MS`. One timer a wait, put off by
|
|
2020
|
+
* being armed again when it finds the content moved since. */
|
|
2021
|
+
private _holdHover(): void {
|
|
2022
|
+
if (!this._cascade?.hoverSensitive) return;
|
|
2023
|
+
this._heldSince = hoverClock.now();
|
|
2024
|
+
if (this._heldHover !== null) return;
|
|
2025
|
+
const rest = (): void => {
|
|
2026
|
+
this._heldHover = null;
|
|
2027
|
+
const at = this._pointerAt;
|
|
2028
|
+
if (!at || this.destroyed) return;
|
|
2029
|
+
const left = this._heldSince + HOVER_REST_MS - hoverClock.now();
|
|
2030
|
+
if (left > 0) {
|
|
2031
|
+
this._heldHover = hoverClock.arm(rest, left);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
this.setHover(at.x, at.y);
|
|
2035
|
+
};
|
|
2036
|
+
this._heldHover = hoverClock.arm(rest, HOVER_REST_MS);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
private _dropHeldHover(): void {
|
|
2040
|
+
if (this._heldHover === null) return;
|
|
2041
|
+
hoverClock.disarm(this._heldHover);
|
|
2042
|
+
this._heldHover = null;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
1544
2045
|
// --- paint ----------------------------------------------------------------
|
|
1545
2046
|
|
|
1546
2047
|
override paint(ctx: Context2D): void {
|
|
@@ -1570,6 +2071,15 @@ export class HtmlViewNode extends Node {
|
|
|
1570
2071
|
}
|
|
1571
2072
|
}
|
|
1572
2073
|
|
|
2074
|
+
/** A surface for the painter to draw a masked element on, asked of the
|
|
2075
|
+
* backend until it is found to have none. */
|
|
2076
|
+
private _surface(width: number, height: number): SurfaceLike | null {
|
|
2077
|
+
if (this._noSurface) return null;
|
|
2078
|
+
const surface = newSurface(this.app, width, height);
|
|
2079
|
+
if (!surface) this._noSurface = true;
|
|
2080
|
+
return surface;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1573
2083
|
private _paint(
|
|
1574
2084
|
ctx: Context2D,
|
|
1575
2085
|
tree: BoxTree,
|
|
@@ -1582,6 +2092,7 @@ export class HtmlViewNode extends Node {
|
|
|
1582
2092
|
// the root's background covers the whole element, not only the
|
|
1583
2093
|
// document: an element grown past its content is canvas too
|
|
1584
2094
|
canvas: this.abs,
|
|
2095
|
+
viewport: this._viewport() ?? undefined,
|
|
1585
2096
|
scale: this._scale,
|
|
1586
2097
|
damage,
|
|
1587
2098
|
selection: range
|
|
@@ -1597,6 +2108,7 @@ export class HtmlViewNode extends Node {
|
|
|
1597
2108
|
const size = image ? this._resources.imageSize(url) : null;
|
|
1598
2109
|
return image && size ? { image, ...size } : null;
|
|
1599
2110
|
},
|
|
2111
|
+
surface: (width, height) => this._surface(width, height),
|
|
1600
2112
|
cached: (key, width, height, draw) =>
|
|
1601
2113
|
(this._shadowCache ??= new SurfaceCache(this.app)).get(
|
|
1602
2114
|
key,
|
|
@@ -1610,6 +2122,30 @@ export class HtmlViewNode extends Node {
|
|
|
1610
2122
|
|
|
1611
2123
|
const EMPTY_SET: ReadonlySet<Element> = new Set();
|
|
1612
2124
|
|
|
2125
|
+
/** Whether a tree has a background fixed to the viewport: an image a box
|
|
2126
|
+
* names (`tree.backgrounds`), or the canvas's image or gradient — the
|
|
2127
|
+
* root's, or the body's it paints, which the root box stands in for where
|
|
2128
|
+
* the markup has neither element. */
|
|
2129
|
+
function hasFixedBackground(tree: BoxTree): boolean {
|
|
2130
|
+
for (const box of tree.backgrounds) {
|
|
2131
|
+
if (box.style.backgroundAttachment === 'fixed') return true;
|
|
2132
|
+
}
|
|
2133
|
+
const root = tree.root;
|
|
2134
|
+
const body = root.children.find((child) => child.el?.name === 'body');
|
|
2135
|
+
for (const box of body ? [root, body] : [root]) {
|
|
2136
|
+
const style = box.style;
|
|
2137
|
+
if (
|
|
2138
|
+
style.backgroundAttachment === 'fixed' &&
|
|
2139
|
+
(style.backgroundImage ||
|
|
2140
|
+
style.backgroundImages ||
|
|
2141
|
+
style.backgroundGradient)
|
|
2142
|
+
) {
|
|
2143
|
+
return true;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
return false;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
1613
2149
|
/** A sheet as the restyle reads it: its text, the encoding it was decoded
|
|
1614
2150
|
* from, the element that brought it, and what its URLs resolve against. */
|
|
1615
2151
|
interface SheetText {
|
|
@@ -1656,6 +2192,94 @@ function boxFor(root: Box, element: Element): Box | null {
|
|
|
1656
2192
|
return null;
|
|
1657
2193
|
}
|
|
1658
2194
|
|
|
2195
|
+
/**
|
|
2196
|
+
* An inline element's boxes: the one it has, or where a block in it broke
|
|
2197
|
+
* it (`breakAround`), each piece, in document order.
|
|
2198
|
+
*/
|
|
2199
|
+
function piecesOf(root: Box, first: Box): Box[] {
|
|
2200
|
+
// the first piece is cut at its end where there are more
|
|
2201
|
+
if (first.kind !== 'inline' || !(first.cut & 2)) return [first];
|
|
2202
|
+
const pieces: Box[] = [];
|
|
2203
|
+
const stack: Box[] = [root];
|
|
2204
|
+
while (stack.length) {
|
|
2205
|
+
const box = stack.pop()!;
|
|
2206
|
+
if (box.el === first.el && box.kind === 'inline') pieces.push(box);
|
|
2207
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
2208
|
+
stack.push(box.children[i]);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
return pieces;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* The bands of one inline box's fragments, or a text's: on the lines of
|
|
2216
|
+
* the block it is laid out in, and no text of a float or a positioned box
|
|
2217
|
+
* inside it — on lines of their own — is one of them (CSSOM View 6.1): a
|
|
2218
|
+
* list item made inline around an absolute link measured as the link.
|
|
2219
|
+
* Each across its text, the inline-blocks and inline boxes' edges it holds
|
|
2220
|
+
* and its own padding and border where it starts or ends there, and as
|
|
2221
|
+
* tall as its border box.
|
|
2222
|
+
*/
|
|
2223
|
+
function inlineBands(
|
|
2224
|
+
root: Box,
|
|
2225
|
+
box: Box,
|
|
2226
|
+
fonts: FontsLike | null,
|
|
2227
|
+
out: Rect[],
|
|
2228
|
+
): void {
|
|
2229
|
+
// its own, apart from any other piece's: `fragmentHeights` makes each of
|
|
2230
|
+
// the bands it is handed as tall as this box on that band's line
|
|
2231
|
+
const bands: Rect[] = [];
|
|
2232
|
+
let block: Box | null = box.parent;
|
|
2233
|
+
while (block && !block.lines) block = block.parent;
|
|
2234
|
+
if (box.subtreeTextEnd > box.subtreeTextStart) {
|
|
2235
|
+
if (box.kind === 'inline' && block?.lines) {
|
|
2236
|
+
lineBands(
|
|
2237
|
+
block.lines,
|
|
2238
|
+
box.subtreeTextStart,
|
|
2239
|
+
box.subtreeTextEnd,
|
|
2240
|
+
0,
|
|
2241
|
+
0,
|
|
2242
|
+
bands,
|
|
2243
|
+
true,
|
|
2244
|
+
);
|
|
2245
|
+
} else {
|
|
2246
|
+
collectBands(root, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
if (box.kind === 'inline') {
|
|
2250
|
+
fragmentReach(box, bands);
|
|
2251
|
+
if (fonts) fragmentHeights(box, bands, fonts);
|
|
2252
|
+
// where `position: relative` on it or an inline box around it moved
|
|
2253
|
+
// it, all of it (CSS 2.1 9.4.3), as `getBoundingClientRect` has it
|
|
2254
|
+
// (CSSOM View 6.1): the bands are where the lines put it, and a link
|
|
2255
|
+
// moved 120px down measured on its line, as wide as from where it was
|
|
2256
|
+
// laid out to where it was drawn
|
|
2257
|
+
const { x, y } = relativeOffsetOf(box);
|
|
2258
|
+
if (x || y) {
|
|
2259
|
+
for (const band of bands) {
|
|
2260
|
+
band.x += x;
|
|
2261
|
+
band.y += y;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
out.push(...bands);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
/** How far an inline box and the inline boxes around it moved it by their
|
|
2269
|
+
* relative offsets (`INLINE_OFFSETS`). */
|
|
2270
|
+
function relativeOffsetOf(box: Box): { x: number; y: number } {
|
|
2271
|
+
let x = 0;
|
|
2272
|
+
let y = 0;
|
|
2273
|
+
for (let at: Box | null = box; at?.kind === 'inline'; at = at.parent) {
|
|
2274
|
+
const offset = INLINE_OFFSETS.get(at);
|
|
2275
|
+
if (offset) {
|
|
2276
|
+
x += offset.x;
|
|
2277
|
+
y += offset.y;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
return { x, y };
|
|
2281
|
+
}
|
|
2282
|
+
|
|
1659
2283
|
/** Where the document's text goes on after a box with none: the start of
|
|
1660
2284
|
* the next text in document order, or the end of the last before it. */
|
|
1661
2285
|
function textAfter(root: Box, target: Box): number {
|
|
@@ -1715,6 +2339,9 @@ function sameRects(a: ControlRect[], b: ControlRect[]): boolean {
|
|
|
1715
2339
|
p.y !== q.y ||
|
|
1716
2340
|
p.width !== q.width ||
|
|
1717
2341
|
p.height !== q.height ||
|
|
2342
|
+
p.fontFamily !== q.fontFamily ||
|
|
2343
|
+
p.fontSize !== q.fontSize ||
|
|
2344
|
+
p.opacity !== q.opacity ||
|
|
1718
2345
|
!sameBare(p.bare, q.bare)
|
|
1719
2346
|
) {
|
|
1720
2347
|
return false;
|
|
@@ -1732,7 +2359,8 @@ function sameBare(a?: BareField, b?: BareField): boolean {
|
|
|
1732
2359
|
a.height === b.height &&
|
|
1733
2360
|
a.color === b.color &&
|
|
1734
2361
|
a.fontFamily === b.fontFamily &&
|
|
1735
|
-
a.fontSize === b.fontSize
|
|
2362
|
+
a.fontSize === b.fontSize &&
|
|
2363
|
+
a.chevron === b.chevron
|
|
1736
2364
|
);
|
|
1737
2365
|
}
|
|
1738
2366
|
|
|
@@ -2004,6 +2632,12 @@ function lineBands(
|
|
|
2004
2632
|
dx: number,
|
|
2005
2633
|
dy: number,
|
|
2006
2634
|
out: Rect[],
|
|
2635
|
+
/** Where the lines put the text, not where a relative offset moved it
|
|
2636
|
+
* (`TEXT_SHIFTS`): what an inline box's own offset is added to. */
|
|
2637
|
+
laidOut = false,
|
|
2638
|
+
/** The rows a highlight covers (`selectionRows`) rather than the line's:
|
|
2639
|
+
* past it where the glyphs are taller than the line. */
|
|
2640
|
+
ink = false,
|
|
2007
2641
|
): void {
|
|
2008
2642
|
// First line whose text can reach `from`, by binary search over the
|
|
2009
2643
|
// sorted text starts; stop at the first line past `to`.
|
|
@@ -2025,6 +2659,16 @@ function lineBands(
|
|
|
2025
2659
|
const b = Math.min(to, text.textEnd);
|
|
2026
2660
|
if (b <= a) continue;
|
|
2027
2661
|
const offsets = layoutOffsetsOf(text.layout);
|
|
2662
|
+
const moved = laidOut ? (TEXT_SHIFTS.get(text)?.x ?? 0) : 0;
|
|
2663
|
+
// The spaces a line ends on are removed (CSS Text 3, 4.1.2), and
|
|
2664
|
+
// take no room on it: the engine sets them past its line's content,
|
|
2665
|
+
// where a caret after them goes, and a band that took them in made
|
|
2666
|
+
// a link that ends a line a space wider than Chrome's. So the text
|
|
2667
|
+
// a line ends with is measured no further than its content.
|
|
2668
|
+
const ends = endsLine(line, text, natural);
|
|
2669
|
+
const rows = ink
|
|
2670
|
+
? selectionRows(line, text, natural)
|
|
2671
|
+
: { y: line.y, height: line.height };
|
|
2028
2672
|
for (const band of bandsFor(
|
|
2029
2673
|
text.layout,
|
|
2030
2674
|
natural,
|
|
@@ -2032,11 +2676,29 @@ function lineBands(
|
|
|
2032
2676
|
layoutOffsetOf(text, a),
|
|
2033
2677
|
layoutOffsetOf(text, b, true),
|
|
2034
2678
|
)) {
|
|
2679
|
+
let left = band.x;
|
|
2680
|
+
let right = band.x + band.width;
|
|
2681
|
+
if (ends) {
|
|
2682
|
+
left = Math.max(left, natural.x);
|
|
2683
|
+
right = Math.max(left, Math.min(right, natural.x + natural.width));
|
|
2684
|
+
}
|
|
2685
|
+
out.push({
|
|
2686
|
+
x: dx + left + text.drawX - moved,
|
|
2687
|
+
y: dy + rows.y,
|
|
2688
|
+
width: right - left,
|
|
2689
|
+
height: rows.height,
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
// the spaces `pre-wrap` keeps that the line ends on, which have no
|
|
2693
|
+
// run: the engine hung them past the line (`LineText.hung`)
|
|
2694
|
+
for (const space of text.hung ?? []) {
|
|
2695
|
+
const at = documentOffsetOf(text, space.at);
|
|
2696
|
+
if (at < a || at >= b) continue;
|
|
2035
2697
|
out.push({
|
|
2036
|
-
x: dx +
|
|
2037
|
-
y: dy +
|
|
2038
|
-
width:
|
|
2039
|
-
height:
|
|
2698
|
+
x: dx + space.x + text.drawX - moved,
|
|
2699
|
+
y: dy + rows.y,
|
|
2700
|
+
width: space.width,
|
|
2701
|
+
height: rows.height,
|
|
2040
2702
|
});
|
|
2041
2703
|
}
|
|
2042
2704
|
}
|
|
@@ -2044,6 +2706,21 @@ function lineBands(
|
|
|
2044
2706
|
}
|
|
2045
2707
|
}
|
|
2046
2708
|
|
|
2709
|
+
/** Whether a line's text is where the line ends: its last, with nothing
|
|
2710
|
+
* placed after it — an inline-block or an image past its content. */
|
|
2711
|
+
function endsLine(
|
|
2712
|
+
line: LineBox,
|
|
2713
|
+
text: LineText,
|
|
2714
|
+
natural: { x: number; width: number },
|
|
2715
|
+
): boolean {
|
|
2716
|
+
if (line.texts[line.texts.length - 1] !== text) return false;
|
|
2717
|
+
const right = text.drawX + natural.x + natural.width;
|
|
2718
|
+
for (const placed of line.atomics) {
|
|
2719
|
+
if (placed.x >= right - 0.01) return false;
|
|
2720
|
+
}
|
|
2721
|
+
return true;
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2047
2724
|
function collectBands(
|
|
2048
2725
|
box: Box,
|
|
2049
2726
|
from: number,
|
|
@@ -2051,14 +2728,15 @@ function collectBands(
|
|
|
2051
2728
|
dx: number,
|
|
2052
2729
|
dy: number,
|
|
2053
2730
|
out: Rect[],
|
|
2731
|
+
ink = false,
|
|
2054
2732
|
): void {
|
|
2055
2733
|
if (box.subtreeTextEnd <= box.subtreeTextStart) return;
|
|
2056
2734
|
if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to) return;
|
|
2057
|
-
if (box.lines) lineBands(box.lines, from, to, dx, dy, out);
|
|
2735
|
+
if (box.lines) lineBands(box.lines, from, to, dx, dy, out, false, ink);
|
|
2058
2736
|
// Atomics are ordinary children, reached below.
|
|
2059
2737
|
for (const child of box.children) {
|
|
2060
2738
|
if (child.kind === 'text' || child.kind === 'break') continue;
|
|
2061
|
-
collectBands(child, from, to, dx, dy, out);
|
|
2739
|
+
collectBands(child, from, to, dx, dy, out, ink);
|
|
2062
2740
|
}
|
|
2063
2741
|
}
|
|
2064
2742
|
|
|
@@ -2078,6 +2756,10 @@ function deepestAt(
|
|
|
2078
2756
|
/** Told whether what was found was found under text: a run's, rather
|
|
2079
2757
|
* than a box's. */
|
|
2080
2758
|
hit?: { text: boolean },
|
|
2759
|
+
/** How far a box fixed to the viewport is drawn from where it was laid
|
|
2760
|
+
* out (`HtmlViewNode._fixedShift`): the point is taken back by it in
|
|
2761
|
+
* there, as paint moved the box by it (`atViewport`). */
|
|
2762
|
+
fixedShift: { x: number; y: number } | null = null,
|
|
2081
2763
|
): Element | null {
|
|
2082
2764
|
const box = tree.root;
|
|
2083
2765
|
let found: Element | null = box.el;
|
|
@@ -2118,10 +2800,31 @@ function deepestAt(
|
|
|
2118
2800
|
// Zen Garden's archive links are absolute `<li>`s in an `overflow:
|
|
2119
2801
|
// hidden` list with no height of its own, and not one of them could be
|
|
2120
2802
|
// hovered or pressed.
|
|
2803
|
+
let atViewport = false;
|
|
2121
2804
|
const enter = (
|
|
2122
2805
|
child: Box,
|
|
2123
2806
|
context: readonly number[],
|
|
2124
2807
|
clipped: readonly Box[],
|
|
2808
|
+
): void => {
|
|
2809
|
+
if (!fixedShift || atViewport || !fixedToViewport(child)) {
|
|
2810
|
+
enterAt(child, context, clipped);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
x -= fixedShift.x;
|
|
2814
|
+
y -= fixedShift.y;
|
|
2815
|
+
atViewport = true;
|
|
2816
|
+
try {
|
|
2817
|
+
enterAt(child, context, clipped);
|
|
2818
|
+
} finally {
|
|
2819
|
+
x += fixedShift.x;
|
|
2820
|
+
y += fixedShift.y;
|
|
2821
|
+
atViewport = false;
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
const enterAt = (
|
|
2825
|
+
child: Box,
|
|
2826
|
+
context: readonly number[],
|
|
2827
|
+
clipped: readonly Box[],
|
|
2125
2828
|
): void => {
|
|
2126
2829
|
const style = child.style;
|
|
2127
2830
|
if (
|
|
@@ -2240,6 +2943,38 @@ function compareKeys(a: readonly number[], b: readonly number[]): number {
|
|
|
2240
2943
|
* that matches a container reaches everything in it. */
|
|
2241
2944
|
const HOVER_RESTYLE_LIMIT = 300;
|
|
2242
2945
|
|
|
2946
|
+
/** How long the content under a still pointer has to have stopped moving
|
|
2947
|
+
* before the hover is asked for again (`HtmlViewNode._holdHover`): a
|
|
2948
|
+
* tenth of a second, which is what WebKit waits after a scroll before it
|
|
2949
|
+
* sends the mouse move that updates its own. */
|
|
2950
|
+
const HOVER_REST_MS = 100;
|
|
2951
|
+
|
|
2952
|
+
const timers = globalThis as {
|
|
2953
|
+
setTimeout?(fn: () => void, ms: number): unknown;
|
|
2954
|
+
clearTimeout?(id: unknown): void;
|
|
2955
|
+
};
|
|
2956
|
+
|
|
2957
|
+
/**
|
|
2958
|
+
* The clock a held hover waits on. Through `globalThis` because `src/`
|
|
2959
|
+
* compiles with `types: []`, and unref'd where the runtime allows it: a
|
|
2960
|
+
* timer of a document's must not keep a process alive that is otherwise
|
|
2961
|
+
* done. One object, exported from this module though not from the package,
|
|
2962
|
+
* so a test can hold it and say when the rest is over (`test/held-clock.ts`).
|
|
2963
|
+
*/
|
|
2964
|
+
export const hoverClock = {
|
|
2965
|
+
now(): number {
|
|
2966
|
+
return Date.now();
|
|
2967
|
+
},
|
|
2968
|
+
arm(step: () => void, ms: number): unknown {
|
|
2969
|
+
const handle = timers.setTimeout?.(step, ms) ?? null;
|
|
2970
|
+
(handle as { unref?(): void } | null)?.unref?.();
|
|
2971
|
+
return handle;
|
|
2972
|
+
},
|
|
2973
|
+
disarm(handle: unknown): void {
|
|
2974
|
+
timers.clearTimeout?.(handle);
|
|
2975
|
+
},
|
|
2976
|
+
};
|
|
2977
|
+
|
|
2243
2978
|
/**
|
|
2244
2979
|
* The computed properties a pointer move may change in place: ink, which
|
|
2245
2980
|
* moves nothing. A background colour is drawn inside the box it colours
|
|
@@ -2292,16 +3027,135 @@ const FACE_FIELDS = [
|
|
|
2292
3027
|
/** Whether two styles differ in ink alone: true where they do, null where
|
|
2293
3028
|
* they do not differ, false where something else does. Custom properties
|
|
2294
3029
|
* are read through the properties that use them. */
|
|
2295
|
-
|
|
3030
|
+
/** What a restyle in place changed beyond ink: how far a box's ink
|
|
3031
|
+
* reaches, the order its layer paints in, or where it is. */
|
|
3032
|
+
interface HoverChange {
|
|
3033
|
+
/** Whether anything that is drawn changed: false where only the custom
|
|
3034
|
+
* properties did, which the elements under this one read. */
|
|
3035
|
+
ink: boolean;
|
|
3036
|
+
reach: boolean;
|
|
3037
|
+
order: boolean;
|
|
3038
|
+
move: boolean;
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
/** An element's `::before` and `::after`, each with its bit in the set
|
|
3042
|
+
* of the ones a restyle found boxes of (`_hoverInPlace`). */
|
|
3043
|
+
const GENERATED_BITS = [
|
|
3044
|
+
['before', 1],
|
|
3045
|
+
['after', 2],
|
|
3046
|
+
] as const;
|
|
3047
|
+
|
|
3048
|
+
/** The replaced boxes that are controls (`BoxTree.controls`, `controls.ts`):
|
|
3049
|
+
* a widget mounted beside the document, or a button whose press it reports. */
|
|
3050
|
+
const WIDGETS = new Set<ReplacedKind>([
|
|
3051
|
+
'input',
|
|
3052
|
+
'textarea',
|
|
3053
|
+
'select',
|
|
3054
|
+
'button',
|
|
3055
|
+
'checkbox',
|
|
3056
|
+
'radio',
|
|
3057
|
+
]);
|
|
3058
|
+
|
|
3059
|
+
/** What only moves how far a box's ink reaches: a shadow, and an outline's
|
|
3060
|
+
* size, which take no room (CSS Backgrounds 3, 7.1; CSS UI 4, 3). */
|
|
3061
|
+
const INK_REACH = new Set([
|
|
3062
|
+
'boxShadow',
|
|
3063
|
+
'outlineStyle',
|
|
3064
|
+
'outlineWidth',
|
|
3065
|
+
'outlineOffset',
|
|
3066
|
+
]);
|
|
3067
|
+
|
|
3068
|
+
/**
|
|
3069
|
+
* How two styles of one element differ, for a restyle in place: null where
|
|
3070
|
+
* they do not, a `HoverChange` where every difference is ink, how far ink
|
|
3071
|
+
* reaches, a `z-index` that stays a stacking context's, or a translation —
|
|
3072
|
+
* none of which moves anything but the box and what it holds (CSS
|
|
3073
|
+
* Transforms 1: a transform does not affect layout) — and false where
|
|
3074
|
+
* anything else differs.
|
|
3075
|
+
*/
|
|
3076
|
+
function hoverChange(
|
|
3077
|
+
was: ComputedStyle,
|
|
3078
|
+
now: ComputedStyle,
|
|
3079
|
+
): HoverChange | null | false {
|
|
2296
3080
|
const a = was as unknown as Record<string, unknown>;
|
|
2297
3081
|
const b = now as unknown as Record<string, unknown>;
|
|
2298
|
-
let
|
|
3082
|
+
let change: HoverChange | null = null;
|
|
2299
3083
|
for (const key in b) {
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
3084
|
+
// the sets are shared by what made them (`Cascade._customFor`): one
|
|
3085
|
+
// object where they are the same
|
|
3086
|
+
if (key === 'custom') {
|
|
3087
|
+
if (a[key] !== b[key]) {
|
|
3088
|
+
change ??= { ink: false, reach: false, order: false, move: false };
|
|
3089
|
+
}
|
|
3090
|
+
continue;
|
|
3091
|
+
}
|
|
3092
|
+
if (sameValue(a[key], b[key])) continue;
|
|
3093
|
+
change ??= { ink: true, reach: false, order: false, move: false };
|
|
3094
|
+
change.ink = true;
|
|
3095
|
+
if (PAINT_ONLY.has(key)) continue;
|
|
3096
|
+
if (INK_REACH.has(key)) {
|
|
3097
|
+
change.reach = true;
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
if (key === 'zIndex') {
|
|
3101
|
+
// one stacking context either way, with nothing hoisted below the
|
|
3102
|
+
// flow (`hoistNegative`): only its place among its layers changes
|
|
3103
|
+
const z0 = a[key];
|
|
3104
|
+
const z1 = b[key];
|
|
3105
|
+
if (typeof z0 !== 'number' || typeof z1 !== 'number') return false;
|
|
3106
|
+
if (z0 < 0 || z1 < 0) return false;
|
|
3107
|
+
change.order = true;
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
if (key === 'transformTranslate' || key === 'translate') {
|
|
3111
|
+
change.move = true;
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3114
|
+
return false;
|
|
2303
3115
|
}
|
|
2304
|
-
return
|
|
3116
|
+
return change;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
/**
|
|
3120
|
+
* Whether a box can take a new translation where it is: a box of its own
|
|
3121
|
+
* rather than an inline one, whose text is what moves; and the same
|
|
3122
|
+
* containing block and stacking context for what is in it, which a
|
|
3123
|
+
* transform makes a box (CSS Transforms 1, 2) — so either transformed
|
|
3124
|
+
* before and after, or already positioned with a `z-index`, and holding
|
|
3125
|
+
* nothing fixed, which only a transform takes in.
|
|
3126
|
+
*/
|
|
3127
|
+
function movable(box: Box, was: ComputedStyle, now: ComputedStyle): boolean {
|
|
3128
|
+
if (box.kind === 'inline') return false;
|
|
3129
|
+
if (transformed(was) === transformed(now)) return true;
|
|
3130
|
+
if (was.position === 'static') return false;
|
|
3131
|
+
if (typeof was.zIndex !== 'number' || typeof now.zIndex !== 'number') {
|
|
3132
|
+
return false;
|
|
3133
|
+
}
|
|
3134
|
+
const stack: Box[] = [...box.children];
|
|
3135
|
+
while (stack.length) {
|
|
3136
|
+
const at = stack.pop()!;
|
|
3137
|
+
if (at.style.position === 'fixed') return false;
|
|
3138
|
+
for (const child of at.children) stack.push(child);
|
|
3139
|
+
}
|
|
3140
|
+
return true;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
/** Where a box draws, in document coordinates: its ink bounds, or for an
|
|
3144
|
+
* inline box or a run of text, which are drawn on their block's lines and
|
|
3145
|
+
* have none of their own, the block's. */
|
|
3146
|
+
function inkOf(box: Box): Rect | null {
|
|
3147
|
+
let at: Box | null = box;
|
|
3148
|
+
while (at && (at.kind === 'text' || at.kind === 'break' || !hasRect(at))) {
|
|
3149
|
+
at = at.parent;
|
|
3150
|
+
}
|
|
3151
|
+
if (!at || at.boundsY === Infinity) return null;
|
|
3152
|
+
if (!(at.boundsWidth > 0 && at.boundsHeight > 0)) return null;
|
|
3153
|
+
return {
|
|
3154
|
+
x: at.boundsX,
|
|
3155
|
+
y: at.boundsY,
|
|
3156
|
+
width: at.boundsWidth,
|
|
3157
|
+
height: at.boundsHeight,
|
|
3158
|
+
};
|
|
2305
3159
|
}
|
|
2306
3160
|
|
|
2307
3161
|
/** Structural equality for a computed value: a number, a string, or the
|
|
@@ -2321,16 +3175,9 @@ function sameValue(x: unknown, y: unknown): boolean {
|
|
|
2321
3175
|
return true;
|
|
2322
3176
|
}
|
|
2323
3177
|
|
|
2324
|
-
/** The element an anonymous box takes its style from: its nearest
|
|
2325
|
-
* ancestor's that has one. */
|
|
2326
|
-
function nearestElement(box: Box): Element | null {
|
|
2327
|
-
for (let at = box.parent; at; at = at.parent) if (at.el) return at.el;
|
|
2328
|
-
return null;
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
3178
|
/**
|
|
2332
|
-
* `text`'s layout made again with the ink of the
|
|
2333
|
-
* restyled: from the runs it was made from, each of a
|
|
3179
|
+
* `text`'s layout made again with the ink of the boxes a pointer move
|
|
3180
|
+
* restyled: from the runs it was made from, each of a restyled text box's
|
|
2334
3181
|
* given the ink its new style makes (`runFor`) and nothing else. Null where
|
|
2335
3182
|
* none of its runs is theirs; false where that cannot be told — a run this
|
|
2336
3183
|
* cannot place in the document, one some other pass inked (a first line's),
|
|
@@ -2339,19 +3186,16 @@ function nearestElement(box: Box): Element | null {
|
|
|
2339
3186
|
function reinked(
|
|
2340
3187
|
text: LineText,
|
|
2341
3188
|
layouts: TextLayoutCache,
|
|
2342
|
-
|
|
2343
|
-
styles: BoxTree['styles'],
|
|
3189
|
+
next: ReadonlyMap<Box, ComputedStyle>,
|
|
2344
3190
|
): TextLayoutLike | null | false {
|
|
2345
3191
|
const spans = text.spans;
|
|
2346
|
-
const ownerAt = (offset: number): Element | null =>
|
|
2347
|
-
spans.boxAt ? (spans.boxAt(offset)?.el ?? null) : null;
|
|
2348
3192
|
const inputs = layouts.inputsOf(text.layout);
|
|
2349
3193
|
if (!inputs || !spans.boxAt) {
|
|
2350
3194
|
// not one this could make again: none of its text may be theirs
|
|
2351
3195
|
for (const line of text.layout.lines) {
|
|
2352
3196
|
for (const run of line.runs) {
|
|
2353
|
-
const
|
|
2354
|
-
if (!spans.boxAt || (
|
|
3197
|
+
const box = spans.boxAt?.(run.start);
|
|
3198
|
+
if (!spans.boxAt || (box && next.has(box))) return false;
|
|
2355
3199
|
}
|
|
2356
3200
|
}
|
|
2357
3201
|
return null;
|
|
@@ -2362,25 +3206,25 @@ function reinked(
|
|
|
2362
3206
|
for (let i = 0; i < content.length; i += 1) {
|
|
2363
3207
|
const run = content[i];
|
|
2364
3208
|
const length = run.text.length;
|
|
2365
|
-
const
|
|
2366
|
-
const style =
|
|
2367
|
-
if (
|
|
2368
|
-
if (
|
|
2369
|
-
const was = runFor(run.text,
|
|
3209
|
+
const box = length ? spans.boxAt(offset) : null;
|
|
3210
|
+
const style = box ? next.get(box) : undefined;
|
|
3211
|
+
if (box && style) {
|
|
3212
|
+
if (spans.boxAt(offset + length - 1) !== box) return false;
|
|
3213
|
+
const was = runFor(run.text, box.style);
|
|
2370
3214
|
const now = runFor(run.text, style);
|
|
2371
3215
|
for (const f of FACE_FIELDS) if (!sameValue(was[f], now[f])) return false;
|
|
2372
|
-
let
|
|
3216
|
+
let inked: Record<string, unknown> | null = null;
|
|
2373
3217
|
for (const f of INK_FIELDS) {
|
|
2374
3218
|
// a run that is not what its style made — a first line's colour
|
|
2375
3219
|
if (!sameValue(run[f], was[f])) return false;
|
|
2376
3220
|
if (sameValue(was[f], now[f])) continue;
|
|
2377
|
-
|
|
2378
|
-
if (now[f] === undefined) delete
|
|
2379
|
-
else
|
|
3221
|
+
inked ??= { ...run };
|
|
3222
|
+
if (now[f] === undefined) delete inked[f];
|
|
3223
|
+
else inked[f] = now[f];
|
|
2380
3224
|
}
|
|
2381
|
-
if (
|
|
3225
|
+
if (inked) {
|
|
2382
3226
|
runs ??= content.slice();
|
|
2383
|
-
runs[i] =
|
|
3227
|
+
runs[i] = inked as unknown as TextRun;
|
|
2384
3228
|
}
|
|
2385
3229
|
}
|
|
2386
3230
|
offset += length;
|