Merge pull request #5472 from Tyriar/tyriar/5466_refactor__unified

Custom glyph: Unify rendering function and move all definitions into a single ordered object
This commit is contained in:
Daniel Imms
2025-12-22 13:31:36 -08:00
committed by GitHub
7 changed files with 551 additions and 579 deletions
-8
View File
@@ -159,14 +159,6 @@
"@typescript-eslint/prefer-namespace-keyword": "warn",
"@typescript-eslint/type-annotation-spacing": "warn",
"comma-dangle": [
"warn",
{
"objects": "never",
"arrays": "never",
"functions": "never"
}
],
"curly": [
"warn",
"multi-line"
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,8 @@
*/
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { blockElementDefinitions, blockShadeComboDefinitions, boxDrawingDefinitions, patternCharacterDefinitions, powerlineDefinitions, rectangularShadeDefinitions, symbolsForLegacyComputingDefinitions } from 'customGlyphs/CustomGlyphDefinitions';
import { CustomGlyphVectorType, type CustomGlyphDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from 'customGlyphs/Types';
import { customGlyphDefinitions } from './CustomGlyphDefinitions';
import { CustomGlyphDefinitionType, CustomGlyphVectorType, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type CustomGlyphRegionDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
/**
* Try drawing a custom block element or box drawing character, returning whether it was
@@ -21,46 +21,33 @@ export function tryDrawCustomGlyph(
fontSize: number,
devicePixelRatio: number
): boolean {
const blockElementDefinition = blockElementDefinitions[c];
if (blockElementDefinition) {
drawBlockVectorChar(ctx, blockElementDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
}
const symbolsForLegacyComputingDefinition = symbolsForLegacyComputingDefinitions[c];
if (symbolsForLegacyComputingDefinition) {
drawPathDefinitionCharacter(ctx, symbolsForLegacyComputingDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
}
const rectangularShadeDefinition = rectangularShadeDefinitions[c];
if (rectangularShadeDefinition) {
drawRectangularShadeChar(ctx, rectangularShadeDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
}
const blockShadeComboDefinition = blockShadeComboDefinitions[c];
if (blockShadeComboDefinition) {
drawBlockShadeComboChar(ctx, blockShadeComboDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
}
const patternDefinition = patternCharacterDefinitions[c];
if (patternDefinition) {
drawPatternChar(ctx, patternDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
}
const boxDrawingDefinition = boxDrawingDefinitions[c];
if (boxDrawingDefinition) {
drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio);
return true;
}
const powerlineDefinition = powerlineDefinitions[c];
if (powerlineDefinition) {
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight, fontSize, devicePixelRatio);
return true;
const unifiedCharDefinition = customGlyphDefinitions[c];
if (unifiedCharDefinition) {
switch (unifiedCharDefinition.type) {
case CustomGlyphDefinitionType.SOLID_OCTANT_BLOCK_VECTOR:
drawBlockVectorChar(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
case CustomGlyphDefinitionType.BLOCK_PATTERN:
drawPatternChar(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
case CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION:
drawBlockPatternWithRegion(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
case CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION_AND_SOLID_OCTANT_BLOCK_VECTOR:
drawBlockPatternWithRegion(ctx, unifiedCharDefinition.data.pattern, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
drawBlockVectorChar(ctx, unifiedCharDefinition.data.vectors, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
case CustomGlyphDefinitionType.PATH_FUNCTION:
case CustomGlyphDefinitionType.PATH:
drawPathDefinitionCharacter(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
return true;
case CustomGlyphDefinitionType.VECTOR_SHAPE:
drawVectorShape(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, fontSize, devicePixelRatio);
return true;
case CustomGlyphDefinitionType.PATH_FUNCTION_WITH_WEIGHT:
drawPathDefinitionCharacterWithWeight(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio);
return true;
}
}
return false;
@@ -89,13 +76,13 @@ function drawBlockVectorChar(
function drawPathDefinitionCharacter(
ctx: CanvasRenderingContext2D,
charDefinition: CustomGlyphDrawFunctionDefinition,
charDefinition: CustomGlyphPathDrawFunctionDefinition | string,
xOffset: number,
yOffset: number,
deviceCellWidth: number,
deviceCellHeight: number
): void {
const instructions = charDefinition(0, 0);
const instructions = typeof charDefinition === 'string' ? charDefinition : charDefinition(0, 0);
ctx.beginPath();
for (const instruction of instructions.split(' ')) {
const type = instruction[0];
@@ -186,9 +173,9 @@ function drawPatternChar(
* Draws rectangular shade characters - medium shade pattern clipped to a region.
* Uses a checkerboard pattern that shifts 1px each row (same as medium shade U+2592).
*/
function drawRectangularShadeChar(
function drawBlockPatternWithRegion(
ctx: CanvasRenderingContext2D,
definition: [CustomGlyphPatternDefinition, [number, number, number, number]],
definition: [pattern: CustomGlyphPatternDefinition, region: CustomGlyphRegionDefinition],
xOffset: number,
yOffset: number,
deviceCellWidth: number,
@@ -216,45 +203,6 @@ function drawRectangularShadeChar(
ctx.restore();
}
/**
* Draws block + inverse shade combo characters.
* Fills the solid region completely, then draws inverse medium shade in the shade region.
*/
function drawBlockShadeComboChar(
ctx: CanvasRenderingContext2D,
regions: [[number, number, number, number], [number, number, number, number]],
xOffset: number,
yOffset: number,
deviceCellWidth: number,
deviceCellHeight: number
): void {
const [solidRegion, shadeRegion] = regions;
// Draw solid block region
const solidX = Math.round(xOffset + solidRegion[0] * deviceCellWidth);
const solidY = Math.round(yOffset + solidRegion[1] * deviceCellHeight);
const solidW = Math.round(solidRegion[2] * deviceCellWidth);
const solidH = Math.round(solidRegion[3] * deviceCellHeight);
ctx.fillRect(solidX, solidY, solidW, solidH);
// Draw inverse medium shade region
const shadeX = Math.round(xOffset + shadeRegion[0] * deviceCellWidth);
const shadeY = Math.round(yOffset + shadeRegion[1] * deviceCellHeight);
const shadeW = Math.round(shadeRegion[2] * deviceCellWidth);
const shadeH = Math.round(shadeRegion[3] * deviceCellHeight);
for (let py = 0; py < shadeH; py++) {
const absY = shadeY + py - yOffset;
for (let px = 0; px < shadeW; px++) {
const absX = shadeX + px - xOffset;
// Inverse checkerboard: fill at (x + y) % 2 === 1
if (((absX + absY) % 2) === 1) {
ctx.fillRect(shadeX + px, shadeY + py, 1, 1);
}
}
}
}
/**
* Draws the following box drawing characters by mapping a subset of SVG d attribute instructions to
* canvas draw calls.
@@ -295,7 +243,7 @@ function drawBlockShadeComboChar(
*
* Source: https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html
*/
function drawBoxDrawingChar(
function drawPathDefinitionCharacterWithWeight(
ctx: CanvasRenderingContext2D,
charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) },
xOffset: number,
@@ -334,7 +282,7 @@ function drawBoxDrawingChar(
}
}
function drawPowerlineChar(
function drawVectorShape(
ctx: CanvasRenderingContext2D,
charDefinition: ICustomGlyphVectorShape,
xOffset: number,
+27 -1
View File
@@ -14,7 +14,7 @@ export interface ICustomGlyphSolidOctantBlockVector {
* @param xp The percentage of 15% of the x axis.
* @param yp The percentage of 15% of the x axis on the y axis.
*/
export type CustomGlyphDrawFunctionDefinition = (xp: number, yp: number) => string;
export type CustomGlyphPathDrawFunctionDefinition = (xp: number, yp: number) => string;
export interface ICustomGlyphVectorShape {
d: string;
@@ -29,3 +29,29 @@ export const enum CustomGlyphVectorType {
}
export type CustomGlyphPatternDefinition = number[][];
export const enum CustomGlyphDefinitionType {
SOLID_OCTANT_BLOCK_VECTOR,
BLOCK_PATTERN,
BLOCK_PATTERN_WITH_REGION,
BLOCK_PATTERN_WITH_REGION_AND_SOLID_OCTANT_BLOCK_VECTOR,
PATH_FUNCTION,
PATH_FUNCTION_WITH_WEIGHT,
PATH,
VECTOR_SHAPE,
}
export type CustomGlyphRegionDefinition = [x: number, y: number, w: number, h: number];
export type CustomGlyphCharacterDefinition = (
{ type: CustomGlyphDefinitionType.SOLID_OCTANT_BLOCK_VECTOR, data: ICustomGlyphSolidOctantBlockVector[] } |
{ type: CustomGlyphDefinitionType.BLOCK_PATTERN, data: CustomGlyphPatternDefinition } |
{ type: CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION, data: [pattern: CustomGlyphPatternDefinition, region: CustomGlyphRegionDefinition] } |
// TODO: Consolidate these, draws should be possible via regions/clipping instead of special
// casing
{ type: CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION_AND_SOLID_OCTANT_BLOCK_VECTOR, data: { pattern: [pattern: CustomGlyphPatternDefinition, region: CustomGlyphRegionDefinition], vectors: ICustomGlyphSolidOctantBlockVector[] } } |
{ type: CustomGlyphDefinitionType.PATH_FUNCTION, data: CustomGlyphPathDrawFunctionDefinition } |
{ type: CustomGlyphDefinitionType.PATH_FUNCTION_WITH_WEIGHT, data: { [fontWeight: number]: string | CustomGlyphPathDrawFunctionDefinition } } |
{ type: CustomGlyphDefinitionType.PATH, data: string } |
{ type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: ICustomGlyphVectorShape }
);
+36 -5
View File
@@ -234,8 +234,8 @@ if (document.location.pathname === '/test') {
document.getElementById('create-new-window').addEventListener('click', createNewWindowButtonHandler);
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler);
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
document.getElementById('symbols-for-legacy-computing').addEventListener('click', writeSymbolsForLegacyComputing);
document.getElementById('custom-glyph-alignment').addEventListener('click', customGlyphAlignmentHandler);
document.getElementById('custom-glyph-ranges').addEventListener('click', customGlyphRangesHandler);
document.getElementById('load-test').addEventListener('click', loadTest);
document.getElementById('load-test-long-lines').addEventListener('click', loadTestLongLines);
document.getElementById('print-cjk').addEventListener('click', addCjk);
@@ -786,7 +786,7 @@ function styleAtlasPage(e: HTMLCanvasElement): void {
e.style.height = `${e.height / window.devicePixelRatio}px`;
}
function writeCustomGlyphHandler(): void {
function customGlyphAlignmentHandler(): void {
term.write('\n\r');
term.write('\n\r');
term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r');
@@ -839,10 +839,41 @@ function writeCustomGlyphHandler(): void {
window.scrollTo(0, 0);
}
function writeSymbolsForLegacyComputing(): void {
function customGlyphRangesHandler(): void {
// Box Drawing
// 2500-257F
// https://www.unicode.org/charts/PDF/U2500.pdf
writeUnicodeTable(term, 'Box Drawing', 0x2500, 0x257F, [
['Light and heavy solid lines', 0x2500, 0x2503],
['Light and heavy dashed lines', 0x2504, 0x250B],
['Light and heavy line box components', 0x250C, 0x254B],
['Light and heavy dashed lines', 0x254C, 0x254F],
['Double lines', 0x2550, 0x2551],
['Light and double line box components', 0x2552, 0x256C],
['Character cell arcs', 0x256D, 0x2570],
['Character cell diagonals', 0x2571, 0x2573],
['Light and heavy half lines', 0x2574, 0x257B],
['Mixed light and heavy lines', 0x257C, 0x257F],
]);
// Box Elements
// 2580-259F
// https://www.unicode.org/charts/PDF/U2580.pdf
writeUnicodeTable(term, 'Box Elements', 0x2580, 0x259F, [
['Block elements', 0x2580, 0x2590],
['Shade characters', 0x2591, 0x2593],
['Block elements', 0x2594, 0x2595],
['Terminal graphic characters', 0x2596, 0x259F],
]);
// Powerline Symbols
// Range: E0A0E0BF
// https://github.com/ryanoasis/nerd-fonts
writeUnicodeTable(term, 'Powerline Symbols', 0xE0A0, 0xE0BF, [
['Powerline Symbols', 0xE0A0, 0xE0B3],
['Powerline Extra Symbols', 0xE0B4, 0xE0BF],
]);
// Symbols for Legacy Computing
// Range: 1FB001FBFF
// Source: The Unicode Standard, Version 17.0 https://www.unicode.org/charts/PDF/U1FB00.pdf
// https://www.unicode.org/charts/PDF/U1FB00.pdf
writeUnicodeTable(term, 'Symbols for Legacy Computing', 0x1FB00, 0x1FBFF, [
['Block mosaic terminal graphic characters (Sextants)', 0x1FB00, 0x1FB3B],
['Smooth mosaic terminal graphic characters', 0x1FB3C, 0x1FB6F],
+2 -2
View File
@@ -92,8 +92,8 @@
<dd><button id="print-cjk-sgr" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized SGR attributes">CJK Unified Ideographs (random SGR)</button></dd>
<dt>Styles</dt>
<dd><button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button></dd>
<dd><button id="symbols-for-legacy-computing" title="Write this unicode range to the terminal">Print Symbols for Legacy Computing (Range: 1FB001FBFF)</button></dd>
<dd><button id="custom-glyph-alignment" title="Write custom glyph alignment tests to the terminal">Custom glyph alignment test</button></dd>
<dd><button id="custom-glyph-ranges" title="Write custom glyph unicode range to the terminal">Custom glyph ranges</button></dd>
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
<dd><button id="underline-test" title="Write text with Kitty's extended underline sequences">Underline test</button></dd>
<dd><button id="sgr-test" title="Write text with SGR attribute">SGR test</button></dd>
+11 -10
View File
@@ -6,7 +6,7 @@ export type UnicodeRangeDefinition = [
label: string,
start: number,
end: number,
]
];
/**
* Write a unicode table to the terminal from start to end code points.
@@ -14,29 +14,29 @@ export type UnicodeRangeDefinition = [
* Beware: Vibe coding ahead
*/
export function writeUnicodeTable(term: Terminal, name: string, start: number, end: number, definitions?: UnicodeRangeDefinition[]): void {
function bold(text: string) {
function bold(text: string): string {
return '\x1b[1m' + text + '\x1b[22m';
}
function faint(text: string) {
function faint(text: string): string {
return '\x1b[2m' + text + '\x1b[22m';
}
// Rotating colors: Red, Green, Yellow, Blue, Magenta, Cyan
const colors = [31, 32, 33, 34, 35, 36];
function color(text: string, colorIndex: number) {
function color(text: string, colorIndex: number): string {
const c = colors[colorIndex % colors.length];
return '\x1b[' + c + 'm' + text + '\x1b[39m';
}
term.write('\n\r');
term.write('\n\r');
term.write(bold(name) + '\n\r');
term.write(`${bold(name)} (${start.toString(16).toUpperCase()}-${end.toString(16).toUpperCase()})\n\r`);
term.write('\n\r');
term.write(bold(' 0 1 2 3 4 5 6 7 8 9 A B C D E F') + '\n\r');
const startRow = Math.floor(start / 16);
// Build a map of codepoint -> label and colorIndex for start positions
const labelStartMap = new Map<number, { label: string; colorIndex: number }>();
const labelStartMap = new Map<number, { label: string, colorIndex: number }>();
// Build a map of codepoint -> colorIndex for all codepoints in each range
const codePointColorMap = new Map<number, number>();
let lastDefinitionEnd = start; // Track the last definition's end to stop printing there
@@ -59,7 +59,7 @@ export function writeUnicodeTable(term: Terminal, name: string, start: number, e
for (let row = startRow; row <= endRow; row++) {
// Collect labels for this row with their column positions and color
const rowLabels: { col: number; label: string; colorIndex: number }[] = [];
const rowLabels: { col: number, label: string, colorIndex: number }[] = [];
for (let col = 0; col < 16; col++) {
const codePoint = row * 16 + col;
const labelInfo = labelStartMap.get(codePoint);
@@ -71,7 +71,8 @@ export function writeUnicodeTable(term: Terminal, name: string, start: number, e
// If labels exist and don't start at column 0, first output chars before first label
if (rowLabels.length > 0 && rowLabels[0].col > 0) {
const rowHex = row.toString(16).toUpperCase();
term.write(bold('U+' + rowHex + 'x '));
const rowPrefix = `U+${rowHex}x`.padEnd(8, ' ');
term.write(bold(rowPrefix));
for (let col = 0; col < rowLabels[0].col; col++) {
const codePoint = row * 16 + col;
term.write(' ');
@@ -93,7 +94,7 @@ export function writeUnicodeTable(term: Terminal, name: string, start: number, e
if (rowLabels.length > 0) {
// Render label lines from top to bottom (first label on top, last label closest to row)
for (let i = 0; i < rowLabels.length; i++) {
const prefix = ' '; // Same width as "U+1FB0x "
const prefix = ' '.repeat(8); // Same width as "U+1FB0x "
let line = '';
let visualLen = 0; // Track visual length separately from string length (escape codes don't count)
for (let col = 0; col < 16; col++) {
@@ -117,7 +118,7 @@ export function writeUnicodeTable(term: Terminal, name: string, start: number, e
// Row prefix (e.g., "U+1FB0x ")
const rowHex = row.toString(16).toUpperCase();
const rowPrefix = 'U+' + rowHex + 'x ';
const rowPrefix = `U+${rowHex}x`.padEnd(8, ' ');
const isSecondPrint = rowLabels.length > 0 && rowLabels[0].col > 0;
term.write(isSecondPrint ? ' '.repeat(rowPrefix.length) : bold(rowPrefix));