diff --git a/addons/addon-webgl/src/CellColorResolver.ts b/addons/addon-webgl/src/CellColorResolver.ts index 6f61a704..3a4661cc 100644 --- a/addons/addon-webgl/src/CellColorResolver.ts +++ b/addons/addon-webgl/src/CellColorResolver.ts @@ -7,6 +7,7 @@ import { ICellData } from 'common/Types'; import { Terminal } from '@xterm/xterm'; import { rgba } from 'common/Color'; import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils'; +import { blockPatternCodepoints } from './customGlyphs/CustomGlyphDefinitions'; // Work variables to avoid garbage collection let $fg = 0; @@ -42,7 +43,7 @@ export class CellColorResolver { * Resolves colors for the cell, putting the result into the shared {@link result}. This resolves * overrides, inverse and selection for the cell which can then be used to feed into the renderer. */ - public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number): void { + public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number, deviceCellHeight: number): void { this.result.bg = cell.bg; this.result.fg = cell.fg; this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0; @@ -63,7 +64,9 @@ export class CellColorResolver { const lineWidth = Math.max(1, Math.floor(this._optionService.rawOptions.fontSize * this._coreBrowserService.dpr / 15)); $variantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2); } - + if ($variantOffset === 0 && blockPatternCodepoints.has(code)) { + $variantOffset = ((x * deviceCellWidth) % 2) * 2 + ((y * deviceCellHeight) % 2); + } // Apply decorations on the bottom layer this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => { if (d.backgroundColorRGB) { diff --git a/addons/addon-webgl/src/TextureAtlas.ts b/addons/addon-webgl/src/TextureAtlas.ts index 97046ba0..5629508f 100644 --- a/addons/addon-webgl/src/TextureAtlas.ts +++ b/addons/addon-webgl/src/TextureAtlas.ts @@ -526,7 +526,8 @@ export class TextureAtlas implements ITextureAtlas { // Draw custom characters if applicable let customGlyph = false; if (this._config.customGlyphs !== false) { - customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css); + const variantOffset = this._workAttributeData.getUnderlineVariantOffset(); + customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css, variantOffset); } // Whether to clear pixels based on a threshold difference between the glyph color and the diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index c2f873c8..e985bf2e 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -500,7 +500,7 @@ export class WebglRenderer extends Disposable implements IRenderer { } // Load colors/resolve overrides into work colors - this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width); + this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width, this.dimensions.device.cell.height); // Override colors for cursor cell if (isCursorVisible && row === cursorY) { diff --git a/addons/addon-webgl/src/customGlyphs/CustomGlyphDefinitions.ts b/addons/addon-webgl/src/customGlyphs/CustomGlyphDefinitions.ts index 79efc306..75e53930 100644 --- a/addons/addon-webgl/src/customGlyphs/CustomGlyphDefinitions.ts +++ b/addons/addon-webgl/src/customGlyphs/CustomGlyphDefinitions.ts @@ -820,6 +820,27 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi // #endregion }; +export const blockPatternCodepoints = new Set([ + // Shade characters (2591-2593) + 0x2591, + 0x2592, + 0x2593, + // Rectangular shade characters (1FB8C-1FB94) + 0x1FB8C, + 0x1FB8D, + 0x1FB8E, + 0x1FB8F, + 0x1FB90, + 0x1FB91, + 0x1FB92, + 0x1FB94, + // Triangular shade characters (1FB9C-1FB9F) + 0x1FB9C, + 0x1FB9D, + 0x1FB9E, + 0x1FB9F +]); + /** * Generates a drawing function for sextant characters. Sextants are a 2x3 grid where each cell * can be on or off. diff --git a/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts b/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts index 6302757f..08928a96 100644 --- a/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts +++ b/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts @@ -22,14 +22,15 @@ export function tryDrawCustomGlyph( deviceCharHeight: number, fontSize: number, devicePixelRatio: number, - backgroundColor?: string + backgroundColor?: string, + variantOffset: number = 0 ): boolean { const unifiedCharDefinition = customGlyphDefinitions[c]; if (unifiedCharDefinition) { // Normalize to array for uniform handling const parts = Array.isArray(unifiedCharDefinition) ? unifiedCharDefinition : [unifiedCharDefinition]; for (const part of parts) { - drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, backgroundColor); + drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, backgroundColor, variantOffset); } return true; } @@ -48,7 +49,8 @@ function drawDefinitionPart( deviceCharHeight: number, fontSize: number, devicePixelRatio: number, - backgroundColor?: string + backgroundColor?: string, + variantOffset: number = 0 ): void { // Handle scaleType - adjust dimensions and offset when scaling to character area let drawWidth = deviceCellWidth; @@ -74,7 +76,7 @@ function drawDefinitionPart( drawBlockVectorChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight); break; case CustomGlyphDefinitionType.BLOCK_PATTERN: - drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight); + drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, variantOffset); break; case CustomGlyphDefinitionType.PATH_FUNCTION: drawPathFunctionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, part.strokeWidth); @@ -437,7 +439,8 @@ function drawPatternChar( xOffset: number, yOffset: number, deviceCellWidth: number, - deviceCellHeight: number + deviceCellHeight: number, + variantOffset: number = 0 ): void { let patternSet = cachedPatterns.get(charDefinition); if (!patternSet) { @@ -486,6 +489,15 @@ function drawPatternChar( pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null)); patternSet.set(fillStyle, pattern); } + // Apply pattern offset to ensure seamless tiling across cells when cell dimensions are odd. + // variantOffset encodes: bit 1 = x pixel shift, bit 0 = y pixel shift. + const dx = (variantOffset >> 1) & 1; + const dy = variantOffset & 1; + if (dx !== 0 || dy !== 0) { + pattern.setTransform(new DOMMatrix().translateSelf(-dx, -dy)); + } else { + pattern.setTransform(new DOMMatrix()); + } ctx.fillStyle = pattern; ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight); } diff --git a/demo/client/components/window/testWindow.ts b/demo/client/components/window/testWindow.ts index acab7b3a..a67fef16 100644 --- a/demo/client/components/window/testWindow.ts +++ b/demo/client/components/window/testWindow.ts @@ -397,6 +397,13 @@ function customGlyphAlignmentHandler(term: Terminal): void { } } + term.write('\x1b[0mTriangular fill tests:\x1b[36m\n\r'); + term.write('1FB9C 1FB9D 1FB9E 1FB9F all\n\r'); + term.write('\u{02592}\u{02592}\u{02592}\u{1FB9C} \u{1FB9D}\u{02592}\u{02592}\u{02592} \u{00020}\u{00020}\u{00020}\u{1FB9E} \u{1FB9F}\u{00020}\u{00020}\u{00020} \u{00020}\u{1FB9E}\u{1FB9F}\u{00020}\n\r'); + term.write('\u{02592}\u{02592}\u{1FB9C}\u{00020} \u{00020}\u{1FB9D}\u{02592}\u{02592} \u{00020}\u{00020}\u{1FB9E}\u{02592} \u{02592}\u{1FB9F}\u{00020}\u{00020} \u{1FB9E}\u{02592}\u{02592}\u{1FB9F}\n\r'); + term.write('\u{02592}\u{1FB9C}\u{00020}\u{00020} \u{00020}\u{00020}\u{1FB9D}\u{02592} \u{00020}\u{1FB9E}\u{02592}\u{02592} \u{02592}\u{02592}\u{1FB9F}\u{00020} \u{1FB9D}\u{02592}\u{02592}\u{1FB9C}\n\r'); + term.write('\u{1FB9C}\u{00020}\u{00020}\u{00020} \u{00020}\u{00020}\u{00020}\u{1FB9D} \u{1FB9E}\u{02592}\u{02592}\u{02592} \u{02592}\u{02592}\u{02592}\u{1FB9F} \u{00020}\u{1FB9D}\u{1FB9C}\u{00020}\n\r'); + term.write('\x1b[0mPowerline alignment tests:\n\r'); const powerlineLeftChars = ['\u{E0B2}', '\u{E0B3}', '\u{E0B6}', '\u{E0B7}', '\u{E0BA}', '\u{E0BB}', '\u{E0BE}', '\u{E0BF}', '\u{E0C2}', '\u{E0C3}', '\u{E0C5}', '\u{E0C7}', '\u{E0CA}', '\u{E0D4}']; const powerlineRightChars = ['\u{E0B0}', '\u{E0B1}', '\u{E0B4}', '\u{E0B5}', '\u{E0B8}', '\u{E0B9}', '\u{E0BC}', '\u{E0BD}', '\u{E0C0}', '\u{E0C1}', '\u{E0C4}', '\u{E0C6}', '\u{E0C8}', '\u{E0D2}', '\u{E0CC}', '\u{E0CD}', '\u{E0CE}', '\u{E0CF}', '\u{E0D0}', '\u{E0D1}'];