Skip to content

Commit

Permalink
Add "open tabs" drop down
Browse files Browse the repository at this point in the history
Adds a drop down allowing to chose among open editors when there is too
little space to show all open tabs.

Part of #12328

Contributed on behalf of STMicroelectronics

Signed-off-by: Thomas Mäder <t.s.maeder@gmail.com>
  • Loading branch information
tsmaeder committed Apr 13, 2023
1 parent 4c8f76d commit a309b28
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 77 deletions.
148 changes: 91 additions & 57 deletions packages/core/src/browser/shell/tab-bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import { IDragEvent } from '@phosphor/dragdrop';
import { LOCKED_CLASS, PINNED_CLASS } from '../widgets/widget';
import { CorePreferences } from '../core-preferences';
import { HoverService } from '../hover-service';
import { Root, createRoot } from 'react-dom/client';
import { SelectComponent } from '../widgets/select-component';
import { createElement } from 'react';

/** The class name added to hidden content nodes, which are required to render vertical side bars. */
const HIDDEN_CONTENT_CLASS = 'theia-TabBar-hidden-content';
Expand Down Expand Up @@ -230,7 +233,7 @@ export class TabBarRenderer extends TabBar.Renderer {
} else {
width = '';
}
return { zIndex, height, width };
return { zIndex, height, minWidth: width, maxWidth: width };
}

/**
Expand Down Expand Up @@ -552,13 +555,6 @@ export class TabBarRenderer extends TabBar.Renderer {

}

export namespace ScrollableTabBar {
export interface Options {
minimumTabSize: number;
defaultTabSize: number;
}
}

/**
* A specialized tab bar for the main and bottom areas.
*/
Expand All @@ -572,13 +568,18 @@ export class ScrollableTabBar extends TabBar<Widget> {
protected needsRecompute = false;
protected tabSize = 0;
private _dynamicTabOptions?: ScrollableTabBar.Options;
protected contentContainer: HTMLElement;
protected topRow: HTMLElement;

protected readonly toDispose = new DisposableCollection();
protected openTabsContainer: HTMLDivElement;
protected openTabsRoot: Root;

constructor(options?: TabBar.IOptions<Widget> & PerfectScrollbar.Options, dynamicTabOptions?: ScrollableTabBar.Options) {
super(options);
this.scrollBarFactory = () => new PerfectScrollbar(this.scrollbarHost, options);
this._dynamicTabOptions = dynamicTabOptions;
this.rewireDOM();
}

set dynamicTabOptions(options: ScrollableTabBar.Options | undefined) {
Expand All @@ -598,6 +599,35 @@ export class ScrollableTabBar extends TabBar<Widget> {
this.toDispose.dispose();
}

/**
* Restructures the DOM defined in PhosphorJS.
*
* By default the tabs (`li`) are contained in the `this.contentNode` (`ul`) which is wrapped in a `div` (`this.node`).
* Instead of this structure, we add a container for the `this.contentNode` and for the toolbar.
* The scrollbar will only work for the `ul` part but it does not affect the toolbar, so it can be on the right hand-side.
*/
private rewireDOM(): void {
const contentNode = this.node.getElementsByClassName(ScrollableTabBar.Styles.TAB_BAR_CONTENT)[0];
if (!contentNode) {
throw new Error("'this.node' does not have the content as a direct child with class name 'p-TabBar-content'.");
}
this.node.removeChild(contentNode);
this.contentContainer = document.createElement('div');
this.contentContainer.classList.add(ScrollableTabBar.Styles.TAB_BAR_CONTENT_CONTAINER);
this.contentContainer.appendChild(contentNode);

this.topRow = document.createElement('div');
this.topRow.classList.add('theia-tabBar-tab-row');
this.topRow.appendChild(this.contentContainer);

this.openTabsContainer = document.createElement('div');
this.openTabsContainer.classList.add('theia-tabBar-open-tabs');
this.openTabsRoot = createRoot(this.openTabsContainer);
this.topRow.appendChild(this.openTabsContainer);

this.node.appendChild(this.topRow);
}

protected override onAfterAttach(msg: Message): void {
if (!this.scrollBar) {
this.scrollBar = this.scrollBarFactory();
Expand Down Expand Up @@ -629,15 +659,36 @@ export class ScrollableTabBar extends TabBar<Widget> {

const content = [];
if (this.dynamicTabOptions) {

this.openTabsRoot.render(createElement(SelectComponent, {
options: this.titles,
onChange: (option, index) => {
this.currentIndex = index;
},
alignment: 'right'
}));

if (this.isMouseOver) {
this.needsRecompute = true;
} else {
this.needsRecompute = false;
if (this.orientation === 'horizontal') {
this.tabSize = Math.max(Math.min(this.scrollbarHost.clientWidth / this.titles.length,
this.dynamicTabOptions.defaultTabSize), this.dynamicTabOptions.minimumTabSize);

let availableWidth = this.scrollbarHost.clientWidth;
if (!this.openTabsContainer.classList.contains('p-mod-hidden')) {
availableWidth += this.openTabsContainer.getBoundingClientRect().width;
}
if (this.dynamicTabOptions.minimumTabSize * this.titles.length <= availableWidth) {
this.openTabsContainer.classList.add('p-mod-hidden');
} else {
this.openTabsContainer.classList.remove('p-mod-hidden');
}
}
}
} else {
this.openTabsContainer.classList.add('p-mod-hidden');
}
for (let i = 0, n = this.titles.length; i < n; ++i) {
const title = this.titles[i];
Expand Down Expand Up @@ -716,10 +767,38 @@ export class ScrollableTabBar extends TabBar<Widget> {
return result;
}

/**
* Overrides the `contentNode` property getter in PhosphorJS' TabBar.
*/
// @ts-expect-error TS2611 `TabBar<T>.contentNode` is declared as `readonly contentNode` but is implemented as a getter.
get contentNode(): HTMLUListElement {
return this.tabBarContainer.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT)[0] as HTMLUListElement;
}

/**
* Overrides the scrollable host from the parent class.
*/
protected get scrollbarHost(): HTMLElement {
return this.node;
return this.tabBarContainer;
}

protected get tabBarContainer(): HTMLElement {
return this.node.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT_CONTAINER)[0] as HTMLElement;
}
}

export namespace ScrollableTabBar {

export interface Options {
minimumTabSize: number;
defaultTabSize: number;
}
export namespace Styles {

export const TAB_BAR_CONTENT = 'p-TabBar-content';
export const TAB_BAR_CONTENT_CONTAINER = 'p-TabBar-content-container';

}
}

/**
Expand All @@ -738,12 +817,9 @@ export class ScrollableTabBar extends TabBar<Widget> {
*
*/
export class ToolbarAwareTabBar extends ScrollableTabBar {

protected contentContainer: HTMLElement;
protected toolbar: TabBarToolbar | undefined;
protected breadcrumbsContainer: HTMLElement;
protected readonly breadcrumbsRenderer: BreadcrumbsRenderer;
protected topRow: HTMLElement;

constructor(
protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
Expand All @@ -754,7 +830,8 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
) {
super(options, dynamicTabOptions);
this.breadcrumbsRenderer = this.breadcrumbsRendererFactory();
this.rewireDOM();
this.addBreadcrumbs();
this.toolbar = this.tabBarToolbarFactory();
this.toDispose.push(this.tabBarToolbarRegistry.onDidChange(() => this.update()));
this.toDispose.push(this.breadcrumbsRenderer);
this.toDispose.push(this.breadcrumbsRenderer.onDidChangeActiveState(active => {
Expand All @@ -769,25 +846,6 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
this.toDispose.push(Disposable.create(() => this.currentChanged.disconnect(handler)));
}

/**
* Overrides the `contentNode` property getter in PhosphorJS' TabBar.
*/
// @ts-expect-error TS2611 `TabBar<T>.contentNode` is declared as `readonly contentNode` but is implemented as a getter.
get contentNode(): HTMLUListElement {
return this.tabBarContainer.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT)[0] as HTMLUListElement;
}

/**
* Overrides the scrollable host from the parent class.
*/
protected override get scrollbarHost(): HTMLElement {
return this.tabBarContainer;
}

protected get tabBarContainer(): HTMLElement {
return this.node.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT_CONTAINER)[0] as HTMLElement;
}

protected async updateBreadcrumbs(): Promise<void> {
const current = this.currentTitle?.owner;
const uri = NavigatableWidget.is(current) ? current.getResourceUri() : undefined;
Expand Down Expand Up @@ -844,38 +902,14 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
* Instead of this structure, we add a container for the `this.contentNode` and for the toolbar.
* The scrollbar will only work for the `ul` part but it does not affect the toolbar, so it can be on the right hand-side.
*/
protected rewireDOM(): void {
const contentNode = this.node.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT)[0];
if (!contentNode) {
throw new Error("'this.node' does not have the content as a direct child with class name 'p-TabBar-content'.");
}
this.node.removeChild(contentNode);
this.topRow = document.createElement('div');
this.topRow.classList.add('theia-tabBar-tab-row');
this.contentContainer = document.createElement('div');
this.contentContainer.classList.add(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT_CONTAINER);
this.contentContainer.appendChild(contentNode);
this.topRow.appendChild(this.contentContainer);
this.node.appendChild(this.topRow);
this.toolbar = this.tabBarToolbarFactory();
private addBreadcrumbs(): void {
this.breadcrumbsContainer = document.createElement('div');
this.breadcrumbsContainer.classList.add('theia-tabBar-breadcrumb-row');
this.breadcrumbsContainer.appendChild(this.breadcrumbsRenderer.host);
this.node.appendChild(this.breadcrumbsContainer);
}
}

export namespace ToolbarAwareTabBar {

export namespace Styles {

export const TAB_BAR_CONTENT = 'p-TabBar-content';
export const TAB_BAR_CONTENT_CONTAINER = 'p-TabBar-content-container';

}

}

/**
* A specialized tab bar for side areas.
*/
Expand Down
71 changes: 63 additions & 8 deletions packages/core/src/browser/style/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
}

.p-TabBar[data-orientation='horizontal'] {
overflow-x: hidden;
overflow-y: hidden;
min-height: var(--theia-horizontal-toolbar-height);
}

Expand All @@ -38,6 +36,7 @@
line-height: var(--theia-private-horizontal-tab-height);
padding: 0px 8px;
align-items: center;
overflow: hidden;
}

.p-TabBar[data-orientation='vertical'] .p-TabBar-tab {
Expand Down Expand Up @@ -204,7 +203,7 @@
height: var(--theia-icon-size);
width: var(--theia-icon-size);
font: normal normal normal 16px/1 codicon;
display: inline-block;
display: none;
text-decoration: none;
text-rendering: auto;
text-align: center;
Expand All @@ -215,7 +214,13 @@
-ms-user-select: none;
}

.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon:hover {
.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-current > .p-TabBar-tabCloseIcon,
.p-TabBar.theia-app-centers .p-TabBar-tab:hover.p-mod-closable > .p-TabBar-tabCloseIcon,
.p-TabBar.theia-app-centers .p-TabBar-tab:hover.theia-mod-pinned > .p-TabBar-tabCloseIcon {
display: inline-block;
}

.p-TabBar.theia-app-centers .p-TabBar-tab:hover.p-mod-closable > .p-TabBar-tabCloseIcon {
border-radius: 5px;
background-color: rgba(50%, 50%, 50%, 0.2);
}
Expand Down Expand Up @@ -303,6 +308,15 @@
bottom: calc((var(--theia-private-horizontal-tab-scrollbar-rail-height) - var(--theia-private-horizontal-tab-scrollbar-height)) / 2);
}

.p-TabBar[data-orientation='vertical'] .p-TabBar-content-container > .ps__rail-y {
width: var(--theia-private-horizontal-tab-scrollbar-rail-height);
z-index: 1000;
}

.p-TabBar[data-orientation='vertical'] .p-TabBar-content-container > .ps__rail-y > .ps__thumb-y {
width: var(--theia-private-horizontal-tab-scrollbar-height) !important;
right: calc((var(--theia-private-horizontal-tab-scrollbar-rail-height) - var(--theia-private-horizontal-tab-scrollbar-height)) / 2);
}

/*-----------------------------------------------------------------------------
| Dragged tabs
Expand Down Expand Up @@ -337,7 +351,7 @@
}

.p-TabBar-content-container {
display: flex;
display: block;
flex: 1;
position: relative; /* This is necessary for perfect-scrollbar */
}
Expand Down Expand Up @@ -405,17 +419,58 @@
flex-direction: column;
}

.theia-tabBar-tab-row {
.p-TabBar[data-orientation='horizontal'] .theia-tabBar-tab-row {
display: flex;
flex-flow: row nowrap;
min-width: 100%;
width: 100%;
}

.p-TabBar[data-orientation='vertical'] .theia-tabBar-tab-row {
display: flex;
flex-flow: column nowrap;
height: 100%;
}


.p-TabBar[data-orientation='horizontal'] .p-TabBar-content {
flex-direction: row;
}

.p-TabBar-tab .theia-tab-icon-label {
.p-TabBar[data-orientation='vertical'] .p-TabBar-content {
flex-direction: column;
}

.p-TabBar.theia-app-centers[data-orientation='horizontal'] .p-TabBar-tabLabel {
mask-image: linear-gradient(to left, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 1) 15px);
-webkit-mask-image: linear-gradient(to left, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 1) 15px);
flex: 1;
}

.p-TabBar[data-orientation='horizontal'] .p-TabBar-tab.p-mod-closable:hover .theia-tab-icon-label,
.p-TabBar[data-orientation='horizontal'] .p-TabBar-tab.p-mod-current .theia-tab-icon-label {
flex: 1;
overflow: hidden;
}


/*-----------------------------------------------------------------------------
| Open tabs dropdown
|----------------------------------------------------------------------------*/
.theia-tabBar-open-tabs>.theia-select-component .theia-select-component-label {
display: none;
}

.theia-tabBar-open-tabs>.theia-select-component {
min-width: auto;
height: 100%;
}

.theia-tabBar-open-tabs {
flex: 0 0 auto;
display: flex;
align-items: center;
}

.theia-tabBar-open-tabs.p-mod-hidden {
display: none
}
Loading

0 comments on commit a309b28

Please sign in to comment.