react-x11 2.47.1 → 2.47.3
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
CHANGED
|
@@ -175,6 +175,11 @@ export class WindowFlush {
|
|
|
175
175
|
// so a frame whose first pass is the floors' own collects once.
|
|
176
176
|
this._floorsCollectOwed = true;
|
|
177
177
|
this._refit();
|
|
178
|
+
// An `'auto'` window waiting for the size its content just asked for
|
|
179
|
+
// lays out when the echo brings it, not at the size it still has
|
|
180
|
+
// (`_holdForRefit`). Nothing here is cleared, so that frame owes
|
|
181
|
+
// everything this one did.
|
|
182
|
+
if (this._refitHolding()) return false;
|
|
178
183
|
}
|
|
179
184
|
const width = this.window.width ?? this._requestedSize?.width ?? 0;
|
|
180
185
|
const height = this.window.height ?? this._requestedSize?.height ?? 0;
|
|
@@ -41,13 +41,16 @@ export class WindowListeners {
|
|
|
41
41
|
// is theirs. Growing it back under a user who has just made it smaller
|
|
42
42
|
// is the one behaviour worse than not fitting the content.
|
|
43
43
|
//
|
|
44
|
-
// Checked against
|
|
45
|
-
// because our own configures come back as echoes, and
|
|
46
|
-
// them would otherwise read as the user taking over on
|
|
47
|
-
// re-fit
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
// Checked against the sizes we asked for rather than ntk's
|
|
45
|
+
// `ev.resized`, because our own configures come back as echoes, and
|
|
46
|
+
// every one of them would otherwise read as the user taking over on
|
|
47
|
+
// the first re-fit — and against all of them still in flight, not
|
|
48
|
+
// only the latest, because a move echo or an earlier re-fit's can land
|
|
49
|
+
// after the record moved on (#929, `_ownSize`).
|
|
50
|
+
if (this._requestedSize) {
|
|
51
|
+
const own = this._ownSize(ev);
|
|
52
|
+
if (!own) this._userSized = true;
|
|
53
|
+
this._settleRefitHold(ev, own);
|
|
51
54
|
}
|
|
52
55
|
// Where the window sits on screen decides where popups anchored to it
|
|
53
56
|
// belong — and finding that out is a server round trip
|
package/src/nodes/window/size.js
CHANGED
|
@@ -46,6 +46,20 @@ const NO_SCOPED_FLOORS = process.env.REACT_X11_NO_SCOPED_FLOORS === '1';
|
|
|
46
46
|
// …and for the width pass reading only the extents a floor is written from
|
|
47
47
|
// (`markUnreadLeaves`): with it set, every leaf is shaped at no width again.
|
|
48
48
|
const NO_UNREAD_WIDTHS = process.env.REACT_X11_NO_UNREAD_WIDTHS === '1';
|
|
49
|
+
// How many sizes an `'auto'` window keeps as asked for and not yet echoed
|
|
50
|
+
// (`_askSize`). Echoes come back within a round trip, so a re-fit a frame
|
|
51
|
+
// is a handful at most.
|
|
52
|
+
const SIZES_IN_FLIGHT = 8;
|
|
53
|
+
|
|
54
|
+
// How long an `'auto'` window's frames wait for the size a re-fit asked for
|
|
55
|
+
// (`_holdForRefit`). A round trip is well under it locally and on a LAN; a
|
|
56
|
+
// slower link runs past it into the frame at the old size, which is the
|
|
57
|
+
// behaviour without the hold. The bound is there for a request answered
|
|
58
|
+
// with no new size at all — a window manager refusing it — where the
|
|
59
|
+
// content would otherwise wait for good.
|
|
60
|
+
const REFIT_HOLD_MS = 150;
|
|
61
|
+
// …and off, for A/B measurement and as first aid.
|
|
62
|
+
const NO_REFIT_HOLD = process.env.REACT_X11_NO_REFIT_HOLD === '1';
|
|
49
63
|
|
|
50
64
|
/** Take a node's extents off, as `collectFloorStale` does a dirty child's:
|
|
51
65
|
* both are measured again, and the minimum yoga holds may no longer be
|
|
@@ -875,6 +889,20 @@ export class WindowSize {
|
|
|
875
889
|
// tree holding the max-content one would hand `flush()` a stale
|
|
876
890
|
// arrangement.
|
|
877
891
|
yoga.calculateLayout(width, needH ? undefined : props.height, dir);
|
|
892
|
+
// **A height floor is a height for a width**, and the ones on hand
|
|
893
|
+
// were measured at the width the window was last laid out at — which
|
|
894
|
+
// is not this one whenever the content asked for a new width. A label
|
|
895
|
+
// that wrapped at the old width carries the two-line floor it earned
|
|
896
|
+
// there, and read back here it is a window two lines tall at a width
|
|
897
|
+
// that fits it on one: the window grew, and shrank again a frame
|
|
898
|
+
// later once the real pass had re-measured (#929). The same probe the
|
|
899
|
+
// real pass asks (`_applyContentFloors`): whether any leaf's height at
|
|
900
|
+
// this width differs from the one its floor was taken at, and only
|
|
901
|
+
// then the measurement and one more pass.
|
|
902
|
+
if (needH && this._probeHeightFloors().owed) {
|
|
903
|
+
measuringExactly(() => this._measureHeightFloors(width, false));
|
|
904
|
+
yoga.calculateLayout(width, undefined, dir);
|
|
905
|
+
}
|
|
878
906
|
const naturalH = needH
|
|
879
907
|
? clampExtent(yoga.getComputedHeight(), undefined, availH)
|
|
880
908
|
: undefined;
|
|
@@ -995,20 +1023,28 @@ export class WindowSize {
|
|
|
995
1023
|
if (asked && next.width === asked.width && next.height === asked.height) {
|
|
996
1024
|
return;
|
|
997
1025
|
}
|
|
998
|
-
this.
|
|
1026
|
+
this._askSize({ width: next.width, height: next.height });
|
|
999
1027
|
// Asked for, not assumed. `window.width` stays what the server last said
|
|
1000
|
-
// until the ConfigureNotify lands
|
|
1001
|
-
// — the
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1004
|
-
//
|
|
1005
|
-
// backing pixmap from the resize event, so a frame painted at a size the
|
|
1006
|
-
// pixmap has not reached yet is a frame clipped to the old one.
|
|
1028
|
+
// until the ConfigureNotify lands — on X11; Cocoa takes a size on the
|
|
1029
|
+
// call — and writing the new size onto the window here would be worse
|
|
1030
|
+
// than waiting: ntk allocates the backing pixmap from the resize event,
|
|
1031
|
+
// so a frame painted at a size the pixmap has not reached yet is a frame
|
|
1032
|
+
// clipped to the old one.
|
|
1007
1033
|
if (typeof wnd.setState === 'function') {
|
|
1008
1034
|
wnd.setState({ width: next.width, height: next.height });
|
|
1009
1035
|
} else {
|
|
1010
1036
|
wnd.resize?.(next.width, next.height);
|
|
1011
1037
|
}
|
|
1038
|
+
// …and laying this frame out at the size the window still has would be
|
|
1039
|
+
// worse again: content that changed *because* it needs more room is
|
|
1040
|
+
// content that does not fit the old size — a label one word longer
|
|
1041
|
+
// wraps, and is cut off at the bottom of a window one line tall — and
|
|
1042
|
+
// that frame went up, for a round trip, before the echo laid it out
|
|
1043
|
+
// again (#929). So the frame waits for the echo (`_holdForRefit`); the
|
|
1044
|
+
// old pixels stay up meanwhile, which is the last thing that was right.
|
|
1045
|
+
if (wnd.width !== next.width || wnd.height !== next.height) {
|
|
1046
|
+
this._holdForRefit(next);
|
|
1047
|
+
}
|
|
1012
1048
|
// A window that grew is a window whose *placement* moved with it, and
|
|
1013
1049
|
// the anchored ones have to be told: a completion list that gains a row
|
|
1014
1050
|
// near the bottom of the screen is one that now flips above the caret.
|
|
@@ -1038,6 +1074,116 @@ export class WindowSize {
|
|
|
1038
1074
|
);
|
|
1039
1075
|
}
|
|
1040
1076
|
|
|
1077
|
+
/**
|
|
1078
|
+
* Hold this window's frames until the backend reports the size `_refit`
|
|
1079
|
+
* just asked for, so the content that asked for it is laid out once, at
|
|
1080
|
+
* that size, and never at the old one. The echo releases the hold (the
|
|
1081
|
+
* 'resize' listener, `_settleRefitHold`), and so does somebody else's size
|
|
1082
|
+
* arriving instead — the user's or a window manager's. A request that is
|
|
1083
|
+
* never answered with a new size is the one case left: a window manager
|
|
1084
|
+
* may refuse it, and its answer then carries the old size, which a move
|
|
1085
|
+
* echo also does. So a timer ends the hold too, and the frame goes up at
|
|
1086
|
+
* whatever size the window has — what every frame did before the hold.
|
|
1087
|
+
*/
|
|
1088
|
+
_holdForRefit(size) {
|
|
1089
|
+
if (NO_REFIT_HOLD) return;
|
|
1090
|
+
clearTimeout(this._refitHeld?.timer);
|
|
1091
|
+
const timer = setTimeout(() => {
|
|
1092
|
+
if (this._refitHeld?.timer !== timer) return;
|
|
1093
|
+
this._releaseRefitHold();
|
|
1094
|
+
}, REFIT_HOLD_MS);
|
|
1095
|
+
timer.unref?.();
|
|
1096
|
+
this._refitHeld = { width: size.width, height: size.height, timer };
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/** Whether this frame waits for the size `_refit` asked for: held, and
|
|
1100
|
+
* the window not that size yet. */
|
|
1101
|
+
_refitHolding() {
|
|
1102
|
+
const held = this._refitHeld;
|
|
1103
|
+
if (!held) return false;
|
|
1104
|
+
const wnd = this.window;
|
|
1105
|
+
if (wnd && (wnd.width !== held.width || wnd.height !== held.height)) {
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
this._releaseRefitHold(false);
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* What a resize event does to a hold: the size it was waiting for, or a
|
|
1114
|
+
* size that is not ours (`own` false), ends it. A move echo at the old
|
|
1115
|
+
* size, or the echo of an earlier re-fit still in flight, does not —
|
|
1116
|
+
* those are exactly the frames the hold is there to skip.
|
|
1117
|
+
*/
|
|
1118
|
+
_settleRefitHold(ev, own) {
|
|
1119
|
+
const held = this._refitHeld;
|
|
1120
|
+
if (!held) return;
|
|
1121
|
+
if (!own || (ev.width === held.width && ev.height === held.height)) {
|
|
1122
|
+
this._releaseRefitHold();
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/** End a hold, and run the frame it held back unless that is the caller's
|
|
1127
|
+
* own (`schedule` false). */
|
|
1128
|
+
_releaseRefitHold(schedule = true) {
|
|
1129
|
+
const held = this._refitHeld;
|
|
1130
|
+
if (!held) return;
|
|
1131
|
+
clearTimeout(held.timer);
|
|
1132
|
+
this._refitHeld = null;
|
|
1133
|
+
if (schedule && !this.destroyed && this.needsLayout) this._scheduleFrame();
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Record a size this window has asked the backend for. `_requestedSize`
|
|
1138
|
+
* is the latest, which `_refit()` compares against to ask only once; the
|
|
1139
|
+
* echoes still to come are kept beside it, because every one of them is
|
|
1140
|
+
* ours and none has to be the latest (`_ownSize`).
|
|
1141
|
+
*/
|
|
1142
|
+
_askSize(size) {
|
|
1143
|
+
this._requestedSize = size;
|
|
1144
|
+
const flight = (this._sizesInFlight ??= []);
|
|
1145
|
+
flight.push(size);
|
|
1146
|
+
// A backend that never echoes — a mock — would grow this for ever, and
|
|
1147
|
+
// a size asked for this many re-fits ago is not coming back.
|
|
1148
|
+
if (flight.length > SIZES_IN_FLIGHT) flight.shift();
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Whether a resize event's size is one this window chose, as opposed to
|
|
1153
|
+
* the user or the window manager choosing it — the question that ends an
|
|
1154
|
+
* `'auto'` window's tracking for good. Settles the record either way.
|
|
1155
|
+
*
|
|
1156
|
+
* Not "is it the latest size asked for" (#929). Two kinds of echo are ours
|
|
1157
|
+
* and carry another size. A **move** reports the size the window has, and
|
|
1158
|
+
* when it reached the server before a resize that was already asked for —
|
|
1159
|
+
* a popup re-anchored by the same re-fit, an `x` prop moving with the
|
|
1160
|
+
* content — it lands after the record moved on. And a re-fit asked for
|
|
1161
|
+
* before the last one was answered has its echo arrive with the size it
|
|
1162
|
+
* asked for, not the one asked for since. So an event is ours when its
|
|
1163
|
+
* size is the one the server last reported (nothing was resized at all:
|
|
1164
|
+
* a move, or a request the manager refused), or one still in flight —
|
|
1165
|
+
* which spends it, and every one asked for before it, since the server
|
|
1166
|
+
* answers in order. Anything else is somebody else's size: the user and
|
|
1167
|
+
* a window manager can only ever *change* it.
|
|
1168
|
+
*/
|
|
1169
|
+
_ownSize(ev) {
|
|
1170
|
+
const is = (size) =>
|
|
1171
|
+
size && size.width === ev.width && size.height === ev.height;
|
|
1172
|
+
const confirmed = this._confirmedSize;
|
|
1173
|
+
this._confirmedSize = { width: ev.width, height: ev.height };
|
|
1174
|
+
// the confirmed size first: a size that came round again (A → B → A)
|
|
1175
|
+
// is in flight too, and spending it there would spend B with it
|
|
1176
|
+
if (is(confirmed)) return true;
|
|
1177
|
+
const flight = this._sizesInFlight ?? [];
|
|
1178
|
+
const at = flight.findIndex(is);
|
|
1179
|
+
if (at < 0) {
|
|
1180
|
+
flight.length = 0;
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
flight.splice(0, at + 1);
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1041
1187
|
/** One layout pass at the window's size, with the content floors it
|
|
1042
1188
|
* needs: fresh ones when something changed them, the ones in hand during
|
|
1043
1189
|
* a live resize, none when nothing moved them. */
|
|
@@ -271,6 +271,13 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
271
271
|
// the only thing that ever does is the user dragging an edge.
|
|
272
272
|
this._requestedSize = null;
|
|
273
273
|
this._userSized = false;
|
|
274
|
+
// what the server last reported, and the sizes asked for since that it
|
|
275
|
+
// has not echoed yet: both are ours when an echo carries them (_ownSize)
|
|
276
|
+
this._confirmedSize = null;
|
|
277
|
+
this._sizesInFlight = [];
|
|
278
|
+
// the size `_refit` is waiting for before it lays out again, and the
|
|
279
|
+
// timer that gives up on it (_holdForRefit)
|
|
280
|
+
this._refitHeld = null;
|
|
274
281
|
// the last `WM_NORMAL_HINTS` struct written and the size it was written
|
|
275
282
|
// at, so that a bound measured every frame is only *sent* on the frames
|
|
276
283
|
// it moves (see _sendSizeHints for why the size is part of it)
|
|
@@ -409,6 +416,7 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
409
416
|
// resize echo is compared against, so it has to be the size the echo
|
|
410
417
|
// will carry (#586, `_snapSize`).
|
|
411
418
|
this._requestedSize = { width: wnd.width, height: wnd.height };
|
|
419
|
+
this._confirmedSize = this._requestedSize;
|
|
412
420
|
// Now that the visual is known: settle the capabilities, re-resolve any
|
|
413
421
|
// `@supports` block against them, and start following the compositor.
|
|
414
422
|
// Before the first paint, and before children realize against it.
|
|
@@ -862,10 +870,12 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
862
870
|
const geo = scaleWindowGeometry(newProps, this.scale);
|
|
863
871
|
if (sizeChanged) {
|
|
864
872
|
this._userSized = false;
|
|
865
|
-
this.
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
873
|
+
this._askSize(
|
|
874
|
+
this._snapSize({
|
|
875
|
+
width: isAutoSize(geo.width) ? wnd.width : geo.width,
|
|
876
|
+
height: isAutoSize(geo.height) ? wnd.height : geo.height,
|
|
877
|
+
}),
|
|
878
|
+
);
|
|
869
879
|
}
|
|
870
880
|
if (geometryChanged) {
|
|
871
881
|
if (typeof wnd.setState === 'function') {
|