Layouts — Split Views & Controls¶
A VisualizerApp exposes the split-view and control machinery through its
self.viz attribute (a plain Visualizer), so you can build a multi-pane
layout with embedded controls and drive it from async handlers — all within the
managed lifecycle (init → block → cleanup).
For the underlying view model (sizes, splitters, panes), see Split Views; for the panel-control methods, see Controls.
The plain scene URL that VisualizerApp.run() opens is itself a one-SceneView
layout, so a GroupView overlay (or any control view mounted via viz.add)
renders there exactly as it does inside a custom split layout — the two modes
share one render path.
Flow containers (StackView/GroupView) accept gap/align/justify, and a
child's preferred_* maps to CSS flex (e.g. Size.fr(1) fills the leftover
space beside a button). See Split Views for the
sizing model and the flex mapping table.
Controls in a layout¶
Controls are declarative *View classes placed in a GroupView/StackView
pane (or mounted in the overlay via viz.add(view)):
| API | Where it appears | Use for |
|---|---|---|
SliderView / DropdownView / ButtonView / GroupView (inside a GroupView/StackView) |
A pane in your SplitView layout |
A sidebar/toolbar next to one or more scene panes |
viz.add(view) |
The default layout's overlay | Quick controls over a scene without a custom layout |
The async handler contract is (value, event). The declarative view classes
are documented in full in Control Views (xxxView).
A split-view app¶
Build the layout, register it, and open it under a single URL. VisualizerApp
does not open layouts by default (its run() opens the plain scene URL), so
override run() to open the layout instead:
import asyncio
from pytanga.geometry import Point, Sphere
from pytanga.viz import (
ButtonView,
CameraConfig3d,
ControlEvent,
GroupView,
SceneView,
Size,
SliderView,
SplitView,
VisualizerApp,
)
class MyApp(VisualizerApp):
def __init__(self):
super().__init__(title="My Split-View App")
self._layout = self._build_layout()
def _build_layout(self):
# Keep the main pane so a button can re-aim its camera at runtime.
self._main_view = SceneView("")
return SplitView(
orientation="horizontal",
children=[
GroupView(
"Controls",
[
SliderView(
"radius",
label="Radius",
min=0.2,
max=5.0,
value=1.0,
on_change=self.on_radius,
),
ButtonView("btn_topdown", label="Top-down", on_click=self.on_topdown),
ButtonView("btn_quit", label="Quit", on_click=self.on_quit),
],
),
SplitView(
orientation="vertical",
sizes=[Size.percent(70), Size.percent(30)],
children=[
self._main_view,
# The same scene from a different initial camera — each
# pane keeps its own orbit/zoom.
SceneView(
"",
camera=CameraConfig3d(
position=(8.0, 0.0, 0.0), target=(0.0, 0.0, 0.0)
),
),
],
),
],
)
def run(self, *, wait_for_browser=True, timeout=30.0):
# Open the layout URL instead of the plain scene URL. ``show(layout=…)``
# also registers the layout (and its control-view handlers).
ok = self.viz.show(layout=self._layout, wait_for_browser=wait_for_browser)
if not ok:
raise RuntimeError(
"Server failed to start or no browser connected. "
f"Open {self.viz.url} manually."
)
try:
asyncio.run(self._app_main())
except KeyboardInterrupt:
pass
finally:
self.viz.stop_server()
async def init(self) -> None:
self._sphere = self.viz(Sphere(Point(0, 0, 0), radius=1.0), opacity=0.3)
self.viz.flush()
async def on_radius(self, value: float, _event: ControlEvent) -> None:
self.viz.update_entity(self._sphere.id, Sphere(Point(0, 0, 0), radius=value))
self.viz.flush()
async def on_topdown(self, _value, _event) -> None:
self.viz.set_view_camera(
self._main_view,
CameraConfig3d(position=(0.0, 8.0, 0.0), target=(0.0, 0.0, 0.0)),
)
async def on_quit(self, _value, _event) -> None:
self.request_shutdown()
async def cleanup(self) -> None:
pass # teardown
if __name__ == "__main__":
MyApp().run()
Key points:
- Handlers are registered automatically.
show(layout=…)→set_layout(...)walks the view tree and registers each control view'son_change/on_click, so aSliderView/ButtonViewbehaves exactly like a panel control. SceneView("")is the main scene. A secondSceneView("")shows the same scene from a different initial camera; each pane keeps its own orbit/zoom.set_view_camera(view, camera)re-aims one pane at runtime (targeted by theSceneViewinstance), without touching the scene or its other panes.
Multiple scenes in a layout¶
SceneView references a scene by name (or VizSceneHandle). Create named
scenes before run() so they exist when the layout browser connects:
def __init__(self):
super().__init__(title="Multi-scene app")
self._detail = self.viz.scene("detail") # create before the browser connects
self._layout = SplitView(
"horizontal",
[SceneView(""), SceneView("detail")],
)
async def init(self):
self._detail.add(Point(2, 0, 0), color="#44ff44")
self.viz.flush()
Controls live in layouts, not scenes: place them in whichever pane's
GroupView/StackView (or overlay) you want.
See Also¶
- Split Views — the view hierarchy,
Sizeunits, splitters, overlays, and per-pane cameras - Controls —
SliderView/DropdownView/ButtonView/GroupView - Handlers & Lifecycle — the handler contract and the app lifecycle