From 8d43dd606a44399c5d54410a4d6a675ae7d2a5a4 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Fri, 13 Feb 2026 13:37:03 +0000 Subject: [PATCH] Add support for reduced transparency in accessibility settings --- extensions/theme-2026/themes/styles.css | 164 ++++++++++++++++++ .../browser/accessibilityService.ts | 40 +++++ .../accessibility/common/accessibility.ts | 2 + .../test/browser/accessibilityService.test.ts | 112 ++++++++++++ .../test/common/testAccessibilityService.ts | 2 + .../browser/workbench.contribution.ts | 12 ++ 6 files changed, 332 insertions(+) create mode 100644 src/vs/platform/accessibility/test/browser/accessibilityService.test.ts diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 35fe38ae75a..b94ea198c4c 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -682,3 +682,167 @@ opacity: 1; color: var(--vscode-descriptionForeground); } + +/* ============================================================================================ + * Reduced Transparency - disable backdrop-filter blur and color-mix transparency effects + * for improved rendering performance. Controlled by workbench.reduceTransparency setting. + * ============================================================================================ */ + +/* Reset blur variables to none */ +.monaco-workbench.monaco-reduce-transparency { + --backdrop-blur-sm: none; + --backdrop-blur-md: none; + --backdrop-blur-lg: none; +} + +/* Quick Input (Command Palette) */ +.monaco-workbench.monaco-reduce-transparency .quick-input-widget { + background-color: var(--vscode-quickInput-background) !important; + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +/* Notifications */ +.monaco-workbench.monaco-reduce-transparency .notification-toast-container { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: var(--vscode-notifications-background) !important; +} + +.monaco-workbench.monaco-reduce-transparency .notifications-center { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: var(--vscode-notifications-background) !important; +} + +/* Context Menu / Action Widget */ +.monaco-workbench.monaco-reduce-transparency .action-widget { + background: var(--vscode-menu-background) !important; + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +/* Suggest Widget */ +.monaco-workbench.monaco-reduce-transparency .monaco-editor .suggest-widget { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: var(--vscode-editorSuggestWidget-background) !important; +} + +/* Find Widget */ +.monaco-workbench.monaco-reduce-transparency .monaco-editor .find-widget { + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +.monaco-workbench.monaco-reduce-transparency .inline-chat-gutter-menu { + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +/* Dialog */ +.monaco-workbench.monaco-reduce-transparency .monaco-dialog-box { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: var(--vscode-editor-background) !important; +} + +/* Peek View */ +.monaco-workbench.monaco-reduce-transparency .monaco-editor .peekview-widget { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: var(--vscode-peekViewEditor-background) !important; +} + +/* Hover */ +.monaco-reduce-transparency .monaco-hover { + background-color: var(--vscode-editorHoverWidget-background) !important; + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +.monaco-reduce-transparency .monaco-hover.workbench-hover, +.monaco-reduce-transparency .workbench-hover { + background-color: var(--vscode-editorHoverWidget-background) !important; + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; +} + +/* Keybinding Widget */ +.monaco-workbench.monaco-reduce-transparency .defineKeybindingWidget { + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +/* Chat Editor Overlay */ +.monaco-workbench.monaco-reduce-transparency .chat-editor-overlay-widget, +.monaco-workbench.monaco-reduce-transparency .chat-diff-change-content-widget { + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +/* Debug Toolbar */ +.monaco-workbench.monaco-reduce-transparency .debug-toolbar { + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; +} + +.monaco-workbench.monaco-reduce-transparency .debug-hover-widget { + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +/* Parameter Hints */ +.monaco-workbench.monaco-reduce-transparency .monaco-editor .parameter-hints-widget { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: var(--vscode-editorWidget-background) !important; +} + +/* Sticky Scroll */ +.monaco-workbench.monaco-reduce-transparency .monaco-editor .sticky-widget { + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; + background: var(--vscode-editor-background) !important; +} + +.monaco-workbench.monaco-reduce-transparency .monaco-editor .sticky-widget .sticky-widget-lines { + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; + background: var(--vscode-editor-background) !important; +} + +/* Rename Box */ +.monaco-reduce-transparency .monaco-editor .rename-box.preview { + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; +} + +/* Notebook */ +.monaco-workbench.monaco-reduce-transparency .notebookOverlay .monaco-list-row .cell-title-toolbar { + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +/* Command Center */ +.monaco-workbench.monaco-reduce-transparency .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { + background: var(--vscode-commandCenter-background) !important; + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +.monaco-workbench.monaco-reduce-transparency .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover { + background: var(--vscode-commandCenter-activeBackground) !important; +} + +/* Breadcrumbs */ +.monaco-workbench.monaco-reduce-transparency .breadcrumbs-picker-widget { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: var(--vscode-breadcrumbPicker-background) !important; +} + +/* Quick Input filter input */ +.monaco-workbench.monaco-reduce-transparency .quick-input-widget .quick-input-filter .monaco-inputbox { + background: var(--vscode-input-background) !important; +} diff --git a/src/vs/platform/accessibility/browser/accessibilityService.ts b/src/vs/platform/accessibility/browser/accessibilityService.ts index d6db2a65b33..35f6639e269 100644 --- a/src/vs/platform/accessibility/browser/accessibilityService.ts +++ b/src/vs/platform/accessibility/browser/accessibilityService.ts @@ -24,6 +24,10 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe protected _systemMotionReduced: boolean; protected readonly _onDidChangeReducedMotion = this._register(new Emitter()); + protected _configTransparencyReduced: 'auto' | 'on' | 'off'; + protected _systemTransparencyReduced: boolean; + protected readonly _onDidChangeReducedTransparency = this._register(new Emitter()); + private _linkUnderlinesEnabled: boolean; protected readonly _onDidChangeLinkUnderline = this._register(new Emitter()); @@ -45,6 +49,10 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe this._configMotionReduced = this._configurationService.getValue('workbench.reduceMotion'); this._onDidChangeReducedMotion.fire(); } + if (e.affectsConfiguration('workbench.reduceTransparency')) { + this._configTransparencyReduced = this._configurationService.getValue('workbench.reduceTransparency'); + this._onDidChangeReducedTransparency.fire(); + } })); updateContextKey(); this._register(this.onDidChangeScreenReaderOptimized(() => updateContextKey())); @@ -53,9 +61,14 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe this._systemMotionReduced = reduceMotionMatcher.matches; this._configMotionReduced = this._configurationService.getValue<'auto' | 'on' | 'off'>('workbench.reduceMotion'); + const reduceTransparencyMatcher = mainWindow.matchMedia(`(prefers-reduced-transparency: reduce)`); + this._systemTransparencyReduced = reduceTransparencyMatcher.matches; + this._configTransparencyReduced = this._configurationService.getValue<'auto' | 'on' | 'off'>('workbench.reduceTransparency'); + this._linkUnderlinesEnabled = this._configurationService.getValue('accessibility.underlineLinks'); this.initReducedMotionListeners(reduceMotionMatcher); + this.initReducedTransparencyListeners(reduceTransparencyMatcher); this.initLinkUnderlineListeners(); } @@ -78,6 +91,24 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe this._register(this.onDidChangeReducedMotion(() => updateRootClasses())); } + private initReducedTransparencyListeners(reduceTransparencyMatcher: MediaQueryList) { + + this._register(addDisposableListener(reduceTransparencyMatcher, 'change', () => { + this._systemTransparencyReduced = reduceTransparencyMatcher.matches; + if (this._configTransparencyReduced === 'auto') { + this._onDidChangeReducedTransparency.fire(); + } + })); + + const updateRootClasses = () => { + const reduce = this.isTransparencyReduced(); + this._layoutService.mainContainer.classList.toggle('monaco-reduce-transparency', reduce); + }; + + updateRootClasses(); + this._register(this.onDidChangeReducedTransparency(() => updateRootClasses())); + } + private initLinkUnderlineListeners() { this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('accessibility.underlineLinks')) { @@ -119,6 +150,15 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe return config === 'on' || (config === 'auto' && this._systemMotionReduced); } + get onDidChangeReducedTransparency(): Event { + return this._onDidChangeReducedTransparency.event; + } + + isTransparencyReduced(): boolean { + const config = this._configTransparencyReduced; + return config === 'on' || (config === 'auto' && this._systemTransparencyReduced); + } + alwaysUnderlineAccessKeys(): Promise { return Promise.resolve(false); } diff --git a/src/vs/platform/accessibility/common/accessibility.ts b/src/vs/platform/accessibility/common/accessibility.ts index 1757eb84e02..741d5fffc5b 100644 --- a/src/vs/platform/accessibility/common/accessibility.ts +++ b/src/vs/platform/accessibility/common/accessibility.ts @@ -14,10 +14,12 @@ export interface IAccessibilityService { readonly onDidChangeScreenReaderOptimized: Event; readonly onDidChangeReducedMotion: Event; + readonly onDidChangeReducedTransparency: Event; alwaysUnderlineAccessKeys(): Promise; isScreenReaderOptimized(): boolean; isMotionReduced(): boolean; + isTransparencyReduced(): boolean; getAccessibilitySupport(): AccessibilitySupport; setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void; alert(message: string): void; diff --git a/src/vs/platform/accessibility/test/browser/accessibilityService.test.ts b/src/vs/platform/accessibility/test/browser/accessibilityService.test.ts new file mode 100644 index 00000000000..869869d1050 --- /dev/null +++ b/src/vs/platform/accessibility/test/browser/accessibilityService.test.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { Event } from '../../../../base/common/event.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { TestInstantiationService } from '../../../instantiation/test/common/instantiationServiceMock.js'; +import { IConfigurationService, IConfigurationChangeEvent } from '../../../configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; +import { IContextKeyService } from '../../../contextkey/common/contextkey.js'; +import { MockContextKeyService } from '../../../keybinding/test/common/mockKeybindingService.js'; +import { ILayoutService } from '../../../layout/browser/layoutService.js'; +import { AccessibilityService } from '../../browser/accessibilityService.js'; + +suite('AccessibilityService', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let configurationService: TestConfigurationService; + let container: HTMLElement; + + function createService(config: Record = {}): AccessibilityService { + const instantiationService = store.add(new TestInstantiationService()); + + configurationService = new TestConfigurationService({ + 'editor.accessibilitySupport': 'off', + 'workbench.reduceMotion': 'off', + 'workbench.reduceTransparency': 'off', + 'accessibility.underlineLinks': false, + ...config, + }); + instantiationService.stub(IConfigurationService, configurationService); + + instantiationService.stub(IContextKeyService, store.add(new MockContextKeyService())); + + container = document.createElement('div'); + instantiationService.stub(ILayoutService, { + mainContainer: container, + activeContainer: container, + getContainer() { return container; }, + onDidLayoutContainer: Event.None, + }); + + return store.add(instantiationService.createInstance(AccessibilityService)); + } + + suite('isTransparencyReduced', () => { + + test('returns false when config is off', () => { + const service = createService({ 'workbench.reduceTransparency': 'off' }); + assert.strictEqual(service.isTransparencyReduced(), false); + }); + + test('returns true when config is on', () => { + const service = createService({ 'workbench.reduceTransparency': 'on' }); + assert.strictEqual(service.isTransparencyReduced(), true); + }); + + test('adds CSS class when config is on', () => { + createService({ 'workbench.reduceTransparency': 'on' }); + assert.strictEqual(container.classList.contains('monaco-reduce-transparency'), true); + }); + + test('does not add CSS class when config is off', () => { + createService({ 'workbench.reduceTransparency': 'off' }); + assert.strictEqual(container.classList.contains('monaco-reduce-transparency'), false); + }); + + test('fires event and updates class on config change', () => { + const service = createService({ 'workbench.reduceTransparency': 'off' }); + assert.strictEqual(service.isTransparencyReduced(), false); + + let fired = false; + store.add(service.onDidChangeReducedTransparency(() => { fired = true; })); + + // Simulate config change + configurationService.setUserConfiguration('workbench.reduceTransparency', 'on'); + configurationService.onDidChangeConfigurationEmitter.fire({ + affectsConfiguration(id: string) { return id === 'workbench.reduceTransparency'; }, + } satisfies Partial as unknown as IConfigurationChangeEvent); + + assert.strictEqual(fired, true); + assert.strictEqual(service.isTransparencyReduced(), true); + assert.strictEqual(container.classList.contains('monaco-reduce-transparency'), true); + }); + }); + + suite('isMotionReduced', () => { + + test('returns false when config is off', () => { + const service = createService({ 'workbench.reduceMotion': 'off' }); + assert.strictEqual(service.isMotionReduced(), false); + }); + + test('returns true when config is on', () => { + const service = createService({ 'workbench.reduceMotion': 'on' }); + assert.strictEqual(service.isMotionReduced(), true); + }); + + test('adds CSS classes when config is on', () => { + createService({ 'workbench.reduceMotion': 'on' }); + assert.strictEqual(container.classList.contains('monaco-reduce-motion'), true); + assert.strictEqual(container.classList.contains('monaco-enable-motion'), false); + }); + + test('adds CSS classes when config is off', () => { + createService({ 'workbench.reduceMotion': 'off' }); + assert.strictEqual(container.classList.contains('monaco-reduce-motion'), false); + assert.strictEqual(container.classList.contains('monaco-enable-motion'), true); + }); + }); +}); diff --git a/src/vs/platform/accessibility/test/common/testAccessibilityService.ts b/src/vs/platform/accessibility/test/common/testAccessibilityService.ts index 4f21111492e..6ef551ba9f2 100644 --- a/src/vs/platform/accessibility/test/common/testAccessibilityService.ts +++ b/src/vs/platform/accessibility/test/common/testAccessibilityService.ts @@ -12,9 +12,11 @@ export class TestAccessibilityService implements IAccessibilityService { onDidChangeScreenReaderOptimized = Event.None; onDidChangeReducedMotion = Event.None; + onDidChangeReducedTransparency = Event.None; isScreenReaderOptimized(): boolean { return false; } isMotionReduced(): boolean { return true; } + isTransparencyReduced(): boolean { return false; } alwaysUnderlineAccessKeys(): Promise { return Promise.resolve(false); } setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { } getAccessibilitySupport(): AccessibilitySupport { return AccessibilitySupport.Unknown; } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index a54437a2633..b1c5637a750 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -715,6 +715,18 @@ const registry = Registry.as(ConfigurationExtensions.Con tags: ['accessibility'], enum: ['on', 'off', 'auto'] }, + 'workbench.reduceTransparency': { + type: 'string', + description: localize('workbench.reduceTransparency', "Controls whether the workbench should render with fewer transparency and blur effects for improved performance."), + 'enumDescriptions': [ + localize('workbench.reduceTransparency.on', "Always render without transparency and blur effects."), + localize('workbench.reduceTransparency.off', "Do not reduce transparency and blur effects."), + localize('workbench.reduceTransparency.auto', "Reduce transparency and blur effects based on OS configuration."), + ], + default: 'off', + tags: ['accessibility'], + enum: ['on', 'off', 'auto'] + }, 'workbench.navigationControl.enabled': { 'type': 'boolean', 'default': true,