From 0cc95c271d312a6f79edbb040ce0da73a859be53 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Tue, 16 May 2023 15:35:22 +0100 Subject: [PATCH] [fix] overlay rendering issues (#1389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes several issues with the way that SVG overlays were rendered. - fixes editing embed shape on firefox (weird SVG pointer events bug) - fixes layering of overlays - collaborator cursors are offset ### Change Type - [x] `patch` — change to unshipped changes ### Test Plan 1. Try editing an embed shape on Firefox 2. Confirm that cursor hints are no longer spinning 3. Confirm that cursors are displayed correctly over other shapes --- .../11-user-presence/UserPresenceExample.tsx | 46 +- packages/editor/editor.css | 92 +++- packages/editor/src/lib/components/Canvas.tsx | 24 +- .../editor/src/lib/components/CropHandles.tsx | 2 +- .../src/lib/components/DefaultBrush.tsx | 13 +- .../components/DefaultCollaboratorHint.tsx | 14 +- .../src/lib/components/DefaultCursor.tsx | 17 +- .../src/lib/components/DefaultHandle.tsx | 16 +- .../src/lib/components/DefaultScribble.tsx | 12 +- .../src/lib/components/DefaultSnapLine.tsx | 11 +- .../src/lib/components/LiveCollaborators.tsx | 19 +- .../editor/src/lib/components/SelectionBg.tsx | 2 +- .../editor/src/lib/components/SelectionFg.tsx | 505 +++++++++--------- .../src/lib/components/ShapeIndicator.tsx | 33 +- 14 files changed, 467 insertions(+), 339 deletions(-) diff --git a/apps/examples/src/11-user-presence/UserPresenceExample.tsx b/apps/examples/src/11-user-presence/UserPresenceExample.tsx index f00da1a65..1e9d27093 100644 --- a/apps/examples/src/11-user-presence/UserPresenceExample.tsx +++ b/apps/examples/src/11-user-presence/UserPresenceExample.tsx @@ -3,6 +3,11 @@ import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/ui.css' import { useRef } from 'react' +const SHOW_MOVING_CURSOR = false +const CURSOR_SPEED = 0.1 +const CIRCLE_RADIUS = 100 +const UPDATE_FPS = 60 + export default function UserPresenceExample() { const rTimeout = useRef(-1) return ( @@ -17,6 +22,7 @@ export default function UserPresenceExample() { // we're having to create these ourselves. const userId = TLUser.createCustomId('user-1') + const user = TLUser.create({ id: userId, name: 'User 1', @@ -48,22 +54,34 @@ export default function UserPresenceExample() { clearTimeout(rTimeout.current) } - rTimeout.current = setInterval(() => { - const SPEED = 0.1 - const R = 400 - const k = 1000 / SPEED - const t = (Date.now() % k) / k - // rotate in a circle - const x = Math.cos(t * Math.PI * 2) * R - const y = Math.sin(t * Math.PI * 2) * R + if (SHOW_MOVING_CURSOR) { + rTimeout.current = setInterval(() => { + const k = 1000 / CURSOR_SPEED + const now = Date.now() + const t = (now % k) / k + // rotate in a circle + app.store.put([ + { + ...userPresence, + cursor: { + x: Math.cos(t * Math.PI * 2) * CIRCLE_RADIUS, + y: Math.sin(t * Math.PI * 2) * CIRCLE_RADIUS, + }, + lastActivityTimestamp: now, + }, + ]) + }, 1000 / UPDATE_FPS) + } else { app.store.put([ - { - ...userPresence, - cursor: { x, y }, - lastActivityTimestamp: Date.now(), - }, + { ...userPresence, cursor: { x: 0, y: 0 }, lastActivityTimestamp: Date.now() }, ]) - }, 100) + + rTimeout.current = setInterval(() => { + app.store.put([ + { ...userPresence, cursor: { x: 0, y: 0 }, lastActivityTimestamp: Date.now() }, + ]) + }, 1000) + } }} /> diff --git a/packages/editor/editor.css b/packages/editor/editor.css index ac8fe04d6..14bba52d6 100644 --- a/packages/editor/editor.css +++ b/packages/editor/editor.css @@ -262,6 +262,15 @@ input, z-index: 2; } +.tl-overlays__item { + position: absolute; + top: 0px; + left: 0px; + overflow: visible; + pointer-events: none; + transform-origin: top left; +} + .tl-svg-context { position: absolute; top: 0px; @@ -271,15 +280,6 @@ input, pointer-events: none; } -.tl-svg-origin-container { - position: absolute; - top: 0px; - left: 0px; - overflow: visible; - pointer-events: none; - transform-origin: top left; -} - /* ------------------- Background ------------------- */ .tl-background { @@ -399,6 +399,68 @@ input, color: inherit; } +/* --------------- Overlay Stack --------------- */ + +/* back of the stack, behind user's stuff */ +.tl-collaborator__scribble { + z-index: 10; +} + +.tl-collaborator__brush { + z-index: 11; +} + +.tl-collaborator__shape-indicator { + z-index: 12; +} + +.tl-user-handles { + z-index: 50; +} + +.tl-user-scribble { + z-index: 52; +} + +.tl-user-brush { + z-index: 51; +} + +.tl-user-indicator__selected { + z-index: 51; +} + +.tl-user-indicator__hovered { + z-index: 52; +} + +.tl-user-snapline { + z-index: 53; +} + +.tl-selection__fg { + pointer-events: none; + z-index: 54; +} + +.tl-user-indicator__hint { + z-index: 55; + stroke-width: calc(2.5px * var(--tl-scale)); +} + +/* behind collaborator cursor */ +.tl-collaborator__cursor-hint { + z-index: 56; +} + +.tl-collaborator__cursor { + z-index: 57; +} + +.tl-cursor { + overflow: visible; +} + /* -------------------- Indicator ------------------- */ .tl-shape-indicator { @@ -407,13 +469,9 @@ input, stroke-width: calc(1.5px * var(--tl-scale)); } -.tl-shape-indicator__hinting { - stroke-width: calc(2.5px * var(--tl-scale)); -} - /* ------------------ SelectionBox ------------------ */ -.tlui-selection__bg { +.tl-selection__bg { position: absolute; top: 0px; left: 0px; @@ -422,11 +480,7 @@ input, pointer-events: all; } -.tlui-selection__fg { - pointer-events: none; -} - -.tlui-selection__fg__outline { +.tl-selection__fg__outline { fill: none; pointer-events: none; stroke: var(--color-selection-stroke); diff --git a/packages/editor/src/lib/components/Canvas.tsx b/packages/editor/src/lib/components/Canvas.tsx index 1e9d1c86a..63f3115cc 100644 --- a/packages/editor/src/lib/components/Canvas.tsx +++ b/packages/editor/src/lib/components/Canvas.tsx @@ -119,15 +119,15 @@ export const Canvas = track(function Canvas({
- + + - - + {debugFlags.newLiveCollaborators.value ? ( ) : ( @@ -162,7 +162,7 @@ const ScribbleWrapper = track(function ScribbleWrapper() { if (!(Scribble && scribble)) return null - return + return }) const BrushWrapper = track(function BrushWrapper() { @@ -172,7 +172,7 @@ const BrushWrapper = track(function BrushWrapper() { if (!(Brush && brush && app.isIn('select.brushing'))) return null - return + return }) export const ZoomBrushWrapper = track(function Zoom() { @@ -182,7 +182,7 @@ export const ZoomBrushWrapper = track(function Zoom() { if (!(ZoomBrush && zoomBrush && app.isIn('zoom'))) return null - return + return }) export const SnapLinesWrapper = track(function SnapLines() { @@ -198,7 +198,7 @@ export const SnapLinesWrapper = track(function SnapLines() { return ( <> {lines.map((line) => ( - + ))} ) @@ -248,7 +248,7 @@ const HandlesWrapper = track(function HandlesWrapper() { handlesToDisplay.sort((a) => (a.type === 'vertex' ? 1 : -1)) return ( - + {handlesToDisplay.map((handle) => { return @@ -317,7 +317,7 @@ const SelectedIdIndicators = track(function SelectedIdIndicators() { return ( <> {app.selectedIds.map((id) => ( - + ))} ) @@ -334,7 +334,7 @@ const HoveredShapeIndicator = function HoveredShapeIndicator() { if (!displayingHoveredId) return null - return + return } const HintedShapeIndicator = track(function HintedShapeIndicator() { @@ -347,7 +347,7 @@ const HintedShapeIndicator = track(function HintedShapeIndicator() { return ( <> {ids.map((id) => ( - + ))} ) @@ -373,7 +373,7 @@ function Cursor() { } function CollaboratorHint() { - return + return } function ArrowheadDot() { diff --git a/packages/editor/src/lib/components/CropHandles.tsx b/packages/editor/src/lib/components/CropHandles.tsx index 24f44ca44..5278a1d2d 100644 --- a/packages/editor/src/lib/components/CropHandles.tsx +++ b/packages/editor/src/lib/components/CropHandles.tsx @@ -13,7 +13,7 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH const offset = cropStrokeWidth / 2 return ( - + {/* Top left */} any | null +export type TLBrushComponent = (props: { + brush: Box2dModel + color?: string + opacity?: number + className?: string +}) => any | null -export const DefaultBrush: TLBrushComponent = ({ brush, color }) => { +export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => { const rSvg = useRef(null) useTransform(rSvg, brush.x, brush.y) return ( - + {color ? ( - + JSX.Element | null export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({ + className, zoom, point, color, viewport, + opacity = 1, }) => { const rSvg = useRef(null) @@ -23,12 +28,13 @@ export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({ clamp(point.x, viewport.minX + 5 / zoom, viewport.maxX - 5 / zoom), clamp(point.y, viewport.minY + 5 / zoom, viewport.maxY - 5 / zoom), 1 / zoom, - radiansToDegrees(Vec2d.Angle(viewport.center, point)) + Vec2d.Angle(viewport.center, point) ) return ( - - + + + ) } diff --git a/packages/editor/src/lib/components/DefaultCursor.tsx b/packages/editor/src/lib/components/DefaultCursor.tsx index d574569db..ca26917a3 100644 --- a/packages/editor/src/lib/components/DefaultCursor.tsx +++ b/packages/editor/src/lib/components/DefaultCursor.tsx @@ -1,23 +1,26 @@ import { Vec2dModel } from '@tldraw/tlschema' -import { memo } from 'react' +import classNames from 'classnames' +import { memo, useRef } from 'react' +import { useTransform } from '../hooks/useTransform' /** @public */ export type TLCursorComponent = (props: { + className?: string point: Vec2dModel | null zoom: number color?: string name: string | null }) => any | null -const _Cursor: TLCursorComponent = ({ zoom, point, color, name }) => { +const _Cursor: TLCursorComponent = ({ className, zoom, point, color, name }) => { + const rDiv = useRef(null) + useTransform(rDiv, point?.x, point?.y, 1 / zoom) + if (!point) return null return ( -
- +
+ {name !== null && name !== '' && ( diff --git a/packages/editor/src/lib/components/DefaultHandle.tsx b/packages/editor/src/lib/components/DefaultHandle.tsx index 348f16960..4aad09b32 100644 --- a/packages/editor/src/lib/components/DefaultHandle.tsx +++ b/packages/editor/src/lib/components/DefaultHandle.tsx @@ -1,11 +1,21 @@ import { TLHandle, TLShapeId } from '@tldraw/tlschema' import classNames from 'classnames' -export type TLHandleComponent = (props: { shapeId: TLShapeId; handle: TLHandle }) => any | null +export type TLHandleComponent = (props: { + shapeId: TLShapeId + handle: TLHandle + className?: string +}) => any | null -export const DefaultHandle: TLHandleComponent = ({ handle }) => { +export const DefaultHandle: TLHandleComponent = ({ handle, className }) => { return ( - + diff --git a/packages/editor/src/lib/components/DefaultScribble.tsx b/packages/editor/src/lib/components/DefaultScribble.tsx index 345ba6cdb..e0ba28942 100644 --- a/packages/editor/src/lib/components/DefaultScribble.tsx +++ b/packages/editor/src/lib/components/DefaultScribble.tsx @@ -1,5 +1,6 @@ import { EASINGS, getStroke } from '@tldraw/primitives' import { TLScribble } from '@tldraw/tlschema' +import classNames from 'classnames' import { getSvgPathFromStroke } from '../utils/svg' /** @public */ @@ -8,9 +9,16 @@ export type TLScribbleComponent = (props: { zoom: number color?: string opacity?: number + className?: string }) => any -export const DefaultScribble: TLScribbleComponent = ({ scribble, zoom, color, opacity }) => { +export const DefaultScribble: TLScribbleComponent = ({ + scribble, + zoom, + color, + opacity, + className, +}) => { const d = getSvgPathFromStroke( getStroke(scribble.points, { size: scribble.size / zoom, @@ -21,7 +29,7 @@ export const DefaultScribble: TLScribbleComponent = ({ scribble, zoom, color, op ) return ( - + any +export type TLSnapLineComponent = (props: { + className?: string + line: SnapLine + zoom: number +}) => any -export const DefaultSnapLine: TLSnapLineComponent = ({ line, zoom }) => { +export const DefaultSnapLine: TLSnapLineComponent = ({ className, line, zoom }) => { return ( - + {line.type === 'points' ? ( ) : line.type === 'gaps' ? ( diff --git a/packages/editor/src/lib/components/LiveCollaborators.tsx b/packages/editor/src/lib/components/LiveCollaborators.tsx index 3fb6e8cf1..9e21578b7 100644 --- a/packages/editor/src/lib/components/LiveCollaborators.tsx +++ b/packages/editor/src/lib/components/LiveCollaborators.tsx @@ -111,10 +111,17 @@ const Collaborator = track(function Collaborator({ return ( <> {brush && CollaboratorBrush ? ( - + ) : null} {isCursorInViewport && CollaboratorCursor ? ( ) : CollaboratorHint ? ( ( - + ))} ) diff --git a/packages/editor/src/lib/components/SelectionBg.tsx b/packages/editor/src/lib/components/SelectionBg.tsx index f7d634500..f996366fa 100644 --- a/packages/editor/src/lib/components/SelectionBg.tsx +++ b/packages/editor/src/lib/components/SelectionBg.tsx @@ -120,7 +120,7 @@ export const SelectionBg = track(function SelectionBg() { return (
-1 + export const SelectionFg = track(function SelectionFg() { const app = useApp() const rSvg = useRef(null) @@ -68,12 +73,11 @@ export const SelectionFg = track(function SelectionFg() { (onlyShape ? !app.getShapeUtil(onlyShape).hideSelectionBoundsFg(onlyShape) : true) && !isChangingStyles - const shouldDisplayBox = + let shouldDisplayBox = (showSelectionBounds && app.isInAny( 'select.idle', 'select.brushing', - 'select.editing_shape', 'select.scribble_brushing', 'select.pointing_canvas', 'select.pointing_selection', @@ -81,10 +85,17 @@ export const SelectionFg = track(function SelectionFg() { 'select.crop.idle', 'select.crop.pointing_crop', 'select.pointing_resize_handle', - 'select.pointing_crop_handle' + 'select.pointing_crop_handle', + 'select.editing_shape' )) || (showSelectionBounds && app.isIn('select.resizing') && onlyShape && shapes[0].type === 'text') + if (IS_FIREFOX && shouldDisplayBox) { + if (app.onlySelectedShape?.type === 'embed') { + shouldDisplayBox = false + } + } + const showCropHandles = app.isInAny('select.pointing_crop_handle', 'select.crop.idle', 'select.crop.pointing_crop') && !isChangingStyles && @@ -162,255 +173,255 @@ export const SelectionFg = track(function SelectionFg() { textHandleHeight * zoom >= 4 return ( - - + + {shouldDisplayBox && ( - - - - {' '} - - {/* Targets */} - - - - - {/* Corner Targets */} - - - - - {/* Resize Handles */} - {showResizeHandles && ( - <> - - - - - - )} - {showTextResizeHandles && ( - <> - - - - )} - {/* Crop Handles */} - {showCropHandles && ( - + + + {' '} + + {/* Targets */} + + + + + {/* Corner Targets */} + + + + + {/* Resize Handles */} + {showResizeHandles && ( + <> + - )} - + + + + + )} + {showTextResizeHandles && ( + <> + + + + )} + {/* Crop Handles */} + {showCropHandles && ( + + )} ) }) diff --git a/packages/editor/src/lib/components/ShapeIndicator.tsx b/packages/editor/src/lib/components/ShapeIndicator.tsx index 8774bd226..9e8092a7f 100644 --- a/packages/editor/src/lib/components/ShapeIndicator.tsx +++ b/packages/editor/src/lib/components/ShapeIndicator.tsx @@ -1,4 +1,3 @@ -import { Matrix2d } from '@tldraw/primitives' import { TLShape, TLShapeId } from '@tldraw/tlschema' import classNames from 'classnames' import * as React from 'react' @@ -51,15 +50,14 @@ export const InnerIndicator = ({ app, id }: { app: App; id: TLShapeId }) => { ) } -export const ShapeIndicator = React.memo(function ShapeIndicator({ - id, - isHinting, - color, -}: { +export type TLShapeIndicatorComponent = (props: { id: TLShapeId - isHinting?: boolean - color?: string -}) { + color?: string | undefined + opacity?: number + className?: string +}) => JSX.Element | null + +const _ShapeIndicator: TLShapeIndicatorComponent = ({ id, className, color, opacity }) => { const app = useApp() const transform = useValue( @@ -67,28 +65,23 @@ export const ShapeIndicator = React.memo(function ShapeIndicator({ () => { const pageTransform = app.getPageTransformById(id) if (!pageTransform) return '' - return Matrix2d.toCssString(pageTransform) + return pageTransform.toCssString() }, [app, id] ) return ( - + ) -}) +} -export type TLShapeIndicatorComponent = (props: { - id: TLShapeId - isHinting?: boolean | undefined - color?: string | undefined -}) => JSX.Element | null +export const ShapeIndicator = React.memo(_ShapeIndicator)