ntk 8.1.1 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app.js +339 -7
- package/lib/drawable.js +10 -0
- package/lib/events_map.js +100 -1
- package/lib/glyphdirectory.js +275 -0
- package/lib/glyphdwire.js +142 -0
- package/lib/glyphset.js +32 -5
- package/lib/index.js +25 -2
- package/lib/pictformat.js +196 -0
- package/lib/pixmap.js +166 -7
- package/lib/region.js +148 -0
- package/lib/renderingcontext_2d.js +830 -235
- package/lib/shapeglyphs.js +94 -26
- package/lib/sharedglyphs.js +433 -0
- package/lib/text/glyphs.js +204 -26
- package/lib/widgets/svgview.js +7 -5
- package/lib/window.js +218 -28
- package/lib/xembed.js +3 -1
- package/package.json +2 -2
package/lib/app.js
CHANGED
|
@@ -4,11 +4,15 @@ import { GLError, backendFor, glCapabilities, glError, resolveGLPolicy } from '.
|
|
|
4
4
|
import { chooseGLXConfig } from './glx.js';
|
|
5
5
|
import Picture from './picture.js';
|
|
6
6
|
import Pixmap from './pixmap.js';
|
|
7
|
+
import Region, { REGION_DOCS } from './region.js';
|
|
8
|
+
import { formatForDepth, parsePictFormats, visualDepths, visualFormats } from './pictformat.js';
|
|
7
9
|
import { DEFAULT_RASTER_POLICY, defaultRasterizer } from './rasterize.js';
|
|
8
10
|
import { dropShadowSurfaces } from './shadow.js';
|
|
11
|
+
import { sharedGlyphsFor } from './sharedglyphs.js';
|
|
9
12
|
import { ShmUploader } from './shm-upload.js';
|
|
10
13
|
import FontManager from './text/fontmanager.js';
|
|
11
14
|
import Window from './window.js';
|
|
15
|
+
import * as xevents from './events_map.js';
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* The highest predefined atom id in the core protocol, `XA_WM_TRANSIENT_FOR`.
|
|
@@ -54,6 +58,38 @@ function modeRate(mode) {
|
|
|
54
58
|
return mode.dot_clock / (mode.h_total * vTotal);
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
/**
|
|
62
|
+
* XFIXES is missing, so no server-side regions.
|
|
63
|
+
*
|
|
64
|
+
* Worth a sentence about who is likely reading it: every X.Org release since
|
|
65
|
+
* 2004 and every XQuartz ship XFIXES, so a server without it is a deliberately
|
|
66
|
+
* minimal one — an embedded/nested server, or node-x11's own pure-JS server,
|
|
67
|
+
* which implements RENDER but not this. That is a fact about the display, not
|
|
68
|
+
* about the call, which is why it carries a `code`: a host that can degrade
|
|
69
|
+
* should branch on it rather than crash.
|
|
70
|
+
*
|
|
71
|
+
* @param {Error} [cause] whatever the extension query failed with
|
|
72
|
+
*/
|
|
73
|
+
function noXFixesError(cause) {
|
|
74
|
+
const err = new Error(
|
|
75
|
+
'ntk: the X server has no XFIXES extension, so it has no regions.\n' +
|
|
76
|
+
'\n' +
|
|
77
|
+
'Region clips (ctx.clipRegion), damage regions and window shapes all need it.\n' +
|
|
78
|
+
'Rectangular and path clips do not — ctx.clip() is core RENDER and works\n' +
|
|
79
|
+
'everywhere:\n' +
|
|
80
|
+
'\n' +
|
|
81
|
+
' ctx.save();\n' +
|
|
82
|
+
' ctx.beginPath();\n' +
|
|
83
|
+
' ctx.rect(x, y, w, h);\n' +
|
|
84
|
+
' ctx.clip();\n' +
|
|
85
|
+
'\n' +
|
|
86
|
+
`${REGION_DOCS}`,
|
|
87
|
+
cause ? { cause } : undefined
|
|
88
|
+
);
|
|
89
|
+
err.code = 'ERR_NTK_NO_XFIXES';
|
|
90
|
+
return err;
|
|
91
|
+
}
|
|
92
|
+
|
|
57
93
|
/**
|
|
58
94
|
* A connection to an X server. Owns the underlying node-x11 client
|
|
59
95
|
* (`app.X`) and acts as a factory for windows and pixmaps.
|
|
@@ -90,12 +126,34 @@ export default class App {
|
|
|
90
126
|
this._isolateAtoms();
|
|
91
127
|
this._fonts = null;
|
|
92
128
|
this._clipboard = null;
|
|
129
|
+
// Decide the shared-glyph question now, while the environment the app
|
|
130
|
+
// was created under is what the caller sees: the kill switch and the
|
|
131
|
+
// option are sampled once per connection, not per page. Constructing the
|
|
132
|
+
// client costs no I/O — discovery waits for the first page to bind.
|
|
133
|
+
sharedGlyphsFor(this);
|
|
93
134
|
this._cursors = null;
|
|
94
135
|
this._solidPictures = new Map();
|
|
95
136
|
this._rasterizer = undefined;
|
|
96
137
|
this._shm = undefined;
|
|
97
|
-
this.
|
|
138
|
+
this._extensionPromises = new Map();
|
|
139
|
+
// extension event routing (see _routeExtensionEvents): server-assigned
|
|
140
|
+
// event type code -> events_map.extension entry. Null until the first
|
|
141
|
+
// extension with events is required, which is also when the client-level
|
|
142
|
+
// listener that consults it is attached.
|
|
143
|
+
this._extEvents = null;
|
|
98
144
|
this._devicesPromise = null;
|
|
145
|
+
// XFIXES, once something has asked for it (regions, `app.fixes()`). The
|
|
146
|
+
// promise lives in _extensionPromises with the other extensions; the
|
|
147
|
+
// resolved object is also kept here because a region clip has to be
|
|
148
|
+
// installed synchronously, in request order with the drawing around it —
|
|
149
|
+
// see RenderingContext2d#clipRegion.
|
|
150
|
+
this._fixes = null;
|
|
151
|
+
// The visual -> picture format table and the formats list it was built
|
|
152
|
+
// from, read once out of node-x11's cached QueryPictFormats reply. Kept
|
|
153
|
+
// resolved alongside the promise so that binding a picture, which happens
|
|
154
|
+
// synchronously, can consult it without waiting (see pictFormats).
|
|
155
|
+
this._pictFormats = null;
|
|
156
|
+
this._pictFormatsPromise = null;
|
|
99
157
|
// node-x11 emits X errors it cannot route to a request callback as
|
|
100
158
|
// 'error' on the client — from inside its packet parser. With no
|
|
101
159
|
// listener that emit throws and the parser never re-arms, silently
|
|
@@ -302,6 +360,89 @@ export default class App {
|
|
|
302
360
|
});
|
|
303
361
|
}
|
|
304
362
|
|
|
363
|
+
/**
|
|
364
|
+
* The node-x11 object for extension `name`, or `null` where the server has
|
|
365
|
+
* none — asked once per connection, the absent answer included.
|
|
366
|
+
*
|
|
367
|
+
* node-x11 caches the `QueryExtension` reply itself since 4.0.1
|
|
368
|
+
* ([node-x11#287](https://github.com/sidorares/node-x11/issues/287)), so a
|
|
369
|
+
* repeated probe no longer costs a round trip even when the answer is
|
|
370
|
+
* absent — "can this machine run a compositor?" is a question about what is
|
|
371
|
+
* *not* there, and it used to pay for asking. This cache stays for what it
|
|
372
|
+
* does beyond that: one promise per name, so concurrent callers share the
|
|
373
|
+
* one query, `_routeExtensionEvents` runs once rather than per call, and an
|
|
374
|
+
* absent extension answers `null` instead of an error.
|
|
375
|
+
*
|
|
376
|
+
* `name` is node-x11's module name (`'fixes'`, not `'XFIXES'`), and the
|
|
377
|
+
* accessors below are the whole supported set. Deliberately not public: a
|
|
378
|
+
* name that arrives misspelled resolves `null` forever, which is
|
|
379
|
+
* indistinguishable from a server that does not have the extension.
|
|
380
|
+
*
|
|
381
|
+
* @param {string} name
|
|
382
|
+
* @returns {Promise<object|null>}
|
|
383
|
+
*/
|
|
384
|
+
_extension(name) {
|
|
385
|
+
let pending = this._extensionPromises.get(name);
|
|
386
|
+
if (!pending) {
|
|
387
|
+
pending = new Promise((resolve) => {
|
|
388
|
+
this.X.require(name, (err, ext) => {
|
|
389
|
+
if (!err && ext) this._routeExtensionEvents(name, ext);
|
|
390
|
+
resolve(err ? null : ext);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
this._extensionPromises.set(name, pending);
|
|
394
|
+
}
|
|
395
|
+
return pending;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Give this extension's events names, and a route to the object they name.
|
|
400
|
+
*
|
|
401
|
+
* A non-generic extension event carries a server-assigned type code
|
|
402
|
+
* (`firstEvent` + a fixed offset) and names its target under the field its
|
|
403
|
+
* own protocol calls it — DamageNotify a `drawable`, ShapeNotify and the
|
|
404
|
+
* XFIXES notifies a `window` — never under the `wid` node-x11 dispatches
|
|
405
|
+
* per-window consumers by. So without this they reach the client's raw
|
|
406
|
+
* `'event'` stream and nothing else (issue #290).
|
|
407
|
+
*
|
|
408
|
+
* Registered here, when the extension is first required through the
|
|
409
|
+
* accessors above, because the type codes exist only once the server has
|
|
410
|
+
* answered. From then on the listener below hands each one to the Window
|
|
411
|
+
* or Pixmap it names — through `_deliverEvent`, so a window's coalescing
|
|
412
|
+
* and frame pacing apply: `damage` unions its rectangles per paced frame
|
|
413
|
+
* exactly as `expose` does. The lookup goes through `X.event_consumers`,
|
|
414
|
+
* where every Window already sits for core dispatch and a Pixmap enrols
|
|
415
|
+
* itself when given a listener (see lib/pixmap.js).
|
|
416
|
+
*/
|
|
417
|
+
_routeExtensionEvents(module, ext) {
|
|
418
|
+
const events = xevents.extension[module];
|
|
419
|
+
if (!events || !ext.firstEvent || !ext.events) return;
|
|
420
|
+
if (!this._extEvents) {
|
|
421
|
+
this._extEvents = new Map();
|
|
422
|
+
this.X.on('event', (ev) => {
|
|
423
|
+
// by the server-assigned type code, which every extension event
|
|
424
|
+
// carries since x11 4.0.0 (node-x11#284 — DamageNotify and a couple
|
|
425
|
+
// of others used to name themselves without it). Only the codes this
|
|
426
|
+
// table registered can match.
|
|
427
|
+
const route = this._extEvents.get(ev.type);
|
|
428
|
+
if (!route) return;
|
|
429
|
+
const target = this.X.event_consumers[ev[route.target]];
|
|
430
|
+
// a consumer that is not an ntk drawable (a caller's own) keeps
|
|
431
|
+
// reading the raw client stream it always read
|
|
432
|
+
if (typeof target?._deliverEvent !== 'function') return;
|
|
433
|
+
const ntkev = route.translate(ev);
|
|
434
|
+
ntkev.target = target;
|
|
435
|
+
if (target instanceof Window) ntkev.window = target;
|
|
436
|
+
target._deliverEvent(route.name, ntkev);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
for (const [key, spec] of Object.entries(events)) {
|
|
440
|
+
const offset = ext.events[key];
|
|
441
|
+
if (offset === undefined) continue;
|
|
442
|
+
this._extEvents.set(ext.firstEvent + offset, spec);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
305
446
|
/**
|
|
306
447
|
* The XInput extension, or `null` where the server has none — asked once
|
|
307
448
|
* per connection.
|
|
@@ -312,12 +453,59 @@ export default class App {
|
|
|
312
453
|
* @returns {Promise<object|null>}
|
|
313
454
|
*/
|
|
314
455
|
xinput() {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
456
|
+
return this._extension('xinput');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The Composite extension, or `null` where the server has none: the
|
|
461
|
+
* redirection half of a compositing manager — `RedirectSubwindows`,
|
|
462
|
+
* `NameWindowPixmap`, `Get`/`ReleaseOverlayWindow`
|
|
463
|
+
* (docs/app.md#extensions).
|
|
464
|
+
*
|
|
465
|
+
* `null` here is a live answer rather than a defensive one, and the one
|
|
466
|
+
* worth branching on: XQuartz carries DAMAGE, XFIXES, SHAPE and RENDER but
|
|
467
|
+
* no Composite at all, so this is where "can this machine run a
|
|
468
|
+
* compositor?" is decided.
|
|
469
|
+
*
|
|
470
|
+
* @returns {Promise<object|null>}
|
|
471
|
+
*/
|
|
472
|
+
composite() {
|
|
473
|
+
return this._extension('composite');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The DAMAGE extension, or `null` where the server has none: what turns
|
|
478
|
+
* "this drawable changed" into an event, so a compositor repaints the
|
|
479
|
+
* region a client drew into rather than the screen
|
|
480
|
+
* (docs/app.md#extensions).
|
|
481
|
+
*
|
|
482
|
+
* @returns {Promise<object|null>}
|
|
483
|
+
*/
|
|
484
|
+
damage() {
|
|
485
|
+
return this._extension('damage');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* The XFIXES extension, or `null` where the server has none: server-side
|
|
490
|
+
* regions and the algebra over them, which is how a damaged area becomes a
|
|
491
|
+
* clip — `SetPictureClipRegion(ctx.picture.id, 0, 0, region)` narrows a 2d
|
|
492
|
+
* context to one (docs/app.md#extensions).
|
|
493
|
+
*
|
|
494
|
+
* @returns {Promise<object|null>}
|
|
495
|
+
*/
|
|
496
|
+
xfixes() {
|
|
497
|
+
return this._extension('fixes');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* The SHAPE extension, or `null` where the server has none: non-rectangular
|
|
502
|
+
* window bounding/clip/input shapes, which a compositor has to read to
|
|
503
|
+
* paint a shaped client correctly (docs/app.md#extensions).
|
|
504
|
+
*
|
|
505
|
+
* @returns {Promise<object|null>}
|
|
506
|
+
*/
|
|
507
|
+
shape() {
|
|
508
|
+
return this._extension('shape');
|
|
321
509
|
}
|
|
322
510
|
|
|
323
511
|
/**
|
|
@@ -355,6 +543,18 @@ export default class App {
|
|
|
355
543
|
return this._clipboard;
|
|
356
544
|
}
|
|
357
545
|
|
|
546
|
+
/**
|
|
547
|
+
* The cross-process shared glyph cache client (docs/shared-glyphs.md), or
|
|
548
|
+
* `null` when the feature is off — `createClient({ sharedGlyphs: false })`
|
|
549
|
+
* or the `NTK_NO_SHARED_GLYPHS` environment kill switch. Purely lazy: the
|
|
550
|
+
* accessor itself touches no server state; discovery (and the first app's
|
|
551
|
+
* self-election as the display's glyph directory) happens when the first
|
|
552
|
+
* glyph page binds.
|
|
553
|
+
*/
|
|
554
|
+
get sharedGlyphs() {
|
|
555
|
+
return sharedGlyphsFor(this);
|
|
556
|
+
}
|
|
557
|
+
|
|
358
558
|
/** per-connection cache of X11 cursor-font cursors (see lib/cursor.js) */
|
|
359
559
|
get cursors() {
|
|
360
560
|
if (!this._cursors) this._cursors = new CursorCache(this);
|
|
@@ -456,6 +656,102 @@ export default class App {
|
|
|
456
656
|
return null;
|
|
457
657
|
}
|
|
458
658
|
|
|
659
|
+
/**
|
|
660
|
+
* The RENDER picture formats this server publishes, and which one belongs
|
|
661
|
+
* to each visual: `{ formats, byVisual }`.
|
|
662
|
+
*
|
|
663
|
+
* No round trip of ntk's own: node-x11 sends `QueryPictFormats` itself
|
|
664
|
+
* while requiring RENDER — that is where its `rgb24`/`rgba32`/`a8` come
|
|
665
|
+
* from — and since 4.0.0 it keeps the reply as `Render.pictFormats`. So
|
|
666
|
+
* the answer is already here before anything can draw. `formats` is the
|
|
667
|
+
* list as objects (`{ id, type, depth, redShift, redMask, ... }`);
|
|
668
|
+
* `byVisual` is a `Map` from visual id to format id.
|
|
669
|
+
*
|
|
670
|
+
* @returns {Promise<{formats: Array<object>, byVisual: Map<number, number>}>}
|
|
671
|
+
*/
|
|
672
|
+
pictFormats() {
|
|
673
|
+
if (this._pictFormatsPromise) return this._pictFormatsPromise;
|
|
674
|
+
this._pictFormatsPromise = new Promise((resolve, reject) => {
|
|
675
|
+
const reply = this.display.Render?.pictFormats;
|
|
676
|
+
if (!reply) {
|
|
677
|
+
reject(new Error('ntk: this connection has no RENDER extension, so it has no picture formats'));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const formats = parsePictFormats(reply);
|
|
681
|
+
this._pictFormats = {
|
|
682
|
+
formats,
|
|
683
|
+
byVisual: visualFormats(this.display, formats, reply)
|
|
684
|
+
};
|
|
685
|
+
resolve(this._pictFormats);
|
|
686
|
+
});
|
|
687
|
+
return this._pictFormatsPromise;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* The picture format a drawable on `visual` is read and written through.
|
|
692
|
+
*
|
|
693
|
+
* Depth is not enough to name a format and a visual is: a depth-16 visual
|
|
694
|
+
* can be 5:6:5 or 5:5:5, a depth-24 one RGB or BGR, and 10:10:10:2 is 32
|
|
695
|
+
* bits wide like 8:8:8:8. Anything drawing on a drawable it did not create
|
|
696
|
+
* — a compositor holding another client's pixmap, an adopted or embedded
|
|
697
|
+
* window — has to ask, or RENDER reads the channels wrong without ever
|
|
698
|
+
* complaining (issue #295).
|
|
699
|
+
*
|
|
700
|
+
* const attrs = await wnd.getAttributes();
|
|
701
|
+
* const format = await app.pictFormatFor(attrs.visual);
|
|
702
|
+
*
|
|
703
|
+
* `visual` may be an id or a handshake visual object. `depth` is the
|
|
704
|
+
* fallback: where the server names no format for the visual — an indexed
|
|
705
|
+
* visual, or one this connection was never told about — the standard
|
|
706
|
+
* format for that depth is returned, which is what ntk used to assume
|
|
707
|
+
* everywhere.
|
|
708
|
+
*
|
|
709
|
+
* @param {number|object} visual visual id, or `{ vid }`
|
|
710
|
+
* @param {object} [options]
|
|
711
|
+
* @param {number} [options.depth] the drawable's depth, for the fallback
|
|
712
|
+
* @returns {Promise<number|undefined>} picture format id
|
|
713
|
+
*/
|
|
714
|
+
async pictFormatFor(visual, { depth } = {}) {
|
|
715
|
+
const id = typeof visual === 'object' && visual ? visual.vid : visual;
|
|
716
|
+
try {
|
|
717
|
+
await this.pictFormats();
|
|
718
|
+
} catch {
|
|
719
|
+
// a server that cannot answer leaves the fallback
|
|
720
|
+
}
|
|
721
|
+
return this._knownPictFormat(id, depth) ?? formatForDepth(this.display.Render, depth);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* The cached answer to `pictFormatFor`, or `undefined` when the table has
|
|
726
|
+
* not arrived, names no format for this visual, or the visual does not
|
|
727
|
+
* describe a drawable of this depth.
|
|
728
|
+
*
|
|
729
|
+
* That last one is the guard against handing RENDER a format it will
|
|
730
|
+
* reject: a picture's format and its drawable's depth have to agree, and
|
|
731
|
+
* the visual on hand is not always the drawable's own — a window's backing
|
|
732
|
+
* pixmap is allocated at a depth ntk sometimes has to assume. Where they
|
|
733
|
+
* disagree the depth is what the drawable really is, so the depth-based
|
|
734
|
+
* format is what it gets.
|
|
735
|
+
*
|
|
736
|
+
* A picture is bound synchronously, in request order with the drawing
|
|
737
|
+
* around it, so the format has to be available without a wait. Callers
|
|
738
|
+
* fall back to the depth and re-bind once `pictFormats()` resolves.
|
|
739
|
+
*/
|
|
740
|
+
_knownPictFormat(visual, depth) {
|
|
741
|
+
if (!visual) return undefined;
|
|
742
|
+
const id = visual >>> 0;
|
|
743
|
+
const format = this._pictFormats?.byVisual.get(id);
|
|
744
|
+
if (format === undefined) return undefined;
|
|
745
|
+
// Depth 0 is not a depth: it is CopyFromParent, which ntk never resolved
|
|
746
|
+
// because only the server did — and a window on that visual is of that
|
|
747
|
+
// visual's depth by definition, so there is nothing to disagree with.
|
|
748
|
+
if (depth) {
|
|
749
|
+
this._visualDepths ??= visualDepths(this.display);
|
|
750
|
+
if (this._visualDepths.get(id) !== depth) return undefined;
|
|
751
|
+
}
|
|
752
|
+
return format;
|
|
753
|
+
}
|
|
754
|
+
|
|
459
755
|
rootWindow(screen = 0) {
|
|
460
756
|
return new Window(this, { id: this.display.screen[screen].root });
|
|
461
757
|
}
|
|
@@ -497,6 +793,42 @@ export default class App {
|
|
|
497
793
|
return new Pixmap(this, args);
|
|
498
794
|
}
|
|
499
795
|
|
|
796
|
+
/**
|
|
797
|
+
* The XFIXES extension for this connection, loaded once and shared.
|
|
798
|
+
*
|
|
799
|
+
* Regions live here — the server-side rectangle sets X uses for damage,
|
|
800
|
+
* window shapes and compositor bookkeeping — along with the requests that
|
|
801
|
+
* combine them and the one that hangs a region on a Picture as its clip.
|
|
802
|
+
*
|
|
803
|
+
* The throwing spelling of `app.xfixes()`: same query, same cache, but a
|
|
804
|
+
* server without XFIXES rejects with code `'ERR_NTK_NO_XFIXES'` instead of
|
|
805
|
+
* resolving `null` — regions cannot degrade, so their absence is an error
|
|
806
|
+
* here rather than an answer.
|
|
807
|
+
*
|
|
808
|
+
* @returns {Promise<object>} node-x11's XFIXES extension object
|
|
809
|
+
*/
|
|
810
|
+
async fixes() {
|
|
811
|
+
const ext = await this.xfixes();
|
|
812
|
+
if (!ext) throw noXFixesError();
|
|
813
|
+
this._fixes = ext;
|
|
814
|
+
return ext;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* A server-side region of the given rectangles (`{ x, y, width, height }`
|
|
819
|
+
* or ntk's own `{ x, y, w, h }`), empty by default.
|
|
820
|
+
*
|
|
821
|
+
* Async because XFIXES is loaded on first use; a second region costs one
|
|
822
|
+
* request and no round trip. Free it with `destroy()` — or `using`, or let
|
|
823
|
+
* the GC do it (docs/resource-management.md).
|
|
824
|
+
*
|
|
825
|
+
* @param {Array<object>} [rects]
|
|
826
|
+
* @returns {Promise<Region>}
|
|
827
|
+
*/
|
|
828
|
+
async createRegion(rects = []) {
|
|
829
|
+
return new Region(this, await this.fixes(), rects);
|
|
830
|
+
}
|
|
831
|
+
|
|
500
832
|
/**
|
|
501
833
|
* A repeating source Picture of one colour, for compositing. Components
|
|
502
834
|
* are 0..1 floats, premultiplied by alpha.
|
package/lib/drawable.js
CHANGED
|
@@ -11,6 +11,16 @@ export default class Drawable extends EventEmitter {
|
|
|
11
11
|
if (!factory) throw new Error(`Unknown rendering context: ${name}`);
|
|
12
12
|
return factory(this, ...args);
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Deliver one named event to this drawable's listeners. Window overrides
|
|
17
|
+
* this with per-frame coalescing and pacing; the base emits directly,
|
|
18
|
+
* for drawables with no frame clock — a Pixmap a DAMAGE object watches
|
|
19
|
+
* (see App#_routeExtensionEvents).
|
|
20
|
+
*/
|
|
21
|
+
_deliverEvent(name, ev) {
|
|
22
|
+
this.emit(name, ev);
|
|
23
|
+
}
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
// populated by the renderingcontext_* modules on import
|
package/lib/events_map.js
CHANGED
|
@@ -86,6 +86,10 @@ export const coalesce = {
|
|
|
86
86
|
mousemove: 'last',
|
|
87
87
|
resize: 'last',
|
|
88
88
|
expose: 'union',
|
|
89
|
+
// DamageNotify is the expose case again — a burst of rectangles whose
|
|
90
|
+
// union is what a repaint wants — reported about a drawable's content
|
|
91
|
+
// instead of a window's visibility
|
|
92
|
+
damage: 'union',
|
|
89
93
|
// 'accumulate' — scroll distance adds up. Keeping the last delta instead
|
|
90
94
|
// would throw away everything but the final step of a fast scroll, and a
|
|
91
95
|
// frame's worth of a touchpad's sub-notch deltas is exactly the case the
|
|
@@ -93,6 +97,101 @@ export const coalesce = {
|
|
|
93
97
|
wheel: 'accumulate'
|
|
94
98
|
};
|
|
95
99
|
|
|
100
|
+
// XFixes SelectionNotify subtype -> why the ownership changed, the same
|
|
101
|
+
// vocabulary clipboard.watch already answers in. The codes are fixed by
|
|
102
|
+
// xfixesproto (SelectionEvent), not assigned by the server.
|
|
103
|
+
const selectionReason = ['new-owner', 'destroyed', 'closed'];
|
|
104
|
+
|
|
105
|
+
// ShapeNotify kind -> which of the window's three shapes changed
|
|
106
|
+
// (shapeproto ShapeKind)
|
|
107
|
+
const shapeKind = ['bounding', 'clip', 'input'];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The non-generic extension events, and how to deliver them.
|
|
111
|
+
*
|
|
112
|
+
* Unlike core events their type codes are assigned by the server at
|
|
113
|
+
* QueryExtension time (`ext.firstEvent` + a fixed offset), and the drawable
|
|
114
|
+
* each one names arrives under the field its own protocol calls it —
|
|
115
|
+
* DamageNotify a `drawable`, the others a `window` — never under the `wid`
|
|
116
|
+
* node-x11 dispatches per-window consumers by. So they need both halves of
|
|
117
|
+
* this table: a name, and which field to route by. App reads it when an
|
|
118
|
+
* extension is required through its accessors (`app.damage()` and friends)
|
|
119
|
+
* and hands each event to the Window or Pixmap it names — see
|
|
120
|
+
* App#_routeExtensionEvents and docs/app.md "Extension events".
|
|
121
|
+
*
|
|
122
|
+
* Keyed by node-x11's module name, then by the event's key in `ext.events`.
|
|
123
|
+
* `translate` shapes the raw node-x11 event into the one delivered.
|
|
124
|
+
*/
|
|
125
|
+
export const extension = {
|
|
126
|
+
damage: {
|
|
127
|
+
DamageNotify: {
|
|
128
|
+
name: 'damage',
|
|
129
|
+
target: 'drawable',
|
|
130
|
+
// expose-shaped, because it is the same news: the box in
|
|
131
|
+
// x/y/width/height so 'union' coalescing applies to it unchanged.
|
|
132
|
+
// Bit 7 of the level byte is the wire's own "more follow" flag —
|
|
133
|
+
// split out, since the report level it rides on is 0..3.
|
|
134
|
+
translate: (ev) => ({
|
|
135
|
+
x: ev.area.x,
|
|
136
|
+
y: ev.area.y,
|
|
137
|
+
width: ev.area.w,
|
|
138
|
+
height: ev.area.h,
|
|
139
|
+
geometry: ev.geometry,
|
|
140
|
+
damage: ev.damage,
|
|
141
|
+
level: ev.level & 0x7f,
|
|
142
|
+
more: !!(ev.level & 0x80),
|
|
143
|
+
time: ev.time
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
fixes: {
|
|
148
|
+
// 'selection' is taken — it is core SelectionNotify, a conversion
|
|
149
|
+
// answered — and this event is about who owns the selection, hence the
|
|
150
|
+
// qualified name
|
|
151
|
+
SelectionNotify: {
|
|
152
|
+
name: 'selection_owner',
|
|
153
|
+
target: 'window',
|
|
154
|
+
translate: (ev) => ({
|
|
155
|
+
selection: ev.selection,
|
|
156
|
+
owner: ev.owner,
|
|
157
|
+
reason: selectionReason[ev.subtype] ?? ev.subtype,
|
|
158
|
+
timestamp: ev.timestamp,
|
|
159
|
+
selectionTimestamp: ev.selectionTimestamp
|
|
160
|
+
})
|
|
161
|
+
},
|
|
162
|
+
CursorNotify: {
|
|
163
|
+
name: 'cursor',
|
|
164
|
+
target: 'window',
|
|
165
|
+
translate: (ev) => ({
|
|
166
|
+
cursorSerial: ev.cursorSerial,
|
|
167
|
+
cursorName: ev.cursorName,
|
|
168
|
+
time: ev.timestamp
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
shape: {
|
|
173
|
+
ShapeNotify: {
|
|
174
|
+
name: 'shape',
|
|
175
|
+
target: 'window',
|
|
176
|
+
translate: (ev) => ({
|
|
177
|
+
kind: shapeKind[ev.kind] ?? ev.kind,
|
|
178
|
+
x: ev.x,
|
|
179
|
+
y: ev.y,
|
|
180
|
+
width: ev.width,
|
|
181
|
+
height: ev.height,
|
|
182
|
+
shaped: !!ev.shaped,
|
|
183
|
+
time: ev.time
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// every routed extension event name — what a Pixmap watches `newListener`
|
|
190
|
+
// for to enrol itself in the routing table (see lib/pixmap.js)
|
|
191
|
+
export const extensionEventNames = new Set(
|
|
192
|
+
Object.values(extension).flatMap((events) => Object.values(events).map((spec) => spec.name))
|
|
193
|
+
);
|
|
194
|
+
|
|
96
195
|
export const toSnake = {
|
|
97
196
|
onMouseMove: 'mousemove',
|
|
98
197
|
onMouseOver: 'mouseover',
|
|
@@ -124,4 +223,4 @@ export const maskCamelCase = Object.fromEntries(
|
|
|
124
223
|
Object.entries(toSnake).map(([camel, snake]) => [camel, mask[snake]])
|
|
125
224
|
);
|
|
126
225
|
|
|
127
|
-
export default { eventName, mask, maskCamelCase, toSnake, coalesce };
|
|
226
|
+
export default { eventName, mask, maskCamelCase, toSnake, coalesce, extension, extensionEventNames };
|