From 77265b8bfe0097e83997a5ad800f5397b213ad3b Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 12:23:49 -0700 Subject: [PATCH 01/29] Editor: stabilize connected tabs across zoom levels Let bottom-row connected tabs own the strip geometry directly instead of cancelling inherited pill gutters with negative offsets. This keeps their cap and document boundary aligned when Chromium rounds fractional zoom values while preserving upper-row pill spacing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/media/connectedEditorTabs.css | 34 +++++---------- .../browser/modernUI.contribution.test.ts | 42 +++++++++++++++++++ 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 923634e83abcc..704fdf178b156 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -82,8 +82,6 @@ --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)); } @@ -96,10 +94,15 @@ 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,11 +127,6 @@ 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; @@ -139,16 +137,6 @@ 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,9 +150,9 @@ --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); border: var(--vscode-strokeThickness) solid var(--modern-ui-connected-tab-border); @@ -206,9 +194,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; } 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 e045309322c36..319345d661b55 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 @@ -2430,6 +2430,48 @@ suite('ModernUIContribution', () => { }); }); + test('uses direct strip geometry for connected rows while retaining upper-row pill gutters', () => { + 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'); + 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], + }; + }; + + assert.deepStrictEqual({ + upper: geometry(upperTab, upperFill), + inactiveBottom: geometry(inactiveBottomTab, inactiveBottomFill), + activeBottom: geometry(activeBottomTab, activeBottomFill), + }, { + upper: { tabBorders: ['2px', '2px'], fillInsets: ['-2px', '-2px'] }, + inactiveBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-1px'] }, + activeBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-2px'] }, + }); + }); + 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'; From e01ff4fe65970c31e9d55c9d448e4080baf0ee49 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 12:26:32 -0700 Subject: [PATCH 02/29] test: update connected tab strip geometry Update the editor-tab layout expectations for direct bottom-row geometry and the intentional one-stroke overlap with the document surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/parts/editor/multiEditorTabsControl.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 f1a1b16a5b854..bc89406b69f13 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -616,8 +616,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: 60, wrapping: true, upperRow: false, gap: -1, clippingGap: 0, bottomRadius: '0px', shoulder: '""', visibleHeights: [28, 28], rowGap: 2 }, + { tabHeight: 'compact', stripHeight: 52, wrapping: true, upperRow: false, gap: -1, clippingGap: 0, bottomRadius: '0px', shoulder: '""', visibleHeights: [24, 24], rowGap: 2 }, ]); }); @@ -700,8 +700,8 @@ suite('MultiEditorTabsControl', () => { inactive: { top: inactiveFillStyle.top, left: inactiveFillStyle.left, right: inactiveFillStyle.right, bottom: inactiveFillStyle.bottom }, rowPaddingLeft: rowStyle.paddingLeft, }, { - 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' }, rowPaddingLeft: '0px', }); }); From 6f8f682d284306575c8e78cb2787b330870e0c18 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 12:52:57 -0700 Subject: [PATCH 03/29] Editor: align connected tab actions to the top edge Top-align the non-wrapping connected strip and give editor actions the same full row height so their controls remain vertically centered with tab labels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../modernUI/browser/media/connectedEditorTabs.css | 9 +++++++++ .../parts/editor/multiEditorTabsControl.test.ts | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 704fdf178b156..476174c99204c 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -90,6 +90,15 @@ padding-left: 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; + 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) > .editor-actions { + height: calc(var(--editor-group-tab-height) + var(--vscode-spacing-size80)); +} + .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container { padding-bottom: var(--vscode-strokeThickness); } 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 bc89406b69f13..437f02d73c610 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -679,7 +679,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 +693,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: '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', }); }); From 88c7618fc6e4f9c7a56248f77e36725aff087aa8 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 13:16:29 -0700 Subject: [PATCH 04/29] test: update component screenshot baseline Accept the Ubuntu CI hash for the dark responsive model resize fixture after verifying the delta is limited to corner antialiasing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/componentFixtures/blocks-ci-screenshots.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index dbadee7838719..f52088160a4d4 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -340,7 +340,7 @@ ![screenshot](https://hediet-screenshots.azurewebsites.net/images/fe4b95bf8348637bba9f8c0dda791924e6c67fd7b5d173398f9b2c0bfc9f7071) #### sessions/chat/input/chatInput/ResponsiveModelResizeCycleMinimal/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/0fadae9fab68892552ea7008bd229daa6b1a8ac5bee117a524bff8ce912766af) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/1c6a9e308738d4f7980af231aaf2e89df2e42aa57b3ed2faa952ac5e1b6e742e) #### sessions/chat/input/chatInput/ResponsiveModelResizeCycleMinimal/Light ![screenshot](https://hediet-screenshots.azurewebsites.net/images/db4c0f5af66921e0f3ec672e488fddc46029fd9377983401dfd411bb1f0795c7) From ca041dca6d6670b499ebc234ab39b659a19a85b1 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 13:32:17 -0700 Subject: [PATCH 05/29] Editor: share modern tab row geometry Own the non-wrapping row-height formula in the shared Modern UI tab layout so connected editor actions cannot drift from tab density changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build/lib/stylelint/vscode-known-variables.json | 1 + .../contrib/modernUI/browser/media/connectedEditorTabs.css | 2 +- src/vs/workbench/contrib/modernUI/browser/media/tabs.css | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 5711cd0391d4b..91eeafd3ad10a 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1115,6 +1115,7 @@ "--modern-ui-editor-tab-inactive-background", "--modern-ui-editor-tab-inactive-foreground", "--modern-ui-editor-tab-last-pinned-border", + "--modern-ui-editor-tab-row-height", "--modern-ui-editor-tab-unfocused-active-background", "--modern-ui-editor-tab-unfocused-active-border", "--modern-ui-editor-tab-unfocused-active-border-top", diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 476174c99204c..b027d35112ed4 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -96,7 +96,7 @@ } .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 { - height: calc(var(--editor-group-tab-height) + var(--vscode-spacing-size80)); + height: var(--modern-ui-editor-tab-row-height); } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container { diff --git a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css index 1046eee3480cb..c0705d16f5356 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css @@ -58,6 +58,7 @@ background-color: transparent !important; cursor: default; --editor-group-tab-height: 24px !important; + --modern-ui-editor-tab-row-height: calc(var(--editor-group-tab-height) + var(--vscode-spacing-size80)); } /* Compact tab height: 20px tab + 4px top + 4px bottom padding = 28px total. @@ -240,7 +241,7 @@ .modern-ui-tabs.monaco-workbench .part.editor .tabs-and-actions-container:not(.wrapping) .tabs-container, .modern-ui-tabs.monaco-workbench .part.editor .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab { - min-height: calc(var(--editor-group-tab-height) + var(--vscode-spacing-size40) * 2); + min-height: var(--modern-ui-editor-tab-row-height); } .modern-ui-tabs.monaco-workbench .part.editor .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab { From dcb5e3d7e8aa71a902ccdc3f733dcab11f390c2f Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 13:52:52 -0700 Subject: [PATCH 06/29] test: restore stable component screenshot baseline Restore the original dark responsive model resize hash after CI oscillated back, confirming the alternate corner-antialias hash was nondeterministic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/componentFixtures/blocks-ci-screenshots.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index f52088160a4d4..dbadee7838719 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -340,7 +340,7 @@ ![screenshot](https://hediet-screenshots.azurewebsites.net/images/fe4b95bf8348637bba9f8c0dda791924e6c67fd7b5d173398f9b2c0bfc9f7071) #### sessions/chat/input/chatInput/ResponsiveModelResizeCycleMinimal/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/1c6a9e308738d4f7980af231aaf2e89df2e42aa57b3ed2faa952ac5e1b6e742e) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/0fadae9fab68892552ea7008bd229daa6b1a8ac5bee117a524bff8ce912766af) #### sessions/chat/input/chatInput/ResponsiveModelResizeCycleMinimal/Light ![screenshot](https://hediet-screenshots.azurewebsites.net/images/db4c0f5af66921e0f3ec672e488fddc46029fd9377983401dfd411bb1f0795c7) From 3495d12ada6917c0cb362f4fb4dbd7371533ef24 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 14:33:03 -0700 Subject: [PATCH 07/29] Editor: isolate connected tab colors and borders Give connected document tabs their own strip color, restore shared 2026 tab colors for Classic and Agents surfaces, and use the existing strip separator color for connected cap and shoulder outlines. Refs #336320 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../lib/stylelint/vscode-known-variables.json | 1 + .../theme-defaults/themes/2026-dark.json | 5 ++-- .../theme-defaults/themes/2026-light.json | 5 ++-- src/vs/workbench/common/theme.ts | 2 ++ .../modernUI/browser/connectedEditorTabs.ts | 4 +-- .../browser/media/connectedEditorTabs.css | 2 +- .../browser/modernUI.contribution.test.ts | 27 ++++++++++++------- 7 files changed, 29 insertions(+), 17 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 91eeafd3ad10a..5f72962d0db79 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -562,6 +562,7 @@ "--vscode-modernEditorTab-activeForeground", "--vscode-modernEditorTab-activeHoverActionBackground", "--vscode-modernEditorTab-activeHoverBackground", + "--vscode-modernEditorTab-connectedStripBackground", "--vscode-modernEditorTab-hoverActionBackground", "--vscode-modernEditorTab-hoverBackground", "--vscode-modernEditorTab-hoverForeground", diff --git a/extensions/theme-defaults/themes/2026-dark.json b/extensions/theme-defaults/themes/2026-dark.json index fd533bf1680ab..a1d345f65cafd 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", + "modernEditorTab.connectedStripBackground": "#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 45dc4f1f026cf..bca9db64ec0d2 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", + "modernEditorTab.connectedStripBackground": "#EAEAEA", "tab.activeBorder": "#FFFFFF", "editorGroupHeader.tabsBorder": "#F0F1F2", "breadcrumb.foreground": "#606060", diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index fdb58dce364a9..6ed3fc1cb3264 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -720,6 +720,8 @@ export const MODERN_EDITOR_TAB_ACTIVE_ACTION_BACKGROUND = registerColor('modernE export const MODERN_EDITOR_TAB_ACTIVE_FOREGROUND = registerColor('modernEditorTab.activeForeground', MODERN_TAB_ACTIVE_FOREGROUND, localize('modernEditorTabActiveForeground', "Foreground color of active editor tabs when the modern tab style is enabled.")); +export const MODERN_EDITOR_TAB_CONNECTED_STRIP_BACKGROUND = registerColor('modernEditorTab.connectedStripBackground', EDITOR_GROUP_HEADER_TABS_BACKGROUND, localize('modernEditorTabConnectedStripBackground', "Background color of the editor tab strip when the connected modern editor tab style is enabled.")); + export const MODERN_EDITOR_TAB_INACTIVE_BACKGROUND = registerColor('modernEditorTab.inactiveBackground', Color.transparent, localize('modernEditorTabInactiveBackground', "Background color of inactive editor tabs when the modern tab style is enabled.")); export const MODERN_EDITOR_TAB_HOVER_BACKGROUND = registerColor('modernEditorTab.hoverBackground', MODERN_TAB_HOVER_BACKGROUND, localize('modernEditorTabHoverBackground', "Background color of editor tabs when hovering and the modern tab style is enabled.")); diff --git a/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts b/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts index df9d6fd0ba850..b8c1c739ab35a 100644 --- a/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts +++ b/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts @@ -5,7 +5,7 @@ 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 { MODERN_EDITOR_TAB_CONNECTED_STRIP_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND } from '../../../common/theme.js'; import './media/connectedEditorTabs.css'; registerThemingParticipant((theme, collector) => { @@ -14,7 +14,7 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-workbench.modern-ui.modern-ui-connected-editor-tabs { --modern-ui-connected-tab-surface: ${background}; }`); } - const stripColor = theme.getColor(EDITOR_GROUP_HEADER_TABS_BACKGROUND) ?? background; + const stripColor = theme.getColor(MODERN_EDITOR_TAB_CONNECTED_STRIP_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}; }`); diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index b027d35112ed4..685850ec6fd69 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -12,7 +12,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; } 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 319345d661b55..67c9c7596b3da 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_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_CONNECTED_STRIP_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'; @@ -1890,7 +1890,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'); @@ -1905,6 +1905,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'); @@ -1918,7 +1920,9 @@ suite('ModernUIContribution', () => { const theme = ColorThemeData.createUnloadedTheme('vs', { [editorBackground]: '#ffffff', 'editorGroup.border': '#123456', + 'editorGroupHeader.tabsBorder': '#445566', 'editorGroupHeader.tabsBackground': '#eeeeee', + [MODERN_EDITOR_TAB_CONNECTED_STRIP_BACKGROUND]: '#ddeeff', [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND]: '#abcdef', }); theme.setCustomColors({ [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND]: color }); @@ -1932,15 +1936,16 @@ 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)'), 'rgb(255, 255, 255)'], header: 'rgb(238, 238, 238)', strip: 'rgb(221, 238, 255)', editorSplitBorder: '#123456' }, + { surface: ['rgb(255, 255, 255)', ...Array(4).fill('rgb(68, 85, 102)'), 'rgb(255, 255, 255)'], header: 'rgb(238, 238, 238)', strip: 'rgb(221, 238, 255)', editorSplitBorder: '#123456' }, + { surface: ['rgb(255, 255, 255)', ...Array(4).fill('rgb(68, 85, 102)'), 'rgb(255, 255, 255)'], header: 'rgb(238, 238, 238)', strip: 'rgb(221, 238, 255)', editorSplitBorder: '#123456' }, ]); }); @@ -2243,6 +2248,7 @@ suite('ModernUIContribution', () => { root.classList.add(themeType); const theme = ColorThemeData.createUnloadedTheme(themeType, { [editorBackground]: '#333333', + 'editorGroupHeader.tabsBorder': '#252526', focusBorder: '#ffaa00', contrastBorder: '#888888', }); @@ -2251,7 +2257,7 @@ 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 border = highContrast ? (active ? 'rgb(255, 170, 0)' : 'rgb(136, 136, 136)') : 'rgb(37, 37, 38)'; const frame = targetWindow.getComputedStyle(body, '::after'); const breadcrumbFrame = targetWindow.getComputedStyle(breadcrumbs, '::after'); assert.deepStrictEqual({ @@ -2685,6 +2691,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 === MODERN_EDITOR_TAB_CONNECTED_STRIP_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); @@ -2698,8 +2705,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, MODERN_EDITOR_TAB_CONNECTED_STRIP_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, @@ -2722,8 +2729,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)', From 0e81bc0f3f157ec90d9f4a4b70b467f3290cee15 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 14:46:36 -0700 Subject: [PATCH 08/29] Editor: share connected tab left border Overlap first and wrapped-row-start connected fills by one stroke so their left edge reuses the editor body border instead of drawing an adjacent double border. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../contrib/modernUI/browser/media/connectedEditorTabs.css | 5 +++++ .../modernUI/test/browser/modernUI.contribution.test.ts | 2 +- .../browser/parts/editor/multiEditorTabsControl.test.ts | 7 ++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 685850ec6fd69..58f3ca6a59e68 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -170,6 +170,11 @@ box-shadow: none; } +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active: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-container > .tab.last-in-row + .tab.active:not(.connected-tab-upper-row) > .tab-fill { + inset-inline-start: calc(-1 * var(--vscode-strokeThickness)); +} + /* The foreground mask below removes the cap's side stroke where each shoulder curves out. */ .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active > .tab-fill::before, .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active > .tab-fill::after { 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 67c9c7596b3da..c9679fe856e7e 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 @@ -2335,7 +2335,7 @@ suite('ModernUIContribution', () => { }, { tabBounds: tabBounds.toJSON(), labelBounds: labelBounds.toJSON(), - fillExpansion: [0, 0, 0], + fillExpansion: connected ? [0, 1, 0] : [0, 0, 0], marginRight: connected ? shoulderWidth : '0px', topRadius: connected ? '5px' : '4px', }, JSON.stringify({ classes, theme, activeGroup, compact })); 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 437f02d73c610..cfc82d5f0de1c 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -411,9 +411,10 @@ 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, firstShoulder: mainWindow.getComputedStyle(firstFill, '::before').content, }, { results: [ @@ -421,7 +422,7 @@ suite('MultiEditorTabsControl', () => { { width: 172, leftShoulderVisible: true, rightShoulderVisible: true }, { width: 120, leftShoulderVisible: true, rightShoulderVisible: false }, ], - firstTabFlush: true, + firstBorderInset: '-1px', firstShoulder: 'none', }); }); @@ -707,7 +708,7 @@ suite('MultiEditorTabsControl', () => { rowPaddingLeft: rowStyle.paddingLeft, rowPaddingTop: rowStyle.paddingTop, }, { - active: { top: '0px', left: '0px', right: '0px', bottom: '-2px' }, + active: { top: '0px', left: '-1px', right: '0px', bottom: '-2px' }, inactive: { top: '0px', left: '0px', right: '0px', bottom: '-1px' }, alignItems: 'flex-start', editorActionsHeight: '32px', From 20294057ea9898e9a71e6b04b77db06347c1d8d8 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 14:55:15 -0700 Subject: [PATCH 09/29] Editor: simplify connected tab color ownership Use a single editorGroupHeader.connectedTabsBackground token and emit its derived styles only under the connected-tabs root marker. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build/lib/stylelint/vscode-known-variables.json | 2 +- extensions/theme-defaults/themes/2026-dark.json | 2 +- extensions/theme-defaults/themes/2026-light.json | 2 +- src/vs/workbench/common/theme.ts | 4 ++-- .../contrib/modernUI/browser/connectedEditorTabs.ts | 12 +++++++----- .../test/browser/modernUI.contribution.test.ts | 8 ++++---- 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 5f72962d0db79..11c15514486b0 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", @@ -562,7 +563,6 @@ "--vscode-modernEditorTab-activeForeground", "--vscode-modernEditorTab-activeHoverActionBackground", "--vscode-modernEditorTab-activeHoverBackground", - "--vscode-modernEditorTab-connectedStripBackground", "--vscode-modernEditorTab-hoverActionBackground", "--vscode-modernEditorTab-hoverBackground", "--vscode-modernEditorTab-hoverForeground", diff --git a/extensions/theme-defaults/themes/2026-dark.json b/extensions/theme-defaults/themes/2026-dark.json index a1d345f65cafd..4c5f1da5be7e8 100644 --- a/extensions/theme-defaults/themes/2026-dark.json +++ b/extensions/theme-defaults/themes/2026-dark.json @@ -206,7 +206,7 @@ "tab.unfocusedInactiveBackground": "#191A1B", "tab.unfocusedInactiveForeground": "#444444", "editorGroupHeader.tabsBackground": "#191A1B", - "modernEditorTab.connectedStripBackground": "#202122", + "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 bca9db64ec0d2..f305d729d3db0 100644 --- a/extensions/theme-defaults/themes/2026-light.json +++ b/extensions/theme-defaults/themes/2026-light.json @@ -213,7 +213,7 @@ "tab.unfocusedInactiveBackground": "#FAFAFD", "tab.unfocusedInactiveForeground": "#BBBBBB", "editorGroupHeader.tabsBackground": "#FAFAFD", - "modernEditorTab.connectedStripBackground": "#EAEAEA", + "editorGroupHeader.connectedTabsBackground": "#EAEAEA", "tab.activeBorder": "#FFFFFF", "editorGroupHeader.tabsBorder": "#F0F1F2", "breadcrumb.foreground": "#606060", diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 6ed3fc1cb3264..1b17eeeda3649 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 the editor group title header when the connected tab style is enabled. 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.")); @@ -720,8 +722,6 @@ export const MODERN_EDITOR_TAB_ACTIVE_ACTION_BACKGROUND = registerColor('modernE export const MODERN_EDITOR_TAB_ACTIVE_FOREGROUND = registerColor('modernEditorTab.activeForeground', MODERN_TAB_ACTIVE_FOREGROUND, localize('modernEditorTabActiveForeground', "Foreground color of active editor tabs when the modern tab style is enabled.")); -export const MODERN_EDITOR_TAB_CONNECTED_STRIP_BACKGROUND = registerColor('modernEditorTab.connectedStripBackground', EDITOR_GROUP_HEADER_TABS_BACKGROUND, localize('modernEditorTabConnectedStripBackground', "Background color of the editor tab strip when the connected modern editor tab style is enabled.")); - export const MODERN_EDITOR_TAB_INACTIVE_BACKGROUND = registerColor('modernEditorTab.inactiveBackground', Color.transparent, localize('modernEditorTabInactiveBackground', "Background color of inactive editor tabs when the modern tab style is enabled.")); export const MODERN_EDITOR_TAB_HOVER_BACKGROUND = registerColor('modernEditorTab.hoverBackground', MODERN_TAB_HOVER_BACKGROUND, localize('modernEditorTabHoverBackground', "Background color of editor tabs when hovering and the modern tab style is enabled.")); diff --git a/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts b/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts index b8c1c739ab35a..9b432f1749b70 100644 --- a/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts +++ b/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts @@ -5,23 +5,25 @@ import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; -import { MODERN_EDITOR_TAB_CONNECTED_STRIP_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND } from '../../../common/theme.js'; +import { EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND } from '../../../common/theme.js'; import './media/connectedEditorTabs.css'; +const connectedTabsSelector = '.monaco-workbench.modern-ui.modern-ui-connected-editor-tabs'; + 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(MODERN_EDITOR_TAB_CONNECTED_STRIP_BACKGROUND) ?? background; + const stripColor = theme.getColor(EDITOR_GROUP_HEADER_CONNECTED_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}; }`); + collector.addRule(`${connectedTabsSelector} { --modern-ui-connected-tab-strip-background: ${stripBackground}; }`); } const hoverBackground = theme.getColor(MODERN_EDITOR_TAB_HOVER_BACKGROUND); if (stripBackground && 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(stripBackground)}; }`); } }); 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 c9679fe856e7e..299a199516e9c 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, 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_CONNECTED_STRIP_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'; @@ -1922,7 +1922,7 @@ suite('ModernUIContribution', () => { 'editorGroup.border': '#123456', 'editorGroupHeader.tabsBorder': '#445566', 'editorGroupHeader.tabsBackground': '#eeeeee', - [MODERN_EDITOR_TAB_CONNECTED_STRIP_BACKGROUND]: '#ddeeff', + [EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND]: '#ddeeff', [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND]: '#abcdef', }); theme.setCustomColors({ [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND]: color }); @@ -2691,7 +2691,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 === MODERN_EDITOR_TAB_CONNECTED_STRIP_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); @@ -2705,7 +2705,7 @@ 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_CONNECTED_STRIP_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)), + 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, From e8263bf3b48be626b5524ae66af830c6bd3886cb Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 14:56:15 -0700 Subject: [PATCH 10/29] Editor: describe connected tab color as reusable Document editorGroupHeader.connectedTabsBackground by visual role rather than feature enablement so it remains suitable for any connected tab treatment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/workbench/common/theme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 1b17eeeda3649..91b466700b3df 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -223,7 +223,7 @@ 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 the editor group title header when the connected tab style is 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.")); From 9ad54347662e2aee3a737afa210fb8bc59730333 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 15:02:36 -0700 Subject: [PATCH 11/29] Editor: centralize connected tab ownership Share the connected-tab root marker across editor controls, contribution setup, and theming, and document the dedicated stylesheet scoping invariant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/workbench/browser/parts/editor/editor.ts | 3 +++ .../browser/parts/editor/editorTabsControl.ts | 4 ++-- .../browser/parts/editor/multiEditorTabsControl.ts | 10 +++++----- .../contrib/modernUI/browser/connectedEditorTabs.ts | 3 ++- .../modernUI/browser/media/connectedEditorTabs.css | 3 +++ .../contrib/modernUI/browser/modernUI.contribution.ts | 6 +++--- 6 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index a9c9cff11001a..d80c0f97a1630 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 = `.modern-ui.${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 6e0590648871f..26e3b4fca240e 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 5b0453c1188d1..45419cc86efea 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'; @@ -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; @@ -2360,7 +2360,7 @@ 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(); diff --git a/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts b/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts index 9b432f1749b70..bd8955b254357 100644 --- a/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts +++ b/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts @@ -5,10 +5,11 @@ import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; +import { CONNECTED_EDITOR_TABS_SELECTOR } from '../../../browser/parts/editor/editor.js'; import { EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND } from '../../../common/theme.js'; import './media/connectedEditorTabs.css'; -const connectedTabsSelector = '.monaco-workbench.modern-ui.modern-ui-connected-editor-tabs'; +const connectedTabsSelector = `.monaco-workbench${CONNECTED_EDITOR_TABS_SELECTOR}`; registerThemingParticipant((theme, collector) => { const background = theme.getColor(editorBackground); diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 58f3ca6a59e68..20031ea4fa24d 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -3,6 +3,9 @@ * 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. */ .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor.editor-tabs-multiple:not(.modal-editor-part) { diff --git a/src/vs/workbench/contrib/modernUI/browser/modernUI.contribution.ts b/src/vs/workbench/contrib/modernUI/browser/modernUI.contribution.ts index 3058363e289e0..041cc46f7450e 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); } From 463cc85508597def8cf24b95718bd88810443d64 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 15:07:39 -0700 Subject: [PATCH 12/29] docs: define connected tab ownership Document the reusable connected header color, border ownership, marker lifecycle, and separation between CSS geometry and editor control behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/workbench/contrib/modernUI/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/README.md b/src/vs/workbench/contrib/modernUI/README.md index 3d0e0b5dd5c5f..8e266d26af72f 100644 --- a/src/vs/workbench/contrib/modernUI/README.md +++ b/src/vs/workbench/contrib/modernUI/README.md @@ -11,20 +11,22 @@ 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 the outside shoulder and share the editor body's left stroke. Upper wrapped rows and a separate pinned row retain the original rounded pills. - `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. `connectedEditorTabs.css` owns all 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. 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 and its action area so 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. Dark 2026 and Light 2026 use connected-specific neutral fills (`#202122` and `#EAEAEA`) while retaining their original global `editorGroupHeader.tabsBackground` and `tab.inactiveBackground` values. Classic editor tabs, pill tabs, and Agents/session tab surfaces therefore do not inherit connected-document colors. 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 +48,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` | From 9de120ceaf5c22b0dc2784bb1878e0d9a0d3aab2 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 15:16:24 -0700 Subject: [PATCH 13/29] Editor: derive connected action height from the row Use flex stretch and the existing separator stroke so connected editor actions follow default and compact row heights without a special row-height variable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build/lib/stylelint/vscode-known-variables.json | 1 - .../contrib/modernUI/browser/media/connectedEditorTabs.css | 4 +++- src/vs/workbench/contrib/modernUI/browser/media/tabs.css | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 11c15514486b0..884c7402c46ad 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1116,7 +1116,6 @@ "--modern-ui-editor-tab-inactive-background", "--modern-ui-editor-tab-inactive-foreground", "--modern-ui-editor-tab-last-pinned-border", - "--modern-ui-editor-tab-row-height", "--modern-ui-editor-tab-unfocused-active-background", "--modern-ui-editor-tab-unfocused-active-border", "--modern-ui-editor-tab-unfocused-active-border-top", diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 20031ea4fa24d..d53e898e98271 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -99,7 +99,9 @@ } .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 { - height: var(--modern-ui-editor-tab-row-height); + 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 { diff --git a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css index c0705d16f5356..1046eee3480cb 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css @@ -58,7 +58,6 @@ background-color: transparent !important; cursor: default; --editor-group-tab-height: 24px !important; - --modern-ui-editor-tab-row-height: calc(var(--editor-group-tab-height) + var(--vscode-spacing-size80)); } /* Compact tab height: 20px tab + 4px top + 4px bottom padding = 28px total. @@ -241,7 +240,7 @@ .modern-ui-tabs.monaco-workbench .part.editor .tabs-and-actions-container:not(.wrapping) .tabs-container, .modern-ui-tabs.monaco-workbench .part.editor .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab { - min-height: var(--modern-ui-editor-tab-row-height); + min-height: calc(var(--editor-group-tab-height) + var(--vscode-spacing-size40) * 2); } .modern-ui-tabs.monaco-workbench .part.editor .tabs-and-actions-container:not(.wrapping) .tabs-container > .tab { From 9e66d76428e8bf5c8f5d5378c051f0b3bcb91d68 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 15:16:30 -0700 Subject: [PATCH 14/29] Editor: isolate all connected tab palettes Move every strip background introduced by #335605 to editorGroupHeader.connectedTabsBackground while preserving the original shared tab backgrounds and independent foreground contrast fixes. Refs #336320 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- extensions/theme-defaults/themes/dark_modern.json | 5 +++-- extensions/theme-defaults/themes/dark_vs.json | 3 +-- extensions/theme-defaults/themes/light_modern.json | 5 +++-- extensions/theme-defaults/themes/light_vs.json | 3 +-- .../theme-quietlight/themes/quietlight-color-theme.json | 3 +-- src/vs/workbench/contrib/modernUI/README.md | 2 +- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/extensions/theme-defaults/themes/dark_modern.json b/extensions/theme-defaults/themes/dark_modern.json index 67688754a90f3..7aee67bd82c35 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 7b9c6d4a99b6f..727ab05c96f74 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 d76ab0458cb76..5704bb3ecf3fc 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 3c5363c5a500c..ef114373e6772 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 253cfb58fe32e..8ec2814a7769c 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/contrib/modernUI/README.md b/src/vs/workbench/contrib/modernUI/README.md index 8e266d26af72f..0abd0d338443a 100644 --- a/src/vs/workbench/contrib/modernUI/README.md +++ b/src/vs/workbench/contrib/modernUI/README.md @@ -26,7 +26,7 @@ Automatic reveal includes the complete shoulder and rounds fractional layout bou The connected design uses `editor.background` for the active tab and its action area so 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. -`editorGroupHeader.connectedTabsBackground` defaults to `editorGroupHeader.tabsBackground`, so themes can keep their established strip palette without customization. Dark 2026 and Light 2026 use connected-specific neutral fills (`#202122` and `#EAEAEA`) while retaining their original global `editorGroupHeader.tabsBackground` and `tab.inactiveBackground` values. Classic editor tabs, pill tabs, and Agents/session tab surfaces therefore do not inherit connected-document colors. +`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, so Classic editor tabs, pill tabs, and Agents/session tab surfaces do not inherit connected-document colors. 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. From b519284f9661892b7723f5afec98f4da66d0da4b Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 15:37:12 -0700 Subject: [PATCH 15/29] Editor: balance connected tab close spacing Reserve upper-row pill insets consistently in tab width and close-action positioning, with single, terminal, wrapped, and left-action regression coverage. Consolidates #336791 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/media/connectedEditorTabs.css | 9 ++ .../editor/editorTabBar.fixture.css | 4 + .../editor/editorTabBar.fixture.ts | 49 +++++++++++ .../editor/multiEditorTabsControl.test.ts | 85 +++++++++++++++++++ 4 files changed, 147 insertions(+) diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index d53e898e98271..f09e70c16eecc 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -442,6 +442,7 @@ --modern-ui-editor-tab-unfocused-hover-background: var(--modern-ui-editor-tab-hover-background); --modern-ui-editor-tab-action-hover-background: var(--modern-ui-editor-tab-hover-background); --modern-ui-editor-tab-action-unfocused-hover-background: var(--modern-ui-editor-tab-hover-background); + --modern-ui-tab-action-padding: calc(var(--vscode-spacing-size280) + var(--modern-ui-connected-tab-shoulder-radius) + 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 { @@ -454,6 +455,14 @@ inset-block: calc(-1 * 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:not(.sticky-compact):not(.tab-actions-left) > .tab-actions { + right: calc(var(--modern-ui-connected-tab-shoulder-radius) + 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:not(.sticky-compact).tab-actions-left > .tab-actions { + left: calc(var(--modern-ui-connected-tab-shoulder-radius) + var(--vscode-spacing-size20)); +} + .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 { border-color: var(--modern-ui-connected-tab-border); } 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 f466022eba202..e09af4300f217 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 ee84e4381c980..e722aa3b62840 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 cfc82d5f0de1c..163bc702f7698 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -349,6 +349,91 @@ 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(); + const measure = () => { + const tab = container.querySelector('.tab.active')!; + const fill = tab.querySelector('.tab-fill')!; + const action = tab.querySelector('.action-label')!; + const label = tab.querySelector('.monaco-icon-label-container')!; + const fillBounds = fill.getBoundingClientRect(); + const actionBounds = action.getBoundingClientRect(); + const actionStyle = mainWindow.getComputedStyle(action); + return { + top: actionBounds.top - fillBounds.top, + right: fillBounds.right - actionBounds.right, + left: actionBounds.left - label.getBoundingClientRect().right, + width: fillBounds.width, + padding: [actionStyle.paddingTop, actionStyle.paddingRight, actionStyle.paddingBottom, actionStyle.paddingLeft], + }; + }; + + await layoutConnectedGroup(group, 400); + const multiple = measure(); + + 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, + }, + wrapped: { + top: wrappedBottom.top === wrappedUpper.top, + right: wrappedBottom.right === wrappedUpper.right, + left: wrappedBottom.left === wrappedUpper.left, + }, + horizontal: { + right: measurements.every(measurement => measurement.right === multiple.right), + left: measurements.every(measurement => measurement.left === multiple.left), + }, + leftAction: { + top: leftSingle.top === leftMultiple.top, + right: leftSingle.right === leftMultiple.right, + left: leftSingle.left === leftMultiple.left, + width: leftSingle.width === leftMultiple.width, + }, + 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 }, + wrapped: { top: true, right: true, left: true }, + horizontal: { right: true, left: true }, + leftAction: { top: true, right: true, left: true, width: true }, + actionPadding: true, + }); + }); + test('reveals the active tab with its right shoulder outside the label and action', async () => { const group = connectedGroup(); const oldOptions = partOptions; From 95fbe92dcedeaf69a228e51284e57be6efd1fcc5 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 15:48:37 -0700 Subject: [PATCH 16/29] Editor: complete connected tab spacing Restore the terminal close-action shoulder reservation from the consolidated fix and remove the generic wrapped-row top inset so connected strips meet the header edge directly.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/media/connectedEditorTabs.css | 14 +++++++++- .../browser/modernUI.contribution.test.ts | 2 ++ .../editor/multiEditorTabsControl.test.ts | 28 +++++++++---------- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index f09e70c16eecc..7dd28158d3d50 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -91,11 +91,11 @@ .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; - 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) > .editor-actions { @@ -386,6 +386,18 @@ } /* 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):not(.tab-actions-left):is(:last-child, .last-in-row) { + --modern-ui-tab-action-padding: calc(var(--vscode-spacing-size280) + var(--modern-ui-connected-tab-shoulder-radius) + var(--modern-ui-connected-tab-shoulder-radius)); +} + +.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).tab-actions-left:is(:last-child, .last-in-row) { + padding-right: calc(var(--vscode-spacing-size80) + var(--modern-ui-connected-tab-shoulder-radius)) !important; +} + +.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):not(.sticky-compact):not(.tab-actions-left):is(:last-child, .last-in-row) > .tab-actions { + right: calc(var(--modern-ui-connected-tab-shoulder-radius) + var(--modern-ui-connected-tab-shoulder-radius)); +} + .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) { margin-right: var(--modern-ui-connected-tab-shoulder-radius); } 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 299a199516e9c..baeff19018713 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 @@ -2468,10 +2468,12 @@ suite('ModernUIContribution', () => { }; assert.deepStrictEqual({ + rowPaddingTop: targetWindow.getComputedStyle(row).paddingTop, upper: geometry(upperTab, upperFill), inactiveBottom: geometry(inactiveBottomTab, inactiveBottomFill), activeBottom: geometry(activeBottomTab, activeBottomFill), }, { + rowPaddingTop: '0px', upper: { tabBorders: ['2px', '2px'], fillInsets: ['-2px', '-2px'] }, inactiveBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-1px'] }, activeBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-2px'] }, 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 163bc702f7698..5cf64775047bd 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -370,6 +370,7 @@ suite('MultiEditorTabsControl', () => { await layoutConnectedGroup(group, 400); const multiple = measure(); + const shoulder = Number.parseFloat(mainWindow.getComputedStyle(container.querySelector('.tab.active > .tab-fill')!, '::after').width); const secondEditor = model.getEditorByIndex(1)!; model.closeEditor(secondEditor); @@ -405,31 +406,30 @@ suite('MultiEditorTabsControl', () => { assert.deepStrictEqual({ single: { top: single.top === multiple.top, - right: single.right === multiple.right, left: single.left === multiple.left, - width: single.width === multiple.width, + rightShoulderReserve: single.right - multiple.right === shoulder, + widthShoulderReserve: single.width - multiple.width === shoulder, }, wrapped: { top: wrappedBottom.top === wrappedUpper.top, - right: wrappedBottom.right === wrappedUpper.right, left: wrappedBottom.left === wrappedUpper.left, + rightShoulderReserve: wrappedBottom.right - wrappedUpper.right === shoulder, }, horizontal: { - right: measurements.every(measurement => measurement.right === multiple.right), left: measurements.every(measurement => measurement.left === multiple.left), }, leftAction: { top: leftSingle.top === leftMultiple.top, - right: leftSingle.right === leftMultiple.right, left: leftSingle.left === leftMultiple.left, - width: leftSingle.width === leftMultiple.width, + rightShoulderReserve: leftSingle.right - leftMultiple.right === shoulder, + widthShoulderReserve: leftSingle.width - leftMultiple.width === shoulder, }, 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 }, - wrapped: { top: true, right: true, left: true }, - horizontal: { right: true, left: true }, - leftAction: { top: true, right: true, left: true, width: true }, + single: { top: true, left: true, rightShoulderReserve: true, widthShoulderReserve: true }, + wrapped: { top: true, left: true, rightShoulderReserve: true }, + horizontal: { left: true }, + leftAction: { top: true, left: true, rightShoulderReserve: true, widthShoulderReserve: true }, actionPadding: true, }); }); @@ -702,8 +702,8 @@ suite('MultiEditorTabsControl', () => { }); } assert.deepStrictEqual(measurements, [ - { tabHeight: 'default', stripHeight: 60, wrapping: true, upperRow: false, gap: -1, clippingGap: 0, bottomRadius: '0px', shoulder: '""', visibleHeights: [28, 28], rowGap: 2 }, - { tabHeight: 'compact', stripHeight: 52, wrapping: true, upperRow: false, gap: -1, 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 }, ]); }); @@ -730,8 +730,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] }, ]); }); From bc06ad0b95a0e7b40b44f80aadda687e1fcb6570 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 15:52:40 -0700 Subject: [PATCH 17/29] Editor: balance terminal tab actions Let the external shoulder reservation own terminal geometry so close actions keep the same visible inset as ordinary and wrapped tabs. Add a regression assertion for balanced label- and edge-side action spacing.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/media/connectedEditorTabs.css | 12 ---------- .../editor/multiEditorTabsControl.test.ts | 23 +++++++++++-------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 7dd28158d3d50..bf6b35ea14fba 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -386,18 +386,6 @@ } /* 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):not(.tab-actions-left):is(:last-child, .last-in-row) { - --modern-ui-tab-action-padding: calc(var(--vscode-spacing-size280) + var(--modern-ui-connected-tab-shoulder-radius) + var(--modern-ui-connected-tab-shoulder-radius)); -} - -.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).tab-actions-left:is(:last-child, .last-in-row) { - padding-right: calc(var(--vscode-spacing-size80) + var(--modern-ui-connected-tab-shoulder-radius)) !important; -} - -.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):not(.sticky-compact):not(.tab-actions-left):is(:last-child, .last-in-row) > .tab-actions { - right: calc(var(--modern-ui-connected-tab-shoulder-radius) + var(--modern-ui-connected-tab-shoulder-radius)); -} - .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) { margin-right: var(--modern-ui-connected-tab-shoulder-radius); } 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 5cf64775047bd..71eb077f75e5f 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -370,7 +370,7 @@ suite('MultiEditorTabsControl', () => { await layoutConnectedGroup(group, 400); const multiple = measure(); - const shoulder = Number.parseFloat(mainWindow.getComputedStyle(container.querySelector('.tab.active > .tab-fill')!, '::after').width); + const stroke = Number.parseFloat(mainWindow.getComputedStyle(container.querySelector('.tab.active')!).getPropertyValue('--vscode-strokeThickness')); const secondEditor = model.getEditorByIndex(1)!; model.closeEditor(secondEditor); @@ -406,30 +406,33 @@ suite('MultiEditorTabsControl', () => { assert.deepStrictEqual({ single: { top: single.top === multiple.top, + right: single.right === multiple.right, left: single.left === multiple.left, - rightShoulderReserve: single.right - multiple.right === shoulder, - widthShoulderReserve: single.width - multiple.width === shoulder, + width: single.width === multiple.width, }, wrapped: { top: wrappedBottom.top === wrappedUpper.top, + right: wrappedBottom.right === wrappedUpper.right, left: wrappedBottom.left === wrappedUpper.left, - rightShoulderReserve: wrappedBottom.right - wrappedUpper.right === shoulder, }, horizontal: { + right: measurements.every(measurement => measurement.right === multiple.right), left: measurements.every(measurement => measurement.left === multiple.left), }, leftAction: { top: leftSingle.top === leftMultiple.top, + right: leftSingle.right === leftMultiple.right, left: leftSingle.left === leftMultiple.left, - rightShoulderReserve: leftSingle.right - leftMultiple.right === shoulder, - widthShoulderReserve: leftSingle.width - leftMultiple.width === shoulder, + width: leftSingle.width === leftMultiple.width, }, + 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, left: true, rightShoulderReserve: true, widthShoulderReserve: true }, - wrapped: { top: true, left: true, rightShoulderReserve: true }, - horizontal: { left: true }, - leftAction: { top: true, left: true, rightShoulderReserve: true, widthShoulderReserve: true }, + single: { top: true, right: true, left: true, width: true }, + wrapped: { top: true, right: true, left: true }, + horizontal: { right: true, left: true }, + leftAction: { top: true, right: true, left: true, width: true }, + balancedActionInsets: true, actionPadding: true, }); }); From 3a00dd03cf1ccc93eb60ef74a8ab8506cea688a3 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 15:58:28 -0700 Subject: [PATCH 18/29] Editor: share connected tab frame borders Classify the outermost connected tab row explicitly so active tabs can reuse the editor frame's top stroke. First-position active tabs also reuse the left stroke, while inner wrapped rows retain pill gutters and balanced close actions.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/parts/editor/multiEditorTabsControl.ts | 5 ++++- src/vs/workbench/contrib/modernUI/README.md | 2 +- .../modernUI/browser/media/connectedEditorTabs.css | 14 ++++++++++++++ .../test/browser/modernUI.contribution.test.ts | 9 +++++---- .../parts/editor/multiEditorTabsControl.test.ts | 11 ++++++++--- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 45419cc86efea..64f8e6b7eb618 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -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); diff --git a/src/vs/workbench/contrib/modernUI/README.md b/src/vs/workbench/contrib/modernUI/README.md index 0abd0d338443a..ceb2475938a15 100644 --- a/src/vs/workbench/contrib/modernUI/README.md +++ b/src/vs/workbench/contrib/modernUI/README.md @@ -11,7 +11,7 @@ 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. First tabs and wrapped-row starts omit the outside shoulder and share the editor body's left stroke. 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 the outside shoulder and share the editor body's left stroke. Upper wrapped rows and a separate pinned row retain rounded pill geometry between rows, while an active tab on the outermost row reuses the editor frame's top stroke and, at the first position, its left stroke. - `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. diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index bf6b35ea14fba..ab3cf788f6ecd 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -455,6 +455,20 @@ inset-block: calc(-1 * var(--vscode-spacing-size20)); } +.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 { + top: calc(-1 * var(--vscode-strokeThickness)); +} + +.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.connected-tab-upper-row > .tab-fill { + top: calc(-1 * (var(--vscode-spacing-size20) + var(--vscode-strokeThickness))); + border-top-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-container > .tab.active.connected-tab-top-row.connected-tab-upper-row:first-child > .tab-fill { + left: calc(-1 * var(--vscode-strokeThickness)); + border-left-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-container > .tab.connected-tab-upper-row:not(.sticky-compact):not(.tab-actions-left) > .tab-actions { right: calc(var(--modern-ui-connected-tab-shoulder-radius) + var(--vscode-spacing-size20)); } 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 baeff19018713..19b3f72ec6871 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 @@ -2451,7 +2451,7 @@ suite('ModernUIContribution', () => { 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'); + 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'); @@ -2464,6 +2464,7 @@ suite('ModernUIContribution', () => { return { tabBorders: [tabStyle.borderTopWidth, tabStyle.borderBottomWidth], fillInsets: [fillStyle.top, fillStyle.bottom], + fillInlineStart: fillStyle.left, }; }; @@ -2474,9 +2475,9 @@ suite('ModernUIContribution', () => { activeBottom: geometry(activeBottomTab, activeBottomFill), }, { rowPaddingTop: '0px', - upper: { tabBorders: ['2px', '2px'], fillInsets: ['-2px', '-2px'] }, - inactiveBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-1px'] }, - activeBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-2px'] }, + upper: { tabBorders: ['2px', '2px'], fillInsets: ['-3px', '-2px'], fillInlineStart: '-1px' }, + inactiveBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-1px'], fillInlineStart: '0px' }, + activeBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-2px'], fillInlineStart: '0px' }, }); }); 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 71eb077f75e5f..584fa0fce8ae9 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -411,7 +411,7 @@ suite('MultiEditorTabsControl', () => { width: single.width === multiple.width, }, wrapped: { - top: wrappedBottom.top === wrappedUpper.top, + topBorderReserve: wrappedUpper.top - wrappedBottom.top === stroke, right: wrappedBottom.right === wrappedUpper.right, left: wrappedBottom.left === wrappedUpper.left, }, @@ -429,7 +429,7 @@ suite('MultiEditorTabsControl', () => { 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 }, - wrapped: { top: true, right: true, left: true }, + wrapped: { topBorderReserve: true, right: true, left: true }, horizontal: { right: true, left: true }, leftAction: { top: true, right: true, left: true, width: true }, balancedActionInsets: true, @@ -600,11 +600,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: '-3px', shoulder: 'none' }, unwrapped: [false, false], unwrappedTop: [true, true] } + ); }); test('connected wrapped last tab adds its shoulder to the editor actions margin', async () => { From fbbeef5be17197246b06de10c2af46d1ad8bb7ca Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 16:05:21 -0700 Subject: [PATCH 19/29] Editor: give frame sole border ownership Avoid overlapping the connected tab and editor frame strokes. The outermost tab now starts at the frame's inner edge with transparent top and left borders, allowing the editor frame and its clipping radius to be the sole painter at every zoom level.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/media/connectedEditorTabs.css | 16 ++++++---------- .../test/browser/modernUI.contribution.test.ts | 7 ++++++- .../parts/editor/multiEditorTabsControl.test.ts | 6 +++--- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index ab3cf788f6ecd..52c42dd1e6427 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -455,18 +455,14 @@ inset-block: calc(-1 * var(--vscode-spacing-size20)); } -.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 { - top: calc(-1 * var(--vscode-strokeThickness)); +.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 > .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.active.connected-tab-top-row.connected-tab-upper-row > .tab-fill { - top: calc(-1 * (var(--vscode-spacing-size20) + var(--vscode-strokeThickness))); - border-top-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-container > .tab.active.connected-tab-top-row.connected-tab-upper-row:first-child > .tab-fill { - left: calc(-1 * var(--vscode-strokeThickness)); - border-left-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-container > .tab.active.connected-tab-top-row:first-child > .tab-fill { + left: 0; + border-top-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-container > .tab.connected-tab-upper-row:not(.sticky-compact):not(.tab-actions-left) > .tab-actions { 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 19b3f72ec6871..52112fc7647c7 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 @@ -2470,12 +2470,17 @@ suite('ModernUIContribution', () => { 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', - upper: { tabBorders: ['2px', '2px'], fillInsets: ['-3px', '-2px'], fillInlineStart: '-1px' }, + 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' }, }); 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 584fa0fce8ae9..af616e88bfdc7 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -411,7 +411,7 @@ suite('MultiEditorTabsControl', () => { width: single.width === multiple.width, }, wrapped: { - topBorderReserve: wrappedUpper.top - wrappedBottom.top === stroke, + top: wrappedBottom.top === wrappedUpper.top, right: wrappedBottom.right === wrappedUpper.right, left: wrappedBottom.left === wrappedUpper.left, }, @@ -429,7 +429,7 @@ suite('MultiEditorTabsControl', () => { 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 }, - wrapped: { topBorderReserve: true, right: true, left: true }, + wrapped: { top: true, right: true, left: true }, horizontal: { right: true, left: true }, leftAction: { top: true, right: true, left: true, width: true }, balancedActionInsets: true, @@ -608,7 +608,7 @@ suite('MultiEditorTabsControl', () => { 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: '-3px', shoulder: 'none' }, unwrapped: [false, false], unwrappedTop: [true, true] } + { wrapped: [true, false], wrappedTop: [true, false], upper: { inset: '-2px', shoulder: 'none' }, unwrapped: [false, false], unwrappedTop: [true, true] } ); }); From b14fea7bba1594d3789e0a794e568c46ed0b3396 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 16:11:10 -0700 Subject: [PATCH 20/29] Editor: center connected tab actions Center the close glyph in its painted action surface and shift that surface outward by the same inset so label- and shoulder-side spacing remain balanced. Preserve sole editor-frame ownership of the outer border.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/media/connectedEditorTabs.css | 16 ++++++---------- .../parts/editor/multiEditorTabsControl.test.ts | 9 +++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 52c42dd1e6427..1068a4908c55f 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -303,20 +303,16 @@ 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-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 .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:not(.sticky-compact):not(.tab-actions-left) > .tab-actions { - right: var(--modern-ui-connected-tab-shoulder-radius); + right: calc(var(--modern-ui-connected-tab-shoulder-radius) - 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-shoulder-radius) - var(--vscode-spacing-size20)); } /* Keep the complete identity and action column; only the basename can ellipsize. */ @@ -466,11 +462,11 @@ } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row:not(.sticky-compact):not(.tab-actions-left) > .tab-actions { - right: calc(var(--modern-ui-connected-tab-shoulder-radius) + var(--vscode-spacing-size20)); + right: var(--modern-ui-connected-tab-shoulder-radius); } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row:not(.sticky-compact).tab-actions-left > .tab-actions { - left: calc(var(--modern-ui-connected-tab-shoulder-radius) + var(--vscode-spacing-size20)); + left: var(--modern-ui-connected-tab-shoulder-radius); } .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/test/browser/parts/editor/multiEditorTabsControl.test.ts b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts index af616e88bfdc7..7079400e4330f 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -355,15 +355,22 @@ suite('MultiEditorTabsControl', () => { 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); return { top: actionBounds.top - fillBounds.top, right: fillBounds.right - actionBounds.right, 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], }; }; @@ -425,6 +432,7 @@ suite('MultiEditorTabsControl', () => { 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]), }, { @@ -432,6 +440,7 @@ suite('MultiEditorTabsControl', () => { wrapped: { top: true, right: true, left: true }, horizontal: { right: true, left: true }, leftAction: { top: true, right: true, left: true, width: true }, + balancedActionSurface: true, balancedActionInsets: true, actionPadding: true, }); From d31aa2ef021565720fa8940d904f41180d04ff1b Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Mon, 21 Sep 2026 18:21:30 -0700 Subject: [PATCH 21/29] Editor: repair connected tab outline regressions Preserve high contrast outlines where the outer editor frame is transparent. Account for both cap radii when hiding clipped tab slivers, and update stale assertions for centered actions and editor-owned first-tab borders. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../parts/editor/multiEditorTabsControl.ts | 2 +- .../browser/media/connectedEditorTabs.css | 4 +- .../browser/modernUI.contribution.test.ts | 45 ++++++++++++++++--- .../editor/multiEditorTabsControl.test.ts | 6 ++- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 64f8e6b7eb618..52ca59adbf4dc 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -2384,7 +2384,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { fillRight: fillBounds.right - tabsBounds.left + scrollLeft, viewportLeft: stickyTabsWidth, viewportRight, - clippingEdgeExtent: Number.parseFloat(fillStyle.borderTopLeftRadius), + clippingEdgeExtent: Math.max(Number.parseFloat(fillStyle.borderTopLeftRadius), Number.parseFloat(fillStyle.borderTopRightRadius)), shoulderExtent: Number.parseFloat(targetWindow.getComputedStyle(activeTabFill, '::after').width), }; } diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 1068a4908c55f..05a446c4ac382 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -451,11 +451,11 @@ inset-block: calc(-1 * var(--vscode-spacing-size20)); } -.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 > .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.connected-tab-top-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.active.connected-tab-top-row:first-child > .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.connected-tab-top-row:first-child > .tab-fill { left: 0; border-top-left-radius: 0; border-left-color: transparent; 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 52112fc7647c7..f70ff2b761cbc 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 @@ -2486,6 +2486,41 @@ suite('ModernUIContribution', () => { }); }); + test('retains high contrast outlines when the outermost tab owns the frame', () => { + 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: 'rgb(255, 170, 0)', left: 'rgb(255, 170, 0)' }, + { theme: 'hc-black', upperRow: true, top: 'rgb(255, 170, 0)', left: 'rgb(255, 170, 0)' }, + { theme: 'hc-light', upperRow: false, top: 'rgb(255, 170, 0)', left: 'rgb(255, 170, 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'; @@ -2575,7 +2610,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'); @@ -2624,12 +2659,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' }, }, }); }); 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 7079400e4330f..7cb687df89f73 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -512,6 +512,7 @@ suite('MultiEditorTabsControl', () => { assert.deepStrictEqual({ results, firstBorderInset: firstFillStyle.left, + firstBorderColor: firstFillStyle.borderLeftColor, firstShoulder: mainWindow.getComputedStyle(firstFill, '::before').content, }, { results: [ @@ -519,7 +520,8 @@ suite('MultiEditorTabsControl', () => { { width: 172, leftShoulderVisible: true, rightShoulderVisible: true }, { width: 120, leftShoulderVisible: true, rightShoulderVisible: false }, ], - firstBorderInset: '-1px', + firstBorderInset: '0px', + firstBorderColor: 'rgba(0, 0, 0, 0)', firstShoulder: 'none', }); }); @@ -1021,7 +1023,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' }, From 1a0bdf6085bfb9a9133810f496f9c35185428681 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Tue, 22 Sep 2026 08:31:23 -0700 Subject: [PATCH 22/29] Editor: derive close clearance from the tab surface Share action clearance across row densities and label reservation, accounting for painted borders instead of shoulder width. Keep standard row starts on the editor frame and join top-row side strokes directly to its shared top edge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../lib/stylelint/vscode-known-variables.json | 1 + .../parts/editor/multiEditorTabsControl.ts | 8 +-- src/vs/workbench/contrib/modernUI/README.md | 6 +- .../browser/media/connectedEditorTabs.css | 64 ++++++++++++++--- .../browser/modernUI.contribution.test.ts | 8 +-- .../editor/multiEditorTabsControl.test.ts | 68 +++++++++++++++---- 6 files changed, 122 insertions(+), 33 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 884c7402c46ad..13e010931fcd1 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1087,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/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 52ca59adbf4dc..ec0ef394f41cf 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -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)); @@ -2370,7 +2370,6 @@ export class MultiEditorTabsControl extends EditorTabsControl { 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`; @@ -2384,7 +2383,6 @@ export class MultiEditorTabsControl extends EditorTabsControl { fillRight: fillBounds.right - tabsBounds.left + scrollLeft, viewportLeft: stickyTabsWidth, viewportRight, - clippingEdgeExtent: Math.max(Number.parseFloat(fillStyle.borderTopLeftRadius), Number.parseFloat(fillStyle.borderTopRightRadius)), shoulderExtent: Number.parseFloat(targetWindow.getComputedStyle(activeTabFill, '::after').width), }; } @@ -2515,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); @@ -2524,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/contrib/modernUI/README.md b/src/vs/workbench/contrib/modernUI/README.md index ceb2475938a15..1cae3e2d2f550 100644 --- a/src/vs/workbench/contrib/modernUI/README.md +++ b/src/vs/workbench/contrib/modernUI/README.md @@ -11,7 +11,7 @@ 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. First tabs and wrapped-row starts omit the outside shoulder and share the editor body's left stroke. Upper wrapped rows and a separate pinned row retain rounded pill geometry between rows, while an active tab on the outermost row reuses the editor frame's top stroke and, at the first position, its left stroke. +- `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. In standard themes, 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. - `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. @@ -20,7 +20,9 @@ The connected root marker is defined with the editor control constants and share 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. In standard themes, connected tabs touching the top frame have straight upper edges so their side strokes meet the shared frame without an unpainted arc or a duplicate top stroke. 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. + +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. Upper pills add their horizontal fill inset to the reservation. Left and right actions use the same centered action surface. 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. diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 05a446c4ac382..958e6a6fe27bd 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -85,7 +85,8 @@ --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-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 { @@ -147,6 +148,8 @@ } .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); } @@ -307,12 +310,50 @@ 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 > .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: calc(var(--modern-ui-connected-tab-shoulder-radius) - var(--vscode-spacing-size20)); + 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: calc(var(--modern-ui-connected-tab-shoulder-radius) - var(--vscode-spacing-size20)); + 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.active: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)); +} + +.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.active: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.active: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.active: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.active: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 .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.wrapping .tabs-container > .tab.last-in-row + .tab.active: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. */ @@ -438,7 +479,11 @@ --modern-ui-editor-tab-unfocused-hover-background: var(--modern-ui-editor-tab-hover-background); --modern-ui-editor-tab-action-hover-background: var(--modern-ui-editor-tab-hover-background); --modern-ui-editor-tab-action-unfocused-hover-background: var(--modern-ui-editor-tab-hover-background); - --modern-ui-tab-action-padding: calc(var(--vscode-spacing-size280) + var(--modern-ui-connected-tab-shoulder-radius) + var(--vscode-spacing-size20)); + --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-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.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row > .tab-fill { @@ -451,22 +496,25 @@ inset-block: calc(-1 * var(--vscode-spacing-size20)); } -.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.connected-tab-top-row > .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.connected-tab-top-row:not(.connected-tab-upper-row) > .tab-fill { border-top-color: transparent; + border-top-left-radius: 0; + border-top-right-radius: 0; } -.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.connected-tab-top-row:first-child > .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:first-child:not(.connected-tab-upper-row) > .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.last-in-row + .tab.active:not(.connected-tab-upper-row) > .tab-fill { left: 0; border-top-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-container > .tab.connected-tab-upper-row:not(.sticky-compact):not(.tab-actions-left) > .tab-actions { - right: var(--modern-ui-connected-tab-shoulder-radius); + 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-container > .tab.connected-tab-upper-row:not(.sticky-compact).tab-actions-left > .tab-actions { - left: var(--modern-ui-connected-tab-shoulder-radius); + 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/test/browser/modernUI.contribution.test.ts b/src/vs/workbench/contrib/modernUI/test/browser/modernUI.contribution.test.ts index f70ff2b761cbc..5b75926280987 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 @@ -2335,9 +2335,9 @@ suite('ModernUIContribution', () => { }, { tabBounds: tabBounds.toJSON(), labelBounds: labelBounds.toJSON(), - fillExpansion: connected ? [0, 1, 0] : [0, 0, 0], + fillExpansion: connected && theme.startsWith('hc-') ? [0, 1, 0] : [0, 0, 0], marginRight: connected ? shoulderWidth : '0px', - topRadius: connected ? '5px' : '4px', + topRadius: connected ? (theme.startsWith('hc-') ? '5px' : '0px') : '4px', }, JSON.stringify({ classes, theme, activeGroup, compact })); } } @@ -2479,8 +2479,8 @@ suite('ModernUIContribution', () => { 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' }, + topFrame: { borderColors: ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0)'], borderTopLeftRadius: '4px' }, + upper: { tabBorders: ['2px', '2px'], fillInsets: ['-2px', '-2px'], fillInlineStart: '2px' }, inactiveBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-1px'], fillInlineStart: '0px' }, activeBottom: { tabBorders: ['0px', '0px'], fillInsets: ['0px', '-2px'], fillInlineStart: '0px' }, }); 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 7cb687df89f73..adf44a7c710c6 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -132,7 +132,7 @@ 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'); @@ -351,6 +351,7 @@ suite('MultiEditorTabsControl', () => { 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')!; @@ -362,9 +363,11 @@ suite('MultiEditorTabsControl', () => { 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, - right: fillBounds.right - actionBounds.right, + 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: [ @@ -417,14 +420,8 @@ suite('MultiEditorTabsControl', () => { left: single.left === multiple.left, width: single.width === multiple.width, }, - wrapped: { - top: wrappedBottom.top === wrappedUpper.top, - right: wrappedBottom.right === wrappedUpper.right, - left: wrappedBottom.left === wrappedUpper.left, - }, horizontal: { - right: measurements.every(measurement => measurement.right === multiple.right), - left: measurements.every(measurement => measurement.left === multiple.left), + clearance: measurements.map(measurement => [measurement.top, measurement.right, measurement.bottom, measurement.left]), }, leftAction: { top: leftSingle.top === leftMultiple.top, @@ -437,8 +434,7 @@ suite('MultiEditorTabsControl', () => { 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 }, - wrapped: { top: true, right: true, left: true }, - horizontal: { right: true, left: 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, @@ -446,6 +442,50 @@ suite('MultiEditorTabsControl', () => { }); }); + 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; @@ -812,7 +852,7 @@ suite('MultiEditorTabsControl', () => { rowPaddingLeft: rowStyle.paddingLeft, rowPaddingTop: rowStyle.paddingTop, }, { - active: { top: '0px', left: '-1px', right: '0px', bottom: '-2px' }, + active: { top: '0px', left: '0px', right: '0px', bottom: '-2px' }, inactive: { top: '0px', left: '0px', right: '0px', bottom: '-1px' }, alignItems: 'flex-start', editorActionsHeight: '32px', @@ -1023,7 +1063,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: '0px 5px 0px 0px', connectedClass: true }, + multiSelected: { clipping: '0px', edge: 'block', radius: '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' }, From a422e889f5efed248fdabe47436059813ea387c9 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Tue, 22 Sep 2026 08:40:05 -0700 Subject: [PATCH 23/29] Editor: keep compact tab outlines visible under actions Let the selected tab fill own its surface instead of painting a second opaque action-container background over the trailing cap stroke. Preserve the close target's own hover and keyboard focus treatment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/workbench/contrib/modernUI/README.md | 2 +- .../modernUI/browser/media/connectedEditorTabs.css | 10 +++++----- .../test/browser/modernUI.contribution.test.ts | 10 +++++----- .../parts/editor/multiEditorTabsControl.test.ts | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/README.md b/src/vs/workbench/contrib/modernUI/README.md index 1cae3e2d2f550..1282d87adb01f 100644 --- a/src/vs/workbench/contrib/modernUI/README.md +++ b/src/vs/workbench/contrib/modernUI/README.md @@ -26,7 +26,7 @@ Close-action clearance is derived from the existing tab height and row gutters, 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 and its action area so 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 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. `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, so Classic editor tabs, pill tabs, and Agents/session tab surfaces do not inherit connected-document colors. diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 958e6a6fe27bd..6cb87e2616d57 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -294,11 +294,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; } 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 5b75926280987..486939e4fe635 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 @@ -1943,13 +1943,13 @@ suite('ModernUIContribution', () => { }); assert.deepStrictEqual(results, [ - { surface: ['rgb(255, 255, 255)', ...Array(4).fill('rgb(68, 85, 102)'), 'rgb(255, 255, 255)'], header: 'rgb(238, 238, 238)', strip: 'rgb(221, 238, 255)', editorSplitBorder: '#123456' }, - { surface: ['rgb(255, 255, 255)', ...Array(4).fill('rgb(68, 85, 102)'), 'rgb(255, 255, 255)'], header: 'rgb(238, 238, 238)', strip: 'rgb(221, 238, 255)', editorSplitBorder: '#123456' }, - { surface: ['rgb(255, 255, 255)', ...Array(4).fill('rgb(68, 85, 102)'), 'rgb(255, 255, 255)'], 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' }, + { 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 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'); @@ -2002,7 +2002,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}`); } } 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 adf44a7c710c6..0011ba54b656d 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -720,7 +720,7 @@ suite('MultiEditorTabsControl', () => { fill: mainWindow.getComputedStyle(fill).backgroundColor, actions: mainWindow.getComputedStyle(actions).backgroundColor, }); - 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, upperRow: activeIndex === 0, fill: 'rgb(18, 52, 86)', actions: 'rgba(0, 0, 0, 0)' }); } root.classList.remove(theme); } From f502a1cb9c369603fd79a5be7e09164d9e43b239 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Tue, 22 Sep 2026 09:04:42 -0700 Subject: [PATCH 24/29] Editor: keep wrapped tab reservation width invariant Reserve the pill inset and active shoulder before classifying rows, and consume the inset in fill and action positioning rather than changing fit widths through row markers. Exercise wrapping boundary widths in both resize directions and with changing active tabs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/workbench/contrib/modernUI/README.md | 2 + .../browser/media/connectedEditorTabs.css | 15 +++++-- .../editor/multiEditorTabsControl.test.ts | 41 +++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/README.md b/src/vs/workbench/contrib/modernUI/README.md index 1282d87adb01f..e41806cd56f77 100644 --- a/src/vs/workbench/contrib/modernUI/README.md +++ b/src/vs/workbench/contrib/modernUI/README.md @@ -24,6 +24,8 @@ The bottom shoulders and freestanding caps share the same control radius plus th 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. Upper pills add their horizontal fill inset to the reservation. Left and right actions use the same centered action surface. +Wrapped tabs reserve the same trailing inset before and after row classification. Their fill and action positions consume that inset without changing tab width; upper-row markers and row-end markers must not trigger another fit-tab reflow. + 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; 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. diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 6cb87e2616d57..d51f53e015f1b 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -149,11 +149,15 @@ .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)); + --modern-ui-tab-action-padding: calc(var(--vscode-spacing-size200) + 2 * var(--modern-ui-connected-tab-action-inset) + var(--vscode-spacing-size20)); 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 > .tabs-and-actions-container.wrapping .tabs-container > .tab > :is(.tab-fill, .tab-connected-edge) { + right: 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, @@ -423,7 +427,8 @@ } /* 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) { +.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.active:not(.connected-tab-upper-row):last-child, +.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.active { margin-right: var(--modern-ui-connected-tab-shoulder-radius); } @@ -479,11 +484,11 @@ --modern-ui-editor-tab-unfocused-hover-background: var(--modern-ui-editor-tab-hover-background); --modern-ui-editor-tab-action-hover-background: var(--modern-ui-editor-tab-hover-background); --modern-ui-editor-tab-action-unfocused-hover-background: var(--modern-ui-editor-tab-hover-background); - --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-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 { @@ -513,6 +518,10 @@ 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.wrapping .tabs-container > .tab: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-container > .tab.connected-tab-upper-row:not(.sticky-compact).tab-actions-left > .tab-actions { left: var(--modern-ui-connected-tab-action-inset); } 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 0011ba54b656d..5c6be387729de 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -687,6 +687,47 @@ suite('MultiEditorTabsControl', () => { }); }); + 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')!; From 319b4c0cb101e9165e9fdbaf4ce3d920909b2bdb Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Tue, 22 Sep 2026 10:49:54 -0700 Subject: [PATCH 25/29] Editor: explicitly retain high contrast close indicators Draw hover and focus outlines on the close action itself while keeping the surrounding action container transparent. Cover focused actions in both high contrast themes and both wrapped rows; account for Electron's hidden test window when checking CSS focus state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../modernUI/browser/media/connectedEditorTabs.css | 10 ++++++++++ .../parts/editor/multiEditorTabsControl.test.ts | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index d51f53e015f1b..f1bb7aa1f810f 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -356,6 +356,16 @@ 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 { + 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.active: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)); } 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 5c6be387729de..281a9a7bc2eb2 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -731,6 +731,7 @@ suite('MultiEditorTabsControl', () => { 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'); @@ -754,14 +755,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: 'rgba(0, 0, 0, 0)' }); + 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); } From c98b9de5ab9be09655d7f98430a2e0f5faa3399c Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Wed, 23 Sep 2026 09:05:20 -0700 Subject: [PATCH 26/29] Editor: stabilize connected tab surfaces and close targets Keep wrapped fills contiguous, round free corners, and reserve close-action stroke clearance independently of selection. Preserve high-contrast target indicators without a second action-container border. Add geometry and selection regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/workbench/contrib/modernUI/README.md | 6 +- .../browser/media/connectedEditorTabs.css | 45 +++--- .../contrib/modernUI/browser/media/tabs.css | 12 +- .../browser/modernUI.contribution.test.ts | 10 +- .../editor/multiEditorTabsControl.test.ts | 147 +++++++++++++++++- 5 files changed, 179 insertions(+), 41 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/README.md b/src/vs/workbench/contrib/modernUI/README.md index e41806cd56f77..600bb5d91b675 100644 --- a/src/vs/workbench/contrib/modernUI/README.md +++ b/src/vs/workbench/contrib/modernUI/README.md @@ -20,11 +20,11 @@ The connected root marker is defined with the editor control constants and share 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 bottom shoulders and freestanding caps share the same control radius plus the outside stroke (5px with the default tokens), including at clipped viewport edges. In standard themes, connected tabs touching the top frame have straight upper edges so their side strokes meet the shared frame without an unpainted arc or a duplicate top stroke. 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. In standard themes, 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. Upper pills add their horizontal fill inset to the reservation. Left and right actions use the same centered action surface. +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 trailing inset before and after row classification. Their fill and action positions consume that inset without changing tab width; upper-row markers and row-end markers must not trigger another fit-tab reflow. +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. diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index f1bb7aa1f810f..c8c87829951d4 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -149,15 +149,11 @@ .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) + 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 > .tabs-and-actions-container.wrapping .tabs-container > .tab > :is(.tab-fill, .tab-connected-edge) { - right: 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, @@ -182,8 +178,8 @@ box-shadow: none; } -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active: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-container > .tab.last-in-row + .tab.active:not(.connected-tab-upper-row) > .tab-fill { +.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:first-child:not(.connected-tab-upper-row) > .tab-fill, +.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.last-in-row + .tab.active:not(.connected-tab-upper-row) > .tab-fill { inset-inline-start: calc(-1 * var(--vscode-strokeThickness)); } @@ -331,23 +327,24 @@ 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.active:not(.connected-tab-upper-row) { +.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)); } -.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.active:not(.connected-tab-upper-row):not(.sticky-compact):not(.tab-actions-left) > .tab-actions { +/* 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.active:not(.connected-tab-upper-row):not(:first-child):not(.sticky-compact).tab-actions-left > .tab-actions { +.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.active:not(.connected-tab-upper-row) { +.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.active:not(.connected-tab-upper-row) > .tab-actions { +.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); } @@ -366,7 +363,7 @@ 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.active:not(.connected-tab-upper-row):not(.sticky-compact).tab-actions-left > .tab-actions { +.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)); } @@ -436,9 +433,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-and-actions-container:not(.wrapping) .tabs-container > .tab.active:not(.connected-tab-upper-row):last-child, -.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.active { +/* 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); } @@ -509,30 +505,27 @@ .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:not(:is(.hc-black, .hc-light)) .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; - border-top-left-radius: 0; - border-top-right-radius: 0; } -.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:first-child:not(.connected-tab-upper-row) > .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.last-in-row + .tab.active:not(.connected-tab-upper-row) > .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:first-child:not(.connected-tab-upper-row) > .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-and-actions-container.wrapping .tabs-container > .tab:first-child > .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.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-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.wrapping .tabs-container > .tab:not(.sticky-compact):not(.tab-actions-left) > .tab-actions { +.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-container > .tab.connected-tab-upper-row:not(.sticky-compact).tab-actions-left > .tab-actions { +.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); } diff --git a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css index 1046eee3480cb..cca77a2681312 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/test/browser/modernUI.contribution.test.ts b/src/vs/workbench/contrib/modernUI/test/browser/modernUI.contribution.test.ts index 486939e4fe635..61db61a9326cb 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 @@ -2091,7 +2091,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;'; @@ -2109,7 +2109,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}`); } } }); @@ -2436,7 +2436,7 @@ suite('ModernUIContribution', () => { }); }); - test('uses direct strip geometry for connected rows while retaining upper-row pill gutters', () => { + 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'); @@ -2479,8 +2479,8 @@ suite('ModernUIContribution', () => { activeBottom: geometry(activeBottomTab, activeBottomFill), }, { rowPaddingTop: '0px', - topFrame: { borderColors: ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0)'], borderTopLeftRadius: '4px' }, - upper: { tabBorders: ['2px', '2px'], fillInsets: ['-2px', '-2px'], fillInlineStart: '2px' }, + 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' }, }); 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 281a9a7bc2eb2..1585f650aff97 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -687,6 +687,151 @@ 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; @@ -1109,7 +1254,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: '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' }, From 08e1341d6903cf591513824a820d4df22e7b3f89 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Wed, 23 Sep 2026 09:37:15 -0700 Subject: [PATCH 27/29] Editor: respect disabled connected tab action hover state Exclude disabled actions without raising hover specificity over keyboard focus. Cover the real Alt-to-Close-Others transition in both high-contrast themes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/media/connectedEditorTabs.css | 2 +- .../playwright/tests/editorTabBar.spec.ts | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 test/componentFixtures/playwright/tests/editorTabBar.spec.ts diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 2407432739a3b..e4fe0fc03be60 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -353,7 +353,7 @@ 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 { +.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)); } diff --git a/test/componentFixtures/playwright/tests/editorTabBar.spec.ts b/test/componentFixtures/playwright/tests/editorTabBar.spec.ts new file mode 100644 index 0000000000000..b3684300df8fd --- /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'); + }); +} From 8e6b60d8d38aa184bf3d0a360d574e8128972f5c Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Wed, 23 Sep 2026 10:35:49 -0700 Subject: [PATCH 28/29] Editor: consolidate connected tab high-contrast frames Share the HC editor-group frame without changing close-target geometry, keep shared strip theming separate from the Modern UI editor palette, and update regression coverage and documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workbench/browser/parts/editor/editor.ts | 2 +- src/vs/workbench/contrib/modernUI/README.md | 8 +- .../modernUI/browser/connectedEditorTabs.ts | 27 +++++-- .../browser/media/connectedEditorTabs.css | 43 +++-------- .../browser/modernUI.contribution.test.ts | 77 +++++++++++++------ .../editor/multiEditorTabsControl.test.ts | 6 +- 6 files changed, 91 insertions(+), 72 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index d80c0f97a1630..ac63294495c45 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -30,7 +30,7 @@ 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 = `.modern-ui.${CONNECTED_EDITOR_TABS_CLASS}`; +export const CONNECTED_EDITOR_TABS_SELECTOR = `.${CONNECTED_EDITOR_TABS_CLASS}`; export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = { showTabs: 'multiple', diff --git a/src/vs/workbench/contrib/modernUI/README.md b/src/vs/workbench/contrib/modernUI/README.md index 600bb5d91b675..2b5a0838308cb 100644 --- a/src/vs/workbench/contrib/modernUI/README.md +++ b/src/vs/workbench/contrib/modernUI/README.md @@ -11,16 +11,16 @@ 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. In standard themes, 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. +- `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. `connectedEditorTabs.css` owns all 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. +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 bottom shoulders and freestanding caps share the same control radius plus the outside stroke (5px with the default tokens), including at clipped viewport edges. In standard themes, 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. +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. @@ -30,7 +30,7 @@ Automatic reveal includes the complete shoulder and rounds fractional layout bou 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. -`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, so Classic editor tabs, pill tabs, and Agents/session tab surfaces do not inherit connected-document colors. +`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. diff --git a/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts b/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts index bd8955b254357..364a06049289e 100644 --- a/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts +++ b/src/vs/workbench/contrib/modernUI/browser/connectedEditorTabs.ts @@ -6,10 +6,11 @@ import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; import { CONNECTED_EDITOR_TABS_SELECTOR } from '../../../browser/parts/editor/editor.js'; -import { EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND } from '../../../common/theme.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${CONNECTED_EDITOR_TABS_SELECTOR}`; +const connectedTabsSelector = `.monaco-workbench.modern-ui-tabs${CONNECTED_EDITOR_TABS_SELECTOR}`; +const connectedEditorTabsSelector = `${connectedTabsSelector}.modern-ui`; registerThemingParticipant((theme, collector) => { const background = theme.getColor(editorBackground); @@ -17,14 +18,24 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`${connectedTabsSelector} { --modern-ui-connected-tab-surface: ${background}; }`); } - const stripColor = theme.getColor(EDITOR_GROUP_HEADER_CONNECTED_TABS_BACKGROUND) ?? background; - const stripBackground = background && stripColor ? stripColor.makeOpaque(background) : stripColor; - if (stripBackground) { - collector.addRule(`${connectedTabsSelector} { --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(`${connectedTabsSelector} { --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 e4fe0fc03be60..7433d8ed1738d 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -7,7 +7,7 @@ * 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; @@ -28,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))); @@ -172,17 +151,13 @@ 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; box-shadow: none; } -.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:first-child:not(.connected-tab-upper-row) > .tab-fill, -.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.last-in-row + .tab.active:not(.connected-tab-upper-row) > .tab-fill { - inset-inline-start: calc(-1 * var(--vscode-strokeThickness)); -} - /* The foreground mask below removes the cap's side stroke where each shoulder curves out. */ .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active > .tab-fill::before, .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active > .tab-fill::after { @@ -508,13 +483,13 @@ inset-inline: 0; } -.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.connected-tab-top-row: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-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:not(:is(.hc-black, .hc-light)) .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:not(:is(.hc-black, .hc-light)) .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:not(:is(.hc-black, .hc-light)) .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.last-in-row + .tab > .tab-fill { +.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; 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 6b3a4e4ff6130..55250d5ffda6b 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 @@ -2028,6 +2028,38 @@ suite('ModernUIContribution', () => { ]); }); + 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())); @@ -2140,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'); @@ -2155,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}`); } @@ -2254,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]; @@ -2273,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}`); } } @@ -2297,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; --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); @@ -2337,22 +2369,19 @@ suite('ModernUIContribution', () => { group.classList.toggle('active', active); const highContrast = themeType.startsWith('hc-'); const border = highContrast ? (active ? 'rgb(255, 170, 0)' : 'rgb(136, 136, 136)') : 'rgb(37, 37, 38)'; - const frame = targetWindow.getComputedStyle(body, '::after'); - const breadcrumbFrame = targetWindow.getComputedStyle(breadcrumbs, '::after'); + 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, frame.borderLeftColor, 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', 'rgb(0, 0, 0)', '0px', 'auto'], }, `${themeType}, active group: ${active}`); } } @@ -2414,9 +2443,9 @@ suite('ModernUIContribution', () => { }, { tabBounds: tabBounds.toJSON(), labelBounds: labelBounds.toJSON(), - fillExpansion: connected && theme.startsWith('hc-') ? [0, 1, 0] : [0, 0, 0], + fillExpansion: [0, 0, 0], marginRight: connected ? shoulderWidth : '0px', - topRadius: connected ? (theme.startsWith('hc-') ? '5px' : '0px') : '4px', + topRadius: connected ? '0px' : '4px', }, JSON.stringify({ classes, theme, activeGroup, compact })); } } @@ -2498,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], @@ -2509,6 +2539,7 @@ suite('ModernUIContribution', () => { middleTab: { shoulders: ['""', '""'], capRadii: ['5px', '5px'], + backgroundClip: 'padding-box', radii: ['5px', '5px'], sizes: [['5px', '5px'], ['5px', '5px']], }, @@ -2565,7 +2596,7 @@ suite('ModernUIContribution', () => { }); }); - test('retains high contrast outlines when the outermost tab owns the frame', () => { + 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;'; @@ -2593,9 +2624,9 @@ suite('ModernUIContribution', () => { root.classList.remove(theme); } assert.deepStrictEqual(outlines, [ - { theme: 'hc-black', upperRow: false, top: 'rgb(255, 170, 0)', left: 'rgb(255, 170, 0)' }, + { 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: '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)' }, ]); }); 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 9f230d70933fc..229d7e39156dd 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -135,7 +135,7 @@ suite('MultiEditorTabsControl', () => { 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); @@ -606,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 => ({ @@ -618,6 +619,7 @@ suite('MultiEditorTabsControl', () => { fillDisplay: 'block', outlineDisplay: 'block', outlineColor: 'rgb(255, 170, 0)', + frameColor: 'rgb(255, 170, 0)', }))); }); From 55a54e6dc3559a0e9dab02b667a5b1f052812b36 Mon Sep 17 00:00:00 2001 From: Cherry Wang Date: Wed, 23 Sep 2026 10:48:36 -0700 Subject: [PATCH 29/29] Editor: ignore unrendered frame color in connected tab tests All four browser and Electron CI failures asserted black for an absent frame whose unused border color inherits currentColor. Assert the color only for the rendered HC frame, retaining geometry and frame-presence coverage. Seed a non-black foreground so focused runs reproduce the full-suite inheritance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../modernUI/test/browser/modernUI.contribution.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 55250d5ffda6b..75d71dbb4550c 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 @@ -2329,7 +2329,7 @@ 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-cornerRadius-large: 8px; --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 editor-tabs-multiple'); @@ -2375,13 +2375,13 @@ suite('ModernUIContribution', () => { cap: [targetWindow.getComputedStyle(fill).borderTopColor, targetWindow.getComputedStyle(fill).borderLeftColor], shoulder: targetWindow.getComputedStyle(fill, '::after').borderLeftColor, separator: targetWindow.getComputedStyle(row, '::after').backgroundColor, - frame: [frame.content, frame.borderLeftWidth, frame.borderBottomWidth, frame.borderTopWidth, frame.borderLeftColor, frame.borderRadius, frame.pointerEvents], + frame: [frame.content, frame.borderLeftWidth, frame.borderBottomWidth, frame.borderTopWidth, highContrast ? frame.borderLeftColor : undefined, frame.borderRadius, frame.pointerEvents], }, { geometry: baseline, cap: ['rgba(0, 0, 0, 0)', border], shoulder: border, separator: border, - frame: highContrast ? ['""', '1px', '1px', '1px', border, '8px', 'none'] : ['none', '0px', '0px', '0px', 'rgb(0, 0, 0)', '0px', 'auto'], + frame: highContrast ? ['""', '1px', '1px', '1px', border, '8px', 'none'] : ['none', '0px', '0px', '0px', undefined, '0px', 'auto'], }, `${themeType}, active group: ${active}`); } }