react-x11 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -129
- package/package.json +12 -4
- package/src/Reconciler.js +19 -31
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/appcontext.js +59 -30
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +303 -49
- package/src/cocoa/bezels.js +175 -30
- package/src/cocoa/dnd.js +27 -13
- package/src/cocoa/fonts.js +3 -3
- package/src/cocoa/glarea.js +24 -5
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/overlay.js +159 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +17 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/threaded.js +246 -0
- package/src/cocoa/window.js +256 -42
- package/src/components/Select.js +2 -2
- package/src/components/anchor.js +3 -3
- package/src/components/native.js +12 -7
- package/src/components/theme.js +2 -2
- package/src/debug.js +1 -1
- package/src/decorations.js +1 -1
- package/src/editmenu.js +2 -2
- package/src/embedding.js +31 -0
- package/src/errors.js +46 -0
- package/src/events.js +78 -18
- package/src/foreignnodes.js +59 -5
- package/src/frames.js +2 -2
- package/src/glnodes.js +172 -41
- package/src/gloverlay.js +383 -0
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -1
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +34 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +16 -3
- package/src/node.js +19 -21
- package/src/nodes/animation.js +644 -0
- package/src/nodes/box.js +21 -0
- package/src/nodes/boxpaint.js +473 -0
- package/src/nodes/canvas.js +269 -0
- package/src/nodes/cascade.js +600 -0
- package/src/nodes/damage.js +183 -0
- package/src/nodes/edithistory.js +124 -0
- package/src/nodes/editmenupopup.js +260 -0
- package/src/nodes/hittest.js +185 -0
- package/src/nodes/image.js +266 -0
- package/src/nodes/install.js +75 -0
- package/src/nodes/invalidate.js +465 -0
- package/src/nodes/kinds.js +31 -0
- package/src/nodes/layout.js +439 -0
- package/src/nodes/layouthost.js +949 -0
- package/src/nodes/node.js +868 -0
- package/src/nodes/paint.js +466 -0
- package/src/nodes/position.js +366 -0
- package/src/nodes/preedit.js +127 -0
- package/src/nodes/queries.js +330 -0
- package/src/nodes/rects.js +102 -0
- package/src/nodes/scrollable.js +891 -0
- package/src/nodes/scrollbars.js +138 -0
- package/src/nodes/scrollblit.js +1034 -0
- package/src/nodes/selectable.js +142 -0
- package/src/nodes/styling.js +225 -0
- package/src/nodes/text.js +649 -0
- package/src/nodes/textarea.js +391 -0
- package/src/nodes/textinput.js +1146 -0
- package/src/nodes/util.js +17 -0
- package/src/nodes/window/anchoring.js +161 -0
- package/src/nodes/window/capabilities.js +190 -0
- package/src/nodes/window/debugpaint.js +83 -0
- package/src/nodes/window/droptarget.js +145 -0
- package/src/nodes/window/floors.js +577 -0
- package/src/nodes/window/flush.js +369 -0
- package/src/nodes/window/hints.js +482 -0
- package/src/nodes/window/listeners.js +222 -0
- package/src/nodes/window/popup.js +71 -0
- package/src/nodes/window/size.js +591 -0
- package/src/nodes/window/window.js +954 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/styles.js +137 -15
- package/src/svgnodes.js +2 -1
- package/src/testing/harness.js +2 -2
- package/src/textselection.js +5 -3
- package/src/trace-registry.js +1 -1
- package/src/types/components.d.ts +38 -6
- package/src/types/elements.d.ts +26 -14
- package/src/types/nodes.d.ts +17 -2
- package/src/types/style.d.ts +94 -3
- package/src/windowstate.js +1 -1
- package/src/yoga.js +1 -1
- package/src/nodes.js +0 -13120
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Small shared pieces with no more specific home under src/nodes/.
|
|
2
|
+
|
|
3
|
+
export const DEV = process.env.NODE_ENV !== 'production';
|
|
4
|
+
|
|
5
|
+
/** Equality for props that may be a scalar, an array or a plain object
|
|
6
|
+
* (window hints are all three shapes), so an unchanged inline object
|
|
7
|
+
* literal does not re-send the property every render. */
|
|
8
|
+
export function shallowEqual(a, b) {
|
|
9
|
+
if (a === b) return true;
|
|
10
|
+
if (typeof a !== 'object' || typeof b !== 'object' || !a || !b) return false;
|
|
11
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
12
|
+
const ka = Object.keys(a);
|
|
13
|
+
const kb = Object.keys(b);
|
|
14
|
+
return ka.length === kb.length && ka.every((k) => a[k] === b[k]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const NO_CHILDREN = Object.freeze([]);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// A window anchored to a rect in another (#255, #280): where it goes, and
|
|
2
|
+
// following the anchor as it moves.
|
|
3
|
+
|
|
4
|
+
import { anchorOffscreen, anchorRect } from '../../anchor.js';
|
|
5
|
+
|
|
6
|
+
/** Anchoring, installed onto `WindowNode.prototype` by window.js. */
|
|
7
|
+
export class WindowAnchoring {
|
|
8
|
+
// --- anchoring ----------------------------------------------------------
|
|
9
|
+
//
|
|
10
|
+
// A `<popup anchor={{to: ref, …}}>` works out its own position, because it
|
|
11
|
+
// is the only thing that can: with `width="auto"` the size is settled
|
|
12
|
+
// inside `realize()`, between `_measure()` and `CreateWindow`, which is
|
|
13
|
+
// after the last moment React could have computed a rect for it — and the
|
|
14
|
+
// placement *needs* the size, since which side it flips to and how far it
|
|
15
|
+
// is pulled back from an edge are both functions of how big it is.
|
|
16
|
+
//
|
|
17
|
+
// So the same order the natural size already established: measure, place,
|
|
18
|
+
// create. The popup is born the right size **and** in the right place,
|
|
19
|
+
// rather than mapped somewhere provisional and corrected a frame later.
|
|
20
|
+
// A widget that knows its own size has no such problem and stays on
|
|
21
|
+
// `useAnchor` / `useAnchorTracking`; both call the same functions
|
|
22
|
+
// (src/anchor.js), so the two agree by construction.
|
|
23
|
+
|
|
24
|
+
/** A `to`/`alignTo` in an `anchor` prop: a ref, or the node itself. */
|
|
25
|
+
_anchorTarget(target) {
|
|
26
|
+
if (target == null || typeof target !== 'object') return null;
|
|
27
|
+
const node = target.abs ? target : target.current;
|
|
28
|
+
return node?.abs ? node : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Where this window goes at `size`, or null when there is nothing to
|
|
32
|
+
* anchor to yet — a ref whose node has not been laid out. */
|
|
33
|
+
_anchorPlacement(size) {
|
|
34
|
+
const anchor = this.props.anchor;
|
|
35
|
+
const node = this._anchorTarget(anchor?.to);
|
|
36
|
+
if (!node) return null;
|
|
37
|
+
// `anchorRect` is public API and speaks logical pixels on both sides;
|
|
38
|
+
// this caller's `size` came from `_measure` (device) and its result is
|
|
39
|
+
// headed for CreateWindow (device), so both convert here.
|
|
40
|
+
const s = this.scale;
|
|
41
|
+
const rect = anchorRect(node, {
|
|
42
|
+
...anchor,
|
|
43
|
+
alignTo: this._anchorTarget(anchor.alignTo) ?? undefined,
|
|
44
|
+
width: size.width / s,
|
|
45
|
+
height: size.height / s,
|
|
46
|
+
});
|
|
47
|
+
if (!rect || s === 1) return rect;
|
|
48
|
+
return {
|
|
49
|
+
...rect,
|
|
50
|
+
x: Math.round(rect.x * s),
|
|
51
|
+
y: Math.round(rect.y * s),
|
|
52
|
+
width: Math.round(rect.width * s),
|
|
53
|
+
height: Math.round(rect.height * s),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Keep an anchored window over the thing it points at: the trigger's own
|
|
59
|
+
* layout moving, an ancestor of it scrolling, the owner window being
|
|
60
|
+
* dragged, or this window's content changing size under it.
|
|
61
|
+
*
|
|
62
|
+
* The first three arrive through the *owner* window's `onAnchorChange`
|
|
63
|
+
* (`_watchAnchor`) — the same signal `useAnchorTracking` reads, since it
|
|
64
|
+
* is the same set of things that can move a trigger. The fourth is
|
|
65
|
+
* `_refit`, which knows the new size before the server does.
|
|
66
|
+
*
|
|
67
|
+
* **Out of view is not a position.** A popup is a real X window, not a
|
|
68
|
+
* web element its ancestors clip, so a caret that has scrolled out of the
|
|
69
|
+
* editor leaves a completion list floating over a document it no longer
|
|
70
|
+
* points into. There is no placement that fixes that, so the window is
|
|
71
|
+
* unmapped for as long as the anchor is gone and mapped again where it
|
|
72
|
+
* belongs when it comes back — the one answer the renderer can give on
|
|
73
|
+
* its own, since whether the popup should *close* is React state and
|
|
74
|
+
* therefore the application's. (An app that would rather close it keeps
|
|
75
|
+
* `useAnchorTracking`'s `onOutOfView`, which is exactly that seam.)
|
|
76
|
+
*/
|
|
77
|
+
_followAnchor(size = this._requestedSize) {
|
|
78
|
+
if (this.destroyed || !this.window || !this.props.anchor) return;
|
|
79
|
+
const node = this._anchorTarget(this.props.anchor.to);
|
|
80
|
+
// A ref that has not attached yet counts as gone, and for the same
|
|
81
|
+
// reason: there is nowhere for the popup to be. Refs attach in the
|
|
82
|
+
// commit phase a popup realizes in, so one written *above* its own
|
|
83
|
+
// trigger in the JSX gets a frame of this — and waiting it out is
|
|
84
|
+
// better than a frame in the corner of the screen.
|
|
85
|
+
const lost = !node || anchorOffscreen(node, this.props.anchor.at);
|
|
86
|
+
if (lost !== Boolean(this._anchorLost)) {
|
|
87
|
+
this._anchorLost = lost;
|
|
88
|
+
// The grab goes with it and comes back with it. X releases a pointer
|
|
89
|
+
// grab whose window stops being viewable, so a menu that hid and
|
|
90
|
+
// reappeared would be one no press outside could dismiss — the
|
|
91
|
+
// `onDismiss` that reads as "click anywhere to close" would simply
|
|
92
|
+
// stop happening. The re-take is `PopupNode._mapNow`'s, which is what
|
|
93
|
+
// keeps it beside the map on every route back to the screen.
|
|
94
|
+
if (lost) {
|
|
95
|
+
if (this.props.grab) this.window.ungrabPointer?.();
|
|
96
|
+
this.window.unmap?.();
|
|
97
|
+
} else {
|
|
98
|
+
this._mapNow();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (lost || !size) return;
|
|
102
|
+
const rect = this._anchorPlacement(size);
|
|
103
|
+
if (!rect) return;
|
|
104
|
+
if (this._placedAt?.x === rect.x && this._placedAt?.y === rect.y) return;
|
|
105
|
+
this._placedAt = { x: rect.x, y: rect.y };
|
|
106
|
+
if (typeof this.window.setState === 'function') {
|
|
107
|
+
this.window.setState({ x: rect.x, y: rect.y });
|
|
108
|
+
} else {
|
|
109
|
+
this.window.move?.(rect.x, rect.y);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Subscribe to whatever can move the anchor. On the **owner's** window,
|
|
115
|
+
* not on this one: what moves is the trigger, and this window's own
|
|
116
|
+
* layout passes say nothing about where it sits on screen.
|
|
117
|
+
*/
|
|
118
|
+
_watchAnchor() {
|
|
119
|
+
this._unwatchAnchor();
|
|
120
|
+
if (!this.props.anchor) return;
|
|
121
|
+
// The anchor's own window where the ref has attached, and the window
|
|
122
|
+
// this popup was *written into* otherwise — which is the same one in
|
|
123
|
+
// every case that matters, and is what makes an unattached ref a
|
|
124
|
+
// one-frame wait rather than a popup nothing ever notifies again. The
|
|
125
|
+
// notification is only ever a prompt to re-measure, so subscribing to a
|
|
126
|
+
// window the anchor turns out not to be in costs a no-op.
|
|
127
|
+
const root =
|
|
128
|
+
this._anchorTarget(this.props.anchor.to)?.root ?? this.parent?.root;
|
|
129
|
+
if (!root?.onAnchorChange || root === this) return;
|
|
130
|
+
this._anchorWatch = root.onAnchorChange(() => this._followAnchor());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_unwatchAnchor() {
|
|
134
|
+
this._anchorWatch?.();
|
|
135
|
+
this._anchorWatch = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Subscribe to "something this window's popups might be anchored to just
|
|
140
|
+
* moved" — a real layout pass (a trigger's own position changing: text
|
|
141
|
+
* wrapping, a sibling growing, an ancestor viewport scrolling — scroll
|
|
142
|
+
* offset is applied during absolutize, so it is a layout change too) or a
|
|
143
|
+
* fresh `_screenOrigin` (the window manager or a script moving this window).
|
|
144
|
+
* Returns an unsubscribe function.
|
|
145
|
+
*
|
|
146
|
+
* Event-driven off the same signals `flush()` and `_refreshScreenOrigin()`
|
|
147
|
+
* already track internally, rather than a polling loop: costs nothing
|
|
148
|
+
* between real changes, and `useAnchor`'s tracking hook (`anchor.js`) is
|
|
149
|
+
* what turns this into a popup that follows its trigger instead of hanging
|
|
150
|
+
* over stale ground once opened.
|
|
151
|
+
*/
|
|
152
|
+
onAnchorChange(cb) {
|
|
153
|
+
(this._anchorListeners ??= new Set()).add(cb);
|
|
154
|
+
return () => this._anchorListeners?.delete(cb);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_notifyAnchorChange() {
|
|
158
|
+
if (!this._anchorListeners?.size) return;
|
|
159
|
+
for (const cb of this._anchorListeners) cb();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// What a window can paint: compositing and ARGB transparency, the window
|
|
2
|
+
// background, and the capabilities `@supports` blocks read.
|
|
3
|
+
|
|
4
|
+
import { cssColorStraight } from 'ntk';
|
|
5
|
+
import {
|
|
6
|
+
argbVisual,
|
|
7
|
+
compositingActive,
|
|
8
|
+
transparencyDisabled,
|
|
9
|
+
watchCompositing,
|
|
10
|
+
} from '../../compositing.js';
|
|
11
|
+
import { DEV } from '../util.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A CSS colour as an X pixel value.
|
|
15
|
+
*
|
|
16
|
+
* The 24-bit TrueColor layout, which is what `redMask`/`greenMask`/`blueMask`
|
|
17
|
+
* say on every server a client meets today and what ntk's own ARGB visual
|
|
18
|
+
* uses. Alpha is dropped: this is the *window background attribute*, a single
|
|
19
|
+
* opaque pixel the server repeats, not something composited.
|
|
20
|
+
*/
|
|
21
|
+
export function pixelFor(color) {
|
|
22
|
+
const rgba = cssColorStraight(color);
|
|
23
|
+
if (!rgba) return null;
|
|
24
|
+
const [r, g, b] = rgba;
|
|
25
|
+
const byte = (c) => Math.max(0, Math.min(255, Math.round(c * 255)));
|
|
26
|
+
return ((byte(r) << 16) | (byte(g) << 8) | byte(b)) >>> 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Connections already told they have no 32-bit visual (`_argbAttributes`).
|
|
30
|
+
const warnedNoArgb = new WeakSet();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `boxShadow` on a `<window>`/`<popup>`, warned about once.
|
|
34
|
+
*
|
|
35
|
+
* A shadow falls *outside* the box, and outside a toplevel there is nothing
|
|
36
|
+
* of ours to paint on — the pixels belong to the desktop. Doing it properly
|
|
37
|
+
* means the window asking for a translucent margin it does not otherwise
|
|
38
|
+
* want (an ARGB visual, a bigger X window, and hit testing that knows the
|
|
39
|
+
* difference), which is a feature of its own rather than a line in the
|
|
40
|
+
* painter. Said out loud because the alternative is a style that reads as
|
|
41
|
+
* ignored for no reason.
|
|
42
|
+
*/
|
|
43
|
+
let warnedWindowShadow = false;
|
|
44
|
+
export function devWarnWindowShadow(kind) {
|
|
45
|
+
if (warnedWindowShadow) return;
|
|
46
|
+
warnedWindowShadow = true;
|
|
47
|
+
console.warn(
|
|
48
|
+
`react-x11: boxShadow on <${kind}> is ignored — a shadow is painted ` +
|
|
49
|
+
'outside the box, and a toplevel window owns no pixels there. Put the ' +
|
|
50
|
+
'shadow on a <box> inside it, or draw the window with a translucent ' +
|
|
51
|
+
'margin of its own (docs/styling.md).',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What the window can paint, installed onto `WindowNode.prototype` by window.js. */
|
|
56
|
+
export class WindowCapabilities {
|
|
57
|
+
/**
|
|
58
|
+
* What this window can actually do, for `@supports` blocks to read and for
|
|
59
|
+
* the paint path to obey. Read-only to callers; recomputed by
|
|
60
|
+
* `_refreshCapabilities`.
|
|
61
|
+
*/
|
|
62
|
+
get capabilities() {
|
|
63
|
+
return this._capabilities;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Will transparency actually be *seen*? Both halves have to hold: the
|
|
68
|
+
* window needs an alpha channel to write, and something has to be
|
|
69
|
+
* compositing it. Miss either and a cleared corner is a black corner, so
|
|
70
|
+
* the paint path fills opaque instead.
|
|
71
|
+
*
|
|
72
|
+
* This is deliberately not the same question as "was `transparent` asked
|
|
73
|
+
* for". A visual is fixed at CreateWindow and cannot follow a compositor
|
|
74
|
+
* that starts or stops mid-session; what it paints can, and does.
|
|
75
|
+
*/
|
|
76
|
+
get transparencyEffective() {
|
|
77
|
+
return this._capabilities.transparency;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Recompute, and if the answer moved, re-resolve every `@supports` block
|
|
82
|
+
* under this window and repaint. Returns whether anything changed.
|
|
83
|
+
*/
|
|
84
|
+
_refreshCapabilities() {
|
|
85
|
+
const transparency = this._transparent && compositingActive(this.app);
|
|
86
|
+
if (transparency === this._capabilities.transparency) return false;
|
|
87
|
+
// a new object rather than a mutation: `resolveQueries` may have handed
|
|
88
|
+
// this map to a memoized style, and identity is how that stays honest
|
|
89
|
+
this._capabilities = { ...this._capabilities, transparency };
|
|
90
|
+
// The window's own style first, and separately: it resolves its style
|
|
91
|
+
// in the Node constructor, before `root` exists to register against, so
|
|
92
|
+
// it is not in its own registry on the pass that matters — the one
|
|
93
|
+
// realize() triggers once the visual is known.
|
|
94
|
+
if (this._supportsQueried) this._sizeQueriesChanged();
|
|
95
|
+
for (const node of [...this._supportsQueryNodes]) {
|
|
96
|
+
if (node.destroyed) this._supportsQueryNodes.delete(node);
|
|
97
|
+
else if (node !== this) node._sizeQueriesChanged();
|
|
98
|
+
}
|
|
99
|
+
// The window's own background is not a styled node and has no query
|
|
100
|
+
// block to re-resolve — it reads `transparencyEffective` directly, so
|
|
101
|
+
// it just needs the repaint.
|
|
102
|
+
this.invalidate(true, null, 'capabilities');
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Follow the compositor for the life of the window. A menu that was opaque
|
|
108
|
+
* because nothing was compositing becomes a rounded translucent one the
|
|
109
|
+
* moment something is, with no remount — which is the whole reason the
|
|
110
|
+
* ARGB visual is taken even when no compositor is running yet.
|
|
111
|
+
*/
|
|
112
|
+
_watchCapabilities() {
|
|
113
|
+
this._refreshCapabilities();
|
|
114
|
+
this._unwatchCompositing ??= watchCompositing(this.app, () => {
|
|
115
|
+
if (!this.destroyed) this._refreshCapabilities();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creation attributes for a per-pixel transparent window: a 32-bit
|
|
121
|
+
* TrueColor visual, and a background of transparent black rather than the
|
|
122
|
+
* server's white, so nothing flashes before the first paint. ntk gives the
|
|
123
|
+
* window its own colormap and border pixel to go with the visual —
|
|
124
|
+
* inheriting either from a parent of a different depth is a BadMatch.
|
|
125
|
+
*
|
|
126
|
+
* Empty when the display has no such visual (XQuartz has none) or ntk is
|
|
127
|
+
* too old to find one, and then `_transparent` stays false and the window
|
|
128
|
+
* paints its background opaque, exactly as it did before. A transparent
|
|
129
|
+
* window that cannot be transparent is a square opaque one, not a broken
|
|
130
|
+
* one — and the alternative, black corners, is worse than square.
|
|
131
|
+
*/
|
|
132
|
+
/** What this window paints as its background — its own, or the palette's. */
|
|
133
|
+
_windowBackground() {
|
|
134
|
+
return this.style.backgroundColor || this.theme.background;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Keep the server's idea of the background in step with ours. Called when
|
|
139
|
+
* the palette moves under an unstyled window and when the style names a new
|
|
140
|
+
* colour; a `transparent` window keeps its 0, which means transparent.
|
|
141
|
+
*/
|
|
142
|
+
_syncWindowBackground() {
|
|
143
|
+
if (this._transparent || !this.window || this.destroyed) return;
|
|
144
|
+
const pixel = pixelFor(this._windowBackground());
|
|
145
|
+
if (pixel === null || pixel === this._backgroundPixel) return;
|
|
146
|
+
if (this.app?.X?._closing) return;
|
|
147
|
+
this._backgroundPixel = pixel;
|
|
148
|
+
// One call for both halves, because a double-buffered window has two
|
|
149
|
+
// backgrounds: the X window attribute the server paints into exposed
|
|
150
|
+
// area, and the colour ntk's backing store clears the part a resize
|
|
151
|
+
// grows into. They used to be free to disagree — the backing store
|
|
152
|
+
// cleared to the screen's white whatever the window said — which is why
|
|
153
|
+
// enlarging a dark window flashed a white strip that survived until
|
|
154
|
+
// something damaged it (ntk#209, 6.6.1).
|
|
155
|
+
this.window.setBackgroundPixel?.(pixel);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_argbAttributes() {
|
|
159
|
+
const argb = argbVisual(this.app);
|
|
160
|
+
if (!argb) {
|
|
161
|
+
// Once per connection. The answer is a property of the display (or of
|
|
162
|
+
// an environment switch) and cannot change while it is open, and since
|
|
163
|
+
// the widgets ask for a transparent popup every time a menu or a
|
|
164
|
+
// tooltip opens, warning per window would turn one piece of news into
|
|
165
|
+
// a running commentary.
|
|
166
|
+
if (DEV && this.app && !warnedNoArgb.has(this.app)) {
|
|
167
|
+
warnedNoArgb.add(this.app);
|
|
168
|
+
const what = this.isPopup ? 'popup' : 'window';
|
|
169
|
+
if (transparencyDisabled()) {
|
|
170
|
+
console.warn(
|
|
171
|
+
'react-x11: REACT_X11_NO_TRANSPARENCY=1 — <%s transparent> ' +
|
|
172
|
+
'ignored, this run is opaque',
|
|
173
|
+
what,
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
console.warn(
|
|
177
|
+
'react-x11: <%s transparent> — no 32-bit TrueColor visual on ' +
|
|
178
|
+
'this display, falling back to an opaque window',
|
|
179
|
+
what,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
// What the paint path keys off: the window really does have an alpha
|
|
186
|
+
// channel, so clearing it means transparent rather than white.
|
|
187
|
+
this._transparent = true;
|
|
188
|
+
return { ...argb, backgroundPixel: 0 };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Debug overlays: REACT_X11_DEBUG_PAINT's damage flashes, and DevTools'
|
|
2
|
+
// highlight and trace-updates outlines.
|
|
3
|
+
|
|
4
|
+
// REACT_X11_DEBUG_PAINT: each frame strokes its damage rects in the next of
|
|
5
|
+
// these, so a region repainting every frame strobes visibly.
|
|
6
|
+
export const FLASH_COLORS = [
|
|
7
|
+
'#e6194b',
|
|
8
|
+
'#3cb44b',
|
|
9
|
+
'#ffe119',
|
|
10
|
+
'#4363d8',
|
|
11
|
+
'#f58231',
|
|
12
|
+
'#911eb4',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// REACT_X11_DEBUG_PAINT, read once: a process.env read is a real
|
|
16
|
+
// environment lookup, and this switch sits on invalidate() and the paint
|
|
17
|
+
// loop — the diagnostics must cost nothing when they are off. Indirected
|
|
18
|
+
// like the animation clock so tests can flip it without a subprocess.
|
|
19
|
+
export let debugPaint = process.env.REACT_X11_DEBUG_PAINT || '';
|
|
20
|
+
export function setDebugPaint(mode) {
|
|
21
|
+
debugPaint = mode || '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Debug overlays, installed onto `WindowNode.prototype` by window.js. */
|
|
25
|
+
export class WindowDebugPaint {
|
|
26
|
+
/** DevTools hover highlight: tint a node's rect on the next paint. */
|
|
27
|
+
setHighlight(node) {
|
|
28
|
+
if (this._highlight === node) return;
|
|
29
|
+
const prev = this._highlight;
|
|
30
|
+
this._highlight = node;
|
|
31
|
+
// The tint leaves one rect and lands on another, and both are already
|
|
32
|
+
// known, so the claim is their union rather than the window. A side
|
|
33
|
+
// with no laid-out rect tints (or tinted) the whole window — the same
|
|
34
|
+
// fallback _paintRegion paints — so only that case stays unbounded.
|
|
35
|
+
const rects = [];
|
|
36
|
+
for (const n of [prev, node]) {
|
|
37
|
+
if (!n) continue;
|
|
38
|
+
if (!n.abs?.width) {
|
|
39
|
+
this.invalidate(false, null, 'highlight');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
rects.push(n.abs);
|
|
43
|
+
}
|
|
44
|
+
for (const rect of rects) this.invalidate(false, rect, 'highlight');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* DevTools' "highlight updates when components render": outline the rects
|
|
49
|
+
* that just re-rendered, in the colour the backend assigned each one (it
|
|
50
|
+
* ramps with the update count and fades them out on its own clock, so
|
|
51
|
+
* this is a dumb overlay — `rects` is the whole state, `null` clears it).
|
|
52
|
+
*/
|
|
53
|
+
setTraceUpdates(rects) {
|
|
54
|
+
const previous = this._traceUpdates;
|
|
55
|
+
const next = rects?.length ? rects : null;
|
|
56
|
+
if (!previous && !next) return;
|
|
57
|
+
this._traceUpdates = next;
|
|
58
|
+
// The stroke sits inside the rect, but a rect whose node has since
|
|
59
|
+
// moved or gone claims where it *was*; both lists are claimed for the
|
|
60
|
+
// same reason setHighlight claims both of its rects.
|
|
61
|
+
for (const r of [...(previous ?? []), ...(next ?? [])]) {
|
|
62
|
+
this.invalidate(false, r, 'trace-updates');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** REACT_X11_DEBUG_LAYOUT=1: outline every drawn node, color by depth. */
|
|
67
|
+
_paintDebugOverlay(ctx, node, depth) {
|
|
68
|
+
const colors = ['#e74c3c', '#27ae60', '#2980b9', '#8e44ad', '#f39c12'];
|
|
69
|
+
for (const child of node.paintOrder()) {
|
|
70
|
+
ctx.strokeStyle = colors[depth % colors.length];
|
|
71
|
+
ctx.lineWidth = 1;
|
|
72
|
+
ctx.beginPath();
|
|
73
|
+
ctx.rect(
|
|
74
|
+
child.abs.x + 0.5,
|
|
75
|
+
child.abs.y + 0.5,
|
|
76
|
+
child.abs.width - 1,
|
|
77
|
+
child.abs.height - 1,
|
|
78
|
+
);
|
|
79
|
+
ctx.stroke();
|
|
80
|
+
this._paintDebugOverlay(ctx, child, depth + 1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The window's half of XDND (docs/architecture/drag-and-drop.md): the drop
|
|
2
|
+
// targets registered under it and the types they accept.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DropSession,
|
|
6
|
+
dndAtoms,
|
|
7
|
+
registerTopLevel,
|
|
8
|
+
XDND_VERSION,
|
|
9
|
+
} from '../../dnd.js';
|
|
10
|
+
import { TYPE_GROUPS } from '../../transfer.js';
|
|
11
|
+
|
|
12
|
+
/** The window's half of XDND, installed onto `WindowNode.prototype` by window.js. */
|
|
13
|
+
export class WindowDropTarget {
|
|
14
|
+
/**
|
|
15
|
+
* XDND drop-target wiring (src/dnd.js): write `XdndAware = 5`, start the
|
|
16
|
+
* atom interning, and route incoming ClientMessages to the session.
|
|
17
|
+
* Unconditional — the property is 4 bytes on a window that exists anyway,
|
|
18
|
+
* and advertising lazily would race sources that cache the window list
|
|
19
|
+
* at drag start. A window with no registered drop targets answers "not
|
|
20
|
+
* accepting" once per entry instead (DropSession).
|
|
21
|
+
*
|
|
22
|
+
* The one exception is a `<popup dragPreview>`. It follows the pointer,
|
|
23
|
+
* so for the whole gesture it is the frontmost window under it, and it
|
|
24
|
+
* must never be what the drag is over. Where react-x11 picks the target
|
|
25
|
+
* itself (src/dnd.js `topLevelAt`) it is skipped by name. Where the OS
|
|
26
|
+
* picks, registering nothing is not enough: AppKit finds the window
|
|
27
|
+
* under the pointer first and does not look past one with no dragged
|
|
28
|
+
* types — the drag then has no destination at all — so the cocoa window
|
|
29
|
+
* is made transparent to the pointer instead (src/cocoa/window.js,
|
|
30
|
+
* `ignoresMouseEvents`, #488). A preview still gets none of this: no
|
|
31
|
+
* session, no registry entry, no property, nothing to refuse with.
|
|
32
|
+
*/
|
|
33
|
+
_initDnd() {
|
|
34
|
+
if (this.props.dragPreview) return;
|
|
35
|
+
const wnd = this.window;
|
|
36
|
+
const X = this.app?.X;
|
|
37
|
+
// A backend with drop machinery of its own (the cocoa backend's
|
|
38
|
+
// NSDraggingDestination, src/cocoa/dnd.js): the same DropSession, driven
|
|
39
|
+
// through its local entry points by the window's transport instead of
|
|
40
|
+
// by XDND ClientMessages. No property to write, nothing to intern.
|
|
41
|
+
if (typeof wnd?.attachDropTransport === 'function') {
|
|
42
|
+
this._dnd = new DropSession(this);
|
|
43
|
+
registerTopLevel(this);
|
|
44
|
+
wnd.attachDropTransport(this._dnd, this);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
!X ||
|
|
49
|
+
typeof X.InternAtom !== 'function' ||
|
|
50
|
+
typeof wnd.on !== 'function' ||
|
|
51
|
+
typeof wnd.setProperty !== 'function'
|
|
52
|
+
) {
|
|
53
|
+
return; // mock app, or an ntk too old to write raw properties
|
|
54
|
+
}
|
|
55
|
+
this._dnd = new DropSession(this);
|
|
56
|
+
registerTopLevel(this);
|
|
57
|
+
void dndAtoms(X).catch(() => {});
|
|
58
|
+
wnd
|
|
59
|
+
.setProperty('XdndAware', [XDND_VERSION], { type: 'ATOM' })
|
|
60
|
+
.catch(() => {});
|
|
61
|
+
wnd.on('message', (ev) => {
|
|
62
|
+
// XDND is a *default action* on a ClientMessage, so it follows the same
|
|
63
|
+
// rule every other one does: it runs after the application's handler
|
|
64
|
+
// and is skipped when that handler called `preventDefault()`. That is
|
|
65
|
+
// the seam for a window answering the drag protocol itself.
|
|
66
|
+
//
|
|
67
|
+
// `_attachWindowListeners` subscribed to this stream first, so normally
|
|
68
|
+
// the flag is already decided by the time this runs. `pending()` is the
|
|
69
|
+
// exception it cannot cover: a message whose type had to be named with
|
|
70
|
+
// a round trip is dispatched a few turns later, and answering the drag
|
|
71
|
+
// before the application has been asked would make `preventDefault()`
|
|
72
|
+
// depend on whether an atom happened to be cached.
|
|
73
|
+
const said = this._clientMessages?.pending();
|
|
74
|
+
const route = () => {
|
|
75
|
+
if (ev.defaultPrevented) return;
|
|
76
|
+
this._dnd.handleMessage(ev);
|
|
77
|
+
// a drag *out* of this window gets its XdndStatus/XdndFinished back
|
|
78
|
+
// on the same channel
|
|
79
|
+
this._dragSession?.handleMessage(ev);
|
|
80
|
+
};
|
|
81
|
+
if (said) said.then(route);
|
|
82
|
+
else route();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Nodes with drop props register with their root; the count gates the
|
|
87
|
+
* whole-window "not accepting" fast path. Child <window>s roll up into
|
|
88
|
+
* their top-level's count, since that is where the messages arrive. */
|
|
89
|
+
_registerDropTarget(node) {
|
|
90
|
+
(this._dropTargets ??= new Set()).add(node);
|
|
91
|
+
this._dndTopLevel()?.window?.dropTargetsChanged?.();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_forgetDropTarget(node) {
|
|
95
|
+
this._dropTargets?.delete(node);
|
|
96
|
+
this._dndOwner()?.forget(node);
|
|
97
|
+
this._dndTopLevel()?.window?.dropTargetsChanged?.();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The top-level whose drop session — and transport — this window's
|
|
101
|
+
* targets roll up into. */
|
|
102
|
+
_dndTopLevel() {
|
|
103
|
+
let node = this;
|
|
104
|
+
while (node && !node._dnd) node = node.parent?.root;
|
|
105
|
+
return node ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The concrete type names every `dropAccept` under this top-level asks
|
|
109
|
+
* for — what a backend that registers its accepted types up front (the
|
|
110
|
+
* cocoa backend) adds to its base set. Groups and predicates name none. */
|
|
111
|
+
_dndConcreteTypes() {
|
|
112
|
+
const out = new Set();
|
|
113
|
+
const walk = (wn) => {
|
|
114
|
+
for (const node of wn._dropTargets ?? []) {
|
|
115
|
+
const accept = node.props.dropAccept;
|
|
116
|
+
for (const entry of Array.isArray(accept) ? accept : []) {
|
|
117
|
+
if (typeof entry === 'string' && !(entry in TYPE_GROUPS)) {
|
|
118
|
+
out.add(entry);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const child of wn.children) {
|
|
123
|
+
if (child.isWindow && !child.isPopup) walk(child);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
walk(this);
|
|
127
|
+
return [...out];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_dndTargetCount() {
|
|
131
|
+
let count = this._dropTargets?.size ?? 0;
|
|
132
|
+
for (const child of this.children) {
|
|
133
|
+
if (child.isWindow && !child.isPopup) count += child._dndTargetCount();
|
|
134
|
+
}
|
|
135
|
+
return count;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The session that owns drags over this window: its own for a
|
|
139
|
+
* top-level, the enclosing top-level's for a nested <window>. */
|
|
140
|
+
_dndOwner() {
|
|
141
|
+
let node = this;
|
|
142
|
+
while (node && !node._dnd) node = node.parent?.root;
|
|
143
|
+
return node?._dnd ?? null;
|
|
144
|
+
}
|
|
145
|
+
}
|