Skip to content
pixi-silk
GitHub repository
Editor kit

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

Pick a layer in the list or on the canvas. Drag a panel by its title to float or dock it.

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 pointWhat it holds
pixi-silk/ui/panelPanel, ScrollView, the themes, dockRect and panelHitTest
pixi-silk/ui/layersLayerTree, pixiTreeAdapter and the tree functions
pixi-silk/ui/inspectorInspector, its fields, bindObjects and bindSignals
pixi-silk/ui/minimapMinimap and its mapping functions

Dock a panel#

Drag a title bar near an edge to dock. Drag an edge to resize.
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.

OptionDefaultWhat it does
dockfloatingAn edge or a corner, such as 'left' or 'top-right'. Needs viewport.
width, height260, 360Size when floating. A docked panel spans its edge.
margin8Gap to the screen edge when docked.
insetsnoneSpace kept free along each edge, for a toolbar.
collapsible, collapsedtrue, falseThe chevron folds the body away.
draggable, resizable, dockabletrueMove, resize and dock by dragging.
scrolltrueScroll content, or fill the body with it.
themedarkPANEL_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#

Drag rows to reorder them or drop them on a group. Double click a name to rename it.Open in Studio
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#

Shift + click a second card. Drag a number sideways or click it to type.Open in Studio
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.

TypeField
numberDrag sideways to scrub, Shift for faster, Alt for finer. Click to type. Takes min, max, step and unit.
textA one-line text editor.
toggleA switch.
choiceSegmented options.
colorA swatch and its hex code. A click opens a colour picker.
vectorTwo numbers in one row. lockAspect keeps their ratio.
sectionA 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#

Drag the frame on the minimap to pan, click it to jump, scroll over it to zoom.
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.

API reference: Panel, LayerTree, Inspector, Minimap