mirror of
https://github.com/roc-lang/roc.git
synced 2025-08-03 03:42:17 +00:00
Merge pull request #5728 from roc-lang/checkmate-ui-improvements
More checkmate improvements
This commit is contained in:
commit
070cb6860d
22 changed files with 1085 additions and 508 deletions
41
crates/compiler/checkmate/www/package-lock.json
generated
41
crates/compiler/checkmate/www/package-lock.json
generated
|
@ -13,6 +13,7 @@
|
|||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-tooltip": "^5.19.0",
|
||||
"reactflow": "^11.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -2604,6 +2605,28 @@
|
|||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz",
|
||||
"integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.1.tgz",
|
||||
"integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.4.1",
|
||||
"@floating-ui/utils": "^0.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz",
|
||||
"integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw=="
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
|
||||
|
@ -6286,6 +6309,11 @@
|
|||
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz",
|
||||
"integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g=="
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
|
||||
},
|
||||
"node_modules/clean-css": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz",
|
||||
|
@ -15894,6 +15922,19 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-tooltip": {
|
||||
"version": "5.19.0",
|
||||
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.19.0.tgz",
|
||||
"integrity": "sha512-NSUk77GMpxYKHFKJVNHL++QQXRuH2QW1qDrXPtJnp2s/MJvUnU73N5TTADwDyrO2+xGlr0xHhjvQphkF60cMEA==",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"classnames": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.14.0",
|
||||
"react-dom": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reactflow": {
|
||||
"version": "11.7.4",
|
||||
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.7.4.tgz",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-tooltip": "^5.19.0",
|
||||
"reactflow": "^11.7.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
import React from "react";
|
||||
import FileInput, { LoadedEvents } from "./components/FileInput";
|
||||
import Ui from "./components/Ui";
|
||||
import data from "./checkmate.json";
|
||||
import { AllEvents } from "./schema";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
export default function App() {
|
||||
const [events, setEvents] = React.useState<LoadedEvents | null>({
|
||||
kind: "ok",
|
||||
events: data as AllEvents,
|
||||
});
|
||||
const [events, setEvents] = React.useState<LoadedEvents | null>(null);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
|
|
|
@ -1,60 +1,28 @@
|
|||
import clsx from "clsx";
|
||||
import { EventEpoch } from "../../engine/engine";
|
||||
import { HashLink } from "react-router-hash-link";
|
||||
|
||||
export enum EpochCellView {
|
||||
Events,
|
||||
Graph,
|
||||
}
|
||||
|
||||
function invert(cell: EpochCellView): EpochCellView {
|
||||
if (cell === EpochCellView.Events) {
|
||||
return EpochCellView.Graph;
|
||||
}
|
||||
return EpochCellView.Events;
|
||||
}
|
||||
|
||||
function asStr(cell: EpochCellView): string {
|
||||
switch (cell) {
|
||||
case EpochCellView.Events:
|
||||
return "events";
|
||||
case EpochCellView.Graph:
|
||||
return "graph";
|
||||
}
|
||||
}
|
||||
|
||||
interface EpochCellProps {
|
||||
view: EpochCellView;
|
||||
epoch: EventEpoch;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
focus?: boolean;
|
||||
}
|
||||
|
||||
const EPOCH_STYLES_ARRAY = [
|
||||
"text-slate-900",
|
||||
"font-mono",
|
||||
"bg-slate-200",
|
||||
"p-1",
|
||||
"py-0",
|
||||
"rounded-sm",
|
||||
"ring-1",
|
||||
"ring-slate-500",
|
||||
"text-sm",
|
||||
];
|
||||
|
||||
export const EPOCH_STYLES = clsx(...EPOCH_STYLES_ARRAY);
|
||||
|
||||
export default function EpochCell({ epoch, className, view }: EpochCellProps) {
|
||||
const invertedView = invert(view);
|
||||
export const EPOCH_STYLES =
|
||||
"text-slate-900 font-mono bg-slate-200 p-1 py-0 rounded-sm text-sm transition ease-in-out duration-700 mr-2";
|
||||
|
||||
export default function EpochCell({
|
||||
className,
|
||||
children,
|
||||
focus,
|
||||
}: EpochCellProps) {
|
||||
return (
|
||||
<HashLink smooth to={`#${asStr(invertedView)}-${epoch}`}>
|
||||
<div
|
||||
id={`${asStr(view)}-${epoch}`}
|
||||
className={clsx(EPOCH_STYLES, className)}
|
||||
>
|
||||
{view === EpochCellView.Graph ? "Epoch " : ""}
|
||||
{epoch}
|
||||
</div>
|
||||
</HashLink>
|
||||
<span
|
||||
className={clsx(
|
||||
EPOCH_STYLES,
|
||||
className,
|
||||
focus === true ? "ring-2 ring-blue-500" : "ring-1 ring-slate-500"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import clsx from "clsx";
|
||||
import { Variable } from "../../schema";
|
||||
import { VariableName } from "./VariableName";
|
||||
|
||||
export interface UnknownVariableProps {
|
||||
variable: Variable;
|
||||
}
|
||||
|
||||
export function UnknownVariable({
|
||||
variable,
|
||||
}: UnknownVariableProps): JSX.Element {
|
||||
return (
|
||||
<div className={clsx("rounded-md whitespace-nowrap space-x-1 pr-1")}>
|
||||
<VariableName className="inline-block" variable={variable} />
|
||||
<span>???</span>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4,6 +4,7 @@ import { QuerySubs, TypeDescriptor } from "../../engine/subs";
|
|||
import { Variable } from "../../schema";
|
||||
import DrawHeadConstructor from "../Content/HeadConstructor";
|
||||
import { contentStyles } from "./../Content";
|
||||
import { VariableName } from "./VariableName";
|
||||
|
||||
interface VariableElProps {
|
||||
variable: Variable;
|
||||
|
@ -51,23 +52,6 @@ function Helper({
|
|||
desc: TypeDescriptor | undefined;
|
||||
}): JSX.Element {
|
||||
const { bg } = contentStyles(desc);
|
||||
const varHeader =
|
||||
!nested || raw ? (
|
||||
<span
|
||||
className={clsx(
|
||||
"ring-1 ring-inset ring-black-100 px-1 bg-white rounded-md cursor",
|
||||
nested ? "text-md" : "p-0.5"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick?.(variable);
|
||||
}}
|
||||
>
|
||||
{variable}
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
|
@ -76,7 +60,13 @@ function Helper({
|
|||
nested ? "text-sm" : "p-0.5 pl-0 text-base"
|
||||
)}
|
||||
>
|
||||
{varHeader}
|
||||
{(!nested || raw) && (
|
||||
<VariableName
|
||||
variable={variable}
|
||||
onClick={onClick}
|
||||
className={nested ? "text-md" : "p-0.5"}
|
||||
/>
|
||||
)}
|
||||
{children ? <span className="px-1">{children}</span> : <></>}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import clsx from "clsx";
|
||||
import { QuerySubs } from "../../engine/subs";
|
||||
import { Variable } from "../../schema";
|
||||
import { VariableName } from "./VariableName";
|
||||
|
||||
export interface VariableLinkProps {
|
||||
variable: Variable;
|
||||
subs: QuerySubs;
|
||||
onClick?: (variable: Variable) => void;
|
||||
}
|
||||
|
||||
export function VariableLink({
|
||||
variable,
|
||||
subs,
|
||||
onClick,
|
||||
}: VariableLinkProps): JSX.Element {
|
||||
const root = subs.get_root_key(variable);
|
||||
|
||||
if (variable === root) {
|
||||
throw new Error("VariableLink: variable is root");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx("rounded-md whitespace-nowrap space-x-1")}>
|
||||
<VariableName className="inline-block" variable={variable} />
|
||||
<span>→</span>
|
||||
<VariableName
|
||||
className="inline-block"
|
||||
variable={root}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import clsx from "clsx";
|
||||
import { Variable } from "../../schema";
|
||||
|
||||
export interface VariableNameProps {
|
||||
variable: Variable;
|
||||
onClick?: (variable: Variable) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function VariableName({
|
||||
variable,
|
||||
onClick,
|
||||
className,
|
||||
}: VariableNameProps): JSX.Element {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"ring-1 ring-inset ring-black-100 px-1 bg-white rounded-md",
|
||||
onClick && "cursor-pointer",
|
||||
className
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick?.(variable);
|
||||
}}
|
||||
>
|
||||
{variable}
|
||||
</span>
|
||||
);
|
||||
}
|
|
@ -64,3 +64,5 @@ export function contentStyles(desc: TypeDescriptor | undefined): ContentStyles {
|
|||
return { name: "Error", bg: "bg-red-400" };
|
||||
}
|
||||
}
|
||||
|
||||
export const LinkStyles: ContentStyles = { name: "Link", bg: "bg-slate-500" };
|
||||
|
|
|
@ -10,9 +10,9 @@ interface VariableProps extends CommonProps {
|
|||
|
||||
export function VariableEl({
|
||||
engine,
|
||||
toggleVariableVis,
|
||||
epoch,
|
||||
variable,
|
||||
graphEe,
|
||||
}: VariableProps): JSX.Element {
|
||||
engine.stepTo(epoch);
|
||||
return (
|
||||
|
@ -20,7 +20,7 @@ export function VariableEl({
|
|||
variable={variable}
|
||||
subs={engine.subs}
|
||||
onClick={(variable: Variable) => {
|
||||
toggleVariableVis(variable);
|
||||
graphEe.emit("focusVariable", variable);
|
||||
}}
|
||||
></VariableElPretty>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { TypedEmitter } from "tiny-typed-emitter";
|
||||
import type { Engine, EventEpoch } from "../../engine/engine";
|
||||
import type { Variable } from "../../schema";
|
||||
import { GraphMessage } from "../../utils/events";
|
||||
|
||||
export interface CommonProps {
|
||||
currentEpoch: EventEpoch;
|
||||
selectedEpochs: EventEpoch[];
|
||||
engine: Engine;
|
||||
toggleVariableVis: (variable: Variable) => void;
|
||||
graphEe: TypedEmitter<GraphMessage>;
|
||||
}
|
||||
|
|
|
@ -1,234 +0,0 @@
|
|||
import clsx from "clsx";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { EventEpoch } from "../engine/engine";
|
||||
import { lastSubEvent } from "../engine/event_util";
|
||||
import { UnificationMode, Event } from "../schema";
|
||||
import { Refine } from "../utils/refine";
|
||||
import EpochCell, { EpochCellView } from "./Common/EpochCell";
|
||||
import { CommonProps } from "./EventItem/types";
|
||||
import { VariableEl } from "./EventItem/Variable";
|
||||
|
||||
interface EventListProps extends CommonProps {
|
||||
events: Event[];
|
||||
root?: boolean;
|
||||
}
|
||||
|
||||
const MT = "mt-2.5";
|
||||
const UNFOCUSED = "opacity-40";
|
||||
|
||||
export default function EventList(props: EventListProps): JSX.Element {
|
||||
const { events, root } = props;
|
||||
return (
|
||||
<ul className={clsx(MT, root ? "ml-2" : "ml-[1.5em]")}>
|
||||
{events.map((event, i) => (
|
||||
<li key={i} className={MT}>
|
||||
<OneEvent {...props} event={event} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
interface OneEventProps extends CommonProps {
|
||||
event: Event;
|
||||
}
|
||||
|
||||
function OneEvent(props: OneEventProps): JSX.Element {
|
||||
const { event } = props;
|
||||
switch (event.type) {
|
||||
case "Unification":
|
||||
return <Unification {...props} event={event} />;
|
||||
case "VariableUnified":
|
||||
return <></>;
|
||||
case "VariableSetDescriptor":
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
const DROPDOWN_CLOSED = "▶";
|
||||
const DROPDOWN_OPEN = "▼";
|
||||
|
||||
const UN_UNKNOWN = "💭";
|
||||
const UN_SUCCESS = "✅";
|
||||
const UN_FAILURE = "❌";
|
||||
|
||||
function epochInRange(
|
||||
epoch: EventEpoch,
|
||||
[start, end]: [EventEpoch, EventEpoch]
|
||||
): boolean {
|
||||
return epoch >= start && epoch <= end;
|
||||
}
|
||||
|
||||
interface UnificationProps extends CommonProps {
|
||||
event: Refine<Event, "Unification">;
|
||||
}
|
||||
|
||||
function Unification(props: UnificationProps): JSX.Element {
|
||||
const { engine, event, currentEpoch } = props;
|
||||
const { mode, subevents, success } = event;
|
||||
|
||||
const beforeUnificationEpoch = engine.getEventIndex(event);
|
||||
const afterUnificationEpoch = engine.getEventIndex(lastSubEvent(event));
|
||||
|
||||
const containsCurrentEpoch = epochInRange(currentEpoch, [
|
||||
beforeUnificationEpoch,
|
||||
afterUnificationEpoch,
|
||||
]);
|
||||
|
||||
const leftVar = useMemo(
|
||||
() => (epoch: EventEpoch) =>
|
||||
<VariableEl {...props} epoch={epoch} variable={event.left} />,
|
||||
[event.left, props]
|
||||
);
|
||||
const rightVar = useMemo(
|
||||
() => (epoch: EventEpoch) =>
|
||||
<VariableEl {...props} epoch={epoch} variable={event.right} />,
|
||||
[event.right, props]
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const modeIcon = useMemo(() => <UnificationModeIcon mode={mode} />, [mode]);
|
||||
|
||||
const resultIcon = success ? UN_SUCCESS : UN_FAILURE;
|
||||
const resultHeadline = <Headline icon={resultIcon}></Headline>;
|
||||
|
||||
const epochCell = useMemo(() => {
|
||||
if (!containsCurrentEpoch) return null;
|
||||
return (
|
||||
<EpochCell
|
||||
view={EpochCellView.Events}
|
||||
epoch={currentEpoch}
|
||||
className="inline-block align-middle mr-2"
|
||||
></EpochCell>
|
||||
);
|
||||
}, [containsCurrentEpoch, currentEpoch]);
|
||||
|
||||
const getHeadline = useCallback(
|
||||
({
|
||||
epoch,
|
||||
includeEpochIfInRange,
|
||||
}: {
|
||||
epoch: EventEpoch;
|
||||
includeEpochIfInRange: boolean;
|
||||
}) => {
|
||||
const topHeadline = (
|
||||
<Headline icon={isOpen ? UN_UNKNOWN : resultIcon}></Headline>
|
||||
);
|
||||
|
||||
const optEpochCell =
|
||||
includeEpochIfInRange && containsCurrentEpoch && epochCell;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-full text-left whitespace-nowrap h-full"
|
||||
>
|
||||
{optEpochCell}
|
||||
<span
|
||||
className={clsx(
|
||||
"mr-2",
|
||||
isOpen ? "text-slate-500" : "text-slate-400"
|
||||
)}
|
||||
>
|
||||
{isOpen ? DROPDOWN_OPEN : DROPDOWN_CLOSED}
|
||||
</span>
|
||||
{topHeadline} {leftVar(epoch)} {modeIcon} {rightVar(epoch)}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
[
|
||||
isOpen,
|
||||
resultIcon,
|
||||
containsCurrentEpoch,
|
||||
epochCell,
|
||||
leftVar,
|
||||
modeIcon,
|
||||
rightVar,
|
||||
]
|
||||
);
|
||||
|
||||
if (!isOpen) {
|
||||
const headLine = getHeadline({
|
||||
epoch: afterUnificationEpoch,
|
||||
includeEpochIfInRange: true,
|
||||
});
|
||||
return (
|
||||
<div className={clsx(!containsCurrentEpoch && UNFOCUSED)}>{headLine}</div>
|
||||
);
|
||||
} else {
|
||||
const optEpochCellAfter =
|
||||
afterUnificationEpoch === currentEpoch && epochCell;
|
||||
const optEpochCellBefore =
|
||||
beforeUnificationEpoch === currentEpoch && epochCell;
|
||||
|
||||
const headlineBefore = getHeadline({
|
||||
epoch: beforeUnificationEpoch,
|
||||
includeEpochIfInRange: false,
|
||||
});
|
||||
|
||||
const dropdownTransparent = (
|
||||
<span className="text-transparent mr-2">{DROPDOWN_OPEN}</span>
|
||||
);
|
||||
|
||||
const headlineAfter = (
|
||||
<div className={clsx("whitespace-nowrap")}>
|
||||
{dropdownTransparent}
|
||||
{resultHeadline} {leftVar(afterUnificationEpoch)} {modeIcon}{" "}
|
||||
{rightVar(afterUnificationEpoch)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid gap-0 grid-cols-[min-content_min-content_auto]">
|
||||
{/* Row 1: unification start */}
|
||||
<div className="row-start-1 col-start-1">{optEpochCellBefore}</div>
|
||||
<div className="row-start-1 col-start-3">{headlineBefore}</div>
|
||||
|
||||
{/* Row 2: inner traces */}
|
||||
<div className="row-start-2 col-start-1"></div>
|
||||
<div className="row-start-2 col-start-3">
|
||||
<EventList
|
||||
{...props}
|
||||
root={false}
|
||||
engine={engine}
|
||||
events={subevents}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 3: inner traces */}
|
||||
<div className="row-start-3 col-start-1">{optEpochCellAfter}</div>
|
||||
<div className="row-start-3 col-start-3">{headlineAfter}</div>
|
||||
|
||||
{/* Col 2: dropdown line */}
|
||||
<div
|
||||
className={clsx(
|
||||
"row-start-1 row-end-4 col-start-2 h-full",
|
||||
"relative z-[1] h-full",
|
||||
"before:content-[''] before:border-l before:border-slate-500 before:z-[-1]",
|
||||
"before:absolute before:w-0 before:h-[calc(100%-1.5rem)] before:top-[1rem] before:left-[0.3rem]"
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function Headline({ icon }: { icon: string }): JSX.Element {
|
||||
return (
|
||||
<div className="inline-block align-middle">
|
||||
<div className="">{icon}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UnificationModeIcon({ mode }: { mode: UnificationMode }): JSX.Element {
|
||||
switch (mode.type) {
|
||||
case "Eq":
|
||||
return <>~</>;
|
||||
case "Present":
|
||||
return <>+=</>;
|
||||
case "LambdaSetSpecialization":
|
||||
return <>|~|</>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
export interface GroupInfo {
|
||||
group: string;
|
||||
groupHover: string;
|
||||
}
|
||||
|
||||
export function depthToGroupInfo(depth: number): GroupInfo {
|
||||
switch (depth) {
|
||||
case 0:
|
||||
return {
|
||||
group: `group/event-0`,
|
||||
groupHover: `group-hover/event-0:opacity-100`,
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
group: `group/event-1`,
|
||||
groupHover: `group-hover/event-1:opacity-100`,
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
group: `group/event-2`,
|
||||
groupHover: `group-hover/event-2:opacity-100`,
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
group: `group/event-3`,
|
||||
groupHover: `group-hover/event-3:opacity-100`,
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
group: `group/event-4`,
|
||||
groupHover: `group-hover/event-4:opacity-100`,
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
group: `group/event-5`,
|
||||
groupHover: `group-hover/event-5:opacity-100`,
|
||||
};
|
||||
case 6:
|
||||
return {
|
||||
group: `group/event-6`,
|
||||
groupHover: `group-hover/event-6:opacity-100`,
|
||||
};
|
||||
case 7:
|
||||
return {
|
||||
group: `group/event-7`,
|
||||
groupHover: `group-hover/event-7:opacity-100`,
|
||||
};
|
||||
case 8:
|
||||
return {
|
||||
group: `group/event-8`,
|
||||
groupHover: `group-hover/event-8:opacity-100`,
|
||||
};
|
||||
case 9:
|
||||
return {
|
||||
group: `group/event-9`,
|
||||
groupHover: `group-hover/event-9:opacity-100`,
|
||||
};
|
||||
case 10:
|
||||
return {
|
||||
group: `group/event-10`,
|
||||
groupHover: `group-hover/event-10:opacity-100`,
|
||||
};
|
||||
case 11:
|
||||
return {
|
||||
group: `group/event-11`,
|
||||
groupHover: `group-hover/event-11:opacity-100`,
|
||||
};
|
||||
case 12:
|
||||
return {
|
||||
group: `group/event-12`,
|
||||
groupHover: `group-hover/event-12:opacity-100`,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Too deep: ${depth}`);
|
||||
}
|
||||
}
|
423
crates/compiler/checkmate/www/src/components/EventList/index.tsx
Normal file
423
crates/compiler/checkmate/www/src/components/EventList/index.tsx
Normal file
|
@ -0,0 +1,423 @@
|
|||
import clsx from "clsx";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { TypedEmitter } from "tiny-typed-emitter";
|
||||
import { EventEpoch } from "../../engine/engine";
|
||||
import { lastSubEvent } from "../../engine/event_util";
|
||||
import { useFocusOutlineEvent } from "../../hooks/useFocusOutlineEvent";
|
||||
import { UnificationMode, Event } from "../../schema";
|
||||
import {
|
||||
EventListMessage,
|
||||
GlobalMessage,
|
||||
GraphMessage,
|
||||
LoadEpochView,
|
||||
} from "../../utils/events";
|
||||
import { Refine } from "../../utils/refine";
|
||||
import EpochCell from "../Common/EpochCell";
|
||||
import { CommonProps } from "../EventItem/types";
|
||||
import { VariableEl } from "../EventItem/Variable";
|
||||
import { depthToGroupInfo } from "./depthGroup";
|
||||
|
||||
interface EventListProps extends CommonProps {
|
||||
events: Event[];
|
||||
eventListEe: TypedEmitter<EventListMessage>;
|
||||
globalEe: TypedEmitter<GlobalMessage>;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
const MT = "my-2.5";
|
||||
const LOWER_OPACITY = "opacity-40";
|
||||
|
||||
export default function EventList(props: EventListProps): JSX.Element {
|
||||
const { events, depth } = props;
|
||||
return (
|
||||
<ul className={clsx(MT, "space-y-2.5", depth === 0 ? "" : "ml-[1em]")}>
|
||||
{events.map((event, i) => (
|
||||
<li key={i}>
|
||||
<OneEvent {...props} event={event} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
interface OneEventProps extends CommonProps {
|
||||
event: Event;
|
||||
eventListEe: TypedEmitter<EventListMessage>;
|
||||
globalEe: TypedEmitter<GlobalMessage>;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
function OneEvent(props: OneEventProps): JSX.Element {
|
||||
const { event } = props;
|
||||
switch (event.type) {
|
||||
case "Unification":
|
||||
return <UnificationEvent {...props} event={event} />;
|
||||
case "VariableUnified":
|
||||
return <></>;
|
||||
case "VariableSetDescriptor":
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
const DROPDOWN_CLOSED = "▶";
|
||||
const DROPDOWN_OPEN = "▼";
|
||||
|
||||
const UN_UNKNOWN = "💭";
|
||||
const UN_SUCCESS = "✅";
|
||||
const UN_FAILURE = "❌";
|
||||
|
||||
function epochInRange(
|
||||
epoch: EventEpoch,
|
||||
[start, end]: [EventEpoch, EventEpoch]
|
||||
): boolean {
|
||||
return epoch >= start && epoch <= end;
|
||||
}
|
||||
|
||||
interface UnificationProps extends CommonProps {
|
||||
event: Refine<Event, "Unification">;
|
||||
eventListEe: TypedEmitter<EventListMessage>;
|
||||
globalEe: TypedEmitter<GlobalMessage>;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
const COL_1_P = "pl-1.5";
|
||||
|
||||
const COL_1_OUTLINE_STYLES = clsx(COL_1_P, "outline-event-col-1");
|
||||
const COL_3_OUTLINE_STYLES = clsx("outline-event-col-3");
|
||||
|
||||
const COL_1_ROUNDED = "rounded-l-md";
|
||||
const COL_3_ROUNDED = "rounded-r-md";
|
||||
|
||||
const UN_EXPANDED_OUTLINE_STYLES = clsx("ring-inset ring-2 ring-blue-500");
|
||||
|
||||
const TRANSITION_SHADOW = "transition-shadow ease-in-out duration-500";
|
||||
const TRANSITION_OPACITY = "transition-opacity ease-in-out duration-150";
|
||||
|
||||
const GROUP_STLYES = "relative overflow-hidden";
|
||||
|
||||
// Space for the hover cells at the end of line.
|
||||
const EOL_SPACE = "pr-12";
|
||||
|
||||
function UnificationEvent(props: UnificationProps): JSX.Element {
|
||||
const {
|
||||
engine,
|
||||
event,
|
||||
selectedEpochs,
|
||||
graphEe,
|
||||
eventListEe,
|
||||
depth,
|
||||
globalEe,
|
||||
} = props;
|
||||
const { mode, subevents, success } = event;
|
||||
|
||||
const beforeUnificationEpoch = engine.getEventIndex(event);
|
||||
const afterUnificationEpoch = engine.getEventIndex(lastSubEvent(event));
|
||||
|
||||
const containedEpoch = selectedEpochs.find((epoch) =>
|
||||
epochInRange(epoch, [beforeUnificationEpoch, afterUnificationEpoch])
|
||||
);
|
||||
|
||||
const leftVar = useMemo(
|
||||
() => (epoch: EventEpoch) =>
|
||||
<VariableEl {...props} epoch={epoch} variable={event.left} />,
|
||||
[event.left, props]
|
||||
);
|
||||
const rightVar = useMemo(
|
||||
() => (epoch: EventEpoch) =>
|
||||
<VariableEl {...props} epoch={epoch} variable={event.right} />,
|
||||
[event.right, props]
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const isOutlined = useFocusOutlineEvent<"focusEpoch", EventEpoch | undefined>(
|
||||
{
|
||||
ee: eventListEe,
|
||||
value: containedEpoch,
|
||||
event: "focusEpoch",
|
||||
}
|
||||
);
|
||||
|
||||
const modeIcon = useMemo(() => <UnificationModeIcon mode={mode} />, [mode]);
|
||||
|
||||
const resultIcon = success ? UN_SUCCESS : UN_FAILURE;
|
||||
const resultHeadline = <Headline icon={resultIcon}></Headline>;
|
||||
|
||||
const epochCell = useMemo(() => {
|
||||
if (containedEpoch === undefined) return null;
|
||||
return <EventListEpochCell epoch={containedEpoch} graphEe={graphEe} />;
|
||||
}, [containedEpoch, graphEe]);
|
||||
|
||||
const getBeforeUnificationHeadline = useCallback(
|
||||
({
|
||||
epoch,
|
||||
collapsedMode,
|
||||
}: {
|
||||
epoch: EventEpoch;
|
||||
collapsedMode?: boolean;
|
||||
}) => {
|
||||
const topHeadline = (
|
||||
<Headline icon={isOpen ? UN_UNKNOWN : resultIcon}></Headline>
|
||||
);
|
||||
|
||||
const optEpochCell =
|
||||
collapsedMode && containedEpoch !== undefined && epochCell;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={clsx(
|
||||
"w-full text-left whitespace-nowrap h-full overflow-scroll",
|
||||
collapsedMode && COL_1_P
|
||||
)}
|
||||
>
|
||||
<span className={EOL_SPACE}>
|
||||
{optEpochCell}
|
||||
<span
|
||||
className={clsx(
|
||||
"mr-2",
|
||||
isOpen ? "text-slate-500" : "text-slate-400"
|
||||
)}
|
||||
>
|
||||
{isOpen ? DROPDOWN_OPEN : DROPDOWN_CLOSED}
|
||||
</span>
|
||||
{topHeadline} {leftVar(epoch)} {modeIcon} {rightVar(epoch)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
[isOpen, resultIcon, containedEpoch, epochCell, leftVar, modeIcon, rightVar]
|
||||
);
|
||||
|
||||
const { group, groupHover } = depthToGroupInfo(depth);
|
||||
|
||||
if (!isOpen) {
|
||||
const headLine = getBeforeUnificationHeadline({
|
||||
epoch: afterUnificationEpoch,
|
||||
collapsedMode: true,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"rounded-md",
|
||||
TRANSITION_SHADOW,
|
||||
group,
|
||||
GROUP_STLYES,
|
||||
containedEpoch !== undefined &&
|
||||
isOutlined &&
|
||||
UN_EXPANDED_OUTLINE_STYLES
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
TRANSITION_OPACITY,
|
||||
containedEpoch === undefined && LOWER_OPACITY
|
||||
)}
|
||||
>
|
||||
{headLine}
|
||||
</div>
|
||||
<LoadEpochGraphLauncher
|
||||
groupHover={groupHover}
|
||||
epoch={afterUnificationEpoch}
|
||||
globalEe={globalEe}
|
||||
className="bottom-0 right-2"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const beforeIsCurrentEpoch = beforeUnificationEpoch === containedEpoch;
|
||||
const afterIsCurrentEpoch = afterUnificationEpoch === containedEpoch;
|
||||
|
||||
const epochCellBefore = beforeIsCurrentEpoch && epochCell;
|
||||
const epochCellAfter = afterIsCurrentEpoch && epochCell;
|
||||
|
||||
const outlineEpochCellAfter = afterIsCurrentEpoch && isOutlined;
|
||||
const outlineEpochCellBefore = beforeIsCurrentEpoch && isOutlined;
|
||||
|
||||
const headlineBefore = getBeforeUnificationHeadline({
|
||||
epoch: beforeUnificationEpoch,
|
||||
});
|
||||
|
||||
const dropdownTransparent = (
|
||||
<span className="text-transparent mr-2">{DROPDOWN_OPEN}</span>
|
||||
);
|
||||
|
||||
const headlineAfter = (
|
||||
<div className={clsx("whitespace-nowrap")}>
|
||||
{dropdownTransparent}
|
||||
{resultHeadline} {leftVar(afterUnificationEpoch)} {modeIcon}{" "}
|
||||
{rightVar(afterUnificationEpoch)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx(group, GROUP_STLYES)}>
|
||||
<div
|
||||
className={clsx(
|
||||
"grid gap-0 grid-cols-[min-content_min-content_1fr_auto] opacity-100",
|
||||
"overflow-scroll",
|
||||
TRANSITION_OPACITY
|
||||
)}
|
||||
>
|
||||
{/* Row 1: unification start */}
|
||||
<div
|
||||
className={clsx(
|
||||
"row-start-1 col-start-1",
|
||||
TRANSITION_SHADOW,
|
||||
COL_1_ROUNDED,
|
||||
outlineEpochCellBefore ? COL_1_OUTLINE_STYLES : COL_1_P
|
||||
)}
|
||||
>
|
||||
{epochCellBefore}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"row-start-1 col-start-3 col-end-5",
|
||||
TRANSITION_SHADOW,
|
||||
COL_3_ROUNDED,
|
||||
outlineEpochCellBefore && COL_3_OUTLINE_STYLES,
|
||||
EOL_SPACE
|
||||
)}
|
||||
>
|
||||
{headlineBefore}
|
||||
</div>
|
||||
|
||||
{/* Row 2: inner traces */}
|
||||
<div className={clsx("row-start-2 col-start-1")}></div>
|
||||
<div className={clsx("row-start-2col-start-3 col-end-4", "w-full")}>
|
||||
<EventList
|
||||
{...props}
|
||||
depth={depth + 1}
|
||||
engine={engine}
|
||||
events={subevents}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 3: unification end */}
|
||||
<div
|
||||
className={clsx(
|
||||
"row-start-3 col-start-1",
|
||||
TRANSITION_SHADOW,
|
||||
COL_1_ROUNDED,
|
||||
outlineEpochCellAfter ? COL_1_OUTLINE_STYLES : COL_1_P
|
||||
)}
|
||||
>
|
||||
{epochCellAfter}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"row-start-3 col-start-3 col-end-5",
|
||||
TRANSITION_SHADOW,
|
||||
COL_3_ROUNDED,
|
||||
outlineEpochCellAfter && COL_3_OUTLINE_STYLES,
|
||||
EOL_SPACE
|
||||
)}
|
||||
>
|
||||
{headlineAfter}
|
||||
</div>
|
||||
|
||||
{/* Col 2: dropdown line */}
|
||||
<div
|
||||
className={clsx(
|
||||
"row-start-1 row-end-4 col-start-2 h-full",
|
||||
"relative z-[1] h-full",
|
||||
"before:content-[''] before:border-l before:border-slate-500 before:z-[-1]",
|
||||
"before:absolute before:w-0 before:h-[calc(100%-1.5rem)] before:top-[1rem] before:left-[0.3rem]"
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<LoadEpochGraphLauncher
|
||||
groupHover={groupHover}
|
||||
epoch={beforeUnificationEpoch}
|
||||
globalEe={globalEe}
|
||||
className="top-0 right-2"
|
||||
/>
|
||||
<LoadEpochGraphLauncher
|
||||
groupHover={groupHover}
|
||||
epoch={afterUnificationEpoch}
|
||||
globalEe={globalEe}
|
||||
className="bottom-0 right-2"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function Headline({ icon }: { icon: string }): JSX.Element {
|
||||
return (
|
||||
<div className="inline-block align-middle">
|
||||
<div className="">{icon}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UnificationModeIcon({ mode }: { mode: UnificationMode }): JSX.Element {
|
||||
switch (mode.type) {
|
||||
case "Eq":
|
||||
return <>~</>;
|
||||
case "Present":
|
||||
return <>+=</>;
|
||||
case "LambdaSetSpecialization":
|
||||
return <>|~|</>;
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListEpochCellProps {
|
||||
epoch: EventEpoch;
|
||||
graphEe: TypedEmitter<GraphMessage>;
|
||||
}
|
||||
|
||||
function EventListEpochCell({
|
||||
epoch,
|
||||
graphEe,
|
||||
}: EventListEpochCellProps): JSX.Element {
|
||||
return (
|
||||
<span
|
||||
id={`events-${epoch}`}
|
||||
className={clsx("cursor-pointer rounded-md")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
graphEe.emit("focusEpoch", epoch);
|
||||
}}
|
||||
>
|
||||
<EpochCell>{epoch}</EpochCell>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface LoadEpochGraphLauncherProps {
|
||||
groupHover: string;
|
||||
epoch: EventEpoch;
|
||||
globalEe: TypedEmitter<GlobalMessage>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function LoadEpochGraphLauncher({
|
||||
groupHover,
|
||||
epoch,
|
||||
className,
|
||||
globalEe,
|
||||
}: LoadEpochGraphLauncherProps): JSX.Element {
|
||||
return (
|
||||
<div className={clsx("absolute opacity-0", groupHover, className)}>
|
||||
<span className="space-x-0.5 bg-gray-200 ring-1 ring-slate-300 rounded-sm px-1 opacity-80 hover:opacity-100">
|
||||
<span
|
||||
className="text-blue-400 hover:text-blue-500 cursor-pointer"
|
||||
onClick={() => {
|
||||
globalEe.emit("loadEpoch", epoch, LoadEpochView.Top);
|
||||
}}
|
||||
>
|
||||
↑
|
||||
</span>
|
||||
<span
|
||||
className="text-blue-400 hover:text-blue-500 cursor-pointer"
|
||||
onClick={() => {
|
||||
globalEe.emit("loadEpoch", epoch, LoadEpochView.Bot);
|
||||
}}
|
||||
>
|
||||
↓
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
import { Variable } from "../../schema";
|
||||
|
||||
export type ToggleVariableHandler = (variable: Variable) => void;
|
||||
export type KeydownHandler = (key: string) => Promise<void>;
|
|
@ -2,25 +2,30 @@ import clsx from "clsx";
|
|||
import { Handle, Position } from "reactflow";
|
||||
import { Variable } from "../../schema";
|
||||
import { assertExhaustive } from "../../utils/exhaustive";
|
||||
import { contentStyles } from "../Content";
|
||||
import { contentStyles, LinkStyles } from "../Content";
|
||||
import { VariableElPretty } from "../Common/Variable";
|
||||
import { SubsSnapshot, TypeDescriptor } from "../../engine/subs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TypedEmitter } from "tiny-typed-emitter";
|
||||
import { VariableLink } from "../Common/VariableLink";
|
||||
import { VariableMessage } from "../../utils/events";
|
||||
import { useFocusOutlineEvent } from "../../hooks/useFocusOutlineEvent";
|
||||
import { UnknownVariable } from "../Common/UnknownVariable";
|
||||
|
||||
type AddSubVariableLink = (from: Variable, subVariable: Variable) => void;
|
||||
|
||||
export interface VariableMessageEvents {
|
||||
focus: (variable: Variable) => void;
|
||||
}
|
||||
type AddSubVariableLink = ({
|
||||
from,
|
||||
variable,
|
||||
}: {
|
||||
from: Variable;
|
||||
variable: Variable;
|
||||
}) => void;
|
||||
|
||||
export interface VariableNodeProps {
|
||||
data: {
|
||||
subs: SubsSnapshot;
|
||||
variable: Variable;
|
||||
rawVariable: Variable;
|
||||
addSubVariableLink: AddSubVariableLink;
|
||||
isOutlined: boolean;
|
||||
ee: TypedEmitter<VariableMessageEvents>;
|
||||
ee: TypedEmitter<VariableMessage>;
|
||||
};
|
||||
targetPosition?: Position;
|
||||
sourcePosition?: Position;
|
||||
|
@ -32,63 +37,101 @@ export default function VariableNode({
|
|||
sourcePosition,
|
||||
}: VariableNodeProps): JSX.Element {
|
||||
const {
|
||||
variable,
|
||||
subs,
|
||||
rawVariable,
|
||||
addSubVariableLink,
|
||||
isOutlined: isOutlinedProp,
|
||||
ee: eeProp,
|
||||
} = data;
|
||||
|
||||
const [isOutlined, setIsOutlined] = useState(isOutlinedProp);
|
||||
const isOutlined = useFocusOutlineEvent({
|
||||
ee: eeProp,
|
||||
value: rawVariable,
|
||||
event: "focus",
|
||||
defaultIsOutlined: isOutlinedProp,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
eeProp.on("focus", (focusVar: Variable) => {
|
||||
if (focusVar !== variable) return;
|
||||
setIsOutlined(true);
|
||||
});
|
||||
}, [eeProp, variable]);
|
||||
const varType = subs.get(rawVariable);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOutlined) return;
|
||||
const timer = setTimeout(() => {
|
||||
setIsOutlined(false);
|
||||
}, 500);
|
||||
let renderContent: JSX.Element;
|
||||
let bgStyles: string;
|
||||
const isContent = varType?.type === "descriptor";
|
||||
switch (varType?.type) {
|
||||
case undefined: {
|
||||
bgStyles = "bg-red-500";
|
||||
renderContent = <UnknownVariable variable={rawVariable} />;
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [isOutlined]);
|
||||
break;
|
||||
}
|
||||
case "link": {
|
||||
bgStyles = LinkStyles.bg;
|
||||
|
||||
const desc = subs.get_root(variable);
|
||||
const styles = contentStyles(desc);
|
||||
const basis: BasisProps = {
|
||||
subs,
|
||||
origin: variable,
|
||||
addSubVariableLink,
|
||||
};
|
||||
renderContent = (
|
||||
<VariableLink
|
||||
subs={subs}
|
||||
variable={rawVariable}
|
||||
onClick={() =>
|
||||
addSubVariableLink({
|
||||
from: rawVariable,
|
||||
variable: subs.get_root_key(rawVariable),
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const content = Object.entries(
|
||||
VariableNodeContent(variable, desc, basis)
|
||||
).filter((el): el is [string, JSX.Element] => !!el[1]);
|
||||
break;
|
||||
}
|
||||
case "descriptor": {
|
||||
const variable = rawVariable;
|
||||
const desc: TypeDescriptor = varType;
|
||||
|
||||
let expandedContent = <></>;
|
||||
if (content.length > 0) {
|
||||
expandedContent = (
|
||||
<ul className="text-sm text-left mt-2 space-y-1">
|
||||
{content.map(([key, value], i) => (
|
||||
<li key={i} className="space-x-2">
|
||||
{key}: {value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
const styles = contentStyles(desc);
|
||||
bgStyles = styles.bg;
|
||||
const basis: BasisProps = {
|
||||
subs,
|
||||
origin: variable,
|
||||
addSubVariableLink,
|
||||
};
|
||||
|
||||
const content = Object.entries(
|
||||
VariableNodeContent(variable, desc, basis)
|
||||
).filter((el): el is [string, JSX.Element] => !!el[1]);
|
||||
|
||||
let expandedContent = <></>;
|
||||
if (content.length > 0) {
|
||||
expandedContent = (
|
||||
<ul className="text-sm text-left mt-2 space-y-1">
|
||||
{content.map(([key, value], i) => (
|
||||
<li key={i} className="space-x-2">
|
||||
{key}: {value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent = (
|
||||
<>
|
||||
<div>
|
||||
<VariableElPretty variable={variable} subs={subs} />
|
||||
</div>
|
||||
{expandedContent}
|
||||
</>
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(varType);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
styles.bg,
|
||||
"bg-opacity-50 py-2 px-4 rounded-lg border transition ease-in-out duration-700",
|
||||
bgStyles,
|
||||
"bg-opacity-50 rounded-md transition ease-in-out duration-700",
|
||||
isContent ? "py-2 px-4 border" : "p-0",
|
||||
isOutlined && "ring-2 ring-blue-500",
|
||||
"text-center font-mono"
|
||||
)}
|
||||
|
@ -97,15 +140,14 @@ export default function VariableNode({
|
|||
type="target"
|
||||
position={targetPosition ?? Position.Top}
|
||||
isConnectable={false}
|
||||
style={{ background: "transparent", border: "none" }}
|
||||
/>
|
||||
<div>
|
||||
<VariableElPretty variable={variable} subs={subs} />
|
||||
</div>
|
||||
{expandedContent}
|
||||
{renderContent}
|
||||
<Handle
|
||||
type="source"
|
||||
position={sourcePosition ?? Position.Bottom}
|
||||
isConnectable={false}
|
||||
style={{ background: "transparent", border: "none" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -238,7 +280,7 @@ function SubVariable({
|
|||
<VariableElPretty
|
||||
variable={variable}
|
||||
subs={subs}
|
||||
onClick={() => addSubVariableLink(origin, variable)}
|
||||
onClick={() => addSubVariableLink({ from: origin, variable })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,25 +18,30 @@ import ReactFlow, {
|
|||
useStore,
|
||||
ReactFlowState,
|
||||
Position,
|
||||
MarkerType,
|
||||
EdgeMarkerType,
|
||||
} from "reactflow";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Variable } from "../../schema";
|
||||
|
||||
import "reactflow/dist/style.css";
|
||||
import clsx from "clsx";
|
||||
import VariableNode, {
|
||||
VariableMessageEvents,
|
||||
VariableNodeProps,
|
||||
} from "./VariableNode";
|
||||
import VariableNode, { VariableNodeProps } from "./VariableNode";
|
||||
import { SubsSnapshot } from "../../engine/subs";
|
||||
import { KeydownHandler } from "../Events";
|
||||
import { TypedEmitter } from "tiny-typed-emitter";
|
||||
import EpochCell, { EpochCellView } from "../Common/EpochCell";
|
||||
import EpochCell from "../Common/EpochCell";
|
||||
import { HashLink } from "react-router-hash-link";
|
||||
import {
|
||||
EventListMessage,
|
||||
GraphMessage,
|
||||
VariableMessage,
|
||||
} from "../../utils/events";
|
||||
import { useFocusOutlineEvent } from "../../hooks/useFocusOutlineEvent";
|
||||
|
||||
export interface VariablesGraphProps {
|
||||
subs: SubsSnapshot;
|
||||
onVariable: (handler: (variable: Variable) => void) => void;
|
||||
onKeydown: (handler: KeydownHandler) => void;
|
||||
graphEe: TypedEmitter<GraphMessage>;
|
||||
eventListEe: TypedEmitter<EventListMessage>;
|
||||
}
|
||||
|
||||
function horizontalityToPositions(isHorizontal: boolean): {
|
||||
|
@ -154,7 +159,9 @@ async function computeLayoutedElements({
|
|||
//height: 50,
|
||||
})),
|
||||
//@ts-ignore
|
||||
edges: edges,
|
||||
edges: edges.map((edge) => ({
|
||||
...edge,
|
||||
})),
|
||||
};
|
||||
|
||||
const layoutedGraph = await elk.layout(graph);
|
||||
|
@ -197,20 +204,22 @@ function newVariable(
|
|||
};
|
||||
}
|
||||
|
||||
function addNodeChange(node: Node, existingNodes: Node[]): NodeChange | null {
|
||||
if (existingNodes.some((n) => n.id === node.id)) {
|
||||
return null;
|
||||
}
|
||||
function canAddVariable(variableName: string, existingNodes: Node[]): boolean {
|
||||
return !existingNodes.some((n) => n.id === variableName);
|
||||
}
|
||||
|
||||
function canAddEdge(edgeName: string, existingEdges: Edge[]): boolean {
|
||||
return !existingEdges.some((e) => e.id === edgeName);
|
||||
}
|
||||
|
||||
function addNode(node: Node): NodeChange {
|
||||
return {
|
||||
type: "add",
|
||||
item: node,
|
||||
};
|
||||
}
|
||||
|
||||
function addEdgeChange(edge: Edge, existingEdges: Edge[]): EdgeChange | null {
|
||||
if (existingEdges.some((e) => e.id === edge.id)) {
|
||||
return null;
|
||||
}
|
||||
function addEdge(edge: Edge): EdgeChange {
|
||||
return {
|
||||
type: "add",
|
||||
item: edge,
|
||||
|
@ -279,11 +288,11 @@ function useAutoLayout(options: ComputeElkLayoutOptions) {
|
|||
function useKeydown({
|
||||
layoutConfig,
|
||||
setLayoutConfig,
|
||||
onKeydown,
|
||||
graphEe,
|
||||
}: {
|
||||
layoutConfig: LayoutConfiguration;
|
||||
setLayoutConfig: React.Dispatch<React.SetStateAction<LayoutConfiguration>>;
|
||||
onKeydown: (handler: KeydownHandler) => void;
|
||||
graphEe: TypedEmitter<GraphMessage>;
|
||||
}) {
|
||||
const redoLayout = useRedoLayout(layoutConfig);
|
||||
|
||||
|
@ -308,33 +317,61 @@ function useKeydown({
|
|||
},
|
||||
[redoLayout, setLayoutConfig]
|
||||
);
|
||||
onKeydown(async (key) => {
|
||||
await keyDownHandler(key);
|
||||
});
|
||||
graphEe.on("keydown", async (key) => await keyDownHandler(key));
|
||||
}
|
||||
|
||||
function Graph({
|
||||
subs,
|
||||
onVariable,
|
||||
onKeydown,
|
||||
graphEe,
|
||||
eventListEe,
|
||||
}: VariablesGraphProps): JSX.Element {
|
||||
const initialNodes: Node[] = [];
|
||||
const initialEdges: Edge[] = [];
|
||||
const instance = useReactFlow();
|
||||
|
||||
const ee = useRef(new TypedEmitter<VariableMessageEvents>());
|
||||
// We need to reset the graph when the subs snapshot changes. I'm not sure
|
||||
// why this isn't done by the existing state manager.
|
||||
useEffect(() => {
|
||||
instance.setNodes([]);
|
||||
instance.setEdges([]);
|
||||
}, [instance, subs.epoch]);
|
||||
|
||||
const varEe = useRef(new TypedEmitter<VariableMessage>());
|
||||
// Allow an unbounded number of listeners since we attach a listener for each
|
||||
// variable.
|
||||
varEe.current.setMaxListeners(Infinity);
|
||||
|
||||
const isOutlined = useFocusOutlineEvent({
|
||||
ee: graphEe,
|
||||
value: subs.epoch,
|
||||
event: "focusEpoch",
|
||||
});
|
||||
|
||||
const [layoutConfig, setLayoutConfig] =
|
||||
useState<LayoutConfiguration>(LAYOUT_CONFIG_DOWN);
|
||||
|
||||
const [elements, setElements] = useState<LayoutedElements>({
|
||||
nodes: initialNodes,
|
||||
edges: initialEdges,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
const [variablesNeedingFocus, setVariablesNeedingFocus] = useState<
|
||||
Set<Variable>
|
||||
>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (variablesNeedingFocus.size === 0) {
|
||||
return;
|
||||
}
|
||||
for (const variable of variablesNeedingFocus) {
|
||||
varEe.current.emit("focus", variable);
|
||||
}
|
||||
setVariablesNeedingFocus(new Set());
|
||||
}, [variablesNeedingFocus]);
|
||||
|
||||
useAutoLayout(layoutConfig);
|
||||
useKeydown({
|
||||
layoutConfig,
|
||||
setLayoutConfig,
|
||||
onKeydown,
|
||||
graphEe,
|
||||
});
|
||||
|
||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||
|
@ -355,81 +392,93 @@ function Graph({
|
|||
});
|
||||
}, []);
|
||||
|
||||
const addSubVariableLink = useCallback(
|
||||
(fromN: Variable, subLinkN: Variable) => {
|
||||
fromN = subs.get_root_key(fromN);
|
||||
subLinkN = subs.get_root_key(subLinkN);
|
||||
const from = fromN.toString();
|
||||
const to = subLinkN.toString();
|
||||
interface AddNewVariableParams {
|
||||
from?: Variable;
|
||||
variable: Variable;
|
||||
}
|
||||
|
||||
const addNewVariable = useCallback(
|
||||
({ from, variable }: AddNewVariableParams) => {
|
||||
const variablesToFocus = new Set<Variable>();
|
||||
|
||||
setElements(({ nodes, edges }) => {
|
||||
const optNewNode = addNodeChange(
|
||||
newVariable(
|
||||
to,
|
||||
{
|
||||
subs,
|
||||
variable: subLinkN,
|
||||
addSubVariableLink,
|
||||
isOutlined: true,
|
||||
ee: ee.current,
|
||||
},
|
||||
layoutConfig.isHorizontal
|
||||
),
|
||||
nodes
|
||||
);
|
||||
const newNodes = optNewNode
|
||||
? applyNodeChanges([optNewNode], nodes)
|
||||
: nodes;
|
||||
let fromVariable: Variable | undefined = from;
|
||||
let toVariable: Variable | undefined = variable;
|
||||
|
||||
const optNewEdge = addEdgeChange(
|
||||
{ id: `${from}->${to}`, source: from, target: to },
|
||||
edges
|
||||
);
|
||||
const newEdges = optNewEdge
|
||||
? applyEdgeChanges([optNewEdge], edges)
|
||||
: edges;
|
||||
const nodeChanges: NodeChange[] = [];
|
||||
const edgeChanges: EdgeChange[] = [];
|
||||
|
||||
while (toVariable !== undefined) {
|
||||
const toVariableName = toVariable.toString();
|
||||
if (canAddVariable(toVariableName, nodes)) {
|
||||
const newVariableNode = newVariable(
|
||||
toVariable.toString(),
|
||||
{
|
||||
subs,
|
||||
rawVariable: toVariable,
|
||||
addSubVariableLink: addNewVariable,
|
||||
isOutlined: true,
|
||||
ee: varEe.current,
|
||||
},
|
||||
layoutConfig.isHorizontal
|
||||
);
|
||||
|
||||
nodeChanges.push(addNode(newVariableNode));
|
||||
}
|
||||
|
||||
if (fromVariable !== undefined) {
|
||||
const edgeName = `${fromVariable}->${toVariable}`;
|
||||
if (canAddEdge(edgeName, edges)) {
|
||||
let markerEnd: EdgeMarkerType | undefined;
|
||||
if (subs.get_root_key(fromVariable) === toVariable) {
|
||||
markerEnd = {
|
||||
type: MarkerType.ArrowClosed,
|
||||
width: 20,
|
||||
height: 20,
|
||||
};
|
||||
}
|
||||
|
||||
const newEdge = addEdge({
|
||||
id: `${fromVariable}->${toVariable}`,
|
||||
source: fromVariable.toString(),
|
||||
target: toVariableName,
|
||||
markerEnd,
|
||||
});
|
||||
|
||||
edgeChanges.push(newEdge);
|
||||
}
|
||||
}
|
||||
|
||||
variablesToFocus.add(toVariable);
|
||||
|
||||
fromVariable = toVariable;
|
||||
const rootToVariable = subs.get_root_key(toVariable);
|
||||
if (toVariable !== rootToVariable) {
|
||||
toVariable = rootToVariable;
|
||||
} else {
|
||||
toVariable = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const newNodes = applyNodeChanges(nodeChanges, nodes);
|
||||
const newEdges = applyEdgeChanges(edgeChanges, edges);
|
||||
|
||||
return { nodes: newNodes, edges: newEdges };
|
||||
});
|
||||
|
||||
ee.current.emit("focus", subLinkN);
|
||||
setVariablesNeedingFocus(variablesToFocus);
|
||||
},
|
||||
[layoutConfig, subs]
|
||||
[layoutConfig.isHorizontal, subs]
|
||||
);
|
||||
|
||||
const addNode = useCallback(
|
||||
(variableN: Variable) => {
|
||||
variableN = subs.get_root_key(variableN);
|
||||
const variable = variableN.toString();
|
||||
|
||||
setElements(({ nodes, edges }) => {
|
||||
const optNewNode = addNodeChange(
|
||||
newVariable(
|
||||
variable,
|
||||
{
|
||||
subs,
|
||||
variable: variableN,
|
||||
addSubVariableLink,
|
||||
isOutlined: true,
|
||||
ee: ee.current,
|
||||
},
|
||||
layoutConfig.isHorizontal
|
||||
),
|
||||
nodes
|
||||
);
|
||||
const newNodes = optNewNode
|
||||
? applyNodeChanges([optNewNode], nodes)
|
||||
: nodes;
|
||||
|
||||
return { nodes: newNodes, edges: edges };
|
||||
});
|
||||
|
||||
ee.current.emit("focus", variableN);
|
||||
const addNewVariableNode = useCallback(
|
||||
(variable: Variable) => {
|
||||
addNewVariable({ variable });
|
||||
},
|
||||
[subs, addSubVariableLink, layoutConfig]
|
||||
[addNewVariable]
|
||||
);
|
||||
|
||||
onVariable(addNode);
|
||||
graphEe.on("focusVariable", addNewVariableNode);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
|
@ -445,9 +494,21 @@ function Graph({
|
|||
// https://reactflow.dev/docs/guides/remove-attribution/
|
||||
hideAttribution: true,
|
||||
}}
|
||||
className={clsx(
|
||||
"ring-inset rounded-md transition ease-in-out duration-700",
|
||||
isOutlined && "ring-2 ring-blue-500"
|
||||
)}
|
||||
>
|
||||
<Panel position="top-left">
|
||||
<EpochCell view={EpochCellView.Graph} epoch={subs.epoch}></EpochCell>
|
||||
<HashLink
|
||||
smooth
|
||||
to={`#events-${subs.epoch}`}
|
||||
onClick={() => {
|
||||
eventListEe.emit("focusEpoch", subs.epoch);
|
||||
}}
|
||||
>
|
||||
<EpochCell>Epoch {subs.epoch}</EpochCell>
|
||||
</HashLink>
|
||||
</Panel>
|
||||
<Panel position="top-right">
|
||||
<LayoutPanel
|
||||
|
@ -493,10 +554,14 @@ function LayoutPanel({
|
|||
);
|
||||
}
|
||||
|
||||
export default function VariablesGraph(props: VariablesGraphProps) {
|
||||
export default function VariablesGraph({
|
||||
subs,
|
||||
graphEe,
|
||||
eventListEe,
|
||||
}: VariablesGraphProps) {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<Graph {...props} />
|
||||
<Graph subs={subs} graphEe={graphEe} eventListEe={eventListEe} />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,68 +1,111 @@
|
|||
import React, { useState } from "react";
|
||||
import { AllEvents, Variable } from "../schema";
|
||||
import { Engine } from "../engine/engine";
|
||||
import EventList from "./EventList";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AllEvents } from "../schema";
|
||||
import { Engine, EventEpoch } from "../engine/engine";
|
||||
import EventList from "./EventList/index";
|
||||
import VariablesGraph from "./Graph/VariablesGraph";
|
||||
import { TypedEmitter } from "tiny-typed-emitter";
|
||||
import { KeydownHandler, ToggleVariableHandler } from "./Events";
|
||||
import {
|
||||
EventListMessage,
|
||||
GlobalMessage,
|
||||
GraphMessage,
|
||||
LoadEpochView,
|
||||
} from "../utils/events";
|
||||
import { assertExhaustive } from "../utils/exhaustive";
|
||||
import { SubsSnapshot } from "../engine/subs";
|
||||
|
||||
interface UiProps {
|
||||
events: AllEvents;
|
||||
}
|
||||
|
||||
interface MessageEvents {
|
||||
toggleVariable: ToggleVariableHandler;
|
||||
keydown: KeydownHandler;
|
||||
}
|
||||
|
||||
export default function Ui({ events }: UiProps): JSX.Element {
|
||||
const engine = new Engine(events);
|
||||
const engine = React.useRef(new Engine(events));
|
||||
|
||||
const ee = new TypedEmitter<MessageEvents>();
|
||||
const toggleVariableHandlers: ToggleVariableHandler[] = [];
|
||||
const keydownHandlers: KeydownHandler[] = [];
|
||||
ee.on("toggleVariable", (variable: Variable) => {
|
||||
toggleVariableHandlers.forEach((handler) => handler(variable));
|
||||
});
|
||||
ee.on("keydown", async (key: string) => {
|
||||
await Promise.all(keydownHandlers.map((handler) => handler(key)));
|
||||
});
|
||||
const graphEe = React.useRef(new TypedEmitter<GraphMessage>());
|
||||
const eventListEe = React.useRef(new TypedEmitter<EventListMessage>());
|
||||
const globalEe = React.useRef(new TypedEmitter<GlobalMessage>());
|
||||
|
||||
engine.stepTo(engine.lastEventIndex());
|
||||
const subs = engine.subsSnapshot();
|
||||
const [subsTop, setSubsTop] = useState<SubsSnapshot | undefined>(undefined);
|
||||
const [subsBot, setSubsBot] = useState<SubsSnapshot | undefined>(undefined);
|
||||
|
||||
// _setEpoch to be used in the future!
|
||||
const [epoch, _setEpoch] = useState(subs.epoch);
|
||||
useEffect(() => {
|
||||
globalEe.current.on("loadEpoch", (epoch, view) => {
|
||||
switch (view) {
|
||||
case LoadEpochView.Top: {
|
||||
setSubsTop(engine.current.stepToSnapshot(epoch));
|
||||
break;
|
||||
}
|
||||
case LoadEpochView.Bot: {
|
||||
setSubsBot(engine.current.stepToSnapshot(epoch));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertExhaustive(view);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectedEpochs = [subsTop?.epoch, subsBot?.epoch]
|
||||
.filter((x): x is EventEpoch => x !== undefined)
|
||||
.sort();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col md:flex-row gap-0 w-full h-full"
|
||||
onKeyDown={(e) => {
|
||||
ee.emit("keydown", e.key);
|
||||
graphEe.current.emit("keydown", e.key);
|
||||
}}
|
||||
>
|
||||
<div className="font-mono mt-2 text-lg md:flex-1 overflow-scroll">
|
||||
<div className="font-mono mt-2 text-lg md:flex-1 overflow-x-hidden overflow-y-scroll">
|
||||
<EventList
|
||||
engine={engine}
|
||||
root
|
||||
engine={engine.current}
|
||||
depth={0}
|
||||
events={events}
|
||||
toggleVariableVis={(variable: Variable) =>
|
||||
ee.emit("toggleVariable", variable)
|
||||
}
|
||||
currentEpoch={epoch}
|
||||
graphEe={graphEe.current}
|
||||
eventListEe={eventListEe.current}
|
||||
globalEe={globalEe.current}
|
||||
selectedEpochs={selectedEpochs}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-h-[50%] h-full">
|
||||
<VariablesGraph
|
||||
subs={subs}
|
||||
onVariable={(handler) => {
|
||||
toggleVariableHandlers.push(handler);
|
||||
}}
|
||||
onKeydown={(handler) => {
|
||||
keydownHandlers.push(handler);
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1 min-h-[50%] h-full flex flex-col place-content-center shadow">
|
||||
{selectedEpochs.length === 0 && (
|
||||
<span className="text-center">
|
||||
<span className="p-2 border rounded-md bg-gray-200 inline-block">
|
||||
Select an event to view.
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{subsTop !== undefined && (
|
||||
<VariablesGraphView
|
||||
subs={subsTop}
|
||||
graphEe={graphEe.current}
|
||||
eventListEe={eventListEe.current}
|
||||
/>
|
||||
)}
|
||||
{/* */}
|
||||
{subsBot !== undefined && (
|
||||
<VariablesGraphView
|
||||
subs={subsBot}
|
||||
graphEe={graphEe.current}
|
||||
eventListEe={eventListEe.current}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface VariablesGraphViewProps {
|
||||
subs: SubsSnapshot;
|
||||
graphEe: TypedEmitter<GraphMessage>;
|
||||
eventListEe: TypedEmitter<EventListMessage>;
|
||||
}
|
||||
|
||||
function VariablesGraphView({
|
||||
subs,
|
||||
graphEe,
|
||||
eventListEe,
|
||||
}: VariablesGraphViewProps): JSX.Element {
|
||||
return (
|
||||
<VariablesGraph subs={subs} graphEe={graphEe} eventListEe={eventListEe} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -83,6 +83,11 @@ export class Engine {
|
|||
}
|
||||
}
|
||||
|
||||
stepToSnapshot(eventIndex: EventEpoch): SubsSnapshot {
|
||||
this.stepTo(eventIndex);
|
||||
return this.subsSnapshot();
|
||||
}
|
||||
|
||||
get subs(): Readonly<Subs> {
|
||||
return this.#subs;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import type { TypedEmitter } from "tiny-typed-emitter";
|
||||
|
||||
type Events<Name extends string, T> = {
|
||||
[K in Name]: (value: T) => void;
|
||||
};
|
||||
|
||||
interface UseFocusOutlineEventProps<Name extends string, T> {
|
||||
value: T;
|
||||
ee: TypedEmitter<Events<Name, T>>;
|
||||
event: Name;
|
||||
defaultIsOutlined?: boolean;
|
||||
}
|
||||
|
||||
export function useFocusOutlineEvent<Name extends string, T>({
|
||||
value,
|
||||
ee,
|
||||
event,
|
||||
defaultIsOutlined = false,
|
||||
}: UseFocusOutlineEventProps<Name, T>) {
|
||||
const [isOutlined, setIsOutlined] = useState(defaultIsOutlined);
|
||||
|
||||
useEffect(() => {
|
||||
ee.on(event, (focusValue: T) => {
|
||||
if (focusValue !== value) return;
|
||||
setIsOutlined(true);
|
||||
});
|
||||
}, [ee, event, value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOutlined) return;
|
||||
const timer = setTimeout(() => {
|
||||
setIsOutlined(false);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [isOutlined]);
|
||||
|
||||
return isOutlined;
|
||||
}
|
|
@ -1,3 +1,17 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.outline-event-col-1 {
|
||||
box-shadow: inset 2px 0px 0px #3B82F6, /* Left */
|
||||
inset -2px 0px 0px transparent, /* Right */
|
||||
inset 0px 2px 0px #3B82F6, /* Bottom */
|
||||
inset 0px -2px 0px #3B82F6; /* Top */
|
||||
}
|
||||
|
||||
.outline-event-col-3 {
|
||||
box-shadow: inset 2px 0px 0px transparent, /* Left */
|
||||
inset -2px 0px 0px #3B82F6, /* Right */
|
||||
inset 0px 2px 0px #3B82F6, /* Bottom */
|
||||
inset 0px -2px 0px #3B82F6; /* Top */
|
||||
}
|
||||
|
|
25
crates/compiler/checkmate/www/src/utils/events.ts
Normal file
25
crates/compiler/checkmate/www/src/utils/events.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { EventEpoch } from "../engine/engine";
|
||||
import { Variable } from "../schema";
|
||||
|
||||
export interface VariableMessage {
|
||||
focus: (variable: Variable) => void;
|
||||
}
|
||||
|
||||
export interface GraphMessage {
|
||||
focusEpoch: (epoch: EventEpoch) => void;
|
||||
focusVariable: (variable: Variable) => void;
|
||||
keydown: (key: string) => void;
|
||||
}
|
||||
|
||||
export interface EventListMessage {
|
||||
focusEpoch: (epoch: EventEpoch) => void;
|
||||
}
|
||||
|
||||
export enum LoadEpochView {
|
||||
Top,
|
||||
Bot,
|
||||
}
|
||||
|
||||
export interface GlobalMessage {
|
||||
loadEpoch: (epoch: EventEpoch, view: LoadEpochView) => void;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue