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/app.js +49 -0
- package/lib/drawable.js +5 -0
- package/lib/events_map.js +11 -1
- package/lib/index.js +19 -0
- package/lib/window.js +313 -41
- package/lib/xembed.js +922 -0
- package/lib/xi2.js +290 -0
- package/package.json +3 -2
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
|
+
"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"
|