ntk 2.2.0 → 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 (47) hide show
  1. package/README.md +156 -151
  2. package/lib/app.js +54 -21
  3. package/lib/cleanup.js +14 -0
  4. package/lib/drawable.js +12 -18
  5. package/lib/events_map.js +92 -98
  6. package/lib/fontconfig.js +123 -17
  7. package/lib/glyphset.js +40 -21
  8. package/lib/image.js +138 -0
  9. package/lib/index.js +123 -57
  10. package/lib/path.js +734 -0
  11. package/lib/picture.js +84 -41
  12. package/lib/pixmap.js +50 -39
  13. package/lib/rasterize.js +178 -0
  14. package/lib/renderingcontext_2d.js +1078 -695
  15. package/lib/renderingcontext_opengl.js +53 -46
  16. package/lib/renderingcontext_x11.js +59 -67
  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 +642 -308
  33. package/package.json +60 -56
  34. package/examples/example-await.js +0 -23
  35. package/examples/example-co.js +0 -13
  36. package/examples/example.js +0 -14
  37. package/examples/fill-styles.js +0 -39
  38. package/examples/glclock.js +0 -100
  39. package/examples/glxpixmap.js +0 -12
  40. package/examples/gradient.js +0 -41
  41. package/examples/package.json +0 -17
  42. package/examples/simpletex.js +0 -72
  43. package/examples/teapot.js +0 -104
  44. package/examples/teapot.json +0 -6
  45. package/examples/text-rendering.js +0 -44
  46. package/lib/fontface.js +0 -103
  47. package/lib/widgets/webview.js +0 -33
package/lib/window.js CHANGED
@@ -1,308 +1,642 @@
1
- var util = require('util');
2
- var Drawable = require('./drawable');
3
- var xevents = require('./events_map');
4
- var x11 = require('x11');
5
-
6
- module.exports = Window;
7
-
8
- function Window(app, args) {
9
- if (!args) args = {};
10
-
11
- if (args.id) {
12
- var cached = Window.cache[args.id];
13
- if (cached) {
14
- // TODO: check event mask, if more events in args - update it
15
- return cached;
16
- }
17
- }
18
-
19
- if (this.constructor !== Window) return new Window(app, args);
20
-
21
- Drawable.call(this);
22
-
23
- // shortcuts
24
- var X = app.X;
25
- this.X = X;
26
- this.app = app;
27
- var display = app.display;
28
- this.display = display;
29
- this._readyPromiseResolve = null;
30
- this._readyPromiseReject = null;
31
- var self = this;
32
- this._readyPromise = new Promise(function(resolve, reject) {
33
- self._readyPromiseResolve = resolve;
34
- self._readyPromiseReject = reject;
35
- });
36
-
37
- var parent, parentId;
38
-
39
- if (!args.parent) parentId = display.screen[0].root;
40
- else {
41
- parentId = args.parent.id;
42
- }
43
-
44
- if (!args.id) {
45
- this.id = X.AllocID();
46
- this.eventMask = x11.eventMask.StructureNotify;
47
-
48
- for (ntkEventName in xevents.maskCamelCase) {
49
- if (typeof args[ntkEventName] == 'function') {
50
- this.eventMask = this.eventMask | xevents.maskCamelCase[ntkEventName];
51
- this.on(xevents.toSnake[ntkEventName], args[ntkEventName]);
52
- }
53
- }
54
-
55
- this.width = args.width ? args.width : 800;
56
- this.height = args.height ? args.height : 800;
57
- this.x = args.x ? args.x : 0;
58
- this.y = args.y ? args.y : 0;
59
- X.CreateWindow(this.id, parentId, this.x, this.y, this.width, this.height, 0, 0, 0, 0, {
60
- eventMask: this.eventMask
61
- });
62
- } else {
63
- this.id = args.id;
64
- // populate width and height
65
- X.GetGeometry(this.id, function(err, res) {
66
- self.width = res.width;
67
- self.height = res.height;
68
- self.x = res.xPos;
69
- self.y = res.yPos;
70
- self._readyPromiseResolve();
71
- });
72
- }
73
-
74
- Window.cache[this.id] = this;
75
-
76
- if (args.title) {
77
- this.setTitle(args.title);
78
- }
79
-
80
- X.event_consumers[this.id] = this;
81
- this.on('event', function(ev) {
82
- var ntkev = ev; // todo: clone
83
- ntkev.window = this;
84
- ntkev.target = this;
85
- var eventName = xevents.eventName[ev.type];
86
- if (eventName) {
87
- // TODO cleanup
88
- // also it seems that in keysym module not all keys have valid unicode values
89
- if (eventName == 'keydown') {
90
- var evKeysym = X.keycode2keysyms[ev.keycode];
91
- var shift = ev.buttons & 1;
92
- var capsLock = ev.buttons & 2;
93
- var capital = (capsLock && !shift) || (shift && !capsLock);
94
- var keysym = require('keysym');
95
- var symInd = capital ? 1 : 0; // TODO: AltGr & other modifiers to use syms 2, 3, 4 etc
96
- var entry = keysym.fromKeysym(evKeysym[symInd]);
97
- if (entry) {
98
- ev.codepoint = entry.unicode;
99
- }
100
- }
101
- this.emit(eventName, ntkev);
102
- } else {
103
- console.log('Unknown event: ', ev.type, ev.name);
104
- }
105
- });
106
- this.on('child-event', function(ev) {
107
- var ntkev = {};
108
- var eventName = xevents.eventName[ev.type];
109
- ntkev.parent = this;
110
- ntkev.window = new Window(app, { id: ev.wid });
111
- // wait until we know that we track correct x,y,w,h values
112
- ntkev.window._readyPromise.then(function() {
113
- self.emit(eventName, ntkev);
114
- });
115
- });
116
-
117
- this.on('newListener', function(name) {
118
- // check eventMask and add handler if missing
119
- var eventMask = xevents.mask[name];
120
- if ((eventMask & self.eventMask) == 0) {
121
- self.eventMask |= eventMask;
122
- X.ChangeWindowAttributes(self.id, { eventMask: this.eventMask }, function() {});
123
- }
124
- });
125
-
126
- this.on('resize', function(ev) {
127
- self.width = ev.width;
128
- self.height = ev.height;
129
- self.x = ev.x;
130
- self.y = ev.y;
131
- });
132
-
133
- this.on('map', () => (this._mapped = true));
134
- this.on('unmap', () => (this._mapped = false));
135
-
136
- if (typeof this.width !== 'undefined') {
137
- this._readyPromiseResolve();
138
- }
139
- }
140
- util.inherits(Window, Drawable);
141
-
142
- Window.cache = [];
143
-
144
- Window.prototype.map = function() {
145
- this.X.MapWindow(this.id);
146
- return this;
147
- };
148
-
149
- Window.prototype.unmap = function() {
150
- this.X.UnmapWindow(this.id);
151
- return this;
152
- };
153
-
154
- Window.prototype.resize = function(w, h) {
155
- this.X.ResizeWindow(this.id, w, h);
156
- return this;
157
- };
158
-
159
- const changeAttributesPropNames =
160
- 'backgroundPixmap backgroundPixel borderPixmap borderPixel bitGravity winGravity backingStore backingPlanes backingPixel overrideRedirect saveUnder eventMask doNotPropagateMask colormap cursor';
161
-
162
- Window.prototype.setState = function(newState) {
163
- if (!this._mapped && newState.visible) {
164
- this.map();
165
- }
166
- if (newState.visible === false && this._mapped) {
167
- this.unmap();
168
- }
169
-
170
- // 'x y width height borderWidth stackMode sibling'
171
- const configureProps = {};
172
- let needConfigureWindow = false;
173
- if (newState.x !== void 0 && this.x !== newState.x) {
174
- needConfigureWindow = true;
175
- configureProps.x = newState.x;
176
- }
177
- if (newState.y !== void 0 && this.y !== newState.y) {
178
- needConfigureWindow = true;
179
- configureProps.y = newState.y;
180
- }
181
- if (newState.width !== void 0 && this.width !== newState.width) {
182
- needConfigureWindow = true;
183
- configureProps.width = newState.width;
184
- }
185
- if (newState.height !== void 0 && this.height !== newState.height) {
186
- needConfigureWindow = true;
187
- newState.height = configureProps.height;
188
- }
189
- if (needConfigureWindow) {
190
- console.log(configureProps);
191
- this.X.ConfigureWindow(this.id, configureProps);
192
- }
193
-
194
- //const changeAttributesProps = {};
195
- //let needChangeAttributes = false;
196
- //this.X.ChangeWindowAttributes(this.id, changeAttributesProps);
197
- // todo others
198
- };
199
-
200
- Window.prototype.inspect = function() {
201
- return `[Window ${this.id}]`;
202
- };
203
-
204
- Window.prototype.move = function(x, y) {
205
- this.X.MoveWindow(this.id, x, y);
206
- return this;
207
- };
208
-
209
- Window.prototype.moveResize = function(x, y, w, h) {
210
- this.X.MoveResizeWindow(this.id, x, y, w, h);
211
- return this;
212
- };
213
-
214
- Window.prototype.setTitle = function(title) {
215
- this.X.ChangeProperty(
216
- 0,
217
- this.id,
218
- this.display.client.atoms.WM_NAME,
219
- this.display.client.atoms.STRING,
220
- 8,
221
- title
222
- );
223
- return this;
224
- };
225
-
226
- Window.prototype.setMouseHintOnly = function(isOn) {
227
- if (isOn && !(this.eventMask & x11.eventMask.PointerMotionHint)) {
228
- this.eventMask |= x11.eventMask.PointerMotionHint;
229
- } else if (!isOn && this.eventMask & x11.eventMask.PointerMotionHint) {
230
- this.eventMask -= x11.eventMask.PointerMotionHint;
231
- } else {
232
- return this;
233
- }
234
- this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, function() {});
235
- return this;
236
- };
237
-
238
- Window.prototype.queryPointer = function(callback) {
239
- this.X.QueryPointer(this.id, callback);
240
- return this;
241
- };
242
-
243
- Window.prototype.grabPointer = function() {
244
- this.X.GrabPointer(this.id, true, x11.eventMask.PointerMotion, 0, 1, 0, 0, 0);
245
- };
246
-
247
- Window.prototype.reparentTo = function(newParent, x, y) {
248
- this.X.ReparentWindow(this.id, newParent.id, x, y);
249
- };
250
-
251
- Window.prototype.destroy = function() {
252
- this.X.DestroyWindow(this.id);
253
- };
254
-
255
- Window.prototype.queryTree = function(callback) {
256
- var app = this.app;
257
- this.X.QueryTree(this.id, function(err, tree) {
258
- if (err) return callback(err);
259
- var children = tree.children.map(function(id) {
260
- return new Window(app, { id: id });
261
- });
262
- var parent = new Window(app, { id: tree.parent });
263
- // note that this root may be different from app.rootWindow() because there can be multiple screens (and roots)
264
- var root = new Window(app, { id: tree.root });
265
- var readyPromises = children.map(w => w._readyPromise);
266
- readyPromises.push(parent._readyPromise);
267
- readyPromises.push(root._readyPromise);
268
- Promise.all(readyPromises).then(function() {
269
- callback(null, { parent: parent, root: root, children: children });
270
- });
271
- });
272
- return this;
273
- };
274
-
275
- Window.prototype.message = function(type, data) {
276
- // send ClientMessage
277
- throw new Error('not implemented');
278
- };
279
-
280
- Window.prototype.createPixmap = function(params) {
281
- var Pixmap = require('../lib/pixmap.js');
282
- var pixmapParams = Object.assign({}, params);
283
- pixmapParams.parent = pixmapParams.parent || this;
284
- pixmapParams.width = pixmapParams.width || this.width;
285
- pixmapParams.height = pixmapParams.height || this.height;
286
- // TODO: use window depth
287
- pixmapParams.depth = pixmapParams.depth || 32;
288
- return new Pixmap(this.app, pixmapParams);
289
- };
290
-
291
- Window.prototype.createWindow = function(params) {
292
- var windowParams = Object.assign({}, params);
293
- windowParams.parent = windowParams.parent || this;
294
- return new Window(this.app, windowParams);
295
- };
296
-
297
- Window.prototype.setActions = function() {
298
- var set_property = require('x11-prop').set_property;
299
- var X = this.X;
300
- var wid = this.id;
301
- X.InternAtom(false, '_NET_WM_ACTION_CLOSE', function(err, _NET_WM_ACTION_CLOSE) {
302
- X.InternAtom(false, 'WM_DELETE_WINDOW', function(err, WM_DELETE_WINDOW) {
303
- //set_property(X, wid, '_NET_WM_ALLOWED_ACTIONS', 'ATOM', 32, [_NET_WM_ACTION_CLOSE]);
304
- console.log('Setting WM_PROTOCOLS');
305
- set_property(X, wid, 'WM_PROTOCOLS', 'ATOM', 32, [WM_DELETE_WINDOW]);
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
+ 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();
13
+
14
+ constructor(app, args = {}) {
15
+ if (args.id) {
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
+ }
20
+
21
+ super();
22
+
23
+ const X = app.X;
24
+ this.X = X;
25
+ this.app = app;
26
+ this.display = app.display;
27
+ this._mapped = false;
28
+ this._readyPromiseResolve = null;
29
+ this._readyPromise = new Promise((resolve) => {
30
+ this._readyPromiseResolve = resolve;
31
+ });
32
+
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
+ };
59
+
60
+ if (!args.id) {
61
+ this.id = X.AllocID();
62
+ this.eventMask = x11.eventMask.StructureNotify;
63
+
64
+ for (const ntkEventName in xevents.maskCamelCase) {
65
+ if (typeof args[ntkEventName] === 'function') {
66
+ this.eventMask |= xevents.maskCamelCase[ntkEventName];
67
+ this.on(xevents.toSnake[ntkEventName], args[ntkEventName]);
68
+ }
69
+ }
70
+
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
+ });
82
+ } else {
83
+ this.id = args.id;
84
+ this.eventMask = 0;
85
+ // populate width and height
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
+ });
95
+ }
96
+
97
+ Window.cache.set(this.id, this);
98
+
99
+ if (args.title) {
100
+ this.setTitle(args.title);
101
+ }
102
+
103
+ X.event_consumers[this.id] = this;
104
+ this.on('event', (ev) => {
105
+ const ntkev = ev; // todo: clone
106
+ ntkev.window = this;
107
+ ntkev.target = this;
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;
135
+ }
136
+ }
137
+ if (this._coalesce && xevents.coalesce[eventName]) {
138
+ this._enqueueCoalesced(eventName, ntkev);
139
+ return;
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();
147
+ });
148
+
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
+ });
159
+ });
160
+
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
+ }
169
+ });
170
+
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');
179
+ });
180
+
181
+ if (typeof this.width !== 'undefined') {
182
+ this._readyPromiseResolve();
183
+ }
184
+ }
185
+
186
+ _forget() {
187
+ Window.cache.delete(this.id);
188
+ delete this.X.event_consumers[this.id];
189
+ }
190
+
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
+ }
206
+
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
+ }
224
+
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
+ }
263
+
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
+ }
270
+ }
271
+
272
+ // ask user code to repaint (full window), on the next paced frame
273
+ _requestRedraw() {
274
+ this._frame.needsRedraw = true;
275
+ this._scheduleFrame();
276
+ }
277
+
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
+ });
290
+ }
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
+ });
320
+ }
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();
349
+ }
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();
376
+ }
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);
391
+ }
392
+
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
+ }
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
+ }
423
+
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
+ }
435
+
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
+ }
450
+
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
+ }
456
+
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
+ }
479
+
480
+ map() {
481
+ this.X.MapWindow(this.id);
482
+ return this;
483
+ }
484
+
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
+ }
550
+
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
+ });
589
+ });
590
+ return this;
591
+ }
592
+
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
601
+ });
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
+ }