react-x11 2.13.0 → 2.15.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/package.json +11 -8
- package/src/Reconciler.js +34 -21
- package/src/components/Select.js +8 -2
- package/src/events.js +8 -2
- package/src/glnodes.js +15 -3
- package/src/index.d.ts +5 -0
- package/src/nodes/boxpaint.js +9 -0
- package/src/nodes/preedit.js +64 -14
- package/src/ntk.js +10 -1
- package/src/scale.js +52 -22
- package/src/screencolor.js +104 -17
- package/src/types/system.d.ts +14 -1
- package/src/wayland/app.js +574 -0
- package/src/wayland/backendwindow.js +1199 -0
- package/src/wayland/clipboard.js +326 -0
- package/src/wayland/connection.js +485 -0
- package/src/wayland/context2d.js +2290 -0
- package/src/wayland/decorations.js +476 -0
- package/src/wayland/device.js +55 -0
- package/src/wayland/dmabuf.js +89 -0
- package/src/wayland/dnd.js +581 -0
- package/src/wayland/fdutil.js +108 -0
- package/src/wayland/framestyle.js +257 -0
- package/src/wayland/framewatch.js +190 -0
- package/src/wayland/glarea.js +372 -0
- package/src/wayland/glcontext.js +415 -0
- package/src/wayland/glyphatlas.js +237 -0
- package/src/wayland/input.js +417 -0
- package/src/wayland/keysymnames.js +35 -0
- package/src/wayland/layershell.js +363 -0
- package/src/wayland/outputs.js +601 -0
- package/src/wayland/protocols/cursor-shape-v1.json +1 -0
- package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
- package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
- package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
- package/src/wayland/protocols/fractional-scale-v1.json +1 -0
- package/src/wayland/protocols/index.json +131 -0
- package/src/wayland/protocols/kde-server-decoration.json +1 -0
- package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
- package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
- package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
- package/src/wayland/protocols/presentation-time.json +1 -0
- package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
- package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
- package/src/wayland/protocols/tablet-v2.json +1 -0
- package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
- package/src/wayland/protocols/viewporter.json +1 -0
- package/src/wayland/protocols/wayland.json +1 -0
- package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
- package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-activation-v1.json +1 -0
- package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-shell.json +1 -0
- package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
- package/src/wayland/readback.js +99 -0
- package/src/wayland/screencopy.js +584 -0
- package/src/wayland/seat.js +584 -0
- package/src/wayland/shm.js +226 -0
- package/src/wayland/ssd.js +234 -0
- package/src/wayland/surface.js +141 -0
- package/src/wayland/swapchain.js +411 -0
- package/src/wayland/tablet.js +522 -0
- package/src/wayland/target.js +269 -0
- package/src/wayland/text.js +113 -0
- package/src/wayland/textinput.js +671 -0
- package/src/wayland/touch.js +284 -0
- package/src/wayland/window.js +854 -0
- package/src/wayland/xkb.js +425 -0
- package/src/windowstate.js +41 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
// Routing seat events into the window tree, in the vocabulary react-x11's
|
|
2
|
+
// `events.js` already speaks.
|
|
3
|
+
//
|
|
4
|
+
// The renderer subscribes to ntk-shaped window events — `mousedown`,
|
|
5
|
+
// `mousemove`, `wheel`, `keydown`, `focus` and so on, with X-style fields:
|
|
6
|
+
// device-pixel `x`/`y`, a `buttons` state mask, a `keycode`, a server
|
|
7
|
+
// timestamp. The Cocoa backend translates NSEvents into that shape; this
|
|
8
|
+
// translates `wl_seat`'s. The tree above never knows which.
|
|
9
|
+
//
|
|
10
|
+
// Three things are this file's own:
|
|
11
|
+
//
|
|
12
|
+
// - **Coordinates.** Seat events are surface-local, logical, and relative to
|
|
13
|
+
// the whole surface including the client-side decorations. The tree wants
|
|
14
|
+
// content-relative device pixels. Every event goes through one function
|
|
15
|
+
// that subtracts the frame and multiplies by the scale.
|
|
16
|
+
// - **The frame.** Presses on the titlebar and edges never reach the tree:
|
|
17
|
+
// they start compositor moves and resizes, toggle maximise, or hit the
|
|
18
|
+
// buttons. Hover over the frame drives the resize cursors; hover over
|
|
19
|
+
// content hands the cursor back to whatever the tree last set.
|
|
20
|
+
// - **Which window.** A seat event names a `wl_surface`; the map from
|
|
21
|
+
// surface to window lives on the app. Keys go to the keyboard-focused
|
|
22
|
+
// surface's window — its toplevel, when that is one of our popups, which
|
|
23
|
+
// mutter gives the keyboard to when it grabs — pointer events to the one
|
|
24
|
+
// under the pointer, and a popup with a grab receives the outside press
|
|
25
|
+
// that should dismiss it as an ordinary `mousedown` outside its bounds —
|
|
26
|
+
// which is what its `<popup grab>` logic on X11 expects from a grab.
|
|
27
|
+
|
|
28
|
+
import { Decorations } from './decorations.js';
|
|
29
|
+
import { XkbKeymap } from './xkb.js';
|
|
30
|
+
import { TOPLEVEL_STATE } from './window.js';
|
|
31
|
+
|
|
32
|
+
export class InputRouter {
|
|
33
|
+
/**
|
|
34
|
+
* @param {import('./app.js').WaylandApp} app
|
|
35
|
+
*/
|
|
36
|
+
constructor(app) {
|
|
37
|
+
this.app = app;
|
|
38
|
+
this.seat = app.seat;
|
|
39
|
+
/** the backend window under the pointer */
|
|
40
|
+
this.pointerWindow = null;
|
|
41
|
+
/** the backend window with keyboard focus — a toplevel, never a popup */
|
|
42
|
+
this.focusWindow = null;
|
|
43
|
+
/** a window whose keyboard leave is held to see if an enter follows */
|
|
44
|
+
this._heldBlur = null;
|
|
45
|
+
/** what the frame's hover last asked the cursor to be, or null */
|
|
46
|
+
this._frameCursor = null;
|
|
47
|
+
/** touch points that began on the frame, which the tree never sees */
|
|
48
|
+
this._frameTouches = new Set();
|
|
49
|
+
this._wire();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_wire() {
|
|
53
|
+
const s = this.seat;
|
|
54
|
+
s.on('enter', (ev) => this._onEnter(ev));
|
|
55
|
+
s.on('leave', (ev) => this._onLeave(ev));
|
|
56
|
+
s.on('motion', (ev) => this._onMotion(ev));
|
|
57
|
+
s.on('buttonpress', (ev) => this._onButton(ev, true));
|
|
58
|
+
s.on('buttonrelease', (ev) => this._onButton(ev, false));
|
|
59
|
+
s.on('wheel', (ev) => this._onWheel(ev));
|
|
60
|
+
s.on('keydown', (ev) => this._onKey('keydown', ev));
|
|
61
|
+
s.on('keyup', (ev) => this._onKey('keyup', ev));
|
|
62
|
+
s.on('focus', (ev) => this._onFocus(ev, true));
|
|
63
|
+
s.on('blur', (ev) => this._onFocus(ev, false));
|
|
64
|
+
// touch.js and tablet.js emit the pointer events above for the finger or
|
|
65
|
+
// tool that is standing in for the pointer; only the raw points are new
|
|
66
|
+
for (const n of ['touchstart', 'touchmove', 'touchend', 'touchcancel'])
|
|
67
|
+
s.on(n, (ev) => this._onTouch(ev));
|
|
68
|
+
s.on('keymap', (_km, xkb) => {
|
|
69
|
+
if (xkb) this.app.X.keycode2keysyms = xkb.keycode2keysyms;
|
|
70
|
+
this.app.X.emit('mapping');
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_window(surfaceId) {
|
|
75
|
+
return surfaceId == null ? null : (this.app.windows.get(surfaceId) ?? null);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The X-style state mask: modifiers and held buttons together. */
|
|
79
|
+
_state() {
|
|
80
|
+
return (
|
|
81
|
+
XkbKeymap.stateOf(this.seat.mods, this.seat.group) | this.seat.buttonMask
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Surface-local logical -> content-relative device pixels. */
|
|
86
|
+
_point(win, x, y) {
|
|
87
|
+
const i = win.insets;
|
|
88
|
+
const s = win.scale;
|
|
89
|
+
return { x: Math.round((x - i.left) * s), y: Math.round((y - i.top) * s) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_emit(win, name, ev) {
|
|
93
|
+
if (!win || win._destroyed) return;
|
|
94
|
+
win.emit(name, ev);
|
|
95
|
+
this.app.afterInput?.();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- pointer ----------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
_onEnter(ev) {
|
|
101
|
+
const win = this._window(ev.surface);
|
|
102
|
+
this.pointerWindow = win;
|
|
103
|
+
if (!win) return;
|
|
104
|
+
this._updateFrameHover(win, ev.x, ev.y);
|
|
105
|
+
const p = this._point(win, ev.x, ev.y);
|
|
106
|
+
this._emit(win, 'mouseover', {
|
|
107
|
+
...p,
|
|
108
|
+
rootx: p.x,
|
|
109
|
+
rooty: p.y,
|
|
110
|
+
buttons: this._state(),
|
|
111
|
+
time: 0,
|
|
112
|
+
// a finger or a pen standing in for the pointer says so (touch.js,
|
|
113
|
+
// tablet.js); a mouse adds nothing, as on the other backends
|
|
114
|
+
...ev.device,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_onLeave(ev) {
|
|
119
|
+
const win = this._window(ev.surface);
|
|
120
|
+
if (this.pointerWindow === win) this.pointerWindow = null;
|
|
121
|
+
if (!win) return;
|
|
122
|
+
if (win.decor && win.decor.hover) {
|
|
123
|
+
win.decor.hover = null;
|
|
124
|
+
win.repaintFrame();
|
|
125
|
+
}
|
|
126
|
+
this._frameCursor = null;
|
|
127
|
+
this._emit(win, 'mouseout', {
|
|
128
|
+
x: 0,
|
|
129
|
+
y: 0,
|
|
130
|
+
rootx: 0,
|
|
131
|
+
rooty: 0,
|
|
132
|
+
buttons: this._state(),
|
|
133
|
+
time: 0,
|
|
134
|
+
...ev.device,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_onMotion(ev) {
|
|
139
|
+
const win = this.pointerWindow ?? this._window(ev.surface);
|
|
140
|
+
if (!win) return;
|
|
141
|
+
const hit = this._updateFrameHover(win, ev.x, ev.y);
|
|
142
|
+
if (hit.kind !== 'content' && !this.seat.buttonMask) return;
|
|
143
|
+
const p = this._point(win, ev.x, ev.y);
|
|
144
|
+
this._emit(win, 'mousemove', {
|
|
145
|
+
...p,
|
|
146
|
+
rootx: p.x,
|
|
147
|
+
rooty: p.y,
|
|
148
|
+
buttons: this._state(),
|
|
149
|
+
time: ev.time,
|
|
150
|
+
...ev.device,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Frame hover: the button under the pointer (for its highlight) and the
|
|
156
|
+
* cursor the edge wants. Returns the hit so callers can skip the tree.
|
|
157
|
+
*/
|
|
158
|
+
_updateFrameHover(win, x, y) {
|
|
159
|
+
if (!win.decor) return { kind: 'content' };
|
|
160
|
+
const hit = win.decor.hitTest(x, y, win.wl.width, win.wl.height);
|
|
161
|
+
const hoverId = hit.kind === 'button' ? hit.id : null;
|
|
162
|
+
if (win.decor.hover !== hoverId) {
|
|
163
|
+
win.decor.hover = hoverId;
|
|
164
|
+
win.repaintFrame();
|
|
165
|
+
}
|
|
166
|
+
const want = hit.kind === 'content' ? null : Decorations.cursorFor(hit);
|
|
167
|
+
if (want !== this._frameCursor) {
|
|
168
|
+
this._frameCursor = want;
|
|
169
|
+
this.seat.setCursor(want ?? win.treeCursor ?? 'default');
|
|
170
|
+
}
|
|
171
|
+
return hit;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_onButton(ev, pressed) {
|
|
175
|
+
const win = this.pointerWindow ?? this._window(ev.surface);
|
|
176
|
+
if (!win) return;
|
|
177
|
+
if (win.decor && pressed && !this.seat.buttonMask) {
|
|
178
|
+
// buttonMask already includes this press; the frame decides on a
|
|
179
|
+
// fresh press only, never mid-drag
|
|
180
|
+
}
|
|
181
|
+
if (win.decor) {
|
|
182
|
+
const hit = win.decor.hitTest(ev.x, ev.y, win.wl.width, win.wl.height);
|
|
183
|
+
if (hit.kind !== 'content') {
|
|
184
|
+
if (pressed) this._framePress(win, hit, ev);
|
|
185
|
+
else this._frameRelease(win, hit, ev);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const p = this._point(win, ev.x, ev.y);
|
|
190
|
+
this._emit(win, pressed ? 'mousedown' : 'mouseup', {
|
|
191
|
+
...p,
|
|
192
|
+
rootx: p.x,
|
|
193
|
+
rooty: p.y,
|
|
194
|
+
keycode: ev.button,
|
|
195
|
+
buttons: ev.state | XkbKeymap.stateOf(this.seat.mods, this.seat.group),
|
|
196
|
+
time: ev.time,
|
|
197
|
+
serial: ev.serial,
|
|
198
|
+
...ev.device,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_framePress(win, hit, ev) {
|
|
203
|
+
const seat = this.seat.seat;
|
|
204
|
+
if (hit.kind === 'resize' && ev.button === 1) {
|
|
205
|
+
win.wl.startResize(seat, ev.serial, hit.edges);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (hit.kind === 'button') {
|
|
209
|
+
win.decor.pressed = hit.id;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// titlebar: what a click means is the desktop's to say (framestyle.js)
|
|
213
|
+
const style = this.app.frameStyle?.value;
|
|
214
|
+
if (ev.button === 1) {
|
|
215
|
+
const now = ev.time;
|
|
216
|
+
if (win._lastTitlePress && now - win._lastTitlePress < 400) {
|
|
217
|
+
win._lastTitlePress = 0;
|
|
218
|
+
this._titlebarAction(win, style?.doubleClick ?? 'toggle-maximize', ev);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
win._lastTitlePress = now;
|
|
222
|
+
win.wl.startMove(seat, ev.serial);
|
|
223
|
+
} else if (ev.button === 2) {
|
|
224
|
+
this._titlebarAction(win, style?.middleClick ?? 'none', ev);
|
|
225
|
+
} else if (ev.button === 3) {
|
|
226
|
+
this._titlebarAction(win, style?.rightClick ?? 'menu', ev);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* One of `org.gnome.desktop.wm.preferences`' titlebar actions. `lower` and
|
|
232
|
+
* `toggle-shade` have no xdg-shell request, and a maximise in one
|
|
233
|
+
* direction is a maximise here: they do what the protocol allows, which
|
|
234
|
+
* for the first two is nothing.
|
|
235
|
+
*/
|
|
236
|
+
_titlebarAction(win, action, ev) {
|
|
237
|
+
switch (action) {
|
|
238
|
+
case 'toggle-maximize':
|
|
239
|
+
case 'toggle-maximize-horizontally':
|
|
240
|
+
case 'toggle-maximize-vertically':
|
|
241
|
+
win.wl.maximize(!win.decor.maximized);
|
|
242
|
+
return;
|
|
243
|
+
case 'minimize':
|
|
244
|
+
win.wl.minimize();
|
|
245
|
+
return;
|
|
246
|
+
case 'menu':
|
|
247
|
+
win.wl.showWindowMenu(this.seat.seat, ev.serial, ev.x, ev.y);
|
|
248
|
+
return;
|
|
249
|
+
default:
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_frameRelease(win, hit, ev) {
|
|
254
|
+
const pressed = win.decor.pressed;
|
|
255
|
+
win.decor.pressed = null;
|
|
256
|
+
if (
|
|
257
|
+
!pressed ||
|
|
258
|
+
hit.kind !== 'button' ||
|
|
259
|
+
hit.id !== pressed ||
|
|
260
|
+
ev.button !== 1
|
|
261
|
+
)
|
|
262
|
+
return;
|
|
263
|
+
if (pressed === 'close') win.requestClose();
|
|
264
|
+
else if (pressed === 'maximize') win.wl.maximize(!win.decor.maximized);
|
|
265
|
+
else if (pressed === 'minimize') win.wl.minimize();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_onWheel(ev) {
|
|
269
|
+
const win = this.pointerWindow ?? this._window(ev.surface);
|
|
270
|
+
if (!win) return;
|
|
271
|
+
if (
|
|
272
|
+
win.decor &&
|
|
273
|
+
win.decor.hitTest(ev.x, ev.y, win.wl.width, win.wl.height).kind !==
|
|
274
|
+
'content'
|
|
275
|
+
)
|
|
276
|
+
return;
|
|
277
|
+
const p = this._point(win, ev.x, ev.y);
|
|
278
|
+
// X's wheel is "down is positive notches"; the seat already has that sign.
|
|
279
|
+
this._emit(win, 'wheel', {
|
|
280
|
+
name: 'wheel',
|
|
281
|
+
...p,
|
|
282
|
+
rootx: p.x,
|
|
283
|
+
rooty: p.y,
|
|
284
|
+
buttons: this._state(),
|
|
285
|
+
deltaX: ev.deltaX,
|
|
286
|
+
deltaY: ev.deltaY,
|
|
287
|
+
smooth: ev.smooth,
|
|
288
|
+
source: ev.smooth ? 'valuator' : 'button',
|
|
289
|
+
time: ev.time,
|
|
290
|
+
...ev.device,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---- touch ------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Raw touch points — `touchstart`/`touchmove`/`touchend`/`touchcancel` on
|
|
298
|
+
* the window, each with every active point over it. No emulation here:
|
|
299
|
+
* touch.js makes the first finger the pointer, and those events arrived
|
|
300
|
+
* above as the pointer's own. A point that began on the frame belongs to
|
|
301
|
+
* the frame (its emulated press already started the move or resize) and
|
|
302
|
+
* is kept from the tree for its whole life.
|
|
303
|
+
*/
|
|
304
|
+
_onTouch(ev) {
|
|
305
|
+
const win = this._window(ev.surface);
|
|
306
|
+
if (!win) return;
|
|
307
|
+
if (ev.type === 'touchstart' && win.decor) {
|
|
308
|
+
const hit = win.decor.hitTest(ev.x, ev.y, win.wl.width, win.wl.height);
|
|
309
|
+
if (hit.kind !== 'content') this._frameTouches.add(ev.id);
|
|
310
|
+
}
|
|
311
|
+
if (this._frameTouches.has(ev.id)) {
|
|
312
|
+
if (ev.type === 'touchend' || ev.type === 'touchcancel')
|
|
313
|
+
this._frameTouches.delete(ev.id);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const p = this._point(win, ev.x, ev.y);
|
|
317
|
+
const s = win.scale;
|
|
318
|
+
this._emit(win, ev.type, {
|
|
319
|
+
...p,
|
|
320
|
+
rootx: p.x,
|
|
321
|
+
rooty: p.y,
|
|
322
|
+
id: ev.id,
|
|
323
|
+
touchId: ev.id,
|
|
324
|
+
radiusX: ev.radiusX * s,
|
|
325
|
+
radiusY: ev.radiusY * s,
|
|
326
|
+
rotationAngle: ev.rotationAngle,
|
|
327
|
+
touches: ev.touches
|
|
328
|
+
.filter(
|
|
329
|
+
(t) => t.surface === ev.surface && !this._frameTouches.has(t.id),
|
|
330
|
+
)
|
|
331
|
+
.map((t) => ({ id: t.id, ...this._point(win, t.x, t.y) })),
|
|
332
|
+
buttons: ev.state | XkbKeymap.stateOf(this.seat.mods, this.seat.group),
|
|
333
|
+
pointerType: 'touch',
|
|
334
|
+
time: ev.time,
|
|
335
|
+
serial: ev.serial,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---- keyboard ---------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
_onKey(name, ev) {
|
|
342
|
+
const win = this.focusWindow ?? this._focusRoot(this._window(ev.surface));
|
|
343
|
+
if (!win) return;
|
|
344
|
+
this._emit(win, name, {
|
|
345
|
+
keycode: ev.keycode,
|
|
346
|
+
keysym: ev.keysym,
|
|
347
|
+
baseKeysym: ev.baseKeysym,
|
|
348
|
+
codepoint: ev.codepoint,
|
|
349
|
+
buttons: ev.buttons,
|
|
350
|
+
group: ev.group,
|
|
351
|
+
time: ev.time,
|
|
352
|
+
repeat: ev.repeat,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The window whose tree has the keyboard when `win`'s surface does: `win`
|
|
358
|
+
* itself, or — for one of our popups — the toplevel its chain hangs from.
|
|
359
|
+
* Mutter gives the keyboard to a popup that grabs (a menu, a Select's
|
|
360
|
+
* list); X never focuses an override-redirect window, and the tree's focus
|
|
361
|
+
* model is X's: keys reach the focused node through its toplevel, wherever
|
|
362
|
+
* in the tree that node is.
|
|
363
|
+
*/
|
|
364
|
+
_focusRoot(win) {
|
|
365
|
+
let w = win;
|
|
366
|
+
for (let i = 0; w?.isPopup && w.parentWindow && i < 16; i++) {
|
|
367
|
+
w = w.parentWindow;
|
|
368
|
+
}
|
|
369
|
+
return w;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* `wl_keyboard` enter and leave. A move inside one window's family — the
|
|
374
|
+
* keyboard onto its grabbing popup and back, which mutter sends as a leave
|
|
375
|
+
* and an enter together — is no focus change at all: a leave is held
|
|
376
|
+
* until the events already read have been handled, and an enter for the
|
|
377
|
+
* same window drops it. Before this the toplevel heard a blur the moment a
|
|
378
|
+
* Select's list was configured, the Select closed the list on it, and on
|
|
379
|
+
* GNOME no dropdown ever showed. A leave naming a surface already gone —
|
|
380
|
+
* the popup that had the keyboard — is a leave of the window that had it.
|
|
381
|
+
*/
|
|
382
|
+
_onFocus(ev, focused) {
|
|
383
|
+
const win = this._focusRoot(this._window(ev.surface));
|
|
384
|
+
if (!focused) {
|
|
385
|
+
const lost = win ?? this.focusWindow;
|
|
386
|
+
if (!lost) return;
|
|
387
|
+
this._heldBlur = lost;
|
|
388
|
+
setImmediate(() => {
|
|
389
|
+
if (this._heldBlur !== lost) return;
|
|
390
|
+
this._heldBlur = null;
|
|
391
|
+
this._setFocus(lost, false);
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const held = this._heldBlur;
|
|
396
|
+
this._heldBlur = null;
|
|
397
|
+
if (held && held !== win) this._setFocus(held, false);
|
|
398
|
+
if (held === win && this.focusWindow === win) return;
|
|
399
|
+
this._setFocus(win, true);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
_setFocus(win, focused) {
|
|
403
|
+
if (focused) this.focusWindow = win;
|
|
404
|
+
else if (this.focusWindow === win) this.focusWindow = null;
|
|
405
|
+
if (!win) return;
|
|
406
|
+
if (win.decor) {
|
|
407
|
+
const wasActive = win.decor.active;
|
|
408
|
+
win.decor.active = focused || win.wl.states.has(TOPLEVEL_STATE.ACTIVATED);
|
|
409
|
+
if (wasActive !== win.decor.active) win.repaintFrame();
|
|
410
|
+
}
|
|
411
|
+
this._emit(win, focused ? 'focus' : 'blur', { buttons: 0, time: 0 });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
destroy() {
|
|
415
|
+
this.seat.removeAllListeners();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Keysym names -> values, for reading an XKB keymap.
|
|
2
|
+
//
|
|
3
|
+
// An `xkb_symbols` section names its keysyms the way `keysymdef.h` does —
|
|
4
|
+
// `q`, `exclam`, `Return`, `ISO_Level3_Shift`, `dead_acute`, `KP_1` — with
|
|
5
|
+
// two extra spellings for anything the table does not have: `U20AC` for a
|
|
6
|
+
// Unicode code point and a plain hex number. node-x11 already ships the
|
|
7
|
+
// whole of keysymdef.h as `XK_<name>: { code }`, so this is a lookup and the
|
|
8
|
+
// two conventions, not a table of its own.
|
|
9
|
+
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const table = require('x11/lib/keysyms.js');
|
|
14
|
+
|
|
15
|
+
const cache = new Map();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} name a keysym as an XKB file spells it
|
|
19
|
+
* @returns {number|undefined} the keysym, 0 for `NoSymbol`, undefined when unknown
|
|
20
|
+
*/
|
|
21
|
+
export function keysymFromName(name) {
|
|
22
|
+
if (!name) return undefined;
|
|
23
|
+
const hit = cache.get(name);
|
|
24
|
+
if (hit !== undefined) return hit;
|
|
25
|
+
let sym;
|
|
26
|
+
if (name === 'NoSymbol' || name === 'VoidSymbol') sym = 0;
|
|
27
|
+
else if (/^U[0-9A-Fa-f]{4,6}$/.test(name)) {
|
|
28
|
+
const cp = parseInt(name.slice(1), 16);
|
|
29
|
+
// Latin-1 is its own keysym range; everything else is 0x01000000 + cp.
|
|
30
|
+
sym = cp >= 0x20 && cp <= 0xff ? cp : 0x01000000 + cp;
|
|
31
|
+
} else if (/^0x[0-9A-Fa-f]+$/.test(name)) sym = parseInt(name, 16);
|
|
32
|
+
else sym = table[`XK_${name}`]?.code;
|
|
33
|
+
cache.set(name, sym);
|
|
34
|
+
return sym;
|
|
35
|
+
}
|