Common tasks
Start with vyuh_diagram_flutter in Flutter, or vyuh_diagram_core in a Dart service. Both expose the same verbs. Keep one controller for the editing lifetime; widgets observe it rather than owning a second document. New to the toolkit? Hello world shows the six core concepts in one program.
The snippets below assume the package's main library. The ones that declare shapes (ShapeDefinition, value fields) also need its extension_api.dart; see libraries.
| Task | Start here |
|---|---|
| Mount a drawing | DiagramController and DiagramCanvas(controller: controller) |
| Create content with registry defaults | createShape, createText |
| Insert an already-authored record | add |
| Connect existing objects | connect |
| Add a label | addConnectorLabel(text: ...) |
| Rename text | controller.setText |
| Preserve rich formatting while editing | controller.replaceText |
| Save content | controller.save(DiagramJsonCodec.new) |
| Observe edits for autosave | controller.diagram.changes |
| Open saved content | DiagramController.open(json, format: DiagramJsonCodec.new, configuration: DiagramConfiguration(extensions: [...])) with the original extensions installed |
| Discover what an element accepts | controller.verbs([id]), controller.canPerform |
| Undo or redo | undo().requireAllowed(), redo().requireAllowed() |
| Customize shapes and label grammar | A DiagramExtension with shapes, textTypes and connectors |
| Show the grid, the minimap or other extension options | controller.options<GridOptions>() and controller.edits.setOptions(...) |
| Customize canvas appearance and UI | Extensions in DiagramController(configuration: DiagramConfiguration(extensions: [...])) and DiagramCanvas(theme: ...) |
Create, connect and rename
dart
final source = controller.createShape(
Shapes.rectangle,
at: const DiagramPoint(100, 100),
text: 'Draft',
);
final target = controller.createText(at: const DiagramPoint(400, 100), text: 'Review');
final connector = controller.connect(source, target);
controller.edits.addConnectorLabel(connector, text: 'Ready\nfor review');
controller.setText(source, 'Proposal');setText preserves story identity and replaces formatting with ordinary paragraphs. Newline sequences become paragraph boundaries, including a final empty line. Text that looks like a list remains Plain Text. Rich Text replacement is available when you need to retain marks or list structure.
For a connector with several labels, pass the desired label's story.id as storyId. An ambiguous connector is denied rather than silently editing its first label. The same canonical admission checks enforce single-line regions, locks and the application's rules. Authored content that violates grammar is rejected; it is not silently truncated.
For a story with several text slots, also pass slot. The edit preserves role and slot bindings and leaves all other slots unchanged. Omitting the slot for an ambiguous story is denied.
Handle verb outcomes
Use this convention consistently in your host:
- Calls through
controller.editsreturn anEditResult; a creating verb reports the new identity inid. Inspect the result, or callrequireAllowed()to throwDiagramChangeDeniedon a denial. - Unknown verbs, missing or invalid arguments, unknown targets, locks and stale revisions are denials with a
DiagramDenialCode, not exceptions. - Selection, camera and extension options change through plain controller methods such as
select,setCameraandsetOptions; they are not verbs, are not undoable or admitted, and have their own preconditions. - Invalid authored data that is not a verb argument and invalid lifecycle calls can throw
ArgumentError,StateErroror format errors. Do not catch those as ordinary rule denials.
dart
try {
controller.setText(source, 'Approved');
} on DiagramChangeDenied catch (error) {
onDenied(error.result); // Your host's feedback callback.
// Use error.result.code to choose host messaging or localization.
// error.result.reason is optional human-readable detail.
}DiagramDenialCode provides stable categories. Rules that return EditResult.denied(reason: ...) have the policy code. Use EditResult.denied(code: code, reason: ...) to supply a category explicitly. Never parse the reason string. A successful verb is one undoable operation; several statements in a try block are not an atomic batch. Wrap them in controller.edits.batch(label, () {...}) when they must undo together.
One error-handling boundary
Direct creation calls return the new ID. Other direct edits complete or throw DiagramChangeDenied on refusal. Calls through controller.edits return an EditResult. For an application that handles denials with exceptions, call requireAllowed() on results and catch DiagramChangeDenied once at the initiating UI boundary. Read a created identity from id after checking the result. Your own verbs, and the key bindings and regions that perform them, use this same result contract, including typed denial codes.
For several changes that must undo together, use controller.batch, or declare a verb whose implementation performs them. Use setValues for declared data, resize or setElementBounds for dimensions and setStyle for appearance. For a single ordinary text edit, use controller.setText with a slot; manual text-story replacement is an advanced escape hatch.
Show the grid and other options
Each extension owns its options, such as the grid's GridOptions. Read the current value and replace it; the change reaches every controller of the diagram and is not an undo step:
dart
final grid = controller.options<GridOptions>();
controller.edits.setOptions(grid.copyWith(visible: true, snap: true, size: 24));Start with other values by installing the extension with them, for example DiagramController(configuration: DiagramConfiguration(extensions: [GridExtension(visible: true)])). See extensions and options.
Choose the correct owner
| Concern | Owner | Lifetime |
|---|---|---|
| Shapes, ports, text regions, label block grammar | Extension contributions, assembled into the ShapeRegistry | Establish when creating a controller; reuse for saved documents. |
| Creation and connection rules | DiagramController(configuration: DiagramConfiguration(rules: DiagramRules(canCreate: ..., canConnect: ...))) | Canonical admission for API and interactive edits. |
| Before-change approval and document validation | DiagramRules(beforeChange: ...) | Canonical admission, including loads and history. |
| Grid, navigation, editor, minimap and widget builders | Extensions in DiagramController(configuration: DiagramConfiguration(extensions: [...])), each with its own options | Read with options<T>(), change with setOptions; not undoable. |
| Font metrics | Text-layout engine | Choose before creating a controller that will be mounted. |
| Autosave | Document-change listener | Subscribe once, remove before host disposal. |
| Pointer preview and camera feedback | General/presentation observation | Do not treat transient previews as committed autosave data. |
Platform capabilities
| Capability | Portable controller | Flutter controller, unmounted | Flutter controller, mounted |
|---|---|---|---|
| Create/edit/history/JSON | Yes | Yes | Yes |
| Text metrics | Explicit backend; default fixed metrics | Flutter shaping | Flutter shaping |
| SVG | Prepared image/font resources | Flutter output adapter | Flutter output adapter |
| PNG | Requires raster output adapter | Supported by Flutter output | Supported by Flutter output |
| Navigate or read viewport | No mounted viewport | Requires attachment | Supported |
| Pointer, keyboard and IME | No | No | Supported |
Native shaping is available through the separate Pango library; native resources and fonts need their own setup and disposal. Fixed metrics are useful for deterministic headless work, but do not promise font-accurate output.
DiagramController(diagram: controller.diagram, participant: DiagramParticipant.agent(id: 'agent', name: 'Agent')) joins the same diagram as another actor with its own selection, camera and undo history. Configure Flutter text layout before mounting. Disposing the owning controller disposes the diagram and every controller joined to it.
Progress to custom definitions
Follow shapes, then connectors, text, embedded widgets and ownership. Add one concern at a time: outline, ports, text regions, declared values, widget slots, then custom tools. Keep content in declared values and stories; a widget builder is a projection and performs verbs through its bounded context. An enabled snapshot is presentation feedback, not permission to bypass live admission.
A small starting surface
For a new Flutter host, start with:
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';
This entry point exposes the everyday canvas, controller, verbs, document model, options, style and error contracts. Custom geometry, tools, verbs, effects, text grammars and output backends are extension authoring types in a second library; add it when the task calls for those capabilities:
import 'package:vyuh_diagram_flutter/extension_api.dart';
Both libraries re-export the same objects as vyuh_diagram_core, not another implementation.
Draw lines without coordinate bookkeeping
dart
final line = controller.createLine(
start: const DiagramPoint(40, 60),
end: const DiagramPoint(240, 160),
);
controller.edits.createPolyline(
points: const [
DiagramPoint(300, 60),
DiagramPoint(460, 60),
DiagramPoint(380, 180),
],
closed: true,
);
final corner = controller.addPathPoint(line, after: 'node-0');
controller.setPathHandleMode(line, corner, DiagramPathHandleMode.smooth);Points are in world coordinates. The controller computes the frame, normalizes nodes, applies parent placement and records one undo step. createPath remains available for authored Bézier nodes, tangent offsets and explicit frames. Creation and edits return an EditResult you can inspect, read the new identity from, or promote to an exception with requireAllowed().
Declare a custom shape and its inspector
A rectangle with editable text needs only ShapeDefinition(type: 'task'). Standard defaults supply its rectangle outline, padded body text part, size and behavior. Declare only the differences:
dart
final task = ShapeDefinition(
type: 'task',
name: 'Task',
values: {
'progress': const NumberValueField(
defaultValue: 0,
minimum: 0,
maximum: 100,
),
'status': TextValueField(
defaultValue: 'Draft',
allowedValues: ['Draft', 'Ready'],
),
},
);
final taskController = DiagramController(
configuration: DiagramConfiguration(
extensions: [commonTasksExtension(task)],
),
);
taskController.createShape(
'task',
at: const DiagramPoint(100, 100),
text: 'Review proposal',
);Declare the extension at the top level of your Dart file:
dart
DiagramExtension commonTasksExtension(ShapeDefinition task) => DiagramExtension(
id: 'app.tasks',
shapes: [task],
);The inspector's standard editors follow from the shape's anatomy: fill and stroke from its outline, text from its text parts and one control per value. definition.propertyEditors lists them in order; add your own with properties:. Visibility does not change edit permissions. Path and gradient controls use the same declarations.