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