take4-console 0.15.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/CHANGELOG.md +287 -0
- package/LICENSE +21 -0
- package/README.md +986 -0
- package/dist/Screen/InterfaceBuilder.d.mts +43 -0
- package/dist/Screen/InterfaceBuilder.d.mts.map +1 -0
- package/dist/Screen/InterfaceBuilder.mjs +355 -0
- package/dist/Screen/InterfaceBuilder.mjs.map +1 -0
- package/dist/Screen/Pos.d.mts +52 -0
- package/dist/Screen/Pos.d.mts.map +1 -0
- package/dist/Screen/Pos.mjs +105 -0
- package/dist/Screen/Pos.mjs.map +1 -0
- package/dist/Screen/Region.d.mts +31 -0
- package/dist/Screen/Region.d.mts.map +1 -0
- package/dist/Screen/Region.mjs +73 -0
- package/dist/Screen/Region.mjs.map +1 -0
- package/dist/Screen/RegistryHolder.d.mts +6 -0
- package/dist/Screen/RegistryHolder.d.mts.map +1 -0
- package/dist/Screen/RegistryHolder.mjs +14 -0
- package/dist/Screen/RegistryHolder.mjs.map +1 -0
- package/dist/Screen/Screen.d.mts +25 -0
- package/dist/Screen/Screen.d.mts.map +1 -0
- package/dist/Screen/Screen.mjs +100 -0
- package/dist/Screen/Screen.mjs.map +1 -0
- package/dist/Screen/Size.d.mts +23 -0
- package/dist/Screen/Size.d.mts.map +1 -0
- package/dist/Screen/Size.mjs +48 -0
- package/dist/Screen/Size.mjs.map +1 -0
- package/dist/Screen/StyleRegistry.d.mts +32 -0
- package/dist/Screen/StyleRegistry.d.mts.map +1 -0
- package/dist/Screen/StyleRegistry.mjs +80 -0
- package/dist/Screen/StyleRegistry.mjs.map +1 -0
- package/dist/Screen/Window.d.mts +121 -0
- package/dist/Screen/Window.d.mts.map +1 -0
- package/dist/Screen/Window.mjs +407 -0
- package/dist/Screen/Window.mjs.map +1 -0
- package/dist/Screen/WindowManager.d.mts +86 -0
- package/dist/Screen/WindowManager.d.mts.map +1 -0
- package/dist/Screen/WindowManager.mjs +399 -0
- package/dist/Screen/WindowManager.mjs.map +1 -0
- package/dist/Screen/controls/BarChart.d.mts +29 -0
- package/dist/Screen/controls/BarChart.d.mts.map +1 -0
- package/dist/Screen/controls/BarChart.mjs +90 -0
- package/dist/Screen/controls/BarChart.mjs.map +1 -0
- package/dist/Screen/controls/Button.d.mts +16 -0
- package/dist/Screen/controls/Button.d.mts.map +1 -0
- package/dist/Screen/controls/Button.mjs +34 -0
- package/dist/Screen/controls/Button.mjs.map +1 -0
- package/dist/Screen/controls/Checkbox.d.mts +23 -0
- package/dist/Screen/controls/Checkbox.d.mts.map +1 -0
- package/dist/Screen/controls/Checkbox.mjs +55 -0
- package/dist/Screen/controls/Checkbox.mjs.map +1 -0
- package/dist/Screen/controls/LineChart.d.mts +29 -0
- package/dist/Screen/controls/LineChart.d.mts.map +1 -0
- package/dist/Screen/controls/LineChart.mjs +172 -0
- package/dist/Screen/controls/LineChart.mjs.map +1 -0
- package/dist/Screen/controls/ListBox.d.mts +34 -0
- package/dist/Screen/controls/ListBox.d.mts.map +1 -0
- package/dist/Screen/controls/ListBox.mjs +138 -0
- package/dist/Screen/controls/ListBox.mjs.map +1 -0
- package/dist/Screen/controls/ProgressBar.d.mts +26 -0
- package/dist/Screen/controls/ProgressBar.d.mts.map +1 -0
- package/dist/Screen/controls/ProgressBar.mjs +70 -0
- package/dist/Screen/controls/ProgressBar.mjs.map +1 -0
- package/dist/Screen/controls/ProgressBarV.d.mts +22 -0
- package/dist/Screen/controls/ProgressBarV.d.mts.map +1 -0
- package/dist/Screen/controls/ProgressBarV.mjs +61 -0
- package/dist/Screen/controls/ProgressBarV.mjs.map +1 -0
- package/dist/Screen/controls/Radio.d.mts +23 -0
- package/dist/Screen/controls/Radio.d.mts.map +1 -0
- package/dist/Screen/controls/Radio.mjs +55 -0
- package/dist/Screen/controls/Radio.mjs.map +1 -0
- package/dist/Screen/controls/Sparkline.d.mts +29 -0
- package/dist/Screen/controls/Sparkline.d.mts.map +1 -0
- package/dist/Screen/controls/Sparkline.mjs +82 -0
- package/dist/Screen/controls/Sparkline.mjs.map +1 -0
- package/dist/Screen/controls/Spinner.d.mts +37 -0
- package/dist/Screen/controls/Spinner.d.mts.map +1 -0
- package/dist/Screen/controls/Spinner.mjs +87 -0
- package/dist/Screen/controls/Spinner.mjs.map +1 -0
- package/dist/Screen/controls/StatusLED.d.mts +22 -0
- package/dist/Screen/controls/StatusLED.d.mts.map +1 -0
- package/dist/Screen/controls/StatusLED.mjs +51 -0
- package/dist/Screen/controls/StatusLED.mjs.map +1 -0
- package/dist/Screen/controls/Tabs.d.mts +42 -0
- package/dist/Screen/controls/Tabs.d.mts.map +1 -0
- package/dist/Screen/controls/Tabs.mjs +126 -0
- package/dist/Screen/controls/Tabs.mjs.map +1 -0
- package/dist/Screen/controls/TextArea.d.mts +41 -0
- package/dist/Screen/controls/TextArea.d.mts.map +1 -0
- package/dist/Screen/controls/TextArea.mjs +197 -0
- package/dist/Screen/controls/TextArea.mjs.map +1 -0
- package/dist/Screen/controls/TextBox.d.mts +35 -0
- package/dist/Screen/controls/TextBox.d.mts.map +1 -0
- package/dist/Screen/controls/TextBox.mjs +135 -0
- package/dist/Screen/controls/TextBox.mjs.map +1 -0
- package/dist/Screen/types.d.mts +399 -0
- package/dist/Screen/types.d.mts.map +1 -0
- package/dist/Screen/types.mjs +22 -0
- package/dist/Screen/types.mjs.map +1 -0
- package/dist/index.d.mts +26 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +41 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +72 -0
- package/src/Screen/InterfaceBuilder.mts +403 -0
- package/src/Screen/Pos.mts +119 -0
- package/src/Screen/Region.mts +88 -0
- package/src/Screen/RegistryHolder.mts +16 -0
- package/src/Screen/Screen.mts +103 -0
- package/src/Screen/Size.mts +55 -0
- package/src/Screen/StyleRegistry.mts +95 -0
- package/src/Screen/Window.mts +439 -0
- package/src/Screen/WindowManager.mts +472 -0
- package/src/Screen/controls/BarChart.mts +109 -0
- package/src/Screen/controls/Button.mts +40 -0
- package/src/Screen/controls/Checkbox.mts +66 -0
- package/src/Screen/controls/LineChart.mts +202 -0
- package/src/Screen/controls/ListBox.mts +154 -0
- package/src/Screen/controls/ProgressBar.mts +88 -0
- package/src/Screen/controls/ProgressBarV.mts +77 -0
- package/src/Screen/controls/Radio.mts +66 -0
- package/src/Screen/controls/Sparkline.mts +101 -0
- package/src/Screen/controls/Spinner.mts +102 -0
- package/src/Screen/controls/StatusLED.mts +65 -0
- package/src/Screen/controls/Tabs.mts +140 -0
- package/src/Screen/controls/TextArea.mts +194 -0
- package/src/Screen/controls/TextBox.mts +139 -0
- package/src/Screen/types.mts +416 -0
- package/src/demo.mts +171 -0
- package/src/index.mts +105 -0
- package/src/layout.yaml +236 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { CellAttributes, StyleId } from './types.mjs';
|
|
2
|
+
import { StyleRegistry } from './StyleRegistry.mjs';
|
|
3
|
+
import { getRegistry, setRegistry } from './RegistryHolder.mjs';
|
|
4
|
+
import { Window } from './Window.mjs';
|
|
5
|
+
import { Pos } from './Pos.mjs';
|
|
6
|
+
import { Size } from './Size.mjs';
|
|
7
|
+
|
|
8
|
+
export class Screen extends Window {
|
|
9
|
+
/** Initializes the root window sized to the current terminal dimensions.
|
|
10
|
+
* Creates a fresh StyleRegistry (with built-in styles pre-registered)
|
|
11
|
+
* and installs it as the global singleton. */
|
|
12
|
+
public constructor() {
|
|
13
|
+
const width = process.stdout.columns ?? 80;
|
|
14
|
+
const height = process.stdout.rows ?? 24;
|
|
15
|
+
const registry = new StyleRegistry();
|
|
16
|
+
setRegistry(registry);
|
|
17
|
+
super({ pos: Pos.topLeft(), size: new Size(width, height), background: 0 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Registers a CellAttributes object in the global style registry and returns its stable ID. */
|
|
21
|
+
public registerStyle(attrs: CellAttributes): StyleId {
|
|
22
|
+
return getRegistry().register(attrs);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Returns the global StyleRegistry. */
|
|
26
|
+
public getStyleRegistry(): StyleRegistry {
|
|
27
|
+
return getRegistry();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Overrides a built-in named style (or registers any named style) and returns its new ID.
|
|
31
|
+
* Controls will use the updated style on their next render() call. */
|
|
32
|
+
public setBuiltinStyle(name: string, attrs: CellAttributes): StyleId {
|
|
33
|
+
return getRegistry().registerNamed(name, attrs);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Composites the full window tree, then writes the result to stdout as a single ANSI string.
|
|
38
|
+
*/
|
|
39
|
+
public override render(): void {
|
|
40
|
+
super.render();
|
|
41
|
+
|
|
42
|
+
const reg = getRegistry();
|
|
43
|
+
const chars = this.region.getChars();
|
|
44
|
+
const styleIds = this.region.getStyleIds();
|
|
45
|
+
let output = '\x1b[H';
|
|
46
|
+
for (let i = 0; i < chars.length; i++) {
|
|
47
|
+
output += this.buildAnsiSequence(reg.get(styleIds[i]));
|
|
48
|
+
output += chars[i];
|
|
49
|
+
}
|
|
50
|
+
output += '\x1b[0m';
|
|
51
|
+
process.stdout.write(output);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Converts cell attributes into an ANSI escape sequence (always resets first). */
|
|
55
|
+
private buildAnsiSequence(attrs: CellAttributes): string {
|
|
56
|
+
const codes: number[] = [0];
|
|
57
|
+
|
|
58
|
+
if (attrs.bold) codes.push(1);
|
|
59
|
+
if (attrs.dim) codes.push(2);
|
|
60
|
+
if (attrs.italic) codes.push(3);
|
|
61
|
+
if (attrs.underline) codes.push(4);
|
|
62
|
+
if (attrs.blink) codes.push(5);
|
|
63
|
+
if (attrs.inverse) codes.push(7);
|
|
64
|
+
if (attrs.strikethrough) codes.push(9);
|
|
65
|
+
|
|
66
|
+
if (attrs.foreground !== undefined) {
|
|
67
|
+
if (typeof attrs.foreground === 'number') {
|
|
68
|
+
codes.push(38, 5, attrs.foreground);
|
|
69
|
+
} else {
|
|
70
|
+
const [r, g, b] = this.hexToRgb(attrs.foreground);
|
|
71
|
+
codes.push(38, 2, r, g, b);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (attrs.background !== undefined) {
|
|
76
|
+
if (typeof attrs.background === 'number') {
|
|
77
|
+
codes.push(48, 5, attrs.background);
|
|
78
|
+
} else {
|
|
79
|
+
const [r, g, b] = this.hexToRgb(attrs.background);
|
|
80
|
+
codes.push(48, 2, r, g, b);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return `\x1b[${codes.join(';')}m`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Parses a hex color string (#rrggbb or #rgb) into [r, g, b] components. */
|
|
88
|
+
private hexToRgb(hex: string): [number, number, number] {
|
|
89
|
+
const clean = hex.replace('#', '');
|
|
90
|
+
if (clean.length === 3) {
|
|
91
|
+
return [
|
|
92
|
+
parseInt(clean[0] + clean[0], 16),
|
|
93
|
+
parseInt(clean[1] + clean[1], 16),
|
|
94
|
+
parseInt(clean[2] + clean[2], 16),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
return [
|
|
98
|
+
parseInt(clean.slice(0, 2), 16),
|
|
99
|
+
parseInt(clean.slice(2, 4), 16),
|
|
100
|
+
parseInt(clean.slice(4, 6), 16),
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { DimSpec } from './types.mjs';
|
|
2
|
+
import { Pct } from './Pos.mjs';
|
|
3
|
+
|
|
4
|
+
/** Converts a user-supplied dimension value to an internal DimSpec. */
|
|
5
|
+
function toDimSpec(v: number | Pct): DimSpec {
|
|
6
|
+
if (v instanceof Pct) return { mode: 'pct', value: v.value };
|
|
7
|
+
return { mode: 'abs', value: v };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Resolves a single dimension spec to a pixel value. */
|
|
11
|
+
function resolveDim(spec: DimSpec, parentSize: number): number {
|
|
12
|
+
if (spec.mode === 'pct') return Math.floor(parentSize * spec.value / 100);
|
|
13
|
+
return spec.value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Encodes window dimensions. Supports absolute pixel values and percentage of parent size. */
|
|
17
|
+
export class Size {
|
|
18
|
+
private wSpec: DimSpec;
|
|
19
|
+
private hSpec: DimSpec;
|
|
20
|
+
|
|
21
|
+
/** Creates a size from width and height values.
|
|
22
|
+
* Pass a plain number for absolute pixels, or a Pct instance for a percentage of the parent. */
|
|
23
|
+
public constructor(w: number | Pct, h: number | Pct) {
|
|
24
|
+
this.wSpec = toDimSpec(w);
|
|
25
|
+
this.hSpec = toDimSpec(h);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Creates a size that fills 100% of the parent in both dimensions. */
|
|
29
|
+
public static fill(): Size {
|
|
30
|
+
return new Size(new Pct(100), new Pct(100));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Creates a size that fills 100% of the parent's width; height is absolute or percentage. */
|
|
34
|
+
public static fillWidth(h: number | Pct): Size {
|
|
35
|
+
return new Size(new Pct(100), h);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Creates a size that fills 100% of the parent's height; width is absolute or percentage. */
|
|
39
|
+
public static fillHeight(w: number | Pct): Size {
|
|
40
|
+
return new Size(w, new Pct(100));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Returns true when both dimensions are absolute pixel values (no parent size needed). */
|
|
44
|
+
public isAbsolute(): boolean {
|
|
45
|
+
return this.wSpec.mode === 'abs' && this.hSpec.mode === 'abs';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Resolves this size to pixel dimensions given the parent's dimensions. */
|
|
49
|
+
public resolve(parentW: number, parentH: number): { w: number; h: number } {
|
|
50
|
+
return {
|
|
51
|
+
w: resolveDim(this.wSpec, parentW),
|
|
52
|
+
h: resolveDim(this.hSpec, parentH),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { CellAttributes, StyleId, Color } from './types.mjs';
|
|
2
|
+
import {
|
|
3
|
+
BUILTIN_WINDOW_BG,
|
|
4
|
+
BUILTIN_BORDER,
|
|
5
|
+
BUILTIN_BORDER_FOCUSED,
|
|
6
|
+
BUILTIN_BORDER_DISABLED,
|
|
7
|
+
BUILTIN_TEXT,
|
|
8
|
+
BUILTIN_TEXT_FOCUSED,
|
|
9
|
+
BUILTIN_TEXT_DISABLED,
|
|
10
|
+
BUILTIN_TEXT_PLACEHOLDER,
|
|
11
|
+
BUILTIN_TEXT_CHECKED,
|
|
12
|
+
BUILTIN_CURSOR,
|
|
13
|
+
} from './types.mjs';
|
|
14
|
+
|
|
15
|
+
/** Central registry that maps integer style IDs to CellAttributes objects.
|
|
16
|
+
* Identical attribute sets always map to the same ID (deduplication). */
|
|
17
|
+
export class StyleRegistry {
|
|
18
|
+
/** Styles indexed by ID. Index 0 is always the empty style {}. */
|
|
19
|
+
private styles: CellAttributes[] = [{}];
|
|
20
|
+
/** Serialized key → ID, for deduplication. */
|
|
21
|
+
private index: Map<string, StyleId> = new Map([['{}', 0]]);
|
|
22
|
+
/** Name → ID map for built-in and user-defined named styles. */
|
|
23
|
+
private named: Map<string, StyleId> = new Map();
|
|
24
|
+
|
|
25
|
+
/** Creates a new StyleRegistry with built-in named styles pre-registered. */
|
|
26
|
+
public constructor() {
|
|
27
|
+
this.registerNamed(BUILTIN_WINDOW_BG, { background: 237 });
|
|
28
|
+
this.registerNamed(BUILTIN_BORDER, { foreground: 240 });
|
|
29
|
+
this.registerNamed(BUILTIN_BORDER_FOCUSED, { foreground: 75 });
|
|
30
|
+
this.registerNamed(BUILTIN_BORDER_DISABLED, { foreground: 238 });
|
|
31
|
+
this.registerNamed(BUILTIN_TEXT, { foreground: 252 });
|
|
32
|
+
this.registerNamed(BUILTIN_TEXT_FOCUSED, { foreground: 255, bold: true });
|
|
33
|
+
this.registerNamed(BUILTIN_TEXT_DISABLED, { foreground: 245, dim: true });
|
|
34
|
+
this.registerNamed(BUILTIN_TEXT_PLACEHOLDER, { foreground: 242, italic: true });
|
|
35
|
+
this.registerNamed(BUILTIN_TEXT_CHECKED, { foreground: 76, bold: true });
|
|
36
|
+
this.registerNamed(BUILTIN_CURSOR, { inverse: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Registers a CellAttributes object and returns its stable ID.
|
|
40
|
+
* If an identical style was registered before, returns the existing ID. */
|
|
41
|
+
public register(attrs: CellAttributes): StyleId {
|
|
42
|
+
const key = this.makeKey(attrs);
|
|
43
|
+
const existing = this.index.get(key);
|
|
44
|
+
if (existing !== undefined) return existing;
|
|
45
|
+
const id = this.styles.length;
|
|
46
|
+
this.styles.push({ ...attrs });
|
|
47
|
+
this.index.set(key, id);
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Registers a CellAttributes object under a given name and returns its stable ID.
|
|
52
|
+
* Calling with the same name again replaces the previous association. */
|
|
53
|
+
public registerNamed(name: string, attrs: CellAttributes): StyleId {
|
|
54
|
+
const id = this.register(attrs);
|
|
55
|
+
this.named.set(name, id);
|
|
56
|
+
return id;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Returns the StyleId associated with the given name, or undefined if not registered. */
|
|
60
|
+
public getNamed(name: string): StyleId | undefined {
|
|
61
|
+
return this.named.get(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Returns the foreground Color of a named style, or the fallback value if the name is
|
|
65
|
+
* not registered or the style has no foreground attribute. */
|
|
66
|
+
public getNamedForeground(name: string, fallback: Color): Color {
|
|
67
|
+
const id = this.named.get(name);
|
|
68
|
+
if (id === undefined) return fallback;
|
|
69
|
+
return this.styles[id]?.foreground ?? fallback;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Returns the CellAttributes for the given style ID. Returns {} for unknown IDs. */
|
|
73
|
+
public get(id: StyleId): CellAttributes {
|
|
74
|
+
return this.styles[id] ?? {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Merges two styles and returns the ID of the result.
|
|
78
|
+
* The `over` style takes precedence over `base` for conflicting keys. */
|
|
79
|
+
public merge(baseId: StyleId, overId: StyleId): StyleId {
|
|
80
|
+
if (overId === 0) return baseId;
|
|
81
|
+
if (baseId === 0) return overId;
|
|
82
|
+
return this.register({ ...this.get(baseId), ...this.get(overId) });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Produces a deterministic JSON key for deduplication. */
|
|
86
|
+
private makeKey(attrs: CellAttributes): string {
|
|
87
|
+
const keys = Object.keys(attrs).sort() as (keyof CellAttributes)[];
|
|
88
|
+
if (keys.length === 0) return '{}';
|
|
89
|
+
const sorted: Partial<CellAttributes> = {};
|
|
90
|
+
for (const k of keys) {
|
|
91
|
+
(sorted as Record<string, unknown>)[k] = attrs[k];
|
|
92
|
+
}
|
|
93
|
+
return JSON.stringify(sorted);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import type { Cell, StyleId, BorderStyle, WindowBorder, WindowProperties, WriteTextOptions, TerminalSize } from './types.mjs';
|
|
2
|
+
import { BUILTIN_TEXT, BUILTIN_TEXT_FOCUSED, BUILTIN_TEXT_DISABLED, BUILTIN_BORDER, BUILTIN_BORDER_FOCUSED, BUILTIN_BORDER_DISABLED } from './types.mjs';
|
|
3
|
+
import { Region } from './Region.mjs';
|
|
4
|
+
import { StyleRegistry } from './StyleRegistry.mjs';
|
|
5
|
+
import { getRegistry } from './RegistryHolder.mjs';
|
|
6
|
+
import type { Pos } from './Pos.mjs';
|
|
7
|
+
import { Size } from './Size.mjs';
|
|
8
|
+
|
|
9
|
+
/** Characters used for each border style. */
|
|
10
|
+
const BORDER_CHARS: Record<BorderStyle, { h: string; v: string; tl: string; tr: string; bl: string; br: string }> = {
|
|
11
|
+
single: { h: '─', v: '│', tl: '┌', tr: '┐', bl: '└', br: '┘' },
|
|
12
|
+
double: { h: '═', v: '║', tl: '╔', tr: '╗', bl: '╚', br: '╝' },
|
|
13
|
+
rounded: { h: '─', v: '│', tl: '╭', tr: '╮', bl: '╰', br: '╯' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Resolves a border option (true / object / false) to a full WindowBorder or false. */
|
|
17
|
+
const resolveBorder = (border: WindowBorder | boolean | undefined): WindowBorder | false => {
|
|
18
|
+
if (!border) return false;
|
|
19
|
+
if (border === true) return { top: true, right: true, bottom: true, left: true };
|
|
20
|
+
return border;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class Window {
|
|
24
|
+
public x: number;
|
|
25
|
+
public y: number;
|
|
26
|
+
/** Composited display buffer – rebuilt by render(); read by blitChild and Screen. */
|
|
27
|
+
protected region: Region;
|
|
28
|
+
/** Global style registry shared by all windows and controls. */
|
|
29
|
+
protected registry: StyleRegistry;
|
|
30
|
+
protected children: Window[];
|
|
31
|
+
/** Whether this window currently has keyboard focus. */
|
|
32
|
+
protected focused: boolean = false;
|
|
33
|
+
/** Whether this window is disabled (inactive and visually dimmed). */
|
|
34
|
+
protected disabled: boolean = false;
|
|
35
|
+
/** Optional text label displayed by the control. */
|
|
36
|
+
protected label: string = '';
|
|
37
|
+
/** Style ID used for normal (default) text rendering. Initialized from BUILTIN_TEXT. */
|
|
38
|
+
protected normalStyleId!: StyleId;
|
|
39
|
+
/** Style ID used when the control is focused. Initialized from BUILTIN_TEXT_FOCUSED. */
|
|
40
|
+
protected focusedStyleId!: StyleId;
|
|
41
|
+
/** Style ID used when the control is disabled. Initialized from BUILTIN_TEXT_DISABLED. */
|
|
42
|
+
protected disabledStyleId!: StyleId;
|
|
43
|
+
|
|
44
|
+
/** User-written content – survives render() cycles. */
|
|
45
|
+
private content: Region;
|
|
46
|
+
private background: StyleId;
|
|
47
|
+
private border: WindowBorder | false;
|
|
48
|
+
/** True when the original border config included an explicit color; prevents auto-sync. */
|
|
49
|
+
private borderColorExplicit: boolean = false;
|
|
50
|
+
private active: boolean;
|
|
51
|
+
private posSpec: Pos;
|
|
52
|
+
private sizeSpec: Size;
|
|
53
|
+
|
|
54
|
+
/** Creates a window from the given properties.
|
|
55
|
+
* For percentage-based sizes, call addChild() before writing content to the window.
|
|
56
|
+
* Uses the global StyleRegistry set by the Screen constructor. */
|
|
57
|
+
public constructor(wp: WindowProperties) {
|
|
58
|
+
const pos = wp.pos;
|
|
59
|
+
const size = wp.size ?? new Size(1, 1);
|
|
60
|
+
|
|
61
|
+
this.posSpec = pos;
|
|
62
|
+
this.sizeSpec = size;
|
|
63
|
+
this.registry = getRegistry();
|
|
64
|
+
this.normalStyleId = this.registry.getNamed(BUILTIN_TEXT)!;
|
|
65
|
+
this.focusedStyleId = this.registry.getNamed(BUILTIN_TEXT_FOCUSED)!;
|
|
66
|
+
this.disabledStyleId = this.registry.getNamed(BUILTIN_TEXT_DISABLED)!;
|
|
67
|
+
this.children = [];
|
|
68
|
+
this.focused = wp.focused ?? false;
|
|
69
|
+
this.disabled = wp.disabled ?? false;
|
|
70
|
+
this.active = wp.active ?? !this.disabled;
|
|
71
|
+
this.label = wp.label ?? '';
|
|
72
|
+
this.background = wp.background ?? 0;
|
|
73
|
+
this.border = resolveBorder(wp.border ?? wp.defaultBorder);
|
|
74
|
+
this.borderColorExplicit = this.border !== false && this.border.color !== undefined;
|
|
75
|
+
|
|
76
|
+
const { w, h } = size.isAbsolute() ? size.resolve(0, 0) : { w: 1, h: 1 };
|
|
77
|
+
this.region = new Region(w, h);
|
|
78
|
+
this.content = new Region(w, h);
|
|
79
|
+
|
|
80
|
+
if (pos.isAbsolute()) {
|
|
81
|
+
const abs = pos.resolveAbsolute();
|
|
82
|
+
this.x = abs.x;
|
|
83
|
+
this.y = abs.y;
|
|
84
|
+
} else {
|
|
85
|
+
this.x = 0;
|
|
86
|
+
this.y = 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Returns the window dimensions (columns × rows). */
|
|
91
|
+
public getSize(): TerminalSize {
|
|
92
|
+
return this.region.getSize();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Returns the number of cells consumed by decorations on each edge. */
|
|
96
|
+
private borderInset(): { top: number; right: number; bottom: number; left: number } {
|
|
97
|
+
const b = this.border;
|
|
98
|
+
if (!b) return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
99
|
+
return {
|
|
100
|
+
top: (b.top ?? false) ? 1 : 0,
|
|
101
|
+
right: (b.right ?? false) ? 1 : 0,
|
|
102
|
+
bottom: (b.bottom ?? false) ? 1 : 0,
|
|
103
|
+
left: (b.left ?? false) ? 1 : 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Returns the top-left offset of the content area, accounting for decorations such as borders. */
|
|
108
|
+
public getInnerOffset(): { x: number; y: number } {
|
|
109
|
+
const { left, top } = this.borderInset();
|
|
110
|
+
return { x: left, y: top };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Returns the dimensions of the content area, accounting for decorations such as borders. */
|
|
114
|
+
public getInnerSize(): TerminalSize {
|
|
115
|
+
const { width, height } = this.getSize();
|
|
116
|
+
const { top, right, bottom, left } = this.borderInset();
|
|
117
|
+
return {
|
|
118
|
+
width: Math.max(0, width - left - right),
|
|
119
|
+
height: Math.max(0, height - top - bottom),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Sets the active state. Affects border and background appearance on next render(). */
|
|
124
|
+
public setActive(active: boolean): void {
|
|
125
|
+
this.active = active;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Sets the focused state. Controls use this to change visual appearance on focus. */
|
|
129
|
+
public setFocused(focused: boolean): void {
|
|
130
|
+
this.focused = focused;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Returns whether this window currently has keyboard focus. */
|
|
134
|
+
public isFocused(): boolean {
|
|
135
|
+
return this.focused;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Sets the disabled state and deactivates the window when disabled. */
|
|
139
|
+
public setDisabled(disabled: boolean): void {
|
|
140
|
+
this.disabled = disabled;
|
|
141
|
+
this.setActive(!disabled);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Returns whether this window is currently disabled. */
|
|
145
|
+
public isDisabled(): boolean {
|
|
146
|
+
return this.disabled;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Sets the label text displayed by the control. */
|
|
150
|
+
public setLabel(label: string): void {
|
|
151
|
+
this.label = label;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Returns the current label text. */
|
|
155
|
+
public getLabel(): string {
|
|
156
|
+
return this.label;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Updates the border configuration. Effective on the next render() call.
|
|
160
|
+
* Intended for use by subclasses that need dynamic decoration (e.g. focus-state colour). */
|
|
161
|
+
protected updateBorder(border: WindowBorder | boolean | undefined): void {
|
|
162
|
+
this.border = resolveBorder(border);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Recomputes the border color from the current focused/disabled state.
|
|
166
|
+
* Called automatically at the start of render() so subclasses never need to do it manually.
|
|
167
|
+
* Only updates the color when no explicit color was provided in the original border config. */
|
|
168
|
+
private syncBorderColor(): void {
|
|
169
|
+
if (!this.border || this.borderColorExplicit) return;
|
|
170
|
+
this.border.color = this.disabled
|
|
171
|
+
? this.registry.getNamedForeground(BUILTIN_BORDER_DISABLED, 238)
|
|
172
|
+
: this.focused
|
|
173
|
+
? this.registry.getNamedForeground(BUILTIN_BORDER_FOCUSED, 75)
|
|
174
|
+
: this.registry.getNamedForeground(BUILTIN_BORDER, 240);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Adds a child window. If the child uses percentage-based sizes they are resolved immediately
|
|
178
|
+
* against this window's inner dimensions (excluding decorations such as borders).
|
|
179
|
+
* Position is resolved relative to the inner area and stored on child.x/y. */
|
|
180
|
+
public addChild(child: Window): void {
|
|
181
|
+
const { width: pw, height: ph } = this.getInnerSize();
|
|
182
|
+
const { x: ox, y: oy } = this.getInnerOffset();
|
|
183
|
+
if (!child.sizeSpec.isAbsolute()) {
|
|
184
|
+
const { w, h } = child.sizeSpec.resolve(pw, ph);
|
|
185
|
+
child.resizeRegions(w, h);
|
|
186
|
+
}
|
|
187
|
+
const { width: cw, height: ch } = child.getSize();
|
|
188
|
+
const { x, y } = child.posSpec.resolve(pw, ph, cw, ch);
|
|
189
|
+
child.x = x + ox;
|
|
190
|
+
child.y = y + oy;
|
|
191
|
+
this.children.push(child);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Returns a resolved Cell (char + CellAttributes) at (x, y) from the display buffer.
|
|
195
|
+
* Throws RangeError if out of bounds. */
|
|
196
|
+
public getCell(x: number, y: number): Cell {
|
|
197
|
+
const char = this.region.getChars()[this.flatIndex(x, y)];
|
|
198
|
+
const styleId = this.region.getStyleId(x, y);
|
|
199
|
+
const attributes = { ...this.registry.get(styleId) };
|
|
200
|
+
return { char, attributes };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Sets only the character at (x, y) without modifying the style ID. Throws RangeError if out of bounds. */
|
|
204
|
+
public setChar(x: number, y: number, char: string): void {
|
|
205
|
+
this.content.setChar(x, y, char);
|
|
206
|
+
this.region.setChar(x, y, char);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Sets the character and style ID at (x, y). Throws RangeError if out of bounds. */
|
|
210
|
+
public setCell(x: number, y: number, char: string, styleId: StyleId = 0): void {
|
|
211
|
+
this.content.setCell(x, y, char, styleId);
|
|
212
|
+
this.region.setCell(x, y, char, styleId);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Merges the given style ID onto the existing style at (x, y) without changing the character.
|
|
216
|
+
* Throws RangeError if out of bounds. */
|
|
217
|
+
public mergeStyle(x: number, y: number, styleId: StyleId): void {
|
|
218
|
+
const mergedContent = this.registry.merge(this.content.getStyleId(x, y), styleId);
|
|
219
|
+
const mergedRegion = this.registry.merge(this.region.getStyleId(x, y), styleId);
|
|
220
|
+
this.content.setStyleId(x, y, mergedContent);
|
|
221
|
+
this.region.setStyleId(x, y, mergedRegion);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Resets every cell to a blank space with style ID 0. */
|
|
225
|
+
public clear(): void {
|
|
226
|
+
this.content.clear();
|
|
227
|
+
this.region.clear();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Fills every cell with the given character and style ID. */
|
|
231
|
+
public fill(char: string, styleId: StyleId = 0): void {
|
|
232
|
+
this.content.fill(char, styleId);
|
|
233
|
+
this.region.fill(char, styleId);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Writes text into the window's content area starting at (x, y) (default 0, 0).
|
|
237
|
+
* Coordinates are relative to the inner content area (i.e. decorations such as borders are excluded).
|
|
238
|
+
* Newline characters move to the next row, resetting x to startX.
|
|
239
|
+
* Characters outside the inner bounds are silently clipped.
|
|
240
|
+
* When no style is provided, automatically picks disabledStyleId, focusedStyleId, or normalStyleId
|
|
241
|
+
* based on the current disabled/focused state. */
|
|
242
|
+
public writeText(text: string, options?: WriteTextOptions): void {
|
|
243
|
+
const { x: ox, y: oy } = this.getInnerOffset();
|
|
244
|
+
const { width: iw, height: ih } = this.getInnerSize();
|
|
245
|
+
const startX = (options?.x ?? 0) + ox;
|
|
246
|
+
const startY = (options?.y ?? 0) + oy;
|
|
247
|
+
const styleId = options?.style ?? (
|
|
248
|
+
this.disabled ? this.disabledStyleId :
|
|
249
|
+
this.focused ? this.focusedStyleId :
|
|
250
|
+
this.normalStyleId
|
|
251
|
+
);
|
|
252
|
+
let cx = startX;
|
|
253
|
+
let cy = startY;
|
|
254
|
+
for (const ch of text) {
|
|
255
|
+
if (ch === '\n') {
|
|
256
|
+
cx = startX;
|
|
257
|
+
cy++;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (cx >= ox && cx < ox + iw && cy >= oy && cy < oy + ih) {
|
|
261
|
+
this.setCell(cx, cy, ch, styleId);
|
|
262
|
+
}
|
|
263
|
+
cx++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Builds the display buffer: background → user content → border → children.
|
|
269
|
+
* The result is stored in region and used by blitChild / Screen.render().
|
|
270
|
+
*/
|
|
271
|
+
public render(): void {
|
|
272
|
+
this.syncBorderColor();
|
|
273
|
+
this.paintBackground();
|
|
274
|
+
this.blitContent();
|
|
275
|
+
this.paintBorder();
|
|
276
|
+
for (const child of this.children) {
|
|
277
|
+
child.render();
|
|
278
|
+
this.blitChild(child);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Fills the display buffer with the background style. When inactive, adds dim to every cell.
|
|
283
|
+
* No-op when background is 0 (transparent). */
|
|
284
|
+
private paintBackground(): void {
|
|
285
|
+
if (this.background === 0) return;
|
|
286
|
+
let bgId = this.background;
|
|
287
|
+
if (!this.active) {
|
|
288
|
+
bgId = this.registry.merge(bgId, this.registry.register({ dim: true }));
|
|
289
|
+
}
|
|
290
|
+
this.region.fill(' ', bgId);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Overlays user-written content onto the display buffer, merging with any background already set. */
|
|
294
|
+
private blitContent(): void {
|
|
295
|
+
const contentChars = this.content.getChars();
|
|
296
|
+
const contentStyleIds = this.content.getStyleIds();
|
|
297
|
+
const regionStyleIds = this.region.getStyleIds();
|
|
298
|
+
const { width, height } = this.content.getSize();
|
|
299
|
+
for (let y = 0; y < height; y++) {
|
|
300
|
+
for (let x = 0; x < width; x++) {
|
|
301
|
+
const i = y * width + x;
|
|
302
|
+
const mergedId = this.registry.merge(regionStyleIds[i], contentStyleIds[i]);
|
|
303
|
+
this.region.setCell(x, y, contentChars[i], mergedId);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Draws border characters on the display buffer edges. When inactive, adds dim to border cells. */
|
|
309
|
+
private paintBorder(): void {
|
|
310
|
+
if (!this.border) return;
|
|
311
|
+
const b = this.border;
|
|
312
|
+
const { width, height } = this.region.getSize();
|
|
313
|
+
if (width < 2 && height < 2) return;
|
|
314
|
+
const chars = BORDER_CHARS[b.style ?? 'single'];
|
|
315
|
+
|
|
316
|
+
const bgColor = this.background !== 0
|
|
317
|
+
? this.registry.get(this.background).background
|
|
318
|
+
: undefined;
|
|
319
|
+
|
|
320
|
+
const baseAttrsParts: import('./types.mjs').CellAttributes = {};
|
|
321
|
+
if (b.color !== undefined) baseAttrsParts.foreground = b.color;
|
|
322
|
+
if (bgColor !== undefined) baseAttrsParts.background = bgColor;
|
|
323
|
+
if (!this.active) baseAttrsParts.dim = true;
|
|
324
|
+
const baseId = this.registry.register(baseAttrsParts);
|
|
325
|
+
|
|
326
|
+
const top = b.top ?? false;
|
|
327
|
+
const bottom = b.bottom ?? false;
|
|
328
|
+
const left = b.left ?? false;
|
|
329
|
+
const right = b.right ?? false;
|
|
330
|
+
|
|
331
|
+
// top row
|
|
332
|
+
if (top && height >= 1) {
|
|
333
|
+
for (let x = 0; x < width; x++) {
|
|
334
|
+
const isLeft = x === 0;
|
|
335
|
+
const isRight = x === width - 1;
|
|
336
|
+
let ch: string;
|
|
337
|
+
if (isLeft && left) ch = chars.tl;
|
|
338
|
+
else if (isRight && right) ch = chars.tr;
|
|
339
|
+
else ch = chars.h;
|
|
340
|
+
this.region.setCell(x, 0, ch, baseId);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// bottom row
|
|
345
|
+
if (bottom && height >= 2) {
|
|
346
|
+
for (let x = 0; x < width; x++) {
|
|
347
|
+
const isLeft = x === 0;
|
|
348
|
+
const isRight = x === width - 1;
|
|
349
|
+
let ch: string;
|
|
350
|
+
if (isLeft && left) ch = chars.bl;
|
|
351
|
+
else if (isRight && right) ch = chars.br;
|
|
352
|
+
else ch = chars.h;
|
|
353
|
+
this.region.setCell(x, height - 1, ch, baseId);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// left column (skip corners already drawn)
|
|
358
|
+
if (left && width >= 1) {
|
|
359
|
+
const rowStart = top ? 1 : 0;
|
|
360
|
+
const rowEnd = bottom ? height - 2 : height - 1;
|
|
361
|
+
for (let y = rowStart; y <= rowEnd; y++) {
|
|
362
|
+
this.region.setCell(0, y, chars.v, baseId);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// right column (skip corners already drawn)
|
|
367
|
+
if (right && width >= 2) {
|
|
368
|
+
const rowStart = top ? 1 : 0;
|
|
369
|
+
const rowEnd = bottom ? height - 2 : height - 1;
|
|
370
|
+
for (let y = rowStart; y <= rowEnd; y++) {
|
|
371
|
+
this.region.setCell(width - 1, y, chars.v, baseId);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Copies a child's display buffer onto this window's display buffer, clipping to this window's bounds.
|
|
377
|
+
* Styles are transferred from the child's registry into this window's registry.
|
|
378
|
+
* Position is re-resolved from the child's Pos spec relative to the inner content area on every render. */
|
|
379
|
+
private blitChild(child: Window): void {
|
|
380
|
+
const chars = child.region.getChars();
|
|
381
|
+
const childStyleIds = child.region.getStyleIds();
|
|
382
|
+
const { width: cw, height: ch } = child.getSize();
|
|
383
|
+
const { width: pw, height: ph } = this.getInnerSize();
|
|
384
|
+
const { x: ox, y: oy } = this.getInnerOffset();
|
|
385
|
+
const { x: cx, y: cy } = child.posSpec.resolve(pw, ph, cw, ch);
|
|
386
|
+
const ax = cx + ox;
|
|
387
|
+
const ay = cy + oy;
|
|
388
|
+
const { width: totalW, height: totalH } = this.getSize();
|
|
389
|
+
|
|
390
|
+
for (let childY = 0; childY < ch; childY++) {
|
|
391
|
+
for (let childX = 0; childX < cw; childX++) {
|
|
392
|
+
const px = ax + childX;
|
|
393
|
+
const py = ay + childY;
|
|
394
|
+
if (px >= 0 && px < totalW && py >= 0 && py < totalH) {
|
|
395
|
+
const i = childY * cw + childX;
|
|
396
|
+
const attrs = child.registry.get(childStyleIds[i]);
|
|
397
|
+
const parentId = this.registry.register(attrs);
|
|
398
|
+
this.region.setCell(px, py, chars[i], parentId);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Removes a previously added child window. No-op if the child is not found. */
|
|
405
|
+
public removeChild(child: Window): void {
|
|
406
|
+
const idx = this.children.indexOf(child);
|
|
407
|
+
if (idx !== -1) this.children.splice(idx, 1);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Replaces both internal regions with new ones of the given dimensions,
|
|
411
|
+
* then re-resolves sizes and positions of all direct children against the updated inner area. */
|
|
412
|
+
private resizeRegions(w: number, h: number): void {
|
|
413
|
+
this.region = new Region(w, h);
|
|
414
|
+
this.content = new Region(w, h);
|
|
415
|
+
this.reflowChildren();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Re-resolves sizes and absolute positions for every direct child against the current inner area.
|
|
419
|
+
* Called after this window is resized so that percentage-based children get correct dimensions. */
|
|
420
|
+
private reflowChildren(): void {
|
|
421
|
+
const { width: pw, height: ph } = this.getInnerSize();
|
|
422
|
+
const { x: ox, y: oy } = this.getInnerOffset();
|
|
423
|
+
for (const child of this.children) {
|
|
424
|
+
if (!child.sizeSpec.isAbsolute()) {
|
|
425
|
+
const { w, h } = child.sizeSpec.resolve(pw, ph);
|
|
426
|
+
child.resizeRegions(w, h);
|
|
427
|
+
}
|
|
428
|
+
const { width: cw, height: ch } = child.getSize();
|
|
429
|
+
const { x, y } = child.posSpec.resolve(pw, ph, cw, ch);
|
|
430
|
+
child.x = x + ox;
|
|
431
|
+
child.y = y + oy;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Returns the flat index for (x, y) in the region buffer (used by getCell). */
|
|
436
|
+
private flatIndex(x: number, y: number): number {
|
|
437
|
+
return y * this.region.getSize().width + x;
|
|
438
|
+
}
|
|
439
|
+
}
|