blob: c732b91ccda7033b8dff36b3c1fa6398eb7fb6dd [file] [log] [blame]
// Copyright (C) 2024 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 {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
import {time} from '../base/time';
import {ScrollToArgs} from '../public/scroll_helper';
import {TraceInfo} from '../public/trace_info';
import {Workspace} from '../public/workspace';
import {raf} from './raf_scheduler';
import {TimelineImpl} from './timeline';
import {TrackManagerImpl} from './track_manager';
// A helper class to help jumping to tracks and time ranges.
// This class must NOT alter in any way the selection status. That
// responsibility belongs to SelectionManager (which uses this).
export class ScrollHelper {
constructor(
private traceInfo: TraceInfo,
private timeline: TimelineImpl,
private workspace: Workspace,
private trackManager: TrackManagerImpl,
) {}
// See comments in ScrollToArgs for the intended semantics.
scrollTo(args: ScrollToArgs) {
const {time, track} = args;
raf.scheduleCanvasRedraw();
if (time !== undefined) {
if (time.end === undefined) {
this.timeline.panToTimestamp(time.start);
} else if (time.viewPercentage !== undefined) {
this.focusHorizontalRangePercentage(
time.start,
time.end,
time.viewPercentage,
);
} else {
this.focusHorizontalRange(time.start, time.end);
}
}
if (track !== undefined) {
this.verticalScrollToTrack(track.uri, track.expandGroup ?? false);
}
}
private 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;
this.timeline.updateVisibleTimeHP(aoi.pad(halfPaddingTime));
}
private focusHorizontalRange(start: time, end: time): void {
const visible = this.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);
this.timeline.updateVisibleTimeHP(padded);
} else {
// Center visible window on the middle of the AOI, preserving 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(this.traceInfo.start, this.traceInfo.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);
this.timeline.updateVisibleTimeHP(padded);
} else {
this.timeline.updateVisibleTimeHP(newVisibleWindow);
}
}
}
private verticalScrollToTrack(trackUri: string, openGroup: boolean) {
// Find the actual track node that uses that URI, we need various properties
// from it.
const trackNode = this.workspace.findTrackByUri(trackUri);
if (!trackNode) return;
// Try finding the track directly.
const element = document.getElementById(trackNode.id);
if (element) {
// block: 'nearest' means that it will only scroll if the track is not
// currently in view.
element.scrollIntoView({behavior: 'smooth', block: 'nearest'});
return;
}
// If we get here, the element for this track was not present in the DOM,
// which might be because it's inside a collapsed group.
if (openGroup) {
// Try to reveal the track node in the workspace by opening up all
// ancestor groups, and mark the track URI to be scrolled to in the
// future.
trackNode.reveal();
this.trackManager.scrollToTrackNodeId = trackNode.id;
} else {
// Find the closest visible ancestor of our target track and scroll to
// that instead.
const container = trackNode.findClosestVisibleAncestor();
document
.getElementById(container.id)
?.scrollIntoView({behavior: 'smooth', block: 'nearest'});
}
}
}