react-x11 2.26.1 → 2.26.3

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.1",
3
+ "version": "2.26.3",
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
@@ -76,13 +76,30 @@ function woffToSfnt(woff) {
76
76
  return out;
77
77
  }
78
78
 
79
+ /**
80
+ * The system font's two names from before CSS gave it one: `-apple-system`
81
+ * is WebKit's and Gecko's, `BlinkMacSystemFont` is Blink's, and a stack
82
+ * written for the web leads with both — GitHub's is `-apple-system,
83
+ * BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial,
84
+ * sans-serif`. CSS Fonts 4 standardised them as `system-ui`, the name the
85
+ * bridge knows. As families of their own neither exists, so the list moved
86
+ * on to the first one that does and a page written for SF was set in
87
+ * Helvetica. They name this platform's face and mean nothing on another,
88
+ * which is why they are resolved here and not in the X11 engine.
89
+ */
90
+ const SYSTEM_UI_ALIASES = new Set(['-apple-system', 'blinkmacsystemfont']);
91
+
79
92
  /** family list string -> array: 'Inter, "SF Pro", sans-serif' */
80
93
  function familyList(family) {
81
- if (Array.isArray(family)) return family;
82
- return String(family ?? 'sans-serif')
83
- .split(',')
84
- .map((f) => f.trim().replace(/^["']|["']$/g, ''))
85
- .filter(Boolean);
94
+ const names = Array.isArray(family)
95
+ ? family
96
+ : String(family ?? 'sans-serif')
97
+ .split(',')
98
+ .map((f) => f.trim().replace(/^["']|["']$/g, ''))
99
+ .filter(Boolean);
100
+ return names.map((name) =>
101
+ SYSTEM_UI_ALIASES.has(String(name).toLowerCase()) ? 'system-ui' : name,
102
+ );
86
103
  }
87
104
 
88
105
  function numericWeight(weight) {
@@ -519,6 +536,7 @@ const GENERIC_FAMILIES = new Set([
519
536
  'system-ui',
520
537
  'ui-sans-serif',
521
538
  'ui-monospace',
539
+ ...SYSTEM_UI_ALIASES,
522
540
  ]);
523
541
 
524
542
  /** The size a face is probed at when the question has no size in it. */
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