@react-x11/components 0.4.0 → 0.5.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 +125 -4
- package/dist/charts/components.d.ts +3 -1
- package/dist/charts/components.d.ts.map +1 -1
- package/dist/charts/components.js +8 -4
- package/dist/charts/components.js.map +1 -1
- package/dist/charts/node.d.ts +13 -6
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +75 -24
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +4 -0
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +11 -6
- package/dist/charts/render.js.map +1 -1
- package/dist/code-editor/node.d.ts +47 -16
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +121 -51
- package/dist/code-editor/node.js.map +1 -1
- package/dist/flow/draw.d.ts +19 -1
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +64 -35
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +24 -8
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/model.d.ts +4 -2
- package/dist/flow/model.d.ts.map +1 -1
- package/dist/flow/model.js +4 -2
- package/dist/flow/model.js.map +1 -1
- package/dist/flow/node.d.ts +40 -9
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +142 -53
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/types.d.ts +46 -21
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/formula/index.d.ts +4 -2
- package/dist/formula/index.d.ts.map +1 -1
- package/dist/formula/index.js.map +1 -1
- package/dist/formula/node.d.ts +5 -1
- package/dist/formula/node.d.ts.map +1 -1
- package/dist/formula/node.js +19 -3
- package/dist/formula/node.js.map +1 -1
- package/dist/html/controls.d.ts +4 -1
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +11 -5
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +18 -8
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +15 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +41 -16
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +3 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3 -1
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/values.d.ts +14 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +11 -8
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +1 -0
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +12 -5
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +7 -4
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +22 -1
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +22 -8
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +97 -15
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +3 -0
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +2 -2
- package/dist/html/paint.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/maps/clip.d.ts +43 -0
- package/dist/maps/clip.d.ts.map +1 -0
- package/dist/maps/clip.js +171 -0
- package/dist/maps/clip.js.map +1 -0
- package/dist/maps/gzip.d.ts +15 -0
- package/dist/maps/gzip.d.ts.map +1 -0
- package/dist/maps/gzip.js +56 -0
- package/dist/maps/gzip.js.map +1 -0
- package/dist/maps/index.d.ts +55 -0
- package/dist/maps/index.d.ts.map +1 -0
- package/dist/maps/index.js +141 -0
- package/dist/maps/index.js.map +1 -0
- package/dist/maps/labels.d.ts +123 -0
- package/dist/maps/labels.d.ts.map +1 -0
- package/dist/maps/labels.js +323 -0
- package/dist/maps/labels.js.map +1 -0
- package/dist/maps/mvt.d.ts +244 -0
- package/dist/maps/mvt.d.ts.map +1 -0
- package/dist/maps/mvt.js +820 -0
- package/dist/maps/mvt.js.map +1 -0
- package/dist/maps/node.d.ts +278 -0
- package/dist/maps/node.d.ts.map +1 -0
- package/dist/maps/node.js +1459 -0
- package/dist/maps/node.js.map +1 -0
- package/dist/maps/overlay.d.ts +157 -0
- package/dist/maps/overlay.d.ts.map +1 -0
- package/dist/maps/overlay.js +472 -0
- package/dist/maps/overlay.js.map +1 -0
- package/dist/maps/paint.d.ts +226 -0
- package/dist/maps/paint.d.ts.map +1 -0
- package/dist/maps/paint.js +705 -0
- package/dist/maps/paint.js.map +1 -0
- package/dist/maps/proj.d.ts +281 -0
- package/dist/maps/proj.d.ts.map +1 -0
- package/dist/maps/proj.js +451 -0
- package/dist/maps/proj.js.map +1 -0
- package/dist/maps/sources.d.ts +256 -0
- package/dist/maps/sources.d.ts.map +1 -0
- package/dist/maps/sources.js +210 -0
- package/dist/maps/sources.js.map +1 -0
- package/dist/maps/style.d.ts +141 -0
- package/dist/maps/style.d.ts.map +1 -0
- package/dist/maps/style.js +147 -0
- package/dist/maps/style.js.map +1 -0
- package/dist/maps/styles.d.ts +82 -0
- package/dist/maps/styles.d.ts.map +1 -0
- package/dist/maps/styles.js +898 -0
- package/dist/maps/styles.js.map +1 -0
- package/dist/maps/tiles.d.ts +283 -0
- package/dist/maps/tiles.d.ts.map +1 -0
- package/dist/maps/tiles.js +469 -0
- package/dist/maps/tiles.js.map +1 -0
- package/dist/maps/types.d.ts +244 -0
- package/dist/maps/types.d.ts.map +1 -0
- package/dist/maps/types.js +2 -0
- package/dist/maps/types.js.map +1 -0
- package/dist/reorder/hx.d.ts +18 -0
- package/dist/reorder/hx.d.ts.map +1 -0
- package/dist/reorder/hx.js +10 -0
- package/dist/reorder/hx.js.map +1 -0
- package/dist/reorder/index.d.ts +314 -0
- package/dist/reorder/index.d.ts.map +1 -0
- package/dist/reorder/index.js +1539 -0
- package/dist/reorder/index.js.map +1 -0
- package/dist/reorder/model.d.ts +107 -0
- package/dist/reorder/model.d.ts.map +1 -0
- package/dist/reorder/model.js +162 -0
- package/dist/reorder/model.js.map +1 -0
- package/dist/richtext/node.d.ts +15 -5
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +40 -11
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +35 -13
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +83 -27
- package/dist/richtext/runs.js.map +1 -1
- package/dist/tabs/index.d.ts +17 -0
- package/dist/tabs/index.d.ts.map +1 -1
- package/dist/tabs/index.js +849 -186
- package/dist/tabs/index.js.map +1 -1
- package/dist/terminal/vt/fonts.d.ts +45 -15
- package/dist/terminal/vt/fonts.d.ts.map +1 -1
- package/dist/terminal/vt/fonts.js +89 -19
- package/dist/terminal/vt/fonts.js.map +1 -1
- package/dist/terminal/vt/node.d.ts +42 -0
- package/dist/terminal/vt/node.d.ts.map +1 -1
- package/dist/terminal/vt/node.js +95 -11
- package/dist/terminal/vt/node.js.map +1 -1
- package/dist/terminal/vt/renderer.d.ts.map +1 -1
- package/dist/terminal/vt/renderer.js +13 -3
- package/dist/terminal/vt/renderer.js.map +1 -1
- package/package.json +13 -3
- package/src/charts/components.ts +14 -7
- package/src/charts/node.ts +81 -26
- package/src/charts/render.ts +16 -6
- package/src/code-editor/node.ts +147 -53
- package/src/flow/draw.ts +104 -35
- package/src/flow/index.ts +33 -13
- package/src/flow/model.ts +4 -2
- package/src/flow/node.ts +158 -59
- package/src/flow/types.ts +46 -21
- package/src/formula/index.ts +4 -2
- package/src/formula/node.ts +24 -5
- package/src/html/controls.ts +4 -1
- package/src/html/css/cascade.ts +18 -7
- package/src/html/css/parse.ts +46 -21
- package/src/html/css/style.ts +6 -1
- package/src/html/css/values.ts +25 -9
- package/src/html/index.ts +1 -0
- package/src/html/layout/boxes.ts +19 -9
- package/src/html/layout/inline.ts +23 -1
- package/src/html/node.ts +110 -17
- package/src/html/paint.ts +17 -2
- package/src/index.ts +98 -0
- package/src/maps/clip.ts +184 -0
- package/src/maps/gzip.ts +66 -0
- package/src/maps/index.ts +316 -0
- package/src/maps/labels.ts +442 -0
- package/src/maps/mvt.ts +857 -0
- package/src/maps/node.ts +1819 -0
- package/src/maps/overlay.ts +639 -0
- package/src/maps/paint.ts +978 -0
- package/src/maps/proj.ts +641 -0
- package/src/maps/sources.ts +425 -0
- package/src/maps/style.ts +309 -0
- package/src/maps/styles.ts +1040 -0
- package/src/maps/tiles.ts +676 -0
- package/src/maps/types.ts +260 -0
- package/src/reorder/hx.ts +31 -0
- package/src/reorder/index.ts +2223 -0
- package/src/reorder/model.ts +232 -0
- package/src/richtext/node.ts +49 -14
- package/src/richtext/runs.ts +115 -30
- package/src/tabs/index.ts +1040 -204
- package/src/terminal/vt/fonts.ts +128 -36
- package/src/terminal/vt/node.ts +105 -12
- package/src/terminal/vt/renderer.ts +13 -3
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// The insertion arithmetic under `<ReorderList>`: which slot a pointer is
|
|
2
|
+
// over, and what a move to that slot does to an order. Pure — no React, no
|
|
3
|
+
// display — so every case is asserted on no server (`test/reorder-model.test.ts`),
|
|
4
|
+
// and so a second consumer (a reorderable `<Table>`, say) can take it
|
|
5
|
+
// without taking the component. That promotion is the reason this is its
|
|
6
|
+
// own module rather than a section of `index.ts`.
|
|
7
|
+
//
|
|
8
|
+
// The model is pragmatic-drag-and-drop's **closest edge**: the item nearest
|
|
9
|
+
// the pointer, and which half of it — along the list's axis — the pointer is
|
|
10
|
+
// in. It needs nothing but the items' own rectangles, so it is the same rule
|
|
11
|
+
// for a vertical list, a horizontal strip, an RTL one and a wrapping grid,
|
|
12
|
+
// and it never has to know the gap between two items to answer for a point
|
|
13
|
+
// in it (`docs/prd-reorder.md`, "Geometry: the closest edge").
|
|
14
|
+
|
|
15
|
+
export type ReorderId = string | number;
|
|
16
|
+
export type ReorderOrientation = 'vertical' | 'horizontal';
|
|
17
|
+
/** Which edge of an item the pointer is nearest — in reading order, so
|
|
18
|
+
* `'before'` is the top of a vertical list and the *right* edge of an RTL
|
|
19
|
+
* strip. */
|
|
20
|
+
export type ReorderEdge = 'before' | 'after';
|
|
21
|
+
|
|
22
|
+
/** A rectangle in any one consistent space — the component hands over
|
|
23
|
+
* logical pixels, and the tests hand over whatever they like. */
|
|
24
|
+
export interface ReorderRect {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ReorderPoint {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Where a pointer would insert: the slot (`0` … `n`, a gap between items),
|
|
37
|
+
* and the item and edge that slot was read off, for whoever draws it. */
|
|
38
|
+
export interface ReorderSlot {
|
|
39
|
+
slot: number;
|
|
40
|
+
index: number;
|
|
41
|
+
edge: ReorderEdge;
|
|
42
|
+
/**
|
|
43
|
+
* The pointer is inside the item's middle band and combining is on, so a
|
|
44
|
+
* drop merges *into* `index` rather than landing beside it. Always false
|
|
45
|
+
* when the caller passes no band, which is what keeps a list that does not
|
|
46
|
+
* combine reading exactly as it did before.
|
|
47
|
+
*/
|
|
48
|
+
combine: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Squared distance from a point to a rectangle; zero inside it. */
|
|
52
|
+
function distance2(rect: ReorderRect, p: ReorderPoint): number {
|
|
53
|
+
const dx = Math.max(rect.x - p.x, 0, p.x - (rect.x + rect.width));
|
|
54
|
+
const dy = Math.max(rect.y - p.y, 0, p.y - (rect.y + rect.height));
|
|
55
|
+
return dx * dx + dy * dy;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The slot a pointer is over, among items laid out in `rects`' order.
|
|
60
|
+
*
|
|
61
|
+
* The nearest item wins — the one under the pointer when there is one, and
|
|
62
|
+
* on a tie the earlier — and its midpoint along the axis decides the edge.
|
|
63
|
+
* `null` only when there are no items, where the caller's answer is slot 0.
|
|
64
|
+
*
|
|
65
|
+
* `combineBand` is the fraction of the item's length at *each* end that
|
|
66
|
+
* still reads as an edge: 0 (the default) means every point picks an edge,
|
|
67
|
+
* and 0.25 means the middle half of an item reads as "merge into this one"
|
|
68
|
+
* — but only for a point genuinely inside it, since a point in the gap
|
|
69
|
+
* between two items is not over either of them.
|
|
70
|
+
*/
|
|
71
|
+
export function closestSlot(
|
|
72
|
+
rects: readonly ReorderRect[],
|
|
73
|
+
point: ReorderPoint,
|
|
74
|
+
orientation: ReorderOrientation,
|
|
75
|
+
direction: 'ltr' | 'rtl' = 'ltr',
|
|
76
|
+
combineBand = 0,
|
|
77
|
+
): ReorderSlot | null {
|
|
78
|
+
if (rects.length === 0) return null;
|
|
79
|
+
let index = 0;
|
|
80
|
+
let best = Infinity;
|
|
81
|
+
for (let i = 0; i < rects.length; i++) {
|
|
82
|
+
const d = distance2(rects[i]!, point);
|
|
83
|
+
if (d < best) {
|
|
84
|
+
best = d;
|
|
85
|
+
index = i;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const rect = rects[index]!;
|
|
89
|
+
const vertical = orientation === 'vertical';
|
|
90
|
+
const start = vertical ? rect.y : rect.x;
|
|
91
|
+
const length = vertical ? rect.height : rect.width;
|
|
92
|
+
const along = vertical ? point.y : point.x;
|
|
93
|
+
let before = along < start + length / 2;
|
|
94
|
+
// the start edge of an RTL strip is on the right
|
|
95
|
+
if (!vertical && direction === 'rtl') before = !before;
|
|
96
|
+
const edge: ReorderEdge = before ? 'before' : 'after';
|
|
97
|
+
// `best === 0` is the point being inside the rectangle: a band is about
|
|
98
|
+
// the item under the pointer, and a gap belongs to neither of its
|
|
99
|
+
// neighbours' middles.
|
|
100
|
+
const fraction = length > 0 ? (along - start) / length : 0.5;
|
|
101
|
+
const combine =
|
|
102
|
+
combineBand > 0 &&
|
|
103
|
+
best === 0 &&
|
|
104
|
+
fraction >= combineBand &&
|
|
105
|
+
fraction <= 1 - combineBand;
|
|
106
|
+
return { slot: before ? index : index + 1, index, edge, combine };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Where the indicator goes for a slot: the **one** place that names it.
|
|
111
|
+
*
|
|
112
|
+
* `closestSlot` answers with the item the pointer is nearest and the half of
|
|
113
|
+
* it the pointer is in, and two different answers can mean the same gap —
|
|
114
|
+
* the lower half of item 2 and the upper half of item 3 are both "between
|
|
115
|
+
* them". Drawing the mark from that answer puts the line in two places for
|
|
116
|
+
* one insertion point, which reads as two, and flickers between them as the
|
|
117
|
+
* pointer crosses. So the mark is derived from the slot instead: the start
|
|
118
|
+
* edge of the item that would follow it, or the end edge of the last item
|
|
119
|
+
* for the slot past the end.
|
|
120
|
+
*/
|
|
121
|
+
export function slotMark(
|
|
122
|
+
count: number,
|
|
123
|
+
slot: number,
|
|
124
|
+
): { index: number; edge: ReorderEdge } | null {
|
|
125
|
+
if (count <= 0) return null;
|
|
126
|
+
const at = Math.min(Math.max(slot, 0), count);
|
|
127
|
+
return at < count
|
|
128
|
+
? { index: at, edge: 'before' }
|
|
129
|
+
: { index: count - 1, edge: 'after' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `list` with the item at `from` moved to `to` — a new array, always, so it
|
|
134
|
+
* can be handed straight to a state setter. Indices past either end clamp.
|
|
135
|
+
* dnd-kit's helper, by its name, because it is what every `onReorder`
|
|
136
|
+
* handler ends up calling.
|
|
137
|
+
*/
|
|
138
|
+
export function arrayMove<T>(
|
|
139
|
+
list: readonly T[],
|
|
140
|
+
from: number,
|
|
141
|
+
to: number,
|
|
142
|
+
): T[] {
|
|
143
|
+
const out = list.slice();
|
|
144
|
+
if (out.length === 0) return out;
|
|
145
|
+
const a = Math.min(Math.max(from, 0), out.length - 1);
|
|
146
|
+
const b = Math.min(Math.max(to, 0), out.length - 1);
|
|
147
|
+
if (a === b) return out;
|
|
148
|
+
const [item] = out.splice(a, 1);
|
|
149
|
+
out.splice(b, 0, item as T);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Whether dropping the item at `from` into `slot` would change nothing:
|
|
154
|
+
* the gap before it, and the gap after it, are both where it already is. */
|
|
155
|
+
export function isNoopSlot(from: number, slot: number): boolean {
|
|
156
|
+
return from >= 0 && (slot === from || slot === from + 1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* What a drop of `id` into `slot` does to `order`: the new order and the
|
|
161
|
+
* two indices, or `null` when nothing would move — the item is not in the
|
|
162
|
+
* list, or the slot is one of its own two gaps. `to` is where the item
|
|
163
|
+
* ends up *after* it has been taken out, which is what `arrayMove` wants
|
|
164
|
+
* and what a caller holding objects splices with.
|
|
165
|
+
*/
|
|
166
|
+
export function moveToSlot(
|
|
167
|
+
order: readonly ReorderId[],
|
|
168
|
+
id: ReorderId,
|
|
169
|
+
slot: number,
|
|
170
|
+
): { items: ReorderId[]; from: number; to: number } | null {
|
|
171
|
+
const many = moveManyToSlot(order, [id], slot, id);
|
|
172
|
+
return many && { items: many.items, from: many.from, to: many.to };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The same move with a **set** of items — a multi-drag. The set travels in
|
|
177
|
+
* the list's own order, whatever order it was selected in, and lands as one
|
|
178
|
+
* run at `slot`; `from`/`to` are where `primary` (the item actually under
|
|
179
|
+
* the pointer) started and ended up.
|
|
180
|
+
*
|
|
181
|
+
* The slot counts gaps in the *original* order, so the arithmetic has to
|
|
182
|
+
* discount the members of the set that were before it — which is also why
|
|
183
|
+
* the single-item no-op rule generalizes rather than being special-cased:
|
|
184
|
+
* a set that lands where it already was produces the order it started from,
|
|
185
|
+
* and that is what `null` reports.
|
|
186
|
+
*/
|
|
187
|
+
export function moveManyToSlot(
|
|
188
|
+
order: readonly ReorderId[],
|
|
189
|
+
ids: readonly ReorderId[],
|
|
190
|
+
slot: number,
|
|
191
|
+
primary: ReorderId = ids[0]!,
|
|
192
|
+
): { items: ReorderId[]; ids: ReorderId[]; from: number; to: number } | null {
|
|
193
|
+
const taking = new Set(ids);
|
|
194
|
+
// the set in the list's order, and only the members the list actually has
|
|
195
|
+
const moving = order.filter((id) => taking.has(id));
|
|
196
|
+
if (moving.length === 0) return null;
|
|
197
|
+
const from = order.indexOf(primary);
|
|
198
|
+
if (from < 0) return null;
|
|
199
|
+
const rest = order.filter((id) => !taking.has(id));
|
|
200
|
+
const before = order
|
|
201
|
+
.slice(0, Math.max(slot, 0))
|
|
202
|
+
.filter((id) => taking.has(id)).length;
|
|
203
|
+
const at = Math.min(Math.max(slot - before, 0), rest.length);
|
|
204
|
+
const items = [...rest.slice(0, at), ...moving, ...rest.slice(at)];
|
|
205
|
+
if (
|
|
206
|
+
items.length === order.length &&
|
|
207
|
+
items.every((id, i) => id === order[i])
|
|
208
|
+
) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return { items, ids: moving, from, to: items.indexOf(primary) };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** `order` with `id` inserted at `slot` — an item arriving from elsewhere. */
|
|
215
|
+
export function insertAtSlot(
|
|
216
|
+
order: readonly ReorderId[],
|
|
217
|
+
id: ReorderId,
|
|
218
|
+
slot: number,
|
|
219
|
+
): ReorderId[] {
|
|
220
|
+
return insertManyAtSlot(order, [id], slot);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The same, with a set arriving together — a multi-drag from another list. */
|
|
224
|
+
export function insertManyAtSlot(
|
|
225
|
+
order: readonly ReorderId[],
|
|
226
|
+
ids: readonly ReorderId[],
|
|
227
|
+
slot: number,
|
|
228
|
+
): ReorderId[] {
|
|
229
|
+
const out = order.slice();
|
|
230
|
+
out.splice(Math.min(Math.max(slot, 0), out.length), 0, ...ids);
|
|
231
|
+
return out;
|
|
232
|
+
}
|
package/src/richtext/node.ts
CHANGED
|
@@ -131,8 +131,10 @@ interface LaidRunLike {
|
|
|
131
131
|
/** Extent within the paragraph, in **code units** (ntk's run vocabulary). */
|
|
132
132
|
start: number;
|
|
133
133
|
end: number;
|
|
134
|
-
|
|
135
|
-
run
|
|
134
|
+
/** Optional for the same reason as `runs.ts`'s `LaidRun`: react-x11's
|
|
135
|
+
* Cocoa engine hands back a run's geometry and nothing else. */
|
|
136
|
+
span?: TextRun;
|
|
137
|
+
run?: {
|
|
136
138
|
font: { metrics(size: number): FontMetricsLike };
|
|
137
139
|
size: number;
|
|
138
140
|
direction?: 'ltr' | 'rtl';
|
|
@@ -252,9 +254,10 @@ export class RichTextNode extends Node {
|
|
|
252
254
|
const fonts = (this.app as { fonts?: FontsLike } | null)?.fonts;
|
|
253
255
|
let layout: TextLayoutLike | null = null;
|
|
254
256
|
if (fonts) {
|
|
257
|
+
const s = this._scale;
|
|
255
258
|
layout = fonts.layout(
|
|
256
|
-
this.
|
|
257
|
-
{ family: 'sans-serif', size: 14 },
|
|
259
|
+
this._deviceRuns(s),
|
|
260
|
+
{ family: 'sans-serif', size: 14 * s },
|
|
258
261
|
{
|
|
259
262
|
maxWidth: Number.isFinite(maxWidth) ? maxWidth : undefined,
|
|
260
263
|
lineHeight: this.style.lineHeight,
|
|
@@ -267,6 +270,34 @@ export class RichTextNode extends Node {
|
|
|
267
270
|
return layout;
|
|
268
271
|
}
|
|
269
272
|
|
|
273
|
+
// --- units ---------------------------------------------------------------
|
|
274
|
+
//
|
|
275
|
+
// The layout, `abs` and the paint are device pixels (react-x11's
|
|
276
|
+
// docs/scale.md). A `TextRun.size` is a length the application wrote, so
|
|
277
|
+
// like every style length it is logical, and unlike a style length nothing
|
|
278
|
+
// in core multiplies it on the way in — `_deviceRuns` does, so the runs are
|
|
279
|
+
// shaped at `size * scale` and a 14 on a 2x panel is the size a
|
|
280
|
+
// `fontSize: 14` is, sharper rather than smaller. A synthetic event's
|
|
281
|
+
// `x`/`y` are logical too, which is what `hrefAtPoint` is handed; the four
|
|
282
|
+
// selection accessors are core's device-pixel contract and stay device.
|
|
283
|
+
|
|
284
|
+
/** Device pixels per logical pixel — the display scale this element's
|
|
285
|
+
* window resolved to, constant for the node's life. */
|
|
286
|
+
private get _scale(): number {
|
|
287
|
+
return this.scale > 0 ? this.scale : 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** The runs with their sizes on the device grid. The array identity is
|
|
291
|
+
* what the layout cache keys on, so at 1x the props' own array is
|
|
292
|
+
* returned untouched. */
|
|
293
|
+
private _deviceRuns(scale: number): TextRun[] {
|
|
294
|
+
const runs = this._runs();
|
|
295
|
+
if (scale === 1) return runs;
|
|
296
|
+
return runs.map((r) =>
|
|
297
|
+
typeof r.size === 'number' ? { ...r, size: r.size * scale } : r,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
270
301
|
/**
|
|
271
302
|
* The layout this node is currently painted with — and, because it is the
|
|
272
303
|
* one thing every accessor below answers from, the reason a caret rect and
|
|
@@ -302,8 +333,9 @@ export class RichTextNode extends Node {
|
|
|
302
333
|
// The four accessors from react-x11#291. Implementing them is the whole of
|
|
303
334
|
// joining a `selectable` document: core walks the subtree, asks, and pushes
|
|
304
335
|
// back a `selectionRange` for `paint` to fill. Indices are **code points**
|
|
305
|
-
// and rectangles are in the **owning window's** coordinates
|
|
306
|
-
// space `abs` and
|
|
336
|
+
// and rectangles are in the **owning window's** coordinates in **device**
|
|
337
|
+
// pixels — the space `abs` is in, and the one core asks in (it reads the
|
|
338
|
+
// point back off the native event; a synthetic `x`/`y` is logical).
|
|
307
339
|
|
|
308
340
|
override textContent(): string {
|
|
309
341
|
if (this._text === null) {
|
|
@@ -344,18 +376,20 @@ export class RichTextNode extends Node {
|
|
|
344
376
|
}));
|
|
345
377
|
}
|
|
346
378
|
|
|
347
|
-
/** The run's link target under a
|
|
348
|
-
*
|
|
349
|
-
*
|
|
379
|
+
/** The run's link target under a **logical** window point — the one a
|
|
380
|
+
* mouse event carries — if any, for click-to-follow. Not part of the
|
|
381
|
+
* selection seam: core deliberately left hover and `cursorAt` out of
|
|
382
|
+
* #291, so following a link stays this package's. */
|
|
350
383
|
hrefAtPoint(x: number, y: number): string | null {
|
|
351
384
|
const layout = this._paintLayout();
|
|
352
385
|
if (!layout) return null;
|
|
353
|
-
const
|
|
354
|
-
const
|
|
386
|
+
const s = this._scale;
|
|
387
|
+
const lx = x * s - this.abs.x;
|
|
388
|
+
const ly = y * s - this.abs.y;
|
|
355
389
|
for (const line of layout.lines) {
|
|
356
390
|
if (ly < line.y || ly >= line.y + line.height) continue;
|
|
357
391
|
for (const r of line.runs) {
|
|
358
|
-
const href = r.span
|
|
392
|
+
const href = r.span?.href;
|
|
359
393
|
if (href == null) continue;
|
|
360
394
|
if (lx >= line.x + r.x && lx <= line.x + r.x + r.width) return href;
|
|
361
395
|
}
|
|
@@ -370,12 +404,13 @@ export class RichTextNode extends Node {
|
|
|
370
404
|
const layout = this._paintLayout();
|
|
371
405
|
if (!layout || !canFill(ctx)) return;
|
|
372
406
|
const { x, y } = this.abs;
|
|
407
|
+
const s = this._scale;
|
|
373
408
|
|
|
374
409
|
ctx.save();
|
|
375
410
|
|
|
376
411
|
// 1. run decorations that sit under everything: the code chip, and the
|
|
377
412
|
// terminal's cell backgrounds
|
|
378
|
-
for (const line of layout.lines) paintRunBackgrounds(ctx, line, x, y);
|
|
413
|
+
for (const line of layout.lines) paintRunBackgrounds(ctx, line, x, y, s);
|
|
379
414
|
|
|
380
415
|
// 2. the band the document selection has claimed of this element's text,
|
|
381
416
|
// translucent so the ink keeps its contrast on either palette (the
|
|
@@ -399,7 +434,7 @@ export class RichTextNode extends Node {
|
|
|
399
434
|
layout.draw(ctx, x, y);
|
|
400
435
|
|
|
401
436
|
// 4. rules over the ink: link underlines, strikethrough
|
|
402
|
-
for (const line of layout.lines) paintRunRules(ctx, line, x, y);
|
|
437
|
+
for (const line of layout.lines) paintRunRules(ctx, line, x, y, s);
|
|
403
438
|
|
|
404
439
|
ctx.restore();
|
|
405
440
|
}
|
package/src/richtext/runs.ts
CHANGED
|
@@ -29,15 +29,26 @@ export function canFill(ctx: unknown): ctx is FillContext {
|
|
|
29
29
|
return typeof (ctx as Partial<FillContext> | null)?.fillRect === 'function';
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* One run of one laid-out line, as much of it as decoration reads.
|
|
34
|
+
*
|
|
35
|
+
* `span` and `run` are what ntk's layout hands back and what every
|
|
36
|
+
* decoration is read from: the span the run came from, markers and all, and
|
|
37
|
+
* the face it was shaped with. Both are optional because react-x11's Cocoa
|
|
38
|
+
* engine (2.3.x) reports only a run's geometry — `{ x, width, start, end }`
|
|
39
|
+
* — and a paint that assumed the rest threw on the first paragraph on
|
|
40
|
+
* macOS. A run without its span has no decoration to draw; a run without
|
|
41
|
+
* its face takes its vertical extent from the line, which both engines
|
|
42
|
+
* report.
|
|
43
|
+
*/
|
|
33
44
|
export interface LaidRun {
|
|
34
45
|
x: number;
|
|
35
46
|
width: number;
|
|
36
47
|
/** Extent within the laid-out text, in **code units** (ntk's vocabulary). */
|
|
37
48
|
start: number;
|
|
38
49
|
end: number;
|
|
39
|
-
span
|
|
40
|
-
run
|
|
50
|
+
span?: TextRun;
|
|
51
|
+
run?: {
|
|
41
52
|
font: { metrics(size: number): { ascent: number; descent: number } };
|
|
42
53
|
size: number;
|
|
43
54
|
direction?: 'ltr' | 'rtl';
|
|
@@ -50,6 +61,11 @@ export interface LaidLine {
|
|
|
50
61
|
y: number;
|
|
51
62
|
height: number;
|
|
52
63
|
baseline: number;
|
|
64
|
+
/** The tallest face on the line, above and below `baseline`. Both engines
|
|
65
|
+
* report them; they stand in for a run's own metrics when the engine did
|
|
66
|
+
* not hand the run's face back. */
|
|
67
|
+
ascent?: number;
|
|
68
|
+
descent?: number;
|
|
53
69
|
start: number;
|
|
54
70
|
end: number;
|
|
55
71
|
runs: LaidRun[];
|
|
@@ -65,24 +81,76 @@ export interface CaretSource {
|
|
|
65
81
|
};
|
|
66
82
|
}
|
|
67
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The vertical extent of a run's ink, above and below the baseline: the
|
|
86
|
+
* run's own face when the engine handed it back, otherwise the line's.
|
|
87
|
+
*/
|
|
88
|
+
function inkExtent(
|
|
89
|
+
r: LaidRun,
|
|
90
|
+
line: LaidLine,
|
|
91
|
+
): { ascent: number; descent: number } {
|
|
92
|
+
if (r.run) return r.run.font.metrics(r.run.size);
|
|
93
|
+
return {
|
|
94
|
+
ascent: line.ascent ?? line.baseline - line.y,
|
|
95
|
+
descent: line.descent ?? line.y + line.height - line.baseline,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let warnedSpanless = false;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Say so, once, when a laid-out run arrives without the span it came from:
|
|
103
|
+
* every decoration is read off that span, so none will be drawn, and a
|
|
104
|
+
* document that suddenly has no code chips or link underlines should not be
|
|
105
|
+
* a mystery. react-x11's Cocoa engine (2.3.x) is the one that does this.
|
|
106
|
+
*
|
|
107
|
+
* `process` and `console` come off `globalThis` because `src/` compiles
|
|
108
|
+
* with `types: []` — a Node global that wandered in would fail the build
|
|
109
|
+
* rather than become an implicit `@types/node` dependency.
|
|
110
|
+
*/
|
|
111
|
+
function warnSpanless(): void {
|
|
112
|
+
if (warnedSpanless) return;
|
|
113
|
+
warnedSpanless = true;
|
|
114
|
+
const g = globalThis as {
|
|
115
|
+
process?: { env?: Record<string, string | undefined> };
|
|
116
|
+
console?: { warn(message: string): void };
|
|
117
|
+
};
|
|
118
|
+
if (g.process?.env?.NODE_ENV === 'production') return;
|
|
119
|
+
g.console?.warn(
|
|
120
|
+
'@react-x11/components: the text engine handed back laid-out runs ' +
|
|
121
|
+
'without their spans, so run decorations (backgrounds, underlines, ' +
|
|
122
|
+
"strikethrough) and inline link hit-testing are off. react-x11's " +
|
|
123
|
+
"Cocoa text engine (2.3.x) does this; ntk's does not.",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
68
127
|
/**
|
|
69
128
|
* The fills that sit **under** the glyphs: the inline-code chip, a
|
|
70
129
|
* terminal's cell backgrounds, a highlighted `<span>`.
|
|
71
130
|
*
|
|
72
131
|
* `dx`/`dy` translate the line's own coordinates into the target space, so a
|
|
73
|
-
* caller that placed this line itself passes where it placed it.
|
|
132
|
+
* caller that placed this line itself passes where it placed it. `scale` is
|
|
133
|
+
* the display scale: the chip's two-pixel inset is a logical length that
|
|
134
|
+
* never passes through a style, and grows with it.
|
|
74
135
|
*/
|
|
75
136
|
export function paintRunBackgrounds(
|
|
76
137
|
ctx: FillContext,
|
|
77
138
|
line: LaidLine,
|
|
78
139
|
dx: number,
|
|
79
140
|
dy: number,
|
|
141
|
+
scale = 1,
|
|
80
142
|
): void {
|
|
143
|
+
const inset = Math.round(2 * scale);
|
|
81
144
|
for (const r of line.runs) {
|
|
82
|
-
const
|
|
145
|
+
const span = r.span;
|
|
146
|
+
if (!span) {
|
|
147
|
+
warnSpanless();
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const bg = span.bg;
|
|
83
151
|
if (!bg) continue;
|
|
84
152
|
ctx.fillStyle = bg;
|
|
85
|
-
if (
|
|
153
|
+
if (span.bgFill === 'line') {
|
|
86
154
|
// Both edges are rounded from absolute positions rather than the left
|
|
87
155
|
// being rounded and the width ceiled, so two adjacent runs agree on the
|
|
88
156
|
// pixel between them: no seam, and no overlap either.
|
|
@@ -96,56 +164,70 @@ export function paintRunBackgrounds(
|
|
|
96
164
|
);
|
|
97
165
|
continue;
|
|
98
166
|
}
|
|
99
|
-
const m =
|
|
167
|
+
const m = inkExtent(r, line);
|
|
100
168
|
ctx.fillRect(
|
|
101
|
-
Math.round(dx + line.x + r.x -
|
|
169
|
+
Math.round(dx + line.x + r.x - inset),
|
|
102
170
|
Math.round(dy + line.baseline - m.ascent),
|
|
103
|
-
Math.ceil(r.width +
|
|
171
|
+
Math.ceil(r.width + inset * 2),
|
|
104
172
|
Math.ceil(m.ascent + m.descent),
|
|
105
173
|
);
|
|
106
174
|
}
|
|
107
175
|
}
|
|
108
176
|
|
|
109
|
-
/** The rules that sit **over** the glyphs: link underlines, strikethrough.
|
|
177
|
+
/** The rules that sit **over** the glyphs: link underlines, strikethrough.
|
|
178
|
+
* A rule is one logical pixel thick — `scale` device pixels, so a link on a
|
|
179
|
+
* 2x panel is underlined as heavily as on a 1x one, not with a hairline. */
|
|
110
180
|
export function paintRunRules(
|
|
111
181
|
ctx: FillContext,
|
|
112
182
|
line: LaidLine,
|
|
113
183
|
dx: number,
|
|
114
184
|
dy: number,
|
|
185
|
+
scale = 1,
|
|
115
186
|
): void {
|
|
187
|
+
const t = ruleThickness(scale);
|
|
116
188
|
for (const r of line.runs) {
|
|
117
|
-
const
|
|
189
|
+
const span = r.span;
|
|
190
|
+
if (!span) continue;
|
|
191
|
+
const { underline, strike } = span;
|
|
118
192
|
if (underline) {
|
|
119
193
|
ctx.fillStyle = underline;
|
|
120
194
|
underlineRule(
|
|
121
195
|
ctx,
|
|
122
196
|
Math.round(dx + line.x + r.x),
|
|
123
|
-
Math.round(dy + line.baseline + 2),
|
|
197
|
+
Math.round(dy + line.baseline + 2 * t),
|
|
124
198
|
Math.ceil(r.width),
|
|
125
|
-
|
|
199
|
+
span.underlineStyle ?? 'single',
|
|
200
|
+
t,
|
|
126
201
|
);
|
|
127
202
|
}
|
|
128
203
|
if (strike) {
|
|
129
|
-
const m =
|
|
204
|
+
const m = inkExtent(r, line);
|
|
130
205
|
ctx.fillStyle = strike;
|
|
131
206
|
ctx.fillRect(
|
|
132
207
|
Math.round(dx + line.x + r.x),
|
|
133
208
|
Math.round(dy + line.baseline - m.ascent * 0.38),
|
|
134
209
|
Math.ceil(r.width),
|
|
135
|
-
|
|
210
|
+
t,
|
|
136
211
|
);
|
|
137
212
|
}
|
|
138
213
|
}
|
|
139
214
|
}
|
|
140
215
|
|
|
216
|
+
/** One logical pixel on the device grid, never less than one device pixel. */
|
|
217
|
+
function ruleThickness(scale: number): number {
|
|
218
|
+
return Math.max(1, Math.round(scale));
|
|
219
|
+
}
|
|
220
|
+
|
|
141
221
|
/**
|
|
142
222
|
* The rule under a run, in one of SGR 4's five styles.
|
|
143
223
|
*
|
|
144
|
-
* All five are built from
|
|
145
|
-
* backend has no path API, and a
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* job.
|
|
224
|
+
* All five are built from rectangles `t` pixels thick — one logical pixel —
|
|
225
|
+
* rather than a stroked path: the mock backend has no path API, and a
|
|
226
|
+
* hairline stroke on a text baseline is not worth an antialiased path even
|
|
227
|
+
* where there is one. The curl is a two-level square wave — at a text size
|
|
228
|
+
* it reads as a squiggle, which is the entire job. The dot pitch and the
|
|
229
|
+
* dash length scale with the thickness, so the pattern is the pattern at
|
|
230
|
+
* any display scale.
|
|
149
231
|
*/
|
|
150
232
|
export function underlineRule(
|
|
151
233
|
ctx: FillContext,
|
|
@@ -153,31 +235,34 @@ export function underlineRule(
|
|
|
153
235
|
y: number,
|
|
154
236
|
width: number,
|
|
155
237
|
style: NonNullable<TextRun['underlineStyle']>,
|
|
238
|
+
t = 1,
|
|
156
239
|
): void {
|
|
157
240
|
switch (style) {
|
|
158
241
|
case 'double':
|
|
159
|
-
ctx.fillRect(x, y, width,
|
|
160
|
-
ctx.fillRect(x, y + 2, width,
|
|
242
|
+
ctx.fillRect(x, y, width, t);
|
|
243
|
+
ctx.fillRect(x, y + 2 * t, width, t);
|
|
161
244
|
return;
|
|
162
245
|
case 'dotted':
|
|
163
|
-
for (let i = 0; i < width; i += 2
|
|
246
|
+
for (let i = 0; i < width; i += 2 * t) {
|
|
247
|
+
ctx.fillRect(x + i, y, Math.min(t, width - i), t);
|
|
248
|
+
}
|
|
164
249
|
return;
|
|
165
250
|
case 'dashed':
|
|
166
|
-
for (let i = 0; i < width; i += 6)
|
|
167
|
-
ctx.fillRect(x + i, y, Math.min(3, width - i),
|
|
251
|
+
for (let i = 0; i < width; i += 6 * t)
|
|
252
|
+
ctx.fillRect(x + i, y, Math.min(3 * t, width - i), t);
|
|
168
253
|
return;
|
|
169
254
|
case 'curly':
|
|
170
|
-
for (let i = 0; i < width; i += 2) {
|
|
255
|
+
for (let i = 0; i < width; i += 2 * t) {
|
|
171
256
|
ctx.fillRect(
|
|
172
257
|
x + i,
|
|
173
|
-
y + (i % 4 === 0 ? 0 :
|
|
174
|
-
Math.min(2, width - i),
|
|
175
|
-
|
|
258
|
+
y + (i % (4 * t) === 0 ? 0 : t),
|
|
259
|
+
Math.min(2 * t, width - i),
|
|
260
|
+
t,
|
|
176
261
|
);
|
|
177
262
|
}
|
|
178
263
|
return;
|
|
179
264
|
default:
|
|
180
|
-
ctx.fillRect(x, y, width,
|
|
265
|
+
ctx.fillRect(x, y, width, t);
|
|
181
266
|
return;
|
|
182
267
|
}
|
|
183
268
|
}
|