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 {hex} from 'color-convert'; 16import * as m from 'mithril'; 17 18import {Actions} from '../common/actions'; 19import {TrackState} from '../common/state'; 20 21import {TRACK_SHELL_WIDTH} from './css_constants'; 22import {PerfettoMouseEvent} from './events'; 23import {globals} from './globals'; 24import {drawGridLines} from './gridline_helper'; 25import {BLANK_CHECKBOX, CHECKBOX, STAR, STAR_BORDER} from './icons'; 26import {Panel, PanelSize} from './panel'; 27import {verticalScrollToTrack} from './scroll_helper'; 28import {SliceRect, Track} from './track'; 29import {trackRegistry} from './track_registry'; 30import { 31 drawVerticalLineAtTime, 32} from './vertical_line_helper'; 33 34function isPinned(id: string) { 35 return globals.state.pinnedTracks.indexOf(id) !== -1; 36} 37 38function isSelected(id: string) { 39 const selection = globals.state.currentSelection; 40 if (selection === null || selection.kind !== 'AREA') return false; 41 const selectedArea = globals.state.areas[selection.areaId]; 42 return selectedArea.tracks.includes(id); 43} 44 45interface TrackShellAttrs { 46 track: Track; 47 trackState: TrackState; 48} 49 50class TrackShell implements m.ClassComponent<TrackShellAttrs> { 51 // Set to true when we click down and drag the 52 private dragging = false; 53 private dropping: 'before'|'after'|undefined = undefined; 54 private attrs?: TrackShellAttrs; 55 56 oninit(vnode: m.Vnode<TrackShellAttrs>) { 57 this.attrs = vnode.attrs; 58 } 59 60 view({attrs}: m.CVnode<TrackShellAttrs>) { 61 // The shell should be highlighted if the current search result is inside 62 // this track. 63 let highlightClass = ''; 64 const searchIndex = globals.frontendLocalState.searchIndex; 65 if (searchIndex !== -1) { 66 const trackId = globals.currentSearchResults 67 .trackIds[globals.frontendLocalState.searchIndex]; 68 if (trackId === attrs.trackState.id) { 69 highlightClass = 'flash'; 70 } 71 } 72 73 const dragClass = this.dragging ? `drag` : ''; 74 const dropClass = this.dropping ? `drop-${this.dropping}` : ''; 75 return m( 76 `.track-shell[draggable=true]`, 77 { 78 class: `${highlightClass} ${dragClass} ${dropClass}`, 79 onmousedown: this.onmousedown.bind(this), 80 ondragstart: this.ondragstart.bind(this), 81 ondragend: this.ondragend.bind(this), 82 ondragover: this.ondragover.bind(this), 83 ondragleave: this.ondragleave.bind(this), 84 ondrop: this.ondrop.bind(this), 85 }, 86 m('h1', 87 { 88 title: attrs.trackState.name, 89 }, 90 attrs.trackState.name), 91 m('.track-buttons', 92 attrs.track.getTrackShellButtons(), 93 m(TrackButton, { 94 action: () => { 95 globals.dispatch( 96 Actions.toggleTrackPinned({trackId: attrs.trackState.id})); 97 }, 98 i: isPinned(attrs.trackState.id) ? STAR : STAR_BORDER, 99 tooltip: isPinned(attrs.trackState.id) ? 'Unpin' : 'Pin to top', 100 showButton: isPinned(attrs.trackState.id), 101 }), 102 globals.state.currentSelection !== null && 103 globals.state.currentSelection.kind === 'AREA' ? 104 m(TrackButton, { 105 action: (e: PerfettoMouseEvent) => { 106 globals.dispatch(Actions.toggleTrackSelection( 107 {id: attrs.trackState.id, isTrackGroup: false})); 108 e.stopPropagation(); 109 }, 110 i: isSelected(attrs.trackState.id) ? CHECKBOX : BLANK_CHECKBOX, 111 tooltip: isSelected(attrs.trackState.id) ? 112 'Remove track' : 113 'Add track to selection', 114 showButton: true, 115 }) : 116 '')); 117 } 118 119 onmousedown(e: MouseEvent) { 120 // Prevent that the click is intercepted by the PanAndZoomHandler and that 121 // we start panning while dragging. 122 e.stopPropagation(); 123 } 124 125 ondragstart(e: DragEvent) { 126 const dataTransfer = e.dataTransfer; 127 if (dataTransfer === null) return; 128 this.dragging = true; 129 globals.rafScheduler.scheduleFullRedraw(); 130 dataTransfer.setData('perfetto/track', `${this.attrs!.trackState.id}`); 131 dataTransfer.setDragImage(new Image(), 0, 0); 132 e.stopImmediatePropagation(); 133 } 134 135 ondragend() { 136 this.dragging = false; 137 globals.rafScheduler.scheduleFullRedraw(); 138 } 139 140 ondragover(e: DragEvent) { 141 if (this.dragging) return; 142 if (!(e.target instanceof HTMLElement)) return; 143 const dataTransfer = e.dataTransfer; 144 if (dataTransfer === null) return; 145 if (!dataTransfer.types.includes('perfetto/track')) return; 146 dataTransfer.dropEffect = 'move'; 147 e.preventDefault(); 148 149 // Apply some hysteresis to the drop logic so that the lightened border 150 // changes only when we get close enough to the border. 151 if (e.offsetY < e.target.scrollHeight / 3) { 152 this.dropping = 'before'; 153 } else if (e.offsetY > e.target.scrollHeight / 3 * 2) { 154 this.dropping = 'after'; 155 } 156 globals.rafScheduler.scheduleFullRedraw(); 157 } 158 159 ondragleave() { 160 this.dropping = undefined; 161 globals.rafScheduler.scheduleFullRedraw(); 162 } 163 164 ondrop(e: DragEvent) { 165 if (this.dropping === undefined) return; 166 const dataTransfer = e.dataTransfer; 167 if (dataTransfer === null) return; 168 globals.rafScheduler.scheduleFullRedraw(); 169 const srcId = dataTransfer.getData('perfetto/track'); 170 const dstId = this.attrs!.trackState.id; 171 globals.dispatch(Actions.moveTrack({srcId, op: this.dropping, dstId})); 172 this.dropping = undefined; 173 } 174} 175 176export interface TrackContentAttrs { track: Track; } 177export class TrackContent implements m.ClassComponent<TrackContentAttrs> { 178 private mouseDownX?: number; 179 private mouseDownY?: number; 180 private selectionOccurred = false; 181 182 view({attrs}: m.CVnode<TrackContentAttrs>) { 183 return m('.track-content', { 184 onmousemove: (e: PerfettoMouseEvent) => { 185 attrs.track.onMouseMove({x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY}); 186 globals.rafScheduler.scheduleRedraw(); 187 }, 188 onmouseout: () => { 189 attrs.track.onMouseOut(); 190 globals.rafScheduler.scheduleRedraw(); 191 }, 192 onmousedown: (e: PerfettoMouseEvent) => { 193 this.mouseDownX = e.layerX; 194 this.mouseDownY = e.layerY; 195 }, 196 onmouseup: (e: PerfettoMouseEvent) => { 197 if (this.mouseDownX === undefined || this.mouseDownY === undefined) { 198 return; 199 } 200 if (Math.abs(e.layerX - this.mouseDownX) > 1 || 201 Math.abs(e.layerY - this.mouseDownY) > 1) { 202 this.selectionOccurred = true; 203 } 204 this.mouseDownX = undefined; 205 this.mouseDownY = undefined; 206 }, 207 onclick: (e: PerfettoMouseEvent) => { 208 // This click event occurs after any selection mouse up/drag events 209 // so we have to look if the mouse moved during this click to know 210 // if a selection occurred. 211 if (this.selectionOccurred) { 212 this.selectionOccurred = false; 213 return; 214 } 215 // Returns true if something was selected, so stop propagation. 216 if (attrs.track.onMouseClick( 217 {x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY})) { 218 e.stopPropagation(); 219 } 220 globals.rafScheduler.scheduleRedraw(); 221 } 222 }); 223 } 224} 225 226interface TrackComponentAttrs { 227 trackState: TrackState; 228 track: Track; 229} 230class TrackComponent implements m.ClassComponent<TrackComponentAttrs> { 231 view({attrs}: m.CVnode<TrackComponentAttrs>) { 232 return m( 233 '.track', 234 { 235 style: { 236 height: `${Math.max(24, attrs.track.getHeight())}px`, 237 }, 238 id: 'track_' + attrs.trackState.id, 239 }, 240 [ 241 m(TrackShell, {track: attrs.track, trackState: attrs.trackState}), 242 m(TrackContent, {track: attrs.track}) 243 ]); 244 } 245 246 oncreate({attrs}: m.CVnode<TrackComponentAttrs>) { 247 if (globals.frontendLocalState.scrollToTrackId === attrs.trackState.id) { 248 verticalScrollToTrack(attrs.trackState.id); 249 globals.frontendLocalState.scrollToTrackId = undefined; 250 } 251 } 252} 253 254export interface TrackButtonAttrs { 255 action: (e: PerfettoMouseEvent) => void; 256 i: string; 257 tooltip: string; 258 showButton: boolean; 259} 260export class TrackButton implements m.ClassComponent<TrackButtonAttrs> { 261 view({attrs}: m.CVnode<TrackButtonAttrs>) { 262 return m( 263 'i.material-icons.track-button', 264 { 265 class: `${attrs.showButton ? 'show' : ''}`, 266 onclick: attrs.action, 267 title: attrs.tooltip, 268 }, 269 attrs.i); 270 } 271} 272 273interface TrackPanelAttrs { 274 id: string; 275 selectable: boolean; 276} 277 278export class TrackPanel extends Panel<TrackPanelAttrs> { 279 private track: Track; 280 private trackState: TrackState; 281 constructor(vnode: m.CVnode<TrackPanelAttrs>) { 282 super(); 283 this.trackState = globals.state.tracks[vnode.attrs.id]; 284 const trackCreator = trackRegistry.get(this.trackState.kind); 285 this.track = trackCreator.create(this.trackState); 286 } 287 288 view() { 289 return m(TrackComponent, {trackState: this.trackState, track: this.track}); 290 } 291 292 highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) { 293 const localState = globals.frontendLocalState; 294 const selection = globals.state.currentSelection; 295 if (!selection || selection.kind !== 'AREA') return; 296 const selectedArea = globals.state.areas[selection.areaId]; 297 if (selectedArea.tracks.includes(this.trackState.id)) { 298 const timeScale = localState.timeScale; 299 ctx.fillStyle = 'rgba(131, 152, 230, 0.3)'; 300 ctx.fillRect( 301 timeScale.timeToPx(selectedArea.startSec) + TRACK_SHELL_WIDTH, 302 0, 303 timeScale.deltaTimeToPx(selectedArea.endSec - selectedArea.startSec), 304 size.height); 305 } 306 } 307 308 renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { 309 ctx.save(); 310 311 drawGridLines( 312 ctx, 313 globals.frontendLocalState.timeScale, 314 globals.frontendLocalState.visibleWindowTime, 315 size.width, 316 size.height); 317 318 ctx.translate(TRACK_SHELL_WIDTH, 0); 319 this.track.render(ctx); 320 ctx.restore(); 321 322 this.highlightIfTrackSelected(ctx, size); 323 324 const localState = globals.frontendLocalState; 325 // Draw vertical line when hovering on the notes panel. 326 if (localState.hoveredNoteTimestamp !== -1) { 327 drawVerticalLineAtTime( 328 ctx, 329 localState.timeScale, 330 localState.hoveredNoteTimestamp, 331 size.height, 332 `#aaa`); 333 } 334 if (localState.hoveredLogsTimestamp !== -1) { 335 drawVerticalLineAtTime( 336 ctx, 337 localState.timeScale, 338 localState.hoveredLogsTimestamp, 339 size.height, 340 `#344596`); 341 } 342 if (globals.state.currentSelection !== null) { 343 if (globals.state.currentSelection.kind === 'NOTE') { 344 const note = globals.state.notes[globals.state.currentSelection.id]; 345 if (note.noteType === 'DEFAULT') { 346 drawVerticalLineAtTime( 347 ctx, 348 localState.timeScale, 349 note.timestamp, 350 size.height, 351 note.color); 352 } 353 } 354 355 if (globals.state.currentSelection.kind === 'SLICE' && 356 globals.sliceDetails.wakeupTs !== undefined) { 357 drawVerticalLineAtTime( 358 ctx, 359 localState.timeScale, 360 globals.sliceDetails.wakeupTs, 361 size.height, 362 `black`); 363 } 364 } 365 // All marked areas should have semi-transparent vertical lines 366 // marking the start and end. 367 for (const note of Object.values(globals.state.notes)) { 368 if (note.noteType === 'AREA') { 369 const transparentNoteColor = 370 'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)'; 371 drawVerticalLineAtTime( 372 ctx, 373 localState.timeScale, 374 globals.state.areas[note.areaId].startSec, 375 size.height, 376 transparentNoteColor, 377 1); 378 drawVerticalLineAtTime( 379 ctx, 380 localState.timeScale, 381 globals.state.areas[note.areaId].endSec, 382 size.height, 383 transparentNoteColor, 384 1); 385 } 386 } 387 } 388 389 getSliceRect(tStart: number, tDur: number, depth: number): SliceRect 390 |undefined { 391 return this.track.getSliceRect(tStart, tDur, depth); 392 } 393} 394