mirror of
https://github.com/roc-lang/roc.git
synced 2025-08-03 19:58:18 +00:00
Keydown and layouter improvements
This commit is contained in:
parent
90d7d87e8c
commit
734111711d
6 changed files with 198 additions and 55 deletions
6
crates/compiler/checkmate/www/package-lock.json
generated
6
crates/compiler/checkmate/www/package-lock.json
generated
|
@ -9,6 +9,7 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.0.2",
|
||||
"elkjs": "^0.8.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"reactflow": "^11.7.4"
|
||||
|
@ -7519,6 +7520,11 @@
|
|||
"integrity": "sha512-1JkvV2sgEGTDXjdsaQCeSwYYuhLRphRpc+g6EHTFELJXEiznLt3/0pZ9JuAOQ5p2rI3YxKTbivtvajirIfhrEQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/elkjs": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz",
|
||||
"integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ=="
|
||||
},
|
||||
"node_modules/emittery": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.0.2",
|
||||
"elkjs": "^0.8.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"reactflow": "^11.7.4"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Variable } from "../../schema";
|
||||
|
||||
export type ToggleVariableHandler = (variable: Variable) => void;
|
||||
export type KeydownHandler = (key: string) => void;
|
||||
export type KeydownHandler = (key: string) => Promise<void>;
|
||||
|
|
|
@ -14,9 +14,15 @@ export interface VariableNodeProps {
|
|||
variable: Variable;
|
||||
addSubVariableLink: AddSubVariableLink;
|
||||
};
|
||||
targetPosition?: Position;
|
||||
sourcePosition?: Position;
|
||||
}
|
||||
|
||||
export default function VariableNode({ data }: VariableNodeProps): JSX.Element {
|
||||
export default function VariableNode({
|
||||
data,
|
||||
targetPosition,
|
||||
sourcePosition,
|
||||
}: VariableNodeProps): JSX.Element {
|
||||
const { variable, subs, addSubVariableLink } = data;
|
||||
|
||||
const desc = subs.get_root(variable);
|
||||
|
@ -52,12 +58,20 @@ export default function VariableNode({ data }: VariableNodeProps): JSX.Element {
|
|||
"text-center font-mono"
|
||||
)}
|
||||
>
|
||||
<Handle type="target" position={Position.Top} isConnectable={false} />
|
||||
<Handle
|
||||
type="target"
|
||||
position={targetPosition ?? Position.Top}
|
||||
isConnectable={false}
|
||||
/>
|
||||
<div>
|
||||
<VariableElPretty variable={variable} subs={subs} />
|
||||
</div>
|
||||
{expandedContent}
|
||||
<Handle type="source" position={Position.Bottom} isConnectable={false} />
|
||||
<Handle
|
||||
type="source"
|
||||
position={sourcePosition ?? Position.Bottom}
|
||||
isConnectable={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import Dagre from "@dagrejs/dagre";
|
||||
import ELK, {
|
||||
type ElkNode,
|
||||
type LayoutOptions,
|
||||
} from "elkjs/lib/elk.bundled.js";
|
||||
import ReactFlow, {
|
||||
Node,
|
||||
Edge,
|
||||
|
@ -14,6 +17,7 @@ import ReactFlow, {
|
|||
NodeTypes,
|
||||
useStore,
|
||||
ReactFlowState,
|
||||
Position,
|
||||
} from "reactflow";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Variable } from "../../schema";
|
||||
|
@ -30,9 +34,41 @@ export interface VariablesGraphProps {
|
|||
onKeydown: (handler: KeydownHandler) => void;
|
||||
}
|
||||
|
||||
type GraphDirection = "TB" | "BT" | "LR" | "RL";
|
||||
enum GraphDirection {
|
||||
LeftRight,
|
||||
TopBottom,
|
||||
}
|
||||
|
||||
const DEFAULT_DIRECTION: GraphDirection = "TB";
|
||||
const DEFAULT_DIRECTION: GraphDirection = GraphDirection.TopBottom;
|
||||
|
||||
function directionToElkDirection(direction: GraphDirection): string {
|
||||
switch (direction) {
|
||||
case GraphDirection.TopBottom:
|
||||
return "DOWN";
|
||||
case GraphDirection.LeftRight:
|
||||
return "RIGHT";
|
||||
}
|
||||
}
|
||||
|
||||
function horizontalDirectionality(direction: GraphDirection): boolean {
|
||||
switch (direction) {
|
||||
case GraphDirection.TopBottom:
|
||||
return false;
|
||||
case GraphDirection.LeftRight:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function directionalityToPositions(direction: GraphDirection): {
|
||||
targetPosition: Position;
|
||||
sourcePosition: Position;
|
||||
} {
|
||||
const isHorizontal = horizontalDirectionality(direction);
|
||||
return {
|
||||
targetPosition: isHorizontal ? Position.Left : Position.Top,
|
||||
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
|
||||
};
|
||||
}
|
||||
|
||||
interface LayoutedElements {
|
||||
nodes: Node[];
|
||||
|
@ -43,47 +79,88 @@ interface ComputeLayoutedElementsProps extends LayoutedElements {
|
|||
direction: GraphDirection;
|
||||
}
|
||||
|
||||
function computeLayoutedElements({
|
||||
// Elk has a *huge* amount of options to configure. To see everything you can
|
||||
// tweak check out:
|
||||
//
|
||||
// - https://www.eclipse.org/elk/reference/algorithms.html
|
||||
// - https://www.eclipse.org/elk/reference/options.html
|
||||
const elkOptions: LayoutOptions = {
|
||||
"elk.algorithm": "layered",
|
||||
"elk.layered.spacing.nodeNodeBetweenLayers": "100",
|
||||
"elk.spacing.nodeNode": "80",
|
||||
};
|
||||
|
||||
async function computeLayoutedElements({
|
||||
nodes,
|
||||
edges,
|
||||
direction,
|
||||
}: ComputeLayoutedElementsProps): LayoutedElements {
|
||||
}: ComputeLayoutedElementsProps): Promise<LayoutedElements> {
|
||||
if (nodes.length === 0) {
|
||||
return {
|
||||
return Promise.resolve({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||
g.setGraph({ rankdir: direction });
|
||||
const isHorizontal = horizontalDirectionality(direction);
|
||||
|
||||
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
|
||||
nodes.forEach((node) => g.setNode(node.id, node));
|
||||
const elk = new ELK();
|
||||
const graph: ElkNode = {
|
||||
id: "root",
|
||||
layoutOptions: {
|
||||
...elkOptions,
|
||||
"elk.direction": directionToElkDirection(direction),
|
||||
},
|
||||
//@ts-ignore
|
||||
children: nodes.map((node) => ({
|
||||
...node,
|
||||
// Adjust the target and source handle positions based on the layout
|
||||
// direction.
|
||||
targetPosition: isHorizontal ? "left" : "top",
|
||||
sourcePosition: isHorizontal ? "right" : "bottom",
|
||||
|
||||
Dagre.layout(g);
|
||||
|
||||
const result = {
|
||||
nodes: nodes.map((node) => {
|
||||
const { x, y } = g.node(node.id);
|
||||
|
||||
return { ...node, position: { x, y } };
|
||||
}),
|
||||
edges,
|
||||
// Hardcode a width and height for elk to use when layouting.
|
||||
//width: 150,
|
||||
//height: 50,
|
||||
})),
|
||||
//@ts-ignore
|
||||
edges: edges,
|
||||
};
|
||||
|
||||
const layoutedGraph = await elk.layout(graph);
|
||||
|
||||
if (!layoutedGraph.children || !layoutedGraph.edges) {
|
||||
throw new Error("Elk did not return a valid graph");
|
||||
}
|
||||
|
||||
return {
|
||||
//@ts-ignore
|
||||
nodes: layoutedGraph.children.map((node) => ({
|
||||
...node,
|
||||
// React Flow expects a position property on the node instead of `x`
|
||||
// and `y` fields.
|
||||
position: { x: node.x, y: node.y },
|
||||
})),
|
||||
//@ts-ignore
|
||||
edges: layoutedGraph.edges,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
const NODE_TYPES: NodeTypes = {
|
||||
variable: VariableNode,
|
||||
};
|
||||
|
||||
function newVariable(id: string, data: VariableNodeProps["data"]): Node {
|
||||
function newVariable(
|
||||
id: string,
|
||||
data: VariableNodeProps["data"],
|
||||
direction: GraphDirection
|
||||
): Node {
|
||||
return {
|
||||
id,
|
||||
position: { x: 0, y: 0 },
|
||||
type: "variable",
|
||||
data,
|
||||
...directionalityToPositions(direction),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -121,18 +198,22 @@ const nodesSetInViewSelector = (state: ReactFlowState) =>
|
|||
(node) => node.width && node.height
|
||||
);
|
||||
|
||||
// Does positioning of the nodes in the graph.
|
||||
function useRedoLayout({ direction }: { direction: GraphDirection }) {
|
||||
type RedoLayoutFn = () => Promise<void>;
|
||||
function useRedoLayout({
|
||||
direction,
|
||||
}: {
|
||||
direction: GraphDirection;
|
||||
}): RedoLayoutFn {
|
||||
const nodeCount = useStore(nodeCountSelector);
|
||||
const nodesInitialized = useStore(nodesSetInViewSelector);
|
||||
const { getNodes, setNodes, getEdges } = useReactFlow();
|
||||
const instance = useReactFlow();
|
||||
|
||||
return useCallback(() => {
|
||||
return useCallback(async () => {
|
||||
if (!nodeCount || !nodesInitialized) {
|
||||
return;
|
||||
}
|
||||
const { nodes } = computeLayoutedElements({
|
||||
const { nodes } = await computeLayoutedElements({
|
||||
nodes: getNodes(),
|
||||
edges: getEdges(),
|
||||
direction,
|
||||
|
@ -157,10 +238,50 @@ function useAutoLayout({ direction }: { direction: GraphDirection }) {
|
|||
const redoLayout = useRedoLayout({ direction });
|
||||
|
||||
useEffect(() => {
|
||||
redoLayout();
|
||||
// This wrapping is of course redundant, but exercised for the purpose of
|
||||
// explicitness.
|
||||
async function inner() {
|
||||
await redoLayout();
|
||||
}
|
||||
inner();
|
||||
}, [direction, redoLayout]);
|
||||
}
|
||||
|
||||
function useKeydown({
|
||||
direction,
|
||||
onKeydown,
|
||||
setDirection,
|
||||
}: {
|
||||
direction: GraphDirection;
|
||||
setDirection: React.Dispatch<React.SetStateAction<GraphDirection>>;
|
||||
onKeydown: (handler: KeydownHandler) => void;
|
||||
}) {
|
||||
const redoLayout = useRedoLayout({ direction });
|
||||
|
||||
const keyDownHandler = useCallback(
|
||||
async (key: string) => {
|
||||
switch (key) {
|
||||
case "c": {
|
||||
await redoLayout();
|
||||
break;
|
||||
}
|
||||
case "j": {
|
||||
setDirection(GraphDirection.TopBottom);
|
||||
break;
|
||||
}
|
||||
case "l": {
|
||||
setDirection(GraphDirection.LeftRight);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[redoLayout, setDirection]
|
||||
);
|
||||
onKeydown(async (key) => {
|
||||
await keyDownHandler(key);
|
||||
});
|
||||
}
|
||||
|
||||
function Graph({
|
||||
subs,
|
||||
onVariable,
|
||||
|
@ -175,8 +296,8 @@ function Graph({
|
|||
edges: initialEdges,
|
||||
});
|
||||
|
||||
const redoLayout = useRedoLayout({ direction });
|
||||
useAutoLayout({ direction });
|
||||
useKeydown({ direction, onKeydown, setDirection });
|
||||
|
||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||
setElements(({ nodes, edges }) => {
|
||||
|
@ -205,11 +326,15 @@ function Graph({
|
|||
|
||||
setElements(({ nodes, edges }) => {
|
||||
const optNewNode = addNodeChange(
|
||||
newVariable(to, {
|
||||
subs,
|
||||
variable: subLinkN,
|
||||
addSubVariableLink,
|
||||
}),
|
||||
newVariable(
|
||||
to,
|
||||
{
|
||||
subs,
|
||||
variable: subLinkN,
|
||||
addSubVariableLink,
|
||||
},
|
||||
direction
|
||||
),
|
||||
nodes
|
||||
);
|
||||
const newNodes = optNewNode
|
||||
|
@ -227,7 +352,7 @@ function Graph({
|
|||
return { nodes: newNodes, edges: newEdges };
|
||||
});
|
||||
},
|
||||
[subs]
|
||||
[direction, subs]
|
||||
);
|
||||
|
||||
const addNode = useCallback(
|
||||
|
@ -237,11 +362,15 @@ function Graph({
|
|||
|
||||
setElements(({ nodes, edges }) => {
|
||||
const optNewNode = addNodeChange(
|
||||
newVariable(variable, {
|
||||
subs,
|
||||
variable: variableN,
|
||||
addSubVariableLink,
|
||||
}),
|
||||
newVariable(
|
||||
variable,
|
||||
{
|
||||
subs,
|
||||
variable: variableN,
|
||||
addSubVariableLink,
|
||||
},
|
||||
direction
|
||||
),
|
||||
nodes
|
||||
);
|
||||
const newNodes = optNewNode
|
||||
|
@ -251,17 +380,10 @@ function Graph({
|
|||
return { nodes: newNodes, edges: edges };
|
||||
});
|
||||
},
|
||||
[subs, addSubVariableLink]
|
||||
[subs, addSubVariableLink, direction]
|
||||
);
|
||||
|
||||
onVariable(addNode);
|
||||
onKeydown((key) => {
|
||||
switch (key) {
|
||||
case "c": {
|
||||
redoLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
|
@ -302,8 +424,8 @@ function DirectionPanel({
|
|||
const commonStyle = "rounded cursor-pointer text-2xl select-none";
|
||||
|
||||
const dirs: { dir: GraphDirection; text: string }[] = [
|
||||
{ dir: "TB", text: "⬇️" },
|
||||
{ dir: "LR", text: "➡️" },
|
||||
{ dir: GraphDirection.TopBottom, text: "⬇️" },
|
||||
{ dir: GraphDirection.LeftRight, text: "➡️" },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
@ -24,8 +24,8 @@ export default function Ui({ events }: UiProps): JSX.Element {
|
|||
ee.on("toggleVariable", (variable: Variable) => {
|
||||
toggleVariableHandlers.forEach((handler) => handler(variable));
|
||||
});
|
||||
ee.on("keydown", (key: string) => {
|
||||
keydownHandlers.forEach((handler) => handler(key));
|
||||
ee.on("keydown", async (key: string) => {
|
||||
await Promise.all(keydownHandlers.map((handler) => handler(key)));
|
||||
});
|
||||
|
||||
engine.stepTo(engine.lastEventIndex());
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue