import type {Snapshot, TimelineData} from '../types';
import type {
Interaction,
Point,
Rect,
Size,
Surface,
ViewRefs,
} from '../view-base';
import {positioningScaleFactor, timestampToPosition} from './utils/positioning';
import {
intersectionOfRects,
rectContainsPoint,
rectEqualToRect,
View,
} from '../view-base';
import {BORDER_SIZE, COLORS, SNAPSHOT_SCRUBBER_SIZE} from './constants';
type OnHover = (node: Snapshot | null) => void;
export class SnapshotsView extends View {
_hoverLocation: Point | null = null;
_intrinsicSize: Size;
_profilerData: TimelineData;
onHover: OnHover | null = null;
constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
super(surface, frame);
this._intrinsicSize = {
width: profilerData.duration,
height: profilerData.snapshotHeight,
};
this._profilerData = profilerData;
}
desiredSize(): Size {
return this._intrinsicSize;
}
draw(context: CanvasRenderingContext2D) {
const snapshotHeight = this._profilerData.snapshotHeight;
const {visibleArea} = this;
context.fillStyle = COLORS.BACKGROUND;
context.fillRect(
visibleArea.origin.x,
visibleArea.origin.y,
visibleArea.size.width,
visibleArea.size.height,
);
const y = visibleArea.origin.y;
let x = visibleArea.origin.x;
while (x < visibleArea.origin.x + visibleArea.size.width) {
const snapshot = this._findClosestSnapshot(x);
if (snapshot === null) {
break;
}
const scaledHeight = snapshotHeight;
const scaledWidth = (snapshot.width * snapshotHeight) / snapshot.height;
const imageRect: Rect = {
origin: {
x,
y,
},
size: {width: scaledWidth, height: scaledHeight},
};
if (snapshot.image === null) {
const img = (snapshot.image = new Image());
img.onload = () => {
this._drawSnapshotImage(context, snapshot, imageRect);
};
img.src = snapshot.imageSource;
} else {
this._drawSnapshotImage(context, snapshot, imageRect);
}
x += scaledWidth + BORDER_SIZE;
}
const hoverLocation = this._hoverLocation;
if (hoverLocation !== null) {
const scrubberWidth = SNAPSHOT_SCRUBBER_SIZE + BORDER_SIZE * 2;
const scrubberOffset = scrubberWidth / 2;
context.fillStyle = COLORS.SCRUBBER_BORDER;
context.fillRect(
hoverLocation.x - scrubberOffset,
visibleArea.origin.y,
scrubberWidth,
visibleArea.size.height,
);
context.fillStyle = COLORS.SCRUBBER_BACKGROUND;
context.fillRect(
hoverLocation.x - scrubberOffset + BORDER_SIZE,
visibleArea.origin.y,
SNAPSHOT_SCRUBBER_SIZE,
visibleArea.size.height,
);
}
}
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
switch (interaction.type) {
case 'mousemove':
case 'wheel-control':
case 'wheel-meta':
case 'wheel-plain':
case 'wheel-shift':
this._updateHover(interaction.payload.location, viewRefs);
break;
}
}
_drawSnapshotImage(
context: CanvasRenderingContext2D,
snapshot: Snapshot,
imageRect: Rect,
) {
const visibleArea = this.visibleArea;
const shouldClip = !rectEqualToRect(imageRect, visibleArea);
if (shouldClip) {
const clippedRect = intersectionOfRects(imageRect, visibleArea);
context.save();
context.beginPath();
context.rect(
clippedRect.origin.x,
clippedRect.origin.y,
clippedRect.size.width,
clippedRect.size.height,
);
context.closePath();
context.clip();
}
context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER;
context.fillRect(
imageRect.origin.x,
imageRect.origin.y,
imageRect.size.width,
imageRect.size.height,
);
context.drawImage(
snapshot.image,
0,
0,
snapshot.width,
snapshot.height,
imageRect.origin.x + BORDER_SIZE,
imageRect.origin.y + BORDER_SIZE,
imageRect.size.width - BORDER_SIZE * 2,
imageRect.size.height - BORDER_SIZE * 2,
);
if (shouldClip) {
context.restore();
}
}
_findClosestSnapshot(x: number): Snapshot | null {
const frame = this.frame;
const scaleFactor = positioningScaleFactor(
this._intrinsicSize.width,
frame,
);
const snapshots = this._profilerData.snapshots;
let startIndex = 0;
let stopIndex = snapshots.length - 1;
while (startIndex <= stopIndex) {
const currentIndex = Math.floor((startIndex + stopIndex) / 2);
const snapshot = snapshots[currentIndex];
const {timestamp} = snapshot;
const snapshotX = Math.floor(
timestampToPosition(timestamp, scaleFactor, frame),
);
if (x < snapshotX) {
stopIndex = currentIndex - 1;
} else {
startIndex = currentIndex + 1;
}
}
return snapshots[stopIndex] || null;
}
_updateHover(location: Point, viewRefs: ViewRefs) {
const {onHover, visibleArea} = this;
if (!onHover) {
return;
}
if (!rectContainsPoint(location, visibleArea)) {
if (this._hoverLocation !== null) {
this._hoverLocation = null;
this.setNeedsDisplay();
}
onHover(null);
return;
}
const snapshot = this._findClosestSnapshot(location.x);
if (snapshot !== null) {
this._hoverLocation = location;
onHover(snapshot);
} else {
this._hoverLocation = null;
onHover(null);
}
this.setNeedsDisplay();
}
}