diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 5711cd0391d4b1..13e010931fcd1c 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -269,6 +269,7 @@ "--vscode-editorGroup-emptyBackground", "--vscode-editorGroup-focusedEmptyBorder", "--vscode-editorGroupHeader-border", + "--vscode-editorGroupHeader-connectedTabsBackground", "--vscode-editorGroupHeader-noTabsBackground", "--vscode-editorGroupHeader-tabsBackground", "--vscode-editorGroupHeader-tabsBorder", @@ -1086,6 +1087,7 @@ "--model-hover-surface-background", "--modern-ui-activitybar-lane", "--modern-ui-connected-tab-action-background", + "--modern-ui-connected-tab-action-inset", "--modern-ui-connected-tab-border", "--modern-ui-connected-tab-cap-radius", "--modern-ui-connected-tab-gutter", diff --git a/extensions/theme-defaults/themes/2026-dark.json b/extensions/theme-defaults/themes/2026-dark.json index fd533bf1680abe..4c5f1da5be7e8b 100644 --- a/extensions/theme-defaults/themes/2026-dark.json +++ b/extensions/theme-defaults/themes/2026-dark.json @@ -194,7 +194,7 @@ "statusBarItem.prominentHoverBackground": "#3994BC", "tab.activeBackground": "#121314", "tab.activeForeground": "#bfbfbf", - "tab.inactiveBackground": "#202122", + "tab.inactiveBackground": "#191A1B", "tab.inactiveForeground": "#8C8C8C", "tab.border": "#2A2B2C", "tab.lastPinnedBorder": "#2A2B2C", @@ -205,7 +205,8 @@ "tab.unfocusedActiveForeground": "#8C8C8C", "tab.unfocusedInactiveBackground": "#191A1B", "tab.unfocusedInactiveForeground": "#444444", - "editorGroupHeader.tabsBackground": "#202122", + "editorGroupHeader.tabsBackground": "#191A1B", + "editorGroupHeader.connectedTabsBackground": "#202122", "tab.activeBorder": "#121314", "editorGroupHeader.tabsBorder": "#2A2B2C", "breadcrumb.foreground": "#8C8C8C", diff --git a/extensions/theme-defaults/themes/2026-light.json b/extensions/theme-defaults/themes/2026-light.json index 45dc4f1f026cf5..f305d729d3db02 100644 --- a/extensions/theme-defaults/themes/2026-light.json +++ b/extensions/theme-defaults/themes/2026-light.json @@ -201,7 +201,7 @@ "toolbar.activeBackground": "#D6D6D8", "tab.activeBackground": "#FFFFFF", "tab.activeForeground": "#202020", - "tab.inactiveBackground": "#EAEAEA", + "tab.inactiveBackground": "#FAFAFD", "tab.inactiveForeground": "#606060", "tab.border": "#F0F1F2", "tab.lastPinnedBorder": "#F0F1F2", @@ -212,7 +212,8 @@ "tab.unfocusedActiveForeground": "#606060", "tab.unfocusedInactiveBackground": "#FAFAFD", "tab.unfocusedInactiveForeground": "#BBBBBB", - "editorGroupHeader.tabsBackground": "#EAEAEA", + "editorGroupHeader.tabsBackground": "#FAFAFD", + "editorGroupHeader.connectedTabsBackground": "#EAEAEA", "tab.activeBorder": "#FFFFFF", "editorGroupHeader.tabsBorder": "#F0F1F2", "breadcrumb.foreground": "#606060", diff --git a/extensions/theme-defaults/themes/dark_modern.json b/extensions/theme-defaults/themes/dark_modern.json index 67688754a90f32..7aee67bd82c356 100644 --- a/extensions/theme-defaults/themes/dark_modern.json +++ b/extensions/theme-defaults/themes/dark_modern.json @@ -34,7 +34,8 @@ "editor.findMatchBackground": "#9E6A03", "editor.foreground": "#CCCCCC", "editorGroup.border": "#FFFFFF17", - "editorGroupHeader.tabsBackground": "#2B2B2B", + "editorGroupHeader.tabsBackground": "#181818", + "editorGroupHeader.connectedTabsBackground": "#2B2B2B", "editorGroupHeader.tabsBorder": "#2B2B2B", "editorGutter.addedBackground": "#2EA043", "editorGutter.deletedBackground": "#F85149", @@ -107,7 +108,7 @@ "tab.selectedBorderTop": "#6caddf", "tab.border": "#2B2B2B", "tab.hoverBackground": "#1F1F1F", - "tab.inactiveBackground": "#2B2B2B", + "tab.inactiveBackground": "#181818", "tab.inactiveForeground": "#9D9D9D", "tab.unfocusedActiveBorder": "#1F1F1F", "tab.unfocusedActiveBorderTop": "#2B2B2B", diff --git a/extensions/theme-defaults/themes/dark_vs.json b/extensions/theme-defaults/themes/dark_vs.json index 7b9c6d4a99b6fa..727ab05c96f74d 100644 --- a/extensions/theme-defaults/themes/dark_vs.json +++ b/extensions/theme-defaults/themes/dark_vs.json @@ -5,7 +5,7 @@ "checkbox.border": "#6B6B6B", "editor.background": "#1E1E1E", "editor.foreground": "#D4D4D4", - "editorGroupHeader.tabsBackground": "#303031", + "editorGroupHeader.connectedTabsBackground": "#303031", "editor.inactiveSelectionBackground": "#3A3D41", "editorIndentGuide.background1": "#404040", "editorIndentGuide.activeBackground1": "#707070", @@ -28,7 +28,6 @@ "sideBarSectionHeader.background": "#0000", "sideBarSectionHeader.border": "#ccc3", "tab.selectedBackground": "#37373D", - "tab.inactiveBackground": "#303031", "tab.inactiveForeground": "#A6A6A6", "tab.selectedForeground": "#FFFFFF", "tab.lastPinnedBorder": "#ccc3", diff --git a/extensions/theme-defaults/themes/light_modern.json b/extensions/theme-defaults/themes/light_modern.json index d76ab0458cb76c..5704bb3ecf3fc5 100644 --- a/extensions/theme-defaults/themes/light_modern.json +++ b/extensions/theme-defaults/themes/light_modern.json @@ -34,7 +34,8 @@ "editor.inactiveSelectionBackground": "#E5EBF1", "editor.selectionHighlightBackground": "#ADD6FF80", "editorGroup.border": "#E5E5E5", - "editorGroupHeader.tabsBackground": "#E5E5E5", + "editorGroupHeader.tabsBackground": "#F8F8F8", + "editorGroupHeader.connectedTabsBackground": "#E5E5E5", "editorGroupHeader.tabsBorder": "#E5E5E5", "editorGutter.addedBackground": "#2EA043", "editorGutter.deletedBackground": "#F85149", @@ -124,7 +125,7 @@ "tab.selectedBorderTop": "#68a3da", "tab.border": "#E5E5E5", "tab.hoverBackground": "#FFFFFF", - "tab.inactiveBackground": "#E5E5E5", + "tab.inactiveBackground": "#F8F8F8", "tab.inactiveForeground": "#616161", "tab.lastPinnedBorder": "#D4D4D4", "tab.unfocusedActiveBorder": "#F8F8F8", diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index 3c5363c5a500ca..ef114373e67720 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -5,7 +5,7 @@ "checkbox.border": "#919191", "editor.background": "#FFFFFF", "editor.foreground": "#000000", - "editorGroupHeader.tabsBackground": "#E8E8E8", + "editorGroupHeader.connectedTabsBackground": "#E8E8E8", "editor.inactiveSelectionBackground": "#E5EBF1", "editorIndentGuide.background1": "#D3D3D3", "editorIndentGuide.activeBackground1": "#939393", @@ -27,7 +27,6 @@ "sideBarSectionHeader.background": "#0000", "sideBarSectionHeader.border": "#61616130", "tab.selectedForeground": "#333333", - "tab.inactiveBackground": "#E8E8E8", "tab.inactiveForeground": "#616161", "tab.selectedBackground": "#E4E6F1", "tab.lastPinnedBorder": "#61616130", diff --git a/extensions/theme-quietlight/themes/quietlight-color-theme.json b/extensions/theme-quietlight/themes/quietlight-color-theme.json index 253cfb58fe32e6..8ec2814a7769c6 100644 --- a/extensions/theme-quietlight/themes/quietlight-color-theme.json +++ b/extensions/theme-quietlight/themes/quietlight-color-theme.json @@ -521,8 +521,7 @@ "peekViewResult.background": "#F2F8FC", "peekView.border": "#705697", "peekViewResult.matchHighlightBackground": "#93C6D6", - "editorGroupHeader.tabsBackground": "#E4E4E4", - "tab.inactiveBackground": "#E4E4E4", + "editorGroupHeader.connectedTabsBackground": "#E4E4E4", "tab.inactiveForeground": "#54494B", "tab.unfocusedInactiveForeground": "#54494B", "tab.hoverBackground": "#F2F2F2", diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index a9c9cff11001a9..ac63294495c45a 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -29,6 +29,9 @@ export interface IEditorPartCreationOptions { export const DEFAULT_EDITOR_MIN_DIMENSIONS = new Dimension(220, 70); export const DEFAULT_EDITOR_MAX_DIMENSIONS = new Dimension(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); +export const CONNECTED_EDITOR_TABS_CLASS = 'modern-ui-connected-editor-tabs'; +export const CONNECTED_EDITOR_TABS_SELECTOR = `.${CONNECTED_EDITOR_TABS_CLASS}`; + export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = { showTabs: 'multiple', highlightModifiedTabs: false, diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 6e0590648871f5..26e3b4fca240e2 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -23,7 +23,7 @@ import { IQuickInputService } from '../../../../platform/quickinput/common/quick import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillEditorsDragData, isWindowDraggedOver } from '../../dnd.js'; import { EditorPane } from './editorPane.js'; -import { IEditorGroupMenuIds, IEditorGroupsView, IEditorGroupView, IEditorPartsView, IInternalEditorOpenOptions } from './editor.js'; +import { CONNECTED_EDITOR_TABS_SELECTOR, IEditorGroupMenuIds, IEditorGroupsView, IEditorGroupView, IEditorPartsView, IInternalEditorOpenOptions } from './editor.js'; import { IEditorCommandsContext, EditorResourceAccessor, IEditorPartOptions, SideBySideEditor, EditorsOrder, EditorInputCapabilities, IToolbarActions, GroupIdentifier, Verbosity } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { ResourceContextKey, ActiveEditorPinnedContext, ActiveEditorStickyContext, ActiveEditorDirtyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, ActiveEditorFirstInGroupContext, ActiveEditorAvailableEditorIdsContext, applyAvailableEditorIds, ActiveEditorLastInGroupContext, ActiveEditorCannotCloseContext } from '../../../common/contextkeys.js'; @@ -631,7 +631,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC if (usesModernMultiTabHeight || this.useModernUITabs) { const height = isCompact ? EditorTabsControl.EDITOR_TAB_HEIGHT.modernUICompact : EditorTabsControl.EDITOR_TAB_HEIGHT.modernUI; // Connected tabs reserve one extra pixel for the separator below the gutter. - return height + (usesModernMultiTabHeight && this.parent.closest('.modern-ui.modern-ui-connected-editor-tabs') ? 1 : 0); + return height + (usesModernMultiTabHeight && this.parent.closest(CONNECTED_EDITOR_TABS_SELECTOR) ? 1 : 0); } return isCompact ? EditorTabsControl.EDITOR_TAB_HEIGHT.compact : EditorTabsControl.EDITOR_TAB_HEIGHT.normal; } diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 5b0453c1188d17..ec0ef394f41cf0 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -34,7 +34,7 @@ import { INotificationService } from '../../../../platform/notification/common/n import { MergeGroupMode, IMergeGroupOptions } from '../../../services/editor/common/editorGroupsService.js'; import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow, ModifierKeyEmitter, $, isHTMLElement } from '../../../../base/browser/dom.js'; import { localize } from '../../../../nls.js'; -import { IEditorGroupMenuIds, IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView, prepareMoveCopyEditors } from './editor.js'; +import { CONNECTED_EDITOR_TABS_SELECTOR, IEditorGroupMenuIds, IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView, prepareMoveCopyEditors } from './editor.js'; import { CloseEditorTabAction, CloseOtherEditorTabsInGroupAction, UnpinEditorAction } from './editorActions.js'; import { assertReturnsAllDefined, assertReturnsDefined } from '../../../../base/common/types.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -118,7 +118,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { private readonly connectedTabTextWidths = new LRUCache(256); private addTabContainer: HTMLElement | undefined; private tabSizingFixedDisposables: DisposableStore | undefined; - private connectedTabBounds: { tab: HTMLElement; overflowEdge: HTMLElement; fillLeft: number; fillRight: number; viewportLeft: number; viewportRight: number; clippingEdgeExtent: number; shoulderExtent: number } | undefined; + private connectedTabBounds: { tab: HTMLElement; overflowEdge: HTMLElement; fillLeft: number; fillRight: number; viewportLeft: number; viewportRight: number; shoulderExtent: number } | undefined; private readonly closeEditorAction = this._register(this.instantiationService.createInstance(CloseEditorTabAction, CloseEditorTabAction.ID, CloseEditorTabAction.LABEL)); private readonly unpinEditorAction = this._register(this.instantiationService.createInstance(UnpinEditorAction, UnpinEditorAction.ID, UnpinEditorAction.LABEL)); @@ -853,7 +853,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } private invalidateConnectedTabLayout(): void { - if (this.connectedTabBounds || this.parent.closest('.modern-ui.modern-ui-connected-editor-tabs')) { + if (this.connectedTabBounds || this.parent.closest(CONNECTED_EDITOR_TABS_SELECTOR)) { this.clearConnectedTabClipping(); this.layout(this.dimensions); } @@ -1828,7 +1828,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Label const resource = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }); let suffix: string | undefined; - if (name && resource && name === basename(resource) && this.parent.closest('.modern-ui.modern-ui-connected-editor-tabs') && !(options.pinnedTabSizing === 'compact' && this.tabsModel.isSticky(tabIndex))) { + if (name && resource && name === basename(resource) && this.parent.closest(CONNECTED_EDITOR_TABS_SELECTOR) && !(options.pinnedTabSizing === 'compact' && this.tabsModel.isSticky(tabIndex))) { const extension = extname(resource); if (extension.length > 1) { suffix = extension; @@ -2065,7 +2065,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } private doLayoutTabs(dimensions: IEditorTitleControlDimensions, options?: IMultiEditorTabsControlLayoutOptions): void { - const connected = Boolean(this.parent.closest('.modern-ui.modern-ui-connected-editor-tabs')); + const connected = Boolean(this.parent.closest(CONNECTED_EDITOR_TABS_SELECTOR)); this.parent.classList.toggle('connected-tabs-labels', connected); if (connected !== this.connectedTabLabels) { this.connectedTabLabels = connected; @@ -2080,10 +2080,13 @@ export class MultiEditorTabsControl extends EditorTabsControl { // wrapping is disabled (e.g. due to space constraints) const tabsWrapMultiLine = this.doLayoutTabsWrapping(dimensions); const tabs = Array.from(assertReturnsDefined(this.tabsContainer).children).filter(isHTMLElement); + const top = tabs.at(0)?.offsetTop; const bottom = tabs.at(-1)?.offsetTop; - const upperTabBar = this.parent.classList.contains('two-tab-bars') && this.parent.firstElementChild === this.tabsAndActionsContainer; + const topTabBar = this.parent.firstElementChild === this.tabsAndActionsContainer; + const upperTabBar = this.parent.classList.contains('two-tab-bars') && topTabBar; for (const tab of tabs) { tab.classList.toggle('connected-tab-upper-row', connected && (upperTabBar || tab.offsetTop !== bottom)); + tab.classList.toggle('connected-tab-top-row', connected && topTabBar && tab.offsetTop === top); } if (!tabsWrapMultiLine) { this.doLayoutTabsNonWrapping(options); @@ -2360,14 +2363,13 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.clearConnectedTabClipping(); const activeTabFill = activeTab?.firstElementChild; const overflowEdge = this.connectedTabOverflowEdge; - if (activeTab && !activeTab.classList.contains('connected-tab-upper-row') && isHTMLElement(activeTabFill) && overflowEdge && !activeTabPositionStatic && this.parent.closest('.modern-ui.modern-ui-connected-editor-tabs')) { + if (activeTab && !activeTab.classList.contains('connected-tab-upper-row') && isHTMLElement(activeTabFill) && overflowEdge && !activeTabPositionStatic && this.parent.closest(CONNECTED_EDITOR_TABS_SELECTOR)) { // DOM bounds reflect native scroll clamping before the custom scrollbar dimensions update. const scrollLeft = tabsContainer.scrollLeft; const tabsBounds = tabsContainer.getBoundingClientRect(); const fillBounds = activeTabFill.getBoundingClientRect(); const scrollableBounds = tabsScrollbar.getDomNode().getBoundingClientRect(); const targetWindow = getWindow(activeTabFill); - const fillStyle = targetWindow.getComputedStyle(activeTabFill); const fillLeft = fillBounds.left - tabsBounds.left + scrollLeft; const viewportRight = visibleTabsWidth - (this.addTabContainer?.offsetWidth ?? 0); overflowEdge.style.top = `${fillBounds.top - scrollableBounds.top}px`; @@ -2381,7 +2383,6 @@ export class MultiEditorTabsControl extends EditorTabsControl { fillRight: fillBounds.right - tabsBounds.left + scrollLeft, viewportLeft: stickyTabsWidth, viewportRight, - clippingEdgeExtent: Number.parseFloat(fillStyle.borderTopLeftRadius), shoulderExtent: Number.parseFloat(targetWindow.getComputedStyle(activeTabFill, '::after').width), }; } @@ -2512,7 +2513,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { return; } - const { tab, overflowEdge, fillLeft, fillRight, viewportLeft, viewportRight, clippingEdgeExtent, shoulderExtent } = this.connectedTabBounds; + const { tab, overflowEdge, fillLeft, fillRight, viewportLeft, viewportRight, shoulderExtent } = this.connectedTabBounds; const visibleLeft = scrollLeft + viewportLeft; const visibleRight = scrollLeft + viewportRight; const visibleFillLeft = Math.max(fillLeft, visibleLeft); @@ -2521,7 +2522,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { const rightClipped = fillRight > visibleRight; const leftEdge = fillLeft - shoulderExtent < visibleLeft; const rightEdge = fillRight + shoulderExtent > visibleRight; - const hidden = visibleFillLeft + clippingEdgeExtent >= visibleFillRight; + const hidden = visibleFillLeft + shoulderExtent >= visibleFillRight; tab.classList.toggle('connected-tab-left-edge', leftEdge); tab.classList.toggle('connected-tab-right-edge', rightEdge); tab.classList.toggle('connected-tab-left-clipped', leftClipped); diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index fdb58dce364a99..91b466700b3df5 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -223,6 +223,8 @@ export const EDITOR_GROUP_HEADER_TABS_BACKGROUND = registerColor('editorGroupHea hcLight: null }, localize('tabsContainerBackground', "Background color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); +export const EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND = registerColor('editorGroupHeader.connectedTabsBackground', EDITOR_GROUP_HEADER_TABS_BACKGROUND, localize('connectedTabsContainerBackground', "Background color of connected tabs in editor group title headers. Editor groups are the containers of editors.")); + export const EDITOR_GROUP_HEADER_TABS_BORDER = registerColor('editorGroupHeader.tabsBorder', null, localize('tabsContainerBorder', "Border color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); export const EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND = registerColor('editorGroupHeader.noTabsBackground', editorBackground, localize('editorGroupHeaderBackground', "Background color of the editor group title header when (`\"workbench.editor.showTabs\": \"single\"`). Editor groups are the containers of editors.")); diff --git a/src/vs/workbench/contrib/modernUI/README.md b/src/vs/workbench/contrib/modernUI/README.md index 3d0e0b5dd5c5f5..2b5a0838308cbf 100644 --- a/src/vs/workbench/contrib/modernUI/README.md +++ b/src/vs/workbench/contrib/modernUI/README.md @@ -11,20 +11,26 @@ The Modern UI colors below are experimental and require a build that supports th `workbench.experimental.modernUIEditorTabStyle` chooses the editor tab treatment when `workbench.experimental.modernUI` is enabled: -- `connected` (default): the bottom tab row spans the strip height without gaps. Its active tab joins the editor surface with an outside stroke and curved shoulders, and the first tab has a straight left edge. Upper wrapped rows and a separate pinned row retain the original rounded pills. +- `connected` (default): the bottom tab row spans the strip height without gaps. Its active tab joins the editor surface with an outside stroke and curved shoulders. First tabs and wrapped-row starts omit their own left stroke and outside shoulder; the editor frame paints that edge once. Upper wrapped rows and a separate pinned row retain rounded pills. An active tab on the outermost row reuses the editor frame's top stroke. In high-contrast themes, one rounded editor-group frame continuously encloses the title and editor surface on all four sides. - `pill`: separate rounded tabs, without the connected stroke or shoulders. Changes apply immediately, including in auxiliary editor windows. This setting does not enable Modern UI by itself and does not change the Agents window's pill tabs. +The connected root marker is defined with the editor control constants and shared by editor layout, the Modern UI contribution, and theming. The contribution owns toggling the marker on workbench containers. Core editor-tab behavior depends on the connected marker independently of the broader Modern UI marker. `connectedEditorTabs.css` owns Modern UI marker-gated geometry, while `connectedEditorTabs.ts` owns marker-gated theme derivations. Core editor-tab code only handles behavior that CSS cannot provide: row classification, label compression, shoulder-aware reveal, and viewport clipping. + Connected tabs preserve at least the first basename character, an ellipsis, the extension, any decoration badge, and the action column when shrinking. File icons collapse first and return when the editor is widened; full names remain available in the hover and accessible label. Default `fit` sizing remains content-sized: the measured minimum is a compression safeguard, not a preferred tab width. Active close actions and dirty indicators remain visible, while clean inactive close actions appear on hover or keyboard focus. The final tab and the right viewport boundary keep an inset curved shoulder instead of a straight clipped edge. Explicit compact pinned tabs retain their icon-only sizing. -The top cap and bottom shoulders share the same control radius plus the outside stroke (5px with the default tokens), including at clipped viewport edges. Upper-row pills retain the original control radius. Wrapped fills occupy equal-height row hit boxes (28px normally, 24px compact), with spacing only between rows and no extra gutter below the final row. Hover does not change their geometry. +The bottom shoulders and freestanding caps share the same control radius plus the outside stroke (5px with the default tokens), including at clipped viewport edges. The outermost connected row shares the editor's top stroke without squaring its free corners. Only corners adjoining the left frame are straight; the editor's own clipping supplies its outer corner radius. Upper wrapped rows use the original control radius on their free corners and align to the same left edge as the bottom row. Wrapped fills occupy equal-height row hit boxes (28px normally, 24px compact), without horizontal gutters between their painted surfaces, with spacing only between rows and no extra gutter below the final row. Hover does not change their geometry. + +Close-action clearance is derived from the existing tab height and row gutters, independently of the shoulder radius. The same clearance reserves label space and positions the 20px hover target. Painted borders are excluded from the available content area: a wrapped connected row centers its action below the cap's top stroke, while a single standard row uses the shared frame. Stroke clearance is reserved in inactive tabs too, so selecting a tab never moves its close target. Separate pinned-row pills add their horizontal fill inset to the reservation. Left and right actions use the same centered action surface. High-contrast hover and focus indicators belong to the close target, not a second border around its action container. + +Wrapped tabs reserve the same action space before and after row classification; upper-row markers and row-end markers must not trigger another fit-tab reflow. Selection must not change a tab's width or margin. Shoulder space is reserved only after the final tab, independently of selection; intermediate shoulders paint over adjacent tab surfaces without inserting a gap. Automatic reveal includes the complete shoulder and rounds fractional layout bounds outward so the selected action is not clipped. Manual scrolling can still move part of the selected tab, including its action, offscreen. In that case a stationary cap and shoulder finish the visible outline; their stroke is aligned with the document separator, and the clipping mask falls back to `editor.background` when the theme does not define a tab-strip background. -The connected design uses `editor.background` for the active tab on every row, its action area, outside stroke, both shoulders, and the strip separator so the selected tab and editor body read as one document well. Upper wrapped and separate pinned rows change only the tab shape, not its selected background. The strip and inactive tabs use `editorGroupHeader.tabsBackground`, and their hover fill is derived from `foreground` over that background. Existing tab foreground customizations continue to apply. `modernEditorTab.activeBackground` remains available to the pill style. +The connected design uses `editor.background` for the active tab; its action container is transparent so it cannot cover the cap stroke in compact rows. The selected tab and editor body read as one document well. Its outside stroke, shoulders, and strip separator use `editorGroupHeader.tabsBorder`, falling back to `tab.border`. Upper wrapped and separate pinned rows change only the tab shape, not its selected background. The strip and inactive tabs use `editorGroupHeader.connectedTabsBackground`, and their hover fill is derived from `foreground` over that background. Existing tab foreground customizations continue to apply. `modernEditorTab.activeBackground` remains available to the pill style. -The default palettes give `editorGroupHeader.tabsBackground` a restrained neutral fill using existing palette colors: Dark 2026 uses `#202122`, Light 2026 uses `#EAEAEA`, Dark Modern uses `#2B2B2B`, Light Modern uses `#E5E5E5`, the classic dark themes use `#303031`, and the classic light themes use `#E8E8E8`. `tab.inactiveBackground` matches the strip. These are theme-level values, so classic tabs also receive the updated inactive fill. Dark+/Light+ inherit the change from their Visual Studio base themes; HC and the other bundled background palettes are unchanged. No new color ID or cross-component token dependency is introduced. +`editorGroupHeader.connectedTabsBackground` defaults to `editorGroupHeader.tabsBackground`, so themes can keep their established strip palette without customization. The bundled connected palettes are Dark 2026 `#202122`, Light 2026 `#EAEAEA`, Dark Modern `#2B2B2B`, Light Modern `#E5E5E5`, the classic dark themes `#303031`, the classic light themes `#E8E8E8`, and Quiet Light `#E4E4E4`. Their original global `editorGroupHeader.tabsBackground` and `tab.inactiveBackground` values remain unchanged. Shared connected theme derivations use `editorGroupHeader.tabsBackground` outside Modern UI; only Modern UI editor tabs adopt the dedicated connected-strip palette. Agents-window tabs retain their existing appearance. Connected tabs use `tab.inactiveForeground` rather than dimming the general foreground to 50% opacity. The default palettes pair these fills with readable inactive text (at least 4.5:1), including in inactive editor groups. Light Modern and the classic light themes use the existing neutral `#616161`; the classic dark themes use `#A6A6A6`. Explicit legacy foreground customizations still take precedence. HC styling and the original pill label defaults are unchanged. @@ -46,6 +52,7 @@ In high contrast, the connected boundary uses `focusBorder` for the active edito | `modernTab.activeForeground` | Foreground of active Modern UI tabs | `list.inactiveSelectionForeground`, then `foreground` | | `modernTab.hoverBackground` | Background of hovered Modern UI tabs | `list.hoverBackground` | | `modernTab.hoverForeground` | Foreground of hovered Modern UI tabs | `list.hoverForeground`, then `foreground` | +| `editorGroupHeader.connectedTabsBackground` | Background of connected tabs in editor group title headers | `editorGroupHeader.tabsBackground` | | `modernEditorTab.activeBackground` | Background of active Modern UI editor tabs | `modernTab.activeBackground` | | `modernEditorTab.activeActionBackground` | Opaque background of actions on active Modern UI editor tabs | `modernEditorTab.activeBackground` composited over `editor.background` | | `modernEditorTab.activeForeground` | Foreground of active Modern UI editor tabs | `modernTab.activeForeground` | diff --git a/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts b/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts index df9d6fd0ba8507..364a06049289ee 100644 --- a/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts +++ b/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts @@ -5,23 +5,37 @@ import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; -import { EDITOR_GROUP_HEADER_TABS_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND } from '../../../common/theme.js'; +import { CONNECTED_EDITOR_TABS_SELECTOR } from '../../../browser/parts/editor/editor.js'; +import { EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND, EDITOR_GROUP_HEADER_TABS_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND } from '../../../common/theme.js'; import './media/connectedEditorTabs.css'; +const connectedTabsSelector = `.monaco-workbench.modern-ui-tabs${CONNECTED_EDITOR_TABS_SELECTOR}`; +const connectedEditorTabsSelector = `${connectedTabsSelector}.modern-ui`; + registerThemingParticipant((theme, collector) => { const background = theme.getColor(editorBackground); if (background) { - collector.addRule(`.monaco-workbench.modern-ui.modern-ui-connected-editor-tabs { --modern-ui-connected-tab-surface: ${background}; }`); + collector.addRule(`${connectedTabsSelector} { --modern-ui-connected-tab-surface: ${background}; }`); } - const stripColor = theme.getColor(EDITOR_GROUP_HEADER_TABS_BACKGROUND) ?? background; - const stripBackground = background && stripColor ? stripColor.makeOpaque(background) : stripColor; - if (stripBackground) { - collector.addRule(`.monaco-workbench.modern-ui.modern-ui-connected-editor-tabs { --modern-ui-connected-tab-strip-background: ${stripBackground}; }`); + const defaultStripColor = theme.getColor(EDITOR_GROUP_HEADER_TABS_BACKGROUND) ?? background; + const defaultStripBackground = background && defaultStripColor ? defaultStripColor.makeOpaque(background) : defaultStripColor; + if (defaultStripBackground) { + collector.addRule(`${connectedTabsSelector} { --modern-ui-connected-tab-strip-background: ${defaultStripBackground}; }`); } + const hoverBackground = theme.getColor(MODERN_EDITOR_TAB_HOVER_BACKGROUND); - if (stripBackground && hoverBackground) { + if (defaultStripBackground && hoverBackground) { // Flatten against the strip, not the document, so the action mask and pill paint one surface. - collector.addRule(`.monaco-workbench.modern-ui.modern-ui-connected-editor-tabs { --modern-ui-connected-tab-upper-hover-background: ${hoverBackground.makeOpaque(stripBackground)}; }`); + collector.addRule(`${connectedTabsSelector} { --modern-ui-connected-tab-upper-hover-background: ${hoverBackground.makeOpaque(defaultStripBackground)}; }`); + } + + const editorStripColor = theme.getColor(EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND) ?? defaultStripColor; + const editorStripBackground = background && editorStripColor ? editorStripColor.makeOpaque(background) : editorStripColor; + if (editorStripBackground) { + collector.addRule(`${connectedEditorTabsSelector} { --modern-ui-connected-tab-strip-background: ${editorStripBackground}; }`); + } + if (editorStripBackground && hoverBackground) { + collector.addRule(`${connectedEditorTabsSelector} { --modern-ui-connected-tab-upper-hover-background: ${hoverBackground.makeOpaque(editorStripBackground)}; }`); } }); diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 8e7b15832dfafe..7433d8ed1738d4 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -3,8 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* Keep every rule in this module scoped to `.modern-ui-connected-editor-tabs`. + * The stylesheet is always bundled, but connected-tab geometry must remain inert without that root marker. */ + /* The connected tab setting is scoped to Modern UI; other modern tabs retain their pills. */ -/* In HC the connected well owns the frame; retain the outer card's layout space, not its stroke. */ +/* In HC the editor group owns one continuous frame; retain the outer card's layout space, not its stroke. */ .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor.editor-tabs-multiple:not(.modal-editor-part) { border-color: transparent; --modern-ui-editor-border-color: transparent; @@ -12,7 +15,7 @@ } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container { - --modern-ui-connected-tab-border: var(--modern-ui-connected-tab-surface); + --modern-ui-connected-tab-border: var(--vscode-editorGroupHeader-tabsBorder, var(--vscode-tab-border, var(--modern-ui-connected-tab-surface))); --modern-ui-connected-well-border: transparent; } @@ -25,42 +28,21 @@ --modern-ui-connected-tab-border: var(--vscode-focusBorder); } -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor.editor-tabs-multiple:not(.modal-editor-part) > .content.empty .editor-group-container.empty { +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor.editor-tabs-multiple:not(.modal-editor-part) > .content .editor-group-container { position: relative; } -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor.editor-tabs-multiple:not(.modal-editor-part) > .content.empty .editor-group-container.empty::after { +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor.editor-tabs-multiple:not(.modal-editor-part) > .content .editor-group-container::after { content: ''; position: absolute; inset: 0; border: var(--vscode-strokeThickness) solid var(--modern-ui-connected-well-border); + border-radius: var(--vscode-cornerRadius-large); box-sizing: border-box; pointer-events: none; z-index: 10; } -/* Reserve the same inside stroke in every theme without changing the editor's layout box. */ -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs ~ .editor-container, -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .breadcrumbs-below-tabs { - position: relative; -} - -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs ~ .editor-container::after, -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .breadcrumbs-below-tabs::after { - content: ''; - position: absolute; - inset: 0; - box-sizing: border-box; - border: var(--vscode-strokeThickness) solid var(--modern-ui-connected-well-border); - border-top: 0; - pointer-events: none; - z-index: 10; -} - -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .breadcrumbs-below-tabs::after { - border-bottom: 0; -} - .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active.multi-selected:not(:focus) > .tab-fill { outline: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder); outline-offset: calc(-1 * (var(--vscode-spacing-size20) + var(--vscode-strokeThickness))); @@ -82,24 +64,39 @@ --modern-ui-connected-tab-radius: var(--vscode-cornerRadius-small); --modern-ui-connected-tab-cap-radius: calc(var(--modern-ui-connected-tab-radius) + var(--vscode-strokeThickness)); --modern-ui-connected-tab-shoulder-radius: var(--modern-ui-connected-tab-cap-radius); - --modern-ui-connected-tab-gutter: calc(var(--vscode-spacing-size40) + var(--vscode-strokeThickness)); - --modern-ui-connected-tab-top-inset: var(--vscode-spacing-size40); - --modern-ui-tab-action-padding: calc(var(--vscode-spacing-size280) + var(--modern-ui-connected-tab-shoulder-radius)); + --modern-ui-connected-tab-action-inset: calc((var(--editor-group-tab-height) - var(--vscode-spacing-size200)) / 2 + var(--vscode-spacing-size40)); + --modern-ui-tab-action-padding: calc(var(--vscode-spacing-size200) + 2 * var(--modern-ui-connected-tab-action-inset)); } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container { background-color: var(--modern-ui-connected-tab-strip-background, var(--vscode-editorGroupHeader-tabsBackground)); padding-left: 0; + padding-top: 0; +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container:not(.wrapping) { + align-items: flex-start; +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container:not(.wrapping) > .editor-actions { + align-self: stretch; + height: auto; + margin-bottom: var(--vscode-strokeThickness); } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container { padding-bottom: var(--vscode-strokeThickness); } +/* Bottom rows occupy the strip directly so their edges cannot round apart from compensating pill gutters at fractional zoom. */ +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:not(.connected-tab-upper-row) { + border-block-width: 0; +} + .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab > .tab-fill { inset-inline: 0; - top: calc(-1 * var(--modern-ui-connected-tab-top-inset)); - bottom: calc(-1 * var(--modern-ui-connected-tab-gutter)); + top: 0; + bottom: calc(-1 * var(--vscode-strokeThickness)); } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .sticky-tabs-background, @@ -124,31 +121,18 @@ background-color: var(--modern-ui-connected-tab-border); } -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.wrapping { - --modern-ui-connected-tab-gutter: var(--vscode-spacing-size20); - --modern-ui-connected-tab-top-inset: var(--vscode-spacing-size20); -} - /* Wrapped fills occupy their row's hit box, without an extra gutter below the final row. */ .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.wrapping > .monaco-scrollable-element { padding-bottom: 0; } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.wrapping .tabs-container { + --modern-ui-connected-tab-action-inset: calc((var(--editor-group-tab-height) - var(--vscode-spacing-size200)) / 2 + var(--vscode-spacing-size20)); + --modern-ui-tab-action-padding: calc(var(--vscode-spacing-size200) + 2 * var(--modern-ui-connected-tab-action-inset)); padding-bottom: 0; row-gap: var(--vscode-spacing-size20); } -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs.two-tab-bars > .tabs-and-actions-container:first-child { - --modern-ui-connected-tab-gutter: calc(var(--vscode-spacing-size20) + var(--vscode-strokeThickness)); - --modern-ui-connected-tab-top-inset: var(--vscode-spacing-size60); -} - -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs.two-tab-bars > .tabs-and-actions-container:not(:first-child) { - --modern-ui-connected-tab-gutter: calc(var(--vscode-spacing-size60) + var(--vscode-strokeThickness)); - --modern-ui-connected-tab-top-inset: var(--vscode-spacing-size20); -} - /* Preserve the existing explicit selection and focus borders in high contrast. */ .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:not(:is(.hc-black, .hc-light)) .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.selected:not(.active).tab-border-top > .tab-fill, .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:not(:is(.hc-black, .hc-light)) .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active.multi-selected.tab-border-top > .tab-fill, @@ -162,11 +146,12 @@ --modern-ui-editor-tab-unfocused-active-border-top: var(--modern-ui-connected-tab-border); --modern-ui-editor-tab-active-border: transparent; --modern-ui-editor-tab-unfocused-active-border: transparent; - top: calc(-1 * var(--modern-ui-connected-tab-top-inset)); + top: 0; inset-inline: 0; - bottom: calc(-1 * var(--modern-ui-connected-tab-gutter) - var(--vscode-strokeThickness)); + bottom: calc(-2 * var(--vscode-strokeThickness)); z-index: 1; background-color: var(--modern-ui-connected-tab-surface); + background-clip: padding-box; border: var(--vscode-strokeThickness) solid var(--modern-ui-connected-tab-border); border-bottom-color: transparent; border-radius: var(--modern-ui-connected-tab-cap-radius) var(--modern-ui-connected-tab-cap-radius) 0 0; @@ -206,9 +191,9 @@ .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active > .tab-connected-edge { display: block; position: absolute; - top: calc(-1 * var(--modern-ui-connected-tab-top-inset)); + top: 0; inset-inline: 0; - bottom: calc(-1 * var(--modern-ui-connected-tab-gutter) - var(--vscode-strokeThickness)); + bottom: calc(-2 * var(--vscode-strokeThickness)); z-index: 8; pointer-events: none; } @@ -284,11 +269,11 @@ } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active > .tab-actions { - --modern-ui-connected-tab-action-background: var(--modern-ui-connected-tab-surface); - --modern-ui-editor-tab-action-active-background: var(--modern-ui-connected-tab-surface); - --modern-ui-editor-tab-action-unfocused-active-background: var(--modern-ui-connected-tab-surface); - --modern-ui-editor-tab-action-active-hover-background: var(--modern-ui-connected-tab-surface); - --modern-ui-editor-tab-action-unfocused-active-hover-background: var(--modern-ui-connected-tab-surface); + --modern-ui-connected-tab-action-background: transparent; + --modern-ui-editor-tab-action-active-background: transparent; + --modern-ui-editor-tab-action-unfocused-active-background: transparent; + --modern-ui-editor-tab-action-active-hover-background: transparent; + --modern-ui-editor-tab-action-unfocused-active-hover-background: transparent; border-radius: 0 var(--modern-ui-connected-tab-radius) 0 0; } @@ -296,20 +281,65 @@ border-radius: var(--modern-ui-connected-tab-radius) 0 0 0; } -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:not(.tab-actions-left) > .tab-actions .actions-container { - justify-content: flex-end; +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab > .tab-actions .actions-container { + justify-content: center; +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab > .tab-actions { + margin-inline: 0; + border-inline-width: 0; } -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.tab-actions-left > .tab-actions .actions-container { - justify-content: flex-start; +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab > .tab-actions > .monaco-action-bar { + width: 100%; } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:not(.sticky-compact):not(.tab-actions-left) > .tab-actions { - right: var(--modern-ui-connected-tab-shoulder-radius); + right: calc(var(--modern-ui-connected-tab-action-inset) - var(--vscode-spacing-size20)); } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:not(.sticky-compact).tab-actions-left > .tab-actions { - left: var(--modern-ui-connected-tab-shoulder-radius); + left: calc(var(--modern-ui-connected-tab-action-inset) - var(--vscode-spacing-size20)); +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab:not(.connected-tab-upper-row) { + --modern-ui-tab-action-padding: calc(var(--vscode-spacing-size200) + 2 * var(--modern-ui-connected-tab-action-inset) + var(--vscode-strokeThickness)); +} + +/* Reserve the connected stroke in every state so selecting a tab only changes its paint. */ +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab:not(.connected-tab-upper-row):not(.sticky-compact):not(.tab-actions-left) > .tab-actions { + right: calc(var(--modern-ui-connected-tab-action-inset) - var(--vscode-spacing-size20) + var(--vscode-strokeThickness)); +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab:not(.connected-tab-upper-row):not(:first-child):not(.sticky-compact).tab-actions-left > .tab-actions { + left: calc(var(--modern-ui-connected-tab-action-inset) - var(--vscode-spacing-size20) + var(--vscode-strokeThickness)); +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab:not(.connected-tab-upper-row) { + --modern-ui-connected-tab-action-inset: calc((var(--editor-group-tab-height) - var(--vscode-spacing-size200) - var(--vscode-strokeThickness)) / 2 + var(--vscode-spacing-size40)); +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab:not(.connected-tab-upper-row) > .tab-actions { + top: var(--vscode-strokeThickness); +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-and-actions-container .tabs-container > .tab:is(:hover, :focus-within) > .tab-actions { + border-color: transparent; + border-inline-width: 0; +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor > .content .editor-group-container > .title.tabs .tab-actions .action-label:hover:where(:not(.disabled)) { + outline: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder); + outline-offset: calc(-1 * var(--vscode-strokeThickness)); +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor > .content .editor-group-container > .title.tabs .tab-actions .action-label:focus { + outline: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); + outline-offset: calc(-1 * var(--vscode-strokeThickness)); +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.wrapping .tabs-container > .tab.last-in-row + .tab:not(.connected-tab-upper-row):not(.sticky-compact).tab-actions-left > .tab-actions { + left: calc(var(--modern-ui-connected-tab-action-inset) - var(--vscode-spacing-size20) - var(--vscode-strokeThickness)); } /* Keep the complete identity and action column; only the basename can ellipsize. */ @@ -378,8 +408,8 @@ content: none; } -/* Terminal shoulders keep additional space rather than being cut off. */ -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active:not(.connected-tab-upper-row):is(:last-child, .last-in-row) { +/* Reserve terminal shoulder space independently of selection, never between adjacent tabs. */ +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab:not(.connected-tab-upper-row):last-child { margin-right: var(--modern-ui-connected-tab-shoulder-radius); } @@ -437,6 +467,11 @@ --modern-ui-editor-tab-action-unfocused-hover-background: var(--modern-ui-editor-tab-hover-background); } +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab.connected-tab-upper-row { + --modern-ui-connected-tab-action-inset: calc((var(--editor-group-tab-height) - var(--vscode-spacing-size200)) / 2); + --modern-ui-tab-action-padding: calc(var(--vscode-spacing-size200) + 2 * var(--modern-ui-connected-tab-action-inset) + var(--vscode-spacing-size20)); +} + .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row > .tab-fill { inset: 0 var(--vscode-spacing-size20); border-radius: var(--vscode-cornerRadius-small); @@ -445,6 +480,28 @@ .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.wrapping .tabs-container > .tab.connected-tab-upper-row > .tab-fill { inset-block: calc(-1 * var(--vscode-spacing-size20)); + inset-inline: 0; +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active.connected-tab-top-row:not(.connected-tab-upper-row) > .tab-fill { + border-top-color: transparent; +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:first-child:not(.connected-tab-upper-row) > .tab-fill, +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.wrapping .tabs-container > .tab:first-child > .tab-fill, +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.last-in-row + .tab > .tab-fill { + left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left-color: transparent; +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab.connected-tab-upper-row:not(.sticky-compact):not(.tab-actions-left) > .tab-actions { + right: var(--modern-ui-connected-tab-action-inset); +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab.connected-tab-upper-row:not(.sticky-compact).tab-actions-left > .tab-actions { + left: var(--modern-ui-connected-tab-action-inset); } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row.active > .tab-fill { diff --git a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css index 1046eee3480cb2..cca77a2681312a 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css @@ -779,38 +779,38 @@ background-color: transparent !important; } -:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:is(.active, .selected):hover > .tab-actions { +:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench:not(.modern-ui-connected-editor-tabs) .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:is(.active, .selected):hover > .tab-actions { border-top: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder); border-right: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder); border-bottom: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder); box-sizing: border-box; } -:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.active):not(.selected):hover > .tab-actions { +:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench:not(.modern-ui-connected-editor-tabs) .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.active):not(.selected):hover > .tab-actions { border-top: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder); border-right: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder); border-bottom: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder); box-sizing: border-box; } -:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab > .tab-actions:focus-within { +:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench:not(.modern-ui-connected-editor-tabs) .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab > .tab-actions:focus-within { border-top: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); border-right: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); border-bottom: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); box-sizing: border-box; } -:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:is(.active, .selected).tab-actions-left:hover > .tab-actions { +:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench:not(.modern-ui-connected-editor-tabs) .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:is(.active, .selected).tab-actions-left:hover > .tab-actions { border-right: 0; border-left: var(--vscode-strokeThickness) solid var(--vscode-contrastActiveBorder); } -:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.active):not(.selected).tab-actions-left:hover > .tab-actions { +:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench:not(.modern-ui-connected-editor-tabs) .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab:not(.active):not(.selected).tab-actions-left:hover > .tab-actions { border-right: 0; border-left: var(--vscode-strokeThickness) dashed var(--vscode-contrastActiveBorder); } -:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.tab-actions-left > .tab-actions:focus-within { +:is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench:not(.modern-ui-connected-editor-tabs) .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.tab-actions-left > .tab-actions:focus-within { border-right: 0; border-left: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); } diff --git a/src/vs/workbench/contrib/modernUI/browser/modernUI.contribution.ts b/src/vs/workbench/contrib/modernUI/browser/modernUI.contribution.ts index 3058363e289e06..041cc46f7450ed 100644 --- a/src/vs/workbench/contrib/modernUI/browser/modernUI.contribution.ts +++ b/src/vs/workbench/contrib/modernUI/browser/modernUI.contribution.ts @@ -16,6 +16,7 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase import { DEFAULT_SCROLLBAR_SIZE, setGlobalDefaultScrollbarSize } from '../../../../base/browser/ui/scrollbar/scrollableElement.js'; import { COMPACT_NOTIFICATION_ROW_HEIGHT, DEFAULT_NOTIFICATION_ROW_HEIGHT, setNotificationRowHeight } from '../../../browser/parts/notifications/notificationsViewer.js'; import { DEFAULT_PANE_HEADER_SIZE, setGlobalPaneHeaderSize } from '../../../../base/browser/ui/splitview/paneview.js'; +import { CONNECTED_EDITOR_TABS_CLASS } from '../../../browser/parts/editor/editor.js'; /** Reduced scrollbar size (px) applied when Modern UI is on. */ const MODERN_UI_SCROLLBAR_SIZE = 8; @@ -58,7 +59,6 @@ interface IModernUIModule { const MODERN_UI_CLASS = 'modern-ui'; const MODERN_UI_COMPACT_CLASS = 'modern-ui-compact'; const MODERN_UI_TABS_CLASS = 'modern-ui-tabs'; -const MODERN_UI_CONNECTED_EDITOR_TABS_CLASS = 'modern-ui-connected-editor-tabs'; const MODERN_UI_NOTIFICATIONS_DIALOGS_CLASS = 'modern-ui-notifications-dialogs'; const MODERN_UI_UPPERCASE_VIEW_HEADERS_CLASS = 'modern-ui-uppercase-view-headers'; @@ -218,7 +218,7 @@ export class ModernUIContribution extends Disposable implements IWorkbenchContri container.classList.toggle(MODERN_UI_CLASS, enabled); container.classList.toggle(MODERN_UI_COMPACT_CLASS, compact); container.classList.toggle(MODERN_UI_TABS_CLASS, enabled); - container.classList.toggle(MODERN_UI_CONNECTED_EDITOR_TABS_CLASS, enabled && this.useConnectedEditorTabs()); + container.classList.toggle(CONNECTED_EDITOR_TABS_CLASS, enabled && this.useConnectedEditorTabs()); container.classList.toggle(MODERN_UI_NOTIFICATIONS_DIALOGS_CLASS, enabled); container.classList.toggle(MODERN_UI_UPPERCASE_VIEW_HEADERS_CLASS, useUppercaseViewHeaders); } @@ -245,7 +245,7 @@ export class ModernUIContribution extends Disposable implements IWorkbenchContri container.classList.remove(MODERN_UI_CLASS); container.classList.remove(MODERN_UI_COMPACT_CLASS); container.classList.remove(MODERN_UI_TABS_CLASS); - container.classList.remove(MODERN_UI_CONNECTED_EDITOR_TABS_CLASS); + container.classList.remove(CONNECTED_EDITOR_TABS_CLASS); container.classList.remove(MODERN_UI_NOTIFICATIONS_DIALOGS_CLASS); container.classList.remove(MODERN_UI_UPPERCASE_VIEW_HEADERS_CLASS); } diff --git a/src/vs/workbench/contrib/modernUI/test/browser/modernUI.contribution.test.ts b/src/vs/workbench/contrib/modernUI/test/browser/modernUI.contribution.test.ts index 58bad3e0bb9c89..75d71dbb4550c4 100644 --- a/src/vs/workbench/contrib/modernUI/test/browser/modernUI.contribution.test.ts +++ b/src/vs/workbench/contrib/modernUI/test/browser/modernUI.contribution.test.ts @@ -27,7 +27,7 @@ import { Registry } from '../../../../../platform/registry/common/platform.js'; import { activeContrastBorder, editorBackground, Extensions as ColorRegistryExtensions, IColorRegistry, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, oneOf, opaque, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; import { foreground } from '../../../../../platform/theme/common/colors/baseColors.js'; import { Extensions as ThemeServiceExtensions, IThemingRegistry } from '../../../../../platform/theme/common/themeService.js'; -import { EDITOR_BORDER, MODERN_ACTIVITY_BAR_BACKGROUND, MODERN_ACTIVITY_BAR_BORDER, MODERN_ACTIVITY_BAR_INACTIVE_BACKGROUND, MODERN_ACTIVITY_BAR_ITEM_ACTIVE_BACKGROUND, MODERN_ACTIVITY_BAR_ITEM_ACTIVE_FOREGROUND, MODERN_ACTIVITY_BAR_ITEM_HOVER_BACKGROUND, MODERN_ACTIVITY_BAR_ITEM_HOVER_FOREGROUND, MODERN_EDITOR_TAB_ACTIVE_ACTION_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_FOREGROUND, MODERN_EDITOR_TAB_ACTIVE_HOVER_ACTION_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND, MODERN_EDITOR_TAB_HOVER_ACTION_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND, MODERN_EDITOR_TAB_HOVER_FOREGROUND, MODERN_EDITOR_TAB_INACTIVE_BACKGROUND, MODERN_EDITOR_TAB_SELECTED_ACTION_BACKGROUND, MODERN_PANEL_BORDER, MODERN_SASH_GRIP_FOREGROUND, MODERN_TAB_ACTIVE_BACKGROUND, MODERN_TAB_ACTIVE_FOREGROUND, MODERN_TAB_HOVER_BACKGROUND, MODERN_TAB_HOVER_FOREGROUND, MODERN_UI_INACTIVE_SHELL_BACKGROUND, MODERN_UI_SHELL_BACKGROUND, PANEL_SECTION_BORDER, PANEL_SECTION_HEADER_BORDER, SIDE_BAR_SECTION_HEADER_BORDER, SURFACE_BORDER, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_BORDER, TAB_ACTIVE_BORDER_TOP, TAB_ACTIVE_FOREGROUND, TAB_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_HOVER_FOREGROUND, TAB_INACTIVE_BACKGROUND, TAB_INACTIVE_FOREGROUND, TAB_LAST_PINNED_BORDER, TAB_SELECTED_BACKGROUND, TAB_UNFOCUSED_HOVER_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_INACTIVE_BACKGROUND } from '../../../../common/theme.js'; +import { EDITOR_BORDER, EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND, EDITOR_GROUP_HEADER_TABS_BACKGROUND, MODERN_ACTIVITY_BAR_BACKGROUND, MODERN_ACTIVITY_BAR_BORDER, MODERN_ACTIVITY_BAR_INACTIVE_BACKGROUND, MODERN_ACTIVITY_BAR_ITEM_ACTIVE_BACKGROUND, MODERN_ACTIVITY_BAR_ITEM_ACTIVE_FOREGROUND, MODERN_ACTIVITY_BAR_ITEM_HOVER_BACKGROUND, MODERN_ACTIVITY_BAR_ITEM_HOVER_FOREGROUND, MODERN_EDITOR_TAB_ACTIVE_ACTION_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_FOREGROUND, MODERN_EDITOR_TAB_ACTIVE_HOVER_ACTION_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND, MODERN_EDITOR_TAB_HOVER_ACTION_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND, MODERN_EDITOR_TAB_HOVER_FOREGROUND, MODERN_EDITOR_TAB_INACTIVE_BACKGROUND, MODERN_EDITOR_TAB_SELECTED_ACTION_BACKGROUND, MODERN_PANEL_BORDER, MODERN_SASH_GRIP_FOREGROUND, MODERN_TAB_ACTIVE_BACKGROUND, MODERN_TAB_ACTIVE_FOREGROUND, MODERN_TAB_HOVER_BACKGROUND, MODERN_TAB_HOVER_FOREGROUND, MODERN_UI_INACTIVE_SHELL_BACKGROUND, MODERN_UI_SHELL_BACKGROUND, PANEL_SECTION_BORDER, PANEL_SECTION_HEADER_BORDER, SIDE_BAR_SECTION_HEADER_BORDER, SURFACE_BORDER, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_BORDER, TAB_ACTIVE_BORDER_TOP, TAB_ACTIVE_FOREGROUND, TAB_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_HOVER_FOREGROUND, TAB_INACTIVE_BACKGROUND, TAB_INACTIVE_FOREGROUND, TAB_LAST_PINNED_BORDER, TAB_SELECTED_BACKGROUND, TAB_UNFOCUSED_HOVER_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_INACTIVE_BACKGROUND } from '../../../../common/theme.js'; import { TestEnvironmentService, TestLayoutService } from '../../../../test/browser/workbenchTestServices.js'; import { LayoutSettings, ModernUIDensity, ModernUIEditorTabStyle } from '../../../../services/layout/browser/layoutService.js'; import { PRESERVE_MERGED_WORKSPACE_NAME_CASE_CLASS, PRESERVE_WORKSPACE_NAME_CASE_CLASS, shouldPreserveWorkspaceNameCase } from '../../../files/browser/views/explorerView.js'; @@ -1969,7 +1969,7 @@ suite('ModernUIContribution', () => { assert.deepStrictEqual(backgrounds, ['#1f1f1f', '#ffffff', '#1f1f1f']); }); - test('uses the editor background for the connected surface and the editor group header background for the strip', () => { + test('uses separate connected strip and border colors without changing the editor group header', () => { const root = document.createElement('div'); root.className = 'monaco-workbench modern-ui modern-ui-tabs modern-ui-connected-editor-tabs'; root.style.setProperty('--vscode-spacing-size20', '2px'); @@ -1984,6 +1984,8 @@ suite('ModernUIContribution', () => { const group = appendElement(content, 'editor-group-container active'); const title = appendElement(group, 'title tabs'); const row = appendElement(title, 'tabs-and-actions-container'); + const headerProbe = appendElement(title, 'header-color-probe'); + headerProbe.style.backgroundColor = 'var(--vscode-editorGroupHeader-tabsBackground)'; const tabs = appendElement(row, 'tabs-container'); appendElement(tabs, 'tab'); const tab = appendElement(tabs, 'tab active'); @@ -1997,7 +1999,9 @@ suite('ModernUIContribution', () => { const theme = ColorThemeData.createUnloadedTheme('vs', { [editorBackground]: '#ffffff', 'editorGroup.border': '#123456', + 'editorGroupHeader.tabsBorder': '#445566', 'editorGroupHeader.tabsBackground': '#eeeeee', + [EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND]: '#ddeeff', [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND]: '#abcdef', }); theme.setCustomColors({ [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND]: color }); @@ -2011,19 +2015,52 @@ suite('ModernUIContribution', () => { targetWindow.getComputedStyle(row, '::after').backgroundColor, targetWindow.getComputedStyle(actions).backgroundColor, ], + header: targetWindow.getComputedStyle(headerProbe).backgroundColor, strip: targetWindow.getComputedStyle(row).backgroundColor, editorSplitBorder: theme.getColor('editorGroup.border')?.toString(), }; }); assert.deepStrictEqual(results, [ - { surface: Array(6).fill('rgb(255, 255, 255)'), strip: 'rgb(238, 238, 238)', editorSplitBorder: '#123456' }, - { surface: Array(6).fill('rgb(255, 255, 255)'), strip: 'rgb(238, 238, 238)', editorSplitBorder: '#123456' }, - { surface: Array(6).fill('rgb(255, 255, 255)'), strip: 'rgb(238, 238, 238)', editorSplitBorder: '#123456' }, + { surface: ['rgb(255, 255, 255)', ...Array(4).fill('rgb(68, 85, 102)'), 'rgba(0, 0, 0, 0)'], header: 'rgb(238, 238, 238)', strip: 'rgb(221, 238, 255)', editorSplitBorder: '#123456' }, + { surface: ['rgb(255, 255, 255)', ...Array(4).fill('rgb(68, 85, 102)'), 'rgba(0, 0, 0, 0)'], header: 'rgb(238, 238, 238)', strip: 'rgb(221, 238, 255)', editorSplitBorder: '#123456' }, + { surface: ['rgb(255, 255, 255)', ...Array(4).fill('rgb(68, 85, 102)'), 'rgba(0, 0, 0, 0)'], header: 'rgb(238, 238, 238)', strip: 'rgb(221, 238, 255)', editorSplitBorder: '#123456' }, ]); }); - test('matches upper-row tab action backgrounds to the composited pill surface', () => { + test('keeps the shared connected strip on the standard header background outside Modern UI', () => { + const root = appendElement(document.body, 'monaco-workbench modern-ui-tabs modern-ui-connected-editor-tabs'); + store.add(toDisposable(() => root.remove())); + const style = document.createElement('style'); + root.appendChild(style); + const stripProbe = appendElement(root, 'strip-color-probe'); + stripProbe.style.backgroundColor = 'var(--modern-ui-connected-tab-strip-background)'; + const hoverProbe = appendElement(root, 'hover-color-probe'); + hoverProbe.style.backgroundColor = 'var(--modern-ui-connected-tab-upper-hover-background)'; + const theme = ColorThemeData.createUnloadedTheme('vs', { + [editorBackground]: '#ffffff', + [EDITOR_GROUP_HEADER_TABS_BACKGROUND]: '#f8f8f8', + [EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND]: '#e5e5e5', + [MODERN_EDITOR_TAB_HOVER_BACKGROUND]: '#00000080', + }); + style.textContent = generateColorThemeCSS(theme, '.monaco-workbench', themingRegistry.getThemingParticipants(), TestEnvironmentService).code; + const targetWindow = getWindow(root); + const colors = () => ({ + strip: targetWindow.getComputedStyle(stripProbe).backgroundColor, + hover: targetWindow.getComputedStyle(hoverProbe).backgroundColor, + }); + + const shared = colors(); + root.classList.add('modern-ui'); + const editor = colors(); + + assert.deepStrictEqual({ shared, editor }, { + shared: { strip: 'rgb(248, 248, 248)', hover: 'rgb(123, 123, 123)' }, + editor: { strip: 'rgb(229, 229, 229)', hover: 'rgb(114, 114, 114)' }, + }); + }); + + test('keeps selected pill surfaces visible beneath transparent tab actions', () => { const root = appendElement(document.body, 'monaco-workbench modern-ui modern-ui-tabs modern-ui-connected-editor-tabs'); store.add(toDisposable(() => root.remove())); const style = document.createElement('style'); @@ -2076,7 +2113,7 @@ suite('ModernUIContribution', () => { }, { fill: active ? 'rgb(17, 34, 51)' : highContrast ? 'rgba(0, 0, 0, 0)' : stripColor, hover: expected, - actions: active ? 'rgb(17, 34, 51)' : highContrast ? stripColor : expected, + actions: active ? 'rgba(0, 0, 0, 0)' : highContrast ? stripColor : expected, }, `${themeId}, ${hover}, compact: ${compact}, focused group: ${focusedGroup}, actions left: ${actionsLeft}, active: ${active}`); } } @@ -2135,7 +2172,7 @@ suite('ModernUIContribution', () => { test('retains the empty root connected editor frame in high contrast', () => { const root = appendElement(document.body, 'monaco-workbench modern-ui modern-ui-tabs modern-ui-connected-editor-tabs'); store.add(toDisposable(() => root.remove())); - root.style.cssText = '--vscode-strokeThickness: 1px; --vscode-focusBorder: #ffaa00; --vscode-contrastBorder: #00aaff;'; + root.style.cssText = '--vscode-strokeThickness: 1px; --vscode-cornerRadius-large: 8px; --vscode-focusBorder: #ffaa00; --vscode-contrastBorder: #00aaff;'; const editor = appendElement(root, 'part editor editor-tabs-multiple'); const content = appendElement(editor, 'content empty'); const group = appendElement(content, 'editor-group-container empty active'); @@ -2150,14 +2187,14 @@ suite('ModernUIContribution', () => { content.classList.toggle('empty', empty); group.classList.toggle('empty', empty); const frame = targetWindow.getComputedStyle(group, '::after'); - const hasFrame = empty && theme.startsWith('hc-'); + const hasFrame = theme.startsWith('hc-'); assert.deepStrictEqual({ frame: frame.content, - border: hasFrame ? [frame.borderTopWidth, frame.borderTopColor, frame.pointerEvents] : undefined, + border: hasFrame ? [frame.borderTopWidth, frame.borderTopColor, frame.borderRadius, frame.pointerEvents] : undefined, hiddenHosts: empty ? [title, body].map(element => targetWindow.getComputedStyle(element).display) : undefined, }, { frame: hasFrame ? '""' : 'none', - border: hasFrame ? ['1px', 'rgb(255, 170, 0)', 'none'] : undefined, + border: hasFrame ? ['1px', 'rgb(255, 170, 0)', '8px', 'none'] : undefined, hiddenHosts: empty ? ['none', 'none'] : undefined, }, `${theme}, floating: ${floating}, empty: ${empty}`); } @@ -2165,7 +2202,7 @@ suite('ModernUIContribution', () => { } }); - test('keeps upper-row terminal pills symmetric in wrapped and separate pinned rows', () => { + test('removes wrapped upper-row gutters while preserving separate pinned-row pills', () => { const root = appendElement(document.body, 'monaco-workbench modern-ui modern-ui-tabs modern-ui-connected-editor-tabs'); store.add(toDisposable(() => root.remove())); root.style.cssText = '--vscode-spacing-size20: 2px; --vscode-cornerRadius-small: 4px; --vscode-strokeThickness: 1px;'; @@ -2183,7 +2220,7 @@ suite('ModernUIContribution', () => { for (const rowEnd of ['', 'last-in-row', 'connected-tab-right-edge']) { tab.className = `tab active connected-tab-upper-row ${rowEnd}`; const style = getWindow(fill).getComputedStyle(fill); - assert.deepStrictEqual([style.left, style.right], ['2px', '2px'], `pinned row: ${pinnedRow}, row end: ${rowEnd}`); + assert.deepStrictEqual([style.left, style.right], pinnedRow ? ['2px', '2px'] : ['0px', '0px'], `pinned row: ${pinnedRow}, row end: ${rowEnd}`); } } }); @@ -2249,7 +2286,7 @@ suite('ModernUIContribution', () => { const content = appendElement(editor, 'content'); const group = appendElement(content, 'editor-group-container active'); const title = appendElement(group, 'title'); - const body = appendElement(group, 'editor-container'); + appendElement(group, 'editor-container'); const geometry = () => [editor, content].map(element => { const bounds = element.getBoundingClientRect(); return [bounds.x, bounds.y, bounds.width, bounds.height]; @@ -2268,19 +2305,19 @@ suite('ModernUIContribution', () => { editor.classList.toggle('editor-tabs-multiple', showTabs === 'multiple'); title.classList.toggle('tabs', showTabs === 'multiple'); const style = getWindow(editor).getComputedStyle(editor); - const wellFrame = getWindow(body).getComputedStyle(body, '::after'); + const groupFrame = getWindow(group).getComputedStyle(group, '::after'); const connectedFrame = connected && showTabs === 'multiple'; const singleFrame = connectedFrame && theme.startsWith('hc-'); assert.deepStrictEqual({ geometry: geometry(), border: [style.borderTopWidth, style.borderLeftWidth, style.borderColor], compactStroke: floating && compact ? style.getPropertyValue('--modern-ui-floating-card-stroke-color').trim() : undefined, - wellFrame: [wellFrame.content, wellFrame.borderLeftWidth, singleFrame ? wellFrame.borderLeftColor : undefined], + groupFrame: [groupFrame.content, groupFrame.borderLeftWidth, groupFrame.borderTopWidth, groupFrame.borderRadius, singleFrame ? groupFrame.borderLeftColor : undefined], }, { geometry: baseline, border: ['1px', '1px', singleFrame || (floating && compact) ? 'rgba(0, 0, 0, 0)' : 'rgb(18, 52, 86)'], compactStroke: floating && compact ? (singleFrame ? 'transparent' : '#123456') : undefined, - wellFrame: [connectedFrame ? '""' : 'none', connectedFrame ? '1px' : '0px', singleFrame ? 'rgb(255, 170, 0)' : undefined], + groupFrame: singleFrame ? ['""', '1px', '1px', '8px', 'rgb(255, 170, 0)'] : ['none', '0px', '0px', '0px', undefined], }, `${theme}, floating: ${floating}, connected: ${connected}, compact: ${compact}, showTabs: ${showTabs}`); } } @@ -2292,17 +2329,17 @@ suite('ModernUIContribution', () => { test('connects HC group borders without moving content when themes or active groups change', () => { const root = appendElement(document.body, 'monaco-workbench modern-ui modern-ui-tabs modern-ui-connected-editor-tabs'); store.add(toDisposable(() => root.remove())); - root.style.cssText = 'width: 500px; --vscode-spacing-size20: 2px; --vscode-spacing-size40: 4px; --vscode-cornerRadius-small: 4px; --vscode-strokeThickness: 1px;'; + root.style.cssText = 'width: 500px; color: #cccccc; --vscode-spacing-size20: 2px; --vscode-spacing-size40: 4px; --vscode-cornerRadius-small: 4px; --vscode-cornerRadius-large: 8px; --vscode-strokeThickness: 1px;'; const themeStyle = document.createElement('style'); root.appendChild(themeStyle); - const editor = appendElement(root, 'part editor'); + const editor = appendElement(root, 'part editor editor-tabs-multiple'); const content = appendElement(editor, 'content'); const group = appendElement(content, 'editor-group-container active'); const title = appendElement(group, 'title tabs'); const row = appendElement(title, 'tabs-and-actions-container'); const tabs = appendElement(row, 'tabs-container'); appendElement(appendElement(tabs, 'tab'), 'tab-fill'); - const tab = appendElement(tabs, 'tab active'); + const tab = appendElement(tabs, 'tab active connected-tab-top-row'); tab.style.width = '120px'; const fill = appendElement(tab, 'tab-fill'); const { label, name } = createEditorTabLabel(tab); @@ -2322,6 +2359,7 @@ suite('ModernUIContribution', () => { root.classList.add(themeType); const theme = ColorThemeData.createUnloadedTheme(themeType, { [editorBackground]: '#333333', + 'editorGroupHeader.tabsBorder': '#252526', focusBorder: '#ffaa00', contrastBorder: '#888888', }); @@ -2330,23 +2368,20 @@ suite('ModernUIContribution', () => { for (const active of [true, false]) { group.classList.toggle('active', active); const highContrast = themeType.startsWith('hc-'); - const border = highContrast ? (active ? 'rgb(255, 170, 0)' : 'rgb(136, 136, 136)') : 'rgb(51, 51, 51)'; - const frame = targetWindow.getComputedStyle(body, '::after'); - const breadcrumbFrame = targetWindow.getComputedStyle(breadcrumbs, '::after'); + const border = highContrast ? (active ? 'rgb(255, 170, 0)' : 'rgb(136, 136, 136)') : 'rgb(37, 37, 38)'; + const frame = targetWindow.getComputedStyle(group, '::after'); assert.deepStrictEqual({ geometry: geometry(), cap: [targetWindow.getComputedStyle(fill).borderTopColor, targetWindow.getComputedStyle(fill).borderLeftColor], shoulder: targetWindow.getComputedStyle(fill, '::after').borderLeftColor, separator: targetWindow.getComputedStyle(row, '::after').backgroundColor, - frame: [frame.borderLeftWidth, frame.borderBottomWidth, frame.borderTopWidth, frame.borderLeftColor, frame.pointerEvents], - breadcrumbs: [breadcrumbFrame.borderLeftWidth, breadcrumbFrame.borderBottomWidth, breadcrumbFrame.borderLeftColor], + frame: [frame.content, frame.borderLeftWidth, frame.borderBottomWidth, frame.borderTopWidth, highContrast ? frame.borderLeftColor : undefined, frame.borderRadius, frame.pointerEvents], }, { geometry: baseline, - cap: [border, border], + cap: ['rgba(0, 0, 0, 0)', border], shoulder: border, separator: border, - frame: ['1px', '1px', '0px', highContrast ? border : 'rgba(0, 0, 0, 0)', 'none'], - breadcrumbs: ['1px', '0px', highContrast ? border : 'rgba(0, 0, 0, 0)'], + frame: highContrast ? ['""', '1px', '1px', '1px', border, '8px', 'none'] : ['none', '0px', '0px', '0px', undefined, '0px', 'auto'], }, `${themeType}, active group: ${active}`); } } @@ -2410,7 +2445,7 @@ suite('ModernUIContribution', () => { labelBounds: labelBounds.toJSON(), fillExpansion: [0, 0, 0], marginRight: connected ? shoulderWidth : '0px', - topRadius: connected ? '5px' : '4px', + topRadius: connected ? '0px' : '4px', }, JSON.stringify({ classes, theme, activeGroup, compact })); } } @@ -2492,6 +2527,7 @@ suite('ModernUIContribution', () => { middleTab: { shoulders: getShoulders(middleFill), capRadii: [targetWindow.getComputedStyle(middleFill).borderTopLeftRadius, targetWindow.getComputedStyle(middleFill).borderTopRightRadius], + backgroundClip: targetWindow.getComputedStyle(middleFill).backgroundClip, radii: [leftShoulderStyle.borderBottomRightRadius, rightShoulderStyle.borderBottomLeftRadius], sizes: [ [leftShoulderStyle.width, leftShoulderStyle.height], @@ -2503,12 +2539,98 @@ suite('ModernUIContribution', () => { middleTab: { shoulders: ['""', '""'], capRadii: ['5px', '5px'], + backgroundClip: 'padding-box', radii: ['5px', '5px'], sizes: [['5px', '5px'], ['5px', '5px']], }, }); }); + test('uses direct strip geometry for both connected and upper wrapped rows', () => { + const root = document.createElement('div'); + root.className = 'monaco-workbench modern-ui modern-ui-tabs modern-ui-connected-editor-tabs'; + root.style.setProperty('--vscode-spacing-size20', '2px'); + root.style.setProperty('--vscode-cornerRadius-small', '4px'); + root.style.setProperty('--vscode-strokeThickness', '1px'); + document.body.appendChild(root); + store.add(toDisposable(() => root.remove())); + + const editor = appendElement(root, 'part editor'); + const content = appendElement(editor, 'content'); + const group = appendElement(content, 'editor-group-container active'); + const title = appendElement(group, 'title tabs'); + const row = appendElement(title, 'tabs-and-actions-container wrapping'); + const tabs = appendElement(row, 'tabs-container'); + const upperTab = appendElement(tabs, 'tab active connected-tab-upper-row connected-tab-top-row'); + const upperFill = appendElement(upperTab, 'tab-fill'); + const inactiveBottomTab = appendElement(tabs, 'tab'); + const inactiveBottomFill = appendElement(inactiveBottomTab, 'tab-fill'); + const activeBottomTab = appendElement(tabs, 'tab active'); + const activeBottomFill = appendElement(activeBottomTab, 'tab-fill'); + const targetWindow = getWindow(root); + const geometry = (tab: HTMLElement, fill: HTMLElement) => { + const tabStyle = targetWindow.getComputedStyle(tab); + const fillStyle = targetWindow.getComputedStyle(fill); + return { + tabBorders: [tabStyle.borderTopWidth, tabStyle.borderBottomWidth], + fillInsets: [fillStyle.top, fillStyle.bottom], + fillInlineStart: fillStyle.left, + }; + }; + + assert.deepStrictEqual({ + rowPaddingTop: targetWindow.getComputedStyle(row).paddingTop, + topFrame: { + borderColors: [targetWindow.getComputedStyle(upperFill).borderTopColor, targetWindow.getComputedStyle(upperFill).borderLeftColor], + borderTopLeftRadius: targetWindow.getComputedStyle(upperFill).borderTopLeftRadius, + }, + upper: geometry(upperTab, upperFill), + inactiveBottom: geometry(inactiveBottomTab, inactiveBottomFill), + activeBottom: geometry(activeBottomTab, activeBottomFill), + }, { + rowPaddingTop: '0px', + topFrame: { borderColors: ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0)'], borderTopLeftRadius: '0px' }, + upper: { tabBorders: ['2px', '2px'], fillInsets: ['-2px', '-2px'], fillInlineStart: '0px' }, + inactiveBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-1px'], fillInlineStart: '0px' }, + activeBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-2px'], fillInlineStart: '0px' }, + }); + }); + + test('shares the high contrast outer frame while retaining upper-row outlines', () => { + const root = document.createElement('div'); + root.className = 'monaco-workbench modern-ui modern-ui-tabs modern-ui-connected-editor-tabs'; + root.style.cssText = '--vscode-strokeThickness: 1px; --vscode-cornerRadius-small: 4px; --vscode-spacing-size20: 2px; --vscode-focusBorder: #ffaa00;'; + document.body.appendChild(root); + store.add(toDisposable(() => root.remove())); + + const editor = appendElement(root, 'part editor'); + const content = appendElement(editor, 'content'); + const group = appendElement(content, 'editor-group-container active'); + const title = appendElement(group, 'title tabs'); + const row = appendElement(title, 'tabs-and-actions-container'); + const tabs = appendElement(row, 'tabs-container'); + const tab = appendElement(tabs, 'tab active connected-tab-top-row'); + const fill = appendElement(tab, 'tab-fill'); + const targetWindow = getWindow(root); + const outlines = []; + for (const theme of ['hc-black', 'hc-light']) { + root.classList.add(theme); + for (const upperRow of [false, true]) { + row.classList.toggle('wrapping', upperRow); + tab.classList.toggle('connected-tab-upper-row', upperRow); + const style = targetWindow.getComputedStyle(fill); + outlines.push({ theme, upperRow, top: style.borderTopColor, left: style.borderLeftColor }); + } + root.classList.remove(theme); + } + assert.deepStrictEqual(outlines, [ + { theme: 'hc-black', upperRow: false, top: 'rgba(0, 0, 0, 0)', left: 'rgba(0, 0, 0, 0)' }, + { theme: 'hc-black', upperRow: true, top: 'rgb(255, 170, 0)', left: 'rgb(255, 170, 0)' }, + { theme: 'hc-light', upperRow: false, top: 'rgba(0, 0, 0, 0)', left: 'rgba(0, 0, 0, 0)' }, + { theme: 'hc-light', upperRow: true, top: 'rgb(255, 170, 0)', left: 'rgb(255, 170, 0)' }, + ]); + }); + test('keeps the right shoulder curved and the left edge straight at row boundaries', () => { const root = document.createElement('div'); root.className = 'monaco-workbench modern-ui modern-ui-tabs modern-ui-connected-editor-tabs'; @@ -2598,7 +2720,7 @@ suite('ModernUIContribution', () => { }); }); - test('aligns connected actions to the tab edge in every theme', () => { + test('centers connected actions within their action surface in every theme', () => { const root = document.createElement('div'); root.className = 'monaco-workbench modern-ui modern-ui-tabs modern-ui-connected-editor-tabs'; root.style.setProperty('--vscode-spacing-size20', '2px'); @@ -2647,12 +2769,12 @@ suite('ModernUIContribution', () => { assert.deepStrictEqual({ connected, highContrast }, { connected: { - right: { radius: '0px 4px 0px 0px', alignment: 'flex-end', targetWidth: '24px', actionBarWidth: '24px' }, - left: { radius: '4px 0px 0px', alignment: 'flex-start', targetWidth: '24px', actionBarWidth: '24px' }, + right: { radius: '0px 4px 0px 0px', alignment: 'center', targetWidth: '24px', actionBarWidth: '24px' }, + left: { radius: '4px 0px 0px', alignment: 'center', targetWidth: '24px', actionBarWidth: '24px' }, }, highContrast: { - right: { radius: '0px 4px 0px 0px', alignment: 'flex-end', targetWidth: '24px', actionBarWidth: '24px' }, - left: { radius: '4px 0px 0px', alignment: 'flex-start', targetWidth: '24px', actionBarWidth: '24px' }, + right: { radius: '0px 4px 0px 0px', alignment: 'center', targetWidth: '24px', actionBarWidth: '24px' }, + left: { radius: '4px 0px 0px', alignment: 'center', targetWidth: '24px', actionBarWidth: '24px' }, }, }); }); @@ -2722,6 +2844,7 @@ suite('ModernUIContribution', () => { const editorActiveForeground = colorRegistry.getColors().find(color => color.id === MODERN_EDITOR_TAB_ACTIVE_FOREGROUND); const editorActiveHoverColor = colorRegistry.getColors().find(color => color.id === MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND); const editorActiveHoverActionColor = colorRegistry.getColors().find(color => color.id === MODERN_EDITOR_TAB_ACTIVE_HOVER_ACTION_BACKGROUND); + const editorConnectedStripColor = colorRegistry.getColors().find(color => color.id === EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND); const editorInactiveColor = colorRegistry.getColors().find(color => color.id === MODERN_EDITOR_TAB_INACTIVE_BACKGROUND); const editorHoverColor = colorRegistry.getColors().find(color => color.id === MODERN_EDITOR_TAB_HOVER_BACKGROUND); const editorHoverActionColor = colorRegistry.getColors().find(color => color.id === MODERN_EDITOR_TAB_HOVER_ACTION_BACKGROUND); @@ -2735,8 +2858,8 @@ suite('ModernUIContribution', () => { activeForegroundDefault: activeForeground?.defaults, hoverDefault: hoverColor?.defaults, hoverForegroundDefault: hoverForeground?.defaults, - editorRegisteredColors: [MODERN_EDITOR_TAB_ACTIVE_ACTION_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_FOREGROUND, MODERN_EDITOR_TAB_ACTIVE_HOVER_ACTION_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND, MODERN_EDITOR_TAB_INACTIVE_BACKGROUND, MODERN_EDITOR_TAB_HOVER_ACTION_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND, MODERN_EDITOR_TAB_HOVER_FOREGROUND, MODERN_EDITOR_TAB_SELECTED_ACTION_BACKGROUND].map(id => colorRegistry.getColors().some(color => color.id === id)), - editorDefaults: [editorActiveActionColor?.defaults, editorActiveColor?.defaults, editorActiveForeground?.defaults, editorActiveHoverActionColor?.defaults, editorActiveHoverColor?.defaults, editorInactiveColor?.defaults, editorHoverActionColor?.defaults, editorHoverColor?.defaults, editorHoverForeground?.defaults, editorSelectedActionColor?.defaults], + editorRegisteredColors: [MODERN_EDITOR_TAB_ACTIVE_ACTION_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_FOREGROUND, MODERN_EDITOR_TAB_ACTIVE_HOVER_ACTION_BACKGROUND, MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND, EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND, MODERN_EDITOR_TAB_INACTIVE_BACKGROUND, MODERN_EDITOR_TAB_HOVER_ACTION_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND, MODERN_EDITOR_TAB_HOVER_FOREGROUND, MODERN_EDITOR_TAB_SELECTED_ACTION_BACKGROUND].map(id => colorRegistry.getColors().some(color => color.id === id)), + editorDefaults: [editorActiveActionColor?.defaults, editorActiveColor?.defaults, editorActiveForeground?.defaults, editorActiveHoverActionColor?.defaults, editorActiveHoverColor?.defaults, editorConnectedStripColor?.defaults, editorInactiveColor?.defaults, editorHoverActionColor?.defaults, editorHoverColor?.defaults, editorHoverForeground?.defaults, editorSelectedActionColor?.defaults], paneTabBackground: getWindow(paneAction.indicator).getComputedStyle(paneAction.indicator).backgroundColor, paneTabForeground: getWindow(paneAction.actionLabel).getComputedStyle(paneAction.actionLabel).color, editorTabBackground: getWindow(tabFill).getComputedStyle(tabFill).backgroundColor, @@ -2759,8 +2882,8 @@ suite('ModernUIContribution', () => { activeForegroundDefault: oneOf(listInactiveSelectionForeground, foreground), hoverDefault: listHoverBackground, hoverForegroundDefault: oneOf(listHoverForeground, foreground), - editorRegisteredColors: [true, true, true, true, true, true, true, true, true, true], - editorDefaults: [opaque(MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, editorBackground), MODERN_TAB_ACTIVE_BACKGROUND, MODERN_TAB_ACTIVE_FOREGROUND, opaque(MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND, editorBackground), MODERN_EDITOR_TAB_HOVER_BACKGROUND, Color.transparent, opaque(MODERN_EDITOR_TAB_HOVER_BACKGROUND, editorBackground), MODERN_TAB_HOVER_BACKGROUND, MODERN_TAB_HOVER_FOREGROUND, opaque(TAB_SELECTED_BACKGROUND, editorBackground)], + editorRegisteredColors: [true, true, true, true, true, true, true, true, true, true, true], + editorDefaults: [opaque(MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, editorBackground), MODERN_TAB_ACTIVE_BACKGROUND, MODERN_TAB_ACTIVE_FOREGROUND, opaque(MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND, editorBackground), MODERN_EDITOR_TAB_HOVER_BACKGROUND, EDITOR_GROUP_HEADER_TABS_BACKGROUND, Color.transparent, opaque(MODERN_EDITOR_TAB_HOVER_BACKGROUND, editorBackground), MODERN_TAB_HOVER_BACKGROUND, MODERN_TAB_HOVER_FOREGROUND, opaque(TAB_SELECTED_BACKGROUND, editorBackground)], paneTabBackground: 'rgb(18, 52, 86)', paneTabForeground: 'rgb(171, 205, 239)', editorTabBackground: 'rgb(35, 69, 103)', diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.css b/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.css index f466022eba202a..e09af4300f2175 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.css +++ b/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.css @@ -20,6 +20,10 @@ background-color: var(--modern-ui-editor-tab-active-hover-background); } +.monaco-workbench .monaco-action-bar:not(.vertical) .action-label.fixture-action-hover:not(.disabled) { + background-color: var(--vscode-toolbar-hoverBackground); +} + .modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.fixture-hover:not(.close-action-off) > .tab-actions { background-color: var(--modern-ui-editor-tab-action-active-hover-background); } diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.ts index ee84e4381c9800..e722aa3b628404 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/editorTabBar.fixture.ts @@ -371,6 +371,7 @@ export interface IEditorTabBarFixtureOptions { readonly headerMenuIds?: IEditorGroupMenuIds; readonly colorCustomizations?: Readonly>; readonly forcedHoverTab?: number; + readonly forcedHoverTabAction?: number; readonly focusedTabAction?: number; readonly editorContents?: string; readonly activeTabClipping?: 'left' | 'right' | 'left-shoulder' | 'right-shoulder'; @@ -591,6 +592,9 @@ export function renderEditorTabBarFixture(ctx: ComponentFixtureContext, options: if (options.forcedHoverTab !== undefined) { tabs[options.forcedHoverTab]?.classList.add('fixture-hover'); } + if (options.forcedHoverTabAction !== undefined) { + tabs[options.forcedHoverTabAction]?.querySelector('.tab-actions .action-label')?.classList.add('fixture-action-hover'); + } if (options.focusedTabAction !== undefined) { const action = tabs[options.focusedTabAction]?.querySelector('.tab-actions .action-label'); if (action) { @@ -826,6 +830,15 @@ function renderWrappedConnectedSurface(activeTabIndex: number, forcedHoverTab?: }); } +function renderWrappedConnectedCloseActionHover(activeTabIndex: number): (ctx: ComponentFixtureContext) => void { + return renderConnectedSurface(activeTabIndex, activeTabIndex, undefined, { + width: 820, + editors: manyEditorSpecs().slice(0, 10).map((spec, index) => ({ ...spec, dirty: false, active: index <= activeTabIndex })), + partOptions: { wrapTabs: true, editorActionsLocation: 'hidden' }, + forcedHoverTabAction: activeTabIndex, + }); +} + const connectedSurfaceThemes: readonly ComponentFixtureAdditionalTheme[] = ['darkModern', 'light2026', 'darkPlus', 'lightPlus', 'visualStudioDark', 'visualStudioLight', 'darkHighContrast', 'lightHighContrast', 'abyss', 'monokai', 'quietLight', 'solarizedDark', 'solarizedLight']; export default defineThemedFixtureGroup({ path: 'editor/editorTabBar/' }, { @@ -863,6 +876,34 @@ export default defineThemedFixtureGroup({ path: 'editor/editorTabBar/' }, { }), expectedVisualDescriptions: ['Hover reveals the inactive close without covering its ellipsized basename or extension.'], }), + SingleTab: defineComponentFixture({ + render: renderConnectedSurface(0, undefined, undefined, { + editors: [{ resource: file('/project/README.md'), pinned: true, active: true }], + partOptions: { editorActionsLocation: 'hidden' }, + }), + additionalThemes: connectedSurfaceThemes, + expectedVisualDescriptions: ['The single connected tab retains the same close-button spacing and visible cap width as an active tab beside another tab. Its terminal shoulder turns into the document well without crowding the action.'], + }), + SingleTabCloseActionHovered: defineComponentFixture({ + render: renderConnectedSurface(0, 0, undefined, { + editors: [{ resource: file('/project/README.md'), pinned: true, active: true }], + partOptions: { editorActionsLocation: 'hidden' }, + forcedHoverTabAction: 0, + }), + expectedVisualDescriptions: ['The close action hover background has even inset spacing on every side and remains separated from the single tab terminal shoulder.'], + }), + LastTabCloseActionHovered: defineComponentFixture({ + render: renderConnectedSurface(3, 3, undefined, { + editors: [ + { resource: file('/project/README.md'), pinned: true }, + { resource: file('/project/src/app/main.ts'), pinned: true }, + { resource: file('/project/src/app/styles.css'), pinned: true }, + { resource: file('/project/package.json'), pinned: true, active: true }, + ], + forcedHoverTabAction: 3, + }), + expectedVisualDescriptions: ['The last tab close action hover background has the same even inset spacing as the single-tab state and remains separated from the terminal shoulder.'], + }), NarrowWindow: defineComponentFixture({ render: renderConnectedSurface(0, undefined, undefined, { width: 420, @@ -882,6 +923,14 @@ export default defineThemedFixtureGroup({ path: 'editor/editorTabBar/' }, { additionalThemes: connectedSurfaceThemes, expectedVisualDescriptions: ['The selected tab in the bottom wrapped row connects directly to the document well with curved shoulders and no bottom gap, just like a single row. The adjacent inactive tab also reaches the well boundary. Upper-row tabs retain separate rounded pills.'], }), + UpperWrappedCloseActionHovered: defineComponentFixture({ + render: renderWrappedConnectedCloseActionHover(0), + expectedVisualDescriptions: ['The upper-row pill close action hover background has even inset spacing on every side and remains separated from the pill edge.'], + }), + BottomWrappedCloseActionHovered: defineComponentFixture({ + render: renderWrappedConnectedCloseActionHover(9), + expectedVisualDescriptions: ['The bottom-row connected tab close action hover background has even inset spacing on every side and remains separated from the terminal shoulder.'], + }), UpperWrappedHover: defineComponentFixture({ render: renderWrappedConnectedSurface(9, 1), additionalThemes: connectedSurfaceThemes, diff --git a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts index 2213f930631e8a..229d7e39156dd5 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -132,10 +132,10 @@ suite('MultiEditorTabsControl', () => { function connectedGroup(): HTMLElement { const root = $('.monaco-workbench.modern-ui.modern-ui-tabs.modern-ui-connected-editor-tabs'); - root.style.cssText = '--vscode-spacing-size20: 2px; --vscode-spacing-size40: 4px; --vscode-spacing-size60: 6px; --vscode-spacing-size80: 8px; --vscode-spacing-size160: 16px; --vscode-spacing-size280: 28px; --vscode-strokeThickness: 1px; --vscode-cornerRadius-small: 4px; --vscode-fontSize-body1: 13px; --vscode-fontWeight-regular: 400;'; + root.style.cssText = '--vscode-spacing-size20: 2px; --vscode-spacing-size40: 4px; --vscode-spacing-size60: 6px; --vscode-spacing-size80: 8px; --vscode-spacing-size160: 16px; --vscode-spacing-size200: 20px; --vscode-spacing-size280: 28px; --vscode-strokeThickness: 1px; --vscode-cornerRadius-small: 4px; --vscode-fontSize-body1: 13px; --vscode-fontWeight-regular: 400;'; mainWindow.document.body.appendChild(root); disposables.add(toDisposable(() => root.remove())); - const editor = $('.part.editor'); + const editor = $('.part.editor.editor-tabs-multiple'); const content = $('.content'); const group = $('.editor-group-container.active'); root.appendChild(editor); @@ -349,6 +349,143 @@ suite('MultiEditorTabsControl', () => { assert.deepStrictEqual({ clean, dirty, focused }, { clean: ['1', '0'], dirty: ['1', '1'], focused: ['1', '1'] }); }); + test('connected close actions keep consistent spacing across terminal and wrapped tabs', async () => { + const group = connectedGroup(); + group.style.setProperty('--vscode-editorGroupHeader-tabsBorder', '#333333'); + const measure = () => { + const tab = container.querySelector('.tab.active')!; + const fill = tab.querySelector('.tab-fill')!; + const action = tab.querySelector('.action-label')!; + const actions = tab.querySelector('.tab-actions')!; + const label = tab.querySelector('.monaco-icon-label-container')!; + const fillBounds = fill.getBoundingClientRect(); + const actionBounds = action.getBoundingClientRect(); + const actionsBounds = actions.getBoundingClientRect(); + const actionStyle = mainWindow.getComputedStyle(action); + const actionsStyle = mainWindow.getComputedStyle(actions); + const fillStyle = mainWindow.getComputedStyle(fill); + return { + top: actionBounds.top - fillBounds.top - (fillStyle.borderTopColor === 'rgba(0, 0, 0, 0)' ? 0 : Number.parseFloat(fillStyle.borderTopWidth)), + bottom: Math.min(fillBounds.bottom, tab.getBoundingClientRect().bottom) - actionBounds.bottom - (!tab.classList.contains('connected-tab-upper-row') && !tab.classList.contains('connected-tab-top-row') ? Number.parseFloat(fillStyle.borderBottomWidth) : 0), + right: fillBounds.right - actionBounds.right - (fillStyle.borderRightColor === 'rgba(0, 0, 0, 0)' ? 0 : Number.parseFloat(fillStyle.borderRightWidth)), + left: actionBounds.left - label.getBoundingClientRect().right, + width: fillBounds.width, + actionInsets: [ + actionBounds.left - actionsBounds.left - Number.parseFloat(actionsStyle.borderLeftWidth), + actionsBounds.right - Number.parseFloat(actionsStyle.borderRightWidth) - actionBounds.right, + ], + padding: [actionStyle.paddingTop, actionStyle.paddingRight, actionStyle.paddingBottom, actionStyle.paddingLeft], + }; + }; + + await layoutConnectedGroup(group, 400); + const multiple = measure(); + const stroke = Number.parseFloat(mainWindow.getComputedStyle(container.querySelector('.tab.active')!).getPropertyValue('--vscode-strokeThickness')); + + const secondEditor = model.getEditorByIndex(1)!; + model.closeEditor(secondEditor); + control.closeEditor(secondEditor); + await layoutConnectedGroup(group, 400); + const single = measure(); + + model.openEditor(secondEditor, { pinned: true, active: true }); + control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); + const oldOptions = partOptions; + partOptions = { ...partOptions, wrapTabs: true, tabSizing: 'fixed', tabSizingFixedMinWidth: 120, tabSizingFixedMaxWidth: 120, editorActionsLocation: 'hidden' }; + control.updateOptions(oldOptions, partOptions); + await layoutConnectedGroup(group, 150); + const wrappedBottom = measure(); + + model.openEditor(model.getEditorByIndex(0)!, { active: true }); + control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); + await layoutConnectedGroup(group, 150); + const wrappedUpper = measure(); + const measurements = [multiple, single, wrappedBottom, wrappedUpper]; + + const oldWrappedOptions = partOptions; + partOptions = { ...partOptions, wrapTabs: false, tabSizing: 'fit', tabActionLocation: 'left' }; + control.updateOptions(oldWrappedOptions, partOptions); + await layoutConnectedGroup(group, 400); + const leftMultiple = measure(); + + model.closeEditor(secondEditor); + control.closeEditor(secondEditor); + await layoutConnectedGroup(group, 400); + const leftSingle = measure(); + + assert.deepStrictEqual({ + single: { + top: single.top === multiple.top, + right: single.right === multiple.right, + left: single.left === multiple.left, + width: single.width === multiple.width, + }, + horizontal: { + clearance: measurements.map(measurement => [measurement.top, measurement.right, measurement.bottom, measurement.left]), + }, + leftAction: { + top: leftSingle.top === leftMultiple.top, + right: leftSingle.right === leftMultiple.right, + left: leftSingle.left === leftMultiple.left, + width: leftSingle.width === leftMultiple.width, + }, + balancedActionSurface: [...measurements, leftMultiple, leftSingle].every(measurement => Math.abs(measurement.actionInsets[0] - measurement.actionInsets[1]) <= stroke), + balancedActionInsets: measurements.every(measurement => Math.abs(measurement.right - measurement.left) <= stroke), + actionPadding: measurements.every(measurement => new Set(measurement.padding).size === 1 && measurement.padding[0] === multiple.padding[0]), + }, { + single: { top: true, right: true, left: true, width: true }, + horizontal: { clearance: [[6, 6, 6, 6], [6, 6, 6, 6], [3, 3, 3, 4], [4, 4, 4, 4]] }, + leftAction: { top: true, right: true, left: true, width: true }, + balancedActionSurface: true, + balancedActionInsets: true, + actionPadding: true, + }); + }); + + test('close hover targets have equal vertical and trailing clearance at both tab densities', async () => { + const group = connectedGroup(); + group.style.setProperty('--vscode-editorGroupHeader-tabsBorder', '#333333'); + const measurements = []; + const expected = []; + for (const tabHeight of ['default', 'compact'] as const) { + for (const tabActionLocation of ['right', 'left'] as const) { + for (const wrapTabs of [false, true]) { + const oldOptions = partOptions; + partOptions = { ...partOptions, tabHeight, tabActionLocation, wrapTabs, tabSizing: 'fixed', tabSizingFixedMinWidth: 120, tabSizingFixedMaxWidth: 120, editorActionsLocation: 'hidden' }; + control.updateOptions(oldOptions, partOptions); + for (const activeIndex of [0, 1]) { + model.openEditor(model.getEditorByIndex(activeIndex)!, { active: true }); + control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); + await layoutConnectedGroup(group, wrapTabs ? 150 : 400); + const tab = container.querySelector('.tab.active')!; + const fillElement = tab.querySelector('.tab-fill')!; + const fill = fillElement.getBoundingClientRect(); + const fillStyle = mainWindow.getComputedStyle(fillElement); + const action = tab.querySelector('.action-label')!.getBoundingClientRect(); + const rowStart = activeIndex === 0 || wrapTabs; + const upperRow = wrapTabs && activeIndex === 0; + measurements.push({ + tabHeight, tabActionLocation, wrapTabs, activeIndex, + top: action.top - fill.top - (fillStyle.borderTopColor === 'rgba(0, 0, 0, 0)' ? 0 : Number.parseFloat(fillStyle.borderTopWidth)), + bottom: Math.min(fill.bottom, tab.getBoundingClientRect().bottom) - action.bottom - (wrapTabs && !upperRow ? Number.parseFloat(fillStyle.borderBottomWidth) : 0), + trailing: tabActionLocation === 'left' + ? action.left - fill.left - (fillStyle.borderLeftColor === 'rgba(0, 0, 0, 0)' ? 0 : Number.parseFloat(fillStyle.borderLeftWidth)) + : fill.right - action.right - (fillStyle.borderRightColor === 'rgba(0, 0, 0, 0)' ? 0 : Number.parseFloat(fillStyle.borderRightWidth)), + leftBorder: rowStart && !upperRow ? mainWindow.getComputedStyle(tab.querySelector('.tab-fill')!).borderLeftColor : undefined, + }); + const clearance = (tabHeight === 'compact' ? 4 : 6) - (wrapTabs ? 2 : 0) - (wrapTabs && activeIndex === 1 ? 1 : 0); + expected.push({ + tabHeight, tabActionLocation, wrapTabs, activeIndex, + top: clearance, bottom: clearance, trailing: clearance, + leftBorder: rowStart && !upperRow ? 'rgba(0, 0, 0, 0)' : undefined, + }); + } + } + } + } + assert.deepStrictEqual(measurements, expected); + }); + test('reveals the active tab with its right shoulder outside the label and action', async () => { const group = connectedGroup(); const oldOptions = partOptions; @@ -411,9 +548,11 @@ suite('MultiEditorTabsControl', () => { } await reveal(0, 240); const firstFill = container.querySelector('.tab.active > .tab-fill')!; + const firstFillStyle = mainWindow.getComputedStyle(firstFill); assert.deepStrictEqual({ results, - firstTabFlush: firstFill.getBoundingClientRect().left === container.querySelector('.monaco-scrollable-element')!.getBoundingClientRect().left, + firstBorderInset: firstFillStyle.left, + firstBorderColor: firstFillStyle.borderLeftColor, firstShoulder: mainWindow.getComputedStyle(firstFill, '::before').content, }, { results: [ @@ -421,7 +560,8 @@ suite('MultiEditorTabsControl', () => { { width: 172, leftShoulderVisible: true, rightShoulderVisible: true }, { width: 120, leftShoulderVisible: true, rightShoulderVisible: false }, ], - firstTabFlush: true, + firstBorderInset: '0px', + firstBorderColor: 'rgba(0, 0, 0, 0)', firstShoulder: 'none', }); }); @@ -466,7 +606,8 @@ suite('MultiEditorTabsControl', () => { hidden: activeTab.classList.contains('connected-tab-hidden'), fillDisplay: fillStyle.display, outlineDisplay: mainWindow.getComputedStyle(edge).display, - outlineColor: fillStyle.borderTopColor, + outlineColor: fillStyle.borderRightColor, + frameColor: mainWindow.getComputedStyle(group, '::after').borderTopColor, }); } assert.deepStrictEqual(results, [4, 3, 2].map(count => ({ @@ -478,6 +619,7 @@ suite('MultiEditorTabsControl', () => { fillDisplay: 'block', outlineDisplay: 'block', outlineColor: 'rgb(255, 170, 0)', + frameColor: 'rgb(255, 170, 0)', }))); }); @@ -511,11 +653,16 @@ suite('MultiEditorTabsControl', () => { await layoutConnectedGroup(group, 150); const tabs = Array.from(container.querySelectorAll('.tab')); const wrapped = tabs.map(tab => tab.classList.contains('connected-tab-upper-row')); + const wrappedTop = tabs.map(tab => tab.classList.contains('connected-tab-top-row')); const fill = tabs[0].querySelector('.tab-fill')!; const upper = { inset: mainWindow.getComputedStyle(fill).top, shoulder: mainWindow.getComputedStyle(fill, '::after').content }; await layoutConnectedGroup(group, 400); const unwrapped = tabs.map(tab => tab.classList.contains('connected-tab-upper-row')); - assert.deepStrictEqual({ wrapped, upper, unwrapped }, { wrapped: [true, false], upper: { inset: '-2px', shoulder: 'none' }, unwrapped: [false, false] }); + const unwrappedTop = tabs.map(tab => tab.classList.contains('connected-tab-top-row')); + assert.deepStrictEqual( + { wrapped, wrappedTop, upper, unwrapped, unwrappedTop }, + { wrapped: [true, false], wrappedTop: [true, false], upper: { inset: '-2px', shoulder: 'none' }, unwrapped: [false, false], unwrappedTop: [true, true] } + ); }); test('connected wrapped last tab adds its shoulder to the editor actions margin', async () => { @@ -542,9 +689,196 @@ suite('MultiEditorTabsControl', () => { }); }); + test('connected tab positions and spacing stay fixed when changing selection', async () => { + const group = connectedGroup(); + for (let index = 2; index < 6; index++) { + const editor = disposables.add(new TestFileEditorInput(URI.file(`/path/file${index}.txt`), 'testEditorInput')); + model.openEditor(editor, { pinned: true, active: false }); + } + control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); + const tabs = Array.from(container.querySelectorAll('.tabs-container > .tab')); + const measure = () => tabs.map(tab => ({ + left: tab.offsetLeft, + top: tab.offsetTop, + width: tab.getBoundingClientRect().width, + margin: mainWindow.getComputedStyle(tab).marginRight, + })); + const mismatches = []; + for (const { tabSizing, highContrast } of (['fit', 'fixed'] as const).flatMap(tabSizing => [false, true].map(highContrast => ({ tabSizing, highContrast })))) { + group.closest('.monaco-workbench')!.classList.toggle('hc-black', highContrast); + for (const wrapTabs of [false, true]) { + const oldOptions = partOptions; + partOptions = { ...partOptions, tabSizing, wrapTabs, tabSizingFixedMinWidth: 120, tabSizingFixedMaxWidth: 120, editorActionsLocation: 'hidden' }; + control.updateOptions(oldOptions, partOptions); + for (const width of wrapTabs ? [245, 365] : [1000]) { + await layoutConnectedGroup(group, width); + const baseline = measure(); + for (let activeIndex = 0; activeIndex < tabs.length; activeIndex++) { + model.openEditor(model.getEditorByIndex(activeIndex)!, { active: true }); + control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); + await layoutConnectedGroup(group, width); + const actual = measure(); + if (JSON.stringify(actual) !== JSON.stringify(baseline)) { + mismatches.push({ tabSizing, highContrast, wrapTabs, width, activeIndex, baseline, actual }); + } + for (let index = 1; index < tabs.length; index++) { + const previous = tabs[index - 1].getBoundingClientRect(); + const current = tabs[index].getBoundingClientRect(); + if (previous.top === current.top && Math.abs(current.left - previous.right) > 0.01) { + mismatches.push({ tabSizing, highContrast, wrapTabs, width, activeIndex, gapAfter: index - 1, gap: current.left - previous.right }); + } + } + } + } + } + } + assert.deepStrictEqual(mismatches, []); + }); + + test('connected close action bounds stay fixed across selection and focus changes', async () => { + const group = connectedGroup(); + for (let index = 2; index < 6; index++) { + const editor = disposables.add(new TestFileEditorInput(URI.file(`/path/file${index}.txt`), 'testEditorInput')); + model.openEditor(editor, { pinned: true, active: false }); + } + control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); + const actions = Array.from(container.querySelectorAll('.tabs-container > .tab .action-label')); + const bounds = (action: HTMLElement) => { + const { x, y, width, height } = action.getBoundingClientRect(); + return { x, y, width, height }; + }; + const measure = () => actions.map(action => { + const resting = bounds(action); + action.focus(); + const focused = bounds(action); + action.blur(); + return { resting, focused }; + }); + const mismatches = []; + const root = group.closest('.monaco-workbench')!; + root.style.setProperty('--vscode-contrastActiveBorder', '#f38518'); + root.style.setProperty('--vscode-focusBorder', '#f38518'); + for (const theme of ['vs', 'vs-dark', 'hc-black', 'hc-light']) { + root.classList.add(theme); + for (const tabHeight of ['default', 'compact'] as const) { + for (const tabActionLocation of ['right', 'left'] as const) { + for (const wrapTabs of [false, true]) { + const oldOptions = partOptions; + partOptions = { ...partOptions, tabHeight, tabActionLocation, wrapTabs, tabSizing: 'fixed', tabSizingFixedMinWidth: 120, tabSizingFixedMaxWidth: 120, editorActionsLocation: 'hidden' }; + control.updateOptions(oldOptions, partOptions); + await layoutConnectedGroup(group, wrapTabs ? 245 : 1000); + const baseline = measure().map(({ resting }) => ({ resting, focused: resting })); + for (let activeIndex = 0; activeIndex < actions.length; activeIndex++) { + model.openEditor(model.getEditorByIndex(activeIndex)!, { active: true }); + control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); + await layoutConnectedGroup(group, wrapTabs ? 245 : 1000); + const actual = measure(); + if (JSON.stringify(actual) !== JSON.stringify(baseline)) { + mismatches.push({ theme, tabHeight, tabActionLocation, wrapTabs, activeIndex, baseline, actual }); + } + } + } + } + } + root.classList.remove(theme); + } + assert.deepStrictEqual({ actionCount: actions.length, mismatches }, { actionCount: model.count, mismatches: [] }); + }); + + test('connected fills meet without gutters and round corners away from the frame', async () => { + const group = connectedGroup(); + for (let index = 2; index < 6; index++) { + const editor = disposables.add(new TestFileEditorInput(URI.file(`/path/file${index}.txt`), 'testEditorInput')); + model.openEditor(editor, { pinned: true, active: false }); + } + control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); + const tabs = Array.from(container.querySelectorAll('.tabs-container > .tab')); + const actual = []; + const expected = []; + for (const compact of [false, true]) { + group.closest('.monaco-workbench')!.classList.toggle('modern-ui-compact', compact); + for (const wrapTabs of [false, true]) { + const oldOptions = partOptions; + partOptions = { ...partOptions, wrapTabs, tabSizing: 'fixed', tabSizingFixedMinWidth: 120, tabSizingFixedMaxWidth: 120, editorActionsLocation: 'hidden' }; + control.updateOptions(oldOptions, partOptions); + for (const activeIndex of [0, 1, 4, 5]) { + model.openEditor(model.getEditorByIndex(activeIndex)!, { active: true }); + control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); + await layoutConnectedGroup(group, wrapTabs ? 245 : 1000); + for (const [index, tab] of tabs.entries()) { + const fill = tab.querySelector('.tab-fill')!; + const bounds = tab.getBoundingClientRect(); + const surface = fill.getBoundingClientRect(); + const style = mainWindow.getComputedStyle(fill); + const rowStart = index === 0 || tabs[index - 1].offsetTop !== tab.offsetTop; + const upper = tab.classList.contains('connected-tab-upper-row'); + const active = index === activeIndex; + const radius = active && !upper ? '5px' : '4px'; + const context = { compact, wrapTabs, activeIndex, index }; + actual.push({ + ...context, + insets: [surface.left - bounds.left, bounds.right - surface.right, surface.top - bounds.top], + corners: [style.borderTopLeftRadius, style.borderTopRightRadius, style.borderBottomRightRadius, style.borderBottomLeftRadius], + leftBorder: rowStart ? style.borderLeftColor : undefined, + }); + expected.push({ + ...context, + insets: [0, 0, 0], + corners: [rowStart ? '0px' : radius, radius, active && !upper ? '0px' : '4px', rowStart || active && !upper ? '0px' : '4px'], + leftBorder: rowStart ? 'rgba(0, 0, 0, 0)' : undefined, + }); + } + } + } + } + assert.deepStrictEqual(actual, expected); + }); + + test('fit-sized connected row markers stay consistent at wrapping boundaries', async () => { + const group = connectedGroup(); + const oldOptions = partOptions; + partOptions = { ...partOptions, wrapTabs: true, tabSizing: 'fit', editorActionsLocation: 'hidden' }; + control.updateOptions(oldOptions, partOptions); + for (let index = 2; index < 4; index++) { + const editor = disposables.add(new TestFileEditorInput(URI.file(`/path/file${index}.txt`), 'testEditorInput')); + model.openEditor(editor, { pinned: true, active: false }); + } + control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); + await layoutConnectedGroup(group, 300); + const tabs = Array.from(container.querySelectorAll('.tabs-container > .tab')); + const boundary = tabs[0].offsetWidth + tabs[1].offsetWidth; + const widths = Array.from({ length: 21 }, (_, index) => boundary - 10 + index); + const mismatches = []; + for (const width of [...widths, ...widths.reverse()]) { + for (const activeIndex of [0, 1, 3]) { + model.openEditor(model.getEditorByIndex(activeIndex)!, { active: true }); + control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); + await layoutConnectedGroup(group, width); + const wrapping = container.querySelector('.tabs-and-actions-container')!.classList.contains('wrapping'); + for (const [index, tab] of tabs.entries()) { + const expected = { + top: tab.offsetTop === tabs[0].offsetTop, + upper: tab.offsetTop !== tabs.at(-1)!.offsetTop, + last: wrapping && (index === tabs.length - 1 || tab.offsetTop !== tabs[index + 1].offsetTop), + }; + const actual = { + top: tab.classList.contains('connected-tab-top-row'), + upper: tab.classList.contains('connected-tab-upper-row'), + last: tab.classList.contains('last-in-row'), + }; + if (actual.top !== expected.top || actual.upper !== expected.upper || actual.last !== expected.last) { + mismatches.push({ width, activeIndex, index, expected, actual }); + } + } + } + } + assert.deepStrictEqual(mismatches, []); + }); + test('selected wrapped tabs and focused actions use the document surface on every row', async () => { const group = connectedGroup(); const root = group.closest('.monaco-workbench')!; + group.style.setProperty('--vscode-focusBorder', '#ffaa00'); group.style.setProperty('--modern-ui-connected-tab-surface', '#123456'); group.style.setProperty('--vscode-editorGroupHeader-tabsBackground', '#654321'); group.style.setProperty('--modern-ui-editor-tab-active-background', '#654321'); @@ -568,14 +902,18 @@ suite('MultiEditorTabsControl', () => { for (const activeGroup of [true, false]) { group.classList.toggle('active', activeGroup); action.focus(); + const focusStyle = mainWindow.getComputedStyle(action); + const windowFocused = mainWindow.document.hasFocus(); measurements.push({ activeIndex, theme, activeGroup, actionFocused: mainWindow.document.activeElement === action, + cssFocused: action.matches(':focus'), upperRow: tab.classList.contains('connected-tab-upper-row'), fill: mainWindow.getComputedStyle(fill).backgroundColor, actions: mainWindow.getComputedStyle(actions).backgroundColor, + focusOutline: theme.startsWith('hc-') && windowFocused ? [focusStyle.outlineWidth, focusStyle.outlineStyle, focusStyle.outlineColor] : undefined, }); - expected.push({ activeIndex, theme, activeGroup, actionFocused: true, upperRow: activeIndex === 0, fill: 'rgb(18, 52, 86)', actions: 'rgb(18, 52, 86)' }); + expected.push({ activeIndex, theme, activeGroup, actionFocused: true, cssFocused: windowFocused, upperRow: activeIndex === 0, fill: 'rgb(18, 52, 86)', actions: 'rgba(0, 0, 0, 0)', focusOutline: theme.startsWith('hc-') && windowFocused ? ['1px', 'solid', 'rgb(255, 170, 0)'] : undefined }); } root.classList.remove(theme); } @@ -616,8 +954,8 @@ suite('MultiEditorTabsControl', () => { }); } assert.deepStrictEqual(measurements, [ - { tabHeight: 'default', stripHeight: 60, wrapping: true, upperRow: false, gap: 0, clippingGap: 0, bottomRadius: '0px', shoulder: '""', visibleHeights: [28, 28], rowGap: 2 }, - { tabHeight: 'compact', stripHeight: 52, wrapping: true, upperRow: false, gap: 0, clippingGap: 0, bottomRadius: '0px', shoulder: '""', visibleHeights: [24, 24], rowGap: 2 }, + { tabHeight: 'default', stripHeight: 58, wrapping: true, upperRow: false, gap: -1, clippingGap: 0, bottomRadius: '0px', shoulder: '""', visibleHeights: [28, 28], rowGap: 2 }, + { tabHeight: 'compact', stripHeight: 50, wrapping: true, upperRow: false, gap: -1, clippingGap: 0, bottomRadius: '0px', shoulder: '""', visibleHeights: [24, 24], rowGap: 2 }, ]); }); @@ -644,8 +982,8 @@ suite('MultiEditorTabsControl', () => { }); } assert.deepStrictEqual(measurements, [ - { tabHeight: 'default', stripHeight: 90, visibleHeights: [28, 28, 28], rowGaps: [2, 2] }, - { tabHeight: 'compact', stripHeight: 78, visibleHeights: [24, 24, 24], rowGaps: [2, 2] }, + { tabHeight: 'default', stripHeight: 88, visibleHeights: [28, 28, 28], rowGaps: [2, 2] }, + { tabHeight: 'compact', stripHeight: 76, visibleHeights: [24, 24, 24], rowGaps: [2, 2] }, ]); }); @@ -679,7 +1017,7 @@ suite('MultiEditorTabsControl', () => { test('connected tabs fill row edges without inter-tab gutters', () => { const root = $('.monaco-workbench.modern-ui.modern-ui-tabs.modern-ui-connected-editor-tabs'); - root.style.cssText = '--vscode-spacing-size40: 4px; --vscode-strokeThickness: 1px;'; + root.style.cssText = '--vscode-spacing-size40: 4px; --vscode-spacing-size80: 8px; --vscode-strokeThickness: 1px;'; mainWindow.document.body.appendChild(root); disposables.add(toDisposable(() => root.remove())); const editor = $('.part.editor'); @@ -693,16 +1031,26 @@ suite('MultiEditorTabsControl', () => { const [activeTab, inactiveTab] = container.querySelectorAll('.tabs-container > .tab'); const activeFillStyle = mainWindow.getComputedStyle(activeTab.querySelector('.tab-fill')!); const inactiveFillStyle = mainWindow.getComputedStyle(inactiveTab.querySelector('.tab-fill')!); - const rowStyle = mainWindow.getComputedStyle(container.querySelector('.tabs-and-actions-container')!); + const row = container.querySelector('.tabs-and-actions-container')!; + const rowStyle = mainWindow.getComputedStyle(row); + const editorActions = row.querySelector('.editor-actions')!; + editorActions.classList.remove('hidden'); + const editorActionsStyle = mainWindow.getComputedStyle(editorActions); assert.deepStrictEqual({ active: { top: activeFillStyle.top, left: activeFillStyle.left, right: activeFillStyle.right, bottom: activeFillStyle.bottom }, inactive: { top: inactiveFillStyle.top, left: inactiveFillStyle.left, right: inactiveFillStyle.right, bottom: inactiveFillStyle.bottom }, + alignItems: rowStyle.alignItems, + editorActionsHeight: editorActionsStyle.height, rowPaddingLeft: rowStyle.paddingLeft, + rowPaddingTop: rowStyle.paddingTop, }, { - active: { top: '-4px', left: '0px', right: '0px', bottom: '-6px' }, - inactive: { top: '-4px', left: '0px', right: '0px', bottom: '-5px' }, + active: { top: '0px', left: '0px', right: '0px', bottom: '-2px' }, + inactive: { top: '0px', left: '0px', right: '0px', bottom: '-1px' }, + alignItems: 'flex-start', + editorActionsHeight: '32px', rowPaddingLeft: '0px', + rowPaddingTop: '0px', }); }); @@ -908,7 +1256,7 @@ suite('MultiEditorTabsControl', () => { reset: overflowEdge.style.left, }, { clippedLeft: { edge: true, clipped: true, fillOffset: '', edgeOffset: ['0px', '0px'], inset: 0, stationaryParent: true, edgeOverlay: ['none', 'block', '8', '5px', '0px', 'border-box', '1px', '1px', 'rgb(51, 51, 51)'] }, - multiSelected: { clipping: '0px', edge: 'block', radius: '5px 5px 0px 0px', connectedClass: true }, + multiSelected: { clipping: '0px', edge: 'block', radius: '0px 5px 0px 0px', connectedClass: true }, singleSelected: { clipping: '0px', connectedClass: true }, terminalOutline: { right: '1px', rightShoulder: '""', rightMask: '""' }, normalOutline: { left: '1px', right: '1px', leftShoulder: '""', rightShoulder: '""', edge: 'block', overflowEdge: 'none', leftMaskHeight: '3px', leftMaskTop: '0px', rightMaskHeight: '3px', rightMaskTop: '0px' }, diff --git a/test/componentFixtures/playwright/tests/editorTabBar.spec.ts b/test/componentFixtures/playwright/tests/editorTabBar.spec.ts new file mode 100644 index 00000000000000..b3684300df8fd5 --- /dev/null +++ b/test/componentFixtures/playwright/tests/editorTabBar.spec.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { expect, test } from '@playwright/test'; +import { openFixture } from './utils.js'; + +for (const theme of ['DarkHighContrast', 'LightHighContrast']) { + test(`connected tab actions respect disabled hover state in ${theme}`, async ({ page }) => { + await openFixture(page, `editor/editorTabBar/editorTabBar/ConnectedSurface/SingleTab/${theme}`, '.tabs-container > .tab'); + const action = page.locator('.tab-actions .action-label'); + + await action.hover(); + await expect(action).toHaveCSS('outline-style', 'dashed'); + + await page.keyboard.down('Alt'); + try { + await expect(action).toHaveAttribute('aria-disabled', 'true'); + await expect(action).toHaveCSS('outline-style', 'none'); + } finally { + await page.keyboard.up('Alt'); + } + + await expect(action).not.toHaveClass(/\bdisabled\b/); + await action.hover(); + await expect(action).toHaveCSS('outline-style', 'dashed'); + await action.focus(); + await expect(action).toHaveCSS('outline-style', 'solid'); + await expect(action).toHaveCSS('outline-width', '1px'); + }); +}