Skip to content

Commit

Permalink
Control panel layout configuration (#70)
Browse files Browse the repository at this point in the history
* Implement Collapse button in sidebar

* Rework sidebar API to allow toggling between collapsible and fixed sidebar

* Update Example 11 to use new API

* Nits, fixes

* prettier

---------

Co-authored-by: Brent Yi <[email protected]>
  • Loading branch information
jonahbedouch and brentyi authored Aug 4, 2023
1 parent 1840578 commit 0e6519e
Show file tree
Hide file tree
Showing 23 changed files with 238 additions and 148 deletions.
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none"
}
6 changes: 6 additions & 0 deletions examples/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none"
}
2 changes: 1 addition & 1 deletion examples/11_colmap_visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def main(
downsample_factor: Downsample factor for the images.
"""
server = viser.ViserServer()
server.configure_theme(titlebar_content=None, fixed_sidebar=True)
server.configure_theme(titlebar_content=None, control_layout="collapsible")

# Load the colmap info.
cameras = read_cameras_binary(colmap_path / "cameras.bin")
Expand Down
5 changes: 1 addition & 4 deletions examples/13_theming.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@

titlebar_theme = TitlebarConfig(buttons=buttons, image=image)

server.configure_theme(
titlebar_content=titlebar_theme,
fixed_sidebar=True,
)
server.configure_theme(titlebar_content=titlebar_theme, control_layout="fixed")
server.world_axes.visible = True

while True:
Expand Down
4 changes: 2 additions & 2 deletions viser/_message_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,14 @@ def configure_theme(
self,
*,
titlebar_content: Optional[theme.TitlebarConfig] = None,
fixed_sidebar: bool = False,
control_layout: Literal["floating", "collapsible", "fixed"] = "floating",
dark_mode: bool = False,
) -> None:
"""Configure the viser front-end's visual appearance."""
self._queue(
_messages.ThemeConfigurationMessage(
titlebar_content=titlebar_content,
fixed_sidebar=fixed_sidebar,
control_layout=control_layout,
dark_mode=dark_mode,
),
)
Expand Down
2 changes: 1 addition & 1 deletion viser/_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,5 +424,5 @@ class ThemeConfigurationMessage(Message):
"""Message from server->client to configure parts of the GUI."""

titlebar_content: Optional[theme.TitlebarConfig]
fixed_sidebar: bool
control_layout: Literal["floating", "collapsible", "fixed"]
dark_mode: bool
14 changes: 7 additions & 7 deletions viser/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ type ViewerContextContents = {
}>;
};
export const ViewerContext = React.createContext<null | ViewerContextContents>(
null
null,
);

THREE.ColorManagement.enabled = true;
Expand All @@ -65,7 +65,7 @@ function SingleViewer() {
return server;
}
const servers = new URLSearchParams(window.location.search).getAll(
searchParamKey
searchParamKey,
);
const initialServer =
servers.length >= 1 ? servers[0] : getDefaultServerFromUrl();
Expand All @@ -87,10 +87,10 @@ function SingleViewer() {
// viewer context changes.
const memoizedWebsocketInterface = React.useMemo(
() => <WebsocketInterface />,
[]
[],
);

const fixed_sidebar = viewer.useGui((state) => state.theme.fixed_sidebar);
const control_layout = viewer.useGui((state) => state.theme.control_layout);
return (
<MantineProvider
withGlobalStyles
Expand All @@ -117,7 +117,7 @@ function SingleViewer() {
top: 0,
bottom: 0,
left: 0,
right: fixed_sidebar ? "20em" : 0,
right: control_layout === "fixed" ? "20em" : 0,
position: "absolute",
backgroundColor:
theme.colorScheme === "light" ? "#fff" : theme.colors.dark[9],
Expand All @@ -126,7 +126,7 @@ function SingleViewer() {
<ViewerCanvas>{memoizedWebsocketInterface}</ViewerCanvas>
</Box>
</MediaQuery>
<ControlPanel fixed_sidebar={fixed_sidebar} />
<ControlPanel control_layout={control_layout} />
</Box>
</ViewerContext.Provider>
</MantineProvider>
Expand Down Expand Up @@ -177,7 +177,7 @@ function SceneContextSetter() {
const { sceneRef, cameraRef } = React.useContext(ViewerContext)!;
sceneRef.current = useThree((state) => state.scene);
cameraRef.current = useThree(
(state) => state.camera as THREE.PerspectiveCamera
(state) => state.camera as THREE.PerspectiveCamera,
);
return <></>;
}
Expand Down
10 changes: 5 additions & 5 deletions viser/client/src/CameraControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function SynchronizedCameraControls() {

const sendCameraThrottled = makeThrottledMessageSender(
viewer.websocketRef,
20
20,
);

// Callback for sending cameras.
Expand Down Expand Up @@ -122,28 +122,28 @@ export function SynchronizedCameraControls() {
cameraControls.rotate(
-0.1 * THREE.MathUtils.DEG2RAD * event?.deltaTime,
0,
true
true,
);
});
rightKey.addEventListener("holding", (event) => {
cameraControls.rotate(
0.1 * THREE.MathUtils.DEG2RAD * event?.deltaTime,
0,
true
true,
);
});
upKey.addEventListener("holding", (event) => {
cameraControls.rotate(
0,
-0.05 * THREE.MathUtils.DEG2RAD * event?.deltaTime,
true
true,
);
});
downKey.addEventListener("holding", (event) => {
cameraControls.rotate(
0,
0.05 * THREE.MathUtils.DEG2RAD * event?.deltaTime,
true
true,
);
});

Expand Down
149 changes: 112 additions & 37 deletions viser/client/src/ControlPanel/ControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ import {
IconCloudCheck,
IconCloudOff,
IconArrowBack,
IconChevronLeft,
IconChevronRight,
} from "@tabler/icons-react";
import React from "react";
import BottomPanel from "./BottomPanel";
import FloatingPanel, { FloatingPanelContext } from "./FloatingPanel";
import { ThemeConfigurationMessage } from "../WebsocketMessages";

// Must match constant in Python.
const ROOT_CONTAINER_ID = "root";
Expand All @@ -29,7 +32,9 @@ function HideWhenCollapsed({ children }: { children: React.ReactNode }) {
return expanded ? children : null;
}

export default function ControlPanel(props: { fixed_sidebar: boolean }) {
export default function ControlPanel(props: {
control_layout: ThemeConfigurationMessage["control_layout"];
}) {
const theme = useMantineTheme();
const useMobileView = useMediaQuery(`(max-width: ${theme.breakpoints.xs})`);

Expand All @@ -38,6 +43,7 @@ export default function ControlPanel(props: { fixed_sidebar: boolean }) {
const showGenerated =
Object.keys(viewer.useGui((state) => state.guiConfigFromId)).length > 0;
const [showSettings, { toggle }] = useDisclosure(false);
const [collapsed, { toggle: toggleCollapse }] = useDisclosure(false);
const handleContents = (
<>
<ConnectionStatus />
Expand All @@ -47,7 +53,7 @@ export default function ControlPanel(props: { fixed_sidebar: boolean }) {
<Box
sx={{
position: "absolute",
right: "0.5em",
right: props.control_layout === "collapsible" ? "2.5em" : "0.5em",
top: "50%",
transform: "translateY(-50%)",
display: showGenerated ? undefined : "none",
Expand All @@ -70,9 +76,56 @@ export default function ControlPanel(props: { fixed_sidebar: boolean }) {
</ActionIcon>
</Box>
</HideWhenCollapsed>
<Box
sx={{
position: "absolute",
right: "0.5em",
top: "50%",
transform: "translateY(-50%)",
display:
props.control_layout === "collapsible" && !useMobileView
? undefined
: "none",
zIndex: 100,
}}
>
<ActionIcon
onClick={(evt) => {
evt.stopPropagation();
toggleCollapse();
}}
>
<Tooltip label={"Collapse Sidebar"}>{<IconChevronRight />}</Tooltip>
</ActionIcon>
</Box>
</>
);

const collapsedView = (
<div
style={{
borderTopLeftRadius: "15%",
borderBottomLeftRadius: "15%",
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
backgroundColor:
theme.colorScheme == "dark"
? theme.colors.dark[5]
: theme.colors.gray[2],
padding: "0.5em",
}}
>
<ActionIcon
onClick={(evt) => {
evt.stopPropagation();
toggleCollapse();
}}
>
<Tooltip label={"Show Sidebar"}>{<IconChevronLeft />}</Tooltip>
</ActionIcon>
</div>
);

const panelContents = (
<>
<Collapse in={!showGenerated || showSettings} p="xs">
Expand All @@ -91,41 +144,61 @@ export default function ControlPanel(props: { fixed_sidebar: boolean }) {
<BottomPanel.Contents>{panelContents}</BottomPanel.Contents>
</BottomPanel>
);
} else if (props.fixed_sidebar) {
} else if (props.control_layout !== "floating") {
return (
<Aside
hiddenBreakpoint={"xs"}
sx={(theme) => ({
width: "20em",
boxSizing: "border-box",
right: 0,
position: "absolute",
top: "0em",
bottom: "0em",
borderLeft: "1px solid",
borderColor:
theme.colorScheme == "light"
? theme.colors.gray[4]
: theme.colors.dark[4],
})}
>
<>
<Box
p="sm"
sx={{
position: "absolute",
right: collapsed ? "0em" : "-2.5em",
top: "0.5em",
transitionProperty: "right",
transitionDuration: "0.5s",
transitionDelay: "0.25s",
}}
>
{collapsedView}
</Box>
<Aside
hiddenBreakpoint={"xs"}
fixed
sx={(theme) => ({
backgroundColor:
theme.colorScheme == "dark"
? theme.colors.dark[5]
: theme.colors.gray[1],
lineHeight: "1.5em",
fontWeight: 400,
position: "relative",
zIndex: 1,
width: collapsed ? 0 : "20em",
bottom: 0,
overflow: "scroll",
boxSizing: "border-box",
borderLeft: "1px solid",
borderColor:
theme.colorScheme == "light"
? theme.colors.gray[4]
: theme.colors.dark[4],
transition: "width 0.5s 0s",
})}
>
{handleContents}
</Box>
{panelContents}
</Aside>
<Box
sx={() => ({
width: "20em",
})}
>
<Box
p="sm"
sx={(theme) => ({
backgroundColor:
theme.colorScheme == "dark"
? theme.colors.dark[5]
: theme.colors.gray[1],
lineHeight: "1.5em",
fontWeight: 400,
position: "relative",
zIndex: 1,
})}
>
{handleContents}
</Box>
{panelContents}
</Box>
</Aside>
</>
);
} else {
return (
Expand All @@ -146,17 +219,19 @@ function ConnectionStatus() {

const StatusIcon = connected ? IconCloudCheck : IconCloudOff;
return (
<>
<span
style={{ display: "flex", alignItems: "center", width: "max-content" }}
>
<StatusIcon
color={connected ? "#0b0" : "#b00"}
style={{
transform: "translateY(0.1em) scale(1.2)",
width: "1em",
height: "1em",
transform: "translateY(-0.05em)",
width: "1.2em",
height: "1.2em",
}}
/>
&nbsp; &nbsp;
{label === "" ? server : label}
</>
</span>
);
}
Loading

0 comments on commit 0e6519e

Please sign in to comment.