Merge "perfetto-ui: Tidy up scrolling code"
diff --git a/ui/src/frontend/keyboard_event_handler.ts b/ui/src/frontend/keyboard_event_handler.ts
index 424c162..3ad39f4 100644
--- a/ui/src/frontend/keyboard_event_handler.ts
+++ b/ui/src/frontend/keyboard_event_handler.ts
@@ -13,10 +13,10 @@
 // limitations under the License.
 
 import {Actions} from '../common/actions';
-import {TimeSpan} from '../common/time';
 
 import {globals} from './globals';
 import {toggleHelp} from './help_modal';
+import {horizontalScrollAndZoomToRange} from './scroll_helper';
 import {executeSearch} from './search_handler';
 
 // Handles all key events than are not handled by the
@@ -67,12 +67,6 @@
   if (startTs !== -1 && endTs !== -1) {
     globals.dispatch(Actions.selectTimeSpan({startTs, endTs}));
     // Zoom into the highlighted time region.
-    const visibleDur = globals.frontendLocalState.visibleWindowTime.end -
-        globals.frontendLocalState.visibleWindowTime.start;
-    const selectDur = endTs - startTs;
-    if (selectDur / visibleDur < 0.05) {
-      globals.frontendLocalState.updateVisibleTime(
-          new TimeSpan(startTs - (selectDur * 2), endTs + (selectDur * 2)));
-    }
+    horizontalScrollAndZoomToRange(startTs, endTs);
   }
 }
diff --git a/ui/src/frontend/scroll_helper.ts b/ui/src/frontend/scroll_helper.ts
new file mode 100644
index 0000000..97f8de6
--- /dev/null
+++ b/ui/src/frontend/scroll_helper.ts
@@ -0,0 +1,83 @@
+// Copyright (C) 2019 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 {getContainingTrackId} from '../common/state';
+import {fromNs, TimeSpan, toNs} from '../common/time';
+
+import {globals} from './globals';
+
+/**
+ * Given a timestamp, if |ts| is not currently in view move the view to
+ * center |ts|, keeping the same zoom level.
+ */
+export function horizontalScrollToTs(ts: number) {
+  const startNs = toNs(globals.frontendLocalState.visibleWindowTime.start);
+  const endNs = toNs(globals.frontendLocalState.visibleWindowTime.end);
+  const currentViewNs = endNs - startNs;
+  if (ts < startNs || ts > endNs) {
+    // TODO(taylori): This is an ugly jump, we should do a smooth pan instead.
+    globals.frontendLocalState.updateVisibleTime(new TimeSpan(
+        fromNs(ts - currentViewNs / 2), fromNs(ts + currentViewNs / 2)));
+  }
+}
+
+/**
+ * Given a start and end timestamp (in ns), move the view to center this range
+ * and zoom to a level where the range is 1/5 of the viewport.
+ */
+export function horizontalScrollAndZoomToRange(startTs: number, endTs: number) {
+  const visibleDur = globals.frontendLocalState.visibleWindowTime.end -
+      globals.frontendLocalState.visibleWindowTime.start;
+  const selectDur = endTs - startTs;
+  const viewStartNs = toNs(globals.frontendLocalState.visibleWindowTime.start);
+  const viewEndNs = toNs(globals.frontendLocalState.visibleWindowTime.end);
+  if (selectDur / visibleDur < 0.05 || startTs < viewStartNs ||
+      endTs > viewEndNs) {
+    globals.frontendLocalState.updateVisibleTime(
+        new TimeSpan(startTs - (selectDur * 2), endTs + (selectDur * 2)));
+  }
+}
+
+/**
+ * Given a track id, find a track with that id and scroll it into view. If the
+ * track is nested inside a track group, scroll to that track group instead.
+ */
+export function verticalScrollToTrack(trackId: string|number) {
+  const trackIdString = trackId.toString();
+  let track = document.querySelector('#track_' + trackIdString);
+
+  if (!track) {
+    const parentTrackId = getContainingTrackId(globals.state, trackIdString);
+    if (parentTrackId) {
+      track = document.querySelector('#track_' + parentTrackId);
+    }
+  }
+
+  if (!track) {
+    console.error(`Can't scroll, track (${trackIdString}) not found.`);
+    return;
+  }
+
+  // block: 'nearest' means that it will only scroll if the track is not
+  // currently in view.
+  track.scrollIntoView({behavior: 'smooth', block: 'nearest'});
+}
+
+/**
+ * Scroll vertically and horizontally to reach track (|trackId|) at |ts|.
+ */
+export function scrollToTrackAndTs(trackId: string|number, ts: number) {
+  verticalScrollToTrack(trackId);
+  horizontalScrollToTs(ts);
+}
\ No newline at end of file
diff --git a/ui/src/frontend/search_handler.ts b/ui/src/frontend/search_handler.ts
index 4c64440..351fd27 100644
--- a/ui/src/frontend/search_handler.ts
+++ b/ui/src/frontend/search_handler.ts
@@ -14,10 +14,10 @@
 
 import {searchSegment} from '../base/binary_search';
 import {Actions} from '../common/actions';
-import {getContainingTrackId} from '../common/state';
-import {fromNs, TimeSpan, toNs} from '../common/time';
+import {toNs} from '../common/time';
 
 import {globals} from './globals';
+import {scrollToTrackAndTs} from './scroll_helper';
 
 export function executeSearch(reverse = false) {
   const state = globals.frontendLocalState;
@@ -58,39 +58,11 @@
 }
 
 function moveViewportToCurrentSearch() {
-  // Move viewport if our selection moves outside.
-  const startNs = toNs(globals.frontendLocalState.visibleWindowTime.start);
-  const endNs = toNs(globals.frontendLocalState.visibleWindowTime.end);
   const currentTs = globals.currentSearchResults
                         .tsStarts[globals.frontendLocalState.searchIndex];
-  const currentViewNs = endNs - startNs;
-  if (currentTs < startNs || currentTs > endNs) {
-    // TODO(taylori): This is an ugly jump, we should do a smooth pan instead.
-    globals.frontendLocalState.updateVisibleTime(new TimeSpan(
-        fromNs(currentTs - currentViewNs / 2),
-        fromNs(currentTs + currentViewNs / 2)));
-  }
-
-  // Update vertical (up/down) scroll position
   const trackId = globals.currentSearchResults
                       .trackIds[globals.frontendLocalState.searchIndex];
-  let track = document.querySelector('#track_' + trackId);
-
-  if (!track) {
-    const parentTrackId = getContainingTrackId(globals.state, trackId);
-    if (parentTrackId) {
-      track = document.querySelector('#track_' + parentTrackId);
-    }
-  }
-
-  if (!track) {
-    console.error(`Can't scroll search result track not found (${trackId})`);
-    return;
-  }
-
-  // block: 'nearest' means that it will only scroll if the track is not
-  // currently in view.
-  track.scrollIntoView({behavior: 'smooth', block: 'nearest'});
+  scrollToTrackAndTs(trackId, currentTs);
 }
 
 function selectCurrentSearchResult() {