Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow tabs to wrap to multi-line #106448

Merged
merged 54 commits into from
Jan 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
a64e0f6
Allow tabs to wrap to multi-line
jmannanc Sep 9, 2020
b52a17a
Merge branch 'master' into wrap-tabs
bpasero Sep 11, 2020
8139024
Address feedback
jmannanc Sep 11, 2020
34dd6cf
Add hidden space after last tab
jmannanc Sep 15, 2020
717ab2d
Merge branch 'master' into wrap-tabs
bpasero Sep 16, 2020
417e97b
some polish for multi-line wrap css class
bpasero Sep 16, 2020
c290d7c
some more polish
bpasero Sep 16, 2020
f0fed35
Merge branch 'master' into wrap-tabs
jmannanc Sep 16, 2020
377a74a
Address feedback
jmannanc Sep 16, 2020
f85e5db
Merge branch 'master' into wrap-tabs
bpasero Sep 18, 2020
e6f1288
some adjustments to move forward
bpasero Sep 18, 2020
318f298
add clarifying comment to tabs layout
bpasero Sep 18, 2020
e6974fd
Fix editor container height
jmannanc Sep 18, 2020
3a3d7f0
WIP - overflowing tabs
jmannanc Sep 18, 2020
4dd752a
Merge branch 'master' into wrap-tabs
bpasero Sep 21, 2020
7df82ec
Merge branch 'master' into wrap-tabs
bpasero Sep 21, 2020
35fdc27
fix getPreferredHeight()
bpasero Sep 21, 2020
c1905a3
Merge remote-tracking branch 'upstream/master' into wrap-tabs
bpasero Sep 22, 2020
4cb7e49
Fix editor drop target for multi-line tabs
jmannanc Sep 22, 2020
8b34eca
Add comments and remove !important
jmannanc Sep 22, 2020
ae05859
fix dnd offset
bpasero Oct 4, 2020
ac0a024
Rework layout algorithm
jmannanc Oct 4, 2020
b5967e5
Make layout return a Dimension
jmannanc Oct 6, 2020
5f7a1aa
WIP - set maxDimensions
jmannanc Oct 7, 2020
df0d4a4
Layout multi-line tabs synchronously
jmannanc Oct 7, 2020
5979d0a
Merge branch 'master' into wrap-tabs
bpasero Oct 13, 2020
9147044
make sure dimensions are always defined and passed down to where needed
bpasero Oct 13, 2020
5835dfa
Merge branch 'master' into wrap-tabs
jmannanc Oct 26, 2020
2be28a8
Rework group.relayout and store lastComputedHeight
jmannanc Nov 2, 2020
8e07e4e
Merge branch 'master' into wrap-tabs
bpasero Dec 4, 2020
8acace5
fix breadcrumbs causing editor to disappear
bpasero Dec 5, 2020
69d6785
Merge branch 'master' into wrap-tabs
bpasero Dec 8, 2020
7222eaa
Merge branch 'master' into wrap-tabs
bpasero Dec 8, 2020
d2893f9
consolidate css rules
bpasero Dec 9, 2020
927dba7
rename setting
bpasero Dec 9, 2020
6e8618b
simplify classes
bpasero Dec 9, 2020
a88d9f1
streamline relayout
bpasero Dec 9, 2020
4bf1319
wrapTabs => experimentalWrapTabs
bpasero Dec 9, 2020
ba683f1
tweak layout
bpasero Dec 9, 2020
a1d21eb
Limit wrapped tabs to 3 rows
jmannanc Dec 15, 2020
d9fc730
Merge branch 'master' into wrap-tabs
bpasero Dec 16, 2020
11ea179
Only use flex-grow for `tabSizing: fit`
jmannanc Dec 16, 2020
d595a75
Merge branch 'master' into wrap-tabs
bpasero Dec 18, 2020
fe75f6c
fix scrollbar reveal to work properly
bpasero Dec 18, 2020
e69e7c3
tabs - get rid of sync layout
bpasero Dec 18, 2020
4585b70
WIP: Move editor actions to the bottom right
jmannanc Dec 20, 2020
b4c809d
some tweaks
bpasero Dec 27, 2020
a53fe3c
introduce css variable for margin-right trick
bpasero Dec 27, 2020
91c6333
add border to separate tabs when wrapping
bpasero Dec 27, 2020
46bdb34
:lipstick:
bpasero Dec 27, 2020
c853f2b
Merge branch 'master' into wrap-tabs
bpasero Dec 27, 2020
4f948dc
Merge branch 'master' into wrap-tabs
bpasero Jan 3, 2021
ff98891
rename setting
bpasero Jan 4, 2021
e6ee5f6
:lipstick: layout method
bpasero Jan 4, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@

.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container {
display: flex;
}

.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom {
position: relative;
position: relative; /* position tabs border bottom or editor actions (when tabs wrap) relative to this container */
}

.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom::after {
Expand Down Expand Up @@ -77,6 +74,13 @@
overflow: scroll !important;
}

.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container {

/* Enable wrapping via flex layout and dynamic height */
height: auto;
flex-wrap: wrap;
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container::-webkit-scrollbar {
display: none; /* Chrome + Safari: hide scrollbar */
}
Expand All @@ -93,6 +97,10 @@
padding-left: 10px;
}

.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child {
margin-right: var(--last-tab-margin-right); /* when tabs wrap, we need a margin away from the absolute positioned editor actions */
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-right,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-off:not(.sticky-compact) {
padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab actions is not left (unless sticky-compact) */
Expand All @@ -105,6 +113,10 @@
flex-shrink: 0;
}

.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab.sizing-fit {
flex-grow: 1; /* grow the tabs to fill each row for a more homogeneous look when tabs wrap */
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink {
min-width: 80px;
flex-basis: 0; /* all tabs are even */
Expand Down Expand Up @@ -149,9 +161,7 @@
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-compact,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-fit.sticky-shrink,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-shrink {

/** Disable sticky positions for sticky compact/shrink tabs if the available space is too little */
position: static;
position: static; /** disable sticky positions for sticky compact/shrink tabs if the available space is too little */
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-left .action-label {
Expand All @@ -163,7 +173,7 @@
content: '';
display: flex;
flex: 0;
width: 5px; /* Reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */
width: 5px; /* reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.tab-actions-left {
Expand Down Expand Up @@ -276,7 +286,7 @@

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-shrink > .tab-actions {
flex: 0;
overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room... */
overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room */
}

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions,
Expand Down Expand Up @@ -366,6 +376,14 @@
height: 35px;
}

.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .editor-actions {

/* When tabs are wrapped, position the editor actions at the end of the very last row */
position: absolute;
bottom: 0;
right: 0;
}

/* Breadcrumbs */

.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control {
Expand Down
129 changes: 109 additions & 20 deletions src/vs/workbench/browser/parts/editor/tabsTitleControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class TabsTitleControl extends TitleControl {
private tabActionBars: ActionBar[] = [];
private tabDisposables: IDisposable[] = [];

private dimensions: ITitleControlDimensions = {
private dimensions: ITitleControlDimensions & { used?: Dimension } = {
container: Dimension.None,
available: Dimension.None
};
Expand Down Expand Up @@ -571,6 +571,7 @@ export class TabsTitleControl extends TitleControl {
oldOptions.showIcons !== newOptions.showIcons ||
oldOptions.hasIcons !== newOptions.hasIcons ||
oldOptions.highlightModifiedTabs !== newOptions.highlightModifiedTabs ||
oldOptions.wrapTabs !== newOptions.wrapTabs ||
!equals(oldOptions.decorations, newOptions.decorations)
) {
this.redraw();
Expand Down Expand Up @@ -1274,19 +1275,30 @@ export class TabsTitleControl extends TitleControl {
}

getDimensions(): IEditorGroupTitleDimensions {
let height = TabsTitleControl.TAB_HEIGHT;
let height: number;

// Wrap: we need to ask `offsetHeight` to get
// the real height of the title area with wrapping.
if (this.accessor.partOptions.wrapTabs && this.tabsAndActionsContainer?.classList.contains('wrapping')) {
height = this.tabsAndActionsContainer.offsetHeight;
} else {
height = TabsTitleControl.TAB_HEIGHT;
}

const offset = height;

// Account for breadcrumbs if visible
if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) {
height += BreadcrumbsControl.HEIGHT; // Account for breadcrumbs if visible
}

return {
height,
offset: TabsTitleControl.TAB_HEIGHT
};
return { height, offset };
}

layout(dimensions: ITitleControlDimensions): Dimension {
this.dimensions = dimensions;

// Remember dimensions that we get
Object.assign(this.dimensions, dimensions);

// The layout of tabs can be an expensive operation because we access DOM properties
// that can result in the browser doing a full page layout to validate them. To buffer
Expand All @@ -1303,17 +1315,33 @@ export class TabsTitleControl extends TitleControl {
}

private doLayout(dimensions: ITitleControlDimensions): void {

// Only layout if we have valid tab index and dimensions
const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined;
if (!activeTabAndIndex || dimensions.container === Dimension.None || dimensions.available === Dimension.None) {
return; // nothing to do if not editor opened or we got no dimensions yet
if (activeTabAndIndex && dimensions.container !== Dimension.None && dimensions.available !== Dimension.None) {

// Breadcrumbs
this.doLayoutBreadcrumbs(dimensions);

// Tabs
const [activeTab, activeIndex] = activeTabAndIndex;
this.doLayoutTabs(activeTab, activeIndex, dimensions);
}

// Breadcrumbs
this.doLayoutBreadcrumbs(dimensions);
// Compute new dimension of tabs title control and remember it for future usages
const oldDimension = this.dimensions.used;
const newDimension = this.dimensions.used = new Dimension(dimensions.container.width, this.getDimensions().height);

// Tabs
const [activeTab, activeIndex] = activeTabAndIndex;
this.doLayoutTabs(activeTab, activeIndex);
// In case the height of the title control changed from before
// (currently only possible if tabs are set to wrap), we need
// to signal this to the outside via a `relayout` call so that
// e.g. the editor control can be adjusted accordingly.
if (
this.accessor.partOptions.wrapTabs &&
oldDimension && oldDimension.height !== newDimension.height
) {
this.group.relayout();
}
}

private doLayoutBreadcrumbs(dimensions: ITitleControlDimensions): void {
Expand All @@ -1322,8 +1350,8 @@ export class TabsTitleControl extends TitleControl {
}
}

private doLayoutTabs(activeTab: HTMLElement, activeIndex: number): void {
const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar);
private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: ITitleControlDimensions): void {
const [tabsAndActionsContainer, tabsContainer, tabsScrollbar, editorToolbarContainer] = assertAllDefined(this.tabsAndActionsContainer, this.tabsContainer, this.tabsScrollbar, this.editorToolbarContainer);

//
// Synopsis
Expand Down Expand Up @@ -1381,6 +1409,55 @@ export class TabsTitleControl extends TitleControl {
tabsContainer.classList.remove('disable-sticky-tabs');
}

// Handle wrapping tabs according to setting:
// - enabled: only add class if tabs wrap and don't exceed available height
// - disabled: remove class
if (this.accessor.partOptions.wrapTabs) {
let tabsWrapMultiLine = tabsAndActionsContainer.classList.contains('wrapping');
let updateScrollbar = false;

// Tabs do not wrap multiline: add wrapping if tabs exceed the tabs container width
// and the height of the tabs container does not exceed the maximum
if (!tabsWrapMultiLine && allTabsWidth > visibleTabsContainerWidth) {
tabsAndActionsContainer.classList.add('wrapping');
tabsWrapMultiLine = true;
}

// Tabs wrap multiline: remove wrapping if height exceeds available height
// or the maximum allowed height
if (tabsWrapMultiLine && tabsContainer.offsetHeight > dimensions.available.height) {
tabsAndActionsContainer.classList.remove('wrapping');
tabsWrapMultiLine = false;
updateScrollbar = true;
}

// If we do not exceed the tabs container width, we cannot simply remove
// the wrap class because by wrapping tabs, they reduce their size
// and we would otherwise constantly add and remove the class. As such
// we need to check if the height of the tabs container is back to normal
// and then remove the wrap class.
if (tabsWrapMultiLine && allTabsWidth === visibleTabsContainerWidth && tabsContainer.offsetHeight === TabsTitleControl.TAB_HEIGHT) {
tabsAndActionsContainer.classList.remove('wrapping');
tabsWrapMultiLine = false;
updateScrollbar = true;
}

// Update `last-tab-margin-right` CSS variable to account for the absolute
// positioned editor actions container when tabs wrap. The margin needs to
// be the width of the editor actions container to avoid screen cheese.
tabsContainer.style.setProperty('--last-tab-margin-right', tabsWrapMultiLine ? `${editorToolbarContainer.offsetWidth}px` : '0');

// When tabs change from wrapping back to normal, we need to indicate this
// to the scrollbar so that revealing the active tab functions properly.
if (updateScrollbar) {
tabsScrollbar.setScrollPosition({
scrollLeft: tabsContainer.scrollLeft
});
}
} else {
tabsAndActionsContainer.classList.remove('wrapping');
}

let activeTabPosX: number | undefined;
let activeTabWidth: number | undefined;

Expand Down Expand Up @@ -1567,11 +1644,23 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
// Add border between tabs and breadcrumbs in high contrast mode.
if (theme.type === ColorScheme.HIGH_CONTRAST) {
const borderColor = (theme.getColor(TAB_BORDER) || theme.getColor(contrastBorder));
if (borderColor) {
collector.addRule(`
.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container {
border-bottom: 1px solid ${borderColor};
}
`);
}
}

// Add bottom border to tabs when wrapping
const borderColor = theme.getColor(TAB_BORDER);
if (borderColor) {
collector.addRule(`
.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container {
border-bottom: 1px solid ${borderColor};
}
`);
.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab {
border-bottom: 1px solid ${borderColor};
}
`);
}

// Styling with Outline color (e.g. high contrast theme)
Expand Down
5 changes: 5 additions & 0 deletions src/vs/workbench/browser/workbench.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ import { isStandalone } from 'vs/base/browser/browser';
'description': nls.localize('showEditorTabs', "Controls whether opened editors should show in tabs or not."),
'default': true
},
'workbench.editor.wrapTabs': {
'type': 'boolean',
'description': nls.localize('wrapTabs', "Controls whether tabs should be wrapped over multiple lines when exceeding available space or wether a scrollbar should appear instead."),
'default': false
},
'workbench.editor.scrollToSwitchTabs': {
'type': 'boolean',
'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration. This value is ignored when `#workbench.editor.showTabs#` is disabled."),
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/common/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,7 @@ export interface IWorkbenchEditorConfiguration {

interface IEditorPartConfiguration {
showTabs?: boolean;
wrapTabs?: boolean;
scrollToSwitchTabs?: boolean;
highlightModifiedTabs?: boolean;
tabCloseButton?: 'left' | 'right' | 'off';
Expand Down