From ab84bb9bade32a2b2bbd9501e242ae8449d1cef0 Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Wed, 26 Jul 2023 03:22:38 -0700 Subject: [PATCH] Add visualiser controls + target mode Summary: Now that we have panels for tree visualiser separately we can have visualiser specific controls. There is a dedicated button for focus mode which should make that more discoverable and a better implementation of target mode which uses a slider. This has several benefits: 1. more discoverable 2. more obvious what is going on with the text prompts and a real slider control instead of mouse enter 3. there is no context menu getting in the way of the content Changelog: UIDebugger Add visualizer target mode feature for selecting views in the z stack easily Changelog: UIDebugger Add FocusMode button to visualiser toolbar Reviewed By: mweststrate Differential Revision: D47671658 fbshipit-source-id: 6f657f9d417280627457624660b934c9898cda58 --- .../components/visualizer/Visualization2D.tsx | 221 ++++++++++-------- .../visualizer/VisualizerControls.tsx | 139 +++++++++++ .../public/ui-debugger/plugin/uiActions.tsx | 2 +- 3 files changed, 267 insertions(+), 95 deletions(-) create mode 100644 desktop/plugins/public/ui-debugger/components/visualizer/VisualizerControls.tsx diff --git a/desktop/plugins/public/ui-debugger/components/visualizer/Visualization2D.tsx b/desktop/plugins/public/ui-debugger/components/visualizer/Visualization2D.tsx index f02e2180594..c5c5200b7c9 100644 --- a/desktop/plugins/public/ui-debugger/components/visualizer/Visualization2D.tsx +++ b/desktop/plugins/public/ui-debugger/components/visualizer/Visualization2D.tsx @@ -7,15 +7,23 @@ * @format */ -import React, {useEffect, useMemo, useRef} from 'react'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; import {Bounds, Coordinate, Id, ClientNode} from '../../ClientTypes'; import {NestedNode, OnSelectNode} from '../../DesktopTypes'; -import {produce, styled, theme, usePlugin, useValue} from 'flipper-plugin'; +import { + produce, + styled, + theme, + usePlugin, + useValue, + Layout, +} from 'flipper-plugin'; import {plugin} from '../../index'; import {head, isEqual, throttle} from 'lodash'; import {useDelay} from '../../hooks/useDelay'; import {Tooltip} from 'antd'; +import {TargetModeState, VisualiserControls} from './VisualizerControls'; export const Visualization2D: React.FC< { @@ -32,7 +40,12 @@ export const Visualization2D: React.FC< const focusedNodeId = useValue(instance.uiState.focusedNode); const selectedNodeId = useValue(instance.uiState.selectedNode); - const hoveredNodeId = head(useValue(instance.uiState.hoveredNodes)); + const hoveredNodes = useValue(instance.uiState.hoveredNodes); + const hoveredNodeId = head(hoveredNodes); + + const [targetMode, setTargetMode] = useState({ + state: 'disabled', + }); const focusState = useMemo(() => { //use the snapshot node as root since we cant realistically visualise any node above this const rootNode = snapshot && toNestedNode(snapshot.nodeId, nodes); @@ -111,84 +124,114 @@ export const Visualization2D: React.FC< const pxScaleFactor = calcPxScaleFactor(snapshotNode.bounds, width); + const overlayCursor = + targetMode.state === 'disabled' ? 'pointer' : 'crosshair'; + + const onClickOverlay = () => { + instance.uiActions.onSelectNode(hoveredNodeId, 'visualiser'); + if (targetMode.state !== 'disabled') { + setTargetMode({ + state: 'selected', + targetedNodes: hoveredNodes.slice().reverse(), + sliderPosition: hoveredNodes.length - 1, + }); + } + }; + return ( -
{ - e.stopPropagation(); - //the context menu triggers this callback but we dont want to remove hover effect - if (!instance.uiState.isContextMenuOpen.get()) { - instance.uiActions.onHoverNode(); - } - - visualizerActive.current = false; - }} - onMouseEnter={() => { - visualizerActive.current = true; - }} - //this div is to ensure that the size of the visualiser doesnt change when focusings on a subtree - style={ - { - backgroundColor: theme.backgroundWash, - borderRadius: theme.borderRadius, - overflowY: 'auto', - overflowX: 'hidden', - position: 'relative', //this is for the absolutely positioned overlays - [pxScaleFactorCssVar]: pxScaleFactor, - width: toPx(focusState.actualRoot.bounds.width), - height: toPx(focusState.actualRoot.bounds.height), - } as React.CSSProperties - }> - {hoveredNodeId && ( - - )} - {selectedNodeId && ( - - )} + + +
- {snapshotNode && ( - { + e.stopPropagation(); + //the context menu triggers this callback but we dont want to remove hover effect + if (!instance.uiState.isContextMenuOpen.get()) { + instance.uiActions.onHoverNode(); + } + + visualizerActive.current = false; + }} + onMouseEnter={() => { + visualizerActive.current = true; + }} + //this div is to ensure that the size of the visualiser doesnt change when focusings on a subtree + style={ + { + backgroundColor: theme.backgroundWash, + borderRadius: theme.borderRadius, + overflowY: 'auto', + overflowX: 'hidden', + position: 'relative', //this is for the absolutely positioned overlays + [pxScaleFactorCssVar]: pxScaleFactor, + width: toPx(focusState.actualRoot.bounds.width), + height: toPx(focusState.actualRoot.bounds.height), + } as React.CSSProperties + }> + {hoveredNodeId && ( + + + + )} + {selectedNodeId && ( + )} - +
+ {snapshotNode && ( + + )} + +
-
+ ); }; @@ -257,15 +300,11 @@ function Visualization2DNode({ ); } -function HoveredOverlay({ - nodeId, - nodes, - onSelectNode, -}: { +const DelayedHoveredToolTip: React.FC<{ nodeId: Id; nodes: Map; - onSelectNode: OnSelectNode; -}) { + children: JSX.Element; +}> = ({nodeId, nodes, children}) => { const node = nodes.get(nodeId); const isVisible = useDelay(longHoverDelay); @@ -281,29 +320,23 @@ function HoveredOverlay({ align={{ offset: [0, 7], }}> - { - onSelectNode(nodeId, 'visualiser'); - }} - nodeId={nodeId} - nodes={nodes} - type="hovered" - /> + {children} ); -} +}; const OverlayBorder = styled.div<{ + cursor: 'pointer' | 'crosshair'; type: 'selected' | 'hovered'; nodeId: Id; nodes: Map; -}>(({type, nodeId, nodes}) => { +}>(({type, nodeId, nodes, cursor}) => { const offset = getTotalOffset(nodeId, nodes); const node = nodes.get(nodeId); return { zIndex: 100, pointerEvents: type === 'selected' ? 'none' : 'auto', - cursor: 'pointer', + cursor: cursor, position: 'absolute', top: toPx(offset.y), left: toPx(offset.x), diff --git a/desktop/plugins/public/ui-debugger/components/visualizer/VisualizerControls.tsx b/desktop/plugins/public/ui-debugger/components/visualizer/VisualizerControls.tsx new file mode 100644 index 00000000000..a2aaba802c8 --- /dev/null +++ b/desktop/plugins/public/ui-debugger/components/visualizer/VisualizerControls.tsx @@ -0,0 +1,139 @@ +/** + * 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. + * + * @format + */ + +import {Button, Slider, Tooltip, Typography} from 'antd'; +import {Layout, produce, theme, usePlugin} from 'flipper-plugin'; +import {Id} from '../../ClientTypes'; +import {plugin} from '../../index'; +import React from 'react'; +import { + AimOutlined, + FullscreenExitOutlined, + FullscreenOutlined, +} from '@ant-design/icons'; + +export type TargetModeState = + | { + state: 'selected'; + targetedNodes: Id[]; + sliderPosition: number; + } + | { + state: 'active'; + } + | { + state: 'disabled'; + }; + +export function VisualiserControls({ + targetMode, + setTargetMode, + selectedNode, + focusedNode, +}: { + selectedNode?: Id; + focusedNode?: Id; + setTargetMode: (targetMode: TargetModeState) => void; + targetMode: TargetModeState; +}) { + const instance = usePlugin(plugin); + + const focusDisabled = focusedNode == null && selectedNode == null; + const focusToolTip = focusDisabled + ? 'Select a node to focus it' + : focusedNode == null + ? 'Focus current node' + : 'Remove focus'; + + const targetToolTip = + targetMode.state === 'disabled' ? 'TargetMode' : 'Exit target mode'; + + return ( + + + {targetMode.state === 'active' && ( + Target mode: Select element + )} + {targetMode.state === 'disabled' && ( + Interactive Visualizer + )} + {targetMode.state === 'selected' && ( + { + setTargetMode( + produce(targetMode, (draft) => { + draft.sliderPosition = value; + }), + ); + instance.uiActions.onSelectNode( + targetMode.targetedNodes[value], + 'visualiser', + ); + }} + /> + )} + + + + +