| // 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 {time} from '../base/time'; |
| import {escapeCSSSelector, exists} from '../base/utils'; |
| import {HighPrecisionTime} from '../common/high_precision_time'; |
| import {HighPrecisionTimeSpan} from '../common/high_precision_time_span'; |
| import {raf} from '../core/raf_scheduler'; |
| 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: time) { |
| const visibleWindow = globals.timeline.visibleWindow; |
| if (!visibleWindow.contains(ts)) { |
| // TODO(hjd): This is an ugly jump, we should do a smooth pan instead. |
| const halfDuration = visibleWindow.duration / 2; |
| const newStart = new HighPrecisionTime(ts).subNumber(halfDuration); |
| const newWindow = new HighPrecisionTimeSpan( |
| newStart, |
| visibleWindow.duration, |
| ); |
| globals.timeline.updateVisibleTimeHP(newWindow); |
| } |
| } |
| |
| // Given a start and end timestamp (in ns), move the viewport to center this |
| // range and zoom if necessary: |
| // - If [viewPercentage] is specified, the viewport will be zoomed so that |
| // the given time range takes up this percentage of the viewport. |
| // The following scenarios assume [viewPercentage] is undefined. |
| // - If the new range is more than 50% of the viewport, zoom out to a level |
| // where |
| // the range is 1/5 of the viewport. |
| // - If the new range is already centered, update the zoom level for the |
| // viewport |
| // to cover 1/5 of the viewport. |
| // - Otherwise, preserve the zoom range. |
| export function focusHorizontalRange( |
| start: time, |
| end: time, |
| viewPercentage?: number, |
| ): void { |
| if (exists(viewPercentage)) { |
| focusHorizontalRangePercentage(start, end, viewPercentage); |
| } else { |
| focusHorizontalRangeImpl(start, end); |
| } |
| } |
| |
| // 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. |
| // If |openGroup| then open the track group and scroll to the track. |
| export function verticalScrollToTrack(trackUri: string, openGroup = false) { |
| const track = document.querySelector('#track_' + escapeCSSSelector(trackUri)); |
| |
| if (track) { |
| // block: 'nearest' means that it will only scroll if the track is not |
| // currently in view. |
| track.scrollIntoView({behavior: 'smooth', block: 'nearest'}); |
| return; |
| } |
| |
| // If we get here, the element for this track was not present in the DOM, this |
| // might be because it's inside a collapsed group. |
| // Find the track node in the current workspace, and reveal it. |
| const trackNode = globals.workspace.getTrackByUri(trackUri); |
| if (!trackNode) return; |
| |
| if (openGroup) { |
| trackNode.reveal(); |
| globals.scrollToTrackUri = trackUri; |
| return; |
| } |
| // Find the first closed ancestor of our target track. |
| const groupNode = trackNode.closestVisibleAncestor; |
| if (groupNode) { |
| document |
| .querySelector('#track_' + groupNode.uri) |
| ?.scrollIntoView({behavior: 'smooth', block: 'nearest'}); |
| } |
| // If we get here, it means this track isn't in the workspace. |
| // TODO(stevegolton): Warn the user about this? |
| } |
| |
| // Scroll vertically and horizontally to reach track |track| at |ts|. |
| export function scrollToTrackAndTs( |
| trackUri: string, |
| ts: time, |
| openGroup = false, |
| ) { |
| verticalScrollToTrack(trackUri, openGroup); |
| horizontalScrollToTs(ts); |
| } |
| |
| // Scroll vertically and horizontally to a track and time range |
| export function scrollToTrackAndTimeSpan( |
| trackUri: string, |
| start: time, |
| end: time, |
| openGroup = false, |
| ) { |
| verticalScrollToTrack(trackUri, openGroup); |
| focusHorizontalRange(start, end); |
| } |
| |
| function focusHorizontalRangePercentage( |
| start: time, |
| end: time, |
| viewPercentage: number, |
| ): void { |
| const aoi = HighPrecisionTimeSpan.fromTime(start, end); |
| |
| if (viewPercentage <= 0.0 || viewPercentage > 1.0) { |
| console.warn( |
| 'Invalid value for [viewPercentage]. ' + |
| 'Value must be between 0.0 (exclusive) and 1.0 (inclusive).', |
| ); |
| // Default to 50%. |
| viewPercentage = 0.5; |
| } |
| const paddingPercentage = 1.0 - viewPercentage; |
| const halfPaddingTime = (aoi.duration * paddingPercentage) / 2; |
| globals.timeline.updateVisibleTimeHP(aoi.pad(halfPaddingTime)); |
| |
| raf.scheduleRedraw(); |
| } |
| |
| function focusHorizontalRangeImpl(start: time, end: time): void { |
| const visible = globals.timeline.visibleWindow; |
| const aoi = HighPrecisionTimeSpan.fromTime(start, end); |
| const fillRatio = 5; // Default amount to make the AOI fill the viewport |
| const padRatio = (fillRatio - 1) / 2; |
| |
| // If the area of interest already fills more than half the viewport, zoom out |
| // so that the AOI fills 20% of the viewport |
| if (aoi.duration * 2 > visible.duration) { |
| const padded = aoi.pad(aoi.duration * padRatio); |
| globals.timeline.updateVisibleTimeHP(padded); |
| } else { |
| // Center visible window on the middle of the AOI, preserving the zoom level |
| const newStart = aoi.midpoint.subNumber(visible.duration / 2); |
| |
| // Adjust the new visible window if it intersects with the trace boundaries. |
| // It's needed to make the "update the zoom level if visible window doesn't |
| // change" logic reliable. |
| const newVisibleWindow = new HighPrecisionTimeSpan( |
| newStart, |
| visible.duration, |
| ).fitWithin(globals.traceContext.start, globals.traceContext.end); |
| |
| // If preserving the zoom doesn't change the visible window, consider this |
| // to be the "second" hotkey press, so just make the AOI fill 20% of the |
| // viewport |
| if (newVisibleWindow.equals(visible)) { |
| const padded = aoi.pad(aoi.duration * padRatio); |
| globals.timeline.updateVisibleTimeHP(padded); |
| } else { |
| globals.timeline.updateVisibleTimeHP(newVisibleWindow); |
| } |
| } |
| |
| raf.scheduleRedraw(); |
| } |