diff --git a/src/vs/code/electron-sandbox/workbench/workbench.ts b/src/vs/code/electron-sandbox/workbench/workbench.ts index 47c3d28a6fcd..44a6051573c0 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.ts +++ b/src/vs/code/electron-sandbox/workbench/workbench.ts @@ -90,15 +90,19 @@ splash.className = baseTheme ?? 'vs-dark'; if (layoutInfo.windowBorder && colorInfo.windowBorder) { - splash.style.position = 'relative'; - splash.style.height = 'calc(100vh - 2px)'; - splash.style.width = 'calc(100vw - 2px)'; - splash.style.border = `1px solid var(--window-border-color)`; - splash.style.setProperty('--window-border-color', colorInfo.windowBorder); + const borderElement = document.createElement('div'); + borderElement.style.position = 'absolute'; + borderElement.style.width = 'calc(100vw - 2px)'; + borderElement.style.height = 'calc(100vh - 2px)'; + borderElement.style.zIndex = '1'; // allow border above other elements + borderElement.style.border = `1px solid var(--window-border-color)`; + borderElement.style.setProperty('--window-border-color', colorInfo.windowBorder); if (layoutInfo.windowBorderRadius) { - splash.style.borderRadius = layoutInfo.windowBorderRadius; + borderElement.style.borderRadius = layoutInfo.windowBorderRadius; } + + splash.appendChild(borderElement); } // ensure there is enough space diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 049c19d1ec57..4d60c1d9f1bb 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -86,6 +86,9 @@ export interface ICommonNativeHostService { getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }>; isMaximized(options?: INativeHostOptions): Promise; + maximizeWindow(options?: INativeHostOptions): Promise; + unmaximizeWindow(options?: INativeHostOptions): Promise; + minimizeWindow(options?: INativeHostOptions): Promise; moveWindowTop(options?: INativeHostOptions): Promise; positionWindow(position: IRectangle, options?: INativeHostOptions): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index c2241a9a4524..c04dabc6785c 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -260,6 +260,21 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return window?.win?.isMaximized() ?? false; } + async maximizeWindow(windowId: number | undefined, options?: INativeHostOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); + window?.win?.maximize(); + } + + async unmaximizeWindow(windowId: number | undefined, options?: INativeHostOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); + window?.win?.unmaximize(); + } + + async minimizeWindow(windowId: number | undefined, options?: INativeHostOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); + window?.win?.minimize(); + } + async moveWindowTop(windowId: number | undefined, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); window?.win?.moveTop(); diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index afed745b58e2..bad5b6d24f19 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -31,7 +31,7 @@ import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js'; import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { Platform, platform } from '../../../base/common/platform.js'; -import { getTitleBarStyle, TitlebarStyle } from '../../window/common/window.js'; +import { getTitleControlsStyle } from '../../window/common/window.js'; import { getZoomFactor } from '../../../base/browser/browser.js'; const $ = dom.$; @@ -923,12 +923,12 @@ class QuickInputDragAndDropController extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); - const customTitleBar = getTitleBarStyle(this.configurationService) === TitlebarStyle.CUSTOM; + const customTitleControls = getTitleControlsStyle(this.configurationService) === 'custom'; // Do not allow the widget to overflow or underflow window controls. // Use CSS calculations to avoid having to force layout with `.clientWidth` - this._controlsOnLeft = customTitleBar && platform === Platform.Mac; - this._controlsOnRight = customTitleBar && (platform === Platform.Windows || platform === Platform.Linux); + this._controlsOnLeft = customTitleControls && platform === Platform.Mac; + this._controlsOnRight = customTitleControls && (platform === Platform.Windows || platform === Platform.Linux); this._registerLayoutListener(); this.registerMouseListeners(); this.dndViewState.set({ ...initialViewState, done: true }, undefined); diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index e230d1a09a5c..7756aea58fc0 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -152,6 +152,7 @@ export interface IWindowSettings { readonly restoreFullscreen: boolean; readonly zoomLevel: number; readonly titleBarStyle: TitlebarStyle; + readonly titleControlsStyle: TitleControlsStyle; readonly autoDetectHighContrast: boolean; readonly autoDetectColorScheme: boolean; readonly menuBarVisibility: MenuBarVisibility; @@ -179,6 +180,12 @@ export const enum TitlebarStyle { CUSTOM = 'custom', } +export const enum TitleControlsStyle { + NATIVE = 'native', + CUSTOM = 'custom', + HIDDEN = 'hidden' +} + export const enum CustomTitleBarVisibility { AUTO = 'auto', WINDOWED = 'windowed', @@ -225,6 +232,20 @@ export function getTitleBarStyle(configurationService: IConfigurationService): T return TitlebarStyle.CUSTOM; // default to custom on all OS } +export function getTitleControlsStyle(configurationService: IConfigurationService): TitleControlsStyle { + if (isWeb || isMacintosh || getTitleBarStyle(configurationService) === TitlebarStyle.NATIVE) { + return TitleControlsStyle.NATIVE; // only supported on Windows/Linux desktop with custom titlebar + } + + const configuration = configurationService.getValue('window'); + const style = configuration?.titleControlsStyle; + if (style === TitleControlsStyle.CUSTOM || style === TitleControlsStyle.HIDDEN) { + return style; + } + + return TitleControlsStyle.NATIVE; // default to native on all OS +} + export const DEFAULT_CUSTOM_TITLEBAR_HEIGHT = 35; // includes space for command center export function useWindowControlsOverlay(configurationService: IConfigurationService): boolean { @@ -236,6 +257,13 @@ export function useWindowControlsOverlay(configurationService: IConfigurationSer return false; // only supported when title bar is custom } + if (!isMacintosh) { + const setting = getTitleControlsStyle(configurationService); + if (setting === TitleControlsStyle.CUSTOM || setting === TitleControlsStyle.HIDDEN) { + return false; // explicitly disabled by choice + } + } + return true; // default } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index fc33edf32941..501d95a87132 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -599,10 +599,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi for (const container of this.containers) { const isMainContainer = container === this.mainContainer; const isActiveContainer = this.activeContainer === container; - const containerWindowId = getWindowId(getWindow(container)); let windowBorder = false; - if (!this.state.runtime.mainWindowFullscreen && !this.state.runtime.maximized.has(containerWindowId) && (activeBorder || inactiveBorder)) { + if (!this.state.runtime.mainWindowFullscreen && (activeBorder || inactiveBorder)) { windowBorder = true; // If the inactive color is missing, fallback to the active one @@ -2079,11 +2078,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } getMainWindowBorderRadius(): string | undefined { - return this.state.runtime.mainWindowBorder && isMacintosh ? '5px' : undefined; + return this.state.runtime.mainWindowBorder && isMacintosh ? '10px' : undefined; } isPanelMaximized(): boolean { - // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment return (this.getPanelAlignment() === 'center' || !isHorizontal(this.getPanelPosition())) && !this.isVisible(Parts.EDITOR_PART, mainWindow); } diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 2f16f7a10370..e1a7a7e2f70f 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -231,6 +231,21 @@ 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; @@ -313,6 +328,42 @@ 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; diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 14cdd2393761..112527794576 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -8,7 +8,7 @@ import { localize, localize2 } from '../../../../nls.js'; import { MultiWindowParts, Part } from '../../part.js'; import { ITitleService } from '../../../services/title/browser/titleService.js'; import { getWCOTitlebarAreaRect, getZoomFactor, isWCOEnabled } from '../../../../base/browser/browser.js'; -import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, hasCustomTitlebar, hasNativeTitlebar, DEFAULT_CUSTOM_TITLEBAR_HEIGHT } from '../../../../platform/window/common/window.js'; +import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, hasCustomTitlebar, hasNativeTitlebar, DEFAULT_CUSTOM_TITLEBAR_HEIGHT, getTitleControlsStyle, TitleControlsStyle } from '../../../../platform/window/common/window.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { IConfigurationService, IConfigurationChangeEvent } from '../../../../platform/configuration/common/configuration.js'; @@ -247,6 +247,8 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { //#endregion protected rootContainer!: HTMLElement; + protected windowControlsContainer: HTMLElement | undefined; + protected dragRegion: HTMLElement | undefined; private title!: HTMLElement; @@ -482,8 +484,10 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { // for something, except for web where a custom menu being supported). not putting the // container helps with allowing to move the window when clicking very close to the // window control buttons. + } else if (getTitleControlsStyle(this.configurationService) === TitleControlsStyle.HIDDEN) { + // Linux/Windows: controls are explicitly disabled } else { - const windowControlsContainer = append(primaryWindowControlsLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container')); + this.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. @@ -491,7 +495,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { } if (isWCOEnabled()) { - windowControlsContainer.classList.add('wco-enabled'); + this.windowControlsContainer.classList.add('wco-enabled'); } } } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index bba032c63875..69756aafa366 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -801,11 +801,11 @@ export const WINDOW_ACTIVE_BORDER = registerColor('window.activeBorder', { light: null, hcDark: contrastBorder, hcLight: contrastBorder -}, localize('windowActiveBorder', "The color used for the border of the window when it is active. Only supported in the macOS and Linux desktop client when using the custom title bar.")); +}, localize('windowActiveBorder', "The color used for the border of the window when it is active on macOS or Linux. Requires custom title bar style and custom or hidden title controls on Linux.")); export const WINDOW_INACTIVE_BORDER = registerColor('window.inactiveBorder', { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder -}, localize('windowInactiveBorder', "The color used for the border of the window when it is inactive. Only supported in the macOS and Linux desktop client when using the custom title bar.")); +}, localize('windowInactiveBorder', "The color used for the border of the window when it is inactive on macOS or Linux. Requires custom title bar style and custom or hidden title controls on Linux.")); diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 41f28eabfc27..a374adcb598c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -32,7 +32,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; -import { getTitleBarStyle, TitlebarStyle } from '../../../../platform/window/common/window.js'; +import { getTitleControlsStyle, TitleControlsStyle } from '../../../../platform/window/common/window.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { EditorTabsMode, IWorkbenchLayoutService, LayoutSettings, Parts } from '../../../services/layout/browser/layoutService.js'; import { CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_IN_DEBUG_MODE, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugConfiguration, IDebugService, State, VIEWLET_ID } from '../common/debug.js'; @@ -80,12 +80,12 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { this.$el = dom.$('div.debug-toolbar'); // Note: changes to this setting require a restart, so no need to listen to it. - const customTitleBar = getTitleBarStyle(this.configurationService) === TitlebarStyle.CUSTOM; + const customTitleControls = getTitleControlsStyle(this.configurationService) === TitleControlsStyle.CUSTOM; // Do not allow the widget to overflow or underflow window controls. // Use CSS calculations to avoid having to force layout with `.clientWidth` - const controlsOnLeft = customTitleBar && platform === Platform.Mac; - const controlsOnRight = customTitleBar && (platform === Platform.Windows || platform === Platform.Linux); + const controlsOnLeft = customTitleControls && platform === Platform.Mac; + const controlsOnRight = customTitleControls && (platform === Platform.Windows || platform === Platform.Linux); this.$el.style.transform = `translate( min( max(${controlsOnLeft ? '60px' : '0px'}, calc(-50% + (100vw * var(--x-position)))), @@ -94,8 +94,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { var(--y-position) )`; - - this.dragArea = dom.append(this.$el, dom.$('div.drag-area' + ThemeIcon.asCSSSelector(icons.debugGripper))); const actionBarContainer = dom.append(this.$el, dom.$('div.action-bar-container')); diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index 9f53c894c91b..0ef403d13606 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -41,6 +41,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo 'window.nativeTabs', 'window.nativeFullScreen', 'window.clickThroughInactive', + 'window.titleControlsStyle', 'update.mode', 'editor.accessibilitySupport', 'security.workspace.trust.enabled', @@ -54,6 +55,7 @@ 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 titleControlsStyle = new ChangeObserver('string'); private readonly updateMode = new ChangeObserver('string'); private accessibilitySupport: 'on' | 'off' | 'auto' | undefined; private readonly workspaceTrustEnabled = new ChangeObserver('boolean'); @@ -117,6 +119,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // macOS: Click through (accept first mouse) processChanged(isMacintosh && this.clickThroughInactive.handleChange(config.window?.clickThroughInactive)); + // Windows/Linux: Title controls style + processChanged(!isMacintosh && this.titleControlsStyle.handleChange(config.window?.titleControlsStyle)); + // Update mode processChanged(this.updateMode.handleChange(config.update?.mode)); diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index c420ccfefc36..5e45beb11ea3 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -236,6 +236,14 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../platform/window/electron-s '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.titleControlsStyle': { + 'type': 'string', + 'enum': ['native', 'custom', 'hidden'], + 'default': 'native', + 'included': !isMacintosh, + 'scope': ConfigurationScope.APPLICATION, + 'description': localize('titleControlsStyle', "Adjust the appearance of the window controls to be native by the OS, custom drawn or hidden. Changes require a full restart to apply."), + }, 'window.customTitleBarVisibility': { 'type': 'string', 'enum': ['auto', 'windowed', 'never'], diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 9e7022319dcd..45fbdbf950da 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from '../../../../base/common/event.js'; import { getZoomFactor } from '../../../../base/browser/browser.js'; -import { addDisposableListener, EventType, getWindow, getWindowId, hide, show } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, append, 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'; @@ -19,6 +20,8 @@ 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'; @@ -49,6 +52,9 @@ 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; @@ -151,6 +157,45 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { }))); } + // Custom Window Controls (Native Windows/Linux) + if ( + !hasNativeTitlebar(this.configurationService) && // not for native title bars + !useWindowControlsOverlay(this.configurationService) && // not when controls are natively drawn + 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)) { @@ -167,6 +212,26 @@ 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(); diff --git a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index 431160fe73d7..9bc958141ff7 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -96,6 +96,9 @@ export class TestNativeHostService implements INativeHostService { async toggleFullScreen(): Promise { } async isMaximized(): Promise { return true; } async isFullScreen(): Promise { return true; } + async maximizeWindow(): Promise { } + async unmaximizeWindow(): Promise { } + async minimizeWindow(): Promise { } async moveWindowTop(options?: INativeHostOptions): Promise { } getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }> { throw new Error('Method not implemented.'); } async positionWindow(position: IRectangle, options?: INativeHostOptions): Promise { }