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,601 @@
|
|
|
1
|
+
// The monitors: every `wl_output` the compositor advertises, published into
|
|
2
|
+
// `src/screens.js` so that `useScreens()`, `availableArea()` and everything
|
|
3
|
+
// that sizes a window against a monitor works with no X server in the path
|
|
4
|
+
// — the way `src/cocoa/app.js`'s `screenLayout()` publishes `NSScreen`.
|
|
5
|
+
//
|
|
6
|
+
// What a Wayland client can and cannot know about the screen shapes all of
|
|
7
|
+
// it:
|
|
8
|
+
//
|
|
9
|
+
// - **Outputs are plural globals.** Each monitor is its own `wl_output` in
|
|
10
|
+
// the registry, so the layout is a fold over every one of them rather
|
|
11
|
+
// than one query, and hot-plug is a `global`/`global_remove` on the
|
|
12
|
+
// registry — which is what connection.js's `Registry` view exists for.
|
|
13
|
+
// - **Position and size are logical.** `wl_output.geometry` places the
|
|
14
|
+
// output in the compositor's global space and `mode` gives the panel's
|
|
15
|
+
// pixels; the logical size is the mode over the scale, turned by the
|
|
16
|
+
// transform — unless `zxdg_output_manager_v1` is there, whose
|
|
17
|
+
// `logical_size` is exact under fractional scaling where that arithmetic
|
|
18
|
+
// is not. The rects are published in the renderer's device pixels
|
|
19
|
+
// (logical × `app.scale`), as cocoa converts points, so `monitorAt()` and
|
|
20
|
+
// the window rects agree; `useScreens()` divides them back.
|
|
21
|
+
// - **There is no primary.** The protocol has no notion of one. The output
|
|
22
|
+
// at the origin is the desktop's anchor on every compositor that lays
|
|
23
|
+
// monitors out from (0,0), and the leftmost stands in otherwise.
|
|
24
|
+
// - **There is no work area.** No `_NET_WORKAREA`, no per-output strut.
|
|
25
|
+
// What exists is `xdg_toplevel.configure_bounds`, sent to a window ahead
|
|
26
|
+
// of its configure: the size it is recommended to fit in — its monitor
|
|
27
|
+
// less the panels, on GNOME. So a window's bounds become the usable rect
|
|
28
|
+
// (`visible`, which `usable()` prefers) of the output the window is on,
|
|
29
|
+
// positioned at the monitor's own origin because the offset is
|
|
30
|
+
// unknowable and, on a backend where a client cannot place a window,
|
|
31
|
+
// irrelevant. Every monitor carries a `visible` — its whole rect until a
|
|
32
|
+
// window on it says otherwise — so that no head is ever clamped per axis
|
|
33
|
+
// by another's work area (the `_NET_WORKAREA` compromise, issue #453). A
|
|
34
|
+
// window that has bounds but no output yet — the bounds arrive before the
|
|
35
|
+
// first buffer, the output after — is put on the smallest monitor its
|
|
36
|
+
// bounds fit, which is the monitor they describe on any desk whose
|
|
37
|
+
// monitors differ. The primary's usable rect is the desktop's `workArea`.
|
|
38
|
+
// - **A surface learns its output by being told.** `wl_surface.enter` and
|
|
39
|
+
// `leave` name the outputs a surface overlaps. That is how `win.output`
|
|
40
|
+
// knows, and where a compositor with neither fractional-scale-v1 nor
|
|
41
|
+
// `wl_surface.preferred_buffer_scale` gets a window's buffer scale from.
|
|
42
|
+
//
|
|
43
|
+
// Published through `setScreensForTests`, as the cocoa backend does: it is
|
|
44
|
+
// the one seam `screens.js` has for a layout that did not come from X, and
|
|
45
|
+
// using it leaves the X11 path untouched.
|
|
46
|
+
|
|
47
|
+
import { EventEmitter } from 'node:events';
|
|
48
|
+
import { setScreensForTests } from '../screens.js';
|
|
49
|
+
import {
|
|
50
|
+
isVirtualDisplay,
|
|
51
|
+
monitorScaleFromMetadata,
|
|
52
|
+
monitorScalesOf,
|
|
53
|
+
} from '../scale.js';
|
|
54
|
+
|
|
55
|
+
/** `wl_output.mode` flags. */
|
|
56
|
+
const MODE_CURRENT = 0x1;
|
|
57
|
+
|
|
58
|
+
/** fractional-scale-v1's granularity; a mode-over-logical ratio snaps to it. */
|
|
59
|
+
const SCALE_DENOM = 120;
|
|
60
|
+
|
|
61
|
+
/** `wl_output.transform` → degrees. The flipped four are ignored as
|
|
62
|
+
* reflections, the way `screens.js` ignores RandR's reflection bits. */
|
|
63
|
+
export function degreesOf(transform) {
|
|
64
|
+
return (transform & 3) * 90;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Hz from `wl_output.mode`'s millihertz, to two decimals, or null where
|
|
69
|
+
* the compositor did not say: a virtual output reports 0, and anything
|
|
70
|
+
* outside the range a panel can refresh at is the same "unknown" in other
|
|
71
|
+
* clothes (`screens.js`'s `refreshRateOf` draws the line in the same place).
|
|
72
|
+
*/
|
|
73
|
+
export function refreshHz(millihertz) {
|
|
74
|
+
if (!(millihertz > 0)) return null;
|
|
75
|
+
const hz = millihertz / 1000;
|
|
76
|
+
if (hz < 20 || hz > 1000) return null;
|
|
77
|
+
return Math.round(hz * 100) / 100;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The current mode's pixels with a quarter turn applied. */
|
|
81
|
+
function turnedMode(rec) {
|
|
82
|
+
const mode = rec.mode;
|
|
83
|
+
if (!mode) return null;
|
|
84
|
+
return rec.transform & 1
|
|
85
|
+
? { width: mode.height, height: mode.width }
|
|
86
|
+
: { width: mode.width, height: mode.height };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The output's rect in logical pixels: xdg_output's where there is one,
|
|
91
|
+
* else geometry's position and the mode over the integer scale.
|
|
92
|
+
*/
|
|
93
|
+
export function logicalRectOf(rec) {
|
|
94
|
+
const at = rec.logicalPosition ?? { x: rec.x, y: rec.y };
|
|
95
|
+
if (rec.logicalSize?.width > 0 && rec.logicalSize?.height > 0)
|
|
96
|
+
return { ...at, ...rec.logicalSize };
|
|
97
|
+
const mode = turnedMode(rec);
|
|
98
|
+
if (!mode) return null;
|
|
99
|
+
const s = rec.scale > 0 ? rec.scale : 1;
|
|
100
|
+
return {
|
|
101
|
+
...at,
|
|
102
|
+
width: Math.max(1, Math.round(mode.width / s)),
|
|
103
|
+
height: Math.max(1, Math.round(mode.height / s)),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Device pixels per logical pixel on this output. `wl_output.scale` is an
|
|
109
|
+
* integer and rounds a fractional desktop up (150% reports 2); the mode
|
|
110
|
+
* against xdg_output's logical size is the fraction itself, snapped to the
|
|
111
|
+
* 1/120 fractional-scale-v1 speaks in so it compares equal to what a
|
|
112
|
+
* surface is told.
|
|
113
|
+
*/
|
|
114
|
+
export function effectiveScaleOf(rec) {
|
|
115
|
+
const mode = turnedMode(rec);
|
|
116
|
+
const logical = rec.logicalSize;
|
|
117
|
+
if (mode?.width > 0 && logical?.width > 0) {
|
|
118
|
+
const ratio = mode.width / logical.width;
|
|
119
|
+
if (ratio > 0) return Math.round(ratio * SCALE_DENOM) / SCALE_DENOM;
|
|
120
|
+
}
|
|
121
|
+
return rec.scale > 0 ? rec.scale : 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The scale ladder's resolution class (src/scale.js, rung 5) for a
|
|
126
|
+
* compositor that scales nothing: `{ zoom: 2 }` where the largest output is
|
|
127
|
+
* a retina-class grid with no millimetres worth believing, `{ zoom: 1 }`
|
|
128
|
+
* everywhere else, with the verdict's reason for `REACT_X11_DEBUG_SCALE`.
|
|
129
|
+
*
|
|
130
|
+
* The case is a VM window over a retina panel. QEMU describes the guest's
|
|
131
|
+
* screen as a ~100dpi monitor, mutter believes it and settles on 100%, and
|
|
132
|
+
* every app laid out at the compositor's word comes up half the size it is
|
|
133
|
+
* on the host. The X11 ladder answers that screen 2x through Xwayland; this
|
|
134
|
+
* is the same rung giving the same answer here. A compositor that scales
|
|
135
|
+
* any output has decided for itself — 133% on that same VM is someone's
|
|
136
|
+
* choice — and credible millimetres were the compositor's own evidence, so
|
|
137
|
+
* both keep their word.
|
|
138
|
+
*/
|
|
139
|
+
export function resolutionZoom(records) {
|
|
140
|
+
let pick = null;
|
|
141
|
+
for (const rec of records) {
|
|
142
|
+
const mode = turnedMode(rec);
|
|
143
|
+
if (!(mode?.width > 0 && mode?.height > 0)) continue;
|
|
144
|
+
const s = effectiveScaleOf(rec);
|
|
145
|
+
if (s !== 1) {
|
|
146
|
+
return {
|
|
147
|
+
zoom: 1,
|
|
148
|
+
name: rec.name,
|
|
149
|
+
reason: `the compositor scales it ${s}x`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (!pick || mode.width * mode.height > pick.w * pick.h)
|
|
153
|
+
pick = { rec, w: mode.width, h: mode.height };
|
|
154
|
+
}
|
|
155
|
+
if (!pick) return { zoom: 1, name: null, reason: 'no outputs yet' };
|
|
156
|
+
const { rec, w, h } = pick;
|
|
157
|
+
const verdict = monitorScaleFromMetadata({
|
|
158
|
+
name: rec.name ?? rec.xdgName ?? '',
|
|
159
|
+
width: w,
|
|
160
|
+
height: h,
|
|
161
|
+
widthMM: rec.physicalWidth,
|
|
162
|
+
heightMM: rec.physicalHeight,
|
|
163
|
+
edid: { virtual: isVirtualDisplay(rec.make || null, rec.model || null) },
|
|
164
|
+
});
|
|
165
|
+
return {
|
|
166
|
+
zoom: verdict.source === 'resolution' ? verdict.scale : 1,
|
|
167
|
+
name: rec.name ?? rec.xdgName ?? null,
|
|
168
|
+
reason: `${verdict.reason}; the compositor says 1x`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One output as a `screens.js` monitor record, at the app's scale. Pure,
|
|
174
|
+
* and exported for that reason: the layout arithmetic is the part worth
|
|
175
|
+
* pinning without a compositor.
|
|
176
|
+
*
|
|
177
|
+
* `rec` is what the tracker keeps per output: `x`, `y`, `physicalWidth`,
|
|
178
|
+
* `physicalHeight`, `make`, `model`, `transform` from `geometry`; `mode`
|
|
179
|
+
* `{ width, height, refresh }`; `scale`; `name`/`description` (wl_output
|
|
180
|
+
* v4, or xdg_output's `xdgName`/`xdgDescription`); `logicalPosition` and
|
|
181
|
+
* `logicalSize` from xdg_output.
|
|
182
|
+
*/
|
|
183
|
+
export function monitorOf(rec, scale = 1) {
|
|
184
|
+
const logical = logicalRectOf(rec);
|
|
185
|
+
if (!logical) return null;
|
|
186
|
+
const px = (v) => Math.round(v * scale);
|
|
187
|
+
const name = rec.name ?? rec.xdgName ?? null;
|
|
188
|
+
return {
|
|
189
|
+
name,
|
|
190
|
+
outputs: name ? [name] : [],
|
|
191
|
+
description: rec.description ?? rec.xdgDescription ?? null,
|
|
192
|
+
x: px(logical.x),
|
|
193
|
+
y: px(logical.y),
|
|
194
|
+
width: Math.max(1, px(logical.width)),
|
|
195
|
+
height: Math.max(1, px(logical.height)),
|
|
196
|
+
primary: false,
|
|
197
|
+
widthMM: rec.physicalWidth > 0 ? rec.physicalWidth : null,
|
|
198
|
+
heightMM: rec.physicalHeight > 0 ? rec.physicalHeight : null,
|
|
199
|
+
refreshRate: refreshHz(rec.mode?.refresh),
|
|
200
|
+
rotation: degreesOf(rec.transform),
|
|
201
|
+
scale: effectiveScaleOf(rec),
|
|
202
|
+
make: rec.make || null,
|
|
203
|
+
model: rec.model || null,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** `bounds` (logical) as a usable rect on `m` (device pixels), at its origin. */
|
|
208
|
+
function usableOf(m, bounds, scale) {
|
|
209
|
+
return {
|
|
210
|
+
x: m.x,
|
|
211
|
+
y: m.y,
|
|
212
|
+
width: Math.min(m.width, Math.max(1, Math.round(bounds.width * scale))),
|
|
213
|
+
height: Math.min(m.height, Math.max(1, Math.round(bounds.height * scale))),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The monitor a window with these bounds and no output yet is on: the
|
|
219
|
+
* smallest that fits them — `configure_bounds` is a monitor less its
|
|
220
|
+
* panels, so it fits its own and any larger one — the primary on a tie.
|
|
221
|
+
*/
|
|
222
|
+
function bestFit(list, bounds, scale) {
|
|
223
|
+
const w = Math.round(bounds.width * scale);
|
|
224
|
+
const h = Math.round(bounds.height * scale);
|
|
225
|
+
let best = null;
|
|
226
|
+
for (const m of list) {
|
|
227
|
+
if (m.width < w || m.height < h) continue;
|
|
228
|
+
const area = m.width * m.height;
|
|
229
|
+
const bestArea = best ? best.width * best.height : Infinity;
|
|
230
|
+
if (area < bestArea || (area === bestArea && m.primary)) best = m;
|
|
231
|
+
}
|
|
232
|
+
return best;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Sort, pick a primary, and fold the windows' bounds in — everything the
|
|
237
|
+
* layout is beyond one monitor at a time. `bounds` is a list of
|
|
238
|
+
* `{ outputs: Set<id>, bounds: { width, height } }`, most recent last.
|
|
239
|
+
*/
|
|
240
|
+
export function layoutOf(monitors, { scale = 1, bounds = [] } = {}) {
|
|
241
|
+
// Left to right, then top to bottom — the order the desktop is laid out
|
|
242
|
+
// in, not the order the registry announced them, which is neither.
|
|
243
|
+
const list = [...monitors].sort((a, b) => a.x - b.x || a.y - b.y);
|
|
244
|
+
const primary = list.find((m) => m.x === 0 && m.y === 0) ?? list[0] ?? null;
|
|
245
|
+
for (const m of list) {
|
|
246
|
+
m.primary = m === primary;
|
|
247
|
+
// Its own rect until a window on it says otherwise. Set on every
|
|
248
|
+
// monitor, so `usable()` takes each as the exact rect it is and never
|
|
249
|
+
// falls back to clamping a wider second head by the primary's.
|
|
250
|
+
m.visible = { x: m.x, y: m.y, width: m.width, height: m.height };
|
|
251
|
+
}
|
|
252
|
+
for (const b of bounds) {
|
|
253
|
+
if (!b.bounds) continue;
|
|
254
|
+
let targets = list.filter((m) => b.outputs.has(m.id));
|
|
255
|
+
if (!targets.length) {
|
|
256
|
+
const fit = bestFit(list, b.bounds, scale);
|
|
257
|
+
targets = fit ? [fit] : [];
|
|
258
|
+
}
|
|
259
|
+
for (const m of targets) m.visible = usableOf(m, b.bounds, scale);
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
monitors: list,
|
|
263
|
+
workArea: primary ? { ...primary.visible } : null,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** What `app.outputs` is. */
|
|
268
|
+
export class WaylandOutputs extends EventEmitter {
|
|
269
|
+
constructor(conn, app) {
|
|
270
|
+
super();
|
|
271
|
+
this.setMaxListeners(0);
|
|
272
|
+
this.conn = conn;
|
|
273
|
+
this.app = app;
|
|
274
|
+
/** registry name -> record */
|
|
275
|
+
this.byName = new Map();
|
|
276
|
+
/** wl_output proxy id -> record */
|
|
277
|
+
this.byId = new Map();
|
|
278
|
+
this.registry = null;
|
|
279
|
+
/** `zxdg_output_manager_v1`, or null on a compositor without it */
|
|
280
|
+
this.manager = null;
|
|
281
|
+
/** the shell windows (WaylandWindow) whose outputs and bounds count */
|
|
282
|
+
this._watched = new Map();
|
|
283
|
+
this._seq = 0;
|
|
284
|
+
this._monitors = [];
|
|
285
|
+
this._scheduled = false;
|
|
286
|
+
this._publishedKey = null;
|
|
287
|
+
this._lastScale = null;
|
|
288
|
+
this.destroyed = false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Bind every output and publish the first layout. Two round trips — one
|
|
293
|
+
* for the registry's list, one for the outputs' state — which is the
|
|
294
|
+
* X11 backend's Xinerama tier in cost, and like it awaited by the app.
|
|
295
|
+
*
|
|
296
|
+
* The app assigns `app.outputs` *before* calling this: the first publish
|
|
297
|
+
* asks `app.noteScale()` to seed the scale from the outputs, and it reads
|
|
298
|
+
* them through that property.
|
|
299
|
+
*/
|
|
300
|
+
async open() {
|
|
301
|
+
this.manager = await this.conn.bind('zxdg_output_manager_v1');
|
|
302
|
+
this.registry = await this.conn.registry();
|
|
303
|
+
for (const g of this.registry.of('wl_output')) this._add(g.name);
|
|
304
|
+
this._onGlobal = (name, iface) => {
|
|
305
|
+
if (iface === 'wl_output') this._add(name);
|
|
306
|
+
};
|
|
307
|
+
this._onGlobalRemove = (name) => this._remove(name);
|
|
308
|
+
this.registry.on('global', this._onGlobal);
|
|
309
|
+
this.registry.on('global_remove', this._onGlobalRemove);
|
|
310
|
+
// The app's factor multiplies every published rect, so it moving —
|
|
311
|
+
// the first window hearing a fractional scale — is a relayout.
|
|
312
|
+
this._onScale = () => {
|
|
313
|
+
if (this.app.scale !== this._lastScale) this._schedule();
|
|
314
|
+
};
|
|
315
|
+
this.app.on?.('scale', this._onScale);
|
|
316
|
+
// Every output's `done` has scheduled a publish by the time this is
|
|
317
|
+
// back; the explicit one is for a v1 output, which has no `done`.
|
|
318
|
+
await this.conn.roundtrip();
|
|
319
|
+
this.publish();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_add(name) {
|
|
323
|
+
if (this.byName.has(name)) return;
|
|
324
|
+
const proxy = this.registry.bind(name, 'wl_output');
|
|
325
|
+
if (!proxy) return;
|
|
326
|
+
const rec = {
|
|
327
|
+
global: name,
|
|
328
|
+
id: proxy.id,
|
|
329
|
+
proxy,
|
|
330
|
+
version: proxy.version,
|
|
331
|
+
xdg: null,
|
|
332
|
+
x: 0,
|
|
333
|
+
y: 0,
|
|
334
|
+
physicalWidth: 0,
|
|
335
|
+
physicalHeight: 0,
|
|
336
|
+
make: '',
|
|
337
|
+
model: '',
|
|
338
|
+
transform: 0,
|
|
339
|
+
mode: null,
|
|
340
|
+
scale: 1,
|
|
341
|
+
name: null,
|
|
342
|
+
description: null,
|
|
343
|
+
xdgName: null,
|
|
344
|
+
xdgDescription: null,
|
|
345
|
+
logicalPosition: null,
|
|
346
|
+
logicalSize: null,
|
|
347
|
+
};
|
|
348
|
+
this.byName.set(name, rec);
|
|
349
|
+
this.byId.set(proxy.id, rec);
|
|
350
|
+
|
|
351
|
+
const touched = () => this._touched(rec);
|
|
352
|
+
proxy.on('geometry', (x, y, pw, ph, _subpixel, make, model, transform) => {
|
|
353
|
+
Object.assign(rec, {
|
|
354
|
+
x,
|
|
355
|
+
y,
|
|
356
|
+
physicalWidth: pw,
|
|
357
|
+
physicalHeight: ph,
|
|
358
|
+
make,
|
|
359
|
+
model,
|
|
360
|
+
transform,
|
|
361
|
+
});
|
|
362
|
+
touched();
|
|
363
|
+
});
|
|
364
|
+
proxy.on('mode', (flags, width, height, refresh) => {
|
|
365
|
+
// A compositor may list every mode the panel has; the current one is
|
|
366
|
+
// the monitor, and failing a flag the last one listed.
|
|
367
|
+
const current = (flags & MODE_CURRENT) !== 0;
|
|
368
|
+
if (current || !rec.mode?.current)
|
|
369
|
+
rec.mode = { width, height, refresh, current };
|
|
370
|
+
touched();
|
|
371
|
+
});
|
|
372
|
+
proxy.on('scale', (factor) => {
|
|
373
|
+
rec.scale = factor > 0 ? factor : 1;
|
|
374
|
+
touched();
|
|
375
|
+
});
|
|
376
|
+
proxy.on('name', (n) => {
|
|
377
|
+
rec.name = n || null;
|
|
378
|
+
touched();
|
|
379
|
+
});
|
|
380
|
+
proxy.on('description', (d) => {
|
|
381
|
+
rec.description = d || null;
|
|
382
|
+
touched();
|
|
383
|
+
});
|
|
384
|
+
proxy.on('done', () => this._schedule());
|
|
385
|
+
|
|
386
|
+
if (this.manager) {
|
|
387
|
+
const xdg = this.manager.$.get_xdg_output(proxy.id);
|
|
388
|
+
xdg.setMaxListeners?.(0);
|
|
389
|
+
rec.xdg = xdg;
|
|
390
|
+
xdg.on('logical_position', (x, y) => {
|
|
391
|
+
rec.logicalPosition = { x, y };
|
|
392
|
+
touched();
|
|
393
|
+
});
|
|
394
|
+
xdg.on('logical_size', (width, height) => {
|
|
395
|
+
rec.logicalSize = { width, height };
|
|
396
|
+
touched();
|
|
397
|
+
});
|
|
398
|
+
xdg.on('name', (n) => {
|
|
399
|
+
rec.xdgName = n || null;
|
|
400
|
+
touched();
|
|
401
|
+
});
|
|
402
|
+
xdg.on('description', (d) => {
|
|
403
|
+
rec.xdgDescription = d || null;
|
|
404
|
+
touched();
|
|
405
|
+
});
|
|
406
|
+
// The atom below manager v3; from v3 on it is wl_output.done above.
|
|
407
|
+
xdg.on('done', () => this._schedule());
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** State moved; `done` is the atom, except on a v1 output that has none. */
|
|
412
|
+
_touched(rec) {
|
|
413
|
+
if (rec.version < 2) this._schedule();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
_remove(name) {
|
|
417
|
+
const rec = this.byName.get(name);
|
|
418
|
+
if (!rec) return;
|
|
419
|
+
this.byName.delete(name);
|
|
420
|
+
this.byId.delete(rec.id);
|
|
421
|
+
this._release(rec);
|
|
422
|
+
this._schedule();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
_release(rec) {
|
|
426
|
+
try {
|
|
427
|
+
rec.xdg?.$.destroy();
|
|
428
|
+
if (rec.version >= 3) rec.proxy.$.release();
|
|
429
|
+
else this.conn.display.deleteId(rec.id);
|
|
430
|
+
} catch {
|
|
431
|
+
/* the connection may already be gone */
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** One publish per turn, however many events a change arrived as. */
|
|
436
|
+
_schedule() {
|
|
437
|
+
if (this._scheduled || this.destroyed) return;
|
|
438
|
+
this._scheduled = true;
|
|
439
|
+
queueMicrotask(() => {
|
|
440
|
+
this._scheduled = false;
|
|
441
|
+
this.publish();
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---- windows ------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Follow a shell window: which outputs it enters (its monitor, and the
|
|
449
|
+
* buffer-scale fallback) and the bounds it is configured with (the work
|
|
450
|
+
* area). Called by the backend window at creation; forgets the window
|
|
451
|
+
* when it is destroyed.
|
|
452
|
+
*/
|
|
453
|
+
watchWindow(wl) {
|
|
454
|
+
if (this._watched.has(wl)) return;
|
|
455
|
+
const entry = { seq: 0 };
|
|
456
|
+
this._watched.set(wl, entry);
|
|
457
|
+
const onOutputs = () => {
|
|
458
|
+
this._applyScale(wl);
|
|
459
|
+
this._schedule();
|
|
460
|
+
};
|
|
461
|
+
const onBounds = () => {
|
|
462
|
+
entry.seq = ++this._seq;
|
|
463
|
+
this._schedule();
|
|
464
|
+
};
|
|
465
|
+
wl.on('outputs', onOutputs);
|
|
466
|
+
wl.on('bounds', onBounds);
|
|
467
|
+
wl.once('destroyed', () => {
|
|
468
|
+
wl.off('outputs', onOutputs);
|
|
469
|
+
wl.off('bounds', onBounds);
|
|
470
|
+
this._watched.delete(wl);
|
|
471
|
+
this._schedule();
|
|
472
|
+
});
|
|
473
|
+
if (wl.bounds) entry.seq = ++this._seq;
|
|
474
|
+
if (wl.outputs.size) this._applyScale(wl);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
_applyScale(wl) {
|
|
478
|
+
const s = this.scaleFor(wl.outputs);
|
|
479
|
+
if (s) wl.noteOutputScale(s);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** The integer scale for a surface over these outputs: the densest, as
|
|
483
|
+
* every toolkit picks it, so nothing is upsampled. 0 for none known. */
|
|
484
|
+
scaleFor(ids) {
|
|
485
|
+
let s = 0;
|
|
486
|
+
for (const id of ids ?? []) {
|
|
487
|
+
const rec = this.byId.get(id);
|
|
488
|
+
if (rec) s = Math.max(s, rec.scale);
|
|
489
|
+
}
|
|
490
|
+
return s;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** The monitor for a surface over these outputs (the densest), or null. */
|
|
494
|
+
monitorFor(ids) {
|
|
495
|
+
let best = null;
|
|
496
|
+
for (const id of ids ?? []) {
|
|
497
|
+
const m = this._monitors.find((mon) => mon.id === id);
|
|
498
|
+
if (m && (!best || m.scale > best.scale)) best = m;
|
|
499
|
+
}
|
|
500
|
+
return best ? publicMonitor(best) : null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ---- the layout ---------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The densest output's scale: what the app lays out at until a window
|
|
507
|
+
* has heard its own. The fraction where fractional-scale-v1 will hand
|
|
508
|
+
* the windows one, the integer where only `wl_output.scale` exists —
|
|
509
|
+
* seeding 1.5 on a compositor that can only answer 2 would be a layout
|
|
510
|
+
* corrected on the first frame.
|
|
511
|
+
*/
|
|
512
|
+
get maxScale() {
|
|
513
|
+
const fractional = Boolean(this.app.fractionalScale);
|
|
514
|
+
let s = 1;
|
|
515
|
+
for (const rec of this.byName.values())
|
|
516
|
+
s = Math.max(s, fractional ? effectiveScaleOf(rec) : rec.scale || 1);
|
|
517
|
+
return s;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** `resolutionZoom` over the outputs as they stand. */
|
|
521
|
+
resolutionZoom() {
|
|
522
|
+
return resolutionZoom([...this.byName.values()]);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** The layout in `screens.js`'s shape, at the app's current scale. */
|
|
526
|
+
layout() {
|
|
527
|
+
const scale = this.app.scale > 0 ? this.app.scale : 1;
|
|
528
|
+
const monitors = [];
|
|
529
|
+
for (const rec of this.byName.values()) {
|
|
530
|
+
const m = monitorOf(rec, scale);
|
|
531
|
+
if (m) monitors.push({ id: rec.id, ...m });
|
|
532
|
+
}
|
|
533
|
+
const bounds = [...this._watched]
|
|
534
|
+
.filter(([wl]) => wl.bounds)
|
|
535
|
+
.sort((a, b) => a[1].seq - b[1].seq)
|
|
536
|
+
.map(([wl]) => ({ outputs: wl.outputs, bounds: wl.bounds }));
|
|
537
|
+
return layoutOf(monitors, { scale, bounds });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Every monitor, left to right, as `screens.js` sees it plus
|
|
541
|
+
* `description`, `scale`, `make` and `model`. */
|
|
542
|
+
list() {
|
|
543
|
+
return this._monitors.map(publicMonitor);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Publish what is known — to `screens.js`, to the per-monitor scale map
|
|
548
|
+
* `useScreens()` joins on, and to anyone listening for `change`. Skipped
|
|
549
|
+
* when nothing moved: a `configure_bounds` repeating the same numbers on
|
|
550
|
+
* every window must not re-render every `useScreens()` subscriber.
|
|
551
|
+
*/
|
|
552
|
+
publish() {
|
|
553
|
+
if (this.destroyed) return;
|
|
554
|
+
// A denser output may have arrived before any window: the app's factor
|
|
555
|
+
// follows, and the rects below are computed at the new one.
|
|
556
|
+
this.app.noteScale?.();
|
|
557
|
+
this._lastScale = this.app.scale;
|
|
558
|
+
const { monitors, workArea } = this.layout();
|
|
559
|
+
this._monitors = monitors;
|
|
560
|
+
const published = monitors.map(publicMonitor);
|
|
561
|
+
const key = JSON.stringify({ published, workArea });
|
|
562
|
+
if (key === this._publishedKey) return;
|
|
563
|
+
this._publishedKey = key;
|
|
564
|
+
|
|
565
|
+
const scales = monitorScalesOf(this.app);
|
|
566
|
+
scales.clear();
|
|
567
|
+
for (const m of published) {
|
|
568
|
+
if (!m.name) continue;
|
|
569
|
+
scales.set(m.name, {
|
|
570
|
+
scale: m.scale,
|
|
571
|
+
source: 'wl_output',
|
|
572
|
+
primary: m.primary,
|
|
573
|
+
x: m.x,
|
|
574
|
+
y: m.y,
|
|
575
|
+
width: m.width,
|
|
576
|
+
height: m.height,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
setScreensForTests(this.app, { monitors: published, workArea });
|
|
580
|
+
this.emit('change', published);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
destroy() {
|
|
584
|
+
if (this.destroyed) return;
|
|
585
|
+
this.destroyed = true;
|
|
586
|
+
this.registry?.off('global', this._onGlobal);
|
|
587
|
+
this.registry?.off('global_remove', this._onGlobalRemove);
|
|
588
|
+
this.app.off?.('scale', this._onScale);
|
|
589
|
+
for (const rec of this.byName.values()) this._release(rec);
|
|
590
|
+
this.byName.clear();
|
|
591
|
+
this.byId.clear();
|
|
592
|
+
this._watched.clear();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** A monitor record without the proxy id the tracker keys on. */
|
|
597
|
+
function publicMonitor(m) {
|
|
598
|
+
// eslint-disable-next-line no-unused-vars
|
|
599
|
+
const { id, ...rest } = m;
|
|
600
|
+
return rest;
|
|
601
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"name":"wp_cursor_shape_manager_v1","version":2,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"get_pointer","args":[{"name":"cursor_shape_device","type":"new_id","interface":"wp_cursor_shape_device_v1"},{"name":"pointer","type":"object","interface":"wl_pointer"}]},{"name":"get_tablet_tool_v2","args":[{"name":"cursor_shape_device","type":"new_id","interface":"wp_cursor_shape_device_v1"},{"name":"tablet_tool","type":"object","interface":"zwp_tablet_tool_v2"}]}],"events":[],"enums":{}},{"name":"wp_cursor_shape_device_v1","version":2,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"set_shape","args":[{"name":"serial","type":"uint"},{"name":"shape","type":"uint","enum":"shape"}]}],"events":[],"enums":{"shape":{"entries":[{"name":"default","value":1},{"name":"context_menu","value":2},{"name":"help","value":3},{"name":"pointer","value":4},{"name":"progress","value":5},{"name":"wait","value":6},{"name":"cell","value":7},{"name":"crosshair","value":8},{"name":"text","value":9},{"name":"vertical_text","value":10},{"name":"alias","value":11},{"name":"copy","value":12},{"name":"move","value":13},{"name":"no_drop","value":14},{"name":"not_allowed","value":15},{"name":"grab","value":16},{"name":"grabbing","value":17},{"name":"e_resize","value":18},{"name":"n_resize","value":19},{"name":"ne_resize","value":20},{"name":"nw_resize","value":21},{"name":"s_resize","value":22},{"name":"se_resize","value":23},{"name":"sw_resize","value":24},{"name":"w_resize","value":25},{"name":"ew_resize","value":26},{"name":"ns_resize","value":27},{"name":"nesw_resize","value":28},{"name":"nwse_resize","value":29},{"name":"col_resize","value":30},{"name":"row_resize","value":31},{"name":"all_scroll","value":32},{"name":"zoom_in","value":33},{"name":"zoom_out","value":34},{"name":"dnd_ask","value":35},{"name":"all_resize","value":36}]},"error":{"entries":[{"name":"invalid_shape","value":1}]}}}]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"name":"ext_idle_notifier_v1","version":2,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"get_idle_notification","args":[{"name":"id","type":"new_id","interface":"ext_idle_notification_v1"},{"name":"timeout","type":"uint"},{"name":"seat","type":"object","interface":"wl_seat"}]},{"name":"get_input_idle_notification","since":2,"args":[{"name":"id","type":"new_id","interface":"ext_idle_notification_v1"},{"name":"timeout","type":"uint"},{"name":"seat","type":"object","interface":"wl_seat"}]}],"events":[],"enums":{}},{"name":"ext_idle_notification_v1","version":2,"requests":[{"name":"destroy","type":"destructor","args":[]}],"events":[{"name":"idled","args":[]},{"name":"resumed","args":[]}],"enums":{}}]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"name":"ext_image_capture_source_v1","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]}],"events":[],"enums":{}},{"name":"ext_output_image_capture_source_manager_v1","version":1,"requests":[{"name":"create_source","args":[{"name":"source","type":"new_id","interface":"ext_image_capture_source_v1"},{"name":"output","type":"object","interface":"wl_output"}]},{"name":"destroy","type":"destructor","args":[]}],"events":[],"enums":{}},{"name":"ext_foreign_toplevel_image_capture_source_manager_v1","version":1,"requests":[{"name":"create_source","args":[{"name":"source","type":"new_id","interface":"ext_image_capture_source_v1"},{"name":"toplevel_handle","type":"object","interface":"ext_foreign_toplevel_handle_v1"}]},{"name":"destroy","type":"destructor","args":[]}],"events":[],"enums":{}}]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"name":"ext_image_copy_capture_manager_v1","version":1,"requests":[{"name":"create_session","args":[{"name":"session","type":"new_id","interface":"ext_image_copy_capture_session_v1"},{"name":"source","type":"object","interface":"ext_image_capture_source_v1"},{"name":"options","type":"uint","enum":"options"}]},{"name":"create_pointer_cursor_session","args":[{"name":"session","type":"new_id","interface":"ext_image_copy_capture_cursor_session_v1"},{"name":"source","type":"object","interface":"ext_image_capture_source_v1"},{"name":"pointer","type":"object","interface":"wl_pointer"}]},{"name":"destroy","type":"destructor","args":[]}],"events":[],"enums":{"error":{"entries":[{"name":"invalid_option","value":1}]},"options":{"entries":[{"name":"paint_cursors","value":1}]}}},{"name":"ext_image_copy_capture_session_v1","version":1,"requests":[{"name":"create_frame","args":[{"name":"frame","type":"new_id","interface":"ext_image_copy_capture_frame_v1"}]},{"name":"destroy","type":"destructor","args":[]}],"events":[{"name":"buffer_size","args":[{"name":"width","type":"uint"},{"name":"height","type":"uint"}]},{"name":"shm_format","args":[{"name":"format","type":"uint","enum":"wl_shm.format"}]},{"name":"dmabuf_device","args":[{"name":"device","type":"array"}]},{"name":"dmabuf_format","args":[{"name":"format","type":"uint"},{"name":"modifiers","type":"array"}]},{"name":"done","args":[]},{"name":"stopped","args":[]}],"enums":{"error":{"entries":[{"name":"duplicate_frame","value":1}]}}},{"name":"ext_image_copy_capture_frame_v1","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"attach_buffer","args":[{"name":"buffer","type":"object","interface":"wl_buffer"}]},{"name":"damage_buffer","args":[{"name":"x","type":"int"},{"name":"y","type":"int"},{"name":"width","type":"int"},{"name":"height","type":"int"}]},{"name":"capture","args":[]}],"events":[{"name":"transform","args":[{"name":"transform","type":"uint","enum":"wl_output.transform"}]},{"name":"damage","args":[{"name":"x","type":"int"},{"name":"y","type":"int"},{"name":"width","type":"int"},{"name":"height","type":"int"}]},{"name":"presentation_time","args":[{"name":"tv_sec_hi","type":"uint"},{"name":"tv_sec_lo","type":"uint"},{"name":"tv_nsec","type":"uint"}]},{"name":"ready","args":[]},{"name":"failed","args":[{"name":"reason","type":"uint","enum":"failure_reason"}]}],"enums":{"error":{"entries":[{"name":"no_buffer","value":1},{"name":"invalid_buffer_damage","value":2},{"name":"already_captured","value":3}]},"failure_reason":{"entries":[{"name":"unknown","value":0},{"name":"buffer_constraints","value":1},{"name":"stopped","value":2}]}}},{"name":"ext_image_copy_capture_cursor_session_v1","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"get_capture_session","args":[{"name":"session","type":"new_id","interface":"ext_image_copy_capture_session_v1"}]}],"events":[{"name":"enter","args":[]},{"name":"leave","args":[]},{"name":"position","args":[{"name":"x","type":"int"},{"name":"y","type":"int"}]},{"name":"hotspot","args":[{"name":"x","type":"int"},{"name":"y","type":"int"}]}],"enums":{"error":{"entries":[{"name":"duplicate_session","value":1}]}}}]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"name":"wp_fractional_scale_manager_v1","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"get_fractional_scale","args":[{"name":"id","type":"new_id","interface":"wp_fractional_scale_v1"},{"name":"surface","type":"object","interface":"wl_surface"}]}],"events":[],"enums":{"error":{"entries":[{"name":"fractional_scale_exists","value":0}]}}},{"name":"wp_fractional_scale_v1","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]}],"events":[{"name":"preferred_scale","args":[{"name":"scale","type":"uint"}]}],"enums":{}}]
|