From ea3e8d29aaf08d2ecd768e178d5d75cc46336311 Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Wed, 21 Feb 2024 18:37:38 +1100 Subject: [PATCH] Implement window drag and drop --- client/src/App.tsx | 8 +- client/src/components/app-bar/Playback.tsx | 4 +- client/src/components/generic/Flex.tsx | 28 +- .../inspector/FullscreenModalHost.tsx | 144 ++++++++++ .../src/components/inspector/ViewControls.tsx | 32 ++- client/src/components/inspector/ViewTree.tsx | 209 +++++++++++---- client/src/components/inspector/index.tsx | 101 +------ client/src/components/title-bar/TitleBar.tsx | 20 +- client/src/pages/AboutPage.tsx | 4 +- client/src/pages/DebugPage.tsx | 4 +- client/src/pages/ExplorePage.tsx | 6 +- client/src/pages/InfoPage.tsx | 4 +- client/src/pages/LayersPage.tsx | 4 +- client/src/pages/Page.tsx | 14 +- client/src/pages/PageMeta.tsx | 5 +- client/src/pages/RecipesPage.tsx | 4 +- client/src/pages/SettingsPage.tsx | 34 ++- client/src/pages/StepsPage.tsx | 4 +- client/src/pages/TreePage.tsx | 4 +- client/src/pages/ViewportPage.tsx | 3 +- client/src/pages/index.tsx | 4 +- client/src/services/SettingsService.tsx | 6 +- client/src/slices/settings.ts | 21 +- client/src/slices/view.ts | 38 ++- client/src/theme.tsx | 2 +- package-lock.json | 250 +++++++++++++++++- package.json | 5 +- 27 files changed, 701 insertions(+), 261 deletions(-) create mode 100644 client/src/components/inspector/FullscreenModalHost.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 4e08fe5d..3c7d661e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -45,8 +45,12 @@ function App() { } function ThemedApp() { - const [{ theme: mode = "light", accentColor: accent = "teal" }] = - useSettings(); + const [ + { + "appearance/theme": mode = "light", + "appearance/accentColor": accent = "teal", + }, + ] = useSettings(); const theme = useMemo(() => makeTheme(mode, accent), [mode, accent]); return ( diff --git a/client/src/components/app-bar/Playback.tsx b/client/src/components/app-bar/Playback.tsx index 3ce27d5e..d286a6fd 100644 --- a/client/src/components/app-bar/Playback.tsx +++ b/client/src/components/app-bar/Playback.tsx @@ -4,7 +4,7 @@ import { SkipNextOutlined as SkipIcon, PauseOutlined as PauseIcon, PlayArrowOutlined as PlayIcon, - StopOutlined as StopIcon + StopOutlined as StopIcon, } from "@mui/icons-material"; import { EditorSetterProps } from "components/Editor"; import { IconButtonWithTooltip as Button } from "components/generic/IconButtonWithTooltip"; @@ -28,7 +28,7 @@ export function PlaybackService({ const { step, end, playing, pause, stepWithBreakpointCheck } = usePlaybackState(value?.key); - const [{ playbackRate = 1 }] = useSettings(); + const [{ "playback/playbackRate": playbackRate = 1 }] = useSettings(); useEffect(() => { if (playing) { diff --git a/client/src/components/generic/Flex.tsx b/client/src/components/generic/Flex.tsx index a7a2fa0a..260bf288 100644 --- a/client/src/components/generic/Flex.tsx +++ b/client/src/components/generic/Flex.tsx @@ -1,18 +1,18 @@ -import { Box, BoxProps } from "@mui/material"; - +import { Box, BoxProps } from "@mui/material"; +import { forwardRef } from "react"; + export type FlexProps = { vertical?: boolean; } & BoxProps; -export function Flex({ vertical, ...props }: FlexProps) { - return ( - - ); -} \ No newline at end of file +export const Flex = forwardRef(({ vertical, ...props }: FlexProps, ref) => ( + +)); diff --git a/client/src/components/inspector/FullscreenModalHost.tsx b/client/src/components/inspector/FullscreenModalHost.tsx new file mode 100644 index 00000000..db7ca6fc --- /dev/null +++ b/client/src/components/inspector/FullscreenModalHost.tsx @@ -0,0 +1,144 @@ +import { Box, Checkbox, FormControlLabel, Typography } from "@mui/material"; +import { Flex } from "components/generic/Flex"; +import Modal, { ModalAppBar } from "components/generic/Modal"; +import { Scroll } from "components/generic/Scrollbars"; +import { Space } from "components/generic/Space"; +import { useSmallDisplay } from "hooks/useSmallDisplay"; +import { pages } from "pages"; +import { PageSlots } from "pages/Page"; +import { ReactNode, useMemo, useState } from "react"; +import { withSlots } from "react-slot-component"; +import { useUIState } from "slices/UIState"; +import { useSettings } from "slices/settings"; +import { PanelState } from "slices/view"; +import { wait } from "utils/timed"; + +export type FullscreenPageProps = { + renderExtras?: (content?: PanelState) => ReactNode; + controls?: ReactNode; + children?: ReactNode; + showOnStartUpChecked?: boolean; + onShowOnStartUpCheckedChange?: (v: boolean) => void; +}; + +export const FullscreenPage = withSlots( + ({ slotProps, showOnStartUpChecked, onShowOnStartUpCheckedChange }) => { + const sm = useSmallDisplay(); + return ( + + {!!slotProps.Options?.children && ( + t.spacing(6) }}> + t.palette.background.paper, + }} + > + + t.spacing(6), + alignItems: "center", + p: 1, + }} + > + {slotProps.Options?.children && ( + <>{slotProps.Options.children} + )} + + + onShowOnStartUpCheckedChange?.(v)} + /> + } + /> + + + {slotProps.Extras?.children} + + )} + + + {slotProps.Content?.children} + + + + ); + } +); + +export function FullscreenModalHost() { + const [{ "behaviour/showOnStart": showOnStart }, setSettings] = useSettings(); + const [{ fullscreenModal: key }, setUIState] = useUIState(); + const [closing, setClosing] = useState(false); + + async function handleClose() { + setClosing(true); + await wait(300); + setUIState(() => ({ fullscreenModal: undefined })); + setClosing(false); + } + + const page = key ? pages[key] : undefined; + + const content = useMemo(() => { + if (page) { + const PageContent = page.content; + + const FullScreenPageTemplate = withSlots( + ({ slotProps, ...props }) => ( + + setSettings(() => ({ + "behaviour/showOnStart": v ? key : undefined, + })) + } + showOnStartUpChecked={showOnStart === key} + > + + {slotProps!.Content?.children} + + + {slotProps!.Options?.children} + + + ) + ); + return ; + } + }, [key, page]); + + return ( + !!page && ( + + + {page.name} + + {content} + + ) + ); +} diff --git a/client/src/components/inspector/ViewControls.tsx b/client/src/components/inspector/ViewControls.tsx index 3da67e6c..3820b54a 100644 --- a/client/src/components/inspector/ViewControls.tsx +++ b/client/src/components/inspector/ViewControls.tsx @@ -20,6 +20,7 @@ type ViewControlsProps = { splitVerticalDisabled?: boolean; splitHorizontalDisabled?: boolean; closeDisabled?: boolean; + popOutDisabled?: boolean; onSplitVertical?: () => void; onSplitHorizontal?: () => void; onClose?: () => void; @@ -34,6 +35,7 @@ export function ViewControls({ splitHorizontalDisabled, splitVerticalDisabled, onPopOut, + popOutDisabled, }: ViewControlsProps) { return ( @@ -79,19 +81,23 @@ export function ViewControls({ Split Horizontal - { - onPopOut?.(); - state.close(); - }} - disabled={closeDisabled} - > - - - - Pop Out - - + {!(popOutDisabled || closeDisabled) && ( + <> + { + onPopOut?.(); + onClose?.(); + state.close(); + }} + > + + + + Pop Out + + + + )} { onClose?.(); diff --git a/client/src/components/inspector/ViewTree.tsx b/client/src/components/inspector/ViewTree.tsx index e5e05725..b68d660e 100644 --- a/client/src/components/inspector/ViewTree.tsx +++ b/client/src/components/inspector/ViewTree.tsx @@ -1,18 +1,37 @@ import Split, { SplitDirection } from "@devbookhq/splitter"; -import { useTheme } from "@mui/material"; +import { DragIndicatorOutlined } from "@mui/icons-material"; +import { Box, useTheme } from "@mui/material"; import { Flex } from "components/generic/Flex"; -import { filter, forEach, map, sumBy } from "lodash"; +import { filter, find, flatMap, forEach, map, sumBy } from "lodash"; import { nanoid } from "nanoid"; import { produce, produce2 } from "produce"; import { Context, ReactNode, createContext, useContext, useMemo } from "react"; +import { DndProvider, useDrag, useDrop } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; import { useCss } from "react-use"; import { Leaf, Root } from "slices/view"; import { ViewControls } from "./ViewControls"; +type TreeNode = + | { + children?: S[]; + } + | object; + +function findInTree>( + data: T, + iterator: (a: T) => boolean +): T | undefined { + const f = (a: T): T[] => + "children" in a && a.children?.length ? flatMap(a.children, f) : [a]; + return find(f(data), iterator); +} + type ViewTreeContextType = { controls?: ReactNode; onChange?: (state: Partial) => void; state?: T; + dragHandle?: ReactNode; }; const ViewTreeContext = createContext({}); @@ -28,16 +47,149 @@ type ViewTreeProps = { onClose?: () => void; depth?: number; onPopOut?: (leaf: Leaf) => void; + canPopOut?: (leaf: Leaf) => boolean; }; -export function ViewTree({ +type ViewBranchProps = ViewTreeProps & { + onSwap?: (a: string, b: string) => void; +}; + +type ViewLeafProps = ViewBranchProps & { root?: Leaf }; + +function handleSwap(root: Root, a: string, b: string) { + const leafA = findInTree(root, (c) => c.key === a); + const leafB = findInTree(root, (c) => c.key === b); + if (leafA?.type === "leaf" && leafB?.type === "leaf") { + const leafAContent = leafA.content; + const leafBContent = leafB.content; + leafA.content = leafBContent; + leafB.content = leafAContent; + } + return root; +} +export function ViewTree(props: ViewTreeProps) { + const { onChange, root } = props; + return ( + + + {...props} + onSwap={(a, b) => { + if (root) { + onChange?.(produce(root, (root) => handleSwap(root, a, b))); + } + }} + /> + + ); +} + +export function ViewLeaf({ root = { type: "leaf", key: "" }, renderLeaf, onChange, onClose, onPopOut, + canPopOut, depth = 0, -}: ViewTreeProps) { + onSwap, +}: ViewLeafProps) { + const [{ isOver }, drop] = useDrop< + { key: string }, + void, + { isOver: boolean } + >(() => ({ + accept: ["panel"], + collect: (monitor) => ({ + isOver: monitor.isOver() && monitor.getItem().key !== root.key, + }), + drop: (item) => onSwap?.(item.key, root.key), + })); + const [{ isDragging }, drag] = useDrag(() => ({ + type: "panel", + item: { key: root.key }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + })); + + const context = useMemo(() => { + const handleSplit = (orientation: "vertical" | "horizontal") => + onChange?.( + produce2(root, (draft) => ({ + key: nanoid(), + type: "branch", + orientation, + children: [ + { ...structuredClone(draft), size: 50, key: nanoid() }, + { ...structuredClone(draft), size: 50, key: nanoid() }, + ], + })) + ); + return root.type === "leaf" + ? { + state: root.content, + controls: ( + handleSplit("horizontal")} + onSplitVertical={() => handleSplit("vertical")} + onPopOut={() => onPopOut?.(root)} + popOutDisabled={!canPopOut?.(root)} + /> + ), + dragHandle: ( + + + + ), + onChange: (c: any) => + onChange?.( + produce(root, (draft) => { + draft.content = { ...draft.content, ...c }; + }) + ), + } + : {}; + }, [onChange, onClose, depth, root, drag]); + + return ( + <> + + isOver ? `inset 0 0 0 2px ${t.palette.primary.main}` : "none", + transition: (t) => t.transitions.create("box-shadow"), + }, + transition: (t) => t.transitions.create("opacity"), + opacity: (t) => (isDragging ? t.palette.action.disabledOpacity : 1), + }} + > + + {renderLeaf?.(root)} + + + + ); +} + +export function ViewBranch(props: ViewBranchProps) { + const { root = { type: "leaf", key: "" }, onChange, depth = 0 } = props; const { palette, spacing, transitions } = useTheme(); const dragCls = useCss({ @@ -74,52 +226,10 @@ export function ViewTree({ return undef ? space / undef : 0; } - const context = useMemo(() => { - const handleSplit = (orientation: "vertical" | "horizontal") => - onChange?.( - produce2(root, (draft) => ({ - key: nanoid(), - type: "branch", - orientation, - children: [ - { ...structuredClone(draft), size: 50, key: nanoid() }, - { ...structuredClone(draft), size: 50, key: nanoid() }, - ], - })) - ); - - return root.type === "leaf" - ? { - state: root.content, - controls: ( - handleSplit("horizontal")} - onSplitVertical={() => handleSplit("vertical")} - onPopOut={() => onPopOut?.(root)} - /> - ), - onChange: (c: any) => - onChange?.( - produce(root, (draft) => { - draft.content = { ...draft.content, ...c }; - }) - ), - } - : {}; - }, [onChange, onClose, depth, root]); - return ( <> {root.type === "leaf" ? ( - - - - {renderLeaf?.(root)} - - - + {...(props as ViewLeafProps)} /> ) : ( ({ } > {map(root.children, (c, i) => ( - onChange?.( produce(root, (draft) => (draft.children[i] = newChild)) ) } - onPopOut={onPopOut} onClose={() => onChange?.( produce2(root, (draft) => { diff --git a/client/src/components/inspector/index.tsx b/client/src/components/inspector/index.tsx index 8a4dceb5..1e5e7980 100644 --- a/client/src/components/inspector/index.tsx +++ b/client/src/components/inspector/index.tsx @@ -1,107 +1,15 @@ -import { Box, Fade, LinearProgress, Typography } from "@mui/material"; +import { Box, Fade, LinearProgress } from "@mui/material"; import { Flex, FlexProps } from "components/generic/Flex"; -import Modal, { ModalAppBar } from "components/generic/Modal"; -import { Scroll } from "components/generic/Scrollbars"; -import { Space } from "components/generic/Space"; import { pages } from "pages"; -import { Page, Slots } from "pages/Page"; -import { ReactNode, createElement, useState } from "react"; -import { withSlots } from "react-slot-component"; +import { Page } from "pages/Page"; +import { createElement } from "react"; import { useUIState } from "slices/UIState"; import { useAnyLoading } from "slices/loading"; import { PanelState, useView } from "slices/view"; -import { wait } from "utils/timed"; import { FullscreenProgress } from "./FullscreenProgress"; import { ViewTree } from "./ViewTree"; import { WorkspaceDropZone } from "./WorkspaceDropZone"; -import { useSmallDisplay } from "hooks/useSmallDisplay"; - -export type FullscreenPageProps = { - renderExtras?: (content?: PanelState) => ReactNode; - controls?: ReactNode; - children?: ReactNode; -}; - -export const FullscreenPage = withSlots( - ({ slotProps }) => { - const sm = useSmallDisplay(); - return ( - - {!!slotProps.Options?.children && ( - t.spacing(6) }}> - t.palette.background.paper, - }} - > - - t.spacing(6), - alignItems: "center", - p: 1, - }} - > - {slotProps.Options?.children && ( - <>{slotProps.Options.children} - )} - - - - - {slotProps.Extras?.children} - - )} - - - {slotProps.Content?.children} - - - - ); - } -); - -export function FullscreenModalHost() { - const [{ fullscreenModal: key }, setUIState] = useUIState(); - const [closing, setClosing] = useState(false); - async function handleClose() { - setClosing(true); - await wait(300); - setUIState(() => ({ fullscreenModal: undefined })); - setClosing(false); - } - const name = key ? pages[key].name : undefined; - return ( - !!key && ( - - - {name} - - {createElement(pages[key].content, { - template: FullscreenPage, - })} - - ) - ); -} +import { FullscreenModalHost } from "./FullscreenModalHost"; type SpecimenInspectorProps = Record & FlexProps; @@ -117,6 +25,7 @@ export function Inspector(props: SpecimenInspectorProps) { onPopOut={(leaf) => setUIState(() => ({ fullscreenModal: leaf.content?.type })) } + canPopOut={(leaf) => !!pages[leaf.content!.type!]?.allowFullscreen} root={view} onChange={(v) => setView(() => ({ view: v }))} renderLeaf={({ content }) => ( diff --git a/client/src/components/title-bar/TitleBar.tsx b/client/src/components/title-bar/TitleBar.tsx index f092f2f2..835618aa 100644 --- a/client/src/components/title-bar/TitleBar.tsx +++ b/client/src/components/title-bar/TitleBar.tsx @@ -80,8 +80,8 @@ export const TitleBar = () => { orientation, key: id(), children: [ - { ...view, size: 75 }, - { type: "leaf", key: id(), content: { type }, size: 25 }, + { ...view, size: 80 }, + { type: "leaf", key: id(), content: { type }, size: 20 }, ], }, }; @@ -112,6 +112,14 @@ export const TitleBar = () => { {[ + { + key: "Panel", + items: values(pages).map(({ name, id, icon }) => ({ + key: `panel-open-${id}`, + name: , + action: () => handleOpenPanel(id), + })), + }, { key: "workspace", items: [ @@ -137,14 +145,6 @@ export const TitleBar = () => { }, ], }, - { - key: "Panel", - items: values(pages).map(({ name, id, icon }) => ({ - key: `panel-open-${id}`, - name: , - action: () => handleOpenPanel(id), - })), - }, { key: "help", items: [ diff --git a/client/src/pages/AboutPage.tsx b/client/src/pages/AboutPage.tsx index d33f3f0f..d09af8e2 100644 --- a/client/src/pages/AboutPage.tsx +++ b/client/src/pages/AboutPage.tsx @@ -30,7 +30,8 @@ const contacts = [ ]; export function AboutPage({ template: Page }: PageContentProps) { - const { controls, onChange, state } = useViewTreeContext(); + const { controls, onChange, state, dragHandle } = useViewTreeContext(); + function renderSection(label: ReactNode, content: ReactNode) { return ( @@ -43,6 +44,7 @@ export function AboutPage({ template: Page }: PageContentProps) { } return ( + {dragHandle} {" "} diff --git a/client/src/pages/DebugPage.tsx b/client/src/pages/DebugPage.tsx index 05ac64e1..fcd36bcb 100644 --- a/client/src/pages/DebugPage.tsx +++ b/client/src/pages/DebugPage.tsx @@ -24,7 +24,8 @@ const divider = ( ); export function DebugPage({ template: Page }: PageContentProps) { - const { controls, onChange, state } = useViewTreeContext(); + const { controls, onChange, state, dragHandle } = useViewTreeContext(); + const [tab, setTab] = useState("standard"); const { key, setKey, layers, layer, setLayer } = useLayer(); const { code } = layer?.source ?? {}; @@ -38,6 +39,7 @@ export function DebugPage({ template: Page }: PageContentProps) { return ( + {dragHandle} } diff --git a/client/src/pages/ExplorePage.tsx b/client/src/pages/ExplorePage.tsx index c3ae74c2..87af7c3a 100644 --- a/client/src/pages/ExplorePage.tsx +++ b/client/src/pages/ExplorePage.tsx @@ -132,7 +132,7 @@ export function FeatureCard({ onOpenClick, ...rest }: Partial & CardProps & { onOpenClick?: () => void }) { - const [{ acrylic }] = useSettings(); + const [{ "appearance/acrylic": acrylic }] = useSettings(); const paper = usePaper(); const { name: authorName, avatar } = useMemo( @@ -233,7 +233,7 @@ export function FeatureCard({ export function ExplorePage({ template: Page }: PageContentProps) { const notify = useSnackbar(); - const { controls, onChange, state } = useViewTreeContext(); + const { controls, onChange, state, dragHandle } = useViewTreeContext(); const [search, setSearch] = useState(""); const [tab, setTab] = useState("explore"); @@ -272,10 +272,10 @@ export function ExplorePage({ template: Page }: PageContentProps) { ), [search, files] ); - return ( + {dragHandle} setTab(v)}> diff --git a/client/src/pages/InfoPage.tsx b/client/src/pages/InfoPage.tsx index 7aa98064..3478c173 100644 --- a/client/src/pages/InfoPage.tsx +++ b/client/src/pages/InfoPage.tsx @@ -9,10 +9,12 @@ import { useLog } from "slices/log"; import { PageContentProps } from "./PageMeta"; export function InfoPage({ template: Page }: PageContentProps) { - const { controls, onChange, state } = useViewTreeContext(); + const { controls, onChange, state, dragHandle } = useViewTreeContext(); + const [log] = useLog(); return ( + {dragHandle} {log.length ? ( diff --git a/client/src/pages/LayersPage.tsx b/client/src/pages/LayersPage.tsx index 347f2f37..56dd2775 100644 --- a/client/src/pages/LayersPage.tsx +++ b/client/src/pages/LayersPage.tsx @@ -3,9 +3,11 @@ import { useViewTreeContext } from "components/inspector/ViewTree"; import { LayerListEditor } from "components/layer-editor/LayerListEditor"; import { PageContentProps } from "./PageMeta"; export function LayersPage({ template: Page }: PageContentProps) { - const { controls, onChange, state } = useViewTreeContext(); + const { controls, onChange, state, dragHandle } = useViewTreeContext(); + return ( + {dragHandle} diff --git a/client/src/pages/Page.tsx b/client/src/pages/Page.tsx index 5f6c4a2f..d6d66a09 100644 --- a/client/src/pages/Page.tsx +++ b/client/src/pages/Page.tsx @@ -9,7 +9,6 @@ import { ReactNode } from "react"; import { withSlots } from "react-slot-component"; import { PanelState } from "slices/view"; import { useAcrylic } from "theme"; -import { DragIndicatorOutlined } from "@mui/icons-material"; const divider = ( ( +export const Page = withSlots( ({ slotProps, onChange, stack }) => { const acrylic = useAcrylic(); return ( @@ -79,11 +81,7 @@ export const Page = withSlots( p: 1, }} > - + {slotProps.Handle?.children} >; + template: ReturnType>; }; export type PageMeta = { @@ -14,4 +14,5 @@ export type PageMeta = { color?: AccentColor; description?: string; content: (props: PageContentProps) => ReactNode; + allowFullscreen?: boolean; }; diff --git a/client/src/pages/RecipesPage.tsx b/client/src/pages/RecipesPage.tsx index 7e3e8b30..623cbcc1 100644 --- a/client/src/pages/RecipesPage.tsx +++ b/client/src/pages/RecipesPage.tsx @@ -26,7 +26,8 @@ function basename(path: string) { export function RecipesPage({ template: Page }: PageContentProps) { const notify = useSnackbar(); - const { controls, onChange, state } = useViewTreeContext(); + const { controls, onChange, state, dragHandle } = useViewTreeContext(); + const { load } = useWorkspace(); const usingLoadingState = useLoadingState(); @@ -62,6 +63,7 @@ export function RecipesPage({ template: Page }: PageContentProps) { return ( + {dragHandle} diff --git a/client/src/pages/SettingsPage.tsx b/client/src/pages/SettingsPage.tsx index 1ae51212..bd914321 100644 --- a/client/src/pages/SettingsPage.tsx +++ b/client/src/pages/SettingsPage.tsx @@ -25,14 +25,15 @@ import { PageContentProps } from "./PageMeta"; const formatLabel = (v: number) => `${v}x`; export function SettingsPage({ template: Page }: PageContentProps) { - const { controls, onChange, state } = useViewTreeContext(); + const { controls, onChange, state, dragHandle } = useViewTreeContext(); + const [ { - playbackRate = 1, - acrylic, - theme = "light", - accentColor = "teal", - "behaviour/showExplorePageOnStart": behaviourShowExplorePageOnStart, + "playback/playbackRate": playbackRate = 1, + "appearance/acrylic": acrylic, + "appearance/theme": theme = "light", + "appearance/accentColor": accentColor = "teal", + "behaviour/showOnStart": showOnStart, }, setSettings, ] = useSettings(); @@ -50,6 +51,7 @@ export function SettingsPage({ template: Page }: PageContentProps) { return ( + {dragHandle} setTab(v)}> @@ -81,7 +83,9 @@ export function SettingsPage({ template: Page }: PageContentProps) { valueLabelDisplay="auto" defaultValue={playbackRate} onChangeCommitted={(_, v) => - setSettings(() => ({ playbackRate: v as number })) + setSettings(() => ({ + "playback/playbackRate": v as number, + })) } /> @@ -91,7 +95,9 @@ export function SettingsPage({ template: Page }: PageContentProps) { setSettings(() => ({ acrylic: v }))} + onChange={(_, v) => + setSettings(() => ({ "appearance/acrylic": v })) + } /> @@ -100,7 +106,9 @@ export function SettingsPage({ template: Page }: PageContentProps) { - setSettings(() => ({ theme: v ? "dark" : "light" })) + setSettings(() => ({ + "appearance/theme": v ? "dark" : "light", + })) } /> @@ -115,7 +123,9 @@ export function SettingsPage({ template: Page }: PageContentProps) { }))} showArrow onChange={(v) => - setSettings(() => ({ accentColor: v as AccentColor })) + setSettings(() => ({ + "appearance/accentColor": v as AccentColor, + })) } /> @@ -124,10 +134,10 @@ export function SettingsPage({ template: Page }: PageContentProps) { {renderLabel("Show Explore Panel on Start-up")} setSettings(() => ({ - "behaviour/showExplorePageOnStart": v, + "behaviour/showOnStart": v ? "explore" : undefined, })) } /> diff --git a/client/src/pages/StepsPage.tsx b/client/src/pages/StepsPage.tsx index 8b77df62..535a89e5 100644 --- a/client/src/pages/StepsPage.tsx +++ b/client/src/pages/StepsPage.tsx @@ -35,7 +35,8 @@ const pxToInt = (s: string) => Number(s.replace(/px$/, "")); export function StepsPage({ template: Page }: PageContentProps) { const { spacing } = useTheme(); - const { controls, onChange, state } = useViewTreeContext(); + const { controls, onChange, state, dragHandle } = useViewTreeContext(); + const ref = useRef(null); const { key, setKey, layers, layer } = useLayer(); const { step, playing, stepTo } = usePlaybackState(key); @@ -70,6 +71,7 @@ export function StepsPage({ template: Page }: PageContentProps) { return ( + {dragHandle} {steps ? ( diff --git a/client/src/pages/TreePage.tsx b/client/src/pages/TreePage.tsx index 516f5c39..0a2ec140 100644 --- a/client/src/pages/TreePage.tsx +++ b/client/src/pages/TreePage.tsx @@ -90,7 +90,8 @@ export function TreePage({ template: Page }: PageContentProps) { const { key, setKey, layer, setLayer, layers } = useLayer(); const throttledStep = useThrottle(layer?.source?.step ?? 0, 600); - const { controls, onChange, state } = useViewTreeContext(); + const { controls, onChange, state, dragHandle } = + useViewTreeContext(); const [radius, setRadius] = useState("small"); @@ -118,6 +119,7 @@ export function TreePage({ template: Page }: PageContentProps) { return ( + {dragHandle} {layer?.source?.trace?.content && cache?.tree ? ( diff --git a/client/src/pages/ViewportPage.tsx b/client/src/pages/ViewportPage.tsx index 37caeb7a..4d46e9be 100644 --- a/client/src/pages/ViewportPage.tsx +++ b/client/src/pages/ViewportPage.tsx @@ -49,7 +49,7 @@ export function autoSelectRenderer( } export function ViewportPage({ template: Page }: PageContentProps) { - const { controls, onChange, state } = + const { controls, onChange, state, dragHandle } = useViewTreeContext(); const [renderers] = useRenderers(); @@ -87,6 +87,7 @@ export function ViewportPage({ template: Page }: PageContentProps) { return ( + {dragHandle} diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx index d1e6fb61..d9f43c44 100644 --- a/client/src/pages/index.tsx +++ b/client/src/pages/index.tsx @@ -15,12 +15,11 @@ import { DebugPage } from "./DebugPage"; import { ExplorePage } from "./ExplorePage"; import { InfoPage } from "./InfoPage"; import { LayersPage } from "./LayersPage"; +import { PageMeta } from "./PageMeta"; import { SettingsPage } from "./SettingsPage"; import { StepsPage } from "./StepsPage"; import { TreePage } from "./TreePage"; import { ViewportPage } from "./ViewportPage"; -import { Page } from "./Page"; -import { PageMeta } from "./PageMeta"; export const pages: Dictionary = { explore: { @@ -30,6 +29,7 @@ export const pages: Dictionary = { description: "Browse a library of examples and guides", icon: , content: ExplorePage, + allowFullscreen: true, }, layers: { id: "layers", diff --git a/client/src/services/SettingsService.tsx b/client/src/services/SettingsService.tsx index 5c04adaa..0bfc0ca4 100644 --- a/client/src/services/SettingsService.tsx +++ b/client/src/services/SettingsService.tsx @@ -3,12 +3,12 @@ import { useUIState } from "slices/UIState"; import { useSettings } from "slices/settings"; export function SettingsService() { - const [{ "behaviour/showExplorePageOnStart": showExplore }, , initialised] = + const [{ "behaviour/showOnStart": showOnStart }, , initialised] = useSettings(); const [, setUIState] = useUIState(); useEffect(() => { - if (showExplore && initialised) { - setUIState(() => ({ fullscreenModal: "explore" })); + if (showOnStart && initialised) { + setUIState(() => ({ fullscreenModal: showOnStart })); } }, [initialised]); return <>; diff --git a/client/src/slices/settings.ts b/client/src/slices/settings.ts index 0a22d8e2..696a68f9 100644 --- a/client/src/slices/settings.ts +++ b/client/src/slices/settings.ts @@ -1,3 +1,4 @@ +import type { pages } from "pages"; import { createSlice, withLocalStorage } from "./createSlice"; import { AccentColor } from "theme"; @@ -18,11 +19,11 @@ export type Renderer = { type Settings = { remote?: Remote[]; renderer?: Renderer[]; - playbackRate?: number; - acrylic?: boolean; - theme?: "dark" | "light"; - accentColor?: AccentColor; - "behaviour/showExplorePageOnStart"?: boolean; + "playback/playbackRate"?: number; + "appearance/acrylic"?: boolean; + "appearance/theme"?: "dark" | "light"; + "appearance/accentColor"?: AccentColor; + "behaviour/showOnStart"?: keyof typeof pages; }; export const defaultRemotes = [ @@ -46,11 +47,11 @@ export const defaultPlaybackRate = 1; const defaults = { renderer: defaultRenderers, remote: defaultRemotes, - playbackRate: defaultPlaybackRate, - theme: "dark", - acrylic: true, - accentColor: "blue", - "behaviour/showExplorePageOnStart": true, + "playback/playbackRate": defaultPlaybackRate, + "appearance/theme": "dark", + "appearance/acrylic": true, + "appearance/accentColor": "blue", + "behaviour/showOnStart": "explore", } as Settings; export const [useSettings, SettingsProvider] = createSlice( diff --git a/client/src/slices/view.ts b/client/src/slices/view.ts index 46d0fc3c..e838d348 100644 --- a/client/src/slices/view.ts +++ b/client/src/slices/view.ts @@ -41,45 +41,37 @@ export const [useView, ViewProvider] = createSlice< } : { view: { - key: id(), + size: 80, type: "branch", + key: id(), orientation: "horizontal", children: [ - { key: id(), type: "leaf", size: 20, content: { type: "explore" } }, { - size: 80, type: "branch", key: id(), - orientation: "horizontal", + orientation: "vertical", + size: 25, children: [ { - type: "branch", + type: "leaf", + size: 40, key: id(), - orientation: "vertical", - size: 25, - children: [ - { - type: "leaf", - size: 40, - key: id(), - content: { type: "layers" }, - }, - { - type: "leaf", - size: 60, - key: id(), - content: { type: "steps" }, - }, - ], + content: { type: "layers" }, }, { - size: 75, type: "leaf", + size: 60, key: id(), - content: { type: "viewport" }, + content: { type: "steps" }, }, ], }, + { + size: 75, + type: "leaf", + key: id(), + content: { type: "viewport" }, + }, ], }, } diff --git a/client/src/theme.tsx b/client/src/theme.tsx index b8b6d106..abb8db7c 100644 --- a/client/src/theme.tsx +++ b/client/src/theme.tsx @@ -98,7 +98,7 @@ export const makeTheme = (mode: "light" | "dark", theme: AccentColor) => }); export function useAcrylic(): SxProps { - const [{ acrylic }] = useSettings(); + const [{ "appearance/acrylic": acrylic }] = useSettings(); return acrylic ? { backdropFilter: "blur(10px)", diff --git a/package-lock.json b/package-lock.json index dc8d1265..c46d9228 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,255 @@ "name": "app", "version": "1.0.5", "hasInstallScript": true, - "license": "ISC" + "license": "ISC", + "dependencies": { + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + } + }, + "dependencies": { + "@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, + "dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "requires": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "requires": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + } + }, + "react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "requires": { + "dnd-core": "^16.0.1" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" } } } diff --git a/package.json b/package.json index c340be42..682474b3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,9 @@ "url": "https://github.com/path-visualiser/app/issues" }, "homepage": "https://path-visualiser.github.io/app", - "dependencies": {}, + "dependencies": { + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1" + }, "type": "module" }