xref: /aosp_15_r20/external/perfetto/ui/src/frontend/viewer_page.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2018 The Android Open Source Project
2*6dbdd20aSAndroid Build Coastguard Worker//
3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License");
4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License.
5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at
6*6dbdd20aSAndroid Build Coastguard Worker//
7*6dbdd20aSAndroid Build Coastguard Worker//      http://www.apache.org/licenses/LICENSE-2.0
8*6dbdd20aSAndroid Build Coastguard Worker//
9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software
10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS,
11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and
13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License.
14*6dbdd20aSAndroid Build Coastguard Worker
15*6dbdd20aSAndroid Build Coastguard Workerimport {hex} from 'color-convert';
16*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril';
17*6dbdd20aSAndroid Build Coastguard Workerimport {removeFalsyValues} from '../base/array_utils';
18*6dbdd20aSAndroid Build Coastguard Workerimport {canvasClip, canvasSave} from '../base/canvas_utils';
19*6dbdd20aSAndroid Build Coastguard Workerimport {findRef, toHTMLElement} from '../base/dom_utils';
20*6dbdd20aSAndroid Build Coastguard Workerimport {Size2D, VerticalBounds} from '../base/geom';
21*6dbdd20aSAndroid Build Coastguard Workerimport {assertExists} from '../base/logging';
22*6dbdd20aSAndroid Build Coastguard Workerimport {clamp} from '../base/math_utils';
23*6dbdd20aSAndroid Build Coastguard Workerimport {Time, TimeSpan} from '../base/time';
24*6dbdd20aSAndroid Build Coastguard Workerimport {TimeScale} from '../base/time_scale';
25*6dbdd20aSAndroid Build Coastguard Workerimport {featureFlags} from '../core/feature_flags';
26*6dbdd20aSAndroid Build Coastguard Workerimport {raf} from '../core/raf_scheduler';
27*6dbdd20aSAndroid Build Coastguard Workerimport {TrackNode} from '../public/workspace';
28*6dbdd20aSAndroid Build Coastguard Workerimport {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
29*6dbdd20aSAndroid Build Coastguard Workerimport {renderFlows} from './flow_events_renderer';
30*6dbdd20aSAndroid Build Coastguard Workerimport {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
31*6dbdd20aSAndroid Build Coastguard Workerimport {NotesPanel} from './notes_panel';
32*6dbdd20aSAndroid Build Coastguard Workerimport {OverviewTimelinePanel} from './overview_timeline_panel';
33*6dbdd20aSAndroid Build Coastguard Workerimport {PanAndZoomHandler} from './pan_and_zoom_handler';
34*6dbdd20aSAndroid Build Coastguard Workerimport {
35*6dbdd20aSAndroid Build Coastguard Worker  PanelContainer,
36*6dbdd20aSAndroid Build Coastguard Worker  PanelOrGroup,
37*6dbdd20aSAndroid Build Coastguard Worker  RenderedPanelInfo,
38*6dbdd20aSAndroid Build Coastguard Worker} from './panel_container';
39*6dbdd20aSAndroid Build Coastguard Workerimport {TabPanel} from './tab_panel';
40*6dbdd20aSAndroid Build Coastguard Workerimport {TickmarkPanel} from './tickmark_panel';
41*6dbdd20aSAndroid Build Coastguard Workerimport {TimeAxisPanel} from './time_axis_panel';
42*6dbdd20aSAndroid Build Coastguard Workerimport {TimeSelectionPanel} from './time_selection_panel';
43*6dbdd20aSAndroid Build Coastguard Workerimport {TrackPanel} from './track_panel';
44*6dbdd20aSAndroid Build Coastguard Workerimport {drawVerticalLineAtTime} from './vertical_line_helper';
45*6dbdd20aSAndroid Build Coastguard Workerimport {TraceImpl} from '../core/trace_impl';
46*6dbdd20aSAndroid Build Coastguard Workerimport {PageWithTraceImplAttrs} from '../core/page_manager';
47*6dbdd20aSAndroid Build Coastguard Workerimport {AppImpl} from '../core/app_impl';
48*6dbdd20aSAndroid Build Coastguard Worker
49*6dbdd20aSAndroid Build Coastguard Workerconst OVERVIEW_PANEL_FLAG = featureFlags.register({
50*6dbdd20aSAndroid Build Coastguard Worker  id: 'overviewVisible',
51*6dbdd20aSAndroid Build Coastguard Worker  name: 'Overview Panel',
52*6dbdd20aSAndroid Build Coastguard Worker  description: 'Show the panel providing an overview of the trace',
53*6dbdd20aSAndroid Build Coastguard Worker  defaultValue: true,
54*6dbdd20aSAndroid Build Coastguard Worker});
55*6dbdd20aSAndroid Build Coastguard Worker
56*6dbdd20aSAndroid Build Coastguard Worker// Checks if the mousePos is within 3px of the start or end of the
57*6dbdd20aSAndroid Build Coastguard Worker// current selected time range.
58*6dbdd20aSAndroid Build Coastguard Workerfunction onTimeRangeBoundary(
59*6dbdd20aSAndroid Build Coastguard Worker  trace: TraceImpl,
60*6dbdd20aSAndroid Build Coastguard Worker  timescale: TimeScale,
61*6dbdd20aSAndroid Build Coastguard Worker  mousePos: number,
62*6dbdd20aSAndroid Build Coastguard Worker): 'START' | 'END' | null {
63*6dbdd20aSAndroid Build Coastguard Worker  const selection = trace.selection.selection;
64*6dbdd20aSAndroid Build Coastguard Worker  if (selection.kind === 'area') {
65*6dbdd20aSAndroid Build Coastguard Worker    // If frontend selectedArea exists then we are in the process of editing the
66*6dbdd20aSAndroid Build Coastguard Worker    // time range and need to use that value instead.
67*6dbdd20aSAndroid Build Coastguard Worker    const area = trace.timeline.selectedArea
68*6dbdd20aSAndroid Build Coastguard Worker      ? trace.timeline.selectedArea
69*6dbdd20aSAndroid Build Coastguard Worker      : selection;
70*6dbdd20aSAndroid Build Coastguard Worker    const start = timescale.timeToPx(area.start);
71*6dbdd20aSAndroid Build Coastguard Worker    const end = timescale.timeToPx(area.end);
72*6dbdd20aSAndroid Build Coastguard Worker    const startDrag = mousePos - TRACK_SHELL_WIDTH;
73*6dbdd20aSAndroid Build Coastguard Worker    const startDistance = Math.abs(start - startDrag);
74*6dbdd20aSAndroid Build Coastguard Worker    const endDistance = Math.abs(end - startDrag);
75*6dbdd20aSAndroid Build Coastguard Worker    const range = 3 * window.devicePixelRatio;
76*6dbdd20aSAndroid Build Coastguard Worker    // We might be within 3px of both boundaries but we should choose
77*6dbdd20aSAndroid Build Coastguard Worker    // the closest one.
78*6dbdd20aSAndroid Build Coastguard Worker    if (startDistance < range && startDistance <= endDistance) return 'START';
79*6dbdd20aSAndroid Build Coastguard Worker    if (endDistance < range && endDistance <= startDistance) return 'END';
80*6dbdd20aSAndroid Build Coastguard Worker  }
81*6dbdd20aSAndroid Build Coastguard Worker  return null;
82*6dbdd20aSAndroid Build Coastguard Worker}
83*6dbdd20aSAndroid Build Coastguard Worker
84*6dbdd20aSAndroid Build Coastguard Workerinterface SelectedContainer {
85*6dbdd20aSAndroid Build Coastguard Worker  readonly containerClass: string;
86*6dbdd20aSAndroid Build Coastguard Worker  readonly dragStartAbsY: number;
87*6dbdd20aSAndroid Build Coastguard Worker  readonly dragEndAbsY: number;
88*6dbdd20aSAndroid Build Coastguard Worker}
89*6dbdd20aSAndroid Build Coastguard Worker
90*6dbdd20aSAndroid Build Coastguard Worker/**
91*6dbdd20aSAndroid Build Coastguard Worker * Top-most level component for the viewer page. Holds tracks, brush timeline,
92*6dbdd20aSAndroid Build Coastguard Worker * panels, and everything else that's part of the main trace viewer page.
93*6dbdd20aSAndroid Build Coastguard Worker */
94*6dbdd20aSAndroid Build Coastguard Workerexport class ViewerPage implements m.ClassComponent<PageWithTraceImplAttrs> {
95*6dbdd20aSAndroid Build Coastguard Worker  private zoomContent?: PanAndZoomHandler;
96*6dbdd20aSAndroid Build Coastguard Worker  // Used to prevent global deselection if a pan/drag select occurred.
97*6dbdd20aSAndroid Build Coastguard Worker  private keepCurrentSelection = false;
98*6dbdd20aSAndroid Build Coastguard Worker
99*6dbdd20aSAndroid Build Coastguard Worker  private overviewTimelinePanel: OverviewTimelinePanel;
100*6dbdd20aSAndroid Build Coastguard Worker  private timeAxisPanel: TimeAxisPanel;
101*6dbdd20aSAndroid Build Coastguard Worker  private timeSelectionPanel: TimeSelectionPanel;
102*6dbdd20aSAndroid Build Coastguard Worker  private notesPanel: NotesPanel;
103*6dbdd20aSAndroid Build Coastguard Worker  private tickmarkPanel: TickmarkPanel;
104*6dbdd20aSAndroid Build Coastguard Worker  private timelineWidthPx?: number;
105*6dbdd20aSAndroid Build Coastguard Worker  private selectedContainer?: SelectedContainer;
106*6dbdd20aSAndroid Build Coastguard Worker  private showPanningHint = false;
107*6dbdd20aSAndroid Build Coastguard Worker
108*6dbdd20aSAndroid Build Coastguard Worker  private readonly PAN_ZOOM_CONTENT_REF = 'pan-and-zoom-content';
109*6dbdd20aSAndroid Build Coastguard Worker
110*6dbdd20aSAndroid Build Coastguard Worker  constructor(vnode: m.CVnode<PageWithTraceImplAttrs>) {
111*6dbdd20aSAndroid Build Coastguard Worker    this.notesPanel = new NotesPanel(vnode.attrs.trace);
112*6dbdd20aSAndroid Build Coastguard Worker    this.timeAxisPanel = new TimeAxisPanel(vnode.attrs.trace);
113*6dbdd20aSAndroid Build Coastguard Worker    this.timeSelectionPanel = new TimeSelectionPanel(vnode.attrs.trace);
114*6dbdd20aSAndroid Build Coastguard Worker    this.tickmarkPanel = new TickmarkPanel(vnode.attrs.trace);
115*6dbdd20aSAndroid Build Coastguard Worker    this.overviewTimelinePanel = new OverviewTimelinePanel(vnode.attrs.trace);
116*6dbdd20aSAndroid Build Coastguard Worker    this.notesPanel = new NotesPanel(vnode.attrs.trace);
117*6dbdd20aSAndroid Build Coastguard Worker    this.timeSelectionPanel = new TimeSelectionPanel(vnode.attrs.trace);
118*6dbdd20aSAndroid Build Coastguard Worker  }
119*6dbdd20aSAndroid Build Coastguard Worker
120*6dbdd20aSAndroid Build Coastguard Worker  oncreate({dom, attrs}: m.CVnodeDOM<PageWithTraceImplAttrs>) {
121*6dbdd20aSAndroid Build Coastguard Worker    const panZoomElRaw = findRef(dom, this.PAN_ZOOM_CONTENT_REF);
122*6dbdd20aSAndroid Build Coastguard Worker    const panZoomEl = toHTMLElement(assertExists(panZoomElRaw));
123*6dbdd20aSAndroid Build Coastguard Worker
124*6dbdd20aSAndroid Build Coastguard Worker    const {top: panTop} = panZoomEl.getBoundingClientRect();
125*6dbdd20aSAndroid Build Coastguard Worker    this.zoomContent = new PanAndZoomHandler({
126*6dbdd20aSAndroid Build Coastguard Worker      element: panZoomEl,
127*6dbdd20aSAndroid Build Coastguard Worker      onPanned: (pannedPx: number) => {
128*6dbdd20aSAndroid Build Coastguard Worker        const timeline = attrs.trace.timeline;
129*6dbdd20aSAndroid Build Coastguard Worker
130*6dbdd20aSAndroid Build Coastguard Worker        if (this.timelineWidthPx === undefined) return;
131*6dbdd20aSAndroid Build Coastguard Worker
132*6dbdd20aSAndroid Build Coastguard Worker        this.keepCurrentSelection = true;
133*6dbdd20aSAndroid Build Coastguard Worker        const timescale = new TimeScale(timeline.visibleWindow, {
134*6dbdd20aSAndroid Build Coastguard Worker          left: 0,
135*6dbdd20aSAndroid Build Coastguard Worker          right: this.timelineWidthPx,
136*6dbdd20aSAndroid Build Coastguard Worker        });
137*6dbdd20aSAndroid Build Coastguard Worker        const tDelta = timescale.pxToDuration(pannedPx);
138*6dbdd20aSAndroid Build Coastguard Worker        timeline.panVisibleWindow(tDelta);
139*6dbdd20aSAndroid Build Coastguard Worker      },
140*6dbdd20aSAndroid Build Coastguard Worker      onZoomed: (zoomedPositionPx: number, zoomRatio: number) => {
141*6dbdd20aSAndroid Build Coastguard Worker        const timeline = attrs.trace.timeline;
142*6dbdd20aSAndroid Build Coastguard Worker        // TODO(hjd): Avoid hardcoding TRACK_SHELL_WIDTH.
143*6dbdd20aSAndroid Build Coastguard Worker        // TODO(hjd): Improve support for zooming in overview timeline.
144*6dbdd20aSAndroid Build Coastguard Worker        const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH;
145*6dbdd20aSAndroid Build Coastguard Worker        const rect = dom.getBoundingClientRect();
146*6dbdd20aSAndroid Build Coastguard Worker        const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH);
147*6dbdd20aSAndroid Build Coastguard Worker        timeline.zoomVisibleWindow(1 - zoomRatio, centerPoint);
148*6dbdd20aSAndroid Build Coastguard Worker        raf.scheduleCanvasRedraw();
149*6dbdd20aSAndroid Build Coastguard Worker      },
150*6dbdd20aSAndroid Build Coastguard Worker      editSelection: (currentPx: number) => {
151*6dbdd20aSAndroid Build Coastguard Worker        if (this.timelineWidthPx === undefined) return false;
152*6dbdd20aSAndroid Build Coastguard Worker        const timescale = new TimeScale(attrs.trace.timeline.visibleWindow, {
153*6dbdd20aSAndroid Build Coastguard Worker          left: 0,
154*6dbdd20aSAndroid Build Coastguard Worker          right: this.timelineWidthPx,
155*6dbdd20aSAndroid Build Coastguard Worker        });
156*6dbdd20aSAndroid Build Coastguard Worker        return onTimeRangeBoundary(attrs.trace, timescale, currentPx) !== null;
157*6dbdd20aSAndroid Build Coastguard Worker      },
158*6dbdd20aSAndroid Build Coastguard Worker      onSelection: (
159*6dbdd20aSAndroid Build Coastguard Worker        dragStartX: number,
160*6dbdd20aSAndroid Build Coastguard Worker        dragStartY: number,
161*6dbdd20aSAndroid Build Coastguard Worker        prevX: number,
162*6dbdd20aSAndroid Build Coastguard Worker        currentX: number,
163*6dbdd20aSAndroid Build Coastguard Worker        currentY: number,
164*6dbdd20aSAndroid Build Coastguard Worker        editing: boolean,
165*6dbdd20aSAndroid Build Coastguard Worker      ) => {
166*6dbdd20aSAndroid Build Coastguard Worker        const traceTime = attrs.trace.traceInfo;
167*6dbdd20aSAndroid Build Coastguard Worker        const timeline = attrs.trace.timeline;
168*6dbdd20aSAndroid Build Coastguard Worker
169*6dbdd20aSAndroid Build Coastguard Worker        if (this.timelineWidthPx === undefined) return;
170*6dbdd20aSAndroid Build Coastguard Worker
171*6dbdd20aSAndroid Build Coastguard Worker        // TODO(stevegolton): Don't get the windowSpan from globals, get it from
172*6dbdd20aSAndroid Build Coastguard Worker        // here!
173*6dbdd20aSAndroid Build Coastguard Worker        const {visibleWindow} = timeline;
174*6dbdd20aSAndroid Build Coastguard Worker        const timespan = visibleWindow.toTimeSpan();
175*6dbdd20aSAndroid Build Coastguard Worker        this.keepCurrentSelection = true;
176*6dbdd20aSAndroid Build Coastguard Worker
177*6dbdd20aSAndroid Build Coastguard Worker        const timescale = new TimeScale(timeline.visibleWindow, {
178*6dbdd20aSAndroid Build Coastguard Worker          left: 0,
179*6dbdd20aSAndroid Build Coastguard Worker          right: this.timelineWidthPx,
180*6dbdd20aSAndroid Build Coastguard Worker        });
181*6dbdd20aSAndroid Build Coastguard Worker
182*6dbdd20aSAndroid Build Coastguard Worker        if (editing) {
183*6dbdd20aSAndroid Build Coastguard Worker          const selection = attrs.trace.selection.selection;
184*6dbdd20aSAndroid Build Coastguard Worker          if (selection.kind === 'area') {
185*6dbdd20aSAndroid Build Coastguard Worker            const area = attrs.trace.timeline.selectedArea
186*6dbdd20aSAndroid Build Coastguard Worker              ? attrs.trace.timeline.selectedArea
187*6dbdd20aSAndroid Build Coastguard Worker              : selection;
188*6dbdd20aSAndroid Build Coastguard Worker            let newTime = timescale
189*6dbdd20aSAndroid Build Coastguard Worker              .pxToHpTime(currentX - TRACK_SHELL_WIDTH)
190*6dbdd20aSAndroid Build Coastguard Worker              .toTime();
191*6dbdd20aSAndroid Build Coastguard Worker            // Have to check again for when one boundary crosses over the other.
192*6dbdd20aSAndroid Build Coastguard Worker            const curBoundary = onTimeRangeBoundary(
193*6dbdd20aSAndroid Build Coastguard Worker              attrs.trace,
194*6dbdd20aSAndroid Build Coastguard Worker              timescale,
195*6dbdd20aSAndroid Build Coastguard Worker              prevX,
196*6dbdd20aSAndroid Build Coastguard Worker            );
197*6dbdd20aSAndroid Build Coastguard Worker            if (curBoundary == null) return;
198*6dbdd20aSAndroid Build Coastguard Worker            const keepTime = curBoundary === 'START' ? area.end : area.start;
199*6dbdd20aSAndroid Build Coastguard Worker            // Don't drag selection outside of current screen.
200*6dbdd20aSAndroid Build Coastguard Worker            if (newTime < keepTime) {
201*6dbdd20aSAndroid Build Coastguard Worker              newTime = Time.max(newTime, timespan.start);
202*6dbdd20aSAndroid Build Coastguard Worker            } else {
203*6dbdd20aSAndroid Build Coastguard Worker              newTime = Time.min(newTime, timespan.end);
204*6dbdd20aSAndroid Build Coastguard Worker            }
205*6dbdd20aSAndroid Build Coastguard Worker            // When editing the time range we always use the saved tracks,
206*6dbdd20aSAndroid Build Coastguard Worker            // since these will not change.
207*6dbdd20aSAndroid Build Coastguard Worker            timeline.selectArea(
208*6dbdd20aSAndroid Build Coastguard Worker              Time.max(Time.min(keepTime, newTime), traceTime.start),
209*6dbdd20aSAndroid Build Coastguard Worker              Time.min(Time.max(keepTime, newTime), traceTime.end),
210*6dbdd20aSAndroid Build Coastguard Worker              selection.trackUris,
211*6dbdd20aSAndroid Build Coastguard Worker            );
212*6dbdd20aSAndroid Build Coastguard Worker          }
213*6dbdd20aSAndroid Build Coastguard Worker        } else {
214*6dbdd20aSAndroid Build Coastguard Worker          let startPx = Math.min(dragStartX, currentX) - TRACK_SHELL_WIDTH;
215*6dbdd20aSAndroid Build Coastguard Worker          let endPx = Math.max(dragStartX, currentX) - TRACK_SHELL_WIDTH;
216*6dbdd20aSAndroid Build Coastguard Worker          if (startPx < 0 && endPx < 0) return;
217*6dbdd20aSAndroid Build Coastguard Worker          startPx = clamp(startPx, 0, this.timelineWidthPx);
218*6dbdd20aSAndroid Build Coastguard Worker          endPx = clamp(endPx, 0, this.timelineWidthPx);
219*6dbdd20aSAndroid Build Coastguard Worker          timeline.selectArea(
220*6dbdd20aSAndroid Build Coastguard Worker            timescale.pxToHpTime(startPx).toTime('floor'),
221*6dbdd20aSAndroid Build Coastguard Worker            timescale.pxToHpTime(endPx).toTime('ceil'),
222*6dbdd20aSAndroid Build Coastguard Worker          );
223*6dbdd20aSAndroid Build Coastguard Worker
224*6dbdd20aSAndroid Build Coastguard Worker          const absStartY = dragStartY + panTop;
225*6dbdd20aSAndroid Build Coastguard Worker          const absCurrentY = currentY + panTop;
226*6dbdd20aSAndroid Build Coastguard Worker          if (this.selectedContainer === undefined) {
227*6dbdd20aSAndroid Build Coastguard Worker            for (const c of dom.querySelectorAll('.pf-panel-container')) {
228*6dbdd20aSAndroid Build Coastguard Worker              const {top, bottom} = c.getBoundingClientRect();
229*6dbdd20aSAndroid Build Coastguard Worker              if (top <= absStartY && absCurrentY <= bottom) {
230*6dbdd20aSAndroid Build Coastguard Worker                const stack = assertExists(c.querySelector('.pf-panel-stack'));
231*6dbdd20aSAndroid Build Coastguard Worker                const stackTop = stack.getBoundingClientRect().top;
232*6dbdd20aSAndroid Build Coastguard Worker                this.selectedContainer = {
233*6dbdd20aSAndroid Build Coastguard Worker                  containerClass: Array.from(c.classList).filter(
234*6dbdd20aSAndroid Build Coastguard Worker                    (x) => x !== 'pf-panel-container',
235*6dbdd20aSAndroid Build Coastguard Worker                  )[0],
236*6dbdd20aSAndroid Build Coastguard Worker                  dragStartAbsY: -stackTop + absStartY,
237*6dbdd20aSAndroid Build Coastguard Worker                  dragEndAbsY: -stackTop + absCurrentY,
238*6dbdd20aSAndroid Build Coastguard Worker                };
239*6dbdd20aSAndroid Build Coastguard Worker                break;
240*6dbdd20aSAndroid Build Coastguard Worker              }
241*6dbdd20aSAndroid Build Coastguard Worker            }
242*6dbdd20aSAndroid Build Coastguard Worker          } else {
243*6dbdd20aSAndroid Build Coastguard Worker            const c = assertExists(
244*6dbdd20aSAndroid Build Coastguard Worker              dom.querySelector(`.${this.selectedContainer.containerClass}`),
245*6dbdd20aSAndroid Build Coastguard Worker            );
246*6dbdd20aSAndroid Build Coastguard Worker            const {top, bottom} = c.getBoundingClientRect();
247*6dbdd20aSAndroid Build Coastguard Worker            const boundedCurrentY = Math.min(
248*6dbdd20aSAndroid Build Coastguard Worker              Math.max(top, absCurrentY),
249*6dbdd20aSAndroid Build Coastguard Worker              bottom,
250*6dbdd20aSAndroid Build Coastguard Worker            );
251*6dbdd20aSAndroid Build Coastguard Worker            const stack = assertExists(c.querySelector('.pf-panel-stack'));
252*6dbdd20aSAndroid Build Coastguard Worker            const stackTop = stack.getBoundingClientRect().top;
253*6dbdd20aSAndroid Build Coastguard Worker            this.selectedContainer = {
254*6dbdd20aSAndroid Build Coastguard Worker              ...this.selectedContainer,
255*6dbdd20aSAndroid Build Coastguard Worker              dragEndAbsY: -stackTop + boundedCurrentY,
256*6dbdd20aSAndroid Build Coastguard Worker            };
257*6dbdd20aSAndroid Build Coastguard Worker          }
258*6dbdd20aSAndroid Build Coastguard Worker          this.showPanningHint = true;
259*6dbdd20aSAndroid Build Coastguard Worker        }
260*6dbdd20aSAndroid Build Coastguard Worker        raf.scheduleCanvasRedraw();
261*6dbdd20aSAndroid Build Coastguard Worker      },
262*6dbdd20aSAndroid Build Coastguard Worker      endSelection: (edit: boolean) => {
263*6dbdd20aSAndroid Build Coastguard Worker        this.selectedContainer = undefined;
264*6dbdd20aSAndroid Build Coastguard Worker        const area = attrs.trace.timeline.selectedArea;
265*6dbdd20aSAndroid Build Coastguard Worker        // If we are editing we need to pass the current id through to ensure
266*6dbdd20aSAndroid Build Coastguard Worker        // the marked area with that id is also updated.
267*6dbdd20aSAndroid Build Coastguard Worker        if (edit) {
268*6dbdd20aSAndroid Build Coastguard Worker          const selection = attrs.trace.selection.selection;
269*6dbdd20aSAndroid Build Coastguard Worker          if (selection.kind === 'area' && area) {
270*6dbdd20aSAndroid Build Coastguard Worker            attrs.trace.selection.selectArea({...area});
271*6dbdd20aSAndroid Build Coastguard Worker          }
272*6dbdd20aSAndroid Build Coastguard Worker        } else if (area) {
273*6dbdd20aSAndroid Build Coastguard Worker          attrs.trace.selection.selectArea({...area});
274*6dbdd20aSAndroid Build Coastguard Worker        }
275*6dbdd20aSAndroid Build Coastguard Worker        // Now the selection has ended we stored the final selected area in the
276*6dbdd20aSAndroid Build Coastguard Worker        // global state and can remove the in progress selection from the
277*6dbdd20aSAndroid Build Coastguard Worker        // timeline.
278*6dbdd20aSAndroid Build Coastguard Worker        attrs.trace.timeline.deselectArea();
279*6dbdd20aSAndroid Build Coastguard Worker        // Full redraw to color track shell.
280*6dbdd20aSAndroid Build Coastguard Worker        raf.scheduleFullRedraw();
281*6dbdd20aSAndroid Build Coastguard Worker      },
282*6dbdd20aSAndroid Build Coastguard Worker    });
283*6dbdd20aSAndroid Build Coastguard Worker  }
284*6dbdd20aSAndroid Build Coastguard Worker
285*6dbdd20aSAndroid Build Coastguard Worker  onremove() {
286*6dbdd20aSAndroid Build Coastguard Worker    if (this.zoomContent) this.zoomContent[Symbol.dispose]();
287*6dbdd20aSAndroid Build Coastguard Worker  }
288*6dbdd20aSAndroid Build Coastguard Worker
289*6dbdd20aSAndroid Build Coastguard Worker  view({attrs}: m.CVnode<PageWithTraceImplAttrs>) {
290*6dbdd20aSAndroid Build Coastguard Worker    const scrollingPanels = renderToplevelPanels(attrs.trace);
291*6dbdd20aSAndroid Build Coastguard Worker
292*6dbdd20aSAndroid Build Coastguard Worker    const result = m(
293*6dbdd20aSAndroid Build Coastguard Worker      '.page.viewer-page',
294*6dbdd20aSAndroid Build Coastguard Worker      m(
295*6dbdd20aSAndroid Build Coastguard Worker        '.pan-and-zoom-content',
296*6dbdd20aSAndroid Build Coastguard Worker        {
297*6dbdd20aSAndroid Build Coastguard Worker          ref: this.PAN_ZOOM_CONTENT_REF,
298*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => {
299*6dbdd20aSAndroid Build Coastguard Worker            // We don't want to deselect when panning/drag selecting.
300*6dbdd20aSAndroid Build Coastguard Worker            if (this.keepCurrentSelection) {
301*6dbdd20aSAndroid Build Coastguard Worker              this.keepCurrentSelection = false;
302*6dbdd20aSAndroid Build Coastguard Worker              return;
303*6dbdd20aSAndroid Build Coastguard Worker            }
304*6dbdd20aSAndroid Build Coastguard Worker            attrs.trace.selection.clear();
305*6dbdd20aSAndroid Build Coastguard Worker          },
306*6dbdd20aSAndroid Build Coastguard Worker        },
307*6dbdd20aSAndroid Build Coastguard Worker        m(
308*6dbdd20aSAndroid Build Coastguard Worker          '.pf-timeline-header',
309*6dbdd20aSAndroid Build Coastguard Worker          m(PanelContainer, {
310*6dbdd20aSAndroid Build Coastguard Worker            trace: attrs.trace,
311*6dbdd20aSAndroid Build Coastguard Worker            className: 'header-panel-container',
312*6dbdd20aSAndroid Build Coastguard Worker            panels: removeFalsyValues([
313*6dbdd20aSAndroid Build Coastguard Worker              OVERVIEW_PANEL_FLAG.get() && this.overviewTimelinePanel,
314*6dbdd20aSAndroid Build Coastguard Worker              this.timeAxisPanel,
315*6dbdd20aSAndroid Build Coastguard Worker              this.timeSelectionPanel,
316*6dbdd20aSAndroid Build Coastguard Worker              this.notesPanel,
317*6dbdd20aSAndroid Build Coastguard Worker              this.tickmarkPanel,
318*6dbdd20aSAndroid Build Coastguard Worker            ]),
319*6dbdd20aSAndroid Build Coastguard Worker            selectedYRange: this.getYRange('header-panel-container'),
320*6dbdd20aSAndroid Build Coastguard Worker          }),
321*6dbdd20aSAndroid Build Coastguard Worker          m('.scrollbar-spacer-vertical'),
322*6dbdd20aSAndroid Build Coastguard Worker        ),
323*6dbdd20aSAndroid Build Coastguard Worker        m(PanelContainer, {
324*6dbdd20aSAndroid Build Coastguard Worker          trace: attrs.trace,
325*6dbdd20aSAndroid Build Coastguard Worker          className: 'pinned-panel-container',
326*6dbdd20aSAndroid Build Coastguard Worker          panels: AppImpl.instance.isLoadingTrace
327*6dbdd20aSAndroid Build Coastguard Worker            ? []
328*6dbdd20aSAndroid Build Coastguard Worker            : attrs.trace.workspace.pinnedTracks.map((trackNode) => {
329*6dbdd20aSAndroid Build Coastguard Worker                if (trackNode.uri) {
330*6dbdd20aSAndroid Build Coastguard Worker                  const tr = attrs.trace.tracks.getTrackRenderer(trackNode.uri);
331*6dbdd20aSAndroid Build Coastguard Worker                  return new TrackPanel({
332*6dbdd20aSAndroid Build Coastguard Worker                    trace: attrs.trace,
333*6dbdd20aSAndroid Build Coastguard Worker                    reorderable: true,
334*6dbdd20aSAndroid Build Coastguard Worker                    node: trackNode,
335*6dbdd20aSAndroid Build Coastguard Worker                    trackRenderer: tr,
336*6dbdd20aSAndroid Build Coastguard Worker                    revealOnCreate: true,
337*6dbdd20aSAndroid Build Coastguard Worker                    indentationLevel: 0,
338*6dbdd20aSAndroid Build Coastguard Worker                    topOffsetPx: 0,
339*6dbdd20aSAndroid Build Coastguard Worker                  });
340*6dbdd20aSAndroid Build Coastguard Worker                } else {
341*6dbdd20aSAndroid Build Coastguard Worker                  return new TrackPanel({
342*6dbdd20aSAndroid Build Coastguard Worker                    trace: attrs.trace,
343*6dbdd20aSAndroid Build Coastguard Worker                    node: trackNode,
344*6dbdd20aSAndroid Build Coastguard Worker                    revealOnCreate: true,
345*6dbdd20aSAndroid Build Coastguard Worker                    indentationLevel: 0,
346*6dbdd20aSAndroid Build Coastguard Worker                    topOffsetPx: 0,
347*6dbdd20aSAndroid Build Coastguard Worker                  });
348*6dbdd20aSAndroid Build Coastguard Worker                }
349*6dbdd20aSAndroid Build Coastguard Worker              }),
350*6dbdd20aSAndroid Build Coastguard Worker          renderUnderlay: (ctx, size) => renderUnderlay(attrs.trace, ctx, size),
351*6dbdd20aSAndroid Build Coastguard Worker          renderOverlay: (ctx, size, panels) =>
352*6dbdd20aSAndroid Build Coastguard Worker            renderOverlay(
353*6dbdd20aSAndroid Build Coastguard Worker              attrs.trace,
354*6dbdd20aSAndroid Build Coastguard Worker              ctx,
355*6dbdd20aSAndroid Build Coastguard Worker              size,
356*6dbdd20aSAndroid Build Coastguard Worker              panels,
357*6dbdd20aSAndroid Build Coastguard Worker              attrs.trace.workspace.pinnedTracksNode,
358*6dbdd20aSAndroid Build Coastguard Worker            ),
359*6dbdd20aSAndroid Build Coastguard Worker          selectedYRange: this.getYRange('pinned-panel-container'),
360*6dbdd20aSAndroid Build Coastguard Worker        }),
361*6dbdd20aSAndroid Build Coastguard Worker        m(PanelContainer, {
362*6dbdd20aSAndroid Build Coastguard Worker          trace: attrs.trace,
363*6dbdd20aSAndroid Build Coastguard Worker          className: 'scrolling-panel-container',
364*6dbdd20aSAndroid Build Coastguard Worker          panels: AppImpl.instance.isLoadingTrace ? [] : scrollingPanels,
365*6dbdd20aSAndroid Build Coastguard Worker          onPanelStackResize: (width) => {
366*6dbdd20aSAndroid Build Coastguard Worker            const timelineWidth = width - TRACK_SHELL_WIDTH;
367*6dbdd20aSAndroid Build Coastguard Worker            this.timelineWidthPx = timelineWidth;
368*6dbdd20aSAndroid Build Coastguard Worker          },
369*6dbdd20aSAndroid Build Coastguard Worker          renderUnderlay: (ctx, size) => renderUnderlay(attrs.trace, ctx, size),
370*6dbdd20aSAndroid Build Coastguard Worker          renderOverlay: (ctx, size, panels) =>
371*6dbdd20aSAndroid Build Coastguard Worker            renderOverlay(
372*6dbdd20aSAndroid Build Coastguard Worker              attrs.trace,
373*6dbdd20aSAndroid Build Coastguard Worker              ctx,
374*6dbdd20aSAndroid Build Coastguard Worker              size,
375*6dbdd20aSAndroid Build Coastguard Worker              panels,
376*6dbdd20aSAndroid Build Coastguard Worker              attrs.trace.workspace.tracks,
377*6dbdd20aSAndroid Build Coastguard Worker            ),
378*6dbdd20aSAndroid Build Coastguard Worker          selectedYRange: this.getYRange('scrolling-panel-container'),
379*6dbdd20aSAndroid Build Coastguard Worker        }),
380*6dbdd20aSAndroid Build Coastguard Worker      ),
381*6dbdd20aSAndroid Build Coastguard Worker      m(TabPanel, {
382*6dbdd20aSAndroid Build Coastguard Worker        trace: attrs.trace,
383*6dbdd20aSAndroid Build Coastguard Worker      }),
384*6dbdd20aSAndroid Build Coastguard Worker      this.showPanningHint && m(HelpPanningNotification),
385*6dbdd20aSAndroid Build Coastguard Worker    );
386*6dbdd20aSAndroid Build Coastguard Worker
387*6dbdd20aSAndroid Build Coastguard Worker    attrs.trace.tracks.flushOldTracks();
388*6dbdd20aSAndroid Build Coastguard Worker    return result;
389*6dbdd20aSAndroid Build Coastguard Worker  }
390*6dbdd20aSAndroid Build Coastguard Worker
391*6dbdd20aSAndroid Build Coastguard Worker  private getYRange(cls: string): VerticalBounds | undefined {
392*6dbdd20aSAndroid Build Coastguard Worker    if (this.selectedContainer?.containerClass !== cls) {
393*6dbdd20aSAndroid Build Coastguard Worker      return undefined;
394*6dbdd20aSAndroid Build Coastguard Worker    }
395*6dbdd20aSAndroid Build Coastguard Worker    const {dragStartAbsY, dragEndAbsY} = this.selectedContainer;
396*6dbdd20aSAndroid Build Coastguard Worker    return {
397*6dbdd20aSAndroid Build Coastguard Worker      top: Math.min(dragStartAbsY, dragEndAbsY),
398*6dbdd20aSAndroid Build Coastguard Worker      bottom: Math.max(dragStartAbsY, dragEndAbsY),
399*6dbdd20aSAndroid Build Coastguard Worker    };
400*6dbdd20aSAndroid Build Coastguard Worker  }
401*6dbdd20aSAndroid Build Coastguard Worker}
402*6dbdd20aSAndroid Build Coastguard Worker
403*6dbdd20aSAndroid Build Coastguard Workerfunction renderUnderlay(
404*6dbdd20aSAndroid Build Coastguard Worker  trace: TraceImpl,
405*6dbdd20aSAndroid Build Coastguard Worker  ctx: CanvasRenderingContext2D,
406*6dbdd20aSAndroid Build Coastguard Worker  canvasSize: Size2D,
407*6dbdd20aSAndroid Build Coastguard Worker): void {
408*6dbdd20aSAndroid Build Coastguard Worker  const size = {
409*6dbdd20aSAndroid Build Coastguard Worker    width: canvasSize.width - TRACK_SHELL_WIDTH,
410*6dbdd20aSAndroid Build Coastguard Worker    height: canvasSize.height,
411*6dbdd20aSAndroid Build Coastguard Worker  };
412*6dbdd20aSAndroid Build Coastguard Worker
413*6dbdd20aSAndroid Build Coastguard Worker  using _ = canvasSave(ctx);
414*6dbdd20aSAndroid Build Coastguard Worker  ctx.translate(TRACK_SHELL_WIDTH, 0);
415*6dbdd20aSAndroid Build Coastguard Worker
416*6dbdd20aSAndroid Build Coastguard Worker  const timewindow = trace.timeline.visibleWindow;
417*6dbdd20aSAndroid Build Coastguard Worker  const timescale = new TimeScale(timewindow, {left: 0, right: size.width});
418*6dbdd20aSAndroid Build Coastguard Worker
419*6dbdd20aSAndroid Build Coastguard Worker  // Just render the gridlines - these should appear underneath all tracks
420*6dbdd20aSAndroid Build Coastguard Worker  drawGridLines(trace, ctx, timewindow.toTimeSpan(), timescale, size);
421*6dbdd20aSAndroid Build Coastguard Worker}
422*6dbdd20aSAndroid Build Coastguard Worker
423*6dbdd20aSAndroid Build Coastguard Workerfunction renderOverlay(
424*6dbdd20aSAndroid Build Coastguard Worker  trace: TraceImpl,
425*6dbdd20aSAndroid Build Coastguard Worker  ctx: CanvasRenderingContext2D,
426*6dbdd20aSAndroid Build Coastguard Worker  canvasSize: Size2D,
427*6dbdd20aSAndroid Build Coastguard Worker  panels: ReadonlyArray<RenderedPanelInfo>,
428*6dbdd20aSAndroid Build Coastguard Worker  trackContainer: TrackNode,
429*6dbdd20aSAndroid Build Coastguard Worker): void {
430*6dbdd20aSAndroid Build Coastguard Worker  const size = {
431*6dbdd20aSAndroid Build Coastguard Worker    width: canvasSize.width - TRACK_SHELL_WIDTH,
432*6dbdd20aSAndroid Build Coastguard Worker    height: canvasSize.height,
433*6dbdd20aSAndroid Build Coastguard Worker  };
434*6dbdd20aSAndroid Build Coastguard Worker
435*6dbdd20aSAndroid Build Coastguard Worker  using _ = canvasSave(ctx);
436*6dbdd20aSAndroid Build Coastguard Worker  ctx.translate(TRACK_SHELL_WIDTH, 0);
437*6dbdd20aSAndroid Build Coastguard Worker  canvasClip(ctx, 0, 0, size.width, size.height);
438*6dbdd20aSAndroid Build Coastguard Worker
439*6dbdd20aSAndroid Build Coastguard Worker  // TODO(primiano): plumb the TraceImpl obj throughout the viwer page.
440*6dbdd20aSAndroid Build Coastguard Worker  renderFlows(trace, ctx, size, panels, trackContainer);
441*6dbdd20aSAndroid Build Coastguard Worker
442*6dbdd20aSAndroid Build Coastguard Worker  const timewindow = trace.timeline.visibleWindow;
443*6dbdd20aSAndroid Build Coastguard Worker  const timescale = new TimeScale(timewindow, {left: 0, right: size.width});
444*6dbdd20aSAndroid Build Coastguard Worker
445*6dbdd20aSAndroid Build Coastguard Worker  renderHoveredNoteVertical(trace, ctx, timescale, size);
446*6dbdd20aSAndroid Build Coastguard Worker  renderHoveredCursorVertical(trace, ctx, timescale, size);
447*6dbdd20aSAndroid Build Coastguard Worker  renderWakeupVertical(trace, ctx, timescale, size);
448*6dbdd20aSAndroid Build Coastguard Worker  renderNoteVerticals(trace, ctx, timescale, size);
449*6dbdd20aSAndroid Build Coastguard Worker}
450*6dbdd20aSAndroid Build Coastguard Worker
451*6dbdd20aSAndroid Build Coastguard Worker// Render the toplevel "scrolling" tracks and track groups
452*6dbdd20aSAndroid Build Coastguard Workerfunction renderToplevelPanels(trace: TraceImpl): PanelOrGroup[] {
453*6dbdd20aSAndroid Build Coastguard Worker  return renderNodes(trace, trace.workspace.children, 0, 0);
454*6dbdd20aSAndroid Build Coastguard Worker}
455*6dbdd20aSAndroid Build Coastguard Worker
456*6dbdd20aSAndroid Build Coastguard Worker// Given a list of tracks and a filter term, return a list pf panels filtered by
457*6dbdd20aSAndroid Build Coastguard Worker// the filter term
458*6dbdd20aSAndroid Build Coastguard Workerfunction renderNodes(
459*6dbdd20aSAndroid Build Coastguard Worker  trace: TraceImpl,
460*6dbdd20aSAndroid Build Coastguard Worker  nodes: ReadonlyArray<TrackNode>,
461*6dbdd20aSAndroid Build Coastguard Worker  indent: number,
462*6dbdd20aSAndroid Build Coastguard Worker  topOffsetPx: number,
463*6dbdd20aSAndroid Build Coastguard Worker): PanelOrGroup[] {
464*6dbdd20aSAndroid Build Coastguard Worker  return nodes.flatMap((node) => {
465*6dbdd20aSAndroid Build Coastguard Worker    if (node.headless) {
466*6dbdd20aSAndroid Build Coastguard Worker      // Render children as if this node doesn't exist
467*6dbdd20aSAndroid Build Coastguard Worker      return renderNodes(trace, node.children, indent, topOffsetPx);
468*6dbdd20aSAndroid Build Coastguard Worker    } else if (node.children.length === 0) {
469*6dbdd20aSAndroid Build Coastguard Worker      return renderTrackPanel(trace, node, indent, topOffsetPx);
470*6dbdd20aSAndroid Build Coastguard Worker    } else {
471*6dbdd20aSAndroid Build Coastguard Worker      const headerPanel = renderTrackPanel(trace, node, indent, topOffsetPx);
472*6dbdd20aSAndroid Build Coastguard Worker      const isSticky = node.isSummary;
473*6dbdd20aSAndroid Build Coastguard Worker      const nextTopOffsetPx = isSticky
474*6dbdd20aSAndroid Build Coastguard Worker        ? topOffsetPx + headerPanel.heightPx
475*6dbdd20aSAndroid Build Coastguard Worker        : topOffsetPx;
476*6dbdd20aSAndroid Build Coastguard Worker      return {
477*6dbdd20aSAndroid Build Coastguard Worker        kind: 'group',
478*6dbdd20aSAndroid Build Coastguard Worker        collapsed: node.collapsed,
479*6dbdd20aSAndroid Build Coastguard Worker        header: headerPanel,
480*6dbdd20aSAndroid Build Coastguard Worker        sticky: isSticky, // && node.collapsed??
481*6dbdd20aSAndroid Build Coastguard Worker        topOffsetPx,
482*6dbdd20aSAndroid Build Coastguard Worker        childPanels: node.collapsed
483*6dbdd20aSAndroid Build Coastguard Worker          ? []
484*6dbdd20aSAndroid Build Coastguard Worker          : renderNodes(trace, node.children, indent + 1, nextTopOffsetPx),
485*6dbdd20aSAndroid Build Coastguard Worker      };
486*6dbdd20aSAndroid Build Coastguard Worker    }
487*6dbdd20aSAndroid Build Coastguard Worker  });
488*6dbdd20aSAndroid Build Coastguard Worker}
489*6dbdd20aSAndroid Build Coastguard Worker
490*6dbdd20aSAndroid Build Coastguard Workerfunction renderTrackPanel(
491*6dbdd20aSAndroid Build Coastguard Worker  trace: TraceImpl,
492*6dbdd20aSAndroid Build Coastguard Worker  trackNode: TrackNode,
493*6dbdd20aSAndroid Build Coastguard Worker  indent: number,
494*6dbdd20aSAndroid Build Coastguard Worker  topOffsetPx: number,
495*6dbdd20aSAndroid Build Coastguard Worker) {
496*6dbdd20aSAndroid Build Coastguard Worker  let tr = undefined;
497*6dbdd20aSAndroid Build Coastguard Worker  if (trackNode.uri) {
498*6dbdd20aSAndroid Build Coastguard Worker    tr = trace.tracks.getTrackRenderer(trackNode.uri);
499*6dbdd20aSAndroid Build Coastguard Worker  }
500*6dbdd20aSAndroid Build Coastguard Worker  return new TrackPanel({
501*6dbdd20aSAndroid Build Coastguard Worker    trace,
502*6dbdd20aSAndroid Build Coastguard Worker    node: trackNode,
503*6dbdd20aSAndroid Build Coastguard Worker    trackRenderer: tr,
504*6dbdd20aSAndroid Build Coastguard Worker    indentationLevel: indent,
505*6dbdd20aSAndroid Build Coastguard Worker    topOffsetPx,
506*6dbdd20aSAndroid Build Coastguard Worker  });
507*6dbdd20aSAndroid Build Coastguard Worker}
508*6dbdd20aSAndroid Build Coastguard Worker
509*6dbdd20aSAndroid Build Coastguard Workerexport function drawGridLines(
510*6dbdd20aSAndroid Build Coastguard Worker  trace: TraceImpl,
511*6dbdd20aSAndroid Build Coastguard Worker  ctx: CanvasRenderingContext2D,
512*6dbdd20aSAndroid Build Coastguard Worker  timespan: TimeSpan,
513*6dbdd20aSAndroid Build Coastguard Worker  timescale: TimeScale,
514*6dbdd20aSAndroid Build Coastguard Worker  size: Size2D,
515*6dbdd20aSAndroid Build Coastguard Worker): void {
516*6dbdd20aSAndroid Build Coastguard Worker  ctx.strokeStyle = TRACK_BORDER_COLOR;
517*6dbdd20aSAndroid Build Coastguard Worker  ctx.lineWidth = 1;
518*6dbdd20aSAndroid Build Coastguard Worker
519*6dbdd20aSAndroid Build Coastguard Worker  if (size.width > 0 && timespan.duration > 0n) {
520*6dbdd20aSAndroid Build Coastguard Worker    const maxMajorTicks = getMaxMajorTicks(size.width);
521*6dbdd20aSAndroid Build Coastguard Worker    const offset = trace.timeline.timestampOffset();
522*6dbdd20aSAndroid Build Coastguard Worker    for (const {type, time} of generateTicks(timespan, maxMajorTicks, offset)) {
523*6dbdd20aSAndroid Build Coastguard Worker      const px = Math.floor(timescale.timeToPx(time));
524*6dbdd20aSAndroid Build Coastguard Worker      if (type === TickType.MAJOR) {
525*6dbdd20aSAndroid Build Coastguard Worker        ctx.beginPath();
526*6dbdd20aSAndroid Build Coastguard Worker        ctx.moveTo(px + 0.5, 0);
527*6dbdd20aSAndroid Build Coastguard Worker        ctx.lineTo(px + 0.5, size.height);
528*6dbdd20aSAndroid Build Coastguard Worker        ctx.stroke();
529*6dbdd20aSAndroid Build Coastguard Worker      }
530*6dbdd20aSAndroid Build Coastguard Worker    }
531*6dbdd20aSAndroid Build Coastguard Worker  }
532*6dbdd20aSAndroid Build Coastguard Worker}
533*6dbdd20aSAndroid Build Coastguard Worker
534*6dbdd20aSAndroid Build Coastguard Workerexport function renderHoveredCursorVertical(
535*6dbdd20aSAndroid Build Coastguard Worker  trace: TraceImpl,
536*6dbdd20aSAndroid Build Coastguard Worker  ctx: CanvasRenderingContext2D,
537*6dbdd20aSAndroid Build Coastguard Worker  timescale: TimeScale,
538*6dbdd20aSAndroid Build Coastguard Worker  size: Size2D,
539*6dbdd20aSAndroid Build Coastguard Worker) {
540*6dbdd20aSAndroid Build Coastguard Worker  if (trace.timeline.hoverCursorTimestamp !== undefined) {
541*6dbdd20aSAndroid Build Coastguard Worker    drawVerticalLineAtTime(
542*6dbdd20aSAndroid Build Coastguard Worker      ctx,
543*6dbdd20aSAndroid Build Coastguard Worker      timescale,
544*6dbdd20aSAndroid Build Coastguard Worker      trace.timeline.hoverCursorTimestamp,
545*6dbdd20aSAndroid Build Coastguard Worker      size.height,
546*6dbdd20aSAndroid Build Coastguard Worker      `#344596`,
547*6dbdd20aSAndroid Build Coastguard Worker    );
548*6dbdd20aSAndroid Build Coastguard Worker  }
549*6dbdd20aSAndroid Build Coastguard Worker}
550*6dbdd20aSAndroid Build Coastguard Worker
551*6dbdd20aSAndroid Build Coastguard Workerexport function renderHoveredNoteVertical(
552*6dbdd20aSAndroid Build Coastguard Worker  trace: TraceImpl,
553*6dbdd20aSAndroid Build Coastguard Worker  ctx: CanvasRenderingContext2D,
554*6dbdd20aSAndroid Build Coastguard Worker  timescale: TimeScale,
555*6dbdd20aSAndroid Build Coastguard Worker  size: Size2D,
556*6dbdd20aSAndroid Build Coastguard Worker) {
557*6dbdd20aSAndroid Build Coastguard Worker  if (trace.timeline.hoveredNoteTimestamp !== undefined) {
558*6dbdd20aSAndroid Build Coastguard Worker    drawVerticalLineAtTime(
559*6dbdd20aSAndroid Build Coastguard Worker      ctx,
560*6dbdd20aSAndroid Build Coastguard Worker      timescale,
561*6dbdd20aSAndroid Build Coastguard Worker      trace.timeline.hoveredNoteTimestamp,
562*6dbdd20aSAndroid Build Coastguard Worker      size.height,
563*6dbdd20aSAndroid Build Coastguard Worker      `#aaa`,
564*6dbdd20aSAndroid Build Coastguard Worker    );
565*6dbdd20aSAndroid Build Coastguard Worker  }
566*6dbdd20aSAndroid Build Coastguard Worker}
567*6dbdd20aSAndroid Build Coastguard Worker
568*6dbdd20aSAndroid Build Coastguard Workerexport function renderWakeupVertical(
569*6dbdd20aSAndroid Build Coastguard Worker  trace: TraceImpl,
570*6dbdd20aSAndroid Build Coastguard Worker  ctx: CanvasRenderingContext2D,
571*6dbdd20aSAndroid Build Coastguard Worker  timescale: TimeScale,
572*6dbdd20aSAndroid Build Coastguard Worker  size: Size2D,
573*6dbdd20aSAndroid Build Coastguard Worker) {
574*6dbdd20aSAndroid Build Coastguard Worker  const selection = trace.selection.selection;
575*6dbdd20aSAndroid Build Coastguard Worker  if (selection.kind === 'track_event' && selection.wakeupTs) {
576*6dbdd20aSAndroid Build Coastguard Worker    drawVerticalLineAtTime(
577*6dbdd20aSAndroid Build Coastguard Worker      ctx,
578*6dbdd20aSAndroid Build Coastguard Worker      timescale,
579*6dbdd20aSAndroid Build Coastguard Worker      selection.wakeupTs,
580*6dbdd20aSAndroid Build Coastguard Worker      size.height,
581*6dbdd20aSAndroid Build Coastguard Worker      `black`,
582*6dbdd20aSAndroid Build Coastguard Worker    );
583*6dbdd20aSAndroid Build Coastguard Worker  }
584*6dbdd20aSAndroid Build Coastguard Worker}
585*6dbdd20aSAndroid Build Coastguard Worker
586*6dbdd20aSAndroid Build Coastguard Workerexport function renderNoteVerticals(
587*6dbdd20aSAndroid Build Coastguard Worker  trace: TraceImpl,
588*6dbdd20aSAndroid Build Coastguard Worker  ctx: CanvasRenderingContext2D,
589*6dbdd20aSAndroid Build Coastguard Worker  timescale: TimeScale,
590*6dbdd20aSAndroid Build Coastguard Worker  size: Size2D,
591*6dbdd20aSAndroid Build Coastguard Worker) {
592*6dbdd20aSAndroid Build Coastguard Worker  // All marked areas should have semi-transparent vertical lines
593*6dbdd20aSAndroid Build Coastguard Worker  // marking the start and end.
594*6dbdd20aSAndroid Build Coastguard Worker  for (const note of trace.notes.notes.values()) {
595*6dbdd20aSAndroid Build Coastguard Worker    if (note.noteType === 'SPAN') {
596*6dbdd20aSAndroid Build Coastguard Worker      const transparentNoteColor =
597*6dbdd20aSAndroid Build Coastguard Worker        'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
598*6dbdd20aSAndroid Build Coastguard Worker      drawVerticalLineAtTime(
599*6dbdd20aSAndroid Build Coastguard Worker        ctx,
600*6dbdd20aSAndroid Build Coastguard Worker        timescale,
601*6dbdd20aSAndroid Build Coastguard Worker        note.start,
602*6dbdd20aSAndroid Build Coastguard Worker        size.height,
603*6dbdd20aSAndroid Build Coastguard Worker        transparentNoteColor,
604*6dbdd20aSAndroid Build Coastguard Worker        1,
605*6dbdd20aSAndroid Build Coastguard Worker      );
606*6dbdd20aSAndroid Build Coastguard Worker      drawVerticalLineAtTime(
607*6dbdd20aSAndroid Build Coastguard Worker        ctx,
608*6dbdd20aSAndroid Build Coastguard Worker        timescale,
609*6dbdd20aSAndroid Build Coastguard Worker        note.end,
610*6dbdd20aSAndroid Build Coastguard Worker        size.height,
611*6dbdd20aSAndroid Build Coastguard Worker        transparentNoteColor,
612*6dbdd20aSAndroid Build Coastguard Worker        1,
613*6dbdd20aSAndroid Build Coastguard Worker      );
614*6dbdd20aSAndroid Build Coastguard Worker    } else if (note.noteType === 'DEFAULT') {
615*6dbdd20aSAndroid Build Coastguard Worker      drawVerticalLineAtTime(
616*6dbdd20aSAndroid Build Coastguard Worker        ctx,
617*6dbdd20aSAndroid Build Coastguard Worker        timescale,
618*6dbdd20aSAndroid Build Coastguard Worker        note.timestamp,
619*6dbdd20aSAndroid Build Coastguard Worker        size.height,
620*6dbdd20aSAndroid Build Coastguard Worker        note.color,
621*6dbdd20aSAndroid Build Coastguard Worker      );
622*6dbdd20aSAndroid Build Coastguard Worker    }
623*6dbdd20aSAndroid Build Coastguard Worker  }
624*6dbdd20aSAndroid Build Coastguard Worker}
625*6dbdd20aSAndroid Build Coastguard Worker
626*6dbdd20aSAndroid Build Coastguard Workerclass HelpPanningNotification implements m.ClassComponent {
627*6dbdd20aSAndroid Build Coastguard Worker  private readonly PANNING_HINT_KEY = 'dismissedPanningHint';
628*6dbdd20aSAndroid Build Coastguard Worker  private dismissed = localStorage.getItem(this.PANNING_HINT_KEY) === 'true';
629*6dbdd20aSAndroid Build Coastguard Worker
630*6dbdd20aSAndroid Build Coastguard Worker  view() {
631*6dbdd20aSAndroid Build Coastguard Worker    // Do not show the help notification in embedded mode because local storage
632*6dbdd20aSAndroid Build Coastguard Worker    // does not persist for iFrames. The host is responsible for communicating
633*6dbdd20aSAndroid Build Coastguard Worker    // to users that they can press '?' for help.
634*6dbdd20aSAndroid Build Coastguard Worker    if (AppImpl.instance.embeddedMode || this.dismissed) {
635*6dbdd20aSAndroid Build Coastguard Worker      return;
636*6dbdd20aSAndroid Build Coastguard Worker    }
637*6dbdd20aSAndroid Build Coastguard Worker    return m(
638*6dbdd20aSAndroid Build Coastguard Worker      '.helpful-hint',
639*6dbdd20aSAndroid Build Coastguard Worker      m(
640*6dbdd20aSAndroid Build Coastguard Worker        '.hint-text',
641*6dbdd20aSAndroid Build Coastguard Worker        'Are you trying to pan? Use the WASD keys or hold shift to click ' +
642*6dbdd20aSAndroid Build Coastguard Worker          "and drag. Press '?' for more help.",
643*6dbdd20aSAndroid Build Coastguard Worker      ),
644*6dbdd20aSAndroid Build Coastguard Worker      m(
645*6dbdd20aSAndroid Build Coastguard Worker        'button.hint-dismiss-button',
646*6dbdd20aSAndroid Build Coastguard Worker        {
647*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => {
648*6dbdd20aSAndroid Build Coastguard Worker            this.dismissed = true;
649*6dbdd20aSAndroid Build Coastguard Worker            localStorage.setItem(this.PANNING_HINT_KEY, 'true');
650*6dbdd20aSAndroid Build Coastguard Worker            raf.scheduleFullRedraw();
651*6dbdd20aSAndroid Build Coastguard Worker          },
652*6dbdd20aSAndroid Build Coastguard Worker        },
653*6dbdd20aSAndroid Build Coastguard Worker        'Dismiss',
654*6dbdd20aSAndroid Build Coastguard Worker      ),
655*6dbdd20aSAndroid Build Coastguard Worker    );
656*6dbdd20aSAndroid Build Coastguard Worker  }
657*6dbdd20aSAndroid Build Coastguard Worker}
658