blob: 38c5eb4164cd1e3dadcb35a8f6db123c7c36f9c9 [file] [log] [blame]
// 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 {Actions} from '../common/actions';
import {
HighPrecisionTime,
HighPrecisionTimeSpan,
} from '../common/high_precision_time';
import {getContainingTrackId} from '../common/state';
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 time = HighPrecisionTime.fromTime(ts);
const visibleWindow = globals.frontendLocalState.visibleWindowTime;
if (!visibleWindow.contains(time)) {
// TODO(hjd): This is an ugly jump, we should do a smooth pan instead.
const halfDuration = visibleWindow.duration.divide(2);
const newStart = time.sub(halfDuration);
const newWindow = new HighPrecisionTimeSpan(
newStart, newStart.add(visibleWindow.duration));
globals.frontendLocalState.updateVisibleTime(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) {
const visible = globals.frontendLocalState.visibleWindowTime;
const trace = globals.stateTraceTime();
const select = HighPrecisionTimeSpan.fromTime(start, end);
if (viewPercentage !== undefined) {
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 paddingTime = select.duration.multiply(paddingPercentage);
const halfPaddingTime = paddingTime.divide(2);
globals.frontendLocalState.updateVisibleTime(select.pad(halfPaddingTime));
return;
}
// If the range is too large to fit on the current zoom level, resize.
if (select.duration.gt(visible.duration.multiply(0.5))) {
const paddedRange = select.pad(select.duration.multiply(2));
globals.frontendLocalState.updateVisibleTime(paddedRange);
return;
}
// Calculate the new visible window preserving the zoom level.
let newStart = select.midpoint.sub(visible.duration.divide(2));
let newEnd = select.midpoint.add(visible.duration.divide(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.
if (newEnd.gt(trace.end)) {
newStart = trace.end.sub(visible.duration);
newEnd = trace.end;
}
if (newStart.lt(trace.start)) {
newStart = trace.start;
newEnd = trace.start.add(visible.duration);
}
const view = new HighPrecisionTimeSpan(newStart, newEnd);
// If preserving the zoom doesn't change the visible window, update the zoom
// level.
if (view.start.eq(visible.start) && view.end.eq(visible.end)) {
const padded = select.pad(select.duration.multiply(2));
globals.frontendLocalState.updateVisibleTime(padded);
} else {
globals.frontendLocalState.updateVisibleTime(view);
}
}
// 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(
trackId: string|number, openGroup = false) {
const trackIdString = `${trackId}`;
const track = document.querySelector('#track_' + trackIdString);
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;
}
let trackGroup = null;
const trackGroupId = getContainingTrackId(globals.state, trackIdString);
if (trackGroupId) {
trackGroup = document.querySelector('#track_' + trackGroupId);
}
if (!trackGroupId || !trackGroup) {
console.error(`Can't scroll, track (${trackIdString}) not found.`);
return;
}
// The requested track is inside a closed track group, either open the track
// group and scroll to the track or just scroll to the track group.
if (openGroup) {
// After the track exists in the dom, it will be scrolled to.
globals.frontendLocalState.scrollToTrackId = trackId;
globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
return;
} else {
trackGroup.scrollIntoView({behavior: 'smooth', block: 'nearest'});
}
}
// Scroll vertically and horizontally to reach track (|trackId|) at |ts|.
export function scrollToTrackAndTs(
trackId: string|number|undefined, ts: time, openGroup = false) {
if (trackId !== undefined) {
verticalScrollToTrack(trackId, openGroup);
}
horizontalScrollToTs(ts);
}
// Scroll vertically and horizontally to a track and time range
export function reveal(
trackId: string|number, start: time, end: time, openGroup = false) {
verticalScrollToTrack(trackId, openGroup);
focusHorizontalRange(start, end);
}