• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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