@react-x11/components 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/code/index.d.ts.map +1 -1
- package/dist/code/index.js +25 -24
- package/dist/code/index.js.map +1 -1
- package/dist/code-language/index.d.ts +1 -1
- package/dist/code-language/index.d.ts.map +1 -1
- package/dist/code-language/index.js +1 -1
- package/dist/code-language/index.js.map +1 -1
- package/dist/code-language/runs.d.ts +31 -0
- package/dist/code-language/runs.d.ts.map +1 -1
- package/dist/code-language/runs.js +105 -0
- package/dist/code-language/runs.js.map +1 -1
- package/dist/codeblock/index.d.ts +13 -0
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +37 -1
- package/dist/codeblock/index.js.map +1 -1
- package/dist/flow/node.d.ts +14 -6
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +82 -64
- package/dist/flow/node.js.map +1 -1
- package/dist/html/controls.d.ts +26 -7
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +28 -3
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +32 -10
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +89 -31
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +19 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +86 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +11 -0
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +114 -4
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +45 -5
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +363 -14
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.js +22 -5
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +10 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +27 -2
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/css/vars.d.ts +5 -0
- package/dist/html/css/vars.d.ts.map +1 -1
- package/dist/html/css/vars.js +2 -2
- package/dist/html/css/vars.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +35 -2
- package/dist/html/dom.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +38 -1
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +11 -2
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +306 -25
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +61 -6
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +65 -16
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +2 -2
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +8 -3
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/floats.d.ts +4 -0
- package/dist/html/layout/floats.d.ts.map +1 -1
- package/dist/html/layout/floats.js +26 -0
- package/dist/html/layout/floats.js.map +1 -1
- package/dist/html/layout/inline.d.ts +29 -2
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +1454 -125
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +33 -0
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +608 -86
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +53 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +746 -214
- package/dist/html/paint.js.map +1 -1
- package/dist/html/surfaces.d.ts +7 -2
- package/dist/html/surfaces.d.ts.map +1 -1
- package/dist/html/surfaces.js +16 -0
- package/dist/html/surfaces.js.map +1 -1
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +27 -20
- package/dist/html/svg.js.map +1 -1
- package/dist/internal/codelines.d.ts +42 -0
- package/dist/internal/codelines.d.ts.map +1 -0
- package/dist/internal/codelines.js +170 -0
- package/dist/internal/codelines.js.map +1 -0
- package/dist/internal/window.d.ts +22 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +35 -8
- package/dist/internal/window.js.map +1 -1
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +98 -26
- package/dist/markdown/index.js.map +1 -1
- package/dist/rich-text-editor/index.d.ts.map +1 -1
- package/dist/rich-text-editor/index.js +3 -8
- package/dist/rich-text-editor/index.js.map +1 -1
- package/dist/rich-text-editor/markdown.d.ts.map +1 -1
- package/dist/rich-text-editor/markdown.js +30 -1
- package/dist/rich-text-editor/markdown.js.map +1 -1
- package/dist/rich-text-editor/replace.d.ts +13 -0
- package/dist/rich-text-editor/replace.d.ts.map +1 -0
- package/dist/rich-text-editor/replace.js +64 -0
- package/dist/rich-text-editor/replace.js.map +1 -0
- package/dist/richtext/node.d.ts +7 -0
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/terminal-output/index.d.ts.map +1 -1
- package/dist/terminal-output/index.js +100 -25
- package/dist/terminal-output/index.js.map +1 -1
- package/dist/terminal-output/runs.d.ts +4 -1
- package/dist/terminal-output/runs.d.ts.map +1 -1
- package/dist/terminal-output/runs.js +5 -1
- package/dist/terminal-output/runs.js.map +1 -1
- package/package.json +5 -3
- package/src/code/index.ts +34 -32
- package/src/code-language/index.ts +1 -1
- package/src/code-language/runs.ts +111 -1
- package/src/codeblock/index.ts +48 -1
- package/src/flow/node.ts +80 -64
- package/src/html/controls.ts +50 -9
- package/src/html/css/cascade.ts +134 -43
- package/src/html/css/color.ts +87 -0
- package/src/html/css/parse.ts +107 -4
- package/src/html/css/style.ts +386 -23
- package/src/html/css/ua.ts +22 -5
- package/src/html/css/values.ts +31 -3
- package/src/html/css/vars.ts +2 -2
- package/src/html/dom.ts +38 -2
- package/src/html/index.ts +39 -1
- package/src/html/layout/block.ts +360 -22
- package/src/html/layout/boxes.ts +98 -23
- package/src/html/layout/cache.ts +8 -3
- package/src/html/layout/floats.ts +24 -0
- package/src/html/layout/inline.ts +1690 -119
- package/src/html/node.ts +648 -101
- package/src/html/paint.ts +883 -283
- package/src/html/surfaces.ts +21 -1
- package/src/html/svg.ts +30 -19
- package/src/internal/codelines.ts +200 -0
- package/src/internal/window.ts +36 -8
- package/src/markdown/index.ts +145 -26
- package/src/rich-text-editor/index.ts +3 -11
- package/src/rich-text-editor/markdown.ts +36 -1
- package/src/rich-text-editor/replace.ts +85 -0
- package/src/richtext/node.ts +7 -0
- package/src/terminal-output/index.ts +127 -34
- package/src/terminal-output/runs.ts +16 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-x11/components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Components for react-x11 that do not belong in the core package",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -164,6 +164,7 @@
|
|
|
164
164
|
"docs": "npm --prefix website start",
|
|
165
165
|
"docs:build": "npm --prefix website run build",
|
|
166
166
|
"bench:zengarden": "node --import tsx scripts/zengarden/run.ts",
|
|
167
|
+
"bench:zengarden:scroll": "node --import tsx scripts/zengarden/scroll.tsx",
|
|
167
168
|
"examples:browser": "node --import tsx examples/browser/browser.tsx",
|
|
168
169
|
"examples:calendar": "node --import tsx examples/calendar.tsx",
|
|
169
170
|
"examples:charts": "node --import tsx examples/charts.tsx",
|
|
@@ -198,6 +199,7 @@
|
|
|
198
199
|
"prepack": "npm run build"
|
|
199
200
|
},
|
|
200
201
|
"dependencies": {
|
|
202
|
+
"bidi-js": "^1.0.3",
|
|
201
203
|
"css-select": "^7.0.0",
|
|
202
204
|
"domhandler": "^6.0.0",
|
|
203
205
|
"domutils": "^4.0.0",
|
|
@@ -219,7 +221,7 @@
|
|
|
219
221
|
"@lydell/node-pty": "^1.0.0",
|
|
220
222
|
"node-pty": "^1.0.0",
|
|
221
223
|
"react": "^19.0.0",
|
|
222
|
-
"react-x11": "^2.
|
|
224
|
+
"react-x11": "^2.26.0"
|
|
223
225
|
},
|
|
224
226
|
"peerDependenciesMeta": {
|
|
225
227
|
"@lezer/highlight": {
|
|
@@ -246,7 +248,7 @@
|
|
|
246
248
|
"pngjs": "^7.0.0",
|
|
247
249
|
"prettier": "^3.6.0",
|
|
248
250
|
"react": "^19.2.8",
|
|
249
|
-
"react-x11": "^2.
|
|
251
|
+
"react-x11": "^2.26.0",
|
|
250
252
|
"tsx": "^4.23.1",
|
|
251
253
|
"typescript": "^7.0.2",
|
|
252
254
|
"y-prosemirror": "^1.3.7",
|
package/src/code/index.ts
CHANGED
|
@@ -17,21 +17,18 @@ import type { ReactElement } from 'react';
|
|
|
17
17
|
import { useTheme } from 'react-x11';
|
|
18
18
|
import type { Style } from 'react-x11/style';
|
|
19
19
|
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
RICHTEXT_ELEMENT,
|
|
23
|
-
useSelectionMenu,
|
|
24
|
-
} from '../richtext/index.js';
|
|
25
|
-
import type { RichTextProps, TextRun } from '../richtext/index.js';
|
|
20
|
+
import { registerRichText, useSelectionMenu } from '../richtext/index.js';
|
|
21
|
+
import type { TextRun } from '../richtext/index.js';
|
|
26
22
|
import {
|
|
27
23
|
codeBlockLook,
|
|
28
|
-
|
|
24
|
+
CodeBlockRunCache,
|
|
29
25
|
codeBlockStyle,
|
|
30
26
|
codeTextStyle,
|
|
31
27
|
} from '../codeblock/index.js';
|
|
32
28
|
import type { Language, TokenStyles } from '../code-language/index.js';
|
|
33
29
|
import { hx } from './hx.js';
|
|
34
30
|
import { useFontPrewarm } from '../internal/prewarm.js';
|
|
31
|
+
import { CodeGutter, CodeLines } from '../internal/codelines.js';
|
|
35
32
|
|
|
36
33
|
const h = React.createElement;
|
|
37
34
|
|
|
@@ -107,39 +104,44 @@ export function Code(props: CodeProps): ReactElement {
|
|
|
107
104
|
);
|
|
108
105
|
useFontPrewarm(look.family);
|
|
109
106
|
|
|
107
|
+
// Kept between renders, so a source that changes at its end is tokenized
|
|
108
|
+
// again from the line that changed (`CodeBlockRunCache`).
|
|
109
|
+
const runCache = React.useRef<CodeBlockRunCache | null>(null);
|
|
110
|
+
runCache.current ??= new CodeBlockRunCache();
|
|
111
|
+
React.useEffect(() => () => runCache.current?.dispose(), []);
|
|
110
112
|
const runs: TextRun[] = React.useMemo(
|
|
111
|
-
() =>
|
|
113
|
+
() =>
|
|
114
|
+
runCache.current!.runs(source, look, { lang, language, resolveLanguage }),
|
|
112
115
|
[source, lang, language, resolveLanguage, look],
|
|
113
116
|
);
|
|
114
117
|
|
|
115
118
|
const lineCount = React.useMemo(() => source.split('\n').length, [source]);
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
// In blocks of lines, so a change lays out the blocks it touches
|
|
121
|
+
// (`../internal/codelines.ts`).
|
|
122
|
+
const code = h(CodeLines, { runs, style: codeTextStyle(look, wrap), wrap });
|
|
119
123
|
|
|
120
124
|
const gutter = lineNumbers
|
|
121
|
-
?
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
})),
|
|
133
|
-
wrap: false,
|
|
134
|
-
style: {
|
|
135
|
-
lineHeight: look.lineHeight,
|
|
136
|
-
textAlign: 'right',
|
|
137
|
-
minWidth:
|
|
138
|
-
Math.max(String(lineCount).length, 2) * Math.ceil(look.size * 0.62),
|
|
139
|
-
marginRight: look.padding,
|
|
140
|
-
flexShrink: 0,
|
|
125
|
+
? hx(
|
|
126
|
+
'box',
|
|
127
|
+
{
|
|
128
|
+
style: {
|
|
129
|
+
flexDirection: 'column',
|
|
130
|
+
minWidth:
|
|
131
|
+
Math.max(String(lineCount).length, 2) *
|
|
132
|
+
Math.ceil(look.size * 0.62),
|
|
133
|
+
marginRight: look.padding,
|
|
134
|
+
flexShrink: 0,
|
|
135
|
+
},
|
|
141
136
|
},
|
|
142
|
-
|
|
137
|
+
// numbers in the same font and line height as the code, blocked the
|
|
138
|
+
// same way, so the two stay in register
|
|
139
|
+
h(CodeGutter, {
|
|
140
|
+
lines: lineCount,
|
|
141
|
+
run: { family: look.family, size: look.size, color: look.dim },
|
|
142
|
+
style: { lineHeight: look.lineHeight, textAlign: 'right' },
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
143
145
|
: null;
|
|
144
146
|
|
|
145
147
|
const rootStyle: Style = {
|
|
@@ -170,7 +172,7 @@ export function Code(props: CodeProps): ReactElement {
|
|
|
170
172
|
hx(
|
|
171
173
|
'box',
|
|
172
174
|
{ style: { overflow: 'scroll', flexDirection: 'column', flexGrow: 1 } },
|
|
173
|
-
|
|
175
|
+
code,
|
|
174
176
|
),
|
|
175
177
|
);
|
|
176
178
|
}
|
|
@@ -34,7 +34,7 @@ export {
|
|
|
34
34
|
} from './theme.js';
|
|
35
35
|
|
|
36
36
|
export { languageForTag, tokenizeText } from './registry.js';
|
|
37
|
-
export { codeRuns } from './runs.js';
|
|
37
|
+
export { codeRuns, CodeRunCache } from './runs.js';
|
|
38
38
|
export type { CodeRun, CodeRunOptions } from './runs.js';
|
|
39
39
|
|
|
40
40
|
export { TOKEN_FALLBACK } from './types.js';
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// AGENTS.md on tree-shaking, and `textmate.ts` for the same shape.
|
|
24
24
|
import { languageForTag, tokenizeText } from './registry.js';
|
|
25
25
|
import { tokenStyleFor } from './theme.js';
|
|
26
|
-
import type { Language, TokenStyles } from './types.js';
|
|
26
|
+
import type { Language, Token, Tokenizer, TokenStyles } from './types.js';
|
|
27
27
|
|
|
28
28
|
/** One styled run of code. Structurally a `<richtext>` run. */
|
|
29
29
|
export interface CodeRun {
|
|
@@ -126,3 +126,113 @@ function push(
|
|
|
126
126
|
out.push(run);
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `codeRuns`, kept between calls for a text that changes a little at a
|
|
132
|
+
* time: a fence being streamed, a source being appended to.
|
|
133
|
+
*
|
|
134
|
+
* One tokenizer lives as long as the cache. It is told which lines changed
|
|
135
|
+
* (`Tokenizer.edit`, the interface `<CodeEditor>` already drives) and asked
|
|
136
|
+
* again only for those and the lines after them its state has to walk
|
|
137
|
+
* through, and every other line keeps the runs it had. `codeRuns` tokenized
|
|
138
|
+
* the whole text afresh: at 5,000 lines, most of the time an appended line
|
|
139
|
+
* took on the client.
|
|
140
|
+
*
|
|
141
|
+
* The runs are the same text in the same colours `codeRuns` gives, cut a
|
|
142
|
+
* little differently: a line's runs never reach into the next, and each
|
|
143
|
+
* newline is a run of its own, so that a line keeps its runs while others
|
|
144
|
+
* change. `decorate` is applied to every run once, when it is made — the
|
|
145
|
+
* family and size a block of code is set in, say.
|
|
146
|
+
*
|
|
147
|
+
* Call `dispose()` when done with it: an engine may keep timers.
|
|
148
|
+
*/
|
|
149
|
+
export class CodeRunCache<R = CodeRun> {
|
|
150
|
+
private key: readonly unknown[] | null = null;
|
|
151
|
+
private tokenizer: Tokenizer | null = null;
|
|
152
|
+
private lines: string[] = [];
|
|
153
|
+
private tokens: (readonly Token[] | undefined)[] = [];
|
|
154
|
+
private lineRuns: (R[] | undefined)[] = [];
|
|
155
|
+
private newline: R | null = null;
|
|
156
|
+
|
|
157
|
+
constructor(
|
|
158
|
+
private readonly decorate: (run: CodeRun) => R = (run) => run as R,
|
|
159
|
+
) {}
|
|
160
|
+
|
|
161
|
+
runs(text: string, tag: string, opts: CodeRunOptions): R[] {
|
|
162
|
+
if (text.length === 0)
|
|
163
|
+
return [this.decorate({ text: '', color: opts.color })];
|
|
164
|
+
const language =
|
|
165
|
+
opts.language ??
|
|
166
|
+
(tag ? (opts.resolveLanguage?.(tag) ?? languageForTag(tag)) : null);
|
|
167
|
+
if (!language) return [this.decorate({ text, color: opts.color })];
|
|
168
|
+
|
|
169
|
+
const key = [language, opts.styles, opts.color, opts.resolveToken];
|
|
170
|
+
if (!this.key || key.some((part, i) => part !== this.key![i])) {
|
|
171
|
+
this.dispose();
|
|
172
|
+
this.key = key;
|
|
173
|
+
this.lines = [];
|
|
174
|
+
this.tokens = [];
|
|
175
|
+
this.lineRuns = [];
|
|
176
|
+
this.newline = this.decorate({ text: '\n', color: opts.color });
|
|
177
|
+
this.tokenizer = language.createTokenizer({ invalidate: () => {} });
|
|
178
|
+
this.tokenizer.setLines(this.lines);
|
|
179
|
+
}
|
|
180
|
+
const tokenizer = this.tokenizer!;
|
|
181
|
+
|
|
182
|
+
// the lines the text shares with the last one, from the top: the edit
|
|
183
|
+
// is everything after them
|
|
184
|
+
const next = text.split('\n');
|
|
185
|
+
const lines = this.lines;
|
|
186
|
+
const shared = Math.min(lines.length, next.length);
|
|
187
|
+
let from = 0;
|
|
188
|
+
while (from < shared && lines[from] === next[from]) from++;
|
|
189
|
+
if (from < lines.length || from < next.length) {
|
|
190
|
+
const removed = lines.length - from;
|
|
191
|
+
const inserted = next.length - from;
|
|
192
|
+
lines.length = from;
|
|
193
|
+
for (let i = from; i < next.length; i++) lines.push(next[i]!);
|
|
194
|
+
tokenizer.edit({ fromLine: from, removed, inserted });
|
|
195
|
+
this.tokens.length = from;
|
|
196
|
+
this.lineRuns.length = from;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Asked in order from the top, so a stream engine walks its frontier
|
|
200
|
+
// line by line and answers each exactly, never from a guess.
|
|
201
|
+
const out: R[] = [];
|
|
202
|
+
for (let i = 0; i < lines.length; i++) {
|
|
203
|
+
if (i > 0) out.push(this.newline!);
|
|
204
|
+
const tokens = tokenizer.lineTokens(i);
|
|
205
|
+
let runs = this.lineRuns[i];
|
|
206
|
+
if (!runs || this.tokens[i] !== tokens) {
|
|
207
|
+
runs = lineRuns(lines[i]!, tokens, opts).map(this.decorate);
|
|
208
|
+
this.lineRuns[i] = runs;
|
|
209
|
+
this.tokens[i] = tokens;
|
|
210
|
+
}
|
|
211
|
+
for (const run of runs) out.push(run);
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
dispose(): void {
|
|
217
|
+
this.tokenizer?.dispose?.();
|
|
218
|
+
this.tokenizer = null;
|
|
219
|
+
this.key = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** One line's runs, as `codeRuns` makes them, up to its end. */
|
|
224
|
+
function lineRuns(
|
|
225
|
+
line: string,
|
|
226
|
+
tokens: readonly Token[],
|
|
227
|
+
opts: CodeRunOptions,
|
|
228
|
+
): CodeRun[] {
|
|
229
|
+
const out: CodeRun[] = [];
|
|
230
|
+
let at = 0;
|
|
231
|
+
for (const t of tokens) {
|
|
232
|
+
if (t.from > at) push(out, line.slice(at, t.from), undefined, opts);
|
|
233
|
+
push(out, line.slice(t.from, t.to), t.type, opts);
|
|
234
|
+
at = t.to;
|
|
235
|
+
}
|
|
236
|
+
if (at < line.length) push(out, line.slice(at), undefined, opts);
|
|
237
|
+
return out;
|
|
238
|
+
}
|
package/src/codeblock/index.ts
CHANGED
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
// A shared module, not a component: no element registration, no React, and
|
|
16
16
|
// nothing imported from either of its consumers (a component never imports
|
|
17
17
|
// another component).
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
codeRuns,
|
|
20
|
+
autoTokenStyles,
|
|
21
|
+
CodeRunCache,
|
|
22
|
+
} from '../code-language/index.js';
|
|
19
23
|
import type { Language, TokenStyles } from '../code-language/index.js';
|
|
20
24
|
import type { TextRun } from '../richtext/index.js';
|
|
21
25
|
import { tint } from 'react-x11/style';
|
|
@@ -130,6 +134,49 @@ export function codeBlockRuns(
|
|
|
130
134
|
}).map((run) => ({ ...run, family: look.family, size: look.size }));
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
/**
|
|
138
|
+
* `codeBlockRuns` kept between calls, for a block of code that changes a
|
|
139
|
+
* little at a time — a fence being streamed, a source being appended to:
|
|
140
|
+
* the tokenizer is told which lines changed and the rest keep their runs
|
|
141
|
+
* (`CodeRunCache`). Call `dispose()` when done with it.
|
|
142
|
+
*/
|
|
143
|
+
export class CodeBlockRunCache {
|
|
144
|
+
private cache: CodeRunCache<TextRun> | null = null;
|
|
145
|
+
private family: string | undefined;
|
|
146
|
+
private size: number | undefined;
|
|
147
|
+
|
|
148
|
+
runs(
|
|
149
|
+
source: string,
|
|
150
|
+
look: CodeBlockLook,
|
|
151
|
+
options: CodeBlockRunOptions = {},
|
|
152
|
+
): TextRun[] {
|
|
153
|
+
if (!this.cache || this.family !== look.family || this.size !== look.size) {
|
|
154
|
+
this.cache?.dispose();
|
|
155
|
+
const { family, size } = look;
|
|
156
|
+
this.family = family;
|
|
157
|
+
this.size = size;
|
|
158
|
+
this.cache = new CodeRunCache<TextRun>((run) => ({
|
|
159
|
+
...run,
|
|
160
|
+
family,
|
|
161
|
+
size,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
const { lang = '', language, resolveLanguage, highlight = true } = options;
|
|
165
|
+
return this.cache.runs(source, highlight ? lang : '', {
|
|
166
|
+
styles: look.styles,
|
|
167
|
+
color: look.color,
|
|
168
|
+
...(highlight && language ? { language } : null),
|
|
169
|
+
...(highlight && resolveLanguage ? { resolveLanguage } : null),
|
|
170
|
+
resolveToken: look.resolveToken,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
dispose(): void {
|
|
175
|
+
this.cache?.dispose();
|
|
176
|
+
this.cache = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
133
180
|
/** The block's own box: fill, radius, padding. What surrounds it — a row
|
|
134
181
|
* with a gutter, a column in a document — is the component's. */
|
|
135
182
|
export function codeBlockStyle(look: CodeBlockLook): Style {
|
package/src/flow/node.ts
CHANGED
|
@@ -215,8 +215,6 @@ interface PanelCanvas {
|
|
|
215
215
|
* global that wandered in would become an implicit `@types/node` dependency
|
|
216
216
|
* a consumer has to satisfy. */
|
|
217
217
|
const timers = globalThis as {
|
|
218
|
-
setInterval?(fn: () => void, ms: number): unknown;
|
|
219
|
-
clearInterval?(id: unknown): void;
|
|
220
218
|
setTimeout?(fn: () => void, ms: number): unknown;
|
|
221
219
|
clearTimeout?(id: unknown): void;
|
|
222
220
|
};
|
|
@@ -301,14 +299,28 @@ const VISUAL_PROPS = [
|
|
|
301
299
|
const clock = globalThis as { performance?: { now(): number } };
|
|
302
300
|
|
|
303
301
|
/**
|
|
304
|
-
* The pane's time: what a pan is stamped with,
|
|
305
|
-
* reads to decide it has held still
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
302
|
+
* The pane's time and the dashes' timer: what a pan is stamped with, what
|
|
303
|
+
* the dashes' march reads to decide it has held still, and what takes the
|
|
304
|
+
* march's steps. A test holds both (`holdClock` in test/held-clock.ts). On
|
|
305
|
+
* the real clock a runner that spends longer than the dashes' wait over one
|
|
306
|
+
* step of a pan lets them march in the middle of it, as they should, and
|
|
307
|
+
* one that stalls before the pan lets a tick claim the dashes the first step
|
|
308
|
+
* copies, which declines the copy. A test of the pan cannot tell either from
|
|
309
|
+
* a march it should not have made.
|
|
310
|
+
*
|
|
311
|
+
* The shape of the map's `glideClock`. Exported from this module and not
|
|
312
|
+
* from `./index.ts`: this is for the tests.
|
|
309
313
|
*/
|
|
310
314
|
export const flowClock = {
|
|
311
|
-
now
|
|
315
|
+
now(): number {
|
|
316
|
+
return clock.performance?.now() ?? Date.now();
|
|
317
|
+
},
|
|
318
|
+
arm(step: () => void, ms: number): unknown {
|
|
319
|
+
return timers.setTimeout?.(step, ms) ?? null;
|
|
320
|
+
},
|
|
321
|
+
disarm(handle: unknown): void {
|
|
322
|
+
timers.clearTimeout?.(handle);
|
|
323
|
+
},
|
|
312
324
|
};
|
|
313
325
|
const now = (): number => flowClock.now();
|
|
314
326
|
|
|
@@ -3230,67 +3242,71 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
3230
3242
|
|
|
3231
3243
|
private _startAnimation(): void {
|
|
3232
3244
|
if (this._animTimer != null) return;
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
// the ground the skipped ticks would have, so they slow down in
|
|
3273
|
-
// steps and not in speed
|
|
3274
|
-
const since = t - this._marchedAt;
|
|
3275
|
-
if (since < Math.max(ANIMATION_MS, cost) - 1) return;
|
|
3276
|
-
steps = Math.min(4, Math.max(1, Math.round(since / ANIMATION_MS)));
|
|
3245
|
+
const tick = (): void => {
|
|
3246
|
+
// A step at a time rather than an interval, so that a test can hold
|
|
3247
|
+
// the timer (`flowClock`), and armed again before anything else,
|
|
3248
|
+
// which is when an interval would have been.
|
|
3249
|
+
this._animTimer = flowClock.arm(tick, ANIMATION_MS);
|
|
3250
|
+
// A pan that blits copies the pane's pixels, dashes and all, and a
|
|
3251
|
+
// tick claims the dashes inside the band it copies — which declines
|
|
3252
|
+
// the copy: every frame a tick landed in repainted the whole pane,
|
|
3253
|
+
// a sixth of a pan's frames and all of its stutter. So the dashes
|
|
3254
|
+
// sit the pan out, phase and all, and what the pan copies and what
|
|
3255
|
+
// it draws agree; they march again once it has held still.
|
|
3256
|
+
//
|
|
3257
|
+
// And the view moving at all is what they wait out, not a blit that
|
|
3258
|
+
// happened: a tick declines the blit it lands beside, so a pan
|
|
3259
|
+
// whose frames ran past the wait had its ticks back, each one
|
|
3260
|
+
// cancelling the next blit and repainting the pane — over the
|
|
3261
|
+
// stress example's widgets in a large window, 2 frames a second,
|
|
3262
|
+
// ticks and pan steps taking turns. A step since the last tick, or
|
|
3263
|
+
// one within two ticks, holds the dashes still whatever a frame
|
|
3264
|
+
// costs. Under GL a tick is a uniform and blits nothing, and they
|
|
3265
|
+
// march through a pan as before.
|
|
3266
|
+
//
|
|
3267
|
+
// A 2D tick is a repaint of the box the dashes are in, which over a
|
|
3268
|
+
// dense graph in a large window is most of the pane: 75 ms a tick on
|
|
3269
|
+
// XQuartz, against a timer of 60. So the wait scales with what
|
|
3270
|
+
// frames cost: the view held still for two frames' worth, and one
|
|
3271
|
+
// tick every two frames at most — the dashes slow down before the
|
|
3272
|
+
// thread is theirs, and a pan whose frames come slowly still sees
|
|
3273
|
+
// them hold still.
|
|
3274
|
+
const t = now();
|
|
3275
|
+
const lastTick = this._tickAt;
|
|
3276
|
+
this._tickAt = t;
|
|
3277
|
+
if (t - this._blittedPanAt < ANIMATION_MS * 2) return;
|
|
3278
|
+
let steps = 1;
|
|
3279
|
+
if (!this._gl) {
|
|
3280
|
+
const cost = this._tickCost * 2;
|
|
3281
|
+
const still = Math.max(ANIMATION_MS * 2, cost);
|
|
3282
|
+
if (this._viewMovedAt > lastTick || t - this._viewMovedAt < still) {
|
|
3283
|
+
return;
|
|
3277
3284
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
// the
|
|
3281
|
-
//
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3285
|
+
// a tick under ANIMATION_MS apart, less a millisecond of timer
|
|
3286
|
+
// slop; further while ticks are dear — and then the dashes cover
|
|
3287
|
+
// the ground the skipped ticks would have, so they slow down in
|
|
3288
|
+
// steps and not in speed
|
|
3289
|
+
const since = t - this._marchedAt;
|
|
3290
|
+
if (since < Math.max(ANIMATION_MS, cost) - 1) return;
|
|
3291
|
+
steps = Math.min(4, Math.max(1, Math.round(since / ANIMATION_MS)));
|
|
3292
|
+
}
|
|
3293
|
+
this._marchedAt = t;
|
|
3294
|
+
this._dashPhase += ANIMATION_SPEED * steps;
|
|
3295
|
+
// the box the last paint saw animated edges in, not the pane: a
|
|
3296
|
+
// marching dash should not cost a full grid repaint per tick. And
|
|
3297
|
+
// only the part of it the pane shows — an edge on its way out of
|
|
3298
|
+
// the pane takes the box past it, and claimed whole, a tick
|
|
3299
|
+
// repainted everything the window has beside the graph.
|
|
3300
|
+
const box = this._animBox ? this._device(this._animBox) : this.abs;
|
|
3301
|
+
const shown = intersectRects(box, this.contentBox());
|
|
3302
|
+
if (shown) this.invalidate(false, shown, 'animation');
|
|
3303
|
+
};
|
|
3304
|
+
this._animTimer = flowClock.arm(tick, ANIMATION_MS);
|
|
3289
3305
|
}
|
|
3290
3306
|
|
|
3291
3307
|
private _stopAnimation(): void {
|
|
3292
3308
|
if (this._animTimer == null) return;
|
|
3293
|
-
|
|
3309
|
+
flowClock.disarm(this._animTimer);
|
|
3294
3310
|
this._animTimer = null;
|
|
3295
3311
|
}
|
|
3296
3312
|
|
package/src/html/controls.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type { Element } from 'domhandler';
|
|
|
24
24
|
import { attr, tagOf } from './dom.js';
|
|
25
25
|
import type { ComputedStyle } from './css/style.js';
|
|
26
26
|
import { isTransparent } from './css/values.js';
|
|
27
|
-
import type { BoxTree, ReplacedKind } from './layout/boxes.js';
|
|
27
|
+
import type { Box, BoxTree, ReplacedKind } from './layout/boxes.js';
|
|
28
28
|
import type { FontsLike } from './layout/inline.js';
|
|
29
29
|
|
|
30
30
|
/** The palette numbers a control's box has to reserve room for. */
|
|
@@ -48,15 +48,24 @@ export interface ControlRect {
|
|
|
48
48
|
width: number;
|
|
49
49
|
height: number;
|
|
50
50
|
/**
|
|
51
|
-
* Set on a text field whose own box the author styled —
|
|
52
|
-
* or a background (`styledField`). The document draws
|
|
53
|
-
* widget goes bare inside its content box, here, with
|
|
54
|
-
* of its own and its text in the element's colour and
|
|
51
|
+
* Set on a text field or a `<select>` whose own box the author styled —
|
|
52
|
+
* gave it a border or a background (`styledField`). The document draws
|
|
53
|
+
* that box, and the widget goes bare inside its content box, here, with
|
|
54
|
+
* no frame or fill of its own and its text in the element's colour and
|
|
55
|
+
* font.
|
|
55
56
|
*/
|
|
56
57
|
bare?: BareField;
|
|
58
|
+
/**
|
|
59
|
+
* How opaque the widget is drawn, where it is less than 1: the element's
|
|
60
|
+
* own `opacity` times every ancestor's, since each fades what is in it as
|
|
61
|
+
* a group. At 0 the widget is not seen and still takes a press, as the
|
|
62
|
+
* element does in a browser — the checkbox a CSS-only dropdown lays,
|
|
63
|
+
* invisible, over its label.
|
|
64
|
+
*/
|
|
65
|
+
opacity?: number;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
/** Where a styled
|
|
68
|
+
/** Where a styled field's widget goes, and how its text looks. */
|
|
60
69
|
export interface BareField {
|
|
61
70
|
x: number;
|
|
62
71
|
y: number;
|
|
@@ -65,6 +74,12 @@ export interface BareField {
|
|
|
65
74
|
color: string;
|
|
66
75
|
fontFamily: string;
|
|
67
76
|
fontSize: number;
|
|
77
|
+
/**
|
|
78
|
+
* A `<select>`'s: whether it draws its arrow. It does unless the page set
|
|
79
|
+
* `appearance: none`, which is how a page that draws its own arrow — as a
|
|
80
|
+
* background image, most often — says so.
|
|
81
|
+
*/
|
|
82
|
+
chevron?: boolean;
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
/** The rectangles every control in a laid-out document landed on. */
|
|
@@ -73,6 +88,8 @@ export function controlRectsOf(tree: BoxTree): ControlRect[] {
|
|
|
73
88
|
for (const box of tree.controls) {
|
|
74
89
|
if (!box.el) continue;
|
|
75
90
|
if (box.width <= 0 || box.height <= 0) continue;
|
|
91
|
+
// a hidden element draws nothing and takes no press (CSS 2.1 11.2)
|
|
92
|
+
if (box.style.visibility !== 'visible') continue;
|
|
76
93
|
const rect: ControlRect = {
|
|
77
94
|
element: box.el,
|
|
78
95
|
kind: box.replaced,
|
|
@@ -81,6 +98,11 @@ export function controlRectsOf(tree: BoxTree): ControlRect[] {
|
|
|
81
98
|
width: box.width,
|
|
82
99
|
height: box.height,
|
|
83
100
|
};
|
|
101
|
+
let opacity = 1;
|
|
102
|
+
for (let at: Box | null = box; at; at = at.parent) {
|
|
103
|
+
opacity *= at.style.opacity;
|
|
104
|
+
}
|
|
105
|
+
if (opacity < 1) rect.opacity = Math.max(0, opacity);
|
|
84
106
|
if (styledField(box.replaced, box.style)) {
|
|
85
107
|
rect.bare = {
|
|
86
108
|
x: box.contentX,
|
|
@@ -90,6 +112,9 @@ export function controlRectsOf(tree: BoxTree): ControlRect[] {
|
|
|
90
112
|
color: box.style.color,
|
|
91
113
|
fontFamily: box.style.fontFamily,
|
|
92
114
|
fontSize: box.style.fontSize,
|
|
115
|
+
...(box.replaced === 'select' && {
|
|
116
|
+
chevron: box.style.appearance !== 'none',
|
|
117
|
+
}),
|
|
93
118
|
};
|
|
94
119
|
}
|
|
95
120
|
out.push(rect);
|
|
@@ -98,17 +123,23 @@ export function controlRectsOf(tree: BoxTree): ControlRect[] {
|
|
|
98
123
|
}
|
|
99
124
|
|
|
100
125
|
/**
|
|
101
|
-
* Whether a
|
|
102
|
-
*
|
|
126
|
+
* Whether a field's box is the author's to draw: one given a border or a
|
|
127
|
+
* background of its own, which the UA sheet gives no control, or set to
|
|
103
128
|
* `appearance: none`, which says so outright. A browser drops a field's
|
|
104
129
|
* native look for the author's then (CSS UI 4 7.1, `appearance`), and so
|
|
105
130
|
* does this: the widget's frame and fill would hide the author's, and what
|
|
106
131
|
* they would draw is the theme's rather than the page's. `appearance: none`
|
|
107
132
|
* is how a design system writes every field it has — meetup.com's search
|
|
108
133
|
* pill holds two with no background, only one of them with a border.
|
|
134
|
+
*
|
|
135
|
+
* A `<select>` is one too. A browser keeps its arrow when the page gave it a
|
|
136
|
+
* border or a background (Blink's `menulist-button`), and leaves that out
|
|
137
|
+
* as well at `appearance: none` — `BareField.chevron`.
|
|
109
138
|
*/
|
|
110
139
|
export function styledField(kind: ReplacedKind, style: ComputedStyle): boolean {
|
|
111
|
-
if (kind !== 'input' && kind !== 'textarea')
|
|
140
|
+
if (kind !== 'input' && kind !== 'textarea' && kind !== 'select') {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
112
143
|
return (
|
|
113
144
|
style.appearance === 'none' ||
|
|
114
145
|
!isTransparent(style.backgroundColor) ||
|
|
@@ -168,6 +199,16 @@ export function measureControl(
|
|
|
168
199
|
}
|
|
169
200
|
case 'select': {
|
|
170
201
|
const widest = optionWidths(el);
|
|
202
|
+
// a select the page styled is its widest option and its arrow, and
|
|
203
|
+
// the room around them is the page's
|
|
204
|
+
if (styledField(kind, style)) {
|
|
205
|
+
return {
|
|
206
|
+
width: Math.round(
|
|
207
|
+
ch * widest + (style.appearance === 'none' ? 0 : em),
|
|
208
|
+
),
|
|
209
|
+
height: lineHeight,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
171
212
|
// The room for the chevron is the widget's, not the document's, but
|
|
172
213
|
// the document has to reserve it or the last letter of the widest
|
|
173
214
|
// option sits under it.
|