xref: /aosp_15_r20/external/perfetto/ui/src/core/tab_manager.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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 {DetailsPanel} from '../public/details_panel';
16import {TabDescriptor, TabManager} from '../public/tab';
17import {
18  CollapsiblePanelVisibility,
19  toggleVisibility,
20} from '../components/widgets/collapsible_panel';
21import {raf} from './raf_scheduler';
22
23export interface ResolvedTab {
24  uri: string;
25  tab?: TabDescriptor;
26}
27
28export type TabPanelVisibility = 'COLLAPSED' | 'VISIBLE' | 'FULLSCREEN';
29
30/**
31 * Stores tab & current selection section registries.
32 * Keeps track of tab lifecycles.
33 */
34export class TabManagerImpl implements TabManager, Disposable {
35  private _registry = new Map<string, TabDescriptor>();
36  private _defaultTabs = new Set<string>();
37  private _detailsPanelRegistry = new Set<DetailsPanel>();
38  private _instantiatedTabs = new Map<string, TabDescriptor>();
39  private _openTabs: string[] = []; // URIs of the tabs open.
40  private _currentTab: string = 'current_selection';
41  private _tabPanelVisibility = CollapsiblePanelVisibility.COLLAPSED;
42  private _tabPanelVisibilityChanged = false;
43
44  [Symbol.dispose]() {
45    // Dispose of all tabs that are currently alive
46    for (const tab of this._instantiatedTabs.values()) {
47      this.disposeTab(tab);
48    }
49    this._instantiatedTabs.clear();
50  }
51
52  registerTab(desc: TabDescriptor): Disposable {
53    this._registry.set(desc.uri, desc);
54    return {
55      [Symbol.dispose]: () => this._registry.delete(desc.uri),
56    };
57  }
58
59  addDefaultTab(uri: string): Disposable {
60    this._defaultTabs.add(uri);
61    return {
62      [Symbol.dispose]: () => this._defaultTabs.delete(uri),
63    };
64  }
65
66  registerDetailsPanel(section: DetailsPanel): Disposable {
67    this._detailsPanelRegistry.add(section);
68    return {
69      [Symbol.dispose]: () => this._detailsPanelRegistry.delete(section),
70    };
71  }
72
73  resolveTab(uri: string): TabDescriptor | undefined {
74    return this._registry.get(uri);
75  }
76
77  showCurrentSelectionTab(): void {
78    this.showTab('current_selection');
79  }
80
81  showTab(uri: string): void {
82    // Add tab, unless we're talking about the special current_selection tab
83    if (uri !== 'current_selection') {
84      // Add tab to tab list if not already
85      if (!this._openTabs.some((x) => x === uri)) {
86        this._openTabs.push(uri);
87      }
88    }
89    this._currentTab = uri;
90
91    // The first time that we show a tab, auto-expand the tab bottom panel.
92    // However, if the user has later collapsed the panel (hence if
93    // _tabPanelVisibilityChanged == true), don't insist and leave things as
94    // they are.
95    if (
96      !this._tabPanelVisibilityChanged &&
97      this._tabPanelVisibility === CollapsiblePanelVisibility.COLLAPSED
98    ) {
99      this.setTabPanelVisibility(CollapsiblePanelVisibility.VISIBLE);
100    }
101
102    raf.scheduleFullRedraw();
103  }
104
105  // Hide a tab in the tab bar pick a new tab to show.
106  // Note: Attempting to hide the "current_selection" tab doesn't work. This tab
107  // is special and cannot be removed.
108  hideTab(uri: string): void {
109    // If the removed tab is the "current" tab, we must find a new tab to focus
110    if (uri === this._currentTab) {
111      // Remember the index of the current tab
112      const currentTabIdx = this._openTabs.findIndex((x) => x === uri);
113
114      // Remove the tab
115      this._openTabs = this._openTabs.filter((x) => x !== uri);
116
117      if (currentTabIdx !== -1) {
118        if (this._openTabs.length === 0) {
119          // No more tabs, use current selection
120          this._currentTab = 'current_selection';
121        } else if (currentTabIdx < this._openTabs.length - 1) {
122          // Pick the tab to the right
123          this._currentTab = this._openTabs[currentTabIdx];
124        } else {
125          // Pick the last tab
126          const lastTab = this._openTabs[this._openTabs.length - 1];
127          this._currentTab = lastTab;
128        }
129      }
130    } else {
131      // Otherwise just remove the tab
132      this._openTabs = this._openTabs.filter((x) => x !== uri);
133    }
134    raf.scheduleFullRedraw();
135  }
136
137  toggleTab(uri: string): void {
138    return this.isOpen(uri) ? this.hideTab(uri) : this.showTab(uri);
139  }
140
141  isOpen(uri: string): boolean {
142    return this._openTabs.find((x) => x == uri) !== undefined;
143  }
144
145  get currentTabUri(): string {
146    return this._currentTab;
147  }
148
149  get openTabsUri(): string[] {
150    return this._openTabs;
151  }
152
153  get tabs(): TabDescriptor[] {
154    return Array.from(this._registry.values());
155  }
156
157  get defaultTabs(): string[] {
158    return Array.from(this._defaultTabs);
159  }
160
161  get detailsPanels(): DetailsPanel[] {
162    return Array.from(this._detailsPanelRegistry);
163  }
164
165  /**
166   * Resolves a list of URIs to tabs and manages tab lifecycles.
167   * @param tabUris List of tabs.
168   * @returns List of resolved tabs.
169   */
170  resolveTabs(tabUris: string[]): ResolvedTab[] {
171    // Refresh the list of old tabs
172    const newTabs = new Map<string, TabDescriptor>();
173    const tabs: ResolvedTab[] = [];
174
175    tabUris.forEach((uri) => {
176      const newTab = this._registry.get(uri);
177      tabs.push({uri, tab: newTab});
178
179      if (newTab) {
180        newTabs.set(uri, newTab);
181      }
182    });
183
184    // Call onShow() on any new tabs.
185    for (const [uri, tab] of newTabs) {
186      const oldTab = this._instantiatedTabs.get(uri);
187      if (!oldTab) {
188        this.initTab(tab);
189      }
190    }
191
192    // Call onHide() on any tabs that have been removed.
193    for (const [uri, tab] of this._instantiatedTabs) {
194      const newTab = newTabs.get(uri);
195      if (!newTab) {
196        this.disposeTab(tab);
197      }
198    }
199
200    this._instantiatedTabs = newTabs;
201
202    return tabs;
203  }
204
205  setTabPanelVisibility(visibility: CollapsiblePanelVisibility): void {
206    this._tabPanelVisibility = visibility;
207    this._tabPanelVisibilityChanged = true;
208  }
209
210  toggleTabPanelVisibility(): void {
211    toggleVisibility(this._tabPanelVisibility, (visibility) =>
212      this.setTabPanelVisibility(visibility),
213    );
214  }
215
216  get tabPanelVisibility() {
217    return this._tabPanelVisibility;
218  }
219
220  /**
221   * Call onShow() on this tab.
222   * @param tab The tab to initialize.
223   */
224  private initTab(tab: TabDescriptor): void {
225    tab.onShow?.();
226  }
227
228  /**
229   * Call onHide() and maybe remove from registry if tab is ephemeral.
230   * @param tab The tab to dispose.
231   */
232  private disposeTab(tab: TabDescriptor): void {
233    // Attempt to call onHide
234    tab.onHide?.();
235
236    // If ephemeral, also unregister the tab
237    if (tab.isEphemeral) {
238      this._registry.delete(tab.uri);
239    }
240  }
241}
242