Skip to content

Commit

Permalink
Refac js components (#166)
Browse files Browse the repository at this point in the history
* Refac components

* GUI api using partial update messages

* Update message typecasting

* Embrace untyped GuiUpdateMessage?

* ruff

* Address CI errors

* Fix tab groups, spacing tweaks

* Fix performance regression (components were unnecessarily re-rendering)

* ruff

* move multislider source

---------

Co-authored-by: Brent Yi <[email protected]>
  • Loading branch information
jkulhanek and brentyi authored Feb 6, 2024
1 parent 517d354 commit 0d05686
Show file tree
Hide file tree
Showing 29 changed files with 1,504 additions and 1,167 deletions.
226 changes: 129 additions & 97 deletions src/viser/_gui_api.py

Large diffs are not rendered by default.

62 changes: 28 additions & 34 deletions src/viser/_gui_handles.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Generic,
Expand All @@ -29,14 +30,10 @@
from ._icons_enum import IconName
from ._message_api import _encode_image_base64
from ._messages import (
GuiAddDropdownMessage,
GuiAddMarkdownMessage,
GuiAddTabGroupMessage,
GuiCloseModalMessage,
GuiRemoveMessage,
GuiSetDisabledMessage,
GuiSetValueMessage,
GuiSetVisibleMessage,
GuiUpdateMessage,
Message,
)
from .infra import ClientId

Expand Down Expand Up @@ -84,7 +81,7 @@ class _GuiHandleState(Generic[T]):
is_button: bool
"""Indicates a button element, which requires special handling."""

sync_cb: Optional[Callable[[ClientId, T], None]]
sync_cb: Optional[Callable[[ClientId, str, Any], None]]
"""Callback for synchronizing inputs across clients."""

disabled: bool
Expand All @@ -95,6 +92,8 @@ class _GuiHandleState(Generic[T]):
initial_value: T
hint: Optional[str]

message_type: Type[Message]


@dataclasses.dataclass
class _GuiInputHandle(Generic[T]):
Expand Down Expand Up @@ -137,7 +136,7 @@ def value(self, value: T | onp.ndarray) -> None:
# Send to client, except for buttons.
if not self._impl.is_button:
self._impl.gui_api._get_api()._queue(
GuiSetValueMessage(self._impl.id, value) # type: ignore
GuiUpdateMessage(self._impl.id, "value", value)
)

# Set internal state. We automatically convert numpy arrays to the expected
Expand Down Expand Up @@ -176,7 +175,7 @@ def disabled(self, disabled: bool) -> None:
return

self._impl.gui_api._get_api()._queue(
GuiSetDisabledMessage(self._impl.id, disabled=disabled)
GuiUpdateMessage(self._impl.id, "disabled", disabled)
)
self._impl.disabled = disabled

Expand All @@ -192,7 +191,7 @@ def visible(self, visible: bool) -> None:
return

self._impl.gui_api._get_api()._queue(
GuiSetVisibleMessage(self._impl.id, visible=visible)
GuiUpdateMessage(self._impl.id, "visible", visible)
)
self._impl.visible = visible

Expand Down Expand Up @@ -312,15 +311,7 @@ def options(self, options: Iterable[StringType]) -> None:
self._impl.initial_value = self._impl_options[0]

self._impl.gui_api._get_api()._queue(
GuiAddDropdownMessage(
order=self._impl.order,
id=self._impl.id,
label=self._impl.label,
container_id=self._impl.container_id,
hint=self._impl.hint,
initial_value=self._impl.initial_value,
options=self._impl_options,
)
GuiUpdateMessage(self._impl.id, "options", self._impl_options)
)

if self.value not in self._impl_options:
Expand All @@ -334,7 +325,6 @@ class GuiTabGroupHandle:
_icons_base64: List[Optional[str]]
_tabs: List[GuiTabHandle]
_gui_api: GuiApi
_container_id: str # Parent.
_order: float

@property
Expand Down Expand Up @@ -364,15 +354,20 @@ def remove(self) -> None:
self._gui_api._get_api()._queue(GuiRemoveMessage(self._tab_group_id))

def _sync_with_client(self) -> None:
"""Send a message that syncs tab state with the client."""
"""Send messages for syncing tab state with the client."""
self._gui_api._get_api()._queue(
GuiUpdateMessage(self._tab_group_id, "tab_labels", tuple(self._labels))
)
self._gui_api._get_api()._queue(
GuiUpdateMessage(
self._tab_group_id, "tab_icons_base64", tuple(self._icons_base64)
)
)
self._gui_api._get_api()._queue(
GuiAddTabGroupMessage(
order=self.order,
id=self._tab_group_id,
container_id=self._container_id,
tab_labels=tuple(self._labels),
tab_icons_base64=tuple(self._icons_base64),
tab_container_ids=tuple(tab._id for tab in self._tabs),
GuiUpdateMessage(
self._tab_group_id,
"tab_container_ids",
tuple(tab._id for tab in self._tabs),
)
)

Expand Down Expand Up @@ -561,11 +556,10 @@ def content(self) -> str:
def content(self, content: str) -> None:
self._content = content
self._gui_api._get_api()._queue(
GuiAddMarkdownMessage(
order=self._order,
id=self._id,
markdown=_parse_markdown(content, self._image_root),
container_id=self._container_id,
GuiUpdateMessage(
self._id,
"markdown",
_parse_markdown(content, self._image_root),
)
)

Expand All @@ -585,7 +579,7 @@ def visible(self, visible: bool) -> None:
if visible == self.visible:
return

self._gui_api._get_api()._queue(GuiSetVisibleMessage(self._id, visible=visible))
self._gui_api._get_api()._queue(GuiUpdateMessage(self._id, "visible", visible))
self._visible = visible

def __post_init__(self) -> None:
Expand Down
95 changes: 55 additions & 40 deletions src/viser/_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from __future__ import annotations

import dataclasses
from typing import Any, Optional, Tuple, Union
from typing import Any, Callable, ClassVar, Optional, Tuple, Type, TypeVar, Union

import numpy as onp
import numpy.typing as onpt
Expand All @@ -16,6 +16,8 @@


class Message(infra.Message):
_tags: ClassVar[Tuple[str, ...]] = tuple()

@override
def redundancy_key(self) -> str:
"""Returns a unique key for this message, used for detecting redundant
Expand All @@ -39,6 +41,19 @@ def redundancy_key(self) -> str:
return "_".join(parts)


T = TypeVar("T", bound=Type[Message])


def tag_class(tag: str) -> Callable[[T], T]:
"""Decorator for tagging a class with a `type` field."""

def wrapper(cls: T) -> T:
cls._tags = (cls._tags or ()) + (tag,)
return cls

return wrapper


@dataclasses.dataclass
class ViewerCameraMessage(Message):
"""Message for a posed viewer camera.
Expand Down Expand Up @@ -348,23 +363,28 @@ class ResetSceneMessage(Message):
"""Reset scene."""


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddFolderMessage(Message):
order: float
id: str
label: str
container_id: str
expand_by_default: bool
visible: bool


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddMarkdownMessage(Message):
order: float
id: str
markdown: str
container_id: str
visible: bool


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddTabGroupMessage(Message):
order: float
Expand All @@ -373,6 +393,7 @@ class GuiAddTabGroupMessage(Message):
tab_labels: Tuple[str, ...]
tab_icons_base64: Tuple[Union[str, None], ...]
tab_container_ids: Tuple[str, ...]
visible: bool


@dataclasses.dataclass
Expand All @@ -384,7 +405,9 @@ class _GuiAddInputBase(Message):
label: str
container_id: str
hint: Optional[str]
initial_value: Any
value: Any
visible: bool
disabled: bool


@dataclasses.dataclass
Expand All @@ -399,11 +422,12 @@ class GuiCloseModalMessage(Message):
id: str


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddButtonMessage(_GuiAddInputBase):
# All GUI elements currently need an `initial_value` field.
# All GUI elements currently need an `value` field.
# This makes our job on the frontend easier.
initial_value: bool
value: bool
color: Optional[
Literal[
"dark",
Expand All @@ -425,84 +449,94 @@ class GuiAddButtonMessage(_GuiAddInputBase):
icon_base64: Optional[str]


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddSliderMessage(_GuiAddInputBase):
min: float
max: float
step: Optional[float]
initial_value: float
value: float
precision: int
marks: Optional[Tuple[GuiSliderMark, ...]] = None


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddMultiSliderMessage(_GuiAddInputBase):
min: float
max: float
step: Optional[float]
min_range: Optional[float]
initial_value: Tuple[float, ...]
precision: int
fixed_endpoints: bool = False
marks: Optional[Tuple[GuiSliderMark, ...]] = None


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddNumberMessage(_GuiAddInputBase):
initial_value: float
value: float
precision: int
step: float
min: Optional[float]
max: Optional[float]


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddRgbMessage(_GuiAddInputBase):
initial_value: Tuple[int, int, int]
value: Tuple[int, int, int]


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddRgbaMessage(_GuiAddInputBase):
initial_value: Tuple[int, int, int, int]
value: Tuple[int, int, int, int]


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddCheckboxMessage(_GuiAddInputBase):
initial_value: bool
value: bool


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddVector2Message(_GuiAddInputBase):
initial_value: Tuple[float, float]
value: Tuple[float, float]
min: Optional[Tuple[float, float]]
max: Optional[Tuple[float, float]]
step: float
precision: int


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddVector3Message(_GuiAddInputBase):
initial_value: Tuple[float, float, float]
value: Tuple[float, float, float]
min: Optional[Tuple[float, float, float]]
max: Optional[Tuple[float, float, float]]
step: float
precision: int


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddTextMessage(_GuiAddInputBase):
initial_value: str
value: str


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddDropdownMessage(_GuiAddInputBase):
initial_value: str
value: str
options: Tuple[str, ...]


@tag_class("GuiAddComponentMessage")
@dataclasses.dataclass
class GuiAddButtonGroupMessage(_GuiAddInputBase):
initial_value: str
value: str
options: Tuple[str, ...]


Expand All @@ -515,34 +549,15 @@ class GuiRemoveMessage(Message):

@dataclasses.dataclass
class GuiUpdateMessage(Message):
"""Sent client->server when a GUI input is changed."""
"""Sent client<->server when any property of a GUI component is changed."""

id: str
value: Any


@dataclasses.dataclass
class GuiSetVisibleMessage(Message):
"""Sent client->server when a GUI input is changed."""
prop_name: str
prop_value: Any

id: str
visible: bool


@dataclasses.dataclass
class GuiSetDisabledMessage(Message):
"""Sent client->server when a GUI input is changed."""

id: str
disabled: bool


@dataclasses.dataclass
class GuiSetValueMessage(Message):
"""Sent server->client to set the value of a particular input."""

id: str
value: Any
@override
def redundancy_key(self) -> str:
return type(self).__name__ + "-" + self.id + "-" + self.prop_name


@dataclasses.dataclass
Expand Down
Loading

0 comments on commit 0d05686

Please sign in to comment.