xref: /aosp_15_r20/external/perfetto/ui/src/components/widgets/collapsible_panel.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.
14import m from 'mithril';
15import {DEFAULT_DETAILS_CONTENT_HEIGHT} from '../../frontend/css_constants';
16import {DisposableStack} from '../../base/disposable_stack';
17import {DragGestureHandler} from '../../base/drag_gesture_handler';
18import {raf} from '../../core/raf_scheduler';
19import {assertExists} from '../../base/logging';
20import {Button} from '../../widgets/button';
21import {toHTMLElement} from '../../base/dom_utils';
22
23export enum CollapsiblePanelVisibility {
24  VISIBLE,
25  FULLSCREEN,
26  COLLAPSED,
27}
28
29export interface CollapsiblePanelAttrs {
30  visibility: CollapsiblePanelVisibility;
31  setVisibility: (visibility: CollapsiblePanelVisibility) => void;
32  headerActions?: m.Children;
33  tabs?: m.Children;
34}
35
36export class CollapsiblePanel
37  implements m.ClassComponent<CollapsiblePanelAttrs>
38{
39  // The actual height of the vdom node. It matches resizableHeight if VISIBLE,
40  // 0 if COLLAPSED, fullscreenHeight if FULLSCREEN.
41  private height = 0;
42
43  // The height when the panel is 'VISIBLE'.
44  private resizableHeight = getDefaultDetailsHeight();
45
46  // The height when the panel is 'FULLSCREEN'.
47  private fullscreenHeight = 0;
48
49  private trash = new DisposableStack();
50
51  view({attrs}: m.CVnode<CollapsiblePanelAttrs>) {
52    switch (attrs.visibility) {
53      case CollapsiblePanelVisibility.VISIBLE:
54        this.height = Math.min(
55          Math.max(this.resizableHeight, 0),
56          this.fullscreenHeight,
57        );
58        break;
59      case CollapsiblePanelVisibility.FULLSCREEN:
60        this.height = this.fullscreenHeight;
61        break;
62      case CollapsiblePanelVisibility.COLLAPSED:
63        this.height = 0;
64        break;
65    }
66
67    return m(
68      '.collapsible-panel',
69      m(
70        '.handle',
71        attrs.headerActions,
72        this.renderTabResizeButtons(attrs.visibility, attrs.setVisibility),
73      ),
74      m(
75        '.details-panel-container',
76        {
77          style: {height: `${this.height}px`},
78        },
79        attrs.tabs,
80      ),
81    );
82  }
83
84  updatePanelVisibility(
85    visibility: CollapsiblePanelVisibility,
86    setVisibility: (visibility: CollapsiblePanelVisibility) => void,
87  ) {
88    setVisibility(visibility);
89    raf.scheduleFullRedraw();
90  }
91
92  oncreate(vnode: m.VnodeDOM<CollapsiblePanelAttrs, this>) {
93    let dragStartY = 0;
94    let heightWhenDragStarted = 0;
95
96    const handle = toHTMLElement(
97      assertExists(vnode.dom.querySelector('.handle')),
98    );
99
100    this.trash.use(
101      new DragGestureHandler(
102        handle,
103        /* onDrag */ (_x, y) => {
104          const deltaYSinceDragStart = dragStartY - y;
105          this.resizableHeight = heightWhenDragStarted + deltaYSinceDragStart;
106          raf.scheduleFullRedraw('force');
107        },
108        /* onDragStarted */ (_x, y) => {
109          this.resizableHeight = this.height;
110          heightWhenDragStarted = this.height;
111          dragStartY = y;
112          vnode.attrs.setVisibility(CollapsiblePanelVisibility.VISIBLE);
113        },
114        /* onDragFinished */ () => {},
115      ),
116    );
117
118    const page = assertExists(vnode.dom.parentElement);
119    this.fullscreenHeight = page.clientHeight;
120    const resizeObs = new ResizeObserver(() => {
121      this.fullscreenHeight = page.clientHeight;
122      raf.scheduleFullRedraw();
123    });
124    resizeObs.observe(page);
125    this.trash.defer(() => resizeObs.disconnect());
126  }
127
128  onremove() {
129    this.trash.dispose();
130  }
131
132  private renderTabResizeButtons(
133    visibility: CollapsiblePanelVisibility,
134    setVisibility: (visibility: CollapsiblePanelVisibility) => void,
135  ): m.Child {
136    const isClosed = visibility === CollapsiblePanelVisibility.COLLAPSED;
137    return m(
138      '.buttons',
139      m(Button, {
140        title: 'Open fullscreen',
141        disabled: visibility === CollapsiblePanelVisibility.FULLSCREEN,
142        icon: 'vertical_align_top',
143        compact: true,
144        onclick: () => {
145          this.updatePanelVisibility(
146            CollapsiblePanelVisibility.FULLSCREEN,
147            setVisibility,
148          );
149        },
150      }),
151      m(Button, {
152        onclick: () => {
153          toggleVisibility(visibility, setVisibility);
154        },
155        title: isClosed ? 'Show panel' : 'Hide panel',
156        icon: isClosed ? 'keyboard_arrow_up' : 'keyboard_arrow_down',
157        compact: true,
158      }),
159    );
160  }
161}
162
163export function toggleVisibility(
164  visibility: CollapsiblePanelVisibility,
165  setVisibility: (visibility: CollapsiblePanelVisibility) => void,
166) {
167  switch (visibility) {
168    case CollapsiblePanelVisibility.COLLAPSED:
169    case CollapsiblePanelVisibility.FULLSCREEN:
170      setVisibility(CollapsiblePanelVisibility.VISIBLE);
171      break;
172    case CollapsiblePanelVisibility.VISIBLE:
173      setVisibility(CollapsiblePanelVisibility.COLLAPSED);
174      break;
175  }
176
177  raf.scheduleFullRedraw();
178}
179
180function getDefaultDetailsHeight() {
181  const DRAG_HANDLE_HEIGHT_PX = 28;
182  // This needs to be a function instead of a const to ensure the CSS constants
183  // have been initialized by the time we perform this calculation;
184  return DRAG_HANDLE_HEIGHT_PX + DEFAULT_DETAILS_CONTENT_HEIGHT;
185}
186