diff --git a/src/CanvasPage.js b/src/CanvasPage.js index 211400f..3d021ae 100644 --- a/src/CanvasPage.js +++ b/src/CanvasPage.js @@ -140,14 +140,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { surfaceRef.current, {origin: zeroPoint, size: {width, height}}, data.flamechart, - data, + data.duration, ); flamechartViewRef.current = flamechartView; const flamechartVScrollWrapper = new VerticalScrollView( surfaceRef.current, {origin: zeroPoint, size: {width, height}}, flamechartView, - flamechartView.intrinsicSize.height, ); const stackedZoomables = new StaticLayoutView( @@ -166,7 +165,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { surfaceRef.current, {origin: zeroPoint, size: {width, height}}, stackedZoomables, - flamechartView.intrinsicSize.width, + reactEventsView.intrinsicSize.width, ); rootViewRef.current = new StaticLayoutView( @@ -250,7 +249,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { const {current: flamechartView} = flamechartViewRef; if (flamechartView) { - flamechartView.onHover = flamechartStackFrame => { + flamechartView.setOnHover(flamechartStackFrame => { if ( !hoveredEvent || hoveredEvent.flamechartStackFrame !== flamechartStackFrame @@ -262,7 +261,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { data, }); } - }; + }); } }, [ reactEventsViewRef, @@ -287,7 +286,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { const {current: flamechartView} = flamechartViewRef; if (flamechartView) { - flamechartView.setHoveredFlamechartNode( + flamechartView.setHoveredFlamechartStackFrame( hoveredEvent ? hoveredEvent.flamechartStackFrame : null, ); } diff --git a/src/canvas/views/FlamechartView.js b/src/canvas/views/FlamechartView.js index 2ae144e..a6e0c0a 100644 --- a/src/canvas/views/FlamechartView.js +++ b/src/canvas/views/FlamechartView.js @@ -4,17 +4,21 @@ import type {Interaction, HoverInteraction} from '../../useCanvasInteraction'; import type { Flamechart, FlamechartStackFrame, - ReactProfilerData, + FlamechartStackLayer, } from '../../types'; import type {Rect, Size} from '../../layout'; import { + ColorView, View, Surface, + StaticLayoutView, rectContainsPoint, rectEqualToRect, rectIntersectsRect, rectIntersectionWithRect, + layeredLayout, + verticallyStackedLayout, } from '../../layout'; import { durationToWidth, @@ -30,9 +34,13 @@ import { REACT_WORK_BORDER_SIZE, } from '../constants'; -export class FlamechartView extends View { - flamechart: Flamechart; - profilerData: ReactProfilerData; +class FlamechartStackLayerView extends View { + /** Layer to display */ + stackLayer: FlamechartStackLayer; + + /** A set of `stackLayer`'s frames, for efficient lookup. */ + stackFrameSet: Set; + intrinsicSize: Size; hoveredStackFrame: FlamechartStackFrame | null = null; @@ -41,15 +49,15 @@ export class FlamechartView extends View { constructor( surface: Surface, frame: Rect, - flamechart: Flamechart, - profilerData: ReactProfilerData, + stackLayer: FlamechartStackLayer, + duration: number, ) { super(surface, frame); - this.flamechart = flamechart; - this.profilerData = profilerData; + this.stackLayer = stackLayer; + this.stackFrameSet = new Set(stackLayer); this.intrinsicSize = { - width: this.profilerData.duration, - height: this.flamechart.length * FLAMECHART_FRAME_HEIGHT, + width: duration, + height: FLAMECHART_FRAME_HEIGHT, }; } @@ -57,19 +65,30 @@ export class FlamechartView extends View { return this.intrinsicSize; } - setHoveredFlamechartNode(hoveredStackFrame: FlamechartStackFrame | null) { + setHoveredFlamechartStackFrame( + hoveredStackFrame: FlamechartStackFrame | null, + ) { if (this.hoveredStackFrame === hoveredStackFrame) { - return; + return; // We're already hovering over this frame + } + + // Only care about frames displayed by this view. + const stackFrameToSet = + hoveredStackFrame && this.stackFrameSet.has(hoveredStackFrame) + ? hoveredStackFrame + : null; + if (this.hoveredStackFrame === stackFrameToSet) { + return; // Resulting state is unchanged } - this.hoveredStackFrame = hoveredStackFrame; + this.hoveredStackFrame = stackFrameToSet; this.setNeedsDisplay(); } draw(context: CanvasRenderingContext2D) { const { frame, - flamechart, - hoveredStackFrame: hoveredFlamechartNode, + stackLayer, + hoveredStackFrame, intrinsicSize, visibleArea, } = this; @@ -88,89 +107,73 @@ export class FlamechartView extends View { const scaleFactor = positioningScaleFactor(intrinsicSize.width, frame); - for (let i = 0; i < flamechart.length; i++) { - const stackLayer = flamechart[i]; + for (let i = 0; i < stackLayer.length; i++) { + const {name, timestamp, duration} = stackLayer[i]; - const layerY = Math.floor(frame.origin.y + i * FLAMECHART_FRAME_HEIGHT); - if ( - layerY + FLAMECHART_FRAME_HEIGHT < visibleArea.origin.y || - visibleArea.origin.y + visibleArea.size.height < layerY - ) { - continue; // Not in view + const width = durationToWidth(duration, scaleFactor); + if (width < 1) { + continue; // Too small to render at this zoom level } - for (let j = 0; j < stackLayer.length; j++) { - const {name, timestamp, duration} = stackLayer[j]; - - const width = durationToWidth(duration, scaleFactor); - if (width < 1) { - continue; // Too small to render at this zoom level - } + const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame)); + const nodeRect: Rect = { + origin: {x, y: frame.origin.y}, + size: { + width: Math.floor(width - REACT_WORK_BORDER_SIZE), + height: Math.floor(FLAMECHART_FRAME_HEIGHT - REACT_WORK_BORDER_SIZE), + }, + }; + if (!rectIntersectsRect(nodeRect, visibleArea)) { + continue; // Not in view + } - const x = Math.floor( - timestampToPosition(timestamp, scaleFactor, frame), + const showHoverHighlight = hoveredStackFrame === stackLayer[i]; + context.fillStyle = showHoverHighlight + ? COLORS.FLAME_CHART_HOVER + : COLORS.FLAME_CHART; + + const drawableRect = rectIntersectionWithRect(nodeRect, visibleArea); + context.fillRect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + drawableRect.size.height, + ); + + if (width > FLAMECHART_TEXT_PADDING * 2) { + const trimmedName = trimFlamechartText( + context, + name, + width - FLAMECHART_TEXT_PADDING * 2 + (x < 0 ? x : 0), ); - const nodeRect: Rect = { - origin: {x, y: layerY}, - size: { - width: Math.floor(width - REACT_WORK_BORDER_SIZE), - height: Math.floor( - FLAMECHART_FRAME_HEIGHT - REACT_WORK_BORDER_SIZE, - ), - }, - }; - if (!rectIntersectsRect(nodeRect, visibleArea)) { - continue; // Not in view - } - const showHoverHighlight = hoveredFlamechartNode === stackLayer[j]; - context.fillStyle = showHoverHighlight - ? COLORS.FLAME_CHART_HOVER - : COLORS.FLAME_CHART; - - const drawableRect = rectIntersectionWithRect(nodeRect, visibleArea); - context.fillRect( - drawableRect.origin.x, - drawableRect.origin.y, - drawableRect.size.width, - drawableRect.size.height, - ); + if (trimmedName !== null) { + context.fillStyle = COLORS.PRIORITY_LABEL; - if (width > FLAMECHART_TEXT_PADDING * 2) { - const trimmedName = trimFlamechartText( - context, - name, - width - FLAMECHART_TEXT_PADDING * 2 + (x < 0 ? x : 0), + // Prevent text from being drawn outside `viewableArea` + const textOverflowsViewableArea = !rectEqualToRect( + drawableRect, + nodeRect, ); - - if (trimmedName !== null) { - context.fillStyle = COLORS.PRIORITY_LABEL; - - // Prevent text from being drawn outside `viewableArea` - const textOverflowsViewableArea = !rectEqualToRect( - drawableRect, - nodeRect, - ); - if (textOverflowsViewableArea) { - context.save(); - context.rect( - drawableRect.origin.x, - drawableRect.origin.y, - drawableRect.size.width, - drawableRect.size.height, - ); - context.clip(); - } - - context.fillText( - trimmedName, - x + FLAMECHART_TEXT_PADDING - (x < 0 ? x : 0), - layerY + FLAMECHART_FRAME_HEIGHT / 2, + if (textOverflowsViewableArea) { + context.save(); + context.rect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + drawableRect.size.height, ); + context.clip(); + } - if (textOverflowsViewableArea) { - context.restore(); - } + context.fillText( + trimmedName, + nodeRect.origin.x + FLAMECHART_TEXT_PADDING - (x < 0 ? x : 0), + nodeRect.origin.y + FLAMECHART_FRAME_HEIGHT / 2, + ); + + if (textOverflowsViewableArea) { + context.restore(); } } } @@ -181,39 +184,19 @@ export class FlamechartView extends View { * @private */ handleHover(interaction: HoverInteraction) { - const {flamechart, frame, intrinsicSize, onHover, visibleArea} = this; - if (!onHover) { - return; - } - + const {stackLayer, frame, intrinsicSize, onHover, visibleArea} = this; const {location} = interaction.payload; - if (!rectContainsPoint(location, visibleArea)) { - onHover(null); + if (!onHover || !rectContainsPoint(location, visibleArea)) { return; } - // Identify the layer being hovered over - const adjustedCanvasMouseY = location.y - frame.origin.y; - const layerIndex = Math.floor( - adjustedCanvasMouseY / FLAMECHART_FRAME_HEIGHT, - ); - if (layerIndex < 0 || layerIndex >= flamechart.length) { - onHover(null); - return; - } - const layer = flamechart[layerIndex]; - - if (!layer) { - return null; - } - // Find the node being hovered over. const scaleFactor = positioningScaleFactor(intrinsicSize.width, frame); let startIndex = 0; - let stopIndex = layer.length - 1; + let stopIndex = stackLayer.length - 1; while (startIndex <= stopIndex) { const currentIndex = Math.floor((startIndex + stopIndex) / 2); - const flamechartStackFrame = layer[currentIndex]; + const flamechartStackFrame = stackLayer[currentIndex]; const {timestamp, duration} = flamechartStackFrame; const width = durationToWidth(duration, scaleFactor); @@ -241,3 +224,124 @@ export class FlamechartView extends View { } } } + +export class FlamechartView extends View { + flamechart: Flamechart; + duration: number; + + intrinsicSize: Size; + + flamechartRowViews: FlamechartStackLayerView[] = []; + /** Container view that vertically stacks flamechart rows */ + verticalStackView: StaticLayoutView; + /** View that layers a background color view behind `verticalStackView` */ + layerStackView: StaticLayoutView; + + onHover: ((node: FlamechartStackFrame | null) => void) | null = null; + + constructor( + surface: Surface, + frame: Rect, + flamechart: Flamechart, + duration: number, + ) { + super(surface, frame); + this.flamechart = flamechart; + this.duration = duration; + this.intrinsicSize = { + width: duration, + height: this.flamechart.length * FLAMECHART_FRAME_HEIGHT, + }; + + this.verticalStackView = new StaticLayoutView( + surface, + frame, + verticallyStackedLayout, + [], + ); + + // Use a plain background view to prevent gaps from appearing between + // flamechartRowViews. + const colorView = new ColorView(surface, frame, COLORS.BACKGROUND); + this.layerStackView = new StaticLayoutView(surface, frame, layeredLayout, [ + colorView, + this.verticalStackView, + ]); + this.layerStackView.superview = this; + } + + desiredSize() { + // TODO: Replace this with one calculated by verticalStackView + return this.intrinsicSize; + } + + setHoveredFlamechartStackFrame( + hoveredStackFrame: FlamechartStackFrame | null, + ) { + this.flamechartRowViews.forEach(rowView => + rowView.setHoveredFlamechartStackFrame(hoveredStackFrame), + ); + } + + setOnHover(onHover: (node: FlamechartStackFrame | null) => void) { + this.onHover = onHover; + this.flamechartRowViews.forEach(rowView => (rowView.onHover = onHover)); + } + + setNeedsDisplay() { + super.setNeedsDisplay(); + this.layerStackView.setNeedsDisplay(); + } + + layoutSubviews() { + if (this.flamechartRowViews.length !== this.flamechart.length) { + // TODO: Remove existing row views from verticalStackView + this.flamechartRowViews = this.flamechart.map(stackLayer => { + const rowView = new FlamechartStackLayerView( + this.surface, + this.frame, + stackLayer, + this.duration, + ); + this.verticalStackView.addSubview(rowView); + rowView.onHover = this.onHover; + return rowView; + }); + this.setNeedsDisplay(); + } + + // Lay out subviews + const {layerStackView} = this; + layerStackView.setFrame(this.frame); + layerStackView.setVisibleArea(this.visibleArea); + } + + draw(context: CanvasRenderingContext2D) { + this.layerStackView.displayIfNeeded(context); + } + + /** + * @private + */ + handleHover(interaction: HoverInteraction) { + const {onHover, visibleArea} = this; + if (!onHover) { + return; + } + + const {location} = interaction.payload; + if (!rectContainsPoint(location, visibleArea)) { + // Clear out any hovered flamechart stack frame + onHover(null); + } + } + + handleInteractionAndPropagateToSubviews(interaction: Interaction) { + switch (interaction.type) { + case 'hover': + this.handleHover(interaction); + break; + } + this.layerStackView.handleInteractionAndPropagateToSubviews(interaction); + } +} diff --git a/src/layout/ColorView.js b/src/layout/ColorView.js new file mode 100644 index 0000000..6c86df0 --- /dev/null +++ b/src/layout/ColorView.js @@ -0,0 +1,29 @@ +// @flow + +import type {Rect} from './geometry'; + +import {Surface} from './Surface'; +import {View} from './View'; + +/** + * View that fills its visible area with a CSS color. + */ +export class ColorView extends View { + _color: string; + + constructor(surface: Surface, frame: Rect, color: string) { + super(surface, frame); + this._color = color; + } + + draw(context: CanvasRenderingContext2D) { + const {_color, visibleArea} = this; + context.fillStyle = _color; + context.fillRect( + visibleArea.origin.x, + visibleArea.origin.y, + visibleArea.size.width, + visibleArea.size.height, + ); + } +} diff --git a/src/layout/StaticLayoutView.js b/src/layout/StaticLayoutView.js index 3d00912..0dc19d6 100644 --- a/src/layout/StaticLayoutView.js +++ b/src/layout/StaticLayoutView.js @@ -18,13 +18,17 @@ export const layeredLayout: Layouter = (views, frame) => subview.setFrame(frame); }); +/** + * Stacks `views` vertically in `frame`. All views in `views` will have their + * widths set to the frame's width. + */ export const verticallyStackedLayout: Layouter = (views, frame) => { let currentY = frame.origin.y; views.forEach(view => { const desiredSize = view.desiredSize(); const height = desiredSize ? desiredSize.height - : frame.size.height - currentY; + : frame.origin.y + frame.size.height - currentY; const proposedFrame = { origin: {x: frame.origin.x, y: currentY}, size: {width: frame.size.width, height}, diff --git a/src/layout/VerticalScrollView.js b/src/layout/VerticalScrollView.js index 906e7f3..4a85139 100644 --- a/src/layout/VerticalScrollView.js +++ b/src/layout/VerticalScrollView.js @@ -37,7 +37,6 @@ function clamp(min: number, max: number, value: number): number { export class VerticalScrollView extends View { contentView: View; - intrinsicContentHeight: number; scrollState: VerticalScrollState = { offsetY: 0, @@ -52,14 +51,12 @@ export class VerticalScrollView extends View { surface: Surface, frame: Rect, contentView: View, - intrinsicContentHeight: number, stateDeriver?: (state: VerticalScrollState) => VerticalScrollState, onStateChange?: (state: VerticalScrollState) => void, ) { super(surface, frame); this.contentView = contentView; contentView.superview = this; - this.intrinsicContentHeight = intrinsicContentHeight; if (stateDeriver) this.stateDeriver = stateDeriver; if (onStateChange) this.onStateChange = onStateChange; } @@ -78,6 +75,13 @@ export class VerticalScrollView extends View { layoutSubviews() { const {offsetY} = this.scrollState; + const desiredSize = this.contentView.desiredSize(); + + const remainingHeight = this.frame.size.height; + const desiredHeight = desiredSize ? desiredSize.height : 0; + // Force last view to take up at least all remaining vertical space. + const height = Math.max(desiredHeight, remainingHeight); + const proposedFrame = { origin: { x: this.frame.origin.x, @@ -85,7 +89,7 @@ export class VerticalScrollView extends View { }, size: { width: this.frame.size.width, - height: this.intrinsicContentHeight, + height, }, }; this.contentView.setFrame(proposedFrame); diff --git a/src/layout/index.js b/src/layout/index.js index df9f5d6..0ed792c 100644 --- a/src/layout/index.js +++ b/src/layout/index.js @@ -1,6 +1,7 @@ // @flow export * from './geometry'; +export * from './ColorView'; export * from './HorizontalPanAndZoomView'; export * from './StaticLayoutView'; export * from './Surface';