diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index cca3805245ee08..7503771ae8a109 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -57,6 +57,7 @@ export const enum TerminalSettingId { EnableMultiLinePasteWarning = 'terminal.integrated.enableMultiLinePasteWarning', DrawBoldTextInBrightColors = 'terminal.integrated.drawBoldTextInBrightColors', FontFamily = 'terminal.integrated.fontFamily', + FontRendering = 'terminal.integrated.fontRendering', FontSize = 'terminal.integrated.fontSize', LetterSpacing = 'terminal.integrated.letterSpacing', LineHeight = 'terminal.integrated.lineHeight', diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/terminalFontRendering.css b/src/vs/workbench/contrib/terminal/browser/xterm/terminalFontRendering.css new file mode 100644 index 00000000000000..f65bbdf90e0cc5 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/xterm/terminalFontRendering.css @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + .xterm.terminal-font-rendering-crisp { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/terminalFontRendering.ts b/src/vs/workbench/contrib/terminal/browser/xterm/terminalFontRendering.ts new file mode 100644 index 00000000000000..cfb118c6f529d7 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/xterm/terminalFontRendering.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Terminal } from '@xterm/xterm'; +import { isMacintosh } from '../../../../../base/common/platform.js'; +import type { ITerminalConfiguration } from '../../common/terminal.js'; +import './terminalFontRendering.css'; + +const enum CssClasses { + Crisp = 'terminal-font-rendering-crisp' +} + +export function updateTerminalFontRendering(terminal: Terminal, fontRendering: ITerminalConfiguration['fontRendering']): void { + const element = terminal.element; + if (!element) { + return; + } + + const crisp = isMacintosh && fontRendering === 'crisp'; + if (element.classList.contains(CssClasses.Crisp) === crisp) { + return; + } + element.classList.toggle(CssClasses.Crisp, crisp); + + // The atlas canvas inherits this policy, but cached glyphs retain their previous pixels. + terminal.clearTextureAtlas(); +} diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 2bf7f8bbec1535..40c20ff01e3936 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -50,6 +50,7 @@ import { isNumber } from '../../../../../base/common/types.js'; import { clamp } from '../../../../../base/common/numbers.js'; import { LayoutSettings } from '../../../../services/layout/browser/layoutService.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; +import { updateTerminalFontRendering } from './terminalFontRendering.js'; const enum RenderConstants { SmoothScrollDuration = 125 @@ -505,6 +506,8 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this.raw.open(container); } + updateTerminalFontRendering(this.raw, this._terminalConfigurationService.config.fontRendering); + // TODO: Move before open so the DOM renderer doesn't initialize if (options.enableGpu) { if (this._shouldLoadWebgl()) { @@ -589,6 +592,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach updateConfig(): void { const config = this._terminalConfigurationService.config; + updateTerminalFontRendering(this.raw, config.fontRendering); this.raw.options.altClickMovesCursor = config.altClickMovesCursor; this._setCursorBlink(config.cursorBlinking); this._setTextBlinking(config.textBlinking); diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index f1bcb84e4d7009..54adc18b068302 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -156,6 +156,7 @@ export interface ITerminalConfiguration { drawBoldTextInBrightColors: boolean; fastScrollSensitivity: number; fontFamily: string; + fontRendering: 'inherit' | 'crisp'; fontWeight: FontWeight; fontWeightBold: FontWeight; minimumContrastRatio: number; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 80c5267d6751d0..4c1228152252bc 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -180,6 +180,17 @@ const terminalConfiguration: IStringDictionary = { markdownDescription: localize('terminal.integrated.fontFamily', "Controls the font family of the terminal. Defaults to {0}'s value.", '`#editor.fontFamily#`'), type: 'string', }, + [TerminalSettingId.FontRendering]: { + markdownDescription: localize('terminal.integrated.fontRendering', "Controls the font rendering style of terminal text on macOS without changing theme colors, font size, or font weight."), + type: 'string', + enum: ['inherit', 'crisp'], + markdownEnumDescriptions: [ + localize('terminal.integrated.fontRendering.inherit', "Use the workbench font smoothing configured by {0}.", '`#workbench.fontAliasing#`'), + localize('terminal.integrated.fontRendering.crisp', "Use pixel-level antialiasing on high-DPI displays. Text may appear lighter and more defined. On other displays, inherit the workbench font smoothing.") + ], + default: 'inherit', + included: isMacintosh + }, [TerminalSettingId.FontLigaturesEnabled]: { markdownDescription: localize('terminal.integrated.fontLigatures.enabled', "Controls whether font ligatures are enabled in the terminal. Ligatures will only work if the configured {0} supports them.", `\`#${TerminalSettingId.FontFamily}#\``), type: 'boolean', diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts index 66e976ccf9a5b9..36086a2907d2fa 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts @@ -10,6 +10,7 @@ import { timeout } from '../../../../../../base/common/async.js'; import { Color, RGBA } from '../../../../../../base/common/color.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { isMacintosh } from '../../../../../../base/common/platform.js'; import { mock } from '../../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js'; @@ -28,6 +29,7 @@ import { registerColors, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_C import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; import { TestLifecycleService } from '../../../../../test/common/workbenchTestServices.js'; import { TestWebglAddon, TestXtermAddonImporter } from './xtermTestUtils.js'; +import { stub } from 'sinon'; registerColors(); @@ -53,6 +55,7 @@ export class TestViewDescriptorService implements Partial = { fontFamily: 'monospace', + fontRendering: 'inherit', fontWeight: 'normal', fontWeightBold: 'normal', gpuAcceleration: 'off', @@ -125,6 +128,86 @@ suite('XtermTerminal', () => { strictEqual(xterm.raw.rows, 30); }); + suite('fontRendering', () => { + async function setFontRendering(fontRendering: 'inherit' | 'crisp'): Promise { + await configurationService.setUserConfiguration('terminal.integrated', { + ...defaultTerminalConfig, + fontRendering + }); + configurationService.onDidChangeConfigurationEmitter.fire(new class extends mock() { + override affectsConfiguration(section: string): boolean { + return section.startsWith('terminal.integrated'); + } + }); + } + + function attach(terminal: XtermTerminal = xterm): HTMLElement { + const container = document.createElement('div'); + document.body.appendChild(container); + store.add(toDisposable(() => container.remove())); + terminal.attachToElement(container, { enableGpu: false }); + return container; + } + + test('inherits the workbench font policy by default', () => { + attach(); + strictEqual(xterm.raw.element!.classList.contains('terminal-font-rendering-crisp'), false); + }); + + test('applies the configured policy when the terminal is opened', async () => { + await setFontRendering('crisp'); + attach(); + strictEqual(xterm.raw.element!.classList.contains('terminal-font-rendering-crisp'), isMacintosh); + }); + + test('refreshes the atlas after changing the policy without changing font options', async () => { + attach(); + const initialOptions = { + fontFamily: xterm.raw.options.fontFamily, + fontSize: xterm.raw.options.fontSize, + fontWeight: xterm.raw.options.fontWeight, + theme: xterm.raw.options.theme + }; + const statesAtRedraw: boolean[] = []; + const listener = stub(xterm.raw, 'clearTextureAtlas').callsFake(() => { + statesAtRedraw.push(xterm.raw.element!.classList.contains('terminal-font-rendering-crisp')); + }); + store.add(toDisposable(() => listener.restore())); + await setFontRendering('crisp'); + await setFontRendering('crisp'); + await setFontRendering('inherit'); + deepStrictEqual({ + statesAtRedraw, + fontFamily: xterm.raw.options.fontFamily, + fontSize: xterm.raw.options.fontSize, + fontWeight: xterm.raw.options.fontWeight, + theme: xterm.raw.options.theme + }, { + statesAtRedraw: isMacintosh ? [true, false] : [], + ...initialOptions + }); + }); + + test('applies updates forwarded to detached terminals', async () => { + const terminal = store.add(instantiationService.createInstance(XtermTerminal, undefined, XTermBaseCtor, { + cols: 80, + rows: 30, + xtermColorProvider: { getBackgroundColor: () => undefined }, + capabilities: store.add(new TerminalCapabilityStore()), + disableShellIntegrationReporting: true, + xtermAddonImporter: new TestXtermAddonImporter(), + detached: true + }, undefined)); + attach(terminal); + await setFontRendering('crisp'); + terminal.updateConfig(); + strictEqual(terminal.raw.element!.classList.contains('terminal-font-rendering-crisp'), isMacintosh); + await setFontRendering('inherit'); + terminal.updateConfig(); + strictEqual(terminal.raw.element!.classList.contains('terminal-font-rendering-crisp'), false); + }); + }); + test('detached terminals do not register decoration shutdown listeners', () => { const listenerCountAfterRegularXterm = listenerCount(onWillShutdown); for (let index = 0; index < 50; index++) { diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index 01ce5480db2bbb..0f0f7941fc89be 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts @@ -30,6 +30,7 @@ import { terminalStrings } from '../../../terminal/common/terminalStrings.js'; import { TerminalStickyScrollSettingId } from '../common/terminalStickyScrollConfiguration.js'; import { terminalStickyScrollBackground, terminalStickyScrollHoverBackground } from './terminalStickyScrollColorRegistry.js'; import { XtermAddonImporter } from '../../../terminal/browser/xterm/xtermAddonImporter.js'; +import { updateTerminalFontRendering } from '../../../terminal/browser/xterm/terminalFontRendering.js'; const enum OverlayState { /** Initial state/disabled by the alt buffer. */ @@ -422,6 +423,7 @@ export class TerminalStickyScrollOverlay extends Disposable { } this._stickyScrollOverlay.open(this._element); + updateTerminalFontRendering(this._stickyScrollOverlay, this._terminalConfigurationService.config.fontRendering); // Prevent tab key from being handled by the xterm overlay to allow natural tab navigation this._stickyScrollOverlay.attachCustomKeyEventHandler((event: KeyboardEvent) => { @@ -476,6 +478,7 @@ export class TerminalStickyScrollOverlay extends Disposable { } this._stickyScrollOverlay.resize(this._xterm.raw.cols, this._stickyScrollOverlay.rows); this._stickyScrollOverlay.options = this._getOptions(); + updateTerminalFontRendering(this._stickyScrollOverlay, this._terminalConfigurationService.config.fontRendering); this._refreshGpuAcceleration(); }