Skip to content

Commit 2db9ee6

Browse files
jonahbedouchorigamiman72brentyi
authored
Implement custom titlebars that can be configured in Python (nerfstudio-project#42)
* Introduce base titlebar component without functionality Co-authored-by: Abhik Ahuja <[email protected]> * Implement button, image, and padding insertion without backend Co-authored by: Abhik Ahuja <[email protected]> * Implement Python Interface for toggling Navbar and adding Buttons, Images, or Padding Co-authored by: Abhik Ahuja <[email protected]> * Introduce base titlebar component without functionality Co-authored-by: Abhik Ahuja <[email protected]> * Implement button, image, and padding insertion without backend Co-authored by: Abhik Ahuja <[email protected]> * Implement Python Interface for toggling Navbar and adding Buttons, Images, or Padding Co-authored by: Abhik Ahuja <[email protected]> * Resolve Build Warnings in Python Co-authored-by: Abhik Ahuja <[email protected]> * Resolve Formatting Issues Co-authored-by: Abhik Ahuja <[email protected]> * Introduce base titlebar component without functionality Co-authored-by: Abhik Ahuja <[email protected]> * Implement button, image, and padding insertion without backend Co-authored by: Abhik Ahuja <[email protected]> * Implement Python Interface for toggling Navbar and adding Buttons, Images, or Padding Co-authored by: Abhik Ahuja <[email protected]> * Introduce base titlebar component without functionality Co-authored-by: Abhik Ahuja <[email protected]> * Implement button, image, and padding insertion without backend Co-authored by: Abhik Ahuja <[email protected]> * Implement Python Interface for toggling Navbar and adding Buttons, Images, or Padding Co-authored by: Abhik Ahuja <[email protected]> * Resolve Build Warnings in Python Co-authored-by: Abhik Ahuja <[email protected]> * Resolve Formatting Issues Co-authored-by: Abhik Ahuja <[email protected]> * Add Support for TypedDicts to the Typescript Type Syncer Co-authored-by: Abhik Ahuja <[email protected]> * Update Titlebar Interface to rely on a fixed layout consisting of buttons and an optional focus image Co-authored-by: Abhik Ahuja <[email protected]> * Add docstrings to titlebar config objects Co-authored-by: Abhik Ahuja <[email protected]> * Remove old Titlebar interface from _message_api.py Co-authored-by: Abhik Ahuja <[email protected]> * Delete viser/.vscode directory * Move theming to separate example * Tweak types * Run ruff --------- Co-authored-by: Abhik Ahuja <[email protected]> Co-authored-by: Brent Yi <[email protected]>
1 parent 1608a2f commit 2db9ee6

18 files changed

+264
-16
lines changed

examples/13_theming.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Theming
2+
3+
Viser is adding support for theming. Work-in-progress.
4+
"""
5+
6+
import time
7+
8+
9+
import viser
10+
from viser.theme import TitlebarButton, TitlebarConfig, TitlebarImage
11+
12+
server = viser.ViserServer()
13+
14+
buttons = (
15+
TitlebarButton(
16+
text="Getting Started",
17+
icon=None,
18+
href="https://nerf.studio",
19+
variant="outlined",
20+
),
21+
TitlebarButton(
22+
text="Github",
23+
icon="GitHub",
24+
href="https://github.com/nerfstudio-project/nerfstudio",
25+
variant="outlined",
26+
),
27+
TitlebarButton(
28+
text="Documentation",
29+
icon="Description",
30+
href="https://docs.nerf.studio",
31+
variant="outlined",
32+
),
33+
TitlebarButton(
34+
text="Viewport Controls",
35+
icon="Keyboard",
36+
href="https://docs.nerf.studio",
37+
variant="outlined",
38+
),
39+
)
40+
image = TitlebarImage(
41+
image_url="https://docs.nerf.studio/en/latest/_static/imgs/logo.png",
42+
image_alt="NerfStudio Logo",
43+
href="https://docs.nerf.studio/",
44+
)
45+
46+
titlebar_theme = TitlebarConfig(buttons=buttons, image=image)
47+
48+
server.configure_theme(
49+
canvas_background_color=(2, 230, 230),
50+
titlebar_content=titlebar_theme,
51+
)
52+
server.world_axes.visible = True
53+
54+
while True:
55+
time.sleep(10.0)

examples/assets/download_colmap_garden.sh

100644100755
File mode changed.

viser/_message_api.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
import trimesh.visual
3636
from typing_extensions import Literal, LiteralString, ParamSpec, TypeAlias, assert_never
3737

38-
from . import _messages, infra
38+
from . import _messages, infra, theme
3939
from ._gui import (
4040
GuiButtonGroupHandle,
4141
GuiButtonHandle,
@@ -164,11 +164,13 @@ def configure_theme(
164164
self,
165165
*,
166166
canvas_background_color: RgbTupleOrArray = (255, 255, 255),
167+
titlebar_content: Optional[theme.TitlebarConfig] = None,
167168
) -> None:
168169
"""Configure the viser front-end's visual appearance."""
169170
self._queue(
170171
_messages.ThemeConfigurationMessage(
171-
canvas_background_color=_encode_rgb(canvas_background_color)
172+
canvas_background_color=_encode_rgb(canvas_background_color),
173+
titlebar_content=titlebar_content,
172174
),
173175
)
174176

viser/_messages.py

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing_extensions import Literal, override
1212

1313
from . import infra
14+
from . import theme
1415

1516

1617
class Message(infra.Message):
@@ -326,3 +327,4 @@ class ThemeConfigurationMessage(Message):
326327
"""Message from server->client to configure parts of the GUI."""
327328

328329
canvas_background_color: int
330+
titlebar_content: Optional[theme.TitlebarConfig]
+3-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"files": {
33
"main.css": "/static/css/main.82973013.css",
4-
"main.js": "/static/js/main.78d359b6.js",
4+
"main.js": "/static/js/main.54bd4a4f.js",
55
"index.html": "/index.html",
66
"main.82973013.css.map": "/static/css/main.82973013.css.map",
7-
"main.78d359b6.js.map": "/static/js/main.78d359b6.js.map"
7+
"main.54bd4a4f.js.map": "/static/js/main.54bd4a4f.js.map"
88
},
99
"entrypoints": [
1010
"static/css/main.82973013.css",
11-
"static/js/main.78d359b6.js"
11+
"static/js/main.54bd4a4f.js"
1212
]
1313
}

viser/client/build/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.svg"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Viser</title><script defer="defer" src="/static/js/main.78d359b6.js"></script><link href="/static/css/main.82973013.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
1+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.svg"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Viser</title><script defer="defer" src="/static/js/main.54bd4a4f.js"></script><link href="/static/css/main.82973013.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

viser/client/build/static/js/main.54bd4a4f.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

viser/client/build/static/js/main.54bd4a4f.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

viser/client/build/static/js/main.78d359b6.js

-3
This file was deleted.

viser/client/build/static/js/main.78d359b6.js.map

-1
This file was deleted.

viser/client/src/ControlPanel/GuiState.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const cleanGuiState: GuiState = {
3939
theme: {
4040
type: "ThemeConfigurationMessage",
4141
canvas_background_color: 0xffffff,
42+
titlebar_content: null,
4243
},
4344
label: "",
4445
server: "ws://localhost:8080", // Currently this will always be overridden.

viser/client/src/Titlebar.tsx

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { Button, Grid, IconButton, SvgIcon } from "@mui/material";
2+
import { useContext } from "react";
3+
import { ViewerContext } from ".";
4+
import { Message } from "./WebsocketMessages";
5+
6+
import * as Icons from "@mui/icons-material";
7+
8+
// Type helpers.
9+
type IconName = keyof typeof Icons;
10+
type ArrayElement<ArrayType extends readonly unknown[]> =
11+
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
12+
type NoNull<T> = Exclude<T, null>;
13+
type TitlebarContent = NoNull<
14+
(Message & { type: "ThemeConfigurationMessage" })["titlebar_content"]
15+
>;
16+
17+
// We inherit props directly from message contents.
18+
export function TitlebarButton(
19+
props: ArrayElement<NoNull<TitlebarContent["buttons"]>>
20+
) {
21+
if (props.icon !== null && props.text === null) {
22+
return (
23+
<IconButton href={props.href ?? ""}>
24+
<SvgIcon component={Icons[props.icon as IconName] ?? null} />
25+
</IconButton>
26+
);
27+
}
28+
return (
29+
<Button
30+
variant={props.variant ?? "contained"}
31+
href={props.href ?? ""}
32+
sx={{
33+
marginY: "0.4em",
34+
marginX: "0.125em",
35+
alignItems: "center",
36+
}}
37+
size="small"
38+
target="_blank"
39+
startIcon={
40+
props.icon ? (
41+
<SvgIcon component={Icons[props.icon as IconName] ?? null} />
42+
) : null
43+
}
44+
>
45+
{props.text ?? ""}
46+
</Button>
47+
);
48+
}
49+
50+
export function TitlebarImage(props: NoNull<TitlebarContent["image"]>) {
51+
const image = (
52+
<img
53+
src={props.image_url}
54+
alt={props.image_alt}
55+
style={{
56+
height: "2em",
57+
marginLeft: "0.125em",
58+
marginRight: "0.125em",
59+
}}
60+
/>
61+
);
62+
if (props.href == null) {
63+
return image;
64+
}
65+
return <a href={props.href}>{image}</a>;
66+
}
67+
68+
export function Titlebar() {
69+
const viewer = useContext(ViewerContext)!;
70+
const content = viewer.useGui((state) => state.theme.titlebar_content);
71+
72+
if (content == null) {
73+
return null;
74+
}
75+
76+
const buttons = content.buttons;
77+
const imageData = content.image;
78+
79+
return (
80+
<Grid
81+
container
82+
sx={{
83+
width: "100%",
84+
zIndex: "1000",
85+
backgroundColor: "rgba(255, 255, 255, 0.85)",
86+
borderBottom: "1px solid",
87+
borderBottomColor: "divider",
88+
direction: "row",
89+
justifyContent: "space-between",
90+
alignItems: "center",
91+
paddingX: "0.875em",
92+
height: "2.5em",
93+
}}
94+
>
95+
<Grid
96+
item
97+
xs="auto"
98+
component="div"
99+
sx={{
100+
display: "flex",
101+
direction: "row",
102+
alignItems: "center",
103+
justifyContent: "left",
104+
overflow: "visible",
105+
}}
106+
>
107+
{buttons?.map((btn) => TitlebarButton(btn))}
108+
</Grid>
109+
<Grid
110+
item
111+
xs={3}
112+
component="div"
113+
sx={{
114+
display: "flex",
115+
direction: "row",
116+
alignItems: "center",
117+
justifyContent: "right",
118+
}}
119+
>
120+
{imageData !== null ? TitlebarImage(imageData) : null}
121+
</Grid>
122+
</Grid>
123+
);
124+
}

viser/client/src/WebsocketMessages.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,17 @@ interface MessageGroupEnd {
172172
interface ThemeConfigurationMessage {
173173
type: "ThemeConfigurationMessage";
174174
canvas_background_color: number;
175+
titlebar_content: {
176+
buttons:
177+
| {
178+
text: string | null;
179+
icon: "GitHub" | "Description" | "Keyboard" | null;
180+
href: string | null;
181+
variant: "text" | "contained" | "outlined" | null;
182+
}[]
183+
| null;
184+
image: { image_url: string; image_alt: string; href: string | null } | null;
185+
} | null;
175186
}
176187

177188
export type Message =

viser/client/src/index.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import WebsocketInterface from "./WebsocketInterface";
2727
import { UseGui, useGuiState } from "./ControlPanel/GuiState";
2828
import { searchParamKey } from "./SearchParamsUtils";
2929

30+
import { Titlebar } from "./Titlebar";
31+
3032
type ViewerContextContents = {
3133
useSceneTree: UseSceneTree;
3234
useGui: UseGui;
@@ -47,8 +49,9 @@ function SingleViewer() {
4749
// Layout and styles.
4850
const Wrapper = styled(Box)`
4951
width: 100%;
50-
height: 100%;
52+
height: 1px;
5153
position: relative;
54+
flex: 1 0 auto;
5255
`;
5356

5457
// Default server logic.
@@ -80,6 +83,7 @@ function SingleViewer() {
8083
};
8184
return (
8285
<ViewerContext.Provider value={viewer}>
86+
<Titlebar></Titlebar>
8387
<Wrapper ref={viewer.wrapperRef}>
8488
<WebsocketInterface />
8589
<ControlPanel />
@@ -157,6 +161,8 @@ function Root() {
157161
height: "100%",
158162
boxSizing: "border-box",
159163
position: "relative",
164+
display: "flex",
165+
flexDirection: "column",
160166
}}
161167
>
162168
<SingleViewer />

viser/infra/_typescript_interface_gen.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Any, ClassVar, Type, Union, get_type_hints
33

44
import numpy as onp
5-
from typing_extensions import Literal, get_args, get_origin
5+
from typing_extensions import Literal, get_args, get_origin, is_typeddict
66

77
from ._messages import Message
88

@@ -32,12 +32,27 @@ def _get_ts_type(typ: Type) -> str:
3232
get_args(typ),
3333
)
3434
)
35+
if is_typeddict(typ):
36+
hints = get_type_hints(typ)
37+
38+
def fmt(key):
39+
val = hints[key]
40+
ret = f"'{key}'" + ": " + _get_ts_type(val)
41+
return ret
42+
43+
ret = "{" + ", ".join(map(fmt, hints)) + "}"
44+
# ret = "{" + f"type: \'{typ.__name__}\', " + ", ".join(map(fmt, hints)) + "}"
45+
return ret
3546
if get_origin(typ) is Union:
36-
return " | ".join(
37-
map(
38-
_get_ts_type,
39-
get_args(typ),
47+
return (
48+
"("
49+
+ " | ".join(
50+
map(
51+
_get_ts_type,
52+
get_args(typ),
53+
)
4054
)
55+
+ ")"
4156
)
4257

4358
if hasattr(typ, "__origin__"):

viser/theme/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
""":mod:`viser.theme` provides interfaces for themeing the viser
2+
frontend from within Python.
3+
"""
4+
5+
from ._titlebar import TitlebarButton as TitlebarButton
6+
from ._titlebar import TitlebarConfig as TitlebarConfig
7+
from ._titlebar import TitlebarImage as TitlebarImage

viser/theme/_titlebar.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import Tuple, Literal, Optional, TypedDict
2+
3+
4+
class TitlebarButton(TypedDict):
5+
"""A link-only button that appears in the Titlebar."""
6+
7+
text: Optional[str]
8+
icon: Optional[Literal["GitHub", "Description", "Keyboard"]]
9+
href: Optional[str]
10+
variant: Optional[Literal["text", "contained", "outlined"]]
11+
12+
13+
class TitlebarImage(TypedDict):
14+
"""An image that appears on the titlebar."""
15+
16+
image_url: str
17+
image_alt: str
18+
href: Optional[str]
19+
20+
21+
class TitlebarConfig(TypedDict):
22+
"""Configure the content that appears in the titlebar."""
23+
24+
buttons: Optional[Tuple[TitlebarButton, ...]]
25+
image: Optional[TitlebarImage]

0 commit comments

Comments
 (0)