[perfetto-ui] Update canvas size when container parent size changes

When we add panels to the pinned container, the scrolling container
height changes but the backing canvas size was not changing. This CL
fixes that. Also fixed a bug where the canvas was the positioned
properly on the first redraw, and cleaned up some code.

Change-Id: I316e72d9b0ff43df90b2e3afb7dc12c6748fbeb8
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 15be904..9329053 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -57,26 +57,29 @@
     renderStats: new RunningStatistics(10),
   };
 
-  // attrs received in the most recent mithril redraw.
-  private attrs?: Attrs;
+  // Attrs received in the most recent mithril redraw. We receive a new vnode
+  // with new attrs on every redraw, and we cache it here so that resize
+  // listeners and canvas redraw callbacks can access it.
+  private attrs: Attrs;
 
-  private canvasOverdrawFactor: number;
   private ctx?: CanvasRenderingContext2D;
 
   private onResize: () => void = () => {};
   private parentOnScroll: () => void = () => {};
   private canvasRedrawer: () => void;
 
+  get canvasOverdrawFactor() {
+    return this.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
+  }
+
   constructor(vnode: m.CVnode<Attrs>) {
-    this.canvasOverdrawFactor =
-        vnode.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
+    this.attrs = vnode.attrs;
     this.canvasRedrawer = () => this.redrawCanvas();
     globals.rafScheduler.addRedrawCallback(this.canvasRedrawer);
     perfDisplay.addContainer(this);
   }
 
   oncreate(vnodeDom: m.CVnodeDOM<Attrs>) {
-    const attrs = vnodeDom.attrs;
     // Save the canvas context in the state.
     const canvas =
         vnodeDom.dom.querySelector('.main-canvas') as HTMLCanvasElement;
@@ -91,21 +94,18 @@
     this.parentWidth = clientRect.width;
     this.parentHeight = clientRect.height;
 
-    this.updatePanelHeightsFromDom(vnodeDom);
+    this.readPanelHeightsFromDom(vnodeDom.dom);
     (vnodeDom.dom as HTMLElement).style.height = `${this.totalPanelHeight}px`;
 
-    this.canvasHeight = this.getCanvasHeight(attrs.doesScroll);
-    this.updateCanvasDimensions(vnodeDom);
+    this.updateCanvasDimensions();
+    this.repositionCanvas();
 
     // Save the resize handler in the state so we can remove it later.
     // TODO: Encapsulate resize handling better.
     this.onResize = () => {
-      const clientRect =
-          assertExists(vnodeDom.dom.parentElement).getBoundingClientRect();
-      this.parentWidth = clientRect.width;
-      this.parentHeight = clientRect.height;
-      this.canvasHeight = this.getCanvasHeight(attrs.doesScroll);
-      this.updateCanvasDimensions(vnodeDom);
+      this.readParentSizeFromDom(vnodeDom.dom);
+      this.updateCanvasDimensions();
+      this.repositionCanvas();
       globals.rafScheduler.scheduleFullRedraw();
     };
 
@@ -113,10 +113,10 @@
     window.addEventListener('resize', this.onResize);
 
     // TODO(dproy): Handle change in doesScroll attribute.
-    if (vnodeDom.attrs.doesScroll) {
+    if (this.attrs.doesScroll) {
       this.parentOnScroll = () => {
-        this.scrollTop = vnodeDom.dom.parentElement!.scrollTop;
-        this.repositionCanvas(vnodeDom);
+        this.scrollTop = assertExists(vnodeDom.dom.parentElement).scrollTop;
+        this.repositionCanvas();
         globals.rafScheduler.scheduleRedraw();
       };
       vnodeDom.dom.parentElement!.addEventListener(
@@ -134,8 +134,6 @@
   }
 
   view({attrs}: m.CVnode<Attrs>) {
-    // We receive a new vnode object with new attrs on every mithril redraw. We
-    // store the latest attrs so redrawCanvas can use it.
     this.attrs = attrs;
     const renderPanel = (panel: m.Vnode) => perfDebug() ?
         m('.panel', panel, m('.debug-panel-border')) :
@@ -148,25 +146,27 @@
   }
 
   onupdate(vnodeDom: m.CVnodeDOM<Attrs>) {
-    this.repositionCanvas(vnodeDom);
+    const totalPanelHeightChanged = this.readPanelHeightsFromDom(vnodeDom.dom);
+    const parentSizeChanged = this.readParentSizeFromDom(vnodeDom.dom);
 
-    if (this.updatePanelHeightsFromDom(vnodeDom)) {
+    if (totalPanelHeightChanged) {
       (vnodeDom.dom as HTMLElement).style.height = `${this.totalPanelHeight}px`;
     }
 
-    // In non-scrolling case, canvas height can change if panel heights changed.
-    const canvasHeight = this.getCanvasHeight(vnodeDom.attrs.doesScroll);
-    if (this.canvasHeight !== canvasHeight) {
-      this.canvasHeight = canvasHeight;
-      this.updateCanvasDimensions(vnodeDom);
+    const canvasSizeShouldChange =
+        this.attrs.doesScroll ? parentSizeChanged : totalPanelHeightChanged;
+    if (canvasSizeShouldChange) {
+      this.updateCanvasDimensions();
+      this.repositionCanvas();
     }
   }
 
-  private updateCanvasDimensions(vnodeDom: m.CVnodeDOM<Attrs>) {
-    const canvas =
-        assertExists(vnodeDom.dom.querySelector('canvas.main-canvas')) as
-        HTMLCanvasElement;
+  private updateCanvasDimensions() {
+    this.canvasHeight = this.attrs.doesScroll ?
+        this.parentHeight * this.canvasOverdrawFactor :
+        this.totalPanelHeight;
     const ctx = assertExists(this.ctx);
+    const canvas = assertExists(ctx.canvas);
     canvas.style.height = `${this.canvasHeight}px`;
     const dpr = window.devicePixelRatio;
     ctx.canvas.width = this.parentWidth * dpr;
@@ -174,13 +174,36 @@
     ctx.scale(dpr, dpr);
   }
 
-  private updatePanelHeightsFromDom(vnodeDom: m.CVnodeDOM<Attrs>): boolean {
+  private repositionCanvas() {
+    const canvas = assertExists(assertExists(this.ctx).canvas);
+    const canvasYStart = this.scrollTop - this.getCanvasOverdrawHeightPerSide();
+    canvas.style.transform = `translateY(${canvasYStart}px)`;
+  }
+
+  /**
+   * Reads dimensions of parent node. Returns true if read dimensions are
+   * different from what was cached in the state.
+   */
+  private readParentSizeFromDom(dom: Element): boolean {
+    const oldWidth = this.parentWidth;
+    const oldHeight = this.parentHeight;
+    const clientRect = assertExists(dom.parentElement).getBoundingClientRect();
+    this.parentWidth = clientRect.width;
+    this.parentHeight = clientRect.height;
+    return this.parentHeight !== oldHeight || this.parentWidth !== oldWidth;
+  }
+
+  /**
+   * Reads dimensions of panels. Returns true if total panel height is different
+   * from what was cached in state.
+   */
+  private readPanelHeightsFromDom(dom: Element): boolean {
     const prevHeight = this.totalPanelHeight;
     this.panelHeights = [];
     this.totalPanelHeight = 0;
 
-    const panels = vnodeDom.dom.querySelectorAll('.panel');
-    assertTrue(panels.length === vnodeDom.attrs.panels.length);
+    const panels = dom.querySelectorAll('.panel');
+    assertTrue(panels.length === this.attrs.panels.length);
     for (let i = 0; i < panels.length; i++) {
       const height = panels[i].getBoundingClientRect().height;
       this.panelHeights[i] = height;
@@ -190,19 +213,6 @@
     return this.totalPanelHeight !== prevHeight;
   }
 
-  private getCanvasHeight(doesScroll: boolean) {
-    return doesScroll ? this.parentHeight * this.canvasOverdrawFactor :
-                        this.totalPanelHeight;
-  }
-
-  private repositionCanvas(vnodeDom: m.CVnodeDOM<Attrs>) {
-    const canvas =
-        assertExists(vnodeDom.dom.querySelector('canvas.main-canvas')) as
-        HTMLCanvasElement;
-    const canvasYStart = this.scrollTop - this.getCanvasOverdrawHeightPerSide();
-    canvas.style.transform = `translateY(${canvasYStart}px)`;
-  }
-
   private overlapsCanvas(yStart: number, yEnd: number) {
     return yEnd > 0 && yStart < this.canvasHeight;
   }