@aurigax-ai/ghostty-web 0.1.0 → 0.3.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 +6 -1
- package/lib/addons/search.ts +190 -0
- package/lib/box-glyphs.ts +278 -0
- package/lib/ghostty.ts +465 -129
- package/lib/glyph-ink.ts +42 -0
- package/lib/index.ts +8 -1
- package/lib/input-handler.ts +55 -2
- package/lib/interfaces.ts +29 -0
- package/lib/providers/osc8-link-provider.ts +4 -10
- package/lib/providers/url-regex-provider.ts +10 -13
- package/lib/providers/web-link.ts +35 -0
- package/lib/renderer.ts +781 -686
- package/lib/selection-manager.ts +8 -3
- package/lib/terminal.ts +205 -85
- package/lib/types.ts +2 -1
- package/lib/webgl-instances.ts +69 -0
- package/lib/webgl-renderer.ts +943 -0
- package/package.json +2 -2
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebGL 2 renderer.
|
|
3
|
+
*
|
|
4
|
+
* Glyphs are rasterized once into an atlas texture (box drawing and block
|
|
5
|
+
* elements from geometry, so they fill their cells); each frame only the rows
|
|
6
|
+
* that changed are rewritten into instance buffers, and the whole grid is
|
|
7
|
+
* drawn with a handful of instanced draw calls. Drawing goes to a canvas of
|
|
8
|
+
* its own laid over the terminal's canvas, which keeps taking pointer events,
|
|
9
|
+
* so a terminal can drop back to `CanvasRenderer` on the same canvas when the
|
|
10
|
+
* context is lost.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { drawBoxGlyph, isBoxGlyph } from './box-glyphs';
|
|
14
|
+
import { glyphInk } from './glyph-ink';
|
|
15
|
+
import type { ITheme } from './interfaces';
|
|
16
|
+
import {
|
|
17
|
+
type FrameOverlay,
|
|
18
|
+
LINK_COLOR,
|
|
19
|
+
type RendererOptions,
|
|
20
|
+
TerminalRenderer,
|
|
21
|
+
isBlank,
|
|
22
|
+
} from './renderer';
|
|
23
|
+
import type { GhosttyCell } from './types';
|
|
24
|
+
import { CellFlags } from './types';
|
|
25
|
+
import {
|
|
26
|
+
GLYPH_WORDS,
|
|
27
|
+
type InstanceArrays,
|
|
28
|
+
RECT_WORDS,
|
|
29
|
+
compactGlyphs,
|
|
30
|
+
compactRects,
|
|
31
|
+
instanceArrays,
|
|
32
|
+
} from './webgl-instances';
|
|
33
|
+
|
|
34
|
+
const CURSOR_RECTS = 4;
|
|
35
|
+
const SCROLLBAR_RECTS = 2;
|
|
36
|
+
const OVERLAY_RECTS = CURSOR_RECTS + SCROLLBAR_RECTS;
|
|
37
|
+
const ATLAS_START = 512;
|
|
38
|
+
const ATLAS_MAX = 4096;
|
|
39
|
+
|
|
40
|
+
const VERTEX_RECT = `#version 300 es
|
|
41
|
+
layout(location = 0) in vec2 a_corner;
|
|
42
|
+
layout(location = 1) in vec4 a_rect;
|
|
43
|
+
layout(location = 2) in vec4 a_color;
|
|
44
|
+
uniform vec2 u_resolution;
|
|
45
|
+
out vec4 v_color;
|
|
46
|
+
void main() {
|
|
47
|
+
vec2 p = a_rect.xy + a_corner * a_rect.zw;
|
|
48
|
+
vec2 clip = p / u_resolution * 2.0 - 1.0;
|
|
49
|
+
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
|
|
50
|
+
v_color = vec4(a_color.rgb * a_color.a, a_color.a);
|
|
51
|
+
}`;
|
|
52
|
+
|
|
53
|
+
const FRAGMENT_RECT = `#version 300 es
|
|
54
|
+
precision mediump float;
|
|
55
|
+
in vec4 v_color;
|
|
56
|
+
out vec4 outColor;
|
|
57
|
+
void main() {
|
|
58
|
+
outColor = v_color;
|
|
59
|
+
}`;
|
|
60
|
+
|
|
61
|
+
const VERTEX_GLYPH = `#version 300 es
|
|
62
|
+
layout(location = 0) in vec2 a_corner;
|
|
63
|
+
layout(location = 1) in vec2 a_pos;
|
|
64
|
+
layout(location = 2) in vec4 a_tex;
|
|
65
|
+
layout(location = 3) in vec4 a_color;
|
|
66
|
+
layout(location = 4) in float a_colored;
|
|
67
|
+
uniform vec2 u_resolution;
|
|
68
|
+
uniform vec2 u_atlas;
|
|
69
|
+
out vec2 v_uv;
|
|
70
|
+
out vec4 v_color;
|
|
71
|
+
out float v_colored;
|
|
72
|
+
void main() {
|
|
73
|
+
vec2 p = a_pos + a_corner * a_tex.zw;
|
|
74
|
+
vec2 clip = p / u_resolution * 2.0 - 1.0;
|
|
75
|
+
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
|
|
76
|
+
v_uv = (a_tex.xy + a_corner * a_tex.zw) / u_atlas;
|
|
77
|
+
v_color = a_color;
|
|
78
|
+
v_colored = a_colored;
|
|
79
|
+
}`;
|
|
80
|
+
|
|
81
|
+
const FRAGMENT_GLYPH = `#version 300 es
|
|
82
|
+
precision mediump float;
|
|
83
|
+
uniform sampler2D u_texture;
|
|
84
|
+
in vec2 v_uv;
|
|
85
|
+
in vec4 v_color;
|
|
86
|
+
in float v_colored;
|
|
87
|
+
out vec4 outColor;
|
|
88
|
+
void main() {
|
|
89
|
+
vec4 texel = texture(u_texture, v_uv);
|
|
90
|
+
if (v_colored > 0.5) {
|
|
91
|
+
outColor = texel * v_color.a;
|
|
92
|
+
} else {
|
|
93
|
+
outColor = vec4(v_color.rgb, 1.0) * texel.a * v_color.a;
|
|
94
|
+
}
|
|
95
|
+
}`;
|
|
96
|
+
|
|
97
|
+
/** A glyph's ink in the atlas; `ox`, `oy` place it within the glyph's padded slot. */
|
|
98
|
+
interface AtlasEntry {
|
|
99
|
+
x: number;
|
|
100
|
+
y: number;
|
|
101
|
+
w: number;
|
|
102
|
+
h: number;
|
|
103
|
+
ox: number;
|
|
104
|
+
oy: number;
|
|
105
|
+
colored: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Packs RGBA bytes into one little-endian word, as the GPU reads UNSIGNED_BYTE × 4. */
|
|
109
|
+
function pack(r: number, g: number, b: number, a: number): number {
|
|
110
|
+
return (r | (g << 8) | (b << 16) | (a << 24)) >>> 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Glyph atlas: a texture packed in shelves of one slot height. Each glyph is
|
|
115
|
+
* rasterized in a scratch canvas the size of one slot and uploaded; the
|
|
116
|
+
* texture grows by copying itself on the GPU, so entries stay valid and no
|
|
117
|
+
* CPU copy of the atlas is kept.
|
|
118
|
+
*/
|
|
119
|
+
class GlyphAtlas {
|
|
120
|
+
size = ATLAS_START;
|
|
121
|
+
private readonly scratch: HTMLCanvasElement;
|
|
122
|
+
private ctx: CanvasRenderingContext2D;
|
|
123
|
+
private entries = new Map<number | string, AtlasEntry | null>();
|
|
124
|
+
private shelfX = 0;
|
|
125
|
+
private shelfY = 0;
|
|
126
|
+
texture: WebGLTexture;
|
|
127
|
+
/** Set when the atlas had to be cleared: every entry handed out before is gone. */
|
|
128
|
+
cleared = false;
|
|
129
|
+
|
|
130
|
+
constructor(
|
|
131
|
+
private readonly gl: WebGL2RenderingContext,
|
|
132
|
+
private slotHeight: number
|
|
133
|
+
) {
|
|
134
|
+
this.scratch = document.createElement('canvas');
|
|
135
|
+
this.ctx = this.scratchContext(1, slotHeight);
|
|
136
|
+
this.texture = this.newTexture(this.size);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private scratchContext(w: number, h: number): CanvasRenderingContext2D {
|
|
140
|
+
if (this.scratch.width < w) this.scratch.width = w;
|
|
141
|
+
if (this.scratch.height !== h) this.scratch.height = h;
|
|
142
|
+
const ctx = this.scratch.getContext('2d', { willReadFrequently: true });
|
|
143
|
+
if (!ctx) throw new Error('Failed to get the glyph scratch context');
|
|
144
|
+
ctx.textBaseline = 'alphabetic';
|
|
145
|
+
ctx.textAlign = 'left';
|
|
146
|
+
return ctx;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private newTexture(size: number): WebGLTexture {
|
|
150
|
+
const gl = this.gl;
|
|
151
|
+
const texture = gl.createTexture()!;
|
|
152
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
153
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
154
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
155
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
156
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
157
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
158
|
+
return texture;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Drops every glyph, for a new font or cell size. */
|
|
162
|
+
reset(slotHeight: number): void {
|
|
163
|
+
this.slotHeight = slotHeight;
|
|
164
|
+
this.entries.clear();
|
|
165
|
+
this.shelfX = 0;
|
|
166
|
+
this.shelfY = 0;
|
|
167
|
+
this.gl.deleteTexture(this.texture);
|
|
168
|
+
this.size = ATLAS_START;
|
|
169
|
+
this.texture = this.newTexture(this.size);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
lookup(key: number | string): AtlasEntry | null | undefined {
|
|
173
|
+
return this.entries.get(key);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Rasterizes a glyph with `draw` (at 0, 0 of a w px wide slot) and uploads it. */
|
|
177
|
+
add(
|
|
178
|
+
key: number | string,
|
|
179
|
+
w: number,
|
|
180
|
+
draw: (ctx: CanvasRenderingContext2D, x: number, y: number) => void
|
|
181
|
+
): AtlasEntry | null {
|
|
182
|
+
const h = this.slotHeight;
|
|
183
|
+
if (this.scratch.width < w || this.scratch.height !== h) this.ctx = this.scratchContext(w, h);
|
|
184
|
+
const ctx = this.ctx;
|
|
185
|
+
ctx.save();
|
|
186
|
+
ctx.beginPath();
|
|
187
|
+
ctx.rect(0, 0, w, h);
|
|
188
|
+
ctx.clip();
|
|
189
|
+
ctx.clearRect(0, 0, w, h);
|
|
190
|
+
draw(ctx, 0, 0);
|
|
191
|
+
ctx.restore();
|
|
192
|
+
const pixels = ctx.getImageData(0, 0, w, h);
|
|
193
|
+
const ink = glyphInk(pixels.data, w, h);
|
|
194
|
+
if (!ink) {
|
|
195
|
+
this.entries.set(key, null);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
if (this.shelfX + w > this.size) {
|
|
199
|
+
this.shelfX = 0;
|
|
200
|
+
this.shelfY += h;
|
|
201
|
+
}
|
|
202
|
+
if (this.shelfY + h > this.size && !this.grow()) {
|
|
203
|
+
this.reset(h);
|
|
204
|
+
this.cleared = true;
|
|
205
|
+
}
|
|
206
|
+
const x = this.shelfX;
|
|
207
|
+
const y = this.shelfY;
|
|
208
|
+
this.shelfX += w;
|
|
209
|
+
const gl = this.gl;
|
|
210
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
211
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
212
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
213
|
+
const entry = {
|
|
214
|
+
x: x + ink.left,
|
|
215
|
+
y: y + ink.top,
|
|
216
|
+
w: ink.width,
|
|
217
|
+
h: ink.height,
|
|
218
|
+
ox: ink.left,
|
|
219
|
+
oy: ink.top,
|
|
220
|
+
colored: ink.colored,
|
|
221
|
+
};
|
|
222
|
+
this.entries.set(key, entry);
|
|
223
|
+
return entry;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Doubles the texture, copying the old one into its top-left corner on the GPU. */
|
|
227
|
+
private grow(): boolean {
|
|
228
|
+
const gl = this.gl;
|
|
229
|
+
const limit = Math.min(ATLAS_MAX, gl.getParameter(gl.MAX_TEXTURE_SIZE) as number);
|
|
230
|
+
if (this.size * 2 > limit) return false;
|
|
231
|
+
const old = this.texture;
|
|
232
|
+
const oldSize = this.size;
|
|
233
|
+
this.size *= 2;
|
|
234
|
+
this.texture = this.newTexture(this.size);
|
|
235
|
+
const framebuffer = gl.createFramebuffer();
|
|
236
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer);
|
|
237
|
+
gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, old, 0);
|
|
238
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
239
|
+
gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, oldSize, oldSize);
|
|
240
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
|
241
|
+
gl.deleteFramebuffer(framebuffer);
|
|
242
|
+
gl.deleteTexture(old);
|
|
243
|
+
this.shelfX = 0;
|
|
244
|
+
this.shelfY = oldSize;
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
dispose(): void {
|
|
249
|
+
this.gl.deleteTexture(this.texture);
|
|
250
|
+
this.entries.clear();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export class WebglRenderer extends TerminalRenderer {
|
|
255
|
+
protected readonly cursorInRows = false;
|
|
256
|
+
private readonly glCanvas: HTMLCanvasElement;
|
|
257
|
+
private readonly gl: WebGL2RenderingContext;
|
|
258
|
+
private readonly rectProgram: WebGLProgram;
|
|
259
|
+
private readonly glyphProgram: WebGLProgram;
|
|
260
|
+
private readonly quad: WebGLBuffer;
|
|
261
|
+
private readonly rectBuffer: WebGLBuffer;
|
|
262
|
+
private readonly glyphBuffer: WebGLBuffer;
|
|
263
|
+
private readonly overlayRectBuffer: WebGLBuffer;
|
|
264
|
+
private readonly overlayGlyphBuffer: WebGLBuffer;
|
|
265
|
+
private readonly decoBuffer: WebGLBuffer;
|
|
266
|
+
private bgVao!: WebGLVertexArrayObject;
|
|
267
|
+
private decoVao!: WebGLVertexArrayObject;
|
|
268
|
+
private glyphVao!: WebGLVertexArrayObject;
|
|
269
|
+
private overlayRectVao!: WebGLVertexArrayObject;
|
|
270
|
+
private overlayGlyphVao!: WebGLVertexArrayObject;
|
|
271
|
+
private scrollbarVao!: WebGLVertexArrayObject;
|
|
272
|
+
private readonly rectResolution: WebGLUniformLocation | null;
|
|
273
|
+
private readonly glyphResolution: WebGLUniformLocation | null;
|
|
274
|
+
private readonly glyphAtlasSize: WebGLUniformLocation | null;
|
|
275
|
+
private readonly glyphTexture: WebGLUniformLocation | null;
|
|
276
|
+
private readonly atlas: GlyphAtlas;
|
|
277
|
+
|
|
278
|
+
private cols = 0;
|
|
279
|
+
private rows = 0;
|
|
280
|
+
private rectWords = new Float32Array(0);
|
|
281
|
+
private rectColors = new Uint32Array(0);
|
|
282
|
+
private glyphWords = new Float32Array(0);
|
|
283
|
+
private glyphColors = new Uint32Array(0);
|
|
284
|
+
private packedBg = instanceArrays(0, RECT_WORDS);
|
|
285
|
+
private packedDeco = instanceArrays(0, RECT_WORDS);
|
|
286
|
+
private packedGlyphs = instanceArrays(0, GLYPH_WORDS);
|
|
287
|
+
private bgCount = 0;
|
|
288
|
+
private decoCount = 0;
|
|
289
|
+
private glyphCount = 0;
|
|
290
|
+
private readonly overlayRects = new Float32Array(OVERLAY_RECTS * RECT_WORDS);
|
|
291
|
+
private readonly overlayRectColors = new Uint32Array(this.overlayRects.buffer);
|
|
292
|
+
private readonly overlayGlyph = new Float32Array(GLYPH_WORDS);
|
|
293
|
+
private readonly overlayGlyphColors = new Uint32Array(this.overlayGlyph.buffer);
|
|
294
|
+
private dirtyFrom = Number.POSITIVE_INFINITY;
|
|
295
|
+
private dirtyTo = -1;
|
|
296
|
+
private needsPresent = true;
|
|
297
|
+
private lastOverlay = '';
|
|
298
|
+
private lost = false;
|
|
299
|
+
|
|
300
|
+
private cellW = 0;
|
|
301
|
+
private cellH = 0;
|
|
302
|
+
private padX = 0;
|
|
303
|
+
private padY = 0;
|
|
304
|
+
private readonly parsedColors = new Map<string, number>();
|
|
305
|
+
private readonly colorProbe: CanvasRenderingContext2D;
|
|
306
|
+
private themeWords!: {
|
|
307
|
+
background: number;
|
|
308
|
+
foreground: number;
|
|
309
|
+
cursor: number;
|
|
310
|
+
cursorAccent: number;
|
|
311
|
+
selectionBackground: number;
|
|
312
|
+
selectionForeground: number;
|
|
313
|
+
link: number;
|
|
314
|
+
match: number;
|
|
315
|
+
activeMatch: number;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/** Called once if the WebGL context is lost; the terminal then switches renderer. */
|
|
319
|
+
public onContextLost?: () => void;
|
|
320
|
+
|
|
321
|
+
constructor(canvas: HTMLCanvasElement, options: RendererOptions = {}) {
|
|
322
|
+
super(canvas, options);
|
|
323
|
+
const glCanvas = document.createElement('canvas');
|
|
324
|
+
glCanvas.style.position = 'absolute';
|
|
325
|
+
glCanvas.style.pointerEvents = 'none';
|
|
326
|
+
glCanvas.style.display = 'block';
|
|
327
|
+
glCanvas.setAttribute('aria-hidden', 'true');
|
|
328
|
+
const gl = glCanvas.getContext('webgl2', {
|
|
329
|
+
alpha: true,
|
|
330
|
+
antialias: false,
|
|
331
|
+
depth: false,
|
|
332
|
+
stencil: false,
|
|
333
|
+
premultipliedAlpha: true,
|
|
334
|
+
preserveDrawingBuffer: false,
|
|
335
|
+
});
|
|
336
|
+
if (!gl) throw new Error('WebGL 2 is not available');
|
|
337
|
+
this.glCanvas = glCanvas;
|
|
338
|
+
this.gl = gl;
|
|
339
|
+
glCanvas.addEventListener('webglcontextlost', this.handleContextLost);
|
|
340
|
+
|
|
341
|
+
const probe = document.createElement('canvas');
|
|
342
|
+
probe.width = 1;
|
|
343
|
+
probe.height = 1;
|
|
344
|
+
this.colorProbe = probe.getContext('2d', { willReadFrequently: true })!;
|
|
345
|
+
|
|
346
|
+
this.rectProgram = linkProgram(gl, VERTEX_RECT, FRAGMENT_RECT);
|
|
347
|
+
this.glyphProgram = linkProgram(gl, VERTEX_GLYPH, FRAGMENT_GLYPH);
|
|
348
|
+
this.quad = gl.createBuffer()!;
|
|
349
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.quad);
|
|
350
|
+
gl.bufferData(
|
|
351
|
+
gl.ARRAY_BUFFER,
|
|
352
|
+
new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]),
|
|
353
|
+
gl.STATIC_DRAW
|
|
354
|
+
);
|
|
355
|
+
this.rectBuffer = gl.createBuffer()!;
|
|
356
|
+
this.glyphBuffer = gl.createBuffer()!;
|
|
357
|
+
this.decoBuffer = gl.createBuffer()!;
|
|
358
|
+
this.overlayRectBuffer = gl.createBuffer()!;
|
|
359
|
+
this.overlayGlyphBuffer = gl.createBuffer()!;
|
|
360
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayRectBuffer);
|
|
361
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.overlayRects.byteLength, gl.DYNAMIC_DRAW);
|
|
362
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayGlyphBuffer);
|
|
363
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.overlayGlyph.byteLength, gl.DYNAMIC_DRAW);
|
|
364
|
+
this.overlayRectVao = this.rectVao(this.overlayRectBuffer, 0);
|
|
365
|
+
this.overlayGlyphVao = this.glyphVaoFor(this.overlayGlyphBuffer);
|
|
366
|
+
this.scrollbarVao = this.rectVao(this.overlayRectBuffer, CURSOR_RECTS * RECT_WORDS * 4);
|
|
367
|
+
this.rectResolution = gl.getUniformLocation(this.rectProgram, 'u_resolution');
|
|
368
|
+
this.glyphResolution = gl.getUniformLocation(this.glyphProgram, 'u_resolution');
|
|
369
|
+
this.glyphAtlasSize = gl.getUniformLocation(this.glyphProgram, 'u_atlas');
|
|
370
|
+
this.glyphTexture = gl.getUniformLocation(this.glyphProgram, 'u_texture');
|
|
371
|
+
|
|
372
|
+
this.measureCellPixels();
|
|
373
|
+
this.atlas = new GlyphAtlas(gl, this.slotHeight());
|
|
374
|
+
this.parseTheme();
|
|
375
|
+
gl.enable(gl.BLEND);
|
|
376
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private readonly handleContextLost = (event: Event): void => {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
if (this.lost) return;
|
|
382
|
+
this.lost = true;
|
|
383
|
+
this.onContextLost?.();
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/** Whether the context was lost; such a renderer never draws again. */
|
|
387
|
+
get contextLost(): boolean {
|
|
388
|
+
return this.lost;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ==========================================================================
|
|
392
|
+
// Geometry
|
|
393
|
+
// ==========================================================================
|
|
394
|
+
|
|
395
|
+
private measureCellPixels(): void {
|
|
396
|
+
const dpr = this.devicePixelRatio;
|
|
397
|
+
this.cellW = this.metrics.width * dpr;
|
|
398
|
+
this.cellH = this.metrics.height * dpr;
|
|
399
|
+
this.padX = Math.ceil(this.cellW / 2);
|
|
400
|
+
this.padY = Math.ceil(this.cellH / 4);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private slotHeight(): number {
|
|
404
|
+
return Math.ceil(this.cellH) + 2 * this.padY;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
protected surfaceMismatch(cols: number, rows: number): boolean {
|
|
408
|
+
return (
|
|
409
|
+
cols !== this.cols ||
|
|
410
|
+
rows !== this.rows ||
|
|
411
|
+
this.glCanvas.width !== Math.round(cols * this.cellW) ||
|
|
412
|
+
this.glCanvas.height !== Math.round(rows * this.cellH)
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
public resize(cols: number, rows: number): void {
|
|
417
|
+
const cssWidth = cols * this.metrics.width;
|
|
418
|
+
const cssHeight = rows * this.metrics.height;
|
|
419
|
+
this.canvas.style.width = `${cssWidth}px`;
|
|
420
|
+
this.canvas.style.height = `${cssHeight}px`;
|
|
421
|
+
this.canvas.width = cssWidth;
|
|
422
|
+
this.canvas.height = cssHeight;
|
|
423
|
+
if (!this.glCanvas.isConnected) this.canvas.after(this.glCanvas);
|
|
424
|
+
this.glCanvas.style.left = `${this.canvas.offsetLeft}px`;
|
|
425
|
+
this.glCanvas.style.top = `${this.canvas.offsetTop}px`;
|
|
426
|
+
this.glCanvas.style.width = `${cssWidth}px`;
|
|
427
|
+
this.glCanvas.style.height = `${cssHeight}px`;
|
|
428
|
+
this.glCanvas.width = Math.round(cols * this.cellW);
|
|
429
|
+
this.glCanvas.height = Math.round(rows * this.cellH);
|
|
430
|
+
|
|
431
|
+
if (cols !== this.cols || rows !== this.rows) {
|
|
432
|
+
this.cols = cols;
|
|
433
|
+
this.rows = rows;
|
|
434
|
+
const cells = cols * rows;
|
|
435
|
+
const rects = new ArrayBuffer(cells * 3 * RECT_WORDS * 4);
|
|
436
|
+
this.rectWords = new Float32Array(rects);
|
|
437
|
+
this.rectColors = new Uint32Array(rects);
|
|
438
|
+
const glyphs = new ArrayBuffer(cells * GLYPH_WORDS * 4);
|
|
439
|
+
this.glyphWords = new Float32Array(glyphs);
|
|
440
|
+
this.glyphColors = new Uint32Array(glyphs);
|
|
441
|
+
this.packedBg = instanceArrays(cells, RECT_WORDS);
|
|
442
|
+
this.packedDeco = instanceArrays(cells * 2, RECT_WORDS);
|
|
443
|
+
this.packedGlyphs = instanceArrays(cells, GLYPH_WORDS);
|
|
444
|
+
const gl = this.gl;
|
|
445
|
+
const allocate = (buffer: WebGLBuffer, packed: InstanceArrays) => {
|
|
446
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
447
|
+
gl.bufferData(gl.ARRAY_BUFFER, packed.bits.byteLength, gl.DYNAMIC_DRAW);
|
|
448
|
+
};
|
|
449
|
+
allocate(this.rectBuffer, this.packedBg);
|
|
450
|
+
allocate(this.decoBuffer, this.packedDeco);
|
|
451
|
+
allocate(this.glyphBuffer, this.packedGlyphs);
|
|
452
|
+
for (const vao of [this.bgVao, this.decoVao, this.glyphVao]) {
|
|
453
|
+
if (vao) gl.deleteVertexArray(vao);
|
|
454
|
+
}
|
|
455
|
+
this.bgVao = this.rectVao(this.rectBuffer, 0);
|
|
456
|
+
this.decoVao = this.rectVao(this.decoBuffer, 0);
|
|
457
|
+
this.glyphVao = this.glyphVaoFor(this.glyphBuffer);
|
|
458
|
+
}
|
|
459
|
+
this.markAllRows();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
public clear(): void {
|
|
463
|
+
this.rectWords.fill(0);
|
|
464
|
+
this.glyphWords.fill(0);
|
|
465
|
+
this.markAllRows();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
protected fontChanged(): void {
|
|
469
|
+
this.measureCellPixels();
|
|
470
|
+
this.atlas.reset(this.slotHeight());
|
|
471
|
+
this.markAllRows();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
public setTheme(theme: ITheme): void {
|
|
475
|
+
super.setTheme(theme);
|
|
476
|
+
this.parseTheme();
|
|
477
|
+
this.markAllRows();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
protected searchColorsChanged(): void {
|
|
481
|
+
this.parseTheme();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private markAllRows(): void {
|
|
485
|
+
this.dirtyFrom = 0;
|
|
486
|
+
this.dirtyTo = this.rows - 1;
|
|
487
|
+
this.needsPresent = true;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ==========================================================================
|
|
491
|
+
// Colors
|
|
492
|
+
// ==========================================================================
|
|
493
|
+
|
|
494
|
+
private parseColor(css: string): number {
|
|
495
|
+
let word = this.parsedColors.get(css);
|
|
496
|
+
if (word !== undefined) return word;
|
|
497
|
+
const ctx = this.colorProbe;
|
|
498
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
499
|
+
ctx.fillStyle = '#000';
|
|
500
|
+
ctx.fillStyle = css;
|
|
501
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
502
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
503
|
+
word = pack(r, g, b, a);
|
|
504
|
+
this.parsedColors.set(css, word);
|
|
505
|
+
return word;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private parseTheme(): void {
|
|
509
|
+
const t = this.theme;
|
|
510
|
+
this.themeWords = {
|
|
511
|
+
background: this.parseColor(t.background),
|
|
512
|
+
foreground: this.parseColor(t.foreground),
|
|
513
|
+
cursor: this.parseColor(t.cursor),
|
|
514
|
+
cursorAccent: this.parseColor(t.cursorAccent),
|
|
515
|
+
selectionBackground: this.parseColor(t.selectionBackground),
|
|
516
|
+
selectionForeground: this.parseColor(t.selectionForeground),
|
|
517
|
+
link: this.parseColor(LINK_COLOR),
|
|
518
|
+
match: this.parseColor(this.highlightColors.match),
|
|
519
|
+
activeMatch: this.parseColor(this.highlightColors.active),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ==========================================================================
|
|
524
|
+
// Rows
|
|
525
|
+
// ==========================================================================
|
|
526
|
+
|
|
527
|
+
protected drawLine(line: GhosttyCell[], y: number, cols: number): void {
|
|
528
|
+
if (y < 0 || y >= this.rows) return;
|
|
529
|
+
const cells = this.cols * this.rows;
|
|
530
|
+
const rects = this.rectWords;
|
|
531
|
+
const rectColors = this.rectColors;
|
|
532
|
+
const glyphs = this.glyphWords;
|
|
533
|
+
const glyphColors = this.glyphColors;
|
|
534
|
+
const cellW = this.cellW;
|
|
535
|
+
const cellH = this.cellH;
|
|
536
|
+
const top = Math.round(y * cellH);
|
|
537
|
+
const bottom = Math.round((y + 1) * cellH);
|
|
538
|
+
const dpr = this.devicePixelRatio;
|
|
539
|
+
const underlineY = Math.round(y * cellH + (this.metrics.baseline + 2) * dpr);
|
|
540
|
+
const strikeY = Math.round(y * cellH + cellH / 2);
|
|
541
|
+
const thickness = Math.max(1, Math.round(dpr));
|
|
542
|
+
const theme = this.themeWords;
|
|
543
|
+
const width = Math.min(cols, this.cols);
|
|
544
|
+
|
|
545
|
+
for (let x = 0; x < this.cols; x++) {
|
|
546
|
+
const index = y * this.cols + x;
|
|
547
|
+
const bg = index * RECT_WORDS;
|
|
548
|
+
const deco1 = (cells + index) * RECT_WORDS;
|
|
549
|
+
const deco2 = (cells * 2 + index) * RECT_WORDS;
|
|
550
|
+
const glyph = index * GLYPH_WORDS;
|
|
551
|
+
rects[bg + 2] = 0;
|
|
552
|
+
rects[deco1 + 2] = 0;
|
|
553
|
+
rects[deco2 + 2] = 0;
|
|
554
|
+
glyphs[glyph + 4] = 0;
|
|
555
|
+
const cell = x < width ? line[x] : undefined;
|
|
556
|
+
if (!cell || cell.width === 0) continue;
|
|
557
|
+
|
|
558
|
+
const left = Math.round(x * cellW);
|
|
559
|
+
const right = Math.round((x + cell.width) * cellW);
|
|
560
|
+
const selected = this.isInSelection(x, y);
|
|
561
|
+
const inverse = cell.flags & CellFlags.INVERSE;
|
|
562
|
+
|
|
563
|
+
let bgWord = 0;
|
|
564
|
+
const highlight = selected ? 0 : this.highlightAt(x, y);
|
|
565
|
+
if (selected) {
|
|
566
|
+
bgWord = theme.selectionBackground;
|
|
567
|
+
} else if (highlight) {
|
|
568
|
+
bgWord = highlight === 2 ? theme.activeMatch : theme.match;
|
|
569
|
+
} else {
|
|
570
|
+
const r = inverse ? cell.fg_r : cell.bg_r;
|
|
571
|
+
const g = inverse ? cell.fg_g : cell.bg_g;
|
|
572
|
+
const b = inverse ? cell.fg_b : cell.bg_b;
|
|
573
|
+
if (r !== 0 || g !== 0 || b !== 0) bgWord = pack(r, g, b, 255);
|
|
574
|
+
}
|
|
575
|
+
if (bgWord !== 0) {
|
|
576
|
+
rects[bg] = left;
|
|
577
|
+
rects[bg + 1] = top;
|
|
578
|
+
rects[bg + 2] = right - left;
|
|
579
|
+
rects[bg + 3] = bottom - top;
|
|
580
|
+
rectColors[bg + 4] = bgWord;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let fgWord: number;
|
|
584
|
+
if (selected) {
|
|
585
|
+
fgWord = theme.selectionForeground;
|
|
586
|
+
} else {
|
|
587
|
+
const fg = inverse
|
|
588
|
+
? (cell.bg_r << 16) | (cell.bg_g << 8) | cell.bg_b
|
|
589
|
+
: (cell.fg_r << 16) | (cell.fg_g << 8) | cell.fg_b;
|
|
590
|
+
const back = inverse
|
|
591
|
+
? (cell.fg_r << 16) | (cell.fg_g << 8) | cell.fg_b
|
|
592
|
+
: (cell.bg_r << 16) | (cell.bg_g << 8) | cell.bg_b;
|
|
593
|
+
const shown = this.contrastedForeground(fg, back === 0 ? null : back);
|
|
594
|
+
fgWord = pack((shown >> 16) & 0xff, (shown >> 8) & 0xff, shown & 0xff, 255);
|
|
595
|
+
}
|
|
596
|
+
if (cell.flags & CellFlags.FAINT) fgWord = ((fgWord & 0x00ffffff) | (128 << 24)) >>> 0;
|
|
597
|
+
if (cell.flags & CellFlags.INVISIBLE) continue;
|
|
598
|
+
|
|
599
|
+
if (!isBlank(cell)) {
|
|
600
|
+
const entry = this.glyphFor(cell, x, y);
|
|
601
|
+
if (entry) {
|
|
602
|
+
glyphs[glyph] = left - this.padX + entry.ox;
|
|
603
|
+
glyphs[glyph + 1] = top - this.padY + entry.oy;
|
|
604
|
+
glyphs[glyph + 2] = entry.x;
|
|
605
|
+
glyphs[glyph + 3] = entry.y;
|
|
606
|
+
glyphs[glyph + 4] = entry.w;
|
|
607
|
+
glyphs[glyph + 5] = entry.h;
|
|
608
|
+
glyphColors[glyph + 6] = fgWord;
|
|
609
|
+
glyphs[glyph + 7] = entry.colored ? 1 : 0;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const link = (cell.hyperlink_id > 0 || this.hoveredLinkRange) && this.linkHovered(cell, x, y);
|
|
614
|
+
if (link || cell.flags & CellFlags.UNDERLINE) {
|
|
615
|
+
rects[deco1] = left;
|
|
616
|
+
rects[deco1 + 1] = underlineY;
|
|
617
|
+
rects[deco1 + 2] = right - left;
|
|
618
|
+
rects[deco1 + 3] = thickness;
|
|
619
|
+
rectColors[deco1 + 4] = link ? theme.link : fgWord;
|
|
620
|
+
}
|
|
621
|
+
if (cell.flags & CellFlags.STRIKETHROUGH) {
|
|
622
|
+
rects[deco2] = left;
|
|
623
|
+
rects[deco2 + 1] = strikeY;
|
|
624
|
+
rects[deco2 + 2] = right - left;
|
|
625
|
+
rects[deco2 + 3] = thickness;
|
|
626
|
+
rectColors[deco2 + 4] = fgWord;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (y < this.dirtyFrom) this.dirtyFrom = y;
|
|
630
|
+
if (y > this.dirtyTo) this.dirtyTo = y;
|
|
631
|
+
this.needsPresent = true;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private glyphFor(cell: GhosttyCell, x: number, y: number): ReturnType<GlyphAtlas['lookup']> {
|
|
635
|
+
const style = cell.flags & (CellFlags.BOLD | CellFlags.ITALIC);
|
|
636
|
+
const variant = (cell.flags & CellFlags.BOLD ? 1 : 0) | (cell.flags & CellFlags.ITALIC ? 2 : 0);
|
|
637
|
+
const simple = cell.grapheme_len === 0;
|
|
638
|
+
const key: number | string = simple
|
|
639
|
+
? cell.codepoint * 16 + variant * 4 + cell.width
|
|
640
|
+
: `${this.cellText(cell, x, y)}\u0000${variant}\u0000${cell.width}`;
|
|
641
|
+
const cached = this.atlas.lookup(key);
|
|
642
|
+
if (cached !== undefined) return cached;
|
|
643
|
+
const slotW = Math.ceil(this.cellW * cell.width) + 2 * this.padX;
|
|
644
|
+
const glyphW = this.cellW * cell.width;
|
|
645
|
+
const font = this.atlasFont(style);
|
|
646
|
+
const text = simple ? String.fromCodePoint(cell.codepoint) : this.cellText(cell, x, y);
|
|
647
|
+
const box = simple && isBoxGlyph(cell.codepoint);
|
|
648
|
+
const baseline = this.metrics.baseline * this.devicePixelRatio;
|
|
649
|
+
return this.atlas.add(key, slotW, (ctx, sx, sy) => {
|
|
650
|
+
ctx.fillStyle = '#ffffff';
|
|
651
|
+
if (box) {
|
|
652
|
+
drawBoxGlyph(ctx, cell.codepoint, sx + this.padX, sy + this.padY, glyphW, this.cellH);
|
|
653
|
+
} else {
|
|
654
|
+
ctx.font = font;
|
|
655
|
+
ctx.fillText(text, sx + this.padX, sy + this.padY + baseline);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private atlasFont(style: number): string {
|
|
661
|
+
return this.fontString(style, this.fontSize * this.devicePixelRatio);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ==========================================================================
|
|
665
|
+
// Frame
|
|
666
|
+
// ==========================================================================
|
|
667
|
+
|
|
668
|
+
protected finishFrame(overlay: FrameOverlay): void {
|
|
669
|
+
if (this.lost) return;
|
|
670
|
+
if (this.atlas.cleared) {
|
|
671
|
+
this.atlas.cleared = false;
|
|
672
|
+
this.markAllRows();
|
|
673
|
+
this.requestFullRedraw();
|
|
674
|
+
this.onNeedsFrame?.();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const overlayKey = this.buildOverlay(overlay);
|
|
678
|
+
if (!this.needsPresent && overlayKey === this.lastOverlay) return;
|
|
679
|
+
this.lastOverlay = overlayKey;
|
|
680
|
+
this.needsPresent = false;
|
|
681
|
+
this.upload();
|
|
682
|
+
this.present(overlayKey);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** Fills the overlay buffers and returns a key that changes whenever they do. */
|
|
686
|
+
private buildOverlay(overlay: FrameOverlay): string {
|
|
687
|
+
const rects = this.overlayRects;
|
|
688
|
+
const colors = this.overlayRectColors;
|
|
689
|
+
rects.fill(0);
|
|
690
|
+
this.overlayGlyph.fill(0);
|
|
691
|
+
const theme = this.themeWords;
|
|
692
|
+
let key = '';
|
|
693
|
+
if (overlay.showCursor && overlay.cursorX >= 0 && overlay.cursorY >= 0) {
|
|
694
|
+
const left = Math.round(overlay.cursorX * this.cellW);
|
|
695
|
+
const top = Math.round(overlay.cursorY * this.cellH);
|
|
696
|
+
const width = Math.round((overlay.cursorX + 1) * this.cellW) - left;
|
|
697
|
+
const height = Math.round((overlay.cursorY + 1) * this.cellH) - top;
|
|
698
|
+
rects[0] = left;
|
|
699
|
+
rects[1] = top;
|
|
700
|
+
rects[2] = width;
|
|
701
|
+
rects[3] = height;
|
|
702
|
+
if (!overlay.focused) {
|
|
703
|
+
const t = Math.max(1, Math.round(this.devicePixelRatio));
|
|
704
|
+
const edges = [
|
|
705
|
+
[left, top, width, t],
|
|
706
|
+
[left, top + height - t, width, t],
|
|
707
|
+
[left, top, t, height],
|
|
708
|
+
[left + width - t, top, t, height],
|
|
709
|
+
];
|
|
710
|
+
edges.forEach((edge, i) => {
|
|
711
|
+
rects.set(edge, i * RECT_WORDS);
|
|
712
|
+
colors[i * RECT_WORDS + 4] = theme.cursor;
|
|
713
|
+
});
|
|
714
|
+
} else if (this.cursorStyle === 'underline') {
|
|
715
|
+
const h = Math.max(2, Math.floor(height * 0.15));
|
|
716
|
+
rects[1] = top + height - h;
|
|
717
|
+
rects[3] = h;
|
|
718
|
+
} else if (this.cursorStyle === 'bar') {
|
|
719
|
+
rects[2] = Math.max(2, Math.floor(width * 0.15));
|
|
720
|
+
}
|
|
721
|
+
colors[4] = theme.cursor;
|
|
722
|
+
key = `${left},${top},${this.cursorStyle},${overlay.focused}`;
|
|
723
|
+
if (overlay.focused && this.cursorStyle === 'block') {
|
|
724
|
+
const cell = this.currentBuffer?.getLine(overlay.cursorY)?.[overlay.cursorX];
|
|
725
|
+
if (cell && !isBlank(cell) && !(cell.flags & CellFlags.INVISIBLE)) {
|
|
726
|
+
const entry = this.glyphFor(cell, overlay.cursorX, overlay.cursorY);
|
|
727
|
+
if (entry) {
|
|
728
|
+
const g = this.overlayGlyph;
|
|
729
|
+
g[0] = left - this.padX + entry.ox;
|
|
730
|
+
g[1] = top - this.padY + entry.oy;
|
|
731
|
+
g[2] = entry.x;
|
|
732
|
+
g[3] = entry.y;
|
|
733
|
+
g[4] = entry.w;
|
|
734
|
+
g[5] = entry.h;
|
|
735
|
+
this.overlayGlyphColors[6] = theme.cursorAccent;
|
|
736
|
+
g[7] = entry.colored ? 1 : 0;
|
|
737
|
+
key += `,${entry.x},${entry.y}`;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const thumb = this.scrollbarThumb(overlay);
|
|
743
|
+
if (thumb) {
|
|
744
|
+
const dpr = this.devicePixelRatio;
|
|
745
|
+
const opacity = overlay.scrollbarOpacity;
|
|
746
|
+
const base = overlay.viewportY > 0 ? 0.5 : 0.3;
|
|
747
|
+
const track = CURSOR_RECTS * RECT_WORDS;
|
|
748
|
+
rects[track] = thumb.x * dpr;
|
|
749
|
+
rects[track + 1] = thumb.trackY * dpr;
|
|
750
|
+
rects[track + 2] = thumb.width * dpr;
|
|
751
|
+
rects[track + 3] = thumb.trackHeight * dpr;
|
|
752
|
+
colors[track + 4] = pack(128, 128, 128, Math.round(0.1 * opacity * 255));
|
|
753
|
+
const bar = (CURSOR_RECTS + 1) * RECT_WORDS;
|
|
754
|
+
rects[bar] = thumb.x * dpr;
|
|
755
|
+
rects[bar + 1] = thumb.thumbY * dpr;
|
|
756
|
+
rects[bar + 2] = thumb.width * dpr;
|
|
757
|
+
rects[bar + 3] = thumb.thumbHeight * dpr;
|
|
758
|
+
colors[bar + 4] = pack(128, 128, 128, Math.round(base * opacity * 255));
|
|
759
|
+
key += `|${thumb.thumbY},${thumb.thumbHeight},${opacity},${base}`;
|
|
760
|
+
}
|
|
761
|
+
return key;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private upload(): void {
|
|
765
|
+
if (this.dirtyTo < this.dirtyFrom) return;
|
|
766
|
+
const gl = this.gl;
|
|
767
|
+
const cells = this.cols * this.rows;
|
|
768
|
+
const slots = { words: this.rectWords, bits: this.rectColors };
|
|
769
|
+
this.bgCount = compactRects(slots, 0, cells, this.packedBg);
|
|
770
|
+
this.decoCount = compactRects(slots, cells, cells * 2, this.packedDeco);
|
|
771
|
+
this.glyphCount = compactGlyphs(
|
|
772
|
+
{ words: this.glyphWords, bits: this.glyphColors },
|
|
773
|
+
cells,
|
|
774
|
+
this.packedGlyphs
|
|
775
|
+
);
|
|
776
|
+
const send = (buffer: WebGLBuffer, packed: InstanceArrays, count: number, wordsPer: number) => {
|
|
777
|
+
if (count === 0) return;
|
|
778
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
779
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, packed.bits, 0, count * wordsPer);
|
|
780
|
+
};
|
|
781
|
+
send(this.rectBuffer, this.packedBg, this.bgCount, RECT_WORDS);
|
|
782
|
+
send(this.decoBuffer, this.packedDeco, this.decoCount, RECT_WORDS);
|
|
783
|
+
send(this.glyphBuffer, this.packedGlyphs, this.glyphCount, GLYPH_WORDS);
|
|
784
|
+
this.dirtyFrom = Number.POSITIVE_INFINITY;
|
|
785
|
+
this.dirtyTo = -1;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
private present(overlayKey: string): void {
|
|
789
|
+
const gl = this.gl;
|
|
790
|
+
const width = this.glCanvas.width;
|
|
791
|
+
const height = this.glCanvas.height;
|
|
792
|
+
gl.viewport(0, 0, width, height);
|
|
793
|
+
const bg = this.themeWords.background;
|
|
794
|
+
const a = (bg >>> 24) / 255;
|
|
795
|
+
gl.clearColor(
|
|
796
|
+
((bg & 0xff) / 255) * a,
|
|
797
|
+
(((bg >>> 8) & 0xff) / 255) * a,
|
|
798
|
+
(((bg >>> 16) & 0xff) / 255) * a,
|
|
799
|
+
a
|
|
800
|
+
);
|
|
801
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
802
|
+
|
|
803
|
+
gl.useProgram(this.rectProgram);
|
|
804
|
+
gl.uniform2f(this.rectResolution, width, height);
|
|
805
|
+
if (this.bgCount > 0) {
|
|
806
|
+
gl.bindVertexArray(this.bgVao);
|
|
807
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.bgCount);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
gl.useProgram(this.glyphProgram);
|
|
811
|
+
gl.uniform2f(this.glyphResolution, width, height);
|
|
812
|
+
gl.uniform2f(this.glyphAtlasSize, this.atlas.size, this.atlas.size);
|
|
813
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
814
|
+
gl.bindTexture(gl.TEXTURE_2D, this.atlas.texture);
|
|
815
|
+
gl.uniform1i(this.glyphTexture, 0);
|
|
816
|
+
if (this.glyphCount > 0) {
|
|
817
|
+
gl.bindVertexArray(this.glyphVao);
|
|
818
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.glyphCount);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
gl.useProgram(this.rectProgram);
|
|
822
|
+
if (this.decoCount > 0) {
|
|
823
|
+
gl.bindVertexArray(this.decoVao);
|
|
824
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.decoCount);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (overlayKey) {
|
|
828
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayRectBuffer);
|
|
829
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.overlayRects);
|
|
830
|
+
gl.bindVertexArray(this.overlayRectVao);
|
|
831
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, CURSOR_RECTS);
|
|
832
|
+
if (this.overlayGlyph[4] > 0) {
|
|
833
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayGlyphBuffer);
|
|
834
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.overlayGlyph);
|
|
835
|
+
gl.useProgram(this.glyphProgram);
|
|
836
|
+
gl.bindVertexArray(this.overlayGlyphVao);
|
|
837
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, 1);
|
|
838
|
+
gl.useProgram(this.rectProgram);
|
|
839
|
+
}
|
|
840
|
+
gl.bindVertexArray(this.scrollbarVao);
|
|
841
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, SCROLLBAR_RECTS);
|
|
842
|
+
}
|
|
843
|
+
gl.bindVertexArray(null);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ==========================================================================
|
|
847
|
+
// GL plumbing
|
|
848
|
+
// ==========================================================================
|
|
849
|
+
|
|
850
|
+
private rectVao(buffer: WebGLBuffer, byteOffset: number): WebGLVertexArrayObject {
|
|
851
|
+
const gl = this.gl;
|
|
852
|
+
const vao = gl.createVertexArray()!;
|
|
853
|
+
gl.bindVertexArray(vao);
|
|
854
|
+
this.bindQuad();
|
|
855
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
856
|
+
const stride = RECT_WORDS * 4;
|
|
857
|
+
gl.enableVertexAttribArray(1);
|
|
858
|
+
gl.vertexAttribPointer(1, 4, gl.FLOAT, false, stride, byteOffset);
|
|
859
|
+
gl.vertexAttribDivisor(1, 1);
|
|
860
|
+
gl.enableVertexAttribArray(2);
|
|
861
|
+
gl.vertexAttribPointer(2, 4, gl.UNSIGNED_BYTE, true, stride, byteOffset + 16);
|
|
862
|
+
gl.vertexAttribDivisor(2, 1);
|
|
863
|
+
gl.bindVertexArray(null);
|
|
864
|
+
return vao;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private glyphVaoFor(buffer: WebGLBuffer): WebGLVertexArrayObject {
|
|
868
|
+
const gl = this.gl;
|
|
869
|
+
const vao = gl.createVertexArray()!;
|
|
870
|
+
gl.bindVertexArray(vao);
|
|
871
|
+
this.bindQuad();
|
|
872
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
873
|
+
const stride = GLYPH_WORDS * 4;
|
|
874
|
+
gl.enableVertexAttribArray(1);
|
|
875
|
+
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, stride, 0);
|
|
876
|
+
gl.vertexAttribDivisor(1, 1);
|
|
877
|
+
gl.enableVertexAttribArray(2);
|
|
878
|
+
gl.vertexAttribPointer(2, 4, gl.FLOAT, false, stride, 8);
|
|
879
|
+
gl.vertexAttribDivisor(2, 1);
|
|
880
|
+
gl.enableVertexAttribArray(3);
|
|
881
|
+
gl.vertexAttribPointer(3, 4, gl.UNSIGNED_BYTE, true, stride, 24);
|
|
882
|
+
gl.vertexAttribDivisor(3, 1);
|
|
883
|
+
gl.enableVertexAttribArray(4);
|
|
884
|
+
gl.vertexAttribPointer(4, 1, gl.FLOAT, false, stride, 28);
|
|
885
|
+
gl.vertexAttribDivisor(4, 1);
|
|
886
|
+
gl.bindVertexArray(null);
|
|
887
|
+
return vao;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private bindQuad(): void {
|
|
891
|
+
const gl = this.gl;
|
|
892
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.quad);
|
|
893
|
+
gl.enableVertexAttribArray(0);
|
|
894
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
public dispose(): void {
|
|
898
|
+
super.dispose();
|
|
899
|
+
this.glCanvas.removeEventListener('webglcontextlost', this.handleContextLost);
|
|
900
|
+
this.glCanvas.remove();
|
|
901
|
+
if (!this.lost) {
|
|
902
|
+
const gl = this.gl;
|
|
903
|
+
this.atlas.dispose();
|
|
904
|
+
for (const buffer of [
|
|
905
|
+
this.quad,
|
|
906
|
+
this.rectBuffer,
|
|
907
|
+
this.decoBuffer,
|
|
908
|
+
this.glyphBuffer,
|
|
909
|
+
this.overlayRectBuffer,
|
|
910
|
+
this.overlayGlyphBuffer,
|
|
911
|
+
]) {
|
|
912
|
+
gl.deleteBuffer(buffer);
|
|
913
|
+
}
|
|
914
|
+
gl.deleteProgram(this.rectProgram);
|
|
915
|
+
gl.deleteProgram(this.glyphProgram);
|
|
916
|
+
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function linkProgram(gl: WebGL2RenderingContext, vertex: string, fragment: string): WebGLProgram {
|
|
922
|
+
const compile = (type: number, source: string) => {
|
|
923
|
+
const shader = gl.createShader(type)!;
|
|
924
|
+
gl.shaderSource(shader, source);
|
|
925
|
+
gl.compileShader(shader);
|
|
926
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
927
|
+
throw new Error(`Shader failed to compile: ${gl.getShaderInfoLog(shader)}`);
|
|
928
|
+
}
|
|
929
|
+
return shader;
|
|
930
|
+
};
|
|
931
|
+
const program = gl.createProgram()!;
|
|
932
|
+
const vs = compile(gl.VERTEX_SHADER, vertex);
|
|
933
|
+
const fs = compile(gl.FRAGMENT_SHADER, fragment);
|
|
934
|
+
gl.attachShader(program, vs);
|
|
935
|
+
gl.attachShader(program, fs);
|
|
936
|
+
gl.linkProgram(program);
|
|
937
|
+
gl.deleteShader(vs);
|
|
938
|
+
gl.deleteShader(fs);
|
|
939
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
940
|
+
throw new Error(`Shader program failed to link: ${gl.getProgramInfoLog(program)}`);
|
|
941
|
+
}
|
|
942
|
+
return program;
|
|
943
|
+
}
|