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 m from 'mithril'; 16import {BigintMath} from '../base/bigint_math'; 17 18import {Actions} from '../common/actions'; 19import {featureFlags} from '../common/feature_flags'; 20 21import {TRACK_SHELL_WIDTH} from './css_constants'; 22import {DetailsPanel} from './details_panel'; 23import {globals} from './globals'; 24import {NotesPanel} from './notes_panel'; 25import {OverviewTimelinePanel} from './overview_timeline_panel'; 26import {createPage} from './pages'; 27import {PanAndZoomHandler} from './pan_and_zoom_handler'; 28import {AnyAttrsVnode, PanelContainer} from './panel_container'; 29import {TickmarkPanel} from './tickmark_panel'; 30import {TimeAxisPanel} from './time_axis_panel'; 31import {TimeSelectionPanel} from './time_selection_panel'; 32import {DISMISSED_PANNING_HINT_KEY} from './topbar'; 33import {TrackGroupPanel} from './track_group_panel'; 34import {TrackPanel} from './track_panel'; 35 36const SIDEBAR_WIDTH = 256; 37 38const OVERVIEW_PANEL_FLAG = featureFlags.register({ 39 id: 'overviewVisible', 40 name: 'Overview Panel', 41 description: 'Show the panel providing an overview of the trace', 42 defaultValue: true, 43}); 44 45// Checks if the mousePos is within 3px of the start or end of the 46// current selected time range. 47function onTimeRangeBoundary(mousePos: number): 'START'|'END'|null { 48 const selection = globals.state.currentSelection; 49 if (selection !== null && selection.kind === 'AREA') { 50 // If frontend selectedArea exists then we are in the process of editing the 51 // time range and need to use that value instead. 52 const area = globals.frontendLocalState.selectedArea ? 53 globals.frontendLocalState.selectedArea : 54 globals.state.areas[selection.areaId]; 55 const {visibleTimeScale} = globals.frontendLocalState; 56 const start = visibleTimeScale.tpTimeToPx(area.start); 57 const end = visibleTimeScale.tpTimeToPx(area.end); 58 const startDrag = mousePos - TRACK_SHELL_WIDTH; 59 const startDistance = Math.abs(start - startDrag); 60 const endDistance = Math.abs(end - startDrag); 61 const range = 3 * window.devicePixelRatio; 62 // We might be within 3px of both boundaries but we should choose 63 // the closest one. 64 if (startDistance < range && startDistance <= endDistance) return 'START'; 65 if (endDistance < range && endDistance <= startDistance) return 'END'; 66 } 67 return null; 68} 69 70export interface TrackGroupAttrs { 71 header: AnyAttrsVnode; 72 collapsed: boolean; 73 childTracks: AnyAttrsVnode[]; 74} 75 76export class TrackGroup implements m.ClassComponent<TrackGroupAttrs> { 77 view() { 78 // TrackGroup component acts as a holder for a bunch of tracks rendered 79 // together: the actual rendering happens in PanelContainer. In order to 80 // avoid confusion, this method remains empty. 81 } 82} 83 84/** 85 * Top-most level component for the viewer page. Holds tracks, brush timeline, 86 * panels, and everything else that's part of the main trace viewer page. 87 */ 88class TraceViewer implements m.ClassComponent { 89 private onResize: () => void = () => {}; 90 private zoomContent?: PanAndZoomHandler; 91 // Used to prevent global deselection if a pan/drag select occurred. 92 private keepCurrentSelection = false; 93 94 oncreate(vnode: m.CVnodeDOM) { 95 const frontendLocalState = globals.frontendLocalState; 96 const updateDimensions = () => { 97 const rect = vnode.dom.getBoundingClientRect(); 98 frontendLocalState.updateLocalLimits( 99 0, 100 rect.width - TRACK_SHELL_WIDTH - 101 frontendLocalState.getScrollbarWidth()); 102 }; 103 104 updateDimensions(); 105 106 // TODO: Do resize handling better. 107 this.onResize = () => { 108 updateDimensions(); 109 globals.rafScheduler.scheduleFullRedraw(); 110 }; 111 112 // Once ResizeObservers are out, we can stop accessing the window here. 113 window.addEventListener('resize', this.onResize); 114 115 const panZoomEl = 116 vnode.dom.querySelector('.pan-and-zoom-content') as HTMLElement; 117 118 this.zoomContent = new PanAndZoomHandler({ 119 element: panZoomEl, 120 contentOffsetX: SIDEBAR_WIDTH, 121 onPanned: (pannedPx: number) => { 122 const { 123 visibleTimeScale, 124 } = globals.frontendLocalState; 125 126 this.keepCurrentSelection = true; 127 const tDelta = visibleTimeScale.pxDeltaToDuration(pannedPx); 128 frontendLocalState.panVisibleWindow(tDelta); 129 130 // If the user has panned they no longer need the hint. 131 localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true'); 132 globals.rafScheduler.scheduleRedraw(); 133 }, 134 onZoomed: (zoomedPositionPx: number, zoomRatio: number) => { 135 // TODO(hjd): Avoid hardcoding TRACK_SHELL_WIDTH. 136 // TODO(hjd): Improve support for zooming in overview timeline. 137 const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH; 138 const rect = vnode.dom.getBoundingClientRect(); 139 const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH); 140 frontendLocalState.zoomVisibleWindow(1 - zoomRatio, centerPoint); 141 globals.rafScheduler.scheduleRedraw(); 142 }, 143 editSelection: (currentPx: number) => { 144 return onTimeRangeBoundary(currentPx) !== null; 145 }, 146 onSelection: ( 147 dragStartX: number, 148 dragStartY: number, 149 prevX: number, 150 currentX: number, 151 currentY: number, 152 editing: boolean) => { 153 const traceTime = globals.state.traceTime; 154 const {visibleTimeScale} = frontendLocalState; 155 this.keepCurrentSelection = true; 156 if (editing) { 157 const selection = globals.state.currentSelection; 158 if (selection !== null && selection.kind === 'AREA') { 159 const area = globals.frontendLocalState.selectedArea ? 160 globals.frontendLocalState.selectedArea : 161 globals.state.areas[selection.areaId]; 162 let newTime = 163 visibleTimeScale.pxToHpTime(currentX - TRACK_SHELL_WIDTH) 164 .toTPTime(); 165 // Have to check again for when one boundary crosses over the other. 166 const curBoundary = onTimeRangeBoundary(prevX); 167 if (curBoundary == null) return; 168 const keepTime = curBoundary === 'START' ? area.end : area.start; 169 // Don't drag selection outside of current screen. 170 if (newTime < keepTime) { 171 newTime = BigintMath.max( 172 newTime, visibleTimeScale.timeSpan.start.toTPTime()); 173 } else { 174 newTime = BigintMath.max( 175 newTime, visibleTimeScale.timeSpan.end.toTPTime()); 176 } 177 // When editing the time range we always use the saved tracks, 178 // since these will not change. 179 frontendLocalState.selectArea( 180 BigintMath.max( 181 BigintMath.min(keepTime, newTime), traceTime.start), 182 BigintMath.min( 183 BigintMath.max(keepTime, newTime), traceTime.end), 184 globals.state.areas[selection.areaId].tracks); 185 } 186 } else { 187 let startPx = Math.min(dragStartX, currentX) - TRACK_SHELL_WIDTH; 188 let endPx = Math.max(dragStartX, currentX) - TRACK_SHELL_WIDTH; 189 if (startPx < 0 && endPx < 0) return; 190 startPx = Math.max(startPx, visibleTimeScale.pxSpan.start); 191 endPx = Math.min(endPx, visibleTimeScale.pxSpan.end); 192 frontendLocalState.selectArea( 193 visibleTimeScale.pxToHpTime(startPx).toTPTime('floor'), 194 visibleTimeScale.pxToHpTime(endPx).toTPTime('ceil'), 195 ); 196 frontendLocalState.areaY.start = dragStartY; 197 frontendLocalState.areaY.end = currentY; 198 } 199 globals.rafScheduler.scheduleRedraw(); 200 }, 201 endSelection: (edit: boolean) => { 202 globals.frontendLocalState.areaY.start = undefined; 203 globals.frontendLocalState.areaY.end = undefined; 204 const area = globals.frontendLocalState.selectedArea; 205 // If we are editing we need to pass the current id through to ensure 206 // the marked area with that id is also updated. 207 if (edit) { 208 const selection = globals.state.currentSelection; 209 if (selection !== null && selection.kind === 'AREA' && area) { 210 globals.dispatch( 211 Actions.editArea({area, areaId: selection.areaId})); 212 } 213 } else if (area) { 214 globals.makeSelection(Actions.selectArea({area})); 215 } 216 // Now the selection has ended we stored the final selected area in the 217 // global state and can remove the in progress selection from the 218 // frontendLocalState. 219 globals.frontendLocalState.deselectArea(); 220 // Full redraw to color track shell. 221 globals.rafScheduler.scheduleFullRedraw(); 222 }, 223 }); 224 } 225 226 onremove() { 227 window.removeEventListener('resize', this.onResize); 228 if (this.zoomContent) this.zoomContent.shutdown(); 229 } 230 231 view() { 232 const scrollingPanels: AnyAttrsVnode[] = globals.state.scrollingTracks.map( 233 (id) => m(TrackPanel, {key: id, id, selectable: true})); 234 235 for (const group of Object.values(globals.state.trackGroups)) { 236 const headerPanel = m(TrackGroupPanel, { 237 trackGroupId: group.id, 238 key: `trackgroup-${group.id}`, 239 selectable: true, 240 }); 241 242 const childTracks: AnyAttrsVnode[] = []; 243 // The first track is the summary track, and is displayed as part of the 244 // group panel, we don't want to display it twice so we start from 1. 245 if (!group.collapsed) { 246 for (let i = 1; i < group.tracks.length; ++i) { 247 const id = group.tracks[i]; 248 childTracks.push(m(TrackPanel, { 249 key: `track-${group.id}-${id}`, 250 id, 251 selectable: true, 252 })); 253 } 254 } 255 scrollingPanels.push(m(TrackGroup, { 256 header: headerPanel, 257 collapsed: group.collapsed, 258 childTracks, 259 } as TrackGroupAttrs)); 260 } 261 262 const overviewPanel = []; 263 if (OVERVIEW_PANEL_FLAG.get()) { 264 overviewPanel.push(m(OverviewTimelinePanel, {key: 'overview'})); 265 } 266 267 return m( 268 '.page', 269 m('.split-panel', 270 m('.pan-and-zoom-content', 271 { 272 onclick: () => { 273 // TODO(stevegolton): Make it possible to click buttons and 274 // things on this element without deselecting the selected 275 // element! 276 // We don't want to deselect when panning/drag selecting. 277 if (this.keepCurrentSelection) { 278 this.keepCurrentSelection = false; 279 return; 280 } 281 globals.makeSelection(Actions.deselect({})); 282 }, 283 }, 284 m('.pinned-panel-container', m(PanelContainer, { 285 doesScroll: false, 286 panels: [ 287 ...overviewPanel, 288 m(TimeAxisPanel, {key: 'timeaxis'}), 289 m(TimeSelectionPanel, {key: 'timeselection'}), 290 m(NotesPanel, {key: 'notes'}), 291 m(TickmarkPanel, {key: 'searchTickmarks'}), 292 ...globals.state.pinnedTracks.map( 293 (id) => m(TrackPanel, {key: id, id, selectable: true})), 294 ], 295 kind: 'OVERVIEW', 296 })), 297 m('.scrolling-panel-container', m(PanelContainer, { 298 doesScroll: true, 299 panels: scrollingPanels, 300 kind: 'TRACKS', 301 })))), 302 m(DetailsPanel)); 303 } 304} 305 306export const ViewerPage = createPage({ 307 view() { 308 return m(TraceViewer); 309 }, 310}); 311