react-x11 2.14.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 +1 -1
- package/src/glnodes.js +15 -3
- package/src/ntk.js +10 -1
- package/src/types/system.d.ts +14 -1
- package/src/wayland/app.js +14 -0
- package/src/wayland/backendwindow.js +86 -10
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +176 -19
- package/src/wayland/device.js +55 -0
- package/src/wayland/framewatch.js +190 -0
- package/src/wayland/glarea.js +1 -0
- package/src/wayland/protocols/index.json +4 -0
- package/src/wayland/protocols/kde-server-decoration.json +1 -0
- package/src/wayland/ssd.js +154 -26
- package/src/wayland/surface.js +18 -0
- package/src/wayland/target.js +6 -0
- package/src/wayland/window.js +36 -9
- package/src/windowstate.js +41 -1
|
@@ -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,190 @@
|
|
|
1
|
+
// Noticing that the compositor has stopped showing this surface.
|
|
2
|
+
//
|
|
3
|
+
// After the first present the frame loop has exactly one source of liveness:
|
|
4
|
+
// `wl_surface.frame`, which a compositor is entitled never to send to a
|
|
5
|
+
// surface it is not showing — occluded, on another workspace, minimised, the
|
|
6
|
+
// screen locked. That is the right behaviour to drive a client with (drawing
|
|
7
|
+
// into a surface nobody sees is work for nothing) and it has one hole: a
|
|
8
|
+
// callback that never fires is not an error. Nothing rejects, nothing throws,
|
|
9
|
+
// the frame stays armed, and every later `_armFrame()` returns at its guard.
|
|
10
|
+
// The app above goes on re-rendering into a tree that reaches no screen, and
|
|
11
|
+
// from outside the process that is indistinguishable from a hang (#567).
|
|
12
|
+
//
|
|
13
|
+
// So the loop is watched. A frame callback outstanding longer than
|
|
14
|
+
// `REACT_X11_WAYLAND_FRAME_STALL_MS` (2s; `0` turns the watch off) is taken
|
|
15
|
+
// as "the compositor has parked us", which is
|
|
16
|
+
//
|
|
17
|
+
// - **said once on stderr, in development**, when the window still had
|
|
18
|
+
// something to draw — the line that the debugging session behind #567
|
|
19
|
+
// would have ended at; and
|
|
20
|
+
// - **published**, as `useWindowState().presenting`, so an app can stop
|
|
21
|
+
// animating and simulating instead of feeding a loop nobody is turning.
|
|
22
|
+
//
|
|
23
|
+
// Nothing here paces or re-arms anything: the parked callback is still queued
|
|
24
|
+
// with the compositor and fires the moment the surface is shown again, which
|
|
25
|
+
// is what un-parks the loop and flips `presenting` back. A timer that painted
|
|
26
|
+
// anyway would be drawing into a surface nobody sees, which is the thing the
|
|
27
|
+
// frame callback is right about.
|
|
28
|
+
|
|
29
|
+
import { writeSync } from 'node:fs';
|
|
30
|
+
|
|
31
|
+
const DEV = process.env.NODE_ENV !== 'production';
|
|
32
|
+
|
|
33
|
+
/** How long a frame callback may be outstanding before the loop counts as
|
|
34
|
+
* parked, in milliseconds. `0` — or anything that is not a number — turns
|
|
35
|
+
* the watch off entirely: no timer, no warning, `presenting` always true. */
|
|
36
|
+
export const FRAME_STALL_MS = (() => {
|
|
37
|
+
const raw = process.env.REACT_X11_WAYLAND_FRAME_STALL_MS;
|
|
38
|
+
if (raw === undefined || raw === '') return 2000;
|
|
39
|
+
const ms = Number(raw);
|
|
40
|
+
return Number.isFinite(ms) && ms >= 0 ? ms : 0;
|
|
41
|
+
})();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The one line. It names the three things the symptom does not: that this is
|
|
45
|
+
* the compositor's decision and not a crash, that the window is the thing to
|
|
46
|
+
* look at, and where to read the state from in code.
|
|
47
|
+
*/
|
|
48
|
+
export function stallMessage(id, ms) {
|
|
49
|
+
return (
|
|
50
|
+
`react-x11 (wayland): window ${id} has had a frame callback outstanding ` +
|
|
51
|
+
`for ${(ms / 1000).toFixed(1)}s with a repaint waiting — the compositor ` +
|
|
52
|
+
'is not showing this surface (covered, on another workspace, minimised, ' +
|
|
53
|
+
'or the screen is locked), so nothing this app draws will reach the ' +
|
|
54
|
+
'screen until it does. The app is not hung. Read it in the tree as ' +
|
|
55
|
+
'useWindowState().presenting, which is false until the next frame ' +
|
|
56
|
+
'callback arrives. Said once per window; ' +
|
|
57
|
+
'REACT_X11_WAYLAND_FRAME_STALL_MS=0 turns it off.\n'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Synchronous, like the frame trace in backendwindow.js and for the same
|
|
62
|
+
// reason: Bun buffers `process.stderr` to a file, and a buffer is what a
|
|
63
|
+
// SIGINT leaves behind — which is exactly how a run that looks hung ends.
|
|
64
|
+
const toStderr = (message) => writeSync(2, message);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* One window's frame-callback liveness.
|
|
68
|
+
*
|
|
69
|
+
* Three calls from the frame loop, and they are the whole interface:
|
|
70
|
+
* `waiting()` when a `wl_surface.frame` has been asked for, `arrived()` when
|
|
71
|
+
* one comes back, `idle()` when the loop stops waiting for one at all (a
|
|
72
|
+
* rejected request, a closing connection). `presenting` is the answer, and
|
|
73
|
+
* `onChange` fires only when it turns over.
|
|
74
|
+
*
|
|
75
|
+
* The timers are injected so the policy above can be tested without a
|
|
76
|
+
* compositor, a GPU, or two seconds of wall clock.
|
|
77
|
+
*/
|
|
78
|
+
export class FrameWatch {
|
|
79
|
+
/**
|
|
80
|
+
* @param {object} opts
|
|
81
|
+
* @param {number|string} [opts.id] the window's id, for the message
|
|
82
|
+
* @param {number} [opts.stallMs] how long is too long
|
|
83
|
+
* @param {() => boolean} [opts.pending] is a repaint waiting? — only then
|
|
84
|
+
* is the silence worth a line on stderr
|
|
85
|
+
* @param {(presenting: boolean) => void} [opts.onChange]
|
|
86
|
+
* @param {(message: string) => void} [opts.warn]
|
|
87
|
+
* @param {boolean} [opts.dev] whether to warn at all
|
|
88
|
+
*/
|
|
89
|
+
constructor({
|
|
90
|
+
id = 0,
|
|
91
|
+
stallMs = FRAME_STALL_MS,
|
|
92
|
+
pending = () => false,
|
|
93
|
+
onChange = () => {},
|
|
94
|
+
warn = toStderr,
|
|
95
|
+
dev = DEV,
|
|
96
|
+
setTimer = (fn, ms) => {
|
|
97
|
+
const t = setTimeout(fn, ms);
|
|
98
|
+
// a diagnostic must never be the reason a process stays up
|
|
99
|
+
t.unref?.();
|
|
100
|
+
return t;
|
|
101
|
+
},
|
|
102
|
+
clearTimer = (t) => clearTimeout(t),
|
|
103
|
+
} = {}) {
|
|
104
|
+
this.id = id;
|
|
105
|
+
this.stallMs = stallMs;
|
|
106
|
+
this.presenting = true;
|
|
107
|
+
this._pending = pending;
|
|
108
|
+
this._onChange = onChange;
|
|
109
|
+
this._warn = warn;
|
|
110
|
+
this._dev = dev;
|
|
111
|
+
this._setTimer = setTimer;
|
|
112
|
+
this._clearTimer = clearTimer;
|
|
113
|
+
this._timer = null;
|
|
114
|
+
this._warned = false;
|
|
115
|
+
this._stopped = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A frame callback has been asked for. */
|
|
119
|
+
waiting() {
|
|
120
|
+
// An already-running timer is left alone: it measures the age of the
|
|
121
|
+
// oldest outstanding request, which is the one that matters.
|
|
122
|
+
if (this._stopped || this._timer || !(this.stallMs > 0)) return;
|
|
123
|
+
this._timer = this._setTimer(() => {
|
|
124
|
+
this._timer = null;
|
|
125
|
+
this._stall();
|
|
126
|
+
}, this.stallMs);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A frame callback arrived: the compositor is showing this surface. */
|
|
130
|
+
arrived() {
|
|
131
|
+
this._disarm();
|
|
132
|
+
this._publish(true);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Nothing is outstanding — a rejected request, a frame that drew nothing
|
|
136
|
+
* and asked for no other. Says nothing about whether we are being shown. */
|
|
137
|
+
idle() {
|
|
138
|
+
this._disarm();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A repaint was asked for while a frame was already in flight.
|
|
143
|
+
*
|
|
144
|
+
* There is nothing to arm — the flight is armed, and the request will ride
|
|
145
|
+
* it — but if that flight is the compositor's silence then this is the
|
|
146
|
+
* moment the app starts drawing for nobody, and it is the other order the
|
|
147
|
+
* bug arrives in: a window can park while it has nothing to draw (nothing
|
|
148
|
+
* to warn about) and be given work a minute later, by which time the
|
|
149
|
+
* deadline has come and gone.
|
|
150
|
+
*/
|
|
151
|
+
workArrived() {
|
|
152
|
+
if (this._stopped || this.presenting) return;
|
|
153
|
+
this._maybeWarn();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The window is going away. */
|
|
157
|
+
stop() {
|
|
158
|
+
this._stopped = true;
|
|
159
|
+
this._disarm();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_disarm() {
|
|
163
|
+
if (this._timer === null) return;
|
|
164
|
+
this._clearTimer(this._timer);
|
|
165
|
+
this._timer = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_stall() {
|
|
169
|
+
if (this._stopped) return;
|
|
170
|
+
this._maybeWarn();
|
|
171
|
+
this._publish(false);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The line, once per window, and only where the app had something to draw:
|
|
176
|
+
* a window that is simply idle is not being let down by anyone, and a
|
|
177
|
+
* warning there would fire on every app that comes to rest.
|
|
178
|
+
*/
|
|
179
|
+
_maybeWarn() {
|
|
180
|
+
if (!this._dev || this._warned || !this._pending()) return;
|
|
181
|
+
this._warned = true;
|
|
182
|
+
this._warn(stallMessage(this.id, this.stallMs));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_publish(presenting) {
|
|
186
|
+
if (this.presenting === presenting) return;
|
|
187
|
+
this.presenting = presenting;
|
|
188
|
+
this._onChange(presenting);
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/wayland/glarea.js
CHANGED
|
@@ -336,6 +336,7 @@ export class WaylandOverlayPane {
|
|
|
336
336
|
this._ctx = new WaylandContext2D(this.app.gl, {
|
|
337
337
|
fontManager: this.app.fonts,
|
|
338
338
|
target: this.target,
|
|
339
|
+
makeCurrent: () => this.app.makeCurrent(),
|
|
339
340
|
});
|
|
340
341
|
this._ctx.init();
|
|
341
342
|
this._ctx.begin(this.target.width, this.target.height);
|
|
@@ -78,6 +78,10 @@
|
|
|
78
78
|
"zxdg_decoration_manager_v1",
|
|
79
79
|
"zxdg_toplevel_decoration_v1"
|
|
80
80
|
],
|
|
81
|
+
"kde-server-decoration": [
|
|
82
|
+
"org_kde_kwin_server_decoration_manager",
|
|
83
|
+
"org_kde_kwin_server_decoration"
|
|
84
|
+
],
|
|
81
85
|
"primary-selection-unstable-v1": [
|
|
82
86
|
"zwp_primary_selection_device_manager_v1",
|
|
83
87
|
"zwp_primary_selection_device_v1",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"name":"org_kde_kwin_server_decoration_manager","version":1,"requests":[{"name":"create","args":[{"name":"id","type":"new_id","interface":"org_kde_kwin_server_decoration"},{"name":"surface","type":"object","interface":"wl_surface"}]}],"events":[{"name":"default_mode","args":[{"name":"mode","type":"uint"}]}],"enums":{"mode":{"entries":[{"name":"none","value":0},{"name":"client","value":1},{"name":"server","value":2}]}}},{"name":"org_kde_kwin_server_decoration","version":1,"requests":[{"name":"release","type":"destructor","args":[]},{"name":"request_mode","args":[{"name":"mode","type":"uint"}]}],"events":[{"name":"mode","args":[{"name":"mode","type":"uint"}]}],"enums":{"mode":{"entries":[{"name":"none","value":0},{"name":"client","value":1},{"name":"server","value":2}]}}}]
|
package/src/wayland/ssd.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Server-side decorations,
|
|
1
|
+
// Server-side decorations: asking the compositor to draw the frame, and
|
|
2
|
+
// falling back to drawing it ourselves only when nothing will.
|
|
2
3
|
//
|
|
3
4
|
// The compositor may draw the frame — wlroots compositors and KDE do, when
|
|
4
5
|
// asked, and GNOME does not advertise the protocol at all — so "who draws the
|
|
@@ -11,6 +12,25 @@
|
|
|
11
12
|
// client_side — which is the state the backend was already in, so
|
|
12
13
|
// `decorations.js` stays and is switched off rather than removed.
|
|
13
14
|
//
|
|
15
|
+
// ## Two protocols say the same thing
|
|
16
|
+
//
|
|
17
|
+
// `xdg-decoration-unstable-v1` is the one to ask with, and where it is
|
|
18
|
+
// offered it is the only one used. But it arrived late (2018) and KDE had
|
|
19
|
+
// shipped `org_kde_kwin_server_decoration` years before, so a compositor may
|
|
20
|
+
// advertise only the older one — KWin still carries both, and a handful of
|
|
21
|
+
// smaller compositors never picked up the xdg protocol. Asking with whatever
|
|
22
|
+
// is there is the difference between a real titlebar and an imitation, so
|
|
23
|
+
// `createServerDecoration` tries them in that order and only gives up on
|
|
24
|
+
// server-side when neither global exists.
|
|
25
|
+
//
|
|
26
|
+
// The KDE protocol is the same conversation in different words: it hangs off
|
|
27
|
+
// the `wl_surface` rather than the `xdg_toplevel`, its enum has a third value
|
|
28
|
+
// (`none`, no frame at all), its destructor is `release`, and — the one
|
|
29
|
+
// difference that matters — its `mode` event is not part of a configure
|
|
30
|
+
// sequence. Nothing promises a configure will follow it, so that driver
|
|
31
|
+
// flushes itself (`flushesOutsideConfigure`); the xdg one keeps the stricter
|
|
32
|
+
// discipline below.
|
|
33
|
+
//
|
|
14
34
|
// The mode is adopted, not applied on arrival, for the reason the configure
|
|
15
35
|
// ack is deferred (window.js): a mode change is a size change — the surface
|
|
16
36
|
// loses or gains its own titlebar — and the frame that adopts it has to be
|
|
@@ -22,6 +42,15 @@ import { EventEmitter } from 'node:events';
|
|
|
22
42
|
/** `zxdg_toplevel_decoration_v1.mode`. */
|
|
23
43
|
export const DECORATION_MODE = { CLIENT_SIDE: 1, SERVER_SIDE: 2 };
|
|
24
44
|
|
|
45
|
+
/**
|
|
46
|
+
* `org_kde_kwin_server_decoration.mode`. The two that overlap with
|
|
47
|
+
* xdg-decoration share its numbering; `NONE` — no frame from either side —
|
|
48
|
+
* has no xdg equivalent and is only ever received, never asked for. A window
|
|
49
|
+
* that wants no frame asks for `CLIENT` and then draws nothing, which is the
|
|
50
|
+
* same picture and one less thing for `decorationPolicy` to express.
|
|
51
|
+
*/
|
|
52
|
+
export const KDE_DECORATION_MODE = { NONE: 0, CLIENT: 1, SERVER: 2 };
|
|
53
|
+
|
|
25
54
|
/**
|
|
26
55
|
* What `decorations` means, at the app (`createRoot({ decorations })`) and
|
|
27
56
|
* per window (`<window decorations={false}>`, the one value the tree passes):
|
|
@@ -45,7 +74,63 @@ export function decorationPolicy(appOption, windowOption) {
|
|
|
45
74
|
return { draw, prefer };
|
|
46
75
|
}
|
|
47
76
|
|
|
48
|
-
|
|
77
|
+
/**
|
|
78
|
+
* What both protocols have in common: a mode that arrives from the
|
|
79
|
+
* compositor, is held until the frame that can paint it adopts it, and an
|
|
80
|
+
* object that has to be destroyed before the thing it decorates.
|
|
81
|
+
*/
|
|
82
|
+
class Decoration extends EventEmitter {
|
|
83
|
+
constructor() {
|
|
84
|
+
super();
|
|
85
|
+
/** the mode in effect: 'server' | 'client', null before the first answer */
|
|
86
|
+
this.mode = null;
|
|
87
|
+
/** a mode the compositor sent that the next configure adopts */
|
|
88
|
+
this.pending = null;
|
|
89
|
+
this.destroyed = false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether an answer can arrive with no configure behind it, in which case
|
|
94
|
+
* whoever owns this has to flush it rather than wait (window.js). False
|
|
95
|
+
* for xdg-decoration, whose mode is part of the configure sequence.
|
|
96
|
+
*/
|
|
97
|
+
get flushesOutsideConfigure() {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The compositor's answer, held until `adopt()`. */
|
|
102
|
+
_answer(mode) {
|
|
103
|
+
this.pending = mode;
|
|
104
|
+
if (this.flushesOutsideConfigure) this.emit('pending');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Adopt the pending mode, if there is one.
|
|
109
|
+
*
|
|
110
|
+
* @returns {boolean} whether the mode in effect changed
|
|
111
|
+
*/
|
|
112
|
+
adopt() {
|
|
113
|
+
if (this.pending == null) return false;
|
|
114
|
+
const changed = this.pending !== this.mode;
|
|
115
|
+
this.mode = this.pending;
|
|
116
|
+
this.pending = null;
|
|
117
|
+
if (changed) this.emit('mode', this.mode);
|
|
118
|
+
return changed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Before the toplevel: destroying them the other way round is a protocol error. */
|
|
122
|
+
destroy() {
|
|
123
|
+
if (this.destroyed) return;
|
|
124
|
+
this.destroyed = true;
|
|
125
|
+
try {
|
|
126
|
+
this._release();
|
|
127
|
+
} catch {
|
|
128
|
+
/* the connection may already be gone */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class ServerDecoration extends Decoration {
|
|
49
134
|
/**
|
|
50
135
|
* Create the decoration object and state a preference. Must run before
|
|
51
136
|
* the toplevel's first commit; `WaylandWindow.createSync` calls it there.
|
|
@@ -58,13 +143,8 @@ export class ServerDecoration extends EventEmitter {
|
|
|
58
143
|
constructor({ manager, toplevel, prefer = 'server' }) {
|
|
59
144
|
super();
|
|
60
145
|
this.proxy = manager.$.get_toplevel_decoration(toplevel.id);
|
|
61
|
-
/** the mode in effect: 'server' | 'client', null before the first configure */
|
|
62
|
-
this.mode = null;
|
|
63
|
-
/** a mode the compositor sent that the next xdg_surface.configure adopts */
|
|
64
|
-
this.pending = null;
|
|
65
|
-
this.destroyed = false;
|
|
66
146
|
this.proxy.on('configure', (mode) => {
|
|
67
|
-
this.
|
|
147
|
+
this._answer(mode === DECORATION_MODE.SERVER_SIDE ? 'server' : 'client');
|
|
68
148
|
});
|
|
69
149
|
this.prefer(prefer);
|
|
70
150
|
}
|
|
@@ -79,28 +159,76 @@ export class ServerDecoration extends EventEmitter {
|
|
|
79
159
|
);
|
|
80
160
|
}
|
|
81
161
|
|
|
162
|
+
_release() {
|
|
163
|
+
this.proxy.$.destroy();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The same conversation over `org_kde_kwin_server_decoration`, for the
|
|
169
|
+
* compositors that have only that one. It decorates the `wl_surface`, so it
|
|
170
|
+
* is created from the surface rather than the toplevel — but the ordering
|
|
171
|
+
* rule is the toplevel's all the same, since that is what carries the frame.
|
|
172
|
+
*/
|
|
173
|
+
export class KdeServerDecoration extends Decoration {
|
|
82
174
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* @
|
|
175
|
+
* @param {object} opts
|
|
176
|
+
* @param {object} opts.manager the `org_kde_kwin_server_decoration_manager`
|
|
177
|
+
* @param {object} opts.surface the `wl_surface` proxy
|
|
178
|
+
* @param {'server'|'client'} [opts.prefer='server']
|
|
86
179
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this.mode
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
180
|
+
constructor({ manager, surface, prefer = 'server' }) {
|
|
181
|
+
super();
|
|
182
|
+
this.proxy = manager.$.create(surface.id);
|
|
183
|
+
this.proxy.on('mode', (mode) => {
|
|
184
|
+
// `none` is not server-side, and this side draws nothing for it: the
|
|
185
|
+
// window that asked for no frame is already drawing nothing.
|
|
186
|
+
this._answer(mode === KDE_DECORATION_MODE.SERVER ? 'server' : 'client');
|
|
187
|
+
});
|
|
188
|
+
this.prefer(prefer);
|
|
94
189
|
}
|
|
95
190
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
191
|
+
/** Nothing sequences this protocol's `mode` event; window.js flushes it. */
|
|
192
|
+
get flushesOutsideConfigure() {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
prefer(prefer) {
|
|
98
197
|
if (this.destroyed) return;
|
|
99
|
-
this.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
198
|
+
this.proxy.$.request_mode(
|
|
199
|
+
prefer === 'client'
|
|
200
|
+
? KDE_DECORATION_MODE.CLIENT
|
|
201
|
+
: KDE_DECORATION_MODE.SERVER,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_release() {
|
|
206
|
+
this.proxy.$.release();
|
|
105
207
|
}
|
|
106
208
|
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Ask whichever protocol the compositor has, preferring the standard one.
|
|
212
|
+
* Returns null only when there is nobody to ask — the case where the frame
|
|
213
|
+
* has to be this backend's own (GNOME).
|
|
214
|
+
*
|
|
215
|
+
* @param {object} opts
|
|
216
|
+
* @param {object} [opts.manager] `zxdg_decoration_manager_v1`, if bound
|
|
217
|
+
* @param {object} [opts.kdeManager] `org_kde_kwin_server_decoration_manager`
|
|
218
|
+
* @param {object} opts.toplevel the `xdg_toplevel` proxy
|
|
219
|
+
* @param {object} opts.surface the `wl_surface` proxy
|
|
220
|
+
* @param {'server'|'client'} [opts.prefer='server']
|
|
221
|
+
* @returns {Decoration|null}
|
|
222
|
+
*/
|
|
223
|
+
export function createServerDecoration({
|
|
224
|
+
manager,
|
|
225
|
+
kdeManager,
|
|
226
|
+
toplevel,
|
|
227
|
+
surface,
|
|
228
|
+
prefer = 'server',
|
|
229
|
+
}) {
|
|
230
|
+
if (manager) return new ServerDecoration({ manager, toplevel, prefer });
|
|
231
|
+
if (kdeManager)
|
|
232
|
+
return new KdeServerDecoration({ manager: kdeManager, surface, prefer });
|
|
233
|
+
return null;
|
|
234
|
+
}
|
package/src/wayland/surface.js
CHANGED
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
// only one surface is current at a time. Every target here shares the app's
|
|
14
14
|
// one EGL context, so `render()` asks the app to make it current before
|
|
15
15
|
// binding the target — which normally costs nothing, because it already is.
|
|
16
|
+
//
|
|
17
|
+
// Sharing one device is also why `getContext('2d')` used to be a trap. ntk
|
|
18
|
+
// tells callers who draw often to hold a context rather than take one per
|
|
19
|
+
// frame, and a held context here drew wherever the device was last pointed
|
|
20
|
+
// — which, outside `render()`, was the window: the draws went nowhere
|
|
21
|
+
// visible and nothing said so (#566). The context now takes the device back
|
|
22
|
+
// whenever it finds it in someone else's hands (device.js), so holding one
|
|
23
|
+
// works as ntk documents, and a node that painted into a surface in the
|
|
24
|
+
// middle of the window's frame no longer has to put the window's GL state
|
|
25
|
+
// back by hand either.
|
|
16
26
|
|
|
17
27
|
import { WaylandContext2D } from './context2d.js';
|
|
18
28
|
import { GLTarget } from './target.js';
|
|
@@ -57,6 +67,10 @@ export class WaylandSurface {
|
|
|
57
67
|
this._ctx = new WaylandContext2D(this.app.gl, {
|
|
58
68
|
fontManager: this.app.fonts,
|
|
59
69
|
target: this.target,
|
|
70
|
+
// A surface may be drawn into between frames, when the app's notion
|
|
71
|
+
// of a current GL surface is whatever it last was; the context asks
|
|
72
|
+
// for one before it re-takes the device, as `render()` does.
|
|
73
|
+
makeCurrent: () => this.app.makeCurrent(),
|
|
60
74
|
});
|
|
61
75
|
this.app.makeCurrent();
|
|
62
76
|
this._ctx.init();
|
|
@@ -67,6 +81,10 @@ export class WaylandSurface {
|
|
|
67
81
|
/**
|
|
68
82
|
* Draw into the surface through a context that starts clean — identity
|
|
69
83
|
* transform, no clip — and is flushed when the callback returns.
|
|
84
|
+
*
|
|
85
|
+
* Drawing through `getContext('2d')` directly works too, and keeps the
|
|
86
|
+
* transform and clip it was left with; this is the scoped form, for a
|
|
87
|
+
* caller that wants a frame's worth of state rather than a canvas's.
|
|
70
88
|
*/
|
|
71
89
|
render(fn) {
|
|
72
90
|
if (this._destroyed) return this;
|
package/src/wayland/target.js
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
// asking EGL for it means the window's own config no longer has to carry
|
|
22
22
|
// stencil bits at all; the request in glcontext.js stays as belt and braces.
|
|
23
23
|
|
|
24
|
+
import { releaseDevice } from './device.js';
|
|
25
|
+
|
|
24
26
|
export class GLTarget {
|
|
25
27
|
/**
|
|
26
28
|
* @param {object} gl the GLES entry points
|
|
@@ -164,6 +166,9 @@ export class GLTarget {
|
|
|
164
166
|
);
|
|
165
167
|
}
|
|
166
168
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
169
|
+
// The framebuffer and the scissor were the 2d contexts' to say; they are
|
|
170
|
+
// not any more, so the next one to draw re-establishes them (device.js).
|
|
171
|
+
releaseDevice(gl);
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
/**
|
|
@@ -229,6 +234,7 @@ export class GLTarget {
|
|
|
229
234
|
gl.NEAREST,
|
|
230
235
|
);
|
|
231
236
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
237
|
+
releaseDevice(gl);
|
|
232
238
|
return true;
|
|
233
239
|
}
|
|
234
240
|
|
package/src/wayland/window.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
// is emitted, so the frame that acks it is painted at the right insets.
|
|
40
40
|
|
|
41
41
|
import { EventEmitter } from 'node:events';
|
|
42
|
-
import {
|
|
42
|
+
import { createServerDecoration } from './ssd.js';
|
|
43
43
|
import { createLayerSurface } from './layershell.js';
|
|
44
44
|
|
|
45
45
|
/** `xdg_toplevel.state` values. */
|
|
@@ -264,14 +264,17 @@ export class WaylandWindow extends EventEmitter {
|
|
|
264
264
|
if (minSize) toplevel.$.set_min_size(minSize.width | 0, minSize.height | 0);
|
|
265
265
|
if (maxSize) toplevel.$.set_max_size(maxSize.width | 0, maxSize.height | 0);
|
|
266
266
|
// Who draws the frame is asked before the first commit; the answer rides
|
|
267
|
-
// the first configure (ssd.js).
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
267
|
+
// the first configure (ssd.js). Whichever protocol the compositor has is
|
|
268
|
+
// asked — the standard one first — and only a compositor with neither
|
|
269
|
+
// leaves the frame to this backend.
|
|
270
|
+
win.decoration = createServerDecoration({
|
|
271
|
+
manager: decorations?.manager,
|
|
272
|
+
kdeManager: decorations?.kdeManager,
|
|
273
|
+
toplevel,
|
|
274
|
+
surface,
|
|
275
|
+
prefer: decorations?.prefer,
|
|
276
|
+
});
|
|
277
|
+
win._wireDecoration();
|
|
275
278
|
// the empty commit that asks for the first configure
|
|
276
279
|
surface.$.commit();
|
|
277
280
|
return win;
|
|
@@ -406,6 +409,30 @@ export class WaylandWindow extends EventEmitter {
|
|
|
406
409
|
this.surface.on('preferred_buffer_transform', () => {});
|
|
407
410
|
}
|
|
408
411
|
|
|
412
|
+
/**
|
|
413
|
+
* The KDE protocol's `mode` event is not part of a configure sequence
|
|
414
|
+
* (ssd.js), so a change of heart after the window is up would otherwise
|
|
415
|
+
* sit in `pending` waiting for a configure that nothing promises. Flush it
|
|
416
|
+
* in a microtask: a configure in the same batch of messages still gets
|
|
417
|
+
* there first and adopts it the ordinary way, and only a mode that really
|
|
418
|
+
* arrived alone is applied on its own — `setMargins` already knows how to
|
|
419
|
+
* resize outside a configure.
|
|
420
|
+
*
|
|
421
|
+
* Before the first configure there is nothing to flush: that configure is
|
|
422
|
+
* coming, and riding it keeps the first frame at the right insets.
|
|
423
|
+
*/
|
|
424
|
+
_wireDecoration() {
|
|
425
|
+
const decoration = this.decoration;
|
|
426
|
+
if (!decoration?.flushesOutsideConfigure) return;
|
|
427
|
+
decoration.on('pending', () => {
|
|
428
|
+
if (!this.configured) return;
|
|
429
|
+
queueMicrotask(() => {
|
|
430
|
+
if (this.destroyed) return;
|
|
431
|
+
if (decoration.adopt()) this.emit('decorationmode', decoration.mode);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
409
436
|
/**
|
|
410
437
|
* The shell's configure — xdg_surface's, or the layer surface's with its
|
|
411
438
|
* size already taken — holds the serial for the frame that adopts it. A
|