// 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 || time.start === time.end) { 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.getTrackByUri(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'}); } } }