diff --git a/packages/react-native/Libraries/Debugging/DebuggingOverlay.js b/packages/react-native/Libraries/Debugging/DebuggingOverlay.js index 7abe6d9be8a851..ab7408df5ba896 100644 --- a/packages/react-native/Libraries/Debugging/DebuggingOverlay.js +++ b/packages/react-native/Libraries/Debugging/DebuggingOverlay.js @@ -10,7 +10,7 @@ import type { ElementRectangle, - Overlay, + TraceUpdate, } from './DebuggingOverlayNativeComponent'; import View from '../Components/View/View'; @@ -26,7 +26,7 @@ const isNativeComponentReady = UIManager.hasViewManagerConfig('DebuggingOverlay'); type DebuggingOverlayHandle = { - highlightTraceUpdates(updates: Overlay[]): void, + highlightTraceUpdates(updates: TraceUpdate[]): void, highlightElements(elements: ElementRectangle[]): void, clearElementsHighlight(): void, }; @@ -44,11 +44,11 @@ function DebuggingOverlay( } const nonEmptyRectangles = updates.filter( - ({rect, color}) => rect.width >= 0 && rect.height >= 0, + ({rectangle, color}) => rectangle.width >= 0 && rectangle.height >= 0, ); if (nativeComponentRef.current != null) { - Commands.draw( + Commands.highlightTraceUpdates( nativeComponentRef.current, JSON.stringify(nonEmptyRectangles), ); diff --git a/packages/react-native/Libraries/Debugging/DebuggingOverlayNativeComponent.js b/packages/react-native/Libraries/Debugging/DebuggingOverlayNativeComponent.js index 204aca19062d0a..5dac5c155db3a1 100644 --- a/packages/react-native/Libraries/Debugging/DebuggingOverlayNativeComponent.js +++ b/packages/react-native/Libraries/Debugging/DebuggingOverlayNativeComponent.js @@ -20,8 +20,10 @@ type NativeProps = $ReadOnly<{| ...ViewProps, |}>; export type DebuggingOverlayNativeComponentType = HostComponent; -export type Overlay = { - rect: ElementRectangle, + +export type TraceUpdate = { + id: number, + rectangle: ElementRectangle, color: ?ProcessedColorValue, }; @@ -33,11 +35,11 @@ export type ElementRectangle = { }; interface NativeCommands { - +draw: ( + +highlightTraceUpdates: ( viewRef: React.ElementRef, // TODO(T144046177): Ideally we can pass array of Overlay, but currently // Array type is not supported in RN codegen for building native commands. - overlays: string, + updates: string, ) => void; +highlightElements: ( viewRef: React.ElementRef, @@ -50,7 +52,11 @@ interface NativeCommands { } export const Commands: NativeCommands = codegenNativeCommands({ - supportedCommands: ['draw', 'highlightElements', 'clearElementsHighlights'], + supportedCommands: [ + 'highlightTraceUpdates', + 'highlightElements', + 'clearElementsHighlights', + ], }); export default (codegenNativeComponent( diff --git a/packages/react-native/Libraries/Debugging/DebuggingOverlayRegistry.js b/packages/react-native/Libraries/Debugging/DebuggingOverlayRegistry.js index 2cc32e6ee65acb..249858dc881fdd 100644 --- a/packages/react-native/Libraries/Debugging/DebuggingOverlayRegistry.js +++ b/packages/react-native/Libraries/Debugging/DebuggingOverlayRegistry.js @@ -20,8 +20,9 @@ import type { ReactDevToolsAgentEvents, ReactDevToolsGlobalHook, } from '../Types/ReactDevToolsTypes'; -import type {Overlay} from './DebuggingOverlayNativeComponent'; +import type {TraceUpdate} from './DebuggingOverlayNativeComponent'; +import {findNodeHandle} from '../ReactNative/RendererProxy'; import processColor from '../StyleSheet/processColor'; // TODO(T171193075): __REACT_DEVTOOLS_GLOBAL_HOOK__ is always injected in dev-bundles, @@ -98,7 +99,7 @@ class DebuggingOverlayRegistry { #onDrawTraceUpdates: ( ...ReactDevToolsAgentEvents['drawTraceUpdates'] ) => void = traceUpdates => { - const promisesToResolve: Array> = []; + const promisesToResolve: Array> = []; traceUpdates.forEach(({node, color}) => { const publicInstance = this.#getPublicInstanceFromInstance(node); @@ -107,11 +108,18 @@ class DebuggingOverlayRegistry { return; } - const frameToDrawPromise = new Promise(resolve => { + const frameToDrawPromise = new Promise((resolve, reject) => { // TODO(T171095283): We should refactor this to use `getBoundingClientRect` when Paper is no longer supported. publicInstance.measure((x, y, width, height, left, top) => { + const id = findNodeHandle(node); + if (id == null) { + reject(); + return; + } + resolve({ - rect: {x: left, y: top, width, height}, + id, + rectangle: {x: left, y: top, width, height}, color: processColor(color), }); }); diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/DebuggingOverlay/RCTDebuggingOverlayComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/DebuggingOverlay/RCTDebuggingOverlayComponentView.mm index afd26e2f74e0c0..9c455569a8f452 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/DebuggingOverlay/RCTDebuggingOverlayComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/DebuggingOverlay/RCTDebuggingOverlayComponentView.mm @@ -50,9 +50,9 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args RCTDebuggingOverlayHandleCommand(self, commandName, args); } -- (void)draw:(NSString *)overlays +- (void)highlightTraceUpdates:(NSString *)updates { - [_overlay draw:overlays]; + [_overlay highlightTraceUpdates:updates]; } - (void)highlightElements:(NSString *)elements diff --git a/packages/react-native/React/Views/RCTDebuggingOverlay.h b/packages/react-native/React/Views/RCTDebuggingOverlay.h index f051de22d3c221..cc3475c649a06f 100644 --- a/packages/react-native/React/Views/RCTDebuggingOverlay.h +++ b/packages/react-native/React/Views/RCTDebuggingOverlay.h @@ -9,10 +9,18 @@ #import +@interface TraceUpdateTuple : NSObject + +@property (nonatomic, strong, readonly) UIView *view; +@property (nonatomic, copy, readonly) dispatch_block_t cleanupBlock; + +- (instancetype)initWithView:(UIView *)view cleanupBlock:(dispatch_block_t)cleanupBlock; + +@end + @interface RCTDebuggingOverlay : RCTView -- (void)draw:(NSString *)serializedNodes; -- (void)clearTraceUpdatesViews; +- (void)highlightTraceUpdates:(NSString *)serializedUpdates; - (void)highlightElements:(NSString *)serializedElements; - (void)clearElementsHighlights; diff --git a/packages/react-native/React/Views/RCTDebuggingOverlay.m b/packages/react-native/React/Views/RCTDebuggingOverlay.m index 986e7ed7b4cadd..861c64408a82b7 100644 --- a/packages/react-native/React/Views/RCTDebuggingOverlay.m +++ b/packages/react-native/React/Views/RCTDebuggingOverlay.m @@ -11,55 +11,99 @@ #import #import +@implementation TraceUpdateTuple + +- (instancetype)initWithView:(UIView *)view cleanupBlock:(dispatch_block_t)cleanupBlock +{ + if (self = [super init]) { + _view = view; + _cleanupBlock = cleanupBlock; + } + + return self; +} + +@end + @implementation RCTDebuggingOverlay { NSMutableArray *_highlightedElements; - NSMutableArray *_highlightedTraceUpdates; + NSMutableDictionary *_idToTraceUpdateMap; } -- (void)draw:(NSString *)serializedNodes +- (instancetype)initWithFrame:(CGRect)frame { - [self clearTraceUpdatesViews]; + self = [super initWithFrame:frame]; + if (self) { + _idToTraceUpdateMap = [NSMutableDictionary new]; + } + return self; +} +- (void)highlightTraceUpdates:(NSString *)serializedUpdates +{ NSError *error = nil; - id deserializedNodes = RCTJSONParse(serializedNodes, &error); + id deserializedUpdates = RCTJSONParse(serializedUpdates, &error); if (error) { - RCTLogError(@"Failed to parse serialized nodes passed to RCTDebuggingOverlay"); + RCTLogError(@"Failed to parse serialized updates passed to RCTDebuggingOverlay"); return; } - if (![deserializedNodes isKindOfClass:[NSArray class]]) { - RCTLogError(@"Expected to receive nodes as an array, got %@", NSStringFromClass([deserializedNodes class])); + if (![deserializedUpdates isKindOfClass:[NSArray class]]) { + RCTLogError(@"Expected to receive updates as an array, got %@", NSStringFromClass([deserializedUpdates class])); return; } - _highlightedTraceUpdates = [NSMutableArray new]; - for (NSDictionary *node in deserializedNodes) { - NSDictionary *nodeRectangle = node[@"rect"]; - NSNumber *nodeColor = node[@"color"]; + for (NSDictionary *update in deserializedUpdates) { + NSNumber *identifier = [RCTConvert NSNumber:update[@"id"]]; + NSDictionary *nodeRectangle = update[@"rectangle"]; + UIColor *nodeColor = [RCTConvert UIColor:update[@"color"]]; CGRect rect = [RCTConvert CGRect:nodeRectangle]; + TraceUpdateTuple *possiblyRegisteredTraceUpdateTuple = [_idToTraceUpdateMap objectForKey:identifier]; + if (possiblyRegisteredTraceUpdateTuple != nil) { + dispatch_block_t cleanupBlock = [possiblyRegisteredTraceUpdateTuple cleanupBlock]; + UIView *view = [possiblyRegisteredTraceUpdateTuple view]; + + dispatch_block_cancel(cleanupBlock); + + view.frame = rect; + view.layer.borderColor = nodeColor.CGColor; + + dispatch_block_t newCleanupBlock = ^{ + [self->_idToTraceUpdateMap removeObjectForKey:identifier]; + [view removeFromSuperview]; + }; + + [_idToTraceUpdateMap setObject:[[TraceUpdateTuple alloc] initWithView:view cleanupBlock:newCleanupBlock] + forKey:identifier]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), newCleanupBlock); + + continue; + } + UIView *box = [[UIView alloc] initWithFrame:rect]; box.backgroundColor = [UIColor clearColor]; box.layer.borderWidth = 2.0f; - box.layer.borderColor = [RCTConvert UIColor:nodeColor].CGColor; + box.layer.borderColor = nodeColor.CGColor; + + dispatch_block_t unmountViewAndPerformCleanup = ^{ + [self->_idToTraceUpdateMap removeObjectForKey:identifier]; + [box removeFromSuperview]; + }; + + TraceUpdateTuple *traceUpdateTuple = [[TraceUpdateTuple alloc] initWithView:box + cleanupBlock:unmountViewAndPerformCleanup]; + [_idToTraceUpdateMap setObject:traceUpdateTuple forKey:identifier]; [self addSubview:box]; - [_highlightedTraceUpdates addObject:box]; - } -} -- (void)clearTraceUpdatesViews -{ - if (_highlightedTraceUpdates != nil) { - for (UIView *v in _highlightedTraceUpdates) { - [v removeFromSuperview]; - } + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), unmountViewAndPerformCleanup); } - - _highlightedTraceUpdates = nil; } - (void)highlightElements:(NSString *)serializedElements diff --git a/packages/react-native/React/Views/RCTDebuggingOverlayManager.m b/packages/react-native/React/Views/RCTDebuggingOverlayManager.m index f65b22307c74cf..f077ab63d8b510 100644 --- a/packages/react-native/React/Views/RCTDebuggingOverlayManager.m +++ b/packages/react-native/React/Views/RCTDebuggingOverlayManager.m @@ -22,13 +22,13 @@ - (UIView *)view return [RCTDebuggingOverlay new]; } -RCT_EXPORT_METHOD(draw : (nonnull NSNumber *)viewTag nodes : (NSString *)serializedNodes) +RCT_EXPORT_METHOD(highlightTraceUpdates : (nonnull NSNumber *)viewTag nodes : (NSString *)serializedUpdates) { [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { UIView *view = viewRegistry[viewTag]; if ([view isKindOfClass:[RCTDebuggingOverlay class]]) { - [(RCTDebuggingOverlay *)view draw:serializedNodes]; + [(RCTDebuggingOverlay *)view highlightTraceUpdates:serializedUpdates]; } else { RCTLogError(@"Expected view to be RCTDebuggingOverlay, got %@", NSStringFromClass([view class])); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/debuggingoverlay/DebuggingOverlay.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/debuggingoverlay/DebuggingOverlay.java index 20c74edf33e5c1..c5523c522d70dc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/debuggingoverlay/DebuggingOverlay.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/debuggingoverlay/DebuggingOverlay.java @@ -13,54 +13,41 @@ import android.graphics.RectF; import android.view.View; import androidx.annotation.UiThread; -import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.bridge.UiThreadUtil; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; public class DebuggingOverlay extends View { - private final Paint mOverlayPaint = new Paint(); - private List mOverlays = new ArrayList(); + private final Paint mTraceUpdatePaint = new Paint(); + private HashMap mTraceUpdatesToDisplayMap = new HashMap(); + private HashMap mTraceUpdateIdToCleanupRunnableMap = new HashMap(); private final Paint mHighlightedElementsPaint = new Paint(); private List mHighlightedElementsRectangles = new ArrayList<>(); - public static class Overlay { - - private final int mColor; - private final RectF mRect; - - public Overlay(int color, RectF rect) { - mColor = color; - mRect = rect; - } - - public int getColor() { - return mColor; - } - - public RectF getPixelRect() { - return new RectF( - PixelUtil.toPixelFromDIP(mRect.left), - PixelUtil.toPixelFromDIP(mRect.top), - PixelUtil.toPixelFromDIP(mRect.right), - PixelUtil.toPixelFromDIP(mRect.bottom)); - } - } - public DebuggingOverlay(Context context) { super(context); - mOverlayPaint.setStyle(Paint.Style.STROKE); - mOverlayPaint.setStrokeWidth(6); + mTraceUpdatePaint.setStyle(Paint.Style.STROKE); + mTraceUpdatePaint.setStrokeWidth(6); mHighlightedElementsPaint.setStyle(Paint.Style.FILL); mHighlightedElementsPaint.setColor(0xCCC8E6FF); } @UiThread - public void setOverlays(List overlays) { - mOverlays = overlays; + public void setTraceUpdates(List traceUpdates) { + for (TraceUpdate traceUpdate : traceUpdates) { + int traceUpdateId = traceUpdate.getId(); + if (mTraceUpdateIdToCleanupRunnableMap.containsKey(traceUpdateId)) { + UiThreadUtil.removeOnUiThread(mTraceUpdateIdToCleanupRunnableMap.get(traceUpdateId)); + } + + mTraceUpdatesToDisplayMap.put(traceUpdateId, traceUpdate); + } + invalidate(); } @@ -81,9 +68,21 @@ public void onDraw(Canvas canvas) { super.onDraw(canvas); // Draw border outside of the given overlays to be aligned with web trace highlights - for (Overlay overlay : mOverlays) { - mOverlayPaint.setColor(overlay.getColor()); - canvas.drawRect(overlay.getPixelRect(), mOverlayPaint); + for (TraceUpdate traceUpdate : mTraceUpdatesToDisplayMap.values()) { + mTraceUpdatePaint.setColor(traceUpdate.getColor()); + canvas.drawRect(traceUpdate.getRectangle(), mTraceUpdatePaint); + + int traceUpdateId = traceUpdate.getId(); + Runnable block = + () -> { + mTraceUpdatesToDisplayMap.remove(traceUpdateId); + mTraceUpdateIdToCleanupRunnableMap.remove(traceUpdateId); + + invalidate(); + }; + + mTraceUpdateIdToCleanupRunnableMap.put(traceUpdateId, block); + UiThreadUtil.runOnUiThread(block, 2000); } for (RectF elementRectangle : mHighlightedElementsRectangles) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/debuggingoverlay/DebuggingOverlayManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/debuggingoverlay/DebuggingOverlayManager.java index 02af5711c6c4cc..b739521d1fb252 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/debuggingoverlay/DebuggingOverlayManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/debuggingoverlay/DebuggingOverlayManager.java @@ -17,7 +17,6 @@ import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.SimpleViewManager; import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.views.debuggingoverlay.DebuggingOverlay.Overlay; import java.util.ArrayList; import java.util.List; import org.json.JSONArray; @@ -26,6 +25,7 @@ @ReactModule(name = DebuggingOverlayManager.REACT_CLASS) public class DebuggingOverlayManager extends SimpleViewManager { + public static final String REACT_CLASS = "DebuggingOverlay"; public DebuggingOverlayManager() {} @@ -34,33 +34,43 @@ public DebuggingOverlayManager() {} public void receiveCommand( DebuggingOverlay view, String commandId, @Nullable ReadableArray args) { switch (commandId) { - case "draw": + case "highlightTraceUpdates": if (args == null) { break; } - String overlaysStr = args.getString(0); - if (overlaysStr == null) { + String serializedTraceUpdates = args.getString(0); + if (serializedTraceUpdates == null) { return; } try { - JSONArray overlaysArr = new JSONArray(overlaysStr); - List overlays = new ArrayList<>(); - for (int i = 0; i < overlaysArr.length(); i++) { - JSONObject overlay = overlaysArr.getJSONObject(i); - JSONObject rectObj = overlay.getJSONObject("rect"); - float left = (float) rectObj.getDouble("x"); - float top = (float) rectObj.getDouble("y"); - float right = (float) (left + rectObj.getDouble("width")); - float bottom = (float) (top + rectObj.getDouble("height")); - RectF rect = new RectF(left, top, right, bottom); - overlays.add(new Overlay(overlay.getInt("color"), rect)); + JSONArray traceUpdates = new JSONArray(serializedTraceUpdates); + List deserializedTraceUpdates = new ArrayList<>(); + for (int i = 0; i < traceUpdates.length(); i++) { + JSONObject traceUpdate = traceUpdates.getJSONObject(i); + + int id = traceUpdate.getInt("id"); + JSONObject serializedRectangle = traceUpdate.getJSONObject("rectangle"); + int color = traceUpdate.getInt("color"); + + float left = (float) serializedRectangle.getDouble("x"); + float top = (float) serializedRectangle.getDouble("y"); + float right = (float) (left + serializedRectangle.getDouble("width")); + float bottom = (float) (top + serializedRectangle.getDouble("height")); + RectF rectangle = + new RectF( + PixelUtil.toPixelFromDIP(left), + PixelUtil.toPixelFromDIP(top), + PixelUtil.toPixelFromDIP(right), + PixelUtil.toPixelFromDIP(bottom)); + + deserializedTraceUpdates.add(new TraceUpdate(id, rectangle, color)); } - view.setOverlays(overlays); + view.setTraceUpdates(deserializedTraceUpdates); } catch (JSONException e) { - FLog.e(REACT_CLASS, "Failed to parse overlays: ", e); + FLog.e(REACT_CLASS, "Failed to parse highlightTraceUpdates payload: ", e); } break; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/debuggingoverlay/TraceUpdate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/debuggingoverlay/TraceUpdate.java new file mode 100644 index 00000000000000..762a61746500ff --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/debuggingoverlay/TraceUpdate.java @@ -0,0 +1,35 @@ +/* + * 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. + */ + +package com.facebook.react.views.debuggingoverlay; + +import android.graphics.RectF; + +public final class TraceUpdate { + + private final int mId; + private final int mColor; + private final RectF mRectangle; + + public TraceUpdate(int id, RectF rectangle, int color) { + mId = id; + mRectangle = rectangle; + mColor = color; + } + + public int getId() { + return mId; + } + + public int getColor() { + return mColor; + } + + public RectF getRectangle() { + return mRectangle; + } +}