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.
Files changed (77) hide show
  1. package/package.json +12 -8
  2. package/src/Reconciler.js +38 -35
  3. package/src/appcontext.js +59 -30
  4. package/src/cocoa/app.js +11 -0
  5. package/src/cocoa/glarea.js +4 -2
  6. package/src/cocoa/overlay.js +159 -0
  7. package/src/cocoa/promotion.js +13 -0
  8. package/src/components/Select.js +8 -2
  9. package/src/embedding.js +31 -0
  10. package/src/events.js +80 -14
  11. package/src/foreignnodes.js +56 -3
  12. package/src/glnodes.js +171 -40
  13. package/src/gloverlay.js +383 -0
  14. package/src/host.d.ts +0 -1
  15. package/src/index.d.ts +19 -1
  16. package/src/node.d.ts +12 -1
  17. package/src/nodes/boxpaint.js +9 -0
  18. package/src/nodes/preedit.js +64 -14
  19. package/src/nodes/window/flush.js +35 -0
  20. package/src/nodes/window/window.js +9 -0
  21. package/src/scale.js +52 -22
  22. package/src/screencolor.js +104 -17
  23. package/src/types/elements.d.ts +15 -13
  24. package/src/wayland/app.js +560 -0
  25. package/src/wayland/backendwindow.js +1123 -0
  26. package/src/wayland/clipboard.js +326 -0
  27. package/src/wayland/connection.js +482 -0
  28. package/src/wayland/context2d.js +2133 -0
  29. package/src/wayland/decorations.js +476 -0
  30. package/src/wayland/dmabuf.js +89 -0
  31. package/src/wayland/dnd.js +581 -0
  32. package/src/wayland/fdutil.js +108 -0
  33. package/src/wayland/framestyle.js +257 -0
  34. package/src/wayland/glarea.js +371 -0
  35. package/src/wayland/glcontext.js +415 -0
  36. package/src/wayland/glyphatlas.js +237 -0
  37. package/src/wayland/input.js +417 -0
  38. package/src/wayland/keysymnames.js +35 -0
  39. package/src/wayland/layershell.js +363 -0
  40. package/src/wayland/outputs.js +601 -0
  41. package/src/wayland/protocols/cursor-shape-v1.json +1 -0
  42. package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
  43. package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
  44. package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
  45. package/src/wayland/protocols/fractional-scale-v1.json +1 -0
  46. package/src/wayland/protocols/index.json +127 -0
  47. package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
  48. package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
  49. package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
  50. package/src/wayland/protocols/presentation-time.json +1 -0
  51. package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
  52. package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
  53. package/src/wayland/protocols/tablet-v2.json +1 -0
  54. package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
  55. package/src/wayland/protocols/viewporter.json +1 -0
  56. package/src/wayland/protocols/wayland.json +1 -0
  57. package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
  58. package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
  59. package/src/wayland/protocols/xdg-activation-v1.json +1 -0
  60. package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
  61. package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
  62. package/src/wayland/protocols/xdg-shell.json +1 -0
  63. package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
  64. package/src/wayland/readback.js +99 -0
  65. package/src/wayland/screencopy.js +584 -0
  66. package/src/wayland/seat.js +584 -0
  67. package/src/wayland/shm.js +226 -0
  68. package/src/wayland/ssd.js +106 -0
  69. package/src/wayland/surface.js +123 -0
  70. package/src/wayland/swapchain.js +411 -0
  71. package/src/wayland/tablet.js +522 -0
  72. package/src/wayland/target.js +263 -0
  73. package/src/wayland/text.js +113 -0
  74. package/src/wayland/textinput.js +671 -0
  75. package/src/wayland/touch.js +284 -0
  76. package/src/wayland/window.js +827 -0
  77. package/src/wayland/xkb.js +425 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -20,6 +20,8 @@
20
20
  "examples:appearance": "tsx examples/appearance.jsx",
21
21
  "examples:animation": "tsx examples/animation.jsx",
22
22
  "examples:simple": "tsx examples/simple.jsx",
23
+ "examples:wayland-gl": "node examples/wayland-gl.mjs",
24
+ "examples:wayland-ui": "node examples/wayland-ui.mjs",
23
25
  "examples:simple-nojsx": "node examples/simple-nojsx.js",
24
26
  "examples:xeyes": "tsx examples/xeyes.jsx",
25
27
  "examples:dashboard": "tsx examples/dashboard.jsx",
@@ -60,6 +62,7 @@
60
62
  "examples:frame": "tsx examples/frame.jsx",
61
63
  "labs:text-baseline": "tsx examples/labs/text-baseline.jsx",
62
64
  "labs:direct-gl": "tsx examples/labs/direct-gl.jsx",
65
+ "labs:gl-overlay": "tsx examples/labs/gl-overlay.jsx",
63
66
  "examples:viewer3d": "tsx examples/viewer3d.jsx",
64
67
  "examples:transparent": "tsx examples/transparent.jsx",
65
68
  "examples:windows": "tsx examples/windows.jsx",
@@ -99,20 +102,21 @@
99
102
  "ical.js": "^2.2.1",
100
103
  "linebreak": "^1.1.0",
101
104
  "ntk": "^8.7.0",
102
- "react-reconciler": "^0.33.0",
105
+ "react-reconciler": "^0.34.0",
103
106
  "yoga-layout": "^3.2.1"
104
107
  },
105
108
  "optionalDependencies": {
106
109
  "@windowkit/appkit": "^0.10.0",
110
+ "@windowkit/wayland": "^3.1.1",
107
111
  "dbus-native": "^0.15.1",
108
- "x11-dri": "^0.7.0"
112
+ "x11-dri": "^0.9.0"
109
113
  },
110
114
  "peerDependencies": {
111
115
  "@babel/core": "^8.0.0",
112
116
  "@babel/plugin-transform-react-jsx": "^8.0.0",
113
117
  "hot-module-replacement": "^4.0.1",
114
- "react": "^19.0.0",
115
- "react-refresh": "^0.18.0"
118
+ "react": "^19.3.0",
119
+ "react-refresh": "^0.19.0"
116
120
  },
117
121
  "peerDependenciesMeta": {
118
122
  "@babel/core": {
@@ -145,9 +149,9 @@
145
149
  "hot-module-replacement": "^4.0.1",
146
150
  "katex": "^0.18.1",
147
151
  "prettier": "^3.6.0",
148
- "react": "^19.2.8",
149
- "react-devtools-core": "^7.0.1",
150
- "react-refresh": "^0.18.0",
152
+ "react": "^19.3.0",
153
+ "react-devtools-core": "^8.0.0",
154
+ "react-refresh": "^0.19.0",
151
155
  "tsx": "^4.23.1",
152
156
  "typescript": "^7.0.2",
153
157
  "ws": "^8.21.1"
package/src/Reconciler.js CHANGED
@@ -138,17 +138,18 @@ const HostConfig = {
138
138
  return {
139
139
  isInsideText: false,
140
140
  isInsideSvg: false,
141
- isInside3d: false,
142
141
  };
143
142
  },
144
143
 
145
144
  getChildHostContext(parentHostContext, type) {
145
+ // A `<glarea>`'s children are drawn nodes like any box's — 2D content
146
+ // above the surface (src/gloverlay.js) — so it opens no context of its
147
+ // own. A scene graph over the surface is `@react-x11/components/three`,
148
+ // with a reconciler of its own.
146
149
  return {
147
150
  isInsideText: parentHostContext.isInsideText || type === 'text',
148
151
  // <svg> children are declarative SVG elements, not react-x11 nodes
149
152
  isInsideSvg: parentHostContext.isInsideSvg || type === 'svg',
150
- // inside <glarea> the children are scene nodes, not drawn nodes
151
- isInside3d: parentHostContext.isInside3d || type === 'glarea',
152
153
  };
153
154
  },
154
155
 
@@ -194,17 +195,6 @@ const HostConfig = {
194
195
  '<text> spans and strings are.',
195
196
  );
196
197
  }
197
- if (hostContext.isInside3d) {
198
- // `<glarea>` is a leaf here: it owns the surface, the frame clock and
199
- // the swap, and `onDraw` is the escape hatch. A *scene graph* over it
200
- // — meshes, materials, lights, post-processing, on either backend —
201
- // is `@react-x11/components/three`, which brings its own reconciler.
202
- throw new Error(
203
- `react-x11: <${type}> is not an element — <glarea> takes no ` +
204
- 'children. Draw through `onDraw`, or use ' +
205
- '`@react-x11/components/three` for a scene graph. See docs/gl.md.',
206
- );
207
- }
208
198
  let node;
209
199
  switch (type) {
210
200
  case 'window':
@@ -463,6 +453,10 @@ const HostConfig = {
463
453
  },
464
454
  startSuspendingCommit() {},
465
455
  suspendInstance() {},
456
+ // react-reconciler 0.34 calls this on every commit whose lanes are all
457
+ // transition, retry or idle lanes, whether or not the tree has a
458
+ // <ViewTransition>. There is no view transition here to wait on.
459
+ suspendOnActiveViewTransition() {},
466
460
  waitForCommitToBeReady() {
467
461
  return null;
468
462
  },
@@ -584,11 +578,11 @@ const isNtkApp = (v) =>
584
578
  */
585
579
  function resolveBackend(options) {
586
580
  const asked = options.backend ?? process.env.REACT_X11_BACKEND ?? 'auto';
587
- if (asked === 'x11' || asked === 'cocoa') return asked;
581
+ if (asked === 'x11' || asked === 'cocoa' || asked === 'wayland') return asked;
588
582
  if (asked !== 'auto') {
589
583
  throw new Error(
590
584
  `react-x11: unknown backend ${JSON.stringify(asked)} — expected ` +
591
- "'x11', 'cocoa' or 'auto'.",
585
+ "'x11', 'cocoa', 'wayland' or 'auto'.",
592
586
  );
593
587
  }
594
588
  // Naming an X endpoint is choosing X11: a `display` or a `stream` (the
@@ -725,25 +719,34 @@ export async function createRoot(options = {}) {
725
719
  rest.backend === 'cocoa' || process.env.REACT_X11_BACKEND === 'cocoa';
726
720
  const connecting = !owned
727
721
  ? Promise.resolve(borrowed)
728
- : backend === 'cocoa'
729
- ? import('./cocoa/app.js')
730
- .then(({ createCocoaApp }) => createCocoaApp(rest))
731
- .catch((err) => {
732
- // Asked for by name, the bridge is required and its absence is
733
- // the error (it says how to install). Reached by 'auto', a mac
734
- // without it falls back to X11 so an XQuartz setup keeps
735
- // working — said once, because a silent fallback would look
736
- // like the native backend being broken rather than absent.
737
- if (cocoaAsked) throw err;
738
- if (process.env.NODE_ENV !== 'production') {
739
- console.warn(
740
- 'react-x11: no @windowkit/appkit bridge — falling back to the ' +
741
- `X11 backend. (${err.message.split('\n')[0]})`,
742
- );
743
- }
744
- return connectX11();
745
- })
746
- : connectX11();
722
+ : backend === 'wayland'
723
+ ? // Opt-in only, never through 'auto': X11 stays the default on Linux
724
+ // because the remote case (docs/remote.md) is the flagship reason
725
+ // this project exists and Wayland has no network transparency. The
726
+ // backend is opt-in; docs/wayland-backend.md says what it does
727
+ // and does not do.
728
+ import('./wayland/app.js').then(({ createWaylandApp }) =>
729
+ createWaylandApp(rest),
730
+ )
731
+ : backend === 'cocoa'
732
+ ? import('./cocoa/app.js')
733
+ .then(({ createCocoaApp }) => createCocoaApp(rest))
734
+ .catch((err) => {
735
+ // Asked for by name, the bridge is required and its absence is
736
+ // the error (it says how to install). Reached by 'auto', a mac
737
+ // without it falls back to X11 so an XQuartz setup keeps
738
+ // working — said once, because a silent fallback would look
739
+ // like the native backend being broken rather than absent.
740
+ if (cocoaAsked) throw err;
741
+ if (process.env.NODE_ENV !== 'production') {
742
+ console.warn(
743
+ 'react-x11: no @windowkit/appkit bridge — falling back to the ' +
744
+ `X11 backend. (${err.message.split('\n')[0]})`,
745
+ );
746
+ }
747
+ return connectX11();
748
+ })
749
+ : connectX11();
747
750
  const layout = loadLayout();
748
751
  const integrations = loadIntegrations(); // null when there is nothing to install
749
752
  const [app] = await Promise.all([connecting, layout, integrations]);
package/src/appcontext.js CHANGED
@@ -26,7 +26,9 @@ import {
26
26
  compositingActive,
27
27
  watchCompositing,
28
28
  } from './compositing.js';
29
+ import { canEmbed } from './embedding.js';
29
30
  import { hasDirectGL, watchDirectGL } from './glbackend.js';
31
+ import { canOverlay } from './gloverlay.js';
30
32
 
31
33
  const AppContext = createContext(null);
32
34
 
@@ -57,17 +59,29 @@ export function useApp() {
57
59
  return app;
58
60
  }
59
61
 
60
- const SUPPORTS_FEATURES = new Set([
61
- 'transparency',
62
- 'shaders',
63
- 'nativeControls',
64
- ]);
65
-
66
- // 'nativeControls' is a property of the backend, decided before the first
67
- // render and never changing after — so its subscription has nothing to
68
- // deliver and its snapshot is a property test.
62
+ // 'nativeControls', 'embedding' and 'glOverlay' are properties of the
63
+ // backend, decided before the first render and never changing after — so
64
+ // their subscription has nothing to deliver and their snapshot is a property
65
+ // test.
69
66
  const NEVER_CHANGES = () => () => {};
70
67
 
68
+ // What `useSupports` watches and reads, per feature. `read` answers a
69
+ // boolean, so the snapshot is stable for a given state — returning the
70
+ // visual object for 'transparency' would tear on every render.
71
+ const FEATURES = {
72
+ transparency: {
73
+ watch: watchCompositing,
74
+ read: (app) => compositingActive(app) && Boolean(argbVisual(app)),
75
+ },
76
+ shaders: { watch: watchDirectGL, read: hasDirectGL },
77
+ nativeControls: {
78
+ watch: NEVER_CHANGES,
79
+ read: (app) => Boolean(app.nativeBezels),
80
+ },
81
+ embedding: { watch: NEVER_CHANGES, read: canEmbed },
82
+ glOverlay: { watch: NEVER_CHANGES, read: canOverlay },
83
+ };
84
+
71
85
  /**
72
86
  * Can this **display** do something, as a value a component can branch on?
73
87
  *
@@ -127,40 +141,55 @@ const NEVER_CHANGES = () => () => {};
127
141
  * first render already reads the final answer. A policy raised after
128
142
  * connecting has missed that probe, and re-renders its readers when it
129
143
  * settles rather than leaving them with two different answers.
144
+ *
145
+ * `'embedding'` is true when this connection can take another process's
146
+ * window into its own — the X11 backend; never Cocoa, which has no such
147
+ * primitive, and never the headless mock. It is the question to ask before
148
+ * rendering a `<foreign>`, which refuses with one `onError` where the answer
149
+ * is no:
150
+ *
151
+ * ```jsx
152
+ * const embedding = useSupports('embedding');
153
+ * <box style={{ flexGrow: 1 }}>
154
+ * {embedding ? <foreign onReady={spawnInto} /> : <text>X11 only</text>}
155
+ * </box>
156
+ * ```
157
+ *
158
+ * Like `'nativeControls'`, it is a property of the backend and never changes.
159
+ *
160
+ * `'glOverlay'` is true when the children of a `<glarea>` are drawn above its
161
+ * GL surface on this connection — laid out in its box, painted on panes over
162
+ * the surface, hit before it. Both backends draw them; what differs is
163
+ * translucency, composited by Core Animation on the Cocoa backend and opaque
164
+ * on X11 (docs/elements.md says exactly how). It is the question to ask
165
+ * before handing a surface its HUD rather than drawing that some other way:
166
+ *
167
+ * ```jsx
168
+ * const overlay = useSupports('glOverlay');
169
+ * <glarea onDraw={drawMap}>{overlay && <Legend />}</glarea>
170
+ * ```
171
+ *
172
+ * A property of the backend too, and it never changes.
130
173
  */
131
174
  export function useSupports(feature) {
132
175
  const app = useApp();
133
- if (!SUPPORTS_FEATURES.has(feature)) {
176
+ const spec = Object.hasOwn(FEATURES, feature) ? FEATURES[feature] : null;
177
+ if (!spec) {
134
178
  throw new Error(
135
179
  `react-x11: useSupports(${JSON.stringify(feature)}) — unknown feature ` +
136
- `(expected one of ${[...SUPPORTS_FEATURES].join(', ')})`,
180
+ `(expected one of ${Object.keys(FEATURES).join(', ')})`,
137
181
  );
138
182
  }
139
- // Both features go through the same store, so the hooks below run in the
183
+ // Every feature goes through the same store, so the hooks below run in the
140
184
  // same order whatever is being asked about. Where compositing comes and
141
185
  // goes for as long as the app runs, the backend settles at most once — and
142
186
  // watching that one moment is what keeps two components rendered either
143
187
  // side of it from disagreeing (see watchDirectGL).
144
188
  const subscribe = useCallback(
145
- (onChange) =>
146
- feature === 'nativeControls'
147
- ? NEVER_CHANGES()
148
- : feature === 'shaders'
149
- ? watchDirectGL(app, onChange)
150
- : watchCompositing(app, onChange),
151
- [app, feature],
152
- );
153
- // a boolean, so the snapshot is stable for a given state — returning the
154
- // visual object here would tear on every render
155
- const snapshot = useCallback(
156
- () =>
157
- feature === 'nativeControls'
158
- ? Boolean(app.nativeBezels)
159
- : feature === 'shaders'
160
- ? hasDirectGL(app)
161
- : compositingActive(app) && Boolean(argbVisual(app)),
162
- [app, feature],
189
+ (onChange) => spec.watch(app, onChange),
190
+ [app, spec],
163
191
  );
192
+ const snapshot = useCallback(() => spec.read(app), [app, spec]);
164
193
  return useSyncExternalStore(subscribe, snapshot, snapshot);
165
194
  }
166
195
 
package/src/cocoa/app.js CHANGED
@@ -26,6 +26,7 @@ import { setScreensForTests } from '../screens.js';
26
26
  import { setScaleForTests } from '../scale.js';
27
27
  import { BezelStore } from './bezels.js';
28
28
  import { CocoaGLArea, cocoaGLConfig, resolveCocoaGLRuntime } from './glarea.js';
29
+ import { CocoaOverlayPane } from './overlay.js';
29
30
  import { CocoaDockMenu } from './dock.js';
30
31
  import { CocoaGlobalMenuExport } from './globalmenu.js';
31
32
  import { CocoaStatusItem } from './statusitem.js';
@@ -418,6 +419,16 @@ export class CocoaApp {
418
419
  return cocoaGLConfig(this, spec);
419
420
  }
420
421
 
422
+ /**
423
+ * The pane a `<glarea>`'s children are drawn on (src/gloverlay.js): one
424
+ * transparent layer above the surface's, which Core Animation composites —
425
+ * so the overlay blends with the GL frame here, where an X11 pane is an
426
+ * opaque child window. Having this at all is how the overlay knows.
427
+ */
428
+ createOverlayPane(attributes) {
429
+ return new CocoaOverlayPane(this, attributes);
430
+ }
431
+
421
432
  /**
422
433
  * The Frame host seam (src/frame/index.js): a pane's composited region
423
434
  * in this window. Its presence is what routes <Frame> to the shared-
@@ -14,7 +14,10 @@
14
14
  // `destroy()`, `requestAnimationFrame`. On X11 that child is a real X
15
15
  // window stacked above the parent's drawing; here it is a sublayer of the
16
16
  // window's root layer with a high zPosition — the same "GL sits above the
17
- // 2D" semantics, by the same mechanism the platform gives us.
17
+ // 2D" semantics, by the same mechanism the platform gives us. The layer
18
+ // takes no input: pointer events are the NSWindow's, and the window's hit
19
+ // test answers the surface for a point over it (`GlAreaNode.hitSurface`),
20
+ // which is where X11's event propagation ends up too.
18
21
  //
19
22
  // ## The API ladder
20
23
  //
@@ -142,7 +145,6 @@ export class CocoaGLArea {
142
145
  this.scale = this.parent.scale ?? app.scale ?? 1;
143
146
  this.destroyed = false;
144
147
  this._reactX11Node = null;
145
- this.onWheel = options.onWheel ?? null;
146
148
  this.layer = this._native.createLayer();
147
149
  this._native.addSublayer(this.parent._layer, this.layer);
148
150
  this.rect = null;
@@ -0,0 +1,159 @@
1
+ // The pane a `<glarea>`'s children are drawn on, on the Cocoa backend: a
2
+ // transparent bitmap layer over the surface (src/gloverlay.js).
3
+ //
4
+ // A sublayer of the window's root layer like the surface's own
5
+ // (src/cocoa/glarea.js), one step above it: the GL layer sits at zPosition
6
+ // 1e7, over everything both presenters put on the root layer, and this one
7
+ // at 1e7 + 1, over that. Core Animation composites it, so what the children
8
+ // leave transparent shows the GL frame, and a translucent fill, an
9
+ // antialiased edge or a shadow blends with it — the one backend where the
10
+ // overlay is not opaque. Every surface's GL layer shares the one zPosition,
11
+ // so where two surfaces overlap, both overlays are above both frames.
12
+ //
13
+ // It speaks the verbs of a window the overlay drives on X11 — `setState`,
14
+ // `map`, `unmap`, `getContext`, `destroy` — plus `present`, which puts what
15
+ // was painted on the layer: ntk blits an X window's backing store on its
16
+ // own, where a layer's contents are a copy the bitmap has to be pushed to.
17
+ import { CocoaContext2D } from './context2d.js';
18
+
19
+ export const OVERLAY_Z = 1e7 + 1;
20
+
21
+ export class CocoaOverlayPane {
22
+ constructor(app, options) {
23
+ this.app = app;
24
+ this.parent = options.parent;
25
+ this._native = app._native;
26
+ this.scale = this.parent.scale ?? app.scale ?? 1;
27
+ this.destroyed = false;
28
+ this.layer = this._native.createLayer();
29
+ this._native.addSublayer(this.parent._layer, this.layer);
30
+ this.rect = null;
31
+ this._surface = null;
32
+ this._surfaceSize = null;
33
+ this._gen = 0;
34
+ this._ctx = null;
35
+ this._dirty = false;
36
+ // Hidden until something is on it: a layer shows its contents from the
37
+ // moment it is added, and before the first present there are none to
38
+ // show, only whatever the render server makes of that.
39
+ this._presented = false;
40
+ this._hidden = false;
41
+ this.setState({
42
+ x: options.x ?? 0,
43
+ y: options.y ?? 0,
44
+ width: options.width ?? 1,
45
+ height: options.height ?? 1,
46
+ });
47
+ }
48
+
49
+ get width() {
50
+ return this.rect?.width ?? 0;
51
+ }
52
+
53
+ get height() {
54
+ return this.rect?.height ?? 0;
55
+ }
56
+
57
+ /** Geometry in device px, the unit the overlay's rects are in. */
58
+ setState(rect) {
59
+ if (this.destroyed) return;
60
+ this.rect = rect;
61
+ const s = this.scale;
62
+ this._setLayerProps({
63
+ frame: [rect.x / s, rect.y / s, rect.width / s, rect.height / s],
64
+ zPosition: OVERLAY_Z,
65
+ hidden: this._hidden || !this._presented,
66
+ });
67
+ }
68
+
69
+ map() {
70
+ this._hidden = false;
71
+ if (this._presented) this._setLayerProps({ hidden: false });
72
+ }
73
+
74
+ unmap() {
75
+ this._hidden = true;
76
+ this._setLayerProps({ hidden: true });
77
+ }
78
+
79
+ /** Implicit animations off, for the reason `CocoaGLArea._setLayerProps`
80
+ * gives: no frame's transaction covers a layer that is not a presenter's. */
81
+ _setLayerProps(props) {
82
+ if (this.destroyed) return;
83
+ const native = this._native;
84
+ native.txBegin({ disableActions: true });
85
+ try {
86
+ native.setLayerProps(this.layer, props);
87
+ } finally {
88
+ native.txCommit();
89
+ }
90
+ }
91
+
92
+ /** The bitmap, the pane's size — a new size is a new bitmap, cleared, and
93
+ * the old one freed now rather than by the handle's finalizer. */
94
+ _ensureSurface() {
95
+ const w = Math.max(1, this.rect?.width ?? 1);
96
+ const h = Math.max(1, this.rect?.height ?? 1);
97
+ const size = this._surfaceSize;
98
+ if (!this._surface || size.width !== w || size.height !== h) {
99
+ this._release();
100
+ this._surface = this._native.createSurface(w, h, this.scale);
101
+ this._native.ctxClearRect(this._surface, 0, 0, w, h);
102
+ this._surfaceSize = { width: w, height: h };
103
+ this._gen++;
104
+ }
105
+ return this._surface;
106
+ }
107
+
108
+ _release() {
109
+ const surface = this._surface;
110
+ this._surface = null;
111
+ if (surface && typeof this._native.releaseSurface === 'function') {
112
+ this._native.releaseSurface(surface);
113
+ }
114
+ }
115
+
116
+ getContext() {
117
+ if (!this._ctx) {
118
+ this._ctx = new CocoaContext2D(
119
+ this._native,
120
+ () => this._ensureSurface(),
121
+ () => {
122
+ this._ensureSurface();
123
+ return this._gen;
124
+ },
125
+ );
126
+ this._ctx._fonts = this.app.fonts;
127
+ this._ctx._onDirty = () => {
128
+ this._dirty = true;
129
+ };
130
+ }
131
+ return this._ctx;
132
+ }
133
+
134
+ /** What was painted, onto the layer — a copy, so the bitmap is free to be
135
+ * painted again at once. */
136
+ present() {
137
+ if (this.destroyed || !this._dirty || !this._surface) return;
138
+ this._dirty = false;
139
+ const native = this._native;
140
+ native.txBegin({ disableActions: true });
141
+ try {
142
+ native.surfaceToLayer(this._surface, this.layer);
143
+ if (!this._presented) {
144
+ this._presented = true;
145
+ if (!this._hidden) native.setLayerProps(this.layer, { hidden: false });
146
+ }
147
+ } finally {
148
+ native.txCommit();
149
+ }
150
+ }
151
+
152
+ destroy() {
153
+ if (this.destroyed) return;
154
+ this.destroyed = true;
155
+ this._release();
156
+ this._ctx = null;
157
+ this._native.removeFromSuperlayer(this.layer);
158
+ }
159
+ }
@@ -142,6 +142,18 @@ function paintsSomething(node) {
142
142
  return Boolean(node.isScroller?.());
143
143
  }
144
144
 
145
+ /**
146
+ * Inside a `<glarea>`: drawn on a pane above the surface (src/gloverlay.js).
147
+ * A layer of its own would sit on the root layer *under* the GL layer, so a
148
+ * promoted node there would vanish behind the surface it is drawn over.
149
+ */
150
+ function insideGlArea(node) {
151
+ for (let n = node.parent; n && !n.isWindow; n = n.parent) {
152
+ if (n.isGlArea) return true;
153
+ }
154
+ return false;
155
+ }
156
+
145
157
  /**
146
158
  * Can this node be a property box on a layer at all — the static half of
147
159
  * the answer, the same whatever the scene around it does: a plain box by
@@ -150,6 +162,7 @@ function paintsSomething(node) {
150
162
  */
151
163
  function promotableNode(node) {
152
164
  if (node.destroyed || !plainBox(node)) return false;
165
+ if (insideGlArea(node)) return false;
153
166
  if (!stylePaintsPlain(node, node._targetStyle ?? node.style)) return false;
154
167
  if (node.isScroller?.()) return false;
155
168
  return !paintsOutline(node);
@@ -354,11 +354,17 @@ export function Select({
354
354
  );
355
355
  const chosenRowTop = MENU_BORDER + metrics.pad + chosen * metrics.row;
356
356
  const top = triggerCapsBottom - chosenRowTop - rowCapsBottom;
357
+ // `top` is where the menu starts against the trigger's own top, so it
358
+ // goes in as an `offset` back past the trigger's bottom — not as an `at`.
359
+ // Tracking asks whether `at` is still in view, and a menu opened over a
360
+ // trigger near the window's top edge starts above that edge: the popup
361
+ // is a real window and may, but the rect asked about was then "scrolled
362
+ // out", and the menu closed on the next unrelated layout pass (#552).
363
+ // What has to stay visible is the trigger.
357
364
  return {
358
365
  placement: 'bottom',
359
- offset: 0,
366
+ offset: top - triggerHeight,
360
367
  flip: false,
361
- at: { x: 0, y: top, width: node ? node.abs.width / scale : 0, height: 0 },
362
368
  alignOffset: -metrics.hang.left,
363
369
  height,
364
370
  width,
@@ -0,0 +1,31 @@
1
+ // Whether a connection can take another process's window into its own.
2
+ //
3
+ // `<foreign>` asks before it builds a socket, and `useSupports('embedding')`
4
+ // asks for a component deciding whether to render one. One function for
5
+ // both, because the two have to agree: a hook that said yes over an element
6
+ // that then refused, or the reverse, is the bug this closes (issue #531).
7
+ //
8
+ // A feature test on the connection, not a question about which backend this
9
+ // is — and not "is there an `X`", which is true everywhere: the Cocoa app
10
+ // carries an X stub for the modules with an X escape hatch, and the headless
11
+ // mock carries the same one. Both also have a `createWindow` that takes a
12
+ // parent, which is how `<foreign>` used to get as far as a socket over them.
13
+ // So each part of embedding is asked about by name: a container window to
14
+ // hold the client, `ReparentWindow` to move somebody else's window into it,
15
+ // and the save set, which keeps that window alive if this process dies
16
+ // holding it — the promise docs/embedding.md makes about a window we do not
17
+ // own.
18
+ //
19
+ // A leaf module on purpose, for glbackend.js's reason: it imports nothing of
20
+ // ours, so both `appcontext.js` and the node layer can use it without the
21
+ // two importing each other.
22
+
23
+ /** Can `app` host another client's window inside one of its own? */
24
+ export function canEmbed(app) {
25
+ const X = app?.X;
26
+ return (
27
+ typeof app?.createWindow === 'function' &&
28
+ typeof X?.ReparentWindow === 'function' &&
29
+ typeof X?.ChangeSaveSet === 'function'
30
+ );
31
+ }