From 8ff8b87a9ed91e047093b44036b27f1719d9d1cd Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Mon, 28 Jun 2021 13:13:34 +0100 Subject: [PATCH] perf improvements around selected / hovered shapes --- components/canvas/bounds/bounding-box.tsx | 19 +- components/canvas/bounds/bounds-bg.tsx | 32 +- components/canvas/bounds/handles.tsx | 12 +- components/canvas/canvas.tsx | 11 +- .../canvas/context-menu/context-menu.tsx | 11 +- components/canvas/defs.tsx | 22 +- components/canvas/hovered-shape.tsx | 43 +++ components/canvas/page.tsx | 23 +- components/canvas/selected.tsx | 4 +- components/canvas/shape.tsx | 302 ++++++++---------- components/controls-panel/controls-panel.tsx | 28 +- components/page-panel/page-panel.tsx | 14 +- components/shared.tsx | 7 + components/style-panel/align-distribute.tsx | 27 +- components/style-panel/color-content.tsx | 19 +- components/style-panel/color-picker.tsx | 21 +- components/style-panel/dash-picker.tsx | 46 ++- components/style-panel/is-filled-picker.tsx | 19 +- components/style-panel/quick-color-select.tsx | 13 +- components/style-panel/quick-dash-select.tsx | 48 ++- components/style-panel/quick-size-select.tsx | 45 ++- components/style-panel/shapes-functions.tsx | 217 +++++++++++++ components/style-panel/size-picker.tsx | 46 +-- components/style-panel/style-panel.tsx | 204 +----------- hooks/usePageShapes.ts | 4 +- state/state.ts | 18 +- 26 files changed, 643 insertions(+), 612 deletions(-) create mode 100644 components/canvas/hovered-shape.tsx create mode 100644 components/style-panel/shapes-functions.tsx diff --git a/components/canvas/bounds/bounding-box.tsx b/components/canvas/bounds/bounding-box.tsx index 74ce11195..f56b4b2a6 100644 --- a/components/canvas/bounds/bounding-box.tsx +++ b/components/canvas/bounds/bounding-box.tsx @@ -2,11 +2,9 @@ import * as React from 'react' import { Edge, Corner } from 'types' import { useSelector } from 'state' import { - deepCompareArrays, getBoundsCenter, getCurrentCamera, getPage, - getSelectedIds, getSelectedShapes, isMobile, } from 'utils' @@ -24,25 +22,22 @@ export default function Bounds(): JSX.Element { const bounds = useSelector((s) => s.values.selectedBounds) - const selectedIds = useSelector( - (s) => Array.from(s.values.selectedIds.values()), - deepCompareArrays - ) - - const rotation = useSelector(({ data }) => - getSelectedIds(data).size === 1 ? getSelectedShapes(data)[0].rotation : 0 + const rotation = useSelector((s) => + s.values.selectedIds.length === 1 + ? getSelectedShapes(s.data)[0].rotation + : 0 ) const isAllLocked = useSelector((s) => { const page = getPage(s.data) - return selectedIds.every((id) => page.shapes[id]?.isLocked) + return s.values.selectedIds.every((id) => page.shapes[id]?.isLocked) }) const isSingleHandles = useSelector((s) => { const page = getPage(s.data) return ( - selectedIds.length === 1 && - page.shapes[selectedIds[0]]?.handles !== undefined + s.values.selectedIds.length === 1 && + page.shapes[s.values.selectedIds[0]]?.handles !== undefined ) }) diff --git a/components/canvas/bounds/bounds-bg.tsx b/components/canvas/bounds/bounds-bg.tsx index a79824502..96fd1fcd6 100644 --- a/components/canvas/bounds/bounds-bg.tsx +++ b/components/canvas/bounds/bounds-bg.tsx @@ -2,7 +2,7 @@ import { useRef } from 'react' import state, { useSelector } from 'state' import inputs from 'state/inputs' import styled from 'styles' -import { deepCompareArrays, getPage } from 'utils' +import { getPage } from 'utils' function handlePointerDown(e: React.PointerEvent) { if (!inputs.canAccept(e.pointerId)) return @@ -31,28 +31,30 @@ export default function BoundsBg(): JSX.Element { const isSelecting = useSelector((s) => s.isIn('selecting')) - const selectedIds = useSelector( - (s) => Array.from(s.values.selectedIds.values()), - deepCompareArrays - ) - const rotation = useSelector((s) => { + const selectedIds = s.values.selectedIds + if (selectedIds.length === 1) { - const { shapes } = getPage(s.data) - const selected = Array.from(s.values.selectedIds.values())[0] - return shapes[selected]?.rotation + const selected = selectedIds[0] + const page = getPage(s.data) + + return page.shapes[selected]?.rotation } else { return 0 } }) const isAllHandles = useSelector((s) => { - const page = getPage(s.data) - const selectedIds = Array.from(s.values.selectedIds.values()) - return ( - selectedIds.length === 1 && - page.shapes[selectedIds[0]]?.handles !== undefined - ) + const selectedIds = s.values.selectedIds + + if (selectedIds.length === 1) { + const page = getPage(s.data) + const selected = selectedIds[0] + + return ( + selectedIds.length === 1 && page.shapes[selected]?.handles !== undefined + ) + } }) if (isAllHandles) return null diff --git a/components/canvas/bounds/handles.tsx b/components/canvas/bounds/handles.tsx index 830e6964f..de105e8e9 100644 --- a/components/canvas/bounds/handles.tsx +++ b/components/canvas/bounds/handles.tsx @@ -3,18 +3,14 @@ import { getShapeUtils } from 'state/shape-utils' import { useRef } from 'react' import { useSelector } from 'state' import styled from 'styles' -import { deepCompareArrays, getPage } from 'utils' +import { getPage } from 'utils' import vec from 'utils/vec' export default function Handles(): JSX.Element { - const selectedIds = useSelector( - (s) => Array.from(s.values.selectedIds.values()), - deepCompareArrays - ) - const shape = useSelector( - ({ data }) => - selectedIds.length === 1 && getPage(data).shapes[selectedIds[0]] + (s) => + s.values.selectedIds.length === 1 && + getPage(s.data).shapes[s.values.selectedIds[0]] ) const isSelecting = useSelector((s) => diff --git a/components/canvas/canvas.tsx b/components/canvas/canvas.tsx index 12d74c24f..3280a391c 100644 --- a/components/canvas/canvas.tsx +++ b/components/canvas/canvas.tsx @@ -13,6 +13,10 @@ import Handles from './bounds/handles' import useCanvasEvents from 'hooks/useCanvasEvents' import ContextMenu from './context-menu/context-menu' +function resetError() { + null +} + export default function Canvas(): JSX.Element { const rCanvas = useRef(null) const rGroup = useRef(null) @@ -28,12 +32,7 @@ export default function Canvas(): JSX.Element { return ( - { - // reset the state of your app so the error doesn't happen again - }} - > + {isReady && ( diff --git a/components/canvas/context-menu/context-menu.tsx b/components/canvas/context-menu/context-menu.tsx index 8916f16df..012812cbf 100644 --- a/components/canvas/context-menu/context-menu.tsx +++ b/components/canvas/context-menu/context-menu.tsx @@ -5,14 +5,7 @@ import { IconButton as _IconButton, RowButton, } from 'components/shared' -import { - commandKey, - deepCompareArrays, - getSelectedIds, - getShape, - isMobile, - setToArray, -} from 'utils' +import { commandKey, deepCompareArrays, getShape, isMobile } from 'utils' import state, { useSelector } from 'state' import { AlignType, @@ -82,7 +75,7 @@ export default function ContextMenu({ children: React.ReactNode }): JSX.Element { const selectedShapeIds = useSelector( - (s) => setToArray(getSelectedIds(s.data)), + (s) => s.values.selectedIds, deepCompareArrays ) diff --git a/components/canvas/defs.tsx b/components/canvas/defs.tsx index efd36051a..212a61803 100644 --- a/components/canvas/defs.tsx +++ b/components/canvas/defs.tsx @@ -1,6 +1,6 @@ import { getShapeStyle } from 'state/shape-styles' import { getShapeUtils } from 'state/shape-utils' -import React, { memo } from 'react' +import React from 'react' import { useSelector } from 'state' import { getCurrentCamera } from 'utils' import { DotCircle, Handle } from './misc' @@ -12,28 +12,32 @@ export default function Defs(): JSX.Element { return ( - {shapeIdsToRender.map((id) => ( - - ))} + {shapeIdsToRender.map((id) => ( + + ))} ) } -const Def = memo(function Def({ id }: { id: string }) { +function Def({ id }: { id: string }) { const shape = useShapeDef(id) if (!shape) return null const style = getShapeStyle(shape.style) - return React.cloneElement( - getShapeUtils(shape).render(shape, { isEditing: false }), - { id, ...style } + return ( + <> + {React.cloneElement( + getShapeUtils(shape).render(shape, { isEditing: false }), + { id, ...style, strokeWidth: undefined } + )} + ) -}) +} function ExpandDef() { const zoom = useSelector((s) => getCurrentCamera(s.data).zoom) diff --git a/components/canvas/hovered-shape.tsx b/components/canvas/hovered-shape.tsx new file mode 100644 index 000000000..18d471dfd --- /dev/null +++ b/components/canvas/hovered-shape.tsx @@ -0,0 +1,43 @@ +import { memo } from 'react' +import { getShape } from 'utils' +import { getShapeUtils } from 'state/shape-utils' +import vec from 'utils/vec' +import styled from 'styles' +import { useSelector } from 'state' +import { getShapeStyle } from 'state/shape-styles' + +function HoveredShape({ id }: { id: string }) { + const transform = useSelector((s) => { + const shape = getShape(s.data, id) + const center = getShapeUtils(shape).getCenter(shape) + const rotation = shape.rotation * (180 / Math.PI) + const parentPoint = getShape(s.data, shape.parentId)?.point || [0, 0] + + return ` + translate(${vec.neg(parentPoint)}) + rotate(${rotation}, ${center}) + translate(${shape.point}) + ` + }) + + const strokeWidth = useSelector((s) => { + const shape = getShape(s.data, id) + const style = getShapeStyle(shape.style) + return +style.strokeWidth + }) + + return ( + + + hello + + ) +} + +const StyledHoverShape = styled('use', { + stroke: '$selected', + filter: 'url(#expand)', + opacity: 0.1, +}) + +export default memo(HoveredShape) diff --git a/components/canvas/page.tsx b/components/canvas/page.tsx index d9b1b1e4a..7278aed63 100644 --- a/components/canvas/page.tsx +++ b/components/canvas/page.tsx @@ -1,5 +1,6 @@ import { useSelector } from 'state' import Shape from './shape' +import HoveredShape from './hovered-shape' import usePageShapes from 'hooks/usePageShapes' /* @@ -8,22 +9,22 @@ on the current page. Kind of expensive but only happens here; and still cheaper than any other pattern I've found. */ -const noOffset = [0, 0] - export default function Page(): JSX.Element { - const currentPageShapeIds = usePageShapes() - const isSelecting = useSelector((s) => s.isIn('selecting')) + const visiblePageShapeIds = usePageShapes() + + const hoveredShapeId = useSelector((s) => { + return visiblePageShapeIds.find((id) => id === s.data.hoveredId) + }) + return ( - {currentPageShapeIds.map((shapeId) => ( - + {isSelecting && hoveredShapeId && ( + + )} + {visiblePageShapeIds.map((id) => ( + ))} ) diff --git a/components/canvas/selected.tsx b/components/canvas/selected.tsx index 9a2573358..e7567932e 100644 --- a/components/canvas/selected.tsx +++ b/components/canvas/selected.tsx @@ -1,12 +1,12 @@ import styled from 'styles' import { useSelector } from 'state' -import { deepCompareArrays, getPage, getSelectedIds, setToArray } from 'utils' +import { deepCompareArrays, getPage } from 'utils' import { getShapeUtils } from 'state/shape-utils' import { memo } from 'react' export default function Selected(): JSX.Element { const currentSelectedShapeIds = useSelector( - ({ data }) => setToArray(getSelectedIds(data)), + (s) => s.values.selectedIds, deepCompareArrays ) diff --git a/components/canvas/shape.tsx b/components/canvas/shape.tsx index 30e4ed9dc..1f6bebba4 100644 --- a/components/canvas/shape.tsx +++ b/components/canvas/shape.tsx @@ -2,31 +2,150 @@ import React, { useRef, memo, useEffect } from 'react' import { useSelector } from 'state' import styled from 'styles' import { getShapeUtils } from 'state/shape-utils' -import { getPage, getSelectedIds, isMobile } from 'utils' +import { deepCompareArrays, getPage, getShape } from 'utils' import useShapeEvents from 'hooks/useShapeEvents' -import { Shape as _Shape } from 'types' import vec from 'utils/vec' import { getShapeStyle } from 'state/shape-styles' - -const isMobileDevice = isMobile() +import useShapeDef from 'hooks/useShape' interface ShapeProps { id: string isSelecting: boolean - parentPoint: number[] } -function Shape({ id, isSelecting, parentPoint }: ShapeProps): JSX.Element { +function Shape({ id, isSelecting }: ShapeProps): JSX.Element { const rGroup = useRef(null) + + const shapeUtils = useSelector((s) => { + const shape = getShape(s.data, id) + return getShapeUtils(shape) + }) + + const isHidden = useSelector((s) => { + const shape = getShape(s.data, id) + return shape.isHidden + }) + + const children = useSelector((s) => { + const shape = getShape(s.data, id) + return shape.children + }, deepCompareArrays) + + const isParent = shapeUtils.isParent + + const isForeignObject = shapeUtils.isForeignObject + + const strokeWidth = useSelector((s) => { + const shape = getShape(s.data, id) + const style = getShapeStyle(shape.style) + return +style.strokeWidth + }) + + const transform = useSelector((s) => { + const shape = getShape(s.data, id) + const center = shapeUtils.getCenter(shape) + const rotation = shape.rotation * (180 / Math.PI) + const parentPoint = getShape(s.data, shape.parentId)?.point || [0, 0] + + return ` + translate(${vec.neg(parentPoint)}) + rotate(${rotation}, ${center}) + translate(${shape.point}) + ` + }) + + const events = useShapeEvents(id, isParent, rGroup) + + return ( + + {isSelecting && + (isForeignObject ? ( + + ) : ( + + ))} + + {!isHidden && + (isForeignObject ? ( + + ) : ( + + ))} + + {isParent && + children.map((shapeId) => ( + + ))} + + ) +} + +interface RealShapeProps { + id: string + isParent: boolean + strokeWidth: number +} + +const RealShape = memo(function RealShape({ + id, + isParent, + strokeWidth, +}: RealShapeProps) { + return ( + + ) +}) + +const ForeignObjectHover = memo(function ForeignObjectHover({ + id, +}: { + id: string +}) { + const size = useSelector((s) => { + const shape = getPage(s.data).shapes[id] + const bounds = getShapeUtils(shape).getBounds(shape) + + return [bounds.width, bounds.height] + }, deepCompareArrays) + + return ( + + ) +}) + +const ForeignObjectRender = memo(function ForeignObjectRender({ + id, +}: { + id: string +}) { + const shape = useShapeDef(id) + const rFocusable = useRef(null) const isEditing = useSelector((s) => s.data.editingId === id) - const isSelected = useSelector((s) => getSelectedIds(s.data).has(id)) - - const shape = useSelector((s) => getPage(s.data).shapes[id]) - - const events = useShapeEvents(id, getShapeUtils(shape)?.isParent, rGroup) + const shapeUtils = getShapeUtils(shape) useEffect(() => { if (isEditing) { @@ -38,85 +157,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps): JSX.Element { } }, [isEditing]) - // This is a problem with deleted shapes. The hooks in this component - // may sometimes run before the hook in the Page component, which means - // a deleted shape will still be pulled here before the page component - // detects the change and pulls this component. - if (!shape) return null - - const style = getShapeStyle(shape.style) - const shapeUtils = getShapeUtils(shape) - - const { isShy, isParent, isForeignObject } = shapeUtils - - const bounds = shapeUtils.getBounds(shape) - const center = shapeUtils.getCenter(shape) - const rotation = shape.rotation * (180 / Math.PI) - - const transform = ` - translate(${vec.neg(parentPoint)}) - rotate(${rotation}, ${center}) - translate(${shape.point}) - ` - - return ( - - {isSelecting && !isShy && ( - <> - {isForeignObject ? ( - - ) : ( - - )} - - )} - - {!shape.isHidden && - (isForeignObject ? ( - shapeUtils.render(shape, { isEditing, ref: rFocusable }) - ) : ( - - ))} - - {isParent && - shape.children.map((shapeId) => ( - - ))} - - ) -} - -interface RealShapeProps { - id: string - shape: _Shape - isParent: boolean -} - -const RealShape = memo(function RealShape({ id, isParent }: RealShapeProps) { - return + return shapeUtils.render(shape, { isEditing, ref: rFocusable }) }) const StyledShape = styled('path', { @@ -125,12 +166,10 @@ const StyledShape = styled('path', { pointerEvents: 'none', }) -const HoverIndicator = styled('path', { - stroke: '$selected', +const EventSoak = styled('use', { + opacity: 0, strokeLinecap: 'round', strokeLinejoin: 'round', - fill: 'transparent', - filter: 'url(#expand)', variants: { variant: { ghost: { @@ -150,81 +189,6 @@ const HoverIndicator = styled('path', { const StyledGroup = styled('g', { outline: 'none', - [`& *[data-shy="true"]`]: { - opacity: '0', - }, - [`& ${HoverIndicator}`]: { - opacity: '0', - }, - variants: { - device: { - mobile: {}, - desktop: {}, - }, - isSelected: { - true: { - [`& *[data-shy="true"]`]: { - opacity: '1', - }, - [`& ${HoverIndicator}`]: { - opacity: '0.2', - }, - }, - false: { - [`& ${HoverIndicator}`]: { - opacity: '0', - }, - }, - }, - }, - compoundVariants: [ - { - device: 'desktop', - isSelected: 'false', - css: { - [`&:hover ${HoverIndicator}`]: { - opacity: '0.16', - }, - [`&:hover *[data-shy="true"]`]: { - opacity: '1', - }, - }, - }, - { - device: 'desktop', - isSelected: 'true', - css: { - [`&:hover ${HoverIndicator}`]: { - opacity: '0.25', - }, - [`&:active ${HoverIndicator}`]: { - opacity: '0.25', - }, - }, - }, - ], }) -// function Label({ children }: { children: React.ReactNode }) { -// return ( -// -// {children} -// -// ) -// } - -// function pp(n: number[]) { -// return '[' + n.map((v) => v.toFixed(1)).join(', ') + ']' -// } - -export { HoverIndicator } - export default memo(Shape) diff --git a/components/controls-panel/controls-panel.tsx b/components/controls-panel/controls-panel.tsx index 812ad5742..c3310b1ec 100644 --- a/components/controls-panel/controls-panel.tsx +++ b/components/controls-panel/controls-panel.tsx @@ -3,40 +3,44 @@ import styled from 'styles' import React, { useRef } from 'react' import state, { useSelector } from 'state' import { X, Code } from 'react-feather' -import { IconButton } from 'components/shared' +import { breakpoints, IconButton } from 'components/shared' import * as Panel from '../panel' import Control from './control' import { deepCompareArrays } from 'utils' +function openCodePanel() { + state.send('CLOSED_CODE_PANEL') +} + +function closeCodePanel() { + state.send('OPENED_CODE_PANEL') +} + const stopKeyboardPropagation = (e: KeyboardEvent | React.KeyboardEvent) => e.stopPropagation() export default function ControlPanel(): JSX.Element { const rContainer = useRef(null) + const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0) const codeControls = useSelector( (state) => Object.keys(state.data.codeControls), deepCompareArrays ) - const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0) return ( {isOpen ? ( - state.send('CLOSED_CODE_PANEL')} - > +

Controls

@@ -48,11 +52,7 @@ export default function ControlPanel(): JSX.Element {
) : ( - state.send('OPENED_CODE_PANEL')} - > + )} diff --git a/components/page-panel/page-panel.tsx b/components/page-panel/page-panel.tsx index e909e53ce..94ec4e6bd 100644 --- a/components/page-panel/page-panel.tsx +++ b/components/page-panel/page-panel.tsx @@ -2,7 +2,7 @@ import styled from 'styles' import * as ContextMenu from '@radix-ui/react-context-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { IconWrapper, RowButton } from 'components/shared' +import { breakpoints, IconWrapper, RowButton } from 'components/shared' import { CheckIcon, PlusIcon } from '@radix-ui/react-icons' import * as Panel from '../panel' import state, { useSelector } from 'state' @@ -40,8 +40,8 @@ export default function PagePanel(): JSX.Element { {documentPages[currentPageId].name} @@ -58,11 +58,7 @@ export default function PagePanel(): JSX.Element { {sorted.map(({ id, name }) => ( - + {name} @@ -91,7 +87,7 @@ export default function PagePanel(): JSX.Element { { setIsOpen(false) state.send('CREATED_PAGE') diff --git a/components/shared.tsx b/components/shared.tsx index bff0470ef..42936727b 100644 --- a/components/shared.tsx +++ b/components/shared.tsx @@ -3,6 +3,8 @@ import * as RadioGroup from '@radix-ui/react-radio-group' import * as Panel from './panel' import styled from 'styles' +export const breakpoints: any = { '@initial': 'mobile', '@sm': 'small' } + export const IconButton = styled('button', { height: '32px', width: '32px', @@ -124,6 +126,11 @@ export const RowButton = styled('button', { width: 'auto', }, }, + variant: { + pageButton: { + paddingRight: 12, + }, + }, }, }) diff --git a/components/style-panel/align-distribute.tsx b/components/style-panel/align-distribute.tsx index 8368d8593..f6ab53d9c 100644 --- a/components/style-panel/align-distribute.tsx +++ b/components/style-panel/align-distribute.tsx @@ -10,7 +10,8 @@ import { StretchHorizontallyIcon, StretchVerticallyIcon, } from '@radix-ui/react-icons' -import { IconButton } from 'components/shared' +import { breakpoints, IconButton } from 'components/shared' +import { memo } from 'react' import state from 'state' import styled from 'styles' import { AlignType, DistributeType, StretchType } from 'types' @@ -55,7 +56,7 @@ function distributeHorizontally() { state.send('DISTRIBUTED', { type: DistributeType.Horizontal }) } -export default function AlignDistribute({ +function AlignDistribute({ hasTwoOrMore, hasThreeOrMore, }: { @@ -65,7 +66,7 @@ export default function AlignDistribute({ return ( void -}): JSX.Element { +function handleColorChange( + e: Event & { currentTarget: { value: ColorStyle } } +): void { + state.send('CHANGED_STYLE', { color: e.currentTarget.value }) +} + +function ColorContent(): JSX.Element { return ( {Object.keys(strokes).map((color: ColorStyle) => ( @@ -17,7 +21,8 @@ export default function ColorContent({ as={IconButton} key={color} title={color} - onSelect={() => onChange(color)} + value={color} + onSelect={handleColorChange} > @@ -25,3 +30,5 @@ export default function ColorContent({ ) } + +export default memo(ColorContent) diff --git a/components/style-panel/color-picker.tsx b/components/style-panel/color-picker.tsx index 4ae9deb21..56a8e4b2e 100644 --- a/components/style-panel/color-picker.tsx +++ b/components/style-panel/color-picker.tsx @@ -1,28 +1,25 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import { strokes } from 'state/shape-styles' -import { ColorStyle } from 'types' -import { RowButton, IconWrapper } from '../shared' +import { RowButton, IconWrapper, breakpoints } from '../shared' import { Square } from 'react-feather' import ColorContent from './color-content' +import { memo } from 'react' +import { useSelector } from 'state' -interface Props { - color: ColorStyle - onChange: (color: ColorStyle) => void -} +function ColorPicker(): JSX.Element { + const color = useSelector((s) => s.values.selectedStyle.color) -export default function ColorPicker({ color, onChange }: Props): JSX.Element { return ( - + - + ) } + +export default memo(ColorPicker) diff --git a/components/style-panel/dash-picker.tsx b/components/style-panel/dash-picker.tsx index 5ae4e0b17..3de4d1ac9 100644 --- a/components/style-panel/dash-picker.tsx +++ b/components/style-panel/dash-picker.tsx @@ -7,40 +7,36 @@ import { } from '../shared' import * as RadioGroup from '@radix-ui/react-radio-group' import { DashStyle } from 'types' -import state from 'state' +import state, { useSelector } from 'state' +import { memo } from 'react' function handleChange(dash: string) { state.send('CHANGED_STYLE', { dash }) } -interface Props { - dash: DashStyle +const dashes = { + [DashStyle.Solid]: , + [DashStyle.Dashed]: , + [DashStyle.Dotted]: , } -export default function DashPicker({ dash }: Props): JSX.Element { +function DashPicker(): JSX.Element { + const dash = useSelector((s) => s.values.selectedStyle.dash) + return ( - - - - - - - - - + {Object.keys(DashStyle).map((dashStyle: DashStyle) => ( + + {dashes[dashStyle]} + + ))} ) } + +export default memo(DashPicker) diff --git a/components/style-panel/is-filled-picker.tsx b/components/style-panel/is-filled-picker.tsx index dc878f87f..313d15010 100644 --- a/components/style-panel/is-filled-picker.tsx +++ b/components/style-panel/is-filled-picker.tsx @@ -2,24 +2,23 @@ import * as Checkbox from '@radix-ui/react-checkbox' import { CheckIcon } from '@radix-ui/react-icons' import { strokes } from 'state/shape-styles' import { Square } from 'react-feather' -import { IconWrapper, RowButton } from '../shared' +import { breakpoints, IconWrapper, RowButton } from '../shared' +import state, { useSelector } from 'state' -interface Props { - isFilled: boolean - onChange: (isFilled: boolean | string) => void +function handleIsFilledChange(isFilled: boolean) { + state.send('CHANGED_STYLE', { isFilled }) } -export default function IsFilledPicker({ - isFilled, - onChange, -}: Props): JSX.Element { +export default function IsFilledPicker(): JSX.Element { + const isFilled = useSelector((s) => s.values.selectedStyle.isFilled) + return ( diff --git a/components/style-panel/quick-color-select.tsx b/components/style-panel/quick-color-select.tsx index b5cf048c7..e58ab898b 100644 --- a/components/style-panel/quick-color-select.tsx +++ b/components/style-panel/quick-color-select.tsx @@ -1,9 +1,9 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { IconButton } from 'components/shared' +import { breakpoints, IconButton } from 'components/shared' import Tooltip from 'components/tooltip' import { strokes } from 'state/shape-styles' import { Square } from 'react-feather' -import state, { useSelector } from 'state' +import { useSelector } from 'state' import ColorContent from './color-content' export default function QuickColorSelect(): JSX.Element { @@ -11,17 +11,12 @@ export default function QuickColorSelect(): JSX.Element { return ( - + - state.send('CHANGED_STYLE', { color })} - /> + ) } diff --git a/components/style-panel/quick-dash-select.tsx b/components/style-panel/quick-dash-select.tsx index 8cdb65e21..5aca12488 100644 --- a/components/style-panel/quick-dash-select.tsx +++ b/components/style-panel/quick-dash-select.tsx @@ -1,6 +1,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { IconButton } from 'components/shared' +import { breakpoints, IconButton } from 'components/shared' import Tooltip from 'components/tooltip' +import { memo } from 'react' import state, { useSelector } from 'state' import { DashStyle } from 'types' import { @@ -17,40 +18,35 @@ const dashes = { [DashStyle.Dotted]: , } -export default function QuickdashSelect(): JSX.Element { +function changeDashStyle( + e: Event & { currentTarget: { value: DashStyle } } +): void { + state.send('CHANGED_STYLE', { dash: e.currentTarget.value }) +} + +function QuickdashSelect(): JSX.Element { const dash = useSelector((s) => s.values.selectedStyle.dash) return ( - + {dashes[dash]} - - - + {Object.keys(DashStyle).map((dashStyle: DashStyle) => ( + + {dashes[dashStyle]} + + ))} ) } -function DashItem({ dash, isActive }: { isActive: boolean; dash: DashStyle }) { - return ( - state.send('CHANGED_STYLE', { dash })} - > - {dashes[dash]} - - ) -} +export default memo(QuickdashSelect) diff --git a/components/style-panel/quick-size-select.tsx b/components/style-panel/quick-size-select.tsx index 95424993c..f544a601d 100644 --- a/components/style-panel/quick-size-select.tsx +++ b/components/style-panel/quick-size-select.tsx @@ -1,6 +1,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { IconButton } from 'components/shared' +import { breakpoints, IconButton } from 'components/shared' import Tooltip from 'components/tooltip' +import { memo } from 'react' import { Circle } from 'react-feather' import state, { useSelector } from 'state' import { SizeStyle } from 'types' @@ -12,39 +13,37 @@ const sizes = { [SizeStyle.Large]: 22, } -export default function QuickSizeSelect(): JSX.Element { +function handleSizeChange( + e: Event & { currentTarget: { value: SizeStyle } } +): void { + state.send('CHANGED_STYLE', { size: e.currentTarget.value }) +} + +function QuickSizeSelect(): JSX.Element { const size = useSelector((s) => s.values.selectedStyle.size) return ( - + - - - + {Object.keys(SizeStyle).map((sizeStyle: SizeStyle) => ( + + + + ))} ) } -function SizeItem({ size, isActive }: { isActive: boolean; size: SizeStyle }) { - return ( - state.send('CHANGED_STYLE', { size })} - > - - - ) -} +export default memo(QuickSizeSelect) diff --git a/components/style-panel/shapes-functions.tsx b/components/style-panel/shapes-functions.tsx new file mode 100644 index 000000000..dfadc9392 --- /dev/null +++ b/components/style-panel/shapes-functions.tsx @@ -0,0 +1,217 @@ +import { IconButton, breakpoints } from 'components/shared' +import { memo } from 'react' +import styled from 'styles' +import { MoveType } from 'types' +import { Trash2 } from 'react-feather' +import state, { useSelector } from 'state' +import Tooltip from 'components/tooltip' + +import { + ArrowDownIcon, + ArrowUpIcon, + AspectRatioIcon, + BoxIcon, + CopyIcon, + EyeClosedIcon, + EyeOpenIcon, + LockClosedIcon, + LockOpen1Icon, + PinBottomIcon, + PinTopIcon, + RotateCounterClockwiseIcon, +} from '@radix-ui/react-icons' +import { getPage, getSelectedIds } from 'utils' + +function handleRotateCcw() { + state.send('ROTATED_CCW') +} + +function handleDuplicate() { + state.send('DUPLICATED') +} + +function handleHide() { + state.send('TOGGLED_SHAPE_HIDE') +} + +function handleLock() { + state.send('TOGGLED_SHAPE_LOCK') +} + +function handleAspectLock() { + state.send('TOGGLED_SHAPE_ASPECT_LOCK') +} + +function handleMoveToBack() { + state.send('MOVED', { type: MoveType.ToBack }) +} + +function handleMoveBackward() { + state.send('MOVED', { type: MoveType.Backward }) +} + +function handleMoveForward() { + state.send('MOVED', { type: MoveType.Forward }) +} + +function handleMoveToFront() { + state.send('MOVED', { type: MoveType.ToFront }) +} + +function handleDelete() { + state.send('DELETED') +} + +function ShapesFunctions() { + const isAllLocked = useSelector((s) => { + const page = getPage(s.data) + return s.values.selectedIds.every((id) => page.shapes[id].isLocked) + }) + + const isAllAspectLocked = useSelector((s) => { + const page = getPage(s.data) + return s.values.selectedIds.every( + (id) => page.shapes[id].isAspectRatioLocked + ) + }) + + const isAllHidden = useSelector((s) => { + const page = getPage(s.data) + return s.values.selectedIds.every((id) => page.shapes[id].isHidden) + }) + + const hasSelection = useSelector((s) => { + return getSelectedIds(s.data).size > 0 + }) + + return ( + <> + + + + + + + + + + + + + + + + {isAllHidden ? : } + + + + + + {isAllLocked ? : } + + + + + + {isAllAspectLocked ? : } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default memo(ShapesFunctions) + +const ButtonsRow = styled('div', { + position: 'relative', + display: 'flex', + width: '100%', + background: 'none', + border: 'none', + cursor: 'pointer', + outline: 'none', + alignItems: 'center', + justifyContent: 'flex-start', + padding: 4, +}) diff --git a/components/style-panel/size-picker.tsx b/components/style-panel/size-picker.tsx index b7d2bfa10..252d5240f 100644 --- a/components/style-panel/size-picker.tsx +++ b/components/style-panel/size-picker.tsx @@ -1,37 +1,37 @@ import { Group, Item } from '../shared' import * as RadioGroup from '@radix-ui/react-radio-group' import { Circle } from 'react-feather' -import state from 'state' +import state, { useSelector } from 'state' import { SizeStyle } from 'types' +import { memo } from 'react' + +const sizes = { + [SizeStyle.Small]: 6, + [SizeStyle.Medium]: 12, + [SizeStyle.Large]: 22, +} function handleChange(size: string) { state.send('CHANGED_STYLE', { size }) } -export default function SizePicker({ size }: { size: SizeStyle }): JSX.Element { +function SizePicker(): JSX.Element { + const size = useSelector((s) => s.values.selectedStyle.size) + return ( - - - - - - - - - + {Object.keys(SizeStyle).map((sizeStyle: SizeStyle) => ( + + + + ))} ) } + +export default memo(SizePicker) diff --git a/components/style-panel/style-panel.tsx b/components/style-panel/style-panel.tsx index 5b32ef2b9..fd38a4026 100644 --- a/components/style-panel/style-panel.tsx +++ b/components/style-panel/style-panel.tsx @@ -3,31 +3,10 @@ import state, { useSelector } from 'state' import * as Panel from 'components/panel' import { useRef } from 'react' import { IconButton } from 'components/shared' -import { ChevronDown, Trash2, X } from 'react-feather' -import { - deepCompare, - deepCompareArrays, - getPage, - getSelectedIds, - setToArray, -} from 'utils' +import { ChevronDown, X } from 'react-feather' +import ShapesFunctions from './shapes-functions' import AlignDistribute from './align-distribute' -import { MoveType } from 'types' import SizePicker from './size-picker' -import { - ArrowDownIcon, - ArrowUpIcon, - AspectRatioIcon, - BoxIcon, - CopyIcon, - EyeClosedIcon, - EyeOpenIcon, - LockClosedIcon, - LockOpen1Icon, - PinBottomIcon, - PinTopIcon, - RotateCounterClockwiseIcon, -} from '@radix-ui/react-icons' import DashPicker from './dash-picker' import QuickColorSelect from './quick-color-select' import ColorPicker from './color-picker' @@ -37,23 +16,12 @@ import QuickdashSelect from './quick-dash-select' import Tooltip from 'components/tooltip' const breakpoints = { '@initial': 'mobile', '@sm': 'small' } as any + const handleStylePanelOpen = () => state.send('TOGGLED_STYLE_PANEL_OPEN') -const handleColorChange = (color) => state.send('CHANGED_STYLE', { color }) -const handleRotateCcw = () => () => state.send('ROTATED_CCW') -const handleIsFilledChange = (dash) => state.send('CHANGED_STYLE', { dash }) -const handleDuplicate = () => state.send('DUPLICATED') -const handleHide = () => state.send('TOGGLED_SHAPE_HIDE') -const handleLock = () => state.send('TOGGLED_SHAPE_LOCK') -const handleAspectLock = () => state.send('TOGGLED_SHAPE_ASPECT_LOCK') -const handleMoveToBack = () => state.send('MOVED', { type: MoveType.ToBack }) -const handleMoveBackward = () => - state.send('MOVED', { type: MoveType.Backward }) -const handleMoveForward = () => state.send('MOVED', { type: MoveType.Forward }) -const handleMoveToFront = () => state.send('MOVED', { type: MoveType.ToFront }) -const handleDelete = () => state.send('DELETED') export default function StylePanel(): JSX.Element { const rContainer = useRef(null) + const isOpen = useSelector((s) => s.data.settings.isStyleOpen) return ( @@ -86,29 +54,7 @@ export default function StylePanel(): JSX.Element { // track of this data manually within our state. function SelectedShapeStyles(): JSX.Element { - const selectedIds = useSelector( - (s) => setToArray(getSelectedIds(s.data)), - deepCompareArrays - ) - - const isAllLocked = useSelector((s) => { - const page = getPage(s.data) - return selectedIds.every((id) => page.shapes[id].isLocked) - }) - - const isAllAspectLocked = useSelector((s) => { - const page = getPage(s.data) - return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked) - }) - - const isAllHidden = useSelector((s) => { - const page = getPage(s.data) - return selectedIds.every((id) => page.shapes[id].isHidden) - }) - - const commonStyle = useSelector((s) => s.values.selectedStyle, deepCompare) - - const hasSelection = selectedIds.length > 0 + const selectedShapesCount = useSelector((s) => s.values.selectedIds.length) return ( @@ -123,133 +69,20 @@ function SelectedShapeStyles(): JSX.Element { - - + + - + - + - - - - - - - - - - - - - - - - {isAllHidden ? : } - - - - - - {isAllLocked ? : } - - - - - - {isAllAspectLocked ? : } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 1} - hasThreeOrMore={selectedIds.length > 2} + hasTwoOrMore={selectedShapesCount > 1} + hasThreeOrMore={selectedShapesCount > 2} /> @@ -306,16 +139,3 @@ const Row = styled('div', { position: 'relative', }, }) - -const ButtonsRow = styled('div', { - position: 'relative', - display: 'flex', - width: '100%', - background: 'none', - border: 'none', - cursor: 'pointer', - outline: 'none', - alignItems: 'center', - justifyContent: 'flex-start', - padding: 4, -}) diff --git a/hooks/usePageShapes.ts b/hooks/usePageShapes.ts index 5311b1341..e2f0912f3 100644 --- a/hooks/usePageShapes.ts +++ b/hooks/usePageShapes.ts @@ -26,7 +26,7 @@ export default function usePageShapes(): string[] { }, []) // Get the shapes that fit into the current window - return useSelector((s) => { + const visiblePageShapeIds = useSelector((s) => { const pageState = getPageState(s.data) if (!viewportCache.has(pageState)) { @@ -46,4 +46,6 @@ export default function usePageShapes(): string[] { }) .map((shape) => shape.id) }, deepCompareArrays) + + return visiblePageShapeIds } diff --git a/state/state.ts b/state/state.ts index 8336a51d0..6671bec84 100644 --- a/state/state.ts +++ b/state/state.ts @@ -1870,9 +1870,17 @@ const state = createState({ data.boundsRotation = 0 }, }, + asyncs: { + async getUpdatedShapes(data) { + return updateFromCode( + data, + data.document.code[data.currentCodeFileId].code + ) + }, + }, values: { selectedIds(data) { - return new Set(getSelectedIds(data)) + return setToArray(getSelectedIds(data)) }, selectedBounds(data) { return getSelectionBounds(data) @@ -1915,14 +1923,6 @@ const state = createState({ return commonStyle }, }, - asyncs: { - async getUpdatedShapes(data) { - return updateFromCode( - data, - data.document.code[data.currentCodeFileId].code - ) - }, - }, }) export default state