From b8b477ba05358f1f7170d1d52550798445f78672 Mon Sep 17 00:00:00 2001 From: Jonas Kulhanek Date: Tue, 30 Jan 2024 17:34:22 +0100 Subject: [PATCH] Update api --- examples/02_gui.py | 16 ++-- src/viser/_gui_api.py | 39 +++++++--- src/viser/_gui_components.py | 75 +++++++++++++------ .../client/src/ControlPanel/Generated.tsx | 19 +---- 4 files changed, 90 insertions(+), 59 deletions(-) diff --git a/examples/02_gui.py b/examples/02_gui.py index 7825fcae5..efc51ef3f 100644 --- a/examples/02_gui.py +++ b/examples/02_gui.py @@ -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, ) @@ -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. diff --git a/src/viser/_gui_api.py b/src/viser/_gui_api.py index 7037d7ad0..b21d94a2a 100644 --- a/src/viser/_gui_api.py +++ b/src/viser/_gui_api.py @@ -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): @@ -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") @@ -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 diff --git a/src/viser/_gui_components.py b/src/viser/_gui_components.py index 8526e0af2..dd7e9c003 100644 --- a/src/viser/_gui_components.py +++ b/src/viser/_gui_components.py @@ -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: @@ -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. @@ -34,6 +42,7 @@ def wrapper(fn: Callable[..., TReturn]) -> Callable[Concatenate[Any, TArgs], TRe The component handle. """ return out + return wrapper @@ -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[ @@ -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 @@ -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 @@ -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 @@ -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) @@ -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) diff --git a/src/viser/client/src/ControlPanel/Generated.tsx b/src/viser/client/src/ControlPanel/Generated.tsx index 83868db12..1eb87f00b 100644 --- a/src/viser/client/src/ControlPanel/Generated.tsx +++ b/src/viser/client/src/ControlPanel/Generated.tsx @@ -52,18 +52,11 @@ function GeneratedComponentFromId({ id }: { id: str export default function GeneratedGuiContainer({ // We need to take viewer as input in drei's elements, where contexts break. containerId, - viewer, folderDepth, }: { containerId: string; - viewer?: ViewerContextContents; folderDepth?: number; }) { - if (viewer !== undefined) { - return - - - } const viewer = React.useContext(ViewerContext)!; const guiIdSet = @@ -82,18 +75,13 @@ export default function GeneratedGuiContainer({ {guiIdOrderPairArray .sort((a, b) => a.order - b.order) - .map((pair, index) => { - const props = - return ( - - ); - })} + )} ); return out; @@ -118,9 +106,10 @@ function GeneratedInput({ // Handle nested containers. if (conf.type == "GuiAddFolderMessage") return ( + <> ); if (conf.type == "GuiAddTabGroupMessage") - return ; + return <>; const messageSender = makeThrottledMessageSender(viewer.websocketRef, 50); function updateValue(value: any) {