Panels
Dockable panels drawn in Pixi with no DOM: a virtualised layer tree, a schema-driven property inspector with undo, and a minimap of the camera view.
Updated pixi-silk 1.0.1View as Markdown
The panels around an editor canvas, drawn as Pixi containers. They take the same pointer events as the scene, lay out with the flex layout and draw text with SilkText. You keep one event system and one style instead of DOM over a canvas.
| Entry point | What it holds |
|---|---|
pixi-silk/ui/panel | Panel, ScrollView, the themes, dockRect and panelHitTest |
pixi-silk/ui/layers | LayerTree, pixiTreeAdapter and the tree functions |
pixi-silk/ui/inspector | Inspector, its fields, bindObjects and bindSignals |
pixi-silk/ui/minimap | Minimap and its mapping functions |
Dock a panel#
import type { Container } from 'pixi.js';
import type { Camera } from 'pixi-silk/ui/camera';
import { Panel } from 'pixi-silk/ui/panel';
declare const camera: Camera, stage: Container, rows: Container[];
const panel = new Panel({ title: 'Library', dock: 'left', width: 260, viewport: camera.state });
for (const row of rows) panel.content.add(row);
stage.addChild(panel);
A panel has a title bar, a body and edges to resize. While a dragged panel nears a screen edge or
corner, a preview shows where it docks. Left and right panels span the screen height, top and
bottom ones its width. viewport is the screen size, as numbers or signals. The camera’s state
works.
The body, panel.content, is a flex column. It scrolls when it is taller than the panel, with a
scrollbar that fades in while it moves. A widget that scrolls itself, like the layer tree, wants
scroll: false, and the body is then a flex container as tall as the panel.
| Option | Default | What it does |
|---|---|---|
dock | floating | An edge or a corner, such as 'left' or 'top-right'. Needs viewport. |
width, height | 260, 360 | Size when floating. A docked panel spans its edge. |
margin | 8 | Gap to the screen edge when docked. |
insets | none | Space kept free along each edge, for a toolbar. |
collapsible, collapsed | true, false | The chevron folds the body away. |
draggable, resizable, dockable | true | Move, resize and dock by dragging. |
scroll | true | Scroll content, or fill the body with it. |
theme | dark | PANEL_THEME_LIGHT, or tokens over the dark theme. |
panel.state.collapsed and panel.state.dock are signals. Write them to fold or dock from code.
panel.actions is a row in the title bar for your own buttons.
The camera listens to the wheel on the whole canvas. Pass
ignore: panelHitTest(panels) to it, and wheels and pan drags over a panel stay with the panel.
List layers#
import type { Container } from 'pixi.js';
import { History } from 'pixi-silk/history';
import { Selection } from 'pixi-silk/selection';
import { LayerTree, pixiTreeAdapter } from 'pixi-silk/ui/layers';
import type { Panel } from 'pixi-silk/ui/panel';
declare const world: Container, panel: Panel;
const selection = new Selection<Container>();
const tree = new LayerTree({ adapter: pixiTreeAdapter(), root: world, selection, history: new History() });
panel.content.add(tree, { grow: 1, onSize: (w, h) => tree.resize(w, h) });
LayerTree reads your data through a TreeAdapter and keeps no copy of it, so call refresh()
after the data changes elsewhere. Only the rows in view exist, so two thousand layers scroll like
twenty. Rows are listed front to back, so the adapter’s first child is the top row.
The rows follow a Selection both ways. A pick on the canvas opens
its groups and scrolls the row into view. Hovering a row sets selection.state.hovered, so the
canvas can outline it. Drag rows between others to reorder them, or onto a group to move them
inside. On touch, hold a row to pick it up. Every rename, toggle and move is one step in history.
pixiTreeAdapter() maps containers to rows. The name is label, the eye sets visible, and the
lock turns pointer events off. For your own model, write the adapter:
import { LayerTree, type TreeAdapter } from 'pixi-silk/ui/layers';
interface Item {
name: string;
children: Item[];
parent: Item | null;
}
declare const doc: Item;
const adapter: TreeAdapter<Item> = {
getChildren: (n) => n.children,
getName: (n) => n.name,
getParent: (n) => n.parent,
setName: (n, name) => (n.name = name),
move: (n, parent, index) => {
n.parent?.children.splice(n.parent.children.indexOf(n), 1);
parent.children.splice(index, 0, n);
n.parent = parent;
},
};
const tree = new LayerTree({ adapter, root: doc });
The tree logic has no Pixi in it and is exported on its own: flattenTree, visibleRange,
dropTarget, applyDrop and undoMoves.
Edit properties#
import type { Container } from 'pixi.js';
import { History } from 'pixi-silk/history';
import type { Selection } from 'pixi-silk/selection';
import { Inspector } from 'pixi-silk/ui/inspector';
import type { Panel } from 'pixi-silk/ui/panel';
declare const selection: Selection<Container>, panel: Panel;
const inspector = new Inspector({
schema: [
{ type: 'text', key: 'label', label: 'Name' },
{ type: 'vector', keys: ['position.x', 'position.y'], label: 'Position' },
{ type: 'number', key: 'angle', label: 'Rotation', unit: 'deg' },
{
type: 'section',
label: 'Layer',
fields: [
{ type: 'number', key: 'alpha', label: 'Opacity', min: 0, max: 1, step: 0.01 },
{ type: 'toggle', key: 'visible', label: 'Visible' },
{ type: 'color', key: 'tint', label: 'Tint' },
],
},
],
history: new History(),
});
selection.state.items.subscribe((items) => inspector.bind(items));
panel.content.add(inspector);
Each schema entry names a path on the edited objects, such as position.x, and a field type.
| Type | Field |
|---|---|
number | Drag sideways to scrub, Shift for faster, Alt for finer. Click to type. Takes min, max, step and unit. |
text | A one-line text editor. |
toggle | A switch. |
choice | Segmented options. |
color | A swatch and its hex code. A click opens a colour picker. |
vector | Two numbers in one row. lockAspect keeps their ratio. |
section | A titled group that folds. |
With several objects bound, values that differ show as Mixed. A scrub moves each value from its
own start, and a typed value sets them all. Every edit is one step in history.
bind() also takes an InspectorBinding with read and write, for a store of your own.
bindSignals() edits signals, for a settings panel. The inspector does not watch the objects, so
call refresh() when they change elsewhere, such as during a gizmo drag.
Show a minimap#
import type { Camera } from 'pixi-silk/ui/camera';
import { Minimap } from 'pixi-silk/ui/minimap';
declare const camera: Camera;
const map = new Minimap({ camera, width: 220, height: 140 });
app.stage.addChild(map);
The minimap frames the content and the view together. The default mode: 'bounds' draws the
outline of each top-level object. mode: 'texture' renders a real thumbnail every interval
seconds (0.5 by default) and needs the renderer. Call refresh() after the content moves.