react-x11 2.10.2 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -129
- package/package.json +10 -3
- package/src/Reconciler.js +15 -17
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +304 -49
- package/src/cocoa/bezels.js +175 -30
- package/src/cocoa/dnd.js +27 -13
- package/src/cocoa/fonts.js +3 -3
- package/src/cocoa/glarea.js +20 -3
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +4 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/screencolor.js +62 -0
- package/src/cocoa/threaded.js +246 -0
- package/src/cocoa/window.js +256 -42
- package/src/components/Select.js +2 -2
- package/src/components/anchor.js +3 -3
- package/src/components/native.js +12 -7
- package/src/components/theme.js +2 -2
- package/src/debug.js +1 -1
- package/src/decorations.js +1 -1
- package/src/editmenu.js +2 -2
- package/src/errors.js +46 -0
- package/src/events.js +6 -6
- package/src/foreignnodes.js +3 -2
- package/src/frames.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -0
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +21 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +4 -2
- package/src/node.js +19 -21
- package/src/nodes/animation.js +644 -0
- package/src/nodes/box.js +21 -0
- package/src/nodes/boxpaint.js +473 -0
- package/src/nodes/canvas.js +269 -0
- package/src/nodes/cascade.js +600 -0
- package/src/nodes/damage.js +183 -0
- package/src/nodes/edithistory.js +124 -0
- package/src/nodes/editmenupopup.js +260 -0
- package/src/nodes/hittest.js +185 -0
- package/src/nodes/image.js +266 -0
- package/src/nodes/install.js +75 -0
- package/src/nodes/invalidate.js +465 -0
- package/src/nodes/kinds.js +31 -0
- package/src/nodes/layout.js +439 -0
- package/src/nodes/layouthost.js +949 -0
- package/src/nodes/node.js +868 -0
- package/src/nodes/paint.js +466 -0
- package/src/nodes/position.js +366 -0
- package/src/nodes/preedit.js +127 -0
- package/src/nodes/queries.js +330 -0
- package/src/nodes/rects.js +102 -0
- package/src/nodes/scrollable.js +891 -0
- package/src/nodes/scrollbars.js +138 -0
- package/src/nodes/scrollblit.js +1034 -0
- package/src/nodes/selectable.js +142 -0
- package/src/nodes/styling.js +225 -0
- package/src/nodes/text.js +649 -0
- package/src/nodes/textarea.js +391 -0
- package/src/nodes/textinput.js +1146 -0
- package/src/nodes/util.js +17 -0
- package/src/nodes/window/anchoring.js +161 -0
- package/src/nodes/window/capabilities.js +190 -0
- package/src/nodes/window/debugpaint.js +83 -0
- package/src/nodes/window/droptarget.js +145 -0
- package/src/nodes/window/floors.js +577 -0
- package/src/nodes/window/flush.js +334 -0
- package/src/nodes/window/hints.js +482 -0
- package/src/nodes/window/listeners.js +222 -0
- package/src/nodes/window/popup.js +71 -0
- package/src/nodes/window/size.js +591 -0
- package/src/nodes/window/window.js +945 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/screencolor.js +212 -38
- package/src/screencolorhooks.js +6 -2
- package/src/styles.js +137 -15
- package/src/svgnodes.js +2 -1
- package/src/testing/harness.js +2 -2
- package/src/textselection.js +5 -3
- package/src/trace-registry.js +1 -1
- package/src/types/components.d.ts +38 -6
- package/src/types/elements.d.ts +11 -1
- package/src/types/nodes.d.ts +33 -5
- package/src/types/screencolor.d.ts +20 -14
- 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,222 @@
|
|
|
1
|
+
// The X events a realized window listens to and what each one does, and the
|
|
2
|
+
// close request's default.
|
|
3
|
+
|
|
4
|
+
import { discrete } from '../../events.js';
|
|
5
|
+
import { createClientMessages } from '../../clientmessage.js';
|
|
6
|
+
import { callHandler } from '../../errors.js';
|
|
7
|
+
import { runWithPriority, DiscreteEventPriority } from '../../priority.js';
|
|
8
|
+
import { DEV } from '../util.js';
|
|
9
|
+
|
|
10
|
+
/** The window's event listeners, installed onto `WindowNode.prototype` by window.js. */
|
|
11
|
+
export class WindowListeners {
|
|
12
|
+
/**
|
|
13
|
+
* `parentWindow` is realize()'s, and only the close handshake reads it:
|
|
14
|
+
* whether the window manager frames this window decides whether
|
|
15
|
+
* WM_DELETE_WINDOW means anything on it.
|
|
16
|
+
*/
|
|
17
|
+
_attachWindowListeners(parentWindow) {
|
|
18
|
+
const wnd = this.window;
|
|
19
|
+
if (typeof wnd.on !== 'function') return;
|
|
20
|
+
wnd.on('resize', (ev) => {
|
|
21
|
+
// ConfigureNotify also fires for pure moves and reparents; only a real
|
|
22
|
+
// size change dirties layout or pixels.
|
|
23
|
+
//
|
|
24
|
+
// Compared against the laid-out rect rather than ntk's `ev.resized`
|
|
25
|
+
// (which is "differs from the last delivered event"), because the two
|
|
26
|
+
// answer different questions and this is the one that matters here: a
|
|
27
|
+
// React-driven resize configures the window and lays out in the same
|
|
28
|
+
// commit, and the server's echo comes back a moment later saying the
|
|
29
|
+
// size changed — true, but already accounted for. `ev.resized` would
|
|
30
|
+
// relayout and fully repaint a second time for every controlled
|
|
31
|
+
// resize.
|
|
32
|
+
if (ev.width !== this.abs.width || ev.height !== this.abs.height) {
|
|
33
|
+
this.needsLayout = true;
|
|
34
|
+
this.invalidate(true, null, 'resize');
|
|
35
|
+
}
|
|
36
|
+
// The end of an `'auto'` window's authority over its own size. A
|
|
37
|
+
// ConfigureNotify that does not match what we last asked for is
|
|
38
|
+
// somebody else's decision — the user dragging an edge, or a window
|
|
39
|
+
// manager applying a policy of its own — and from here on the window
|
|
40
|
+
// is theirs. Growing it back under a user who has just made it smaller
|
|
41
|
+
// is the one behaviour worse than not fitting the content.
|
|
42
|
+
//
|
|
43
|
+
// Checked against `_requestedSize` rather than ntk's `ev.resized`
|
|
44
|
+
// because our own configures come back as echoes, and every one of
|
|
45
|
+
// them would otherwise read as the user taking over on the first
|
|
46
|
+
// re-fit.
|
|
47
|
+
const asked = this._requestedSize;
|
|
48
|
+
if (asked && (ev.width !== asked.width || ev.height !== asked.height)) {
|
|
49
|
+
this._userSized = true;
|
|
50
|
+
}
|
|
51
|
+
// Where the window sits on screen decides where popups anchored to it
|
|
52
|
+
// belong — and finding that out is a server round trip
|
|
53
|
+
// (TranslateCoordinates), so it is worth not making one per frame of a
|
|
54
|
+
// resize drag that never moved the window. `ev.moved` is ntk >= 6.2
|
|
55
|
+
// (sidorares/ntk#184), which is the floor; the `?? true` is for a mock
|
|
56
|
+
// window or a deduped older copy, which then keep the unconditional
|
|
57
|
+
// refresh rather than losing the anchor.
|
|
58
|
+
if (ev.moved ?? true) this._refreshScreenOrigin();
|
|
59
|
+
if (this.props.onResize) {
|
|
60
|
+
// the payload is application-facing: an app that stores this size
|
|
61
|
+
// and writes it back as `width`/`height` props must round-trip
|
|
62
|
+
// through one unit, and props are logical
|
|
63
|
+
const s = this.scale;
|
|
64
|
+
this.props.onResize(
|
|
65
|
+
s === 1
|
|
66
|
+
? ev
|
|
67
|
+
: {
|
|
68
|
+
...ev,
|
|
69
|
+
width: ev.width / s,
|
|
70
|
+
height: ev.height / s,
|
|
71
|
+
x: ev.x / s,
|
|
72
|
+
y: ev.y / s,
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
// A reparent is the other way the origin moves: the window manager puts
|
|
78
|
+
// the window inside its frame, and ConfigureNotify coordinates become
|
|
79
|
+
// frame-relative from then on. It usually arrives with a ConfigureNotify
|
|
80
|
+
// whose coordinates changed, but "usually" is not a guarantee — a frame
|
|
81
|
+
// whose client offset happens to match the old root position reports no
|
|
82
|
+
// move at all. StructureNotify is already selected for 'resize', so
|
|
83
|
+
// listening costs nothing.
|
|
84
|
+
wnd.on('reparent', () => this._refreshScreenOrigin());
|
|
85
|
+
// the frame clock emits 'draw' when the backing store content is invalid
|
|
86
|
+
wnd.on('draw', () => {
|
|
87
|
+
(this._frameReasons ??= new Set()).add('expose');
|
|
88
|
+
this.needsPaint = true;
|
|
89
|
+
this.flush();
|
|
90
|
+
});
|
|
91
|
+
wnd.on('expose', (ev) => {
|
|
92
|
+
this.props.onExpose?.(ev);
|
|
93
|
+
});
|
|
94
|
+
// Every ClientMessage addressed to this window (src/clientmessage.js).
|
|
95
|
+
// Unconditional, unlike the two opt-ins below it: a ClientMessage is
|
|
96
|
+
// delivered to the window's owner whatever event mask it selected, so
|
|
97
|
+
// there is nothing to arm and nothing a window without the prop pays.
|
|
98
|
+
// That in turn means the handler can be read from `props` per message —
|
|
99
|
+
// the rule every other event here follows — instead of being frozen at
|
|
100
|
+
// realize time.
|
|
101
|
+
//
|
|
102
|
+
// Attached before `_initDnd`'s listener on the same stream, which is what
|
|
103
|
+
// makes `preventDefault()` able to stop react-x11 answering XDND itself.
|
|
104
|
+
//
|
|
105
|
+
// Another client asking this one for something is a user action arriving
|
|
106
|
+
// by another route, so it lands at the priority — and in the paint — a
|
|
107
|
+
// click would get: `discrete`, like the WM close below.
|
|
108
|
+
this._clientMessages = createClientMessages(
|
|
109
|
+
this,
|
|
110
|
+
discrete((ev) => {
|
|
111
|
+
// Read here rather than where the message was taken, since a type the
|
|
112
|
+
// server had to be asked to name puts a round trip in between and
|
|
113
|
+
// React may have replaced the handler across it.
|
|
114
|
+
const handler = this.props.onClientMessage;
|
|
115
|
+
if (!handler) return;
|
|
116
|
+
runWithPriority(DiscreteEventPriority, () => {
|
|
117
|
+
callHandler(this, 'onClientMessage', handler, ev);
|
|
118
|
+
});
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
wnd.on('message', (raw) => {
|
|
122
|
+
// A window with no handler takes nothing on the queue and asks the
|
|
123
|
+
// server for nothing, on a stream that carries every XDND step of a
|
|
124
|
+
// drag passing over it.
|
|
125
|
+
if (this.props.onClientMessage) this._clientMessages.handle(raw);
|
|
126
|
+
});
|
|
127
|
+
// What the window manager actually did, which is the other half of the
|
|
128
|
+
// controlled pair — the props say what to ask for, this says what is
|
|
129
|
+
// true. Subscribing is what makes ntk select PropertyChange and watch
|
|
130
|
+
// `_NET_WM_STATE`, so it is opt-in: a window with no handler pays
|
|
131
|
+
// nothing. Read at realize time like onCloseRequest, since the
|
|
132
|
+
// subscription is a property of the X window, not of a render.
|
|
133
|
+
if (this.props.onStatesChange && typeof wnd.getWmStates === 'function') {
|
|
134
|
+
wnd.on('statechange', (states) => {
|
|
135
|
+
// a WM state change is something the user did to the window, so it
|
|
136
|
+
// carries the same priority a click would
|
|
137
|
+
runWithPriority(DiscreteEventPriority, () => {
|
|
138
|
+
this.props.onStatesChange?.(states);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// WM close button. Armed for every window the window manager actually
|
|
143
|
+
// manages, prop or no prop, because the alternative is not "no close
|
|
144
|
+
// handling" but a killed connection: a client with no WM_DELETE_WINDOW
|
|
145
|
+
// in WM_PROTOCOLS cannot be *asked* to close, so XKillClient is the only
|
|
146
|
+
// move the WM has left. Effects never clean up, and IceWM puts a "do you
|
|
147
|
+
// want to kill this client?" dialog in front of the user first. Every
|
|
148
|
+
// other toolkit arms this unconditionally for the same reason; making it
|
|
149
|
+
// the prop's side effect only moved that trap one level up.
|
|
150
|
+
//
|
|
151
|
+
// Not armed where the property is dead weight, which is every window the
|
|
152
|
+
// WM does not frame: a child <window> (a region inside another window)
|
|
153
|
+
// and an override-redirect <popup>. A `<popup overrideRedirect={false}>`
|
|
154
|
+
// is a real dialog and does get it.
|
|
155
|
+
//
|
|
156
|
+
// ntk >= 5.3 owns the protocol: listening for 'close' self-arms
|
|
157
|
+
// WM_PROTOCOLS and decodes the ClientMessage (#160). Its default action
|
|
158
|
+
// — destroy the window — is always prevented, because what happens next
|
|
159
|
+
// is React's decision: ntk tearing the window down underneath the
|
|
160
|
+
// reconciler is exactly what this handler exists to avoid. This also
|
|
161
|
+
// leaves the raw 'message' stream free for protocols react-x11 speaks
|
|
162
|
+
// itself (XDND, src/dnd.js).
|
|
163
|
+
if (!parentWindow && this.attributes?.overrideRedirect !== true) {
|
|
164
|
+
wnd.on(
|
|
165
|
+
'close',
|
|
166
|
+
// a WM close is a user action: discrete priority and a discrete
|
|
167
|
+
// paint, like a click. An onCloseRequest that answers with a
|
|
168
|
+
// "save your work?" dialog rather than an unmount is the case that
|
|
169
|
+
// notices — the dialog is the response to the press on the WM's
|
|
170
|
+
// close button, and it is one paint away.
|
|
171
|
+
discrete((ev) => {
|
|
172
|
+
ev.preventDefault();
|
|
173
|
+
runWithPriority(DiscreteEventPriority, () => {
|
|
174
|
+
const handler = this.props.onCloseRequest;
|
|
175
|
+
if (handler) callHandler(this, 'onCloseRequest', handler, ev);
|
|
176
|
+
else this._defaultCloseRequest();
|
|
177
|
+
});
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
this.events.attach();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A close request nobody handled — `onCloseRequest` is the override, this
|
|
186
|
+
* is what happens without one.
|
|
187
|
+
*
|
|
188
|
+
* Closing the app's primary window closes the app, which is what the
|
|
189
|
+
* button means everywhere else on the desktop. The tree unmounts and the
|
|
190
|
+
* connection closes, so effects clean up and the process ends on a drained
|
|
191
|
+
* loop rather than on a dead socket.
|
|
192
|
+
*
|
|
193
|
+
* Any other top-level window is a dialog or a satellite, and whether it
|
|
194
|
+
* goes away is app state this renderer cannot write: a `{open && <window/>}`
|
|
195
|
+
* was opened by a `setOpen(true)` somewhere, and unmapping it behind
|
|
196
|
+
* React's back would leave a window the app still believes is open and can
|
|
197
|
+
* never reopen. So the request is refused, and in dev it is said out loud —
|
|
198
|
+
* an inert close button is a bug, but a recoverable one, where guessing at
|
|
199
|
+
* the app's state is not.
|
|
200
|
+
*/
|
|
201
|
+
_defaultCloseRequest() {
|
|
202
|
+
if (this._isPrimaryWindow()) {
|
|
203
|
+
// fire and forget: unmount() is async (it awaits the connection
|
|
204
|
+
// closing) and a WM close request is answered synchronously or not at
|
|
205
|
+
// all. Errors reach the app's own handler, never an unhandled rejection.
|
|
206
|
+
Promise.resolve(this.app?._reactX11Root?.unmount?.()).catch((err) => {
|
|
207
|
+
this.app?.options?.onXError?.(err);
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (DEV && !this._warnedNoCloseHandler) {
|
|
212
|
+
this._warnedNoCloseHandler = true;
|
|
213
|
+
console.warn(
|
|
214
|
+
'react-x11: the window manager asked <window%s> to close, and it has ' +
|
|
215
|
+
"no onCloseRequest — so nothing happened. Only the app's primary " +
|
|
216
|
+
'window closes the app by default; a second window is opened by ' +
|
|
217
|
+
'app state and only app state can close it.',
|
|
218
|
+
this.props.title ? ` title=${JSON.stringify(this.props.title)}` : '',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// <popup>: a top-level window of its own for menus, tooltips and dropdowns —
|
|
2
|
+
// override-redirect unless it asks otherwise, holding a pointer grab while it
|
|
3
|
+
// is up if it asks for one.
|
|
4
|
+
|
|
5
|
+
import { WindowNode } from './window.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* <popup>: an override-redirect top-level window (needs ntk >= 3.1.0, which
|
|
9
|
+
* forwards the attribute — sidorares/ntk#55). The window manager ignores it:
|
|
10
|
+
* no decorations, no focus stealing — menus, tooltips, dropdowns. `x`/`y`
|
|
11
|
+
* are screen coordinates (anchor with ev.nativeEvent.rootx/rooty or a ref's
|
|
12
|
+
* abs rect + owner window position). It may appear anywhere in the JSX tree
|
|
13
|
+
* but is always its own paint/event root, realized against the screen root
|
|
14
|
+
* in commitMount.
|
|
15
|
+
*/
|
|
16
|
+
export class PopupNode extends WindowNode {
|
|
17
|
+
/**
|
|
18
|
+
* `grab`: hold a pointer grab while this popup is up. That is how menus
|
|
19
|
+
* work on X — without it a press that lands anywhere else (another app,
|
|
20
|
+
* the root, or this app's own window *frame*, which belongs to the window
|
|
21
|
+
* manager) never reaches us, so the menu stays open behind whatever the
|
|
22
|
+
* user clicked. With the grab, that press arrives here instead, outside
|
|
23
|
+
* our bounds, and `onDismiss` fires. Needs ntk >= 3.7.0; without it the
|
|
24
|
+
* popup simply behaves as before.
|
|
25
|
+
*
|
|
26
|
+
* The grab rides the map, not `realize()`: X refuses a grab on an
|
|
27
|
+
* unviewable window (`GrabNotViewable`) and silently drops one whose
|
|
28
|
+
* window unmaps, so a popup born `hidden` — or one whose anchor is off
|
|
29
|
+
* screen — takes the grab when it actually reaches the screen. Grabbing
|
|
30
|
+
* from realize looked equivalent until `hidden` existed, and would have
|
|
31
|
+
* left a revealed menu holding no grab: open forever behind the first
|
|
32
|
+
* outside click, with nothing saying why.
|
|
33
|
+
*/
|
|
34
|
+
_mapNow() {
|
|
35
|
+
if (!super._mapNow()) return false;
|
|
36
|
+
if (this.props.grab) this.window.grabPointer?.({}, () => {});
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
destroySubtree() {
|
|
41
|
+
if (this.props.grab) this.window?.ungrabPointer?.();
|
|
42
|
+
super.destroySubtree();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
constructor(app, attributes, props) {
|
|
46
|
+
// Override-redirect is the default and is what keeps the window manager
|
|
47
|
+
// from repositioning or decorating a menu — but it is now a default
|
|
48
|
+
// rather than a fact, because it is the one bit standing between
|
|
49
|
+
// `<popup>` and a real, WM-managed dialog: `overrideRedirect={false}`
|
|
50
|
+
// gives a decorated, movable window the WM will stack above its owner
|
|
51
|
+
// and iconify with it. Menus, tooltips and `Select` keep the default.
|
|
52
|
+
//
|
|
53
|
+
// The EWMH type hint is additive — the spec asks for it on
|
|
54
|
+
// override-redirect windows too, so compositing managers can give menus
|
|
55
|
+
// and tooltips consistent shadows/animations. `windowType` overrides the
|
|
56
|
+
// default (e.g. "tooltip", "dropdown_menu"); `popup_menu` is the
|
|
57
|
+
// least-wrong answer for a popup that declares nothing, and the widgets
|
|
58
|
+
// that know better say so themselves — `Select`'s sheet is a
|
|
59
|
+
// `dropdown_menu`, a `Tooltip` a `tooltip` (issue #298).
|
|
60
|
+
super(
|
|
61
|
+
app,
|
|
62
|
+
{
|
|
63
|
+
...attributes,
|
|
64
|
+
overrideRedirect: attributes.overrideRedirect ?? true,
|
|
65
|
+
windowType: attributes.windowType ?? 'popup_menu',
|
|
66
|
+
},
|
|
67
|
+
props,
|
|
68
|
+
);
|
|
69
|
+
this.isPopup = true;
|
|
70
|
+
}
|
|
71
|
+
}
|