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 m from 'mithril'; 17 18import {assertExists} from '../base/logging'; 19import {Actions} from '../common/actions'; 20import { 21 getContainingTrackId, 22 TrackGroupState, 23 TrackState, 24} from '../common/state'; 25 26import {globals} from './globals'; 27import {drawGridLines} from './gridline_helper'; 28import { 29 BLANK_CHECKBOX, 30 CHECKBOX, 31 EXPAND_DOWN, 32 EXPAND_UP, 33 INDETERMINATE_CHECKBOX, 34} from './icons'; 35import {Panel, PanelSize} from './panel'; 36import {Track} from './track'; 37import {TrackContent} from './track_panel'; 38import {trackRegistry} from './track_registry'; 39import { 40 drawVerticalLineAtTime, 41} from './vertical_line_helper'; 42 43interface Attrs { 44 trackGroupId: string; 45 selectable: boolean; 46} 47 48export class TrackGroupPanel extends Panel<Attrs> { 49 private readonly trackGroupId: string; 50 private shellWidth = 0; 51 private backgroundColor = '#ffffff'; // Updated from CSS later. 52 private summaryTrack: Track|undefined; 53 54 constructor({attrs}: m.CVnode<Attrs>) { 55 super(); 56 this.trackGroupId = attrs.trackGroupId; 57 const trackCreator = trackRegistry.get(this.summaryTrackState.kind); 58 const engineId = this.summaryTrackState.engineId; 59 const engine = globals.engines.get(engineId); 60 if (engine !== undefined) { 61 this.summaryTrack = 62 trackCreator.create({trackId: this.summaryTrackState.id, engine}); 63 } 64 } 65 66 get trackGroupState(): TrackGroupState { 67 return assertExists(globals.state.trackGroups[this.trackGroupId]); 68 } 69 70 get summaryTrackState(): TrackState { 71 return assertExists(globals.state.tracks[this.trackGroupState.tracks[0]]); 72 } 73 74 view({attrs}: m.CVnode<Attrs>) { 75 const collapsed = this.trackGroupState.collapsed; 76 let name = this.trackGroupState.name; 77 if (name[0] === '/') { 78 name = StripPathFromExecutable(name); 79 } 80 81 // The shell should be highlighted if the current search result is inside 82 // this track group. 83 let highlightClass = ''; 84 const searchIndex = globals.state.searchIndex; 85 if (searchIndex !== -1) { 86 const trackId = globals.currentSearchResults.trackIds[searchIndex]; 87 const parentTrackId = getContainingTrackId(globals.state, trackId); 88 if (parentTrackId === attrs.trackGroupId) { 89 highlightClass = 'flash'; 90 } 91 } 92 93 const selection = globals.state.currentSelection; 94 95 const trackGroup = globals.state.trackGroups[attrs.trackGroupId]; 96 let checkBox = BLANK_CHECKBOX; 97 if (selection !== null && selection.kind === 'AREA') { 98 const selectedArea = globals.state.areas[selection.areaId]; 99 if (selectedArea.tracks.includes(attrs.trackGroupId) && 100 trackGroup.tracks.every((id) => selectedArea.tracks.includes(id))) { 101 checkBox = CHECKBOX; 102 } else if ( 103 selectedArea.tracks.includes(attrs.trackGroupId) || 104 trackGroup.tracks.some((id) => selectedArea.tracks.includes(id))) { 105 checkBox = INDETERMINATE_CHECKBOX; 106 } 107 } 108 109 let child = null; 110 if (this.summaryTrackState.labels && 111 this.summaryTrackState.labels.length > 0) { 112 child = this.summaryTrackState.labels.join(', '); 113 } 114 115 return m( 116 `.track-group-panel[collapsed=${collapsed}]`, 117 {id: 'track_' + this.trackGroupId}, 118 m(`.shell`, 119 { 120 onclick: (e: MouseEvent) => { 121 globals.dispatch(Actions.toggleTrackGroupCollapsed({ 122 trackGroupId: attrs.trackGroupId, 123 })), 124 e.stopPropagation(); 125 }, 126 class: `${highlightClass}`, 127 }, 128 129 m('.fold-button', 130 m('i.material-icons', 131 this.trackGroupState.collapsed ? EXPAND_DOWN : EXPAND_UP)), 132 m('.title-wrapper', 133 m('h1.track-title', 134 {title: name}, 135 name, 136 ('namespace' in this.summaryTrackState.config) && 137 m('span.chip', 'metric')), 138 (this.trackGroupState.collapsed && child !== null) ? 139 m('h2.track-subtitle', child) : 140 null), 141 selection && selection.kind === 'AREA' ? 142 m('i.material-icons.track-button', 143 { 144 onclick: (e: MouseEvent) => { 145 globals.dispatch(Actions.toggleTrackSelection( 146 {id: attrs.trackGroupId, isTrackGroup: true})); 147 e.stopPropagation(); 148 }, 149 }, 150 checkBox) : 151 ''), 152 153 this.summaryTrack ? 154 m(TrackContent, 155 {track: this.summaryTrack}, 156 (!this.trackGroupState.collapsed && child !== null) ? 157 m('span', child) : 158 null) : 159 null); 160 } 161 162 oncreate(vnode: m.CVnodeDOM<Attrs>) { 163 this.onupdate(vnode); 164 } 165 166 onupdate({dom}: m.CVnodeDOM<Attrs>) { 167 const shell = assertExists(dom.querySelector('.shell')); 168 this.shellWidth = shell.getBoundingClientRect().width; 169 // TODO(andrewbb): move this to css_constants 170 if (this.trackGroupState.collapsed) { 171 this.backgroundColor = 172 getComputedStyle(dom).getPropertyValue('--collapsed-background'); 173 } else { 174 this.backgroundColor = 175 getComputedStyle(dom).getPropertyValue('--expanded-background'); 176 } 177 if (this.summaryTrack !== undefined) { 178 this.summaryTrack.onFullRedraw(); 179 } 180 } 181 182 onremove() { 183 if (this.summaryTrack !== undefined) { 184 this.summaryTrack.onDestroy(); 185 this.summaryTrack = undefined; 186 } 187 } 188 189 highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) { 190 const {visibleTimeScale} = globals.frontendLocalState; 191 const selection = globals.state.currentSelection; 192 if (!selection || selection.kind !== 'AREA') return; 193 const selectedArea = globals.state.areas[selection.areaId]; 194 const selectedAreaDuration = selectedArea.end - selectedArea.start; 195 if (selectedArea.tracks.includes(this.trackGroupId)) { 196 ctx.fillStyle = 'rgba(131, 152, 230, 0.3)'; 197 ctx.fillRect( 198 visibleTimeScale.tpTimeToPx(selectedArea.start) + this.shellWidth, 199 0, 200 visibleTimeScale.durationToPx(selectedAreaDuration), 201 size.height); 202 } 203 } 204 205 renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { 206 const collapsed = this.trackGroupState.collapsed; 207 208 ctx.fillStyle = this.backgroundColor; 209 ctx.fillRect(0, 0, size.width, size.height); 210 211 if (!collapsed) return; 212 213 this.highlightIfTrackSelected(ctx, size); 214 215 drawGridLines( 216 ctx, 217 size.width, 218 size.height); 219 220 ctx.save(); 221 ctx.translate(this.shellWidth, 0); 222 if (this.summaryTrack) { 223 this.summaryTrack.render(ctx); 224 } 225 ctx.restore(); 226 227 this.highlightIfTrackSelected(ctx, size); 228 229 const {visibleTimeScale} = globals.frontendLocalState; 230 // Draw vertical line when hovering on the notes panel. 231 if (globals.state.hoveredNoteTimestamp !== -1n) { 232 drawVerticalLineAtTime( 233 ctx, 234 visibleTimeScale, 235 globals.state.hoveredNoteTimestamp, 236 size.height, 237 `#aaa`); 238 } 239 if (globals.state.hoverCursorTimestamp !== -1n) { 240 drawVerticalLineAtTime( 241 ctx, 242 visibleTimeScale, 243 globals.state.hoverCursorTimestamp, 244 size.height, 245 `#344596`); 246 } 247 248 if (globals.state.currentSelection !== null) { 249 if (globals.state.currentSelection.kind === 'SLICE' && 250 globals.sliceDetails.wakeupTs !== undefined) { 251 drawVerticalLineAtTime( 252 ctx, 253 visibleTimeScale, 254 globals.sliceDetails.wakeupTs, 255 size.height, 256 `black`); 257 } 258 } 259 // All marked areas should have semi-transparent vertical lines 260 // marking the start and end. 261 for (const note of Object.values(globals.state.notes)) { 262 if (note.noteType === 'AREA') { 263 const transparentNoteColor = 264 'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)'; 265 drawVerticalLineAtTime( 266 ctx, 267 visibleTimeScale, 268 globals.state.areas[note.areaId].start, 269 size.height, 270 transparentNoteColor, 271 1); 272 drawVerticalLineAtTime( 273 ctx, 274 visibleTimeScale, 275 globals.state.areas[note.areaId].end, 276 size.height, 277 transparentNoteColor, 278 1); 279 } else if (note.noteType === 'DEFAULT') { 280 drawVerticalLineAtTime( 281 ctx, visibleTimeScale, note.timestamp, size.height, note.color); 282 } 283 } 284 } 285} 286 287function StripPathFromExecutable(path: string) { 288 return path.split('/').slice(-1)[0]; 289} 290