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,476 @@
|
|
|
1
|
+
// Client-side decorations: the frame a window draws for itself where the
|
|
2
|
+
// compositor will not — GNOME's mutter has no xdg-decoration at all, so
|
|
3
|
+
// without one a window there is a bare rectangle.
|
|
4
|
+
//
|
|
5
|
+
// It is drawn to look like the frames beside it, which on GNOME are
|
|
6
|
+
// libadwaita's: from what the desktop says (framestyle.js — the button
|
|
7
|
+
// layout, the title's font, what titlebar clicks mean; appearance.js — light
|
|
8
|
+
// or dark) and from a libadwaita 1.7 window measured on a GNOME 48 desktop:
|
|
9
|
+
//
|
|
10
|
+
// - a headerbar 47px tall for an 11pt title and as tall as a bigger title
|
|
11
|
+
// needs (48px for 23pt), a hairline under it, a 1px highlight along its
|
|
12
|
+
// top in the dark scheme, 12px corners;
|
|
13
|
+
// - the title bold in the interface font, centred across the window;
|
|
14
|
+
// - window buttons as 24px circles 37px apart, the outermost centred 24px
|
|
15
|
+
// from the edge, holding the icon theme's own 16px symbolic glyphs;
|
|
16
|
+
// - a button is the bar mixed towards the text colour: 10% at rest, 15%
|
|
17
|
+
// under the pointer, 30% pressed — which gives exactly the greys
|
|
18
|
+
// measured, in both schemes and both focus states;
|
|
19
|
+
// - an unfocused window's text at half strength over a flatter bar;
|
|
20
|
+
// - a soft shadow, larger for the focused window, cast downward.
|
|
21
|
+
//
|
|
22
|
+
// The shadow is drawn in a margin round the window that is part of the
|
|
23
|
+
// surface and not of the window: `set_window_geometry` excludes it, the
|
|
24
|
+
// input region stops a resize band into it, and a maximised or tiled window
|
|
25
|
+
// has none (window.js owns that boundary; `marginsFor` here says how big).
|
|
26
|
+
// `insets()` includes it, so everything that converts between surface and
|
|
27
|
+
// content coordinates — input, drag and drop, the text-input caret — needs
|
|
28
|
+
// no idea there is a shadow at all.
|
|
29
|
+
//
|
|
30
|
+
// Interaction is requests: a titlebar press starts `xdg_toplevel.move`, a
|
|
31
|
+
// press in the band starts `resize` with the edges it is nearest, and the
|
|
32
|
+
// buttons do what they say. input.js routes; this answers where things are.
|
|
33
|
+
|
|
34
|
+
import fs from 'node:fs';
|
|
35
|
+
import { Path2D } from 'ntk';
|
|
36
|
+
import { RESIZE_EDGE, TOPLEVEL_STATE } from './window.js';
|
|
37
|
+
import { DEFAULTS, parseButtonLayout, titleFont } from './framestyle.js';
|
|
38
|
+
|
|
39
|
+
/** how far outside the window a resize grab still lands (the input band) */
|
|
40
|
+
export const RESIZE_MARGIN = 8;
|
|
41
|
+
/** how far along an edge from a corner a grab resizes both ways */
|
|
42
|
+
const CORNER = 18;
|
|
43
|
+
const RADIUS = 12;
|
|
44
|
+
const SEPARATOR = 1;
|
|
45
|
+
const BUTTON = 24;
|
|
46
|
+
const BUTTON_STEP = 37;
|
|
47
|
+
const BUTTON_INSET = 24;
|
|
48
|
+
|
|
49
|
+
/** the room a shadow needs; a maximised, tiled or fullscreen window has none */
|
|
50
|
+
export const SHADOW_MARGINS = Object.freeze({
|
|
51
|
+
left: 30,
|
|
52
|
+
top: 26,
|
|
53
|
+
right: 30,
|
|
54
|
+
bottom: 38,
|
|
55
|
+
});
|
|
56
|
+
const NO_MARGINS = Object.freeze({ left: 0, top: 0, right: 0, bottom: 0 });
|
|
57
|
+
|
|
58
|
+
/** [focused, unfocused] per scheme; the rest is derived. */
|
|
59
|
+
const PALETTE = {
|
|
60
|
+
dark: {
|
|
61
|
+
bar: ['#2e2e32', '#222226'],
|
|
62
|
+
fg: ['#ffffff', '#909092'],
|
|
63
|
+
highlight: 'rgba(255, 255, 255, 0.07)',
|
|
64
|
+
separator: 0.36,
|
|
65
|
+
},
|
|
66
|
+
light: {
|
|
67
|
+
bar: ['#ffffff', '#fafafb'],
|
|
68
|
+
fg: ['#333338', '#959597'],
|
|
69
|
+
highlight: null,
|
|
70
|
+
separator: 0.12,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Shadow layers — offset down, feather, opacity — focused, then not. The
|
|
76
|
+
* rounded-rectangle shader feathers as `1 - smoothstep(-f, f, d)`: half
|
|
77
|
+
* strength at the window's edge, nothing `f` beyond it. Four such layers
|
|
78
|
+
* fit libadwaita's measured falloff (the alpha of black at 1, 5, 10, 20px
|
|
79
|
+
* from a focused window's edge: 0.18, 0.12, 0.071, 0.032 against 0.19,
|
|
80
|
+
* 0.12, 0.071, 0.023 here); an unfocused window's is tighter and fainter.
|
|
81
|
+
* The margins above leave the widest room enough to fade out before the
|
|
82
|
+
* surface ends, so no layer is cut off.
|
|
83
|
+
*/
|
|
84
|
+
const SHADOWS = [
|
|
85
|
+
[
|
|
86
|
+
{ dy: 1, blur: 6, alpha: 0.1 },
|
|
87
|
+
{ dy: 2, blur: 18, alpha: 0.14 },
|
|
88
|
+
{ dy: 3, blur: 34, alpha: 0.13 },
|
|
89
|
+
{ dy: 4, blur: 48, alpha: 0.06 },
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
{ dy: 1, blur: 5, alpha: 0.12 },
|
|
93
|
+
{ dy: 1, blur: 14, alpha: 0.14 },
|
|
94
|
+
{ dy: 2, blur: 26, alpha: 0.06 },
|
|
95
|
+
],
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const hexRgb = (hex) => {
|
|
99
|
+
const n = parseInt(hex.slice(1), 16);
|
|
100
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
101
|
+
};
|
|
102
|
+
/** `a` moved towards `b` by `t`, as a CSS colour. */
|
|
103
|
+
function mix(a, b, t) {
|
|
104
|
+
const x = hexRgb(a);
|
|
105
|
+
const y = hexRgb(b);
|
|
106
|
+
const c = x.map((v, i) => Math.round(v + (y[i] - v) * t));
|
|
107
|
+
return `rgb(${c[0]}, ${c[1]}, ${c[2]})`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The window buttons' glyphs, from the installed icon theme when it has
|
|
111
|
+
// them — they are the pixels every GTK window beside this one shows — and
|
|
112
|
+
// Adwaita's, embedded, when it does not.
|
|
113
|
+
const GLYPH_DIRS = [
|
|
114
|
+
'/usr/share/icons/Adwaita/symbolic/ui',
|
|
115
|
+
'/usr/share/icons/Adwaita/scalable/ui',
|
|
116
|
+
];
|
|
117
|
+
const FALLBACK_GLYPHS = {
|
|
118
|
+
'window-close':
|
|
119
|
+
'M4.7 4 L8 7.3 L11.3 4 L12 4.7 L8.7 8 L12 11.3 L11.3 12 L8 8.7 L4.7 12 L4 11.3 L7.3 8 L4 4.7 Z',
|
|
120
|
+
'window-maximize':
|
|
121
|
+
'm 3.988281 3.992188 v 8.011718 h 8.011719 v -8.011718 z m 2 2 h 4.011719 v 4.011718 h -4.011719 z',
|
|
122
|
+
'window-restore':
|
|
123
|
+
'm 4.988281 4.992188 v 6.011718 h 6.011719 v -6.011718 z m 2 2 h 2.011719 v 2.011718 h -2.011719 z',
|
|
124
|
+
'window-minimize': 'm 4 10.007812 h 8 v 1.988282 h -8 z',
|
|
125
|
+
};
|
|
126
|
+
const glyphs = new Map();
|
|
127
|
+
function glyph(name) {
|
|
128
|
+
if (glyphs.has(name)) return glyphs.get(name);
|
|
129
|
+
let path = null;
|
|
130
|
+
for (const dir of GLYPH_DIRS) {
|
|
131
|
+
try {
|
|
132
|
+
const svg = fs.readFileSync(`${dir}/${name}-symbolic.svg`, 'utf8');
|
|
133
|
+
const ds = [...svg.matchAll(/\sd="([^"]+)"/g)].map((m) => m[1]);
|
|
134
|
+
if (ds.length) {
|
|
135
|
+
path = new Path2D(ds.join(' '));
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// not in this directory
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
path ??= new Path2D(FALLBACK_GLYPHS[name]);
|
|
143
|
+
glyphs.set(name, path);
|
|
144
|
+
return path;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class Decorations {
|
|
148
|
+
/**
|
|
149
|
+
* @param {object} [opts]
|
|
150
|
+
* @param {boolean} [opts.enabled=true] false: no frame at all (the
|
|
151
|
+
* compositor draws one, or the window asked for none)
|
|
152
|
+
*/
|
|
153
|
+
constructor({ enabled = true } = {}) {
|
|
154
|
+
this.enabled = enabled;
|
|
155
|
+
this.title = '';
|
|
156
|
+
this.active = false;
|
|
157
|
+
this.maximized = false;
|
|
158
|
+
this.tiled = false;
|
|
159
|
+
this.fullscreen = false;
|
|
160
|
+
this.hover = null; // 'close' | 'maximize' | 'minimize' | null
|
|
161
|
+
this.pressed = null;
|
|
162
|
+
/** `(text, size, weight, family) => { width, ascent, descent, runs } | null` */
|
|
163
|
+
this.measure = null;
|
|
164
|
+
/** the shell window's current margins (window.js) */
|
|
165
|
+
this.marginsOf = null;
|
|
166
|
+
/** asks for another frame when the title's font was still loading */
|
|
167
|
+
this.onNeedsRepaint = null;
|
|
168
|
+
this.setStyle(DEFAULTS, null);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The margin a shadow needs in these states. */
|
|
172
|
+
static marginsFor(states, enabled = true) {
|
|
173
|
+
if (!enabled) return NO_MARGINS;
|
|
174
|
+
for (const s of states ?? []) {
|
|
175
|
+
if (
|
|
176
|
+
s === TOPLEVEL_STATE.MAXIMIZED ||
|
|
177
|
+
s === TOPLEVEL_STATE.FULLSCREEN ||
|
|
178
|
+
(s >= TOPLEVEL_STATE.TILED_LEFT && s <= TOPLEVEL_STATE.TILED_BOTTOM)
|
|
179
|
+
) {
|
|
180
|
+
return NO_MARGINS;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return SHADOW_MARGINS;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* What the desktop says (framestyle.js) and its light or dark
|
|
188
|
+
* (appearance.js's snapshot). The titlebar's height follows the title's
|
|
189
|
+
* font, so a caller compares `insets()` before and after.
|
|
190
|
+
*/
|
|
191
|
+
setStyle(style, appearance) {
|
|
192
|
+
this.style = style ?? DEFAULTS;
|
|
193
|
+
this.dark = appearance?.colorScheme === 'dark';
|
|
194
|
+
this.highContrast = appearance?.contrast === 'high';
|
|
195
|
+
this.font = titleFont(this.style);
|
|
196
|
+
this.layout = parseButtonLayout(this.style.buttonLayout);
|
|
197
|
+
// A line of the title's font, plus the headerbar's padding; never less
|
|
198
|
+
// than libadwaita's 47px. From the font size, not from shaped metrics,
|
|
199
|
+
// so the height is known before the font has been opened.
|
|
200
|
+
this.barHeight = Math.max(47, Math.ceil(this.font.size * 1.17 + 12));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
setState(states) {
|
|
204
|
+
this.maximized = states.has(TOPLEVEL_STATE.MAXIMIZED);
|
|
205
|
+
this.tiled = [...states].some(
|
|
206
|
+
(s) => s >= TOPLEVEL_STATE.TILED_LEFT && s <= TOPLEVEL_STATE.TILED_BOTTOM,
|
|
207
|
+
);
|
|
208
|
+
this.fullscreen = states.has(TOPLEVEL_STATE.FULLSCREEN);
|
|
209
|
+
this.active = states.has(TOPLEVEL_STATE.ACTIVATED);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
margins() {
|
|
213
|
+
if (!this.enabled) return NO_MARGINS;
|
|
214
|
+
return this.marginsOf?.() ?? NO_MARGINS;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The frame's insets round the content, shadow margin included, in
|
|
218
|
+
* logical pixels of the surface. */
|
|
219
|
+
insets() {
|
|
220
|
+
if (!this.enabled || this.fullscreen) return { ...NO_MARGINS };
|
|
221
|
+
const m = this.margins();
|
|
222
|
+
return {
|
|
223
|
+
top: m.top + this.barHeight + SEPARATOR,
|
|
224
|
+
left: m.left,
|
|
225
|
+
right: m.right,
|
|
226
|
+
bottom: m.bottom,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** How much bigger than the content the surface is. */
|
|
231
|
+
extra() {
|
|
232
|
+
const i = this.insets();
|
|
233
|
+
return { width: i.left + i.right, height: i.top + i.bottom };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Rounded while the window floats; square once maximised or tiled. */
|
|
237
|
+
get radius() {
|
|
238
|
+
return this.maximized || this.tiled || this.fullscreen ? 0 : RADIUS;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The buttons, where they are: window-geometry coordinates. */
|
|
242
|
+
_buttons(geomWidth) {
|
|
243
|
+
const cy = Math.floor((this.barHeight - 1) / 2);
|
|
244
|
+
const out = [];
|
|
245
|
+
this.layout.left.forEach((id, k) => {
|
|
246
|
+
out.push({ id, cx: BUTTON_INSET + k * BUTTON_STEP, cy });
|
|
247
|
+
});
|
|
248
|
+
const right = this.layout.right;
|
|
249
|
+
right.forEach((id, k) => {
|
|
250
|
+
out.push({
|
|
251
|
+
id,
|
|
252
|
+
cx: geomWidth - BUTTON_INSET - (right.length - 1 - k) * BUTTON_STEP,
|
|
253
|
+
cy,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** How far from an edge the buttons on that side reach. */
|
|
260
|
+
_reach(side) {
|
|
261
|
+
const n = this.layout[side].length;
|
|
262
|
+
return n ? BUTTON_INSET + (n - 1) * BUTTON_STEP + BUTTON / 2 + 6 : 12;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* What is under a surface-local point: a resize edge, a titlebar button,
|
|
267
|
+
* the titlebar itself, or the content.
|
|
268
|
+
*
|
|
269
|
+
* @returns {{kind:'content'}|{kind:'titlebar'}|{kind:'button',id:string}|{kind:'resize',edges:number}}
|
|
270
|
+
*/
|
|
271
|
+
hitTest(x, y, surfaceWidth, surfaceHeight) {
|
|
272
|
+
if (!this.enabled || this.fullscreen) return { kind: 'content' };
|
|
273
|
+
const m = this.margins();
|
|
274
|
+
const gx = x - m.left;
|
|
275
|
+
const gy = y - m.top;
|
|
276
|
+
const gw = surfaceWidth - m.left - m.right;
|
|
277
|
+
const gh = surfaceHeight - m.top - m.bottom;
|
|
278
|
+
const inside = gx >= 0 && gx < gw && gy >= 0 && gy < gh;
|
|
279
|
+
if (!inside) {
|
|
280
|
+
if (this.maximized || this.tiled) return { kind: 'content' };
|
|
281
|
+
// the band outside the window: the edges the point is beyond, and a
|
|
282
|
+
// corner when it is near one
|
|
283
|
+
let left = gx < 0;
|
|
284
|
+
let right = gx >= gw;
|
|
285
|
+
let top = gy < 0;
|
|
286
|
+
let bottom = gy >= gh;
|
|
287
|
+
if (left || right) {
|
|
288
|
+
top ||= gy < CORNER;
|
|
289
|
+
bottom ||= gy >= gh - CORNER;
|
|
290
|
+
}
|
|
291
|
+
if (top || bottom) {
|
|
292
|
+
left ||= gx < CORNER;
|
|
293
|
+
right ||= gx >= gw - CORNER;
|
|
294
|
+
}
|
|
295
|
+
let edges = 0;
|
|
296
|
+
if (left) edges |= RESIZE_EDGE.LEFT;
|
|
297
|
+
else if (right) edges |= RESIZE_EDGE.RIGHT;
|
|
298
|
+
if (top) edges |= RESIZE_EDGE.TOP;
|
|
299
|
+
else if (bottom) edges |= RESIZE_EDGE.BOTTOM;
|
|
300
|
+
return { kind: 'resize', edges };
|
|
301
|
+
}
|
|
302
|
+
if (gy < this.barHeight) {
|
|
303
|
+
for (const b of this._buttons(gw)) {
|
|
304
|
+
if (
|
|
305
|
+
Math.abs(gx - b.cx) <= BUTTON / 2 + 4 &&
|
|
306
|
+
Math.abs(gy - b.cy) <= BUTTON / 2 + 4
|
|
307
|
+
)
|
|
308
|
+
return { kind: 'button', id: b.id };
|
|
309
|
+
}
|
|
310
|
+
return { kind: 'titlebar' };
|
|
311
|
+
}
|
|
312
|
+
return { kind: 'content' };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The cursor for a hit. */
|
|
316
|
+
static cursorFor(hit) {
|
|
317
|
+
if (hit.kind !== 'resize') return 'default';
|
|
318
|
+
const e = hit.edges;
|
|
319
|
+
if (e === RESIZE_EDGE.TOP_LEFT || e === RESIZE_EDGE.BOTTOM_RIGHT)
|
|
320
|
+
return 'nwse-resize';
|
|
321
|
+
if (e === RESIZE_EDGE.TOP_RIGHT || e === RESIZE_EDGE.BOTTOM_LEFT)
|
|
322
|
+
return 'nesw-resize';
|
|
323
|
+
if (e === RESIZE_EDGE.LEFT || e === RESIZE_EDGE.RIGHT) return 'ew-resize';
|
|
324
|
+
return 'ns-resize';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The colours for the current scheme and focus. */
|
|
328
|
+
_colors() {
|
|
329
|
+
const p = this.dark ? PALETTE.dark : PALETTE.light;
|
|
330
|
+
const k = this.active ? 0 : 1;
|
|
331
|
+
const bar = p.bar[k];
|
|
332
|
+
const fg = p.fg[k];
|
|
333
|
+
const sep = this.highContrast ? Math.min(1, p.separator * 2) : p.separator;
|
|
334
|
+
return {
|
|
335
|
+
bar,
|
|
336
|
+
fg,
|
|
337
|
+
highlight: this.active ? p.highlight : null,
|
|
338
|
+
// opaque: the row under the bar belongs to the frame, and a
|
|
339
|
+
// translucent line there would be drawn over nothing
|
|
340
|
+
separator: mix(bar, '#000006', sep),
|
|
341
|
+
button: (id) =>
|
|
342
|
+
mix(
|
|
343
|
+
bar,
|
|
344
|
+
fg,
|
|
345
|
+
this.pressed === id ? 0.3 : this.hover === id ? 0.15 : 0.1,
|
|
346
|
+
),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Draw the frame into the surface. `width`/`height` are the surface's in
|
|
352
|
+
* logical pixels; the context's transform already carries the output
|
|
353
|
+
* scale. Idempotent, because it runs every frame over the last one: the
|
|
354
|
+
* shadow's region is cleared before the shadow goes down, and nothing is
|
|
355
|
+
* drawn inside the window but the bar, which is opaque.
|
|
356
|
+
*/
|
|
357
|
+
paint(ctx, width, height, measure = this.measure) {
|
|
358
|
+
if (!this.enabled || this.fullscreen) return;
|
|
359
|
+
const m = this.margins();
|
|
360
|
+
const gx = m.left;
|
|
361
|
+
const gy = m.top;
|
|
362
|
+
const gw = width - m.left - m.right;
|
|
363
|
+
const gh = height - m.top - m.bottom;
|
|
364
|
+
const r = this.radius;
|
|
365
|
+
const c = this._colors();
|
|
366
|
+
|
|
367
|
+
if (m.left || m.top || m.right || m.bottom) {
|
|
368
|
+
// outside the window only: the region between the surface and the
|
|
369
|
+
// rounded window, even-odd
|
|
370
|
+
ctx.save();
|
|
371
|
+
ctx.beginPath();
|
|
372
|
+
ctx.rect(0, 0, width, height);
|
|
373
|
+
ctx.roundRect(gx, gy, gw, gh, r);
|
|
374
|
+
ctx.clip('evenodd');
|
|
375
|
+
ctx.clearRect(0, 0, width, height);
|
|
376
|
+
for (const s of SHADOWS[this.active ? 0 : 1]) {
|
|
377
|
+
ctx.fillShadow(
|
|
378
|
+
{ x: gx, y: gy + s.dy, width: gw, height: gh },
|
|
379
|
+
r,
|
|
380
|
+
s.blur,
|
|
381
|
+
`rgba(0, 0, 0, ${s.alpha})`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
ctx.restore();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// the headerbar: rounded at the top, square where it meets the content
|
|
388
|
+
const bh = this.barHeight;
|
|
389
|
+
ctx.fillStyle = c.bar;
|
|
390
|
+
if (r > 0) {
|
|
391
|
+
ctx.beginPath();
|
|
392
|
+
ctx.roundRect(gx, gy, gw, bh, r);
|
|
393
|
+
ctx.fill();
|
|
394
|
+
ctx.fillRect(gx, gy + r, gw, bh - r);
|
|
395
|
+
} else {
|
|
396
|
+
ctx.fillRect(gx, gy, gw, bh);
|
|
397
|
+
}
|
|
398
|
+
if (c.highlight) {
|
|
399
|
+
ctx.fillStyle = c.highlight;
|
|
400
|
+
ctx.fillRect(gx + r, gy, gw - 2 * r, 1);
|
|
401
|
+
}
|
|
402
|
+
ctx.fillStyle = c.separator;
|
|
403
|
+
ctx.fillRect(gx, gy + bh, gw, SEPARATOR);
|
|
404
|
+
|
|
405
|
+
for (const b of this._buttons(gw)) {
|
|
406
|
+
// centred on the pixel, not on the grid line between two: a 24px
|
|
407
|
+
// circle at a whole-pixel centre sits half a pixel up and left of
|
|
408
|
+
// libadwaita's
|
|
409
|
+
const cx = gx + b.cx + 0.5;
|
|
410
|
+
const cy = gy + b.cy + 0.5;
|
|
411
|
+
ctx.fillStyle = c.button(b.id);
|
|
412
|
+
ctx.beginPath();
|
|
413
|
+
ctx.roundRect(
|
|
414
|
+
cx - BUTTON / 2,
|
|
415
|
+
cy - BUTTON / 2,
|
|
416
|
+
BUTTON,
|
|
417
|
+
BUTTON,
|
|
418
|
+
BUTTON / 2,
|
|
419
|
+
);
|
|
420
|
+
ctx.fill();
|
|
421
|
+
const name =
|
|
422
|
+
b.id === 'maximize'
|
|
423
|
+
? this.maximized
|
|
424
|
+
? 'window-restore'
|
|
425
|
+
: 'window-maximize'
|
|
426
|
+
: `window-${b.id}`;
|
|
427
|
+
ctx.save();
|
|
428
|
+
ctx.translate(cx - 8, cy - 8);
|
|
429
|
+
ctx.fillStyle = c.fg;
|
|
430
|
+
ctx.fill(glyph(name));
|
|
431
|
+
ctx.restore();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
this._paintTitle(ctx, gx, gy, gw, c.fg, measure);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
_paintTitle(ctx, gx, gy, gw, color, measure) {
|
|
438
|
+
if (!this.title || !measure) return;
|
|
439
|
+
const f = this.font;
|
|
440
|
+
const fit = (text) => measure(text, f.size, f.weight, f.family);
|
|
441
|
+
let label = fit(this.title);
|
|
442
|
+
if (!label) {
|
|
443
|
+
// the font is still opening; ask again rather than show no title
|
|
444
|
+
this.onNeedsRepaint?.();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const side = Math.max(this._reach('left'), this._reach('right'));
|
|
448
|
+
const avail = Math.max(0, gw - 2 * side);
|
|
449
|
+
if (label.width > avail) {
|
|
450
|
+
// ellipsised at the end, as a GTK title is: the longest prefix that fits
|
|
451
|
+
let lo = 0;
|
|
452
|
+
let hi = this.title.length;
|
|
453
|
+
let best = null;
|
|
454
|
+
while (lo <= hi) {
|
|
455
|
+
const mid = (lo + hi) >> 1;
|
|
456
|
+
const l = fit(this.title.slice(0, mid).trimEnd() + '…');
|
|
457
|
+
if (l && l.width <= avail) {
|
|
458
|
+
best = l;
|
|
459
|
+
lo = mid + 1;
|
|
460
|
+
} else hi = mid - 1;
|
|
461
|
+
}
|
|
462
|
+
if (!best) return;
|
|
463
|
+
label = best;
|
|
464
|
+
}
|
|
465
|
+
const x = gx + (gw - label.width) / 2;
|
|
466
|
+
const baseline =
|
|
467
|
+
gy + (this.barHeight - (label.ascent + label.descent)) / 2 + label.ascent;
|
|
468
|
+
ctx.save();
|
|
469
|
+
ctx.beginPath();
|
|
470
|
+
ctx.rect(gx + side, gy, avail, this.barHeight);
|
|
471
|
+
ctx.clip();
|
|
472
|
+
ctx.translate(x, baseline);
|
|
473
|
+
ctx.drawTextRuns(label.runs.map((run) => ({ ...run, color })));
|
|
474
|
+
ctx.restore();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Who owns the GL state right now.
|
|
2
|
+
//
|
|
3
|
+
// Every 2d context on this backend draws through one GLES context: a
|
|
4
|
+
// window's, the pane a `<glarea>`'s children are painted on, and each
|
|
5
|
+
// offscreen surface's are all the same device, differing only in which
|
|
6
|
+
// framebuffer they bind and what they left the blend, scissor and stencil
|
|
7
|
+
// state saying. That is unlike every other backend react-x11 has — an
|
|
8
|
+
// XRender context is an encoder aimed at a drawable, a Cocoa one is a CGL
|
|
9
|
+
// bitmap that carries its own state — and it is why ntk's advice to hold a
|
|
10
|
+
// surface's context and draw into it whenever you like did not hold here:
|
|
11
|
+
// a held context's draws landed wherever the *last* context had pointed the
|
|
12
|
+
// device (issue #566).
|
|
13
|
+
//
|
|
14
|
+
// So the device gets an owner. A context claims it before it touches GL,
|
|
15
|
+
// and claiming is where the target is bound and the state it assumes is put
|
|
16
|
+
// back. Whoever drew in between does not have to know who comes next, and a
|
|
17
|
+
// caller does not have to know there is a device at all.
|
|
18
|
+
//
|
|
19
|
+
// One record per `gl`, keyed weakly: a GPU that goes away takes its record
|
|
20
|
+
// with it.
|
|
21
|
+
const devices = new WeakMap();
|
|
22
|
+
|
|
23
|
+
/** Whether this `gl` can key the table — a context may be built without one. */
|
|
24
|
+
const shared = (gl) =>
|
|
25
|
+
gl !== null && (typeof gl === 'object' || typeof gl === 'function');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The record for a device. `owner` is the context whose state is installed,
|
|
29
|
+
* or null when something outside this bookkeeping — foreign GL in a
|
|
30
|
+
* `<glarea>`, a framebuffer blit — has had its way with it.
|
|
31
|
+
*
|
|
32
|
+
* A context built with no GL behind it — `new WaylandContext2D(null)`, the
|
|
33
|
+
* seams above the first draw that `test/wayland/pure.test.js` and
|
|
34
|
+
* `glyph-runs.test.js` exercise — shares nothing, and gets a record of its
|
|
35
|
+
* own rather than an error.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} gl the GLES entry points
|
|
38
|
+
* @returns {{ owner: object|null }}
|
|
39
|
+
*/
|
|
40
|
+
export function glDevice(gl) {
|
|
41
|
+
if (!shared(gl)) return { owner: null };
|
|
42
|
+
let device = devices.get(gl);
|
|
43
|
+
if (!device) devices.set(gl, (device = { owner: null }));
|
|
44
|
+
return device;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Say that the state no longer belongs to whoever last claimed it, so the
|
|
49
|
+
* next context to draw re-establishes its own. Call after touching GL
|
|
50
|
+
* behind the contexts' backs.
|
|
51
|
+
*/
|
|
52
|
+
export function releaseDevice(gl) {
|
|
53
|
+
const device = shared(gl) ? devices.get(gl) : null;
|
|
54
|
+
if (device) device.owner = null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Turning a GPU buffer into something the compositor can show.
|
|
2
|
+
//
|
|
3
|
+
// This is the whole of the Wayland side of Tier D, and it is short, which is
|
|
4
|
+
// the point the RFC was making: x11-dri's `Surface.swap()` already answers
|
|
5
|
+
// `{ fd, stride, offset, modifier }`, and that is exactly the argument list
|
|
6
|
+
// `zwp_linux_buffer_params_v1.add` wants. No pixels cross the socket — the
|
|
7
|
+
// descriptor goes over once per buffer, and every later frame in that buffer
|
|
8
|
+
// is an `attach` of a `wl_buffer` the compositor already holds.
|
|
9
|
+
//
|
|
10
|
+
// The one judgement call is `create` versus `create_immed`. `create_immed`
|
|
11
|
+
// saves a round trip and answers the buffer synchronously; the price is that
|
|
12
|
+
// a refusal is a *protocol error*, which is fatal and takes the connection
|
|
13
|
+
// with it. A refusal is not hypothetical — it is what happens when client and
|
|
14
|
+
// compositor are on different DRM devices and the tiled layout means nothing
|
|
15
|
+
// to the one displaying it — and it has a real remedy, a linear buffer. So
|
|
16
|
+
// the first import of a generation goes through `create`, whose `failed`
|
|
17
|
+
// event is catchable and gives the fallback somewhere to happen. Buffers are
|
|
18
|
+
// created a handful of times per generation and never again in steady state,
|
|
19
|
+
// so this is a startup cost, not a per-frame one.
|
|
20
|
+
|
|
21
|
+
/** `zwp_linux_buffer_params_v1.flags` — none of which we want. */
|
|
22
|
+
const NO_FLAGS = 0;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Import one dma-buf as a `wl_buffer`.
|
|
26
|
+
*
|
|
27
|
+
* Takes ownership of `out.fd`: the transport closes descriptors once they are
|
|
28
|
+
* on the wire, which is the contract every fd-passing Wayland request has.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} dmabuf a bound `zwp_linux_dmabuf_v1`
|
|
31
|
+
* @param {object} out a `SwapResult` from `Surface.swap()`, with `isNew: true`
|
|
32
|
+
* @param {number} format a DRM fourcc — `x11-dri`'s `FORMAT.XRGB8888` and friends
|
|
33
|
+
* @returns {Promise<object>} the `wl_buffer` proxy
|
|
34
|
+
*/
|
|
35
|
+
export async function importDmabuf(dmabuf, out, format) {
|
|
36
|
+
const params = await dmabuf.create_params();
|
|
37
|
+
// A single plane. The modifier is split across two uint arguments because
|
|
38
|
+
// the wire has no 64-bit integer type.
|
|
39
|
+
const modifier = BigInt(out.modifier ?? 0n);
|
|
40
|
+
await params.add(
|
|
41
|
+
out.fd,
|
|
42
|
+
0, // plane_idx
|
|
43
|
+
out.offset >>> 0,
|
|
44
|
+
out.stride >>> 0,
|
|
45
|
+
Number(modifier >> 32n) >>> 0,
|
|
46
|
+
Number(modifier & 0xffffffffn) >>> 0,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return await new Promise((resolve, reject) => {
|
|
50
|
+
let settled = false;
|
|
51
|
+
params.on('created', (buffer) => {
|
|
52
|
+
if (settled) return;
|
|
53
|
+
settled = true;
|
|
54
|
+
// The params object is single-use; the buffer outlives it.
|
|
55
|
+
params.destroy().catch(() => {});
|
|
56
|
+
resolve(buffer);
|
|
57
|
+
});
|
|
58
|
+
params.on('failed', () => {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
params.destroy().catch(() => {});
|
|
62
|
+
reject(
|
|
63
|
+
new DmabufRefused(
|
|
64
|
+
`the compositor refused a ${out.width}x${out.height} dma-buf ` +
|
|
65
|
+
`(stride ${out.stride}, modifier 0x${modifier.toString(16)})`,
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
params.create(out.width, out.height, format, NO_FLAGS).catch((err) => {
|
|
70
|
+
if (settled) return;
|
|
71
|
+
settled = true;
|
|
72
|
+
reject(err);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The compositor would not take this buffer.
|
|
79
|
+
*
|
|
80
|
+
* Distinguished from any other failure because it has a remedy — retry the
|
|
81
|
+
* generation with a linear layout — and because it is the expected answer on
|
|
82
|
+
* a multi-GPU machine rather than a bug.
|
|
83
|
+
*/
|
|
84
|
+
export class DmabufRefused extends Error {
|
|
85
|
+
constructor(message) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = 'DmabufRefused';
|
|
88
|
+
}
|
|
89
|
+
}
|