Sandbox — LSD is in active testing. All systems operational·payments are test-mode (no real charges).
Overview Use Cases News Docs Tool Store Support Community Get LSD
All docs
Docs/ Building Tools/ Contributing UI with @tool.view
Building Tools

Contributing UI with @tool.view

One decorator, @tool.view, contributes UI. It always returns a View; location= (sidebar / overlay / editor) is only where the View lands. Nothing imports GTK.

TL;DRA tool contributes UI with one decorator, @tool.view, and it always returns a View. location= only chooses where that View lands — "sidebar" (a docked input form, informally a panel), "overlay" (a HUD over the 3D viewer), or "editor" (an editor tab, the default). A View is param() fields plus @action buttons, and (with a kind) represents data as a table, chart, flowchart, or custom drawing. There is no separate panel or overlay type — just one View, placed. Nothing imports GTK.

Beyond nodes, a tool can contribute its own UI. There is one decorator@tool.view — and it always returns one class, a View. location= only chooses where that View lands, and a View never imports GTK:

  • location="sidebar" — docks it as an input form in the shell (informally a panel);
  • location="overlay" — floats it over the 3D viewer as a HUD;
  • location="editor" — an editor tab that represents data (the default).

These are placements of the one View, not three types. See the UI overview for the whole picture, and naming for how these words map to the View class.

Usage#

A sidebar view is just a View — a subclass with param() fields and @action methods. @tool.view decorates a factory (ctx) -> View that returns an instance; the desktop docks it:

Python
from lsdtools import Tool, View, action, param

tool = Tool("mine")

class MineControls(View):
    count = param(int, default=5, min=1, label="Count")

    @action(label="Run", style="suggested")
    def run(self):
        self.ctx.ui.notify(f"running with {self.count}")

@tool.view("mine.controls", title="Mine controls", location="sidebar")
def controls(ctx):
    return MineControls()
# → a "Mine controls" view docks in the sidebar

An action handler reads the fields as typed attributes (self.count) — there is no values dict to keep in sync. On mount the view's self.ctx is injected, so a handler can reach self.ctx.ui.notify(...), self.ctx.engine, and the rest of the Context without any wiring.

@tool.view(view_id, title=None, *, location="sidebar", icon=None) decorates a factory that returns a View. location places it ("sidebar", "editor", …). Because a panel is just a View, the same object renders in the desktop, prints in the console, and is driven by tests through view.invoke("run") — one dispatch entry for every front-end. See Params & actions for the full View authoring guide.

Overlay (a HUD)#

@tool.view(view_id, title=None, *, location="overlay", dock="top", view_kind="viewer3d", icon=None) is a HUD over a viewport. It returns a View too — typically an action-only one — docked at dock (top/bottom/left/right) over the viewport named by view_kind:

Python
class MineHud(View):
    @action(label="Fit view", style="suggested")
    def fit(self):
        self.ctx.ui.notify("fit view")

@tool.view("mine.hud", location="overlay", dock="top", view_kind="viewer3d")
def hud(ctx):
    return MineHud()

The desktop reads the dock / view_kind hints and mounts the view's actions as a toolbar over the viewer.

Editor (a data view)#

@tool.view(view_id, title=None, *, location="editor", icon=None, menu=None) contributes a data view: the decorated factory is (ctx, entity=None) -> View. A View with a kind is a representation of data — a table, chart, or flowchart from lsdtools.views, or a custom one:

Python
from lsdtools import Tool, views

tool = Tool("mine")

@tool.view("mine.report", title="Grade report", location="editor")
def report_view(ctx, entity=None):
    return views.chart(kind="histogram", title="Grades").watch(entity)   # follows the entity
# → an editor tab titled "Grade report" that refreshes when the entity re-runs

The factory returns the View; the front-end resolves its shell — the desktop draws the GTK control, a headless host uses to_text() / to_image(). Returning a raw GTK widget is a mount error: a package's only path to GTK is a custom view's shell.

Receiving an entity's output — watch#

A data-view factory is (ctx, entity=None) -> View: the two-argument signature is what marks it a per-entity view, and the desktop opens it for a matching entity (via opens_for=). The view gets that entity's data through one verbwatch(entity) — and refreshes on every re-run. You never hand-feed it; the framework wires the bus for you:

Python
@tool.view("mine.grades", title="Grades", location="editor")
def grades(ctx, entity=None):
    return views.table(title="Grades").watch(entity)   # holds the entity's output; auto-refreshes

watch returns the view, so views.table(...).watch(entity) reads as one phrase. Any order works — watching an entity that has already run pulls its output immediately. A deliver step that returns a ViewerPayload is routed to the same watching views. Pass opens_for=predicate to @tool.view (see the opens_for_type helper in lsdtools.advanced) to have the desktop open this view on demand for a matching entity; otherwise it is mounted once. A View (no entity) that wants live data can still self.ctx.engine.get("name") and watch it from an @action.

Custom views#

For a representation the built-in kinds can't express, subclass View with its own kind, feed(data), and to_text() / to_image(). Its GTK drawing shell registers through the one sanctioned seam — set_view_shell from lsdtools.views:

Python
from lsdtools.views import set_view_shell

set_view_shell("sparkline", build_sparkline_shell)   # build_sparkline_shell(view) -> a Gtk.Widget

The full walkthrough — the View subclass and its shell — is in Custom views.

LearnCommands & actions · UI overview · Naming: which is which

APITool · Context · Table

ExamplesA dockable panel · A complete package

Frequently asked questions

What do the different location= values mean?

location= only chooses where the one View lands. "sidebar" docks it as an input form in the shell (informally a panel); "overlay" floats it over a viewport as a HUD (with dock="top"/"bottom"/etc. over the viewport named by view_kind); "editor" (the default) is an editor tab. Same View class every time — there is no separate panel or overlay type.

When does a view show data instead of controls?

A View with no kind renders as a form — param() fields plus @action buttons. A View with a kind (table/chart/flowchart/custom) represents data via to_text()/to_image(). It is one class either way; the kind is what turns controls into a data representation, independent of location=.

By LSD Team · Last updated Jul 18, 2026 Ask on Discord View as Markdown