diff --git a/src/pages/edit/Editor/renderer/Node/components/Interface/RenameButton.tsx b/src/pages/edit/Editor/renderer/Node/components/Interface/RenameButton.tsx new file mode 100644 index 0000000..291f52e --- /dev/null +++ b/src/pages/edit/Editor/renderer/Node/components/Interface/RenameButton.tsx @@ -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(null); + const [newName, setNewName] = useState(props.name); + const trimmedNewName = newName.trim(); + + return ( + <> + e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + setAnchorEl(e.currentTarget); + setNewName(props.name); + }} + > + + + setAnchorEl(null)} + slotProps={{ + root: { onPointerDown: (e) => e.stopPropagation() }, + paper: { sx: { width: "300px", p: 2 } }, + }} + > +
{ + e.preventDefault(); + if (!trimmedNewName) return; + props.onNameChange(trimmedNewName); + setAnchorEl(null); + }} + > + + Rename Pin + + setNewName(e.target.value)} + size="small" + fullWidth + autoFocus + /> + + + + + +
+ + ); +} diff --git a/src/pages/edit/Editor/renderer/Node/components/Interface/index.tsx b/src/pages/edit/Editor/renderer/Node/components/Interface/index.tsx index 6fc4f2d..81aa463 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Interface/index.tsx +++ b/src/pages/edit/Editor/renderer/Node/components/Interface/index.tsx @@ -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, @@ -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 = @@ -73,18 +76,25 @@ export function CCComponentEditorRendererNodeInterfaceRenderer( } : {})} /> - - {label} - +
+ {label} + {isEditMode && ( + + store.componentPins.update(componentPin.id, { name }) + } + /> + )} +
+ ); } diff --git a/src/pages/edit/Editor/renderer/Node/components/Interface/style.module.css b/src/pages/edit/Editor/renderer/Node/components/Interface/style.module.css new file mode 100644 index 0000000..d56e336 --- /dev/null +++ b/src/pages/edit/Editor/renderer/Node/components/Interface/style.module.css @@ -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; +} diff --git a/src/store/react/selectors.ts b/src/store/react/selectors.ts index c90c73f..0c32215 100644 --- a/src/store/react/selectors.ts +++ b/src/store/react/selectors.ts @@ -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 "."; @@ -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(