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.
Files changed (47) hide show
  1. package/README.md +156 -151
  2. package/lib/app.js +64 -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 +129 -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 +691 -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,691 @@
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
+ // 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
+ }