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 {Gate} from '../base/mithril_utils'; 17import {EmptyState} from '../widgets/empty_state'; 18import {raf} from '../core/raf_scheduler'; 19import {DetailsShell} from '../widgets/details_shell'; 20import {GridLayout, GridLayoutColumn} from '../widgets/grid_layout'; 21import {Section} from '../widgets/section'; 22import {Tree, TreeNode} from '../widgets/tree'; 23import {TraceImpl, TraceImplAttrs} from '../core/trace_impl'; 24import {MenuItem, PopupMenu2} from '../widgets/menu'; 25import {Button} from '../widgets/button'; 26import {CollapsiblePanel} from '../components/widgets/collapsible_panel'; 27 28export type TabPanelAttrs = TraceImplAttrs; 29 30export interface Tab { 31 // Unique key for this tab, passed to callbacks. 32 key: string; 33 34 // Tab title to show on the tab handle. 35 title: m.Children; 36 37 // Whether to show a close button on the tab handle or not. 38 // Default = false. 39 hasCloseButton?: boolean; 40} 41 42interface TabWithContent extends Tab { 43 content: m.Children; 44} 45 46export interface TabDropdownEntry { 47 // Unique key for this tab dropdown entry. 48 key: string; 49 50 // Title to show on this entry. 51 title: string; 52 53 // Called when tab dropdown entry is clicked. 54 onClick: () => void; 55 56 // Whether this tab is checked or not 57 checked: boolean; 58} 59 60export class TabPanel implements m.ClassComponent<TabPanelAttrs> { 61 private readonly trace: TraceImpl; 62 // Tabs panel starts collapsed. 63 64 private fadeContext = new FadeContext(); 65 66 constructor({attrs}: m.CVnode<TabPanelAttrs>) { 67 this.trace = attrs.trace; 68 } 69 70 view() { 71 const tabMan = this.trace.tabs; 72 const tabList = this.trace.tabs.openTabsUri; 73 const resolvedTabs = tabMan.resolveTabs(tabList); 74 75 const tabs = resolvedTabs.map(({uri, tab: tabDesc}): TabWithContent => { 76 if (tabDesc) { 77 return { 78 key: uri, 79 hasCloseButton: true, 80 title: tabDesc.content.getTitle(), 81 content: tabDesc.content.render(), 82 }; 83 } else { 84 return { 85 key: uri, 86 hasCloseButton: true, 87 title: 'Tab does not exist', 88 content: undefined, 89 }; 90 } 91 }); 92 93 // Add the permanent current selection tab to the front of the list of tabs 94 tabs.unshift({ 95 key: 'current_selection', 96 title: 'Current Selection', 97 content: this.renderCSTabContentWithFading(), 98 }); 99 100 return m(CollapsiblePanel, { 101 visibility: this.trace.tabs.tabPanelVisibility, 102 setVisibility: (visibility) => 103 this.trace.tabs.setTabPanelVisibility(visibility), 104 headerActions: [ 105 this.renderTripleDotDropdownMenu(), 106 this.renderTabStrip(tabs), 107 ], 108 tabs: tabs.map(({key, content}) => { 109 const active = key === this.trace.tabs.currentTabUri; 110 return m(Gate, {open: active}, content); 111 }), 112 }); 113 } 114 115 private renderTripleDotDropdownMenu(): m.Child { 116 const entries = this.trace.tabs.tabs 117 .filter((tab) => tab.isEphemeral === false) 118 .map(({content, uri}): TabDropdownEntry => { 119 return { 120 key: uri, 121 title: content.getTitle(), 122 onClick: () => this.trace.tabs.toggleTab(uri), 123 checked: this.trace.tabs.isOpen(uri), 124 }; 125 }); 126 127 return m( 128 '.buttons', 129 m( 130 PopupMenu2, 131 { 132 trigger: m(Button, { 133 compact: true, 134 icon: 'more_vert', 135 disabled: entries.length === 0, 136 title: 'More Tabs', 137 }), 138 }, 139 entries.map((entry) => { 140 return m(MenuItem, { 141 key: entry.key, 142 label: entry.title, 143 onclick: () => entry.onClick(), 144 icon: entry.checked ? 'check_box' : 'check_box_outline_blank', 145 }); 146 }), 147 ), 148 ); 149 } 150 151 private renderTabStrip(tabs: Tab[]): m.Child { 152 const currentTabKey = this.trace.tabs.currentTabUri; 153 return m( 154 '.tabs', 155 tabs.map((tab) => { 156 const {key, hasCloseButton = false} = tab; 157 const tag = currentTabKey === key ? '.tab[active]' : '.tab'; 158 return m( 159 tag, 160 { 161 key, 162 onclick: (event: Event) => { 163 if (!event.defaultPrevented) { 164 this.trace.tabs.showTab(key); 165 } 166 }, 167 // Middle click to close 168 onauxclick: (event: MouseEvent) => { 169 if (!event.defaultPrevented) { 170 this.trace.tabs.hideTab(key); 171 } 172 }, 173 }, 174 m('span.pf-tab-title', tab.title), 175 hasCloseButton && 176 m(Button, { 177 onclick: (event: Event) => { 178 this.trace.tabs.hideTab(key); 179 event.preventDefault(); 180 }, 181 compact: true, 182 icon: 'close', 183 }), 184 ); 185 }), 186 ); 187 } 188 189 private renderCSTabContentWithFading(): m.Children { 190 const section = this.renderCSTabContent(); 191 if (section.isLoading) { 192 return m(FadeIn, section.content); 193 } else { 194 return m(FadeOut, {context: this.fadeContext}, section.content); 195 } 196 } 197 198 private renderCSTabContent(): {isLoading: boolean; content: m.Children} { 199 const currentSelection = this.trace.selection.selection; 200 if (currentSelection.kind === 'empty') { 201 return { 202 isLoading: false, 203 content: m( 204 EmptyState, 205 { 206 className: 'pf-noselection', 207 title: 'Nothing selected', 208 }, 209 'Selection details will appear here', 210 ), 211 }; 212 } 213 214 if (currentSelection.kind === 'track') { 215 return { 216 isLoading: false, 217 content: this.renderTrackDetailsPanel(currentSelection.trackUri), 218 }; 219 } 220 221 const detailsPanel = this.trace.selection.getDetailsPanelForSelection(); 222 if (currentSelection.kind === 'track_event' && detailsPanel !== undefined) { 223 return { 224 isLoading: detailsPanel.isLoading, 225 content: detailsPanel.render(), 226 }; 227 } 228 229 // Get the first "truthy" details panel 230 const detailsPanels = this.trace.tabs.detailsPanels.map((dp) => { 231 return { 232 content: dp.render(currentSelection), 233 isLoading: dp.isLoading?.() ?? false, 234 }; 235 }); 236 237 const panel = detailsPanels.find(({content}) => content); 238 239 if (panel) { 240 return panel; 241 } else { 242 return { 243 isLoading: false, 244 content: m( 245 EmptyState, 246 { 247 className: 'pf-noselection', 248 title: 'No details available', 249 icon: 'warning', 250 }, 251 `Selection kind: '${currentSelection.kind}'`, 252 ), 253 }; 254 } 255 } 256 257 private renderTrackDetailsPanel(trackUri: string) { 258 const track = this.trace.tracks.getTrack(trackUri); 259 if (track) { 260 return m( 261 DetailsShell, 262 {title: 'Track', description: track.title}, 263 m( 264 GridLayout, 265 m( 266 GridLayoutColumn, 267 m( 268 Section, 269 {title: 'Details'}, 270 m( 271 Tree, 272 m(TreeNode, {left: 'Name', right: track.title}), 273 m(TreeNode, {left: 'URI', right: track.uri}), 274 m(TreeNode, {left: 'Plugin ID', right: track.pluginId}), 275 m( 276 TreeNode, 277 {left: 'Tags'}, 278 track.tags && 279 Object.entries(track.tags).map(([key, value]) => { 280 return m(TreeNode, {left: key, right: value?.toString()}); 281 }), 282 ), 283 ), 284 ), 285 ), 286 ), 287 ); 288 } else { 289 return undefined; // TODO show something sensible here 290 } 291 } 292} 293 294const FADE_TIME_MS = 50; 295 296class FadeContext { 297 private resolver = () => {}; 298 299 putResolver(res: () => void) { 300 this.resolver = res; 301 } 302 303 resolve() { 304 this.resolver(); 305 this.resolver = () => {}; 306 } 307} 308 309interface FadeOutAttrs { 310 context: FadeContext; 311} 312 313class FadeOut implements m.ClassComponent<FadeOutAttrs> { 314 onbeforeremove({attrs}: m.VnodeDOM<FadeOutAttrs>): Promise<void> { 315 return new Promise((res) => { 316 attrs.context.putResolver(res); 317 setTimeout(res, FADE_TIME_MS); 318 }); 319 } 320 321 oncreate({attrs}: m.VnodeDOM<FadeOutAttrs>) { 322 attrs.context.resolve(); 323 } 324 325 view(vnode: m.Vnode<FadeOutAttrs>): void | m.Children { 326 return vnode.children; 327 } 328} 329 330class FadeIn implements m.ClassComponent { 331 private show = false; 332 333 oncreate(_: m.VnodeDOM) { 334 setTimeout(() => { 335 this.show = true; 336 raf.scheduleFullRedraw(); 337 }, FADE_TIME_MS); 338 } 339 340 view(vnode: m.Vnode): m.Children { 341 return this.show ? vnode.children : undefined; 342 } 343} 344