xref: /aosp_15_r20/external/perfetto/ui/src/frontend/tab_panel.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2024 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 {Gate} from '../base/mithril_utils';
17*6dbdd20aSAndroid Build Coastguard Workerimport {EmptyState} from '../widgets/empty_state';
18*6dbdd20aSAndroid Build Coastguard Workerimport {raf} from '../core/raf_scheduler';
19*6dbdd20aSAndroid Build Coastguard Workerimport {DetailsShell} from '../widgets/details_shell';
20*6dbdd20aSAndroid Build Coastguard Workerimport {GridLayout, GridLayoutColumn} from '../widgets/grid_layout';
21*6dbdd20aSAndroid Build Coastguard Workerimport {Section} from '../widgets/section';
22*6dbdd20aSAndroid Build Coastguard Workerimport {Tree, TreeNode} from '../widgets/tree';
23*6dbdd20aSAndroid Build Coastguard Workerimport {TraceImpl, TraceImplAttrs} from '../core/trace_impl';
24*6dbdd20aSAndroid Build Coastguard Workerimport {MenuItem, PopupMenu2} from '../widgets/menu';
25*6dbdd20aSAndroid Build Coastguard Workerimport {Button} from '../widgets/button';
26*6dbdd20aSAndroid Build Coastguard Workerimport {CollapsiblePanel} from '../components/widgets/collapsible_panel';
27*6dbdd20aSAndroid Build Coastguard Worker
28*6dbdd20aSAndroid Build Coastguard Workerexport type TabPanelAttrs = TraceImplAttrs;
29*6dbdd20aSAndroid Build Coastguard Worker
30*6dbdd20aSAndroid Build Coastguard Workerexport interface Tab {
31*6dbdd20aSAndroid Build Coastguard Worker  // Unique key for this tab, passed to callbacks.
32*6dbdd20aSAndroid Build Coastguard Worker  key: string;
33*6dbdd20aSAndroid Build Coastguard Worker
34*6dbdd20aSAndroid Build Coastguard Worker  // Tab title to show on the tab handle.
35*6dbdd20aSAndroid Build Coastguard Worker  title: m.Children;
36*6dbdd20aSAndroid Build Coastguard Worker
37*6dbdd20aSAndroid Build Coastguard Worker  // Whether to show a close button on the tab handle or not.
38*6dbdd20aSAndroid Build Coastguard Worker  // Default = false.
39*6dbdd20aSAndroid Build Coastguard Worker  hasCloseButton?: boolean;
40*6dbdd20aSAndroid Build Coastguard Worker}
41*6dbdd20aSAndroid Build Coastguard Worker
42*6dbdd20aSAndroid Build Coastguard Workerinterface TabWithContent extends Tab {
43*6dbdd20aSAndroid Build Coastguard Worker  content: m.Children;
44*6dbdd20aSAndroid Build Coastguard Worker}
45*6dbdd20aSAndroid Build Coastguard Worker
46*6dbdd20aSAndroid Build Coastguard Workerexport interface TabDropdownEntry {
47*6dbdd20aSAndroid Build Coastguard Worker  // Unique key for this tab dropdown entry.
48*6dbdd20aSAndroid Build Coastguard Worker  key: string;
49*6dbdd20aSAndroid Build Coastguard Worker
50*6dbdd20aSAndroid Build Coastguard Worker  // Title to show on this entry.
51*6dbdd20aSAndroid Build Coastguard Worker  title: string;
52*6dbdd20aSAndroid Build Coastguard Worker
53*6dbdd20aSAndroid Build Coastguard Worker  // Called when tab dropdown entry is clicked.
54*6dbdd20aSAndroid Build Coastguard Worker  onClick: () => void;
55*6dbdd20aSAndroid Build Coastguard Worker
56*6dbdd20aSAndroid Build Coastguard Worker  // Whether this tab is checked or not
57*6dbdd20aSAndroid Build Coastguard Worker  checked: boolean;
58*6dbdd20aSAndroid Build Coastguard Worker}
59*6dbdd20aSAndroid Build Coastguard Worker
60*6dbdd20aSAndroid Build Coastguard Workerexport class TabPanel implements m.ClassComponent<TabPanelAttrs> {
61*6dbdd20aSAndroid Build Coastguard Worker  private readonly trace: TraceImpl;
62*6dbdd20aSAndroid Build Coastguard Worker  // Tabs panel starts collapsed.
63*6dbdd20aSAndroid Build Coastguard Worker
64*6dbdd20aSAndroid Build Coastguard Worker  private fadeContext = new FadeContext();
65*6dbdd20aSAndroid Build Coastguard Worker
66*6dbdd20aSAndroid Build Coastguard Worker  constructor({attrs}: m.CVnode<TabPanelAttrs>) {
67*6dbdd20aSAndroid Build Coastguard Worker    this.trace = attrs.trace;
68*6dbdd20aSAndroid Build Coastguard Worker  }
69*6dbdd20aSAndroid Build Coastguard Worker
70*6dbdd20aSAndroid Build Coastguard Worker  view() {
71*6dbdd20aSAndroid Build Coastguard Worker    const tabMan = this.trace.tabs;
72*6dbdd20aSAndroid Build Coastguard Worker    const tabList = this.trace.tabs.openTabsUri;
73*6dbdd20aSAndroid Build Coastguard Worker    const resolvedTabs = tabMan.resolveTabs(tabList);
74*6dbdd20aSAndroid Build Coastguard Worker
75*6dbdd20aSAndroid Build Coastguard Worker    const tabs = resolvedTabs.map(({uri, tab: tabDesc}): TabWithContent => {
76*6dbdd20aSAndroid Build Coastguard Worker      if (tabDesc) {
77*6dbdd20aSAndroid Build Coastguard Worker        return {
78*6dbdd20aSAndroid Build Coastguard Worker          key: uri,
79*6dbdd20aSAndroid Build Coastguard Worker          hasCloseButton: true,
80*6dbdd20aSAndroid Build Coastguard Worker          title: tabDesc.content.getTitle(),
81*6dbdd20aSAndroid Build Coastguard Worker          content: tabDesc.content.render(),
82*6dbdd20aSAndroid Build Coastguard Worker        };
83*6dbdd20aSAndroid Build Coastguard Worker      } else {
84*6dbdd20aSAndroid Build Coastguard Worker        return {
85*6dbdd20aSAndroid Build Coastguard Worker          key: uri,
86*6dbdd20aSAndroid Build Coastguard Worker          hasCloseButton: true,
87*6dbdd20aSAndroid Build Coastguard Worker          title: 'Tab does not exist',
88*6dbdd20aSAndroid Build Coastguard Worker          content: undefined,
89*6dbdd20aSAndroid Build Coastguard Worker        };
90*6dbdd20aSAndroid Build Coastguard Worker      }
91*6dbdd20aSAndroid Build Coastguard Worker    });
92*6dbdd20aSAndroid Build Coastguard Worker
93*6dbdd20aSAndroid Build Coastguard Worker    // Add the permanent current selection tab to the front of the list of tabs
94*6dbdd20aSAndroid Build Coastguard Worker    tabs.unshift({
95*6dbdd20aSAndroid Build Coastguard Worker      key: 'current_selection',
96*6dbdd20aSAndroid Build Coastguard Worker      title: 'Current Selection',
97*6dbdd20aSAndroid Build Coastguard Worker      content: this.renderCSTabContentWithFading(),
98*6dbdd20aSAndroid Build Coastguard Worker    });
99*6dbdd20aSAndroid Build Coastguard Worker
100*6dbdd20aSAndroid Build Coastguard Worker    return m(CollapsiblePanel, {
101*6dbdd20aSAndroid Build Coastguard Worker      visibility: this.trace.tabs.tabPanelVisibility,
102*6dbdd20aSAndroid Build Coastguard Worker      setVisibility: (visibility) =>
103*6dbdd20aSAndroid Build Coastguard Worker        this.trace.tabs.setTabPanelVisibility(visibility),
104*6dbdd20aSAndroid Build Coastguard Worker      headerActions: [
105*6dbdd20aSAndroid Build Coastguard Worker        this.renderTripleDotDropdownMenu(),
106*6dbdd20aSAndroid Build Coastguard Worker        this.renderTabStrip(tabs),
107*6dbdd20aSAndroid Build Coastguard Worker      ],
108*6dbdd20aSAndroid Build Coastguard Worker      tabs: tabs.map(({key, content}) => {
109*6dbdd20aSAndroid Build Coastguard Worker        const active = key === this.trace.tabs.currentTabUri;
110*6dbdd20aSAndroid Build Coastguard Worker        return m(Gate, {open: active}, content);
111*6dbdd20aSAndroid Build Coastguard Worker      }),
112*6dbdd20aSAndroid Build Coastguard Worker    });
113*6dbdd20aSAndroid Build Coastguard Worker  }
114*6dbdd20aSAndroid Build Coastguard Worker
115*6dbdd20aSAndroid Build Coastguard Worker  private renderTripleDotDropdownMenu(): m.Child {
116*6dbdd20aSAndroid Build Coastguard Worker    const entries = this.trace.tabs.tabs
117*6dbdd20aSAndroid Build Coastguard Worker      .filter((tab) => tab.isEphemeral === false)
118*6dbdd20aSAndroid Build Coastguard Worker      .map(({content, uri}): TabDropdownEntry => {
119*6dbdd20aSAndroid Build Coastguard Worker        return {
120*6dbdd20aSAndroid Build Coastguard Worker          key: uri,
121*6dbdd20aSAndroid Build Coastguard Worker          title: content.getTitle(),
122*6dbdd20aSAndroid Build Coastguard Worker          onClick: () => this.trace.tabs.toggleTab(uri),
123*6dbdd20aSAndroid Build Coastguard Worker          checked: this.trace.tabs.isOpen(uri),
124*6dbdd20aSAndroid Build Coastguard Worker        };
125*6dbdd20aSAndroid Build Coastguard Worker      });
126*6dbdd20aSAndroid Build Coastguard Worker
127*6dbdd20aSAndroid Build Coastguard Worker    return m(
128*6dbdd20aSAndroid Build Coastguard Worker      '.buttons',
129*6dbdd20aSAndroid Build Coastguard Worker      m(
130*6dbdd20aSAndroid Build Coastguard Worker        PopupMenu2,
131*6dbdd20aSAndroid Build Coastguard Worker        {
132*6dbdd20aSAndroid Build Coastguard Worker          trigger: m(Button, {
133*6dbdd20aSAndroid Build Coastguard Worker            compact: true,
134*6dbdd20aSAndroid Build Coastguard Worker            icon: 'more_vert',
135*6dbdd20aSAndroid Build Coastguard Worker            disabled: entries.length === 0,
136*6dbdd20aSAndroid Build Coastguard Worker            title: 'More Tabs',
137*6dbdd20aSAndroid Build Coastguard Worker          }),
138*6dbdd20aSAndroid Build Coastguard Worker        },
139*6dbdd20aSAndroid Build Coastguard Worker        entries.map((entry) => {
140*6dbdd20aSAndroid Build Coastguard Worker          return m(MenuItem, {
141*6dbdd20aSAndroid Build Coastguard Worker            key: entry.key,
142*6dbdd20aSAndroid Build Coastguard Worker            label: entry.title,
143*6dbdd20aSAndroid Build Coastguard Worker            onclick: () => entry.onClick(),
144*6dbdd20aSAndroid Build Coastguard Worker            icon: entry.checked ? 'check_box' : 'check_box_outline_blank',
145*6dbdd20aSAndroid Build Coastguard Worker          });
146*6dbdd20aSAndroid Build Coastguard Worker        }),
147*6dbdd20aSAndroid Build Coastguard Worker      ),
148*6dbdd20aSAndroid Build Coastguard Worker    );
149*6dbdd20aSAndroid Build Coastguard Worker  }
150*6dbdd20aSAndroid Build Coastguard Worker
151*6dbdd20aSAndroid Build Coastguard Worker  private renderTabStrip(tabs: Tab[]): m.Child {
152*6dbdd20aSAndroid Build Coastguard Worker    const currentTabKey = this.trace.tabs.currentTabUri;
153*6dbdd20aSAndroid Build Coastguard Worker    return m(
154*6dbdd20aSAndroid Build Coastguard Worker      '.tabs',
155*6dbdd20aSAndroid Build Coastguard Worker      tabs.map((tab) => {
156*6dbdd20aSAndroid Build Coastguard Worker        const {key, hasCloseButton = false} = tab;
157*6dbdd20aSAndroid Build Coastguard Worker        const tag = currentTabKey === key ? '.tab[active]' : '.tab';
158*6dbdd20aSAndroid Build Coastguard Worker        return m(
159*6dbdd20aSAndroid Build Coastguard Worker          tag,
160*6dbdd20aSAndroid Build Coastguard Worker          {
161*6dbdd20aSAndroid Build Coastguard Worker            key,
162*6dbdd20aSAndroid Build Coastguard Worker            onclick: (event: Event) => {
163*6dbdd20aSAndroid Build Coastguard Worker              if (!event.defaultPrevented) {
164*6dbdd20aSAndroid Build Coastguard Worker                this.trace.tabs.showTab(key);
165*6dbdd20aSAndroid Build Coastguard Worker              }
166*6dbdd20aSAndroid Build Coastguard Worker            },
167*6dbdd20aSAndroid Build Coastguard Worker            // Middle click to close
168*6dbdd20aSAndroid Build Coastguard Worker            onauxclick: (event: MouseEvent) => {
169*6dbdd20aSAndroid Build Coastguard Worker              if (!event.defaultPrevented) {
170*6dbdd20aSAndroid Build Coastguard Worker                this.trace.tabs.hideTab(key);
171*6dbdd20aSAndroid Build Coastguard Worker              }
172*6dbdd20aSAndroid Build Coastguard Worker            },
173*6dbdd20aSAndroid Build Coastguard Worker          },
174*6dbdd20aSAndroid Build Coastguard Worker          m('span.pf-tab-title', tab.title),
175*6dbdd20aSAndroid Build Coastguard Worker          hasCloseButton &&
176*6dbdd20aSAndroid Build Coastguard Worker            m(Button, {
177*6dbdd20aSAndroid Build Coastguard Worker              onclick: (event: Event) => {
178*6dbdd20aSAndroid Build Coastguard Worker                this.trace.tabs.hideTab(key);
179*6dbdd20aSAndroid Build Coastguard Worker                event.preventDefault();
180*6dbdd20aSAndroid Build Coastguard Worker              },
181*6dbdd20aSAndroid Build Coastguard Worker              compact: true,
182*6dbdd20aSAndroid Build Coastguard Worker              icon: 'close',
183*6dbdd20aSAndroid Build Coastguard Worker            }),
184*6dbdd20aSAndroid Build Coastguard Worker        );
185*6dbdd20aSAndroid Build Coastguard Worker      }),
186*6dbdd20aSAndroid Build Coastguard Worker    );
187*6dbdd20aSAndroid Build Coastguard Worker  }
188*6dbdd20aSAndroid Build Coastguard Worker
189*6dbdd20aSAndroid Build Coastguard Worker  private renderCSTabContentWithFading(): m.Children {
190*6dbdd20aSAndroid Build Coastguard Worker    const section = this.renderCSTabContent();
191*6dbdd20aSAndroid Build Coastguard Worker    if (section.isLoading) {
192*6dbdd20aSAndroid Build Coastguard Worker      return m(FadeIn, section.content);
193*6dbdd20aSAndroid Build Coastguard Worker    } else {
194*6dbdd20aSAndroid Build Coastguard Worker      return m(FadeOut, {context: this.fadeContext}, section.content);
195*6dbdd20aSAndroid Build Coastguard Worker    }
196*6dbdd20aSAndroid Build Coastguard Worker  }
197*6dbdd20aSAndroid Build Coastguard Worker
198*6dbdd20aSAndroid Build Coastguard Worker  private renderCSTabContent(): {isLoading: boolean; content: m.Children} {
199*6dbdd20aSAndroid Build Coastguard Worker    const currentSelection = this.trace.selection.selection;
200*6dbdd20aSAndroid Build Coastguard Worker    if (currentSelection.kind === 'empty') {
201*6dbdd20aSAndroid Build Coastguard Worker      return {
202*6dbdd20aSAndroid Build Coastguard Worker        isLoading: false,
203*6dbdd20aSAndroid Build Coastguard Worker        content: m(
204*6dbdd20aSAndroid Build Coastguard Worker          EmptyState,
205*6dbdd20aSAndroid Build Coastguard Worker          {
206*6dbdd20aSAndroid Build Coastguard Worker            className: 'pf-noselection',
207*6dbdd20aSAndroid Build Coastguard Worker            title: 'Nothing selected',
208*6dbdd20aSAndroid Build Coastguard Worker          },
209*6dbdd20aSAndroid Build Coastguard Worker          'Selection details will appear here',
210*6dbdd20aSAndroid Build Coastguard Worker        ),
211*6dbdd20aSAndroid Build Coastguard Worker      };
212*6dbdd20aSAndroid Build Coastguard Worker    }
213*6dbdd20aSAndroid Build Coastguard Worker
214*6dbdd20aSAndroid Build Coastguard Worker    if (currentSelection.kind === 'track') {
215*6dbdd20aSAndroid Build Coastguard Worker      return {
216*6dbdd20aSAndroid Build Coastguard Worker        isLoading: false,
217*6dbdd20aSAndroid Build Coastguard Worker        content: this.renderTrackDetailsPanel(currentSelection.trackUri),
218*6dbdd20aSAndroid Build Coastguard Worker      };
219*6dbdd20aSAndroid Build Coastguard Worker    }
220*6dbdd20aSAndroid Build Coastguard Worker
221*6dbdd20aSAndroid Build Coastguard Worker    const detailsPanel = this.trace.selection.getDetailsPanelForSelection();
222*6dbdd20aSAndroid Build Coastguard Worker    if (currentSelection.kind === 'track_event' && detailsPanel !== undefined) {
223*6dbdd20aSAndroid Build Coastguard Worker      return {
224*6dbdd20aSAndroid Build Coastguard Worker        isLoading: detailsPanel.isLoading,
225*6dbdd20aSAndroid Build Coastguard Worker        content: detailsPanel.render(),
226*6dbdd20aSAndroid Build Coastguard Worker      };
227*6dbdd20aSAndroid Build Coastguard Worker    }
228*6dbdd20aSAndroid Build Coastguard Worker
229*6dbdd20aSAndroid Build Coastguard Worker    // Get the first "truthy" details panel
230*6dbdd20aSAndroid Build Coastguard Worker    const detailsPanels = this.trace.tabs.detailsPanels.map((dp) => {
231*6dbdd20aSAndroid Build Coastguard Worker      return {
232*6dbdd20aSAndroid Build Coastguard Worker        content: dp.render(currentSelection),
233*6dbdd20aSAndroid Build Coastguard Worker        isLoading: dp.isLoading?.() ?? false,
234*6dbdd20aSAndroid Build Coastguard Worker      };
235*6dbdd20aSAndroid Build Coastguard Worker    });
236*6dbdd20aSAndroid Build Coastguard Worker
237*6dbdd20aSAndroid Build Coastguard Worker    const panel = detailsPanels.find(({content}) => content);
238*6dbdd20aSAndroid Build Coastguard Worker
239*6dbdd20aSAndroid Build Coastguard Worker    if (panel) {
240*6dbdd20aSAndroid Build Coastguard Worker      return panel;
241*6dbdd20aSAndroid Build Coastguard Worker    } else {
242*6dbdd20aSAndroid Build Coastguard Worker      return {
243*6dbdd20aSAndroid Build Coastguard Worker        isLoading: false,
244*6dbdd20aSAndroid Build Coastguard Worker        content: m(
245*6dbdd20aSAndroid Build Coastguard Worker          EmptyState,
246*6dbdd20aSAndroid Build Coastguard Worker          {
247*6dbdd20aSAndroid Build Coastguard Worker            className: 'pf-noselection',
248*6dbdd20aSAndroid Build Coastguard Worker            title: 'No details available',
249*6dbdd20aSAndroid Build Coastguard Worker            icon: 'warning',
250*6dbdd20aSAndroid Build Coastguard Worker          },
251*6dbdd20aSAndroid Build Coastguard Worker          `Selection kind: '${currentSelection.kind}'`,
252*6dbdd20aSAndroid Build Coastguard Worker        ),
253*6dbdd20aSAndroid Build Coastguard Worker      };
254*6dbdd20aSAndroid Build Coastguard Worker    }
255*6dbdd20aSAndroid Build Coastguard Worker  }
256*6dbdd20aSAndroid Build Coastguard Worker
257*6dbdd20aSAndroid Build Coastguard Worker  private renderTrackDetailsPanel(trackUri: string) {
258*6dbdd20aSAndroid Build Coastguard Worker    const track = this.trace.tracks.getTrack(trackUri);
259*6dbdd20aSAndroid Build Coastguard Worker    if (track) {
260*6dbdd20aSAndroid Build Coastguard Worker      return m(
261*6dbdd20aSAndroid Build Coastguard Worker        DetailsShell,
262*6dbdd20aSAndroid Build Coastguard Worker        {title: 'Track', description: track.title},
263*6dbdd20aSAndroid Build Coastguard Worker        m(
264*6dbdd20aSAndroid Build Coastguard Worker          GridLayout,
265*6dbdd20aSAndroid Build Coastguard Worker          m(
266*6dbdd20aSAndroid Build Coastguard Worker            GridLayoutColumn,
267*6dbdd20aSAndroid Build Coastguard Worker            m(
268*6dbdd20aSAndroid Build Coastguard Worker              Section,
269*6dbdd20aSAndroid Build Coastguard Worker              {title: 'Details'},
270*6dbdd20aSAndroid Build Coastguard Worker              m(
271*6dbdd20aSAndroid Build Coastguard Worker                Tree,
272*6dbdd20aSAndroid Build Coastguard Worker                m(TreeNode, {left: 'Name', right: track.title}),
273*6dbdd20aSAndroid Build Coastguard Worker                m(TreeNode, {left: 'URI', right: track.uri}),
274*6dbdd20aSAndroid Build Coastguard Worker                m(TreeNode, {left: 'Plugin ID', right: track.pluginId}),
275*6dbdd20aSAndroid Build Coastguard Worker                m(
276*6dbdd20aSAndroid Build Coastguard Worker                  TreeNode,
277*6dbdd20aSAndroid Build Coastguard Worker                  {left: 'Tags'},
278*6dbdd20aSAndroid Build Coastguard Worker                  track.tags &&
279*6dbdd20aSAndroid Build Coastguard Worker                    Object.entries(track.tags).map(([key, value]) => {
280*6dbdd20aSAndroid Build Coastguard Worker                      return m(TreeNode, {left: key, right: value?.toString()});
281*6dbdd20aSAndroid Build Coastguard Worker                    }),
282*6dbdd20aSAndroid Build Coastguard Worker                ),
283*6dbdd20aSAndroid Build Coastguard Worker              ),
284*6dbdd20aSAndroid Build Coastguard Worker            ),
285*6dbdd20aSAndroid Build Coastguard Worker          ),
286*6dbdd20aSAndroid Build Coastguard Worker        ),
287*6dbdd20aSAndroid Build Coastguard Worker      );
288*6dbdd20aSAndroid Build Coastguard Worker    } else {
289*6dbdd20aSAndroid Build Coastguard Worker      return undefined; // TODO show something sensible here
290*6dbdd20aSAndroid Build Coastguard Worker    }
291*6dbdd20aSAndroid Build Coastguard Worker  }
292*6dbdd20aSAndroid Build Coastguard Worker}
293*6dbdd20aSAndroid Build Coastguard Worker
294*6dbdd20aSAndroid Build Coastguard Workerconst FADE_TIME_MS = 50;
295*6dbdd20aSAndroid Build Coastguard Worker
296*6dbdd20aSAndroid Build Coastguard Workerclass FadeContext {
297*6dbdd20aSAndroid Build Coastguard Worker  private resolver = () => {};
298*6dbdd20aSAndroid Build Coastguard Worker
299*6dbdd20aSAndroid Build Coastguard Worker  putResolver(res: () => void) {
300*6dbdd20aSAndroid Build Coastguard Worker    this.resolver = res;
301*6dbdd20aSAndroid Build Coastguard Worker  }
302*6dbdd20aSAndroid Build Coastguard Worker
303*6dbdd20aSAndroid Build Coastguard Worker  resolve() {
304*6dbdd20aSAndroid Build Coastguard Worker    this.resolver();
305*6dbdd20aSAndroid Build Coastguard Worker    this.resolver = () => {};
306*6dbdd20aSAndroid Build Coastguard Worker  }
307*6dbdd20aSAndroid Build Coastguard Worker}
308*6dbdd20aSAndroid Build Coastguard Worker
309*6dbdd20aSAndroid Build Coastguard Workerinterface FadeOutAttrs {
310*6dbdd20aSAndroid Build Coastguard Worker  context: FadeContext;
311*6dbdd20aSAndroid Build Coastguard Worker}
312*6dbdd20aSAndroid Build Coastguard Worker
313*6dbdd20aSAndroid Build Coastguard Workerclass FadeOut implements m.ClassComponent<FadeOutAttrs> {
314*6dbdd20aSAndroid Build Coastguard Worker  onbeforeremove({attrs}: m.VnodeDOM<FadeOutAttrs>): Promise<void> {
315*6dbdd20aSAndroid Build Coastguard Worker    return new Promise((res) => {
316*6dbdd20aSAndroid Build Coastguard Worker      attrs.context.putResolver(res);
317*6dbdd20aSAndroid Build Coastguard Worker      setTimeout(res, FADE_TIME_MS);
318*6dbdd20aSAndroid Build Coastguard Worker    });
319*6dbdd20aSAndroid Build Coastguard Worker  }
320*6dbdd20aSAndroid Build Coastguard Worker
321*6dbdd20aSAndroid Build Coastguard Worker  oncreate({attrs}: m.VnodeDOM<FadeOutAttrs>) {
322*6dbdd20aSAndroid Build Coastguard Worker    attrs.context.resolve();
323*6dbdd20aSAndroid Build Coastguard Worker  }
324*6dbdd20aSAndroid Build Coastguard Worker
325*6dbdd20aSAndroid Build Coastguard Worker  view(vnode: m.Vnode<FadeOutAttrs>): void | m.Children {
326*6dbdd20aSAndroid Build Coastguard Worker    return vnode.children;
327*6dbdd20aSAndroid Build Coastguard Worker  }
328*6dbdd20aSAndroid Build Coastguard Worker}
329*6dbdd20aSAndroid Build Coastguard Worker
330*6dbdd20aSAndroid Build Coastguard Workerclass FadeIn implements m.ClassComponent {
331*6dbdd20aSAndroid Build Coastguard Worker  private show = false;
332*6dbdd20aSAndroid Build Coastguard Worker
333*6dbdd20aSAndroid Build Coastguard Worker  oncreate(_: m.VnodeDOM) {
334*6dbdd20aSAndroid Build Coastguard Worker    setTimeout(() => {
335*6dbdd20aSAndroid Build Coastguard Worker      this.show = true;
336*6dbdd20aSAndroid Build Coastguard Worker      raf.scheduleFullRedraw();
337*6dbdd20aSAndroid Build Coastguard Worker    }, FADE_TIME_MS);
338*6dbdd20aSAndroid Build Coastguard Worker  }
339*6dbdd20aSAndroid Build Coastguard Worker
340*6dbdd20aSAndroid Build Coastguard Worker  view(vnode: m.Vnode): m.Children {
341*6dbdd20aSAndroid Build Coastguard Worker    return this.show ? vnode.children : undefined;
342*6dbdd20aSAndroid Build Coastguard Worker  }
343*6dbdd20aSAndroid Build Coastguard Worker}
344