ntk 2.2.0 → 3.1.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 +156 -151
- package/lib/app.js +64 -21
- package/lib/cleanup.js +14 -0
- package/lib/drawable.js +12 -18
- package/lib/events_map.js +92 -98
- package/lib/fontconfig.js +123 -17
- package/lib/glyphset.js +40 -21
- package/lib/image.js +138 -0
- package/lib/index.js +129 -57
- package/lib/path.js +734 -0
- package/lib/picture.js +84 -41
- package/lib/pixmap.js +50 -39
- package/lib/rasterize.js +178 -0
- package/lib/renderingcontext_2d.js +1078 -695
- package/lib/renderingcontext_opengl.js +53 -46
- package/lib/renderingcontext_x11.js +59 -67
- package/lib/text/font.js +196 -0
- package/lib/text/fontmanager.js +233 -0
- package/lib/text/fontsource.js +179 -0
- package/lib/text/glyphs.js +456 -0
- package/lib/text/layout.js +356 -0
- package/lib/text/shape.js +141 -0
- package/lib/trapezoid.js +131 -0
- package/lib/widgets/css.js +676 -0
- package/lib/widgets/highlight.js +97 -0
- package/lib/widgets/htmlview.js +879 -0
- package/lib/widgets/markdown.js +148 -0
- package/lib/widgets/markdownview.js +487 -0
- package/lib/widgets/mermaid.js +720 -0
- package/lib/widgets/svgview.js +528 -0
- package/lib/widgets/tex.js +681 -0
- package/lib/window.js +691 -308
- package/package.json +60 -56
- package/examples/example-await.js +0 -23
- package/examples/example-co.js +0 -13
- package/examples/example.js +0 -14
- package/examples/fill-styles.js +0 -39
- package/examples/glclock.js +0 -100
- package/examples/glxpixmap.js +0 -12
- package/examples/gradient.js +0 -41
- package/examples/package.json +0 -17
- package/examples/simpletex.js +0 -72
- package/examples/teapot.js +0 -104
- package/examples/teapot.json +0 -6
- package/examples/text-rendering.js +0 -44
- package/lib/fontface.js +0 -103
- package/lib/widgets/webview.js +0 -33
package/lib/window.js
CHANGED
|
@@ -1,308 +1,691 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
this.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
1
|
+
import keysym from 'keysym';
|
|
2
|
+
import x11 from 'x11';
|
|
3
|
+
|
|
4
|
+
import { safeRelease } from './cleanup.js';
|
|
5
|
+
import Drawable from './drawable.js';
|
|
6
|
+
import Pixmap from './pixmap.js';
|
|
7
|
+
import * as xevents from './events_map.js';
|
|
8
|
+
|
|
9
|
+
// X window attributes forwarded verbatim from constructor args into the
|
|
10
|
+
// CreateWindow value list (node-x11 valueMask names, see x11
|
|
11
|
+
// lib/corereqs.js). Deliberately not forwarded:
|
|
12
|
+
// - eventMask: computed from the onXxx handler args (an explicit
|
|
13
|
+
// args.eventMask is OR-ed into the computed mask instead)
|
|
14
|
+
// - backingStore: ntk's `backingStore: false` creation option opts out of
|
|
15
|
+
// client-side double buffering (see docs/window.md) and predates this
|
|
16
|
+
// list — it is a different concept from the X backing-store attribute,
|
|
17
|
+
// which is therefore not forwarded to avoid a silent collision
|
|
18
|
+
const forwardedXAttributes = [
|
|
19
|
+
'backgroundPixmap',
|
|
20
|
+
'backgroundPixel',
|
|
21
|
+
'borderPixmap',
|
|
22
|
+
'borderPixel',
|
|
23
|
+
'bitGravity',
|
|
24
|
+
'winGravity',
|
|
25
|
+
'backingPlanes',
|
|
26
|
+
'backingPixel',
|
|
27
|
+
'overrideRedirect',
|
|
28
|
+
'saveUnder',
|
|
29
|
+
'doNotPropagateMask',
|
|
30
|
+
'colormap',
|
|
31
|
+
'cursor'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export default class Window extends Drawable {
|
|
35
|
+
// one wrapper per X window id and connection: constructing a Window for a
|
|
36
|
+
// known id returns the cached instance
|
|
37
|
+
static cache = new Map();
|
|
38
|
+
|
|
39
|
+
constructor(app, args = {}) {
|
|
40
|
+
if (args.id) {
|
|
41
|
+
const cached = Window.cache.get(args.id);
|
|
42
|
+
// TODO: check event mask, if more events in args - update it
|
|
43
|
+
if (cached) return cached;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
super();
|
|
47
|
+
|
|
48
|
+
const X = app.X;
|
|
49
|
+
this.X = X;
|
|
50
|
+
this.app = app;
|
|
51
|
+
this.display = app.display;
|
|
52
|
+
this._mapped = false;
|
|
53
|
+
this._destroyed = false;
|
|
54
|
+
this._titleSerial = 0;
|
|
55
|
+
this._readyPromiseResolve = null;
|
|
56
|
+
this._readyPromise = new Promise((resolve) => {
|
|
57
|
+
this._readyPromiseResolve = resolve;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const parentId = args.parent ? args.parent.id : app.display.screen[0].root;
|
|
61
|
+
|
|
62
|
+
// double buffering (see docs/window.md): windows we created get a
|
|
63
|
+
// backing pixmap when a 2d context is requested, unless opted out
|
|
64
|
+
this._foreign = !!args.id;
|
|
65
|
+
this._backingOptOut = args.backingStore === false;
|
|
66
|
+
this._backing = null;
|
|
67
|
+
this._dirty = false;
|
|
68
|
+
this._presentPending = false;
|
|
69
|
+
|
|
70
|
+
// frame clock state: coalesced events, synthetic redraws and
|
|
71
|
+
// requestAnimationFrame callbacks are delivered in paced frames
|
|
72
|
+
// (see docs/window.md "Frames, coalescing and slow connections")
|
|
73
|
+
this._coalesce = args.coalesceEvents !== false;
|
|
74
|
+
this._frameSyncEnabled = args.frameSync !== false;
|
|
75
|
+
this.frameInterval = args.frameInterval ?? 16;
|
|
76
|
+
this.frameLatency = null;
|
|
77
|
+
this._frame = {
|
|
78
|
+
pending: new Map(), // coalescible event name -> merged event
|
|
79
|
+
rafCbs: [],
|
|
80
|
+
rafId: 0,
|
|
81
|
+
inFlight: false, // fence round-trip awaiting the server's reply
|
|
82
|
+
timer: null,
|
|
83
|
+
scheduled: false,
|
|
84
|
+
needsRedraw: false
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (!args.id) {
|
|
88
|
+
this.id = X.AllocID();
|
|
89
|
+
this.eventMask = x11.eventMask.StructureNotify;
|
|
90
|
+
|
|
91
|
+
for (const ntkEventName in xevents.maskCamelCase) {
|
|
92
|
+
if (typeof args[ntkEventName] === 'function') {
|
|
93
|
+
this.eventMask |= xevents.maskCamelCase[ntkEventName];
|
|
94
|
+
this.on(xevents.toSnake[ntkEventName], args[ntkEventName]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof args.eventMask === 'number') {
|
|
99
|
+
this.eventMask |= args.eventMask;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.width = args.width ?? 800;
|
|
103
|
+
this.height = args.height ?? 800;
|
|
104
|
+
this.x = args.x ?? 0;
|
|
105
|
+
this.y = args.y ?? 0;
|
|
106
|
+
const values = {
|
|
107
|
+
// NorthWest bit gravity: keep the old content anchored during
|
|
108
|
+
// resize instead of discarding it (less flicker between the resize
|
|
109
|
+
// and our redraw)
|
|
110
|
+
bitGravity: 1,
|
|
111
|
+
eventMask: this.eventMask
|
|
112
|
+
};
|
|
113
|
+
for (const name of forwardedXAttributes) {
|
|
114
|
+
if (args[name] !== undefined) values[name] = args[name];
|
|
115
|
+
}
|
|
116
|
+
X.CreateWindow(this.id, parentId, this.x, this.y, this.width, this.height, 0, 0, 0, 0, values);
|
|
117
|
+
} else {
|
|
118
|
+
this.id = args.id;
|
|
119
|
+
this.eventMask = 0;
|
|
120
|
+
// populate width and height
|
|
121
|
+
X.GetGeometry(this.id, (err, res) => {
|
|
122
|
+
if (!err) {
|
|
123
|
+
this.width = res.width;
|
|
124
|
+
this.height = res.height;
|
|
125
|
+
this.x = res.xPos;
|
|
126
|
+
this.y = res.yPos;
|
|
127
|
+
}
|
|
128
|
+
this._readyPromiseResolve();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Window.cache.set(this.id, this);
|
|
133
|
+
|
|
134
|
+
if (args.title) {
|
|
135
|
+
this.setTitle(args.title);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
X.event_consumers[this.id] = this;
|
|
139
|
+
this.on('event', (ev) => {
|
|
140
|
+
const ntkev = ev; // todo: clone
|
|
141
|
+
ntkev.window = this;
|
|
142
|
+
ntkev.target = this;
|
|
143
|
+
const eventName = xevents.eventName[ev.type];
|
|
144
|
+
if (!eventName) return;
|
|
145
|
+
// geometry bookkeeping happens on the raw stream so wnd.x/y/width/height
|
|
146
|
+
// are current even while a user-facing 'resize' event sits coalesced
|
|
147
|
+
if (eventName === 'resize') {
|
|
148
|
+
this.width = ev.width;
|
|
149
|
+
this.height = ev.height;
|
|
150
|
+
this.x = ev.x;
|
|
151
|
+
this.y = ev.y;
|
|
152
|
+
}
|
|
153
|
+
// double-buffered windows serve expose events from the backing pixmap
|
|
154
|
+
// — user code is only asked to redraw when the content is invalid
|
|
155
|
+
if (eventName === 'expose' && this._backing) {
|
|
156
|
+
this._handleExpose(ev);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// TODO cleanup
|
|
160
|
+
// also it seems that in keysym module not all keys have valid unicode values
|
|
161
|
+
if (eventName === 'keydown') {
|
|
162
|
+
const evKeysym = X.keycode2keysyms[ev.keycode];
|
|
163
|
+
const shift = ev.buttons & 1;
|
|
164
|
+
const capsLock = ev.buttons & 2;
|
|
165
|
+
const capital = (capsLock && !shift) || (shift && !capsLock);
|
|
166
|
+
const symInd = capital ? 1 : 0; // TODO: AltGr & other modifiers to use syms 2, 3, 4 etc
|
|
167
|
+
const entry = evKeysym && keysym.fromKeysym(evKeysym[symInd]);
|
|
168
|
+
if (entry) {
|
|
169
|
+
ev.codepoint = entry.unicode;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (this._coalesce && xevents.coalesce[eventName]) {
|
|
173
|
+
this._enqueueCoalesced(eventName, ntkev);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// deliver buffered state events before a discrete one so handlers see
|
|
177
|
+
// them in the order they happened (a drag sees the move, then the up)
|
|
178
|
+
this._flushCoalesced();
|
|
179
|
+
this.emit(eventName, ntkev);
|
|
180
|
+
// anything drawn during the handlers becomes visible in one blit
|
|
181
|
+
if (this._dirty) this._present();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
this.on('child-event', (ev) => {
|
|
185
|
+
const eventName = xevents.eventName[ev.type];
|
|
186
|
+
const ntkev = {
|
|
187
|
+
parent: this,
|
|
188
|
+
window: new Window(app, { id: ev.wid })
|
|
189
|
+
};
|
|
190
|
+
// wait until we know that we track correct x,y,w,h values
|
|
191
|
+
ntkev.window._readyPromise.then(() => {
|
|
192
|
+
this.emit(eventName, ntkev);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.on('newListener', (name) => {
|
|
197
|
+
// extend the server-side event mask if this event needs it
|
|
198
|
+
const eventMask = xevents.mask[name];
|
|
199
|
+
if (!eventMask) return;
|
|
200
|
+
if ((eventMask & this.eventMask) === 0) {
|
|
201
|
+
this.eventMask |= eventMask;
|
|
202
|
+
X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, () => {});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
this.on('map', () => (this._mapped = true));
|
|
207
|
+
this.on('unmap', () => (this._mapped = false));
|
|
208
|
+
this.on('destroy', () => {
|
|
209
|
+
this._destroyed = true;
|
|
210
|
+
this._forget();
|
|
211
|
+
this._teardownFrame();
|
|
212
|
+
// the server frees window-backed pictures with the window — let
|
|
213
|
+
// contexts drop their wrappers without issuing FreePicture
|
|
214
|
+
this.emit('_destroyed');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (typeof this.width !== 'undefined') {
|
|
218
|
+
this._readyPromiseResolve();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_forget() {
|
|
223
|
+
Window.cache.delete(this.id);
|
|
224
|
+
delete this.X.event_consumers[this.id];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 2d contexts on windows draw into a backing pixmap by default and blit to
|
|
229
|
+
* the window in single CopyArea requests (after event handlers return, and
|
|
230
|
+
* coalesced per event-loop tick otherwise) — no clear-then-draw flicker.
|
|
231
|
+
* Expose events are served from the pixmap without invoking user handlers;
|
|
232
|
+
* an 'expose' (and 'draw') event is emitted only when a real redraw is
|
|
233
|
+
* needed (first paint, resize). Opt out with
|
|
234
|
+
* `createWindow({ backingStore: false })`.
|
|
235
|
+
*/
|
|
236
|
+
getContext(name, ...args) {
|
|
237
|
+
if (name === '2d' && !this._backing && !this._backingOptOut && !this._foreign) {
|
|
238
|
+
this._enableBackingStore();
|
|
239
|
+
}
|
|
240
|
+
return super.getContext(name, ...args);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_enableBackingStore() {
|
|
244
|
+
this._dirty = false;
|
|
245
|
+
this._backingValid = false;
|
|
246
|
+
this._presentScheduled = false;
|
|
247
|
+
this._backedW = this.width;
|
|
248
|
+
this._backedH = this.height;
|
|
249
|
+
|
|
250
|
+
this._presentGc = this.X.AllocID();
|
|
251
|
+
this.X.CreateGC(this._presentGc, this.id, { graphicsExposures: 0 });
|
|
252
|
+
this._allocBacking(this.width, this.height);
|
|
253
|
+
|
|
254
|
+
// the redraw cycle is driven by Expose interception — make sure the
|
|
255
|
+
// server sends them even if user code only listens to 'draw'
|
|
256
|
+
if (!(this.eventMask & x11.eventMask.Exposure)) {
|
|
257
|
+
this.eventMask |= x11.eventMask.Exposure;
|
|
258
|
+
this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, () => {});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.on('resize', (ev) => {
|
|
262
|
+
if (ev.width === this._backedW && ev.height === this._backedH) return;
|
|
263
|
+
this._backedW = ev.width;
|
|
264
|
+
this._backedH = ev.height;
|
|
265
|
+
this._allocBacking(ev.width, ev.height);
|
|
266
|
+
this._backingValid = false;
|
|
267
|
+
this._requestRedraw();
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// grow-only backing pixmap (re)allocation; new area is cleared to white
|
|
272
|
+
_allocBacking(w, h) {
|
|
273
|
+
const cur = this._backing;
|
|
274
|
+
if (cur && cur.width >= w && cur.height >= h) return;
|
|
275
|
+
const newW = Math.max(w, cur ? cur.width : 0);
|
|
276
|
+
const newH = Math.max(h, cur ? cur.height : 0);
|
|
277
|
+
const pixmap = new Pixmap(this.app, {
|
|
278
|
+
parent: this,
|
|
279
|
+
width: newW,
|
|
280
|
+
height: newH,
|
|
281
|
+
depth: this.display.screen[0].root_depth
|
|
282
|
+
});
|
|
283
|
+
if (!this._clearGc) {
|
|
284
|
+
// reusable across reallocs: a GC is valid for any drawable of the
|
|
285
|
+
// same screen and depth (node-x11 has no FreeGC request)
|
|
286
|
+
this._clearGc = this.X.AllocID();
|
|
287
|
+
this.X.CreateGC(this._clearGc, pixmap.id, {
|
|
288
|
+
foreground: this.display.screen[0].white_pixel,
|
|
289
|
+
graphicsExposures: 0
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
this.X.PolyFillRectangle(pixmap.id, this._clearGc, [0, 0, newW, newH]);
|
|
293
|
+
if (cur) cur.destroy();
|
|
294
|
+
this._backing = pixmap;
|
|
295
|
+
this._backingValid = false;
|
|
296
|
+
// contexts re-bind their pictures to the new pixmap
|
|
297
|
+
this.emit('_backing');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_handleExpose(ev) {
|
|
301
|
+
if (this._backingValid) {
|
|
302
|
+
this.X.CopyArea(this._backing.id, this.id, this._presentGc, ev.x, ev.y, ev.x, ev.y, ev.width, ev.height);
|
|
303
|
+
} else {
|
|
304
|
+
this._requestRedraw();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ask user code to repaint (full window), on the next paced frame
|
|
309
|
+
_requestRedraw() {
|
|
310
|
+
this._frame.needsRedraw = true;
|
|
311
|
+
this._scheduleFrame();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// called by rendering contexts after each drawing operation
|
|
315
|
+
_markDirty() {
|
|
316
|
+
if (!this._backing) return;
|
|
317
|
+
this._dirty = true;
|
|
318
|
+
this._backingValid = true;
|
|
319
|
+
if (this._presentScheduled) return;
|
|
320
|
+
// coalesce draws that happen outside event handlers into one blit
|
|
321
|
+
this._presentScheduled = true;
|
|
322
|
+
setImmediate(() => {
|
|
323
|
+
this._presentScheduled = false;
|
|
324
|
+
if (this._dirty) this._present();
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/*
|
|
329
|
+
* Frame clock. Noisy events (see events_map coalesce), synthetic redraws
|
|
330
|
+
* and requestAnimationFrame callbacks are delivered in "frames", paced by
|
|
331
|
+
* two independent gates:
|
|
332
|
+
*
|
|
333
|
+
* - a fence: a cheap request with a reply (GetInputFocus) sent after each
|
|
334
|
+
* frame; X processes requests in order, so its reply confirms the
|
|
335
|
+
* server consumed everything the frame drew. At most one fence is in
|
|
336
|
+
* flight — on a slow connection frames degrade to one per round-trip
|
|
337
|
+
* instead of queueing a trail of stale updates.
|
|
338
|
+
* - a timer: at most one paced frame per `frameInterval` ms, so a fast
|
|
339
|
+
* local server doesn't get redraws at input-device rate.
|
|
340
|
+
*
|
|
341
|
+
* Discrete events (mousedown, keydown, ...) bypass the timer for latency,
|
|
342
|
+
* but their blits still respect the fence via _present().
|
|
343
|
+
*/
|
|
344
|
+
_scheduleFrame() {
|
|
345
|
+
const f = this._frame;
|
|
346
|
+
if (f.scheduled || f.inFlight || f.timer) return;
|
|
347
|
+
f.scheduled = true;
|
|
348
|
+
setImmediate(() => {
|
|
349
|
+
f.scheduled = false;
|
|
350
|
+
// gates may have been armed after this got scheduled (work queued
|
|
351
|
+
// from inside a running frame, e.g. a rAF loop re-registering) —
|
|
352
|
+
// the fence reply / timer expiry will reschedule then
|
|
353
|
+
if (f.inFlight || f.timer) return;
|
|
354
|
+
this._runFrame();
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_runFrame() {
|
|
359
|
+
const f = this._frame;
|
|
360
|
+
if (!f.pending.size && !f.rafCbs.length && !f.needsRedraw && !this._dirty && !this._presentPending) return;
|
|
361
|
+
this._flushCoalesced();
|
|
362
|
+
if (f.needsRedraw) {
|
|
363
|
+
f.needsRedraw = false;
|
|
364
|
+
const ev = {
|
|
365
|
+
x: 0,
|
|
366
|
+
y: 0,
|
|
367
|
+
width: this.width,
|
|
368
|
+
height: this.height,
|
|
369
|
+
synthetic: true,
|
|
370
|
+
window: this,
|
|
371
|
+
target: this
|
|
372
|
+
};
|
|
373
|
+
this.emit('draw', ev);
|
|
374
|
+
this.emit('expose', ev);
|
|
375
|
+
}
|
|
376
|
+
if (f.rafCbs.length) {
|
|
377
|
+
const cbs = f.rafCbs;
|
|
378
|
+
f.rafCbs = [];
|
|
379
|
+
const now = performance.now();
|
|
380
|
+
for (const entry of cbs) entry.cb(now);
|
|
381
|
+
}
|
|
382
|
+
if (this._dirty || this._presentPending) this._presentNow();
|
|
383
|
+
this._armFence();
|
|
384
|
+
this._armTimer();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
_enqueueCoalesced(name, ev) {
|
|
388
|
+
const pending = this._frame.pending;
|
|
389
|
+
const prev = pending.get(name);
|
|
390
|
+
if (!prev) {
|
|
391
|
+
ev.coalesced = [ev];
|
|
392
|
+
if (xevents.coalesce[name] === 'union') {
|
|
393
|
+
ev.rects = [{ x: ev.x, y: ev.y, width: ev.width, height: ev.height }];
|
|
394
|
+
}
|
|
395
|
+
pending.set(name, ev);
|
|
396
|
+
} else if (xevents.coalesce[name] === 'union') {
|
|
397
|
+
prev.coalesced.push(ev);
|
|
398
|
+
prev.rects.push({ x: ev.x, y: ev.y, width: ev.width, height: ev.height });
|
|
399
|
+
const x2 = Math.max(prev.x + prev.width, ev.x + ev.width);
|
|
400
|
+
const y2 = Math.max(prev.y + prev.height, ev.y + ev.height);
|
|
401
|
+
prev.x = Math.min(prev.x, ev.x);
|
|
402
|
+
prev.y = Math.min(prev.y, ev.y);
|
|
403
|
+
prev.width = x2 - prev.x;
|
|
404
|
+
prev.height = y2 - prev.y;
|
|
405
|
+
} else {
|
|
406
|
+
// keep-last: the newest event wins, older ones ride along
|
|
407
|
+
ev.coalesced = prev.coalesced;
|
|
408
|
+
ev.coalesced.push(ev);
|
|
409
|
+
pending.set(name, ev);
|
|
410
|
+
}
|
|
411
|
+
this._scheduleFrame();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
_flushCoalesced() {
|
|
415
|
+
const pending = this._frame.pending;
|
|
416
|
+
if (!pending.size) return;
|
|
417
|
+
this._frame.pending = new Map();
|
|
418
|
+
// resize first: geometry-dependent handlers (re-layout) run before repaints
|
|
419
|
+
for (const name of ['resize', 'expose']) {
|
|
420
|
+
const ev = pending.get(name);
|
|
421
|
+
if (ev) {
|
|
422
|
+
pending.delete(name);
|
|
423
|
+
this.emit(name, ev);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
for (const [name, ev] of pending) this.emit(name, ev);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_armFence() {
|
|
430
|
+
if (!this._frameSyncEnabled) return;
|
|
431
|
+
const f = this._frame;
|
|
432
|
+
if (f.inFlight) return;
|
|
433
|
+
const start = performance.now();
|
|
434
|
+
safeRelease(this.X, () => {
|
|
435
|
+
f.inFlight = true;
|
|
436
|
+
this.X.GetInputFocus(() => {
|
|
437
|
+
f.inFlight = false;
|
|
438
|
+
this.frameLatency = performance.now() - start;
|
|
439
|
+
if (this._presentPending) {
|
|
440
|
+
// a blit deferred while the fence was in flight: show it now
|
|
441
|
+
this._presentNow();
|
|
442
|
+
this._armFence();
|
|
443
|
+
}
|
|
444
|
+
if ((f.pending.size || f.rafCbs.length || f.needsRedraw) && !f.timer) this._scheduleFrame();
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
_armTimer() {
|
|
450
|
+
if (!(this.frameInterval > 0)) return;
|
|
451
|
+
const f = this._frame;
|
|
452
|
+
if (f.timer) return;
|
|
453
|
+
f.timer = setTimeout(() => {
|
|
454
|
+
f.timer = null;
|
|
455
|
+
if (f.pending.size || f.rafCbs.length || f.needsRedraw) this._scheduleFrame();
|
|
456
|
+
}, this.frameInterval);
|
|
457
|
+
if (typeof f.timer.unref === 'function') f.timer.unref();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
_teardownFrame() {
|
|
461
|
+
const f = this._frame;
|
|
462
|
+
if (f.timer) {
|
|
463
|
+
clearTimeout(f.timer);
|
|
464
|
+
f.timer = null;
|
|
465
|
+
}
|
|
466
|
+
f.pending.clear();
|
|
467
|
+
f.rafCbs = [];
|
|
468
|
+
f.needsRedraw = false;
|
|
469
|
+
this._presentPending = false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* DOM-style requestAnimationFrame: `cb(now)` runs on this window's next
|
|
474
|
+
* paced frame — at most once per `frameInterval` ms and with at most one
|
|
475
|
+
* frame's requests unacknowledged by the server, so animation loops adapt
|
|
476
|
+
* to connection latency instead of flooding a slow link.
|
|
477
|
+
* Returns an id for cancelAnimationFrame().
|
|
478
|
+
*/
|
|
479
|
+
requestAnimationFrame(cb) {
|
|
480
|
+
const f = this._frame;
|
|
481
|
+
const id = ++f.rafId;
|
|
482
|
+
f.rafCbs.push({ id, cb });
|
|
483
|
+
this._scheduleFrame();
|
|
484
|
+
return id;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
cancelAnimationFrame(id) {
|
|
488
|
+
const f = this._frame;
|
|
489
|
+
const idx = f.rafCbs.findIndex((entry) => entry.id === id);
|
|
490
|
+
if (idx !== -1) f.rafCbs.splice(idx, 1);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
_present() {
|
|
494
|
+
if (!this._backing) return;
|
|
495
|
+
if (this._frame.inFlight) {
|
|
496
|
+
// the server hasn't confirmed the previous frame yet — defer the blit
|
|
497
|
+
this._presentPending = true;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this._presentNow();
|
|
501
|
+
this._armFence();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
_presentNow() {
|
|
505
|
+
this._presentPending = false;
|
|
506
|
+
if (!this._backing) return;
|
|
507
|
+
this._dirty = false;
|
|
508
|
+
const w = Math.min(this.width, this._backing.width);
|
|
509
|
+
const h = Math.min(this.height, this._backing.height);
|
|
510
|
+
// a paced frame can fire after the connection started closing
|
|
511
|
+
safeRelease(this.X, () => {
|
|
512
|
+
this.X.CopyArea(this._backing.id, this.id, this._presentGc, 0, 0, 0, 0, w, h);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
map() {
|
|
517
|
+
this.X.MapWindow(this.id);
|
|
518
|
+
return this;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
unmap() {
|
|
522
|
+
this.X.UnmapWindow(this.id);
|
|
523
|
+
return this;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
resize(w, h) {
|
|
527
|
+
this.X.ResizeWindow(this.id, w, h);
|
|
528
|
+
return this;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
move(x, y) {
|
|
532
|
+
this.X.MoveWindow(this.id, x, y);
|
|
533
|
+
return this;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
moveResize(x, y, w, h) {
|
|
537
|
+
this.X.MoveResizeWindow(this.id, x, y, w, h);
|
|
538
|
+
return this;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
setState(newState) {
|
|
542
|
+
if (newState.visible && !this._mapped) {
|
|
543
|
+
this.map();
|
|
544
|
+
}
|
|
545
|
+
if (newState.visible === false && this._mapped) {
|
|
546
|
+
this.unmap();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const configureProps = {};
|
|
550
|
+
let needConfigureWindow = false;
|
|
551
|
+
if (newState.x !== undefined && this.x !== newState.x) {
|
|
552
|
+
needConfigureWindow = true;
|
|
553
|
+
configureProps.x = newState.x;
|
|
554
|
+
}
|
|
555
|
+
if (newState.y !== undefined && this.y !== newState.y) {
|
|
556
|
+
needConfigureWindow = true;
|
|
557
|
+
configureProps.y = newState.y;
|
|
558
|
+
}
|
|
559
|
+
if (newState.width !== undefined && this.width !== newState.width) {
|
|
560
|
+
needConfigureWindow = true;
|
|
561
|
+
configureProps.width = newState.width;
|
|
562
|
+
}
|
|
563
|
+
if (newState.height !== undefined && this.height !== newState.height) {
|
|
564
|
+
needConfigureWindow = true;
|
|
565
|
+
configureProps.height = newState.height;
|
|
566
|
+
}
|
|
567
|
+
if (needConfigureWindow) {
|
|
568
|
+
this.X.ConfigureWindow(this.id, configureProps);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// TODO: map other window attributes ( border, gravity, cursor etc )
|
|
572
|
+
// via ChangeWindowAttributes
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
setTitle(title) {
|
|
576
|
+
const X = this.X;
|
|
577
|
+
const wid = this.id;
|
|
578
|
+
// legacy WM_NAME is latin-1 only (node-x11 encodes plain strings as
|
|
579
|
+
// latin1) — kept for old window managers
|
|
580
|
+
X.ChangeProperty(0, wid, X.atoms.WM_NAME, X.atoms.STRING, 8, title);
|
|
581
|
+
// modern WMs prefer the EWMH _NET_WM_NAME property, which is UTF-8;
|
|
582
|
+
// interned atoms are cached by node-x11, so this round-trips only once.
|
|
583
|
+
// The lookups are async: by the time they resolve the window may be
|
|
584
|
+
// destroyed (ChangeProperty would BadWindow) or retitled (an older
|
|
585
|
+
// write would clobber a newer one) — the serial/destroyed guards drop
|
|
586
|
+
// the stale write in both cases.
|
|
587
|
+
const serial = ++this._titleSerial;
|
|
588
|
+
X.InternAtom(false, '_NET_WM_NAME', (err, netWmName) => {
|
|
589
|
+
X.InternAtom(false, 'UTF8_STRING', (err2, utf8String) => {
|
|
590
|
+
if (err || err2 || this._destroyed || serial !== this._titleSerial) return;
|
|
591
|
+
safeRelease(X, () => {
|
|
592
|
+
X.ChangeProperty(0, wid, netWmName, utf8String, 8, Buffer.from(title, 'utf8'));
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
return this;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
setMouseHintOnly(isOn) {
|
|
600
|
+
if (isOn && !(this.eventMask & x11.eventMask.PointerMotionHint)) {
|
|
601
|
+
this.eventMask |= x11.eventMask.PointerMotionHint;
|
|
602
|
+
} else if (!isOn && this.eventMask & x11.eventMask.PointerMotionHint) {
|
|
603
|
+
this.eventMask &= ~x11.eventMask.PointerMotionHint;
|
|
604
|
+
} else {
|
|
605
|
+
return this;
|
|
606
|
+
}
|
|
607
|
+
this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, () => {});
|
|
608
|
+
return this;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
queryPointer(callback) {
|
|
612
|
+
this.X.QueryPointer(this.id, callback);
|
|
613
|
+
return this;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
grabPointer() {
|
|
617
|
+
this.X.GrabPointer(this.id, true, x11.eventMask.PointerMotion, 0, 1, 0, 0, 0);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
reparentTo(newParent, x, y) {
|
|
621
|
+
this.X.ReparentWindow(this.id, newParent.id, x, y);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
queryTree(callback) {
|
|
625
|
+
const app = this.app;
|
|
626
|
+
this.X.QueryTree(this.id, (err, tree) => {
|
|
627
|
+
if (err) return callback(err);
|
|
628
|
+
const children = tree.children.map((id) => new Window(app, { id }));
|
|
629
|
+
const parent = new Window(app, { id: tree.parent });
|
|
630
|
+
// note that this root may be different from app.rootWindow() because
|
|
631
|
+
// there can be multiple screens (and roots)
|
|
632
|
+
const root = new Window(app, { id: tree.root });
|
|
633
|
+
const readyPromises = [...children, parent, root].map((w) => w._readyPromise);
|
|
634
|
+
Promise.all(readyPromises).then(() => {
|
|
635
|
+
callback(null, { parent, root, children });
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
return this;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
createPixmap(params = {}) {
|
|
642
|
+
return new Pixmap(this.app, {
|
|
643
|
+
parent: this,
|
|
644
|
+
width: this.width,
|
|
645
|
+
height: this.height,
|
|
646
|
+
// TODO: use window depth
|
|
647
|
+
depth: 32,
|
|
648
|
+
...params
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
createWindow(params = {}) {
|
|
653
|
+
return new Window(this.app, { parent: this, ...params });
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// opt in to the WM_DELETE_WINDOW protocol: the window manager sends a
|
|
657
|
+
// 'message' event instead of killing the connection when the user closes
|
|
658
|
+
// the window
|
|
659
|
+
setActions() {
|
|
660
|
+
const X = this.X;
|
|
661
|
+
const wid = this.id;
|
|
662
|
+
X.InternAtom(false, 'WM_PROTOCOLS', (err, WM_PROTOCOLS) => {
|
|
663
|
+
X.InternAtom(false, 'WM_DELETE_WINDOW', (err2, WM_DELETE_WINDOW) => {
|
|
664
|
+
if (err || err2) return;
|
|
665
|
+
const data = Buffer.alloc(4);
|
|
666
|
+
data.writeUInt32LE(WM_DELETE_WINDOW, 0);
|
|
667
|
+
X.ChangeProperty(0, wid, WM_PROTOCOLS, X.atoms.ATOM, 32, data);
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
destroy() {
|
|
673
|
+
this._destroyed = true;
|
|
674
|
+
this._forget();
|
|
675
|
+
this._teardownFrame();
|
|
676
|
+
this.emit('_destroyed');
|
|
677
|
+
if (this._backing) {
|
|
678
|
+
this._backing.destroy();
|
|
679
|
+
this._backing = null;
|
|
680
|
+
}
|
|
681
|
+
safeRelease(this.X, () => this.X.DestroyWindow(this.id));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
[Symbol.dispose]() {
|
|
685
|
+
this.destroy();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
inspect() {
|
|
689
|
+
return `[Window ${this.id}]`;
|
|
690
|
+
}
|
|
691
|
+
}
|