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,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,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
|
+
}
|