# Contributing UI with @tool.view
URL: https://lsd.tools/docs/tools-panels-overlays-views
Updated: 2026-07-18

> A 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`](/docs/ui-overview). `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](/docs/ui-overview)
for the whole picture, and [naming](/docs/tools-glossary) 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`](/docs/api-context)
without any wiring.

## Sidebar (params + actions)

`@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](/docs/ui-panels) 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`](/docs/ui-widgets) 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 verb** — [`watch(entity)`](/docs/api-view-watch) — 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`](/docs/tools-deliver) 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`](/docs/ui-widgets) 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](/docs/ui-custom-views).

## Related

**Learn** — [Commands & actions](/docs/tools-commands-actions) · [UI overview](/docs/ui-overview) · [Naming: which is which](/docs/tools-glossary)

**API** — [Tool](/docs/api-tool) · [Context](/docs/api-context) · [Table](/docs/api-table)

**Examples** — [A dockable panel](/docs/ex-panel) · [A complete package](/docs/ex-full-package)

## FAQ
### 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=.
