Extensions and options
A diagram is configured in one place: its DiagramConfiguration, passed to the controller. Everything the diagram can do, from shapes and tools to the editor and the minimap, is an extension in its one extensions list, and each extension brings its own configuration. Beside the extensions it holds what the diagram as a whole is and allows: the canvas bounds, the fonts, the rules and the license. The canvas takes the controller and, optionally, a theme.
dart
final controller = DiagramController(
configuration: DiagramConfiguration(
extensions: [
EditorExtension(),
MinimapExtension(),
GridExtension(visible: true, snap: true),
],
),
);
DiagramCanvas(controller: controller, theme: DiagramTheme.dark);Configuration
dart
const DiagramConfiguration({
List<DiagramExtension> extensions = const [],
CanvasBounds bounds = CanvasBounds.infinite,
DiagramFontChoices fonts = const DiagramFontChoices.empty(),
String? defaultFontFamily,
DiagramRules rules = const DiagramRules(),
String? license,
})| Parameter | Default | Purpose |
|---|---|---|
extensions | none | Every extension the diagram installs beside the core ones, portable and Flutter alike: shapes, text, connectors, verbs, tools, the editor, overlays and their options. |
bounds | CanvasBounds.infinite | How far the canvas extends; see bounds. |
fonts | no choices | The font families the property panel offers; see fonts. |
defaultFontFamily | the text layout's default | The family text uses unless a run names its own. |
rules | admit everything | DiagramRules: which changes, creations and connections the application admits; see rules. |
license | none | A license token; it removes the canvas attribution. See license. |
A configuration is immutable and shared per document. The shared Diagram holds the document, its configuration, one undo history and the participants; each DiagramController is one actor on it, with its own selection, tool and camera. controller.configuration reads the current configuration and is observable. The runtime setters replace it with a changed copy for every controller of the diagram, without replacing content or undo history, and none of them is undoable:
| Change | Call |
|---|---|
| Canvas bounds | controller.edits.setBounds(bounds) |
| Font choices | controller.edits.setFonts(fonts) |
| Default font family | controller.edits.setDefaultFontFamily(family) |
| Rules | controller.diagram.setRules(rules) |
The installed extensions and the license never change: the extensions establish the document's grammar, so to change them, create a new controller. copyWith(bounds:, fonts:, defaultFontFamily:, rules:) builds a changed copy for a new diagram.
Controller
dart
DiagramController({
DiagramConfiguration? configuration,
DiagramDocument? document,
Diagram? diagram,
DiagramParticipant participant = DiagramParticipant.local,
})| Parameter | Purpose |
|---|---|
configuration | The new diagram's DiagramConfiguration; defaults to an empty one. |
document | Starting content. |
diagram | Join an existing shared Diagram instead of creating one; do not pass configuration or document with it. See collaboration. |
participant | Who this actor is, a DiagramParticipant; defaults to the local user. |
DiagramController.open(data, format: DiagramJsonCodec.new, configuration: DiagramConfiguration(extensions: [...])) opens saved content as a new controller. Diagram(document: ..., configuration: ...) creates a diagram that several controllers share.
controller.diagram.extensions lists the installed extensions and controller.diagram.registry is their assembled grammar; see extension reference.
Install extensions
The core extensions are always installed: base shapes and port markers, Plain Text, the grid, the default connector type with straight routes, base arrowheads, the interaction tools and the core verbs. Add everything else to extensions:
- Dependencies install themselves. An extension declares the extensions it needs, such as
ShapesExtensionneedingTableExtension. Each dependency is installed with its defaults unless the application installs an extension with the same id, in which case the application's instance is used. - A core id replaces the core default.
GridExtension(vyuh.core.grid) is installed by default; installing one with other arguments replaces the default. - Ids are unique. Installing two different extensions with one id, or two non-core extensions contributing the same shape type, tool or other key, is rejected with an
ArgumentError.
Order does not matter, and each feature is one class: a Flutter extension that needs a portable one declares it as a dependency, so list only TableFlutterExtension(), not TableExtension() beside it.
Options
Options are an extension's runtime configuration, such as whether the grid shows. Canvas bounds and fonts are not options: they belong to the configuration. They are typed, immutable values:
- Keyed by Dart type, with exactly one owner. An extension declares one options value holding all of its settings, and its flat constructor arguments build the initial value:
super(id: 'vyuh.core.grid', options: GridOptions(...)). - Conflicts fail at assembly. If two installed extensions declare the same options type, creating the diagram throws an
ArgumentErrornaming both extensions. - Read with
controller.options<T>(). It returns the current value and is observable, so anObserverthat reads it rebuilds when it changes. It throws aStateErrorwhen no installed extension ownsT. - Change with
controller.edits.setOptions(value). The controller finds the owner byvalue.runtimeTypeand replaces its options for every controller of the diagram. The change is not an edit: it records no undo entry and rules do not admit it. When no installed extension owns that type it changes nothing and returns a deniedEditResultwith the reason"<T>: no installed extension owns these options".
There are no string keys and no lookup of extension instances; the options type is the key. The grid is the pattern every extension follows:
dart
final controller = DiagramController(
configuration: DiagramConfiguration(
extensions: [GridExtension(visible: true, size: 24)],
),
);
final grid = controller.options<GridOptions>();
controller.edits.setOptions(grid.copyWith(snap: true));dart
Observer(
builder: (_) => Switch(
value: controller.options<GridOptions>().visible,
onChanged: (visible) => controller.edits.setOptions(
controller.options<GridOptions>().copyWith(visible: visible),
),
),
)| Extension (owner) | Options | Settings it holds |
|---|---|---|
GridExtension (core, vyuh.core.grid) | GridOptions | visible, snap, size, pattern, patterns (the patterns the property panel offers; duplicate ids throw an ArgumentError); see grids |
EditorExtension | EditorOptions | palette, toolbar (which shell regions show), inspector (InspectorPlacement.none, floating or docked), inspectorSide, inspectorWidth, inspectorDockedMinimumWidth (900 by default; 0 allows compact docking), libraries (shape library tabs in the inspector panel), rotationSnap (degrees Shift snaps rotation to), connectionSnapRadius (screen pixels; zero disables connection magnets), theme (EditorTheme) |
NavigationExtension | NavigationOptions | edgePanEnabled, edgePadding, maximumPanSpeed, keyboardStep, shiftMultiplier, fitOnAttach; see navigation |
ImagesExtension | ImagesOptions | limits, picker; see images |
RenderingExtension (adapter.dart) | RenderingOptions | lod, render; see visual detail |
MinimapExtension | MinimapOptions | visible, policy, initiallyExpanded, alignment, margin, theme |
SnapGuidesExtension | SnapGuidesOptions | enabled, policy, tolerance, theme |
TableFlutterExtension | TableFlutterOptions | standalone, embedded, resolve (per-table overlay modes, read with modeFor), theme; see tables |
Each options class of the Flutter extensions has copyWith, so a runtime change keeps the other settings:
dart
controller.edits.setOptions(
controller.options<MinimapOptions>().copyWith(visible: false),
);EditorOptions implements the core DiagramSnapSettings interface the engine reads while editing. Without an editor installed, the engine uses DiagramSnapSettings.standard (15° and 10 pixels); without the navigation, images or rendering extension installed, the canvas uses those options' defaults. A canvas reads ImagesOptions and RenderingOptions when it mounts; the other options apply live.
An extension of your own declares options the same way:
dart
import 'package:vyuh_diagram_flutter/extension_api.dart';
final class Badges {
const Badges({this.visible = true});
final bool visible;
}
final class BadgesExtension extends DiagramFlutterExtension {
BadgesExtension({bool visible = true})
: super(id: 'app.badges', options: Badges(visible: visible));
}
// controller.edits.setOptions(const Badges(visible: false));Give an options class value equality (== and hashCode) so observers rebuild only when a value actually changes.
Options in the property panel
An extension contributes its options to the inspector by overriding propertiesFor(scope). The grid extension offers its show-grid toggle, pattern and size in the canvas group this way, and the editor offers its rotation snap. Core offers the finite-canvas toggle for the canvas bounds. A property typed on an options class reads and updates that options value:
dart
import 'package:vyuh_diagram_flutter/extension_api.dart';
@override
Iterable<DiagramProperty<Object?, Object?>> propertiesFor(
DiagramPropertyScope scope,
) sync* {
yield DiagramProperty.toggle<Badges>(
'show-badges',
label: 'Show badges',
group: DiagramPropertyGroup.canvas,
read: (badges) => badges.visible,
update: (badges, visible) => Badges(visible: visible),
);
}See the property grammar.
Editing
A canvas edits when an editable extension is installed; EditorExtension() is the editable extension. Without one, the canvas is view-only: it navigates and selects, and the controller's verbs still edit programmatically.
EditorExtension() also wraps the canvas in a tool palette, an inspector panel and a toolbar. Turn a region off with palette: false, inspector: InspectorPlacement.none or toolbar: false, dock or float the inspector with InspectorPlacement.docked or floating, and change any of them at runtime through its EditorOptions, or replace the whole shell with container. tools: [...] limits and orders the palette; by default every installed tool the grammar supports is offered and the default tool is select. See editor composition.
Rules
dart
DiagramController(
configuration: DiagramConfiguration(
rules: DiagramRules(
beforeChange: (proposal) => readOnly
? const EditResult.denied(reason: 'This drawing is read-only.')
: const EditResult.allow(),
canConnect: (intent) => intent.source == intent.target
? const EditResult.denied(reason: 'No loops')
: const EditResult.allow(),
),
),
);beforeChange sees every proposed change from any controller: verbs, gestures, loads, undo and redo. canCreate admits prepared shapes and text elements, and canConnect admits connections from the first port hover to the commit. A denied edit publishes nothing and the verb returns the denial. The initial document is not checked. See rules.
Bounds
The canvas is infinite by default. CanvasBounds.fit(padding: ...) bounds it to the content:
dart
final controller = DiagramController(
configuration: DiagramConfiguration(bounds: CanvasBounds.fit(padding: 64)),
);
controller.edits.setBounds(CanvasBounds.infinite);A fit canvas is the content extent plus padding viewport (screen) pixels on every side, so the room around the content looks the same at every zoom. It grows and shrinks with the content and keeps the camera within it; an empty document gets a viewport-sized canvas around the origin. Read the current bounds with the observable controller.bounds and change them with controller.edits.setBounds(...). See canvas bounds.
Themes
DiagramCanvas(theme: ...) sets the DiagramTheme for one canvas; without it the canvas reads the host Theme's DiagramTheme, or DiagramTheme.light. The chrome an extension adds is themed by that extension's own options: each Flutter extension takes an optional theme in its constructor, and without one picks its light or dark default by DiagramTheme.brightness:
dart
final controller = DiagramController(
configuration: DiagramConfiguration(
extensions: [
EditorExtension(theme: EditorTheme.dark),
MinimapExtension(
theme: MinimapTheme.dark.copyWith(viewport: Colors.teal),
),
SnapGuidesExtension(theme: const SnapGuideTheme(color: Colors.teal)),
TableFlutterExtension(
theme: TableTheme.dark.copyWith(range: Colors.teal),
),
],
),
);
DiagramCanvas(controller: controller, theme: DiagramTheme.dark);A table theme without a range color uses the canvas selection color. The canvas background is DiagramTheme(canvas: CanvasTheme(background: ...)). See theming.
Fonts
The configuration holds the font catalog the inspector offers for standalone text, shape stories and connector labels, and the family text inherits:
dart
final controller = DiagramController(
configuration: DiagramConfiguration(
fonts: DiagramFontChoices(const [
DiagramFontFamily(family: 'IBM Plex Sans', label: 'Sans serif'),
DiagramFontFamily(family: 'Shantell Sans', label: 'Handwritten'),
]),
defaultFontFamily: 'Shantell Sans',
),
);| Value | Meaning |
|---|---|
DiagramFontFamily.family | Exact registered family name. |
DiagramFontFamily.label | Display and accessibility label. |
Read them from controller.configuration.fonts and controller.configuration.defaultFontFamily. The host bundles or loads the font files; extensions do not download fonts. Explicit text-run families override the inherited one, and imported families do not expand the catalog. controller.edits.setFonts(fonts) replaces the catalog, and controller.edits.setDefaultFontFamily(family) reflows text without editing document content. Headless output must use the same family and font resources; see headless text.
Canvas tuning
Three Flutter extensions tune how a canvas behaves. Each is optional; without it the canvas uses its defaults. Their constructor arguments build their options (NavigationOptions, ImagesOptions, RenderingOptions).
| Extension | Configures |
|---|---|
NavigationExtension({edgePanEnabled, edgePadding, maximumPanSpeed, keyboardStep, shiftMultiplier, fitOnAttach}) | Edge auto-pan, keyboard movement and fitting content when the canvas first has a viewport; see navigation. |
ImagesExtension({limits, picker}) | Image loading limits and the image tool's source picker. |
RenderingExtension({lod, render}) | Level of detail at low zoom and experimental retain and raster switches. A renderer tuning extension: import it from package:vyuh_diagram_flutter/adapter.dart. |
Images
dart
ImagesExtension(
limits: DiagramImageConfiguration(
maximumConcurrentLoads: 2,
retentionBytes: 128 * 1024 * 1024,
),
picker: () async => pickMyImage(),
)DiagramImageConfiguration({maximumConcurrentLoads = 4, loadTimeout = 30 s, retentionBytes = 256 MiB}) values must be positive. The timeout begins when a load is admitted, excluding queue time. retentionBytes is a soft limit for retained decoded resources; visible images stay pinned. The picker returns a DiagramImageSource, or null to cancel; the default uses the platform file picker. Export uses its explicit output options.
Visual detail
dart
import 'package:vyuh_diagram_flutter/adapter.dart';
RenderingExtension(
lod: DiagramLodPolicy(
adaptive: true,
compactBelowPixels: 32,
thumbnailBelowPixels: 10,
),
)Full detail is the default. With adaptive detail enabled, projected size selects compact or thumbnail rendering. At thumbnail size, connector markers, connector labels and ports are omitted as well. Interaction-critical elements retain full detail. This changes only how the canvas draws, not document geometry or output content.
DiagramRenderPolicy({retainContent, rasterContent}) switches are experimental and off unless the matching --dart-define (DIAGRAM_RETAIN_CONTENT, DIAGRAM_RASTER_CONTENT) is set. They can make large, mostly static drawings cheaper to repaint at the cost of memory; they never change what is drawn or exported.
Text layout and output
A mounted Flutter canvas supplies text layout and SVG and PNG output. Without a canvas, an extension supplies them; at most one installed extension supplies each:
| Host | Extension |
|---|---|
| Flutter, exporting before a canvas mounts | DiagramExtension(id: 'app.backends', textLayout: FlutterDiagramTextLayoutEngine(), output: FlutterDiagramOutput()), both from package:vyuh_diagram_flutter/extension_api.dart |
| Dart service, SVG | SvgExtension() from package:vyuh_diagram_codec_svg/vyuh_diagram_codec_svg.dart |
| Dart VM service, native text shaping | PangoTextLayoutExtension(fontFiles: [...], defaultFontFamily: ...) from package:vyuh_diagram_core/vyuh_diagram_pango.dart |
Replacing a backend does not introduce a second document or scene authority. See output and headless text.
Key bindings and verbs
Edit document content by performing controller verbs. Extensions and shape definitions declare verbs, and keyboard shortcuts are DiagramKeyBindings, contributed through DiagramFlutterExtension(shortcuts: [...]), that name an installed verb. See canvas key bindings.