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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
8adf153380
commit
ab84bb9bad
@@ -7,15 +7,23 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useEffect, useMemo, useRef} from 'react';
|
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
||||||
import {Bounds, Coordinate, Id, ClientNode} from '../../ClientTypes';
|
import {Bounds, Coordinate, Id, ClientNode} from '../../ClientTypes';
|
||||||
import {NestedNode, OnSelectNode} from '../../DesktopTypes';
|
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 {plugin} from '../../index';
|
||||||
import {head, isEqual, throttle} from 'lodash';
|
import {head, isEqual, throttle} from 'lodash';
|
||||||
import {useDelay} from '../../hooks/useDelay';
|
import {useDelay} from '../../hooks/useDelay';
|
||||||
import {Tooltip} from 'antd';
|
import {Tooltip} from 'antd';
|
||||||
|
import {TargetModeState, VisualiserControls} from './VisualizerControls';
|
||||||
|
|
||||||
export const Visualization2D: React.FC<
|
export const Visualization2D: React.FC<
|
||||||
{
|
{
|
||||||
@@ -32,7 +40,12 @@ export const Visualization2D: React.FC<
|
|||||||
const focusedNodeId = useValue(instance.uiState.focusedNode);
|
const focusedNodeId = useValue(instance.uiState.focusedNode);
|
||||||
|
|
||||||
const selectedNodeId = useValue(instance.uiState.selectedNode);
|
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<TargetModeState>({
|
||||||
|
state: 'disabled',
|
||||||
|
});
|
||||||
const focusState = useMemo(() => {
|
const focusState = useMemo(() => {
|
||||||
//use the snapshot node as root since we cant realistically visualise any node above this
|
//use the snapshot node as root since we cant realistically visualise any node above this
|
||||||
const rootNode = snapshot && toNestedNode(snapshot.nodeId, nodes);
|
const rootNode = snapshot && toNestedNode(snapshot.nodeId, nodes);
|
||||||
@@ -111,7 +124,29 @@ export const Visualization2D: React.FC<
|
|||||||
|
|
||||||
const pxScaleFactor = calcPxScaleFactor(snapshotNode.bounds, width);
|
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 (
|
return (
|
||||||
|
<Layout.Container>
|
||||||
|
<VisualiserControls
|
||||||
|
focusedNode={focusedNodeId}
|
||||||
|
selectedNode={selectedNodeId?.id}
|
||||||
|
setTargetMode={setTargetMode}
|
||||||
|
targetMode={targetMode}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -139,15 +174,22 @@ export const Visualization2D: React.FC<
|
|||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}>
|
}>
|
||||||
{hoveredNodeId && (
|
{hoveredNodeId && (
|
||||||
<HoveredOverlay
|
<DelayedHoveredToolTip
|
||||||
onSelectNode={instance.uiActions.onSelectNode}
|
|
||||||
key={hoveredNodeId}
|
key={hoveredNodeId}
|
||||||
nodeId={hoveredNodeId}
|
nodeId={hoveredNodeId}
|
||||||
|
nodes={nodes}>
|
||||||
|
<OverlayBorder
|
||||||
|
cursor={overlayCursor}
|
||||||
|
onClick={onClickOverlay}
|
||||||
|
nodeId={hoveredNodeId}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
|
type="hovered"
|
||||||
/>
|
/>
|
||||||
|
</DelayedHoveredToolTip>
|
||||||
)}
|
)}
|
||||||
{selectedNodeId && (
|
{selectedNodeId && (
|
||||||
<OverlayBorder
|
<OverlayBorder
|
||||||
|
cursor={overlayCursor}
|
||||||
type="selected"
|
type="selected"
|
||||||
nodeId={selectedNodeId.id}
|
nodeId={selectedNodeId.id}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -189,6 +231,7 @@ export const Visualization2D: React.FC<
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Layout.Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -257,15 +300,11 @@ function Visualization2DNode({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HoveredOverlay({
|
const DelayedHoveredToolTip: React.FC<{
|
||||||
nodeId,
|
|
||||||
nodes,
|
|
||||||
onSelectNode,
|
|
||||||
}: {
|
|
||||||
nodeId: Id;
|
nodeId: Id;
|
||||||
nodes: Map<Id, ClientNode>;
|
nodes: Map<Id, ClientNode>;
|
||||||
onSelectNode: OnSelectNode;
|
children: JSX.Element;
|
||||||
}) {
|
}> = ({nodeId, nodes, children}) => {
|
||||||
const node = nodes.get(nodeId);
|
const node = nodes.get(nodeId);
|
||||||
|
|
||||||
const isVisible = useDelay(longHoverDelay);
|
const isVisible = useDelay(longHoverDelay);
|
||||||
@@ -281,29 +320,23 @@ function HoveredOverlay({
|
|||||||
align={{
|
align={{
|
||||||
offset: [0, 7],
|
offset: [0, 7],
|
||||||
}}>
|
}}>
|
||||||
<OverlayBorder
|
{children}
|
||||||
onClick={() => {
|
|
||||||
onSelectNode(nodeId, 'visualiser');
|
|
||||||
}}
|
|
||||||
nodeId={nodeId}
|
|
||||||
nodes={nodes}
|
|
||||||
type="hovered"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const OverlayBorder = styled.div<{
|
const OverlayBorder = styled.div<{
|
||||||
|
cursor: 'pointer' | 'crosshair';
|
||||||
type: 'selected' | 'hovered';
|
type: 'selected' | 'hovered';
|
||||||
nodeId: Id;
|
nodeId: Id;
|
||||||
nodes: Map<Id, ClientNode>;
|
nodes: Map<Id, ClientNode>;
|
||||||
}>(({type, nodeId, nodes}) => {
|
}>(({type, nodeId, nodes, cursor}) => {
|
||||||
const offset = getTotalOffset(nodeId, nodes);
|
const offset = getTotalOffset(nodeId, nodes);
|
||||||
const node = nodes.get(nodeId);
|
const node = nodes.get(nodeId);
|
||||||
return {
|
return {
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
pointerEvents: type === 'selected' ? 'none' : 'auto',
|
pointerEvents: type === 'selected' ? 'none' : 'auto',
|
||||||
cursor: 'pointer',
|
cursor: cursor,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: toPx(offset.y),
|
top: toPx(offset.y),
|
||||||
left: toPx(offset.x),
|
left: toPx(offset.x),
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Layout.Right style={{padding: theme.space.medium}} gap="medium" center>
|
||||||
|
<Layout.Container style={{userSelect: 'none'}}>
|
||||||
|
{targetMode.state === 'active' && (
|
||||||
|
<Typography.Text strong>Target mode: Select element</Typography.Text>
|
||||||
|
)}
|
||||||
|
{targetMode.state === 'disabled' && (
|
||||||
|
<Typography.Text strong>Interactive Visualizer</Typography.Text>
|
||||||
|
)}
|
||||||
|
{targetMode.state === 'selected' && (
|
||||||
|
<Slider
|
||||||
|
min={0}
|
||||||
|
tooltipVisible={false}
|
||||||
|
value={targetMode.sliderPosition}
|
||||||
|
max={targetMode.targetedNodes.length - 1}
|
||||||
|
onChange={(value) => {
|
||||||
|
setTargetMode(
|
||||||
|
produce(targetMode, (draft) => {
|
||||||
|
draft.sliderPosition = value;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
instance.uiActions.onSelectNode(
|
||||||
|
targetMode.targetedNodes[value],
|
||||||
|
'visualiser',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Layout.Container>
|
||||||
|
|
||||||
|
<Layout.Horizontal gap="medium">
|
||||||
|
<Tooltip title={targetToolTip}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (targetMode.state === 'disabled') {
|
||||||
|
setTargetMode({state: 'active'});
|
||||||
|
} else {
|
||||||
|
setTargetMode({state: 'disabled'});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
icon={
|
||||||
|
<AimOutlined
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
targetMode.state === 'disabled'
|
||||||
|
? theme.black
|
||||||
|
: theme.primaryColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={focusToolTip}>
|
||||||
|
<Button
|
||||||
|
disabled={focusDisabled}
|
||||||
|
onClick={() => {
|
||||||
|
if (focusedNode == null) {
|
||||||
|
instance.uiActions.onFocusNode(selectedNode);
|
||||||
|
} else {
|
||||||
|
instance.uiActions.onFocusNode();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
icon={
|
||||||
|
focusedNode == null ? (
|
||||||
|
<FullscreenExitOutlined
|
||||||
|
style={{
|
||||||
|
color: theme.black,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FullscreenOutlined
|
||||||
|
style={{
|
||||||
|
color: theme.primaryColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Layout.Right>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -90,7 +90,7 @@ export function uiActions(
|
|||||||
tracker.track('node-focused', {name: focusedNode.name, tags});
|
tracker.track('node-focused', {name: focusedNode.name, tags});
|
||||||
}
|
}
|
||||||
|
|
||||||
uiState.selectedNode.set(undefined);
|
uiState.selectedNode.set({id: node, source: 'visualiser'});
|
||||||
}
|
}
|
||||||
|
|
||||||
uiState.focusedNode.set(node);
|
uiState.focusedNode.set(node);
|
||||||
|
|||||||
Reference in New Issue
Block a user