react-x11 2.12.0 → 2.14.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/package.json +12 -8
- package/src/Reconciler.js +38 -35
- package/src/appcontext.js +59 -30
- package/src/cocoa/app.js +11 -0
- package/src/cocoa/glarea.js +4 -2
- package/src/cocoa/overlay.js +159 -0
- package/src/cocoa/promotion.js +13 -0
- package/src/components/Select.js +8 -2
- package/src/embedding.js +31 -0
- package/src/events.js +80 -14
- package/src/foreignnodes.js +56 -3
- package/src/glnodes.js +171 -40
- package/src/gloverlay.js +383 -0
- package/src/host.d.ts +0 -1
- package/src/index.d.ts +19 -1
- package/src/node.d.ts +12 -1
- package/src/nodes/boxpaint.js +9 -0
- package/src/nodes/preedit.js +64 -14
- package/src/nodes/window/flush.js +35 -0
- package/src/nodes/window/window.js +9 -0
- package/src/scale.js +52 -22
- package/src/screencolor.js +104 -17
- package/src/types/elements.d.ts +15 -13
- package/src/wayland/app.js +560 -0
- package/src/wayland/backendwindow.js +1123 -0
- package/src/wayland/clipboard.js +326 -0
- package/src/wayland/connection.js +482 -0
- package/src/wayland/context2d.js +2133 -0
- package/src/wayland/decorations.js +476 -0
- package/src/wayland/dmabuf.js +89 -0
- package/src/wayland/dnd.js +581 -0
- package/src/wayland/fdutil.js +108 -0
- package/src/wayland/framestyle.js +257 -0
- package/src/wayland/glarea.js +371 -0
- package/src/wayland/glcontext.js +415 -0
- package/src/wayland/glyphatlas.js +237 -0
- package/src/wayland/input.js +417 -0
- package/src/wayland/keysymnames.js +35 -0
- package/src/wayland/layershell.js +363 -0
- package/src/wayland/outputs.js +601 -0
- package/src/wayland/protocols/cursor-shape-v1.json +1 -0
- package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
- package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
- package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
- package/src/wayland/protocols/fractional-scale-v1.json +1 -0
- package/src/wayland/protocols/index.json +127 -0
- package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
- package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
- package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
- package/src/wayland/protocols/presentation-time.json +1 -0
- package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
- package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
- package/src/wayland/protocols/tablet-v2.json +1 -0
- package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
- package/src/wayland/protocols/viewporter.json +1 -0
- package/src/wayland/protocols/wayland.json +1 -0
- package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
- package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-activation-v1.json +1 -0
- package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-shell.json +1 -0
- package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
- package/src/wayland/readback.js +99 -0
- package/src/wayland/screencopy.js +584 -0
- package/src/wayland/seat.js +584 -0
- package/src/wayland/shm.js +226 -0
- package/src/wayland/ssd.js +106 -0
- package/src/wayland/surface.js +123 -0
- package/src/wayland/swapchain.js +411 -0
- package/src/wayland/tablet.js +522 -0
- package/src/wayland/target.js +263 -0
- package/src/wayland/text.js +113 -0
- package/src/wayland/textinput.js +671 -0
- package/src/wayland/touch.js +284 -0
- package/src/wayland/window.js +827 -0
- package/src/wayland/xkb.js +425 -0
package/src/events.js
CHANGED
|
@@ -47,6 +47,10 @@ const WHEEL_BUTTONS = new Set([4, 5, 6, 7]);
|
|
|
47
47
|
*/
|
|
48
48
|
export const WHEEL_NOTCH_PX = 48;
|
|
49
49
|
const RIGHT_BUTTON = 3;
|
|
50
|
+
// A LeaveNotify's detail (core protocol): the pointer went to an ancestor of
|
|
51
|
+
// the window it left, or into a window inside it — see `_leftIntoSurface`.
|
|
52
|
+
const NOTIFY_ANCESTOR = 0;
|
|
53
|
+
const NOTIFY_INFERIOR = 2;
|
|
50
54
|
|
|
51
55
|
/** How many leading entries two node paths share. */
|
|
52
56
|
function sharedPrefix(a, b) {
|
|
@@ -637,8 +641,32 @@ export class EventManager {
|
|
|
637
641
|
return node.isWindow ? node.window : node;
|
|
638
642
|
}
|
|
639
643
|
|
|
644
|
+
/**
|
|
645
|
+
* The node a pointer event landed on: a surface first, then the tree,
|
|
646
|
+
* front to back. A `<glarea>` is stacked above every 2D thing in its
|
|
647
|
+
* window, so a point inside one is over it whatever the tree's order says
|
|
648
|
+
* (`GlAreaNode.hitSurface`).
|
|
649
|
+
*/
|
|
640
650
|
_hit(ev) {
|
|
641
|
-
return
|
|
651
|
+
return (
|
|
652
|
+
this._surfaceAt(ev.x, ev.y) ?? this.node.hitTest(ev.x, ev.y) ?? this.node
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* The frontmost of this window's surfaces a point is over, or null. The
|
|
658
|
+
* newest is on top on both backends (`GlAreaNode._create`). An empty list
|
|
659
|
+
* is one length read on the motion path, which is all a window with no
|
|
660
|
+
* `<glarea>` pays for this.
|
|
661
|
+
*/
|
|
662
|
+
_surfaceAt(x, y) {
|
|
663
|
+
const surfaces = this.node._surfaces;
|
|
664
|
+
if (!surfaces?.length) return null;
|
|
665
|
+
for (let i = surfaces.length - 1; i >= 0; i--) {
|
|
666
|
+
const hit = surfaces[i].hitSurface(x, y);
|
|
667
|
+
if (hit) return hit;
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
642
670
|
}
|
|
643
671
|
|
|
644
672
|
_path(target) {
|
|
@@ -742,16 +770,7 @@ export class EventManager {
|
|
|
742
770
|
* way somewhere rather than one, and React must be free to interrupt the
|
|
743
771
|
* render it started for the notch before.
|
|
744
772
|
*/
|
|
745
|
-
|
|
746
|
-
* @param {object} native ntk's wheel event, in this window's coordinates
|
|
747
|
-
* @param {object} [over] the node the wheel happened over, when the caller
|
|
748
|
-
* already knows. A `<glarea>` owns its own X window, so the server
|
|
749
|
-
* delivers the event *there* and the surface hands it back translated
|
|
750
|
-
* (src/glnodes.js) — and a hit test would answer with the box behind it,
|
|
751
|
-
* because a window-owning child is not in its parent's paint order.
|
|
752
|
-
* Knowing beats guessing; everything after this line is the same.
|
|
753
|
-
*/
|
|
754
|
-
_onWheel(native, over = null) {
|
|
773
|
+
_onWheel(native) {
|
|
755
774
|
// The first wheel is what says this window wants smooth scrolling. It was
|
|
756
775
|
// created on core events — an XI2 selection costs four times as many
|
|
757
776
|
// bytes per *pointer move*, which a window that is never scrolled would
|
|
@@ -765,7 +784,7 @@ export class EventManager {
|
|
|
765
784
|
// outside gets, and for the same reason (`_pressOutside`).
|
|
766
785
|
if (this._dismissOutside(native)) return;
|
|
767
786
|
runWithPriority(ContinuousEventPriority, () => {
|
|
768
|
-
const target =
|
|
787
|
+
const target = this._hit(native);
|
|
769
788
|
// Shift turns a vertical wheel sideways — the convention for the mouse
|
|
770
789
|
// and the touchpad that have no horizontal axis. Read off the delta
|
|
771
790
|
// rather than off the source: a plain wheel mouse on an XI2 connection
|
|
@@ -1043,6 +1062,13 @@ export class EventManager {
|
|
|
1043
1062
|
}
|
|
1044
1063
|
|
|
1045
1064
|
_onMouseOut(native) {
|
|
1065
|
+
// The pointer went into one of this window's surfaces, not out of the
|
|
1066
|
+
// window: X reports crossing into a child window as leaving this one,
|
|
1067
|
+
// but the input over a surface still arrives here — it selects none of
|
|
1068
|
+
// its own — and the next motion names it. Answered as a leave, every
|
|
1069
|
+
// ancestor was told the pointer had gone and then that it was back, and
|
|
1070
|
+
// `onMouseOut` that it had left a window it was still in.
|
|
1071
|
+
if (this._leftIntoSurface(native)) return;
|
|
1046
1072
|
runWithPriority(ContinuousEventPriority, () => {
|
|
1047
1073
|
this._updateHover([], native);
|
|
1048
1074
|
// the pointer is somewhere else entirely: whatever it was heading for
|
|
@@ -1061,6 +1087,40 @@ export class EventManager {
|
|
|
1061
1087
|
});
|
|
1062
1088
|
}
|
|
1063
1089
|
|
|
1090
|
+
/**
|
|
1091
|
+
* Whether a LeaveNotify leaves the pointer in one of this window's
|
|
1092
|
+
* surfaces rather than out of the window.
|
|
1093
|
+
*
|
|
1094
|
+
* A real server says "into a window inside this one" with detail
|
|
1095
|
+
* NotifyInferior, and says it twice over a surface: when the pointer moves
|
|
1096
|
+
* onto it, and when a grab ends with the pointer on it (mode NotifyUngrab)
|
|
1097
|
+
* — which every click and every wheel notch over a surface does, since the
|
|
1098
|
+
* press's implicit grab is this window's. node-x11's in-process server
|
|
1099
|
+
* says it with detail NotifyAncestor and the child named instead, and
|
|
1100
|
+
* sends no crossings for grabs at all.
|
|
1101
|
+
*
|
|
1102
|
+
* Either way the point says which inferior, and only a surface's rect
|
|
1103
|
+
* makes it ours: a nested `<window>` or a `<foreign>` takes its own input,
|
|
1104
|
+
* so leaving for one of those is still a leave. So is a grab taken
|
|
1105
|
+
* elsewhere while the pointer is over a surface — detail NotifyVirtual or
|
|
1106
|
+
* NonlinearVirtual, the surface named as the child it left from — since
|
|
1107
|
+
* the pointer is the grab's now. The Cocoa backend's leave carries no
|
|
1108
|
+
* detail, and is always one.
|
|
1109
|
+
*/
|
|
1110
|
+
_leftIntoSurface(native) {
|
|
1111
|
+
if (!native) return false;
|
|
1112
|
+
const into =
|
|
1113
|
+
native.detail === NOTIFY_INFERIOR ||
|
|
1114
|
+
(native.detail === NOTIFY_ANCESTOR && Boolean(native.child));
|
|
1115
|
+
if (!into) return false;
|
|
1116
|
+
const wnd = this.node.window;
|
|
1117
|
+
const { x, y } = native;
|
|
1118
|
+
if (x < 0 || y < 0 || x >= (wnd?.width ?? 0) || y >= (wnd?.height ?? 0)) {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
return this._surfaceAt(x, y) !== null;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1064
1124
|
/**
|
|
1065
1125
|
* The chain of the press that is still live, as far along it as the
|
|
1066
1126
|
* pointer has stayed. `downPath` is where the press landed and does not
|
|
@@ -1285,8 +1345,14 @@ export class EventManager {
|
|
|
1285
1345
|
* then the element's own `defaultComposition` unless one of them called
|
|
1286
1346
|
* `preventDefault()`. Same seam, same order as `defaultKeyDown`.
|
|
1287
1347
|
*/
|
|
1288
|
-
_composition(phase, target, data, native) {
|
|
1289
|
-
|
|
1348
|
+
_composition(phase, target, data, native, extra) {
|
|
1349
|
+
// `extra` is what an input method knows beyond the text — the cursor
|
|
1350
|
+
// inside its preedit (`cursorBegin`/`cursorEnd`, src/wayland/textinput.js);
|
|
1351
|
+
// the client-side composer has none, and the event is the same shape
|
|
1352
|
+
const ev = this.dispatch('Composition' + phase, target, native, {
|
|
1353
|
+
data,
|
|
1354
|
+
...extra,
|
|
1355
|
+
});
|
|
1290
1356
|
if (!ev.defaultPrevented) target.defaultComposition?.(ev);
|
|
1291
1357
|
return ev;
|
|
1292
1358
|
}
|
package/src/foreignnodes.js
CHANGED
|
@@ -33,12 +33,18 @@
|
|
|
33
33
|
import { XEMBED, XEmbedSocket } from 'ntk';
|
|
34
34
|
|
|
35
35
|
import { isFocusable } from './a11y.js';
|
|
36
|
+
import { canEmbed } from './embedding.js';
|
|
36
37
|
import { lastInputTime } from './inputtime.js';
|
|
37
38
|
import { Node } from './nodes/node.js';
|
|
38
39
|
import { pixelFor } from './nodes/window/capabilities.js';
|
|
39
40
|
|
|
40
41
|
const px = (v) => Math.max(1, Math.round(v || 0));
|
|
41
42
|
|
|
43
|
+
// Apps already told, by a <foreign> with no `onError`, that they cannot
|
|
44
|
+
// embed. Once per app: the answer belongs to the backend, so the second pane
|
|
45
|
+
// to ask carries no news — the same rule as a display with no ARGB visual.
|
|
46
|
+
const warnedCannotEmbed = new WeakSet();
|
|
47
|
+
|
|
42
48
|
/**
|
|
43
49
|
* `<foreign>` — another client's top-level window, laid out as an element.
|
|
44
50
|
*
|
|
@@ -89,6 +95,18 @@ const px = (v) => Math.max(1, Math.round(v || 0));
|
|
|
89
95
|
* pointer is *over* the embedded client the client receives keys directly
|
|
90
96
|
* and no handler of ours runs. Chords work everywhere else in the window,
|
|
91
97
|
* which is the trade a proxy would make in reverse.
|
|
98
|
+
*
|
|
99
|
+
* ### Where there is no embedding
|
|
100
|
+
*
|
|
101
|
+
* All of this needs a connection that can reparent somebody else's window,
|
|
102
|
+
* and two apps this node meets have none: the Cocoa backend and the headless
|
|
103
|
+
* mock. Both carry an X stub and a `createWindow` that takes a parent, which
|
|
104
|
+
* used to be enough to build a socket over them — on Cocoa around a child GL
|
|
105
|
+
* surface whose `id` is undefined, handed out through `onReady` and spawned
|
|
106
|
+
* `xterm -into undefined` with; on the mock, a throw from inside the commit.
|
|
107
|
+
* So `realize()` asks `canEmbed` first, the question
|
|
108
|
+
* `useSupports('embedding')` answers too, and a no is final: no container, no
|
|
109
|
+
* `onReady`, one `onError`, and an empty box in the layout.
|
|
92
110
|
*/
|
|
93
111
|
export class ForeignNode extends Node {
|
|
94
112
|
constructor(props, app) {
|
|
@@ -97,8 +115,12 @@ export class ForeignNode extends Node {
|
|
|
97
115
|
this.socket = null;
|
|
98
116
|
/** `{ id, xembed, version }` while a client is in, else null */
|
|
99
117
|
this.client = null;
|
|
100
|
-
/** the error that stopped an embed, if one did
|
|
118
|
+
/** the error that stopped an embed, if one did — or, where this
|
|
119
|
+
* connection cannot embed at all, the refusal */
|
|
101
120
|
this.error = null;
|
|
121
|
+
// set once `realize()` finds this connection cannot embed, and final:
|
|
122
|
+
// the backend does not change under a node
|
|
123
|
+
this._refused = false;
|
|
102
124
|
/** geometry last sent to the X windows */
|
|
103
125
|
this.rect = null;
|
|
104
126
|
// an embedded client is a control the user can Tab to, like a
|
|
@@ -133,9 +155,10 @@ export class ForeignNode extends Node {
|
|
|
133
155
|
* where React can discard the work.
|
|
134
156
|
*/
|
|
135
157
|
realize() {
|
|
136
|
-
if (this.socket || this.destroyed) return;
|
|
158
|
+
if (this.socket || this.destroyed || this._refused) return;
|
|
137
159
|
const parent = this.root?.window;
|
|
138
|
-
if (!parent
|
|
160
|
+
if (!parent) return;
|
|
161
|
+
if (!canEmbed(this.app)) return this._refuse();
|
|
139
162
|
const rect = this._geometry();
|
|
140
163
|
this.rect = rect;
|
|
141
164
|
const socket = new XEmbedSocket(parent, {
|
|
@@ -159,6 +182,36 @@ export class ForeignNode extends Node {
|
|
|
159
182
|
this._syncMapped();
|
|
160
183
|
}
|
|
161
184
|
|
|
185
|
+
/**
|
|
186
|
+
* This connection cannot embed: say so once, and be an empty box.
|
|
187
|
+
*
|
|
188
|
+
* On a microtask for `onReady`'s reason: this runs in the commit phase, and
|
|
189
|
+
* the ordinary answer to the news is to set state. Nothing else needs
|
|
190
|
+
* undoing — with no socket, every other path through this node (layout,
|
|
191
|
+
* props, focus, keys, teardown) already finds nothing to act on.
|
|
192
|
+
*/
|
|
193
|
+
_refuse() {
|
|
194
|
+
this._refused = true;
|
|
195
|
+
const err = new Error(
|
|
196
|
+
'react-x11: <foreign> needs the X11 backend — this one has no ' +
|
|
197
|
+
'cross-process window embedding, so nothing can be put in it. Ask ' +
|
|
198
|
+
"useSupports('embedding') before rendering one.",
|
|
199
|
+
);
|
|
200
|
+
this.error = err;
|
|
201
|
+
// The client is the whole reason this node is a Tab stop by default, and
|
|
202
|
+
// one that can never arrive would leave a dead one. `focusable` still
|
|
203
|
+
// wins, as it does on a box.
|
|
204
|
+
this.focusableByDefault = false;
|
|
205
|
+
queueMicrotask(() => {
|
|
206
|
+
if (this.destroyed) return;
|
|
207
|
+
if (this.props.onError) this.props.onError(err);
|
|
208
|
+
else if (this.app && !warnedCannotEmbed.has(this.app)) {
|
|
209
|
+
warnedCannotEmbed.add(this.app);
|
|
210
|
+
console.warn(err.message);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
162
215
|
/**
|
|
163
216
|
* Embed `windowId`, or — with none — wait for something to put a window
|
|
164
217
|
* inside the container and take that.
|
package/src/glnodes.js
CHANGED
|
@@ -10,6 +10,7 @@ import { cssColorStraight } from 'ntk';
|
|
|
10
10
|
// re-exported so the GL element layer stays one import for consumers
|
|
11
11
|
export { directGLFailure, hasDirectGL } from './glbackend.js';
|
|
12
12
|
|
|
13
|
+
import { GlOverlay, canOverlay } from './gloverlay.js';
|
|
13
14
|
import { Node } from './nodes/node.js';
|
|
14
15
|
import { FramePacer, resolveFrameRate } from './pacing.js';
|
|
15
16
|
|
|
@@ -105,13 +106,22 @@ const px = (v) => Math.max(1, Math.round(v || 0));
|
|
|
105
106
|
* vocabulary as `<window frameRate>`. Defaults to the owning window's.
|
|
106
107
|
* - `glx` — a `chooseGLXConfig` spec, e.g. `{ DEPTH_SIZE: 24 }`.
|
|
107
108
|
*
|
|
108
|
-
* The X child window is stacked above everything drawn in the parent, so
|
|
109
|
-
* content cannot overlap it —
|
|
109
|
+
* The X child window is stacked above everything drawn in the parent, so the
|
|
110
|
+
* parent's 2D content cannot overlap it — but this node's own children do:
|
|
111
|
+
* they are laid out in its box like a `<box>`'s and drawn above the surface,
|
|
112
|
+
* on panes of their own (src/gloverlay.js).
|
|
113
|
+
*
|
|
114
|
+
* Pointer input over the surface is the tree's, on both backends: a press,
|
|
115
|
+
* a drag or a wheel over it is a synthetic event at the child under the
|
|
116
|
+
* pointer, or at this node, bubbling to its ancestors like anyone else's
|
|
117
|
+
* (`hitSurface` says how).
|
|
110
118
|
*/
|
|
111
119
|
export class GlAreaNode extends Node {
|
|
112
120
|
constructor(props, app) {
|
|
113
121
|
super('glarea', props, app);
|
|
114
122
|
this.window = null;
|
|
123
|
+
// the panes the children are drawn on, while there are children
|
|
124
|
+
this._overlay = null;
|
|
115
125
|
this.gl = null;
|
|
116
126
|
this.rect = null; // geometry last sent to the X window
|
|
117
127
|
this._realizing = false;
|
|
@@ -150,11 +160,21 @@ export class GlAreaNode extends Node {
|
|
|
150
160
|
|
|
151
161
|
_setRoot(root) {
|
|
152
162
|
super._setRoot(root);
|
|
163
|
+
// Children mounted along with this node — React builds a subtree before
|
|
164
|
+
// it attaches it — are drawn from the first frame of the window it joins.
|
|
165
|
+
if (this.children.length) root?._overlaid?.add(this);
|
|
153
166
|
// the owning window may already exist (a <glarea> mounted into a live
|
|
154
167
|
// tree); otherwise WindowNode.realize picks the subtree up
|
|
155
168
|
if (root?.window) this.realize();
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
insertBefore(child, beforeChild) {
|
|
172
|
+
super.insertBefore(child, beforeChild);
|
|
173
|
+
// 2D content above the surface: the owning window's next frame gives it
|
|
174
|
+
// a pane (`_syncOverlay`), and the child-list claim asks for that frame
|
|
175
|
+
this.root?._overlaid?.add(this);
|
|
176
|
+
}
|
|
177
|
+
|
|
158
178
|
/** Create the GL child window. Async: the visual comes from the server. */
|
|
159
179
|
realize() {
|
|
160
180
|
if (this.window || this.destroyed || this._realizing) return;
|
|
@@ -199,6 +219,9 @@ export class GlAreaNode extends Node {
|
|
|
199
219
|
recordGlxFailure(this.app, err);
|
|
200
220
|
this.gl = null;
|
|
201
221
|
if (this.window) {
|
|
222
|
+
// the children's panes stay, over whatever the fallback draws, and
|
|
223
|
+
// stay hittable; with none, nothing of this node covers the rect
|
|
224
|
+
if (!this._overlay) this._leaveSurfaces();
|
|
202
225
|
this.window.destroy?.();
|
|
203
226
|
this.window = null;
|
|
204
227
|
this.rect = null;
|
|
@@ -222,26 +245,32 @@ export class GlAreaNode extends Node {
|
|
|
222
245
|
// GL draws into the window itself: no 2d backing pixmap, and the
|
|
223
246
|
// frame clock is ours to drive
|
|
224
247
|
backingStore: false,
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// takes it back for a scene that zooms instead, and the default action
|
|
233
|
-
// scrolls the nearest container the way it does anywhere else.
|
|
248
|
+
// No pointer input is selected here, and that is the whole of how the
|
|
249
|
+
// pointer over the surface reaches the tree. X reports a device event
|
|
250
|
+
// to the first window up the hierarchy that selected it, so a press,
|
|
251
|
+
// a motion or a wheel over this window arrives at the owning window
|
|
252
|
+
// instead — in its coordinates and under its implicit grab, so a drag
|
|
253
|
+
// that leaves the surface keeps coming — and its event manager takes
|
|
254
|
+
// it from there like any other (`hitSurface` names this node).
|
|
234
255
|
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
256
|
+
// Selecting one here takes it away from the tree, which is how the
|
|
257
|
+
// wheel alone used to be handled: the surface selected ButtonPress to
|
|
258
|
+
// hear it and handed it back, and so every *press* on the surface
|
|
259
|
+
// ended here too, with the drag and the release that should have
|
|
260
|
+
// followed it. A listener on `node.window` does the same — ntk selects
|
|
261
|
+
// what a window is listened to for — which is what `forwardsPointer`
|
|
262
|
+
// exists to tell an element built on this one.
|
|
240
263
|
});
|
|
241
264
|
this.window = wnd;
|
|
242
265
|
this.rect = rect;
|
|
243
266
|
this.config = config;
|
|
244
267
|
wnd._reactX11Node = this;
|
|
268
|
+
// Above everything 2D in the owning window on both backends, and above
|
|
269
|
+
// every surface made before this one: X stacks a new child window over
|
|
270
|
+
// its siblings, and Core Animation a layer added later over one at the
|
|
271
|
+
// same zPosition. The window's hit test reads the list in that order
|
|
272
|
+
// (`EventManager._surfaceAt`).
|
|
273
|
+
this._joinSurfaces();
|
|
245
274
|
this.gl = wnd.getContext('opengl', config);
|
|
246
275
|
// a buffer freed by the display is a frame that can be drawn again
|
|
247
276
|
if (typeof this.gl?.onFrameAvailable !== 'undefined') {
|
|
@@ -256,6 +285,9 @@ export class GlAreaNode extends Node {
|
|
|
256
285
|
});
|
|
257
286
|
wnd.on?.('expose', () => this.requestFrame());
|
|
258
287
|
wnd.map?.();
|
|
288
|
+
// made on top of its siblings — over the panes of children that were
|
|
289
|
+
// laid out and painted before the visual query answered
|
|
290
|
+
this._overlay?.restack();
|
|
259
291
|
this.requestFrame();
|
|
260
292
|
}
|
|
261
293
|
|
|
@@ -385,31 +417,117 @@ export class GlAreaNode extends Node {
|
|
|
385
417
|
}
|
|
386
418
|
|
|
387
419
|
/**
|
|
388
|
-
*
|
|
420
|
+
* `true`: pointer input over this surface is the tree's on this backend —
|
|
421
|
+
* dispatched through the owning window's event manager, at this node, and
|
|
422
|
+
* bubbling from it like anyone else's.
|
|
389
423
|
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
424
|
+
* It is here to be asked by an element built on `<glarea>` that listens on
|
|
425
|
+
* `node.window` for the pointer, which it had to do before core delivered
|
|
426
|
+
* it. That listener has to go wherever this is true: on X11 it selects the
|
|
427
|
+
* event on the surface's own window, and X then delivers it *there*
|
|
428
|
+
* instead of to the tree (see `_create`) — every press, and the wheel with
|
|
429
|
+
* them, since both are ButtonPress.
|
|
394
430
|
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
431
|
+
* A getter on the class rather than a flag on each instance, so it can be
|
|
432
|
+
* read without rendering anything: `GlAreaNode.prototype.forwardsPointer`,
|
|
433
|
+
* from `react-x11/node`.
|
|
398
434
|
*/
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
435
|
+
get forwardsPointer() {
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* What a point in the owning window's space lands on, if it is over this
|
|
441
|
+
* surface: the child under it where there is one — the children are drawn
|
|
442
|
+
* above the surface (src/gloverlay.js) — and otherwise this node.
|
|
443
|
+
*
|
|
444
|
+
* A point that is over the surface is over it whatever the tree's own
|
|
445
|
+
* order says: X stacks the child window above the parent's drawing, and
|
|
446
|
+
* the Cocoa backend puts the layer at a zPosition over both presenters. So
|
|
447
|
+
* the window asks its surfaces before it hit-tests its tree
|
|
448
|
+
* (`EventManager._hit`), and a point inside lands here rather than on the
|
|
449
|
+
* box behind — which is all a tree walk can find, a window-owning node not
|
|
450
|
+
* being in its parent's paint order. It is the same answer X gives: the
|
|
451
|
+
* event it propagates to the owning window names this window, or a pane
|
|
452
|
+
* over it, as the child the pointer is in.
|
|
453
|
+
*
|
|
454
|
+
* The rect is the surface's own, in whole pixels, since the server decides
|
|
455
|
+
* by those. With no GL surface — not made yet, or given up after
|
|
456
|
+
* `onError` — only the children answer, and a point between them is the
|
|
457
|
+
* tree's. Hidden, or `pointerEvents: 'none'` here or above, lets the
|
|
458
|
+
* pointer through to what the tree has behind, as it does for any node.
|
|
459
|
+
*/
|
|
460
|
+
hitSurface(x, y) {
|
|
461
|
+
const panes = this._overlay?.panes.length ?? 0;
|
|
462
|
+
if (!this.window && panes === 0) return null;
|
|
463
|
+
const rect = this.rect ?? this._geometry();
|
|
464
|
+
if (
|
|
465
|
+
x < rect.x ||
|
|
466
|
+
y < rect.y ||
|
|
467
|
+
x >= rect.x + rect.width ||
|
|
468
|
+
y >= rect.y + rect.height
|
|
469
|
+
) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
for (let n = this; n; n = n.parent) {
|
|
473
|
+
if (n.destroyed || n.hidden) return null;
|
|
474
|
+
if (n.style?.display === 'none' || n.style?.pointerEvents === 'none') {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if (n.isWindow) break;
|
|
478
|
+
}
|
|
479
|
+
if (panes !== 0) {
|
|
480
|
+
// front to back, as the tree's own hit test walks a box's children
|
|
481
|
+
const order = this.paintOrder();
|
|
482
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
483
|
+
const hit = order[i].hitTest(x, y);
|
|
484
|
+
if (hit) return hit;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return this.window ? this : null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Into the owning window's hit test, once: a GL window or a pane covers
|
|
491
|
+
* the rect now (`EventManager._surfaceAt`). */
|
|
492
|
+
_joinSurfaces() {
|
|
493
|
+
const surfaces = this.root?._surfaces;
|
|
494
|
+
if (surfaces && !surfaces.includes(this)) surfaces.push(this);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Out of the owning window's hit test: nothing covers the rect any more,
|
|
498
|
+
* and what the tree has behind it answers again. */
|
|
499
|
+
_leaveSurfaces() {
|
|
500
|
+
const surfaces = this.root?._surfaces;
|
|
501
|
+
const at = surfaces ? surfaces.indexOf(this) : -1;
|
|
502
|
+
if (at !== -1) surfaces.splice(at, 1);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The owning window's frame, after layout (`WindowNode._syncOverlays`):
|
|
507
|
+
* panes for where the children are now. True when a pane was made,
|
|
508
|
+
* resized or dropped — a paint the frame then owes.
|
|
509
|
+
*/
|
|
510
|
+
_syncOverlay() {
|
|
511
|
+
if (!this._overlay) {
|
|
512
|
+
if (this.children.length === 0 || !canOverlay(this.app)) {
|
|
513
|
+
this.root?._overlaid?.delete(this);
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
this._overlay = new GlOverlay(this);
|
|
517
|
+
}
|
|
518
|
+
const changed = this._overlay.sync();
|
|
519
|
+
if (this._overlay.panes.length) this._joinSurfaces();
|
|
520
|
+
if (this.children.length === 0 && this._overlay.panes.length === 0) {
|
|
521
|
+
this._overlay = null;
|
|
522
|
+
this.root?._overlaid?.delete(this);
|
|
523
|
+
if (!this.window) this._leaveSurfaces();
|
|
524
|
+
}
|
|
525
|
+
return changed;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** …and the paint it owes them, with the frame's damage. */
|
|
529
|
+
_paintOverlay(damage) {
|
|
530
|
+
this._overlay?.paint(damage);
|
|
413
531
|
}
|
|
414
532
|
|
|
415
533
|
applyProps(newProps, oldProps) {
|
|
@@ -424,14 +542,27 @@ export class GlAreaNode extends Node {
|
|
|
424
542
|
super.setHidden(hidden);
|
|
425
543
|
if (hidden) this.window?.unmap?.();
|
|
426
544
|
else this.window?.map?.();
|
|
545
|
+
this._overlay?.setHidden(hidden);
|
|
427
546
|
}
|
|
428
547
|
|
|
429
|
-
//
|
|
430
|
-
// 2d context, and
|
|
548
|
+
// The surface covers this rect: nothing of this node is painted into the
|
|
549
|
+
// parent's 2d context, and its children are painted above the surface on
|
|
550
|
+
// panes of their own (src/gloverlay.js) rather than in the window's walk.
|
|
431
551
|
paint() {}
|
|
432
552
|
|
|
553
|
+
// …which is also why they are cut to its box: a pane never reaches past
|
|
554
|
+
// the surface, and the hit test and the damage model have to agree with
|
|
555
|
+
// the panes about where the children can be
|
|
556
|
+
clipsChildren() {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
433
560
|
destroySubtree() {
|
|
434
561
|
if (this.destroyed) return;
|
|
562
|
+
this._leaveSurfaces();
|
|
563
|
+
this.root?._overlaid?.delete(this);
|
|
564
|
+
this._overlay?.destroy();
|
|
565
|
+
this._overlay = null;
|
|
435
566
|
super.destroySubtree();
|
|
436
567
|
this._pacer.cancel();
|
|
437
568
|
this.gl?.destroy?.();
|