1// Copyright (C) 2024 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 {canvasSave} from '../../base/canvas_utils'; 17import {DisposableStack} from '../../base/disposable_stack'; 18import {toHTMLElement} from '../../base/dom_utils'; 19import {Rect2D, Size2D} from '../../base/geom'; 20import {assertExists} from '../../base/logging'; 21import {TimeScale} from '../../base/time_scale'; 22import {ZonedInteractionHandler} from '../../base/zoned_interaction_handler'; 23import {TraceImpl} from '../../core/trace_impl'; 24import { 25 VirtualOverlayCanvas, 26 VirtualOverlayCanvasDrawContext, 27} from '../../widgets/virtual_overlay_canvas'; 28import {TRACK_SHELL_WIDTH} from '../css_constants'; 29import {NotesPanel} from './notes_panel'; 30import {TickmarkPanel} from './tickmark_panel'; 31import {TimeAxisPanel} from './time_axis_panel'; 32import {TimeSelectionPanel} from './time_selection_panel'; 33import { 34 shiftDragPanInteraction, 35 wheelNavigationInteraction, 36} from './timeline_interactions'; 37 38export interface TimelineHeaderAttrs { 39 // The trace to use for timeline access et al. 40 readonly trace: TraceImpl; 41 42 // Called when the visible area of the timeline changes size. This is the area 43 // to the right of the header is actually rendered on. 44 onTimelineBoundsChange?(rect: Rect2D): void; 45 46 readonly className?: string; 47} 48 49// TODO(stevegolton): The panel concept has been largely removed. It's just 50// defined here so that we don't have to change the implementation of the 51// various header panels listed here. We should consolidate this in the future. 52interface Panel { 53 readonly height: number; 54 render(): m.Children; 55 renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D): void; 56} 57 58/** 59 * This component defines the header of the timeline and handles it's mouse 60 * interactions. 61 * 62 * The timeline header contains: 63 * - The axis (ticks) and time labels 64 * - The selection bar 65 * - The notes bar 66 * - The tickmark bar (highlights that appear when searching) 67 */ 68export class TimelineHeader implements m.ClassComponent<TimelineHeaderAttrs> { 69 private readonly trash = new DisposableStack(); 70 private readonly trace: TraceImpl; 71 private readonly panels: ReadonlyArray<Panel>; 72 private interactions?: ZonedInteractionHandler; 73 74 constructor({attrs}: m.Vnode<TimelineHeaderAttrs>) { 75 this.trace = attrs.trace; 76 this.panels = [ 77 new TimeAxisPanel(attrs.trace), 78 new TimeSelectionPanel(attrs.trace), 79 new NotesPanel(attrs.trace), 80 new TickmarkPanel(attrs.trace), 81 ]; 82 } 83 84 view({attrs}: m.Vnode<TimelineHeaderAttrs>) { 85 return m( 86 '.pf-timeline-header', 87 {className: attrs.className}, 88 m( 89 VirtualOverlayCanvas, 90 { 91 onMount: (redrawCanvas) => 92 attrs.trace.raf.addCanvasRedrawCallback(redrawCanvas), 93 disableCanvasRedrawOnMithrilUpdates: true, 94 onCanvasRedraw: (ctx) => { 95 const rect = new Rect2D({ 96 left: TRACK_SHELL_WIDTH, 97 right: ctx.virtualCanvasSize.width, 98 top: 0, 99 bottom: 0, 100 }); 101 attrs.onTimelineBoundsChange?.(rect); 102 this.drawCanvas(ctx); 103 }, 104 }, 105 this.panels.map((p) => p.render()), 106 ), 107 ); 108 } 109 110 oncreate({dom}: m.VnodeDOM<TimelineHeaderAttrs>) { 111 const timelineHeaderElement = toHTMLElement(dom); 112 this.interactions = new ZonedInteractionHandler(timelineHeaderElement); 113 this.trash.use(this.interactions); 114 } 115 116 onremove() { 117 this.trash.dispose(); 118 } 119 120 private drawCanvas({ 121 ctx, 122 virtualCanvasSize, 123 }: VirtualOverlayCanvasDrawContext) { 124 let top = 0; 125 for (const p of this.panels) { 126 using _ = canvasSave(ctx); 127 ctx.translate(0, top); 128 p.renderCanvas(ctx, {width: virtualCanvasSize.width, height: p.height}); 129 top += p.height; 130 } 131 132 const timelineRect = new Rect2D({ 133 left: TRACK_SHELL_WIDTH, 134 top: 0, 135 right: virtualCanvasSize.width, 136 bottom: virtualCanvasSize.height, 137 }); 138 139 // Always grab the latest visible window and create a timescale 140 // out of it. 141 const visibleWindow = this.trace.timeline.visibleWindow; 142 const timescale = new TimeScale(visibleWindow, timelineRect); 143 144 assertExists(this.interactions).update([ 145 shiftDragPanInteraction(this.trace, timelineRect, timescale), 146 wheelNavigationInteraction(this.trace, timelineRect, timescale), 147 { 148 // Allow making area selections (no tracks) by dragging on the header 149 // timeline. 150 id: 'area-selection', 151 area: timelineRect, 152 drag: { 153 minDistance: 1, 154 cursorWhileDragging: 'text', 155 onDrag: (e) => { 156 const dragRect = Rect2D.fromPoints(e.dragStart, e.dragCurrent); 157 const timeSpan = timescale 158 .pxSpanToHpTimeSpan(dragRect) 159 .toTimeSpan(); 160 this.trace.timeline.selectedSpan = timeSpan; 161 }, 162 onDragEnd: (e) => { 163 const dragRect = Rect2D.fromPoints(e.dragStart, e.dragCurrent); 164 const timeSpan = timescale 165 .pxSpanToHpTimeSpan(dragRect) 166 .toTimeSpan(); 167 this.trace.selection.selectArea({ 168 start: timeSpan.start, 169 end: timeSpan.end, 170 trackUris: [], 171 }); 172 this.trace.timeline.selectedSpan = undefined; 173 }, 174 }, 175 }, 176 ]); 177 } 178} 179