better UI

This commit is contained in:
Joeri Exelmans 2025-10-20 16:29:48 +02:00
parent 44fb8726ca
commit 1f9379df7f
16 changed files with 440 additions and 248 deletions

View file

@ -1,4 +1,4 @@
import { ReactElement, useEffect, useRef, useState } from "react";
import { Dispatch, ReactElement, SetStateAction, useEffect, useRef, useState } from "react";
import { emptyStatechart, Statechart } from "../statecharts/abstract_syntax";
import { handleInputEvent, initialize } from "../statecharts/interpreter";
@ -9,26 +9,73 @@ import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
import "../index.css";
import "./App.css";
import { Box, Stack } from "@mui/material";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import { TopPanel } from "./TopPanel";
import { RTHistory } from "./RTHistory";
import { ShowAST, ShowOutputEvents } from "./ShowAST";
import { ShowAST, ShowInputEvents, ShowOutputEvents } from "./ShowAST";
import { TraceableError } from "../statecharts/parser";
import { getKeyHandler } from "./shortcut_handler";
import { BottomPanel } from "./BottomPanel";
import { emptyState, VisualEditorState } from "@/statecharts/concrete_syntax";
import { usePersistentState } from "@/util/persistent_state";
type EditHistory = {
current: VisualEditorState,
history: VisualEditorState[],
future: VisualEditorState[],
}
export function App() {
const [mode, setMode] = useState<InsertMode>("and");
const [historyState, setHistoryState] = useState<EditHistory>({current: emptyState, history: [], future: []});
const [ast, setAST] = useState<Statechart>(emptyStatechart);
const [errors, setErrors] = useState<TraceableError[]>([]);
const [rt, setRT] = useState<BigStep[]>([]);
const [rtIdx, setRTIdx] = useState<number|undefined>();
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
const [modal, setModal] = useState<ReactElement|null>(null);
const editorState = historyState.current;
const setEditorState = (cb: (value: VisualEditorState) => VisualEditorState) => {
setHistoryState(historyState => ({...historyState, current: cb(historyState.current)}));
}
const refRightSideBar = useRef<HTMLDivElement>(null);
function makeCheckPoint() {
setHistoryState(historyState => ({
...historyState,
history: [...historyState.history, historyState.current],
future: [],
}));
}
function onUndo() {
setHistoryState(historyState => {
if (historyState.history.length === 0) {
return historyState; // no change
}
return {
current: historyState.history.at(-1)!,
history: historyState.history.slice(0,-1),
future: [...historyState.future, historyState.current],
}
})
}
function onRedo() {
setHistoryState(historyState => {
if (historyState.future.length === 0) {
return historyState; // no change
}
return {
current: historyState.future.at(-1)!,
history: [...historyState.history, historyState.current],
future: historyState.future.slice(0,-1),
}
});
}
function onInit() {
const config = initialize(ast);
setRT([{inputEvent: null, simtime: 0, ...config}]);
@ -140,13 +187,16 @@ export function App() {
// return state && state.parent?.kind !== "and";
// })) || new Set();
const highlightActive = (rtIdx === undefined) ? new Set() : rt[rtIdx].mode;
const highlightActive: Set<string> = (rtIdx === undefined) ? new Set() : rt[rtIdx].mode;
const highlightTransitions = (rtIdx === undefined) ? [] : rt[rtIdx].firedTransitions;
console.log(ast);
const [showStateTree, setShowStateTree] = usePersistentState("showStateTree", true);
const [showInputEvents, setShowInputEvents] = usePersistentState("showInputEvents", true);
const [showOutputEvents, setShowOutputEvents] = usePersistentState("showOutputEvents", true);
return <>
{/* Modal dialog */}
{modal && <div
className="modalOuter"
@ -157,56 +207,83 @@ export function App() {
</span>
</div>
</div>}
<Stack sx={{height:'100vh'}}>
{/* Top bar */}
<Box
sx={{
display: "flex",
borderBottom: 1,
borderColor: "divider",
alignItems: 'center',
flex: '0 0 content',
}}>
<TopPanel
rt={rtIdx === undefined ? undefined : rt[rtIdx]}
{...{rtIdx, ast, time, setTime, onInit, onClear, onRaise, onBack, mode, setMode, setModal}}
/>
</Box>
{/* Everything below the top bar */}
<Stack direction="row" sx={{
overflow: 'auto',
}}>
<Stack sx={{height:'100%'}}>
<Stack direction="row" sx={{flexGrow:1, overflow: "auto"}}>
{/* main */}
<Box sx={{
flexGrow:1,
overflow:'auto',
}}>
<VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions, setModal}}/>
{/* Left: top bar and main editor */}
<Box sx={{flexGrow:1, overflow: "auto"}}>
<Stack sx={{height:'100%'}}>
{/* Top bar */}
<Box sx={{
display: "flex",
borderBottom: 1,
borderColor: "divider",
alignItems: 'center',
flex: '0 0 content',
}}>
<TopPanel
rt={rtIdx === undefined ? undefined : rt[rtIdx]}
{...{rtIdx, ast, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, mode, setMode, setModal}}
/>
</Box>
{/* Below the top bar: Editor */}
<Box sx={{flexGrow:1, overflow: "auto"}}>
<VisualEditor {...{state: editorState, setState: setEditorState, ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint}}/>
</Box>
</Stack>
</Box>
{/* right sidebar */}
<Box
sx={{
borderLeft: 1,
borderColor: "divider",
flex: '0 0 content',
overflowY: "auto",
overflowX: "visible",
maxWidth: 'min(300px, 30vw)',
}}>
<ShowAST {...{...ast, rt: rt.at(rtIdx!), highlightActive}}/>
<ShowOutputEvents outputEvents={ast.outputEvents}/>
<br/>
<div ref={refRightSideBar}>
<RTHistory {...{ast, rt, rtIdx, setTime, setRTIdx, refRightSideBar}}/>
</div>
</Box>
{/* Right: sidebar */}
<Box sx={{
borderLeft: 1,
borderColor: "divider",
flex: '0 0 content',
overflowY: "auto",
overflowX: "visible",
maxWidth: 'min(300px, 30vw)',
}}>
<Stack sx={{height:'100%'}}>
<Box className="onTop" sx={{flex: '0 0 content', backgroundColor: ''}}>
<details open={showStateTree}
onToggle={e => setShowStateTree(e.newState === "open")}>
<summary>state tree</summary>
<ul>
<ShowAST {...{...ast, rt: rt.at(rtIdx!), highlightActive}}/>
</ul>
</details>
<hr/>
<details open={showInputEvents}
onToggle={e => setShowInputEvents(e.newState === "open")}>
<summary>input events</summary>
<ShowInputEvents inputEvents={ast.inputEvents} onRaise={onRaise} disabled={rtIdx===undefined}/>
</details>
<hr/>
<details open={showOutputEvents}
onToggle={e => setShowOutputEvents(e.newState === "open")}>
<summary>output events</summary>
<ShowOutputEvents outputEvents={ast.outputEvents}/>
</details>
</Box>
<Box sx={{
flexGrow:1,
overflow:'auto',
minHeight: '75%', // <-- allows us to always scroll down the sidebar far enough such that the execution history is enough in view
}}>
<Box sx={{ height: '100%'}}>
<div ref={refRightSideBar}>
<RTHistory {...{ast, rt, rtIdx, setTime, setRTIdx, refRightSideBar}}/>
</div>
</Box>
</Box>
</Stack>
</Box>
</Stack>
<Box sx={{
flex: '0 0 content',
}}>
{/* Bottom panel */}
<Box sx={{flex: '0 0 content'}}>
<BottomPanel {...{errors}}/>
</Box>
</Stack>