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
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Edit } from "@mui/icons-material";
import {
Button,
IconButton,
Popover,
Stack,
TextField,
Typography,
} from "@mui/material";
import { useState } from "react";

type Props = {
name: string;
onNameChange: (name: string) => void;
};

export function CCComponentEditorRendererNodeInterfaceRendererRenameButton(
props: Props,
) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const [newName, setNewName] = useState(props.name);
const trimmedNewName = newName.trim();

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);
setNewName(props.name);
}}
>
<Edit sx={{ width: "10px", height: "12px" }} />
</IconButton>
<Popover
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
slotProps={{
root: { onPointerDown: (e) => e.stopPropagation() },
paper: { sx: { width: "300px", p: 2 } },
}}
>
<form
onSubmit={(e) => {
e.preventDefault();
if (!trimmedNewName) return;
props.onNameChange(trimmedNewName);
setAnchorEl(null);
}}
>
<Typography variant="h6" gutterBottom>
Rename Pin
</Typography>
<TextField
value={newName}
onChange={(e) => setNewName(e.target.value)}
size="small"
fullWidth
autoFocus
/>
<Stack direction="row" justifyContent="flex-end" mt={2} gap={1}>
<Button
type="button"
variant="outlined"
size="small"
onClick={() => setAnchorEl(null)}
>
Cancel
</Button>
<Button
type="submit"
variant="contained"
size="small"
disabled={!trimmedNewName}
>
Apply
</Button>
</Stack>
</form>
</Popover>
</>
);
}
84 changes: 47 additions & 37 deletions src/pages/edit/Editor/renderer/Node/components/Interface/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import nullthrows from "nullthrows";
import { ccIntrinsicComponentTypes } from "../../../../../../../store/intrinsics/types";
import { useStore } from "../../../../../../../store/react";
import { useComponentPin } from "../../../../../../../store/react/selectors";
import { wrappingIncrementSimulationValue } from "../../../../../../../store/simulation";
import { useComponentEditorStore } from "../../../../store";
import { stringifySimulationValue } from "../../../../store/slices/core";
import styles from "../../style.module.css";
import type { CCComponentEditorRendererNodeRendererProps } from "../../types";
import { ccComponentEditorRendererNodeInterfaceLayoutConstants as constants } from "./geometry";
import { CCComponentEditorRendererNodeInterfaceRendererRenameButton } from "./RenameButton";
import interfaceStyles from "./style.module.css";

export function CCComponentEditorRendererNodeInterfaceRenderer(
props: CCComponentEditorRendererNodeRendererProps,
Expand All @@ -18,35 +21,35 @@ export function CCComponentEditorRendererNodeInterfaceRenderer(
store.nodePins.getManyByNodeId(props.node.id)[0],
`Interface node ${props.node.id} is missing its pin`,
);
const componentPin = nullthrows(
store.componentPins.getByImplementation(nodePin.id),
const componentPin = useComponentPin(
nullthrows(store.componentPins.getByImplementation(nodePin.id)).id,
);
const isEditMode = editorState.editorMode === "edit";

const { label, onClick } =
editorState.editorMode === "edit"
? { label: componentPin.name, onClick: null }
: componentPin.type === "input"
? (() => {
const inputValueKey = {
componentPinId: componentPin.id,
timeStep: editorState.timeStep,
};
const value = nullthrows(editorState.getInputValue(inputValueKey));
return {
label: stringifySimulationValue(value),
onClick: () =>
editorState.setInputValue(
inputValueKey,
wrappingIncrementSimulationValue(value),
),
};
})()
: {
label: stringifySimulationValue(
nullthrows(editorState.getNodePinValue(nodePin.id)),
),
onClick: null,
const { label, onClick } = isEditMode
? { label: componentPin.name, onClick: null }
: componentPin.type === "input"
? (() => {
const inputValueKey = {
componentPinId: componentPin.id,
timeStep: editorState.timeStep,
};
const value = nullthrows(editorState.getInputValue(inputValueKey));
return {
label: stringifySimulationValue(value),
onClick: () =>
editorState.setInputValue(
inputValueKey,
wrappingIncrementSimulationValue(value),
),
};
})()
: {
label: stringifySimulationValue(
nullthrows(editorState.getNodePinValue(nodePin.id)),
),
onClick: null,
};

const { bodyWidth, arrowDepth, width, height } = constants;
const isPinOnRight =
Expand All @@ -73,18 +76,25 @@ export function CCComponentEditorRendererNodeInterfaceRenderer(
}
: {})}
/>
<text
x={bodyX + bodyWidth / 2}
y={height / 2}
className={styles.text}
textAnchor="middle"
dominantBaseline="middle"
fontSize={10}
dy={1}
style={{ pointerEvents: "none" }}
<foreignObject
x={bodyX}
y={0}
width={bodyWidth}
height={height}
className={interfaceStyles.labelContainer}
>
{label}
</text>
<div className={interfaceStyles.label}>
{label}
{isEditMode && (
<CCComponentEditorRendererNodeInterfaceRendererRenameButton
name={componentPin.name}
onNameChange={(name) =>
store.componentPins.update(componentPin.id, { name })
}
/>
)}
</div>
</foreignObject>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* Let the clicks reach the shape behind, except on the buttons */
.labelContainer {
pointer-events: none;
}

.labelContainer button {
pointer-events: auto;
}

.label {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
height: 100%;
color: var(--color-text-primary);
font-size: 10px;
white-space: nowrap;
}
23 changes: 22 additions & 1 deletion src/store/react/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type CCComponentId,
isEachInputPinConnected,
} from "../component";
import type { CCComponentPin } from "../componentPin";
import type { CCComponentPin, CCComponentPinId } from "../componentPin";
import type { CCNode, CCNodeId } from "../node";
import type { CCNodePin } from "../nodePin";
import { useStore } from ".";
Expand Down Expand Up @@ -141,6 +141,27 @@ export function useNode(nodeId: CCNodeId) {
return useSyncExternalStore(subscribe, getSnapshot);
}

export function useComponentPin(componentPinId: CCComponentPinId) {
const { store } = useStore();
const getSnapshot = useCallback(
() => nullthrows(store.componentPins.get(componentPinId)),
[store, componentPinId],
);
const subscribe = useCallback(
(onStoreChange: () => void) => {
const handler = (componentPin: CCComponentPin) => {
if (componentPin.id === componentPinId) onStoreChange();
};
store.componentPins.on("didUpdate", handler);
return () => {
store.componentPins.off("didUpdate", handler);
};
},
[store, componentPinId],
);
return useSyncExternalStore(subscribe, getSnapshot);
}

export function useComponentPins(componentId: CCComponentId) {
const { store } = useStore();
const getSnapshot = useMemo(
Expand Down
Loading