# Everything you show is a View
URL: https://lsd.tools/docs/ui-overview
Updated: 2026-07-18

> LSD has two kinds of thing: entities you run and views you show. The show side is one declarative class, View. With a kind it represents data (a table, chart, or flowchart) via to_text()/to_image(); with no kind it collects input — param() fields plus @action buttons (a sidebar form, informally a "panel"). There is no separate Panel type. show() presents either — a window, or console text when headless — and none import GTK. The inheritance chain is Configurable → View.

LSD splits everything an `Engine` holds into two kinds of thing: **entities you run** (pipelines)
and **views you show** (UI). This page is the show side. See
[How an LSD package fits together](/docs/start-architecture) for the whole picture and
[Naming: which is which](/docs/tools-glossary) for the exact words.

The show side is **one declarative class, `View`**. A View does one of two things — it **represents
data** (a table, chart, or flowchart) or **collects input** (params + actions) — and you **present**
it with `show()`. There is no separate "panel" type; the difference is simply whether the View has a
`kind`. None of it imports GTK — the desktop registers a renderer and LSD stays front-end-agnostic,
so the same View works in a window or as console text.

## One View, two shapes

| The View | Is | You author it with | Headless rendering |
|---|---|---|---|
| **Represents data** | a `View` with a `kind` — a table, chart, flowchart, or custom | `@tool.view` → a `View` | `to_text()` / `to_image()` |
| **Collects input** | a `View` with no `kind` — `param()` fields + `@action` (the base View; informally a *panel* when docked in the sidebar) | `@tool.view` → a `View` | `to_text()` (params + actions) |

Where a View lands is set by `location=` (`editor` / `sidebar` / `overlay`), and any View is
**presented** with `a_view.show()` or `engine.add(a_view)` + `engine.show()`.

## Data view — represent data

A **data view** is a `View` with a `kind`. It takes data through `feed(data)` and renders it. The
three built-in kinds come from the [`views`](/docs/ui-widgets) façade — `views.table`, `views.chart`,
`views.flowchart` — and you can subclass `View` for a [custom one](/docs/ui-custom-views). Every data
view answers `to_text()` and `to_image()`, so an agent or the CLI sees it with no display:

```python
from lsdtools import views

w = views.chart(kind="line", title="Loss")
w.feed({"series": [{"name": "train", "x": [0, 1, 2, 3], "y": [0.9, 0.6, 0.4, 0.25]}]})
print(w.to_text())
# → [line] Loss
#     train: 4 pts, min=0.25 max=0.9 last=0.25
```

`feed(data)` is for literal data. To make a data view **follow a pipeline**, hand it an entity with
one verb — [`watch`](/docs/api-view-watch) — instead of feeding a one-shot snapshot:

```python
grid = views.table(title="Grades").watch(entity)   # holds the entity's output; refreshes on re-run
```

`watch` reads the entity's live output and refreshes on every re-run (`views.table(entity.output)`
would read once and never update). `watch` returns the view, so it chains as one phrase, and the
framework wires the event bus for you. It is the same verb a [deliver step's
`ViewerPayload`](/docs/tools-deliver) is routed through.

## Collect input — a View with no kind

A `View` with no `kind` is `param()` fields plus `@action` buttons — the base View, for collecting
input and running actions. Where it shows is `location=` (the desktop docks a `sidebar` one at the
side — what people informally call a *panel* — but that's just placement). An action handler reads the
fields as typed attributes; there is no values dict to keep in sync. See
[A View of params and actions](/docs/ui-panels) for the full story.

```python
from lsdtools import View, action, param

class ClipPanel(View):
    cutoff = param(float, default=0.5, min=0.0, max=1.0, label="Cut-off")

    @action(label="Apply", style="suggested")
    def apply(self):
        return f"keeping grades ≥ {self.cutoff}"

ClipPanel(cutoff=0.7).invoke("apply")   # → 'keeping grades ≥ 0.7'
```

## Presenting a View

Every `View` has the base verb `show()`: a window when a front-end is loaded, a console rendering
otherwise. Attaching a view to the engine mirrors attaching an entity — `engine.add(a_view)` then
`engine.show()` is the exact partner of `engine.add(entity)` then `engine.run()`:

```python
from lsdtools import Engine, views, Table

t = Table({"name": ["ann", "bob"], "score": [12, 30]})

engine = Engine()
engine.add(views.table(t, title="Grades"))
engine.show()          # a window, or console text when headless
```

`a_view.show(mode="text")` always prints the text rendering, which is what makes a view testable
without a display.

## The inheritance chain

One short chain underlies it all, which is why `param()` behaves the same everywhere:

- **`Configurable`** — anything with `param()` fields (steps, entities, the engine).
- **`View`** — a `Configurable` that also has `@action` buttons and optional data (`feed` +
  `to_text` / `to_image`). With a `kind` it represents data; with none it is an input form.

Steps are `Configurable`s, **not** `View`s — they have parameters but no actions. See
[Naming: which is which](/docs/tools-glossary) for the overloaded words (View the class vs
`@tool.view` the decorator, `views.table` vs the `Table` class).

## Related

**Learn** — [Params & actions](/docs/ui-panels) · [Data views](/docs/ui-widgets) · [Custom views](/docs/ui-custom-views)

**Start** — [How an LSD package fits together](/docs/start-architecture) · [Naming: which is which](/docs/tools-glossary)

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

## FAQ
### Are panels and views two different things?

No — there is one View class. A View with a kind (table/chart/flowchart/custom) represents data via feed() + to_text()/to_image(); a View with no kind collects input — param() fields plus @action buttons. "Panel" is just informal shorthand for that no-kind View when it is docked in the sidebar; there is no separate Panel class or @tool.panel decorator. show() presents either; neither imports GTK.

### Do I need a display to build or test a surface?

No. A View introspects headless (get_parameters / get_actions) and, when it has a kind, renders with to_text() / to_image(). View.show(mode="text") prints the console rendering, so the same code runs in the CLI, in CI, or from an agent. GTK only enters when the desktop registers the renderer.
