@vibecook/ghosttea-react 0.11.1 → 0.13.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 +86 -4
- package/dist/TerminalSurface.d.ts +53 -0
- package/dist/TerminalSurface.d.ts.map +1 -1
- package/dist/TerminalSurface.js +106 -12
- package/dist/TerminalSurface.js.map +1 -1
- package/dist/appearance/AppearanceSettings.d.ts +5 -2
- package/dist/appearance/AppearanceSettings.d.ts.map +1 -1
- package/dist/appearance/AppearanceSettings.js +14 -3
- package/dist/appearance/AppearanceSettings.js.map +1 -1
- package/dist/appearance/BackdropBlurControl.d.ts +5 -0
- package/dist/appearance/BackdropBlurControl.d.ts.map +1 -0
- package/dist/appearance/BackdropBlurControl.js +42 -0
- package/dist/appearance/BackdropBlurControl.js.map +1 -0
- package/dist/appearance/types.d.ts +10 -0
- package/dist/appearance/types.d.ts.map +1 -1
- package/dist/catalog-budget.d.ts +2 -2
- package/dist/catalog-budget.d.ts.map +1 -1
- package/dist/catalog-budget.js +2 -2
- package/dist/catalog-budget.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/link-input.d.ts +18 -0
- package/dist/link-input.d.ts.map +1 -0
- package/dist/link-input.js +27 -0
- package/dist/link-input.js.map +1 -0
- package/dist/renderers/background-geometry.d.ts +16 -0
- package/dist/renderers/background-geometry.d.ts.map +1 -0
- package/dist/renderers/background-geometry.js +20 -0
- package/dist/renderers/background-geometry.js.map +1 -0
- package/dist/renderers/glyph-atlas.d.ts +43 -0
- package/dist/renderers/glyph-atlas.d.ts.map +1 -0
- package/dist/renderers/glyph-atlas.js +175 -0
- package/dist/renderers/glyph-atlas.js.map +1 -0
- package/dist/renderers/light-adaptation.d.ts +36 -0
- package/dist/renderers/light-adaptation.d.ts.map +1 -0
- package/dist/renderers/light-adaptation.js +332 -0
- package/dist/renderers/light-adaptation.js.map +1 -0
- package/dist/renderers/types.d.ts +23 -0
- package/dist/renderers/types.d.ts.map +1 -1
- package/dist/renderers/types.js.map +1 -1
- package/dist/renderers/webgpu-renderer.d.ts +4 -0
- package/dist/renderers/webgpu-renderer.d.ts.map +1 -1
- package/dist/renderers/webgpu-renderer.js +331 -239
- package/dist/renderers/webgpu-renderer.js.map +1 -1
- package/dist/routed-control.d.ts +26 -2
- package/dist/routed-control.d.ts.map +1 -1
- package/dist/routed-control.js +145 -27
- package/dist/routed-control.js.map +1 -1
- package/dist/routed-frames.d.ts +1 -1
- package/dist/routed-frames.d.ts.map +1 -1
- package/dist/routed-frames.js +3 -7
- package/dist/routed-frames.js.map +1 -1
- package/dist/row-damage.d.ts +21 -0
- package/dist/row-damage.d.ts.map +1 -0
- package/dist/row-damage.js +76 -0
- package/dist/row-damage.js.map +1 -0
- package/dist/runtime.d.ts +97 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +489 -49
- package/dist/runtime.js.map +1 -1
- package/dist/styles.css +24 -0
- package/dist/terminal-render.worker.js +979 -297
- package/dist/terminal-render.worker.js.map +4 -4
- package/dist/use-terminal-links.d.ts +17 -0
- package/dist/use-terminal-links.d.ts.map +1 -0
- package/dist/use-terminal-links.js +101 -0
- package/dist/use-terminal-links.js.map +1 -0
- package/dist/worker-messages.d.ts +27 -1
- package/dist/worker-messages.d.ts.map +1 -1
- package/dist/workspace/Workspace.d.ts +19 -3
- package/dist/workspace/Workspace.d.ts.map +1 -1
- package/dist/workspace/Workspace.js +53 -20
- package/dist/workspace/Workspace.js.map +1 -1
- package/dist/workspace/index.d.ts +2 -2
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/pane-focus.d.ts +8 -0
- package/dist/workspace/pane-focus.d.ts.map +1 -0
- package/dist/workspace/pane-focus.js +24 -0
- package/dist/workspace/pane-focus.js.map +1 -0
- package/dist/workspace/pane-layout.d.ts +5 -2
- package/dist/workspace/pane-layout.d.ts.map +1 -1
- package/dist/workspace/pane-layout.js +45 -6
- package/dist/workspace/pane-layout.js.map +1 -1
- package/package.json +4 -4
|
@@ -3,9 +3,11 @@ import { CursorStyle, GlyphFormat } from "@vibecook/ghosttea-frame";
|
|
|
3
3
|
import { emptyRenderMetrics } from "../performance.js";
|
|
4
4
|
import { CELL_WIDTH, LINE_HEIGHT, ORIGIN_X, ORIGIN_Y, effectiveCursorStyle, renderedSizeChanged, } from "./types.js";
|
|
5
5
|
import { graphemeCellWidth, splitGraphemes } from "../cell-width.js";
|
|
6
|
+
import { backgroundRunBounds } from "./background-geometry.js";
|
|
6
7
|
import { rowsForDamage } from "./render-damage.js";
|
|
7
8
|
import { SHADER_EFFECT_WGSL } from "./shader-effects.js";
|
|
8
|
-
|
|
9
|
+
import { createLightAdaptation, darkPaintedBase, ensureContrast } from "./light-adaptation.js";
|
|
10
|
+
import { AtlasBatchFullError, AtlasCapacityError, GlyphAtlasPages } from "./glyph-atlas.js";
|
|
9
11
|
const GEOMETRY_CACHE_LIMIT = 8;
|
|
10
12
|
const FONT_STACK = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace";
|
|
11
13
|
const EFFECT_MODES = {
|
|
@@ -179,74 +181,29 @@ class DynamicVertexBuffer {
|
|
|
179
181
|
}
|
|
180
182
|
class FallbackGlyphAtlas {
|
|
181
183
|
device;
|
|
182
|
-
|
|
183
|
-
#sampler;
|
|
184
|
-
#bindGroup;
|
|
185
|
-
#cache = new Map();
|
|
184
|
+
pages;
|
|
186
185
|
#scratch = new OffscreenCanvas(1, 1);
|
|
187
|
-
#x = 1;
|
|
188
|
-
#y = 1;
|
|
189
|
-
#rowHeight = 0;
|
|
190
186
|
#uploadBytes = 0;
|
|
191
187
|
#uploadCalls = 0;
|
|
192
|
-
#generation = 0;
|
|
193
188
|
constructor(device, bindGroupLayout) {
|
|
194
189
|
this.device = device;
|
|
195
|
-
this
|
|
196
|
-
label: "terminal glyph atlas",
|
|
197
|
-
size: [ATLAS_SIZE, ATLAS_SIZE],
|
|
198
|
-
format: "rgba8unorm",
|
|
199
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
200
|
-
});
|
|
201
|
-
this.#sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
|
|
202
|
-
this.#bindGroup = device.createBindGroup({
|
|
203
|
-
label: "terminal glyph atlas bindings",
|
|
204
|
-
layout: bindGroupLayout,
|
|
205
|
-
entries: [
|
|
206
|
-
{ binding: 0, resource: this.#texture.createView() },
|
|
207
|
-
{ binding: 1, resource: this.#sampler },
|
|
208
|
-
],
|
|
209
|
-
});
|
|
190
|
+
this.pages = new GlyphAtlasPages(device, bindGroupLayout, "terminal diagnostic glyph atlas", "rgba8unorm", 1);
|
|
210
191
|
}
|
|
211
192
|
get bindGroup() {
|
|
212
|
-
return this
|
|
193
|
+
return this.pages.bindGroup;
|
|
213
194
|
}
|
|
214
195
|
get generation() {
|
|
215
|
-
return this
|
|
196
|
+
return this.pages.generation;
|
|
216
197
|
}
|
|
217
198
|
prepare(values, dpr) {
|
|
218
199
|
const scale = Math.max(1, Math.min(3, dpr));
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.#y = 1;
|
|
227
|
-
this.#rowHeight = 0;
|
|
228
|
-
if (!this.#fits(all, scale, this.#x, this.#y, this.#rowHeight)) {
|
|
229
|
-
throw new Error("The diagnostic glyph atlas cannot fit the text visible in one frame");
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
#fits(values, scale, startX, startY, startRowHeight) {
|
|
233
|
-
let x = startX;
|
|
234
|
-
let y = startY;
|
|
235
|
-
let rowHeight = startRowHeight;
|
|
236
|
-
for (const value of values) {
|
|
237
|
-
const width = Math.max(1, Math.ceil(CELL_WIDTH * graphemeCellWidth(value) * scale));
|
|
238
|
-
const height = Math.max(1, Math.ceil(LINE_HEIGHT * scale));
|
|
239
|
-
if (x + width + 1 > ATLAS_SIZE) {
|
|
240
|
-
x = 1;
|
|
241
|
-
y += rowHeight + 1;
|
|
242
|
-
rowHeight = 0;
|
|
243
|
-
}
|
|
244
|
-
if (y + height + 1 > ATLAS_SIZE)
|
|
245
|
-
return false;
|
|
246
|
-
x += width + 1;
|
|
247
|
-
rowHeight = Math.max(rowHeight, height);
|
|
248
|
-
}
|
|
249
|
-
return true;
|
|
200
|
+
this.pages.prepare([...new Set(values)]
|
|
201
|
+
.filter((value) => value.trim().length > 0)
|
|
202
|
+
.map((value) => ({
|
|
203
|
+
key: `${scale.toFixed(2)}:${value}`,
|
|
204
|
+
width: Math.max(1, Math.ceil(CELL_WIDTH * graphemeCellWidth(value) * scale)),
|
|
205
|
+
height: Math.max(1, Math.ceil(LINE_HEIGHT * scale)),
|
|
206
|
+
})));
|
|
250
207
|
}
|
|
251
208
|
glyph(grapheme, dpr) {
|
|
252
209
|
if (grapheme.trim().length === 0)
|
|
@@ -254,19 +211,11 @@ class FallbackGlyphAtlas {
|
|
|
254
211
|
const scale = Math.max(1, Math.min(3, dpr));
|
|
255
212
|
const cells = graphemeCellWidth(grapheme);
|
|
256
213
|
const key = `${scale.toFixed(2)}:${grapheme}`;
|
|
257
|
-
const cached = this
|
|
214
|
+
const cached = this.pages.get(key);
|
|
258
215
|
if (cached)
|
|
259
216
|
return cached;
|
|
260
217
|
const width = Math.max(1, Math.ceil(CELL_WIDTH * cells * scale));
|
|
261
218
|
const height = Math.max(1, Math.ceil(LINE_HEIGHT * scale));
|
|
262
|
-
if (this.#x + width + 1 > ATLAS_SIZE) {
|
|
263
|
-
this.#x = 1;
|
|
264
|
-
this.#y += this.#rowHeight + 1;
|
|
265
|
-
this.#rowHeight = 0;
|
|
266
|
-
}
|
|
267
|
-
if (this.#y + height + 1 > ATLAS_SIZE) {
|
|
268
|
-
throw new Error("Temporary glyph atlas exhausted");
|
|
269
|
-
}
|
|
270
219
|
this.#scratch.width = width;
|
|
271
220
|
this.#scratch.height = height;
|
|
272
221
|
const context = this.#scratch.getContext("2d", { willReadFrequently: true });
|
|
@@ -278,23 +227,17 @@ class FallbackGlyphAtlas {
|
|
|
278
227
|
context.textBaseline = "top";
|
|
279
228
|
context.fillText(grapheme, 0, Math.max(0, Math.floor((height - 15 * scale) / 2)));
|
|
280
229
|
const image = context.getImageData(0, 0, width, height);
|
|
281
|
-
|
|
230
|
+
const { location, texture, origin } = this.pages.insert(key, width, height, cells);
|
|
231
|
+
this.device.queue.writeTexture({ texture, origin }, image.data, { bytesPerRow: width * 4, rowsPerImage: height }, [
|
|
232
|
+
width,
|
|
233
|
+
height,
|
|
234
|
+
]);
|
|
282
235
|
this.#uploadBytes += image.data.byteLength;
|
|
283
236
|
this.#uploadCalls += 1;
|
|
284
|
-
const location = {
|
|
285
|
-
u0: this.#x / ATLAS_SIZE,
|
|
286
|
-
v0: this.#y / ATLAS_SIZE,
|
|
287
|
-
u1: (this.#x + width) / ATLAS_SIZE,
|
|
288
|
-
v1: (this.#y + height) / ATLAS_SIZE,
|
|
289
|
-
cells,
|
|
290
|
-
};
|
|
291
|
-
this.#cache.set(key, location);
|
|
292
|
-
this.#x += width + 1;
|
|
293
|
-
this.#rowHeight = Math.max(this.#rowHeight, height);
|
|
294
237
|
return location;
|
|
295
238
|
}
|
|
296
239
|
destroy() {
|
|
297
|
-
this
|
|
240
|
+
this.pages.destroy();
|
|
298
241
|
}
|
|
299
242
|
uploadMetrics() {
|
|
300
243
|
return { bytes: this.#uploadBytes, calls: this.#uploadCalls };
|
|
@@ -304,111 +247,44 @@ class NativeGlyphAtlas {
|
|
|
304
247
|
device;
|
|
305
248
|
label;
|
|
306
249
|
format;
|
|
307
|
-
|
|
308
|
-
#sampler;
|
|
309
|
-
#bindGroup;
|
|
310
|
-
#cache = new Map();
|
|
311
|
-
#x = 1;
|
|
312
|
-
#y = 1;
|
|
313
|
-
#rowHeight = 0;
|
|
250
|
+
pages;
|
|
314
251
|
#uploadBytes = 0;
|
|
315
252
|
#uploadCalls = 0;
|
|
316
|
-
#generation = 0;
|
|
317
253
|
constructor(device, bindGroupLayout, label, format) {
|
|
318
254
|
this.device = device;
|
|
319
255
|
this.label = label;
|
|
320
256
|
this.format = format;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
format,
|
|
325
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
326
|
-
});
|
|
327
|
-
this.#sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
|
|
328
|
-
this.#bindGroup = device.createBindGroup({
|
|
329
|
-
label: `${label} bindings`,
|
|
330
|
-
layout: bindGroupLayout,
|
|
331
|
-
entries: [
|
|
332
|
-
{ binding: 0, resource: this.#texture.createView() },
|
|
333
|
-
{ binding: 1, resource: this.#sampler },
|
|
334
|
-
],
|
|
335
|
-
});
|
|
257
|
+
// At most 16 MiB mono + 32 MiB color + 16 MiB diagnostic pages. Empty
|
|
258
|
+
// formats allocate no textures. The ordinary ASCII working set uses 4 MiB.
|
|
259
|
+
this.pages = new GlyphAtlasPages(device, bindGroupLayout, label, format, format === "r8unorm" ? 4 : 2);
|
|
336
260
|
}
|
|
337
261
|
get bindGroup() {
|
|
338
|
-
return this
|
|
262
|
+
return this.pages.bindGroup;
|
|
339
263
|
}
|
|
340
264
|
get generation() {
|
|
341
|
-
return this
|
|
265
|
+
return this.pages.generation;
|
|
342
266
|
}
|
|
343
267
|
prepare(definitions) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
missing.set(definition.id, definition);
|
|
350
|
-
}
|
|
351
|
-
if (this.#fits(missing.values(), this.#x, this.#y, this.#rowHeight))
|
|
352
|
-
return;
|
|
353
|
-
this.#cache.clear();
|
|
354
|
-
this.#generation += 1;
|
|
355
|
-
this.#x = 1;
|
|
356
|
-
this.#y = 1;
|
|
357
|
-
this.#rowHeight = 0;
|
|
358
|
-
if (!this.#fits(all.values(), this.#x, this.#y, this.#rowHeight)) {
|
|
359
|
-
throw new Error(`${this.label} cannot fit the glyphs visible in one frame`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
#fits(definitions, startX, startY, startRowHeight) {
|
|
363
|
-
let x = startX;
|
|
364
|
-
let y = startY;
|
|
365
|
-
let rowHeight = startRowHeight;
|
|
366
|
-
for (const { width, height } of definitions) {
|
|
367
|
-
if (width + 2 > ATLAS_SIZE || height + 2 > ATLAS_SIZE)
|
|
368
|
-
return false;
|
|
369
|
-
if (x + width + 1 > ATLAS_SIZE) {
|
|
370
|
-
x = 1;
|
|
371
|
-
y += rowHeight + 1;
|
|
372
|
-
rowHeight = 0;
|
|
373
|
-
}
|
|
374
|
-
if (y + height + 1 > ATLAS_SIZE)
|
|
375
|
-
return false;
|
|
376
|
-
x += width + 1;
|
|
377
|
-
rowHeight = Math.max(rowHeight, height);
|
|
378
|
-
}
|
|
379
|
-
return true;
|
|
268
|
+
this.pages.prepare(Array.from(definitions, (definition) => ({
|
|
269
|
+
key: definition.id,
|
|
270
|
+
width: definition.width,
|
|
271
|
+
height: definition.height,
|
|
272
|
+
})));
|
|
380
273
|
}
|
|
381
274
|
glyph(definition) {
|
|
382
|
-
const cached = this
|
|
275
|
+
const cached = this.pages.get(definition.id);
|
|
383
276
|
if (cached)
|
|
384
277
|
return cached;
|
|
385
278
|
const { width, height } = definition;
|
|
386
|
-
|
|
387
|
-
this.#x = 1;
|
|
388
|
-
this.#y += this.#rowHeight + 1;
|
|
389
|
-
this.#rowHeight = 0;
|
|
390
|
-
}
|
|
391
|
-
if (this.#y + height + 1 > ATLAS_SIZE)
|
|
392
|
-
throw new Error(`${this.label} exhausted`);
|
|
393
|
-
const pixels = definition.pixels;
|
|
279
|
+
const { location, texture, origin } = this.pages.insert(definition.id, width, height, 0);
|
|
394
280
|
const bytesPerPixel = this.format === "r8unorm" ? 1 : 4;
|
|
395
|
-
this.device.queue.writeTexture({ texture
|
|
396
|
-
this.#uploadBytes += pixels.byteLength;
|
|
281
|
+
this.device.queue.writeTexture({ texture, origin }, definition.pixels, { bytesPerRow: width * bytesPerPixel, rowsPerImage: height }, [width, height]);
|
|
282
|
+
this.#uploadBytes += definition.pixels.byteLength;
|
|
397
283
|
this.#uploadCalls += 1;
|
|
398
|
-
const location = {
|
|
399
|
-
u0: this.#x / ATLAS_SIZE,
|
|
400
|
-
v0: this.#y / ATLAS_SIZE,
|
|
401
|
-
u1: (this.#x + width) / ATLAS_SIZE,
|
|
402
|
-
v1: (this.#y + height) / ATLAS_SIZE,
|
|
403
|
-
cells: 0,
|
|
404
|
-
};
|
|
405
|
-
this.#cache.set(definition.id, location);
|
|
406
|
-
this.#x += width + 1;
|
|
407
|
-
this.#rowHeight = Math.max(this.#rowHeight, height);
|
|
408
284
|
return location;
|
|
409
285
|
}
|
|
410
286
|
destroy() {
|
|
411
|
-
this
|
|
287
|
+
this.pages.destroy();
|
|
412
288
|
}
|
|
413
289
|
uploadMetrics() {
|
|
414
290
|
return { bytes: this.#uploadBytes, calls: this.#uploadCalls };
|
|
@@ -437,13 +313,14 @@ function createCachedVertexBuffer(device, label, data) {
|
|
|
437
313
|
device.queue.writeBuffer(buffer, 0, data);
|
|
438
314
|
return buffer;
|
|
439
315
|
}
|
|
440
|
-
function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations) {
|
|
316
|
+
function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations, adaptationKey) {
|
|
441
317
|
const selection = view.selection
|
|
442
318
|
? `${view.selection.anchor.row},${view.selection.anchor.column},${view.selection.focus.row},${view.selection.focus.column}`
|
|
443
319
|
: "-";
|
|
444
320
|
return [
|
|
445
321
|
view.sessionEpoch.toString(),
|
|
446
322
|
view.layoutEpoch.toString(),
|
|
323
|
+
view.cols,
|
|
447
324
|
hasNativeRows ? "native" : "fallback",
|
|
448
325
|
rows.map((row) => `${row}:${view.rowRevisions[row]?.toString() ?? "-"}`).join(","),
|
|
449
326
|
[
|
|
@@ -455,6 +332,8 @@ function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations) {
|
|
|
455
332
|
...view.theme.selectionForeground,
|
|
456
333
|
].join(","),
|
|
457
334
|
view.theme.backgroundOpacityCells ? "opacity-cells" : "opaque-cells",
|
|
335
|
+
view.theme.minimumContrast ?? 1,
|
|
336
|
+
adaptationKey,
|
|
458
337
|
selection,
|
|
459
338
|
effectiveCursorStyle(view) === CursorStyle.Block ? `${view.cursor.x},${view.cursor.y}` : "-",
|
|
460
339
|
atlasGenerations.join(","),
|
|
@@ -657,22 +536,51 @@ function pushBlockCursorBackground(vertices, view, renderRows, scale, viewportWi
|
|
|
657
536
|
return;
|
|
658
537
|
pushRectangle(vertices, (ORIGIN_X + view.cursor.x * CELL_WIDTH) * scale, (ORIGIN_Y + view.cursor.y * LINE_HEIGHT) * scale, CELL_WIDTH * scale, LINE_HEIGHT * scale, view.theme.cursor, viewportWidth, viewportHeight);
|
|
659
538
|
}
|
|
660
|
-
function resolveStyle(style, theme) {
|
|
539
|
+
function resolveStyle(style, theme, adaptation = null) {
|
|
661
540
|
let foreground = style?.foreground ? rgb(style.foreground) : theme.foreground;
|
|
662
541
|
let background = style?.background ? rgb(style.background) : null;
|
|
663
|
-
|
|
542
|
+
let fill = foreground;
|
|
543
|
+
if (adaptation) {
|
|
544
|
+
// Application colors were designed on the app's dark base; theme-owned
|
|
545
|
+
// defaults already belong to the light theme and pass through.
|
|
546
|
+
const source = { foreground: style?.foreground, background: style?.background };
|
|
547
|
+
if (style?.inverse) {
|
|
548
|
+
background = source.foreground ? adaptation.surface(source.foreground) : theme.foreground;
|
|
549
|
+
foreground = source.background
|
|
550
|
+
? adaptation.ink(source.background, source.foreground ?? adaptation.base, style?.backgroundPalette)
|
|
551
|
+
: theme.background;
|
|
552
|
+
fill = foreground;
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
foreground = source.foreground
|
|
556
|
+
? adaptation.ink(source.foreground, source.background ?? adaptation.base, style?.foregroundPalette)
|
|
557
|
+
: theme.foreground;
|
|
558
|
+
background = source.background ? adaptation.surface(source.background) : null;
|
|
559
|
+
fill = source.foreground ? adaptation.surface(source.foreground) : theme.foreground;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else if (style?.inverse) {
|
|
664
563
|
const originalForeground = foreground;
|
|
665
564
|
foreground = background ?? theme.background;
|
|
666
565
|
background = originalForeground;
|
|
566
|
+
fill = foreground;
|
|
667
567
|
}
|
|
668
568
|
if (background && theme.backgroundOpacityCells) {
|
|
669
569
|
background = [background[0], background[1], background[2], theme.background[3]];
|
|
670
570
|
}
|
|
671
|
-
|
|
571
|
+
// Graphics (box drawing, block elements) and color glyphs keep the raw
|
|
572
|
+
// foreground, as in Ghostty; the backdrop ignores window transparency.
|
|
573
|
+
let text = ensureContrast(foreground, over(background ?? theme.background, theme.background), theme.minimumContrast ?? 1);
|
|
574
|
+
if (style?.faint) {
|
|
672
575
|
foreground = [foreground[0], foreground[1], foreground[2], foreground[3] * 0.55];
|
|
576
|
+
fill = [fill[0], fill[1], fill[2], fill[3] * 0.55];
|
|
577
|
+
text = [text[0], text[1], text[2], text[3] * 0.55];
|
|
578
|
+
}
|
|
673
579
|
return {
|
|
674
580
|
foreground,
|
|
675
581
|
background,
|
|
582
|
+
fill,
|
|
583
|
+
text,
|
|
676
584
|
underline: style?.underline ?? false,
|
|
677
585
|
strikethrough: style?.strikethrough ?? false,
|
|
678
586
|
invisible: style?.invisible ?? false,
|
|
@@ -819,7 +727,10 @@ export class WebGpuTerminalRenderer {
|
|
|
819
727
|
#monoAtlas;
|
|
820
728
|
#colorAtlas;
|
|
821
729
|
#fallbackAtlas;
|
|
730
|
+
#adaptations = new Map();
|
|
822
731
|
#performanceMeasurementEnabled = false;
|
|
732
|
+
#submission = { queueSubmits: 0, surfaceIds: [] };
|
|
733
|
+
#scrollScratch;
|
|
823
734
|
constructor(device, format) {
|
|
824
735
|
this.device = device;
|
|
825
736
|
this.format = format;
|
|
@@ -1018,6 +929,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1018
929
|
geometryCache: new Map(),
|
|
1019
930
|
geometryCandidates: new Map(),
|
|
1020
931
|
sceneValid: false,
|
|
932
|
+
adaptation: null,
|
|
933
|
+
sceneHadSelection: false,
|
|
934
|
+
sceneHadOverhang: false,
|
|
935
|
+
sceneCursorRow: null,
|
|
1021
936
|
};
|
|
1022
937
|
this.#surfaces.set(id, surface);
|
|
1023
938
|
this.#configure(surface);
|
|
@@ -1039,6 +954,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1039
954
|
surface.cursorBuffer.destroy();
|
|
1040
955
|
clearGeometryCache(surface);
|
|
1041
956
|
this.#surfaces.delete(id);
|
|
957
|
+
if (this.#surfaces.size === 0) {
|
|
958
|
+
this.#scrollScratch?.destroy();
|
|
959
|
+
this.#scrollScratch = undefined;
|
|
960
|
+
}
|
|
1042
961
|
}
|
|
1043
962
|
resize(id, size) {
|
|
1044
963
|
const surface = this.#surfaces.get(id);
|
|
@@ -1071,7 +990,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1071
990
|
label: "terminal scene before Ghostty custom shaders",
|
|
1072
991
|
size: [Math.max(1, width), Math.max(1, height)],
|
|
1073
992
|
format: this.format,
|
|
1074
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT |
|
|
993
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT |
|
|
994
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
995
|
+
GPUTextureUsage.COPY_SRC |
|
|
996
|
+
GPUTextureUsage.COPY_DST,
|
|
1075
997
|
});
|
|
1076
998
|
}
|
|
1077
999
|
#createEffectTexture(width, height, suffix) {
|
|
@@ -1188,37 +1110,227 @@ export class WebGpuTerminalRenderer {
|
|
|
1188
1110
|
drawCalls: effectPassCount,
|
|
1189
1111
|
};
|
|
1190
1112
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1113
|
+
/**
|
|
1114
|
+
* Engage, re-key, or release light adaptation for a pane. Any change remaps
|
|
1115
|
+
* every color, so the persistent scene and cached geometry are invalidated.
|
|
1116
|
+
*/
|
|
1117
|
+
#prepareAdaptation(surface, view) {
|
|
1118
|
+
const base = darkPaintedBase(view, surface.adaptation !== null);
|
|
1119
|
+
let next = null;
|
|
1120
|
+
if (base) {
|
|
1121
|
+
const key = `${base.join(",")}|${view.theme.background.join(",")}|${view.theme.foreground.join(",")}`;
|
|
1122
|
+
next = this.#adaptations.get(key) ?? null;
|
|
1123
|
+
if (!next) {
|
|
1124
|
+
next = createLightAdaptation(base, view.theme.background, view.theme.foreground);
|
|
1125
|
+
this.#adaptations.set(key, next);
|
|
1126
|
+
if (this.#adaptations.size > 8)
|
|
1127
|
+
this.#adaptations.delete(this.#adaptations.keys().next().value);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if ((next?.key ?? null) === (surface.adaptation?.key ?? null))
|
|
1194
1131
|
return;
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1132
|
+
surface.adaptation = next;
|
|
1133
|
+
surface.sceneValid = false;
|
|
1134
|
+
clearGeometryCache(surface);
|
|
1135
|
+
}
|
|
1136
|
+
submission() {
|
|
1137
|
+
return this.#submission;
|
|
1138
|
+
}
|
|
1139
|
+
#releaseAtlasBatch() {
|
|
1140
|
+
this.#monoAtlas.pages.releaseBatch();
|
|
1141
|
+
this.#colorAtlas.pages.releaseBatch();
|
|
1142
|
+
this.#fallbackAtlas.pages.releaseBatch();
|
|
1143
|
+
}
|
|
1144
|
+
#prepareAtlases(view, renderRows, dpr) {
|
|
1145
|
+
const mono = new Map();
|
|
1146
|
+
const color = new Map();
|
|
1147
|
+
for (const row of renderRows) {
|
|
1148
|
+
for (const glyph of view.nativeRows[row] ?? []) {
|
|
1149
|
+
const definition = view.glyphDefinitions.get(glyph.glyphId);
|
|
1150
|
+
if (definition)
|
|
1151
|
+
(definition.format === GlyphFormat.Alpha8 ? mono : color).set(definition.id, definition);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
this.#monoAtlas.prepare(mono.values());
|
|
1155
|
+
this.#colorAtlas.prepare(color.values());
|
|
1156
|
+
const fallback = !view.nativeRows.some((row) => row.length > 0);
|
|
1157
|
+
if (fallback) {
|
|
1158
|
+
this.#fallbackAtlas.prepare(renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")), dpr);
|
|
1159
|
+
}
|
|
1160
|
+
// Pin only after every format fits, so a rejected pane cannot occupy the
|
|
1161
|
+
// batch budget with pages that no pending draw actually references.
|
|
1162
|
+
if (mono.size > 0)
|
|
1163
|
+
this.#monoAtlas.pages.pin();
|
|
1164
|
+
if (color.size > 0)
|
|
1165
|
+
this.#colorAtlas.pages.pin();
|
|
1166
|
+
if (fallback && renderRows.length > 0)
|
|
1167
|
+
this.#fallbackAtlas.pages.pin();
|
|
1168
|
+
}
|
|
1169
|
+
render(id, view) {
|
|
1170
|
+
return this.renderBatch([{ id, view }])[0];
|
|
1171
|
+
}
|
|
1172
|
+
#canCopyScroll(surface, view) {
|
|
1173
|
+
const shift = view.damage?.scrollRows ?? 0;
|
|
1174
|
+
const top = ORIGIN_Y * surface.dpr;
|
|
1175
|
+
const line = LINE_HEIGHT * surface.dpr;
|
|
1176
|
+
const count = view.rows.length;
|
|
1177
|
+
return (surface.sceneValid &&
|
|
1178
|
+
!view.damage?.full &&
|
|
1179
|
+
Number.isSafeInteger(shift) &&
|
|
1180
|
+
shift !== 0 &&
|
|
1181
|
+
Math.abs(shift) < count &&
|
|
1182
|
+
!view.selection &&
|
|
1183
|
+
!surface.sceneHadSelection &&
|
|
1184
|
+
Number.isInteger(top) &&
|
|
1185
|
+
Number.isInteger(line) &&
|
|
1186
|
+
top + count * line <= surface.canvas.height);
|
|
1187
|
+
}
|
|
1188
|
+
#prepareScrollScratch(entries) {
|
|
1189
|
+
let width = 0, height = 0;
|
|
1190
|
+
for (const { id, view } of entries) {
|
|
1191
|
+
const surface = this.#surfaces.get(id);
|
|
1192
|
+
if (!surface || !this.#canCopyScroll(surface, view))
|
|
1193
|
+
continue;
|
|
1194
|
+
width = Math.max(width, surface.canvas.width);
|
|
1195
|
+
height = Math.max(height, view.rows.length * LINE_HEIGHT * surface.dpr);
|
|
1196
|
+
}
|
|
1197
|
+
if (width === 0)
|
|
1198
|
+
return false;
|
|
1199
|
+
// One shared staging texture, never one per pane. Fall back to redraw if
|
|
1200
|
+
// the batch's maximum extent would exceed this fixed memory ceiling.
|
|
1201
|
+
if (width * height * 4 > 64 * 1024 * 1024)
|
|
1202
|
+
return false;
|
|
1203
|
+
if (!this.#scrollScratch || this.#scrollScratch.width < width || this.#scrollScratch.height < height) {
|
|
1204
|
+
this.#scrollScratch?.destroy();
|
|
1205
|
+
this.#scrollScratch = this.device.createTexture({
|
|
1206
|
+
label: "terminal shared scroll staging",
|
|
1207
|
+
size: [width, height],
|
|
1208
|
+
format: this.format,
|
|
1209
|
+
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
return true;
|
|
1213
|
+
}
|
|
1214
|
+
#copyScroll(surface, view, encoder) {
|
|
1215
|
+
const dy = view.damage.scrollRows * LINE_HEIGHT * surface.dpr;
|
|
1216
|
+
const top = ORIGIN_Y * surface.dpr;
|
|
1217
|
+
const size = [surface.canvas.width, view.rows.length * LINE_HEIGHT * surface.dpr - Math.abs(dy)];
|
|
1218
|
+
// A texture cannot copy overlapping regions to itself. Encoder ordering
|
|
1219
|
+
// lets all panes safely share this scratch texture, even within one batch.
|
|
1220
|
+
encoder.copyTextureToTexture({ texture: surface.sceneTexture, origin: [0, top + Math.max(0, dy)] }, { texture: this.#scrollScratch }, size);
|
|
1221
|
+
encoder.copyTextureToTexture({ texture: this.#scrollScratch }, { texture: surface.sceneTexture, origin: [0, top + Math.max(0, -dy)] }, size);
|
|
1203
1222
|
}
|
|
1204
1223
|
renderBatch(entries) {
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1224
|
+
this.#submission = { queueSubmits: 0, surfaceIds: [] };
|
|
1225
|
+
this.#releaseAtlasBatch();
|
|
1226
|
+
const prepared = entries.map(({ id, view }) => {
|
|
1227
|
+
const surface = this.#surfaces.get(id);
|
|
1228
|
+
// Light adaptation first, for the whole batch: a change remaps every color
|
|
1229
|
+
// and invalidates the scene, so every decision below (the overhang
|
|
1230
|
+
// shortcut, the scroll staging, the scroll copy) sees it invalidated.
|
|
1231
|
+
if (surface)
|
|
1232
|
+
this.#prepareAdaptation(surface, view);
|
|
1233
|
+
const overhang = view.damage?.geometryChanged === false && surface?.sceneValid
|
|
1234
|
+
? surface.sceneHadOverhang
|
|
1235
|
+
: view.nativeRows.some((row) => row.some((glyph) => glyph.y < 0 || glyph.y + glyph.height > LINE_HEIGHT));
|
|
1236
|
+
// Row clears cannot reconstruct cross-row alpha composition at the
|
|
1237
|
+
// repair boundary. Preserve those glyphs with a complete scene redraw.
|
|
1238
|
+
if (view.damage &&
|
|
1239
|
+
!view.damage.full &&
|
|
1240
|
+
(view.damage.scrollRows || view.damage.rows.size > 0) &&
|
|
1241
|
+
(overhang || surface?.sceneHadOverhang)) {
|
|
1242
|
+
view = { ...view, damage: { full: true, rows: new Set(), geometryChanged: true } };
|
|
1243
|
+
}
|
|
1244
|
+
return { id, view, overhang };
|
|
1245
|
+
});
|
|
1246
|
+
const canStageScroll = this.#prepareScrollScratch(prepared);
|
|
1247
|
+
let encoder;
|
|
1248
|
+
let pendingIds = [];
|
|
1209
1249
|
const byId = new Map();
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1250
|
+
const submit = () => {
|
|
1251
|
+
if (encoder && pendingIds.length > 0) {
|
|
1252
|
+
this.device.queue.submit([encoder.finish()]);
|
|
1253
|
+
this.#submission.queueSubmits += 1;
|
|
1254
|
+
this.#submission.surfaceIds.push(...pendingIds);
|
|
1255
|
+
const first = pendingIds.map((id) => byId.get(id)).find((metric) => metric !== undefined);
|
|
1256
|
+
if (first)
|
|
1257
|
+
first.queueSubmits += 1;
|
|
1258
|
+
}
|
|
1259
|
+
encoder = undefined;
|
|
1260
|
+
pendingIds = [];
|
|
1261
|
+
this.#releaseAtlasBatch();
|
|
1262
|
+
};
|
|
1263
|
+
try {
|
|
1264
|
+
for (const { id, view: originalView, overhang } of prepared) {
|
|
1265
|
+
const surface = this.#surfaces.get(id);
|
|
1266
|
+
if (!surface)
|
|
1267
|
+
continue;
|
|
1268
|
+
let view = originalView;
|
|
1269
|
+
const copyScroll = canStageScroll && this.#canCopyScroll(surface, view);
|
|
1270
|
+
if (view.damage?.scrollRows) {
|
|
1271
|
+
if (copyScroll) {
|
|
1272
|
+
const rows = new Set(view.damage.rows);
|
|
1273
|
+
const shift = view.damage.scrollRows;
|
|
1274
|
+
// Repaint exposed rows, the two clipped edges and the cursor
|
|
1275
|
+
// baked into the last submitted scene (possibly several frames ago).
|
|
1276
|
+
rows.add(0);
|
|
1277
|
+
rows.add(view.rows.length - 1);
|
|
1278
|
+
for (let row = 0; row < view.rows.length; row++) {
|
|
1279
|
+
if (row + shift < 0 || row + shift >= view.rows.length)
|
|
1280
|
+
rows.add(row);
|
|
1281
|
+
}
|
|
1282
|
+
if (surface.sceneCursorRow !== null)
|
|
1283
|
+
rows.add(surface.sceneCursorRow - shift);
|
|
1284
|
+
if (effectiveCursorStyle(view) !== null)
|
|
1285
|
+
rows.add(view.cursor.y);
|
|
1286
|
+
view = { ...view, damage: { ...view.damage, rows, geometryChanged: true } };
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
view = { ...view, damage: { full: true, rows: new Set(), geometryChanged: true } };
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
const count = Math.max(view.rows.length, view.nativeRows.length, view.nativeStyleRows.length);
|
|
1293
|
+
const { rows } = rowsForDamage(count, view.damage, surface.sceneValid);
|
|
1294
|
+
try {
|
|
1295
|
+
try {
|
|
1296
|
+
this.#prepareAtlases(view, rows, surface.dpr);
|
|
1297
|
+
}
|
|
1298
|
+
catch (error) {
|
|
1299
|
+
if (!(error instanceof AtlasBatchFullError) || pendingIds.length === 0)
|
|
1300
|
+
throw error;
|
|
1301
|
+
// All recyclable pages are referenced by pending draws. Submit
|
|
1302
|
+
// them before overwriting any texels, then retry this pane once.
|
|
1303
|
+
submit();
|
|
1304
|
+
this.#prepareAtlases(view, rows, surface.dpr);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
catch (error) {
|
|
1308
|
+
if (!(error instanceof AtlasCapacityError))
|
|
1309
|
+
throw error;
|
|
1310
|
+
// Capacity failure is isolated to this pane and preserves its old
|
|
1311
|
+
// scene. Other panes still submit; new content/resize can retry it.
|
|
1312
|
+
console.error(`[terminal-renderer] cannot render ${id}`, error);
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
encoder ??= this.device.createCommandEncoder({ label: "terminal frame batch" });
|
|
1316
|
+
if (copyScroll)
|
|
1317
|
+
this.#copyScroll(surface, view, encoder);
|
|
1318
|
+
byId.set(id, view.damage?.geometryChanged === false
|
|
1319
|
+
? this.#encodeCachedRender(id, view, encoder)
|
|
1320
|
+
: this.#encodeRender(id, view, encoder));
|
|
1321
|
+
surface.sceneHadSelection = view.selection !== null;
|
|
1322
|
+
surface.sceneHadOverhang = overhang;
|
|
1323
|
+
surface.sceneCursorRow = effectiveCursorStyle(view) === null ? null : view.cursor.y;
|
|
1324
|
+
pendingIds.push(id);
|
|
1325
|
+
}
|
|
1326
|
+
submit();
|
|
1327
|
+
}
|
|
1328
|
+
finally {
|
|
1329
|
+
this.#releaseAtlasBatch();
|
|
1214
1330
|
}
|
|
1215
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1216
|
-
const firstMetrics = [...byId.values()].find((metrics) => metrics !== undefined);
|
|
1217
|
-
if (firstMetrics)
|
|
1218
|
-
firstMetrics.queueSubmits = 1;
|
|
1219
1331
|
return entries.map(({ id }) => byId.get(id));
|
|
1220
1332
|
}
|
|
1221
|
-
#buildGeometry(view, renderRows, rowCount, fullRedraw, hasNativeRows, scale, viewportWidth, viewportHeight) {
|
|
1333
|
+
#buildGeometry(view, renderRows, rowCount, fullRedraw, hasNativeRows, scale, viewportWidth, viewportHeight, adaptation) {
|
|
1222
1334
|
const rectangleVertices = [];
|
|
1223
1335
|
const glyphVertices = [];
|
|
1224
1336
|
const colorGlyphVertices = [];
|
|
@@ -1228,22 +1340,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1228
1340
|
const cached = resolvedStyles.get(styleId);
|
|
1229
1341
|
if (cached)
|
|
1230
1342
|
return cached;
|
|
1231
|
-
const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme);
|
|
1343
|
+
const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme, adaptation);
|
|
1232
1344
|
resolvedStyles.set(styleId, resolved);
|
|
1233
1345
|
return resolved;
|
|
1234
1346
|
};
|
|
1235
|
-
const monoDefinitions = new Map();
|
|
1236
|
-
const colorDefinitions = new Map();
|
|
1237
|
-
for (const row of renderRows) {
|
|
1238
|
-
for (const glyph of view.nativeRows[row] ?? []) {
|
|
1239
|
-
const definition = view.glyphDefinitions.get(glyph.glyphId);
|
|
1240
|
-
if (!definition)
|
|
1241
|
-
continue;
|
|
1242
|
-
(definition.format === GlyphFormat.Rgba8Premultiplied ? colorDefinitions : monoDefinitions).set(definition.id, definition);
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
this.#monoAtlas.prepare(monoDefinitions.values());
|
|
1246
|
-
this.#colorAtlas.prepare(colorDefinitions.values());
|
|
1247
1347
|
if (!fullRedraw) {
|
|
1248
1348
|
for (const row of renderRows) {
|
|
1249
1349
|
const top = row === 0 ? 0 : (ORIGIN_Y + row * LINE_HEIGHT) * scale;
|
|
@@ -1259,7 +1359,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1259
1359
|
const style = styleFor(run.styleId);
|
|
1260
1360
|
if (!style.background)
|
|
1261
1361
|
continue;
|
|
1262
|
-
|
|
1362
|
+
const bounds = backgroundRunBounds(run, row, view.cols, rowCount, scale, viewportWidth, viewportHeight);
|
|
1363
|
+
if (!bounds)
|
|
1364
|
+
continue;
|
|
1365
|
+
pushRectangle(rectangleVertices, bounds.x, bounds.y, bounds.width, bounds.height, style.background, viewportWidth, viewportHeight);
|
|
1263
1366
|
}
|
|
1264
1367
|
}
|
|
1265
1368
|
const backgroundInstanceCount = rectangleVertices.length / 10;
|
|
@@ -1276,9 +1379,6 @@ export class WebGpuTerminalRenderer {
|
|
|
1276
1379
|
}
|
|
1277
1380
|
pushBlockCursorBackground(rectangleVertices, view, renderRows, scale, viewportWidth, viewportHeight);
|
|
1278
1381
|
const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
|
|
1279
|
-
if (!hasNativeRows) {
|
|
1280
|
-
this.#fallbackAtlas.prepare(renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")), scale);
|
|
1281
|
-
}
|
|
1282
1382
|
if (hasNativeRows) {
|
|
1283
1383
|
for (const row of renderRows) {
|
|
1284
1384
|
const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "");
|
|
@@ -1310,12 +1410,14 @@ export class WebGpuTerminalRenderer {
|
|
|
1310
1410
|
let backdrop = style.background ?? view.theme.background;
|
|
1311
1411
|
if (isSelected)
|
|
1312
1412
|
backdrop = view.theme.selection;
|
|
1313
|
-
|
|
1413
|
+
const blockForeground = foreground === style.foreground ? style.fill : foreground;
|
|
1414
|
+
if (pushBlockElement(rectangleVertices, blockElement, instance.cellStart, row, scale, over(blockForeground, backdrop), viewportWidth, viewportHeight))
|
|
1314
1415
|
continue;
|
|
1315
1416
|
}
|
|
1316
|
-
const
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1417
|
+
const mono = definition.format === GlyphFormat.Alpha8;
|
|
1418
|
+
const atlas = mono ? this.#monoAtlas : this.#colorAtlas;
|
|
1419
|
+
const vertices = mono ? glyphVertices : colorGlyphVertices;
|
|
1420
|
+
pushGlyph(vertices, (ORIGIN_X + instance.x) * scale, (ORIGIN_Y + row * LINE_HEIGHT + instance.y) * scale, instance.width * scale, instance.height * scale, atlas.glyph(definition), mono && foreground === style.foreground ? style.text : foreground, viewportWidth, viewportHeight);
|
|
1319
1421
|
}
|
|
1320
1422
|
}
|
|
1321
1423
|
}
|
|
@@ -1347,10 +1449,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1347
1449
|
const rowTop = (ORIGIN_Y + row * LINE_HEIGHT) * scale;
|
|
1348
1450
|
const stroke = Math.max(1, Math.round(scale));
|
|
1349
1451
|
if (style.underline) {
|
|
1350
|
-
pushRectangle(rectangleVertices, x, Math.round(rowTop + 16 * scale), width, stroke, style.
|
|
1452
|
+
pushRectangle(rectangleVertices, x, Math.round(rowTop + 16 * scale), width, stroke, style.text, viewportWidth, viewportHeight);
|
|
1351
1453
|
}
|
|
1352
1454
|
if (style.strikethrough) {
|
|
1353
|
-
pushRectangle(rectangleVertices, x, Math.round(rowTop + 9 * scale), width, stroke, style.
|
|
1455
|
+
pushRectangle(rectangleVertices, x, Math.round(rowTop + 9 * scale), width, stroke, style.text, viewportWidth, viewportHeight);
|
|
1354
1456
|
}
|
|
1355
1457
|
}
|
|
1356
1458
|
}
|
|
@@ -1394,22 +1496,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1394
1496
|
const cached = resolvedStyles.get(styleId);
|
|
1395
1497
|
if (cached)
|
|
1396
1498
|
return cached;
|
|
1397
|
-
const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme);
|
|
1499
|
+
const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme, surface.adaptation);
|
|
1398
1500
|
resolvedStyles.set(styleId, resolved);
|
|
1399
1501
|
return resolved;
|
|
1400
1502
|
};
|
|
1401
|
-
const monoDefinitions = new Map();
|
|
1402
|
-
const colorDefinitions = new Map();
|
|
1403
|
-
for (const row of renderRows) {
|
|
1404
|
-
for (const glyph of view.nativeRows[row] ?? []) {
|
|
1405
|
-
const definition = view.glyphDefinitions.get(glyph.glyphId);
|
|
1406
|
-
if (!definition)
|
|
1407
|
-
continue;
|
|
1408
|
-
(definition.format === GlyphFormat.Rgba8Premultiplied ? colorDefinitions : monoDefinitions).set(definition.id, definition);
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
this.#monoAtlas.prepare(monoDefinitions.values());
|
|
1412
|
-
this.#colorAtlas.prepare(colorDefinitions.values());
|
|
1413
1503
|
if (!fullRedraw) {
|
|
1414
1504
|
for (const row of renderRows) {
|
|
1415
1505
|
const top = row === 0 ? 0 : (ORIGIN_Y + row * LINE_HEIGHT) * scale;
|
|
@@ -1425,7 +1515,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1425
1515
|
const style = styleFor(run.styleId);
|
|
1426
1516
|
if (!style.background)
|
|
1427
1517
|
continue;
|
|
1428
|
-
|
|
1518
|
+
const bounds = backgroundRunBounds(run, row, view.cols, rowCount, scale, viewportWidth, viewportHeight);
|
|
1519
|
+
if (!bounds)
|
|
1520
|
+
continue;
|
|
1521
|
+
pushRectangle(rectangleVertices, bounds.x, bounds.y, bounds.width, bounds.height, style.background, viewportWidth, viewportHeight);
|
|
1429
1522
|
}
|
|
1430
1523
|
}
|
|
1431
1524
|
const backgroundInstanceCount = rectangleVertices.length / 10;
|
|
@@ -1443,9 +1536,6 @@ export class WebGpuTerminalRenderer {
|
|
|
1443
1536
|
pushBlockCursorBackground(rectangleVertices, view, renderRows, scale, viewportWidth, viewportHeight);
|
|
1444
1537
|
const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
|
|
1445
1538
|
const hasNativeRows = view.nativeRows.some((row) => row.length > 0);
|
|
1446
|
-
if (!hasNativeRows) {
|
|
1447
|
-
this.#fallbackAtlas.prepare(renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")), scale);
|
|
1448
|
-
}
|
|
1449
1539
|
if (hasNativeRows) {
|
|
1450
1540
|
for (const row of renderRows) {
|
|
1451
1541
|
const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "");
|
|
@@ -1479,13 +1569,15 @@ export class WebGpuTerminalRenderer {
|
|
|
1479
1569
|
let backdrop = style.background ?? view.theme.background;
|
|
1480
1570
|
if (isSelected)
|
|
1481
1571
|
backdrop = view.theme.selection;
|
|
1482
|
-
const
|
|
1572
|
+
const blockForeground = foreground === style.foreground ? style.fill : foreground;
|
|
1573
|
+
const blockColor = over(blockForeground, backdrop);
|
|
1483
1574
|
if (pushBlockElement(rectangleVertices, blockElement, instance.cellStart, row, scale, blockColor, viewportWidth, viewportHeight))
|
|
1484
1575
|
continue;
|
|
1485
1576
|
}
|
|
1486
|
-
const
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1577
|
+
const mono = definition.format === GlyphFormat.Alpha8;
|
|
1578
|
+
const atlas = mono ? this.#monoAtlas : this.#colorAtlas;
|
|
1579
|
+
const vertices = mono ? glyphVertices : colorGlyphVertices;
|
|
1580
|
+
pushGlyph(vertices, (ORIGIN_X + instance.x) * scale, (ORIGIN_Y + row * LINE_HEIGHT + instance.y) * scale, instance.width * scale, instance.height * scale, atlas.glyph(definition), mono && foreground === style.foreground ? style.text : foreground, viewportWidth, viewportHeight);
|
|
1489
1581
|
}
|
|
1490
1582
|
}
|
|
1491
1583
|
}
|
|
@@ -1517,10 +1609,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1517
1609
|
const rowTop = (ORIGIN_Y + row * LINE_HEIGHT) * scale;
|
|
1518
1610
|
const stroke = Math.max(1, Math.round(scale));
|
|
1519
1611
|
if (style.underline) {
|
|
1520
|
-
pushRectangle(rectangleVertices, x, Math.round(rowTop + 16 * scale), width, stroke, style.
|
|
1612
|
+
pushRectangle(rectangleVertices, x, Math.round(rowTop + 16 * scale), width, stroke, style.text, viewportWidth, viewportHeight);
|
|
1521
1613
|
}
|
|
1522
1614
|
if (style.strikethrough) {
|
|
1523
|
-
pushRectangle(rectangleVertices, x, Math.round(rowTop + 9 * scale), width, stroke, style.
|
|
1615
|
+
pushRectangle(rectangleVertices, x, Math.round(rowTop + 9 * scale), width, stroke, style.text, viewportWidth, viewportHeight);
|
|
1524
1616
|
}
|
|
1525
1617
|
}
|
|
1526
1618
|
}
|
|
@@ -1684,7 +1776,7 @@ export class WebGpuTerminalRenderer {
|
|
|
1684
1776
|
this.#colorAtlas.generation,
|
|
1685
1777
|
this.#fallbackAtlas.generation,
|
|
1686
1778
|
];
|
|
1687
|
-
let key = geometryCacheKey(view, damage.rows, hasNativeRows, generations());
|
|
1779
|
+
let key = geometryCacheKey(view, damage.rows, hasNativeRows, generations(), surface.adaptation?.key ?? "-");
|
|
1688
1780
|
const cached = surface.geometryCache.get(key);
|
|
1689
1781
|
const cacheHit = cached !== undefined;
|
|
1690
1782
|
if (cached) {
|
|
@@ -1695,8 +1787,8 @@ export class WebGpuTerminalRenderer {
|
|
|
1695
1787
|
else {
|
|
1696
1788
|
const admitted = surface.geometryCandidates.has(key);
|
|
1697
1789
|
const candidateKey = key;
|
|
1698
|
-
const cpu = this.#buildGeometry(view, damage.rows, rowCount, false, hasNativeRows, scale, viewportWidth, viewportHeight);
|
|
1699
|
-
key = geometryCacheKey(view, damage.rows, hasNativeRows, generations());
|
|
1790
|
+
const cpu = this.#buildGeometry(view, damage.rows, rowCount, false, hasNativeRows, scale, viewportWidth, viewportHeight, surface.adaptation);
|
|
1791
|
+
key = geometryCacheKey(view, damage.rows, hasNativeRows, generations(), surface.adaptation?.key ?? "-");
|
|
1700
1792
|
const promote = admitted && key === candidateKey;
|
|
1701
1793
|
geometry = {
|
|
1702
1794
|
rectangleBuffer: promote
|