Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vivid curly #4880

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions addons/addon-canvas/src/BaseRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,9 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer

let glyph: IRasterizedGlyph;
if (chars && chars.length > 1) {
glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, true);
glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, this._cellColorResolver.result.underlineVariantOffset, true);
} else {
glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, true);
glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, this._cellColorResolver.result.underlineVariantOffset, true);
}
if (!glyph.size.x || !glyph.size.y) {
return;
Expand Down
10 changes: 5 additions & 5 deletions addons/addon-webgl/src/GlyphRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,15 +212,15 @@ export class GlyphRenderer extends Disposable {
return this._atlas ? this._atlas.beginFrame() : true;
}

public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number, underlineVariantOffset: number): void {
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
// should not instantiate any variables unless a new glyph is drawn to the cache where the
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
// each glyph.
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg);
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg, underlineVariantOffset);
}

private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number, underlineVariantOffset: number): void {
$i = (y * this._terminal.cols + x) * INDICES_PER_CELL;

// Exit early if this is a null character, allow space character to continue as it may have
Expand All @@ -236,9 +236,9 @@ export class GlyphRenderer extends Disposable {

// Get the glyph
if (chars && chars.length > 1) {
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, false);
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, underlineVariantOffset, false);
} else {
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false);
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, underlineVariantOffset, false);
}

$leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2);
Expand Down
4 changes: 2 additions & 2 deletions addons/addon-webgl/src/WebglRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;

this._glyphRenderer.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
this._glyphRenderer.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg, this._cellColorResolver.result.underlineVariantOffset);

if (isJoined) {
// Restore work cell
Expand All @@ -505,7 +505,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Null out non-first cells
for (x++; x < lastCharX; x++) {
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0, 0);
this._model.cells[j] = NULL_CELL_CODE;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
Expand Down
29 changes: 20 additions & 9 deletions src/browser/renderer/shared/CellColorResolver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ReadonlyColorSet } from 'browser/Types';
import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants';
import { Attributes, BgFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants';
import { IDecorationService, IOptionsService } from 'common/services/Services';
import { ICellData } from 'common/Types';
import { Terminal } from '@xterm/xterm';
import { getCurlyVariantOffset } from 'browser/renderer/shared/RendererUtils';

// Work variables to avoid garbage collection
let $fg = 0;
Expand All @@ -13,17 +14,19 @@ let $hasFg = false;
let $hasBg = false;
let $isSelected = false;
let $colors: ReadonlyColorSet | undefined;
let $variantOffset = 0;
// let $variantOffset = 0;
let $underlineVariantOffset = 0;

export class CellColorResolver {
/**
* The shared result of the {@link resolve} call. This is only safe to use immediately after as
* any other calls will share object.
*/
public readonly result: { fg: number, bg: number, ext: number } = {
public readonly result: { fg: number, bg: number, ext: number, underlineVariantOffset: number } = {
fg: 0,
bg: 0,
ext: 0
ext: 0,
underlineVariantOffset: 0
};

constructor(
Expand Down Expand Up @@ -54,12 +57,19 @@ export class CellColorResolver {
$hasFg = false;
$isSelected = false;
$colors = this._themeService.colors;
$variantOffset = 0;
// $variantOffset = 0;
$underlineVariantOffset = 0;

const code = cell.getCode();
if (code !== NULL_CELL_CODE && cell.extended.underlineStyle === UnderlineStyle.DOTTED) {

// Underline handle
if (code !== NULL_CELL_CODE && cell.extended.underlineStyle !== UnderlineStyle.NONE) {
const lineWidth = Math.max(1, Math.floor(this._optionService.rawOptions.fontSize * this._coreBrowserService.dpr / 15));
$variantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2);
if (cell.extended.underlineStyle === UnderlineStyle.DOTTED) {
$underlineVariantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2);
} else if (cell.extended.underlineStyle === UnderlineStyle.CURLY) {
$underlineVariantOffset = getCurlyVariantOffset(x, deviceCellWidth, lineWidth);
}
}

// Apply decorations on the bottom layer
Expand Down Expand Up @@ -144,7 +154,8 @@ export class CellColorResolver {
this.result.fg = $hasFg ? $fg : this.result.fg;

// Reset overrides variantOffset
this.result.ext &= ~ExtFlags.VARIANT_OFFSET;
this.result.ext |= ($variantOffset << 29) & ExtFlags.VARIANT_OFFSET;
// this.result.ext &= ~ExtFlags.VARIANT_OFFSET;
// this.result.ext |= ($variantOffset << 29) & ExtFlags.VARIANT_OFFSET;
this.result.underlineVariantOffset = $underlineVariantOffset;
}
}
4 changes: 2 additions & 2 deletions src/browser/renderer/shared/RendererUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @license MIT
*/

import { computeNextVariantOffset } from 'browser/renderer/shared/RendererUtils';
import { computeNextVariantOffset, createDrawCurlyPlan } from 'browser/renderer/shared/RendererUtils';
import { assert } from 'chai';

describe('RendererUtils', () => {
Expand All @@ -28,7 +28,7 @@ describe('RendererUtils', () => {
line = 2;
variantOffset = 0;
cells = [cellWidth, cellWidth, doubleCellWidth, doubleCellWidth];
result = [3, 2, 0 ,2];
result = [3, 2, 0, 2];
for (let index = 0; index < cells.length; index++) {
const cell = cells[index];
variantOffset = computeNextVariantOffset(cell, line, variantOffset);
Expand Down
104 changes: 103 additions & 1 deletion src/browser/renderer/shared/RendererUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* @license MIT
*/

import { IDimensions, IRenderDimensions } from 'browser/renderer/shared/Types';
import { IDimensions, IRenderDimensions, UnderlineCurlyJoinOrLine, UnderlineCurlyLineType, UnderlineDrawCurlyOp } from 'browser/renderer/shared/Types';
import { TwoKeyMap } from 'common/MultiKeyMap';

export function throwIfFalsy<T>(value: T | undefined | null): T {
if (!value) {
Expand Down Expand Up @@ -60,3 +61,104 @@ function createDimension(): IDimensions {
export function computeNextVariantOffset(cellWidth: number, lineWidth: number, currentOffset: number = 0): number {
return (cellWidth - (Math.round(lineWidth) * 2 - currentOffset)) % (Math.round(lineWidth) * 2);
}

// TwoKeyMap
const curlyVariantCache = new TwoKeyMap<number, number, string[]>();

export function getCurlyVariant(cellWidth: number, lineWidth: number, offset: number): string {
if (curlyVariantCache.get(cellWidth, lineWidth)) {
const curlyVariants = curlyVariantCache.get(cellWidth, lineWidth);
if (curlyVariants && curlyVariants.length > 0) {
if (!curlyVariants[offset]) {
return curlyVariants[0];
}
return curlyVariants[offset];
}
}
return '';
}

export function getCurlyVariantOffset(x: number, cellWidth: number, lineWidth: number): number {
if (curlyVariantCache.get(cellWidth, lineWidth)) {
const curlyVariants = curlyVariantCache.get(cellWidth, lineWidth) as any[];
return x % curlyVariants.length;
}
if (!curlyVariantCache.get(cellWidth, lineWidth)) {
const curlyVariants = createDrawCurlyPlan(cellWidth, lineWidth);
curlyVariantCache.set(cellWidth, lineWidth, curlyVariants);
return x % curlyVariants.length;
}
return 0;
}

const defaultCurlyLinePixels = 3;

export function createDrawCurlyPlan(cellWidth: number, lineWidth: number): string[] {
return createVariantSequences(cellWidth, lineWidth, defaultCurlyLinePixels + (lineWidth > 1 ? 1 : 0));
}

function createVariantSequences(cellWidth: number, joinPixels: number, linePixels: number): string[] {
const result: string[] = [];
let totalPixels = cellWidth * ((joinPixels + linePixels) * 2);
let joinOrLine: UnderlineCurlyJoinOrLine = 'join';
let upOrDown: UnderlineCurlyLineType = 'up';
let lastUpOrDown: UnderlineCurlyLineType = 'up';
// Split between cells to be processed
let waitHandlePixels = 0;
while (totalPixels > 0) {
const cellResult: any[] = [];
let cellCurrentWidth = cellWidth;
while (cellCurrentWidth > 0) {
if (joinOrLine === 'join') {
let token: UnderlineDrawCurlyOp = upOrDown === 'up' ? 'Y' : 'B';
if (waitHandlePixels > 0) {
// right
token = lastUpOrDown === 'up' ? 'M' : 'P';
cellResult.push(`${token}${waitHandlePixels}`);
cellCurrentWidth -= waitHandlePixels;
waitHandlePixels = 0;
joinOrLine = 'line';
} else {
// left
const usingWidth = joinPixels;
if (usingWidth > cellCurrentWidth) {
token = lastUpOrDown === 'up' ? 'Z' : 'Q';
cellResult.push(`${token}${cellCurrentWidth}`);
waitHandlePixels = usingWidth - cellCurrentWidth;
cellCurrentWidth = 0;
} else {
cellResult.push(`${token}${joinPixels}`);
cellCurrentWidth -= joinPixels;
joinOrLine = 'line';
}
}
} else if (joinOrLine === 'line') {
const token: UnderlineDrawCurlyOp = upOrDown === 'up' ? 'U' : 'D';
if (waitHandlePixels > 0) {
cellResult.push(`${token}${waitHandlePixels}`);
cellCurrentWidth -= waitHandlePixels;
waitHandlePixels = 0;
joinOrLine = 'join';
lastUpOrDown = upOrDown;
upOrDown = upOrDown === 'up' ? 'down' : 'up';
} else {
const usingWidth = linePixels;
if (usingWidth > cellCurrentWidth) {
cellResult.push(`${token}${cellCurrentWidth}`);
waitHandlePixels = usingWidth - cellCurrentWidth;
cellCurrentWidth = 0;
} else {
cellResult.push(`${token}${linePixels}`);
cellCurrentWidth -= linePixels;
joinOrLine = 'join';
lastUpOrDown = upOrDown;
upOrDown = upOrDown === 'up' ? 'down' : 'up';
}
}
}
}
totalPixels -= cellWidth;
result.push(cellResult.join(' '));
}
return result;
}
Loading