ntk 3.5.3 → 3.7.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/README.md CHANGED
@@ -132,10 +132,12 @@ Note that on many systems indirect GLX is disabled by default —
132
132
  import { createClient } from 'ntk';
133
133
 
134
134
  const app = await createClient();
135
- const wnd = app.createWindow({ width: 300, height: 300 });
135
+ // GLX drawables need a GLX-capable visual, chosen before the window exists
136
+ const glx = await app.chooseGLXConfig({ DEPTH_SIZE: 24 });
137
+ const wnd = app.createWindow({ width: 300, height: 300, visual: glx.visual, depth: glx.depth });
136
138
  wnd.map();
137
139
 
138
- const gl = wnd.getContext('opengl');
140
+ const gl = wnd.getContext('opengl', glx);
139
141
  gl.ClearColor(0.3, 0.3, 0.3, 0.0);
140
142
  gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
141
143
  gl.Begin(gl.TRIANGLES);
package/lib/app.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import Clipboard from './clipboard.js';
2
2
  import { CursorCache } from './cursor.js';
3
+ import { chooseGLXConfig } from './glx.js';
3
4
  import Pixmap from './pixmap.js';
4
5
  import FontManager from './text/fontmanager.js';
5
6
  import Window from './window.js';
@@ -13,7 +14,7 @@ export default class App {
13
14
  * @param {object} display node-x11 display
14
15
  * @param {object} [options] environment hooks: { fontSource (see
15
16
  * text/fontsource.js), glxVisual (visual id for getContext('opengl')
16
- * when glxinfo can't be shelled out to) }
17
+ * instead of querying the server for one) }
17
18
  */
18
19
  constructor(display, options = {}) {
19
20
  this.display = display;
@@ -56,6 +57,30 @@ export default class App {
56
57
  return new Window(this, args);
57
58
  }
58
59
 
60
+ /**
61
+ * Allocate a colormap for `visual` (X CreateColormap, alloc None — the
62
+ * only legal value for the TrueColor visuals we use). Windows created
63
+ * with an explicit `visual` get one of these automatically.
64
+ * @returns {number} the colormap id
65
+ */
66
+ createColormap(visual, screen = 0) {
67
+ const mid = this.X.AllocID();
68
+ this.X.CreateColormap(mid, this.display.screen[screen].root, visual, 0);
69
+ return mid;
70
+ }
71
+
72
+ /**
73
+ * Pick a GLX-capable visual (docs/context-opengl.md). Resolves with
74
+ * `{ visual, depth, class, doubleBuffer, depthSize, fbconfig, screen }` —
75
+ * pass `visual`/`depth` to `createWindow` and the whole object to
76
+ * `wnd.getContext('opengl', config)`.
77
+ * @param {object} [spec] GLX attributes, e.g.
78
+ * `{ DEPTH_SIZE: 24, DOUBLEBUFFER: true, ALPHA_SIZE: 8 }`
79
+ */
80
+ chooseGLXConfig(spec) {
81
+ return chooseGLXConfig(this, spec);
82
+ }
83
+
59
84
  rootWindow() {
60
85
  return new Window(this, { id: this.display.screen[0].root });
61
86
  }
package/lib/events_map.js CHANGED
@@ -9,6 +9,10 @@ eventName[5] = 'mouseup';
9
9
  eventName[6] = 'mousemove';
10
10
  eventName[7] = 'mouseover';
11
11
  eventName[8] = 'mouseout';
12
+ // FocusIn/FocusOut: keyboard focus arriving at or leaving this window,
13
+ // usually because the window manager moved it
14
+ eventName[9] = 'focus';
15
+ eventName[10] = 'blur';
12
16
  eventName[12] = 'expose';
13
17
  eventName[17] = 'destroy';
14
18
  eventName[18] = 'unmap';
@@ -40,6 +44,8 @@ export const mask = {
40
44
  resize: x11.eventMask.StructureNotify,
41
45
  reparent: x11.eventMask.StructureNotify,
42
46
  keydown: x11.eventMask.KeyPress,
47
+ focus: x11.eventMask.FocusChange,
48
+ blur: x11.eventMask.FocusChange,
43
49
  expose: x11.eventMask.Exposure,
44
50
  map_request: x11.eventMask.SubstructureRedirect,
45
51
  configure_request: x11.eventMask.SubstructureRedirect,
@@ -74,6 +80,8 @@ export const toSnake = {
74
80
  onResize: 'resize',
75
81
  onReparent: 'reparent',
76
82
  onKeyDown: 'keydown',
83
+ onFocus: 'focus',
84
+ onBlur: 'blur',
77
85
  onExpose: 'expose',
78
86
  onMapRequest: 'map_request',
79
87
  onConfigureRequest: 'configure_request',
package/lib/glx.js ADDED
@@ -0,0 +1,195 @@
1
+ // GLX visual/fbconfig discovery.
2
+ //
3
+ // A GLX drawable has to use a visual the GL context was created for, so the
4
+ // visual must be known *before* CreateWindow. This module asks the server
5
+ // (GetFBConfigs, falling back to the GLX 1.2 GetVisualConfigs) instead of
6
+ // shelling out to `glxinfo`, so it works headlessly and in CI.
7
+
8
+ // defaults applied on top of the caller's spec; `null` means "don't care"
9
+ const DEFAULT_SPEC = {
10
+ DOUBLEBUFFER: true,
11
+ DEPTH_SIZE: 16,
12
+ RED_SIZE: 8,
13
+ GREEN_SIZE: 8,
14
+ BLUE_SIZE: 8
15
+ };
16
+
17
+ // GLX 1.2 GetVisualConfigs property names for the attributes we filter on
18
+ const legacyProps = {
19
+ DOUBLEBUFFER: 'doubleBufferMode',
20
+ STEREO: 'stereoMode',
21
+ BUFFER_SIZE: 'rgbBits',
22
+ DEPTH_SIZE: 'depthBits',
23
+ STENCIL_SIZE: 'stencilBits',
24
+ RED_SIZE: 'redBits',
25
+ GREEN_SIZE: 'greenBits',
26
+ BLUE_SIZE: 'blueBits',
27
+ ALPHA_SIZE: 'alphaBits',
28
+ AUX_BUFFERS: 'numAuxBuffers',
29
+ LEVEL: 'level'
30
+ };
31
+
32
+ // attributes compared as "at least this much"; the rest must match exactly
33
+ const minimums = new Set([
34
+ 'BUFFER_SIZE',
35
+ 'DEPTH_SIZE',
36
+ 'STENCIL_SIZE',
37
+ 'RED_SIZE',
38
+ 'GREEN_SIZE',
39
+ 'BLUE_SIZE',
40
+ 'ALPHA_SIZE',
41
+ 'AUX_BUFFERS',
42
+ 'SAMPLE_BUFFERS',
43
+ 'SAMPLES'
44
+ ]);
45
+
46
+ function toNumber(value) {
47
+ if (typeof value === 'boolean') return value ? 1 : 0;
48
+ return value;
49
+ }
50
+
51
+ // the X depth and visual info for a visual id, or null if the screen has no
52
+ // such visual (fbconfigs with VISUAL_ID 0 are pbuffer/pixmap only)
53
+ function visualInfo(screen, visualId) {
54
+ if (!visualId) return null;
55
+ for (const depth in screen.depths) {
56
+ const visual = screen.depths[depth][visualId];
57
+ if (visual) return { depth: Number(depth), visual };
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function describe(spec) {
63
+ return (
64
+ Object.entries(spec)
65
+ .filter(([, v]) => v !== null && v !== undefined)
66
+ .map(([k, v]) => `${k}=${v}`)
67
+ .join(' ') || '(no constraints)'
68
+ );
69
+ }
70
+
71
+ function fbConfigs(GLX, screen, spec) {
72
+ return new Promise((resolve, reject) => {
73
+ // ChooseFBConfig filters and sorts per the GLX 1.4 selection rules
74
+ GLX.ChooseFBConfig(screen, spec, (err, configs) => {
75
+ // a bad attribute name is the caller's bug, not a missing feature —
76
+ // everything else means "this server has no fbconfigs for us"
77
+ if (err instanceof TypeError) return reject(err);
78
+ resolve(err ? [] : configs);
79
+ });
80
+ });
81
+ }
82
+
83
+ function visualConfigs(GLX, screen) {
84
+ return new Promise((resolve) => {
85
+ GLX.GetVisualConfigs(screen, (err, configs) => resolve(err ? [] : configs));
86
+ });
87
+ }
88
+
89
+ // GLX 1.2 fallback: filter GetVisualConfigs results by the same spec
90
+ function matchLegacy(configs, spec) {
91
+ return configs.filter((cfg) => {
92
+ if (!cfg.rgbMode) return false;
93
+ for (const key in spec) {
94
+ const want = toNumber(spec[key]);
95
+ if (want === null || want === undefined) continue;
96
+ const prop = legacyProps[key];
97
+ // attributes GetVisualConfigs doesn't report (RENDER_TYPE,
98
+ // DRAWABLE_TYPE, ...) can't be checked — RGBA windows are all this
99
+ // path can offer anyway
100
+ if (!prop) continue;
101
+ const have = cfg[prop] || 0;
102
+ if (minimums.has(key) ? have < want : have !== want) return false;
103
+ }
104
+ return true;
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Choose a GLX-capable visual on `app`'s display.
110
+ *
111
+ * @param {import('./app.js').default} app
112
+ * @param {object} [spec] GLX attribute constraints by name (see
113
+ * `GLX.glxAttrib`), e.g. `{ DEPTH_SIZE: 24, DOUBLEBUFFER: true }`;
114
+ * `null` means "don't care". `screen` picks the X screen (default 0),
115
+ * `visual` pins a specific visual id and skips the search.
116
+ * @returns {Promise<{visual: number, depth: number, class: number,
117
+ * doubleBuffer: boolean, depthSize: number, fbconfig: number|null,
118
+ * config: object}>}
119
+ */
120
+ export async function chooseGLXConfig(app, spec = {}) {
121
+ const GLX = app.display.GLX;
122
+ if (!GLX) {
123
+ throw new Error(
124
+ 'GLX: extension not available on this display — indirect GLX is often disabled (X server flag +iglx / AllowIndirectGLX)'
125
+ );
126
+ }
127
+
128
+ const { screen: screenNum = 0, preferVisual, ...wanted } = spec;
129
+ const want = { ...DEFAULT_SPEC, ...wanted };
130
+ const screen = app.display.screen[screenNum];
131
+ // a candidate list is searched for `preferVisual` first: a context must
132
+ // be created for the visual its drawable already has
133
+ const pick = (candidates) =>
134
+ (preferVisual && candidates.find((c) => c.visual === preferVisual)) || candidates[0];
135
+
136
+ if (want.visual) {
137
+ const info = visualInfo(screen, want.visual);
138
+ if (!info) throw new Error(`GLX: visual 0x${want.visual.toString(16)} is not on screen ${screenNum}`);
139
+ return {
140
+ visual: want.visual,
141
+ depth: info.depth,
142
+ class: info.visual.class,
143
+ doubleBuffer: !!toNumber(want.DOUBLEBUFFER),
144
+ depthSize: toNumber(want.DEPTH_SIZE) || 0,
145
+ screen: screenNum,
146
+ fbconfig: null,
147
+ config: {}
148
+ };
149
+ }
150
+
151
+ // GLX >= 1.3 fbconfigs first; they carry the attributes we care about
152
+ const fromFbConfigs = [];
153
+ for (const cfg of await fbConfigs(GLX, screenNum, want)) {
154
+ const info = visualInfo(screen, cfg.VISUAL_ID);
155
+ if (!info) continue; // no X visual: pbuffer/pixmap-only config
156
+ fromFbConfigs.push({
157
+ visual: cfg.VISUAL_ID,
158
+ depth: info.depth,
159
+ class: info.visual.class,
160
+ doubleBuffer: !!cfg.DOUBLEBUFFER,
161
+ depthSize: cfg.DEPTH_SIZE || 0,
162
+ screen: screenNum,
163
+ fbconfig: cfg.FBCONFIG_ID,
164
+ config: cfg
165
+ });
166
+ }
167
+ const best = pick(fromFbConfigs);
168
+ if (best) return best;
169
+
170
+ // GLX 1.2 servers (and servers that report no usable fbconfig)
171
+ const legacy = await visualConfigs(GLX, screenNum);
172
+ const fromVisuals = [];
173
+ for (const cfg of matchLegacy(legacy, want)) {
174
+ const info = visualInfo(screen, cfg.visualID);
175
+ if (!info) continue;
176
+ fromVisuals.push({
177
+ visual: cfg.visualID,
178
+ depth: info.depth,
179
+ class: info.visual.class,
180
+ doubleBuffer: !!cfg.doubleBufferMode,
181
+ depthSize: cfg.depthBits || 0,
182
+ screen: screenNum,
183
+ fbconfig: null,
184
+ config: cfg
185
+ });
186
+ }
187
+ const legacyBest = pick(fromVisuals);
188
+ if (legacyBest) return legacyBest;
189
+
190
+ throw new Error(
191
+ `GLX: no visual on screen ${screenNum} matches ${describe(want)} (server offered ${legacy.length} GLX visuals)`
192
+ );
193
+ }
194
+
195
+ export default chooseGLXConfig;
package/lib/index.js CHANGED
@@ -22,6 +22,7 @@ import SvgView from './widgets/svgview.js';
22
22
  import MarkdownView from './widgets/markdownview.js';
23
23
  import TexView, { configureTex, layoutTex, TexBox } from './widgets/tex.js';
24
24
  import { tokenize as highlightCode } from './widgets/highlight.js';
25
+ import { cssColor, cssLength } from './widgets/css.js';
25
26
 
26
27
  // rendering context modules register themselves on Drawable
27
28
  import './renderingcontext_x11.js';
@@ -40,8 +41,8 @@ import './renderingcontext_opengl.js';
40
41
  * @param {object} [options] passed through to x11.createClient
41
42
  * (e.g. { display: ':1' }); ntk additionally understands
42
43
  * { fontSource } — a pluggable system-font lookup (see docs/fonts.md) —
43
- * { glxVisual } — the visual id for getContext('opengl') in
44
- * environments where `glxinfo` cannot be shelled out to — and
44
+ * { glxVisual } — a visual id for getContext('opengl') to use instead of
45
+ * querying the server for one — and
45
46
  * { onXError } — called with X protocol errors no request callback
46
47
  * claimed (default: console.warn)
47
48
  * @param {function} [callback] optional node-style callback
@@ -126,7 +127,9 @@ export {
126
127
  TexBox,
127
128
  layoutTex,
128
129
  configureTex,
129
- highlightCode
130
+ highlightCode,
131
+ cssColor,
132
+ cssLength
130
133
  };
131
134
  // the yoga-layout instance ntk lays HtmlView out with — downstream layout
132
135
  // consumers (e.g. the react-x11 renderer) must share it to avoid loading a
@@ -1,53 +1,143 @@
1
1
  import Drawable from './drawable.js';
2
+ import { chooseGLXConfig } from './glx.js';
2
3
 
3
4
  // Indirect GLX rendering context exposing an OpenGL 1.4-style api.
4
5
  // Note: many modern X servers ship with indirect GLX disabled — see README.
6
+ //
7
+ // Setting up a context is asynchronous by protocol: the visual has to be
8
+ // discovered (GetFBConfigs) and, above all, GLX Render requests carry the
9
+ // *context tag* the server returns from MakeCurrent — not the context XID.
10
+ // The api stays synchronous by queueing GL calls until the tag arrives and
11
+ // replaying them in order; `await gl.ready` waits for the setup instead.
5
12
  class RenderingContextOpenGL {
6
- constructor(window, visual) {
7
- // TODO: use glx api to query the best visual instead of shelling out
8
- if (typeof visual === 'undefined') visual = window.app?.options?.glxVisual;
9
- if (typeof visual === 'undefined') {
10
- // lazy builtin lookup: browser bundles must not depend on child_process
11
- const cp = globalThis.process?.getBuiltinModule?.('node:child_process');
12
- if (!cp) {
13
- throw new Error(
14
- "getContext('opengl'): no visual id — pass one explicitly or via createClient({ glxVisual }) when glxinfo cannot be run"
15
- );
16
- }
17
- visual = parseInt(cp.execSync('glxinfo -i -b').toString(), 10);
18
- }
19
-
13
+ constructor(window, config) {
20
14
  const X = window.X;
21
15
  const GLX = window.display.GLX;
22
- this.window = window;
16
+ if (!GLX) {
17
+ throw new Error(
18
+ "getContext('opengl'): the X server has no GLX extension — indirect GLX is often disabled (+iglx / AllowIndirectGLX)"
19
+ );
20
+ }
23
21
 
24
- const ctx = X.AllocID();
25
- GLX.CreateContext(ctx, visual, 0, 0, 0);
26
- GLX.MakeCurrent(window.id, ctx, 0, () => {});
22
+ this.window = window;
23
+ this.X = X;
24
+ this.GLX = GLX;
25
+ /** the MakeCurrent context tag every Render request carries; 0 until ready */
26
+ this.contextTag = 0;
27
+ /** the GLX context XID */
28
+ this.contextId = 0;
29
+ /** the config chosen (or passed in), see app.chooseGLXConfig */
30
+ this.config = null;
31
+ this.visual = 0;
32
+ this.error = null;
27
33
 
28
- const gl = GLX.renderPipeline(ctx);
29
- for (const key in gl) {
30
- if (key === 'SwapBuffers') continue;
31
- const val = gl[key];
32
- this[key] = typeof val === 'function' ? val.bind(gl) : val;
34
+ // GL commands land here until the tag is known, then in the real
35
+ // pipeline. renderPipeline() issues no traffic, so a throwaway one is a
36
+ // safe source of the method names and GL constants.
37
+ let pipeline = null;
38
+ const queue = [];
39
+ const template = GLX.renderPipeline(0);
40
+ for (const key in template) {
41
+ if (typeof template[key] !== 'function') {
42
+ this[key] = template[key];
43
+ continue;
44
+ }
45
+ this[key] = (...args) => {
46
+ if (pipeline) return pipeline[key](...args);
47
+ queue.push([key, args]);
48
+ };
33
49
  }
34
50
 
35
- const windowId = window.id;
36
- this.SwapBuffers = () => gl.SwapBuffers.call(gl, windowId);
51
+ // SwapBuffers swaps the owning window
52
+ const swapBuffers = this.SwapBuffers;
53
+ this.SwapBuffers = () => swapBuffers(window.id);
54
+
55
+ const afterReady = (fn) => {
56
+ if (this.contextTag) return fn();
57
+ this.ready.then(fn, () => {});
58
+ };
37
59
 
38
60
  this.CreateGLXPixmap = (pixmapId) => {
39
61
  const glxpixmapId = X.AllocID();
40
- GLX.CreateGLXPixmap(0, visual, pixmapId, glxpixmapId);
62
+ afterReady(() => GLX.CreateGLXPixmap(0, this.visual, pixmapId, glxpixmapId));
41
63
  return glxpixmapId;
42
64
  };
43
- this.BindTexImage = (pixmap, buffer) => {
44
- GLX.BindTexImage(ctx, pixmap, buffer);
65
+ this.BindTexImage = (pixmap, buffer, attribs) => {
66
+ afterReady(() => GLX.BindTexImage(this.contextTag, pixmap, buffer, attribs));
67
+ };
68
+ this.ReleaseTexImage = (pixmap, buffer) => {
69
+ afterReady(() => GLX.ReleaseTexImage(this.contextTag, pixmap, buffer));
45
70
  };
71
+
72
+ /** resolves with this context once it is current, rejects on failure */
73
+ this.ready = (async () => {
74
+ const cfg = await this._resolveConfig(config);
75
+ this.config = cfg;
76
+ this.visual = cfg.visual;
77
+ if (window.visual && window.visual !== cfg.visual) {
78
+ console.warn(
79
+ `ntk: getContext('opengl') is using visual 0x${cfg.visual.toString(16)} on a window created with visual 0x${window.visual.toString(16)} — create the window with the visual from app.chooseGLXConfig()`
80
+ );
81
+ }
82
+
83
+ const ctx = X.AllocID();
84
+ this.contextId = ctx;
85
+ GLX.CreateContext(ctx, cfg.visual, cfg.screen ?? 0, 0, 0);
86
+ const tag = await new Promise((resolve, reject) => {
87
+ GLX.MakeCurrent(window.id, ctx, 0, (err, contextTag) => (err ? reject(err) : resolve(contextTag)));
88
+ });
89
+
90
+ this.contextTag = tag;
91
+ pipeline = GLX.renderPipeline(tag);
92
+ for (const [key, args] of queue) pipeline[key](...args);
93
+ queue.length = 0;
94
+ return this;
95
+ })();
96
+
97
+ this.ready.catch((err) => {
98
+ this.error = err;
99
+ queue.length = 0;
100
+ console.warn(
101
+ `ntk: getContext('opengl') failed: ${err.message} (await gl.ready to handle this yourself)`
102
+ );
103
+ });
104
+ }
105
+
106
+ // an explicit config object or visual id wins; otherwise ask the server
107
+ // for one, preferring a config for the visual the window already has
108
+ async _resolveConfig(config) {
109
+ if (config && typeof config === 'object') {
110
+ if (!config.visual)
111
+ throw new Error("getContext('opengl'): config has no visual — use app.chooseGLXConfig()");
112
+ return config;
113
+ }
114
+ const app = this.window.app;
115
+ const visual = config ?? app?.options?.glxVisual;
116
+ if (visual) return chooseGLXConfig(app, { visual });
117
+ const screen = app.display.screen[0];
118
+ return chooseGLXConfig(app, {
119
+ preferVisual: this.window.visual || screen.root_visual
120
+ });
121
+ }
122
+
123
+ /** release the context (the window keeps its drawable) */
124
+ destroy() {
125
+ if (!this.contextId) return;
126
+ const { GLX } = this;
127
+ const ctx = this.contextId;
128
+ const tag = this.contextTag;
129
+ this.contextId = 0;
130
+ this.contextTag = 0;
131
+ if (tag) GLX.MakeCurrent(0, 0, tag, () => {});
132
+ GLX.DestroyContext(ctx);
133
+ }
134
+
135
+ [Symbol.dispose]() {
136
+ this.destroy();
46
137
  }
47
138
  }
48
139
 
49
140
  // register context
50
- Drawable.renderingContextFactory['opengl'] = (window, visual) =>
51
- new RenderingContextOpenGL(window, visual);
141
+ Drawable.renderingContextFactory['opengl'] = (window, config) => new RenderingContextOpenGL(window, config);
52
142
 
53
143
  export default RenderingContextOpenGL;
package/lib/window.js CHANGED
@@ -15,6 +15,8 @@ import * as xevents from './events_map.js';
15
15
  // client-side double buffering (see docs/window.md) and predates this
16
16
  // list — it is a different concept from the X backing-store attribute,
17
17
  // which is therefore not forwarded to avoid a silent collision
18
+ // `visual`, `depth`, `windowClass` and `borderWidth` are CreateWindow header
19
+ // fields rather than attributes, and are handled separately below
18
20
  const forwardedXAttributes = [
19
21
  'backgroundPixmap',
20
22
  'backgroundPixel',
@@ -113,10 +115,29 @@ export default class Window extends Drawable {
113
115
  for (const name of forwardedXAttributes) {
114
116
  if (args[name] !== undefined) values[name] = args[name];
115
117
  }
116
- X.CreateWindow(this.id, parentId, this.x, this.y, this.width, this.height, 0, 0, 0, 0, values);
118
+ // a window on a non-default visual (GLX, ARGB) also needs its own
119
+ // colormap and an explicit border pixel: inheriting either from a
120
+ // parent of a different depth is a BadMatch (X11 CreateWindow)
121
+ this.visual = args.visual ?? 0;
122
+ this.depth = args.depth ?? 0;
123
+ this.windowClass = args.windowClass ?? 0;
124
+ if (this.visual) {
125
+ if (values.colormap === undefined) {
126
+ this._ownedColormap = app.createColormap(this.visual);
127
+ values.colormap = this._ownedColormap;
128
+ }
129
+ if (values.borderPixel === undefined && values.borderPixmap === undefined) values.borderPixel = 0;
130
+ }
131
+ const borderWidth = args.borderWidth ?? 0;
132
+ X.CreateWindow(
133
+ this.id, parentId, this.x, this.y, this.width, this.height,
134
+ borderWidth, this.depth, this.windowClass, this.visual, values
135
+ );
117
136
  } else {
118
137
  this.id = args.id;
119
138
  this.eventMask = 0;
139
+ this.visual = 0;
140
+ this.depth = 0;
120
141
  // populate width and height
121
142
  X.GetGeometry(this.id, (err, res) => {
122
143
  if (!err) {
@@ -124,6 +145,7 @@ export default class Window extends Drawable {
124
145
  this.height = res.height;
125
146
  this.x = res.xPos;
126
147
  this.y = res.yPos;
148
+ this.depth = res.depth;
127
149
  }
128
150
  this._readyPromiseResolve();
129
151
  });
@@ -293,7 +315,9 @@ export default class Window extends Drawable {
293
315
  parent: this,
294
316
  width: newW,
295
317
  height: newH,
296
- depth: this.display.screen[0].root_depth
318
+ // must match the window's depth: CopyArea between drawables of
319
+ // different depths is a BadMatch
320
+ depth: this.depth || this.display.screen[0].root_depth
297
321
  });
298
322
  if (!this._clearGc) {
299
323
  // reusable across reallocs: a GC is valid for any drawable of the
@@ -898,8 +922,77 @@ export default class Window extends Drawable {
898
922
  return this;
899
923
  }
900
924
 
901
- grabPointer() {
902
- this.X.GrabPointer(this.id, true, x11.eventMask.PointerMotion, 0, 1, 0, 0, 0);
925
+ /**
926
+ * Take the keyboard focus (X SetInputFocus). `revertTo` says where focus
927
+ * goes if this window becomes unviewable: 0 None, 1 PointerRoot,
928
+ * 2 Parent (the default, and the sane choice for a child window).
929
+ *
930
+ * Note a window manager may take focus back — the authority is the
931
+ * 'focus'/'blur' events, not the request.
932
+ */
933
+ focus(revertTo = 2) {
934
+ safeRelease(this.X, () => this.X.SetInputFocus(this.id, revertTo));
935
+ return this;
936
+ }
937
+
938
+ /** Which window the server currently gives keyboard input to. */
939
+ queryFocus(callback) {
940
+ this.X.GetInputFocus(callback);
941
+ return this;
942
+ }
943
+
944
+ /**
945
+ * Take a pointer grab (X GrabPointer). This is how menus work on X: while
946
+ * the grab is held, presses anywhere on the screen are reported to this
947
+ * window instead of going to the window under the pointer — including the
948
+ * window manager's frames — so a click outside can dismiss the menu.
949
+ *
950
+ * With `ownerEvents` (the default) events over the client's *own* windows
951
+ * are still delivered to them normally, so a submenu keeps working.
952
+ *
953
+ * @param {object} [options] { ownerEvents, events, cursor, confineTo, time }
954
+ * @param {function} [callback] cb(err, status) — 0 is Success, 1 is
955
+ * AlreadyGrabbed by another client
956
+ */
957
+ grabPointer(options = {}, callback) {
958
+ const {
959
+ ownerEvents = true,
960
+ events = x11.eventMask.ButtonPress | x11.eventMask.ButtonRelease | x11.eventMask.PointerMotion,
961
+ pointerMode = 1, // Asynchronous
962
+ keyboardMode = 1,
963
+ confineTo = 0,
964
+ cursor = 0,
965
+ time = 0 // CurrentTime
966
+ } = options;
967
+ this.X.GrabPointer(
968
+ this.id,
969
+ ownerEvents,
970
+ events,
971
+ pointerMode,
972
+ keyboardMode,
973
+ confineTo,
974
+ cursor,
975
+ time,
976
+ callback ?? (() => {})
977
+ );
978
+ return this;
979
+ }
980
+
981
+ ungrabPointer(time = 0) {
982
+ safeRelease(this.X, () => this.X.UngrabPointer(time));
983
+ return this;
984
+ }
985
+
986
+ /** Keyboard counterpart: keys go here while the grab is held. */
987
+ grabKeyboard(options = {}, callback) {
988
+ const { ownerEvents = true, pointerMode = 1, keyboardMode = 1, time = 0 } = options;
989
+ this.X.GrabKeyboard(this.id, ownerEvents, time, pointerMode, keyboardMode, callback ?? (() => {}));
990
+ return this;
991
+ }
992
+
993
+ ungrabKeyboard(time = 0) {
994
+ safeRelease(this.X, () => this.X.UngrabKeyboard(time));
995
+ return this;
903
996
  }
904
997
 
905
998
  reparentTo(newParent, x, y) {
@@ -974,6 +1067,11 @@ export default class Window extends Drawable {
974
1067
  this._backing = null;
975
1068
  }
976
1069
  safeRelease(this.X, () => this.X.DestroyWindow(this.id));
1070
+ if (this._ownedColormap) {
1071
+ const colormap = this._ownedColormap;
1072
+ this._ownedColormap = null;
1073
+ safeRelease(this.X, () => this.X.FreeColormap(colormap));
1074
+ }
977
1075
  }
978
1076
 
979
1077
  [Symbol.dispose]() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "3.5.3",
3
+ "version": "3.7.0",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",