From c7b0b0cab8f8c6cbbdf1e284a27369857e3fdd1c Mon Sep 17 00:00:00 2001 From: Jonas Kulhanek Date: Mon, 29 Jan 2024 14:14:04 +0100 Subject: [PATCH] Refac components --- src/viser/_gui_api.py | 157 ++-- src/viser/_gui_handles.py | 7 +- src/viser/_messages.py | 31 +- .../client/src/ControlPanel/Generated.tsx | 765 ++---------------- .../src/ControlPanel/GuiComponentContext.tsx | 18 + .../client/src/ControlPanel/GuiState.tsx | 25 +- src/viser/client/src/WebsocketInterface.tsx | 7 +- src/viser/client/src/WebsocketMessages.tsx | 79 +- src/viser/client/src/components/Button.tsx | 69 ++ .../client/src/components/ButtonGroup.tsx | 40 + src/viser/client/src/components/Checkbox.tsx | 49 ++ src/viser/client/src/components/Dropdown.tsx | 36 + src/viser/client/src/components/Folder.tsx | 77 ++ src/viser/client/src/components/Markdown.tsx | 18 + .../client/src/components/NumberInput.tsx | 36 + src/viser/client/src/components/Rgb.tsx | 28 + src/viser/client/src/components/Rgba.tsx | 27 + src/viser/client/src/components/Slider.tsx | 91 +++ src/viser/client/src/components/TabGroup.tsx | 56 ++ src/viser/client/src/components/TextInput.tsx | 28 + src/viser/client/src/components/Vector2.tsx | 23 + src/viser/client/src/components/Vector3.tsx | 23 + src/viser/client/src/components/common.tsx | 147 ++++ src/viser/client/src/components/utils.tsx | 32 + 24 files changed, 1039 insertions(+), 830 deletions(-) create mode 100644 src/viser/client/src/ControlPanel/GuiComponentContext.tsx create mode 100644 src/viser/client/src/components/Button.tsx create mode 100644 src/viser/client/src/components/ButtonGroup.tsx create mode 100644 src/viser/client/src/components/Checkbox.tsx create mode 100644 src/viser/client/src/components/Dropdown.tsx create mode 100644 src/viser/client/src/components/Folder.tsx create mode 100644 src/viser/client/src/components/Markdown.tsx create mode 100644 src/viser/client/src/components/NumberInput.tsx create mode 100644 src/viser/client/src/components/Rgb.tsx create mode 100644 src/viser/client/src/components/Rgba.tsx create mode 100644 src/viser/client/src/components/Slider.tsx create mode 100644 src/viser/client/src/components/TabGroup.tsx create mode 100644 src/viser/client/src/components/TextInput.tsx create mode 100644 src/viser/client/src/components/Vector2.tsx create mode 100644 src/viser/client/src/components/Vector3.tsx create mode 100644 src/viser/client/src/components/common.tsx create mode 100644 src/viser/client/src/components/utils.tsx diff --git a/src/viser/_gui_api.py b/src/viser/_gui_api.py index 53e0f46b5..c3a7ac024 100644 --- a/src/viser/_gui_api.py +++ b/src/viser/_gui_api.py @@ -196,6 +196,7 @@ def add_gui_folder( label: str, order: Optional[float] = None, expand_by_default: bool = True, + visible: bool = True, ) -> GuiFolderHandle: """Add a folder, and return a handle that can be used to populate it. @@ -204,6 +205,7 @@ def add_gui_folder( order: Optional ordering, smallest values will be displayed first. expand_by_default: Open the folder by default. Set to False to collapse it by default. + visible: Whether the component is visible. Returns: A handle that can be used as a context to populate the folder. @@ -217,6 +219,7 @@ def add_gui_folder( label=label, container_id=self._get_container_id(), expand_by_default=expand_by_default, + visible=visible, ) ) return GuiFolderHandle( @@ -258,11 +261,13 @@ def add_gui_modal( def add_gui_tab_group( self, order: Optional[float] = None, + visible: bool = True, ) -> GuiTabGroupHandle: """Add a tab group. Args: order: Optional ordering, smallest values will be displayed first. + visible: Whether the component is visible. Returns: A handle that can be used as a context to populate the tab group. @@ -277,6 +282,7 @@ def add_gui_tab_group( _gui_api=self, _container_id=self._get_container_id(), _order=order, + _visible=visible, ) def add_gui_markdown( @@ -284,6 +290,7 @@ def add_gui_markdown( content: str, image_root: Optional[Path] = None, order: Optional[float] = None, + visible: bool = True, ) -> GuiMarkdownHandle: """Add markdown to the GUI. @@ -291,6 +298,7 @@ def add_gui_markdown( content: Markdown content to display. image_root: Optional root directory to resolve relative image paths. order: Optional ordering, smallest values will be displayed first. + visible: Whether the component is visible. Returns: A handle that can be used to interact with the GUI element. @@ -298,7 +306,7 @@ def add_gui_markdown( handle = GuiMarkdownHandle( _gui_api=self, _id=_make_unique_id(), - _visible=True, + _visible=visible, _container_id=self._get_container_id(), _order=_apply_default_order(order), _image_root=image_root, @@ -357,19 +365,19 @@ def add_gui_button( order = _apply_default_order(order) return GuiButtonHandle( self._create_gui_input( - initial_value=False, + value=False, message=_messages.GuiAddButtonMessage( order=order, id=id, label=label, container_id=self._get_container_id(), hint=hint, - initial_value=False, + value=False, color=color, icon_base64=None if icon is None else base64_from_icon(icon), + disabled=disabled, + visible=visible, ), - disabled=disabled, - visible=visible, is_button=True, )._impl ) @@ -425,23 +433,23 @@ def add_gui_button_group( Returns: A handle that can be used to interact with the GUI element. """ - initial_value = options[0] + value = options[0] id = _make_unique_id() order = _apply_default_order(order) return GuiButtonGroupHandle( self._create_gui_input( - initial_value, + value, message=_messages.GuiAddButtonGroupMessage( order=order, id=id, label=label, container_id=self._get_container_id(), hint=hint, - initial_value=initial_value, + value=value, options=tuple(options), + disabled=disabled, + visible=visible, ), - disabled=disabled, - visible=visible, )._impl, ) @@ -467,21 +475,22 @@ def add_gui_checkbox( Returns: A handle that can be used to interact with the GUI element. """ - assert isinstance(initial_value, bool) + value = initial_value + assert isinstance(value, bool) id = _make_unique_id() order = _apply_default_order(order) return self._create_gui_input( - initial_value, + value, message=_messages.GuiAddCheckboxMessage( order=order, id=id, label=label, container_id=self._get_container_id(), hint=hint, - initial_value=initial_value, + value=value, + disabled=disabled, + visible=visible, ), - disabled=disabled, - visible=visible, ) def add_gui_text( @@ -506,21 +515,22 @@ def add_gui_text( Returns: A handle that can be used to interact with the GUI element. """ - assert isinstance(initial_value, str) + value = initial_value + assert isinstance(value, str) id = _make_unique_id() order = _apply_default_order(order) return self._create_gui_input( - initial_value, + value, message=_messages.GuiAddTextMessage( order=order, id=id, label=label, container_id=self._get_container_id(), hint=hint, - initial_value=initial_value, + value=value, + disabled=disabled, + visible=visible, ), - disabled=disabled, - visible=visible, ) def add_gui_number( @@ -552,8 +562,9 @@ def add_gui_number( Returns: A handle that can be used to interact with the GUI element. """ + value = initial_value - assert isinstance(initial_value, (int, float)) + assert isinstance(value, (int, float)) if step is None: # It's ok that `step` is always a float, even if the value is an integer, @@ -561,7 +572,7 @@ def add_gui_number( step = float( # type: ignore onp.min( [ - _compute_step(initial_value), + _compute_step(value), _compute_step(min), _compute_step(max), ] @@ -573,21 +584,21 @@ def add_gui_number( id = _make_unique_id() order = _apply_default_order(order) return self._create_gui_input( - initial_value=initial_value, + value, message=_messages.GuiAddNumberMessage( order=order, id=id, label=label, container_id=self._get_container_id(), hint=hint, - initial_value=initial_value, + value=value, min=min, max=max, precision=_compute_precision_digits(step), step=step, + disabled=disabled, + visible=visible, ), - disabled=disabled, - visible=visible, is_button=False, ) @@ -619,7 +630,8 @@ def add_gui_vector2( Returns: A handle that can be used to interact with the GUI element. """ - initial_value = cast_vector(initial_value, 2) + value = initial_value + value = cast_vector(value, 2) min = cast_vector(min, 2) if min is not None else None max = cast_vector(max, 2) if max is not None else None id = _make_unique_id() @@ -627,7 +639,7 @@ def add_gui_vector2( if step is None: possible_steps: List[float] = [] - possible_steps.extend([_compute_step(x) for x in initial_value]) + possible_steps.extend([_compute_step(x) for x in value]) if min is not None: possible_steps.extend([_compute_step(x) for x in min]) if max is not None: @@ -635,21 +647,21 @@ def add_gui_vector2( step = float(onp.min(possible_steps)) return self._create_gui_input( - initial_value, + value, message=_messages.GuiAddVector2Message( order=order, id=id, label=label, container_id=self._get_container_id(), hint=hint, - initial_value=initial_value, + value=value, min=min, max=max, step=step, precision=_compute_precision_digits(step), + disabled=disabled, + visible=visible, ), - disabled=disabled, - visible=visible, ) def add_gui_vector3( @@ -680,7 +692,8 @@ def add_gui_vector3( Returns: A handle that can be used to interact with the GUI element. """ - initial_value = cast_vector(initial_value, 2) + value = initial_value + value = cast_vector(value, 2) min = cast_vector(min, 3) if min is not None else None max = cast_vector(max, 3) if max is not None else None id = _make_unique_id() @@ -688,7 +701,7 @@ def add_gui_vector3( if step is None: possible_steps: List[float] = [] - possible_steps.extend([_compute_step(x) for x in initial_value]) + possible_steps.extend([_compute_step(x) for x in value]) if min is not None: possible_steps.extend([_compute_step(x) for x in min]) if max is not None: @@ -696,21 +709,21 @@ def add_gui_vector3( step = float(onp.min(possible_steps)) return self._create_gui_input( - initial_value, + value, message=_messages.GuiAddVector3Message( order=order, id=id, label=label, container_id=self._get_container_id(), hint=hint, - initial_value=initial_value, + value=value, min=min, max=max, step=step, precision=_compute_precision_digits(step), + disabled=disabled, + visible=visible, ), - disabled=disabled, - visible=visible, ) # See add_gui_dropdown for notes on overloads. @@ -764,24 +777,25 @@ def add_gui_dropdown( Returns: A handle that can be used to interact with the GUI element. """ - if initial_value is None: - initial_value = options[0] + value = initial_value + if value is None: + value = options[0] id = _make_unique_id() order = _apply_default_order(order) return GuiDropdownHandle( self._create_gui_input( - initial_value, + value, message=_messages.GuiAddDropdownMessage( order=order, id=id, label=label, container_id=self._get_container_id(), hint=hint, - initial_value=initial_value, + value=value, options=tuple(options), + disabled=disabled, + visible=visible, ), - disabled=disabled, - visible=visible, )._impl, _impl_options=tuple(options), ) @@ -814,27 +828,28 @@ def add_gui_slider( Returns: A handle that can be used to interact with the GUI element. """ + value = initial_value assert max >= min if step > max - min: step = max - min - assert max >= initial_value >= min + assert max >= value >= min # GUI callbacks cast incoming values to match the type of the initial value. If # the min, max, or step is a float, we should cast to a float. - if type(initial_value) is int and ( + if type(value) is int and ( type(min) is float or type(max) is float or type(step) is float ): - initial_value = float(initial_value) # type: ignore + value = float(value) # type: ignore # TODO: as of 6/5/2023, this assert will break something in nerfstudio. (at # least LERF) # - # assert type(min) == type(max) == type(step) == type(initial_value) + # assert type(min) == type(max) == type(step) == type(value) id = _make_unique_id() order = _apply_default_order(order) return self._create_gui_input( - initial_value=initial_value, + value, message=_messages.GuiAddSliderMessage( order=order, id=id, @@ -844,11 +859,11 @@ def add_gui_slider( min=min, max=max, step=step, - initial_value=initial_value, + value=value, precision=_compute_precision_digits(step), + visible=visible, + disabled=disabled, ), - disabled=disabled, - visible=visible, is_button=False, ) @@ -875,20 +890,21 @@ def add_gui_rgb( A handle that can be used to interact with the GUI element. """ + value = initial_value id = _make_unique_id() order = _apply_default_order(order) return self._create_gui_input( - initial_value, + value, message=_messages.GuiAddRgbMessage( order=order, id=id, label=label, container_id=self._get_container_id(), hint=hint, - initial_value=initial_value, + value=value, + disabled=disabled, + visible=visible, ), - disabled=disabled, - visible=visible, ) def add_gui_rgba( @@ -913,28 +929,27 @@ def add_gui_rgba( Returns: A handle that can be used to interact with the GUI element. """ + value = initial_value id = _make_unique_id() order = _apply_default_order(order) return self._create_gui_input( - initial_value, + value, message=_messages.GuiAddRgbaMessage( order=order, id=id, label=label, container_id=self._get_container_id(), hint=hint, - initial_value=initial_value, + value=value, + disabled=disabled, + visible=visible, ), - disabled=disabled, - visible=visible, ) def _create_gui_input( self, - initial_value: T, + value: T, message: _messages._GuiAddInputBase, - disabled: bool, - visible: bool, is_button: bool = False, ) -> GuiInputHandle[T]: """Private helper for adding a simple GUI element.""" @@ -945,19 +960,19 @@ def _create_gui_input( # Construct handle. handle_state = _GuiHandleState( label=message.label, - typ=type(initial_value), + typ=type(value), gui_api=self, - value=initial_value, + value=value, + initial_value=value, update_timestamp=time.time(), container_id=self._get_container_id(), update_cb=[], is_button=is_button, sync_cb=None, - disabled=False, - visible=True, + disabled=message.disabled, + visible=message.visible, id=message.id, order=message.order, - initial_value=initial_value, hint=message.hint, ) @@ -974,10 +989,4 @@ def sync_other_clients(client_id: ClientId, value: Any) -> None: handle = GuiInputHandle(handle_state) - # Set the disabled/visible fields. These will queue messages under-the-hood. - if disabled: - handle.disabled = disabled - if not visible: - handle.visible = visible - return handle diff --git a/src/viser/_gui_handles.py b/src/viser/_gui_handles.py index f150ae604..aae704412 100644 --- a/src/viser/_gui_handles.py +++ b/src/viser/_gui_handles.py @@ -319,8 +319,10 @@ def options(self, options: Iterable[StringType]) -> None: label=self._impl.label, container_id=self._impl.container_id, hint=self._impl.hint, - initial_value=self._impl.initial_value, + value=self._impl.initial_value, options=self._impl_options, + visible=self._impl.visible, + disabled=self._impl.disabled, ) ) @@ -337,6 +339,7 @@ class GuiTabGroupHandle: _gui_api: GuiApi _container_id: str # Parent. _order: float + _visible: bool @property def order(self) -> float: @@ -374,6 +377,7 @@ def _sync_with_client(self) -> None: tab_labels=tuple(self._labels), tab_icons_base64=tuple(self._icons_base64), tab_container_ids=tuple(tab._id for tab in self._tabs), + visible=self._visible, ) ) @@ -567,6 +571,7 @@ def content(self, content: str) -> None: id=self._id, markdown=_parse_markdown(content, self._image_root), container_id=self._container_id, + visible=self._visible, ) ) diff --git a/src/viser/_messages.py b/src/viser/_messages.py index 9cf8cd411..fabdea20a 100644 --- a/src/viser/_messages.py +++ b/src/viser/_messages.py @@ -353,6 +353,7 @@ class GuiAddFolderMessage(Message): label: str container_id: str expand_by_default: bool + visible: bool @dataclasses.dataclass @@ -361,6 +362,7 @@ class GuiAddMarkdownMessage(Message): id: str markdown: str container_id: str + visible: bool @dataclasses.dataclass @@ -371,6 +373,7 @@ class GuiAddTabGroupMessage(Message): tab_labels: Tuple[str, ...] tab_icons_base64: Tuple[Union[str, None], ...] tab_container_ids: Tuple[str, ...] + visible: bool @dataclasses.dataclass @@ -382,7 +385,9 @@ class _GuiAddInputBase(Message): label: str container_id: str hint: Optional[str] - initial_value: Any + value: Any + visible: bool + disabled: bool @dataclasses.dataclass @@ -399,9 +404,9 @@ class GuiCloseModalMessage(Message): @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", @@ -428,13 +433,13 @@ class GuiAddSliderMessage(_GuiAddInputBase): min: float max: float step: Optional[float] - initial_value: float + value: float precision: int @dataclasses.dataclass class GuiAddNumberMessage(_GuiAddInputBase): - initial_value: float + value: float precision: int step: float min: Optional[float] @@ -443,22 +448,22 @@ class GuiAddNumberMessage(_GuiAddInputBase): @dataclasses.dataclass class GuiAddRgbMessage(_GuiAddInputBase): - initial_value: Tuple[int, int, int] + value: Tuple[int, int, int] @dataclasses.dataclass class GuiAddRgbaMessage(_GuiAddInputBase): - initial_value: Tuple[int, int, int, int] + value: Tuple[int, int, int, int] @dataclasses.dataclass class GuiAddCheckboxMessage(_GuiAddInputBase): - initial_value: bool + value: bool @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 @@ -467,7 +472,7 @@ class GuiAddVector2Message(_GuiAddInputBase): @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 @@ -476,18 +481,18 @@ class GuiAddVector3Message(_GuiAddInputBase): @dataclasses.dataclass class GuiAddTextMessage(_GuiAddInputBase): - initial_value: str + value: str @dataclasses.dataclass class GuiAddDropdownMessage(_GuiAddInputBase): - initial_value: str + value: str options: Tuple[str, ...] @dataclasses.dataclass class GuiAddButtonGroupMessage(_GuiAddInputBase): - initial_value: str + value: str options: Tuple[str, ...] diff --git a/src/viser/client/src/ControlPanel/Generated.tsx b/src/viser/client/src/ControlPanel/Generated.tsx index 9419eb2a1..6e12c131f 100644 --- a/src/viser/client/src/ControlPanel/Generated.tsx +++ b/src/viser/client/src/ControlPanel/Generated.tsx @@ -1,50 +1,30 @@ -import { - GuiAddFolderMessage, - GuiAddTabGroupMessage, -} from "../WebsocketMessages"; -import { ViewerContext, ViewerContextContents } from "../App"; +import { ViewerContext } from "../App"; import { makeThrottledMessageSender } from "../WebsocketFunctions"; -import { computeRelativeLuminance } from "./GuiState"; -import { - Collapse, - Image, - Paper, - Tabs, - TabsValue, - useMantineTheme, -} from "@mantine/core"; +import { GuiConfig } from "./GuiState"; +import { GuiComponentContext } from "./GuiComponentContext"; import { Box, - Button, - Checkbox, - ColorInput, - Flex, - NumberInput, - Select, - Slider, - Text, - TextInput, - Tooltip, } from "@mantine/core"; import React from "react"; -import Markdown from "../Markdown"; -import { ErrorBoundary } from "react-error-boundary"; -import { useDisclosure } from "@mantine/hooks"; -import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; - -/** Root of generated inputs. */ -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) viewer = React.useContext(ViewerContext)!; +import ButtonComponent from "../components/Button"; +import SliderComponent from "../components/Slider"; +import NumberInputComponent from "../components/NumberInput"; +import TextInputComponent from "../components/TextInput"; +import CheckboxComponent from "../components/Checkbox"; +import Vector2Component from "../components/Vector2"; +import Vector3Component from "../components/Vector3"; +import DropdownComponent from "../components/Dropdown"; +import RgbComponent from "../components/Rgb"; +import RgbaComponent from "../components/Rgba"; +import ButtonGroupComponent from "../components/ButtonGroup"; +import MarkdownComponent from "../components/Markdown"; +import TabGroupComponent from "../components/TabGroup"; +import FolderComponent from "../components/Folder"; + + +function GuiContainer({ containerId }: { containerId: string }) { + const viewer = React.useContext(ViewerContext)!; const guiIdSet = viewer.useGui((state) => state.guiIdSetFromContainerId[containerId]) ?? {}; @@ -54,685 +34,84 @@ export default function GeneratedGuiContainer({ const guiOrderFromId = viewer!.useGui((state) => state.guiOrderFromId); if (guiIdSet === undefined) return null; - const guiIdOrderPairArray = guiIdArray.map((id) => ({ + let guiIdOrderPairArray = guiIdArray.map((id) => ({ id: id, order: guiOrderFromId[id], })); + let pb = undefined; + guiIdOrderPairArray = guiIdOrderPairArray.sort((a, b) => a.order - b.order); + const inputProps = viewer.useGui((state) => guiIdOrderPairArray.map(pair => state.guiConfigFromId[pair.id])); + const lastProps = inputProps && inputProps[inputProps.length - 1]; + + // Done to match the old behaviour. Is it still needed? + if (lastProps !== undefined && lastProps.type === "GuiAddFolderMessage") { + pb = "0.125em"; + } const out = ( - - {guiIdOrderPairArray - .sort((a, b) => a.order - b.order) - .map((pair, index) => ( - - ))} + + {inputProps.map((conf) => )} ); return out; } -/** A single generated GUI element. */ -function GeneratedInput({ - id, - viewer, - folderDepth, - last, -}: { - id: string; - viewer?: ViewerContextContents; - folderDepth: number; - last: boolean; -}) { - // Handle GUI input types. - if (viewer === undefined) viewer = React.useContext(ViewerContext)!; - const conf = viewer.useGui((state) => state.guiConfigFromId[id]); - - // Handle nested containers. - if (conf.type == "GuiAddFolderMessage") - return ( - - - - ); - if (conf.type == "GuiAddTabGroupMessage") - return ; - if (conf.type == "GuiAddMarkdownMessage") { - let { visible } = - viewer.useGui((state) => state.guiAttributeFromId[conf.id]) || {}; - visible = visible ?? true; - if (!visible) return <>; - return ( - - Markdown Failed to Render} - > - {conf.markdown} - - - ); - } - +/** Root of generated inputs. */ +export default function GeneratedGuiContainer({ containerId }: { containerId: string; }) { + const viewer = React.useContext(ViewerContext)!; const messageSender = makeThrottledMessageSender(viewer.websocketRef, 50); - function updateValue(value: any) { - setGuiValue(conf.id, value); - messageSender({ type: "GuiUpdateMessage", id: conf.id, value: value }); + function setValue(id: string, value: any) { + setGuiValue(id, value); + messageSender({ type: "GuiUpdateMessage", id: id, value: value }); } const setGuiValue = viewer.useGui((state) => state.setGuiValue); - const value = - viewer.useGui((state) => state.guiValueFromId[conf.id]) ?? - conf.initial_value; - const theme = useMantineTheme(); + return + + - let { visible, disabled } = - viewer.useGui((state) => state.guiAttributeFromId[conf.id]) || {}; - - visible = visible ?? true; - disabled = disabled ?? false; - - if (!visible) return <>; - - let inputColor = - computeRelativeLuminance(theme.fn.primaryColor()) > 50.0 - ? theme.colors.gray[9] - : theme.white; +} - let labeled = true; - let input = null; +/** A single generated GUI element. */ +function GeneratedInput(conf: GuiConfig) { switch (conf.type) { + case "GuiAddFolderMessage": + return ; + case "GuiAddTabGroupMessage": + return ; + case "GuiAddMarkdownMessage": + return ; case "GuiAddButtonMessage": - labeled = false; - if (conf.color !== null) { - inputColor = - computeRelativeLuminance( - theme.colors[conf.color][theme.fn.primaryShade()], - ) > 50.0 - ? theme.colors.gray[9] - : theme.white; - } - - input = ( - - ); - break; + return ; case "GuiAddSliderMessage": - input = ( - - - ({ - thumb: { - background: theme.fn.primaryColor(), - borderRadius: "0.1em", - height: "0.75em", - width: "0.625em", - }, - })} - pt="0.2em" - showLabelOnHover={false} - min={conf.min} - max={conf.max} - step={conf.step ?? undefined} - precision={conf.precision} - value={value} - onChange={updateValue} - marks={[{ value: conf.min }, { value: conf.max }]} - disabled={disabled} - /> - - {parseInt(conf.min.toFixed(6))} - {parseInt(conf.max.toFixed(6))} - - - { - // Ignore empty values. - newValue !== "" && updateValue(newValue); - }} - size="xs" - min={conf.min} - max={conf.max} - hideControls - step={conf.step ?? undefined} - precision={conf.precision} - sx={{ width: "3rem" }} - styles={{ - input: { - padding: "0.375em", - letterSpacing: "-0.5px", - minHeight: "1.875em", - height: "1.875em", - }, - }} - ml="xs" - /> - - ); - break; + return ; case "GuiAddNumberMessage": - input = ( - { - // Ignore empty values. - newValue !== "" && updateValue(newValue); - }} - styles={{ - input: { - minHeight: "1.625rem", - height: "1.625rem", - }, - }} - disabled={disabled} - stepHoldDelay={500} - stepHoldInterval={(t) => Math.max(1000 / t ** 2, 25)} - /> - ); - break; + return ; case "GuiAddTextMessage": - input = ( - { - updateValue(value.target.value); - }} - styles={{ - input: { - minHeight: "1.625rem", - height: "1.625rem", - padding: "0 0.5em", - }, - }} - disabled={disabled} - /> - ); - break; + return ; case "GuiAddCheckboxMessage": - input = ( - { - updateValue(value.target.checked); - }} - disabled={disabled} - styles={{ - icon: { - color: inputColor + " !important", - }, - }} - /> - ); - break; + return ; case "GuiAddVector2Message": - input = ( - - ); - break; + return ; case "GuiAddVector3Message": - input = ( - - ); - break; + return ; case "GuiAddDropdownMessage": - input = ( - setValue(id, value)} + disabled={disabled} + searchable + maxDropdownHeight={400} + size="xs" + styles={{ + input: { + padding: "0.5em", + letterSpacing: "-0.5px", + minHeight: "1.625rem", + height: "1.625rem", + }, + }} + // zIndex of dropdown should be >modal zIndex. + // On edge cases: it seems like existing dropdowns are always closed when a new modal is opened. + zIndex={1000} + withinPortal + /> + ; +} \ No newline at end of file diff --git a/src/viser/client/src/components/Folder.tsx b/src/viser/client/src/components/Folder.tsx new file mode 100644 index 000000000..3620f60ac --- /dev/null +++ b/src/viser/client/src/components/Folder.tsx @@ -0,0 +1,77 @@ +import * as React from "react"; +import { useDisclosure } from "@mantine/hooks"; +import { GuiAddFolderMessage } from "../WebsocketMessages"; +import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; +import { Box, Collapse, Paper } from "@mantine/core"; +import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; +import { ViewerContext } from "../App"; + + +export default function FolderComponent({ + id, + label, + visible, + expand_by_default, +}: GuiAddFolderMessage) { + const viewer = React.useContext(ViewerContext)!; + const [opened, { toggle }] = useDisclosure(expand_by_default); + const guiIdSet = viewer.useGui( + (state) => state.guiIdSetFromContainerId[id], + ); + const guiContext = React.useContext(GuiComponentContext)!; + const isEmpty = guiIdSet === undefined || Object.keys(guiIdSet).length === 0; + + const ToggleIcon = opened ? IconChevronUp : IconChevronDown; + if (!visible) return <>; + return ( + + + {label} + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/viser/client/src/components/Markdown.tsx b/src/viser/client/src/components/Markdown.tsx new file mode 100644 index 000000000..da1e79682 --- /dev/null +++ b/src/viser/client/src/components/Markdown.tsx @@ -0,0 +1,18 @@ +import { Box, Text } from "@mantine/core"; +import Markdown from "../Markdown"; +import { ErrorBoundary } from "react-error-boundary"; +import { GuiAddMarkdownMessage } from "../WebsocketMessages"; + + +export default function MarkdownComponent({ visible, markdown }: GuiAddMarkdownMessage) { + if (!visible) return <>; + return ( + + Markdown Failed to Render} + > + {markdown} + + + ); +} \ No newline at end of file diff --git a/src/viser/client/src/components/NumberInput.tsx b/src/viser/client/src/components/NumberInput.tsx new file mode 100644 index 000000000..5056601b9 --- /dev/null +++ b/src/viser/client/src/components/NumberInput.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; +import { GuiAddNumberMessage } from "../WebsocketMessages"; +import { ViserInputComponent } from "./common"; +import { NumberInput } from "@mantine/core"; + + +export default function NumberInputComponent({ visible, id, label, hint, value, disabled, ...otherProps }: GuiAddNumberMessage) { + const { setValue } = React.useContext(GuiComponentContext)!; + const { precision, min, max, step } = otherProps; + if (!visible) return <>; + return + { + // Ignore empty values. + newValue !== "" && setValue(id, newValue); + }} + styles={{ + input: { + minHeight: "1.625rem", + height: "1.625rem", + }, + }} + disabled={disabled} + stepHoldDelay={500} + stepHoldInterval={(t) => Math.max(1000 / t ** 2, 25)} + /> + ; +} \ No newline at end of file diff --git a/src/viser/client/src/components/Rgb.tsx b/src/viser/client/src/components/Rgb.tsx new file mode 100644 index 000000000..f5c5fbbe6 --- /dev/null +++ b/src/viser/client/src/components/Rgb.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { ColorInput } from "@mantine/core"; +import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; +import { rgbToHex, hexToRgb } from "./utils"; +import { ViserInputComponent } from "./common"; +import { GuiAddRgbMessage } from "../WebsocketMessages"; + +export default function RgbComponent({ id, label, hint, value, disabled, visible }: GuiAddRgbMessage) { + const { setValue } = React.useContext(GuiComponentContext)!; + if (!visible) return <>; + return + setValue(id, hexToRgb(v))} + format="hex" + // zIndex of dropdown should be >modal zIndex. + // On edge cases: it seems like existing dropdowns are always closed when a new modal is opened. + dropdownZIndex={1000} + withinPortal + styles={{ + input: { height: "1.625rem", minHeight: "1.625rem" }, + icon: { transform: "scale(0.8)" }, + }} + /> + ; +} \ No newline at end of file diff --git a/src/viser/client/src/components/Rgba.tsx b/src/viser/client/src/components/Rgba.tsx new file mode 100644 index 000000000..755a5e51c --- /dev/null +++ b/src/viser/client/src/components/Rgba.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import { ColorInput } from "@mantine/core"; +import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; +import { rgbaToHex, hexToRgba } from "./utils"; +import { ViserInputComponent } from "./common"; +import { GuiAddRgbaMessage } from "../WebsocketMessages"; + +export default function RgbaComponent({ id, label, hint, value, disabled, visible }: GuiAddRgbaMessage) { + const { setValue } = React.useContext(GuiComponentContext)!; + if (!visible) return <>; + return + setValue(id, hexToRgba(v))} + format="hexa" + // zIndex of dropdown should be >modal zIndex. + // On edge cases: it seems like existing dropdowns are always closed when a new modal is opened. + dropdownZIndex={1000} + withinPortal + styles={{ + input: { height: "1.625rem", minHeight: "1.625rem" }, + }} + /> + ; +} \ No newline at end of file diff --git a/src/viser/client/src/components/Slider.tsx b/src/viser/client/src/components/Slider.tsx new file mode 100644 index 000000000..02bf300c3 --- /dev/null +++ b/src/viser/client/src/components/Slider.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { GuiAddSliderMessage } from "../WebsocketMessages"; +import { + Slider, + Box, + Flex, + Text, + NumberInput, +} from "@mantine/core"; +import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; +import { ViserInputComponent } from "./common"; + + + +export default function SliderComponent({ id, label, hint, visible, disabled, value, ...otherProps }: GuiAddSliderMessage) { + const { setValue } = React.useContext(GuiComponentContext)!; + if (!visible) return <>; + const updateValue = (value: number) => setValue(id, value); + const { min, max, precision, step } = otherProps; + let input = ( + + + ({ + thumb: { + background: theme.fn.primaryColor(), + borderRadius: "0.1em", + height: "0.75em", + width: "0.625em", + }, + })} + pt="0.2em" + showLabelOnHover={false} + min={min} + max={max} + step={step ?? undefined} + precision={precision} + value={value} + onChange={updateValue} + marks={[{ value: min }, { value: max }]} + disabled={disabled} + /> + + {parseInt(min.toFixed(6))} + {parseInt(max.toFixed(6))} + + + { + // Ignore empty values. + newValue !== "" && updateValue(newValue); + }} + size="xs" + min={min} + max={max} + hideControls + step={step ?? undefined} + precision={precision} + sx={{ width: "3rem" }} + styles={{ + input: { + padding: "0.375em", + letterSpacing: "-0.5px", + minHeight: "1.875em", + height: "1.875em", + }, + }} + ml="xs" + /> + + ); + + const containerProps = {}; + // if (marks?.some(x => x.label)) + // containerProps = { ...containerProps, "mb": "md" }; + + input = {input} + return {input}; +} \ No newline at end of file diff --git a/src/viser/client/src/components/TabGroup.tsx b/src/viser/client/src/components/TabGroup.tsx new file mode 100644 index 000000000..11f21a3f5 --- /dev/null +++ b/src/viser/client/src/components/TabGroup.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { GuiAddTabGroupMessage } from "../WebsocketMessages"; +import { Tabs, TabsValue } from "@mantine/core"; +import { Image } from "@mantine/core"; +import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; + + +export default function TabGroupComponent({ + tab_labels, + tab_icons_base64, + tab_container_ids, + visible, +}: GuiAddTabGroupMessage) { + const [tabState, setTabState] = React.useState("0"); + const icons = tab_icons_base64; + const { GuiContainer } = React.useContext(GuiComponentContext)!; + if (!visible) return <>; + return ( + + + {tab_labels.map((label, index) => ( + ({ + filter: + theme.colorScheme == "dark" ? "invert(1)" : undefined, + })} + src={"data:image/svg+xml;base64," + icons[index]} + /> + ) + } + > + {label} + + ))} + + {tab_container_ids.map((containerId, index) => ( + + + + ))} + + ); +} \ No newline at end of file diff --git a/src/viser/client/src/components/TextInput.tsx b/src/viser/client/src/components/TextInput.tsx new file mode 100644 index 000000000..671354162 --- /dev/null +++ b/src/viser/client/src/components/TextInput.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { TextInput } from "@mantine/core"; +import { ViserInputComponent } from "./common"; +import { GuiAddTextMessage } from "../WebsocketMessages"; +import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; + +export default function TextInputComponent(props: GuiAddTextMessage) { + const { id, hint, label, value, disabled, visible } = props; + const { setValue } = React.useContext(GuiComponentContext)!; + if (!visible) return <>; + return + { + setValue(id, value.target.value); + }} + styles={{ + input: { + minHeight: "1.625rem", + height: "1.625rem", + padding: "0 0.5em", + }, + }} + disabled={disabled} + /> + ; +} \ No newline at end of file diff --git a/src/viser/client/src/components/Vector2.tsx b/src/viser/client/src/components/Vector2.tsx new file mode 100644 index 000000000..1c98276c6 --- /dev/null +++ b/src/viser/client/src/components/Vector2.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; +import { GuiAddVector2Message } from "../WebsocketMessages"; +import { VectorInput, ViserInputComponent } from "./common"; + +export default function Vector2Component({ id, hint, label, visible, disabled, value, ...otherProps }: GuiAddVector2Message) { + const { min, max, step, precision } = otherProps; + const { setValue } = React.useContext(GuiComponentContext)!; + if (!visible) return <>; + return + setValue(id, value)} + min={min} + max={max} + step={step} + precision={precision} + disabled={disabled} + /> + ; +} \ No newline at end of file diff --git a/src/viser/client/src/components/Vector3.tsx b/src/viser/client/src/components/Vector3.tsx new file mode 100644 index 000000000..42cb569df --- /dev/null +++ b/src/viser/client/src/components/Vector3.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; +import { GuiAddVector3Message } from "../WebsocketMessages"; +import { VectorInput, ViserInputComponent } from "./common"; + +export default function Vector3Component({ id, hint, label, visible, disabled, value, ...otherProps }: GuiAddVector3Message) { + const { min, max, step, precision } = otherProps; + const { setValue } = React.useContext(GuiComponentContext)!; + if (!visible) return <>; + return + setValue(id, value)} + min={min} + max={max} + step={step} + precision={precision} + disabled={disabled} + /> + ; +} \ No newline at end of file diff --git a/src/viser/client/src/components/common.tsx b/src/viser/client/src/components/common.tsx new file mode 100644 index 000000000..5a9bfac6b --- /dev/null +++ b/src/viser/client/src/components/common.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +import { + Box, + Flex, + Text, + NumberInput, + Tooltip, +} from '@mantine/core'; +import { GuiComponentContext } from "../ControlPanel/GuiComponentContext"; + +export function ViserInputComponent({ id, label, hint, children }: { id: string, children: React.ReactNode, label?: string, hint?: string | null }) { + const { folderDepth } = React.useContext(GuiComponentContext)!; + if (hint !== undefined && hint !== null) { + children = // We need to add for inputs that we can't assign refs to. + ( + + {children} + + ); + } + + if (label !== undefined) + children = ( + + ); + + return ( + + {children} + + ); +} + + +/** GUI input with a label horizontally placed to the left of it. */ +function LabeledInput(props: { + id: string; + label: string; + input: React.ReactNode; + folderDepth: number; +}) { + return ( + + + + + + + {props.input} + + ); +} + + +export function VectorInput( + props: + | { + id: string; + n: 2; + value: [number, number]; + min: [number, number] | null; + max: [number, number] | null; + step: number; + precision: number; + onChange: (value: number[]) => void; + disabled: boolean; + } + | { + id: string; + n: 3; + value: [number, number, number]; + min: [number, number, number] | null; + max: [number, number, number] | null; + step: number; + precision: number; + onChange: (value: number[]) => void; + disabled: boolean; + }, +) { + return ( + + {[...Array(props.n).keys()].map((i) => ( + { + const updated = [...props.value]; + updated[i] = v === "" ? 0.0 : v; + props.onChange(updated); + }} + size="xs" + styles={{ + root: { flexGrow: 1, width: 0 }, + input: { + paddingLeft: "0.5em", + paddingRight: "1.75em", + textAlign: "right", + minHeight: "1.875em", + height: "1.875em", + }, + rightSection: { width: "1.2em" }, + control: { + width: "1.1em", + }, + }} + precision={props.precision} + step={props.step} + min={props.min === null ? undefined : props.min[i]} + max={props.max === null ? undefined : props.max[i]} + stepHoldDelay={500} + stepHoldInterval={(t) => Math.max(1000 / t ** 2, 25)} + disabled={props.disabled} + /> + ))} + + ); +} diff --git a/src/viser/client/src/components/utils.tsx b/src/viser/client/src/components/utils.tsx new file mode 100644 index 000000000..0ec064548 --- /dev/null +++ b/src/viser/client/src/components/utils.tsx @@ -0,0 +1,32 @@ +// Color conversion helpers. + +export function rgbToHex([r, g, b]: [number, number, number]): string { + const hexR = r.toString(16).padStart(2, "0"); + const hexG = g.toString(16).padStart(2, "0"); + const hexB = b.toString(16).padStart(2, "0"); + return `#${hexR}${hexG}${hexB}`; +} + +export function hexToRgb(hexColor: string): [number, number, number] { + const hex = hexColor.slice(1); // Remove the # in #ffffff. + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + return [r, g, b]; +} +export function rgbaToHex([r, g, b, a]: [number, number, number, number]): string { + const hexR = r.toString(16).padStart(2, "0"); + const hexG = g.toString(16).padStart(2, "0"); + const hexB = b.toString(16).padStart(2, "0"); + const hexA = a.toString(16).padStart(2, "0"); + return `#${hexR}${hexG}${hexB}${hexA}`; +} + +export function hexToRgba(hexColor: string): [number, number, number, number] { + const hex = hexColor.slice(1); // Remove the # in #ffffff. + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + const a = parseInt(hex.substring(6, 8), 16); + return [r, g, b, a]; +} \ No newline at end of file