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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which RENDER picture format describes a drawable's pixels.
|
|
3
|
+
*
|
|
4
|
+
* A picture format is how the server is told to read and write a drawable:
|
|
5
|
+
* where the red, green, blue and alpha bits are and how many of each. Depth
|
|
6
|
+
* does not name one — a depth-16 visual can be 5:6:5 or 5:5:5 with a spare
|
|
7
|
+
* bit, a depth-24 one can be RGB or BGR, and both 8:8:8:8 and 10:10:10:2 are
|
|
8
|
+
* 32 bits wide. The **visual** is what fixes the layout, so that is what a
|
|
9
|
+
* format has to be picked from (issue #295).
|
|
10
|
+
*
|
|
11
|
+
* The mapping the server keeps is in `QueryPictFormats`' screens/depths/
|
|
12
|
+
* visuals section, which node-x11 decodes since 4.0.0
|
|
13
|
+
* ([node-x11#280](https://github.com/sidorares/node-x11/issues/280)) — that
|
|
14
|
+
* is the server's own answer, and it is what is used when the reply carries
|
|
15
|
+
* it.
|
|
16
|
+
*
|
|
17
|
+
* A visual the reply leaves out — one whose depth this server's RENDER does
|
|
18
|
+
* not describe — is reconstructed the way the server would have built it: a
|
|
19
|
+
* visual's `red_mask`/`green_mask`/`blue_mask` from the connection handshake,
|
|
20
|
+
* matched against the shift/mask pairs of the formats list. Both sides come
|
|
21
|
+
* from the same server, so this is a lookup rather than a guess — the only
|
|
22
|
+
* thing it cannot recover is a format for an *indexed* visual, which has no
|
|
23
|
+
* masks to match on and which only the reply can name.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** `type` in a formats-list entry. */
|
|
27
|
+
export const PICT_TYPE = { Indexed: 0, Direct: 1 };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The fields of a `QueryPictFormats` entry, which node-x11 hands over as a
|
|
31
|
+
* positional array (`x11/lib/ext/render.js`).
|
|
32
|
+
*/
|
|
33
|
+
const FIELD = {
|
|
34
|
+
id: 0,
|
|
35
|
+
type: 1,
|
|
36
|
+
depth: 2,
|
|
37
|
+
redShift: 3,
|
|
38
|
+
redMask: 4,
|
|
39
|
+
greenShift: 5,
|
|
40
|
+
greenMask: 6,
|
|
41
|
+
blueShift: 7,
|
|
42
|
+
blueMask: 8,
|
|
43
|
+
alphaShift: 9,
|
|
44
|
+
alphaMask: 10,
|
|
45
|
+
colormap: 11
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** A formats-list entry as an object, from node-x11's positional array. */
|
|
49
|
+
export function parsePictFormats(reply) {
|
|
50
|
+
const list = (Array.isArray(reply) ? reply : reply?.formats) ?? [];
|
|
51
|
+
return list.map((f) => {
|
|
52
|
+
if (!Array.isArray(f)) return f;
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const name in FIELD) out[name] = f[FIELD[name]];
|
|
55
|
+
return out;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Where a channel's bits sit in a pixel, as one mask over the whole word. */
|
|
60
|
+
const channelMask = (shift, mask) => ((mask >>> 0) << shift) >>> 0;
|
|
61
|
+
|
|
62
|
+
/** How many bits a mask covers. */
|
|
63
|
+
function bitCount(mask) {
|
|
64
|
+
let n = 0;
|
|
65
|
+
for (let m = mask >>> 0; m; m >>>= 1) n += m & 1;
|
|
66
|
+
return n;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The format id for a visual, or `null` where the formats list holds none.
|
|
71
|
+
*
|
|
72
|
+
* The colour masks have to agree exactly; what is left over is the choice
|
|
73
|
+
* between, say, `a8r8g8b8` and `x8r8g8b8`, and the spare bits settle it. A
|
|
74
|
+
* whole byte the channels do not account for is the alpha channel every ARGB
|
|
75
|
+
* visual has; a bit or two — the spare bit of a 5:5:5 in 16, the two of a
|
|
76
|
+
* 10:10:10 in 32 — is padding no client ever wrote, and reading it as alpha
|
|
77
|
+
* would composite a garbage bit as transparency.
|
|
78
|
+
*
|
|
79
|
+
* Depth is matched first and relaxed only if that finds nothing, because the
|
|
80
|
+
* two lists do not always agree on it. A server names the format it created
|
|
81
|
+
* for a visual with that visual's depth, but the fixed formats every RENDER
|
|
82
|
+
* server also publishes carry the *pixel* width instead — `a2r10g10b10` is
|
|
83
|
+
* listed at depth 32, and a 10:10:10 visual is depth 30. Matching masks alone
|
|
84
|
+
* still identifies the layout, which is the part that decides how pixels are
|
|
85
|
+
* read.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} visual a handshake visual (`{ red_mask, green_mask, blue_mask }`)
|
|
88
|
+
* @param {number} depth the depth its `depths` entry is under
|
|
89
|
+
* @param {Array<object>} formats parsed formats list
|
|
90
|
+
* @returns {number|null}
|
|
91
|
+
*/
|
|
92
|
+
export function matchVisualFormat(visual, depth, formats) {
|
|
93
|
+
const red = (visual?.red_mask ?? 0) >>> 0;
|
|
94
|
+
const green = (visual?.green_mask ?? 0) >>> 0;
|
|
95
|
+
const blue = (visual?.blue_mask ?? 0) >>> 0;
|
|
96
|
+
// An indexed visual (PseudoColor, GrayScale and the static pair) carries no
|
|
97
|
+
// masks: its format is an Indexed one, tied to a colormap this list cannot
|
|
98
|
+
// be matched against. Nothing here can name it, and saying so is better
|
|
99
|
+
// than naming a Direct format whose channels it does not have.
|
|
100
|
+
if (!(red || green || blue)) return null;
|
|
101
|
+
|
|
102
|
+
const wantAlpha = depth - (bitCount(red) + bitCount(green) + bitCount(blue)) >= 8;
|
|
103
|
+
// near misses, in the order they would be settled for: same depth but the
|
|
104
|
+
// wrong side of the alpha choice, then the two the same way round again
|
|
105
|
+
// with the depth relaxed
|
|
106
|
+
let sameDepthAlt = null;
|
|
107
|
+
let otherDepth = null;
|
|
108
|
+
let otherDepthAlt = null;
|
|
109
|
+
|
|
110
|
+
for (const f of formats) {
|
|
111
|
+
if (f.type !== PICT_TYPE.Direct) continue;
|
|
112
|
+
if (channelMask(f.redShift, f.redMask) !== red) continue;
|
|
113
|
+
if (channelMask(f.greenShift, f.greenMask) !== green) continue;
|
|
114
|
+
if (channelMask(f.blueShift, f.blueMask) !== blue) continue;
|
|
115
|
+
const hasAlpha = (f.alphaMask ?? 0) !== 0;
|
|
116
|
+
if (f.depth === depth) {
|
|
117
|
+
if (hasAlpha === wantAlpha) return f.id;
|
|
118
|
+
sameDepthAlt ??= f.id;
|
|
119
|
+
} else if (hasAlpha === wantAlpha) {
|
|
120
|
+
otherDepth ??= f.id;
|
|
121
|
+
} else {
|
|
122
|
+
otherDepthAlt ??= f.id;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return sameDepthAlt ?? otherDepth ?? otherDepthAlt;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* visual id -> format id, from the reply's screens section where the server
|
|
130
|
+
* sent one, and matched on colour masks where it did not.
|
|
131
|
+
*
|
|
132
|
+
* Visual ids are unique across screens, so one flat map answers for all of
|
|
133
|
+
* them. The server's own table is the whole answer where it is there — it
|
|
134
|
+
* names indexed visuals too, which masks cannot — and mask matching then
|
|
135
|
+
* fills in only the visuals it left out.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} display node-x11's display object
|
|
138
|
+
* @param {Array<object>} formats parsed formats list
|
|
139
|
+
* @param {object} [reply] the `QueryPictFormats` reply, for its `screens`
|
|
140
|
+
* @returns {Map<number, number>}
|
|
141
|
+
*/
|
|
142
|
+
export function visualFormats(display, formats, reply) {
|
|
143
|
+
const byVisual = new Map();
|
|
144
|
+
for (const screen of reply?.screens ?? []) {
|
|
145
|
+
for (const depth of screen?.depths ?? []) {
|
|
146
|
+
for (const { visual, format } of depth?.visuals ?? []) {
|
|
147
|
+
byVisual.set(visual >>> 0, format);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const screen of display?.screen ?? []) {
|
|
152
|
+
for (const depth in screen.depths ?? {}) {
|
|
153
|
+
for (const visual of Object.values(screen.depths[depth])) {
|
|
154
|
+
const vid = visual.vid >>> 0;
|
|
155
|
+
if (byVisual.has(vid)) continue;
|
|
156
|
+
const format = matchVisualFormat(visual, Number(depth), formats);
|
|
157
|
+
if (format != null) byVisual.set(vid, format);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return byVisual;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* visual id -> the depth its pixels are, straight from the handshake.
|
|
166
|
+
*
|
|
167
|
+
* @param {object} display node-x11's display object
|
|
168
|
+
* @returns {Map<number, number>}
|
|
169
|
+
*/
|
|
170
|
+
export function visualDepths(display) {
|
|
171
|
+
const byVisual = new Map();
|
|
172
|
+
for (const screen of display?.screen ?? []) {
|
|
173
|
+
for (const depth in screen.depths ?? {}) {
|
|
174
|
+
for (const visual of Object.values(screen.depths[depth])) {
|
|
175
|
+
byVisual.set(visual.vid >>> 0, Number(depth));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return byVisual;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The format a drawable of this depth is read through when its visual is not
|
|
184
|
+
* available — a pixmap, which has no visual at all, or a window whose visual
|
|
185
|
+
* has not been asked for yet.
|
|
186
|
+
*
|
|
187
|
+
* The standard formats node-x11 picks out of the list by their masks. Right
|
|
188
|
+
* for the layouts an X client meets most of the time (8:8:8 colour, 8-bit
|
|
189
|
+
* coverage) and wrong for everything else, which is why it is the fallback
|
|
190
|
+
* and not the answer.
|
|
191
|
+
*/
|
|
192
|
+
export function formatForDepth(Render, depth) {
|
|
193
|
+
if (depth === 32) return Render.rgba32;
|
|
194
|
+
if (depth === 8) return Render.a8;
|
|
195
|
+
return Render.rgb24;
|
|
196
|
+
}
|
package/lib/pixmap.js
CHANGED
|
@@ -1,15 +1,46 @@
|
|
|
1
1
|
import { safeRelease } from './cleanup.js';
|
|
2
2
|
import Drawable from './drawable.js';
|
|
3
|
+
import { extensionEventNames } from './events_map.js';
|
|
3
4
|
|
|
4
5
|
// GC fallback: free the server-side pixmap when the wrapper is collected
|
|
5
6
|
// without an explicit destroy()
|
|
6
|
-
const registry = new FinalizationRegistry(({ X, id }) => {
|
|
7
|
+
const registry = new FinalizationRegistry(({ X, id, releaseId }) => {
|
|
7
8
|
safeRelease(X, () => {
|
|
8
9
|
X.FreePixmap(id);
|
|
9
|
-
X.ReleaseID(id);
|
|
10
|
+
if (releaseId) X.ReleaseID(id);
|
|
10
11
|
});
|
|
11
12
|
});
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* A GetGeometry reply in ntk's spelling (window.js has the window-shaped
|
|
16
|
+
* twin). For a pixmap, `x`, `y` and `borderWidth` are always 0 — X reports
|
|
17
|
+
* them anyway, and returning the same shape as `wnd.getGeometry()` keeps the
|
|
18
|
+
* two interchangeable.
|
|
19
|
+
*/
|
|
20
|
+
function unpackGeometry(res) {
|
|
21
|
+
return {
|
|
22
|
+
x: res.xPos,
|
|
23
|
+
y: res.yPos,
|
|
24
|
+
width: res.width,
|
|
25
|
+
height: res.height,
|
|
26
|
+
depth: res.depth,
|
|
27
|
+
borderWidth: res.borderWidth,
|
|
28
|
+
root: res.windowid
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether this connection's allocator handed out `id`. Owning an adopted
|
|
34
|
+
* pixmap does not always mean the id was ours: XCompositeNameWindowPixmap
|
|
35
|
+
* names a pixmap into an id the adopter allocated itself, but a pixmap made
|
|
36
|
+
* by another client carries an id from that client's range. Only an id of
|
|
37
|
+
* ours may go back into the pool on destroy — releasing a foreign one would
|
|
38
|
+
* eventually make AllocID hand out an id the server refuses as not ours.
|
|
39
|
+
*/
|
|
40
|
+
function ownAllocation(display, id) {
|
|
41
|
+
return ((id & ~display.resource_mask) >>> 0) === (display.resource_base >>> 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
13
44
|
export default class Pixmap extends Drawable {
|
|
14
45
|
constructor(app, args = {}) {
|
|
15
46
|
super();
|
|
@@ -18,29 +49,157 @@ export default class Pixmap extends Drawable {
|
|
|
18
49
|
this.X = X;
|
|
19
50
|
this.display = app.display;
|
|
20
51
|
|
|
21
|
-
|
|
22
|
-
|
|
52
|
+
// A pixmap has no visual of its own — X gives it a depth and nothing
|
|
53
|
+
// else. What its pixels mean is decided by whoever put them there, so a
|
|
54
|
+
// pixmap holding a window's content (a backing store, a compositor's
|
|
55
|
+
// NameWindowPixmap) is told which visual that was: it is what names the
|
|
56
|
+
// picture format the pixels can be read through (issue #295). Left 0,
|
|
57
|
+
// the format is picked from the depth as before.
|
|
58
|
+
this.visualId = args.visual ?? 0;
|
|
59
|
+
|
|
60
|
+
// `pixmap.ready` (see the getter): resolved as soon as this wrapper
|
|
61
|
+
// knows its geometry — at the end of the constructor for a pixmap ntk
|
|
62
|
+
// created or one adopted with its geometry declared, when GetGeometry
|
|
63
|
+
// replies for one adopted by bare id
|
|
64
|
+
this._readyPromiseResolve = null;
|
|
65
|
+
this._readyPromise = new Promise((resolve) => {
|
|
66
|
+
this._readyPromiseResolve = resolve;
|
|
67
|
+
});
|
|
68
|
+
this._adoptError = null;
|
|
23
69
|
|
|
24
70
|
if (!args.id) {
|
|
71
|
+
const parentId = args.parent ? args.parent.id : app.display.screen[0].root;
|
|
72
|
+
this.depth = args.depth || 24;
|
|
25
73
|
this.id = X.AllocID();
|
|
26
74
|
X.CreatePixmap(this.id, parentId, this.depth, args.width, args.height);
|
|
27
75
|
this.width = args.width;
|
|
28
76
|
this.height = args.height;
|
|
29
77
|
this._owned = true;
|
|
30
|
-
|
|
78
|
+
this._releaseId = true;
|
|
79
|
+
registry.register(this, { X, id: this.id, releaseId: true }, this);
|
|
80
|
+
this._readyPromiseResolve(this);
|
|
31
81
|
} else {
|
|
32
82
|
this.id = args.id;
|
|
33
|
-
|
|
83
|
+
// Nothing is defaulted for an adopted pixmap: what the caller declared
|
|
84
|
+
// is recorded, and what they did not is asked of the server rather
|
|
85
|
+
// than guessed — a depth invented here would pick the picture format
|
|
86
|
+
// everything drawing on it reads the pixels through (issue #291).
|
|
87
|
+
this.width = args.width;
|
|
88
|
+
this.height = args.height;
|
|
89
|
+
this.depth = args.depth;
|
|
90
|
+
this._owned = !!args.own;
|
|
91
|
+
this._releaseId = this._owned && ownAllocation(this.display, this.id);
|
|
92
|
+
if (this._owned) {
|
|
93
|
+
registry.register(this, { X, id: this.id, releaseId: this._releaseId }, this);
|
|
94
|
+
}
|
|
95
|
+
if (this.width !== undefined && this.height !== undefined && this.depth !== undefined) {
|
|
96
|
+
this._readyPromiseResolve(this);
|
|
97
|
+
} else {
|
|
98
|
+
X.GetGeometry(this.id, (err, res) => {
|
|
99
|
+
// A pixmap freed between the id reaching us and this request
|
|
100
|
+
// reaching the server answers BadDrawable, and there is nothing to
|
|
101
|
+
// record. `ready` still resolves — a wait that never ends is worse
|
|
102
|
+
// than one that ends with `width` undefined — and `Pixmap.adopt`
|
|
103
|
+
// is the form that turns this into a rejection.
|
|
104
|
+
if (err) this._adoptError = err;
|
|
105
|
+
else this._applyGeometry(unpackGeometry(res));
|
|
106
|
+
this._readyPromiseResolve(this);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Extension events name their drawable, and a DAMAGE object can watch a
|
|
112
|
+
// pixmap, so routing (see App#_routeExtensionEvents) looks the target up
|
|
113
|
+
// in `X.event_consumers` — where windows already live. A pixmap enrols
|
|
114
|
+
// only when someone listens, because the entry is a strong reference:
|
|
115
|
+
// enrolling every pixmap would pin them all past the GC fallback above.
|
|
116
|
+
// A listening pixmap therefore stays until destroy(), which is also what
|
|
117
|
+
// removes it from the table.
|
|
118
|
+
this.on('newListener', (name) => {
|
|
119
|
+
if (extensionEventNames.has(name) && X.event_consumers) {
|
|
120
|
+
X.event_consumers[this.id] = this;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Adopt an existing pixmap id: ask the server for its geometry and depth,
|
|
127
|
+
* and take responsibility for freeing it. This is the compositor's case —
|
|
128
|
+
* XCompositeNameWindowPixmap hands back a pixmap the adopting client must
|
|
129
|
+
* FreePixmap, and must re-adopt on every resize of the named window — but
|
|
130
|
+
* any handover of a pixmap id works the same way.
|
|
131
|
+
*
|
|
132
|
+
* Rejects if the pixmap does not exist (already freed — for a compositor,
|
|
133
|
+
* re-name the window and adopt the fresh id). Pass `own: false` to observe
|
|
134
|
+
* a pixmap that stays another client's to free, `visual` to name the
|
|
135
|
+
* visual its pixels are laid out in (see the constructor), and any of
|
|
136
|
+
* `width`/`height`/`depth` already known — with all three declared no
|
|
137
|
+
* round trip is made.
|
|
138
|
+
*/
|
|
139
|
+
static async adopt(app, id, { own = true, ...args } = {}) {
|
|
140
|
+
const pixmap = new Pixmap(app, { ...args, id, own });
|
|
141
|
+
await pixmap.ready;
|
|
142
|
+
if (pixmap._adoptError) {
|
|
143
|
+
// there is nothing server-side to own, so the GC fallback must not
|
|
144
|
+
// send a FreePixmap of its own to fail the same way
|
|
145
|
+
pixmap._owned = false;
|
|
146
|
+
registry.unregister(pixmap);
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Pixmap.adopt: pixmap 0x${id.toString(16)} does not exist — it was freed ` +
|
|
149
|
+
'between its id being obtained and this request. A compositor sees this ' +
|
|
150
|
+
'when the named window was resized or destroyed: name it again and adopt ' +
|
|
151
|
+
'the fresh id.',
|
|
152
|
+
{ cause: pixmap._adoptError }
|
|
153
|
+
);
|
|
34
154
|
}
|
|
155
|
+
return pixmap;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolves with this pixmap once its geometry and depth are known —
|
|
160
|
+
* immediately for a pixmap ntk created or one adopted with `width`,
|
|
161
|
+
* `height` and `depth` all declared, when the GetGeometry sent by the
|
|
162
|
+
* constructor replies for one adopted by bare id. Never rejects: a pixmap
|
|
163
|
+
* that was already gone resolves with `width` still `undefined`.
|
|
164
|
+
*/
|
|
165
|
+
get ready() {
|
|
166
|
+
return this._readyPromise;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Ask the server about this pixmap now — `{ x, y, width, height, depth,
|
|
171
|
+
* borderWidth, root }`, the same shape `wnd.getGeometry()` resolves with
|
|
172
|
+
* (`x`, `y` and `borderWidth` are 0 for a pixmap). The reply is written
|
|
173
|
+
* back to `width`/`height`/`depth`, and resolves `ready` if it was still
|
|
174
|
+
* pending.
|
|
175
|
+
*/
|
|
176
|
+
getGeometry() {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
this.X.GetGeometry(this.id, (err, res) => {
|
|
179
|
+
if (err) return reject(err);
|
|
180
|
+
const geometry = unpackGeometry(res);
|
|
181
|
+
this._applyGeometry(geometry);
|
|
182
|
+
resolve(geometry);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Record a GetGeometry reply, and let anything waiting on it go. */
|
|
188
|
+
_applyGeometry({ width, height, depth }) {
|
|
189
|
+
this.width = width;
|
|
190
|
+
this.height = height;
|
|
191
|
+
this.depth = depth;
|
|
192
|
+
this._readyPromiseResolve(this);
|
|
35
193
|
}
|
|
36
194
|
|
|
37
195
|
destroy() {
|
|
196
|
+
if (this.X.event_consumers?.[this.id] === this) delete this.X.event_consumers[this.id];
|
|
38
197
|
if (!this._owned) return;
|
|
39
198
|
this._owned = false;
|
|
40
199
|
registry.unregister(this);
|
|
41
200
|
safeRelease(this.X, () => {
|
|
42
201
|
this.X.FreePixmap(this.id);
|
|
43
|
-
this.X.ReleaseID(this.id);
|
|
202
|
+
if (this._releaseId) this.X.ReleaseID(this.id);
|
|
44
203
|
});
|
|
45
204
|
}
|
|
46
205
|
|
package/lib/region.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { safeRelease } from './cleanup.js';
|
|
2
|
+
|
|
3
|
+
export const REGION_DOCS = 'https://github.com/sidorares/ntk/blob/master/docs/context-2d.md#region-clips';
|
|
4
|
+
|
|
5
|
+
const registry = new FinalizationRegistry(({ fixes, X, id }) => {
|
|
6
|
+
safeRelease(X, () => {
|
|
7
|
+
fixes.DestroyRegion(id);
|
|
8
|
+
X.ReleaseID(id);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One rectangle in the form XFIXES wants, from either spelling.
|
|
14
|
+
*
|
|
15
|
+
* ntk's own boxes are `{ x, y, w, h }` — that is what `_clipRect()` and the
|
|
16
|
+
* damage rectangles a window reports look like — and the protocol's are
|
|
17
|
+
* `{ x, y, width, height }`. Taking both means a region can be built out of
|
|
18
|
+
* boxes ntk handed the caller without a translation step whose only job is
|
|
19
|
+
* to rename two fields.
|
|
20
|
+
*/
|
|
21
|
+
function toRect(r) {
|
|
22
|
+
const width = r.width ?? r.w;
|
|
23
|
+
const height = r.height ?? r.h;
|
|
24
|
+
if (!Number.isFinite(r.x) || !Number.isFinite(r.y) || !Number.isFinite(width) || !Number.isFinite(height)) {
|
|
25
|
+
throw new TypeError(`ntk: region rectangle needs finite x/y/width/height, got ${JSON.stringify(r)}`);
|
|
26
|
+
}
|
|
27
|
+
return { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(width), height: Math.round(height) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const toRects = (rects) => (rects || []).map(toRect);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The id of an XFIXES region, given a `Region`, a raw id, or anything else
|
|
34
|
+
* carrying one (`{ id }`). Regions are ids on the wire, so a caller who made
|
|
35
|
+
* one through node-x11 directly can still hand it to `ctx.clipRegion()`.
|
|
36
|
+
*/
|
|
37
|
+
export function regionId(value) {
|
|
38
|
+
const id = typeof value === 'number' ? value : value?.id;
|
|
39
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
40
|
+
throw new TypeError(
|
|
41
|
+
`ntk: expected an XFIXES region (app.createRegion(...)) or its id, got ${JSON.stringify(value)}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A server-side XFIXES region: a set of rectangles the X server keeps and
|
|
49
|
+
* combines for you, with no round trip per operation.
|
|
50
|
+
*
|
|
51
|
+
* Regions are how X describes a non-rectangular area — damage from an
|
|
52
|
+
* expose, a window's SHAPE, the area a compositor has left to paint after
|
|
53
|
+
* subtracting the windows in front. `ctx.clipRegion(region)` clips a 2d
|
|
54
|
+
* context to one (docs/context-2d.md).
|
|
55
|
+
*
|
|
56
|
+
* Build one with `await app.createRegion(rects)`; the await is the XFIXES
|
|
57
|
+
* extension being loaded on first use, not a round trip per region.
|
|
58
|
+
* Everything after that is asynchronous in the X sense — requests go out and
|
|
59
|
+
* nothing waits — except `fetch()`, which is a reply.
|
|
60
|
+
*/
|
|
61
|
+
export default class Region {
|
|
62
|
+
/**
|
|
63
|
+
* Not called directly: `app.createRegion(rects)` loads XFIXES first and is
|
|
64
|
+
* the supported way in.
|
|
65
|
+
*
|
|
66
|
+
* @param {App} app
|
|
67
|
+
* @param {object} fixes the XFIXES extension, as `app.fixes()` resolves it
|
|
68
|
+
* @param {Array<object>} [rects] `{ x, y, width, height }` or `{ x, y, w, h }`
|
|
69
|
+
*/
|
|
70
|
+
constructor(app, fixes, rects = []) {
|
|
71
|
+
this.app = app;
|
|
72
|
+
this.X = app.X;
|
|
73
|
+
this.fixes = fixes;
|
|
74
|
+
this.id = this.X.AllocID();
|
|
75
|
+
this.fixes.CreateRegion(this.id, toRects(rects));
|
|
76
|
+
this._owned = true;
|
|
77
|
+
registry.register(this, { fixes, X: this.X, id: this.id }, this);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Replace the contents with this rectangle list. */
|
|
81
|
+
set(rects) {
|
|
82
|
+
this.fixes.SetRegion(this.id, toRects(rects));
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Replace the contents with another region's. */
|
|
87
|
+
copyFrom(other) {
|
|
88
|
+
this.fixes.CopyRegion(regionId(other), this.id);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Move by (dx, dy). */
|
|
93
|
+
translate(dx, dy) {
|
|
94
|
+
this.fixes.TranslateRegion(this.id, Math.round(dx), Math.round(dy));
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Keep only what is also in `other`. In place. */
|
|
99
|
+
intersect(other) {
|
|
100
|
+
this.fixes.IntersectRegion(this.id, regionId(other), this.id);
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Add everything in `other`. In place. */
|
|
105
|
+
union(other) {
|
|
106
|
+
this.fixes.UnionRegion(this.id, regionId(other), this.id);
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Remove everything in `other`. In place — and the operation a compositor
|
|
112
|
+
* runs once per window, painting front to back and taking each window's
|
|
113
|
+
* shape out of what is left for the ones behind it.
|
|
114
|
+
*/
|
|
115
|
+
subtract(other) {
|
|
116
|
+
this.fixes.SubtractRegion(this.id, regionId(other), this.id);
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Read the region back: `{ extents, rectangles }`, each rectangle
|
|
122
|
+
* `{ x, y, width, height }`. The one round trip in the class — regions are
|
|
123
|
+
* meant to be combined server-side, so reach for this to inspect or to
|
|
124
|
+
* test, not inside a paint loop.
|
|
125
|
+
*
|
|
126
|
+
* @returns {Promise<{extents: object, rectangles: Array<object>}>}
|
|
127
|
+
*/
|
|
128
|
+
fetch() {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
this.fixes.FetchRegion(this.id, (err, region) => (err ? reject(err) : resolve(region)));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Free the region server-side. Idempotent. */
|
|
135
|
+
destroy() {
|
|
136
|
+
if (!this._owned) return;
|
|
137
|
+
this._owned = false;
|
|
138
|
+
registry.unregister(this);
|
|
139
|
+
safeRelease(this.X, () => {
|
|
140
|
+
this.fixes.DestroyRegion(this.id);
|
|
141
|
+
this.X.ReleaseID(this.id);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
[Symbol.dispose]() {
|
|
146
|
+
this.destroy();
|
|
147
|
+
}
|
|
148
|
+
}
|