react-x11 2.6.0 → 2.7.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 +5 -3
- package/package.json +10 -3
- package/src/activate.js +12 -0
- package/src/anchor.js +6 -0
- package/src/appearance.js +351 -28
- package/src/appearancehooks.js +5 -2
- package/src/application.js +41 -0
- package/src/cocoa/app.js +358 -20
- package/src/cocoa/bezels.js +51 -1
- package/src/cocoa/context2d.js +271 -25
- package/src/cocoa/dnd.js +347 -0
- package/src/cocoa/dock.js +39 -0
- package/src/cocoa/filepanels.js +155 -0
- package/src/cocoa/fonts.js +93 -2
- package/src/cocoa/globalmenu.js +41 -33
- package/src/cocoa/notifications.js +244 -0
- package/src/cocoa/permissions.js +74 -0
- package/src/cocoa/presenter.js +190 -2
- package/src/cocoa/statusitem.js +112 -0
- package/src/cocoa/window.js +85 -4
- package/src/components/Button.js +20 -1
- package/src/components/Checkbox.js +17 -2
- package/src/components/Menu.js +108 -38
- package/src/components/Radio.js +17 -2
- package/src/components/Select.js +159 -27
- package/src/components/Switch.js +8 -1
- package/src/components/native.js +99 -0
- package/src/components/theme.js +37 -20
- package/src/desktopsettings.js +34 -2
- package/src/dnd.js +92 -3
- package/src/errors.js +6 -3
- package/src/filedialog.js +81 -16
- package/src/index.d.ts +17 -1
- package/src/index.js +17 -0
- package/src/launcher.js +170 -0
- package/src/launcherhooks.js +81 -0
- package/src/nodes.js +553 -35
- package/src/notificationhooks.js +56 -0
- package/src/notifications.js +558 -0
- package/src/palette.js +144 -8
- package/src/permissionhooks.js +89 -0
- package/src/permissions.js +196 -0
- package/src/screens.js +39 -4
- package/src/style.d.ts +10 -4
- package/src/style.js +1 -0
- package/src/styles.js +161 -15
- package/src/textselection.js +1 -4
- package/src/trayhooks.js +90 -0
- package/src/types/appearance.d.ts +24 -0
- package/src/types/components.d.ts +10 -0
- package/src/types/elements.d.ts +14 -0
- package/src/types/events.d.ts +14 -0
- package/src/types/filedialog.d.ts +18 -7
- package/src/types/launcher.d.ts +43 -0
- package/src/types/notifications.d.ts +113 -0
- package/src/types/permissions.d.ts +100 -0
- package/src/types/style.d.ts +30 -2
- package/src/types/system.d.ts +5 -3
- package/src/types/tray.d.ts +54 -0
- package/src/windowid.js +23 -0
package/src/nodes.js
CHANGED
|
@@ -32,14 +32,17 @@ import {
|
|
|
32
32
|
isLayoutProp,
|
|
33
33
|
styleUsesTokens,
|
|
34
34
|
resolveTokens,
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
queryKinds,
|
|
36
|
+
QUERY_SIZE,
|
|
37
|
+
QUERY_SUPPORTS,
|
|
38
|
+
QUERY_CONTAINER,
|
|
39
|
+
containerQueryNames,
|
|
40
|
+
containerAnswers,
|
|
37
41
|
resolveQueries,
|
|
38
42
|
DEFAULT_FOCUS_RING,
|
|
39
43
|
resolveHitSlop,
|
|
40
44
|
resolveBorderWidths,
|
|
41
45
|
resolveBorderColors,
|
|
42
|
-
tint,
|
|
43
46
|
} from './styles.js';
|
|
44
47
|
import {
|
|
45
48
|
blurKernel,
|
|
@@ -83,6 +86,7 @@ import {
|
|
|
83
86
|
registerTopLevel,
|
|
84
87
|
XDND_VERSION,
|
|
85
88
|
} from './dnd.js';
|
|
89
|
+
import { TYPE_GROUPS } from './transfer.js';
|
|
86
90
|
import { addPendingFrame, clearPendingFrame } from './frames.js';
|
|
87
91
|
import { createClientMessages } from './clientmessage.js';
|
|
88
92
|
import {
|
|
@@ -671,6 +675,11 @@ export function appearanceChanged(app) {
|
|
|
671
675
|
// started here would reach the connection a tick after it closed and throw
|
|
672
676
|
// out of the frame clock, where nothing is waiting to catch it.
|
|
673
677
|
if (!app || app.X?._closing) return;
|
|
678
|
+
// A backend that renders native control bezels caches them by every
|
|
679
|
+
// parameter that changes the pixels — except the desktop's accent, which
|
|
680
|
+
// the toolkit reads for itself. The repaint below would blit the old
|
|
681
|
+
// colour back out of that cache, so it is forgotten first.
|
|
682
|
+
app.nativeBezels?.clear?.();
|
|
674
683
|
for (const node of app._rootChildren ?? []) {
|
|
675
684
|
if (node.destroyed) continue;
|
|
676
685
|
node._themeChanged();
|
|
@@ -718,6 +727,22 @@ function shadowExtentOf(style, scale = 1) {
|
|
|
718
727
|
|
|
719
728
|
const DEV = process.env.NODE_ENV !== 'production';
|
|
720
729
|
|
|
730
|
+
/** How many extra layout passes a flush spends settling `@container` blocks
|
|
731
|
+
* before it takes the layout it has — see `_settleContainerQueries`. */
|
|
732
|
+
const CONTAINER_QUERY_PASSES = 3;
|
|
733
|
+
|
|
734
|
+
/** The sizes a node's container blocks were resolved against, as one
|
|
735
|
+
* comparable string — what a pinned node is held at. */
|
|
736
|
+
function containersKey(containers) {
|
|
737
|
+
if (!containers) return '';
|
|
738
|
+
let key = '';
|
|
739
|
+
for (const name of Object.keys(containers)) {
|
|
740
|
+
const c = containers[name];
|
|
741
|
+
key += `${name}:${c.width}x${c.height};`;
|
|
742
|
+
}
|
|
743
|
+
return key;
|
|
744
|
+
}
|
|
745
|
+
|
|
721
746
|
// Connections already told they have no 32-bit visual (`_argbAttributes`).
|
|
722
747
|
const warnedNoArgb = new WeakSet();
|
|
723
748
|
|
|
@@ -1937,6 +1962,13 @@ export class Node {
|
|
|
1937
1962
|
// `null` is "commitMount is still to come", `false` is "it has been and
|
|
1938
1963
|
// gone", and an Error is one waiting for it
|
|
1939
1964
|
this._tokenError = null;
|
|
1965
|
+
// `'@container …'` blocks (styles.js), on the nodes that carry them and
|
|
1966
|
+
// null on every other node: the style the record was built for, the
|
|
1967
|
+
// container names it asks about (`''` for the unnamed ones), the sizes
|
|
1968
|
+
// and answers the blocks last resolved to, and the pin an oscillating
|
|
1969
|
+
// design is held at — see WindowNode._resolveContainerQueries. Before
|
|
1970
|
+
// `_syncStyle`, which reads and writes it.
|
|
1971
|
+
this._cq = null;
|
|
1940
1972
|
this._syncStyle(props);
|
|
1941
1973
|
this.yoga = yoga ? createLayoutNode() : null;
|
|
1942
1974
|
if (this.yoga) {
|
|
@@ -1993,7 +2025,8 @@ export class Node {
|
|
|
1993
2025
|
this.states[':disabled'] = Boolean(props.disabled);
|
|
1994
2026
|
// window size queries fold into the base before state blocks, so a
|
|
1995
2027
|
// `:hover` inside the wide layout still wins over the wide layout
|
|
1996
|
-
const
|
|
2028
|
+
const kinds = queryKinds(this._baseStyle);
|
|
2029
|
+
const queried = (kinds & QUERY_SIZE) !== 0;
|
|
1997
2030
|
if (queried !== this._queried) {
|
|
1998
2031
|
this._queried = queried;
|
|
1999
2032
|
const root = this.root;
|
|
@@ -2009,7 +2042,7 @@ export class Node {
|
|
|
2009
2042
|
// assigned, so the first pass has nowhere to register and a "did it
|
|
2010
2043
|
// change" guard would keep it unregistered forever. Set.add is
|
|
2011
2044
|
// idempotent and these blocks are rare.
|
|
2012
|
-
const asks =
|
|
2045
|
+
const asks = (kinds & QUERY_SUPPORTS) !== 0;
|
|
2013
2046
|
this._supportsQueried = asks;
|
|
2014
2047
|
if (this.root?._supportsQueryNodes) {
|
|
2015
2048
|
if (asks) this.root._supportsQueryNodes.add(this);
|
|
@@ -2029,13 +2062,53 @@ export class Node {
|
|
|
2029
2062
|
if (wantsAttention) this.root._attentionNodes.add(this);
|
|
2030
2063
|
else this.root._attentionNodes.delete(this);
|
|
2031
2064
|
}
|
|
2032
|
-
|
|
2065
|
+
// `@container` blocks keep a third registry: what re-resolves them is a
|
|
2066
|
+
// layout pass moving the container they ask about — neither a resize
|
|
2067
|
+
// nor the server's answer. The record exists only on the nodes that
|
|
2068
|
+
// ask, so every other node pays one bit test here and nothing below.
|
|
2069
|
+
// Insertion registers through `_registerSizeQueries`; this is for a
|
|
2070
|
+
// style that starts or stops asking on a node already in a window.
|
|
2071
|
+
const asksContainers = (kinds & QUERY_CONTAINER) !== 0;
|
|
2072
|
+
let containers = null;
|
|
2073
|
+
if (asksContainers) {
|
|
2074
|
+
const base = this._baseStyle;
|
|
2075
|
+
let cq = this._cq;
|
|
2076
|
+
if (cq === null || cq.style !== base) {
|
|
2077
|
+
if (cq === null) {
|
|
2078
|
+
cq = this._cq = {
|
|
2079
|
+
style: base,
|
|
2080
|
+
names: null,
|
|
2081
|
+
containers: null,
|
|
2082
|
+
answers: '',
|
|
2083
|
+
pin: null,
|
|
2084
|
+
warned: false,
|
|
2085
|
+
};
|
|
2086
|
+
} else {
|
|
2087
|
+
// a different style asks different questions, and a pin held for
|
|
2088
|
+
// the old one is not an answer to the new one
|
|
2089
|
+
cq.style = base;
|
|
2090
|
+
cq.pin = null;
|
|
2091
|
+
}
|
|
2092
|
+
cq.names = containerQueryNames(base);
|
|
2093
|
+
}
|
|
2094
|
+
this.root?._containerQueryNodes?.add(this);
|
|
2095
|
+
containers = this._pinnedContainerSizes();
|
|
2096
|
+
} else if (this._cq !== null) {
|
|
2097
|
+
this._cq = null;
|
|
2098
|
+
this.root?._containerQueryNodes?.delete(this);
|
|
2099
|
+
}
|
|
2100
|
+
if (queried || asks || asksContainers) {
|
|
2033
2101
|
this._baseStyle = resolveQueries(this._baseStyle, {
|
|
2034
2102
|
size: this.root?.querySize ?? null,
|
|
2035
2103
|
// null before the window is realized, which reads as "not
|
|
2036
2104
|
// supported" — the fallback design is the one that works everywhere
|
|
2037
2105
|
supports: this.root?.capabilities ?? null,
|
|
2106
|
+
containers,
|
|
2038
2107
|
});
|
|
2108
|
+
if (asksContainers) {
|
|
2109
|
+
this._cq.containers = containers;
|
|
2110
|
+
this._cq.answers = containerAnswers(this._baseStyle, containers);
|
|
2111
|
+
}
|
|
2039
2112
|
}
|
|
2040
2113
|
this._stateful = hasStateStyles(this._baseStyle);
|
|
2041
2114
|
// The scale multiplies *after* every merge — state blocks, queries,
|
|
@@ -2085,7 +2158,7 @@ export class Node {
|
|
|
2085
2158
|
const duration = transitionFor(target, prop);
|
|
2086
2159
|
if (duration <= 0) continue;
|
|
2087
2160
|
if (interpolate(from, to, 0.5) === null) continue; // no midpoint: snap
|
|
2088
|
-
|
|
2161
|
+
const entry = {
|
|
2089
2162
|
from,
|
|
2090
2163
|
to,
|
|
2091
2164
|
duration,
|
|
@@ -2094,8 +2167,23 @@ export class Node {
|
|
|
2094
2167
|
// be seconds old — and the first tick would then find the
|
|
2095
2168
|
// transition already over and jump straight to the end
|
|
2096
2169
|
start: now(),
|
|
2097
|
-
}
|
|
2098
|
-
this.
|
|
2170
|
+
};
|
|
2171
|
+
const previous = this._anim?.get(prop);
|
|
2172
|
+
(this._anim ??= new Map()).set(prop, entry);
|
|
2173
|
+
// A presenter that can run it in the render server takes it here:
|
|
2174
|
+
// the node's style then goes straight to the target — the layer's
|
|
2175
|
+
// model value — and the one frame that sends it carries the
|
|
2176
|
+
// animation with it (src/cocoa/presenter.js). Declined, or with no
|
|
2177
|
+
// such presenter, the window's frame clock runs it as it always has.
|
|
2178
|
+
if (this._offload(prop, entry)) {
|
|
2179
|
+
entry.offloaded = true;
|
|
2180
|
+
this.root?.invalidate(false, damageForAnimation(this), 'animation');
|
|
2181
|
+
} else {
|
|
2182
|
+
// …and one the presenter had must not keep running underneath the
|
|
2183
|
+
// values the clock is about to write
|
|
2184
|
+
if (previous?.offloaded) this._cancelOffload(prop, previous);
|
|
2185
|
+
this.root?._startAnimating(this);
|
|
2186
|
+
}
|
|
2099
2187
|
}
|
|
2100
2188
|
}
|
|
2101
2189
|
// After the transitions, before the style is assembled: a loop that just
|
|
@@ -2187,10 +2275,77 @@ export class Node {
|
|
|
2187
2275
|
|
|
2188
2276
|
_animatedValues() {
|
|
2189
2277
|
const values = {};
|
|
2190
|
-
for (const [prop, a] of this._anim)
|
|
2278
|
+
for (const [prop, a] of this._anim) {
|
|
2279
|
+
// an offloaded property shows its target: the render server draws the
|
|
2280
|
+
// motion over the model value, and the model is the style
|
|
2281
|
+
if (!a.offloaded) values[prop] = a.value ?? a.from;
|
|
2282
|
+
}
|
|
2191
2283
|
return values;
|
|
2192
2284
|
}
|
|
2193
2285
|
|
|
2286
|
+
// --- the presenter's half of an animation ---------------------------------
|
|
2287
|
+
//
|
|
2288
|
+
// Three feature-detected hooks on the window (src/cocoa/window.js, layers
|
|
2289
|
+
// mode only): `animateNode(node, prop, entry)` answers true when the
|
|
2290
|
+
// presenter will run the entry itself, `cancelNodeAnimation(node, prop)`
|
|
2291
|
+
// stops what it runs for the property, and the presenter calls back
|
|
2292
|
+
// through `_offloadEnded` / `_offloadDeclined` below. An entry the
|
|
2293
|
+
// presenter took is `offloaded`: it stays in `_anim` — so a retarget, a
|
|
2294
|
+
// loop-stop rule and `sameAnimation` all see it — but it contributes no
|
|
2295
|
+
// value to the style, is skipped by the tick, and keeps the node out of the
|
|
2296
|
+
// window's animating set. The X11 path has none of these hooks and is
|
|
2297
|
+
// byte-identical (docs/architecture/animation.md §4).
|
|
2298
|
+
|
|
2299
|
+
_offload(prop, entry) {
|
|
2300
|
+
const wnd = this.root?.window;
|
|
2301
|
+
if (typeof wnd?.animateNode !== 'function') return false;
|
|
2302
|
+
return wnd.animateNode(this, prop, entry) === true;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
_cancelOffload(prop, entry) {
|
|
2306
|
+
if (!entry?.offloaded) return;
|
|
2307
|
+
this.root?.window?.cancelNodeAnimation?.(this, prop);
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
/** The presenter is done with `entry` — it ran out, or its layer went.
|
|
2311
|
+
* A transition is over either way (the model is the target). A loop
|
|
2312
|
+
* never ends on its own, so a loop that comes back this way lost its
|
|
2313
|
+
* layer, and the frame clock takes it over rather than letting it stop. */
|
|
2314
|
+
_offloadEnded(prop, entry) {
|
|
2315
|
+
if (this._anim?.get(prop) !== entry) return;
|
|
2316
|
+
if (entry.loop && !this.destroyed) {
|
|
2317
|
+
this._offloadDeclined(prop, entry);
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
this._anim.delete(prop);
|
|
2321
|
+
if (!this._anim.size) this.root?._animating.delete(this);
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
/** The presenter could not run `entry` after all — the node turned into a
|
|
2325
|
+
* raster between the swap and the frame. The frame clock takes it from
|
|
2326
|
+
* the top; the property's declared start is where the pixels still are. */
|
|
2327
|
+
_offloadDeclined(prop, entry) {
|
|
2328
|
+
if (this._anim?.get(prop) !== entry || this.destroyed) return;
|
|
2329
|
+
entry.offloaded = false;
|
|
2330
|
+
entry.start = now();
|
|
2331
|
+
this.style = { ...this._targetStyle, ...this._animatedValues() };
|
|
2332
|
+
this.root?._startAnimating(this);
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
/** Keep the frame clock running only for what the clock itself animates;
|
|
2336
|
+
* an offloaded-only node needs one frame — the one that sends the model
|
|
2337
|
+
* and the animation — and not a loop of them. */
|
|
2338
|
+
_scheduleAnimationFrames() {
|
|
2339
|
+
for (const a of this._anim?.values() ?? []) {
|
|
2340
|
+
if (!a.offloaded) {
|
|
2341
|
+
this.root?._startAnimating(this);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
this.root?._animating.delete(this);
|
|
2346
|
+
this.root?.invalidate(false, damageForAnimation(this), 'animation');
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2194
2349
|
/**
|
|
2195
2350
|
* The style declared a set of loops (`animation`, styles.js): remember
|
|
2196
2351
|
* them and reconcile what is running against them.
|
|
@@ -2237,6 +2392,7 @@ export class Node {
|
|
|
2237
2392
|
if (!a.loop) continue;
|
|
2238
2393
|
if (running && specs.some((spec) => spec.prop === prop)) continue;
|
|
2239
2394
|
anim.delete(prop);
|
|
2395
|
+
this._cancelOffload(prop, a);
|
|
2240
2396
|
changed = true;
|
|
2241
2397
|
if (isLayoutProp(prop)) layoutTouched = true;
|
|
2242
2398
|
}
|
|
@@ -2249,13 +2405,28 @@ export class Node {
|
|
|
2249
2405
|
// spinner that jumps back to the start whenever anything above it
|
|
2250
2406
|
// re-rendered — which is the frame after every state change in the
|
|
2251
2407
|
// app.
|
|
2252
|
-
if (current?.loop && sameAnimation(current, spec))
|
|
2253
|
-
|
|
2408
|
+
if (current?.loop && sameAnimation(current, spec)) {
|
|
2409
|
+
// A loop the clock started before the window had a presenter —
|
|
2410
|
+
// one declared at mount runs from `_setRoot`, before `realize` —
|
|
2411
|
+
// moves over the first time a presenter can take it. Its phase is
|
|
2412
|
+
// the render server's from here, which is what a restart costs.
|
|
2413
|
+
if (!current.offloaded && this._offload(spec.prop, current)) {
|
|
2414
|
+
current.offloaded = true;
|
|
2415
|
+
changed = true;
|
|
2416
|
+
}
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
// a changed declaration, or a transition the loop takes over from:
|
|
2420
|
+
// whatever the presenter ran for the property stops first
|
|
2421
|
+
if (current?.offloaded) this._cancelOffload(spec.prop, current);
|
|
2422
|
+
const entry = {
|
|
2254
2423
|
...spec,
|
|
2255
2424
|
loop: true,
|
|
2256
2425
|
start: now(),
|
|
2257
2426
|
value: animationValueAt(spec, 0),
|
|
2258
|
-
}
|
|
2427
|
+
};
|
|
2428
|
+
(this._anim ??= new Map()).set(spec.prop, entry);
|
|
2429
|
+
if (this._offload(spec.prop, entry)) entry.offloaded = true;
|
|
2259
2430
|
changed = true;
|
|
2260
2431
|
if (isLayoutProp(spec.prop)) layoutTouched = true;
|
|
2261
2432
|
}
|
|
@@ -2269,7 +2440,7 @@ export class Node {
|
|
|
2269
2440
|
// a stop has to leave the frame clock idle, and a tick is exactly what
|
|
2270
2441
|
// there may never be another of.
|
|
2271
2442
|
if (!this._anim?.size) this.root?._animating.delete(this);
|
|
2272
|
-
if (running) this.
|
|
2443
|
+
if (running) this._scheduleAnimationFrames();
|
|
2273
2444
|
if (!write) return true;
|
|
2274
2445
|
if (layoutTouched && this.yoga) {
|
|
2275
2446
|
applyLayoutStyle(this.yoga, this.style, before);
|
|
@@ -2316,19 +2487,23 @@ export class Node {
|
|
|
2316
2487
|
_tickAnimations(now) {
|
|
2317
2488
|
if (!this._anim?.size) return false;
|
|
2318
2489
|
let layoutChanged = false;
|
|
2490
|
+
let ticking = 0; // entries the clock runs, as against the presenter's
|
|
2319
2491
|
const before = this.style;
|
|
2320
2492
|
for (const [prop, a] of this._anim) {
|
|
2493
|
+
if (a.offloaded) continue;
|
|
2321
2494
|
if (a.loop) {
|
|
2322
2495
|
// No end to test for and no rounding to accumulate: the phase is a
|
|
2323
2496
|
// modulo of the elapsed time, so a bar that has been going for an
|
|
2324
2497
|
// hour is exactly where the clock says.
|
|
2325
2498
|
a.value = animationValueAt(a, now - a.start);
|
|
2326
2499
|
if (isLayoutProp(prop)) layoutChanged = true;
|
|
2500
|
+
ticking++;
|
|
2327
2501
|
continue;
|
|
2328
2502
|
}
|
|
2329
2503
|
const t = a.duration > 0 ? Math.min(1, (now - a.start) / a.duration) : 1;
|
|
2330
2504
|
a.value = t >= 1 ? a.to : (interpolate(a.from, a.to, ease(t)) ?? a.to);
|
|
2331
2505
|
if (t >= 1) this._anim.delete(prop);
|
|
2506
|
+
else ticking++;
|
|
2332
2507
|
if (isLayoutProp(prop)) layoutChanged = true;
|
|
2333
2508
|
}
|
|
2334
2509
|
this.style = this._anim.size
|
|
@@ -2351,7 +2526,7 @@ export class Node {
|
|
|
2351
2526
|
// it *per frame*: a transitioned `color` is a new ink every frame, for
|
|
2352
2527
|
// this node and for everything inheriting from it.
|
|
2353
2528
|
if (inheritedTextChanged(this.style, before)) this._retextSubtree();
|
|
2354
|
-
return
|
|
2529
|
+
return ticking > 0;
|
|
2355
2530
|
}
|
|
2356
2531
|
|
|
2357
2532
|
/**
|
|
@@ -2398,11 +2573,129 @@ export class Node {
|
|
|
2398
2573
|
if (this._wantsAttention && this.root?._attentionNodes) {
|
|
2399
2574
|
this.root._attentionNodes.add(this);
|
|
2400
2575
|
}
|
|
2576
|
+
if (this._cq !== null && this.root?._containerQueryNodes) {
|
|
2577
|
+
this.root._containerQueryNodes.add(this);
|
|
2578
|
+
// it can see the containers above it now — the constructor's
|
|
2579
|
+
// resolution had no ancestors to find one in
|
|
2580
|
+
this._sizeQueriesChanged();
|
|
2581
|
+
}
|
|
2401
2582
|
for (const child of this.children) {
|
|
2402
2583
|
if (!child.isWindow) child._registerSizeQueries();
|
|
2403
2584
|
}
|
|
2404
2585
|
}
|
|
2405
2586
|
|
|
2587
|
+
/**
|
|
2588
|
+
* The sizes this node's `@container` blocks resolve against: for each name
|
|
2589
|
+
* the style asks about, the nearest ancestor declaring it, in this node's
|
|
2590
|
+
* **logical** pixels — the unit the threshold beside `width: 400` was
|
|
2591
|
+
* written in, so the two numbers mean the same thing. Yoga's computed
|
|
2592
|
+
* size rather than `abs`: inside a flush, `abs` is still the previous
|
|
2593
|
+
* frame's.
|
|
2594
|
+
*
|
|
2595
|
+
* A container that has not been laid out yet contributes nothing, so its
|
|
2596
|
+
* blocks do not apply — the way a capability block does not before the
|
|
2597
|
+
* window exists: the fallback design is the one that works everywhere.
|
|
2598
|
+
* "Laid out" is `_placed` between frames and every attached node while
|
|
2599
|
+
* the window is settling a pass it just ran (`_cqFresh`); a fresh yoga
|
|
2600
|
+
* node answers NaN. Null when no container is known, which is the
|
|
2601
|
+
* identity `resolveQueries` keeps.
|
|
2602
|
+
*/
|
|
2603
|
+
_containerSizes() {
|
|
2604
|
+
const names = this._cq?.names;
|
|
2605
|
+
if (!names) return null;
|
|
2606
|
+
let sizes = null;
|
|
2607
|
+
const s = this.scale || 1;
|
|
2608
|
+
const fresh = Boolean(this.root?._cqFresh);
|
|
2609
|
+
for (const name of names) {
|
|
2610
|
+
const c = this._containerFor(name);
|
|
2611
|
+
if (!c) {
|
|
2612
|
+
// In a window and nothing above declares one: a forgotten
|
|
2613
|
+
// declaration, not a component rendered outside its context — that
|
|
2614
|
+
// is what the *named* form is for, and a missing name applies
|
|
2615
|
+
// nothing quietly. `root` rather than `parent`: React builds a
|
|
2616
|
+
// subtree bottom-up, so a node can have a parent and no window yet,
|
|
2617
|
+
// and the container it will find is further up.
|
|
2618
|
+
if (DEV && name === '' && this.root) this._noContainer();
|
|
2619
|
+
continue;
|
|
2620
|
+
}
|
|
2621
|
+
if (!c.yoga || !(fresh || c._placed)) continue;
|
|
2622
|
+
const width = c.yoga.getComputedWidth() / s;
|
|
2623
|
+
const height = c.yoga.getComputedHeight() / s;
|
|
2624
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) continue;
|
|
2625
|
+
(sizes ??= {})[name] = { width, height };
|
|
2626
|
+
}
|
|
2627
|
+
return sizes;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
/** `_containerSizes()`, unless this node is pinned at the sizes it is
|
|
2631
|
+
* looking at — then the sizes its held answer came from, so a restyle
|
|
2632
|
+
* arriving from React does not undo what the layout pass decided. */
|
|
2633
|
+
_pinnedContainerSizes() {
|
|
2634
|
+
const live = this._containerSizes();
|
|
2635
|
+
const cq = this._cq;
|
|
2636
|
+
const pin = cq.pin;
|
|
2637
|
+
if (!pin) return live;
|
|
2638
|
+
if (pin.key === containersKey(live)) return pin.containers;
|
|
2639
|
+
cq.pin = null;
|
|
2640
|
+
return live;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
/**
|
|
2644
|
+
* The nearest ancestor whose style declares `container` — any container
|
|
2645
|
+
* for the unnamed query (`''`), the one carrying `name` otherwise, however
|
|
2646
|
+
* many nearer containers that reaches past. A window ends the walk after
|
|
2647
|
+
* offering itself, and a window asks nothing: a `<popup>` inside a
|
|
2648
|
+
* container is a root of its own and asks its own window with `@width`.
|
|
2649
|
+
*
|
|
2650
|
+
* Walked rather than cached: it is a dozen property reads per dependent
|
|
2651
|
+
* per layout pass, and a cache would have to follow every insert, every
|
|
2652
|
+
* reorder and every `container` value that changes above.
|
|
2653
|
+
*/
|
|
2654
|
+
_containerFor(name) {
|
|
2655
|
+
if (this.isWindow) return null;
|
|
2656
|
+
for (let n = this.parent; n; n = n.parent) {
|
|
2657
|
+
const c = n.style?.container;
|
|
2658
|
+
if (name === '' ? c === true || typeof c === 'string' : c === name) {
|
|
2659
|
+
return n;
|
|
2660
|
+
}
|
|
2661
|
+
if (n.isWindow) break;
|
|
2662
|
+
}
|
|
2663
|
+
return null;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
_noContainer() {
|
|
2667
|
+
this._tokenProblem(
|
|
2668
|
+
[
|
|
2669
|
+
`react-x11: <${this.kind}> has an "@container" block and no ` +
|
|
2670
|
+
'container above it — declare one with `container: true` in an ' +
|
|
2671
|
+
"ancestor's style (or name it and ask for it by name), or ask " +
|
|
2672
|
+
'the window with "@width"',
|
|
2673
|
+
],
|
|
2674
|
+
true,
|
|
2675
|
+
'The block does not apply and the app carries on',
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
/** Said once per node, in development: a design that cannot settle looks
|
|
2680
|
+
* like a layout bug, and the frame it is pinned at is the only clue. */
|
|
2681
|
+
_warnContainerOscillation() {
|
|
2682
|
+
const cq = this._cq;
|
|
2683
|
+
if (cq.warned) return;
|
|
2684
|
+
cq.warned = true;
|
|
2685
|
+
const asked = [...(cq.names ?? [])]
|
|
2686
|
+
.map((n) => (n === '' ? 'its container' : `"${n}"`))
|
|
2687
|
+
.join(', ');
|
|
2688
|
+
console.warn(
|
|
2689
|
+
`react-x11: the "@container" blocks on <${this.kind}> cannot settle: ` +
|
|
2690
|
+
`a block that matches at one size of ${asked} changes that size to ` +
|
|
2691
|
+
'one where it no longer matches, and back. A container query must ' +
|
|
2692
|
+
'not move the size it asks about — give the container a size of its ' +
|
|
2693
|
+
'own, or minWidth: 0 and a flexBasis so its content cannot grow it. ' +
|
|
2694
|
+
'The current answer is held until the container moves for another ' +
|
|
2695
|
+
'reason (docs/styling.md#container-queries).',
|
|
2696
|
+
);
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2406
2699
|
/** Style names this element claims as its own semantics (see WindowNode).
|
|
2407
2700
|
* Registered elements declare theirs to `registerElement`, so the common
|
|
2408
2701
|
* case needs no subclass. */
|
|
@@ -2686,12 +2979,14 @@ export class Node {
|
|
|
2686
2979
|
* swallow the error instead of raising it late. Those throw at once, like
|
|
2687
2980
|
* the keyed reorder they resemble.
|
|
2688
2981
|
*/
|
|
2689
|
-
_tokenProblem(problems, mounting) {
|
|
2982
|
+
_tokenProblem(problems, mounting, consequence = undefined) {
|
|
2690
2983
|
if (!STRICT_TOKENS) {
|
|
2691
2984
|
// every one of them: two misspellings in a style are two things to
|
|
2692
2985
|
// fix, and a report that named only the first would send someone back
|
|
2693
2986
|
// for a second run to find the second
|
|
2694
|
-
for (const message of problems)
|
|
2987
|
+
for (const message of problems) {
|
|
2988
|
+
reportStyleError(this, message, consequence);
|
|
2989
|
+
}
|
|
2695
2990
|
return;
|
|
2696
2991
|
}
|
|
2697
2992
|
const error = new Error(problems[0]);
|
|
@@ -2699,10 +2994,16 @@ export class Node {
|
|
|
2699
2994
|
else throw error;
|
|
2700
2995
|
}
|
|
2701
2996
|
|
|
2702
|
-
/** The owning window resized
|
|
2703
|
-
*
|
|
2997
|
+
/** The owning window resized, the server's answer moved, or a layout pass
|
|
2998
|
+
* moved a container this node asks about: re-resolve, since a query block
|
|
2999
|
+
* may now match that did not, or the other way round. */
|
|
2704
3000
|
_sizeQueriesChanged() {
|
|
2705
|
-
if (
|
|
3001
|
+
if (
|
|
3002
|
+
!(this._queried || this._supportsQueried || this._cq !== null) ||
|
|
3003
|
+
this.destroyed
|
|
3004
|
+
) {
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
2706
3007
|
const before = this.style;
|
|
2707
3008
|
// a query block may name `fontSize`, and `_syncStyle` → `_retarget` is
|
|
2708
3009
|
// what pushes that into the subtree; only the node-local text props are
|
|
@@ -2710,7 +3011,19 @@ export class Node {
|
|
|
2710
3011
|
this._syncStyle(this.props);
|
|
2711
3012
|
if (localTextStyleChanged(this.style, before)) this._textContentChanged();
|
|
2712
3013
|
if (this.yoga && this.style !== before) {
|
|
2713
|
-
|
|
3014
|
+
// A block that moved a layout property changed the tree the content
|
|
3015
|
+
// floors were measured from — the debt a style change from React
|
|
3016
|
+
// leaves too (`invalidate`, reason 'props'). It has to be marked as a
|
|
3017
|
+
// *content* change: the live-resize deferral takes a plain
|
|
3018
|
+
// `_floorsDirty` for the drag itself and lays out against the floors
|
|
3019
|
+
// in hand, which are the old arrangement's, and by the time the
|
|
3020
|
+
// catch-up looks the dirty flags are spent and no leaf's height moved
|
|
3021
|
+
// — so the floor of a card that turned from a row into a column would
|
|
3022
|
+
// stay the row's, and yoga would squeeze the column down to it.
|
|
3023
|
+
if (applyLayoutStyle(this.yoga, this.style, before) && this.root) {
|
|
3024
|
+
this.root._floorsDirty = true;
|
|
3025
|
+
this.root._floorsContentDirty = true;
|
|
3026
|
+
}
|
|
2714
3027
|
}
|
|
2715
3028
|
}
|
|
2716
3029
|
|
|
@@ -3699,11 +4012,58 @@ export class Node {
|
|
|
3699
4012
|
this.yoga.getComputedWidth(),
|
|
3700
4013
|
this.yoga.getComputedHeight(),
|
|
3701
4014
|
);
|
|
4015
|
+
if (this.props.onLayout) this._reportLayout();
|
|
3702
4016
|
for (const child of this.children) {
|
|
3703
4017
|
if (!child.isWindow) child.absolutize(this.abs.x, this.abs.y);
|
|
3704
4018
|
}
|
|
3705
4019
|
}
|
|
3706
4020
|
|
|
4021
|
+
/**
|
|
4022
|
+
* `onLayout`: the rect a layout pass gave this node, reported when it
|
|
4023
|
+
* changed — React Native's contract, and the seam for a decision that is
|
|
4024
|
+
* not a style (docs/react-features.md): how many columns to build, which
|
|
4025
|
+
* component to render. Where the decision *is* a style, a container
|
|
4026
|
+
* query answers it in the same frame instead (docs/styling.md).
|
|
4027
|
+
*
|
|
4028
|
+
* `x`/`y` are the position **within the parent as laid out** — yoga's
|
|
4029
|
+
* answer, which a scroll does not move — rather than the window
|
|
4030
|
+
* coordinates `abs` holds: a list scrolling under the pointer must not
|
|
4031
|
+
* re-render every row on every notch. Logical pixels, this node's own,
|
|
4032
|
+
* the same division `measure()` makes.
|
|
4033
|
+
*
|
|
4034
|
+
* Deferred, like `onViewport`: this runs inside the layout pass, and a
|
|
4035
|
+
* `setState` from the handler would re-enter it. One report per frame,
|
|
4036
|
+
* because `absolutize` runs once, after the container blocks have
|
|
4037
|
+
* settled — a card whose block changed its height reports the height it
|
|
4038
|
+
* ended the frame at, not the one it had between passes.
|
|
4039
|
+
*/
|
|
4040
|
+
_reportLayout() {
|
|
4041
|
+
const s = this.scale;
|
|
4042
|
+
const next = {
|
|
4043
|
+
x: this.yoga.getComputedLeft() / s,
|
|
4044
|
+
y: this.yoga.getComputedTop() / s,
|
|
4045
|
+
width: this.abs.width / s,
|
|
4046
|
+
height: this.abs.height / s,
|
|
4047
|
+
};
|
|
4048
|
+
const last = this._lastLayout;
|
|
4049
|
+
if (
|
|
4050
|
+
last &&
|
|
4051
|
+
last.x === next.x &&
|
|
4052
|
+
last.y === next.y &&
|
|
4053
|
+
last.width === next.width &&
|
|
4054
|
+
last.height === next.height
|
|
4055
|
+
) {
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
this._lastLayout = next;
|
|
4059
|
+
setImmediate(() => {
|
|
4060
|
+
// the handler as it is *now*: React may have re-rendered in between
|
|
4061
|
+
const notify = this.props.onLayout;
|
|
4062
|
+
if (this.destroyed || !notify) return;
|
|
4063
|
+
callHandler(this, 'onLayout', notify, next);
|
|
4064
|
+
});
|
|
4065
|
+
}
|
|
4066
|
+
|
|
3707
4067
|
/**
|
|
3708
4068
|
* absolutize's write to `abs`, funneled through one place so a bounded
|
|
3709
4069
|
* frame's layout diff sees every node the pass actually moved or resized.
|
|
@@ -5458,8 +5818,14 @@ export class TextNode extends Node {
|
|
|
5458
5818
|
weight: base.weight,
|
|
5459
5819
|
style: base.style,
|
|
5460
5820
|
});
|
|
5461
|
-
const
|
|
5462
|
-
if (!
|
|
5821
|
+
const measured = font?.metrics?.(base.size)?.capHeight;
|
|
5822
|
+
if (!measured) return null; // no metrics: leave the box alone
|
|
5823
|
+
// Whole pixels: the trimmed box's top is the baseline less this, so a
|
|
5824
|
+
// fractional cap height — 9.15px for a 13px face — puts the baseline
|
|
5825
|
+
// between two rows, and the rasteriser lands the letters a row low on
|
|
5826
|
+
// one backend and half-covers two rows on the other. Rounded, the
|
|
5827
|
+
// baseline sits on a pixel wherever the box does.
|
|
5828
|
+
const capHeight = Math.round(measured);
|
|
5463
5829
|
const shift = halfLeading(layout);
|
|
5464
5830
|
const firstBaseline = shift + lines[0].baseline;
|
|
5465
5831
|
const lastBaseline = shift + lines[lines.length - 1].baseline;
|
|
@@ -6045,6 +6411,7 @@ export const Scrollable = (Base) =>
|
|
|
6045
6411
|
this.yoga.getComputedWidth(),
|
|
6046
6412
|
this.yoga.getComputedHeight(),
|
|
6047
6413
|
);
|
|
6414
|
+
if (this.props.onLayout) this._reportLayout();
|
|
6048
6415
|
this._absolutizeChildren(this.abs.x, this.abs.y);
|
|
6049
6416
|
}
|
|
6050
6417
|
|
|
@@ -8496,8 +8863,7 @@ export class TextInputNode extends Node {
|
|
|
8496
8863
|
// colour does that on both a light and a dark palette. `#b3d4fc`
|
|
8497
8864
|
// under the dark palette's near-white ink is 1.3:1, which is nothing.
|
|
8498
8865
|
// Tinting the surface instead leaves the ink's own contrast intact.
|
|
8499
|
-
ctx.fillStyle =
|
|
8500
|
-
this.props.selectionColor ?? tint(this.theme.accent, 0.35);
|
|
8866
|
+
ctx.fillStyle = this.props.selectionColor ?? this.theme.selection;
|
|
8501
8867
|
// One band per direction run, not one rectangle between the two caret
|
|
8502
8868
|
// positions: a range is contiguous in logical order and a line is laid
|
|
8503
8869
|
// out in visual order, so a selection that crosses into an Arabic word
|
|
@@ -8520,7 +8886,7 @@ export class TextInputNode extends Node {
|
|
|
8520
8886
|
this._paintPreedit(ctx, valueX, textY, style);
|
|
8521
8887
|
|
|
8522
8888
|
if (this._focused && this._caretOn && a === b) {
|
|
8523
|
-
ctx.fillStyle = this.props.caretColor ?? style.color;
|
|
8889
|
+
ctx.fillStyle = this.props.caretColor ?? this.theme.caret ?? style.color;
|
|
8524
8890
|
ctx.fillRect(
|
|
8525
8891
|
valueX + caretX,
|
|
8526
8892
|
markY,
|
|
@@ -8887,8 +9253,7 @@ export class TextAreaNode extends TextInputNode {
|
|
|
8887
9253
|
// colour does that on both a light and a dark palette. `#b3d4fc`
|
|
8888
9254
|
// under the dark palette's near-white ink is 1.3:1, which is nothing.
|
|
8889
9255
|
// Tinting the surface instead leaves the ink's own contrast intact.
|
|
8890
|
-
ctx.fillStyle =
|
|
8891
|
-
this.props.selectionColor ?? tint(this.theme.accent, 0.35);
|
|
9256
|
+
ctx.fillStyle = this.props.selectionColor ?? this.theme.selection;
|
|
8892
9257
|
// The bands `textRangeRects` reports — one per line and one per
|
|
8893
9258
|
// direction run inside a line, which is what a selection crossing into
|
|
8894
9259
|
// an Arabic word actually covers. Two caret positions and a rectangle
|
|
@@ -8997,6 +9362,12 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
8997
9362
|
// nodes with `@supports` blocks, re-resolved when the server's answer
|
|
8998
9363
|
// changes rather than on every layout
|
|
8999
9364
|
this._supportsQueryNodes = new Set();
|
|
9365
|
+
// nodes with `@container` blocks, re-resolved after every layout pass
|
|
9366
|
+
// against the containers they ask about — see _resolveContainerQueries.
|
|
9367
|
+
// `_cqFresh` is true while a pass this window just ran is being settled,
|
|
9368
|
+
// when every attached node has a computed size to offer
|
|
9369
|
+
this._containerQueryNodes = new Set();
|
|
9370
|
+
this._cqFresh = false;
|
|
9000
9371
|
// nodes whose child list changed and whose own size is pinned: their new
|
|
9001
9372
|
// arrangement is only measurable once layout has run (see
|
|
9002
9373
|
// Node._childListChanged)
|
|
@@ -9493,6 +9864,13 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9493
9864
|
|
|
9494
9865
|
let size = measure();
|
|
9495
9866
|
if (this._resolveSizeQueries(size.width, size.height)) size = measure();
|
|
9867
|
+
// …and the container blocks against the arrangement that produced it,
|
|
9868
|
+
// so the window is created at the size its content will actually take
|
|
9869
|
+
if (this._containerQueryNodes.size !== 0) {
|
|
9870
|
+
this._settleContainerQueries(() => {
|
|
9871
|
+
size = measure();
|
|
9872
|
+
});
|
|
9873
|
+
}
|
|
9496
9874
|
|
|
9497
9875
|
// A cap the content decides is its natural size, never below a floor
|
|
9498
9876
|
// that was named as a number: `WM_NORMAL_HINTS` with a min above its own
|
|
@@ -10093,6 +10471,16 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10093
10471
|
_initDnd() {
|
|
10094
10472
|
const wnd = this.window;
|
|
10095
10473
|
const X = this.app?.X;
|
|
10474
|
+
// A backend with drop machinery of its own (the cocoa backend's
|
|
10475
|
+
// NSDraggingDestination, src/cocoa/dnd.js): the same DropSession, driven
|
|
10476
|
+
// through its local entry points by the window's transport instead of
|
|
10477
|
+
// by XDND ClientMessages. No property to write, nothing to intern.
|
|
10478
|
+
if (typeof wnd?.attachDropTransport === 'function') {
|
|
10479
|
+
this._dnd = new DropSession(this);
|
|
10480
|
+
registerTopLevel(this);
|
|
10481
|
+
wnd.attachDropTransport(this._dnd, this);
|
|
10482
|
+
return;
|
|
10483
|
+
}
|
|
10096
10484
|
if (
|
|
10097
10485
|
!X ||
|
|
10098
10486
|
typeof X.InternAtom !== 'function' ||
|
|
@@ -10137,11 +10525,43 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10137
10525
|
* their top-level's count, since that is where the messages arrive. */
|
|
10138
10526
|
_registerDropTarget(node) {
|
|
10139
10527
|
(this._dropTargets ??= new Set()).add(node);
|
|
10528
|
+
this._dndTopLevel()?.window?.dropTargetsChanged?.();
|
|
10140
10529
|
}
|
|
10141
10530
|
|
|
10142
10531
|
_forgetDropTarget(node) {
|
|
10143
10532
|
this._dropTargets?.delete(node);
|
|
10144
10533
|
this._dndOwner()?.forget(node);
|
|
10534
|
+
this._dndTopLevel()?.window?.dropTargetsChanged?.();
|
|
10535
|
+
}
|
|
10536
|
+
|
|
10537
|
+
/** The top-level whose drop session — and transport — this window's
|
|
10538
|
+
* targets roll up into. */
|
|
10539
|
+
_dndTopLevel() {
|
|
10540
|
+
let node = this;
|
|
10541
|
+
while (node && !node._dnd) node = node.parent?.root;
|
|
10542
|
+
return node ?? null;
|
|
10543
|
+
}
|
|
10544
|
+
|
|
10545
|
+
/** The concrete type names every `dropAccept` under this top-level asks
|
|
10546
|
+
* for — what a backend that registers its accepted types up front (the
|
|
10547
|
+
* cocoa backend) adds to its base set. Groups and predicates name none. */
|
|
10548
|
+
_dndConcreteTypes() {
|
|
10549
|
+
const out = new Set();
|
|
10550
|
+
const walk = (wn) => {
|
|
10551
|
+
for (const node of wn._dropTargets ?? []) {
|
|
10552
|
+
const accept = node.props.dropAccept;
|
|
10553
|
+
for (const entry of Array.isArray(accept) ? accept : []) {
|
|
10554
|
+
if (typeof entry === 'string' && !(entry in TYPE_GROUPS)) {
|
|
10555
|
+
out.add(entry);
|
|
10556
|
+
}
|
|
10557
|
+
}
|
|
10558
|
+
}
|
|
10559
|
+
for (const child of wn.children) {
|
|
10560
|
+
if (child.isWindow && !child.isPopup) walk(child);
|
|
10561
|
+
}
|
|
10562
|
+
};
|
|
10563
|
+
walk(this);
|
|
10564
|
+
return [...out];
|
|
10145
10565
|
}
|
|
10146
10566
|
|
|
10147
10567
|
_dndTargetCount() {
|
|
@@ -11001,6 +11421,105 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11001
11421
|
return true;
|
|
11002
11422
|
}
|
|
11003
11423
|
|
|
11424
|
+
/** One layout pass at the window's size, with the content floors it
|
|
11425
|
+
* needs: fresh ones when something changed them, the ones in hand during
|
|
11426
|
+
* a live resize, none when nothing moved them. */
|
|
11427
|
+
_layoutStep(width, height) {
|
|
11428
|
+
if (!this._floorsDirty && this._floorsWidth === width) {
|
|
11429
|
+
this._layoutRoot(width, height);
|
|
11430
|
+
} else if (this._deferContentFloors(width)) {
|
|
11431
|
+
this._scheduleFloorsCatchUp();
|
|
11432
|
+
this._layoutRoot(width, height);
|
|
11433
|
+
} else {
|
|
11434
|
+
this._applyContentFloors(width, height);
|
|
11435
|
+
}
|
|
11436
|
+
}
|
|
11437
|
+
|
|
11438
|
+
/**
|
|
11439
|
+
* Resolve the `@container` blocks against the layout just produced, and
|
|
11440
|
+
* lay out again while an answer moves — a container's size is what a pass
|
|
11441
|
+
* *produces*, so it can only be asked about afterwards, and a block that
|
|
11442
|
+
* changed may carry layout properties. `relayout` is whatever pass the
|
|
11443
|
+
* caller runs: `_layoutStep` in a flush, `measure()` while an auto-sized
|
|
11444
|
+
* window is working out how big to be.
|
|
11445
|
+
*
|
|
11446
|
+
* Bounded. Two passes settle the common case (a block that matches at the
|
|
11447
|
+
* width the content took), and nested containers can honestly need a
|
|
11448
|
+
* third — an outer answer moving an inner container past one of its own
|
|
11449
|
+
* thresholds — so the cap is a small fixed number rather than "once".
|
|
11450
|
+
* What it must not do is chase a design no size satisfies; that is
|
|
11451
|
+
* detected per node inside `_resolveContainerQueries`, and pinned.
|
|
11452
|
+
*/
|
|
11453
|
+
_settleContainerQueries(relayout) {
|
|
11454
|
+
if (this._containerQueryNodes.size === 0) return;
|
|
11455
|
+
const held = new Map();
|
|
11456
|
+
this._cqFresh = true;
|
|
11457
|
+
try {
|
|
11458
|
+
for (let pass = 0; pass < CONTAINER_QUERY_PASSES; pass++) {
|
|
11459
|
+
if (!this._resolveContainerQueries(held)) return;
|
|
11460
|
+
relayout();
|
|
11461
|
+
}
|
|
11462
|
+
if (DEV && this._resolveContainerQueries(held, false)) {
|
|
11463
|
+
console.warn(
|
|
11464
|
+
'react-x11: "@container" blocks did not settle in ' +
|
|
11465
|
+
`${CONTAINER_QUERY_PASSES} layout passes; the last one stands. ` +
|
|
11466
|
+
'More than two nested containers, each changing the next, is ' +
|
|
11467
|
+
'the shape that gets here (docs/styling.md#container-queries).',
|
|
11468
|
+
);
|
|
11469
|
+
}
|
|
11470
|
+
} finally {
|
|
11471
|
+
this._cqFresh = false;
|
|
11472
|
+
}
|
|
11473
|
+
}
|
|
11474
|
+
|
|
11475
|
+
/**
|
|
11476
|
+
* One round of the above: every dependent whose blocks answer differently
|
|
11477
|
+
* against the containers as they are now is re-resolved, and the caller
|
|
11478
|
+
* hears whether any was. With `apply` false it only answers.
|
|
11479
|
+
*
|
|
11480
|
+
* `held` is what each node has answered so far this frame. A node that
|
|
11481
|
+
* comes back to an answer it already held is a design that oscillates —
|
|
11482
|
+
* the block moves the size it asks about, which CSS forbids by
|
|
11483
|
+
* construction (size containment) and yoga cannot — so it is **pinned**:
|
|
11484
|
+
* the answer it has stands, and stays until the container's size moves
|
|
11485
|
+
* for some other reason. Without the pin the next frame would find the
|
|
11486
|
+
* other answer, apply it, get the other size, and strobe on every layout.
|
|
11487
|
+
*/
|
|
11488
|
+
_resolveContainerQueries(held, apply = true) {
|
|
11489
|
+
let changed = false;
|
|
11490
|
+
for (const node of [...this._containerQueryNodes]) {
|
|
11491
|
+
if (node.destroyed) {
|
|
11492
|
+
this._containerQueryNodes.delete(node);
|
|
11493
|
+
continue;
|
|
11494
|
+
}
|
|
11495
|
+
const cq = node._cq;
|
|
11496
|
+
const containers = node._containerSizes();
|
|
11497
|
+
const key = containersKey(containers);
|
|
11498
|
+
if (cq.pin) {
|
|
11499
|
+
if (cq.pin.key === key) continue;
|
|
11500
|
+
cq.pin = null;
|
|
11501
|
+
}
|
|
11502
|
+
const answers = containerAnswers(node._baseStyle, containers);
|
|
11503
|
+
if (answers === cq.answers) continue;
|
|
11504
|
+
if (!apply) return true;
|
|
11505
|
+
let seen = held.get(node);
|
|
11506
|
+
if (seen?.includes(answers)) {
|
|
11507
|
+
cq.pin = { key, containers: cq.containers };
|
|
11508
|
+
if (DEV) node._warnContainerOscillation();
|
|
11509
|
+
continue;
|
|
11510
|
+
}
|
|
11511
|
+
if (!seen) held.set(node, (seen = [cq.answers]));
|
|
11512
|
+
seen.push(answers);
|
|
11513
|
+
node._sizeQueriesChanged();
|
|
11514
|
+
changed = true;
|
|
11515
|
+
}
|
|
11516
|
+
// a block may carry layout properties, so the floors measured from the
|
|
11517
|
+
// styles it is replacing are not the answer any more — the same debt a
|
|
11518
|
+
// window query leaves (`_resolveSizeQueries`)
|
|
11519
|
+
if (changed) this._floorsDirty = true;
|
|
11520
|
+
return changed;
|
|
11521
|
+
}
|
|
11522
|
+
|
|
11004
11523
|
/**
|
|
11005
11524
|
* A node in this window has a loop declared on it. Registration is what
|
|
11006
11525
|
* makes the window watch its own visibility — and only then: a
|
|
@@ -11375,13 +11894,12 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11375
11894
|
// ledger's rect moves with the shift (issue #398).
|
|
11376
11895
|
this._laidOut = true;
|
|
11377
11896
|
this._resolveSizeQueries(width, height);
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
this._applyContentFloors(width, height);
|
|
11897
|
+
this._layoutStep(width, height);
|
|
11898
|
+
// `@container` blocks are answered by the pass, not before it, and a
|
|
11899
|
+
// changed answer is one more pass — before `absolutize`, so the layout
|
|
11900
|
+
// diff below sees one arrangement against the last frame's
|
|
11901
|
+
if (this._containerQueryNodes.size !== 0) {
|
|
11902
|
+
this._settleContainerQueries(() => this._layoutStep(width, height));
|
|
11385
11903
|
}
|
|
11386
11904
|
this.abs = { x: 0, y: 0, width, height };
|
|
11387
11905
|
this._placed = true;
|