Save Layouts, History, And Clipboard

Copy Markdown View Source

LivePanels keeps recoverable canvas behavior on the same state model as normal item interaction.

That means moving an item, reconnecting graph connectors, committing a vector item, copying a selection, undoing a change, and restoring a layout all operate on canvas state instead of separate app-maintained models.

What A Layout Contains

A saved layout is a versioned runtime payload.

It can include:

  • item ids, types, placement, z-order, visibility, parent/attachment fields;
  • persistent item props and allowed item options;
  • capability snapshots needed to restore runtime behavior;
  • vector state for committed vector items;
  • graph connectors between persistent items;
  • a schema header describing the layout format.

It does not include every display detail. Browser-only previews, temporary loading state, open menus, unsaved form fields, and product records remain outside the layout payload.

Store your domain data in your app. Store recoverable canvas arrangement in the LivePanels layout.

Configure Layout Persistence

The default LivePanels.Canvas.LayoutStore.Registry store is ETS-backed. It is useful for development and single-node experiments, but it is not durable across server restarts.

For durable persistence in an Ecto-backed Phoenix app, define a layout store with LivePanels.Canvas.LayoutStore.Ecto:

defmodule MyApp.CanvasLayouts do
  use LivePanels.Canvas.LayoutStore.Ecto,
    repo: MyApp.Repo,
    schema: MyApp.LivePanelsCanvasLayout
end

Wire it into the canvas:

use LivePanels.Canvas,
  pubsub: MyApp.PubSub,
  session: [layout_store: MyApp.CanvasLayouts]

load/1 runs during mount before content-bearing items render. save/2 runs after accepted mutations that affect recoverable layout state.

Keep both callbacks synchronous and fast. A failed save should return {:error, reason} rather than crashing the canvas. Implement LivePanels.Canvas.LayoutStore directly when the application uses a non-Ecto database or external cache.

Save Accepted State, Not Pointer Previews

Persistence follows accepted commands.

During a drag or resize, the browser may preview many frames. The layout store should not receive every pointer frame. It receives the accepted item placement after the command path validates and applies the change.

The same rule applies to drawing and graph work:

  • a drawing draft stays in the browser until it is committed as a vector item;
  • a graph connection draft stays transient until a valid connector is created;
  • undo and redo work against accepted state changes;
  • shared mode broadcasts accepted state, not private browser previews.

Use History

Render history controls with toolkit attrs:

<button {undo_attrs()}>Undo</button>
<button {redo_attrs()}>Redo</button>

Or call the owner module:

socket = LivePanels.Canvas.History.undo!(socket)
socket = LivePanels.Canvas.History.redo!(socket)

History is actor-scoped. In a shared canvas, one participant's undo stack does not have to be another participant's undo stack.

Applications can inspect history projection to enable controls:

history = LivePanels.Canvas.Read.history(socket)

can_undo? = LivePanels.Canvas.History.can_undo?(history)
can_redo? = LivePanels.Canvas.History.can_redo?(history)

Transactions create one logical history entry for multi-step work.

Use Clipboard

Render clipboard controls:

<button {copy_selection_attrs()}>Copy</button>
<button {paste_selection_attrs()}>Paste</button>

Or call the owner module:

socket = LivePanels.Canvas.Clipboard.copy!(socket)
socket = LivePanels.Canvas.Clipboard.paste!(socket, offset: %{x: 32, y: 32})

Clipboard is actor-scoped. It copies selected canvas items and graph connectors whose endpoints are inside the copied item set. Vector items are items, so they can participate in copy and paste like other persistent items.

Use the read side to drive UI:

clipboard = LivePanels.Canvas.Read.clipboard(socket)
has_clipboard? = not LivePanels.Canvas.Clipboard.empty?(clipboard)

Persistent And Ephemeral Items

Some items should restore with the canvas. Others are temporary runtime chrome.

Use persistent items for content, graph nodes, vector items, and user-created workspace objects.

Use ephemeral or viewport items for local HUDs, transient overlays, menus, and other UI that should be rebuilt by the application instead of restored from layout.

The practical question is: if the browser reloads or a shared room recovers, should this thing come back as part of the canvas? If yes, make it persistent canvas state. If no, keep it as application UI or ephemeral runtime state.

Shared Canvases

Shared canvases use the same layout payload, but the coordination path changes.

Configure durable layout persistence in the shared backend or canvas config so the shared coordinator can recover room state. Do not maintain separate per-browser layout stores for the same shared canvas id.

The shared-canvas guide covers setup. The shared operations guide covers production backend and recovery concerns.

What LivePanels Owns

LivePanels owns the versioned layout payload, accepted mutation history, actor-scoped clipboard projection, and restore timing.

Your application owns the durable store configuration, product records referenced by item props, and the visible controls for save, restore, undo, redo, copy, and paste.