@react-x11/components 0.8.0 → 0.9.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 +95 -34
- package/dist/charts/node.d.ts +1 -1
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +6 -2
- package/dist/charts/node.js.map +1 -1
- package/dist/embed/client.d.ts +48 -3
- package/dist/embed/client.d.ts.map +1 -1
- package/dist/embed/client.js +79 -5
- package/dist/embed/client.js.map +1 -1
- package/dist/embed/index.d.ts +1 -1
- package/dist/embed/index.d.ts.map +1 -1
- package/dist/embed/index.js +1 -1
- package/dist/embed/index.js.map +1 -1
- package/dist/html/node.d.ts +3 -1
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +7 -5
- package/dist/html/node.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/{markdown → internal/markdown}/ast.d.ts +12 -0
- package/dist/internal/markdown/ast.d.ts.map +1 -0
- package/dist/internal/markdown/ast.js +22 -0
- package/dist/internal/markdown/ast.js.map +1 -0
- package/dist/internal/markdown/parse.d.ts.map +1 -0
- package/dist/{markdown → internal/markdown}/parse.js +112 -22
- package/dist/internal/markdown/parse.js.map +1 -0
- package/dist/internal/markdown/stringify.d.ts +4 -0
- package/dist/internal/markdown/stringify.d.ts.map +1 -0
- package/dist/internal/markdown/stringify.js +684 -0
- package/dist/internal/markdown/stringify.js.map +1 -0
- package/dist/internal/markdown/tags.d.ts.map +1 -0
- package/dist/internal/markdown/tags.js.map +1 -0
- package/dist/internal/scroll.d.ts +3 -2
- package/dist/internal/scroll.d.ts.map +1 -1
- package/dist/internal/scroll.js +1 -0
- package/dist/internal/scroll.js.map +1 -1
- package/dist/maps/anchors.d.ts +98 -0
- package/dist/maps/anchors.d.ts.map +1 -0
- package/dist/maps/anchors.js +531 -0
- package/dist/maps/anchors.js.map +1 -0
- package/dist/maps/controller.d.ts +283 -0
- package/dist/maps/controller.d.ts.map +1 -0
- package/dist/maps/controller.js +743 -0
- package/dist/maps/controller.js.map +1 -0
- package/dist/maps/gl/buckets.d.ts +95 -0
- package/dist/maps/gl/buckets.d.ts.map +1 -0
- package/dist/maps/gl/buckets.js +362 -0
- package/dist/maps/gl/buckets.js.map +1 -0
- package/dist/maps/gl/build-worker.d.ts +13 -0
- package/dist/maps/gl/build-worker.d.ts.map +1 -0
- package/dist/maps/gl/build-worker.js +44 -0
- package/dist/maps/gl/build-worker.js.map +1 -0
- package/dist/maps/gl/color.d.ts +8 -0
- package/dist/maps/gl/color.d.ts.map +1 -0
- package/dist/maps/gl/color.js +57 -0
- package/dist/maps/gl/color.js.map +1 -0
- package/dist/maps/gl/cover.d.ts +51 -0
- package/dist/maps/gl/cover.d.ts.map +1 -0
- package/dist/maps/gl/cover.js +124 -0
- package/dist/maps/gl/cover.js.map +1 -0
- package/dist/maps/gl/index.d.ts +3 -0
- package/dist/maps/gl/index.d.ts.map +1 -0
- package/dist/maps/gl/index.js +20 -0
- package/dist/maps/gl/index.js.map +1 -0
- package/dist/maps/gl/markers.d.ts +28 -0
- package/dist/maps/gl/markers.d.ts.map +1 -0
- package/dist/maps/gl/markers.js +60 -0
- package/dist/maps/gl/markers.js.map +1 -0
- package/dist/maps/gl/overlays.d.ts +68 -0
- package/dist/maps/gl/overlays.d.ts.map +1 -0
- package/dist/maps/gl/overlays.js +300 -0
- package/dist/maps/gl/overlays.js.map +1 -0
- package/dist/maps/gl/pane.d.ts +12 -0
- package/dist/maps/gl/pane.d.ts.map +1 -0
- package/dist/maps/gl/pane.js +37 -0
- package/dist/maps/gl/pane.js.map +1 -0
- package/dist/maps/gl/placement.d.ts +111 -0
- package/dist/maps/gl/placement.d.ts.map +1 -0
- package/dist/maps/gl/placement.js +567 -0
- package/dist/maps/gl/placement.js.map +1 -0
- package/dist/maps/gl/renderer.d.ts +352 -0
- package/dist/maps/gl/renderer.d.ts.map +1 -0
- package/dist/maps/gl/renderer.js +1127 -0
- package/dist/maps/gl/renderer.js.map +1 -0
- package/dist/maps/gl/shaders.d.ts +120 -0
- package/dist/maps/gl/shaders.d.ts.map +1 -0
- package/dist/maps/gl/shaders.js +451 -0
- package/dist/maps/gl/shaders.js.map +1 -0
- package/dist/maps/gl/store.d.ts +115 -0
- package/dist/maps/gl/store.d.ts.map +1 -0
- package/dist/maps/gl/store.js +469 -0
- package/dist/maps/gl/store.js.map +1 -0
- package/dist/maps/gl/text.d.ts +154 -0
- package/dist/maps/gl/text.d.ts.map +1 -0
- package/dist/maps/gl/text.js +382 -0
- package/dist/maps/gl/text.js.map +1 -0
- package/dist/maps/gl/view.d.ts +54 -0
- package/dist/maps/gl/view.d.ts.map +1 -0
- package/dist/maps/gl/view.js +974 -0
- package/dist/maps/gl/view.js.map +1 -0
- package/dist/maps/index.d.ts +1 -1
- package/dist/maps/index.d.ts.map +1 -1
- package/dist/maps/index.js +216 -52
- package/dist/maps/index.js.map +1 -1
- package/dist/maps/labels.d.ts +37 -16
- package/dist/maps/labels.d.ts.map +1 -1
- package/dist/maps/labels.js +188 -119
- package/dist/maps/labels.js.map +1 -1
- package/dist/maps/node.d.ts +162 -56
- package/dist/maps/node.d.ts.map +1 -1
- package/dist/maps/node.js +607 -515
- package/dist/maps/node.js.map +1 -1
- package/dist/maps/overlay.d.ts +72 -9
- package/dist/maps/overlay.d.ts.map +1 -1
- package/dist/maps/overlay.js +108 -28
- package/dist/maps/overlay.js.map +1 -1
- package/dist/maps/paint.d.ts +3 -0
- package/dist/maps/paint.d.ts.map +1 -1
- package/dist/maps/paint.js.map +1 -1
- package/dist/maps/proj.d.ts +14 -0
- package/dist/maps/proj.d.ts.map +1 -1
- package/dist/maps/proj.js +28 -0
- package/dist/maps/proj.js.map +1 -1
- package/dist/maps/renderer.d.ts +69 -0
- package/dist/maps/renderer.d.ts.map +1 -0
- package/dist/maps/renderer.js +66 -0
- package/dist/maps/renderer.js.map +1 -0
- package/dist/maps/sources.d.ts +25 -3
- package/dist/maps/sources.d.ts.map +1 -1
- package/dist/maps/sources.js +17 -0
- package/dist/maps/sources.js.map +1 -1
- package/dist/maps/theme.d.ts +19 -0
- package/dist/maps/theme.d.ts.map +1 -0
- package/dist/maps/theme.js +44 -0
- package/dist/maps/theme.js.map +1 -0
- package/dist/maps/tiles.d.ts +117 -17
- package/dist/maps/tiles.d.ts.map +1 -1
- package/dist/maps/tiles.js +163 -30
- package/dist/maps/tiles.js.map +1 -1
- package/dist/maps/types.d.ts +188 -20
- package/dist/maps/types.d.ts.map +1 -1
- package/dist/markdown/expressions.d.ts +1 -1
- package/dist/markdown/expressions.d.ts.map +1 -1
- package/dist/markdown/expressions.js.map +1 -1
- package/dist/markdown/index.d.ts +7 -6
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +9 -7
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/spans.d.ts +1 -1
- package/dist/markdown/spans.d.ts.map +1 -1
- package/dist/media-player/index.d.ts +11 -3
- package/dist/media-player/index.d.ts.map +1 -1
- package/dist/media-player/index.js +13 -2
- package/dist/media-player/index.js.map +1 -1
- package/dist/rich-text-editor/clipboard.d.ts +23 -0
- package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
- package/dist/rich-text-editor/clipboard.js +307 -0
- package/dist/rich-text-editor/clipboard.js.map +1 -0
- package/dist/rich-text-editor/collab.d.ts +32 -0
- package/dist/rich-text-editor/collab.d.ts.map +1 -0
- package/dist/rich-text-editor/collab.js +55 -0
- package/dist/rich-text-editor/collab.js.map +1 -0
- package/dist/rich-text-editor/commands.d.ts +50 -0
- package/dist/rich-text-editor/commands.d.ts.map +1 -0
- package/dist/rich-text-editor/commands.js +376 -0
- package/dist/rich-text-editor/commands.js.map +1 -0
- package/dist/rich-text-editor/drag.d.ts +68 -0
- package/dist/rich-text-editor/drag.d.ts.map +1 -0
- package/dist/rich-text-editor/drag.js +148 -0
- package/dist/rich-text-editor/drag.js.map +1 -0
- package/dist/rich-text-editor/html.d.ts +145 -0
- package/dist/rich-text-editor/html.d.ts.map +1 -0
- package/dist/rich-text-editor/html.js +516 -0
- package/dist/rich-text-editor/html.js.map +1 -0
- package/dist/rich-text-editor/index.d.ts +263 -0
- package/dist/rich-text-editor/index.d.ts.map +1 -0
- package/dist/rich-text-editor/index.js +822 -0
- package/dist/rich-text-editor/index.js.map +1 -0
- package/dist/rich-text-editor/inline.d.ts +94 -0
- package/dist/rich-text-editor/inline.d.ts.map +1 -0
- package/dist/rich-text-editor/inline.js +252 -0
- package/dist/rich-text-editor/inline.js.map +1 -0
- package/dist/rich-text-editor/keymap.d.ts +71 -0
- package/dist/rich-text-editor/keymap.d.ts.map +1 -0
- package/dist/rich-text-editor/keymap.js +393 -0
- package/dist/rich-text-editor/keymap.js.map +1 -0
- package/dist/rich-text-editor/keys.d.ts +29 -0
- package/dist/rich-text-editor/keys.d.ts.map +1 -0
- package/dist/rich-text-editor/keys.js +93 -0
- package/dist/rich-text-editor/keys.js.map +1 -0
- package/dist/rich-text-editor/look.d.ts +71 -0
- package/dist/rich-text-editor/look.d.ts.map +1 -0
- package/dist/rich-text-editor/look.js +342 -0
- package/dist/rich-text-editor/look.js.map +1 -0
- package/dist/rich-text-editor/markdown.d.ts +18 -0
- package/dist/rich-text-editor/markdown.d.ts.map +1 -0
- package/dist/rich-text-editor/markdown.js +496 -0
- package/dist/rich-text-editor/markdown.js.map +1 -0
- package/dist/rich-text-editor/nodes.d.ts +135 -0
- package/dist/rich-text-editor/nodes.d.ts.map +1 -0
- package/dist/rich-text-editor/nodes.js +397 -0
- package/dist/rich-text-editor/nodes.js.map +1 -0
- package/dist/rich-text-editor/render.d.ts +68 -0
- package/dist/rich-text-editor/render.d.ts.map +1 -0
- package/dist/rich-text-editor/render.js +500 -0
- package/dist/rich-text-editor/render.js.map +1 -0
- package/dist/rich-text-editor/schema.d.ts +16 -0
- package/dist/rich-text-editor/schema.d.ts.map +1 -0
- package/dist/rich-text-editor/schema.js +386 -0
- package/dist/rich-text-editor/schema.js.map +1 -0
- package/dist/rich-text-editor/store.d.ts +11 -0
- package/dist/rich-text-editor/store.d.ts.map +1 -0
- package/dist/rich-text-editor/store.js +43 -0
- package/dist/rich-text-editor/store.js.map +1 -0
- package/dist/rich-text-editor/suggest.d.ts +127 -0
- package/dist/rich-text-editor/suggest.d.ts.map +1 -0
- package/dist/rich-text-editor/suggest.js +0 -0
- package/dist/rich-text-editor/suggest.js.map +1 -0
- package/dist/rich-text-editor/tables.d.ts +41 -0
- package/dist/rich-text-editor/tables.d.ts.map +1 -0
- package/dist/rich-text-editor/tables.js +204 -0
- package/dist/rich-text-editor/tables.js.map +1 -0
- package/dist/rich-text-editor/toolbar.d.ts +58 -0
- package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
- package/dist/rich-text-editor/toolbar.js +548 -0
- package/dist/rich-text-editor/toolbar.js.map +1 -0
- package/dist/rich-text-editor/view.d.ts +368 -0
- package/dist/rich-text-editor/view.d.ts.map +1 -0
- package/dist/rich-text-editor/view.js +1968 -0
- package/dist/rich-text-editor/view.js.map +1 -0
- package/dist/rich-text-editor/virtual.d.ts +54 -0
- package/dist/rich-text-editor/virtual.d.ts.map +1 -0
- package/dist/rich-text-editor/virtual.js +262 -0
- package/dist/rich-text-editor/virtual.js.map +1 -0
- package/dist/richtext/node.d.ts +25 -2
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +46 -24
- package/dist/richtext/node.js.map +1 -1
- package/dist/terminal/backends.d.ts +4 -1
- package/dist/terminal/backends.d.ts.map +1 -1
- package/dist/terminal/backends.js.map +1 -1
- package/dist/terminal/index.d.ts +18 -7
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +28 -11
- package/dist/terminal/index.js.map +1 -1
- package/package.json +23 -4
- package/src/charts/node.ts +6 -2
- package/src/embed/client.ts +102 -8
- package/src/embed/index.ts +2 -0
- package/src/html/node.ts +7 -5
- package/src/index.ts +2 -0
- package/src/{markdown → internal/markdown}/ast.ts +24 -7
- package/src/{markdown → internal/markdown}/parse.ts +134 -24
- package/src/internal/markdown/stringify.ts +772 -0
- package/src/internal/scroll.ts +5 -3
- package/src/maps/anchors.ts +607 -0
- package/src/maps/controller.ts +999 -0
- package/src/maps/gl/buckets.ts +457 -0
- package/src/maps/gl/build-worker.ts +55 -0
- package/src/maps/gl/color.ts +62 -0
- package/src/maps/gl/cover.ts +170 -0
- package/src/maps/gl/index.ts +23 -0
- package/src/maps/gl/markers.ts +83 -0
- package/src/maps/gl/overlays.ts +379 -0
- package/src/maps/gl/pane.ts +42 -0
- package/src/maps/gl/placement.ts +816 -0
- package/src/maps/gl/renderer.ts +1576 -0
- package/src/maps/gl/shaders.ts +486 -0
- package/src/maps/gl/store.ts +566 -0
- package/src/maps/gl/text.ts +495 -0
- package/src/maps/gl/view.ts +1224 -0
- package/src/maps/index.ts +270 -81
- package/src/maps/labels.ts +214 -121
- package/src/maps/node.ts +763 -629
- package/src/maps/overlay.ts +148 -43
- package/src/maps/paint.ts +3 -0
- package/src/maps/proj.ts +31 -0
- package/src/maps/renderer.ts +155 -0
- package/src/maps/sources.ts +38 -3
- package/src/maps/theme.ts +51 -0
- package/src/maps/tiles.ts +191 -33
- package/src/maps/types.ts +188 -20
- package/src/markdown/expressions.ts +5 -1
- package/src/markdown/index.ts +14 -11
- package/src/markdown/spans.ts +1 -1
- package/src/media-player/index.ts +18 -4
- package/src/rich-text-editor/clipboard.ts +439 -0
- package/src/rich-text-editor/collab.ts +91 -0
- package/src/rich-text-editor/commands.ts +448 -0
- package/src/rich-text-editor/dom-globals.d.ts +23 -0
- package/src/rich-text-editor/drag.ts +211 -0
- package/src/rich-text-editor/html.ts +619 -0
- package/src/rich-text-editor/index.ts +1331 -0
- package/src/rich-text-editor/inline.ts +345 -0
- package/src/rich-text-editor/keymap.ts +510 -0
- package/src/rich-text-editor/keys.ts +119 -0
- package/src/rich-text-editor/look.ts +427 -0
- package/src/rich-text-editor/markdown.ts +653 -0
- package/src/rich-text-editor/nodes.ts +497 -0
- package/src/rich-text-editor/render.ts +800 -0
- package/src/rich-text-editor/schema.ts +431 -0
- package/src/rich-text-editor/store.ts +39 -0
- package/src/rich-text-editor/suggest.ts +597 -0
- package/src/rich-text-editor/tables.ts +272 -0
- package/src/rich-text-editor/toolbar.ts +724 -0
- package/src/rich-text-editor/view.ts +2287 -0
- package/src/rich-text-editor/virtual.ts +347 -0
- package/src/richtext/node.ts +59 -30
- package/src/terminal/backends.ts +4 -1
- package/src/terminal/index.ts +43 -15
- package/dist/markdown/ast.d.ts.map +0 -1
- package/dist/markdown/ast.js +0 -17
- package/dist/markdown/ast.js.map +0 -1
- package/dist/markdown/parse.d.ts.map +0 -1
- package/dist/markdown/parse.js.map +0 -1
- package/dist/markdown/tags.d.ts.map +0 -1
- package/dist/markdown/tags.js.map +0 -1
- /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.js +0 -0
- /package/src/{markdown → internal/markdown}/tags.ts +0 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
// Labels, the text half: strings set by the platform's own text engine,
|
|
2
|
+
// read back as coverage, and packed into one GL texture.
|
|
3
|
+
//
|
|
4
|
+
// A `<glarea>` is stacked over every 2D thing in its window, so a label on a
|
|
5
|
+
// GL map has to be *in* the GL frame — there is no drawing text over it
|
|
6
|
+
// afterwards. The glyphs come from where every other string in the app
|
|
7
|
+
// comes from: `app.fonts.layout()` shapes it (CoreText on the Cocoa
|
|
8
|
+
// backend, ntk's own shaper on X11, font fallback and all), and the layout
|
|
9
|
+
// draws into an offscreen {@link Surface} whose pixels are read back. That
|
|
10
|
+
// is one readback per *batch* of labels, not per label, and each label is
|
|
11
|
+
// set once and drawn every frame after.
|
|
12
|
+
//
|
|
13
|
+
// A label is rasterized **whole**, not glyph by glyph: placement sets every
|
|
14
|
+
// line label on a straight stretch, so a label is one rigid piece of type —
|
|
15
|
+
// one textured quad, rotated as a unit — and shaping, kerning and scripts
|
|
16
|
+
// stay the text engine's business. What is stored is **coverage**, drawn in
|
|
17
|
+
// white and read from the alpha channel; colour and halo are the shader's
|
|
18
|
+
// (see `./shaders.ts`), so one raster serves every palette and a
|
|
19
|
+
// light-to-dark switch re-rasterizes nothing.
|
|
20
|
+
import { Surface } from 'react-x11/ntk';
|
|
21
|
+
|
|
22
|
+
/** A set string's box, in device pixels. */
|
|
23
|
+
export interface TextBox {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One string to set, at `size` device pixels. */
|
|
29
|
+
export interface TextItem {
|
|
30
|
+
text: string;
|
|
31
|
+
size: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A rasterized string: RGBA as read back, coverage in alpha, `pad`
|
|
35
|
+
* pixels of clear margin included. */
|
|
36
|
+
export interface TextRaster {
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
pixels: Uint8Array;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What the atlas asks of a text engine. */
|
|
43
|
+
export interface TextEngine {
|
|
44
|
+
/** The box `text` sets in, synchronously — placement needs it now. */
|
|
45
|
+
measure(text: string, size: number): TextBox;
|
|
46
|
+
/**
|
|
47
|
+
* Rasterize as many of `items` as fit one batch. Answers per item: a
|
|
48
|
+
* raster; `null` for one that can never be drawn (wider than the engine
|
|
49
|
+
* can set); `undefined` for one left for the next batch.
|
|
50
|
+
*/
|
|
51
|
+
rasterize(
|
|
52
|
+
items: readonly TextItem[],
|
|
53
|
+
pad: number,
|
|
54
|
+
): Promise<(TextRaster | null | undefined)[]>;
|
|
55
|
+
dispose(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** ntk's font cache, structurally — `../labels.ts`' `FontsLike`. */
|
|
59
|
+
interface FontsLike {
|
|
60
|
+
layout(
|
|
61
|
+
content: string,
|
|
62
|
+
style: Record<string, unknown>,
|
|
63
|
+
): {
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
draw(ctx: unknown, x: number, y: number): void;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ImageLike {
|
|
71
|
+
data: Uint8ClampedArray;
|
|
72
|
+
width: number;
|
|
73
|
+
height: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ContextLike {
|
|
77
|
+
getImageData(x: number, y: number, w: number, h: number): Promise<ImageLike>;
|
|
78
|
+
destroy?(): void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface SurfaceLike {
|
|
82
|
+
getContext(name: '2d'): ContextLike;
|
|
83
|
+
clear(): void;
|
|
84
|
+
destroy(): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The offscreen surface a batch is set on, in device pixels. A readback
|
|
88
|
+
* has to stay inside it — X answers a GetImage past a pixmap's edge with
|
|
89
|
+
* BadMatch — so a batch is whatever fits. */
|
|
90
|
+
const STAGING_WIDTH = 1024;
|
|
91
|
+
const STAGING_HEIGHT = 512;
|
|
92
|
+
/** Milliseconds of drawing one batch may take. A batch runs between two
|
|
93
|
+
* frames, and at 75 Hz the gap between them is a few milliseconds. */
|
|
94
|
+
const DRAW_BUDGET_MS = 1.5;
|
|
95
|
+
|
|
96
|
+
/** The text engine over an app's fonts and an offscreen surface. */
|
|
97
|
+
export class SurfaceTextEngine implements TextEngine {
|
|
98
|
+
private readonly _app: unknown;
|
|
99
|
+
private readonly _fonts: FontsLike;
|
|
100
|
+
private readonly _family: string;
|
|
101
|
+
private readonly _layouts = new Map<
|
|
102
|
+
string,
|
|
103
|
+
ReturnType<FontsLike['layout']>
|
|
104
|
+
>();
|
|
105
|
+
private _surface: SurfaceLike | null = null;
|
|
106
|
+
private _ctx: ContextLike | null = null;
|
|
107
|
+
|
|
108
|
+
constructor(app: unknown, fonts: FontsLike, family: string) {
|
|
109
|
+
this._app = app;
|
|
110
|
+
this._fonts = fonts;
|
|
111
|
+
this._family = family;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The engine for the app a node belongs to, or null where there is
|
|
115
|
+
* nothing to set text with. */
|
|
116
|
+
static forApp(app: unknown, family: string): SurfaceTextEngine | null {
|
|
117
|
+
const fonts = (app as { fonts?: FontsLike } | null | undefined)?.fonts;
|
|
118
|
+
if (!fonts || typeof fonts.layout !== 'function') return null;
|
|
119
|
+
return new SurfaceTextEngine(app, fonts, family);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private _layout(text: string, size: number): ReturnType<FontsLike['layout']> {
|
|
123
|
+
const key = `${size}|${text}`;
|
|
124
|
+
let layout = this._layouts.get(key);
|
|
125
|
+
if (!layout) {
|
|
126
|
+
// Bounded like the retained renderer's shaper: cleared wholesale, and
|
|
127
|
+
// what is on screen is set again once.
|
|
128
|
+
if (this._layouts.size > 8000) this._layouts.clear();
|
|
129
|
+
layout = this._fonts.layout(text, {
|
|
130
|
+
family: this._family,
|
|
131
|
+
size,
|
|
132
|
+
weight: 400,
|
|
133
|
+
style: 'normal',
|
|
134
|
+
color: '#ffffff',
|
|
135
|
+
});
|
|
136
|
+
this._layouts.set(key, layout);
|
|
137
|
+
}
|
|
138
|
+
return layout;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
measure(text: string, size: number): TextBox {
|
|
142
|
+
const layout = this._layout(text, size);
|
|
143
|
+
return { width: Math.ceil(layout.width), height: Math.ceil(layout.height) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async rasterize(
|
|
147
|
+
items: readonly TextItem[],
|
|
148
|
+
pad: number,
|
|
149
|
+
): Promise<(TextRaster | null | undefined)[]> {
|
|
150
|
+
const places: (
|
|
151
|
+
{ x: number; y: number; w: number; h: number } | null | undefined
|
|
152
|
+
)[] = [];
|
|
153
|
+
let x = 0;
|
|
154
|
+
let y = 0;
|
|
155
|
+
let row = 0;
|
|
156
|
+
for (const item of items) {
|
|
157
|
+
const box = this.measure(item.text, item.size);
|
|
158
|
+
const w = box.width + pad * 2;
|
|
159
|
+
const h = box.height + pad * 2;
|
|
160
|
+
if (w > STAGING_WIDTH || h > STAGING_HEIGHT) {
|
|
161
|
+
places.push(null);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (x + w > STAGING_WIDTH) {
|
|
165
|
+
x = 0;
|
|
166
|
+
y += row;
|
|
167
|
+
row = 0;
|
|
168
|
+
}
|
|
169
|
+
if (y + h > STAGING_HEIGHT) {
|
|
170
|
+
places.push(undefined);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
places.push({ x, y, w, h });
|
|
174
|
+
x += w;
|
|
175
|
+
row = Math.max(row, h);
|
|
176
|
+
}
|
|
177
|
+
if (!places.some((p) => p)) {
|
|
178
|
+
return places.map((p) => (p === null ? null : undefined));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!this._surface) {
|
|
182
|
+
this._surface = new Surface(this._app as never, {
|
|
183
|
+
width: STAGING_WIDTH,
|
|
184
|
+
height: STAGING_HEIGHT,
|
|
185
|
+
}) as unknown as SurfaceLike;
|
|
186
|
+
this._ctx = this._surface.getContext('2d');
|
|
187
|
+
}
|
|
188
|
+
const surface = this._surface;
|
|
189
|
+
const ctx = this._ctx!;
|
|
190
|
+
surface.clear();
|
|
191
|
+
const started = now();
|
|
192
|
+
// The rows drawn into: what is read back, and no more.
|
|
193
|
+
let used = 0;
|
|
194
|
+
items.forEach((item, i) => {
|
|
195
|
+
const place = places[i];
|
|
196
|
+
if (!place) return;
|
|
197
|
+
// Drawing is this thread's time; past the budget the rest wait for
|
|
198
|
+
// the next batch.
|
|
199
|
+
if (now() - started > DRAW_BUDGET_MS) {
|
|
200
|
+
places[i] = undefined;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this._layout(item.text, item.size).draw(
|
|
204
|
+
ctx,
|
|
205
|
+
place.x + pad,
|
|
206
|
+
place.y + pad,
|
|
207
|
+
);
|
|
208
|
+
used = Math.max(used, place.y + place.h);
|
|
209
|
+
});
|
|
210
|
+
const image = await ctx.getImageData(0, 0, STAGING_WIDTH, used);
|
|
211
|
+
return places.map((place) => {
|
|
212
|
+
if (!place) return place;
|
|
213
|
+
// Row by row, as the readback has them: a copy, not a pass over the
|
|
214
|
+
// pixels, and uploaded as it is — the shader reads alpha alone.
|
|
215
|
+
const rowBytes = place.w * 4;
|
|
216
|
+
const pixels = new Uint8Array(rowBytes * place.h);
|
|
217
|
+
for (let r = 0; r < place.h; r++) {
|
|
218
|
+
const src = ((place.y + r) * image.width + place.x) * 4;
|
|
219
|
+
pixels.set(image.data.subarray(src, src + rowBytes), r * rowBytes);
|
|
220
|
+
}
|
|
221
|
+
return { width: place.w, height: place.h, pixels };
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
dispose(): void {
|
|
226
|
+
this._ctx?.destroy?.();
|
|
227
|
+
this._ctx = null;
|
|
228
|
+
this._surface?.destroy();
|
|
229
|
+
this._surface = null;
|
|
230
|
+
this._layouts.clear();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A rasterized label's place in the atlas: its whole padded raster. */
|
|
235
|
+
export interface AtlasEntry {
|
|
236
|
+
key: string;
|
|
237
|
+
x: number;
|
|
238
|
+
y: number;
|
|
239
|
+
width: number;
|
|
240
|
+
height: number;
|
|
241
|
+
/** The raster, kept to upload again — after a compaction moves it, or
|
|
242
|
+
* into the texture of a new context. */
|
|
243
|
+
pixels: Uint8Array;
|
|
244
|
+
/** In the texture where it is now; only then is it drawn. Set by the
|
|
245
|
+
* renderer that uploads it. */
|
|
246
|
+
ready: boolean;
|
|
247
|
+
/** The frame it was last asked for — what a compaction keeps. */
|
|
248
|
+
used: number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const keyOf = (text: string, size: number): string => `${size}|${text}`;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The widest halo a raster's margin holds, in pixels. The drawn quad sits
|
|
255
|
+
* {@link quadInset} inside the margin and a halo samples up to this far
|
|
256
|
+
* past the quad, so its samples stay a texel inside the raster's own clear
|
|
257
|
+
* margin and never reach a neighbour's.
|
|
258
|
+
*/
|
|
259
|
+
export const haloReach = (pad: number): number =>
|
|
260
|
+
Math.max(0, Math.floor((pad - 2) / 2));
|
|
261
|
+
|
|
262
|
+
export const quadInset = (pad: number): number => pad - haloReach(pad) - 1;
|
|
263
|
+
|
|
264
|
+
/** Strings a frame measures however little budget it has left, so a frame
|
|
265
|
+
* that starts late still makes progress. One: on X11 a string's first
|
|
266
|
+
* measurement can cost milliseconds, and a floor of several put a pan
|
|
267
|
+
* frame 27 ms over. */
|
|
268
|
+
const MIN_MEASURES = 1;
|
|
269
|
+
/** Strings per rasterization batch at most — a few rows of the staging
|
|
270
|
+
* surface, so the readback and the copies out of it stay small too. */
|
|
271
|
+
const BATCH = 32;
|
|
272
|
+
/** Frames an unused entry survives a compaction for. */
|
|
273
|
+
const KEEP_FRAMES = 120;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Every label raster, packed into one texture's worth of shelves, with the
|
|
277
|
+
* uploads the renderer owes it.
|
|
278
|
+
*
|
|
279
|
+
* The atlas is CPU-side and outlives any one GL context: it keeps each
|
|
280
|
+
* entry's raster, so a renderer that is new — the surface was recreated —
|
|
281
|
+
* uploads it again from here ({@link restart}), and asks nothing of the text
|
|
282
|
+
* engine. What it holds is bounded by the texture it packs into: 16 MB for
|
|
283
|
+
* the default 2048², whatever is panned past.
|
|
284
|
+
*
|
|
285
|
+
* **Measuring is budgeted, drawing is asynchronous, uploading is metered.**
|
|
286
|
+
* Placement needs a label's box the frame it is considered, which is a
|
|
287
|
+
* synchronous shaping call — tens of microseconds on the Cocoa backend, but
|
|
288
|
+
* milliseconds each the first time on X11, where fonts load and glyphs
|
|
289
|
+
* rasterize in JS — so a frame measures until its deadline and leaves the
|
|
290
|
+
* rest for the next. A raster is asked for only once a label has won a
|
|
291
|
+
* place, and arrives a batch at a time, a readback later; the renderer
|
|
292
|
+
* takes a few dozen a frame into the texture, and a label fades in once its
|
|
293
|
+
* raster is there.
|
|
294
|
+
*/
|
|
295
|
+
export class LabelAtlas {
|
|
296
|
+
readonly size: number;
|
|
297
|
+
/** Clear margin rasterized around every string, in pixels — what a halo
|
|
298
|
+
* grows into, and what keeps it from sampling a neighbour. */
|
|
299
|
+
readonly pad: number;
|
|
300
|
+
/** A frame's measuring ran out of budget: there is more to place. */
|
|
301
|
+
starved = false;
|
|
302
|
+
onChange: (() => void) | null = null;
|
|
303
|
+
private _engine: TextEngine | null;
|
|
304
|
+
private readonly _entries = new Map<string, AtlasEntry>();
|
|
305
|
+
/** Entries the texture does not have where they are, oldest first. */
|
|
306
|
+
private _uploads: AtlasEntry[] = [];
|
|
307
|
+
private readonly _wanted = new Map<string, TextItem>();
|
|
308
|
+
private readonly _failed = new Set<string>();
|
|
309
|
+
private readonly _measured = new Map<string, TextBox>();
|
|
310
|
+
private _busy = false;
|
|
311
|
+
private _shelves: { y: number; height: number; x: number }[] = [];
|
|
312
|
+
private _top = 0;
|
|
313
|
+
private _frame = 0;
|
|
314
|
+
private _deadline = Infinity;
|
|
315
|
+
private _measures = 0;
|
|
316
|
+
|
|
317
|
+
constructor(
|
|
318
|
+
engine: TextEngine | null,
|
|
319
|
+
options: { size?: number; pad: number },
|
|
320
|
+
) {
|
|
321
|
+
this._engine = engine;
|
|
322
|
+
this.size = options.size ?? 2048;
|
|
323
|
+
this.pad = options.pad;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Strings asked for and not yet rasterized, or a batch in flight. */
|
|
327
|
+
get pending(): boolean {
|
|
328
|
+
return this._busy || this._wanted.size > 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Rasters waiting for the texture. */
|
|
332
|
+
get uploading(): boolean {
|
|
333
|
+
return this._uploads.length > 0;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
get entries(): number {
|
|
337
|
+
return this._entries.size;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** A new frame, whose measuring stops at `deadline` (after one). */
|
|
341
|
+
beginFrame(deadline: number): void {
|
|
342
|
+
this._frame++;
|
|
343
|
+
this._deadline = deadline;
|
|
344
|
+
this._measures = 0;
|
|
345
|
+
this.starved = false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The box a string sets in, or null when this frame's budget is spent. */
|
|
349
|
+
measure(text: string, size: number): TextBox | null {
|
|
350
|
+
const key = keyOf(text, size);
|
|
351
|
+
const hit = this._measured.get(key);
|
|
352
|
+
if (hit) return hit;
|
|
353
|
+
const engine = this._engine;
|
|
354
|
+
if (!engine) return null;
|
|
355
|
+
if (this._measures >= MIN_MEASURES && now() > this._deadline) {
|
|
356
|
+
this.starved = true;
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
this._measures++;
|
|
360
|
+
const box = engine.measure(text, size);
|
|
361
|
+
if (this._measured.size > 20000) this._measured.clear();
|
|
362
|
+
this._measured.set(key, box);
|
|
363
|
+
return box;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** The string's raster, once the texture has it; null until then —
|
|
367
|
+
* asking is what gets it rasterized. */
|
|
368
|
+
entry(text: string, size: number): AtlasEntry | null {
|
|
369
|
+
const key = keyOf(text, size);
|
|
370
|
+
const entry = this._entries.get(key);
|
|
371
|
+
if (entry) {
|
|
372
|
+
entry.used = this._frame;
|
|
373
|
+
return entry.ready ? entry : null;
|
|
374
|
+
}
|
|
375
|
+
if (!this._failed.has(key) && !this._wanted.has(key)) {
|
|
376
|
+
this._wanted.set(key, { text, size });
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Start rasterizing what has been asked for, unless a batch is out. */
|
|
382
|
+
pump(): void {
|
|
383
|
+
const engine = this._engine;
|
|
384
|
+
if (!engine || this._busy || this._wanted.size === 0) return;
|
|
385
|
+
const batch: TextItem[] = [];
|
|
386
|
+
for (const item of this._wanted.values()) {
|
|
387
|
+
batch.push(item);
|
|
388
|
+
if (batch.length >= BATCH) break;
|
|
389
|
+
}
|
|
390
|
+
this._busy = true;
|
|
391
|
+
engine.rasterize(batch, this.pad).then(
|
|
392
|
+
(rasters) => {
|
|
393
|
+
this._busy = false;
|
|
394
|
+
rasters.forEach((raster, i) => {
|
|
395
|
+
if (raster === undefined) return;
|
|
396
|
+
const key = keyOf(batch[i].text, batch[i].size);
|
|
397
|
+
this._wanted.delete(key);
|
|
398
|
+
if (raster === null || !this._add(key, raster)) this._failed.add(key);
|
|
399
|
+
});
|
|
400
|
+
this.onChange?.();
|
|
401
|
+
},
|
|
402
|
+
() => {
|
|
403
|
+
// A readback that fails once fails every time (no surface, a
|
|
404
|
+
// backend without one): stop asking, and draw no labels.
|
|
405
|
+
this._busy = false;
|
|
406
|
+
this._engine = null;
|
|
407
|
+
this._wanted.clear();
|
|
408
|
+
this.onChange?.();
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Up to `max` rasters for the texture, oldest first. The renderer marks
|
|
414
|
+
* each `ready` once it is uploaded. */
|
|
415
|
+
takeUploads(max: number): AtlasEntry[] {
|
|
416
|
+
return this._uploads.splice(0, max);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** The texture is new, and has none of it: everything goes up again. */
|
|
420
|
+
restart(): void {
|
|
421
|
+
for (const entry of this._entries.values()) entry.ready = false;
|
|
422
|
+
this._uploads = [...this._entries.values()];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
dispose(): void {
|
|
426
|
+
this._engine?.dispose();
|
|
427
|
+
this._engine = null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private _add(key: string, raster: TextRaster): boolean {
|
|
431
|
+
let place = this._allocate(raster.width, raster.height);
|
|
432
|
+
if (!place) {
|
|
433
|
+
this._compact();
|
|
434
|
+
place = this._allocate(raster.width, raster.height);
|
|
435
|
+
}
|
|
436
|
+
if (!place) return false;
|
|
437
|
+
const entry: AtlasEntry = {
|
|
438
|
+
key,
|
|
439
|
+
x: place.x,
|
|
440
|
+
y: place.y,
|
|
441
|
+
width: raster.width,
|
|
442
|
+
height: raster.height,
|
|
443
|
+
pixels: raster.pixels,
|
|
444
|
+
ready: false,
|
|
445
|
+
used: this._frame,
|
|
446
|
+
};
|
|
447
|
+
this._entries.set(key, entry);
|
|
448
|
+
this._uploads.push(entry);
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** A shelf packer: rows of one height class (rounded up to 8 pixels),
|
|
453
|
+
* filled left to right, a new row below the last when none has room. */
|
|
454
|
+
private _allocate(w: number, h: number): { x: number; y: number } | null {
|
|
455
|
+
if (w > this.size || h > this.size) return null;
|
|
456
|
+
const height = Math.ceil(h / 8) * 8;
|
|
457
|
+
for (const shelf of this._shelves) {
|
|
458
|
+
if (shelf.height === height && shelf.x + w <= this.size) {
|
|
459
|
+
const at = { x: shelf.x, y: shelf.y };
|
|
460
|
+
shelf.x += w;
|
|
461
|
+
return at;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (this._top + height > this.size) return null;
|
|
465
|
+
const shelf = { y: this._top, height, x: w };
|
|
466
|
+
this._shelves.push(shelf);
|
|
467
|
+
this._top += height;
|
|
468
|
+
return { x: 0, y: shelf.y };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Drop what has not been drawn for a while and pack the rest again,
|
|
472
|
+
* tallest first. Everything kept has moved, so none of it is drawn
|
|
473
|
+
* until the renderer has uploaded it where it now is. */
|
|
474
|
+
private _compact(): void {
|
|
475
|
+
const keep = [...this._entries.values()]
|
|
476
|
+
.filter((e) => e.used >= this._frame - KEEP_FRAMES)
|
|
477
|
+
.sort((a, b) => b.height - a.height);
|
|
478
|
+
this._entries.clear();
|
|
479
|
+
this._shelves = [];
|
|
480
|
+
this._top = 0;
|
|
481
|
+
this._uploads = [];
|
|
482
|
+
for (const entry of keep) {
|
|
483
|
+
const place = this._allocate(entry.width, entry.height);
|
|
484
|
+
if (!place) continue;
|
|
485
|
+
entry.x = place.x;
|
|
486
|
+
entry.y = place.y;
|
|
487
|
+
entry.ready = false;
|
|
488
|
+
this._entries.set(entry.key, entry);
|
|
489
|
+
this._uploads.push(entry);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const globals = globalThis as { performance?: { now(): number } };
|
|
495
|
+
const now = (): number => globals.performance?.now() ?? Date.now();
|