diff --git a/package.json b/package.json index d682689abd..a7a63d689e 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@types/rbush": "^3.0.0", "@types/react": "^18.0.26", "@types/react-dom": "^18.0.0", + "@types/react-resizable": "^3.0.7", "@types/react-virtualized-auto-sizer": "^1.0.0", "@types/set-value": "^4.0.1", "@types/string-template": "^1.0.2", diff --git a/packages/app-core/src/ui/App/App.tsx b/packages/app-core/src/ui/App/App.tsx index ccdace9c0a..91a763b880 100644 --- a/packages/app-core/src/ui/App/App.tsx +++ b/packages/app-core/src/ui/App/App.tsx @@ -10,7 +10,8 @@ import { MenuItem as JBMenuItem } from '@jbrowse/core/ui/Menu' // locals import AppToolbar from './AppToolbar' import ViewLauncher from './ViewLauncher' -import ViewPanel from './ViewPanel' +import StaticViewPanel from './StaticViewPanel' +import FloatingViewPanel from './FloatingViewPanel' import DialogQueue from './DialogQueue' import AppFab from './AppFab' @@ -66,9 +67,21 @@ const ViewContainer = observer(function (props: Props) { return (
{views.length > 0 ? ( - views.map(view => ( - - )) + views.map(view => + view.floating ? ( + + ) : ( + + ), + ) ) : ( )} @@ -79,15 +92,30 @@ const ViewContainer = observer(function (props: Props) { ) }) -const App = observer(function (props: Props) { - const { session } = props +const AppContainer = observer(function (props: Props) { + const { classes } = useStyles() + return ( +
+ + + + +
+ ) +}) + +const DrawerWrapper = observer(function ( + props: Props & { children: React.ReactNode }, +) { + const { children } = props const { classes } = useStyles() + const { session } = props const { minimized, visibleWidget, drawerWidth, drawerPosition } = session const drawerVisible = visibleWidget && !minimized const d = drawerVisible ? `[drawer] ${drawerWidth}px` : undefined + const grid = drawerPosition === 'right' ? ['[main] 1fr', d] : [d, '[main] 1fr'] - return (
) : null} - -
- - - - -
- + + {children} {drawerVisible && drawerPosition === 'right' ? ( ) : null} +
+ ) +}) +const App = observer(function (props: Props) { + const { session } = props + return ( + + + + -
+ ) }) diff --git a/packages/app-core/src/ui/App/FloatingViewPanel.tsx b/packages/app-core/src/ui/App/FloatingViewPanel.tsx new file mode 100644 index 0000000000..33470e1788 --- /dev/null +++ b/packages/app-core/src/ui/App/FloatingViewPanel.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import { ResizableBox } from 'react-resizable' +import { observer } from 'mobx-react' +import { AbstractViewModel, SessionWithDrawerWidgets } from '@jbrowse/core/util' +import { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel' +import { MenuItem as JBMenuItem } from '@jbrowse/core/ui/Menu' +import DraggableDialog from '@jbrowse/core/ui/DraggableDialog' + +// locals +import StaticViewPanel from './StaticViewPanel' +import './test.css' + +type AppSession = SessionWithDrawerWidgets & { + savedSessionNames: string[] + menus: { label: string; menuItems: JBMenuItem[] }[] + snackbarMessages: SnackbarMessage[] + renameCurrentSession: (arg: string) => void + popSnackbarMessage: () => unknown +} + +const FloatingViewPanel = observer(function ({ + view, + session, +}: { + view: AbstractViewModel + session: AppSession +}) { + const zIndex = session.focusedViewId === view.id ? 101 : 100 + return ( + + + + + + ) +}) + +export default FloatingViewPanel diff --git a/packages/app-core/src/ui/App/ViewPanel.tsx b/packages/app-core/src/ui/App/StaticViewPanel.tsx similarity index 93% rename from packages/app-core/src/ui/App/ViewPanel.tsx rename to packages/app-core/src/ui/App/StaticViewPanel.tsx index 57bbcddbee..7532c51255 100644 --- a/packages/app-core/src/ui/App/ViewPanel.tsx +++ b/packages/app-core/src/ui/App/StaticViewPanel.tsx @@ -26,7 +26,7 @@ type AppSession = SessionWithDrawerWidgets & { popSnackbarMessage: () => unknown } -const ViewPanel = observer(function ({ +const StaticViewPanel = observer(function StaticViewPanel2({ view, session, }: { @@ -57,11 +57,9 @@ const ViewPanel = observer(function ({ - ) : ( - false - )} + ) : null} ) }) -export default ViewPanel +export default StaticViewPanel diff --git a/packages/app-core/src/ui/App/StaticViewWrapper.tsx b/packages/app-core/src/ui/App/StaticViewWrapper.tsx new file mode 100644 index 0000000000..12838cf8de --- /dev/null +++ b/packages/app-core/src/ui/App/StaticViewWrapper.tsx @@ -0,0 +1,19 @@ +import React, { useEffect, useRef } from 'react' +import { observer } from 'mobx-react' + +const StaticViewWrapper = observer(function ({ + children, +}: { + children: React.ReactNode +}) { + const scrollRef = useRef(null) + + // scroll the view into view when first mounted. note: this effect will run + // only once, because of the empty array second param + useEffect(() => { + scrollRef.current?.scrollIntoView({ block: 'center' }) + }, []) + return
{children}
+}) + +export default StaticViewWrapper diff --git a/packages/app-core/src/ui/App/ViewContainer.tsx b/packages/app-core/src/ui/App/ViewContainer.tsx index 5e1d5387e7..6bd591aaec 100644 --- a/packages/app-core/src/ui/App/ViewContainer.tsx +++ b/packages/app-core/src/ui/App/ViewContainer.tsx @@ -8,6 +8,7 @@ import { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util' // locals import ViewHeader from './ViewHeader' +import StaticViewWrapper from './StaticViewWrapper' const useStyles = makeStyles()(theme => ({ viewContainer: { @@ -23,7 +24,7 @@ const useStyles = makeStyles()(theme => ({ }, })) -const ViewContainer = observer(function ({ +const ViewContainer = observer(function ViewContainer2({ view, onClose, onMinimize, @@ -38,6 +39,7 @@ const ViewContainer = observer(function ({ const ref = useWidthSetter(view, theme.spacing(1)) const { classes, cx } = useStyles() const session = getSession(view) as SessionWithFocusedViewAndDrawerWidgets + const { focusedViewId } = session useEffect(() => { function handleSelectView(e: Event) { @@ -60,12 +62,18 @@ const ViewContainer = observer(function ({ elevation={12} className={cx( classes.viewContainer, - session.focusedViewId === view.id - ? classes.focusedView - : classes.unfocusedView, + focusedViewId === view.id ? classes.focusedView : classes.unfocusedView, )} > - + {view.floating ? ( +
+ +
+ ) : ( + + + + )} {children} ) diff --git a/packages/app-core/src/ui/App/ViewContainerTitle.tsx b/packages/app-core/src/ui/App/ViewContainerTitle.tsx index 8404a27457..41701d65de 100644 --- a/packages/app-core/src/ui/App/ViewContainerTitle.tsx +++ b/packages/app-core/src/ui/App/ViewContainerTitle.tsx @@ -25,6 +25,7 @@ const useStyles = makeStyles()(theme => ({ backgroundColor: theme.palette.secondary.light, }, })) + const ViewContainerTitle = observer(function ({ view, }: { diff --git a/packages/app-core/src/ui/App/ViewHeader.tsx b/packages/app-core/src/ui/App/ViewHeader.tsx index 7e3526bdef..8270d1210a 100644 --- a/packages/app-core/src/ui/App/ViewHeader.tsx +++ b/packages/app-core/src/ui/App/ViewHeader.tsx @@ -1,19 +1,16 @@ import React, { useEffect, useRef } from 'react' -import { IconButton } from '@mui/material' import { makeStyles } from 'tss-react/mui' import { observer } from 'mobx-react' import { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes/models' +import { getSession } from '@jbrowse/core/util' // icons -import CloseIcon from '@mui/icons-material/Close' -import MinimizeIcon from '@mui/icons-material/Minimize' -import AddIcon from '@mui/icons-material/Add' import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight' // locals import ViewMenu from './ViewMenu' import ViewContainerTitle from './ViewContainerTitle' -import { getSession } from '@jbrowse/core/util' +import ViewHeaderButtons from './ViewHeaderButtons' const useStyles = makeStyles()(theme => ({ icon: { @@ -31,32 +28,6 @@ const useStyles = makeStyles()(theme => ({ }, })) -const ViewButtons = observer(function ({ - view, - onClose, - onMinimize, -}: { - view: IBaseViewModel - onClose: () => void - onMinimize: () => void -}) { - const { classes } = useStyles() - return ( - <> - - {view.minimized ? ( - - ) : ( - - )} - - - - - - ) -}) - const ViewHeader = observer(function ({ view, onClose, @@ -66,7 +37,7 @@ const ViewHeader = observer(function ({ onClose: () => void onMinimize: () => void }) { - const { classes } = useStyles() + const { classes, cx } = useStyles() const scrollRef = useRef(null) const session = getSession(view) @@ -78,7 +49,7 @@ const ViewHeader = observer(function ({ } }, []) return ( -
+
@@ -88,7 +59,11 @@ const ViewHeader = observer(function ({
- +
) }) diff --git a/packages/app-core/src/ui/App/ViewHeaderButtons.tsx b/packages/app-core/src/ui/App/ViewHeaderButtons.tsx new file mode 100644 index 0000000000..062d561461 --- /dev/null +++ b/packages/app-core/src/ui/App/ViewHeaderButtons.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import { IconButton } from '@mui/material' +import { makeStyles } from 'tss-react/mui' +import { observer } from 'mobx-react' +import { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes/models' + +// icons +import CloseIcon from '@mui/icons-material/Close' +import MinimizeIcon from '@mui/icons-material/Minimize' +import AddIcon from '@mui/icons-material/Add' + +// locals +import OpenInNew from '@mui/icons-material/OpenInNew' + +const useStyles = makeStyles()(theme => ({ + icon: { + color: theme.palette.secondary.contrastText, + }, +})) + +const ViewHeaderButtons = observer(function ({ + view, + onClose, + onMinimize, +}: { + view: IBaseViewModel + onClose: () => void + onMinimize: () => void +}) { + const { classes } = useStyles() + return ( + <> + { + view.setFloating(!view.floating) + }} + > + + + + {view.minimized ? ( + + ) : ( + + )} + + + + + + ) +}) + +export default ViewHeaderButtons diff --git a/packages/app-core/src/ui/App/ViewTitle.tsx b/packages/app-core/src/ui/App/ViewTitle.tsx new file mode 100644 index 0000000000..8bc7ad6eaf --- /dev/null +++ b/packages/app-core/src/ui/App/ViewTitle.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { makeStyles } from 'tss-react/mui' +import { observer } from 'mobx-react' +import { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes/models' + +// icons +import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight' + +// locals +import ViewContainerTitle from './ViewContainerTitle' +import { getSession } from '@jbrowse/core/util' + +const useStyles = makeStyles()(theme => ({ + icon: { + color: theme.palette.secondary.contrastText, + }, + grow: { + flexGrow: 1, + }, + viewHeader: { + display: 'flex', + }, + viewTitle: { + display: 'flex', + alignItems: 'center', + }, +})) + +const ViewTitle = observer(function ({ view }: { view: IBaseViewModel }) { + const { classes } = useStyles() + const session = getSession(view) + return ( +
+ {session.focusedViewId === view.id ? ( + + ) : null} + +
+ ) +}) + +export default ViewTitle diff --git a/packages/app-core/src/ui/App/test.css b/packages/app-core/src/ui/App/test.css new file mode 100644 index 0000000000..e2c80854c3 --- /dev/null +++ b/packages/app-core/src/ui/App/test.css @@ -0,0 +1,65 @@ +.react-resizable { + position: relative; +} +.react-resizable-handle { + position: absolute; + width: 20px; + height: 20px; + background-repeat: no-repeat; + background-origin: content-box; + box-sizing: border-box; + background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+'); + background-position: bottom right; + padding: 0 3px 3px 0; +} +.react-resizable-handle-sw { + bottom: 0; + left: 0; + cursor: sw-resize; + transform: rotate(90deg); +} +.react-resizable-handle-se { + bottom: 0; + right: 0; + cursor: se-resize; +} +.react-resizable-handle-nw { + top: 0; + left: 0; + cursor: nw-resize; + transform: rotate(180deg); +} +.react-resizable-handle-ne { + top: 0; + right: 0; + cursor: ne-resize; + transform: rotate(270deg); +} +.react-resizable-handle-w, +.react-resizable-handle-e { + top: 50%; + margin-top: -10px; + cursor: ew-resize; +} +.react-resizable-handle-w { + left: 0; + transform: rotate(135deg); +} +.react-resizable-handle-e { + right: 0; + transform: rotate(315deg); +} +.react-resizable-handle-n, +.react-resizable-handle-s { + left: 50%; + margin-left: -10px; + cursor: ns-resize; +} +.react-resizable-handle-n { + top: 0; + transform: rotate(225deg); +} +.react-resizable-handle-s { + bottom: 0; + transform: rotate(45deg); +} diff --git a/packages/core/package.json b/packages/core/package.json index ef29e52800..558f8a1d29 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -54,7 +54,9 @@ "load-script": "^2.0.0", "material-ui-popup-state": "^5.0.0", "rbush": "^3.0.1", + "react-draggable": "^4.4.5", "react-error-boundary": "^4.0.3", + "react-resizable": "^3.0.5", "serialize-error": "^8.0.0", "source-map-js": "^1.0.2", "svg-path-generator": "^1.1.0" diff --git a/packages/core/pluggableElementTypes/models/BaseViewModel.ts b/packages/core/pluggableElementTypes/models/BaseViewModel.ts index 13732c0d70..a60fef740f 100644 --- a/packages/core/pluggableElementTypes/models/BaseViewModel.ts +++ b/packages/core/pluggableElementTypes/models/BaseViewModel.ts @@ -26,7 +26,12 @@ const BaseViewModel = types /** * #property */ - minimized: false, + minimized: types.optional(types.boolean, false), + + /** + * #property + */ + floating: types.optional(types.boolean, false), }) .volatile(() => ({ width: 800, @@ -40,6 +45,12 @@ const BaseViewModel = types }, })) .actions(self => ({ + /** + * #action + */ + setFloating(b: boolean) { + self.floating = b + }, /** * #action */ @@ -49,11 +60,11 @@ const BaseViewModel = types /** * #action - * width is an important attribute of the view model, when it becomes set, it - * often indicates when the app can start drawing to it. certain views like - * lgv are strict about this because if it tries to draw before it knows the - * width it should draw to, it may start fetching data for regions it doesn't - * need to + * width is an important attribute of the view model, when it becomes set, + * it often indicates when the app can start drawing to it. certain views + * like lgv are strict about this because if it tries to draw before it + * knows the width it should draw to, it may start fetching data for + * regions it doesn't need to * * setWidth is updated by a ResizeObserver generally, the views often need * to know how wide they are to properly draw genomic regions diff --git a/packages/core/ui/DraggableDialog.tsx b/packages/core/ui/DraggableDialog.tsx new file mode 100644 index 0000000000..a24ae3b6ff --- /dev/null +++ b/packages/core/ui/DraggableDialog.tsx @@ -0,0 +1,42 @@ +import React, { useRef } from 'react' +import { Portal } from '@mui/material' +import { observer } from 'mobx-react' + +import { + useClientPoint, + useFloating, + useInteractions, +} from '@floating-ui/react' +import Draggable from 'react-draggable' + +const DraggableDialog = observer(function DraggableDialog({ + children, + zIndex = 100, +}: { + zIndex?: number + children: React.ReactNode +}) { + const ref = useRef(null) + const { refs, floatingStyles, context } = useFloating({ + placement: 'bottom-start', + }) + const clientPoint = useClientPoint(context, { x: 100, y: 100 }) + const { getFloatingProps } = useInteractions([clientPoint]) + return ( + + +
+
+ {children} +
+
+
+
+ ) +}) + +export default DraggableDialog diff --git a/packages/core/util/types/index.ts b/packages/core/util/types/index.ts index 11b1471a9d..c4db874ccc 100644 --- a/packages/core/util/types/index.ts +++ b/packages/core/util/types/index.ts @@ -31,6 +31,7 @@ export * from './util' export interface AbstractViewContainer extends IStateTreeNode> { views: AbstractViewModel[] + floating?: boolean removeView(view: AbstractViewModel): void addView( typeName: string, @@ -279,11 +280,13 @@ export interface AbstractViewModel { id: string type: string width: number + floating: boolean minimized: boolean setWidth(width: number): void setMinimized(flag: boolean): void displayName: string | undefined setDisplayName: (arg: string) => void + setFloating: (arg: boolean) => void menuItems: () => MenuItem[] } export function isViewModel(thing: unknown): thing is AbstractViewModel { diff --git a/yarn.lock b/yarn.lock index 51dbb2ea47..62f61caa52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5099,6 +5099,13 @@ dependencies: "@types/react" "*" +"@types/react-resizable@^3.0.7": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/react-resizable/-/react-resizable-3.0.8.tgz#b27001b4d262c82cc076272df4b8ef91d9487918" + integrity sha512-Pcvt2eGA7KNXldt1hkhVhAgZ8hK41m0mp89mFgQi7LAAEZiaLgm4fHJ5zbJZ/4m2LVaAyYrrRRv1LHDcrGQanA== + dependencies: + "@types/react" "*" + "@types/react-transition-group@^4.4.10": version "4.4.11" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5" @@ -13758,7 +13765,7 @@ promzard@^1.0.0: dependencies: read "^3.0.1" -prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@15.x, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -13955,7 +13962,7 @@ react-docgen@^7.0.0: loose-envify "^1.1.0" scheduler "^0.23.2" -react-draggable@^4.4.5: +react-draggable@^4.0.3, react-draggable@^4.4.5: version "4.4.6" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.6.tgz#63343ee945770881ca1256a5b6fa5c9f5983fe1e" integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw== @@ -14023,6 +14030,14 @@ react-refresh@^0.14.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== +react-resizable@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.5.tgz#362721f2efbd094976f1780ae13f1ad7739786c1" + integrity sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w== + dependencies: + prop-types "15.x" + react-draggable "^4.0.3" + react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"