Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GUI fixes and improvements #88

Merged
merged 4 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions viser/_gui_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

import imageio.v3 as iio
import numpy as onp
from typing_extensions import LiteralString
from typing_extensions import Literal, LiteralString

from . import _messages
from ._gui_handles import (
Expand All @@ -45,6 +45,8 @@
_GuiInputHandle,
_make_unique_id,
)
from ._icons import base64_from_icon
from ._icons_enum import Icon
from ._message_api import MessageApi, _encode_image_base64, cast_vector

if TYPE_CHECKING:
Expand Down Expand Up @@ -159,7 +161,18 @@ def _handle_gui_updates(

# Trigger callbacks.
for cb in handle_state.update_cb:
cb(GuiEvent(client_id, handle))
from ._viser import ClientHandle, ViserServer

# Get the handle of the client that triggered this event.
api = self._get_api()
if isinstance(api, ClientHandle):
client = api
elif isinstance(api, ViserServer):
client = api.get_clients()[client_id]
else:
assert False

cb(GuiEvent(client_id, client, handle))
if handle_state.sync_cb is not None:
handle_state.sync_cb(client_id, value)

Expand Down Expand Up @@ -262,6 +275,25 @@ def add_gui_button(
disabled: bool = False,
visible: bool = True,
hint: Optional[str] = None,
color: Optional[
Literal[
"dark",
"gray",
"red",
"pink",
"grape",
"violet",
"indigo",
"blue",
"cyan",
"green",
"lime",
"yellow",
"orange",
"teal",
]
] = None,
icon: Optional[Icon] = None,
) -> GuiButtonHandle:
"""Add a button to the GUI. The value of this input is set to `True` every time
it is clicked; to detect clicks, we can manually set it back to `False`."""
Expand All @@ -278,6 +310,8 @@ def add_gui_button(
container_id=self._get_container_id(),
hint=hint,
initial_value=False,
color=color,
icon_base64=None if icon is None else base64_from_icon(icon),
),
disabled=disabled,
visible=visible,
Expand Down
10 changes: 9 additions & 1 deletion viser/_gui_handles.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

if TYPE_CHECKING:
from ._gui_api import GuiApi
from ._viser import ClientHandle


T = TypeVar("T")
Expand Down Expand Up @@ -138,7 +139,13 @@ def value(self, value: Union[T, onp.ndarray]) -> None:
# Pushing callbacks into separate threads helps prevent deadlocks when we
# have a lock in a callback. TODO: revisit other callbacks.
threading.Thread(
target=lambda: cb(GuiEvent(client_id=None, target=self))
target=lambda: cb(
GuiEvent(
client_id=None,
client=None,
target=self,
)
)
).start()

@property
Expand Down Expand Up @@ -223,6 +230,7 @@ class GuiEvent(Generic[TGuiHandle]):
Passed as input to callback functions."""

client_id: Optional[ClientId]
client: Optional[ClientHandle]
target: TGuiHandle


Expand Down
14 changes: 11 additions & 3 deletions viser/_message_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,16 @@ def add_3d_gui_container(
# Avoids circular import.
from ._gui_api import GuiApi, _make_unique_id

# New name to make the type checker happy; ViserServer and ClientHandle inherit
# from both GuiApi and MessageApi. The pattern below is unideal.
gui_api = self
assert isinstance(gui_api, GuiApi)

# Remove the 3D GUI container if it already exists. This will make sure
# contained GUI elements are removed, preventing potential memory leaks.
if name in gui_api._handle_from_node_name:
gui_api._handle_from_node_name[name].remove()

container_id = _make_unique_id()
self._queue(
_messages.Gui3DMessage(
Expand All @@ -576,7 +586,5 @@ def add_3d_gui_container(
container_id=container_id,
)
)
assert isinstance(self, MessageApi)
node_handle = SceneNodeHandle._make(self, name, wxyz, position)
assert isinstance(self, GuiApi)
return Gui3dContainerHandle(node_handle._impl, self, container_id)
return Gui3dContainerHandle(node_handle._impl, gui_api, container_id)
19 changes: 19 additions & 0 deletions viser/_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,25 @@ class GuiAddButtonMessage(_GuiAddInputBase):
# All GUI elements currently need an `initial_value` field.
# This makes our job on the frontend easier.
initial_value: bool
color: Optional[
Literal[
"dark",
"gray",
"red",
"pink",
"grape",
"violet",
"indigo",
"blue",
"cyan",
"green",
"lime",
"yellow",
"orange",
"teal",
]
]
icon_base64: Optional[str]


@dataclasses.dataclass
Expand Down
3 changes: 2 additions & 1 deletion viser/_scene_handles.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,10 @@ class Gui3dContainerHandle(SceneNodeHandle):
default_factory=dict
)

def __enter__(self) -> None:
def __enter__(self) -> Gui3dContainerHandle:
self._container_id_restore = self._gui_api._get_container_id()
self._gui_api._set_container_id(self._container_id)
return self

def __exit__(self, *args) -> None:
del args
Expand Down
16 changes: 2 additions & 14 deletions viser/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { SceneNodeThreeObject, UseSceneTree } from "./SceneTree";
import "./index.css";

import ControlPanel from "./ControlPanel/ControlPanel";
import { UseGui, useGuiState } from "./ControlPanel/GuiState";
import { UseGui, useGuiState, useMantineTheme } from "./ControlPanel/GuiState";
import { searchParamKey } from "./SearchParamsUtils";
import {
WebsocketMessageProducer,
Expand Down Expand Up @@ -105,23 +105,11 @@ function ViewerRoot() {
function ViewerContents() {
const viewer = React.useContext(ViewerContext)!;
const control_layout = viewer.useGui((state) => state.theme.control_layout);
const colors = viewer.useGui((state) => state.theme.colors);
return (
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{
colorScheme: viewer.useGui((state) => state.theme.dark_mode)
? "dark"
: "light",
primaryColor: colors === null ? undefined : "custom",
colors:
colors === null
? undefined
: {
custom: colors,
},
}}
theme={useMantineTheme()}
>
<Titlebar />
<ViserModal />
Expand Down
16 changes: 15 additions & 1 deletion viser/client/src/ControlPanel/Generated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ErrorBoundary } from "react-error-boundary";

/** Root of generated inputs. */
export default function GeneratedGuiContainer({
// We need to take viewer as input in drei's <Html /> elements, where contexts break.
containerId,
viewer,
}: {
Expand Down Expand Up @@ -116,7 +117,7 @@ function GeneratedInput({
<Button
id={conf.id}
fullWidth
color={"0x000"}
color={conf.color ?? undefined}
onClick={() =>
messageSender({
type: "GuiUpdateMessage",
Expand All @@ -127,6 +128,19 @@ function GeneratedInput({
style={{ height: "1.875rem" }}
disabled={disabled}
size="sm"
leftIcon={
conf.icon_base64 === null ? undefined : (
<Image
/*^In Safari, both the icon's height and width need to be set, otherwise the icon is clipped.*/
height={"0.9rem"}
width={"0.9rem"}
sx={(theme) => ({
filter: theme.colorScheme == "dark" ? "invert(1)" : undefined,
})}
src={"data:image/svg+xml;base64," + conf.icon_base64}
/>
)
}
>
{conf.label}
</Button>
Expand Down
19 changes: 19 additions & 0 deletions viser/client/src/ControlPanel/GuiState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as Messages from "../WebsocketMessages";
import React from "react";
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { ViewerContext } from "../App";
import { MantineThemeOverride } from "@mantine/core";

export type GuiConfig =
| Messages.GuiAddButtonMessage
Expand Down Expand Up @@ -137,5 +139,22 @@ export function useGuiState(initialServer: string) {
)[0];
}

export function useMantineTheme(): MantineThemeOverride {
const viewer = React.useContext(ViewerContext)!;
const colors = viewer.useGui((state) => state.theme.colors);
return {
colorScheme: viewer.useGui((state) => state.theme.dark_mode)
? "dark"
: "light",
primaryColor: colors === null ? undefined : "custom",
colors:
colors === null
? undefined
: {
custom: colors,
},
};
}

/** Type corresponding to a zustand-style useGuiState hook. */
export type UseGui = ReturnType<typeof useGuiState>;
27 changes: 12 additions & 15 deletions viser/client/src/SceneTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ function SceneNodeThreeChildren(props: {
{children.map((child_id) => {
return <SceneNodeThreeObject key={child_id} name={child_id} />;
})}
<SceneNodeLabel name={props.name} />
</group>,
props.parent,
);
Expand Down Expand Up @@ -124,17 +125,11 @@ export function SceneNodeThreeObject(props: { name: string }) {
const [obj, setRef] = React.useState<THREE.Object3D | null>(null);

// Create object + children.
const objNode = React.useMemo(
() => makeObject && makeObject(setRef),
[setRef, makeObject],
);
const children = React.useMemo(
() =>
obj === null ? null : (
<SceneNodeThreeChildren name={props.name} parent={obj} />
),
[props.name, obj],
);
const objNode = makeObject && makeObject(setRef);
const children =
obj === null ? null : (
<SceneNodeThreeChildren name={props.name} parent={obj} />
);

// Update attributes on a per-frame basis. Currently does redundant work,
// although this shouldn't be a bottleneck.
Expand Down Expand Up @@ -186,7 +181,9 @@ export function SceneNodeThreeObject(props: { name: string }) {
return nodeAttributes?.visibility ?? false;
}

if (clickable) {
if (objNode === undefined) {
return <>{children}</>;
} else if (clickable) {
return (
<>
<group
Expand Down Expand Up @@ -222,15 +219,15 @@ export function SceneNodeThreeObject(props: { name: string }) {
>
<Select enabled={hovered}>{objNode}</Select>
</group>
<SceneNodeLabel name={props.name} />
{children}
</>
);
} else {
return (
<>
{objNode}
<SceneNodeLabel name={props.name} />
{/* This <group /> does nothing, but switching between clickable vs not
causes strange transform behavior without it. */}
<group>{objNode}</group>
{children}
</>
);
Expand Down
Loading
Loading