diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index bbf98acb265d4..2fe64b0c56519 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -273,10 +273,16 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { }, [width, height]); const interactor = useCallback(interaction => { - if (canvasRef.current === null) { + const canvas = canvasRef.current; + if (canvas === null) { return; } - surfaceRef.current.handleInteraction(interaction); + + const surface = surfaceRef.current; + surface.handleInteraction(interaction); + + canvas.style.cursor = surface.getCurrentCursor() || 'default'; + // Defer drawing to canvas until React's commit phase, to avoid drawing // twice and to ensure that both the canvas and DOM elements managed by // React are in sync. diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js index 6b839a39f71cb..99fbc761a0088 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -12,7 +12,13 @@ import type { FlamechartStackFrame, FlamechartStackLayer, } from '../types'; -import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base'; +import type { + Interaction, + MouseMoveInteraction, + Rect, + Size, + ViewRef, +} from '../view-base'; import { ColorView, @@ -240,7 +246,11 @@ class FlamechartStackLayerView extends View { /** * @private */ - _handleMouseMove(interaction: MouseMoveInteraction) { + _handleMouseMove( + interaction: MouseMoveInteraction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { const {_stackLayer, frame, _intrinsicSize, _onHover, visibleArea} = this; const {location} = interaction.payload; if (!_onHover || !rectContainsPoint(location, visibleArea)) { @@ -259,6 +269,8 @@ class FlamechartStackLayerView extends View { const width = durationToWidth(duration, scaleFactor); const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame)); if (x <= location.x && x + width >= location.x) { + this.currentCursor = 'pointer'; + hoveredViewRef.current = this; _onHover(flamechartStackFrame); return; } @@ -273,10 +285,16 @@ class FlamechartStackLayerView extends View { _onHover(null); } - handleInteraction(interaction: Interaction) { + _didGrab: boolean = false; + + handleInteraction( + interaction: Interaction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { switch (interaction.type) { case 'mousemove': - this._handleMouseMove(interaction); + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef); break; } } diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js index 3666f64539732..53905e2492878 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js @@ -8,7 +8,13 @@ */ import type {NativeEvent, ReactProfilerData} from '../types'; -import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base'; +import type { + Interaction, + MouseMoveInteraction, + Rect, + Size, + ViewRef, +} from '../view-base'; import { durationToWidth, @@ -72,7 +78,6 @@ export class NativeEventsView extends View { this._profilerData = profilerData; this._performPreflightComputations(); - console.log(this._depthToNativeEvent); } _performPreflightComputations() { @@ -250,7 +255,11 @@ export class NativeEventsView extends View { /** * @private */ - _handleMouseMove(interaction: MouseMoveInteraction) { + _handleMouseMove( + interaction: MouseMoveInteraction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { const {frame, _intrinsicSize, onHover, visibleArea} = this; if (!onHover) { return; @@ -279,6 +288,10 @@ export class NativeEventsView extends View { hoverTimestamp >= timestamp && hoverTimestamp <= timestamp + duration ) { + this.currentCursor = 'pointer'; + + hoveredViewRef.current = this; + onHover(nativeEvent); return; } @@ -288,10 +301,14 @@ export class NativeEventsView extends View { onHover(null); } - handleInteraction(interaction: Interaction) { + handleInteraction( + interaction: Interaction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { switch (interaction.type) { case 'mousemove': - this._handleMouseMove(interaction); + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef); break; } } diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js index f76df3bed5592..2c2c1d5e99176 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js @@ -8,7 +8,13 @@ */ import type {ReactEvent, ReactProfilerData} from '../types'; -import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base'; +import type { + Interaction, + MouseMoveInteraction, + Rect, + Size, + ViewRef, +} from '../view-base'; import { positioningScaleFactor, @@ -225,7 +231,11 @@ export class ReactEventsView extends View { /** * @private */ - _handleMouseMove(interaction: MouseMoveInteraction) { + _handleMouseMove( + interaction: MouseMoveInteraction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { const {frame, onHover, visibleArea} = this; if (!onHover) { return; @@ -260,6 +270,8 @@ export class ReactEventsView extends View { timestamp - eventTimestampAllowance <= hoverTimestamp && hoverTimestamp <= timestamp + eventTimestampAllowance ) { + this.currentCursor = 'pointer'; + hoveredViewRef.current = this; onHover(event); return; } @@ -268,10 +280,14 @@ export class ReactEventsView extends View { onHover(null); } - handleInteraction(interaction: Interaction) { + handleInteraction( + interaction: Interaction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { switch (interaction.type) { case 'mousemove': - this._handleMouseMove(interaction); + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef); break; } } diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js index 61e804223a9e7..fc46a3b270471 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js @@ -8,7 +8,13 @@ */ import type {ReactLane, ReactMeasure, ReactProfilerData} from '../types'; -import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base'; +import type { + Interaction, + MouseMoveInteraction, + Rect, + Size, + ViewRef, +} from '../view-base'; import { durationToWidth, @@ -250,7 +256,11 @@ export class ReactMeasuresView extends View { /** * @private */ - _handleMouseMove(interaction: MouseMoveInteraction) { + _handleMouseMove( + interaction: MouseMoveInteraction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { const { frame, _intrinsicSize, @@ -300,6 +310,8 @@ export class ReactMeasuresView extends View { hoverTimestamp >= timestamp && hoverTimestamp <= timestamp + duration ) { + this.currentCursor = 'pointer'; + hoveredViewRef.current = this; onHover(measure); return; } @@ -308,10 +320,14 @@ export class ReactMeasuresView extends View { onHover(null); } - handleInteraction(interaction: Interaction) { + handleInteraction( + interaction: Interaction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { switch (interaction.type) { case 'mousemove': - this._handleMouseMove(interaction); + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef); break; } } diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js b/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js index 7a822716db6b0..7ac95c82253a8 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js @@ -8,7 +8,13 @@ */ import type {UserTimingMark} from '../types'; -import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base'; +import type { + Interaction, + MouseMoveInteraction, + Rect, + Size, + ViewRef, +} from '../view-base'; import { positioningScaleFactor, @@ -185,7 +191,11 @@ export class UserTimingMarksView extends View { /** * @private */ - _handleMouseMove(interaction: MouseMoveInteraction) { + _handleMouseMove( + interaction: MouseMoveInteraction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { const {frame, onHover, visibleArea} = this; if (!onHover) { return; @@ -218,6 +228,8 @@ export class UserTimingMarksView extends View { timestamp - markTimestampAllowance <= hoverTimestamp && hoverTimestamp <= timestamp + markTimestampAllowance ) { + this.currentCursor = 'pointer'; + hoveredViewRef.current = this; onHover(mark); return; } @@ -226,10 +238,14 @@ export class UserTimingMarksView extends View { onHover(null); } - handleInteraction(interaction: Interaction) { + handleInteraction( + interaction: Interaction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { switch (interaction.type) { case 'mousemove': - this._handleMouseMove(interaction); + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef); break; } } diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js b/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js index d294e4d0fa8a1..706159ac40d19 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js @@ -19,6 +19,7 @@ import type { } from './useCanvasInteraction'; import type {Rect} from './geometry'; import type {ScrollState} from './utils/scrollState'; +import type {ViewRef} from './Surface'; import {Surface} from './Surface'; import {View} from './View'; @@ -155,13 +156,39 @@ export class HorizontalPanAndZoomView extends View { this._setScrollState(newState); } - _handleMouseDown(interaction: MouseDownInteraction) { + _handleMouseDown( + interaction: MouseDownInteraction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { if (rectContainsPoint(interaction.payload.location, this.frame)) { this._isPanning = true; + + activeViewRef.current = this; + + this.currentCursor = 'grabbing'; } } - _handleMouseMove(interaction: MouseMoveInteraction) { + _handleMouseMove( + interaction: MouseMoveInteraction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { + const isHovered = rectContainsPoint( + interaction.payload.location, + this.frame, + ); + if (isHovered) { + hoveredViewRef.current = this; + } + + if (activeViewRef.current === this) { + this.currentCursor = 'grabbing'; + } else if (isHovered) { + this.currentCursor = 'grab'; + } + if (!this._isPanning) { return; } @@ -173,10 +200,18 @@ export class HorizontalPanAndZoomView extends View { this._setStateAndInformCallbacksIfChanged(newState); } - _handleMouseUp(interaction: MouseUpInteraction) { + _handleMouseUp( + interaction: MouseUpInteraction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { if (this._isPanning) { this._isPanning = false; } + + if (activeViewRef.current === this) { + activeViewRef.current = null; + } } _handleWheelPlain(interaction: WheelPlainInteraction) { @@ -238,16 +273,20 @@ export class HorizontalPanAndZoomView extends View { this._setStateAndInformCallbacksIfChanged(newState); } - handleInteraction(interaction: Interaction) { + handleInteraction( + interaction: Interaction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { switch (interaction.type) { case 'mousedown': - this._handleMouseDown(interaction); + this._handleMouseDown(interaction, activeViewRef, hoveredViewRef); break; case 'mousemove': - this._handleMouseMove(interaction); + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef); break; case 'mouseup': - this._handleMouseUp(interaction); + this._handleMouseUp(interaction, activeViewRef, hoveredViewRef); break; case 'wheel-plain': this._handleWheelPlain(interaction); diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js index c69b982c47cdc..3b9edde41e787 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js @@ -14,6 +14,7 @@ import type { MouseUpInteraction, } from './useCanvasInteraction'; import type {Rect, Size} from './geometry'; +import type {ViewRef} from './Surface'; import {COLORS} from '../content-views/constants'; import nullthrows from 'nullthrows'; @@ -82,28 +83,49 @@ class ResizeBar extends View { this._updateColor(); } - _handleMouseDown(interaction: MouseDownInteraction) { + _handleMouseDown( + interaction: MouseDownInteraction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { const cursorInView = rectContainsPoint( interaction.payload.location, this.frame, ); if (cursorInView) { this._setInteractionState('dragging'); + activeViewRef.current = this; } } - _handleMouseMove(interaction: MouseMoveInteraction) { + _handleMouseMove( + interaction: MouseMoveInteraction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { const cursorInView = rectContainsPoint( interaction.payload.location, this.frame, ); + + if (cursorInView || activeViewRef.current === this) { + this.currentCursor = 'ns-resize'; + } + if (cursorInView) { + hoveredViewRef.current = this; + } + if (this._interactionState === 'dragging') { return; } this._setInteractionState(cursorInView ? 'hovered' : 'normal'); } - _handleMouseUp(interaction: MouseUpInteraction) { + _handleMouseUp( + interaction: MouseUpInteraction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { const cursorInView = rectContainsPoint( interaction.payload.location, this.frame, @@ -111,18 +133,26 @@ class ResizeBar extends View { if (this._interactionState === 'dragging') { this._setInteractionState(cursorInView ? 'hovered' : 'normal'); } + + if (activeViewRef.current === this) { + activeViewRef.current = null; + } } - handleInteraction(interaction: Interaction) { + handleInteraction( + interaction: Interaction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { switch (interaction.type) { case 'mousedown': - this._handleMouseDown(interaction); + this._handleMouseDown(interaction, activeViewRef, hoveredViewRef); return; case 'mousemove': - this._handleMouseMove(interaction); + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef); return; case 'mouseup': - this._handleMouseUp(interaction); + this._handleMouseUp(interaction, activeViewRef, hoveredViewRef); return; } } @@ -281,18 +311,6 @@ export class ResizableSplitView extends View { } _handleMouseMove(interaction: MouseMoveInteraction) { - const cursorLocation = interaction.payload.location; - const resizeBarFrame = this._getResizeBar().frame; - - const canvas = this._canvasRef.current; - if (canvas !== null) { - if (rectContainsPoint(cursorLocation, resizeBarFrame)) { - canvas.style.cursor = 'ns-resize'; - } else { - canvas.style.cursor = 'default'; - } - } - const {_resizingState} = this; if (_resizingState) { this._resizingState = { @@ -309,7 +327,23 @@ export class ResizableSplitView extends View { } } - handleInteraction(interaction: Interaction) { + _didGrab: boolean = false; + + getCursorActiveSubView(interaction: Interaction): View | null { + const cursorLocation = interaction.payload.location; + const resizeBarFrame = this._getResizeBar().frame; + if (rectContainsPoint(cursorLocation, resizeBarFrame)) { + return this; + } else { + return null; + } + } + + handleInteraction( + interaction: Interaction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { switch (interaction.type) { case 'mousedown': this._handleMouseDown(interaction); diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js b/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js index eb4285da7be78..aa5943c107a97 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js @@ -10,11 +10,14 @@ import type {Interaction} from './useCanvasInteraction'; import type {Size} from './geometry'; +import {createRef} from 'react'; import memoize from 'memoize-one'; import {View} from './View'; import {zeroPoint} from './geometry'; +export type ViewRef = {|current: View | null|}; + // hidpi canvas: https://www.html5rocks.com/en/tutorials/canvas/hidpi/ function configureRetinaCanvas(canvas, height, width) { const dpr: number = window.devicePixelRatio || 1; @@ -48,9 +51,13 @@ const getCanvasContext = memoize( */ export class Surface { rootView: ?View; + _context: ?CanvasRenderingContext2D; _canvasSize: ?Size; + _activeViewRef: ViewRef = createRef(); + _hoveredViewRef: ViewRef = createRef(); + setCanvas(canvas: HTMLCanvasElement, canvasSize: Size) { this._context = getCanvasContext( canvas, @@ -80,10 +87,28 @@ export class Surface { rootView.displayIfNeeded(_context); } + getCurrentCursor(): string | null { + const activeView = this._activeViewRef.current; + if (activeView !== null) { + return activeView.currentCursor; + } else { + const hoveredView = this._hoveredViewRef.current; + if (hoveredView !== null) { + return hoveredView.currentCursor; + } + } + + return null; + } + handleInteraction(interaction: Interaction) { if (!this.rootView) { return; } - this.rootView.handleInteractionAndPropagateToSubviews(interaction); + this.rootView.handleInteractionAndPropagateToSubviews( + interaction, + this._activeViewRef, + this._hoveredViewRef, + ); } } diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/View.js b/packages/react-devtools-scheduling-profiler/src/view-base/View.js index d92a55b6fe573..678ed4ee18f46 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/View.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/View.js @@ -10,6 +10,7 @@ import type {Interaction} from './useCanvasInteraction'; import type {Rect, Size} from './geometry'; import type {Layouter} from './layouter'; +import type {ViewRef} from './Surface'; import {Surface} from './Surface'; import { @@ -28,6 +29,8 @@ import {noopLayout, viewsToLayout, collapseLayoutIntoViews} from './layouter'; * subclasses. */ export class View { + currentCursor: string | null = null; + surface: Surface; frame: Rect; @@ -253,7 +256,11 @@ export class View { // Internal note: Do not call directly! Use // `handleInteractionAndPropagateToSubviews` so that interactions are // propagated to subviews. - handleInteraction(interaction: Interaction) {} + handleInteraction( + interaction: Interaction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) {} /** * Handle an `interaction` and propagates it to all of this view's @@ -265,10 +272,18 @@ export class View { * @see handleInteraction * @protected */ - handleInteractionAndPropagateToSubviews(interaction: Interaction) { - this.handleInteraction(interaction); + handleInteractionAndPropagateToSubviews( + interaction: Interaction, + activeViewRef: ViewRef, + hoveredViewRef: ViewRef, + ) { + this.handleInteraction(interaction, activeViewRef, hoveredViewRef); this.subviews.forEach(subview => - subview.handleInteractionAndPropagateToSubviews(interaction), + subview.handleInteractionAndPropagateToSubviews( + interaction, + activeViewRef, + hoveredViewRef, + ), ); } }