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