diff options
author | Steve Golton <stevegolton@google.com> | 2023-05-17 08:16:55 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2023-05-17 08:16:55 +0000 |
commit | 06ea518ef4373ccb1acaf75e833537ee7f1f5fa8 (patch) | |
tree | 8a37a5021553477668e092053063f6ff765c9846 | |
parent | e97fe02aaee18a0acc361a64f45751e9a3ed1057 (diff) | |
parent | e7aaa4cf1f495db6e5389cd69d2de36a059a3a1f (diff) | |
download | perfetto-06ea518ef4373ccb1acaf75e833537ee7f1f5fa8.tar.gz |
Merge "Add lazy-loading tree node component"
-rw-r--r-- | ui/src/frontend/widgets/tree.ts | 78 | ||||
-rw-r--r-- | ui/src/frontend/widgets_page.ts | 16 |
2 files changed, 89 insertions, 5 deletions
diff --git a/ui/src/frontend/widgets/tree.ts b/ui/src/frontend/widgets/tree.ts index b362391d1..e79734386 100644 --- a/ui/src/frontend/widgets/tree.ts +++ b/ui/src/frontend/widgets/tree.ts @@ -1,7 +1,10 @@ import m from 'mithril'; + import {classNames} from '../classnames'; import {globals} from '../globals'; + import {Button} from './button'; +import {Spinner} from './spinner'; import {hasChildren} from './utils'; export enum TreeLayout { @@ -50,14 +53,14 @@ export class Tree implements m.ClassComponent<TreeAttrs> { interface TreeNodeAttrs { // Content to display in the left hand column. // If omitted, this side will be blank. - left?: m.Child; + left?: m.Children; // Content to display in the right hand column. // If omitted, this side will be left blank. - right?: m.Child; + right?: m.Children; // Content to display in the right hand column when the node is collapsed. // If omitted, the value of `right` shall be shown when collapsed instead. // If the node has no children, this value is never shown. - summary?: m.Child; + summary?: m.Children; // Whether this node is collapsed or not. // If omitted, collapsed state 'uncontrolled' - i.e. controlled internally. collapsed?: boolean; @@ -92,7 +95,7 @@ export class TreeNode implements m.ClassComponent<TreeNodeAttrs> { private renderRight(vnode: m.CVnode<TreeNodeAttrs>) { const {attrs: {right, summary}} = vnode; - if (hasChildren(vnode) && this.collapsed) { + if (hasChildren(vnode) && this.isCollapsed(vnode)) { return m('.pf-tree-right', summary ?? right); } else { return m('.pf-tree-right', right); @@ -146,3 +149,70 @@ export function dictToTree(dict: {[key: string]: m.Child}): m.Children { } return m(Tree, children); } + +interface LazyTreeNodeAttrs { + // Same as TreeNode (see above). + left?: m.Children; + // Same as TreeNode (see above). + right?: m.Children; + // Same as TreeNode (see above). + summary?: m.Children; + // A callback to be called when the TreeNode is expanded, in order to fetch + // child nodes. + // The callback must return a promise to a function which returns m.Children. + // The reason the promise must return a function rather than the actual + // children is to avoid storing vnodes between render cycles, which is a bug + // in Mithril. + fetchData: () => Promise<() => m.Children>; + // Whether to keep child nodes in memory after the node has been collapsed. + // Defaults to true + hoardData?: boolean; +} + +// This component is a TreeNode which only loads child nodes when it's expanded. +// This allows us to represent huge trees without having to load all the data +// up front, and even allows us to represent infinite or recursive trees. +export class LazyTreeNode implements m.ClassComponent<LazyTreeNodeAttrs> { + private collapsed: boolean = true; + private renderChildren = this.renderSpinner; + + private renderSpinner(): m.Children { + return m(TreeNode, {left: m(Spinner)}); + } + + view({attrs}: m.CVnode<LazyTreeNodeAttrs>): m.Children { + const { + left, + right, + summary, + fetchData, + hoardData = true, + } = attrs; + + return m( + TreeNode, + { + left, + right, + summary, + collapsed: this.collapsed, + onCollapseChanged: (collapsed) => { + if (collapsed) { + if (!hoardData) { + this.renderChildren = this.renderSpinner; + } + } else { + fetchData().then((result) => { + if (!this.collapsed) { + this.renderChildren = result; + globals.rafScheduler.scheduleFullRedraw(); + } + }); + } + this.collapsed = collapsed; + globals.rafScheduler.scheduleFullRedraw(); + }, + }, + this.renderChildren()); + } +} diff --git a/ui/src/frontend/widgets_page.ts b/ui/src/frontend/widgets_page.ts index 5d550e2a9..989be89be 100644 --- a/ui/src/frontend/widgets_page.ts +++ b/ui/src/frontend/widgets_page.ts @@ -34,7 +34,7 @@ import {Select} from './widgets/select'; import {Spinner} from './widgets/spinner'; import {Switch} from './widgets/switch'; import {TextInput} from './widgets/text_input'; -import {Tree, TreeLayout, TreeNode} from './widgets/tree'; +import {LazyTreeNode, Tree, TreeLayout, TreeNode} from './widgets/tree'; const options: {[key: string]: boolean} = { foobar: false, @@ -243,6 +243,18 @@ class WidgetShowcase implements m.ClassComponent<WidgetShowcaseAttrs> { } } +function recursiveLazyTreeNode( + left: string, summary: string, hoardData: boolean): m.Children { + return m(LazyTreeNode, { + left, + summary, + hoardData, + fetchData: async () => { + await new Promise((r) => setTimeout(r, 200)); + return () => recursiveLazyTreeNode(left, summary, hoardData); + }, + }); +} export const WidgetsPage = createPage({ view() { @@ -563,6 +575,8 @@ export const WidgetsPage = createPage({ left: 'Process', right: m(Anchor, {text: '/bin/foo[789]', icon: 'open_in_new'}), }), + recursiveLazyTreeNode('Lazy', '(hoarding)', true), + recursiveLazyTreeNode('Lazy', '(non-hoarding)', false), m( TreeNode, { |