import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useState } from "react"; import { TimerElapseEvent, Timers } from "../statecharts/runtime_types"; import { getSimTime, setPaused, setRealtime, TimeMode } from "../statecharts/time"; import { InsertMode } from "../VisualEditor/VisualEditor"; import { About } from "./About"; import { EditHistory, TraceState } from "./App"; import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo"; import { UndoRedoButtons } from "./TopPanel/UndoRedoButtons"; import { ZoomButtons } from "./TopPanel/ZoomButtons"; import { formatTime } from "./util"; import AccessAlarmIcon from '@mui/icons-material/AccessAlarm'; import CachedIcon from '@mui/icons-material/Cached'; import InfoOutlineIcon from '@mui/icons-material/InfoOutline'; import KeyboardIcon from '@mui/icons-material/Keyboard'; import PauseIcon from '@mui/icons-material/Pause'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import SkipNextIcon from '@mui/icons-material/SkipNext'; import StopIcon from '@mui/icons-material/Stop'; import { InsertModes } from "./TopPanel/InsertModes"; import { usePersistentState } from "@/util/persistent_state"; export type TopPanelProps = { trace: TraceState | null, time: TimeMode, setTime: Dispatch>, onUndo: () => void, onRedo: () => void, onInit: () => void, onClear: () => void, onBack: () => void, insertMode: InsertMode, setInsertMode: Dispatch>, setModal: Dispatch>, zoom: number, setZoom: Dispatch>, showKeys: boolean, setShowKeys: Dispatch>, editHistory: EditHistory, } const ShortCutShowKeys = ~; export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}: TopPanelProps) { const [displayTime, setDisplayTime] = useState("0.000"); const [timescale, setTimescale] = usePersistentState("timescale", 1); const config = trace && trace.trace[trace.idx]; const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden; const updateDisplayedTime = useCallback(() => { const now = Math.round(performance.now()); const timeMs = getSimTime(time, now); setDisplayTime(formatTime(timeMs)); }, [time, setDisplayTime]); useEffect(() => { // This has no effect on statechart execution. In between events, the statechart is doing nothing. However, by updating the displayed time, we give the illusion of continuous progress. const interval = setInterval(() => { updateDisplayedTime(); }, 43); // every X ms -> we want a value that makes the numbers 'dance' while not using too much CPU return () => { clearInterval(interval); } }, [time, updateDisplayedTime]); const onChangePaused = useCallback((paused: boolean, wallclktime: number) => { setTime(time => { if (paused) { return setPaused(time, wallclktime); } else { return setRealtime(time, timescale, wallclktime); } }); updateDisplayedTime(); }, [setTime, timescale, updateDisplayedTime]); const onTimeScaleChange = useCallback((newValue: string, wallclktime: number) => { const asFloat = parseFloat(newValue); if (Number.isNaN(asFloat)) { return; } const maxed = Math.min(asFloat, 64); const mined = Math.max(maxed, 1/64); setTimescale(mined); setTime(time => { if (time.kind === "paused") { return time; } else { return setRealtime(time, mined, wallclktime); } }); }, [setTime, setTimescale]); // timestamp of next timed transition, in simulated time const timers: Timers = config?.kind === "bigstep" && config.environment.get("_timers") || []; const nextTimedTransition: [number, TimerElapseEvent] | undefined = timers[0]; const onSkip = useCallback(() => { const now = Math.round(performance.now()); if (nextTimedTransition) { setTime(time => { if (time.kind === "paused") { return {kind: "paused", simtime: nextTimedTransition[0]}; } else { return {kind: "realtime", scale: time.scale, since: {simtime: nextTimedTransition[0], wallclktime: now}}; } }); } }, [nextTimedTransition, setTime]); const onSlower = useCallback(() => { onTimeScaleChange((timescale/2).toString(), Math.round(performance.now())); }, [onTimeScaleChange, timescale]); const onFaster = useCallback(() => { onTimeScaleChange((timescale*2).toString(), Math.round(performance.now())); }, [onTimeScaleChange, timescale]); useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (!e.ctrlKey) { if (e.key === " ") { e.preventDefault(); if (config) { onChangePaused(time.kind !== "paused", Math.round(performance.now())); } }; if (e.key === "i") { e.preventDefault(); onInit(); } if (e.key === "c") { e.preventDefault(); onClear(); } if (e.key === "Tab") { if (trace === null) { onInit(); } else { onSkip(); } e.preventDefault(); } if (e.key === "s") { e.preventDefault(); onSlower(); } if (e.key === "f") { e.preventDefault(); onFaster(); } if (e.key === "`") { e.preventDefault(); setShowKeys(show => !show); } if (e.key === "Backspace") { e.preventDefault(); onBack(); } } else { // ctrl is down if (e.key === "z") { e.preventDefault(); onUndo(); } if (e.key === "Z") { e.preventDefault(); onRedo(); } } }; window.addEventListener("keydown", onKeyDown); return () => { window.removeEventListener("keydown", onKeyDown); }; }, [trace, config, time, onInit, timescale, onChangePaused, setShowKeys, onUndo, onRedo, onSlower, onFaster, onSkip, onBack, onClear]); return
{/* shortcuts / about */}
{/* zoom */}
{/* undo / redo */}
{/* insert rountangle / arrow / ... */}
{/* execution */}
{/* init / clear */} I}> C}>   {/* pause / real time */} Space toggles}>
{/* speed */}
  S}> onTimeScaleChange(e.target.value, Math.round(performance.now()))}/> F}>
{/* time, next */}
 
  Tab}>
; });