ntk 7.3.3 → 7.5.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/xi2.js ADDED
@@ -0,0 +1,290 @@
1
+ /**
2
+ * XI2 (X Input Extension 2) device events: what to select, how to turn one
3
+ * into an ntk event, and the per-device bookkeeping a scroll delta needs.
4
+ *
5
+ * The core protocol flattens every input device onto one virtual pointer and
6
+ * one virtual keyboard, and reports a wheel as a click of button 4/5/6/7. A
7
+ * touchpad's two-finger scroll therefore reaches a core client as a series of
8
+ * whole notches, which is why nothing built on core X can scroll smoothly.
9
+ * XI2 carries the same gesture as a *valuator*: an absolute accumulator per
10
+ * scroll axis, whose difference between two events is the distance scrolled,
11
+ * in units of the axis' `increment` (one notch).
12
+ *
13
+ * Three facts shape everything here:
14
+ *
15
+ * 1. **Valuators are absolute.** A delta is `(now - before) / increment`, so
16
+ * the first event from a device has nothing to subtract from and must seed
17
+ * rather than report — otherwise the first scroll of a session jumps by
18
+ * however far the axis had travelled before the window existed.
19
+ * 2. **A device's axes are described once, not per event.** Which valuator is
20
+ * the vertical scroll axis, and what one notch of it is worth, comes from
21
+ * `XIQueryDevice`'s `Scroll` classes. They change under a master device
22
+ * when the user switches from a mouse to a touchpad, which is what
23
+ * `XIDeviceChanged` announces — and that also resets the accumulators.
24
+ * 3. **Selecting XI2 turns off core delivery.** The server delivers an event
25
+ * to a client once: an XI2 selection on a window suppresses the core
26
+ * events of the same types *for that client on that window*. So a window
27
+ * that selects XI2 Motion stops receiving core MotionNotify, and this
28
+ * module has to reproduce the core-shaped event ntk consumers already
29
+ * listen for.
30
+ */
31
+
32
+ /**
33
+ * XI2 event types ntk can deliver, and the ntk event each becomes.
34
+ *
35
+ * Only the types node-x11 decodes into fields are here. The rest —
36
+ * Enter/Leave, FocusIn/FocusOut, HierarchyChanged, the barriers — arrive with
37
+ * their bodies undecoded (`ev.data`), and selecting one would suppress the
38
+ * core event that *is* decoded in exchange for nothing.
39
+ */
40
+ export const xi2EventName = {
41
+ Motion: 'mousemove',
42
+ ButtonPress: 'mousedown',
43
+ ButtonRelease: 'mouseup',
44
+ KeyPress: 'keydown',
45
+ KeyRelease: 'keyup',
46
+ TouchBegin: 'touchstart',
47
+ TouchUpdate: 'touchmove',
48
+ TouchEnd: 'touchend'
49
+ };
50
+
51
+ /**
52
+ * What `xi2: true` selects: the pointer, which is what smooth scrolling
53
+ * needs. Keyboard and touch are opt-in by name — a window that selects XI2
54
+ * KeyPress takes on decoding it (and loses the core KeyPress it was getting).
55
+ */
56
+ export const DEFAULT_XI2_EVENTS = ['Motion', 'ButtonPress', 'ButtonRelease'];
57
+
58
+ // XIScrollClass.scrollType
59
+ const SCROLL_VERTICAL = 1;
60
+
61
+ // XIPointerEventFlags.PointerEmulated: this button event is the server's
62
+ // rendering of a scroll axis into a wheel click, for clients that cannot read
63
+ // valuators. Both halves of the scroll are sent, so a client that reads the
64
+ // axis has to drop this one or count every notch twice.
65
+ export const POINTER_EMULATED = 1 << 16;
66
+
67
+ /** Wheel buttons in the core protocol: up, down, left, right. */
68
+ export const WHEEL_BUTTONS = { 4: [0, -1], 5: [0, 1], 6: [-1, 0], 7: [1, 0] };
69
+
70
+ /**
71
+ * Check a list of XI2 event-type names, and say what a caller can ask for.
72
+ *
73
+ * The failure this catches is a silent one: an unknown name reaches
74
+ * `XISelectEvents` as `undefined` and throws from node-x11 naming a bit
75
+ * position, and a *known* name ntk cannot decode (`Enter`) would select
76
+ * happily and then deliver nothing anyone can read.
77
+ *
78
+ * @param {string[]|string} types
79
+ * @returns {string[]}
80
+ */
81
+ export function normalizeXI2Types(types) {
82
+ const list = typeof types === 'string' ? [types] : Array.from(types ?? []);
83
+ for (const name of list) {
84
+ if (xi2EventName[name]) continue;
85
+ const known = Object.keys(xi2EventName).join(', ');
86
+ throw new Error(
87
+ `ntk: selectXI2 cannot deliver '${name}'. Supported XI2 event types: ${known}. ` +
88
+ (name?.startsWith?.('Raw')
89
+ ? 'Raw events carry no window and are only sent for selections on the root, ' +
90
+ 'so they are not routed to a window — read them off app.X directly.'
91
+ : 'Other XI2 events arrive with their bodies undecoded, and selecting one ' +
92
+ 'would suppress the core event carrying the same information.')
93
+ );
94
+ }
95
+ return list;
96
+ }
97
+
98
+ /**
99
+ * The scroll axes of one device, from its `XIQueryDevice` classes:
100
+ * valuator number -> `{ vertical, increment }`.
101
+ *
102
+ * A device with no scroll class (a plain wheel mouse on an old driver, a
103
+ * tablet) yields an empty map, and everything below then leaves scrolling to
104
+ * the emulated buttons.
105
+ */
106
+ export function scrollAxes(device) {
107
+ const axes = new Map();
108
+ for (const cls of device?.classes ?? []) {
109
+ // ClassType.Scroll
110
+ if (cls.type !== 3) continue;
111
+ if (!cls.increment) continue; // an axis one of whose notches is 0 long
112
+ axes.set(cls.number, {
113
+ vertical: cls.scrollType === SCROLL_VERTICAL,
114
+ increment: cls.increment
115
+ });
116
+ }
117
+ return axes;
118
+ }
119
+
120
+ /**
121
+ * Per-device scroll accumulators.
122
+ *
123
+ * Keyed by the *source* device as well as the master, because a master
124
+ * pointer's valuators are whatever its last-used slave put there: switching
125
+ * from the touchpad to the mouse continues the numbering with an unrelated
126
+ * value, and subtracting across that switch is the same jump as subtracting
127
+ * from zero.
128
+ */
129
+ export class ScrollTracker {
130
+ constructor() {
131
+ this._last = new Map();
132
+ }
133
+
134
+ /** Forget a device's axes: the next event from it seeds again. */
135
+ reset(deviceId) {
136
+ for (const key of this._last.keys()) {
137
+ if (key.startsWith(`${deviceId}:`)) this._last.delete(key);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * The scroll an event's valuators describe, or null when it carries no
143
+ * scroll axis and is therefore about something else (a pointer move).
144
+ *
145
+ * `moved` distinguishes a scroll from the first event of one: the seeding
146
+ * event carries a scroll axis — so it is not a pointer move and must not be
147
+ * reported as one — but has nothing to subtract from and so no distance to
148
+ * report. Distances are in notches, fractional, positive down and right
149
+ * (the direction a DOM `wheel` event calls positive).
150
+ *
151
+ * @param {object} ev a decoded XI2 device event
152
+ * @param {Map<number, {vertical: boolean, increment: number}>} axes
153
+ * @returns {{deltaX: number, deltaY: number, moved: boolean}|null}
154
+ */
155
+ delta(ev, axes) {
156
+ if (!axes?.size) return null;
157
+ let deltaX = 0;
158
+ let deltaY = 0;
159
+ let scrolled = false;
160
+ let moved = false;
161
+ for (const [number, axis] of axes) {
162
+ const value = ev.valuators?.[number];
163
+ if (value === undefined) continue;
164
+ scrolled = true;
165
+ const key = `${ev.deviceId}:${ev.sourceId}:${number}`;
166
+ const previous = this._last.get(key);
167
+ this._last.set(key, value);
168
+ // seeding: an absolute accumulator says nothing on its own
169
+ if (previous === undefined || value === previous) continue;
170
+ moved = true;
171
+ // a negative increment is how a device declares its axis inverted, and
172
+ // dividing by it puts the delta back in the natural direction
173
+ const notches = (value - previous) / axis.increment;
174
+ if (axis.vertical) deltaY += notches;
175
+ else deltaX += notches;
176
+ }
177
+ return scrolled ? { deltaX, deltaY, moved } : null;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * The core `state` field, rebuilt from an XI2 event.
183
+ *
184
+ * ntk hands this to consumers as `ev.buttons` and to the keyboard decoder as
185
+ * the layout group, both of which predate XI2 and read the core bit layout:
186
+ * modifiers in bits 0-7, buttons 1-5 in bits 8-12, the XKB group in 13-14.
187
+ * XI2 splits the same information three ways, and reports buttons as a list.
188
+ */
189
+ export function coreState(ev) {
190
+ let state = (ev.mods?.effective ?? 0) & 0xff;
191
+ for (const button of ev.buttons ?? []) {
192
+ if (button >= 1 && button <= 5) state |= 1 << (7 + button);
193
+ }
194
+ state |= ((ev.group?.effective ?? 0) & 3) << 13;
195
+ return state;
196
+ }
197
+
198
+ // Core event codes, so a translated event answers `ev.type` the way the event
199
+ // it stands in for would.
200
+ const coreType = {
201
+ mousemove: 6,
202
+ mousedown: 4,
203
+ mouseup: 5,
204
+ keydown: 2,
205
+ keyup: 3
206
+ };
207
+
208
+ /**
209
+ * An XI2 device event in the shape ntk delivers.
210
+ *
211
+ * Core-compatible where the two overlap — a handler written against
212
+ * `mousedown` cannot tell the difference — with the XI2 extras alongside:
213
+ * `deviceId`/`sourceId`, the sub-pixel `preciseX`/`preciseY` that FP1616
214
+ * coordinates carry and the integer `x`/`y` cannot, and the raw `valuators`.
215
+ */
216
+ export function toNtkEvent(name, ev) {
217
+ const out = {
218
+ type: coreType[name] ?? ev.type,
219
+ name: ev.name,
220
+ xi2: true,
221
+ time: ev.time,
222
+ root: ev.root,
223
+ wid: ev.wid,
224
+ child: ev.child,
225
+ // core X reports integer coordinates; keep that the delivered contract and
226
+ // put the precision XI2 adds next to it rather than in place of it
227
+ x: Math.round(ev.x),
228
+ y: Math.round(ev.y),
229
+ rootx: Math.round(ev.rootx),
230
+ rooty: Math.round(ev.rooty),
231
+ preciseX: ev.x,
232
+ preciseY: ev.y,
233
+ preciseRootX: ev.rootx,
234
+ preciseRootY: ev.rooty,
235
+ buttons: coreState(ev),
236
+ buttonsDown: ev.buttons,
237
+ deviceId: ev.deviceId,
238
+ sourceId: ev.sourceId,
239
+ flags: ev.flags,
240
+ valuators: ev.valuators,
241
+ sameScreen: 1
242
+ };
243
+ // `detail` is the keycode, the button number or the touch id, depending;
244
+ // core X calls the first two `keycode`, and ntk documents them under that
245
+ if (name === 'touchstart' || name === 'touchmove' || name === 'touchend') {
246
+ out.touchId = ev.detail;
247
+ } else {
248
+ out.keycode = ev.detail;
249
+ }
250
+ return out;
251
+ }
252
+
253
+ /**
254
+ * A `wheel` event: `deltaX`/`deltaY` in notches, positive down and right.
255
+ *
256
+ * Notches rather than pixels, because a notch is the only unit the device
257
+ * agrees on — `increment` is defined as the value that makes one — and how
258
+ * many pixels one is worth is the consumer's line height, not ours. A
259
+ * touchpad reports fractions of one, which is the whole point: `smooth` says
260
+ * whether this delta came from a device that can, so a consumer can decide
261
+ * whether to animate the scroll or snap it.
262
+ *
263
+ * @param {string} source 'valuator' (XI2 scroll axes) or 'button' (4-7)
264
+ */
265
+ export function wheelEvent(base, deltaX, deltaY, source) {
266
+ return {
267
+ // `type` stays that of the X event this was derived from — a button press
268
+ // or a motion — because a wheel has no core event code of its own
269
+ ...base,
270
+ name: 'wheel',
271
+ deltaX,
272
+ deltaY,
273
+ deltaMode: 'line',
274
+ smooth: source === 'valuator',
275
+ source
276
+ };
277
+ }
278
+
279
+ export default {
280
+ xi2EventName,
281
+ DEFAULT_XI2_EVENTS,
282
+ POINTER_EMULATED,
283
+ WHEEL_BUTTONS,
284
+ normalizeXI2Types,
285
+ scrollAxes,
286
+ ScrollTracker,
287
+ coreState,
288
+ toNtkEvent,
289
+ wheelEvent
290
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "7.3.3",
3
+ "version": "7.5.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",
@@ -24,7 +24,8 @@
24
24
  "type": "module",
25
25
  "main": "./lib/index.js",
26
26
  "exports": {
27
- ".": "./lib/index.js"
27
+ ".": "./lib/index.js",
28
+ "./xembed": "./lib/xembed.js"
28
29
  },
29
30
  "files": [
30
31
  "lib"