You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ActionButton.tsx 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import * as React from 'react'
  2. import { Tooltip } from '~components/Primitives/Tooltip/Tooltip'
  3. import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
  4. import { useTldrawApp } from '~hooks'
  5. import { styled } from '~styles'
  6. import { AlignType, TDSnapshot, DistributeType, StretchType } from '~types'
  7. import {
  8. ArrowDownIcon,
  9. ArrowUpIcon,
  10. AspectRatioIcon,
  11. CopyIcon,
  12. DotsHorizontalIcon,
  13. GroupIcon,
  14. LockClosedIcon,
  15. LockOpen1Icon,
  16. PinBottomIcon,
  17. PinTopIcon,
  18. RotateCounterClockwiseIcon,
  19. AlignBottomIcon,
  20. AlignCenterHorizontallyIcon,
  21. AlignCenterVerticallyIcon,
  22. AlignLeftIcon,
  23. AlignRightIcon,
  24. AlignTopIcon,
  25. SpaceEvenlyHorizontallyIcon,
  26. SpaceEvenlyVerticallyIcon,
  27. StretchHorizontallyIcon,
  28. StretchVerticallyIcon,
  29. BoxIcon,
  30. AngleIcon,
  31. } from '@radix-ui/react-icons'
  32. import { DMContent } from '~components/Primitives/DropdownMenu'
  33. import { Divider } from '~components/Primitives/Divider'
  34. import { TrashIcon } from '~components/Primitives/icons'
  35. import { ToolButton } from '~components/Primitives/ToolButton'
  36. const selectedShapesCountSelector = (s: TDSnapshot) =>
  37. s.document.pageStates[s.appState.currentPageId].selectedIds.length
  38. const isAllLockedSelector = (s: TDSnapshot) => {
  39. const page = s.document.pages[s.appState.currentPageId]
  40. const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
  41. return selectedIds.every((id) => page.shapes[id].isLocked)
  42. }
  43. const isAllAspectLockedSelector = (s: TDSnapshot) => {
  44. const page = s.document.pages[s.appState.currentPageId]
  45. const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
  46. return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
  47. }
  48. const isAllGroupedSelector = (s: TDSnapshot) => {
  49. const page = s.document.pages[s.appState.currentPageId]
  50. const selectedShapes = s.document.pageStates[s.appState.currentPageId].selectedIds.map(
  51. (id) => page.shapes[id]
  52. )
  53. return selectedShapes.every(
  54. (shape) =>
  55. shape.children !== undefined ||
  56. (shape.parentId === selectedShapes[0].parentId &&
  57. selectedShapes[0].parentId !== s.appState.currentPageId)
  58. )
  59. }
  60. const hasSelectionSelector = (s: TDSnapshot) => {
  61. const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
  62. return selectedIds.length > 0
  63. }
  64. const hasMultipleSelectionSelector = (s: TDSnapshot) => {
  65. const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
  66. return selectedIds.length > 1
  67. }
  68. export function ActionButton(): JSX.Element {
  69. const app = useTldrawApp()
  70. const isAllLocked = app.useStore(isAllLockedSelector)
  71. const isAllAspectLocked = app.useStore(isAllAspectLockedSelector)
  72. const isAllGrouped = app.useStore(isAllGroupedSelector)
  73. const hasSelection = app.useStore(hasSelectionSelector)
  74. const hasMultipleSelection = app.useStore(hasMultipleSelectionSelector)
  75. const selectedShapesCount = app.useStore(selectedShapesCountSelector)
  76. const hasTwoOrMore = selectedShapesCount > 1
  77. const hasThreeOrMore = selectedShapesCount > 2
  78. const handleRotate = React.useCallback(() => {
  79. app.rotate()
  80. }, [app])
  81. const handleDuplicate = React.useCallback(() => {
  82. app.duplicate()
  83. }, [app])
  84. const handleToggleLocked = React.useCallback(() => {
  85. app.toggleLocked()
  86. }, [app])
  87. const handleToggleAspectRatio = React.useCallback(() => {
  88. app.toggleAspectRatioLocked()
  89. }, [app])
  90. const handleGroup = React.useCallback(() => {
  91. app.group()
  92. }, [app])
  93. const handleMoveToBack = React.useCallback(() => {
  94. app.moveToBack()
  95. }, [app])
  96. const handleMoveBackward = React.useCallback(() => {
  97. app.moveBackward()
  98. }, [app])
  99. const handleMoveForward = React.useCallback(() => {
  100. app.moveForward()
  101. }, [app])
  102. const handleMoveToFront = React.useCallback(() => {
  103. app.moveToFront()
  104. }, [app])
  105. const handleResetAngle = React.useCallback(() => {
  106. app.setShapeProps({ rotation: 0 })
  107. }, [app])
  108. const alignTop = React.useCallback(() => {
  109. app.align(AlignType.Top)
  110. }, [app])
  111. const alignCenterVertical = React.useCallback(() => {
  112. app.align(AlignType.CenterVertical)
  113. }, [app])
  114. const alignBottom = React.useCallback(() => {
  115. app.align(AlignType.Bottom)
  116. }, [app])
  117. const stretchVertically = React.useCallback(() => {
  118. app.stretch(StretchType.Vertical)
  119. }, [app])
  120. const distributeVertically = React.useCallback(() => {
  121. app.distribute(DistributeType.Vertical)
  122. }, [app])
  123. const alignLeft = React.useCallback(() => {
  124. app.align(AlignType.Left)
  125. }, [app])
  126. const alignCenterHorizontal = React.useCallback(() => {
  127. app.align(AlignType.CenterHorizontal)
  128. }, [app])
  129. const alignRight = React.useCallback(() => {
  130. app.align(AlignType.Right)
  131. }, [app])
  132. const stretchHorizontally = React.useCallback(() => {
  133. app.stretch(StretchType.Horizontal)
  134. }, [app])
  135. const distributeHorizontally = React.useCallback(() => {
  136. app.distribute(DistributeType.Horizontal)
  137. }, [app])
  138. return (
  139. <DropdownMenu.Root dir="ltr">
  140. <DropdownMenu.Trigger dir="ltr" asChild>
  141. <ToolButton variant="circle">
  142. <DotsHorizontalIcon />
  143. </ToolButton>
  144. </DropdownMenu.Trigger>
  145. <DMContent sideOffset={16}>
  146. <>
  147. <ButtonsRow>
  148. <ToolButton variant="icon" disabled={!hasSelection} onClick={handleDuplicate}>
  149. <Tooltip label="Duplicate" kbd={`#D`}>
  150. <CopyIcon />
  151. </Tooltip>
  152. </ToolButton>
  153. <ToolButton disabled={!hasSelection} onClick={handleRotate}>
  154. <Tooltip label="Rotate">
  155. <RotateCounterClockwiseIcon />
  156. </Tooltip>
  157. </ToolButton>
  158. <ToolButton disabled={!hasSelection} onClick={handleToggleLocked}>
  159. <Tooltip label="Toggle Locked" kbd={`#L`}>
  160. {isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
  161. </Tooltip>
  162. </ToolButton>
  163. <ToolButton disabled={!hasSelection} onClick={handleToggleAspectRatio}>
  164. <Tooltip label="Toggle Aspect Ratio Lock">
  165. {isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
  166. </Tooltip>
  167. </ToolButton>
  168. <ToolButton
  169. disabled={!hasSelection || (!isAllGrouped && !hasMultipleSelection)}
  170. onClick={handleGroup}
  171. >
  172. <Tooltip label="Group" kbd={`#G`}>
  173. <GroupIcon />
  174. </Tooltip>
  175. </ToolButton>
  176. </ButtonsRow>
  177. <ButtonsRow>
  178. <ToolButton disabled={!hasSelection} onClick={handleMoveToBack}>
  179. <Tooltip label="Move to Back" kbd={`#⇧[`}>
  180. <PinBottomIcon />
  181. </Tooltip>
  182. </ToolButton>
  183. <ToolButton disabled={!hasSelection} onClick={handleMoveBackward}>
  184. <Tooltip label="Move Backward" kbd={`#[`}>
  185. <ArrowDownIcon />
  186. </Tooltip>
  187. </ToolButton>
  188. <ToolButton disabled={!hasSelection} onClick={handleMoveForward}>
  189. <Tooltip label="Move Forward" kbd={`#]`}>
  190. <ArrowUpIcon />
  191. </Tooltip>
  192. </ToolButton>
  193. <ToolButton disabled={!hasSelection} onClick={handleMoveToFront}>
  194. <Tooltip label="Move to Front" kbd={`#⇧]`}>
  195. <PinTopIcon />
  196. </Tooltip>
  197. </ToolButton>
  198. <ToolButton disabled={!hasSelection} onClick={handleResetAngle}>
  199. <Tooltip label="Reset Angle">
  200. <AngleIcon />
  201. </Tooltip>
  202. </ToolButton>
  203. </ButtonsRow>
  204. <Divider />
  205. <ButtonsRow>
  206. <ToolButton disabled={!hasTwoOrMore} onClick={alignLeft}>
  207. <AlignLeftIcon />
  208. </ToolButton>
  209. <ToolButton disabled={!hasTwoOrMore} onClick={alignCenterHorizontal}>
  210. <AlignCenterHorizontallyIcon />
  211. </ToolButton>
  212. <ToolButton disabled={!hasTwoOrMore} onClick={alignRight}>
  213. <AlignRightIcon />
  214. </ToolButton>
  215. <ToolButton disabled={!hasTwoOrMore} onClick={stretchHorizontally}>
  216. <StretchHorizontallyIcon />
  217. </ToolButton>
  218. <ToolButton disabled={!hasThreeOrMore} onClick={distributeHorizontally}>
  219. <SpaceEvenlyHorizontallyIcon />
  220. </ToolButton>
  221. </ButtonsRow>
  222. <ButtonsRow>
  223. <ToolButton disabled={!hasTwoOrMore} onClick={alignTop}>
  224. <AlignTopIcon />
  225. </ToolButton>
  226. <ToolButton disabled={!hasTwoOrMore} onClick={alignCenterVertical}>
  227. <AlignCenterVerticallyIcon />
  228. </ToolButton>
  229. <ToolButton disabled={!hasTwoOrMore} onClick={alignBottom}>
  230. <AlignBottomIcon />
  231. </ToolButton>
  232. <ToolButton disabled={!hasTwoOrMore} onClick={stretchVertically}>
  233. <StretchVerticallyIcon />
  234. </ToolButton>
  235. <ToolButton disabled={!hasThreeOrMore} onClick={distributeVertically}>
  236. <SpaceEvenlyVerticallyIcon />
  237. </ToolButton>
  238. </ButtonsRow>
  239. </>
  240. </DMContent>
  241. </DropdownMenu.Root>
  242. )
  243. }
  244. export const ButtonsRow = styled('div', {
  245. position: 'relative',
  246. display: 'flex',
  247. width: '100%',
  248. background: 'none',
  249. border: 'none',
  250. cursor: 'pointer',
  251. outline: 'none',
  252. alignItems: 'center',
  253. justifyContent: 'flex-start',
  254. padding: 0,
  255. })