blob: 92e163189299cdebd356a260a76d0b06395d25d8 [file] [log] [blame]
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import m from 'mithril';
import {Anchor} from './anchor';
import {classNames} from './classnames';
import {globals} from './globals';
import {LIBRARY_ADD_CHECK} from './icons';
import {createPage} from './pages';
import {PopupMenuButton} from './popup_menu';
import {TableShowcase} from './tables/table_showcase';
import {Button} from './widgets/button';
import {Checkbox} from './widgets/checkbox';
import {EmptyState} from './widgets/empty_state';
import {Icon} from './widgets/icon';
import {Menu, MenuDivider, MenuItem, PopupMenu2} from './widgets/menu';
import {MultiSelect, MultiSelectDiff} from './widgets/multiselect';
import {Popup, PopupPosition} from './widgets/popup';
import {Portal} from './widgets/portal';
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';
const options: {[key: string]: boolean} = {
foobar: false,
foo: false,
bar: false,
baz: false,
qux: false,
quux: false,
corge: false,
grault: false,
garply: false,
waldo: false,
fred: false,
plugh: false,
xyzzy: false,
thud: false,
};
function PortalButton() {
let portalOpen = false;
return {
view: function({attrs}: any) {
const {
zIndex = true,
absolute = true,
top = true,
} = attrs;
return [
m(Button, {
label: 'Toggle Portal',
onclick: () => {
portalOpen = !portalOpen;
globals.rafScheduler.scheduleFullRedraw();
},
}),
portalOpen &&
m(Portal,
{
style: {
position: absolute && 'absolute',
top: top && '0',
zIndex: zIndex ? '10' : '0',
background: 'white',
},
},
m('', `A very simple portal - a div rendered outside of the normal
flow of the page`)),
];
},
};
}
function lorem() {
const text =
`Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.`;
return m('', {style: {width: '200px'}}, text);
}
function ControlledPopup() {
let popupOpen = false;
return {
view: function() {
return m(
Popup,
{
trigger:
m(Button, {label: `${popupOpen ? 'Close' : 'Open'} Popup`}),
isOpen: popupOpen,
onChange: (shouldOpen: boolean) => popupOpen = shouldOpen,
},
m(Button, {
label: 'Close Popup',
onclick: () => {
popupOpen = !popupOpen;
globals.rafScheduler.scheduleFullRedraw();
},
}),
);
},
};
}
type Options = {
[key: string]: EnumOption|boolean
};
interface WidgetShowcaseAttrs {
initialOpts?: Options;
renderWidget: (options: any) => any;
wide?: boolean;
}
class EnumOption {
constructor(public initial: string, public options: string[]) {}
}
// A little helper class to render any vnode with a dynamic set of options
class WidgetShowcase implements m.ClassComponent<WidgetShowcaseAttrs> {
private optValues: any = {};
private opts?: Options;
renderOptions(listItems: m.Child[]): m.Child {
if (listItems.length === 0) {
return null;
}
return m(
'.widget-controls',
m('h3', 'Options'),
m('ul', listItems),
);
}
oninit({attrs: {initialOpts: opts}}: m.Vnode<WidgetShowcaseAttrs, this>) {
this.opts = opts;
if (opts) {
// Make the initial options values
for (const key in opts) {
if (Object.prototype.hasOwnProperty.call(opts, key)) {
const option = opts[key];
if (option instanceof EnumOption) {
this.optValues[key] = option.initial;
} else if (typeof option === 'boolean') {
this.optValues[key] = option;
}
}
}
}
}
view({attrs: {renderWidget, wide}}: m.CVnode<WidgetShowcaseAttrs>) {
const listItems = [];
if (this.opts) {
for (const key in this.opts) {
if (Object.prototype.hasOwnProperty.call(this.opts, key)) {
listItems.push(m('li', this.renderControlForOption(key)));
}
}
}
return [
m(
'.widget-block',
m(
'div',
{
class: classNames(
'widget-container',
wide && 'widget-container-wide',
),
},
renderWidget(this.optValues),
),
this.renderOptions(listItems),
),
];
}
private renderControlForOption(key: string) {
if (!this.opts) return null;
const value = this.opts[key];
if (value instanceof EnumOption) {
return this.renderEnumOption(key, value);
} else if (typeof value === 'boolean') {
return this.renderBooleanOption(key);
} else {
return null;
}
}
private renderBooleanOption(key: string) {
return m(Checkbox, {
checked: this.optValues[key],
label: key,
onchange: () => {
this.optValues[key] = !this.optValues[key];
globals.rafScheduler.scheduleFullRedraw();
},
});
}
private renderEnumOption(key: string, opt: EnumOption) {
const optionElements = opt.options.map((option: string) => {
return m('option', {value: option}, option);
});
return m(
Select,
{
value: this.optValues[key],
onchange: (e: Event) => {
const el = e.target as HTMLSelectElement;
this.optValues[key] = el.value;
globals.rafScheduler.scheduleFullRedraw();
},
},
optionElements);
}
}
export const WidgetsPage = createPage({
view() {
return m(
'.widgets-page',
m('h1', 'Widgets'),
m('h2', 'Button'),
m(WidgetShowcase, {
renderWidget: ({label, icon, rightIcon, ...rest}) => m(Button, {
icon: icon ? 'send' : undefined,
rightIcon: rightIcon ? 'arrow_forward' : undefined,
label: label ? 'Button' : '',
...rest,
}),
initialOpts: {
label: true,
icon: true,
rightIcon: false,
disabled: false,
minimal: false,
active: false,
compact: false,
},
}),
m('h2', 'Checkbox'),
m(WidgetShowcase, {
renderWidget: (opts) => m(Checkbox, {label: 'Checkbox', ...opts}),
initialOpts: {
disabled: false,
},
}),
m('h2', 'Switch'),
m(WidgetShowcase, {
renderWidget: ({label, ...rest}: any) =>
m(Switch, {label: label ? 'Switch' : undefined, ...rest}),
initialOpts: {
label: true,
disabled: false,
},
}),
m('h2', 'Text Input'),
m(WidgetShowcase, {
renderWidget: ({placeholder, ...rest}) => m(TextInput, {
placeholder: placeholder ? 'Placeholder...' : '',
...rest,
}),
initialOpts: {
placeholder: true,
disabled: false,
},
}),
m('h2', 'Select'),
m(WidgetShowcase, {
renderWidget: (opts) =>
m(Select,
opts,
[
m('option', {value: 'foo', label: 'Foo'}),
m('option', {value: 'bar', label: 'Bar'}),
m('option', {value: 'baz', label: 'Baz'}),
]),
initialOpts: {
disabled: false,
},
}),
m('h2', 'Empty State'),
m(WidgetShowcase, {
renderWidget: ({header, content}) =>
m(EmptyState,
{
header: header && 'No search results found...',
},
content && m(Button, {label: 'Try again'})),
initialOpts: {
header: true,
content: true,
},
}),
m('h2', 'Anchor'),
m(WidgetShowcase, {
renderWidget: ({icon}) => m(
Anchor,
{
icon: icon && 'open_in_new',
href: 'https://perfetto.dev/docs/',
target: '_blank',
},
'Docs',
),
initialOpts: {
icon: true,
},
}),
m('h2', 'Table'),
m(WidgetShowcase,
{renderWidget: () => m(TableShowcase), initialOpts: {}, wide: true}),
m('h2', 'Portal'),
m('p', `A portal is a div rendered out of normal flow of the
hierarchy.`),
m(WidgetShowcase, {
renderWidget: (opts) => m(PortalButton, opts),
initialOpts: {
absolute: true,
zIndex: true,
top: true,
},
}),
m('h2', 'Popup'),
m('p', `A popup is a nicely styled portal element whose position is
dynamically updated to appear to float alongside a specific element on
the page, even as the element is moved and scrolled around.`),
m(WidgetShowcase, {
renderWidget: (opts) => m(
Popup,
{
trigger: m(Button, {label: 'Toggle Popup'}),
...opts,
},
lorem(),
),
initialOpts: {
position: new EnumOption(
PopupPosition.Auto,
Object.values(PopupPosition),
),
closeOnEscape: true,
closeOnOutsideClick: true,
},
}),
m('h2', 'Controlled Popup'),
m('p', `The open/close state of a controlled popup is passed in via
the 'isOpen' attribute. This means we can get open or close the popup
from wherever we like. E.g. from a button inside the popup.
Keeping this state external also means we can modify other parts of the
page depending on whether the popup is open or not, such as the text
on this button.
Note, this is the same component as the popup above, but used in
controlled mode.`),
m(WidgetShowcase, {
renderWidget: (opts) => m(ControlledPopup, opts),
initialOpts: {},
}),
m('h2', 'Icon'),
m(WidgetShowcase, {
renderWidget: (opts) => m(Icon, {icon: 'star', ...opts}),
initialOpts: {filled: false},
}),
m('h2', 'MultiSelect'),
m(WidgetShowcase, {
renderWidget: ({icon, ...rest}) => m(MultiSelect, {
options: Object.entries(options).map(([key, value]) => {
return {
id: key,
name: key,
checked: value,
};
}),
popupPosition: PopupPosition.Top,
label: 'Multi Select',
icon: icon ? LIBRARY_ADD_CHECK : undefined,
onChange: (diffs: MultiSelectDiff[]) => {
diffs.forEach(({id, checked}) => {
options[id] = checked;
});
globals.rafScheduler.scheduleFullRedraw();
},
...rest,
}),
initialOpts: {
icon: true,
showNumSelected: true,
repeatCheckedItemsAtTop: false,
},
}),
m('h2', 'PopupMenu'),
m(WidgetShowcase, {
renderWidget: () => {
return m(PopupMenuButton, {
icon: 'description',
items: [
{itemType: 'regular', text: 'New', callback: () => {}},
{itemType: 'regular', text: 'Open', callback: () => {}},
{itemType: 'regular', text: 'Save', callback: () => {}},
{itemType: 'regular', text: 'Delete', callback: () => {}},
{
itemType: 'group',
text: 'Share',
itemId: 'foo',
children: [
{itemType: 'regular', text: 'Friends', callback: () => {}},
{itemType: 'regular', text: 'Family', callback: () => {}},
{itemType: 'regular', text: 'Everyone', callback: () => {}},
],
},
],
});
},
}),
m('h2', 'Menu'),
m(WidgetShowcase, {
renderWidget: () => m(
Menu,
m(MenuItem, {label: 'New', icon: 'add'}),
m(MenuItem, {label: 'Open', icon: 'folder_open'}),
m(MenuItem, {label: 'Save', icon: 'save', disabled: true}),
m(MenuDivider),
m(MenuItem, {label: 'Delete', icon: 'delete'}),
m(MenuDivider),
m(
MenuItem,
{label: 'Share', icon: 'share'},
m(MenuItem, {label: 'Everyone', icon: 'public'}),
m(MenuItem, {label: 'Friends', icon: 'group'}),
m(
MenuItem,
{label: 'Specific people', icon: 'person_add'},
m(MenuItem, {label: 'Alice', icon: 'person'}),
m(MenuItem, {label: 'Bob', icon: 'person'}),
),
),
m(
MenuItem,
{label: 'More', icon: 'more_horiz'},
m(MenuItem, {label: 'Query', icon: 'database'}),
m(MenuItem, {label: 'Download', icon: 'download'}),
m(MenuItem, {label: 'Clone', icon: 'copy_all'}),
),
),
}),
m('h2', 'PopupMenu2'),
m(WidgetShowcase, {
renderWidget: (opts) => m(
PopupMenu2,
{
trigger: m(Button, {label: 'Menu', icon: 'arrow_drop_down'}),
...opts,
},
m(MenuItem, {label: 'New', icon: 'add'}),
m(MenuItem, {label: 'Open', icon: 'folder_open'}),
m(MenuItem, {label: 'Save', icon: 'save', disabled: true}),
m(MenuDivider),
m(MenuItem, {label: 'Delete', icon: 'delete'}),
m(MenuDivider),
m(
MenuItem,
{label: 'Share', icon: 'share'},
m(MenuItem, {label: 'Everyone', icon: 'public'}),
m(MenuItem, {label: 'Friends', icon: 'group'}),
m(
MenuItem,
{label: 'Specific people', icon: 'person_add'},
m(MenuItem, {label: 'Alice', icon: 'person'}),
m(MenuItem, {label: 'Bob', icon: 'person'}),
),
),
m(
MenuItem,
{label: 'More', icon: 'more_horiz'},
m(MenuItem, {label: 'Query', icon: 'database'}),
m(MenuItem, {label: 'Download', icon: 'download'}),
m(MenuItem, {label: 'Clone', icon: 'copy_all'}),
),
),
initialOpts: {
popupPosition: new EnumOption(
PopupPosition.Bottom,
Object.values(PopupPosition),
),
},
}),
m('h2', 'Spinner'),
m('p', `Simple spinner, rotates forever. Width and height match the font
size.`),
m(WidgetShowcase, {
renderWidget: ({fontSize, easing}) =>
m('', {style: {fontSize}}, m(Spinner, {easing})),
initialOpts: {
fontSize: new EnumOption(
'16px',
['12px', '16px', '24px', '32px', '64px', '128px'],
),
easing: false,
},
}),
m('h2', 'Tree'),
m(WidgetShowcase, {
renderWidget: (opts) => m(
Tree,
opts,
m(TreeNode, {left: 'Name', right: 'my_event'}),
m(TreeNode, {left: 'CPU', right: '2'}),
m(TreeNode, {
left: 'SQL',
right: m(
PopupMenu2,
{
trigger: m(Anchor, {
text: 'SELECT * FROM ftrace_event WHERE id = 123',
icon: 'unfold_more',
}),
},
m(MenuItem, {
label: 'Copy SQL Query',
icon: 'content_copy',
}),
m(MenuItem, {
label: 'Execute Query in new tab',
icon: 'open_in_new',
}),
),
}),
m(TreeNode, {
left: 'Thread',
right: m(Anchor, {text: 'my_thread[456]', icon: 'open_in_new'}),
}),
m(TreeNode, {
left: 'Process',
right: m(Anchor, {text: '/bin/foo[789]', icon: 'open_in_new'}),
}),
m(
TreeNode,
{left: 'Args', right: 'foo: bar, baz: qux'},
m(TreeNode, {left: 'foo', right: 'bar'}),
m(TreeNode, {left: 'baz', right: 'qux'}),
m(
TreeNode,
{left: 'quux'},
m(TreeNode, {left: '[0]', right: 'corge'}),
m(TreeNode, {left: '[1]', right: 'grault'}),
m(TreeNode, {left: '[2]', right: 'garply'}),
m(TreeNode, {left: '[3]', right: 'waldo'}),
),
),
),
initialOpts: {
layout: new EnumOption(
TreeLayout.Grid,
Object.values(TreeLayout),
),
},
wide: true,
}),
);
},
});