react-x11 2.6.1 → 2.8.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 +367 -3
- package/src/cocoa/bezels.js +51 -1
- package/src/cocoa/dnd.js +358 -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 +274 -33
- package/src/cocoa/promotion.js +708 -0
- package/src/cocoa/statusitem.js +112 -0
- package/src/cocoa/window.js +113 -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 +137 -11
- package/src/errors.js +6 -3
- package/src/filedialog.js +81 -16
- package/src/index.d.ts +29 -2
- package/src/index.js +17 -0
- package/src/launcher.js +170 -0
- package/src/launcherhooks.js +81 -0
- package/src/nodes.js +604 -37
- 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/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
|
|
|
@@ -1888,6 +1913,10 @@ export class Node {
|
|
|
1888
1913
|
this._floorMeasureMode = null;
|
|
1889
1914
|
this.root = null; // owning WindowNode once attached
|
|
1890
1915
|
this.hidden = false;
|
|
1916
|
+
// Composited on a layer of its own above the window's bitmap, by a
|
|
1917
|
+
// presenter that can (src/cocoa/promotion.js): the paint walk leaves a
|
|
1918
|
+
// hole where it is, and the presenter draws it — the `<glarea>` idiom.
|
|
1919
|
+
this._promoted = false;
|
|
1891
1920
|
this.destroyed = false;
|
|
1892
1921
|
// absolute rect within the owning window, filled by absolutize()
|
|
1893
1922
|
this.abs = { x: 0, y: 0, width: 0, height: 0 };
|
|
@@ -1937,6 +1966,13 @@ export class Node {
|
|
|
1937
1966
|
// `null` is "commitMount is still to come", `false` is "it has been and
|
|
1938
1967
|
// gone", and an Error is one waiting for it
|
|
1939
1968
|
this._tokenError = null;
|
|
1969
|
+
// `'@container …'` blocks (styles.js), on the nodes that carry them and
|
|
1970
|
+
// null on every other node: the style the record was built for, the
|
|
1971
|
+
// container names it asks about (`''` for the unnamed ones), the sizes
|
|
1972
|
+
// and answers the blocks last resolved to, and the pin an oscillating
|
|
1973
|
+
// design is held at — see WindowNode._resolveContainerQueries. Before
|
|
1974
|
+
// `_syncStyle`, which reads and writes it.
|
|
1975
|
+
this._cq = null;
|
|
1940
1976
|
this._syncStyle(props);
|
|
1941
1977
|
this.yoga = yoga ? createLayoutNode() : null;
|
|
1942
1978
|
if (this.yoga) {
|
|
@@ -1993,7 +2029,8 @@ export class Node {
|
|
|
1993
2029
|
this.states[':disabled'] = Boolean(props.disabled);
|
|
1994
2030
|
// window size queries fold into the base before state blocks, so a
|
|
1995
2031
|
// `:hover` inside the wide layout still wins over the wide layout
|
|
1996
|
-
const
|
|
2032
|
+
const kinds = queryKinds(this._baseStyle);
|
|
2033
|
+
const queried = (kinds & QUERY_SIZE) !== 0;
|
|
1997
2034
|
if (queried !== this._queried) {
|
|
1998
2035
|
this._queried = queried;
|
|
1999
2036
|
const root = this.root;
|
|
@@ -2009,7 +2046,7 @@ export class Node {
|
|
|
2009
2046
|
// assigned, so the first pass has nowhere to register and a "did it
|
|
2010
2047
|
// change" guard would keep it unregistered forever. Set.add is
|
|
2011
2048
|
// idempotent and these blocks are rare.
|
|
2012
|
-
const asks =
|
|
2049
|
+
const asks = (kinds & QUERY_SUPPORTS) !== 0;
|
|
2013
2050
|
this._supportsQueried = asks;
|
|
2014
2051
|
if (this.root?._supportsQueryNodes) {
|
|
2015
2052
|
if (asks) this.root._supportsQueryNodes.add(this);
|
|
@@ -2029,13 +2066,53 @@ export class Node {
|
|
|
2029
2066
|
if (wantsAttention) this.root._attentionNodes.add(this);
|
|
2030
2067
|
else this.root._attentionNodes.delete(this);
|
|
2031
2068
|
}
|
|
2032
|
-
|
|
2069
|
+
// `@container` blocks keep a third registry: what re-resolves them is a
|
|
2070
|
+
// layout pass moving the container they ask about — neither a resize
|
|
2071
|
+
// nor the server's answer. The record exists only on the nodes that
|
|
2072
|
+
// ask, so every other node pays one bit test here and nothing below.
|
|
2073
|
+
// Insertion registers through `_registerSizeQueries`; this is for a
|
|
2074
|
+
// style that starts or stops asking on a node already in a window.
|
|
2075
|
+
const asksContainers = (kinds & QUERY_CONTAINER) !== 0;
|
|
2076
|
+
let containers = null;
|
|
2077
|
+
if (asksContainers) {
|
|
2078
|
+
const base = this._baseStyle;
|
|
2079
|
+
let cq = this._cq;
|
|
2080
|
+
if (cq === null || cq.style !== base) {
|
|
2081
|
+
if (cq === null) {
|
|
2082
|
+
cq = this._cq = {
|
|
2083
|
+
style: base,
|
|
2084
|
+
names: null,
|
|
2085
|
+
containers: null,
|
|
2086
|
+
answers: '',
|
|
2087
|
+
pin: null,
|
|
2088
|
+
warned: false,
|
|
2089
|
+
};
|
|
2090
|
+
} else {
|
|
2091
|
+
// a different style asks different questions, and a pin held for
|
|
2092
|
+
// the old one is not an answer to the new one
|
|
2093
|
+
cq.style = base;
|
|
2094
|
+
cq.pin = null;
|
|
2095
|
+
}
|
|
2096
|
+
cq.names = containerQueryNames(base);
|
|
2097
|
+
}
|
|
2098
|
+
this.root?._containerQueryNodes?.add(this);
|
|
2099
|
+
containers = this._pinnedContainerSizes();
|
|
2100
|
+
} else if (this._cq !== null) {
|
|
2101
|
+
this._cq = null;
|
|
2102
|
+
this.root?._containerQueryNodes?.delete(this);
|
|
2103
|
+
}
|
|
2104
|
+
if (queried || asks || asksContainers) {
|
|
2033
2105
|
this._baseStyle = resolveQueries(this._baseStyle, {
|
|
2034
2106
|
size: this.root?.querySize ?? null,
|
|
2035
2107
|
// null before the window is realized, which reads as "not
|
|
2036
2108
|
// supported" — the fallback design is the one that works everywhere
|
|
2037
2109
|
supports: this.root?.capabilities ?? null,
|
|
2110
|
+
containers,
|
|
2038
2111
|
});
|
|
2112
|
+
if (asksContainers) {
|
|
2113
|
+
this._cq.containers = containers;
|
|
2114
|
+
this._cq.answers = containerAnswers(this._baseStyle, containers);
|
|
2115
|
+
}
|
|
2039
2116
|
}
|
|
2040
2117
|
this._stateful = hasStateStyles(this._baseStyle);
|
|
2041
2118
|
// The scale multiplies *after* every merge — state blocks, queries,
|
|
@@ -2085,7 +2162,7 @@ export class Node {
|
|
|
2085
2162
|
const duration = transitionFor(target, prop);
|
|
2086
2163
|
if (duration <= 0) continue;
|
|
2087
2164
|
if (interpolate(from, to, 0.5) === null) continue; // no midpoint: snap
|
|
2088
|
-
|
|
2165
|
+
const entry = {
|
|
2089
2166
|
from,
|
|
2090
2167
|
to,
|
|
2091
2168
|
duration,
|
|
@@ -2094,8 +2171,23 @@ export class Node {
|
|
|
2094
2171
|
// be seconds old — and the first tick would then find the
|
|
2095
2172
|
// transition already over and jump straight to the end
|
|
2096
2173
|
start: now(),
|
|
2097
|
-
}
|
|
2098
|
-
this.
|
|
2174
|
+
};
|
|
2175
|
+
const previous = this._anim?.get(prop);
|
|
2176
|
+
(this._anim ??= new Map()).set(prop, entry);
|
|
2177
|
+
// A presenter that can run it in the render server takes it here:
|
|
2178
|
+
// the node's style then goes straight to the target — the layer's
|
|
2179
|
+
// model value — and the one frame that sends it carries the
|
|
2180
|
+
// animation with it (src/cocoa/presenter.js). Declined, or with no
|
|
2181
|
+
// such presenter, the window's frame clock runs it as it always has.
|
|
2182
|
+
if (this._offload(prop, entry)) {
|
|
2183
|
+
entry.offloaded = true;
|
|
2184
|
+
this.root?.invalidate(false, damageForAnimation(this), 'animation');
|
|
2185
|
+
} else {
|
|
2186
|
+
// …and one the presenter had must not keep running underneath the
|
|
2187
|
+
// values the clock is about to write
|
|
2188
|
+
if (previous?.offloaded) this._cancelOffload(prop, previous);
|
|
2189
|
+
this.root?._startAnimating(this);
|
|
2190
|
+
}
|
|
2099
2191
|
}
|
|
2100
2192
|
}
|
|
2101
2193
|
// After the transitions, before the style is assembled: a loop that just
|
|
@@ -2187,10 +2279,78 @@ export class Node {
|
|
|
2187
2279
|
|
|
2188
2280
|
_animatedValues() {
|
|
2189
2281
|
const values = {};
|
|
2190
|
-
for (const [prop, a] of this._anim)
|
|
2282
|
+
for (const [prop, a] of this._anim) {
|
|
2283
|
+
// an offloaded property shows its target: the render server draws the
|
|
2284
|
+
// motion over the model value, and the model is the style
|
|
2285
|
+
if (!a.offloaded) values[prop] = a.value ?? a.from;
|
|
2286
|
+
}
|
|
2191
2287
|
return values;
|
|
2192
2288
|
}
|
|
2193
2289
|
|
|
2290
|
+
// --- the presenter's half of an animation ---------------------------------
|
|
2291
|
+
//
|
|
2292
|
+
// Two feature-detected hooks on the window (src/cocoa/window.js: the
|
|
2293
|
+
// layer presenter, and the surface presenter's layer promotion —
|
|
2294
|
+
// src/cocoa/promotion.js): `animateNode(node, prop, entry)` answers true
|
|
2295
|
+
// when the presenter will run the entry itself, `cancelNodeAnimation(node,
|
|
2296
|
+
// prop)` stops what it runs for the property, and the presenter calls back
|
|
2297
|
+
// through `_offloadEnded` / `_offloadDeclined` below. An entry the
|
|
2298
|
+
// presenter took is `offloaded`: it stays in `_anim` — so a retarget, a
|
|
2299
|
+
// loop-stop rule and `sameAnimation` all see it — but it contributes no
|
|
2300
|
+
// value to the style, is skipped by the tick, and keeps the node out of the
|
|
2301
|
+
// window's animating set. The X11 path has none of these hooks and is
|
|
2302
|
+
// byte-identical (docs/architecture/animation.md §4).
|
|
2303
|
+
|
|
2304
|
+
_offload(prop, entry) {
|
|
2305
|
+
const wnd = this.root?.window;
|
|
2306
|
+
if (typeof wnd?.animateNode !== 'function') return false;
|
|
2307
|
+
return wnd.animateNode(this, prop, entry) === true;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
_cancelOffload(prop, entry) {
|
|
2311
|
+
if (!entry?.offloaded) return;
|
|
2312
|
+
this.root?.window?.cancelNodeAnimation?.(this, prop);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
/** The presenter is done with `entry` — it ran out, or its layer went.
|
|
2316
|
+
* A transition is over either way (the model is the target). A loop
|
|
2317
|
+
* never ends on its own, so a loop that comes back this way lost its
|
|
2318
|
+
* layer, and the frame clock takes it over rather than letting it stop. */
|
|
2319
|
+
_offloadEnded(prop, entry) {
|
|
2320
|
+
if (this._anim?.get(prop) !== entry) return;
|
|
2321
|
+
if (entry.loop && !this.destroyed) {
|
|
2322
|
+
this._offloadDeclined(prop, entry);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
this._anim.delete(prop);
|
|
2326
|
+
if (!this._anim.size) this.root?._animating.delete(this);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
/** The presenter could not run `entry` after all — the node turned into a
|
|
2330
|
+
* raster between the swap and the frame. The frame clock takes it from
|
|
2331
|
+
* the top; the property's declared start is where the pixels still are. */
|
|
2332
|
+
_offloadDeclined(prop, entry) {
|
|
2333
|
+
if (this._anim?.get(prop) !== entry || this.destroyed) return;
|
|
2334
|
+
entry.offloaded = false;
|
|
2335
|
+
entry.start = now();
|
|
2336
|
+
this.style = { ...this._targetStyle, ...this._animatedValues() };
|
|
2337
|
+
this.root?._startAnimating(this);
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
/** Keep the frame clock running only for what the clock itself animates;
|
|
2341
|
+
* an offloaded-only node needs one frame — the one that sends the model
|
|
2342
|
+
* and the animation — and not a loop of them. */
|
|
2343
|
+
_scheduleAnimationFrames() {
|
|
2344
|
+
for (const a of this._anim?.values() ?? []) {
|
|
2345
|
+
if (!a.offloaded) {
|
|
2346
|
+
this.root?._startAnimating(this);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
this.root?._animating.delete(this);
|
|
2351
|
+
this.root?.invalidate(false, damageForAnimation(this), 'animation');
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2194
2354
|
/**
|
|
2195
2355
|
* The style declared a set of loops (`animation`, styles.js): remember
|
|
2196
2356
|
* them and reconcile what is running against them.
|
|
@@ -2237,6 +2397,7 @@ export class Node {
|
|
|
2237
2397
|
if (!a.loop) continue;
|
|
2238
2398
|
if (running && specs.some((spec) => spec.prop === prop)) continue;
|
|
2239
2399
|
anim.delete(prop);
|
|
2400
|
+
this._cancelOffload(prop, a);
|
|
2240
2401
|
changed = true;
|
|
2241
2402
|
if (isLayoutProp(prop)) layoutTouched = true;
|
|
2242
2403
|
}
|
|
@@ -2249,13 +2410,28 @@ export class Node {
|
|
|
2249
2410
|
// spinner that jumps back to the start whenever anything above it
|
|
2250
2411
|
// re-rendered — which is the frame after every state change in the
|
|
2251
2412
|
// app.
|
|
2252
|
-
if (current?.loop && sameAnimation(current, spec))
|
|
2253
|
-
|
|
2413
|
+
if (current?.loop && sameAnimation(current, spec)) {
|
|
2414
|
+
// A loop the clock started before the window had a presenter —
|
|
2415
|
+
// one declared at mount runs from `_setRoot`, before `realize` —
|
|
2416
|
+
// moves over the first time a presenter can take it. Its phase is
|
|
2417
|
+
// the render server's from here, which is what a restart costs.
|
|
2418
|
+
if (!current.offloaded && this._offload(spec.prop, current)) {
|
|
2419
|
+
current.offloaded = true;
|
|
2420
|
+
changed = true;
|
|
2421
|
+
}
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
// a changed declaration, or a transition the loop takes over from:
|
|
2425
|
+
// whatever the presenter ran for the property stops first
|
|
2426
|
+
if (current?.offloaded) this._cancelOffload(spec.prop, current);
|
|
2427
|
+
const entry = {
|
|
2254
2428
|
...spec,
|
|
2255
2429
|
loop: true,
|
|
2256
2430
|
start: now(),
|
|
2257
2431
|
value: animationValueAt(spec, 0),
|
|
2258
|
-
}
|
|
2432
|
+
};
|
|
2433
|
+
(this._anim ??= new Map()).set(spec.prop, entry);
|
|
2434
|
+
if (this._offload(spec.prop, entry)) entry.offloaded = true;
|
|
2259
2435
|
changed = true;
|
|
2260
2436
|
if (isLayoutProp(spec.prop)) layoutTouched = true;
|
|
2261
2437
|
}
|
|
@@ -2269,7 +2445,7 @@ export class Node {
|
|
|
2269
2445
|
// a stop has to leave the frame clock idle, and a tick is exactly what
|
|
2270
2446
|
// there may never be another of.
|
|
2271
2447
|
if (!this._anim?.size) this.root?._animating.delete(this);
|
|
2272
|
-
if (running) this.
|
|
2448
|
+
if (running) this._scheduleAnimationFrames();
|
|
2273
2449
|
if (!write) return true;
|
|
2274
2450
|
if (layoutTouched && this.yoga) {
|
|
2275
2451
|
applyLayoutStyle(this.yoga, this.style, before);
|
|
@@ -2316,19 +2492,23 @@ export class Node {
|
|
|
2316
2492
|
_tickAnimations(now) {
|
|
2317
2493
|
if (!this._anim?.size) return false;
|
|
2318
2494
|
let layoutChanged = false;
|
|
2495
|
+
let ticking = 0; // entries the clock runs, as against the presenter's
|
|
2319
2496
|
const before = this.style;
|
|
2320
2497
|
for (const [prop, a] of this._anim) {
|
|
2498
|
+
if (a.offloaded) continue;
|
|
2321
2499
|
if (a.loop) {
|
|
2322
2500
|
// No end to test for and no rounding to accumulate: the phase is a
|
|
2323
2501
|
// modulo of the elapsed time, so a bar that has been going for an
|
|
2324
2502
|
// hour is exactly where the clock says.
|
|
2325
2503
|
a.value = animationValueAt(a, now - a.start);
|
|
2326
2504
|
if (isLayoutProp(prop)) layoutChanged = true;
|
|
2505
|
+
ticking++;
|
|
2327
2506
|
continue;
|
|
2328
2507
|
}
|
|
2329
2508
|
const t = a.duration > 0 ? Math.min(1, (now - a.start) / a.duration) : 1;
|
|
2330
2509
|
a.value = t >= 1 ? a.to : (interpolate(a.from, a.to, ease(t)) ?? a.to);
|
|
2331
2510
|
if (t >= 1) this._anim.delete(prop);
|
|
2511
|
+
else ticking++;
|
|
2332
2512
|
if (isLayoutProp(prop)) layoutChanged = true;
|
|
2333
2513
|
}
|
|
2334
2514
|
this.style = this._anim.size
|
|
@@ -2351,7 +2531,7 @@ export class Node {
|
|
|
2351
2531
|
// it *per frame*: a transitioned `color` is a new ink every frame, for
|
|
2352
2532
|
// this node and for everything inheriting from it.
|
|
2353
2533
|
if (inheritedTextChanged(this.style, before)) this._retextSubtree();
|
|
2354
|
-
return
|
|
2534
|
+
return ticking > 0;
|
|
2355
2535
|
}
|
|
2356
2536
|
|
|
2357
2537
|
/**
|
|
@@ -2398,11 +2578,129 @@ export class Node {
|
|
|
2398
2578
|
if (this._wantsAttention && this.root?._attentionNodes) {
|
|
2399
2579
|
this.root._attentionNodes.add(this);
|
|
2400
2580
|
}
|
|
2581
|
+
if (this._cq !== null && this.root?._containerQueryNodes) {
|
|
2582
|
+
this.root._containerQueryNodes.add(this);
|
|
2583
|
+
// it can see the containers above it now — the constructor's
|
|
2584
|
+
// resolution had no ancestors to find one in
|
|
2585
|
+
this._sizeQueriesChanged();
|
|
2586
|
+
}
|
|
2401
2587
|
for (const child of this.children) {
|
|
2402
2588
|
if (!child.isWindow) child._registerSizeQueries();
|
|
2403
2589
|
}
|
|
2404
2590
|
}
|
|
2405
2591
|
|
|
2592
|
+
/**
|
|
2593
|
+
* The sizes this node's `@container` blocks resolve against: for each name
|
|
2594
|
+
* the style asks about, the nearest ancestor declaring it, in this node's
|
|
2595
|
+
* **logical** pixels — the unit the threshold beside `width: 400` was
|
|
2596
|
+
* written in, so the two numbers mean the same thing. Yoga's computed
|
|
2597
|
+
* size rather than `abs`: inside a flush, `abs` is still the previous
|
|
2598
|
+
* frame's.
|
|
2599
|
+
*
|
|
2600
|
+
* A container that has not been laid out yet contributes nothing, so its
|
|
2601
|
+
* blocks do not apply — the way a capability block does not before the
|
|
2602
|
+
* window exists: the fallback design is the one that works everywhere.
|
|
2603
|
+
* "Laid out" is `_placed` between frames and every attached node while
|
|
2604
|
+
* the window is settling a pass it just ran (`_cqFresh`); a fresh yoga
|
|
2605
|
+
* node answers NaN. Null when no container is known, which is the
|
|
2606
|
+
* identity `resolveQueries` keeps.
|
|
2607
|
+
*/
|
|
2608
|
+
_containerSizes() {
|
|
2609
|
+
const names = this._cq?.names;
|
|
2610
|
+
if (!names) return null;
|
|
2611
|
+
let sizes = null;
|
|
2612
|
+
const s = this.scale || 1;
|
|
2613
|
+
const fresh = Boolean(this.root?._cqFresh);
|
|
2614
|
+
for (const name of names) {
|
|
2615
|
+
const c = this._containerFor(name);
|
|
2616
|
+
if (!c) {
|
|
2617
|
+
// In a window and nothing above declares one: a forgotten
|
|
2618
|
+
// declaration, not a component rendered outside its context — that
|
|
2619
|
+
// is what the *named* form is for, and a missing name applies
|
|
2620
|
+
// nothing quietly. `root` rather than `parent`: React builds a
|
|
2621
|
+
// subtree bottom-up, so a node can have a parent and no window yet,
|
|
2622
|
+
// and the container it will find is further up.
|
|
2623
|
+
if (DEV && name === '' && this.root) this._noContainer();
|
|
2624
|
+
continue;
|
|
2625
|
+
}
|
|
2626
|
+
if (!c.yoga || !(fresh || c._placed)) continue;
|
|
2627
|
+
const width = c.yoga.getComputedWidth() / s;
|
|
2628
|
+
const height = c.yoga.getComputedHeight() / s;
|
|
2629
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) continue;
|
|
2630
|
+
(sizes ??= {})[name] = { width, height };
|
|
2631
|
+
}
|
|
2632
|
+
return sizes;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
/** `_containerSizes()`, unless this node is pinned at the sizes it is
|
|
2636
|
+
* looking at — then the sizes its held answer came from, so a restyle
|
|
2637
|
+
* arriving from React does not undo what the layout pass decided. */
|
|
2638
|
+
_pinnedContainerSizes() {
|
|
2639
|
+
const live = this._containerSizes();
|
|
2640
|
+
const cq = this._cq;
|
|
2641
|
+
const pin = cq.pin;
|
|
2642
|
+
if (!pin) return live;
|
|
2643
|
+
if (pin.key === containersKey(live)) return pin.containers;
|
|
2644
|
+
cq.pin = null;
|
|
2645
|
+
return live;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
/**
|
|
2649
|
+
* The nearest ancestor whose style declares `container` — any container
|
|
2650
|
+
* for the unnamed query (`''`), the one carrying `name` otherwise, however
|
|
2651
|
+
* many nearer containers that reaches past. A window ends the walk after
|
|
2652
|
+
* offering itself, and a window asks nothing: a `<popup>` inside a
|
|
2653
|
+
* container is a root of its own and asks its own window with `@width`.
|
|
2654
|
+
*
|
|
2655
|
+
* Walked rather than cached: it is a dozen property reads per dependent
|
|
2656
|
+
* per layout pass, and a cache would have to follow every insert, every
|
|
2657
|
+
* reorder and every `container` value that changes above.
|
|
2658
|
+
*/
|
|
2659
|
+
_containerFor(name) {
|
|
2660
|
+
if (this.isWindow) return null;
|
|
2661
|
+
for (let n = this.parent; n; n = n.parent) {
|
|
2662
|
+
const c = n.style?.container;
|
|
2663
|
+
if (name === '' ? c === true || typeof c === 'string' : c === name) {
|
|
2664
|
+
return n;
|
|
2665
|
+
}
|
|
2666
|
+
if (n.isWindow) break;
|
|
2667
|
+
}
|
|
2668
|
+
return null;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
_noContainer() {
|
|
2672
|
+
this._tokenProblem(
|
|
2673
|
+
[
|
|
2674
|
+
`react-x11: <${this.kind}> has an "@container" block and no ` +
|
|
2675
|
+
'container above it — declare one with `container: true` in an ' +
|
|
2676
|
+
"ancestor's style (or name it and ask for it by name), or ask " +
|
|
2677
|
+
'the window with "@width"',
|
|
2678
|
+
],
|
|
2679
|
+
true,
|
|
2680
|
+
'The block does not apply and the app carries on',
|
|
2681
|
+
);
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
/** Said once per node, in development: a design that cannot settle looks
|
|
2685
|
+
* like a layout bug, and the frame it is pinned at is the only clue. */
|
|
2686
|
+
_warnContainerOscillation() {
|
|
2687
|
+
const cq = this._cq;
|
|
2688
|
+
if (cq.warned) return;
|
|
2689
|
+
cq.warned = true;
|
|
2690
|
+
const asked = [...(cq.names ?? [])]
|
|
2691
|
+
.map((n) => (n === '' ? 'its container' : `"${n}"`))
|
|
2692
|
+
.join(', ');
|
|
2693
|
+
console.warn(
|
|
2694
|
+
`react-x11: the "@container" blocks on <${this.kind}> cannot settle: ` +
|
|
2695
|
+
`a block that matches at one size of ${asked} changes that size to ` +
|
|
2696
|
+
'one where it no longer matches, and back. A container query must ' +
|
|
2697
|
+
'not move the size it asks about — give the container a size of its ' +
|
|
2698
|
+
'own, or minWidth: 0 and a flexBasis so its content cannot grow it. ' +
|
|
2699
|
+
'The current answer is held until the container moves for another ' +
|
|
2700
|
+
'reason (docs/styling.md#container-queries).',
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2406
2704
|
/** Style names this element claims as its own semantics (see WindowNode).
|
|
2407
2705
|
* Registered elements declare theirs to `registerElement`, so the common
|
|
2408
2706
|
* case needs no subclass. */
|
|
@@ -2686,12 +2984,14 @@ export class Node {
|
|
|
2686
2984
|
* swallow the error instead of raising it late. Those throw at once, like
|
|
2687
2985
|
* the keyed reorder they resemble.
|
|
2688
2986
|
*/
|
|
2689
|
-
_tokenProblem(problems, mounting) {
|
|
2987
|
+
_tokenProblem(problems, mounting, consequence = undefined) {
|
|
2690
2988
|
if (!STRICT_TOKENS) {
|
|
2691
2989
|
// every one of them: two misspellings in a style are two things to
|
|
2692
2990
|
// fix, and a report that named only the first would send someone back
|
|
2693
2991
|
// for a second run to find the second
|
|
2694
|
-
for (const message of problems)
|
|
2992
|
+
for (const message of problems) {
|
|
2993
|
+
reportStyleError(this, message, consequence);
|
|
2994
|
+
}
|
|
2695
2995
|
return;
|
|
2696
2996
|
}
|
|
2697
2997
|
const error = new Error(problems[0]);
|
|
@@ -2699,10 +2999,16 @@ export class Node {
|
|
|
2699
2999
|
else throw error;
|
|
2700
3000
|
}
|
|
2701
3001
|
|
|
2702
|
-
/** The owning window resized
|
|
2703
|
-
*
|
|
3002
|
+
/** The owning window resized, the server's answer moved, or a layout pass
|
|
3003
|
+
* moved a container this node asks about: re-resolve, since a query block
|
|
3004
|
+
* may now match that did not, or the other way round. */
|
|
2704
3005
|
_sizeQueriesChanged() {
|
|
2705
|
-
if (
|
|
3006
|
+
if (
|
|
3007
|
+
!(this._queried || this._supportsQueried || this._cq !== null) ||
|
|
3008
|
+
this.destroyed
|
|
3009
|
+
) {
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
2706
3012
|
const before = this.style;
|
|
2707
3013
|
// a query block may name `fontSize`, and `_syncStyle` → `_retarget` is
|
|
2708
3014
|
// what pushes that into the subtree; only the node-local text props are
|
|
@@ -2710,7 +3016,19 @@ export class Node {
|
|
|
2710
3016
|
this._syncStyle(this.props);
|
|
2711
3017
|
if (localTextStyleChanged(this.style, before)) this._textContentChanged();
|
|
2712
3018
|
if (this.yoga && this.style !== before) {
|
|
2713
|
-
|
|
3019
|
+
// A block that moved a layout property changed the tree the content
|
|
3020
|
+
// floors were measured from — the debt a style change from React
|
|
3021
|
+
// leaves too (`invalidate`, reason 'props'). It has to be marked as a
|
|
3022
|
+
// *content* change: the live-resize deferral takes a plain
|
|
3023
|
+
// `_floorsDirty` for the drag itself and lays out against the floors
|
|
3024
|
+
// in hand, which are the old arrangement's, and by the time the
|
|
3025
|
+
// catch-up looks the dirty flags are spent and no leaf's height moved
|
|
3026
|
+
// — so the floor of a card that turned from a row into a column would
|
|
3027
|
+
// stay the row's, and yoga would squeeze the column down to it.
|
|
3028
|
+
if (applyLayoutStyle(this.yoga, this.style, before) && this.root) {
|
|
3029
|
+
this.root._floorsDirty = true;
|
|
3030
|
+
this.root._floorsContentDirty = true;
|
|
3031
|
+
}
|
|
2714
3032
|
}
|
|
2715
3033
|
}
|
|
2716
3034
|
|
|
@@ -3699,11 +4017,58 @@ export class Node {
|
|
|
3699
4017
|
this.yoga.getComputedWidth(),
|
|
3700
4018
|
this.yoga.getComputedHeight(),
|
|
3701
4019
|
);
|
|
4020
|
+
if (this.props.onLayout) this._reportLayout();
|
|
3702
4021
|
for (const child of this.children) {
|
|
3703
4022
|
if (!child.isWindow) child.absolutize(this.abs.x, this.abs.y);
|
|
3704
4023
|
}
|
|
3705
4024
|
}
|
|
3706
4025
|
|
|
4026
|
+
/**
|
|
4027
|
+
* `onLayout`: the rect a layout pass gave this node, reported when it
|
|
4028
|
+
* changed — React Native's contract, and the seam for a decision that is
|
|
4029
|
+
* not a style (docs/react-features.md): how many columns to build, which
|
|
4030
|
+
* component to render. Where the decision *is* a style, a container
|
|
4031
|
+
* query answers it in the same frame instead (docs/styling.md).
|
|
4032
|
+
*
|
|
4033
|
+
* `x`/`y` are the position **within the parent as laid out** — yoga's
|
|
4034
|
+
* answer, which a scroll does not move — rather than the window
|
|
4035
|
+
* coordinates `abs` holds: a list scrolling under the pointer must not
|
|
4036
|
+
* re-render every row on every notch. Logical pixels, this node's own,
|
|
4037
|
+
* the same division `measure()` makes.
|
|
4038
|
+
*
|
|
4039
|
+
* Deferred, like `onViewport`: this runs inside the layout pass, and a
|
|
4040
|
+
* `setState` from the handler would re-enter it. One report per frame,
|
|
4041
|
+
* because `absolutize` runs once, after the container blocks have
|
|
4042
|
+
* settled — a card whose block changed its height reports the height it
|
|
4043
|
+
* ended the frame at, not the one it had between passes.
|
|
4044
|
+
*/
|
|
4045
|
+
_reportLayout() {
|
|
4046
|
+
const s = this.scale;
|
|
4047
|
+
const next = {
|
|
4048
|
+
x: this.yoga.getComputedLeft() / s,
|
|
4049
|
+
y: this.yoga.getComputedTop() / s,
|
|
4050
|
+
width: this.abs.width / s,
|
|
4051
|
+
height: this.abs.height / s,
|
|
4052
|
+
};
|
|
4053
|
+
const last = this._lastLayout;
|
|
4054
|
+
if (
|
|
4055
|
+
last &&
|
|
4056
|
+
last.x === next.x &&
|
|
4057
|
+
last.y === next.y &&
|
|
4058
|
+
last.width === next.width &&
|
|
4059
|
+
last.height === next.height
|
|
4060
|
+
) {
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
4063
|
+
this._lastLayout = next;
|
|
4064
|
+
setImmediate(() => {
|
|
4065
|
+
// the handler as it is *now*: React may have re-rendered in between
|
|
4066
|
+
const notify = this.props.onLayout;
|
|
4067
|
+
if (this.destroyed || !notify) return;
|
|
4068
|
+
callHandler(this, 'onLayout', notify, next);
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
|
|
3707
4072
|
/**
|
|
3708
4073
|
* absolutize's write to `abs`, funneled through one place so a bounded
|
|
3709
4074
|
* frame's layout diff sees every node the pass actually moved or resized.
|
|
@@ -4955,6 +5320,7 @@ export class Node {
|
|
|
4955
5320
|
ctx.clip();
|
|
4956
5321
|
}
|
|
4957
5322
|
for (const child of order) {
|
|
5323
|
+
if (child._promoted) continue; // on a layer of its own: a hole here
|
|
4958
5324
|
if (child._offscreen()) continue;
|
|
4959
5325
|
if (child._outsideDamage()) continue;
|
|
4960
5326
|
child.paint(ctx);
|
|
@@ -5458,8 +5824,14 @@ export class TextNode extends Node {
|
|
|
5458
5824
|
weight: base.weight,
|
|
5459
5825
|
style: base.style,
|
|
5460
5826
|
});
|
|
5461
|
-
const
|
|
5462
|
-
if (!
|
|
5827
|
+
const measured = font?.metrics?.(base.size)?.capHeight;
|
|
5828
|
+
if (!measured) return null; // no metrics: leave the box alone
|
|
5829
|
+
// Whole pixels: the trimmed box's top is the baseline less this, so a
|
|
5830
|
+
// fractional cap height — 9.15px for a 13px face — puts the baseline
|
|
5831
|
+
// between two rows, and the rasteriser lands the letters a row low on
|
|
5832
|
+
// one backend and half-covers two rows on the other. Rounded, the
|
|
5833
|
+
// baseline sits on a pixel wherever the box does.
|
|
5834
|
+
const capHeight = Math.round(measured);
|
|
5463
5835
|
const shift = halfLeading(layout);
|
|
5464
5836
|
const firstBaseline = shift + lines[0].baseline;
|
|
5465
5837
|
const lastBaseline = shift + lines[lines.length - 1].baseline;
|
|
@@ -6045,6 +6417,7 @@ export const Scrollable = (Base) =>
|
|
|
6045
6417
|
this.yoga.getComputedWidth(),
|
|
6046
6418
|
this.yoga.getComputedHeight(),
|
|
6047
6419
|
);
|
|
6420
|
+
if (this.props.onLayout) this._reportLayout();
|
|
6048
6421
|
this._absolutizeChildren(this.abs.x, this.abs.y);
|
|
6049
6422
|
}
|
|
6050
6423
|
|
|
@@ -8496,8 +8869,7 @@ export class TextInputNode extends Node {
|
|
|
8496
8869
|
// colour does that on both a light and a dark palette. `#b3d4fc`
|
|
8497
8870
|
// under the dark palette's near-white ink is 1.3:1, which is nothing.
|
|
8498
8871
|
// Tinting the surface instead leaves the ink's own contrast intact.
|
|
8499
|
-
ctx.fillStyle =
|
|
8500
|
-
this.props.selectionColor ?? tint(this.theme.accent, 0.35);
|
|
8872
|
+
ctx.fillStyle = this.props.selectionColor ?? this.theme.selection;
|
|
8501
8873
|
// One band per direction run, not one rectangle between the two caret
|
|
8502
8874
|
// positions: a range is contiguous in logical order and a line is laid
|
|
8503
8875
|
// out in visual order, so a selection that crosses into an Arabic word
|
|
@@ -8520,7 +8892,7 @@ export class TextInputNode extends Node {
|
|
|
8520
8892
|
this._paintPreedit(ctx, valueX, textY, style);
|
|
8521
8893
|
|
|
8522
8894
|
if (this._focused && this._caretOn && a === b) {
|
|
8523
|
-
ctx.fillStyle = this.props.caretColor ?? style.color;
|
|
8895
|
+
ctx.fillStyle = this.props.caretColor ?? this.theme.caret ?? style.color;
|
|
8524
8896
|
ctx.fillRect(
|
|
8525
8897
|
valueX + caretX,
|
|
8526
8898
|
markY,
|
|
@@ -8887,8 +9259,7 @@ export class TextAreaNode extends TextInputNode {
|
|
|
8887
9259
|
// colour does that on both a light and a dark palette. `#b3d4fc`
|
|
8888
9260
|
// under the dark palette's near-white ink is 1.3:1, which is nothing.
|
|
8889
9261
|
// Tinting the surface instead leaves the ink's own contrast intact.
|
|
8890
|
-
ctx.fillStyle =
|
|
8891
|
-
this.props.selectionColor ?? tint(this.theme.accent, 0.35);
|
|
9262
|
+
ctx.fillStyle = this.props.selectionColor ?? this.theme.selection;
|
|
8892
9263
|
// The bands `textRangeRects` reports — one per line and one per
|
|
8893
9264
|
// direction run inside a line, which is what a selection crossing into
|
|
8894
9265
|
// an Arabic word actually covers. Two caret positions and a rectangle
|
|
@@ -8997,6 +9368,12 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
8997
9368
|
// nodes with `@supports` blocks, re-resolved when the server's answer
|
|
8998
9369
|
// changes rather than on every layout
|
|
8999
9370
|
this._supportsQueryNodes = new Set();
|
|
9371
|
+
// nodes with `@container` blocks, re-resolved after every layout pass
|
|
9372
|
+
// against the containers they ask about — see _resolveContainerQueries.
|
|
9373
|
+
// `_cqFresh` is true while a pass this window just ran is being settled,
|
|
9374
|
+
// when every attached node has a computed size to offer
|
|
9375
|
+
this._containerQueryNodes = new Set();
|
|
9376
|
+
this._cqFresh = false;
|
|
9000
9377
|
// nodes whose child list changed and whose own size is pinned: their new
|
|
9001
9378
|
// arrangement is only measurable once layout has run (see
|
|
9002
9379
|
// Node._childListChanged)
|
|
@@ -9493,6 +9870,13 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9493
9870
|
|
|
9494
9871
|
let size = measure();
|
|
9495
9872
|
if (this._resolveSizeQueries(size.width, size.height)) size = measure();
|
|
9873
|
+
// …and the container blocks against the arrangement that produced it,
|
|
9874
|
+
// so the window is created at the size its content will actually take
|
|
9875
|
+
if (this._containerQueryNodes.size !== 0) {
|
|
9876
|
+
this._settleContainerQueries(() => {
|
|
9877
|
+
size = measure();
|
|
9878
|
+
});
|
|
9879
|
+
}
|
|
9496
9880
|
|
|
9497
9881
|
// A cap the content decides is its natural size, never below a floor
|
|
9498
9882
|
// that was named as a number: `WM_NORMAL_HINTS` with a min above its own
|
|
@@ -10093,6 +10477,16 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10093
10477
|
_initDnd() {
|
|
10094
10478
|
const wnd = this.window;
|
|
10095
10479
|
const X = this.app?.X;
|
|
10480
|
+
// A backend with drop machinery of its own (the cocoa backend's
|
|
10481
|
+
// NSDraggingDestination, src/cocoa/dnd.js): the same DropSession, driven
|
|
10482
|
+
// through its local entry points by the window's transport instead of
|
|
10483
|
+
// by XDND ClientMessages. No property to write, nothing to intern.
|
|
10484
|
+
if (typeof wnd?.attachDropTransport === 'function') {
|
|
10485
|
+
this._dnd = new DropSession(this);
|
|
10486
|
+
registerTopLevel(this);
|
|
10487
|
+
wnd.attachDropTransport(this._dnd, this);
|
|
10488
|
+
return;
|
|
10489
|
+
}
|
|
10096
10490
|
if (
|
|
10097
10491
|
!X ||
|
|
10098
10492
|
typeof X.InternAtom !== 'function' ||
|
|
@@ -10137,11 +10531,43 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10137
10531
|
* their top-level's count, since that is where the messages arrive. */
|
|
10138
10532
|
_registerDropTarget(node) {
|
|
10139
10533
|
(this._dropTargets ??= new Set()).add(node);
|
|
10534
|
+
this._dndTopLevel()?.window?.dropTargetsChanged?.();
|
|
10140
10535
|
}
|
|
10141
10536
|
|
|
10142
10537
|
_forgetDropTarget(node) {
|
|
10143
10538
|
this._dropTargets?.delete(node);
|
|
10144
10539
|
this._dndOwner()?.forget(node);
|
|
10540
|
+
this._dndTopLevel()?.window?.dropTargetsChanged?.();
|
|
10541
|
+
}
|
|
10542
|
+
|
|
10543
|
+
/** The top-level whose drop session — and transport — this window's
|
|
10544
|
+
* targets roll up into. */
|
|
10545
|
+
_dndTopLevel() {
|
|
10546
|
+
let node = this;
|
|
10547
|
+
while (node && !node._dnd) node = node.parent?.root;
|
|
10548
|
+
return node ?? null;
|
|
10549
|
+
}
|
|
10550
|
+
|
|
10551
|
+
/** The concrete type names every `dropAccept` under this top-level asks
|
|
10552
|
+
* for — what a backend that registers its accepted types up front (the
|
|
10553
|
+
* cocoa backend) adds to its base set. Groups and predicates name none. */
|
|
10554
|
+
_dndConcreteTypes() {
|
|
10555
|
+
const out = new Set();
|
|
10556
|
+
const walk = (wn) => {
|
|
10557
|
+
for (const node of wn._dropTargets ?? []) {
|
|
10558
|
+
const accept = node.props.dropAccept;
|
|
10559
|
+
for (const entry of Array.isArray(accept) ? accept : []) {
|
|
10560
|
+
if (typeof entry === 'string' && !(entry in TYPE_GROUPS)) {
|
|
10561
|
+
out.add(entry);
|
|
10562
|
+
}
|
|
10563
|
+
}
|
|
10564
|
+
}
|
|
10565
|
+
for (const child of wn.children) {
|
|
10566
|
+
if (child.isWindow && !child.isPopup) walk(child);
|
|
10567
|
+
}
|
|
10568
|
+
};
|
|
10569
|
+
walk(this);
|
|
10570
|
+
return [...out];
|
|
10145
10571
|
}
|
|
10146
10572
|
|
|
10147
10573
|
_dndTargetCount() {
|
|
@@ -11001,6 +11427,105 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11001
11427
|
return true;
|
|
11002
11428
|
}
|
|
11003
11429
|
|
|
11430
|
+
/** One layout pass at the window's size, with the content floors it
|
|
11431
|
+
* needs: fresh ones when something changed them, the ones in hand during
|
|
11432
|
+
* a live resize, none when nothing moved them. */
|
|
11433
|
+
_layoutStep(width, height) {
|
|
11434
|
+
if (!this._floorsDirty && this._floorsWidth === width) {
|
|
11435
|
+
this._layoutRoot(width, height);
|
|
11436
|
+
} else if (this._deferContentFloors(width)) {
|
|
11437
|
+
this._scheduleFloorsCatchUp();
|
|
11438
|
+
this._layoutRoot(width, height);
|
|
11439
|
+
} else {
|
|
11440
|
+
this._applyContentFloors(width, height);
|
|
11441
|
+
}
|
|
11442
|
+
}
|
|
11443
|
+
|
|
11444
|
+
/**
|
|
11445
|
+
* Resolve the `@container` blocks against the layout just produced, and
|
|
11446
|
+
* lay out again while an answer moves — a container's size is what a pass
|
|
11447
|
+
* *produces*, so it can only be asked about afterwards, and a block that
|
|
11448
|
+
* changed may carry layout properties. `relayout` is whatever pass the
|
|
11449
|
+
* caller runs: `_layoutStep` in a flush, `measure()` while an auto-sized
|
|
11450
|
+
* window is working out how big to be.
|
|
11451
|
+
*
|
|
11452
|
+
* Bounded. Two passes settle the common case (a block that matches at the
|
|
11453
|
+
* width the content took), and nested containers can honestly need a
|
|
11454
|
+
* third — an outer answer moving an inner container past one of its own
|
|
11455
|
+
* thresholds — so the cap is a small fixed number rather than "once".
|
|
11456
|
+
* What it must not do is chase a design no size satisfies; that is
|
|
11457
|
+
* detected per node inside `_resolveContainerQueries`, and pinned.
|
|
11458
|
+
*/
|
|
11459
|
+
_settleContainerQueries(relayout) {
|
|
11460
|
+
if (this._containerQueryNodes.size === 0) return;
|
|
11461
|
+
const held = new Map();
|
|
11462
|
+
this._cqFresh = true;
|
|
11463
|
+
try {
|
|
11464
|
+
for (let pass = 0; pass < CONTAINER_QUERY_PASSES; pass++) {
|
|
11465
|
+
if (!this._resolveContainerQueries(held)) return;
|
|
11466
|
+
relayout();
|
|
11467
|
+
}
|
|
11468
|
+
if (DEV && this._resolveContainerQueries(held, false)) {
|
|
11469
|
+
console.warn(
|
|
11470
|
+
'react-x11: "@container" blocks did not settle in ' +
|
|
11471
|
+
`${CONTAINER_QUERY_PASSES} layout passes; the last one stands. ` +
|
|
11472
|
+
'More than two nested containers, each changing the next, is ' +
|
|
11473
|
+
'the shape that gets here (docs/styling.md#container-queries).',
|
|
11474
|
+
);
|
|
11475
|
+
}
|
|
11476
|
+
} finally {
|
|
11477
|
+
this._cqFresh = false;
|
|
11478
|
+
}
|
|
11479
|
+
}
|
|
11480
|
+
|
|
11481
|
+
/**
|
|
11482
|
+
* One round of the above: every dependent whose blocks answer differently
|
|
11483
|
+
* against the containers as they are now is re-resolved, and the caller
|
|
11484
|
+
* hears whether any was. With `apply` false it only answers.
|
|
11485
|
+
*
|
|
11486
|
+
* `held` is what each node has answered so far this frame. A node that
|
|
11487
|
+
* comes back to an answer it already held is a design that oscillates —
|
|
11488
|
+
* the block moves the size it asks about, which CSS forbids by
|
|
11489
|
+
* construction (size containment) and yoga cannot — so it is **pinned**:
|
|
11490
|
+
* the answer it has stands, and stays until the container's size moves
|
|
11491
|
+
* for some other reason. Without the pin the next frame would find the
|
|
11492
|
+
* other answer, apply it, get the other size, and strobe on every layout.
|
|
11493
|
+
*/
|
|
11494
|
+
_resolveContainerQueries(held, apply = true) {
|
|
11495
|
+
let changed = false;
|
|
11496
|
+
for (const node of [...this._containerQueryNodes]) {
|
|
11497
|
+
if (node.destroyed) {
|
|
11498
|
+
this._containerQueryNodes.delete(node);
|
|
11499
|
+
continue;
|
|
11500
|
+
}
|
|
11501
|
+
const cq = node._cq;
|
|
11502
|
+
const containers = node._containerSizes();
|
|
11503
|
+
const key = containersKey(containers);
|
|
11504
|
+
if (cq.pin) {
|
|
11505
|
+
if (cq.pin.key === key) continue;
|
|
11506
|
+
cq.pin = null;
|
|
11507
|
+
}
|
|
11508
|
+
const answers = containerAnswers(node._baseStyle, containers);
|
|
11509
|
+
if (answers === cq.answers) continue;
|
|
11510
|
+
if (!apply) return true;
|
|
11511
|
+
let seen = held.get(node);
|
|
11512
|
+
if (seen?.includes(answers)) {
|
|
11513
|
+
cq.pin = { key, containers: cq.containers };
|
|
11514
|
+
if (DEV) node._warnContainerOscillation();
|
|
11515
|
+
continue;
|
|
11516
|
+
}
|
|
11517
|
+
if (!seen) held.set(node, (seen = [cq.answers]));
|
|
11518
|
+
seen.push(answers);
|
|
11519
|
+
node._sizeQueriesChanged();
|
|
11520
|
+
changed = true;
|
|
11521
|
+
}
|
|
11522
|
+
// a block may carry layout properties, so the floors measured from the
|
|
11523
|
+
// styles it is replacing are not the answer any more — the same debt a
|
|
11524
|
+
// window query leaves (`_resolveSizeQueries`)
|
|
11525
|
+
if (changed) this._floorsDirty = true;
|
|
11526
|
+
return changed;
|
|
11527
|
+
}
|
|
11528
|
+
|
|
11004
11529
|
/**
|
|
11005
11530
|
* A node in this window has a loop declared on it. Registration is what
|
|
11006
11531
|
* makes the window watch its own visibility — and only then: a
|
|
@@ -11033,6 +11558,13 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11033
11558
|
watchDesktopSettings(this.app, () => this._refreshLoops()),
|
|
11034
11559
|
];
|
|
11035
11560
|
this._loopVisibilityChanged();
|
|
11561
|
+
// The window has a presenter now, which it did not when a loop declared
|
|
11562
|
+
// at mount started on the clock (`_setRoot` runs before `realize`):
|
|
11563
|
+
// every loop is asked again here, so one a presenter can take moves
|
|
11564
|
+
// over on the window's first frame rather than at the next swap that
|
|
11565
|
+
// happens to re-resolve its style. Where nothing can take it, the
|
|
11566
|
+
// second look at an unchanged declaration is a no-op.
|
|
11567
|
+
this._refreshLoops();
|
|
11036
11568
|
}
|
|
11037
11569
|
|
|
11038
11570
|
_unwatchLoops() {
|
|
@@ -11206,8 +11738,19 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11206
11738
|
// A retained presenter keeps a per-node diff instead of damage rects,
|
|
11207
11739
|
// and this is the one channel every change already announces itself on
|
|
11208
11740
|
// (docs/macos.md §"One renderer, two presenters"). Feature-detected: an
|
|
11209
|
-
// ntk window has no ear here and the X11 path is byte-identical.
|
|
11210
|
-
|
|
11741
|
+
// ntk window has no ear here and the X11 path is byte-identical. A
|
|
11742
|
+
// presenter that answers `true` has taken the claim onto a layer of its
|
|
11743
|
+
// own (src/cocoa/promotion.js): the bitmap owes nothing for it, and the
|
|
11744
|
+
// frame that is still owed — for the presenter's half, `prepareFrame` —
|
|
11745
|
+
// paints nothing unless something else claims.
|
|
11746
|
+
const taken =
|
|
11747
|
+
this.window?.noteInvalidate?.(damage, layoutChanged, reason) === true;
|
|
11748
|
+
if (taken && !layoutChanged) {
|
|
11749
|
+
this._damage ??= [];
|
|
11750
|
+
this.needsPaint = true;
|
|
11751
|
+
this._scheduleFrame();
|
|
11752
|
+
return;
|
|
11753
|
+
}
|
|
11211
11754
|
if (layoutChanged) {
|
|
11212
11755
|
this.needsLayout = true;
|
|
11213
11756
|
// The content floors are measured from the tree, so anything that
|
|
@@ -11313,6 +11856,11 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11313
11856
|
this._damage = addDamageRect(this._damage, bounds, this._damageRectCap());
|
|
11314
11857
|
}
|
|
11315
11858
|
this.needsPaint = true;
|
|
11859
|
+
this._scheduleFrame();
|
|
11860
|
+
}
|
|
11861
|
+
|
|
11862
|
+
/** A frame, on the window's clock. */
|
|
11863
|
+
_scheduleFrame() {
|
|
11316
11864
|
// Recorded before the `_scheduled` gate, not inside it: the debt is
|
|
11317
11865
|
// "this window has damage", which a discrete event may pay off early
|
|
11318
11866
|
// (see frames.js). Tying it to whether a callback is outstanding would
|
|
@@ -11375,13 +11923,12 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11375
11923
|
// ledger's rect moves with the shift (issue #398).
|
|
11376
11924
|
this._laidOut = true;
|
|
11377
11925
|
this._resolveSizeQueries(width, height);
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
this._applyContentFloors(width, height);
|
|
11926
|
+
this._layoutStep(width, height);
|
|
11927
|
+
// `@container` blocks are answered by the pass, not before it, and a
|
|
11928
|
+
// changed answer is one more pass — before `absolutize`, so the layout
|
|
11929
|
+
// diff below sees one arrangement against the last frame's
|
|
11930
|
+
if (this._containerQueryNodes.size !== 0) {
|
|
11931
|
+
this._settleContainerQueries(() => this._layoutStep(width, height));
|
|
11385
11932
|
}
|
|
11386
11933
|
this.abs = { x: 0, y: 0, width, height };
|
|
11387
11934
|
this._placed = true;
|
|
@@ -11441,6 +11988,13 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11441
11988
|
for (const node of this._reflowed) node._reflowBefore = null;
|
|
11442
11989
|
this._reflowed.clear();
|
|
11443
11990
|
}
|
|
11991
|
+
// A presenter compositing part of the tree on layers of its own — the
|
|
11992
|
+
// surface presenter's promoted nodes (src/cocoa/promotion.js) — gets
|
|
11993
|
+
// its word in here: after layout, so it sees where everything landed,
|
|
11994
|
+
// and before the damage is taken, so a node it moves onto or off a
|
|
11995
|
+
// layer claims the bitmap under it in this very frame. Feature-detected
|
|
11996
|
+
// like `presentFrame`; an ntk window has no such half.
|
|
11997
|
+
this.window.prepareFrame?.(this, layoutRan);
|
|
11444
11998
|
// any node this pass laid out may be what an open popup is anchored to
|
|
11445
11999
|
if (layoutRan) this._notifyAnchorChange();
|
|
11446
12000
|
// after layout (the claims above included), before the damage is taken:
|
|
@@ -11986,6 +12540,11 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11986
12540
|
if (!check(child)) return false;
|
|
11987
12541
|
continue;
|
|
11988
12542
|
}
|
|
12543
|
+
// A node on a layer of its own (src/cocoa/promotion.js) has no
|
|
12544
|
+
// pixels in the bitmap the band is cut from, so nothing of it can
|
|
12545
|
+
// be dragged along — a pulsing toast over a list is what promotion
|
|
12546
|
+
// is for, and this is the half of it that keeps the pan a blit.
|
|
12547
|
+
if (child._promoted) continue;
|
|
11989
12548
|
if (rectsOverlap(child._subtreeBounds(), vp)) return false;
|
|
11990
12549
|
}
|
|
11991
12550
|
return true;
|
|
@@ -12188,6 +12747,14 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
12188
12747
|
this._lastReasons = EMPTY_REASONS;
|
|
12189
12748
|
}
|
|
12190
12749
|
if (damage === FULL_DAMAGE || !damage) return null;
|
|
12750
|
+
// an empty list: every claim this frame made was answered on a layer of
|
|
12751
|
+
// its own (`invalidate`, the presenter's `true`), and the bitmap paints
|
|
12752
|
+
// nothing — which is not the same as nothing having been claimed
|
|
12753
|
+
if (damage.length === 0) {
|
|
12754
|
+
this._lastDamageRects = [];
|
|
12755
|
+
this._lastDamage = { x: 0, y: 0, width: 0, height: 0 };
|
|
12756
|
+
return [];
|
|
12757
|
+
}
|
|
12191
12758
|
const rects = [];
|
|
12192
12759
|
for (const claimed of damage) {
|
|
12193
12760
|
const clamped = this._clampDamage(claimed, width, height);
|