/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { createFactory, createRef, PureComponent, } = require("resource://devtools/client/shared/vendor/react.mjs"); const { connect, } = require("resource://devtools/client/shared/vendor/react-redux.js"); const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); const AnimationList = createFactory( require("resource://devtools/client/inspector/animation/components/AnimationList.js") ); const CurrentTimeScrubber = createFactory( require("resource://devtools/client/inspector/animation/components/CurrentTimeScrubber.js") ); const ProgressInspectionPanel = createFactory( require("resource://devtools/client/inspector/animation/components/ProgressInspectionPanel.js") ); const { findOptimalTimeInterval, } = require("resource://devtools/client/inspector/animation/utils/utils.js"); const { getStr, } = require("resource://devtools/client/inspector/animation/utils/l10n.js"); const { throttle } = require("resource://devtools/shared/throttle.js"); // The minimum spacing between 2 time graduation headers in the timeline (px). const TIME_GRADUATION_MIN_SPACING = 40; class AnimationListContainer extends PureComponent { static get propTypes() { return { addAnimationsCurrentTimeListener: PropTypes.func.isRequired, animations: PropTypes.arrayOf(PropTypes.object).isRequired, direction: PropTypes.string.isRequired, dispatch: PropTypes.func.isRequired, getAnimatedPropertyMap: PropTypes.func.isRequired, getNodeFromActor: PropTypes.func.isRequired, removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, selectAnimation: PropTypes.func.isRequired, setAnimationsCurrentTime: PropTypes.func.isRequired, setHighlightedNode: PropTypes.func.isRequired, setSelectedNode: PropTypes.func.isRequired, sidebarWidth: PropTypes.number.isRequired, simulateAnimation: PropTypes.func.isRequired, timeScale: PropTypes.object.isRequired, }; } constructor(props) { super(props); this._ref = createRef(); this.updateDisplayableRange = throttle( this.updateDisplayableRange, 100, this ); this.state = { // tick labels and lines on the progress inspection panel ticks: [], // Displayable range. displayableRange: { startIndex: 0, endIndex: 0 }, }; } componentDidMount() { this.updateTicks(this.props); const current = this._ref.current; this._inspectionPanelEl = current.querySelector( ".progress-inspection-panel" ); this._inspectionPanelEl.addEventListener("scroll", () => { this.updateDisplayableRange(); }); this._animationListEl = current.querySelector(".animation-list"); const resizeObserver = new current.ownerGlobal.ResizeObserver(() => { this.updateDisplayableRange(); }); resizeObserver.observe(this._animationListEl); const animationItemEl = current.querySelector(".animation-item"); this._itemHeight = animationItemEl.offsetHeight; this.updateDisplayableRange(); } componentDidUpdate(prevProps) { const { timeScale, sidebarWidth } = this.props; if ( timeScale.getDuration() !== prevProps.timeScale.getDuration() || timeScale.zeroPositionTime !== prevProps.timeScale.zeroPositionTime || sidebarWidth !== prevProps.sidebarWidth ) { this.updateTicks(this.props); } } /** * Since it takes too much time if we render all of animation graphs, * we restrict to render the items that are not in displaying area. * This function calculates the displayable item range. */ updateDisplayableRange() { const count = Math.floor(this._animationListEl.offsetHeight / this._itemHeight) + 1; const index = Math.floor( this._inspectionPanelEl.scrollTop / this._itemHeight ); this.setState({ displayableRange: { startIndex: index, endIndex: index + count }, }); } updateTicks(props) { const { animations, timeScale } = props; const tickLinesEl = this._ref.current.querySelector(".tick-lines"); const width = tickLinesEl.offsetWidth; const animationDuration = timeScale.getDuration(); const minTimeInterval = (TIME_GRADUATION_MIN_SPACING * animationDuration) / width; const intervalLength = findOptimalTimeInterval(minTimeInterval); const intervalWidth = (intervalLength * width) / animationDuration; const tickCount = parseInt(width / intervalWidth, 10); const isAllDurationInfinity = animations.every( animation => animation.state.duration === Infinity ); const zeroBasePosition = width * (timeScale.zeroPositionTime / animationDuration); const shiftWidth = zeroBasePosition % intervalWidth; const needToShift = zeroBasePosition !== 0 && shiftWidth !== 0; const ticks = []; // Need to display first graduation since position will be shifted. if (needToShift) { const label = timeScale.formatTime(timeScale.distanceToRelativeTime(0)); ticks.push({ position: 0, label, width: shiftWidth }); } for (let i = 0; i <= tickCount; i++) { const position = ((i * intervalWidth + shiftWidth) * 100) / width; const distance = timeScale.distanceToRelativeTime(position); const label = isAllDurationInfinity && i === tickCount ? getStr("player.infiniteTimeLabel") : timeScale.formatTime(distance); ticks.push({ position, label, width: intervalWidth }); } this.setState({ ticks }); } render() { const { addAnimationsCurrentTimeListener, animations, direction, dispatch, getAnimatedPropertyMap, getNodeFromActor, removeAnimationsCurrentTimeListener, selectAnimation, setAnimationsCurrentTime, setHighlightedNode, setSelectedNode, simulateAnimation, timeScale, } = this.props; const { displayableRange, ticks } = this.state; return dom.div( { className: "animation-list-container", ref: this._ref, }, ProgressInspectionPanel({ indicator: CurrentTimeScrubber({ addAnimationsCurrentTimeListener, direction, removeAnimationsCurrentTimeListener, setAnimationsCurrentTime, timeScale, }), list: AnimationList({ animations, dispatch, displayableRange, getAnimatedPropertyMap, getNodeFromActor, selectAnimation, setHighlightedNode, setSelectedNode, simulateAnimation, timeScale, }), ticks, }) ); } } const mapStateToProps = state => { return { sidebarWidth: state.animations.sidebarSize ? state.animations.sidebarSize.width : 0, }; }; module.exports = connect(mapStateToProps)(AnimationListContainer);