/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js
253 строки
7 KB
YongSeok Jang (장용석)
[DevTools] Use Popover API for TraceUpdates highlighting (#32614)
07 май 2025, 17:48
Не верифицирован
07 май 2025, 17:48
53c9f81
Код
Авторство
О чём код?
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type {Data} from './index'; import type {Rect} from '../utils'; import type {HostInstance} from '../../types'; import type Agent from '../../agent'; import {isReactNativeEnvironment} from 'react-devtools-shared/src/backend/utils'; // Note these colors are in sync with DevTools Profiler chart colors. const COLORS = [ '#37afa9', '#63b19e', '#80b393', '#97b488', '#abb67d', '#beb771', '#cfb965', '#dfba57', '#efbb49', '#febc38', ]; let canvas: HTMLCanvasElement | null = null; function drawNative(nodeToData: Map<HostInstance, Data>, agent: Agent) { const nodesToDraw = []; iterateNodes(nodeToData, ({color, node}) => { nodesToDraw.push({node, color}); }); agent.emit('drawTraceUpdates', nodesToDraw); const mergedNodes = groupAndSortNodes(nodeToData); agent.emit('drawGroupedTraceUpdatesWithNames', mergedNodes); } function drawWeb(nodeToData: Map<HostInstance, Data>) { if (canvas === null) { initialize(); } const dpr = window.devicePixelRatio || 1; const canvasFlow: HTMLCanvasElement = ((canvas: any): HTMLCanvasElement); canvasFlow.width = window.innerWidth * dpr; canvasFlow.height = window.innerHeight * dpr; canvasFlow.style.width = `${window.innerWidth}px`; canvasFlow.style.height = `${window.innerHeight}px`; const context = canvasFlow.getContext('2d'); context.scale(dpr, dpr); context.clearRect(0, 0, canvasFlow.width / dpr, canvasFlow.height / dpr); const mergedNodes = groupAndSortNodes(nodeToData); mergedNodes.forEach(group => { drawGroupBorders(context, group); drawGroupLabel(context, group); }); if (canvas !== null) { if (nodeToData.size === 0 && canvas.matches(':popover-open')) { // $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API canvas.hidePopover(); return; } // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API if (canvas.matches(':popover-open')) { // $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API canvas.hidePopover(); } // $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API canvas.showPopover(); } } type GroupItem = { rect: Rect, color: string, displayName: string | null, count: number, }; export type {GroupItem}; export function groupAndSortNodes( nodeToData: Map<HostInstance, Data>, ): Array<Array<GroupItem>> { const positionGroups: Map<string, Array<GroupItem>> = new Map(); iterateNodes(nodeToData, ({rect, color, displayName, count}) => { if (!rect) return; const key = `${rect.left},${rect.top}`; if (!positionGroups.has(key)) positionGroups.set(key, []); positionGroups.get(key)?.push({rect, color, displayName, count}); }); return Array.from(positionGroups.values()).sort((groupA, groupB) => { const maxCountA = Math.max(...groupA.map(item => item.count)); const maxCountB = Math.max(...groupB.map(item => item.count)); return maxCountA - maxCountB; }); } function drawGroupBorders( context: CanvasRenderingContext2D, group: Array<GroupItem>, ) { group.forEach(({color, rect}) => { context.beginPath(); context.strokeStyle = color; context.rect(rect.left, rect.top, rect.width - 1, rect.height - 1); context.stroke(); }); } function drawGroupLabel( context: CanvasRenderingContext2D, group: Array<GroupItem>, ) { const mergedName = group .map(({displayName, count}) => displayName ? `${displayName}${count > 1 ? ` x${count}` : ''}` : '', ) .filter(Boolean) .join(', '); if (mergedName) { drawLabel(context, group[0].rect, mergedName, group[0].color); } } export function draw(nodeToData: Map<HostInstance, Data>, agent: Agent): void { return isReactNativeEnvironment() ? drawNative(nodeToData, agent) : drawWeb(nodeToData); } type DataWithColorAndNode = { ...Data, color: string, node: HostInstance, }; function iterateNodes( nodeToData: Map<HostInstance, Data>, execute: (data: DataWithColorAndNode) => void, ) { nodeToData.forEach((data, node) => { const colorIndex = Math.min(COLORS.length - 1, data.count - 1); const color = COLORS[colorIndex]; execute({ color, node, count: data.count, displayName: data.displayName, expirationTime: data.expirationTime, lastMeasuredAt: data.lastMeasuredAt, rect: data.rect, }); }); } function drawLabel( context: CanvasRenderingContext2D, rect: Rect, text: string, color: string, ): void { const {left, top} = rect; context.font = '10px monospace'; context.textBaseline = 'middle'; context.textAlign = 'center'; const padding = 2; const textHeight = 14; const metrics = context.measureText(text); const backgroundWidth = metrics.width + padding * 2; const backgroundHeight = textHeight; const labelX = left; const labelY = top - backgroundHeight; context.fillStyle = color; context.fillRect(labelX, labelY, backgroundWidth, backgroundHeight); context.fillStyle = '#000000'; context.fillText( text, labelX + backgroundWidth / 2, labelY + backgroundHeight / 2, ); } function destroyNative(agent: Agent) { agent.emit('disableTraceUpdates'); } function destroyWeb() { if (canvas !== null) { if (canvas.matches(':popover-open')) { // $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API canvas.hidePopover(); } // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API and loses canvas nullability tracking if (canvas.parentNode != null) { // $FlowFixMe[incompatible-call]: Flow doesn't track that canvas is non-null here canvas.parentNode.removeChild(canvas); } canvas = null; } } export function destroy(agent: Agent): void { return isReactNativeEnvironment() ? destroyNative(agent) : destroyWeb(); } function initialize(): void { canvas = window.document.createElement('canvas'); canvas.setAttribute('popover', 'manual'); // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API canvas.style.cssText = ` xx-background-color: red; xx-opacity: 0.5; bottom: 0; left: 0; pointer-events: none; position: fixed; right: 0; top: 0; background-color: transparent; outline: none; box-shadow: none; border: none; `; const root = window.document.documentElement; root.insertBefore(canvas, root.firstChild); }