ntk 6.0.1 → 6.2.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/lib/app.js +22 -0
- package/lib/image.js +11 -7
- package/lib/renderingcontext_2d.js +52 -29
- package/lib/shm-upload.js +166 -0
- package/lib/widgets/htmlview.js +4 -2
- package/lib/widgets/markdownview.js +4 -2
- package/lib/window.js +56 -5
- package/package.json +2 -2
package/lib/app.js
CHANGED
|
@@ -4,6 +4,7 @@ import { chooseGLXConfig } from './glx.js';
|
|
|
4
4
|
import Picture from './picture.js';
|
|
5
5
|
import Pixmap from './pixmap.js';
|
|
6
6
|
import { DEFAULT_RASTER_POLICY, defaultRasterizer } from './rasterize.js';
|
|
7
|
+
import { ShmUploader } from './shm-upload.js';
|
|
7
8
|
import FontManager from './text/fontmanager.js';
|
|
8
9
|
import Window from './window.js';
|
|
9
10
|
|
|
@@ -45,6 +46,7 @@ export default class App {
|
|
|
45
46
|
this._cursors = null;
|
|
46
47
|
this._solidPictures = new Map();
|
|
47
48
|
this._rasterizer = undefined;
|
|
49
|
+
this._shm = undefined;
|
|
48
50
|
// node-x11 emits X errors it cannot route to a request callback as
|
|
49
51
|
// 'error' on the client — from inside its packet parser. With no
|
|
50
52
|
// listener that emit throws and the parser never re-arms, silently
|
|
@@ -81,6 +83,22 @@ export default class App {
|
|
|
81
83
|
this._rasterizer = value;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The MIT-SHM upload helper: large PutImage/GetImage traffic (images,
|
|
88
|
+
* putImageData, getImageData) travels through shared memory on a local
|
|
89
|
+
* connection instead of the socket, and falls back to core requests
|
|
90
|
+
* everywhere else. Resolved lazily; availability follows node-x11's `shm`
|
|
91
|
+
* connection option (unset = on where possible, `false`/`'off'` = disabled).
|
|
92
|
+
* See lib/shm-upload.js and docs/context-2d.md.
|
|
93
|
+
*/
|
|
94
|
+
get shm() {
|
|
95
|
+
if (this._shm === undefined) {
|
|
96
|
+
this._shm = new ShmUploader(this);
|
|
97
|
+
this._shm.resolve(() => {}); // probe availability in the background
|
|
98
|
+
}
|
|
99
|
+
return this._shm;
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
/** thresholds for that decision, merged over DEFAULT_RASTER_POLICY */
|
|
85
103
|
get rasterPolicy() {
|
|
86
104
|
return this.options.rasterPolicy
|
|
@@ -212,6 +230,10 @@ export default class App {
|
|
|
212
230
|
this._cursors.dispose();
|
|
213
231
|
this._cursors = null;
|
|
214
232
|
}
|
|
233
|
+
if (this._shm) {
|
|
234
|
+
this._shm.dispose();
|
|
235
|
+
this._shm = undefined;
|
|
236
|
+
}
|
|
215
237
|
// shared by every context that ever asked (see solidPicture), so their
|
|
216
238
|
// lifetime is the connection's — context teardown must not free them
|
|
217
239
|
for (const picture of this._solidPictures.values()) {
|
package/lib/image.js
CHANGED
|
@@ -59,13 +59,17 @@ export class Image {
|
|
|
59
59
|
gc = app._imageUploadGC = X.AllocID();
|
|
60
60
|
X.CreateGC(gc, pixmap.id);
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
62
|
+
const bytes = Buffer.isBuffer(bgra) ? bgra : Buffer.from(bgra.buffer, bgra.byteOffset, bgra.byteLength);
|
|
63
|
+
// Shared memory for a large image; otherwise upload row bands that stay
|
|
64
|
+
// under the server's maximum request size.
|
|
65
|
+
if (!app.shm.putImage(pixmap.id, gc, { width: this.width, height: this.height, depth: 32, data: bytes })) {
|
|
66
|
+
const stride = this.width * 4;
|
|
67
|
+
const maxBytes = ((app.display.max_request_length ?? 65535) - 8) * 4;
|
|
68
|
+
const rowsPerBand = Math.max(1, Math.floor(maxBytes / stride));
|
|
69
|
+
for (let y = 0; y < this.height; y += rowsPerBand) {
|
|
70
|
+
const rows = Math.min(rowsPerBand, this.height - y);
|
|
71
|
+
X.PutImage(2, pixmap.id, gc, this.width, rows, 0, y, 0, 32, bytes.subarray(y * stride, (y + rows) * stride));
|
|
72
|
+
}
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
const picture = new Picture(app, { drawable: pixmap, format: Render.rgba32 });
|
|
@@ -1942,24 +1942,36 @@ class RenderingContext2d {
|
|
|
1942
1942
|
|
|
1943
1943
|
const layout = this._layout;
|
|
1944
1944
|
const bytes = fromStraightRgba(rgba, layout, sw, sh);
|
|
1945
|
-
//
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1945
|
+
// Shared memory for a large blit; otherwise upload row bands that stay
|
|
1946
|
+
// under the server's maximum request size.
|
|
1947
|
+
if (
|
|
1948
|
+
!this.window.app.shm.putImage(this._target.id, this._gc, {
|
|
1949
|
+
width: sw,
|
|
1950
|
+
height: sh,
|
|
1951
|
+
depth: layout.depth,
|
|
1952
|
+
dstX: x + sx,
|
|
1953
|
+
dstY: y + sy,
|
|
1954
|
+
data: bytes,
|
|
1955
|
+
})
|
|
1956
|
+
) {
|
|
1957
|
+
const stride = sw * 4;
|
|
1958
|
+
const maxBytes = ((this.display.max_request_length ?? 65535) - 8) * 4;
|
|
1959
|
+
const rowsPerBand = Math.max(1, Math.floor(maxBytes / stride));
|
|
1960
|
+
for (let row = 0; row < sh; row += rowsPerBand) {
|
|
1961
|
+
const rows = Math.min(rowsPerBand, sh - row);
|
|
1962
|
+
this.X.PutImage(
|
|
1963
|
+
2, // ZPixmap
|
|
1964
|
+
this._target.id,
|
|
1965
|
+
this._gc,
|
|
1966
|
+
sw,
|
|
1967
|
+
rows,
|
|
1968
|
+
x + sx,
|
|
1969
|
+
y + sy + row,
|
|
1970
|
+
0,
|
|
1971
|
+
layout.depth,
|
|
1972
|
+
bytes.subarray(row * stride, (row + rows) * stride),
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1963
1975
|
}
|
|
1964
1976
|
this._markDirty();
|
|
1965
1977
|
}
|
|
@@ -2007,20 +2019,31 @@ class RenderingContext2d {
|
|
|
2007
2019
|
*/
|
|
2008
2020
|
readPixels(x, y, w, h) {
|
|
2009
2021
|
const layout = this._layout;
|
|
2022
|
+
const target = this._target.id;
|
|
2023
|
+
const X = this.X;
|
|
2010
2024
|
return new Promise((resolve, reject) => {
|
|
2011
|
-
|
|
2012
|
-
2,
|
|
2013
|
-
this._target.id,
|
|
2014
|
-
x,
|
|
2015
|
-
y,
|
|
2016
|
-
w,
|
|
2017
|
-
h,
|
|
2018
|
-
0xffffffff,
|
|
2019
|
-
(err, img) => {
|
|
2025
|
+
const coreGet = () =>
|
|
2026
|
+
X.GetImage(2, target, x, y, w, h, 0xffffffff, (err, img) => {
|
|
2020
2027
|
if (err) return reject(err);
|
|
2021
2028
|
resolve({ width: w, height: h, data: img.data, layout, ...layout });
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
// A readback is where shared memory helps most: a core GetImage sends the
|
|
2032
|
+
// whole image back over the socket and can block the server for tens of
|
|
2033
|
+
// milliseconds. Route large ones through a segment; fall back on any miss.
|
|
2034
|
+
const bytesPerPixel = layout.bitsPerPixel ? layout.bitsPerPixel >> 3 : 4;
|
|
2035
|
+
const shm = this.window.app.shm;
|
|
2036
|
+
if (shm.wantsReadback(w * h * bytesPerPixel)) {
|
|
2037
|
+
shm.getImage(target, x, y, w, h, layout.depth, (err, buf, rep) => {
|
|
2038
|
+
if (err) return coreGet();
|
|
2039
|
+
// buf is the segment's own memory, reused after this callback returns
|
|
2040
|
+
// — copy out synchronously before it is handed to the next reader
|
|
2041
|
+
const data = Buffer.from(buf.subarray(0, rep.size));
|
|
2042
|
+
resolve({ width: w, height: h, data, layout, ...layout });
|
|
2043
|
+
});
|
|
2044
|
+
} else {
|
|
2045
|
+
coreGet();
|
|
2046
|
+
}
|
|
2024
2047
|
});
|
|
2025
2048
|
}
|
|
2026
2049
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Optional MIT-SHM fast path for bulk pixel transfers.
|
|
2
|
+
//
|
|
3
|
+
// Large PutImage/GetImage traffic on a local connection is much cheaper through
|
|
4
|
+
// shared memory than down the socket — roughly 2x on uploads and many times
|
|
5
|
+
// faster on readbacks (which otherwise pay for a whole image-sized reply). This
|
|
6
|
+
// wraps node-x11's provider-backed MIT-SHM API (X.require('shm')) into a small,
|
|
7
|
+
// ordering-safe helper the 2d context and Image use, and falls back to core
|
|
8
|
+
// PutImage/GetImage wherever SHM will not attach (remote display, old server,
|
|
9
|
+
// no /dev/shm) or the transfer is too small to bother.
|
|
10
|
+
//
|
|
11
|
+
// Ordering discipline: an SHM upload is only ever issued through a segment that
|
|
12
|
+
// is *already attached* — so it slots into the request stream synchronously,
|
|
13
|
+
// exactly where a core PutImage would have gone, and never reorders drawing.
|
|
14
|
+
// Attaching a segment costs a round trip, so the first upload of a given size
|
|
15
|
+
// falls back to core and warms a segment in the background; the next one is a
|
|
16
|
+
// synchronous shared-memory write. A segment is returned to the pool only after
|
|
17
|
+
// the server signals (ShmCompletion) it has finished reading, so a reused
|
|
18
|
+
// buffer is never overwritten mid-read.
|
|
19
|
+
|
|
20
|
+
// Below this many bytes the socket is as fast as shared memory (measured: a
|
|
21
|
+
// PutImage and a tmpfs write are both syscall-bound at a few KB; the win starts
|
|
22
|
+
// around 64 KB and grows with size). Coverage masks (a8, 1 byte/pixel) stay
|
|
23
|
+
// under it at every size the rasterizer produces, which is why the 2d fill mask
|
|
24
|
+
// keeps using core PutImage.
|
|
25
|
+
const MIN_UPLOAD_BYTES = 64 * 1024;
|
|
26
|
+
// A readback pays the win sooner: a core GetImage reply carries the pixels back
|
|
27
|
+
// over the socket and, on some drivers, blocks the server for tens of
|
|
28
|
+
// milliseconds, so route more of them through shared memory.
|
|
29
|
+
const MIN_READBACK_BYTES = 16 * 1024;
|
|
30
|
+
// Round segment sizes up so a handful of buckets serve many nearby sizes and
|
|
31
|
+
// the pool actually hits.
|
|
32
|
+
const SIZE_QUANTUM = 64 * 1024;
|
|
33
|
+
|
|
34
|
+
function roundUp(n) {
|
|
35
|
+
return Math.max(SIZE_QUANTUM, Math.ceil(n / SIZE_QUANTUM) * SIZE_QUANTUM);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ShmUploader {
|
|
39
|
+
/** @param {import('./app.js').default} app */
|
|
40
|
+
constructor(app) {
|
|
41
|
+
this.app = app;
|
|
42
|
+
this.X = app.X;
|
|
43
|
+
this.ext = null; // node-x11 shm extension once required; false if unavailable
|
|
44
|
+
this.ready = false; // usable() confirmed a segment attaches
|
|
45
|
+
this._pool = []; // free, already-read segments (not in flight)
|
|
46
|
+
this._warming = false;
|
|
47
|
+
this._resolving = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve availability once (requires the extension and probes usable()).
|
|
52
|
+
* `cb(ready)`. Cheap and idempotent; safe to call before every draw.
|
|
53
|
+
*/
|
|
54
|
+
resolve(cb) {
|
|
55
|
+
if (this.ready || this.ext === false) return cb(this.ready);
|
|
56
|
+
if (this._resolving) {
|
|
57
|
+
this._resolving.push(cb);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this._resolving = [cb];
|
|
61
|
+
this.X.require('shm', (err, ext) => {
|
|
62
|
+
// The provider-backed API (usable/createSegment) needs x11 >= 3.7; an
|
|
63
|
+
// older x11 still resolves the extension but without it, so treat that
|
|
64
|
+
// as "no shared memory" rather than throwing.
|
|
65
|
+
if (err || !ext || typeof ext.usable !== 'function') return this._resolved(false, false);
|
|
66
|
+
ext.usable((e, ok) => this._resolved(ok ? ext : false, !!ok));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_resolved(ext, ready) {
|
|
71
|
+
this.ext = ext;
|
|
72
|
+
this.ready = ready;
|
|
73
|
+
const cbs = this._resolving || [];
|
|
74
|
+
this._resolving = null;
|
|
75
|
+
for (const cb of cbs) cb(ready);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_acquire(bytes) {
|
|
79
|
+
for (let i = 0; i < this._pool.length; ++i) {
|
|
80
|
+
if (this._pool[i].size >= bytes) return this._pool.splice(i, 1)[0];
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_warm(bytes) {
|
|
86
|
+
if (this._warming || !this.ready) return;
|
|
87
|
+
this._warming = true;
|
|
88
|
+
this.ext.createSegment(roundUp(bytes), (err, seg) => {
|
|
89
|
+
this._warming = false;
|
|
90
|
+
if (!err && seg) this._pool.push(seg);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Upload `data` (server-layout pixels, `stride*height` bytes at offset 0) to
|
|
96
|
+
* `drawable` at `dstX,dstY` as a `depth` image `width*height`. Returns true
|
|
97
|
+
* when shared memory handled it (the caller must then issue nothing else),
|
|
98
|
+
* or false to signal "do it with core PutImage" — including every case where
|
|
99
|
+
* SHM is unavailable, too small, or momentarily out of free segments (a
|
|
100
|
+
* segment is warmed in the background for next time).
|
|
101
|
+
*/
|
|
102
|
+
putImage(drawable, gc, { width, height, depth, dstX = 0, dstY = 0, data }) {
|
|
103
|
+
const bytes = data.length;
|
|
104
|
+
if (!this.ready || bytes < MIN_UPLOAD_BYTES) return false;
|
|
105
|
+
const seg = this._acquire(bytes);
|
|
106
|
+
if (!seg) {
|
|
107
|
+
this._warm(bytes);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
data.copy ? data.copy(seg.buffer, 0, 0, bytes) : seg.buffer.set(data.subarray(0, bytes));
|
|
111
|
+
seg.putImage(drawable, gc, { width, height, depth, dstX, dstY, sendEvent: true });
|
|
112
|
+
// reuse only once the server is done reading it
|
|
113
|
+
seg.once('complete', () => this._pool.push(seg));
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Whether a readback of `bytes` should use shared memory (usable and large
|
|
119
|
+
* enough). The caller does the async segment dance through `getImage` below.
|
|
120
|
+
*/
|
|
121
|
+
wantsReadback(bytes) {
|
|
122
|
+
return this.ready && bytes >= MIN_READBACK_BYTES;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Read `width*height` pixels of `depth` from `drawable` at `x,y` through a
|
|
127
|
+
* shared segment. `cb(err, buffer)` — `buffer` is a view of the segment's
|
|
128
|
+
* bytes valid until the next `getImage`; copy out before then. Falls back is
|
|
129
|
+
* the caller's job: only call this when `wantsReadback` was true.
|
|
130
|
+
*/
|
|
131
|
+
getImage(drawable, x, y, width, height, depth, cb) {
|
|
132
|
+
const bytes = width * height * ((depth + 7) >> 3 === 3 ? 4 : (depth + 7) >> 3);
|
|
133
|
+
const use = (seg) => {
|
|
134
|
+
seg.getImage(drawable, x, y, width, height, 0xffffffff, undefined, 0, (err, rep) => {
|
|
135
|
+
if (err) {
|
|
136
|
+
this._pool.push(seg);
|
|
137
|
+
return cb(err);
|
|
138
|
+
}
|
|
139
|
+
// hand back the buffer, then reclaim the segment after the caller has
|
|
140
|
+
// synchronously copied out (they must, per the doc above)
|
|
141
|
+
cb(null, seg.buffer, rep);
|
|
142
|
+
this._pool.push(seg);
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
const seg = this._acquire(bytes);
|
|
146
|
+
if (seg) return use(seg);
|
|
147
|
+
this.ext.createSegment(roundUp(bytes), (err, s) => {
|
|
148
|
+
if (err || !s) return cb(err || new Error('shm: no segment'));
|
|
149
|
+
use(s);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Detach every pooled segment (called from App#close). */
|
|
154
|
+
dispose() {
|
|
155
|
+
const pool = this._pool;
|
|
156
|
+
this._pool = [];
|
|
157
|
+
this.ready = false;
|
|
158
|
+
for (const seg of pool) {
|
|
159
|
+
try {
|
|
160
|
+
seg.detach();
|
|
161
|
+
} catch {
|
|
162
|
+
/* connection may already be gone */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
package/lib/widgets/htmlview.js
CHANGED
|
@@ -105,8 +105,10 @@ export default class HtmlView {
|
|
|
105
105
|
if (this.window) {
|
|
106
106
|
this._ctx = this.window.getContext('2d');
|
|
107
107
|
this.window.on('expose', () => this.render());
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
// a move is a ConfigureNotify too; re-laying out a document per step
|
|
109
|
+
// of a window drag is exactly what ev.resized is for
|
|
110
|
+
this.window.on('resize', (ev) => {
|
|
111
|
+
if (ev.resized) this._layoutWidth = -1;
|
|
110
112
|
});
|
|
111
113
|
this.window.on('mousedown', (ev) => {
|
|
112
114
|
if (ev.keycode === 4 || ev.keycode === 5) {
|
|
@@ -74,8 +74,10 @@ export default class MarkdownView {
|
|
|
74
74
|
if (this.window) {
|
|
75
75
|
this._ctx = this.window.getContext('2d');
|
|
76
76
|
this.window.on('expose', () => this.render());
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
// a move is a ConfigureNotify too; re-laying out a document per step
|
|
78
|
+
// of a window drag is exactly what ev.resized is for
|
|
79
|
+
this.window.on('resize', (ev) => {
|
|
80
|
+
if (ev.resized) this._layoutWidth = -1; // invalidate
|
|
79
81
|
});
|
|
80
82
|
this.window.on('mousedown', (ev) => {
|
|
81
83
|
if (ev.keycode !== 1 || !this.onLink) return; // left button only
|
package/lib/window.js
CHANGED
|
@@ -386,6 +386,10 @@ export default class Window extends Drawable {
|
|
|
386
386
|
this.height = args.height ?? 800;
|
|
387
387
|
this.x = args.x ?? 0;
|
|
388
388
|
this.y = args.y ?? 0;
|
|
389
|
+
// the baseline 'resize' compares against (see _tagResize): what the
|
|
390
|
+
// window was asked to be, so the window manager's first ConfigureNotify
|
|
391
|
+
// reports only what it actually overrode
|
|
392
|
+
this._deliveredGeom = { x: this.x, y: this.y, width: this.width, height: this.height };
|
|
389
393
|
const values = {
|
|
390
394
|
// NorthWest bit gravity: keep the old content anchored during
|
|
391
395
|
// resize instead of discarding it (less flicker between the resize
|
|
@@ -419,6 +423,8 @@ export default class Window extends Drawable {
|
|
|
419
423
|
this.eventMask = 0;
|
|
420
424
|
this.visual = 0;
|
|
421
425
|
this.depth = 0;
|
|
426
|
+
// an adopted window's geometry is not known until GetGeometry replies
|
|
427
|
+
this._deliveredGeom = null;
|
|
422
428
|
// populate width and height
|
|
423
429
|
X.GetGeometry(this.id, (err, res) => {
|
|
424
430
|
if (!err) {
|
|
@@ -427,6 +433,10 @@ export default class Window extends Drawable {
|
|
|
427
433
|
this.x = res.xPos;
|
|
428
434
|
this.y = res.yPos;
|
|
429
435
|
this.depth = res.depth;
|
|
436
|
+
// nothing was known about this window until now, so until this
|
|
437
|
+
// reply lands a 'resize' reports both moved and resized (see
|
|
438
|
+
// _tagResize) rather than guessing
|
|
439
|
+
this._deliveredGeom ??= { x: this.x, y: this.y, width: this.width, height: this.height };
|
|
430
440
|
}
|
|
431
441
|
this._readyPromiseResolve();
|
|
432
442
|
});
|
|
@@ -523,6 +533,7 @@ export default class Window extends Drawable {
|
|
|
523
533
|
// deliver buffered state events before a discrete one so handlers see
|
|
524
534
|
// them in the order they happened (a drag sees the move, then the up)
|
|
525
535
|
this._flushCoalesced();
|
|
536
|
+
if (eventName === 'resize') this._tagResize(ntkev);
|
|
526
537
|
this.emit(eventName, ntkev);
|
|
527
538
|
// a WM_DELETE_WINDOW ClientMessage is the window manager *asking*, and
|
|
528
539
|
// 'close' is that question in a form an application can answer
|
|
@@ -628,8 +639,6 @@ export default class Window extends Drawable {
|
|
|
628
639
|
this._dirty = false;
|
|
629
640
|
this._backingValid = false;
|
|
630
641
|
this._presentScheduled = false;
|
|
631
|
-
this._backedW = this.width;
|
|
632
|
-
this._backedH = this.height;
|
|
633
642
|
|
|
634
643
|
this._presentGc = this.X.AllocID();
|
|
635
644
|
this.X.CreateGC(this._presentGc, this.id, { graphicsExposures: 0 });
|
|
@@ -642,10 +651,11 @@ export default class Window extends Drawable {
|
|
|
642
651
|
this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, () => {});
|
|
643
652
|
}
|
|
644
653
|
|
|
654
|
+
// a pure move is a ConfigureNotify too, and reallocating a backing
|
|
655
|
+
// pixmap per step of a window drag is the bug this flag exists to spare
|
|
656
|
+
// everyone (see _tagResize)
|
|
645
657
|
this.on('resize', (ev) => {
|
|
646
|
-
if (ev.
|
|
647
|
-
this._backedW = ev.width;
|
|
648
|
-
this._backedH = ev.height;
|
|
658
|
+
if (!ev.resized) return;
|
|
649
659
|
this._allocBacking(ev.width, ev.height);
|
|
650
660
|
this._backingValid = false;
|
|
651
661
|
this._requestRedraw();
|
|
@@ -875,6 +885,46 @@ export default class Window extends Drawable {
|
|
|
875
885
|
this._scheduleFrame();
|
|
876
886
|
}
|
|
877
887
|
|
|
888
|
+
/**
|
|
889
|
+
* Say what a 'resize' actually changed.
|
|
890
|
+
*
|
|
891
|
+
* 'resize' is ConfigureNotify, which fires for pure moves and for
|
|
892
|
+
* reparents as much as for size changes — under an opaque-move window
|
|
893
|
+
* manager a window drag is one per pointer step. Every consumer used to
|
|
894
|
+
* rediscover that by keeping its own copy of the last size (this library
|
|
895
|
+
* included, in the backing-store listener), and one that didn't paid a
|
|
896
|
+
* full relayout per step of a drag.
|
|
897
|
+
*
|
|
898
|
+
* So the delivered event says which it was:
|
|
899
|
+
*
|
|
900
|
+
* - `ev.resized` — the size differs from the last delivered event's
|
|
901
|
+
* - `ev.moved` — the position does
|
|
902
|
+
* - `ev.previous` — that event's `{x, y, width, height}`, for a delta;
|
|
903
|
+
* null on an adopted window whose geometry is not known yet, where
|
|
904
|
+
* both flags read true because nothing can be ruled out
|
|
905
|
+
*
|
|
906
|
+
* Measured against the last *delivered* event, not the last raw one, so
|
|
907
|
+
* that coalescing cannot swallow a change: a frame that merges two moves
|
|
908
|
+
* and a resize reports both, where per-hop flags would report only the
|
|
909
|
+
* final hop. The merged raw events in `ev.coalesced` are not tagged for
|
|
910
|
+
* that reason — the flags are a property of the delivery.
|
|
911
|
+
*
|
|
912
|
+
* Position is compared as reported. A reparenting window manager sends
|
|
913
|
+
* real ConfigureNotify in frame coordinates and synthetic ones in root
|
|
914
|
+
* coordinates (ICCCM 4.2.3), so a frame's own offset can read as a move
|
|
915
|
+
* on the first event after the switch. That is the safe direction —
|
|
916
|
+
* spurious work, never missed work — and the alternative is a
|
|
917
|
+
* TranslateCoordinates round trip per event, which is the cost this flag
|
|
918
|
+
* exists to avoid.
|
|
919
|
+
*/
|
|
920
|
+
_tagResize(ev) {
|
|
921
|
+
const prev = this._deliveredGeom;
|
|
922
|
+
ev.previous = prev;
|
|
923
|
+
ev.moved = !prev || ev.x !== prev.x || ev.y !== prev.y;
|
|
924
|
+
ev.resized = !prev || ev.width !== prev.width || ev.height !== prev.height;
|
|
925
|
+
this._deliveredGeom = { x: ev.x, y: ev.y, width: ev.width, height: ev.height };
|
|
926
|
+
}
|
|
927
|
+
|
|
878
928
|
_flushCoalesced() {
|
|
879
929
|
const pending = this._frame.pending;
|
|
880
930
|
if (!pending.size) return;
|
|
@@ -884,6 +934,7 @@ export default class Window extends Drawable {
|
|
|
884
934
|
const ev = pending.get(name);
|
|
885
935
|
if (ev) {
|
|
886
936
|
pending.delete(name);
|
|
937
|
+
if (name === 'resize') this._tagResize(ev);
|
|
887
938
|
this.emit(name, ev);
|
|
888
939
|
}
|
|
889
940
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntk",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
|
|
5
5
|
"author": "Andrey Sidorov <sidorares@yandex.ru>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"parse-color": "^1.0.0",
|
|
49
49
|
"pngjs": "^7.0.0",
|
|
50
50
|
"postcss": "^8.5.23",
|
|
51
|
-
"x11": "^3.
|
|
51
|
+
"x11": "^3.7.0",
|
|
52
52
|
"yoga-layout": "^3.2.1"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|