Skip to content

Commit a833ec5

Browse files
committed
refactor(studio): place the selection chrome from rect variables on one wrapper
1 parent 097250e commit a833ec5

5 files changed

Lines changed: 122 additions & 20 deletions

File tree

‎packages/studio/src/components/editor/DomEditCropHandles.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "./domEditOverlayCrop";
1616
import { buildInsetClipPathSides, type ClipPathInsetSides } from "./clipPathHelpers";
1717
import { readCropFollowingResize } from "./cropResize";
18+
import { CHROME_HEIGHT, CHROME_LEFT, CHROME_TOP, CHROME_WIDTH } from "./domEditChromeRect";
1819

1920
interface CropGestureState {
2021
edge: CropEdge | "move";
@@ -243,8 +244,8 @@ export function DomEditCropHandles({
243244
data-dom-edit-crop-frame="true"
244245
className="pointer-events-none absolute"
245246
style={{
246-
left: frame.left,
247-
top: frame.top,
247+
left: `calc(${CHROME_LEFT} + ${CHROME_WIDTH} / 2 - ${frame.width / 2}px)`,
248+
top: `calc(${CHROME_TOP} + ${CHROME_HEIGHT} / 2 - ${frame.height / 2}px)`,
248249
width: frame.width,
249250
height: frame.height,
250251
transform: frame.angleDeg !== 0 ? `rotate(${frame.angleDeg}deg)` : undefined,

‎packages/studio/src/components/editor/DomEditRotateHandle.tsx‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { PointerEvent as ReactPointerEvent } from "react";
2-
import type { OverlayRect } from "./domEditOverlayGeometry";
2+
import { CHROME_HEIGHT, CHROME_LEFT, CHROME_TOP, CHROME_WIDTH } from "./domEditChromeRect";
33

44
/** Rotate handle below the selection: an attached circular-arrows icon chip
55
* (no connecting stem). Anchors to the crop outline when the element is
@@ -8,25 +8,20 @@ import type { OverlayRect } from "./domEditOverlayGeometry";
88
* (resolveDomEditRotationGesture), so the handle position doesn't affect the
99
* math. Sits 12px below the bbox, past the bottom crop handle's hit strip. */
1010
export function DomEditRotateHandle({
11-
overlayRect,
1211
cropOutlineInsetPx,
1312
onStartRotate,
1413
}: {
15-
overlayRect: OverlayRect;
1614
cropOutlineInsetPx?: { top: number; right: number; bottom: number; left: number };
1715
onStartRotate: (e: ReactPointerEvent<HTMLButtonElement>) => void;
1816
}) {
1917
const inset = cropOutlineInsetPx ?? { top: 0, right: 0, bottom: 0, left: 0 };
20-
const visibleLeft = overlayRect.left + inset.left;
21-
const visibleWidth = Math.max(0, overlayRect.width - inset.left - inset.right);
22-
const visibleBottom = overlayRect.top + overlayRect.height - inset.bottom;
2318
return (
2419
<button
2520
type="button"
2621
className="pointer-events-auto absolute flex items-center justify-center border-0 bg-transparent p-0"
2722
style={{
28-
left: visibleLeft + visibleWidth / 2,
29-
top: visibleBottom + 12,
23+
left: `calc(${CHROME_LEFT} + ${inset.left}px + max(0px, ${CHROME_WIDTH} - ${inset.left + inset.right}px) / 2)`,
24+
top: `calc(${CHROME_TOP} + ${CHROME_HEIGHT} + ${12 - inset.bottom}px)`,
3025
width: 22,
3126
height: 22,
3227
transform: "translateX(-50%)",

‎packages/studio/src/components/editor/DomEditSelectionChrome.test.tsx‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,79 @@ describe("DomEditSelectionChrome with body drag off", () => {
322322
act(() => root.unmount());
323323
});
324324
});
325+
326+
describe("DomEditSelectionChrome rect variables", () => {
327+
const rectA = { left: 40, top: 30, width: 200, height: 100, editScaleX: 1, editScaleY: 1 };
328+
const rectB = { left: 75.5, top: 12, width: 120, height: 64, editScaleX: 1, editScaleY: 1 };
329+
330+
function renderAt(rect: typeof rectA) {
331+
const element = document.createElement("div");
332+
document.body.append(element);
333+
const { selection, host, root } = selectionFixture(element, "#el", true);
334+
act(() => {
335+
root.render(
336+
<DomEditSelectionChrome
337+
selection={selection}
338+
overlayRect={rect}
339+
allowCanvasMovement
340+
allowBodyDrag
341+
boxRef={createRef()}
342+
boxChromeClass=""
343+
boxClipPath={undefined}
344+
selectionKey="el"
345+
groupSelectionCount={0}
346+
gestures={{ startGesture: vi.fn() } as never}
347+
onStyleCommit={vi.fn()}
348+
onBoxClick={vi.fn()}
349+
/>,
350+
);
351+
});
352+
const wrapper = host.firstElementChild as HTMLElement;
353+
const handles = [
354+
...host.querySelectorAll<HTMLElement>('[aria-label="Rotate selection"], .h-4.w-4'),
355+
host.querySelector<HTMLElement>("[data-dom-edit-crop-frame]")!,
356+
];
357+
// Resolves a handle's position against the wrapper's variables, as the browser does.
358+
const at = (value: string) => {
359+
const expr = value
360+
.replace(/var\((--[\w-]+)\)/g, (_, name) => wrapper.style.getPropertyValue(name))
361+
.replace(/max\(/g, "Math.max(")
362+
.replace(/calc\(/g, "(")
363+
.replace(/px/g, "");
364+
return Number(new Function(`return ${expr}`)());
365+
};
366+
const done = () => {
367+
act(() => root.unmount());
368+
host.remove();
369+
};
370+
return { handles, at, done };
371+
}
372+
373+
it("moves every handle with the rect while their own styles stay the same", () => {
374+
const a = renderAt(rectA);
375+
const b = renderAt(rectB);
376+
try {
377+
expect(a.handles).toHaveLength(6);
378+
const styleOf = (el: HTMLElement) => [el.style.left, el.style.top];
379+
expect(b.handles.map(styleOf)).toEqual(a.handles.map(styleOf));
380+
const places = (r: ReturnType<typeof renderAt>) =>
381+
r.handles.map((el) => [r.at(el.style.left), r.at(el.style.top)]);
382+
const dots = (r: typeof rectA) => [
383+
[r.left - 8, r.top - 8],
384+
[r.left + r.width - 8, r.top - 8],
385+
[r.left - 8, r.top + r.height - 8],
386+
[r.left + r.width - 8, r.top + r.height - 8],
387+
];
388+
const expected = (r: typeof rectA) => [
389+
[r.left + r.width / 2, r.top + r.height + 12],
390+
...dots(r),
391+
[r.left, r.top],
392+
];
393+
expect(places(a)).toEqual(expected(rectA));
394+
expect(places(b)).toEqual(expected(rectB));
395+
} finally {
396+
a.done();
397+
b.done();
398+
}
399+
});
400+
});

‎packages/studio/src/components/editor/DomEditSelectionChrome.tsx‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import { DomEditCropHandles } from "./DomEditCropHandles";
1111
import { DomEditRotateHandle } from "./DomEditRotateHandle";
1212
import { resolveRotatedResizeCursor } from "./domEditResizeLocal";
1313
import { usePreviewReadOnly } from "./previewReadOnlyContext";
14+
import {
15+
CHROME_HEIGHT,
16+
CHROME_LEFT,
17+
CHROME_TOP,
18+
CHROME_WIDTH,
19+
chromeRectVars,
20+
} from "./domEditChromeRect";
1421

1522
// Corner resize handles, Canva-style: one per corner, diagonal cursors.
1623
// Corners scale about the element center; the translate keeps the center
@@ -32,7 +39,6 @@ const NO_CROP_INSET: CropInset = { top: 0, right: 0, bottom: 0, left: 0 };
3239

3340
function resizeHandleStyle(
3441
def: (typeof RESIZE_HANDLE_DEFS)[number],
35-
overlayRect: { left: number; top: number; width: number; height: number },
3642
cropInset?: CropInset,
3743
): React.CSSProperties {
3844
const half = RESIZE_HANDLE_HIT_PX / 2;
@@ -44,12 +50,12 @@ function resizeHandleStyle(
4450
// overlap the dot circle at the corner.
4551
style.left =
4652
def.x === "left"
47-
? overlayRect.left + inset.left - half
48-
: overlayRect.left + overlayRect.width - inset.right - half;
53+
? `calc(${CHROME_LEFT} + ${inset.left - half}px)`
54+
: `calc(${CHROME_LEFT} + ${CHROME_WIDTH} - ${inset.right + half}px)`;
4955
style.top =
5056
def.y === "top"
51-
? overlayRect.top + inset.top - half
52-
: overlayRect.top + overlayRect.height - inset.bottom - half;
57+
? `calc(${CHROME_TOP} + ${inset.top - half}px)`
58+
: `calc(${CHROME_TOP} + ${CHROME_HEIGHT} - ${inset.bottom + half}px)`;
5359
return style;
5460
}
5561

@@ -167,17 +173,16 @@ export function DomEditSelectionChrome({
167173
const canManipulate = allowCanvasMovement && !readOnly;
168174

169175
return (
170-
<>
176+
<div className="contents" style={chromeRectVars(overlayRect)}>
171177
<div
172178
className="pointer-events-none absolute inset-0"
173179
style={{
174-
transformOrigin: `${overlayRect.left + overlayRect.width / 2}px ${overlayRect.top + overlayRect.height / 2}px`,
180+
transformOrigin: `calc(${CHROME_LEFT} + ${CHROME_WIDTH} / 2) calc(${CHROME_TOP} + ${CHROME_HEIGHT} / 2)`,
175181
transform: overlayRect.angle ? `rotate(${overlayRect.angle}deg)` : undefined,
176182
}}
177183
>
178184
{canManipulate && !editing && selection.capabilities.canApplyManualRotation && (
179185
<DomEditRotateHandle
180-
overlayRect={overlayRect}
181186
cropOutlineInsetPx={cropOutlineInsetPx}
182187
onStartRotate={(e) => {
183188
e.stopPropagation();
@@ -247,7 +252,7 @@ export function DomEditSelectionChrome({
247252
key={def.handle}
248253
className="pointer-events-auto absolute flex h-4 w-4 items-center justify-center"
249254
style={{
250-
...resizeHandleStyle(def, overlayRect, cropOutlineInsetPx ?? undefined),
255+
...resizeHandleStyle(def, cropOutlineInsetPx ?? undefined),
251256
// Cursor rotates with the object: bucket the corner's base
252257
// diagonal + element rotation into the 8 CSS resize cursors.
253258
cursor: resolveRotatedResizeCursor(def.handle, overlayRect.angle ?? 0),
@@ -271,6 +276,6 @@ export function DomEditSelectionChrome({
271276
onStyleCommit={onStyleCommit}
272277
/>
273278
)}
274-
</>
279+
</div>
275280
);
276281
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { CSSProperties } from "react";
2+
3+
type Rect = { left: number; top: number; width: number; height: number };
4+
5+
/** The selection chrome positions from these, so one style write on its wrapper moves all of it. */
6+
const VAR = {
7+
left: "--hf-chrome-left",
8+
top: "--hf-chrome-top",
9+
width: "--hf-chrome-width",
10+
height: "--hf-chrome-height",
11+
} as const;
12+
13+
export const CHROME_LEFT = `var(${VAR.left})`;
14+
export const CHROME_TOP = `var(${VAR.top})`;
15+
export const CHROME_WIDTH = `var(${VAR.width})`;
16+
export const CHROME_HEIGHT = `var(${VAR.height})`;
17+
18+
export function chromeRectVars(rect: Rect): CSSProperties {
19+
return {
20+
[VAR.left]: `${rect.left}px`,
21+
[VAR.top]: `${rect.top}px`,
22+
[VAR.width]: `${rect.width}px`,
23+
[VAR.height]: `${rect.height}px`,
24+
} as CSSProperties;
25+
}

0 commit comments

Comments
 (0)