feat(layers): add layer management panel
This commit is contained in:
@@ -1,28 +1,100 @@
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { BoundingBox } from "@phosphor-icons/react";
|
||||
import { commandIds } from "@commands/ids";
|
||||
import type { Rect } from "@core/geometry";
|
||||
import type { AppStore } from "@editor/store";
|
||||
import type { TransformTarget } from "@editor/transform";
|
||||
import { BottomControlDivider } from "./Divider";
|
||||
import { bottomControlIconSlotClass, bottomControlLabelClass, bottomControlValueClass } from "./styles";
|
||||
import { bottomControlIconSlotClass, bottomControlInputClass, bottomControlLabelClass } from "./styles";
|
||||
|
||||
export type TransformControlsProps = {
|
||||
bounds: Rect;
|
||||
target: TransformTarget;
|
||||
dispatch: AppStore["dispatch"];
|
||||
};
|
||||
|
||||
export function TransformControls({ bounds }: TransformControlsProps) {
|
||||
type BoundsField = keyof Rect;
|
||||
|
||||
export function TransformControls({ bounds, target, dispatch }: TransformControlsProps) {
|
||||
const [draft, setDraft] = useState(() => draftFromBounds(bounds));
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(draftFromBounds(bounds));
|
||||
}, [bounds.x, bounds.y, bounds.w, bounds.h]);
|
||||
|
||||
const commitField = (field: BoundsField) => {
|
||||
const value = Number.parseFloat(draft[field]);
|
||||
if (!Number.isFinite(value)) {
|
||||
setDraft(draftFromBounds(bounds));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(commandIds.transformSetBounds, {
|
||||
target,
|
||||
bounds: {
|
||||
...bounds,
|
||||
[field]: field === "w" || field === "h" ? Math.max(1, value) : value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center gap-2 tabular-nums">
|
||||
<span className={bottomControlIconSlotClass()}>
|
||||
<BoundingBox size={16} weight="regular" />
|
||||
</span>
|
||||
<BottomControlDivider />
|
||||
<span className={bottomControlLabelClass()}>X</span>
|
||||
<span className={bottomControlValueClass()}>{Math.round(bounds.x)}</span>
|
||||
<span className={bottomControlLabelClass()}>Y</span>
|
||||
<span className={bottomControlValueClass()}>{Math.round(bounds.y)}</span>
|
||||
<BoundsInput label="X" field="x" draft={draft.x} setDraft={setDraft} commitField={commitField} />
|
||||
<BoundsInput label="Y" field="y" draft={draft.y} setDraft={setDraft} commitField={commitField} />
|
||||
<BottomControlDivider />
|
||||
<span className={bottomControlLabelClass()}>W</span>
|
||||
<span className={bottomControlValueClass()}>{Math.round(bounds.w)}</span>
|
||||
<span className={bottomControlLabelClass()}>H</span>
|
||||
<span className={bottomControlValueClass()}>{Math.round(bounds.h)}</span>
|
||||
<BoundsInput label="W" field="w" draft={draft.w} setDraft={setDraft} commitField={commitField} />
|
||||
<BoundsInput label="H" field="h" draft={draft.h} setDraft={setDraft} commitField={commitField} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BoundsInput({
|
||||
label,
|
||||
field,
|
||||
draft,
|
||||
setDraft,
|
||||
commitField,
|
||||
}: {
|
||||
label: string;
|
||||
field: BoundsField;
|
||||
draft: string;
|
||||
setDraft: Dispatch<SetStateAction<Record<BoundsField, string>>>;
|
||||
commitField: (field: BoundsField) => void;
|
||||
}) {
|
||||
return (
|
||||
<label className="flex items-center gap-1">
|
||||
<span className={bottomControlLabelClass()}>{label}</span>
|
||||
<input
|
||||
className={bottomControlInputClass()}
|
||||
inputMode="decimal"
|
||||
value={draft}
|
||||
aria-label={label}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, [field]: event.target.value }))}
|
||||
onBlur={() => commitField(field)}
|
||||
onFocus={(event) => event.currentTarget.select()}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
if (event.key === "Enter") {
|
||||
commitField(field);
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
if (event.key === "Escape") event.currentTarget.blur();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function draftFromBounds(bounds: Rect): Record<BoundsField, string> {
|
||||
return {
|
||||
x: String(Math.round(bounds.x)),
|
||||
y: String(Math.round(bounds.y)),
|
||||
w: String(Math.round(bounds.w)),
|
||||
h: String(Math.round(bounds.h)),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user