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.
- package/README.md +278 -129
- package/package.json +10 -3
- package/src/Reconciler.js +15 -17
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +304 -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 +20 -3
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +4 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/screencolor.js +62 -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/errors.js +46 -0
- package/src/events.js +6 -6
- package/src/foreignnodes.js +3 -2
- package/src/frames.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -0
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +21 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +4 -2
- 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 +334 -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 +945 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/screencolor.js +212 -38
- package/src/screencolorhooks.js +6 -2
- 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 +11 -1
- package/src/types/nodes.d.ts +33 -5
- package/src/types/screencolor.d.ts +20 -14
- 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,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
|
+
}
|