blob: cb6e1cd75e09bf3a8ddc4e6f2c8bc742021494d0 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TimeRange} from 'app/timeline_data';
import {TRACE_INFO} from 'app/trace_info';
import {Timestamp, TimestampType} from 'trace/timestamp';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceType} from 'trace/trace_type';
import {Color} from '../../colors';
import {CanvasDrawer} from '../canvas/canvas_drawer';
import {CanvasMouseHandler} from '../canvas/canvas_mouse_handler';
import {DraggableCanvasObject} from '../canvas/draggable_canvas_object';
import {Segment} from './utils';
export class MiniCanvasDrawerInput {
constructor(
public fullRange: TimeRange,
public selectedPosition: Timestamp,
public selection: TimeRange,
public zoomRange: TimeRange,
public traces: Traces
) {}
transform(mapToRange: Segment): MiniCanvasDrawerData {
const transformer = new Transformer(this.zoomRange, mapToRange);
return new MiniCanvasDrawerData(
transformer.transform(this.selectedPosition),
{
from: transformer.transform(this.selection.from),
to: transformer.transform(this.selection.to),
},
this.transformTracesTimestamps(transformer),
transformer
);
}
private transformTracesTimestamps(transformer: Transformer): Map<TraceType, number[]> {
const transformedTraceSegments = new Map<TraceType, number[]>();
this.traces.forEachTrace((trace) => {
transformedTraceSegments.set(trace.type, this.transformTraceTimestamps(transformer, trace));
});
return transformedTraceSegments;
}
private transformTraceTimestamps(transformer: Transformer, trace: Trace<{}>): number[] {
const result: number[] = [];
trace.forEachTimestamp((timestamp) => {
result.push(transformer.transform(timestamp));
});
return result;
}
}
export class Transformer {
private timestampType: TimestampType;
private fromWidth: bigint;
private targetWidth: number;
private fromOffset: bigint;
private toOffset: number;
constructor(private fromRange: TimeRange, private toRange: Segment) {
this.timestampType = fromRange.from.getType();
this.fromWidth = this.fromRange.to.getValueNs() - this.fromRange.from.getValueNs();
// Needs to be a whole number to be compatible with bigints
this.targetWidth = Math.round(this.toRange.to - this.toRange.from);
this.fromOffset = this.fromRange.from.getValueNs();
// Needs to be a whole number to be compatible with bigints
this.toOffset = this.toRange.from;
}
transform(x: Timestamp): number {
return (
this.toOffset +
(this.targetWidth * Number(x.getValueNs() - this.fromOffset)) / Number(this.fromWidth)
);
}
untransform(x: number): Timestamp {
x = Math.round(x);
const valueNs =
this.fromOffset + (BigInt(x - this.toOffset) * this.fromWidth) / BigInt(this.targetWidth);
return new Timestamp(this.timestampType, valueNs);
}
}
class MiniCanvasDrawerOutput {
constructor(public selectedPosition: Timestamp, public selection: TimeRange) {}
}
class MiniCanvasDrawerData {
constructor(
public selectedPosition: number,
public selection: Segment,
public timelineEntries: Map<TraceType, number[]>,
public transformer: Transformer
) {}
toOutput(): MiniCanvasDrawerOutput {
return new MiniCanvasDrawerOutput(this.transformer.untransform(this.selectedPosition), {
from: this.transformer.untransform(this.selection.from),
to: this.transformer.untransform(this.selection.to),
});
}
}
export class MiniCanvasDrawer implements CanvasDrawer {
ctx: CanvasRenderingContext2D;
handler: CanvasMouseHandler;
private activePointer: DraggableCanvasObject;
private leftFocusSectionSelector: DraggableCanvasObject;
private rightFocusSectionSelector: DraggableCanvasObject;
private get pointerWidth() {
return this.getHeight() / 6;
}
getXScale() {
return this.ctx.getTransform().m11;
}
getYScale() {
return this.ctx.getTransform().m22;
}
getWidth() {
return this.canvas.width / this.getXScale();
}
getHeight() {
return this.canvas.height / this.getYScale();
}
get usableRange() {
return {
from: this.padding.left,
to: this.getWidth() - this.padding.left - this.padding.right,
};
}
get input(): MiniCanvasDrawerData {
return this.inputGetter().transform(this.usableRange);
}
constructor(
public canvas: HTMLCanvasElement,
private inputGetter: () => MiniCanvasDrawerInput,
private onPointerPositionDragging: (pos: Timestamp) => void,
private onPointerPositionChanged: (pos: Timestamp) => void,
private onSelectionChanged: (selection: TimeRange) => void,
private onZoomChanged: (zoom: TimeRange) => void,
private onUnhandledClick: (pos: Timestamp) => void
) {
const ctx = canvas.getContext('2d');
if (ctx === null) {
throw Error('MiniTimeline canvas context was null!');
}
this.ctx = ctx;
const onUnhandledClickInternal = (x: number, y: number) => {
this.onUnhandledClick(this.input.transformer.untransform(x));
};
this.handler = new CanvasMouseHandler(this, 'pointer', onUnhandledClickInternal);
this.activePointer = new DraggableCanvasObject(
this,
() => this.selectedPosition,
(ctx: CanvasRenderingContext2D, position: number) => {
const barWidth = 3;
const triangleHeight = this.pointerWidth / 2;
ctx.beginPath();
ctx.moveTo(position - triangleHeight, 0);
ctx.lineTo(position + triangleHeight, 0);
ctx.lineTo(position + barWidth / 2, triangleHeight);
ctx.lineTo(position + barWidth / 2, this.getHeight());
ctx.lineTo(position - barWidth / 2, this.getHeight());
ctx.lineTo(position - barWidth / 2, triangleHeight);
ctx.closePath();
},
{
fillStyle: Color.ACTIVE_POINTER,
fill: true,
},
(x) => {
this.input.selectedPosition = x;
this.onPointerPositionDragging(this.input.transformer.untransform(x));
},
(x) => {
this.input.selectedPosition = x;
this.onPointerPositionChanged(this.input.transformer.untransform(x));
},
() => this.usableRange
);
const focusSelectorDrawConfig = {
fillStyle: Color.SELECTOR_COLOR,
fill: true,
};
const onLeftSelectionChanged = (x: number) => {
this.selection.from = x;
this.onSelectionChanged({
from: this.input.transformer.untransform(x),
to: this.input.transformer.untransform(this.selection.to),
});
};
const onRightSelectionChanged = (x: number) => {
this.selection.to = x;
this.onSelectionChanged({
from: this.input.transformer.untransform(this.selection.from),
to: this.input.transformer.untransform(x),
});
};
const onLeftZoomChanged = (x: number) => {
this.onZoomChanged({
from: this.input.transformer.untransform(x),
to: this.inputGetter().zoomRange.to,
});
}
const onRightZoomChanged = (x: number) => {
this.onZoomChanged({
from: this.inputGetter().zoomRange.from,
to: this.input.transformer.untransform(x),
});
}
const barWidth = 6;
const selectorArrowWidth = this.innerHeight / 12;
const selectorArrowHeight = selectorArrowWidth * 2;
this.leftFocusSectionSelector = new DraggableCanvasObject(
this,
() => this.selection.from,
(ctx: CanvasRenderingContext2D, position: number) => {
ctx.beginPath();
ctx.moveTo(position - barWidth, this.padding.top);
ctx.lineTo(position, this.padding.top);
ctx.lineTo(position + selectorArrowWidth, this.padding.top + selectorArrowWidth);
ctx.lineTo(position, this.padding.top + selectorArrowHeight);
ctx.lineTo(position, this.padding.top + this.innerHeight);
ctx.lineTo(position - barWidth, this.padding.top + this.innerHeight);
ctx.lineTo(position - barWidth, this.padding.top);
ctx.closePath();
},
focusSelectorDrawConfig,
onLeftSelectionChanged,
(x: number) => {
onLeftSelectionChanged(x);
onLeftZoomChanged(x);
},
() => {
return {
from: this.usableRange.from,
to: this.rightFocusSectionSelector.position - selectorArrowWidth - barWidth,
};
}
);
this.rightFocusSectionSelector = new DraggableCanvasObject(
this,
() => this.selection.to,
(ctx: CanvasRenderingContext2D, position: number) => {
ctx.beginPath();
ctx.moveTo(position + barWidth, this.padding.top);
ctx.lineTo(position, this.padding.top);
ctx.lineTo(position - selectorArrowWidth, this.padding.top + selectorArrowWidth);
ctx.lineTo(position, this.padding.top + selectorArrowHeight);
ctx.lineTo(position, this.padding.top + this.innerHeight);
ctx.lineTo(position + barWidth, this.padding.top + this.innerHeight);
ctx.closePath();
},
focusSelectorDrawConfig,
onRightSelectionChanged,
(x: number) => {
onRightSelectionChanged(x);
onRightZoomChanged(x);
},
() => {
return {
from: this.leftFocusSectionSelector.position + selectorArrowWidth + barWidth,
to: this.usableRange.to,
};
}
);
}
get selectedPosition() {
return this.input.selectedPosition;
}
get selection() {
return this.input.selection;
}
get timelineEntries() {
return this.input.timelineEntries;
}
get padding() {
return {
top: Math.ceil(this.getHeight() / 5),
bottom: Math.ceil(this.getHeight() / 5),
left: Math.ceil(this.pointerWidth / 2),
right: Math.ceil(this.pointerWidth / 2),
};
}
get innerHeight() {
return this.getHeight() - this.padding.top - this.padding.bottom;
}
draw() {
this.ctx.clearRect(0, 0, this.getWidth(), this.getHeight());
this.drawSelectionBackground();
this.drawTraceLines();
this.drawTimelineGuides();
this.leftFocusSectionSelector.draw(this.ctx);
this.rightFocusSectionSelector.draw(this.ctx);
this.activePointer.draw(this.ctx);
}
private drawSelectionBackground() {
const triangleHeight = this.innerHeight / 6;
// Selection background
this.ctx.globalAlpha = 0.8;
this.ctx.fillStyle = Color.SELECTION_BACKGROUND;
const width = this.selection.to - this.selection.from;
this.ctx.fillRect(
this.selection.from,
this.padding.top + triangleHeight / 2,
width,
this.innerHeight - triangleHeight / 2
);
this.ctx.restore();
}
private drawTraceLines() {
const lineHeight = this.innerHeight / 8;
let fromTop = this.padding.top + (this.innerHeight * 2) / 3 - lineHeight;
this.timelineEntries.forEach((entries, traceType) => {
// TODO: Only if active or a selected trace
for (const entry of entries) {
this.ctx.globalAlpha = 0.7;
this.ctx.fillStyle = TRACE_INFO[traceType].color;
const width = 5;
this.ctx.fillRect(entry - width / 2, fromTop, width, lineHeight);
this.ctx.globalAlpha = 1.0;
}
fromTop -= (lineHeight * 4) / 3;
});
}
private drawTimelineGuides() {
const edgeBarHeight = (this.innerHeight * 1) / 2;
const edgeBarWidth = 4;
const boldBarHeight = (this.innerHeight * 1) / 5;
const boldBarWidth = edgeBarWidth;
const lightBarHeight = (this.innerHeight * 1) / 6;
const lightBarWidth = 2;
const minSpacing = lightBarWidth * 7;
const barsInSetWidth = 9 * lightBarWidth + boldBarWidth;
const barSets = Math.floor(
(this.getWidth() - edgeBarWidth * 2 - minSpacing) / (barsInSetWidth + 10 * minSpacing)
);
const bars = barSets * 10;
// Draw start bar
this.ctx.fillStyle = Color.GUIDE_BAR;
this.ctx.fillRect(
0,
this.padding.top + this.innerHeight - edgeBarHeight,
edgeBarWidth,
edgeBarHeight
);
// Draw end bar
this.ctx.fillStyle = Color.GUIDE_BAR;
this.ctx.fillRect(
this.getWidth() - edgeBarWidth,
this.padding.top + this.innerHeight - edgeBarHeight,
edgeBarWidth,
edgeBarHeight
);
const spacing = (this.getWidth() - barSets * barsInSetWidth - edgeBarWidth) / bars;
let start = edgeBarWidth + spacing;
for (let i = 1; i < bars; i++) {
if (i % 10 === 0) {
// Draw boldbar
this.ctx.fillStyle = Color.GUIDE_BAR;
this.ctx.fillRect(
start,
this.padding.top + this.innerHeight - boldBarHeight,
boldBarWidth,
boldBarHeight
);
start += boldBarWidth; // TODO: Shift a bit
} else {
// Draw lightbar
this.ctx.fillStyle = Color.GUIDE_BAR_LIGHT;
this.ctx.fillRect(
start,
this.padding.top + this.innerHeight - lightBarHeight,
lightBarWidth,
lightBarHeight
);
start += lightBarWidth;
}
start += spacing;
}
}
}