diff --git a/gui/package.json b/gui/package.json index a5cd407e..20a2fcbf 100644 --- a/gui/package.json +++ b/gui/package.json @@ -20,11 +20,11 @@ "test-watch": "vitest" }, "dependencies": { - "@devbookhq/splitter": "^1.4.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fi-sci/misc": "^0.0.8", "@fi-sci/modal-window": "^0.0.2", + "@geoffcox/react-splitter": "^2.1.2", "@monaco-editor/react": "^4.6.0", "@mui/icons-material": "^5.15.17", "@mui/material": "^5.15.17", diff --git a/gui/src/app/FileEditor/StanFileEditor.tsx b/gui/src/app/FileEditor/StanFileEditor.tsx index 1ed6df8a..50c10173 100644 --- a/gui/src/app/FileEditor/StanFileEditor.tsx +++ b/gui/src/app/FileEditor/StanFileEditor.tsx @@ -1,8 +1,8 @@ +import { Split } from "@geoffcox/react-splitter"; import { AutoFixHigh, Cancel, Settings } from "@mui/icons-material"; -import useMediaQuery from "@mui/material/useMediaQuery"; -import { SplitDirection, Splitter } from "@SpComponents/Splitter"; import StanCompileResultWindow from "@SpComponents/StanCompileResultWindow"; -import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; +import TextEditor from "@SpComponents/TextEditor"; +import { ToolbarItem } from "@SpComponents/ToolBar"; import compileStanProgram from "@SpStanc/compileStanProgram"; import { stancErrorsToCodeMarkers } from "@SpStanc/Linting"; import useStanc from "@SpStanc/useStanc"; @@ -123,7 +123,6 @@ const StanFileEditor: FunctionComponent = ({ } }, [fileContent, handleCompile, didInitialCompile]); - const showLabelsOnButtons = useMediaQuery("(min-width:600px)"); const [syntaxWindowVisible, setSyntaxWindowVisible] = useState(false); const toolbarItems: ToolbarItem[] = useMemo(() => { @@ -134,7 +133,7 @@ const StanFileEditor: FunctionComponent = ({ ret.push({ type: "button", icon: , - label: showLabelsOnButtons ? "Syntax error" : "", + label: "Syntax error", color: "darkred", tooltip: "Syntax error in Stan file", onClick: () => { @@ -145,7 +144,7 @@ const StanFileEditor: FunctionComponent = ({ ret.push({ type: "button", icon: , - label: showLabelsOnButtons ? "Syntax warning" : "", + label: "Syntax warning", color: "blue", tooltip: "Syntax warning in Stan file", onClick: () => { @@ -155,17 +154,15 @@ const StanFileEditor: FunctionComponent = ({ } // auto format - if (!readOnly) { - if (editedFileContent) { - ret.push({ - type: "button", - icon: , - tooltip: "Auto format this stan file", - label: showLabelsOnButtons ? "auto format" : undefined, - onClick: requestFormat, - color: "darkblue", - }); - } + if (!readOnly && editedFileContent && validSyntax) { + ret.push({ + type: "button", + icon: , + tooltip: "Auto format this stan file", + label: "Auto format", + onClick: requestFormat, + color: "darkblue", + }); } if (editedFileContent && editedFileContent === fileContent) { if (compileStatus !== "compiling") { @@ -173,7 +170,7 @@ const StanFileEditor: FunctionComponent = ({ ret.push({ type: "button", tooltip: "Compile Stan model", - label: "compile", + label: "Compile", icon: , onClick: handleCompile, color: "darkblue", @@ -183,7 +180,8 @@ const StanFileEditor: FunctionComponent = ({ if (compileStatus !== "") { ret.push({ type: "text", - label: compileMessage, + label: + compileMessage.charAt(0).toUpperCase() + compileMessage.slice(1), color: compileStatus === "compiled" ? "green" @@ -200,7 +198,6 @@ const StanFileEditor: FunctionComponent = ({ fileContent, handleCompile, requestFormat, - showLabelsOnButtons, validSyntax, compileStatus, compileMessage, @@ -219,25 +216,30 @@ const StanFileEditor: FunctionComponent = ({ <> ); - const initialSizes = syntaxWindowVisible ? [60, 40] : [100, 0]; + const editor = ( + + ); return ( - - + + {editor} {window} - + ); }; diff --git a/gui/src/app/FileEditor/TextEditor.tsx b/gui/src/app/FileEditor/TextEditor.tsx index 0156df67..7931e46c 100644 --- a/gui/src/app/FileEditor/TextEditor.tsx +++ b/gui/src/app/FileEditor/TextEditor.tsx @@ -1,16 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { SmallIconButton } from "@fi-sci/misc"; import { Editor, loader, useMonaco } from "@monaco-editor/react"; -import { Save } from "@mui/icons-material"; -import Link from "@mui/material/Link"; + import monacoAddStanLang from "@SpComponents/stanLang"; +import { ToolBar, ToolbarItem } from "@SpComponents/ToolBar"; import { CodeMarker } from "@SpStanc/Linting"; import { editor, MarkerSeverity } from "monaco-editor"; import { FunctionComponent, - PropsWithChildren, useCallback, useEffect, + useMemo, useState, } from "react"; @@ -30,21 +29,6 @@ type Props = { contentOnEmpty?: string | HTMLSpanElement; }; -export type ToolbarItem = - | { - type: "button"; - tooltip?: string; - label?: string; - icon?: any; - onClick?: () => void; - color?: string; - } - | { - type: "text"; - label: string; - color?: string; - }; - const TextEditor: FunctionComponent = ({ text, onSaveText, @@ -131,32 +115,19 @@ const TextEditor: FunctionComponent = ({ [onSaveText, readOnly], ); + const edited = useMemo(() => { + return editedText !== text; + }, [editedText, text]); + return (
- -
- {label} -     - {!readOnly && text !== editedText && ( - } - title="Save file" - disabled={text === editedText} - label="save" - /> - )} -     - {editedText !== text && edited} -     - {readOnly && read only} -     - {toolbarItems && - toolbarItems.map((item, i) => ( - - ))} -
-
+ = ({ readOnly, domReadOnly: readOnly, wordWrap: "on", + minimap: { enabled: false }, }} />
@@ -186,61 +158,6 @@ const toMonacoMarkerSeverity = ( } }; -const ToolbarItemComponent: FunctionComponent<{ item: ToolbarItem }> = ({ - item, -}) => { - if (item.type === "button") { - const { onClick, color, label, tooltip, icon } = item; - if (icon) { - return ( - - -     - - ); - } else { - if (!onClick) { - return ( - - {label}    - - ); - } - return ( - - - {label} - -     - - ); - } - } else if (item.type === "text") { - return ( - - {item.label}    - - ); - } else { - return unknown toolbar item type; - } -}; - -const NotSelectable: FunctionComponent = ({ children }) => { - return
{children}
; -}; - const createHintTextContentWidget = (content: string | HTMLSpanElement) => { return { getDomNode: () => { diff --git a/gui/src/app/FileEditor/ToolBar.tsx b/gui/src/app/FileEditor/ToolBar.tsx new file mode 100644 index 00000000..51ced36b --- /dev/null +++ b/gui/src/app/FileEditor/ToolBar.tsx @@ -0,0 +1,125 @@ +import { FunctionComponent, useMemo } from "react"; +import { Save } from "@mui/icons-material"; +import Link from "@mui/material/Link"; +import IconButton from "@mui/material/IconButton"; + +export type ToolbarItem = + | { + type: "button"; + tooltip?: string; + label?: string; + icon?: any; + onClick: () => void; + color?: string; + } + | { + type: "text"; + label: string; + color?: string; + }; + +type ToolbarProps = { + items: ToolbarItem[]; + label: string; + onSaveText: () => void; + edited: boolean; + readOnly: boolean; +}; + +export const ToolBar: FunctionComponent = ({ + items, + label, + onSaveText, + edited, + readOnly, +}) => { + const toolBarItems = useMemo(() => { + const editorItems: ToolbarItem[] = []; + + if (readOnly) { + editorItems.push({ + type: "text", + label: "Read Only", + color: "gray", + }); + } else if (edited) { + editorItems.push({ + type: "button", + icon: , + onClick: onSaveText, + tooltip: "Save file", + label: "Save", + }); + editorItems.push({ + type: "text", + label: "Edited", + color: "red", + }); + } + + return editorItems.concat(items); + }, [edited, items, onSaveText, readOnly]); + + return ( +
+
+ {label} + {toolBarItems && + toolBarItems.map((item, i) => ( + + ))} +
+
+ ); +}; + +const ToolbarItemComponent: FunctionComponent<{ item: ToolbarItem }> = ({ + item, +}) => { + if (item.type === "button") { + const { onClick, color, label, tooltip, icon } = item; + if (icon) { + return ( + + + {icon} + {label && {label}} + + + ); + } else { + return ( + + + {label} + +     + + ); + } + } else if (item.type === "text") { + return ( + + {item.label}    + + ); + } else { + return unknown toolbar item type; + } +}; diff --git a/gui/src/app/Scripting/PlottingScriptEditor.tsx b/gui/src/app/Scripting/PlottingScriptEditor.tsx index 0a6028bc..64aed71c 100644 --- a/gui/src/app/Scripting/PlottingScriptEditor.tsx +++ b/gui/src/app/Scripting/PlottingScriptEditor.tsx @@ -1,15 +1,15 @@ import { FunctionComponent, RefObject } from "react"; import ScriptEditor, { ScriptEditorProps } from "./ScriptEditor"; -import { SplitDirection, Splitter } from "@SpComponents/Splitter"; +import { Split } from "@geoffcox/react-splitter"; const PlottingScriptEditor: FunctionComponent< ScriptEditorProps & { imagesRef: RefObject } > = (props) => { return ( - + - + ); }; diff --git a/gui/src/app/Scripting/ScriptEditor.tsx b/gui/src/app/Scripting/ScriptEditor.tsx index 97ec153f..86969d64 100644 --- a/gui/src/app/Scripting/ScriptEditor.tsx +++ b/gui/src/app/Scripting/ScriptEditor.tsx @@ -1,6 +1,6 @@ import { Help, PlayArrow } from "@mui/icons-material"; -import { SplitDirection, Splitter } from "@SpComponents/Splitter"; -import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; +import TextEditor from "@SpComponents/TextEditor"; +import { ToolbarItem } from "@SpComponents/ToolBar"; import { FileNames } from "@SpCore/FileMapping"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; @@ -12,6 +12,7 @@ import { useMemo, } from "react"; import { InterpreterStatus } from "./InterpreterTypes"; +import { Split } from "@geoffcox/react-splitter"; const interpreterNames = { python: "pyodide", r: "webR" } as const; @@ -91,7 +92,7 @@ const ScriptEditor: FunctionComponent = ({ ]); return ( - + = ({ contentOnEmpty={contentOnEmpty} /> - + ); }; diff --git a/gui/src/app/Scripting/pyodide/pyodideWorker.ts b/gui/src/app/Scripting/pyodide/pyodideWorker.ts index 049e0be2..3c6cce28 100644 --- a/gui/src/app/Scripting/pyodide/pyodideWorker.ts +++ b/gui/src/app/Scripting/pyodide/pyodideWorker.ts @@ -147,7 +147,8 @@ patch_matplotlib(_SP_ADD_IMAGE) postamble += ` import matplotlib.pyplot as plt -plt.show() +if len(plt.gcf().get_children()) > 1: + plt.show() `; } diff --git a/gui/src/app/Scripting/webR/runR.ts b/gui/src/app/Scripting/webR/runR.ts index 9d56ee87..f314ce81 100644 --- a/gui/src/app/Scripting/webR/runR.ts +++ b/gui/src/app/Scripting/webR/runR.ts @@ -30,8 +30,8 @@ const captureOutputOptions = { captureStreams: true, captureConditions: false, captureGraphics: { - width: 340, - height: 340, + height: 504, // default values for height and width + width: 504, bg: "white", // default: transparent pointsize: 12, capture: true, @@ -103,6 +103,7 @@ invisible(.SP_DATA)`; // Set canvas size to image canvas.width = img.width; canvas.height = img.height; + canvas.style.width = "100%"; // Draw image onto Canvas const ctx = canvas.getContext("2d"); diff --git a/gui/src/app/components/Splitter.tsx b/gui/src/app/components/Splitter.tsx deleted file mode 100644 index d167b3f3..00000000 --- a/gui/src/app/components/Splitter.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// @devbookhq/splitter has a known issue, https://github.com/DevbookHQ/splitter/issues/11, -// where re-renders of internal components can cause the splitter to lose its state. -// This wrapper captures the splitter sizes and stores them in a state to avoid this issue. -import DevbookSplit, { - SplitDirection as DevbookSplitDirection, - GutterTheme as DevbookGutterTheme, -} from "@devbookhq/splitter"; -import { - FunctionComponent, - useState, - useCallback, - PropsWithChildren, - useEffect, -} from "react"; - -interface SplitterProps { - direction?: keyof typeof DevbookSplitDirection; - minWidths?: number[]; - minHeights?: number[]; - initialSizes?: number[]; - gutterTheme?: keyof typeof DevbookGutterTheme; - gutterClassName?: string; - draggerClassName?: string; - onResizeStarted?: (pairIdx: number) => void; - onResizeFinished?: (pairIdx: number, newSizes: number[]) => void; - classes?: string[]; -} - -export const SplitDirection = DevbookSplitDirection; -export const GutterTheme = DevbookGutterTheme; - -export const Splitter: FunctionComponent> = ({ - direction = "Horizontal", - gutterTheme = "Light", - children, - initialSizes, - ...props -}) => { - const [persistentSizes, setPersistentSizes] = useState( - initialSizes, - ); - - useEffect(() => { - setPersistentSizes(initialSizes); - }, [initialSizes]); - - const handleResizeFinished = useCallback((_: number, newSizes: number[]) => { - setPersistentSizes(newSizes); - }, []); - - return ( - - {children} - - ); -}; diff --git a/gui/src/app/components/TabWidget.tsx b/gui/src/app/components/TabWidget.tsx index 3c941e8d..2691aa9a 100644 --- a/gui/src/app/components/TabWidget.tsx +++ b/gui/src/app/components/TabWidget.tsx @@ -31,7 +31,12 @@ const TabWidget: FunctionComponent = ({ labels, children }) => { return ( - + {labels.map((label, i) => ( ))} diff --git a/gui/src/app/pages/HomePage/HomePage.tsx b/gui/src/app/pages/HomePage/HomePage.tsx index bcd116ec..75042619 100644 --- a/gui/src/app/pages/HomePage/HomePage.tsx +++ b/gui/src/app/pages/HomePage/HomePage.tsx @@ -1,7 +1,6 @@ import Box from "@mui/material/Box"; import styled from "@mui/material/styles/styled"; import useMediaQuery from "@mui/material/useMediaQuery"; -import { GutterTheme, SplitDirection, Splitter } from "@SpComponents/Splitter"; import StanFileEditor from "@SpComponents/StanFileEditor"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; import { @@ -23,6 +22,7 @@ import { FileNames } from "@SpCore/FileMapping"; import DataRWindow from "@SpScripting/DataGeneration/DataRWindow"; import DataPyWindow from "@SpScripting/DataGeneration/DataPyWindow"; import TextEditor from "@SpComponents/TextEditor"; +import { Split } from "@geoffcox/react-splitter"; type Props = { // @@ -63,14 +63,10 @@ const HomePage: FunctionComponent = () => { /> - + - + ); @@ -103,10 +99,7 @@ const LeftView: FunctionComponent = ({ }) => { const { data, update } = useContext(ProjectContext); return ( - + = ({ readOnly={false} contentOnEmpty={"Enter JSON data or use the data generation tab"} /> - + ); }; diff --git a/gui/src/app/pages/HomePage/TopBar.tsx b/gui/src/app/pages/HomePage/TopBar.tsx index 3d07d14f..918bd37d 100644 --- a/gui/src/app/pages/HomePage/TopBar.tsx +++ b/gui/src/app/pages/HomePage/TopBar.tsx @@ -18,7 +18,12 @@ const TopBar: FunctionComponent = ({ title, onSetCollapsed }) => { position="sticky" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }} > - +