1// Copyright 2020 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4import {FocusEvent, SelectRelatedEvent, ToolTipEvent} from '../events.mjs';
5import {CSSColor} from '../helper.mjs';
6import {DOM, V8CustomElement} from '../helper.mjs';
7
8DOM.defineCustomElement('./view/map-panel/map-transitions',
9                        (templateText) =>
10                            class MapTransitions extends V8CustomElement {
11  _timeline;
12  _map;
13  _edgeToColor = new Map();
14  _selectedLogEntries;
15  _displayedMapsInTree;
16  _toggleSubtreeHandler = this._handleToggleSubtree.bind(this);
17  _mapClickHandler = this._handleMapClick.bind(this);
18  _mapDoubleClickHandler = this._handleMapDoubleClick.bind(this);
19  _mouseoverMapHandler = this._handleMouseoverMap.bind(this);
20
21  constructor() {
22    super(templateText);
23    this.currentNode = this.transitionView;
24  }
25
26  get transitionView() {
27    return this.$('#transitionView');
28  }
29
30  set timeline(timeline) {
31    this._timeline = timeline;
32    this._edgeToColor.clear();
33    timeline.getBreakdown().forEach(breakdown => {
34      this._edgeToColor.set(breakdown.key, CSSColor.at(breakdown.id));
35    });
36  }
37
38  set selectedLogEntries(list) {
39    this._selectedLogEntries = list;
40    this.requestUpdate();
41  }
42
43  _update() {
44    this.transitionView.style.display = 'none';
45    DOM.removeAllChildren(this.transitionView);
46    if (this._selectedLogEntries.length == 0) return;
47    this._displayedMapsInTree = new Set();
48    // Limit view to 200 maps for performance reasons.
49    this._selectedLogEntries.slice(0, 200).forEach(
50        (map) => this._addMapAndParentTransitions(map));
51    this._displayedMapsInTree = undefined;
52    this.transitionView.style.display = '';
53  }
54
55  _addMapAndParentTransitions(map) {
56    if (map === undefined) return;
57    if (this._displayedMapsInTree.has(map)) return;
58    this._displayedMapsInTree.add(map);
59    this.currentNode = this.transitionView;
60    let parents = map.getParents();
61    if (parents.length > 0) {
62      this._addTransitionTo(parents.pop());
63      parents.reverse().forEach((each) => this._addTransitionTo(each));
64    }
65    let mapNode = this._addSubtransitions(map);
66    // Mark and show the selected map.
67    mapNode.classList.add('selected');
68    if (this.selectedMap == map) {
69      setTimeout(
70          () => mapNode.scrollIntoView({
71            behavior: 'smooth',
72            block: 'nearest',
73            inline: 'nearest',
74          }),
75          1);
76    }
77  }
78
79  _addSubtransitions(map) {
80    let mapNode = this._addTransitionTo(map);
81    // Draw outgoing linear transition line.
82    let current = map;
83    while (current.children.length == 1) {
84      current = current.children[0].to;
85      this._addTransitionTo(current);
86    }
87    return mapNode;
88  }
89
90  _addTransitionEdge(map) {
91    let classes = ['transitionEdge'];
92    let edge = DOM.div(classes);
93    edge.style.backgroundColor = this._edgeToColor.get(map.edge.type);
94    let labelNode = DOM.div('transitionLabel');
95    labelNode.innerText = map.edge.toString();
96    edge.appendChild(labelNode);
97    return edge;
98  }
99
100  _addTransitionTo(map) {
101    // transition[ transitions[ transition[...], transition[...], ...]];
102    this._displayedMapsInTree?.add(map);
103    let transition = DOM.div('transition');
104    if (map.isDeprecated()) transition.classList.add('deprecated');
105    if (map.edge) {
106      transition.appendChild(this._addTransitionEdge(map));
107    }
108    let mapNode = this._addMapNode(map);
109    transition.appendChild(mapNode);
110
111    let subtree = DOM.div('transitions');
112    transition.appendChild(subtree);
113
114    this.currentNode.appendChild(transition);
115    this.currentNode = subtree;
116
117    return mapNode;
118  }
119
120  _addMapNode(map) {
121    let node = DOM.div('map');
122    if (map.edge)
123      node.style.backgroundColor = this._edgeToColor.get(map.edge.type);
124    node.map = map;
125    node.onclick = this._mapClickHandler
126    node.ondblclick = this._mapDoubleClickHandler
127    node.onmouseover = this._mouseoverMapHandler
128    if (map.children.length > 1) {
129      node.innerText = map.children.length;
130      const showSubtree = DOM.div('showSubtransitions');
131      showSubtree.onclick = this._toggleSubtreeHandler
132      node.appendChild(showSubtree);
133    }
134    else if (map.children.length == 0) {
135      node.innerHTML = '●';
136    }
137    this.currentNode.appendChild(node);
138    return node;
139  }
140
141  _handleMapClick(event) {
142    const map = event.currentTarget.map;
143    this.dispatchEvent(new FocusEvent(map));
144  }
145
146  _handleMapDoubleClick(event) {
147    this.dispatchEvent(new SelectRelatedEvent(event.currentTarget.map));
148  }
149
150  _handleMouseoverMap(event) {
151    this.dispatchEvent(
152        new ToolTipEvent(event.currentTarget.map, event.currentTarget));
153  }
154
155  _handleToggleSubtree(event) {
156    event.stopImmediatePropagation();
157    const node = event.currentTarget.parentElement;
158    const map = node.map;
159    event.target.classList.toggle('opened');
160    const transitionsNode = node.parentElement.querySelector('.transitions');
161    const subtransitionNodes = transitionsNode.children;
162    if (subtransitionNodes.length <= 1) {
163      // Add subtransitions except the one that's already shown.
164      let visibleTransitionMap = subtransitionNodes.length == 1 ?
165          transitionsNode.querySelector('.map').map :
166          undefined;
167      map.children.forEach((edge) => {
168        if (edge.to != visibleTransitionMap) {
169          this.currentNode = transitionsNode;
170          this._addSubtransitions(edge.to);
171        }
172      });
173    } else {
174      // remove all but the first (currently selected) subtransition
175      for (let i = subtransitionNodes.length - 1; i > 0; i--) {
176        transitionsNode.removeChild(subtransitionNodes[i]);
177      }
178    }
179    return false;
180  }
181});
182