xref: /aosp_15_r20/external/perfetto/ui/src/widgets/track_widget.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2024 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 {classNames} from '../base/classnames';
17*6dbdd20aSAndroid Build Coastguard Workerimport {currentTargetOffset} from '../base/dom_utils';
18*6dbdd20aSAndroid Build Coastguard Workerimport {Bounds2D, Point2D, Vector2D} from '../base/geom';
19*6dbdd20aSAndroid Build Coastguard Workerimport {Icons} from '../base/semantic_icons';
20*6dbdd20aSAndroid Build Coastguard Workerimport {ButtonBar} from './button';
21*6dbdd20aSAndroid Build Coastguard Workerimport {Chip, ChipBar} from './chip';
22*6dbdd20aSAndroid Build Coastguard Workerimport {Icon} from './icon';
23*6dbdd20aSAndroid Build Coastguard Workerimport {MiddleEllipsis} from './middle_ellipsis';
24*6dbdd20aSAndroid Build Coastguard Workerimport {clamp} from '../base/math_utils';
25*6dbdd20aSAndroid Build Coastguard Worker
26*6dbdd20aSAndroid Build Coastguard Worker/**
27*6dbdd20aSAndroid Build Coastguard Worker * The TrackWidget defines the look and style of a track.
28*6dbdd20aSAndroid Build Coastguard Worker *
29*6dbdd20aSAndroid Build Coastguard Worker * ┌──────────────────────────────────────────────────────────────────┐
30*6dbdd20aSAndroid Build Coastguard Worker * │pf-track (grid)                                                   │
31*6dbdd20aSAndroid Build Coastguard Worker * │┌─────────────────────────────────────────┐┌─────────────────────┐│
32*6dbdd20aSAndroid Build Coastguard Worker * ││pf-track-shell                           ││pf-track-content     ││
33*6dbdd20aSAndroid Build Coastguard Worker * ││┌───────────────────────────────────────┐││                     ││
34*6dbdd20aSAndroid Build Coastguard Worker * │││pf-track-menubar (sticky)              │││                     ││
35*6dbdd20aSAndroid Build Coastguard Worker * │││┌───────────────┐┌────────────────────┐│││                     ││
36*6dbdd20aSAndroid Build Coastguard Worker * ││││pf-track-title ││pf-track-buttons    ││││                     ││
37*6dbdd20aSAndroid Build Coastguard Worker * │││└───────────────┘└────────────────────┘│││                     ││
38*6dbdd20aSAndroid Build Coastguard Worker * ││└───────────────────────────────────────┘││                     ││
39*6dbdd20aSAndroid Build Coastguard Worker * │└─────────────────────────────────────────┘└─────────────────────┘│
40*6dbdd20aSAndroid Build Coastguard Worker * └──────────────────────────────────────────────────────────────────┘
41*6dbdd20aSAndroid Build Coastguard Worker */
42*6dbdd20aSAndroid Build Coastguard Worker
43*6dbdd20aSAndroid Build Coastguard Workerexport interface TrackComponentAttrs {
44*6dbdd20aSAndroid Build Coastguard Worker  // The title of this track.
45*6dbdd20aSAndroid Build Coastguard Worker  readonly title: string;
46*6dbdd20aSAndroid Build Coastguard Worker
47*6dbdd20aSAndroid Build Coastguard Worker  // The full path to this track.
48*6dbdd20aSAndroid Build Coastguard Worker  readonly path?: string;
49*6dbdd20aSAndroid Build Coastguard Worker
50*6dbdd20aSAndroid Build Coastguard Worker  // Show dropdown arrow and make clickable. Defaults to false.
51*6dbdd20aSAndroid Build Coastguard Worker  readonly collapsible?: boolean;
52*6dbdd20aSAndroid Build Coastguard Worker
53*6dbdd20aSAndroid Build Coastguard Worker  // Show an up or down dropdown arrow.
54*6dbdd20aSAndroid Build Coastguard Worker  readonly collapsed: boolean;
55*6dbdd20aSAndroid Build Coastguard Worker
56*6dbdd20aSAndroid Build Coastguard Worker  // Height of the track in pixels. All tracks have a fixed height.
57*6dbdd20aSAndroid Build Coastguard Worker  readonly heightPx: number;
58*6dbdd20aSAndroid Build Coastguard Worker
59*6dbdd20aSAndroid Build Coastguard Worker  // Optional buttons to place on the RHS of the track shell.
60*6dbdd20aSAndroid Build Coastguard Worker  readonly buttons?: m.Children;
61*6dbdd20aSAndroid Build Coastguard Worker
62*6dbdd20aSAndroid Build Coastguard Worker  // Optional list of chips to display after the track title.
63*6dbdd20aSAndroid Build Coastguard Worker  readonly chips?: ReadonlyArray<string>;
64*6dbdd20aSAndroid Build Coastguard Worker
65*6dbdd20aSAndroid Build Coastguard Worker  // Render this track in error colours.
66*6dbdd20aSAndroid Build Coastguard Worker  readonly error?: boolean;
67*6dbdd20aSAndroid Build Coastguard Worker
68*6dbdd20aSAndroid Build Coastguard Worker  // The integer indentation level of this track. If omitted, defaults to 0.
69*6dbdd20aSAndroid Build Coastguard Worker  readonly indentationLevel?: number;
70*6dbdd20aSAndroid Build Coastguard Worker
71*6dbdd20aSAndroid Build Coastguard Worker  // Track titles are sticky. This is the offset in pixels from the top of the
72*6dbdd20aSAndroid Build Coastguard Worker  // scrolling parent. Defaults to 0.
73*6dbdd20aSAndroid Build Coastguard Worker  readonly topOffsetPx?: number;
74*6dbdd20aSAndroid Build Coastguard Worker
75*6dbdd20aSAndroid Build Coastguard Worker  // Issues a scrollTo() on this DOM element at creation time. Default: false.
76*6dbdd20aSAndroid Build Coastguard Worker  readonly revealOnCreate?: boolean;
77*6dbdd20aSAndroid Build Coastguard Worker
78*6dbdd20aSAndroid Build Coastguard Worker  // Called when arrow clicked.
79*6dbdd20aSAndroid Build Coastguard Worker  readonly onToggleCollapsed?: () => void;
80*6dbdd20aSAndroid Build Coastguard Worker
81*6dbdd20aSAndroid Build Coastguard Worker  // Style the component differently if it has children.
82*6dbdd20aSAndroid Build Coastguard Worker  readonly isSummary?: boolean;
83*6dbdd20aSAndroid Build Coastguard Worker
84*6dbdd20aSAndroid Build Coastguard Worker  // HTML id applied to the root element.
85*6dbdd20aSAndroid Build Coastguard Worker  readonly id: string;
86*6dbdd20aSAndroid Build Coastguard Worker
87*6dbdd20aSAndroid Build Coastguard Worker  // Whether to highlight the track or not.
88*6dbdd20aSAndroid Build Coastguard Worker  readonly highlight?: boolean;
89*6dbdd20aSAndroid Build Coastguard Worker
90*6dbdd20aSAndroid Build Coastguard Worker  // Whether the shell should be draggable and emit drag/drop events.
91*6dbdd20aSAndroid Build Coastguard Worker  readonly reorderable?: boolean;
92*6dbdd20aSAndroid Build Coastguard Worker
93*6dbdd20aSAndroid Build Coastguard Worker  // Mouse events.
94*6dbdd20aSAndroid Build Coastguard Worker  readonly onTrackContentMouseMove?: (
95*6dbdd20aSAndroid Build Coastguard Worker    pos: Point2D,
96*6dbdd20aSAndroid Build Coastguard Worker    contentSize: Bounds2D,
97*6dbdd20aSAndroid Build Coastguard Worker  ) => void;
98*6dbdd20aSAndroid Build Coastguard Worker  readonly onTrackContentMouseOut?: () => void;
99*6dbdd20aSAndroid Build Coastguard Worker  readonly onTrackContentClick?: (
100*6dbdd20aSAndroid Build Coastguard Worker    pos: Point2D,
101*6dbdd20aSAndroid Build Coastguard Worker    contentSize: Bounds2D,
102*6dbdd20aSAndroid Build Coastguard Worker  ) => boolean;
103*6dbdd20aSAndroid Build Coastguard Worker
104*6dbdd20aSAndroid Build Coastguard Worker  // If reorderable, these functions will be called when track shells are
105*6dbdd20aSAndroid Build Coastguard Worker  // dragged and dropped.
106*6dbdd20aSAndroid Build Coastguard Worker  readonly onMoveBefore?: (nodeId: string) => void;
107*6dbdd20aSAndroid Build Coastguard Worker  readonly onMoveAfter?: (nodeId: string) => void;
108*6dbdd20aSAndroid Build Coastguard Worker}
109*6dbdd20aSAndroid Build Coastguard Worker
110*6dbdd20aSAndroid Build Coastguard Workerconst TRACK_HEIGHT_MIN_PX = 18;
111*6dbdd20aSAndroid Build Coastguard Workerconst INDENTATION_LEVEL_MAX = 16;
112*6dbdd20aSAndroid Build Coastguard Worker
113*6dbdd20aSAndroid Build Coastguard Workerexport class TrackWidget implements m.ClassComponent<TrackComponentAttrs> {
114*6dbdd20aSAndroid Build Coastguard Worker  view({attrs}: m.CVnode<TrackComponentAttrs>) {
115*6dbdd20aSAndroid Build Coastguard Worker    const {
116*6dbdd20aSAndroid Build Coastguard Worker      indentationLevel = 0,
117*6dbdd20aSAndroid Build Coastguard Worker      collapsible,
118*6dbdd20aSAndroid Build Coastguard Worker      collapsed,
119*6dbdd20aSAndroid Build Coastguard Worker      highlight,
120*6dbdd20aSAndroid Build Coastguard Worker      heightPx,
121*6dbdd20aSAndroid Build Coastguard Worker      id,
122*6dbdd20aSAndroid Build Coastguard Worker      isSummary,
123*6dbdd20aSAndroid Build Coastguard Worker    } = attrs;
124*6dbdd20aSAndroid Build Coastguard Worker
125*6dbdd20aSAndroid Build Coastguard Worker    const trackHeight = Math.max(heightPx, TRACK_HEIGHT_MIN_PX);
126*6dbdd20aSAndroid Build Coastguard Worker    const expanded = collapsible && !collapsed;
127*6dbdd20aSAndroid Build Coastguard Worker
128*6dbdd20aSAndroid Build Coastguard Worker    return m(
129*6dbdd20aSAndroid Build Coastguard Worker      '.pf-track',
130*6dbdd20aSAndroid Build Coastguard Worker      {
131*6dbdd20aSAndroid Build Coastguard Worker        id,
132*6dbdd20aSAndroid Build Coastguard Worker        className: classNames(
133*6dbdd20aSAndroid Build Coastguard Worker          expanded && 'pf-expanded',
134*6dbdd20aSAndroid Build Coastguard Worker          highlight && 'pf-highlight',
135*6dbdd20aSAndroid Build Coastguard Worker          isSummary && 'pf-is-summary',
136*6dbdd20aSAndroid Build Coastguard Worker        ),
137*6dbdd20aSAndroid Build Coastguard Worker        style: {
138*6dbdd20aSAndroid Build Coastguard Worker          // Note: Sub-pixel track heights can mess with sticky elements.
139*6dbdd20aSAndroid Build Coastguard Worker          // Round up to the nearest integer number of pixels.
140*6dbdd20aSAndroid Build Coastguard Worker          '--indent': clamp(indentationLevel, 0, INDENTATION_LEVEL_MAX),
141*6dbdd20aSAndroid Build Coastguard Worker          'height': `${Math.ceil(trackHeight)}px`,
142*6dbdd20aSAndroid Build Coastguard Worker        },
143*6dbdd20aSAndroid Build Coastguard Worker      },
144*6dbdd20aSAndroid Build Coastguard Worker      this.renderShell(attrs),
145*6dbdd20aSAndroid Build Coastguard Worker      this.renderContent(attrs),
146*6dbdd20aSAndroid Build Coastguard Worker    );
147*6dbdd20aSAndroid Build Coastguard Worker  }
148*6dbdd20aSAndroid Build Coastguard Worker
149*6dbdd20aSAndroid Build Coastguard Worker  oncreate(vnode: m.VnodeDOM<TrackComponentAttrs>) {
150*6dbdd20aSAndroid Build Coastguard Worker    this.onupdate(vnode);
151*6dbdd20aSAndroid Build Coastguard Worker
152*6dbdd20aSAndroid Build Coastguard Worker    if (vnode.attrs.revealOnCreate) {
153*6dbdd20aSAndroid Build Coastguard Worker      vnode.dom.scrollIntoView({behavior: 'smooth', block: 'nearest'});
154*6dbdd20aSAndroid Build Coastguard Worker    }
155*6dbdd20aSAndroid Build Coastguard Worker  }
156*6dbdd20aSAndroid Build Coastguard Worker
157*6dbdd20aSAndroid Build Coastguard Worker  onupdate(vnode: m.VnodeDOM<TrackComponentAttrs>) {
158*6dbdd20aSAndroid Build Coastguard Worker    this.decidePopupRequired(vnode.dom);
159*6dbdd20aSAndroid Build Coastguard Worker  }
160*6dbdd20aSAndroid Build Coastguard Worker
161*6dbdd20aSAndroid Build Coastguard Worker  // Works out whether to display a title popup on hover, based on whether the
162*6dbdd20aSAndroid Build Coastguard Worker  // current title is truncated.
163*6dbdd20aSAndroid Build Coastguard Worker  private decidePopupRequired(dom: Element) {
164*6dbdd20aSAndroid Build Coastguard Worker    const popupTitleElement = dom.querySelector(
165*6dbdd20aSAndroid Build Coastguard Worker      '.pf-track-title-popup',
166*6dbdd20aSAndroid Build Coastguard Worker    ) as HTMLElement;
167*6dbdd20aSAndroid Build Coastguard Worker    const truncatedTitleElement = dom.querySelector(
168*6dbdd20aSAndroid Build Coastguard Worker      '.pf-middle-ellipsis',
169*6dbdd20aSAndroid Build Coastguard Worker    ) as HTMLElement;
170*6dbdd20aSAndroid Build Coastguard Worker
171*6dbdd20aSAndroid Build Coastguard Worker    if (popupTitleElement.clientWidth > truncatedTitleElement.clientWidth) {
172*6dbdd20aSAndroid Build Coastguard Worker      popupTitleElement.classList.add('pf-visible');
173*6dbdd20aSAndroid Build Coastguard Worker    } else {
174*6dbdd20aSAndroid Build Coastguard Worker      popupTitleElement.classList.remove('pf-visible');
175*6dbdd20aSAndroid Build Coastguard Worker    }
176*6dbdd20aSAndroid Build Coastguard Worker  }
177*6dbdd20aSAndroid Build Coastguard Worker
178*6dbdd20aSAndroid Build Coastguard Worker  private renderShell(attrs: TrackComponentAttrs): m.Children {
179*6dbdd20aSAndroid Build Coastguard Worker    const chips =
180*6dbdd20aSAndroid Build Coastguard Worker      attrs.chips &&
181*6dbdd20aSAndroid Build Coastguard Worker      m(
182*6dbdd20aSAndroid Build Coastguard Worker        ChipBar,
183*6dbdd20aSAndroid Build Coastguard Worker        attrs.chips.map((chip) =>
184*6dbdd20aSAndroid Build Coastguard Worker          m(Chip, {label: chip, compact: true, rounded: true}),
185*6dbdd20aSAndroid Build Coastguard Worker        ),
186*6dbdd20aSAndroid Build Coastguard Worker      );
187*6dbdd20aSAndroid Build Coastguard Worker
188*6dbdd20aSAndroid Build Coastguard Worker    const {
189*6dbdd20aSAndroid Build Coastguard Worker      id,
190*6dbdd20aSAndroid Build Coastguard Worker      topOffsetPx = 0,
191*6dbdd20aSAndroid Build Coastguard Worker      collapsible,
192*6dbdd20aSAndroid Build Coastguard Worker      collapsed,
193*6dbdd20aSAndroid Build Coastguard Worker      reorderable = false,
194*6dbdd20aSAndroid Build Coastguard Worker      onMoveAfter = () => {},
195*6dbdd20aSAndroid Build Coastguard Worker      onMoveBefore = () => {},
196*6dbdd20aSAndroid Build Coastguard Worker    } = attrs;
197*6dbdd20aSAndroid Build Coastguard Worker
198*6dbdd20aSAndroid Build Coastguard Worker    return m(
199*6dbdd20aSAndroid Build Coastguard Worker      `.pf-track-shell[data-track-node-id=${id}]`,
200*6dbdd20aSAndroid Build Coastguard Worker      {
201*6dbdd20aSAndroid Build Coastguard Worker        className: classNames(collapsible && 'pf-clickable'),
202*6dbdd20aSAndroid Build Coastguard Worker        onclick: (e: MouseEvent) => {
203*6dbdd20aSAndroid Build Coastguard Worker          // Block all clicks on the shell from propagating through to the
204*6dbdd20aSAndroid Build Coastguard Worker          // canvas
205*6dbdd20aSAndroid Build Coastguard Worker          e.stopPropagation();
206*6dbdd20aSAndroid Build Coastguard Worker          if (collapsible) {
207*6dbdd20aSAndroid Build Coastguard Worker            attrs.onToggleCollapsed?.();
208*6dbdd20aSAndroid Build Coastguard Worker          }
209*6dbdd20aSAndroid Build Coastguard Worker        },
210*6dbdd20aSAndroid Build Coastguard Worker        draggable: reorderable,
211*6dbdd20aSAndroid Build Coastguard Worker        ondragstart: (e: DragEvent) => {
212*6dbdd20aSAndroid Build Coastguard Worker          e.dataTransfer?.setData('text/plain', id);
213*6dbdd20aSAndroid Build Coastguard Worker        },
214*6dbdd20aSAndroid Build Coastguard Worker        ondragover: (e: DragEvent) => {
215*6dbdd20aSAndroid Build Coastguard Worker          if (!reorderable) {
216*6dbdd20aSAndroid Build Coastguard Worker            return;
217*6dbdd20aSAndroid Build Coastguard Worker          }
218*6dbdd20aSAndroid Build Coastguard Worker          const target = e.currentTarget as HTMLElement;
219*6dbdd20aSAndroid Build Coastguard Worker          const threshold = target.offsetHeight / 2;
220*6dbdd20aSAndroid Build Coastguard Worker          if (e.offsetY > threshold) {
221*6dbdd20aSAndroid Build Coastguard Worker            target.classList.remove('pf-drag-before');
222*6dbdd20aSAndroid Build Coastguard Worker            target.classList.add('pf-drag-after');
223*6dbdd20aSAndroid Build Coastguard Worker          } else {
224*6dbdd20aSAndroid Build Coastguard Worker            target.classList.remove('pf-drag-after');
225*6dbdd20aSAndroid Build Coastguard Worker            target.classList.add('pf-drag-before');
226*6dbdd20aSAndroid Build Coastguard Worker          }
227*6dbdd20aSAndroid Build Coastguard Worker        },
228*6dbdd20aSAndroid Build Coastguard Worker        ondragleave: (e: DragEvent) => {
229*6dbdd20aSAndroid Build Coastguard Worker          if (!reorderable) {
230*6dbdd20aSAndroid Build Coastguard Worker            return;
231*6dbdd20aSAndroid Build Coastguard Worker          }
232*6dbdd20aSAndroid Build Coastguard Worker          const target = e.currentTarget as HTMLElement;
233*6dbdd20aSAndroid Build Coastguard Worker          const related = e.relatedTarget as HTMLElement | null;
234*6dbdd20aSAndroid Build Coastguard Worker          if (related && !target.contains(related)) {
235*6dbdd20aSAndroid Build Coastguard Worker            target.classList.remove('pf-drag-after');
236*6dbdd20aSAndroid Build Coastguard Worker            target.classList.remove('pf-drag-before');
237*6dbdd20aSAndroid Build Coastguard Worker          }
238*6dbdd20aSAndroid Build Coastguard Worker        },
239*6dbdd20aSAndroid Build Coastguard Worker        ondrop: (e: DragEvent) => {
240*6dbdd20aSAndroid Build Coastguard Worker          if (!reorderable) {
241*6dbdd20aSAndroid Build Coastguard Worker            return;
242*6dbdd20aSAndroid Build Coastguard Worker          }
243*6dbdd20aSAndroid Build Coastguard Worker          const id = e.dataTransfer?.getData('text/plain');
244*6dbdd20aSAndroid Build Coastguard Worker          const target = e.currentTarget as HTMLElement;
245*6dbdd20aSAndroid Build Coastguard Worker          const threshold = target.offsetHeight / 2;
246*6dbdd20aSAndroid Build Coastguard Worker          if (id !== undefined) {
247*6dbdd20aSAndroid Build Coastguard Worker            if (e.offsetY > threshold) {
248*6dbdd20aSAndroid Build Coastguard Worker              onMoveAfter(id);
249*6dbdd20aSAndroid Build Coastguard Worker            } else {
250*6dbdd20aSAndroid Build Coastguard Worker              onMoveBefore(id);
251*6dbdd20aSAndroid Build Coastguard Worker            }
252*6dbdd20aSAndroid Build Coastguard Worker          }
253*6dbdd20aSAndroid Build Coastguard Worker          target.classList.remove('pf-drag-after');
254*6dbdd20aSAndroid Build Coastguard Worker          target.classList.remove('pf-drag-before');
255*6dbdd20aSAndroid Build Coastguard Worker        },
256*6dbdd20aSAndroid Build Coastguard Worker      },
257*6dbdd20aSAndroid Build Coastguard Worker      m(
258*6dbdd20aSAndroid Build Coastguard Worker        '.pf-track-menubar',
259*6dbdd20aSAndroid Build Coastguard Worker        {
260*6dbdd20aSAndroid Build Coastguard Worker          style: {
261*6dbdd20aSAndroid Build Coastguard Worker            position: 'sticky',
262*6dbdd20aSAndroid Build Coastguard Worker            top: `${topOffsetPx}px`,
263*6dbdd20aSAndroid Build Coastguard Worker          },
264*6dbdd20aSAndroid Build Coastguard Worker        },
265*6dbdd20aSAndroid Build Coastguard Worker        m(
266*6dbdd20aSAndroid Build Coastguard Worker          'h1.pf-track-title',
267*6dbdd20aSAndroid Build Coastguard Worker          {
268*6dbdd20aSAndroid Build Coastguard Worker            ref: attrs.path, // TODO(stevegolton): Replace with aria tags?
269*6dbdd20aSAndroid Build Coastguard Worker          },
270*6dbdd20aSAndroid Build Coastguard Worker          collapsible &&
271*6dbdd20aSAndroid Build Coastguard Worker            m(Icon, {icon: collapsed ? Icons.ExpandDown : Icons.ExpandUp}),
272*6dbdd20aSAndroid Build Coastguard Worker          m(
273*6dbdd20aSAndroid Build Coastguard Worker            MiddleEllipsis,
274*6dbdd20aSAndroid Build Coastguard Worker            {text: attrs.title},
275*6dbdd20aSAndroid Build Coastguard Worker            m('.pf-track-title-popup', attrs.title),
276*6dbdd20aSAndroid Build Coastguard Worker          ),
277*6dbdd20aSAndroid Build Coastguard Worker          chips,
278*6dbdd20aSAndroid Build Coastguard Worker        ),
279*6dbdd20aSAndroid Build Coastguard Worker        m(
280*6dbdd20aSAndroid Build Coastguard Worker          ButtonBar,
281*6dbdd20aSAndroid Build Coastguard Worker          {
282*6dbdd20aSAndroid Build Coastguard Worker            className: 'pf-track-buttons',
283*6dbdd20aSAndroid Build Coastguard Worker            // Block button clicks from hitting the shell's on click event
284*6dbdd20aSAndroid Build Coastguard Worker            onclick: (e: MouseEvent) => e.stopPropagation(),
285*6dbdd20aSAndroid Build Coastguard Worker          },
286*6dbdd20aSAndroid Build Coastguard Worker          attrs.buttons,
287*6dbdd20aSAndroid Build Coastguard Worker        ),
288*6dbdd20aSAndroid Build Coastguard Worker      ),
289*6dbdd20aSAndroid Build Coastguard Worker    );
290*6dbdd20aSAndroid Build Coastguard Worker  }
291*6dbdd20aSAndroid Build Coastguard Worker
292*6dbdd20aSAndroid Build Coastguard Worker  private mouseDownPos?: Vector2D;
293*6dbdd20aSAndroid Build Coastguard Worker  private selectionOccurred = false;
294*6dbdd20aSAndroid Build Coastguard Worker
295*6dbdd20aSAndroid Build Coastguard Worker  private renderContent(attrs: TrackComponentAttrs): m.Children {
296*6dbdd20aSAndroid Build Coastguard Worker    const {
297*6dbdd20aSAndroid Build Coastguard Worker      heightPx,
298*6dbdd20aSAndroid Build Coastguard Worker      onTrackContentMouseMove,
299*6dbdd20aSAndroid Build Coastguard Worker      onTrackContentMouseOut,
300*6dbdd20aSAndroid Build Coastguard Worker      onTrackContentClick,
301*6dbdd20aSAndroid Build Coastguard Worker    } = attrs;
302*6dbdd20aSAndroid Build Coastguard Worker    const trackHeight = Math.max(heightPx, TRACK_HEIGHT_MIN_PX);
303*6dbdd20aSAndroid Build Coastguard Worker
304*6dbdd20aSAndroid Build Coastguard Worker    return m('.pf-track-content', {
305*6dbdd20aSAndroid Build Coastguard Worker      style: {
306*6dbdd20aSAndroid Build Coastguard Worker        height: `${trackHeight}px`,
307*6dbdd20aSAndroid Build Coastguard Worker      },
308*6dbdd20aSAndroid Build Coastguard Worker      className: classNames(attrs.error && 'pf-track-content-error'),
309*6dbdd20aSAndroid Build Coastguard Worker      onmousemove: (e: MouseEvent) => {
310*6dbdd20aSAndroid Build Coastguard Worker        onTrackContentMouseMove?.(
311*6dbdd20aSAndroid Build Coastguard Worker          currentTargetOffset(e),
312*6dbdd20aSAndroid Build Coastguard Worker          getTargetContainerSize(e),
313*6dbdd20aSAndroid Build Coastguard Worker        );
314*6dbdd20aSAndroid Build Coastguard Worker      },
315*6dbdd20aSAndroid Build Coastguard Worker      onmouseout: () => {
316*6dbdd20aSAndroid Build Coastguard Worker        onTrackContentMouseOut?.();
317*6dbdd20aSAndroid Build Coastguard Worker      },
318*6dbdd20aSAndroid Build Coastguard Worker      onmousedown: (e: MouseEvent) => {
319*6dbdd20aSAndroid Build Coastguard Worker        this.mouseDownPos = currentTargetOffset(e);
320*6dbdd20aSAndroid Build Coastguard Worker      },
321*6dbdd20aSAndroid Build Coastguard Worker      onmouseup: (e: MouseEvent) => {
322*6dbdd20aSAndroid Build Coastguard Worker        if (!this.mouseDownPos) return;
323*6dbdd20aSAndroid Build Coastguard Worker        if (
324*6dbdd20aSAndroid Build Coastguard Worker          this.mouseDownPos.sub(currentTargetOffset(e)).manhattanDistance > 1
325*6dbdd20aSAndroid Build Coastguard Worker        ) {
326*6dbdd20aSAndroid Build Coastguard Worker          this.selectionOccurred = true;
327*6dbdd20aSAndroid Build Coastguard Worker        }
328*6dbdd20aSAndroid Build Coastguard Worker        this.mouseDownPos = undefined;
329*6dbdd20aSAndroid Build Coastguard Worker      },
330*6dbdd20aSAndroid Build Coastguard Worker      onclick: (e: MouseEvent) => {
331*6dbdd20aSAndroid Build Coastguard Worker        // This click event occurs after any selection mouse up/drag events
332*6dbdd20aSAndroid Build Coastguard Worker        // so we have to look if the mouse moved during this click to know
333*6dbdd20aSAndroid Build Coastguard Worker        // if a selection occurred.
334*6dbdd20aSAndroid Build Coastguard Worker        if (this.selectionOccurred) {
335*6dbdd20aSAndroid Build Coastguard Worker          this.selectionOccurred = false;
336*6dbdd20aSAndroid Build Coastguard Worker          return;
337*6dbdd20aSAndroid Build Coastguard Worker        }
338*6dbdd20aSAndroid Build Coastguard Worker
339*6dbdd20aSAndroid Build Coastguard Worker        // Returns true if something was selected, so stop propagation.
340*6dbdd20aSAndroid Build Coastguard Worker        if (
341*6dbdd20aSAndroid Build Coastguard Worker          onTrackContentClick?.(
342*6dbdd20aSAndroid Build Coastguard Worker            currentTargetOffset(e),
343*6dbdd20aSAndroid Build Coastguard Worker            getTargetContainerSize(e),
344*6dbdd20aSAndroid Build Coastguard Worker          )
345*6dbdd20aSAndroid Build Coastguard Worker        ) {
346*6dbdd20aSAndroid Build Coastguard Worker          e.stopPropagation();
347*6dbdd20aSAndroid Build Coastguard Worker        }
348*6dbdd20aSAndroid Build Coastguard Worker      },
349*6dbdd20aSAndroid Build Coastguard Worker    });
350*6dbdd20aSAndroid Build Coastguard Worker  }
351*6dbdd20aSAndroid Build Coastguard Worker}
352*6dbdd20aSAndroid Build Coastguard Worker
353*6dbdd20aSAndroid Build Coastguard Workerfunction getTargetContainerSize(event: MouseEvent): Bounds2D {
354*6dbdd20aSAndroid Build Coastguard Worker  const target = event.target as HTMLElement;
355*6dbdd20aSAndroid Build Coastguard Worker  return target.getBoundingClientRect();
356*6dbdd20aSAndroid Build Coastguard Worker}
357