1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import Agent from 'react-devtools-shared/src/backend/agent';
    
  11. import resolveBoxStyle from './resolveBoxStyle';
    
  12. import isArray from 'react-devtools-shared/src/isArray';
    
  13. 
    
  14. import type {BackendBridge} from 'react-devtools-shared/src/bridge';
    
  15. import type {RendererID} from '../types';
    
  16. import type {StyleAndLayout} from './types';
    
  17. 
    
  18. export type ResolveNativeStyle = (stylesheetID: any) => ?Object;
    
  19. export type SetupNativeStyleEditor = typeof setupNativeStyleEditor;
    
  20. 
    
  21. export default function setupNativeStyleEditor(
    
  22.   bridge: BackendBridge,
    
  23.   agent: Agent,
    
  24.   resolveNativeStyle: ResolveNativeStyle,
    
  25.   validAttributes?: $ReadOnlyArray<string> | null,
    
  26. ) {
    
  27.   bridge.addListener(
    
  28.     'NativeStyleEditor_measure',
    
  29.     ({id, rendererID}: {id: number, rendererID: RendererID}) => {
    
  30.       measureStyle(agent, bridge, resolveNativeStyle, id, rendererID);
    
  31.     },
    
  32.   );
    
  33. 
    
  34.   bridge.addListener(
    
  35.     'NativeStyleEditor_renameAttribute',
    
  36.     ({
    
  37.       id,
    
  38.       rendererID,
    
  39.       oldName,
    
  40.       newName,
    
  41.       value,
    
  42.     }: {
    
  43.       id: number,
    
  44.       rendererID: RendererID,
    
  45.       oldName: string,
    
  46.       newName: string,
    
  47.       value: string,
    
  48.     }) => {
    
  49.       renameStyle(agent, id, rendererID, oldName, newName, value);
    
  50.       setTimeout(() =>
    
  51.         measureStyle(agent, bridge, resolveNativeStyle, id, rendererID),
    
  52.       );
    
  53.     },
    
  54.   );
    
  55. 
    
  56.   bridge.addListener(
    
  57.     'NativeStyleEditor_setValue',
    
  58.     ({
    
  59.       id,
    
  60.       rendererID,
    
  61.       name,
    
  62.       value,
    
  63.     }: {
    
  64.       id: number,
    
  65.       rendererID: number,
    
  66.       name: string,
    
  67.       value: string,
    
  68.     }) => {
    
  69.       setStyle(agent, id, rendererID, name, value);
    
  70.       setTimeout(() =>
    
  71.         measureStyle(agent, bridge, resolveNativeStyle, id, rendererID),
    
  72.       );
    
  73.     },
    
  74.   );
    
  75. 
    
  76.   bridge.send('isNativeStyleEditorSupported', {
    
  77.     isSupported: true,
    
  78.     validAttributes,
    
  79.   });
    
  80. }
    
  81. 
    
  82. const EMPTY_BOX_STYLE = {
    
  83.   top: 0,
    
  84.   left: 0,
    
  85.   right: 0,
    
  86.   bottom: 0,
    
  87. };
    
  88. 
    
  89. const componentIDToStyleOverrides: Map<number, Object> = new Map();
    
  90. 
    
  91. function measureStyle(
    
  92.   agent: Agent,
    
  93.   bridge: BackendBridge,
    
  94.   resolveNativeStyle: ResolveNativeStyle,
    
  95.   id: number,
    
  96.   rendererID: RendererID,
    
  97. ) {
    
  98.   const data = agent.getInstanceAndStyle({id, rendererID});
    
  99.   if (!data || !data.style) {
    
  100.     bridge.send(
    
  101.       'NativeStyleEditor_styleAndLayout',
    
  102.       ({
    
  103.         id,
    
  104.         layout: null,
    
  105.         style: null,
    
  106.       }: StyleAndLayout),
    
  107.     );
    
  108.     return;
    
  109.   }
    
  110. 
    
  111.   const {instance, style} = data;
    
  112. 
    
  113.   let resolvedStyle = resolveNativeStyle(style);
    
  114. 
    
  115.   // If it's a host component we edited before, amend styles.
    
  116.   const styleOverrides = componentIDToStyleOverrides.get(id);
    
  117.   if (styleOverrides != null) {
    
  118.     resolvedStyle = Object.assign({}, resolvedStyle, styleOverrides);
    
  119.   }
    
  120. 
    
  121.   if (!instance || typeof instance.measure !== 'function') {
    
  122.     bridge.send(
    
  123.       'NativeStyleEditor_styleAndLayout',
    
  124.       ({
    
  125.         id,
    
  126.         layout: null,
    
  127.         style: resolvedStyle || null,
    
  128.       }: StyleAndLayout),
    
  129.     );
    
  130.     return;
    
  131.   }
    
  132. 
    
  133.   instance.measure((x, y, width, height, left, top) => {
    
  134.     // RN Android sometimes returns undefined here. Don't send measurements in this case.
    
  135.     // https://github.com/jhen0409/react-native-debugger/issues/84#issuecomment-304611817
    
  136.     if (typeof x !== 'number') {
    
  137.       bridge.send(
    
  138.         'NativeStyleEditor_styleAndLayout',
    
  139.         ({
    
  140.           id,
    
  141.           layout: null,
    
  142.           style: resolvedStyle || null,
    
  143.         }: StyleAndLayout),
    
  144.       );
    
  145.       return;
    
  146.     }
    
  147.     const margin =
    
  148.       (resolvedStyle != null && resolveBoxStyle('margin', resolvedStyle)) ||
    
  149.       EMPTY_BOX_STYLE;
    
  150.     const padding =
    
  151.       (resolvedStyle != null && resolveBoxStyle('padding', resolvedStyle)) ||
    
  152.       EMPTY_BOX_STYLE;
    
  153.     bridge.send(
    
  154.       'NativeStyleEditor_styleAndLayout',
    
  155.       ({
    
  156.         id,
    
  157.         layout: {
    
  158.           x,
    
  159.           y,
    
  160.           width,
    
  161.           height,
    
  162.           left,
    
  163.           top,
    
  164.           margin,
    
  165.           padding,
    
  166.         },
    
  167.         style: resolvedStyle || null,
    
  168.       }: StyleAndLayout),
    
  169.     );
    
  170.   });
    
  171. }
    
  172. 
    
  173. function shallowClone(object: Object): Object {
    
  174.   const cloned: {[string]: $FlowFixMe} = {};
    
  175.   for (const n in object) {
    
  176.     cloned[n] = object[n];
    
  177.   }
    
  178.   return cloned;
    
  179. }
    
  180. 
    
  181. function renameStyle(
    
  182.   agent: Agent,
    
  183.   id: number,
    
  184.   rendererID: RendererID,
    
  185.   oldName: string,
    
  186.   newName: string,
    
  187.   value: string,
    
  188. ): void {
    
  189.   const data = agent.getInstanceAndStyle({id, rendererID});
    
  190.   if (!data || !data.style) {
    
  191.     return;
    
  192.   }
    
  193. 
    
  194.   const {instance, style} = data;
    
  195. 
    
  196.   const newStyle = newName
    
  197.     ? {[oldName]: undefined, [newName]: value}
    
  198.     : {[oldName]: undefined};
    
  199. 
    
  200.   let customStyle;
    
  201. 
    
  202.   // TODO It would be nice if the renderer interface abstracted this away somehow.
    
  203.   if (instance !== null && typeof instance.setNativeProps === 'function') {
    
  204.     // In the case of a host component, we need to use setNativeProps().
    
  205.     // Remember to "correct" resolved styles when we read them next time.
    
  206.     const styleOverrides = componentIDToStyleOverrides.get(id);
    
  207.     if (!styleOverrides) {
    
  208.       componentIDToStyleOverrides.set(id, newStyle);
    
  209.     } else {
    
  210.       Object.assign(styleOverrides, newStyle);
    
  211.     }
    
  212.     // TODO Fabric does not support setNativeProps; chat with Sebastian or Eli
    
  213.     instance.setNativeProps({style: newStyle});
    
  214.   } else if (isArray(style)) {
    
  215.     const lastIndex = style.length - 1;
    
  216.     if (typeof style[lastIndex] === 'object' && !isArray(style[lastIndex])) {
    
  217.       customStyle = shallowClone(style[lastIndex]);
    
  218.       delete customStyle[oldName];
    
  219.       if (newName) {
    
  220.         customStyle[newName] = value;
    
  221.       } else {
    
  222.         customStyle[oldName] = undefined;
    
  223.       }
    
  224. 
    
  225.       agent.overrideValueAtPath({
    
  226.         type: 'props',
    
  227.         id,
    
  228.         rendererID,
    
  229.         path: ['style', lastIndex],
    
  230.         value: customStyle,
    
  231.       });
    
  232.     } else {
    
  233.       agent.overrideValueAtPath({
    
  234.         type: 'props',
    
  235.         id,
    
  236.         rendererID,
    
  237.         path: ['style'],
    
  238.         value: style.concat([newStyle]),
    
  239.       });
    
  240.     }
    
  241.   } else if (typeof style === 'object') {
    
  242.     customStyle = shallowClone(style);
    
  243.     delete customStyle[oldName];
    
  244.     if (newName) {
    
  245.       customStyle[newName] = value;
    
  246.     } else {
    
  247.       customStyle[oldName] = undefined;
    
  248.     }
    
  249. 
    
  250.     agent.overrideValueAtPath({
    
  251.       type: 'props',
    
  252.       id,
    
  253.       rendererID,
    
  254.       path: ['style'],
    
  255.       value: customStyle,
    
  256.     });
    
  257.   } else {
    
  258.     agent.overrideValueAtPath({
    
  259.       type: 'props',
    
  260.       id,
    
  261.       rendererID,
    
  262.       path: ['style'],
    
  263.       value: [style, newStyle],
    
  264.     });
    
  265.   }
    
  266. 
    
  267.   agent.emit('hideNativeHighlight');
    
  268. }
    
  269. 
    
  270. function setStyle(
    
  271.   agent: Agent,
    
  272.   id: number,
    
  273.   rendererID: RendererID,
    
  274.   name: string,
    
  275.   value: string,
    
  276. ) {
    
  277.   const data = agent.getInstanceAndStyle({id, rendererID});
    
  278.   if (!data || !data.style) {
    
  279.     return;
    
  280.   }
    
  281. 
    
  282.   const {instance, style} = data;
    
  283.   const newStyle = {[name]: value};
    
  284. 
    
  285.   // TODO It would be nice if the renderer interface abstracted this away somehow.
    
  286.   if (instance !== null && typeof instance.setNativeProps === 'function') {
    
  287.     // In the case of a host component, we need to use setNativeProps().
    
  288.     // Remember to "correct" resolved styles when we read them next time.
    
  289.     const styleOverrides = componentIDToStyleOverrides.get(id);
    
  290.     if (!styleOverrides) {
    
  291.       componentIDToStyleOverrides.set(id, newStyle);
    
  292.     } else {
    
  293.       Object.assign(styleOverrides, newStyle);
    
  294.     }
    
  295.     // TODO Fabric does not support setNativeProps; chat with Sebastian or Eli
    
  296.     instance.setNativeProps({style: newStyle});
    
  297.   } else if (isArray(style)) {
    
  298.     const lastLength = style.length - 1;
    
  299.     if (typeof style[lastLength] === 'object' && !isArray(style[lastLength])) {
    
  300.       agent.overrideValueAtPath({
    
  301.         type: 'props',
    
  302.         id,
    
  303.         rendererID,
    
  304.         path: ['style', lastLength, name],
    
  305.         value,
    
  306.       });
    
  307.     } else {
    
  308.       agent.overrideValueAtPath({
    
  309.         type: 'props',
    
  310.         id,
    
  311.         rendererID,
    
  312.         path: ['style'],
    
  313.         value: style.concat([newStyle]),
    
  314.       });
    
  315.     }
    
  316.   } else {
    
  317.     agent.overrideValueAtPath({
    
  318.       type: 'props',
    
  319.       id,
    
  320.       rendererID,
    
  321.       path: ['style'],
    
  322.       value: [style, newStyle],
    
  323.     });
    
  324.   }
    
  325. 
    
  326.   agent.emit('hideNativeHighlight');
    
  327. }