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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /* eslint-disable @typescript-eslint/ban-ts-comment */
  2. import styled from "styles"
  3. import { useStateDesigner } from "@state-designer/react"
  4. import React, { useEffect, useRef } from "react"
  5. import { motion } from "framer-motion"
  6. import state, { useSelector } from "state"
  7. import { CodeFile } from "types"
  8. import CodeDocs from "./code-docs"
  9. import CodeEditor from "./code-editor"
  10. import { getShapesFromCode } from "lib/code/generate"
  11. import {
  12. X,
  13. Code,
  14. Info,
  15. PlayCircle,
  16. ChevronUp,
  17. ChevronDown,
  18. } from "react-feather"
  19. const getErrorLineAndColumn = (e: any) => {
  20. if ("line" in e) {
  21. return { line: Number(e.line), column: e.column }
  22. }
  23. const result = e.stack.match(/:([0-9]+):([0-9]+)/)
  24. if (result) {
  25. return { line: Number(result[1]) - 1, column: result[2] }
  26. }
  27. }
  28. export default function CodePanel() {
  29. const rContainer = useRef<HTMLDivElement>(null)
  30. const isReadOnly = useSelector((s) => s.data.isReadOnly)
  31. const fileId = useSelector((s) => s.data.currentCodeFileId)
  32. const file = useSelector(
  33. (s) => s.data.document.code[s.data.currentCodeFileId]
  34. )
  35. const isOpen = true
  36. const fontSize = useSelector((s) => s.data.settings.fontSize)
  37. const local = useStateDesigner({
  38. data: {
  39. code: file.code,
  40. error: null as { message: string; line: number; column: number } | null,
  41. },
  42. on: {
  43. MOUNTED: "setCode",
  44. CHANGED_FILE: "loadFile",
  45. },
  46. initial: "editingCode",
  47. states: {
  48. editingCode: {
  49. on: {
  50. RAN_CODE: "runCode",
  51. SAVED_CODE: ["runCode", "saveCode"],
  52. CHANGED_CODE: { secretlyDo: "setCode" },
  53. CLEARED_ERROR: { if: "hasError", do: "clearError" },
  54. TOGGLED_DOCS: { to: "viewingDocs" },
  55. },
  56. },
  57. viewingDocs: {
  58. on: {
  59. TOGGLED_DOCS: { to: "editingCode" },
  60. },
  61. },
  62. },
  63. conditions: {
  64. hasError(data) {
  65. return !!data.error
  66. },
  67. },
  68. actions: {
  69. loadFile(data, payload: { file: CodeFile }) {
  70. data.code = payload.file.code
  71. },
  72. setCode(data, payload: { code: string }) {
  73. data.code = payload.code
  74. },
  75. runCode(data) {
  76. let error = null
  77. try {
  78. const shapes = getShapesFromCode(data.code)
  79. state.send("GENERATED_SHAPES_FROM_CODE", { shapes })
  80. } catch (e) {
  81. console.error(e)
  82. error = { message: e.message, ...getErrorLineAndColumn(e) }
  83. }
  84. data.error = error
  85. },
  86. saveCode(data) {
  87. const { code } = data
  88. state.send("SAVED_CODE", { code })
  89. },
  90. clearError(data) {
  91. data.error = null
  92. },
  93. },
  94. })
  95. useEffect(() => {
  96. local.send("CHANGED_FILE", { file })
  97. }, [file])
  98. useEffect(() => {
  99. local.send("MOUNTED", { code: state.data.document.code[fileId].code })
  100. return () => {
  101. state.send("CHANGED_CODE", { fileId, code: local.data.code })
  102. }
  103. }, [])
  104. const { error } = local.data
  105. return (
  106. <PanelContainer
  107. data-bp-desktop
  108. ref={rContainer}
  109. dragMomentum={false}
  110. isCollapsed={!isOpen}
  111. >
  112. {isOpen ? (
  113. <Content>
  114. <Header>
  115. <IconButton onClick={() => state.send("CLOSED_CODE_PANEL")}>
  116. <X />
  117. </IconButton>
  118. <h3>Code</h3>
  119. <ButtonsGroup>
  120. <FontSizeButtons>
  121. <IconButton
  122. disabled={!local.isIn("editingCode")}
  123. onClick={() => state.send("INCREASED_CODE_FONT_SIZE")}
  124. >
  125. <ChevronUp />
  126. </IconButton>
  127. <IconButton
  128. disabled={!local.isIn("editingCode")}
  129. onClick={() => state.send("DECREASED_CODE_FONT_SIZE")}
  130. >
  131. <ChevronDown />
  132. </IconButton>
  133. </FontSizeButtons>
  134. <IconButton onClick={() => local.send("TOGGLED_DOCS")}>
  135. <Info />
  136. </IconButton>
  137. <IconButton
  138. disabled={!local.isIn("editingCode")}
  139. onClick={() => local.send("SAVED_CODE")}
  140. >
  141. <PlayCircle />
  142. </IconButton>
  143. </ButtonsGroup>
  144. </Header>
  145. <EditorContainer>
  146. <CodeEditor
  147. fontSize={fontSize}
  148. readOnly={isReadOnly}
  149. value={file.code}
  150. error={error}
  151. onChange={(code) => local.send("CHANGED_CODE", { code })}
  152. onSave={() => local.send("SAVED_CODE")}
  153. onKey={() => local.send("CLEARED_ERROR")}
  154. />
  155. <CodeDocs isHidden={!local.isIn("viewingDocs")} />
  156. </EditorContainer>
  157. <ErrorContainer>
  158. {error &&
  159. (error.line
  160. ? `(${Number(error.line) - 2}:${error.column}) ${error.message}`
  161. : error.message)}
  162. </ErrorContainer>
  163. </Content>
  164. ) : (
  165. <IconButton onClick={() => state.send("OPENED_CODE_PANEL")}>
  166. <Code />
  167. </IconButton>
  168. )}
  169. </PanelContainer>
  170. )
  171. }
  172. const PanelContainer = styled(motion.div, {
  173. position: "absolute",
  174. top: "48px",
  175. right: "8px",
  176. bottom: "48px",
  177. backgroundColor: "$panel",
  178. borderRadius: "4px",
  179. overflow: "hidden",
  180. border: "1px solid $border",
  181. pointerEvents: "all",
  182. userSelect: "none",
  183. zIndex: 200,
  184. button: {
  185. border: "none",
  186. },
  187. variants: {
  188. isCollapsed: {
  189. true: {},
  190. false: {},
  191. },
  192. },
  193. })
  194. const IconButton = styled("button", {
  195. height: "40px",
  196. width: "40px",
  197. backgroundColor: "$panel",
  198. borderRadius: "4px",
  199. border: "1px solid $border",
  200. padding: "0",
  201. margin: "0",
  202. display: "flex",
  203. alignItems: "center",
  204. justifyContent: "center",
  205. outline: "none",
  206. pointerEvents: "all",
  207. cursor: "pointer",
  208. "&:hover:not(:disabled)": {
  209. backgroundColor: "$panel",
  210. },
  211. "&:disabled": {
  212. opacity: "0.5",
  213. },
  214. svg: {
  215. height: "20px",
  216. width: "20px",
  217. strokeWidth: "2px",
  218. stroke: "$text",
  219. },
  220. })
  221. const Content = styled("div", {
  222. display: "grid",
  223. gridTemplateColumns: "1fr",
  224. gridTemplateRows: "auto 1fr 28px",
  225. height: "100%",
  226. width: 560,
  227. minWidth: "100%",
  228. maxWidth: 560,
  229. overflow: "hidden",
  230. userSelect: "none",
  231. pointerEvents: "all",
  232. })
  233. const Header = styled("div", {
  234. pointerEvents: "all",
  235. display: "grid",
  236. gridTemplateColumns: "auto 1fr",
  237. alignItems: "center",
  238. justifyContent: "center",
  239. borderBottom: "1px solid $border",
  240. "& button": {
  241. gridColumn: "1",
  242. gridRow: "1",
  243. },
  244. "& h3": {
  245. gridColumn: "1 / span 3",
  246. gridRow: "1",
  247. textAlign: "center",
  248. margin: "0",
  249. padding: "0",
  250. fontSize: "16px",
  251. },
  252. })
  253. const ButtonsGroup = styled("div", {
  254. gridRow: "1",
  255. gridColumn: "3",
  256. display: "flex",
  257. })
  258. const EditorContainer = styled("div", {
  259. position: "relative",
  260. pointerEvents: "all",
  261. overflowY: "scroll",
  262. })
  263. const ErrorContainer = styled("div", {
  264. overflowX: "scroll",
  265. color: "$text",
  266. font: "$debug",
  267. padding: "0 12px",
  268. display: "flex",
  269. alignItems: "center",
  270. })
  271. const FontSizeButtons = styled("div", {
  272. paddingRight: 4,
  273. "& > button": {
  274. height: "50%",
  275. width: "100%",
  276. "&:nth-of-type(1)": {
  277. paddingTop: 4,
  278. },
  279. "&:nth-of-type(2)": {
  280. paddingBottom: 4,
  281. },
  282. "& svg": {
  283. height: 12,
  284. },
  285. },
  286. })