diff --git a/src/pages/edit/Editor/components/ValueEditor/index.tsx b/src/pages/edit/Editor/components/ValueEditor/index.tsx new file mode 100644 index 0000000..2e5a5e7 --- /dev/null +++ b/src/pages/edit/Editor/components/ValueEditor/index.tsx @@ -0,0 +1,102 @@ +import { + Button, + Stack, + styled, + ToggleButton, + ToggleButtonGroup, +} from "@mui/material"; +import { useState } from "react"; +import type { CCIntrinsicComponentConstSpecConfigViewMode as Mode } from "../../../../../store/intrinsics/types"; +import type { SimulationValue } from "../../../../../store/simulation"; +import { + fitSimulationValue, + formatSimulationValue, + parseSimulationValue, +} from "./value"; + +const modes: Record = { + binary: "Bin", + hex: "Hex", + "utf-8": "UTF-8", +}; + +const Editor = styled("textarea")(({ theme }) => ({ + padding: theme.spacing(1), + flex: 1, + resize: "none", + borderStyle: "solid", + borderWidth: "1px", + borderRadius: theme.shape.borderRadius, + outline: "none", + fontFamily: "monospace", + wordBreak: "break-all", +})); + +type Props = { + value: SimulationValue; + onChange: (value: SimulationValue) => void; + mode: Mode; + onModeChange: (mode: Mode) => void; + bitWidth?: number; + readOnly?: boolean; +}; + +export function CCComponentEditorValueEditor(props: Props) { + const [text, setText] = useState(() => + formatSimulationValue(props.value, props.mode), + ); + + const parse = (text: string, mode: Mode) => { + const value = parseSimulationValue(text, mode); + return value && props.bitWidth !== undefined + ? fitSimulationValue(value, props.bitWidth) + : value; + }; + const newValue = parse(text, props.mode); + + const changeMode = (mode: Mode) => { + props.onModeChange(mode); + setText(formatSimulationValue(newValue ?? props.value, mode)); + }; + + return ( + + + { + if (!Object.hasOwn(modes, value)) return; + changeMode(value as Mode); + }} + exclusive + size="small" + sx={{ flex: 1 }} + > + {Object.entries(modes).map(([mode, label]) => ( + + {label} + + ))} + + {!props.readOnly && ( + + )} + + setText(e.target.value)} + spellCheck={false} + sx={{ borderColor: newValue ? "divider" : "error.main" }} + readOnly={props.readOnly ?? false} + /> + + ); +} diff --git a/src/pages/edit/Editor/renderer/Node/components/Const/value.ts b/src/pages/edit/Editor/components/ValueEditor/value.ts similarity index 76% rename from src/pages/edit/Editor/renderer/Node/components/Const/value.ts rename to src/pages/edit/Editor/components/ValueEditor/value.ts index 45eb314..06051fc 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Const/value.ts +++ b/src/pages/edit/Editor/components/ValueEditor/value.ts @@ -1,6 +1,6 @@ import { chunk } from "es-toolkit"; -import type { CCIntrinsicComponentConstSpecConfigViewMode as Mode } from "../../../../../../../store/intrinsics/types"; -import type { SimulationValue } from "../../../../../../../store/simulation"; +import type { CCIntrinsicComponentConstSpecConfigViewMode as Mode } from "../../../../../store/intrinsics/types"; +import type { SimulationValue } from "../../../../../store/simulation"; const bitsPerHexDigit = 4; const bitsPerByte = 8; @@ -23,7 +23,10 @@ function padBits(bits: SimulationValue, unit: number): boolean[] { return [...Array(padding).fill(false), ...bits]; } -export function formatConstData(data: SimulationValue, mode: Mode): string { +export function formatSimulationValue( + data: SimulationValue, + mode: Mode, +): string { switch (mode) { case "binary": return data.map((bit) => (bit ? "1" : "0")).join(""); @@ -42,7 +45,7 @@ export function formatConstData(data: SimulationValue, mode: Mode): string { } /** Returns null if `text` is not a valid representation in `mode`. */ -export function parseConstData( +export function parseSimulationValue( text: string, mode: Mode, ): SimulationValue | null { @@ -66,3 +69,15 @@ export function parseConstData( } } } + +export function fitSimulationValue( + value: SimulationValue, + bitWidth: number, +): SimulationValue | null { + const overflow = value.length - bitWidth; + if (overflow <= 0) { + return [...Array(-overflow).fill(false), ...value]; + } + if (value.slice(0, overflow).some((bit) => bit)) return null; + return value.slice(overflow); +} diff --git a/src/pages/edit/Editor/renderer/Node/components/Const/index.tsx b/src/pages/edit/Editor/renderer/Node/components/Const/index.tsx index b7931c0..49a41bd 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Const/index.tsx +++ b/src/pages/edit/Editor/renderer/Node/components/Const/index.tsx @@ -1,33 +1,10 @@ -import { Stack, styled, ToggleButton, ToggleButtonGroup } from "@mui/material"; -import { useState } from "react"; -import type { - CCIntrinsicComponentConstSpec, - CCIntrinsicComponentConstSpecConfigViewMode as Mode, -} from "../../../../../../../store/intrinsics/types"; +import type { CCIntrinsicComponentConstSpec } from "../../../../../../../store/intrinsics/types"; import { useStore } from "../../../../../../../store/react"; +import { CCComponentEditorValueEditor } from "../../../../components/ValueEditor"; import { useComponentEditorStore } from "../../../../store"; import type { CCComponentEditorRendererNodeRendererProps } from "../../types"; import { CCComponentEditorRendererNodeDefaultRenderer } from "../Default"; import { ccComponentEditorRendererNodeConstLayoutConstants as constants } from "./geometry"; -import { formatConstData, parseConstData } from "./value"; - -const modes: Record = { - binary: "Binary", - hex: "Hex", - "utf-8": "UTF-8", -}; - -const Editor = styled("textarea")(({ theme }) => ({ - padding: theme.spacing(1), - flex: 1, - resize: "none", - borderStyle: "solid", - borderWidth: "1px", - borderRadius: theme.shape.borderRadius, - outline: "none", - fontFamily: "monospace", - wordBreak: "break-all", -})); export function CCComponentEditorRendererNodeConstRenderer( props: CCComponentEditorRendererNodeRendererProps, @@ -36,27 +13,9 @@ export function CCComponentEditorRendererNodeConstRenderer( const editorMode = useComponentEditorStore()((state) => state.editorMode); const config = props.node.config as CCIntrinsicComponentConstSpec["config"]; - const [text, setText] = useState(() => - formatConstData(config.data, config.mode), - ); - const [isInvalid, setIsInvalid] = useState(false); - const updateConfig = (newConfig: CCIntrinsicComponentConstSpec["config"]) => store.nodes.update(props.node.id, { config: newConfig }); - const changeMode = (mode: Mode) => { - updateConfig({ ...config, mode }); - setText(formatConstData(config.data, mode)); - setIsInvalid(false); - }; - - const changeText = (newText: string) => { - setText(newText); - const data = parseConstData(newText, config.mode); - setIsInvalid(!data); - if (data) updateConfig({ ...config, data }); - }; - return ( <> @@ -67,32 +26,13 @@ export function CCComponentEditorRendererNodeConstRenderer( height={props.geometry.rect.size.y - constants.padding * 2} onPointerDown={(e) => e.stopPropagation()} > - - { - if (!Object.hasOwn(modes, value)) return; - changeMode(value as Mode); - }} - exclusive - size="small" - sx={{ width: "100%" }} - disabled={editorMode !== "edit"} - > - {Object.entries(modes).map(([mode, label]) => ( - - {label} - - ))} - - changeText(e.target.value)} - spellCheck={false} - sx={{ borderColor: isInvalid ? "error.main" : "divider" }} - disabled={editorMode !== "edit"} - /> - + updateConfig({ ...config, data })} + mode={config.mode} + onModeChange={(mode) => updateConfig({ ...config, mode })} + readOnly={editorMode !== "edit"} + /> ); diff --git a/src/pages/edit/Editor/renderer/Node/components/Interface/RenameButton.tsx b/src/pages/edit/Editor/renderer/Node/components/Interface/RenameButton.tsx index 291f52e..27838c7 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Interface/RenameButton.tsx +++ b/src/pages/edit/Editor/renderer/Node/components/Interface/RenameButton.tsx @@ -33,7 +33,7 @@ export function CCComponentEditorRendererNodeInterfaceRendererRenameButton( setNewName(props.name); }} > - + void; +}; + +export function CCComponentEditorRendererNodeInterfaceRendererValueEditButton( + props: Props, +) { + const [anchorEl, setAnchorEl] = useState(null); + const [mode, setMode] = useState("hex"); + + return ( + <> + e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + setAnchorEl(e.currentTarget); + }} + > + + + setAnchorEl(null)} + slotProps={{ + root: { onPointerDown: (e) => e.stopPropagation() }, + paper: { sx: { width: "300px", height: "120px", p: 1 } }, + }} + > + { + props.onChange(value); + setAnchorEl(null); + }} + mode={mode} + onModeChange={setMode} + bitWidth={props.value.length} + /> + + + ); +} diff --git a/src/pages/edit/Editor/renderer/Node/components/Interface/Value/index.tsx b/src/pages/edit/Editor/renderer/Node/components/Interface/Value/index.tsx index e197d38..e9de385 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Interface/Value/index.tsx +++ b/src/pages/edit/Editor/renderer/Node/components/Interface/Value/index.tsx @@ -1,9 +1,7 @@ import clsx from "clsx"; -import { - type SimulationValue, - wrappingIncrementSimulationValue, -} from "../../../../../../../../store/simulation"; +import type { SimulationValue } from "../../../../../../../../store/simulation"; import { stringifySimulationValue } from "../../../../../store/slices/core"; +import { CCComponentEditorRendererNodeInterfaceRendererValueEditButton } from "./EditButton"; import styles from "./style.module.css"; /** Values up to this bit width are shown bit by bit */ @@ -22,18 +20,16 @@ export function CCComponentEditorRendererNodeInterfaceRendererValue({ onChange, }: Props) { if (value.length > maxBitWidthToShowBits) { - const label = stringifySimulationValue(value); - return onChange ? ( - - ) : ( - {label} + return ( + + {stringifySimulationValue(value)} + {onChange && ( + + )} + ); } diff --git a/src/pages/edit/Editor/renderer/Node/components/Interface/Value/style.module.css b/src/pages/edit/Editor/renderer/Node/components/Interface/Value/style.module.css index 1027f7f..70d2fc7 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Interface/Value/style.module.css +++ b/src/pages/edit/Editor/renderer/Node/components/Interface/Value/style.module.css @@ -1,14 +1,10 @@ +/* The value in hexadecimal, followed by the edit button if editable */ .text { - padding: 0; - border: none; - background: none; - color: inherit; - font: inherit; - text-align: center; - - &:is(button) { - cursor: pointer; - } + display: flex; + align-items: center; + justify-content: center; + gap: 2px; + font-family: monospace; } .bits { diff --git a/src/store/simulation.ts b/src/store/simulation.ts index a9e77c5..d17e82f 100644 --- a/src/store/simulation.ts +++ b/src/store/simulation.ts @@ -15,20 +15,6 @@ import type { CCNodePin, CCNodePinId } from "./nodePin"; export type SimulationValue = boolean[]; -export function wrappingIncrementSimulationValue( - value: SimulationValue, -): SimulationValue { - const result = value.slice(); - for (let i = result.length - 1; i >= 0; i--) { - if (!result[i]) { - result[i] = true; - break; - } - result[i] = false; // carry the increment - } - return result; -} - export type SimulationFrame = { componentId: CCComponentId; nodes: Map<