@react-x11/components 0.4.0 → 0.5.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/README.md +125 -4
- package/dist/charts/components.d.ts +3 -1
- package/dist/charts/components.d.ts.map +1 -1
- package/dist/charts/components.js +8 -4
- package/dist/charts/components.js.map +1 -1
- package/dist/charts/node.d.ts +13 -6
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +75 -24
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +4 -0
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +11 -6
- package/dist/charts/render.js.map +1 -1
- package/dist/code-editor/node.d.ts +47 -16
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +121 -51
- package/dist/code-editor/node.js.map +1 -1
- package/dist/flow/draw.d.ts +19 -1
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +64 -35
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +24 -8
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/model.d.ts +4 -2
- package/dist/flow/model.d.ts.map +1 -1
- package/dist/flow/model.js +4 -2
- package/dist/flow/model.js.map +1 -1
- package/dist/flow/node.d.ts +40 -9
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +142 -53
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/types.d.ts +46 -21
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/formula/index.d.ts +4 -2
- package/dist/formula/index.d.ts.map +1 -1
- package/dist/formula/index.js.map +1 -1
- package/dist/formula/node.d.ts +5 -1
- package/dist/formula/node.d.ts.map +1 -1
- package/dist/formula/node.js +19 -3
- package/dist/formula/node.js.map +1 -1
- package/dist/html/controls.d.ts +4 -1
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +11 -5
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +18 -8
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +15 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +41 -16
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +3 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3 -1
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/values.d.ts +14 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +11 -8
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +1 -0
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +12 -5
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +7 -4
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +22 -1
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +22 -8
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +97 -15
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +3 -0
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +2 -2
- package/dist/html/paint.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/maps/clip.d.ts +43 -0
- package/dist/maps/clip.d.ts.map +1 -0
- package/dist/maps/clip.js +171 -0
- package/dist/maps/clip.js.map +1 -0
- package/dist/maps/gzip.d.ts +15 -0
- package/dist/maps/gzip.d.ts.map +1 -0
- package/dist/maps/gzip.js +56 -0
- package/dist/maps/gzip.js.map +1 -0
- package/dist/maps/index.d.ts +55 -0
- package/dist/maps/index.d.ts.map +1 -0
- package/dist/maps/index.js +141 -0
- package/dist/maps/index.js.map +1 -0
- package/dist/maps/labels.d.ts +123 -0
- package/dist/maps/labels.d.ts.map +1 -0
- package/dist/maps/labels.js +323 -0
- package/dist/maps/labels.js.map +1 -0
- package/dist/maps/mvt.d.ts +244 -0
- package/dist/maps/mvt.d.ts.map +1 -0
- package/dist/maps/mvt.js +820 -0
- package/dist/maps/mvt.js.map +1 -0
- package/dist/maps/node.d.ts +278 -0
- package/dist/maps/node.d.ts.map +1 -0
- package/dist/maps/node.js +1459 -0
- package/dist/maps/node.js.map +1 -0
- package/dist/maps/overlay.d.ts +157 -0
- package/dist/maps/overlay.d.ts.map +1 -0
- package/dist/maps/overlay.js +472 -0
- package/dist/maps/overlay.js.map +1 -0
- package/dist/maps/paint.d.ts +226 -0
- package/dist/maps/paint.d.ts.map +1 -0
- package/dist/maps/paint.js +705 -0
- package/dist/maps/paint.js.map +1 -0
- package/dist/maps/proj.d.ts +281 -0
- package/dist/maps/proj.d.ts.map +1 -0
- package/dist/maps/proj.js +451 -0
- package/dist/maps/proj.js.map +1 -0
- package/dist/maps/sources.d.ts +256 -0
- package/dist/maps/sources.d.ts.map +1 -0
- package/dist/maps/sources.js +210 -0
- package/dist/maps/sources.js.map +1 -0
- package/dist/maps/style.d.ts +141 -0
- package/dist/maps/style.d.ts.map +1 -0
- package/dist/maps/style.js +147 -0
- package/dist/maps/style.js.map +1 -0
- package/dist/maps/styles.d.ts +82 -0
- package/dist/maps/styles.d.ts.map +1 -0
- package/dist/maps/styles.js +898 -0
- package/dist/maps/styles.js.map +1 -0
- package/dist/maps/tiles.d.ts +283 -0
- package/dist/maps/tiles.d.ts.map +1 -0
- package/dist/maps/tiles.js +469 -0
- package/dist/maps/tiles.js.map +1 -0
- package/dist/maps/types.d.ts +244 -0
- package/dist/maps/types.d.ts.map +1 -0
- package/dist/maps/types.js +2 -0
- package/dist/maps/types.js.map +1 -0
- package/dist/reorder/hx.d.ts +18 -0
- package/dist/reorder/hx.d.ts.map +1 -0
- package/dist/reorder/hx.js +10 -0
- package/dist/reorder/hx.js.map +1 -0
- package/dist/reorder/index.d.ts +314 -0
- package/dist/reorder/index.d.ts.map +1 -0
- package/dist/reorder/index.js +1539 -0
- package/dist/reorder/index.js.map +1 -0
- package/dist/reorder/model.d.ts +107 -0
- package/dist/reorder/model.d.ts.map +1 -0
- package/dist/reorder/model.js +162 -0
- package/dist/reorder/model.js.map +1 -0
- package/dist/richtext/node.d.ts +15 -5
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +40 -11
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +35 -13
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +83 -27
- package/dist/richtext/runs.js.map +1 -1
- package/dist/tabs/index.d.ts +17 -0
- package/dist/tabs/index.d.ts.map +1 -1
- package/dist/tabs/index.js +849 -186
- package/dist/tabs/index.js.map +1 -1
- package/dist/terminal/vt/fonts.d.ts +45 -15
- package/dist/terminal/vt/fonts.d.ts.map +1 -1
- package/dist/terminal/vt/fonts.js +89 -19
- package/dist/terminal/vt/fonts.js.map +1 -1
- package/dist/terminal/vt/node.d.ts +42 -0
- package/dist/terminal/vt/node.d.ts.map +1 -1
- package/dist/terminal/vt/node.js +95 -11
- package/dist/terminal/vt/node.js.map +1 -1
- package/dist/terminal/vt/renderer.d.ts.map +1 -1
- package/dist/terminal/vt/renderer.js +13 -3
- package/dist/terminal/vt/renderer.js.map +1 -1
- package/package.json +13 -3
- package/src/charts/components.ts +14 -7
- package/src/charts/node.ts +81 -26
- package/src/charts/render.ts +16 -6
- package/src/code-editor/node.ts +147 -53
- package/src/flow/draw.ts +104 -35
- package/src/flow/index.ts +33 -13
- package/src/flow/model.ts +4 -2
- package/src/flow/node.ts +158 -59
- package/src/flow/types.ts +46 -21
- package/src/formula/index.ts +4 -2
- package/src/formula/node.ts +24 -5
- package/src/html/controls.ts +4 -1
- package/src/html/css/cascade.ts +18 -7
- package/src/html/css/parse.ts +46 -21
- package/src/html/css/style.ts +6 -1
- package/src/html/css/values.ts +25 -9
- package/src/html/index.ts +1 -0
- package/src/html/layout/boxes.ts +19 -9
- package/src/html/layout/inline.ts +23 -1
- package/src/html/node.ts +110 -17
- package/src/html/paint.ts +17 -2
- package/src/index.ts +98 -0
- package/src/maps/clip.ts +184 -0
- package/src/maps/gzip.ts +66 -0
- package/src/maps/index.ts +316 -0
- package/src/maps/labels.ts +442 -0
- package/src/maps/mvt.ts +857 -0
- package/src/maps/node.ts +1819 -0
- package/src/maps/overlay.ts +639 -0
- package/src/maps/paint.ts +978 -0
- package/src/maps/proj.ts +641 -0
- package/src/maps/sources.ts +425 -0
- package/src/maps/style.ts +309 -0
- package/src/maps/styles.ts +1040 -0
- package/src/maps/tiles.ts +676 -0
- package/src/maps/types.ts +260 -0
- package/src/reorder/hx.ts +31 -0
- package/src/reorder/index.ts +2223 -0
- package/src/reorder/model.ts +232 -0
- package/src/richtext/node.ts +49 -14
- package/src/richtext/runs.ts +115 -30
- package/src/tabs/index.ts +1040 -204
- package/src/terminal/vt/fonts.ts +128 -36
- package/src/terminal/vt/node.ts +105 -12
- package/src/terminal/vt/renderer.ts +13 -3
package/src/html/controls.ts
CHANGED
|
@@ -35,7 +35,10 @@ export interface ControlChrome {
|
|
|
35
35
|
borderColor: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** Where a control goes, in the element's own coordinate space.
|
|
38
|
+
/** Where a control goes, in the element's own coordinate space. Inside the
|
|
39
|
+
* engine (`controlRectsOf`) these are device pixels like every box; what
|
|
40
|
+
* `onControls` reports is the same rect in logical pixels, because it
|
|
41
|
+
* becomes the style of a widget. */
|
|
39
42
|
export interface ControlRect {
|
|
40
43
|
element: Element;
|
|
41
44
|
kind: ReplacedKind;
|
package/src/html/css/cascade.ts
CHANGED
|
@@ -219,10 +219,15 @@ export class Cascade {
|
|
|
219
219
|
private _pointer: PointerState = NO_POINTER;
|
|
220
220
|
readonly initial: ComputedStyle;
|
|
221
221
|
readonly look: RootLook;
|
|
222
|
-
/** Viewport width the media queries were evaluated at
|
|
222
|
+
/** Viewport width the media queries were evaluated at, in device pixels
|
|
223
|
+
* like every other length here. */
|
|
223
224
|
viewportWidth: number;
|
|
224
225
|
viewportHeight: number;
|
|
225
|
-
/**
|
|
226
|
+
/** Device pixels per CSS pixel. Every computed length is device; a
|
|
227
|
+
* `@media` width is the one CSS-pixel comparison left, and it divides. */
|
|
228
|
+
readonly scale: number;
|
|
229
|
+
/** Every width at which some `@media` rule changes its mind, in CSS
|
|
230
|
+
* pixels — the unit the author wrote them in. */
|
|
226
231
|
readonly breakpoints: number[];
|
|
227
232
|
|
|
228
233
|
constructor(
|
|
@@ -230,11 +235,13 @@ export class Cascade {
|
|
|
230
235
|
look: RootLook,
|
|
231
236
|
viewportWidth: number,
|
|
232
237
|
viewportHeight: number,
|
|
238
|
+
scale = 1,
|
|
233
239
|
) {
|
|
234
240
|
this.look = look;
|
|
235
241
|
this.initial = initialStyle(look);
|
|
236
242
|
this.viewportWidth = viewportWidth;
|
|
237
243
|
this.viewportHeight = viewportHeight;
|
|
244
|
+
this.scale = scale;
|
|
238
245
|
const breakpoints = new Set<number>();
|
|
239
246
|
for (const sheet of sheets) {
|
|
240
247
|
for (const rule of sheet.rules) this._index.add(rule);
|
|
@@ -266,12 +273,14 @@ export class Cascade {
|
|
|
266
273
|
this._pointer = pointer;
|
|
267
274
|
}
|
|
268
275
|
|
|
269
|
-
/** Which media band a width falls in. Two widths in the same
|
|
270
|
-
* identical styles, which is what lets a resize skip
|
|
276
|
+
/** Which media band a device-pixel width falls in. Two widths in the same
|
|
277
|
+
* band produce identical styles, which is what lets a resize skip
|
|
278
|
+
* restyling. */
|
|
271
279
|
mediaBand(width: number): number {
|
|
280
|
+
const cssWidth = width / this.scale;
|
|
272
281
|
let band = 0;
|
|
273
282
|
for (const bp of this.breakpoints) {
|
|
274
|
-
if (
|
|
283
|
+
if (cssWidth >= bp) band += 1;
|
|
275
284
|
else break;
|
|
276
285
|
}
|
|
277
286
|
return band;
|
|
@@ -299,6 +308,7 @@ export class Cascade {
|
|
|
299
308
|
rem: this.initial.fontSize,
|
|
300
309
|
vw: this.viewportWidth,
|
|
301
310
|
vh: this.viewportHeight,
|
|
311
|
+
scale: this.scale,
|
|
302
312
|
};
|
|
303
313
|
for (const c of candidates) {
|
|
304
314
|
for (const d of pick(c)) {
|
|
@@ -345,13 +355,14 @@ export class Cascade {
|
|
|
345
355
|
|
|
346
356
|
private _candidates(el: Element): Candidate[] {
|
|
347
357
|
const out: Candidate[] = [];
|
|
348
|
-
|
|
358
|
+
// A media query's width is CSS pixels; the viewport is kept in device.
|
|
359
|
+
const width = this.viewportWidth / this.scale;
|
|
349
360
|
|
|
350
361
|
const consider = (bucket: IndexedRule[] | undefined): void => {
|
|
351
362
|
if (!bucket) return;
|
|
352
363
|
for (const indexed of bucket) {
|
|
353
364
|
const rule = indexed.rule;
|
|
354
|
-
if (!mediaMatches(rule.media, width)) continue;
|
|
365
|
+
if (!mediaMatches(rule.media, width, this.look.colorScheme)) continue;
|
|
355
366
|
if (!indexed.compiled) {
|
|
356
367
|
indexed.compiled = true;
|
|
357
368
|
try {
|
package/src/html/css/parse.ts
CHANGED
|
@@ -51,13 +51,17 @@ export interface Stylesheet {
|
|
|
51
51
|
breakpoints: number[];
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/** The
|
|
55
|
-
* `orientation`, `print` — is
|
|
54
|
+
/** The tests this evaluates live: a width, a colour scheme, or both.
|
|
55
|
+
* Anything else — `orientation`, `print`, `prefers-reduced-motion` — is
|
|
56
|
+
* decided once, at parse time, by `staticPass`. */
|
|
56
57
|
export interface MediaCondition {
|
|
57
58
|
min?: number;
|
|
58
59
|
max?: number;
|
|
59
|
-
/**
|
|
60
|
-
|
|
60
|
+
/** `prefers-color-scheme`, answered from the palette in force. */
|
|
61
|
+
scheme?: 'light' | 'dark';
|
|
62
|
+
/** Set when the query could not be evaluated as a width or scheme test:
|
|
63
|
+
* `true` keeps the rule, `false` drops it, and neither depends on the
|
|
64
|
+
* viewport or the theme. */
|
|
61
65
|
staticPass?: boolean;
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -403,10 +407,10 @@ function importUrl(prelude: string): string | null {
|
|
|
403
407
|
}
|
|
404
408
|
|
|
405
409
|
/**
|
|
406
|
-
* A `@media` prelude, reduced to the width tests this can
|
|
407
|
-
* group is one condition and they are OR-ed; within a
|
|
408
|
-
* tests intersect, so a group with a feature this
|
|
409
|
-
* decided statically by that feature alone.
|
|
410
|
+
* A `@media` prelude, reduced to the width and colour-scheme tests this can
|
|
411
|
+
* honour. Each comma group is one condition and they are OR-ed; within a
|
|
412
|
+
* group, `and` means the tests intersect, so a group with a feature this
|
|
413
|
+
* does not understand is decided statically by that feature alone.
|
|
410
414
|
*/
|
|
411
415
|
export function parseMediaQuery(prelude: string): MediaCondition[] {
|
|
412
416
|
const out: MediaCondition[] = [];
|
|
@@ -432,14 +436,23 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
|
|
|
432
436
|
} else if (key === 'max-width' && px !== null) {
|
|
433
437
|
condition.max = Math.min(condition.max ?? Infinity, px);
|
|
434
438
|
sawWidth = true;
|
|
435
|
-
} else if (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
439
|
+
} else if (key === 'prefers-color-scheme') {
|
|
440
|
+
// Answered live, from the palette in force: a document dropped
|
|
441
|
+
// into a dark application takes its dark branch, and follows the
|
|
442
|
+
// desktop when that changes. The two schemes are the whole
|
|
443
|
+
// vocabulary; anything else never matches.
|
|
444
|
+
const scheme = feature[2].trim().toLowerCase();
|
|
445
|
+
if (scheme === 'light' || scheme === 'dark') {
|
|
446
|
+
if (condition.scheme && condition.scheme !== scheme) pass = false;
|
|
447
|
+
condition.scheme = scheme;
|
|
448
|
+
} else {
|
|
449
|
+
pass = false;
|
|
450
|
+
}
|
|
451
|
+
} else if (key === 'prefers-reduced-motion') {
|
|
452
|
+
// Nothing here moves, so there is nothing to reduce: the branch an
|
|
453
|
+
// animated page keeps for this preference is not one this
|
|
454
|
+
// renderer needs.
|
|
455
|
+
pass = false;
|
|
443
456
|
} else if (key === 'orientation') {
|
|
444
457
|
pass = feature[2].trim().toLowerCase() === 'landscape';
|
|
445
458
|
}
|
|
@@ -450,8 +463,13 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
|
|
|
450
463
|
if (type === 'print' || type === 'speech') pass = false;
|
|
451
464
|
}
|
|
452
465
|
if (negated) {
|
|
453
|
-
// `not` over a
|
|
454
|
-
//
|
|
466
|
+
// `not` over a scheme is the other scheme. `not` over a width range
|
|
467
|
+
// is not expressible as one range; the honest reduction is to decide
|
|
468
|
+
// it statically rather than invert it wrongly.
|
|
469
|
+
if (!sawWidth && pass && condition.scheme) {
|
|
470
|
+
out.push({ scheme: condition.scheme === 'dark' ? 'light' : 'dark' });
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
455
473
|
out.push({ staticPass: !sawWidth && pass ? false : !pass });
|
|
456
474
|
continue;
|
|
457
475
|
}
|
|
@@ -461,6 +479,7 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
|
|
|
461
479
|
}
|
|
462
480
|
if (
|
|
463
481
|
!sawWidth &&
|
|
482
|
+
condition.scheme === undefined &&
|
|
464
483
|
condition.min === undefined &&
|
|
465
484
|
condition.max === undefined
|
|
466
485
|
) {
|
|
@@ -472,12 +491,17 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
|
|
|
472
491
|
return out.length ? out : [{ staticPass: true }];
|
|
473
492
|
}
|
|
474
493
|
|
|
475
|
-
|
|
494
|
+
/** A `@media` width is compared with the viewport in CSS pixels — the
|
|
495
|
+
* cascade divides the device width by the scale before asking — so the
|
|
496
|
+
* thresholds parse at scale 1 whatever panel the document lands on. */
|
|
497
|
+
const ZERO_UNITS = { em: 16, rem: 16, vw: 0, vh: 0, scale: 1 };
|
|
476
498
|
|
|
477
|
-
/** Whether a rule's `@media` blocks all hold at this viewport width
|
|
499
|
+
/** Whether a rule's `@media` blocks all hold at this viewport width, under
|
|
500
|
+
* this colour scheme. */
|
|
478
501
|
export function mediaMatches(
|
|
479
502
|
media: MediaCondition[][] | null,
|
|
480
503
|
width: number,
|
|
504
|
+
scheme: 'light' | 'dark' = 'light',
|
|
481
505
|
): boolean {
|
|
482
506
|
if (!media) return true;
|
|
483
507
|
for (const block of media) {
|
|
@@ -489,7 +513,8 @@ export function mediaMatches(
|
|
|
489
513
|
}
|
|
490
514
|
if (
|
|
491
515
|
(c.min === undefined || width >= c.min) &&
|
|
492
|
-
(c.max === undefined || width <= c.max)
|
|
516
|
+
(c.max === undefined || width <= c.max) &&
|
|
517
|
+
(c.scheme === undefined || c.scheme === scheme)
|
|
493
518
|
) {
|
|
494
519
|
any = true;
|
|
495
520
|
}
|
package/src/html/css/style.ts
CHANGED
|
@@ -214,6 +214,9 @@ export interface RootLook {
|
|
|
214
214
|
borderColor: string;
|
|
215
215
|
mutedColor: string;
|
|
216
216
|
background: string;
|
|
217
|
+
/** Which scheme the palette is — what `@media (prefers-color-scheme)`
|
|
218
|
+
* is answered from. */
|
|
219
|
+
colorScheme: 'light' | 'dark';
|
|
217
220
|
/**
|
|
218
221
|
* The palette's own control chrome, carried here because the **box in the
|
|
219
222
|
* flow has to be the size the widget will be** and layout runs long before
|
|
@@ -1063,7 +1066,9 @@ function alignKeyword(value: string): string | null {
|
|
|
1063
1066
|
|
|
1064
1067
|
function borderWidth(value: string, ctx: UnitContext): number | null {
|
|
1065
1068
|
const kw = BORDER_WIDTH_KEYWORDS[value.trim().toLowerCase()];
|
|
1066
|
-
|
|
1069
|
+
// The keywords are CSS pixels that never pass through `parseLength`, so
|
|
1070
|
+
// they take the display scale here.
|
|
1071
|
+
if (kw !== undefined) return kw * ctx.scale;
|
|
1067
1072
|
const len = parseLength(value, ctx);
|
|
1068
1073
|
return typeof len === 'number' ? Math.max(0, len) : null;
|
|
1069
1074
|
}
|
package/src/html/css/values.ts
CHANGED
|
@@ -54,7 +54,18 @@ export function resolveOrNull(len: Len, base: number): number | null {
|
|
|
54
54
|
return Number.isFinite(base) ? (len.pct / 100) * base : null;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The context a length parses against.
|
|
59
|
+
*
|
|
60
|
+
* Every length that comes out of `parseLength` is in **device pixels** —
|
|
61
|
+
* the unit the box tree, the paint and the hit tests all share. `em`, `rem`,
|
|
62
|
+
* `vw` and `vh` are already device (the root font size and the viewport are
|
|
63
|
+
* handed over that way); the absolute units are CSS pixels the author wrote,
|
|
64
|
+
* and `scale` is how many device pixels one of those is (core's
|
|
65
|
+
* `docs/scale.md`). At 1x it is the identity, which is how a renderer that
|
|
66
|
+
* never multiplied passed every test and drew a `16px` heading eight logical
|
|
67
|
+
* pixels tall on a retina panel.
|
|
68
|
+
*/
|
|
58
69
|
export interface UnitContext {
|
|
59
70
|
/** The element's own `font-size`, already computed. */
|
|
60
71
|
em: number;
|
|
@@ -63,6 +74,8 @@ export interface UnitContext {
|
|
|
63
74
|
/** Viewport, for `vw`/`vh`/`vmin`/`vmax`. */
|
|
64
75
|
vw: number;
|
|
65
76
|
vh: number;
|
|
77
|
+
/** Device pixels per CSS pixel — the display scale. */
|
|
78
|
+
scale: number;
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
const LENGTH_RE =
|
|
@@ -89,31 +102,34 @@ export function parseLength(
|
|
|
89
102
|
if (!m) return null;
|
|
90
103
|
const n = Number(m[1]);
|
|
91
104
|
const unit = m[2];
|
|
92
|
-
if (!unit) return bareIsPx ? n : null;
|
|
105
|
+
if (!unit) return bareIsPx ? n * ctx.scale : null;
|
|
93
106
|
if (unit === '%') return { pct: n };
|
|
94
107
|
return n * unitScale(unit, ctx);
|
|
95
108
|
}
|
|
96
109
|
|
|
110
|
+
/** Device pixels per one of `unit`. The absolute units are CSS pixels and
|
|
111
|
+
* multiples of them, so they carry the display scale; the relative ones
|
|
112
|
+
* resolve against values that are device already. */
|
|
97
113
|
function unitScale(unit: string, ctx: UnitContext): number {
|
|
98
114
|
switch (unit) {
|
|
99
115
|
case 'px':
|
|
100
|
-
return
|
|
116
|
+
return ctx.scale;
|
|
101
117
|
case 'em':
|
|
102
118
|
return ctx.em;
|
|
103
119
|
case 'rem':
|
|
104
120
|
return ctx.rem;
|
|
105
121
|
case 'pt':
|
|
106
|
-
return 96 / 72;
|
|
122
|
+
return (96 / 72) * ctx.scale;
|
|
107
123
|
case 'pc':
|
|
108
|
-
return 16;
|
|
124
|
+
return 16 * ctx.scale;
|
|
109
125
|
case 'in':
|
|
110
|
-
return 96;
|
|
126
|
+
return 96 * ctx.scale;
|
|
111
127
|
case 'cm':
|
|
112
|
-
return 96 / 2.54;
|
|
128
|
+
return (96 / 2.54) * ctx.scale;
|
|
113
129
|
case 'mm':
|
|
114
|
-
return 96 / 25.4;
|
|
130
|
+
return (96 / 25.4) * ctx.scale;
|
|
115
131
|
case 'q':
|
|
116
|
-
return 96 / 101.6;
|
|
132
|
+
return (96 / 101.6) * ctx.scale;
|
|
117
133
|
// Approximations rather than font queries: both are within a few percent
|
|
118
134
|
// for every text face, and asking the font manager here would make the
|
|
119
135
|
// cascade depend on font loading.
|
package/src/html/index.ts
CHANGED
|
@@ -181,6 +181,7 @@ function deriveLook(
|
|
|
181
181
|
borderColor: String(theme.border ?? '#b2bec3'),
|
|
182
182
|
mutedColor: String(theme.textMuted ?? '#7f8c8d'),
|
|
183
183
|
background: String(theme.background ?? 'white'),
|
|
184
|
+
colorScheme: theme.scheme === 'dark' ? 'dark' : 'light',
|
|
184
185
|
surface: String(theme.surface ?? theme.background ?? 'white'),
|
|
185
186
|
controlPadY: Number(theme.paddingY ?? 6),
|
|
186
187
|
controlBorder: Number(theme.borderWidth ?? 1),
|
package/src/html/layout/boxes.ts
CHANGED
|
@@ -124,9 +124,11 @@ export interface TextLayoutLike {
|
|
|
124
124
|
end: number;
|
|
125
125
|
/** The span as handed in — a richtext `TextRun`, which is how the
|
|
126
126
|
* decoration a `<span>` carried reaches the paint pass on the same
|
|
127
|
-
* object the glyphs did.
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
* object the glyphs did. Optional, with `run`, for the reason
|
|
128
|
+
* `src/richtext/runs.ts` gives: react-x11's Cocoa engine hands back
|
|
129
|
+
* a run's geometry and nothing else. */
|
|
130
|
+
span?: TextRun;
|
|
131
|
+
run?: {
|
|
130
132
|
font: { metrics(size: number): { ascent: number; descent: number } };
|
|
131
133
|
size: number;
|
|
132
134
|
direction?: 'ltr' | 'rtl';
|
|
@@ -339,8 +341,13 @@ export interface BoxTree {
|
|
|
339
341
|
|
|
340
342
|
export interface BuildOptions {
|
|
341
343
|
cascade: Cascade;
|
|
342
|
-
/**
|
|
343
|
-
*
|
|
344
|
+
/** Device pixels per CSS pixel. An image's pixels and a `width="600"`
|
|
345
|
+
* attribute are CSS pixels; every box is device, so both are multiplied
|
|
346
|
+
* on the way in. Default 1. */
|
|
347
|
+
scale?: number;
|
|
348
|
+
/** Intrinsic size for an image the host has already loaded, in the
|
|
349
|
+
* image's own pixels. `null` when it has not: the box takes the attribute
|
|
350
|
+
* size, or a placeholder. */
|
|
344
351
|
imageSize(el: Element): { width: number; height: number } | null;
|
|
345
352
|
/** The size a real widget wants, so the box in the flow is the size the
|
|
346
353
|
* control will be drawn at. */
|
|
@@ -495,17 +502,20 @@ class Builder {
|
|
|
495
502
|
else if (style.float !== 'none') box.isFloat = true;
|
|
496
503
|
|
|
497
504
|
if (replaced === 'image') {
|
|
505
|
+
// Both sources are CSS pixels — an image pixel is one, and so is an
|
|
506
|
+
// attribute — and the box is device.
|
|
507
|
+
const scale = this._options.scale ?? 1;
|
|
498
508
|
const loaded = this._options.imageSize(el);
|
|
499
509
|
if (loaded) {
|
|
500
|
-
box.intrinsicWidth = loaded.width;
|
|
501
|
-
box.intrinsicHeight = loaded.height;
|
|
510
|
+
box.intrinsicWidth = loaded.width * scale;
|
|
511
|
+
box.intrinsicHeight = loaded.height * scale;
|
|
502
512
|
} else {
|
|
503
513
|
// An image that has not arrived still needs a box, or the document
|
|
504
514
|
// reflows under the reader when it does. The attributes are the
|
|
505
515
|
// author telling us the size in advance; without them the box is a
|
|
506
516
|
// small placeholder rather than nothing.
|
|
507
|
-
box.intrinsicWidth = numberAttr(el, 'width') ?? 0;
|
|
508
|
-
box.intrinsicHeight = numberAttr(el, 'height') ?? 0;
|
|
517
|
+
box.intrinsicWidth = (numberAttr(el, 'width') ?? 0) * scale;
|
|
518
|
+
box.intrinsicHeight = (numberAttr(el, 'height') ?? 0) * scale;
|
|
509
519
|
}
|
|
510
520
|
// The alt text joins the document text, so a document read with the
|
|
511
521
|
// images blocked still copies as prose.
|
|
@@ -317,7 +317,14 @@ export function layoutInline(block: Box, options: InlineOptions): InlineResult {
|
|
|
317
317
|
open.x += first.width;
|
|
318
318
|
open.left = band.left;
|
|
319
319
|
|
|
320
|
-
|
|
320
|
+
// Did the segment wrap? ntk answers with `truncated`. A layout that does
|
|
321
|
+
// not carry the flag — react-x11's Cocoa engine reports none — is asked
|
|
322
|
+
// the same of its line ends instead: it wrapped if anything but
|
|
323
|
+
// whitespace follows the first line. Read as "fitted", a cut fragment
|
|
324
|
+
// advanced past the whole segment, and a paragraph beside a float lost
|
|
325
|
+
// every line after its first.
|
|
326
|
+
const wrapped = fragment.truncated ?? inkBeyond(segment.runs, first.end);
|
|
327
|
+
if (!wrapped) {
|
|
321
328
|
// It fitted: the cursor stays on this line for whatever comes next.
|
|
322
329
|
//
|
|
323
330
|
// With one correction first. ntk strips a line's trailing whitespace —
|
|
@@ -805,6 +812,21 @@ function spaceAdvance(fonts: FontsLike, run: TextRun): number {
|
|
|
805
812
|
return advance;
|
|
806
813
|
}
|
|
807
814
|
|
|
815
|
+
/** Whether anything but whitespace lies past a code-unit offset into the
|
|
816
|
+
* runs' joined text — "did `maxLines` drop content", for a layout that
|
|
817
|
+
* does not say. */
|
|
818
|
+
function inkBeyond(runs: TextRun[], offset: number): boolean {
|
|
819
|
+
let at = 0;
|
|
820
|
+
for (const run of runs) {
|
|
821
|
+
const next = at + run.text.length;
|
|
822
|
+
if (next > offset && /\S/.test(run.text.slice(Math.max(0, offset - at)))) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
at = next;
|
|
826
|
+
}
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
|
|
808
830
|
// --- style questions --------------------------------------------------------
|
|
809
831
|
|
|
810
832
|
function wraps(style: ComputedStyle): boolean {
|
package/src/html/node.ts
CHANGED
|
@@ -101,7 +101,8 @@ export interface HtmlViewProps {
|
|
|
101
101
|
request: ResourceRequest,
|
|
102
102
|
) => Promise<ResourceResult | null> | ResourceResult | null;
|
|
103
103
|
onScript?: (script: ScriptRequest) => void;
|
|
104
|
-
/** Where the real widgets go, in the element's own coordinates
|
|
104
|
+
/** Where the real widgets go, in the element's own coordinates and in
|
|
105
|
+
* logical pixels — the unit the style that mounts each one is in. */
|
|
105
106
|
onControls?: (rects: ControlRect[]) => void;
|
|
106
107
|
/** The parsed document, once per parse — the DOM handle. */
|
|
107
108
|
onDocument?: (document: Document) => void;
|
|
@@ -168,6 +169,70 @@ export class HtmlViewNode extends Node {
|
|
|
168
169
|
return this.props as unknown as HtmlViewProps;
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
// --- units ----------------------------------------------------------------
|
|
173
|
+
//
|
|
174
|
+
// The whole pipeline — the cascade's lengths, the box tree, the paint, the
|
|
175
|
+
// walks the accessors run — works in **device pixels**, the unit `abs`,
|
|
176
|
+
// `contentBox()`, the paint context and `paintDamage()` arrive in (core's
|
|
177
|
+
// `docs/scale.md`). Three things cross into that unit and each converts
|
|
178
|
+
// once, here:
|
|
179
|
+
//
|
|
180
|
+
// - a CSS pixel: every `px` the author wrote, the UA sheet's, an image's
|
|
181
|
+
// own pixels, a `width="600"` attribute, the theme's font size and
|
|
182
|
+
// control chrome in `look`. `UnitContext.scale` and `_deviceLook()`
|
|
183
|
+
// multiply them on the way in;
|
|
184
|
+
// - a synthetic event's `x`/`y`, and so the public point queries
|
|
185
|
+
// (`elementAtPoint`, `hrefAtPoint`, `setHover`): logical, multiplied in
|
|
186
|
+
// `_toDocument`. The selection seam (`textIndexAt` and the two rect
|
|
187
|
+
// accessors) is core's device-pixel contract and stays device;
|
|
188
|
+
// - a control rect on its way out to `onControls`: divided, because it
|
|
189
|
+
// becomes a style.
|
|
190
|
+
//
|
|
191
|
+
// At 1x every conversion is the identity, which is how a view that
|
|
192
|
+
// compared `ev.x` with `abs` and laid `16px` out as sixteen device pixels
|
|
193
|
+
// passed every test and then hovered the wrong element and drew the
|
|
194
|
+
// document at half size on a retina panel.
|
|
195
|
+
|
|
196
|
+
/** Device pixels per logical pixel — the display scale this element's
|
|
197
|
+
* window resolved to, constant for the node's life. */
|
|
198
|
+
private get _scale(): number {
|
|
199
|
+
return this.scale > 0 ? this.scale : 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private _deviceLookFor: RootLook | null = null;
|
|
203
|
+
private _deviceLookAt = 0;
|
|
204
|
+
private _deviceLookValue: RootLook | null = null;
|
|
205
|
+
|
|
206
|
+
/** The look with its lengths on the device grid: the theme's font size and
|
|
207
|
+
* the control chrome are logical pixels, and the cascade's initial style
|
|
208
|
+
* and `measureControl` want device ones. Memoized on the look's identity
|
|
209
|
+
* so the cascade and the control measurer see one object. */
|
|
210
|
+
private _deviceLook(): RootLook {
|
|
211
|
+
const look = this._props().look;
|
|
212
|
+
const s = this._scale;
|
|
213
|
+
if (
|
|
214
|
+
this._deviceLookValue &&
|
|
215
|
+
this._deviceLookFor === look &&
|
|
216
|
+
this._deviceLookAt === s
|
|
217
|
+
) {
|
|
218
|
+
return this._deviceLookValue;
|
|
219
|
+
}
|
|
220
|
+
const scaled =
|
|
221
|
+
s === 1
|
|
222
|
+
? look
|
|
223
|
+
: {
|
|
224
|
+
...look,
|
|
225
|
+
fontSize: look.fontSize * s,
|
|
226
|
+
controlPadY: look.controlPadY * s,
|
|
227
|
+
controlBorder: look.controlBorder * s,
|
|
228
|
+
controlRadius: look.controlRadius * s,
|
|
229
|
+
};
|
|
230
|
+
this._deviceLookFor = look;
|
|
231
|
+
this._deviceLookAt = s;
|
|
232
|
+
this._deviceLookValue = scaled;
|
|
233
|
+
return scaled;
|
|
234
|
+
}
|
|
235
|
+
|
|
171
236
|
// --- the pipeline ---------------------------------------------------------
|
|
172
237
|
|
|
173
238
|
private _invalidate(stale: Stale): void {
|
|
@@ -232,7 +297,8 @@ export class HtmlViewNode extends Node {
|
|
|
232
297
|
/** Rebuild the cascade — the document's sheets plus the host's. */
|
|
233
298
|
private _restyle(width: number): void {
|
|
234
299
|
const props = this._props();
|
|
235
|
-
const
|
|
300
|
+
const look = this._deviceLook();
|
|
301
|
+
const sheets: Stylesheet[] = [uaStylesheet(look)];
|
|
236
302
|
let order = 0;
|
|
237
303
|
for (const ref of this._source.facts().sheets) {
|
|
238
304
|
const text =
|
|
@@ -267,9 +333,10 @@ export class HtmlViewNode extends Node {
|
|
|
267
333
|
}
|
|
268
334
|
this._cascade = new Cascade(
|
|
269
335
|
sheets,
|
|
270
|
-
|
|
336
|
+
look,
|
|
271
337
|
width,
|
|
272
338
|
this._viewportHeight(),
|
|
339
|
+
this._scale,
|
|
273
340
|
);
|
|
274
341
|
this._cascade.setPointer({
|
|
275
342
|
hovered: new Set(this._hovered),
|
|
@@ -321,12 +388,13 @@ export class HtmlViewNode extends Node {
|
|
|
321
388
|
cascade.viewportHeight = this._viewportHeight();
|
|
322
389
|
|
|
323
390
|
if (this._stale >= Stale.Boxes || !this._tree) {
|
|
324
|
-
const
|
|
391
|
+
const look = this._deviceLook();
|
|
325
392
|
this._tree = buildBoxes(this._source.document, {
|
|
326
393
|
cascade,
|
|
394
|
+
scale: this._scale,
|
|
327
395
|
imageSize: (el) => this._resources.imageSize(attr(el, 'src') ?? ''),
|
|
328
396
|
controlSize: (el, kind, style) =>
|
|
329
|
-
measureControl(el, kind, style, this._fonts(),
|
|
397
|
+
measureControl(el, kind, style, this._fonts(), look),
|
|
330
398
|
});
|
|
331
399
|
this._textPoints = null;
|
|
332
400
|
this._pointsAreUnits = null;
|
|
@@ -355,7 +423,20 @@ export class HtmlViewNode extends Node {
|
|
|
355
423
|
const tree = this._tree;
|
|
356
424
|
const report = this._props().onControls;
|
|
357
425
|
if (!tree || !report) return;
|
|
358
|
-
|
|
426
|
+
// The boxes are device pixels; each rect becomes the style of a widget
|
|
427
|
+
// mounted beside this element, and a style is logical.
|
|
428
|
+
const s = this._scale;
|
|
429
|
+
const rects = controlRectsOf(tree).map((r) =>
|
|
430
|
+
s === 1
|
|
431
|
+
? r
|
|
432
|
+
: {
|
|
433
|
+
...r,
|
|
434
|
+
x: r.x / s,
|
|
435
|
+
y: r.y / s,
|
|
436
|
+
width: r.width / s,
|
|
437
|
+
height: r.height / s,
|
|
438
|
+
},
|
|
439
|
+
);
|
|
359
440
|
if (sameRects(rects, this._controls)) return;
|
|
360
441
|
this._controls = rects;
|
|
361
442
|
report(rects);
|
|
@@ -463,7 +544,9 @@ export class HtmlViewNode extends Node {
|
|
|
463
544
|
override textIndexAt(x: number, y: number): number {
|
|
464
545
|
const tree = this._tree;
|
|
465
546
|
if (!tree) return 0;
|
|
466
|
-
|
|
547
|
+
// Core's contract: a device-pixel point, the same space the two rect
|
|
548
|
+
// accessors below answer in.
|
|
549
|
+
const local = { x: x - this.abs.x, y: y - this.abs.y };
|
|
467
550
|
const hit = nearestText(tree.root, local.x, local.y);
|
|
468
551
|
if (!hit) return 0;
|
|
469
552
|
return this._toPoints(hit);
|
|
@@ -495,10 +578,15 @@ export class HtmlViewNode extends Node {
|
|
|
495
578
|
}
|
|
496
579
|
|
|
497
580
|
// --- pointer --------------------------------------------------------------
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
581
|
+
//
|
|
582
|
+
// The three point queries take **logical** window pixels — what a synthetic
|
|
583
|
+
// event's `x`/`y` carry and what `useLinkClicks` and an application's
|
|
584
|
+
// handler hand over — and are the one place the two units meet on the way
|
|
585
|
+
// in. `_toDocument` multiplies.
|
|
586
|
+
|
|
587
|
+
/** The link under a logical window point, if any. Not part of the
|
|
588
|
+
* selection seam: core deliberately left hover and `cursorAt` out of
|
|
589
|
+
* #291, so following a link stays this package's. */
|
|
502
590
|
hrefAtPoint(x: number, y: number): string | null {
|
|
503
591
|
const el = this.elementAtPoint(x, y);
|
|
504
592
|
let node: Element | null = el;
|
|
@@ -510,7 +598,7 @@ export class HtmlViewNode extends Node {
|
|
|
510
598
|
return null;
|
|
511
599
|
}
|
|
512
600
|
|
|
513
|
-
/** The deepest element whose box contains a window
|
|
601
|
+
/** The deepest element whose box contains a logical window point. */
|
|
514
602
|
elementAtPoint(x: number, y: number): Element | null {
|
|
515
603
|
const tree = this._tree;
|
|
516
604
|
if (!tree) return null;
|
|
@@ -519,9 +607,10 @@ export class HtmlViewNode extends Node {
|
|
|
519
607
|
}
|
|
520
608
|
|
|
521
609
|
/**
|
|
522
|
-
* The pointer moved. Returns true when the
|
|
523
|
-
* changed, so the caller knows whether to
|
|
524
|
-
* does for a document that actually
|
|
610
|
+
* The pointer moved, to a logical window point. Returns true when the
|
|
611
|
+
* cascade's answer could have changed, so the caller knows whether to
|
|
612
|
+
* invalidate — which it only ever does for a document that actually
|
|
613
|
+
* contains a `:hover` rule.
|
|
525
614
|
*/
|
|
526
615
|
setHover(x: number, y: number): boolean {
|
|
527
616
|
const cascade = this._cascade;
|
|
@@ -570,8 +659,10 @@ export class HtmlViewNode extends Node {
|
|
|
570
659
|
return this._source.facts().title;
|
|
571
660
|
}
|
|
572
661
|
|
|
662
|
+
/** A logical window point in document (device) coordinates. */
|
|
573
663
|
private _toDocument(x: number, y: number): { x: number; y: number } {
|
|
574
|
-
|
|
664
|
+
const s = this._scale;
|
|
665
|
+
return { x: x * s - this.abs.x, y: y * s - this.abs.y };
|
|
575
666
|
}
|
|
576
667
|
|
|
577
668
|
// --- pointer defaults -----------------------------------------------------
|
|
@@ -618,6 +709,7 @@ export class HtmlViewNode extends Node {
|
|
|
618
709
|
paintDocument(ctx as PaintContext, tree, {
|
|
619
710
|
originX: this.abs.x,
|
|
620
711
|
originY: this.abs.y,
|
|
712
|
+
scale: this._scale,
|
|
621
713
|
damage,
|
|
622
714
|
selection: range
|
|
623
715
|
? { start: this._toUnits(range.start), end: this._toUnits(range.end) }
|
|
@@ -951,7 +1043,8 @@ function deepestAt(box: Box, x: number, y: number): Element | null {
|
|
|
951
1043
|
for (const run of natural.runs) {
|
|
952
1044
|
const left = text.drawX + natural.x + run.x;
|
|
953
1045
|
if (x >= left && x < left + run.width) {
|
|
954
|
-
const owner = (run.span as { element?: Element })
|
|
1046
|
+
const owner = (run.span as { element?: Element } | undefined)
|
|
1047
|
+
?.element;
|
|
955
1048
|
if (owner) found = owner;
|
|
956
1049
|
}
|
|
957
1050
|
}
|