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,257 @@
|
|
|
1
|
+
// What the desktop says a window frame looks like and does — the part of it
|
|
2
|
+
// a client-side frame needs: which buttons go where, the title's font, and
|
|
3
|
+
// what a click on the titlebar means.
|
|
4
|
+
//
|
|
5
|
+
// GTK reads these from GSettings; anything else reads the same keys through
|
|
6
|
+
// the settings portal, which serves the `org.gnome.desktop.*` schemas beside
|
|
7
|
+
// the four standard appearance values (src/appearance.js reads those, and
|
|
8
|
+
// the frame takes its light or dark from there). The keys, with GNOME's
|
|
9
|
+
// defaults:
|
|
10
|
+
//
|
|
11
|
+
// org.gnome.desktop.wm.preferences
|
|
12
|
+
// button-layout 'appmenu:close' left:right, comma lists
|
|
13
|
+
// action-double-click-titlebar 'toggle-maximize'
|
|
14
|
+
// action-middle-click-titlebar 'none'
|
|
15
|
+
// action-right-click-titlebar 'menu'
|
|
16
|
+
// org.gnome.desktop.interface
|
|
17
|
+
// font-name 'Adwaita Sans 11' a Pango description
|
|
18
|
+
// text-scaling-factor 1.0
|
|
19
|
+
//
|
|
20
|
+
// The title is drawn in the *interface* font, bold, because that is what a
|
|
21
|
+
// libadwaita headerbar does: measured against one on a desktop whose
|
|
22
|
+
// `font-name` is 'Sans 23', its title's cap height is a 23pt DejaVu Sans
|
|
23
|
+
// Bold's. `titlebar-font` is for frames the window manager draws (Xwayland
|
|
24
|
+
// windows); a client frame that used it would not match the GTK apps beside
|
|
25
|
+
// it.
|
|
26
|
+
//
|
|
27
|
+
// Where there is no portal — another desktop, a test — the defaults stand.
|
|
28
|
+
// They are GNOME's, so a frame drawn to them is the frame GTK would draw.
|
|
29
|
+
|
|
30
|
+
import { EventEmitter } from 'node:events';
|
|
31
|
+
import { sessionBus } from '../bus.js';
|
|
32
|
+
import { desktopIntegrationEnabled } from '../desktopintegration.js';
|
|
33
|
+
|
|
34
|
+
const PORTAL_NAME = 'org.freedesktop.portal.Desktop';
|
|
35
|
+
const PORTAL_PATH = '/org/freedesktop/portal/desktop';
|
|
36
|
+
const SETTINGS_IFACE = 'org.freedesktop.portal.Settings';
|
|
37
|
+
const WM = 'org.gnome.desktop.wm.preferences';
|
|
38
|
+
const IFACE = 'org.gnome.desktop.interface';
|
|
39
|
+
const NAMESPACES = [WM, IFACE];
|
|
40
|
+
|
|
41
|
+
export const DEFAULTS = Object.freeze({
|
|
42
|
+
buttonLayout: 'appmenu:close',
|
|
43
|
+
fontName: 'Adwaita Sans 11',
|
|
44
|
+
textScaling: 1,
|
|
45
|
+
doubleClick: 'toggle-maximize',
|
|
46
|
+
middleClick: 'none',
|
|
47
|
+
rightClick: 'menu',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** The buttons a client frame can draw; `appmenu`, `icon`, `spacer` have
|
|
51
|
+
* nothing to show here and are dropped. */
|
|
52
|
+
const BUTTONS = new Set(['close', 'minimize', 'maximize']);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `'icon,menu:minimize,maximize,close'` → `{ left: [], right: ['minimize',
|
|
56
|
+
* 'maximize', 'close'] }`. A layout with no colon is all on the left, as in
|
|
57
|
+
* GTK; a button named twice is drawn once, where it first appears.
|
|
58
|
+
*/
|
|
59
|
+
export function parseButtonLayout(value) {
|
|
60
|
+
const [left = '', right = ''] = String(value ?? '').split(':');
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
const side = (list) =>
|
|
63
|
+
list
|
|
64
|
+
.split(',')
|
|
65
|
+
.map((b) => b.trim())
|
|
66
|
+
.filter((b) => {
|
|
67
|
+
if (!BUTTONS.has(b) || seen.has(b)) return false;
|
|
68
|
+
seen.add(b);
|
|
69
|
+
return true;
|
|
70
|
+
});
|
|
71
|
+
return { left: side(left), right: side(right) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const WEIGHTS = {
|
|
75
|
+
thin: 100,
|
|
76
|
+
ultralight: 200,
|
|
77
|
+
'ultra-light': 200,
|
|
78
|
+
extralight: 200,
|
|
79
|
+
'extra-light': 200,
|
|
80
|
+
light: 300,
|
|
81
|
+
semilight: 350,
|
|
82
|
+
'semi-light': 350,
|
|
83
|
+
book: 380,
|
|
84
|
+
regular: 400,
|
|
85
|
+
normal: 400,
|
|
86
|
+
medium: 500,
|
|
87
|
+
semibold: 600,
|
|
88
|
+
'semi-bold': 600,
|
|
89
|
+
demibold: 600,
|
|
90
|
+
'demi-bold': 600,
|
|
91
|
+
bold: 700,
|
|
92
|
+
ultrabold: 800,
|
|
93
|
+
'ultra-bold': 800,
|
|
94
|
+
extrabold: 800,
|
|
95
|
+
'extra-bold': 800,
|
|
96
|
+
heavy: 900,
|
|
97
|
+
black: 900,
|
|
98
|
+
};
|
|
99
|
+
const STYLE_WORDS = new Set([
|
|
100
|
+
'italic',
|
|
101
|
+
'oblique',
|
|
102
|
+
'roman',
|
|
103
|
+
'small-caps',
|
|
104
|
+
'condensed',
|
|
105
|
+
'semi-condensed',
|
|
106
|
+
'extra-condensed',
|
|
107
|
+
'ultra-condensed',
|
|
108
|
+
'expanded',
|
|
109
|
+
'semi-expanded',
|
|
110
|
+
'extra-expanded',
|
|
111
|
+
'ultra-expanded',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A Pango font description — `"Adwaita Sans Bold 11"`, `"Sans 23"`,
|
|
116
|
+
* `"DejaVu Sans Condensed Italic 12px"` — as `{ family, size, unit, weight,
|
|
117
|
+
* italic }`. Style words are read off the end until one is not a style
|
|
118
|
+
* word; what remains is the family (the first of a comma list).
|
|
119
|
+
*/
|
|
120
|
+
export function parseFontName(value) {
|
|
121
|
+
const words = String(value ?? '')
|
|
122
|
+
.trim()
|
|
123
|
+
.split(/\s+/)
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
let size = null;
|
|
126
|
+
let unit = 'pt';
|
|
127
|
+
const last = words.at(-1);
|
|
128
|
+
if (last && /^\d+(\.\d+)?(px)?$/i.test(last)) {
|
|
129
|
+
if (/px$/i.test(last)) unit = 'px';
|
|
130
|
+
size = parseFloat(last);
|
|
131
|
+
words.pop();
|
|
132
|
+
}
|
|
133
|
+
let weight = 400;
|
|
134
|
+
let italic = false;
|
|
135
|
+
while (words.length > 1) {
|
|
136
|
+
const w = words.at(-1).toLowerCase();
|
|
137
|
+
if (Object.hasOwn(WEIGHTS, w)) weight = WEIGHTS[w];
|
|
138
|
+
else if (STYLE_WORDS.has(w)) {
|
|
139
|
+
if (w === 'italic' || w === 'oblique') italic = true;
|
|
140
|
+
} else break;
|
|
141
|
+
words.pop();
|
|
142
|
+
}
|
|
143
|
+
const family = words.join(' ').split(',')[0].trim() || 'sans-serif';
|
|
144
|
+
return { family, size: size ?? 11, unit, weight, italic };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The title's font: the interface font, bold, at 96dpi times the scaling
|
|
148
|
+
* factor — in logical pixels. */
|
|
149
|
+
export function titleFont(style = DEFAULTS) {
|
|
150
|
+
const f = parseFontName(style.fontName);
|
|
151
|
+
const scaling = Number(style.textScaling) > 0 ? Number(style.textScaling) : 1;
|
|
152
|
+
const px = (f.unit === 'px' ? f.size : (f.size * 96) / 72) * scaling;
|
|
153
|
+
return { family: f.family, size: px, weight: 700 };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The two namespaces `ReadAll` answered, as a frame style. */
|
|
157
|
+
export function fromPortal(all) {
|
|
158
|
+
const wm = all?.[WM] ?? {};
|
|
159
|
+
const iface = all?.[IFACE] ?? {};
|
|
160
|
+
const str = (v, d) => (typeof v === 'string' && v ? v : d);
|
|
161
|
+
const num = (v, d) => (typeof v === 'number' && v > 0 ? v : d);
|
|
162
|
+
return Object.freeze({
|
|
163
|
+
buttonLayout: str(wm['button-layout'], DEFAULTS.buttonLayout),
|
|
164
|
+
fontName: str(iface['font-name'], DEFAULTS.fontName),
|
|
165
|
+
textScaling: num(iface['text-scaling-factor'], DEFAULTS.textScaling),
|
|
166
|
+
doubleClick: str(wm['action-double-click-titlebar'], DEFAULTS.doubleClick),
|
|
167
|
+
middleClick: str(wm['action-middle-click-titlebar'], DEFAULTS.middleClick),
|
|
168
|
+
rightClick: str(wm['action-right-click-titlebar'], DEFAULTS.rightClick),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The desktop's frame style, followed: `value` is always usable (the
|
|
174
|
+
* defaults until the portal answers), and 'change' fires when it moves.
|
|
175
|
+
*/
|
|
176
|
+
export class FrameStyle extends EventEmitter {
|
|
177
|
+
constructor() {
|
|
178
|
+
super();
|
|
179
|
+
this.value = DEFAULTS;
|
|
180
|
+
/** 'portal' once the portal answered; 'defaults' until then, or for good */
|
|
181
|
+
this.source = 'defaults';
|
|
182
|
+
this._stop = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read once and subscribe. Never throws: no portal means the defaults.
|
|
187
|
+
* The order is appearance.js's, for its reasons — the match rule and the
|
|
188
|
+
* handler go on before the read, so a change landing mid-read is caught,
|
|
189
|
+
* and reads are sequenced so an older answer never overwrites a newer one.
|
|
190
|
+
* The bus ref is released at the end: the subscription outlives it, and
|
|
191
|
+
* holding it would keep the process alive for as long as the frame cared
|
|
192
|
+
* what the desktop said.
|
|
193
|
+
*/
|
|
194
|
+
async start() {
|
|
195
|
+
if (!desktopIntegrationEnabled()) return this;
|
|
196
|
+
let ref = null;
|
|
197
|
+
try {
|
|
198
|
+
ref = await sessionBus();
|
|
199
|
+
} catch {
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
if (!ref) return this;
|
|
203
|
+
let sub = null;
|
|
204
|
+
try {
|
|
205
|
+
sub = await ref.bus.watch(
|
|
206
|
+
`type='signal',sender='${PORTAL_NAME}',` +
|
|
207
|
+
`interface='${SETTINGS_IFACE}',member='SettingChanged'`,
|
|
208
|
+
);
|
|
209
|
+
let started = 0;
|
|
210
|
+
let published = 0;
|
|
211
|
+
const bus = ref.bus;
|
|
212
|
+
const refresh = async () => {
|
|
213
|
+
const mine = ++started;
|
|
214
|
+
const all = await bus.invoke(
|
|
215
|
+
{
|
|
216
|
+
destination: PORTAL_NAME,
|
|
217
|
+
path: PORTAL_PATH,
|
|
218
|
+
interface: SETTINGS_IFACE,
|
|
219
|
+
member: 'ReadAll',
|
|
220
|
+
signature: 'as',
|
|
221
|
+
body: [NAMESPACES],
|
|
222
|
+
},
|
|
223
|
+
{ timeout: 5_000 },
|
|
224
|
+
);
|
|
225
|
+
if (!all || mine <= published) return;
|
|
226
|
+
published = mine;
|
|
227
|
+
this.value = fromPortal(all);
|
|
228
|
+
this.source = 'portal';
|
|
229
|
+
this.emit('change', this.value);
|
|
230
|
+
};
|
|
231
|
+
const key = bus.mangle(PORTAL_PATH, SETTINGS_IFACE, 'SettingChanged');
|
|
232
|
+
const onSignal = ([namespace]) => {
|
|
233
|
+
if (!NAMESPACES.includes(namespace)) return;
|
|
234
|
+
refresh().catch(() => {
|
|
235
|
+
// the portal went away mid-session; the last answer stands
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
bus.signals.on(key, onSignal);
|
|
239
|
+
const subscription = sub;
|
|
240
|
+
this._stop = () => {
|
|
241
|
+
bus.signals.off?.(key, onSignal);
|
|
242
|
+
subscription?.remove().catch(() => {});
|
|
243
|
+
};
|
|
244
|
+
await refresh();
|
|
245
|
+
} catch {
|
|
246
|
+
await sub?.remove().catch(() => {});
|
|
247
|
+
} finally {
|
|
248
|
+
await ref.release();
|
|
249
|
+
}
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
stop() {
|
|
254
|
+
this._stop?.();
|
|
255
|
+
this._stop = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// `<glarea>` on the Wayland backend: a viewport into the window's backing
|
|
2
|
+
// target, not a window of its own.
|
|
3
|
+
//
|
|
4
|
+
// On X11 a `<glarea>` is a child window — a separate drawable the X server
|
|
5
|
+
// stacks above the parent's drawing — and on the Cocoa backend a sublayer
|
|
6
|
+
// the WindowServer composites. Neither exists here. A Wayland client owns
|
|
7
|
+
// one buffer per surface, and a subsurface would be a second swapchain, a
|
|
8
|
+
// second frame clock and a second dma-buf import per frame for what is, on
|
|
9
|
+
// this backend, already one GL context drawing into one target. So the
|
|
10
|
+
// surface is a *rectangle of the backing target*: `makeCurrent()` binds the
|
|
11
|
+
// window's target with the viewport and scissor set to the area's rect, the
|
|
12
|
+
// app's GL calls land inside it, and `SwapBuffers()` is the note that the
|
|
13
|
+
// rect changed. The frame loop orders it — the tree's 2D paint is flushed
|
|
14
|
+
// under it, the area draws over, and the panes its children were painted
|
|
15
|
+
// on are composited over that (backendwindow.js `_runSurfaces`).
|
|
16
|
+
//
|
|
17
|
+
// What `GlAreaNode` (src/glnodes.js) needs is the child-"window" contract:
|
|
18
|
+
// `createWindow({ parent, … })` answering an object with
|
|
19
|
+
// `getContext('opengl', config)`, `setState(rect)`, `map()`, `destroy()`,
|
|
20
|
+
// `requestAnimationFrame`; and from the context `backend`, `ready`,
|
|
21
|
+
// `makeCurrent`, `SwapBuffers`, `canRender`, `onFrameAvailable`, plus the
|
|
22
|
+
// WebGL-shaped GL table itself. The table is the shared one, prototype-
|
|
23
|
+
// delegated as src/cocoa/glarea.js does it, with the entry points that name
|
|
24
|
+
// framebuffer coordinates — `viewport`, `scissor`, `bindFramebuffer`, and
|
|
25
|
+
// the scissor switch — re-based on the rect, so a scene written for a
|
|
26
|
+
// window of its own draws in its corner of ours.
|
|
27
|
+
//
|
|
28
|
+
// Two coordinate systems, as everywhere in this backend: the rect is in the
|
|
29
|
+
// window's content device pixels, y down; GL's viewport and scissor are in
|
|
30
|
+
// the target's pixels from the bottom-left. The content origin (the frame's
|
|
31
|
+
// insets) is added and y is flipped once, here, in `place()`.
|
|
32
|
+
//
|
|
33
|
+
// The pane (`WaylandOverlayPane`) is the other half of the same idea. The
|
|
34
|
+
// children of a `<glarea>` are 2D content drawn *above* its frames
|
|
35
|
+
// (src/gloverlay.js); on a backend that composites, one pane covers the
|
|
36
|
+
// surface and the backend blends it over the GL. Here a pane is an offscreen
|
|
37
|
+
// target the tree paints into during its flush, and the window draws it over
|
|
38
|
+
// the surface with `drawImage` once the surface has drawn — so a translucent
|
|
39
|
+
// legend, an antialiased edge or a shadow blends with the GL frame under it,
|
|
40
|
+
// as on the Cocoa backend and unlike X11's opaque child windows.
|
|
41
|
+
|
|
42
|
+
import { GLTarget } from './target.js';
|
|
43
|
+
import { WaylandContext2D } from './context2d.js';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The `visual` `chooseGLConfig` answers with. `GlAreaNode` passes the config's
|
|
47
|
+
* visual and depth straight back to `createWindow`, which is how the app
|
|
48
|
+
* tells a `<glarea>`'s child "window" apart from a real one.
|
|
49
|
+
*/
|
|
50
|
+
export const GLAREA_VISUAL = 'wayland:glarea';
|
|
51
|
+
|
|
52
|
+
export class WaylandGLArea {
|
|
53
|
+
/**
|
|
54
|
+
* @param {import('./backendwindow.js').WaylandBackendWindow} parent
|
|
55
|
+
* @param {{x?:number,y?:number,width?:number,height?:number}} options
|
|
56
|
+
*/
|
|
57
|
+
constructor(parent, options = {}) {
|
|
58
|
+
this.parent = parent;
|
|
59
|
+
this.app = parent.app;
|
|
60
|
+
this.destroyed = false;
|
|
61
|
+
this.mapped = false;
|
|
62
|
+
this._reactX11Node = null;
|
|
63
|
+
this._context = null;
|
|
64
|
+
this._listeners = new Map();
|
|
65
|
+
/** set by `SwapBuffers` for the frame it happened in */
|
|
66
|
+
this.drewThisFrame = false;
|
|
67
|
+
this.rect = {
|
|
68
|
+
x: options.x ?? 0,
|
|
69
|
+
y: options.y ?? 0,
|
|
70
|
+
width: Math.max(1, options.width ?? 1),
|
|
71
|
+
height: Math.max(1, options.height ?? 1),
|
|
72
|
+
};
|
|
73
|
+
parent._surfaces.add(this);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get width() {
|
|
77
|
+
return this.rect.width;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get height() {
|
|
81
|
+
return this.rect.height;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Geometry in content device pixels, the unit `GlAreaNode`'s rects are in. */
|
|
85
|
+
setState(rect) {
|
|
86
|
+
if (this.destroyed) return this;
|
|
87
|
+
this.rect = {
|
|
88
|
+
x: rect.x ?? this.rect.x,
|
|
89
|
+
y: rect.y ?? this.rect.y,
|
|
90
|
+
width: Math.max(1, rect.width ?? this.rect.width),
|
|
91
|
+
height: Math.max(1, rect.height ?? this.rect.height),
|
|
92
|
+
};
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
move(x, y) {
|
|
97
|
+
return this.setState({ x, y });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
resize(width, height) {
|
|
101
|
+
return this.setState({ width, height });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
map() {
|
|
105
|
+
this.mapped = true;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
unmap() {
|
|
110
|
+
this.mapped = false;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
on(name, fn) {
|
|
115
|
+
let set = this._listeners.get(name);
|
|
116
|
+
if (!set) this._listeners.set(name, (set = new Set()));
|
|
117
|
+
set.add(fn);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
off(name, fn) {
|
|
122
|
+
this._listeners.get(name)?.delete(fn);
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
emit(name, ev) {
|
|
127
|
+
for (const fn of this._listeners.get(name) ?? []) fn(ev);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The area's frame is a part of its window's: the callback runs after the
|
|
132
|
+
* tree's paint, in the same frame, so what it draws goes over the 2D and
|
|
133
|
+
* out with the same present.
|
|
134
|
+
*/
|
|
135
|
+
requestAnimationFrame(fn) {
|
|
136
|
+
return this.parent.requestSurfaceFrame(fn);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getContext(kind, config) {
|
|
140
|
+
if (this.destroyed) return null;
|
|
141
|
+
if (kind !== 'opengl' && kind !== 'gles' && kind !== 'webgl') return null;
|
|
142
|
+
if (!this._context) this._context = createGLAreaContext(this, config);
|
|
143
|
+
return this._context;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
destroy() {
|
|
147
|
+
if (this.destroyed) return;
|
|
148
|
+
this.destroyed = true;
|
|
149
|
+
this._context?._destroy();
|
|
150
|
+
this._context = null;
|
|
151
|
+
this.parent._surfaces.delete(this);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The `gl` a `<glarea>`'s `onDraw` receives: the shared WebGL-shaped table,
|
|
157
|
+
* with this area's rect behind the entry points that would otherwise name
|
|
158
|
+
* the whole target.
|
|
159
|
+
*/
|
|
160
|
+
function createGLAreaContext(area, config) {
|
|
161
|
+
const parent = area.parent;
|
|
162
|
+
const gl = parent.glctx.gl;
|
|
163
|
+
const ctx = Object.create(gl);
|
|
164
|
+
let destroyed = false;
|
|
165
|
+
// the rect in the target's GL coordinates, as of the last makeCurrent
|
|
166
|
+
let bx = 0;
|
|
167
|
+
let by = 0;
|
|
168
|
+
let bw = 1;
|
|
169
|
+
let bh = 1;
|
|
170
|
+
|
|
171
|
+
const place = () => {
|
|
172
|
+
const o = parent.contentOrigin;
|
|
173
|
+
const r = area.rect;
|
|
174
|
+
const H = parent.glctx.backing?.height ?? 0;
|
|
175
|
+
bx = o.x + r.x;
|
|
176
|
+
bw = r.width;
|
|
177
|
+
bh = r.height;
|
|
178
|
+
by = H - (o.y + r.y + r.height);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
ctx.backend = 'direct';
|
|
182
|
+
ctx.config = config;
|
|
183
|
+
ctx.glVersion = parent.glctx.glVersion;
|
|
184
|
+
ctx.ready = Promise.resolve();
|
|
185
|
+
ctx.onFrameAvailable = null;
|
|
186
|
+
// No swapchain of its own to be held up by: the frame it draws in is the
|
|
187
|
+
// window's, and the window's frame loop only runs it when there is one.
|
|
188
|
+
ctx.canRender = () => !destroyed && !parent._destroyed;
|
|
189
|
+
|
|
190
|
+
ctx.makeCurrent = () => {
|
|
191
|
+
if (destroyed || parent._destroyed) return;
|
|
192
|
+
parent.app.makeCurrent();
|
|
193
|
+
parent.glctx.bindBacking();
|
|
194
|
+
place();
|
|
195
|
+
// The scissor is what keeps a `clear()` inside the rect. It stays on for
|
|
196
|
+
// the whole draw: `disable(SCISSOR_TEST)` below re-asserts it.
|
|
197
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
198
|
+
gl.scissor(bx, by, bw, bh);
|
|
199
|
+
gl.viewport(bx, by, bw, bh);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
ctx.viewport = (x, y, w, h) => gl.viewport(bx + x, by + y, w, h);
|
|
203
|
+
|
|
204
|
+
// A scissor of the scene's own is cut to the rect: nothing it asks for
|
|
205
|
+
// can reach outside, and `(0, 0, width, height)` is the whole of it.
|
|
206
|
+
ctx.scissor = (x, y, w, h) => {
|
|
207
|
+
const x0 = Math.max(bx, bx + x);
|
|
208
|
+
const y0 = Math.max(by, by + y);
|
|
209
|
+
const x1 = Math.min(bx + bw, bx + x + w);
|
|
210
|
+
const y1 = Math.min(by + bh, by + y + h);
|
|
211
|
+
gl.scissor(x0, y0, Math.max(0, x1 - x0), Math.max(0, y1 - y0));
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
ctx.disable = (cap) => {
|
|
215
|
+
if (cap === gl.SCISSOR_TEST) {
|
|
216
|
+
gl.scissor(bx, by, bw, bh);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
gl.disable(cap);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// WebGL's "null means the default framebuffer" — and this surface's
|
|
223
|
+
// default is the window's backing target, not GL's framebuffer zero,
|
|
224
|
+
// which here is the swapchain buffer the 2D never draws into. A scene
|
|
225
|
+
// that renders through its own FBO and unbinds at the end lands back on
|
|
226
|
+
// the window (the SSAA pattern, the shadow-map pattern).
|
|
227
|
+
ctx.bindFramebuffer = (target, fb) => {
|
|
228
|
+
gl.bindFramebuffer(
|
|
229
|
+
target,
|
|
230
|
+
fb == null ? (parent.glctx.backing?.fbo ?? null) : fb,
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// The frame is in the target already; what a swap means here is "this
|
|
235
|
+
// rect changed", for the present's damage and for the panes to be drawn
|
|
236
|
+
// over it again.
|
|
237
|
+
ctx.SwapBuffers = () => {
|
|
238
|
+
if (destroyed) return;
|
|
239
|
+
area.drewThisFrame = true;
|
|
240
|
+
parent._surfaceDrew(area);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
ctx._destroy = () => {
|
|
244
|
+
destroyed = true;
|
|
245
|
+
};
|
|
246
|
+
// `GlAreaNode.destroySubtree` calls `gl.destroy?.()` before the window's
|
|
247
|
+
ctx.destroy = ctx._destroy;
|
|
248
|
+
|
|
249
|
+
return ctx;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The pane a `<glarea>`'s children are drawn on: an offscreen target with a
|
|
254
|
+
* 2d context of its own, painted by the tree inside the window's flush and
|
|
255
|
+
* blended over the surface by the window's frame loop.
|
|
256
|
+
*
|
|
257
|
+
* It speaks the verbs the overlay drives (src/gloverlay.js `Pane`):
|
|
258
|
+
* `setState`, `map`, `unmap`, `getContext`, `present`, `destroy`. The
|
|
259
|
+
* context is handed out *inside a frame* — `begin()` has been called — so
|
|
260
|
+
* the overlay can draw at once, as it does on the other backends where a
|
|
261
|
+
* context has no frame; `present()` closes that frame and opens the next.
|
|
262
|
+
*/
|
|
263
|
+
export class WaylandOverlayPane {
|
|
264
|
+
/**
|
|
265
|
+
* @param {import('./app.js').WaylandApp} app
|
|
266
|
+
* @param {{parent:object,x?:number,y?:number,width?:number,height?:number}} options
|
|
267
|
+
*/
|
|
268
|
+
constructor(app, options) {
|
|
269
|
+
this.app = app;
|
|
270
|
+
this.parent = options.parent;
|
|
271
|
+
this.destroyed = false;
|
|
272
|
+
this.hidden = false;
|
|
273
|
+
/** something has been painted on it: before that there is nothing to composite */
|
|
274
|
+
this.presented = false;
|
|
275
|
+
this.rect = {
|
|
276
|
+
x: options.x ?? 0,
|
|
277
|
+
y: options.y ?? 0,
|
|
278
|
+
width: Math.max(1, options.width ?? 1),
|
|
279
|
+
height: Math.max(1, options.height ?? 1),
|
|
280
|
+
};
|
|
281
|
+
this.target = null;
|
|
282
|
+
this._ctx = null;
|
|
283
|
+
this.parent._panes.add(this);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
get width() {
|
|
287
|
+
return this.rect.width;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
get height() {
|
|
291
|
+
return this.rect.height;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Geometry in content device pixels. A new size is a new (cleared) target
|
|
295
|
+
* and the overlay repaints it whole (`Pane.place` says so). */
|
|
296
|
+
setState(rect) {
|
|
297
|
+
if (this.destroyed) return this;
|
|
298
|
+
const resized =
|
|
299
|
+
rect.width !== this.rect.width || rect.height !== this.rect.height;
|
|
300
|
+
this.rect = { ...rect };
|
|
301
|
+
if (resized && this.target) {
|
|
302
|
+
this.app.makeCurrent();
|
|
303
|
+
this.target.resize(this.rect.width, this.rect.height);
|
|
304
|
+
this.presented = false;
|
|
305
|
+
this._ctx?.begin(this.target.width, this.target.height);
|
|
306
|
+
this.app.rebindWindowTarget();
|
|
307
|
+
}
|
|
308
|
+
return this;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
map() {
|
|
312
|
+
this.hidden = false;
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
unmap() {
|
|
317
|
+
this.hidden = true;
|
|
318
|
+
return this;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** `Pane` listens for 'draw' — pixels lost to a resize. A target's are
|
|
322
|
+
* lost too, and `Pane.place` already repaints whole on a size change. */
|
|
323
|
+
on() {
|
|
324
|
+
return this;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
getContext(kind = '2d') {
|
|
328
|
+
if (kind !== '2d' || this.destroyed) return null;
|
|
329
|
+
if (!this._ctx) {
|
|
330
|
+
this.app.makeCurrent();
|
|
331
|
+
this.target = new GLTarget(this.app.gl, {
|
|
332
|
+
width: this.rect.width,
|
|
333
|
+
height: this.rect.height,
|
|
334
|
+
stencil: true,
|
|
335
|
+
});
|
|
336
|
+
this._ctx = new WaylandContext2D(this.app.gl, {
|
|
337
|
+
fontManager: this.app.fonts,
|
|
338
|
+
target: this.target,
|
|
339
|
+
});
|
|
340
|
+
this._ctx.init();
|
|
341
|
+
this._ctx.begin(this.target.width, this.target.height);
|
|
342
|
+
// the window's own context is mid-frame; its target goes back
|
|
343
|
+
this.app.rebindWindowTarget();
|
|
344
|
+
}
|
|
345
|
+
return this._ctx;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** What was painted is complete; the window blends it over the surface
|
|
349
|
+
* in this frame. The next paint finds the context open again. */
|
|
350
|
+
present() {
|
|
351
|
+
if (this.destroyed || !this._ctx) return;
|
|
352
|
+
this.app.makeCurrent();
|
|
353
|
+
this._ctx.end();
|
|
354
|
+
this.presented = true;
|
|
355
|
+
this.parent._paneDrew(this);
|
|
356
|
+
this._ctx.begin(this.target.width, this.target.height);
|
|
357
|
+
this.app.rebindWindowTarget();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
destroy() {
|
|
361
|
+
if (this.destroyed) return;
|
|
362
|
+
this.destroyed = true;
|
|
363
|
+
this.parent._panes.delete(this);
|
|
364
|
+
this.app.makeCurrent();
|
|
365
|
+
this._ctx?.destroy();
|
|
366
|
+
this._ctx = null;
|
|
367
|
+
this.target?.destroy();
|
|
368
|
+
this.target = null;
|
|
369
|
+
this.app.rebindWindowTarget();
|
|
370
|
+
}
|
|
371
|
+
}
|