Skip to content

Commit

Permalink
Add assignable .content property to markdown handles (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
brentyi authored Oct 3, 2023
1 parent a9c0a09 commit 56a8d0f
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 67 deletions.
36 changes: 22 additions & 14 deletions examples/14_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,32 @@
server = viser.ViserServer()
server.world_axes.visible = True

markdown_counter = server.add_gui_markdown("Counter: 0")

@server.on_client_connect
def _(client: viser.ClientHandle) -> None:
here = Path(__file__).absolute().parent
markdown_source = (here / "./assets/mdx_example.mdx").read_text()
markdown = client.add_gui_markdown(markdown=markdown_source, image_root=here)
here = Path(__file__).absolute().parent

button = client.add_gui_button("Remove Markdown")
checkbox = client.add_gui_checkbox("Visibility", initial_value=True)
button = server.add_gui_button("Remove blurb")
checkbox = server.add_gui_checkbox("Visibility", initial_value=True)

@button.on_click
def _(_):
markdown.remove()
markdown_source = (here / "./assets/mdx_example.mdx").read_text()
markdown_blurb = server.add_gui_markdown(
content=markdown_source,
image_root=here,
)

@checkbox.on_update
def _(_):
markdown.visible = checkbox.value

@button.on_click
def _(_):
markdown_blurb.remove()


@checkbox.on_update
def _(_):
markdown_blurb.visible = checkbox.value


counter = 0
while True:
time.sleep(10.0)
markdown_counter.content = f"Counter: {counter}"
counter += 1
time.sleep(0.1)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ warn_unused_configs = true
exclude="viser/client/.nodeenv"

[tool.pyright]
exclude = ["./docs/**/*", "./examples/assets/**/*", "./viser/client/.nodeenv", "./build"]
exclude = ["./docs/**/*", "./examples/assets/**/*", "./src/viser/client/.nodeenv", "./build"]

[tool.black]
exclude = "viser/client/.nodeenv"
Expand Down
62 changes: 11 additions & 51 deletions src/viser/_gui_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@

import abc
import dataclasses
import re
import threading
import time
import urllib.parse
import warnings
from pathlib import Path
from typing import (
Expand All @@ -24,7 +22,6 @@
overload,
)

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

Expand All @@ -47,7 +44,7 @@
)
from ._icons import base64_from_icon
from ._icons_enum import Icon
from ._message_api import MessageApi, _encode_image_base64, cast_vector
from ._message_api import MessageApi, cast_vector

if TYPE_CHECKING:
from .infra import ClientId
Expand Down Expand Up @@ -87,38 +84,6 @@ def _compute_precision_digits(x: float) -> int:
return digits


def _get_data_url(url: str, image_root: Optional[Path]) -> str:
if not url.startswith("http") and not image_root:
warnings.warn(
"No `image_root` provided. All relative paths will be scoped to viser's installation path.",
stacklevel=2,
)
if url.startswith("http"):
return url
if image_root is None:
image_root = Path(__file__).parent
try:
image = iio.imread(image_root / url)
data_uri = _encode_image_base64(image, "png")
url = urllib.parse.quote(f"{data_uri[1]}")
return f"data:{data_uri[0]};base64,{url}"
except (IOError, FileNotFoundError):
warnings.warn(
f"Failed to read image {url}, with image_root set to {image_root}.",
stacklevel=2,
)
return url


def _parse_markdown(markdown: str, image_root: Optional[Path]) -> str:
markdown = re.sub(
r"\!\[([^]]*)\]\(([^]]*)\)",
lambda match: f"![{match.group(1)}]({_get_data_url(match.group(2), image_root)})",
markdown,
)
return markdown


@dataclasses.dataclass
class _RootGuiContainer:
_children: Dict[str, SupportsRemoveProtocol]
Expand Down Expand Up @@ -277,30 +242,25 @@ def add_gui_tab_group(

def add_gui_markdown(
self,
markdown: str,
content: str,
image_root: Optional[Path] = None,
order: Optional[float] = None,
) -> GuiMarkdownHandle:
"""Add markdown to the GUI."""
markdown = _parse_markdown(markdown, image_root)
markdown_id = _make_unique_id()
order = _apply_default_order(order)
self._get_api()._queue(
_messages.GuiAddMarkdownMessage(
order=order,
id=markdown_id,
markdown=markdown,
container_id=self._get_container_id(),
)
)
return GuiMarkdownHandle(
handle = GuiMarkdownHandle(
_gui_api=self,
_id=markdown_id,
_id=_make_unique_id(),
_visible=True,
_container_id=self._get_container_id(),
_order=order,
_order=_apply_default_order(order),
_image_root=image_root,
_content=None,
)

# Assigning content will send a GuiAddMarkdownMessage.
handle.content = content
return handle

def add_gui_button(
self,
label: str,
Expand Down
59 changes: 59 additions & 0 deletions src/viser/_gui_handles.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from __future__ import annotations

import dataclasses
import re
import threading
import time
import urllib.parse
import uuid
import warnings
from pathlib import Path
from typing import (
TYPE_CHECKING,
Callable,
Expand All @@ -18,13 +22,16 @@
Union,
)

import imageio.v3 as iio
import numpy as onp
from typing_extensions import Protocol

from ._icons import base64_from_icon
from ._icons_enum import Icon
from ._message_api import _encode_image_base64
from ._messages import (
GuiAddDropdownMessage,
GuiAddMarkdownMessage,
GuiAddTabGroupMessage,
GuiCloseModalMessage,
GuiRemoveMessage,
Expand Down Expand Up @@ -493,6 +500,38 @@ def remove(self) -> None:
child.remove()


def _get_data_url(url: str, image_root: Optional[Path]) -> str:
if not url.startswith("http") and not image_root:
warnings.warn(
"No `image_root` provided. All relative paths will be scoped to viser's installation path.",
stacklevel=2,
)
if url.startswith("http"):
return url
if image_root is None:
image_root = Path(__file__).parent
try:
image = iio.imread(image_root / url)
data_uri = _encode_image_base64(image, "png")
url = urllib.parse.quote(f"{data_uri[1]}")
return f"data:{data_uri[0]};base64,{url}"
except (IOError, FileNotFoundError):
warnings.warn(
f"Failed to read image {url}, with image_root set to {image_root}.",
stacklevel=2,
)
return url


def _parse_markdown(markdown: str, image_root: Optional[Path]) -> str:
markdown = re.sub(
r"\!\[([^]]*)\]\(([^]]*)\)",
lambda match: f"![{match.group(1)}]({_get_data_url(match.group(2), image_root)})",
markdown,
)
return markdown


@dataclasses.dataclass
class GuiMarkdownHandle:
"""Use to remove markdown."""
Expand All @@ -502,6 +541,26 @@ class GuiMarkdownHandle:
_visible: bool
_container_id: str # Parent.
_order: float
_image_root: Optional[Path]
_content: Optional[str]

@property
def content(self) -> str:
"""Current content of this markdown element. Synchronized automatically when assigned."""
assert self._content is not None
return self._content

@content.setter
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,
)
)

@property
def order(self) -> float:
Expand Down
2 changes: 1 addition & 1 deletion src/viser/client/src/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export default function Markdown(props: { children?: string }) {
} catch {
setChild(<Title order={2}>Error Parsing Markdown...</Title>);
}
}, []);
}, [props.children]);

return child;
}

0 comments on commit 56a8d0f

Please sign in to comment.