Skip to content

Commit 3870507

Browse files
authored
Merge pull request #3676 from meganrogge/merogge/scroll-bar-decorations
add overview ruler
2 parents 3e283e9 + a0a7abb commit 3870507

File tree

15 files changed

+479
-248
lines changed

15 files changed

+479
-248
lines changed

css/xterm.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,10 @@
178178
z-index: 6;
179179
position: absolute;
180180
}
181+
182+
.xterm-decoration-overview-ruler {
183+
z-index: 7;
184+
position: absolute;
185+
top: 0;
186+
right: 0;
187+
}

demo/client.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ if (document.location.pathname === '/test') {
151151
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
152152
document.getElementById('load-test').addEventListener('click', loadTest);
153153
document.getElementById('add-decoration').addEventListener('click', addDecoration);
154+
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
154155
}
155156

156157
function createTerminal(): void {
@@ -544,9 +545,23 @@ function loadTest() {
544545
}
545546

546547
function addDecoration() {
548+
term.options['overviewRulerWidth'] = 15;
547549
const marker = term.addMarker(1);
548-
const decoration = term.registerDecoration({ marker });
549-
decoration.onRender(() => {
550-
decoration.element.style.backgroundColor = 'red';
550+
const decoration = term.registerDecoration({ marker, overviewRulerOptions: { color: '#ef2929'} });
551+
decoration.onRender((e) => {
552+
if (e.classList.value === 'xterm-decoration') {
553+
e.style.backgroundColor = '#ef2929';
554+
}
551555
});
552556
}
557+
558+
function addOverviewRuler() {
559+
term.options['overviewRulerWidth'] = 15;
560+
term.registerDecoration({marker: term.addMarker(1), overviewRulerOptions: { color: '#ef2929' }});
561+
term.registerDecoration({marker: term.addMarker(3), overviewRulerOptions: { color: '#8ae234' }});
562+
term.registerDecoration({marker: term.addMarker(5), overviewRulerOptions: { color: '#729fcf' }});
563+
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#ef2929', position: 'left' }});
564+
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#8ae234', position: 'center' }});
565+
term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#729fcf', position: 'right' }});
566+
}
567+

demo/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ <h3>Test</h3>
6969
<button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button>
7070
<button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button>
7171
<button id="add-decoration" title="Add a decoration to the terminal">Decoration</button>
72+
<button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button>
7273
</div>
7374
</div>
7475
</div>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { addDisposableDomListener } from 'browser/Lifecycle';
7+
import { IRenderService } from 'browser/services/Services';
8+
import { Disposable } from 'common/Lifecycle';
9+
import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services';
10+
11+
export class BufferDecorationRenderer extends Disposable {
12+
private readonly _container: HTMLElement;
13+
private readonly _decorationElements: Map<IInternalDecoration, HTMLElement> = new Map();
14+
15+
private _animationFrame: number | undefined;
16+
private _altBufferIsActive: boolean = false;
17+
18+
constructor(
19+
private readonly _screenElement: HTMLElement,
20+
@IBufferService private readonly _bufferService: IBufferService,
21+
@IDecorationService private readonly _decorationService: IDecorationService,
22+
@IRenderService private readonly _renderService: IRenderService
23+
) {
24+
super();
25+
26+
this._container = document.createElement('div');
27+
this._container.classList.add('xterm-decoration-container');
28+
this._screenElement.appendChild(this._container);
29+
30+
this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh()));
31+
this.register(this._renderService.onDimensionsChange(() => this._queueRefresh()));
32+
this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh()));
33+
this.register(this._bufferService.buffers.onBufferActivate(() => {
34+
this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt;
35+
}));
36+
this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh()));
37+
this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration)));
38+
}
39+
40+
public override dispose(): void {
41+
this._container.remove();
42+
this._decorationElements.clear();
43+
super.dispose();
44+
}
45+
46+
private _queueRefresh(): void {
47+
if (this._animationFrame !== undefined) {
48+
return;
49+
}
50+
this._animationFrame = window.requestAnimationFrame(() => {
51+
this.refreshDecorations();
52+
this._animationFrame = undefined;
53+
});
54+
}
55+
56+
public refreshDecorations(): void {
57+
for (const decoration of this._decorationService.decorations) {
58+
this._renderDecoration(decoration);
59+
}
60+
}
61+
62+
private _renderDecoration(decoration: IInternalDecoration): void {
63+
let element = this._decorationElements.get(decoration);
64+
if (!element) {
65+
element = this._createElement(decoration);
66+
decoration.onDispose(() => this._removeDecoration(decoration));
67+
decoration.marker.onDispose(() => decoration.dispose());
68+
decoration.element = element;
69+
this._decorationElements.set(decoration, element);
70+
this._container.appendChild(element);
71+
}
72+
this._refreshStyle(decoration, element);
73+
decoration.onRenderEmitter.fire(element);
74+
}
75+
76+
private _createElement(decoration: IInternalDecoration): HTMLElement {
77+
const element = document.createElement('div');
78+
element.classList.add('xterm-decoration');
79+
element.style.width = `${(decoration.options.width || 1) * this._renderService.dimensions.actualCellWidth}px`;
80+
element.style.height = `${(decoration.options.height || 1) * this._renderService.dimensions.actualCellHeight}px`;
81+
element.style.top = `${(decoration.marker.line - this._bufferService.buffers.active.ydisp) * this._renderService.dimensions.actualCellHeight}px`;
82+
element.style.lineHeight = `${this._renderService.dimensions.actualCellHeight}px`;
83+
84+
const x = decoration.options.x ?? 0;
85+
if (x && x > this._bufferService.cols) {
86+
// exceeded the container width, so hide
87+
element.style.display = 'none';
88+
}
89+
if ((decoration.options.anchor || 'left') === 'right') {
90+
element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
91+
} else {
92+
element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
93+
}
94+
95+
return element;
96+
}
97+
98+
private _refreshStyle(decoration: IInternalDecoration, element: HTMLElement): void {
99+
const line = decoration.marker.line - this._bufferService.buffers.active.ydisp;
100+
if (line < 0 || line > this._bufferService.rows) {
101+
// outside of viewport
102+
element.style.display = 'none';
103+
} else {
104+
element.style.top = `${line * this._renderService.dimensions.actualCellHeight}px`;
105+
element.style.display = this._altBufferIsActive ? 'none' : 'block';
106+
}
107+
}
108+
109+
private _removeDecoration(decoration: IInternalDecoration): void {
110+
this._decorationElements.get(decoration)?.remove();
111+
this._decorationElements.delete(decoration);
112+
}
113+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { addDisposableDomListener } from 'browser/Lifecycle';
7+
import { IRenderService } from 'browser/services/Services';
8+
import { Disposable } from 'common/Lifecycle';
9+
import { IBufferService, IDecorationService, IInternalDecoration, IOptionsService } from 'common/services/Services';
10+
11+
// This is used to reduce memory usage
12+
// when refreshStyle is called
13+
// by storing and updating
14+
// the sizes of the decorations to be drawn
15+
const renderSizes = new Uint16Array(3);
16+
const enum SizeIndex {
17+
OUTER_SIZE = 0,
18+
INNER_SIZE = 0
19+
}
20+
21+
export class OverviewRulerRenderer extends Disposable {
22+
private readonly _canvas: HTMLCanvasElement;
23+
private readonly _ctx: CanvasRenderingContext2D;
24+
private readonly _decorationElements: Map<IInternalDecoration, HTMLElement> = new Map();
25+
private get _width(): number {
26+
return this._optionsService.options.overviewRulerWidth || 0;
27+
}
28+
private _animationFrame: number | undefined;
29+
30+
constructor(
31+
private readonly _viewportElement: HTMLElement,
32+
private readonly _screenElement: HTMLElement,
33+
@IBufferService private readonly _bufferService: IBufferService,
34+
@IDecorationService private readonly _decorationService: IDecorationService,
35+
@IRenderService private readonly _renderService: IRenderService,
36+
@IOptionsService private readonly _optionsService: IOptionsService
37+
) {
38+
super();
39+
this._canvas = document.createElement('canvas');
40+
this._canvas.classList.add('xterm-decoration-overview-ruler');
41+
this._viewportElement.parentElement?.insertBefore(this._canvas, this._viewportElement);
42+
const ctx = this._canvas.getContext('2d');
43+
if (!ctx) {
44+
throw new Error('Ctx cannot be null');
45+
} else {
46+
this._ctx = ctx;
47+
}
48+
this._queueRefresh(true);
49+
this.register(this._bufferService.buffers.onBufferActivate(() => {
50+
this._canvas!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block';
51+
}));
52+
this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh()));
53+
this.register(this._renderService.onDimensionsChange(() => this._queueRefresh(true, true)));
54+
this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh(true)));
55+
this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh(undefined, true)));
56+
this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration)));
57+
this.register(this._optionsService.onOptionChange(o => {
58+
if (o === 'overviewRulerWidth') {
59+
renderSizes[SizeIndex.OUTER_SIZE] = Math.floor(this._width / 3);
60+
renderSizes[SizeIndex.INNER_SIZE] = Math.ceil(this._width / 3);
61+
this._queueRefresh();
62+
}
63+
}));
64+
console.log(this._width/3);
65+
renderSizes[SizeIndex.OUTER_SIZE] = Math.floor(this._width / 3);
66+
renderSizes[SizeIndex.INNER_SIZE] = Math.ceil(this._width / 3);
67+
}
68+
69+
public override dispose(): void {
70+
for (const decoration of this._decorationElements) {
71+
this._ctx?.clearRect(
72+
0,
73+
Math.round(this._canvas.height * (decoration[0].marker.line / this._bufferService.buffers.active.lines.length)),
74+
this._canvas.width,
75+
window.devicePixelRatio
76+
);
77+
}
78+
this._decorationElements.clear();
79+
this._canvas?.remove();
80+
super.dispose();
81+
}
82+
83+
private _refreshStyle(decoration: IInternalDecoration, updateAnchor?: boolean): void {
84+
if (updateAnchor) {
85+
if (decoration.options.anchor === 'right') {
86+
this._canvas.style.right = decoration.options.x ? `${decoration.options.x * this._renderService.dimensions.actualCellWidth}px` : '';
87+
} else {
88+
this._canvas.style.left = decoration.options.x ? `${decoration.options.x * this._renderService.dimensions.actualCellWidth}px` : '';
89+
}
90+
}
91+
if (!decoration.options.overviewRulerOptions) {
92+
this._decorationElements.delete(decoration);
93+
return;
94+
}
95+
this._ctx.lineWidth = !decoration.options.overviewRulerOptions.position ? 2 : 6;
96+
this._ctx.strokeStyle = decoration.options.overviewRulerOptions.color;
97+
this._ctx.strokeRect(
98+
!decoration.options.overviewRulerOptions.position || decoration.options.overviewRulerOptions.position === 'left' ? 0 : decoration.options.overviewRulerOptions.position === 'right' ? renderSizes[SizeIndex.OUTER_SIZE] + renderSizes[SizeIndex.INNER_SIZE]: renderSizes[SizeIndex.OUTER_SIZE],
99+
Math.round(this._canvas.height * (decoration.options.marker.line / this._bufferService.buffers.active.lines.length)),
100+
!decoration.options.overviewRulerOptions.position ? this._width : decoration.options.overviewRulerOptions.position === 'center' ? renderSizes[SizeIndex.INNER_SIZE]: renderSizes[SizeIndex.OUTER_SIZE],
101+
window.devicePixelRatio
102+
);
103+
}
104+
105+
private _refreshDecorations(updateCanvasDimensions?: boolean, updateAnchor?: boolean): void {
106+
if (updateCanvasDimensions) {
107+
this._canvas.style.width = `${this._width}px`;
108+
this._canvas.style.height = `${this._screenElement.clientHeight}px`;
109+
this._canvas.width = Math.floor((this._width)* window.devicePixelRatio);
110+
this._canvas.height = Math.floor(this._screenElement.clientHeight * window.devicePixelRatio);
111+
}
112+
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
113+
for (const decoration of this._decorationService.decorations) {
114+
this._renderDecoration(decoration, updateAnchor);
115+
}
116+
}
117+
118+
private _renderDecoration(decoration: IInternalDecoration, updateAnchor?: boolean): void {
119+
const element = this._decorationElements.get(decoration);
120+
if (!element) {
121+
this._decorationElements.set(decoration, this._canvas);
122+
}
123+
this._refreshStyle(decoration, updateAnchor);
124+
decoration.onRenderEmitter.fire(this._canvas);
125+
}
126+
127+
private _queueRefresh(updateCanvasDimensions?: boolean, updateAnchor?: boolean): void {
128+
if (this._animationFrame !== undefined) {
129+
return;
130+
}
131+
this._animationFrame = window.requestAnimationFrame(() => {
132+
this._refreshDecorations(updateCanvasDimensions, updateAnchor);
133+
this._animationFrame = undefined;
134+
});
135+
}
136+
137+
private _removeDecoration(decoration: IInternalDecoration): void {
138+
this._decorationElements.get(decoration)?.remove();
139+
this._decorationElements.delete(decoration);
140+
}
141+
}

0 commit comments

Comments
 (0)