react-x11 2.11.0 → 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 +292 -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/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/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 +17 -2
- 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,330 @@
|
|
|
1
|
+
// Size and container queries (#470): the blocks a node's style declares,
|
|
2
|
+
// the container sizes they are answered against, and the passes a window
|
|
3
|
+
// spends settling them.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
applyLayoutStyle,
|
|
7
|
+
localTextStyleChanged,
|
|
8
|
+
containerAnswers,
|
|
9
|
+
} from '../styles.js';
|
|
10
|
+
import { DEV } from './util.js';
|
|
11
|
+
|
|
12
|
+
/** How many extra layout passes a flush spends settling `@container` blocks
|
|
13
|
+
* before it takes the layout it has — see `_settleContainerQueries`. */
|
|
14
|
+
const CONTAINER_QUERY_PASSES = 3;
|
|
15
|
+
|
|
16
|
+
/** The sizes a node's container blocks were resolved against, as one
|
|
17
|
+
* comparable string — what a pinned node is held at. */
|
|
18
|
+
function containersKey(containers) {
|
|
19
|
+
if (!containers) return '';
|
|
20
|
+
let key = '';
|
|
21
|
+
for (const name of Object.keys(containers)) {
|
|
22
|
+
const c = containers[name];
|
|
23
|
+
key += `${name}:${c.width}x${c.height};`;
|
|
24
|
+
}
|
|
25
|
+
return key;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Node's half of size and container queries, installed onto `Node.prototype` by node.js. */
|
|
29
|
+
export class NodeQueries {
|
|
30
|
+
/** Join the owning window's size-query registry, and take the current
|
|
31
|
+
* size into account — a node mounted after a resize has to match against
|
|
32
|
+
* the size the window is now, not the one it started at. */
|
|
33
|
+
_registerSizeQueries() {
|
|
34
|
+
if (this._queried && this.root?._sizeQueryNodes) {
|
|
35
|
+
this.root._sizeQueryNodes.add(this);
|
|
36
|
+
if (this.root.querySize) this._sizeQueriesChanged();
|
|
37
|
+
}
|
|
38
|
+
if (this._supportsQueried && this.root?._supportsQueryNodes) {
|
|
39
|
+
this.root._supportsQueryNodes.add(this);
|
|
40
|
+
// a node mounted into a window that already knows its capabilities
|
|
41
|
+
// has to match against those, not against the startup default
|
|
42
|
+
this._sizeQueriesChanged();
|
|
43
|
+
}
|
|
44
|
+
if (this._wantsAttention && this.root?._attentionNodes) {
|
|
45
|
+
this.root._attentionNodes.add(this);
|
|
46
|
+
}
|
|
47
|
+
if (this._cq !== null && this.root?._containerQueryNodes) {
|
|
48
|
+
this.root._containerQueryNodes.add(this);
|
|
49
|
+
// it can see the containers above it now — the constructor's
|
|
50
|
+
// resolution had no ancestors to find one in
|
|
51
|
+
this._sizeQueriesChanged();
|
|
52
|
+
}
|
|
53
|
+
for (const child of this.children) {
|
|
54
|
+
if (!child.isWindow) child._registerSizeQueries();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The sizes this node's `@container` blocks resolve against: for each name
|
|
60
|
+
* the style asks about, the nearest ancestor declaring it, in this node's
|
|
61
|
+
* **logical** pixels — the unit the threshold beside `width: 400` was
|
|
62
|
+
* written in, so the two numbers mean the same thing. Yoga's computed
|
|
63
|
+
* size rather than `abs`: inside a flush, `abs` is still the previous
|
|
64
|
+
* frame's.
|
|
65
|
+
*
|
|
66
|
+
* A container that has not been laid out yet contributes nothing, so its
|
|
67
|
+
* blocks do not apply — the way a capability block does not before the
|
|
68
|
+
* window exists: the fallback design is the one that works everywhere.
|
|
69
|
+
* "Laid out" is `_placed` between frames and every attached node while
|
|
70
|
+
* the window is settling a pass it just ran (`_cqFresh`); a fresh yoga
|
|
71
|
+
* node answers NaN. Null when no container is known, which is the
|
|
72
|
+
* identity `resolveQueries` keeps.
|
|
73
|
+
*/
|
|
74
|
+
_containerSizes() {
|
|
75
|
+
const names = this._cq?.names;
|
|
76
|
+
if (!names) return null;
|
|
77
|
+
let sizes = null;
|
|
78
|
+
const s = this.scale || 1;
|
|
79
|
+
const fresh = Boolean(this.root?._cqFresh);
|
|
80
|
+
for (const name of names) {
|
|
81
|
+
const c = this._containerFor(name);
|
|
82
|
+
if (!c) {
|
|
83
|
+
// In a window and nothing above declares one: a forgotten
|
|
84
|
+
// declaration, not a component rendered outside its context — that
|
|
85
|
+
// is what the *named* form is for, and a missing name applies
|
|
86
|
+
// nothing quietly. `root` rather than `parent`: React builds a
|
|
87
|
+
// subtree bottom-up, so a node can have a parent and no window yet,
|
|
88
|
+
// and the container it will find is further up.
|
|
89
|
+
if (DEV && name === '' && this.root) this._noContainer();
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!c.yoga || !(fresh || c._placed)) continue;
|
|
93
|
+
const width = c.yoga.getComputedWidth() / s;
|
|
94
|
+
const height = c.yoga.getComputedHeight() / s;
|
|
95
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) continue;
|
|
96
|
+
(sizes ??= {})[name] = { width, height };
|
|
97
|
+
}
|
|
98
|
+
return sizes;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** `_containerSizes()`, unless this node is pinned at the sizes it is
|
|
102
|
+
* looking at — then the sizes its held answer came from, so a restyle
|
|
103
|
+
* arriving from React does not undo what the layout pass decided. */
|
|
104
|
+
_pinnedContainerSizes() {
|
|
105
|
+
const live = this._containerSizes();
|
|
106
|
+
const cq = this._cq;
|
|
107
|
+
const pin = cq.pin;
|
|
108
|
+
if (!pin) return live;
|
|
109
|
+
if (pin.key === containersKey(live)) return pin.containers;
|
|
110
|
+
cq.pin = null;
|
|
111
|
+
return live;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The nearest ancestor whose style declares `container` — any container
|
|
116
|
+
* for the unnamed query (`''`), the one carrying `name` otherwise, however
|
|
117
|
+
* many nearer containers that reaches past. A window ends the walk after
|
|
118
|
+
* offering itself, and a window asks nothing: a `<popup>` inside a
|
|
119
|
+
* container is a root of its own and asks its own window with `@width`.
|
|
120
|
+
*
|
|
121
|
+
* Walked rather than cached: it is a dozen property reads per dependent
|
|
122
|
+
* per layout pass, and a cache would have to follow every insert, every
|
|
123
|
+
* reorder and every `container` value that changes above.
|
|
124
|
+
*/
|
|
125
|
+
_containerFor(name) {
|
|
126
|
+
if (this.isWindow) return null;
|
|
127
|
+
for (let n = this.parent; n; n = n.parent) {
|
|
128
|
+
const c = n.style?.container;
|
|
129
|
+
if (name === '' ? c === true || typeof c === 'string' : c === name) {
|
|
130
|
+
return n;
|
|
131
|
+
}
|
|
132
|
+
if (n.isWindow) break;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_noContainer() {
|
|
138
|
+
this._tokenProblem(
|
|
139
|
+
[
|
|
140
|
+
`react-x11: <${this.kind}> has an "@container" block and no ` +
|
|
141
|
+
'container above it — declare one with `container: true` in an ' +
|
|
142
|
+
"ancestor's style (or name it and ask for it by name), or ask " +
|
|
143
|
+
'the window with "@width"',
|
|
144
|
+
],
|
|
145
|
+
true,
|
|
146
|
+
'The block does not apply and the app carries on',
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Said once per node, in development: a design that cannot settle looks
|
|
151
|
+
* like a layout bug, and the frame it is pinned at is the only clue. */
|
|
152
|
+
_warnContainerOscillation() {
|
|
153
|
+
const cq = this._cq;
|
|
154
|
+
if (cq.warned) return;
|
|
155
|
+
cq.warned = true;
|
|
156
|
+
const asked = [...(cq.names ?? [])]
|
|
157
|
+
.map((n) => (n === '' ? 'its container' : `"${n}"`))
|
|
158
|
+
.join(', ');
|
|
159
|
+
console.warn(
|
|
160
|
+
`react-x11: the "@container" blocks on <${this.kind}> cannot settle: ` +
|
|
161
|
+
`a block that matches at one size of ${asked} changes that size to ` +
|
|
162
|
+
'one where it no longer matches, and back. A container query must ' +
|
|
163
|
+
'not move the size it asks about — give the container a size of its ' +
|
|
164
|
+
'own, or minWidth: 0 and a flexBasis so its content cannot grow it. ' +
|
|
165
|
+
'The current answer is held until the container moves for another ' +
|
|
166
|
+
'reason (docs/styling.md#container-queries).',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The owning window resized, the server's answer moved, or a layout pass
|
|
171
|
+
* moved a container this node asks about: re-resolve, since a query block
|
|
172
|
+
* may now match that did not, or the other way round. */
|
|
173
|
+
_sizeQueriesChanged() {
|
|
174
|
+
if (
|
|
175
|
+
!(this._queried || this._supportsQueried || this._cq !== null) ||
|
|
176
|
+
this.destroyed
|
|
177
|
+
) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const before = this.style;
|
|
181
|
+
// a query block may name `fontSize`, and `_syncStyle` → `_retarget` is
|
|
182
|
+
// what pushes that into the subtree; only the node-local text props are
|
|
183
|
+
// left to notice here
|
|
184
|
+
this._syncStyle(this.props);
|
|
185
|
+
if (localTextStyleChanged(this.style, before)) this._textContentChanged();
|
|
186
|
+
if (this.yoga && this.style !== before) {
|
|
187
|
+
// A block that moved a layout property changed the tree the content
|
|
188
|
+
// floors were measured from — the debt a style change from React
|
|
189
|
+
// leaves too (`invalidate`, reason 'props'). It has to be marked as a
|
|
190
|
+
// *content* change: the live-resize deferral takes a plain
|
|
191
|
+
// `_floorsDirty` for the drag itself and lays out against the floors
|
|
192
|
+
// in hand, which are the old arrangement's, and by the time the
|
|
193
|
+
// catch-up looks the dirty flags are spent and no leaf's height moved
|
|
194
|
+
// — so the floor of a card that turned from a row into a column would
|
|
195
|
+
// stay the row's, and yoga would squeeze the column down to it.
|
|
196
|
+
if (applyLayoutStyle(this.yoga, this.style, before) && this.root) {
|
|
197
|
+
this.root._floorsDirty = true;
|
|
198
|
+
this.root._floorsContentDirty = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** WindowNode's half of size and container queries, installed onto `WindowNode.prototype` by window/window.js. */
|
|
205
|
+
export class WindowQueries {
|
|
206
|
+
/**
|
|
207
|
+
* Re-evaluate the size-query blocks for this window's current size, just
|
|
208
|
+
* before laying out. This is the whole reason a size query may carry
|
|
209
|
+
* layout properties while a state block may not: it only ever runs inside
|
|
210
|
+
* a layout pass the resize already required.
|
|
211
|
+
*
|
|
212
|
+
* Callers pass the size in device pixels — it comes off the window or out
|
|
213
|
+
* of yoga, and both live on the device grid — but `querySize` is stored
|
|
214
|
+
* in **logical** pixels, because that is the unit the thresholds were
|
|
215
|
+
* written in: `'@width >= 620'` sits in a style block next to `width:
|
|
216
|
+
* 620`, and the same number must mean the same thing. At scale 1 the two
|
|
217
|
+
* coincide, which is how comparing device pixels survived every 1x
|
|
218
|
+
* display it was ever run on and broke on the first retina one (every
|
|
219
|
+
* query read double, so none of them ever changed answer under a drag).
|
|
220
|
+
*/
|
|
221
|
+
_resolveSizeQueries(deviceWidth, deviceHeight) {
|
|
222
|
+
const s = this.scale || 1;
|
|
223
|
+
const width = deviceWidth / s;
|
|
224
|
+
const height = deviceHeight / s;
|
|
225
|
+
if (this._sizeQueryNodes.size === 0) {
|
|
226
|
+
this.querySize = this.querySize ?? { width, height };
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (this.querySize?.width === width && this.querySize?.height === height) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
this.querySize = { width, height };
|
|
233
|
+
// a query block may carry layout properties, so the floors measured from
|
|
234
|
+
// the styles it is replacing are not the answer any more
|
|
235
|
+
this._floorsDirty = true;
|
|
236
|
+
for (const node of [...this._sizeQueryNodes]) {
|
|
237
|
+
if (node.destroyed) this._sizeQueryNodes.delete(node);
|
|
238
|
+
else node._sizeQueriesChanged();
|
|
239
|
+
}
|
|
240
|
+
// Whether the layout may have moved under this, which is what an
|
|
241
|
+
// auto-sizing pass needs to know: it resolves these against a size it is
|
|
242
|
+
// still working out, and has to look again if the answer changed.
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Resolve the `@container` blocks against the layout just produced, and
|
|
248
|
+
* lay out again while an answer moves — a container's size is what a pass
|
|
249
|
+
* *produces*, so it can only be asked about afterwards, and a block that
|
|
250
|
+
* changed may carry layout properties. `relayout` is whatever pass the
|
|
251
|
+
* caller runs: `_layoutStep` in a flush, `measure()` while an auto-sized
|
|
252
|
+
* window is working out how big to be.
|
|
253
|
+
*
|
|
254
|
+
* Bounded. Two passes settle the common case (a block that matches at the
|
|
255
|
+
* width the content took), and nested containers can honestly need a
|
|
256
|
+
* third — an outer answer moving an inner container past one of its own
|
|
257
|
+
* thresholds — so the cap is a small fixed number rather than "once".
|
|
258
|
+
* What it must not do is chase a design no size satisfies; that is
|
|
259
|
+
* detected per node inside `_resolveContainerQueries`, and pinned.
|
|
260
|
+
*/
|
|
261
|
+
_settleContainerQueries(relayout) {
|
|
262
|
+
if (this._containerQueryNodes.size === 0) return;
|
|
263
|
+
const held = new Map();
|
|
264
|
+
this._cqFresh = true;
|
|
265
|
+
try {
|
|
266
|
+
for (let pass = 0; pass < CONTAINER_QUERY_PASSES; pass++) {
|
|
267
|
+
if (!this._resolveContainerQueries(held)) return;
|
|
268
|
+
relayout();
|
|
269
|
+
}
|
|
270
|
+
if (DEV && this._resolveContainerQueries(held, false)) {
|
|
271
|
+
console.warn(
|
|
272
|
+
'react-x11: "@container" blocks did not settle in ' +
|
|
273
|
+
`${CONTAINER_QUERY_PASSES} layout passes; the last one stands. ` +
|
|
274
|
+
'More than two nested containers, each changing the next, is ' +
|
|
275
|
+
'the shape that gets here (docs/styling.md#container-queries).',
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
} finally {
|
|
279
|
+
this._cqFresh = false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* One round of the above: every dependent whose blocks answer differently
|
|
285
|
+
* against the containers as they are now is re-resolved, and the caller
|
|
286
|
+
* hears whether any was. With `apply` false it only answers.
|
|
287
|
+
*
|
|
288
|
+
* `held` is what each node has answered so far this frame. A node that
|
|
289
|
+
* comes back to an answer it already held is a design that oscillates —
|
|
290
|
+
* the block moves the size it asks about, which CSS forbids by
|
|
291
|
+
* construction (size containment) and yoga cannot — so it is **pinned**:
|
|
292
|
+
* the answer it has stands, and stays until the container's size moves
|
|
293
|
+
* for some other reason. Without the pin the next frame would find the
|
|
294
|
+
* other answer, apply it, get the other size, and strobe on every layout.
|
|
295
|
+
*/
|
|
296
|
+
_resolveContainerQueries(held, apply = true) {
|
|
297
|
+
let changed = false;
|
|
298
|
+
for (const node of [...this._containerQueryNodes]) {
|
|
299
|
+
if (node.destroyed) {
|
|
300
|
+
this._containerQueryNodes.delete(node);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const cq = node._cq;
|
|
304
|
+
const containers = node._containerSizes();
|
|
305
|
+
const key = containersKey(containers);
|
|
306
|
+
if (cq.pin) {
|
|
307
|
+
if (cq.pin.key === key) continue;
|
|
308
|
+
cq.pin = null;
|
|
309
|
+
}
|
|
310
|
+
const answers = containerAnswers(node._baseStyle, containers);
|
|
311
|
+
if (answers === cq.answers) continue;
|
|
312
|
+
if (!apply) return true;
|
|
313
|
+
let seen = held.get(node);
|
|
314
|
+
if (seen?.includes(answers)) {
|
|
315
|
+
cq.pin = { key, containers: cq.containers };
|
|
316
|
+
if (DEV) node._warnContainerOscillation();
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (!seen) held.set(node, (seen = [cq.answers]));
|
|
320
|
+
seen.push(answers);
|
|
321
|
+
node._sizeQueriesChanged();
|
|
322
|
+
changed = true;
|
|
323
|
+
}
|
|
324
|
+
// a block may carry layout properties, so the floors measured from the
|
|
325
|
+
// styles it is replacing are not the answer any more — the same debt a
|
|
326
|
+
// window query leaves (`_resolveSizeQueries`)
|
|
327
|
+
if (changed) this._floorsDirty = true;
|
|
328
|
+
return changed;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Rect algebra: the pure geometry the damage model, hit testing, positions
|
|
2
|
+
// and the scroll blit do their sums with. Nothing in here knows about nodes.
|
|
3
|
+
|
|
4
|
+
export const rectContains = (outer, inner) =>
|
|
5
|
+
outer.x <= inner.x &&
|
|
6
|
+
outer.y <= inner.y &&
|
|
7
|
+
outer.x + outer.width >= inner.x + inner.width &&
|
|
8
|
+
outer.y + outer.height >= inner.y + inner.height;
|
|
9
|
+
|
|
10
|
+
export const isIntegerRect = (r) =>
|
|
11
|
+
Number.isInteger(r.x) &&
|
|
12
|
+
Number.isInteger(r.y) &&
|
|
13
|
+
Number.isInteger(r.width) &&
|
|
14
|
+
Number.isInteger(r.height);
|
|
15
|
+
|
|
16
|
+
/** `rect` shrunk by `by` on every side. */
|
|
17
|
+
export function insetRect(rect, by) {
|
|
18
|
+
return {
|
|
19
|
+
x: rect.x + by,
|
|
20
|
+
y: rect.y + by,
|
|
21
|
+
width: rect.width - 2 * by,
|
|
22
|
+
height: rect.height - 2 * by,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The whole pixels inside `rect` — a fractional edge left out — or null
|
|
27
|
+
* when none are. */
|
|
28
|
+
export function innerPixels(rect) {
|
|
29
|
+
const x = Math.ceil(rect.x);
|
|
30
|
+
const y = Math.ceil(rect.y);
|
|
31
|
+
const right = Math.floor(rect.x + rect.width);
|
|
32
|
+
const bottom = Math.floor(rect.y + rect.height);
|
|
33
|
+
if (right <= x || bottom <= y) return null;
|
|
34
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The overlap of two rects, or null when they have none. */
|
|
38
|
+
export function intersectRects(a, b) {
|
|
39
|
+
const x = Math.max(a.x, b.x);
|
|
40
|
+
const y = Math.max(a.y, b.y);
|
|
41
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
42
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
43
|
+
if (right <= x || bottom <= y) return null;
|
|
44
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function unionRect(a, b) {
|
|
48
|
+
if (!b) return a ?? null;
|
|
49
|
+
if (!a) return { ...b };
|
|
50
|
+
const x = Math.min(a.x, b.x);
|
|
51
|
+
const y = Math.min(a.y, b.y);
|
|
52
|
+
return {
|
|
53
|
+
x,
|
|
54
|
+
y,
|
|
55
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
56
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function rectArea(r) {
|
|
61
|
+
return Math.max(0, r.width) * Math.max(0, r.height);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The box around a non-empty list of rects. */
|
|
65
|
+
export function rectsBounds(rects) {
|
|
66
|
+
let out = rects[0];
|
|
67
|
+
for (let i = 1; i < rects.length; i++) out = unionRect(out, rects[i]);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Does `rect` reach into any of the four `radius`-sized corner squares of
|
|
72
|
+
* `box` — the only part of a rounded border a translation cannot keep? */
|
|
73
|
+
export function cornerSquaresOverlap(box, radius, rect) {
|
|
74
|
+
const r = Math.min(radius, box.width / 2, box.height / 2);
|
|
75
|
+
if (!(r > 0)) return false;
|
|
76
|
+
const corners = [
|
|
77
|
+
{ x: box.x, y: box.y, width: r, height: r },
|
|
78
|
+
{ x: box.x + box.width - r, y: box.y, width: r, height: r },
|
|
79
|
+
{ x: box.x, y: box.y + box.height - r, width: r, height: r },
|
|
80
|
+
{
|
|
81
|
+
x: box.x + box.width - r,
|
|
82
|
+
y: box.y + box.height - r,
|
|
83
|
+
width: r,
|
|
84
|
+
height: r,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
return corners.some((square) => rectsOverlap(square, rect));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Do two rects share any area? Touching edges do not count. With a
|
|
92
|
+
* `margin`, do they come within that many pixels of each other: the same
|
|
93
|
+
* question asked of either one grown by `margin` on every side.
|
|
94
|
+
*/
|
|
95
|
+
export function rectsOverlap(a, b, margin = 0) {
|
|
96
|
+
return (
|
|
97
|
+
a.x < b.x + b.width + margin &&
|
|
98
|
+
b.x < a.x + a.width + margin &&
|
|
99
|
+
a.y < b.y + b.height + margin &&
|
|
100
|
+
b.y < a.y + a.height + margin
|
|
101
|
+
);
|
|
102
|
+
}
|