react-x11 2.26.0 → 2.26.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.26.0",
3
+ "version": "2.26.2",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
package/src/cocoa/app.js CHANGED
@@ -259,6 +259,12 @@ export class CocoaApp {
259
259
  ? new CocoaFilePanels(this)
260
260
  : null;
261
261
 
262
+ // A window that is not the active one does not track the pointer here:
263
+ // no `:hover`, no enter or leave, no motion, until it is the active one
264
+ // again (`EventManager.hoverLive`). The platform's convention and not
265
+ // X11's, where every toolkit hovers in a window without the keyboard.
266
+ this.hoverNeedsActiveWindow = true;
267
+
262
268
  // AppKit-rendered control bezels. Its *presence* is the capability:
263
269
  // `useSupports('nativeControls')` and the widget set's `controls:
264
270
  // 'auto'` policy both test for this property, so a backend without it
@@ -150,6 +150,31 @@ function unpackLines(lineData = NO_GEOMETRY, runData = NO_GEOMETRY) {
150
150
  return lines;
151
151
  }
152
152
 
153
+ /**
154
+ * The one line of an empty paragraph, placed the way the native places a
155
+ * line with text in it: a `lineHeight` multiplies the face's natural line,
156
+ * and the leading is split either side of the glyphs (CSS half-leading,
157
+ * which ntk and `@windowkit/appkit` >= 0.15 both follow).
158
+ */
159
+ function blankLine(metrics, lineHeight, x) {
160
+ const { ascent, descent } = metrics;
161
+ const natural = metrics.lineHeight ?? ascent + descent;
162
+ const multiple = lineHeight >= 0 && lineHeight < Infinity ? lineHeight : 1;
163
+ const height = natural * multiple;
164
+ return {
165
+ x,
166
+ y: 0,
167
+ width: 0,
168
+ height,
169
+ baseline: (height - (ascent + descent)) / 2 + ascent,
170
+ ascent,
171
+ descent,
172
+ start: 0,
173
+ end: 0,
174
+ runs: [],
175
+ };
176
+ }
177
+
153
178
  /** Whether two spans read the same, field for field. */
154
179
  function sameSpan(a, b) {
155
180
  if (a === b) return true;
@@ -302,6 +327,19 @@ class CocoaTextLayout {
302
327
  ctx._drawLayout(this, x, y);
303
328
  }
304
329
 
330
+ /**
331
+ * An empty paragraph as ntk lays it out: one line with no runs, as tall as
332
+ * a line of the face it would have been set in. CoreText sets no line for
333
+ * no text, and a layout with no lines has no height and a caret of none —
334
+ * so an empty field's caret was its three-pixel mark padding either side
335
+ * of nothing, and grew to the line the moment the first letter arrived.
336
+ */
337
+ _setBlank(line) {
338
+ this.lines = [line];
339
+ this.height = line.height;
340
+ this._blank = line;
341
+ }
342
+
305
343
  /** Code point boundary nearest the point, in layout coordinates. */
306
344
  indexAt(x, y) {
307
345
  return this._cpOf(this._native.layoutIndexAt(this._handle, x, y));
@@ -309,6 +347,13 @@ class CocoaTextLayout {
309
347
 
310
348
  /** Caret rect for a code-point index: { x, y, height }. */
311
349
  caretPosition(cp) {
350
+ // the rect the native gives a caret on a line with text in it — the
351
+ // line's box — so typing the first letter moves the caret and nothing
352
+ // else
353
+ const blank = this._blank;
354
+ if (blank) {
355
+ return { x: blank.x, y: blank.y, height: blank.height, line: 0 };
356
+ }
312
357
  return this._native.layoutCaret(this._handle, this._cuOf(cp));
313
358
  }
314
359
 
@@ -1315,6 +1360,24 @@ export class CocoaFontManager {
1315
1360
  minContent ? laid.map((span) => span.text).join('') : text,
1316
1361
  );
1317
1362
  layout._contextInk = contextInk;
1363
+ if (!text) {
1364
+ const first = spans[0] ?? {};
1365
+ const size = first.size ?? base.size ?? 14;
1366
+ const face = first.font ?? base.font;
1367
+ const metrics = (
1368
+ typeof face?.metrics === 'function'
1369
+ ? face
1370
+ : this.match(first.family ?? base.family, {
1371
+ weight: first.weight ?? base.weight,
1372
+ style: first.style ?? base.style,
1373
+ })
1374
+ ).metrics(size);
1375
+ const container =
1376
+ Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : 0;
1377
+ layout._setBlank(
1378
+ blankLine(metrics, lineHeight, flushFor(align, direction) * container),
1379
+ );
1380
+ }
1318
1381
  if (!minContent) {
1319
1382
  const runs = new Map();
1320
1383
  layout._hangRuns(sources, (i, rtl) => {
package/src/events.js CHANGED
@@ -647,6 +647,12 @@ export class EventManager {
647
647
  // …and the focused node hears the focus group's answer, not this
648
648
  // window's, whichever of them the X focus just moved between
649
649
  manager._syncDefaultFocus();
650
+ // …and so does hover, where the platform keeps it to the active window:
651
+ // this window's, and the top-level's whose answer it is — a dialog
652
+ // taking the keyboard is what puts its owner's hover back
653
+ const top = this.topLevelManager;
654
+ this._syncHover(native);
655
+ if (top !== this) top._syncHover(native);
650
656
  if (!changed) return;
651
657
  // …and the things that keep a window of their own open on the strength
652
658
  // of this one having focus — a menu, a dropdown — which the focused node
@@ -1014,6 +1020,12 @@ export class EventManager {
1014
1020
  inspectHandler('move', this._hit(native), native);
1015
1021
  return;
1016
1022
  }
1023
+ // a window the pointer does not hover in hears no motion (`hoverLive`);
1024
+ // a gesture that began in it is the pointer's own business and runs on
1025
+ if (!this.hoverLive && !this._gesture()) {
1026
+ this._dropHover(native);
1027
+ return;
1028
+ }
1017
1029
  runWithPriority(ContinuousEventPriority, () => {
1018
1030
  // an active drag owns the pointer: drag-path diffing replaces hover,
1019
1031
  // onDrag replaces mousemove. Below the threshold this falls through.
@@ -1088,6 +1100,77 @@ export class EventManager {
1088
1100
  return isFocusable(node);
1089
1101
  }
1090
1102
 
1103
+ /**
1104
+ * Whether the pointer hovers in this window at all.
1105
+ *
1106
+ * Nothing in CSS or in the DOM's events ties hover to focus: `:hover` is
1107
+ * "while the user designates an element with a pointing device", and a
1108
+ * `mousemove` is owed whenever the pointer moves over an element. What
1109
+ * ties them is the platform. On macOS a window that is not the active one
1110
+ * does not track the pointer — AppKit's own controls roll over in the key
1111
+ * window alone, and a browser there drops the moves over a window that is
1112
+ * neither key nor main and tells the page the pointer left — while on
1113
+ * X11, Wayland and Windows every toolkit hovers in a window that does not
1114
+ * have the keyboard, and with focus-follows-mouse the question hardly
1115
+ * arises. So the backend says which it is (`app.hoverNeedsActiveWindow`),
1116
+ * and only the Cocoa one does.
1117
+ *
1118
+ * It is also half enforced there already, which is how this was found:
1119
+ * a cursor set by an application that is not the active one is ignored by
1120
+ * the window server, so a link in a background window lit up and the
1121
+ * pointer over it stayed an arrow.
1122
+ *
1123
+ * "Active" is the top-level's answer for everything sharing its focus
1124
+ * (`keyboardFocused`), so a window whose dialog has the keyboard still
1125
+ * hovers, as a main window does under a key panel. A popup nothing
1126
+ * manages — a menu, a dropdown, a popover under a tray item — is on the
1127
+ * screen because the user just asked for it, in an application that may
1128
+ * have no active window at all, and always hovers.
1129
+ */
1130
+ get hoverLive() {
1131
+ if (!this.node.app?.hoverNeedsActiveWindow) return true;
1132
+ if (this.node.isPopup && this.node.attributes?.overrideRedirect === true) {
1133
+ return true;
1134
+ }
1135
+ return this.topLevelManager.keyboardFocused;
1136
+ }
1137
+
1138
+ /** A press, a capture or a drag holds the pointer. */
1139
+ _gesture() {
1140
+ return Boolean(this._captured() || this.downNode || this._dragArmed);
1141
+ }
1142
+
1143
+ /**
1144
+ * The pointer is over the window and hovers nothing in it: what its
1145
+ * leaving does to hover, less the leaving. `_pointer` stays, so the hover
1146
+ * comes back at that point when the window does (`_syncHover`), and
1147
+ * `<window onMouseOut>` is not called, since the pointer is still in it.
1148
+ */
1149
+ _dropHover(native) {
1150
+ if (!this.hoverPath.length && !this.attentionNode) return;
1151
+ runWithPriority(ContinuousEventPriority, () => {
1152
+ this._updateHover([], native);
1153
+ if (this._attentionNodes.size !== 0) {
1154
+ this._attentionSamples.length = 0;
1155
+ this._setAttention(null, native);
1156
+ }
1157
+ });
1158
+ }
1159
+
1160
+ /**
1161
+ * The keyboard moved between windows: hover goes with it where the
1162
+ * platform has it follow (`hoverLive`). A window that stopped being the
1163
+ * active one drops its hover as if the pointer had left; one that became
1164
+ * it is asked what is under the pointer, which did not move to say so.
1165
+ * Not during a gesture, which keeps what it has until it ends.
1166
+ */
1167
+ _syncHover(native) {
1168
+ if (!this.node.app?.hoverNeedsActiveWindow) return;
1169
+ if (this.node.destroyed || this._gesture()) return;
1170
+ if (this.hoverLive) this.refreshHover();
1171
+ else this._dropHover({ ...native, ...this._pointer });
1172
+ }
1173
+
1091
1174
  _onMouseOut(native) {
1092
1175
  // The pointer went into one of this window's surfaces, not out of the
1093
1176
  // window: X reports crossing into a child window as leaving this one,
@@ -1211,7 +1294,7 @@ export class EventManager {
1211
1294
  refreshHover() {
1212
1295
  const at = this._pointer;
1213
1296
  if (!at || inspectHandler || this.node.destroyed) return;
1214
- if (this._captured() || this.downNode || this._dragArmed) return;
1297
+ if (this._gesture() || !this.hoverLive) return;
1215
1298
  runWithPriority(ContinuousEventPriority, () => {
1216
1299
  const target = this._hit(at);
1217
1300
  const path = this._path(target);
@@ -618,6 +618,11 @@ function PaneHostView({
618
618
  onMouseDown: forward('mousedown', (ev) => ({ keycode: ev.button ?? 1 })),
619
619
  onMouseUp: forward('mouseup', (ev) => ({ keycode: ev.button ?? 1 })),
620
620
  onMouseMove: forward('mousemove', () => ({})),
621
+ // …and the pointer leaving, which the pane has no window to hear for
622
+ // itself: without it what the pointer last crossed in the pane stayed
623
+ // hovered once it was over the host, or the host was no longer the
624
+ // active window (`EventManager.hoverLive`)
625
+ onMouseLeave: forward('mouseout', () => ({})),
621
626
  // The host's event is in pixels — events.js turned each notch into
622
627
  // WHEEL_NOTCH_PX of them — and the pane's window turns what it is sent
623
628
  // into pixels again, so it is sent notches: sent the pixels, one notch