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 {raf} from '../../core/raf_scheduler'; 17import {TraceImpl} from '../../core/trace_impl'; 18import {DetailsShell} from '../../widgets/details_shell'; 19import {EmptyState} from '../../widgets/empty_state'; 20import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout'; 21import {Section} from '../../widgets/section'; 22import {Tree, TreeNode} from '../../widgets/tree'; 23import { 24 AreaSelection, 25 NoteSelection, 26 TrackSelection, 27} from '../../public/selection'; 28import {assertUnreachable} from '../../base/logging'; 29import {Button, ButtonBar} from '../../widgets/button'; 30import {NoteEditor} from '../note_editor'; 31 32export interface CurrentSelectionTabAttrs { 33 readonly trace: TraceImpl; 34} 35 36export class CurrentSelectionTab 37 implements m.ClassComponent<CurrentSelectionTabAttrs> 38{ 39 private readonly fadeContext = new FadeContext(); 40 private currentAreaSubTabId?: string; 41 42 view({attrs}: m.Vnode<CurrentSelectionTabAttrs>): m.Children { 43 const section = this.renderCurrentSelectionTabContent(attrs.trace); 44 if (section.isLoading) { 45 return m(FadeIn, section.content); 46 } else { 47 return m(FadeOut, {context: this.fadeContext}, section.content); 48 } 49 } 50 51 private renderCurrentSelectionTabContent(trace: TraceImpl) { 52 const selection = trace.selection.selection; 53 const selectionKind = selection.kind; 54 55 switch (selectionKind) { 56 case 'empty': 57 return this.renderEmptySelection('Nothing selected'); 58 case 'track': 59 return this.renderTrackSelection(trace, selection); 60 case 'track_event': 61 return this.renderTrackEventSelection(trace); 62 case 'area': 63 return this.renderAreaSelection(trace, selection); 64 case 'note': 65 return this.renderNoteSelection(trace, selection); 66 default: 67 assertUnreachable(selectionKind); 68 } 69 } 70 71 private renderEmptySelection(message: string) { 72 return { 73 isLoading: false, 74 content: m(EmptyState, { 75 className: 'pf-noselection', 76 title: message, 77 }), 78 }; 79 } 80 81 private renderTrackSelection(trace: TraceImpl, selection: TrackSelection) { 82 return { 83 isLoading: false, 84 content: this.renderTrackDetailsPanel(trace, selection.trackUri), 85 }; 86 } 87 88 private renderTrackEventSelection(trace: TraceImpl) { 89 // The selection panel has already loaded the details panel for us... let's 90 // hope it's the right one! 91 const detailsPanel = trace.selection.getDetailsPanelForSelection(); 92 if (detailsPanel) { 93 return { 94 isLoading: detailsPanel.isLoading, 95 content: detailsPanel.render(), 96 }; 97 } else { 98 return { 99 isLoading: true, 100 content: 'Loading...', 101 }; 102 } 103 } 104 105 private renderAreaSelection(trace: TraceImpl, selection: AreaSelection) { 106 const tabs = trace.selection.areaSelectionTabs.sort( 107 (a, b) => (b.priority ?? 0) - (a.priority ?? 0), 108 ); 109 110 const renderedTabs = tabs 111 .map((tab) => [tab, tab.render(selection)] as const) 112 .filter(([_, content]) => content !== undefined); 113 114 if (renderedTabs.length === 0) { 115 return this.renderEmptySelection('No details available for selection'); 116 } 117 118 // Find the active tab or just pick the first one if that selected tab is 119 // not available. 120 const [activeTab, tabContent] = 121 renderedTabs.find(([tab]) => tab.id === this.currentAreaSubTabId) ?? 122 renderedTabs[0]; 123 124 return { 125 isLoading: tabContent?.isLoading ?? false, 126 content: m( 127 DetailsShell, 128 { 129 title: 'Area Selection', 130 description: m( 131 ButtonBar, 132 renderedTabs.map(([tab]) => 133 m(Button, { 134 label: tab.name, 135 key: tab.id, 136 active: activeTab === tab, 137 onclick: () => (this.currentAreaSubTabId = tab.id), 138 }), 139 ), 140 ), 141 }, 142 tabContent?.content, 143 ), 144 }; 145 } 146 147 private renderNoteSelection(trace: TraceImpl, selection: NoteSelection) { 148 return { 149 isLoading: false, 150 content: m(NoteEditor, {trace, selection}), 151 }; 152 } 153 154 private renderTrackDetailsPanel(trace: TraceImpl, trackUri: string) { 155 const track = trace.tracks.getTrack(trackUri); 156 if (track) { 157 return m( 158 DetailsShell, 159 {title: 'Track', description: track.title}, 160 m( 161 GridLayout, 162 m( 163 GridLayoutColumn, 164 m( 165 Section, 166 {title: 'Details'}, 167 m( 168 Tree, 169 m(TreeNode, {left: 'Name', right: track.title}), 170 m(TreeNode, {left: 'URI', right: track.uri}), 171 m(TreeNode, {left: 'Plugin ID', right: track.pluginId}), 172 m( 173 TreeNode, 174 {left: 'Tags'}, 175 track.tags && 176 Object.entries(track.tags).map(([key, value]) => { 177 return m(TreeNode, {left: key, right: value?.toString()}); 178 }), 179 ), 180 ), 181 ), 182 ), 183 ), 184 ); 185 } else { 186 return undefined; // TODO show something sensible here 187 } 188 } 189} 190 191const FADE_TIME_MS = 50; 192 193class FadeContext { 194 private resolver = () => {}; 195 196 putResolver(res: () => void) { 197 this.resolver = res; 198 } 199 200 resolve() { 201 this.resolver(); 202 this.resolver = () => {}; 203 } 204} 205 206interface FadeOutAttrs { 207 readonly context: FadeContext; 208} 209 210class FadeOut implements m.ClassComponent<FadeOutAttrs> { 211 onbeforeremove({attrs}: m.VnodeDOM<FadeOutAttrs>): Promise<void> { 212 return new Promise((res) => { 213 attrs.context.putResolver(res); 214 setTimeout(res, FADE_TIME_MS); 215 }); 216 } 217 218 oncreate({attrs}: m.VnodeDOM<FadeOutAttrs>) { 219 attrs.context.resolve(); 220 } 221 222 view(vnode: m.Vnode<FadeOutAttrs>): void | m.Children { 223 return vnode.children; 224 } 225} 226 227class FadeIn implements m.ClassComponent { 228 private show = false; 229 230 oncreate(_: m.VnodeDOM) { 231 setTimeout(() => { 232 this.show = true; 233 raf.scheduleFullRedraw(); 234 }, FADE_TIME_MS); 235 } 236 237 view(vnode: m.Vnode): m.Children { 238 return this.show ? vnode.children : undefined; 239 } 240} 241