Skip to content

Commit

Permalink
Implement basic sidebar layout (#60)
Browse files Browse the repository at this point in the history
* Update names

* Icon changes, add DataControls for info, copy URL, and set data URL

* Add Zebrahub logo

* Remove commented code. Tweak appearance.

* Match toolbar sizes

* Fix SDS deprecation warning

* Partially functioning popover for Zarr URL input

* Add snackbar when copying URL

* Minor padding tweaks

* Use frames instead of percent for track highlight length

* Remove arbitrary camera position and target in PointCanvas constructor (see #62)

* Update data URL input to use cancel/apply buttons instead of loading immediately
  • Loading branch information
aganders3 authored Apr 5, 2024
1 parent 603334c commit c21ffb7
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 115 deletions.
2 changes: 1 addition & 1 deletion __tests__/main.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test } from "vitest";

import { PointCanvas } from "../src/lib/PointCanvas.ts";
import Scene from "../src/components/scene";
import Scene from "../src/components/Scene";
import React from "react";
import { render } from "@testing-library/react";

Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/components/main.tsx"></script>
<script type="module" src="/src/components/Main.tsx"></script>
</body>
</html>
Binary file added public/zebrahub-favicon-60x60.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 89 additions & 31 deletions src/components/app.tsx → src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useState, useEffect } from "react";
import "@/css/app.css";

import { Box } from "@mui/material";
import { Box, Divider, Drawer } from "@mui/material";

import Scene from "@/components/scene.tsx";
import DataControls from "@/components/dataControls.tsx";
import PlaybackControls from "@/components/playbackControls.tsx";
import Scene from "@/components/Scene";
import DataControls from "@/components/DataControls";
import TrackControls from "@/components/TrackControls";
import PlaybackControls from "@/components/PlaybackControls";

import useSelectionBox from "@/hooks/useSelectionBox";

Expand All @@ -18,6 +19,8 @@ const initialViewerState = ViewerState.fromUrlHash(window.location.hash);
console.log("initial viewer state: %s", JSON.stringify(initialViewerState));
clearUrlHash();

const drawerWidth = 256;

export default function App() {
// Use references here for two things:
// * manage objects that should never change, even when the component re-renders
Expand Down Expand Up @@ -190,33 +193,88 @@ export default function App() {
}, [numTimes, curTime, playing]);

return (
<Box sx={{ display: "flex", flexDirection: "column", width: "80%", height: "100%" }}>
<DataControls
dataUrl={dataUrl}
initialDataUrl={initialViewerState.dataUrl}
trackManager={trackManager}
trackHighlightLength={trackHighlightLength}
setDataUrl={setDataUrl}
setTrackHighlightLength={setTrackHighlightLength}
copyShareableUrlToClipboard={copyShareableUrlToClipboard}
clearTracks={() => canvas?.removeAllTracks()}
/>
<Scene
setCanvas={setCanvas}
loading={loading}
initialCameraPosition={initialViewerState.cameraPosition}
initialCameraTarget={initialViewerState.cameraTarget}
/>
<PlaybackControls
enabled={true}
autoRotate={autoRotate}
playing={playing}
curTime={curTime}
numTimes={numTimes}
setAutoRotate={setAutoRotate}
setPlaying={setPlaying}
setCurTime={setCurTime}
/>
<Box sx={{ display: "flex", width: "100%", height: "100%" }}>
{/* TODO: components *could* go deeper still for organization */}
<Drawer
anchor="left"
variant="permanent"
sx={{
"width": drawerWidth,
"flexShrink": 0,
"& .MuiDrawer-paper": { width: drawerWidth, boxSizing: "border-box" },
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
height: "100%",
}}
>
<Box
sx={{
flexGrow: 0,
padding: "1em 1.5em",
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
<img src="/zebrahub-favicon-60x60.png" alt="logo" />
<Divider orientation="vertical" flexItem />
<h2>ZEBRAHUB</h2>
</Box>
<Box flexGrow={1} padding="2em">
<TrackControls
trackManager={trackManager}
trackHighlightLength={trackHighlightLength}
setTrackHighlightLength={setTrackHighlightLength}
clearTracks={() => canvas?.removeAllTracks()}
/>
</Box>
<Divider />
<Box flexGrow={0} padding="1em">
<DataControls
dataUrl={dataUrl}
initialDataUrl={initialViewerState.dataUrl}
setDataUrl={setDataUrl}
copyShareableUrlToClipboard={copyShareableUrlToClipboard}
validTrackManager={trackManager !== null}
/>
</Box>
</Box>
</Drawer>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "100%",
overflow: "hidden",
}}
>
<Scene
setCanvas={setCanvas}
loading={loading}
initialCameraPosition={initialViewerState.cameraPosition}
initialCameraTarget={initialViewerState.cameraTarget}
/>
<Box flexGrow={0} padding="1em">
<PlaybackControls
enabled={true}
autoRotate={autoRotate}
playing={playing}
curTime={curTime}
numTimes={numTimes}
setAutoRotate={setAutoRotate}
setPlaying={setPlaying}
setCurTime={setCurTime}
/>
</Box>
</Box>
</Box>
);
}
154 changes: 154 additions & 0 deletions src/components/DataControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { useEffect, useState } from "react";

import { Alert, Box, Popover, Snackbar, Stack, Typography, styled } from "@mui/material";

import { Button, ButtonIcon, InputText, fontBodyXs } from "@czi-sds/components";

interface DataControlsProps {
dataUrl: string;
initialDataUrl: string;
setDataUrl: (dataUrl: string) => void;
copyShareableUrlToClipboard: () => void;
validTrackManager: boolean;
}

export default function DataControls(props: DataControlsProps) {
const [copyUrlSnackBarOpen, setCopyUrlSnackBarOpen] = useState(false);
const [urlPopoverAnchor, setUrlPopoverAnchor] = useState<HTMLButtonElement | null>(null);

const copyShareableUrlToClipBoard = () => {
props.copyShareableUrlToClipboard();
setCopyUrlSnackBarOpen(true);
};

const handleShareableUrlSnackBarClose = () => {
setCopyUrlSnackBarOpen(false);
};

const showUrlPopover = (event: React.MouseEvent<HTMLButtonElement>) => {
setUrlPopoverAnchor(event.currentTarget);
};

const handleUrlPopoverClose = () => {
setUrlPopoverAnchor(null);
};

const handleDataUrlSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const urlInput = document.getElementById("data-url-input") as HTMLInputElement;
if (urlInput) {
props.setDataUrl(urlInput.value);
}
};

// only close the popover if the URL gives a valid track manager
useEffect(() => {
if (props.validTrackManager && urlPopoverAnchor) {
setUrlPopoverAnchor(null);
}
}, [props.validTrackManager]);

return (
<Box
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
// 5px margin makes this bar match the PlaybackControls height
// because that component uses primary buttons, which hava a 5px margin
margin: "5px",
}}
>
{/* TODO: make this do something */}
<ButtonIcon
sdsIcon="infoCircle"
sdsSize="large"
sdsType="secondary"
onClick={() => {
window.alert("Not implemented :)");
}}
/>

<ButtonIcon
sdsIcon="share"
sdsSize="large"
sdsType="secondary"
disabled={!props.validTrackManager}
onClick={copyShareableUrlToClipBoard}
/>
<Snackbar
open={copyUrlSnackBarOpen}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
autoHideDuration={2500}
onClose={handleShareableUrlSnackBarClose}
// This is a hack to make the snackbar appear above the bottom bar
sx={{
"&.MuiSnackbar-root": { bottom: "100px" },
}}
>
<Alert
// SDS alert does not work in here
severity="success"
variant="filled"
>
Shareable URL copied to clipboard!
</Alert>
</Snackbar>

<ButtonIcon sdsIcon="globeBasic" sdsSize="large" sdsType="secondary" onClick={showUrlPopover} />
<Popover
open={Boolean(urlPopoverAnchor)}
anchorEl={urlPopoverAnchor}
onClose={handleUrlPopoverClose}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
transformOrigin={{
vertical: "bottom",
horizontal: "left",
}}
disableRestoreFocus // this is needed to autofocus the input when opening
>
<form onSubmit={handleDataUrlSubmit}>
<Stack
spacing={4}
sx={{
padding: "1em",
width: "50vw",
}}
>
<label htmlFor="data-url-input">
<h5 style={{ margin: 0 }}>Zarr URL</h5>
</label>
<InputText
id="data-url-input"
autoFocus
label="Zarr URL"
hideLabel
placeholder={props.initialDataUrl}
defaultValue={props.dataUrl}
fullWidth={true}
intent={props.validTrackManager ? "default" : "error"}
/>
<Note>
<strong>Note:</strong> Changing this URL will replace the image and reset the canvas.
</Note>
<Stack direction="row" spacing={4}>
<Button sdsStyle="square" sdsType="secondary" onClick={handleUrlPopoverClose}>
Cancel
</Button>
<Button sdsStyle="square" sdsType="primary" type="submit">
Apply
</Button>
</Stack>
</Stack>
</form>
</Popover>
</Box>
);
}

const Note = styled(Typography)`
${fontBodyXs}
`;
2 changes: 1 addition & 1 deletion src/components/main.tsx → src/components/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles";
import { defaultTheme } from "@czi-sds/components";

import App from "@/components/app.tsx";
import App from "@/components/App.tsx";
import "@/css/index.css";

const domNode = document.getElementById("app")!;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Stack } from "@mui/material";

import { InputSlider, InputToggle } from "@czi-sds/components";
import { Box } from "@mui/material";
import { ButtonIcon, InputSlider } from "@czi-sds/components";

interface PlaybackControlsProps {
enabled: boolean;
Expand All @@ -15,22 +14,14 @@ interface PlaybackControlsProps {

export default function PlaybackControls(props: PlaybackControlsProps) {
return (
<Stack direction="row" spacing={8} sx={{ margin: "2em" }}>
<label htmlFor="auto-rotate-toggle">Auto Rotate</label>
<InputToggle
checked={props.autoRotate}
disabled={!props.enabled}
onChange={(e) => {
props.setAutoRotate((e.target as HTMLInputElement).checked);
}}
/>
<label htmlFor="playback-toggle">Playback</label>
<InputToggle
checked={props.playing}
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "center", gap: "2em" }}>
<ButtonIcon
sdsIcon="play"
sdsSize="large"
sdsType="primary"
on={props.playing}
disabled={!props.enabled}
onChange={(e) => {
props.setPlaying((e.target as HTMLInputElement).checked);
}}
onClick={() => props.setPlaying(!props.playing)}
/>
<InputSlider
id="time-frame-slider"
Expand All @@ -42,6 +33,15 @@ export default function PlaybackControls(props: PlaybackControlsProps) {
onChange={(_, value) => props.setCurTime(value as number)}
value={props.curTime}
/>
</Stack>
{/* TODO: add control button groups - perhaps a separate component */}
<ButtonIcon
sdsIcon="dna"
sdsSize="large"
sdsType="primary"
on={props.autoRotate}
disabled={!props.enabled}
onClick={() => props.setAutoRotate(!props.autoRotate)}
/>
</Box>
);
}
File renamed without changes.
Loading

0 comments on commit c21ffb7

Please sign in to comment.