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,366 @@
1
+ // Positions (#534): a node laid out in flow and then moved by the scheme its
2
+ // `position` names — `sticky`, or one registered in layouts.js — against
3
+ // the scroll pane and the container it is measured from.
4
+
5
+ import { Yoga } from '../yoga.js';
6
+ import {
7
+ positionOf,
8
+ resolveOptions,
9
+ unknownPositionMessage,
10
+ } from '../layouts.js';
11
+ import { reportLayoutError, reportStyleProblem } from '../errors.js';
12
+ import { now } from './animation.js';
13
+ import { FULL_DAMAGE, addDamageRect } from './damage.js';
14
+ import { offsetInParent } from './layout.js';
15
+ import { intersectRects } from './rects.js';
16
+ import { BLIT_POISONED } from './scrollblit.js';
17
+
18
+ /** Node's half of positions, installed onto `Node.prototype` by node.js. */
19
+ export class NodePosition {
20
+ /**
21
+ * The scroll pane a placement is measured against: the nearest ancestor
22
+ * that scrolls — a `<box>` or a `<window>` with `overflow: 'scroll'`.
23
+ * Only that. `'hidden'` clips without scrolling here, which is CSS's
24
+ * `clip` rather than its `hidden`, so a card that rounds its corners does
25
+ * not quietly capture the sticky header inside it — the CSS trap of a
26
+ * sticky element that "does nothing". Null when nothing above scrolls.
27
+ */
28
+ _scrollPane() {
29
+ for (let n = this.parent; n; n = n.parent) {
30
+ if (n.isScroller?.()) return n;
31
+ if (n.isWindow) return null;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ /**
37
+ * Where layout put this node before anything shifted it: the origin its
38
+ * parent hands its children — scrolled, when the parent is a scroll pane
39
+ * — plus yoga's offset. The same sum `absolutize` makes, asked again so
40
+ * that nothing has to remember which shifts a rect already carries: the
41
+ * scroll fast path moves a placed node along with its pane's content
42
+ * (`_shiftAbs`), offset and all.
43
+ */
44
+ _laidOutAt() {
45
+ const parent = this.parent;
46
+ const origin = (parent.isScroller?.() && parent._childOrigin) || parent.abs;
47
+ const offset = offsetInParent(this);
48
+ return { x: origin.x + offset.x, y: origin.y + offset.y };
49
+ }
50
+
51
+ /**
52
+ * The position this node's style places it with — `sticky` is the
53
+ * built-in one — resolved: the definition, and its options with lengths in
54
+ * device pixels. Null for none, and for one that cannot be had, which is
55
+ * reported once and leaves the node where layout put it. Cached per style:
56
+ * the pass asks every frame, and the style only moves through `_retarget`.
57
+ */
58
+ _placement() {
59
+ const style = this.style;
60
+ const cache = this._placementCache;
61
+ if (cache !== null && cache.style === style) return cache.request;
62
+ const found = positionOf(style);
63
+ let request = null;
64
+ if (found !== null) {
65
+ if (!found.def) {
66
+ reportStyleProblem(
67
+ this,
68
+ unknownPositionMessage(found.name),
69
+ 'It is laid out in flow, as relative is',
70
+ );
71
+ } else {
72
+ const { options, problem } = resolveOptions(
73
+ found.def.options,
74
+ found.raw,
75
+ this.scale,
76
+ `<${this.kind} style={{ position: "${found.name}" }}>`,
77
+ );
78
+ if (problem) reportStyleProblem(this, problem, 'It takes its default');
79
+ request = { name: found.name, def: found.def, options };
80
+ }
81
+ }
82
+ // a placement that threw is not asked again until the style names
83
+ // another one
84
+ if (
85
+ this._placementFailed !== null &&
86
+ this._placementFailed !== request?.def
87
+ ) {
88
+ this._placementFailed = null;
89
+ }
90
+ this._placementCache = { style, request };
91
+ return request;
92
+ }
93
+
94
+ /**
95
+ * Move this node to where its placement puts it this frame — or, with no
96
+ * request, back to where layout has it, which is what a node whose style
97
+ * just stopped asking needs. The subtree rides along. Returns whether the
98
+ * placement asked for another frame.
99
+ *
100
+ * Nothing remembers an offset. The scroll fast path (#405) moves a placed
101
+ * node along with its pane's content, offset and all, so each pass
102
+ * re-derives where layout put it (`_laidOutAt`) and moves it from wherever
103
+ * it is now: a stored offset goes stale on exactly the frames that matter.
104
+ */
105
+ _place(request, t) {
106
+ const at = this._laidOutAt();
107
+ let dx = 0;
108
+ let dy = 0;
109
+ let again = false;
110
+ if (request !== null && this._placementFailed !== request.def) {
111
+ let result = null;
112
+ try {
113
+ result = request.def.place(
114
+ this,
115
+ this._placementContext(at, request.options, t),
116
+ );
117
+ } catch (error) {
118
+ this._placementFailed = request.def;
119
+ reportLayoutError(
120
+ this,
121
+ `position "${request.name}"`,
122
+ error,
123
+ 'The node stays where layout put it until its style names another ' +
124
+ 'position',
125
+ );
126
+ }
127
+ if (result) {
128
+ // whole pixels, which is what keeps a pane's scroll blit a copy
129
+ const x = Math.round(result.x ?? 0);
130
+ const y = Math.round(result.y ?? 0);
131
+ if (Number.isFinite(x) && Number.isFinite(y)) {
132
+ dx = x;
133
+ dy = y;
134
+ again = result.again === true;
135
+ } else {
136
+ reportStyleProblem(
137
+ this,
138
+ `react-x11: position "${request.name}" moved <${this.kind}> by ` +
139
+ `{ x: ${result.x}, y: ${result.y} } — an offset is two finite ` +
140
+ 'numbers, in device pixels',
141
+ 'The node stays where layout put it',
142
+ );
143
+ }
144
+ }
145
+ }
146
+ const mx = at.x + dx - this.abs.x;
147
+ const my = at.y + dy - this.abs.y;
148
+ if (mx !== 0 || my !== 0) {
149
+ this._shiftAbs(mx, my);
150
+ // every cached union above still counts this subtree where it was
151
+ this._clearHitBounds();
152
+ }
153
+ return again;
154
+ }
155
+
156
+ /**
157
+ * What a position's placement is handed (docs/extending.md, "A position
158
+ * of your own"): where layout put the node, the scroll pane above it and the
159
+ * box it has to stay inside — window coordinates, device pixels, the space
160
+ * `abs` is in — so a placement that holds a node against an edge adds and
161
+ * subtracts, and never converts.
162
+ */
163
+ _placementContext(at, options, t) {
164
+ const own = this.yoga;
165
+ const pane = this._scrollPane();
166
+ let scrolled = null;
167
+ if (pane) {
168
+ const py = pane.yoga;
169
+ const abs = pane.abs;
170
+ scrolled = {
171
+ // the band the content scrolls through: inside the border, over
172
+ // the padding
173
+ scrollport: {
174
+ left: abs.x + py.getComputedBorder(Yoga.EDGE_LEFT),
175
+ top: abs.y + py.getComputedBorder(Yoga.EDGE_TOP),
176
+ right: abs.x + abs.width - py.getComputedBorder(Yoga.EDGE_RIGHT),
177
+ bottom: abs.y + abs.height - py.getComputedBorder(Yoga.EDGE_BOTTOM),
178
+ },
179
+ scrollX: pane.scrollX,
180
+ scrollY: pane.scrollY,
181
+ };
182
+ }
183
+ return {
184
+ laidOut: {
185
+ x: at.x,
186
+ y: at.y,
187
+ width: this.abs.width,
188
+ height: this.abs.height,
189
+ },
190
+ pane: scrolled,
191
+ container: this._placementContainer(pane),
192
+ margin: {
193
+ left: own.getComputedMargin(Yoga.EDGE_LEFT),
194
+ top: own.getComputedMargin(Yoga.EDGE_TOP),
195
+ right: own.getComputedMargin(Yoga.EDGE_RIGHT),
196
+ bottom: own.getComputedMargin(Yoga.EDGE_BOTTOM),
197
+ },
198
+ direction: this.direction,
199
+ scale: this.scale,
200
+ now: t,
201
+ options,
202
+ };
203
+ }
204
+
205
+ /**
206
+ * The box a placed node is contained by: its parent's content box — or,
207
+ * when the parent is the scroll pane itself, the content the pane scrolls:
208
+ * at least the pane's own box, and as far as `measureScrollContent` found
209
+ * the children reaching — so a header that is a direct child of the pane
210
+ * sticks for the whole of its scroll. Window coordinates; the node's own
211
+ * margins are the placement's to apply.
212
+ */
213
+ _placementContainer(pane) {
214
+ const parent = this.parent;
215
+ const py = parent.yoga;
216
+ const inner = (edge) =>
217
+ py.getComputedBorder(edge) + py.getComputedPadding(edge);
218
+ if (parent === pane) {
219
+ const origin = pane._childOrigin ?? pane.abs;
220
+ const { width, height } = pane.abs;
221
+ const padLeft = py.getComputedPadding(Yoga.EDGE_LEFT);
222
+ const padRight = py.getComputedPadding(Yoga.EDGE_RIGHT);
223
+ const bottom =
224
+ Math.max(
225
+ height - py.getComputedBorder(Yoga.EDGE_BOTTOM),
226
+ pane.contentHeight,
227
+ ) - py.getComputedPadding(Yoga.EDGE_BOTTOM);
228
+ // `contentWidth` is measured from the edge the content starts at,
229
+ // which is the right-hand one under RTL (measureScrollContent)
230
+ const box =
231
+ pane.direction === 'rtl'
232
+ ? {
233
+ left:
234
+ origin.x +
235
+ padLeft +
236
+ Math.min(
237
+ py.getComputedBorder(Yoga.EDGE_LEFT),
238
+ width - pane.contentWidth,
239
+ ),
240
+ right: origin.x + width - inner(Yoga.EDGE_RIGHT),
241
+ }
242
+ : {
243
+ left: origin.x + inner(Yoga.EDGE_LEFT),
244
+ right:
245
+ origin.x +
246
+ Math.max(
247
+ width - py.getComputedBorder(Yoga.EDGE_RIGHT),
248
+ pane.contentWidth,
249
+ ) -
250
+ padRight,
251
+ };
252
+ box.top = origin.y + inner(Yoga.EDGE_TOP);
253
+ box.bottom = origin.y + bottom;
254
+ return box;
255
+ }
256
+ const abs = parent.abs;
257
+ return {
258
+ left: abs.x + inner(Yoga.EDGE_LEFT),
259
+ top: abs.y + inner(Yoga.EDGE_TOP),
260
+ right: abs.x + abs.width - inner(Yoga.EDGE_RIGHT),
261
+ bottom: abs.y + abs.height - inner(Yoga.EDGE_BOTTOM),
262
+ };
263
+ }
264
+ }
265
+
266
+ /** WindowNode's half of positions, installed onto `WindowNode.prototype` by window/window.js. */
267
+ export class WindowPosition {
268
+ /**
269
+ * Place every node whose `position` is placed after layout — `sticky` is
270
+ * the built-in one (docs/styling.md, "Custom positions"). Layout lays one
271
+ * out in flow; this is the other half, run once the pass has placed
272
+ * everything — including the pass a scroll runs, so a header lands in the
273
+ * frame that scrolled rather than the one after it — and on a frame with
274
+ * nothing to lay out when an animated placement asked for one
275
+ * (`_placementsDue`).
276
+ *
277
+ * Ancestors first: a placed node inside another rides the outer one's
278
+ * shift, and measures its own from where that left it.
279
+ *
280
+ * A shift here is a move nothing else claims — the layout diff has already
281
+ * run, and the scroll fast path moves a pane's content without one — so it
282
+ * claims its own pixels: where the node was shown and where it is going.
283
+ * "Where it was shown" is the last placement's reach, moved along by the
284
+ * blit if one is pending over it. A node that rode the scroll like its
285
+ * neighbours lands exactly there and claims nothing, which is what keeps
286
+ * a pane of headers on the blit path; a held one claims two header-sized
287
+ * rects, written into the blitting pane's ledger and clipped to it the
288
+ * way `_reflowed`'s claims are. They overlap by all but the scroll's
289
+ * delta, and the ledger folds them into one entry (`_recordBlitClaim`),
290
+ * with every held child's claims inside it. True when anything was
291
+ * claimed.
292
+ */
293
+ _placeNodes() {
294
+ this._placementsDue = false;
295
+ const nodes = [];
296
+ for (const node of this._placedNodes) {
297
+ if (
298
+ node.destroyed ||
299
+ node.root !== this ||
300
+ node.isWindow ||
301
+ !node.yoga ||
302
+ !node.parent
303
+ ) {
304
+ this._placedNodes.delete(node);
305
+ node._placedShown = null;
306
+ continue;
307
+ }
308
+ nodes.push(node);
309
+ }
310
+ if (nodes.length > 1) {
311
+ const depth = new Map();
312
+ for (const node of nodes) {
313
+ let d = 0;
314
+ for (let n = node.parent; n; n = n.parent) d++;
315
+ depth.set(node, d);
316
+ }
317
+ nodes.sort((a, b) => depth.get(a) - depth.get(b));
318
+ }
319
+ const cap = this._damageRectCap();
320
+ const t = now();
321
+ let claimed = false;
322
+ for (const node of nodes) {
323
+ const request = node._placement();
324
+ if (node._place(request, t)) this._placementsDue = true;
325
+ // one that stopped asking is back where layout has it: let it go
326
+ if (request === null) this._placedNodes.delete(node);
327
+ const before = node._placedShown;
328
+ const after =
329
+ request !== null && !node.hidden && node.style.display !== 'none'
330
+ ? node.paintBounds()
331
+ : null;
332
+ node._placedShown = after;
333
+ if (this._damage === FULL_DAMAGE) continue;
334
+ const sv = node._blitViewport();
335
+ let was = before;
336
+ if (was && sv) {
337
+ const shift = sv._blitShift();
338
+ was = { ...was, x: was.x + shift.x, y: was.y + shift.y };
339
+ }
340
+ if (
341
+ was &&
342
+ after &&
343
+ was.x === after.x &&
344
+ was.y === after.y &&
345
+ was.width === after.width &&
346
+ was.height === after.height
347
+ ) {
348
+ continue;
349
+ }
350
+ for (const rect of [was, after]) {
351
+ if (!rect) continue;
352
+ const claim = sv ? intersectRects(rect, sv.paintBounds()) : rect;
353
+ if (!claim) continue;
354
+ if (sv && !sv._recordBlitClaim(claim)) {
355
+ sv._pendingBlitFrom = BLIT_POISONED;
356
+ }
357
+ this._damage = addDamageRect(this._damage, claim, cap);
358
+ claimed = true;
359
+ }
360
+ }
361
+ // an animated placement's next frame, on the window's clock — asked from
362
+ // inside a flush, which answers it once this frame is over
363
+ if (this._placementsDue) this._scheduleFrame();
364
+ return claimed;
365
+ }
366
+ }
@@ -0,0 +1,127 @@
1
+ // IME composition for <textinput> and <textarea>: the preedit an input
2
+ // method shows at the caret before it commits, and the mapping between the
3
+ // value and what is displayed while one is open.
4
+
5
+ /** IME composition, installed onto `TextInputNode.prototype` by textinput.js. */
6
+ export class TextInputPreedit {
7
+ // --- composition ---------------------------------------------------------
8
+ //
9
+ // A composition is text the user is still typing: a dead key that has not
10
+ // met its letter yet, or a Compose sequence half entered (src/compose.js).
11
+ // It is shown at the caret and underlined, and it is deliberately *not*
12
+ // the value:
13
+ //
14
+ // - `value`, `onChange` and the undo history never see it, so a commit is
15
+ // one entry rather than one per keystroke, and Ctrl+Z after `dead_acute`
16
+ // + `e` steps over `é` rather than into it;
17
+ // - a **controlled** field whose parent rewrites `value` mid-composition
18
+ // cannot corrupt the buffer, because the buffer is not in the value.
19
+ // That is the classic web bug, and it is structurally absent here;
20
+ // - abandoning it — Escape, focus leaving, a press elsewhere — is
21
+ // dropping a string, not undoing an edit.
22
+ //
23
+ // The one thing it does change is the *displayed* string, which is what
24
+ // `_displayValue` is: everything measuring or hit-testing goes through
25
+ // that, and `_displayIndex` is the door between the two index spaces.
26
+
27
+ /** Where the composition sits in the value — clamped, because a
28
+ * controlled parent may have replaced the value under it. */
29
+ _preeditStart() {
30
+ return Math.min(this._preeditAt, this._chars().length);
31
+ }
32
+
33
+ /** The string the field draws — the value with any composition spliced in
34
+ * where it will land. */
35
+ _displayValue() {
36
+ if (!this._preedit) return this.value;
37
+ const chars = this._chars();
38
+ const at = this._preeditStart();
39
+ return (
40
+ chars.slice(0, at).join('') + this._preedit + chars.slice(at).join('')
41
+ );
42
+ }
43
+
44
+ /** A value index in the displayed string. The caret is at the composition
45
+ * while it is open, so it maps to the *end* of the preedit — which is
46
+ * where the next keystroke of the sequence appears. */
47
+ _displayIndex(index) {
48
+ if (!this._preedit) return index;
49
+ return index >= this._preeditStart()
50
+ ? index + Array.from(this._preedit).length
51
+ : index;
52
+ }
53
+
54
+ /** The inverse, for indices that come back out of a layout. A hit inside
55
+ * the preedit answers where the preedit starts: the composition is one
56
+ * thing, not a run of characters to put a caret between. */
57
+ _valueIndex(index) {
58
+ if (!this._preedit) return index;
59
+ const start = this._preeditStart();
60
+ if (index <= start) return index;
61
+ return Math.max(start, index - Array.from(this._preedit).length);
62
+ }
63
+
64
+ _setPreedit(text) {
65
+ if (text === this._preedit) return;
66
+ if (!this._preedit) this._preeditAt = this._selection()[0];
67
+ this._preedit = text;
68
+ this._repaint();
69
+ }
70
+
71
+ /**
72
+ * The composition default action, after `onCompositionStart` /
73
+ * `onCompositionUpdate` / `onCompositionEnd` have had their say.
74
+ *
75
+ * `compositionEnd` carries the text the sequence produced — empty when it
76
+ * was abandoned — and inserting it is an ordinary edit, so it replaces the
77
+ * selection, respects `maxLength`, fires `onChange` and joins the undo run
78
+ * the surrounding typing is in. `é` undoes like the letter it is.
79
+ */
80
+ defaultComposition(ev) {
81
+ if (ev.type !== 'compositionEnd') {
82
+ this._setPreedit(ev.data);
83
+ return;
84
+ }
85
+ this._setPreedit('');
86
+ if (!ev.data) return;
87
+ // the same bookkeeping `defaultKeyDown` does, so the `onChange` this
88
+ // produces carries the keystroke that committed the sequence
89
+ const previous = this._keyNative;
90
+ this._keyNative = ev.nativeEvent ?? null;
91
+ try {
92
+ this._insert(ev.data, 'type');
93
+ } finally {
94
+ this._keyNative = previous;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Underline the composition. The convention every toolkit shares, and the
100
+ * reason it is worth having: the accent showing at the caret is text the
101
+ * user has not typed yet, and nothing else about it says so — it is in the
102
+ * field's own ink, in the field's own font, where the next character will
103
+ * be. The line is what makes it provisional.
104
+ */
105
+ _paintPreedit(ctx, originX, originY, style) {
106
+ if (!this._preedit) return;
107
+ const layout = this._valueLayout();
108
+ if (!layout?.lines?.length) return;
109
+ const start = this._preeditStart();
110
+ const from = layout.caretPosition(start);
111
+ const to = layout.caretPosition(start + Array.from(this._preedit).length);
112
+ ctx.fillStyle = style.color;
113
+ // a span per line rather than one rectangle, because `<textarea>` shares
114
+ // this and a composition at a wrap point is two spans — drawn as one
115
+ // batch, so the underline costs one request however it wraps
116
+ const rects = [];
117
+ for (let li = from.line; li <= to.line; li++) {
118
+ const line = layout.lines[li];
119
+ if (!line) break;
120
+ const x0 = li === from.line ? from.x : line.x;
121
+ const x1 = li === to.line ? to.x : line.x + line.width;
122
+ if (x1 <= x0) continue;
123
+ rects.push(originX + x0, originY + line.y + line.ascent + 1, x1 - x0, 1);
124
+ }
125
+ if (rects.length) ctx.fillRects(rects);
126
+ }
127
+ }