Optimization: Reduce number of renders in tab bar
By extracting Tab into a separate, memoized component, caching handlers and tweaking how icons and other props are passed, we're able to only re-render tabs that actually changed, rather than re-rendering the entire tab bar all the time. This should be especially noticeable when dragging tabs around but will reduce CPU cycles used in general.
This commit is contained in:
parent
d9790b707a
commit
73bc0cbb94
2 changed files with 134 additions and 57 deletions
|
@ -25,12 +25,69 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React, { forwardRef, useState, useRef, useImperativeHandle, useEffect, useLayoutEffect } from 'react';
|
import React, { forwardRef, useState, useRef, useImperativeHandle, useEffect, useLayoutEffect, memo, useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
const { CSSIcon } = require('./icons');
|
const { CSSIcon, CSSItemTypeIcon } = require('./icons');
|
||||||
|
|
||||||
const SCROLL_ARROW_SCROLL_BY = 222;
|
const SCROLL_ARROW_SCROLL_BY = 222;
|
||||||
|
|
||||||
|
const Tab = memo((props) => {
|
||||||
|
const { icon, id, index, isBeingDragged, isItemType, onContextMenu, onDragEnd, onDragStart, onTabClick, onTabClose, onTabMouseDown, selected, title } = props;
|
||||||
|
|
||||||
|
const handleTabMouseDown = useCallback(event => onTabMouseDown(event, id), [onTabMouseDown, id]);
|
||||||
|
const handleContextMenu = useCallback(event => onContextMenu(event, id), [onContextMenu, id]);
|
||||||
|
const handleTabClick = useCallback(event => onTabClick(event, id), [onTabClick, id]);
|
||||||
|
const handleDragStart = useCallback(event => onDragStart(event, id, index), [onDragStart, id, index]);
|
||||||
|
const handleTabClose = useCallback(event => onTabClose(event, id), [onTabClose, id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={id}
|
||||||
|
data-id={id}
|
||||||
|
className={cx('tab', { selected, dragging: isBeingDragged })}
|
||||||
|
draggable={true}
|
||||||
|
onMouseDown={handleTabMouseDown}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
onClick={handleTabClick}
|
||||||
|
onAuxClick={handleTabClick}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
tabIndex="-1"
|
||||||
|
>
|
||||||
|
{ isItemType
|
||||||
|
? <CSSItemTypeIcon itemType={icon} className="tab-icon" />
|
||||||
|
: <CSSIcon name={icon} className="tab-icon" />
|
||||||
|
}
|
||||||
|
<div className="tab-name" title={title}>{title}</div>
|
||||||
|
<div
|
||||||
|
className="tab-close"
|
||||||
|
onClick={handleTabClose}
|
||||||
|
>
|
||||||
|
<CSSIcon name="x-8" className="icon-16" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Tab.displayName = 'Tab';
|
||||||
|
Tab.propTypes = {
|
||||||
|
icon: PropTypes.string,
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
isBeingDragged: PropTypes.bool.isRequired,
|
||||||
|
isItemType: PropTypes.bool,
|
||||||
|
onContextMenu: PropTypes.func.isRequired,
|
||||||
|
onDragEnd: PropTypes.func.isRequired,
|
||||||
|
onDragStart: PropTypes.func.isRequired,
|
||||||
|
onTabClick: PropTypes.func.isRequired,
|
||||||
|
onTabClose: PropTypes.func.isRequired,
|
||||||
|
onTabMouseDown: PropTypes.func.isRequired,
|
||||||
|
selected: PropTypes.bool.isRequired,
|
||||||
|
title: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const TabBar = forwardRef(function (props, ref) {
|
const TabBar = forwardRef(function (props, ref) {
|
||||||
const [tabs, setTabs] = useState([]);
|
const [tabs, setTabs] = useState([]);
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
|
@ -109,7 +166,7 @@ const TabBar = forwardRef(function (props, ref) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTabMouseDown(event, id) {
|
const handleTabMouseDown = useCallback((event, id) => {
|
||||||
// Don't select tab if it'll be closed with middle button click on mouse up
|
// Don't select tab if it'll be closed with middle button click on mouse up
|
||||||
// or on right-click
|
// or on right-click
|
||||||
if ([1, 2].includes(event.button)) {
|
if ([1, 2].includes(event.button)) {
|
||||||
|
@ -121,23 +178,23 @@ const TabBar = forwardRef(function (props, ref) {
|
||||||
}
|
}
|
||||||
props.onTabSelect(id);
|
props.onTabSelect(id);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}, [props.onTabSelect]);
|
||||||
|
|
||||||
function handleContextMenu(event, id) {
|
const handleContextMenu = useCallback((event, id) => {
|
||||||
let { screenX, screenY } = event;
|
let { screenX, screenY } = event;
|
||||||
// Popup gets immediately closed without this
|
// Popup gets immediately closed without this
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
props.onContextMenu(screenX, screenY, id);
|
props.onContextMenu(screenX, screenY, id);
|
||||||
});
|
});
|
||||||
}
|
}, [props.onContextMenu]);
|
||||||
|
|
||||||
function handleTabClick(event, id) {
|
const handleTabClick = useCallback((event, id) => {
|
||||||
if (event.button === 1) {
|
if (event.button === 1) {
|
||||||
props.onTabClose(id);
|
props.onTabClose(id);
|
||||||
}
|
}
|
||||||
}
|
}, [props.onTabClose]);
|
||||||
|
|
||||||
function handleDragStart(event, id, index) {
|
const handleDragStart = useCallback((event, id, index) => {
|
||||||
// Library tab is not draggable
|
// Library tab is not draggable
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -157,14 +214,14 @@ const TabBar = forwardRef(function (props, ref) {
|
||||||
setDragging(true);
|
setDragging(true);
|
||||||
// Store the current tab id
|
// Store the current tab id
|
||||||
dragIDRef.current = id;
|
dragIDRef.current = id;
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
function handleDragEnd() {
|
const handleDragEnd = useCallback(() => {
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
props.refocusReader();
|
props.refocusReader();
|
||||||
}
|
}, [props.refocusReader]);
|
||||||
|
|
||||||
function handleTabBarDragOver(event) {
|
const handleTabBarDragOver = useCallback((event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.dataTransfer.dropEffect = 'move';
|
event.dataTransfer.dropEffect = 'move';
|
||||||
// Throttle
|
// Throttle
|
||||||
|
@ -220,15 +277,15 @@ const TabBar = forwardRef(function (props, ref) {
|
||||||
props.onTabMove(dragIDRef.current, index);
|
props.onTabMove(dragIDRef.current, index);
|
||||||
}
|
}
|
||||||
mouseMoveWaitUntil.current = Date.now() + 20;
|
mouseMoveWaitUntil.current = Date.now() + 20;
|
||||||
}
|
}, [props.onTabMove]);
|
||||||
|
|
||||||
function handleTabClose(event, id) {
|
const handleTabClose = useCallback((event, id) => {
|
||||||
props.onTabClose(id);
|
props.onTabClose(id);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}, [props.onTabClose]);
|
||||||
|
|
||||||
|
|
||||||
function handleWheel(event) {
|
const handleWheel = useCallback((event) => {
|
||||||
// Normalize wheel speed
|
// Normalize wheel speed
|
||||||
let x = event.deltaX || event.deltaY;
|
let x = event.deltaX || event.deltaY;
|
||||||
if (x && event.deltaMode) {
|
if (x && event.deltaMode) {
|
||||||
|
@ -242,54 +299,27 @@ const TabBar = forwardRef(function (props, ref) {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
tabsRef.current.scrollLeft += x;
|
tabsRef.current.scrollLeft += x;
|
||||||
});
|
});
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
function handleClickScrollStart() {
|
const handleClickScrollStart = useCallback(() => {
|
||||||
tabsRef.current.scrollTo({
|
tabsRef.current.scrollTo({
|
||||||
left: tabsRef.current.scrollLeft - (SCROLL_ARROW_SCROLL_BY * (Zotero.rtl ? -1 : 1)),
|
left: tabsRef.current.scrollLeft - (SCROLL_ARROW_SCROLL_BY * (Zotero.rtl ? -1 : 1)),
|
||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
});
|
});
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
function handleClickScrollEnd() {
|
const handleClickScrollEnd = useCallback(() => {
|
||||||
tabsRef.current.scrollTo({
|
tabsRef.current.scrollTo({
|
||||||
left: tabsRef.current.scrollLeft + (SCROLL_ARROW_SCROLL_BY * (Zotero.rtl ? -1 : 1)),
|
left: tabsRef.current.scrollLeft + (SCROLL_ARROW_SCROLL_BY * (Zotero.rtl ? -1 : 1)),
|
||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
});
|
});
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
// Prevent maximizing/minimizing window
|
// Prevent maximizing/minimizing window
|
||||||
function handleScrollArrowDoubleClick(event) {
|
const handleScrollArrowDoubleClick = useCallback((event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
function renderTab({ id, title, selected, icon }, index) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={id}
|
|
||||||
data-id={id}
|
|
||||||
className={cx('tab', { selected, dragging: dragging && id === dragIDRef.current })}
|
|
||||||
draggable={true}
|
|
||||||
onMouseDown={(event) => handleTabMouseDown(event, id)}
|
|
||||||
onContextMenu={(event) => handleContextMenu(event, id)}
|
|
||||||
onClick={(event) => handleTabClick(event, id)}
|
|
||||||
onAuxClick={(event) => handleTabClick(event, id)}
|
|
||||||
onDragStart={(event) => handleDragStart(event, id, index)}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
tabIndex="-1"
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
<div className="tab-name" title={title}>{title}</div>
|
|
||||||
<div
|
|
||||||
className="tab-close"
|
|
||||||
onClick={(event) => handleTabClose(event, id)}
|
|
||||||
>
|
|
||||||
<CSSIcon name="x-8" className="icon-16" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
@ -301,7 +331,20 @@ const TabBar = forwardRef(function (props, ref) {
|
||||||
<div
|
<div
|
||||||
className="tabs"
|
className="tabs"
|
||||||
>
|
>
|
||||||
{tabs.length ? renderTab(tabs[0], 0) : null}
|
{tabs.length
|
||||||
|
? <Tab
|
||||||
|
{ ...tabs[0] }
|
||||||
|
key={tabs[0].id}
|
||||||
|
index={0}
|
||||||
|
isBeingDragged={ false }
|
||||||
|
onContextMenu={ handleContextMenu}
|
||||||
|
onDragEnd={ handleDragEnd }
|
||||||
|
onDragStart={ handleDragStart}
|
||||||
|
onTabClick={ handleTabClick}
|
||||||
|
onTabClose={ handleTabClose}
|
||||||
|
onTabMouseDown = { handleTabMouseDown }
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -324,7 +367,18 @@ const TabBar = forwardRef(function (props, ref) {
|
||||||
onScroll={updateScrollArrows}
|
onScroll={updateScrollArrows}
|
||||||
dir={Zotero.dir}
|
dir={Zotero.dir}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, index) => renderTab(tab, index))}
|
{tabs.map((tab, index) => <Tab
|
||||||
|
{...tab}
|
||||||
|
key={tab.id}
|
||||||
|
index={index}
|
||||||
|
isBeingDragged={dragging && dragIDRef.current === tab.id}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onTabClick={handleTabClick}
|
||||||
|
onTabClose={handleTabClose}
|
||||||
|
onTabMouseDown={handleTabMouseDown}
|
||||||
|
/>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -346,4 +400,27 @@ const TabBar = forwardRef(function (props, ref) {
|
||||||
|
|
||||||
TabBar.displayName = 'TabBar';
|
TabBar.displayName = 'TabBar';
|
||||||
|
|
||||||
|
TabBar.propTypes = {
|
||||||
|
onTabSelect: PropTypes.func.isRequired,
|
||||||
|
onTabClose: PropTypes.func.isRequired,
|
||||||
|
onTabMove: PropTypes.func.isRequired,
|
||||||
|
refocusReader: PropTypes.func.isRequired,
|
||||||
|
onContextMenu: PropTypes.func.isRequired,
|
||||||
|
tabs: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
icon: PropTypes.element.isRequired,
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
isBeingDragged: PropTypes.bool.isRequired,
|
||||||
|
onContextMenu: PropTypes.func.isRequired,
|
||||||
|
onDragEnd: PropTypes.func.isRequired,
|
||||||
|
onDragStart: PropTypes.func.isRequired,
|
||||||
|
onTabClick: PropTypes.func.isRequired,
|
||||||
|
onTabMouseDown: PropTypes.func.isRequired,
|
||||||
|
selected: PropTypes.bool.isRequired,
|
||||||
|
title: PropTypes.string.isRequired
|
||||||
|
})
|
||||||
|
).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
export default TabBar;
|
export default TabBar;
|
||||||
|
|
|
@ -99,18 +99,18 @@ var Zotero_Tabs = new function () {
|
||||||
let index = ZoteroPane.collectionsView?.selection?.focused;
|
let index = ZoteroPane.collectionsView?.selection?.focused;
|
||||||
if (typeof index !== 'undefined' && ZoteroPane.collectionsView.getRow(index)) {
|
if (typeof index !== 'undefined' && ZoteroPane.collectionsView.getRow(index)) {
|
||||||
let iconName = ZoteroPane.collectionsView.getIconName(index);
|
let iconName = ZoteroPane.collectionsView.getIconName(index);
|
||||||
icon = <CSSIcon name={iconName} className="tab-icon" />;
|
icon = { isItemType: false, icon: iconName };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (tab.data?.itemID) {
|
else if (tab.data?.itemID) {
|
||||||
try {
|
try {
|
||||||
let item = Zotero.Items.get(tab.data.itemID);
|
let item = Zotero.Items.get(tab.data.itemID);
|
||||||
icon = <CSSItemTypeIcon itemType={item.getItemTypeIconName(true)} className="tab-icon" />;
|
icon = { isItemType: true, icon: item.getItemTypeIconName(true) };
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
// item might not yet be loaded, we will get the right icon on the next update
|
// item might not yet be loaded, we will get the right icon on the next update
|
||||||
// but until then use a default placeholder
|
// but until then use a default placeholder
|
||||||
icon = <CSSItemTypeIcon className="tab-icon" />;
|
icon = { isItemType: true, icon: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ var Zotero_Tabs = new function () {
|
||||||
type: tab.type,
|
type: tab.type,
|
||||||
title: tab.title,
|
title: tab.title,
|
||||||
selected: tab.id == this._selectedID,
|
selected: tab.id == this._selectedID,
|
||||||
icon,
|
...icon,
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
// Disable File > Close menuitem if multiple tabs are open
|
// Disable File > Close menuitem if multiple tabs are open
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue