blob: c1dfcaca3c49cb2282d4c884de62ed75ea886610 [file] [log] [blame]
// Copyright (C) 2018 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 * as m from 'mithril';
import {Animation} from './animation';
import {DragGestureHandler} from './drag_gesture_handler';
import {TimeAxis} from './time_axis';
import {TimeScale} from './time_scale';
/**
* Overview timeline with a brush for time-based selections.
*/
export const OverviewTimeline = {
oninit() {
this.timeScale = new TimeScale([0, 1], [0, 0]);
this.padding = {top: 0, right: 20, bottom: 0, left: 20};
},
oncreate(vnode) {
const rect = vnode.dom.getBoundingClientRect();
this.timeScale.setLimitsPx(
this.padding.left, rect.width - this.padding.left - this.padding.right);
},
onupdate(vnode) {
const rect = vnode.dom.getBoundingClientRect();
this.timeScale.setLimitsPx(
this.padding.left, rect.width - this.padding.left - this.padding.right);
},
view({attrs}) {
this.timeScale.setLimitsMs(
attrs.maxVisibleWindowMs.start, attrs.maxVisibleWindowMs.end);
return m(
'.overview-timeline',
m(TimeAxis, {
timeScale: this.timeScale,
contentOffset: 0,
visibleWindowMs: attrs.maxVisibleWindowMs,
}),
m('.visualization', {
style: {
width: '100%',
height: '100%',
}
}),
m('.brushes',
{
style: {
position: 'absolute',
left: `${this.padding.left}px`,
top: '41px',
width: 'calc(100% - 40px)',
height: 'calc(100% - 41px)',
}
},
m(HorizontalBrushSelection, {
onBrushedPx: (startPx: number, endPx: number) => {
attrs.onBrushedMs(
this.timeScale.pxToMs(startPx), this.timeScale.pxToMs(endPx));
},
selectionPx: {
start: this.timeScale.msToPx(attrs.visibleWindowMs.start),
end: this.timeScale.msToPx(attrs.visibleWindowMs.end)
},
})));
},
} as
m.Component<
{
visibleWindowMs: {start: number, end: number},
maxVisibleWindowMs: {start: number, end: number},
onBrushedMs: (start: number, end: number) => void,
},
{
timeScale: TimeScale,
padding: {top: number, right: number, bottom: number, left: number},
}>;
const ZOOM_IN_PERCENTAGE_PER_MS = 0.998;
const ZOOM_OUT_PERCENTAGE_PER_MS = 1 / ZOOM_IN_PERCENTAGE_PER_MS;
const WHEEL_ZOOM_DURATION = 200;
/**
* Interactive horizontal brush for pixel-based selections.
*/
const HorizontalBrushSelection = {
oncreate(vnode) {
const el = vnode.dom as HTMLElement;
this.offsetLeft = (el.getBoundingClientRect() as DOMRect).x;
const startHandle =
el.getElementsByClassName('brush-handle-start')[0] as HTMLElement;
const endHandle =
el.getElementsByClassName('brush-handle-end')[0] as HTMLElement;
let dragState: 'draggingStartHandle'|'draggingEndHandle'|'notDragging' =
'notDragging';
const dragged = (posX: number) => {
if ((dragState === 'draggingEndHandle' &&
posX < this.selectionPx.start) ||
(dragState === 'draggingStartHandle' &&
posX > this.selectionPx.end)) {
// Flip start and end if handle has been dragged past the other limit.
dragState = dragState === 'draggingStartHandle' ? 'draggingEndHandle' :
'draggingStartHandle';
}
if (dragState === 'draggingStartHandle') {
this.onBrushedPx(posX, this.selectionPx.end);
} else {
this.onBrushedPx(this.selectionPx.start, posX);
}
};
new DragGestureHandler(
startHandle,
x => dragged(x - this.offsetLeft),
() => dragState = 'draggingStartHandle',
() => dragState = 'notDragging');
new DragGestureHandler(
endHandle,
x => dragged(x - this.offsetLeft),
() => dragState = 'draggingEndHandle',
() => dragState = 'notDragging');
new DragGestureHandler(el, x => dragged(x - this.offsetLeft), x => {
this.selectionPx.start = this.selectionPx.end = x - this.offsetLeft;
dragState = 'draggingEndHandle';
}, () => dragState = 'notDragging');
this.onMouseMove = e => {
this.mousePositionX = e.clientX - this.offsetLeft;
};
let zoomingIn = true;
const zoomAnimation = new Animation((timeSinceLastMs: number) => {
const percentagePerMs =
zoomingIn ? ZOOM_IN_PERCENTAGE_PER_MS : ZOOM_OUT_PERCENTAGE_PER_MS;
const percentage = Math.pow(percentagePerMs, timeSinceLastMs);
const selectionLength = this.selectionPx.end - this.selectionPx.start;
const newSelectionLength = selectionLength * percentage;
// Brush toward the mouse, like zooming.
const zoomPositionPercentage =
(this.mousePositionX - this.selectionPx.start) / selectionLength;
const brushStart =
this.mousePositionX - zoomPositionPercentage * newSelectionLength;
const brushEnd = this.mousePositionX +
(1 - zoomPositionPercentage) * newSelectionLength;
this.onBrushedPx(brushStart, brushEnd);
});
this.onWheel = e => {
if (e.deltaY) {
zoomingIn = e.deltaY < 0;
zoomAnimation.start(WHEEL_ZOOM_DURATION);
}
};
},
onupdate(vnode) {
const el = vnode.dom as HTMLElement;
this.offsetLeft = (el.getBoundingClientRect() as DOMRect).x;
},
view({attrs}) {
this.onBrushedPx = attrs.onBrushedPx;
this.selectionPx = attrs.selectionPx;
return m(
'.brushes',
{
onwheel: this.onWheel,
onmousemove: this.onMouseMove,
style: {
width: '100%',
height: '100%',
}
},
m('.brush-left.brush-rect', {
style: {
'border-right': '1px solid #aaa',
left: '0',
width: `${attrs.selectionPx.start}px`,
}
}),
m('.brush-right.brush-rect', {
style: {
'border-left': '1px solid #aaa',
left: `${attrs.selectionPx.end}px`,
width: `calc(100% - ${attrs.selectionPx.end}px)`,
}
}),
m(BrushHandle, {
left: attrs.selectionPx.start,
className: 'brush-handle-start',
}),
m(BrushHandle, {
left: attrs.selectionPx.end,
className: 'brush-handle-end',
}));
}
} as m.Component<{
onBrushedPx: (start: number, end: number) => void,
selectionPx: {start: number, end: number},
},
{
selectionPx: {start: number, end: number},
onBrushedPx: (start: number, end: number) =>
void,
offsetLeft: number,
onWheel: (e: WheelEvent) => void,
onMouseMove: (e: MouseEvent) => void,
mousePositionX: number,
}>;
/**
* Creates a visual handle with three horizontal bars.
*/
const BrushHandle = {
view({attrs}) {
const handleBar = m('.handle-bar', {
style: {
height: '5px',
width: '8px',
'margin-left': '2px',
'border-top': '1px solid #888',
}
});
return m(
`.brush-handle.${attrs.className}`,
{
style: {
left: `${attrs.left - 6}px`,
}
},
m('.handle-bars',
{
style: {
position: 'relative',
top: '9px',
}
},
handleBar,
handleBar,
handleBar));
}
} as m.Component<{
left: number,
className: string,
},
{}>;