react-x11 2.10.2 → 2.12.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 +278 -129
- package/package.json +10 -3
- package/src/Reconciler.js +15 -17
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +304 -49
- package/src/cocoa/bezels.js +175 -30
- package/src/cocoa/dnd.js +27 -13
- package/src/cocoa/fonts.js +3 -3
- package/src/cocoa/glarea.js +20 -3
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +4 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/screencolor.js +62 -0
- package/src/cocoa/threaded.js +246 -0
- package/src/cocoa/window.js +256 -42
- package/src/components/Select.js +2 -2
- package/src/components/anchor.js +3 -3
- package/src/components/native.js +12 -7
- package/src/components/theme.js +2 -2
- package/src/debug.js +1 -1
- package/src/decorations.js +1 -1
- package/src/editmenu.js +2 -2
- package/src/errors.js +46 -0
- package/src/events.js +6 -6
- package/src/foreignnodes.js +3 -2
- package/src/frames.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -0
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +21 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +4 -2
- package/src/node.js +19 -21
- package/src/nodes/animation.js +644 -0
- package/src/nodes/box.js +21 -0
- package/src/nodes/boxpaint.js +473 -0
- package/src/nodes/canvas.js +269 -0
- package/src/nodes/cascade.js +600 -0
- package/src/nodes/damage.js +183 -0
- package/src/nodes/edithistory.js +124 -0
- package/src/nodes/editmenupopup.js +260 -0
- package/src/nodes/hittest.js +185 -0
- package/src/nodes/image.js +266 -0
- package/src/nodes/install.js +75 -0
- package/src/nodes/invalidate.js +465 -0
- package/src/nodes/kinds.js +31 -0
- package/src/nodes/layout.js +439 -0
- package/src/nodes/layouthost.js +949 -0
- package/src/nodes/node.js +868 -0
- package/src/nodes/paint.js +466 -0
- package/src/nodes/position.js +366 -0
- package/src/nodes/preedit.js +127 -0
- package/src/nodes/queries.js +330 -0
- package/src/nodes/rects.js +102 -0
- package/src/nodes/scrollable.js +891 -0
- package/src/nodes/scrollbars.js +138 -0
- package/src/nodes/scrollblit.js +1034 -0
- package/src/nodes/selectable.js +142 -0
- package/src/nodes/styling.js +225 -0
- package/src/nodes/text.js +649 -0
- package/src/nodes/textarea.js +391 -0
- package/src/nodes/textinput.js +1146 -0
- package/src/nodes/util.js +17 -0
- package/src/nodes/window/anchoring.js +161 -0
- package/src/nodes/window/capabilities.js +190 -0
- package/src/nodes/window/debugpaint.js +83 -0
- package/src/nodes/window/droptarget.js +145 -0
- package/src/nodes/window/floors.js +577 -0
- package/src/nodes/window/flush.js +334 -0
- package/src/nodes/window/hints.js +482 -0
- package/src/nodes/window/listeners.js +222 -0
- package/src/nodes/window/popup.js +71 -0
- package/src/nodes/window/size.js +591 -0
- package/src/nodes/window/window.js +945 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/screencolor.js +212 -38
- package/src/screencolorhooks.js +6 -2
- package/src/styles.js +137 -15
- package/src/svgnodes.js +2 -1
- package/src/testing/harness.js +2 -2
- package/src/textselection.js +5 -3
- package/src/trace-registry.js +1 -1
- package/src/types/components.d.ts +38 -6
- package/src/types/elements.d.ts +11 -1
- package/src/types/nodes.d.ts +33 -5
- package/src/types/screencolor.d.ts +20 -14
- package/src/types/style.d.ts +94 -3
- package/src/windowstate.js +1 -1
- package/src/yoga.js +1 -1
- package/src/nodes.js +0 -13120
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
// <window>: the node that owns a real X11 window. Realizing it, mapping it at
|
|
2
|
+
// the end of the commit that created it (#201), stacking its child windows,
|
|
3
|
+
// and the props and tree operations a window handles differently from a
|
|
4
|
+
// box. The rest of WindowNode lives with its concerns — the window-only ones
|
|
5
|
+
// beside this file, and its halves of the ones it shares with Node beside
|
|
6
|
+
// Node's — installed at the bottom (see install.js).
|
|
7
|
+
|
|
8
|
+
import { applyLayoutStyle, paintPropsChanged } from '../../styles.js';
|
|
9
|
+
import { EventManager } from '../../events.js';
|
|
10
|
+
import { forgetTopLevel, hasDropProps } from '../../dnd.js';
|
|
11
|
+
import { clearPendingFrame } from '../../frames.js';
|
|
12
|
+
import { FramePacer } from '../../pacing.js';
|
|
13
|
+
import { endWindowState } from '../../windowstate.js';
|
|
14
|
+
import { anchorOffscreen } from '../../anchor.js';
|
|
15
|
+
import { topLevelWindows } from '../../windowid.js';
|
|
16
|
+
import { WindowAnimation } from '../animation.js';
|
|
17
|
+
import { WindowCascade } from '../cascade.js';
|
|
18
|
+
import { NO_DAMAGE } from '../damage.js';
|
|
19
|
+
import { installMethods } from '../install.js';
|
|
20
|
+
import { WindowInvalidate } from '../invalidate.js';
|
|
21
|
+
import { WindowLayoutHost } from '../layouthost.js';
|
|
22
|
+
import { DEVTOOLS_FAKE_DOCUMENT, Node } from '../node.js';
|
|
23
|
+
import { WindowPaint } from '../paint.js';
|
|
24
|
+
import { WindowPosition } from '../position.js';
|
|
25
|
+
import { WindowQueries } from '../queries.js';
|
|
26
|
+
import { Scrollable } from '../scrollable.js';
|
|
27
|
+
import { WindowScrollBlit } from '../scrollblit.js';
|
|
28
|
+
import { WindowAnchoring } from './anchoring.js';
|
|
29
|
+
import { pixelFor, WindowCapabilities } from './capabilities.js';
|
|
30
|
+
import { WindowDebugPaint } from './debugpaint.js';
|
|
31
|
+
import { WindowDropTarget } from './droptarget.js';
|
|
32
|
+
import { WindowFlush } from './flush.js';
|
|
33
|
+
import {
|
|
34
|
+
isAutoSize,
|
|
35
|
+
canonicalSize,
|
|
36
|
+
assertWindowSize,
|
|
37
|
+
scaleWindowGeometry,
|
|
38
|
+
windowAttributes,
|
|
39
|
+
windowStates,
|
|
40
|
+
applyWindowStates,
|
|
41
|
+
applyDecorations,
|
|
42
|
+
WINDOW_SEMANTIC_NAMES,
|
|
43
|
+
WindowHints,
|
|
44
|
+
} from './hints.js';
|
|
45
|
+
import { WindowListeners } from './listeners.js';
|
|
46
|
+
import { WindowSize } from './size.js';
|
|
47
|
+
|
|
48
|
+
// X ConfigureWindow stack-mode: Below places the window directly under the
|
|
49
|
+
// named sibling (X11 protocol, ConfigureWindow).
|
|
50
|
+
const STACK_BELOW = 1;
|
|
51
|
+
|
|
52
|
+
// Windows whose child stacking order may have gone stale during the commit
|
|
53
|
+
// in progress; drained by flushWindowRestacks from resetAfterCommit.
|
|
54
|
+
const pendingRestack = new Set();
|
|
55
|
+
|
|
56
|
+
// Windows realized during the commit in progress, waiting to be mapped;
|
|
57
|
+
// drained by flushWindowMaps from resetAfterCommit. See beginWindowMaps.
|
|
58
|
+
const pendingMaps = new Set();
|
|
59
|
+
let inCommit = false;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A window maps at the *end* of the commit that realized it, not when
|
|
63
|
+
* `realize()` runs.
|
|
64
|
+
*
|
|
65
|
+
* React inserts a host instance before it hides it: `hideInstance` runs
|
|
66
|
+
* after the whole mutation phase, so a `<window>` born inside a hidden
|
|
67
|
+
* `<Activity>` — or inside a `<Suspense>` that suspends on its first render
|
|
68
|
+
* — used to be mapped and unmapped back to back. That pair is only safe
|
|
69
|
+
* when nothing redirects the map. Under a window manager holding
|
|
70
|
+
* SubstructureRedirect on the root the MapWindow is **not performed**: the
|
|
71
|
+
* server turns it into a MapRequest and leaves the window unmapped, so the
|
|
72
|
+
* UnmapWindow that follows lands on an already-unmapped window and is
|
|
73
|
+
* discarded. The window manager then services its MapRequest and the
|
|
74
|
+
* "hidden" window is on screen for good (issue #201).
|
|
75
|
+
*
|
|
76
|
+
* Deferring costs nothing — `resetAfterCommit` runs inside the same
|
|
77
|
+
* synchronous `render()` — and it means the map is decided at the one
|
|
78
|
+
* moment when whether the window is hidden is already known.
|
|
79
|
+
*
|
|
80
|
+
* Outside a commit (a `<popup>` realized from `commitMount`, which runs in
|
|
81
|
+
* the layout phase, or one built imperatively like the text controls' edit
|
|
82
|
+
* menu) there is no such phase to wait for, and no hiding on the way
|
|
83
|
+
* either: those map immediately.
|
|
84
|
+
*/
|
|
85
|
+
export function beginWindowMaps() {
|
|
86
|
+
// A commit that never reached `resetAfterCommit` left its queue behind,
|
|
87
|
+
// and a window that is owed a map had better get one late rather than
|
|
88
|
+
// never — that failure mode is an application with no windows in it.
|
|
89
|
+
flushWindowMaps();
|
|
90
|
+
inCommit = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Map every window this commit realized and did not then hide. */
|
|
94
|
+
export function flushWindowMaps() {
|
|
95
|
+
inCommit = false;
|
|
96
|
+
const nodes = [...pendingMaps];
|
|
97
|
+
pendingMaps.clear();
|
|
98
|
+
for (const node of nodes) node._mapNow();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The server-side event mask every realized window ends up with. The
|
|
102
|
+
// subscriptions are a constant — the EventManager's pointer/key/focus
|
|
103
|
+
// listeners, the window's own resize/draw/expose pair, the backing store's
|
|
104
|
+
// Exposure — but ntk grows the mask lazily, one ChangeWindowAttributes per
|
|
105
|
+
// first listener of each kind: nine requests per window for a value known
|
|
106
|
+
// before the window exists. Declaring the union in CreateWindow makes every
|
|
107
|
+
// one of those a detected no-op (ntk ORs `eventMask` into what it derives,
|
|
108
|
+
// and `newListener` only issues the request for bits still missing).
|
|
109
|
+
//
|
|
110
|
+
// The values are core-protocol SETofEVENT bits, fixed since X11R1 — the
|
|
111
|
+
// same numbers ntk's own table maps event names to, written out because ntk
|
|
112
|
+
// does not export them. EnterWindow is deliberately absent: hover tracking
|
|
113
|
+
// reads `mousemove`/`mouseout` only, and parity with the lazily-grown mask
|
|
114
|
+
// is what keeps this a request-count change and nothing else.
|
|
115
|
+
const WINDOW_EVENT_MASK =
|
|
116
|
+
(1 << 0) | // KeyPress — keydown
|
|
117
|
+
(1 << 1) | // KeyRelease — keyup
|
|
118
|
+
(1 << 2) | // ButtonPress — mousedown, and the core half of wheel
|
|
119
|
+
(1 << 3) | // ButtonRelease — mouseup
|
|
120
|
+
(1 << 5) | // LeaveWindow — mouseout
|
|
121
|
+
(1 << 6) | // PointerMotion — mousemove (hover, drag)
|
|
122
|
+
(1 << 15) | // Exposure — draw/expose, and the backing store's redraws
|
|
123
|
+
(1 << 17) | // StructureNotify — resize/map/destroy (ntk's own baseline)
|
|
124
|
+
(1 << 21); // FocusChange — focus/blur
|
|
125
|
+
|
|
126
|
+
/** Apply any child-window stacking changes the commit produced, once. */
|
|
127
|
+
export function flushWindowRestacks() {
|
|
128
|
+
const nodes = [...pendingRestack];
|
|
129
|
+
pendingRestack.clear();
|
|
130
|
+
for (const node of nodes) node._restackWindowChildren();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* <window>: backed by a real X11 window. Acts as the flex root and
|
|
135
|
+
* paint/event root for its drawn subtree. The node is a lightweight handle
|
|
136
|
+
* during the render phase — the real window is created top-down in the
|
|
137
|
+
* commit phase by realize(), so every CreateWindow names its actual parent
|
|
138
|
+
* from the start (no ReparentWindow, no override-redirect staging;
|
|
139
|
+
* issue #4).
|
|
140
|
+
*/
|
|
141
|
+
export class WindowNode extends Scrollable(Node) {
|
|
142
|
+
constructor(app, attributes, props) {
|
|
143
|
+
super('window', props, app, { yoga: true });
|
|
144
|
+
assertWindowSize(props, this.kind);
|
|
145
|
+
this.root = this;
|
|
146
|
+
this.attributes = attributes;
|
|
147
|
+
this.window = null;
|
|
148
|
+
// `hidden` has two writers — the reconciler (React hiding a subtree for
|
|
149
|
+
// `<Suspense>`/`<Activity>`) and the element's own `hidden` prop — and
|
|
150
|
+
// the window is off screen while *either* says so. The reconciler's half
|
|
151
|
+
// is remembered here so that a `<Suspense>` revealing its content does
|
|
152
|
+
// not map a window whose prop still hides it. `this.hidden` stays the
|
|
153
|
+
// one flag everything reads (`_mapNow`, painting, a11y, anchoring).
|
|
154
|
+
this._reactHidden = false;
|
|
155
|
+
this.hidden = Boolean(props.hidden);
|
|
156
|
+
// whether this is the tree's own top-level window rather than a nested
|
|
157
|
+
// one or a popup — decided by realize(), read when it maps
|
|
158
|
+
this._topLevel = false;
|
|
159
|
+
// set by realize() only once the ARGB visual is actually there, so the
|
|
160
|
+
// paint path never assumes an alpha channel the window does not have
|
|
161
|
+
this._transparent = false;
|
|
162
|
+
// What `@supports` blocks are answered from, and what the paint path
|
|
163
|
+
// reads. `transparency` needs *both* halves — an alpha channel to write
|
|
164
|
+
// and a compositor to blend it — and starts false so a window that has
|
|
165
|
+
// not resolved either yet paints the design that works everywhere.
|
|
166
|
+
this._capabilities = { transparency: false };
|
|
167
|
+
this._unwatchCompositing = null;
|
|
168
|
+
this.needsLayout = true;
|
|
169
|
+
this.needsPaint = true;
|
|
170
|
+
this._scheduled = false;
|
|
171
|
+
// The frame pacer (src/pacing.js): whether a claim waits before its
|
|
172
|
+
// frame is scheduled, priced by what the last frames cost. Off unless
|
|
173
|
+
// the `frameRate` prop, the root's default or the environment says
|
|
174
|
+
// otherwise — resolved again whenever the prop changes.
|
|
175
|
+
this._pacer = new FramePacer();
|
|
176
|
+
this._framePolicy = null;
|
|
177
|
+
this._syncFramePolicy();
|
|
178
|
+
// a claim raised by the frame on itself is scheduled once the frame is
|
|
179
|
+
// over and its cost is known (`flush`)
|
|
180
|
+
this._inFlush = false;
|
|
181
|
+
this._claimAfterFlush = false;
|
|
182
|
+
// the nodes answering `opaqueRect()`, and — during a paint pass one of
|
|
183
|
+
// them covers — that node with its ancestors, whose fills are skipped
|
|
184
|
+
// (`_coverFor`, `Node._paintBackground`)
|
|
185
|
+
this._opaqueNodes = new Set();
|
|
186
|
+
this._coverChain = null;
|
|
187
|
+
// Nodes that want the `attention` event (ntk#37) — an
|
|
188
|
+
// `unstable_onAttention` prop,
|
|
189
|
+
// an `:attention` block, or both. Built before the EventManager so the
|
|
190
|
+
// manager can hold the reference itself: the whole feature has to be
|
|
191
|
+
// behind one `size` read on the motion path, and a tree that never asked
|
|
192
|
+
// for attention must not pay a property walk to find that out.
|
|
193
|
+
this._attentionNodes = new Set();
|
|
194
|
+
this.events = new EventManager(this);
|
|
195
|
+
// ids of the child windows in the order the *server* stacks them,
|
|
196
|
+
// bottom to top — see _restackWindowChildren
|
|
197
|
+
this._xStack = [];
|
|
198
|
+
// nodes with a transition in flight
|
|
199
|
+
this._animating = new Set();
|
|
200
|
+
// …and the nodes whose style declares a *loop*, running or not: the set
|
|
201
|
+
// every stop condition is applied over, and what decides whether this
|
|
202
|
+
// window is watching its own visibility at all
|
|
203
|
+
this._loopNodes = new Set();
|
|
204
|
+
this._loopsPaused = false;
|
|
205
|
+
this._loopWatch = null;
|
|
206
|
+
// nodes with `@width`/`@height` blocks, and the size they last matched
|
|
207
|
+
// against
|
|
208
|
+
this._sizeQueryNodes = new Set();
|
|
209
|
+
// nodes with `@supports` blocks, re-resolved when the server's answer
|
|
210
|
+
// changes rather than on every layout
|
|
211
|
+
this._supportsQueryNodes = new Set();
|
|
212
|
+
// nodes with `@container` blocks, re-resolved after every layout pass
|
|
213
|
+
// against the containers they ask about — see _resolveContainerQueries.
|
|
214
|
+
// `_cqFresh` is true while a pass this window just ran is being settled,
|
|
215
|
+
// when every attached node has a computed size to offer
|
|
216
|
+
this._containerQueryNodes = new Set();
|
|
217
|
+
this._cqFresh = false;
|
|
218
|
+
// nodes whose `position` is placed after layout (sticky among them),
|
|
219
|
+
// placed after every layout pass — see _placeNodes — and whether one
|
|
220
|
+
// asked for a frame of its own, with nothing to lay out (an animated
|
|
221
|
+
// position)
|
|
222
|
+
this._placedNodes = new Set();
|
|
223
|
+
this._placementsDue = false;
|
|
224
|
+
// nodes arranging their children with a layout algorithm, placed at the
|
|
225
|
+
// end of every layout step — see _placeLayoutHosts — and the ones whose
|
|
226
|
+
// algorithm threw this pass, laid out as flexbox before the frame is done
|
|
227
|
+
this._layoutHosts = new Set();
|
|
228
|
+
this._failedHosts = new Set();
|
|
229
|
+
// nodes whose child list changed and whose own size is pinned: their new
|
|
230
|
+
// arrangement is only measurable once layout has run (see
|
|
231
|
+
// Node._childListChanged)
|
|
232
|
+
this._reflowed = new Set();
|
|
233
|
+
this.querySize = null;
|
|
234
|
+
// The geometry we last asked the server for, and whether anything else
|
|
235
|
+
// has since decided otherwise. Together they are the rule for `'auto'`:
|
|
236
|
+
// it keeps up with the content until someone takes the size over, and
|
|
237
|
+
// the only thing that ever does is the user dragging an edge.
|
|
238
|
+
this._requestedSize = null;
|
|
239
|
+
this._userSized = false;
|
|
240
|
+
// the last `WM_NORMAL_HINTS` struct written and the size it was written
|
|
241
|
+
// at, so that a bound measured every frame is only *sent* on the frames
|
|
242
|
+
// it moves (see _sendSizeHints for why the size is part of it)
|
|
243
|
+
this._sentHints = null;
|
|
244
|
+
this._sentHintsAt = null;
|
|
245
|
+
// The automatic minimum size (#249): whether the floors are still the
|
|
246
|
+
// answer, the width the height half of them was measured for, the nodes
|
|
247
|
+
// this frame found stale (`collectFloorStale`), and two counters the
|
|
248
|
+
// tests read — layout passes over the root and nodes measured.
|
|
249
|
+
this._floorsDirty = true;
|
|
250
|
+
this._floorsWidth = null;
|
|
251
|
+
this._floorsStale = new Set();
|
|
252
|
+
// whether the first floors pass has run (`collectFloorStale`'s sweep)
|
|
253
|
+
this._floorsSwept = false;
|
|
254
|
+
this._floorsMeasured = 0;
|
|
255
|
+
this._layoutPasses = 0;
|
|
256
|
+
// whether the tree's CONTENT moved since the floors were measured — a
|
|
257
|
+
// resize alone does not, which is what lets a live resize defer them
|
|
258
|
+
this._floorsContentDirty = true;
|
|
259
|
+
this._floorsCatchUp = false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Create the real X11 window (commit phase only). Children windows are
|
|
263
|
+
* realized against this window, then mapped before it so the whole
|
|
264
|
+
* subtree appears at once when the outermost window maps. */
|
|
265
|
+
realize(parentWindow) {
|
|
266
|
+
if (this.window || this.destroyed) return;
|
|
267
|
+
const attributes = { ...this.attributes };
|
|
268
|
+
if (parentWindow) {
|
|
269
|
+
attributes.parent = parentWindow;
|
|
270
|
+
}
|
|
271
|
+
// Before CreateWindow, so an auto-sized window is *born* the right size.
|
|
272
|
+
// Doing it after would mean a window mapped at 800x800 and corrected a
|
|
273
|
+
// frame later, which is the jump this exists to avoid.
|
|
274
|
+
const natural = this._measure();
|
|
275
|
+
attributes.width = natural.width;
|
|
276
|
+
attributes.height = natural.height;
|
|
277
|
+
this._requestedSize = { width: natural.width, height: natural.height };
|
|
278
|
+
// And straight after it, for the same reason: the placement is a
|
|
279
|
+
// function of the size, so this is the first moment it can be worked
|
|
280
|
+
// out — and the last one before the window exists at a position.
|
|
281
|
+
const placed = this._anchorPlacement(natural);
|
|
282
|
+
if (placed) {
|
|
283
|
+
attributes.x = placed.x;
|
|
284
|
+
attributes.y = placed.y;
|
|
285
|
+
this._placedAt = { x: placed.x, y: placed.y };
|
|
286
|
+
}
|
|
287
|
+
// A bound the content decides is a number by now, and the window manager
|
|
288
|
+
// reads `WM_NORMAL_HINTS` when it frames the window — so it goes in with
|
|
289
|
+
// the creation attributes rather than chasing the map with a second
|
|
290
|
+
// property write.
|
|
291
|
+
if (Object.keys(natural.hints).length > 0) {
|
|
292
|
+
this._sentHints = this._hintsToSend(this.props, natural.hints);
|
|
293
|
+
attributes.sizeHints = this._sentHints;
|
|
294
|
+
}
|
|
295
|
+
// **What the server paints into newly exposed area.** A resize enlarges
|
|
296
|
+
// the window before the app can possibly have drawn the new part, and X
|
|
297
|
+
// fills it with this attribute in the meantime — so without one, growing
|
|
298
|
+
// a window flashes whatever the server's default is, which on a dark
|
|
299
|
+
// palette is a bright rectangle. Setting it to the colour that is about
|
|
300
|
+
// to be painted there makes the flash the same colour as the result.
|
|
301
|
+
const pixel = pixelFor(this._windowBackground());
|
|
302
|
+
if (pixel !== null) {
|
|
303
|
+
attributes.backgroundPixel = pixel;
|
|
304
|
+
this._backgroundPixel = pixel;
|
|
305
|
+
}
|
|
306
|
+
// Before the window exists, because a visual is a CreateWindow field: a
|
|
307
|
+
// window cannot become transparent later, which is also why `transparent`
|
|
308
|
+
// is read here and never in the update path. It overrides the pixel above
|
|
309
|
+
// with 0 — transparent black — when the ARGB visual is really there.
|
|
310
|
+
if (this.props.transparent)
|
|
311
|
+
Object.assign(attributes, this._argbAttributes());
|
|
312
|
+
// **Smooth scrolling, where the server can — and where the window turns
|
|
313
|
+
// out to want it.** XI2 carries a scroll as the device's own valuators,
|
|
314
|
+
// so a touchpad's two-finger scroll arrives as the fractions of a notch
|
|
315
|
+
// it was rather than as the whole clicks of button 4/5 the server
|
|
316
|
+
// emulates for clients that cannot read them. ntk translates the device
|
|
317
|
+
// events back into the core-shaped ones the rest of this file reads, and
|
|
318
|
+
// falls back to those buttons where there is no XI2 (issue #273).
|
|
319
|
+
//
|
|
320
|
+
// **It is not free, which is why it is no longer selected up front.** An
|
|
321
|
+
// XI2 selection *replaces* the core one for the same event type, and an
|
|
322
|
+
// XIMotion is 136 bytes on the wire against a core MotionNotify's 32
|
|
323
|
+
// (`npm run xi2:probe`, Xorg 21.1). Motion is the one event that keeps
|
|
324
|
+
// arriving at frame rate for as long as the pointer is over the window,
|
|
325
|
+
// so an eager selection bills every window ~8 KB/s of pointer traffic
|
|
326
|
+
// while the pointer crosses it — for a feature most windows never use. A
|
|
327
|
+
// dialog, a toolbar, a form, a splash screen never see a wheel at all.
|
|
328
|
+
//
|
|
329
|
+
// So `'auto'` — the default — creates the window on core events and takes
|
|
330
|
+
// the selection the first time the window is actually scrolled
|
|
331
|
+
// (`upgradeToXI2`, from `EventManager._onWheel`). What that costs is the
|
|
332
|
+
// opening event of the first gesture in a window's life, and it costs
|
|
333
|
+
// less than it sounds: a mouse wheel reports whole notches whichever way
|
|
334
|
+
// the scroll arrived, and ntk's `ScrollTracker` treats the first valuator
|
|
335
|
+
// event as a seed with no distance to report — so under an eager
|
|
336
|
+
// selection that same first event moves nothing at all. `xi2` selects at
|
|
337
|
+
// creation for an app whose whole interaction is the touchpad; `false`
|
|
338
|
+
// refuses the selection outright.
|
|
339
|
+
//
|
|
340
|
+
// Never on a `<popup>`, which is the window that holds a pointer grab: a
|
|
341
|
+
// core grab delivers core events, and ntk drops the emulated wheel
|
|
342
|
+
// buttons on a window whose valuators are flowing — a menu that had
|
|
343
|
+
// selected XI2 would be a menu the wheel could not reach while it was
|
|
344
|
+
// grabbing. An explicit `xi2` still wins there, because an app that says
|
|
345
|
+
// so has said so.
|
|
346
|
+
const wantsXI2 = this.props.xi2 ?? 'auto';
|
|
347
|
+
// `'auto'` is ours and must not reach ntk, whose `args.xi2` is truthiness
|
|
348
|
+
attributes.xi2 = wantsXI2 === true;
|
|
349
|
+
this._xi2Pending = wantsXI2 === 'auto' && !this.isPopup;
|
|
350
|
+
// The full event mask, declared at creation — see WINDOW_EVENT_MASK.
|
|
351
|
+
attributes.eventMask = (attributes.eventMask ?? 0) | WINDOW_EVENT_MASK;
|
|
352
|
+
const wnd = this.app.createWindow(attributes);
|
|
353
|
+
this.window = wnd;
|
|
354
|
+
// Now that the visual is known: settle the capabilities, re-resolve any
|
|
355
|
+
// `@supports` block against them, and start following the compositor.
|
|
356
|
+
// Before the first paint, and before children realize against it.
|
|
357
|
+
this._watchCapabilities();
|
|
358
|
+
wnd._reactX11Node = this;
|
|
359
|
+
wnd._reactFiber = this._reactFiber;
|
|
360
|
+
// windows are DevTools public instances too — see Node.getClientRects
|
|
361
|
+
const s = this.scale;
|
|
362
|
+
wnd.getClientRects ??= () => [
|
|
363
|
+
{
|
|
364
|
+
x: 0,
|
|
365
|
+
y: 0,
|
|
366
|
+
left: 0,
|
|
367
|
+
top: 0,
|
|
368
|
+
width: wnd.width / s,
|
|
369
|
+
height: wnd.height / s,
|
|
370
|
+
},
|
|
371
|
+
];
|
|
372
|
+
wnd.measure ??= (callback) =>
|
|
373
|
+
callback?.(0, 0, wnd.width / s, wnd.height / s, 0, 0);
|
|
374
|
+
wnd.ownerDocument ??= DEVTOOLS_FAKE_DOCUMENT;
|
|
375
|
+
this._attachWindowListeners(parentWindow);
|
|
376
|
+
for (const child of this.children) {
|
|
377
|
+
if (child.isWindow && !child.isPopup) {
|
|
378
|
+
child.realize(wnd);
|
|
379
|
+
if (child.window) this._xStack.push(child.window.id);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
this._restackWindowChildren();
|
|
383
|
+
// <glarea>s and <foreign>s mounted before the window existed own a
|
|
384
|
+
// child X window too
|
|
385
|
+
this._realizeChildWindows(this);
|
|
386
|
+
// Before the map, deliberately. EWMH 7.7 gives an unmapped window a
|
|
387
|
+
// different mechanism — it *declares* its initial state by writing the
|
|
388
|
+
// property, where a mapped one has to *ask* the window manager — and
|
|
389
|
+
// declaring is the only way to open already fullscreen rather than
|
|
390
|
+
// flashing at the normal size first. Same for the Motif hint: a WM
|
|
391
|
+
// reads decorations when it frames the window, which is at map time.
|
|
392
|
+
if (this.props.decorations === false) applyDecorations(wnd, false);
|
|
393
|
+
applyWindowStates(wnd, [...windowStates(this.props)], 'add');
|
|
394
|
+
// ICCCM 4.1.2.6 has the window manager read WM_TRANSIENT_FOR when the
|
|
395
|
+
// transient is mapped, so this belongs before the map too. ntk writes it
|
|
396
|
+
// with predefined atoms and no round trip, so "before" is free.
|
|
397
|
+
this._applyTransientFor(this.props.transientFor);
|
|
398
|
+
// Top-level windows advertise XDND before the map, like the EWMH
|
|
399
|
+
// properties above: a declaration, made before anyone can look. Child
|
|
400
|
+
// <window>s never advertise (XDND v3 puts XdndAware on top-levels
|
|
401
|
+
// only); drags over them arrive here and are routed down in JS.
|
|
402
|
+
if (!parentWindow) this._initDnd();
|
|
403
|
+
// The launch's own properties, and the same "before the map" rule as
|
|
404
|
+
// everything above it: EWMH's guarantee about `_NET_WM_USER_TIME` is
|
|
405
|
+
// about the window's state at the moment it is mapped. First toplevel
|
|
406
|
+
// only — a later `<window>` is not the launch (src/startup.js).
|
|
407
|
+
this._topLevel = !parentWindow && !this.isPopup;
|
|
408
|
+
if (this._topLevel) this.app._reactX11Startup?.decorate(wnd);
|
|
409
|
+
// Before the map for the same reason the properties above are: a popup
|
|
410
|
+
// whose anchor is already out of view — an editor scrolled between the
|
|
411
|
+
// keystroke that opened the completion list and the commit that
|
|
412
|
+
// realized it — should never be on screen at all, rather than appear
|
|
413
|
+
// and vanish.
|
|
414
|
+
if (this.props.anchor) {
|
|
415
|
+
this._watchAnchor();
|
|
416
|
+
const node = this._anchorTarget(this.props.anchor.to);
|
|
417
|
+
this._anchorLost = !node || anchorOffscreen(node, this.props.anchor.at);
|
|
418
|
+
}
|
|
419
|
+
// Queued rather than mapped, when there is a commit to queue behind:
|
|
420
|
+
// React hides a subtree only once it has inserted it (beginWindowMaps).
|
|
421
|
+
if (inCommit) pendingMaps.add(this);
|
|
422
|
+
else this._mapNow();
|
|
423
|
+
// ask before anything can be anchored to it, so the first popup is
|
|
424
|
+
// placed as well as the second
|
|
425
|
+
this._refreshScreenOrigin();
|
|
426
|
+
this.invalidate(true, null, 'mount');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Put the window on screen, unless this commit went on to hide it.
|
|
431
|
+
*
|
|
432
|
+
* The only caller that maps a window for the first time is
|
|
433
|
+
* `flushWindowMaps` (or `realize` itself outside a commit); `setHidden`
|
|
434
|
+
* comes back through here so that a window born hidden and revealed later
|
|
435
|
+
* still ends the startup sequence on its real first map.
|
|
436
|
+
*/
|
|
437
|
+
_mapNow() {
|
|
438
|
+
if (this.destroyed || !this.window || this.hidden) return false;
|
|
439
|
+
// An `embeddable` window never maps itself: a window waiting to be
|
|
440
|
+
// embedded is unmapped — that is what waiting looks like — and from the
|
|
441
|
+
// reparent on, mapping is the embedder's decision (ntk's XEmbedSocket
|
|
442
|
+
// maps a plain client the moment it takes it). Self-mapping here would
|
|
443
|
+
// put a frame pane on the desktop as a top-level for the beat before
|
|
444
|
+
// its <Frame> embeds it, long enough for a window manager to frame it.
|
|
445
|
+
if (this.props.embeddable) return false;
|
|
446
|
+
// An anchor that is not on screen is a popup that has nowhere to be
|
|
447
|
+
// (`_followAnchor`); it maps from there, when the anchor comes back.
|
|
448
|
+
if (this._anchorLost) return false;
|
|
449
|
+
this.window.map?.();
|
|
450
|
+
if (this._topLevel) this.app._reactX11Startup?.mapped(this.window);
|
|
451
|
+
// whether the map went out, so `PopupNode` can hang its grab off it
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Take the XI2 selection this window was deliberately created without —
|
|
457
|
+
* see `realize()` for why `xi2: 'auto'` starts on core events. Called by
|
|
458
|
+
* `EventManager._onWheel`, so the window that is scrolled is the window
|
|
459
|
+
* that pays for smooth scrolling.
|
|
460
|
+
*
|
|
461
|
+
* **One-shot and one-way.** `_xi2Pending` is cleared before the request
|
|
462
|
+
* goes out, so a burst of wheel events in one frame asks once. Coming back
|
|
463
|
+
* down is not offered: the only signal that would justify it is "nothing in
|
|
464
|
+
* here scrolls any more", which cannot be read without a per-node registry,
|
|
465
|
+
* and getting it wrong drops a live gesture from valuators back to notches
|
|
466
|
+
* mid-scroll — a visible regression, to save bytes on a window the user is
|
|
467
|
+
* actively using.
|
|
468
|
+
*
|
|
469
|
+
* Silent where the server has no XInput2: `selectXI2()` resolves `false`
|
|
470
|
+
* and the window keeps the emulated wheel buttons it already had, which is
|
|
471
|
+
* exactly where an eager selection would have landed too. Feature-detected
|
|
472
|
+
* on the method, for an ntk older than 7.5.0.
|
|
473
|
+
*/
|
|
474
|
+
upgradeToXI2() {
|
|
475
|
+
if (!this._xi2Pending || this.destroyed) return;
|
|
476
|
+
this._xi2Pending = false;
|
|
477
|
+
if (typeof this.window?.selectXI2 !== 'function') return;
|
|
478
|
+
// fire and forget, like the eager selection in ntk's own createWindow:
|
|
479
|
+
// until the extension answers the window is on core events, which is
|
|
480
|
+
// where it would have been anyway
|
|
481
|
+
this.window.selectXI2().catch((err) => {
|
|
482
|
+
this.app?.options?.onXError?.(err);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Where this window's top-left corner actually is on the screen, cached
|
|
488
|
+
* on the ntk window for `anchorRect` to read.
|
|
489
|
+
*
|
|
490
|
+
* It cannot be taken from `window.x`/`y`. Those come from ConfigureNotify,
|
|
491
|
+
* and once a reparenting window manager has put the window inside its
|
|
492
|
+
* frame — which is every WM worth the name — those coordinates are
|
|
493
|
+
* relative to the *frame*, not the root. A popup anchored with them lands
|
|
494
|
+
* near the corner of the screen instead of under its trigger. The server
|
|
495
|
+
* will translate for us, and its answer is right whatever the WM did.
|
|
496
|
+
*/
|
|
497
|
+
_refreshScreenOrigin() {
|
|
498
|
+
const wnd = this.window;
|
|
499
|
+
const X = this.app?.X;
|
|
500
|
+
const root = X?.display?.screen?.[0]?.root;
|
|
501
|
+
if (!wnd || root == null || typeof X.TranslateCoordinates !== 'function') {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
X.TranslateCoordinates(wnd.id, root, 0, 0, (err, res) => {
|
|
505
|
+
if (err || this.destroyed || !this.window) return;
|
|
506
|
+
this.window._screenOrigin = { x: res.destX, y: res.destY };
|
|
507
|
+
this._notifyAnchorChange();
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Subscribe to this window gaining or losing the **window manager's**
|
|
513
|
+
* focus. Returns an unsubscribe function.
|
|
514
|
+
*
|
|
515
|
+
* Deliberately not the same thing as a node's `onBlur`: a window losing
|
|
516
|
+
* focus does not blur the node inside it — the node keeps focus and stops
|
|
517
|
+
* looking active, which is what the DOM does with `document.activeElement`
|
|
518
|
+
* and what a caret coming back where you left it depends on. So nothing in
|
|
519
|
+
* the tree hears about it, and the things that must — a menu holding a
|
|
520
|
+
* pointer grab, most of all — have nowhere else to ask.
|
|
521
|
+
*/
|
|
522
|
+
onWindowFocusChange(cb) {
|
|
523
|
+
(this._windowFocusListeners ??= new Set()).add(cb);
|
|
524
|
+
return () => this._windowFocusListeners?.delete(cb);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
_notifyWindowFocus(focused) {
|
|
528
|
+
if (!this._windowFocusListeners?.size) return;
|
|
529
|
+
for (const cb of [...this._windowFocusListeners]) cb(focused);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Walk the drawn subtree and give every element that owns a real child X
|
|
534
|
+
* window one — `<glarea>` and `<foreign>`.
|
|
535
|
+
*
|
|
536
|
+
* Here rather than in `createInstance` because the render phase is
|
|
537
|
+
* discardable: a CreateWindow from a render React throws away leaks a
|
|
538
|
+
* server resource, and a ReparentWindow from one has moved another
|
|
539
|
+
* client's window for real (docs/extending.md).
|
|
540
|
+
*/
|
|
541
|
+
_realizeChildWindows(node) {
|
|
542
|
+
for (const child of node.children) {
|
|
543
|
+
if (child.isWindow) continue;
|
|
544
|
+
if (child.isGlArea || child.isForeign) child.realize();
|
|
545
|
+
else this._realizeChildWindows(child);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
get semanticNames() {
|
|
550
|
+
return WINDOW_SEMANTIC_NAMES;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Whether this is the window whose close button means "quit".
|
|
555
|
+
*
|
|
556
|
+
* Inferred, not declared, because the tree already says it. A window that
|
|
557
|
+
* is somebody's `transientFor` is a dialog *of* that window, and one with
|
|
558
|
+
* an EWMH type of its own (`dialog`, `utility`, `splash`, …) has already
|
|
559
|
+
* announced it is not the main window; what is left, in creation order, is
|
|
560
|
+
* the app. That is the same rule startup.js uses to decide which window
|
|
561
|
+
* carries the launch id, and for one-window apps — nearly all of them — it
|
|
562
|
+
* is not a heuristic at all.
|
|
563
|
+
*
|
|
564
|
+
* A lone window is the app whatever it calls itself: an app whose only
|
|
565
|
+
* window is a `utility` still has to be closable. An app that disagrees
|
|
566
|
+
* with any of this passes `onCloseRequest`, which never reaches here.
|
|
567
|
+
*/
|
|
568
|
+
_isPrimaryWindow() {
|
|
569
|
+
const tops = topLevelWindows(this.app);
|
|
570
|
+
if (!tops.includes(this)) return false;
|
|
571
|
+
const candidates = tops.filter((node) => node._isPrimaryCandidate());
|
|
572
|
+
if (candidates.length === 0) return tops.length === 1;
|
|
573
|
+
return candidates[0] === this;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Top-level, nobody's dialog, and of no special type: a main-window shape. */
|
|
577
|
+
_isPrimaryCandidate() {
|
|
578
|
+
if (this.props.transientFor != null) return false;
|
|
579
|
+
const type = this.props.windowType;
|
|
580
|
+
const plain = (t) => t == null || t === 'normal';
|
|
581
|
+
return Array.isArray(type) ? plain(type[0]) : plain(type);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Child <window>s in the order they should stack, bottom to top: the same
|
|
585
|
+
* rule drawn children paint by (later sibling on top, `zIndex` first). */
|
|
586
|
+
_windowStackOrder() {
|
|
587
|
+
return this.children
|
|
588
|
+
.filter((c) => c.isWindow && !c.isPopup && c.window)
|
|
589
|
+
.map((node, i) => ({ node, i }))
|
|
590
|
+
.sort(
|
|
591
|
+
(a, b) =>
|
|
592
|
+
(a.node.style.zIndex ?? 0) - (b.node.style.zIndex ?? 0) || a.i - b.i,
|
|
593
|
+
)
|
|
594
|
+
.map((e) => e.node);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Make the server's stacking order match the JSX order. X stacks a new
|
|
599
|
+
* window on top of its siblings, so plain mount order already comes out
|
|
600
|
+
* right and this sends nothing; it costs requests only when React moves a
|
|
601
|
+
* child window or a `zIndex` changes. Walking top-down and putting each
|
|
602
|
+
* window directly below the one above it fixes any permutation in one
|
|
603
|
+
* pass — after step i, everything from i upwards is a contiguous run in
|
|
604
|
+
* the right order. Top-level windows are excluded on purpose: they are
|
|
605
|
+
* the window manager's to stack, and it redirects the request anyway;
|
|
606
|
+
* so are popups, which are children of the screen root wherever they sit
|
|
607
|
+
* in the tree. Only `<window>` children are ordered against each other —
|
|
608
|
+
* a `<glarea>`'s X window is a sibling at the server, but it belongs to
|
|
609
|
+
* the drawn tree, which has no stacking relationship with them.
|
|
610
|
+
*/
|
|
611
|
+
_restackWindowChildren() {
|
|
612
|
+
const X = this.app?.X;
|
|
613
|
+
if (!this.window || typeof X?.ConfigureWindow !== 'function') return;
|
|
614
|
+
const stack = this._windowStackOrder();
|
|
615
|
+
const ids = stack.map((c) => c.window.id);
|
|
616
|
+
if (
|
|
617
|
+
ids.length === this._xStack.length &&
|
|
618
|
+
ids.every((id, i) => id === this._xStack[i])
|
|
619
|
+
) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
for (let i = stack.length - 2; i >= 0; i--) {
|
|
623
|
+
X.ConfigureWindow(ids[i], {
|
|
624
|
+
sibling: ids[i + 1],
|
|
625
|
+
stackMode: STACK_BELOW,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
this._xStack = ids;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
insertBefore(child, beforeChild) {
|
|
632
|
+
if (child.isPopup) {
|
|
633
|
+
Node.prototype.insertBefore.call(this, child, beforeChild);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (child.isWindow) {
|
|
637
|
+
const mounting = child.parent == null;
|
|
638
|
+
this._spliceChild(child, beforeChild);
|
|
639
|
+
child.parent = this;
|
|
640
|
+
// Initial children are realized when this window realizes; a child
|
|
641
|
+
// appended to an already-realized window is created immediately,
|
|
642
|
+
// top-down against its real parent — and lands on top of its
|
|
643
|
+
// siblings, which _restackWindowChildren then corrects if the JSX
|
|
644
|
+
// order says otherwise.
|
|
645
|
+
if (this.window && !child.window) {
|
|
646
|
+
child.realize(this.window);
|
|
647
|
+
if (child.window) this._xStack.push(child.window.id);
|
|
648
|
+
}
|
|
649
|
+
if (this.theme || child.props.theme) child._themeChanged(mounting);
|
|
650
|
+
// React reorders a keyed list with one insertBefore per moved child;
|
|
651
|
+
// restacking once at the end of the commit skips the intermediate
|
|
652
|
+
// orders, which nobody ever sees.
|
|
653
|
+
pendingRestack.add(this);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
Node.prototype.insertBefore.call(this, child, beforeChild);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
removeChild(child) {
|
|
660
|
+
if (child.isWindow) {
|
|
661
|
+
const index = this._indexOfChild(child);
|
|
662
|
+
if (index !== -1) {
|
|
663
|
+
this.children.splice(index, 1);
|
|
664
|
+
this._nonYogaKids--;
|
|
665
|
+
}
|
|
666
|
+
const id = child.window?.id;
|
|
667
|
+
child.parent = null;
|
|
668
|
+
child.destroySubtree();
|
|
669
|
+
if (id != null) this._xStack = this._xStack.filter((w) => w !== id);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
Node.prototype.removeChild.call(this, child);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
destroySubtree() {
|
|
676
|
+
if (this.destroyed) return;
|
|
677
|
+
this.destroyed = true;
|
|
678
|
+
clearPendingFrame(this);
|
|
679
|
+
this._pacer.cancel();
|
|
680
|
+
this._unwatchCompositing?.();
|
|
681
|
+
this._unwatchCompositing = null;
|
|
682
|
+
// before endWindowState below, which is the session this is subscribed to
|
|
683
|
+
this._unwatchLoops();
|
|
684
|
+
this._loopNodes.clear();
|
|
685
|
+
this._animating.clear();
|
|
686
|
+
// `useWindowState()`'s listeners, and the raw VisibilityNotify handler
|
|
687
|
+
// it put on the shared connection, which nothing else would take off
|
|
688
|
+
endWindowState(this);
|
|
689
|
+
// Before the owner's next layout pass, which is this same commit: a
|
|
690
|
+
// popup that has gone still holds a subscription to the window it was
|
|
691
|
+
// anchored to, and answering that notification would configure a dead
|
|
692
|
+
// window.
|
|
693
|
+
this._unwatchAnchor();
|
|
694
|
+
// out of the drag registries before the window goes: a drag routed to
|
|
695
|
+
// a dead window would translate against a null _screenOrigin
|
|
696
|
+
forgetTopLevel(this);
|
|
697
|
+
this._dragSession?.cancel();
|
|
698
|
+
for (const child of this.children) child.destroySubtree();
|
|
699
|
+
if (this.window && typeof this.window.destroy === 'function') {
|
|
700
|
+
this.window.destroy();
|
|
701
|
+
}
|
|
702
|
+
this.window = null;
|
|
703
|
+
if (this.yoga) {
|
|
704
|
+
this.yoga.freeRecursive();
|
|
705
|
+
this.yoga = null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
applyProps(newProps, oldProps) {
|
|
710
|
+
const before = oldProps ?? this.props;
|
|
711
|
+
assertWindowSize(newProps, this.kind);
|
|
712
|
+
const beforeStyle = this.style;
|
|
713
|
+
const themeChanged = newProps.theme !== before.theme;
|
|
714
|
+
this.props = newProps;
|
|
715
|
+
if (themeChanged) this._themeChanged();
|
|
716
|
+
const style = this._syncStyle(newProps);
|
|
717
|
+
if (Boolean(newProps.trapFocus) !== Boolean(before.trapFocus)) {
|
|
718
|
+
this._syncFocusScope();
|
|
719
|
+
}
|
|
720
|
+
if (newProps.frameRate !== before.frameRate) this._syncFramePolicy();
|
|
721
|
+
// a <window onDrop> is a whole-window dropzone; same edge as Node
|
|
722
|
+
if (hasDropProps(newProps) !== hasDropProps(before)) {
|
|
723
|
+
if (hasDropProps(newProps)) this._registerDropTarget(this);
|
|
724
|
+
else this._forgetDropTarget(this);
|
|
725
|
+
}
|
|
726
|
+
const wnd = this.window;
|
|
727
|
+
if (!wnd) {
|
|
728
|
+
// Not realized yet: refresh creation attributes instead — through the
|
|
729
|
+
// same filter createInstance used, since these are the arguments ntk's
|
|
730
|
+
// constructor will see. Spreading raw props here was the bug behind
|
|
731
|
+
// `ev.preventDefault is not a function` in a <popup>'s onKeyDown: ntk
|
|
732
|
+
// registers any `onFoo` in its creation args as a raw listener, so the
|
|
733
|
+
// handler was called a second time with the native X event.
|
|
734
|
+
this.attributes = {
|
|
735
|
+
...this.attributes,
|
|
736
|
+
...windowAttributes(newProps, this.scale),
|
|
737
|
+
};
|
|
738
|
+
// The flag `realize()`'s map will read — set directly, since there is
|
|
739
|
+
// nothing on screen yet for the notification half of `_applyHidden`
|
|
740
|
+
// to be about.
|
|
741
|
+
this.hidden = this._reactHidden || Boolean(newProps.hidden);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (newProps.title !== before.title) {
|
|
746
|
+
wnd.setTitle?.(newProps.title || '');
|
|
747
|
+
}
|
|
748
|
+
// The colour the server fills a resize with, kept in step with the one
|
|
749
|
+
// the app paints.
|
|
750
|
+
this._syncWindowBackground();
|
|
751
|
+
// a popup is a child of the screen root, not of the node it is written
|
|
752
|
+
// under, so its zIndex means nothing — and its parent here may well be
|
|
753
|
+
// a drawn node with no children to stack
|
|
754
|
+
if (
|
|
755
|
+
(style.zIndex ?? 0) !== (beforeStyle.zIndex ?? 0) &&
|
|
756
|
+
!this.isPopup &&
|
|
757
|
+
this.parent?._restackWindowChildren
|
|
758
|
+
) {
|
|
759
|
+
pendingRestack.add(this.parent);
|
|
760
|
+
}
|
|
761
|
+
this._applyWindowHints(newProps, before);
|
|
762
|
+
// Position and size part ways below: both are sent to the server, but
|
|
763
|
+
// only a size change re-lays-out — the window's own coordinate space is
|
|
764
|
+
// untouched by where the window sits on screen, so a pointer-tracking
|
|
765
|
+
// popup does not repaint itself per motion.
|
|
766
|
+
// Compared after normalising, so that an app switching between an
|
|
767
|
+
// omitted size and a spelled-out `'auto'` — the same request written two
|
|
768
|
+
// ways — is not a change and does not reset the state below.
|
|
769
|
+
const sizeChanged =
|
|
770
|
+
canonicalSize(newProps.width) !== canonicalSize(before.width) ||
|
|
771
|
+
canonicalSize(newProps.height) !== canonicalSize(before.height);
|
|
772
|
+
// An anchored window's position is the anchor's business, not the
|
|
773
|
+
// app's: `x`/`y` are ignored while `anchor` is set (`_followAnchor`
|
|
774
|
+
// sends the moves), so a commit that changed nothing else is not a
|
|
775
|
+
// configure back to a stale prop.
|
|
776
|
+
const anchored = Boolean(newProps.anchor);
|
|
777
|
+
const movedByProps =
|
|
778
|
+
!anchored && (newProps.x !== before.x || newProps.y !== before.y);
|
|
779
|
+
const geometryChanged = sizeChanged || movedByProps;
|
|
780
|
+
// A size that became a number is the app taking the size over, which is
|
|
781
|
+
// exactly the state `_userSized` names — and one that became `'auto'` is
|
|
782
|
+
// the app handing it back, so the window fits its content again on the
|
|
783
|
+
// next layout even if the user had resized it before.
|
|
784
|
+
//
|
|
785
|
+
// The record moves with it, and an axis still on `'auto'` records the
|
|
786
|
+
// size the window *has*, because that is the one this configure is not
|
|
787
|
+
// about to change. Without that the echo of a one-axis configure would
|
|
788
|
+
// disagree with the record on the other axis and read as somebody else
|
|
789
|
+
// setting the size — locking the window on the app's own update.
|
|
790
|
+
// The comparisons above ran on the raw props — logical against logical
|
|
791
|
+
// — and everything below talks to the server, so it is device from here
|
|
792
|
+
// (`wnd.width`, `_requestedSize` and the ConfigureNotify echo are all
|
|
793
|
+
// device pixels; a logical number among them would misread every user
|
|
794
|
+
// resize as `_userSized`).
|
|
795
|
+
const geo = scaleWindowGeometry(newProps, this.scale);
|
|
796
|
+
if (sizeChanged) {
|
|
797
|
+
this._userSized = false;
|
|
798
|
+
this._requestedSize = {
|
|
799
|
+
width: isAutoSize(geo.width) ? wnd.width : geo.width,
|
|
800
|
+
height: isAutoSize(geo.height) ? wnd.height : geo.height,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
if (geometryChanged) {
|
|
804
|
+
if (typeof wnd.setState === 'function') {
|
|
805
|
+
wnd.setState({
|
|
806
|
+
x: anchored ? undefined : geo.x,
|
|
807
|
+
y: anchored ? undefined : geo.y,
|
|
808
|
+
// `'auto'` is not a geometry ntk can be given: an axis the app has
|
|
809
|
+
// handed back is left alone here and resolved by `_refit()` on the
|
|
810
|
+
// layout this same commit is about to schedule.
|
|
811
|
+
width: isAutoSize(geo.width) ? undefined : geo.width,
|
|
812
|
+
height: isAutoSize(geo.height) ? undefined : geo.height,
|
|
813
|
+
});
|
|
814
|
+
} else {
|
|
815
|
+
if (sizeChanged && !isAutoSize(geo.width) && !isAutoSize(geo.height)) {
|
|
816
|
+
wnd.resize?.(geo.width, geo.height);
|
|
817
|
+
}
|
|
818
|
+
if (movedByProps) {
|
|
819
|
+
wnd.move?.(geo.x, geo.y);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Re-read every commit: the options object is rebuilt by every render,
|
|
825
|
+
// and a moving `at` — a caret — is the whole point of one. Only the
|
|
826
|
+
// *subscription* is conditional, because only the node it hangs off can
|
|
827
|
+
// make it stale.
|
|
828
|
+
if (newProps.anchor?.to !== before.anchor?.to) this._watchAnchor();
|
|
829
|
+
if (anchored) {
|
|
830
|
+
this._followAnchor();
|
|
831
|
+
} else if (before.anchor) {
|
|
832
|
+
this._placedAt = null;
|
|
833
|
+
// A popup that gives up its anchor gives up being hidden by one: it
|
|
834
|
+
// is an ordinary `x`/`y` popup from here, and the configure above
|
|
835
|
+
// has already put it where the app asked.
|
|
836
|
+
const wasLost = this._anchorLost;
|
|
837
|
+
this._anchorLost = false;
|
|
838
|
+
if (wasLost) this._mapNow();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// After the geometry above on purpose: a window revealed and moved in
|
|
842
|
+
// the same commit is configured first and mapped second, so it is never
|
|
843
|
+
// on screen at the position it was hidden at.
|
|
844
|
+
if (Boolean(newProps.hidden) !== Boolean(before.hidden)) {
|
|
845
|
+
this._applyHidden();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const layoutChanged =
|
|
849
|
+
style !== beforeStyle && applyLayoutStyle(this.yoga, style, beforeStyle);
|
|
850
|
+
// The window's own paint is its background, which covers the whole
|
|
851
|
+
// window — so a change to it is unbounded, and `this` is the right
|
|
852
|
+
// damage. A commit that only changed children reaches here too, though
|
|
853
|
+
// (React updates the parent whenever its child list is rebuilt), and
|
|
854
|
+
// that must not widen the damage those children just recorded.
|
|
855
|
+
const ownPaintChanged = paintPropsChanged(style, beforeStyle);
|
|
856
|
+
// A size change is unbounded — the window's old bounds do not cover the
|
|
857
|
+
// grown area. A style-driven relayout at the same size is bounded by the
|
|
858
|
+
// window itself (its paint covers all of it; NO_DAMAGE alongside a
|
|
859
|
+
// layout change would fall through invalidate's bounds bookkeeping with
|
|
860
|
+
// no rect at all). An x/y-only commit contributes nothing.
|
|
861
|
+
this.invalidate(
|
|
862
|
+
layoutChanged || sizeChanged,
|
|
863
|
+
sizeChanged ? null : ownPaintChanged || layoutChanged ? this : NO_DAMAGE,
|
|
864
|
+
'props',
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* A window is hidden by unmapping it, not by yoga's `display: none`: it is
|
|
870
|
+
* its own layout root, so collapsing it would throw away the arrangement
|
|
871
|
+
* it comes back to — and there is no parent flex line for it to leave.
|
|
872
|
+
* The flag is still recorded, because it is what tells a map that has not
|
|
873
|
+
* gone out yet not to bother.
|
|
874
|
+
*/
|
|
875
|
+
setHidden(hidden) {
|
|
876
|
+
this._reactHidden = hidden;
|
|
877
|
+
this._applyHidden();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Re-derive `this.hidden` from its two writers — the reconciler's flag and
|
|
882
|
+
* the `hidden` prop — and make the window agree. Either saying "hidden"
|
|
883
|
+
* wins, so a `<Suspense>` revealing its content does not map a window the
|
|
884
|
+
* app is holding off screen, and clearing the prop does not map one React
|
|
885
|
+
* still hides.
|
|
886
|
+
*/
|
|
887
|
+
_applyHidden() {
|
|
888
|
+
const hidden = this._reactHidden || Boolean(this.props.hidden);
|
|
889
|
+
if (hidden === this.hidden) return;
|
|
890
|
+
this.hidden = hidden;
|
|
891
|
+
// An unmapped window draws nothing, so a loop inside one is frames
|
|
892
|
+
// nobody sees — the same stop the WM's own minimize gets, by the same
|
|
893
|
+
// route, and the flag is kept true so a later VisibilityNotify agrees
|
|
894
|
+
// with it.
|
|
895
|
+
if (this._loopNodes.size) this._loopVisibilityChanged();
|
|
896
|
+
// An unmapped window is off screen however focused the server thinks it
|
|
897
|
+
// is, and a `<popup>` is worse than that: it shares the owner window's
|
|
898
|
+
// keyboard, so a node inside one that is no longer on screen would go on
|
|
899
|
+
// taking keys the owner window is still receiving.
|
|
900
|
+
this._visibilityChanged(!hidden);
|
|
901
|
+
// A map still queued for the end of this commit reads `hidden` when it
|
|
902
|
+
// runs, so there is nothing to send here — and an unmap sent now would
|
|
903
|
+
// do nothing anyway, the server not having mapped the window yet
|
|
904
|
+
// (issue #201).
|
|
905
|
+
if (pendingMaps.has(this)) return;
|
|
906
|
+
if (hidden) {
|
|
907
|
+
// release before the unmap, the order `_followAnchor` uses — X would
|
|
908
|
+
// drop the grab with the viewability anyway, but not on the mock, and
|
|
909
|
+
// an explicit release is one less state to reason about
|
|
910
|
+
if (this.props.grab) this.window?.ungrabPointer?.();
|
|
911
|
+
this.window?.unmap?.();
|
|
912
|
+
} else if (inCommit) {
|
|
913
|
+
// A reveal mid-commit waits for the end of it the way a fresh window's
|
|
914
|
+
// first map does, so anything later in the same commit that hides the
|
|
915
|
+
// window again (React hides a subtree only after mutating it) is
|
|
916
|
+
// known before the map goes out — the same WM race as issue #201.
|
|
917
|
+
pendingMaps.add(this);
|
|
918
|
+
} else {
|
|
919
|
+
this._mapNow();
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// The rest of WindowNode's methods live with their concerns: the
|
|
925
|
+
// window-only ones beside this file, and its halves of the concerns it
|
|
926
|
+
// shares with Node beside Node's (see install.js).
|
|
927
|
+
installMethods(
|
|
928
|
+
WindowNode,
|
|
929
|
+
WindowCascade,
|
|
930
|
+
WindowQueries,
|
|
931
|
+
WindowAnimation,
|
|
932
|
+
WindowLayoutHost,
|
|
933
|
+
WindowPosition,
|
|
934
|
+
WindowInvalidate,
|
|
935
|
+
WindowScrollBlit,
|
|
936
|
+
WindowPaint,
|
|
937
|
+
WindowListeners,
|
|
938
|
+
WindowHints,
|
|
939
|
+
WindowSize,
|
|
940
|
+
WindowFlush,
|
|
941
|
+
WindowDebugPaint,
|
|
942
|
+
WindowAnchoring,
|
|
943
|
+
WindowCapabilities,
|
|
944
|
+
WindowDropTarget,
|
|
945
|
+
);
|