14
15
import {isReactNativeEnvironment} from 'react-devtools-shared/src/backend/utils';
16
17
-const OUTLINE_COLOR = '#f0f0f0';
18
-
17
// Note these colors are in sync with DevTools Profiler chart colors.
18
const COLORS = [
19
'#37afa9',
32
33
function drawNative(nodeToData: Map<HostInstance, Data>, agent: Agent) {
34
const nodesToDraw = [];
37
- iterateNodes(nodeToData, (_, color, node) => {
35
+ iterateNodes(nodeToData, ({color, node}) => {
36
nodesToDraw.push({node, color});
37
});
38
39
agent.emit('drawTraceUpdates', nodesToDraw);
40
+
41
+ const mergedNodes = groupAndSortNodes(nodeToData);
42
+ agent.emit('drawGroupedTraceUpdatesWithNames', mergedNodes);
43
}
44
45
function drawWeb(nodeToData: Map<HostInstance, Data>) {
47
initialize();
48
}
49
50
+ const dpr = window.devicePixelRatio || 1;
51
const canvasFlow: HTMLCanvasElement = ((canvas: any): HTMLCanvasElement);
50
- canvasFlow.width = window.innerWidth;
51
- canvasFlow.height = window.innerHeight;
52
+ canvasFlow.width = window.innerWidth * dpr;
53
+ canvasFlow.height = window.innerHeight * dpr;
54
+ canvasFlow.style.width = `${window.innerWidth}px`;
55
+ canvasFlow.style.height = `${window.innerHeight}px`;
56
57
const context = canvasFlow.getContext('2d');
54
- context.clearRect(0, 0, canvasFlow.width, canvasFlow.height);
55
- iterateNodes(nodeToData, (rect, color) => {
56
- if (rect !== null) {
57
- drawBorder(context, rect, color);
58
- }
58
+ context.scale(dpr, dpr);
59
+
60
+ context.clearRect(0, 0, canvasFlow.width / dpr, canvasFlow.height / dpr);
61
+
62
+ const mergedNodes = groupAndSortNodes(nodeToData);
63
+
64
+ mergedNodes.forEach(group => {
65
+ drawGroupBorders(context, group);
66
+ drawGroupLabel(context, group);
67
+ });
68
+}
69
+
70
+type GroupItem = {
71
+ rect: Rect,
72
+ color: string,
73
+ displayName: string | null,
74
+ count: number,
75
+};
76
+
77
+export type {GroupItem};
78
+
79
+export function groupAndSortNodes(
80
+ nodeToData: Map<HostInstance, Data>,
81
+): Array<Array<GroupItem>> {
82
+ const positionGroups: Map<string, Array<GroupItem>> = new Map();
83
+
84
+ iterateNodes(nodeToData, ({rect, color, displayName, count}) => {
85
+ if (!rect) return;
86
+ const key = `${rect.left},${rect.top}`;
87
+ if (!positionGroups.has(key)) positionGroups.set(key, []);
88
+ positionGroups.get(key)?.push({rect, color, displayName, count});
89
+ });
90
+
91
+ return Array.from(positionGroups.values()).sort((groupA, groupB) => {
92
+ const maxCountA = Math.max(...groupA.map(item => item.count));
93
+ const maxCountB = Math.max(...groupB.map(item => item.count));
94
+ return maxCountA - maxCountB;
95
+ });
96
+}
97
+
98
+function drawGroupBorders(
99
+ context: CanvasRenderingContext2D,
100
+ group: Array<GroupItem>,
101
+) {
102
+ group.forEach(({color, rect}) => {
103
+ context.beginPath();
104
+ context.strokeStyle = color;
105
+ context.rect(rect.left, rect.top, rect.width - 1, rect.height - 1);
106
+ context.stroke();
107
});
108
}
109
110
+function drawGroupLabel(
111
+ context: CanvasRenderingContext2D,
112
+ group: Array<GroupItem>,
113
+) {
114
+ const mergedName = group
115
+ .map(({displayName, count}) =>
116
+ displayName ? `${displayName}${count > 1 ? ` x${count}` : ''}` : '',
117
+ )
118
+ .filter(Boolean)
119
+ .join(', ');
120
+
121
+ if (mergedName) {
122
+ drawLabel(context, group[0].rect, mergedName, group[0].color);
123
+ }
124
+}
125
+
126
export function draw(nodeToData: Map<HostInstance, Data>, agent: Agent): void {
127
return isReactNativeEnvironment()
128
? drawNative(nodeToData, agent)
129
: drawWeb(nodeToData);
130
}
131
132
+type DataWithColorAndNode = {
133
+ ...Data,
134
+ color: string,
135
+ node: HostInstance,
136
+};
137
+
138
function iterateNodes(
139
nodeToData: Map<HostInstance, Data>,
70
- execute: (rect: Rect | null, color: string, node: HostInstance) => void,
140
+ execute: (data: DataWithColorAndNode) => void,
141
) {
72
- nodeToData.forEach(({count, rect}, node) => {
73
- const colorIndex = Math.min(COLORS.length - 1, count - 1);
142
+ nodeToData.forEach((data, node) => {
143
+ const colorIndex = Math.min(COLORS.length - 1, data.count - 1);
144
const color = COLORS[colorIndex];
75
- execute(rect, color, node);
145
+ execute({
146
+ color,
147
+ node,
148
+ count: data.count,
149
+ displayName: data.displayName,
150
+ expirationTime: data.expirationTime,
151
+ lastMeasuredAt: data.lastMeasuredAt,
152
+ rect: data.rect,
153
+ });
154
});
155
}
156
79
-function drawBorder(
157
+function drawLabel(
158
context: CanvasRenderingContext2D,
159
rect: Rect,
160
+ text: string,
161
color: string,
162
): void {
84
- const {height, left, top, width} = rect;
85
-
86
- // outline
87
- context.lineWidth = 1;
88
- context.strokeStyle = OUTLINE_COLOR;
89
-
90
- context.strokeRect(left - 1, top - 1, width + 2, height + 2);
91
-
92
- // inset
93
- context.lineWidth = 1;
94
- context.strokeStyle = OUTLINE_COLOR;
95
- context.strokeRect(left + 1, top + 1, width - 1, height - 1);
96
- context.strokeStyle = color;
97
-
98
- context.setLineDash([0]);
99
-
100
- // border
101
- context.lineWidth = 1;
102
- context.strokeRect(left, top, width - 1, height - 1);
103
-
104
- context.setLineDash([0]);
163
+ const {left, top} = rect;
164
+ context.font = '10px monospace';
165
+ context.textBaseline = 'middle';
166
+ context.textAlign = 'center';
167
+
168
+ const padding = 2;
169
+ const textHeight = 14;
170
+
171
+ const metrics = context.measureText(text);
172
+ const backgroundWidth = metrics.width + padding * 2;
173
+ const backgroundHeight = textHeight;
174
+ const labelX = left;
175
+ const labelY = top - backgroundHeight;
176
+
177
+ context.fillStyle = color;
178
+ context.fillRect(labelX, labelY, backgroundWidth, backgroundHeight);
179
+
180
+ context.fillStyle = '#000000';
181
+ context.fillText(
182
+ text,
183
+ labelX + backgroundWidth / 2,
184
+ labelY + backgroundHeight / 2,
185
+ );
186
}
187
188
function destroyNative(agent: Agent) {