react-x11 2.41.0 → 2.41.1
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/package.json +3 -3
- package/src/cocoa/panehost.js +48 -7
- package/src/offscreen.js +8 -7
- package/src/win32/fonts.js +116 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.41.
|
|
3
|
+
"version": "2.41.1",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -104,14 +104,14 @@
|
|
|
104
104
|
"ical.js": "^2.2.1",
|
|
105
105
|
"image-in-browser": "^3.6.0",
|
|
106
106
|
"linebreak": "^1.1.0",
|
|
107
|
-
"ntk": "^8.
|
|
107
|
+
"ntk": "^8.23.0",
|
|
108
108
|
"react-reconciler": "^0.34.0",
|
|
109
109
|
"yoga-layout": "^3.2.1"
|
|
110
110
|
},
|
|
111
111
|
"optionalDependencies": {
|
|
112
112
|
"@windowkit/appkit": "^0.25.0",
|
|
113
113
|
"@windowkit/wayland": "^3.1.1",
|
|
114
|
-
"@windowkit/win32": "^0.0.
|
|
114
|
+
"@windowkit/win32": "^0.0.9",
|
|
115
115
|
"dbus-native": "^0.16.0",
|
|
116
116
|
"x11-dri": "^0.9.0"
|
|
117
117
|
},
|
package/src/cocoa/panehost.js
CHANGED
|
@@ -31,6 +31,12 @@ const ASKED_KEPT = 16;
|
|
|
31
31
|
* drag itself. */
|
|
32
32
|
const GESTURE_GAP_MS = 500;
|
|
33
33
|
|
|
34
|
+
/** How far inside the budget a frame of a pane that fell behind has to
|
|
35
|
+
* come for its frames to be anchored again (`_answered`): a pane whose
|
|
36
|
+
* frames come near the budget keeps one look rather than taking the
|
|
37
|
+
* other at every frame. */
|
|
38
|
+
const KEPT_UP = 0.75;
|
|
39
|
+
|
|
34
40
|
/** One shared surface on glass: a sublayer of a window's root layer, its
|
|
35
41
|
* contents the IOSurface the pane last presented into it. */
|
|
36
42
|
class PaneLayer {
|
|
@@ -54,6 +60,10 @@ class PaneLayer {
|
|
|
54
60
|
this._asked = [];
|
|
55
61
|
this._askedAt = null;
|
|
56
62
|
this._behind = false;
|
|
63
|
+
// when a frame last answered a size, and how many frames in a row came
|
|
64
|
+
// later than the budget (`_answered`)
|
|
65
|
+
this._answeredAt = -Infinity;
|
|
66
|
+
this._late = 0;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/**
|
|
@@ -73,13 +83,28 @@ class PaneLayer {
|
|
|
73
83
|
* size may take before the old one is shown in its place: 0 stretches
|
|
74
84
|
* always, as before. A bridge that predates `contentsGravity` ignores it,
|
|
75
85
|
* and stretches.
|
|
86
|
+
*
|
|
87
|
+
* Behind is a pane whose frames come later than the budget, not one with
|
|
88
|
+
* a size waiting that long. `<Frame>` sends a pane the newest size once
|
|
89
|
+
* it has painted the last, so the sizes a drag passes through while a
|
|
90
|
+
* frame is being painted are never sent, and never answered: a size
|
|
91
|
+
* waits until a frame of a later one lands, two pane frames at most. A
|
|
92
|
+
* page that painted in 27 ms answered every size it was sent in 31, and
|
|
93
|
+
* still had one 52 ms old at every step of the drag — so its layer was
|
|
94
|
+
* stretched at every step and anchored again at every frame, the page
|
|
95
|
+
* scaling and coming back thirty times a second. Here a size waiting
|
|
96
|
+
* longer than the budget counts only when no frame has answered any
|
|
97
|
+
* size in that time, which is a pane that stopped, and a frame that
|
|
98
|
+
* lands decides the rest (`_answered`).
|
|
76
99
|
*/
|
|
77
100
|
_gravityNow() {
|
|
78
101
|
const budget = this.app._resizeWait ?? RESIZE_WAIT_MS;
|
|
79
102
|
if (!(budget > 0)) return 'resize';
|
|
80
|
-
// a size asked longer ago than the budget, and no frame of
|
|
103
|
+
// a size asked longer ago than the budget, and no frame of any since
|
|
81
104
|
const oldest = this._asked[0];
|
|
82
|
-
if (oldest && now() - oldest.at > budget)
|
|
105
|
+
if (oldest && now() - Math.max(oldest.at, this._answeredAt) > budget) {
|
|
106
|
+
this._behind = true;
|
|
107
|
+
}
|
|
83
108
|
return this._behind ? 'resize' : 'topLeft';
|
|
84
109
|
}
|
|
85
110
|
|
|
@@ -106,20 +131,35 @@ class PaneLayer {
|
|
|
106
131
|
return { contentsScale: scale };
|
|
107
132
|
}
|
|
108
133
|
|
|
109
|
-
/**
|
|
110
|
-
*
|
|
134
|
+
/**
|
|
135
|
+
* A frame of `width` by `height` device px landed: what it answers of
|
|
136
|
+
* the sizes asked, and how long it took. Two frames in a row later than
|
|
137
|
+
* the budget put the pane behind, and one well inside it — `KEPT_UP` of
|
|
138
|
+
* it — brings it back: a frame the collector held up stretches nothing,
|
|
139
|
+
* and a pane whose frames come near the budget is not stretched and
|
|
140
|
+
* anchored by turns.
|
|
141
|
+
*/
|
|
111
142
|
_answered(width, height) {
|
|
112
143
|
const asked = this._asked;
|
|
113
144
|
// the pane rounds the logical size it was sent back to device pixels
|
|
114
145
|
const slack = Math.max(2, this.wnd.scale ?? 1);
|
|
115
|
-
|
|
146
|
+
// the last time it was asked: a drag back and forth asks a size twice
|
|
147
|
+
const at = asked.findLastIndex(
|
|
116
148
|
(a) =>
|
|
117
149
|
Math.abs(a.width - width) <= slack &&
|
|
118
150
|
Math.abs(a.height - height) <= slack,
|
|
119
151
|
);
|
|
120
152
|
if (at < 0) return;
|
|
121
|
-
this.
|
|
122
|
-
|
|
153
|
+
const budget = this.app._resizeWait ?? RESIZE_WAIT_MS;
|
|
154
|
+
const took = now() - asked[at].at;
|
|
155
|
+
this._answeredAt = now();
|
|
156
|
+
if (took > budget) {
|
|
157
|
+
this._late += 1;
|
|
158
|
+
if (this._late >= 2) this._behind = true;
|
|
159
|
+
} else {
|
|
160
|
+
this._late = 0;
|
|
161
|
+
if (took <= budget * KEPT_UP) this._behind = false;
|
|
162
|
+
}
|
|
123
163
|
asked.splice(0, at + 1);
|
|
124
164
|
}
|
|
125
165
|
|
|
@@ -144,6 +184,7 @@ class PaneLayer {
|
|
|
144
184
|
if (at - (this._askedAt ?? -Infinity) >= GESTURE_GAP_MS) {
|
|
145
185
|
this._asked = [];
|
|
146
186
|
this._behind = false;
|
|
187
|
+
this._late = 0;
|
|
147
188
|
}
|
|
148
189
|
this._askedAt = at;
|
|
149
190
|
if (this._asked.length === ASKED_KEPT) this._asked.shift();
|
package/src/offscreen.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
// An app that makes its own surfaces answers `createSurface(options)` — the
|
|
6
6
|
// Cocoa app over a CG bitmap (src/cocoa/surface.js), the Windows app over a
|
|
7
7
|
// Direct2D bitmap, the Wayland app over a GL render target
|
|
8
|
-
// (src/wayland/surface.js)
|
|
9
|
-
// ntk
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// `
|
|
14
|
-
//
|
|
8
|
+
// (src/wayland/surface.js), and ntk's own app over an X connection with its
|
|
9
|
+
// pixmap (ntk >= 8.21). An app without the method — an older ntk deduped
|
|
10
|
+
// into the tree — gets that pixmap from ntk's root, which only an X
|
|
11
|
+
// connection loads (src/ntkroot.js). The result is whichever implementation
|
|
12
|
+
// answered, not an instance of this class: the contract is the shape —
|
|
13
|
+
// `width`/`height`, `getContext('2d')`, `render`, `clear`, `copyWithin`,
|
|
14
|
+
// `destroy`, and `ctx.drawImage(surface, …)` — (docs/extending.md "Scrolling
|
|
15
|
+
// the pixels, not just the offset"), and nothing needs `instanceof`.
|
|
15
16
|
//
|
|
16
17
|
// Part of that shape is that a context may be **held**: ntk tells a caller
|
|
17
18
|
// doing many draws to take `getContext('2d')` once rather than one per frame,
|
package/src/win32/fonts.js
CHANGED
|
@@ -407,20 +407,68 @@ class Win32TextLayout {
|
|
|
407
407
|
* where CoreText cannot and the Cocoa engine breaks at `linebreak`'s
|
|
408
408
|
* opportunities itself. */
|
|
409
409
|
get minWidth() {
|
|
410
|
+
// asked of the bridge the first time, where it left it out of the
|
|
411
|
+
// metrics: the line breaker over the whole text again, which a layout
|
|
412
|
+
// made to be drawn is never asked for
|
|
413
|
+
if (this._metrics.minWidth === undefined) {
|
|
414
|
+
this._metrics.minWidth =
|
|
415
|
+
this._handle != null && this._native.layoutMinWidth
|
|
416
|
+
? (this._native.layoutMinWidth(this._handle) ?? 0)
|
|
417
|
+
: 0;
|
|
418
|
+
}
|
|
410
419
|
return this._metrics.minWidth;
|
|
411
420
|
}
|
|
412
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Keep the first `cap` lines of what was laid out: they are all this
|
|
424
|
+
* layout reports, answers for and draws (`_keptEnd`, `_keptHeight`). A
|
|
425
|
+
* capped paragraph is laid out past its cap to find where the cap falls,
|
|
426
|
+
* and keeping that layout's first lines is the same picture as laying the
|
|
427
|
+
* kept text out again, for one native layout where it took two.
|
|
428
|
+
*/
|
|
429
|
+
_keep(cap) {
|
|
430
|
+
const lines = this._metrics.lines.slice(0, cap);
|
|
431
|
+
const last = lines[lines.length - 1];
|
|
432
|
+
if (!last) return;
|
|
433
|
+
let width = 0;
|
|
434
|
+
for (const line of lines)
|
|
435
|
+
width = Math.max(width, Math.ceil(line.width - 1e-3));
|
|
436
|
+
this._metrics = {
|
|
437
|
+
...this._metrics,
|
|
438
|
+
lines,
|
|
439
|
+
width,
|
|
440
|
+
height: Math.ceil(last.y + last.height),
|
|
441
|
+
minWidth: undefined,
|
|
442
|
+
};
|
|
443
|
+
this._keptEnd = last.end;
|
|
444
|
+
this._keptHeight = last.y + last.height;
|
|
445
|
+
}
|
|
446
|
+
|
|
413
447
|
draw(ctx, x, y) {
|
|
448
|
+
if (this._keptHeight === undefined) {
|
|
449
|
+
ctx._drawLayout(this, x, y);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// the lines past the cap are in the layout, and stay out of the picture
|
|
453
|
+
ctx.save();
|
|
454
|
+
ctx.beginPath();
|
|
455
|
+
ctx.rect(x - 1e4, y, 2e4 + this._metrics.width, this._keptHeight);
|
|
456
|
+
ctx.clip();
|
|
414
457
|
ctx._drawLayout(this, x, y);
|
|
458
|
+
ctx.restore();
|
|
415
459
|
}
|
|
416
460
|
|
|
417
461
|
indexAt(x, y) {
|
|
418
462
|
const unit = this._native.layoutIndexAt(this._handle, x, y);
|
|
419
|
-
return this._cpOf(unit);
|
|
463
|
+
return this._cpOf(Math.min(unit, this._keptEnd ?? unit));
|
|
420
464
|
}
|
|
421
465
|
|
|
422
466
|
caretPosition(cp) {
|
|
423
|
-
|
|
467
|
+
const unit = this._cuOf(cp);
|
|
468
|
+
return this._native.layoutCaret(
|
|
469
|
+
this._handle,
|
|
470
|
+
Math.min(unit, this._keptEnd ?? unit),
|
|
471
|
+
);
|
|
424
472
|
}
|
|
425
473
|
|
|
426
474
|
/**
|
|
@@ -567,6 +615,20 @@ export class Win32FontManager {
|
|
|
567
615
|
// family|weight|italic|size -> the bridge's handle, or null when there is
|
|
568
616
|
// no such face
|
|
569
617
|
this._handles = new Map();
|
|
618
|
+
// what this bridge's layouts take, asked once (`textFeatures`, from
|
|
619
|
+
// @windowkit/win32 0.0.9): an older one answers nothing and takes neither
|
|
620
|
+
this._features = native.textFeatures?.() ?? {};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Which of ntk's `justify` modes `layout` takes. DirectWrite justifies
|
|
625
|
+
* every line but a paragraph's last, which is `justify: true`, and has no
|
|
626
|
+
* way to justify a last line: `'rest'` says so, and a caller justifies the
|
|
627
|
+
* other two itself. <Html>, which spaced every justified line through
|
|
628
|
+
* letter spacing on this engine, lays a justified paragraph out once.
|
|
629
|
+
*/
|
|
630
|
+
get justifies() {
|
|
631
|
+
return this._features.justify === true ? 'rest' : false;
|
|
570
632
|
}
|
|
571
633
|
|
|
572
634
|
/**
|
|
@@ -759,7 +821,9 @@ export class Win32FontManager {
|
|
|
759
821
|
return this._laidOut(sources, base, { ...options, maxWidth: least });
|
|
760
822
|
}
|
|
761
823
|
let laid = sources;
|
|
762
|
-
let layout =
|
|
824
|
+
let layout =
|
|
825
|
+
this._prefixLaidOut(sources, base, options, cap) ??
|
|
826
|
+
this._laidOut(laid, base, options);
|
|
763
827
|
// With no width the bridge lays a paragraph out in a box a million
|
|
764
828
|
// pixels wide, and aligns it there: a centred line's `x` was half a
|
|
765
829
|
// million, an RTL one's a million, and <Html> measured a centred
|
|
@@ -776,7 +840,11 @@ export class Win32FontManager {
|
|
|
776
840
|
layout = this._laidOut(laid, base, { ...options, maxWidth: widest });
|
|
777
841
|
}
|
|
778
842
|
layout.truncated = layout.lines.length > cap;
|
|
779
|
-
if (layout.truncated) {
|
|
843
|
+
if (layout.truncated && !elides) {
|
|
844
|
+
// what was laid out, a prefix or the whole, keeps its first lines
|
|
845
|
+
laid = spansTo(sources, layout._text.length);
|
|
846
|
+
layout._keep(cap);
|
|
847
|
+
} else if (layout.truncated) {
|
|
780
848
|
const last = layout.lines[cap - 1];
|
|
781
849
|
const text = layout._text;
|
|
782
850
|
layout.destroy();
|
|
@@ -800,6 +868,43 @@ export class Win32FontManager {
|
|
|
800
868
|
return layout;
|
|
801
869
|
}
|
|
802
870
|
|
|
871
|
+
/**
|
|
872
|
+
* A capped paragraph laid out only as far as the cap needs: a prefix of
|
|
873
|
+
* its text that breaks into more lines than `cap`, or null where the
|
|
874
|
+
* whole text is no longer than a prefix would be. The first lines of a
|
|
875
|
+
* prefix are the whole text's first lines wherever it reaches past them —
|
|
876
|
+
* a line's break waits only on the word after it, which then starts a
|
|
877
|
+
* line of the prefix — and the prefix ends at a break, its last word
|
|
878
|
+
* whole. <Html> lays a paragraph beside a float out a line at a time,
|
|
879
|
+
* each `maxLines: 1` over the rest of the paragraph; laid out whole, each
|
|
880
|
+
* line cost the whole rest of it, twice, and a paragraph cost the square
|
|
881
|
+
* of its length.
|
|
882
|
+
*/
|
|
883
|
+
_prefixLaidOut(sources, base, options, cap) {
|
|
884
|
+
const { maxWidth } = options;
|
|
885
|
+
if (!(cap < Infinity) || !(maxWidth > 0) || !Number.isFinite(maxWidth)) {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
const text = sources.map((span) => String(span.text)).join('');
|
|
889
|
+
let size = Infinity;
|
|
890
|
+
for (const span of sources)
|
|
891
|
+
size = Math.min(size, sizeOf(span.size ?? base.size));
|
|
892
|
+
// a third of an em a character is narrower than nearly any text is set
|
|
893
|
+
// in, so a prefix that long nearly always reaches past the lines wanted;
|
|
894
|
+
// where it does not, one twice as long is tried
|
|
895
|
+
let take = Math.ceil(maxWidth / (Math.max(1, size) * 0.3)) * cap + 16;
|
|
896
|
+
while (take < text.length) {
|
|
897
|
+
let end = take;
|
|
898
|
+
while (end < text.length && !BREAKING_SPACE.test(text[end])) end++;
|
|
899
|
+
if (end >= text.length) return null;
|
|
900
|
+
const layout = this._laidOut(spansTo(sources, end), base, options);
|
|
901
|
+
if (layout.lines.length > cap) return layout;
|
|
902
|
+
layout.destroy();
|
|
903
|
+
take *= 2;
|
|
904
|
+
}
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
|
|
803
908
|
/**
|
|
804
909
|
* Where the last line kept of an eliding paragraph ends: as far into
|
|
805
910
|
* `[start, end)` as fits beside the mark in `maxWidth`, white space before
|
|
@@ -899,11 +1004,17 @@ export class Win32FontManager {
|
|
|
899
1004
|
align: options.align,
|
|
900
1005
|
lineHeight: options.lineHeight,
|
|
901
1006
|
rtl: options.direction === 'rtl',
|
|
1007
|
+
// the one mode DirectWrite has (`justifies`); 'last' and 'all' are
|
|
1008
|
+
// the caller's
|
|
1009
|
+
justify: options.justify === true || undefined,
|
|
902
1010
|
},
|
|
903
1011
|
ranges,
|
|
904
1012
|
);
|
|
905
1013
|
|
|
906
|
-
const raw = this._native.layoutMetrics(
|
|
1014
|
+
const raw = this._native.layoutMetrics(
|
|
1015
|
+
handle,
|
|
1016
|
+
this._features.lazyMinWidth ? { minWidth: false } : undefined,
|
|
1017
|
+
);
|
|
907
1018
|
if (DEBUG) {
|
|
908
1019
|
// The family is in here because the one way this goes quietly wrong is
|
|
909
1020
|
// `resolveStack` not recognising a name and answering `sans-serif`: the
|