Refactor Scrolling and Tracks using Panels

In this CL:
* We introduce the interface Panel, which only two methods: renderCanvas
  and updateDom.
* Tracks are internally reimplemented as Panels. Track extension API
  remains unchanged (except they now get timeScale and
  visibleWindowMs through a global.)
* Introduce LightRedrawer, which schedules rafs for high frequency
  drawing. All canvas drawing now happens through LightRedrawer,
  although m.redraw still remains in most places until we change
  overview timeline to work with LightRedrawer.
* Lift out state used by canvas redrawing to global FrontendLocalState
  object, since attributes do not propagate through the mithril tree
  if m.redraw is not called.
* Scrolling architecture is simplified. CanvasController, Scrollable
  Container, and CanvasWrapper are all gone, replaced with the single
  ScrollingPanelContainer.

What's not in this CL:
* We don't have a non-scrolling panel container. We can factor out the
  common code between scrolling and non-scrolling container and do that
  in a seperate CL.
* We don't have any additional panels other than tracks.

Known issues:
* The trace doesn't draw after loading until you pan/zoom for the first
  time. We will need to initialize the time scale and visibleWindowMs
  properly after a trace load.


Change-Id: I924be19e10e160f710dae920bfa1927e66ec8be9
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
new file mode 100644
index 0000000..6339cd8
--- /dev/null
+++ b/ui/src/frontend/track_panel.ts
@@ -0,0 +1,140 @@
+// 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 {moveTrack} from '../common/actions';
+import {TrackState} from '../common/state';
+
+import {globals} from './globals';
+import {drawGridLines} from './gridline_helper';
+import {quietDispatch} from './mithril_helpers';
+import {Panel} from './panel';
+import {Track} from './track';
+import {trackRegistry} from './track_registry';
+
+export const TRACK_SHELL_WIDTH = 200;
+
+const TrackShell = {
+  view({attrs}) {
+    return m(
+        '.track-shell',
+        {
+          style: {
+            position: 'absolute',
+            left: '0px',
+            width: `${TRACK_SHELL_WIDTH}px`,
+            'box-sizing': 'border-box',
+          }
+        },
+        m('h1', attrs.trackState.name),
+        m('.reorder-icons',
+          m(TrackMoveButton, {
+            direction: 'up',
+            trackId: attrs.trackState.id,
+            top: 10,
+          }),
+          m(TrackMoveButton, {
+            direction: 'down',
+            trackId: attrs.trackState.id,
+            top: 40,
+          })));
+  },
+} as m.Component<{trackState: TrackState}>;
+
+const TrackContent = {
+  view({attrs}) {
+    return m('.track-content', {
+      style: {
+        position: 'absolute',
+        left: `${TRACK_SHELL_WIDTH}px`,
+        // TODO: We can use flex-box here and not do this manual
+        // calculation.
+        width: `calc(100% - ${TRACK_SHELL_WIDTH}px)`,
+        height: '100%',
+      },
+      onmousemove: (e: MouseEvent) => {
+        // TODO(hjd): Trigger a repaint here not a full m.redraw.
+        attrs.track.onMouseMove({x: e.layerX, y: e.layerY});
+        m.redraw();
+      },
+      onmouseout: () => {
+        attrs.track.onMouseOut();
+        m.redraw();
+      },
+    }, );
+  }
+} as m.Component<{track: Track}>;
+
+const TrackComponent = {
+  view({attrs}) {
+    return m('.track', [
+      m(TrackShell, {trackState: attrs.trackState}),
+      m(TrackContent, {track: attrs.track})
+    ], );
+  }
+} as m.Component<{trackState: TrackState, track: Track}>;
+
+const TrackMoveButton = {
+  view({attrs}) {
+    return m(
+        'i.material-icons.track-move-icons',
+        {
+          onclick: quietDispatch(moveTrack(attrs.trackId, attrs.direction)),
+          style: {
+            top: `${attrs.top}px`,
+          }
+        },
+        attrs.direction === 'up' ? 'arrow_upward_alt' : 'arrow_downward_alt');
+  }
+} as m.Component<{
+  direction: 'up' | 'down',
+  trackId: string,
+  top: number,
+},
+                        {}>;
+
+export class TrackPanel implements Panel {
+  private track: Track;
+  constructor(public trackState: TrackState) {
+    // TODO: Since ES6 modules are asynchronous and it is conceivable that we
+    // want to load a track implementation on demand, we should not rely here on
+    // the fact that the track is already registered. We should show some
+    // default content until a track implementation is found.
+    const trackCreator = trackRegistry.get(this.trackState.kind);
+    this.track = trackCreator.create(this.trackState);
+  }
+
+  updateDom(dom: Element): void {
+    // TODO: Let tracks render DOM in the content area.
+    m.render(
+        dom,
+        m(TrackComponent, {trackState: this.trackState, track: this.track}));
+  }
+
+  renderCanvas(ctx: CanvasRenderingContext2D) {
+    ctx.translate(TRACK_SHELL_WIDTH, 0);
+    const {visibleWindowMs} = globals.frontendLocalState;
+    drawGridLines(
+        ctx,
+        globals.frontendLocalState.timeScale,
+        [visibleWindowMs.start, visibleWindowMs.end],
+        // TODO: Height should be a property of panel.
+        this.trackState.height);
+
+    const trackData = globals.trackDataStore.get(this.trackState.id);
+    if (trackData !== undefined) this.track.consumeData(trackData);
+    this.track.renderCanvas(ctx);
+  }
+}
\ No newline at end of file