diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 848df29f9ca..de96a08b24d 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -30,6 +30,7 @@ export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = { highlightModifiedTabs: false, tabCloseButton: 'right', tabSizing: 'fit', + pinnedTabSizing: 'compact', titleScrollbarSizing: 'default', focusRecentEditorAfterClose: true, showIcons: true, diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index dbf66755d80..095ab967a79 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -63,8 +63,8 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.close-button-right, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.close-button-off:not(.sticky) { - padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab close button is not left (unless sticky) */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.close-button-off:not(.sticky-compact) { + padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab close button is not left (unless sticky-compact) */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit { @@ -75,34 +75,51 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink { - min-width: 60px; + min-width: 80px; flex-basis: 0; /* all tabs are even */ flex-grow: 1; /* all tabs grow even */ max-width: fit-content; max-width: -moz-fit-content; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit.sticky, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.sticky { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit.sticky-compact, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.sticky-compact, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit.sticky-shrink, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.sticky-shrink { - /** Sticky tabs do not scroll in case of overflow and are always above unsticky tabs which scroll under */ + /** Sticky compact/shrink tabs do not scroll in case of overflow and are always above unsticky tabs which scroll under */ position: sticky; z-index: 1; - /** Sticky tabs are even and never grow */ + /** Sticky compact/shrink tabs are even and never grow */ flex-basis: 0; flex-grow: 0; +} - /** Sticky tabs have a fixed width of 38px */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit.sticky-compact, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.sticky-compact { + + /** Sticky compact tabs have a fixed width of 38px */ width: 38px; min-width: 38px; max-width: 38px; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-fit.sticky, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit.sticky-shrink, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.sticky-shrink { - /** Disable sticky positions for sticky tabs if the available space is too little */ + /** Sticky shrink tabs have a fixed width of 80px */ + width: 80px; + min-width: 80px; + max-width: 80px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-fit.sticky-compact, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-compact, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-fit.sticky-shrink, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-shrink { + + /** Disable sticky positions for sticky compact/shrink tabs if the available space is too little */ position: static; } @@ -198,8 +215,8 @@ opacity: 0; /* when tab has the focus this shade breaks the tab border (fixes https://github.com/Microsoft/vscode/issues/57819) */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky:not(.has-icon) .monaco-icon-label { - text-align: center; /* ensure that sticky tabs without icon have label centered */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky-compact:not(.has-icon) .monaco-icon-label { + text-align: center; /* ensure that sticky-compact tabs without icon have label centered */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label, @@ -239,8 +256,8 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off:not(.dirty) > .tab-close, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.sticky > .tab-close { - display: none; /* hide the close action bar when we are configured to hide it (unless dirty, but always when sticky) */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.sticky-compact > .tab-close { + display: none; /* hide the close action bar when we are configured to hide it (unless dirty, but always when sticky-compact) */ } .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active > .tab-close .action-label, /* always show it for active tab */ @@ -286,16 +303,16 @@ padding-right: 10px; /* give a little bit more room if close button is off */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-off:not(.sticky) { - padding-right: 5px; /* we need less room when sizing is shrink (unless tab is sticky) */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-off:not(.sticky-compact) { + padding-right: 5px; /* we need less room when sizing is shrink (unless tab is sticky-compact) */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty-border-top > .tab-close { display: none; /* hide dirty state when highlightModifiedTabs is enabled and when running without close button */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top):not(.sticky) { - padding-right: 0; /* remove extra padding when we are running without close button (unless tab is sticky) */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top):not(.sticky-compact) { + padding-right: 0; /* remove extra padding when we are running without close button (unless tab is sticky-compact) */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off > .tab-close { diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index f3eb689c73a..77983df375c 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -63,7 +63,8 @@ export class TabsTitleControl extends TitleControl { }; private static readonly TAB_SIZES = { - sticky: 38, + compact: 38, + shrink: 80, fit: 120 }; @@ -521,6 +522,7 @@ export class TabsTitleControl extends TitleControl { oldOptions.labelFormat !== newOptions.labelFormat || oldOptions.tabCloseButton !== newOptions.tabCloseButton || oldOptions.tabSizing !== newOptions.tabSizing || + oldOptions.pinnedTabSizing !== newOptions.pinnedTabSizing || oldOptions.showIcons !== newOptions.showIcons || oldOptions.hasIcons !== newOptions.hasIcons || oldOptions.highlightModifiedTabs !== newOptions.highlightModifiedTabs @@ -1025,14 +1027,15 @@ export class TabsTitleControl extends TitleControl { const isTabSticky = this.group.isSticky(index); const options = this.accessor.partOptions; - const tabCloseButton = isTabSticky ? 'off' /* treat sticky tabs as tabCloseButton: 'off' */ : options.tabCloseButton; + const tabCloseButton = isTabSticky && options.pinnedTabSizing === 'compact' ? 'off' /* treat sticky compact tabs as tabCloseButton: 'off' */ : options.tabCloseButton; ['off', 'left', 'right'].forEach(option => { const domAction = tabCloseButton === option ? addClass : removeClass; domAction(tabContainer, `close-button-${option}`); }); + const tabSizing = isTabSticky && options.pinnedTabSizing === 'shrink' ? 'shrink' /* treat sticky shrink tabs as tabSizing: 'shrink' */ : options.tabSizing; ['fit', 'shrink'].forEach(option => { - const domAction = options.tabSizing === option ? addClass : removeClass; + const domAction = tabSizing === option ? addClass : removeClass; domAction(tabContainer, `sizing-${option}`); }); @@ -1042,13 +1045,26 @@ export class TabsTitleControl extends TitleControl { removeClass(tabContainer, 'has-icon'); } - // Sticky Tabs need a position to remain at their location + ['compact', 'shrink', 'inherit'].forEach(option => { + const domAction = isTabSticky && options.pinnedTabSizing === option ? addClass : removeClass; + domAction(tabContainer, `sticky-${option}`); + }); + + // Sticky compact/shrink tabs need a position to remain at their location // when scrolling to stay in view (requirement for position: sticky) - if (isTabSticky) { - addClass(tabContainer, 'sticky'); - tabContainer.style.left = `${index * TabsTitleControl.TAB_SIZES.sticky}px`; + if (isTabSticky && options.pinnedTabSizing !== 'inherit') { + let stickyTabWidth = 0; + switch (options.pinnedTabSizing) { + case 'compact': + stickyTabWidth = TabsTitleControl.TAB_SIZES.compact; + break; + case 'shrink': + stickyTabWidth = TabsTitleControl.TAB_SIZES.shrink; + break; + } + + tabContainer.style.left = `${index * stickyTabWidth}px`; } else { - removeClass(tabContainer, 'sticky'); tabContainer.style.left = 'auto'; } @@ -1057,17 +1073,19 @@ export class TabsTitleControl extends TitleControl { } private redrawLabel(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void { - const isTabSticky = this.group.isSticky(index); + const options = this.accessor.partOptions; - // Unless tabs are sticky, show the full label and description - // Sticky tabs will only show an icon if icons are enabled + // Unless tabs are sticky compact, show the full label and description + // Sticky compact tabs will only show an icon if icons are enabled // or their first character of the name otherwise let name: string | undefined; + let forceLabel = false; let description: string; - if (isTabSticky) { - const isShowingIcons = this.accessor.partOptions.showIcons && this.accessor.partOptions.hasIcons; + if (this.group.isSticky(index) && options.pinnedTabSizing === 'compact') { + const isShowingIcons = options.showIcons && options.hasIcons; name = isShowingIcons ? '' : tabLabel.name?.charAt(0).toUpperCase(); description = ''; + forceLabel = true; } else { name = tabLabel.name; description = tabLabel.description || ''; @@ -1086,7 +1104,7 @@ export class TabsTitleControl extends TitleControl { // Label tabLabelWidget.setResource( { name, description, resource: toResource(editor, { supportSideBySide: SideBySideEditor.BOTH }) }, - { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor), forceLabel: isTabSticky } + { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor), forceLabel } ); // Tests helper @@ -1249,7 +1267,7 @@ export class TabsTitleControl extends TitleControl { // // Synopsis // - allTabsWidth: sum of all tab widths - // - stickyTabsWidth: sum of all sticky tab widths + // - stickyTabsWidth: sum of all sticky tab widths (unless they inherit look from other tabs) // - visibleContainerWidth: size of tab container // - availableContainerWidth: size of tab container minus size of sticky tabs // @@ -1265,7 +1283,24 @@ export class TabsTitleControl extends TitleControl { const visibleTabsContainerWidth = tabsContainer.offsetWidth; const allTabsWidth = tabsContainer.scrollWidth; - let stickyTabsWidth = this.group.stickyCount * TabsTitleControl.TAB_SIZES.sticky; + // Compute width of sticky tabs depending on pinned tab sizing + // - compact: sticky-tabs * TAB_SIZES.compact + // - shrink: sticky-tabs * TAB_SIZES.shrink + // - inherit: 0 (sticky tabs inherit look and feel from non-sticky tabs) + let stickyTabsWidth = 0; + if (this.group.stickyCount > 0) { + let stickyTabWidth = 0; + switch (this.accessor.partOptions.pinnedTabSizing) { + case 'compact': + stickyTabWidth = TabsTitleControl.TAB_SIZES.compact; + break; + case 'shrink': + stickyTabWidth = TabsTitleControl.TAB_SIZES.shrink; + break; + } + + stickyTabsWidth = this.group.stickyCount * stickyTabWidth; + } let activeTabSticky = this.group.isSticky(activeIndex); let availableTabsContainerWidth = visibleTabsContainerWidth - stickyTabsWidth; @@ -1583,11 +1618,11 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = // Adjust gradient for focused and unfocused hover background const makeTabHoverBackgroundRule = (color: Color, colorDrag: Color, hasFocus = false) => ` - .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):not(.sticky):hover > .tab-label::after { + .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):not(.sticky-compact):hover > .tab-label::after { background: linear-gradient(to left, ${color}, transparent) !important; } - .monaco-workbench .part.editor > .content.dragged-over .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):not(.sticky):hover > .tab-label::after { + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):not(.sticky-compact):hover > .tab-label::after { background: linear-gradient(to left, ${colorDrag}, transparent) !important; } `; @@ -1610,19 +1645,19 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = if (editorDragAndDropBackground && adjustedTabDragBackground) { const adjustedColorDrag = editorDragAndDropBackground.flatten(adjustedTabDragBackground); collector.addRule(` - .monaco-workbench .part.editor > .content.dragged-over .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.active):not(.dragged):not(.sticky) > .tab-label::after, - .monaco-workbench .part.editor > .content.dragged-over .editor-group-container:not(.active) > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.dragged):not(.sticky) > .tab-label::after { + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.active):not(.dragged):not(.sticky-compact) > .tab-label::after, + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container:not(.active) > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.dragged):not(.sticky-compact) > .tab-label::after { background: linear-gradient(to left, ${adjustedColorDrag}, transparent) !important; } `); } const makeTabBackgroundRule = (color: Color, colorDrag: Color, focused: boolean, active: boolean) => ` - .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged):not(.sticky) > .tab-label::after { + .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged):not(.sticky-compact) > .tab-label::after { background: linear-gradient(to left, ${color}, transparent); } - .monaco-workbench .part.editor > .content.dragged-over .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged):not(.sticky) > .tab-label::after { + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged):not(.sticky-compact) > .tab-label::after { background: linear-gradient(to left, ${colorDrag}, transparent); } `; diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 7656a699f3c..facc4a985ee 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -34,12 +34,12 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio }, 'workbench.editor.scrollToSwitchTabs': { 'type': 'boolean', - 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration."), + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration. This value is ignored when `#workbench.editor.showTabs#` is `false`."), 'default': false }, 'workbench.editor.highlightModifiedTabs': { 'type': 'boolean', - 'description': nls.localize('highlightModifiedTabs', "Controls whether a top border is drawn on modified (dirty) editor tabs or not."), + 'markdownDescription': nls.localize('highlightModifiedTabs', "Controls whether a top border is drawn on modified (dirty) editor tabs or not. This value is ignored when `#workbench.editor.showTabs#` is `false`."), 'default': false }, 'workbench.editor.labelFormat': { @@ -74,7 +74,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'type': 'string', 'enum': ['left', 'right', 'off'], 'default': 'right', - 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'editorTabCloseButton' }, "Controls the position of the editor's tabs close buttons, or disables them when set to 'off'.") + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'editorTabCloseButton' }, "Controls the position of the editor's tabs close buttons, or disables them when set to 'off'. This value is ignored when `#workbench.editor.showTabs#` is `false`.") }, 'workbench.editor.tabSizing': { 'type': 'string', @@ -84,7 +84,18 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio nls.localize('workbench.editor.tabSizing.fit', "Always keep tabs large enough to show the full editor label."), nls.localize('workbench.editor.tabSizing.shrink', "Allow tabs to get smaller when the available space is not enough to show all tabs at once.") ], - 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'tabSizing' }, "Controls the sizing of editor tabs.") + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'tabSizing' }, "Controls the sizing of editor tabs. This value is ignored when `#workbench.editor.showTabs#` is `false`.") + }, + 'workbench.editor.pinnedTabSizing': { + 'type': 'string', + 'enum': ['compact', 'shrink', 'inherit'], + 'default': 'compact', + 'enumDescriptions': [ + nls.localize('workbench.editor.pinnedTabSizing.compact', "A pinned tab will show in a compact form with only icon or first letter of the editor name."), + nls.localize('workbench.editor.pinnedTabSizing.shrink', "A pinned tab shrinks to a compact fixed size showing parts of the editor name."), + nls.localize('workbench.editor.pinnedTabSizing.inherit', "A pinned tab inherits the look of non pinned tabs.") + ], + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'pinnedTabSizing' }, "Controls the sizing of pinned editor tabs. Pinned tabs are sorted to the begining of all opened tabs and typically do not close until unpinned. This value is ignored when `#workbench.editor.showTabs#` is `false`.") }, 'workbench.editor.splitSizing': { 'type': 'string', @@ -330,7 +341,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio nls.localize('window.menuBarVisibility.visible', "Menu is always visible even in full screen mode."), nls.localize('window.menuBarVisibility.toggle', "Menu is hidden but can be displayed via Alt key."), nls.localize('window.menuBarVisibility.hidden', "Menu is always hidden."), - nls.localize('window.menuBarVisibility.compact', "Menu is displayed as a compact button in the sidebar. This value is ignored when 'window.titleBarStyle' is 'native'.") + nls.localize('window.menuBarVisibility.compact', "Menu is displayed as a compact button in the sidebar. This value is ignored when `#window.titleBarStyle#` is `native`.") ], 'default': isWeb ? 'compact' : 'default', 'scope': ConfigurationScope.APPLICATION, diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 84f8ce721ad..d4c2b563d43 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1212,6 +1212,7 @@ interface IEditorPartConfiguration { highlightModifiedTabs?: boolean; tabCloseButton?: 'left' | 'right' | 'off'; tabSizing?: 'fit' | 'shrink'; + pinnedTabSizing?: 'compact' | 'shrink' | 'inherit'; titleScrollbarSizing?: 'default' | 'large'; focusRecentEditorAfterClose?: boolean; showIcons?: boolean;