Skip to content

Commit 86074c9

Browse files
authored
Merge pull request xtermjs#4703 from tisilent/underline-dotted
Use variants for underline dotted.
2 parents 4036ab7 + 2f074b8 commit 86074c9

File tree

11 files changed

+143
-13
lines changed

11 files changed

+143
-13
lines changed

addons/addon-canvas/src/BaseRenderLayer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
5858
protected readonly _coreBrowserService: ICoreBrowserService
5959
) {
6060
super();
61-
this._cellColorResolver = new CellColorResolver(this._terminal, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService);
61+
this._cellColorResolver = new CellColorResolver(this._terminal, this._optionsService, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService);
6262
this._canvas = this._coreBrowserService.mainDocument.createElement('canvas');
6363
this._canvas.classList.add(`xterm-${id}-layer`);
6464
this._canvas.style.zIndex = zIndex.toString();
@@ -365,7 +365,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
365365
*/
366366
protected _drawChars(cell: ICellData, x: number, y: number): void {
367367
const chars = cell.getChars();
368-
this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y);
368+
this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y, this._deviceCellWidth);
369369

370370
if (!this._charAtlas) {
371371
return;

addons/addon-image/src/ImageStorage.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ class ExtendedAttrsImage implements IExtendedAttrsImage {
5353
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
5454
}
5555

56+
public get underlineVariantOffset(): number {
57+
const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29;
58+
if (val < 0) {
59+
return val ^ 0xFFFFFFF8;
60+
}
61+
return val;
62+
}
63+
public set underlineVariantOffset(value: number) {
64+
this._ext &= ~ExtFlags.VARIANT_OFFSET;
65+
this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET;
66+
}
67+
5668
private _urlId: number = 0;
5769
public get urlId(): number {
5870
return this._urlId;

addons/addon-webgl/src/WebglRenderer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
7575

7676
this.register(this._themeService.onChangeColors(() => this._handleColorChange()));
7777

78-
this._cellColorResolver = new CellColorResolver(this._terminal, this._model.selection, this._decorationService, this._coreBrowserService, this._themeService);
78+
this._cellColorResolver = new CellColorResolver(this._terminal, this._optionsService, this._model.selection, this._decorationService, this._coreBrowserService, this._themeService);
7979

8080
this._core = (this._terminal as any)._core;
8181

@@ -442,7 +442,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
442442
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
443443

444444
// Load colors/resolve overrides into work colors
445-
this._cellColorResolver.resolve(cell, x, row);
445+
this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width);
446446

447447
// Override colors for cursor cell
448448
if (isCursorVisible && row === cursorY) {

src/browser/renderer/shared/CellColorResolver.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
22
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
33
import { ReadonlyColorSet } from 'browser/Types';
4-
import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
5-
import { IDecorationService } from 'common/services/Services';
4+
import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants';
5+
import { IDecorationService, IOptionsService } from 'common/services/Services';
66
import { ICellData } from 'common/Types';
77
import { Terminal } from '@xterm/xterm';
88

@@ -13,6 +13,7 @@ let $hasFg = false;
1313
let $hasBg = false;
1414
let $isSelected = false;
1515
let $colors: ReadonlyColorSet | undefined;
16+
let $variantOffset = 0;
1617

1718
export class CellColorResolver {
1819
/**
@@ -27,6 +28,7 @@ export class CellColorResolver {
2728

2829
constructor(
2930
private readonly _terminal: Terminal,
31+
private readonly _optionService: IOptionsService,
3032
private readonly _selectionRenderModel: ISelectionRenderModel,
3133
private readonly _decorationService: IDecorationService,
3234
private readonly _coreBrowserService: ICoreBrowserService,
@@ -38,7 +40,7 @@ export class CellColorResolver {
3840
* Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
3941
* overrides, inverse and selection for the cell which can then be used to feed into the renderer.
4042
*/
41-
public resolve(cell: ICellData, x: number, y: number): void {
43+
public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number): void {
4244
this.result.bg = cell.bg;
4345
this.result.fg = cell.fg;
4446
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
@@ -52,6 +54,13 @@ export class CellColorResolver {
5254
$hasFg = false;
5355
$isSelected = false;
5456
$colors = this._themeService.colors;
57+
$variantOffset = 0;
58+
59+
const code = cell.getCode();
60+
if (code !== NULL_CELL_CODE && cell.extended.underlineStyle === UnderlineStyle.DOTTED) {
61+
const lineWidth = Math.max(1, Math.floor(this._optionService.rawOptions.fontSize * this._coreBrowserService.dpr / 15));
62+
$variantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2);
63+
}
5564

5665
// Apply decorations on the bottom layer
5766
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
@@ -133,5 +142,9 @@ export class CellColorResolver {
133142
// Use the override if it exists
134143
this.result.bg = $hasBg ? $bg : this.result.bg;
135144
this.result.fg = $hasFg ? $fg : this.result.fg;
145+
146+
// Reset overrides variantOffset
147+
this.result.ext &= ~ExtFlags.VARIANT_OFFSET;
148+
this.result.ext |= ($variantOffset << 29) & ExtFlags.VARIANT_OFFSET;
136149
}
137150
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
3+
* @license MIT
4+
*/
5+
6+
import { computeNextVariantOffset } from 'browser/renderer/shared/RendererUtils';
7+
import { assert } from 'chai';
8+
9+
describe('RendererUtils', () => {
10+
it('computeNextVariantOffset', () => {
11+
const cellWidth = 11;
12+
const doubleCellWidth = 22;
13+
let line = 1;
14+
let variantOffset = 0;
15+
16+
// should line 1
17+
// =,_,=_,=_,
18+
let cells = [cellWidth, cellWidth, doubleCellWidth, doubleCellWidth];
19+
let result = [1, 0, 0, 0];
20+
for (let index = 0; index < cells.length; index++) {
21+
const cell = cells[index];
22+
variantOffset = computeNextVariantOffset(cell, line, variantOffset);
23+
assert.equal(variantOffset, result[index]);
24+
}
25+
26+
// should line 2
27+
// ==__==__==_,_==__==__==,__==__==__==__==__==__,==__==__==__==__==__==,
28+
line = 2;
29+
variantOffset = 0;
30+
cells = [cellWidth, cellWidth, doubleCellWidth, doubleCellWidth];
31+
result = [3, 2, 0 ,2];
32+
for (let index = 0; index < cells.length; index++) {
33+
const cell = cells[index];
34+
variantOffset = computeNextVariantOffset(cell, line, variantOffset);
35+
assert.equal(variantOffset, result[index]);
36+
}
37+
38+
// should line 3
39+
// ===___===__,_===___===_,__===___===___===___==,=___===___===___===___,
40+
line = 3;
41+
variantOffset = 0;
42+
cells = [cellWidth, cellWidth, doubleCellWidth, doubleCellWidth];
43+
result = [5, 4, 2, 0];
44+
for (let index = 0; index < cells.length; index++) {
45+
const cell = cells[index];
46+
variantOffset = computeNextVariantOffset(cell, line, variantOffset);
47+
assert.equal(variantOffset, result[index]);
48+
}
49+
});
50+
});

src/browser/renderer/shared/RendererUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,7 @@ function createDimension(): IDimensions {
5656
height: 0
5757
};
5858
}
59+
60+
export function computeNextVariantOffset(cellWidth: number, lineWidth: number, currentOffset: number = 0): number {
61+
return (cellWidth - (Math.round(lineWidth) * 2 - currentOffset)) % (Math.round(lineWidth) * 2);
62+
}

src/browser/renderer/shared/TextureAtlas.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { IColorContrastCache } from 'browser/Types';
77
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
88
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
9-
import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
9+
import { computeNextVariantOffset, excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
1010
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types';
1111
import { NULL_COLOR, color, rgba } from 'common/Color';
1212
import { EventEmitter } from 'common/EventEmitter';
@@ -545,6 +545,7 @@ export class TextureAtlas implements ITextureAtlas {
545545
const yTop = Math.ceil(padding + this._config.deviceCharHeight) - yOffset - (restrictToCellHeight ? lineWidth * 2 : 0);
546546
const yMid = yTop + lineWidth;
547547
const yBot = yTop + lineWidth * 2;
548+
let nextOffset = this._workAttributeData.getUnderlineVariantOffset();
548549

549550
for (let i = 0; i < chWidth; i++) {
550551
this._tmpCtx.save();
@@ -594,9 +595,22 @@ export class TextureAtlas implements ITextureAtlas {
594595
);
595596
break;
596597
case UnderlineStyle.DOTTED:
597-
this._tmpCtx.setLineDash([Math.round(lineWidth), Math.round(lineWidth)]);
598-
this._tmpCtx.moveTo(xChLeft, yTop);
599-
this._tmpCtx.lineTo(xChRight, yTop);
598+
const offsetWidth = nextOffset === 0 ? 0 :
599+
(nextOffset >= lineWidth ? lineWidth * 2 - nextOffset : lineWidth - nextOffset);
600+
// a line and a gap.
601+
const isLineStart = nextOffset >= lineWidth ? false : true;
602+
if (isLineStart === false || offsetWidth === 0) {
603+
this._tmpCtx.setLineDash([Math.round(lineWidth), Math.round(lineWidth)]);
604+
this._tmpCtx.moveTo(xChLeft + offsetWidth, yTop);
605+
this._tmpCtx.lineTo(xChRight, yTop);
606+
} else {
607+
this._tmpCtx.setLineDash([Math.round(lineWidth), Math.round(lineWidth)]);
608+
this._tmpCtx.moveTo(xChLeft, yTop);
609+
this._tmpCtx.lineTo(xChLeft + offsetWidth, yTop);
610+
this._tmpCtx.moveTo(xChLeft + offsetWidth + lineWidth, yTop);
611+
this._tmpCtx.lineTo(xChRight, yTop);
612+
}
613+
nextOffset = computeNextVariantOffset(xChRight - xChLeft, lineWidth, nextOffset);
600614
break;
601615
case UnderlineStyle.DASHED:
602616
this._tmpCtx.setLineDash([this._config.devicePixelRatio * 4, this._config.devicePixelRatio * 3]);

src/common/Types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export interface IExtendedAttrs {
119119
ext: number;
120120
underlineStyle: UnderlineStyle;
121121
underlineColor: number;
122+
underlineVariantOffset: number;
122123
urlId: number;
123124
clone(): IExtendedAttrs;
124125
isEmpty(): boolean;
@@ -209,6 +210,7 @@ export interface IAttributeData {
209210
isUnderlineColorPalette(): boolean;
210211
isUnderlineColorDefault(): boolean;
211212
getUnderlineStyle(): number;
213+
getUnderlineVariantOffset(): number;
212214
}
213215

214216
/** Cell data */

src/common/buffer/AttributeData.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ export class AttributeData implements IAttributeData {
126126
? (this.bg & BgFlags.HAS_EXTENDED ? this.extended.underlineStyle : UnderlineStyle.SINGLE)
127127
: UnderlineStyle.NONE;
128128
}
129+
public getUnderlineVariantOffset(): number {
130+
return this.extended.underlineVariantOffset;
131+
}
129132
}
130133

131134

@@ -174,6 +177,18 @@ export class ExtendedAttrs implements IExtendedAttrs {
174177
this._urlId = value;
175178
}
176179

180+
public get underlineVariantOffset(): number {
181+
const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29;
182+
if (val < 0) {
183+
return val ^ 0xFFFFFFF8;
184+
}
185+
return val;
186+
}
187+
public set underlineVariantOffset(value: number) {
188+
this._ext &= ~ExtFlags.VARIANT_OFFSET;
189+
this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET;
190+
}
191+
177192
constructor(
178193
ext: number = 0,
179194
urlId: number = 0

src/common/buffer/BufferLine.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ describe('AttributeData', () => {
119119
attrs.fg &= ~FgFlags.UNDERLINE;
120120
assert.equal(attrs.getUnderlineStyle(), UnderlineStyle.NONE);
121121
});
122+
it('getUnderlineVariantOffset', () => {
123+
const attrs = new AttributeData();
124+
125+
// defaults to no offset
126+
assert.equal(attrs.getUnderlineVariantOffset(), 0);
127+
128+
// should return 0 - 7
129+
for (let i = 0; i < 8; ++i) {
130+
attrs.extended.underlineVariantOffset = i;
131+
assert.equal(attrs.getUnderlineVariantOffset(), i);
132+
}
133+
});
122134
});
123135
});
124136

0 commit comments

Comments
 (0)