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.
Files changed (100) hide show
  1. package/README.md +278 -129
  2. package/package.json +10 -3
  3. package/src/Reconciler.js +15 -17
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/bootstrap.js +14 -0
  7. package/src/clientmessage.js +1 -1
  8. package/src/cocoa/app.js +304 -49
  9. package/src/cocoa/bezels.js +175 -30
  10. package/src/cocoa/dnd.js +27 -13
  11. package/src/cocoa/fonts.js +3 -3
  12. package/src/cocoa/glarea.js +20 -3
  13. package/src/cocoa/main.d.ts +8 -0
  14. package/src/cocoa/main.js +43 -0
  15. package/src/cocoa/panehost.js +15 -5
  16. package/src/cocoa/presenter.js +13 -9
  17. package/src/cocoa/promotion.js +4 -7
  18. package/src/cocoa/relaunch.js +207 -0
  19. package/src/cocoa/screencolor.js +62 -0
  20. package/src/cocoa/threaded.js +246 -0
  21. package/src/cocoa/window.js +256 -42
  22. package/src/components/Select.js +2 -2
  23. package/src/components/anchor.js +3 -3
  24. package/src/components/native.js +12 -7
  25. package/src/components/theme.js +2 -2
  26. package/src/debug.js +1 -1
  27. package/src/decorations.js +1 -1
  28. package/src/editmenu.js +2 -2
  29. package/src/errors.js +46 -0
  30. package/src/events.js +6 -6
  31. package/src/foreignnodes.js +3 -2
  32. package/src/frames.js +2 -2
  33. package/src/glnodes.js +1 -1
  34. package/src/grid.js +1653 -0
  35. package/src/host.d.ts +230 -0
  36. package/src/host.js +11 -3
  37. package/src/imagesource.js +1 -1
  38. package/src/index.d.ts +21 -4
  39. package/src/index.js +9 -1
  40. package/src/layouts.js +721 -0
  41. package/src/node.d.ts +4 -2
  42. package/src/node.js +19 -21
  43. package/src/nodes/animation.js +644 -0
  44. package/src/nodes/box.js +21 -0
  45. package/src/nodes/boxpaint.js +473 -0
  46. package/src/nodes/canvas.js +269 -0
  47. package/src/nodes/cascade.js +600 -0
  48. package/src/nodes/damage.js +183 -0
  49. package/src/nodes/edithistory.js +124 -0
  50. package/src/nodes/editmenupopup.js +260 -0
  51. package/src/nodes/hittest.js +185 -0
  52. package/src/nodes/image.js +266 -0
  53. package/src/nodes/install.js +75 -0
  54. package/src/nodes/invalidate.js +465 -0
  55. package/src/nodes/kinds.js +31 -0
  56. package/src/nodes/layout.js +439 -0
  57. package/src/nodes/layouthost.js +949 -0
  58. package/src/nodes/node.js +868 -0
  59. package/src/nodes/paint.js +466 -0
  60. package/src/nodes/position.js +366 -0
  61. package/src/nodes/preedit.js +127 -0
  62. package/src/nodes/queries.js +330 -0
  63. package/src/nodes/rects.js +102 -0
  64. package/src/nodes/scrollable.js +891 -0
  65. package/src/nodes/scrollbars.js +138 -0
  66. package/src/nodes/scrollblit.js +1034 -0
  67. package/src/nodes/selectable.js +142 -0
  68. package/src/nodes/styling.js +225 -0
  69. package/src/nodes/text.js +649 -0
  70. package/src/nodes/textarea.js +391 -0
  71. package/src/nodes/textinput.js +1146 -0
  72. package/src/nodes/util.js +17 -0
  73. package/src/nodes/window/anchoring.js +161 -0
  74. package/src/nodes/window/capabilities.js +190 -0
  75. package/src/nodes/window/debugpaint.js +83 -0
  76. package/src/nodes/window/droptarget.js +145 -0
  77. package/src/nodes/window/floors.js +577 -0
  78. package/src/nodes/window/flush.js +334 -0
  79. package/src/nodes/window/hints.js +482 -0
  80. package/src/nodes/window/listeners.js +222 -0
  81. package/src/nodes/window/popup.js +71 -0
  82. package/src/nodes/window/size.js +591 -0
  83. package/src/nodes/window/window.js +945 -0
  84. package/src/palette.js +1 -1
  85. package/src/registry.js +7 -3
  86. package/src/screencolor.js +212 -38
  87. package/src/screencolorhooks.js +6 -2
  88. package/src/styles.js +137 -15
  89. package/src/svgnodes.js +2 -1
  90. package/src/testing/harness.js +2 -2
  91. package/src/textselection.js +5 -3
  92. package/src/trace-registry.js +1 -1
  93. package/src/types/components.d.ts +38 -6
  94. package/src/types/elements.d.ts +11 -1
  95. package/src/types/nodes.d.ts +33 -5
  96. package/src/types/screencolor.d.ts +20 -14
  97. package/src/types/style.d.ts +94 -3
  98. package/src/windowstate.js +1 -1
  99. package/src/yoga.js +1 -1
  100. 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
+ }