Skip to content

Commit

Permalink
Update api
Browse files Browse the repository at this point in the history
  • Loading branch information
jkulhanek committed Jan 30, 2024
1 parent 7719f72 commit b8b477b
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 59 deletions.
16 changes: 8 additions & 8 deletions examples/02_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def main():
with server.add_gui_folder("Read-only"):
gui_counter = server.add_gui_number(
"Counter",
initial_value=0,
value=0,
disabled=True,
)

Expand All @@ -25,38 +25,38 @@ def main():
min=0,
max=100,
step=1,
initial_value=0,
value=0,
disabled=True,
)

with server.add_gui_folder("Editable"):
gui_vector2 = server.add_gui_vector2(
"Position",
initial_value=(0.0, 0.0),
value=(0.0, 0.0),
step=0.1,
)
gui_vector3 = server.add_gui_vector3(
"Size",
initial_value=(1.0, 1.0, 1.0),
value=(1.0, 1.0, 1.0),
step=0.25,
)
with server.add_gui_folder("Text toggle"):
gui_checkbox_hide = server.add_gui_checkbox(
"Hide",
initial_value=False,
value=False,
)
gui_text = server.add_gui_text(
"Text",
initial_value="Hello world",
value="Hello world",
)
gui_button = server.add_gui_button("Button")
gui_checkbox_disable = server.add_gui_checkbox(
"Disable",
initial_value=False,
value=False,
)
gui_rgb = server.add_gui_rgb(
"Color",
initial_value=(255, 255, 0),
value=(255, 255, 0),
)

# Pre-generate a point cloud to send.
Expand Down
39 changes: 27 additions & 12 deletions src/viser/_gui_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,41 @@ def _apply_default_order(order: Optional[float]) -> float:
class ComponentHandle(Generic[TProps]):
_id: str
_props: TProps
_api_update: Callable[[str, dict], None]
_gui_api: 'GuiApi'
_update_timestamp: float
_container_id: str
_backup_container_id: Optional[str] = None

def __init__(self, update: Callable[[str, Dict[str, Any]]], id: str, props: TProps):
def __init__(self, gui_api: 'GuiApi', id: str, props: TProps):
self._id = id
self._props = props
self._api_update = update
self._gui_api = gui_api
self._update_timestamp = time.time()
self._container_id = gui_api._get_container_id()
props._order = _apply_default_order(props._order)
self._register()

def _register(self):
self._gui_api._get_api()._queue(_messages.GuiAddComponentMessage(
order=self.order,
id=self.id,
props=self._props,
container_id=self._container_id,
))

@property
def id(self):
return self._id

def __enter__(self):
self._backup_container_id = self._gui_api._get_container_id()
self._gui_api._set_container_id(self.id)
return self

def __exit__(self, exc_type, exc_value, traceback):
self._gui_api._set_container_id(self._backup_container_id)
return None

def _update(self, **kwargs):
for k, v in kwargs.items():
if not hasattr(self._props, k):
Expand All @@ -157,7 +179,7 @@ def _update(self, **kwargs):
self._update_timestamp = time.time()

# Raise message to update component.
self._api_update(self.id, kwargs)
self._gui_api._get_api()._queue(_messages.GuiUpdateComponentMessage(id=id, **kwargs))

def property(self, name: str) -> Property[T]:
props = object.__getattribute__(self, "_props")
Expand Down Expand Up @@ -269,13 +291,6 @@ def _update_component_props(self, id: str, kwargs: Dict[str, Any]) -> None:
self._get_api()._queue(_messages.GuiUpdateMessage(id=id, **kwargs))

def gui_add_component(self, props: TProps) -> TProps:
props.order = _apply_default_order(props.order)
handle = ComponentHandle(self._update_component_props, id=_make_unique_id(), props=props)
self._get_api()._queue(_messages.GuiAddComponentMessage(
order=handle.order,
id=handle.id,
props=props,
container_id=self._get_container_id()
))
handle = ComponentHandle(self, id=_make_unique_id(), props=props)
self._gui_handle_from_id[handle.id] = handle
return handle
75 changes: 51 additions & 24 deletions src/viser/_gui_components.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from dataclasses import field, InitVar
import typing
from dataclasses import field, InitVar, KW_ONLY
from functools import wraps
import time
from typing import Optional, Literal, Union, TypeVar, Generic, Tuple, Type
from typing import Callable, Any
from dataclasses import dataclass

try:
from typing import Concatenate
except ImportError:
from typing_extensions import Concatenate
try:
from typing import Self
except ImportError:
from typing_extensions import Self
try:
from typing import Protocol
except ImportError:
Expand All @@ -19,13 +25,15 @@


TProps = TypeVar("TProps")
TReturn = TypeVar('TReturn')
TArgs = ParamSpec('TArgs')
TReturn = TypeVar("TReturn")
TArgs = ParamSpec("TArgs")
T = TypeVar("T")


def copy_signature(fn_signature: Callable[TArgs, Any]):
def wrapper(fn: Callable[..., TReturn]) -> Callable[Concatenate[Any, TArgs], TReturn]:
def wrapper(
fn: Callable[..., TReturn]
) -> Callable[Concatenate[Any, TArgs], TReturn]:
out = wraps(fn_signature)(fn)
# TODO: perhaps copy signature from fn_signature and get help for arguments
out.__doc__ = f"""Creates a new GUI {fn_signature.__name__} component and returns a handle to it.
Expand All @@ -34,6 +42,7 @@ def wrapper(fn: Callable[..., TReturn]) -> Callable[Concatenate[Any, TArgs], TRe
The component handle.
"""
return out

return wrapper


Expand Down Expand Up @@ -68,10 +77,18 @@ def property(self, name: str) -> Property[T]:
raise NotImplementedError()


@dataclass(kw_only=True)
class GuiContainer(Protocol):
def __enter__(self) -> Self:
raise NotImplementedError()

def __exit__(self, exc_type, exc_value, traceback):
return None


@dataclass
class Button(GuiComponent, Protocol):
"""Button component
"""
"""Button component"""

label: str
"""Button label"""
color: Optional[
Expand Down Expand Up @@ -101,33 +118,38 @@ class Button(GuiComponent, Protocol):
"""Button tooltip."""


@dataclass(kw_only=True)
@dataclass
class Input(GuiComponent, Protocol):
value: str
label: str
hint: Optional[str]
_: KW_ONLY
hint: Optional[str] = None
disabled: bool = False


@dataclass(kw_only=True)
class TextInput(Input, Protocol):
pass
value: str

@dataclass(kw_only=True)
class Folder(GuiComponent, Protocol):

@dataclass
class Folder(GuiComponent, GuiContainer, Protocol):
label: str
_: KW_ONLY
expand_by_default: bool = True


@dataclass(kw_only=True)
class Markdown(GuiComponent, Protocol):
markdown: str


@dataclass(kw_only=True)
class TabGroup(GuiComponent, Protocol):
tab_labels: Tuple[str, ...]
tab_icons_base64: Tuple[Union[str, None], ...]
tab_container_ids: Tuple[str, ...]


@dataclass(kw_only=True)
class Modal(GuiComponent, Protocol):
order: float
Expand All @@ -144,10 +166,10 @@ class Slider(Input, Protocol):
precision: Optional[int] = None


@dataclass(kw_only=True)
@dataclass
class NumberInput(Input, Protocol):
value: float
step: float
step: Optional[float] = None
min: Optional[float] = None
max: Optional[float] = None
precision: Optional[int] = None
Expand All @@ -158,17 +180,17 @@ class RgbInput(Input, Protocol):
value: Tuple[int, int, int]


@dataclass(kw_only=True)
@dataclass
class RgbaInput(Input, Protocol):
value: Tuple[int, int, int, int]


@dataclass(kw_only=True)
@dataclass
class Checkbox(Input, Protocol):
value: bool


@dataclass(kw_only=True)
@dataclass
class Vector2Input(Input, Protocol):
value: Tuple[float, float]
step: float
Expand All @@ -177,20 +199,25 @@ class Vector2Input(Input, Protocol):
precision: Optional[int] = None


@dataclass(kw_only=True)
@dataclass
class Vector3Input(Input, Protocol):
value: Tuple[float, float, float]
min: Optional[Tuple[float, float, float]]
max: Optional[Tuple[float, float, float]]
step: float
precision: int
min: Optional[Tuple[float, float, float]] = None
max: Optional[Tuple[float, float, float]] = None
precision: Optional[int] = None


@dataclass(kw_only=True)
@dataclass
class Dropdown(Input, Protocol):
options: Tuple[str, ...]
value: Optional[str] = None

def __post_init__(self, *args, **kwargs):
if self.value is None and len(self.options) > 0:
self.value = self.options[0]
return super().__post_init__(*args, **kwargs)


class GuiApiMixin:
@copy_signature(Button)
Expand All @@ -199,7 +226,7 @@ def add_gui_button(self, *args, **kwargs) -> Button:
return self.gui_add_component(props)

@copy_signature(TextInput)
def gui_add_text_input(self, *args, **kwargs) -> TextInput:
def add_gui_text(self, *args, **kwargs) -> TextInput:
props = TextInput(*args, **kwargs)
return self.gui_add_component(props)

Expand Down
19 changes: 4 additions & 15 deletions src/viser/client/src/ControlPanel/Generated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,11 @@ function GeneratedComponentFromId<T extends AllComponentProps>({ id }: { id: str
export default function GeneratedGuiContainer({
// We need to take viewer as input in drei's <Html /> elements, where contexts break.
containerId,
viewer,
folderDepth,
}: {
containerId: string;
viewer?: ViewerContextContents;
folderDepth?: number;
}) {
if (viewer !== undefined) {
return <ViewerContext.Provider value={viewer}>
<GeneratedGuiContainer containerId={containerId} folderDepth={folderDepth} />
</ViewerContext.Provider>
}
const viewer = React.useContext(ViewerContext)!;

const guiIdSet =
Expand All @@ -82,18 +75,13 @@ export default function GeneratedGuiContainer({
<Box pt="0.75em">
{guiIdOrderPairArray
.sort((a, b) => a.order - b.order)
.map((pair, index) => {
const props =
return (
<GuiGenerateContext.Provider key={pair.id} value={pair.id
<GeneratedInput
.map((pair, index) => <GeneratedInput
id={pair.id}
viewer={viewer}
folderDepth={folderDepth ?? 0}
last={index === guiIdOrderPairArray.length - 1}
/>
);
})}
)}
</Box>
);
return out;
Expand All @@ -118,9 +106,10 @@ function GeneratedInput({
// Handle nested containers.
if (conf.type == "GuiAddFolderMessage")
return (
<></>
);
if (conf.type == "GuiAddTabGroupMessage")
return <GeneratedTabGroup conf={conf} />;
return <></>;

const messageSender = makeThrottledMessageSender(viewer.websocketRef, 50);
function updateValue(value: any) {
Expand Down

0 comments on commit b8b477b

Please sign in to comment.