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,854 @@
|
|
|
1
|
+
// A surface with a shell role — a toplevel or a popup — and the handshake
|
|
2
|
+
// that is the biggest single difference between this backend and X11.
|
|
3
|
+
//
|
|
4
|
+
// On X11 a client asks and the server answers: `GetGeometry` returns a size,
|
|
5
|
+
// `ConfigureWindow` sets one, and a request takes effect as issued. Here the
|
|
6
|
+
// client *requests* and the compositor *decides*. A window is created with no
|
|
7
|
+
// size at all; the compositor sends `configure` saying how big it should be
|
|
8
|
+
// and what state it is in, the client acknowledges that serial, and only then
|
|
9
|
+
// may it show a buffer at that size. Nothing about a window's position is
|
|
10
|
+
// knowable, ever.
|
|
11
|
+
//
|
|
12
|
+
// The sequence is strict and a compositor will disconnect a client that gets
|
|
13
|
+
// it wrong:
|
|
14
|
+
//
|
|
15
|
+
// create wl_surface -> get_xdg_surface -> get_toplevel / get_popup
|
|
16
|
+
// commit with NO buffer (asks for the first configure)
|
|
17
|
+
// <- xdg_toplevel.configure(w, h, states) (0x0 means "you choose")
|
|
18
|
+
// <- xdg_surface.configure(serial)
|
|
19
|
+
// ack_configure(serial) -> attach -> damage -> commit
|
|
20
|
+
//
|
|
21
|
+
// **The ack is deferred to the frame that adopts the state.** Acking as soon
|
|
22
|
+
// as the configure arrives is legal, but it tells the compositor the *next*
|
|
23
|
+
// commit is at the new size — and if that commit carries a buffer the
|
|
24
|
+
// renderer painted at the old size, the compositor shows it stretched. So the
|
|
25
|
+
// serial is held and `ackPending()` is called immediately before the commit
|
|
26
|
+
// of a frame painted at the configured size (glcontext.js does this), which
|
|
27
|
+
// is what makes an interactive resize look right.
|
|
28
|
+
//
|
|
29
|
+
// Everything here is synchronous through the library's `$` namespace, because
|
|
30
|
+
// React's commit phase is synchronous and `WindowNode.realize()` calls
|
|
31
|
+
// `createWindow` inside it. The one genuinely asynchronous step — binding
|
|
32
|
+
// globals — is done once by the app at startup.
|
|
33
|
+
//
|
|
34
|
+
// Two more things hang off the same class. A *layer surface* (layershell.js)
|
|
35
|
+
// is what a dock or a wallpaper is: no xdg_surface, a `zwlr_layer_surface_v1`
|
|
36
|
+
// whose configure carries a size and is acked through it. And a toplevel may
|
|
37
|
+
// carry a *decoration* object (ssd.js) whose mode — who draws the frame —
|
|
38
|
+
// rides the same configure sequence and is adopted just before 'configure'
|
|
39
|
+
// is emitted, so the frame that acks it is painted at the right insets.
|
|
40
|
+
|
|
41
|
+
import { EventEmitter } from 'node:events';
|
|
42
|
+
import { createServerDecoration } from './ssd.js';
|
|
43
|
+
import { createLayerSurface } from './layershell.js';
|
|
44
|
+
|
|
45
|
+
/** `xdg_toplevel.state` values. */
|
|
46
|
+
export const TOPLEVEL_STATE = {
|
|
47
|
+
MAXIMIZED: 1,
|
|
48
|
+
FULLSCREEN: 2,
|
|
49
|
+
RESIZING: 3,
|
|
50
|
+
ACTIVATED: 4,
|
|
51
|
+
TILED_LEFT: 5,
|
|
52
|
+
TILED_RIGHT: 6,
|
|
53
|
+
TILED_TOP: 7,
|
|
54
|
+
TILED_BOTTOM: 8,
|
|
55
|
+
SUSPENDED: 9,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** `xdg_toplevel.resize_edge`. */
|
|
59
|
+
export const RESIZE_EDGE = {
|
|
60
|
+
NONE: 0,
|
|
61
|
+
TOP: 1,
|
|
62
|
+
BOTTOM: 2,
|
|
63
|
+
LEFT: 4,
|
|
64
|
+
TOP_LEFT: 5,
|
|
65
|
+
BOTTOM_LEFT: 6,
|
|
66
|
+
RIGHT: 8,
|
|
67
|
+
TOP_RIGHT: 9,
|
|
68
|
+
BOTTOM_RIGHT: 10,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** `xdg_positioner.anchor` / `.gravity` share one numbering. */
|
|
72
|
+
const ANCHOR = {
|
|
73
|
+
NONE: 0,
|
|
74
|
+
TOP: 1,
|
|
75
|
+
BOTTOM: 2,
|
|
76
|
+
LEFT: 3,
|
|
77
|
+
RIGHT: 4,
|
|
78
|
+
TOP_LEFT: 5,
|
|
79
|
+
BOTTOM_LEFT: 6,
|
|
80
|
+
TOP_RIGHT: 7,
|
|
81
|
+
BOTTOM_RIGHT: 8,
|
|
82
|
+
};
|
|
83
|
+
/** `xdg_positioner.constraint_adjustment` bits. */
|
|
84
|
+
const ADJUST = {
|
|
85
|
+
SLIDE_X: 1,
|
|
86
|
+
SLIDE_Y: 2,
|
|
87
|
+
FLIP_X: 4,
|
|
88
|
+
FLIP_Y: 8,
|
|
89
|
+
RESIZE_X: 16,
|
|
90
|
+
RESIZE_Y: 32,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** What a toplevel falls back to when the compositor says "you choose". */
|
|
94
|
+
const DEFAULT_SIZE = { width: 800, height: 600 };
|
|
95
|
+
|
|
96
|
+
/** fractional-scale-v1 reports scale × 120. */
|
|
97
|
+
const SCALE_DENOM = 120;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* How far outside the window the input region reaches, into the margin a
|
|
101
|
+
* client-side shadow is drawn in: the band a resize grab still lands in
|
|
102
|
+
* (decorations.js `RESIZE_MARGIN`). Beyond it, input goes to whatever is
|
|
103
|
+
* under the shadow, as it does beside a GTK window.
|
|
104
|
+
*/
|
|
105
|
+
const INPUT_BAND = 8;
|
|
106
|
+
|
|
107
|
+
const NO_MARGINS = Object.freeze({ left: 0, top: 0, right: 0, bottom: 0 });
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A positioner for a popup at a point (its top-left at `x`, `y` in the
|
|
111
|
+
* parent's window geometry) or under an anchor rectangle, sliding and
|
|
112
|
+
* flipping to stay on screen the way `anchor.js` would have done it.
|
|
113
|
+
*/
|
|
114
|
+
function popupPositioner(wmBase, { x, y, width, height, anchorRect = null }) {
|
|
115
|
+
const p = wmBase.$.create_positioner();
|
|
116
|
+
p.$.set_size(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)));
|
|
117
|
+
if (anchorRect) {
|
|
118
|
+
p.$.set_anchor_rect(
|
|
119
|
+
Math.round(anchorRect.x),
|
|
120
|
+
Math.round(anchorRect.y),
|
|
121
|
+
Math.max(1, Math.round(anchorRect.width)),
|
|
122
|
+
Math.max(1, Math.round(anchorRect.height)),
|
|
123
|
+
);
|
|
124
|
+
p.$.set_anchor(ANCHOR.BOTTOM_LEFT);
|
|
125
|
+
} else {
|
|
126
|
+
p.$.set_anchor_rect(Math.round(x), Math.round(y), 1, 1);
|
|
127
|
+
p.$.set_anchor(ANCHOR.TOP_LEFT);
|
|
128
|
+
}
|
|
129
|
+
p.$.set_gravity(ANCHOR.BOTTOM_RIGHT);
|
|
130
|
+
p.$.set_constraint_adjustment(
|
|
131
|
+
ADJUST.SLIDE_X | ADJUST.SLIDE_Y | ADJUST.FLIP_Y,
|
|
132
|
+
);
|
|
133
|
+
if (p.version >= 3) p.$.set_reactive();
|
|
134
|
+
return p;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class WaylandWindow extends EventEmitter {
|
|
138
|
+
constructor({ conn, surface, xdgSurface, role, kind, size, parent = null }) {
|
|
139
|
+
super();
|
|
140
|
+
this.conn = conn;
|
|
141
|
+
/** the `wl_surface` — what a renderer attaches buffers to */
|
|
142
|
+
this.surface = surface;
|
|
143
|
+
this.xdgSurface = xdgSurface;
|
|
144
|
+
/** the `xdg_toplevel` or `xdg_popup` */
|
|
145
|
+
this.role = role;
|
|
146
|
+
/** 'toplevel' | 'popup' */
|
|
147
|
+
this.kind = kind;
|
|
148
|
+
this.parent = parent;
|
|
149
|
+
this.toplevel = kind === 'toplevel' ? role : null;
|
|
150
|
+
this.popup = kind === 'popup' ? role : null;
|
|
151
|
+
|
|
152
|
+
/** logical size, as the compositor last configured (or we chose) */
|
|
153
|
+
this.width = size.width;
|
|
154
|
+
this.height = size.height;
|
|
155
|
+
/** popups: position relative to the parent, as configured */
|
|
156
|
+
this.x = 0;
|
|
157
|
+
this.y = 0;
|
|
158
|
+
/**
|
|
159
|
+
* Output scale. Integer from `preferred_buffer_scale`, refined to a
|
|
160
|
+
* fraction by fractional-scale-v1 when the compositor has it. The buffer
|
|
161
|
+
* is `logical × scale`; with a viewport the buffer scale stays 1.
|
|
162
|
+
*/
|
|
163
|
+
this.scale = 1;
|
|
164
|
+
this.states = new Set();
|
|
165
|
+
/**
|
|
166
|
+
* The `wl_output` proxies (by id) this surface currently overlaps, from
|
|
167
|
+
* `wl_surface.enter`/`leave` — the only word a client gets on where it
|
|
168
|
+
* is. Empty until the surface is mapped; outputs.js turns ids into
|
|
169
|
+
* monitors.
|
|
170
|
+
*/
|
|
171
|
+
this.outputs = new Set();
|
|
172
|
+
/** `xdg_toplevel.configure_bounds`: the logical size to fit in, or null */
|
|
173
|
+
this.bounds = null;
|
|
174
|
+
/** a configure has arrived: a buffer may be committed once it is acked */
|
|
175
|
+
this.configured = false;
|
|
176
|
+
this.mapped = false;
|
|
177
|
+
this.destroyed = false;
|
|
178
|
+
|
|
179
|
+
this._pendingSerial = null;
|
|
180
|
+
this._preferredBufferScale = null;
|
|
181
|
+
this._configured = new Promise((resolve) => {
|
|
182
|
+
this._onConfigured = resolve;
|
|
183
|
+
});
|
|
184
|
+
this._viewport = null;
|
|
185
|
+
this._fractional = null;
|
|
186
|
+
this._geometry = null;
|
|
187
|
+
/** the xdg-decoration object, where the compositor has the protocol (ssd.js) */
|
|
188
|
+
this.decoration = null;
|
|
189
|
+
/** a layer surface's role (layershell.js); null on a toplevel or popup */
|
|
190
|
+
this.layer = null;
|
|
191
|
+
this.layerSurface = null;
|
|
192
|
+
/** the last toplevel configure named a size: maximised, tiled, mid-resize */
|
|
193
|
+
this.sizeImposed = false;
|
|
194
|
+
/** a popup made with `commit: false`, waiting for its map to commit */
|
|
195
|
+
this._initialCommitPending = false;
|
|
196
|
+
/** a popup's setup is over — its initial commit went out — and so is its
|
|
197
|
+
* chance to take a grab */
|
|
198
|
+
this._setupDone = false;
|
|
199
|
+
/** a popup's role-to-be: placement and grab, until its initial commit */
|
|
200
|
+
this._popupSetup = null;
|
|
201
|
+
/**
|
|
202
|
+
* The margin around the window a client-side shadow is drawn in, part of
|
|
203
|
+
* the surface and not of the window: `width`/`height` are the surface's,
|
|
204
|
+
* and window geometry (what the compositor configures, places and tiles)
|
|
205
|
+
* is the surface less this. `marginsFor(states)` answers it per state —
|
|
206
|
+
* a maximised or tiled window has none.
|
|
207
|
+
*/
|
|
208
|
+
this.margins = NO_MARGINS;
|
|
209
|
+
this.marginsFor = null;
|
|
210
|
+
/** `wl_compositor`, for the input region; set by whoever made the window */
|
|
211
|
+
this.compositor = null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---- creation --------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A toplevel, built without waiting for anything.
|
|
218
|
+
*
|
|
219
|
+
* The window is not yet usable for painting: the first buffer may only be
|
|
220
|
+
* attached after the first `configure` has been acked. `configured` says
|
|
221
|
+
* when, and `whenConfigured` is the promise for anyone who can wait.
|
|
222
|
+
*
|
|
223
|
+
* @param {object} opts
|
|
224
|
+
* @param {{ manager: object, prefer: 'server'|'client' }} [opts.decorations]
|
|
225
|
+
* the `zxdg_decoration_manager_v1` proxy and which side should draw the
|
|
226
|
+
* frame; omitted where the compositor has no such protocol
|
|
227
|
+
*/
|
|
228
|
+
static createSync({
|
|
229
|
+
conn,
|
|
230
|
+
compositor,
|
|
231
|
+
wmBase,
|
|
232
|
+
title,
|
|
233
|
+
appId,
|
|
234
|
+
width,
|
|
235
|
+
height,
|
|
236
|
+
minSize,
|
|
237
|
+
maxSize,
|
|
238
|
+
parent = null,
|
|
239
|
+
decorations = null,
|
|
240
|
+
}) {
|
|
241
|
+
wirePing(wmBase);
|
|
242
|
+
const surface = compositor.$.create_surface();
|
|
243
|
+
const xdgSurface = wmBase.$.get_xdg_surface(surface.id);
|
|
244
|
+
const toplevel = xdgSurface.$.get_toplevel();
|
|
245
|
+
|
|
246
|
+
const win = new WaylandWindow({
|
|
247
|
+
conn,
|
|
248
|
+
surface,
|
|
249
|
+
xdgSurface,
|
|
250
|
+
role: toplevel,
|
|
251
|
+
kind: 'toplevel',
|
|
252
|
+
parent,
|
|
253
|
+
size: {
|
|
254
|
+
width: width ?? DEFAULT_SIZE.width,
|
|
255
|
+
height: height ?? DEFAULT_SIZE.height,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
win._wireCommon();
|
|
259
|
+
win._wireToplevel();
|
|
260
|
+
|
|
261
|
+
toplevel.$.set_title(title ?? 'react-x11');
|
|
262
|
+
toplevel.$.set_app_id(appId ?? 'react-x11');
|
|
263
|
+
if (parent?.toplevel) toplevel.$.set_parent(parent.toplevel.id);
|
|
264
|
+
if (minSize) toplevel.$.set_min_size(minSize.width | 0, minSize.height | 0);
|
|
265
|
+
if (maxSize) toplevel.$.set_max_size(maxSize.width | 0, maxSize.height | 0);
|
|
266
|
+
// Who draws the frame is asked before the first commit; the answer rides
|
|
267
|
+
// the first configure (ssd.js). Whichever protocol the compositor has is
|
|
268
|
+
// asked — the standard one first — and only a compositor with neither
|
|
269
|
+
// leaves the frame to this backend.
|
|
270
|
+
win.decoration = createServerDecoration({
|
|
271
|
+
manager: decorations?.manager,
|
|
272
|
+
kdeManager: decorations?.kdeManager,
|
|
273
|
+
toplevel,
|
|
274
|
+
surface,
|
|
275
|
+
prefer: decorations?.prefer,
|
|
276
|
+
});
|
|
277
|
+
win._wireDecoration();
|
|
278
|
+
// the empty commit that asks for the first configure
|
|
279
|
+
surface.$.commit();
|
|
280
|
+
return win;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* A layer surface — a dock, a panel, a wallpaper, an overlay — where the
|
|
285
|
+
* compositor has wlr-layer-shell. The body, and the meaning of the
|
|
286
|
+
* options, live in layershell.js.
|
|
287
|
+
*/
|
|
288
|
+
static createLayerSync(opts) {
|
|
289
|
+
return createLayerSurface(WaylandWindow, opts);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* A popup, positioned relative to its parent surface.
|
|
294
|
+
*
|
|
295
|
+
* Where an X `<popup>` places itself at screen coordinates it had to
|
|
296
|
+
* learn, an `xdg_popup` describes *where it wants to be relative to the
|
|
297
|
+
* parent* and how it may be adjusted, and the compositor places it. The
|
|
298
|
+
* flip/slide policy react-x11's `anchor.js` implements client-side on X11
|
|
299
|
+
* is exactly the positioner's constraint adjustment, so the intent
|
|
300
|
+
* transfers; the math moves to the other side of the socket.
|
|
301
|
+
*
|
|
302
|
+
* @param {object} opts
|
|
303
|
+
* @param {WaylandWindow} opts.parent the toplevel or popup this hangs off
|
|
304
|
+
* @param {number} opts.x anchor point, in the parent's surface coordinates
|
|
305
|
+
* @param {number} opts.y
|
|
306
|
+
* @param {number} opts.width requested size
|
|
307
|
+
* @param {number} opts.height
|
|
308
|
+
* @param {object} [opts.grab] `{ seat, serial }` to take an implicit grab
|
|
309
|
+
* (menus: the press outside that dismisses them arrives here)
|
|
310
|
+
*/
|
|
311
|
+
static createPopupSync({
|
|
312
|
+
conn,
|
|
313
|
+
compositor,
|
|
314
|
+
wmBase,
|
|
315
|
+
parent,
|
|
316
|
+
x,
|
|
317
|
+
y,
|
|
318
|
+
width,
|
|
319
|
+
height,
|
|
320
|
+
grab = null,
|
|
321
|
+
anchorRect = null,
|
|
322
|
+
commit = true,
|
|
323
|
+
}) {
|
|
324
|
+
if (!parent?.xdgSurface && !parent?.layerSurface)
|
|
325
|
+
throw new Error('a popup needs a parent window on this connection');
|
|
326
|
+
wirePing(wmBase);
|
|
327
|
+
const w = Math.max(1, Math.round(width || 1));
|
|
328
|
+
const h = Math.max(1, Math.round(height || 1));
|
|
329
|
+
const surface = compositor.$.create_surface();
|
|
330
|
+
const xdgSurface = wmBase.$.get_xdg_surface(surface.id);
|
|
331
|
+
const win = new WaylandWindow({
|
|
332
|
+
conn,
|
|
333
|
+
surface,
|
|
334
|
+
xdgSurface,
|
|
335
|
+
role: null,
|
|
336
|
+
kind: 'popup',
|
|
337
|
+
parent,
|
|
338
|
+
size: { width: w, height: h },
|
|
339
|
+
});
|
|
340
|
+
win.x = x;
|
|
341
|
+
win.y = y;
|
|
342
|
+
win._wireCommon();
|
|
343
|
+
// The role — positioner, `get_popup`, grab — is assigned at the initial
|
|
344
|
+
// commit, from the placement and grab the popup has by then.
|
|
345
|
+
// `commit: false` leaves that commit to `commitInitial()`, which the
|
|
346
|
+
// tree's map calls: a grab asked for at map time still precedes it (see
|
|
347
|
+
// `takeGrab`), and a popup moved before it is shown — a tooltip that
|
|
348
|
+
// measured itself at (0, 0) — is placed right the first time instead of
|
|
349
|
+
// flashing in its parent's corner until a reposition lands.
|
|
350
|
+
win._popupSetup = {
|
|
351
|
+
wmBase,
|
|
352
|
+
parent,
|
|
353
|
+
placement: { x, y, width: w, height: h, anchorRect },
|
|
354
|
+
grab: grab?.seat && grab.serial != null ? grab : null,
|
|
355
|
+
};
|
|
356
|
+
win._initialCommitPending = true;
|
|
357
|
+
if (commit) win.commitInitial();
|
|
358
|
+
return win;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** The xdg_popup role, from the placement and grab asked for so far. */
|
|
362
|
+
_assignPopupRole() {
|
|
363
|
+
const { wmBase, parent, placement, grab } = this._popupSetup;
|
|
364
|
+
this._popupSetup = null;
|
|
365
|
+
const positioner = popupPositioner(wmBase, placement);
|
|
366
|
+
// A layer surface is not an xdg_surface: its popups are created with no
|
|
367
|
+
// parent and adopted by the layer surface before their first commit.
|
|
368
|
+
const popup = parent.xdgSurface
|
|
369
|
+
? this.xdgSurface.$.get_popup(parent.xdgSurface.id, positioner.id)
|
|
370
|
+
: this.xdgSurface.$.get_popup(null, positioner.id);
|
|
371
|
+
positioner.$.destroy();
|
|
372
|
+
if (!parent.xdgSurface) parent.layerSurface.$.get_popup(popup.id);
|
|
373
|
+
this.role = popup;
|
|
374
|
+
this.popup = popup;
|
|
375
|
+
popup.on('configure', (px, py, pw, ph) => {
|
|
376
|
+
this.x = px;
|
|
377
|
+
this.y = py;
|
|
378
|
+
if (pw > 0 && ph > 0) {
|
|
379
|
+
const changed = pw !== this.width || ph !== this.height;
|
|
380
|
+
this.width = pw;
|
|
381
|
+
this.height = ph;
|
|
382
|
+
if (changed) this.emit('resize', { width: pw, height: ph });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
popup.on('popup_done', () => this.emit('close'));
|
|
386
|
+
popup.on('repositioned', () => {});
|
|
387
|
+
if (grab) popup.$.grab(grab.seat.id, grab.serial);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ---- events --------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
_wireCommon() {
|
|
393
|
+
this.xdgSurface?.on('configure', (serial) =>
|
|
394
|
+
this._onShellConfigure(serial),
|
|
395
|
+
);
|
|
396
|
+
this.surface.on('preferred_buffer_scale', (scale) => {
|
|
397
|
+
// Only authoritative when fractional scale is not in play.
|
|
398
|
+
if (scale > 0) this._preferredBufferScale = scale;
|
|
399
|
+
if (!this._fractional && scale > 0 && scale !== this.scale)
|
|
400
|
+
this._setScale(scale);
|
|
401
|
+
});
|
|
402
|
+
this.surface.on('enter', (output) => {
|
|
403
|
+
this.outputs.add(output);
|
|
404
|
+
this.emit('outputs', this.outputs);
|
|
405
|
+
});
|
|
406
|
+
this.surface.on('leave', (output) => {
|
|
407
|
+
if (this.outputs.delete(output)) this.emit('outputs', this.outputs);
|
|
408
|
+
});
|
|
409
|
+
this.surface.on('preferred_buffer_transform', () => {});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The KDE protocol's `mode` event is not part of a configure sequence
|
|
414
|
+
* (ssd.js), so a change of heart after the window is up would otherwise
|
|
415
|
+
* sit in `pending` waiting for a configure that nothing promises. Flush it
|
|
416
|
+
* in a microtask: a configure in the same batch of messages still gets
|
|
417
|
+
* there first and adopts it the ordinary way, and only a mode that really
|
|
418
|
+
* arrived alone is applied on its own — `setMargins` already knows how to
|
|
419
|
+
* resize outside a configure.
|
|
420
|
+
*
|
|
421
|
+
* Before the first configure there is nothing to flush: that configure is
|
|
422
|
+
* coming, and riding it keeps the first frame at the right insets.
|
|
423
|
+
*/
|
|
424
|
+
_wireDecoration() {
|
|
425
|
+
const decoration = this.decoration;
|
|
426
|
+
if (!decoration?.flushesOutsideConfigure) return;
|
|
427
|
+
decoration.on('pending', () => {
|
|
428
|
+
if (!this.configured) return;
|
|
429
|
+
queueMicrotask(() => {
|
|
430
|
+
if (this.destroyed) return;
|
|
431
|
+
if (decoration.adopt()) this.emit('decorationmode', decoration.mode);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The shell's configure — xdg_surface's, or the layer surface's with its
|
|
438
|
+
* size already taken — holds the serial for the frame that adopts it. A
|
|
439
|
+
* decoration mode that arrived with it is adopted first, so a listener
|
|
440
|
+
* hears 'decorationmode' before 'configure' and paints at the new insets.
|
|
441
|
+
*/
|
|
442
|
+
_onShellConfigure(serial) {
|
|
443
|
+
this._pendingSerial = serial;
|
|
444
|
+
if (this.decoration?.adopt()) {
|
|
445
|
+
this.emit('decorationmode', this.decoration.mode);
|
|
446
|
+
}
|
|
447
|
+
const first = !this.configured;
|
|
448
|
+
this.configured = true;
|
|
449
|
+
if (first) this._onConfigured?.();
|
|
450
|
+
this.emit('configure', {
|
|
451
|
+
width: this.width,
|
|
452
|
+
height: this.height,
|
|
453
|
+
states: this.states,
|
|
454
|
+
serial,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
_wireToplevel() {
|
|
459
|
+
this.toplevel.on('configure', (width, height, states) => {
|
|
460
|
+
// 0x0 is "pick your own" — the compositor is not imposing a size.
|
|
461
|
+
this.sizeImposed = width > 0 && height > 0;
|
|
462
|
+
const next = decodeStates(states);
|
|
463
|
+
// The configured size is the window geometry's. The surface is that
|
|
464
|
+
// plus the shadow's margin, which depends on the states in this same
|
|
465
|
+
// configure: a window that is being maximised loses its margin in the
|
|
466
|
+
// configure that maximises it.
|
|
467
|
+
const was = this.margins;
|
|
468
|
+
const m = this.marginsFor?.(next) ?? was;
|
|
469
|
+
const gw = width > 0 ? width : this.width - was.left - was.right;
|
|
470
|
+
const gh = height > 0 ? height : this.height - was.top - was.bottom;
|
|
471
|
+
const w = Math.max(1, gw + m.left + m.right);
|
|
472
|
+
const h = Math.max(1, gh + m.top + m.bottom);
|
|
473
|
+
this.margins = m;
|
|
474
|
+
if (w !== this.width || h !== this.height) {
|
|
475
|
+
this.width = w;
|
|
476
|
+
this.height = h;
|
|
477
|
+
this.emit('resize', { width: w, height: h });
|
|
478
|
+
}
|
|
479
|
+
const wasStates = this.states;
|
|
480
|
+
this.states = next;
|
|
481
|
+
if (!sameSet(wasStates, next)) this.emit('statechange', [...next]);
|
|
482
|
+
});
|
|
483
|
+
this.toplevel.on('close', () => this.emit('close'));
|
|
484
|
+
this.toplevel.on('configure_bounds', (w, h) => {
|
|
485
|
+
// The size the compositor recommends fitting in — a monitor less its
|
|
486
|
+
// panels, on GNOME. 0×0 withdraws it. The closest thing this protocol
|
|
487
|
+
// has to a work area, so outputs.js listens.
|
|
488
|
+
this.bounds = w > 0 && h > 0 ? { width: w, height: h } : null;
|
|
489
|
+
this.emit('bounds', this.bounds);
|
|
490
|
+
});
|
|
491
|
+
this.toplevel.on('wm_capabilities', (caps) => {
|
|
492
|
+
this.wmCapabilities = decodeStates(caps);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Attach fractional scale and a viewport, when the compositor offers them. */
|
|
497
|
+
useScaling({ fractionalScaleManager, viewporter }) {
|
|
498
|
+
if (viewporter && !this._viewport) {
|
|
499
|
+
this._viewport = viewporter.$.get_viewport(this.surface.id);
|
|
500
|
+
}
|
|
501
|
+
if (fractionalScaleManager && !this._fractional) {
|
|
502
|
+
this._fractional = fractionalScaleManager.$.get_fractional_scale(
|
|
503
|
+
this.surface.id,
|
|
504
|
+
);
|
|
505
|
+
this._fractional.on('preferred_scale', (numerator) => {
|
|
506
|
+
const scale = numerator / SCALE_DENOM;
|
|
507
|
+
if (scale > 0 && scale !== this.scale) this._setScale(scale);
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* The scale of the output(s) under the surface, as the last resort.
|
|
514
|
+
*
|
|
515
|
+
* Three sources, in order of authority: fractional-scale-v1 (a fraction,
|
|
516
|
+
* per surface), `wl_surface.preferred_buffer_scale` (an integer, per
|
|
517
|
+
* surface, wl_compositor 6), and this — the integer `wl_output.scale` of
|
|
518
|
+
* whatever the surface has entered, which is all a compositor with neither
|
|
519
|
+
* of the first two offers. Ignored the moment either of them has spoken.
|
|
520
|
+
*/
|
|
521
|
+
noteOutputScale(scale) {
|
|
522
|
+
if (this._fractional || this._preferredBufferScale != null) return;
|
|
523
|
+
if (scale > 0 && scale !== this.scale) this._setScale(scale);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
_setScale(scale) {
|
|
527
|
+
this.scale = scale;
|
|
528
|
+
this._applyScale();
|
|
529
|
+
this.emit('scale', scale);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Tell the compositor how the buffer maps onto the surface.
|
|
534
|
+
*
|
|
535
|
+
* With a viewport the buffer can be any size and the destination is the
|
|
536
|
+
* logical size, which is how a fractional scale is expressed (a buffer
|
|
537
|
+
* scale must be an integer). Without one, the buffer scale carries it.
|
|
538
|
+
*/
|
|
539
|
+
_applyScale() {
|
|
540
|
+
if (this._viewport) {
|
|
541
|
+
this._viewport.$.set_destination(this.width, this.height);
|
|
542
|
+
if (this.surface.version >= 3) this.surface.$.set_buffer_scale(1);
|
|
543
|
+
} else if (this.surface.version >= 3) {
|
|
544
|
+
this.surface.$.set_buffer_scale(Math.max(1, Math.round(this.scale)));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** The size the backing buffer has to be for the current scale. */
|
|
549
|
+
get bufferWidth() {
|
|
550
|
+
return Math.max(1, Math.round(this.width * this.scale));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
get bufferHeight() {
|
|
554
|
+
return Math.max(1, Math.round(this.height * this.scale));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Resolves once the first configure has arrived. */
|
|
558
|
+
get whenConfigured() {
|
|
559
|
+
return this._configured;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Acknowledge the configure the next commit adopts, and record the surface
|
|
564
|
+
* geometry it implies. Called by the frame that was painted at this size,
|
|
565
|
+
* immediately before its commit.
|
|
566
|
+
*/
|
|
567
|
+
ackPending() {
|
|
568
|
+
if (this._pendingSerial != null) {
|
|
569
|
+
(this.xdgSurface ?? this.layerSurface).$.ack_configure(
|
|
570
|
+
this._pendingSerial,
|
|
571
|
+
);
|
|
572
|
+
this._pendingSerial = null;
|
|
573
|
+
}
|
|
574
|
+
const m = this.margins;
|
|
575
|
+
const g = this._geometry;
|
|
576
|
+
if (!g || g.width !== this.width || g.height !== this.height || g.m !== m) {
|
|
577
|
+
this._geometry = { width: this.width, height: this.height, m };
|
|
578
|
+
// The window is the surface less a client-side shadow's margin; a
|
|
579
|
+
// layer surface has no window geometry — the surface is the window.
|
|
580
|
+
const gw = Math.max(1, this.width - m.left - m.right);
|
|
581
|
+
const gh = Math.max(1, this.height - m.top - m.bottom);
|
|
582
|
+
this.xdgSurface?.$.set_window_geometry(m.left, m.top, gw, gh);
|
|
583
|
+
this._applyInputRegion(m, gw, gh);
|
|
584
|
+
this._applyScale();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* New margins, keeping the window geometry: the surface grows or shrinks
|
|
590
|
+
* round the window, which the compositor sized and placed. Margins change
|
|
591
|
+
* outside a configure too — a decoration mode the compositor settled after
|
|
592
|
+
* the first frame (sway answers server-side, then client-side), and the
|
|
593
|
+
* size has to follow, or the window shrinks by its own shadow.
|
|
594
|
+
*/
|
|
595
|
+
setMargins(m) {
|
|
596
|
+
const was = this.margins;
|
|
597
|
+
if (!m || m === was) return;
|
|
598
|
+
const w = Math.max(1, this.width - was.left - was.right + m.left + m.right);
|
|
599
|
+
const h = Math.max(
|
|
600
|
+
1,
|
|
601
|
+
this.height - was.top - was.bottom + m.top + m.bottom,
|
|
602
|
+
);
|
|
603
|
+
this.margins = m;
|
|
604
|
+
if (w !== this.width || h !== this.height) {
|
|
605
|
+
this.width = w;
|
|
606
|
+
this.height = h;
|
|
607
|
+
this.emit('resize', { width: w, height: h });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Input goes to the window and a resize band round it, not to the rest of
|
|
613
|
+
* the shadow — a press on the soft edge of a shadow belongs to what is
|
|
614
|
+
* under it. With no margin, the whole surface.
|
|
615
|
+
*/
|
|
616
|
+
_applyInputRegion(m, gw, gh) {
|
|
617
|
+
if (!this.compositor || !this.xdgSurface) return;
|
|
618
|
+
const shadowed = m.left || m.top || m.right || m.bottom;
|
|
619
|
+
if (!shadowed && !this._inputRegionSet) return;
|
|
620
|
+
const band = shadowed ? INPUT_BAND : 0;
|
|
621
|
+
const x = Math.max(0, m.left - band);
|
|
622
|
+
const y = Math.max(0, m.top - band);
|
|
623
|
+
const right = Math.min(this.width, m.left + gw + band);
|
|
624
|
+
const bottom = Math.min(this.height, m.top + gh + band);
|
|
625
|
+
const region = this.compositor.$.create_region();
|
|
626
|
+
region.$.add(x, y, right - x, bottom - y);
|
|
627
|
+
this.surface.$.set_input_region(region.id);
|
|
628
|
+
region.$.destroy();
|
|
629
|
+
this._inputRegionSet = true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Who draws the frame: 'server' once a compositor has agreed to, else 'client'. */
|
|
633
|
+
get decorationMode() {
|
|
634
|
+
return this.decoration?.mode ?? 'client';
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Whether the compositor has sent a configure we have not adopted yet. */
|
|
638
|
+
get configurePending() {
|
|
639
|
+
return this._pendingSerial != null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ---- frame clock ---------------------------------------------------------
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Ask to be told when it is a good time to draw the next frame, and hand
|
|
646
|
+
* back the promise that says so.
|
|
647
|
+
*
|
|
648
|
+
* **Call this before the frame's commit, and await it after.** A frame
|
|
649
|
+
* request is only *delivered* by the next commit, so the canonical loop
|
|
650
|
+
* puts the request and the buffer in the same commit. Committing
|
|
651
|
+
* separately to deliver the request costs a whole refresh period — the
|
|
652
|
+
* compositor takes the empty commit as the frame, and the next real one
|
|
653
|
+
* lands a vblank late — which halves the frame rate with nothing in a
|
|
654
|
+
* screenshot to show for it.
|
|
655
|
+
*
|
|
656
|
+
* The library writes a request's bytes synchronously before returning its
|
|
657
|
+
* promise, so not awaiting this until after the commit is what sequences
|
|
658
|
+
* the two.
|
|
659
|
+
*
|
|
660
|
+
* @returns {Promise<number>} the frame timestamp, in the compositor's
|
|
661
|
+
* millisecond clock
|
|
662
|
+
*/
|
|
663
|
+
scheduleFrame() {
|
|
664
|
+
if (this.destroyed) return Promise.resolve(0);
|
|
665
|
+
const done = this.surface.frame().then((time) => {
|
|
666
|
+
if (!this.destroyed) this.emit('frame', time);
|
|
667
|
+
return time;
|
|
668
|
+
});
|
|
669
|
+
// The caller attaches its handlers after an await or two; a connection
|
|
670
|
+
// dying in that window (process exit under Bun kills the reader thread
|
|
671
|
+
// first) would otherwise surface as an unhandled rejection. Marking it
|
|
672
|
+
// handled here changes nothing for a caller that does handle it.
|
|
673
|
+
done.catch(() => {});
|
|
674
|
+
return done;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ---- requests ------------------------------------------------------------
|
|
678
|
+
|
|
679
|
+
setTitle(title) {
|
|
680
|
+
this.toplevel?.$.set_title(String(title ?? ''));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
setAppId(appId) {
|
|
684
|
+
this.toplevel?.$.set_app_id(String(appId ?? ''));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Size limits, in surface pixels like everything else here; the
|
|
688
|
+
* compositor is told the window's, without the shadow's margin. */
|
|
689
|
+
setMinSize(width, height) {
|
|
690
|
+
const m = this.margins;
|
|
691
|
+
this.toplevel?.$.set_min_size(
|
|
692
|
+
Math.max(0, (width - m.left - m.right) | 0),
|
|
693
|
+
Math.max(0, (height - m.top - m.bottom) | 0),
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
setMaxSize(width, height) {
|
|
698
|
+
const m = this.margins;
|
|
699
|
+
this.toplevel?.$.set_max_size(
|
|
700
|
+
Math.max(0, (width - m.left - m.right) | 0),
|
|
701
|
+
Math.max(0, (height - m.top - m.bottom) | 0),
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
setParent(parent) {
|
|
706
|
+
if (!this.toplevel) return;
|
|
707
|
+
// null clears the parent
|
|
708
|
+
this.toplevel.$.set_parent(parent?.toplevel ?? null);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
maximize(on = true) {
|
|
712
|
+
if (!this.toplevel) return;
|
|
713
|
+
if (on) this.toplevel.$.set_maximized();
|
|
714
|
+
else this.toplevel.$.unset_maximized();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
fullscreen(on = true) {
|
|
718
|
+
if (!this.toplevel) return;
|
|
719
|
+
// a null output: whichever the compositor puts a fullscreen window on
|
|
720
|
+
if (on) this.toplevel.$.set_fullscreen(null);
|
|
721
|
+
else this.toplevel.$.unset_fullscreen();
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
minimize() {
|
|
725
|
+
this.toplevel?.$.set_minimized();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Start an interactive move or resize.
|
|
730
|
+
*
|
|
731
|
+
* There is no other way to move a window: the client cannot place itself,
|
|
732
|
+
* so a titlebar drag is a *request* that hands the gesture to the
|
|
733
|
+
* compositor, which is also why it needs the serial of the button press
|
|
734
|
+
* that started it.
|
|
735
|
+
*/
|
|
736
|
+
startMove(seatProxy, serial) {
|
|
737
|
+
this.toplevel?.$.move(seatProxy.id, serial);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
startResize(seatProxy, serial, edges) {
|
|
741
|
+
this.toplevel?.$.resize(seatProxy.id, serial, edges);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** `x`/`y` surface-local; the request wants them in window geometry. */
|
|
745
|
+
showWindowMenu(seatProxy, serial, x, y) {
|
|
746
|
+
const m = this.margins;
|
|
747
|
+
this.toplevel?.$.show_window_menu(
|
|
748
|
+
seatProxy.id,
|
|
749
|
+
serial,
|
|
750
|
+
(x - m.left) | 0,
|
|
751
|
+
(y - m.top) | 0,
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** Reposition a popup (xdg_popup v3): a new positioner, same surface. */
|
|
756
|
+
reposition(wmBase, { x, y, width, height }) {
|
|
757
|
+
if (this._popupSetup) {
|
|
758
|
+
// No role yet: this is the placement the initial commit will use.
|
|
759
|
+
this._popupSetup.placement = { x, y, width, height, anchorRect: null };
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
if (!this.popup || this.popup.version < 3) return false;
|
|
763
|
+
const p = popupPositioner(wmBase, { x, y, width, height });
|
|
764
|
+
this.popup.$.reposition(
|
|
765
|
+
p.id,
|
|
766
|
+
++this._repositionToken || (this._repositionToken = 1),
|
|
767
|
+
);
|
|
768
|
+
p.$.destroy();
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* A popup's explicit grab. Only possible before its initial commit: the
|
|
774
|
+
* compositor finishes a popup's setup on that commit, and a grab after it
|
|
775
|
+
* is `invalid_grab` — a fatal protocol error, "tried to grab after popup
|
|
776
|
+
* was mapped". Every `<popup grab>` hit it while the grab rode the tree's
|
|
777
|
+
* map and the commit rode creation.
|
|
778
|
+
*
|
|
779
|
+
* @returns {boolean} whether the grab was sent
|
|
780
|
+
*/
|
|
781
|
+
takeGrab(seat, serial) {
|
|
782
|
+
if (this.kind !== 'popup' || !this._popupSetup || this.destroyed)
|
|
783
|
+
return false;
|
|
784
|
+
this._popupSetup.grab = { seat, serial };
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** The initial commit of a popup made with `commit: false`. */
|
|
789
|
+
commitInitial() {
|
|
790
|
+
if (!this._initialCommitPending || this.destroyed) return;
|
|
791
|
+
this._initialCommitPending = false;
|
|
792
|
+
if (this._popupSetup) this._assignPopupRole();
|
|
793
|
+
this._setupDone = true;
|
|
794
|
+
this.surface.$.commit();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** Whether a popup is still waiting for its initial commit. */
|
|
798
|
+
get initialCommitPending() {
|
|
799
|
+
return this._initialCommitPending;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** Hide without destroying: a null buffer unmaps the surface. */
|
|
803
|
+
unmap() {
|
|
804
|
+
if (this.destroyed || !this.mapped) return;
|
|
805
|
+
this.surface.$.attach(null, 0, 0);
|
|
806
|
+
this.surface.$.commit();
|
|
807
|
+
this.mapped = false;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
destroy() {
|
|
811
|
+
if (this.destroyed) return;
|
|
812
|
+
this.destroyed = true;
|
|
813
|
+
// Order matters: the decoration before the toplevel it decorates, the
|
|
814
|
+
// role before the surface it wraps.
|
|
815
|
+
try {
|
|
816
|
+
this.decoration?.destroy();
|
|
817
|
+
this._fractional?.$.destroy?.();
|
|
818
|
+
this._viewport?.$.destroy?.();
|
|
819
|
+
this.role?.$.destroy?.();
|
|
820
|
+
this.xdgSurface?.$.destroy?.();
|
|
821
|
+
this.surface.$.destroy?.();
|
|
822
|
+
} catch {
|
|
823
|
+
/* the connection may already be gone */
|
|
824
|
+
}
|
|
825
|
+
this.emit('destroyed');
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* A compositor pings to check the client is alive and kills it if it does
|
|
831
|
+
* not answer. Once per `xdg_wm_base`, which is once per connection.
|
|
832
|
+
*/
|
|
833
|
+
function wirePing(wmBase) {
|
|
834
|
+
if (wmBase._pingWired) return;
|
|
835
|
+
wmBase._pingWired = true;
|
|
836
|
+
wmBase.on('ping', (serial) => wmBase.$.pong(serial));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/** `xdg_toplevel.configure`'s states arrive as a wl_array of uint32. */
|
|
840
|
+
function decodeStates(states) {
|
|
841
|
+
const out = new Set();
|
|
842
|
+
if (!states) return out;
|
|
843
|
+
const bytes = states instanceof Uint8Array ? states : new Uint8Array(states);
|
|
844
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
845
|
+
for (let i = 0; i + 4 <= bytes.byteLength; i += 4)
|
|
846
|
+
out.add(view.getUint32(i, true));
|
|
847
|
+
return out;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function sameSet(a, b) {
|
|
851
|
+
if (a.size !== b.size) return false;
|
|
852
|
+
for (const v of a) if (!b.has(v)) return false;
|
|
853
|
+
return true;
|
|
854
|
+
}
|