ntk 8.12.13 → 8.13.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/lib/app.js +61 -0
- package/lib/clipboard.js +16 -5
- package/lib/cliprect.js +117 -0
- package/lib/renderingcontext_2d.js +78 -8
- package/lib/text/layout.js +164 -15
- package/lib/window.js +87 -9
- package/package.json +1 -1
package/lib/app.js
CHANGED
|
@@ -311,6 +311,67 @@ export default class App {
|
|
|
311
311
|
for (const wnd of Window._cacheFor(this).values()) wnd._adoptDefaultFrameInterval(ms);
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Whether a compositing manager runs on this connection's screen: `true`,
|
|
316
|
+
* `false`, or `null` until the server has been asked, which the first
|
|
317
|
+
* window to present does (see docs/window.md "Under a compositor").
|
|
318
|
+
*
|
|
319
|
+
* Under one, the window's frames end on the fence and its blits are
|
|
320
|
+
* CopyArea (`Window#_preferFence`): the compositor already puts the screen
|
|
321
|
+
* together at the vertical blank, so a present synced to it buys no tearing
|
|
322
|
+
* a copy would cause, and its clock costs what issue #402 measured — up to
|
|
323
|
+
* a frame of latency on every change, and on NVIDIA's driver a completion
|
|
324
|
+
* whose phase drifts across the whole period, so half the frames a late
|
|
325
|
+
* one owes miss their vblank. Xwayland is the exception (`xwayland`): its
|
|
326
|
+
* Present clock is the Wayland compositor's frame callback, which is what
|
|
327
|
+
* stops an occluded window from rendering.
|
|
328
|
+
*
|
|
329
|
+
* Followed for the life of the connection, through the `_NET_WM_CM_S`
|
|
330
|
+
* selection the EWMH has a compositor own, so one started or stopped
|
|
331
|
+
* while the app runs moves every window to the clock that fits.
|
|
332
|
+
*/
|
|
333
|
+
get compositing() {
|
|
334
|
+
if (this._compositorProbe === undefined) this._probeCompositor();
|
|
335
|
+
return this._compositing ?? null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Whether this server is Xwayland, once `compositing` has asked. */
|
|
339
|
+
get xwayland() {
|
|
340
|
+
return this._xwayland ?? false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
_probeCompositor() {
|
|
344
|
+
this._compositorProbe = null; // in progress; only ever started once
|
|
345
|
+
const X = this.X;
|
|
346
|
+
if (connectionGone(X)) return;
|
|
347
|
+
const name = '_NET_WM_CM_S0';
|
|
348
|
+
// Each answer can arrive after the app has closed — a window that
|
|
349
|
+
// presented once and was torn down at once — and a request made from it
|
|
350
|
+
// then throws out of the reply dispatch, where no caller can catch it.
|
|
351
|
+
X.QueryExtension('XWAYLAND', (xwErr, ext) => {
|
|
352
|
+
this._xwayland = !xwErr && !!ext?.present;
|
|
353
|
+
if (connectionGone(X)) return;
|
|
354
|
+
X.InternAtom(false, name, (atomErr, atom) => {
|
|
355
|
+
if (atomErr || !atom || connectionGone(X)) return;
|
|
356
|
+
X.GetSelectionOwner(atom, (ownerErr, owner) => {
|
|
357
|
+
if (ownerErr || connectionGone(X)) return;
|
|
358
|
+
this._setCompositing(owner !== 0);
|
|
359
|
+
this.clipboard
|
|
360
|
+
.watch(name, (ev) => this._setCompositing(ev.reason === 'new-owner' && ev.owner !== 0))
|
|
361
|
+
.catch(() => {
|
|
362
|
+
// no XFixes: the state at startup is what windows go by
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_setCompositing(on) {
|
|
370
|
+
if (this._compositing === on) return;
|
|
371
|
+
this._compositing = on;
|
|
372
|
+
for (const wnd of Window._cacheFor(this).values()) wnd._compositingChanged?.();
|
|
373
|
+
}
|
|
374
|
+
|
|
314
375
|
/** the text API entry point: font matching/loading, shaping, layout */
|
|
315
376
|
get fonts() {
|
|
316
377
|
if (!this._fonts) this._fonts = new FontManager({ source: this.options.fontSource });
|
package/lib/clipboard.js
CHANGED
|
@@ -63,6 +63,7 @@ export default class Clipboard {
|
|
|
63
63
|
this._fixes = null; // XFixes extension, required on first watch()
|
|
64
64
|
this._fixesFirstEvent = -1; // its event base, to recognise its events
|
|
65
65
|
this._selectionWatchers = new Map(); // selection atom -> { name, handlers }
|
|
66
|
+
this._listening = false; // _onXEvent is on the raw event stream
|
|
66
67
|
this._onXEvent = this._onXEvent.bind(this);
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -328,11 +329,17 @@ export default class Clipboard {
|
|
|
328
329
|
// never delivers them to the Window wrapper — listen on the raw
|
|
329
330
|
// client instead. INCR chunk pacing needs PropertyNotify for the
|
|
330
331
|
// requestor's window, which is not ours either.
|
|
331
|
-
this.
|
|
332
|
+
this._listen();
|
|
332
333
|
})();
|
|
333
334
|
return this._ready;
|
|
334
335
|
}
|
|
335
336
|
|
|
337
|
+
_listen() {
|
|
338
|
+
if (this._listening) return;
|
|
339
|
+
this._listening = true;
|
|
340
|
+
this.X.on('event', this._onXEvent);
|
|
341
|
+
}
|
|
342
|
+
|
|
336
343
|
/**
|
|
337
344
|
* Call `handler` whenever a selection changes hands.
|
|
338
345
|
*
|
|
@@ -355,6 +362,9 @@ export default class Clipboard {
|
|
|
355
362
|
*
|
|
356
363
|
* Watchers share one server-side registration per selection, so watching
|
|
357
364
|
* the same selection twice costs one extra callback and no extra protocol.
|
|
365
|
+
* The registration is on the root window, as GDK's is, so watching makes
|
|
366
|
+
* no window of the clipboard's own: an app that only watches — every app
|
|
367
|
+
* does, through `App#compositing` — has none.
|
|
358
368
|
*
|
|
359
369
|
* @param {string} selection selection atom name, e.g. 'CLIPBOARD' or 'PRIMARY'
|
|
360
370
|
* @param {function} handler called with the event
|
|
@@ -364,9 +374,10 @@ export default class Clipboard {
|
|
|
364
374
|
if (typeof handler !== 'function') {
|
|
365
375
|
throw new TypeError('clipboard: watch needs a handler function');
|
|
366
376
|
}
|
|
367
|
-
await this._ensure();
|
|
368
377
|
const fixes = await this._ensureFixes();
|
|
369
378
|
const sel = await this._atom(selection);
|
|
379
|
+
this._listen();
|
|
380
|
+
const root = this.app.display.screen[0].root;
|
|
370
381
|
|
|
371
382
|
let entry = this._selectionWatchers.get(sel);
|
|
372
383
|
if (!entry) {
|
|
@@ -376,7 +387,7 @@ export default class Clipboard {
|
|
|
376
387
|
fixes.SelectionEventMask.SetSelectionOwner |
|
|
377
388
|
fixes.SelectionEventMask.SelectionWindowDestroy |
|
|
378
389
|
fixes.SelectionEventMask.SelectionClientClose;
|
|
379
|
-
safeRelease(this.X, () => fixes.SelectSelectionInput(
|
|
390
|
+
safeRelease(this.X, () => fixes.SelectSelectionInput(root, sel, mask));
|
|
380
391
|
}
|
|
381
392
|
entry.handlers.add(handler);
|
|
382
393
|
|
|
@@ -390,7 +401,7 @@ export default class Clipboard {
|
|
|
390
401
|
if (current.handlers.size) return;
|
|
391
402
|
// last watcher for this selection: drop the server-side registration
|
|
392
403
|
this._selectionWatchers.delete(sel);
|
|
393
|
-
safeRelease(this.X, () => fixes.SelectSelectionInput(
|
|
404
|
+
safeRelease(this.X, () => fixes.SelectSelectionInput(root, sel, 0));
|
|
394
405
|
};
|
|
395
406
|
}
|
|
396
407
|
|
|
@@ -522,7 +533,6 @@ export default class Clipboard {
|
|
|
522
533
|
}
|
|
523
534
|
|
|
524
535
|
_onXEvent(ev) {
|
|
525
|
-
if (!this._window) return;
|
|
526
536
|
// XFixes events carry a server-assigned type above the core range, so
|
|
527
537
|
// this cannot be confused with core SelectionNotify, which is 31
|
|
528
538
|
if (ev.type === this._fixesFirstEvent && this._selectionWatchers.size) {
|
|
@@ -532,6 +542,7 @@ export default class Clipboard {
|
|
|
532
542
|
);
|
|
533
543
|
return;
|
|
534
544
|
}
|
|
545
|
+
if (!this._window) return;
|
|
535
546
|
if (ev.type === 28) {
|
|
536
547
|
// PropertyNotify, state 1 = Deleted: a requestor consumed the INCR
|
|
537
548
|
// chunk we gave it and is asking for the next one
|
package/lib/cliprect.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Geometry clipped to a rectangle, for what goes to the server in XRender's
|
|
2
|
+
// 16.16 fixed point: trapezoids and triangles. A coordinate past 32,767 there
|
|
3
|
+
// overflows the 32-bit word node-x11 writes it into, and the RangeError comes
|
|
4
|
+
// out of the paint that drew it. A box of a code block twenty thousand lines
|
|
5
|
+
// tall in a scroll pane was one, filled as a rounded rectangle.
|
|
6
|
+
//
|
|
7
|
+
// Only what reaches past the rectangle is clipped: everything else goes out
|
|
8
|
+
// as it came, so a drawing that fits is sent exactly as before.
|
|
9
|
+
|
|
10
|
+
/** Whether every point of a flat `[x, y, …]` list is inside the rectangle. */
|
|
11
|
+
export function withinRect(pts, x0, y0, x1, y1) {
|
|
12
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
13
|
+
const x = pts[i];
|
|
14
|
+
const y = pts[i + 1];
|
|
15
|
+
if (x < x0 || x > x1 || y < y0 || y > y1) return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A closed ring clipped to the rectangle (Sutherland–Hodgman), as a flat
|
|
22
|
+
* `[x, y, …]` list, empty when nothing of it is inside. Clipping each ring of
|
|
23
|
+
* a shape on its own keeps its fill, nonzero or even-odd, the same at every
|
|
24
|
+
* point inside a convex region.
|
|
25
|
+
*/
|
|
26
|
+
export function clipRingToRect(pts, x0, y0, x1, y1) {
|
|
27
|
+
let ring = pts;
|
|
28
|
+
// each edge of the rectangle: which coordinate, which bound, which side
|
|
29
|
+
for (const [axis, bound, keepAbove] of [
|
|
30
|
+
[0, x0, true],
|
|
31
|
+
[0, x1, false],
|
|
32
|
+
[1, y0, true],
|
|
33
|
+
[1, y1, false],
|
|
34
|
+
]) {
|
|
35
|
+
const n = ring.length / 2;
|
|
36
|
+
if (n === 0) break;
|
|
37
|
+
const out = [];
|
|
38
|
+
const inside = (i) =>
|
|
39
|
+
keepAbove ? ring[i * 2 + axis] >= bound : ring[i * 2 + axis] <= bound;
|
|
40
|
+
for (let i = 0; i < n; i++) {
|
|
41
|
+
const j = (i + n - 1) % n; // the previous point
|
|
42
|
+
const iin = inside(i);
|
|
43
|
+
const jin = inside(j);
|
|
44
|
+
if (iin !== jin) {
|
|
45
|
+
// where the segment from j to i crosses the bound
|
|
46
|
+
const a = ring[j * 2 + axis];
|
|
47
|
+
const b = ring[i * 2 + axis];
|
|
48
|
+
const t = (bound - a) / (b - a);
|
|
49
|
+
const other = 1 - axis;
|
|
50
|
+
const oa = ring[j * 2 + other];
|
|
51
|
+
const ob = ring[i * 2 + other];
|
|
52
|
+
const p = [0, 0];
|
|
53
|
+
p[axis] = bound;
|
|
54
|
+
p[other] = oa + (ob - oa) * t;
|
|
55
|
+
out.push(p[0], p[1]);
|
|
56
|
+
}
|
|
57
|
+
if (iin) out.push(ring[i * 2], ring[i * 2 + 1]);
|
|
58
|
+
}
|
|
59
|
+
ring = out;
|
|
60
|
+
}
|
|
61
|
+
return ring;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A flat list of triangles, six numbers each, with every one that reaches
|
|
66
|
+
* past the rectangle clipped to it and cut back into triangles (a fan: what
|
|
67
|
+
* is left of a triangle inside a rectangle is convex).
|
|
68
|
+
*/
|
|
69
|
+
export function clipTrianglesToRect(tris, x0, y0, x1, y1) {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (let i = 0; i + 5 < tris.length; i += 6) {
|
|
72
|
+
const tri = tris.slice(i, i + 6);
|
|
73
|
+
if (withinRect(tri, x0, y0, x1, y1)) {
|
|
74
|
+
out.push(...tri);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const poly = clipRingToRect(tri, x0, y0, x1, y1);
|
|
78
|
+
for (let k = 2; k * 2 + 1 < poly.length; k++) {
|
|
79
|
+
out.push(
|
|
80
|
+
poly[0],
|
|
81
|
+
poly[1],
|
|
82
|
+
poly[(k - 1) * 2],
|
|
83
|
+
poly[(k - 1) * 2 + 1],
|
|
84
|
+
poly[k * 2],
|
|
85
|
+
poly[k * 2 + 1],
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A flat `[x, y, w, h, …]` list of rectangles cut to `[0, width) × [0,
|
|
94
|
+
* height)`, dropping the ones with nothing left: core X's rectangles are 16
|
|
95
|
+
* bits. The list itself when every rectangle is already on the surface.
|
|
96
|
+
*/
|
|
97
|
+
export function rectsOnSurface(flat, width, height) {
|
|
98
|
+
let out = null;
|
|
99
|
+
for (let i = 0; i + 3 < flat.length; i += 4) {
|
|
100
|
+
const x = flat[i];
|
|
101
|
+
const y = flat[i + 1];
|
|
102
|
+
const w = flat[i + 2];
|
|
103
|
+
const h = flat[i + 3];
|
|
104
|
+
const inside = x >= 0 && y >= 0 && x + w <= width && y + h <= height;
|
|
105
|
+
if (inside) {
|
|
106
|
+
if (out) out.push(x, y, w, h);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!out) out = flat.slice(0, i);
|
|
110
|
+
const x0 = Math.max(x, 0);
|
|
111
|
+
const y0 = Math.max(y, 0);
|
|
112
|
+
const x1 = Math.min(x + w, width);
|
|
113
|
+
const y1 = Math.min(y + h, height);
|
|
114
|
+
if (x1 > x0 && y1 > y0) out.push(x0, y0, x1 - x0, y1 - y0);
|
|
115
|
+
}
|
|
116
|
+
return out ?? flat;
|
|
117
|
+
}
|
|
@@ -29,6 +29,12 @@ import { formatForDepth } from "./pictformat.js";
|
|
|
29
29
|
import Pixmap from "./pixmap.js";
|
|
30
30
|
import { REGION_DOCS, regionId } from "./region.js";
|
|
31
31
|
import { routeRaster } from "./rasterize.js";
|
|
32
|
+
import {
|
|
33
|
+
clipRingToRect,
|
|
34
|
+
clipTrianglesToRect,
|
|
35
|
+
rectsOnSurface,
|
|
36
|
+
withinRect,
|
|
37
|
+
} from "./cliprect.js";
|
|
32
38
|
import {
|
|
33
39
|
blurCoverage,
|
|
34
40
|
cachedShadow,
|
|
@@ -1868,9 +1874,36 @@ class RenderingContext2d {
|
|
|
1868
1874
|
});
|
|
1869
1875
|
}
|
|
1870
1876
|
|
|
1871
|
-
_rasterizePolys(
|
|
1877
|
+
_rasterizePolys(
|
|
1878
|
+
picture,
|
|
1879
|
+
shapes,
|
|
1880
|
+
rule,
|
|
1881
|
+
dx = 0,
|
|
1882
|
+
dy = 0,
|
|
1883
|
+
w = this.width,
|
|
1884
|
+
h = this.height,
|
|
1885
|
+
) {
|
|
1872
1886
|
if (!shapes.length) return;
|
|
1873
|
-
|
|
1887
|
+
// Trapezoids are 16.16 fixed point, and a coordinate past 32,767 throws
|
|
1888
|
+
// out of the paint (lib/cliprect.js). A shape that reaches past the
|
|
1889
|
+
// picture is cut to it first, a pixel to spare on every side: nothing
|
|
1890
|
+
// outside the picture is ever composited.
|
|
1891
|
+
const x0 = -dx - 1;
|
|
1892
|
+
const y0 = -dy - 1;
|
|
1893
|
+
const x1 = w - dx + 1;
|
|
1894
|
+
const y1 = h - dy + 1;
|
|
1895
|
+
let fitted = null;
|
|
1896
|
+
for (let i = 0; i < shapes.length; i++) {
|
|
1897
|
+
const shape = shapes[i];
|
|
1898
|
+
if (withinRect(shape, x0, y0, x1, y1)) {
|
|
1899
|
+
if (fitted) fitted.push(shape);
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
if (!fitted) fitted = shapes.slice(0, i);
|
|
1903
|
+
const ring = clipRingToRect(shape, x0, y0, x1, y1);
|
|
1904
|
+
if (ring.length >= 6) fitted.push(ring);
|
|
1905
|
+
}
|
|
1906
|
+
const traps = trapezoidize(fitted ?? shapes, dx, dy, [], rule);
|
|
1874
1907
|
// stay under the server's maximum request size
|
|
1875
1908
|
const chunk = 4000 * 6;
|
|
1876
1909
|
for (let i = 0; i < traps.length; i += chunk) {
|
|
@@ -2480,6 +2513,18 @@ class RenderingContext2d {
|
|
|
2480
2513
|
}
|
|
2481
2514
|
if (w < tris.length) tris.length = w;
|
|
2482
2515
|
if (!tris.length) return;
|
|
2516
|
+
// Triangles are 16.16 fixed point, and the source origin a direct stroke
|
|
2517
|
+
// hands the server is 16 bits: a stroke that reaches past the surface is
|
|
2518
|
+
// cut to it first (lib/cliprect.js), a pixel to spare on every side, so
|
|
2519
|
+
// what the surface shows is drawn exactly as before.
|
|
2520
|
+
const edgeX = this.width + 1;
|
|
2521
|
+
const edgeY = this.height + 1;
|
|
2522
|
+
if (!withinRect(tris, -1, -1, edgeX, edgeY)) {
|
|
2523
|
+
const cut = clipTrianglesToRect(tris, -1, -1, edgeX, edgeY);
|
|
2524
|
+
tris.length = 0;
|
|
2525
|
+
for (let i = 0; i < cut.length; i++) tris.push(cut[i]);
|
|
2526
|
+
if (!tris.length) return;
|
|
2527
|
+
}
|
|
2483
2528
|
|
|
2484
2529
|
const op = this._op();
|
|
2485
2530
|
// round-cap/join disks overlap the stroke body; overlapping coverage
|
|
@@ -3018,6 +3063,20 @@ class RenderingContext2d {
|
|
|
3018
3063
|
* for the whole list and then draws the rectangles through here */
|
|
3019
3064
|
_fillRect(x, y, w, h) {
|
|
3020
3065
|
if (matIsIdentity(this._m)) {
|
|
3066
|
+
// X coordinates and sizes are 16 bits, and a coordinate past 32,767
|
|
3067
|
+
// throws out of the paint (lib/cliprect.js): a rectangle that reaches
|
|
3068
|
+
// past the surface is filled as the part of it the surface has.
|
|
3069
|
+
if (x < 0 || y < 0 || x + w > this.width || y + h > this.height) {
|
|
3070
|
+
const x0 = Math.max(x, 0);
|
|
3071
|
+
const y0 = Math.max(y, 0);
|
|
3072
|
+
const x1 = Math.min(x + w, this.width);
|
|
3073
|
+
const y1 = Math.min(y + h, this.height);
|
|
3074
|
+
if (!(x1 > x0 && y1 > y0)) return;
|
|
3075
|
+
x = x0;
|
|
3076
|
+
y = y0;
|
|
3077
|
+
w = x1 - x0;
|
|
3078
|
+
h = y1 - y0;
|
|
3079
|
+
}
|
|
3021
3080
|
if (!prepareStyle(this._backgroundPicture, this._m)) return;
|
|
3022
3081
|
const op = this._op();
|
|
3023
3082
|
// A rectangular clip is a smaller rectangle to fill, not a mask — this
|
|
@@ -3113,6 +3172,10 @@ class RenderingContext2d {
|
|
|
3113
3172
|
}
|
|
3114
3173
|
if (this._hasPolyClip) return;
|
|
3115
3174
|
if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
|
|
3175
|
+
// the part of each the surface has, as `_fillRect` fills it: the wire's
|
|
3176
|
+
// coordinates are 16 bits
|
|
3177
|
+
const onSurface = rectsOnSurface(flat, this.width, this.height);
|
|
3178
|
+
if (!onSurface.length) return;
|
|
3116
3179
|
const R = this.Render;
|
|
3117
3180
|
if (clip) this._setPictureClip(clip);
|
|
3118
3181
|
// globalAlpha folds into the premultiplied colour — scaling all four
|
|
@@ -3122,12 +3185,14 @@ class RenderingContext2d {
|
|
|
3122
3185
|
const color = this._foldedColor(this._fillStyle);
|
|
3123
3186
|
// stay under the server's maximum request size
|
|
3124
3187
|
const chunk = 10000 * 4;
|
|
3125
|
-
for (let i = 0; i <
|
|
3188
|
+
for (let i = 0; i < onSurface.length; i += chunk) {
|
|
3126
3189
|
R.FillRectangles(
|
|
3127
3190
|
this._op(),
|
|
3128
3191
|
this._dst(),
|
|
3129
3192
|
color,
|
|
3130
|
-
i === 0 &&
|
|
3193
|
+
i === 0 && onSurface.length <= chunk
|
|
3194
|
+
? onSurface
|
|
3195
|
+
: onSurface.slice(i, i + chunk),
|
|
3131
3196
|
);
|
|
3132
3197
|
}
|
|
3133
3198
|
if (clip) this._invalidatePictureClip();
|
|
@@ -3166,10 +3231,13 @@ class RenderingContext2d {
|
|
|
3166
3231
|
let x1 = -Infinity;
|
|
3167
3232
|
let y1 = -Infinity;
|
|
3168
3233
|
for (let i = 0; i < flat.length; i += 4) {
|
|
3169
|
-
|
|
3170
|
-
const
|
|
3171
|
-
const
|
|
3172
|
-
const
|
|
3234
|
+
// the part of it on the surface: the wire's coordinates are 16 bits
|
|
3235
|
+
const left = Math.trunc(flat[i]);
|
|
3236
|
+
const top = Math.trunc(flat[i + 1]);
|
|
3237
|
+
const x = Math.max(0, left);
|
|
3238
|
+
const y = Math.max(0, top);
|
|
3239
|
+
const w = Math.min(this.width, left + Math.trunc(flat[i + 2])) - x;
|
|
3240
|
+
const h = Math.min(this.height, top + Math.trunc(flat[i + 3])) - y;
|
|
3173
3241
|
if (w <= 0 || h <= 0) continue;
|
|
3174
3242
|
rects.push(x, y, w, h);
|
|
3175
3243
|
if (x < x0) x0 = x;
|
|
@@ -3953,6 +4021,8 @@ class RenderingContext2d {
|
|
|
3953
4021
|
entry.rule,
|
|
3954
4022
|
-bb.x,
|
|
3955
4023
|
-bb.y,
|
|
4024
|
+
bb.w,
|
|
4025
|
+
bb.h,
|
|
3956
4026
|
);
|
|
3957
4027
|
}
|
|
3958
4028
|
R.Composite(
|
package/lib/text/layout.js
CHANGED
|
@@ -12,6 +12,19 @@ const WS = new Set([0x20, 0x09]);
|
|
|
12
12
|
const HARD_BREAKS = /[\n\r\u2028\u2029]+$/;
|
|
13
13
|
const TRAILING_WS = /[ \t]+$/;
|
|
14
14
|
|
|
15
|
+
/** Scripts written without spaces between words, whose line breaking
|
|
16
|
+
* needs a dictionary (UAX #14's SA class, and Javanese, Balinese and
|
|
17
|
+
* Buginese, which are AL there): a token of theirs is a run of words. */
|
|
18
|
+
const UNSPACED =
|
|
19
|
+
/[\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}\p{Script=Tai_Le}\p{Script=New_Tai_Lue}\p{Script=Tai_Tham}\p{Script=Tai_Viet}\p{Script=Ahom}\p{Script=Javanese}\p{Script=Balinese}\p{Script=Buginese}]/u;
|
|
20
|
+
|
|
21
|
+
/** How wide a line's tokens are, the white space it ends on left out. */
|
|
22
|
+
function tokensWidth(toks) {
|
|
23
|
+
let width = 0;
|
|
24
|
+
for (const token of toks) width += token.width;
|
|
25
|
+
return toks.length ? width - toks[toks.length - 1].wsWidth : 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
function isWsGlyph(g) {
|
|
16
29
|
return g.codePoints.length > 0 && g.codePoints.every((cp) => WS.has(cp));
|
|
17
30
|
}
|
|
@@ -27,8 +40,12 @@ function isWsGlyph(g) {
|
|
|
27
40
|
*
|
|
28
41
|
* Content is a plain string or an array of spans
|
|
29
42
|
* `{ text, family?, size?, weight?, style?, features?, language?,
|
|
30
|
-
* letterSpacing?, color? }`;
|
|
31
|
-
* span fields override the base style.
|
|
43
|
+
* letterSpacing?, color?, nowrap? }`;
|
|
44
|
+
* span fields override the base style. Spans that share a truthy `nowrap` —
|
|
45
|
+
* `true`, or any value a caller tells its groups apart by — have no break
|
|
46
|
+
* opportunity inside them or between them, as the text of an element with
|
|
47
|
+
* CSS's `white-space: nowrap` has none: the break after the last of them is
|
|
48
|
+
* the next span's to allow. Options:
|
|
32
49
|
*
|
|
33
50
|
* - `maxWidth` — target container width (default: unlimited)
|
|
34
51
|
* - `align` — 'left' | 'right' | 'center' | 'start' | 'end'
|
|
@@ -36,7 +53,16 @@ function isWsGlyph(g) {
|
|
|
36
53
|
* - `direction` — 'ltr' | 'rtl' | 'auto' base paragraph direction
|
|
37
54
|
* - `maxLines` — cap the number of lines (default: unlimited)
|
|
38
55
|
* - `overflow` — 'clip' (default) or 'ellipsis', what a `maxLines` cut looks
|
|
39
|
-
* like
|
|
56
|
+
* like, and a line that does not wrap cut at `maxWidth`. See `_elide`.
|
|
57
|
+
* - `wrap` — false lays the text out a line to each forced break however
|
|
58
|
+
* wide it is; with `overflow: 'ellipsis'` a line wider than `maxWidth` is
|
|
59
|
+
* cut there with an ellipsis, inside a word if need be, as CSS's
|
|
60
|
+
* `text-overflow` cuts text that does not wrap. Default true.
|
|
61
|
+
* - `overflowWrap` — 'break-word' (default) cuts a word wider than the line
|
|
62
|
+
* at a grapheme boundary; 'normal' keeps it whole on a line of its own,
|
|
63
|
+
* past the line's end, as CSS's `overflow-wrap: normal` does — but for
|
|
64
|
+
* text in a script written without spaces (Thai, Khmer, Javanese…), whose
|
|
65
|
+
* words the breaker cannot find, which is cut as a fallback
|
|
40
66
|
*
|
|
41
67
|
* The result is inspectable before/without drawing: `width`, `height`,
|
|
42
68
|
* `truncated`, and `lines[] = { x, y, height, baseline, width, ascent,
|
|
@@ -75,6 +101,13 @@ export class TextLayout {
|
|
|
75
101
|
|
|
76
102
|
// ---- greedy line fill ----
|
|
77
103
|
const lineTokens = [];
|
|
104
|
+
// a word wider than the line stays whole, past the line's end, where
|
|
105
|
+
// the options say so; cut inside itself where they do not — and in a
|
|
106
|
+
// script written without spaces, where the breaker's "word" is a phrase
|
|
107
|
+
// it had no dictionary to cut, cut anyway (CSS Text 3, 5.1)
|
|
108
|
+
const whole = options.overflowWrap === 'normal';
|
|
109
|
+
// text that does not wrap is a line to each forced break, however wide
|
|
110
|
+
const wraps = options.wrap !== false;
|
|
78
111
|
{
|
|
79
112
|
let cur = [];
|
|
80
113
|
let curWidth = 0;
|
|
@@ -84,9 +117,11 @@ export class TextLayout {
|
|
|
84
117
|
curWidth = 0;
|
|
85
118
|
};
|
|
86
119
|
for (let token of tokens) {
|
|
87
|
-
while (curWidth + token.width - token.wsWidth > maxWidth) {
|
|
120
|
+
while (wraps && curWidth + token.width - token.wsWidth > maxWidth) {
|
|
88
121
|
if (cur.length > 0) {
|
|
89
122
|
flush();
|
|
123
|
+
} else if (whole && !UNSPACED.test(text.slice(token.start, token.end))) {
|
|
124
|
+
break;
|
|
90
125
|
} else {
|
|
91
126
|
// single token wider than the container: force-break it
|
|
92
127
|
const [head, rest] = this._forceBreak(token, maxWidth);
|
|
@@ -109,10 +144,13 @@ export class TextLayout {
|
|
|
109
144
|
const maxLines = Number.isFinite(options.maxLines)
|
|
110
145
|
? Math.max(0, Math.floor(options.maxLines))
|
|
111
146
|
: Infinity;
|
|
112
|
-
/** did `maxLines` drop content? */
|
|
147
|
+
/** did `maxLines` drop content, or a line that does not wrap its end? */
|
|
113
148
|
this.truncated = lineTokens.length > maxLines;
|
|
114
149
|
if (this.truncated) lineTokens.length = maxLines;
|
|
115
|
-
const
|
|
150
|
+
const ellipses = options.overflow === 'ellipsis';
|
|
151
|
+
const elide = this.truncated && ellipses;
|
|
152
|
+
// and a line that does not wrap is cut where it runs past the width
|
|
153
|
+
const cutWide = ellipses && !wraps && Number.isFinite(maxWidth);
|
|
116
154
|
|
|
117
155
|
// ---- assemble lines: strip trailing ws, bidi-reorder, position ----
|
|
118
156
|
const baseSpan = spans[0];
|
|
@@ -126,10 +164,15 @@ export class TextLayout {
|
|
|
126
164
|
const lineStart = toks.length ? toks[0].start : 0;
|
|
127
165
|
let lineEnd = toks.length ? toks[toks.length - 1].end : lineStart;
|
|
128
166
|
|
|
129
|
-
// Elision is the last kept line's business
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
// Elision is the last kept line's business, or a line's that does not
|
|
168
|
+
// wrap and runs past the width, and it happens before entries exist,
|
|
169
|
+
// because dropping content means re-shaping the tail.
|
|
170
|
+
const cut = (elide && li === lineTokens.length - 1) || (cutWide && tokensWidth(toks) > maxWidth);
|
|
171
|
+
const ellipsis = cut ? this._elide(toks, baseSpan) : null;
|
|
172
|
+
if (ellipsis) {
|
|
173
|
+
toks = this._fitBefore(toks, maxWidth - ellipsis.width);
|
|
174
|
+
this.truncated = true;
|
|
175
|
+
}
|
|
133
176
|
|
|
134
177
|
// entries carry .level so reorderRuns can order them (UAX#9 L2);
|
|
135
178
|
// .start/.end are absolute code-unit ranges into the full text, kept
|
|
@@ -238,10 +281,13 @@ export class TextLayout {
|
|
|
238
281
|
let align = options.align ?? 'start';
|
|
239
282
|
if (align === 'start') align = this.baseLevel ? 'right' : 'left';
|
|
240
283
|
if (align === 'end') align = this.baseLevel ? 'left' : 'right';
|
|
241
|
-
if (align !== 'left') {
|
|
284
|
+
if (align !== 'left' || this.baseLevel) {
|
|
242
285
|
for (const line of this.lines) {
|
|
243
286
|
const free = container - line.width;
|
|
244
|
-
line
|
|
287
|
+
// a line too long for its container starts at its start edge and
|
|
288
|
+
// runs past its end, whatever the alignment (CSS Text 3, 7.1)
|
|
289
|
+
if (free < 0) line.x = this.baseLevel ? free : 0;
|
|
290
|
+
else if (align !== 'left') line.x = align === 'right' ? free : free / 2;
|
|
245
291
|
}
|
|
246
292
|
}
|
|
247
293
|
}
|
|
@@ -264,6 +310,13 @@ export class TextLayout {
|
|
|
264
310
|
// and every line still needs a style to take its metrics from, so
|
|
265
311
|
// stand one in rather than crashing on the first empty line.
|
|
266
312
|
const given = typeof content === 'string' ? [{ text: content }] : content;
|
|
313
|
+
// What a span is shaped with, one per distinct style rather than one a
|
|
314
|
+
// span (`shapingStyleOf`): the spans of a highlighted source file are its
|
|
315
|
+
// tokens, half a million for twenty thousand lines over a handful of
|
|
316
|
+
// styles, and the font lookup and the shaping memo's key, worked out
|
|
317
|
+
// afresh for each, were most of what laying such a file out cost.
|
|
318
|
+
const styles = [];
|
|
319
|
+
const shapings = [];
|
|
267
320
|
const spans = (given.length ? given : [{ text: '' }]).map((s) => {
|
|
268
321
|
const merged = {
|
|
269
322
|
...s,
|
|
@@ -281,7 +334,9 @@ export class TextLayout {
|
|
|
281
334
|
letterSpacing: s.letterSpacing ?? style.letterSpacing,
|
|
282
335
|
color: s.color ?? style.color ?? null
|
|
283
336
|
};
|
|
284
|
-
|
|
337
|
+
const shaping = shapingStyleOf(styles, merged, s.font ?? style.font, fonts);
|
|
338
|
+
merged.font = shaping.font;
|
|
339
|
+
shapings.push(shaping);
|
|
285
340
|
return merged;
|
|
286
341
|
});
|
|
287
342
|
|
|
@@ -309,17 +364,24 @@ export class TextLayout {
|
|
|
309
364
|
}
|
|
310
365
|
return {
|
|
311
366
|
span: spans[lo],
|
|
367
|
+
shaping: shapings[lo],
|
|
312
368
|
end: lo + 1 < spanStarts.length ? spanStarts[lo + 1] : text.length
|
|
313
369
|
};
|
|
314
370
|
};
|
|
315
371
|
|
|
316
372
|
// ---- tokenize at UAX#14 break opportunities ----
|
|
373
|
+
// none between two characters of spans that share a `nowrap`
|
|
374
|
+
const held = spans.some((s) => s.nowrap);
|
|
317
375
|
const tokens = [];
|
|
318
376
|
{
|
|
319
377
|
const breaker = new LineBreaker(text);
|
|
320
378
|
let prev = 0;
|
|
321
379
|
let bk;
|
|
322
380
|
while ((bk = breaker.nextBreak())) {
|
|
381
|
+
if (held && !bk.required && bk.position < text.length) {
|
|
382
|
+
const before = spanAt(bk.position - 1).span.nowrap;
|
|
383
|
+
if (before && before === spanAt(bk.position).span.nowrap) continue;
|
|
384
|
+
}
|
|
323
385
|
tokens.push(this._makeToken(text, prev, bk.position, levels, spanAt, bk.required));
|
|
324
386
|
prev = bk.position;
|
|
325
387
|
}
|
|
@@ -336,13 +398,13 @@ export class TextLayout {
|
|
|
336
398
|
let width = 0;
|
|
337
399
|
let pos = start;
|
|
338
400
|
while (pos < end) {
|
|
339
|
-
const { span, end: spanEnd } = spanAt(pos);
|
|
401
|
+
const { span, shaping, end: spanEnd } = spanAt(pos);
|
|
340
402
|
const fragEnd = Math.min(end, spanEnd);
|
|
341
403
|
// hard-break controls only terminate the line; never shape them
|
|
342
404
|
const fragText = text.slice(pos, fragEnd).replace(HARD_BREAKS, '');
|
|
343
405
|
if (fragText.length > 0) {
|
|
344
406
|
const fragLevels = normalizedLevels(levels, pos, pos + fragText.length);
|
|
345
|
-
const shaped = this.fonts._shapeCached(fragText,
|
|
407
|
+
const shaped = this.fonts._shapeCached(fragText, shaping, fragLevels);
|
|
346
408
|
// the levels ride along so a later split can re-shape a piece at the
|
|
347
409
|
// level it actually has, rather than assuming ltr
|
|
348
410
|
fragments.push({ text: fragText, span, shaped, start: pos, levels: fragLevels });
|
|
@@ -543,7 +605,20 @@ export class TextLayout {
|
|
|
543
605
|
}
|
|
544
606
|
batch = [];
|
|
545
607
|
};
|
|
608
|
+
// Only the lines the context can show. A layout taller than the view it
|
|
609
|
+
// is drawn in — a code block of twenty thousand lines in a scroll pane —
|
|
610
|
+
// built every line's glyphs, sent them, and had the server clip them
|
|
611
|
+
// away, on every paint. A line is skipped when its box, grown by its
|
|
612
|
+
// height and its font's extent again on either side for ink that
|
|
613
|
+
// overhangs the box, is outside the rows the clip lets through: nothing
|
|
614
|
+
// of it could have landed on a pixel.
|
|
615
|
+
const rows = visibleRows(ctx, y);
|
|
546
616
|
for (const line of this.lines) {
|
|
617
|
+
if (rows) {
|
|
618
|
+
const reach = line.height + line.ascent + line.descent;
|
|
619
|
+
if (line.y + line.height + reach < rows.top) continue;
|
|
620
|
+
if (line.y - reach > rows.bottom) continue;
|
|
621
|
+
}
|
|
547
622
|
for (const r of line.runs) {
|
|
548
623
|
const color = r.span.color;
|
|
549
624
|
if (batch.length && color !== batchColor) flush();
|
|
@@ -963,3 +1038,77 @@ function sliceLevels(key, start, end) {
|
|
|
963
1038
|
if (!key.includes(',')) return key;
|
|
964
1039
|
return key.split(',').slice(start, end).join(',');
|
|
965
1040
|
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* The rows of a layout drawn at `y` that the context can show — its clip's
|
|
1044
|
+
* box, or the whole surface with no clip — in the layout's own coordinates.
|
|
1045
|
+
* Null where that cannot be said, and the whole layout is drawn: a context
|
|
1046
|
+
* that is not ntk's, a transform that is more than a translation, or a
|
|
1047
|
+
* shadow, which lands where the text is not.
|
|
1048
|
+
*/
|
|
1049
|
+
function visibleRows(ctx, y) {
|
|
1050
|
+
if (typeof ctx.drawGlyphs !== 'function') return null;
|
|
1051
|
+
if (typeof ctx._clipExtents !== 'function') return null;
|
|
1052
|
+
const m = ctx._m;
|
|
1053
|
+
if (!m || m[0] !== 1 || m[1] !== 0 || m[2] !== 0 || m[3] !== 1) return null;
|
|
1054
|
+
if (typeof ctx._shadowed !== 'function' || ctx._shadowed()) return null;
|
|
1055
|
+
const height = ctx.height;
|
|
1056
|
+
if (!(height > 0)) return null;
|
|
1057
|
+
// the clip's extents skip a region clip, so they are never smaller than
|
|
1058
|
+
// what the server lets through
|
|
1059
|
+
const box = ctx._clipExtents() ?? { y: 0, h: height };
|
|
1060
|
+
const top = box.y - (y + m[5]);
|
|
1061
|
+
return { top, bottom: top + box.h };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* How many distinct styles one layout shares shaping styles among. Past it a
|
|
1066
|
+
* span is shaped with a style of its own, as every span once was, rather
|
|
1067
|
+
* than the search growing with the document.
|
|
1068
|
+
*/
|
|
1069
|
+
const MAX_SHAPING_STYLES = 16;
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* The style `span` is shaped with: one already made for a span whose every
|
|
1073
|
+
* property shaping reads is the same (`===`), or a new one. Its font is the
|
|
1074
|
+
* one the span or the layout named (`given`), else the one its family
|
|
1075
|
+
* matches, looked up once for all the spans that share it. What shaping
|
|
1076
|
+
* reads is what `shapeText`, `fallbackFor` and the memo key read: the face
|
|
1077
|
+
* and what picks it, the size, and features, language and spacing.
|
|
1078
|
+
*/
|
|
1079
|
+
function shapingStyleOf(known, span, given, fonts) {
|
|
1080
|
+
for (let i = 0; i < known.length; i++) {
|
|
1081
|
+
const k = known[i];
|
|
1082
|
+
if (
|
|
1083
|
+
k.given === given &&
|
|
1084
|
+
k.family === span.family &&
|
|
1085
|
+
k.size === span.size &&
|
|
1086
|
+
k.weight === span.weight &&
|
|
1087
|
+
k.style === span.style &&
|
|
1088
|
+
k.variations === span.variations &&
|
|
1089
|
+
k.opticalSize === span.opticalSize &&
|
|
1090
|
+
k.opticalSizing === span.opticalSizing &&
|
|
1091
|
+
k.features === span.features &&
|
|
1092
|
+
k.language === span.language &&
|
|
1093
|
+
k.letterSpacing === span.letterSpacing
|
|
1094
|
+
) {
|
|
1095
|
+
return k;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const k = {
|
|
1099
|
+
given,
|
|
1100
|
+
font: given ?? fonts.match(span.family, span),
|
|
1101
|
+
family: span.family,
|
|
1102
|
+
size: span.size,
|
|
1103
|
+
weight: span.weight,
|
|
1104
|
+
style: span.style,
|
|
1105
|
+
variations: span.variations,
|
|
1106
|
+
opticalSize: span.opticalSize,
|
|
1107
|
+
opticalSizing: span.opticalSizing,
|
|
1108
|
+
features: span.features,
|
|
1109
|
+
language: span.language,
|
|
1110
|
+
letterSpacing: span.letterSpacing
|
|
1111
|
+
};
|
|
1112
|
+
if (known.length < MAX_SHAPING_STYLES) known.push(k);
|
|
1113
|
+
return k;
|
|
1114
|
+
}
|
package/lib/window.js
CHANGED
|
@@ -448,6 +448,16 @@ function decodeProperty(prop, as, X) {
|
|
|
448
448
|
return { type: prop.type, data: prop.data };
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
+
/**
|
|
452
|
+
* The frame clock `NTK_FRAME_CLOCK` names for every window that does not
|
|
453
|
+
* name one itself: `'auto'`, `'present'` or `'fence'` (see `frameClock`),
|
|
454
|
+
* for trying a machine both ways without touching the app.
|
|
455
|
+
*/
|
|
456
|
+
function envFrameClock() {
|
|
457
|
+
const v = globalThis.process?.env?.NTK_FRAME_CLOCK;
|
|
458
|
+
return v === 'auto' || v === 'present' || v === 'fence' ? v : undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
451
461
|
export default class Window extends Drawable {
|
|
452
462
|
// one wrapper per X window id and connection: constructing a Window for a
|
|
453
463
|
// known id returns the cached instance. Keyed by connection as well as
|
|
@@ -545,7 +555,7 @@ export default class Window extends Drawable {
|
|
|
545
555
|
// everywhere else, including a server whose Present has no display behind
|
|
546
556
|
// it (see _probeMsc); 'present' takes Present's clock even there; 'fence'
|
|
547
557
|
// pins the round-trip clock
|
|
548
|
-
this._frameClockPref = args.frameClock ?? 'auto';
|
|
558
|
+
this._frameClockPref = args.frameClock ?? envFrameClock() ?? 'auto';
|
|
549
559
|
// _NET_WM_SYNC_REQUEST (see enableSyncRequest): the counter the window
|
|
550
560
|
// manager watches, the value it last asked for, and the watchdog that
|
|
551
561
|
// guarantees an answer even when nothing repaints
|
|
@@ -766,7 +776,10 @@ export default class Window extends Drawable {
|
|
|
766
776
|
}
|
|
767
777
|
// EWMH wants a pid and a client machine on top-level windows; child
|
|
768
778
|
// windows are an implementation detail nothing asks about
|
|
769
|
-
|
|
779
|
+
// a window of ours at the root: the one a window manager and a
|
|
780
|
+
// compositor treat as the app's window (see `_askNotToBypass`)
|
|
781
|
+
this._topLevel = !args.id && parentId === app.display.screen[0].root;
|
|
782
|
+
if (this._topLevel && args.pid !== false) {
|
|
770
783
|
this.setPid(typeof args.pid === 'number' ? args.pid : undefined);
|
|
771
784
|
}
|
|
772
785
|
if (args.alwaysOnTop) {
|
|
@@ -1539,6 +1552,7 @@ export default class Window extends Drawable {
|
|
|
1539
1552
|
this._frameSyncEnabled &&
|
|
1540
1553
|
!this._clockStalled &&
|
|
1541
1554
|
!this._presentAbandoned() &&
|
|
1555
|
+
!this._preferFence() &&
|
|
1542
1556
|
!!this._presentExt &&
|
|
1543
1557
|
!!this._presentEid
|
|
1544
1558
|
);
|
|
@@ -2432,6 +2446,10 @@ export default class Window extends Drawable {
|
|
|
2432
2446
|
* caller who opted out is not sent events it will not read.
|
|
2433
2447
|
*/
|
|
2434
2448
|
_selectPresentInput() {
|
|
2449
|
+
// whether a compositor runs decides how this window's frames end, so the
|
|
2450
|
+
// first window to present asks (App#compositing)
|
|
2451
|
+
void this.app?.compositing;
|
|
2452
|
+
if (this._preferFence()) this._askNotToBypass();
|
|
2435
2453
|
const P = this._presentExt;
|
|
2436
2454
|
if (!P || this._presentEid || this._destroyed) return;
|
|
2437
2455
|
if (this._frameClockPref === 'fence' || !this._frameSyncEnabled) return;
|
|
@@ -2453,10 +2471,62 @@ export default class Window extends Drawable {
|
|
|
2453
2471
|
*/
|
|
2454
2472
|
_blitsThroughPresent() {
|
|
2455
2473
|
return (
|
|
2456
|
-
!!this._presentExt &&
|
|
2474
|
+
!!this._presentExt &&
|
|
2475
|
+
!!this._fixesExt &&
|
|
2476
|
+
!!this._updateRegion &&
|
|
2477
|
+
!this._presentAbandoned() &&
|
|
2478
|
+
!this._preferFence()
|
|
2457
2479
|
);
|
|
2458
2480
|
}
|
|
2459
2481
|
|
|
2482
|
+
/**
|
|
2483
|
+
* Whether this window leaves Present to the compositor: under a
|
|
2484
|
+
* compositing manager (`app.compositing`) the fence ends its frames and
|
|
2485
|
+
* CopyArea blits them, the configuration a window whose server makes its
|
|
2486
|
+
* vblanks up already runs on (`_presentAbandoned`). The blit goes with the
|
|
2487
|
+
* clock for the same reason as there — a present owns the backing store
|
|
2488
|
+
* until it executes, and a frame drawn behind one on the fence can put
|
|
2489
|
+
* half of itself on the screen (#223).
|
|
2490
|
+
*
|
|
2491
|
+
* Not on Xwayland, whose Present clock is the Wayland compositor's frame
|
|
2492
|
+
* callback; not while `frameClock: 'present'` asks for Present's clock by
|
|
2493
|
+
* name; and not until the server has said whether a compositor runs.
|
|
2494
|
+
* Reversible, unlike `_presentAbandoned`: a compositor that stops hands
|
|
2495
|
+
* the window back to Present's clock at its next frame.
|
|
2496
|
+
*/
|
|
2497
|
+
_preferFence() {
|
|
2498
|
+
const app = this.app;
|
|
2499
|
+
return this._frameClockPref === 'auto' && app?.compositing === true && !app.xwayland;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
/**
|
|
2503
|
+
* EWMH `_NET_WM_BYPASS_COMPOSITOR` = 2 on a top-level window leaving
|
|
2504
|
+
* Present to the compositor: "do not unredirect me". Mutter, Muffin and
|
|
2505
|
+
* KWin unredirect a window that covers the screen, and its CopyArea blits
|
|
2506
|
+
* would then go to the scanout directly, where a copy can tear — what a
|
|
2507
|
+
* present synced to the vertical blank was there to prevent. Asked once;
|
|
2508
|
+
* a compositor that ignores it is one that keeps every window redirected.
|
|
2509
|
+
*/
|
|
2510
|
+
_askNotToBypass() {
|
|
2511
|
+
if (this._bypassAsked || !this._topLevel || this._destroyed) return;
|
|
2512
|
+
this._bypassAsked = true;
|
|
2513
|
+
const X = this.X;
|
|
2514
|
+
this._withAtoms(['_NET_WM_BYPASS_COMPOSITOR'], (atoms) => {
|
|
2515
|
+
safeRelease(X, () => {
|
|
2516
|
+
X.ChangeProperty(0, this.id, atoms._NET_WM_BYPASS_COMPOSITOR, X.atoms.CARDINAL, 32, [2]);
|
|
2517
|
+
});
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
/** The compositor started or stopped: the next frame goes by the clock
|
|
2522
|
+
* that fits. A present in flight still gates it until its completion
|
|
2523
|
+
* hands the backing store back, whichever clock is running. */
|
|
2524
|
+
_compositingChanged() {
|
|
2525
|
+
if (this._preferFence()) this._askNotToBypass();
|
|
2526
|
+
const f = this._frame;
|
|
2527
|
+
if (f.pending.size || f.rafCbs.length || f.needsRedraw) this._scheduleFrame();
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2460
2530
|
/**
|
|
2461
2531
|
* The Present form of the blit. Returns false when it did not happen, so
|
|
2462
2532
|
* the caller falls back to CopyArea — which is also what runs before the
|
|
@@ -2601,13 +2671,21 @@ export default class Window extends Drawable {
|
|
|
2601
2671
|
needConfigureWindow = true;
|
|
2602
2672
|
configureProps.y = newState.y;
|
|
2603
2673
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2674
|
+
// A new size goes out whole, both axes, even when only one of them
|
|
2675
|
+
// changed. A window manager answers a request that names one axis with
|
|
2676
|
+
// its own idea of the other, and Muffin (Cinnamon) takes the height as its
|
|
2677
|
+
// frame's, title bar included: every width-only request grew the window
|
|
2678
|
+
// by 32 px, the next one by 32 more. Xlib's XResizeWindow and GTK always
|
|
2679
|
+
// send both, which is why nobody else trips over it.
|
|
2680
|
+
if (
|
|
2681
|
+
(newState.width !== undefined && this.width !== newState.width) ||
|
|
2682
|
+
(newState.height !== undefined && this.height !== newState.height)
|
|
2683
|
+
) {
|
|
2684
|
+
const width = newState.width ?? this.width;
|
|
2685
|
+
const height = newState.height ?? this.height;
|
|
2609
2686
|
needConfigureWindow = true;
|
|
2610
|
-
configureProps.
|
|
2687
|
+
if (width !== undefined) configureProps.width = width;
|
|
2688
|
+
if (height !== undefined) configureProps.height = height;
|
|
2611
2689
|
}
|
|
2612
2690
|
if (needConfigureWindow) {
|
|
2613
2691
|
this.X.ConfigureWindow(this.id, configureProps);
|