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('‎')), 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