Commit 2654567b authored by John Lee's avatar John Lee Committed by Commit Bot

WebUI Tab Strip: Optimize scroll calculations

This CL sets up the scroll container to the the TabListElement itself,
allowing the TabListElement to directly control its own scroll
position without having to modify anything outside of itself. This
also allows for various optimizations so that layout does not need
to be re-calculated multiple times.

Other optimizations include using a cache of CSS variables to
determine the relatively static dimensions of tabs and viewport.

Bug: 1023492
Change-Id: I00874f36ab022aed15477c98652f8014b6098ce8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1948059Reviewed-by: default avatarDemetrios Papadopoulos <dpapad@chromium.org>
Commit-Queue: John Lee <johntlee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#721770}
parent fe4bb2e3
......@@ -8,7 +8,8 @@
background: var(--tabstrip-background-color);
box-sizing: border-box;
display: flex;
min-width: fit-content;
height: 100%;
overflow: overlay hidden;
width: 100%;
}
......@@ -33,6 +34,7 @@
#tabsContainer {
display: flex;
min-width: fit-content;
overflow: hidden;
padding: var(--tabstrip-tab-spacing);
padding-inline-end: 0;
......
......@@ -24,6 +24,14 @@ import {TabData, TabsApiProxy} from './tabs_api_proxy.js';
*/
const SCROLL_PADDING = 32;
/**
* @enum {string}
*/
const LayoutVariable = {
VIEWPORT_WIDTH: '--tabstrip-viewport-width',
TAB_WIDTH: '--tabstrip-tab-thumbnail-width',
};
/**
* @param {!Element} element
* @return {boolean}
......@@ -75,6 +83,7 @@ class TabListElement extends CustomElement {
entry.target.tab.id, entry.isIntersecting);
}
}, {
root: this,
// The horizontal root margin is set to 100% to also track thumbnails that
// are one standard finger swipe away.
rootMargin: '0% 100%',
......@@ -91,9 +100,6 @@ class TabListElement extends CustomElement {
/** @type {!Element} */ (
this.shadowRoot.querySelector('#pinnedTabsContainer'));
/** @private {!Element} */
this.scrollingParent_ = document.documentElement;
/** @private {!TabStripEmbedderProxy} */
this.tabStripEmbedderProxy_ = TabStripEmbedderProxy.getInstance();
......@@ -172,7 +178,6 @@ class TabListElement extends CustomElement {
this.tabsApi_.getTabs().then(tabs => {
this.tabStripEmbedderProxy_.reportTabDataReceivedDuration(
tabs.length, Date.now() - getTabsStartTimestamp);
tabs.forEach(tab => this.onTabCreated_(tab));
addWebUIListener('tab-created', tab => this.onTabCreated_(tab));
addWebUIListener(
......@@ -232,6 +237,14 @@ class TabListElement extends CustomElement {
this.shadowRoot.querySelector('tabstrip-tab[active]'));
}
/**
* @param {!LayoutVariable} variable
* @return {number} in pixels
*/
getLayoutVariable_(variable) {
return parseInt(this.style.getPropertyValue(variable), 10);
}
/**
* @param {!TabElement} tabElement
* @param {number} index
......@@ -374,6 +387,9 @@ class TabListElement extends CustomElement {
if (newlyActiveTab) {
newlyActiveTab.tab = /** @type {!TabData} */ (
Object.assign({}, newlyActiveTab.tab, {active: true}));
if (!this.tabStripEmbedderProxy_.isVisible()) {
this.scrollToTab_(newlyActiveTab);
}
}
}
......@@ -494,21 +510,31 @@ class TabListElement extends CustomElement {
* @private
*/
scrollToTab_(tabElement) {
const screenLeft = this.scrollingParent_.scrollLeft;
const screenRight = screenLeft + this.scrollingParent_.offsetWidth;
const tabElementLeft = tabElement.getBoundingClientRect().left;
if (screenLeft > tabElement.offsetLeft) {
let scrollBy = 0;
if (tabElementLeft === SCROLL_PADDING) {
// Perfectly aligned to the left.
return;
} else if (tabElementLeft < SCROLL_PADDING) {
// If the element's left is to the left of the visible screen, scroll
// such that the element's left edge is aligned with the screen's edge
this.scrollingParent_.scrollLeft = tabElement.offsetLeft - SCROLL_PADDING;
} else if (screenRight < tabElement.offsetLeft + tabElement.offsetWidth) {
// If the element's right is to the right of the visible screen, scroll
// such that the element's right edge is aligned with the screen's right
// edge.
this.scrollingParent_.scrollLeft = tabElement.offsetLeft +
tabElement.offsetWidth - this.scrollingParent_.offsetWidth +
SCROLL_PADDING;
// such that the element's left edge is aligned with the screen's edge.
scrollBy = tabElementLeft - SCROLL_PADDING;
} else {
const tabElementWidth = this.getLayoutVariable_(LayoutVariable.TAB_WIDTH);
const tabElementRight = tabElementLeft + tabElementWidth;
const viewportWidth =
this.getLayoutVariable_(LayoutVariable.VIEWPORT_WIDTH);
if (tabElementRight + SCROLL_PADDING > viewportWidth) {
scrollBy = (tabElementRight + SCROLL_PADDING) - viewportWidth;
} else {
// Perfectly aligned to the right.
return;
}
}
this.scrollLeft += scrollBy;
}
/**
......
......@@ -17,14 +17,18 @@
--tabstrip-background-color: $i18n{frameColor};
--tabstrip-tab-border-radius: 8px;
--tabstrip-tab-active-border-color: rgb(var(--google-blue-500-rgb));
overflow: hidden;
}
body {
background: var(--tabstrip-background-color);
height: 100%;
margin: 0;
overflow: overlay hidden;
overflow: hidden;
padding: 0;
touch-action: pan-x;
width: 100%;
}
</style>
</head>
......
......@@ -18,6 +18,7 @@ TabStripUILayout TabStripUILayout::CalculateForWebViewportSize(
TabStripUILayout layout;
layout.padding_around_tab_list = 16;
layout.tab_title_height = 40;
layout.viewport_width = viewport_size.width();
if (viewport_size.IsEmpty()) {
layout.tab_thumbnail_size =
......@@ -51,6 +52,8 @@ base::Value TabStripUILayout::AsDictionary() const {
base::NumberToString(tab_thumbnail_size.width()) + "px");
dict.SetStringKey("--tabstrip-tab-thumbnail-height",
base::NumberToString(tab_thumbnail_size.height()) + "px");
dict.SetStringKey("--tabstrip-viewport-width",
base::NumberToString(viewport_width) + "px");
return dict;
}
......
......@@ -24,6 +24,7 @@ struct TabStripUILayout {
int padding_around_tab_list;
int tab_title_height;
int viewport_width;
gfx::Size tab_thumbnail_size;
};
......
......@@ -106,6 +106,7 @@ suite('TabList', () => {
setup(() => {
document.body.innerHTML = '';
document.body.style.margin = 0;
testTabsApiProxy = new TestTabsApiProxy();
testTabsApiProxy.setTabs(tabs);
......@@ -373,7 +374,7 @@ suite('TabList', () => {
// point, all 3 tabs should fit within the root and rootMargin of the
// IntersectionObserver. Since the 3rd tab was not being tracked before,
// it should be the only tab to become tracked.
document.documentElement.scrollLeft = tabElements[1].offsetLeft;
tabList.scrollLeft = tabElements[1].offsetLeft;
[tabId, thumbnailTracked] =
await testTabsApiProxy.whenCalled('setThumbnailTracked');
assertEquals(tabId, tabElements[2].tab.id);
......@@ -384,7 +385,7 @@ suite('TabList', () => {
// Scroll such that the third tab is now the only visible tab. At this
// point, the first tab should be outside of the rootMargin of the
// IntersectionObserver.
document.documentElement.scrollLeft = tabElements[2].offsetLeft;
tabList.scrollLeft = tabElements[2].offsetLeft;
[tabId, thumbnailTracked] =
await testTabsApiProxy.whenCalled('setThumbnailTracked');
assertEquals(tabId, tabElements[0].tab.id);
......@@ -480,4 +481,48 @@ suite('TabList', () => {
assertEquals(contextMenuArgs[0], 1);
assertEquals(contextMenuArgs[1], 2);
});
test('scrolls to active tabs', async () => {
await tabList.animationPromises;
const scrollPadding = 32;
const tabWidth = 200;
const viewportWidth = 300;
// Mock the width of each tab element.
tabList.style.setProperty(
'--tabstrip-tab-thumbnail-width', `${tabWidth}px`);
tabList.style.setProperty('--tabstrip-tab-spacing', '0px');
const tabElements = getUnpinnedTabs();
tabElements.forEach(tabElement => {
tabElement.style.width = `${tabWidth}px`;
});
// Mock the scroller size such that it cannot fit only 1 tab at a time.
tabList.style.setProperty(
'--tabstrip-viewport-width', `${viewportWidth}px`);
tabList.style.width = `${viewportWidth}px`;
// Verify the scrollLeft is currently at its default state of 0, and then
// send a visibilitychange event to cause a scroll.
assertEquals(tabList.scrollLeft, 0);
webUIListenerCallback('tab-active-changed', tabs[1].id);
testTabStripEmbedderProxy.setVisible(false);
document.dispatchEvent(new Event('visibilitychange'));
// The 2nd tab should be off-screen to the right, so activating it should
// scroll so that the element's right edge is aligned with the screen's
// right edge.
let activeTab = getUnpinnedTabs()[1];
assertEquals(
tabList.scrollLeft + tabList.offsetWidth,
activeTab.offsetLeft + activeTab.offsetWidth + scrollPadding);
// The 1st tab should be now off-screen to the left, so activating it should
// scroll so that the element's left edge is aligned with the screen's
// left edge.
webUIListenerCallback('tab-active-changed', tabs[0].id);
activeTab = getUnpinnedTabs()[0];
assertEquals(tabList.scrollLeft, 0);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment