perfetto-ui: Zoom follows mouse
Bug: 122350836
Change-Id: I3d59509bc97b8fbd10aa7454347b26fd353d998f
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index be8e4a2..4dba4d6 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -16,7 +16,7 @@
--topbar-height: 48px;
--monospace-font: 'Roboto Mono', monospace;
- // Keep in sync with TRACK_SHELL_WIDTH in track_panel.ts .
+ // Keep in sync with TRACK_SHELL_WIDTH in track_panel.ts
--track-shell-width: 250px;
}
diff --git a/ui/src/frontend/time_scale.ts b/ui/src/frontend/time_scale.ts
index 0e22955..7444afb 100644
--- a/ui/src/frontend/time_scale.ts
+++ b/ui/src/frontend/time_scale.ts
@@ -14,26 +14,28 @@
import {TimeSpan} from '../common/time';
+const MAX_ZOOM_SPAN_SEC = 1e-4; // 0.1 ms.
+
/**
- * Defines a mapping between number and Milliseconds for the entire application.
+ * Defines a mapping between number and seconds for the entire application.
* Linearly scales time values from boundsMs to pixel values in boundsPx and
* back.
*/
export class TimeScale {
private timeBounds: TimeSpan;
- private startPx: number;
- private endPx: number;
+ private _startPx: number;
+ private _endPx: number;
private secPerPx = 0;
constructor(timeBounds: TimeSpan, boundsPx: [number, number]) {
this.timeBounds = timeBounds;
- this.startPx = boundsPx[0];
- this.endPx = boundsPx[1];
+ this._startPx = boundsPx[0];
+ this._endPx = boundsPx[1];
this.updateSlope();
}
private updateSlope() {
- this.secPerPx = this.timeBounds.duration / (this.endPx - this.startPx);
+ this.secPerPx = this.timeBounds.duration / (this._endPx - this._startPx);
}
deltaTimeToPx(time: number): number {
@@ -41,11 +43,11 @@
}
timeToPx(time: number): number {
- return this.startPx + (time - this.timeBounds.start) / this.secPerPx;
+ return this._startPx + (time - this.timeBounds.start) / this.secPerPx;
}
pxToTime(px: number): number {
- return this.timeBounds.start + (px - this.startPx) * this.secPerPx;
+ return this.timeBounds.start + (px - this._startPx) * this.secPerPx;
}
deltaPxToDuration(px: number): number {
@@ -58,8 +60,32 @@
}
setLimitsPx(pxStart: number, pxEnd: number) {
- this.startPx = pxStart;
- this.endPx = pxEnd;
+ this._startPx = pxStart;
+ this._endPx = pxEnd;
this.updateSlope();
}
+
+ get startPx(): number {
+ return this._startPx;
+ }
+
+ get endPx(): number {
+ return this._endPx;
+ }
+}
+
+export function computeZoom(
+ scale: TimeScale, span: TimeSpan, zoomFactor: number, zoomPx: number):
+ TimeSpan {
+ const startPx = scale.startPx;
+ const endPx = scale.endPx;
+ const deltaPx = endPx - startPx;
+ const deltaTime = span.end - span.start;
+ const newDeltaTime = Math.max(deltaTime * zoomFactor, MAX_ZOOM_SPAN_SEC);
+ const clampedZoomPx = Math.max(startPx, Math.min(endPx, zoomPx));
+ const zoomTime = scale.pxToTime(clampedZoomPx);
+ const r = (clampedZoomPx - startPx) / deltaPx;
+ const newStartTime = zoomTime - newDeltaTime * r;
+ const newEndTime = newStartTime + newDeltaTime;
+ return new TimeSpan(newStartTime, newEndTime);
}
diff --git a/ui/src/frontend/time_scale_unittest.ts b/ui/src/frontend/time_scale_unittest.ts
index e8bbdef..ed9fc41 100644
--- a/ui/src/frontend/time_scale_unittest.ts
+++ b/ui/src/frontend/time_scale_unittest.ts
@@ -14,7 +14,7 @@
import {TimeSpan} from '../common/time';
-import {TimeScale} from './time_scale';
+import {computeZoom, TimeScale} from './time_scale';
test('time scale to work', () => {
const scale = new TimeScale(new TimeSpan(0, 100), [200, 1000]);
@@ -44,4 +44,28 @@
expect(scale.timeToPx(0)).toEqual(200);
expect(scale.timeToPx(100)).toEqual(600);
expect(scale.timeToPx(200)).toEqual(1000);
-});
\ No newline at end of file
+});
+
+test('it zooms', () => {
+ const span = new TimeSpan(0, 20);
+ const scale = new TimeScale(span, [0, 100]);
+ const newSpan = computeZoom(scale, span, 0.5, 50);
+ expect(newSpan.start).toEqual(5);
+ expect(newSpan.end).toEqual(15);
+});
+
+test('it zooms an offset scale and span', () => {
+ const span = new TimeSpan(1000, 1020);
+ const scale = new TimeScale(span, [200, 300]);
+ const newSpan = computeZoom(scale, span, 0.5, 250);
+ expect(newSpan.start).toEqual(1005);
+ expect(newSpan.end).toEqual(1015);
+});
+
+test('it clamps zoom in', () => {
+ const span = new TimeSpan(1000, 1040);
+ const scale = new TimeScale(span, [200, 300]);
+ const newSpan = computeZoom(scale, span, 0.0000000001, 225);
+ expect((newSpan.end - newSpan.start) / 2 + newSpan.start).toBeCloseTo(1010);
+ expect(newSpan.end - newSpan.start).toBeCloseTo(1e-4, 8);
+});
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index ba2a98d..085feab 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -27,11 +27,11 @@
import {Panel} from './panel';
import {AnyAttrsVnode, PanelContainer} from './panel_container';
import {TimeAxisPanel} from './time_axis_panel';
+import {computeZoom} from './time_scale';
import {TrackGroupPanel} from './track_group_panel';
import {TRACK_SHELL_WIDTH} from './track_panel';
import {TrackPanel} from './track_panel';
-const MAX_ZOOM_SPAN_SEC = 1e-4; // 0.1 ms.
const DRAG_HANDLE_HEIGHT_PX = 12;
class QueryTable extends Panel {
@@ -189,16 +189,15 @@
frontendLocalState.updateVisibleTime(new TimeSpan(tStart, tEnd));
globals.rafScheduler.scheduleRedraw();
},
- onZoomed: (_: number, zoomRatio: number) => {
- const vizTime = frontendLocalState.visibleWindowTime;
- const curSpanSec = vizTime.duration;
- const newSpanSec =
- Math.max(curSpanSec - curSpanSec * zoomRatio, MAX_ZOOM_SPAN_SEC);
- const deltaSec = (curSpanSec - newSpanSec) / 2;
- const newStartSec = vizTime.start + deltaSec;
- const newEndSec = vizTime.end - deltaSec;
- frontendLocalState.updateVisibleTime(
- new TimeSpan(newStartSec, newEndSec));
+ onZoomed: (zoomedPositionPx: number, zoomRatio: number) => {
+ // TODO(hjd): Avoid hardcoding TRACK_SHELL_WIDTH.
+ // TODO(hjd): Improve support for zooming in overview timeline.
+ const span = frontendLocalState.visibleWindowTime;
+ const scale = frontendLocalState.timeScale;
+ const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH;
+ const newSpan = computeZoom(scale, span, 1 - zoomRatio, zoomPx);
+ frontendLocalState.updateVisibleTime(newSpan);
+ globals.rafScheduler.scheduleRedraw();
}
});
}