• 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';
16
17import {DisposableStack} from '../base/disposable';
18import {raf} from '../core/raf_scheduler';
19import {Button} from '../widgets/button';
20import {MenuItem, PopupMenu2} from '../widgets/menu';
21
22import {DEFAULT_DETAILS_CONTENT_HEIGHT} from './css_constants';
23import {DragGestureHandler} from './drag_gesture_handler';
24import {globals} from './globals';
25
26const DRAG_HANDLE_HEIGHT_PX = 28;
27const UP_ICON = 'keyboard_arrow_up';
28const DOWN_ICON = 'keyboard_arrow_down';
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
42export interface TabDropdownEntry {
43  // Unique key for this tab dropdown entry.
44  key: string;
45
46  // Title to show on this entry.
47  title: string;
48
49  // Called when tab dropdown entry is clicked.
50  onClick: () => void;
51
52  // Whether this tab is checked or not
53  checked: boolean;
54}
55
56export interface DragHandleAttrs {
57  // The current height of the panel.
58  height: number;
59
60  // Called when the panel is dragged.
61  resize: (height: number) => void;
62
63  // A list of tabs to show in the tab bar.
64  tabs: Tab[];
65
66  // The key of the "current" tab.
67  currentTabKey?: string;
68
69  // A list of entries to show in the tab dropdown.
70  // If undefined, the tab dropdown button will not be displayed.
71  tabDropdownEntries?: TabDropdownEntry[];
72
73  // Called when a tab is clicked.
74  onTabClick: (key: string) => void;
75
76  // Called when a tab is closed using its close button.
77  onTabClose?: (key: string) => void;
78}
79
80export function getDefaultDetailsHeight() {
81  // This needs to be a function instead of a const to ensure the CSS constants
82  // have been initialized by the time we perform this calculation;
83  return DRAG_HANDLE_HEIGHT_PX + DEFAULT_DETAILS_CONTENT_HEIGHT;
84}
85
86function getFullScreenHeight() {
87  const page = document.querySelector('.page') as HTMLElement;
88  if (page === null) {
89    // Fall back to at least partially open.
90    return getDefaultDetailsHeight();
91  } else {
92    return page.clientHeight;
93  }
94}
95
96export class DragHandle implements m.ClassComponent<DragHandleAttrs> {
97  private dragStartHeight = 0;
98  private height = 0;
99  private previousHeight = this.height;
100  private resize: (height: number) => void = () => {};
101  private isClosed = this.height <= 0;
102  private isFullscreen = false;
103  // We can't get real fullscreen height until the pan_and_zoom_handler
104  // exists.
105  private fullscreenHeight = 0;
106  private trash = new DisposableStack();
107
108  oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) {
109    this.resize = attrs.resize;
110    this.height = attrs.height;
111    this.isClosed = this.height <= 0;
112    this.fullscreenHeight = getFullScreenHeight();
113    const elem = dom as HTMLElement;
114    this.trash.use(
115      new DragGestureHandler(
116        elem,
117        this.onDrag.bind(this),
118        this.onDragStart.bind(this),
119        this.onDragEnd.bind(this),
120      ),
121    );
122    const cmd = globals.commandManager.registerCommand({
123      id: 'perfetto.ToggleDrawer',
124      name: 'Toggle drawer',
125      defaultHotkey: 'Q',
126      callback: () => {
127        this.toggleVisibility();
128      },
129    });
130    this.trash.use(cmd);
131  }
132
133  private toggleVisibility() {
134    if (this.height === 0) {
135      this.isClosed = false;
136      if (this.previousHeight === 0) {
137        this.previousHeight = getDefaultDetailsHeight();
138      }
139      this.resize(this.previousHeight);
140    } else {
141      this.isFullscreen = false;
142      this.isClosed = true;
143      this.previousHeight = this.height;
144      this.resize(0);
145    }
146    raf.scheduleFullRedraw();
147  }
148
149  onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) {
150    this.resize = attrs.resize;
151    this.height = attrs.height;
152    this.isClosed = this.height <= 0;
153  }
154
155  onremove(_: m.CVnodeDOM<DragHandleAttrs>) {
156    this.trash.dispose();
157  }
158
159  onDrag(_x: number, y: number) {
160    const newHeight = Math.floor(
161      this.dragStartHeight + DRAG_HANDLE_HEIGHT_PX / 2 - y,
162    );
163    this.isClosed = newHeight <= 0;
164    this.isFullscreen = newHeight >= this.fullscreenHeight;
165    this.resize(newHeight);
166    raf.scheduleFullRedraw();
167  }
168
169  onDragStart(_x: number, _y: number) {
170    this.dragStartHeight = this.height;
171  }
172
173  onDragEnd() {}
174
175  view({attrs}: m.CVnode<DragHandleAttrs>) {
176    const {
177      tabDropdownEntries,
178      currentTabKey,
179      tabs,
180      onTabClick,
181      onTabClose = () => {},
182    } = attrs;
183
184    const icon = this.isClosed ? UP_ICON : DOWN_ICON;
185    const title = this.isClosed ? 'Show panel' : 'Hide panel';
186    const renderTab = (tab: Tab) => {
187      const {key, hasCloseButton = false} = tab;
188      const tag = currentTabKey === key ? '.tab[active]' : '.tab';
189      return m(
190        tag,
191        {
192          key,
193          onclick: (event: Event) => {
194            if (!event.defaultPrevented) {
195              onTabClick(key);
196            }
197          },
198          // Middle click to close
199          onauxclick: (event: MouseEvent) => {
200            if (!event.defaultPrevented) {
201              onTabClose(key);
202            }
203          },
204        },
205        m('span.pf-tab-title', tab.title),
206        hasCloseButton &&
207          m(Button, {
208            onclick: (event: Event) => {
209              onTabClose(key);
210              event.preventDefault();
211            },
212            compact: true,
213            icon: 'close',
214          }),
215      );
216    };
217
218    return m(
219      '.handle',
220      m(
221        '.buttons',
222        tabDropdownEntries && this.renderTabDropdown(tabDropdownEntries),
223      ),
224      m('.tabs', tabs.map(renderTab)),
225      m(
226        '.buttons',
227        m(Button, {
228          onclick: () => {
229            this.isClosed = false;
230            this.isFullscreen = true;
231            // Ensure fullscreenHeight is up to date.
232            this.fullscreenHeight = getFullScreenHeight();
233            this.resize(this.fullscreenHeight);
234            raf.scheduleFullRedraw();
235          },
236          title: 'Open fullscreen',
237          disabled: this.isFullscreen,
238          icon: 'vertical_align_top',
239          compact: true,
240        }),
241        m(Button, {
242          onclick: () => {
243            this.toggleVisibility();
244          },
245          title,
246          icon,
247          compact: true,
248        }),
249      ),
250    );
251  }
252
253  private renderTabDropdown(entries: TabDropdownEntry[]) {
254    return m(
255      PopupMenu2,
256      {
257        trigger: m(Button, {
258          compact: true,
259          icon: 'more_vert',
260          disabled: entries.length === 0,
261          title: 'More Tabs',
262        }),
263      },
264      entries.map((entry) => {
265        return m(MenuItem, {
266          key: entry.key,
267          label: entry.title,
268          onclick: () => entry.onClick(),
269          icon: entry.checked ? 'check_box' : 'check_box_outline_blank',
270        });
271      }),
272    );
273  }
274}
275