react-x11 2.22.0 → 2.22.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/nodes/damage.js +43 -23
- package/src/nodes/rects.js +13 -6
- package/src/nodes/scrollblit.js +81 -80
package/package.json
CHANGED
package/src/nodes/damage.js
CHANGED
|
@@ -140,39 +140,59 @@ function coalesceRects(rects) {
|
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
142
|
* Add one rect to a capped, disjoint damage list, returning a new list.
|
|
143
|
-
*
|
|
144
|
-
* Over the cap, the pair whose merge wastes the least area is merged — waste
|
|
145
|
-
* being the area the merged rect covers that neither of the two did, so
|
|
146
|
-
* neighbours go first and far-apart rects last. That merge can itself overlap a
|
|
147
|
-
* third rect, so the result is coalesced again.
|
|
148
143
|
*/
|
|
149
144
|
export function addDamageRect(rects, add, cap = MAX_DAMAGE_RECTS) {
|
|
145
|
+
return addDamageRects(rects, [add], cap);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Add several rects to a capped, disjoint damage list at once, returning a
|
|
150
|
+
* new list.
|
|
151
|
+
*
|
|
152
|
+
* Over the cap, the pair whose merge leaves the least area to paint is
|
|
153
|
+
* merged, so neighbours go first and far-apart rects last. The merged rect
|
|
154
|
+
* can overlap a third one, which it then swallows — and the grown rect a
|
|
155
|
+
* fourth — so a pair is priced by the list it leaves once that has run its
|
|
156
|
+
* course, not by the box around the two. Priced by the box alone, the
|
|
157
|
+
* cheapest pair could be the one whose merge cascades: a rounded pane
|
|
158
|
+
* panned left with furniture in both bottom corners merged its zoom
|
|
159
|
+
* controls into a corner's repair, and the box grew through the minimap
|
|
160
|
+
* and the exposed strip into nearly the whole pane — every frame of the
|
|
161
|
+
* pan going one way was a full repaint (issue #691). Where no merge
|
|
162
|
+
* cascades the two prices order the pairs the same way.
|
|
163
|
+
*
|
|
164
|
+
* Everything a caller has to add goes in one call where it can: added one
|
|
165
|
+
* at a time, each rect is squeezed under the cap as it arrives, and a
|
|
166
|
+
* small one arriving last can only join a box an earlier merge already
|
|
167
|
+
* grew. Offered together, the small ones merge with their neighbours
|
|
168
|
+
* first — an element blit's strips, furniture and corner repairs repaint
|
|
169
|
+
* about a fifth of a pane that way where one at a time took half.
|
|
170
|
+
*/
|
|
171
|
+
export function addDamageRects(rects, adds, cap = MAX_DAMAGE_RECTS) {
|
|
150
172
|
let out = coalesceRects(
|
|
151
|
-
(rects ?? []).concat(
|
|
152
|
-
{ x:
|
|
153
|
-
|
|
173
|
+
(rects ?? []).concat(
|
|
174
|
+
adds.map((r) => ({ x: r.x, y: r.y, width: r.width, height: r.height })),
|
|
175
|
+
),
|
|
154
176
|
);
|
|
155
177
|
while (out.length > cap) {
|
|
156
|
-
let
|
|
157
|
-
let
|
|
158
|
-
let bestWaste = Infinity;
|
|
178
|
+
let best = null;
|
|
179
|
+
let bestArea = Infinity;
|
|
159
180
|
for (let i = 0; i < out.length; i++) {
|
|
160
181
|
for (let j = i + 1; j < out.length; j++) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
182
|
+
const merged = coalesceRects(
|
|
183
|
+
out
|
|
184
|
+
.filter((_, k) => k !== i && k !== j)
|
|
185
|
+
.concat([unionRect(out[i], out[j])]),
|
|
186
|
+
);
|
|
187
|
+
let area = 0;
|
|
188
|
+
for (const r of merged) area += rectArea(r);
|
|
189
|
+
if (area < bestArea) {
|
|
190
|
+
bestArea = area;
|
|
191
|
+
best = merged;
|
|
169
192
|
}
|
|
170
193
|
}
|
|
171
194
|
}
|
|
172
|
-
|
|
173
|
-
out = coalesceRects(
|
|
174
|
-
out.filter((_, k) => k !== bestI && k !== bestJ).concat([merged]),
|
|
175
|
-
);
|
|
195
|
+
out = best;
|
|
176
196
|
}
|
|
177
197
|
return out;
|
|
178
198
|
}
|
package/src/nodes/rects.js
CHANGED
|
@@ -91,12 +91,12 @@ export function rectsBounds(rects) {
|
|
|
91
91
|
return out;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/**
|
|
95
|
-
*
|
|
96
|
-
export function
|
|
94
|
+
/** The four `radius`-sized corner squares of `box` — the only part of a
|
|
95
|
+
* rounded border a translation cannot keep — none when there is no radius. */
|
|
96
|
+
export function cornerSquares(box, radius) {
|
|
97
97
|
const r = Math.min(radius, box.width / 2, box.height / 2);
|
|
98
|
-
if (!(r > 0)) return
|
|
99
|
-
|
|
98
|
+
if (!(r > 0)) return [];
|
|
99
|
+
return [
|
|
100
100
|
{ x: box.x, y: box.y, width: r, height: r },
|
|
101
101
|
{ x: box.x + box.width - r, y: box.y, width: r, height: r },
|
|
102
102
|
{ x: box.x, y: box.y + box.height - r, width: r, height: r },
|
|
@@ -107,7 +107,14 @@ export function cornerSquaresOverlap(box, radius, rect) {
|
|
|
107
107
|
height: r,
|
|
108
108
|
},
|
|
109
109
|
];
|
|
110
|
-
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Does `rect` reach into any of the four `radius`-sized corner squares of
|
|
113
|
+
* `box`? */
|
|
114
|
+
export function cornerSquaresOverlap(box, radius, rect) {
|
|
115
|
+
return cornerSquares(box, radius).some((square) =>
|
|
116
|
+
rectsOverlap(square, rect),
|
|
117
|
+
);
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
/**
|
package/src/nodes/scrollblit.js
CHANGED
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
// viewport, so the fast path can cost correctness nothing.
|
|
9
9
|
|
|
10
10
|
import { EMPTY_STYLE, resolveBorderWidths } from '../styles.js';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
DAMAGE_SLOP,
|
|
13
|
+
MAX_DAMAGE_RECTS,
|
|
14
|
+
addDamageRect,
|
|
15
|
+
addDamageRects,
|
|
16
|
+
} from './damage.js';
|
|
12
17
|
import {
|
|
13
18
|
rectContains,
|
|
14
19
|
isIntegerRect,
|
|
@@ -18,7 +23,7 @@ import {
|
|
|
18
23
|
intersectRects,
|
|
19
24
|
unionRect,
|
|
20
25
|
rectArea,
|
|
21
|
-
|
|
26
|
+
cornerSquares,
|
|
22
27
|
rectsOverlap,
|
|
23
28
|
outside,
|
|
24
29
|
} from './rects.js';
|
|
@@ -61,6 +66,15 @@ const SCROLL_BLIT_MIN_KEEP = 0.5;
|
|
|
61
66
|
// Past this the blit is buying a shift and paying for the viewport anyway.
|
|
62
67
|
const SCROLL_BLIT_MAX_REPAINT = 0.75;
|
|
63
68
|
|
|
69
|
+
// The rects an element blit may leave its frame to paint: the strips the
|
|
70
|
+
// shift exposed, on top of a frame's usual allowance. A diagonal pan of a
|
|
71
|
+
// rounded pane with furniture in two corners owes two strips, two pieces of
|
|
72
|
+
// furniture and four corner repairs; held to four rects they merged into
|
|
73
|
+
// boxes a third of the pane, and a graph panned diagonally at 41fps on
|
|
74
|
+
// XQuartz and 52 on macOS where two more rects make it 60 and 78 (issue
|
|
75
|
+
// #691). Eight bought nothing past six.
|
|
76
|
+
const CONTENTS_BLIT_MAX_RECTS = MAX_DAMAGE_RECTS + 2;
|
|
77
|
+
|
|
64
78
|
// A scroll that must not blit this frame: content inside the viewport
|
|
65
79
|
// already changed (see the arming check in scrollTo, and the claim-time
|
|
66
80
|
// cancel in WindowNode.invalidate — react-x11#295). Truthy on purpose, so
|
|
@@ -214,6 +228,18 @@ function insidePinned(rect, pins) {
|
|
|
214
228
|
return false;
|
|
215
229
|
}
|
|
216
230
|
|
|
231
|
+
/** Does every corner square of a rounded `box` that reaches into `vp` lie
|
|
232
|
+
* inside one of `pins` — repaired after the copy rather than dragged along
|
|
233
|
+
* by it (issue #691)? True when none reaches in. */
|
|
234
|
+
function cornersPinned(box, radius, vp, pins) {
|
|
235
|
+
for (const square of cornerSquares(box, radius)) {
|
|
236
|
+
if (!rectsOverlap(square, vp)) continue;
|
|
237
|
+
const inside = intersectRects(outerPixels(square), vp);
|
|
238
|
+
if (inside && !insidePinned(inside, pins)) return false;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
217
243
|
/** One of `scrollContents`' lists as an array, read once — an iterable may
|
|
218
244
|
* be a generator — null when it was left out, and undefined when it is not
|
|
219
245
|
* a list at all. */
|
|
@@ -1053,48 +1079,20 @@ export class WindowScrollBlit {
|
|
|
1053
1079
|
}
|
|
1054
1080
|
const keep = this._blitKeptDamage(vp, true, target.damage);
|
|
1055
1081
|
if (!keep) return;
|
|
1056
|
-
// An ancestor's rounded corners reach into the
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
: vp;
|
|
1071
|
-
if (shifted.height <= 0 || Math.abs(dy) >= shifted.height) return;
|
|
1072
|
-
if (
|
|
1073
|
-
(shifted.width - Math.abs(dx)) * (shifted.height - Math.abs(dy)) <
|
|
1074
|
-
area * SCROLL_BLIT_MIN_KEEP
|
|
1075
|
-
) {
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
if (!this._scrollBlitSafe(node, shifted, target.top, riders, pinned)) {
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
let rects = keep;
|
|
1082
|
-
if (bands.top) {
|
|
1083
|
-
rects = addDamageRect(rects, {
|
|
1084
|
-
x: vp.x,
|
|
1085
|
-
y: vp.y,
|
|
1086
|
-
width: vp.width,
|
|
1087
|
-
height: bands.top,
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
if (bands.bottom) {
|
|
1091
|
-
rects = addDamageRect(rects, {
|
|
1092
|
-
x: vp.x,
|
|
1093
|
-
y: shifted.y + shifted.height,
|
|
1094
|
-
width: vp.width,
|
|
1095
|
-
height: bands.bottom,
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1082
|
+
// An ancestor's rounded corners reach into the region (a graph pane
|
|
1083
|
+
// inside a rounded card is the common shape). Their arcs do not
|
|
1084
|
+
// translate, but they live in four small squares and nowhere else, so
|
|
1085
|
+
// they are pinned like the element's own furniture (issue #691): the
|
|
1086
|
+
// whole region shifts, and each square is repaired where it is and where
|
|
1087
|
+
// the copy dragged its image. Carved out as the rows they reach into,
|
|
1088
|
+
// they took the region's full width at the top and at the bottom —
|
|
1089
|
+
// every card and edge in those rows repainted through the rounded clip
|
|
1090
|
+
// on every frame — and next to furniture in two corners, one rect more
|
|
1091
|
+
// than the damage cap has room for.
|
|
1092
|
+
const corners = this._cornerPins(node, vp, target.top);
|
|
1093
|
+
const pins =
|
|
1094
|
+
corners && pinned ? [...pinned, ...corners] : (corners ?? pinned);
|
|
1095
|
+
if (!this._scrollBlitSafe(node, vp, target.top, riders, pins)) return;
|
|
1098
1096
|
// The strips the shift exposed, on the sides the pixels came from. The
|
|
1099
1097
|
// horizontal one takes the full width and the vertical one takes what
|
|
1100
1098
|
// is left, so a diagonal shift claims two rects that do not overlap —
|
|
@@ -1103,39 +1101,44 @@ export class WindowScrollBlit {
|
|
|
1103
1101
|
const strips = [];
|
|
1104
1102
|
if (dy !== 0) {
|
|
1105
1103
|
strips.push({
|
|
1106
|
-
x:
|
|
1107
|
-
y: dy > 0 ?
|
|
1108
|
-
width:
|
|
1104
|
+
x: vp.x,
|
|
1105
|
+
y: dy > 0 ? vp.y : vp.y + vp.height + dy,
|
|
1106
|
+
width: vp.width,
|
|
1109
1107
|
height: Math.abs(dy),
|
|
1110
1108
|
});
|
|
1111
1109
|
}
|
|
1112
1110
|
if (dx !== 0) {
|
|
1113
1111
|
strips.push({
|
|
1114
|
-
x: dx > 0 ?
|
|
1115
|
-
y: dy > 0 ?
|
|
1112
|
+
x: dx > 0 ? vp.x : vp.x + vp.width + dx,
|
|
1113
|
+
y: dy > 0 ? vp.y + dy : vp.y,
|
|
1116
1114
|
width: Math.abs(dx),
|
|
1117
|
-
height:
|
|
1115
|
+
height: vp.height - Math.abs(dy),
|
|
1118
1116
|
});
|
|
1119
1117
|
}
|
|
1120
|
-
|
|
1121
|
-
if (
|
|
1118
|
+
const pieces = [...strips];
|
|
1119
|
+
if (pins) {
|
|
1122
1120
|
// Each piece of furniture where it stays, and where the copy put the
|
|
1123
1121
|
// image of it it dragged along — one box, since a pan step moves it a
|
|
1124
|
-
// few pixels, and inside
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
//
|
|
1129
|
-
for (const pin of
|
|
1122
|
+
// few pixels, and inside the region, since the copy wrote nothing
|
|
1123
|
+
// outside. Less the strips, which repaint anyway: furniture flush
|
|
1124
|
+
// against the side the pixels came from would otherwise merge with
|
|
1125
|
+
// the strip down that side into the box of the two, most of the
|
|
1126
|
+
// region's height.
|
|
1127
|
+
for (const pin of pins) {
|
|
1130
1128
|
const image = { ...pin, x: pin.x + dx, y: pin.y + dy };
|
|
1131
|
-
const repair = intersectRects(unionRect(pin, image),
|
|
1129
|
+
const repair = intersectRects(unionRect(pin, image), vp);
|
|
1132
1130
|
if (!repair) continue;
|
|
1133
|
-
let
|
|
1131
|
+
let cut = [repair];
|
|
1134
1132
|
for (const strip of strips) {
|
|
1135
|
-
|
|
1133
|
+
cut = cut.flatMap((piece) => outside(piece, strip));
|
|
1136
1134
|
}
|
|
1137
|
-
|
|
1135
|
+
pieces.push(...cut);
|
|
1138
1136
|
}
|
|
1137
|
+
}
|
|
1138
|
+
// in one call, so the small pieces find their neighbours before the
|
|
1139
|
+
// cap makes anything merge (`addDamageRects`)
|
|
1140
|
+
const rects = addDamageRects(keep, pieces, CONTENTS_BLIT_MAX_RECTS);
|
|
1141
|
+
if (pins) {
|
|
1139
1142
|
// Furniture can be most of a pane, and the damage cap can merge a
|
|
1140
1143
|
// repair with a strip into the box of the two: past this, the blit is
|
|
1141
1144
|
// buying a shift and paying for the region anyway.
|
|
@@ -1149,32 +1152,29 @@ export class WindowScrollBlit {
|
|
|
1149
1152
|
// the element's deltas are already how far the pixels moved, the sense
|
|
1150
1153
|
// scrollRegion takes (0 + x rather than x: a caller's -0 would survive
|
|
1151
1154
|
// into request buffers and test comparisons)
|
|
1152
|
-
if (!target.blit({ ...
|
|
1155
|
+
if (!target.blit({ ...vp }, 0 + dx, 0 + dy)) return;
|
|
1153
1156
|
target.damage = rects;
|
|
1154
1157
|
}
|
|
1155
1158
|
|
|
1156
1159
|
/**
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
* (`_scrollBlitSafe`). Whole pixels, and zero when no corner reaches in.
|
|
1160
|
+
* The squares of `vp` an ancestor's rounded corners reach into — the only
|
|
1161
|
+
* pixels of a rounded outline a translation cannot keep — in whole pixels,
|
|
1162
|
+
* for an element blit to pin (issue #691). Null when no corner reaches in.
|
|
1161
1163
|
* Ancestors below `upTo`, the node the blit's surface belongs to
|
|
1162
1164
|
* (`_blitTarget`): a pane is a rectangle no corner above it clips.
|
|
1163
1165
|
*/
|
|
1164
|
-
|
|
1165
|
-
let
|
|
1166
|
-
let bottom = 0;
|
|
1166
|
+
_cornerPins(node, vp, upTo = this) {
|
|
1167
|
+
let out = null;
|
|
1167
1168
|
for (let n = node.parent; n && n !== upTo; n = n.parent) {
|
|
1168
1169
|
const radius = n.style?.borderRadius ?? 0;
|
|
1169
1170
|
if (!(radius > 0) || !n.abs) continue;
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
);
|
|
1171
|
+
for (const square of cornerSquares(n.abs, radius)) {
|
|
1172
|
+
if (!rectsOverlap(square, vp)) continue;
|
|
1173
|
+
const inside = intersectRects(outerPixels(square), vp);
|
|
1174
|
+
if (inside) (out ??= []).push(inside);
|
|
1175
|
+
}
|
|
1176
1176
|
}
|
|
1177
|
-
return
|
|
1177
|
+
return out;
|
|
1178
1178
|
}
|
|
1179
1179
|
|
|
1180
1180
|
/**
|
|
@@ -1229,10 +1229,11 @@ export class WindowScrollBlit {
|
|
|
1229
1229
|
// radius-sized squares at the corners, and the straight run of
|
|
1230
1230
|
// the edge between them is the border ring already excluded
|
|
1231
1231
|
// above. A viewport that reaches the edge but stays clear of the
|
|
1232
|
-
// squares
|
|
1233
|
-
//
|
|
1232
|
+
// squares is translation-safe, and so is an element blit that
|
|
1233
|
+
// pins the squares it reaches into (`_cornerPins`) — repaired
|
|
1234
|
+
// where they are and where the copy dragged them.
|
|
1234
1235
|
const radius = child.style?.borderRadius ?? 0;
|
|
1235
|
-
if (radius > 0 &&
|
|
1236
|
+
if (radius > 0 && !cornersPinned(child.abs, radius, vp, pinned)) {
|
|
1236
1237
|
return false;
|
|
1237
1238
|
}
|
|
1238
1239
|
if (typeof child._scrollbars === 'function') {
|