Building an App: Layout, Auto-UI & App Builder

These modules help you assemble a complete, runnable Dash application: arrange a responsive page, optionally auto-generate controls from typed Python, wire up WebSocket communication, and assemble the server. They are convenience helpers – you can mix in plain Dash components, callbacks, and layouts wherever you like.

Tutorial

The snippet below shows a typical end-to-end flow; each step links to its detailed section. Only the layout and the app builder are essential – the viewer and the auto-generated UI are optional and shown here to illustrate a full app.

from dataclasses import dataclass

from kaolin.visualize.dash import ViewerBuilder, auto_ui
from kaolin.visualize.dash.layout import StandardLayoutHelper
from kaolin.visualize.dash.builder import WebappBuilder
from kaolin.render.easy_render import default_camera

# 1. (Optional) Build an interactive viewer (server-side rendering over a
#    WebSocket). Skip this for a non-3D app.
vb = ViewerBuilder(camera=default_camera(800))
layer = vb.add_layer('canvas')
vb.add_remote_rendering(active_layer_id=layer, connection_id='main-ws')
viewer = vb.build()

# 2. (Optional) Auto-generate UI controls from a typed dataclass. You can
#    instead hand-write any plain Dash components and callbacks.
@dataclass
class RenderSettings:
    exposure: float = 1.0
    wireframe: bool = False

controls, field_specs, store = auto_ui.controls_from_dataclass(
    RenderSettings(), id_prefix='render', make_store=True)

# 3. Arrange your content in a responsive page (plain Dash components welcome).
layout = StandardLayoutHelper(title='My Kaolin App')
layout.add_sidebar()
layout.add_sidebar_components(controls + [store])
layout.add_main_content([viewer])

# 4. Assemble the server, optionally registering a WebSocket handler.
builder = WebappBuilder(debug=True)
builder.set_layout_helper(layout)
app = builder.build(ws_handlers=[(EchoHandler, ())])
# app.run_server(...)

The pieces, in order:

  1. (Optional) Add a viewer built with a ViewerBuilder – see the dedicated Kaolin Viewer: Layers and Behaviors page.

  2. (Optional) Auto-generate UI from a dataclass (or function, or behavior) with auto_ui – or just use plain Dash components and callbacks. See Auto-UI.

  3. Lay it out with a StandardLayoutHelper – see Layout.

  4. Assemble the server with WebappBuilder, optionally registering a WebSocket handler for custom client-server communication – see App builder and WebSocket handlers.

See Customizing your app for going beyond these helpers.

Auto-UI

Auto-UI is entirely optional: it is just a shortcut for generating standard Dash controls. You are free to hand-write any Dash components (dcc.Slider, dbc.Input, …) and wire them with ordinary Dash @callback / clientside callbacks instead – mix and match as you like.

When it helps, auto_ui builds Dash controls automatically from typed sources – a dataclass instance (controls_from_dataclass()), a callable’s signature (controls_from_function()), or a list of OptionSpec objects (make_controls()). Generated controls get deterministic ids of the form f"{id_prefix}-{field_name}" so they are easy to wire into callbacks, and an optional backing dash.dcc.Store can be seeded from the instance.

from dataclasses import dataclass
from kaolin.visualize.dash import auto_ui

@dataclass
class RenderSettings:
    exposure: float = 1.0
    wireframe: bool = False

controls, field_specs, store = auto_ui.controls_from_dataclass(
    RenderSettings(), id_prefix='render', make_store=True)

Note

To generate controls for a behavior’s options rather than a dataclass, use add_user_behavior_options(), which is described under Built-in behaviors on the viewer page. It builds on the same auto_ui machinery, additionally pushing each edit straight to the live behavior in the browser.

Auto-generation of Dash controls from typed sources.

Controls are built from OptionSpec objects, which can be obtained from several sources:

Generated controls have a deterministic id of the form f"{id_prefix}-{field_name}" so they can be wired to a dash.dcc.Store or to callbacks without manual id juggling.

controls_from_dataclass() can also build a backing dash.dcc.Store seeded from the instance (pass make_store=True); apply_store_to_dataclass() restores store data back onto a typed instance. bind_controls_to_behavior_clientside() pipes control changes to a JS InteractiveBehavior via kaolin.core.event.requestBehaviorSetOption.

class kaolin.visualize.dash.auto_ui.FieldSpec(name: str, control_id: str, value_prop: str, json_to_py: ~typing.Callable[[~typing.Any], ~typing.Any] = <function FieldSpec.<lambda>>)

Bases: object

Describes how a generated control wires into a dash.dcc.Store (or any other sink).

One FieldSpec is produced alongside every control. The FieldSpec tells a binder (a) which Dash prop carries the value, and (b) how to convert the JSON-friendly value back into the original Python type when reconstructing an underlying instance on the server.

name

original field/parameter/option name as written in the source (e.g. selection_brush_radius). Used as the dict key inside the store. For fields produced by recursing into a nested dataclass, name is a dotted path such as "outer.inner".

Type

str

control_id

deterministic Dash component id of the generated control (e.g. "usr-selection_brush_radius"); use as Input(control_id, value_prop) or getElementById(control_id).

Type

str

value_prop

name of the Dash prop that carries the field’s current value. Differs by control type: "on" for dash_daq.BooleanSwitch, "value" for dcc.Slider, dcc.Dropdown, dcc.Input.

Type

str

json_to_py

Callable[[Any], Any] that converts the raw JSON-friendly value emitted by the control (str / int / float / bool) back into the field’s Python type. Sourced from OptionSpec.json_to_py (the type itself for primitives, the Enum subclass for enums, identity otherwise). Defaults to the identity.

Type

Callable[[Any], Any]

kaolin.visualize.dash.auto_ui.apply_store_to_dataclass(store_data: dict, instance: Any, field_specs: list[kaolin.visualize.dash.auto_ui.FieldSpec]) None

Mutate instance (and any nested dataclasses it owns) in place from store data.

Inverse of _make_default_store_data(). For every spec, reads store_data[spec.name], applies spec.json_to_py, and writes it onto the appropriate nested attribute. Missing keys are skipped silently; None values are written through without casting.

kaolin.visualize.dash.auto_ui.bind_controls_to_behavior_clientside(field_specs: list[kaolin.visualize.dash.auto_ui.FieldSpec], behavior_id: str, viewer_id: str, triggers: list[dash.development.base_component.Component]) None

Pipe generated controls to a JS InteractiveBehavior on every change.

Requires a dummy trigger component.

kaolin.visualize.dash.auto_ui.controls_from_dataclass(instance: Any, id_prefix: str, names: Optional[Iterable[str]] = None, annotations: dict[str, Any] | None = None, make_store: bool = False, storage_type: str = 'session') tuple[list[dash.development.base_component.Component], list[kaolin.visualize.dash.auto_ui.FieldSpec], dash.dcc.Store.Store | None]

Build Dash controls + field specs (and optionally a backing store) for a dataclass.

Derives an OptionSpec list via kaolin.visualize.dash.option.specs_from_dataclass() and renders it with make_controls(). When make_store is set, also returns a dash.dcc.Store seeded with the instance’s current values (keyed to match the field specs, with dotted keys for nested-dataclass fields).

Parameters
  • instance (Any) – a dataclass instance (or a class instantiable with defaults).

  • id_prefix (str) – prefix for generated component ids and the store id.

  • names (Optional[Iterable[str]]) – field names to include and order; defaults to all fields in declaration order.

  • annotations (Optional[dict]) – {field_name: annotation} overrides.

  • make_store (bool) – if True, build and return a backing dcc.Store.

  • storage_type (str) – dcc.Store storage type when make_store is set.

Returns

(tuple) (controls, field_specs, store) where store is None unless make_store is True.

kaolin.visualize.dash.auto_ui.controls_from_function(fnc: Callable, id_prefix: str, names: Optional[Iterable[str]] = None, annotations: dict[str, Any] | None = None) tuple[list[dash.development.base_component.Component], list[kaolin.visualize.dash.auto_ui.FieldSpec]]

Build Dash controls + field specs for a callable’s parameters.

Behaves like controls_from_dataclass() but introspects a function signature via kaolin.visualize.dash.option.specs_from_function() (*args / **kwargs are skipped, parameters without a default map to default=None). No backing store is produced, as there is no instance to persist.

Parameters
  • fnc (Callable) – callable to introspect (bound methods are supported).

  • id_prefix (str) – prefix for generated component ids.

  • names (Optional[Iterable[str]]) – parameter names to include and order.

  • annotations (Optional[dict]) – {param_name: annotation} overrides.

Returns

(tuple) (controls, field_specs).

kaolin.visualize.dash.auto_ui.make_control(spec: OptionSpec, id_prefix: str, persistence_type: str | None = None, name_prefix: str = '') tuple[list[dash.development.base_component.Component], list[kaolin.visualize.dash.auto_ui.FieldSpec]]

Render one OptionSpec into Dash components plus matching field specs.

The spec’s OptionKind selects the control: BOOL -> dash_daq.BooleanSwitch, ENUM -> dash.dcc.Dropdown, INT / FLOAT with both bounds -> dash.dcc.Slider (otherwise a numeric dcc.Input), STRING -> text input, COLOR -> native color input, GROUP -> a recursively rendered section, and ANY (or anything unexpected) -> a text-input fallback. Each generated control gets a deterministic id f"{id_prefix}-{name_prefix}{spec.name}".

Parameters
  • spec (OptionSpec) – the option to render.

  • id_prefix (str) – prefix for the generated component id.

  • persistence_type (Optional[str]) – if set, enables Dash persistence on the control with this persistence_type (e.g. 'session').

  • name_prefix (str) – dotted prefix prepended to the spec name for nested (group) children, so field-spec names read like "outer.inner".

Returns

(tuple) (components, field_specs) – a one-element component list (a wrapped control, or a section Div for a group) and one FieldSpec per rendered leaf control.

kaolin.visualize.dash.auto_ui.make_controls(specs: Iterable[OptionSpec], id_prefix: str, persistence_type: str | None = None) tuple[list[dash.development.base_component.Component], list[kaolin.visualize.dash.auto_ui.FieldSpec]]

Render a list of OptionSpec objects into Dash components + field specs.

The single rendering pass shared by the OptionSpec ingestion entry points; each spec is delegated to make_control().

Parameters
  • specs (Iterable[OptionSpec]) – the options to render, in order.

  • id_prefix (str) – prefix applied to every generated component id.

  • persistence_type (Optional[str]) – if set, enables Dash persistence on each control with this persistence_type (e.g. 'session').

Returns

(tuple) (components, field_specs) – two lists; a GROUP spec contributes a single section Div but one FieldSpec per leaf (with dotted names).

Layout

layout provides the AppLayoutHelper protocol and StandardLayoutHelper, a responsive (Bootstrap) scaffold for sidebar + viewport apps. Build a layout by adding a sidebar, dropping components/sections into it, and placing your viewer(s) in the main content area:

from kaolin.visualize.dash.layout import StandardLayoutHelper

layout = StandardLayoutHelper(title='My Kaolin App')
layout.add_sidebar()
layout.add_sidebar_components(controls + [store])
layout.add_main_content([viewer])
# or: layout.add_viewer_grid([viewer_a, viewer_b], columns=2)
class kaolin.visualize.dash.layout.AppLayoutHelper(*args, **kwargs)

Bases: Protocol

class kaolin.visualize.dash.layout.StandardLayoutHelper(title='Kaolin App', max_width: Optional[Literal['sm', 'md', 'lg', 'xl', 'xxl', 'flex']] = 'lg')

Bases: AppLayoutHelper

add_modal(content, title=None)

Create a centered overlay modal with a close (X) button.

Parameters
  • content – A Dash component (or list of components) to display inside the modal body.

  • title – Optional title shown at the top of the modal.

Returns

A dict describing the modal with keys component, overlay_id, close_id and trigger_ids (a list of ids whose clicks open the modal; append to it to add triggers).

add_sidebar(disappearing=False, width=20, min_width_px=300)

Adds sidebar component to the layout. :param disappearing:

Returns:

add_sidebar_section(name, collapsible=True, bootstrap_icon=None, collapsed_on_load=False)

Add a titled section to the sidebar.

Parameters
  • name – Section title displayed in the header.

  • collapsible – If True (default), the section header is clickable to collapse/expand its content, with a dropdown caret indicator shown in the header.

  • bootstrap_icon – Optional Bootstrap icon name (e.g. "bi-boxes") shown before the section title.

  • collapsed_on_load – If True, a collapsible section starts collapsed on page load (default False, expanded).

Returns

The section container component.

add_viewer_grid(viewers, columns=None, labels=None)

Lay out a flat list of viewers in a CSS grid in the main content area.

Cells are equal 1fr tracks at full height; grid items stretch to fill their cell, which gives each viewer a definite, non-zero size (viewers are width:100%/height:100% and size their canvas from the cell’s pixel dimensions). Wraps the grid in a div and forwards it to add_main_content().

Parameters
  • viewers – flat list of components, placed left-to-right, top-to-bottom.

  • columns – number of columns. Defaults to a single row (all viewers side by side). E.g. columns=2 with four viewers yields a 2x2 grid.

  • labels – optional list of strings, one per viewer. When provided, each viewer is wrapped in a column with a text header above it. Must be the same length as viewers.

register_help_trigger(trigger)

Register an existing component (or its id) as an extra trigger that opens the help page.

Must be called after add_help_page() and before the app is built.

WebSocket handlers

Rich client-server communication (server-side rendering, streaming geometry, custom binary messages) flows over WebSockets. On the server you implement a message handler – a class satisfying SyncMessageHandlerProtocol (or its async sibling AsyncMessageHandlerProtocol) – and register it with the app builder. A handler declares which message tags it accepts and reacts to them, writing messages back to the client:

from kaolin.visualize.web.sockets import SyncMessageHandlerProtocol

class EchoHandler(SyncMessageHandlerProtocol):
    def accepted_message_tags(self):
        return ['ping']

    def on_connection_open(self, write_message_fn):
        write_message_fn({'status': 'connected'}, False)  # False -> JSON

    def on_message(self, message_tag, message_content, write_message_fn):
        if message_tag == 'ping':
            write_message_fn({'pong': message_content}, False)

Handlers are registered by passing ws_handlers to build() as a list of (handler_class, args) (or (handler_class, args, kwargs)) tuples; a fresh handler instance is constructed per connection, so per-client state is never shared:

app = builder.build(ws_handlers=[(EchoHandler, ())])

For large payloads such as images or tensors, use binary messages: encode them on the server with kaolin.visualize.web.io and decode them in the browser with the matching utilities in the JavaScript API. The clientside counterpart that opens these connections and dispatches messages by tag is core/sockets.ts, also documented there.

The full handler API – the protocols, ready-made handlers (e.g. AnyRendererMessageHandler), and the WebSocketHandlerManager – lives on the dedicated kaolin.visualize.web.sockets page.

Customizing your app

WebappBuilder is deliberately thin; reach for plain Dash whenever it serves you better, and use the builder hooks below to extend an app:

  • Custom client-side behaviors – drop a plain .js file defining and registering your behaviors (see Custom behaviors on the viewer page) into your app’s assets/ directory. Dash serves assets/ automatically and loads .js files on page load, so the kaolin namespace and your BehaviorRegister.register(...) calls are available with no extra wiring. To make Python aware of those behaviors (e.g. for add_user_behavior_options()), also call BehaviorLibrary.register_user_directory(.

  • Extra scripts / stylesheetsadd_extra_scripts() and add_extra_stylesheets().

  • ES module imports (e.g. third-party libraries) – add_importmap_item().

  • Serving your own files / directoriesadd_static_file() and add_static_dir().

  • Raw HTML in the document bodyadd_raw_body_html().

  • Persisted, per-tab user settingsadd_user_settings().

For anything beyond these, build the dash.Dash app (and tornado server) yourself – nothing here is required.

App builder

WebappBuilder ties layout, assets, user settings, and WebSocket handlers into a single Dash + tornado server. SessionRegistry bridges Dash callbacks and per-tab WebSocket state.

class kaolin.visualize.dash.builder.SessionRegistry

Bases: object

Process-wide registry mapping tab_uuid to a _SessionEntry.

Bridges Dash (@callback triggered by a dcc.Store change with State('kaolin-tab-uuid', 'data')) and the WebSocket transport (WebSocketHandlerManager.tab_uuid). Both sides converge on the same tab_uuid key so a setting written by a control reaches every WS handler belonging to that browser tab.

NOTE: prototype-quality, app-level. Future improvements:
  • Promote to a library helper (e.g. kaolin.visualize.dash.session_registry).

  • GC stale entries when the last WS handler for a tab disconnects (use GlobalWebSocketConnectionManager.get_handlers_by_tab(tab_uuid) to decide). Acceptable to leak for now – short-lived dev runs.

  • Make the per-entry payload pluggable (factory) instead of hard-coding ServerSideUserSettings.

classmethod remove(tab_uuid: Optional[str]) None

Drop an entry. Idempotent. Intended for future GC; unused for now.