Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions src/pages/edit/Editor/components/ValueEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Mode, string> = {
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 (
<Stack sx={{ height: "100%", gap: 0.5 }}>
<Stack direction="row" sx={{ gap: 0.5 }}>
<ToggleButtonGroup
value={props.mode}
onChange={(_, value) => {
if (!Object.hasOwn(modes, value)) return;
changeMode(value as Mode);
}}
exclusive
size="small"
sx={{ flex: 1 }}
>
{Object.entries(modes).map(([mode, label]) => (
<ToggleButton key={mode} value={mode} sx={{ flex: 1, p: 0.25 }}>
{label}
</ToggleButton>
))}
</ToggleButtonGroup>
{!props.readOnly && (
<Button
variant="outlined"
size="small"
disabled={!newValue}
onClick={() => newValue && props.onChange(newValue)}
sx={{ py: 0.25 }}
>
Apply
</Button>
)}
</Stack>
<Editor
value={text}
onChange={(e) => setText(e.target.value)}
spellCheck={false}
sx={{ borderColor: newValue ? "divider" : "error.main" }}
readOnly={props.readOnly ?? false}
/>
</Stack>
);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -23,7 +23,10 @@ function padBits(bits: SimulationValue, unit: number): boolean[] {
return [...Array<boolean>(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("");
Expand All @@ -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 {
Expand All @@ -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<boolean>(-overflow).fill(false), ...value];
}
if (value.slice(0, overflow).some((bit) => bit)) return null;
return value.slice(overflow);
}
78 changes: 9 additions & 69 deletions src/pages/edit/Editor/renderer/Node/components/Const/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Mode, string> = {
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,
Expand All @@ -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 (
<>
<CCComponentEditorRendererNodeDefaultRenderer {...props} />
Expand All @@ -67,32 +26,13 @@ export function CCComponentEditorRendererNodeConstRenderer(
height={props.geometry.rect.size.y - constants.padding * 2}
onPointerDown={(e) => e.stopPropagation()}
>
<Stack sx={{ height: "100%", gap: 0.5 }}>
<ToggleButtonGroup
value={config.mode}
onChange={(_, value) => {
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]) => (
<ToggleButton key={mode} value={mode} sx={{ flex: 1, p: 0.25 }}>
{label}
</ToggleButton>
))}
</ToggleButtonGroup>
<Editor
value={text}
onChange={(e) => changeText(e.target.value)}
spellCheck={false}
sx={{ borderColor: isInvalid ? "error.main" : "divider" }}
disabled={editorMode !== "edit"}
/>
</Stack>
<CCComponentEditorValueEditor
value={config.data}
onChange={(data) => updateConfig({ ...config, data })}
mode={config.mode}
onModeChange={(mode) => updateConfig({ ...config, mode })}
readOnly={editorMode !== "edit"}
/>
</foreignObject>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function CCComponentEditorRendererNodeInterfaceRendererRenameButton(
setNewName(props.name);
}}
>
<Edit sx={{ width: "10px", height: "12px" }} />
<Edit sx={{ width: "10px", height: "10px" }} />
</IconButton>
<Popover
anchorEl={anchorEl}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Edit } from "@mui/icons-material";
import { IconButton, Popover } from "@mui/material";
import { useState } from "react";
import type { CCIntrinsicComponentConstSpecConfigViewMode as Mode } from "../../../../../../../../store/intrinsics/types";
import type { SimulationValue } from "../../../../../../../../store/simulation";
import { CCComponentEditorValueEditor } from "../../../../../components/ValueEditor";

type Props = {
value: SimulationValue;
onChange: (value: SimulationValue) => void;
};

export function CCComponentEditorRendererNodeInterfaceRendererValueEditButton(
props: Props,
) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const [mode, setMode] = useState<Mode>("hex");

return (
<>
<IconButton
sx={{ width: "14px", height: "14px" }}
// Prevent the drag behavior of the parent node when clicking the button
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
setAnchorEl(e.currentTarget);
}}
>
<Edit sx={{ width: "10px", height: "10px" }} />
</IconButton>
<Popover
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
slotProps={{
root: { onPointerDown: (e) => e.stopPropagation() },
paper: { sx: { width: "300px", height: "120px", p: 1 } },
}}
>
<CCComponentEditorValueEditor
value={props.value}
onChange={(value) => {
props.onChange(value);
setAnchorEl(null);
}}
mode={mode}
onModeChange={setMode}
bitWidth={props.value.length}
/>
</Popover>
</>
);
}
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -22,18 +20,16 @@ export function CCComponentEditorRendererNodeInterfaceRendererValue({
onChange,
}: Props) {
if (value.length > maxBitWidthToShowBits) {
const label = stringifySimulationValue(value);
return onChange ? (
<button
type="button"
className={styles.text}
onPointerDown={(e) => e.stopPropagation()}
onClick={() => onChange(wrappingIncrementSimulationValue(value))}
>
{label}
</button>
) : (
<span className={styles.text}>{label}</span>
return (
<span className={styles.text}>
{stringifySimulationValue(value)}
{onChange && (
<CCComponentEditorRendererNodeInterfaceRendererValueEditButton
value={value}
onChange={onChange}
/>
)}
</span>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
14 changes: 0 additions & 14 deletions src/store/simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
Loading