Skip to content

Commit

Permalink
Introduce Dynamic Tab Resizing Strategy (#12360)
Browse files Browse the repository at this point in the history
* Introduce dynamic tab resizing strategy

Part of #12328

Contributed on behalf of STMicroelectronics

Signed-off-by: Thomas Mäder <t.s.maeder@gmail.com>
tsmaeder authored Apr 13, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent c335538 commit 4c8f76d
Showing 5 changed files with 135 additions and 10 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -3,6 +3,11 @@
## History

- [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/)
## v1.37.0 0 -

<a name="breaking_changes_1.37.0">[Breaking Changes:](#breaking_changes_1.37.0)</a>
- [core] Inject core preference into `DockPanelRenderer` constructor [12360](https://github.com/eclipse-theia/theia/pull/12360)
- [core] Introduced `ScrollableTabBar.updateTabs()` to fully render tabs [12360](https://github.com/eclipse-theia/theia/pull/12360)

## v1.36.0 0 - 03/30/2023

20 changes: 20 additions & 0 deletions packages/core/src/browser/core-preferences.ts
Original file line number Diff line number Diff line change
@@ -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.shrinkToFit.enabled': {
type: 'boolean',
default: false,
description: nls.localize('theia/core/tabShrinkToFit', 'Shrink tabs to fit available space.')
},
'workbench.tab.shrinkToFit.minimumSize': {
type: 'number',
default: 50,
minimum: 10,
description: nls.localize('theia/core/tabMinimumSize', 'Specifies the minimum size for tabs.')
},
'workbench.tab.shrinkToFit.defaultSize': {
type: 'number',
default: 200,
minimum: 10,
description: nls.localize('theia/core/tabDefaultSize', 'Specifies the default size for tabs.')
}
}
};
@@ -259,6 +276,9 @@ export interface CoreConfiguration {
'workbench.sash.hoverDelay': number;
'workbench.sash.size': number;
'workbench.tab.maximize': boolean;
'workbench.tab.shrinkToFit.enabled': boolean;
'workbench.tab.shrinkToFit.minimumSize': number;
'workbench.tab.shrinkToFit.defaultSize': number;
}

export const CorePreferenceContribution = Symbol('CorePreferenceContribution');
22 changes: 21 additions & 1 deletion packages/core/src/browser/shell/application-shell.ts
Original file line number Diff line number Diff line change
@@ -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.shrinkToFit.enabled')) {
return {
minimumTabSize: this.corePreferences.get('workbench.tab.shrinkToFit.minimumSize'),
defaultTabSize: this.corePreferences.get('workbench.tab.shrinkToFit.defaultSize')
};
} else {
return undefined;
}
};

const renderer = this.tabBarRendererFactory();
const tabBar = new ToolbarAwareTabBar(
this.tabBarToolbarRegistry,
@@ -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.shrinkToFit.enabled' ||
change.preferenceName === 'workbench.tab.shrinkToFit.minimumSize' ||
change.preferenceName === 'workbench.tab.shrinkToFit.defaultSize') {
tabBar.dynamicTabOptions = getDynamicTabOptions();
}
});
this.onDidCreateTabBarEmitter.fire(tabBar);
return tabBar;
}
89 changes: 80 additions & 9 deletions packages/core/src/browser/shell/tab-bars.ts
Original file line number Diff line number Diff line change
@@ -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.
@@ -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;
@@ -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 };
}

/**
@@ -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.
*/
@@ -551,12 +568,26 @@ export class ScrollableTabBar extends TabBar<Widget> {

private scrollBarFactory: () => PerfectScrollbar;
private pendingReveal?: Promise<void>;
private isMouseOver = false;
protected needsRecompute = false;
protected tabSize = 0;
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 {
@@ -571,6 +602,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);
}

@@ -583,14 +622,44 @@ export class ScrollableTabBar extends TabBar<Widget> {
}

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);
@@ -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()));
@@ -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 {
@@ -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);
@@ -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();
}
@@ -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);
9 changes: 9 additions & 0 deletions packages/core/src/browser/style/tabs.css
Original file line number Diff line number Diff line change
@@ -410,3 +410,12 @@
flex-flow: row nowrap;
min-width: 100%;
}

.p-TabBar-tab .theia-tab-icon-label {
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 {
overflow: hidden;
}

0 comments on commit 4c8f76d

Please sign in to comment.