@aurigax-ai/ghostty-web 0.2.0 → 0.3.1
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 +2 -1
- package/lib/addons/search.ts +190 -0
- package/lib/buffer.ts +1 -3
- package/lib/ghostty.ts +236 -7
- package/lib/glyph-ink.ts +42 -0
- package/lib/index.ts +6 -0
- package/lib/input-handler.ts +45 -3
- package/lib/interfaces.ts +23 -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 +266 -33
- package/lib/selection-manager.ts +3 -16
- package/lib/terminal.ts +154 -73
- package/lib/types.ts +2 -1
- package/lib/webgl-instances.ts +69 -0
- package/lib/webgl-renderer.ts +176 -103
- package/package.json +1 -1
package/lib/webgl-renderer.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { drawBoxGlyph, isBoxGlyph } from './box-glyphs';
|
|
14
|
+
import { glyphInk } from './glyph-ink';
|
|
14
15
|
import type { ITheme } from './interfaces';
|
|
15
16
|
import {
|
|
16
17
|
type FrameOverlay,
|
|
@@ -21,13 +22,20 @@ import {
|
|
|
21
22
|
} from './renderer';
|
|
22
23
|
import type { GhosttyCell } from './types';
|
|
23
24
|
import { CellFlags } from './types';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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;
|
|
29
38
|
const ATLAS_MAX = 4096;
|
|
30
|
-
const COLORED_SPREAD = 24;
|
|
31
39
|
|
|
32
40
|
const VERTEX_RECT = `#version 300 es
|
|
33
41
|
layout(location = 0) in vec2 a_corner;
|
|
@@ -86,11 +94,14 @@ void main() {
|
|
|
86
94
|
}
|
|
87
95
|
}`;
|
|
88
96
|
|
|
97
|
+
/** A glyph's ink in the atlas; `ox`, `oy` place it within the glyph's padded slot. */
|
|
89
98
|
interface AtlasEntry {
|
|
90
99
|
x: number;
|
|
91
100
|
y: number;
|
|
92
101
|
w: number;
|
|
93
102
|
h: number;
|
|
103
|
+
ox: number;
|
|
104
|
+
oy: number;
|
|
94
105
|
colored: boolean;
|
|
95
106
|
}
|
|
96
107
|
|
|
@@ -99,10 +110,15 @@ function pack(r: number, g: number, b: number, a: number): number {
|
|
|
99
110
|
return (r | (g << 8) | (b << 16) | (a << 24)) >>> 0;
|
|
100
111
|
}
|
|
101
112
|
|
|
102
|
-
/**
|
|
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
|
+
*/
|
|
103
119
|
class GlyphAtlas {
|
|
104
120
|
size = ATLAS_START;
|
|
105
|
-
private
|
|
121
|
+
private readonly scratch: HTMLCanvasElement;
|
|
106
122
|
private ctx: CanvasRenderingContext2D;
|
|
107
123
|
private entries = new Map<number | string, AtlasEntry | null>();
|
|
108
124
|
private shelfX = 0;
|
|
@@ -115,31 +131,31 @@ class GlyphAtlas {
|
|
|
115
131
|
private readonly gl: WebGL2RenderingContext,
|
|
116
132
|
private slotHeight: number
|
|
117
133
|
) {
|
|
118
|
-
this.
|
|
119
|
-
this.ctx = this.
|
|
120
|
-
this.texture =
|
|
121
|
-
this.allocateTexture();
|
|
134
|
+
this.scratch = document.createElement('canvas');
|
|
135
|
+
this.ctx = this.scratchContext(1, slotHeight);
|
|
136
|
+
this.texture = this.newTexture(this.size);
|
|
122
137
|
}
|
|
123
138
|
|
|
124
|
-
private
|
|
125
|
-
this.
|
|
126
|
-
this.
|
|
127
|
-
const ctx = this.
|
|
128
|
-
if (!ctx) throw new Error('Failed to get the glyph
|
|
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');
|
|
129
144
|
ctx.textBaseline = 'alphabetic';
|
|
130
145
|
ctx.textAlign = 'left';
|
|
131
146
|
return ctx;
|
|
132
147
|
}
|
|
133
148
|
|
|
134
|
-
private
|
|
149
|
+
private newTexture(size: number): WebGLTexture {
|
|
135
150
|
const gl = this.gl;
|
|
136
|
-
gl.
|
|
137
|
-
gl.
|
|
151
|
+
const texture = gl.createTexture()!;
|
|
152
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
138
153
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
139
154
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
140
155
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
141
156
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
142
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE,
|
|
157
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
158
|
+
return texture;
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
/** Drops every glyph, for a new font or cell size. */
|
|
@@ -148,21 +164,37 @@ class GlyphAtlas {
|
|
|
148
164
|
this.entries.clear();
|
|
149
165
|
this.shelfX = 0;
|
|
150
166
|
this.shelfY = 0;
|
|
151
|
-
this.
|
|
152
|
-
this.
|
|
167
|
+
this.gl.deleteTexture(this.texture);
|
|
168
|
+
this.size = ATLAS_START;
|
|
169
|
+
this.texture = this.newTexture(this.size);
|
|
153
170
|
}
|
|
154
171
|
|
|
155
172
|
lookup(key: number | string): AtlasEntry | null | undefined {
|
|
156
173
|
return this.entries.get(key);
|
|
157
174
|
}
|
|
158
175
|
|
|
159
|
-
/** Rasterizes a glyph with `draw`
|
|
176
|
+
/** Rasterizes a glyph with `draw` (at 0, 0 of a w px wide slot) and uploads it. */
|
|
160
177
|
add(
|
|
161
178
|
key: number | string,
|
|
162
179
|
w: number,
|
|
163
180
|
draw: (ctx: CanvasRenderingContext2D, x: number, y: number) => void
|
|
164
181
|
): AtlasEntry | null {
|
|
165
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
|
+
}
|
|
166
198
|
if (this.shelfX + w > this.size) {
|
|
167
199
|
this.shelfX = 0;
|
|
168
200
|
this.shelfY += h;
|
|
@@ -174,56 +206,42 @@ class GlyphAtlas {
|
|
|
174
206
|
const x = this.shelfX;
|
|
175
207
|
const y = this.shelfY;
|
|
176
208
|
this.shelfX += w;
|
|
177
|
-
const ctx = this.ctx;
|
|
178
|
-
ctx.save();
|
|
179
|
-
ctx.beginPath();
|
|
180
|
-
ctx.rect(x, y, w, h);
|
|
181
|
-
ctx.clip();
|
|
182
|
-
ctx.clearRect(x, y, w, h);
|
|
183
|
-
draw(ctx, x, y);
|
|
184
|
-
ctx.restore();
|
|
185
|
-
const pixels = ctx.getImageData(x, y, w, h);
|
|
186
|
-
const data = pixels.data;
|
|
187
|
-
let inked = false;
|
|
188
|
-
let colored = false;
|
|
189
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
190
|
-
if (data[i + 3] === 0) continue;
|
|
191
|
-
inked = true;
|
|
192
|
-
const r = data[i];
|
|
193
|
-
const g = data[i + 1];
|
|
194
|
-
const b = data[i + 2];
|
|
195
|
-
if (Math.max(r, g, b) - Math.min(r, g, b) > COLORED_SPREAD) {
|
|
196
|
-
colored = true;
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (!inked) {
|
|
201
|
-
this.shelfX = x;
|
|
202
|
-
this.entries.set(key, null);
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
209
|
const gl = this.gl;
|
|
206
210
|
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
207
211
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
208
212
|
gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
209
|
-
const entry = {
|
|
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
|
+
};
|
|
210
222
|
this.entries.set(key, entry);
|
|
211
223
|
return entry;
|
|
212
224
|
}
|
|
213
225
|
|
|
226
|
+
/** Doubles the texture, copying the old one into its top-left corner on the GPU. */
|
|
214
227
|
private grow(): boolean {
|
|
215
|
-
const
|
|
228
|
+
const gl = this.gl;
|
|
229
|
+
const limit = Math.min(ATLAS_MAX, gl.getParameter(gl.MAX_TEXTURE_SIZE) as number);
|
|
216
230
|
if (this.size * 2 > limit) return false;
|
|
217
|
-
const old =
|
|
218
|
-
|
|
219
|
-
old.height = this.size;
|
|
220
|
-
old.getContext('2d')!.drawImage(this.mirror, 0, 0);
|
|
231
|
+
const old = this.texture;
|
|
232
|
+
const oldSize = this.size;
|
|
221
233
|
this.size *= 2;
|
|
222
|
-
this.
|
|
223
|
-
|
|
224
|
-
|
|
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);
|
|
225
243
|
this.shelfX = 0;
|
|
226
|
-
this.shelfY =
|
|
244
|
+
this.shelfY = oldSize;
|
|
227
245
|
return true;
|
|
228
246
|
}
|
|
229
247
|
|
|
@@ -244,6 +262,7 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
244
262
|
private readonly glyphBuffer: WebGLBuffer;
|
|
245
263
|
private readonly overlayRectBuffer: WebGLBuffer;
|
|
246
264
|
private readonly overlayGlyphBuffer: WebGLBuffer;
|
|
265
|
+
private readonly decoBuffer: WebGLBuffer;
|
|
247
266
|
private bgVao!: WebGLVertexArrayObject;
|
|
248
267
|
private decoVao!: WebGLVertexArrayObject;
|
|
249
268
|
private glyphVao!: WebGLVertexArrayObject;
|
|
@@ -262,6 +281,12 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
262
281
|
private rectColors = new Uint32Array(0);
|
|
263
282
|
private glyphWords = new Float32Array(0);
|
|
264
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;
|
|
265
290
|
private readonly overlayRects = new Float32Array(OVERLAY_RECTS * RECT_WORDS);
|
|
266
291
|
private readonly overlayRectColors = new Uint32Array(this.overlayRects.buffer);
|
|
267
292
|
private readonly overlayGlyph = new Float32Array(GLYPH_WORDS);
|
|
@@ -286,6 +311,8 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
286
311
|
selectionBackground: number;
|
|
287
312
|
selectionForeground: number;
|
|
288
313
|
link: number;
|
|
314
|
+
match: number;
|
|
315
|
+
activeMatch: number;
|
|
289
316
|
};
|
|
290
317
|
|
|
291
318
|
/** Called once if the WebGL context is lost; the terminal then switches renderer. */
|
|
@@ -327,6 +354,7 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
327
354
|
);
|
|
328
355
|
this.rectBuffer = gl.createBuffer()!;
|
|
329
356
|
this.glyphBuffer = gl.createBuffer()!;
|
|
357
|
+
this.decoBuffer = gl.createBuffer()!;
|
|
330
358
|
this.overlayRectBuffer = gl.createBuffer()!;
|
|
331
359
|
this.overlayGlyphBuffer = gl.createBuffer()!;
|
|
332
360
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayRectBuffer);
|
|
@@ -335,7 +363,7 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
335
363
|
gl.bufferData(gl.ARRAY_BUFFER, this.overlayGlyph.byteLength, gl.DYNAMIC_DRAW);
|
|
336
364
|
this.overlayRectVao = this.rectVao(this.overlayRectBuffer, 0);
|
|
337
365
|
this.overlayGlyphVao = this.glyphVaoFor(this.overlayGlyphBuffer);
|
|
338
|
-
this.scrollbarVao = this.rectVao(this.overlayRectBuffer, RECT_WORDS * 4);
|
|
366
|
+
this.scrollbarVao = this.rectVao(this.overlayRectBuffer, CURSOR_RECTS * RECT_WORDS * 4);
|
|
339
367
|
this.rectResolution = gl.getUniformLocation(this.rectProgram, 'u_resolution');
|
|
340
368
|
this.glyphResolution = gl.getUniformLocation(this.glyphProgram, 'u_resolution');
|
|
341
369
|
this.glyphAtlasSize = gl.getUniformLocation(this.glyphProgram, 'u_atlas');
|
|
@@ -410,16 +438,22 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
410
438
|
const glyphs = new ArrayBuffer(cells * GLYPH_WORDS * 4);
|
|
411
439
|
this.glyphWords = new Float32Array(glyphs);
|
|
412
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);
|
|
413
444
|
const gl = this.gl;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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);
|
|
418
452
|
for (const vao of [this.bgVao, this.decoVao, this.glyphVao]) {
|
|
419
453
|
if (vao) gl.deleteVertexArray(vao);
|
|
420
454
|
}
|
|
421
455
|
this.bgVao = this.rectVao(this.rectBuffer, 0);
|
|
422
|
-
this.decoVao = this.rectVao(this.
|
|
456
|
+
this.decoVao = this.rectVao(this.decoBuffer, 0);
|
|
423
457
|
this.glyphVao = this.glyphVaoFor(this.glyphBuffer);
|
|
424
458
|
}
|
|
425
459
|
this.markAllRows();
|
|
@@ -443,6 +477,10 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
443
477
|
this.markAllRows();
|
|
444
478
|
}
|
|
445
479
|
|
|
480
|
+
protected searchColorsChanged(): void {
|
|
481
|
+
this.parseTheme();
|
|
482
|
+
}
|
|
483
|
+
|
|
446
484
|
private markAllRows(): void {
|
|
447
485
|
this.dirtyFrom = 0;
|
|
448
486
|
this.dirtyTo = this.rows - 1;
|
|
@@ -477,6 +515,8 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
477
515
|
selectionBackground: this.parseColor(t.selectionBackground),
|
|
478
516
|
selectionForeground: this.parseColor(t.selectionForeground),
|
|
479
517
|
link: this.parseColor(LINK_COLOR),
|
|
518
|
+
match: this.parseColor(this.highlightColors.match),
|
|
519
|
+
activeMatch: this.parseColor(this.highlightColors.active),
|
|
480
520
|
};
|
|
481
521
|
}
|
|
482
522
|
|
|
@@ -521,8 +561,11 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
521
561
|
const inverse = cell.flags & CellFlags.INVERSE;
|
|
522
562
|
|
|
523
563
|
let bgWord = 0;
|
|
564
|
+
const highlight = selected ? 0 : this.highlightAt(x, y);
|
|
524
565
|
if (selected) {
|
|
525
566
|
bgWord = theme.selectionBackground;
|
|
567
|
+
} else if (highlight) {
|
|
568
|
+
bgWord = highlight === 2 ? theme.activeMatch : theme.match;
|
|
526
569
|
} else {
|
|
527
570
|
const r = inverse ? cell.fg_r : cell.bg_r;
|
|
528
571
|
const g = inverse ? cell.fg_g : cell.bg_g;
|
|
@@ -538,17 +581,26 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
538
581
|
}
|
|
539
582
|
|
|
540
583
|
let fgWord: number;
|
|
541
|
-
if (selected)
|
|
542
|
-
|
|
543
|
-
else
|
|
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
|
+
}
|
|
544
596
|
if (cell.flags & CellFlags.FAINT) fgWord = ((fgWord & 0x00ffffff) | (128 << 24)) >>> 0;
|
|
545
597
|
if (cell.flags & CellFlags.INVISIBLE) continue;
|
|
546
598
|
|
|
547
599
|
if (!isBlank(cell)) {
|
|
548
600
|
const entry = this.glyphFor(cell, x, y);
|
|
549
601
|
if (entry) {
|
|
550
|
-
glyphs[glyph] = left - this.padX;
|
|
551
|
-
glyphs[glyph + 1] = top - this.padY;
|
|
602
|
+
glyphs[glyph] = left - this.padX + entry.ox;
|
|
603
|
+
glyphs[glyph + 1] = top - this.padY + entry.oy;
|
|
552
604
|
glyphs[glyph + 2] = entry.x;
|
|
553
605
|
glyphs[glyph + 3] = entry.y;
|
|
554
606
|
glyphs[glyph + 4] = entry.w;
|
|
@@ -606,10 +658,7 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
606
658
|
}
|
|
607
659
|
|
|
608
660
|
private atlasFont(style: number): string {
|
|
609
|
-
|
|
610
|
-
if (style & CellFlags.ITALIC) prefix += 'italic ';
|
|
611
|
-
if (style & CellFlags.BOLD) prefix += 'bold ';
|
|
612
|
-
return `${prefix}${this.fontSize * this.devicePixelRatio}px ${this.fontFamily}`;
|
|
661
|
+
return this.fontString(style, this.fontSize * this.devicePixelRatio);
|
|
613
662
|
}
|
|
614
663
|
|
|
615
664
|
// ==========================================================================
|
|
@@ -650,7 +699,19 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
650
699
|
rects[1] = top;
|
|
651
700
|
rects[2] = width;
|
|
652
701
|
rects[3] = height;
|
|
653
|
-
if (
|
|
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') {
|
|
654
715
|
const h = Math.max(2, Math.floor(height * 0.15));
|
|
655
716
|
rects[1] = top + height - h;
|
|
656
717
|
rects[3] = h;
|
|
@@ -658,15 +719,15 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
658
719
|
rects[2] = Math.max(2, Math.floor(width * 0.15));
|
|
659
720
|
}
|
|
660
721
|
colors[4] = theme.cursor;
|
|
661
|
-
key = `${left},${top},${this.cursorStyle}`;
|
|
662
|
-
if (this.cursorStyle === 'block') {
|
|
722
|
+
key = `${left},${top},${this.cursorStyle},${overlay.focused}`;
|
|
723
|
+
if (overlay.focused && this.cursorStyle === 'block') {
|
|
663
724
|
const cell = this.currentBuffer?.getLine(overlay.cursorY)?.[overlay.cursorX];
|
|
664
725
|
if (cell && !isBlank(cell) && !(cell.flags & CellFlags.INVISIBLE)) {
|
|
665
726
|
const entry = this.glyphFor(cell, overlay.cursorX, overlay.cursorY);
|
|
666
727
|
if (entry) {
|
|
667
728
|
const g = this.overlayGlyph;
|
|
668
|
-
g[0] = left - this.padX;
|
|
669
|
-
g[1] = top - this.padY;
|
|
729
|
+
g[0] = left - this.padX + entry.ox;
|
|
730
|
+
g[1] = top - this.padY + entry.oy;
|
|
670
731
|
g[2] = entry.x;
|
|
671
732
|
g[3] = entry.y;
|
|
672
733
|
g[4] = entry.w;
|
|
@@ -683,13 +744,13 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
683
744
|
const dpr = this.devicePixelRatio;
|
|
684
745
|
const opacity = overlay.scrollbarOpacity;
|
|
685
746
|
const base = overlay.viewportY > 0 ? 0.5 : 0.3;
|
|
686
|
-
const track = RECT_WORDS;
|
|
747
|
+
const track = CURSOR_RECTS * RECT_WORDS;
|
|
687
748
|
rects[track] = thumb.x * dpr;
|
|
688
749
|
rects[track + 1] = thumb.trackY * dpr;
|
|
689
750
|
rects[track + 2] = thumb.width * dpr;
|
|
690
751
|
rects[track + 3] = thumb.trackHeight * dpr;
|
|
691
752
|
colors[track + 4] = pack(128, 128, 128, Math.round(0.1 * opacity * 255));
|
|
692
|
-
const bar =
|
|
753
|
+
const bar = (CURSOR_RECTS + 1) * RECT_WORDS;
|
|
693
754
|
rects[bar] = thumb.x * dpr;
|
|
694
755
|
rects[bar + 1] = thumb.thumbY * dpr;
|
|
695
756
|
rects[bar + 2] = thumb.width * dpr;
|
|
@@ -704,16 +765,22 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
704
765
|
if (this.dirtyTo < this.dirtyFrom) return;
|
|
705
766
|
const gl = this.gl;
|
|
706
767
|
const cells = this.cols * this.rows;
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
|
|
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);
|
|
717
784
|
this.dirtyFrom = Number.POSITIVE_INFINITY;
|
|
718
785
|
this.dirtyTo = -1;
|
|
719
786
|
}
|
|
@@ -722,7 +789,6 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
722
789
|
const gl = this.gl;
|
|
723
790
|
const width = this.glCanvas.width;
|
|
724
791
|
const height = this.glCanvas.height;
|
|
725
|
-
const cells = this.cols * this.rows;
|
|
726
792
|
gl.viewport(0, 0, width, height);
|
|
727
793
|
const bg = this.themeWords.background;
|
|
728
794
|
const a = (bg >>> 24) / 255;
|
|
@@ -736,8 +802,10 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
736
802
|
|
|
737
803
|
gl.useProgram(this.rectProgram);
|
|
738
804
|
gl.uniform2f(this.rectResolution, width, height);
|
|
739
|
-
|
|
740
|
-
|
|
805
|
+
if (this.bgCount > 0) {
|
|
806
|
+
gl.bindVertexArray(this.bgVao);
|
|
807
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.bgCount);
|
|
808
|
+
}
|
|
741
809
|
|
|
742
810
|
gl.useProgram(this.glyphProgram);
|
|
743
811
|
gl.uniform2f(this.glyphResolution, width, height);
|
|
@@ -745,18 +813,22 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
745
813
|
gl.activeTexture(gl.TEXTURE0);
|
|
746
814
|
gl.bindTexture(gl.TEXTURE_2D, this.atlas.texture);
|
|
747
815
|
gl.uniform1i(this.glyphTexture, 0);
|
|
748
|
-
|
|
749
|
-
|
|
816
|
+
if (this.glyphCount > 0) {
|
|
817
|
+
gl.bindVertexArray(this.glyphVao);
|
|
818
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.glyphCount);
|
|
819
|
+
}
|
|
750
820
|
|
|
751
821
|
gl.useProgram(this.rectProgram);
|
|
752
|
-
|
|
753
|
-
|
|
822
|
+
if (this.decoCount > 0) {
|
|
823
|
+
gl.bindVertexArray(this.decoVao);
|
|
824
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.decoCount);
|
|
825
|
+
}
|
|
754
826
|
|
|
755
827
|
if (overlayKey) {
|
|
756
828
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayRectBuffer);
|
|
757
829
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.overlayRects);
|
|
758
830
|
gl.bindVertexArray(this.overlayRectVao);
|
|
759
|
-
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6,
|
|
831
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, CURSOR_RECTS);
|
|
760
832
|
if (this.overlayGlyph[4] > 0) {
|
|
761
833
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayGlyphBuffer);
|
|
762
834
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.overlayGlyph);
|
|
@@ -766,7 +838,7 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
766
838
|
gl.useProgram(this.rectProgram);
|
|
767
839
|
}
|
|
768
840
|
gl.bindVertexArray(this.scrollbarVao);
|
|
769
|
-
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6,
|
|
841
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, SCROLLBAR_RECTS);
|
|
770
842
|
}
|
|
771
843
|
gl.bindVertexArray(null);
|
|
772
844
|
}
|
|
@@ -832,6 +904,7 @@ export class WebglRenderer extends TerminalRenderer {
|
|
|
832
904
|
for (const buffer of [
|
|
833
905
|
this.quad,
|
|
834
906
|
this.rectBuffer,
|
|
907
|
+
this.decoBuffer,
|
|
835
908
|
this.glyphBuffer,
|
|
836
909
|
this.overlayRectBuffer,
|
|
837
910
|
this.overlayGlyphBuffer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aurigax-ai/ghostty-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Ghostty's terminal engine (libghostty-vt) in the browser: WebAssembly build, an xterm.js-shaped API and WebGL and canvas renderers. A hard fork of coder/ghostty-web.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.ts",
|