xref: /aosp_15_r20/external/perfetto/ui/src/widgets/tree.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2023 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 {classNames} from '../base/classnames';
17import {hasChildren} from '../base/mithril_utils';
18import {scheduleFullRedraw} from './raf';
19
20// Heirachical tree layout with left and right values.
21// Right and left values of the same indentation level are horizontally aligned.
22// Example:
23// foo    bar
24//  ├ baz  qux
25//  └ quux corge
26//    ├ looong_left aaa
27//    └ a           bbb
28// grault garply
29
30interface TreeAttrs {
31  // Space delimited class list applied to our tree element.
32  className?: string;
33}
34
35export class Tree implements m.ClassComponent<TreeAttrs> {
36  view({attrs, children}: m.Vnode<TreeAttrs>): m.Children {
37    const {className = ''} = attrs;
38
39    const classes = classNames(className);
40
41    return m('.pf-tree', {class: classes}, children);
42  }
43}
44
45interface TreeNodeAttrs {
46  // Content to display in the left hand column.
47  // If omitted, this side will be blank.
48  left?: m.Children;
49  // Content to display in the right hand column.
50  // If omitted, this side will be left blank.
51  right?: m.Children;
52  // Content to display in the right hand column when the node is collapsed.
53  // If omitted, the value of `right` shall be shown when collapsed instead.
54  // If the node has no children, this value is never shown.
55  summary?: m.Children;
56  // Whether this node is collapsed or not.
57  // If omitted, collapsed state 'uncontrolled' - i.e. controlled internally.
58  collapsed?: boolean;
59  // Whether the node should start collapsed or not, default: false.
60  startsCollapsed?: boolean;
61  loading?: boolean;
62  showCaret?: boolean;
63  // Optional icon to show to the left of the text.
64  // If this node contains children, this icon is ignored.
65  icon?: string;
66  // Called when the collapsed state is changed, mainly used in controlled mode.
67  onCollapseChanged?: (collapsed: boolean, attrs: TreeNodeAttrs) => void;
68}
69
70export class TreeNode implements m.ClassComponent<TreeNodeAttrs> {
71  private collapsed;
72
73  constructor({attrs}: m.CVnode<TreeNodeAttrs>) {
74    this.collapsed = attrs.startsCollapsed ?? false;
75  }
76
77  view(vnode: m.CVnode<TreeNodeAttrs>): m.Children {
78    const {
79      children,
80      attrs,
81      attrs: {left, onCollapseChanged = () => {}},
82    } = vnode;
83    return [
84      m(
85        '.pf-tree-node',
86        {
87          class: classNames(this.getClassNameForNode(vnode)),
88        },
89        m(
90          '.pf-tree-left',
91          m('span.pf-tree-gutter', {
92            onclick: () => {
93              this.collapsed = !this.isCollapsed(vnode);
94              onCollapseChanged(this.collapsed, attrs);
95              scheduleFullRedraw();
96            },
97          }),
98          left,
99        ),
100        this.renderRight(vnode),
101      ),
102      hasChildren(vnode) && m('.pf-tree-children', children),
103    ];
104  }
105
106  private getClassNameForNode(vnode: m.CVnode<TreeNodeAttrs>) {
107    const {loading = false, showCaret = false} = vnode.attrs;
108    if (loading) {
109      return 'pf-loading';
110    } else if (hasChildren(vnode) || showCaret) {
111      if (this.isCollapsed(vnode)) {
112        return 'pf-collapsed';
113      } else {
114        return 'pf-expanded';
115      }
116    } else {
117      return undefined;
118    }
119  }
120
121  private renderRight(vnode: m.CVnode<TreeNodeAttrs>) {
122    const {
123      attrs: {right, summary},
124    } = vnode;
125    if (hasChildren(vnode) && this.isCollapsed(vnode)) {
126      return m('.pf-tree-right', summary ?? right);
127    } else {
128      return m('.pf-tree-right', right);
129    }
130  }
131
132  private isCollapsed({attrs}: m.Vnode<TreeNodeAttrs>): boolean {
133    // If collapsed is omitted, use our local collapsed state instead.
134    const {collapsed = this.collapsed} = attrs;
135
136    return collapsed;
137  }
138}
139
140export function dictToTreeNodes(dict: {[key: string]: m.Child}): m.Child[] {
141  const children: m.Child[] = [];
142  for (const key of Object.keys(dict)) {
143    if (dict[key] == undefined) {
144      continue;
145    }
146    children.push(
147      m(TreeNode, {
148        left: key,
149        right: dict[key],
150      }),
151    );
152  }
153  return children;
154}
155
156// Create a flat tree from a POJO
157export function dictToTree(dict: {[key: string]: m.Child}): m.Children {
158  return m(Tree, dictToTreeNodes(dict));
159}
160interface LazyTreeNodeAttrs {
161  // Same as TreeNode (see above).
162  left?: m.Children;
163  // Same as TreeNode (see above).
164  right?: m.Children;
165  // Same as TreeNode (see above).
166  icon?: string;
167  // Same as TreeNode (see above).
168  summary?: m.Children;
169  // A callback to be called when the TreeNode is expanded, in order to fetch
170  // child nodes.
171  // The callback must return a promise to a function which returns m.Children.
172  // The reason the promise must return a function rather than the actual
173  // children is to avoid storing vnodes between render cycles, which is a bug
174  // in Mithril.
175  fetchData: () => Promise<() => m.Children>;
176  // Whether to unload children on collapse.
177  // Defaults to false, data will be kept in memory until the node is destroyed.
178  unloadOnCollapse?: boolean;
179}
180
181// This component is a TreeNode which only loads child nodes when it's expanded.
182// This allows us to represent huge trees without having to load all the data
183// up front, and even allows us to represent infinite or recursive trees.
184export class LazyTreeNode implements m.ClassComponent<LazyTreeNodeAttrs> {
185  private collapsed: boolean = true;
186  private loading: boolean = false;
187  private renderChildren?: () => m.Children;
188
189  view({attrs}: m.CVnode<LazyTreeNodeAttrs>): m.Children {
190    const {
191      left,
192      right,
193      icon,
194      summary,
195      fetchData,
196      unloadOnCollapse = false,
197    } = attrs;
198
199    return m(
200      TreeNode,
201      {
202        left,
203        right,
204        icon,
205        summary,
206        showCaret: true,
207        loading: this.loading,
208        collapsed: this.collapsed,
209        onCollapseChanged: (collapsed) => {
210          if (collapsed) {
211            if (unloadOnCollapse) {
212              this.renderChildren = undefined;
213            }
214          } else {
215            // Expanding
216            if (this.renderChildren) {
217              this.collapsed = false;
218              scheduleFullRedraw();
219            } else {
220              this.loading = true;
221              fetchData().then((result) => {
222                this.loading = false;
223                this.collapsed = false;
224                this.renderChildren = result;
225                scheduleFullRedraw();
226              });
227            }
228          }
229          this.collapsed = collapsed;
230          scheduleFullRedraw();
231        },
232      },
233      this.renderChildren && this.renderChildren(),
234    );
235  }
236}
237