Skip to content

Commit

Permalink
Merge pull request #4077 from Tyriar/4063
Browse files Browse the repository at this point in the history
Take char width into account when drawing underlines/strokethrough
  • Loading branch information
Tyriar committed Aug 27, 2022
2 parents 2526409 + 4d837e2 commit 3105797
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 64 deletions.
5 changes: 3 additions & 2 deletions addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { generateConfig, configEquals } from './CharAtlasUtils';
import { WebglCharAtlas } from './WebglCharAtlas';
import { ICharAtlasConfig } from './Types';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IColorSet, ITerminal } from 'browser/Types';

interface ICharAtlasCacheEntry {
atlas: WebglCharAtlas;
Expand Down Expand Up @@ -64,8 +64,9 @@ export function acquireCharAtlas(
}
}

const core: ITerminal = (terminal as any)._core;
const newEntry: ICharAtlasCacheEntry = {
atlas: new WebglCharAtlas(document, newConfig),
atlas: new WebglCharAtlas(document, newConfig, core.unicodeService),
config: newConfig,
ownedBy: [terminal]
};
Expand Down
138 changes: 77 additions & 61 deletions addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AttributeData } from 'common/buffer/AttributeData';
import { color, rgba } from 'common/Color';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils';
import { IUnicodeService } from 'common/services/Services';

// For debugging purposes, it can be useful to set this to a really tiny value,
// to verify that LRU eviction works.
Expand Down Expand Up @@ -89,7 +90,8 @@ export class WebglCharAtlas implements IDisposable {

constructor(
document: Document,
private _config: ICharAtlasConfig
private readonly _config: ICharAtlasConfig,
private readonly _unicodeService: IUnicodeService
) {
this.cacheCanvas = document.createElement('canvas');
this.cacheCanvas.width = TEXTURE_WIDTH;
Expand Down Expand Up @@ -436,6 +438,13 @@ export class WebglCharAtlas implements IDisposable {
// underline colors to prevent important colors could get cleared.
let enableClearThresholdCheck = !powerlineGlyph;

let chWidth: number;
if (typeof codeOrChars === 'number') {
chWidth = this._unicodeService.wcwidth(codeOrChars);
} else {
chWidth = this._unicodeService.getStringCellWidth(codeOrChars);
}

// Draw underline
if (underline) {
this._tmpCtx.save();
Expand All @@ -462,69 +471,76 @@ export class WebglCharAtlas implements IDisposable {
// Underline style/stroke
this._tmpCtx.beginPath();
const xLeft = padding;
const xRight = padding + this._config.scaledCellWidth;
const xRight = padding + this._config.scaledCellWidth * chWidth;
const yTop = Math.ceil(padding + this._config.scaledCharHeight) - yOffset;
const yMid = padding + this._config.scaledCharHeight + lineWidth - yOffset;
const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth * 2) - yOffset;
switch (this._workAttributeData.extended.underlineStyle) {
case UnderlineStyle.DOUBLE:
this._tmpCtx.moveTo(xLeft, yTop);
this._tmpCtx.lineTo(xRight, yTop);
this._tmpCtx.moveTo(xLeft, yBot);
this._tmpCtx.lineTo(xRight, yBot);
break;
case UnderlineStyle.CURLY:
const xMid = padding + this._config.scaledCellWidth / 2;
// Choose the bezier top and bottom based on the device pixel ratio, the curly line is
// made taller when the line width is as otherwise it's not very clear otherwise.
const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset;
const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset;
// Clip the left and right edges of the underline such that it can be drawn just outside
// the edge of the cell to ensure a continuous stroke when there are multiple underlined
// glyphs adjacent to one another.
const clipRegion = new Path2D();
clipRegion.rect(xLeft, yTop, this._config.scaledCellWidth, yBot - yTop);
this._tmpCtx.clip(clipRegion);
// Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells
this._tmpCtx.moveTo(xLeft - this._config.scaledCellWidth / 2, yMid);
this._tmpCtx.bezierCurveTo(
xLeft - this._config.scaledCellWidth / 2, yCurlyTop,
xLeft, yCurlyTop,
xLeft, yMid
);
this._tmpCtx.bezierCurveTo(
xLeft, yCurlyBot,
xMid, yCurlyBot,
xMid, yMid
);
this._tmpCtx.bezierCurveTo(
xMid, yCurlyTop,
xRight, yCurlyTop,
xRight, yMid
);
this._tmpCtx.bezierCurveTo(
xRight, yCurlyBot,
xRight + this._config.scaledCellWidth / 2, yCurlyBot,
xRight + this._config.scaledCellWidth / 2, yMid
);
break;
case UnderlineStyle.DOTTED:
this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]);
this._tmpCtx.moveTo(xLeft, yTop);
this._tmpCtx.lineTo(xRight, yTop);
break;
case UnderlineStyle.DASHED:
this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]);
this._tmpCtx.moveTo(xLeft, yTop);
this._tmpCtx.lineTo(xRight, yTop);
break;
case UnderlineStyle.SINGLE:
default:
this._tmpCtx.moveTo(xLeft, yTop);
this._tmpCtx.lineTo(xRight, yTop);
break;

for (let i = 0; i < chWidth; i++) {
this._tmpCtx.save();
const xChLeft = xLeft + i * this._config.scaledCellWidth;
const xChRight = xLeft + (i + 1) * this._config.scaledCellWidth;
const xChMid = xChLeft + this._config.scaledCellWidth / 2;
switch (this._workAttributeData.extended.underlineStyle) {
case UnderlineStyle.DOUBLE:
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
this._tmpCtx.moveTo(xChLeft, yBot);
this._tmpCtx.lineTo(xChRight, yBot);
break;
case UnderlineStyle.CURLY:
// Choose the bezier top and bottom based on the device pixel ratio, the curly line is
// made taller when the line width is as otherwise it's not very clear otherwise.
const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset;
const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset;
// Clip the left and right edges of the underline such that it can be drawn just outside
// the edge of the cell to ensure a continuous stroke when there are multiple underlined
// glyphs adjacent to one another.
const clipRegion = new Path2D();
clipRegion.rect(xChLeft, yTop, this._config.scaledCellWidth, yBot - yTop);
this._tmpCtx.clip(clipRegion);
// Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells
this._tmpCtx.moveTo(xChLeft - this._config.scaledCellWidth / 2, yMid);
this._tmpCtx.bezierCurveTo(
xChLeft - this._config.scaledCellWidth / 2, yCurlyTop,
xChLeft, yCurlyTop,
xChLeft, yMid
);
this._tmpCtx.bezierCurveTo(
xChLeft, yCurlyBot,
xChMid, yCurlyBot,
xChMid, yMid
);
this._tmpCtx.bezierCurveTo(
xChMid, yCurlyTop,
xChRight, yCurlyTop,
xChRight, yMid
);
this._tmpCtx.bezierCurveTo(
xChRight, yCurlyBot,
xChRight + this._config.scaledCellWidth / 2, yCurlyBot,
xChRight + this._config.scaledCellWidth / 2, yMid
);
break;
case UnderlineStyle.DOTTED:
this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]);
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
break;
case UnderlineStyle.DASHED:
this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]);
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
break;
case UnderlineStyle.SINGLE:
default:
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
break;
}
this._tmpCtx.stroke();
this._tmpCtx.restore();
}
this._tmpCtx.stroke();
this._tmpCtx.restore();

// Draw stroke in the background color for non custom characters in order to give an outline
Expand Down Expand Up @@ -590,7 +606,7 @@ export class WebglCharAtlas implements IDisposable {
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
this._tmpCtx.beginPath();
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth * chWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
this._tmpCtx.stroke();
}

Expand Down
2 changes: 1 addition & 1 deletion demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ function underlineTest() {
for (let i = 0; i < 10; i++) {
numbers += i.toString();
}
return `${u(id)}4:${id}m - ${name}\x1b[4:0m`.padEnd(33, ' ') + `${u(id)}${alphabet} ${numbers}\x1b[4:0m`;
return `${u(id)}4:${id}m - ${name}\x1b[4:0m`.padEnd(33, ' ') + `${u(id)}${alphabet} ${numbers} 汉语 한국어 👽\x1b[4:0m`;
}
term.writeln(showSequence(0, 'No underline'));
term.writeln(showSequence(1, 'Straight'));
Expand Down

0 comments on commit 3105797

Please sign in to comment.