From 5de42b2d0232276500709b667a47e41ed7c1c0fc Mon Sep 17 00:00:00 2001 From: Ivan Starkov Date: Thu, 12 Dec 2024 17:09:51 +0700 Subject: [PATCH] fix: Ensure outline stays within canvas bounds (#4570) ## Description Playground https://p-183a0702-0620-49ac-84b6-066dbce1d300-dot-fix-outline.development.webstudio.is/ image todo: - [x] - fix if horizontal scroll image - [x] - fix if vertical ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file --- .../features/workspace/canvas-iframe.tsx | 10 ++- .../workspace/canvas-tools/canvas-tools.tsx | 9 ++- .../canvas-tools/outline/apply-scale.ts | 8 +- .../outline/block-instance-outline.tsx | 9 ++- .../collaborative-instance-outline.tsx | 17 ++++- .../outline/hovered-instance-outline.tsx | 11 ++- .../canvas-tools/outline/outline.stories.tsx | 75 +++++++++++++++++-- .../canvas-tools/outline/outline.tsx | 56 +++++++++++++- .../outline/selected-instance-outline.tsx | 9 ++- .../app/builder/shared/nano-states/index.ts | 41 ++++++++++ apps/builder/app/canvas/canvas.tsx | 3 + apps/builder/app/canvas/scrollbar-width.ts | 28 +++++++ apps/builder/app/shared/sync/sync-stores.ts | 2 + 13 files changed, 251 insertions(+), 27 deletions(-) create mode 100644 apps/builder/app/canvas/scrollbar-width.ts diff --git a/apps/builder/app/builder/features/workspace/canvas-iframe.tsx b/apps/builder/app/builder/features/workspace/canvas-iframe.tsx index 3b0b770b937e..1b140b6ef24f 100644 --- a/apps/builder/app/builder/features/workspace/canvas-iframe.tsx +++ b/apps/builder/app/builder/features/workspace/canvas-iframe.tsx @@ -45,7 +45,15 @@ const CanvasRectUpdater = ({ } const rect = iframeRef.current.getBoundingClientRect(); - $canvasRect.set(rect); + + $canvasRect.set( + new DOMRect( + Math.round(rect.x), + Math.round(rect.y), + Math.round(rect.width), + Math.round(rect.height) + ) + ); }; setUpdateCallback(() => task); diff --git a/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx b/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx index 33762b22af9a..f855015c6141 100644 --- a/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx +++ b/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx @@ -17,7 +17,7 @@ import { Label } from "./outline/label"; import { Outline } from "./outline/outline"; import { useSubscribeDragAndDropState } from "./use-subscribe-drag-drop-state"; import { applyScale } from "./outline"; -import { $scale } from "~/builder/shared/nano-states"; +import { $clampingRect, $scale } from "~/builder/shared/nano-states"; import { BlockChildHoveredInstanceOutline } from "./outline/block-instance-outline"; const containerStyle = css({ @@ -41,11 +41,16 @@ export const CanvasTools = () => { const dragAndDropState = useStore($dragAndDropState); const instances = useStore($instances); const scale = useStore($scale); + const clampingRect = useStore($clampingRect); if (!canvasToolsVisible) { return; } + if (clampingRect === undefined) { + return; + } + if (dragAndDropState.isDragging) { if (dragAndDropState.placementIndicator === undefined) { return; @@ -59,7 +64,7 @@ export const CanvasTools = () => { return dropTargetInstance ? (
- + {placementIndicator !== undefined && ( diff --git a/apps/builder/app/builder/features/workspace/canvas-tools/outline/apply-scale.ts b/apps/builder/app/builder/features/workspace/canvas-tools/outline/apply-scale.ts index 6f11188a161f..10ddbcbf4743 100644 --- a/apps/builder/app/builder/features/workspace/canvas-tools/outline/apply-scale.ts +++ b/apps/builder/app/builder/features/workspace/canvas-tools/outline/apply-scale.ts @@ -4,9 +4,9 @@ export const applyScale = (rect: Rect, scale: number = 1) => { // Calculate in the "scale" that is applied to the canvas const scaleFactor = scale / 100; return { - top: rect.top * scaleFactor, - left: rect.left * scaleFactor, - width: rect.width * scaleFactor, - height: rect.height * scaleFactor, + top: Math.round(rect.top * scaleFactor), + left: Math.round(rect.left * scaleFactor), + width: Math.round(rect.width * scaleFactor), + height: Math.round(rect.height * scaleFactor), }; }; diff --git a/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx b/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx index 5164d5f65379..aca03db013c0 100644 --- a/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx +++ b/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx @@ -30,7 +30,7 @@ import { } from "@webstudio-is/design-system"; import { Outline } from "./outline"; import { applyScale } from "./apply-scale"; -import { $scale } from "~/builder/shared/nano-states"; +import { $clampingRect, $scale } from "~/builder/shared/nano-states"; import { PlusIcon, TrashIcon } from "@webstudio-is/icons"; import { BoxIcon } from "@webstudio-is/icons/svg"; import { useRef, useState } from "react"; @@ -342,6 +342,7 @@ export const BlockChildHoveredInstanceOutline = () => { const isContentMode = useStore($isContentMode); const modifierKeys = useStore($modifierKeys); const instances = useStore($instances); + const clampingRect = useStore($clampingRect); const timeoutRef = useRef>( undefined @@ -366,6 +367,10 @@ export const BlockChildHoveredInstanceOutline = () => { return; } + if (clampingRect === undefined) { + return; + } + const blockInstanceSelector = findBlockSelector(outline.selector, instances); if (blockInstanceSelector === undefined) { @@ -412,7 +417,7 @@ export const BlockChildHoveredInstanceOutline = () => { ); return ( - +
{ const scale = useStore($scale); const instanceRect = useStore($collaborativeInstanceRect); const ephemeralStyles = useStore($ephemeralStyles); + const clampingRect = useStore($clampingRect); - if (instanceRect === undefined || ephemeralStyles.length !== 0) { + if ( + instanceRect === undefined || + ephemeralStyles.length !== 0 || + clampingRect === undefined + ) { return; } const rect = applyScale(instanceRect, scale); - return ; + return ( + + ); }; diff --git a/apps/builder/app/builder/features/workspace/canvas-tools/outline/hovered-instance-outline.tsx b/apps/builder/app/builder/features/workspace/canvas-tools/outline/hovered-instance-outline.tsx index 804463a27a3c..34dece26756e 100644 --- a/apps/builder/app/builder/features/workspace/canvas-tools/outline/hovered-instance-outline.tsx +++ b/apps/builder/app/builder/features/workspace/canvas-tools/outline/hovered-instance-outline.tsx @@ -10,7 +10,7 @@ import { import { Outline } from "./outline"; import { Label } from "./label"; import { applyScale } from "./apply-scale"; -import { $scale } from "~/builder/shared/nano-states"; +import { $clampingRect, $scale } from "~/builder/shared/nano-states"; import { findClosestSlot } from "~/shared/instance-utils"; import { shallowEqual } from "shallow-equal"; import type { InstanceSelector } from "~/shared/tree-utils"; @@ -31,8 +31,13 @@ export const HoveredInstanceOutline = () => { const scale = useStore($scale); const textEditingInstanceSelector = useStore($textEditingInstanceSelector); const isContentMode = useStore($isContentMode); + const clampingRect = useStore($clampingRect); - if (outline === undefined || hoveredInstanceSelector === undefined) { + if ( + outline === undefined || + hoveredInstanceSelector === undefined || + clampingRect === undefined + ) { return; } @@ -58,7 +63,7 @@ export const HoveredInstanceOutline = () => { const rect = applyScale(outline.rect, scale); return ( - +