/**
* 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 Agent from 'react-devtools-shared/src/backend/agent';
import resolveBoxStyle from './resolveBoxStyle';
import isArray from 'react-devtools-shared/src/isArray';
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
import type {RendererID} from '../types';
import type {StyleAndLayout} from './types';
export type ResolveNativeStyle = (stylesheetID: any) => ?Object;
export type SetupNativeStyleEditor = typeof setupNativeStyleEditor;
export default function setupNativeStyleEditor(
bridge: BackendBridge,
agent: Agent,
resolveNativeStyle: ResolveNativeStyle,
validAttributes?: $ReadOnlyArray<string> | null,
) {
bridge.addListener(
'NativeStyleEditor_measure',
({id, rendererID}: {id: number, rendererID: RendererID}) => {
measureStyle(agent, bridge, resolveNativeStyle, id, rendererID);
},
);
bridge.addListener(
'NativeStyleEditor_renameAttribute',
({
id,
rendererID,
oldName,
newName,
value,
}: {
id: number,
rendererID: RendererID,
oldName: string,
newName: string,
value: string,
}) => {
renameStyle(agent, id, rendererID, oldName, newName, value);
setTimeout(() =>
measureStyle(agent, bridge, resolveNativeStyle, id, rendererID),
);
},
);
bridge.addListener(
'NativeStyleEditor_setValue',
({
id,
rendererID,
name,
value,
}: {
id: number,
rendererID: number,
name: string,
value: string,
}) => {
setStyle(agent, id, rendererID, name, value);
setTimeout(() =>
measureStyle(agent, bridge, resolveNativeStyle, id, rendererID),
);
},
);
bridge.send('isNativeStyleEditorSupported', {
isSupported: true,
validAttributes,
});
}
const EMPTY_BOX_STYLE = {
top: 0,
left: 0,
right: 0,
bottom: 0,
};
const componentIDToStyleOverrides: Map<number, Object> = new Map();
function measureStyle(
agent: Agent,
bridge: BackendBridge,
resolveNativeStyle: ResolveNativeStyle,
id: number,
rendererID: RendererID,
) {
const data = agent.getInstanceAndStyle({id, rendererID});
if (!data || !data.style) {
bridge.send(
'NativeStyleEditor_styleAndLayout',
({
id,
layout: null,
style: null,
}: StyleAndLayout),
);
return;
}
const {instance, style} = data;
let resolvedStyle = resolveNativeStyle(style);
// If it's a host component we edited before, amend styles.
const styleOverrides = componentIDToStyleOverrides.get(id);
if (styleOverrides != null) {
resolvedStyle = Object.assign({}, resolvedStyle, styleOverrides);
}
if (!instance || typeof instance.measure !== 'function') {
bridge.send(
'NativeStyleEditor_styleAndLayout',
({
id,
layout: null,
style: resolvedStyle || null,
}: StyleAndLayout),
);
return;
}
instance.measure((x, y, width, height, left, top) => {
// RN Android sometimes returns undefined here. Don't send measurements in this case.
// https://github.com/jhen0409/react-native-debugger/issues/84#issuecomment-304611817
if (typeof x !== 'number') {
bridge.send(
'NativeStyleEditor_styleAndLayout',
({
id,
layout: null,
style: resolvedStyle || null,
}: StyleAndLayout),
);
return;
}
const margin =
(resolvedStyle != null && resolveBoxStyle('margin', resolvedStyle)) ||
EMPTY_BOX_STYLE;
const padding =
(resolvedStyle != null && resolveBoxStyle('padding', resolvedStyle)) ||
EMPTY_BOX_STYLE;
bridge.send(
'NativeStyleEditor_styleAndLayout',
({
id,
layout: {
x,
y,
width,
height,
left,
top,
margin,
padding,
},
style: resolvedStyle || null,
}: StyleAndLayout),
);
});
}
function shallowClone(object: Object): Object {
const cloned: {[string]: $FlowFixMe} = {};
for (const n in object) {
cloned[n] = object[n];
}
return cloned;
}
function renameStyle(
agent: Agent,
id: number,
rendererID: RendererID,
oldName: string,
newName: string,
value: string,
): void {
const data = agent.getInstanceAndStyle({id, rendererID});
if (!data || !data.style) {
return;
}
const {instance, style} = data;
const newStyle = newName
? {[oldName]: undefined, [newName]: value}
: {[oldName]: undefined};
let customStyle;
// TODO It would be nice if the renderer interface abstracted this away somehow.
if (instance !== null && typeof instance.setNativeProps === 'function') {
// In the case of a host component, we need to use setNativeProps().
// Remember to "correct" resolved styles when we read them next time.
const styleOverrides = componentIDToStyleOverrides.get(id);
if (!styleOverrides) {
componentIDToStyleOverrides.set(id, newStyle);
} else {
Object.assign(styleOverrides, newStyle);
}
// TODO Fabric does not support setNativeProps; chat with Sebastian or Eli
instance.setNativeProps({style: newStyle});
} else if (isArray(style)) {
const lastIndex = style.length - 1;
if (typeof style[lastIndex] === 'object' && !isArray(style[lastIndex])) {
customStyle = shallowClone(style[lastIndex]);
delete customStyle[oldName];
if (newName) {
customStyle[newName] = value;
} else {
customStyle[oldName] = undefined;
}
agent.overrideValueAtPath({
type: 'props',
id,
rendererID,
path: ['style', lastIndex],
value: customStyle,
});
} else {
agent.overrideValueAtPath({
type: 'props',
id,
rendererID,
path: ['style'],
value: style.concat([newStyle]),
});
}
} else if (typeof style === 'object') {
customStyle = shallowClone(style);
delete customStyle[oldName];
if (newName) {
customStyle[newName] = value;
} else {
customStyle[oldName] = undefined;
}
agent.overrideValueAtPath({
type: 'props',
id,
rendererID,
path: ['style'],
value: customStyle,
});
} else {
agent.overrideValueAtPath({
type: 'props',
id,
rendererID,
path: ['style'],
value: [style, newStyle],
});
}
agent.emit('hideNativeHighlight');
}
function setStyle(
agent: Agent,
id: number,
rendererID: RendererID,
name: string,
value: string,
) {
const data = agent.getInstanceAndStyle({id, rendererID});
if (!data || !data.style) {
return;
}
const {instance, style} = data;
const newStyle = {[name]: value};
// TODO It would be nice if the renderer interface abstracted this away somehow.
if (instance !== null && typeof instance.setNativeProps === 'function') {
// In the case of a host component, we need to use setNativeProps().
// Remember to "correct" resolved styles when we read them next time.
const styleOverrides = componentIDToStyleOverrides.get(id);
if (!styleOverrides) {
componentIDToStyleOverrides.set(id, newStyle);
} else {
Object.assign(styleOverrides, newStyle);
}
// TODO Fabric does not support setNativeProps; chat with Sebastian or Eli
instance.setNativeProps({style: newStyle});
} else if (isArray(style)) {
const lastLength = style.length - 1;
if (typeof style[lastLength] === 'object' && !isArray(style[lastLength])) {
agent.overrideValueAtPath({
type: 'props',
id,
rendererID,
path: ['style', lastLength, name],
value,
});
} else {
agent.overrideValueAtPath({
type: 'props',
id,
rendererID,
path: ['style'],
value: style.concat([newStyle]),
});
}
} else {
agent.overrideValueAtPath({
type: 'props',
id,
rendererID,
path: ['style'],
value: [style, newStyle],
});
}
agent.emit('hideNativeHighlight');
}