Skip to content

Commit 5767f6f

Browse files
authored
minimap - allow variable scaling (microsoft#82265)
* minimap - allow variable scaling This PR allows the minimap to be scaled to several constant values. Most of the work in this PR is adjusting the the font renderer to render character at variable sizes. It turns out most generic image scaling algorithms are not built to scale down to one or two pixels (the default minimap font size has 1px by 2px characters), so some work was needed to make this possible and look good. Generating fonts at runtime does incur a small performance penalty, taking about 0.6m at 1x scale and 0.9ms at 4x scale on my machine to create the font the first time we render a minimap. If we want to avoid this, we could consider shipping pre-rendered font for the first few scale settings. At this moment this only supports scaling to a constant integer--effectively, scaling the character width, since we start at 1x2px. More granular scaling would be interesting, but will come at a runtime cost as we'll need to do linear interpolation for each character we draw at a non-integral coordinate. Draw speed is comparable to the previous version, the profiler reported in the range of 8-11ms to render my test file in both the previous and new code. I've tested this on my high DPI Macbook display and it appears to work well there too. Talking to Alex, something we may need to look into is matching the user font and render settings. Previously, and continuing in this PR, we use the default monospace font on the system with a restricted set of character codes. Previously the sidebar's font was too small to be visible, but now its content can be seen under large settings. We may need to look and reworking how this data is rendered. Perhaps we generate the characters we need on the fly into their own buffers? Open to ideas. Fixes microsoft#21773 * fixup! not caching created factory * fix common/browser component layering * fixup! use a constant upscale for hDPI * small tweaks * fixup! pr comments * fixup! reduce max minimap scale
1 parent aad1e68 commit 5767f6f

16 files changed

Lines changed: 505 additions & 1701 deletions

src/vs/base/common/iterator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export module Iterator {
6060
};
6161
}
6262

63-
export function fromArray<T>(array: T[], index = 0, length = array.length): Iterator<T> {
63+
export function fromArray<T>(array: ReadonlyArray<T>, index = 0, length = array.length): Iterator<T> {
6464
return {
6565
next(): IteratorResult<T> {
6666
if (index >= length) {

src/vs/editor/browser/viewParts/minimap/minimap.ts

Lines changed: 38 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ import { Range } from 'vs/editor/common/core/range';
1818
import { RGBA8 } from 'vs/editor/common/core/rgba';
1919
import { IConfiguration, ScrollType } from 'vs/editor/common/editorCommon';
2020
import { ColorId } from 'vs/editor/common/modes';
21-
import { Constants, MinimapCharRenderer, MinimapTokensColorTracker } from 'vs/editor/common/view/minimapCharRenderer';
21+
import { MinimapCharRenderer } from 'vs/editor/browser/viewParts/minimap/minimapCharRenderer';
22+
import { Constants } from 'vs/editor/browser/viewParts/minimap/minimapCharSheet';
23+
import { MinimapTokensColorTracker } from 'vs/editor/common/viewModel/minimapTokensColorTracker';
2224
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
23-
import { getOrCreateMinimapCharRenderer } from 'vs/editor/common/view/runtimeMinimapCharRenderer';
2425
import { ViewContext } from 'vs/editor/common/view/viewContext';
2526
import * as viewEvents from 'vs/editor/common/view/viewEvents';
2627
import { ViewLineData } from 'vs/editor/common/viewModel/viewModel';
@@ -30,33 +31,22 @@ import { ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel'
3031
import { Selection } from 'vs/editor/common/core/selection';
3132
import { Color } from 'vs/base/common/color';
3233
import { GestureEvent, EventType, Gesture } from 'vs/base/browser/touch';
34+
import { MinimapCharRendererFactory } from 'vs/editor/browser/viewParts/minimap/minimapCharRendererFactory';
3335

34-
function getMinimapLineHeight(renderMinimap: RenderMinimap): number {
35-
if (renderMinimap === RenderMinimap.Large) {
36-
return Constants.x2_CHAR_HEIGHT;
36+
function getMinimapLineHeight(renderMinimap: RenderMinimap, scale: number): number {
37+
if (renderMinimap === RenderMinimap.Text) {
38+
return Constants.BASE_CHAR_HEIGHT * scale;
3739
}
38-
if (renderMinimap === RenderMinimap.LargeBlocks) {
39-
return Constants.x2_CHAR_HEIGHT + 2;
40-
}
41-
if (renderMinimap === RenderMinimap.Small) {
42-
return Constants.x1_CHAR_HEIGHT;
43-
}
44-
// RenderMinimap.SmallBlocks
45-
return Constants.x1_CHAR_HEIGHT + 1;
40+
// RenderMinimap.Blocks
41+
return (Constants.BASE_CHAR_HEIGHT + 1) * scale;
4642
}
4743

48-
function getMinimapCharWidth(renderMinimap: RenderMinimap): number {
49-
if (renderMinimap === RenderMinimap.Large) {
50-
return Constants.x2_CHAR_WIDTH;
51-
}
52-
if (renderMinimap === RenderMinimap.LargeBlocks) {
53-
return Constants.x2_CHAR_WIDTH;
54-
}
55-
if (renderMinimap === RenderMinimap.Small) {
56-
return Constants.x1_CHAR_WIDTH;
44+
function getMinimapCharWidth(renderMinimap: RenderMinimap, scale: number): number {
45+
if (renderMinimap === RenderMinimap.Text) {
46+
return Constants.BASE_CHAR_WIDTH * scale;
5747
}
58-
// RenderMinimap.SmallBlocks
59-
return Constants.x1_CHAR_WIDTH;
48+
// RenderMinimap.Blocks
49+
return Constants.BASE_CHAR_WIDTH * scale;
6050
}
6151

6252
/**
@@ -78,6 +68,10 @@ class MinimapOptions {
7868

7969
public readonly lineHeight: number;
8070

71+
public readonly fontScale: number;
72+
73+
public readonly charRenderer: MinimapCharRenderer;
74+
8175
/**
8276
* container dom node left position (in CSS px)
8377
*/
@@ -119,6 +113,8 @@ class MinimapOptions {
119113
this.scrollBeyondLastLine = options.get(EditorOption.scrollBeyondLastLine);
120114
const minimapOpts = options.get(EditorOption.minimap);
121115
this.showSlider = minimapOpts.showSlider;
116+
this.fontScale = Math.round(minimapOpts.scale * pixelRatio);
117+
this.charRenderer = MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily);
122118
this.pixelRatio = pixelRatio;
123119
this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth;
124120
this.lineHeight = options.get(EditorOption.lineHeight);
@@ -140,6 +136,7 @@ class MinimapOptions {
140136
&& this.pixelRatio === other.pixelRatio
141137
&& this.typicalHalfwidthCharacterWidth === other.typicalHalfwidthCharacterWidth
142138
&& this.lineHeight === other.lineHeight
139+
&& this.fontScale === other.fontScale
143140
&& this.minimapLeft === other.minimapLeft
144141
&& this.minimapWidth === other.minimapWidth
145142
&& this.minimapHeight === other.minimapHeight
@@ -225,7 +222,7 @@ class MinimapLayout {
225222
previousLayout: MinimapLayout | null
226223
): MinimapLayout {
227224
const pixelRatio = options.pixelRatio;
228-
const minimapLineHeight = getMinimapLineHeight(options.renderMinimap);
225+
const minimapLineHeight = getMinimapLineHeight(options.renderMinimap, options.fontScale);
229226
const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight);
230227
const lineHeight = options.lineHeight;
231228

@@ -524,7 +521,7 @@ export class Minimap extends ViewPart {
524521
if (!this._lastRenderData) {
525522
return;
526523
}
527-
const minimapLineHeight = getMinimapLineHeight(renderMinimap);
524+
const minimapLineHeight = getMinimapLineHeight(renderMinimap, this._options.fontScale);
528525
const internalOffsetY = this._options.pixelRatio * e.browserEvent.offsetY;
529526
const lineIndex = Math.floor(internalOffsetY / minimapLineHeight);
530527

@@ -778,7 +775,7 @@ export class Minimap extends ViewPart {
778775

779776
// Compute horizontal slider coordinates
780777
const scrollLeftChars = renderingCtx.scrollLeft / this._options.typicalHalfwidthCharacterWidth;
781-
const horizontalSliderLeft = Math.min(this._options.minimapWidth, Math.round(scrollLeftChars * getMinimapCharWidth(this._options.renderMinimap) / this._options.pixelRatio));
778+
const horizontalSliderLeft = Math.min(this._options.minimapWidth, Math.round(scrollLeftChars * getMinimapCharWidth(this._options.renderMinimap, this._options.fontScale) / this._options.pixelRatio));
782779
this._sliderHorizontal.setLeft(horizontalSliderLeft);
783780
this._sliderHorizontal.setWidth(this._options.minimapWidth - horizontalSliderLeft);
784781
this._sliderHorizontal.setTop(0);
@@ -794,8 +791,8 @@ export class Minimap extends ViewPart {
794791
const decorations = this._context.model.getDecorationsInViewport(new Range(layout.startLineNumber, 1, layout.endLineNumber, this._context.model.getLineMaxColumn(layout.endLineNumber)));
795792

796793
const { renderMinimap, canvasInnerWidth, canvasInnerHeight } = this._options;
797-
const lineHeight = getMinimapLineHeight(renderMinimap);
798-
const characterWidth = getMinimapCharWidth(renderMinimap);
794+
const lineHeight = getMinimapLineHeight(renderMinimap, this._options.fontScale);
795+
const characterWidth = getMinimapCharWidth(renderMinimap, this._options.fontScale);
799796
const tabSize = this._context.model.getOptions().tabSize;
800797
const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!;
801798

@@ -890,7 +887,7 @@ export class Minimap extends ViewPart {
890887
const renderMinimap = this._options.renderMinimap;
891888
const startLineNumber = layout.startLineNumber;
892889
const endLineNumber = layout.endLineNumber;
893-
const minimapLineHeight = getMinimapLineHeight(renderMinimap);
890+
const minimapLineHeight = getMinimapLineHeight(renderMinimap, this._options.fontScale);
894891

895892
// Check if nothing changed w.r.t. lines from last frame
896893
if (this._lastRenderData && this._lastRenderData.linesEquals(layout)) {
@@ -929,10 +926,11 @@ export class Minimap extends ViewPart {
929926
useLighterFont,
930927
renderMinimap,
931928
this._tokensColorTracker,
932-
getOrCreateMinimapCharRenderer(),
929+
this._options.charRenderer,
933930
dy,
934931
tabSize,
935-
lineInfo.data[lineIndex]!
932+
lineInfo.data[lineIndex]!,
933+
this._options.fontScale
936934
);
937935
}
938936
renderedLines[lineIndex] = new MinimapLine(dy);
@@ -1056,11 +1054,12 @@ export class Minimap extends ViewPart {
10561054
minimapCharRenderer: MinimapCharRenderer,
10571055
dy: number,
10581056
tabSize: number,
1059-
lineData: ViewLineData
1057+
lineData: ViewLineData,
1058+
fontScale: number
10601059
): void {
10611060
const content = lineData.content;
10621061
const tokens = lineData.tokens;
1063-
const charWidth = getMinimapCharWidth(renderMinimap);
1062+
const charWidth = getMinimapCharWidth(renderMinimap, fontScale);
10641063
const maxDx = target.width - charWidth;
10651064

10661065
let dx = 0;
@@ -1092,16 +1091,12 @@ export class Minimap extends ViewPart {
10921091
const count = strings.isFullWidthCharacter(charCode) ? 2 : 1;
10931092

10941093
for (let i = 0; i < count; i++) {
1095-
if (renderMinimap === RenderMinimap.Large) {
1096-
minimapCharRenderer.x2RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont);
1097-
} else if (renderMinimap === RenderMinimap.Small) {
1098-
minimapCharRenderer.x1RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont);
1099-
} else if (renderMinimap === RenderMinimap.LargeBlocks) {
1100-
minimapCharRenderer.x2BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont);
1101-
} else {
1102-
// RenderMinimap.SmallBlocks
1103-
minimapCharRenderer.x1BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont);
1094+
if (renderMinimap === RenderMinimap.Blocks) {
1095+
minimapCharRenderer.blockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont);
1096+
} else { // RenderMinimap.Text
1097+
minimapCharRenderer.renderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont);
11041098
}
1099+
11051100
dx += charWidth;
11061101

11071102
if (dx > maxDx) {
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 { RGBA8 } from 'vs/editor/common/core/rgba';
7+
import { Constants, getCharIndex } from './minimapCharSheet';
8+
9+
export class MinimapCharRenderer {
10+
_minimapCharRendererBrand: void;
11+
12+
private readonly charDataNormal: Uint8ClampedArray;
13+
private readonly charDataLight: Uint8ClampedArray;
14+
15+
constructor(charData: Uint8ClampedArray, public readonly scale: number) {
16+
this.charDataNormal = MinimapCharRenderer.soften(charData, 12 / 15);
17+
this.charDataLight = MinimapCharRenderer.soften(charData, 50 / 60);
18+
}
19+
20+
private static soften(input: Uint8ClampedArray, ratio: number): Uint8ClampedArray {
21+
let result = new Uint8ClampedArray(input.length);
22+
for (let i = 0, len = input.length; i < len; i++) {
23+
result[i] = input[i] * ratio;
24+
}
25+
return result;
26+
}
27+
28+
public renderChar(
29+
target: ImageData,
30+
dx: number,
31+
dy: number,
32+
chCode: number,
33+
color: RGBA8,
34+
backgroundColor: RGBA8,
35+
useLighterFont: boolean
36+
): void {
37+
const charWidth = Constants.BASE_CHAR_WIDTH * this.scale;
38+
const charHeight = Constants.BASE_CHAR_HEIGHT * this.scale;
39+
if (dx + charWidth > target.width || dy + charHeight > target.height) {
40+
console.warn('bad render request outside image data');
41+
return;
42+
}
43+
44+
const charData = useLighterFont ? this.charDataLight : this.charDataNormal;
45+
const charIndex = getCharIndex(chCode);
46+
47+
const destWidth = target.width * Constants.RGBA_CHANNELS_CNT;
48+
49+
const backgroundR = backgroundColor.r;
50+
const backgroundG = backgroundColor.g;
51+
const backgroundB = backgroundColor.b;
52+
53+
const deltaR = color.r - backgroundR;
54+
const deltaG = color.g - backgroundG;
55+
const deltaB = color.b - backgroundB;
56+
57+
const dest = target.data;
58+
let sourceOffset = charIndex * charWidth * charHeight;
59+
60+
let row = dy * destWidth + dx * Constants.RGBA_CHANNELS_CNT;
61+
for (let y = 0; y < charHeight; y++) {
62+
let column = row;
63+
for (let x = 0; x < charWidth; x++) {
64+
const c = charData[sourceOffset++] / 255;
65+
dest[column++] = backgroundR + deltaR * c;
66+
dest[column++] = backgroundG + deltaG * c;
67+
dest[column++] = backgroundB + deltaB * c;
68+
column++;
69+
}
70+
71+
row += destWidth;
72+
}
73+
}
74+
75+
public blockRenderChar(
76+
target: ImageData,
77+
dx: number,
78+
dy: number,
79+
color: RGBA8,
80+
backgroundColor: RGBA8,
81+
useLighterFont: boolean
82+
): void {
83+
const charWidth = Constants.BASE_CHAR_WIDTH * this.scale;
84+
const charHeight = Constants.BASE_CHAR_HEIGHT * this.scale;
85+
if (dx + charWidth > target.width || dy + charHeight > target.height) {
86+
console.warn('bad render request outside image data');
87+
return;
88+
}
89+
90+
const destWidth = target.width * Constants.RGBA_CHANNELS_CNT;
91+
92+
const c = 0.5;
93+
94+
const backgroundR = backgroundColor.r;
95+
const backgroundG = backgroundColor.g;
96+
const backgroundB = backgroundColor.b;
97+
98+
const deltaR = color.r - backgroundR;
99+
const deltaG = color.g - backgroundG;
100+
const deltaB = color.b - backgroundB;
101+
102+
const colorR = backgroundR + deltaR * c;
103+
const colorG = backgroundG + deltaG * c;
104+
const colorB = backgroundB + deltaB * c;
105+
106+
const dest = target.data;
107+
108+
let row = dy * destWidth + dx * Constants.RGBA_CHANNELS_CNT;
109+
for (let y = 0; y < charHeight; y++) {
110+
let column = row;
111+
for (let x = 0; x < charWidth; x++) {
112+
dest[column++] = colorR;
113+
dest[column++] = colorG;
114+
dest[column++] = colorB;
115+
column++;
116+
}
117+
118+
row += destWidth;
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)