Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable wco on macOS #239666

Merged
merged 5 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/vs/platform/native/common/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ export interface ICommonNativeHostService {
getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }>;

isMaximized(options?: INativeHostOptions): Promise<boolean>;
maximizeWindow(options?: INativeHostOptions): Promise<void>;
unmaximizeWindow(options?: INativeHostOptions): Promise<void>;
minimizeWindow(options?: INativeHostOptions): Promise<void>;
moveWindowTop(options?: INativeHostOptions): Promise<void>;
positionWindow(position: IRectangle, options?: INativeHostOptions): Promise<void>;

Expand Down
15 changes: 0 additions & 15 deletions src/vs/platform/native/electron-main/nativeHostMainService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,21 +262,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
return window?.win?.isMaximized() ?? false;
}

async maximizeWindow(windowId: number | undefined, options?: INativeHostOptions): Promise<void> {
const window = this.windowById(options?.targetWindowId, windowId);
window?.win?.maximize();
}

async unmaximizeWindow(windowId: number | undefined, options?: INativeHostOptions): Promise<void> {
const window = this.windowById(options?.targetWindowId, windowId);
window?.win?.unmaximize();
}

async minimizeWindow(windowId: number | undefined, options?: INativeHostOptions): Promise<void> {
const window = this.windowById(options?.targetWindowId, windowId);
window?.win?.minimize();
}

async moveWindowTop(windowId: number | undefined, options?: INativeHostOptions): Promise<void> {
const window = this.windowById(options?.targetWindowId, windowId);
window?.win?.moveTop();
Expand Down
15 changes: 3 additions & 12 deletions src/vs/platform/window/common/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ export interface IWindowSettings {
readonly clickThroughInactive: boolean;
readonly newWindowProfile: string;
readonly density: IDensitySettings;
readonly experimentalControlOverlay?: boolean;
}

export interface IDensitySettings {
Expand Down Expand Up @@ -239,23 +238,15 @@ export function getTitleBarStyle(configurationService: IConfigurationService): T
export const DEFAULT_CUSTOM_TITLEBAR_HEIGHT = 35; // includes space for command center

export function useWindowControlsOverlay(configurationService: IConfigurationService): boolean {
if (isMacintosh || isWeb) {
return false; // only supported on a Windows/Linux desktop instances
if (isWeb) {
return false; // only supported on desktop instances
}

if (hasNativeTitlebar(configurationService)) {
return false; // only supported when title bar is custom
}

if (isLinux) {
const setting = configurationService.getValue('window.experimentalControlOverlay');
if (typeof setting === 'boolean') {
return setting;
}
}

// Default to true.
return true;
return true; // default
}

export function useNativeFullScreen(configurationService: IConfigurationService): boolean {
Expand Down
10 changes: 4 additions & 6 deletions src/vs/platform/windows/electron-main/windowImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow {
}

// Update the window controls immediately based on cached or default values
if (useCustomTitleStyle && (useWindowControlsOverlay(this.configurationService) || isMacintosh)) {
if (useCustomTitleStyle && useWindowControlsOverlay(this.configurationService)) {
const cachedWindowControlHeight = this.stateService.getItem<number>((BaseWindow.windowControlHeightStateStorageKey));
if (cachedWindowControlHeight) {
this.updateWindowControls({ height: cachedWindowControlHeight });
Expand Down Expand Up @@ -318,8 +318,6 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow {

private static readonly windowControlHeightStateStorageKey = 'windowControlHeight';

private readonly hasWindowControlOverlay = useWindowControlsOverlay(this.configurationService);

updateWindowControls(options: { height?: number; backgroundColor?: string; foregroundColor?: string }): void {
const win = this.win;
if (!win) {
Expand All @@ -331,16 +329,16 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow {
this.stateService.setItem((CodeWindow.windowControlHeightStateStorageKey), options.height);
}

// Windows/Linux: window control overlay (WCO)
if (this.hasWindowControlOverlay) {
// Windows/Linux: update window controls via setTitleBarOverlay()
if (!isMacintosh && useWindowControlsOverlay(this.configurationService)) {
win.setTitleBarOverlay({
color: options.backgroundColor?.trim() === '' ? undefined : options.backgroundColor,
symbolColor: options.foregroundColor?.trim() === '' ? undefined : options.foregroundColor,
height: options.height ? options.height - 1 : undefined // account for window border
});
}

// macOS: traffic lights
// macOS: update window controls via setWindowButtonPosition()
else if (isMacintosh && options.height !== undefined) {
// The traffic lights have a height of 12px. There's an invisible margin
// of 2px at the top and bottom, and 1px on the left and right. Therefore,
Expand Down
2 changes: 2 additions & 0 deletions src/vs/platform/windows/electron-main/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt
// This logic will not perfectly guess the right colors
// to use on initialization, but prefer to keep things
// simple as it is temporary and not noticeable
// On macOS, only the presence of `titleBarOverlay` is
// considered, the properties are ignored.

const titleBarColor = themeMainService.getWindowSplash(undefined)?.colorInfo.titleBarBackground ?? themeMainService.getBackgroundColor();
const symbolColor = Color.fromHex(titleBarColor).isDarker() ? '#FFFFFF' : '#000000';
Expand Down
5 changes: 4 additions & 1 deletion src/vs/workbench/browser/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
if (
isWeb ||
isWindows || // not working well with zooming (border often not visible)
useWindowControlsOverlay(this.configurationService) || // not working with WCO (border cannot draw over the overlay)
(
(isWindows || isLinux) &&
useWindowControlsOverlay(this.configurationService) // Windows/Linux: not working with WCO (border cannot draw over the overlay)
) ||
hasNativeTitlebar(this.configurationService)
) {
return;
Expand Down
51 changes: 0 additions & 51 deletions src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css
Original file line number Diff line number Diff line change
Expand Up @@ -231,21 +231,6 @@
zoom: var(--zoom-factor); /* helps to position the menu properly when counter zooming */
}

/* Resizer */
.monaco-workbench.windows .part.titlebar > .titlebar-container > .resizer,
.monaco-workbench.linux .part.titlebar > .titlebar-container > .resizer {
-webkit-app-region: no-drag;
position: absolute;
top: 0;
width: 100%;
height: 4px;
}

.monaco-workbench.windows.fullscreen .part.titlebar > .titlebar-container > .resizer,
.monaco-workbench.linux.fullscreen .part.titlebar > .titlebar-container > .resizer {
display: none;
}

/* App Icon */
.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-left > .window-appicon {
width: 35px;
Expand Down Expand Up @@ -332,42 +317,6 @@
width: 70px;
}

/* Window Control Icons */
.monaco-workbench .part.titlebar .window-controls-container > .window-icon {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 46px;
font-size: 16px;
color: var(--vscode-titleBar-activeForeground);
}

.monaco-workbench .part.titlebar.inactive .window-controls-container > .window-icon {
color: var(--vscode-titleBar-inactiveForeground);
}

.monaco-workbench .part.titlebar .window-controls-container > .window-icon::before {
height: 16px;
line-height: 16px;
}

.monaco-workbench .part.titlebar .window-controls-container > .window-icon:hover {
background-color: rgba(255, 255, 255, 0.1);
}

.monaco-workbench .part.titlebar.light .window-controls-container > .window-icon:hover {
background-color: rgba(0, 0, 0, 0.1);
}

.monaco-workbench .part.titlebar .window-controls-container > .window-icon.window-close:hover {
background-color: rgba(232, 17, 35, 0.9);
}

.monaco-workbench .part.titlebar .window-controls-container .window-icon.window-close:hover {
color: white;
}

/* Action Tool Bar Controls */
.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-right > .action-toolbar-container {
display: none;
Expand Down
5 changes: 2 additions & 3 deletions src/vs/workbench/browser/parts/titlebar/titlebarPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,6 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart {
//#endregion

protected rootContainer!: HTMLElement;
protected windowControlsContainer: HTMLElement | undefined;
protected dragRegion: HTMLElement | undefined;
private title!: HTMLElement;

Expand Down Expand Up @@ -482,15 +481,15 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart {
// container helps with allowing to move the window when clicking very close to the
// window control buttons.
} else {
this.windowControlsContainer = append(primaryWindowControlsLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container'));
const windowControlsContainer = append(primaryWindowControlsLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container'));
if (isWeb) {
// Web: its possible to have control overlays on both sides, for example on macOS
// with window controls on the left and PWA controls on the right.
append(primaryWindowControlsLocation === 'left' ? this.rightContent : this.leftContent, $('div.window-controls-container'));
}

if (isWCOEnabled()) {
this.windowControlsContainer.classList.add('wco-enabled');
windowControlsContainer.classList.add('wco-enabled');
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo
'window.nativeTabs',
'window.nativeFullScreen',
'window.clickThroughInactive',
'window.experimentalControlOverlay',
'update.mode',
'editor.accessibilitySupport',
'security.workspace.trust.enabled',
Expand All @@ -55,7 +54,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo
private readonly nativeTabs = new ChangeObserver('boolean');
private readonly nativeFullScreen = new ChangeObserver('boolean');
private readonly clickThroughInactive = new ChangeObserver('boolean');
private readonly linuxWindowControlOverlay = new ChangeObserver('boolean');
private readonly updateMode = new ChangeObserver('string');
private accessibilitySupport: 'on' | 'off' | 'auto' | undefined;
private readonly workspaceTrustEnabled = new ChangeObserver('boolean');
Expand Down Expand Up @@ -119,9 +117,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo
// macOS: Click through (accept first mouse)
processChanged(isMacintosh && this.clickThroughInactive.handleChange(config.window?.clickThroughInactive));

// Linux: WCO
processChanged(isLinux && this.linuxWindowControlOverlay.handleChange(config.window?.experimentalControlOverlay));

// Update mode
processChanged(this.updateMode.handleChange(config.update?.mode));

Expand Down
7 changes: 0 additions & 7 deletions src/vs/workbench/electron-sandbox/desktop.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,6 @@ import { LinuxCustomTitlebarExperiment } from './parts/titlebar/titlebarPart.js'
'scope': ConfigurationScope.APPLICATION,
'description': localize('titleBarStyle', "Adjust the appearance of the window title bar to be native by the OS or custom. On Linux and Windows, this setting also affects the application and context menu appearances. Changes require a full restart to apply."),
},
'window.experimentalControlOverlay': {
'type': 'boolean',
'included': isLinux,
'markdownDescription': localize('window.experimentalControlOverlay', "Show the native window controls when {0} is set to `custom` (Linux only).", '`#window.titleBarStyle#`'),
'default': true,
'scope': ConfigurationScope.APPLICATION,
},
'window.customTitleBarVisibility': {
'type': 'string',
'enum': ['auto', 'windowed', 'never'],
Expand Down
72 changes: 4 additions & 68 deletions src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Event } from '../../../../base/common/event.js';
import { getZoomFactor, isWCOEnabled } from '../../../../base/browser/browser.js';
import { $, addDisposableListener, append, EventType, getWindow, getWindowId, hide, show } from '../../../../base/browser/dom.js';
import { getZoomFactor } from '../../../../base/browser/browser.js';
import { addDisposableListener, EventType, getWindow, getWindowId, hide, show } from '../../../../base/browser/dom.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IConfigurationService, IConfigurationChangeEvent } from '../../../../platform/configuration/common/configuration.js';
import { IStorageService } from '../../../../platform/storage/common/storage.js';
import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-sandbox/environmentService.js';
import { IHostService } from '../../../services/host/browser/host.js';
import { isMacintosh, isWindows, isLinux, isNative, isBigSurOrNewer } from '../../../../base/common/platform.js';
import { isMacintosh, isWindows, isLinux, isBigSurOrNewer } from '../../../../base/common/platform.js';
import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';
import { BrowserTitlebarPart as BrowserTitlebarPart, BrowserTitleService, IAuxiliaryTitlebarPart } from '../../../browser/parts/titlebar/titlebarPart.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
Expand All @@ -20,8 +19,6 @@ import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser
import { INativeHostService } from '../../../../platform/native/common/native.js';
import { hasNativeTitlebar, useWindowControlsOverlay, DEFAULT_CUSTOM_TITLEBAR_HEIGHT } from '../../../../platform/window/common/window.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { NativeMenubarControl } from './menubarControl.js';
import { IEditorGroupsContainer, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
Expand Down Expand Up @@ -56,8 +53,6 @@ export class NativeTitlebarPart extends BrowserTitlebarPart {

//#endregion

private maxRestoreControl: HTMLElement | undefined;
private resizer: HTMLElement | undefined;
private cachedWindowControlStyles: { bgColor: string; fgColor: string } | undefined;
private cachedWindowControlHeight: number | undefined;

Expand Down Expand Up @@ -160,41 +155,6 @@ export class NativeTitlebarPart extends BrowserTitlebarPart {
})));
}

// Window Controls (Native Linux when WCO is disabled)
if (isLinux && !hasNativeTitlebar(this.configurationService) && !isWCOEnabled() && this.windowControlsContainer) {

// Minimize
const minimizeIcon = append(this.windowControlsContainer, $('div.window-icon.window-minimize' + ThemeIcon.asCSSSelector(Codicon.chromeMinimize)));
this._register(addDisposableListener(minimizeIcon, EventType.CLICK, () => {
this.nativeHostService.minimizeWindow({ targetWindowId });
}));

// Restore
this.maxRestoreControl = append(this.windowControlsContainer, $('div.window-icon.window-max-restore'));
this._register(addDisposableListener(this.maxRestoreControl, EventType.CLICK, async () => {
const maximized = await this.nativeHostService.isMaximized({ targetWindowId });
if (maximized) {
return this.nativeHostService.unmaximizeWindow({ targetWindowId });
}

return this.nativeHostService.maximizeWindow({ targetWindowId });
}));

// Close
const closeIcon = append(this.windowControlsContainer, $('div.window-icon.window-close' + ThemeIcon.asCSSSelector(Codicon.chromeClose)));
this._register(addDisposableListener(closeIcon, EventType.CLICK, () => {
this.nativeHostService.closeWindow({ targetWindowId });
}));

// Resizer
this.resizer = append(this.rootContainer, $('div.resizer'));
this._register(Event.runAndSubscribe(this.layoutService.onDidChangeWindowMaximized, ({ windowId, maximized }) => {
if (windowId === targetWindowId) {
this.onDidChangeWindowMaximized(maximized);
}
}, { windowId: targetWindowId, maximized: this.layoutService.isWindowMaximized(targetWindow) }));
}

// Window System Context Menu
// See https://github.com/electron/electron/issues/24893
if (isWindows && !hasNativeTitlebar(this.configurationService)) {
Expand All @@ -211,30 +171,9 @@ export class NativeTitlebarPart extends BrowserTitlebarPart {
return result;
}

private onDidChangeWindowMaximized(maximized: boolean): void {
if (this.maxRestoreControl) {
if (maximized) {
this.maxRestoreControl.classList.remove(...ThemeIcon.asClassNameArray(Codicon.chromeMaximize));
this.maxRestoreControl.classList.add(...ThemeIcon.asClassNameArray(Codicon.chromeRestore));
} else {
this.maxRestoreControl.classList.remove(...ThemeIcon.asClassNameArray(Codicon.chromeRestore));
this.maxRestoreControl.classList.add(...ThemeIcon.asClassNameArray(Codicon.chromeMaximize));
}
}

if (this.resizer) {
if (maximized) {
hide(this.resizer);
} else {
show(this.resizer);
}
}
}

override updateStyles(): void {
super.updateStyles();

// WCO styles only supported on Windows currently
if (useWindowControlsOverlay(this.configurationService)) {
if (
!this.cachedWindowControlStyles ||
Expand All @@ -253,10 +192,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart {
override layout(width: number, height: number): void {
super.layout(width, height);

if (
useWindowControlsOverlay(this.configurationService) ||
(isMacintosh && isNative && !hasNativeTitlebar(this.configurationService))
) {
if (useWindowControlsOverlay(this.configurationService)) {

// When the user goes into full screen mode, the height of the title bar becomes 0.
// Instead, set it back to the default titlebar height for Catalina users
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,6 @@ export class TestNativeHostService implements INativeHostService {
async toggleFullScreen(): Promise<void> { }
async isMaximized(): Promise<boolean> { return true; }
async isFullScreen(): Promise<boolean> { return true; }
async maximizeWindow(): Promise<void> { }
async unmaximizeWindow(): Promise<void> { }
async minimizeWindow(): Promise<void> { }
async moveWindowTop(options?: INativeHostOptions): Promise<void> { }
getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }> { throw new Error('Method not implemented.'); }
async positionWindow(position: IRectangle, options?: INativeHostOptions): Promise<void> { }
Expand Down
Loading