@uniflowed/tui 0.0.0-alpha.18
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/capability.js +366 -0
- package/cells.js +332 -0
- package/components.js +602 -0
- package/diff.js +286 -0
- package/index.js +206 -0
- package/internal/hits.js +156 -0
- package/internal/host.js +928 -0
- package/internal/paint.js +664 -0
- package/internal/tree.js +373 -0
- package/keys.js +585 -0
- package/layout.js +833 -0
- package/mouse.js +333 -0
- package/package.json +36 -0
- package/selection.js +101 -0
- package/terminal.js +457 -0
- package/widths.js +201 -0
package/mouse.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// The mouse: what a terminal reports, and what a handler is given.
|
|
4
|
+
//
|
|
5
|
+
// A terminal says nothing about the mouse until it is asked to. The ask is a
|
|
6
|
+
// set of private modes (`terminal.js` writes them), and what comes back is an
|
|
7
|
+
// escape sequence in the middle of the same byte stream the keys arrive on —
|
|
8
|
+
// so this module is the grammar of that sequence, and `keys.js` is where the
|
|
9
|
+
// stream is split between the two.
|
|
10
|
+
//
|
|
11
|
+
// # SGR-1006, and only SGR-1006
|
|
12
|
+
//
|
|
13
|
+
// The original encoding (`ESC [ M` and three bytes) adds 32 to each coordinate
|
|
14
|
+
// so that they are printable, which stops working at column 223 — on a wide
|
|
15
|
+
// terminal it reports a position that is not where the pointer is. The 1006
|
|
16
|
+
// extension writes the numbers as decimal parameters and has no limit, and it
|
|
17
|
+
// also distinguishes a release from a press, which the original does not: a
|
|
18
|
+
// terminal in the old mode reports button 3 for *every* release, so an
|
|
19
|
+
// application cannot tell which button was let go.
|
|
20
|
+
//
|
|
21
|
+
// Both matter here, so this decoder accepts the extension only. A terminal
|
|
22
|
+
// that ignored `?1006h` reports the old form, and those bytes are recognised
|
|
23
|
+
// and dropped rather than decoded — see {@link legacyReportLength}. Dropping
|
|
24
|
+
// them is deliberate: decoding a position that is wrong past column 223 is
|
|
25
|
+
// worse than reporting no mouse at all, and letting the three raw bytes
|
|
26
|
+
// through would deliver them as keys, which is how a click on such a terminal
|
|
27
|
+
// types random characters into an `Input`.
|
|
28
|
+
//
|
|
29
|
+
// # Names are OpenTUI's, and so is the shape of the event
|
|
30
|
+
//
|
|
31
|
+
// `"down"`, `"up"`, `"move"`, `"drag"`, `"drag-end"`, `"drop"`, `"over"`,
|
|
32
|
+
// `"out"`, `"scroll"` — OpenTUI's nine `event.type` values, delivered to
|
|
33
|
+
// `onMouseDown`, `onMouseUp` and the rest of its handler names, bubbling from
|
|
34
|
+
// the node under the pointer up through its parents. A component written
|
|
35
|
+
// against OpenTUI's interaction page behaves the same way here.
|
|
36
|
+
//
|
|
37
|
+
// `preventDefault()` is here now, and it is worth saying what it prevents,
|
|
38
|
+
// because a method that suppressed nothing would be a promise rather than a
|
|
39
|
+
// method — which is why this module did not have one until there was a default
|
|
40
|
+
// to suppress. There is exactly one: a left press clears the selection and, if
|
|
41
|
+
// it landed on selectable text, starts a new one. A handler that calls
|
|
42
|
+
// `preventDefault()` on that `down` keeps the selection the reader already
|
|
43
|
+
// had and starts none, which is what a box that does its own thing with a
|
|
44
|
+
// drag — a slider, a canvas, a splitter — needs in order not to leave a
|
|
45
|
+
// highlight behind it. Nothing else this renderer does to a mouse event can be
|
|
46
|
+
// prevented, and no other event type has anything to prevent.
|
|
47
|
+
//
|
|
48
|
+
// It is not a severity of `stopPropagation()`, the same way it is not one for
|
|
49
|
+
// a key: one of them decides whether anybody else sees the event, the other
|
|
50
|
+
// decides whether the renderer acts on it.
|
|
51
|
+
|
|
52
|
+
/** OpenTUI's nine mouse event types. */
|
|
53
|
+
export type MouseEventType =
|
|
54
|
+
| "down"
|
|
55
|
+
| "up"
|
|
56
|
+
| "move"
|
|
57
|
+
| "drag"
|
|
58
|
+
| "drag-end"
|
|
59
|
+
| "drop"
|
|
60
|
+
| "over"
|
|
61
|
+
| "out"
|
|
62
|
+
| "scroll";
|
|
63
|
+
|
|
64
|
+
/** Which way a wheel turned. */
|
|
65
|
+
export type ScrollDirection = "up" | "down" | "left" | "right";
|
|
66
|
+
|
|
67
|
+
/** One turn of the wheel: which way, and how far. */
|
|
68
|
+
export type Scroll = {
|
|
69
|
+
readonly direction: ScrollDirection,
|
|
70
|
+
/** Notches. A terminal reports one report per notch, so this is always 1. */
|
|
71
|
+
readonly delta: number,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The three buttons a terminal can report, by the numbers it reports them as.
|
|
76
|
+
*
|
|
77
|
+
* A terminal has no notion of a fourth button or of a chord: the two low bits
|
|
78
|
+
* of its report hold one of these three and a fourth value meaning "none",
|
|
79
|
+
* which is why {@link MouseEvent.button} is nullable rather than being a
|
|
80
|
+
* fourth constant here.
|
|
81
|
+
*/
|
|
82
|
+
export const MouseButton: {
|
|
83
|
+
readonly LEFT: number,
|
|
84
|
+
readonly MIDDLE: number,
|
|
85
|
+
readonly RIGHT: number,
|
|
86
|
+
} = Object.freeze({ LEFT: 0, MIDDLE: 1, RIGHT: 2 });
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* One mouse report, as the handler on a node sees it.
|
|
90
|
+
*
|
|
91
|
+
* `x` and `y` are cells of the frame, counted from zero at the top left — the
|
|
92
|
+
* same coordinates layout writes onto a node, so a handler can compare them
|
|
93
|
+
* with a node's geometry without converting. The terminal counts from one and
|
|
94
|
+
* that is converted here, once.
|
|
95
|
+
*
|
|
96
|
+
* `target`, `currentTarget` and `source` are the `id` prop of a box rather
|
|
97
|
+
* than the box itself. This package has no public node type — the tree is
|
|
98
|
+
* `internal/tree.js` precisely so that nothing outside can hold a node across
|
|
99
|
+
* a commit and read stale geometry off it — so what an event can carry is the
|
|
100
|
+
* name the application gave the box. A box with no `id` reports `null`, which
|
|
101
|
+
* is the right answer for the common case: a handler already knows which node
|
|
102
|
+
* it is on, because it is its own closure.
|
|
103
|
+
*/
|
|
104
|
+
export type MouseEvent = {
|
|
105
|
+
/** Which of the two things a terminal's byte stream carries. */
|
|
106
|
+
readonly kind: "mouse",
|
|
107
|
+
readonly type: MouseEventType,
|
|
108
|
+
/**
|
|
109
|
+
* The button, as {@link MouseButton} names them, or `null`.
|
|
110
|
+
*
|
|
111
|
+
* `null` for a wheel report and for motion with nothing held down: both are
|
|
112
|
+
* encoded by the terminal as "no button", and reporting a left button for
|
|
113
|
+
* them would make `event.button === MouseButton.LEFT` true for a plain
|
|
114
|
+
* hover.
|
|
115
|
+
*/
|
|
116
|
+
readonly button: number | null,
|
|
117
|
+
/** The cell under the pointer, counted from zero. */
|
|
118
|
+
readonly x: number,
|
|
119
|
+
readonly y: number,
|
|
120
|
+
readonly ctrl: boolean,
|
|
121
|
+
readonly shift: boolean,
|
|
122
|
+
readonly meta: boolean,
|
|
123
|
+
/** The bytes this arrived as. */
|
|
124
|
+
readonly raw: string,
|
|
125
|
+
/** The wheel, on a `"scroll"` event, and `null` on every other. */
|
|
126
|
+
readonly scroll: Scroll | null,
|
|
127
|
+
/** The `id` of the topmost box under the pointer. */
|
|
128
|
+
target: string | null,
|
|
129
|
+
/** The `id` of the box whose handler is running; changes as it bubbles. */
|
|
130
|
+
currentTarget: string | null,
|
|
131
|
+
/**
|
|
132
|
+
* The `id` of the box a drag started on.
|
|
133
|
+
*
|
|
134
|
+
* Set on `"drag"`, `"drag-end"` and `"drop"`, and `null` otherwise. It is
|
|
135
|
+
* what makes a drop useful: the box that was dropped *on* receives the
|
|
136
|
+
* event, and this says what was dropped.
|
|
137
|
+
*/
|
|
138
|
+
source: string | null,
|
|
139
|
+
/** Stop the event reaching this node's ancestors. */
|
|
140
|
+
stopPropagation(): void,
|
|
141
|
+
/**
|
|
142
|
+
* Keep the renderer from doing its own thing with this event.
|
|
143
|
+
*
|
|
144
|
+
* On a left `"down"` that is the selection: the one the reader had is kept,
|
|
145
|
+
* and no new one begins under this press. Every other event type has no
|
|
146
|
+
* default, so calling this on one is harmless and does nothing.
|
|
147
|
+
*/
|
|
148
|
+
preventDefault(): void,
|
|
149
|
+
/** Whether `stopPropagation()` was called. */
|
|
150
|
+
propagationStopped: boolean,
|
|
151
|
+
/** Whether `preventDefault()` was called. */
|
|
152
|
+
defaultPrevented: boolean,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** The fields a decoded report carries before routing fills the rest in. */
|
|
156
|
+
type MouseFields = {
|
|
157
|
+
type: MouseEventType,
|
|
158
|
+
button: number | null,
|
|
159
|
+
x: number,
|
|
160
|
+
y: number,
|
|
161
|
+
ctrl: boolean,
|
|
162
|
+
shift: boolean,
|
|
163
|
+
meta: boolean,
|
|
164
|
+
raw: string,
|
|
165
|
+
scroll?: Scroll | null,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** Build a mouse event with its propagation flag wired up. */
|
|
169
|
+
export function mouseEvent(fields: MouseFields): MouseEvent {
|
|
170
|
+
const event: MouseEvent = {
|
|
171
|
+
kind: "mouse",
|
|
172
|
+
type: fields.type,
|
|
173
|
+
button: fields.button,
|
|
174
|
+
x: fields.x,
|
|
175
|
+
y: fields.y,
|
|
176
|
+
ctrl: fields.ctrl,
|
|
177
|
+
shift: fields.shift,
|
|
178
|
+
meta: fields.meta,
|
|
179
|
+
raw: fields.raw,
|
|
180
|
+
scroll: fields.scroll ?? null,
|
|
181
|
+
target: null,
|
|
182
|
+
currentTarget: null,
|
|
183
|
+
source: null,
|
|
184
|
+
propagationStopped: false,
|
|
185
|
+
defaultPrevented: false,
|
|
186
|
+
stopPropagation() {
|
|
187
|
+
event.propagationStopped = true;
|
|
188
|
+
},
|
|
189
|
+
preventDefault() {
|
|
190
|
+
event.defaultPrevented = true;
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
return event;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The same report again, as another type.
|
|
198
|
+
*
|
|
199
|
+
* `"over"` and `"out"` are not reported by a terminal; they are what the
|
|
200
|
+
* renderer says when the topmost node under the pointer changed, and they
|
|
201
|
+
* carry the position and modifiers of the report that moved it. Deriving them
|
|
202
|
+
* rather than synthesising a bare event is what keeps `event.ctrl` true for
|
|
203
|
+
* the `"over"` that a Ctrl-drag caused.
|
|
204
|
+
*/
|
|
205
|
+
export function derive(from: MouseEvent, type: MouseEventType): MouseEvent {
|
|
206
|
+
return mouseEvent({
|
|
207
|
+
type,
|
|
208
|
+
button: from.button,
|
|
209
|
+
x: from.x,
|
|
210
|
+
y: from.y,
|
|
211
|
+
ctrl: from.ctrl,
|
|
212
|
+
shift: from.shift,
|
|
213
|
+
meta: from.meta,
|
|
214
|
+
raw: from.raw,
|
|
215
|
+
scroll: from.scroll,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Which bit of a terminal's button byte means what. */
|
|
220
|
+
const SHIFT = 4;
|
|
221
|
+
const META = 8;
|
|
222
|
+
const CTRL = 16;
|
|
223
|
+
const MOTION = 32;
|
|
224
|
+
const WHEEL = 64;
|
|
225
|
+
/** The two low bits when no button is down. */
|
|
226
|
+
const NO_BUTTON = 3;
|
|
227
|
+
|
|
228
|
+
/** The wheel directions, in the order the two low bits name them. */
|
|
229
|
+
const WHEEL_DIRECTIONS: $ReadOnlyArray<ScrollDirection> = ["up", "down", "left", "right"];
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Decode one SGR-1006 report: `ESC [ < button ; column ; row M` or `… m`.
|
|
233
|
+
*
|
|
234
|
+
* `start` must be the `ESC`, and the caller must already have established that
|
|
235
|
+
* `[` and `<` follow. Returns `null` for anything that is not a complete
|
|
236
|
+
* report, which includes one cut in half by the end of a chunk — the caller
|
|
237
|
+
* then treats the bytes as it treats any other unfinished sequence.
|
|
238
|
+
*
|
|
239
|
+
* The final byte is the whole of the press/release distinction: `M` is a
|
|
240
|
+
* press or a motion, `m` is a release, and the button bits say which button
|
|
241
|
+
* in both cases. That is the half of this encoding the original does not
|
|
242
|
+
* have.
|
|
243
|
+
*/
|
|
244
|
+
export function decodeMouse(
|
|
245
|
+
input: string,
|
|
246
|
+
start: number,
|
|
247
|
+
): { event: MouseEvent, length: number } | null {
|
|
248
|
+
let cursor = start + 3;
|
|
249
|
+
let parameters = "";
|
|
250
|
+
while (cursor < input.length && /[0-9;]/.test(input[cursor])) {
|
|
251
|
+
parameters += input[cursor];
|
|
252
|
+
cursor += 1;
|
|
253
|
+
}
|
|
254
|
+
const final = input[cursor];
|
|
255
|
+
if (final !== "M" && final !== "m") {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const [rawCode, rawColumn, rawRow] = parameters.split(";");
|
|
259
|
+
const code = Number.parseInt(rawCode, 10);
|
|
260
|
+
const column = Number.parseInt(rawColumn, 10);
|
|
261
|
+
const row = Number.parseInt(rawRow, 10);
|
|
262
|
+
if (!Number.isFinite(code) || !Number.isFinite(column) || !Number.isFinite(row)) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const raw = input.slice(start, cursor + 1);
|
|
267
|
+
const modifiers = {
|
|
268
|
+
shift: (code & SHIFT) !== 0,
|
|
269
|
+
meta: (code & META) !== 0,
|
|
270
|
+
ctrl: (code & CTRL) !== 0,
|
|
271
|
+
};
|
|
272
|
+
// A terminal counts from one, this renderer counts from zero, and the
|
|
273
|
+
// conversion happens exactly here so that no handler ever has to know the
|
|
274
|
+
// terminal had a different opinion.
|
|
275
|
+
const x = Math.max(0, column - 1);
|
|
276
|
+
const y = Math.max(0, row - 1);
|
|
277
|
+
const held = code & NO_BUTTON;
|
|
278
|
+
|
|
279
|
+
if ((code & WHEEL) !== 0) {
|
|
280
|
+
return {
|
|
281
|
+
event: mouseEvent({
|
|
282
|
+
type: "scroll",
|
|
283
|
+
button: null,
|
|
284
|
+
x,
|
|
285
|
+
y,
|
|
286
|
+
...modifiers,
|
|
287
|
+
raw,
|
|
288
|
+
scroll: { direction: WHEEL_DIRECTIONS[held], delta: 1 },
|
|
289
|
+
}),
|
|
290
|
+
length: raw.length,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const button = held === NO_BUTTON ? null : held;
|
|
295
|
+
const type: MouseEventType =
|
|
296
|
+
final === "m" ? "up" : (code & MOTION) !== 0 ? (button == null ? "move" : "drag") : "down";
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
event: mouseEvent({ type, button, x, y, ...modifiers, raw }),
|
|
300
|
+
length: raw.length,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* How long an old-style `ESC [ M` report is: three introducer bytes and one
|
|
306
|
+
* each for the button, the column and the row.
|
|
307
|
+
*
|
|
308
|
+
* Named rather than written twice, because `keys.js` needs the same number to
|
|
309
|
+
* know when one of these has not all arrived yet.
|
|
310
|
+
*/
|
|
311
|
+
export const LEGACY_REPORT_LENGTH: number = 6;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* How many bytes an old-style `ESC [ M` report occupies, or zero.
|
|
315
|
+
*
|
|
316
|
+
* {@link LEGACY_REPORT_LENGTH} of them. The caller consumes them and emits
|
|
317
|
+
* nothing, which is the documented behaviour of this package on a terminal
|
|
318
|
+
* that does not implement SGR mouse reporting — see this module's header for
|
|
319
|
+
* why that is better than decoding them.
|
|
320
|
+
*
|
|
321
|
+
* Returns zero when fewer than that have arrived, so that a report split
|
|
322
|
+
* across two reads is not half-consumed. A decoder reading a stream holds
|
|
323
|
+
* those bytes until the rest of them arrive (`keys.js`, `incomplete`); the
|
|
324
|
+
* pure `decodeInput` has no later chunk to wait for and delivers three payload
|
|
325
|
+
* bytes as keys, which is the one case where this still leaks and needs a
|
|
326
|
+
* terminal without SGR *and* a caller that is not buffering.
|
|
327
|
+
*/
|
|
328
|
+
export function legacyReportLength(input: string, start: number): number {
|
|
329
|
+
if (input[start + 2] !== "M") {
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
return input.length - start >= LEGACY_REPORT_LENGTH ? LEGACY_REPORT_LENGTH : 0;
|
|
333
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uniflowed/tui",
|
|
3
|
+
"version": "0.0.0-alpha.18",
|
|
4
|
+
"description": "A React renderer whose host is a terminal, following OpenTUI, for the Unified Toolchain for Flow.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ubugeeei-prod/uf.git",
|
|
11
|
+
"directory": "packages/tui"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./index.js",
|
|
15
|
+
"./capability": "./capability.js",
|
|
16
|
+
"./cells": "./cells.js",
|
|
17
|
+
"./components": "./components.js",
|
|
18
|
+
"./diff": "./diff.js",
|
|
19
|
+
"./keys": "./keys.js",
|
|
20
|
+
"./layout": "./layout.js",
|
|
21
|
+
"./mouse": "./mouse.js",
|
|
22
|
+
"./selection": "./selection.js",
|
|
23
|
+
"./terminal": "./terminal.js",
|
|
24
|
+
"./widths": "./widths.js",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"*.js",
|
|
29
|
+
"internal/*.js",
|
|
30
|
+
"!*.test.js"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@uniflowed/react": "0.0.0-alpha.18",
|
|
34
|
+
"react-reconciler": "^0.33.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/selection.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// What a drag over the frame selected: two cells, and the order between them.
|
|
4
|
+
//
|
|
5
|
+
// A selection here is geometry and nothing else. It is not a range of
|
|
6
|
+
// characters inside a string, and it is not a pair of nodes with offsets into
|
|
7
|
+
// them — it is the cell the pointer went down on and the cell it has reached,
|
|
8
|
+
// in the coordinates every other part of this package already uses.
|
|
9
|
+
//
|
|
10
|
+
// # Why two points on the screen rather than an offset into the text
|
|
11
|
+
//
|
|
12
|
+
// The obvious model is the DOM's: a start node and an offset, an end node and
|
|
13
|
+
// an offset. It is the wrong one here for the same reason the hit test is a
|
|
14
|
+
// grid rather than a walk over the tree. What a reader is selecting is what
|
|
15
|
+
// they can *see*, and what they can see is the frame: a row scrolled out of a
|
|
16
|
+
// `ScrollBox` has last frame's geometry on it, `overflow: "hidden"` cut a
|
|
17
|
+
// child off somewhere its geometry does not admit to, and a box painted later
|
|
18
|
+
// erased the text under it. Two screen cells are true about the picture in
|
|
19
|
+
// front of the reader; a node and an offset are true about a tree that
|
|
20
|
+
// disagrees with it.
|
|
21
|
+
//
|
|
22
|
+
// It is also what a terminal's own selection is, which matters more than it
|
|
23
|
+
// looks: a reader who drags across this renderer's output and a reader who
|
|
24
|
+
// drags across `cat`'s output are performing the same gesture, and the second
|
|
25
|
+
// one has taught them what to expect from the first.
|
|
26
|
+
//
|
|
27
|
+
// The cost of the model is stated rather than hidden. A selection survives a
|
|
28
|
+
// re-render, because two cells are still two cells; if the content under them
|
|
29
|
+
// moved, the selection now covers whatever moved into those cells. That is
|
|
30
|
+
// again what a terminal does, and the alternative — dropping the selection on
|
|
31
|
+
// every commit — would drop it on the keystroke that scrolled the window a
|
|
32
|
+
// reader was selecting from.
|
|
33
|
+
//
|
|
34
|
+
// # Reading order, not a rectangle
|
|
35
|
+
//
|
|
36
|
+
// `start` and `end` are the two points sorted by row and then by column, which
|
|
37
|
+
// is the order text is read in and the order OpenTUI documents
|
|
38
|
+
// `getSelectedText()` as joining in: top to bottom, left to right. A selection
|
|
39
|
+
// from the middle of one line to the middle of the line below it therefore
|
|
40
|
+
// covers the end of the first line, and not a rectangle standing on the two
|
|
41
|
+
// columns. Column selection is a different gesture and this is not it.
|
|
42
|
+
|
|
43
|
+
/** One cell of the frame, counted from zero at the top left. */
|
|
44
|
+
export type SelectionPoint = {
|
|
45
|
+
readonly x: number,
|
|
46
|
+
readonly y: number,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One selection, as the renderer holds it.
|
|
51
|
+
*
|
|
52
|
+
* There is at most one per renderer, which is OpenTUI's rule and a terminal's:
|
|
53
|
+
* a second selection would have to be shown, and the frame has one way of
|
|
54
|
+
* showing a cell is selected.
|
|
55
|
+
*
|
|
56
|
+
* `anchor` and `focus` are the gesture — where the press landed and where the
|
|
57
|
+
* pointer has reached — and are kept because that is what an application
|
|
58
|
+
* asking "which way is this drag going" needs. `start` and `end` are the same
|
|
59
|
+
* two points in reading order, which is what everything that walks the
|
|
60
|
+
* selection needs, and they are stored rather than recomputed so that no two
|
|
61
|
+
* readers of one selection can sort it differently.
|
|
62
|
+
*/
|
|
63
|
+
export type Selection = {
|
|
64
|
+
/** The cell the press that began this selection landed on. */
|
|
65
|
+
readonly anchor: SelectionPoint,
|
|
66
|
+
/** The cell the pointer has reached. Moves as the drag continues. */
|
|
67
|
+
readonly focus: SelectionPoint,
|
|
68
|
+
/** The earlier of the two, by row and then by column. */
|
|
69
|
+
readonly start: SelectionPoint,
|
|
70
|
+
/** The later of the two. Both ends are *inside* the selection. */
|
|
71
|
+
readonly end: SelectionPoint,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Whether `a` is at or before `b` in reading order. */
|
|
75
|
+
function atOrBefore(a: SelectionPoint, b: SelectionPoint): boolean {
|
|
76
|
+
return a.y < b.y || (a.y === b.y && a.x <= b.x);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The selection a drag from `anchor` to `focus` describes.
|
|
81
|
+
*
|
|
82
|
+
* Both ends are inclusive: a press and a release on the same cell select that
|
|
83
|
+
* one cell rather than nothing. A reader who drags across a single character
|
|
84
|
+
* expects to have selected it, and a half-open range would make the shortest
|
|
85
|
+
* possible selection the empty one.
|
|
86
|
+
*/
|
|
87
|
+
export function selectionBetween(anchor: SelectionPoint, focus: SelectionPoint): Selection {
|
|
88
|
+
const forwards = atOrBefore(anchor, focus);
|
|
89
|
+
return {
|
|
90
|
+
anchor,
|
|
91
|
+
focus,
|
|
92
|
+
start: forwards ? anchor : focus,
|
|
93
|
+
end: forwards ? focus : anchor,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Whether a cell is inside a selection, in reading order. */
|
|
98
|
+
export function selectionContains(selection: Selection, x: number, y: number): boolean {
|
|
99
|
+
const point = { x, y };
|
|
100
|
+
return atOrBefore(selection.start, point) && atOrBefore(point, selection.end);
|
|
101
|
+
}
|