xref: /aosp_15_r20/external/perfetto/ui/src/frontend/notes_panel.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2019 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 size 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 {currentTargetOffset} from '../base/dom_utils';
17*6dbdd20aSAndroid Build Coastguard Workerimport {Icons} from '../base/semantic_icons';
18*6dbdd20aSAndroid Build Coastguard Workerimport {randomColor} from '../components/colorizer';
19*6dbdd20aSAndroid Build Coastguard Workerimport {SpanNote, Note} from '../public/note';
20*6dbdd20aSAndroid Build Coastguard Workerimport {raf} from '../core/raf_scheduler';
21*6dbdd20aSAndroid Build Coastguard Workerimport {Button, ButtonBar} from '../widgets/button';
22*6dbdd20aSAndroid Build Coastguard Workerimport {TRACK_SHELL_WIDTH} from './css_constants';
23*6dbdd20aSAndroid Build Coastguard Workerimport {getMaxMajorTicks, generateTicks, TickType} from './gridline_helper';
24*6dbdd20aSAndroid Build Coastguard Workerimport {Size2D} from '../base/geom';
25*6dbdd20aSAndroid Build Coastguard Workerimport {Panel} from './panel_container';
26*6dbdd20aSAndroid Build Coastguard Workerimport {Timestamp} from '../components/widgets/timestamp';
27*6dbdd20aSAndroid Build Coastguard Workerimport {assertUnreachable} from '../base/logging';
28*6dbdd20aSAndroid Build Coastguard Workerimport {DetailsPanel} from '../public/details_panel';
29*6dbdd20aSAndroid Build Coastguard Workerimport {TimeScale} from '../base/time_scale';
30*6dbdd20aSAndroid Build Coastguard Workerimport {canvasClip} from '../base/canvas_utils';
31*6dbdd20aSAndroid Build Coastguard Workerimport {Selection} from '../public/selection';
32*6dbdd20aSAndroid Build Coastguard Workerimport {TraceImpl} from '../core/trace_impl';
33*6dbdd20aSAndroid Build Coastguard Worker
34*6dbdd20aSAndroid Build Coastguard Workerconst FLAG_WIDTH = 16;
35*6dbdd20aSAndroid Build Coastguard Workerconst AREA_TRIANGLE_WIDTH = 10;
36*6dbdd20aSAndroid Build Coastguard Workerconst FLAG = `\uE153`;
37*6dbdd20aSAndroid Build Coastguard Worker
38*6dbdd20aSAndroid Build Coastguard Workerfunction toSummary(s: string) {
39*6dbdd20aSAndroid Build Coastguard Worker  const newlineIndex = s.indexOf('\n') > 0 ? s.indexOf('\n') : s.length;
40*6dbdd20aSAndroid Build Coastguard Worker  return s.slice(0, Math.min(newlineIndex, s.length, 16));
41*6dbdd20aSAndroid Build Coastguard Worker}
42*6dbdd20aSAndroid Build Coastguard Worker
43*6dbdd20aSAndroid Build Coastguard Workerfunction getStartTimestamp(note: Note | SpanNote) {
44*6dbdd20aSAndroid Build Coastguard Worker  const noteType = note.noteType;
45*6dbdd20aSAndroid Build Coastguard Worker  switch (noteType) {
46*6dbdd20aSAndroid Build Coastguard Worker    case 'SPAN':
47*6dbdd20aSAndroid Build Coastguard Worker      return note.start;
48*6dbdd20aSAndroid Build Coastguard Worker    case 'DEFAULT':
49*6dbdd20aSAndroid Build Coastguard Worker      return note.timestamp;
50*6dbdd20aSAndroid Build Coastguard Worker    default:
51*6dbdd20aSAndroid Build Coastguard Worker      assertUnreachable(noteType);
52*6dbdd20aSAndroid Build Coastguard Worker  }
53*6dbdd20aSAndroid Build Coastguard Worker}
54*6dbdd20aSAndroid Build Coastguard Worker
55*6dbdd20aSAndroid Build Coastguard Workerexport class NotesPanel implements Panel {
56*6dbdd20aSAndroid Build Coastguard Worker  readonly kind = 'panel';
57*6dbdd20aSAndroid Build Coastguard Worker  readonly selectable = false;
58*6dbdd20aSAndroid Build Coastguard Worker  private readonly trace: TraceImpl;
59*6dbdd20aSAndroid Build Coastguard Worker  private timescale?: TimeScale; // The timescale from the last render()
60*6dbdd20aSAndroid Build Coastguard Worker  private hoveredX: null | number = null;
61*6dbdd20aSAndroid Build Coastguard Worker  private mouseDragging = false;
62*6dbdd20aSAndroid Build Coastguard Worker
63*6dbdd20aSAndroid Build Coastguard Worker  constructor(trace: TraceImpl) {
64*6dbdd20aSAndroid Build Coastguard Worker    this.trace = trace;
65*6dbdd20aSAndroid Build Coastguard Worker  }
66*6dbdd20aSAndroid Build Coastguard Worker
67*6dbdd20aSAndroid Build Coastguard Worker  render(): m.Children {
68*6dbdd20aSAndroid Build Coastguard Worker    const allCollapsed = this.trace.workspace.flatTracks.every(
69*6dbdd20aSAndroid Build Coastguard Worker      (n) => n.collapsed,
70*6dbdd20aSAndroid Build Coastguard Worker    );
71*6dbdd20aSAndroid Build Coastguard Worker
72*6dbdd20aSAndroid Build Coastguard Worker    return m(
73*6dbdd20aSAndroid Build Coastguard Worker      '.notes-panel',
74*6dbdd20aSAndroid Build Coastguard Worker      {
75*6dbdd20aSAndroid Build Coastguard Worker        onmousedown: () => {
76*6dbdd20aSAndroid Build Coastguard Worker          // If the user clicks & drags, very likely they just want to measure
77*6dbdd20aSAndroid Build Coastguard Worker          // the time horizontally, not set a flag. This debouncing is done to
78*6dbdd20aSAndroid Build Coastguard Worker          // avoid setting accidental flags like measuring the time on the brush
79*6dbdd20aSAndroid Build Coastguard Worker          // timeline.
80*6dbdd20aSAndroid Build Coastguard Worker          this.mouseDragging = false;
81*6dbdd20aSAndroid Build Coastguard Worker        },
82*6dbdd20aSAndroid Build Coastguard Worker        onclick: (e: MouseEvent) => {
83*6dbdd20aSAndroid Build Coastguard Worker          if (!this.mouseDragging) {
84*6dbdd20aSAndroid Build Coastguard Worker            const x = currentTargetOffset(e).x - TRACK_SHELL_WIDTH;
85*6dbdd20aSAndroid Build Coastguard Worker            this.onClick(x);
86*6dbdd20aSAndroid Build Coastguard Worker            e.stopPropagation();
87*6dbdd20aSAndroid Build Coastguard Worker          }
88*6dbdd20aSAndroid Build Coastguard Worker        },
89*6dbdd20aSAndroid Build Coastguard Worker        onmousemove: (e: MouseEvent) => {
90*6dbdd20aSAndroid Build Coastguard Worker          this.mouseDragging = true;
91*6dbdd20aSAndroid Build Coastguard Worker          this.hoveredX = currentTargetOffset(e).x - TRACK_SHELL_WIDTH;
92*6dbdd20aSAndroid Build Coastguard Worker          raf.scheduleCanvasRedraw();
93*6dbdd20aSAndroid Build Coastguard Worker        },
94*6dbdd20aSAndroid Build Coastguard Worker        onmouseenter: (e: MouseEvent) => {
95*6dbdd20aSAndroid Build Coastguard Worker          this.hoveredX = currentTargetOffset(e).x - TRACK_SHELL_WIDTH;
96*6dbdd20aSAndroid Build Coastguard Worker          raf.scheduleCanvasRedraw();
97*6dbdd20aSAndroid Build Coastguard Worker        },
98*6dbdd20aSAndroid Build Coastguard Worker        onmouseout: () => {
99*6dbdd20aSAndroid Build Coastguard Worker          this.hoveredX = null;
100*6dbdd20aSAndroid Build Coastguard Worker          this.trace.timeline.hoveredNoteTimestamp = undefined;
101*6dbdd20aSAndroid Build Coastguard Worker        },
102*6dbdd20aSAndroid Build Coastguard Worker      },
103*6dbdd20aSAndroid Build Coastguard Worker      m(
104*6dbdd20aSAndroid Build Coastguard Worker        ButtonBar,
105*6dbdd20aSAndroid Build Coastguard Worker        {className: 'pf-toolbar'},
106*6dbdd20aSAndroid Build Coastguard Worker        m(Button, {
107*6dbdd20aSAndroid Build Coastguard Worker          onclick: (e: Event) => {
108*6dbdd20aSAndroid Build Coastguard Worker            e.preventDefault();
109*6dbdd20aSAndroid Build Coastguard Worker            if (allCollapsed) {
110*6dbdd20aSAndroid Build Coastguard Worker              this.trace.commands.runCommand(
111*6dbdd20aSAndroid Build Coastguard Worker                'perfetto.CoreCommands#ExpandAllGroups',
112*6dbdd20aSAndroid Build Coastguard Worker              );
113*6dbdd20aSAndroid Build Coastguard Worker            } else {
114*6dbdd20aSAndroid Build Coastguard Worker              this.trace.commands.runCommand(
115*6dbdd20aSAndroid Build Coastguard Worker                'perfetto.CoreCommands#CollapseAllGroups',
116*6dbdd20aSAndroid Build Coastguard Worker              );
117*6dbdd20aSAndroid Build Coastguard Worker            }
118*6dbdd20aSAndroid Build Coastguard Worker          },
119*6dbdd20aSAndroid Build Coastguard Worker          title: allCollapsed ? 'Expand all' : 'Collapse all',
120*6dbdd20aSAndroid Build Coastguard Worker          icon: allCollapsed ? 'unfold_more' : 'unfold_less',
121*6dbdd20aSAndroid Build Coastguard Worker          compact: true,
122*6dbdd20aSAndroid Build Coastguard Worker        }),
123*6dbdd20aSAndroid Build Coastguard Worker        m(Button, {
124*6dbdd20aSAndroid Build Coastguard Worker          onclick: (e: Event) => {
125*6dbdd20aSAndroid Build Coastguard Worker            e.preventDefault();
126*6dbdd20aSAndroid Build Coastguard Worker            this.trace.workspace.pinnedTracks.forEach((t) =>
127*6dbdd20aSAndroid Build Coastguard Worker              this.trace.workspace.unpinTrack(t),
128*6dbdd20aSAndroid Build Coastguard Worker            );
129*6dbdd20aSAndroid Build Coastguard Worker            raf.scheduleFullRedraw();
130*6dbdd20aSAndroid Build Coastguard Worker          },
131*6dbdd20aSAndroid Build Coastguard Worker          title: 'Clear all pinned tracks',
132*6dbdd20aSAndroid Build Coastguard Worker          icon: 'clear_all',
133*6dbdd20aSAndroid Build Coastguard Worker          compact: true,
134*6dbdd20aSAndroid Build Coastguard Worker        }),
135*6dbdd20aSAndroid Build Coastguard Worker        // TODO(stevegolton): Re-introduce this when we fix track filtering
136*6dbdd20aSAndroid Build Coastguard Worker        // m(TextInput, {
137*6dbdd20aSAndroid Build Coastguard Worker        //   placeholder: 'Filter tracks...',
138*6dbdd20aSAndroid Build Coastguard Worker        //   title:
139*6dbdd20aSAndroid Build Coastguard Worker        //     'Track filter - enter one or more comma-separated search terms',
140*6dbdd20aSAndroid Build Coastguard Worker        //   value: this.trace.state.trackFilterTerm,
141*6dbdd20aSAndroid Build Coastguard Worker        //   oninput: (e: Event) => {
142*6dbdd20aSAndroid Build Coastguard Worker        //     const filterTerm = (e.target as HTMLInputElement).value;
143*6dbdd20aSAndroid Build Coastguard Worker        //     this.trace.dispatch(Actions.setTrackFilterTerm({filterTerm}));
144*6dbdd20aSAndroid Build Coastguard Worker        //   },
145*6dbdd20aSAndroid Build Coastguard Worker        // }),
146*6dbdd20aSAndroid Build Coastguard Worker        // m(Button, {
147*6dbdd20aSAndroid Build Coastguard Worker        //   type: 'reset',
148*6dbdd20aSAndroid Build Coastguard Worker        //   icon: 'backspace',
149*6dbdd20aSAndroid Build Coastguard Worker        //   onclick: () => {
150*6dbdd20aSAndroid Build Coastguard Worker        //     this.trace.dispatch(
151*6dbdd20aSAndroid Build Coastguard Worker        //       Actions.setTrackFilterTerm({filterTerm: undefined}),
152*6dbdd20aSAndroid Build Coastguard Worker        //     );
153*6dbdd20aSAndroid Build Coastguard Worker        //   },
154*6dbdd20aSAndroid Build Coastguard Worker        //   title: 'Clear track filter',
155*6dbdd20aSAndroid Build Coastguard Worker        // }),
156*6dbdd20aSAndroid Build Coastguard Worker      ),
157*6dbdd20aSAndroid Build Coastguard Worker    );
158*6dbdd20aSAndroid Build Coastguard Worker  }
159*6dbdd20aSAndroid Build Coastguard Worker
160*6dbdd20aSAndroid Build Coastguard Worker  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
161*6dbdd20aSAndroid Build Coastguard Worker    ctx.fillStyle = '#999';
162*6dbdd20aSAndroid Build Coastguard Worker    ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
163*6dbdd20aSAndroid Build Coastguard Worker
164*6dbdd20aSAndroid Build Coastguard Worker    const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
165*6dbdd20aSAndroid Build Coastguard Worker
166*6dbdd20aSAndroid Build Coastguard Worker    ctx.save();
167*6dbdd20aSAndroid Build Coastguard Worker    ctx.translate(TRACK_SHELL_WIDTH, 0);
168*6dbdd20aSAndroid Build Coastguard Worker    canvasClip(ctx, 0, 0, trackSize.width, trackSize.height);
169*6dbdd20aSAndroid Build Coastguard Worker    this.renderPanel(ctx, trackSize);
170*6dbdd20aSAndroid Build Coastguard Worker    ctx.restore();
171*6dbdd20aSAndroid Build Coastguard Worker  }
172*6dbdd20aSAndroid Build Coastguard Worker
173*6dbdd20aSAndroid Build Coastguard Worker  private renderPanel(ctx: CanvasRenderingContext2D, size: Size2D): void {
174*6dbdd20aSAndroid Build Coastguard Worker    let aNoteIsHovered = false;
175*6dbdd20aSAndroid Build Coastguard Worker
176*6dbdd20aSAndroid Build Coastguard Worker    const visibleWindow = this.trace.timeline.visibleWindow;
177*6dbdd20aSAndroid Build Coastguard Worker    const timescale = new TimeScale(visibleWindow, {
178*6dbdd20aSAndroid Build Coastguard Worker      left: 0,
179*6dbdd20aSAndroid Build Coastguard Worker      right: size.width,
180*6dbdd20aSAndroid Build Coastguard Worker    });
181*6dbdd20aSAndroid Build Coastguard Worker    const timespan = visibleWindow.toTimeSpan();
182*6dbdd20aSAndroid Build Coastguard Worker
183*6dbdd20aSAndroid Build Coastguard Worker    this.timescale = timescale;
184*6dbdd20aSAndroid Build Coastguard Worker
185*6dbdd20aSAndroid Build Coastguard Worker    if (size.width > 0 && timespan.duration > 0n) {
186*6dbdd20aSAndroid Build Coastguard Worker      const maxMajorTicks = getMaxMajorTicks(size.width);
187*6dbdd20aSAndroid Build Coastguard Worker      const offset = this.trace.timeline.timestampOffset();
188*6dbdd20aSAndroid Build Coastguard Worker      const tickGen = generateTicks(timespan, maxMajorTicks, offset);
189*6dbdd20aSAndroid Build Coastguard Worker      for (const {type, time} of tickGen) {
190*6dbdd20aSAndroid Build Coastguard Worker        const px = Math.floor(timescale.timeToPx(time));
191*6dbdd20aSAndroid Build Coastguard Worker        if (type === TickType.MAJOR) {
192*6dbdd20aSAndroid Build Coastguard Worker          ctx.fillRect(px, 0, 1, size.height);
193*6dbdd20aSAndroid Build Coastguard Worker        }
194*6dbdd20aSAndroid Build Coastguard Worker      }
195*6dbdd20aSAndroid Build Coastguard Worker    }
196*6dbdd20aSAndroid Build Coastguard Worker
197*6dbdd20aSAndroid Build Coastguard Worker    ctx.textBaseline = 'bottom';
198*6dbdd20aSAndroid Build Coastguard Worker    ctx.font = '10px Helvetica';
199*6dbdd20aSAndroid Build Coastguard Worker
200*6dbdd20aSAndroid Build Coastguard Worker    for (const note of this.trace.notes.notes.values()) {
201*6dbdd20aSAndroid Build Coastguard Worker      const timestamp = getStartTimestamp(note);
202*6dbdd20aSAndroid Build Coastguard Worker      // TODO(hjd): We should still render area selection marks in viewport is
203*6dbdd20aSAndroid Build Coastguard Worker      // *within* the area (e.g. both lhs and rhs are out of bounds).
204*6dbdd20aSAndroid Build Coastguard Worker      if (
205*6dbdd20aSAndroid Build Coastguard Worker        (note.noteType === 'DEFAULT' &&
206*6dbdd20aSAndroid Build Coastguard Worker          !visibleWindow.contains(note.timestamp)) ||
207*6dbdd20aSAndroid Build Coastguard Worker        (note.noteType === 'SPAN' &&
208*6dbdd20aSAndroid Build Coastguard Worker          !visibleWindow.overlaps(note.start, note.end))
209*6dbdd20aSAndroid Build Coastguard Worker      ) {
210*6dbdd20aSAndroid Build Coastguard Worker        continue;
211*6dbdd20aSAndroid Build Coastguard Worker      }
212*6dbdd20aSAndroid Build Coastguard Worker      const currentIsHovered =
213*6dbdd20aSAndroid Build Coastguard Worker        this.hoveredX !== null && this.hitTestNote(this.hoveredX, note);
214*6dbdd20aSAndroid Build Coastguard Worker      if (currentIsHovered) aNoteIsHovered = true;
215*6dbdd20aSAndroid Build Coastguard Worker
216*6dbdd20aSAndroid Build Coastguard Worker      const selection = this.trace.selection.selection;
217*6dbdd20aSAndroid Build Coastguard Worker      const isSelected = selection.kind === 'note' && selection.id === note.id;
218*6dbdd20aSAndroid Build Coastguard Worker      const x = timescale.timeToPx(timestamp);
219*6dbdd20aSAndroid Build Coastguard Worker      const left = Math.floor(x);
220*6dbdd20aSAndroid Build Coastguard Worker
221*6dbdd20aSAndroid Build Coastguard Worker      // Draw flag or marker.
222*6dbdd20aSAndroid Build Coastguard Worker      if (note.noteType === 'SPAN') {
223*6dbdd20aSAndroid Build Coastguard Worker        this.drawAreaMarker(
224*6dbdd20aSAndroid Build Coastguard Worker          ctx,
225*6dbdd20aSAndroid Build Coastguard Worker          left,
226*6dbdd20aSAndroid Build Coastguard Worker          Math.floor(timescale.timeToPx(note.end)),
227*6dbdd20aSAndroid Build Coastguard Worker          note.color,
228*6dbdd20aSAndroid Build Coastguard Worker          isSelected,
229*6dbdd20aSAndroid Build Coastguard Worker        );
230*6dbdd20aSAndroid Build Coastguard Worker      } else {
231*6dbdd20aSAndroid Build Coastguard Worker        this.drawFlag(ctx, left, size.height, note.color, isSelected);
232*6dbdd20aSAndroid Build Coastguard Worker      }
233*6dbdd20aSAndroid Build Coastguard Worker
234*6dbdd20aSAndroid Build Coastguard Worker      if (note.text) {
235*6dbdd20aSAndroid Build Coastguard Worker        const summary = toSummary(note.text);
236*6dbdd20aSAndroid Build Coastguard Worker        const measured = ctx.measureText(summary);
237*6dbdd20aSAndroid Build Coastguard Worker        // Add a white semi-transparent background for the text.
238*6dbdd20aSAndroid Build Coastguard Worker        ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
239*6dbdd20aSAndroid Build Coastguard Worker        ctx.fillRect(
240*6dbdd20aSAndroid Build Coastguard Worker          left + FLAG_WIDTH + 2,
241*6dbdd20aSAndroid Build Coastguard Worker          size.height + 2,
242*6dbdd20aSAndroid Build Coastguard Worker          measured.width + 2,
243*6dbdd20aSAndroid Build Coastguard Worker          -12,
244*6dbdd20aSAndroid Build Coastguard Worker        );
245*6dbdd20aSAndroid Build Coastguard Worker        ctx.fillStyle = '#3c4b5d';
246*6dbdd20aSAndroid Build Coastguard Worker        ctx.fillText(summary, left + FLAG_WIDTH + 3, size.height + 1);
247*6dbdd20aSAndroid Build Coastguard Worker      }
248*6dbdd20aSAndroid Build Coastguard Worker    }
249*6dbdd20aSAndroid Build Coastguard Worker
250*6dbdd20aSAndroid Build Coastguard Worker    // A real note is hovered so we don't need to see the preview line.
251*6dbdd20aSAndroid Build Coastguard Worker    // TODO(hjd): Change cursor to pointer here.
252*6dbdd20aSAndroid Build Coastguard Worker    if (aNoteIsHovered) {
253*6dbdd20aSAndroid Build Coastguard Worker      this.trace.timeline.hoveredNoteTimestamp = undefined;
254*6dbdd20aSAndroid Build Coastguard Worker    }
255*6dbdd20aSAndroid Build Coastguard Worker
256*6dbdd20aSAndroid Build Coastguard Worker    // View preview note flag when hovering on notes panel.
257*6dbdd20aSAndroid Build Coastguard Worker    if (!aNoteIsHovered && this.hoveredX !== null) {
258*6dbdd20aSAndroid Build Coastguard Worker      const timestamp = timescale.pxToHpTime(this.hoveredX).toTime();
259*6dbdd20aSAndroid Build Coastguard Worker      if (visibleWindow.contains(timestamp)) {
260*6dbdd20aSAndroid Build Coastguard Worker        this.trace.timeline.hoveredNoteTimestamp = timestamp;
261*6dbdd20aSAndroid Build Coastguard Worker        const x = timescale.timeToPx(timestamp);
262*6dbdd20aSAndroid Build Coastguard Worker        const left = Math.floor(x);
263*6dbdd20aSAndroid Build Coastguard Worker        this.drawFlag(ctx, left, size.height, '#aaa', /* fill */ true);
264*6dbdd20aSAndroid Build Coastguard Worker      }
265*6dbdd20aSAndroid Build Coastguard Worker    }
266*6dbdd20aSAndroid Build Coastguard Worker
267*6dbdd20aSAndroid Build Coastguard Worker    ctx.restore();
268*6dbdd20aSAndroid Build Coastguard Worker  }
269*6dbdd20aSAndroid Build Coastguard Worker
270*6dbdd20aSAndroid Build Coastguard Worker  private drawAreaMarker(
271*6dbdd20aSAndroid Build Coastguard Worker    ctx: CanvasRenderingContext2D,
272*6dbdd20aSAndroid Build Coastguard Worker    x: number,
273*6dbdd20aSAndroid Build Coastguard Worker    xEnd: number,
274*6dbdd20aSAndroid Build Coastguard Worker    color: string,
275*6dbdd20aSAndroid Build Coastguard Worker    fill: boolean,
276*6dbdd20aSAndroid Build Coastguard Worker  ) {
277*6dbdd20aSAndroid Build Coastguard Worker    ctx.fillStyle = color;
278*6dbdd20aSAndroid Build Coastguard Worker    ctx.strokeStyle = color;
279*6dbdd20aSAndroid Build Coastguard Worker    const topOffset = 10;
280*6dbdd20aSAndroid Build Coastguard Worker    // Don't draw in the track shell section.
281*6dbdd20aSAndroid Build Coastguard Worker    if (x >= 0) {
282*6dbdd20aSAndroid Build Coastguard Worker      // Draw left triangle.
283*6dbdd20aSAndroid Build Coastguard Worker      ctx.beginPath();
284*6dbdd20aSAndroid Build Coastguard Worker      ctx.moveTo(x, topOffset);
285*6dbdd20aSAndroid Build Coastguard Worker      ctx.lineTo(x, topOffset + AREA_TRIANGLE_WIDTH);
286*6dbdd20aSAndroid Build Coastguard Worker      ctx.lineTo(x + AREA_TRIANGLE_WIDTH, topOffset);
287*6dbdd20aSAndroid Build Coastguard Worker      ctx.lineTo(x, topOffset);
288*6dbdd20aSAndroid Build Coastguard Worker      if (fill) ctx.fill();
289*6dbdd20aSAndroid Build Coastguard Worker      ctx.stroke();
290*6dbdd20aSAndroid Build Coastguard Worker    }
291*6dbdd20aSAndroid Build Coastguard Worker    // Draw right triangle.
292*6dbdd20aSAndroid Build Coastguard Worker    ctx.beginPath();
293*6dbdd20aSAndroid Build Coastguard Worker    ctx.moveTo(xEnd, topOffset);
294*6dbdd20aSAndroid Build Coastguard Worker    ctx.lineTo(xEnd, topOffset + AREA_TRIANGLE_WIDTH);
295*6dbdd20aSAndroid Build Coastguard Worker    ctx.lineTo(xEnd - AREA_TRIANGLE_WIDTH, topOffset);
296*6dbdd20aSAndroid Build Coastguard Worker    ctx.lineTo(xEnd, topOffset);
297*6dbdd20aSAndroid Build Coastguard Worker    if (fill) ctx.fill();
298*6dbdd20aSAndroid Build Coastguard Worker    ctx.stroke();
299*6dbdd20aSAndroid Build Coastguard Worker
300*6dbdd20aSAndroid Build Coastguard Worker    // Start line after track shell section, join triangles.
301*6dbdd20aSAndroid Build Coastguard Worker    const startDraw = Math.max(x, 0);
302*6dbdd20aSAndroid Build Coastguard Worker    ctx.beginPath();
303*6dbdd20aSAndroid Build Coastguard Worker    ctx.moveTo(startDraw, topOffset);
304*6dbdd20aSAndroid Build Coastguard Worker    ctx.lineTo(xEnd, topOffset);
305*6dbdd20aSAndroid Build Coastguard Worker    ctx.stroke();
306*6dbdd20aSAndroid Build Coastguard Worker  }
307*6dbdd20aSAndroid Build Coastguard Worker
308*6dbdd20aSAndroid Build Coastguard Worker  private drawFlag(
309*6dbdd20aSAndroid Build Coastguard Worker    ctx: CanvasRenderingContext2D,
310*6dbdd20aSAndroid Build Coastguard Worker    x: number,
311*6dbdd20aSAndroid Build Coastguard Worker    height: number,
312*6dbdd20aSAndroid Build Coastguard Worker    color: string,
313*6dbdd20aSAndroid Build Coastguard Worker    fill?: boolean,
314*6dbdd20aSAndroid Build Coastguard Worker  ) {
315*6dbdd20aSAndroid Build Coastguard Worker    const prevFont = ctx.font;
316*6dbdd20aSAndroid Build Coastguard Worker    const prevBaseline = ctx.textBaseline;
317*6dbdd20aSAndroid Build Coastguard Worker    ctx.textBaseline = 'alphabetic';
318*6dbdd20aSAndroid Build Coastguard Worker    // Adjust height for icon font.
319*6dbdd20aSAndroid Build Coastguard Worker    ctx.font = '24px Material Symbols Sharp';
320*6dbdd20aSAndroid Build Coastguard Worker    ctx.fillStyle = color;
321*6dbdd20aSAndroid Build Coastguard Worker    ctx.strokeStyle = color;
322*6dbdd20aSAndroid Build Coastguard Worker    // The ligatures have padding included that means the icon is not drawn
323*6dbdd20aSAndroid Build Coastguard Worker    // exactly at the x value. This adjusts for that.
324*6dbdd20aSAndroid Build Coastguard Worker    const iconPadding = 6;
325*6dbdd20aSAndroid Build Coastguard Worker    if (fill) {
326*6dbdd20aSAndroid Build Coastguard Worker      ctx.fillText(FLAG, x - iconPadding, height + 2);
327*6dbdd20aSAndroid Build Coastguard Worker    } else {
328*6dbdd20aSAndroid Build Coastguard Worker      ctx.strokeText(FLAG, x - iconPadding, height + 2.5);
329*6dbdd20aSAndroid Build Coastguard Worker    }
330*6dbdd20aSAndroid Build Coastguard Worker    ctx.font = prevFont;
331*6dbdd20aSAndroid Build Coastguard Worker    ctx.textBaseline = prevBaseline;
332*6dbdd20aSAndroid Build Coastguard Worker  }
333*6dbdd20aSAndroid Build Coastguard Worker
334*6dbdd20aSAndroid Build Coastguard Worker  private onClick(x: number) {
335*6dbdd20aSAndroid Build Coastguard Worker    if (!this.timescale) {
336*6dbdd20aSAndroid Build Coastguard Worker      return;
337*6dbdd20aSAndroid Build Coastguard Worker    }
338*6dbdd20aSAndroid Build Coastguard Worker
339*6dbdd20aSAndroid Build Coastguard Worker    // Select the hovered note, or create a new single note & select it
340*6dbdd20aSAndroid Build Coastguard Worker    if (x < 0) return;
341*6dbdd20aSAndroid Build Coastguard Worker    for (const note of this.trace.notes.notes.values()) {
342*6dbdd20aSAndroid Build Coastguard Worker      if (this.hoveredX !== null && this.hitTestNote(this.hoveredX, note)) {
343*6dbdd20aSAndroid Build Coastguard Worker        this.trace.selection.selectNote({id: note.id});
344*6dbdd20aSAndroid Build Coastguard Worker        return;
345*6dbdd20aSAndroid Build Coastguard Worker      }
346*6dbdd20aSAndroid Build Coastguard Worker    }
347*6dbdd20aSAndroid Build Coastguard Worker    const timestamp = this.timescale.pxToHpTime(x).toTime();
348*6dbdd20aSAndroid Build Coastguard Worker    const color = randomColor();
349*6dbdd20aSAndroid Build Coastguard Worker    const noteId = this.trace.notes.addNote({timestamp, color});
350*6dbdd20aSAndroid Build Coastguard Worker    this.trace.selection.selectNote({id: noteId});
351*6dbdd20aSAndroid Build Coastguard Worker  }
352*6dbdd20aSAndroid Build Coastguard Worker
353*6dbdd20aSAndroid Build Coastguard Worker  private hitTestNote(x: number, note: SpanNote | Note): boolean {
354*6dbdd20aSAndroid Build Coastguard Worker    if (!this.timescale) {
355*6dbdd20aSAndroid Build Coastguard Worker      return false;
356*6dbdd20aSAndroid Build Coastguard Worker    }
357*6dbdd20aSAndroid Build Coastguard Worker
358*6dbdd20aSAndroid Build Coastguard Worker    const timescale = this.timescale;
359*6dbdd20aSAndroid Build Coastguard Worker    const noteX = timescale.timeToPx(getStartTimestamp(note));
360*6dbdd20aSAndroid Build Coastguard Worker    if (note.noteType === 'SPAN') {
361*6dbdd20aSAndroid Build Coastguard Worker      return (
362*6dbdd20aSAndroid Build Coastguard Worker        (noteX <= x && x < noteX + AREA_TRIANGLE_WIDTH) ||
363*6dbdd20aSAndroid Build Coastguard Worker        (timescale.timeToPx(note.end) > x &&
364*6dbdd20aSAndroid Build Coastguard Worker          x > timescale.timeToPx(note.end) - AREA_TRIANGLE_WIDTH)
365*6dbdd20aSAndroid Build Coastguard Worker      );
366*6dbdd20aSAndroid Build Coastguard Worker    } else {
367*6dbdd20aSAndroid Build Coastguard Worker      const width = FLAG_WIDTH;
368*6dbdd20aSAndroid Build Coastguard Worker      return noteX <= x && x < noteX + width;
369*6dbdd20aSAndroid Build Coastguard Worker    }
370*6dbdd20aSAndroid Build Coastguard Worker  }
371*6dbdd20aSAndroid Build Coastguard Worker}
372*6dbdd20aSAndroid Build Coastguard Worker
373*6dbdd20aSAndroid Build Coastguard Workerexport class NotesEditorTab implements DetailsPanel {
374*6dbdd20aSAndroid Build Coastguard Worker  constructor(private trace: TraceImpl) {}
375*6dbdd20aSAndroid Build Coastguard Worker
376*6dbdd20aSAndroid Build Coastguard Worker  render(selection: Selection) {
377*6dbdd20aSAndroid Build Coastguard Worker    if (selection.kind !== 'note') {
378*6dbdd20aSAndroid Build Coastguard Worker      return undefined;
379*6dbdd20aSAndroid Build Coastguard Worker    }
380*6dbdd20aSAndroid Build Coastguard Worker
381*6dbdd20aSAndroid Build Coastguard Worker    const id = selection.id;
382*6dbdd20aSAndroid Build Coastguard Worker
383*6dbdd20aSAndroid Build Coastguard Worker    const note = this.trace.notes.getNote(id);
384*6dbdd20aSAndroid Build Coastguard Worker    if (note === undefined) {
385*6dbdd20aSAndroid Build Coastguard Worker      return m('.', `No Note with id ${id}`);
386*6dbdd20aSAndroid Build Coastguard Worker    }
387*6dbdd20aSAndroid Build Coastguard Worker    const startTime = getStartTimestamp(note);
388*6dbdd20aSAndroid Build Coastguard Worker    return m(
389*6dbdd20aSAndroid Build Coastguard Worker      '.notes-editor-panel',
390*6dbdd20aSAndroid Build Coastguard Worker      {
391*6dbdd20aSAndroid Build Coastguard Worker        key: id, // Every note shoul get its own brand new DOM.
392*6dbdd20aSAndroid Build Coastguard Worker      },
393*6dbdd20aSAndroid Build Coastguard Worker      m(
394*6dbdd20aSAndroid Build Coastguard Worker        '.notes-editor-panel-heading-bar',
395*6dbdd20aSAndroid Build Coastguard Worker        m(
396*6dbdd20aSAndroid Build Coastguard Worker          '.notes-editor-panel-heading',
397*6dbdd20aSAndroid Build Coastguard Worker          `Annotation at `,
398*6dbdd20aSAndroid Build Coastguard Worker          m(Timestamp, {ts: startTime}),
399*6dbdd20aSAndroid Build Coastguard Worker        ),
400*6dbdd20aSAndroid Build Coastguard Worker        m('input[type=text]', {
401*6dbdd20aSAndroid Build Coastguard Worker          oncreate: (v: m.VnodeDOM) => {
402*6dbdd20aSAndroid Build Coastguard Worker            // NOTE: due to bad design decisions elsewhere this component is
403*6dbdd20aSAndroid Build Coastguard Worker            // rendered every time the mouse moves on the canvas. We cannot set
404*6dbdd20aSAndroid Build Coastguard Worker            // `value: note.text` as an input as that will clobber the input
405*6dbdd20aSAndroid Build Coastguard Worker            // value as we move the mouse.
406*6dbdd20aSAndroid Build Coastguard Worker            const inputElement = v.dom as HTMLInputElement;
407*6dbdd20aSAndroid Build Coastguard Worker            inputElement.value = note.text;
408*6dbdd20aSAndroid Build Coastguard Worker            inputElement.focus();
409*6dbdd20aSAndroid Build Coastguard Worker          },
410*6dbdd20aSAndroid Build Coastguard Worker          onchange: (e: InputEvent) => {
411*6dbdd20aSAndroid Build Coastguard Worker            const newText = (e.target as HTMLInputElement).value;
412*6dbdd20aSAndroid Build Coastguard Worker            this.trace.notes.changeNote(id, {text: newText});
413*6dbdd20aSAndroid Build Coastguard Worker          },
414*6dbdd20aSAndroid Build Coastguard Worker        }),
415*6dbdd20aSAndroid Build Coastguard Worker        m(
416*6dbdd20aSAndroid Build Coastguard Worker          'span.color-change',
417*6dbdd20aSAndroid Build Coastguard Worker          `Change color: `,
418*6dbdd20aSAndroid Build Coastguard Worker          m('input[type=color]', {
419*6dbdd20aSAndroid Build Coastguard Worker            value: note.color,
420*6dbdd20aSAndroid Build Coastguard Worker            onchange: (e: Event) => {
421*6dbdd20aSAndroid Build Coastguard Worker              const newColor = (e.target as HTMLInputElement).value;
422*6dbdd20aSAndroid Build Coastguard Worker              this.trace.notes.changeNote(id, {color: newColor});
423*6dbdd20aSAndroid Build Coastguard Worker            },
424*6dbdd20aSAndroid Build Coastguard Worker          }),
425*6dbdd20aSAndroid Build Coastguard Worker        ),
426*6dbdd20aSAndroid Build Coastguard Worker        m(Button, {
427*6dbdd20aSAndroid Build Coastguard Worker          label: 'Remove',
428*6dbdd20aSAndroid Build Coastguard Worker          icon: Icons.Delete,
429*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => this.trace.notes.removeNote(id),
430*6dbdd20aSAndroid Build Coastguard Worker        }),
431*6dbdd20aSAndroid Build Coastguard Worker      ),
432*6dbdd20aSAndroid Build Coastguard Worker    );
433*6dbdd20aSAndroid Build Coastguard Worker  }
434*6dbdd20aSAndroid Build Coastguard Worker}
435