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,391 @@
1
+ // <textarea>: the multi-line <textinput>, scrolling vertically with a bar of
2
+ // its own.
3
+
4
+ import {
5
+ XK_RETURN,
6
+ XK_KP_ENTER,
7
+ XK_HOME,
8
+ XK_UP,
9
+ XK_DOWN,
10
+ XK_PAGE_UP,
11
+ XK_PAGE_DOWN,
12
+ XK_END,
13
+ } from '../keysyms.js';
14
+ import {
15
+ scrollbarGeometry,
16
+ scrollbarHit,
17
+ paintScrollbarThumb,
18
+ } from './scrollbars.js';
19
+ import { rangeBands } from './text.js';
20
+ import { CARET_WIDTH, CARET_RESERVE, TextInputNode } from './textinput.js';
21
+
22
+ /**
23
+ * <textarea>: multi-line editable text on the same editing core as
24
+ * <textinput>. Word-wraps at the content width (ntk TextLayout), Enter
25
+ * inserts a newline (Ctrl+Enter fires onSubmit), Up/Down move the caret
26
+ * between visual lines keeping a goal column, Home/End are wrap-aware,
27
+ * selection spans lines, and the view scrolls vertically to follow the
28
+ * caret (wheel scrolls too). `rows` (default 3) sets the preferred height.
29
+ */
30
+ export class TextAreaNode extends TextInputNode {
31
+ /**
32
+ * The bar takes the press before the caret does — otherwise grabbing the
33
+ * thumb would drop the caret into whatever text sits behind it and start
34
+ * a selection drag.
35
+ */
36
+ defaultMouseDown(ev) {
37
+ const bar = this._scrollbar();
38
+ // device coordinates against device bar geometry, as in Scrollable
39
+ const nx = ev.nativeEvent?.x ?? ev.x * this.scale;
40
+ const ny = ev.nativeEvent?.y ?? ev.y * this.scale;
41
+ const hit = scrollbarHit(bar, nx, ny);
42
+ if (!hit) return super.defaultMouseDown(ev);
43
+ if (hit === 'thumb') {
44
+ this._barGrab = ny - bar.thumbStart;
45
+ ev.capturePointer();
46
+ return;
47
+ }
48
+ const page = this.contentBox().height;
49
+ this._scrollTo(this._scrollY + (ny < bar.thumbStart ? -page : page), bar);
50
+ }
51
+
52
+ defaultMouseDrag(ev) {
53
+ if (this._barGrab == null) return super.defaultMouseDrag(ev);
54
+ const bar = this._scrollbar();
55
+ if (!bar || bar.travel <= 0) return;
56
+ const ny = ev.nativeEvent?.y ?? ev.y * this.scale;
57
+ this._scrollTo(
58
+ ((ny - this._barGrab - bar.trackStart) / bar.travel) * bar.range,
59
+ bar,
60
+ );
61
+ }
62
+
63
+ defaultMouseUp(ev) {
64
+ if (this._barGrab != null) {
65
+ this._barGrab = null;
66
+ return;
67
+ }
68
+ super.defaultMouseUp(ev);
69
+ }
70
+
71
+ _scrollTo(y, bar) {
72
+ const next = Math.min(Math.max(0, y), bar.range);
73
+ if (next === this._scrollY) return;
74
+ this._scrollY = next;
75
+ // an inner scroll moves pixels only inside the field's own clip
76
+ this.root?.invalidate(false, this, 'scroll');
77
+ }
78
+
79
+ constructor(props, app) {
80
+ super(props, app, 'textarea');
81
+ this._scrollY = 0;
82
+ this._goalX = null;
83
+ }
84
+
85
+ /** Wider than a single-line field, and `rows` lines tall — a height that
86
+ * comes from a prop rather than from the style, which is what makes
87
+ * `invalidateMeasure` necessary below. */
88
+ measureContent({ width }) {
89
+ const rows = Math.max(1, this.props.rows ?? 3);
90
+ return {
91
+ width: Math.min(220, width),
92
+ height: Math.ceil(this._lineHeight() * rows),
93
+ };
94
+ }
95
+
96
+ /** Multi-line: preserve newlines (normalize CRLF). */
97
+ _normalizeInsert(text) {
98
+ return String(text).replace(/\r\n?/g, '\n');
99
+ }
100
+
101
+ /** Undo moves the caret, so the Up/Down goal column no longer applies. */
102
+ _applyHistory(value, caret, anchor) {
103
+ this._goalX = null;
104
+ super._applyHistory(value, caret, anchor);
105
+ }
106
+
107
+ /** Wrapped, styled layout of the value (or placeholder), cached per
108
+ * (text, style, width). Used for painting and all caret geometry, so
109
+ * caret math always agrees with what is on screen. */
110
+ _valueLayout() {
111
+ const fonts = this.app?.fonts;
112
+ if (!fonts) return null;
113
+ const text = this._displayValue();
114
+ const isEmpty = text.length === 0;
115
+ const shown = isEmpty ? (this.props.placeholder ?? '') : text;
116
+ const s = this.resolvedTextStyle();
117
+ const color = isEmpty
118
+ ? (this.props.placeholderColor ?? this.theme.textMuted)
119
+ : s.color;
120
+ const direction = this.direction;
121
+ const align = this.style.textAlign ?? 'start';
122
+ // A wrapped field *has* a container, so — unlike `<textinput>` — the
123
+ // alignment is ntk's to apply: every line is placed inside the box it
124
+ // wrapped to, and `start` is resolved against the base direction, which
125
+ // puts an RTL value against the right-hand edge.
126
+ //
127
+ // The container is the content box less the caret's own width, because
128
+ // in RTL every line *starts* flush against that right edge — line 0,
129
+ // column 0 of an empty field included — and a caret is drawn rightwards
130
+ // from the boundary it marks, so with no reserve it lands entirely
131
+ // outside the clip and an RTL textarea shows no caret at all. In LTR the
132
+ // flush edge is the left one, where the caret has the whole box in front
133
+ // of it, and only a line that fills its width exactly could reach the
134
+ // other end — so nothing is taken off a direction that does not need it.
135
+ const width = this.contentBox().width || undefined;
136
+ const container =
137
+ width === undefined || direction !== 'rtl'
138
+ ? width
139
+ : Math.max(0, width - CARET_RESERVE * this.scale);
140
+ const key = `${width}|${color}|${shown}|${s.family}|${s.size}|${s.weight}|${s.style}|${direction}|${align}`;
141
+ if (this._valueLayoutKey !== key) {
142
+ this._valueLayoutKey = key;
143
+ this._valueLayoutCache = fonts.layout([{ text: shown, ...s, color }], s, {
144
+ maxWidth: container,
145
+ align,
146
+ direction,
147
+ });
148
+ }
149
+ return this._valueLayoutCache;
150
+ }
151
+
152
+ applyProps(newProps, oldProps) {
153
+ const before = oldProps ?? this.props;
154
+ super.applyProps(newProps, oldProps);
155
+ if (newProps.rows !== before.rows) this.invalidateMeasure('props');
156
+ }
157
+
158
+ /** The wrapped value flows from the top of the content box, scrolled —
159
+ * there is no single line to centre, so none of `_lineMetrics` applies.
160
+ * Everything written against this (`textIndexAt`, `textCaretRect`,
161
+ * `textRangeRects`, click-to-caret) needs no override. */
162
+ _placedValue() {
163
+ const layout = this._valueLayout();
164
+ if (!layout) return null;
165
+ const content = this.contentBox();
166
+ return { layout, x: content.x, y: content.y - this._scrollY };
167
+ }
168
+
169
+ /** How far the wrapped text reaches past the viewport. The measurement a
170
+ * `Scrollable` takes off its children, taken off the layout that is
171
+ * actually painted — which is what makes a self-painting element a member
172
+ * of the scroll protocol rather than a special case in it. */
173
+ _maxScrollY() {
174
+ const layout = this._valueLayout();
175
+ if (!layout) return 0;
176
+ return Math.max(0, layout.height - this.contentBox().height);
177
+ }
178
+
179
+ /**
180
+ * The chain protocol's first half (issue #253). Vertical only — the text
181
+ * wraps, so there is never anything to the right — and false when the
182
+ * value fits, which is what lets the wheel chain outward to the pane or
183
+ * the window behind a short field instead of dying on it.
184
+ */
185
+ canScroll(dx, dy) {
186
+ return Boolean(dy) && this._maxScrollY() > 0;
187
+ }
188
+
189
+ /** `scrollBy(dy)`, or `scrollBy({x, y})` — the shape `Scrollable` takes,
190
+ * so the wheel's default action calls every scroller the same way. `x` is
191
+ * accepted and ignored: wrapped text has no horizontal extent. */
192
+ scrollBy(by) {
193
+ // logical, like Scrollable's — the public unit
194
+ const dy = typeof by === 'number' ? by : (by?.y ?? 0);
195
+ this._scrollByDevice(0, dy * this.scale);
196
+ }
197
+
198
+ /** Device-pixel core, the wheel's entry (see Scrollable). */
199
+ _scrollByDevice(dx, dy) {
200
+ if (!dy) return;
201
+ const next = Math.min(Math.max(0, this._scrollY + dy), this._maxScrollY());
202
+ if (next === this._scrollY) return;
203
+ this._scrollY = next;
204
+ // an inner scroll moves pixels only inside the field's own clip
205
+ this.root?.invalidate(false, this, 'scroll');
206
+ }
207
+
208
+ /** Visual lines that fit in the viewport — one Page keypress worth. */
209
+ _pageLines() {
210
+ const height = this.contentBox().height;
211
+ const line = this._lineHeight() || 1;
212
+ return Math.max(1, Math.floor(height / line));
213
+ }
214
+
215
+ /** Caret index on an adjacent visual line, keeping the goal column. */
216
+ _verticalMove(layout, delta) {
217
+ const pos = layout.caretPosition(this._caret);
218
+ const li = pos.line + delta;
219
+ if (li < 0) {
220
+ this._goalX = null;
221
+ return 0;
222
+ }
223
+ if (li >= layout.lines.length) {
224
+ this._goalX = null;
225
+ return this._chars().length;
226
+ }
227
+ const x = this._goalX ?? pos.x;
228
+ this._goalX = x;
229
+ const line = layout.lines[li];
230
+ return layout.indexAt(x, line.y + (line.ascent + line.descent) / 2);
231
+ }
232
+
233
+ _editKeyDown(ev) {
234
+ const k = ev.keysym;
235
+ const layout = this._valueLayout();
236
+
237
+ if (k === XK_RETURN || k === XK_KP_ENTER) {
238
+ if (ev.ctrlKey) {
239
+ this._fireValueEvent('onSubmit', this.value, ev.nativeEvent);
240
+ return true;
241
+ }
242
+ this._goalX = null;
243
+ this._insert('\n');
244
+ return true;
245
+ }
246
+ if ((k === XK_UP || k === XK_DOWN) && layout && this.value.length > 0) {
247
+ const i = this._verticalMove(layout, k === XK_UP ? -1 : 1);
248
+ this._moveCaret(i, ev.shiftKey);
249
+ if (ev.shiftKey) this._copySelection('PRIMARY');
250
+ return true;
251
+ }
252
+ if (
253
+ (k === XK_PAGE_UP || k === XK_PAGE_DOWN) &&
254
+ layout &&
255
+ this.value.length > 0
256
+ ) {
257
+ const i = this._verticalMove(
258
+ layout,
259
+ this._pageLines() * (k === XK_PAGE_UP ? -1 : 1),
260
+ );
261
+ this._moveCaret(i, ev.shiftKey);
262
+ if (ev.shiftKey) this._copySelection('PRIMARY');
263
+ return true;
264
+ }
265
+ if ((k === XK_HOME || k === XK_END) && layout && this.value.length > 0) {
266
+ const pos = layout.caretPosition(this._caret);
267
+ const line = layout.lines[pos.line];
268
+ const y = line.y + (line.ascent + line.descent) / 2;
269
+ // indexAt clamps into the line: far left = line start; just past the
270
+ // right edge = end of visible content (before the newline)
271
+ const i =
272
+ k === XK_HOME
273
+ ? layout.indexAt(-1e6, y)
274
+ : layout.indexAt(line.x + line.width + 0.01, y);
275
+ this._goalX = null;
276
+ this._moveCaret(i, ev.shiftKey);
277
+ return true;
278
+ }
279
+ this._goalX = null;
280
+ return super._editKeyDown(ev);
281
+ }
282
+
283
+ /** Thumb for the vertical overflow, same look as a scroll box's. */
284
+ _paintScrollbar(ctx, layout) {
285
+ const bar = this._scrollbar(layout);
286
+ if (bar) paintScrollbarThumb(ctx, bar, this.props.scrollbarColor);
287
+ }
288
+
289
+ _scrollbar(layout = this._valueLayout()) {
290
+ if (this.props.scrollbar === false || !layout) return null;
291
+ const box = this.contentBox();
292
+ return scrollbarGeometry({
293
+ axis: 'y',
294
+ start: box.y,
295
+ viewport: box.height,
296
+ content: layout.height,
297
+ across: box.x,
298
+ crossSize: box.width,
299
+ scroll: this._scrollY,
300
+ scale: this.scale,
301
+ });
302
+ }
303
+
304
+ paintContent(ctx) {
305
+ const layout = this._valueLayout();
306
+ if (!layout) return;
307
+ const content = this.contentBox();
308
+ if (content.width <= 0 || content.height <= 0) return;
309
+ const isEmpty = this._displayValue().length === 0;
310
+
311
+ // Keep the caret line inside the viewport, and only while focused — the
312
+ // caret starts at the end of the value, so chasing it unconditionally
313
+ // opened a textarea already scrolled past its first lines. Unlike
314
+ // <textinput> this does not reset the offset when focus leaves: a
315
+ // textarea scrolls on the wheel and has a scrollbar, so where an
316
+ // unfocused one is scrolled to is the reader's business.
317
+ const pos = layout.caretPosition(this._displayIndex(this._caret));
318
+ if (this._focused) {
319
+ if (pos.y + pos.height - this._scrollY > content.height) {
320
+ this._scrollY = pos.y + pos.height - content.height;
321
+ }
322
+ if (pos.y - this._scrollY < 0) {
323
+ this._scrollY = pos.y;
324
+ }
325
+ }
326
+ this._scrollY = Math.min(
327
+ this._scrollY,
328
+ Math.max(0, layout.height - content.height),
329
+ );
330
+ this._scrollY = Math.max(0, this._scrollY);
331
+
332
+ ctx.save();
333
+ ctx.beginPath();
334
+ ctx.rect(content.x, content.y, content.width, content.height);
335
+ ctx.clip();
336
+ const originX = content.x;
337
+ const originY = content.y - this._scrollY;
338
+
339
+ const [a, b] = this._selection();
340
+ if (this._showsSelection() && a !== b && !isEmpty) {
341
+ // A **translucent** accent rather than an opaque light blue. The ink
342
+ // on top is `style.color`, which this fill does not control, so an
343
+ // opaque highlight has to be picked to contrast with it — and no one
344
+ // colour does that on both a light and a dark palette. `#b3d4fc`
345
+ // under the dark palette's near-white ink is 1.3:1, which is nothing.
346
+ // Tinting the surface instead leaves the ink's own contrast intact.
347
+ ctx.fillStyle = this.props.selectionColor ?? this.theme.selection;
348
+ // The bands `textRangeRects` reports — one per line and one per
349
+ // direction run inside a line, which is what a selection crossing into
350
+ // an Arabic word actually covers. Two caret positions and a rectangle
351
+ // between them, which is what this was, paints over text nobody
352
+ // selected on a line that has runs going both ways.
353
+ //
354
+ // Only the lines on screen are *sent*, and all of them in one request.
355
+ // Ctrl+A in a 5000-line value selects 5000 lines and shows twenty: the
356
+ // clip throws the rest away *after* they have been sent, which is the
357
+ // one cost a clip cannot save.
358
+ const bottom = this._scrollY + content.height;
359
+ const rects = [];
360
+ for (const band of rangeBands(
361
+ layout,
362
+ this._displayValue(),
363
+ this._displayIndex(a),
364
+ this._displayIndex(b),
365
+ )) {
366
+ if (band.y > bottom) break;
367
+ if (band.y + band.height < this._scrollY) continue;
368
+ rects.push(originX + band.x, originY + band.y, band.width, band.height);
369
+ }
370
+ // one `Render.FillRectangles` for the whole highlight, where a fill per
371
+ // line is a full-surface-masked composite per line (ntk >= 7.6)
372
+ if (rects.length) ctx.fillRects(rects);
373
+ }
374
+
375
+ layout.draw(ctx, originX, originY);
376
+ this._paintPreedit(ctx, originX, originY, this.resolvedTextStyle());
377
+
378
+ if (this._focused && this._caretOn && a === b) {
379
+ ctx.fillStyle = this.props.caretColor ?? this.resolvedTextStyle().color;
380
+ ctx.fillRect(
381
+ originX + pos.x,
382
+ originY + pos.y,
383
+ CARET_WIDTH * this.scale,
384
+ pos.height,
385
+ );
386
+ }
387
+ // inside the clip, so the thumb is bounded by the content box
388
+ this._paintScrollbar(ctx, layout);
389
+ ctx.restore();
390
+ }
391
+ }