• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2018 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import * as m from 'mithril';
16
17import {Actions, DeferredAction} from '../common/actions';
18import {TrackState} from '../common/state';
19
20import {globals} from './globals';
21import {drawGridLines} from './gridline_helper';
22import {drawVerticalSelection,
23        drawVerticalLineAtTime} from './vertical_line_helper';
24import {Panel, PanelSize} from './panel';
25import {Track} from './track';
26import {TRACK_SHELL_WIDTH} from './track_constants';
27import {trackRegistry} from './track_registry';
28
29function isPinned(id: string) {
30  return globals.state.pinnedTracks.indexOf(id) !== -1;
31}
32
33interface TrackShellAttrs {
34  trackState: TrackState;
35}
36
37class TrackShell implements m.ClassComponent<TrackShellAttrs> {
38  // Set to true when we click down and drag the
39  private dragging = false;
40  private dropping: 'before'|'after'|undefined = undefined;
41  private attrs?: TrackShellAttrs;
42
43  oninit(vnode: m.Vnode<TrackShellAttrs>) {
44    this.attrs = vnode.attrs;
45  }
46
47  view({attrs}: m.CVnode<TrackShellAttrs>) {
48    const dragClass = this.dragging ? `.drag` : '';
49    const dropClass = this.dropping ? `.drop-${this.dropping}` : '';
50    return m(
51        `.track-shell${dragClass}${dropClass}[draggable=true]`,
52        {
53          onmousedown: this.onmousedown.bind(this),
54          ondragstart: this.ondragstart.bind(this),
55          ondragend: this.ondragend.bind(this),
56          ondragover: this.ondragover.bind(this),
57          ondragleave: this.ondragleave.bind(this),
58          ondrop: this.ondrop.bind(this),
59        },
60        m('h1',
61          {
62            title: attrs.trackState.name,
63          },
64          attrs.trackState.name,
65          m.trust('&#x200E;')),
66        m(TrackButton, {
67          action: Actions.toggleTrackPinned({trackId: attrs.trackState.id}),
68          i: isPinned(attrs.trackState.id) ? 'star' : 'star_border',
69        }));
70  }
71
72  onmousedown(e: MouseEvent) {
73    // Prevent that the click is intercepted by the PanAndZoomHandler and that
74    // we start panning while dragging.
75    e.stopPropagation();
76  }
77
78  ondragstart(e: DragEvent) {
79    const dataTransfer = e.dataTransfer;
80    if (dataTransfer === null) return;
81    this.dragging = true;
82    globals.rafScheduler.scheduleFullRedraw();
83    dataTransfer.setData('perfetto/track', `${this.attrs!.trackState.id}`);
84    dataTransfer.setDragImage(new Image(), 0, 0);
85    e.stopImmediatePropagation();
86  }
87
88  ondragend() {
89    this.dragging = false;
90    globals.rafScheduler.scheduleFullRedraw();
91  }
92
93  ondragover(e: DragEvent) {
94    if (this.dragging) return;
95    if (!(e.target instanceof HTMLElement)) return;
96    const dataTransfer = e.dataTransfer;
97    if (dataTransfer === null) return;
98    if (!dataTransfer.types.includes('perfetto/track')) return;
99    dataTransfer.dropEffect = 'move';
100    e.preventDefault();
101
102    // Apply some hysteresis to the drop logic so that the lightened border
103    // changes only when we get close enough to the border.
104    if (e.offsetY < e.target.scrollHeight / 3) {
105      this.dropping = 'before';
106    } else if (e.offsetY > e.target.scrollHeight / 3 * 2) {
107      this.dropping = 'after';
108    }
109    globals.rafScheduler.scheduleFullRedraw();
110  }
111
112  ondragleave() {
113    this.dropping = undefined;
114    globals.rafScheduler.scheduleFullRedraw();
115  }
116
117  ondrop(e: DragEvent) {
118    if (this.dropping === undefined) return;
119    const dataTransfer = e.dataTransfer;
120    if (dataTransfer === null) return;
121    globals.rafScheduler.scheduleFullRedraw();
122    const srcId = dataTransfer.getData('perfetto/track');
123    const dstId = this.attrs!.trackState.id;
124    globals.dispatch(Actions.moveTrack({srcId, op: this.dropping, dstId}));
125    this.dropping = undefined;
126  }
127}
128
129export interface TrackContentAttrs { track: Track; }
130export class TrackContent implements m.ClassComponent<TrackContentAttrs> {
131  view({attrs}: m.CVnode<TrackContentAttrs>) {
132    return m('.track-content', {
133      onmousemove: (e: MouseEvent) => {
134        attrs.track.onMouseMove({x: e.layerX, y: e.layerY});
135        globals.rafScheduler.scheduleRedraw();
136      },
137      onmouseout: () => {
138        attrs.track.onMouseOut();
139        globals.rafScheduler.scheduleRedraw();
140      },
141      onclick: (e:MouseEvent) => {
142        // If we are selecting a timespan - do not pass the click to the track.
143        const selection = globals.state.currentSelection;
144        if (selection && selection.kind === 'TIMESPAN') return;
145
146        if (attrs.track.onMouseClick({x: e.layerX, y: e.layerY})) {
147          e.stopPropagation();
148        }
149        globals.rafScheduler.scheduleRedraw();
150      }
151    });
152  }
153}
154
155interface TrackComponentAttrs {
156  trackState: TrackState;
157  track: Track;
158}
159class TrackComponent implements m.ClassComponent<TrackComponentAttrs> {
160  view({attrs}: m.CVnode<TrackComponentAttrs>) {
161    return m('.track', [
162      m(TrackShell, {trackState: attrs.trackState}),
163      m(TrackContent, {track: attrs.track})
164    ]);
165  }
166}
167
168interface TrackButtonAttrs {
169  action: DeferredAction;
170  i: string;
171}
172class TrackButton implements m.ClassComponent<TrackButtonAttrs> {
173  view({attrs}: m.CVnode<TrackButtonAttrs>) {
174    return m(
175        'i.material-icons.track-button',
176        {
177          onclick: () => globals.dispatch(attrs.action),
178        },
179        attrs.i);
180  }
181}
182
183interface TrackPanelAttrs {
184  id: string;
185}
186
187export class TrackPanel extends Panel<TrackPanelAttrs> {
188  private track: Track;
189  private trackState: TrackState;
190  constructor(vnode: m.CVnode<TrackPanelAttrs>) {
191    super();
192    this.trackState = globals.state.tracks[vnode.attrs.id];
193    const trackCreator = trackRegistry.get(this.trackState.kind);
194    this.track = trackCreator.create(this.trackState);
195  }
196
197  view() {
198    return m(
199        '.track',
200        {
201          style: {
202            height: `${this.track.getHeight()}px`,
203          }
204        },
205        [
206          m(TrackShell, {trackState: this.trackState}),
207          m(TrackContent, {track: this.track})
208        ]);
209    return m(TrackComponent, {trackState: this.trackState, track: this.track});
210  }
211
212  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
213    ctx.save();
214    drawGridLines(
215        ctx,
216        globals.frontendLocalState.timeScale,
217        globals.frontendLocalState.visibleWindowTime,
218        size.width,
219        size.height);
220
221    ctx.translate(TRACK_SHELL_WIDTH, 0);
222
223    this.track.renderCanvas(ctx);
224
225    ctx.restore();
226
227    const localState = globals.frontendLocalState;
228    // Draw vertical line when hovering on the the notes panel.
229    if (localState.showNotePreview) {
230      drawVerticalLineAtTime(ctx,
231                             localState.timeScale,
232                             localState.hoveredTimestamp,
233                             size.height,
234                             `#aaa`);
235    }
236    // Draw vertical line when shift is pressed.
237    if (localState.showTimeSelectPreview) {
238      drawVerticalLineAtTime(ctx,
239                             localState.timeScale,
240                             localState.hoveredTimestamp,
241                             size.height,
242                             `rgb(52,69,150)`);
243    }
244
245    if (globals.state.currentSelection !== null) {
246      if (globals.state.currentSelection.kind === 'NOTE') {
247        const note = globals.state.notes[globals.state.currentSelection.id];
248        drawVerticalLineAtTime(ctx,
249                               localState.timeScale,
250                               note.timestamp,
251                               size.height,
252                               note.color);
253      }
254      if (globals.state.currentSelection.kind === 'TIMESPAN') {
255        drawVerticalSelection(ctx,
256                              localState.timeScale,
257                              globals.state.currentSelection.startTs,
258                              globals.state.currentSelection.endTs,
259                              size.height,
260                              `rgba(52,69,150,0.3)`);
261      }
262      if (globals.state.currentSelection.kind === 'SLICE' &&
263          globals.sliceDetails.wakeupTs !== undefined) {
264        drawVerticalLineAtTime(
265            ctx,
266            localState.timeScale,
267            globals.sliceDetails.wakeupTs,
268            size.height,
269            `black`);
270      }
271    }
272  }
273}
274