Merge pull request #968 from Tyriar/line_height_fixes

Fix several issues with line height
This commit is contained in:
Daniel Imms
2017-09-11 09:42:06 -07:00
committed by GitHub
14 changed files with 130 additions and 169 deletions
+2 -1
View File
@@ -5,7 +5,7 @@
import { ILinkMatcherOptions } from './Interfaces';
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
import { IColorSet } from './renderer/Interfaces';
import { IColorSet, IRenderer } from './renderer/Interfaces';
import { IMouseZoneManager } from './input/Interfaces';
export interface IBrowser {
@@ -36,6 +36,7 @@ export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElement
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
renderer: IRenderer;
rows: number;
cols: number;
browser: IBrowser;
+6 -3
View File
@@ -191,7 +191,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
private inputHandler: InputHandler;
private parser: Parser;
private renderer: IRenderer;
public renderer: IRenderer;
public selectionManager: SelectionManager;
public linkifier: ILinkifier;
public buffers: BufferSet;
@@ -623,7 +623,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.charMeasure = new CharMeasure(document, this.helperContainer);
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
this.charMeasure.on('charsizechanged', () => this.viewport.syncScrollArea());
this.renderer = new Renderer(this);
this.on('cursormove', () => this.renderer.onCursorMove());
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
@@ -631,6 +630,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.on('focus', () => this.renderer.onFocus());
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true));
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
this.selectionManager = new SelectionManager(this, this.buffer, this.charMeasure);
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
@@ -643,7 +643,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.textarea.focus();
this.textarea.select();
});
this.on('scroll', () => this.selectionManager.refresh());
this.on('scroll', () => {
this.viewport.syncScrollArea();
this.selectionManager.refresh();
});
this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
// Measure the character size
-90
View File
@@ -1,90 +0,0 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { Viewport } from './Viewport';
import { BufferSet } from './BufferSet';
describe('Viewport', () => {
let terminal;
let viewportElement;
let charMeasure;
let viewport;
let scrollAreaElement;
const CHARACTER_HEIGHT = 10;
beforeEach(() => {
terminal = {
rows: 0,
ydisp: 0,
on: () => {},
rowContainer: {
style: {
lineHeight: 0
}
},
selectionContainer: {
style: {
height: 0
}
},
options: {
scrollback: 10,
lineHeight: 1
}
};
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
viewportElement = {
addEventListener: () => {},
style: {
height: 0,
lineHeight: 0
}
};
scrollAreaElement = {
style: {
height: 0
}
};
charMeasure = {
height: CHARACTER_HEIGHT
};
viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasure);
});
describe('refresh', () => {
it('should set the height of the viewport when the line-height changed', () => {
terminal.buffer.lines.push('');
terminal.buffer.lines.push('');
terminal.rows = 1;
viewport.refresh();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
charMeasure.height = 2 * CHARACTER_HEIGHT;
viewport.refresh();
assert.equal(viewportElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
});
});
describe('syncScrollArea', () => {
it('should sync the scroll area', done => {
// Allow CharMeasure to be initialized
setTimeout(() => {
terminal.buffer.lines.push('');
terminal.rows = 1;
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
viewport.syncScrollArea();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
terminal.buffer.lines.push('');
viewport.syncScrollArea();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
done();
}, 0);
});
});
});
+16 -21
View File
@@ -12,9 +12,10 @@ import { IColorSet } from './renderer/Interfaces';
* Logic for the virtual scroll bar is included in this object.
*/
export class Viewport implements IViewport {
private currentRowHeight: number;
private lastRecordedBufferLength: number;
private lastRecordedViewportHeight: number;
private currentRowHeight: number = 0;
private lastRecordedBufferLength: number = 0;
private lastRecordedViewportHeight: number = 0;
private lastRecordedBufferHeight: number = 0;
private lastTouchY: number;
/**
@@ -30,12 +31,6 @@ export class Viewport implements IViewport {
private scrollArea: HTMLElement,
private charMeasure: CharMeasure
) {
this.currentRowHeight = 0;
this.lastRecordedBufferLength = 0;
this.lastRecordedViewportHeight = 0;
this.terminal.on('scroll', this.syncScrollArea.bind(this));
this.terminal.on('resize', this.syncScrollArea.bind(this));
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
// Perform this async to ensure the CharMeasure is ready.
@@ -52,18 +47,18 @@ export class Viewport implements IViewport {
*/
private refresh(): void {
if (this.charMeasure.height > 0) {
const lineHeight = Math.ceil(this.charMeasure.height * this.terminal.options.lineHeight);
const rowHeightChanged = lineHeight !== this.currentRowHeight;
if (rowHeightChanged) {
this.currentRowHeight = lineHeight;
this.viewportElement.style.lineHeight = lineHeight + 'px';
this.currentRowHeight = this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio;
if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) {
this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight;
this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px';
}
const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows;
if (rowHeightChanged || viewportHeightChanged) {
this.lastRecordedViewportHeight = this.terminal.rows;
this.viewportElement.style.height = lineHeight * this.terminal.rows + 'px';
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength);
if (this.lastRecordedBufferHeight !== newBufferHeight) {
this.lastRecordedBufferHeight = newBufferHeight;
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
}
this.scrollArea.style.height = (lineHeight * this.lastRecordedBufferLength) + 'px';
}
}
@@ -75,12 +70,12 @@ export class Viewport implements IViewport {
// If buffer height changed
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
this.refresh();
} else if (this.lastRecordedViewportHeight !== this.terminal.rows) {
} else if (this.lastRecordedViewportHeight !== (<any>this.terminal).renderer.dimensions.canvasHeight) {
// If viewport height changed
this.refresh();
} else {
// If size has changed, refresh viewport
if (Math.ceil(this.charMeasure.height * this.terminal.options.lineHeight) !== this.currentRowHeight) {
if (this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio !== this.currentRowHeight) {
this.refresh();
}
}
+1 -1
View File
@@ -47,7 +47,7 @@
var availableWidth = parentElementWidth - elementPaddingHor;
var geometry = {
cols: Math.floor(availableWidth / term.charMeasure.width),
rows: Math.floor(availableHeight / Math.ceil(term.charMeasure.height * term.getOption('lineHeight')))
rows: Math.floor(availableHeight / Math.floor(term.charMeasure.height * term.getOption('lineHeight')))
};
return geometry;
+3 -3
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IColorSet } from './Interfaces';
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
@@ -18,8 +18,8 @@ export class BackgroundRenderLayer extends BaseRenderLayer {
this._state = new GridCache<number>();
}
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
// Resizing the canvas discards the contents of the canvas so clear state
this._state.clear();
this._state.resize(terminal.cols, terminal.rows);
+10 -29
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IRenderLayer, IColorSet } from './Interfaces';
import { IRenderLayer, IColorSet, IRenderDimensions } from './Interfaces';
import { ITerminal, ITerminalOptions } from '../Interfaces';
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
import { CharData } from '../Types';
@@ -61,34 +61,15 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
}
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
// Calculate the scaled character dimensions, if devicePixelRatio is a
// floating point number then the value is ceiled to ensure there is enough
// space to draw the character to the cell
this.scaledCharWidth = Math.ceil(terminal.charMeasure.width * window.devicePixelRatio);
this.scaledCharHeight = Math.ceil(terminal.charMeasure.height * window.devicePixelRatio);
// Calculate the scaled line height, if lineHeight is not 1 then the value
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
this.scaledLineHeight = Math.floor(this.scaledCharHeight * terminal.options.lineHeight);
// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
this.scaledLineDrawY = terminal.options.lineHeight === 1 ? 0 : Math.round((this.scaledLineHeight - this.scaledCharHeight) / 2);
// Recalcualte the canvas dimensions; width/height define the actual number
// of pixels in the canvas, style.width/height define the size of the canvas
// on the page. It's very important that this rounds to nearest integer and
// not ceils as browsers often set window.devicePixelRatio as something like
// 1.100000023841858, when it's actually 1.1. Ceiling causes blurriness as
// the backing canvas image is 1 pixel too large for the canvas element
// size.
this._canvas.width = Math.round(canvasWidth * window.devicePixelRatio);
this._canvas.height = Math.round(canvasHeight * window.devicePixelRatio);
this._canvas.style.width = `${canvasWidth}px`;
this._canvas.style.height = `${canvasHeight}px`;
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
this.scaledCharWidth = dim.scaledCharWidth;
this.scaledCharHeight = dim.scaledCharHeight;
this.scaledLineHeight = dim.scaledLineHeight;
this.scaledLineDrawY = dim.scaledLineDrawY;
this._canvas.width = dim.scaledCanvasWidth;
this._canvas.height = dim.scaledCanvasHeight;
this._canvas.style.width = `${dim.canvasWidth}px`;
this._canvas.style.height = `${dim.canvasHeight}px`;
if (charSizeChanged) {
this._refreshCharAtlas(terminal, this.colors);
+3 -3
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IColorSet } from './Interfaces';
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
@@ -47,8 +47,8 @@ export class CursorRenderLayer extends BaseRenderLayer {
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
}
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
x: null,
+3 -3
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IColorSet } from './Interfaces';
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
import { FLAGS } from './Types';
@@ -26,8 +26,8 @@ export class ForegroundRenderLayer extends BaseRenderLayer {
this._state = new GridCache<CharData>();
}
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
// Resizing the canvas discards the contents of the canvas so clear state
this._state.clear();
this._state.resize(terminal.cols, terminal.rows);
+16 -3
View File
@@ -3,9 +3,11 @@
* @license MIT
*/
import { ITerminal, ITerminalOptions, ITheme } from '../Interfaces';
import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interfaces';
export interface IRenderer extends IEventEmitter {
dimensions: IRenderDimensions;
export interface IRenderer {
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number, didCharSizeChange: boolean): void;
@@ -59,7 +61,7 @@ export interface IRenderLayer {
/**
* Resize the render layer.
*/
resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void;
resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void;
/**
* Clear the state of the render layer.
@@ -75,3 +77,14 @@ export interface IColorSet {
selection: string;
ansi: string[];
}
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledLineHeight: number;
scaledLineDrawY: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
}
+3 -3
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IColorSet } from './Interfaces';
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
@@ -20,8 +20,8 @@ export class LinkRenderLayer extends BaseRenderLayer {
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
}
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = null;
}
+52 -5
View File
@@ -11,10 +11,11 @@ import { SelectionRenderLayer } from './SelectionRenderLayer';
import { CursorRenderLayer } from './CursorRenderLayer';
import { ColorManager } from './ColorManager';
import { BaseRenderLayer } from './BaseRenderLayer';
import { IRenderLayer, IColorSet, IRenderer } from './Interfaces';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter';
export class Renderer implements IRenderer {
export class Renderer extends EventEmitter implements IRenderer {
/** A queue of the rows to be refreshed */
private _refreshRowsQueue: {start: number, end: number}[] = [];
private _refreshAnimationFrame = null;
@@ -23,8 +24,10 @@ export class Renderer implements IRenderer {
private _devicePixelRatio: number;
private _colorManager: ColorManager;
public dimensions: IRenderDimensions;
constructor(private _terminal: ITerminal) {
super();
this._colorManager = new ColorManager();
this._renderLayers = [
new BackgroundRenderLayer(this._terminal.element, 0, this._colorManager.colors),
@@ -33,6 +36,16 @@ export class Renderer implements IRenderer {
new LinkRenderLayer(this._terminal.element, 3, this._colorManager.colors, this._terminal),
new CursorRenderLayer(this._terminal.element, 4, this._colorManager.colors)
];
this.dimensions = {
scaledCharWidth: null,
scaledCharHeight: null,
scaledLineHeight: null,
scaledLineDrawY: null,
scaledCanvasWidth: null,
scaledCanvasHeight: null,
canvasWidth: null,
canvasHeight: null
};
this._devicePixelRatio = window.devicePixelRatio;
}
@@ -63,12 +76,46 @@ export class Renderer implements IRenderer {
if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) {
return;
}
const width = this._terminal.charMeasure.width * cols;
const height = Math.floor(this._terminal.charMeasure.height * this._terminal.options.lineHeight) * rows;
// Calculate the scaled character dimensions, if devicePixelRatio is a
// floating point number then the value is ceiled to ensure there is enough
// space to draw the character to the cell
this.dimensions.scaledCharWidth = Math.ceil(this._terminal.charMeasure.width * window.devicePixelRatio);
this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio);
// Calculate the scaled line height, if lineHeight is not 1 then the value
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
this.dimensions.scaledLineHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledLineDrawY = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledLineHeight - this.dimensions.scaledCharHeight) / 2);
// Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledLineHeight;
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCharWidth;
// The the size of the canvas on the page. It's very important that this
// rounds to nearest integer and not ceils as browsers often set
// window.devicePixelRatio as something like 1.100000023841858, when it's
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
// pixel too large for the canvas element size.
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
// Resize all render layers
this._renderLayers.forEach(l => l.resize(this._terminal, width, height, didCharSizeChange));
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange));
// Force a refresh
this._terminal.refresh(0, this._terminal.rows - 1);
this.emit('resize', {
width: this.dimensions.canvasWidth,
height: this.dimensions.canvasHeight
});
}
public onCharSizeChanged(): void {
+3 -3
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IColorSet } from './Interfaces';
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
@@ -21,8 +21,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
};
}
public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void {
super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
start: null,
+12 -1
View File
@@ -7,9 +7,10 @@ import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManag
import { LineData } from '../Types';
import { Buffer } from '../Buffer';
import * as Browser from './Browser';
import { IColorSet, IRenderer } from '../renderer/Interfaces';
import { IColorSet, IRenderer, IRenderDimensions } from '../renderer/Interfaces';
export class MockTerminal implements ITerminal {
renderer: IRenderer;
linkifier: ILinkifier;
isFocused: boolean;
options: ITerminalOptions = {};
@@ -215,6 +216,16 @@ export class MockBuffer implements IBuffer {
}
export class MockRenderer implements IRenderer {
on(type: string, listener: IListenerType): void {
throw new Error('Method not implemented.');
}
off(type: string, listener: IListenerType): void {
throw new Error('Method not implemented.');
}
emit(type: string, data?: any): void {
throw new Error('Method not implemented.');
}
dimensions: IRenderDimensions;
setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; }
onResize(cols: number, rows: number, didCharSizeChange: boolean): void {}
onCharSizeChanged(): void {}