react-x11 2.13.0 → 2.14.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/index.d.ts +5 -0
- package/src/nodes/boxpaint.js +9 -0
- package/src/nodes/preedit.js +64 -14
- package/src/scale.js +52 -22
- package/src/screencolor.js +104 -17
- package/src/wayland/app.js +560 -0
- package/src/wayland/backendwindow.js +1123 -0
- package/src/wayland/clipboard.js +326 -0
- package/src/wayland/connection.js +482 -0
- package/src/wayland/context2d.js +2133 -0
- package/src/wayland/decorations.js +476 -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/glarea.js +371 -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 +127 -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 +106 -0
- package/src/wayland/surface.js +123 -0
- package/src/wayland/swapchain.js +411 -0
- package/src/wayland/tablet.js +522 -0
- package/src/wayland/target.js +263 -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 +827 -0
- package/src/wayland/xkb.js +425 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
// The backend object `createRoot({ backend: 'wayland' })` renders through.
|
|
2
|
+
//
|
|
3
|
+
// react-x11 has three of these — an ntk X connection, `src/cocoa/app.js`,
|
|
4
|
+
// and this — and the shape they share is the contract: an app makes windows
|
|
5
|
+
// and offscreen surfaces, owns the fonts and the clipboard, and routes input;
|
|
6
|
+
// a window hands out a 2d context and tells the renderer when it may paint;
|
|
7
|
+
// everything above `src/nodes/` is supposed not to know which one it got.
|
|
8
|
+
//
|
|
9
|
+
// All the asynchrony in a Wayland client lives in `open()`, on purpose.
|
|
10
|
+
// Binding a global needs the registry and the registry needs a round trip —
|
|
11
|
+
// but that is the only part of making a window that does, so it is done
|
|
12
|
+
// once here and `createWindow` is synchronous, which React's commit phase
|
|
13
|
+
// requires (windows are realised inside it).
|
|
14
|
+
//
|
|
15
|
+
// `X` is a stand-in, not an X connection. The renderer reaches
|
|
16
|
+
// `app.X.keycode2keysyms` for accelerators, `app.X.on('end')` to notice the
|
|
17
|
+
// connection going, and a few guarded properties; the stand-in answers
|
|
18
|
+
// those and nothing else, and the keysym table on it comes from the
|
|
19
|
+
// compositor's keymap (xkb.js).
|
|
20
|
+
//
|
|
21
|
+
// The long-term home for all of this is ntk, beside its `Window` and behind
|
|
22
|
+
// the same `App`/`Drawable` contracts — the RFC's open question 2. It lives
|
|
23
|
+
// here because that is where it can be read and run without vendoring a
|
|
24
|
+
// second copy of the toolkit.
|
|
25
|
+
|
|
26
|
+
import { EventEmitter } from 'node:events';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import { WaylandConnection } from './connection.js';
|
|
29
|
+
import { WaylandSeat } from './seat.js';
|
|
30
|
+
import { WaylandBackendWindow } from './backendwindow.js';
|
|
31
|
+
import { WaylandSurface } from './surface.js';
|
|
32
|
+
import { InputRouter } from './input.js';
|
|
33
|
+
import { WaylandTouch } from './touch.js';
|
|
34
|
+
import { WaylandTablet } from './tablet.js';
|
|
35
|
+
import { createWaylandClipboard } from './clipboard.js';
|
|
36
|
+
import { WaylandOutputs } from './outputs.js';
|
|
37
|
+
import { WaylandTextInput } from './textinput.js';
|
|
38
|
+
import { WaylandDnd } from './dnd.js';
|
|
39
|
+
import { createScreenCapture } from './screencopy.js';
|
|
40
|
+
import { sharedGpu } from './glcontext.js';
|
|
41
|
+
import { TextShaper } from './text.js';
|
|
42
|
+
import { FrameStyle } from './framestyle.js';
|
|
43
|
+
import { watchAppearance } from '../appearance.js';
|
|
44
|
+
import { WaylandGLArea, WaylandOverlayPane, GLAREA_VISUAL } from './glarea.js';
|
|
45
|
+
import { pinnedScale, setScaleForTests, traceScale } from '../scale.js';
|
|
46
|
+
import { setCompositingForTests } from '../compositing.js';
|
|
47
|
+
|
|
48
|
+
const require = createRequire(import.meta.url);
|
|
49
|
+
|
|
50
|
+
/** The X connection stand-in. */
|
|
51
|
+
class XStandIn extends EventEmitter {
|
|
52
|
+
constructor(app) {
|
|
53
|
+
super();
|
|
54
|
+
this.setMaxListeners(0);
|
|
55
|
+
this.app = app;
|
|
56
|
+
this.keycode2keysyms = [];
|
|
57
|
+
this._closing = false;
|
|
58
|
+
// `_refreshScreenOrigin` and friends bail on a null root, which is right:
|
|
59
|
+
// no Wayland client knows where it is on the screen.
|
|
60
|
+
this.display = { screen: [{ root: null }], Render: null };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
require(name) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`react-x11 (wayland): X.require('${name}') — there is no X server behind this app`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Selections are the clipboard's business (src/wayland/clipboard.js); the
|
|
70
|
+
// text controls' direct calls are accepted and routed there.
|
|
71
|
+
SetSelectionOwner(_wid, atom) {
|
|
72
|
+
void atom;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ChangeWindowAttributes() {}
|
|
76
|
+
flush() {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class WaylandApp extends EventEmitter {
|
|
80
|
+
constructor(conn, options) {
|
|
81
|
+
super();
|
|
82
|
+
this.setMaxListeners(0);
|
|
83
|
+
this.conn = conn;
|
|
84
|
+
this.options = options;
|
|
85
|
+
this.backend = 'wayland';
|
|
86
|
+
/** wl_surface id -> WaylandBackendWindow */
|
|
87
|
+
this.windows = new Map();
|
|
88
|
+
this.toplevels = [];
|
|
89
|
+
this.seat = null;
|
|
90
|
+
this.input = null;
|
|
91
|
+
/** the seat's touch and tablet devices; the tablet is null without the protocol */
|
|
92
|
+
this.touch = null;
|
|
93
|
+
this.tablet = null;
|
|
94
|
+
this.fonts = null;
|
|
95
|
+
this.fontManager = null;
|
|
96
|
+
this.clipboard = null;
|
|
97
|
+
/** the seat's `zwp_text_input_v3`, or null where the compositor has none */
|
|
98
|
+
this.textInput = null;
|
|
99
|
+
this.dnd = null;
|
|
100
|
+
this.compositor = null;
|
|
101
|
+
this.wmBase = null;
|
|
102
|
+
this.dmabuf = null;
|
|
103
|
+
this.viewporter = null;
|
|
104
|
+
this.fractionalScale = null;
|
|
105
|
+
this.activation = null;
|
|
106
|
+
/** the monitors, published into src/screens.js (outputs.js) */
|
|
107
|
+
this.outputs = null;
|
|
108
|
+
/** xdg-decoration, layer-shell, shm: null where the compositor has none */
|
|
109
|
+
this.decorationManager = null;
|
|
110
|
+
this.layerShell = null;
|
|
111
|
+
this.shm = null;
|
|
112
|
+
/** screen capture and the eyedropper (screencopy.js); null on GNOME */
|
|
113
|
+
this.screenCapture = null;
|
|
114
|
+
this.X = new XStandIn(this);
|
|
115
|
+
this.display = this.X.display;
|
|
116
|
+
this.gpu = null;
|
|
117
|
+
this.gl = null;
|
|
118
|
+
this._holder = null;
|
|
119
|
+
this._currentSurface = null;
|
|
120
|
+
/** the app-wide output scale: buffer pixels per surface pixel, which
|
|
121
|
+
* windows size their buffers and convert input by (`layoutScale` is
|
|
122
|
+
* what the renderer lays out with) */
|
|
123
|
+
this.scale = 1;
|
|
124
|
+
/** REACT_X11_SCALE, else a numeric createRoot({ scale }): pinned */
|
|
125
|
+
this._pinnedScale = null;
|
|
126
|
+
/** the resolution class's factor over a compositor that says 1 */
|
|
127
|
+
this._zoom = 1;
|
|
128
|
+
/** `layoutScale` as last published to src/scale.js */
|
|
129
|
+
this._publishedScale = null;
|
|
130
|
+
this._shaper = null;
|
|
131
|
+
this.frameText = null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static async open(options = {}) {
|
|
135
|
+
const conn = await WaylandConnection.open({
|
|
136
|
+
display: options.waylandDisplay ?? options.display,
|
|
137
|
+
transport: options.transport,
|
|
138
|
+
});
|
|
139
|
+
const app = new WaylandApp(conn, options);
|
|
140
|
+
conn.on('error', (err) => app.emit('error', err));
|
|
141
|
+
conn.on('close', () => {
|
|
142
|
+
app.X._closing = true;
|
|
143
|
+
app.X.emit('end');
|
|
144
|
+
app.emit('close');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
app.compositor = await conn.require('wl_compositor');
|
|
148
|
+
app.wmBase = await conn.require('xdg_wm_base');
|
|
149
|
+
app.dmabuf = await conn.bind('zwp_linux_dmabuf_v1');
|
|
150
|
+
app.viewporter = await conn.bind('wp_viewporter');
|
|
151
|
+
app.fractionalScale = await conn.bind('wp_fractional_scale_manager_v1');
|
|
152
|
+
app.activation = await conn.bind('xdg_activation_v1');
|
|
153
|
+
// The scale session first, so the outputs have a per-monitor map to
|
|
154
|
+
// fill; then the outputs, awaited like the X11 backend's Xinerama tier:
|
|
155
|
+
// an auto-sized window clamps to a monitor synchronously inside
|
|
156
|
+
// realize(), and the first window is laid out at whatever `app.scale`
|
|
157
|
+
// says by then — which the densest output seeds (`noteScale`). The
|
|
158
|
+
// session exists before createRoot's `beginScale`, which therefore
|
|
159
|
+
// defers to it: the pin is read here instead, from the same options.
|
|
160
|
+
await app._beginOutputs(conn, options);
|
|
161
|
+
app.decorationManager = await conn.bind('zxdg_decoration_manager_v1');
|
|
162
|
+
app.layerShell = await conn.bind('zwlr_layer_shell_v1');
|
|
163
|
+
app.shm = await conn.bind('wl_shm');
|
|
164
|
+
app.seat = await WaylandSeat.bind(conn);
|
|
165
|
+
app.input = new InputRouter(app);
|
|
166
|
+
// both drive the router through the seat's own pointer events
|
|
167
|
+
app.touch = WaylandTouch.attach(app.seat);
|
|
168
|
+
app.tablet = await WaylandTablet.bind(conn, app.seat);
|
|
169
|
+
app.clipboard = await createWaylandClipboard({
|
|
170
|
+
conn,
|
|
171
|
+
seat: app.seat.seat,
|
|
172
|
+
serial: () => app.seat.lastSerial,
|
|
173
|
+
});
|
|
174
|
+
// The compositor's input method (textinput.js): enter/leave follow the
|
|
175
|
+
// keyboard, and the windows keep it told about their focused field.
|
|
176
|
+
app.textInput = await WaylandTextInput.create(app);
|
|
177
|
+
// Drag and drop shares the clipboard's `wl_data_device` — the DnD events
|
|
178
|
+
// arrive on the same object its selection events do (src/wayland/dnd.js).
|
|
179
|
+
if (app.clipboard?.dataDevice && app.clipboard?.manager) {
|
|
180
|
+
app.dnd = new WaylandDnd({
|
|
181
|
+
app,
|
|
182
|
+
device: app.clipboard.dataDevice,
|
|
183
|
+
manager: app.clipboard.manager,
|
|
184
|
+
seat: app.seat,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
app.screenCapture = await createScreenCapture(app);
|
|
188
|
+
|
|
189
|
+
const ntk = require('ntk');
|
|
190
|
+
// `app.fonts` is the name the renderer reaches for (src/fonts.js): one
|
|
191
|
+
// FontManager per connection, so a face registered by `loadFont` is
|
|
192
|
+
// visible to every window and there is one glyph cache rather than two.
|
|
193
|
+
app.fonts = new ntk.FontManager({ source: options.fontSource ?? 'system' });
|
|
194
|
+
app.fontManager = app.fonts;
|
|
195
|
+
app._shaper = new TextShaper(app.fonts);
|
|
196
|
+
app.frameText = (text, size, weight, family = 'sans-serif') =>
|
|
197
|
+
app._shaper.measureSync(text, { family, size, weight });
|
|
198
|
+
// What the desktop says a frame looks like and does (framestyle.js):
|
|
199
|
+
// read before the first window is sized — the title's font sets the
|
|
200
|
+
// titlebar's height — and followed after, with the light or dark the
|
|
201
|
+
// frame takes from the appearance.
|
|
202
|
+
app.frameStyle = await new FrameStyle().start();
|
|
203
|
+
app.frameStyle.on('change', () => app._frameStyleChanged());
|
|
204
|
+
app._unwatchAppearance = watchAppearance(() => app._frameStyleChanged());
|
|
205
|
+
|
|
206
|
+
// The GPU context is shared by every window and surface; make it now so
|
|
207
|
+
// an offscreen surface created before the first window has one to use.
|
|
208
|
+
const dri = require('x11-dri');
|
|
209
|
+
app.dri = dri;
|
|
210
|
+
app.gpu = sharedGpu(dri, {
|
|
211
|
+
format: dri.FORMAT.ARGB8888,
|
|
212
|
+
depthSize: options.glPolicy?.depthSize ?? 16,
|
|
213
|
+
stencilSize: options.glPolicy?.stencilSize ?? 8,
|
|
214
|
+
devicePath: options.glPolicy?.devicePath,
|
|
215
|
+
});
|
|
216
|
+
app.gl = app.gpu.gl;
|
|
217
|
+
app._glCapsResolved = { direct: true };
|
|
218
|
+
// Every Wayland surface is composited: the compositor blends each
|
|
219
|
+
// buffer by its alpha, so `useSupports('transparency')` and the
|
|
220
|
+
// `'@supports transparency'` style block are true — the answer the cocoa
|
|
221
|
+
// backend gives too. Without it, tooltips and menus took the square,
|
|
222
|
+
// opaque look meant for an X server with no compositor running.
|
|
223
|
+
setCompositingForTests(app, true);
|
|
224
|
+
return app;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- GL context plumbing ----------------------------------------------
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Make the shared GL context current. Drawing into a render target needs
|
|
231
|
+
* *a* current surface; any window's will do, and when there is none yet a
|
|
232
|
+
* 1×1 holder surface stands in.
|
|
233
|
+
*/
|
|
234
|
+
makeCurrent() {
|
|
235
|
+
if (this._currentSurface) {
|
|
236
|
+
try {
|
|
237
|
+
this.gpu.makeCurrent(this._currentSurface);
|
|
238
|
+
return;
|
|
239
|
+
} catch {
|
|
240
|
+
this._currentSurface = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const w of this.windows.values()) {
|
|
244
|
+
const s = w.glctx?.chain?.gbm;
|
|
245
|
+
if (s) {
|
|
246
|
+
this._currentSurface = s;
|
|
247
|
+
this.gpu.makeCurrent(s);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!this._holder) this._holder = this.gpu.createSurface(1, 1);
|
|
252
|
+
this._currentSurface = this._holder;
|
|
253
|
+
this.gpu.makeCurrent(this._holder);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** After an offscreen render, put the window's backing target back. */
|
|
257
|
+
rebindWindowTarget() {
|
|
258
|
+
for (const w of this.windows.values()) {
|
|
259
|
+
if (w._frameSize) {
|
|
260
|
+
w.glctx.bindBacking();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---- windows ----------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Make a window. **Synchronous**, because `WindowNode.realize()` runs in
|
|
270
|
+
* React's commit phase and calls `setTitle`/`getContext` on the result the
|
|
271
|
+
* moment it has it. `overrideRedirect` makes it a popup, positioned by the
|
|
272
|
+
* `x`/`y` the tree's anchoring computed, relative to its parent.
|
|
273
|
+
*/
|
|
274
|
+
createWindow(attributes = {}) {
|
|
275
|
+
// A `<glarea>`'s child "window" is a rect of its parent's backing target
|
|
276
|
+
// (glarea.js); `chooseGLConfig` marked the visual so it can be told apart.
|
|
277
|
+
if (attributes.visual === GLAREA_VISUAL) {
|
|
278
|
+
if (!attributes.parent?.glctx) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
'react-x11 (wayland): a <glarea> needs a window of this backend to draw into',
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return new WaylandGLArea(attributes.parent, attributes);
|
|
284
|
+
}
|
|
285
|
+
const win = new WaylandBackendWindow(this, attributes);
|
|
286
|
+
this.windows.set(win.wl.surface.id, win);
|
|
287
|
+
if (!win.isPopup) this.toplevels.push(win);
|
|
288
|
+
win.on('_destroyed', () => this._forgetWindow(win));
|
|
289
|
+
return win;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_forgetWindow(win) {
|
|
293
|
+
if (this.windows.get(win.wl.surface.id) === win)
|
|
294
|
+
this.windows.delete(win.wl.surface.id);
|
|
295
|
+
const i = this.toplevels.indexOf(win);
|
|
296
|
+
if (i >= 0) this.toplevels.splice(i, 1);
|
|
297
|
+
if (win.glctx?.chain?.gbm === this._currentSurface)
|
|
298
|
+
this._currentSurface = null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
windowById(id) {
|
|
302
|
+
for (const w of this.windows.values()) if (w.id === id) return w;
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The window a new window hangs off: the explicit parent the tree passed,
|
|
308
|
+
* else the focused toplevel, else the one under the pointer, else the most
|
|
309
|
+
* recent — a popup opens from an interaction, and one of those is where it
|
|
310
|
+
* happened.
|
|
311
|
+
*/
|
|
312
|
+
parentFor(attributes, { toplevelOnly = false } = {}) {
|
|
313
|
+
const explicit = attributes.parent;
|
|
314
|
+
if (explicit && (!toplevelOnly || !explicit.isPopup)) return explicit;
|
|
315
|
+
if (toplevelOnly) return null;
|
|
316
|
+
return (
|
|
317
|
+
this.input?.focusWindow ??
|
|
318
|
+
this.input?.pointerWindow ??
|
|
319
|
+
this.toplevels[this.toplevels.length - 1] ??
|
|
320
|
+
null
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Called by windows when an output scale changes, and by outputs.js. */
|
|
325
|
+
noteScale() {
|
|
326
|
+
// Until a window exists the outputs are the only word on the scale, and
|
|
327
|
+
// the first window is laid out at whatever this says — so their
|
|
328
|
+
// densest one seeds it. Once a window has heard its own, the windows'
|
|
329
|
+
// is the truth: fractional-scale says 1.5 where `wl_output.scale`
|
|
330
|
+
// rounds up to 2.
|
|
331
|
+
let scale = this.windows.size === 0 ? (this.outputs?.maxScale ?? 1) : 1;
|
|
332
|
+
for (const w of this.windows.values()) scale = Math.max(scale, w.wl.scale);
|
|
333
|
+
const moved = scale !== this.scale;
|
|
334
|
+
this.scale = scale;
|
|
335
|
+
if (this._publishScale() || moved) this.emit('scale', scale);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* What the renderer lays out with: device pixels per logical pixel, the
|
|
340
|
+
* number src/scale.js keeps for the tree. The compositor's scale, except
|
|
341
|
+
* where it is pinned — `REACT_X11_SCALE`, then `createRoot({ scale })`,
|
|
342
|
+
* absolute exactly as on X11 — or where the compositor says 1 over a
|
|
343
|
+
* retina-class grid it has no real millimetres for (`resolutionZoom`).
|
|
344
|
+
* A window's buffer is always in these pixels and its surface is them
|
|
345
|
+
* over the compositor's scale, so a zoom is a bigger window drawn at full
|
|
346
|
+
* resolution, never a smaller one stretched.
|
|
347
|
+
*/
|
|
348
|
+
get layoutScale() {
|
|
349
|
+
if (this._pinnedScale) return this._pinnedScale.scale;
|
|
350
|
+
return this.scale === 1 ? this._zoom : this.scale;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Hand `layoutScale` to src/scale.js; whether it moved. */
|
|
354
|
+
_publishScale() {
|
|
355
|
+
const layout = this.layoutScale;
|
|
356
|
+
const source =
|
|
357
|
+
this._pinnedScale?.source ??
|
|
358
|
+
(this.scale === 1 && this._zoom !== 1 ? 'resolution' : 'wayland');
|
|
359
|
+
setScaleForTests(this, layout, source);
|
|
360
|
+
const moved = layout !== this._publishedScale;
|
|
361
|
+
this._publishedScale = layout;
|
|
362
|
+
return moved;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The scale's pin, then the outputs — awaited, because the first window
|
|
367
|
+
* is sized against them — then what they say about the scale. The middle
|
|
368
|
+
* of `open()`, apart so a test runs exactly this against a mock.
|
|
369
|
+
*/
|
|
370
|
+
async _beginOutputs(conn, options = {}) {
|
|
371
|
+
this._pinnedScale = pinnedScale(options.scale);
|
|
372
|
+
this._publishScale();
|
|
373
|
+
this.outputs = new WaylandOutputs(conn, this);
|
|
374
|
+
await this.outputs.open();
|
|
375
|
+
this._zoomFromOutputs();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Once the outputs are known: the resolution class, unless pinned. */
|
|
379
|
+
_zoomFromOutputs() {
|
|
380
|
+
const pinned = this._pinnedScale;
|
|
381
|
+
if (pinned) {
|
|
382
|
+
traceScale(
|
|
383
|
+
`${pinned.scale}x from ` +
|
|
384
|
+
(pinned.source === 'option'
|
|
385
|
+
? 'createRoot({ scale })'
|
|
386
|
+
: pinned.source) +
|
|
387
|
+
`, over the compositor's ${this.scale}x`,
|
|
388
|
+
);
|
|
389
|
+
} else {
|
|
390
|
+
const v = this.outputs?.resolutionZoom?.() ?? {
|
|
391
|
+
zoom: 1,
|
|
392
|
+
name: null,
|
|
393
|
+
reason: 'no outputs',
|
|
394
|
+
};
|
|
395
|
+
this._zoom = v.zoom;
|
|
396
|
+
traceScale(` ${v.name ?? 'output'}: ${v.zoom}x over it — ${v.reason}`);
|
|
397
|
+
traceScale(
|
|
398
|
+
`${this.layoutScale}x from ` +
|
|
399
|
+
(this.scale === 1 && v.zoom !== 1
|
|
400
|
+
? 'the resolution class'
|
|
401
|
+
: `the compositor (${this.scale}x)`),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (this._publishScale()) this.emit('scale', this.scale);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The desktop's frame style or colours changed: every frame repaints,
|
|
408
|
+
* and resizes if its titlebar did. */
|
|
409
|
+
_frameStyleChanged() {
|
|
410
|
+
for (const w of this.windows.values()) w._frameStyleChanged?.();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Hook for the input router: a discrete event may want a paint now. */
|
|
414
|
+
afterInput() {}
|
|
415
|
+
|
|
416
|
+
// ---- app-level requests -------------------------------------------------
|
|
417
|
+
|
|
418
|
+
createSurface(options) {
|
|
419
|
+
return new WaylandSurface(this, options);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* What the machine can do, as a promise like the X11 and Cocoa answers
|
|
424
|
+
* (src/glbackend.js chains on it): here the answer was settled when the
|
|
425
|
+
* GPU context came up in `open()`.
|
|
426
|
+
*/
|
|
427
|
+
async glCapabilities() {
|
|
428
|
+
const caps = {
|
|
429
|
+
direct: true,
|
|
430
|
+
indirect: false,
|
|
431
|
+
backend: 'gles',
|
|
432
|
+
version: this.gpu?.glVersion?.string ?? null,
|
|
433
|
+
};
|
|
434
|
+
this._glCapsResolved = caps;
|
|
435
|
+
return caps;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
frameIntervalFor() {
|
|
439
|
+
// The compositor paces us through `wl_surface.frame`, and nothing in
|
|
440
|
+
// the shared renderer reads this (only the cocoa window does, to pace
|
|
441
|
+
// itself). The refresh rate is known — `win.output.refreshRate`, from
|
|
442
|
+
// the output's current mode — but an interval here would only invite a
|
|
443
|
+
// second clock beside the compositor's.
|
|
444
|
+
return 0;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
findArgbVisual() {
|
|
448
|
+
return { depth: 32 };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* What a `<glarea>` draws with: the window's own GLES context, into a rect
|
|
453
|
+
* of its backing target (glarea.js). A promise, as the X11 and Cocoa
|
|
454
|
+
* answers are — `GlAreaNode.realize` chains on it.
|
|
455
|
+
*
|
|
456
|
+
* `spec.api` is the `<glarea glx>` prop's rung: `'auto'` and `'gles'` are
|
|
457
|
+
* this; `'gl'` (desktop GL) and `'webgpu'` are named so the ladder is
|
|
458
|
+
* visible before it is built.
|
|
459
|
+
*/
|
|
460
|
+
async chooseGLConfig(spec) {
|
|
461
|
+
const mode = this.options.glPolicy?.mode ?? 'auto';
|
|
462
|
+
if (mode === 'off') {
|
|
463
|
+
const err = new Error(
|
|
464
|
+
"glPolicy is 'off' on this connection, so no GL context is created at all",
|
|
465
|
+
);
|
|
466
|
+
err.code = 'GL_DISABLED';
|
|
467
|
+
throw err;
|
|
468
|
+
}
|
|
469
|
+
if (mode === 'indirect') {
|
|
470
|
+
const err = new Error(
|
|
471
|
+
"glPolicy is 'indirect', which is GLX — there is no X server here to speak it to. Use 'auto' or 'direct'.",
|
|
472
|
+
);
|
|
473
|
+
err.code = 'GL_POLICY_INDIRECT';
|
|
474
|
+
throw err;
|
|
475
|
+
}
|
|
476
|
+
const api = spec?.api ?? 'auto';
|
|
477
|
+
if (api !== 'auto' && api !== 'gles') {
|
|
478
|
+
const err = new Error(
|
|
479
|
+
api === 'gl' || api === 'webgpu'
|
|
480
|
+
? `<glarea> api '${api}' is a named rung this backend has not built yet — today's rungs are auto, gles (GLES 3 on the window's own context)`
|
|
481
|
+
: `<glarea> api '${api}' is not a rung — expected one of auto, gles, gl, webgpu.`,
|
|
482
|
+
);
|
|
483
|
+
err.code = 'GL_API_UNAVAILABLE';
|
|
484
|
+
throw err;
|
|
485
|
+
}
|
|
486
|
+
return { backend: 'direct', api: 'gles', visual: GLAREA_VISUAL, depth: 32 };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The pane a `<glarea>`'s children are drawn on (src/gloverlay.js): an
|
|
491
|
+
* offscreen target the window blends over the surface, so the overlay is
|
|
492
|
+
* translucent here as on the Cocoa backend. Having this at all is how the
|
|
493
|
+
* overlay knows the backend composites.
|
|
494
|
+
*/
|
|
495
|
+
createOverlayPane(attributes) {
|
|
496
|
+
return new WaylandOverlayPane(this, attributes);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Ask the compositor to activate a window. Stacking is the compositor's;
|
|
501
|
+
* xdg-activation lets a client ask with a token from a recent interaction,
|
|
502
|
+
* and the compositor decides whether the interaction was recent enough.
|
|
503
|
+
*/
|
|
504
|
+
activate(win) {
|
|
505
|
+
if (!this.activation || !win?.wl?.surface) return;
|
|
506
|
+
const token = this.activation.$.get_activation_token();
|
|
507
|
+
token.on('done', (value) => {
|
|
508
|
+
this.activation.$.activate(value, win.wl.surface.id);
|
|
509
|
+
token.$.destroy();
|
|
510
|
+
});
|
|
511
|
+
token.$.set_surface(win.wl.surface.id);
|
|
512
|
+
if (this.seat.lastSerial)
|
|
513
|
+
token.$.set_serial(this.seat.lastSerial, this.seat.seat.id);
|
|
514
|
+
token.$.commit();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
raiseWindow(win) {
|
|
518
|
+
this.activate(win);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
requestAttention() {
|
|
522
|
+
// xdg-activation with a stale serial is exactly "attention, please":
|
|
523
|
+
// compositors that support it mark the window rather than focusing it.
|
|
524
|
+
const w = this.toplevels[0];
|
|
525
|
+
if (w) this.activate(w);
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
cancelAttention() {}
|
|
530
|
+
|
|
531
|
+
async close() {
|
|
532
|
+
for (const w of [...this.windows.values()]) w.destroy();
|
|
533
|
+
this.windows.clear();
|
|
534
|
+
this.toplevels.length = 0;
|
|
535
|
+
this.textInput?.destroy();
|
|
536
|
+
this.frameStyle?.stop();
|
|
537
|
+
this._unwatchAppearance?.();
|
|
538
|
+
this.input?.destroy();
|
|
539
|
+
this.tablet?.destroy();
|
|
540
|
+
this.touch?.destroy();
|
|
541
|
+
this.screenCapture?.destroy();
|
|
542
|
+
this.seat?.destroy();
|
|
543
|
+
this.outputs?.destroy();
|
|
544
|
+
if (this._holder) {
|
|
545
|
+
try {
|
|
546
|
+
this._holder.destroy();
|
|
547
|
+
} catch {
|
|
548
|
+
/* gone */
|
|
549
|
+
}
|
|
550
|
+
this._holder = null;
|
|
551
|
+
}
|
|
552
|
+
this.X._closing = true;
|
|
553
|
+
this.conn.destroy();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** What `Reconciler.js` imports. */
|
|
558
|
+
export async function createWaylandApp(options = {}) {
|
|
559
|
+
return await WaylandApp.open(options);
|
|
560
|
+
}
|