Refactor Studio customization and message dispatch to use decorator-based handlers

This change replaces fixed callback protocols (e.g., ViewerGuiHook, ViewerUpdateHook, SimEventHandler) with a general-purpose, priority-based message and event handling mechanism.

Key changes:
- Handler decorator and registry: Introduced the `@messages.handler(priority=...)` decorator and `HandlerRegistry` (`handler_registry.py`). Methods marked as handlers are automatically discovered and dispatched by priority (CRITICAL, USER, LIBRARY, INTERNAL) or method resolution order.
- Local lifecycle events: Added `ViewerAppInitEvent`, `BuildGuiEvent`, and `UpdateEvent` to `messages.py`. Custom GUI rendering and per-frame update logic can now be implemented as standard event handlers without needing separate interface protocols.
- Streamlined launch and app APIs: Replaced individual hook and handler arguments in `launch_passive`, `ViewerApp`, and `ViewerHandle` with unified `viewer_handlers` and `sim_handlers` lists.
- Module restructuring: Extracted simulation-side message handling and `ViewerHandle` from `sim_app.py` into a dedicated `viewer_handle.py` module, removing `sim_app.py`.
- Sample updates: Migrated existing examples (such as `implot.py`) to use the new handler pattern and lifecycle events.

PiperOrigin-RevId: 941729322
Change-Id: I93e7c0edf0a13a8dc854f9e2083451f7c25a1825
This commit is contained in:
Matija Kecman
2026-07-02 09:14:02 -07:00
committed by Copybara-Service
parent 1ca64b441b
commit 0dfa4b509a
7 changed files with 507 additions and 358 deletions
+82 -2
View File
@@ -14,8 +14,10 @@
"""Messages and Channels for Studio."""
import dataclasses
from typing import Protocol
from typing import runtime_checkable
import enum
import inspect
from typing import Any, Callable, Protocol, runtime_checkable
import mujoco
from mujoco.experimental.studio import sim
import numpy as np
@@ -147,3 +149,81 @@ class StepControlSnapshot(Snapshot):
@dataclasses.dataclass(frozen=True)
class ExitEvent(Event):
"""An event requesting to exit."""
# ---------------------------------------------------------------------------
# Message handling decorator.
# ---------------------------------------------------------------------------
_HANDLER_INFO_ATTR = '_studio_handler_info'
@dataclasses.dataclass(frozen=True)
class _HandlerInfo:
"""Metadata stamped on a decorated method."""
priority: int
message_type: type[Message]
class Priority(enum.IntEnum):
"""Execution priority levels for message handlers.
Handlers with higher values execute first. When multiple handlers share the
same priority, their relative order is undefined.
"""
INTERNAL = 1 # For built-in handlers.
LIBRARY = 10 # For library extensions.
USER = 100 # For user extensions. Default when no priority is specified.
CRITICAL = 1000 # For handlers that must run before everything else.
def handler(
fn: Callable[..., Any] | None = None, *, priority: int = Priority.USER
) -> Callable[..., Any]:
"""Decorator to mark a class method as a message handler.
The class method must accept exactly two arguments: self and a message event.
e.g., ``@handler`` or ``@handler(priority=...)``.
Args:
fn: The method to stamp with handler metadata.
priority: The priority of the handler.
Returns:
The stamped method.
"""
def stamp(target: Callable[..., Any]) -> Callable[..., Any]:
fn_name = getattr(target, '__name__', str(target))
params = list(inspect.signature(target).parameters.values())
if len(params) != 2:
raise TypeError(
f'{fn_name} must accept exactly two arguments, got {len(params)}'
)
message_type = params[1].annotation
if message_type is inspect.Parameter.empty:
raise TypeError(
f'{fn_name}: second parameter must have a type annotation'
)
if not (
isinstance(message_type, type) and issubclass(message_type, Message)
):
raise TypeError(
f'{fn_name}: second parameter type annotation {message_type} is not'
' a Message subclass'
)
info = _HandlerInfo(priority=priority, message_type=message_type)
setattr(target, _HANDLER_INFO_ATTR, info)
return target
# Used without parens: e.g., @handler
if fn is not None:
return stamp(fn)
# Used with parens: e.g., @handler() or @handler(priority=...)
return stamp