@motionscript/code 0.1.0-alpha.8 → 0.1.0-alpha.9

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/src/index.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from './node';
2
- export * from './engine';
3
- export { NODES } from "./nodes";
1
+ export * from './node';
2
+ export * from './engine';
3
+ export { NODES } from "./nodes";
package/src/layout.ts CHANGED
@@ -1,157 +1,157 @@
1
- import type { InsetsResolved, MeasureContext2D, RenderContext2D } from "@motionscript/sdk";
2
- import type { TokenAdvanceCache } from "./measure-cache";
3
- import type { IdLine } from "./tokens";
4
-
5
- /**
6
- * Everything about a listing's geometry that is *not* its content — the type,
7
- * the padding, whether there is a gutter. Split out from the node so a layout
8
- * can be computed for a token structure the node is no longer holding: during a
9
- * structural transition the frame is the interpolation of two of them, and the
10
- * one that is going away is not reachable through `this` any more.
11
- */
12
- export interface CodeMetrics {
13
- fontSize: number;
14
- fontFamily: string;
15
- lineHeight: number;
16
- letterSpacing: number;
17
- padding: InsetsResolved;
18
- showLineNumbers: boolean;
19
- lineNumberGap: number;
20
- }
21
-
22
- /** Where one token sits: its LEFT edge, and the vertical CENTRE of its line's slot. */
23
- export interface TokenBox {
24
- x: number;
25
- y: number;
26
- }
27
-
28
- /**
29
- * A resolved listing geometry: every token placed, in the node's own space
30
- * (origin at the block's centre, y-up).
31
- *
32
- * Positions are absolute rather than "a running sum plus a width scale", which
33
- * is the whole point. A token's place in the *next* structure is knowable
34
- * without drawing it, so a transition can move a token from where it was to
35
- * where it will be instead of faking the motion by collapsing the advance of
36
- * its neighbours — which is what made an insert look like a pile of glyphs at
37
- * the left margin springing apart.
38
- */
39
- export interface CodeLayout {
40
- /** Token id → placement. */
41
- tokens: Map<number, TokenBox>;
42
- /** Vertical centre of each line's slot, by line index. */
43
- lineY: number[];
44
- /**
45
- * Width of each line's ink, by line index — the sum of its tokens' advances,
46
- * so a blank line is `0` and a short line is short.
47
- *
48
- * Returned rather than kept local because it is the only thing that knows a
49
- * listing is **ragged**: `innerW` is the longest line, and everything that
50
- * asks where the text actually is (hit testing, for one) would otherwise have
51
- * to treat the block as a rectangle the width of its longest row.
52
- */
53
- lineW: number[];
54
- /** Line id → its index in this structure (i.e. its line *number* minus one). */
55
- lineIndex: Map<number, number>;
56
- gutter: number;
57
- gutterGap: number;
58
- /** Content size, no padding. */
59
- innerW: number;
60
- innerH: number;
61
- /** Content size plus padding — what the block draws and measures as. */
62
- blockW: number;
63
- blockH: number;
64
- /** Left edge of the code column (after padding and gutter). */
65
- startX: number;
66
- }
67
-
68
- /**
69
- * Signature of everything a layout depends on other than the token structure
70
- * itself. A cached layout survives exactly as long as this is unchanged.
71
- */
72
- export function metricsSignature(m: CodeMetrics): string {
73
- const p = m.padding;
74
- return [
75
- m.fontSize, m.fontFamily, m.lineHeight, m.letterSpacing,
76
- p.left, p.right, p.top, p.bottom,
77
- m.showLineNumbers ? 1 : 0, m.lineNumberGap,
78
- ].join("|");
79
- }
80
-
81
- /**
82
- * Horizontal gap between the line-number column and the code text. Sized in
83
- * space-widths so it scales with the font, and measured with letterSpacing 0 —
84
- * line numbers and the gap don't carry the code's letter-spacing.
85
- */
86
- function gutterGapOf(scope: MeasureContext2D | RenderContext2D, m: CodeMetrics, cache: TokenAdvanceCache): number {
87
- return cache.advance(scope, " ", m.fontSize, m.fontFamily, 0) * m.lineNumberGap;
88
- }
89
-
90
- /** Width of the line-number column for a structure of `lineCount` lines, gap included. */
91
- function gutterOf(
92
- scope: MeasureContext2D | RenderContext2D,
93
- m: CodeMetrics,
94
- cache: TokenAdvanceCache,
95
- lineCount: number,
96
- gap: number,
97
- ): number {
98
- if (!m.showLineNumbers) return 0;
99
- const sample = String(Math.max(1, lineCount));
100
- return cache.advance(scope, sample, m.fontSize, m.fontFamily, 0) + gap;
101
- }
102
-
103
- /**
104
- * Place every token of `lines`.
105
- *
106
- * Pure in (lines, metrics, font measurements), which is what lets a caller cache
107
- * one per structure rather than recomputing it per frame — a transition's two
108
- * endpoints are both fixed for its whole duration.
109
- */
110
- export function layoutCode(
111
- lines: IdLine[],
112
- m: CodeMetrics,
113
- cache: TokenAdvanceCache,
114
- scope: MeasureContext2D | RenderContext2D,
115
- ): CodeLayout {
116
- const lineH = m.fontSize * m.lineHeight;
117
- const gutterGap = m.showLineNumbers ? gutterGapOf(scope, m, cache) : 0;
118
- const gutter = gutterOf(scope, m, cache, lines.length, gutterGap);
119
-
120
- const widths: number[] = new Array(lines.length);
121
- let maxLineWidth = 0;
122
- for (let i = 0; i < lines.length; i++) {
123
- let w = 0;
124
- for (const tok of lines[i].tokens) {
125
- w += cache.advance(scope, tok.content, m.fontSize, m.fontFamily, m.letterSpacing);
126
- }
127
- widths[i] = w;
128
- if (w > maxLineWidth) maxLineWidth = w;
129
- }
130
-
131
- const innerW = maxLineWidth + gutter;
132
- const innerH = lines.length * lineH;
133
- const blockW = innerW + m.padding.left + m.padding.right;
134
- const blockH = innerH + m.padding.top + m.padding.bottom;
135
- const startX = -blockW / 2 + m.padding.left + gutter;
136
- // y-up author space: the first line sits at the TOP of the block, so the
137
- // cursor starts at the top edge (+half height, minus top padding) and steps
138
- // DOWNWARD by subtracting a line height per row.
139
- const startY = blockH / 2 - m.padding.top;
140
-
141
- const tokens = new Map<number, TokenBox>();
142
- const lineY: number[] = new Array(lines.length);
143
- const lineIndex = new Map<number, number>();
144
-
145
- for (let i = 0; i < lines.length; i++) {
146
- const centerY = startY - i * lineH - lineH / 2;
147
- lineY[i] = centerY;
148
- lineIndex.set(lines[i].id, i);
149
- let x = startX;
150
- for (const tok of lines[i].tokens) {
151
- tokens.set(tok.id, { x, y: centerY });
152
- x += cache.advance(scope, tok.content, m.fontSize, m.fontFamily, m.letterSpacing);
153
- }
154
- }
155
-
156
- return { tokens, lineY, lineW: widths, lineIndex, gutter, gutterGap, innerW, innerH, blockW, blockH, startX };
157
- }
1
+ import type { InsetsResolved, MeasureContext2D, RenderContext2D } from "@motionscript/sdk";
2
+ import type { TokenAdvanceCache } from "./measure-cache";
3
+ import type { IdLine } from "./tokens";
4
+
5
+ /**
6
+ * Everything about a listing's geometry that is *not* its content — the type,
7
+ * the padding, whether there is a gutter. Split out from the node so a layout
8
+ * can be computed for a token structure the node is no longer holding: during a
9
+ * structural transition the frame is the interpolation of two of them, and the
10
+ * one that is going away is not reachable through `this` any more.
11
+ */
12
+ export interface CodeMetrics {
13
+ fontSize: number;
14
+ fontFamily: string;
15
+ lineHeight: number;
16
+ letterSpacing: number;
17
+ padding: InsetsResolved;
18
+ showLineNumbers: boolean;
19
+ lineNumberGap: number;
20
+ }
21
+
22
+ /** Where one token sits: its LEFT edge, and the vertical CENTRE of its line's slot. */
23
+ export interface TokenBox {
24
+ x: number;
25
+ y: number;
26
+ }
27
+
28
+ /**
29
+ * A resolved listing geometry: every token placed, in the node's own space
30
+ * (origin at the block's centre, y-up).
31
+ *
32
+ * Positions are absolute rather than "a running sum plus a width scale", which
33
+ * is the whole point. A token's place in the *next* structure is knowable
34
+ * without drawing it, so a transition can move a token from where it was to
35
+ * where it will be instead of faking the motion by collapsing the advance of
36
+ * its neighbours — which is what made an insert look like a pile of glyphs at
37
+ * the left margin springing apart.
38
+ */
39
+ export interface CodeLayout {
40
+ /** Token id → placement. */
41
+ tokens: Map<number, TokenBox>;
42
+ /** Vertical centre of each line's slot, by line index. */
43
+ lineY: number[];
44
+ /**
45
+ * Width of each line's ink, by line index — the sum of its tokens' advances,
46
+ * so a blank line is `0` and a short line is short.
47
+ *
48
+ * Returned rather than kept local because it is the only thing that knows a
49
+ * listing is **ragged**: `innerW` is the longest line, and everything that
50
+ * asks where the text actually is (hit testing, for one) would otherwise have
51
+ * to treat the block as a rectangle the width of its longest row.
52
+ */
53
+ lineW: number[];
54
+ /** Line id → its index in this structure (i.e. its line *number* minus one). */
55
+ lineIndex: Map<number, number>;
56
+ gutter: number;
57
+ gutterGap: number;
58
+ /** Content size, no padding. */
59
+ innerW: number;
60
+ innerH: number;
61
+ /** Content size plus padding — what the block draws and measures as. */
62
+ blockW: number;
63
+ blockH: number;
64
+ /** Left edge of the code column (after padding and gutter). */
65
+ startX: number;
66
+ }
67
+
68
+ /**
69
+ * Signature of everything a layout depends on other than the token structure
70
+ * itself. A cached layout survives exactly as long as this is unchanged.
71
+ */
72
+ export function metricsSignature(m: CodeMetrics): string {
73
+ const p = m.padding;
74
+ return [
75
+ m.fontSize, m.fontFamily, m.lineHeight, m.letterSpacing,
76
+ p.left, p.right, p.top, p.bottom,
77
+ m.showLineNumbers ? 1 : 0, m.lineNumberGap,
78
+ ].join("|");
79
+ }
80
+
81
+ /**
82
+ * Horizontal gap between the line-number column and the code text. Sized in
83
+ * space-widths so it scales with the font, and measured with letterSpacing 0 —
84
+ * line numbers and the gap don't carry the code's letter-spacing.
85
+ */
86
+ function gutterGapOf(scope: MeasureContext2D | RenderContext2D, m: CodeMetrics, cache: TokenAdvanceCache): number {
87
+ return cache.advance(scope, " ", m.fontSize, m.fontFamily, 0) * m.lineNumberGap;
88
+ }
89
+
90
+ /** Width of the line-number column for a structure of `lineCount` lines, gap included. */
91
+ function gutterOf(
92
+ scope: MeasureContext2D | RenderContext2D,
93
+ m: CodeMetrics,
94
+ cache: TokenAdvanceCache,
95
+ lineCount: number,
96
+ gap: number,
97
+ ): number {
98
+ if (!m.showLineNumbers) return 0;
99
+ const sample = String(Math.max(1, lineCount));
100
+ return cache.advance(scope, sample, m.fontSize, m.fontFamily, 0) + gap;
101
+ }
102
+
103
+ /**
104
+ * Place every token of `lines`.
105
+ *
106
+ * Pure in (lines, metrics, font measurements), which is what lets a caller cache
107
+ * one per structure rather than recomputing it per frame — a transition's two
108
+ * endpoints are both fixed for its whole duration.
109
+ */
110
+ export function layoutCode(
111
+ lines: IdLine[],
112
+ m: CodeMetrics,
113
+ cache: TokenAdvanceCache,
114
+ scope: MeasureContext2D | RenderContext2D,
115
+ ): CodeLayout {
116
+ const lineH = m.fontSize * m.lineHeight;
117
+ const gutterGap = m.showLineNumbers ? gutterGapOf(scope, m, cache) : 0;
118
+ const gutter = gutterOf(scope, m, cache, lines.length, gutterGap);
119
+
120
+ const widths: number[] = new Array(lines.length);
121
+ let maxLineWidth = 0;
122
+ for (let i = 0; i < lines.length; i++) {
123
+ let w = 0;
124
+ for (const tok of lines[i].tokens) {
125
+ w += cache.advance(scope, tok.content, m.fontSize, m.fontFamily, m.letterSpacing);
126
+ }
127
+ widths[i] = w;
128
+ if (w > maxLineWidth) maxLineWidth = w;
129
+ }
130
+
131
+ const innerW = maxLineWidth + gutter;
132
+ const innerH = lines.length * lineH;
133
+ const blockW = innerW + m.padding.left + m.padding.right;
134
+ const blockH = innerH + m.padding.top + m.padding.bottom;
135
+ const startX = -blockW / 2 + m.padding.left + gutter;
136
+ // y-up author space: the first line sits at the TOP of the block, so the
137
+ // cursor starts at the top edge (+half height, minus top padding) and steps
138
+ // DOWNWARD by subtracting a line height per row.
139
+ const startY = blockH / 2 - m.padding.top;
140
+
141
+ const tokens = new Map<number, TokenBox>();
142
+ const lineY: number[] = new Array(lines.length);
143
+ const lineIndex = new Map<number, number>();
144
+
145
+ for (let i = 0; i < lines.length; i++) {
146
+ const centerY = startY - i * lineH - lineH / 2;
147
+ lineY[i] = centerY;
148
+ lineIndex.set(lines[i].id, i);
149
+ let x = startX;
150
+ for (const tok of lines[i].tokens) {
151
+ tokens.set(tok.id, { x, y: centerY });
152
+ x += cache.advance(scope, tok.content, m.fontSize, m.fontFamily, m.letterSpacing);
153
+ }
154
+ }
155
+
156
+ return { tokens, lineY, lineW: widths, lineIndex, gutter, gutterGap, innerW, innerH, blockW, blockH, startX };
157
+ }