react-x11 2.11.0 → 2.13.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 +12 -4
  3. package/src/Reconciler.js +19 -31
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/appcontext.js +59 -30
  7. package/src/bootstrap.js +14 -0
  8. package/src/clientmessage.js +1 -1
  9. package/src/cocoa/app.js +303 -49
  10. package/src/cocoa/bezels.js +175 -30
  11. package/src/cocoa/dnd.js +27 -13
  12. package/src/cocoa/fonts.js +3 -3
  13. package/src/cocoa/glarea.js +24 -5
  14. package/src/cocoa/main.d.ts +8 -0
  15. package/src/cocoa/main.js +43 -0
  16. package/src/cocoa/overlay.js +159 -0
  17. package/src/cocoa/panehost.js +15 -5
  18. package/src/cocoa/presenter.js +13 -9
  19. package/src/cocoa/promotion.js +17 -7
  20. package/src/cocoa/relaunch.js +207 -0
  21. package/src/cocoa/threaded.js +246 -0
  22. package/src/cocoa/window.js +256 -42
  23. package/src/components/Select.js +2 -2
  24. package/src/components/anchor.js +3 -3
  25. package/src/components/native.js +12 -7
  26. package/src/components/theme.js +2 -2
  27. package/src/debug.js +1 -1
  28. package/src/decorations.js +1 -1
  29. package/src/editmenu.js +2 -2
  30. package/src/embedding.js +31 -0
  31. package/src/errors.js +46 -0
  32. package/src/events.js +78 -18
  33. package/src/foreignnodes.js +59 -5
  34. package/src/frames.js +2 -2
  35. package/src/glnodes.js +172 -41
  36. package/src/gloverlay.js +383 -0
  37. package/src/grid.js +1653 -0
  38. package/src/host.d.ts +230 -1
  39. package/src/host.js +11 -3
  40. package/src/imagesource.js +1 -1
  41. package/src/index.d.ts +34 -4
  42. package/src/index.js +9 -1
  43. package/src/layouts.js +721 -0
  44. package/src/node.d.ts +16 -3
  45. package/src/node.js +19 -21
  46. package/src/nodes/animation.js +644 -0
  47. package/src/nodes/box.js +21 -0
  48. package/src/nodes/boxpaint.js +473 -0
  49. package/src/nodes/canvas.js +269 -0
  50. package/src/nodes/cascade.js +600 -0
  51. package/src/nodes/damage.js +183 -0
  52. package/src/nodes/edithistory.js +124 -0
  53. package/src/nodes/editmenupopup.js +260 -0
  54. package/src/nodes/hittest.js +185 -0
  55. package/src/nodes/image.js +266 -0
  56. package/src/nodes/install.js +75 -0
  57. package/src/nodes/invalidate.js +465 -0
  58. package/src/nodes/kinds.js +31 -0
  59. package/src/nodes/layout.js +439 -0
  60. package/src/nodes/layouthost.js +949 -0
  61. package/src/nodes/node.js +868 -0
  62. package/src/nodes/paint.js +466 -0
  63. package/src/nodes/position.js +366 -0
  64. package/src/nodes/preedit.js +127 -0
  65. package/src/nodes/queries.js +330 -0
  66. package/src/nodes/rects.js +102 -0
  67. package/src/nodes/scrollable.js +891 -0
  68. package/src/nodes/scrollbars.js +138 -0
  69. package/src/nodes/scrollblit.js +1034 -0
  70. package/src/nodes/selectable.js +142 -0
  71. package/src/nodes/styling.js +225 -0
  72. package/src/nodes/text.js +649 -0
  73. package/src/nodes/textarea.js +391 -0
  74. package/src/nodes/textinput.js +1146 -0
  75. package/src/nodes/util.js +17 -0
  76. package/src/nodes/window/anchoring.js +161 -0
  77. package/src/nodes/window/capabilities.js +190 -0
  78. package/src/nodes/window/debugpaint.js +83 -0
  79. package/src/nodes/window/droptarget.js +145 -0
  80. package/src/nodes/window/floors.js +577 -0
  81. package/src/nodes/window/flush.js +369 -0
  82. package/src/nodes/window/hints.js +482 -0
  83. package/src/nodes/window/listeners.js +222 -0
  84. package/src/nodes/window/popup.js +71 -0
  85. package/src/nodes/window/size.js +591 -0
  86. package/src/nodes/window/window.js +954 -0
  87. package/src/palette.js +1 -1
  88. package/src/registry.js +7 -3
  89. package/src/styles.js +137 -15
  90. package/src/svgnodes.js +2 -1
  91. package/src/testing/harness.js +2 -2
  92. package/src/textselection.js +5 -3
  93. package/src/trace-registry.js +1 -1
  94. package/src/types/components.d.ts +38 -6
  95. package/src/types/elements.d.ts +26 -14
  96. package/src/types/nodes.d.ts +17 -2
  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,142 @@
1
+ // The text an element answers for (#259) and being a selection surface: what
2
+ // a reader can select and copy out of a node, and the accessors every
3
+ // text-bearing element implements.
4
+
5
+ import { TextSelection } from '../textselection.js';
6
+
7
+ /** Text and selection, installed onto `Node.prototype` by node.js. */
8
+ export class NodeSelectable {
9
+ // --- the text an element answers for (issue #259) ------------------------
10
+ //
11
+ // Four questions, in one index space and one coordinate space: characters
12
+ // are **code points** (an emoji is one position, not two — the space ntk's
13
+ // caret API speaks), and rectangles are in the owning window's coordinates,
14
+ // the same ones `abs`, `contentBox()` and a mouse event's `x`/`y` are in.
15
+ //
16
+ // They are what a selection is made of, and the reason they are on `Node`
17
+ // rather than on `<text>`: the selection service walks a subtree and asks,
18
+ // so an element that answers them joins a document without core knowing it
19
+ // exists. The defaults are the honest answers for an element with no text
20
+ // — a `<box>` has none, and `null` says so rather than claiming an empty
21
+ // string sits somewhere inside it.
22
+
23
+ /** This element's text, or null when it has none — the string the three
24
+ * accessors below index into. */
25
+ textContent() {
26
+ return null;
27
+ }
28
+
29
+ /** The character boundary nearest a point, in window coordinates. Clamps,
30
+ * so a point past the end of the text answers with the end of it. */
31
+ textIndexAt(x, y) {
32
+ return 0;
33
+ }
34
+
35
+ /** Where a caret at this index would stand, in window coordinates — a
36
+ * zero-width rect from the top of the glyphs to the bottom of them. */
37
+ textCaretRect(index) {
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * The bands a highlight over `[start, end)` fills, in window coordinates:
43
+ * one per line, and more than one on a line whose text changes direction
44
+ * halfway across it. Empty when the range is empty or off the end.
45
+ */
46
+ textRangeRects(start, end) {
47
+ return [];
48
+ }
49
+
50
+ /**
51
+ * The part of this element's text the document selection covers, as
52
+ * `{ start, end }` in code points, or null. An element that paints its own
53
+ * text paints a band under it while this is set — `textRangeRects` gives
54
+ * the rectangles and `selectionColor` the fill — and that is the whole of
55
+ * taking part in a selection. `<text>` keeps no more state than this.
56
+ */
57
+ get selectionRange() {
58
+ return this._selRange
59
+ ? { start: this._selRange.start, end: this._selRange.end }
60
+ : null;
61
+ }
62
+
63
+ /** What to fill `textRangeRects` with while `selectionRange` is set: the
64
+ * surface's `selectionColor`, or the theme's accent tinted. */
65
+ get selectionColor() {
66
+ return this._selRange?.color ?? null;
67
+ }
68
+
69
+ // --- being a selection surface -------------------------------------------
70
+
71
+ /** `selectable` arrived or left. `true` makes this element the surface a
72
+ * drag inside it selects across; `false` opts its subtree out of the one
73
+ * above it, and is read where the surface is looked up. */
74
+ _syncSelectable(props) {
75
+ const wanted = props.selectable === true;
76
+ if (wanted === Boolean(this._textSelection)) return;
77
+ if (wanted) {
78
+ this._textSelection = new TextSelection(this);
79
+ // The I-beam is what says the text here can be taken. A surface is
80
+ // also a focus target — a11y.js reads the same prop — because Ctrl+C
81
+ // is a keystroke and a keystroke has to arrive somewhere.
82
+ this.defaultCursor ??= 'text';
83
+ } else {
84
+ this._textSelection.destroy();
85
+ this._textSelection = null;
86
+ if (this.defaultCursor === 'text') this.defaultCursor = undefined;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * The document selection this element owns, or null: a snapshot of
92
+ * `{ isCollapsed, text, ranges }`, not a live object.
93
+ *
94
+ * Named for the text rather than called `selection`, because a base class
95
+ * that claims a plain noun claims it from every element built on it — and
96
+ * `this.selection = ...` in a subclass constructor is then a TypeError
97
+ * against a getter, which is a bad way to find out.
98
+ */
99
+ get textSelection() {
100
+ const selection = this._textSelection;
101
+ if (!selection) return null;
102
+ return {
103
+ isCollapsed: selection.isCollapsed,
104
+ text: selection.text(),
105
+ ranges: [...selection.ranges].map(([node, [start, end]]) => ({
106
+ node,
107
+ start,
108
+ end,
109
+ })),
110
+ };
111
+ }
112
+
113
+ /** Select everything in this surface, and take PRIMARY with it. */
114
+ selectAll() {
115
+ this._textSelection?.selectAll();
116
+ return this;
117
+ }
118
+
119
+ /** Drop the selection in this surface. PRIMARY is left where it is: the
120
+ * text stays pasteable, which is what every other X client does. */
121
+ clearSelection() {
122
+ this._textSelection?.clear();
123
+ return this;
124
+ }
125
+
126
+ /** What a copy would put on the clipboard. */
127
+ selectedText() {
128
+ return this._textSelection?.text() ?? '';
129
+ }
130
+
131
+ /** Set both ends by hand — `{ node, index }` each, indices in code points.
132
+ * `setSelection(null)` is `clearSelection()`. */
133
+ setSelection(anchor, focus = anchor) {
134
+ this._textSelection?.setSelection(anchor, focus);
135
+ return this;
136
+ }
137
+
138
+ /** Another surface is showing the app's selection now. */
139
+ _selectionLost() {
140
+ this._textSelection?.lost();
141
+ }
142
+ }
@@ -0,0 +1,225 @@
1
+ // A node's own style: `props.style` resolved against its states, tokens,
2
+ // queries and scale into `this.style`, and the style names an element
3
+ // claims as its own semantics.
4
+
5
+ import {
6
+ flattenStyle,
7
+ validateStyle,
8
+ resolveStyleStates,
9
+ resolveComputedStyle,
10
+ scaleResolvedStyle,
11
+ hasStateStyles,
12
+ isStyleProp,
13
+ EMPTY_STYLE,
14
+ styleUsesTokens,
15
+ resolveTokens,
16
+ queryKinds,
17
+ QUERY_SIZE,
18
+ QUERY_SUPPORTS,
19
+ QUERY_CONTAINER,
20
+ containerQueryNames,
21
+ containerAnswers,
22
+ resolveQueries,
23
+ } from '../styles.js';
24
+ import { CUSTOM_SEMANTIC_NAMES } from './kinds.js';
25
+ import { DEV, shallowEqual } from './util.js';
26
+
27
+ /**
28
+ * A style property passed flat used to be silently dropped — it was neither
29
+ * a layout prop, a paint prop nor an `on*` handler, so nothing looked at it
30
+ * and nothing said so. Now that style has its own channel there is exactly
31
+ * one place it can go, and the wrong place is an error that names the fix.
32
+ */
33
+ function assertNoFlatStyleProps(props, kind, semantic) {
34
+ if (!DEV) return;
35
+ for (const key of Object.keys(props)) {
36
+ if (!isStyleProp(key) || semantic.has(key)) continue;
37
+ throw new Error(
38
+ `react-x11: <${kind} ${key}=…> is a style property — pass it in ` +
39
+ `style: <${kind} style={{ ${key}: … }} />`,
40
+ );
41
+ }
42
+ }
43
+
44
+ // Names an element owns as semantics, which therefore never mean style on
45
+ // it. `<window width>` is the X window's width; `<box width>` would be yoga
46
+ // style, and there is no element where a name means both.
47
+ const NO_SEMANTIC_NAMES = new Set();
48
+
49
+ /** A node's own style, installed onto `Node.prototype` by node.js. */
50
+ export class NodeStyling {
51
+ /**
52
+ * Everything that paints or lays out reads `this.style`, never `this.props`
53
+ * — props carry element semantics (`title`, `value`, geometry, handlers)
54
+ * and style carries the CSS-like vocabulary, with no name shared between
55
+ * them. `baseStyle` is the flattened `style` prop; `style` is that with
56
+ * the active state blocks overlaid.
57
+ */
58
+ _syncStyle(props, mounting = false) {
59
+ if (DEV && this.stylable) {
60
+ assertNoFlatStyleProps(props, this.kind, this.semanticNames);
61
+ validateStyle(flattenStyle(props.style), `<${this.kind} style>`);
62
+ }
63
+ this._baseStyle = this.stylable ? flattenStyle(props.style) : EMPTY_STYLE;
64
+ this._usesTokens = this.stylable && styleUsesTokens(this._baseStyle);
65
+ if (this._usesTokens) {
66
+ const theme = this.theme;
67
+ const strict = this.placed;
68
+ const problems = strict ? [] : null;
69
+ this._baseStyle = resolveTokens(
70
+ this._baseStyle,
71
+ theme,
72
+ `<${this.kind} style>`,
73
+ strict,
74
+ problems,
75
+ );
76
+ if (problems?.length) this._tokenProblem(problems, mounting);
77
+ }
78
+ // `disabled` is a prop, not something the pointer does, so it is read
79
+ // straight off props rather than driven by the event manager
80
+ this.states[':disabled'] = Boolean(props.disabled);
81
+ // window size queries fold into the base before state blocks, so a
82
+ // `:hover` inside the wide layout still wins over the wide layout
83
+ const kinds = queryKinds(this._baseStyle);
84
+ const queried = (kinds & QUERY_SIZE) !== 0;
85
+ if (queried !== this._queried) {
86
+ this._queried = queried;
87
+ const root = this.root;
88
+ if (root?._sizeQueryNodes) {
89
+ if (queried) root._sizeQueryNodes.add(this);
90
+ else root._sizeQueryNodes.delete(this);
91
+ }
92
+ }
93
+ // `@supports` blocks keep their own registry: what re-resolves them is
94
+ // the server's answer changing, not a resize
95
+ // Registered unconditionally rather than on change: a `<window>`'s own
96
+ // style is resolved by the Node constructor, before `root` is even
97
+ // assigned, so the first pass has nowhere to register and a "did it
98
+ // change" guard would keep it unregistered forever. Set.add is
99
+ // idempotent and these blocks are rare.
100
+ const asks = (kinds & QUERY_SUPPORTS) !== 0;
101
+ this._supportsQueried = asks;
102
+ if (this.root?._supportsQueryNodes) {
103
+ if (asks) this.root._supportsQueryNodes.add(this);
104
+ else this.root._supportsQueryNodes.delete(this);
105
+ }
106
+ // Attention candidates keep a registry for the same reason and are
107
+ // registered the same unconditional way — a `<window>`'s own style is
108
+ // resolved before `root` is assigned, so a "did it change" guard would
109
+ // leave it unregistered forever. Unlike hover, attention is *matched*
110
+ // against this set rather than hit-tested, so a node that is not in it
111
+ // is not a candidate at all.
112
+ const wantsAttention = Boolean(
113
+ props.unstable_onAttention || this._baseStyle[':attention'],
114
+ );
115
+ this._wantsAttention = wantsAttention;
116
+ if (this.root?._attentionNodes) {
117
+ if (wantsAttention) this.root._attentionNodes.add(this);
118
+ else this.root._attentionNodes.delete(this);
119
+ }
120
+ // `@container` blocks keep a third registry: what re-resolves them is a
121
+ // layout pass moving the container they ask about — neither a resize
122
+ // nor the server's answer. The record exists only on the nodes that
123
+ // ask, so every other node pays one bit test here and nothing below.
124
+ // Insertion registers through `_registerSizeQueries`; this is for a
125
+ // style that starts or stops asking on a node already in a window.
126
+ const asksContainers = (kinds & QUERY_CONTAINER) !== 0;
127
+ let containers = null;
128
+ if (asksContainers) {
129
+ const base = this._baseStyle;
130
+ let cq = this._cq;
131
+ if (cq === null || cq.style !== base) {
132
+ if (cq === null) {
133
+ cq = this._cq = {
134
+ style: base,
135
+ names: null,
136
+ containers: null,
137
+ answers: '',
138
+ pin: null,
139
+ warned: false,
140
+ };
141
+ } else {
142
+ // a different style asks different questions, and a pin held for
143
+ // the old one is not an answer to the new one
144
+ cq.style = base;
145
+ cq.pin = null;
146
+ }
147
+ cq.names = containerQueryNames(base);
148
+ }
149
+ this.root?._containerQueryNodes?.add(this);
150
+ containers = this._pinnedContainerSizes();
151
+ } else if (this._cq !== null) {
152
+ this._cq = null;
153
+ this.root?._containerQueryNodes?.delete(this);
154
+ }
155
+ if (queried || asks || asksContainers) {
156
+ this._baseStyle = resolveQueries(this._baseStyle, {
157
+ size: this.root?.querySize ?? null,
158
+ // null before the window is realized, which reads as "not
159
+ // supported" — the fallback design is the one that works everywhere
160
+ supports: this.root?.capabilities ?? null,
161
+ containers,
162
+ });
163
+ if (asksContainers) {
164
+ this._cq.containers = containers;
165
+ this._cq.answers = containerAnswers(this._baseStyle, containers);
166
+ }
167
+ }
168
+ this._stateful = hasStateStyles(this._baseStyle);
169
+ // The scale multiplies *after* every merge — state blocks, queries,
170
+ // the flex shorthand — so each of those keeps thinking in the logical
171
+ // pixels the app wrote, and device pixels exist only downstream of
172
+ // this line (src/scale.js).
173
+ return this._retarget(
174
+ scaleResolvedStyle(
175
+ resolveComputedStyle(
176
+ this._stateful
177
+ ? resolveStyleStates(this._baseStyle, this.states)
178
+ : this._baseStyle,
179
+ ),
180
+ this.scale,
181
+ ),
182
+ );
183
+ }
184
+
185
+ /**
186
+ * A node state changed (hover, focus, press). Only nodes that actually
187
+ * declare a block for it do anything, and what they do is a repaint —
188
+ * no React render, no reflow, since state blocks cannot touch layout.
189
+ */
190
+ setStyleState(name, on) {
191
+ if (this.states[name] === on) return;
192
+ this.states[name] = on;
193
+ // the focus ring's reach is read off `:focus-visible` whether or not
194
+ // the node has a state block of its own (`_outlineExtent`)
195
+ this._clearPaintBounds();
196
+ if (!this._stateful || this.destroyed) return;
197
+ const next = scaleResolvedStyle(
198
+ resolveComputedStyle(resolveStyleStates(this._baseStyle, this.states)),
199
+ this.scale,
200
+ );
201
+ if (shallowEqual(next, this._targetStyle)) return;
202
+ this._retarget(next);
203
+ // a state block may only set paint properties, so the node's own region
204
+ // is the whole of what changed
205
+ this.root?.invalidate(false, this, 'style-state');
206
+ }
207
+
208
+ /** Style names this element claims as its own semantics (see WindowNode).
209
+ * Registered elements declare theirs to `registerElement`, so the common
210
+ * case needs no subclass. */
211
+ get semanticNames() {
212
+ return CUSTOM_SEMANTIC_NAMES.get(this.kind) ?? NO_SEMANTIC_NAMES;
213
+ }
214
+
215
+ /**
216
+ * Whether this element is styled at all. The 3D scene elements and the
217
+ * declarative SVG children are not: they carry their own vocabularies —
218
+ * `position`, `color`, `width` mean a transform, a material and a radius
219
+ * there — so the style channel does not apply to them, the same way it
220
+ * does not apply to an `<input type>` in the DOM.
221
+ */
222
+ get stylable() {
223
+ return true;
224
+ }
225
+ }