UNPKG

chrome-devtools-frontend

Version:
194 lines (171 loc) 7.65 kB
// Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Trace from '../../models/trace/trace.js'; let instance: BoundsManager|null = null; export class StateChangedEvent extends Event { static readonly eventName = 'traceboundsstatechanged'; constructor( public state: Readonly<State>, public updateType: 'RESET'|'MINIMAP_BOUNDS'|'VISIBLE_WINDOW', public options: { shouldAnimate?: boolean, } = {shouldAnimate: false}, ) { super(StateChangedEvent.eventName, {composed: true, bubbles: true}); } } // Exposed as a shortcut to BoundsManager.instance().addEventListener, which // also takes care of type-casting the event to StateChangedEvent. export function onChange(cb: (event: StateChangedEvent) => void): void { BoundsManager.instance().addEventListener( StateChangedEvent.eventName, // Cast the callback as TS doesn't know that these events will emit // StateChangedEvent types. cb as (event: Event) => void); } export function removeListener(cb: (event: StateChangedEvent) => void): void { BoundsManager.instance().removeEventListener(StateChangedEvent.eventName, cb as (event: Event) => void); } export interface State { readonly micro: Readonly<TraceWindows<Trace.Types.Timing.Micro>>; readonly milli: Readonly<TraceWindows<Trace.Types.Timing.Milli>>; } export interface TraceWindows<TimeFormat extends Trace.Types.Timing.Micro|Trace.Types.Timing.Milli> { /** * This is the bounds of the entire trace. Once a trace is imported/recorded * and this is set, it cannot be changed. */ readonly entireTraceBounds: Trace.Types.Timing.TraceWindow<TimeFormat>; /** * This is the bounds of the minimap and represents the left and right bound * being shown by the minimap. It can be changed by a user action: for * example, when a user creates a breadcrumb, that breadcrumb becomes the * minimap trace bounds. By default, and when a trace is first loaded, the * minimapTraceBounds are equivalent to the entireTraceBounds. * Note that this is NOT the active time window that the user has dragged * the minimap handles to; this is the min/max being shown by the minimap. */ minimapTraceBounds: Trace.Types.Timing.TraceWindow<TimeFormat>; /** * This represents the trace window that is being shown on the main timeline. * The reason this is called a "Window" rather than "Bounds" is because the * user is not bound by this value - they can use their mouse to pan/zoom * in/out beyond the limits of this window (the limit is the * minimapTraceBounds). Another way to think of this value is that the * min/max of this value is what is represented by the two drag handles on * the TimelineMiniMap that the user can drag to change their current window. */ timelineTraceWindow: Trace.Types.Timing.TraceWindow<TimeFormat>; } export class BoundsManager extends EventTarget { static instance(opts: { forceNew: boolean|null, } = {forceNew: null}): BoundsManager { const forceNew = Boolean(opts.forceNew); if (!instance || forceNew) { instance = new BoundsManager(); } return instance; } static removeInstance(): void { instance = null; } #currentState: TraceWindows<Trace.Types.Timing.Micro>|null = null; private constructor() { // Defined to enable us to mark it as Private. super(); } resetWithNewBounds(initialBounds: Trace.Types.Timing.TraceWindowMicro): this { this.#currentState = { entireTraceBounds: initialBounds, minimapTraceBounds: initialBounds, timelineTraceWindow: initialBounds, }; this.dispatchEvent(new StateChangedEvent(this.state() as State, 'RESET')); return this; } state(): Readonly<State>|null { if (this.#currentState === null) { return null; } const entireBoundsMilli = Trace.Helpers.Timing.traceWindowMilliSeconds(this.#currentState.entireTraceBounds); const minimapBoundsMilli = Trace.Helpers.Timing.traceWindowMilliSeconds(this.#currentState.minimapTraceBounds); const timelineTraceWindowMilli = Trace.Helpers.Timing.traceWindowMilliSeconds(this.#currentState.timelineTraceWindow); return { micro: this.#currentState, milli: { entireTraceBounds: entireBoundsMilli, minimapTraceBounds: minimapBoundsMilli, timelineTraceWindow: timelineTraceWindowMilli, }, }; } setMiniMapBounds(newBounds: Trace.Types.Timing.TraceWindowMicro): void { if (!this.#currentState) { // If we don't have the existing state and know the trace bounds, we // cannot set the minimap bounds. console.error('TraceBounds.setMiniMapBounds could not set bounds because there is no existing trace window set.'); return; } const existingBounds = this.#currentState.minimapTraceBounds; if (newBounds.min === existingBounds.min && newBounds.max === existingBounds.max) { // New bounds are identical to the old ones so no action required. return; } if (newBounds.range < 1_000) { // Minimum minimap bounds range is 1 millisecond. return; } this.#currentState.minimapTraceBounds = newBounds; // this.state() cannot be null here. this.dispatchEvent(new StateChangedEvent(this.state() as State, 'MINIMAP_BOUNDS')); } /** * Updates the visible part of the trace that the user can see. * @param options.ignoreMiniMapBounds - by default the visible window will be * bound by the minimap bounds. If you set this to `true` then the timeline * visible window will not be constrained by the minimap bounds. Be careful * with this! Unless you deal with this situation, the UI of the performance * panel will break. */ setTimelineVisibleWindow(newWindow: Trace.Types.Timing.TraceWindowMicro, options: { shouldAnimate?: boolean, ignoreMiniMapBounds?: boolean, } = { shouldAnimate: false, ignoreMiniMapBounds: false, }): void { if (!this.#currentState) { // This is a weird state to be in: we can't change the visible timeline // window if we don't already have an existing state with the trace // bounds set. console.error( 'TraceBounds.setTimelineVisibleWindow could not set bounds because there is no existing trace window set.'); return; } const existingWindow = this.#currentState.timelineTraceWindow; if (newWindow.range < 1_000) { // Minimum timeline visible window range is 1 millisecond. return; } if (newWindow.min === existingWindow.min && newWindow.max === existingWindow.max) { // New bounds are identical to the old ones so no action required. return; } if (!options.ignoreMiniMapBounds) { // Ensure that the setTimelineVisibleWindow can never go outside the bounds of the minimap bounds. newWindow.min = Trace.Types.Timing.Micro(Math.max(this.#currentState.minimapTraceBounds.min, newWindow.min)); newWindow.max = Trace.Types.Timing.Micro(Math.min(this.#currentState.minimapTraceBounds.max, newWindow.max)); } if (newWindow.min === existingWindow.min && newWindow.max === existingWindow.max) { // If, after we adjust for the minimap bounds, the new window matches the // old one, we can exit as no action is required. return; } this.#currentState.timelineTraceWindow = newWindow; this.dispatchEvent( new StateChangedEvent(this.state() as State, 'VISIBLE_WINDOW', {shouldAnimate: options.shouldAnimate})); } }