Skip to content

Commit

Permalink
Introduce dynamic tab resizing strategy
Browse files Browse the repository at this point in the history
Part of eclipse-theia#12328

Contributed on behalf of STMicroelectronics

Signed-off-by: Thomas Mäder <[email protected]>
  • Loading branch information
tsmaeder committed Apr 11, 2023
1 parent 2b81f49 commit a3c43d1
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 16 deletions.
20 changes: 20 additions & 0 deletions packages/core/src/browser/core-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,23 @@ export const corePreferenceSchema: PreferenceSchema = {
type: 'boolean',
default: false,
description: nls.localize('theia/core/tabMaximize', 'Controls whether to maximize tabs on double click.')
},
'workbench.tab.dynamicTabs': {
type: 'boolean',
default: 'false',
description: nls.localize('theis/core/tabDynamic', 'Resize tabs dynamically')
},
'workbench.tab.minimumSize': {
type: 'number',
default: 50,
minimum: 10,
description: nls.localize('theia/core/tabMinimumSize', 'Minimum size for dynamic tabs')
},
'workbench.tab.defaultSize': {
type: 'number',
default: 200,
minimum: 10,
description: nls.localize('theia/core/tabDefaultSize', 'Default size for dynamic tabs')
}
}
};
Expand Down Expand Up @@ -259,6 +276,9 @@ export interface CoreConfiguration {
'workbench.sash.hoverDelay': number;
'workbench.sash.size': number;
'workbench.tab.maximize': boolean;
'workbench.tab.dynamicTabs': boolean;
'workbench.tab.minimumSize': number;
'workbench.tab.defaultSize': number;
}

export const CorePreferenceContribution = Symbol('CorePreferenceContribution');
Expand Down
22 changes: 21 additions & 1 deletion packages/core/src/browser/shell/application-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,25 @@ export class DockPanelRenderer implements DockLayout.IRenderer {
@inject(TabBarToolbarRegistry) protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
@inject(TabBarToolbarFactory) protected readonly tabBarToolbarFactory: TabBarToolbarFactory,
@inject(BreadcrumbsRendererFactory) protected readonly breadcrumbsRendererFactory: BreadcrumbsRendererFactory,
@inject(CorePreferences) protected readonly corePreferences: CorePreferences
) { }

get onDidCreateTabBar(): CommonEvent<TabBar<Widget>> {
return this.onDidCreateTabBarEmitter.event;
}

createTabBar(): TabBar<Widget> {
const getDynamicTabOptions: () => ScrollableTabBar.Options | undefined = () => {
if (this.corePreferences.get('workbench.tab.dynamicTabs')) {
return {
minimumTabSize: this.corePreferences.get('workbench.tab.minimumSize'),
defaultTabSize: this.corePreferences.get('workbench.tab.defaultSize')
};
} else {
return undefined;
}
};

const renderer = this.tabBarRendererFactory();
const tabBar = new ToolbarAwareTabBar(
this.tabBarToolbarRegistry,
Expand All @@ -115,12 +127,20 @@ export class DockPanelRenderer implements DockLayout.IRenderer {
useBothWheelAxes: true,
scrollXMarginOffset: 4,
suppressScrollY: true
});
},
getDynamicTabOptions());
this.tabBarClasses.forEach(c => tabBar.addClass(c));
renderer.tabBar = tabBar;
tabBar.disposed.connect(() => renderer.dispose());
renderer.contextMenuPath = SHELL_TABBAR_CONTEXT_MENU;
tabBar.currentChanged.connect(this.onCurrentTabChanged, this);
this.corePreferences.onPreferenceChanged(change => {
if (change.preferenceName === 'workbench.tab.dynamicTabs' ||
change.preferenceName === 'workbench.tab.minimumSize' ||
change.preferenceName === 'workbench.tab.defaultSize') {
tabBar.dynamicTabOptions = getDynamicTabOptions();
}
});
this.onDidCreateTabBarEmitter.fire(tabBar);
return tabBar;
}
Expand Down
91 changes: 81 additions & 10 deletions packages/core/src/browser/shell/tab-bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export interface SideBarRenderData extends TabBar.IRenderData<Widget> {
paddingBottom?: number;
}

export interface ScrollableRenderData extends TabBar.IRenderData<Widget> {
tabWidth?: number;
}

/**
* A tab bar renderer that offers a context menu. In addition, this renderer is able to
* set an explicit position and size on the icon and label of each tab in a side bar.
Expand Down Expand Up @@ -204,11 +208,12 @@ export class TabBarRenderer extends TabBar.Renderer {
* If size information is available for the label and icon, set an explicit height on the tab.
* The height value also considers padding, which should be derived from CSS settings.
*/
override createTabStyle(data: SideBarRenderData): ElementInlineStyle {
override createTabStyle(data: SideBarRenderData & ScrollableRenderData): ElementInlineStyle {
const zIndex = `${data.zIndex}`;
const labelSize = data.labelSize;
const iconSize = data.iconSize;
let height: string | undefined;
let width: string | undefined;
if (labelSize || iconSize) {
const labelHeight = labelSize ? (this.tabBar && this.tabBar.orientation === 'horizontal' ? labelSize.height : labelSize.width) : 0;
const iconHeight = iconSize ? iconSize.height : 0;
Expand All @@ -220,7 +225,12 @@ export class TabBarRenderer extends TabBar.Renderer {
const paddingBottom = data.paddingBottom || 0;
height = `${labelHeight + iconHeight + paddingTop + paddingBottom}px`;
}
return { zIndex, height };
if (data.tabWidth) {
width = `${data.tabWidth}px`;
} else {
width = '';
}
return { zIndex, height, width };
}

/**
Expand Down Expand Up @@ -542,6 +552,13 @@ 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 @@ -551,12 +568,24 @@ export class ScrollableTabBar extends TabBar<Widget> {

private scrollBarFactory: () => PerfectScrollbar;
private pendingReveal?: Promise<void>;
private isMouseOver = false;
private _dynamicTabOptions?: ScrollableTabBar.Options;

protected readonly toDispose = new DisposableCollection();

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

set dynamicTabOptions(options: ScrollableTabBar.Options | undefined) {
this._dynamicTabOptions = options;
this.updateTabs();
}

get dynamicTabOptions(): ScrollableTabBar.Options | undefined {
return this._dynamicTabOptions;
}

override dispose(): void {
Expand All @@ -571,6 +600,14 @@ export class ScrollableTabBar extends TabBar<Widget> {
if (!this.scrollBar) {
this.scrollBar = this.scrollBarFactory();
}
this.node.addEventListener('mouseenter', () => { this.isMouseOver = true; });
this.node.addEventListener('mouseleave', () => {
this.isMouseOver = false;
if (this.needsRecompute) {
this.updateTabs();
}
});

super.onAfterAttach(msg);
}

Expand All @@ -582,15 +619,47 @@ export class ScrollableTabBar extends TabBar<Widget> {
}
}

needsRecompute = false;
tabSize = 0;

protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.updateTabs();
}

protected updateTabs(): void {
const content = [];
if (this.dynamicTabOptions) {
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);
}
}
}
for (let i = 0, n = this.titles.length; i < n; ++i) {
const title = this.titles[i];
const current = title === this.currentTitle;
const zIndex = current ? n : n - i - 1;
const renderData: ScrollableRenderData = { title: title, current: current, zIndex: zIndex };
if (this.dynamicTabOptions && this.orientation === 'horizontal') {
renderData.tabWidth = this.tabSize;
}
content[i] = this.renderer.renderTab(renderData);
}
VirtualDOM.render(content, this.contentNode);
if (this.scrollBar) {
this.scrollBar.update();
}
}

protected override onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
if (this.dynamicTabOptions) {
this.updateTabs();
}
if (this.scrollBar) {
if (this.currentIndex >= 0) {
this.revealTab(this.currentIndex);
Expand Down Expand Up @@ -680,9 +749,10 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
protected readonly tabBarToolbarFactory: () => TabBarToolbar,
protected readonly breadcrumbsRendererFactory: BreadcrumbsRendererFactory,
protected readonly options?: TabBar.IOptions<Widget> & PerfectScrollbar.Options,
options?: TabBar.IOptions<Widget> & PerfectScrollbar.Options,
dynamicTabOptions?: ScrollableTabBar.Options
) {
super(options);
super(options, dynamicTabOptions);
this.breadcrumbsRenderer = this.breadcrumbsRendererFactory();
this.rewireDOM();
this.toDispose.push(this.tabBarToolbarRegistry.onDidChange(() => this.update()));
Expand Down Expand Up @@ -746,8 +816,8 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
}

protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.updateToolbar();
super.onUpdateRequest(msg);
}

protected updateToolbar(): void {
Expand All @@ -756,6 +826,7 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
}
const widget = this.currentTitle?.owner ?? undefined;
this.toolbar.updateTarget(widget);
this.updateTabs();
}

override handleEvent(event: Event): void {
Expand Down Expand Up @@ -859,7 +930,7 @@ export class SideTabBar extends ScrollableTabBar {

protected override onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.renderTabBar();
this.updateTabs();
this.node.addEventListener('p-dragenter', this);
this.node.addEventListener('p-dragover', this);
this.node.addEventListener('p-dragleave', this);
Expand All @@ -875,7 +946,7 @@ export class SideTabBar extends ScrollableTabBar {
}

protected override onUpdateRequest(msg: Message): void {
this.renderTabBar();
this.updateTabs();
if (this.scrollBar) {
this.scrollBar.update();
}
Expand All @@ -885,7 +956,7 @@ export class SideTabBar extends ScrollableTabBar {
* Render the tab bar in the _hidden content node_ (see `hiddenContentNode` for explanation),
* then gather size information for labels and render it again in the proper content node.
*/
protected renderTabBar(): void {
protected override updateTabs(): void {
if (this.isAttached) {
// Render into the invisible node
this.renderTabs(this.hiddenContentNode);
Expand Down
19 changes: 14 additions & 5 deletions packages/core/src/browser/style/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@
}

.p-TabBar[data-orientation='horizontal'] .p-TabBar-tab .theia-tab-icon-label,
.p-TabBar-tab.p-mod-drag-image .theia-tab-icon-label {
display: flex;
line-height: var(--theia-content-line-height);
align-items: center;
.p-TabBar-tab.p-mod-drag-image .theia-tab-icon-label {
flex: 1 1 0px;
display: flex;
line-height: var(--theia-content-line-height);
align-items: center;
overflow: hidden;
}


Expand Down Expand Up @@ -112,10 +114,14 @@
display: inline-block;
}

.p-TabBar.theia-app-centers .p-TabBar-tabLabel {
flex: 1 1 0;
}

.p-TabBar.theia-app-centers .p-TabBar-tabLabelDetails {
margin-left: 5px;
color: var(--theia-descriptionForeground);
flex: 1 1 auto;
flex: 1 1 0;
overflow: hidden;
white-space: nowrap;
}
Expand Down Expand Up @@ -213,6 +219,7 @@
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
flex-shrink: 0;
}

.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon:hover {
Expand Down Expand Up @@ -330,6 +337,7 @@
.p-TabBar-toolbar {
z-index: var(--theia-tabbar-toolbar-z-index); /* Due to the scrollbar (`z-index: 1000;`) it has a greater `z-index`. */
display: flex;
flex: 0 0 auto;
flex-direction: row-reverse;
padding: 4px;
padding-left: 0px;
Expand Down Expand Up @@ -406,6 +414,7 @@
}

.theia-tabBar-tab-row {
flex: 1 1;
display: flex;
flex-flow: row nowrap;
min-width: 100%;
Expand Down

0 comments on commit a3c43d1

Please sign in to comment.