@twip/web 0.0.0-canary-f16fa12

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.
Files changed (82) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +35 -0
  3. package/dist/THIRD-PARTY-NOTICES.md +5457 -0
  4. package/dist/commands.d.ts +637 -0
  5. package/dist/commands.js +188 -0
  6. package/dist/editor.d.ts +218 -0
  7. package/dist/editor.js +460 -0
  8. package/dist/errors.d.ts +21 -0
  9. package/dist/errors.js +26 -0
  10. package/dist/events.d.ts +126 -0
  11. package/dist/events.js +1 -0
  12. package/dist/fonts/google.d.ts +3 -0
  13. package/dist/fonts/google.js +39 -0
  14. package/dist/fonts/library.d.ts +65 -0
  15. package/dist/fonts/library.js +130 -0
  16. package/dist/host/a11y.d.ts +3 -0
  17. package/dist/host/a11y.js +62 -0
  18. package/dist/host/chrome.d.ts +28 -0
  19. package/dist/host/chrome.js +144 -0
  20. package/dist/host/clipboard.d.ts +16 -0
  21. package/dist/host/clipboard.js +128 -0
  22. package/dist/host/cursors.d.ts +15 -0
  23. package/dist/host/cursors.js +5 -0
  24. package/dist/host/dom.d.ts +45 -0
  25. package/dist/host/dom.js +122 -0
  26. package/dist/host/frames.d.ts +14 -0
  27. package/dist/host/frames.js +87 -0
  28. package/dist/host/gpu.d.ts +58 -0
  29. package/dist/host/gpu.js +79 -0
  30. package/dist/host/ime.d.ts +3 -0
  31. package/dist/host/ime.js +11 -0
  32. package/dist/host/input.d.ts +12 -0
  33. package/dist/host/input.js +86 -0
  34. package/dist/host/keys.d.ts +16 -0
  35. package/dist/host/keys.js +27 -0
  36. package/dist/host/mount.d.ts +11 -0
  37. package/dist/host/mount.js +72 -0
  38. package/dist/host/pointer.d.ts +4 -0
  39. package/dist/host/pointer.js +196 -0
  40. package/dist/host/scroll.d.ts +14 -0
  41. package/dist/host/scroll.js +95 -0
  42. package/dist/index.d.ts +27 -0
  43. package/dist/index.js +10 -0
  44. package/dist/review.d.ts +156 -0
  45. package/dist/review.js +38 -0
  46. package/dist/services/accessibility.d.ts +52 -0
  47. package/dist/services/accessibility.js +71 -0
  48. package/dist/services/dialogs.d.ts +181 -0
  49. package/dist/services/dialogs.js +18 -0
  50. package/dist/services/elements.d.ts +146 -0
  51. package/dist/services/elements.js +18 -0
  52. package/dist/services/find.d.ts +50 -0
  53. package/dist/services/find.js +12 -0
  54. package/dist/services/menus.d.ts +82 -0
  55. package/dist/services/menus.js +8 -0
  56. package/dist/services/navigation.d.ts +58 -0
  57. package/dist/services/navigation.js +12 -0
  58. package/dist/services/ruler.d.ts +95 -0
  59. package/dist/services/ruler.js +16 -0
  60. package/dist/services/spelling.d.ts +34 -0
  61. package/dist/services/spelling.js +14 -0
  62. package/dist/services/view.d.ts +70 -0
  63. package/dist/services/view.js +19 -0
  64. package/dist/state.d.ts +483 -0
  65. package/dist/state.js +1 -0
  66. package/dist/transport/direct.d.ts +31 -0
  67. package/dist/transport/direct.js +405 -0
  68. package/dist/transport/module.d.ts +51 -0
  69. package/dist/transport/module.js +17 -0
  70. package/dist/transport/protocol.d.ts +265 -0
  71. package/dist/transport/protocol.js +132 -0
  72. package/dist/transport/worker.d.ts +54 -0
  73. package/dist/transport/worker.js +203 -0
  74. package/dist/twip-wasm.js +2 -0
  75. package/dist/twip_wasm.wasm +0 -0
  76. package/dist/types.d.ts +134 -0
  77. package/dist/types.js +16 -0
  78. package/dist/worker/entry.d.ts +1 -0
  79. package/dist/worker/entry.js +53 -0
  80. package/dist/worker.d.ts +1 -0
  81. package/dist/worker.js +2 -0
  82. package/package.json +30 -0
@@ -0,0 +1,45 @@
1
+ /** What a mount owns: `container > scroller > [canvas, content > layer]`, each marked with `data-twip`. */
2
+ export interface Dom {
3
+ /** Scrolls natively over the content; the canvas stays in view. */
4
+ scroller: HTMLDivElement;
5
+ /** The viewport's pixels. */
6
+ canvas: HTMLCanvasElement;
7
+ /** As large as the document at the zoom: it gives the scroller its extent. ContentPoint coordinates. */
8
+ content: HTMLDivElement;
9
+ /** Over the content: the input element, the accessibility DOM and the balloon layer. */
10
+ layer: HTMLDivElement;
11
+ }
12
+ /** What every live ledger holds, and the engine worker (transport/worker.ts), for the tests (`globalThis[resources]()`). */
13
+ export declare const live: {
14
+ nodes: number;
15
+ listeners: number;
16
+ observers: number;
17
+ timers: number;
18
+ frames: number;
19
+ workers: number;
20
+ };
21
+ /** The symbol the resource counts are read through. Internal: for the SDK's tests. */
22
+ export declare const resources: unique symbol;
23
+ /** The resources of one mount. */
24
+ export declare class Ledger {
25
+ #private;
26
+ /** `addEventListener`, removed on release. */
27
+ listen<K extends keyof HTMLElementEventMap>(target: HTMLElement, type: K, listener: (e: HTMLElementEventMap[K]) => void, options?: AddEventListenerOptions): void;
28
+ listen(target: EventTarget, type: string, listener: (e: Event) => void, options?: AddEventListenerOptions): void;
29
+ /** An observer, disconnected on release. */
30
+ observe<T extends {
31
+ disconnect(): void;
32
+ }>(observer: T): T;
33
+ /** A node appended to `parent`, removed on release. */
34
+ node<T extends Element>(parent: Element, node: T): T;
35
+ /** A one-shot timer, cleared on release. Returns its cancel. */
36
+ timeout(run: () => void, ms: number): () => void;
37
+ /** One animation frame, cancelled on release. Returns its cancel. */
38
+ frame(run: () => void): () => void;
39
+ /** Undoes everything, last made first. Idempotent. */
40
+ release(): void;
41
+ }
42
+ /** Claims `container` for one editor, or throws `unavailable`. Returns the release. */
43
+ export declare function claim(container: HTMLElement): () => void;
44
+ /** Builds the editor's DOM in `container`; the ledger removes it. */
45
+ export declare function createDom(container: HTMLElement, ledger: Ledger): Dom;
@@ -0,0 +1,122 @@
1
+ // The editor's DOM inside the application's container, and the ledger of everything a mount creates: listeners,
2
+ // observers, timers and animation frames are made through it, so destroy (or a failed mount) undoes all of it.
3
+ import { EditorError } from '../errors.js';
4
+ /** What every live ledger holds, and the engine worker (transport/worker.ts), for the tests (`globalThis[resources]()`). */
5
+ export const live = { nodes: 0, listeners: 0, observers: 0, timers: 0, frames: 0, workers: 0 };
6
+ /** The symbol the resource counts are read through. Internal: for the SDK's tests. */
7
+ export const resources = Symbol.for('twip.resources');
8
+ globalThis[resources] = () => ({ ...live });
9
+ /** The resources of one mount. */
10
+ export class Ledger {
11
+ #undo = [];
12
+ #timers = new Set();
13
+ #frames = new Set();
14
+ #released = false;
15
+ listen(target, type, listener, options) {
16
+ const l = listener;
17
+ target.addEventListener(type, l, options);
18
+ live.listeners++;
19
+ this.#undo.push(() => {
20
+ target.removeEventListener(type, l, options);
21
+ live.listeners--;
22
+ });
23
+ }
24
+ /** An observer, disconnected on release. */
25
+ observe(observer) {
26
+ live.observers++;
27
+ this.#undo.push(() => {
28
+ observer.disconnect();
29
+ live.observers--;
30
+ });
31
+ return observer;
32
+ }
33
+ /** A node appended to `parent`, removed on release. */
34
+ node(parent, node) {
35
+ parent.append(node);
36
+ live.nodes++;
37
+ this.#undo.push(() => {
38
+ node.remove();
39
+ live.nodes--;
40
+ });
41
+ return node;
42
+ }
43
+ /** A one-shot timer, cleared on release. Returns its cancel. */
44
+ timeout(run, ms) {
45
+ if (this.#released)
46
+ return () => { };
47
+ const handle = setTimeout(() => {
48
+ this.#timers.delete(handle);
49
+ live.timers--;
50
+ run();
51
+ }, ms);
52
+ this.#timers.add(handle);
53
+ live.timers++;
54
+ return () => {
55
+ if (this.#timers.delete(handle)) {
56
+ clearTimeout(handle);
57
+ live.timers--;
58
+ }
59
+ };
60
+ }
61
+ /** One animation frame, cancelled on release. Returns its cancel. */
62
+ frame(run) {
63
+ if (this.#released)
64
+ return () => { };
65
+ const handle = requestAnimationFrame(() => {
66
+ this.#frames.delete(handle);
67
+ live.frames--;
68
+ run();
69
+ });
70
+ this.#frames.add(handle);
71
+ live.frames++;
72
+ return () => {
73
+ if (this.#frames.delete(handle)) {
74
+ cancelAnimationFrame(handle);
75
+ live.frames--;
76
+ }
77
+ };
78
+ }
79
+ /** Undoes everything, last made first. Idempotent. */
80
+ release() {
81
+ this.#released = true;
82
+ for (const handle of this.#timers)
83
+ clearTimeout(handle);
84
+ for (const handle of this.#frames)
85
+ cancelAnimationFrame(handle);
86
+ live.timers -= this.#timers.size;
87
+ live.frames -= this.#frames.size;
88
+ this.#timers.clear();
89
+ this.#frames.clear();
90
+ for (const undo of this.#undo.splice(0).reverse())
91
+ undo();
92
+ }
93
+ }
94
+ /** Containers with an editor, or one mounting. */
95
+ const occupied = new WeakSet();
96
+ /** Claims `container` for one editor, or throws `unavailable`. Returns the release. */
97
+ export function claim(container) {
98
+ if (occupied.has(container) || container.querySelector(':scope > [data-twip]')) {
99
+ throw new EditorError('unavailable', 'the container already holds an editor');
100
+ }
101
+ occupied.add(container);
102
+ return () => occupied.delete(container);
103
+ }
104
+ function element(tag, name, css) {
105
+ const e = document.createElement(tag);
106
+ e.dataset.twip = name;
107
+ e.style.cssText = css;
108
+ return e;
109
+ }
110
+ /** Builds the editor's DOM in `container`; the ledger removes it. */
111
+ export function createDom(container, ledger) {
112
+ // One grid cell holds the canvas and the content: the sticky canvas can then stay in view over the content's whole
113
+ // extent, in both directions.
114
+ const scroller = element('div', 'editor', 'position:relative;width:100%;height:100%;overflow:auto;display:grid;outline:none;');
115
+ const canvas = element('canvas', 'canvas', 'grid-area:1/1;position:sticky;left:0;top:0;align-self:start;justify-self:start;display:block;touch-action:none;');
116
+ const content = element('div', 'content', 'grid-area:1/1;position:relative;pointer-events:none;');
117
+ const layer = element('div', 'layer', 'position:absolute;inset:0;overflow:hidden;');
118
+ content.append(layer);
119
+ scroller.append(canvas, content);
120
+ ledger.node(container, scroller);
121
+ return { scroller, canvas, content, layer };
122
+ }
@@ -0,0 +1,14 @@
1
+ import type { Session } from '../editor.js';
2
+ import { type FrameInfo } from '../transport/protocol.js';
3
+ import type { Dom, Ledger } from './dom.js';
4
+ /** The frame loop of a mounted editor. */
5
+ export interface Frames {
6
+ /** The last frame's content size. */
7
+ readonly info: FrameInfo;
8
+ /** Draws a frame at the next animation frame. */
9
+ request(): void;
10
+ /** Resolves once a frame was drawn after this call; at once while the editor has no size. */
11
+ drawn(): Promise<void>;
12
+ }
13
+ /** Starts the frame loop; `onFrame` runs after each frame drawn. */
14
+ export declare function startFrames(s: Session, dom: Dom, ledger: Ledger, report: (e: unknown) => void, onFrame: (info: FrameInfo) => void): Promise<Frames>;
@@ -0,0 +1,87 @@
1
+ import { Kind } from '../transport/protocol.js';
2
+ /** Starts the frame loop; `onFrame` runs after each frame drawn. */
3
+ export async function startFrames(s, dom, ledger, report, onFrame) {
4
+ await s.guard(() => s.transport.attachCanvas(s.id, dom.canvas));
5
+ let info = { contentWidth: 0, contentHeight: 0, pages: 0 };
6
+ let surface = { width: 0, height: 0, dpr: 0 };
7
+ let cancel;
8
+ let busy = false;
9
+ let again = false;
10
+ const waiting = [];
11
+ const settle = () => {
12
+ for (const resolve of waiting.splice(0))
13
+ resolve();
14
+ };
15
+ const draw = () => {
16
+ cancel = undefined;
17
+ if (busy) {
18
+ again = true;
19
+ return;
20
+ }
21
+ const { width, height, dpr } = surface;
22
+ if (!width || !height)
23
+ return settle();
24
+ busy = true;
25
+ s.frame(Math.round(width * dpr), Math.round(height * dpr))
26
+ .then((drawn) => {
27
+ if (drawn) {
28
+ info = drawn;
29
+ onFrame(drawn);
30
+ }
31
+ }, report)
32
+ .finally(() => {
33
+ busy = false;
34
+ settle();
35
+ if (again) {
36
+ again = false;
37
+ request();
38
+ }
39
+ });
40
+ };
41
+ const request = () => {
42
+ if (!cancel && !s.destroyed)
43
+ cancel = ledger.frame(draw);
44
+ };
45
+ // The viewport's size and density go to the engine as the surface; the canvas covers the viewport.
46
+ const resize = () => {
47
+ const next = { width: dom.scroller.clientWidth, height: dom.scroller.clientHeight, dpr: globalThis.devicePixelRatio || 1 };
48
+ if (next.width !== surface.width || next.height !== surface.height || next.dpr !== surface.dpr) {
49
+ surface = next;
50
+ dom.canvas.style.width = `${next.width}px`;
51
+ dom.canvas.style.height = `${next.height}px`;
52
+ s.input({ type: 'surface', ...next }).catch(report);
53
+ }
54
+ request();
55
+ };
56
+ // A density change (browser zoom, another monitor) is a `resolution` media query that stops matching.
57
+ const watchDensity = () => {
58
+ const query = matchMedia(`(resolution: ${globalThis.devicePixelRatio || 1}dppx)`);
59
+ ledger.listen(query, 'change', () => {
60
+ resize();
61
+ watchDensity();
62
+ }, { once: true });
63
+ };
64
+ watchDensity();
65
+ ledger.observe(new ResizeObserver(resize)).observe(dom.scroller);
66
+ // Shown again (display, visibility, scrolled into the page): draw.
67
+ ledger.observe(new IntersectionObserver((entries) => entries.some((e) => e.isIntersecting) && resize())).observe(dom.scroller);
68
+ const onMessage = (message) => {
69
+ if (message.kind === Kind.RequestFrame)
70
+ request();
71
+ };
72
+ s.hostListeners.add(onMessage);
73
+ s.disposers.push(() => s.hostListeners.delete(onMessage));
74
+ resize();
75
+ return {
76
+ get info() {
77
+ return info;
78
+ },
79
+ request,
80
+ drawn: () => {
81
+ if (!surface.width || !surface.height || s.destroyed)
82
+ return Promise.resolve();
83
+ request();
84
+ return new Promise((resolve) => waiting.push(resolve));
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,58 @@
1
+ import type { TwipModule } from '../transport/module.js';
2
+ /** The parts of a WebGPU device the engine imports. */
3
+ export interface GpuDevice {
4
+ queue: {
5
+ writeTexture(destination: {
6
+ texture: unknown;
7
+ }, data: Uint8Array, layout: {
8
+ bytesPerRow: number;
9
+ }, size: {
10
+ width: number;
11
+ height: number;
12
+ }): void;
13
+ };
14
+ /** Settles when the device is lost: destroyed, or the GPU reset. */
15
+ lost: Promise<unknown>;
16
+ }
17
+ /** The parts of a WebGPU canvas context used here. */
18
+ interface GpuCanvasContext {
19
+ configure(options: {
20
+ device: GpuDevice;
21
+ format: string;
22
+ alphaMode: string;
23
+ usage: number;
24
+ }): void;
25
+ getCurrentTexture(): unknown;
26
+ }
27
+ /** A canvas configured for WebGPU frames. */
28
+ export interface WebGpuTarget {
29
+ canvas: HTMLCanvasElement | OffscreenCanvas;
30
+ context: GpuCanvasContext;
31
+ bgra: boolean;
32
+ /** CPU pixels are copied into the texture with this device, which the engine didn't adopt. */
33
+ cpu?: GpuDevice;
34
+ }
35
+ /** A canvas with the engine's WebGL 2 context. */
36
+ export interface WebGlTarget {
37
+ canvas: HTMLCanvasElement | OffscreenCanvas;
38
+ handle: number;
39
+ }
40
+ /** Adopts a WebGPU device for every session of the engine. False if the engine can't use it: use WebGL then. */
41
+ export declare function attachWebGpu(m: TwipModule, device: GpuDevice): boolean;
42
+ /** Configures `canvas` for WebGPU frames with `device`; for CPU pixels copied in, with `cpu`. */
43
+ export declare function webGpuTarget(canvas: HTMLCanvasElement | OffscreenCanvas, device: GpuDevice, cpu?: boolean): WebGpuTarget;
44
+ /** The page's WebGPU device, or null when it has none. */
45
+ export declare function gpuDevice(): Promise<GpuDevice | null>;
46
+ /** Draws a session's frame into the target's current texture; the frame's `TwipFrameInfo` goes to `info`. */
47
+ export declare function frameWebGpu(m: TwipModule, engine: number, session: number, target: WebGpuTarget, info: number): boolean;
48
+ /** Copies CPU pixels (premultiplied RGBA) into a CPU target's current texture. */
49
+ export declare function writeWebGpu(target: WebGpuTarget, pixels: Uint8Array, width: number, height: number): void;
50
+ /** Creates a WebGL 2 context on `canvas` for a session (one per canvas), or null when WebGL 2 is unavailable. */
51
+ export declare function webGlTarget(m: TwipModule, engine: number, session: number, canvas: HTMLCanvasElement | OffscreenCanvas): WebGlTarget | null;
52
+ /** The session's WebGL target again once its lost context is restored: a new context handle, attached anew. */
53
+ export declare function restoreWebGl(m: TwipModule, engine: number, session: number, target: WebGlTarget): WebGlTarget | null;
54
+ /** Keeps a lost WebGL context restorable (by default the browser drops it), calling `lost` then `restored`. */
55
+ export declare function watchWebGl(canvas: HTMLCanvasElement | OffscreenCanvas, lost: () => void, restored: () => void): () => void;
56
+ /** Draws a session's frame into its WebGL canvas; the frame's `TwipFrameInfo` goes to `info`. */
57
+ export declare function frameWebGl(m: TwipModule, engine: number, session: number, target: WebGlTarget, info: number): boolean;
58
+ export {};
@@ -0,0 +1,79 @@
1
+ /** Adopts a WebGPU device for every session of the engine. False if the engine can't use it: use WebGL then. */
2
+ export function attachWebGpu(m, device) {
3
+ if (!m.WebGPU)
4
+ return false;
5
+ const instance = m._twip_gpu_instance();
6
+ const imported = m.WebGPU.importJsDevice(device, instance);
7
+ return m._twip_attach_webgpu(instance, imported, m.WebGPU.importJsQueue(device.queue, instance)) === 0;
8
+ }
9
+ /** Configures `canvas` for WebGPU frames with `device`; for CPU pixels copied in, with `cpu`. */
10
+ export function webGpuTarget(canvas, device, cpu = false) {
11
+ const context = canvas.getContext('webgpu');
12
+ if (cpu) {
13
+ // COPY_DST: the engine's pixels are premultiplied RGBA.
14
+ context.configure({ device, format: 'rgba8unorm', alphaMode: 'premultiplied', usage: 0x02 });
15
+ return { canvas, context, bgra: false, cpu: device };
16
+ }
17
+ const gpu = globalThis.navigator?.gpu;
18
+ const format = gpu?.getPreferredCanvasFormat() ?? 'rgba8unorm';
19
+ // COPY_SRC | COPY_DST | TEXTURE_BINDING | RENDER_ATTACHMENT: what Graphite needs of the canvas texture.
20
+ context.configure({ device, format, alphaMode: 'premultiplied', usage: 0x01 | 0x02 | 0x04 | 0x10 });
21
+ return { canvas, context, bgra: format === 'bgra8unorm' };
22
+ }
23
+ /** The page's WebGPU device, or null when it has none. */
24
+ export async function gpuDevice() {
25
+ const gpu = globalThis.navigator?.gpu;
26
+ try {
27
+ return (await (await gpu?.requestAdapter())?.requestDevice()) ?? null;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ /** Draws a session's frame into the target's current texture; the frame's `TwipFrameInfo` goes to `info`. */
34
+ export function frameWebGpu(m, engine, session, target, info) {
35
+ if (!m.WebGPU)
36
+ return false;
37
+ const texture = m.WebGPU.importJsTexture(target.context.getCurrentTexture());
38
+ return m._twip_frame_webgpu(engine, session, texture, target.bgra ? 1 : 0, target.canvas.height, info) === 0;
39
+ }
40
+ /** Copies CPU pixels (premultiplied RGBA) into a CPU target's current texture. */
41
+ export function writeWebGpu(target, pixels, width, height) {
42
+ target.cpu?.queue.writeTexture({ texture: target.context.getCurrentTexture() }, pixels, { bytesPerRow: width * 4 }, { width, height });
43
+ }
44
+ /** Creates a WebGL 2 context on `canvas` for a session (one per canvas), or null when WebGL 2 is unavailable. */
45
+ export function webGlTarget(m, engine, session, canvas) {
46
+ if (!m.GL)
47
+ return null;
48
+ const attributes = { majorVersion: 2, minorVersion: 0, alpha: 1, depth: 0, stencil: 8, antialias: 0, premultipliedAlpha: 1 };
49
+ const handle = m.GL.createContext(canvas, attributes);
50
+ if (!handle)
51
+ return null;
52
+ m.GL.makeContextCurrent(handle);
53
+ return m._twip_attach_webgl(engine, session) === 0 ? { canvas, handle } : null;
54
+ }
55
+ /** The session's WebGL target again once its lost context is restored: a new context handle, attached anew. */
56
+ export function restoreWebGl(m, engine, session, target) {
57
+ m.GL?.deleteContext(target.handle);
58
+ return webGlTarget(m, engine, session, target.canvas);
59
+ }
60
+ /** Keeps a lost WebGL context restorable (by default the browser drops it), calling `lost` then `restored`. */
61
+ export function watchWebGl(canvas, lost, restored) {
62
+ const onLost = (e) => {
63
+ e.preventDefault();
64
+ lost();
65
+ };
66
+ canvas.addEventListener('webglcontextlost', onLost);
67
+ canvas.addEventListener('webglcontextrestored', restored);
68
+ return () => {
69
+ canvas.removeEventListener('webglcontextlost', onLost);
70
+ canvas.removeEventListener('webglcontextrestored', restored);
71
+ };
72
+ }
73
+ /** Draws a session's frame into its WebGL canvas; the frame's `TwipFrameInfo` goes to `info`. */
74
+ export function frameWebGl(m, engine, session, target, info) {
75
+ if (!m.GL)
76
+ return false;
77
+ m.GL.makeContextCurrent(target.handle);
78
+ return m._twip_frame_webgl(engine, session, target.canvas.width, target.canvas.height, info) === 0;
79
+ }
@@ -0,0 +1,3 @@
1
+ import type { Session } from '../editor.js';
2
+ import type { Ledger } from './dom.js';
3
+ export declare function startIme(s: Session, input: HTMLTextAreaElement, ledger: Ledger, report: (e: unknown) => void): void;
@@ -0,0 +1,11 @@
1
+ const Phase = { start: 0, update: 1, end: 2 };
2
+ export function startIme(s, input, ledger, report) {
3
+ const compose = (phase, text) => s.input({ type: 'composition', phase, text }).catch(report);
4
+ ledger.listen(input, 'compositionstart', () => compose(Phase.start, ''));
5
+ ledger.listen(input, 'compositionupdate', (e) => compose(Phase.update, e.data));
6
+ ledger.listen(input, 'compositionend', (e) => {
7
+ compose(Phase.end, e.data);
8
+ // The element only ever holds the composition.
9
+ input.value = '';
10
+ });
11
+ }
@@ -0,0 +1,12 @@
1
+ import type { Session } from '../editor.js';
2
+ import type { Dom, Ledger } from './dom.js';
3
+ /** The input element of a mounted editor. */
4
+ export interface Input {
5
+ readonly element: HTMLTextAreaElement;
6
+ focus(): void;
7
+ }
8
+ /**
9
+ * Starts keyboard and text input. `claims` says whether a chord is a command's, an application shortcut or a host
10
+ * key (`shortcuts.lookup`): those keys are the editor's, and their browser defaults (Save, Print, Find) don't run.
11
+ */
12
+ export declare function startInput(s: Session, dom: Dom, ledger: Ledger, report: (e: unknown) => void, claims: (chord: string) => boolean): Input;
@@ -0,0 +1,86 @@
1
+ import { Kind } from '../transport/protocol.js';
2
+ import { keyNumber, modsOf } from './keys.js';
3
+ /** Keys that edit or move, with Ctrl or Shift or neither (not Alt, which the browser keeps): the engine's. */
4
+ const editingKeys = new Set([
5
+ 'Backspace', 'Delete', 'Enter', 'NumpadEnter', 'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End',
6
+ 'PageUp', 'PageDown', 'Escape',
7
+ ]);
8
+ /** The clipboard's keys: the browser turns them into `copy`, `cut` and `paste` events (host/clipboard.ts). */
9
+ const clipboardKeys = new Set(['ctrl+c', 'ctrl+x', 'ctrl+v', 'ctrl+shift+v', 'ctrl+insert', 'shift+insert', 'shift+delete']);
10
+ /** The keys that ask for a dialog (`dialogRequested`): the engine's, though `shortcuts.lookup` names none of them. */
11
+ const dialogKeys = new Set(['ctrl+d']);
12
+ /** The key's names in shortcut strings, by `KeyboardEvent.code`, besides `KeyB` as `B` and `Digit1` as `1`. */
13
+ const names = {
14
+ Minus: '-', Equal: '=', BracketLeft: '[', BracketRight: ']', Backslash: '\\', Semicolon: ';', Quote: "'", Backquote: '`',
15
+ Comma: ',', Period: '.', Slash: '/', ArrowLeft: 'Left', ArrowRight: 'Right', ArrowUp: 'Up', ArrowDown: 'Down',
16
+ NumpadEnter: 'Enter',
17
+ };
18
+ /** A keydown as the shortcut strings spell it: `'Ctrl+Shift+>'`, by the key's code and by the character it made. */
19
+ function chords(e) {
20
+ // shortcut: Cmd is read as Ctrl, so the Windows shortcut strings serve the Mac too.
21
+ const mods = `${e.ctrlKey || e.metaKey ? 'Ctrl+' : ''}${e.altKey ? 'Alt+' : ''}${e.shiftKey ? 'Shift+' : ''}`;
22
+ const keys = new Set([names[e.code] ?? e.code.replace(/^Key|^Digit/, ''), e.code]);
23
+ if (e.key.length === 1)
24
+ keys.add(e.key.toUpperCase());
25
+ return [...keys].map((k) => mods + k);
26
+ }
27
+ /**
28
+ * Starts keyboard and text input. `claims` says whether a chord is a command's, an application shortcut or a host
29
+ * key (`shortcuts.lookup`): those keys are the editor's, and their browser defaults (Save, Print, Find) don't run.
30
+ */
31
+ export function startInput(s, dom, ledger, report, claims) {
32
+ const input = ledger.node(dom.layer, document.createElement('textarea'));
33
+ input.dataset.twip = 'input';
34
+ input.setAttribute('aria-label', 'Document');
35
+ input.setAttribute('aria-multiline', 'true');
36
+ input.setAttribute('autocomplete', 'off');
37
+ input.setAttribute('autocorrect', 'off');
38
+ input.setAttribute('autocapitalize', 'off');
39
+ input.spellcheck = false;
40
+ input.style.cssText =
41
+ 'position:absolute;left:0;top:0;width:1px;height:1em;opacity:0;border:0;padding:0;margin:0;resize:none;overflow:hidden;' +
42
+ 'white-space:pre;outline:none;caret-color:transparent;pointer-events:none;';
43
+ ledger.listen(input, 'keydown', (e) => {
44
+ if (e.isComposing || e.keyCode === 229)
45
+ return;
46
+ const key = keyNumber(e.code);
47
+ if (!key)
48
+ return;
49
+ const all = chords(e);
50
+ if (all.some((c) => clipboardKeys.has(c.toLowerCase())))
51
+ return;
52
+ // The menu keys open the application's menu only when it listens; otherwise the browser's opens.
53
+ const menu = e.code === 'ContextMenu' || all.includes('Shift+F10');
54
+ if (menu && !s.listening('contextMenu'))
55
+ return;
56
+ const claimed = all.some((c) => claims(c) || dialogKeys.has(c.toLowerCase()));
57
+ if (!menu && !(!e.altKey && editingKeys.has(e.code)) && !claimed)
58
+ return;
59
+ e.preventDefault();
60
+ s.input({ type: 'key', key, mods: modsOf(e) }).catch(report);
61
+ });
62
+ ledger.listen(input, 'beforeinput', (e) => {
63
+ if (e.inputType !== 'insertText' && e.inputType !== 'insertReplacementText')
64
+ return;
65
+ e.preventDefault();
66
+ const text = e.data ?? e.dataTransfer?.getData('text/plain');
67
+ if (text)
68
+ s.input({ type: 'text', text }).catch(report);
69
+ });
70
+ const focused = (focused) => () => s.input({ type: 'focus', focused }).catch(report);
71
+ ledger.listen(input, 'focus', focused(true));
72
+ ledger.listen(input, 'blur', focused(false));
73
+ // The caret request: the input element follows the caret, so the IME's window opens beside it.
74
+ const onMessage = (m) => {
75
+ const r = m.payload;
76
+ if (m.kind !== Kind.HostRequest || r.type !== 'caret')
77
+ return;
78
+ input.style.left = `${r.x}px`;
79
+ input.style.top = `${r.y}px`;
80
+ input.style.height = `${Math.max(1, r.height)}px`;
81
+ input.style.fontSize = `${Math.max(1, r.height * 0.8)}px`;
82
+ };
83
+ s.hostListeners.add(onMessage);
84
+ s.disposers.push(() => s.hostListeners.delete(onMessage));
85
+ return { element: input, focus: () => input.focus({ preventScroll: true }) };
86
+ }
@@ -0,0 +1,16 @@
1
+ /** A key's number for the engine, or 0 when the engine doesn't know it. */
2
+ export declare function keyNumber(code: string): number;
3
+ /** Modifier bits of the C ABI. */
4
+ export declare const Mods: {
5
+ readonly ctrl: 1;
6
+ readonly shift: 2;
7
+ readonly alt: 4;
8
+ readonly meta: 8;
9
+ };
10
+ /** A keyboard event's modifier bits. */
11
+ export declare function modsOf(e: {
12
+ shiftKey: boolean;
13
+ ctrlKey: boolean;
14
+ altKey: boolean;
15
+ metaKey: boolean;
16
+ }): number;
@@ -0,0 +1,27 @@
1
+ // Keyboard input as the engine takes it: a key number from KeyboardEvent.code and a modifier bit set.
2
+ /**
3
+ * The key table of the C ABI (`TwipKey` in `twip.h`), by `KeyboardEvent.code`, so the physical key is what counts and
4
+ * the layout is the engine's business. 0 is no key. The numbers must match the engine's table exactly.
5
+ */
6
+ const numbers = {
7
+ KeyA: 1, KeyB: 2, KeyC: 3, KeyD: 4, KeyE: 5, KeyF: 6, KeyG: 7, KeyH: 8, KeyI: 9, KeyJ: 10, KeyK: 11, KeyL: 12,
8
+ KeyM: 13, KeyN: 14, KeyO: 15, KeyP: 16, KeyQ: 17, KeyR: 18, KeyS: 19, KeyT: 20, KeyU: 21, KeyV: 22, KeyW: 23,
9
+ KeyX: 24, KeyY: 25, KeyZ: 26, Digit0: 27, Digit1: 28, Digit2: 29, Digit3: 30, Digit4: 31, Digit5: 32, Digit6: 33,
10
+ Digit7: 34, Digit8: 35, Digit9: 36, F1: 37, F2: 38, F3: 39, F4: 40, F5: 41, F6: 42, F7: 43, F8: 44, F9: 45,
11
+ F10: 46, F11: 47, F12: 48, Enter: 61, Tab: 62, Backspace: 63, Delete: 64, Escape: 65, Space: 66, Insert: 67,
12
+ Home: 68, End: 69, PageUp: 70, PageDown: 71, ArrowLeft: 72, ArrowRight: 73, ArrowUp: 74, ArrowDown: 75,
13
+ ContextMenu: 76, Minus: 77, Equal: 78, BracketLeft: 79, BracketRight: 80, Backslash: 81, Semicolon: 82, Quote: 83,
14
+ Backquote: 84, Comma: 85, Period: 86, Slash: 87, IntlBackslash: 88, NumpadEnter: 89, NumpadAdd: 90,
15
+ NumpadSubtract: 91, NumpadMultiply: 92, NumpadDivide: 93, NumpadDecimal: 94, Numpad0: 95, Numpad1: 96, Numpad2: 97,
16
+ Numpad3: 98, Numpad4: 99, Numpad5: 100, Numpad6: 101, Numpad7: 102, Numpad8: 103, Numpad9: 104,
17
+ };
18
+ /** A key's number for the engine, or 0 when the engine doesn't know it. */
19
+ export function keyNumber(code) {
20
+ return Object.hasOwn(numbers, code) ? numbers[code] : 0;
21
+ }
22
+ /** Modifier bits of the C ABI. */
23
+ export const Mods = { ctrl: 1, shift: 2, alt: 4, meta: 8 };
24
+ /** A keyboard event's modifier bits. */
25
+ export function modsOf(e) {
26
+ return (e.shiftKey ? Mods.shift : 0) | (e.ctrlKey ? Mods.ctrl : 0) | (e.altKey ? Mods.alt : 0) | (e.metaKey ? Mods.meta : 0);
27
+ }
@@ -0,0 +1,11 @@
1
+ import type { MountOptions, Session } from '../editor.js';
2
+ import { type Chrome } from './chrome.js';
3
+ import { type Clipboard } from './clipboard.js';
4
+ /** What a mounted editor gets from its host. */
5
+ export interface Host extends Chrome, Clipboard {
6
+ focus(): void;
7
+ /** Resolves once the first viewport is drawn, or at once when the container is hidden or has no size. */
8
+ ready(): Promise<void>;
9
+ }
10
+ /** Builds the host for `session` in `container`. `claims` is `shortcuts.lookup`, as a yes or no. */
11
+ export declare function mountHost(container: HTMLElement, s: Session, options: MountOptions, claims: (chord: string) => boolean): Promise<Host>;
@@ -0,0 +1,72 @@
1
+ import { EditorError } from '../errors.js';
2
+ import { Method } from '../transport/protocol.js';
3
+ import { startA11y } from './a11y.js';
4
+ import { startChrome } from './chrome.js';
5
+ import { startClipboard } from './clipboard.js';
6
+ import { createDom, Ledger } from './dom.js';
7
+ import { startFrames } from './frames.js';
8
+ import { startIme } from './ime.js';
9
+ import { startInput } from './input.js';
10
+ import { startPointer } from './pointer.js';
11
+ import { startScroll } from './scroll.js';
12
+ /** Builds the host for `session` in `container`. `claims` is `shortcuts.lookup`, as a yes or no. */
13
+ export async function mountHost(container, s, options, claims) {
14
+ const ledger = new Ledger();
15
+ s.disposers.push(() => ledger.release());
16
+ const dom = createDom(container, ledger);
17
+ const report = (e) => {
18
+ if (s.destroyed)
19
+ return;
20
+ s.emit('error', e instanceof EditorError ? e : new EditorError('internal', String(e), { cause: e }));
21
+ };
22
+ // Client points (the API's) to viewport points (the engine's): the canvas covers the viewport.
23
+ const toViewport = (at) => {
24
+ const r = dom.canvas.getBoundingClientRect();
25
+ return { x: at.clientX - r.left, y: at.clientY - r.top };
26
+ };
27
+ // The engine's events carry viewport points; the API's are client points.
28
+ s.adjust = (type, event) => {
29
+ const r = dom.canvas.getBoundingClientRect();
30
+ const client = (p) => ({ clientX: p.clientX + r.left, clientY: p.clientY + r.top });
31
+ if ((type === 'contextMenu' || type === 'gesture') && event)
32
+ return { ...event, at: client(event.at) };
33
+ if (type === 'tooltip' && event) {
34
+ const t = event;
35
+ return { ...t, at: client(t.at), rect: { ...t.rect, left: t.rect.left + r.left, top: t.rect.top + r.top } };
36
+ }
37
+ if (type === 'rulerChanged' && event) {
38
+ const v = event;
39
+ return { ...v, horizontal: { ...v.horizontal, origin: v.horizontal.origin + r.left }, vertical: { ...v.vertical, origin: v.vertical.origin + r.top } };
40
+ }
41
+ return event;
42
+ };
43
+ let chrome;
44
+ const frames = await startFrames(s, dom, ledger, report, (info) => {
45
+ scroll.layout(info);
46
+ chrome?.framed(info);
47
+ });
48
+ const scroll = startScroll(s, dom, ledger, frames, report);
49
+ chrome = startChrome(s, dom, ledger, frames, report, toViewport);
50
+ const input = startInput(s, dom, ledger, report, claims);
51
+ startIme(s, input.element, ledger, report);
52
+ const clipboard = startClipboard(s, dom, input.element, ledger, report, toViewport);
53
+ const balloons = chrome.balloonLayer;
54
+ startPointer(s, dom, ledger, scroll, report, input.focus, (t) => t instanceof Node && balloons.contains(t) && t !== balloons);
55
+ startA11y(s, dom, ledger);
56
+ await chrome.view.setBackground(options.background ?? '#E6E6E6');
57
+ if (options.overlayColors)
58
+ await chrome.view.setOverlayColors(options.overlayColors);
59
+ if (options.drawLeaders)
60
+ chrome.drawLeaders(true);
61
+ if (options.paintBudget) {
62
+ await s.call(Method.HostSetPaintBudget, { recordings: options.paintBudget.recordings ?? 32 << 20 });
63
+ }
64
+ if (options.caretBlink !== undefined || options.caretBlinkStopAfter !== undefined) {
65
+ await chrome.view.setCaretBlink(options.caretBlink ?? 530, options.caretBlinkStopAfter);
66
+ }
67
+ if (options.markupPaneWidth)
68
+ await chrome.setMarkupPaneWidth(options.markupPaneWidth);
69
+ if (options.zoom !== undefined && options.zoom !== 100)
70
+ await s.run({ type: 'setZoom', value: options.zoom });
71
+ return { ...chrome, ...clipboard, focus: input.focus, ready: () => frames.drawn() };
72
+ }