react-x11 2.24.0 → 2.25.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 +5 -5
- package/src/backend/context2d.js +368 -20
- package/src/backend/shadowtiles.js +84 -0
- package/src/compositing.js +21 -1
- package/src/events.js +42 -0
- package/src/node.d.ts +20 -1
- package/src/nodes/boxpaint.js +32 -0
- package/src/nodes/invalidate.js +23 -0
- package/src/nodes/node.js +6 -1
- package/src/nodes/paint.js +38 -13
- package/src/nodes/scrollable.js +16 -6
- package/src/nodes/scrollblit.js +38 -1
- package/src/nodes/text.js +80 -3
- package/src/nodes/window/floors.js +1 -1
- package/src/nodes/window/flush.js +13 -1
- package/src/nodes/window/size.js +56 -32
- package/src/nodes/window/spine.js +143 -10
- package/src/nodes/window/window.js +6 -0
- package/src/xsettings.js +29 -3
|
@@ -15,8 +15,13 @@
|
|
|
15
15
|
//
|
|
16
16
|
// - Down a column whose height is its content's, the measuring passes have
|
|
17
17
|
// no free space to squash into, so a block in it is laid out at its
|
|
18
|
-
// content height in every pass.
|
|
19
|
-
//
|
|
18
|
+
// content height in every pass. What it holds is laid out in the height
|
|
19
|
+
// the pass offers the column, though — none under a pane that scrolls,
|
|
20
|
+
// and 0 under the window, which the collapse lays out at 0 and every box
|
|
21
|
+
// on the way down passes on as a bound — and a bound is not nothing: a
|
|
22
|
+
// pane in the block that names a floor is squashed to it, and a basis
|
|
23
|
+
// counts. Laid out alone as the one item of a column that offers the
|
|
24
|
+
// same (`frameFor`), the block comes out the same.
|
|
20
25
|
// - Across, a block stretched over its column is as wide as the column's
|
|
21
26
|
// content box, in every pass: now, at the width the heights are measured
|
|
22
27
|
// for, and in the width pass, whose width the way down from the root
|
|
@@ -30,7 +35,12 @@
|
|
|
30
35
|
// Anything else — a row on the way, a margin or a width that is not a
|
|
31
36
|
// length — is not a spine, and the tree is measured whole.
|
|
32
37
|
|
|
33
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
declaresOwnMinimum,
|
|
40
|
+
inFlow,
|
|
41
|
+
mainAxisOf,
|
|
42
|
+
namesOwnFloor,
|
|
43
|
+
} from './floors.js';
|
|
34
44
|
import { Yoga } from '../../yoga.js';
|
|
35
45
|
|
|
36
46
|
const LENGTHS_ACROSS = [
|
|
@@ -149,7 +159,7 @@ export function spineScope(root, sources, listOnly) {
|
|
|
149
159
|
if (!child.yoga || child.isWindow || child.style.display === 'none') {
|
|
150
160
|
continue;
|
|
151
161
|
}
|
|
152
|
-
const widths = rootWidths(child, offered);
|
|
162
|
+
const widths = rootWidths(child, offered, node);
|
|
153
163
|
if (widths === null) return null;
|
|
154
164
|
roots.set(child, widths);
|
|
155
165
|
}
|
|
@@ -161,6 +171,11 @@ export function spineScope(root, sources, listOnly) {
|
|
|
161
171
|
return false;
|
|
162
172
|
};
|
|
163
173
|
for (const node of [...roots.keys()]) if (within(node)) roots.delete(node);
|
|
174
|
+
// …and one that is not has to be laid out alone as the whole tree lays it
|
|
175
|
+
// out, which only the pass knows for some (`frameFor`)
|
|
176
|
+
for (const [node, widths] of roots) {
|
|
177
|
+
if (!heightHolds(node, widths.frame)) return null;
|
|
178
|
+
}
|
|
164
179
|
for (const node of [...spine]) {
|
|
165
180
|
if (roots.has(node) || within(node)) spine.delete(node);
|
|
166
181
|
}
|
|
@@ -186,6 +201,121 @@ export function spineScope(root, sources, listOnly) {
|
|
|
186
201
|
};
|
|
187
202
|
}
|
|
188
203
|
|
|
204
|
+
/**
|
|
205
|
+
* The column a root is laid out alone as the one item of, for its heights
|
|
206
|
+
* (`_measureSpineRoot`): one that lays it out in what the collapse pass
|
|
207
|
+
* over the whole tree offers the column it sits in.
|
|
208
|
+
*
|
|
209
|
+
* - `COLLAPSED` under the window, which that pass lays out at 0, and under
|
|
210
|
+
* a stop that only names its floors or clips when every box on the way
|
|
211
|
+
* up from it passes that 0 on (`passesBound`). Every column the spine
|
|
212
|
+
* sums passes it on too, having no height of its own — unless it names
|
|
213
|
+
* a basis or an aspect ratio to lay out what it holds in instead.
|
|
214
|
+
* - `UNBOUNDED` in a column under a pane that scrolls, which measures what
|
|
215
|
+
* it holds with no bound. The column is laid out at its content's height,
|
|
216
|
+
* as long as the pane's own item on the way has no basis to start from.
|
|
217
|
+
* - An item of the pane itself is laid out in the pane's height: 0 when
|
|
218
|
+
* the pane is squashed to nothing, as a pane under the window is, which
|
|
219
|
+
* the frame copies by scrolling too (`SQUASHED_PANE`); in any other
|
|
220
|
+
* pane's, which only the pass knows (`OPEN_PANE`).
|
|
221
|
+
*
|
|
222
|
+
* `null` where a box on the way lays out what it holds in a height of its
|
|
223
|
+
* own, which only the pass knows too.
|
|
224
|
+
*/
|
|
225
|
+
function frameFor(column, stop) {
|
|
226
|
+
if (stop.style.overflow === 'scroll') {
|
|
227
|
+
if (column === stop) {
|
|
228
|
+
return boundFrom(stop) ? SQUASHED_PANE : OPEN_PANE;
|
|
229
|
+
}
|
|
230
|
+
for (let node = column; node !== stop; node = node.parent) {
|
|
231
|
+
if (node.style.aspectRatio > 0) return null;
|
|
232
|
+
if (node.parent === stop && basisOf(node.style) !== 'auto') return null;
|
|
233
|
+
}
|
|
234
|
+
return UNBOUNDED;
|
|
235
|
+
}
|
|
236
|
+
for (let node = column; node !== stop; node = node.parent) {
|
|
237
|
+
if (!passesBound(node)) return null;
|
|
238
|
+
}
|
|
239
|
+
return boundFrom(stop) ? COLLAPSED : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const COLLAPSED = Object.freeze({ height: 0, scrolls: false });
|
|
243
|
+
const UNBOUNDED = Object.freeze({ height: undefined, scrolls: false });
|
|
244
|
+
const SQUASHED_PANE = Object.freeze({ height: 0, scrolls: true });
|
|
245
|
+
// laid out with no bound, as `UNBOUNDED` — for the roots that nothing about
|
|
246
|
+
// the pane's height decides (`heightHolds`)
|
|
247
|
+
const OPEN_PANE = Object.freeze({ height: undefined, scrolls: false });
|
|
248
|
+
|
|
249
|
+
/** Whether the collapse's 0 reaches what `node` holds: no box from it up to
|
|
250
|
+
* the window has a height of its own to replace it with, and none above
|
|
251
|
+
* it scrolls, measuring what it holds with no bound. */
|
|
252
|
+
function boundFrom(node) {
|
|
253
|
+
for (let n = node; !n.isWindow; n = n.parent) {
|
|
254
|
+
if (!n.parent) return false;
|
|
255
|
+
if (n !== node && n.style.overflow === 'scroll') return false;
|
|
256
|
+
if (!passesBound(n)) return false;
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Whether a box lays out what it holds in the bound it is laid out in
|
|
263
|
+
* itself: no height of its own named, no floor above nothing, no basis but
|
|
264
|
+
* 0 and no aspect ratio to take one from. Asked of the style, as everything
|
|
265
|
+
* here is — yoga's getters are a call into the engine and an object each,
|
|
266
|
+
* and a fling asks this of every box up to the window — and it is the
|
|
267
|
+
* style's floor that counts: the one the floors wrote is taken off a box a
|
|
268
|
+
* change is inside before anything is measured (`collectFloorStale`), and
|
|
269
|
+
* every box asked here is one.
|
|
270
|
+
*/
|
|
271
|
+
function passesBound(node) {
|
|
272
|
+
const style = node.style;
|
|
273
|
+
if (style.height !== undefined && style.height !== 'auto') return false;
|
|
274
|
+
if (style.minHeight !== undefined && style.minHeight !== 0) return false;
|
|
275
|
+
const basis = basisOf(style);
|
|
276
|
+
if (share(basis) || basis > 0) return false;
|
|
277
|
+
return !(style.aspectRatio > 0);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** A length given as a share of something — a percentage — rather than as
|
|
281
|
+
* a number or `auto`. */
|
|
282
|
+
const share = (value) => typeof value === 'string' && value !== 'auto';
|
|
283
|
+
|
|
284
|
+
/** The basis a style gives a flex item, as yoga takes it: the longhand, or
|
|
285
|
+
* the shorthand's — 0 for a number, `auto` for either keyword. */
|
|
286
|
+
function basisOf(style) {
|
|
287
|
+
if (style.flexBasis !== undefined) return style.flexBasis;
|
|
288
|
+
return typeof style.flex === 'number' ? 0 : 'auto';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Whether a root laid out alone in `frame` comes out as the whole tree lays
|
|
293
|
+
* it out: there is a frame; no length on its height axis is a share of the
|
|
294
|
+
* column it is not laid out in here; and in a pane that is not squashed,
|
|
295
|
+
* whose height only the pass knows, the root's own height is not decided
|
|
296
|
+
* by it — it does not grow into it, give way to it with a floor, or start
|
|
297
|
+
* from a basis.
|
|
298
|
+
*/
|
|
299
|
+
function heightHolds(root, frame) {
|
|
300
|
+
if (frame === null) return false;
|
|
301
|
+
const style = root.style;
|
|
302
|
+
const basis = basisOf(style);
|
|
303
|
+
if (
|
|
304
|
+
share(style.height) ||
|
|
305
|
+
share(style.minHeight) ||
|
|
306
|
+
share(style.maxHeight) ||
|
|
307
|
+
share(basis)
|
|
308
|
+
) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
if (frame !== OPEN_PANE) return true;
|
|
312
|
+
return !(
|
|
313
|
+
root.yoga.getFlexGrow() > 0 ||
|
|
314
|
+
namesOwnFloor(root, 'height') ||
|
|
315
|
+
basis !== 'auto'
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
189
319
|
const OUT_OF_FLOW = 1;
|
|
190
320
|
const RELATIVE_MARGIN = 2;
|
|
191
321
|
const STYLE_FLAGS = new WeakMap();
|
|
@@ -240,7 +370,7 @@ function staleChildren(node) {
|
|
|
240
370
|
* that is not a length, a percentage or `auto`, ends one: `auto` centres the
|
|
241
371
|
* box instead of stretching it.
|
|
242
372
|
*/
|
|
243
|
-
function rootWidths(node, offered) {
|
|
373
|
+
function rootWidths(node, offered, stop) {
|
|
244
374
|
const style = node.style;
|
|
245
375
|
const parent = node.parent;
|
|
246
376
|
if (!parent || !inFlow(node) || node._host !== null) return null;
|
|
@@ -249,12 +379,13 @@ function rootWidths(node, offered) {
|
|
|
249
379
|
if (!stretched(node)) return null;
|
|
250
380
|
if (MARGINS.some((key) => typeof style[key] === 'string')) return null;
|
|
251
381
|
if (PADDINGS.some((key) => typeof style[key] === 'string')) return null;
|
|
252
|
-
return offered.in(parent);
|
|
382
|
+
return offered.in(parent, stop);
|
|
253
383
|
}
|
|
254
384
|
|
|
255
385
|
/**
|
|
256
386
|
* The widths a column offers the roots in it (`rootWidths`), worked out once
|
|
257
|
-
* a column rather than once a root
|
|
387
|
+
* a column rather than once a root — and the column they are laid out in
|
|
388
|
+
* for their heights (`frameFor`), the same for every root in it.
|
|
258
389
|
*
|
|
259
390
|
* Every root in a column is offered the same two, and working out the width
|
|
260
391
|
* pass's walks the way up to the window, with four yoga reads a box for its
|
|
@@ -272,14 +403,16 @@ class OfferedWidths {
|
|
|
272
403
|
this.passWidths = new Map();
|
|
273
404
|
}
|
|
274
405
|
|
|
275
|
-
/** `{ width, widthPass }` for a root in `column`: its content box
|
|
276
|
-
* and in the width pass over the whole tree
|
|
277
|
-
in
|
|
406
|
+
/** `{ width, widthPass, frame }` for a root in `column`: its content box
|
|
407
|
+
* now, and in the width pass over the whole tree; and the column it is
|
|
408
|
+
* laid out in, under `stop`. */
|
|
409
|
+
in(column, stop) {
|
|
278
410
|
let offer = this.offers.get(column);
|
|
279
411
|
if (offer === undefined) {
|
|
280
412
|
offer = {
|
|
281
413
|
width: Math.max(0, column.yoga.getComputedWidth() - this.inset(column)),
|
|
282
414
|
widthPass: this.passContent(column),
|
|
415
|
+
frame: frameFor(column, stop),
|
|
283
416
|
};
|
|
284
417
|
this.offers.set(column, offer);
|
|
285
418
|
}
|
|
@@ -185,11 +185,17 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
185
185
|
// over and its cost is known (`flush`)
|
|
186
186
|
this._inFlush = false;
|
|
187
187
|
this._claimAfterFlush = false;
|
|
188
|
+
// a frame laid out, so what is under the pointer may have moved
|
|
189
|
+
// (`flush`, `EventManager.refreshHover`)
|
|
190
|
+
this._hoverRecheck = false;
|
|
188
191
|
// the nodes answering `opaqueRect()`, and — during a paint pass one of
|
|
189
192
|
// them covers — that node with its ancestors, whose fills are skipped
|
|
190
193
|
// (`_coverFor`, `Node._paintBackground`)
|
|
191
194
|
this._opaqueNodes = new Set();
|
|
192
195
|
this._coverChain = null;
|
|
196
|
+
// the nodes answering `viewportFixedRects()`, whose rects a scroll pane
|
|
197
|
+
// they are in repairs when it blits (`_viewportFixedPins`)
|
|
198
|
+
this._viewportFixedNodes = new Set();
|
|
193
199
|
// Nodes that want the `attention` event (ntk#37) — an
|
|
194
200
|
// `unstable_onAttention` prop,
|
|
195
201
|
// an `:attention` block, or both. Built before the EventManager so the
|
package/src/xsettings.js
CHANGED
|
@@ -127,6 +127,9 @@ class XSettingsSession {
|
|
|
127
127
|
this.stopped = false;
|
|
128
128
|
this._selectionAtom = 0;
|
|
129
129
|
this._propertyAtom = 0;
|
|
130
|
+
/** What `stop` takes off the connection: the event listeners and the
|
|
131
|
+
* XFixes address window `watchOwner` put on it. */
|
|
132
|
+
this._teardown = [];
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
_set(values) {
|
|
@@ -150,6 +153,17 @@ class XSettingsSession {
|
|
|
150
153
|
stop() {
|
|
151
154
|
this.stopped = true;
|
|
152
155
|
this.listeners.clear();
|
|
156
|
+
// Taken off, not just ignored: a listener that checks `stopped` and
|
|
157
|
+
// returns still runs for every event on the connection, and an app that
|
|
158
|
+
// mounts one root after another — a test suite, a hot reload — gained a
|
|
159
|
+
// listener and a window a root.
|
|
160
|
+
for (const undo of this._teardown.splice(0)) {
|
|
161
|
+
try {
|
|
162
|
+
undo();
|
|
163
|
+
} catch {
|
|
164
|
+
// a connection already gone takes all of it with it
|
|
165
|
+
}
|
|
166
|
+
}
|
|
153
167
|
}
|
|
154
168
|
}
|
|
155
169
|
|
|
@@ -238,8 +252,12 @@ async function readSettings(session) {
|
|
|
238
252
|
async function watchOwner(session) {
|
|
239
253
|
const app = session.app;
|
|
240
254
|
const X = app.X;
|
|
255
|
+
// Started without being awaited, so a root unmounted in the meantime has
|
|
256
|
+
// already stopped the session — and a listener added now would be one
|
|
257
|
+
// `stop` never saw.
|
|
258
|
+
if (session.stopped) return;
|
|
241
259
|
|
|
242
|
-
|
|
260
|
+
const onProperty = (ev) => {
|
|
243
261
|
if (session.stopped) return;
|
|
244
262
|
if (
|
|
245
263
|
ev.type === PROPERTY_NOTIFY &&
|
|
@@ -251,7 +269,9 @@ async function watchOwner(session) {
|
|
|
251
269
|
() => {},
|
|
252
270
|
);
|
|
253
271
|
}
|
|
254
|
-
}
|
|
272
|
+
};
|
|
273
|
+
X.on('event', onProperty);
|
|
274
|
+
session._teardown.push(() => X.removeListener('event', onProperty));
|
|
255
275
|
|
|
256
276
|
const fixes = await requireExtension(app, 'fixes');
|
|
257
277
|
if (!fixes || session.stopped) return;
|
|
@@ -261,11 +281,17 @@ async function watchOwner(session) {
|
|
|
261
281
|
X.CreateWindow(id, app.display.screen[0].root, -10, -10, 1, 1, 0, 0, 2, 0, {
|
|
262
282
|
eventMask: 0,
|
|
263
283
|
});
|
|
264
|
-
|
|
284
|
+
const onOwner = (ev) => {
|
|
265
285
|
if (session.stopped) return;
|
|
266
286
|
if (ev.type !== fixes.firstEvent) return;
|
|
267
287
|
if (ev.selection !== session._selectionAtom) return;
|
|
268
288
|
latch(session).catch(() => {});
|
|
289
|
+
};
|
|
290
|
+
X.on('event', onOwner);
|
|
291
|
+
session._teardown.push(() => {
|
|
292
|
+
X.removeListener('event', onOwner);
|
|
293
|
+
// the selection input goes with the window it was addressed to
|
|
294
|
+
X.DestroyWindow(id);
|
|
269
295
|
});
|
|
270
296
|
fixes.SelectSelectionInput(
|
|
271
297
|
id,
|