未验证 提交 baf15f10 编写于 作者: S SneakyFish5 提交者: GitHub

Allow tabs to wrap to multi-line (#106448)

* Allow tabs to wrap to multi-line

* Address feedback

* Add hidden space after last tab

* some polish for multi-line wrap css class

* some more polish

* Address feedback

* some adjustments to move forward

* add clarifying comment to tabs layout

* Fix editor container height

* WIP - overflowing tabs

* fix getPreferredHeight()

* Fix editor drop target for multi-line tabs

* Add comments and remove !important

* fix dnd offset

* Rework layout algorithm

* Make layout return a Dimension

* WIP - set maxDimensions

* Layout multi-line tabs synchronously

* make sure dimensions are always defined and passed down to where needed

* Rework group.relayout and store lastComputedHeight

* fix breadcrumbs causing editor to disappear

* consolidate css rules

* rename setting

* simplify classes

* streamline relayout

* wrapTabs => experimentalWrapTabs

* tweak layout

* Limit wrapped tabs to 3 rows

* Only use flex-grow for `tabSizing: fit`

* fix scrollbar reveal to work properly

* tabs - get rid of sync layout

* WIP: Move editor actions to the bottom right

* some tweaks

* introduce css variable for margin-right trick

* add border to separate tabs when wrapping

* 💄

* rename setting

* 💄 layout method
Co-authored-by: NBenjamin Pasero <benjpas@microsoft.com>
上级 8ed50905
......@@ -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 {
......@@ -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 */
}
......@@ -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) */
......@@ -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 */
......@@ -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 {
......@@ -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 {
......@@ -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,
......@@ -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 {
......
......@@ -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
};
......@@ -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();
......@@ -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
......@@ -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 {
......@@ -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
......@@ -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;
......@@ -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)
......
......@@ -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."),
......
......@@ -1265,6 +1265,7 @@ export interface IWorkbenchEditorConfiguration {
interface IEditorPartConfiguration {
showTabs?: boolean;
wrapTabs?: boolean;
scrollToSwitchTabs?: boolean;
highlightModifiedTabs?: boolean;
tabCloseButton?: 'left' | 'right' | 'off';
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册