diff --git a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css index 55933caa1a4cb5..923634e83abcca 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/connectedEditorTabs.css @@ -378,9 +378,13 @@ content: none; } -/* Terminal shoulders turn inside the available slot rather than being cut off. */ -.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active:not(.connected-tab-upper-row):is(:last-child, .last-in-row, .connected-tab-right-edge) > :is(.tab-fill, .tab-connected-edge) { - right: var(--modern-ui-connected-tab-shoulder-radius); +/* Terminal shoulders keep additional space rather than being cut off. */ +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.active:not(.connected-tab-upper-row):is(:last-child, .last-in-row) { + margin-right: var(--modern-ui-connected-tab-shoulder-radius); +} + +.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child { + margin-right: calc(var(--last-tab-margin-right) + var(--modern-ui-connected-tab-shoulder-radius)); } .modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element > .tab-connected-overflow-edge.connected-tab-right-clipped::after { diff --git a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css index 8327533709823c..1046eee3480cb2 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/tabs.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/tabs.css @@ -82,8 +82,8 @@ padding-left: var(--vscode-spacing-size80) !important; } -.modern-ui-tabs .part.editor .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child { - margin-right: calc(var(--last-tab-margin-right) + var(--vscode-spacing-size40)) !important; +.modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child { + margin-right: calc(var(--last-tab-margin-right) + var(--vscode-spacing-size40)); } .modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab { 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 c62e0add89f24f..e045309322c363 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 @@ -2273,7 +2273,7 @@ suite('ModernUIContribution', () => { } }); - test('paints connected tab strokes outside the fill without moving tab content', () => { + test('reserves the connected terminal shoulder without moving tab content', () => { const root = document.createElement('div'); root.style.setProperty('--vscode-spacing-size20', '2px'); root.style.setProperty('--vscode-spacing-size40', '4px'); @@ -2313,6 +2313,8 @@ suite('ModernUIContribution', () => { tab.classList.add('active'); const activeFillBounds = fill.getBoundingClientRect(); const activeFillStyle = targetWindow.getComputedStyle(fill); + const shoulderWidth = targetWindow.getComputedStyle(fill, '::after').width; + const activeTabStyle = targetWindow.getComputedStyle(tab); assert.deepStrictEqual({ tabBounds: tab.getBoundingClientRect().toJSON(), @@ -2322,11 +2324,13 @@ suite('ModernUIContribution', () => { fillBounds.left - activeFillBounds.left, activeFillBounds.right - fillBounds.right, ], + marginRight: activeTabStyle.marginRight, topRadius: activeFillStyle.borderTopLeftRadius, }, { tabBounds: tabBounds.toJSON(), labelBounds: labelBounds.toJSON(), - fillExpansion: [0, 0, connected ? -5 : 0], + fillExpansion: [0, 0, 0], + marginRight: connected ? shoulderWidth : '0px', topRadius: connected ? '5px' : '4px', }, JSON.stringify({ classes, theme, activeGroup, compact })); } diff --git a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts index 97b5400cb6b5c7..f1a1b16a5b8545 100644 --- a/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/multiEditorTabsControl.test.ts @@ -395,7 +395,7 @@ suite('MultiEditorTabsControl', () => { await new Promise(resolve => disposables.add(scheduleAtNextAnimationFrame(mainWindow, () => resolve()))); }; const results = []; - for (const { width, from } of [{ width: 240, from: 3 }, { width: 167, from: 0 }, { width: 120, from: 0 }]) { + for (const { width, from } of [{ width: 240, from: 3 }, { width: 172, from: 0 }, { width: 120, from: 0 }]) { await reveal(from, width); await reveal(1, width); const tab = container.querySelector('.tab.active')!; @@ -418,7 +418,7 @@ suite('MultiEditorTabsControl', () => { }, { results: [ { width: 240, leftShoulderVisible: true, rightShoulderVisible: true }, - { width: 167, leftShoulderVisible: true, rightShoulderVisible: true }, + { width: 172, leftShoulderVisible: true, rightShoulderVisible: true }, { width: 120, leftShoulderVisible: true, rightShoulderVisible: false }, ], firstTabFlush: true, @@ -472,7 +472,7 @@ suite('MultiEditorTabsControl', () => { assert.deepStrictEqual(results, [4, 3, 2].map(count => ({ clampedBeforeLayout: true, activeIndex: count - 1, - scrollLeft: count * 160 - 200, + scrollLeft: count * 160 - 200 + 5, fillLeft: (count - 1) * 160, hidden: false, fillDisplay: 'block', @@ -518,6 +518,30 @@ suite('MultiEditorTabsControl', () => { assert.deepStrictEqual({ wrapped, upper, unwrapped }, { wrapped: [true, false], upper: { inset: '-2px', shoulder: 'none' }, unwrapped: [false, false] }); }); + test('connected wrapped last tab adds its shoulder to the editor actions margin', async () => { + const group = connectedGroup(); + const oldOptions = partOptions; + partOptions = { ...partOptions, wrapTabs: true, tabSizing: 'fixed', tabSizingFixedMinWidth: 120, tabSizingFixedMaxWidth: 120, editorActionsLocation: 'hidden' }; + control.updateOptions(oldOptions, partOptions); + + await layoutConnectedGroup(group, 150); + const tabsAndActionsContainer = container.querySelector('.tabs-and-actions-container')!; + const tabsContainer = container.querySelector('.tabs-container')!; + tabsContainer.style.setProperty('--last-tab-margin-right', '17px'); + tabsContainer.style.setProperty('--modern-ui-connected-tab-shoulder-radius', '5px'); + const lastTab = tabsContainer.querySelector('.tab:last-child')!; + + assert.deepStrictEqual({ + wrapping: tabsAndActionsContainer.classList.contains('wrapping'), + active: lastTab.classList.contains('active'), + margin: mainWindow.getComputedStyle(lastTab).marginRight, + }, { + wrapping: true, + active: false, + margin: '22px', + }); + }); + test('selected wrapped tabs and focused actions use the document surface on every row', async () => { const group = connectedGroup(); const root = group.closest('.monaco-workbench')!; @@ -918,7 +942,7 @@ suite('MultiEditorTabsControl', () => { }); }); - test('refreshes connected clipping geometry after dirty width changes', async () => { + test('invalidates connected clipping geometry after dirty and capability changes', async () => { const root = $('.monaco-workbench.modern-ui.modern-ui-tabs.modern-ui-connected-editor-tabs'); root.style.cssText = '--vscode-spacing-size20: 2px; --vscode-spacing-size40: 4px; --vscode-spacing-size60: 6px; --vscode-spacing-size80: 8px; --vscode-spacing-size280: 28px; --vscode-strokeThickness: 1px; --vscode-cornerRadius-small: 4px; --vscode-editor-background: #ffffff; --modern-ui-connected-tab-surface: #333333;'; mainWindow.document.body.appendChild(root); @@ -943,49 +967,33 @@ suite('MultiEditorTabsControl', () => { const tabs = container.querySelector('.tabs-container')!; const [firstTab, activeTab] = tabs.querySelectorAll('.tab'); - const activeFill = activeTab.querySelector('.tab-fill')!; const overflowEdge = container.querySelector('.tab-connected-overflow-edge')!; const scroll = (left: number) => { tabs.classList.add('scroll'); tabs.scrollLeft = left; tabs.dispatchEvent(new UIEvent(EventType.SCROLL)); }; - const getLogicalFillRight = () => activeFill.getBoundingClientRect().right - tabs.getBoundingClientRect().left + tabs.scrollLeft; scroll(0); - const cleanFillRight = getLogicalFillRight(); const firstEditor = model.getEditorByIndex(0) as TestFileEditorInput; firstEditor.setDirty(); control.updateEditorDirty(firstEditor); - const dirtyFillRight = getLogicalFillRight(); const invalidatedBeforeLayout = overflowEdge.style.left === '' && !activeTab.classList.contains('connected-tab-right-edge'); await new Promise(resolve => disposables.add(scheduleAtNextAnimationFrame(mainWindow, () => resolve()))); - const shoulderExtent = Number.parseFloat(mainWindow.getComputedStyle(activeFill, '::after').width); - const targetVisibleRight = (cleanFillRight + dirtyFillRight) / 2 + shoulderExtent; - scroll(targetVisibleRight - tabs.clientWidth); - const visibleRight = tabs.scrollLeft + tabs.clientWidth; - const rightEdge = activeTab.classList.contains('connected-tab-right-edge'); - const currentGeometryNeedsEdge = dirtyFillRight + shoulderExtent > visibleRight; - const staleGeometryWouldNeedEdge = cleanFillRight + shoulderExtent > visibleRight; + const rebuiltAfterLayout = overflowEdge.style.left !== ''; firstEditor.capabilities = EditorInputCapabilities.CannotClose; control.updateEditorCapabilities(firstEditor); const capabilityUpdateInvalidated = overflowEdge.style.left === '' && !activeTab.classList.contains('connected-tab-right-edge'); assert.deepStrictEqual({ firstTabDirty: firstTab.classList.contains('dirty'), - widthIncreased: dirtyFillRight > cleanFillRight, invalidatedBeforeLayout, - rightEdge, - currentGeometryNeedsEdge, - staleGeometryWouldNeedEdge, + rebuiltAfterLayout, capabilityUpdateInvalidated, }, { firstTabDirty: true, - widthIncreased: true, invalidatedBeforeLayout: true, - rightEdge: true, - currentGeometryNeedsEdge: true, - staleGeometryWouldNeedEdge: false, + rebuiltAfterLayout: true, capabilityUpdateInvalidated: true, }); });