From aaeff0ed46791fe7a9ca860f3ee220ae0d868368 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Thu, 27 Jul 2023 18:49:40 +0100 Subject: [PATCH] feat: Add optional "inline" mode for displaying nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This uses up less space, and reads nicely (more text-like) for `λ`/`∀`/`Λ` nodes. We can trigger this mode through the tree/text toggle. This is a slight abuse of this element, and we'll likely revisit this if/when we have more optional display behaviour. On the other hand, this display option is fairly experimental for now, and it may well be that we don't keep both tree styles around in the long run. Signed-off-by: George Thomas --- src/components/Edit/index.tsx | 24 +++- src/components/EvalFull/index.tsx | 10 +- src/components/SelectionInfo/index.tsx | 20 ++- src/components/Toolbar/index.tsx | 11 +- .../TreeReactFlow/TreeReactFlow.stories.tsx | 18 +++ src/components/TreeReactFlow/Types.ts | 1 + src/components/TreeReactFlow/index.tsx | 116 ++++++++++++------ 7 files changed, 147 insertions(+), 53 deletions(-) diff --git a/src/components/Edit/index.tsx b/src/components/Edit/index.tsx index 175f36f6..d9bbaea5 100644 --- a/src/components/Edit/index.tsx +++ b/src/components/Edit/index.tsx @@ -48,8 +48,10 @@ import { } from "@/primer-api"; import { defaultTreeReactFlowProps, + inlineTreeReactFlowProps, ScrollToDef, } from "@/components/TreeReactFlow"; +import { Mode } from "../Toolbar"; // hardcoded values (for now) const initialLevel: Level = "Expert"; @@ -198,6 +200,8 @@ const AppNoError = ({ undoAvailable: boolean; redoAvailable: boolean; }): JSX.Element => { + const initialMode = "tree 1"; + const [mode, setMode] = useState(initialMode); const [level, setLevel] = useState(initialLevel); const toggleLevel = (): void => { switch (level) { @@ -265,6 +269,17 @@ const AppNoError = ({ .sort((a, b) => cmpName(a.name, b.name)) .map((d) => d.name.baseName); + const treeProps = (() => { + switch (mode) { + case "text": + return defaultTreeReactFlowProps; + case "tree 1": + return defaultTreeReactFlowProps; + case "tree 2": + return inlineTreeReactFlowProps; + } + })(); + return (
@@ -272,7 +287,7 @@ const AppNoError = ({ sel && setSelection(sel)} defs={p.module.defs} @@ -286,9 +301,7 @@ const AppNoError = ({
{ - console.log("Toggle mode"); - }} + onModeChange={setMode} level={level} onLevelChange={toggleLevel} undoAvailable={p.undoAvailable} @@ -307,7 +320,7 @@ const AppNoError = ({ }) .then(p.setProg); }} - initialMode="tree" + initialMode={mode} />
@@ -324,6 +337,7 @@ const AppNoError = ({ defs={defs} initialEvalDef={evalTarget} typeOrKind={p.selectionTypeOrKind} + extraTreeProps={treeProps} />
diff --git a/src/components/EvalFull/index.tsx b/src/components/EvalFull/index.tsx index 1bcd429a..a8431a7c 100644 --- a/src/components/EvalFull/index.tsx +++ b/src/components/EvalFull/index.tsx @@ -2,7 +2,10 @@ import { useState } from "react"; import { NodeChange, ReactFlowProvider, useReactFlow } from "reactflow"; import { EvalFullResp, GlobalName, Level } from "@/primer-api"; import { SelectMenu, TreeReactFlowOne } from "@/components"; -import { defaultTreeReactFlowProps } from "../TreeReactFlow"; +import { + TreeReactFlowOneProps, + defaultTreeReactFlowProps, +} from "../TreeReactFlow"; export type EvalFullProps = { moduleName: string[]; @@ -13,12 +16,14 @@ export type EvalFullProps = { level: Level; defs: string[]; initialEvalDef: string | undefined; + extraTreeProps: Partial; }; const Evaluated = (p: { defName: GlobalName; evaluated?: EvalFullResp; level: Level; + extraTreeProps: Partial; }) => { const padding = 1.0; const { fitView } = useReactFlow(); @@ -34,6 +39,7 @@ const Evaluated = (p: { zoomBarProps={{ padding }} onNodesChange={onNodesChange} fitViewOptions={{ padding }} + {...p.extraTreeProps} /> ); }; @@ -47,6 +53,7 @@ export const EvalFull = ({ moduleName, level, initialEvalDef, + extraTreeProps, }: EvalFullProps): JSX.Element => { const [evalDef, setEvalDef0] = useState(initialEvalDef ?? disableEval); const setEvalDef = (e: string) => { @@ -73,6 +80,7 @@ export const EvalFull = ({ defName={{ qualifiedModule: moduleName, baseName: evalDef }} {...(evalFull.result ? { evaluated: evalFull.result } : {})} level={level} + extraTreeProps={extraTreeProps} />
diff --git a/src/components/SelectionInfo/index.tsx b/src/components/SelectionInfo/index.tsx index 153e8247..c63fd953 100644 --- a/src/components/SelectionInfo/index.tsx +++ b/src/components/SelectionInfo/index.tsx @@ -1,14 +1,22 @@ import { NodeChange, ReactFlowProvider, useReactFlow } from "reactflow"; import { Level, TypeOrKind } from "@/primer-api"; import { TreeReactFlowOne } from "@/components"; -import { defaultTreeReactFlowProps } from "../TreeReactFlow"; +import { + TreeReactFlowOneProps, + defaultTreeReactFlowProps, +} from "../TreeReactFlow"; export type SelectionInfoProps = { typeOrKind: TypeOrKind | undefined; level: Level; + extraTreeProps: Partial; }; -const TypeOrKindTree = (p: { typeOrKind: TypeOrKind; level: Level }) => { +const TypeOrKindTree = (p: { + typeOrKind: TypeOrKind; + level: Level; + extraTreeProps: Partial; +}) => { const padding = 1.0; const { fitView } = useReactFlow(); const onNodesChange = (_: NodeChange[]) => { @@ -23,6 +31,7 @@ const TypeOrKindTree = (p: { typeOrKind: TypeOrKind; level: Level }) => { zoomBarProps={{ padding }} onNodesChange={onNodesChange} fitViewOptions={{ padding }} + {...p.extraTreeProps} /> ); }; @@ -30,6 +39,7 @@ const TypeOrKindTree = (p: { typeOrKind: TypeOrKind; level: Level }) => { export const SelectionInfo = ({ typeOrKind, level, + extraTreeProps, }: SelectionInfoProps): JSX.Element => { return (
@@ -40,7 +50,11 @@ export const SelectionInfo = ({
- +
diff --git a/src/components/Toolbar/index.tsx b/src/components/Toolbar/index.tsx index f03bbd38..8adc3f1f 100644 --- a/src/components/Toolbar/index.tsx +++ b/src/components/Toolbar/index.tsx @@ -23,7 +23,7 @@ export type ToolbarProps = { undoAvailable: boolean; onClickUndo: MouseEventHandler; }; -export type Mode = "text" | "tree"; +export type Mode = "text" | "tree 1" | "tree 2"; const iconClasses = "stroke-[2] p-1"; const heavyIconClasses = "w-7 stroke-[3] p-1"; @@ -32,7 +32,8 @@ const modeSvg = (m: Mode) => { switch (m) { case "text": return ; - case "tree": + case "tree 1": + case "tree 2": return ; } }; @@ -40,8 +41,10 @@ const modeSvg = (m: Mode) => { const nextMode = (m: Mode): Mode => { switch (m) { case "text": - return "tree"; - case "tree": + return "tree 1"; + case "tree 1": + return "tree 2"; + case "tree 2": return "text"; } }; diff --git a/src/components/TreeReactFlow/TreeReactFlow.stories.tsx b/src/components/TreeReactFlow/TreeReactFlow.stories.tsx index 50ce0ef1..5d7eb12b 100644 --- a/src/components/TreeReactFlow/TreeReactFlow.stories.tsx +++ b/src/components/TreeReactFlow/TreeReactFlow.stories.tsx @@ -1,6 +1,7 @@ import { ComponentStory, ComponentMeta } from "@storybook/react"; import { defaultTreeReactFlowProps, + inlineTreeReactFlowProps, TreeReactFlow, TreeReactFlowProps, } from "./"; @@ -294,3 +295,20 @@ export const OddAndEvenMiscStyles: ComponentStory = ( contents: { def: def5.name, node: { nodeType: "BodyNode", meta: 5 } }, }, }); +export const OddAndEvenInline: ComponentStory = ( + args: TreeReactFlowProps +) => + treeSized({ + ...inlineTreeReactFlowProps, + ...args, + defs: oddEvenTrees.map(([baseName, term]) => ({ + name: { qualifiedModule: [], baseName }, + term, + type_: emptyTypeTree(baseName), + })), + typeDefs: [], + selection: { + tag: "SelectionDef", + contents: { def: def5.name, node: { nodeType: "BodyNode", meta: 5 } }, + }, + }); diff --git a/src/components/TreeReactFlow/Types.ts b/src/components/TreeReactFlow/Types.ts index 1f36074d..8729ea94 100644 --- a/src/components/TreeReactFlow/Types.ts +++ b/src/components/TreeReactFlow/Types.ts @@ -195,6 +195,7 @@ export type PrimerCommonNodeProps = { width: number; height: number; selected: boolean; + style: "inline" | "corner"; }; /** Our edge type. Much like `PrimerNode`, `PrimerEdge` extends ReactFlow's `Edge`. diff --git a/src/components/TreeReactFlow/index.tsx b/src/components/TreeReactFlow/index.tsx index b861e017..df0b7938 100644 --- a/src/components/TreeReactFlow/index.tsx +++ b/src/components/TreeReactFlow/index.tsx @@ -92,7 +92,9 @@ type ReactFlowParams = { }; /** These properties are needed to construct nodes, but are invariant across all nodes. */ +export type NodeStyle = "corner" | "inline"; type NodeParams = { + style: NodeStyle; nodeWidth: number; nodeHeight: number; boxPadding: number; @@ -123,6 +125,7 @@ export const defaultTreeReactFlowProps: Pick< TreeReactFlowProps, "treePadding" | "forestLayout" | "defParams" | "layout" | keyof NodeParams > = { + style: "corner", level: "Expert", forestLayout: "Horizontal", treePadding: 100, @@ -132,9 +135,14 @@ export const defaultTreeReactFlowProps: Pick< defParams: { nameNodeMultipliers: { width: 3, height: 2 } }, layout: { type: WasmLayoutType.Tidy, - margins: { child: 25, sibling: 18 }, + margins: { child: 15, sibling: 12 }, }, }; +export const inlineTreeReactFlowProps: typeof defaultTreeReactFlowProps = { + ...defaultTreeReactFlowProps, + style: "inline", + nodeWidth: 100, +}; // These should probably take a `GlobalName` instead, but we're not // quite there yet. @@ -146,47 +154,70 @@ const handle = (type: HandleType, position: Position) => ( ); const nodeTypes = { - primer: ({ data }: { data: PrimerNodeProps & PrimerCommonNodeProps }) => ( - <> - {handle("target", Position.Top)} - {handle("target", Position.Left)} -
-
- {data.contents} -
+ primer: ({ data }: { data: PrimerNodeProps & PrimerCommonNodeProps }) => { + const classes = (() => { + switch (data.style) { + case "corner": + return { + root: classNames( + { + "ring-4 ring-offset-4": data.selected, + "hover:ring-opacity-50": !data.selected, + }, + "flex items-center justify-center border-4 text-grey-tertiary", + flavorClasses(data.flavor) + ), + label: classNames( + "z-20 p-1 absolute rounded-full text-sm xl:text-base", + data.syntax ? "-top-4" : "-right-2 -top-4", + flavorLabelClasses(data.flavor) + ), + contents: classNames( + "block truncate px-1 font-code text-sm xl:text-base", + flavorContentClasses(data.flavor) + ), + }; + case "inline": + return { + root: classNames( + { + "ring-4 ring-offset-4": data.selected, + "hover:ring-opacity-50": !data.selected, + }, + "grid grid-cols-[2rem_auto] border-4 overflow-hidden text-grey-tertiary", + flavorClasses(data.flavor) + ), + label: classNames( + "flex items-center justify-center pr-1 text-sm xl:text-base", + flavorLabelClasses(data.flavor) + ), + contents: classNames( + "flex items-center truncate justify-self-center pr-1 font-code text-sm xl:text-base", + flavorContentClasses(data.flavor) + ), + }; + } + })(); + return ( + <> + {handle("target", Position.Top)} + {handle("target", Position.Left)}
- {flavorLabel(data.flavor)} +
{flavorLabel(data.flavor)}
+
{data.contents}
-
- {handle("source", Position.Bottom)} - {handle("source", Position.Right)} - - ), + {handle("source", Position.Bottom)} + {handle("source", Position.Right)} + + ); + }, "primer-simple": ({ data, }: { @@ -518,6 +549,7 @@ const makePrimerNode = async ( height: p.nodeHeight, selected, nodeData, + style: p.style, }; const edgeCommon = ( child: PrimerNode, @@ -604,7 +636,7 @@ const makePrimerNode = async ( ...common, // TODO This is necessary to ensure that all syntax labels fit. // It can be removed when we have dynamic node sizes. - width: 130, + width: 150, }, zIndex, }, @@ -773,6 +805,7 @@ const defToTree = async ( const defNameNode: PrimerNodeWithNestedAndDef = { id: defNodeId, data: { + style: p.style, def: def.name, width: p.nodeWidth * p.nameNodeMultipliers.width, height: p.nodeHeight * p.nameNodeMultipliers.height, @@ -850,6 +883,7 @@ const typeDefToTree = async ( id, type: "primer-typedef-param", data: { + style: p.style, def: def.name, width: p.nodeWidth, height: p.nodeHeight, @@ -931,6 +965,7 @@ const typeDefToTree = async ( id: consId, type: "primer-typedef-cons", data: { + style: p.style, def: def.name, name: cons.name, width: p.nodeWidth, @@ -967,6 +1002,7 @@ const typeDefToTree = async ( id: rootId, type: "primer-typedef-name", data: { + style: p.style, def: def.name, name: def.name, height: p.nodeHeight * p.nameNodeMultipliers.height,