react-x11 1.2.0 → 2.0.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/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// What the window manager has done with a window, as something a component
|
|
2
|
+
// re-renders on.
|
|
3
|
+
//
|
|
4
|
+
// A `<window>` asks for `fullscreen` or `maximized` and the window manager
|
|
5
|
+
// decides — and it decides on its own account too, when the user hits a
|
|
6
|
+
// hotkey or the titlebar button. `nodes.js` sends the requests; this is the
|
|
7
|
+
// half that reads back what actually happened, plus the two things an app
|
|
8
|
+
// needs that are not states at all: whether it has the keyboard, and whether
|
|
9
|
+
// anyone can see it.
|
|
10
|
+
//
|
|
11
|
+
// ## Where each field comes from
|
|
12
|
+
//
|
|
13
|
+
// | | |
|
|
14
|
+
// | --- | --- |
|
|
15
|
+
// | `focused` | FocusIn/FocusOut, via the event manager that already tracks it |
|
|
16
|
+
// | `minimized` `maximized` `fullscreen` `states` | `_NET_WM_STATE`, as ntk's `statechange` event |
|
|
17
|
+
// | `obscured` | VisibilityNotify |
|
|
18
|
+
// | `desktop` | `_NET_WM_DESKTOP` |
|
|
19
|
+
//
|
|
20
|
+
// Nothing here is asked for until something subscribes. `_NET_WM_STATE`
|
|
21
|
+
// costs a PropertyChange selection and a round trip per change,
|
|
22
|
+
// VisibilityNotify costs a mask bit, and an app that never asks pays for
|
|
23
|
+
// neither.
|
|
24
|
+
//
|
|
25
|
+
// ## The compositor caveat, because it will otherwise be found as a bug
|
|
26
|
+
//
|
|
27
|
+
// **`obscured` is always false when a compositing manager is running**, and
|
|
28
|
+
// one is running on every stock GNOME and KDE session. A composited window
|
|
29
|
+
// is redirected to an offscreen pixmap, so the server considers it entirely
|
|
30
|
+
// visible whatever is stacked on top of it, and VisibilityNotify says so.
|
|
31
|
+
// That is X working as designed and there is no protocol answer to it.
|
|
32
|
+
//
|
|
33
|
+
// So `obscured` is the bare-WM optimisation it looks like, and `visible` —
|
|
34
|
+
// which folds in `minimized`, the signal that *does* survive compositing —
|
|
35
|
+
// is the field to branch on. An animation paused on `visible` stops when the
|
|
36
|
+
// window is minimized everywhere, and additionally when it is buried on a
|
|
37
|
+
// desktop with no compositor.
|
|
38
|
+
|
|
39
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
40
|
+
|
|
41
|
+
import { useAppOrNull } from './appcontext.js';
|
|
42
|
+
import { useTopLevelWindow, windowIdOf } from './windowid.js';
|
|
43
|
+
|
|
44
|
+
const VISIBILITY_NOTIFY = 15;
|
|
45
|
+
const VISIBILITY_CHANGE_MASK = 65536; // x11.eventMask.VisibilityChange
|
|
46
|
+
const FULLY_OBSCURED = 2;
|
|
47
|
+
const DESKTOP_PROPERTY = '_NET_WM_DESKTOP';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What is true before the window manager has said anything.
|
|
51
|
+
*
|
|
52
|
+
* `focused` and `visible` start **true**, and that is the whole reason this
|
|
53
|
+
* constant is written out rather than being a pile of falses: a window opens
|
|
54
|
+
* focused and on screen far more often than not, and a title bar that
|
|
55
|
+
* renders dimmed on the first frame and un-dims on the second is a visible
|
|
56
|
+
* flash on every launch. Starting from the common case makes the first paint
|
|
57
|
+
* right and the correction — for the rarer window that opened in the
|
|
58
|
+
* background — the thing that costs a re-render.
|
|
59
|
+
*/
|
|
60
|
+
const UNKNOWN = Object.freeze({
|
|
61
|
+
focused: true,
|
|
62
|
+
visible: true,
|
|
63
|
+
obscured: false,
|
|
64
|
+
minimized: false,
|
|
65
|
+
maximized: false,
|
|
66
|
+
fullscreen: false,
|
|
67
|
+
states: Object.freeze([]),
|
|
68
|
+
desktop: null,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const sessions = new WeakMap();
|
|
72
|
+
|
|
73
|
+
const SAME = (a, b) =>
|
|
74
|
+
a.focused === b.focused &&
|
|
75
|
+
a.visible === b.visible &&
|
|
76
|
+
a.obscured === b.obscured &&
|
|
77
|
+
a.minimized === b.minimized &&
|
|
78
|
+
a.maximized === b.maximized &&
|
|
79
|
+
a.fullscreen === b.fullscreen &&
|
|
80
|
+
a.desktop === b.desktop &&
|
|
81
|
+
a.states.length === b.states.length &&
|
|
82
|
+
a.states.every((s, i) => s === b.states[i]);
|
|
83
|
+
|
|
84
|
+
class WindowStateSession {
|
|
85
|
+
constructor(node) {
|
|
86
|
+
this.node = node;
|
|
87
|
+
this.snapshot = UNKNOWN;
|
|
88
|
+
this.listeners = new Set();
|
|
89
|
+
this.armed = false;
|
|
90
|
+
this.stopped = false;
|
|
91
|
+
this._desktopAtom = null;
|
|
92
|
+
this._offs = [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fold new fields into the snapshot and notify. A change that changes
|
|
97
|
+
* nothing notifies nobody: `statechange` fires on every `_NET_WM_STATE`
|
|
98
|
+
* rewrite, and window managers rewrite it with the same contents more
|
|
99
|
+
* often than they change it.
|
|
100
|
+
*/
|
|
101
|
+
publish(values) {
|
|
102
|
+
const states = values.states ?? this.snapshot.states;
|
|
103
|
+
const next = Object.freeze({
|
|
104
|
+
...this.snapshot,
|
|
105
|
+
...values,
|
|
106
|
+
states: Object.freeze([...states]),
|
|
107
|
+
// Derived rather than reported, in one place, so `visible` cannot
|
|
108
|
+
// disagree with the fields it is made of.
|
|
109
|
+
minimized: values.states
|
|
110
|
+
? states.includes('hidden')
|
|
111
|
+
: this.snapshot.minimized,
|
|
112
|
+
maximized: values.states
|
|
113
|
+
? states.includes('maximized_vert') && states.includes('maximized_horz')
|
|
114
|
+
: this.snapshot.maximized,
|
|
115
|
+
fullscreen: values.states
|
|
116
|
+
? states.includes('fullscreen')
|
|
117
|
+
: this.snapshot.fullscreen,
|
|
118
|
+
});
|
|
119
|
+
const settled = Object.freeze({
|
|
120
|
+
...next,
|
|
121
|
+
visible: !next.minimized && !next.obscured,
|
|
122
|
+
});
|
|
123
|
+
if (SAME(settled, this.snapshot)) return;
|
|
124
|
+
this.snapshot = settled;
|
|
125
|
+
for (const fn of [...this.listeners]) {
|
|
126
|
+
try {
|
|
127
|
+
fn();
|
|
128
|
+
} catch {
|
|
129
|
+
// one subscriber throwing must not take the others with it, nor the
|
|
130
|
+
// X event loop this runs on
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
stop() {
|
|
136
|
+
this.stopped = true;
|
|
137
|
+
for (const off of this._offs) {
|
|
138
|
+
try {
|
|
139
|
+
off();
|
|
140
|
+
} catch {
|
|
141
|
+
// a window already destroyed takes its listeners with it
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this._offs.length = 0;
|
|
145
|
+
this.listeners.clear();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The `WindowNode` behind a ref, a node, an ntk window or an XID.
|
|
151
|
+
*
|
|
152
|
+
* Popups are **not** excluded, unlike `topLevelWindows()`: a `<popup>` is
|
|
153
|
+
* override-redirect so the window manager never gives it a `_NET_WM_STATE`,
|
|
154
|
+
* and reporting the defaults for one is a truer answer than refusing to
|
|
155
|
+
* resolve it.
|
|
156
|
+
*/
|
|
157
|
+
function nodeOf(app, target) {
|
|
158
|
+
const value =
|
|
159
|
+
target &&
|
|
160
|
+
typeof target === 'object' &&
|
|
161
|
+
'current' in target &&
|
|
162
|
+
!target.isWindow
|
|
163
|
+
? target.current
|
|
164
|
+
: target;
|
|
165
|
+
if (!value) return null;
|
|
166
|
+
if (value.isWindow && value.window) return value;
|
|
167
|
+
const id = windowIdOf(value);
|
|
168
|
+
if (id == null) return null;
|
|
169
|
+
return (
|
|
170
|
+
(app?._rootChildren ?? []).find(
|
|
171
|
+
(node) => node?.isWindow && node.window?.id === id,
|
|
172
|
+
) ?? null
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sessionFor(node) {
|
|
177
|
+
let session = sessions.get(node);
|
|
178
|
+
if (!session) {
|
|
179
|
+
session = new WindowStateSession(node);
|
|
180
|
+
sessions.set(node, session);
|
|
181
|
+
}
|
|
182
|
+
return session;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Start listening on the X side. Once per window: the mask bits and the
|
|
187
|
+
* PropertyChange selection cannot be given back when the last subscriber
|
|
188
|
+
* leaves, and re-arming on the next one would cost another round trip for a
|
|
189
|
+
* selection the server still has.
|
|
190
|
+
*/
|
|
191
|
+
function arm(session) {
|
|
192
|
+
if (session.armed) return;
|
|
193
|
+
const node = session.node;
|
|
194
|
+
const wnd = node.window;
|
|
195
|
+
// Not armed yet, rather than armed against nothing: a window realizes
|
|
196
|
+
// during the commit that created it, so a subscription that got here first
|
|
197
|
+
// gets another chance on the next one.
|
|
198
|
+
if (!wnd) return;
|
|
199
|
+
session.armed = true;
|
|
200
|
+
|
|
201
|
+
// Focus, from the event manager rather than from ntk directly: it already
|
|
202
|
+
// dedups FocusIn/FocusOut and already knows the answer for a window that
|
|
203
|
+
// has not seen either yet.
|
|
204
|
+
if (typeof node.onWindowFocusChange === 'function') {
|
|
205
|
+
session._offs.push(
|
|
206
|
+
node.onWindowFocusChange((focused) => {
|
|
207
|
+
if (!session.stopped) session.publish({ focused });
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
const now = node.events?.windowFocused;
|
|
211
|
+
if (typeof now === 'boolean') session.publish({ focused: now });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// _NET_WM_STATE. Adding the listener is what makes ntk select
|
|
215
|
+
// PropertyChange and intern the atom, so the read below is only for the
|
|
216
|
+
// state the window already had when this ran.
|
|
217
|
+
if (typeof wnd.on === 'function') {
|
|
218
|
+
const onState = (states) => {
|
|
219
|
+
if (!session.stopped) session.publish({ states: states ?? [] });
|
|
220
|
+
};
|
|
221
|
+
wnd.on('statechange', onState);
|
|
222
|
+
session._offs.push(() => wnd.off?.('statechange', onState));
|
|
223
|
+
Promise.resolve(wnd.getWmStates?.()).then(
|
|
224
|
+
(states) => states && !session.stopped && session.publish({ states }),
|
|
225
|
+
() => {},
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// _NET_WM_DESKTOP rides on the PropertyChange selection the line above
|
|
229
|
+
// already asked for, so watching it costs one comparison per property
|
|
230
|
+
// change rather than a second selection.
|
|
231
|
+
const onProperty = (ev) => {
|
|
232
|
+
if (session.stopped || ev?.atom == null) return;
|
|
233
|
+
if (ev.atom !== session._desktopAtom) return;
|
|
234
|
+
readDesktop(session);
|
|
235
|
+
};
|
|
236
|
+
wnd.on('property', onProperty);
|
|
237
|
+
session._offs.push(() => wnd.off?.('property', onProperty));
|
|
238
|
+
Promise.resolve(wnd.atom?.(DESKTOP_PROPERTY)).then(
|
|
239
|
+
(atom) => {
|
|
240
|
+
session._desktopAtom = atom;
|
|
241
|
+
if (!session.stopped) readDesktop(session);
|
|
242
|
+
},
|
|
243
|
+
() => {},
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
watchVisibility(session);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function readDesktop(session) {
|
|
251
|
+
Promise.resolve(
|
|
252
|
+
session.node.window?.getProperty?.(DESKTOP_PROPERTY, { as: 'numbers' }),
|
|
253
|
+
).then(
|
|
254
|
+
(values) => {
|
|
255
|
+
if (session.stopped) return;
|
|
256
|
+
const desktop = values?.[0];
|
|
257
|
+
// 0xffffffff is EWMH for "on every desktop", which `states` already
|
|
258
|
+
// reports as `sticky` — a workspace number is the wrong shape for it.
|
|
259
|
+
session.publish({
|
|
260
|
+
desktop:
|
|
261
|
+
typeof desktop === 'number' && desktop !== 0xffffffff
|
|
262
|
+
? desktop
|
|
263
|
+
: null,
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
() => {},
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* VisibilityNotify, which ntk has no event name for — its mask table stops
|
|
272
|
+
* at the events a widget toolkit needs — so the mask goes on through
|
|
273
|
+
* `selectInput` (which ORs into ntk's own tracked mask, so nothing it adds
|
|
274
|
+
* later drops this) and the event is read off the raw connection.
|
|
275
|
+
*/
|
|
276
|
+
function watchVisibility(session) {
|
|
277
|
+
const wnd = session.node.window;
|
|
278
|
+
const X = session.node.app?.X ?? wnd?.X;
|
|
279
|
+
if (!X?.on || typeof wnd?.selectInput !== 'function') return;
|
|
280
|
+
|
|
281
|
+
const onEvent = (ev) => {
|
|
282
|
+
if (session.stopped) return;
|
|
283
|
+
if (ev.type !== VISIBILITY_NOTIFY || ev.wid !== wnd.id) return;
|
|
284
|
+
session.publish({ obscured: ev.state === FULLY_OBSCURED });
|
|
285
|
+
};
|
|
286
|
+
X.on('event', onEvent);
|
|
287
|
+
session._offs.push(() => X.off?.('event', onEvent));
|
|
288
|
+
|
|
289
|
+
Promise.resolve(wnd.selectInput(VISIBILITY_CHANGE_MASK)).catch(() => {
|
|
290
|
+
// a window destroyed between the ref resolving and this call; the
|
|
291
|
+
// defaults stand and nothing further will arrive
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** What is known about a window right now. Stable until it changes. */
|
|
296
|
+
export function windowStateSnapshot(app, target) {
|
|
297
|
+
const node = nodeOf(app, target);
|
|
298
|
+
if (!node) return UNKNOWN;
|
|
299
|
+
return sessions.get(node)?.snapshot ?? UNKNOWN;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Subscribe to a window's state. Not public — `useWindowState()` is. */
|
|
303
|
+
export function watchWindowState(app, target, onChange) {
|
|
304
|
+
const node = nodeOf(app, target);
|
|
305
|
+
if (!node) return () => {};
|
|
306
|
+
const session = sessionFor(node);
|
|
307
|
+
arm(session);
|
|
308
|
+
session.listeners.add(onChange);
|
|
309
|
+
return () => session.listeners.delete(onChange);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Test seam: state what the window manager did, without a window manager.
|
|
314
|
+
* Marks the session armed, so nothing reaches for `_NET_WM_STATE` behind the
|
|
315
|
+
* value; `states` re-derives `minimized`/`maximized`/`fullscreen` the way a
|
|
316
|
+
* real `statechange` does.
|
|
317
|
+
*/
|
|
318
|
+
export function setWindowStateForTests(app, target, values) {
|
|
319
|
+
const node = nodeOf(app, target);
|
|
320
|
+
if (!node) return null;
|
|
321
|
+
const session = sessionFor(node);
|
|
322
|
+
session.armed = true;
|
|
323
|
+
session.publish(values);
|
|
324
|
+
return session.snapshot;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Tear down with the window. Called from `WindowNode.destroy`. */
|
|
328
|
+
export function endWindowState(node) {
|
|
329
|
+
const session = sessions.get(node);
|
|
330
|
+
if (!session) return;
|
|
331
|
+
session.stop();
|
|
332
|
+
sessions.delete(node);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* What the window manager has done with this window, live.
|
|
337
|
+
*
|
|
338
|
+
* ```jsx
|
|
339
|
+
* const { focused, visible, fullscreen } = useWindowState();
|
|
340
|
+
*
|
|
341
|
+
* // a title bar that dims when the window is not the active one
|
|
342
|
+
* <text style={{ color: focused ? theme.text : theme.textMuted }}>{title}</text>
|
|
343
|
+
*
|
|
344
|
+
* // and work that is pointless while nobody can see it
|
|
345
|
+
* useEffect(() => {
|
|
346
|
+
* if (!visible) return;
|
|
347
|
+
* const id = setInterval(poll, 1000);
|
|
348
|
+
* return () => clearInterval(id);
|
|
349
|
+
* }, [visible]);
|
|
350
|
+
* ```
|
|
351
|
+
*
|
|
352
|
+
* | | |
|
|
353
|
+
* | --- | --- |
|
|
354
|
+
* | `focused` | this window has the keyboard |
|
|
355
|
+
* | `visible` | **the one to branch on** — not minimized, not fully covered |
|
|
356
|
+
* | `minimized` | `_NET_WM_STATE_HIDDEN`: iconified, or shaded away |
|
|
357
|
+
* | `maximized` | both axes; one axis alone shows up in `states` |
|
|
358
|
+
* | `fullscreen` | what the WM actually did, not what `<window fullscreen>` asked for |
|
|
359
|
+
* | `obscured` | fully covered by other windows — **always false under a compositor** |
|
|
360
|
+
* | `states` | the raw `_NET_WM_STATE` names, e.g. `['maximized_vert', 'focused']` |
|
|
361
|
+
* | `desktop` | the workspace index, or null (a sticky window shows up in `states`) |
|
|
362
|
+
*
|
|
363
|
+
* With no argument it reads the window the component is in, inferred the way
|
|
364
|
+
* `useTopLevelWindow()` infers it — exact for the one-window app, and a
|
|
365
|
+
* documented guess for a tree with several. Pass a ref to be certain:
|
|
366
|
+
*
|
|
367
|
+
* ```jsx
|
|
368
|
+
* const win = useRef(null);
|
|
369
|
+
* const { fullscreen } = useWindowState(win);
|
|
370
|
+
* return <window ref={win}>…</window>;
|
|
371
|
+
* ```
|
|
372
|
+
*
|
|
373
|
+
* Nothing is asked of the server until something calls this, and the first
|
|
374
|
+
* render answers from the defaults — focused and visible — rather than
|
|
375
|
+
* waiting for a round trip. See the compositor note in this file's header
|
|
376
|
+
* before reaching for `obscured`.
|
|
377
|
+
*/
|
|
378
|
+
export function useWindowState(ref = null) {
|
|
379
|
+
const app = useAppOrNull();
|
|
380
|
+
const owner = useTopLevelWindow();
|
|
381
|
+
const target = ref ?? owner;
|
|
382
|
+
const subscribe = useCallback(
|
|
383
|
+
(onChange) => watchWindowState(app, target, onChange),
|
|
384
|
+
[app, target],
|
|
385
|
+
);
|
|
386
|
+
const snapshot = useCallback(
|
|
387
|
+
() => windowStateSnapshot(app, target),
|
|
388
|
+
[app, target],
|
|
389
|
+
);
|
|
390
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** @typedef {typeof UNKNOWN} WindowState */
|