@uniflowed/tui 0.0.0-alpha.18 → 0.15.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/capability.js +15 -12
- package/cells.js +1 -1
- package/clipboard.js +61 -0
- package/components.js +766 -57
- package/index.js +41 -22
- package/internal/hits.js +2 -2
- package/internal/host.js +63 -22
- package/internal/paint.js +43 -2
- package/internal/tree.js +168 -38
- package/internal/widgets.js +771 -0
- package/keys.js +214 -15
- package/layout.js +653 -132
- package/package.json +5 -4
- package/terminal.js +49 -19
- package/widths.js +4 -1
package/index.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// **Rust was rejected for three reasons, in order of weight.**
|
|
24
24
|
//
|
|
25
25
|
// *First, there is no bridge, and building one is a bigger and different
|
|
26
|
-
// project than this.* Every `nativeRuntimeRequired` in `
|
|
26
|
+
// project than this.* Every `nativeRuntimeRequired` in `npm/` is a
|
|
27
27
|
// promise of a JavaScript-to-native boundary that this repository does not
|
|
28
28
|
// have: no Node-API addon, no FFI, no prebuilt platform binaries, no loader.
|
|
29
29
|
// Choosing Rust here would have meant that the first deliverable was that
|
|
@@ -67,22 +67,25 @@
|
|
|
67
67
|
// # What is here, and what is not
|
|
68
68
|
//
|
|
69
69
|
// Implemented, tested, and true: a component tree, flexbox layout in whole
|
|
70
|
-
// cells
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
70
|
+
// cells — wrapping, `auto` margins, absolute positioning against the nearest
|
|
71
|
+
// positioned ancestor and `zIndex` included — a cell buffer with correct wide-grapheme handling, a diff that emits
|
|
72
|
+
// only changed cells, keyboard input with OpenTUI's key names, propagation
|
|
73
|
+
// rules and Kitty press/repeat/release event types, bracketed paste,
|
|
74
|
+
// declarative focus, mouse input — press, release, hover, drag with capture,
|
|
75
|
+
// drop and wheel, routed by a hit grid the painter records — text selection by
|
|
76
|
+
// drag, one per renderer, read back with
|
|
77
|
+
// `getSelectedText()`, a scrolling window onto content taller than it, a
|
|
78
|
+
// list, a row of tabs and a multi-line editor with OpenTUI's keys, terminal
|
|
79
|
+
// capability and *size* detection that agrees with the CLI's, and an
|
|
77
80
|
// in-memory renderer that runs the same code the terminal one does.
|
|
78
81
|
//
|
|
79
|
-
// Not here:
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// APIs" —
|
|
83
|
-
// string back
|
|
84
|
-
// ubugeeei-prod/uf#314, and they are absent rather than present as
|
|
85
|
-
// that throw — because a stub is what this package used to be.
|
|
82
|
+
// Not here: the repeated-press gestures that widen a selection to a word or a
|
|
83
|
+
// line, images, the rich content components, `Slider` and `ScrollBar`, and
|
|
84
|
+
// everything under OpenTUI's
|
|
85
|
+
// "application APIs" — except for the write-only OSC 52 clipboard — which is
|
|
86
|
+
// why `getSelectedText()` hands a string back and leaves policy to the caller.
|
|
87
|
+
// They are ubugeeei-prod/uf#314, and they are absent rather than present as
|
|
88
|
+
// functions that throw — because a stub is what this package used to be.
|
|
86
89
|
//
|
|
87
90
|
// Also not here, and worth saying because ubugeeei-prod/uf#247 asked for it:
|
|
88
91
|
// uf's own CLI does not draw through this. It cannot — `crates/uf_term` is
|
|
@@ -100,7 +103,7 @@
|
|
|
100
103
|
// and there are three guards rather than a promise. `capability.js` reproduces
|
|
101
104
|
// `crates/uf_term/src/capability.rs`'s colour precedence and its size
|
|
102
105
|
// precedence, `widths.js` holds the same Unicode tables as
|
|
103
|
-
// `crates/uf_term/src/text/tables.rs`, and `
|
|
106
|
+
// `crates/uf_term/src/text/tables.rs`, and `npm/tui/tui.test.js` reads
|
|
104
107
|
// both Rust files and fails when either side is edited alone.
|
|
105
108
|
//
|
|
106
109
|
// # How the package is laid out
|
|
@@ -117,13 +120,15 @@
|
|
|
117
120
|
// - `capability.js` — what this terminal can render and how big it is, by the
|
|
118
121
|
// CLI's own rules.
|
|
119
122
|
// - `terminal.js` — a real terminal, and the in-memory one tests use.
|
|
120
|
-
// - `components.js` — `Box`, `Text`, `Input`, `ScrollBox`,
|
|
123
|
+
// - `components.js` — `Box`, `Text`, `Input`, `ScrollBox`, `Select`,
|
|
124
|
+
// `TabSelect`, `Textarea`, and the hooks.
|
|
121
125
|
//
|
|
122
|
-
// `internal/` holds the
|
|
126
|
+
// `internal/` holds the five that a consumer must not be able to reach past:
|
|
123
127
|
// `tree.js` (props become a layout style once, here), `paint.js` (both passes
|
|
124
|
-
// must break lines the same way), `
|
|
125
|
-
//
|
|
126
|
-
//
|
|
128
|
+
// must break lines the same way), `widgets.js` (the same, for a textarea's
|
|
129
|
+
// cursor and its painter), `hits.js` (what is under the pointer is only true
|
|
130
|
+
// for the frame that recorded it) and `host.js` (one React root, one terminal,
|
|
131
|
+
// one owner). Each says so in its own header. There is no
|
|
127
132
|
// `internal/util.js`: a module that cannot say what it is about does not
|
|
128
133
|
// belong in this package.
|
|
129
134
|
|
|
@@ -151,21 +156,26 @@ export {
|
|
|
151
156
|
export type { Color, Frame, Rect, Style } from "./cells.js";
|
|
152
157
|
export { Attributes, INHERIT, frameRow, frameText, parseColor } from "./cells.js";
|
|
153
158
|
|
|
159
|
+
export type { Clipboard } from "./clipboard.js";
|
|
160
|
+
|
|
154
161
|
export type {
|
|
162
|
+
AlignContent,
|
|
155
163
|
AlignItems,
|
|
156
164
|
AlignSelf,
|
|
157
165
|
Dimension,
|
|
158
166
|
FlexDirection,
|
|
167
|
+
FlexWrap,
|
|
159
168
|
JustifyContent,
|
|
160
169
|
LayoutStyle,
|
|
161
170
|
Overflow,
|
|
171
|
+
Position,
|
|
162
172
|
} from "./layout.js";
|
|
163
173
|
|
|
164
174
|
export type { WrapMode } from "./internal/paint.js";
|
|
165
175
|
|
|
166
176
|
export type { Update } from "./diff.js";
|
|
167
177
|
|
|
168
|
-
export type { InputDecoder, InputEvent, KeyEvent, KeySource } from "./keys.js";
|
|
178
|
+
export type { InputDecoder, InputEvent, KeyEvent, KeyEventType, KeySource } from "./keys.js";
|
|
169
179
|
export { createInputDecoder, decodeInput, decodeKeys } from "./keys.js";
|
|
170
180
|
|
|
171
181
|
export type { MouseEvent, MouseEventType, Scroll, ScrollDirection } from "./mouse.js";
|
|
@@ -191,15 +201,24 @@ export type {
|
|
|
191
201
|
InputProps,
|
|
192
202
|
MouseProps,
|
|
193
203
|
ScrollBoxProps,
|
|
204
|
+
SelectColorProps,
|
|
205
|
+
SelectProps,
|
|
206
|
+
TabSelectProps,
|
|
194
207
|
TextProps,
|
|
195
208
|
TextStyleProps,
|
|
209
|
+
TextareaProps,
|
|
196
210
|
TitleAlignment,
|
|
197
211
|
} from "./components.js";
|
|
212
|
+
export type { EditWrapMode, SelectOption } from "./internal/widgets.js";
|
|
198
213
|
export {
|
|
199
214
|
Box,
|
|
200
215
|
Input,
|
|
201
216
|
ScrollBox,
|
|
217
|
+
Select,
|
|
218
|
+
TabSelect,
|
|
202
219
|
Text,
|
|
220
|
+
Textarea,
|
|
221
|
+
useClipboard,
|
|
203
222
|
useKeyboard,
|
|
204
223
|
useRenderer,
|
|
205
224
|
useTerminalSize,
|
package/internal/hits.js
CHANGED
|
@@ -74,8 +74,8 @@ export function createHitGrid(width: number, height: number): HitGrid {
|
|
|
74
74
|
return {
|
|
75
75
|
width,
|
|
76
76
|
height,
|
|
77
|
-
nodes: new Array(size).fill(null),
|
|
78
|
-
text: new Array(size).fill(null),
|
|
77
|
+
nodes: new Array<TuiNode | null>(size).fill(null),
|
|
78
|
+
text: new Array<TuiNode | null>(size).fill(null),
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
package/internal/host.js
CHANGED
|
@@ -62,22 +62,25 @@ import { DefaultEventPriority, DiscreteEventPriority } from "react-reconciler/co
|
|
|
62
62
|
import type { Capabilities } from "../capability.js";
|
|
63
63
|
import type { Frame, Rect } from "../cells.js";
|
|
64
64
|
import { createFrame } from "../cells.js";
|
|
65
|
+
import type { Clipboard } from "../clipboard.js";
|
|
66
|
+
import { unsupportedClipboard } from "../clipboard.js";
|
|
65
67
|
import type { Update } from "../diff.js";
|
|
66
68
|
import { diffFrames } from "../diff.js";
|
|
67
69
|
import type { KeyEvent } from "../keys.js";
|
|
68
70
|
import { layout } from "../layout.js";
|
|
69
|
-
import type { MouseEvent } from "../mouse.js";
|
|
71
|
+
import type { MouseEvent, MouseEventType } from "../mouse.js";
|
|
70
72
|
import { MouseButton, derive } from "../mouse.js";
|
|
71
73
|
import type { Selection, SelectionPoint } from "../selection.js";
|
|
72
74
|
import { selectionBetween } from "../selection.js";
|
|
73
75
|
import type { HitGrid } from "./hits.js";
|
|
74
76
|
import { createHitGrid, hitAt, textAt } from "./hits.js";
|
|
75
77
|
import { measureText, paint, paintSelection, selectionText, wrapModeOf } from "./paint.js";
|
|
76
|
-
import type { TuiNode, TuiProps } from "./tree.js";
|
|
78
|
+
import type { TuiNode, TuiProps, Widget } from "./tree.js";
|
|
77
79
|
import { applyProps, createNode, invalidate } from "./tree.js";
|
|
80
|
+
import { measureWidget } from "./widgets.js";
|
|
78
81
|
|
|
79
82
|
/** A global key handler, as `useKeyboard` registers one. */
|
|
80
|
-
export type KeyHandler = (key: KeyEvent) =>
|
|
83
|
+
export type KeyHandler = (key: KeyEvent) => mixed;
|
|
81
84
|
|
|
82
85
|
/**
|
|
83
86
|
* Everything one mounted application owns.
|
|
@@ -152,6 +155,15 @@ export type Renderer = {
|
|
|
152
155
|
hasSelection(): boolean,
|
|
153
156
|
clearSelection(): void,
|
|
154
157
|
getSelectedText(): string,
|
|
158
|
+
/**
|
|
159
|
+
* The terminal clipboard transport.
|
|
160
|
+
*
|
|
161
|
+
* It is installed by `terminal.js`, not by the reconciler. A renderer that
|
|
162
|
+
* lives in memory or behind a pipe has no terminal that can receive OSC 52,
|
|
163
|
+
* and the unsupported object says so explicitly instead of accepting text
|
|
164
|
+
* and dropping it.
|
|
165
|
+
*/
|
|
166
|
+
clipboard: Clipboard,
|
|
155
167
|
};
|
|
156
168
|
|
|
157
169
|
/**
|
|
@@ -206,6 +218,7 @@ export function createRenderer(
|
|
|
206
218
|
getSelectedText() {
|
|
207
219
|
return selectedText(renderer);
|
|
208
220
|
},
|
|
221
|
+
clipboard: unsupportedClipboard,
|
|
209
222
|
};
|
|
210
223
|
return renderer;
|
|
211
224
|
}
|
|
@@ -360,16 +373,18 @@ export function dispatchKey(renderer: Renderer, key: KeyEvent): void {
|
|
|
360
373
|
* page finds its handler called here. `onMouse` is not in this table because
|
|
361
374
|
* it is called for every type, after the specific one.
|
|
362
375
|
*/
|
|
363
|
-
const MOUSE_HANDLERS: {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
376
|
+
const MOUSE_HANDLERS: {
|
|
377
|
+
readonly [MouseEventType]: (props: TuiProps) => ?(event: MouseEvent) => mixed,
|
|
378
|
+
} = {
|
|
379
|
+
down: (props) => props.onMouseDown,
|
|
380
|
+
up: (props) => props.onMouseUp,
|
|
381
|
+
move: (props) => props.onMouseMove,
|
|
382
|
+
drag: (props) => props.onMouseDrag,
|
|
383
|
+
"drag-end": (props) => props.onMouseDragEnd,
|
|
384
|
+
drop: (props) => props.onMouseDrop,
|
|
385
|
+
over: (props) => props.onMouseOver,
|
|
386
|
+
out: (props) => props.onMouseOut,
|
|
387
|
+
scroll: (props) => props.onMouseScroll,
|
|
373
388
|
};
|
|
374
389
|
|
|
375
390
|
/** The `id` a box was given, which is the only name an event can carry. */
|
|
@@ -426,7 +441,7 @@ function bubble(
|
|
|
426
441
|
while (current != null) {
|
|
427
442
|
if (current.type === "box") {
|
|
428
443
|
event.currentTarget = nodeId(current);
|
|
429
|
-
const specific =
|
|
444
|
+
const specific = MOUSE_HANDLERS[event.type](current.props);
|
|
430
445
|
if (typeof specific === "function") {
|
|
431
446
|
specific(event);
|
|
432
447
|
}
|
|
@@ -606,6 +621,30 @@ let currentPriority: number = DefaultEventPriority;
|
|
|
606
621
|
|
|
607
622
|
const noop = () => {};
|
|
608
623
|
|
|
624
|
+
/** The host elements `components.js` creates for the three self-drawn widgets. */
|
|
625
|
+
const WIDGETS: { readonly [string]: Widget } = {
|
|
626
|
+
"uf-select": "select",
|
|
627
|
+
"uf-tab-select": "tab-select",
|
|
628
|
+
"uf-textarea": "textarea",
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Move or insert `child` before `before` among `parent`'s children, or at the
|
|
633
|
+
* end when `before` is not one of them.
|
|
634
|
+
*
|
|
635
|
+
* A function of its own because two host-config entries need it: the one React
|
|
636
|
+
* calls for a box and the one it calls for the root. Reaching it through
|
|
637
|
+
* `hostConfig` from inside `hostConfig` made the object's type depend on
|
|
638
|
+
* itself.
|
|
639
|
+
*/
|
|
640
|
+
function insertBefore(parent: TuiNode, child: TuiNode, before: TuiNode): void {
|
|
641
|
+
child.parent = parent;
|
|
642
|
+
invalidate(parent, 0);
|
|
643
|
+
remove(parent.children, child);
|
|
644
|
+
const at = parent.children.indexOf(before);
|
|
645
|
+
parent.children.splice(at < 0 ? parent.children.length : at, 0, child);
|
|
646
|
+
}
|
|
647
|
+
|
|
609
648
|
const hostConfig = {
|
|
610
649
|
supportsMutation: true,
|
|
611
650
|
supportsPersistence: false,
|
|
@@ -639,6 +678,14 @@ const hostConfig = {
|
|
|
639
678
|
|
|
640
679
|
createInstance: (type: string, props: TuiProps): TuiNode => {
|
|
641
680
|
const node = createNode(type === "uf-text" ? "text" : "box", props);
|
|
681
|
+
const widget = WIDGETS[type];
|
|
682
|
+
if (widget != null) {
|
|
683
|
+
// A box that draws itself. Its size is its own to report, the way a
|
|
684
|
+
// text node's is, and for the same reason: nothing inside it can.
|
|
685
|
+
node.widget = widget;
|
|
686
|
+
node.measure = (available: number) => measureWidget(node, widget, available);
|
|
687
|
+
invalidate(node);
|
|
688
|
+
}
|
|
642
689
|
if (node.type === "text") {
|
|
643
690
|
// A text node is the only leaf that knows its own size, and it only
|
|
644
691
|
// knows it once it is told how wide it may be. This is Yoga's measure
|
|
@@ -682,15 +729,9 @@ const hostConfig = {
|
|
|
682
729
|
invalidate(renderer.root, renderer.root.children.length);
|
|
683
730
|
renderer.root.children.push(child);
|
|
684
731
|
},
|
|
685
|
-
insertBefore
|
|
686
|
-
child.parent = parent;
|
|
687
|
-
invalidate(parent, 0);
|
|
688
|
-
remove(parent.children, child);
|
|
689
|
-
const at = parent.children.indexOf(before);
|
|
690
|
-
parent.children.splice(at < 0 ? parent.children.length : at, 0, child);
|
|
691
|
-
},
|
|
732
|
+
insertBefore,
|
|
692
733
|
insertInContainerBefore: (renderer: Renderer, child: TuiNode, before: TuiNode): void => {
|
|
693
|
-
|
|
734
|
+
insertBefore(renderer.root, child, before);
|
|
694
735
|
},
|
|
695
736
|
removeChild: (parent: TuiNode, child: TuiNode): void => {
|
|
696
737
|
invalidate(parent, 0);
|
package/internal/paint.js
CHANGED
|
@@ -50,6 +50,7 @@ import type { Selection } from "../selection.js";
|
|
|
50
50
|
import type { HitGrid } from "./hits.js";
|
|
51
51
|
import { recordHit, recordText } from "./hits.js";
|
|
52
52
|
import type { TuiNode } from "./tree.js";
|
|
53
|
+
import { paintWidget } from "./widgets.js";
|
|
53
54
|
import { ROOT_TEXT_STYLE, borderOf, textRuns, textStyleFromProps } from "./tree.js";
|
|
54
55
|
import type { Grapheme } from "../widths.js";
|
|
55
56
|
import { graphemes } from "../widths.js";
|
|
@@ -194,7 +195,7 @@ export function paint(
|
|
|
194
195
|
const inherited = selectableOf(node, selectable);
|
|
195
196
|
switch (node.type) {
|
|
196
197
|
case "root":
|
|
197
|
-
for (const child of node.children) {
|
|
198
|
+
for (const child of paintOrder(node.children)) {
|
|
198
199
|
paint(child, frame, capabilities, clip, hits, inherited);
|
|
199
200
|
}
|
|
200
201
|
return;
|
|
@@ -213,6 +214,38 @@ export function paint(
|
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
/** A node's `zIndex`, from either spelling; `0` when it has none. */
|
|
218
|
+
function zIndexOf(node: TuiNode): number {
|
|
219
|
+
const own = node.props.zIndex;
|
|
220
|
+
if (typeof own === "number" && Number.isFinite(own)) {
|
|
221
|
+
return own;
|
|
222
|
+
}
|
|
223
|
+
const style = node.props.style;
|
|
224
|
+
const nested = style != null && typeof style === "object" ? style.zIndex : undefined;
|
|
225
|
+
return typeof nested === "number" && Number.isFinite(nested) ? nested : 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The order siblings are painted in: by `zIndex`, and in tree order within one.
|
|
230
|
+
*
|
|
231
|
+
* Painting is destructive, so the order *is* the stacking: what is painted
|
|
232
|
+
* last is on top, both in the frame and in the hit grid, and a higher `zIndex`
|
|
233
|
+
* is painted later. It is OpenTUI's prop and its rule, and it matters as soon
|
|
234
|
+
* as `position: "absolute"` lets two siblings share cells. The sort is stable,
|
|
235
|
+
* so siblings that do not say keep the order the tree gives them, and it is
|
|
236
|
+
* skipped outright when none of them says anything — the common case pays for
|
|
237
|
+
* one look at each child and no array.
|
|
238
|
+
*/
|
|
239
|
+
function paintOrder(children: Array<TuiNode>): $ReadOnlyArray<TuiNode> {
|
|
240
|
+
if (!children.some((child) => zIndexOf(child) !== 0)) {
|
|
241
|
+
return children;
|
|
242
|
+
}
|
|
243
|
+
return children
|
|
244
|
+
.map((child, index) => ({ child, index, z: zIndexOf(child) }))
|
|
245
|
+
.sort((a, b) => a.z - b.z || a.index - b.index)
|
|
246
|
+
.map((entry) => entry.child);
|
|
247
|
+
}
|
|
248
|
+
|
|
216
249
|
/**
|
|
217
250
|
* Whether text under `node` may be selected.
|
|
218
251
|
*
|
|
@@ -254,6 +287,14 @@ function paintBox(
|
|
|
254
287
|
paintBorder(node, frame, capabilities, clip, border, background);
|
|
255
288
|
}
|
|
256
289
|
|
|
290
|
+
if (node.widget != null) {
|
|
291
|
+
// A `Select`, a `TabSelect` or a `Textarea`, which draws what it shows
|
|
292
|
+
// itself because what it shows depends on the size just laid out. It has
|
|
293
|
+
// no children to walk; `widgets.js` says why.
|
|
294
|
+
paintWidget(node, node.widget, frame, capabilities, clip, hits, selectable);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
257
298
|
// `overflow: "hidden"` clips children to what is inside the border and
|
|
258
299
|
// padding. `"visible"` — the default — lets them draw over the border,
|
|
259
300
|
// which is how a badge sits on a box's top edge. `"scroll"` clips like
|
|
@@ -281,7 +322,7 @@ function paintBox(
|
|
|
281
322
|
paint(node.children[index], frame, capabilities, childClip, hits, selectable);
|
|
282
323
|
}
|
|
283
324
|
} else {
|
|
284
|
-
for (const child of node.children) {
|
|
325
|
+
for (const child of paintOrder(node.children)) {
|
|
285
326
|
paint(child, frame, capabilities, childClip, hits, selectable);
|
|
286
327
|
}
|
|
287
328
|
}
|
package/internal/tree.js
CHANGED
|
@@ -34,10 +34,23 @@
|
|
|
34
34
|
// treats them as the same thing, so this does too. The direct prop wins when
|
|
35
35
|
// both are present, which is the rule a reader guesses.
|
|
36
36
|
|
|
37
|
-
import type {
|
|
37
|
+
import type {
|
|
38
|
+
AlignContent,
|
|
39
|
+
AlignItems,
|
|
40
|
+
AlignSelf,
|
|
41
|
+
FlexDirection,
|
|
42
|
+
FlexWrap,
|
|
43
|
+
JustifyContent,
|
|
44
|
+
LayoutStyle,
|
|
45
|
+
Overflow,
|
|
46
|
+
Position,
|
|
47
|
+
ScrollIndex,
|
|
48
|
+
} from "../layout.js";
|
|
38
49
|
import type { Color, Style } from "../cells.js";
|
|
39
50
|
import { Attributes, INHERIT, PLAIN, parseColor } from "../cells.js";
|
|
40
51
|
import type { BorderStyle } from "../capability.js";
|
|
52
|
+
import type { MouseProps } from "../components.js";
|
|
53
|
+
import type { KeyEvent } from "../keys.js";
|
|
41
54
|
|
|
42
55
|
/**
|
|
43
56
|
* What kind of node this is.
|
|
@@ -50,8 +63,24 @@ import type { BorderStyle } from "../capability.js";
|
|
|
50
63
|
*/
|
|
51
64
|
export type TuiNodeType = "root" | "box" | "text" | "chars";
|
|
52
65
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The three kinds of box that draw themselves: a `Select`, a `TabSelect` and a
|
|
68
|
+
* `Textarea`. `widgets.js` draws them and says why they are drawn rather than
|
|
69
|
+
* composed.
|
|
70
|
+
*/
|
|
71
|
+
export type Widget = "select" | "tab-select" | "textarea";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Anything a component put on a node. Read by `paint.js`, not by layout.
|
|
75
|
+
*
|
|
76
|
+
* The event handlers are named so the renderer can call them with the event
|
|
77
|
+
* `Box` declares for them; everything else is read and checked by name.
|
|
78
|
+
*/
|
|
79
|
+
export type TuiProps = {
|
|
80
|
+
...MouseProps,
|
|
81
|
+
readonly onKeyDown?: (key: KeyEvent) => mixed,
|
|
82
|
+
readonly [string]: mixed,
|
|
83
|
+
};
|
|
55
84
|
|
|
56
85
|
/** One node of the tree. Mutable: React owns its shape, layout owns its geometry. */
|
|
57
86
|
export type TuiNode = {
|
|
@@ -96,6 +125,18 @@ export type TuiNode = {
|
|
|
96
125
|
scrollDirtyFrom: number,
|
|
97
126
|
/** A scrolling box's stack of child heights; see `layout.js`. */
|
|
98
127
|
scrollIndex: ScrollIndex | null,
|
|
128
|
+
/**
|
|
129
|
+
* What this box draws in place of children, when it is a `Select`, a
|
|
130
|
+
* `TabSelect` or a `Textarea`; `null` for every other node. See
|
|
131
|
+
* `widgets.js`, which is also the only reader of the two fields below.
|
|
132
|
+
*/
|
|
133
|
+
widget: Widget | null,
|
|
134
|
+
/**
|
|
135
|
+
* The first line and column a `Textarea` shows, kept between frames so the
|
|
136
|
+
* window moves only as far as the cursor makes it. Written by the painter.
|
|
137
|
+
*/
|
|
138
|
+
viewTop: number,
|
|
139
|
+
viewLeft: number,
|
|
99
140
|
};
|
|
100
141
|
|
|
101
142
|
/** Read a prop, preferring the direct spelling over the one inside `style`. */
|
|
@@ -114,6 +155,9 @@ function prop(props: TuiProps, name: string): mixed {
|
|
|
114
155
|
const asNumber = (value: mixed): number | void =>
|
|
115
156
|
typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
116
157
|
|
|
158
|
+
const asMargin = (value: mixed): number | "auto" | void =>
|
|
159
|
+
value === "auto" ? "auto" : asNumber(value);
|
|
160
|
+
|
|
117
161
|
const asDimension = (value: mixed): number | string | void => {
|
|
118
162
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
119
163
|
return value;
|
|
@@ -141,6 +185,73 @@ export function borderOf(props: TuiProps): BorderStyle | null {
|
|
|
141
185
|
return prop(props, "border") === true ? "single" : null;
|
|
142
186
|
}
|
|
143
187
|
|
|
188
|
+
/** The style keys whose value is a cell count or a percentage. */
|
|
189
|
+
const DIMENSION_KEYS = [
|
|
190
|
+
"width",
|
|
191
|
+
"height",
|
|
192
|
+
"minWidth",
|
|
193
|
+
"minHeight",
|
|
194
|
+
"maxWidth",
|
|
195
|
+
"maxHeight",
|
|
196
|
+
"flexBasis",
|
|
197
|
+
"top",
|
|
198
|
+
"right",
|
|
199
|
+
"bottom",
|
|
200
|
+
"left",
|
|
201
|
+
] as const;
|
|
202
|
+
|
|
203
|
+
/** The style keys whose value is a plain number. */
|
|
204
|
+
const NUMBER_KEYS = [
|
|
205
|
+
"flexGrow",
|
|
206
|
+
"flexShrink",
|
|
207
|
+
"padding",
|
|
208
|
+
"paddingTop",
|
|
209
|
+
"paddingRight",
|
|
210
|
+
"paddingBottom",
|
|
211
|
+
"paddingLeft",
|
|
212
|
+
"gap",
|
|
213
|
+
"rowGap",
|
|
214
|
+
"columnGap",
|
|
215
|
+
"scrollTop",
|
|
216
|
+
] as const;
|
|
217
|
+
|
|
218
|
+
/** The style keys whose value is a margin: cells, or `"auto"`. */
|
|
219
|
+
const MARGIN_KEYS = ["margin", "marginTop", "marginRight", "marginBottom", "marginLeft"] as const;
|
|
220
|
+
|
|
221
|
+
const FLEX_DIRECTIONS: $ReadOnlyArray<FlexDirection> = [
|
|
222
|
+
"row",
|
|
223
|
+
"row-reverse",
|
|
224
|
+
"column",
|
|
225
|
+
"column-reverse",
|
|
226
|
+
];
|
|
227
|
+
const JUSTIFY_CONTENTS: $ReadOnlyArray<JustifyContent> = [
|
|
228
|
+
"flex-start",
|
|
229
|
+
"center",
|
|
230
|
+
"flex-end",
|
|
231
|
+
"space-between",
|
|
232
|
+
"space-around",
|
|
233
|
+
"space-evenly",
|
|
234
|
+
];
|
|
235
|
+
const ALIGN_ITEMS: $ReadOnlyArray<AlignItems> = ["flex-start", "center", "flex-end", "stretch"];
|
|
236
|
+
const ALIGN_SELVES: $ReadOnlyArray<AlignSelf> = ["auto", ...ALIGN_ITEMS];
|
|
237
|
+
const OVERFLOWS: $ReadOnlyArray<Overflow> = ["visible", "hidden", "scroll"];
|
|
238
|
+
const POSITIONS: $ReadOnlyArray<Position> = ["static", "relative", "absolute"];
|
|
239
|
+
const FLEX_WRAPS: $ReadOnlyArray<FlexWrap> = ["no-wrap", "wrap", "wrap-reverse"];
|
|
240
|
+
const ALIGN_CONTENTS: $ReadOnlyArray<AlignContent> = [
|
|
241
|
+
"flex-start",
|
|
242
|
+
"center",
|
|
243
|
+
"flex-end",
|
|
244
|
+
"stretch",
|
|
245
|
+
"space-between",
|
|
246
|
+
"space-around",
|
|
247
|
+
"space-evenly",
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
/** `value` when it is one of `words`, and `undefined` for anything else. */
|
|
251
|
+
function oneOf<T>(value: mixed, words: $ReadOnlyArray<T>): T | void {
|
|
252
|
+
return words.find((word) => word === value);
|
|
253
|
+
}
|
|
254
|
+
|
|
144
255
|
/**
|
|
145
256
|
* The layout style a node's props describe.
|
|
146
257
|
*
|
|
@@ -150,46 +261,62 @@ export function borderOf(props: TuiProps): BorderStyle | null {
|
|
|
150
261
|
* two places and make them disagree the first time one of them changed.
|
|
151
262
|
*/
|
|
152
263
|
export function styleFromProps(props: TuiProps): LayoutStyle {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
264
|
+
// Writable while it is being built, and the same keys and value types as the
|
|
265
|
+
// `LayoutStyle` it is returned as.
|
|
266
|
+
const style: { [K in $Keys<LayoutStyle>]?: LayoutStyle[K] } = {};
|
|
267
|
+
for (const name of DIMENSION_KEYS) {
|
|
268
|
+
const value = asDimension(prop(props, name));
|
|
156
269
|
if (value !== undefined) {
|
|
157
270
|
style[name] = value;
|
|
158
271
|
}
|
|
159
|
-
};
|
|
160
|
-
for (const name of [
|
|
161
|
-
"width",
|
|
162
|
-
"height",
|
|
163
|
-
"minWidth",
|
|
164
|
-
"minHeight",
|
|
165
|
-
"maxWidth",
|
|
166
|
-
"maxHeight",
|
|
167
|
-
"flexBasis",
|
|
168
|
-
]) {
|
|
169
|
-
copy(name, asDimension);
|
|
170
272
|
}
|
|
171
|
-
for (const name of
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
273
|
+
for (const name of NUMBER_KEYS) {
|
|
274
|
+
const value = asNumber(prop(props, name));
|
|
275
|
+
if (value !== undefined) {
|
|
276
|
+
style[name] = value;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const name of MARGIN_KEYS) {
|
|
280
|
+
const value = asMargin(prop(props, name));
|
|
281
|
+
if (value !== undefined) {
|
|
282
|
+
style[name] = value;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// The keyword props, each read against the words `layout.js` knows. A word
|
|
287
|
+
// it does not know is left unset, so layout's default applies — which is
|
|
288
|
+
// what a misspelt keyword already did there, now said here instead.
|
|
289
|
+
const flexDirection = oneOf(prop(props, "flexDirection"), FLEX_DIRECTIONS);
|
|
290
|
+
if (flexDirection !== undefined) {
|
|
291
|
+
style.flexDirection = flexDirection;
|
|
292
|
+
}
|
|
293
|
+
const justifyContent = oneOf(prop(props, "justifyContent"), JUSTIFY_CONTENTS);
|
|
294
|
+
if (justifyContent !== undefined) {
|
|
295
|
+
style.justifyContent = justifyContent;
|
|
296
|
+
}
|
|
297
|
+
const alignItems = oneOf(prop(props, "alignItems"), ALIGN_ITEMS);
|
|
298
|
+
if (alignItems !== undefined) {
|
|
299
|
+
style.alignItems = alignItems;
|
|
300
|
+
}
|
|
301
|
+
const alignSelf = oneOf(prop(props, "alignSelf"), ALIGN_SELVES);
|
|
302
|
+
if (alignSelf !== undefined) {
|
|
303
|
+
style.alignSelf = alignSelf;
|
|
304
|
+
}
|
|
305
|
+
const overflow = oneOf(prop(props, "overflow"), OVERFLOWS);
|
|
306
|
+
if (overflow !== undefined) {
|
|
307
|
+
style.overflow = overflow;
|
|
308
|
+
}
|
|
309
|
+
const position = oneOf(prop(props, "position"), POSITIONS);
|
|
310
|
+
if (position !== undefined) {
|
|
311
|
+
style.position = position;
|
|
312
|
+
}
|
|
313
|
+
const flexWrap = oneOf(prop(props, "flexWrap"), FLEX_WRAPS);
|
|
314
|
+
if (flexWrap !== undefined) {
|
|
315
|
+
style.flexWrap = flexWrap;
|
|
190
316
|
}
|
|
191
|
-
|
|
192
|
-
|
|
317
|
+
const alignContent = oneOf(prop(props, "alignContent"), ALIGN_CONTENTS);
|
|
318
|
+
if (alignContent !== undefined) {
|
|
319
|
+
style.alignContent = alignContent;
|
|
193
320
|
}
|
|
194
321
|
|
|
195
322
|
// `paddingX` and `paddingY`, which OpenTUI documents and which are the two
|
|
@@ -312,6 +439,9 @@ export function createNode(type: TuiNodeType, props: TuiProps): TuiNode {
|
|
|
312
439
|
measuredHeight: 0,
|
|
313
440
|
scrollDirtyFrom: 0,
|
|
314
441
|
scrollIndex: null,
|
|
442
|
+
widget: null,
|
|
443
|
+
viewTop: 0,
|
|
444
|
+
viewLeft: 0,
|
|
315
445
|
};
|
|
316
446
|
applyProps(node, props);
|
|
317
447
|
return node;
|