xref: /aosp_15_r20/external/perfetto/ui/src/frontend/track_panel.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 m from 'mithril';
16*6dbdd20aSAndroid Build Coastguard Workerimport {canvasClip, canvasSave} from '../base/canvas_utils';
17*6dbdd20aSAndroid Build Coastguard Workerimport {classNames} from '../base/classnames';
18*6dbdd20aSAndroid Build Coastguard Workerimport {Bounds2D, Size2D, VerticalBounds} from '../base/geom';
19*6dbdd20aSAndroid Build Coastguard Workerimport {Icons} from '../base/semantic_icons';
20*6dbdd20aSAndroid Build Coastguard Workerimport {TimeScale} from '../base/time_scale';
21*6dbdd20aSAndroid Build Coastguard Workerimport {RequiredField} from '../base/utils';
22*6dbdd20aSAndroid Build Coastguard Workerimport {calculateResolution} from '../common/resolution';
23*6dbdd20aSAndroid Build Coastguard Workerimport {featureFlags} from '../core/feature_flags';
24*6dbdd20aSAndroid Build Coastguard Workerimport {TrackRenderer} from '../core/track_manager';
25*6dbdd20aSAndroid Build Coastguard Workerimport {TrackDescriptor, TrackRenderContext} from '../public/track';
26*6dbdd20aSAndroid Build Coastguard Workerimport {TrackNode} from '../public/workspace';
27*6dbdd20aSAndroid Build Coastguard Workerimport {Button} from '../widgets/button';
28*6dbdd20aSAndroid Build Coastguard Workerimport {Popup, PopupPosition} from '../widgets/popup';
29*6dbdd20aSAndroid Build Coastguard Workerimport {Tree, TreeNode} from '../widgets/tree';
30*6dbdd20aSAndroid Build Coastguard Workerimport {SELECTION_FILL_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
31*6dbdd20aSAndroid Build Coastguard Workerimport {Panel} from './panel_container';
32*6dbdd20aSAndroid Build Coastguard Workerimport {TrackWidget} from '../widgets/track_widget';
33*6dbdd20aSAndroid Build Coastguard Workerimport {raf} from '../core/raf_scheduler';
34*6dbdd20aSAndroid Build Coastguard Workerimport {Intent} from '../widgets/common';
35*6dbdd20aSAndroid Build Coastguard Workerimport {TraceImpl} from '../core/trace_impl';
36*6dbdd20aSAndroid Build Coastguard Worker
37*6dbdd20aSAndroid Build Coastguard Workerconst SHOW_TRACK_DETAILS_BUTTON = featureFlags.register({
38*6dbdd20aSAndroid Build Coastguard Worker  id: 'showTrackDetailsButton',
39*6dbdd20aSAndroid Build Coastguard Worker  name: 'Show track details button',
40*6dbdd20aSAndroid Build Coastguard Worker  description: 'Show track details button in track shells.',
41*6dbdd20aSAndroid Build Coastguard Worker  defaultValue: false,
42*6dbdd20aSAndroid Build Coastguard Worker});
43*6dbdd20aSAndroid Build Coastguard Worker
44*6dbdd20aSAndroid Build Coastguard Worker// Default height of a track element that has no track, or is collapsed.
45*6dbdd20aSAndroid Build Coastguard Worker// Note: This is designed to roughly match the height of a cpu slice track.
46*6dbdd20aSAndroid Build Coastguard Workerexport const DEFAULT_TRACK_HEIGHT_PX = 30;
47*6dbdd20aSAndroid Build Coastguard Worker
48*6dbdd20aSAndroid Build Coastguard Workerinterface TrackPanelAttrs {
49*6dbdd20aSAndroid Build Coastguard Worker  readonly trace: TraceImpl;
50*6dbdd20aSAndroid Build Coastguard Worker  readonly node: TrackNode;
51*6dbdd20aSAndroid Build Coastguard Worker  readonly indentationLevel: number;
52*6dbdd20aSAndroid Build Coastguard Worker  readonly trackRenderer?: TrackRenderer;
53*6dbdd20aSAndroid Build Coastguard Worker  readonly revealOnCreate?: boolean;
54*6dbdd20aSAndroid Build Coastguard Worker  readonly topOffsetPx: number;
55*6dbdd20aSAndroid Build Coastguard Worker  readonly reorderable?: boolean;
56*6dbdd20aSAndroid Build Coastguard Worker}
57*6dbdd20aSAndroid Build Coastguard Worker
58*6dbdd20aSAndroid Build Coastguard Workerexport class TrackPanel implements Panel {
59*6dbdd20aSAndroid Build Coastguard Worker  readonly kind = 'panel';
60*6dbdd20aSAndroid Build Coastguard Worker  readonly selectable = true;
61*6dbdd20aSAndroid Build Coastguard Worker  readonly trackNode?: TrackNode;
62*6dbdd20aSAndroid Build Coastguard Worker
63*6dbdd20aSAndroid Build Coastguard Worker  private readonly attrs: TrackPanelAttrs;
64*6dbdd20aSAndroid Build Coastguard Worker
65*6dbdd20aSAndroid Build Coastguard Worker  constructor(attrs: TrackPanelAttrs) {
66*6dbdd20aSAndroid Build Coastguard Worker    this.attrs = attrs;
67*6dbdd20aSAndroid Build Coastguard Worker    this.trackNode = attrs.node;
68*6dbdd20aSAndroid Build Coastguard Worker  }
69*6dbdd20aSAndroid Build Coastguard Worker
70*6dbdd20aSAndroid Build Coastguard Worker  get heightPx(): number {
71*6dbdd20aSAndroid Build Coastguard Worker    const {trackRenderer, node} = this.attrs;
72*6dbdd20aSAndroid Build Coastguard Worker
73*6dbdd20aSAndroid Build Coastguard Worker    // If the node is a summary track and is expanded, shrink it to save
74*6dbdd20aSAndroid Build Coastguard Worker    // vertical real estate).
75*6dbdd20aSAndroid Build Coastguard Worker    if (node.isSummary && node.expanded) return DEFAULT_TRACK_HEIGHT_PX;
76*6dbdd20aSAndroid Build Coastguard Worker
77*6dbdd20aSAndroid Build Coastguard Worker    // Otherwise return the height of the track, if we have one.
78*6dbdd20aSAndroid Build Coastguard Worker    return trackRenderer?.track.getHeight() ?? DEFAULT_TRACK_HEIGHT_PX;
79*6dbdd20aSAndroid Build Coastguard Worker  }
80*6dbdd20aSAndroid Build Coastguard Worker
81*6dbdd20aSAndroid Build Coastguard Worker  render(): m.Children {
82*6dbdd20aSAndroid Build Coastguard Worker    const {
83*6dbdd20aSAndroid Build Coastguard Worker      node,
84*6dbdd20aSAndroid Build Coastguard Worker      indentationLevel,
85*6dbdd20aSAndroid Build Coastguard Worker      trackRenderer,
86*6dbdd20aSAndroid Build Coastguard Worker      revealOnCreate,
87*6dbdd20aSAndroid Build Coastguard Worker      topOffsetPx,
88*6dbdd20aSAndroid Build Coastguard Worker      reorderable = false,
89*6dbdd20aSAndroid Build Coastguard Worker    } = this.attrs;
90*6dbdd20aSAndroid Build Coastguard Worker
91*6dbdd20aSAndroid Build Coastguard Worker    const error = trackRenderer?.getError();
92*6dbdd20aSAndroid Build Coastguard Worker
93*6dbdd20aSAndroid Build Coastguard Worker    const buttons = [
94*6dbdd20aSAndroid Build Coastguard Worker      SHOW_TRACK_DETAILS_BUTTON.get() &&
95*6dbdd20aSAndroid Build Coastguard Worker        renderTrackDetailsButton(node, trackRenderer?.desc),
96*6dbdd20aSAndroid Build Coastguard Worker      trackRenderer?.track.getTrackShellButtons?.(),
97*6dbdd20aSAndroid Build Coastguard Worker      node.removable && renderCloseButton(node),
98*6dbdd20aSAndroid Build Coastguard Worker      // We don't want summary tracks to be pinned as they rarely have
99*6dbdd20aSAndroid Build Coastguard Worker      // useful information.
100*6dbdd20aSAndroid Build Coastguard Worker      !node.isSummary && renderPinButton(node),
101*6dbdd20aSAndroid Build Coastguard Worker      this.renderAreaSelectionCheckbox(node),
102*6dbdd20aSAndroid Build Coastguard Worker      error && renderCrashButton(error, trackRenderer?.desc.pluginId),
103*6dbdd20aSAndroid Build Coastguard Worker    ];
104*6dbdd20aSAndroid Build Coastguard Worker
105*6dbdd20aSAndroid Build Coastguard Worker    let scrollIntoView = false;
106*6dbdd20aSAndroid Build Coastguard Worker    const tracks = this.attrs.trace.tracks;
107*6dbdd20aSAndroid Build Coastguard Worker    if (tracks.scrollToTrackNodeId === node.id) {
108*6dbdd20aSAndroid Build Coastguard Worker      tracks.scrollToTrackNodeId = undefined;
109*6dbdd20aSAndroid Build Coastguard Worker      scrollIntoView = true;
110*6dbdd20aSAndroid Build Coastguard Worker    }
111*6dbdd20aSAndroid Build Coastguard Worker
112*6dbdd20aSAndroid Build Coastguard Worker    return m(TrackWidget, {
113*6dbdd20aSAndroid Build Coastguard Worker      id: node.id,
114*6dbdd20aSAndroid Build Coastguard Worker      title: node.title,
115*6dbdd20aSAndroid Build Coastguard Worker      path: node.fullPath.join('/'),
116*6dbdd20aSAndroid Build Coastguard Worker      heightPx: this.heightPx,
117*6dbdd20aSAndroid Build Coastguard Worker      error: Boolean(trackRenderer?.getError()),
118*6dbdd20aSAndroid Build Coastguard Worker      chips: trackRenderer?.desc.chips,
119*6dbdd20aSAndroid Build Coastguard Worker      indentationLevel,
120*6dbdd20aSAndroid Build Coastguard Worker      topOffsetPx,
121*6dbdd20aSAndroid Build Coastguard Worker      buttons,
122*6dbdd20aSAndroid Build Coastguard Worker      revealOnCreate: revealOnCreate || scrollIntoView,
123*6dbdd20aSAndroid Build Coastguard Worker      collapsible: node.hasChildren,
124*6dbdd20aSAndroid Build Coastguard Worker      collapsed: node.collapsed,
125*6dbdd20aSAndroid Build Coastguard Worker      highlight: this.isHighlighted(node),
126*6dbdd20aSAndroid Build Coastguard Worker      isSummary: node.isSummary,
127*6dbdd20aSAndroid Build Coastguard Worker      reorderable,
128*6dbdd20aSAndroid Build Coastguard Worker      onToggleCollapsed: () => {
129*6dbdd20aSAndroid Build Coastguard Worker        node.hasChildren && node.toggleCollapsed();
130*6dbdd20aSAndroid Build Coastguard Worker      },
131*6dbdd20aSAndroid Build Coastguard Worker      onTrackContentMouseMove: (pos, bounds) => {
132*6dbdd20aSAndroid Build Coastguard Worker        const timescale = this.getTimescaleForBounds(bounds);
133*6dbdd20aSAndroid Build Coastguard Worker        trackRenderer?.track.onMouseMove?.({
134*6dbdd20aSAndroid Build Coastguard Worker          ...pos,
135*6dbdd20aSAndroid Build Coastguard Worker          timescale,
136*6dbdd20aSAndroid Build Coastguard Worker        });
137*6dbdd20aSAndroid Build Coastguard Worker        raf.scheduleCanvasRedraw();
138*6dbdd20aSAndroid Build Coastguard Worker      },
139*6dbdd20aSAndroid Build Coastguard Worker      onTrackContentMouseOut: () => {
140*6dbdd20aSAndroid Build Coastguard Worker        trackRenderer?.track.onMouseOut?.();
141*6dbdd20aSAndroid Build Coastguard Worker        raf.scheduleCanvasRedraw();
142*6dbdd20aSAndroid Build Coastguard Worker      },
143*6dbdd20aSAndroid Build Coastguard Worker      onTrackContentClick: (pos, bounds) => {
144*6dbdd20aSAndroid Build Coastguard Worker        const timescale = this.getTimescaleForBounds(bounds);
145*6dbdd20aSAndroid Build Coastguard Worker        raf.scheduleCanvasRedraw();
146*6dbdd20aSAndroid Build Coastguard Worker        return (
147*6dbdd20aSAndroid Build Coastguard Worker          trackRenderer?.track.onMouseClick?.({
148*6dbdd20aSAndroid Build Coastguard Worker            ...pos,
149*6dbdd20aSAndroid Build Coastguard Worker            timescale,
150*6dbdd20aSAndroid Build Coastguard Worker          }) ?? false
151*6dbdd20aSAndroid Build Coastguard Worker        );
152*6dbdd20aSAndroid Build Coastguard Worker      },
153*6dbdd20aSAndroid Build Coastguard Worker      onupdate: () => {
154*6dbdd20aSAndroid Build Coastguard Worker        trackRenderer?.track.onFullRedraw?.();
155*6dbdd20aSAndroid Build Coastguard Worker      },
156*6dbdd20aSAndroid Build Coastguard Worker      onMoveBefore: (nodeId: string) => {
157*6dbdd20aSAndroid Build Coastguard Worker        const targetNode = node.workspace?.getTrackById(nodeId);
158*6dbdd20aSAndroid Build Coastguard Worker        if (targetNode !== undefined) {
159*6dbdd20aSAndroid Build Coastguard Worker          // Insert the target node before this one
160*6dbdd20aSAndroid Build Coastguard Worker          targetNode.parent?.addChildBefore(targetNode, node);
161*6dbdd20aSAndroid Build Coastguard Worker        }
162*6dbdd20aSAndroid Build Coastguard Worker      },
163*6dbdd20aSAndroid Build Coastguard Worker      onMoveAfter: (nodeId: string) => {
164*6dbdd20aSAndroid Build Coastguard Worker        const targetNode = node.workspace?.getTrackById(nodeId);
165*6dbdd20aSAndroid Build Coastguard Worker        if (targetNode !== undefined) {
166*6dbdd20aSAndroid Build Coastguard Worker          // Insert the target node after this one
167*6dbdd20aSAndroid Build Coastguard Worker          targetNode.parent?.addChildAfter(targetNode, node);
168*6dbdd20aSAndroid Build Coastguard Worker        }
169*6dbdd20aSAndroid Build Coastguard Worker      },
170*6dbdd20aSAndroid Build Coastguard Worker    });
171*6dbdd20aSAndroid Build Coastguard Worker  }
172*6dbdd20aSAndroid Build Coastguard Worker
173*6dbdd20aSAndroid Build Coastguard Worker  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
174*6dbdd20aSAndroid Build Coastguard Worker    const {trackRenderer: tr, node} = this.attrs;
175*6dbdd20aSAndroid Build Coastguard Worker
176*6dbdd20aSAndroid Build Coastguard Worker    // Don't render if expanded and isSummary
177*6dbdd20aSAndroid Build Coastguard Worker    if (node.isSummary && node.expanded) {
178*6dbdd20aSAndroid Build Coastguard Worker      return;
179*6dbdd20aSAndroid Build Coastguard Worker    }
180*6dbdd20aSAndroid Build Coastguard Worker
181*6dbdd20aSAndroid Build Coastguard Worker    const trackSize = {
182*6dbdd20aSAndroid Build Coastguard Worker      width: size.width - TRACK_SHELL_WIDTH,
183*6dbdd20aSAndroid Build Coastguard Worker      height: size.height,
184*6dbdd20aSAndroid Build Coastguard Worker    };
185*6dbdd20aSAndroid Build Coastguard Worker
186*6dbdd20aSAndroid Build Coastguard Worker    using _ = canvasSave(ctx);
187*6dbdd20aSAndroid Build Coastguard Worker    ctx.translate(TRACK_SHELL_WIDTH, 0);
188*6dbdd20aSAndroid Build Coastguard Worker    canvasClip(ctx, 0, 0, trackSize.width, trackSize.height);
189*6dbdd20aSAndroid Build Coastguard Worker
190*6dbdd20aSAndroid Build Coastguard Worker    const visibleWindow = this.attrs.trace.timeline.visibleWindow;
191*6dbdd20aSAndroid Build Coastguard Worker    const timescale = new TimeScale(visibleWindow, {
192*6dbdd20aSAndroid Build Coastguard Worker      left: 0,
193*6dbdd20aSAndroid Build Coastguard Worker      right: trackSize.width,
194*6dbdd20aSAndroid Build Coastguard Worker    });
195*6dbdd20aSAndroid Build Coastguard Worker
196*6dbdd20aSAndroid Build Coastguard Worker    if (tr) {
197*6dbdd20aSAndroid Build Coastguard Worker      if (!tr.getError()) {
198*6dbdd20aSAndroid Build Coastguard Worker        const trackRenderCtx: TrackRenderContext = {
199*6dbdd20aSAndroid Build Coastguard Worker          trackUri: tr.desc.uri,
200*6dbdd20aSAndroid Build Coastguard Worker          visibleWindow,
201*6dbdd20aSAndroid Build Coastguard Worker          size: trackSize,
202*6dbdd20aSAndroid Build Coastguard Worker          resolution: calculateResolution(visibleWindow, trackSize.width),
203*6dbdd20aSAndroid Build Coastguard Worker          ctx,
204*6dbdd20aSAndroid Build Coastguard Worker          timescale,
205*6dbdd20aSAndroid Build Coastguard Worker        };
206*6dbdd20aSAndroid Build Coastguard Worker        tr.render(trackRenderCtx);
207*6dbdd20aSAndroid Build Coastguard Worker      }
208*6dbdd20aSAndroid Build Coastguard Worker    }
209*6dbdd20aSAndroid Build Coastguard Worker
210*6dbdd20aSAndroid Build Coastguard Worker    this.highlightIfTrackInAreaSelection(ctx, timescale, node, trackSize);
211*6dbdd20aSAndroid Build Coastguard Worker  }
212*6dbdd20aSAndroid Build Coastguard Worker
213*6dbdd20aSAndroid Build Coastguard Worker  getSliceVerticalBounds(depth: number): VerticalBounds | undefined {
214*6dbdd20aSAndroid Build Coastguard Worker    if (this.attrs.trackRenderer === undefined) {
215*6dbdd20aSAndroid Build Coastguard Worker      return undefined;
216*6dbdd20aSAndroid Build Coastguard Worker    }
217*6dbdd20aSAndroid Build Coastguard Worker    return this.attrs.trackRenderer.track.getSliceVerticalBounds?.(depth);
218*6dbdd20aSAndroid Build Coastguard Worker  }
219*6dbdd20aSAndroid Build Coastguard Worker
220*6dbdd20aSAndroid Build Coastguard Worker  private getTimescaleForBounds(bounds: Bounds2D) {
221*6dbdd20aSAndroid Build Coastguard Worker    const timeWindow = this.attrs.trace.timeline.visibleWindow;
222*6dbdd20aSAndroid Build Coastguard Worker    return new TimeScale(timeWindow, {
223*6dbdd20aSAndroid Build Coastguard Worker      left: 0,
224*6dbdd20aSAndroid Build Coastguard Worker      right: bounds.right - bounds.left,
225*6dbdd20aSAndroid Build Coastguard Worker    });
226*6dbdd20aSAndroid Build Coastguard Worker  }
227*6dbdd20aSAndroid Build Coastguard Worker
228*6dbdd20aSAndroid Build Coastguard Worker  private isHighlighted(node: TrackNode) {
229*6dbdd20aSAndroid Build Coastguard Worker    // The track should be highlighted if the current search result matches this
230*6dbdd20aSAndroid Build Coastguard Worker    // track or one of its children.
231*6dbdd20aSAndroid Build Coastguard Worker    const searchIndex = this.attrs.trace.search.resultIndex;
232*6dbdd20aSAndroid Build Coastguard Worker    const searchResults = this.attrs.trace.search.searchResults;
233*6dbdd20aSAndroid Build Coastguard Worker
234*6dbdd20aSAndroid Build Coastguard Worker    if (searchIndex !== -1 && searchResults !== undefined) {
235*6dbdd20aSAndroid Build Coastguard Worker      const uri = searchResults.trackUris[searchIndex];
236*6dbdd20aSAndroid Build Coastguard Worker      // Highlight if this or any children match the search results
237*6dbdd20aSAndroid Build Coastguard Worker      if (
238*6dbdd20aSAndroid Build Coastguard Worker        uri === node.uri ||
239*6dbdd20aSAndroid Build Coastguard Worker        node.flatTracksOrdered.find((t) => t.uri === uri)
240*6dbdd20aSAndroid Build Coastguard Worker      ) {
241*6dbdd20aSAndroid Build Coastguard Worker        return true;
242*6dbdd20aSAndroid Build Coastguard Worker      }
243*6dbdd20aSAndroid Build Coastguard Worker    }
244*6dbdd20aSAndroid Build Coastguard Worker
245*6dbdd20aSAndroid Build Coastguard Worker    const curSelection = this.attrs.trace.selection;
246*6dbdd20aSAndroid Build Coastguard Worker    if (
247*6dbdd20aSAndroid Build Coastguard Worker      curSelection.selection.kind === 'track' &&
248*6dbdd20aSAndroid Build Coastguard Worker      curSelection.selection.trackUri === node.uri
249*6dbdd20aSAndroid Build Coastguard Worker    ) {
250*6dbdd20aSAndroid Build Coastguard Worker      return true;
251*6dbdd20aSAndroid Build Coastguard Worker    }
252*6dbdd20aSAndroid Build Coastguard Worker
253*6dbdd20aSAndroid Build Coastguard Worker    return false;
254*6dbdd20aSAndroid Build Coastguard Worker  }
255*6dbdd20aSAndroid Build Coastguard Worker
256*6dbdd20aSAndroid Build Coastguard Worker  private highlightIfTrackInAreaSelection(
257*6dbdd20aSAndroid Build Coastguard Worker    ctx: CanvasRenderingContext2D,
258*6dbdd20aSAndroid Build Coastguard Worker    timescale: TimeScale,
259*6dbdd20aSAndroid Build Coastguard Worker    node: TrackNode,
260*6dbdd20aSAndroid Build Coastguard Worker    size: Size2D,
261*6dbdd20aSAndroid Build Coastguard Worker  ) {
262*6dbdd20aSAndroid Build Coastguard Worker    const selection = this.attrs.trace.selection.selection;
263*6dbdd20aSAndroid Build Coastguard Worker    if (selection.kind !== 'area') {
264*6dbdd20aSAndroid Build Coastguard Worker      return;
265*6dbdd20aSAndroid Build Coastguard Worker    }
266*6dbdd20aSAndroid Build Coastguard Worker
267*6dbdd20aSAndroid Build Coastguard Worker    const tracksWithUris = node.flatTracks.filter(
268*6dbdd20aSAndroid Build Coastguard Worker      (t) => t.uri !== undefined,
269*6dbdd20aSAndroid Build Coastguard Worker    ) as ReadonlyArray<RequiredField<TrackNode, 'uri'>>;
270*6dbdd20aSAndroid Build Coastguard Worker
271*6dbdd20aSAndroid Build Coastguard Worker    let selected = false;
272*6dbdd20aSAndroid Build Coastguard Worker    if (node.isSummary) {
273*6dbdd20aSAndroid Build Coastguard Worker      selected = tracksWithUris.some((track) =>
274*6dbdd20aSAndroid Build Coastguard Worker        selection.trackUris.includes(track.uri),
275*6dbdd20aSAndroid Build Coastguard Worker      );
276*6dbdd20aSAndroid Build Coastguard Worker    } else {
277*6dbdd20aSAndroid Build Coastguard Worker      if (node.uri) {
278*6dbdd20aSAndroid Build Coastguard Worker        selected = selection.trackUris.includes(node.uri);
279*6dbdd20aSAndroid Build Coastguard Worker      }
280*6dbdd20aSAndroid Build Coastguard Worker    }
281*6dbdd20aSAndroid Build Coastguard Worker
282*6dbdd20aSAndroid Build Coastguard Worker    if (selected) {
283*6dbdd20aSAndroid Build Coastguard Worker      const selectedAreaDuration = selection.end - selection.start;
284*6dbdd20aSAndroid Build Coastguard Worker      ctx.fillStyle = SELECTION_FILL_COLOR;
285*6dbdd20aSAndroid Build Coastguard Worker      ctx.fillRect(
286*6dbdd20aSAndroid Build Coastguard Worker        timescale.timeToPx(selection.start),
287*6dbdd20aSAndroid Build Coastguard Worker        0,
288*6dbdd20aSAndroid Build Coastguard Worker        timescale.durationToPx(selectedAreaDuration),
289*6dbdd20aSAndroid Build Coastguard Worker        size.height,
290*6dbdd20aSAndroid Build Coastguard Worker      );
291*6dbdd20aSAndroid Build Coastguard Worker    }
292*6dbdd20aSAndroid Build Coastguard Worker  }
293*6dbdd20aSAndroid Build Coastguard Worker
294*6dbdd20aSAndroid Build Coastguard Worker  private renderAreaSelectionCheckbox(node: TrackNode): m.Children {
295*6dbdd20aSAndroid Build Coastguard Worker    const selectionManager = this.attrs.trace.selection;
296*6dbdd20aSAndroid Build Coastguard Worker    const selection = selectionManager.selection;
297*6dbdd20aSAndroid Build Coastguard Worker    if (selection.kind === 'area') {
298*6dbdd20aSAndroid Build Coastguard Worker      if (node.isSummary) {
299*6dbdd20aSAndroid Build Coastguard Worker        const tracksWithUris = node.flatTracks.filter(
300*6dbdd20aSAndroid Build Coastguard Worker          (t) => t.uri !== undefined,
301*6dbdd20aSAndroid Build Coastguard Worker        ) as ReadonlyArray<RequiredField<TrackNode, 'uri'>>;
302*6dbdd20aSAndroid Build Coastguard Worker        // Check if any nodes within are selected
303*6dbdd20aSAndroid Build Coastguard Worker        const childTracksInSelection = tracksWithUris.map((t) =>
304*6dbdd20aSAndroid Build Coastguard Worker          selection.trackUris.includes(t.uri),
305*6dbdd20aSAndroid Build Coastguard Worker        );
306*6dbdd20aSAndroid Build Coastguard Worker        if (childTracksInSelection.every((b) => b)) {
307*6dbdd20aSAndroid Build Coastguard Worker          return m(Button, {
308*6dbdd20aSAndroid Build Coastguard Worker            onclick: (e: MouseEvent) => {
309*6dbdd20aSAndroid Build Coastguard Worker              const uris = tracksWithUris.map((t) => t.uri);
310*6dbdd20aSAndroid Build Coastguard Worker              selectionManager.toggleGroupAreaSelection(uris);
311*6dbdd20aSAndroid Build Coastguard Worker              e.stopPropagation();
312*6dbdd20aSAndroid Build Coastguard Worker            },
313*6dbdd20aSAndroid Build Coastguard Worker            compact: true,
314*6dbdd20aSAndroid Build Coastguard Worker            icon: Icons.Checkbox,
315*6dbdd20aSAndroid Build Coastguard Worker            title: 'Remove child tracks from selection',
316*6dbdd20aSAndroid Build Coastguard Worker          });
317*6dbdd20aSAndroid Build Coastguard Worker        } else if (childTracksInSelection.some((b) => b)) {
318*6dbdd20aSAndroid Build Coastguard Worker          return m(Button, {
319*6dbdd20aSAndroid Build Coastguard Worker            onclick: (e: MouseEvent) => {
320*6dbdd20aSAndroid Build Coastguard Worker              const uris = tracksWithUris.map((t) => t.uri);
321*6dbdd20aSAndroid Build Coastguard Worker              selectionManager.toggleGroupAreaSelection(uris);
322*6dbdd20aSAndroid Build Coastguard Worker              e.stopPropagation();
323*6dbdd20aSAndroid Build Coastguard Worker            },
324*6dbdd20aSAndroid Build Coastguard Worker            compact: true,
325*6dbdd20aSAndroid Build Coastguard Worker            icon: Icons.IndeterminateCheckbox,
326*6dbdd20aSAndroid Build Coastguard Worker            title: 'Add remaining child tracks to selection',
327*6dbdd20aSAndroid Build Coastguard Worker          });
328*6dbdd20aSAndroid Build Coastguard Worker        } else {
329*6dbdd20aSAndroid Build Coastguard Worker          return m(Button, {
330*6dbdd20aSAndroid Build Coastguard Worker            onclick: (e: MouseEvent) => {
331*6dbdd20aSAndroid Build Coastguard Worker              const uris = tracksWithUris.map((t) => t.uri);
332*6dbdd20aSAndroid Build Coastguard Worker              selectionManager.toggleGroupAreaSelection(uris);
333*6dbdd20aSAndroid Build Coastguard Worker              e.stopPropagation();
334*6dbdd20aSAndroid Build Coastguard Worker            },
335*6dbdd20aSAndroid Build Coastguard Worker            compact: true,
336*6dbdd20aSAndroid Build Coastguard Worker            icon: Icons.BlankCheckbox,
337*6dbdd20aSAndroid Build Coastguard Worker            title: 'Add child tracks to selection',
338*6dbdd20aSAndroid Build Coastguard Worker          });
339*6dbdd20aSAndroid Build Coastguard Worker        }
340*6dbdd20aSAndroid Build Coastguard Worker      } else {
341*6dbdd20aSAndroid Build Coastguard Worker        const nodeUri = node.uri;
342*6dbdd20aSAndroid Build Coastguard Worker        if (nodeUri) {
343*6dbdd20aSAndroid Build Coastguard Worker          return (
344*6dbdd20aSAndroid Build Coastguard Worker            selection.kind === 'area' &&
345*6dbdd20aSAndroid Build Coastguard Worker            m(Button, {
346*6dbdd20aSAndroid Build Coastguard Worker              onclick: (e: MouseEvent) => {
347*6dbdd20aSAndroid Build Coastguard Worker                selectionManager.toggleTrackAreaSelection(nodeUri);
348*6dbdd20aSAndroid Build Coastguard Worker                e.stopPropagation();
349*6dbdd20aSAndroid Build Coastguard Worker              },
350*6dbdd20aSAndroid Build Coastguard Worker              compact: true,
351*6dbdd20aSAndroid Build Coastguard Worker              ...(selection.trackUris.includes(nodeUri)
352*6dbdd20aSAndroid Build Coastguard Worker                ? {icon: Icons.Checkbox, title: 'Remove track'}
353*6dbdd20aSAndroid Build Coastguard Worker                : {icon: Icons.BlankCheckbox, title: 'Add track to selection'}),
354*6dbdd20aSAndroid Build Coastguard Worker            })
355*6dbdd20aSAndroid Build Coastguard Worker          );
356*6dbdd20aSAndroid Build Coastguard Worker        }
357*6dbdd20aSAndroid Build Coastguard Worker      }
358*6dbdd20aSAndroid Build Coastguard Worker    }
359*6dbdd20aSAndroid Build Coastguard Worker    return undefined;
360*6dbdd20aSAndroid Build Coastguard Worker  }
361*6dbdd20aSAndroid Build Coastguard Worker}
362*6dbdd20aSAndroid Build Coastguard Worker
363*6dbdd20aSAndroid Build Coastguard Workerfunction renderCrashButton(error: Error, pluginId?: string) {
364*6dbdd20aSAndroid Build Coastguard Worker  return m(
365*6dbdd20aSAndroid Build Coastguard Worker    Popup,
366*6dbdd20aSAndroid Build Coastguard Worker    {
367*6dbdd20aSAndroid Build Coastguard Worker      trigger: m(Button, {
368*6dbdd20aSAndroid Build Coastguard Worker        icon: Icons.Crashed,
369*6dbdd20aSAndroid Build Coastguard Worker        compact: true,
370*6dbdd20aSAndroid Build Coastguard Worker      }),
371*6dbdd20aSAndroid Build Coastguard Worker    },
372*6dbdd20aSAndroid Build Coastguard Worker    m(
373*6dbdd20aSAndroid Build Coastguard Worker      '.pf-track-crash-popup',
374*6dbdd20aSAndroid Build Coastguard Worker      m('span', 'This track has crashed.'),
375*6dbdd20aSAndroid Build Coastguard Worker      pluginId && m('span', `Owning plugin: ${pluginId}`),
376*6dbdd20aSAndroid Build Coastguard Worker      m(Button, {
377*6dbdd20aSAndroid Build Coastguard Worker        label: 'View & Report Crash',
378*6dbdd20aSAndroid Build Coastguard Worker        intent: Intent.Primary,
379*6dbdd20aSAndroid Build Coastguard Worker        className: Popup.DISMISS_POPUP_GROUP_CLASS,
380*6dbdd20aSAndroid Build Coastguard Worker        onclick: () => {
381*6dbdd20aSAndroid Build Coastguard Worker          throw error;
382*6dbdd20aSAndroid Build Coastguard Worker        },
383*6dbdd20aSAndroid Build Coastguard Worker      }),
384*6dbdd20aSAndroid Build Coastguard Worker      // TODO(stevegolton): In the future we should provide a quick way to
385*6dbdd20aSAndroid Build Coastguard Worker      // disable the plugin, or provide a link to the plugin page, but this
386*6dbdd20aSAndroid Build Coastguard Worker      // relies on the plugin page being fully functional.
387*6dbdd20aSAndroid Build Coastguard Worker    ),
388*6dbdd20aSAndroid Build Coastguard Worker  );
389*6dbdd20aSAndroid Build Coastguard Worker}
390*6dbdd20aSAndroid Build Coastguard Worker
391*6dbdd20aSAndroid Build Coastguard Workerfunction renderCloseButton(node: TrackNode) {
392*6dbdd20aSAndroid Build Coastguard Worker  return m(Button, {
393*6dbdd20aSAndroid Build Coastguard Worker    onclick: (e) => {
394*6dbdd20aSAndroid Build Coastguard Worker      node.remove();
395*6dbdd20aSAndroid Build Coastguard Worker      e.stopPropagation();
396*6dbdd20aSAndroid Build Coastguard Worker    },
397*6dbdd20aSAndroid Build Coastguard Worker    icon: Icons.Close,
398*6dbdd20aSAndroid Build Coastguard Worker    title: 'Close track',
399*6dbdd20aSAndroid Build Coastguard Worker    compact: true,
400*6dbdd20aSAndroid Build Coastguard Worker  });
401*6dbdd20aSAndroid Build Coastguard Worker}
402*6dbdd20aSAndroid Build Coastguard Worker
403*6dbdd20aSAndroid Build Coastguard Workerfunction renderPinButton(node: TrackNode): m.Children {
404*6dbdd20aSAndroid Build Coastguard Worker  const isPinned = node.isPinned;
405*6dbdd20aSAndroid Build Coastguard Worker  return m(Button, {
406*6dbdd20aSAndroid Build Coastguard Worker    className: classNames(!isPinned && 'pf-visible-on-hover'),
407*6dbdd20aSAndroid Build Coastguard Worker    onclick: (e) => {
408*6dbdd20aSAndroid Build Coastguard Worker      isPinned ? node.unpin() : node.pin();
409*6dbdd20aSAndroid Build Coastguard Worker      e.stopPropagation();
410*6dbdd20aSAndroid Build Coastguard Worker    },
411*6dbdd20aSAndroid Build Coastguard Worker    icon: Icons.Pin,
412*6dbdd20aSAndroid Build Coastguard Worker    iconFilled: isPinned,
413*6dbdd20aSAndroid Build Coastguard Worker    title: isPinned ? 'Unpin' : 'Pin to top',
414*6dbdd20aSAndroid Build Coastguard Worker    compact: true,
415*6dbdd20aSAndroid Build Coastguard Worker  });
416*6dbdd20aSAndroid Build Coastguard Worker}
417*6dbdd20aSAndroid Build Coastguard Worker
418*6dbdd20aSAndroid Build Coastguard Workerfunction renderTrackDetailsButton(
419*6dbdd20aSAndroid Build Coastguard Worker  node: TrackNode,
420*6dbdd20aSAndroid Build Coastguard Worker  td?: TrackDescriptor,
421*6dbdd20aSAndroid Build Coastguard Worker): m.Children {
422*6dbdd20aSAndroid Build Coastguard Worker  let parent = node.parent;
423*6dbdd20aSAndroid Build Coastguard Worker  let fullPath: m.ChildArray = [node.title];
424*6dbdd20aSAndroid Build Coastguard Worker  while (parent && parent instanceof TrackNode) {
425*6dbdd20aSAndroid Build Coastguard Worker    fullPath = [parent.title, ' \u2023 ', ...fullPath];
426*6dbdd20aSAndroid Build Coastguard Worker    parent = parent.parent;
427*6dbdd20aSAndroid Build Coastguard Worker  }
428*6dbdd20aSAndroid Build Coastguard Worker  return m(
429*6dbdd20aSAndroid Build Coastguard Worker    Popup,
430*6dbdd20aSAndroid Build Coastguard Worker    {
431*6dbdd20aSAndroid Build Coastguard Worker      trigger: m(Button, {
432*6dbdd20aSAndroid Build Coastguard Worker        className: 'pf-visible-on-hover',
433*6dbdd20aSAndroid Build Coastguard Worker        icon: 'info',
434*6dbdd20aSAndroid Build Coastguard Worker        title: 'Show track details',
435*6dbdd20aSAndroid Build Coastguard Worker        compact: true,
436*6dbdd20aSAndroid Build Coastguard Worker      }),
437*6dbdd20aSAndroid Build Coastguard Worker      position: PopupPosition.Bottom,
438*6dbdd20aSAndroid Build Coastguard Worker    },
439*6dbdd20aSAndroid Build Coastguard Worker    m(
440*6dbdd20aSAndroid Build Coastguard Worker      '.pf-track-details-dropdown',
441*6dbdd20aSAndroid Build Coastguard Worker      m(
442*6dbdd20aSAndroid Build Coastguard Worker        Tree,
443*6dbdd20aSAndroid Build Coastguard Worker        m(TreeNode, {left: 'Track Node ID', right: node.id}),
444*6dbdd20aSAndroid Build Coastguard Worker        m(TreeNode, {left: 'Collapsed', right: `${node.collapsed}`}),
445*6dbdd20aSAndroid Build Coastguard Worker        m(TreeNode, {left: 'URI', right: node.uri}),
446*6dbdd20aSAndroid Build Coastguard Worker        m(TreeNode, {left: 'Is Summary Track', right: `${node.isSummary}`}),
447*6dbdd20aSAndroid Build Coastguard Worker        m(TreeNode, {
448*6dbdd20aSAndroid Build Coastguard Worker          left: 'SortOrder',
449*6dbdd20aSAndroid Build Coastguard Worker          right: node.sortOrder ?? '0 (undefined)',
450*6dbdd20aSAndroid Build Coastguard Worker        }),
451*6dbdd20aSAndroid Build Coastguard Worker        m(TreeNode, {left: 'Path', right: fullPath}),
452*6dbdd20aSAndroid Build Coastguard Worker        m(TreeNode, {left: 'Title', right: node.title}),
453*6dbdd20aSAndroid Build Coastguard Worker        m(TreeNode, {
454*6dbdd20aSAndroid Build Coastguard Worker          left: 'Workspace',
455*6dbdd20aSAndroid Build Coastguard Worker          right: node.workspace?.title ?? '[no workspace]',
456*6dbdd20aSAndroid Build Coastguard Worker        }),
457*6dbdd20aSAndroid Build Coastguard Worker        td && m(TreeNode, {left: 'Plugin ID', right: td.pluginId}),
458*6dbdd20aSAndroid Build Coastguard Worker        td &&
459*6dbdd20aSAndroid Build Coastguard Worker          m(
460*6dbdd20aSAndroid Build Coastguard Worker            TreeNode,
461*6dbdd20aSAndroid Build Coastguard Worker            {left: 'Tags'},
462*6dbdd20aSAndroid Build Coastguard Worker            td.tags &&
463*6dbdd20aSAndroid Build Coastguard Worker              Object.entries(td.tags).map(([key, value]) => {
464*6dbdd20aSAndroid Build Coastguard Worker                return m(TreeNode, {left: key, right: value?.toString()});
465*6dbdd20aSAndroid Build Coastguard Worker              }),
466*6dbdd20aSAndroid Build Coastguard Worker          ),
467*6dbdd20aSAndroid Build Coastguard Worker      ),
468*6dbdd20aSAndroid Build Coastguard Worker    ),
469*6dbdd20aSAndroid Build Coastguard Worker  );
470*6dbdd20aSAndroid Build Coastguard Worker}
471