From c38e017d2a045f6e1f750fc87357adc46a6e03f8 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 21 Nov 2021 18:24:40 -0500 Subject: [PATCH] Highlight current search result in Timeline --- .../react-devtools-timeline/src/CanvasPage.js | 13 ++++---- .../src/TimelineContext.js | 23 +++++++++++-- .../content-views/ComponentMeasuresView.js | 32 +++++++++++++++++-- .../src/content-views/constants.js | 4 +++ packages/react-devtools-timeline/src/types.js | 13 ++++++++ 5 files changed, 74 insertions(+), 11 deletions(-) diff --git a/packages/react-devtools-timeline/src/CanvasPage.js b/packages/react-devtools-timeline/src/CanvasPage.js index 3a0bcb2262ec6..737add0129ddb 100644 --- a/packages/react-devtools-timeline/src/CanvasPage.js +++ b/packages/react-devtools-timeline/src/CanvasPage.js @@ -174,12 +174,9 @@ function AutoSizedCanvas({ const {searchIndex, searchResults} = useContext(TimelineSearchContext); useLayoutEffect(() => { - if (searchResults.length === 0) { - return; - } - - const componentMeasure = searchResults[searchIndex]; - if (componentMeasure !== null) { + const componentMeasure = + searchResults.length > 0 ? searchResults[searchIndex] : null; + if (componentMeasure != null) { const scrollState = moveStateToRange({ state: viewState.horizontalScrollState, rangeStart: componentMeasure.timestamp, @@ -191,8 +188,11 @@ function AutoSizedCanvas({ }); viewState.updateHorizontalScrollState(scrollState); + viewState.updateSearchResult({componentMeasure}); surfaceRef.current.displayIfNeeded(); + } else { + viewState.updateSearchResult(null); } }, [searchIndex, searchResults, viewState]); @@ -360,6 +360,7 @@ function AutoSizedCanvas({ surface, defaultFrame, data, + viewState, ); componentMeasuresViewRef.current = componentMeasuresView; componentMeasuresViewWrapper = createViewHelper( diff --git a/packages/react-devtools-timeline/src/TimelineContext.js b/packages/react-devtools-timeline/src/TimelineContext.js index 9e2949e145d1f..d6427eceb61e1 100644 --- a/packages/react-devtools-timeline/src/TimelineContext.js +++ b/packages/react-devtools-timeline/src/TimelineContext.js @@ -10,7 +10,12 @@ import * as React from 'react'; import {createContext, useMemo, useRef, useState} from 'react'; -import type {HorizontalScrollStateChangeCallback, ViewState} from './types'; +import type { + HorizontalScrollStateChangeCallback, + SearchResult, + SearchResultStateChangeCallback, + ViewState, +} from './types'; import type {RefObject} from 'shared/ReactTypes'; export type Context = {| @@ -34,17 +39,22 @@ function TimelineContextController({children}: Props) { // Recreate view state any time new profiling data is imported. const viewState = useMemo(() => { const horizontalScrollStateChangeCallbacks: Set = new Set(); + const searchResultStateChangeCallbacks: Set = new Set(); const horizontalScrollState = { offset: 0, length: 0, }; - return { + const state: ViewState = { horizontalScrollState, onHorizontalScrollStateChange: callback => { horizontalScrollStateChangeCallbacks.add(callback); }, + onSearchResultStateChange: callback => { + searchResultStateChangeCallbacks.add(callback); + }, + searchResult: null, updateHorizontalScrollState: scrollState => { if ( horizontalScrollState.offset === scrollState.offset && @@ -60,8 +70,17 @@ function TimelineContextController({children}: Props) { callback(scrollState); }); }, + updateSearchResult: (searchResult: SearchResult | null) => { + state.searchResult = searchResult; + + searchResultStateChangeCallbacks.forEach(callback => { + callback(searchResult); + }); + }, viewToMutableViewStateMap: new Map(), }; + + return state; }, [file]); const value = useMemo( diff --git a/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js b/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js index a241498a05a05..970bb1c9bae93 100644 --- a/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js +++ b/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js @@ -7,7 +7,12 @@ * @flow */ -import type {ReactComponentMeasure, ReactProfilerData} from '../types'; +import type { + ReactComponentMeasure, + ReactProfilerData, + SearchResult, + ViewState, +} from '../types'; import type { Interaction, IntrinsicSize, @@ -31,7 +36,7 @@ import { rectIntersectsRect, intersectionOfRects, } from '../view-base'; -import {COLORS, NATIVE_EVENT_HEIGHT, BORDER_SIZE} from './constants'; +import {BORDER_SIZE, COLORS, NATIVE_EVENT_HEIGHT} from './constants'; const ROW_WITH_BORDER_HEIGHT = NATIVE_EVENT_HEIGHT + BORDER_SIZE; @@ -39,12 +44,20 @@ export class ComponentMeasuresView extends View { _hoveredComponentMeasure: ReactComponentMeasure | null = null; _intrinsicSize: IntrinsicSize; _profilerData: ReactProfilerData; + _searchResult: ReactComponentMeasure | null = null; onHover: ((event: ReactComponentMeasure | null) => void) | null = null; - constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + constructor( + surface: Surface, + frame: Rect, + profilerData: ReactProfilerData, + viewState: ViewState, + ) { super(surface, frame); + viewState.onSearchResultStateChange(this._handleSearchResultStateChange); + this._profilerData = profilerData; this._intrinsicSize = { @@ -74,6 +87,7 @@ export class ComponentMeasuresView extends View { componentMeasure: ReactComponentMeasure, scaleFactor: number, showHoverHighlight: boolean, + showSearchResultBorder: boolean, ): boolean { const {frame} = this; const { @@ -150,6 +164,11 @@ export class ComponentMeasuresView extends View { break; } } + + if (showSearchResultBorder) { + context.fillStyle = COLORS.SEARCH_RESULT_FILL; + } + context.fillRect( drawableRect.origin.x, drawableRect.origin.y, @@ -171,6 +190,7 @@ export class ComponentMeasuresView extends View { frame, _profilerData: {componentMeasures}, _hoveredComponentMeasure, + _searchResult, visibleArea, } = this; @@ -197,6 +217,7 @@ export class ComponentMeasuresView extends View { componentMeasure, scaleFactor, componentMeasure === _hoveredComponentMeasure, + componentMeasure === _searchResult, ) || didDrawMeasure; }); @@ -256,6 +277,11 @@ export class ComponentMeasuresView extends View { onHover(null); } + _handleSearchResultStateChange = (searchResult: SearchResult | null) => { + this._searchResult = + searchResult === null ? null : searchResult.componentMeasure; + }; + handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { switch (interaction.type) { case 'mousemove': diff --git a/packages/react-devtools-timeline/src/content-views/constants.js b/packages/react-devtools-timeline/src/content-views/constants.js index f2920ac19b516..bf60a1fbf9a7a 100644 --- a/packages/react-devtools-timeline/src/content-views/constants.js +++ b/packages/react-devtools-timeline/src/content-views/constants.js @@ -91,6 +91,7 @@ export let COLORS = { SCROLL_CARET: '', SCRUBBER_BACKGROUND: '', SCRUBBER_BORDER: '', + SEARCH_RESULT_FILL: '', TEXT_COLOR: '', TEXT_DIM_COLOR: '', TIME_MARKER_LABEL: '', @@ -235,6 +236,9 @@ export function updateColorsToMatchTheme(element: Element): boolean { SCRUBBER_BACKGROUND: computedStyle.getPropertyValue( '--color-timeline-react-suspense-rejected', ), + SEARCH_RESULT_FILL: computedStyle.getPropertyValue( + '--color-timeline-react-suspense-rejected', + ), SCRUBBER_BORDER: computedStyle.getPropertyValue( '--color-timeline-text-color', ), diff --git a/packages/react-devtools-timeline/src/types.js b/packages/react-devtools-timeline/src/types.js index 5a532495187ee..b9d424db27fe6 100644 --- a/packages/react-devtools-timeline/src/types.js +++ b/packages/react-devtools-timeline/src/types.js @@ -167,10 +167,18 @@ export type FlamechartStackLayer = FlamechartStackFrame[]; export type Flamechart = FlamechartStackLayer[]; +export type SearchResult = {| + componentMeasure: ReactComponentMeasure | null, +|}; + export type HorizontalScrollStateChangeCallback = ( scrollState: ScrollState, ) => void; +export type SearchResultStateChangeCallback = ( + searchResult: SearchResult | null, +) => void; + // Imperative view state that corresponds to profiler data. // This state lives outside of React's lifecycle // and should be erased/reset whenever new profiler data is loaded. @@ -179,7 +187,12 @@ export type ViewState = {| onHorizontalScrollStateChange: ( callback: HorizontalScrollStateChangeCallback, ) => void, + onSearchResultStateChange: ( + callback: SearchResultStateChangeCallback, + ) => void, + searchResult: SearchResult | null, updateHorizontalScrollState: (scrollState: ScrollState) => void, + updateSearchResult: (searchResult: SearchResult | null) => void, viewToMutableViewStateMap: Map, |};