diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index e92a4cc2ee65ba..d078b793f18f1d 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -833,6 +833,7 @@ "--vscode-tab-activeForeground", "--vscode-tab-activeModifiedBorder", "--vscode-tab-border", + "--vscode-tab-divider", "--vscode-tab-dragAndDropBorder", "--vscode-tab-hoverBackground", "--vscode-tab-hoverBorder", @@ -1079,6 +1080,7 @@ "--tab-sizing-current-width", "--tab-sizing-fixed-min-width", "--tab-sizing-fixed-max-width", + "--editor-tab-border-indicator-display", "--editor-group-tab-height", "--editor-group-tabs-height", "--testMessageDecorationFontFamily", @@ -1120,11 +1122,19 @@ "--modern-ui-editor-tab-active-foreground", "--modern-ui-editor-tab-active-hover-background", "--modern-ui-editor-tab-border", + "--modern-ui-editor-tab-current-border-bottom", + "--modern-ui-editor-tab-current-border-top", + "--modern-ui-editor-tab-custom-action-active-background", + "--modern-ui-editor-tab-custom-action-active-hover-background", + "--modern-ui-editor-tab-custom-action-hover-background", "--modern-ui-editor-tab-custom-active-background", + "--modern-ui-editor-tab-custom-active-border-top", "--modern-ui-editor-tab-custom-active-hover-background", + "--modern-ui-editor-tab-custom-border", "--modern-ui-editor-tab-custom-hover-background", "--modern-ui-editor-tab-custom-inactive-background", "--modern-ui-editor-tab-custom-unfocused-active-background", + "--modern-ui-editor-tab-custom-unfocused-active-border-top", "--modern-ui-editor-tab-custom-unfocused-active-hover-background", "--modern-ui-editor-tab-custom-unfocused-hover-background", "--modern-ui-editor-tab-custom-unfocused-inactive-background", diff --git a/src/vs/sessions/test/browser/editorPart.test.ts b/src/vs/sessions/test/browser/editorPart.test.ts index 39e9085790f946..0de4ab788104ed 100644 --- a/src/vs/sessions/test/browser/editorPart.test.ts +++ b/src/vs/sessions/test/browser/editorPart.test.ts @@ -54,7 +54,7 @@ suite('Sessions - EditorPart', () => { const row = appendElement(title, 'tabs-and-actions-container'); const scrollable = appendElement(row, 'monaco-scrollable-element'); const tabs = appendElement(scrollable, 'tabs-container'); - const first = appendElement(tabs, 'tab active connected-tab-top-row'); + const first = appendElement(tabs, 'tab active connected-tab-top-row first-in-row'); const firstFill = appendElement(first, 'tab-fill'); const second = appendElement(tabs, 'tab connected-tab-top-row'); const secondFill = appendElement(second, 'tab-fill'); @@ -107,6 +107,7 @@ suite('Sessions - EditorPart', () => { row.classList.toggle('wrapping', wrapped); first.classList.toggle('last-in-row', wrapped); first.classList.toggle('connected-tab-upper-row', wrapped); + second.classList.toggle('first-in-row', wrapped); second.classList.toggle('connected-tab-top-row', !wrapped); const cap = mainWindow.getComputedStyle(secondFill); assert.deepStrictEqual({ diff --git a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index 72ad2119dd0399..1e11ae5f864b17 100644 --- a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -127,6 +127,19 @@ display: none; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-divider { + display: none; + position: absolute; + top: 50%; + left: 0; + z-index: 9; + width: var(--vscode-strokeThickness); + height: var(--vscode-spacing-size160); + background-color: var(--vscode-tab-divider, transparent); + pointer-events: none; + transform: translateY(-50%); +} + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container > .monaco-scrollable-element > .tab-connected-overflow-edge { display: none; } @@ -316,7 +329,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title:not(.two-tab-bars) .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, .monaco-workbench .part.editor > .content .editor-group-container > .title.two-tab-bars .tabs-and-actions-container:not(:first-child) .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { - display: block; + display: var(--editor-tab-border-indicator-display, block); position: absolute; left: 0; pointer-events: none; diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 55e31a16835591..c1089df6b9dec7 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -998,6 +998,8 @@ export class MultiEditorTabsControl extends EditorTabsControl { tabContainer.appendChild(tabFillContainer); const tabConnectedEdgeContainer = $('.tab-connected-edge', { 'aria-hidden': true }); tabContainer.appendChild(tabConnectedEdgeContainer); + const tabDivider = $('.tab-divider', { 'aria-hidden': true }); + tabContainer.appendChild(tabDivider); // Tab Border Top const tabBorderTopContainer = $('.tab-border-top-container'); @@ -2808,7 +2810,7 @@ registerThemingParticipant((theme, collector) => { if (tabHoverBorder) { collector.addRule(` .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover > .tab-border-bottom-container { - display: block; + display: var(--editor-tab-border-indicator-display, block); position: absolute; left: 0; pointer-events: none; @@ -2825,7 +2827,7 @@ registerThemingParticipant((theme, collector) => { if (tabUnfocusedHoverBorder) { collector.addRule(` .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-border-bottom-container { - display: block; + display: var(--editor-tab-border-indicator-display, block); position: absolute; left: 0; pointer-events: none; diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 91b466700b3df5..9634bef5f72739 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -105,7 +105,9 @@ export const TAB_BORDER = registerColor('tab.border', { light: '#F3F3F3', hcDark: contrastBorder, hcLight: contrastBorder, -}, localize('tabBorder', "Border to separate tabs from each other. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); +}, localize('tabBorder', "Border around tabs. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); + +export const TAB_DIVIDER = registerColor('tab.divider', TAB_BORDER, localize('tabDivider', "Color of dividers between editor tabs.")); export const TAB_LAST_PINNED_BORDER = registerColor('tab.lastPinnedBorder', { dark: treeIndentGuidesStroke, diff --git a/src/vs/workbench/contrib/modernUI/README.md b/src/vs/workbench/contrib/modernUI/README.md index 126fbf539910f6..c70938aefd2fd9 100644 --- a/src/vs/workbench/contrib/modernUI/README.md +++ b/src/vs/workbench/contrib/modernUI/README.md @@ -24,18 +24,20 @@ 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. 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 Inner radius plus the outside stroke (7px with the default tokens), including at clipped viewport edges. Each shoulder paints a complete curved stroke meeting the body separator; the edge mask removes only the cap's straight continuation behind that curve. Theme colors and explicit border customizations do not change these dimensions. 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 Inner radius on their free corners and align to the same left edge as the bottom row. Wrapped rows retain equal-height hit boxes, with spacing only between rows and one shared separator slot below the final row. Hover does not change their geometry. Close-action clearance is derived from the existing tab height and row gutters, independently of the shoulder radius. The same clearance reserves label space and positions the 20px hover target. Painted borders are excluded from the available content area: a wrapped connected row centers its action below the cap's top stroke, while a single standard row uses the shared frame. Stroke clearance is reserved in inactive tabs too, so selecting a tab never moves its close target. Separate pinned-row pills add their horizontal fill inset to the reservation. Left and right actions use the same centered action surface. High-contrast hover and focus indicators belong to the close target, not a second border around its action container. -Wrapped tabs reserve the same action space before and after row classification; upper-row markers and row-end markers must not trigger another fit-tab reflow. Selection must not change a tab's width or margin. Shoulder space is reserved only after the final tab, independently of selection; intermediate shoulders paint over adjacent tab surfaces without inserting a gap. +Wrapped tabs reserve the upper pill's action space before row classification; assigning upper-row and row-end markers must not change fit-tab widths or trigger another reflow. Close targets share the same trailing clearance on upper and bottom rows, including at compact density. 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. -By default, the connected design uses `editor.background` for the active tab so the selected tab and editor body read as one document well. The strip and inactive tabs use `editorGroupHeader.connectedTabsBackground`, and their hover fill is derived from `foreground` over that background. Explicit tab fill and label color customizations from `tab.*`, `modernTab.*`, and `modernEditorTab.*` override those defaults for connected and pill tabs alike; editor-specific Modern UI colors take precedence over shared Modern UI colors, which take precedence over legacy colors. Connected action surfaces and frame colors retain their connected-specific treatment: the active tab's action container remains transparent, while 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 fill or label color precedence. +By default, the connected design uses `editor.background` for the active tab so the selected tab and editor body read as one document well. The strip and inactive tabs use `editorGroupHeader.connectedTabsBackground`, and their hover fill is derived from `foreground` over that background. Explicit tab fill, label, and action color customizations from `tab.*`, `modernTab.*`, and `modernEditorTab.*` override those defaults for connected and pill tabs alike; editor-specific Modern UI colors take precedence over shared Modern UI colors, which take precedence over legacy colors. The active Connected tab's action container remains transparent unless a corresponding `modernEditorTab.*ActionBackground` color is explicitly customized. 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 color precedence. Agents chat tabs use the same connected shape, strip colors, and theme-aware stroke as side-panel tabs, with the selected tab joining the active or inactive session's background instead of `editor.background`. Overflowing chat tabs expose a thin, draggable horizontal scrollbar on hover without increasing the tab-row height. Tab selection, close actions, status indicators, inline renaming, drag and drop, and keyboard navigation retain their existing behavior. +An explicitly customized `tab.selectedBorderTop` remains the active tab's fallback when the theme has no active top-border color for that group's focus state. It does not override an existing active top-border color or enable the theme's uncustomized top accent. Explicit hover-action backgrounds apply equally to bottom connected tabs, upper wrapped pills, and separate pinned rows. + `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, including Agents-window tabs, use `editorGroupHeader.tabsBackground` outside Modern UI; only Modern UI editor tabs adopt the dedicated connected-strip palette. 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. @@ -188,7 +190,7 @@ Resize grip dots mark only boundaries between top-level parts, not modal editor Activity bar items in non-default top or bottom positions use the `modernTab.*` colors because they share the pane tab presentation. -Editor tab fill and label colors have the same precedence in the `connected` and `pill` styles. Existing `tab.*` fill and label customizations remain effective unless the corresponding `modernTab.*` or `modernEditorTab.*` color is explicitly customized. Connected action backgrounds and frame borders continue to use the connected treatment described above. +Editor tab fill, label, and action colors have the same precedence in the `connected` and `pill` styles. Existing `tab.*` fill and label customizations remain effective unless the corresponding `modernTab.*` or `modernEditorTab.*` color is explicitly customized. Connected action backgrounds retain their seamless defaults until an explicit editor action color overrides them; frame borders continue to use the connected treatment described above. `statusBar.inactiveBackground` only changes the resting background in an inactive window with a workspace or folder open. Debugging and other status bar background overrides take precedence. Empty windows continue to use `statusBar.noFolderBackground`. Leaving the inactive color unset preserves the existing status bar behavior. diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index cea416ec6d5741..c10a79648b9c4f 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -16,7 +16,8 @@ .modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container, .modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .modern-ui-editor-tab-group { - --modern-ui-connected-tab-border: var(--vscode-editorGroupHeader-tabsBorder, var(--vscode-tab-border, var(--modern-ui-connected-tab-surface))); + --editor-tab-border-indicator-display: block; + --modern-ui-connected-tab-border: var(--modern-ui-editor-tab-custom-border, var(--vscode-editorGroupHeader-tabsBorder, var(--vscode-tab-border, var(--modern-ui-connected-tab-surface)))); --modern-ui-connected-well-border: transparent; } @@ -65,6 +66,18 @@ background-color: var(--modern-ui-connected-tab-surface); } +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:not(.last-in-row) + .tab > .tab-divider { + display: none; +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:not(.active):not(.last-in-row) + .tab:not(.active) > .tab-divider { + display: block; +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:not(.active):not(.last-in-row) + .tab:not(.active) > .tab-divider { + display: none; +} + .modern-ui-tabs.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))); @@ -84,8 +97,8 @@ --modern-ui-editor-tab-unfocused-inactive-background: var(--modern-ui-editor-tab-custom-unfocused-inactive-background, var(--modern-ui-connected-tab-strip-background, var(--vscode-editorGroupHeader-tabsBackground))); --modern-ui-editor-tab-hover-background: var(--modern-ui-editor-tab-custom-hover-background, color-mix(in srgb, var(--vscode-foreground) 6%, var(--modern-ui-connected-tab-strip-background, var(--vscode-editorGroupHeader-tabsBackground)))); --modern-ui-editor-tab-unfocused-hover-background: var(--modern-ui-editor-tab-custom-unfocused-hover-background, color-mix(in srgb, var(--vscode-foreground) 6%, var(--modern-ui-connected-tab-strip-background, var(--vscode-editorGroupHeader-tabsBackground)))); - --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-unfocused-hover-background); + --modern-ui-editor-tab-action-hover-background: var(--modern-ui-editor-tab-custom-action-hover-background, var(--modern-ui-editor-tab-hover-background)); + --modern-ui-editor-tab-action-unfocused-hover-background: var(--modern-ui-editor-tab-custom-action-hover-background, var(--modern-ui-editor-tab-unfocused-hover-background)); --modern-ui-connected-tab-action-background: var(--modern-ui-editor-tab-inactive-background); --modern-ui-connected-tab-radius: var(--vscode-cornerRadius-medium, var(--vscode-cornerRadius-small)); --modern-ui-connected-tab-cap-radius: calc(var(--modern-ui-connected-tab-radius) + var(--vscode-strokeThickness)); @@ -149,6 +162,20 @@ bottom: calc(-1 * var(--vscode-strokeThickness)); } +/* In connected tabs the active cap owns the structural tab boundary. Inactive + * tabs use the dedicated divider instead of repeating that boundary around each fill. */ +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench:not(:is(.hc-black, .hc-light)) .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:not(.active):not(.connected-tab-upper-row) > .tab-fill { + border-color: transparent; +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:hover:not(.selected):not(.active):not(.connected-tab-upper-row) > .tab-border-bottom-container { + bottom: calc(-1 * var(--vscode-strokeThickness)); +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row:hover:not(.selected):not(.active) > .tab-border-bottom-container { + display: none; +} + .modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .sticky-tabs-background, .modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tabs-bar-add-tab { background-color: var(--modern-ui-connected-tab-strip-background, var(--vscode-editorGroupHeader-tabsBackground)); @@ -188,13 +215,13 @@ .modern-ui-tabs.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)); + /* Reserve upper-pill clearance before row classification so assigning rows cannot rewrap tabs. */ + --modern-ui-tab-action-padding: calc(var(--vscode-spacing-size200) + 2 * var(--modern-ui-connected-tab-action-inset) + var(--vscode-spacing-size20)); row-gap: var(--vscode-spacing-size20); } /* Preserve the existing explicit selection and focus borders in high contrast. */ .modern-ui-tabs.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-tabs.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, .modern-ui-tabs.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:not(.close-action-off) > .tab-actions, .modern-ui-tabs.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:not(.close-action-off) > .tab-actions { border-color: transparent; @@ -218,7 +245,63 @@ box-shadow: none; } -/* The foreground mask below removes the cap's side stroke where each shoulder curves out. */ +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench.floating-panels .monaco-grid-view .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active:not(.connected-tab-upper-row) > .tab-fill, +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench.floating-panels .monaco-grid-view .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active:not(.connected-tab-upper-row) > .tab-connected-edge { + bottom: calc(-3 * var(--vscode-strokeThickness)); +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench.floating-panels .monaco-grid-view .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active:not(.connected-tab-upper-row) > .tab-fill, +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench.floating-panels .monaco-grid-view .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active:not(.connected-tab-upper-row) > .tab-connected-edge, +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench.floating-panels .monaco-grid-view .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active:not(.connected-tab-upper-row) > .tab-border-top-container { + transform: translateY(calc(-1 * var(--vscode-strokeThickness))); +} + +/* Keep one structural color around the rounded cap. The explicit active-top + * accent is inset so it cannot split or square either corner. */ +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active.connected-tab-upper-row.tab-border-top > .tab-fill { + border-top-color: var(--modern-ui-editor-tab-custom-active-border-top, var(--modern-ui-connected-tab-border)); +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container:not(.active) > .title.tabs .tabs-container > .tab.active.connected-tab-upper-row.tab-border-top > .tab-fill { + border-top-color: var(--modern-ui-editor-tab-custom-unfocused-active-border-top, var(--modern-ui-connected-tab-border)); +} + +.modern-ui-tabs.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 { + border-color: var(--modern-ui-connected-tab-border); + border-bottom-color: transparent; +} + +.modern-ui-tabs.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-border-top > .tab-border-top-container { + display: block; + top: 0; + left: var(--modern-ui-connected-tab-cap-radius); + width: calc(100% - 2 * var(--modern-ui-connected-tab-cap-radius)); + height: var(--vscode-strokeThickness); + background-color: var(--modern-ui-editor-tab-custom-active-border-top, transparent); +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container:not(.active) > .title.tabs .tabs-container > .tab.active:not(.connected-tab-upper-row).tab-border-top > .tab-border-top-container { + background-color: var(--modern-ui-editor-tab-custom-unfocused-active-border-top, transparent); +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active.connected-tab-upper-row.tab-border-top > .tab-border-top-container { + display: none; +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container { + bottom: calc(-1 * var(--vscode-strokeThickness)); +} + +/* Non-active selection remains an accent rather than another connected cap. + * Match the inset modified indicator so the stroke stops before either corner. */ +.modern-ui-tabs.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-border-top-container { + left: var(--vscode-spacing-size20); + width: calc(100% - var(--vscode-spacing-size40)); + height: var(--vscode-spacing-size20); + border-radius: var(--vscode-cornerRadius-small) var(--vscode-cornerRadius-small) 0 0; +} + +/* The foreground mask below removes the cap's straight side stroke behind each curved shoulder. */ .modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active > .tab-fill::before, .modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active > .tab-fill::after, .modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .modern-ui-editor-tab.active > .modern-ui-editor-tab-fill::before, @@ -242,11 +325,6 @@ box-shadow: calc(var(--modern-ui-connected-tab-shoulder-radius) / 2) calc(var(--modern-ui-connected-tab-shoulder-radius) / 2) 0 calc(var(--modern-ui-connected-tab-shoulder-radius) / 2) var(--modern-ui-connected-tab-current-background); } -.modern-ui-tabs.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 > .tab-fill::before, -.modern-ui-tabs.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 > .tab-fill::after { - border-bottom-color: transparent; -} - .modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active:first-child > .tab-fill::before, .modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.last-in-row + .tab.active > .tab-fill::before { content: none; @@ -364,10 +442,10 @@ .modern-ui-tabs.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: 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; + --modern-ui-editor-tab-action-active-background: var(--modern-ui-editor-tab-custom-action-active-background, transparent); + --modern-ui-editor-tab-action-unfocused-active-background: var(--modern-ui-editor-tab-custom-action-active-background, transparent); + --modern-ui-editor-tab-action-active-hover-background: var(--modern-ui-editor-tab-custom-action-active-hover-background, transparent); + --modern-ui-editor-tab-action-unfocused-active-hover-background: var(--modern-ui-editor-tab-custom-action-active-hover-background, transparent); border-radius: 0 var(--modern-ui-connected-tab-radius) 0 0; } @@ -401,11 +479,11 @@ } /* Reserve the connected stroke in every state so selecting a tab only changes its paint. */ -.modern-ui-tabs.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 { +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container .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-tabs.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 { +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container .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)); } @@ -433,7 +511,7 @@ } .modern-ui-tabs.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)); + left: calc(var(--modern-ui-connected-tab-action-inset) - var(--vscode-spacing-size20)); } /* Keep the complete identity and action column; only the basename can ellipsize. */ @@ -568,8 +646,11 @@ .modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row { --modern-ui-editor-tab-hover-background: var(--modern-ui-editor-tab-custom-hover-background, var(--modern-ui-connected-tab-upper-hover-background)); --modern-ui-editor-tab-unfocused-hover-background: var(--modern-ui-editor-tab-custom-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-unfocused-hover-background); + --modern-ui-editor-tab-action-hover-background: var(--modern-ui-editor-tab-custom-action-hover-background, var(--modern-ui-editor-tab-hover-background)); + --modern-ui-editor-tab-action-unfocused-hover-background: var(--modern-ui-editor-tab-custom-action-hover-background, var(--modern-ui-editor-tab-unfocused-hover-background)); +} + +.modern-ui-tabs.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-tab-action-padding: calc(var(--vscode-spacing-size200) + 2 * var(--modern-ui-connected-tab-action-inset) + var(--vscode-spacing-size20)); } @@ -593,7 +674,7 @@ right: var(--vscode-spacing-size20); } -.modern-ui-tabs.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 { +.modern-ui-tabs.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):not(.tab-border-top) > .tab-fill { border-top-color: transparent; } @@ -613,6 +694,11 @@ border-left-color: transparent; } +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .monaco-grid-view .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:first-child:not(.active):not(.selected):not(:focus):not(:hover) > .tab-fill { + border-left-width: 0; + border-left-color: transparent; +} + .modern-ui-tabs.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); } @@ -630,6 +716,38 @@ content: none; } +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active.connected-tab-upper-row.tab-border-bottom > .tab-border-bottom-container { + display: none; +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active.connected-tab-upper-row.tab-border-bottom > .tab-fill::after, +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row:hover:not(.selected):not(.active) > .tab-fill::after { + content: ''; + position: absolute; + right: var(--vscode-cornerRadius-small); + bottom: calc(-1 * var(--vscode-strokeThickness)); + left: var(--vscode-cornerRadius-small); + width: auto; + height: var(--vscode-strokeThickness); + border: 0; + border-radius: 0; + box-shadow: none; + clip-path: none; + pointer-events: none; +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active.connected-tab-upper-row.tab-border-bottom > .tab-fill::after { + background-color: var(--tab-border-bottom-color); +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container.active > .title.tabs .tabs-container > .tab.connected-tab-upper-row:hover:not(.selected):not(.active) > .tab-fill::after { + background-color: var(--modern-ui-editor-tab-hover-border); +} + +.modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container:not(.active) > .title.tabs .tabs-container > .tab.connected-tab-upper-row:hover:not(.selected):not(.active) > .tab-fill::after { + background-color: var(--modern-ui-editor-tab-unfocused-hover-border); +} + .modern-ui-tabs.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row > .tab-connected-edge { display: none; } diff --git a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css index cb4768e68bb005..5e45afbaadccd3 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css @@ -47,6 +47,7 @@ --modern-ui-editor-tab-action-active-hover-background: var(--vscode-modernEditorTab-activeHoverActionBackground); --modern-ui-editor-tab-action-unfocused-active-hover-background: var(--modern-ui-editor-tab-action-active-hover-background); --modern-ui-editor-tabs-border: transparent; + --editor-tab-border-indicator-display: none; } /* Keep connected labels readable without overriding explicit legacy color customizations. */ @@ -75,7 +76,6 @@ font-weight: var(--vscode-fontWeight-regular); box-shadow: none !important; padding: 0 var(--vscode-spacing-size80) 0 var(--vscode-spacing-size60) !important; - --tab-border-top-color: transparent !important; } .modern-ui-tabs.monaco-workbench:not(.file-icons-enabled) .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab:not(.sticky-compact):not(.tab-actions-left), @@ -109,14 +109,12 @@ z-index: 0; border-radius: var(--vscode-cornerRadius-small); box-sizing: border-box; - box-shadow: inset calc(-1 * var(--vscode-strokeThickness)) 0 var(--modern-ui-editor-tab-border); + border: var(--vscode-strokeThickness) solid var(--modern-ui-editor-tab-border); pointer-events: none; } .modern-ui-tabs.monaco-workbench .part.editor .tabs-container > .tab.sticky + .tab:not(.sticky) > .tab-fill { - box-shadow: - inset calc(-1 * var(--vscode-strokeThickness)) 0 var(--modern-ui-editor-tab-border), - inset var(--vscode-strokeThickness) 0 var(--modern-ui-editor-tab-last-pinned-border); + box-shadow: inset var(--vscode-strokeThickness) 0 var(--modern-ui-editor-tab-last-pinned-border); } /* Reuses the last-pinned-border color for the horizontal divider between the pinned and normal tab rows. */ @@ -277,8 +275,12 @@ border-block-width: var(--vscode-spacing-size20) var(--vscode-spacing-size60); } -.modern-ui-tabs .part.editor .tabs-container > .tab .tab-border-bottom-container { - display: none !important; +.modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.last-in-row) + .tab > .tab-divider { + display: block; +} + +.modern-ui-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.last-in-row) + .tab > .tab-divider { + display: none; } /* Tab fills retain the theme's transparency. The action overlay uses a separately flattened @@ -311,18 +313,48 @@ .modern-ui-tabs.monaco-workbench:not(:is(.hc-black, .hc-light)) .part.editor .editor-group-container.active .tabs-container > .tab.active > .tab-fill, .modern-ui-tabs.monaco-workbench:not(:is(.hc-black, .hc-light)) .modern-ui-editor-tab-group.modern-ui-editor-tab-group-active .modern-ui-editor-tab.active > .modern-ui-editor-tab-fill { + --modern-ui-editor-tab-current-border-top: var(--modern-ui-editor-tab-active-border-top); + --modern-ui-editor-tab-current-border-bottom: var(--modern-ui-editor-tab-active-border); background-color: var(--modern-ui-editor-tab-active-background); - border-top: var(--vscode-strokeThickness) solid var(--modern-ui-editor-tab-active-border-top); - border-bottom: var(--vscode-strokeThickness) solid var(--modern-ui-editor-tab-active-border); } .modern-ui-tabs.monaco-workbench:not(:is(.hc-black, .hc-light)) .part.editor .editor-group-container:not(.active) .tabs-container > .tab.active > .tab-fill, .modern-ui-tabs.monaco-workbench:not(:is(.hc-black, .hc-light)) .modern-ui-editor-tab-group:not(.modern-ui-editor-tab-group-active) .modern-ui-editor-tab.active > .modern-ui-editor-tab-fill { + --modern-ui-editor-tab-current-border-top: var(--modern-ui-editor-tab-unfocused-active-border-top); + --modern-ui-editor-tab-current-border-bottom: var(--modern-ui-editor-tab-unfocused-active-border); background-color: var(--modern-ui-editor-tab-unfocused-active-background); +} + +.modern-ui-tabs.monaco-workbench:not(:is(.hc-black, .hc-light)) .modern-ui-editor-tab-group.modern-ui-editor-tab-group-active .modern-ui-editor-tab.active > .modern-ui-editor-tab-fill { + border-top: var(--vscode-strokeThickness) solid var(--modern-ui-editor-tab-active-border-top); + border-bottom: var(--vscode-strokeThickness) solid var(--modern-ui-editor-tab-active-border); +} + +.modern-ui-tabs.monaco-workbench:not(:is(.hc-black, .hc-light)) .modern-ui-editor-tab-group:not(.modern-ui-editor-tab-group-active) .modern-ui-editor-tab.active > .modern-ui-editor-tab-fill { border-top: var(--vscode-strokeThickness) solid var(--modern-ui-editor-tab-unfocused-active-border-top); border-bottom: var(--vscode-strokeThickness) solid var(--modern-ui-editor-tab-unfocused-active-border); } +.modern-ui-tabs.monaco-workbench:not(.modern-ui-connected-editor-tabs):not(:is(.hc-black, .hc-light)) .part.editor .tabs-container > .tab.active:not(.multi-selected) > .tab-fill::before, +.modern-ui-tabs.monaco-workbench:not(.modern-ui-connected-editor-tabs):not(:is(.hc-black, .hc-light)) .part.editor .tabs-container > .tab.active:not(.multi-selected) > .tab-fill::after { + content: ''; + position: absolute; + right: var(--vscode-cornerRadius-small); + left: var(--vscode-cornerRadius-small); + height: var(--vscode-strokeThickness); + pointer-events: none; +} + +.modern-ui-tabs.monaco-workbench:not(.modern-ui-connected-editor-tabs):not(:is(.hc-black, .hc-light)) .part.editor .tabs-container > .tab.active:not(.multi-selected) > .tab-fill::before { + top: calc(-1 * var(--vscode-strokeThickness)); + background-color: var(--modern-ui-editor-tab-current-border-top); +} + +.modern-ui-tabs.monaco-workbench:not(.modern-ui-connected-editor-tabs):not(:is(.hc-black, .hc-light)) .part.editor .tabs-container > .tab.active:not(.multi-selected) > .tab-fill::after { + bottom: calc(-1 * var(--vscode-strokeThickness)); + background-color: var(--modern-ui-editor-tab-current-border-bottom); +} + .modern-ui-tabs.monaco-workbench:not(:is(.hc-black, .hc-light)) .part.editor .editor-group-container.active .tabs-container > .tab.active:hover > .tab-fill, .modern-ui-tabs.monaco-workbench:not(:is(.hc-black, .hc-light)) .modern-ui-editor-tab-group.modern-ui-editor-tab-group-active .modern-ui-editor-tab.active:hover > .modern-ui-editor-tab-fill { background-color: var(--modern-ui-editor-tab-active-hover-background); @@ -603,10 +635,6 @@ z-index: 11; } -.modern-ui-tabs .part.editor .tabs-container > .tab .tab-border-top-container { - display: none !important; -} - .modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.dirty-border-top > .tab-border-top-container { display: block !important; position: absolute; @@ -822,6 +850,10 @@ outline: none !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:not(.active):not(.selected):not(:focus):not(:hover) > .tab-fill { + border-color: var(--vscode-tab-border); +} + :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):not(:focus) > .tab-fill, :is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench.modern-ui-connected-editor-tabs .part.editor > .content .editor-group-container > .title .tabs-and-actions-container .tabs-container > .tab.selected:not(.active):not(:focus) > .tab-fill, :is(.hc-black, .hc-light).modern-ui-tabs.monaco-workbench:not(.modern-ui-connected-editor-tabs) .modern-ui-editor-tab.active:not(:focus) > .modern-ui-editor-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 26fd078bac01d2..8862fdabef1b92 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_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_ACTIVE_BACKGROUND, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_INACTIVE_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_DIVIDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_HOVER_FOREGROUND, TAB_INACTIVE_BACKGROUND, TAB_INACTIVE_FOREGROUND, TAB_LAST_PINNED_BORDER, TAB_SELECTED_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_INACTIVE_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'; @@ -2333,6 +2333,119 @@ suite('ModernUIContribution', () => { }); }); + test('uses legacy border customizations for connected tabs', () => { + const root = appendElement(document.body, 'monaco-workbench modern-ui modern-ui-tabs modern-ui-connected-editor-tabs'); + store.add(toDisposable(() => root.remove())); + root.style.setProperty('--modern-ui-editor-tab-custom-active-border-top', '#123456'); + root.style.setProperty('--modern-ui-editor-tab-custom-unfocused-active-border-top', '#234567'); + const content = appendElement(appendElement(root, 'part editor'), 'content'); + const createGroup = (active: boolean) => { + const group = appendElement(content, `editor-group-container${active ? ' active' : ''}`); + const title = appendElement(group, 'title tabs'); + const tabs = appendElement(appendElement(title, 'tabs-and-actions-container'), 'tabs-container'); + const activeTab = appendElement(tabs, 'tab active tab-border-top tab-border-bottom'); + activeTab.style.setProperty('--tab-border-top-color', active ? '#123456' : '#234567'); + activeTab.style.setProperty('--tab-border-bottom-color', active ? '#345678' : '#456789'); + const activeFill = appendElement(activeTab, 'tab-fill'); + const activeTopBorder = appendElement(activeTab, 'tab-border-top-container'); + const activeBottomBorder = appendElement(activeTab, 'tab-border-bottom-container'); + const selectedTab = appendElement(tabs, 'tab selected tab-border-top'); + selectedTab.style.setProperty('--tab-border-top-color', '#56789a'); + appendElement(selectedTab, 'tab-fill'); + const selectedTopBorder = appendElement(selectedTab, 'tab-border-top-container'); + return { activeFill, activeTopBorder, activeBottomBorder, selectedTopBorder }; + }; + const activeGroup = createGroup(true); + const unfocusedGroup = createGroup(false); + const targetWindow = getWindow(root); + const borderStyles = (group: ReturnType) => { + const activeTop = targetWindow.getComputedStyle(group.activeTopBorder); + const activeBottom = targetWindow.getComputedStyle(group.activeBottomBorder); + const selectedTop = targetWindow.getComputedStyle(group.selectedTopBorder); + return { + activeTop: [activeTop.display, targetWindow.getComputedStyle(group.activeFill).borderTopColor, activeTop.backgroundColor], + activeBottom: [activeBottom.display, activeBottom.backgroundColor], + selectedTop: [selectedTop.display, selectedTop.backgroundColor], + }; + }; + + assert.deepStrictEqual({ + activeGroup: borderStyles(activeGroup), + unfocusedGroup: borderStyles(unfocusedGroup), + }, { + activeGroup: { + activeTop: ['block', 'rgb(0, 0, 0)', 'rgb(18, 52, 86)'], + activeBottom: ['block', 'rgb(52, 86, 120)'], + selectedTop: ['block', 'rgb(86, 120, 154)'], + }, + unfocusedGroup: { + activeTop: ['block', 'rgb(0, 0, 0)', 'rgb(35, 69, 103)'], + activeBottom: ['block', 'rgb(69, 103, 137)'], + selectedTop: ['block', 'rgb(86, 120, 154)'], + }, + }); + }); + + test('uses explicitly customized selected borders only when the active top border is absent', () => { + const root = appendElement(document.body, 'selected-border-fallback monaco-workbench modern-ui-tabs modern-ui-connected-editor-tabs'); + store.add(toDisposable(() => root.remove())); + const style = document.createElement('style'); + root.appendChild(style); + const content = appendElement(appendElement(root, 'part editor'), 'content'); + const indicators = [true, false].map(active => { + const group = appendElement(content, `editor-group-container${active ? ' active' : ''}`); + const title = appendElement(group, 'title tabs'); + const tabs = appendElement(appendElement(title, 'tabs-and-actions-container'), 'tabs-container'); + const tab = appendElement(tabs, 'tab active selected multi-selected tab-border-top'); + appendElement(tab, 'tab-fill'); + return appendElement(tab, 'tab-border-top-container'); + }); + const cases: { colors: Record; customizations: Record; expected: string[] }[] = [ + { colors: {}, customizations: {}, expected: ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0)'] }, + { colors: {}, customizations: { 'tab.selectedBorderTop': '#a3e635' }, expected: ['rgb(163, 230, 53)', 'rgb(163, 230, 53)'] }, + { colors: { 'tab.activeBorderTop': '#3994bc' }, customizations: { 'tab.selectedBorderTop': '#a3e635' }, expected: ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0)'] }, + { colors: {}, customizations: { 'tab.selectedBorderTop': '#a3e635', 'tab.activeBorderTop': '#22d3ee', 'tab.unfocusedActiveBorderTop': '#c084fc' }, expected: ['rgb(34, 211, 238)', 'rgb(192, 132, 252)'] }, + { colors: {}, customizations: { 'tab.selectedBorderTop': '#a3e635', 'tab.activeBorderTop': '#00000000' }, expected: ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0)'] }, + ]; + const actual = cases.map(({ colors, customizations }) => { + const theme = ColorThemeData.createUnloadedTheme('vs-dark', colors); + theme.setCustomColors(customizations); + style.textContent = generateColorThemeCSS(theme, '.selected-border-fallback', themingRegistry.getThemingParticipants(), TestEnvironmentService).code; + return indicators.map(indicator => getWindow(indicator).getComputedStyle(indicator).backgroundColor); + }); + assert.deepStrictEqual(actual, cases.map(testCase => testCase.expected)); + }); + + test('preserves hover action customization on connected wrapped and separate pinned rows', () => { + const root = appendElement(document.body, 'upper-action-customization monaco-workbench modern-ui-tabs modern-ui-connected-editor-tabs'); + store.add(toDisposable(() => root.remove())); + const style = document.createElement('style'); + root.appendChild(style); + const content = appendElement(appendElement(root, 'part editor'), 'content'); + const probes = [true, false].flatMap(active => { + const group = appendElement(content, `editor-group-container${active ? ' active' : ''}`); + return ['single', 'wrapped', 'pinned'].map(layout => { + const title = appendElement(group, `title tabs${layout === 'pinned' ? ' two-tab-bars' : ''}`); + const row = appendElement(title, `tabs-and-actions-container${layout === 'wrapped' ? ' wrapping' : ''}`); + const tabs = appendElement(row, 'tabs-container'); + const tab = appendElement(tabs, `tab${layout === 'single' ? '' : ' connected-tab-upper-row'}`); + const probe = appendElement(tab, 'action-hover-probe'); + probe.style.backgroundColor = `var(--modern-ui-editor-tab-action-${active ? '' : 'unfocused-'}hover-background)`; + return probe; + }); + }); + const actual = [false, true].map(customizeAction => { + const theme = ColorThemeData.createUnloadedTheme('vs-dark'); + theme.setCustomColors({ + [MODERN_EDITOR_TAB_HOVER_BACKGROUND]: '#7c2d12', + ...(customizeAction ? { [MODERN_EDITOR_TAB_HOVER_ACTION_BACKGROUND]: '#5a1f0c' } : {}), + }); + style.textContent = generateColorThemeCSS(theme, '.upper-action-customization', themingRegistry.getThemingParticipants(), TestEnvironmentService).code; + return probes.map(probe => getWindow(probe).getComputedStyle(probe).backgroundColor); + }); + assert.deepStrictEqual(actual, [Array(6).fill('rgb(124, 45, 18)'), Array(6).fill('rgb(90, 31, 12)')]); + }); + 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'; @@ -2709,7 +2822,7 @@ suite('ModernUIContribution', () => { const title = appendElement(group, 'title tabs'); const row = appendElement(title, 'tabs-and-actions-container'); const tabs = appendElement(row, 'tabs-container'); - const firstFill = appendElement(appendElement(tabs, 'tab'), 'tab-fill'); + const firstFill = appendElement(appendElement(tabs, 'tab first-in-row'), 'tab-fill'); const tab = appendElement(tabs, 'tab active connected-tab-top-row'); tab.style.width = '120px'; const fill = appendElement(tab, 'tab-fill'); @@ -2747,7 +2860,10 @@ suite('ModernUIContribution', () => { geometry: geometry(), firstBorderLeftWidth: targetWindow.getComputedStyle(firstFill).borderLeftWidth, cap: [targetWindow.getComputedStyle(fill).borderTopColor, targetWindow.getComputedStyle(fill).borderLeftColor], - shoulder: targetWindow.getComputedStyle(fill, '::after').borderLeftColor, + shoulders: [ + [targetWindow.getComputedStyle(fill, '::before').borderRightColor, targetWindow.getComputedStyle(fill, '::before').borderBottomColor], + [targetWindow.getComputedStyle(fill, '::after').borderLeftColor, targetWindow.getComputedStyle(fill, '::after').borderBottomColor], + ], separator: targetWindow.getComputedStyle(row, '::after').backgroundColor, frame: [frame.content, frame.borderLeftWidth, frame.borderBottomWidth, frame.borderTopWidth, highContrast ? frame.borderLeftColor : undefined, frame.borderRadius, frame.pointerEvents], headerBackground: targetWindow.getComputedStyle(header).backgroundColor, @@ -2755,7 +2871,7 @@ suite('ModernUIContribution', () => { geometry: baseline, firstBorderLeftWidth: '0px', cap: ['rgba(0, 0, 0, 0)', border], - shoulder: border, + shoulders: [[border, border], [border, border]], separator: border, frame: highContrast ? ['""', '1px', '1px', '1px', border, '8px', 'none'] : ['none', '0px', '0px', '0px', undefined, '0px', 'auto'], headerBackground: 'rgb(51, 51, 51)', @@ -2818,7 +2934,7 @@ suite('ModernUIContribution', () => { 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'); + const tab = appendElement(tabs, 'tab first-in-row'); tab.style.position = 'relative'; tab.style.width = '120px'; const fill = appendElement(tab, 'tab-fill'); @@ -2921,7 +3037,7 @@ suite('ModernUIContribution', () => { const group = appendElement(content, 'editor-group-container active'); const title = appendElement(group, 'title tabs'); const tabs = appendElement(title, 'tabs-container'); - const firstTab = appendElement(tabs, 'tab active'); + const firstTab = appendElement(tabs, 'tab active first-in-row'); const firstFill = appendElement(firstTab, 'tab-fill'); const middleTab = appendElement(tabs, 'tab'); const middleFill = appendElement(middleTab, 'tab-fill'); @@ -2966,26 +3082,28 @@ suite('ModernUIContribution', () => { 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-spacing-size200', '20px'); root.style.setProperty('--vscode-cornerRadius-small', '4px'); root.style.setProperty('--vscode-cornerRadius-medium', '6px'); root.style.setProperty('--vscode-strokeThickness', '1px'); document.body.appendChild(root); store.add(toDisposable(() => root.remove())); - const editor = appendElement(root, 'part editor'); + const gridView = appendElement(root, 'monaco-grid-view'); + const editor = appendElement(gridView, 'part editor'); const content = appendElement(editor, 'content'); const group = appendElement(content, 'editor-group-container active'); - const singleTitle = appendElement(group, 'title tabs'); + const singleTitle = appendElement(group, 'title tabs connected-tabs-labels'); const singleRow = appendElement(singleTitle, 'tabs-and-actions-container'); const singleTabs = appendElement(singleRow, 'tabs-container'); const inactiveSingleTab = appendElement(singleTabs, 'tab'); const inactiveSingleFill = appendElement(inactiveSingleTab, 'tab-fill'); const activeSingleTab = appendElement(singleTabs, 'tab active'); const activeSingleFill = appendElement(activeSingleTab, 'tab-fill'); - const title = appendElement(group, 'title tabs'); + const title = appendElement(group, 'title tabs connected-tabs-labels'); const row = appendElement(title, 'tabs-and-actions-container wrapping'); const tabs = appendElement(row, 'tabs-container'); - const upperTab = appendElement(tabs, 'tab active connected-tab-upper-row connected-tab-top-row'); + const upperTab = appendElement(tabs, 'tab active connected-tab-upper-row connected-tab-top-row first-in-row'); const upperFill = appendElement(upperTab, 'tab-fill'); const inactiveBottomTab = appendElement(tabs, 'tab'); const inactiveBottomFill = appendElement(inactiveBottomTab, 'tab-fill'); @@ -3004,6 +3122,7 @@ suite('ModernUIContribution', () => { assert.deepStrictEqual({ rowPaddingTop: targetWindow.getComputedStyle(row).paddingTop, + actionReservationMatches: targetWindow.getComputedStyle(upperTab).paddingRight === targetWindow.getComputedStyle(activeBottomTab).paddingRight, separatorSlots: [targetWindow.getComputedStyle(singleTabs).paddingBottom, targetWindow.getComputedStyle(tabs).paddingBottom], separatorOffsets: [targetWindow.getComputedStyle(singleRow, '::after').bottom, targetWindow.getComputedStyle(row, '::after').bottom], topFrame: { @@ -3022,6 +3141,7 @@ suite('ModernUIContribution', () => { }, }, { rowPaddingTop: '2px', + actionReservationMatches: true, separatorSlots: ['1px', '1px'], separatorOffsets: ['0px', '0px'], topFrame: { borderColors: ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0)'], borderTopLeftRadius: '6px', borderWidths: ['1px', '1px'] }, @@ -3044,13 +3164,14 @@ suite('ModernUIContribution', () => { document.body.appendChild(root); store.add(toDisposable(() => root.remove())); - const editor = appendElement(root, 'part editor'); + const gridView = appendElement(root, 'monaco-grid-view'); + const editor = appendElement(gridView, '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 tab = appendElement(tabs, 'tab active connected-tab-top-row first-in-row'); const fill = appendElement(tab, 'tab-fill'); const targetWindow = getWindow(root); const outlines = []; @@ -3091,7 +3212,7 @@ suite('ModernUIContribution', () => { const rowEndTab = appendElement(tabs, 'tab active last-in-row'); const rowEndFill = appendElement(rowEndTab, 'tab-fill'); const rowEndEdge = appendElement(rowEndTab, 'tab-connected-edge'); - const rowStartTab = appendElement(tabs, 'tab'); + const rowStartTab = appendElement(tabs, 'tab first-in-row'); const rowStartFill = appendElement(rowStartTab, 'tab-fill'); const rowStartEdge = appendElement(rowStartTab, 'tab-connected-edge'); appendElement(appendElement(tabs, 'tab'), 'tab-fill'); @@ -3713,6 +3834,94 @@ suite('ModernUIContribution', () => { }); }); + test('tab divider inherits tab border unless customized', () => { + const theme = ColorThemeData.createUnloadedTheme('vs-dark'); + theme.setCustomColors({ [TAB_BORDER]: '#123456' }); + const inherited = theme.getColor(TAB_DIVIDER)?.toString(); + theme.setCustomColors({ [TAB_BORDER]: '#123456', [TAB_DIVIDER]: '#654321' }); + + assert.deepStrictEqual({ + inherited, + customized: theme.getColor(TAB_DIVIDER)?.toString(), + }, { + inherited: '#123456', + customized: '#654321', + }); + }); + + test('keeps the connected editor tab color audit exhaustive', () => { + const ids = colorRegistry.getColors().map(color => color.id); + const tabColors = ids.filter(id => id.startsWith('tab.')).sort(); + const modernTabColors = ids.filter(id => id.startsWith('modernTab.')).sort(); + const modernEditorTabColors = ids.filter(id => id.startsWith('modernEditorTab.')).sort(); + + assert.deepStrictEqual({ + tabColors, + modernTabColors, + modernEditorTabColors, + editorGroupHeaderColors: [ + 'editorGroupHeader.connectedTabsBackground', + 'editorGroupHeader.tabsBackground', + 'editorGroupHeader.tabsBorder', + ].filter(id => ids.includes(id)), + }, { + tabColors: [ + 'tab.activeBackground', + 'tab.activeBorder', + 'tab.activeBorderTop', + 'tab.activeForeground', + 'tab.activeModifiedBorder', + 'tab.border', + 'tab.divider', + 'tab.dragAndDropBorder', + 'tab.hoverBackground', + 'tab.hoverBorder', + 'tab.hoverForeground', + 'tab.inactiveBackground', + 'tab.inactiveForeground', + 'tab.inactiveModifiedBorder', + 'tab.lastPinnedBorder', + 'tab.selectedBackground', + 'tab.selectedBorderTop', + 'tab.selectedForeground', + 'tab.unfocusedActiveBackground', + 'tab.unfocusedActiveBorder', + 'tab.unfocusedActiveBorderTop', + 'tab.unfocusedActiveForeground', + 'tab.unfocusedActiveModifiedBorder', + 'tab.unfocusedHoverBackground', + 'tab.unfocusedHoverBorder', + 'tab.unfocusedHoverForeground', + 'tab.unfocusedInactiveBackground', + 'tab.unfocusedInactiveForeground', + 'tab.unfocusedInactiveModifiedBorder', + ], + modernTabColors: [ + 'modernTab.activeBackground', + 'modernTab.activeForeground', + 'modernTab.hoverBackground', + 'modernTab.hoverForeground', + ], + modernEditorTabColors: [ + 'modernEditorTab.activeActionBackground', + 'modernEditorTab.activeBackground', + 'modernEditorTab.activeForeground', + 'modernEditorTab.activeHoverActionBackground', + 'modernEditorTab.activeHoverBackground', + 'modernEditorTab.hoverActionBackground', + 'modernEditorTab.hoverBackground', + 'modernEditorTab.hoverForeground', + 'modernEditorTab.inactiveBackground', + 'modernEditorTab.selectedActionBackground', + ], + editorGroupHeaderColors: [ + 'editorGroupHeader.connectedTabsBackground', + 'editorGroupHeader.tabsBackground', + 'editorGroupHeader.tabsBorder', + ], + }); + }); + test('uses legacy color customizations for Modern UI editor tabs only', () => { const theme = ColorThemeData.createUnloadedTheme('vs-dark', { [editorBackground]: '#000000', @@ -3798,6 +4007,7 @@ suite('ModernUIContribution', () => { const separatorProbe = appendElement(root, 'separator-probe'); separatorProbe.style.color = 'var(--modern-ui-editor-tab-border)'; separatorProbe.style.borderColor = 'var(--modern-ui-editor-tab-last-pinned-border)'; + separatorProbe.style.backgroundColor = 'var(--modern-ui-editor-tab-custom-border)'; const twoRowTitle = appendElement(activeGroup, 'title two-tab-bars'); const pinnedRow = appendElement(twoRowTitle, 'tabs-and-actions-container'); appendElement(twoRowTitle, 'tabs-and-actions-container'); @@ -3811,8 +4021,8 @@ suite('ModernUIContribution', () => { paneForeground: getWindow(paneAction.actionLabel).getComputedStyle(paneAction.actionLabel).color, activeBackground: getWindow(activeFill).getComputedStyle(activeFill).backgroundColor, activeForeground: getWindow(activeLabelAnchor).getComputedStyle(activeLabelAnchor).color, - activeBorderTop: getWindow(activeFill).getComputedStyle(activeFill).borderTopColor, - activeBorderBottom: getWindow(activeFill).getComputedStyle(activeFill).borderBottomColor, + activeBorderTop: getWindow(activeFill).getComputedStyle(activeFill, '::before').backgroundColor, + activeBorderBottom: getWindow(activeFill).getComputedStyle(activeFill, '::after').backgroundColor, inactiveBackground: getWindow(inactiveFill).getComputedStyle(inactiveFill).backgroundColor, inactiveForeground: getWindow(inactiveLabelAnchor).getComputedStyle(inactiveLabelAnchor).color, unfocusedActiveBackground: getWindow(unfocusedFill).getComputedStyle(unfocusedFill).backgroundColor, @@ -3836,6 +4046,7 @@ suite('ModernUIContribution', () => { actionFadeBackgroundClip: activeTabActionFadeStyle.backgroundClip, actionFadeBorderBlockWidth: [activeTabActionFadeStyle.borderTopWidth, activeTabActionFadeStyle.borderBottomWidth], separatorColor: getWindow(separatorProbe).getComputedStyle(separatorProbe).color, + customConnectedBorder: getWindow(separatorProbe).getComputedStyle(separatorProbe).backgroundColor, lastPinnedBorder: getWindow(separatorProbe).getComputedStyle(separatorProbe).borderTopColor, pinnedRowUsesLastPinnedBorder: pinnedRowStyle.boxShadow.includes('rgb(136, 153, 170)'), }, { @@ -3868,6 +4079,7 @@ suite('ModernUIContribution', () => { actionFadeBackgroundClip: 'padding-box', actionFadeBorderBlockWidth: ['1px', '1px'], separatorColor: 'rgb(119, 136, 153)', + customConnectedBorder: 'rgb(119, 136, 153)', lastPinnedBorder: 'rgb(136, 153, 170)', pinnedRowUsesLastPinnedBorder: true, }); @@ -3878,6 +4090,8 @@ suite('ModernUIContribution', () => { theme.setCustomColors({ [MODERN_TAB_ACTIVE_BACKGROUND]: '#ABCDEF', [MODERN_TAB_ACTIVE_FOREGROUND]: '#102030', + [MODERN_TAB_HOVER_BACKGROUND]: '#9A3412', + [MODERN_TAB_HOVER_FOREGROUND]: '#FFF7ED', [TAB_ACTIVE_BACKGROUND]: '#123456', [TAB_ACTIVE_FOREGROUND]: '#FEDCBA', }); @@ -3888,7 +4102,7 @@ suite('ModernUIContribution', () => { store.add(toDisposable(() => style.remove())); const root = document.createElement('div'); - root.className = 'modern-tab-customization-theme monaco-workbench modern-ui-tabs'; + root.className = 'modern-tab-customization-theme monaco-workbench modern-ui-tabs modern-ui-connected-editor-tabs'; document.body.appendChild(root); store.add(toDisposable(() => root.remove())); const paneAction = createCompositeAction(root, 35, true); @@ -3902,17 +4116,25 @@ suite('ModernUIContribution', () => { const label = appendElement(tab, 'tab-label'); const labelAnchor = document.createElement('a'); label.appendChild(labelAnchor); + const hoverBackgroundProbe = appendElement(root, 'hover-background-probe'); + hoverBackgroundProbe.style.backgroundColor = 'var(--modern-ui-editor-tab-custom-hover-background)'; + const hoverForegroundProbe = appendElement(root, 'hover-foreground-probe'); + hoverForegroundProbe.style.color = 'var(--modern-ui-editor-tab-hover-foreground)'; assert.deepStrictEqual({ paneBackground: getWindow(paneAction.indicator).getComputedStyle(paneAction.indicator).backgroundColor, paneForeground: getWindow(paneAction.actionLabel).getComputedStyle(paneAction.actionLabel).color, editorBackground: getWindow(fill).getComputedStyle(fill).backgroundColor, editorForeground: getWindow(labelAnchor).getComputedStyle(labelAnchor).color, + hoverBackground: getWindow(hoverBackgroundProbe).getComputedStyle(hoverBackgroundProbe).backgroundColor, + hoverForeground: getWindow(hoverForegroundProbe).getComputedStyle(hoverForegroundProbe).color, }, { paneBackground: 'rgb(171, 205, 239)', paneForeground: 'rgb(16, 32, 48)', editorBackground: 'rgb(171, 205, 239)', editorForeground: 'rgb(16, 32, 48)', + hoverBackground: 'rgb(154, 52, 18)', + hoverForeground: 'rgb(255, 247, 237)', }); }); @@ -3924,9 +4146,15 @@ suite('ModernUIContribution', () => { }); theme.setCustomColors({ [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND]: '#2468AC', + [MODERN_EDITOR_TAB_ACTIVE_ACTION_BACKGROUND]: '#0E3747', [MODERN_EDITOR_TAB_ACTIVE_FOREGROUND]: '#13579B', [MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND]: '#48ACF0', + [MODERN_EDITOR_TAB_ACTIVE_HOVER_ACTION_BACKGROUND]: '#4C1678', [MODERN_EDITOR_TAB_INACTIVE_BACKGROUND]: '#369CF0', + [MODERN_EDITOR_TAB_HOVER_BACKGROUND]: '#7C2D12', + [MODERN_EDITOR_TAB_HOVER_ACTION_BACKGROUND]: '#5A1F0C', + [MODERN_EDITOR_TAB_HOVER_FOREGROUND]: '#FFEDD5', + [MODERN_EDITOR_TAB_SELECTED_ACTION_BACKGROUND]: '#166534', [TAB_ACTIVE_BACKGROUND]: '#123456', [TAB_ACTIVE_FOREGROUND]: '#FEDCBA', [TAB_HOVER_BACKGROUND]: '#456789', @@ -3959,6 +4187,16 @@ suite('ModernUIContribution', () => { activeHoverProbe.style.backgroundColor = 'var(--modern-ui-editor-tab-active-hover-background)'; const activeHoverActionProbe = appendElement(root, 'active-hover-action-probe'); activeHoverActionProbe.style.backgroundColor = 'var(--modern-ui-editor-tab-action-active-hover-background)'; + const activeActionProbe = appendElement(root, 'active-action-probe'); + activeActionProbe.style.backgroundColor = 'var(--modern-ui-editor-tab-custom-action-active-background)'; + const hoverBackgroundProbe = appendElement(root, 'hover-background-probe'); + hoverBackgroundProbe.style.backgroundColor = 'var(--modern-ui-editor-tab-custom-hover-background)'; + const hoverActionProbe = appendElement(root, 'hover-action-probe'); + hoverActionProbe.style.backgroundColor = 'var(--modern-ui-editor-tab-custom-action-hover-background)'; + const hoverForegroundProbe = appendElement(root, 'hover-foreground-probe'); + hoverForegroundProbe.style.color = 'var(--modern-ui-editor-tab-hover-foreground)'; + const selectedActionProbe = appendElement(root, 'selected-action-probe'); + selectedActionProbe.style.backgroundColor = 'var(--vscode-modernEditorTab-selectedActionBackground)'; assert.deepStrictEqual({ paneBackground: getWindow(paneAction.indicator).getComputedStyle(paneAction.indicator).backgroundColor, @@ -3966,16 +4204,26 @@ suite('ModernUIContribution', () => { editorBackground: getWindow(fill).getComputedStyle(fill).backgroundColor, editorForeground: getWindow(labelAnchor).getComputedStyle(labelAnchor).color, editorInactiveBackground: getWindow(inactiveFill).getComputedStyle(inactiveFill).backgroundColor, + editorActiveActionBackground: getWindow(activeActionProbe).getComputedStyle(activeActionProbe).backgroundColor, editorActiveHoverBackground: getWindow(activeHoverProbe).getComputedStyle(activeHoverProbe).backgroundColor, editorActiveHoverActionBackground: getWindow(activeHoverActionProbe).getComputedStyle(activeHoverActionProbe).backgroundColor, + editorHoverBackground: getWindow(hoverBackgroundProbe).getComputedStyle(hoverBackgroundProbe).backgroundColor, + editorHoverActionBackground: getWindow(hoverActionProbe).getComputedStyle(hoverActionProbe).backgroundColor, + editorHoverForeground: getWindow(hoverForegroundProbe).getComputedStyle(hoverForegroundProbe).color, + editorSelectedActionBackground: getWindow(selectedActionProbe).getComputedStyle(selectedActionProbe).backgroundColor, }, { paneBackground: 'rgb(171, 205, 239)', paneForeground: 'rgb(16, 32, 48)', editorBackground: 'rgb(36, 104, 172)', editorForeground: 'rgb(19, 87, 155)', editorInactiveBackground: 'rgb(54, 156, 240)', + editorActiveActionBackground: 'rgb(14, 55, 71)', editorActiveHoverBackground: 'rgb(72, 172, 240)', - editorActiveHoverActionBackground: 'rgb(72, 172, 240)', + editorActiveHoverActionBackground: 'rgb(76, 22, 120)', + editorHoverBackground: 'rgb(124, 45, 18)', + editorHoverActionBackground: 'rgb(90, 31, 12)', + editorHoverForeground: 'rgb(255, 237, 213)', + editorSelectedActionBackground: 'rgb(22, 101, 52)', }); }); diff --git a/src/vs/workbench/services/themes/browser/modernTabColorCustomizations.ts b/src/vs/workbench/services/themes/browser/modernTabColorCustomizations.ts index 4d7dac208a7355..b1530e9d13d7a5 100644 --- a/src/vs/workbench/services/themes/browser/modernTabColorCustomizations.ts +++ b/src/vs/workbench/services/themes/browser/modernTabColorCustomizations.ts @@ -6,7 +6,7 @@ import { Color } from '../../../../base/common/color.js'; import { ColorIdentifier, editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; -import { 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_TAB_ACTIVE_BACKGROUND, MODERN_TAB_ACTIVE_FOREGROUND, MODERN_TAB_HOVER_BACKGROUND, MODERN_TAB_HOVER_FOREGROUND, 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_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, TAB_UNFOCUSED_HOVER_FOREGROUND, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND } from '../../../common/theme.js'; +import { 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_TAB_ACTIVE_BACKGROUND, MODERN_TAB_ACTIVE_FOREGROUND, MODERN_TAB_HOVER_BACKGROUND, MODERN_TAB_HOVER_FOREGROUND, 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_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, TAB_UNFOCUSED_HOVER_FOREGROUND, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND } from '../../../common/theme.js'; import { ColorThemeData } from '../common/colorThemeData.js'; /** @@ -33,9 +33,9 @@ function resolveExplicitTabColor(theme: ColorThemeData, modernColorId: ColorIden return resolveLegacyTabColor(theme, legacyColorId, modernColorIds, ...relatedLegacyColorIds); } -function addColorVariable(declarations: string[], name: string, color: Color | undefined): void { +function addColorVariable(collector: (name: string, color: Color) => void, name: string, color: Color | undefined): void { if (color) { - declarations.push(`${name}: ${color};`); + collector(name, color); } } @@ -43,12 +43,7 @@ function flattenTabBackground(color: Color, editorBackgroundColor: Color | undef return editorBackgroundColor ? color.makeOpaque(editorBackgroundColor) : color; } -registerThemingParticipant((theme, collector) => { - if (!(theme instanceof ColorThemeData)) { - return; - } - - const declarations: string[] = []; +function collectModernTabColorCustomizations(theme: ColorThemeData, collector: (name: string, color: Color) => void): void { const activeBackground = resolveLegacyTabColor(theme, TAB_ACTIVE_BACKGROUND, [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, MODERN_TAB_ACTIVE_BACKGROUND]); const unfocusedActiveBackground = resolveLegacyTabColor(theme, TAB_UNFOCUSED_ACTIVE_BACKGROUND, [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, MODERN_TAB_ACTIVE_BACKGROUND], TAB_ACTIVE_BACKGROUND); const inactiveBackground = resolveLegacyTabColor(theme, TAB_INACTIVE_BACKGROUND, [MODERN_EDITOR_TAB_INACTIVE_BACKGROUND]); @@ -61,6 +56,9 @@ registerThemingParticipant((theme, collector) => { const hasModernActiveActionBackground = !!theme.getColorCustomization(MODERN_EDITOR_TAB_ACTIVE_ACTION_BACKGROUND); const hasModernHoverActionBackground = !!theme.getColorCustomization(MODERN_EDITOR_TAB_HOVER_ACTION_BACKGROUND); const hasModernActiveHoverActionBackground = !!theme.getColorCustomization(MODERN_EDITOR_TAB_ACTIVE_HOVER_ACTION_BACKGROUND); + const explicitActiveActionBackground = hasModernActiveActionBackground ? theme.getColor(MODERN_EDITOR_TAB_ACTIVE_ACTION_BACKGROUND) : undefined; + const explicitHoverActionBackground = hasModernHoverActionBackground ? theme.getColor(MODERN_EDITOR_TAB_HOVER_ACTION_BACKGROUND) : undefined; + const explicitActiveHoverActionBackground = hasModernActiveHoverActionBackground ? theme.getColor(MODERN_EDITOR_TAB_ACTIVE_HOVER_ACTION_BACKGROUND) : undefined; const explicitActiveBackground = resolveExplicitTabColor(theme, MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, MODERN_TAB_ACTIVE_BACKGROUND], TAB_ACTIVE_BACKGROUND); const explicitUnfocusedActiveBackground = resolveExplicitTabColor(theme, MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND, MODERN_TAB_ACTIVE_BACKGROUND], TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND); const explicitInactiveBackground = resolveExplicitTabColor(theme, MODERN_EDITOR_TAB_INACTIVE_BACKGROUND, [MODERN_EDITOR_TAB_INACTIVE_BACKGROUND], TAB_INACTIVE_BACKGROUND); @@ -69,57 +67,76 @@ registerThemingParticipant((theme, collector) => { const explicitUnfocusedHoverBackground = resolveExplicitTabColor(theme, MODERN_EDITOR_TAB_HOVER_BACKGROUND, [MODERN_EDITOR_TAB_HOVER_BACKGROUND, MODERN_TAB_HOVER_BACKGROUND], TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_HOVER_BACKGROUND); const explicitActiveHoverBackground = resolveExplicitTabColor(theme, MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND, [MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND, MODERN_TAB_HOVER_BACKGROUND], TAB_HOVER_BACKGROUND); const explicitUnfocusedActiveHoverBackground = resolveExplicitTabColor(theme, MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND, [MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND, MODERN_EDITOR_TAB_HOVER_BACKGROUND, MODERN_TAB_HOVER_BACKGROUND], TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_HOVER_BACKGROUND); + const tabBorder = resolveLegacyTabColor(theme, TAB_BORDER, []); + const activeBorderTop = resolveLegacyTabColor(theme, TAB_ACTIVE_BORDER_TOP, []); + const unfocusedActiveBorderTop = resolveLegacyTabColor(theme, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, [], TAB_ACTIVE_BORDER_TOP); + const selectedBorderTop = resolveLegacyTabColor(theme, TAB_SELECTED_BORDER_TOP, []); + const explicitActiveBorderTop = theme.getColor(TAB_ACTIVE_BORDER_TOP) ? activeBorderTop : selectedBorderTop; + const explicitUnfocusedActiveBorderTop = theme.getColor(TAB_UNFOCUSED_ACTIVE_BORDER_TOP) ? unfocusedActiveBorderTop : selectedBorderTop; - addColorVariable(declarations, '--modern-ui-editor-tab-active-background', activeBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-unfocused-active-background', unfocusedActiveBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-inactive-background', inactiveBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-unfocused-inactive-background', unfocusedInactiveBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-hover-background', hoverBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-unfocused-hover-background', unfocusedHoverBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-active-hover-background', activeHoverBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-unfocused-active-hover-background', unfocusedActiveHoverBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-active-foreground', resolveLegacyTabColor(theme, TAB_ACTIVE_FOREGROUND, [MODERN_EDITOR_TAB_ACTIVE_FOREGROUND, MODERN_TAB_ACTIVE_FOREGROUND])); - addColorVariable(declarations, '--modern-ui-editor-tab-unfocused-active-foreground', resolveLegacyTabColor(theme, TAB_UNFOCUSED_ACTIVE_FOREGROUND, [MODERN_EDITOR_TAB_ACTIVE_FOREGROUND, MODERN_TAB_ACTIVE_FOREGROUND], TAB_ACTIVE_FOREGROUND)); - addColorVariable(declarations, '--modern-ui-editor-tab-inactive-foreground', resolveLegacyTabColor(theme, TAB_INACTIVE_FOREGROUND, [], TAB_ACTIVE_FOREGROUND)); - addColorVariable(declarations, '--modern-ui-editor-tab-unfocused-inactive-foreground', resolveLegacyTabColor(theme, TAB_UNFOCUSED_INACTIVE_FOREGROUND, [], TAB_INACTIVE_FOREGROUND, TAB_ACTIVE_FOREGROUND)); - addColorVariable(declarations, '--modern-ui-editor-tab-hover-foreground', resolveLegacyTabColor(theme, TAB_HOVER_FOREGROUND, [MODERN_EDITOR_TAB_HOVER_FOREGROUND, MODERN_TAB_HOVER_FOREGROUND])); - addColorVariable(declarations, '--modern-ui-editor-tab-unfocused-hover-foreground', resolveLegacyTabColor(theme, TAB_UNFOCUSED_HOVER_FOREGROUND, [MODERN_EDITOR_TAB_HOVER_FOREGROUND, MODERN_TAB_HOVER_FOREGROUND], TAB_HOVER_FOREGROUND)); - addColorVariable(declarations, '--modern-ui-editor-tab-border', resolveLegacyTabColor(theme, TAB_BORDER, [])); - addColorVariable(declarations, '--modern-ui-editor-tab-last-pinned-border', resolveLegacyTabColor(theme, TAB_LAST_PINNED_BORDER, [])); - addColorVariable(declarations, '--modern-ui-editor-tab-active-border', resolveLegacyTabColor(theme, TAB_ACTIVE_BORDER, [])); - addColorVariable(declarations, '--modern-ui-editor-tab-unfocused-active-border', resolveLegacyTabColor(theme, TAB_UNFOCUSED_ACTIVE_BORDER, [], TAB_ACTIVE_BORDER)); - addColorVariable(declarations, '--modern-ui-editor-tab-active-border-top', resolveLegacyTabColor(theme, TAB_ACTIVE_BORDER_TOP, [])); - addColorVariable(declarations, '--modern-ui-editor-tab-unfocused-active-border-top', resolveLegacyTabColor(theme, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, [], TAB_ACTIVE_BORDER_TOP)); - addColorVariable(declarations, '--modern-ui-editor-tab-hover-border', resolveLegacyTabColor(theme, TAB_HOVER_BORDER, [])); - addColorVariable(declarations, '--modern-ui-editor-tab-unfocused-hover-border', resolveLegacyTabColor(theme, TAB_UNFOCUSED_HOVER_BORDER, [], TAB_HOVER_BORDER)); - addColorVariable(declarations, '--modern-ui-editor-tab-custom-active-background', explicitActiveBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-custom-unfocused-active-background', explicitUnfocusedActiveBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-custom-inactive-background', explicitInactiveBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-custom-unfocused-inactive-background', explicitUnfocusedInactiveBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-custom-hover-background', explicitHoverBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-custom-unfocused-hover-background', explicitUnfocusedHoverBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-custom-active-hover-background', explicitActiveHoverBackground); - addColorVariable(declarations, '--modern-ui-editor-tab-custom-unfocused-active-hover-background', explicitUnfocusedActiveHoverBackground); + addColorVariable(collector, '--modern-ui-editor-tab-active-background', activeBackground); + addColorVariable(collector, '--modern-ui-editor-tab-unfocused-active-background', unfocusedActiveBackground); + addColorVariable(collector, '--modern-ui-editor-tab-inactive-background', inactiveBackground); + addColorVariable(collector, '--modern-ui-editor-tab-unfocused-inactive-background', unfocusedInactiveBackground); + addColorVariable(collector, '--modern-ui-editor-tab-hover-background', hoverBackground); + addColorVariable(collector, '--modern-ui-editor-tab-unfocused-hover-background', unfocusedHoverBackground); + addColorVariable(collector, '--modern-ui-editor-tab-active-hover-background', activeHoverBackground); + addColorVariable(collector, '--modern-ui-editor-tab-unfocused-active-hover-background', unfocusedActiveHoverBackground); + addColorVariable(collector, '--modern-ui-editor-tab-active-foreground', resolveLegacyTabColor(theme, TAB_ACTIVE_FOREGROUND, [MODERN_EDITOR_TAB_ACTIVE_FOREGROUND, MODERN_TAB_ACTIVE_FOREGROUND])); + addColorVariable(collector, '--modern-ui-editor-tab-unfocused-active-foreground', resolveLegacyTabColor(theme, TAB_UNFOCUSED_ACTIVE_FOREGROUND, [MODERN_EDITOR_TAB_ACTIVE_FOREGROUND, MODERN_TAB_ACTIVE_FOREGROUND], TAB_ACTIVE_FOREGROUND)); + addColorVariable(collector, '--modern-ui-editor-tab-inactive-foreground', resolveLegacyTabColor(theme, TAB_INACTIVE_FOREGROUND, [], TAB_ACTIVE_FOREGROUND)); + addColorVariable(collector, '--modern-ui-editor-tab-unfocused-inactive-foreground', resolveLegacyTabColor(theme, TAB_UNFOCUSED_INACTIVE_FOREGROUND, [], TAB_INACTIVE_FOREGROUND, TAB_ACTIVE_FOREGROUND)); + addColorVariable(collector, '--modern-ui-editor-tab-hover-foreground', resolveLegacyTabColor(theme, TAB_HOVER_FOREGROUND, [MODERN_EDITOR_TAB_HOVER_FOREGROUND, MODERN_TAB_HOVER_FOREGROUND])); + addColorVariable(collector, '--modern-ui-editor-tab-unfocused-hover-foreground', resolveLegacyTabColor(theme, TAB_UNFOCUSED_HOVER_FOREGROUND, [MODERN_EDITOR_TAB_HOVER_FOREGROUND, MODERN_TAB_HOVER_FOREGROUND], TAB_HOVER_FOREGROUND)); + addColorVariable(collector, '--modern-ui-editor-tab-border', tabBorder); + addColorVariable(collector, '--modern-ui-editor-tab-custom-border', tabBorder); + addColorVariable(collector, '--modern-ui-editor-tab-last-pinned-border', resolveLegacyTabColor(theme, TAB_LAST_PINNED_BORDER, [])); + addColorVariable(collector, '--modern-ui-editor-tab-active-border', resolveLegacyTabColor(theme, TAB_ACTIVE_BORDER, [])); + addColorVariable(collector, '--modern-ui-editor-tab-unfocused-active-border', resolveLegacyTabColor(theme, TAB_UNFOCUSED_ACTIVE_BORDER, [], TAB_ACTIVE_BORDER)); + addColorVariable(collector, '--modern-ui-editor-tab-active-border-top', activeBorderTop); + addColorVariable(collector, '--modern-ui-editor-tab-unfocused-active-border-top', unfocusedActiveBorderTop); + addColorVariable(collector, '--modern-ui-editor-tab-custom-active-border-top', explicitActiveBorderTop); + addColorVariable(collector, '--modern-ui-editor-tab-custom-unfocused-active-border-top', explicitUnfocusedActiveBorderTop); + addColorVariable(collector, '--modern-ui-editor-tab-hover-border', resolveLegacyTabColor(theme, TAB_HOVER_BORDER, [])); + addColorVariable(collector, '--modern-ui-editor-tab-unfocused-hover-border', resolveLegacyTabColor(theme, TAB_UNFOCUSED_HOVER_BORDER, [], TAB_HOVER_BORDER)); + addColorVariable(collector, '--modern-ui-editor-tab-custom-active-background', explicitActiveBackground); + addColorVariable(collector, '--modern-ui-editor-tab-custom-unfocused-active-background', explicitUnfocusedActiveBackground); + addColorVariable(collector, '--modern-ui-editor-tab-custom-inactive-background', explicitInactiveBackground); + addColorVariable(collector, '--modern-ui-editor-tab-custom-unfocused-inactive-background', explicitUnfocusedInactiveBackground); + addColorVariable(collector, '--modern-ui-editor-tab-custom-hover-background', explicitHoverBackground); + addColorVariable(collector, '--modern-ui-editor-tab-custom-unfocused-hover-background', explicitUnfocusedHoverBackground); + addColorVariable(collector, '--modern-ui-editor-tab-custom-active-hover-background', explicitActiveHoverBackground); + addColorVariable(collector, '--modern-ui-editor-tab-custom-unfocused-active-hover-background', explicitUnfocusedActiveHoverBackground); + addColorVariable(collector, '--modern-ui-editor-tab-custom-action-active-background', explicitActiveActionBackground); + addColorVariable(collector, '--modern-ui-editor-tab-custom-action-hover-background', explicitHoverActionBackground); + addColorVariable(collector, '--modern-ui-editor-tab-custom-action-active-hover-background', explicitActiveHoverActionBackground); if (activeBackground && !hasModernActiveActionBackground) { - addColorVariable(declarations, '--modern-ui-editor-tab-action-active-background', flattenTabBackground(activeBackground, editorBackgroundColor)); + addColorVariable(collector, '--modern-ui-editor-tab-action-active-background', flattenTabBackground(activeBackground, editorBackgroundColor)); } if (unfocusedActiveBackground && !hasModernActiveActionBackground) { - addColorVariable(declarations, '--modern-ui-editor-tab-action-unfocused-active-background', flattenTabBackground(unfocusedActiveBackground, editorBackgroundColor)); + addColorVariable(collector, '--modern-ui-editor-tab-action-unfocused-active-background', flattenTabBackground(unfocusedActiveBackground, editorBackgroundColor)); } if (hoverBackground && !hasModernHoverActionBackground) { - addColorVariable(declarations, '--modern-ui-editor-tab-action-hover-background', flattenTabBackground(hoverBackground, editorBackgroundColor)); + addColorVariable(collector, '--modern-ui-editor-tab-action-hover-background', flattenTabBackground(hoverBackground, editorBackgroundColor)); } if (unfocusedHoverBackground && !hasModernHoverActionBackground) { - addColorVariable(declarations, '--modern-ui-editor-tab-action-unfocused-hover-background', flattenTabBackground(unfocusedHoverBackground, editorBackgroundColor)); + addColorVariable(collector, '--modern-ui-editor-tab-action-unfocused-hover-background', flattenTabBackground(unfocusedHoverBackground, editorBackgroundColor)); } if (activeHoverBackground && !hasModernActiveHoverActionBackground) { - addColorVariable(declarations, '--modern-ui-editor-tab-action-active-hover-background', flattenTabBackground(activeHoverBackground, editorBackgroundColor)); + addColorVariable(collector, '--modern-ui-editor-tab-action-active-hover-background', flattenTabBackground(activeHoverBackground, editorBackgroundColor)); } if (unfocusedActiveHoverBackground && !hasModernActiveHoverActionBackground) { - addColorVariable(declarations, '--modern-ui-editor-tab-action-unfocused-active-hover-background', flattenTabBackground(unfocusedActiveHoverBackground, editorBackgroundColor)); + addColorVariable(collector, '--modern-ui-editor-tab-action-unfocused-active-hover-background', flattenTabBackground(unfocusedActiveHoverBackground, editorBackgroundColor)); } +} +registerThemingParticipant((theme, collector) => { + if (!(theme instanceof ColorThemeData)) { + return; + } + const declarations: string[] = []; + collectModernTabColorCustomizations(theme, (name, color) => declarations.push(`${name}: ${color};`)); if (declarations.length > 0) { // The doubled `.monaco-workbench` raises specificity above the defaults in tabs.css so these overrides win regardless of style injection order. collector.addRule(`.modern-ui-tabs.monaco-workbench.monaco-workbench { ${declarations.join('\n')} }`); diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/tabs.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/tabs.fixture.ts index 66d0115a703225..475ad48d9e4ff0 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/tabs.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/tabs.fixture.ts @@ -7,7 +7,7 @@ import { $, Dimension, getWindow, scheduleAtNextAnimationFrame } from '../../../ import { Action } from '../../../../../base/common/actions.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { basename, dirname } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -21,6 +21,7 @@ import { TestConfigurationService } from '../../../../../platform/configuration/ import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js'; import { listErrorForeground, listWarningForeground } from '../../../../../platform/theme/common/colors/listColors.js'; +import { isDark, isHighContrast } from '../../../../../platform/theme/common/theme.js'; import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { TestThemeService } from '../../../../../platform/theme/test/common/testThemeService.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; @@ -32,20 +33,44 @@ import { EditorInput } from '../../../../common/editor/editorInput.js'; import { EditorInputCapabilities, EditorsOrder, IEditorPartOptions, IToolbarActions, Verbosity } from '../../../../common/editor.js'; import { EditorGroupModel } from '../../../../common/editor/editorGroupModel.js'; import { + EDITOR_BORDER, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_HEADER_TABS_BACKGROUND, + 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, + TAB_ACTIVE_BORDER, + TAB_ACTIVE_BORDER_TOP, + TAB_BORDER, + TAB_DIVIDER, + TAB_HOVER_BACKGROUND, + TAB_HOVER_BORDER, + TAB_SELECTED_BORDER_TOP, + TAB_UNFOCUSED_ACTIVE_BORDER, + TAB_UNFOCUSED_ACTIVE_BORDER_TOP, + TAB_UNFOCUSED_HOVER_BORDER, } from '../../../../common/theme.js'; import { DEFAULT_EDITOR_PART_OPTIONS, IEditorGroupMenuIds, IEditorGroupsView, IEditorGroupView, IEditorPartsView } from '../../../../browser/parts/editor/editor.js'; import { BreadcrumbsService, IBreadcrumbsService } from '../../../../browser/parts/editor/breadcrumbs.js'; import { EditorTitleControl } from '../../../../browser/parts/editor/editorTitleControl.js'; import { IDecorationData, IDecorationsProvider, IDecorationsService } from '../../../../services/decorations/common/decorations.js'; import { DecorationsService } from '../../../../services/decorations/browser/decorationsService.js'; +import '../../../../services/themes/browser/modernTabColorCustomizations.js'; +import { ColorThemeData } from '../../../../services/themes/common/colorThemeData.js'; import { INotebookDocumentService, NotebookDocumentWorkbenchService } from '../../../../services/notebook/common/notebookDocumentService.js'; import { IOutlineService } from '../../../../services/outline/browser/outline.js'; import { LayoutSettings, ModernUIEditorTabStyle } from '../../../../services/layout/browser/layoutService.js'; import { TestContextService } from '../../../common/workbenchTestServices.js'; import { workbenchInstantiationService } from '../../workbenchTestServices.js'; import { ComponentFixtureAdditionalTheme, ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; +import { getThemeStyleSheet } from '../fixtureUtilsCss.js'; import '../../../../contrib/modernUI/browser/media/tabs.css'; import '../../../../contrib/modernUI/browser/connectedEditorTabs.js'; @@ -276,6 +301,21 @@ export interface IEditorTabsFixtureOptions { readonly headerMenuIds?: IEditorGroupMenuIds; readonly editorContents?: string; readonly activeTabClipping?: 'left' | 'right' | 'left-shoulder' | 'right-shoulder'; + readonly colorCustomizations?: Readonly>; + readonly editorFrame?: boolean; +} + +let customizedThemeId = 0; + +function customizeTheme(theme: ColorThemeData, customizations: Readonly> | undefined): ColorThemeData { + if (!customizations) { + return theme; + } + + const customizedTheme = Object.assign(ColorThemeData.createLoadedEmptyTheme(theme.id, theme.settingsId), theme); + customizedTheme.id = `${theme.id} tab-custom-colors-${customizedThemeId++}`; + customizedTheme.setCustomColors(customizations); + return customizedTheme; } function createPartOptions(overrides?: Partial): IEditorPartOptions { @@ -290,7 +330,7 @@ function populateModel(model: EditorGroupModel, specs: IEditorSpec[], disposable // Open sticky editors first so their indices stay at the front. const ordered = [...specs].sort((a, b) => (a.sticky === b.sticky) ? 0 : a.sticky ? -1 : 1); const inputBySpec = new Map(); - for (const spec of ordered) { + for (const [index, spec] of ordered.entries()) { const input = disposableStore.add(new FixtureEditorInput(spec.resource, { typeId: spec.typeId, dirty: spec.dirty, @@ -299,6 +339,7 @@ function populateModel(model: EditorGroupModel, specs: IEditorSpec[], disposable })); inputBySpec.set(spec, input); model.openEditor(input, { + index, pinned: spec.pinned ?? true, sticky: spec.sticky, active: spec.active, @@ -335,8 +376,18 @@ export function renderEditorTabsFixture(ctx: ComponentFixtureContext, options: I // Feed the fixture's themes to the shared theme service so tab-bar theme lookups resolve. const themeService = instantiationService.get(IThemeService) as TestThemeService; - themeService.setTheme(theme); + const fixtureTheme = customizeTheme(theme, options.colorCustomizations); + themeService.setTheme(fixtureTheme); themeService.setFileIconTheme(fileIconTheme); + if (fixtureTheme !== theme) { + const styleSheet = getThemeStyleSheet(fixtureTheme); + const targetDocument = container.ownerDocument; + container.classList.add(...fixtureTheme.classNames); + targetDocument.adoptedStyleSheets = [...targetDocument.adoptedStyleSheets, styleSheet]; + disposableStore.add(toDisposable(() => { + targetDocument.adoptedStyleSheets = targetDocument.adoptedStyleSheets.filter(sheet => sheet !== styleSheet); + })); + } // Services the base workbench harness does not stub but the tab bar needs. instantiationService.stub(ITreeViewsDnDService, new TreeViewsDnDService()); @@ -423,7 +474,7 @@ export function renderEditorTabsFixture(ctx: ComponentFixtureContext, options: I titleContainer.classList.toggle('tabs', partOptions.showTabs === 'multiple'); titleContainer.classList.toggle('show-file-icons', partOptions.showIcons); - const headerBackground = theme.getColor(partOptions.showTabs === 'multiple' ? EDITOR_GROUP_HEADER_TABS_BACKGROUND : EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND); + const headerBackground = fixtureTheme.getColor(partOptions.showTabs === 'multiple' ? EDITOR_GROUP_HEADER_TABS_BACKGROUND : EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND); if (headerBackground) { titleContainer.style.backgroundColor = headerBackground.toString(); } @@ -436,7 +487,23 @@ export function renderEditorTabsFixture(ctx: ComponentFixtureContext, options: I content.appendChild(groupContainer); groupContainer.appendChild(titleContainer); groupContainer.appendChild(editorContainer); - container.appendChild(editorPart); + if (options.editorFrame) { + container.classList.add('floating-panels'); + editorPart.style.border = 'var(--vscode-strokeThickness) solid var(--vscode-editor-border)'; + editorPart.style.borderRadius = 'var(--vscode-cornerRadius-large)'; + editorPart.style.boxSizing = 'border-box'; + editorPart.style.overflow = 'hidden'; + editorPart.style.backgroundColor = 'var(--vscode-editor-background)'; + const grid = $('.monaco-grid-view'); + grid.appendChild(editorPart); + container.appendChild(grid); + } else { + container.appendChild(editorPart); + } + + container.style.width = `${width}px`; + groupContainer.style.width = options.editorFrame ? '100%' : `${width}px`; + const layoutWidth = groupContainer.clientWidth; if (options.editorContents !== undefined && model.activeEditor instanceof FixtureEditorInput) { editorContainer.style.height = '240px'; @@ -452,12 +519,9 @@ export function renderEditorTabsFixture(ctx: ComponentFixtureContext, options: I padding: { top: 16 }, }, { contributions: [] })); editor.setModel(textModel); - editor.layout(new Dimension(width, 240)); + editor.layout(new Dimension(layoutWidth, 240)); } - container.style.width = `${width}px`; - groupContainer.style.width = `${width}px`; - const titleControl = disposableStore.add(instantiationService.createInstance( EditorTitleControl, titleContainer, @@ -473,8 +537,8 @@ export function renderEditorTabsFixture(ctx: ComponentFixtureContext, options: I const layout = () => { titleControl.layout({ - container: new Dimension(width, titleControl.getHeight().total), - available: new Dimension(width, 200), + container: new Dimension(layoutWidth, titleControl.getHeight().total), + available: new Dimension(layoutWidth, 200), }, options.headerWidth); }; groupView.relayoutFn = layout; @@ -516,14 +580,113 @@ function render(modernUI: boolean, options: Omit Promise | void { - const wrapped = layout === 'wrapped'; +function getLegacyEditorTabBorderCustomizations(): Readonly> { + return { + [TAB_ACTIVE_BORDER]: '#F43F5E', + [TAB_ACTIVE_BORDER_TOP]: '#22D3EE', + [TAB_BORDER]: '#FACC15', + [TAB_DIVIDER]: '#FFFFFF', + [TAB_HOVER_BORDER]: '#F97316', + [TAB_UNFOCUSED_ACTIVE_BORDER]: '#FB923C', + [TAB_UNFOCUSED_ACTIVE_BORDER_TOP]: '#C084FC', + [TAB_UNFOCUSED_HOVER_BORDER]: '#A855F7', + [TAB_SELECTED_BORDER_TOP]: '#A3E635', + }; +} + +function getModernEditorTabColorCustomizations(theme: ComponentFixtureContext['theme']): Readonly> { + const dark = isDark(theme.type); + return { + [MODERN_EDITOR_TAB_ACTIVE_BACKGROUND]: dark ? '#164E63' : '#BAE6FD', + [MODERN_EDITOR_TAB_ACTIVE_ACTION_BACKGROUND]: dark ? '#0E3747' : '#7DD3FC', + [MODERN_EDITOR_TAB_ACTIVE_FOREGROUND]: dark ? '#CFFAFE' : '#0C4A6E', + [MODERN_EDITOR_TAB_INACTIVE_BACKGROUND]: dark ? '#1E293B' : '#E2E8F0', + [MODERN_EDITOR_TAB_HOVER_BACKGROUND]: dark ? '#7C2D12' : '#FED7AA', + [MODERN_EDITOR_TAB_HOVER_ACTION_BACKGROUND]: dark ? '#5A1F0C' : '#FDBA74', + [MODERN_EDITOR_TAB_HOVER_FOREGROUND]: dark ? '#FFEDD5' : '#7C2D12', + [MODERN_EDITOR_TAB_ACTIVE_HOVER_BACKGROUND]: dark ? '#6B21A8' : '#E9D5FF', + [MODERN_EDITOR_TAB_ACTIVE_HOVER_ACTION_BACKGROUND]: dark ? '#4C1678' : '#D8B4FE', + [MODERN_EDITOR_TAB_SELECTED_ACTION_BACKGROUND]: dark ? '#166534' : '#BBF7D0', + }; +} + +function renderBorderOwnership(modernUI: boolean, editorTabStyle?: ModernUIEditorTabStyle, customizeColors = true): (ctx: ComponentFixtureContext) => void { + return ctx => renderEditorTabsFixture(ctx, { + modernUI, + editorTabStyle, + width: 1200, + editors: [ + { resource: file('/project/alpha.ts'), pinned: true }, + { resource: file('/project/beta.ts'), pinned: true }, + { resource: file('/project/gamma.ts'), pinned: true }, + { resource: file('/project/delta.ts'), pinned: true, active: true }, + { resource: file('/project/epsilon.ts'), pinned: true }, + { resource: file('/project/zeta.ts'), pinned: true }, + { resource: file('/project/eta.ts'), pinned: true }, + { resource: file('/project/theta.ts'), pinned: true }, + ], + colorCustomizations: customizeColors && !isHighContrast(ctx.theme.type) ? getLegacyEditorTabBorderCustomizations() : undefined, + }); +} + +function renderConnectedLegacyBorders(active: boolean): (ctx: ComponentFixtureContext) => void { + return render(true, { + active, + editors: multiSelectEditorSpecs(), + colorCustomizations: getLegacyEditorTabBorderCustomizations(), + }); +} + +function renderConnectedBorderContinuity(activeTabIndex: number): (ctx: ComponentFixtureContext) => void { + return render(true, { + editorFrame: true, + editors: [ + { resource: file('/project/alpha.ts'), pinned: true, active: activeTabIndex === 0 }, + { resource: file('/project/beta.ts'), pinned: true, active: activeTabIndex === 1 }, + { resource: file('/project/gamma.ts'), pinned: true, active: activeTabIndex === 2 }, + ], + colorCustomizations: { + [EDITOR_BORDER]: '#22D3EE', + [TAB_ACTIVE_BORDER_TOP]: '#22D3EE', + [TAB_BORDER]: '#22D3EE', + [TAB_DIVIDER]: '#00000000', + }, + }); +} + +function renderWrappedConnectedBorderOwnership(): (ctx: ComponentFixtureContext) => void { + return render(true, { + width: 820, + editors: manyEditorSpecs().slice(0, 10).map((spec, index) => ({ ...spec, active: index === 0 })), + partOptions: { wrapTabs: true, editorActionsLocation: 'hidden' }, + colorCustomizations: getLegacyEditorTabBorderCustomizations(), + }); +} + +function renderConnectedModernEditorTabCustomizations(layout: 'single' | 'wrapped' | 'pinned' = 'single'): (ctx: ComponentFixtureContext) => void { + return ctx => renderEditorTabsFixture(ctx, { + modernUI: true, + editorTabStyle: ModernUIEditorTabStyle.Connected, + partOptions: layout === 'wrapped' ? { wrapTabs: true } : layout === 'pinned' ? { pinnedTabsOnSeparateRow: true, pinnedTabSizing: 'normal' } : undefined, + width: layout === 'wrapped' ? 260 : undefined, + editors: [ + { resource: file('/project/alpha.ts'), pinned: true, selected: layout === 'single', sticky: layout === 'pinned' }, + { resource: file('/project/beta.ts'), pinned: true, active: true, selected: true }, + { resource: file('/project/gamma.ts'), pinned: true }, + ], + colorCustomizations: getModernEditorTabColorCustomizations(ctx.theme), + }); +} + +function renderDensityLayout(layout: 'singleRow' | 'wrapped' | 'wrappedBottomActive' | 'pinnedSeparateRow', tabHeight: IEditorPartOptions['tabHeight']): (ctx: ComponentFixtureContext) => Promise | void { + const wrapped = layout === 'wrapped' || layout === 'wrappedBottomActive'; + const bottomActive = layout === 'wrappedBottomActive'; const renderFixture = layout === 'pinnedSeparateRow' ? renderPinnedSeparateRow(tabHeight) : render(true, { - partOptions: { wrapTabs: wrapped, tabHeight }, - editors: wrapped ? manyEditorSpecs() : undefined, - width: wrapped ? 520 : undefined, + partOptions: { wrapTabs: wrapped, tabHeight, ...(bottomActive ? { editorActionsLocation: 'hidden' } : {}) }, + editors: bottomActive ? manyEditorSpecs(8).slice(0, 10) : wrapped ? manyEditorSpecs() : undefined, + width: bottomActive ? 820 : wrapped ? 520 : undefined, }); if (!wrapped) { return renderFixture; @@ -543,6 +706,12 @@ function renderDensityLayout(layout: 'singleRow' | 'wrapped' | 'pinnedSeparateRo tabs: tabs.map(tab => [tab.offsetLeft, tab.offsetTop, tab.offsetWidth, tab.offsetHeight]), }); if (layout === previousLayout) { + if (tabs.some((tab, index) => tab.classList.contains('last-in-row') !== (index === tabs.length - 1 || tab.offsetTop !== tabs[index + 1].offsetTop))) { + throw new Error('Wrapped tab row markers do not match the rendered rows'); + } + if (bottomActive && tabs.some(tab => tab.classList.contains('active') && (tab.offsetTop !== Math.max(...tabs.map(tab => tab.offsetTop)) || tab.classList.contains('connected-tab-upper-row')))) { + throw new Error('The active tab must be connected to the bottom wrapped row'); + } return; } previousLayout = layout; @@ -584,6 +753,18 @@ function createDensityFixtures() { expectedVisualDescriptions: ['Compact-density tabs wrap into equal-height rows while labels and actions remain vertically centered.'], }), }), + WrappedBottomActive: defineThemedFixtureGroup({ + Default: defineComponentFixture({ + render: renderDensityLayout('wrappedBottomActive', 'default'), + additionalThemes: ['darkHighContrast', 'lightHighContrast'], + expectedVisualDescriptions: ['The active bottom-row tab joins the body separator with a continuous curved stroke on both sides, without changing the height of any row.'], + }), + Compact: defineComponentFixture({ + render: renderDensityLayout('wrappedBottomActive', 'compact'), + additionalThemes: ['darkHighContrast', 'lightHighContrast'], + expectedVisualDescriptions: ['Compact bottom-row tabs preserve the same continuous shoulder stroke as default density; upper rows remain separate pills.'], + }), + }), }; } @@ -651,6 +832,91 @@ function renderPinnedSeparateRow(tabHeight: IEditorPartOptions['tabHeight']): (c const extendedTabThemes: readonly ComponentFixtureAdditionalTheme[] = ['darkModern', 'light2026', 'darkPlus', 'lightPlus', 'visualStudioDark', 'visualStudioLight', 'darkHighContrast', 'lightHighContrast', 'abyss', 'monokai', 'quietLight', 'solarizedDark', 'solarizedLight']; export default defineThemedFixtureGroup({ path: 'editor/' }, { + Colors: defineThemedFixtureGroup({ + ConnectedLegacyBorders: defineThemedFixtureGroup({ + ActiveGroup: defineComponentFixture({ + render: renderConnectedLegacyBorders(true), + expectedVisualDescriptions: ['The active connected tab shows customized focused top and bottom borders; selected tabs retain their selected accent.'], + }), + InactiveGroup: defineComponentFixture({ + render: renderConnectedLegacyBorders(false), + expectedVisualDescriptions: ['The active connected tab shows customized unfocused top and bottom borders; selected tabs retain their selected accent.'], + }), + }), + BorderOwnership: defineThemedFixtureGroup({ + Legacy: defineComponentFixture({ + render: renderBorderOwnership(false), + themes: ['dark'], + expectedVisualDescriptions: ['Legacy tabs retain standard indicators and use tab.border as the only shared-edge separator.'], + }), + Pill: defineComponentFixture({ + render: renderBorderOwnership(true, ModernUIEditorTabStyle.Pill), + themes: ['dark'], + additionalThemes: ['darkHighContrast', 'lightHighContrast'], + expectedVisualDescriptions: ['Pill tabs use rounded tab boundaries and dedicated dividers in standard themes, without redundant HC dividers.'], + }), + Connected: defineComponentFixture({ + render: renderBorderOwnership(true, ModernUIEditorTabStyle.Connected), + themes: ['dark'], + additionalThemes: ['darkHighContrast', 'lightHighContrast'], + expectedVisualDescriptions: ['Connected tabs give the active cap boundary ownership and show dividers only between inactive tabs in standard themes.'], + }), + ConnectedDefault: defineComponentFixture({ + render: renderBorderOwnership(true, ModernUIEditorTabStyle.Connected, false), + themes: ['dark'], + expectedVisualDescriptions: ['Default Connected tabs use the same cap, shoulder, mask, and separator geometry as customized Connected tabs.'], + }), + ConnectedWrapped: defineComponentFixture({ + render: renderWrappedConnectedBorderOwnership(), + themes: ['dark'], + expectedVisualDescriptions: ['Upper wrapped tabs retain pill geometry with inset customized accents.'], + }), + ConnectedModernEditorTokens: defineComponentFixture({ + render: renderConnectedModernEditorTabCustomizations(), + themes: ['dark'], + expectedVisualDescriptions: ['Connected tabs honor every explicitly customized modernEditorTab fill, label, and action color.'], + }), + WrappedModernEditorTokens: defineComponentFixture({ + render: renderConnectedModernEditorTabCustomizations('wrapped'), + themes: ['dark'], + expectedVisualDescriptions: ['Upper wrapped tabs retain the explicitly customized hover action background independently of the tab hover background.'], + }), + PinnedModernEditorTokens: defineComponentFixture({ + render: renderConnectedModernEditorTabCustomizations('pinned'), + themes: ['dark'], + expectedVisualDescriptions: ['Separate pinned-row tabs retain the same hover action color customization as ordinary connected tabs.'], + }), + }), + SelectedBorderFallback: defineThemedFixtureGroup({ + ActiveGroup: defineComponentFixture({ + render: render(true, { editors: multiSelectEditorSpecs(), colorCustomizations: { [TAB_SELECTED_BORDER_TOP]: '#a3e635' } }), + additionalThemes: ['darkPlus'], + expectedVisualDescriptions: ['An explicitly customized selected top border appears on the active tab when the theme has no active top border, but does not override an existing active top border.'], + }), + InactiveGroup: defineComponentFixture({ + render: render(true, { active: false, editors: multiSelectEditorSpecs(), colorCustomizations: { [TAB_SELECTED_BORDER_TOP]: '#a3e635' } }), + additionalThemes: ['darkPlus'], + expectedVisualDescriptions: ['The unfocused active tab uses the explicit selected-border fallback only when its active top border is absent.'], + }), + }), + DerivedColors: defineComponentFixture({ + render: render(true, { active: false, colorCustomizations: { [TAB_BORDER]: '#22d3ee', [TAB_HOVER_BACKGROUND]: '#7c2d12' } }), + additionalThemes: ['darkPlus'], + expectedVisualDescriptions: ['Dividers inherit the customized tab border. Unfocused hover preserves an explicitly defined theme color or derives the translucent hover color when the theme leaves it unset.'], + }), + Continuity: defineThemedFixtureGroup({ + FirstActive: defineComponentFixture({ + render: renderConnectedBorderContinuity(0), + themes: ['dark'], + expectedVisualDescriptions: ['The first active connected tab joins the customized outer editor frame without a duplicate left edge.'], + }), + MiddleActive: defineComponentFixture({ + render: renderConnectedBorderContinuity(1), + themes: ['dark'], + expectedVisualDescriptions: ['The middle active connected tab has one continuous customized cap and document boundary.'], + }), + }), + }), Density: defineThemedFixtureGroup(createDensityFixtures()), FileIcons: defineThemedFixtureGroup({ Disabled: defineComponentFixture({ 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 51021561ab8185..5902991f2ee6b0 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -160,9 +160,11 @@ suite('MultiEditorTabsControl', () => { mainWindow.document.body.appendChild(root); disposables.add(toDisposable(() => root.remove())); const editor = $('.part.editor.editor-tabs-multiple'); + const gridView = $('.monaco-grid-view'); const content = $('.content'); const group = $('.editor-group-container.active'); - root.appendChild(editor); + root.appendChild(gridView); + gridView.appendChild(editor); editor.appendChild(content); content.appendChild(group); group.appendChild(container); @@ -514,7 +516,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 }, - horizontal: { clearance: [[6, 6, 6, 6], [6, 6, 6, 6], [3, 3, 3, 4], [4, 4, 4, 4]] }, + horizontal: { clearance: [[6, 6, 6, 6], [6, 6, 6, 6], [3, 4, 3, 5], [4, 4, 4, 4]] }, leftAction: { top: true, right: true, left: true, width: true }, balancedActionSurface: true, balancedActionInsets: true, @@ -593,7 +595,7 @@ suite('MultiEditorTabsControl', () => { ]); }); - test('close hover targets have equal vertical and trailing clearance at both tab densities', async () => { + test('close hover targets stay vertically centered and share trailing clearance across wrapped rows', async () => { const group = connectedGroup(); group.style.setProperty('--vscode-editorGroupHeader-tabsBorder', '#333333'); const measurements = []; @@ -624,10 +626,11 @@ suite('MultiEditorTabsControl', () => { : 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); + const trailing = (tabHeight === 'compact' ? 4 : 6) - (wrapTabs ? 2 : 0); + const clearance = trailing - (wrapTabs && activeIndex === 1 ? 1 : 0); expected.push({ tabHeight, tabActionLocation, wrapTabs, activeIndex, - top: clearance, bottom: clearance, trailing: clearance, + top: clearance, bottom: clearance, trailing, leftBorder: rowStart && !upperRow ? 'rgba(0, 0, 0, 0)' : undefined, }); } @@ -1131,28 +1134,35 @@ suite('MultiEditorTabsControl', () => { 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 }); + for (const tabHeight of ['default', 'compact'] as const) { + const oldOptions = partOptions; + partOptions = { ...partOptions, tabHeight }; + control.updateOptions(oldOptions, partOptions); + container.classList.toggle('compact-height', tabHeight === 'compact'); + await layoutConnectedGroup(group, 300); + const boundary = tabs[0].offsetWidth + tabs[1].offsetWidth; + const widths = Array.from({ length: 21 }, (_, index) => boundary - 10 + index); + 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({ tabHeight, width, activeIndex, index, expected, actual }); + } } } } @@ -1210,6 +1220,7 @@ suite('MultiEditorTabsControl', () => { test('wrapped fills have equal visible heights and the bottom tab reaches the document', async () => { const group = connectedGroup(); group.style.setProperty('--modern-ui-connected-tab-surface', '#ffffff'); + group.style.setProperty('--modern-ui-editor-tab-custom-border', '#22d3ee'); model.openEditor(model.getEditorByIndex(1)!, { active: true }); control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL)); const measurements = []; @@ -1223,6 +1234,7 @@ suite('MultiEditorTabsControl', () => { const tab = strip.querySelector('.tab.active')!; const fill = tab.querySelector('.tab-fill')!; const fillStyle = mainWindow.getComputedStyle(fill); + const shoulderStyle = mainWindow.getComputedStyle(fill, '::after'); const stripStyle = mainWindow.getComputedStyle(strip, '::after'); const stripBounds = strip.getBoundingClientRect(); const tabBounds = tab.getBoundingClientRect(); @@ -1239,6 +1251,8 @@ suite('MultiEditorTabsControl', () => { clippingGap: strip.getBoundingClientRect().bottom - clippingBottom, bottomRadius: fillStyle.borderBottomRightRadius, shoulder: mainWindow.getComputedStyle(fill, '::after').content, + shoulderStroke: [shoulderStyle.borderLeftColor, shoulderStyle.borderBottomColor], + shoulderBaselineOffset: fill.getBoundingClientRect().bottom - parseFloat(fillStyle.borderBottomWidth) - parseFloat(shoulderStyle.bottom) - (separatorTop + parseFloat(stripStyle.height)), tabFillHeights: fills.map((rect, index) => Math.min(rect.bottom, tabs[index].bottom) - rect.top), rowGap: fills[1].top - fills[0].bottom, overflow: Array.from(strip.querySelectorAll('.tabs-container, .monaco-scrollable-element'), element => mainWindow.getComputedStyle(element).overflow), @@ -1247,8 +1261,8 @@ suite('MultiEditorTabsControl', () => { }); } assert.deepStrictEqual(measurements, [ - { tabHeight: 'default', stripHeight: 61, wrapping: true, upperRow: false, gap: -1, clippingGap: 0, bottomRadius: '0px', shoulder: '""', tabFillHeights: [28, 28], rowGap: 2, overflow: ['visible', 'visible'], separatorOffset: 0, connectionOverlap: 1 }, - { tabHeight: 'compact', stripHeight: 53, wrapping: true, upperRow: false, gap: -1, clippingGap: 0, bottomRadius: '0px', shoulder: '""', tabFillHeights: [24, 24], rowGap: 2, overflow: ['visible', 'visible'], separatorOffset: 0, connectionOverlap: 1 }, + { tabHeight: 'default', stripHeight: 61, wrapping: true, upperRow: false, gap: -1, clippingGap: 0, bottomRadius: '0px', shoulder: '""', shoulderStroke: ['rgb(34, 211, 238)', 'rgb(34, 211, 238)'], shoulderBaselineOffset: 0, tabFillHeights: [28, 28], rowGap: 2, overflow: ['visible', 'visible'], separatorOffset: 0, connectionOverlap: 1 }, + { tabHeight: 'compact', stripHeight: 53, wrapping: true, upperRow: false, gap: -1, clippingGap: 0, bottomRadius: '0px', shoulder: '""', shoulderStroke: ['rgb(34, 211, 238)', 'rgb(34, 211, 238)'], shoulderBaselineOffset: 0, tabFillHeights: [24, 24], rowGap: 2, overflow: ['visible', 'visible'], separatorOffset: 0, connectionOverlap: 1 }, ]); }); diff --git a/test/componentFixtures/playwright/tests/editorTabBar.spec.ts b/test/componentFixtures/playwright/tests/editorTabBar.spec.ts index 37102bf383608a..9bee3ff63bfe6b 100644 --- a/test/componentFixtures/playwright/tests/editorTabBar.spec.ts +++ b/test/componentFixtures/playwright/tests/editorTabBar.spec.ts @@ -44,6 +44,518 @@ test('Dark Modern keeps legacy, connected, and pill tab surfaces distinct', asyn }); }); +test('Connected defaults do not surface the theme active-top accent', async ({ page }) => { + await openFixture(page, 'editor/tabs/TabStyles/Connected/Dark', '.tabs-container > .tab.active'); + const colors = await page.locator('.editor-group-container').evaluate(group => { + const activeFill = group.querySelector('.tab.active > .tab-fill'); + if (!activeFill) { + throw new Error('Expected an active connected tab fill'); + } + const groupStyle = getComputedStyle(group); + const leftShoulder = getComputedStyle(activeFill, '::before'); + return { + capTop: getComputedStyle(activeFill).borderTopColor, + structuralBoundary: groupStyle.getPropertyValue('--modern-ui-connected-tab-border').trim(), + themeActiveTop: getComputedStyle(group.closest('.monaco-workbench')!).getPropertyValue('--vscode-tab-activeBorderTop').trim(), + shoulder: { + bottom: leftShoulder.bottom, + height: leftShoulder.height, + radius: leftShoulder.borderBottomRightRadius, + color: leftShoulder.borderBottomColor, + }, + }; + }); + expect(colors).toEqual({ + capTop: 'rgb(42, 43, 44)', + structuralBoundary: '#2a2b2c', + themeActiveTop: '#3994bc', + shoulder: { + bottom: '0px', + height: '7px', + radius: '7px', + color: 'rgb(42, 43, 44)', + }, + }); +}); + +for (const [group, expected] of [ + ['ActiveGroup', { + activeTop: { indicator: 'block', capColor: 'rgb(250, 204, 21)', accentColor: 'rgb(34, 211, 238)' }, + activeBottom: { display: 'block', color: 'rgb(244, 63, 94)' }, + activeSide: 'rgb(250, 204, 21)', + selectedTop: { display: 'block', color: 'rgb(163, 230, 53)', height: 2, leftInset: 2, rightInset: 2 }, + selectedBorder: 'rgba(0, 0, 0, 0)', + }], + ['InactiveGroup', { + activeTop: { indicator: 'block', capColor: 'rgb(250, 204, 21)', accentColor: 'rgb(192, 132, 252)' }, + activeBottom: { display: 'block', color: 'rgb(251, 146, 60)' }, + activeSide: 'rgb(250, 204, 21)', + selectedTop: { display: 'block', color: 'rgb(163, 230, 53)', height: 2, leftInset: 2, rightInset: 2 }, + selectedBorder: 'rgba(0, 0, 0, 0)', + }], +] as const) { + test(`connected tabs show legacy border customizations in ${group}`, async ({ page }) => { + await openFixture(page, `editor/tabs/Colors/ConnectedLegacyBorders/${group}/Dark`, '.tabs-container > .tab.active'); + + const colors = await page.locator('.tabs-container').evaluate(tabs => { + const active = tabs.querySelector('.tab.active'); + const selected = tabs.querySelector('.tab.selected:not(.active)'); + const activeTop = active?.querySelector('.tab-border-top-container'); + const activeBottom = active?.querySelector('.tab-border-bottom-container'); + const activeFill = active?.querySelector('.tab-fill'); + const selectedTop = selected?.querySelector('.tab-border-top-container'); + const selectedFill = selected?.querySelector('.tab-fill'); + if (!activeTop || !activeBottom || !activeFill || !selectedTop || !selectedFill) { + throw new Error('Expected active and selected connected-tab border indicators'); + } + const style = (element: HTMLElement) => { + const computedStyle = getComputedStyle(element); + return { display: computedStyle.display, color: computedStyle.backgroundColor }; + }; + return { + activeTop: { + indicator: getComputedStyle(activeTop).display, + capColor: getComputedStyle(activeFill).borderTopColor, + accentColor: getComputedStyle(activeTop).backgroundColor, + }, + activeBottom: style(activeBottom), + activeSide: getComputedStyle(activeFill).borderRightColor, + selectedTop: { + ...style(selectedTop), + height: selectedTop.getBoundingClientRect().height, + leftInset: selectedTop.getBoundingClientRect().left - selectedFill.getBoundingClientRect().left, + rightInset: selectedFill.getBoundingClientRect().right - selectedTop.getBoundingClientRect().right, + }, + selectedBorder: getComputedStyle(selectedFill).borderRightColor, + }; + }); + + expect(colors).toEqual(expected); + }); +} + +for (const [style, expected] of [ + ['Legacy', { + topIndicator: { display: 'block', color: 'rgb(34, 211, 238)' }, + bottomIndicator: { display: 'block', color: 'rgb(244, 63, 94)' }, + visibleDividers: [], + }], + ['Pill', { + topIndicator: { display: 'none' }, + bottomIndicator: { display: 'none' }, + fillTop: 'rgb(250, 204, 21)', + fillBottom: 'rgb(250, 204, 21)', + fillSide: 'rgb(250, 204, 21)', + topAccent: 'rgb(34, 211, 238)', + bottomAccent: 'rgb(244, 63, 94)', + visibleDividers: Array(7).fill('rgb(255, 255, 255)'), + }], + ['Connected', { + topIndicator: { display: 'block', color: 'rgb(34, 211, 238)' }, + bottomIndicator: { display: 'block', color: 'rgb(244, 63, 94)' }, + fillTop: 'rgb(250, 204, 21)', + fillBottom: 'rgba(0, 0, 0, 0)', + fillSide: 'rgb(250, 204, 21)', + inactiveBorder: 'rgba(0, 0, 0, 0)', + visibleDividers: Array(5).fill('rgb(255, 255, 255)'), + }], +] as const) { + test(`${style} tabs retain their border ownership`, async ({ page }) => { + await openFixture(page, `editor/tabs/Colors/BorderOwnership/${style}/Dark`, '.tabs-container > .tab.active'); + const ownership = await page.locator('.tabs-container > .tab.active').evaluate(active => { + const top = active.querySelector('.tab-border-top-container'); + const bottom = active.querySelector('.tab-border-bottom-container'); + const fill = active.querySelector('.tab-fill'); + if (!top || !bottom || !fill) { + throw new Error('Expected active tab border elements'); + } + const fillStyle = getComputedStyle(fill); + const inactiveFill = active.parentElement!.querySelector('.tab:not(.active) > .tab-fill'); + if (!inactiveFill) { + throw new Error('Expected an inactive tab fill'); + } + const visibleDividers = [...active.parentElement!.querySelectorAll('.tab-divider')] + .filter(element => getComputedStyle(element).display !== 'none') + .map(element => getComputedStyle(element).backgroundColor); + const tabsAndActions = active.closest('.tabs-and-actions-container'); + if (!tabsAndActions) { + throw new Error('Expected tabs and actions container'); + } + const indicatorStyle = (element: HTMLElement) => { + const style = getComputedStyle(element); + return { display: style.display, color: style.backgroundColor }; + }; + return { + topIndicator: indicatorStyle(top), + bottomIndicator: indicatorStyle(bottom), + fillTop: fillStyle.borderTopColor, + fillBottom: fillStyle.borderBottomColor, + fillSide: fillStyle.borderRightColor, + inactiveBorder: getComputedStyle(inactiveFill).borderRightColor, + topAccent: getComputedStyle(fill, '::before').backgroundColor, + bottomAccent: getComputedStyle(fill, '::after').backgroundColor, + bottomBoundaryOffset: bottom.getBoundingClientRect().bottom - tabsAndActions.getBoundingClientRect().bottom, + visibleDividers, + }; + }); + expect(ownership).toMatchObject(expected); + if (style === 'Connected') { + expect(ownership.bottomBoundaryOffset).toBe(0); + } + }); +} + +test('default and customized connected tabs share identical geometry', async ({ page }) => { + const readGeometry = async (fixture: string) => { + await openFixture(page, `editor/tabs/Colors/BorderOwnership/${fixture}/Dark`, '.tabs-container > .tab.active'); + return page.locator('.tabs-container > .tab.active').evaluate(active => { + const fill = active.querySelector('.tab-fill'); + const edge = active.querySelector('.tab-connected-edge'); + const strip = active.closest('.tabs-and-actions-container'); + if (!fill || !edge || !strip) { + throw new Error('Expected connected cap geometry'); + } + const activeRect = active.getBoundingClientRect(); + const fillRect = fill.getBoundingClientRect(); + const edgeRect = edge.getBoundingClientRect(); + const fillStyle = getComputedStyle(fill); + const leftShoulder = getComputedStyle(fill, '::before'); + const rightShoulder = getComputedStyle(fill, '::after'); + const leftMask = getComputedStyle(edge, '::before'); + return { + fillInsets: [ + fillRect.left - activeRect.left, + fillRect.top - activeRect.top, + activeRect.right - fillRect.right, + fillRect.bottom - activeRect.bottom, + ], + edgeInsets: [ + edgeRect.left - activeRect.left, + edgeRect.top - activeRect.top, + activeRect.right - edgeRect.right, + edgeRect.bottom - activeRect.bottom, + ], + capRadius: [fillStyle.borderTopLeftRadius, fillStyle.borderTopRightRadius], + shoulders: [ + [leftShoulder.bottom, leftShoulder.width, leftShoulder.height, leftShoulder.borderBottomRightRadius], + [rightShoulder.bottom, rightShoulder.width, rightShoulder.height, rightShoulder.borderBottomLeftRadius], + ], + mask: [leftMask.bottom, leftMask.width, leftMask.height], + separator: [getComputedStyle(strip, '::after').bottom, getComputedStyle(strip, '::after').height], + }; + }); + }; + + expect(await readGeometry('Connected')).toEqual(await readGeometry('ConnectedDefault')); +}); + +for (const [theme, expected] of [ + ['DarkHighContrast', { activeTop: 'rgb(243, 133, 24)', accent: 'rgb(243, 133, 24)', tabBorder: 'rgb(111, 195, 223)' }], + ['LightHighContrast', { activeTop: 'rgb(0, 107, 189)', accent: 'rgb(0, 107, 189)', tabBorder: 'rgb(15, 74, 133)' }], +] as const) { + test(`pill borders retain high contrast ownership in ${theme}`, async ({ page }) => { + await openFixture(page, `editor/tabs/Colors/BorderOwnership/Pill/${theme}`, '.tabs-container > .tab.active'); + const ownership = await page.locator('.tabs-container').evaluate(tabs => { + const activeFill = tabs.querySelector('.tab.active > .tab-fill'); + const inactiveFill = tabs.querySelector('.tab:not(.active) > .tab-fill'); + if (!activeFill || !inactiveFill) { + throw new Error('Expected active and inactive pill tab fills'); + } + return { + active: { + top: getComputedStyle(activeFill).borderTopColor, + side: getComputedStyle(activeFill).borderRightColor, + }, + inactive: { + top: getComputedStyle(inactiveFill).borderTopColor, + side: getComputedStyle(inactiveFill).borderRightColor, + }, + visibleDividers: [...tabs.querySelectorAll('.tab-divider')] + .filter(element => getComputedStyle(element).display !== 'none').length, + }; + }); + expect(ownership).toEqual({ + active: { top: expected.accent, side: expected.accent }, + inactive: { top: expected.tabBorder, side: expected.tabBorder }, + visibleDividers: 0, + }); + }); + + test(`connected borders retain high contrast ownership in ${theme}`, async ({ page }) => { + await openFixture(page, `editor/tabs/Colors/BorderOwnership/Connected/${theme}`, '.tabs-container > .tab.active'); + const ownership = await page.locator('.editor-group-container').evaluate(group => { + const activeFill = group.querySelector('.tab.active > .tab-fill'); + const inactiveFill = group.querySelector('.tab:not(.active):not(:first-child) > .tab-fill'); + const firstFill = group.querySelector('.tab:first-child > .tab-fill'); + if (!activeFill || !inactiveFill || !firstFill) { + throw new Error('Expected active and inactive connected tab fills'); + } + + const activeStyle = getComputedStyle(activeFill); + const inactiveStyle = getComputedStyle(inactiveFill); + const visibleDividers = [...group.querySelectorAll('.tab-divider')] + .filter(element => getComputedStyle(element).display !== 'none') + .map(element => getComputedStyle(element).backgroundColor); + return { + active: { + top: activeStyle.borderTopColor, + side: activeStyle.borderRightColor, + bottom: activeStyle.borderBottomColor, + }, + inactive: { + top: inactiveStyle.borderTopColor, + side: inactiveStyle.borderRightColor, + }, + firstLeft: getComputedStyle(firstFill).borderLeftColor, + frame: getComputedStyle(group, '::after').borderColor, + visibleDividers, + }; + }); + expect(ownership).toEqual({ + active: { + top: expected.activeTop, + side: expected.accent, + bottom: 'rgba(0, 0, 0, 0)', + }, + inactive: { + top: expected.tabBorder, + side: expected.tabBorder, + }, + firstLeft: expected.tabBorder, + frame: expected.accent, + visibleDividers: [], + }); + }); +} + +test('wrapped upper connected tabs inset customized border accents', async ({ page }) => { + await openFixture(page, 'editor/tabs/Colors/BorderOwnership/ConnectedWrapped/Dark', '.tabs-container > .tab.active.connected-tab-upper-row'); + const ownership = await page.locator('.tab.active.connected-tab-upper-row').evaluate(active => { + const fill = active.querySelector('.tab-fill'); + const top = active.querySelector('.tab-border-top-container'); + const bottom = active.querySelector('.tab-border-bottom-container'); + if (!fill || !top || !bottom) { + throw new Error('Expected wrapped active tab border elements'); + } + const bottomAccent = getComputedStyle(fill, '::after'); + const fillStyle = getComputedStyle(fill); + const availableWidth = fill.getBoundingClientRect().width + - Number.parseFloat(fillStyle.borderLeftWidth) + - Number.parseFloat(fillStyle.borderRightWidth) + - Number.parseFloat(bottomAccent.left) + - Number.parseFloat(bottomAccent.right); + return { + topIndicator: getComputedStyle(top).display, + topColor: getComputedStyle(fill).borderTopColor, + bottomIndicator: getComputedStyle(bottom).display, + bottomAccent: { + color: bottomAccent.backgroundColor, + left: bottomAccent.left, + right: bottomAccent.right, + height: bottomAccent.height, + spansAvailableWidth: Math.abs(Number.parseFloat(bottomAccent.width) - availableWidth) < 0.1, + }, + }; + }); + expect(ownership).toEqual({ + topIndicator: 'none', + topColor: 'rgb(34, 211, 238)', + bottomIndicator: 'none', + bottomAccent: { + color: 'rgb(244, 63, 94)', + left: '4px', + right: '4px', + height: '1px', + spansAvailableWidth: true, + }, + }); +}); + +test('wrapped upper connected hover borders use focused and unfocused inset accents', async ({ page }) => { + await openFixture(page, 'editor/tabs/Colors/BorderOwnership/ConnectedWrapped/Dark', '.tabs-container > .tab.active.connected-tab-upper-row'); + const hovered = page.locator('.tabs-container > .tab.connected-tab-upper-row:not(.active)').first(); + await hovered.hover(); + const readOwnership = () => hovered.evaluate(tab => { + const fill = tab.querySelector('.tab-fill'); + const bottom = tab.querySelector('.tab-border-bottom-container'); + if (!fill || !bottom) { + throw new Error('Expected hovered wrapped tab border elements'); + } + const accent = getComputedStyle(fill, '::after'); + const fillStyle = getComputedStyle(fill); + const availableWidth = fill.getBoundingClientRect().width + - Number.parseFloat(fillStyle.borderLeftWidth) + - Number.parseFloat(fillStyle.borderRightWidth) + - Number.parseFloat(accent.left) + - Number.parseFloat(accent.right); + return { + fillBottom: getComputedStyle(fill).borderBottomColor, + indicator: getComputedStyle(bottom).display, + accent: { + color: accent.backgroundColor, + left: accent.left, + right: accent.right, + height: accent.height, + spansAvailableWidth: Math.abs(Number.parseFloat(accent.width) - availableWidth) < 0.1, + }, + }; + }); + expect(await readOwnership()).toEqual({ + fillBottom: 'rgba(0, 0, 0, 0)', + indicator: 'none', + accent: { + color: 'rgb(249, 115, 22)', + left: '4px', + right: '4px', + height: '1px', + spansAvailableWidth: true, + }, + }); + await page.locator('.editor-group-container').evaluate(group => group.classList.remove('active')); + expect(await readOwnership()).toEqual({ + fillBottom: 'rgba(0, 0, 0, 0)', + indicator: 'none', + accent: { + color: 'rgb(168, 85, 247)', + left: '4px', + right: '4px', + height: '1px', + spansAvailableWidth: true, + }, + }); +}); + +test('connected tabs honor all modern editor tab customizations', async ({ page }) => { + await openFixture(page, 'editor/tabs/Colors/BorderOwnership/ConnectedModernEditorTokens/Dark', '.tabs-container > .tab.active'); + const tabs = page.locator('.tabs-container'); + const active = tabs.locator('> .tab.active'); + const inactive = tabs.locator('> .tab:not(.active):not(.selected)').first(); + const selected = tabs.locator('> .tab.selected:not(.active)').first(); + const readTab = (tab: typeof active) => tab.evaluate(element => { + const fill = element.querySelector('.tab-fill'); + const label = element.querySelector('.tab-label a'); + const actions = element.querySelector('.tab-actions'); + if (!fill || !label || !actions) { + throw new Error('Expected tab fill, label, and actions'); + } + return { + background: getComputedStyle(fill).backgroundColor, + foreground: getComputedStyle(label).color, + actionBackground: getComputedStyle(actions).backgroundColor, + }; + }); + + await page.locator('.editor-container').hover(); + await active.locator('.action-label').focus(); + expect(await readTab(active)).toEqual({ + background: 'rgb(22, 78, 99)', + foreground: 'rgb(207, 250, 254)', + actionBackground: 'rgb(14, 55, 71)', + }); + + await active.hover(); + expect(await readTab(active)).toEqual({ + background: 'rgb(107, 33, 168)', + foreground: 'rgb(207, 250, 254)', + actionBackground: 'rgb(76, 22, 120)', + }); + + await inactive.hover(); + expect(await readTab(inactive)).toEqual({ + background: 'rgb(124, 45, 18)', + foreground: 'rgb(255, 237, 213)', + actionBackground: 'rgb(90, 31, 12)', + }); + + await selected.hover(); + expect((await readTab(selected)).actionBackground).toBe('rgb(22, 101, 52)'); + + await page.locator('.editor-group-container').evaluate(group => group.classList.remove('active')); + await active.hover(); + expect(await readTab(active)).toEqual({ + background: 'rgb(107, 33, 168)', + foreground: 'rgb(207, 250, 254)', + actionBackground: 'rgb(76, 22, 120)', + }); + await inactive.hover(); + expect(await readTab(inactive)).toEqual({ + background: 'rgb(124, 45, 18)', + foreground: 'rgb(255, 237, 213)', + actionBackground: 'rgb(90, 31, 12)', + }); +}); + +for (const [fixture, expected] of [ + ['FirstActive', { + capLeft: 'rgba(0, 0, 0, 0)', + capLeftWidth: '0px', + dividers: ['rgba(0, 0, 0, 0)'], + }], + ['MiddleActive', { + capLeft: 'rgb(34, 211, 238)', + capLeftWidth: '1px', + dividers: [], + }], +] as const) { + test(`connected border continuity stays aligned for ${fixture}`, async ({ page }) => { + await openFixture(page, `editor/tabs/Colors/Continuity/${fixture}/Dark`, '.tabs-container > .tab.active'); + const state = await page.locator('.part.editor').evaluate(editor => { + const group = editor.querySelector('.editor-group-container.active'); + const active = group?.querySelector('.tab.active'); + const fill = active?.querySelector('.tab-fill'); + const indicator = active?.querySelector('.tab-border-top-container'); + const strip = group?.querySelector('.tabs-and-actions-container'); + const body = group?.querySelector('.editor-container'); + if (!group || !active || !fill || !indicator || !strip || !body) { + throw new Error('Expected connected editor frame and active tab'); + } + const editorRect = editor.getBoundingClientRect(); + const fillRect = fill.getBoundingClientRect(); + const bodyRect = body.getBoundingClientRect(); + const fillStyle = getComputedStyle(fill); + const leftShoulder = getComputedStyle(fill, '::before'); + const rightShoulder = getComputedStyle(fill, '::after'); + const visibleDividers = [...group.querySelectorAll('.tab-divider')] + .filter(element => getComputedStyle(element).display !== 'none') + .map(element => getComputedStyle(element).backgroundColor); + return { + editorBorder: getComputedStyle(editor).borderTopColor, + capTop: getComputedStyle(fill).borderTopColor, + capLeft: fillStyle.borderLeftColor, + capLeftWidth: fillStyle.borderLeftWidth, + capSide: getComputedStyle(fill).borderRightColor, + separator: getComputedStyle(strip, '::after').backgroundColor, + indicator: getComputedStyle(indicator).display, + topAligned: editorRect.top === fillRect.top, + bodyOverlap: fillRect.bottom - bodyRect.top, + frameInsets: [bodyRect.left - editorRect.left, editorRect.right - bodyRect.right], + shoulderTangents: { + left: [leftShoulder.bottom, leftShoulder.height, leftShoulder.borderBottomRightRadius, leftShoulder.borderBottomColor], + right: [rightShoulder.bottom, rightShoulder.height, rightShoulder.borderBottomLeftRadius, rightShoulder.borderBottomColor], + }, + visibleDividers, + }; + }); + expect(state).toEqual({ + editorBorder: 'rgb(34, 211, 238)', + capTop: 'rgb(34, 211, 238)', + capLeft: expected.capLeft, + capLeftWidth: expected.capLeftWidth, + capSide: 'rgb(34, 211, 238)', + separator: 'rgb(34, 211, 238)', + indicator: 'block', + topAligned: true, + bodyOverlap: 1, + frameInsets: [1, 1], + shoulderTangents: { + left: ['0px', '7px', '7px', 'rgb(34, 211, 238)'], + right: ['0px', '7px', '7px', 'rgb(34, 211, 238)'], + }, + visibleDividers: expected.dividers, + }); + }); +} + for (const theme of ['DarkHighContrast', 'LightHighContrast']) { test(`connected tab actions respect disabled hover state in ${theme}`, async ({ page }) => { await openFixture(page, `editor/tabs/Layout/SingleEditor/${theme}`, '.tabs-container > .tab');