Hover CPU slices

Tracks get optional onMouseMove and onMouseOut methods which are called
with track-specific event coordinates from the TrackComponent.

The CpuSliceTrack uses these coordinates to find and highlight the
currently hovered slice.

Screenshot: https://screenshot.googleplex.com/yzNOmJqpnBp
Change-Id: Ibf67646607e3a1a5b8744d0e3e2738d8c0f68031
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index 14a42fe..aaad16b 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -45,4 +45,6 @@
   abstract renderCanvas(
       ctx: CanvasRenderingContext2D, timeScale: TimeScale,
       visibleWindowMs: {start: number, end: number}): void;
+  onMouseMove(_position: {x: number, y: number}) {}
+  onMouseOut() {}
 }
diff --git a/ui/src/frontend/track_component.ts b/ui/src/frontend/track_component.ts
index fad2a9b..ef9e90f 100644
--- a/ui/src/frontend/track_component.ts
+++ b/ui/src/frontend/track_component.ts
@@ -163,6 +163,13 @@
             }))),
         m('.track-content',
           {
+            onmousemove: (e: MouseEvent) => {
+              // TODO(hjd): Trigger a repaint here not a full m.redraw.
+              this.track.onMouseMove({x: e.layerX, y: e.layerY});
+            },
+            onmouseout: () => {
+              this.track.onMouseOut();
+            },
             style: {
               width: `calc(100% - ${TRACK_SHELL_WIDTH}px)`,
               left: `${TRACK_SHELL_WIDTH}px`,
diff --git a/ui/src/tracks/cpu_slices/common.ts b/ui/src/tracks/cpu_slices/common.ts
index d2b0a4b..c67b3c5 100644
--- a/ui/src/tracks/cpu_slices/common.ts
+++ b/ui/src/tracks/cpu_slices/common.ts
@@ -14,7 +14,7 @@
 
 export const TRACK_KIND = 'CpuSliceTrack';
 
-interface CpuSlice {
+export interface CpuSlice {
   start: number;
   end: number;
   title: string;
diff --git a/ui/src/tracks/cpu_slices/frontend.ts b/ui/src/tracks/cpu_slices/frontend.ts
index 1671a1e..97c4d22 100644
--- a/ui/src/tracks/cpu_slices/frontend.ts
+++ b/ui/src/tracks/cpu_slices/frontend.ts
@@ -16,7 +16,7 @@
 import {TimeScale} from '../../frontend/time_scale';
 import {Track} from '../../frontend/track';
 import {trackRegistry} from '../../frontend/track_registry';
-import {CpuSliceTrackData, TRACK_KIND} from './common';
+import {CpuSlice, CpuSliceTrackData, TRACK_KIND} from './common';
 
 function sliceIsVisible(
     slice: {start: number, end: number},
@@ -31,6 +31,8 @@
   }
 
   private trackData: CpuSliceTrackData|undefined;
+  private timeScale: TimeScale|undefined;
+  private hoveredSlice: CpuSlice|null = null;
 
   constructor(trackState: TrackState) {
     super(trackState);
@@ -48,9 +50,30 @@
       if (!sliceIsVisible(slice, visibleWindowMs)) continue;
       const rectStart = timeScale.msToPx(slice.start);
       const rectEnd = timeScale.msToPx(slice.end);
-      ctx.fillStyle = '#4682b4';
+      ctx.fillStyle = slice === this.hoveredSlice ? '#b35846' : '#4682b4';
       ctx.fillRect(rectStart, 40, rectEnd - rectStart, 30);
     }
+    this.timeScale = timeScale;
+  }
+
+  onMouseMove({x, y}: {x: number, y: number}) {
+    if (!this.trackData || !this.timeScale) return;
+    if (y < 40 || y > 70) {
+      this.hoveredSlice = null;
+      return;
+    }
+    const xMs = this.timeScale.pxToMs(x);
+    this.hoveredSlice = null;
+
+    for (const slice of this.trackData.slices) {
+      if (slice.start <= xMs && slice.end >= xMs) {
+        this.hoveredSlice = slice;
+      }
+    }
+  }
+
+  onMouseOut() {
+    this.hoveredSlice = null;
   }
 }