Skip to content

DiagramCanvas ​

Mount a controller inside a bounded Flutter layout. The canvas owns no second document or configuration.

For content inside an individual shape, see the visual shape anatomy guide.

dart
Expanded(child: DiagramCanvas(controller: controller))

DiagramCanvas({required controller, DiagramTheme? theme}) takes only the controller and an optional theme. It uses the Flutter extensions installed on the controller. A canvas edits when an editable extension, such as EditorExtension(), is installed; otherwise it navigates and selects only. Host code can always use admitted controller verbs to update the document.

Compose an editor ​

The optional editor package supplies EditorExtension(), the editable Flutter extension that makes the canvas edit and wraps it in the editing shell: a tool palette on the left, an inspector panel with any shape libraries, a toolbar below the canvas and the on-canvas affordances shapes declare.

dart
import 'package:vyuh_diagram_extension_editor/vyuh_diagram_extension_editor.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';

final controller = DiagramController(
  configuration: DiagramConfiguration(extensions: [EditorExtension()]),
);

DiagramCanvas(controller: controller);
ArgumentDefaultMeaning
toolsevery installed toolThe tools offered, in order. The default tool is select.
palette, toolbartrueTurn the tool palette or the history toolbar off; the shell follows EditorOptions at runtime.
inspectorInspectorPlacement.dockedWhere the inspector panel shows: docked beside the canvas on wide layouts, which narrows the canvas; floating as a card over the canvas on any layout; none to hide it.
inspectorSideInspectorSide.rightThe side of the canvas the inspector panel takes.
inspectorWidth280The panel's width, from 240 to 520. Users resize it by dragging its inner edge, the hairline facing the canvas when docked, and double-click that edge to restore 280.
librariesnoneShapeLibrary tabs above the inspector: click a shape to add it at the center of the view, or drag it onto the canvas.
containerthe default shell(context, controller, canvas) builder that replaces the whole shell.
rotationSnap, connectionSnapRadius15, 10Editing snaps; zero radius disables connection magnets.
themelight or dark by DiagramTheme.brightnessEditorTheme colors of the toolbars and the inspector panel; see theming.

Every argument except tools and container builds the editor's EditorOptions, changeable at runtime with setOptions; see extensions and options. The panel's header has the same switch: its button shows whether the panel is docked or floating and toggles it.

dart
controller.edits.setOptions(
  controller.options<EditorOptions>().copyWith(
    inspector: InspectorPlacement.floating,
  ),
);

An editable canvas without chrome turns every region off:

dart
final controller = DiagramController(
  configuration: DiagramConfiguration(
    extensions: [
      EditorExtension(
        tools: [DiagramTools.select, DiagramTools.rectangle],
        palette: false,
        inspector: InspectorPlacement.none,
        toolbar: false,
      ),
    ],
  ),
);

To compose your own layout, pass container: (context, controller, canvas). Returning canvas keeps editing without surrounding chrome. Tool palettes and toolbars accept direction: Axis.vertical or Axis.horizontal; their position is determined by your container. For example, use a Row containing a vertical toolbar, an expanded canvas, and an inspector, or rearrange those widgets to choose another side. DiagramToolPalette, DiagramToolbar, DiagramShapeLibraries and DiagramInspector can also be composed individually around that same canvas and controller; each takes its colors from the editor's EditorTheme.

DiagramFloatingToolbar tool buttons expose their name and shortcut as one accessible button. Shift shortcuts display a Shift icon while their accessible name spells out the modifier, for example Table — Shift+T.

Flutter extensions may contribute a container around the supplied canvas; containers wrap deterministically in installation order. Do not create a second canvas or controller inside the container. Try the composition choices in the Playground.

The optional property inspector, DiagramInspector(controller: controller), uses the same registry and options. A controller supports one mounted canvas at a time; unmount before controller.dispose(). To show the same diagram in a second canvas, give it its own controller on that diagram; see collaboration.

Unmounting cancels an unfinished canvas gesture and restores its content baseline without adding an undo entry. Late pointer events cannot change a subsequent edit. This applies to registered custom tools as well as the default tools.

The canvas clips scene paint and its local overlays to the allocated viewport. Shapes, text, connector markers and selection chrome cannot paint over adjacent host widgets. This screen-space clip does not alter document geometry or export bounds; frame/shape content clipping remains a separate world-space rule.

Set the canvas background with DiagramTheme(canvas: CanvasTheme(background: color)).

On this page ​

CategoryWhat you can do
Canvas boundsChoose an infinite canvas or one that fits the content.
Embedded controlsMount interactive content inside elements.
AffordancesDeclare buttons beside selected shapes that perform verbs.
Key bindingsBind key chords to verbs.
Shape regionsPerform a shape's verb from a tapped region.
AccessibilityExpose content and actions to assistive tools.
Touch navigationConfigure touch input behavior.

View-only diagrams ​

A controller without an editable extension gets a lightweight selectable view with pan, scroll and zoom. It mounts shared scene painting without editor gesture controllers, text editing or IME.

dart
final controller = DiagramController(document: document);
DiagramCanvas(controller: controller);

The diagram remains the document authority: host code can still update content. A view-only canvas restricts edits through user interaction, not programmatic access. Installed native widgets render read-only; without their Flutter builders, shapes use portable painted fallbacks. Leaf Flutter extensions such as minimaps can render and use the shared demand-driven frame clock. Animated path effects use the edit surface; the view surface paints canonical static path geometry. See the view-only example.

Canvas bounds ​

Bounds are core configuration, shared by every controller on the diagram. The canvas is infinite unless DiagramConfiguration(bounds: ...) says otherwise.

APIContract
CanvasBounds.infiniteUnrestricted camera positioning within the configured zoom range. The default.
CanvasBounds.fit({double padding = 0})The canvas is the content extent plus padding viewport (screen) pixels on every side, so the room looks the same at every zoom. It grows and shrinks with the content; an empty document gets a viewport-sized canvas around the origin.
controller.boundsThe current bounds. Observable.
controller.edits.setBounds(bounds)Replaces the bounds for every controller of the diagram and re-clamps every camera. Not undoable. Throws ArgumentError for a padding that is negative or not finite.

CanvasBounds is sealed: switch over InfiniteCanvasBounds() and FitCanvasBounds(:final padding) to tell them apart.

dart
final controller = DiagramController(
  configuration: DiagramConfiguration(bounds: CanvasBounds.fit(padding: 64)),
);
controller.edits.setBounds(CanvasBounds.infinite);

final label = switch (controller.bounds) {
  InfiniteCanvasBounds() => 'Infinite',
  FitCanvasBounds(:final padding) => 'Fit, $padding px',
};

Bounds changes do not rewrite document coordinates or enter undo history. The inspector's Finite canvas toggle switches between CanvasBounds.infinite and CanvasBounds.fit(padding: 48). Try the bounds example.

Arbitrary Flutter widgets inside a node ​

Use declared widget slots for sliders, dropdowns, buttons and text fields inside shapes. The slot is part of the shape definition; Flutter builders are registered once through DiagramFlutterExtension(widgets: {...}) installed in the configuration.

ObjectResponsibility
ShapeWidgetA body part: the slot's place in the shape, builder key and portable fallback part.
DiagramFlutterExtension.widgetsMaps builder keys to Flutter controls.
ShapeElement.valuesCanonical, validated control values.
ShapeWidgetContextReads values and performs setValues through admission and history.

The canvas applies owner transforms, clipping, visibility and paint ordering to native slots. Controls own interactive gestures; non-interactive slot space participates in shape selection and movement. Host overlays remain useful for canvas chrome, but are not the embedding mechanism for node content.

See Widgets inside shapes for declarations, builder lifetime, value-edit gestures, focus ownership and export fallbacks. SVG uses the declared fallback rather than serializing an arbitrary Flutter subtree.

Very dense scenes with hundreds of native widgets can be slow to draw when zoomed out to an overview; prefer declared fallbacks or adaptive visual detail for those views.

Shape affordances ​

A shape declares the on-canvas controls it offers beside its single selection in ShapeDefinition.affordances. Each performs a verb as one undoable edit and shows only while that verb is available:

AffordancePerforms
ShapeValueToggleAffordance(verb:, argument:, valueKey:, label:)The boolean setter verb with argument set to the opposite of the value valueKey, such as showSecondarySection with shown.
ShapeOptionalSlotAffordance(slotId:, anchorSlotId:, label:, initialText:, enabledValueKey:)The pair addSlot / removeSlot for the optional text slot slotId, placed beside anchorSlotId.
ShapeAspectLockAffordance(lockLabel:, round:)setAspectLocked; locking squares the frame first. Drawn as a circle handle on the selection's top edge, left of the top-right resize handle: filled while locked, an outline while free.

The core card declares its tag (diagramTagAffordance), rectangles and ellipses their aspect lock, and the shapes package's entity its tag and secondary section. EditorExtension() draws whatever the selected shape declares, without naming slots, values or shape types.

dart
import 'package:vyuh_diagram_flutter/extension_api.dart';

final entity = ShapeDefinition(
  type: 'app.entity',
  verbs: [showSecondarySection], // A DiagramVerb that sets 'secondaryEnabled'.
  affordances: const [
    ShapeValueToggleAffordance(
      verb: 'showSecondarySection',
      argument: 'shown',
      valueKey: 'secondaryEnabled',
      label: 'secondary section',
    ),
    ShapeAspectLockAffordance(),
  ],
);

Key bindings ​

A DiagramKeyBinding (from package:vyuh_diagram_flutter/extension_api.dart) binds one exact key chord to an installed verb. Register bindings through DiagramFlutterExtension(shortcuts: [...]); assembly rejects a binding whose verb is not installed. A matched binding performs the verb on the selected element, or on the diagram in canvas scope, with the arguments args computes. A setter bound to a key passes the value it sets.

The playground's controls card declares a setCount setter (apps/demo/lib/playground/widget_card.dart), and its key binding passes the next count (apps/demo/lib/playground/actions.dart):

dart
import 'package:vyuh_diagram_flutter/extension_api.dart';

DiagramKeyBinding(
  verb: 'setCount',
  args: (controller, elementId) => {
    'count': playgroundCount(controller, elementId) + 1,
  },
  key: LogicalKeyboardKey.equal,
  scope: DiagramKeyScope.selection,
  shapeType: playgroundControlsType,
  modifiers: const {DiagramKeyModifier.command, DiagramKeyModifier.shift},
)
ArgumentMeaning
verb, argsThe verb to perform, and a function of (controller, elementId) returning its arguments.
key, modifiersThe chord; DiagramKeyModifier.command is Meta on macOS and Control elsewhere. Repeats are ignored.
scopeDiagramKeyScope.canvas, .selection or .text; text never falls through to canvas or selection bindings.
shapeType, textType, slotIdRestrict the binding to a shape type, the active text slot's text type (text scope only) or a slot.

Shape regions ​

A ShapeSurface in a shape's body can name an interaction with onInteract. When it names a verb of the shape, a tap that ends inside the region performs that verb with no arguments. The standard image shape's choose-image region is not a verb: the canvas opens the image source picker for an image without a source. Choose the picker with ImagesExtension(picker: ...).

Accessibility ​

When Flutter semantics are enabled, visible canvas objects expose their type and available text and lock state. Selectable objects also expose selection state and an activation action through the same controller.setSelection selection path as pointer selection. Setting selectable: false keeps the content readable without a selection action or selection hint; it does not hide the content from assistive technology.

Semantic bounds follow the camera and intersect the resolved ancestor clip paths. Offscreen and fully clipped objects and connector labels are omitted. Removing a clip makes the content available again, including through undo and redo. Partially clipped content uses the bounding rectangle of the remaining region, since semantic bounds are rectangular.

Set ShapeDefinition.name to give custom shapes a readable kind name in both the inspector and screen-reader labels. The host can localize that name when constructing its registry. The canonical type ID remains unchanged.

While editing text, the shared paragraph component exposes the active block as a text field, including its current value and selection. Accessibility text and selection changes use the same canonical editing pipeline as the IME and remain subject to change admission and undo/redo.

The mounted text input follows the canonical story selection. Switching to a different story switches the input target; an object selection exits text mode. Undo that restores an editable story selection restores text input, and remounting the canvas retains that selection. If the target becomes non-editable, selection falls back to the object when selectable, or clears otherwise.

This is initial support, not a complete screen-reader editing contract. Traversal between offscreen objects and paragraph/list blocks, custom localized object descriptions, richer editing actions, and assistive-technology compatibility remain under development.

Touch navigation ​

Two fingers pan and zoom the viewport. The second contact cancels any active object preview, exits text editing, and transfers ownership to camera navigation. A combined sample publishes one camera state, respects zoom limits and finite-canvas bounds, and does not add an undo entry. After one finger lifts, the remaining contact cannot become an object drag; release every contact before starting another single-finger interaction.

A third contact does not change the active pair until one of that pair ends. Contact cancellation releases ownership just like lifting a finger.