@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/dist/html/node.js
CHANGED
|
@@ -40,20 +40,20 @@ import { registerElement, registeredElements } from 'react-x11/host';
|
|
|
40
40
|
import { Node } from 'react-x11/node';
|
|
41
41
|
import { codePointAtOffset, codeUnitOffsets } from '../internal/text.js';
|
|
42
42
|
import { attr, HtmlSource, imageUrlOf, isElement, tagOf } from './dom.js';
|
|
43
|
-
import { Cascade } from './css/cascade.js';
|
|
43
|
+
import { Cascade, HOVER_FOLLOWED, HOVER_PSEUDO_NONE, HOVER_PSEUDO_OTHER, HOVER_UNTOUCHED, } from './css/cascade.js';
|
|
44
44
|
import { mediaMatches, parseStylesheet } from './css/parse.js';
|
|
45
45
|
import { uaStylesheet } from './css/ua.js';
|
|
46
|
-
import { BOX_RAISES, buildBoxes, CONTENT_IMAGES, LINE_BOX_RAISES, SHIFTED_LINES, } from './layout/boxes.js';
|
|
47
|
-
import { layoutDocument } from './layout/block.js';
|
|
46
|
+
import { BOX_RAISES, buildBoxes, CONTENT_IMAGES, CLEARED_FROM, CUT_BLOCKS, GENERATED_FROM, INLINE_OFFSETS, LINE_BOX_RAISES, SHIFTED_LINES, TEXT_SHIFTS, } from './layout/boxes.js';
|
|
47
|
+
import { layoutDocument, retranslate, transformed } from './layout/block.js';
|
|
48
48
|
import { TextLayoutCache } from './layout/cache.js';
|
|
49
49
|
import { shapingSafe } from './layout/shaping.js';
|
|
50
|
-
import { SurfaceCache } from './surfaces.js';
|
|
50
|
+
import { SurfaceCache, newSurface } from './surfaces.js';
|
|
51
51
|
import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
|
|
52
52
|
// Through the inline module rather than a second cache: the offsets table for
|
|
53
53
|
// a layout is built once, on the first selection that needs it.
|
|
54
54
|
import { documentOffsetOf, layoutOffsetOf, layoutOffsets as layoutOffsetsOf, } from './layout/inline.js';
|
|
55
55
|
import { lineBands as bandsFor } from '../richtext/runs.js';
|
|
56
|
-
import { clipsOverflow, containingBlockOf, hasRect, holds, holdsAbsolute, paintDocument, queryChildIndex, } from './paint.js';
|
|
56
|
+
import { clipsOverflow, computePaintBounds, containingBlockOf, FIXED_BOXES, fixedToViewport, forgetDecoratedAncestors, hasRect, hoistNegative, holds, holdsAbsolute, paintDocument, queryChildIndex, selectionRows, stackLayers, } from './paint.js';
|
|
57
57
|
import { controlRectsOf, measureControl } from './controls.js';
|
|
58
58
|
import { ResourceStore } from './resources.js';
|
|
59
59
|
import { WebFonts } from './fonts.js';
|
|
@@ -118,6 +118,8 @@ export class HtmlViewNode extends Node {
|
|
|
118
118
|
_sheetsRead = null;
|
|
119
119
|
/** Blurred shadows, drawn once each. */
|
|
120
120
|
_shadowCache = null;
|
|
121
|
+
/** Whether the backend was found to have no offscreen surface. */
|
|
122
|
+
_noSurface = false;
|
|
121
123
|
_cascade = null;
|
|
122
124
|
_tree = null;
|
|
123
125
|
/** The faces `_warmFaces` has asked the font matcher for, and of which
|
|
@@ -133,6 +135,8 @@ export class HtmlViewNode extends Node {
|
|
|
133
135
|
* that read it (`vw`, `vh`). */
|
|
134
136
|
_styledWidth = -1;
|
|
135
137
|
_styledHeight = -1;
|
|
138
|
+
/** The cascade the box tree's styles were computed by. */
|
|
139
|
+
_styledWith = null;
|
|
136
140
|
_documentHeight = 0;
|
|
137
141
|
_documentWidth = 0;
|
|
138
142
|
/** The viewport height the document was last laid out under, and whether
|
|
@@ -150,6 +154,20 @@ export class HtmlViewNode extends Node {
|
|
|
150
154
|
* the text carries surrogate pairs. null until checked. */
|
|
151
155
|
_pointsAreUnits = null;
|
|
152
156
|
_hovered = [];
|
|
157
|
+
/** Where the pointer last moved to, in logical window pixels, while it
|
|
158
|
+
* is over the element (`defaultMouseMove`). */
|
|
159
|
+
_pointerAt = null;
|
|
160
|
+
/** A hover held until the content under a still pointer stops moving:
|
|
161
|
+
* the timer, and when the content last moved (`_holdHover`). */
|
|
162
|
+
_heldHover = null;
|
|
163
|
+
_heldSince = 0;
|
|
164
|
+
/**
|
|
165
|
+
* The elements the build of the boxes that is due has to style again,
|
|
166
|
+
* where every change asking for it said which (`_invalidate`), and null
|
|
167
|
+
* where any element's style may have changed — a new cascade, a viewport
|
|
168
|
+
* a `vw` reads, a face that arrived.
|
|
169
|
+
*/
|
|
170
|
+
_restyleOnly = null;
|
|
153
171
|
_scriptsSeen = new WeakSet();
|
|
154
172
|
_controls = [];
|
|
155
173
|
_reportedDomRevision = -1;
|
|
@@ -228,6 +246,7 @@ export class HtmlViewNode extends Node {
|
|
|
228
246
|
controlPadY: look.controlPadY * s,
|
|
229
247
|
controlBorder: look.controlBorder * s,
|
|
230
248
|
controlRadius: look.controlRadius * s,
|
|
249
|
+
controlFontSize: (look.controlFontSize ?? look.fontSize) * s,
|
|
231
250
|
};
|
|
232
251
|
this._deviceLookFor = look;
|
|
233
252
|
this._deviceLookAt = s;
|
|
@@ -235,7 +254,26 @@ export class HtmlViewNode extends Node {
|
|
|
235
254
|
return scaled;
|
|
236
255
|
}
|
|
237
256
|
// --- the pipeline ---------------------------------------------------------
|
|
238
|
-
|
|
257
|
+
/**
|
|
258
|
+
* `restyle` with `Stale.Boxes`: the elements whose styles the change
|
|
259
|
+
* could have touched, where that is all of it — a pointer move's reach
|
|
260
|
+
* (`_hoverReach`). The build it asks for keeps every other element's
|
|
261
|
+
* (`_restyleOnly`).
|
|
262
|
+
*/
|
|
263
|
+
_invalidate(stale, restyle = null) {
|
|
264
|
+
if (stale >= Stale.Boxes) {
|
|
265
|
+
const only = stale === Stale.Boxes ? restyle : null;
|
|
266
|
+
if (this._stale < Stale.Boxes) {
|
|
267
|
+
this._restyleOnly = only ? new Set(only) : null;
|
|
268
|
+
}
|
|
269
|
+
else if (this._restyleOnly && only) {
|
|
270
|
+
for (const el of only)
|
|
271
|
+
this._restyleOnly.add(el);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
this._restyleOnly = null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
239
277
|
if (stale > this._stale)
|
|
240
278
|
this._stale = stale;
|
|
241
279
|
if (stale >= Stale.Layout)
|
|
@@ -315,14 +353,16 @@ export class HtmlViewNode extends Node {
|
|
|
315
353
|
*/
|
|
316
354
|
_requestBackgrounds(tree) {
|
|
317
355
|
for (const box of tree.backgrounds) {
|
|
318
|
-
|
|
356
|
+
// a pseudo-element's are its element's
|
|
357
|
+
const element = box.el ?? GENERATED_FROM.get(box);
|
|
358
|
+
if (!element)
|
|
319
359
|
continue;
|
|
320
360
|
// each layer's, where there is more than one
|
|
321
361
|
for (const url of box.style.backgroundImages ?? [
|
|
322
362
|
box.style.backgroundImage,
|
|
323
363
|
]) {
|
|
324
364
|
if (typeof url === 'string') {
|
|
325
|
-
this._resources.request({ url, kind: 'image', element
|
|
365
|
+
this._resources.request({ url, kind: 'image', element });
|
|
326
366
|
}
|
|
327
367
|
}
|
|
328
368
|
const border = box.style.borderImage.source;
|
|
@@ -330,9 +370,15 @@ export class HtmlViewNode extends Node {
|
|
|
330
370
|
this._resources.request({
|
|
331
371
|
url: border,
|
|
332
372
|
kind: 'image',
|
|
333
|
-
element
|
|
373
|
+
element,
|
|
334
374
|
});
|
|
335
375
|
}
|
|
376
|
+
// and each mask layer's, which is an image as a background's is
|
|
377
|
+
for (const url of box.style.mask.images) {
|
|
378
|
+
if (typeof url === 'string') {
|
|
379
|
+
this._resources.request({ url, kind: 'image', element });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
336
382
|
}
|
|
337
383
|
// and every image generated content names, which is only known there
|
|
338
384
|
// too
|
|
@@ -433,7 +479,10 @@ export class HtmlViewNode extends Node {
|
|
|
433
479
|
faces = kept.faces;
|
|
434
480
|
}
|
|
435
481
|
else {
|
|
436
|
-
|
|
482
|
+
// the UA sheet's `px` are CSS pixels, which the cascade scales like an
|
|
483
|
+
// author's, so it is written from the look in CSS pixels: from the
|
|
484
|
+
// device one, a control's size and a button's chrome were doubled at 2x
|
|
485
|
+
const sheets = [uaStylesheet(props.look)];
|
|
437
486
|
const imports = [];
|
|
438
487
|
faces = [];
|
|
439
488
|
let order = 0;
|
|
@@ -480,8 +529,9 @@ export class HtmlViewNode extends Node {
|
|
|
480
529
|
// The faces this width and scheme declare: a `@font-face` may sit in a
|
|
481
530
|
// `@media` block like any rule.
|
|
482
531
|
const cssWidth = width / this._scale;
|
|
532
|
+
const cssHeight = this._viewportHeight() / this._scale;
|
|
483
533
|
this._webFonts.setFallback(look.fontFamily);
|
|
484
|
-
this._webFonts.setFaces(faces.filter((f) => mediaMatches(f.rule.media, cssWidth, look.colorScheme)));
|
|
534
|
+
this._webFonts.setFaces(faces.filter((f) => mediaMatches(f.rule.media, cssWidth, look.colorScheme, cssHeight, this._scale)));
|
|
485
535
|
this._cascade.setPointer({
|
|
486
536
|
hovered: new Set(this._hovered),
|
|
487
537
|
active: EMPTY_SET,
|
|
@@ -548,6 +598,58 @@ export class HtmlViewNode extends Node {
|
|
|
548
598
|
}
|
|
549
599
|
return true;
|
|
550
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* The viewport the document is seen through, in window coordinates: the
|
|
603
|
+
* content box of the pane that scrolls the element, where one does —
|
|
604
|
+
* what `position: fixed` and a fixed background are placed against, as
|
|
605
|
+
* a browser places them against its page area. Null where nothing
|
|
606
|
+
* scrolls it, and the element is its own.
|
|
607
|
+
*/
|
|
608
|
+
_viewport() {
|
|
609
|
+
for (let node = this.parent; node; node = node.parent) {
|
|
610
|
+
// `Scrollable`'s, and not every node's: asked the way core asks it
|
|
611
|
+
const scroller = node;
|
|
612
|
+
if (!scroller.isScroller?.())
|
|
613
|
+
continue;
|
|
614
|
+
const box = node.contentBox();
|
|
615
|
+
return box.width > 0 && box.height > 0 ? box : null;
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* What the document draws fixed to the viewport of the pane that scrolls
|
|
621
|
+
* it, for that pane's scroll blit to repaint rather than copy with the
|
|
622
|
+
* text (react-x11's `viewportFixedRects`): the whole viewport where a
|
|
623
|
+
* fixed background shows through it, which makes a scroll a repaint, and
|
|
624
|
+
* where each fixed box is drawn. Null for a document with neither, and
|
|
625
|
+
* where nothing scrolls the element.
|
|
626
|
+
*/
|
|
627
|
+
viewportFixedRects() {
|
|
628
|
+
const tree = this._tree;
|
|
629
|
+
const viewport = tree && this._viewport();
|
|
630
|
+
if (!tree || !viewport)
|
|
631
|
+
return null;
|
|
632
|
+
if (hasFixedBackground(tree))
|
|
633
|
+
return [viewport];
|
|
634
|
+
const boxes = FIXED_BOXES.get(tree);
|
|
635
|
+
if (!boxes)
|
|
636
|
+
return null;
|
|
637
|
+
// laid out against the viewport at the document's top, and drawn at the
|
|
638
|
+
// viewport (`atViewport`)
|
|
639
|
+
const root = tree.root;
|
|
640
|
+
const out = [];
|
|
641
|
+
for (const box of boxes) {
|
|
642
|
+
if (!(box.boundsWidth > 0 && box.boundsHeight > 0))
|
|
643
|
+
continue;
|
|
644
|
+
out.push({
|
|
645
|
+
x: viewport.x + box.boundsX - root.x,
|
|
646
|
+
y: viewport.y + box.boundsY - root.y,
|
|
647
|
+
width: box.boundsWidth,
|
|
648
|
+
height: box.boundsHeight,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return out.length ? out : null;
|
|
652
|
+
}
|
|
551
653
|
_viewportHeight() {
|
|
552
654
|
// The viewport a `vh` resolves against is the one the document is seen
|
|
553
655
|
// through, not the document — a document taller than it does not make
|
|
@@ -658,9 +760,14 @@ export class HtmlViewNode extends Node {
|
|
|
658
760
|
const onlyViewport = this._stale === Stale.Nothing && this._laidOutWidth === target;
|
|
659
761
|
const wasWidth = this._documentWidth;
|
|
660
762
|
const wasHeight = this._documentHeight;
|
|
763
|
+
// the styles a build may keep (`_restyleOnly`), unless something
|
|
764
|
+
// found below changes them all
|
|
765
|
+
let restyleOnly = this._stale === Stale.Boxes ? this._restyleOnly : null;
|
|
766
|
+
this._restyleOnly = null;
|
|
661
767
|
if (this._stale >= Stale.Style || !this._cascade) {
|
|
662
768
|
this._restyle(target);
|
|
663
769
|
this._stale = Math.max(this._stale, Stale.Boxes);
|
|
770
|
+
restyleOnly = null;
|
|
664
771
|
}
|
|
665
772
|
else if (this._cascade.mediaBand(target) !== this._mediaBand) {
|
|
666
773
|
// A resize that crossed a `@media` breakpoint is the one resize that
|
|
@@ -668,6 +775,7 @@ export class HtmlViewNode extends Node {
|
|
|
668
775
|
// breakpoints are collected at parse time.
|
|
669
776
|
this._restyle(target);
|
|
670
777
|
this._stale = Math.max(this._stale, Stale.Boxes);
|
|
778
|
+
restyleOnly = null;
|
|
671
779
|
}
|
|
672
780
|
const cascade = this._cascade;
|
|
673
781
|
if (!cascade)
|
|
@@ -678,21 +786,32 @@ export class HtmlViewNode extends Node {
|
|
|
678
786
|
// styles computed for another viewport are wrong for this one: built
|
|
679
787
|
// again, where some style reads the side that moved. A document that
|
|
680
788
|
// reads neither — most — goes on skipping the cascade on a resize.
|
|
681
|
-
if (
|
|
682
|
-
(
|
|
683
|
-
|
|
684
|
-
|
|
789
|
+
if ((cascade.readsViewportWidth && this._styledWidth !== target) ||
|
|
790
|
+
(cascade.readsViewportHeight && this._styledHeight !== viewport)) {
|
|
791
|
+
if (this._stale < Stale.Boxes)
|
|
792
|
+
this._stale = Stale.Boxes;
|
|
793
|
+
restyleOnly = null;
|
|
685
794
|
}
|
|
686
795
|
if (this._stale >= Stale.Boxes || !this._tree) {
|
|
687
796
|
const look = this._deviceLook();
|
|
797
|
+
// A build a pointer move asked for styles the elements the move
|
|
798
|
+
// reached, and takes every other element's from the tree it replaces
|
|
799
|
+
// (`Cascade.beginSharing`): the first build only, since a second is
|
|
800
|
+
// one the first found a reason for — a face, an image's size.
|
|
801
|
+
let kept = restyleOnly && this._tree && this._styledWith === cascade
|
|
802
|
+
? { styles: this._tree.styles, restyle: restyleOnly }
|
|
803
|
+
: null;
|
|
688
804
|
const build = () => buildBoxes(this._source.document, {
|
|
689
805
|
cascade,
|
|
806
|
+
kept,
|
|
690
807
|
scale: this._scale,
|
|
691
808
|
imageSize: (el) => this._resources.imageSize(imageUrlOf(el) ?? ''),
|
|
692
809
|
urlSize: (url) => this._resources.imageSize(url),
|
|
693
810
|
controlSize: (el, kind, style) => measureControl(el, kind, style, this._fonts(), look),
|
|
694
811
|
});
|
|
695
812
|
this._tree = build();
|
|
813
|
+
kept = null;
|
|
814
|
+
this._styledWith = cascade;
|
|
696
815
|
this._styledWidth = target;
|
|
697
816
|
this._styledHeight = viewport;
|
|
698
817
|
this._requestBackgrounds(this._tree);
|
|
@@ -758,6 +877,7 @@ export class HtmlViewNode extends Node {
|
|
|
758
877
|
y: r.y / s,
|
|
759
878
|
width: r.width / s,
|
|
760
879
|
height: r.height / s,
|
|
880
|
+
fontSize: r.fontSize / s,
|
|
761
881
|
...(r.bare && {
|
|
762
882
|
bare: {
|
|
763
883
|
...r.bare,
|
|
@@ -852,6 +972,7 @@ export class HtmlViewNode extends Node {
|
|
|
852
972
|
}
|
|
853
973
|
}
|
|
854
974
|
destroySubtree() {
|
|
975
|
+
this._dropHeldHover();
|
|
855
976
|
this._resources.destroy();
|
|
856
977
|
this._webFonts.destroy();
|
|
857
978
|
this._source.destroy();
|
|
@@ -946,7 +1067,8 @@ export class HtmlViewNode extends Node {
|
|
|
946
1067
|
if (to <= from)
|
|
947
1068
|
return [];
|
|
948
1069
|
const out = [];
|
|
949
|
-
|
|
1070
|
+
// the rows the highlight paints, over glyphs taller than their line
|
|
1071
|
+
collectBands(tree.root, from, to, this.abs.x, this.abs.y, out, true);
|
|
950
1072
|
return out;
|
|
951
1073
|
}
|
|
952
1074
|
// --- pointer --------------------------------------------------------------
|
|
@@ -1002,29 +1124,57 @@ export class HtmlViewNode extends Node {
|
|
|
1002
1124
|
}
|
|
1003
1125
|
else {
|
|
1004
1126
|
const bands = [];
|
|
1005
|
-
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}
|
|
1016
|
-
else {
|
|
1017
|
-
collectBands(tree.root, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
|
|
1018
|
-
}
|
|
1127
|
+
const fonts = this._layouts?.fonts ?? null;
|
|
1128
|
+
// an inline box broken around the blocks in it is its pieces, a box
|
|
1129
|
+
// of the element's each (CSS 2.1 9.2.1.1), and its fragments are
|
|
1130
|
+
// theirs; and on the lines between them it has a fragment around each
|
|
1131
|
+
// block, across the box the block is in and as tall as its border
|
|
1132
|
+
// box, as a browser's has — Blink's block-in-inline, Gecko's split
|
|
1133
|
+
// inline's anonymous block. A list item made inline around a block
|
|
1134
|
+
// link measured as the empty edge after it
|
|
1135
|
+
for (const piece of piecesOf(tree.root, box)) {
|
|
1136
|
+
inlineBands(tree.root, piece, fonts, bands);
|
|
1019
1137
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1138
|
+
for (const block of (box.cut ? CUT_BLOCKS.get(box) : null) ?? []) {
|
|
1139
|
+
const around = block.parent;
|
|
1140
|
+
if (!around)
|
|
1141
|
+
continue;
|
|
1142
|
+
// from where clearance moved it down from, as Blink's line around
|
|
1143
|
+
// a block in an inline box starts there: the Zen Garden's 214
|
|
1144
|
+
// makes its content an inline box of floats and a footer that
|
|
1145
|
+
// clears them, and Chrome measures it from the floats' top
|
|
1146
|
+
const top = Math.min(block.y, CLEARED_FROM.get(block) ?? block.y);
|
|
1147
|
+
bands.push({
|
|
1148
|
+
x: around.contentX,
|
|
1149
|
+
y: top,
|
|
1150
|
+
width: around.contentWidth,
|
|
1151
|
+
height: block.y + block.height - top,
|
|
1152
|
+
});
|
|
1025
1153
|
}
|
|
1026
1154
|
for (const band of bands)
|
|
1027
1155
|
rect = rect ? unionRect(rect, band) : band;
|
|
1156
|
+
if (!rect) {
|
|
1157
|
+
// on no line, where its block has none: text set at no size takes
|
|
1158
|
+
// no room and gets no box, and CSS puts it on lines of no height
|
|
1159
|
+
// at the block's top (9.4.2), as a browser measures it — nothing,
|
|
1160
|
+
// where the block's content starts. Where the text after it
|
|
1161
|
+
// starts was the next text that took room, elsewhere in the
|
|
1162
|
+
// document: the Zen Garden's 170 hides a heading at `font-size:
|
|
1163
|
+
// 0`, and an `<abbr>` in it measured as the end of the paragraph
|
|
1164
|
+
// above it
|
|
1165
|
+
let block = box.parent;
|
|
1166
|
+
while (block && block.kind === 'inline')
|
|
1167
|
+
block = block.parent;
|
|
1168
|
+
if (block && !block.lines?.length) {
|
|
1169
|
+
const rtl = block.style.direction === 'rtl';
|
|
1170
|
+
rect = {
|
|
1171
|
+
x: rtl ? block.contentX + block.contentWidth : block.contentX,
|
|
1172
|
+
y: block.contentY,
|
|
1173
|
+
width: 0,
|
|
1174
|
+
height: 0,
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1028
1178
|
if (!rect) {
|
|
1029
1179
|
// an empty subtree's range is `[0, 0)`, wherever it stands: where
|
|
1030
1180
|
// its text would be is where the text after it starts
|
|
@@ -1048,7 +1198,20 @@ export class HtmlViewNode extends Node {
|
|
|
1048
1198
|
if (!tree)
|
|
1049
1199
|
return null;
|
|
1050
1200
|
const local = this._toDocument(x, y);
|
|
1051
|
-
return deepestAt(tree, local.x, local.y);
|
|
1201
|
+
return deepestAt(tree, local.x, local.y, undefined, this._fixedShift());
|
|
1202
|
+
}
|
|
1203
|
+
/** How far a box fixed to the viewport is drawn from where it was laid
|
|
1204
|
+
* out, in the document's pixels: the scroll of the pane the element is
|
|
1205
|
+
* in (`atViewport`). Null where nothing scrolls it. */
|
|
1206
|
+
_fixedShift() {
|
|
1207
|
+
const tree = this._tree;
|
|
1208
|
+
const viewport = tree && FIXED_BOXES.has(tree) && this._viewport();
|
|
1209
|
+
if (!tree || !viewport)
|
|
1210
|
+
return null;
|
|
1211
|
+
return {
|
|
1212
|
+
x: viewport.x - this.abs.x - tree.root.x,
|
|
1213
|
+
y: viewport.y - this.abs.y - tree.root.y,
|
|
1214
|
+
};
|
|
1052
1215
|
}
|
|
1053
1216
|
/**
|
|
1054
1217
|
* The cursor for a point of this element's, in device pixels: what core
|
|
@@ -1065,7 +1228,7 @@ export class HtmlViewNode extends Node {
|
|
|
1065
1228
|
if (!tree)
|
|
1066
1229
|
return null;
|
|
1067
1230
|
const hit = { text: false };
|
|
1068
|
-
const el = deepestAt(tree, x - this.abs.x, y - this.abs.y, hit);
|
|
1231
|
+
const el = deepestAt(tree, x - this.abs.x, y - this.abs.y, hit, this._fixedShift());
|
|
1069
1232
|
const cursor = el ? tree.styles.get(el)?.style.cursor : null;
|
|
1070
1233
|
// a keyword; a `url()` this cannot load falls back, as its list would
|
|
1071
1234
|
if (cursor && cursor !== 'auto' && /^[a-z-]+$/.test(cursor))
|
|
@@ -1079,6 +1242,7 @@ export class HtmlViewNode extends Node {
|
|
|
1079
1242
|
* contains a `:hover` rule.
|
|
1080
1243
|
*/
|
|
1081
1244
|
setHover(x, y) {
|
|
1245
|
+
this._dropHeldHover();
|
|
1082
1246
|
const cascade = this._cascade;
|
|
1083
1247
|
if (!cascade || !cascade.hoverSensitive)
|
|
1084
1248
|
return false;
|
|
@@ -1097,6 +1261,7 @@ export class HtmlViewNode extends Node {
|
|
|
1097
1261
|
return true;
|
|
1098
1262
|
}
|
|
1099
1263
|
clearHover() {
|
|
1264
|
+
this._dropHeldHover();
|
|
1100
1265
|
if (!this._hovered.length)
|
|
1101
1266
|
return false;
|
|
1102
1267
|
const was = this._hovered;
|
|
@@ -1105,64 +1270,53 @@ export class HtmlViewNode extends Node {
|
|
|
1105
1270
|
this._restyleHover(was, []);
|
|
1106
1271
|
return true;
|
|
1107
1272
|
}
|
|
1108
|
-
/**
|
|
1273
|
+
/**
|
|
1274
|
+
* The hovered chain moved: nothing, where no rule that tests the pointer
|
|
1275
|
+
* names an element it moved over; a restyle where they are, where all it
|
|
1276
|
+
* changed is ink (`_hoverInPlace`); and otherwise the boxes built again,
|
|
1277
|
+
* with the styles of every element the move did not reach kept.
|
|
1278
|
+
*/
|
|
1109
1279
|
_restyleHover(was, now) {
|
|
1110
|
-
const
|
|
1111
|
-
if (
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
this.
|
|
1280
|
+
const reach = this._hoverReach(was, now);
|
|
1281
|
+
if (reach !== null) {
|
|
1282
|
+
if (!reach.size)
|
|
1283
|
+
return;
|
|
1284
|
+
if (this._hoverInPlace(reach, was))
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
this._invalidate(Stale.Boxes, reach);
|
|
1115
1288
|
}
|
|
1116
1289
|
/**
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
* A move to another element is a change to the cascade, and the pipeline
|
|
1120
|
-
* answers one by building every box again and laying the document out:
|
|
1121
|
-
* 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
|
|
1122
|
-
* link the pointer crossed. But the rules a page writes for `:hover`
|
|
1123
|
-
* nearly all change a colour, an underline, a background or a border's
|
|
1124
|
-
* colour — 77 of that article's 79 — and those move nothing. So:
|
|
1125
|
-
*
|
|
1126
|
-
* - Only an element whose hover state flipped and that a compound testing
|
|
1127
|
-
* the pointer could match can change (`Cascade.hoverTouches`), with its
|
|
1128
|
-
* subtree, and its later siblings where a sibling combinator follows.
|
|
1129
|
-
* A move between two paragraphs under `a:hover` touches nothing.
|
|
1130
|
-
* - Those are styled again from their parents. Where anything but ink
|
|
1131
|
-
* differs (`PAINT_ONLY`), or a box is not the element's own — an
|
|
1132
|
-
* anonymous or a pseudo-element box, a marker, a control — this is
|
|
1133
|
-
* not the move to take.
|
|
1134
|
-
* - The boxes take their new styles, and each layout of their text is
|
|
1135
|
-
* made again from the runs it was made from (`TextLayoutCache.inputsOf`)
|
|
1136
|
-
* with the new ink (`runFor`), where its geometry comes out the same.
|
|
1290
|
+
* The elements whose styles a move of the hovered chain can change, or
|
|
1291
|
+
* null where that cannot be said (`Cascade.hoverLocal`).
|
|
1137
1292
|
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1293
|
+
* Only an element whose hover state flipped and that a compound testing
|
|
1294
|
+
* the pointer could match can change (`Cascade.hoverTouches`), with its
|
|
1295
|
+
* subtree, and its later siblings' where a sibling combinator is in play;
|
|
1296
|
+
* and the elements a `:has()` testing the pointer may flip, which are
|
|
1297
|
+
* around the ones that changed. A move between two paragraphs under
|
|
1298
|
+
* `a:hover` reaches nothing.
|
|
1141
1299
|
*/
|
|
1142
|
-
|
|
1300
|
+
_hoverReach(was, now) {
|
|
1143
1301
|
const cascade = this._cascade;
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
if (!cascade || !tree || !layouts || !cascade.hoverLocal)
|
|
1147
|
-
return false;
|
|
1148
|
-
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0)
|
|
1149
|
-
return false;
|
|
1302
|
+
if (!cascade || !cascade.hoverLocal)
|
|
1303
|
+
return null;
|
|
1150
1304
|
const before = new Set(was);
|
|
1151
1305
|
const after = new Set(now);
|
|
1152
|
-
const roots =
|
|
1153
|
-
// and the elements a `:has()` testing the pointer may flip, which
|
|
1154
|
-
// are around the ones that changed; the chains are ancestor chains,
|
|
1155
|
-
// so the deepest that changed on each side is where to look from
|
|
1306
|
+
const roots = new Map();
|
|
1156
1307
|
const flipped = (el, other) => {
|
|
1157
1308
|
if (other.has(el))
|
|
1158
1309
|
return;
|
|
1159
|
-
|
|
1160
|
-
|
|
1310
|
+
const touch = cascade.hoverTouches(el);
|
|
1311
|
+
if (touch > (roots.get(el) ?? HOVER_UNTOUCHED))
|
|
1312
|
+
roots.set(el, touch);
|
|
1161
1313
|
};
|
|
1162
1314
|
for (const el of was)
|
|
1163
1315
|
flipped(el, after);
|
|
1164
1316
|
for (const el of now)
|
|
1165
1317
|
flipped(el, before);
|
|
1318
|
+
// the chains are ancestor chains, so the deepest that changed on each
|
|
1319
|
+
// side is where a `:has()` is looked for from
|
|
1166
1320
|
const deepest = (chain, other) => {
|
|
1167
1321
|
const el = chain[0];
|
|
1168
1322
|
if (el && !other.has(el))
|
|
@@ -1170,11 +1324,9 @@ export class HtmlViewNode extends Node {
|
|
|
1170
1324
|
};
|
|
1171
1325
|
deepest(was, after);
|
|
1172
1326
|
deepest(now, before);
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
//
|
|
1176
|
-
// descendant combinator reaches, and their later siblings' too where a
|
|
1177
|
-
// sibling combinator follows a compound that tests the pointer
|
|
1327
|
+
// the roots' subtrees, which inherit from them and a descendant
|
|
1328
|
+
// combinator reaches, and their later siblings' too where a sibling
|
|
1329
|
+
// combinator follows the compound that tests the pointer
|
|
1178
1330
|
const reach = new Set();
|
|
1179
1331
|
const collect = (el) => {
|
|
1180
1332
|
const stack = [el];
|
|
@@ -1183,28 +1335,91 @@ export class HtmlViewNode extends Node {
|
|
|
1183
1335
|
if (reach.has(at))
|
|
1184
1336
|
continue;
|
|
1185
1337
|
reach.add(at);
|
|
1186
|
-
if (reach.size > HOVER_RESTYLE_LIMIT)
|
|
1187
|
-
return false;
|
|
1188
1338
|
for (const child of at.children)
|
|
1189
1339
|
if (isElement(child))
|
|
1190
1340
|
stack.push(child);
|
|
1191
1341
|
}
|
|
1192
|
-
return true;
|
|
1193
1342
|
};
|
|
1194
|
-
for (const root of roots) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
if (!cascade.hoverSiblings)
|
|
1343
|
+
for (const [root, touch] of roots) {
|
|
1344
|
+
collect(root);
|
|
1345
|
+
if (touch !== HOVER_FOLLOWED)
|
|
1198
1346
|
continue;
|
|
1199
1347
|
for (let s = root.nextSibling; s; s = s.nextSibling) {
|
|
1200
|
-
if (isElement(s)
|
|
1201
|
-
|
|
1348
|
+
if (isElement(s))
|
|
1349
|
+
collect(s);
|
|
1202
1350
|
}
|
|
1203
1351
|
}
|
|
1352
|
+
return reach;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* A pointer move restyled where it happened, when all it changed is ink.
|
|
1356
|
+
*
|
|
1357
|
+
* A move to another element is a change to the cascade, and the pipeline
|
|
1358
|
+
* answers one by building every box again and laying the document out:
|
|
1359
|
+
* 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
|
|
1360
|
+
* link the pointer crossed. But the rules a page writes for `:hover`
|
|
1361
|
+
* nearly all change a colour, an underline, a background or a border's
|
|
1362
|
+
* colour — 77 of that article's 79 — and those move nothing. So:
|
|
1363
|
+
*
|
|
1364
|
+
* - The elements the move reached (`_hoverReach`) are styled again from
|
|
1365
|
+
* their parents — the ones a rule testing the pointer answers
|
|
1366
|
+
* differently for, or whose parent's style changed; the rest, most of
|
|
1367
|
+
* a hovered card or table row, have the styles they had
|
|
1368
|
+
* (`Cascade.pointerChanged`). Where anything but ink differs
|
|
1369
|
+
* (`PAINT_ONLY`), this is not the move to take.
|
|
1370
|
+
* - The boxes take their new styles: an element's own, the text in it,
|
|
1371
|
+
* the anonymous boxes the fix-up made around what is in it, and its
|
|
1372
|
+
* `::before` and `::after`, styled again by their own rules. A box
|
|
1373
|
+
* whose style is derived some other way — a marker, a first letter, a
|
|
1374
|
+
* control — is not one to restyle here.
|
|
1375
|
+
* - Each layout of their text is made again from the runs it was made
|
|
1376
|
+
* from (`TextLayoutCache.inputsOf`) with the new ink (`runFor`), where
|
|
1377
|
+
* its geometry comes out the same.
|
|
1378
|
+
*
|
|
1379
|
+
* Everything is checked before anything is changed, and nothing is looked
|
|
1380
|
+
* at but the blocks the restyled elements are in (`_blocksOf`): a move
|
|
1381
|
+
* costs what it changed, in a document of any length. True where it was
|
|
1382
|
+
* restyled here, or had nothing to restyle; false where the boxes have to
|
|
1383
|
+
* be built again.
|
|
1384
|
+
*/
|
|
1385
|
+
_hoverInPlace(reach, was) {
|
|
1386
|
+
const cascade = this._cascade;
|
|
1387
|
+
const tree = this._tree;
|
|
1388
|
+
const layouts = this._layouts;
|
|
1389
|
+
if (!cascade || !tree || !layouts)
|
|
1390
|
+
return false;
|
|
1391
|
+
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0)
|
|
1392
|
+
return false;
|
|
1393
|
+
if (reach.size > HOVER_RESTYLE_LIMIT)
|
|
1394
|
+
return false;
|
|
1395
|
+
// a first line's, a first letter's and a marker's styles are layout's
|
|
1396
|
+
const pseudoRules = cascade.hoverPseudo;
|
|
1397
|
+
if (pseudoRules === HOVER_PSEUDO_OTHER)
|
|
1398
|
+
return false;
|
|
1399
|
+
// whether the rules that test the pointer answer differently for an
|
|
1400
|
+
// element than they did before the move
|
|
1401
|
+
const before = { hovered: new Set(was), active: EMPTY_SET };
|
|
1402
|
+
const flips = new Map();
|
|
1403
|
+
const flipped = (el) => {
|
|
1404
|
+
let flip = flips.get(el);
|
|
1405
|
+
if (flip === undefined) {
|
|
1406
|
+
flip = cascade.pointerChanged(el, before);
|
|
1407
|
+
flips.set(el, flip);
|
|
1408
|
+
}
|
|
1409
|
+
return flip;
|
|
1410
|
+
};
|
|
1204
1411
|
// styled again from their parents, a parent first
|
|
1205
1412
|
const fresh = new Map();
|
|
1206
1413
|
const changed = new Map();
|
|
1414
|
+
// the ones whose ink is what it was: only their custom properties
|
|
1415
|
+
// changed, which the elements under them read
|
|
1416
|
+
const quiet = new Set();
|
|
1207
1417
|
let refused = false;
|
|
1418
|
+
// what changed beyond ink (`hoverChange`): how far some box's ink
|
|
1419
|
+
// reaches, the order its layer paints in, where one is
|
|
1420
|
+
let reaches = false;
|
|
1421
|
+
let reorders = false;
|
|
1422
|
+
const moving = new Set();
|
|
1208
1423
|
const styleOf = (el) => {
|
|
1209
1424
|
const kept = tree.styles.get(el);
|
|
1210
1425
|
if (!kept)
|
|
@@ -1222,13 +1437,27 @@ export class HtmlViewNode extends Node {
|
|
|
1222
1437
|
refused = true;
|
|
1223
1438
|
return null;
|
|
1224
1439
|
}
|
|
1225
|
-
const
|
|
1440
|
+
const parentWas = parent ? tree.styles.get(parent).style : parentStyle;
|
|
1441
|
+
let style = kept.style;
|
|
1442
|
+
if (parentStyle !== parentWas || flipped(el)) {
|
|
1443
|
+
const made = cascade.styleFor(el, parentStyle, kept.inFlex);
|
|
1444
|
+
const diff = hoverChange(kept.style, made);
|
|
1445
|
+
if (diff === false)
|
|
1446
|
+
refused = true;
|
|
1447
|
+
else if (diff) {
|
|
1448
|
+
style = made;
|
|
1449
|
+
changed.set(el, made);
|
|
1450
|
+
if (!diff.ink)
|
|
1451
|
+
quiet.add(el);
|
|
1452
|
+
if (diff.reach)
|
|
1453
|
+
reaches = true;
|
|
1454
|
+
if (diff.order)
|
|
1455
|
+
reorders = true;
|
|
1456
|
+
if (diff.move)
|
|
1457
|
+
moving.add(el);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1226
1460
|
fresh.set(el, style);
|
|
1227
|
-
const diff = inkOnly(kept.style, style);
|
|
1228
|
-
if (diff === false)
|
|
1229
|
-
refused = true;
|
|
1230
|
-
else if (diff)
|
|
1231
|
-
changed.set(el, style);
|
|
1232
1461
|
return style;
|
|
1233
1462
|
};
|
|
1234
1463
|
for (const el of reach) {
|
|
@@ -1236,38 +1465,122 @@ export class HtmlViewNode extends Node {
|
|
|
1236
1465
|
if (refused)
|
|
1237
1466
|
return false;
|
|
1238
1467
|
}
|
|
1239
|
-
if (!changed.size)
|
|
1240
|
-
return
|
|
1468
|
+
if (!changed.size && pseudoRules === HOVER_PSEUDO_NONE)
|
|
1469
|
+
return true;
|
|
1241
1470
|
for (const el of changed.keys()) {
|
|
1242
1471
|
const tag = tagOf(el);
|
|
1243
1472
|
// their backgrounds are the canvas's
|
|
1244
1473
|
if (tag === 'html' || tag === 'body')
|
|
1245
1474
|
return false;
|
|
1246
1475
|
}
|
|
1247
|
-
//
|
|
1248
|
-
//
|
|
1476
|
+
// The boxes, each under its parent: what a box takes depends on what
|
|
1477
|
+
// the box it is in took.
|
|
1249
1478
|
const fonts = layouts.fonts;
|
|
1479
|
+
const blocks = this._blocksOf(tree, pseudoRules === HOVER_PSEUDO_NONE ? changed.keys() : reach);
|
|
1250
1480
|
const restyled = [];
|
|
1481
|
+
const next = new Map();
|
|
1482
|
+
/** The restyled boxes whose ink changed, to repaint. */
|
|
1483
|
+
const inked = new Set();
|
|
1484
|
+
/** The `::before` and `::after` boxes found, by element: 1 and 2. */
|
|
1485
|
+
const generated = new Map();
|
|
1251
1486
|
const redecorated = [];
|
|
1252
|
-
const walk =
|
|
1487
|
+
const walk = blocks.slice();
|
|
1253
1488
|
while (walk.length) {
|
|
1254
1489
|
const box = walk.pop();
|
|
1255
1490
|
for (const child of box.children)
|
|
1256
1491
|
walk.push(child);
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1492
|
+
const parent = box.parent;
|
|
1493
|
+
const above = parent ? next.get(parent) : undefined;
|
|
1494
|
+
let style;
|
|
1495
|
+
let ink = !!parent && inked.has(parent);
|
|
1496
|
+
if (box.kind === 'text' || box.kind === 'break') {
|
|
1497
|
+
// Text is set in the style of the box it is in — an element's, a
|
|
1498
|
+
// pseudo-element's — or of its element, where the fix-up put an
|
|
1499
|
+
// anonymous box around it.
|
|
1500
|
+
const el = box.el;
|
|
1501
|
+
if (parent && box.style === parent.style)
|
|
1502
|
+
style = above;
|
|
1503
|
+
else if (el && box.style === tree.styles.get(el)?.style) {
|
|
1504
|
+
style = changed.get(el);
|
|
1505
|
+
ink = !quiet.has(el);
|
|
1506
|
+
}
|
|
1507
|
+
else if (above || (el && changed.has(el)))
|
|
1508
|
+
return false;
|
|
1509
|
+
}
|
|
1510
|
+
else if (box.pseudo === 'before' || box.pseudo === 'after') {
|
|
1511
|
+
const from = GENERATED_FROM.get(box);
|
|
1512
|
+
if (!from) {
|
|
1513
|
+
// an inside marker, made of its item's style and its own rules
|
|
1514
|
+
if (above)
|
|
1515
|
+
return false;
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
if (!reach.has(from))
|
|
1519
|
+
continue;
|
|
1520
|
+
generated.set(from, (generated.get(from) ?? 0) | (box.pseudo === 'before' ? 1 : 2));
|
|
1521
|
+
const inherits = fresh.get(from);
|
|
1522
|
+
const kept = tree.styles.get(from)?.style;
|
|
1523
|
+
if (!inherits || !kept)
|
|
1524
|
+
return false;
|
|
1525
|
+
if (inherits === kept && !flipped(from))
|
|
1526
|
+
continue;
|
|
1527
|
+
// styled by its own rules over its element's style, as the builder
|
|
1528
|
+
// styled it, in its element's box
|
|
1529
|
+
if (!parent || parent.style !== kept || box.marker)
|
|
1530
|
+
return false;
|
|
1531
|
+
const made = cascade.pseudoStyleFor(from, box.pseudo, inherits);
|
|
1532
|
+
if (!made || made.display === 'none')
|
|
1533
|
+
return false;
|
|
1534
|
+
const diff = hoverChange(box.style, made);
|
|
1535
|
+
if (diff === false)
|
|
1536
|
+
return false;
|
|
1537
|
+
if (!diff)
|
|
1538
|
+
continue;
|
|
1539
|
+
if (diff.reach || diff.order || diff.move)
|
|
1540
|
+
return false;
|
|
1541
|
+
style = made;
|
|
1542
|
+
ink = diff.ink;
|
|
1543
|
+
}
|
|
1544
|
+
else if (box.pseudo) {
|
|
1545
|
+
// a first letter, styled from the box its letter is in
|
|
1546
|
+
if (above)
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
else if (box.el) {
|
|
1550
|
+
const el = box.el;
|
|
1551
|
+
style = changed.get(el);
|
|
1552
|
+
if (!style)
|
|
1553
|
+
continue;
|
|
1554
|
+
const kept = tree.styles.get(el).style;
|
|
1555
|
+
// a style derived from the element's, or drawn from it somewhere
|
|
1556
|
+
// else — a widget mounted beside the document takes its look from
|
|
1557
|
+
// it. An image, a drawing or a rule is drawn here, from its box,
|
|
1558
|
+
// like any.
|
|
1559
|
+
if (box.style !== kept)
|
|
1560
|
+
return false;
|
|
1561
|
+
if (box.marker || WIDGETS.has(box.replaced))
|
|
1562
|
+
return false;
|
|
1563
|
+
if (moving.has(el) && !movable(box, kept, style))
|
|
1564
|
+
return false;
|
|
1565
|
+
ink = !quiet.has(el);
|
|
1566
|
+
}
|
|
1567
|
+
else if (above) {
|
|
1568
|
+
// An anonymous box takes what its parent's style passes on
|
|
1569
|
+
// (`anonymousStyles`), so it takes that of the new one: the text of
|
|
1570
|
+
// a link made a flex row is in one, and takes the link's colour.
|
|
1571
|
+
if (GENERATED_FROM.has(box))
|
|
1572
|
+
return false;
|
|
1573
|
+
const display = box.style.display;
|
|
1574
|
+
if (tree.anonymous(parent.style, display) !== box.style)
|
|
1575
|
+
return false;
|
|
1576
|
+
style = tree.anonymous(above, display);
|
|
1577
|
+
}
|
|
1261
1578
|
if (!style)
|
|
1262
1579
|
continue;
|
|
1263
|
-
|
|
1264
|
-
// an anonymous box, a pseudo-element, a marker, a control: a style
|
|
1265
|
-
// derived from the element's, or drawn from it somewhere else
|
|
1266
|
-
if (!box.el || box.style !== kept.style)
|
|
1267
|
-
return false;
|
|
1268
|
-
if (box.marker || box.replaced !== 'none')
|
|
1269
|
-
return false;
|
|
1580
|
+
next.set(box, style);
|
|
1270
1581
|
restyled.push([box, style]);
|
|
1582
|
+
if (ink)
|
|
1583
|
+
inked.add(box);
|
|
1271
1584
|
if (box.kind === 'inline') {
|
|
1272
1585
|
const decoration = inlineDecoration(fonts, box, style);
|
|
1273
1586
|
if ((decoration === null) !== (box.decoration === null)) {
|
|
@@ -1279,11 +1592,34 @@ export class HtmlViewNode extends Node {
|
|
|
1279
1592
|
redecorated.push([box, decoration]);
|
|
1280
1593
|
}
|
|
1281
1594
|
}
|
|
1282
|
-
//
|
|
1283
|
-
//
|
|
1595
|
+
// a `::before` or an `::after` a rule that tests the pointer now gives
|
|
1596
|
+
// an element is a box to build
|
|
1597
|
+
if (pseudoRules !== HOVER_PSEUDO_NONE) {
|
|
1598
|
+
for (const el of reach) {
|
|
1599
|
+
if (!flipped(el))
|
|
1600
|
+
continue;
|
|
1601
|
+
const style = fresh.get(el);
|
|
1602
|
+
if (!style)
|
|
1603
|
+
continue;
|
|
1604
|
+
const has = generated.get(el) ?? 0;
|
|
1605
|
+
for (const [which, bit] of GENERATED_BITS) {
|
|
1606
|
+
if (has & bit)
|
|
1607
|
+
continue;
|
|
1608
|
+
const made = cascade.pseudoStyleFor(el, which, style);
|
|
1609
|
+
if (made && made.display !== 'none')
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (!restyled.length)
|
|
1615
|
+
return true;
|
|
1616
|
+
// the text: each layout holding a run of a restyled box's, made again
|
|
1617
|
+
// with its new ink, where it comes out the same shape
|
|
1284
1618
|
const relaid = new Map();
|
|
1285
1619
|
const texts = [];
|
|
1286
|
-
|
|
1620
|
+
if (inked.size)
|
|
1621
|
+
for (const block of blocks)
|
|
1622
|
+
walk.push(block);
|
|
1287
1623
|
while (walk.length) {
|
|
1288
1624
|
const box = walk.pop();
|
|
1289
1625
|
for (const child of box.children)
|
|
@@ -1295,27 +1631,156 @@ export class HtmlViewNode extends Node {
|
|
|
1295
1631
|
texts.push(text);
|
|
1296
1632
|
if (relaid.has(text.layout))
|
|
1297
1633
|
continue;
|
|
1298
|
-
const
|
|
1299
|
-
if (
|
|
1634
|
+
const again = reinked(text, layouts, next);
|
|
1635
|
+
if (again === false)
|
|
1300
1636
|
return false;
|
|
1301
|
-
relaid.set(text.layout,
|
|
1637
|
+
relaid.set(text.layout, again);
|
|
1302
1638
|
}
|
|
1303
1639
|
}
|
|
1304
1640
|
}
|
|
1641
|
+
// what each box drew before, to be repainted with what it draws after
|
|
1642
|
+
const inks = [];
|
|
1643
|
+
for (const box of inked) {
|
|
1644
|
+
const ink = inkOf(box);
|
|
1645
|
+
if (ink)
|
|
1646
|
+
inks.push(ink);
|
|
1647
|
+
}
|
|
1305
1648
|
// and only now, all of it
|
|
1306
|
-
|
|
1649
|
+
const moved = [];
|
|
1650
|
+
for (const [box, style] of restyled) {
|
|
1651
|
+
const was = box.style;
|
|
1307
1652
|
box.style = style;
|
|
1308
|
-
|
|
1653
|
+
if (box.el && moving.has(box.el) && box.kind !== 'text') {
|
|
1654
|
+
moved.push([box, was]);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
for (const [box, decoration] of redecorated) {
|
|
1658
|
+
// what is inside it keeps which boxes around it paint, and this is
|
|
1659
|
+
// one more or one fewer of them
|
|
1660
|
+
if ((decoration === null) !== (box.decoration === null)) {
|
|
1661
|
+
forgetDecoratedAncestors(box);
|
|
1662
|
+
}
|
|
1309
1663
|
box.decoration = decoration;
|
|
1664
|
+
}
|
|
1310
1665
|
for (const text of texts) {
|
|
1311
|
-
const
|
|
1312
|
-
if (
|
|
1313
|
-
text.layout =
|
|
1666
|
+
const again = relaid.get(text.layout);
|
|
1667
|
+
if (again)
|
|
1668
|
+
text.layout = again;
|
|
1314
1669
|
}
|
|
1315
1670
|
for (const [el, style] of changed) {
|
|
1316
1671
|
tree.styles.set(el, { style, inFlex: tree.styles.get(el).inFlex });
|
|
1317
1672
|
}
|
|
1318
|
-
|
|
1673
|
+
// A box that moved moves what is in it, as layout moved it the first
|
|
1674
|
+
// time (`applyRelativeOffsets`), and a later layout moves it the same
|
|
1675
|
+
// way; where it reached the document's end, before or after, the
|
|
1676
|
+
// document's height may have changed with it, which only a layout says.
|
|
1677
|
+
let relayout = false;
|
|
1678
|
+
for (const [box, was] of moved) {
|
|
1679
|
+
const before = box.boundsY + box.boundsHeight;
|
|
1680
|
+
retranslate(box, was);
|
|
1681
|
+
tree.relative = true;
|
|
1682
|
+
if (before >= this._documentHeight)
|
|
1683
|
+
relayout = true;
|
|
1684
|
+
}
|
|
1685
|
+
// how far the boxes' ink reaches, and the order their layers paint in,
|
|
1686
|
+
// as layout leaves them (`layoutDocument`)
|
|
1687
|
+
if (reaches || reorders || moved.length) {
|
|
1688
|
+
computePaintBounds(tree.root, tree.movedInline);
|
|
1689
|
+
if (tree.negative)
|
|
1690
|
+
hoistNegative(tree.root);
|
|
1691
|
+
stackLayers(tree.root);
|
|
1692
|
+
}
|
|
1693
|
+
for (const [box] of moved) {
|
|
1694
|
+
if (box.boundsY + box.boundsHeight >= this._documentHeight) {
|
|
1695
|
+
relayout = true;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (moved.length)
|
|
1699
|
+
this._reportControls();
|
|
1700
|
+
if (relayout) {
|
|
1701
|
+
this._invalidate(Stale.Layout);
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
for (const box of inked) {
|
|
1705
|
+
const ink = inkOf(box);
|
|
1706
|
+
if (ink)
|
|
1707
|
+
inks.push(ink);
|
|
1708
|
+
}
|
|
1709
|
+
this._repaintInk(inks);
|
|
1710
|
+
return true;
|
|
1711
|
+
}
|
|
1712
|
+
/** An element's first box in document order, for the tree it was asked
|
|
1713
|
+
* of: made on the first move over a tree, which is one walk of it. */
|
|
1714
|
+
_firstBoxes = null;
|
|
1715
|
+
/**
|
|
1716
|
+
* The blocks a restyle of `changed` has to look in: for each element, the
|
|
1717
|
+
* box its text is set in the lines of — the nearest box around its first
|
|
1718
|
+
* that is not an inline one — which holds every box that takes its style
|
|
1719
|
+
* from it, the pieces a block in it broke it into among them
|
|
1720
|
+
* (`breakAround`). One that is inside another is left to it.
|
|
1721
|
+
*/
|
|
1722
|
+
_blocksOf(tree, changed) {
|
|
1723
|
+
let index = this._firstBoxes;
|
|
1724
|
+
if (index?.tree !== tree) {
|
|
1725
|
+
const of = new Map();
|
|
1726
|
+
const stack = [tree.root];
|
|
1727
|
+
while (stack.length) {
|
|
1728
|
+
const box = stack.pop();
|
|
1729
|
+
if (box.el && !of.has(box.el))
|
|
1730
|
+
of.set(box.el, box);
|
|
1731
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1732
|
+
stack.push(box.children[i]);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
index = this._firstBoxes = { tree, of };
|
|
1736
|
+
}
|
|
1737
|
+
const blocks = new Set();
|
|
1738
|
+
for (const el of changed) {
|
|
1739
|
+
let box = index.of.get(el) ?? null;
|
|
1740
|
+
while (box?.parent &&
|
|
1741
|
+
(box.kind === 'inline' || box.kind === 'text' || box.kind === 'break')) {
|
|
1742
|
+
box = box.parent;
|
|
1743
|
+
}
|
|
1744
|
+
if (box)
|
|
1745
|
+
blocks.add(box);
|
|
1746
|
+
}
|
|
1747
|
+
const out = [];
|
|
1748
|
+
for (const block of blocks) {
|
|
1749
|
+
let inside = false;
|
|
1750
|
+
for (let at = block.parent; at && !inside; at = at.parent) {
|
|
1751
|
+
inside = blocks.has(at);
|
|
1752
|
+
}
|
|
1753
|
+
if (!inside)
|
|
1754
|
+
out.push(block);
|
|
1755
|
+
}
|
|
1756
|
+
return out;
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Repaint the document where `inks` are, in its own coordinates: the ink
|
|
1760
|
+
* of the boxes a hover restyled, before and after. Past a handful of rects
|
|
1761
|
+
* the damage is one around them all, which is what core would make of
|
|
1762
|
+
* them anyway.
|
|
1763
|
+
*/
|
|
1764
|
+
_repaintInk(inks) {
|
|
1765
|
+
if (!inks.length)
|
|
1766
|
+
return;
|
|
1767
|
+
const x = this.abs.x;
|
|
1768
|
+
const y = this.abs.y;
|
|
1769
|
+
const place = (r) => ({
|
|
1770
|
+
x: Math.floor(x + r.x),
|
|
1771
|
+
y: Math.floor(y + r.y),
|
|
1772
|
+
width: Math.ceil(r.width) + 1,
|
|
1773
|
+
height: Math.ceil(r.height) + 1,
|
|
1774
|
+
});
|
|
1775
|
+
if (inks.length > 6) {
|
|
1776
|
+
let all = inks[0];
|
|
1777
|
+
for (const r of inks)
|
|
1778
|
+
all = unionRect(all, r);
|
|
1779
|
+
this.invalidate(false, place(all), 'props');
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
for (const r of inks)
|
|
1783
|
+
this.invalidate(false, place(r), 'props');
|
|
1319
1784
|
}
|
|
1320
1785
|
/** The document, for an application that wants to read or change it. */
|
|
1321
1786
|
get document() {
|
|
@@ -1348,17 +1813,64 @@ export class HtmlViewNode extends Node {
|
|
|
1348
1813
|
// `:hover` is wired at the element rather than through React: the cascade
|
|
1349
1814
|
// already knows whether any rule in the document tests it, `setHover`
|
|
1350
1815
|
// returns without work when none does, and a document that does use it
|
|
1351
|
-
// re-styles only when the hovered chain actually changed
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1816
|
+
// re-styles only when the hovered chain actually changed, and then only
|
|
1817
|
+
// the elements the change reached (`_restyleHover`).
|
|
1818
|
+
//
|
|
1819
|
+
// **A hover waits for a scroll to stop.** Core asks again what is under a
|
|
1820
|
+
// pointer that stayed where it was after every frame that laid out —
|
|
1821
|
+
// hover follows content, react-x11#793 — and says so with a move to the
|
|
1822
|
+
// point the pointer is already at. While a document scrolls that is every
|
|
1823
|
+
// frame, each with something else under the pointer: a restyle a frame
|
|
1824
|
+
// at best, and where a hover moves something, the boxes built again a
|
|
1825
|
+
// frame, which took a page scrolled under a parked pointer from 52 frames
|
|
1826
|
+
// a second to 4. A browser holds its hover until the scroll is over, for
|
|
1827
|
+
// the same reason; so does this, for `HOVER_REST_MS` after the content
|
|
1828
|
+
// last moved. What was hovered stays hovered as it scrolls away, as it
|
|
1829
|
+
// does there. A move of the pointer itself is answered at once.
|
|
1354
1830
|
defaultMouseMove(ev) {
|
|
1355
1831
|
super.defaultMouseMove?.(ev);
|
|
1832
|
+
const at = this._pointerAt;
|
|
1833
|
+
if (at !== null && at.x === ev.x && at.y === ev.y) {
|
|
1834
|
+
this._holdHover();
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
this._pointerAt = { x: ev.x, y: ev.y };
|
|
1356
1838
|
this.setHover(ev.x, ev.y);
|
|
1357
1839
|
}
|
|
1358
1840
|
defaultMouseLeave(ev) {
|
|
1359
1841
|
super.defaultMouseLeave?.(ev);
|
|
1842
|
+
this._pointerAt = null;
|
|
1360
1843
|
this.clearHover();
|
|
1361
1844
|
}
|
|
1845
|
+
/** The content moved under the pointer: the hover is asked for once it
|
|
1846
|
+
* has been still for `HOVER_REST_MS`. One timer a wait, put off by
|
|
1847
|
+
* being armed again when it finds the content moved since. */
|
|
1848
|
+
_holdHover() {
|
|
1849
|
+
if (!this._cascade?.hoverSensitive)
|
|
1850
|
+
return;
|
|
1851
|
+
this._heldSince = hoverClock.now();
|
|
1852
|
+
if (this._heldHover !== null)
|
|
1853
|
+
return;
|
|
1854
|
+
const rest = () => {
|
|
1855
|
+
this._heldHover = null;
|
|
1856
|
+
const at = this._pointerAt;
|
|
1857
|
+
if (!at || this.destroyed)
|
|
1858
|
+
return;
|
|
1859
|
+
const left = this._heldSince + HOVER_REST_MS - hoverClock.now();
|
|
1860
|
+
if (left > 0) {
|
|
1861
|
+
this._heldHover = hoverClock.arm(rest, left);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
this.setHover(at.x, at.y);
|
|
1865
|
+
};
|
|
1866
|
+
this._heldHover = hoverClock.arm(rest, HOVER_REST_MS);
|
|
1867
|
+
}
|
|
1868
|
+
_dropHeldHover() {
|
|
1869
|
+
if (this._heldHover === null)
|
|
1870
|
+
return;
|
|
1871
|
+
hoverClock.disarm(this._heldHover);
|
|
1872
|
+
this._heldHover = null;
|
|
1873
|
+
}
|
|
1362
1874
|
// --- paint ----------------------------------------------------------------
|
|
1363
1875
|
paint(ctx) {
|
|
1364
1876
|
super.paint(ctx); // background, border, clip to `abs`
|
|
@@ -1388,6 +1900,16 @@ export class HtmlViewNode extends Node {
|
|
|
1388
1900
|
this._fail(error, this._laidOutWidth);
|
|
1389
1901
|
}
|
|
1390
1902
|
}
|
|
1903
|
+
/** A surface for the painter to draw a masked element on, asked of the
|
|
1904
|
+
* backend until it is found to have none. */
|
|
1905
|
+
_surface(width, height) {
|
|
1906
|
+
if (this._noSurface)
|
|
1907
|
+
return null;
|
|
1908
|
+
const surface = newSurface(this.app, width, height);
|
|
1909
|
+
if (!surface)
|
|
1910
|
+
this._noSurface = true;
|
|
1911
|
+
return surface;
|
|
1912
|
+
}
|
|
1391
1913
|
_paint(ctx, tree, range, damage) {
|
|
1392
1914
|
paintDocument(ctx, tree, {
|
|
1393
1915
|
originX: this.abs.x,
|
|
@@ -1395,6 +1917,7 @@ export class HtmlViewNode extends Node {
|
|
|
1395
1917
|
// the root's background covers the whole element, not only the
|
|
1396
1918
|
// document: an element grown past its content is canvas too
|
|
1397
1919
|
canvas: this.abs,
|
|
1920
|
+
viewport: this._viewport() ?? undefined,
|
|
1398
1921
|
scale: this._scale,
|
|
1399
1922
|
damage,
|
|
1400
1923
|
selection: range
|
|
@@ -1410,11 +1933,34 @@ export class HtmlViewNode extends Node {
|
|
|
1410
1933
|
const size = image ? this._resources.imageSize(url) : null;
|
|
1411
1934
|
return image && size ? { image, ...size } : null;
|
|
1412
1935
|
},
|
|
1936
|
+
surface: (width, height) => this._surface(width, height),
|
|
1413
1937
|
cached: (key, width, height, draw) => (this._shadowCache ??= new SurfaceCache(this.app)).get(key, width, height, draw),
|
|
1414
1938
|
});
|
|
1415
1939
|
}
|
|
1416
1940
|
}
|
|
1417
1941
|
const EMPTY_SET = new Set();
|
|
1942
|
+
/** Whether a tree has a background fixed to the viewport: an image a box
|
|
1943
|
+
* names (`tree.backgrounds`), or the canvas's image or gradient — the
|
|
1944
|
+
* root's, or the body's it paints, which the root box stands in for where
|
|
1945
|
+
* the markup has neither element. */
|
|
1946
|
+
function hasFixedBackground(tree) {
|
|
1947
|
+
for (const box of tree.backgrounds) {
|
|
1948
|
+
if (box.style.backgroundAttachment === 'fixed')
|
|
1949
|
+
return true;
|
|
1950
|
+
}
|
|
1951
|
+
const root = tree.root;
|
|
1952
|
+
const body = root.children.find((child) => child.el?.name === 'body');
|
|
1953
|
+
for (const box of body ? [root, body] : [root]) {
|
|
1954
|
+
const style = box.style;
|
|
1955
|
+
if (style.backgroundAttachment === 'fixed' &&
|
|
1956
|
+
(style.backgroundImage ||
|
|
1957
|
+
style.backgroundImages ||
|
|
1958
|
+
style.backgroundGradient)) {
|
|
1959
|
+
return true;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1418
1964
|
/** The first box an element made, depth first. */
|
|
1419
1965
|
function boxFor(root, element) {
|
|
1420
1966
|
const stack = [root];
|
|
@@ -1428,6 +1974,83 @@ function boxFor(root, element) {
|
|
|
1428
1974
|
}
|
|
1429
1975
|
return null;
|
|
1430
1976
|
}
|
|
1977
|
+
/**
|
|
1978
|
+
* An inline element's boxes: the one it has, or where a block in it broke
|
|
1979
|
+
* it (`breakAround`), each piece, in document order.
|
|
1980
|
+
*/
|
|
1981
|
+
function piecesOf(root, first) {
|
|
1982
|
+
// the first piece is cut at its end where there are more
|
|
1983
|
+
if (first.kind !== 'inline' || !(first.cut & 2))
|
|
1984
|
+
return [first];
|
|
1985
|
+
const pieces = [];
|
|
1986
|
+
const stack = [root];
|
|
1987
|
+
while (stack.length) {
|
|
1988
|
+
const box = stack.pop();
|
|
1989
|
+
if (box.el === first.el && box.kind === 'inline')
|
|
1990
|
+
pieces.push(box);
|
|
1991
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1992
|
+
stack.push(box.children[i]);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
return pieces;
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* The bands of one inline box's fragments, or a text's: on the lines of
|
|
1999
|
+
* the block it is laid out in, and no text of a float or a positioned box
|
|
2000
|
+
* inside it — on lines of their own — is one of them (CSSOM View 6.1): a
|
|
2001
|
+
* list item made inline around an absolute link measured as the link.
|
|
2002
|
+
* Each across its text, the inline-blocks and inline boxes' edges it holds
|
|
2003
|
+
* and its own padding and border where it starts or ends there, and as
|
|
2004
|
+
* tall as its border box.
|
|
2005
|
+
*/
|
|
2006
|
+
function inlineBands(root, box, fonts, out) {
|
|
2007
|
+
// its own, apart from any other piece's: `fragmentHeights` makes each of
|
|
2008
|
+
// the bands it is handed as tall as this box on that band's line
|
|
2009
|
+
const bands = [];
|
|
2010
|
+
let block = box.parent;
|
|
2011
|
+
while (block && !block.lines)
|
|
2012
|
+
block = block.parent;
|
|
2013
|
+
if (box.subtreeTextEnd > box.subtreeTextStart) {
|
|
2014
|
+
if (box.kind === 'inline' && block?.lines) {
|
|
2015
|
+
lineBands(block.lines, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands, true);
|
|
2016
|
+
}
|
|
2017
|
+
else {
|
|
2018
|
+
collectBands(root, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
if (box.kind === 'inline') {
|
|
2022
|
+
fragmentReach(box, bands);
|
|
2023
|
+
if (fonts)
|
|
2024
|
+
fragmentHeights(box, bands, fonts);
|
|
2025
|
+
// where `position: relative` on it or an inline box around it moved
|
|
2026
|
+
// it, all of it (CSS 2.1 9.4.3), as `getBoundingClientRect` has it
|
|
2027
|
+
// (CSSOM View 6.1): the bands are where the lines put it, and a link
|
|
2028
|
+
// moved 120px down measured on its line, as wide as from where it was
|
|
2029
|
+
// laid out to where it was drawn
|
|
2030
|
+
const { x, y } = relativeOffsetOf(box);
|
|
2031
|
+
if (x || y) {
|
|
2032
|
+
for (const band of bands) {
|
|
2033
|
+
band.x += x;
|
|
2034
|
+
band.y += y;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
out.push(...bands);
|
|
2039
|
+
}
|
|
2040
|
+
/** How far an inline box and the inline boxes around it moved it by their
|
|
2041
|
+
* relative offsets (`INLINE_OFFSETS`). */
|
|
2042
|
+
function relativeOffsetOf(box) {
|
|
2043
|
+
let x = 0;
|
|
2044
|
+
let y = 0;
|
|
2045
|
+
for (let at = box; at?.kind === 'inline'; at = at.parent) {
|
|
2046
|
+
const offset = INLINE_OFFSETS.get(at);
|
|
2047
|
+
if (offset) {
|
|
2048
|
+
x += offset.x;
|
|
2049
|
+
y += offset.y;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
return { x, y };
|
|
2053
|
+
}
|
|
1431
2054
|
/** Where the document's text goes on after a box with none: the start of
|
|
1432
2055
|
* the next text in document order, or the end of the last before it. */
|
|
1433
2056
|
function textAfter(root, target) {
|
|
@@ -1488,6 +2111,9 @@ function sameRects(a, b) {
|
|
|
1488
2111
|
p.y !== q.y ||
|
|
1489
2112
|
p.width !== q.width ||
|
|
1490
2113
|
p.height !== q.height ||
|
|
2114
|
+
p.fontFamily !== q.fontFamily ||
|
|
2115
|
+
p.fontSize !== q.fontSize ||
|
|
2116
|
+
p.opacity !== q.opacity ||
|
|
1491
2117
|
!sameBare(p.bare, q.bare)) {
|
|
1492
2118
|
return false;
|
|
1493
2119
|
}
|
|
@@ -1503,7 +2129,8 @@ function sameBare(a, b) {
|
|
|
1503
2129
|
a.height === b.height &&
|
|
1504
2130
|
a.color === b.color &&
|
|
1505
2131
|
a.fontFamily === b.fontFamily &&
|
|
1506
|
-
a.fontSize === b.fontSize
|
|
2132
|
+
a.fontSize === b.fontSize &&
|
|
2133
|
+
a.chevron === b.chevron);
|
|
1507
2134
|
}
|
|
1508
2135
|
// --- walks over the laid-out tree -------------------------------------------
|
|
1509
2136
|
/** Distance from a coordinate to an interval, zero inside it. */
|
|
@@ -1768,7 +2395,13 @@ function fragmentHeights(box, bands, fonts) {
|
|
|
1768
2395
|
}
|
|
1769
2396
|
}
|
|
1770
2397
|
/** The bands of a text range on one block's own lines. */
|
|
1771
|
-
function lineBands(lines, from, to, dx, dy, out
|
|
2398
|
+
function lineBands(lines, from, to, dx, dy, out,
|
|
2399
|
+
/** Where the lines put the text, not where a relative offset moved it
|
|
2400
|
+
* (`TEXT_SHIFTS`): what an inline box's own offset is added to. */
|
|
2401
|
+
laidOut = false,
|
|
2402
|
+
/** The rows a highlight covers (`selectionRows`) rather than the line's:
|
|
2403
|
+
* past it where the glyphs are taller than the line. */
|
|
2404
|
+
ink = false) {
|
|
1772
2405
|
// First line whose text can reach `from`, by binary search over the
|
|
1773
2406
|
// sorted text starts; stop at the first line past `to`.
|
|
1774
2407
|
let lo = 0;
|
|
@@ -1794,30 +2427,71 @@ function lineBands(lines, from, to, dx, dy, out) {
|
|
|
1794
2427
|
if (b <= a)
|
|
1795
2428
|
continue;
|
|
1796
2429
|
const offsets = layoutOffsetsOf(text.layout);
|
|
2430
|
+
const moved = laidOut ? (TEXT_SHIFTS.get(text)?.x ?? 0) : 0;
|
|
2431
|
+
// The spaces a line ends on are removed (CSS Text 3, 4.1.2), and
|
|
2432
|
+
// take no room on it: the engine sets them past its line's content,
|
|
2433
|
+
// where a caret after them goes, and a band that took them in made
|
|
2434
|
+
// a link that ends a line a space wider than Chrome's. So the text
|
|
2435
|
+
// a line ends with is measured no further than its content.
|
|
2436
|
+
const ends = endsLine(line, text, natural);
|
|
2437
|
+
const rows = ink
|
|
2438
|
+
? selectionRows(line, text, natural)
|
|
2439
|
+
: { y: line.y, height: line.height };
|
|
1797
2440
|
for (const band of bandsFor(text.layout, natural, offsets, layoutOffsetOf(text, a), layoutOffsetOf(text, b, true))) {
|
|
2441
|
+
let left = band.x;
|
|
2442
|
+
let right = band.x + band.width;
|
|
2443
|
+
if (ends) {
|
|
2444
|
+
left = Math.max(left, natural.x);
|
|
2445
|
+
right = Math.max(left, Math.min(right, natural.x + natural.width));
|
|
2446
|
+
}
|
|
2447
|
+
out.push({
|
|
2448
|
+
x: dx + left + text.drawX - moved,
|
|
2449
|
+
y: dy + rows.y,
|
|
2450
|
+
width: right - left,
|
|
2451
|
+
height: rows.height,
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
// the spaces `pre-wrap` keeps that the line ends on, which have no
|
|
2455
|
+
// run: the engine hung them past the line (`LineText.hung`)
|
|
2456
|
+
for (const space of text.hung ?? []) {
|
|
2457
|
+
const at = documentOffsetOf(text, space.at);
|
|
2458
|
+
if (at < a || at >= b)
|
|
2459
|
+
continue;
|
|
1798
2460
|
out.push({
|
|
1799
|
-
x: dx +
|
|
1800
|
-
y: dy +
|
|
1801
|
-
width:
|
|
1802
|
-
height:
|
|
2461
|
+
x: dx + space.x + text.drawX - moved,
|
|
2462
|
+
y: dy + rows.y,
|
|
2463
|
+
width: space.width,
|
|
2464
|
+
height: rows.height,
|
|
1803
2465
|
});
|
|
1804
2466
|
}
|
|
1805
2467
|
}
|
|
1806
2468
|
}
|
|
1807
2469
|
}
|
|
1808
2470
|
}
|
|
1809
|
-
|
|
2471
|
+
/** Whether a line's text is where the line ends: its last, with nothing
|
|
2472
|
+
* placed after it — an inline-block or an image past its content. */
|
|
2473
|
+
function endsLine(line, text, natural) {
|
|
2474
|
+
if (line.texts[line.texts.length - 1] !== text)
|
|
2475
|
+
return false;
|
|
2476
|
+
const right = text.drawX + natural.x + natural.width;
|
|
2477
|
+
for (const placed of line.atomics) {
|
|
2478
|
+
if (placed.x >= right - 0.01)
|
|
2479
|
+
return false;
|
|
2480
|
+
}
|
|
2481
|
+
return true;
|
|
2482
|
+
}
|
|
2483
|
+
function collectBands(box, from, to, dx, dy, out, ink = false) {
|
|
1810
2484
|
if (box.subtreeTextEnd <= box.subtreeTextStart)
|
|
1811
2485
|
return;
|
|
1812
2486
|
if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to)
|
|
1813
2487
|
return;
|
|
1814
2488
|
if (box.lines)
|
|
1815
|
-
lineBands(box.lines, from, to, dx, dy, out);
|
|
2489
|
+
lineBands(box.lines, from, to, dx, dy, out, false, ink);
|
|
1816
2490
|
// Atomics are ordinary children, reached below.
|
|
1817
2491
|
for (const child of box.children) {
|
|
1818
2492
|
if (child.kind === 'text' || child.kind === 'break')
|
|
1819
2493
|
continue;
|
|
1820
|
-
collectBands(child, from, to, dx, dy, out);
|
|
2494
|
+
collectBands(child, from, to, dx, dy, out, ink);
|
|
1821
2495
|
}
|
|
1822
2496
|
}
|
|
1823
2497
|
/** The layers of a hit test, in the order CSS paints them within a
|
|
@@ -1831,7 +2505,11 @@ const HIT_POSITIONED = 3;
|
|
|
1831
2505
|
function deepestAt(tree, x, y,
|
|
1832
2506
|
/** Told whether what was found was found under text: a run's, rather
|
|
1833
2507
|
* than a box's. */
|
|
1834
|
-
hit
|
|
2508
|
+
hit,
|
|
2509
|
+
/** How far a box fixed to the viewport is drawn from where it was laid
|
|
2510
|
+
* out (`HtmlViewNode._fixedShift`): the point is taken back by it in
|
|
2511
|
+
* there, as paint moved the box by it (`atViewport`). */
|
|
2512
|
+
fixedShift = null) {
|
|
1835
2513
|
const box = tree.root;
|
|
1836
2514
|
let found = box.el;
|
|
1837
2515
|
let foundKey = [];
|
|
@@ -1872,7 +2550,25 @@ hit) {
|
|
|
1872
2550
|
// Zen Garden's archive links are absolute `<li>`s in an `overflow:
|
|
1873
2551
|
// hidden` list with no height of its own, and not one of them could be
|
|
1874
2552
|
// hovered or pressed.
|
|
2553
|
+
let atViewport = false;
|
|
1875
2554
|
const enter = (child, context, clipped) => {
|
|
2555
|
+
if (!fixedShift || atViewport || !fixedToViewport(child)) {
|
|
2556
|
+
enterAt(child, context, clipped);
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
x -= fixedShift.x;
|
|
2560
|
+
y -= fixedShift.y;
|
|
2561
|
+
atViewport = true;
|
|
2562
|
+
try {
|
|
2563
|
+
enterAt(child, context, clipped);
|
|
2564
|
+
}
|
|
2565
|
+
finally {
|
|
2566
|
+
x += fixedShift.x;
|
|
2567
|
+
y += fixedShift.y;
|
|
2568
|
+
atViewport = false;
|
|
2569
|
+
}
|
|
2570
|
+
};
|
|
2571
|
+
const enterAt = (child, context, clipped) => {
|
|
1876
2572
|
const style = child.style;
|
|
1877
2573
|
if (style.position !== 'static' ||
|
|
1878
2574
|
(child.parent?.kind === 'flex' && typeof style.zIndex === 'number')) {
|
|
@@ -1984,6 +2680,32 @@ function compareKeys(a, b) {
|
|
|
1984
2680
|
* is built again instead: a link's subtree is a handful, and a compound
|
|
1985
2681
|
* that matches a container reaches everything in it. */
|
|
1986
2682
|
const HOVER_RESTYLE_LIMIT = 300;
|
|
2683
|
+
/** How long the content under a still pointer has to have stopped moving
|
|
2684
|
+
* before the hover is asked for again (`HtmlViewNode._holdHover`): a
|
|
2685
|
+
* tenth of a second, which is what WebKit waits after a scroll before it
|
|
2686
|
+
* sends the mouse move that updates its own. */
|
|
2687
|
+
const HOVER_REST_MS = 100;
|
|
2688
|
+
const timers = globalThis;
|
|
2689
|
+
/**
|
|
2690
|
+
* The clock a held hover waits on. Through `globalThis` because `src/`
|
|
2691
|
+
* compiles with `types: []`, and unref'd where the runtime allows it: a
|
|
2692
|
+
* timer of a document's must not keep a process alive that is otherwise
|
|
2693
|
+
* done. One object, exported from this module though not from the package,
|
|
2694
|
+
* so a test can hold it and say when the rest is over (`test/held-clock.ts`).
|
|
2695
|
+
*/
|
|
2696
|
+
export const hoverClock = {
|
|
2697
|
+
now() {
|
|
2698
|
+
return Date.now();
|
|
2699
|
+
},
|
|
2700
|
+
arm(step, ms) {
|
|
2701
|
+
const handle = timers.setTimeout?.(step, ms) ?? null;
|
|
2702
|
+
handle?.unref?.();
|
|
2703
|
+
return handle;
|
|
2704
|
+
},
|
|
2705
|
+
disarm(handle) {
|
|
2706
|
+
timers.clearTimeout?.(handle);
|
|
2707
|
+
},
|
|
2708
|
+
};
|
|
1987
2709
|
/**
|
|
1988
2710
|
* The computed properties a pointer move may change in place: ink, which
|
|
1989
2711
|
* moves nothing. A background colour is drawn inside the box it colours
|
|
@@ -2030,21 +2752,127 @@ const FACE_FIELDS = [
|
|
|
2030
2752
|
'letterSpacing',
|
|
2031
2753
|
'features',
|
|
2032
2754
|
];
|
|
2033
|
-
/**
|
|
2034
|
-
*
|
|
2035
|
-
|
|
2036
|
-
|
|
2755
|
+
/** An element's `::before` and `::after`, each with its bit in the set
|
|
2756
|
+
* of the ones a restyle found boxes of (`_hoverInPlace`). */
|
|
2757
|
+
const GENERATED_BITS = [
|
|
2758
|
+
['before', 1],
|
|
2759
|
+
['after', 2],
|
|
2760
|
+
];
|
|
2761
|
+
/** The replaced boxes that are controls (`BoxTree.controls`, `controls.ts`):
|
|
2762
|
+
* a widget mounted beside the document, or a button whose press it reports. */
|
|
2763
|
+
const WIDGETS = new Set([
|
|
2764
|
+
'input',
|
|
2765
|
+
'textarea',
|
|
2766
|
+
'select',
|
|
2767
|
+
'button',
|
|
2768
|
+
'checkbox',
|
|
2769
|
+
'radio',
|
|
2770
|
+
]);
|
|
2771
|
+
/** What only moves how far a box's ink reaches: a shadow, and an outline's
|
|
2772
|
+
* size, which take no room (CSS Backgrounds 3, 7.1; CSS UI 4, 3). */
|
|
2773
|
+
const INK_REACH = new Set([
|
|
2774
|
+
'boxShadow',
|
|
2775
|
+
'outlineStyle',
|
|
2776
|
+
'outlineWidth',
|
|
2777
|
+
'outlineOffset',
|
|
2778
|
+
]);
|
|
2779
|
+
/**
|
|
2780
|
+
* How two styles of one element differ, for a restyle in place: null where
|
|
2781
|
+
* they do not, a `HoverChange` where every difference is ink, how far ink
|
|
2782
|
+
* reaches, a `z-index` that stays a stacking context's, or a translation —
|
|
2783
|
+
* none of which moves anything but the box and what it holds (CSS
|
|
2784
|
+
* Transforms 1: a transform does not affect layout) — and false where
|
|
2785
|
+
* anything else differs.
|
|
2786
|
+
*/
|
|
2787
|
+
function hoverChange(was, now) {
|
|
2037
2788
|
const a = was;
|
|
2038
2789
|
const b = now;
|
|
2039
|
-
let
|
|
2790
|
+
let change = null;
|
|
2040
2791
|
for (const key in b) {
|
|
2041
|
-
|
|
2792
|
+
// the sets are shared by what made them (`Cascade._customFor`): one
|
|
2793
|
+
// object where they are the same
|
|
2794
|
+
if (key === 'custom') {
|
|
2795
|
+
if (a[key] !== b[key]) {
|
|
2796
|
+
change ??= { ink: false, reach: false, order: false, move: false };
|
|
2797
|
+
}
|
|
2798
|
+
continue;
|
|
2799
|
+
}
|
|
2800
|
+
if (sameValue(a[key], b[key]))
|
|
2801
|
+
continue;
|
|
2802
|
+
change ??= { ink: true, reach: false, order: false, move: false };
|
|
2803
|
+
change.ink = true;
|
|
2804
|
+
if (PAINT_ONLY.has(key))
|
|
2805
|
+
continue;
|
|
2806
|
+
if (INK_REACH.has(key)) {
|
|
2807
|
+
change.reach = true;
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
if (key === 'zIndex') {
|
|
2811
|
+
// one stacking context either way, with nothing hoisted below the
|
|
2812
|
+
// flow (`hoistNegative`): only its place among its layers changes
|
|
2813
|
+
const z0 = a[key];
|
|
2814
|
+
const z1 = b[key];
|
|
2815
|
+
if (typeof z0 !== 'number' || typeof z1 !== 'number')
|
|
2816
|
+
return false;
|
|
2817
|
+
if (z0 < 0 || z1 < 0)
|
|
2818
|
+
return false;
|
|
2819
|
+
change.order = true;
|
|
2820
|
+
continue;
|
|
2821
|
+
}
|
|
2822
|
+
if (key === 'transformTranslate' || key === 'translate') {
|
|
2823
|
+
change.move = true;
|
|
2042
2824
|
continue;
|
|
2043
|
-
|
|
2825
|
+
}
|
|
2826
|
+
return false;
|
|
2827
|
+
}
|
|
2828
|
+
return change;
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Whether a box can take a new translation where it is: a box of its own
|
|
2832
|
+
* rather than an inline one, whose text is what moves; and the same
|
|
2833
|
+
* containing block and stacking context for what is in it, which a
|
|
2834
|
+
* transform makes a box (CSS Transforms 1, 2) — so either transformed
|
|
2835
|
+
* before and after, or already positioned with a `z-index`, and holding
|
|
2836
|
+
* nothing fixed, which only a transform takes in.
|
|
2837
|
+
*/
|
|
2838
|
+
function movable(box, was, now) {
|
|
2839
|
+
if (box.kind === 'inline')
|
|
2840
|
+
return false;
|
|
2841
|
+
if (transformed(was) === transformed(now))
|
|
2842
|
+
return true;
|
|
2843
|
+
if (was.position === 'static')
|
|
2844
|
+
return false;
|
|
2845
|
+
if (typeof was.zIndex !== 'number' || typeof now.zIndex !== 'number') {
|
|
2846
|
+
return false;
|
|
2847
|
+
}
|
|
2848
|
+
const stack = [...box.children];
|
|
2849
|
+
while (stack.length) {
|
|
2850
|
+
const at = stack.pop();
|
|
2851
|
+
if (at.style.position === 'fixed')
|
|
2044
2852
|
return false;
|
|
2045
|
-
|
|
2853
|
+
for (const child of at.children)
|
|
2854
|
+
stack.push(child);
|
|
2046
2855
|
}
|
|
2047
|
-
return
|
|
2856
|
+
return true;
|
|
2857
|
+
}
|
|
2858
|
+
/** Where a box draws, in document coordinates: its ink bounds, or for an
|
|
2859
|
+
* inline box or a run of text, which are drawn on their block's lines and
|
|
2860
|
+
* have none of their own, the block's. */
|
|
2861
|
+
function inkOf(box) {
|
|
2862
|
+
let at = box;
|
|
2863
|
+
while (at && (at.kind === 'text' || at.kind === 'break' || !hasRect(at))) {
|
|
2864
|
+
at = at.parent;
|
|
2865
|
+
}
|
|
2866
|
+
if (!at || at.boundsY === Infinity)
|
|
2867
|
+
return null;
|
|
2868
|
+
if (!(at.boundsWidth > 0 && at.boundsHeight > 0))
|
|
2869
|
+
return null;
|
|
2870
|
+
return {
|
|
2871
|
+
x: at.boundsX,
|
|
2872
|
+
y: at.boundsY,
|
|
2873
|
+
width: at.boundsWidth,
|
|
2874
|
+
height: at.boundsHeight,
|
|
2875
|
+
};
|
|
2048
2876
|
}
|
|
2049
2877
|
/** Structural equality for a computed value: a number, a string, or the
|
|
2050
2878
|
* plain objects and arrays a length or a shadow list is. */
|
|
@@ -2068,32 +2896,23 @@ function sameValue(x, y) {
|
|
|
2068
2896
|
return false;
|
|
2069
2897
|
return true;
|
|
2070
2898
|
}
|
|
2071
|
-
/** The element an anonymous box takes its style from: its nearest
|
|
2072
|
-
* ancestor's that has one. */
|
|
2073
|
-
function nearestElement(box) {
|
|
2074
|
-
for (let at = box.parent; at; at = at.parent)
|
|
2075
|
-
if (at.el)
|
|
2076
|
-
return at.el;
|
|
2077
|
-
return null;
|
|
2078
|
-
}
|
|
2079
2899
|
/**
|
|
2080
|
-
* `text`'s layout made again with the ink of the
|
|
2081
|
-
* restyled: from the runs it was made from, each of a
|
|
2900
|
+
* `text`'s layout made again with the ink of the boxes a pointer move
|
|
2901
|
+
* restyled: from the runs it was made from, each of a restyled text box's
|
|
2082
2902
|
* given the ink its new style makes (`runFor`) and nothing else. Null where
|
|
2083
2903
|
* none of its runs is theirs; false where that cannot be told — a run this
|
|
2084
2904
|
* cannot place in the document, one some other pass inked (a first line's),
|
|
2085
2905
|
* or a layout that comes out another shape.
|
|
2086
2906
|
*/
|
|
2087
|
-
function reinked(text, layouts,
|
|
2907
|
+
function reinked(text, layouts, next) {
|
|
2088
2908
|
const spans = text.spans;
|
|
2089
|
-
const ownerAt = (offset) => spans.boxAt ? (spans.boxAt(offset)?.el ?? null) : null;
|
|
2090
2909
|
const inputs = layouts.inputsOf(text.layout);
|
|
2091
2910
|
if (!inputs || !spans.boxAt) {
|
|
2092
2911
|
// not one this could make again: none of its text may be theirs
|
|
2093
2912
|
for (const line of text.layout.lines) {
|
|
2094
2913
|
for (const run of line.runs) {
|
|
2095
|
-
const
|
|
2096
|
-
if (!spans.boxAt || (
|
|
2914
|
+
const box = spans.boxAt?.(run.start);
|
|
2915
|
+
if (!spans.boxAt || (box && next.has(box)))
|
|
2097
2916
|
return false;
|
|
2098
2917
|
}
|
|
2099
2918
|
}
|
|
@@ -2105,32 +2924,32 @@ function reinked(text, layouts, changed, styles) {
|
|
|
2105
2924
|
for (let i = 0; i < content.length; i += 1) {
|
|
2106
2925
|
const run = content[i];
|
|
2107
2926
|
const length = run.text.length;
|
|
2108
|
-
const
|
|
2109
|
-
const style =
|
|
2110
|
-
if (
|
|
2111
|
-
if (
|
|
2927
|
+
const box = length ? spans.boxAt(offset) : null;
|
|
2928
|
+
const style = box ? next.get(box) : undefined;
|
|
2929
|
+
if (box && style) {
|
|
2930
|
+
if (spans.boxAt(offset + length - 1) !== box)
|
|
2112
2931
|
return false;
|
|
2113
|
-
const was = runFor(run.text,
|
|
2932
|
+
const was = runFor(run.text, box.style);
|
|
2114
2933
|
const now = runFor(run.text, style);
|
|
2115
2934
|
for (const f of FACE_FIELDS)
|
|
2116
2935
|
if (!sameValue(was[f], now[f]))
|
|
2117
2936
|
return false;
|
|
2118
|
-
let
|
|
2937
|
+
let inked = null;
|
|
2119
2938
|
for (const f of INK_FIELDS) {
|
|
2120
2939
|
// a run that is not what its style made — a first line's colour
|
|
2121
2940
|
if (!sameValue(run[f], was[f]))
|
|
2122
2941
|
return false;
|
|
2123
2942
|
if (sameValue(was[f], now[f]))
|
|
2124
2943
|
continue;
|
|
2125
|
-
|
|
2944
|
+
inked ??= { ...run };
|
|
2126
2945
|
if (now[f] === undefined)
|
|
2127
|
-
delete
|
|
2946
|
+
delete inked[f];
|
|
2128
2947
|
else
|
|
2129
|
-
|
|
2948
|
+
inked[f] = now[f];
|
|
2130
2949
|
}
|
|
2131
|
-
if (
|
|
2950
|
+
if (inked) {
|
|
2132
2951
|
runs ??= content.slice();
|
|
2133
|
-
runs[i] =
|
|
2952
|
+
runs[i] = inked;
|
|
2134
2953
|
}
|
|
2135
2954
|
}
|
|
2136
2955
|
offset += length;
|