diff --git a/src/components/marker-chart/Canvas.js b/src/components/marker-chart/Canvas.js index 86de442a9a..d2e68f0033 100644 --- a/src/components/marker-chart/Canvas.js +++ b/src/components/marker-chart/Canvas.js @@ -18,6 +18,7 @@ import memoize from 'memoize-immutable'; import { typeof updatePreviewSelection as UpdatePreviewSelection, typeof changeRightClickedMarker as ChangeRightClickedMarker, + typeof changeMouseTimePosition as ChangeMouseTimePosition, } from 'firefox-profiler/actions/profile-view'; import { TIMELINE_MARGIN_LEFT } from 'firefox-profiler/app-logic/constants'; import type { @@ -79,6 +80,7 @@ type OwnProps = {| +getMarker: (MarkerIndex) => Marker, +threadsKey: ThreadsKey, +updatePreviewSelection: WrapFunctionInDispatch, + +changeMouseTimePosition: ChangeMouseTimePosition, +changeRightClickedMarker: ChangeRightClickedMarker, +marginLeft: CssPixels, +marginRight: CssPixels, @@ -757,6 +759,37 @@ class MarkerChartCanvasImpl extends React.PureComponent { return { markerIndex, rowIndexOfLabel }; }; + onMouseMove = (event: { nativeEvent: MouseEvent }) => { + const { + changeMouseTimePosition, + rangeStart, + rangeEnd, + marginLeft, + marginRight, + viewport: { viewportLeft, viewportRight, containerWidth }, + } = this.props; + const viewportLength: UnitIntervalOfProfileRange = + viewportRight - viewportLeft; + const markerContainerWidth = containerWidth - marginLeft - marginRight; + // This is the x position in terms of unit interval (so, between 0 and 1). + const xInUnitInterval: UnitIntervalOfProfileRange = + viewportLeft + + viewportLength * + ((event.nativeEvent.offsetX - marginLeft) / markerContainerWidth); + + if (xInUnitInterval < 0 || xInUnitInterval > 1) { + changeMouseTimePosition(null); + } else { + const rangeLength: Milliseconds = rangeEnd - rangeStart; + const xInTime: Milliseconds = rangeStart + xInUnitInterval * rangeLength; + changeMouseTimePosition(xInTime); + } + }; + + onMouseLeave = () => { + this.props.changeMouseTimePosition(null); + }; + onDoubleClickMarker = (hoveredItems: HoveredMarkerChartItems | null) => { const markerIndex = hoveredItems === null ? null : hoveredItems.markerIndex; if (markerIndex === null) { @@ -818,6 +851,8 @@ class MarkerChartCanvasImpl extends React.PureComponent { getHoveredItemInfo={this.getHoveredMarkerInfo} drawCanvas={this.drawCanvas} hitTest={this.hitTest} + onMouseMove={this.onMouseMove} + onMouseLeave={this.onMouseLeave} /> ); } diff --git a/src/components/marker-chart/index.js b/src/components/marker-chart/index.js index 17f926593d..fb0ff81d7b 100644 --- a/src/components/marker-chart/index.js +++ b/src/components/marker-chart/index.js @@ -23,6 +23,7 @@ import { getTimelineMarginLeft } from 'firefox-profiler/selectors/app'; import { updatePreviewSelection, changeRightClickedMarker, + changeMouseTimePosition, } from 'firefox-profiler/actions/profile-view'; import { ContextMenuTrigger } from 'firefox-profiler/components/shared/ContextMenuTrigger'; @@ -47,6 +48,7 @@ const ROW_HEIGHT = 16; type DispatchProps = {| +updatePreviewSelection: typeof updatePreviewSelection, +changeRightClickedMarker: typeof changeRightClickedMarker, + +changeMouseTimePosition: typeof changeMouseTimePosition, |}; type StateProps = {| @@ -106,6 +108,7 @@ class MarkerChartImpl extends React.PureComponent { getMarker, previewSelection, updatePreviewSelection, + changeMouseTimePosition, changeRightClickedMarker, rightClickedMarkerIndex, timelineMarginLeft, @@ -150,6 +153,7 @@ class MarkerChartImpl extends React.PureComponent { getMarker, // $FlowFixMe Error introduced by upgrading to v0.96.0. See issue #1936. updatePreviewSelection, + changeMouseTimePosition, changeRightClickedMarker, rangeStart: timeRange.start, rangeEnd: timeRange.end, @@ -194,6 +198,10 @@ export const MarkerChart = explicitConnect<{||}, StateProps, DispatchProps>({ timelineTrackOrganization: getTimelineTrackOrganization(state), }; }, - mapDispatchToProps: { updatePreviewSelection, changeRightClickedMarker }, + mapDispatchToProps: { + updatePreviewSelection, + changeMouseTimePosition, + changeRightClickedMarker, + }, component: MarkerChartImpl, }); diff --git a/src/components/shared/chart/Canvas.js b/src/components/shared/chart/Canvas.js index ffacb54b6b..4d896e5b2a 100644 --- a/src/components/shared/chart/Canvas.js +++ b/src/components/shared/chart/Canvas.js @@ -30,6 +30,9 @@ type Props = {| // Default to true. Set to false if the chart should be redrawn right away after // rerender. +drawCanvasAfterRaf?: boolean, + + +onMouseMove?: (e: { nativeEvent: MouseEvent }) => mixed, + +onMouseLeave?: (e: { nativeEvent: MouseEvent }) => mixed, |}; // The naming of the X and Y coordinates here correspond to the ones @@ -223,6 +226,14 @@ export class ChartCanvas extends React.Component< } }; + _onMouseLeave = ( + event: { nativeEvent: MouseEvent } & SyntheticMouseEvent<> + ) => { + if (this.props.onMouseLeave) { + this.props.onMouseLeave(event); + } + }; + _onMouseMove = ( event: { nativeEvent: MouseEvent } & SyntheticMouseEvent<> ) => { @@ -230,6 +241,10 @@ export class ChartCanvas extends React.Component< return; } + if (this.props.onMouseMove) { + this.props.onMouseMove(event); + } + this._offsetX = event.nativeEvent.offsetX; this._offsetY = event.nativeEvent.offsetY; // event.buttons is a bitfield representing which buttons are pressed at the @@ -347,6 +362,7 @@ export class ChartCanvas extends React.Component< ref={this._takeCanvasRef} onMouseDown={this._onMouseDown} onClick={this._onClick} + onMouseLeave={this._onMouseLeave} onMouseMove={this._onMouseMove} onMouseOut={this._onMouseOut} onDoubleClick={this._onDoubleClick} diff --git a/src/components/stack-chart/Canvas.js b/src/components/stack-chart/Canvas.js index 7a7ccc12c4..a845117067 100644 --- a/src/components/stack-chart/Canvas.js +++ b/src/components/stack-chart/Canvas.js @@ -15,7 +15,10 @@ import { ChartCanvas } from '../shared/chart/Canvas'; import { FastFillStyle } from '../../utils'; import TextMeasurement from '../../utils/text-measurement'; import { formatMilliseconds } from '../../utils/format-numbers'; -import { updatePreviewSelection } from '../../actions/profile-view'; +import { + updatePreviewSelection, + typeof changeMouseTimePosition as ChangeMouseTimePosition, +} from '../../actions/profile-view'; import { mapCategoryColorNameToStackChartStyles } from '../../utils/colors'; import { TooltipCallNode } from '../tooltip/CallNode'; import { TooltipMarker } from '../tooltip/Marker'; @@ -63,6 +66,7 @@ type OwnProps = {| +updatePreviewSelection: WrapFunctionInDispatch< typeof updatePreviewSelection, >, + +changeMouseTimePosition: ChangeMouseTimePosition, +getMarker: (MarkerIndex) => Marker, +categories: CategoryList, +callNodeInfo: CallNodeInfo, @@ -579,6 +583,36 @@ class StackChartCanvasImpl extends React.PureComponent { return null; }; + onMouseMove = (event: { nativeEvent: MouseEvent }) => { + const { + changeMouseTimePosition, + rangeStart, + rangeEnd, + marginLeft, + viewport: { viewportLeft, viewportRight, containerWidth }, + } = this.props; + + const innerDevicePixelsWidth = + containerWidth - marginLeft - TIMELINE_MARGIN_RIGHT; + const rangeLength: Milliseconds = rangeEnd - rangeStart; + const viewportLength: UnitIntervalOfProfileRange = + viewportRight - viewportLeft; + const unitIntervalTime: UnitIntervalOfProfileRange = + viewportLeft + + viewportLength * + ((event.nativeEvent.offsetX - marginLeft) / innerDevicePixelsWidth); + if (unitIntervalTime < 0 || unitIntervalTime > 1) { + changeMouseTimePosition(null); + } else { + const time: Milliseconds = rangeStart + unitIntervalTime * rangeLength; + changeMouseTimePosition(time); + } + }; + + onMouseLeave = () => { + this.props.changeMouseTimePosition(null); + }; + render() { const { containerWidth, containerHeight, isDragging } = this.props.viewport; @@ -593,6 +627,8 @@ class StackChartCanvasImpl extends React.PureComponent { getHoveredItemInfo={this._getHoveredStackInfo} drawCanvas={this._drawCanvas} hitTest={this._hitTest} + onMouseMove={this.onMouseMove} + onMouseLeave={this.onMouseLeave} onSelectItem={this._onSelectItem} onRightClick={this._onRightClick} /> diff --git a/src/components/stack-chart/index.js b/src/components/stack-chart/index.js index 8ed6344774..85522c1c6b 100644 --- a/src/components/stack-chart/index.js +++ b/src/components/stack-chart/index.js @@ -35,6 +35,7 @@ import { changeRightClickedCallNode, handleCallNodeTransformShortcut, updateBottomBoxContentsAndMaybeOpen, + changeMouseTimePosition, } from '../../actions/profile-view'; import { getCallNodePathFromIndex } from '../../profile-logic/profile-data'; @@ -94,6 +95,7 @@ type DispatchProps = {| +updatePreviewSelection: typeof updatePreviewSelection, +handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut, +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, + +changeMouseTimePosition: typeof changeMouseTimePosition, |}; type Props = ConnectedProps<{||}, StateProps, DispatchProps>; @@ -209,6 +211,7 @@ class StackChartImpl extends React.PureComponent { interval, previewSelection, updatePreviewSelection, + changeMouseTimePosition, callNodeInfo, categories, selectedCallNodeIndex, @@ -263,6 +266,7 @@ class StackChartImpl extends React.PureComponent { getMarker, // $FlowFixMe Error introduced by upgrading to v0.96.0. See issue #1936. updatePreviewSelection, + changeMouseTimePosition, rangeStart: timeRange.start, rangeEnd: timeRange.end, stackFrameHeight: STACK_FRAME_HEIGHT, @@ -324,6 +328,7 @@ export const StackChart = explicitConnect<{||}, StateProps, DispatchProps>({ updatePreviewSelection, handleCallNodeTransformShortcut, updateBottomBoxContentsAndMaybeOpen, + changeMouseTimePosition, }, component: StackChartImpl, }); diff --git a/src/components/timeline/Selection.css b/src/components/timeline/Selection.css index 79a27a832a..8552eb9139 100644 --- a/src/components/timeline/Selection.css +++ b/src/components/timeline/Selection.css @@ -24,16 +24,11 @@ z-index: 1; top: 0; bottom: 0; - display: none; width: 1px; background: rgb(0 0 0 / 0.4); pointer-events: none; } -.timelineSelection:hover > .timelineSelectionHoverLine { - display: block; -} - .timelineSelectionOverlay { position: absolute; z-index: 2; diff --git a/src/components/timeline/Selection.js b/src/components/timeline/Selection.js index 5f1a1266f8..8229d5695a 100644 --- a/src/components/timeline/Selection.js +++ b/src/components/timeline/Selection.js @@ -262,6 +262,10 @@ class TimelineRulerAndSelection extends React.PureComponent { } }; + _onMouseLeave = () => { + this.props.changeMouseTimePosition(null); + }; + _makeOnMove = (fun: (number) => { startDelta: number, endDelta: number }) => ( @@ -412,6 +416,7 @@ class TimelineRulerAndSelection extends React.PureComponent { ref={this._containerCreated} onMouseDown={this._onMouseDown} onMouseMove={this._onMouseMove} + onMouseLeave={this._onMouseLeave} > {children} {previewSelection.hasSelection diff --git a/src/reducers/profile-view.js b/src/reducers/profile-view.js index a2d2ee89b3..d0e205f0b0 100644 --- a/src/reducers/profile-view.js +++ b/src/reducers/profile-view.js @@ -697,7 +697,6 @@ const hoveredMarker: Reducer = ( }; /** - * TODO: This is not used yet, see issue #222 * This is for tracking mouse position in timeline-axis */ const mouseTimePosition: Reducer = ( diff --git a/src/test/components/MarkerChart.test.js b/src/test/components/MarkerChart.test.js index 338816da50..2d26d3454f 100644 --- a/src/test/components/MarkerChart.test.js +++ b/src/test/components/MarkerChart.test.js @@ -328,6 +328,58 @@ describe('MarkerChart', function () { expect(drawLogBefore.length > drawLogAfter.length * 2).toBe(true); }); + it('changes the mouse time position when the mouse moves', () => { + window.devicePixelRatio = 1; + + const profile = getProfileWithMarkers(MARKERS); + const { flushRafCalls, getState, dispatch, fireMouseEvent } = + setupWithProfile(profile); + + dispatch(changeSelectedTab('marker-chart')); + flushRafCalls(); + + const drawLogBefore = flushDrawLog(); + + // Expect the mouseTimePosition to not be set at the beginning of the test. + expect(getState().profileView.viewOptions.mouseTimePosition).toBeNull(); + + // Move the mouse on top of an item, ensure mouseTimePosition is set. + const { x, y } = findFillTextPositionFromDrawLog(drawLogBefore, 'Marker B'); + fireMouseEvent('mousemove', { + offsetX: x, + offsetY: y, + pageX: x, + pageY: y, + }); + const mouseTimePosition = + getState().profileView.viewOptions.mouseTimePosition; + expect(typeof mouseTimePosition).toEqual('number'); + + // Move the mouse on top of another item, ensure mouseTimePosition changed. + const { x: x2, y: y2 } = findFillTextPositionFromDrawLog( + drawLogBefore, + 'Marker A' + ); + expect(x2).not.toEqual(x); + fireMouseEvent('mousemove', { + offsetX: x2, + offsetY: y2, + pageX: x2, + pageY: y2, + }); + expect(getState().profileView.viewOptions.mouseTimePosition).not.toEqual( + mouseTimePosition + ); + + // Move the mouse out of the marker chart, ensure mouseTimePosition is no + // longer set. + // React uses mouseover/mouseout events to implement mouseenter/mouseleave. + // See https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31 + fireMouseEvent('mouseout', {}); + + expect(getState().profileView.viewOptions.mouseTimePosition).toBeNull(); + }); + describe('context menus', () => { beforeEach(() => { // Always use fake timers when dealing with context menus.