react-x11 2.24.0 → 2.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/compositing.js +21 -1
- package/src/nodes/paint.js +38 -13
- package/src/nodes/scrollable.js +16 -6
- package/src/nodes/text.js +80 -3
- package/src/nodes/window/floors.js +1 -1
- package/src/nodes/window/size.js +56 -32
- package/src/nodes/window/spine.js +143 -10
- package/src/xsettings.js +29 -3
package/package.json
CHANGED
package/src/compositing.js
CHANGED
|
@@ -40,6 +40,9 @@ class CompositingSession {
|
|
|
40
40
|
this.stopped = false;
|
|
41
41
|
this._window = null;
|
|
42
42
|
this._selection = null;
|
|
43
|
+
/** What `stop` takes off the connection: the listener and the XFixes
|
|
44
|
+
* address window `watchSelection` put on it. */
|
|
45
|
+
this._teardown = [];
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
_set(supported) {
|
|
@@ -64,6 +67,16 @@ class CompositingSession {
|
|
|
64
67
|
stop() {
|
|
65
68
|
this.stopped = true;
|
|
66
69
|
this.listeners.clear();
|
|
70
|
+
// Taken off, not just ignored — the same as XSETTINGS's session: an app
|
|
71
|
+
// that mounts one root after another on one connection gained a
|
|
72
|
+
// listener and a window a root.
|
|
73
|
+
for (const undo of this._teardown.splice(0)) {
|
|
74
|
+
try {
|
|
75
|
+
undo();
|
|
76
|
+
} catch {
|
|
77
|
+
// a connection already gone takes all of it with it
|
|
78
|
+
}
|
|
79
|
+
}
|
|
67
80
|
}
|
|
68
81
|
}
|
|
69
82
|
|
|
@@ -144,13 +157,20 @@ async function watchSelection(app, session, atom) {
|
|
|
144
157
|
fixes.SelectionEventMask.SetSelectionOwner |
|
|
145
158
|
fixes.SelectionEventMask.SelectionWindowDestroy |
|
|
146
159
|
fixes.SelectionEventMask.SelectionClientClose;
|
|
147
|
-
|
|
160
|
+
const onOwner = (ev) => {
|
|
148
161
|
if (session.stopped) return;
|
|
149
162
|
// XFixes events carry a server-assigned type above the core range, so
|
|
150
163
|
// this cannot be confused with core SelectionNotify (31)
|
|
151
164
|
if (ev.type !== fixes.firstEvent) return;
|
|
152
165
|
if (ev.selection !== atom) return;
|
|
153
166
|
session._set(Boolean(ev.owner));
|
|
167
|
+
};
|
|
168
|
+
X.on('event', onOwner);
|
|
169
|
+
session._teardown.push(() => {
|
|
170
|
+
X.removeListener('event', onOwner);
|
|
171
|
+
// the selection input goes with the window it was addressed to
|
|
172
|
+
X.DestroyWindow(id);
|
|
173
|
+
session._window = null;
|
|
154
174
|
});
|
|
155
175
|
fixes.SelectSelectionInput(id, atom, mask);
|
|
156
176
|
}
|
package/src/nodes/paint.js
CHANGED
|
@@ -135,9 +135,9 @@ export class NodePaint {
|
|
|
135
135
|
* wherever nothing inside it overlaps.
|
|
136
136
|
*/
|
|
137
137
|
_paintGroup(ctx, opacity) {
|
|
138
|
-
|
|
138
|
+
let rect = this._groupRect();
|
|
139
139
|
if (!rect) return;
|
|
140
|
-
|
|
140
|
+
let surface = this._groupSurfaceFor(rect.width, rect.height);
|
|
141
141
|
const alpha = ctx.globalAlpha ?? 1;
|
|
142
142
|
if (!surface) {
|
|
143
143
|
ctx.globalAlpha = alpha * opacity;
|
|
@@ -148,17 +148,32 @@ export class NodePaint {
|
|
|
148
148
|
}
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
151
|
+
const draw = () =>
|
|
152
|
+
surface.render((sctx) => {
|
|
153
|
+
sctx.clearRect(0, 0, rect.width, rect.height);
|
|
154
|
+
sctx.translate(-rect.x, -rect.y);
|
|
155
|
+
const damage = this.root?._paintDamage;
|
|
156
|
+
if (damage) {
|
|
157
|
+
sctx.beginPath();
|
|
158
|
+
sctx.rect(damage.x, damage.y, damage.width, damage.height);
|
|
159
|
+
sctx.clip();
|
|
160
|
+
}
|
|
161
|
+
this._paintNode(sctx);
|
|
162
|
+
});
|
|
163
|
+
draw();
|
|
164
|
+
// A paragraph learns how far its ink reaches past its box by drawing
|
|
165
|
+
// (`TextNode._keepInkReach`). One its paint lays out at a width nothing
|
|
166
|
+
// measured it at — a box of no width is drawn unwrapped — learns it only
|
|
167
|
+
// here, after the surface was sized, which cut its overflow off on the
|
|
168
|
+
// first frame and drew it whole on every frame after. Drawn once more,
|
|
169
|
+
// at the size it turned out to need: the one time it is not known.
|
|
170
|
+
const after = this._groupRect();
|
|
171
|
+
if (after && !rectContains(rect, after)) {
|
|
172
|
+
rect = after;
|
|
173
|
+
surface = this._groupSurfaceFor(rect.width, rect.height);
|
|
174
|
+
if (!surface) return;
|
|
175
|
+
draw();
|
|
176
|
+
}
|
|
162
177
|
ctx.globalAlpha = alpha * opacity;
|
|
163
178
|
try {
|
|
164
179
|
ctx.drawImage(surface, rect.x, rect.y);
|
|
@@ -522,6 +537,16 @@ export class NodePaint {
|
|
|
522
537
|
}
|
|
523
538
|
}
|
|
524
539
|
|
|
540
|
+
/** Whether `outer` holds all of `inner`. */
|
|
541
|
+
function rectContains(outer, inner) {
|
|
542
|
+
return (
|
|
543
|
+
inner.x >= outer.x &&
|
|
544
|
+
inner.y >= outer.y &&
|
|
545
|
+
inner.x + inner.width <= outer.x + outer.width &&
|
|
546
|
+
inner.y + inner.height <= outer.y + outer.height
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
525
550
|
/** WindowNode's half of the paint walk, installed onto `WindowNode.prototype` by window/window.js. */
|
|
526
551
|
export class WindowPaint {
|
|
527
552
|
/** Repaint one damage rect, or the whole window when `damage` is null. */
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -67,6 +67,8 @@ function contentReach(node, top) {
|
|
|
67
67
|
const boxes = layoutDiff.boxes;
|
|
68
68
|
for (const child of node.children) {
|
|
69
69
|
if (child.isWindow || !child.yoga || child.hidden) continue;
|
|
70
|
+
// not laid out, and not there: see `absolutize`
|
|
71
|
+
if (child.style.display === 'none') continue;
|
|
70
72
|
const box = child.yoga.getComputedLayout();
|
|
71
73
|
// …for the walk that places this child, later in the same one
|
|
72
74
|
boxes?.set(child, box);
|
|
@@ -239,12 +241,20 @@ export const Scrollable = (Base) =>
|
|
|
239
241
|
try {
|
|
240
242
|
this._assignAbs(x, y, width, height);
|
|
241
243
|
if (this.props.onLayout) this._reportLayout();
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
// A `display: 'none'` subtree takes part in no layout: yoga leaves
|
|
245
|
+
// its children where the last pass that reached them did — and a
|
|
246
|
+
// child inserted or moved while it was hidden with no layout at all,
|
|
247
|
+
// NaN, which a scroll pane in there measured its content from and
|
|
248
|
+
// threw on, out of the frame. The box itself has its empty rect;
|
|
249
|
+
// what is inside is placed when it is shown and laid out again.
|
|
250
|
+
if (this.style.display !== 'none') {
|
|
251
|
+
this._absolutizeChildren(
|
|
252
|
+
this.abs.x,
|
|
253
|
+
this.abs.y,
|
|
254
|
+
this.abs.x - wasX,
|
|
255
|
+
this.abs.y - wasY,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
248
258
|
} finally {
|
|
249
259
|
if (move) {
|
|
250
260
|
layoutDiff.shift = null;
|
package/src/nodes/text.js
CHANGED
|
@@ -52,6 +52,72 @@ function outside(ink, box) {
|
|
|
52
52
|
/** `TextNode._trim`'s answer, per layout. */
|
|
53
53
|
const trims = new WeakMap();
|
|
54
54
|
|
|
55
|
+
/** {@link glyphInk}'s answer, per layout. */
|
|
56
|
+
const glyphInks = new WeakMap();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Where a layout's glyphs put ink past its lines' boxes, from its origin —
|
|
60
|
+
* or null where the engine cannot say. A glyph inks past its advance: a `j`
|
|
61
|
+
* that starts a line hooks back over the line's start and an `f` that ends
|
|
62
|
+
* one reaches past its end, by their own boxes (`glyphExtents`, ntk's). And
|
|
63
|
+
* a line set tighter than its face has ascenders over its top and
|
|
64
|
+
* descenders under its bottom, by the face's own reach (`metrics`).
|
|
65
|
+
*
|
|
66
|
+
* Found once per layout, and only where it can leave the layout's box: at a
|
|
67
|
+
* line's two ends, and over the first line and under the last. A glyph an
|
|
68
|
+
* em inside a line's ends reaches neither end, and measuring every glyph
|
|
69
|
+
* cost about as much again as laying the paragraph out.
|
|
70
|
+
*/
|
|
71
|
+
function glyphInk(layout) {
|
|
72
|
+
const known = glyphInks.get(layout);
|
|
73
|
+
if (known !== undefined) return known;
|
|
74
|
+
let left = Infinity;
|
|
75
|
+
let top = Infinity;
|
|
76
|
+
let right = -Infinity;
|
|
77
|
+
let bottom = -Infinity;
|
|
78
|
+
const lines = layout.lines;
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
const line = lines[i];
|
|
81
|
+
const outer = i === 0 || i === lines.length - 1;
|
|
82
|
+
const start = line.x;
|
|
83
|
+
const end = line.x + line.width;
|
|
84
|
+
for (const placed of line.runs ?? []) {
|
|
85
|
+
const run = placed.run;
|
|
86
|
+
if (!run?.glyphs) continue;
|
|
87
|
+
if (outer) {
|
|
88
|
+
const m = run.font?.metrics?.(run.size);
|
|
89
|
+
if (m && m.ascent >= 0 && m.descent >= 0) {
|
|
90
|
+
top = Math.min(top, line.baseline - m.ascent);
|
|
91
|
+
bottom = Math.max(bottom, line.baseline + m.descent);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// the pen as `layout.draw` moves it
|
|
95
|
+
let pen = line.x + placed.x;
|
|
96
|
+
for (const glyph of run.glyphs) {
|
|
97
|
+
const gx = pen + glyph.dx;
|
|
98
|
+
const gy = line.baseline - glyph.dy;
|
|
99
|
+
pen += glyph.ax;
|
|
100
|
+
if (gx - run.size > start && gx + run.size < end) continue;
|
|
101
|
+
// a face that cannot say leaves its glyphs to the lines' boxes, and
|
|
102
|
+
// a blank glyph — a space — has no ink to reach anywhere with
|
|
103
|
+
const face = glyph.font ?? run.font;
|
|
104
|
+
const e = face?.glyphExtents?.(glyph.id, run.size);
|
|
105
|
+
if (!e || !(e.maxX > e.minX && e.maxY > e.minY)) continue;
|
|
106
|
+
left = Math.min(left, gx + e.minX);
|
|
107
|
+
right = Math.max(right, gx + e.maxX);
|
|
108
|
+
if (outer) {
|
|
109
|
+
top = Math.min(top, gy + e.minY);
|
|
110
|
+
bottom = Math.max(bottom, gy + e.maxY);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const ink =
|
|
116
|
+
left <= right || top <= bottom ? { left, top, right, bottom } : null;
|
|
117
|
+
glyphInks.set(layout, ink);
|
|
118
|
+
return ink;
|
|
119
|
+
}
|
|
120
|
+
|
|
55
121
|
/** The size under which this connection's text is painted as strips. */
|
|
56
122
|
export function setTextStripBelow(app, below) {
|
|
57
123
|
if (below === undefined) return;
|
|
@@ -739,7 +805,8 @@ export class TextNode extends Node {
|
|
|
739
805
|
if (placed) layoutDiff.sink(insetRect(placed, -DAMAGE_SLOP), this);
|
|
740
806
|
}
|
|
741
807
|
|
|
742
|
-
/** The rect the lines of a placed layout cover,
|
|
808
|
+
/** The rect the lines of a placed layout cover, and their glyphs' ink
|
|
809
|
+
* where it reaches past them, in window coordinates. */
|
|
743
810
|
_inkOf({ layout, x, y }) {
|
|
744
811
|
const lines = layout.lines;
|
|
745
812
|
if (!lines?.length) return null;
|
|
@@ -749,11 +816,21 @@ export class TextNode extends Node {
|
|
|
749
816
|
left = Math.min(left, line.x);
|
|
750
817
|
right = Math.max(right, line.x + line.width);
|
|
751
818
|
}
|
|
819
|
+
let top = 0;
|
|
820
|
+
let bottom = layout.height;
|
|
821
|
+
const glyphs = glyphInk(layout);
|
|
822
|
+
if (glyphs) {
|
|
823
|
+
// each is ±Infinity where nothing was found to reach that way
|
|
824
|
+
left = Math.min(left, glyphs.left);
|
|
825
|
+
right = Math.max(right, glyphs.right);
|
|
826
|
+
top = Math.min(top, glyphs.top);
|
|
827
|
+
bottom = Math.max(bottom, glyphs.bottom);
|
|
828
|
+
}
|
|
752
829
|
return {
|
|
753
830
|
x: x + left,
|
|
754
|
-
y,
|
|
831
|
+
y: y + top,
|
|
755
832
|
width: right - left,
|
|
756
|
-
height:
|
|
833
|
+
height: bottom - top,
|
|
757
834
|
};
|
|
758
835
|
}
|
|
759
836
|
|
|
@@ -56,7 +56,7 @@ export function captureLeafHeights(node, out) {
|
|
|
56
56
|
* is back to meaning only how eagerly the space *above* the floor is given
|
|
57
57
|
* up.
|
|
58
58
|
*/
|
|
59
|
-
function namesOwnFloor(node, axis) {
|
|
59
|
+
export function namesOwnFloor(node, axis) {
|
|
60
60
|
const style = node.style;
|
|
61
61
|
if (style.overflow === 'scroll' || style.overflow === 'hidden') return true;
|
|
62
62
|
return (
|
package/src/nodes/window/size.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// floors, laying the root out, deferring floors during a live resize and
|
|
3
3
|
// catching up after, and refitting the window to what it holds.
|
|
4
4
|
|
|
5
|
-
import { measuringExactly } from '../../styles.js';
|
|
5
|
+
import { exactLayoutConfig, measuringExactly } from '../../styles.js';
|
|
6
6
|
import { availableArea } from '../../screens.js';
|
|
7
7
|
import { Yoga } from '../../yoga.js';
|
|
8
8
|
import {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
columnHeightSpan,
|
|
12
12
|
markUnreadLeaves,
|
|
13
13
|
declaresOwnMinimum,
|
|
14
|
+
namesOwnFloor,
|
|
14
15
|
dirtyOutside,
|
|
15
16
|
floorBoundaryOf,
|
|
16
17
|
probeHeightFloors,
|
|
@@ -327,12 +328,18 @@ export class WindowSize {
|
|
|
327
328
|
|
|
328
329
|
/**
|
|
329
330
|
* One spine root's floors, measured with it laid out alone — at the width
|
|
330
|
-
* it has in each of the passes over the whole tree (`spine.js`), and
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
331
|
+
* it has in each of the passes over the whole tree (`spine.js`), and, for
|
|
332
|
+
* its heights, as the one item of a column that offers it what each of
|
|
333
|
+
* those passes does: no height in the first, and in the collapse what
|
|
334
|
+
* the column it sits in offers there (`frameFor`), which is 0 under the
|
|
335
|
+
* window. Laid out as an item rather than as a root, it is resolved as
|
|
336
|
+
* its column resolves it there — a basis counts, a floor of its own is
|
|
337
|
+
* given way to — and what it holds is laid out in the bound yoga passes
|
|
338
|
+
* down, which squashes a pane in it that names a floor: laid out with no
|
|
339
|
+
* height at all, a <Tree>'s pane came out as tall as its 100,000 rows.
|
|
340
|
+
* Its own extents are written here: unlike a box that sizes itself, a
|
|
341
|
+
* root's are its content's, and the column it sits in is summed from
|
|
342
|
+
* them (`_sumSpine`).
|
|
336
343
|
*/
|
|
337
344
|
_measureSpineRoot(root, widths, axis, probe = false) {
|
|
338
345
|
onExactCopy(root, () =>
|
|
@@ -356,33 +363,50 @@ export class WindowSize {
|
|
|
356
363
|
restoreShrink(shrunk);
|
|
357
364
|
return;
|
|
358
365
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
// the column it is laid out in (`frameFor`), as wide as the one it sits in
|
|
367
|
+
const column = Yoga.Node.create(exactLayoutConfig());
|
|
368
|
+
if (widths.frame.scrolls) column.setOverflow(Yoga.OVERFLOW_SCROLL);
|
|
369
|
+
column.insertChild(yoga, 0);
|
|
370
|
+
const pass = (height) => {
|
|
371
|
+
this._scopedFloorPasses += 1;
|
|
372
|
+
column.calculateLayout(widths.width, height, dir);
|
|
373
|
+
};
|
|
374
|
+
try {
|
|
375
|
+
pass(undefined);
|
|
376
|
+
if (probe) {
|
|
377
|
+
const hit = { marked: false, owed: false };
|
|
378
|
+
probeHeightFloors(root, this, hit, root);
|
|
379
|
+
if (hit.marked) {
|
|
380
|
+
this._writeFloors('height');
|
|
381
|
+
pass(undefined);
|
|
382
|
+
}
|
|
368
383
|
}
|
|
384
|
+
const intrinsic = new Map();
|
|
385
|
+
captureLeafHeights(root, intrinsic);
|
|
386
|
+
const frozen = [];
|
|
387
|
+
freezeWidths(root, frozen);
|
|
388
|
+
const shrunk = [];
|
|
389
|
+
setMeasuringShrink(root, axis, shrunk);
|
|
390
|
+
// …and gives way in it as `setMeasuringShrink` has everything else
|
|
391
|
+
// give way: to a floor it names, and to nothing else
|
|
392
|
+
const shrink = namesOwnFloor(root, axis) ? 1 : 0;
|
|
393
|
+
if ((root.style.flexShrink ?? 1) !== shrink) {
|
|
394
|
+
yoga.setFlexShrink(shrink);
|
|
395
|
+
shrunk.push(root);
|
|
396
|
+
}
|
|
397
|
+
pass(widths.frame.height);
|
|
398
|
+
const span = contentSpan(root, axis, intrinsic, this);
|
|
399
|
+
root._floorH = declaresOwnMinimum(root, axis)
|
|
400
|
+
? yoga.getComputedHeight()
|
|
401
|
+
: span;
|
|
402
|
+
root._floorAtW = yoga.getComputedWidth();
|
|
403
|
+
this._floorsMeasured += 1;
|
|
404
|
+
restoreWidths(frozen);
|
|
405
|
+
restoreShrink(shrunk);
|
|
406
|
+
} finally {
|
|
407
|
+
column.removeChild(yoga);
|
|
408
|
+
column.free();
|
|
369
409
|
}
|
|
370
|
-
const intrinsic = new Map();
|
|
371
|
-
captureLeafHeights(root, intrinsic);
|
|
372
|
-
const frozen = [];
|
|
373
|
-
freezeWidths(root, frozen);
|
|
374
|
-
const shrunk = [];
|
|
375
|
-
setMeasuringShrink(root, axis, shrunk);
|
|
376
|
-
this._scopedFloorPasses += 1;
|
|
377
|
-
yoga.calculateLayout(widths.width, undefined, dir);
|
|
378
|
-
const span = contentSpan(root, axis, intrinsic, this);
|
|
379
|
-
root._floorH = declaresOwnMinimum(root, axis)
|
|
380
|
-
? yoga.getComputedHeight()
|
|
381
|
-
: span;
|
|
382
|
-
root._floorAtW = yoga.getComputedWidth();
|
|
383
|
-
this._floorsMeasured += 1;
|
|
384
|
-
restoreWidths(frozen);
|
|
385
|
-
restoreShrink(shrunk);
|
|
386
410
|
}
|
|
387
411
|
|
|
388
412
|
/** The columns of this frame's spines, deepest first, summed from their
|
|
@@ -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
|
}
|
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,
|