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.
- package/README.md +278 -129
- package/package.json +12 -4
- package/src/Reconciler.js +19 -31
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/appcontext.js +59 -30
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +303 -49
- package/src/cocoa/bezels.js +175 -30
- package/src/cocoa/dnd.js +27 -13
- package/src/cocoa/fonts.js +3 -3
- package/src/cocoa/glarea.js +24 -5
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/overlay.js +159 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +17 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/threaded.js +246 -0
- package/src/cocoa/window.js +256 -42
- package/src/components/Select.js +2 -2
- package/src/components/anchor.js +3 -3
- package/src/components/native.js +12 -7
- package/src/components/theme.js +2 -2
- package/src/debug.js +1 -1
- package/src/decorations.js +1 -1
- package/src/editmenu.js +2 -2
- package/src/embedding.js +31 -0
- package/src/errors.js +46 -0
- package/src/events.js +78 -18
- package/src/foreignnodes.js +59 -5
- package/src/frames.js +2 -2
- package/src/glnodes.js +172 -41
- package/src/gloverlay.js +383 -0
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -1
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +34 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +16 -3
- package/src/node.js +19 -21
- package/src/nodes/animation.js +644 -0
- package/src/nodes/box.js +21 -0
- package/src/nodes/boxpaint.js +473 -0
- package/src/nodes/canvas.js +269 -0
- package/src/nodes/cascade.js +600 -0
- package/src/nodes/damage.js +183 -0
- package/src/nodes/edithistory.js +124 -0
- package/src/nodes/editmenupopup.js +260 -0
- package/src/nodes/hittest.js +185 -0
- package/src/nodes/image.js +266 -0
- package/src/nodes/install.js +75 -0
- package/src/nodes/invalidate.js +465 -0
- package/src/nodes/kinds.js +31 -0
- package/src/nodes/layout.js +439 -0
- package/src/nodes/layouthost.js +949 -0
- package/src/nodes/node.js +868 -0
- package/src/nodes/paint.js +466 -0
- package/src/nodes/position.js +366 -0
- package/src/nodes/preedit.js +127 -0
- package/src/nodes/queries.js +330 -0
- package/src/nodes/rects.js +102 -0
- package/src/nodes/scrollable.js +891 -0
- package/src/nodes/scrollbars.js +138 -0
- package/src/nodes/scrollblit.js +1034 -0
- package/src/nodes/selectable.js +142 -0
- package/src/nodes/styling.js +225 -0
- package/src/nodes/text.js +649 -0
- package/src/nodes/textarea.js +391 -0
- package/src/nodes/textinput.js +1146 -0
- package/src/nodes/util.js +17 -0
- package/src/nodes/window/anchoring.js +161 -0
- package/src/nodes/window/capabilities.js +190 -0
- package/src/nodes/window/debugpaint.js +83 -0
- package/src/nodes/window/droptarget.js +145 -0
- package/src/nodes/window/floors.js +577 -0
- package/src/nodes/window/flush.js +369 -0
- package/src/nodes/window/hints.js +482 -0
- package/src/nodes/window/listeners.js +222 -0
- package/src/nodes/window/popup.js +71 -0
- package/src/nodes/window/size.js +591 -0
- package/src/nodes/window/window.js +954 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/styles.js +137 -15
- package/src/svgnodes.js +2 -1
- package/src/testing/harness.js +2 -2
- package/src/textselection.js +5 -3
- package/src/trace-registry.js +1 -1
- package/src/types/components.d.ts +38 -6
- package/src/types/elements.d.ts +26 -14
- package/src/types/nodes.d.ts +17 -2
- package/src/types/style.d.ts +94 -3
- package/src/windowstate.js +1 -1
- package/src/yoga.js +1 -1
- 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
|
+
}
|