Merge "perfetto-ui: Scrollable details panel"
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index dc4e411..85720b8 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -53,7 +53,6 @@
 
 * {
     box-sizing: border-box;
-    overflow: hidden;
     -webkit-tap-highlight-color: none;
     touch-action: none;
 }
@@ -97,6 +96,7 @@
     grid-template-rows: auto auto 1fr;
     grid-template-columns: auto 1fr;
     color: #121212;
+    overflow: hidden;
 }
 
 button {
@@ -132,7 +132,8 @@
     grid-area: page;
     position: relative;
     display: flex;
-    flex-direction: column
+    flex-direction: column;
+    overflow: hidden;
 }
 
 .split-panel {
@@ -140,6 +141,7 @@
   display: flex;
   flex-flow: row;
   position: relative;
+  overflow: hidden;
 }
 
 .video-panel {
@@ -346,6 +348,24 @@
   overflow-y: auto;
   flex: 1 1 auto;
   will-change: transform;  // Force layer creation.
+  display: grid;
+  grid-template-columns: 1fr;
+  grid-template-rows: 1fr;
+  grid-template-areas: "space";
+}
+
+.details-panel-container {
+  position: relative;
+  overflow-x: hidden;
+  overflow-y: auto;
+  flex: 1 1 auto;
+  // TODO(taylori): This causes the sticky header to flicker when scrolling.
+  // Is will-change necessary in the details panel?
+  // will-change: transform;
+  display: grid;
+  grid-template-columns: 1fr;
+  grid-template-rows: 1fr;
+  grid-template-areas: "space";
 }
 
 .pinned-panel-container {
@@ -355,6 +375,10 @@
   overflow: visible;
   box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
   z-index: 2;
+  display: grid;
+  grid-template-columns: 1fr;
+  grid-template-rows: 1fr;
+  grid-template-areas: "space";
 }
 
 // In the scrolling case, since the canvas is overdrawn and continuously
@@ -362,14 +386,17 @@
 // height equaling the total height of the content to prevent scrolling
 // height from growing.
 .scroll-limiter {
-  overflow: hidden;
   position: relative;
+  grid-area: space;
+  overflow: hidden;
 }
 
 canvas.main-canvas {
-  top: 0px;
   z-index: -1;
-  position: absolute;
+}
+
+.panels {
+  grid-area: space;
 }
 
 .panel {
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index 5a0e44a..d5f4d5c 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -33,7 +33,7 @@
       .tab {
         font-family: 'Google Sans';
         color: #3c4b5d;
-        padding: 3px 10px 10px 10px;
+        padding: 3px 10px 0px 10px;
         margin-top: 3px;
         font-size: 13px;
         border-radius: 5px 5px 0px 0px;
@@ -78,26 +78,25 @@
     }
   }
 
-  .details-panel-container {
-    .scroll-limiter {
-      height: 100%;
-      display: flex;
-      flex-direction: column;
-      .panel:last-child {
-        flex-grow: 1;
-      }
-    }
-  }
 }
 
 .details-panel {
-  padding: 10px;
   font-family: 'Google Sans';
   color: #3c4b5d;
-
   .details-panel-heading {
-    font-size: 16px;
-    padding-bottom: 5px;
+    padding: 10px 0 5px 0;
+    position: sticky;
+    top: 0px;
+    display: flex;
+    background: white;
+    h2 {
+      font-size: 16px;
+      font-family: 'Google Sans';
+      padding: 0 10px;
+      &.split {
+        width: 50%;
+      }
+    }
   }
 
   table {
@@ -109,6 +108,7 @@
     max-width: 50%;
     table-layout: fixed;
     word-wrap: break-word;
+    padding: 10px;
     tr:hover {
       background-color: hsl(214, 22%, 90%);
     }
@@ -141,12 +141,14 @@
     padding-bottom: .5rem;
     border-radius: .25rem;
     margin-top: 12px;
+    margin-left: 10px;
   }
 
   .explanation {
     font-size: 14px;
     width: 35%;
     margin-top: 10px;
+    padding-left: 10px;
   }
 
   .material-icons {
@@ -209,17 +211,16 @@
   display: grid;
   grid-template-rows: auto 1fr;
 
+  header {
+    position: sticky;
+    top: 0px;
+    z-index: 1;
+  }
+
   header.stale {
     color: grey;
   }
 
-  .scrolling-container {
-    overflow-y: scroll;
-    position: relative;
-    width: 100%;
-    background-color: #fefefe;
-    border-bottom: 1px solid hsl(213, 22%, 75%);
-
     .rows {
       position: relative;
       direction: ltr;
@@ -280,6 +281,5 @@
           }
         }
       }
-    }
   }
 }
diff --git a/ui/src/frontend/chrome_slice_panel.ts b/ui/src/frontend/chrome_slice_panel.ts
index 97ce65c..b43286e 100644
--- a/ui/src/frontend/chrome_slice_panel.ts
+++ b/ui/src/frontend/chrome_slice_panel.ts
@@ -25,7 +25,7 @@
     if (sliceInfo.ts && sliceInfo.dur && sliceInfo.name) {
       return m(
           '.details-panel',
-          m('.details-panel-heading', `Slice Details:`),
+          m('.details-panel-heading', m('h2', `Slice Details`)),
           m(
               '.details-table',
               [m('table',
@@ -47,10 +47,11 @@
     } else {
       return m(
           '.details-panel',
-          m(
-              '.details-panel-heading',
-              `Slice Details:`,
-              ));
+          m('.details-panel-heading',
+            m(
+                'h2',
+                `Slice Details`,
+                )));
     }
   }
   renderCanvas(_ctx: CanvasRenderingContext2D, _size: PanelSize) {}
diff --git a/ui/src/frontend/counter_panel.ts b/ui/src/frontend/counter_panel.ts
index 458a037..df207ff 100644
--- a/ui/src/frontend/counter_panel.ts
+++ b/ui/src/frontend/counter_panel.ts
@@ -29,7 +29,7 @@
         counterInfo.duration !== undefined) {
       return m(
           '.details-panel',
-          m('.details-panel-heading', `Counter Details:`),
+          m('.details-panel-heading', m('h2', `Counter Details`)),
           m(
               '.details-table',
               [m('table',
@@ -50,7 +50,8 @@
               ));
     } else {
       return m(
-          '.details-panel', m('.details-panel-heading', `Counter Details:`));
+          '.details-panel',
+          m('.details-panel-heading', m('h2', `Counter Details`)));
     }
   }
 
diff --git a/ui/src/frontend/heap_profile_panel.ts b/ui/src/frontend/heap_profile_panel.ts
index 938a80b..524bb40 100644
--- a/ui/src/frontend/heap_profile_panel.ts
+++ b/ui/src/frontend/heap_profile_panel.ts
@@ -37,7 +37,7 @@
       this.pid = heapDumpInfo.pid;
       return m(
           '.details-panel',
-          m('.details-panel-heading', `Heap Profile Details:`),
+          m('.details-panel-heading', m('h2', `Heap Profile Details`)),
           m(
               '.details-table',
               [m('table',
@@ -76,7 +76,7 @@
     } else {
       return m(
           '.details-panel',
-          m('.details-panel-heading', `Heap Snapshot Details:`));
+          m('.details-panel-heading', m('h2', `Heap Profile Details`)));
     }
   }
 
diff --git a/ui/src/frontend/logs_panel.ts b/ui/src/frontend/logs_panel.ts
index 1f80869..2f4c485 100644
--- a/ui/src/frontend/logs_panel.ts
+++ b/ui/src/frontend/logs_panel.ts
@@ -59,8 +59,8 @@
   }
 
   oncreate({dom}: m.CVnodeDOM) {
-    this.scrollContainer =
-        assertExists(dom.querySelector('.scrolling-container') as HTMLElement);
+    this.scrollContainer = assertExists(
+        dom.parentElement!.parentElement!.parentElement as HTMLElement);
     this.scrollContainer.addEventListener(
         'scroll', this.onScroll.bind(this), {passive: true});
     this.bounds = globals.trackDataStore.get(LogBoundsKey) as LogBounds;
@@ -143,8 +143,7 @@
             'class': isStale ? 'stale' : '',
           },
           `Logs rows [${offset}, ${offset + count}] / ${total}`),
-        m('.scrolling-container',
-          m('.rows', {style: {height: `${total * ROW_H}px`}}, rows)));
+        m('.rows', {style: {height: `${total * ROW_H}px`}}, rows));
   }
 
   renderCanvas() {}
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 80f99c2..d040eea 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -140,10 +140,13 @@
         m('.panel', panel, m('.debug-panel-border')) :
         m('.panel', {key: panel.key}, panel);
 
-    return m(
-        '.scroll-limiter',
-        m('canvas.main-canvas'),
-        attrs.panels.map(renderPanel));
+    return [
+      m(
+          '.scroll-limiter',
+          m('canvas.main-canvas'),
+          ),
+      m('.panels', attrs.panels.map(renderPanel))
+    ];
   }
 
   onupdate(vnodeDom: m.CVnodeDOM<Attrs>) {
@@ -210,7 +213,7 @@
     this.panelHeights = [];
     this.totalPanelHeight = 0;
 
-    const panels = dom.querySelectorAll('.panel');
+    const panels = dom.parentElement!.querySelectorAll('.panel');
     assertTrue(panels.length === this.attrs.panels.length);
     for (let i = 0; i < panels.length; i++) {
       const height = panels[i].getBoundingClientRect().height;
diff --git a/ui/src/frontend/slice_panel.ts b/ui/src/frontend/slice_panel.ts
index 1437c37..7a37de7 100644
--- a/ui/src/frontend/slice_panel.ts
+++ b/ui/src/frontend/slice_panel.ts
@@ -19,7 +19,7 @@
 import {translateState} from '../common/thread_state';
 import {timeToCode, toNs} from '../common/time';
 
-import {globals} from './globals';
+import {globals, SliceDetails, ThreadDesc} from './globals';
 import {Panel, PanelSize} from './panel';
 import {scrollToTrackAndTs} from './scroll_helper';
 
@@ -29,47 +29,47 @@
     if (sliceInfo.utid === undefined) return;
     const threadInfo = globals.threads.get(sliceInfo.utid);
 
-    if (threadInfo && sliceInfo.ts !== undefined &&
-        sliceInfo.dur !== undefined) {
-      return m(
-          '.details-panel',
-          m('.details-panel-heading', `Slice Details:`),
-          m(
-              '.details-table',
-              [m('table',
-                 [
-                   m('tr',
-                     m('th', `Process`),
-                     m('td', `${threadInfo.procName} [${threadInfo.pid}]`)),
-                   m('tr',
-                     m('th', `Thread`),
-                     m('td',
-                       `${threadInfo.threadName} [${threadInfo.tid}]`,
-                       m('i.material-icons',
-                         {
-                           onclick: () => this.goToThread(),
-                           title: 'Go to thread'
-                         },
-                         'call_made'))),
-                   m('tr',
-                     m('th', `Start time`),
-                     m('td', `${timeToCode(sliceInfo.ts)}`)),
-                   m('tr',
-                     m('th', `Duration`),
-                     m('td', `${timeToCode(sliceInfo.dur)}`)),
-                   m('tr', m('th', `Prio`), m('td', `${sliceInfo.priority}`)),
-                   m('tr',
-                     m('th', `End State`),
-                     m('td', `${translateState(sliceInfo.endState)}`))
-                 ])],
-              ));
+    return m(
+        '.details-panel',
+        m('.details-panel-heading',
+          m('h2.split', `Slice Details`),
+          (sliceInfo.wakeupTs && sliceInfo.wakerUtid) ?
+              m('h2.split', 'Scheduling Latency') :
+              ''),
+        this.getDetails(sliceInfo, threadInfo));
+  }
+
+  getDetails(sliceInfo: SliceDetails, threadInfo: ThreadDesc|undefined) {
+    if (!threadInfo || sliceInfo.ts === undefined ||
+        sliceInfo.dur === undefined) {
+      return null;
     } else {
       return m(
-          '.details-panel',
-          m(
-              '.details-panel-heading',
-              `Slice Details:`,
-              ));
+          '.details-table',
+          m('table',
+            [
+              m('tr',
+                m('th', `Process`),
+                m('td', `${threadInfo.procName} [${threadInfo.pid}]`)),
+              m('tr',
+                m('th', `Thread`),
+                m('td',
+                  `${threadInfo.threadName} [${threadInfo.tid}]`,
+                  m('i.material-icons',
+                    {onclick: () => this.goToThread(), title: 'Go to thread'},
+                    'call_made'))),
+              m('tr',
+                m('th', `Start time`),
+                m('td', `${timeToCode(sliceInfo.ts)}`)),
+              m('tr',
+                m('th', `Duration`),
+                m('td', `${timeToCode(sliceInfo.dur)}`)),
+              m('tr', m('th', `Prio`), m('td', `${sliceInfo.priority}`)),
+              m('tr',
+                m('th', `End State`),
+                m('td', `${translateState(sliceInfo.endState)}`))
+            ]),
+      );
     }
   }
 
@@ -113,13 +113,8 @@
     // Show expanded details on the scheduling of the currently selected slice.
     if (details.wakeupTs && details.wakerUtid !== undefined) {
       const threadInfo = globals.threads.get(details.wakerUtid);
-      // Draw separation line.
-      ctx.fillStyle = '#3c4b5d';
-      ctx.fillRect(size.width / 2, 10, 1, size.height - 10);
-      ctx.font = '16px Google Sans';
-      ctx.fillText('Scheduling Latency:', size.width / 2 + 30, 30);
       // Draw diamond and vertical line.
-      const startDraw = {x: size.width / 2 + 30, y: 52};
+      const startDraw = {x: size.width / 2 + 20, y: 52};
       ctx.beginPath();
       ctx.moveTo(startDraw.x, startDraw.y + 28);
       ctx.fillStyle = 'black';
diff --git a/ui/src/frontend/thread_state_panel.ts b/ui/src/frontend/thread_state_panel.ts
index 3df1884..7ad6127 100644
--- a/ui/src/frontend/thread_state_panel.ts
+++ b/ui/src/frontend/thread_state_panel.ts
@@ -36,7 +36,7 @@
     if (threadInfo) {
       return m(
           '.details-panel',
-          m('.details-panel-heading', 'Thread State'),
+          m('.details-panel-heading', m('h2', 'Thread State')),
           m('.details-table', [m('table', [
               m('tr',
                 m('th', `Start time`),
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 22e5b9a..c5fc8d7 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -153,7 +153,7 @@
   view({attrs}: m.CVnode<TrackContentAttrs>) {
     return m('.track-content', {
       onmousemove: (e: MouseEvent) => {
-        attrs.track.onMouseMove({x: e.layerX, y: e.layerY});
+        attrs.track.onMouseMove({x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY});
         globals.rafScheduler.scheduleRedraw();
       },
       onmouseout: () => {
@@ -165,15 +165,16 @@
         // track.
         if (e.shiftKey) return;
         // If the click is outside of the current time range, clear it.
-        const clickTime =
-            globals.frontendLocalState.timeScale.pxToTime(e.layerX);
+        const clickTime = globals.frontendLocalState.timeScale.pxToTime(
+            e.layerX - TRACK_SHELL_WIDTH);
         const start = globals.frontendLocalState.selectedTimeRange.startSec;
         const end = globals.frontendLocalState.selectedTimeRange.endSec;
         if (start !== undefined && end !== undefined &&
             (clickTime < start || clickTime > end)) {
           globals.frontendLocalState.removeTimeRange();
           e.stopPropagation();
-        } else if (attrs.track.onMouseClick({x: e.layerX, y: e.layerY})) {
+        } else if (attrs.track.onMouseClick(
+                       {x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY})) {
           e.stopPropagation();
         }
         globals.rafScheduler.scheduleRedraw();