diff --git a/src/App/App.tsx b/src/App/App.tsx index b940e9c..520eb62 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useEffect, useMemo, useRef, useState } from "react"; +import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { emptyStatechart, Statechart, Transition } from "../statecharts/abstract_syntax"; import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter"; @@ -85,9 +85,9 @@ export function App() { const plant = plants.find(([pn, p]) => pn === plantName)![1]; const editorState = historyState.current; - const setEditorState = (cb: (value: VisualEditorState) => VisualEditorState) => { + const setEditorState = useCallback((cb: (value: VisualEditorState) => VisualEditorState) => { setHistoryState(historyState => ({...historyState, current: cb(historyState.current)})); - } + }, [setHistoryState]); const refRightSideBar = useRef(null); diff --git a/src/App/KeyInfo.tsx b/src/App/KeyInfo.tsx index 40e8617..6609d51 100644 --- a/src/App/KeyInfo.tsx +++ b/src/App/KeyInfo.tsx @@ -1,6 +1,6 @@ -import { Box, Stack } from "@mui/material"; +import { memo, PropsWithChildren, ReactElement } from "react"; -export function KeyInfoVisible(props: {keyInfo, children, horizontal?: boolean}) { +export const KeyInfoVisible = memo(function KeyInfoVisible(props: PropsWithChildren<{keyInfo: ReactElement, horizontal?: boolean}>) { return
{/* */}
@@ -11,8 +11,8 @@ export function KeyInfoVisible(props: {keyInfo, children, horizontal?: boolean})
{/*
*/}
-} +}); -export function KeyInfoHidden(props: {children}) { +export const KeyInfoHidden = memo(function KeyInfoHidden(props: PropsWithChildren<{}>) { return <>{props.children}; -} +}); diff --git a/src/App/ShowAST.tsx b/src/App/ShowAST.tsx index 059d63f..b0e6139 100644 --- a/src/App/ShowAST.tsx +++ b/src/App/ShowAST.tsx @@ -32,7 +32,7 @@ export function ShowAction(props: {action: Action}) { } } -export function ShowAST(props: {root: ConcreteState | PseudoState, transitions: Map, trace: TraceState | null, highlightActive: Set}) { +export const ShowAST = memo(function ShowAST(props: {root: ConcreteState | PseudoState, transitions: Map, trace: TraceState | null, highlightActive: Set}) { const description = stateDescription(props.root); // const outgoing = props.transitions.get(props.root.uid) || []; @@ -69,11 +69,11 @@ export function ShowAST(props: {root: ConcreteState | PseudoState, transitions: // outgoing.map(transition => <> 
) // } */} // ; -} +}); import BoltIcon from '@mui/icons-material/Bolt'; import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo"; -import { useEffect } from "react"; +import { memo, useEffect } from "react"; import { TraceState } from "./App"; export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean, showKeys: boolean}) { diff --git a/src/App/TopPanel.tsx b/src/App/TopPanel.tsx index 0c63674..3d0f076 100644 --- a/src/App/TopPanel.tsx +++ b/src/App/TopPanel.tsx @@ -1,4 +1,4 @@ -import { Dispatch, ReactElement, SetStateAction, useEffect, useState } from "react"; +import { Dispatch, memo, ReactElement, SetStateAction, useEffect, useState } from "react"; import { BigStep, TimerElapseEvent, Timers } from "../statecharts/runtime_types"; import { getSimTime, setPaused, setRealtime, TimeMode } from "../statecharts/time"; import { Statechart } from "../statecharts/abstract_syntax"; @@ -11,12 +11,8 @@ import SkipNextIcon from '@mui/icons-material/SkipNext'; import SkipPreviousIcon from '@mui/icons-material/SkipPrevious';import TrendingFlatIcon from '@mui/icons-material/TrendingFlat'; import AccessAlarmIcon from '@mui/icons-material/AccessAlarm'; import StopIcon from '@mui/icons-material/Stop'; -import UndoIcon from '@mui/icons-material/Undo'; -import RedoIcon from '@mui/icons-material/Redo'; import InfoOutlineIcon from '@mui/icons-material/InfoOutline'; import KeyboardIcon from '@mui/icons-material/Keyboard'; -import ZoomInIcon from '@mui/icons-material/ZoomIn'; -import ZoomOutIcon from '@mui/icons-material/ZoomOut'; import { formatTime } from "./util"; import { InsertMode } from "../VisualEditor/VisualEditor"; @@ -26,20 +22,20 @@ import { usePersistentState } from "@/util/persistent_state"; import { RountangleIcon, PseudoStateIcon, HistoryIcon } from "./Icons"; import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from "@/VisualEditor/parameters"; import { EditHistory, TraceState } from "./App"; +import { ZoomButtons } from "./TopPanel/ZoomButtons"; +import { UndoRedoButtons } from "./TopPanel/UndoRedoButtons"; export type TopPanelProps = { trace: TraceState | null, - // rt?: BigStep, - // rtIdx?: number, time: TimeMode, setTime: Dispatch>, onUndo: () => void, onRedo: () => void, onInit: () => void, onClear: () => void, - onRaise: (e: string, p: any) => void, + // onRaise: (e: string, p: any) => void, onBack: () => void, - ast: Statechart, + // ast: Statechart, mode: InsertMode, setMode: Dispatch>, setModal: Dispatch>, @@ -50,7 +46,7 @@ export type TopPanelProps = { history: EditHistory, } -export function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, ast, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys, history}: TopPanelProps) { +export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onBack, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys, history}: TopPanelProps) { const [displayTime, setDisplayTime] = useState("0.000"); const [timescale, setTimescale] = useState(1); @@ -111,14 +107,6 @@ export function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, e.preventDefault(); onRedo(); } - if (e.key === "+") { - e.preventDefault(); - onZoomIn(); - } - if (e.key === "-") { - e.preventDefault(); - onZoomOut(); - } } }; window.addEventListener("keydown", onKeyDown); @@ -143,12 +131,6 @@ export function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, } }, [time]); - function onZoomIn() { - setZoom(zoom => Math.min(zoom * ZOOM_STEP, ZOOM_MAX)); - } - function onZoomOut() { - setZoom(zoom => Math.max(zoom / ZOOM_STEP, ZOOM_MIN)); - } function onChangePaused(paused: boolean, wallclktime: number) { setTime(time => { @@ -218,24 +200,13 @@ export function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, {/* zoom */}
- Ctrl+-}> - - - - Ctrl++}> - - +
{/* undo / redo */}
- Ctrl+Z}> - - - Ctrl+Shift+Z}> - - +
@@ -349,4 +320,4 @@ export function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, */} ; -} +}); diff --git a/src/App/TopPanel/UndoRedoButtons.tsx b/src/App/TopPanel/UndoRedoButtons.tsx new file mode 100644 index 0000000..de8d3be --- /dev/null +++ b/src/App/TopPanel/UndoRedoButtons.tsx @@ -0,0 +1,17 @@ +import { memo } from "react"; +import { KeyInfoHidden, KeyInfoVisible } from "../KeyInfo"; + +import UndoIcon from '@mui/icons-material/Undo'; +import RedoIcon from '@mui/icons-material/Redo'; + +export const UndoRedoButtons = memo(function UndoRedoButtons({showKeys, onUndo, onRedo, historyLength, futureLength}: {showKeys: boolean, onUndo: () => void, onRedo: () => void, historyLength: number, futureLength: number}) { + const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden; + return <> + Ctrl+Z}> + + + Ctrl+Shift+Z}> + + + ; +}); diff --git a/src/App/TopPanel/ZoomButtons.tsx b/src/App/TopPanel/ZoomButtons.tsx new file mode 100644 index 0000000..0930b18 --- /dev/null +++ b/src/App/TopPanel/ZoomButtons.tsx @@ -0,0 +1,47 @@ +import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from "@/VisualEditor/parameters"; +import { Dispatch, memo, SetStateAction, useEffect } from "react"; +import { KeyInfoHidden, KeyInfoVisible } from "../KeyInfo"; + +import ZoomInIcon from '@mui/icons-material/ZoomIn'; +import ZoomOutIcon from '@mui/icons-material/ZoomOut'; + +export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}: {showKeys: boolean, zoom: number, setZoom: Dispatch>}) { + + const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden; + + function onZoomIn() { + setZoom(zoom => Math.min(zoom * ZOOM_STEP, ZOOM_MAX)); + } + function onZoomOut() { + setZoom(zoom => Math.max(zoom / ZOOM_STEP, ZOOM_MIN)); + } + + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey) { + if (e.key === "+") { + e.preventDefault(); + onZoomIn(); + } + if (e.key === "-") { + e.preventDefault(); + onZoomOut(); + } + } + }; + window.addEventListener("keydown", onKeyDown); + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + }, []); + + return <> + Ctrl+-}> + + + + Ctrl++}> + + + ; +}); diff --git a/src/VisualEditor/ArrowSVG.tsx b/src/VisualEditor/ArrowSVG.tsx index b77882c..a998a99 100644 --- a/src/VisualEditor/ArrowSVG.tsx +++ b/src/VisualEditor/ArrowSVG.tsx @@ -1,9 +1,10 @@ +import { memo } from "react"; import { Arrow } from "../statecharts/concrete_syntax"; import { ArcDirection, euclideanDistance } from "./geometry"; import { CORNER_HELPER_RADIUS } from "./parameters"; -export function ArrowSVG(props: { arrow: Arrow; selected: string[]; errors: string[]; highlight: boolean; fired: boolean; arc: ArcDirection; initialMarker: boolean }) { +export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: string[]; errors: string[]; highlight: boolean; fired: boolean; arc: ArcDirection; initialMarker: boolean }) { const { start, end, uid } = props.arrow; const radius = euclideanDistance(start, end) / 1.6; let largeArc = "1"; @@ -78,4 +79,4 @@ export function ArrowSVG(props: { arrow: Arrow; selected: string[]; errors: stri data-parts="end" />} ; -} +}); diff --git a/src/VisualEditor/DiamondSVG.tsx b/src/VisualEditor/DiamondSVG.tsx index 608c1f8..5052ba8 100644 --- a/src/VisualEditor/DiamondSVG.tsx +++ b/src/VisualEditor/DiamondSVG.tsx @@ -1,9 +1,10 @@ import { Diamond, RountanglePart } from "@/statecharts/concrete_syntax"; import { rountangleMinSize } from "./VisualEditor"; -import { Rect2D, Vec2D } from "./geometry"; +import { Vec2D } from "./geometry"; import { RectHelper } from "./RectHelpers"; +import { memo } from "react"; -export function DiamondShape(props: {size: Vec2D, extraAttrs: object}) { +export const DiamondShape = memo(function DiamondShape(props: {size: Vec2D, extraAttrs: object}) { const minSize = rountangleMinSize(props.size); return ; -} +}); -export function DiamondSVG(props: { diamond: Diamond; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) { +export const DiamondSVG = memo(function DiamondSVG(props: { diamond: Diamond; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) { const minSize = rountangleMinSize(props.diamond.size); const extraAttrs = { className: '' @@ -38,4 +39,4 @@ export function DiamondSVG(props: { diamond: Diamond; selected: string[]; highli ; -} +}); diff --git a/src/VisualEditor/HistorySVG.tsx b/src/VisualEditor/HistorySVG.tsx index 6ca1be1..b6b3d7b 100644 --- a/src/VisualEditor/HistorySVG.tsx +++ b/src/VisualEditor/HistorySVG.tsx @@ -1,7 +1,8 @@ +import { memo } from "react"; import { Vec2D } from "./geometry"; import { HISTORY_RADIUS } from "./parameters"; -export function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"|"deep", selected: boolean, highlight: boolean}) { +export const HistorySVG = memo(function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"|"deep", selected: boolean, highlight: boolean}) { const text = props.kind === "shallow" ? "H" : "H*"; return <> } ; -} +}); diff --git a/src/VisualEditor/RectHelpers.tsx b/src/VisualEditor/RectHelpers.tsx index 06b25c3..6272c5c 100644 --- a/src/VisualEditor/RectHelpers.tsx +++ b/src/VisualEditor/RectHelpers.tsx @@ -1,3 +1,4 @@ +import { memo } from "react"; import { RountanglePart } from "../statecharts/concrete_syntax"; import { Vec2D } from "./geometry"; import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS } from "./parameters"; @@ -11,7 +12,7 @@ function lineGeometryProps(size: Vec2D): [RountanglePart, object][] { ]; } -export function RectHelper(props: { uid: string, size: Vec2D, selected: string[], highlight: RountanglePart[] }) { +export const RectHelper = memo(function RectHelper(props: { uid: string, size: Vec2D, selected: string[], highlight: RountanglePart[] }) { const geomProps = lineGeometryProps(props.size); return <> {geomProps.map(([side, ps]) => @@ -53,4 +54,4 @@ export function RectHelper(props: { uid: string, size: Vec2D, selected: string[] data-uid={props.uid} data-parts="bottom left" /> ; -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/VisualEditor/RountangleSVG.tsx b/src/VisualEditor/RountangleSVG.tsx index 51ca578..278ee14 100644 --- a/src/VisualEditor/RountangleSVG.tsx +++ b/src/VisualEditor/RountangleSVG.tsx @@ -1,10 +1,11 @@ +import { memo } from "react"; import { Rountangle, RountanglePart } from "../statecharts/concrete_syntax"; import { ROUNTANGLE_RADIUS } from "./parameters"; import { RectHelper } from "./RectHelpers"; import { rountangleMinSize } from "./VisualEditor"; -export function RountangleSVG(props: { rountangle: Rountangle; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) { +export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rountangle; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) { const { topLeft, size, uid } = props.rountangle; // always draw a rountangle with a minimum size // during resizing, rountangle can be smaller than this size and even have a negative size, but we don't show it @@ -37,4 +38,4 @@ export function RountangleSVG(props: { rountangle: Rountangle; selected: string[ selected={props.selected} highlight={props.highlight} /> ; -} +}) diff --git a/src/VisualEditor/TextSVG.tsx b/src/VisualEditor/TextSVG.tsx index 87b68cd..b2287a7 100644 --- a/src/VisualEditor/TextSVG.tsx +++ b/src/VisualEditor/TextSVG.tsx @@ -1,9 +1,9 @@ import { TextDialog } from "@/App/TextDialog"; import { TraceableError } from "..//statecharts/parser"; import {Text} from "../statecharts/concrete_syntax"; -import { Dispatch, ReactElement, SetStateAction } from "react"; +import { Dispatch, memo, ReactElement, SetStateAction } from "react"; -export function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (newText: string) => void, setModal: Dispatch>}) { +export const TextSVG = memo(function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (text: Text, newText: string) => void, setModal: Dispatch>}) { const commonProps = { "data-uid": props.text.uid, "data-parts": "text", @@ -37,11 +37,11 @@ export function TextSVG(props: {text: Text, error: TraceableError|undefined, sel onDoubleClick={() => { props.setModal( { if (newText) { - props.onEdit(newText); + props.onEdit(props.text, newText); } }} />) }}> {textNode} {props.text.text} ; -} \ No newline at end of file +}); diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx index 1a9d1a3..fe367ef 100644 --- a/src/VisualEditor/VisualEditor.tsx +++ b/src/VisualEditor/VisualEditor.tsx @@ -1,9 +1,8 @@ -import { ClipboardEvent, Dispatch, ReactElement, SetStateAction, useEffect, useMemo, useRef, useState } from "react"; +import { ClipboardEvent, Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Statechart } from "../statecharts/abstract_syntax"; import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text } from "../statecharts/concrete_syntax"; import { parseStatechart, TraceableError } from "../statecharts/parser"; -import { BigStep } from "../statecharts/runtime_types"; import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "./geometry"; import { MIN_ROUNTANGLE_SIZE } from "./parameters"; import { getBBoxInSvgCoords } from "./svg_helper"; @@ -64,7 +63,7 @@ export type InsertMode = "and"|"or"|"pseudo"|"shallow"|"deep"|"transition"|"text type VisualEditorProps = { state: VisualEditorState, setState: Dispatch<(v:VisualEditorState) => VisualEditorState>, - ast: Statechart, + // ast: Statechart, setAST: Dispatch>, trace: TraceState | null, errors: TraceableError[], @@ -77,7 +76,7 @@ type VisualEditorProps = { zoom: number; }; -export function VisualEditor({state, setState, ast, setAST, trace, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) { +export const VisualEditor = memo(function VisualEditor({state, setState, setAST, trace, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) { const [dragging, setDragging] = useState(false); @@ -642,7 +641,7 @@ export function VisualEditor({state, setState, ast, setAST, trace, errors, setEr } } - function onEditText(text: Text, newText: string) { + const onEditText = useCallback((text: Text, newText: string) => { if (newText === "") { // delete text node setState(state => ({ @@ -666,8 +665,9 @@ export function VisualEditor({state, setState, ast, setAST, trace, errors, setEr }), })); } - } + }, [setState]); + // @ts-ignore const active = trace && trace.trace[trace.idx].mode || new Set(); const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message); @@ -774,14 +774,14 @@ export function VisualEditor({state, setState, ast, setAST, trace, errors, setEr text={txt} selected={Boolean(selection.find(s => s.uid === txt.uid)?.parts?.length)} highlight={textsToHighlight.hasOwnProperty(txt.uid)} - onEdit={newText => onEditText(txt, newText)} + onEdit={onEditText} setModal={setModal} /> })} {selectingState && } ; -} +}); export function rountangleMinSize(size: Vec2D): Vec2D { if (size.x >= 40 && size.y >= 40) { diff --git a/todo.txt b/todo.txt index d506807..b18377b 100644 --- a/todo.txt +++ b/todo.txt @@ -53,6 +53,8 @@ TODO - experimental features: - multiverse execution history + stable tree layout? + https://pub.dev/packages/ploeg_tree_layout - local scopes