@react-x11/components 0.12.0 → 0.14.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/html/controls.d.ts +41 -5
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +85 -57
- 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 +545 -126
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +18 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +27 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +47 -6
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +336 -30
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +142 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +563 -147
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/transform.d.ts +85 -0
- package/dist/html/css/transform.d.ts.map +1 -0
- package/dist/html/css/transform.js +425 -0
- package/dist/html/css/transform.js.map +1 -0
- 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 -9
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +4 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +3 -1
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +78 -4
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +373 -19
- 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 -301
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/axes.d.ts +14 -0
- package/dist/html/layout/axes.d.ts.map +1 -0
- package/dist/html/layout/axes.js +28 -0
- package/dist/html/layout/axes.js.map +1 -0
- package/dist/html/layout/block.d.ts +93 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +393 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +90 -2
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +74 -10
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +1 -1
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +6 -0
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +90 -18
- package/dist/html/layout/css-grid.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +574 -84
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +3 -0
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +154 -54
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/multicol.d.ts +42 -0
- package/dist/html/layout/multicol.d.ts.map +1 -0
- package/dist/html/layout/multicol.js +897 -0
- package/dist/html/layout/multicol.js.map +1 -0
- package/dist/html/layout/shaping.d.ts +7 -0
- package/dist/html/layout/shaping.d.ts.map +1 -1
- package/dist/html/layout/shaping.js +25 -20
- package/dist/html/layout/shaping.js.map +1 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +62 -18
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +90 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +771 -186
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +101 -5
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1502 -160
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +15 -2
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +249 -17
- package/dist/html/svg.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 +700 -0
- package/dist/html/widgets.js.map +1 -0
- package/dist/html/woff2.d.ts +14 -0
- package/dist/html/woff2.d.ts.map +1 -0
- package/dist/html/woff2.js +608 -0
- package/dist/html/woff2.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/maps/controller.d.ts +17 -1
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +27 -7
- package/dist/maps/controller.js.map +1 -1
- package/dist/richtext/node.d.ts +8 -1
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +5 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +25 -6
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +240 -24
- package/dist/richtext/runs.js.map +1 -1
- package/package.json +2 -2
- package/src/html/controls.ts +114 -51
- package/src/html/css/cascade.ts +614 -140
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +388 -33
- package/src/html/css/style.ts +688 -150
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +77 -10
- package/src/html/css/values.ts +4 -0
- package/src/html/dom.ts +3 -1
- package/src/html/fonts.ts +417 -24
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -338
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +447 -37
- package/src/html/layout/boxes.ts +143 -14
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +95 -22
- package/src/html/layout/flex.ts +675 -78
- package/src/html/layout/inline.ts +159 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +62 -19
- package/src/html/node.ts +798 -172
- package/src/html/paint.ts +1742 -241
- package/src/html/svg.ts +277 -19
- package/src/html/widgets.ts +821 -0
- package/src/html/woff2.ts +612 -0
- package/src/index.ts +1 -0
- package/src/maps/controller.ts +28 -7
- package/src/richtext/node.ts +13 -2
- package/src/richtext/runs.ts +313 -35
package/src/html/node.ts
CHANGED
|
@@ -50,22 +50,33 @@ 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
|
-
import type { Stylesheet } from './css/parse.js';
|
|
62
|
+
import type { MediaCondition, Stylesheet } from './css/parse.js';
|
|
57
63
|
import { uaStylesheet } from './css/ua.js';
|
|
58
64
|
import type { ComputedStyle, RootLook } from './css/style.js';
|
|
59
65
|
import {
|
|
60
66
|
BOX_RAISES,
|
|
61
67
|
buildBoxes,
|
|
62
68
|
CONTENT_IMAGES,
|
|
69
|
+
CLEARED_FROM,
|
|
70
|
+
COLUMN_LINES,
|
|
71
|
+
COLUMN_PIECES,
|
|
72
|
+
COLUMN_ROWS,
|
|
63
73
|
CUT_BLOCKS,
|
|
64
74
|
GENERATED_FROM,
|
|
65
75
|
INLINE_OFFSETS,
|
|
66
76
|
LINE_BOX_RAISES,
|
|
67
77
|
SHIFTED_LINES,
|
|
68
78
|
TEXT_SHIFTS,
|
|
79
|
+
columned,
|
|
69
80
|
} from './layout/boxes.js';
|
|
70
81
|
import type {
|
|
71
82
|
Box,
|
|
@@ -75,8 +86,10 @@ import type {
|
|
|
75
86
|
ReplacedKind,
|
|
76
87
|
TextLayoutLike,
|
|
77
88
|
} from './layout/boxes.js';
|
|
78
|
-
import { layoutDocument,
|
|
89
|
+
import { layoutDocument, placedMatrix, retranslate } from './layout/block.js';
|
|
90
|
+
import { invert, mapPoint, mapRect, transformed } from './css/transform.js';
|
|
79
91
|
import { TextLayoutCache } from './layout/cache.js';
|
|
92
|
+
import { weightAxes } from './layout/axes.js';
|
|
80
93
|
import { shapingSafe } from './layout/shaping.js';
|
|
81
94
|
import { SurfaceCache, newSurface } from './surfaces.js';
|
|
82
95
|
import type { SurfaceLike } from './surfaces.js';
|
|
@@ -102,9 +115,16 @@ import {
|
|
|
102
115
|
hoistNegative,
|
|
103
116
|
holds,
|
|
104
117
|
holdsAbsolute,
|
|
118
|
+
inClip,
|
|
119
|
+
inClipPath,
|
|
120
|
+
onLine,
|
|
121
|
+
ownBounds,
|
|
105
122
|
paintDocument,
|
|
123
|
+
pathClips,
|
|
106
124
|
queryChildIndex,
|
|
125
|
+
selectionRows,
|
|
107
126
|
stackLayers,
|
|
127
|
+
stacksLayers,
|
|
108
128
|
} from './paint.js';
|
|
109
129
|
import type { PaintContext } from './paint.js';
|
|
110
130
|
import { controlRectsOf, measureControl } from './controls.js';
|
|
@@ -249,6 +269,8 @@ export class HtmlViewNode extends Node {
|
|
|
249
269
|
* that read it (`vw`, `vh`). */
|
|
250
270
|
private _styledWidth = -1;
|
|
251
271
|
private _styledHeight = -1;
|
|
272
|
+
/** The cascade the box tree's styles were computed by. */
|
|
273
|
+
private _styledWith: Cascade | null = null;
|
|
252
274
|
private _documentHeight = 0;
|
|
253
275
|
private _documentWidth = 0;
|
|
254
276
|
/** The viewport height the document was last laid out under, and whether
|
|
@@ -266,6 +288,20 @@ export class HtmlViewNode extends Node {
|
|
|
266
288
|
* the text carries surrogate pairs. null until checked. */
|
|
267
289
|
private _pointsAreUnits: boolean | null = null;
|
|
268
290
|
private _hovered: Element[] = [];
|
|
291
|
+
/** Where the pointer last moved to, in logical window pixels, while it
|
|
292
|
+
* is over the element (`defaultMouseMove`). */
|
|
293
|
+
private _pointerAt: { x: number; y: number } | null = null;
|
|
294
|
+
/** A hover held until the content under a still pointer stops moving:
|
|
295
|
+
* the timer, and when the content last moved (`_holdHover`). */
|
|
296
|
+
private _heldHover: unknown = null;
|
|
297
|
+
private _heldSince = 0;
|
|
298
|
+
/**
|
|
299
|
+
* The elements the build of the boxes that is due has to style again,
|
|
300
|
+
* where every change asking for it said which (`_invalidate`), and null
|
|
301
|
+
* where any element's style may have changed — a new cascade, a viewport
|
|
302
|
+
* a `vw` reads, a face that arrived.
|
|
303
|
+
*/
|
|
304
|
+
private _restyleOnly: Set<Element> | null = null;
|
|
269
305
|
private _scriptsSeen = new WeakSet<Element>();
|
|
270
306
|
private _controls: ControlRect[] = [];
|
|
271
307
|
private _reportedDomRevision = -1;
|
|
@@ -363,6 +399,7 @@ export class HtmlViewNode extends Node {
|
|
|
363
399
|
controlPadY: look.controlPadY * s,
|
|
364
400
|
controlBorder: look.controlBorder * s,
|
|
365
401
|
controlRadius: look.controlRadius * s,
|
|
402
|
+
controlFontSize: (look.controlFontSize ?? look.fontSize) * s,
|
|
366
403
|
};
|
|
367
404
|
this._deviceLookFor = look;
|
|
368
405
|
this._deviceLookAt = s;
|
|
@@ -372,7 +409,26 @@ export class HtmlViewNode extends Node {
|
|
|
372
409
|
|
|
373
410
|
// --- the pipeline ---------------------------------------------------------
|
|
374
411
|
|
|
375
|
-
|
|
412
|
+
/**
|
|
413
|
+
* `restyle` with `Stale.Boxes`: the elements whose styles the change
|
|
414
|
+
* could have touched, where that is all of it — a pointer move's reach
|
|
415
|
+
* (`_hoverReach`). The build it asks for keeps every other element's
|
|
416
|
+
* (`_restyleOnly`).
|
|
417
|
+
*/
|
|
418
|
+
private _invalidate(
|
|
419
|
+
stale: Stale,
|
|
420
|
+
restyle: ReadonlySet<Element> | null = null,
|
|
421
|
+
): void {
|
|
422
|
+
if (stale >= Stale.Boxes) {
|
|
423
|
+
const only = stale === Stale.Boxes ? restyle : null;
|
|
424
|
+
if (this._stale < Stale.Boxes) {
|
|
425
|
+
this._restyleOnly = only ? new Set(only) : null;
|
|
426
|
+
} else if (this._restyleOnly && only) {
|
|
427
|
+
for (const el of only) this._restyleOnly.add(el);
|
|
428
|
+
} else {
|
|
429
|
+
this._restyleOnly = null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
376
432
|
if (stale > this._stale) this._stale = stale;
|
|
377
433
|
if (stale >= Stale.Layout) this.invalidateMeasure('content');
|
|
378
434
|
this.invalidate(stale >= Stale.Layout, this, 'props');
|
|
@@ -578,7 +634,10 @@ export class HtmlViewNode extends Node {
|
|
|
578
634
|
kept.cascade.viewportHeight = this._viewportHeight();
|
|
579
635
|
faces = kept.faces;
|
|
580
636
|
} else {
|
|
581
|
-
|
|
637
|
+
// the UA sheet's `px` are CSS pixels, which the cascade scales like an
|
|
638
|
+
// author's, so it is written from the look in CSS pixels: from the
|
|
639
|
+
// device one, a control's size and a button's chrome were doubled at 2x
|
|
640
|
+
const sheets: Stylesheet[] = [uaStylesheet(props.look)];
|
|
582
641
|
const imports: ImportRead[][] = [];
|
|
583
642
|
faces = [];
|
|
584
643
|
let order = 0;
|
|
@@ -633,10 +692,17 @@ export class HtmlViewNode extends Node {
|
|
|
633
692
|
// The faces this width and scheme declare: a `@font-face` may sit in a
|
|
634
693
|
// `@media` block like any rule.
|
|
635
694
|
const cssWidth = width / this._scale;
|
|
695
|
+
const cssHeight = this._viewportHeight() / this._scale;
|
|
636
696
|
this._webFonts.setFallback(look.fontFamily);
|
|
637
697
|
this._webFonts.setFaces(
|
|
638
698
|
faces.filter((f) =>
|
|
639
|
-
mediaMatches(
|
|
699
|
+
mediaMatches(
|
|
700
|
+
f.rule.media,
|
|
701
|
+
cssWidth,
|
|
702
|
+
look.colorScheme,
|
|
703
|
+
cssHeight,
|
|
704
|
+
this._scale,
|
|
705
|
+
),
|
|
640
706
|
),
|
|
641
707
|
);
|
|
642
708
|
this._cascade.setPointer({
|
|
@@ -662,15 +728,23 @@ export class HtmlViewNode extends Node {
|
|
|
662
728
|
place: (sheet: Stylesheet, element: Element) => void,
|
|
663
729
|
depth = 0,
|
|
664
730
|
chain: Set<string> = new Set(),
|
|
731
|
+
/** The media queries `sheet` is itself under, where it was imported
|
|
732
|
+
* under some. */
|
|
733
|
+
under: MediaCondition[][] | null = null,
|
|
665
734
|
): void {
|
|
666
|
-
for (
|
|
735
|
+
for (let i = 0; i < sheet.imports.length; i += 1) {
|
|
736
|
+
const url = sheet.imports[i];
|
|
737
|
+
// what it imports is under its import's media queries, and under
|
|
738
|
+
// the ones the sheet importing it is under
|
|
739
|
+
const conditions = sheet.importConditions[i];
|
|
740
|
+
const media = conditions ? [...(under ?? []), conditions] : under;
|
|
667
741
|
this._resources.request({ url, kind: 'stylesheet', element });
|
|
668
742
|
const fetched = this._resources.stylesheet(url, [encoding]);
|
|
669
743
|
const base = this._resources.sheetBase(url);
|
|
670
744
|
seen.push({ url, text: fetched?.text ?? null, base });
|
|
671
745
|
const key = base ?? url;
|
|
672
746
|
if (!fetched || depth >= MAX_IMPORT_DEPTH || chain.has(key)) continue;
|
|
673
|
-
const imported = parseStylesheet(fetched.text, 0, layers, base);
|
|
747
|
+
const imported = parseStylesheet(fetched.text, 0, layers, base, media);
|
|
674
748
|
chain.add(key);
|
|
675
749
|
this._placeImports(
|
|
676
750
|
imported,
|
|
@@ -681,6 +755,7 @@ export class HtmlViewNode extends Node {
|
|
|
681
755
|
place,
|
|
682
756
|
depth + 1,
|
|
683
757
|
chain,
|
|
758
|
+
media,
|
|
684
759
|
);
|
|
685
760
|
chain.delete(key);
|
|
686
761
|
place(imported, element);
|
|
@@ -795,11 +870,22 @@ export class HtmlViewNode extends Node {
|
|
|
795
870
|
return height && height > 0 ? height : 600;
|
|
796
871
|
}
|
|
797
872
|
|
|
873
|
+
/** The engine this document lays text out with, and the one it is over:
|
|
874
|
+
* one object while the engine is, since caches are kept by it. */
|
|
875
|
+
private _engine: { over: FontsLike; fonts: FontsLike } | null = null;
|
|
876
|
+
|
|
798
877
|
private _fonts(): FontsLike | null {
|
|
799
878
|
const fonts = (this.app as { fonts?: FontsLike } | null)?.fonts;
|
|
879
|
+
if (!fonts) return null;
|
|
800
880
|
// a face the engine cannot shape from costs its characters, not the
|
|
801
881
|
// document (`shaping.ts`)
|
|
802
|
-
|
|
882
|
+
const safe = shapingSafe(fonts);
|
|
883
|
+
if (this._engine?.over !== safe) {
|
|
884
|
+
// and a variable face of the document's is set at the weight its
|
|
885
|
+
// rule has for a style's (`axes.ts`)
|
|
886
|
+
this._engine = { over: safe, fonts: weightAxes(safe, this._webFonts) };
|
|
887
|
+
}
|
|
888
|
+
return this._engine.fonts;
|
|
803
889
|
}
|
|
804
890
|
|
|
805
891
|
/** The text layouts the last pass made, for this one to reuse. */
|
|
@@ -893,15 +979,21 @@ export class HtmlViewNode extends Node {
|
|
|
893
979
|
const wasWidth = this._documentWidth;
|
|
894
980
|
const wasHeight = this._documentHeight;
|
|
895
981
|
|
|
982
|
+
// the styles a build may keep (`_restyleOnly`), unless something
|
|
983
|
+
// found below changes them all
|
|
984
|
+
let restyleOnly = this._stale === Stale.Boxes ? this._restyleOnly : null;
|
|
985
|
+
this._restyleOnly = null;
|
|
896
986
|
if (this._stale >= Stale.Style || !this._cascade) {
|
|
897
987
|
this._restyle(target);
|
|
898
988
|
this._stale = Math.max(this._stale, Stale.Boxes) as Stale;
|
|
989
|
+
restyleOnly = null;
|
|
899
990
|
} else if (this._cascade.mediaBand(target) !== this._mediaBand) {
|
|
900
991
|
// A resize that crossed a `@media` breakpoint is the one resize that
|
|
901
992
|
// does have to restyle. Knowing which resizes those are is why the
|
|
902
993
|
// breakpoints are collected at parse time.
|
|
903
994
|
this._restyle(target);
|
|
904
995
|
this._stale = Math.max(this._stale, Stale.Boxes) as Stale;
|
|
996
|
+
restyleOnly = null;
|
|
905
997
|
}
|
|
906
998
|
|
|
907
999
|
const cascade = this._cascade;
|
|
@@ -913,18 +1005,27 @@ export class HtmlViewNode extends Node {
|
|
|
913
1005
|
// again, where some style reads the side that moved. A document that
|
|
914
1006
|
// reads neither — most — goes on skipping the cascade on a resize.
|
|
915
1007
|
if (
|
|
916
|
-
|
|
917
|
-
(
|
|
918
|
-
(cascade.readsViewportHeight && this._styledHeight !== viewport))
|
|
1008
|
+
(cascade.readsViewportWidth && this._styledWidth !== target) ||
|
|
1009
|
+
(cascade.readsViewportHeight && this._styledHeight !== viewport)
|
|
919
1010
|
) {
|
|
920
|
-
this._stale = Stale.Boxes;
|
|
1011
|
+
if (this._stale < Stale.Boxes) this._stale = Stale.Boxes;
|
|
1012
|
+
restyleOnly = null;
|
|
921
1013
|
}
|
|
922
1014
|
|
|
923
1015
|
if (this._stale >= Stale.Boxes || !this._tree) {
|
|
924
1016
|
const look = this._deviceLook();
|
|
1017
|
+
// A build a pointer move asked for styles the elements the move
|
|
1018
|
+
// reached, and takes every other element's from the tree it replaces
|
|
1019
|
+
// (`Cascade.beginSharing`): the first build only, since a second is
|
|
1020
|
+
// one the first found a reason for — a face, an image's size.
|
|
1021
|
+
let kept: KeptStyles | null =
|
|
1022
|
+
restyleOnly && this._tree && this._styledWith === cascade
|
|
1023
|
+
? { styles: this._tree.styles, restyle: restyleOnly }
|
|
1024
|
+
: null;
|
|
925
1025
|
const build = () =>
|
|
926
1026
|
buildBoxes(this._source.document, {
|
|
927
1027
|
cascade,
|
|
1028
|
+
kept,
|
|
928
1029
|
scale: this._scale,
|
|
929
1030
|
imageSize: (el) => this._resources.imageSize(imageUrlOf(el) ?? ''),
|
|
930
1031
|
urlSize: (url) => this._resources.imageSize(url),
|
|
@@ -932,6 +1033,8 @@ export class HtmlViewNode extends Node {
|
|
|
932
1033
|
measureControl(el, kind, style, this._fonts(), look),
|
|
933
1034
|
});
|
|
934
1035
|
this._tree = build();
|
|
1036
|
+
kept = null;
|
|
1037
|
+
this._styledWith = cascade;
|
|
935
1038
|
this._styledWidth = target;
|
|
936
1039
|
this._styledHeight = viewport;
|
|
937
1040
|
this._requestBackgrounds(this._tree);
|
|
@@ -1001,7 +1104,7 @@ export class HtmlViewNode extends Node {
|
|
|
1001
1104
|
// The boxes are device pixels; each rect becomes the style of a widget
|
|
1002
1105
|
// mounted beside this element, and a style is logical.
|
|
1003
1106
|
const s = this._scale;
|
|
1004
|
-
const rects = controlRectsOf(tree).map((r) =>
|
|
1107
|
+
const rects = controlRectsOf(tree, s).map((r) =>
|
|
1005
1108
|
s === 1
|
|
1006
1109
|
? r
|
|
1007
1110
|
: {
|
|
@@ -1010,6 +1113,15 @@ export class HtmlViewNode extends Node {
|
|
|
1010
1113
|
y: r.y / s,
|
|
1011
1114
|
width: r.width / s,
|
|
1012
1115
|
height: r.height / s,
|
|
1116
|
+
fontSize: r.fontSize / s,
|
|
1117
|
+
...(r.clip && {
|
|
1118
|
+
clip: {
|
|
1119
|
+
x: r.clip.x / s,
|
|
1120
|
+
y: r.clip.y / s,
|
|
1121
|
+
width: r.clip.width / s,
|
|
1122
|
+
height: r.clip.height / s,
|
|
1123
|
+
},
|
|
1124
|
+
}),
|
|
1013
1125
|
...(r.bare && {
|
|
1014
1126
|
bare: {
|
|
1015
1127
|
...r.bare,
|
|
@@ -1112,6 +1224,7 @@ export class HtmlViewNode extends Node {
|
|
|
1112
1224
|
}
|
|
1113
1225
|
|
|
1114
1226
|
override destroySubtree(): void {
|
|
1227
|
+
this._dropHeldHover();
|
|
1115
1228
|
this._resources.destroy();
|
|
1116
1229
|
this._webFonts.destroy();
|
|
1117
1230
|
this._source.destroy();
|
|
@@ -1209,7 +1322,8 @@ export class HtmlViewNode extends Node {
|
|
|
1209
1322
|
const to = this._toUnits(end);
|
|
1210
1323
|
if (to <= from) return [];
|
|
1211
1324
|
const out: Rect[] = [];
|
|
1212
|
-
|
|
1325
|
+
// the rows the highlight paints, over glyphs taller than their line
|
|
1326
|
+
collectBands(tree.root, from, to, this.abs.x, this.abs.y, out, true);
|
|
1213
1327
|
return out;
|
|
1214
1328
|
}
|
|
1215
1329
|
|
|
@@ -1280,11 +1394,16 @@ export class HtmlViewNode extends Node {
|
|
|
1280
1394
|
for (const block of (box.cut ? CUT_BLOCKS.get(box) : null) ?? []) {
|
|
1281
1395
|
const around = block.parent;
|
|
1282
1396
|
if (!around) continue;
|
|
1397
|
+
// from where clearance moved it down from, as Blink's line around
|
|
1398
|
+
// a block in an inline box starts there: the Zen Garden's 214
|
|
1399
|
+
// makes its content an inline box of floats and a footer that
|
|
1400
|
+
// clears them, and Chrome measures it from the floats' top
|
|
1401
|
+
const top = Math.min(block.y, CLEARED_FROM.get(block) ?? block.y);
|
|
1283
1402
|
bands.push({
|
|
1284
1403
|
x: around.contentX,
|
|
1285
|
-
y:
|
|
1404
|
+
y: top,
|
|
1286
1405
|
width: around.contentWidth,
|
|
1287
|
-
height: block.height,
|
|
1406
|
+
height: block.y + block.height - top,
|
|
1288
1407
|
});
|
|
1289
1408
|
}
|
|
1290
1409
|
for (const band of bands) rect = rect ? unionRect(rect, band) : band;
|
|
@@ -1317,6 +1436,9 @@ export class HtmlViewNode extends Node {
|
|
|
1317
1436
|
rect = { x: caret.x, y: caret.y, width: 0, height: caret.height };
|
|
1318
1437
|
}
|
|
1319
1438
|
}
|
|
1439
|
+
// where it is drawn: the rectangle around it, turned and scaled as the
|
|
1440
|
+
// boxes it is in are, and itself (`getBoundingClientRect`, CSSOM View)
|
|
1441
|
+
rect = throughTransforms(rect, box);
|
|
1320
1442
|
const s = this._scale;
|
|
1321
1443
|
return {
|
|
1322
1444
|
x: rect.x / s,
|
|
@@ -1381,6 +1503,7 @@ export class HtmlViewNode extends Node {
|
|
|
1381
1503
|
* contains a `:hover` rule.
|
|
1382
1504
|
*/
|
|
1383
1505
|
setHover(x: number, y: number): boolean {
|
|
1506
|
+
this._dropHeldHover();
|
|
1384
1507
|
const cascade = this._cascade;
|
|
1385
1508
|
if (!cascade || !cascade.hoverSensitive) return false;
|
|
1386
1509
|
const chain: Element[] = [];
|
|
@@ -1398,6 +1521,7 @@ export class HtmlViewNode extends Node {
|
|
|
1398
1521
|
}
|
|
1399
1522
|
|
|
1400
1523
|
clearHover(): boolean {
|
|
1524
|
+
this._dropHeldHover();
|
|
1401
1525
|
if (!this._hovered.length) return false;
|
|
1402
1526
|
const was = this._hovered;
|
|
1403
1527
|
this._hovered = [];
|
|
@@ -1406,95 +1530,145 @@ export class HtmlViewNode extends Node {
|
|
|
1406
1530
|
return true;
|
|
1407
1531
|
}
|
|
1408
1532
|
|
|
1409
|
-
/**
|
|
1533
|
+
/**
|
|
1534
|
+
* The hovered chain moved: nothing, where no rule that tests the pointer
|
|
1535
|
+
* names an element it moved over; a restyle where they are, where all it
|
|
1536
|
+
* changed is ink (`_hoverInPlace`); and otherwise the boxes built again,
|
|
1537
|
+
* with the styles of every element the move did not reach kept.
|
|
1538
|
+
*/
|
|
1410
1539
|
private _restyleHover(was: readonly Element[], now: readonly Element[]) {
|
|
1411
|
-
|
|
1412
|
-
if (
|
|
1540
|
+
const reach = this._hoverReach(was, now);
|
|
1541
|
+
if (reach !== null) {
|
|
1542
|
+
if (!reach.size) return;
|
|
1543
|
+
if (this._hoverInPlace(reach, was)) return;
|
|
1544
|
+
}
|
|
1545
|
+
this._invalidate(Stale.Boxes, reach);
|
|
1413
1546
|
}
|
|
1414
1547
|
|
|
1415
1548
|
/**
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
* A move to another element is a change to the cascade, and the pipeline
|
|
1419
|
-
* answers one by building every box again and laying the document out:
|
|
1420
|
-
* 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
|
|
1421
|
-
* link the pointer crossed. But the rules a page writes for `:hover`
|
|
1422
|
-
* nearly all change a colour, an underline, a background or a border's
|
|
1423
|
-
* colour — 77 of that article's 79 — and those move nothing. So:
|
|
1549
|
+
* The elements whose styles a move of the hovered chain can change, or
|
|
1550
|
+
* null where that cannot be said (`Cascade.hoverLocal`).
|
|
1424
1551
|
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1431
|
-
* anonymous or a pseudo-element box, a marker, a control — this is
|
|
1432
|
-
* not the move to take.
|
|
1433
|
-
* - The boxes take their new styles, and each layout of their text is
|
|
1434
|
-
* made again from the runs it was made from (`TextLayoutCache.inputsOf`)
|
|
1435
|
-
* with the new ink (`runFor`), where its geometry comes out the same.
|
|
1436
|
-
*
|
|
1437
|
-
* Everything is checked before anything is changed. 'none' where nothing
|
|
1438
|
-
* could change, 'painted' where it was restyled here, false where the
|
|
1439
|
-
* document has to be built again.
|
|
1552
|
+
* Only an element whose hover state flipped and that a compound testing
|
|
1553
|
+
* the pointer could match can change (`Cascade.hoverTouches`), with its
|
|
1554
|
+
* subtree, and its later siblings' where a sibling combinator is in play;
|
|
1555
|
+
* and the elements a `:has()` testing the pointer may flip, which are
|
|
1556
|
+
* around the ones that changed. A move between two paragraphs under
|
|
1557
|
+
* `a:hover` reaches nothing.
|
|
1440
1558
|
*/
|
|
1441
|
-
private
|
|
1559
|
+
private _hoverReach(
|
|
1442
1560
|
was: readonly Element[],
|
|
1443
1561
|
now: readonly Element[],
|
|
1444
|
-
):
|
|
1562
|
+
): Set<Element> | null {
|
|
1445
1563
|
const cascade = this._cascade;
|
|
1446
|
-
|
|
1447
|
-
const layouts = this._layouts;
|
|
1448
|
-
if (!cascade || !tree || !layouts || !cascade.hoverLocal) return false;
|
|
1449
|
-
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0) return false;
|
|
1450
|
-
|
|
1564
|
+
if (!cascade || !cascade.hoverLocal) return null;
|
|
1451
1565
|
const before = new Set(was);
|
|
1452
1566
|
const after = new Set(now);
|
|
1453
|
-
const roots
|
|
1454
|
-
// and the elements a `:has()` testing the pointer may flip, which
|
|
1455
|
-
// are around the ones that changed; the chains are ancestor chains,
|
|
1456
|
-
// so the deepest that changed on each side is where to look from
|
|
1567
|
+
const roots = new Map<Element, HoverTouch>();
|
|
1457
1568
|
const flipped = (el: Element, other: Set<Element>): void => {
|
|
1458
1569
|
if (other.has(el)) return;
|
|
1459
|
-
|
|
1570
|
+
const touch = cascade.hoverTouches(el);
|
|
1571
|
+
if (touch > (roots.get(el) ?? HOVER_UNTOUCHED)) roots.set(el, touch);
|
|
1460
1572
|
};
|
|
1461
1573
|
for (const el of was) flipped(el, after);
|
|
1462
1574
|
for (const el of now) flipped(el, before);
|
|
1575
|
+
// the chains are ancestor chains, so the deepest that changed on each
|
|
1576
|
+
// side is where a `:has()` is looked for from
|
|
1463
1577
|
const deepest = (chain: readonly Element[], other: Set<Element>) => {
|
|
1464
1578
|
const el = chain[0];
|
|
1465
1579
|
if (el && !other.has(el)) cascade.hoverAnchors(el, roots);
|
|
1466
1580
|
};
|
|
1467
1581
|
deepest(was, after);
|
|
1468
1582
|
deepest(now, before);
|
|
1469
|
-
if (!roots.length) return 'none';
|
|
1470
1583
|
|
|
1471
|
-
//
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1584
|
+
// the roots' subtrees, which inherit from them and a descendant
|
|
1585
|
+
// combinator reaches, and their later siblings' too where a sibling
|
|
1586
|
+
// combinator follows the compound that tests the pointer
|
|
1474
1587
|
const reach = new Set<Element>();
|
|
1475
|
-
const collect = (el: Element):
|
|
1588
|
+
const collect = (el: Element): void => {
|
|
1476
1589
|
const stack: Element[] = [el];
|
|
1477
1590
|
while (stack.length) {
|
|
1478
1591
|
const at = stack.pop()!;
|
|
1479
1592
|
if (reach.has(at)) continue;
|
|
1480
1593
|
reach.add(at);
|
|
1481
|
-
if (reach.size > HOVER_RESTYLE_LIMIT) return false;
|
|
1482
1594
|
for (const child of at.children)
|
|
1483
1595
|
if (isElement(child)) stack.push(child);
|
|
1484
1596
|
}
|
|
1485
|
-
return true;
|
|
1486
1597
|
};
|
|
1487
|
-
for (const root of roots) {
|
|
1488
|
-
|
|
1489
|
-
if (
|
|
1598
|
+
for (const [root, touch] of roots) {
|
|
1599
|
+
collect(root);
|
|
1600
|
+
if (touch !== HOVER_FOLLOWED) continue;
|
|
1490
1601
|
for (let s = root.nextSibling; s; s = s.nextSibling) {
|
|
1491
|
-
if (isElement(s)
|
|
1602
|
+
if (isElement(s)) collect(s);
|
|
1492
1603
|
}
|
|
1493
1604
|
}
|
|
1605
|
+
return reach;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* A pointer move restyled where it happened, when all it changed is ink.
|
|
1610
|
+
*
|
|
1611
|
+
* A move to another element is a change to the cascade, and the pipeline
|
|
1612
|
+
* answers one by building every box again and laying the document out:
|
|
1613
|
+
* 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
|
|
1614
|
+
* link the pointer crossed. But the rules a page writes for `:hover`
|
|
1615
|
+
* nearly all change a colour, an underline, a background or a border's
|
|
1616
|
+
* colour — 77 of that article's 79 — and those move nothing. So:
|
|
1617
|
+
*
|
|
1618
|
+
* - The elements the move reached (`_hoverReach`) are styled again from
|
|
1619
|
+
* their parents — the ones a rule testing the pointer answers
|
|
1620
|
+
* differently for, or whose parent's style changed; the rest, most of
|
|
1621
|
+
* a hovered card or table row, have the styles they had
|
|
1622
|
+
* (`Cascade.pointerChanged`). Where anything but ink differs
|
|
1623
|
+
* (`PAINT_ONLY`), this is not the move to take.
|
|
1624
|
+
* - The boxes take their new styles: an element's own, the text in it,
|
|
1625
|
+
* the anonymous boxes the fix-up made around what is in it, and its
|
|
1626
|
+
* `::before` and `::after`, styled again by their own rules. A box
|
|
1627
|
+
* whose style is derived some other way — a marker, a first letter, a
|
|
1628
|
+
* control — is not one to restyle here.
|
|
1629
|
+
* - Each layout of their text is made again from the runs it was made
|
|
1630
|
+
* from (`TextLayoutCache.inputsOf`) with the new ink (`runFor`), where
|
|
1631
|
+
* its geometry comes out the same.
|
|
1632
|
+
*
|
|
1633
|
+
* Everything is checked before anything is changed, and nothing is looked
|
|
1634
|
+
* at but the blocks the restyled elements are in (`_blocksOf`): a move
|
|
1635
|
+
* costs what it changed, in a document of any length. True where it was
|
|
1636
|
+
* restyled here, or had nothing to restyle; false where the boxes have to
|
|
1637
|
+
* be built again.
|
|
1638
|
+
*/
|
|
1639
|
+
private _hoverInPlace(
|
|
1640
|
+
reach: ReadonlySet<Element>,
|
|
1641
|
+
was: readonly Element[],
|
|
1642
|
+
): boolean {
|
|
1643
|
+
const cascade = this._cascade;
|
|
1644
|
+
const tree = this._tree;
|
|
1645
|
+
const layouts = this._layouts;
|
|
1646
|
+
if (!cascade || !tree || !layouts) return false;
|
|
1647
|
+
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0) return false;
|
|
1648
|
+
if (reach.size > HOVER_RESTYLE_LIMIT) return false;
|
|
1649
|
+
// a first line's, a first letter's and a marker's styles are layout's
|
|
1650
|
+
const pseudoRules = cascade.hoverPseudo;
|
|
1651
|
+
if (pseudoRules === HOVER_PSEUDO_OTHER) return false;
|
|
1652
|
+
|
|
1653
|
+
// whether the rules that test the pointer answer differently for an
|
|
1654
|
+
// element than they did before the move
|
|
1655
|
+
const before = { hovered: new Set(was), active: EMPTY_SET };
|
|
1656
|
+
const flips = new Map<Element, boolean>();
|
|
1657
|
+
const flipped = (el: Element): boolean => {
|
|
1658
|
+
let flip = flips.get(el);
|
|
1659
|
+
if (flip === undefined) {
|
|
1660
|
+
flip = cascade.pointerChanged(el, before);
|
|
1661
|
+
flips.set(el, flip);
|
|
1662
|
+
}
|
|
1663
|
+
return flip;
|
|
1664
|
+
};
|
|
1494
1665
|
|
|
1495
1666
|
// styled again from their parents, a parent first
|
|
1496
1667
|
const fresh = new Map<Element, ComputedStyle>();
|
|
1497
1668
|
const changed = new Map<Element, ComputedStyle>();
|
|
1669
|
+
// the ones whose ink is what it was: only their custom properties
|
|
1670
|
+
// changed, which the elements under them read
|
|
1671
|
+
const quiet = new Set<Element>();
|
|
1498
1672
|
let refused = false;
|
|
1499
1673
|
// what changed beyond ink (`hoverChange`): how far some box's ink
|
|
1500
1674
|
// reaches, the order its layer paints in, where one is
|
|
@@ -1515,51 +1689,123 @@ export class HtmlViewNode extends Node {
|
|
|
1515
1689
|
refused = true;
|
|
1516
1690
|
return null;
|
|
1517
1691
|
}
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
if (diff
|
|
1525
|
-
|
|
1526
|
-
|
|
1692
|
+
const parentWas = parent ? tree.styles.get(parent)!.style : parentStyle;
|
|
1693
|
+
let style = kept.style;
|
|
1694
|
+
if (parentStyle !== parentWas || flipped(el)) {
|
|
1695
|
+
const made = cascade.styleFor(el, parentStyle, kept.inFlex);
|
|
1696
|
+
const diff = hoverChange(kept.style, made);
|
|
1697
|
+
if (diff === false) refused = true;
|
|
1698
|
+
else if (diff) {
|
|
1699
|
+
style = made;
|
|
1700
|
+
changed.set(el, made);
|
|
1701
|
+
if (!diff.ink) quiet.add(el);
|
|
1702
|
+
if (diff.reach) reaches = true;
|
|
1703
|
+
if (diff.order) reorders = true;
|
|
1704
|
+
if (diff.move) moving.add(el);
|
|
1705
|
+
}
|
|
1527
1706
|
}
|
|
1707
|
+
fresh.set(el, style);
|
|
1528
1708
|
return style;
|
|
1529
1709
|
};
|
|
1530
1710
|
for (const el of reach) {
|
|
1531
1711
|
styleOf(el);
|
|
1532
1712
|
if (refused) return false;
|
|
1533
1713
|
}
|
|
1534
|
-
if (!changed.size) return
|
|
1714
|
+
if (!changed.size && pseudoRules === HOVER_PSEUDO_NONE) return true;
|
|
1535
1715
|
for (const el of changed.keys()) {
|
|
1536
1716
|
const tag = tagOf(el);
|
|
1537
1717
|
// their backgrounds are the canvas's
|
|
1538
1718
|
if (tag === 'html' || tag === 'body') return false;
|
|
1539
1719
|
}
|
|
1540
1720
|
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1721
|
+
// The boxes, each under its parent: what a box takes depends on what
|
|
1722
|
+
// the box it is in took.
|
|
1543
1723
|
const fonts = layouts.fonts;
|
|
1724
|
+
const blocks = this._blocksOf(
|
|
1725
|
+
tree,
|
|
1726
|
+
pseudoRules === HOVER_PSEUDO_NONE ? changed.keys() : reach,
|
|
1727
|
+
);
|
|
1544
1728
|
const restyled: [Box, ComputedStyle][] = [];
|
|
1729
|
+
const next = new Map<Box, ComputedStyle>();
|
|
1730
|
+
/** The restyled boxes whose ink changed, to repaint. */
|
|
1731
|
+
const inked = new Set<Box>();
|
|
1732
|
+
/** The `::before` and `::after` boxes found, by element: 1 and 2. */
|
|
1733
|
+
const generated = new Map<Element, number>();
|
|
1545
1734
|
const redecorated: [Box, Box['decoration']][] = [];
|
|
1546
|
-
const walk: Box[] =
|
|
1735
|
+
const walk: Box[] = blocks.slice();
|
|
1547
1736
|
while (walk.length) {
|
|
1548
1737
|
const box = walk.pop()!;
|
|
1549
1738
|
for (const child of box.children) walk.push(child);
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1739
|
+
const parent = box.parent;
|
|
1740
|
+
const above = parent ? next.get(parent) : undefined;
|
|
1741
|
+
let style: ComputedStyle | undefined;
|
|
1742
|
+
let ink = !!parent && inked.has(parent);
|
|
1743
|
+
if (box.kind === 'text' || box.kind === 'break') {
|
|
1744
|
+
// Text is set in the style of the box it is in — an element's, a
|
|
1745
|
+
// pseudo-element's — or of its element, where the fix-up put an
|
|
1746
|
+
// anonymous box around it.
|
|
1747
|
+
const el = box.el;
|
|
1748
|
+
if (parent && box.style === parent.style) style = above;
|
|
1749
|
+
else if (el && box.style === tree.styles.get(el)?.style) {
|
|
1750
|
+
style = changed.get(el);
|
|
1751
|
+
ink = !quiet.has(el);
|
|
1752
|
+
} else if (above || (el && changed.has(el))) return false;
|
|
1753
|
+
} else if (box.pseudo === 'before' || box.pseudo === 'after') {
|
|
1754
|
+
const from = GENERATED_FROM.get(box);
|
|
1755
|
+
if (!from) {
|
|
1756
|
+
// an inside marker, made of its item's style and its own rules
|
|
1757
|
+
if (above) return false;
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
if (!reach.has(from)) continue;
|
|
1761
|
+
generated.set(
|
|
1762
|
+
from,
|
|
1763
|
+
(generated.get(from) ?? 0) | (box.pseudo === 'before' ? 1 : 2),
|
|
1764
|
+
);
|
|
1765
|
+
const inherits = fresh.get(from);
|
|
1766
|
+
const kept = tree.styles.get(from)?.style;
|
|
1767
|
+
if (!inherits || !kept) return false;
|
|
1768
|
+
if (inherits === kept && !flipped(from)) continue;
|
|
1769
|
+
// styled by its own rules over its element's style, as the builder
|
|
1770
|
+
// styled it, in its element's box
|
|
1771
|
+
if (!parent || parent.style !== kept || box.marker) return false;
|
|
1772
|
+
const made = cascade.pseudoStyleFor(from, box.pseudo, inherits);
|
|
1773
|
+
if (!made || made.display === 'none') return false;
|
|
1774
|
+
const diff = hoverChange(box.style, made);
|
|
1775
|
+
if (diff === false) return false;
|
|
1776
|
+
if (!diff) continue;
|
|
1777
|
+
if (diff.reach || diff.order || diff.move) return false;
|
|
1778
|
+
style = made;
|
|
1779
|
+
ink = diff.ink;
|
|
1780
|
+
} else if (box.pseudo) {
|
|
1781
|
+
// a first letter, styled from the box its letter is in
|
|
1782
|
+
if (above) return false;
|
|
1783
|
+
} else if (box.el) {
|
|
1784
|
+
const el = box.el;
|
|
1785
|
+
style = changed.get(el);
|
|
1786
|
+
if (!style) continue;
|
|
1787
|
+
const kept = tree.styles.get(el)!.style;
|
|
1788
|
+
// a style derived from the element's, or drawn from it somewhere
|
|
1789
|
+
// else — a widget mounted beside the document takes its look from
|
|
1790
|
+
// it. An image, a drawing or a rule is drawn here, from its box,
|
|
1791
|
+
// like any.
|
|
1792
|
+
if (box.style !== kept) return false;
|
|
1793
|
+
if (box.marker || WIDGETS.has(box.replaced)) return false;
|
|
1794
|
+
if (moving.has(el) && !movable(box, kept, style)) return false;
|
|
1795
|
+
ink = !quiet.has(el);
|
|
1796
|
+
} else if (above) {
|
|
1797
|
+
// An anonymous box takes what its parent's style passes on
|
|
1798
|
+
// (`anonymousStyles`), so it takes that of the new one: the text of
|
|
1799
|
+
// a link made a flex row is in one, and takes the link's colour.
|
|
1800
|
+
if (GENERATED_FROM.has(box)) return false;
|
|
1801
|
+
const display = box.style.display;
|
|
1802
|
+
if (tree.anonymous(parent!.style, display) !== box.style) return false;
|
|
1803
|
+
style = tree.anonymous(above, display);
|
|
1804
|
+
}
|
|
1553
1805
|
if (!style) continue;
|
|
1554
|
-
|
|
1555
|
-
// an anonymous box, a pseudo-element, a marker, a control: a style
|
|
1556
|
-
// derived from the element's, or drawn from it somewhere else — a
|
|
1557
|
-
// widget mounted beside the document takes its look from it. An
|
|
1558
|
-
// image, a drawing or a rule is drawn here, from its box, like any.
|
|
1559
|
-
if (!box.el || box.style !== kept.style) return false;
|
|
1560
|
-
if (box.marker || WIDGETS.has(box.replaced)) return false;
|
|
1561
|
-
if (moving.has(owner) && !movable(box, kept.style, style)) return false;
|
|
1806
|
+
next.set(box, style);
|
|
1562
1807
|
restyled.push([box, style]);
|
|
1808
|
+
if (ink) inked.add(box);
|
|
1563
1809
|
if (box.kind === 'inline') {
|
|
1564
1810
|
const decoration = inlineDecoration(fonts, box, style);
|
|
1565
1811
|
if ((decoration === null) !== (box.decoration === null)) {
|
|
@@ -1569,12 +1815,28 @@ export class HtmlViewNode extends Node {
|
|
|
1569
1815
|
if (decoration !== box.decoration) redecorated.push([box, decoration]);
|
|
1570
1816
|
}
|
|
1571
1817
|
}
|
|
1818
|
+
// a `::before` or an `::after` a rule that tests the pointer now gives
|
|
1819
|
+
// an element is a box to build
|
|
1820
|
+
if (pseudoRules !== HOVER_PSEUDO_NONE) {
|
|
1821
|
+
for (const el of reach) {
|
|
1822
|
+
if (!flipped(el)) continue;
|
|
1823
|
+
const style = fresh.get(el);
|
|
1824
|
+
if (!style) continue;
|
|
1825
|
+
const has = generated.get(el) ?? 0;
|
|
1826
|
+
for (const [which, bit] of GENERATED_BITS) {
|
|
1827
|
+
if (has & bit) continue;
|
|
1828
|
+
const made = cascade.pseudoStyleFor(el, which, style);
|
|
1829
|
+
if (made && made.display !== 'none') return false;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (!restyled.length) return true;
|
|
1572
1834
|
|
|
1573
|
-
// the text: each layout holding a run of a
|
|
1574
|
-
//
|
|
1835
|
+
// the text: each layout holding a run of a restyled box's, made again
|
|
1836
|
+
// with its new ink, where it comes out the same shape
|
|
1575
1837
|
const relaid = new Map<TextLayoutLike, TextLayoutLike | null>();
|
|
1576
1838
|
const texts: LineText[] = [];
|
|
1577
|
-
walk.push(
|
|
1839
|
+
if (inked.size) for (const block of blocks) walk.push(block);
|
|
1578
1840
|
while (walk.length) {
|
|
1579
1841
|
const box = walk.pop()!;
|
|
1580
1842
|
for (const child of box.children) walk.push(child);
|
|
@@ -1583,16 +1845,16 @@ export class HtmlViewNode extends Node {
|
|
|
1583
1845
|
for (const text of line.texts) {
|
|
1584
1846
|
texts.push(text);
|
|
1585
1847
|
if (relaid.has(text.layout)) continue;
|
|
1586
|
-
const
|
|
1587
|
-
if (
|
|
1588
|
-
relaid.set(text.layout,
|
|
1848
|
+
const again = reinked(text, layouts, next);
|
|
1849
|
+
if (again === false) return false;
|
|
1850
|
+
relaid.set(text.layout, again);
|
|
1589
1851
|
}
|
|
1590
1852
|
}
|
|
1591
1853
|
}
|
|
1592
1854
|
|
|
1593
1855
|
// what each box drew before, to be repainted with what it draws after
|
|
1594
1856
|
const inks: Rect[] = [];
|
|
1595
|
-
for (const
|
|
1857
|
+
for (const box of inked) {
|
|
1596
1858
|
const ink = inkOf(box);
|
|
1597
1859
|
if (ink) inks.push(ink);
|
|
1598
1860
|
}
|
|
@@ -1602,7 +1864,9 @@ export class HtmlViewNode extends Node {
|
|
|
1602
1864
|
for (const [box, style] of restyled) {
|
|
1603
1865
|
const was = box.style;
|
|
1604
1866
|
box.style = style;
|
|
1605
|
-
if (moving.has(box.el
|
|
1867
|
+
if (box.el && moving.has(box.el) && box.kind !== 'text') {
|
|
1868
|
+
moved.push([box, was]);
|
|
1869
|
+
}
|
|
1606
1870
|
}
|
|
1607
1871
|
for (const [box, decoration] of redecorated) {
|
|
1608
1872
|
// what is inside it keeps which boxes around it paint, and this is
|
|
@@ -1613,8 +1877,8 @@ export class HtmlViewNode extends Node {
|
|
|
1613
1877
|
box.decoration = decoration;
|
|
1614
1878
|
}
|
|
1615
1879
|
for (const text of texts) {
|
|
1616
|
-
const
|
|
1617
|
-
if (
|
|
1880
|
+
const again = relaid.get(text.layout);
|
|
1881
|
+
if (again) text.layout = again;
|
|
1618
1882
|
}
|
|
1619
1883
|
for (const [el, style] of changed) {
|
|
1620
1884
|
tree.styles.set(el, { style, inFlex: tree.styles.get(el)!.inFlex });
|
|
@@ -1645,14 +1909,61 @@ export class HtmlViewNode extends Node {
|
|
|
1645
1909
|
if (moved.length) this._reportControls();
|
|
1646
1910
|
if (relayout) {
|
|
1647
1911
|
this._invalidate(Stale.Layout);
|
|
1648
|
-
return
|
|
1912
|
+
return true;
|
|
1649
1913
|
}
|
|
1650
|
-
for (const
|
|
1914
|
+
for (const box of inked) {
|
|
1651
1915
|
const ink = inkOf(box);
|
|
1652
1916
|
if (ink) inks.push(ink);
|
|
1653
1917
|
}
|
|
1654
1918
|
this._repaintInk(inks);
|
|
1655
|
-
return
|
|
1919
|
+
return true;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
/** An element's first box in document order, for the tree it was asked
|
|
1923
|
+
* of: made on the first move over a tree, which is one walk of it. */
|
|
1924
|
+
private _firstBoxes: { tree: BoxTree; of: Map<Element, Box> } | null = null;
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* The blocks a restyle of `changed` has to look in: for each element, the
|
|
1928
|
+
* box its text is set in the lines of — the nearest box around its first
|
|
1929
|
+
* that is not an inline one — which holds every box that takes its style
|
|
1930
|
+
* from it, the pieces a block in it broke it into among them
|
|
1931
|
+
* (`breakAround`). One that is inside another is left to it.
|
|
1932
|
+
*/
|
|
1933
|
+
private _blocksOf(tree: BoxTree, changed: Iterable<Element>): Box[] {
|
|
1934
|
+
let index = this._firstBoxes;
|
|
1935
|
+
if (index?.tree !== tree) {
|
|
1936
|
+
const of = new Map<Element, Box>();
|
|
1937
|
+
const stack: Box[] = [tree.root];
|
|
1938
|
+
while (stack.length) {
|
|
1939
|
+
const box = stack.pop()!;
|
|
1940
|
+
if (box.el && !of.has(box.el)) of.set(box.el, box);
|
|
1941
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1942
|
+
stack.push(box.children[i]);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
index = this._firstBoxes = { tree, of };
|
|
1946
|
+
}
|
|
1947
|
+
const blocks = new Set<Box>();
|
|
1948
|
+
for (const el of changed) {
|
|
1949
|
+
let box: Box | null = index.of.get(el) ?? null;
|
|
1950
|
+
while (
|
|
1951
|
+
box?.parent &&
|
|
1952
|
+
(box.kind === 'inline' || box.kind === 'text' || box.kind === 'break')
|
|
1953
|
+
) {
|
|
1954
|
+
box = box.parent;
|
|
1955
|
+
}
|
|
1956
|
+
if (box) blocks.add(box);
|
|
1957
|
+
}
|
|
1958
|
+
const out: Box[] = [];
|
|
1959
|
+
for (const block of blocks) {
|
|
1960
|
+
let inside = false;
|
|
1961
|
+
for (let at = block.parent; at && !inside; at = at.parent) {
|
|
1962
|
+
inside = blocks.has(at);
|
|
1963
|
+
}
|
|
1964
|
+
if (!inside) out.push(block);
|
|
1965
|
+
}
|
|
1966
|
+
return out;
|
|
1656
1967
|
}
|
|
1657
1968
|
|
|
1658
1969
|
/**
|
|
@@ -1715,20 +2026,65 @@ export class HtmlViewNode extends Node {
|
|
|
1715
2026
|
// `:hover` is wired at the element rather than through React: the cascade
|
|
1716
2027
|
// already knows whether any rule in the document tests it, `setHover`
|
|
1717
2028
|
// returns without work when none does, and a document that does use it
|
|
1718
|
-
// re-styles only when the hovered chain actually changed
|
|
1719
|
-
//
|
|
1720
|
-
//
|
|
2029
|
+
// re-styles only when the hovered chain actually changed, and then only
|
|
2030
|
+
// the elements the change reached (`_restyleHover`).
|
|
2031
|
+
//
|
|
2032
|
+
// **A hover waits for a scroll to stop.** Core asks again what is under a
|
|
2033
|
+
// pointer that stayed where it was after every frame that laid out —
|
|
2034
|
+
// hover follows content, react-x11#793 — and says so with a move to the
|
|
2035
|
+
// point the pointer is already at. While a document scrolls that is every
|
|
2036
|
+
// frame, each with something else under the pointer: a restyle a frame
|
|
2037
|
+
// at best, and where a hover moves something, the boxes built again a
|
|
2038
|
+
// frame, which took a page scrolled under a parked pointer from 52 frames
|
|
2039
|
+
// a second to 4. A browser holds its hover until the scroll is over, for
|
|
2040
|
+
// the same reason; so does this, for `HOVER_REST_MS` after the content
|
|
2041
|
+
// last moved. What was hovered stays hovered as it scrolls away, as it
|
|
2042
|
+
// does there. A move of the pointer itself is answered at once.
|
|
1721
2043
|
|
|
1722
2044
|
override defaultMouseMove(ev: X11MouseEvent): void {
|
|
1723
2045
|
super.defaultMouseMove?.(ev);
|
|
2046
|
+
const at = this._pointerAt;
|
|
2047
|
+
if (at !== null && at.x === ev.x && at.y === ev.y) {
|
|
2048
|
+
this._holdHover();
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
this._pointerAt = { x: ev.x, y: ev.y };
|
|
1724
2052
|
this.setHover(ev.x, ev.y);
|
|
1725
2053
|
}
|
|
1726
2054
|
|
|
1727
2055
|
override defaultMouseLeave(ev: X11MouseEvent): void {
|
|
1728
2056
|
super.defaultMouseLeave?.(ev);
|
|
2057
|
+
this._pointerAt = null;
|
|
1729
2058
|
this.clearHover();
|
|
1730
2059
|
}
|
|
1731
2060
|
|
|
2061
|
+
/** The content moved under the pointer: the hover is asked for once it
|
|
2062
|
+
* has been still for `HOVER_REST_MS`. One timer a wait, put off by
|
|
2063
|
+
* being armed again when it finds the content moved since. */
|
|
2064
|
+
private _holdHover(): void {
|
|
2065
|
+
if (!this._cascade?.hoverSensitive) return;
|
|
2066
|
+
this._heldSince = hoverClock.now();
|
|
2067
|
+
if (this._heldHover !== null) return;
|
|
2068
|
+
const rest = (): void => {
|
|
2069
|
+
this._heldHover = null;
|
|
2070
|
+
const at = this._pointerAt;
|
|
2071
|
+
if (!at || this.destroyed) return;
|
|
2072
|
+
const left = this._heldSince + HOVER_REST_MS - hoverClock.now();
|
|
2073
|
+
if (left > 0) {
|
|
2074
|
+
this._heldHover = hoverClock.arm(rest, left);
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
this.setHover(at.x, at.y);
|
|
2078
|
+
};
|
|
2079
|
+
this._heldHover = hoverClock.arm(rest, HOVER_REST_MS);
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
private _dropHeldHover(): void {
|
|
2083
|
+
if (this._heldHover === null) return;
|
|
2084
|
+
hoverClock.disarm(this._heldHover);
|
|
2085
|
+
this._heldHover = null;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1732
2088
|
// --- paint ----------------------------------------------------------------
|
|
1733
2089
|
|
|
1734
2090
|
override paint(ctx: Context2D): void {
|
|
@@ -2026,7 +2382,10 @@ function sameRects(a: ControlRect[], b: ControlRect[]): boolean {
|
|
|
2026
2382
|
p.y !== q.y ||
|
|
2027
2383
|
p.width !== q.width ||
|
|
2028
2384
|
p.height !== q.height ||
|
|
2385
|
+
p.fontFamily !== q.fontFamily ||
|
|
2386
|
+
p.fontSize !== q.fontSize ||
|
|
2029
2387
|
p.opacity !== q.opacity ||
|
|
2388
|
+
!sameClip(p.clip, q.clip) ||
|
|
2030
2389
|
!sameBare(p.bare, q.bare)
|
|
2031
2390
|
) {
|
|
2032
2391
|
return false;
|
|
@@ -2035,6 +2394,13 @@ function sameRects(a: ControlRect[], b: ControlRect[]): boolean {
|
|
|
2035
2394
|
return true;
|
|
2036
2395
|
}
|
|
2037
2396
|
|
|
2397
|
+
function sameClip(a?: ControlRect['clip'], b?: ControlRect['clip']): boolean {
|
|
2398
|
+
if (!a || !b) return a === b;
|
|
2399
|
+
return (
|
|
2400
|
+
a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2038
2404
|
function sameBare(a?: BareField, b?: BareField): boolean {
|
|
2039
2405
|
if (!a || !b) return a === b;
|
|
2040
2406
|
return (
|
|
@@ -2084,7 +2450,16 @@ function nearestText(box: Box, x: number, y: number): number | null {
|
|
|
2084
2450
|
const distance = dy * 4 + dx;
|
|
2085
2451
|
if (distance >= bestDistance) continue;
|
|
2086
2452
|
bestDistance = distance;
|
|
2087
|
-
|
|
2453
|
+
// in the row that is this line's, where the layout's other lines are
|
|
2454
|
+
// in another column and a point past this one is not over them
|
|
2455
|
+
let row = y - text.drawY;
|
|
2456
|
+
if (columned.any && COLUMN_ROWS.has(text)) {
|
|
2457
|
+
row = Math.max(
|
|
2458
|
+
natural.y,
|
|
2459
|
+
Math.min(row, natural.y + natural.height - 0.01),
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
const local = text.layout.indexAt(x - text.drawX, row);
|
|
2088
2463
|
const offsets = layoutOffsetsOf(text.layout);
|
|
2089
2464
|
const units = offsets.length
|
|
2090
2465
|
? offsets[Math.max(0, Math.min(local, offsets.length - 1))]
|
|
@@ -2098,14 +2473,37 @@ function nearestText(box: Box, x: number, y: number): number | null {
|
|
|
2098
2473
|
axisDistance(y, node.boundsY, node.boundsHeight) * 4 +
|
|
2099
2474
|
axisDistance(x, node.boundsX, node.boundsWidth);
|
|
2100
2475
|
if (boundsDistance >= bestDistance) return;
|
|
2476
|
+
// in a box painted through a matrix, the point taken back through it
|
|
2477
|
+
// (`deepestAt`): the text is where it was laid out
|
|
2478
|
+
const matrix = placedMatrix(node);
|
|
2479
|
+
if (!matrix) {
|
|
2480
|
+
visitIn(node);
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
const back = invert(matrix);
|
|
2484
|
+
if (!back) return;
|
|
2485
|
+
const px = x;
|
|
2486
|
+
const py = y;
|
|
2487
|
+
[x, y] = mapPoint(back, px, py);
|
|
2488
|
+
try {
|
|
2489
|
+
visitIn(node);
|
|
2490
|
+
} finally {
|
|
2491
|
+
x = px;
|
|
2492
|
+
y = py;
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2101
2495
|
|
|
2496
|
+
const visitIn = (node: Box): void => {
|
|
2102
2497
|
const lines = node.lines;
|
|
2103
2498
|
if (lines?.length) {
|
|
2104
2499
|
// The line nearest in y, by binary search; then outward both ways
|
|
2105
2500
|
// while vertical distance alone could still beat the best.
|
|
2106
2501
|
let lo = 0;
|
|
2107
2502
|
let hi = lines.length;
|
|
2108
|
-
|
|
2503
|
+
// the lines columns took apart are in no order down the page: each
|
|
2504
|
+
// is tried
|
|
2505
|
+
const apart = columned.any && COLUMN_LINES.has(lines);
|
|
2506
|
+
while (!apart && lo < hi) {
|
|
2109
2507
|
const mid = (lo + hi) >> 1;
|
|
2110
2508
|
if (lines[mid].y > y) hi = mid;
|
|
2111
2509
|
else lo = mid + 1;
|
|
@@ -2116,8 +2514,10 @@ function nearestText(box: Box, x: number, y: number): number | null {
|
|
|
2116
2514
|
tryLine(lines[i]);
|
|
2117
2515
|
}
|
|
2118
2516
|
for (let i = lo; i < lines.length; i += 1) {
|
|
2119
|
-
if (axisDistance(y, lines[i].y, lines[i].height) * 4 >= bestDistance)
|
|
2517
|
+
if (axisDistance(y, lines[i].y, lines[i].height) * 4 >= bestDistance) {
|
|
2518
|
+
if (apart) continue;
|
|
2120
2519
|
break;
|
|
2520
|
+
}
|
|
2121
2521
|
tryLine(lines[i]);
|
|
2122
2522
|
}
|
|
2123
2523
|
}
|
|
@@ -2204,7 +2604,16 @@ function caretAt(
|
|
|
2204
2604
|
const caret = text.layout.caretPosition(
|
|
2205
2605
|
codePointAtOffset(offsets, layoutUnits),
|
|
2206
2606
|
);
|
|
2207
|
-
|
|
2607
|
+
const at = throughTransforms(
|
|
2608
|
+
{
|
|
2609
|
+
x: text.drawX + caret.x,
|
|
2610
|
+
y: line.y,
|
|
2611
|
+
width: 0,
|
|
2612
|
+
height: line.height,
|
|
2613
|
+
},
|
|
2614
|
+
node,
|
|
2615
|
+
);
|
|
2616
|
+
found = { x: at.x, y: at.y, height: at.height };
|
|
2208
2617
|
return;
|
|
2209
2618
|
}
|
|
2210
2619
|
}
|
|
@@ -2219,6 +2628,21 @@ function caretAt(
|
|
|
2219
2628
|
return found;
|
|
2220
2629
|
}
|
|
2221
2630
|
|
|
2631
|
+
/**
|
|
2632
|
+
* A rectangle in a box's own coordinates — the ones it and what it holds
|
|
2633
|
+
* were laid out in — as the rectangle around where it is drawn: through
|
|
2634
|
+
* the matrix of each box from `from` up that is painted through one
|
|
2635
|
+
* (`placedMatrix`). The rectangle itself, in a document that transforms
|
|
2636
|
+
* nothing but by moving it.
|
|
2637
|
+
*/
|
|
2638
|
+
function throughTransforms(rect: Rect, from: Box | null): Rect {
|
|
2639
|
+
for (let at = from; at; at = at.parent) {
|
|
2640
|
+
const matrix = placedMatrix(at);
|
|
2641
|
+
if (matrix) rect = mapRect(matrix, rect.x, rect.y, rect.width, rect.height);
|
|
2642
|
+
}
|
|
2643
|
+
return rect;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2222
2646
|
/** Every band a document range covers, in window coordinates. The subtree
|
|
2223
2647
|
* text ranges prune the walk to the boxes the range actually crosses, so a
|
|
2224
2648
|
* drag's small range costs its own paragraphs; only Ctrl+A pays for all. */
|
|
@@ -2320,6 +2744,9 @@ function lineBands(
|
|
|
2320
2744
|
/** Where the lines put the text, not where a relative offset moved it
|
|
2321
2745
|
* (`TEXT_SHIFTS`): what an inline box's own offset is added to. */
|
|
2322
2746
|
laidOut = false,
|
|
2747
|
+
/** The rows a highlight covers (`selectionRows`) rather than the line's:
|
|
2748
|
+
* past it where the glyphs are taller than the line. */
|
|
2749
|
+
ink = false,
|
|
2323
2750
|
): void {
|
|
2324
2751
|
// First line whose text can reach `from`, by binary search over the
|
|
2325
2752
|
// sorted text starts; stop at the first line past `to`.
|
|
@@ -2348,6 +2775,9 @@ function lineBands(
|
|
|
2348
2775
|
// a link that ends a line a space wider than Chrome's. So the text
|
|
2349
2776
|
// a line ends with is measured no further than its content.
|
|
2350
2777
|
const ends = endsLine(line, text, natural);
|
|
2778
|
+
const rows = ink
|
|
2779
|
+
? selectionRows(line, text, natural)
|
|
2780
|
+
: { y: line.y, height: line.height };
|
|
2351
2781
|
for (const band of bandsFor(
|
|
2352
2782
|
text.layout,
|
|
2353
2783
|
natural,
|
|
@@ -2363,9 +2793,22 @@ function lineBands(
|
|
|
2363
2793
|
}
|
|
2364
2794
|
out.push({
|
|
2365
2795
|
x: dx + left + text.drawX - moved,
|
|
2366
|
-
y: dy +
|
|
2796
|
+
y: dy + rows.y,
|
|
2367
2797
|
width: right - left,
|
|
2368
|
-
height:
|
|
2798
|
+
height: rows.height,
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
// The spaces a piece of a line ends on, where the line goes on
|
|
2802
|
+
// after them: the engine stripped them from the piece it laid out
|
|
2803
|
+
// as a line, and they are the text's all the same, in the elements
|
|
2804
|
+
// that hold its end — a link whose text ends in a space, before
|
|
2805
|
+
// the next link, is that space wider than its letters
|
|
2806
|
+
if (text.trail && b >= text.textEnd && !rtlLine(natural)) {
|
|
2807
|
+
out.push({
|
|
2808
|
+
x: dx + natural.x + natural.width + text.drawX - moved,
|
|
2809
|
+
y: dy + rows.y,
|
|
2810
|
+
width: text.trail,
|
|
2811
|
+
height: rows.height,
|
|
2369
2812
|
});
|
|
2370
2813
|
}
|
|
2371
2814
|
// the spaces `pre-wrap` keeps that the line ends on, which have no
|
|
@@ -2375,9 +2818,9 @@ function lineBands(
|
|
|
2375
2818
|
if (at < a || at >= b) continue;
|
|
2376
2819
|
out.push({
|
|
2377
2820
|
x: dx + space.x + text.drawX - moved,
|
|
2378
|
-
y: dy +
|
|
2821
|
+
y: dy + rows.y,
|
|
2379
2822
|
width: space.width,
|
|
2380
|
-
height:
|
|
2823
|
+
height: rows.height,
|
|
2381
2824
|
});
|
|
2382
2825
|
}
|
|
2383
2826
|
}
|
|
@@ -2385,6 +2828,14 @@ function lineBands(
|
|
|
2385
2828
|
}
|
|
2386
2829
|
}
|
|
2387
2830
|
|
|
2831
|
+
/** Whether a laid-out line's last run reads right to left, its end at its
|
|
2832
|
+
* left. */
|
|
2833
|
+
function rtlLine(natural: {
|
|
2834
|
+
runs: { run?: { direction?: string } }[];
|
|
2835
|
+
}): boolean {
|
|
2836
|
+
return natural.runs[natural.runs.length - 1]?.run?.direction === 'rtl';
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2388
2839
|
/** Whether a line's text is where the line ends: its last, with nothing
|
|
2389
2840
|
* placed after it — an inline-block or an image past its content. */
|
|
2390
2841
|
function endsLine(
|
|
@@ -2407,14 +2858,30 @@ function collectBands(
|
|
|
2407
2858
|
dx: number,
|
|
2408
2859
|
dy: number,
|
|
2409
2860
|
out: Rect[],
|
|
2861
|
+
ink = false,
|
|
2410
2862
|
): void {
|
|
2411
2863
|
if (box.subtreeTextEnd <= box.subtreeTextStart) return;
|
|
2412
2864
|
if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to) return;
|
|
2413
|
-
|
|
2865
|
+
const first = out.length;
|
|
2866
|
+
if (box.lines) lineBands(box.lines, from, to, dx, dy, out, false, ink);
|
|
2414
2867
|
// Atomics are ordinary children, reached below.
|
|
2415
2868
|
for (const child of box.children) {
|
|
2416
2869
|
if (child.kind === 'text' || child.kind === 'break') continue;
|
|
2417
|
-
collectBands(child, from, to, dx, dy, out);
|
|
2870
|
+
collectBands(child, from, to, dx, dy, out, ink);
|
|
2871
|
+
}
|
|
2872
|
+
// where a box painted through a matrix draws them
|
|
2873
|
+
const matrix = out.length > first ? placedMatrix(box) : null;
|
|
2874
|
+
if (!matrix) return;
|
|
2875
|
+
for (let i = first; i < out.length; i += 1) {
|
|
2876
|
+
const band = out[i];
|
|
2877
|
+
const to = mapRect(
|
|
2878
|
+
matrix,
|
|
2879
|
+
band.x - dx,
|
|
2880
|
+
band.y - dy,
|
|
2881
|
+
band.width,
|
|
2882
|
+
band.height,
|
|
2883
|
+
);
|
|
2884
|
+
out[i] = { ...band, ...to, x: to.x + dx, y: to.y + dy };
|
|
2418
2885
|
}
|
|
2419
2886
|
}
|
|
2420
2887
|
|
|
@@ -2455,7 +2922,22 @@ function deepestAt(
|
|
|
2455
2922
|
// are painted in `z-index` order and then the document's (`byZIndex`),
|
|
2456
2923
|
// the negative ones under the flow: the Zen Garden's `›` has
|
|
2457
2924
|
// `z-index: 3` over the bar the "View All Designs" link fills after it.
|
|
2458
|
-
|
|
2925
|
+
// Nor is a box the pointer passes through under it: one that is not
|
|
2926
|
+
// visible, which is no target for the pointer in any browser (CSS 2.1
|
|
2927
|
+
// 11.2 has it invisible, and a hit test finds what is seen), or one with
|
|
2928
|
+
// `pointer-events: none` (CSS UI 4, 5.2). Both are inherited, and what is
|
|
2929
|
+
// in such a box may say otherwise, so the walk goes on through it. A
|
|
2930
|
+
// site's closed menu is laid out over its page, hidden: GitHub's covered
|
|
2931
|
+
// its repository's tabs, and the pointer over a tab was over the menu.
|
|
2932
|
+
const take = (
|
|
2933
|
+
el: Element,
|
|
2934
|
+
style: ComputedStyle,
|
|
2935
|
+
key: readonly number[],
|
|
2936
|
+
text: boolean,
|
|
2937
|
+
) => {
|
|
2938
|
+
if (style.visibility !== 'visible' || style.pointerEvents === 'none') {
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2459
2941
|
if (compareKeys(key, foundKey) < 0) return;
|
|
2460
2942
|
found = el;
|
|
2461
2943
|
foundKey = key;
|
|
@@ -2499,12 +2981,72 @@ function deepestAt(
|
|
|
2499
2981
|
atViewport = false;
|
|
2500
2982
|
}
|
|
2501
2983
|
};
|
|
2984
|
+
// A box painted through a matrix (`placedMatrix`) is under the point
|
|
2985
|
+
// where the matrix puts it: inside the rectangle around what it draws
|
|
2986
|
+
// there, the point is taken back through the matrix, into the
|
|
2987
|
+
// coordinates the box and all it holds were laid out in, and the walk
|
|
2988
|
+
// goes on with that. An icon turned a quarter is pressed where it is
|
|
2989
|
+
// drawn; a box flattened to nothing is nowhere.
|
|
2502
2990
|
const enterAt = (
|
|
2503
2991
|
child: Box,
|
|
2504
2992
|
context: readonly number[],
|
|
2505
2993
|
clipped: readonly Box[],
|
|
2994
|
+
): void => {
|
|
2995
|
+
const matrix = placedMatrix(child);
|
|
2996
|
+
if (!matrix) {
|
|
2997
|
+
enterIn(child, context, clipped, null);
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
if (
|
|
3001
|
+
x < child.boundsX ||
|
|
3002
|
+
x >= child.boundsX + child.boundsWidth ||
|
|
3003
|
+
y < child.boundsY ||
|
|
3004
|
+
y >= child.boundsY + child.boundsHeight
|
|
3005
|
+
) {
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
const back = invert(matrix);
|
|
3009
|
+
if (!back) return;
|
|
3010
|
+
const px = x;
|
|
3011
|
+
const py = y;
|
|
3012
|
+
[x, y] = mapPoint(back, px, py);
|
|
3013
|
+
try {
|
|
3014
|
+
enterIn(child, context, clipped, ownBounds(child));
|
|
3015
|
+
} finally {
|
|
3016
|
+
x = px;
|
|
3017
|
+
y = py;
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
const enterIn = (
|
|
3021
|
+
child: Box,
|
|
3022
|
+
context: readonly number[],
|
|
3023
|
+
clipped: readonly Box[],
|
|
3024
|
+
/** What the box draws in its own coordinates, where those are not the
|
|
3025
|
+
* ones its bounds are in: a box painted through a matrix. */
|
|
3026
|
+
bounds: Rect | null,
|
|
2506
3027
|
): void => {
|
|
2507
3028
|
const style = child.style;
|
|
3029
|
+
// What a `clip-path` cuts away of a box it cuts away of all the box
|
|
3030
|
+
// holds, whatever that is positioned from (CSS Masking 1, 5.1), and
|
|
3031
|
+
// nothing not drawn is under a pointer: a label a page hides for a
|
|
3032
|
+
// screen reader alone under `clip-path: inset(50%)` took the hover and
|
|
3033
|
+
// the press of what was drawn where it lay.
|
|
3034
|
+
if (pathClips(child) && !inClipPath(child, x, y)) return;
|
|
3035
|
+
// `clip` is not that rule as paint has it. It shows the part of an
|
|
3036
|
+
// absolute box it names (CSS 2.1 11.1.2), and past that part nothing is
|
|
3037
|
+
// drawn of the box, nor of what is painted with it. A stacking context
|
|
3038
|
+
// paints all it holds, inside its clip. A box that is none leaves its
|
|
3039
|
+
// positioned boxes to the context around it, which cuts each to the
|
|
3040
|
+
// clips its containing block is under (`clipsFor`): this one for all
|
|
3041
|
+
// but a fixed box, which is under none and is drawn past it. So past
|
|
3042
|
+
// its clip the first is left, and the second is walked as a box that
|
|
3043
|
+
// clips its overflow is past its edge — whether or not the point is in
|
|
3044
|
+
// its own rectangle, which a clip may show only a part of. Both were
|
|
3045
|
+
// walked as if they showed whole: a label hidden for a screen reader
|
|
3046
|
+
// alone under `clip: rect(0, 0, 0, 0)` took the hover and the press of
|
|
3047
|
+
// the link drawn where it lay.
|
|
3048
|
+
let cut = !inClip(child, x, y);
|
|
3049
|
+
if (cut && stacksLayers(child)) return;
|
|
2508
3050
|
if (
|
|
2509
3051
|
style.position !== 'static' ||
|
|
2510
3052
|
(child.parent?.kind === 'flex' && typeof style.zIndex === 'number')
|
|
@@ -2512,6 +3054,14 @@ function deepestAt(
|
|
|
2512
3054
|
// a flex item with a `z-index` is layered unpositioned (`layered`)
|
|
2513
3055
|
const z = style.zIndex === 'auto' ? 0 : style.zIndex;
|
|
2514
3056
|
context = [...context, z < 0 ? HIT_NEGATIVE : HIT_POSITIONED, z];
|
|
3057
|
+
} else if (
|
|
3058
|
+
transformed(style) &&
|
|
3059
|
+
child.kind !== 'inline' &&
|
|
3060
|
+
child.parent &&
|
|
3061
|
+
!onLine(child.parent, child)
|
|
3062
|
+
) {
|
|
3063
|
+
// a transformed box is painted with the positioned ones (`layered`)
|
|
3064
|
+
context = [...context, HIT_POSITIONED, 0];
|
|
2515
3065
|
} else if (style.float !== 'none') context = [...context, HIT_FLOAT];
|
|
2516
3066
|
if (clipped.length !== 0 && child.outOfFlow) {
|
|
2517
3067
|
const containing = containingBlockOf(child);
|
|
@@ -2519,34 +3069,46 @@ function deepestAt(
|
|
|
2519
3069
|
? clipped.filter((clip) => holds(clip, containing))
|
|
2520
3070
|
: [];
|
|
2521
3071
|
}
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
3072
|
+
// in one of its pieces, where columns broke it: its rect takes in the
|
|
3073
|
+
// columns between them, which are other boxes'
|
|
3074
|
+
const pieces = columned.any ? COLUMN_PIECES.get(child) : undefined;
|
|
3075
|
+
const inside = pieces
|
|
3076
|
+
? pieces.some(
|
|
3077
|
+
(piece) =>
|
|
3078
|
+
x >= piece.x &&
|
|
3079
|
+
x < piece.x + piece.width &&
|
|
3080
|
+
y >= piece.y &&
|
|
3081
|
+
y < piece.y + piece.height,
|
|
3082
|
+
)
|
|
3083
|
+
: x >= child.x &&
|
|
3084
|
+
x < child.x + child.width &&
|
|
3085
|
+
y >= child.y &&
|
|
3086
|
+
y < child.y + child.height;
|
|
2527
3087
|
const own = inside && hasRect(child);
|
|
2528
3088
|
if (!own) {
|
|
2529
|
-
const
|
|
2530
|
-
const
|
|
2531
|
-
|
|
2532
|
-
|
|
3089
|
+
const left = bounds ? bounds.x : child.boundsX;
|
|
3090
|
+
const reach = bounds ? bounds.y : child.boundsY;
|
|
3091
|
+
const across = bounds ? bounds.width : child.boundsWidth;
|
|
3092
|
+
const down = bounds ? bounds.height : child.boundsHeight;
|
|
3093
|
+
const known = Number.isFinite(reach) && (across > 0 || down > 0);
|
|
2533
3094
|
if (!known) {
|
|
2534
3095
|
if (!inside) return;
|
|
2535
3096
|
} else if (
|
|
2536
|
-
x <
|
|
2537
|
-
x >=
|
|
3097
|
+
x < left ||
|
|
3098
|
+
x >= left + across ||
|
|
2538
3099
|
y < reach ||
|
|
2539
|
-
y >= reach +
|
|
3100
|
+
y >= reach + down
|
|
2540
3101
|
) {
|
|
2541
3102
|
return;
|
|
2542
3103
|
}
|
|
2543
|
-
if (clipsOverflow(child))
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
3104
|
+
if (clipsOverflow(child)) cut = true;
|
|
3105
|
+
}
|
|
3106
|
+
if (cut) {
|
|
3107
|
+
if (!holdsAbsolute(child)) return;
|
|
3108
|
+
clipped = [...clipped, child];
|
|
2547
3109
|
}
|
|
2548
3110
|
if (own && child.el && clipped.length === 0) {
|
|
2549
|
-
take(child.el, [...context, HIT_BLOCK], false);
|
|
3111
|
+
take(child.el, style, [...context, HIT_BLOCK], false);
|
|
2550
3112
|
}
|
|
2551
3113
|
visit(child, context, clipped);
|
|
2552
3114
|
};
|
|
@@ -2593,7 +3155,8 @@ function deepestAt(
|
|
|
2593
3155
|
tree.textBoxes,
|
|
2594
3156
|
text.spans.documentAt(run.start),
|
|
2595
3157
|
);
|
|
2596
|
-
|
|
3158
|
+
const style = owner && tree.styles.get(owner)?.style;
|
|
3159
|
+
if (style) take(owner, style, [...context, HIT_INLINE], true);
|
|
2597
3160
|
}
|
|
2598
3161
|
}
|
|
2599
3162
|
}
|
|
@@ -2621,6 +3184,38 @@ function compareKeys(a: readonly number[], b: readonly number[]): number {
|
|
|
2621
3184
|
* that matches a container reaches everything in it. */
|
|
2622
3185
|
const HOVER_RESTYLE_LIMIT = 300;
|
|
2623
3186
|
|
|
3187
|
+
/** How long the content under a still pointer has to have stopped moving
|
|
3188
|
+
* before the hover is asked for again (`HtmlViewNode._holdHover`): a
|
|
3189
|
+
* tenth of a second, which is what WebKit waits after a scroll before it
|
|
3190
|
+
* sends the mouse move that updates its own. */
|
|
3191
|
+
const HOVER_REST_MS = 100;
|
|
3192
|
+
|
|
3193
|
+
const timers = globalThis as {
|
|
3194
|
+
setTimeout?(fn: () => void, ms: number): unknown;
|
|
3195
|
+
clearTimeout?(id: unknown): void;
|
|
3196
|
+
};
|
|
3197
|
+
|
|
3198
|
+
/**
|
|
3199
|
+
* The clock a held hover waits on. Through `globalThis` because `src/`
|
|
3200
|
+
* compiles with `types: []`, and unref'd where the runtime allows it: a
|
|
3201
|
+
* timer of a document's must not keep a process alive that is otherwise
|
|
3202
|
+
* done. One object, exported from this module though not from the package,
|
|
3203
|
+
* so a test can hold it and say when the rest is over (`test/held-clock.ts`).
|
|
3204
|
+
*/
|
|
3205
|
+
export const hoverClock = {
|
|
3206
|
+
now(): number {
|
|
3207
|
+
return Date.now();
|
|
3208
|
+
},
|
|
3209
|
+
arm(step: () => void, ms: number): unknown {
|
|
3210
|
+
const handle = timers.setTimeout?.(step, ms) ?? null;
|
|
3211
|
+
(handle as { unref?(): void } | null)?.unref?.();
|
|
3212
|
+
return handle;
|
|
3213
|
+
},
|
|
3214
|
+
disarm(handle: unknown): void {
|
|
3215
|
+
timers.clearTimeout?.(handle);
|
|
3216
|
+
},
|
|
3217
|
+
};
|
|
3218
|
+
|
|
2624
3219
|
/**
|
|
2625
3220
|
* The computed properties a pointer move may change in place: ink, which
|
|
2626
3221
|
* moves nothing. A background colour is drawn inside the box it colours
|
|
@@ -2632,6 +3227,9 @@ const PAINT_ONLY = new Set([
|
|
|
2632
3227
|
'color',
|
|
2633
3228
|
'textFillColor',
|
|
2634
3229
|
'cursor',
|
|
3230
|
+
'pointerEvents',
|
|
3231
|
+
'fill',
|
|
3232
|
+
'stroke',
|
|
2635
3233
|
'backgroundColor',
|
|
2636
3234
|
'borderTopColor',
|
|
2637
3235
|
'borderRightColor',
|
|
@@ -2648,6 +3246,8 @@ const PAINT_ONLY = new Set([
|
|
|
2648
3246
|
'underlineThickness',
|
|
2649
3247
|
'underlineOffset',
|
|
2650
3248
|
'lineThrough',
|
|
3249
|
+
'lineThroughStyle',
|
|
3250
|
+
'lineThroughThickness',
|
|
2651
3251
|
]);
|
|
2652
3252
|
|
|
2653
3253
|
/** The fields of a run its ink is: what `runFor` takes from `PAINT_ONLY`. */
|
|
@@ -2658,6 +3258,8 @@ const INK_FIELDS = [
|
|
|
2658
3258
|
'underlineOffset',
|
|
2659
3259
|
'underlineThickness',
|
|
2660
3260
|
'strike',
|
|
3261
|
+
'strikeStyle',
|
|
3262
|
+
'strikeThickness',
|
|
2661
3263
|
] as const;
|
|
2662
3264
|
|
|
2663
3265
|
/** The fields of a run its shape is, which ink never changes. */
|
|
@@ -2676,11 +3278,21 @@ const FACE_FIELDS = [
|
|
|
2676
3278
|
/** What a restyle in place changed beyond ink: how far a box's ink
|
|
2677
3279
|
* reaches, the order its layer paints in, or where it is. */
|
|
2678
3280
|
interface HoverChange {
|
|
3281
|
+
/** Whether anything that is drawn changed: false where only the custom
|
|
3282
|
+
* properties did, which the elements under this one read. */
|
|
3283
|
+
ink: boolean;
|
|
2679
3284
|
reach: boolean;
|
|
2680
3285
|
order: boolean;
|
|
2681
3286
|
move: boolean;
|
|
2682
3287
|
}
|
|
2683
3288
|
|
|
3289
|
+
/** An element's `::before` and `::after`, each with its bit in the set
|
|
3290
|
+
* of the ones a restyle found boxes of (`_hoverInPlace`). */
|
|
3291
|
+
const GENERATED_BITS = [
|
|
3292
|
+
['before', 1],
|
|
3293
|
+
['after', 2],
|
|
3294
|
+
] as const;
|
|
3295
|
+
|
|
2684
3296
|
/** The replaced boxes that are controls (`BoxTree.controls`, `controls.ts`):
|
|
2685
3297
|
* a widget mounted beside the document, or a button whose press it reports. */
|
|
2686
3298
|
const WIDGETS = new Set<ReplacedKind>([
|
|
@@ -2701,10 +3313,20 @@ const INK_REACH = new Set([
|
|
|
2701
3313
|
'outlineOffset',
|
|
2702
3314
|
]);
|
|
2703
3315
|
|
|
3316
|
+
/** What transforms a box: it moves, turns or scales where it is, and
|
|
3317
|
+
* nothing around it does (CSS Transforms 1, 3). */
|
|
3318
|
+
const TRANSFORM_FIELDS = new Set([
|
|
3319
|
+
'translate',
|
|
3320
|
+
'rotate',
|
|
3321
|
+
'scale',
|
|
3322
|
+
'transform',
|
|
3323
|
+
'transformOrigin',
|
|
3324
|
+
]);
|
|
3325
|
+
|
|
2704
3326
|
/**
|
|
2705
3327
|
* How two styles of one element differ, for a restyle in place: null where
|
|
2706
3328
|
* they do not, a `HoverChange` where every difference is ink, how far ink
|
|
2707
|
-
* reaches, a `z-index` that stays a stacking context's, or a
|
|
3329
|
+
* reaches, a `z-index` that stays a stacking context's, or a transform —
|
|
2708
3330
|
* none of which moves anything but the box and what it holds (CSS
|
|
2709
3331
|
* Transforms 1: a transform does not affect layout) — and false where
|
|
2710
3332
|
* anything else differs.
|
|
@@ -2717,8 +3339,17 @@ function hoverChange(
|
|
|
2717
3339
|
const b = now as unknown as Record<string, unknown>;
|
|
2718
3340
|
let change: HoverChange | null = null;
|
|
2719
3341
|
for (const key in b) {
|
|
2720
|
-
|
|
2721
|
-
|
|
3342
|
+
// the sets are shared by what made them (`Cascade._customFor`): one
|
|
3343
|
+
// object where they are the same
|
|
3344
|
+
if (key === 'custom') {
|
|
3345
|
+
if (a[key] !== b[key]) {
|
|
3346
|
+
change ??= { ink: false, reach: false, order: false, move: false };
|
|
3347
|
+
}
|
|
3348
|
+
continue;
|
|
3349
|
+
}
|
|
3350
|
+
if (sameValue(a[key], b[key])) continue;
|
|
3351
|
+
change ??= { ink: true, reach: false, order: false, move: false };
|
|
3352
|
+
change.ink = true;
|
|
2722
3353
|
if (PAINT_ONLY.has(key)) continue;
|
|
2723
3354
|
if (INK_REACH.has(key)) {
|
|
2724
3355
|
change.reach = true;
|
|
@@ -2734,7 +3365,7 @@ function hoverChange(
|
|
|
2734
3365
|
change.order = true;
|
|
2735
3366
|
continue;
|
|
2736
3367
|
}
|
|
2737
|
-
if (key
|
|
3368
|
+
if (TRANSFORM_FIELDS.has(key)) {
|
|
2738
3369
|
change.move = true;
|
|
2739
3370
|
continue;
|
|
2740
3371
|
}
|
|
@@ -2777,12 +3408,17 @@ function inkOf(box: Box): Rect | null {
|
|
|
2777
3408
|
}
|
|
2778
3409
|
if (!at || at.boundsY === Infinity) return null;
|
|
2779
3410
|
if (!(at.boundsWidth > 0 && at.boundsHeight > 0)) return null;
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
3411
|
+
// its bounds are in the coordinates of the box it is in, which a box
|
|
3412
|
+
// around it painted through a matrix draws somewhere else
|
|
3413
|
+
return throughTransforms(
|
|
3414
|
+
{
|
|
3415
|
+
x: at.boundsX,
|
|
3416
|
+
y: at.boundsY,
|
|
3417
|
+
width: at.boundsWidth,
|
|
3418
|
+
height: at.boundsHeight,
|
|
3419
|
+
},
|
|
3420
|
+
at.parent,
|
|
3421
|
+
);
|
|
2786
3422
|
}
|
|
2787
3423
|
|
|
2788
3424
|
/** Structural equality for a computed value: a number, a string, or the
|
|
@@ -2802,16 +3438,9 @@ function sameValue(x: unknown, y: unknown): boolean {
|
|
|
2802
3438
|
return true;
|
|
2803
3439
|
}
|
|
2804
3440
|
|
|
2805
|
-
/** The element an anonymous box takes its style from: its nearest
|
|
2806
|
-
* ancestor's that has one. */
|
|
2807
|
-
function nearestElement(box: Box): Element | null {
|
|
2808
|
-
for (let at = box.parent; at; at = at.parent) if (at.el) return at.el;
|
|
2809
|
-
return null;
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
3441
|
/**
|
|
2813
|
-
* `text`'s layout made again with the ink of the
|
|
2814
|
-
* restyled: from the runs it was made from, each of a
|
|
3442
|
+
* `text`'s layout made again with the ink of the boxes a pointer move
|
|
3443
|
+
* restyled: from the runs it was made from, each of a restyled text box's
|
|
2815
3444
|
* given the ink its new style makes (`runFor`) and nothing else. Null where
|
|
2816
3445
|
* none of its runs is theirs; false where that cannot be told — a run this
|
|
2817
3446
|
* cannot place in the document, one some other pass inked (a first line's),
|
|
@@ -2820,19 +3449,16 @@ function nearestElement(box: Box): Element | null {
|
|
|
2820
3449
|
function reinked(
|
|
2821
3450
|
text: LineText,
|
|
2822
3451
|
layouts: TextLayoutCache,
|
|
2823
|
-
|
|
2824
|
-
styles: BoxTree['styles'],
|
|
3452
|
+
next: ReadonlyMap<Box, ComputedStyle>,
|
|
2825
3453
|
): TextLayoutLike | null | false {
|
|
2826
3454
|
const spans = text.spans;
|
|
2827
|
-
const ownerAt = (offset: number): Element | null =>
|
|
2828
|
-
spans.boxAt ? (spans.boxAt(offset)?.el ?? null) : null;
|
|
2829
3455
|
const inputs = layouts.inputsOf(text.layout);
|
|
2830
3456
|
if (!inputs || !spans.boxAt) {
|
|
2831
3457
|
// not one this could make again: none of its text may be theirs
|
|
2832
3458
|
for (const line of text.layout.lines) {
|
|
2833
3459
|
for (const run of line.runs) {
|
|
2834
|
-
const
|
|
2835
|
-
if (!spans.boxAt || (
|
|
3460
|
+
const box = spans.boxAt?.(run.start);
|
|
3461
|
+
if (!spans.boxAt || (box && next.has(box))) return false;
|
|
2836
3462
|
}
|
|
2837
3463
|
}
|
|
2838
3464
|
return null;
|
|
@@ -2843,25 +3469,25 @@ function reinked(
|
|
|
2843
3469
|
for (let i = 0; i < content.length; i += 1) {
|
|
2844
3470
|
const run = content[i];
|
|
2845
3471
|
const length = run.text.length;
|
|
2846
|
-
const
|
|
2847
|
-
const style =
|
|
2848
|
-
if (
|
|
2849
|
-
if (
|
|
2850
|
-
const was = runFor(run.text,
|
|
3472
|
+
const box = length ? spans.boxAt(offset) : null;
|
|
3473
|
+
const style = box ? next.get(box) : undefined;
|
|
3474
|
+
if (box && style) {
|
|
3475
|
+
if (spans.boxAt(offset + length - 1) !== box) return false;
|
|
3476
|
+
const was = runFor(run.text, box.style);
|
|
2851
3477
|
const now = runFor(run.text, style);
|
|
2852
3478
|
for (const f of FACE_FIELDS) if (!sameValue(was[f], now[f])) return false;
|
|
2853
|
-
let
|
|
3479
|
+
let inked: Record<string, unknown> | null = null;
|
|
2854
3480
|
for (const f of INK_FIELDS) {
|
|
2855
3481
|
// a run that is not what its style made — a first line's colour
|
|
2856
3482
|
if (!sameValue(run[f], was[f])) return false;
|
|
2857
3483
|
if (sameValue(was[f], now[f])) continue;
|
|
2858
|
-
|
|
2859
|
-
if (now[f] === undefined) delete
|
|
2860
|
-
else
|
|
3484
|
+
inked ??= { ...run };
|
|
3485
|
+
if (now[f] === undefined) delete inked[f];
|
|
3486
|
+
else inked[f] = now[f];
|
|
2861
3487
|
}
|
|
2862
|
-
if (
|
|
3488
|
+
if (inked) {
|
|
2863
3489
|
runs ??= content.slice();
|
|
2864
|
-
runs[i] =
|
|
3490
|
+
runs[i] = inked as unknown as TextRun;
|
|
2865
3491
|
}
|
|
2866
3492
|
}
|
|
2867
3493
|
offset += length;
|