Merge "perfetto-ui: Draw area selection as a box"
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index 97acfd3..a64c13a 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -100,8 +100,6 @@
   localOnlyMode = false;
   sidebarVisible = true;
   showPanningHint = false;
-  // This is used to calculate the tracks within a Y range for area selection.
-  areaY: Range = {};
   visibleTracks = new Set<string>();
   prevVisibleTracks = new Set<string>();
   searchIndex = -1;
@@ -109,6 +107,12 @@
   scrollToTrackId?: string|number;
   httpRpcState: HttpRpcState = {connected: false};
   newVersionAvailable = false;
+
+  // This is used to calculate the tracks within a Y range for area selection.
+  areaY: Range = {};
+  // True if the user is in the process of doing an area selection.
+  selectingArea = false;
+
   private scrollBarWidth?: number;
 
   private _omniboxState: OmniboxState = {
diff --git a/ui/src/frontend/pan_and_zoom_handler.ts b/ui/src/frontend/pan_and_zoom_handler.ts
index 1f504b4..5d2af09 100644
--- a/ui/src/frontend/pan_and_zoom_handler.ts
+++ b/ui/src/frontend/pan_and_zoom_handler.ts
@@ -44,7 +44,7 @@
 const WHEEL_ZOOM_SPEED = -0.02;
 
 const EDITING_RANGE_CURSOR = 'ew-resize';
-const DRAG_CURSOR = 'text';
+const DRAG_CURSOR = 'default';
 const PAN_CURSOR = 'move';
 
 enum Pan {
@@ -98,25 +98,38 @@
   private onSelection:
       (dragStartX: number, dragStartY: number, prevX: number, currentX: number,
        currentY: number, editing: boolean) => void;
+  private selectingStarted: () => void;
+  private selectingEnded: () => void;
 
-  constructor(
-      {element, contentOffsetX, onPanned, onZoomed, editSelection, onSelection}:
-          {
-            element: HTMLElement,
-            contentOffsetX: number,
-            onPanned: (movedPx: number) => void,
-            onZoomed: (zoomPositionPx: number, zoomRatio: number) => void,
-            editSelection: (currentPx: number) => boolean,
-            onSelection:
-                (dragStartX: number, dragStartY: number, prevX: number,
-                 currentX: number, currentY: number, editing: boolean) => void,
-          }) {
+  constructor({
+    element,
+    contentOffsetX,
+    onPanned,
+    onZoomed,
+    editSelection,
+    onSelection,
+    selectingStarted,
+    selectingEnded
+  }: {
+    element: HTMLElement,
+    contentOffsetX: number,
+    onPanned: (movedPx: number) => void,
+    onZoomed: (zoomPositionPx: number, zoomRatio: number) => void,
+    editSelection: (currentPx: number) => boolean,
+    onSelection:
+        (dragStartX: number, dragStartY: number, prevX: number,
+         currentX: number, currentY: number, editing: boolean) => void,
+    selectingStarted: () => void,
+    selectingEnded: () => void,
+  }) {
     this.element = element;
     this.contentOffsetX = contentOffsetX;
     this.onPanned = onPanned;
     this.onZoomed = onZoomed;
     this.editSelection = editSelection;
     this.onSelection = onSelection;
+    this.selectingStarted = selectingStarted;
+    this.selectingEnded = selectingEnded;
 
     document.body.addEventListener('keydown', this.boundOnKeyDown);
     document.body.addEventListener('keyup', this.boundOnKeyUp);
@@ -147,6 +160,7 @@
           if (edit) {
             this.element.style.cursor = EDITING_RANGE_CURSOR;
           } else if (!this.shiftDown) {
+            this.selectingStarted();
             this.element.style.cursor = DRAG_CURSOR;
           }
         },
@@ -155,6 +169,7 @@
           this.element.style.cursor = this.shiftDown ? PAN_CURSOR : DRAG_CURSOR;
           dragStartX = -1;
           dragStartY = -1;
+          this.selectingEnded();
         });
   }
 
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 8698422..b2b34ba 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -343,10 +343,87 @@
       this.ctx.restore();
       panelYStart += panelHeight;
     }
+
+    this.drawTopLayerOnCanvas();
     const redrawDur = debugNow() - redrawStart;
     this.updatePerfStats(redrawDur, panels.length, totalOnCanvas);
   }
 
+  // The panels each draw on the canvas but some details need to be drawn across
+  // the whole canvas rather than per panel.
+  private drawTopLayerOnCanvas() {
+    if (!this.ctx) return;
+    if (this.attrs.kind !== 'TRACKS') return;
+    const selection = globals.frontendLocalState.selectedArea;
+    const area = selection.area;
+    if (area === undefined ||
+        globals.frontendLocalState.areaY.start === undefined ||
+        globals.frontendLocalState.areaY.end === undefined ||
+        !globals.frontendLocalState.selectingArea) {
+      return;
+    }
+    if (this.panelPositions.length === 0) return;
+    this.ctx.save();
+    this.ctx.strokeStyle = 'rgba(52,69,150)';
+    this.ctx.lineWidth = 1;
+    const canvasYStart =
+        Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
+    this.ctx.translate(TRACK_SHELL_WIDTH, -canvasYStart);
+
+    const startX = globals.frontendLocalState.timeScale.timeToPx(area.startSec);
+    const endX = globals.frontendLocalState.timeScale.timeToPx(area.endSec);
+    const firstPanelY = this.panelPositions[0].y;
+    const boxTop = Math.min(
+                       globals.frontendLocalState.areaY.start,
+                       globals.frontendLocalState.areaY.end) +
+        TOPBAR_HEIGHT;
+    const boxBottom = Math.max(
+                          globals.frontendLocalState.areaY.start,
+                          globals.frontendLocalState.areaY.end) +
+        TOPBAR_HEIGHT;
+
+    let snapStartY = -1;
+    let snapEndY = -1;
+    for (let i = 0; i < this.panelPositions.length; i++) {
+      const y = this.panelPositions[i].y;
+      // If the box top is within this panel set the snapStart to the top of
+      // the panel.
+      if (y <= boxTop && y + this.panelPositions[i].height >= boxTop) {
+        snapStartY = y;
+      }
+      // If the box bottom is within this panel set the snapBottom to the bottom
+      // of the panel.
+      if (y <= boxBottom && y + this.panelPositions[i].height >= boxBottom) {
+        snapEndY = y + this.panelPositions[i].height;
+        break;
+      }
+    }
+
+    // If the y selection is outside all panels, do not draw a box.
+    if (snapStartY === -1 && snapEndY === -1) {
+      this.ctx.restore();
+      return;
+    }
+
+    // If the y selection starts above all panels, snap to the first panel.
+    if (snapStartY === -1) {
+      snapStartY = firstPanelY;
+    }
+    // If the y selection ends after all panels, snap to the bottom of the
+    // last panel.
+    if (snapEndY === -1) {
+      snapEndY = this.totalPanelHeight;
+    }
+
+    // The areaY values are given from the top of the pan and zoom handler.
+    // To align with the current panel container subtract the first panel Y.
+    snapStartY -= firstPanelY;
+    snapEndY -= firstPanelY;
+    this.ctx.strokeRect(
+        startX, snapStartY, endX - startX, snapEndY - snapStartY);
+    this.ctx.restore();
+  }
+
   private updatePanelStats(
       panelIndex: number, panel: Panel, renderTime: number,
       ctx: CanvasRenderingContext2D, size: PanelSize) {
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index f08a0e2..89d143e 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -81,8 +81,9 @@
     }
 
     const selectedArea = globals.frontendLocalState.selectedArea.area;
-    const markSelectedClass =
-        selectedArea && selectedArea.tracks.includes(attrs.trackGroupId) ?
+    const markSelectedClass = selectedArea &&
+            selectedArea.tracks.includes(attrs.trackGroupId) &&
+            !globals.frontendLocalState.selectingArea ?
         'selected' :
         '';
 
@@ -121,6 +122,19 @@
         getComputedStyle(dom).getPropertyValue('--collapsed-background');
   }
 
+  highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
+    const localState = globals.frontendLocalState;
+    const area = localState.selectedArea.area;
+    if (area && area.tracks.includes(this.trackGroupId)) {
+      ctx.fillStyle = '#ebeef9';
+      ctx.fillRect(
+          localState.timeScale.timeToPx(area.startSec) + this.shellWidth,
+          0,
+          localState.timeScale.deltaTimeToPx(area.endSec - area.startSec),
+          size.height);
+    }
+  }
+
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
     const collapsed = this.trackGroupState.collapsed;
     if (!collapsed) return;
@@ -130,6 +144,8 @@
     ctx.fillStyle = this.backgroundColor;
     ctx.fillRect(0, 0, size.width, size.height);
 
+    this.highlightIfTrackSelected(ctx, size);
+
     drawGridLines(
         ctx,
         globals.frontendLocalState.timeScale,
@@ -138,6 +154,7 @@
         size.height);
 
     ctx.translate(this.shellWidth, 0);
+
     if (this.summaryTrack) {
       this.summaryTrack.render(ctx);
     }
@@ -160,7 +177,8 @@
                             size.height,
                             `rgb(52,69,150)`);
     }
-    if (localState.selectedArea.area !== undefined) {
+    if (localState.selectedArea.area !== undefined &&
+        !globals.frontendLocalState.selectingArea) {
       drawVerticalSelection(
           ctx,
           localState.timeScale,
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 098bd81..fb798b4 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -64,8 +64,9 @@
     const dragClass = this.dragging ? `drag` : '';
     const dropClass = this.dropping ? `drop-${this.dropping}` : '';
     const selectedArea = globals.frontendLocalState.selectedArea.area;
-    const markSelectedClass =
-        selectedArea && selectedArea.tracks.includes(attrs.trackState.id) ?
+    const markSelectedClass = selectedArea &&
+            selectedArea.tracks.includes(attrs.trackState.id) &&
+            !globals.frontendLocalState.selectingArea ?
         'selected' :
         '';
     return m(
@@ -253,8 +254,25 @@
     return m(TrackComponent, {trackState: this.trackState, track: this.track});
   }
 
+  highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
+    const localState = globals.frontendLocalState;
+    const area = localState.selectedArea.area;
+    if (area && area.tracks.includes(this.trackState.id)) {
+      const timeScale = localState.timeScale;
+      ctx.fillStyle = '#ebeef9';
+      ctx.fillRect(
+          timeScale.timeToPx(area.startSec) + TRACK_SHELL_WIDTH,
+          0,
+          timeScale.deltaTimeToPx(area.endSec - area.startSec),
+          size.height);
+    }
+  }
+
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
     ctx.save();
+
+    this.highlightIfTrackSelected(ctx, size);
+
     drawGridLines(
         ctx,
         globals.frontendLocalState.timeScale,
@@ -286,7 +304,8 @@
                              size.height,
                              `rgb(52,69,150)`);
     }
-    if (localState.selectedArea.area !== undefined) {
+    if (localState.selectedArea.area !== undefined &&
+        !globals.frontendLocalState.selectingArea) {
       drawVerticalSelection(
           ctx,
           localState.timeScale,
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index aa8d0c4..c64a852 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -225,6 +225,16 @@
           frontendLocalState.areaY.end = currentY;
         }
         globals.rafScheduler.scheduleRedraw();
+      },
+      selectingStarted: () => {
+        globals.frontendLocalState.selectingArea = true;
+      },
+      selectingEnded: () => {
+        globals.frontendLocalState.selectingArea = false;
+        globals.frontendLocalState.areaY.start = undefined;
+        globals.frontendLocalState.areaY.end = undefined;
+        // Full redraw to color track shell.
+        globals.rafScheduler.scheduleFullRedraw();
       }
     });
   }