Skip to content

Commit

Permalink
feat[DebuggingOverlay]: support native commands for highlighting elem…
Browse files Browse the repository at this point in the history
…ents (#41745)

Summary:

Changelog: [Internal]

Support `highlightElements` and `clearElementsHighlights` commands in `DebuggingOverlay` native components.

These later will be used for highlighting inspected component in React DevTools. These commands unblock highlighting elements on the native side, currently we do it on JS side and it mutates the React tree.

We still need to serialize the array before passing it to the native command, because codegen doesn't support it yet.

Differential Revision: D51603861
  • Loading branch information
Ruslan Lesiutin authored and facebook-github-bot committed Dec 6, 2023
1 parent d2b7c46 commit 608d169
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,18 @@ interface NativeCommands {
// Array type is not supported in RN codegen for building native commands.
overlays: string,
) => void;
+highlightElements: (
viewRef: React.ElementRef<DebuggingOverlayNativeComponentType>,
// TODO(T144046177): Codegen doesn't support array type for native commands yet.
elements: string,
) => void;
+clearElementsHighlights: (
viewRef: React.ElementRef<DebuggingOverlayNativeComponentType>,
) => void;
}

export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
supportedCommands: ['draw'],
supportedCommands: ['draw', 'highlightElements', 'clearElementsHighlights'],
});

export default (codegenNativeComponent<NativeProps>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ - (void)draw:(NSString *)overlays
[_overlay draw:overlays];
}

- (void)highlightElements:(NSString *)elements
{
[_overlay highlightElements:elements];
}

- (void)clearElementsHighlights
{
[_overlay clearElementsHighlights];
}

@end

Class<RCTComponentViewProtocol> RCTDebuggingOverlayCls(void)
Expand Down
3 changes: 3 additions & 0 deletions packages/react-native/React/Views/RCTDebuggingOverlay.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@
@interface RCTDebuggingOverlay : RCTView

- (void)draw:(NSString *)serializedNodes;
- (void)clearTraceUpdatesViews;
- (void)highlightElements:(NSString *)serializedElements;
- (void)clearElementsHighlights;

@end
63 changes: 58 additions & 5 deletions packages/react-native/React/Views/RCTDebuggingOverlay.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
#import <React/RCTLog.h>
#import <React/RCTUtils.h>

@implementation RCTDebuggingOverlay
@implementation RCTDebuggingOverlay {
NSMutableArray<UIView *> *_highlightedElements;
NSMutableArray<UIView *> *_highlightedTraceUpdates;
}

- (void)draw:(NSString *)serializedNodes
{
NSArray *subViewsToRemove = [self subviews];
for (UIView *v in subViewsToRemove) {
[v removeFromSuperview];
}
[self clearTraceUpdatesViews];

NSError *error = nil;
id deserializedNodes = RCTJSONParse(serializedNodes, &error);
Expand All @@ -33,6 +33,7 @@ - (void)draw:(NSString *)serializedNodes
return;
}

_highlightedTraceUpdates = [NSMutableArray new];
for (NSDictionary *node in deserializedNodes) {
NSDictionary *nodeRectangle = node[@"rect"];
NSNumber *nodeColor = node[@"color"];
Expand All @@ -51,7 +52,59 @@ - (void)draw:(NSString *)serializedNodes
box.layer.borderColor = [RCTConvert UIColor:nodeColor].CGColor;

[self addSubview:box];
[_highlightedTraceUpdates addObject:box];
}
}

- (void)clearTraceUpdatesViews
{
if (_highlightedTraceUpdates != nil) {
for (UIView *v in _highlightedTraceUpdates) {
[v removeFromSuperview];
}
}

_highlightedTraceUpdates = nil;
}

- (void)highlightElements:(NSString *)serializedElements
{
NSError *error = nil;
id deserializedRectangles = RCTJSONParse(serializedElements, &error);

if (error) {
RCTLogError(@"Failed to parse serialized elements passed to RCTDebuggingOverlay");
return;
}

if (![deserializedRectangles isKindOfClass:[NSArray class]]) {
RCTLogError(
@"Expected to receive rectangles as an array, got %@", NSStringFromClass([deserializedRectangles class]));
return;
}

if (_highlightedElements == nil) {
_highlightedElements = [NSMutableArray new];
}

for (NSDictionary *rectangle in deserializedRectangles) {
UIView *view = [[UIView alloc] initWithFrame:[RCTConvert CGRect:rectangle]];
view.backgroundColor = [UIColor colorWithRed:200 / 255.0 green:230 / 255.0 blue:255 / 255.0 alpha:0.8];

[self addSubview:view];
[_highlightedElements addObject:view];
}
}

- (void)clearElementsHighlights
{
if (_highlightedElements != nil) {
for (UIView *v in _highlightedElements) {
[v removeFromSuperview];
}
}

_highlightedElements = nil;
}

@end
26 changes: 26 additions & 0 deletions packages/react-native/React/Views/RCTDebuggingOverlayManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,30 @@ - (UIView *)view
}];
}

RCT_EXPORT_METHOD(highlightElements : (nonnull NSNumber *)viewTag elements : (NSString *)serializedElements)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
UIView *view = viewRegistry[viewTag];

if ([view isKindOfClass:[RCTDebuggingOverlay class]]) {
[(RCTDebuggingOverlay *)view highlightElements:serializedElements];
} else {
RCTLogError(@"Expected view to be RCTDebuggingOverlay, got %@", NSStringFromClass([view class]));
}
}];
}

RCT_EXPORT_METHOD(clearElementsHighlights : (nonnull NSNumber *)viewTag)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
UIView *view = viewRegistry[viewTag];

if ([view isKindOfClass:[RCTDebuggingOverlay class]]) {
[(RCTDebuggingOverlay *)view clearElementsHighlights];
} else {
RCTLogError(@"Expected view to be RCTDebuggingOverlay, got %@", NSStringFromClass([view class]));
}
}];
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@
import java.util.List;

public class DebuggingOverlay extends View {

private final Paint mOverlayPaint = new Paint();
private List<Overlay> mOverlays = new ArrayList<Overlay>();

private final Paint mHighlightedElementsPaint = new Paint();
private List<RectF> mHighlightedElementsRectangles = new ArrayList<>();

public static class Overlay {

private final int mColor;
private final RectF mRect;

Expand All @@ -45,8 +50,12 @@ public RectF getPixelRect() {

public DebuggingOverlay(Context context) {
super(context);

mOverlayPaint.setStyle(Paint.Style.STROKE);
mOverlayPaint.setStrokeWidth(6);

mHighlightedElementsPaint.setStyle(Paint.Style.FILL);
mHighlightedElementsPaint.setColor(0xCCC8E6FF);
}

@UiThread
Expand All @@ -55,16 +64,30 @@ public void setOverlays(List<Overlay> overlays) {
invalidate();
}

@UiThread
public void setHighlightedElementsRectangles(List<RectF> elementsRectangles) {
mHighlightedElementsRectangles = elementsRectangles;
invalidate();
}

@UiThread
public void clearElementsHighlights() {
mHighlightedElementsRectangles.clear();
invalidate();
}

@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);

if (!mOverlays.isEmpty()) {
// 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);
}
// 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 (RectF elementRectangle : mHighlightedElementsRectangles) {
canvas.drawRect(elementRectangle, mHighlightedElementsPaint);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.module.annotations.ReactModule;
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;
Expand Down Expand Up @@ -63,6 +64,47 @@ public void receiveCommand(
}
break;

case "highlightElements":
if (args == null) {
return;
}

String serializedElements = args.getString(0);
if (serializedElements == null) {
return;
}

try {
JSONArray deserializedElements = new JSONArray(serializedElements);
List<RectF> elementsRectangles = new ArrayList<>();
for (int i = 0; i < deserializedElements.length(); i++) {
JSONObject element = deserializedElements.getJSONObject(i);

float left = (float) element.getDouble("x");
float top = (float) element.getDouble("y");
float right = (float) (left + element.getDouble("width"));
float bottom = (float) (top + element.getDouble("height"));
RectF rect =
new RectF(
PixelUtil.toPixelFromDIP(left),
PixelUtil.toPixelFromDIP(top),
PixelUtil.toPixelFromDIP(right),
PixelUtil.toPixelFromDIP(bottom));

elementsRectangles.add(rect);
}

view.setHighlightedElementsRectangles(elementsRectangles);
} catch (JSONException e) {
FLog.e(REACT_CLASS, "Failed to parse highlightElements payload: ", e);
}
break;

case "clearElementsHighlights":
view.clearElementsHighlights();

break;

default:
ReactSoftExceptionLogger.logSoftException(
REACT_CLASS,
Expand Down

0 comments on commit 608d169

Please sign in to comment.