Verbs and keyboard bindings
A verb is a named, typed edit. Buttons, shape regions, keyboard shortcuts, the inspector and agents all perform the same verbs, so a shortcut and a button can never disagree. Declare a shape's own verbs on its definition; perform them through controller.edits.perform([id], 'verb', args). Install the same definitions when reopening a document: verbs are host behavior, not serialized content. See verbs for the full contract.
dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';
final controller = DiagramController(
configuration: DiagramConfiguration(extensions: [counterFlutterExtension()]),
);
final canvas = DiagramCanvas(controller: controller);
/// Sets a counter's value. Performing it with the previous count reverses it.
final setCount = DiagramVerb.update<ShapeElement>(
id: 'setCount',
label: 'Count',
target: DiagramVerbTarget.element,
parameters: [DiagramVerbParameter.integer('count', minimum: 0)],
update: (shape, call) => shape.copyWith(
values: {...shape.values, 'count': call.arg<int>('count')},
),
);
/// Sets the count back to zero; `setCount` expresses the reverse.
final resetCount = DiagramVerb(
id: 'resetCount',
label: 'Reset count',
symmetry: const DiagramVerbSymmetry.derived(reversedBy: 'setCount'),
target: DiagramVerbTarget.element,
perform: (call) =>
call.controller.edits.perform([call.id], 'setCount', {'count': 0}),
);
ShapeDefinition counterShape() => ShapeDefinition(
type: 'app.counter',
size: const DiagramSize(160, 96),
values: const {
'count': NumberValueField(defaultValue: 0, minimum: 0, integer: true),
},
verbs: [setCount, resetCount],
body: ShapeColumn([
ShapeSized(
extent: 24,
child: const ShapeSurface(
id: 'reset',
style: ShapeStyle(
fill: DiagramColor(0xFFF1F5F9),
strokeWidth: 0,
cornerRadius: 0,
),
onInteract: 'resetCount',
child: ShapeText(
'reset-label',
text: 'Reset',
editable: false,
alignment: ParagraphAlignment.center,
),
),
),
const ShapeText('body', horizontalPadding: 12, verticalPadding: 12),
]),
);
/// The portable half: a headless host installs this one alone.
DiagramExtension counterExtension() =>
DiagramExtension(id: 'app.counter', shapes: [counterShape()]);
int countOf(DiagramController controller, String? id) =>
switch (controller.document.elementById(id ?? '')) {
ShapeElement(:final values) => (values['count'] as num? ?? 0).toInt(),
_ => 0,
};
/// The Flutter half: key bindings, installing the counter shape with it.
DiagramFlutterExtension counterFlutterExtension() => DiagramFlutterExtension(
id: 'app.counter_keys',
dependencies: [counterExtension()],
shortcuts: [
DiagramKeyBinding(
verb: 'setCount',
args: (controller, id) => {'count': countOf(controller, id) + 1},
key: LogicalKeyboardKey.equal,
modifiers: const {DiagramKeyModifier.command, DiagramKeyModifier.shift},
scope: DiagramKeyScope.selection,
shapeType: 'app.counter',
),
],
);A button checks the same verb with canPerform and performs it on activation. Availability is checked again when the verb runs; unknown verbs, missing targets and locked targets are denied. Read availability inside an Observer so a control refreshes when the document or selection changes.
dart
Widget incrementButton(DiagramController controller, String id) {
final next = {'count': countOf(controller, id) + 1};
return TextButton(
onPressed: controller.canPerform([id], 'setCount', next).allowed
? () => controller.edits.perform([id], 'setCount', next)
: null,
child: Text('Count ${countOf(controller, id)}'),
);
}Setters, not toggles
Every verb is two-way, so an agent can always state the reverse edit. A verb declares its symmetry:
| Symmetry | Use it for | Example |
|---|---|---|
DiagramVerbSymmetry.setter | A verb that sets a value; performing it with the previous value reverses it. DiagramVerb.update declares one. | setCount(count) |
DiagramVerbSymmetry.pair('counterpart') | Half of an insert/remove pair. | insertRow / removeRow |
DiagramVerbSymmetry.derived(reversedBy: 'setter') | A verb that computes new values a setter expresses. | resetCount, reversed by setCount |
There is no increment or toggle verb. A control that increments computes the next value and performs the setter, as the key binding above does; a toggle performs the setter with the opposite of the current value. Building the diagram rejects a verb whose counterpart or setter is not installed.
One edit, one Undo
Each perform is one admitted edit and one undo entry, however many elements it changes. A verb that composes other verbs, such as resetCount, still records one entry; use controller.edits.batch(label, () {...}) to group several performs from your own code. A denial changes nothing.
DiagramVerb.update covers setters that replace their target element. For anything else, DiagramVerb(perform: ...) receives a DiagramVerbCall with the controller, the target ids and the decoded args; compose other verbs with call.controller.perform, or submit a transaction with call.submit.
Verbs are synchronous. Fetch external data before performing a verb, and pass the expectedRevision you captured before the await so an edit prepared against an older document is denied as a conflict.
Publication automatically resolves and renders affected shapes. Rows and columns reflow within updated dimensions; they do not implicitly resize every container. The shape's declared textSizing policy can grow a text body automatically, or a verb can explicitly resize it. No refresh method is needed.
Keyboard ownership
Bindings have three scopes, DiagramKeyScope:
| Scope | Eligible input owner |
|---|---|
canvas | Canvas with an empty selection |
selection | Canvas with an object selection |
text | The active diagram text editor |
A binding performs its verb on the selected element, or on the diagram in canvas scope. args computes the arguments from the controller and that element when the chord is pressed. The verb must be installed: the canvas rejects a binding whose verb no installed extension, shape or part offers.
Use shapeType to restrict a binding to one shape type and slotId to restrict a text binding to one named slot. A slot restriction requires scope: text. More specific matching bindings win: slot and shape, then slot, then shape, then the unrestricted scope. Duplicate scope/chord declarations are rejected.
command means Meta on macOS and Control elsewhere. Modifier matching is exact. Bindings receive initial key-down events, not repeats. A matched binding consumes its chord even when its verb is unavailable; an unmatched key retains the normal editor behavior. Escape, active IME composition, pointer gestures, and focused embedded controls retain their existing input ownership. Selection and canvas bindings never run in diagram text editing.
The playground binds Command+Enter on a selected entity to its primary.insertRow part verb, with the row count as at. It appends a row to the entity's primary section. Tab retains normal editor behavior and does not add rows.
Buttons and interactive regions
Any Flutter control can perform a verb. The playground Count button performs setCount with the next count; Command+Shift+= performs the same verb on a selected controls card. Embedded controls continue to own their own focused keystrokes.
For an interactive region, give a ShapeSurface in the shape's body an onInteract naming one of the shape's verbs. The pointer shows a click cursor over it. The verb is performed without arguments on pointer release inside the same region, so name a verb that needs none, such as resetCount; cancel or release outside does not perform it. A region interaction is always a verb, so it is admitted, undoable and available to agents like any other edit.
No repeatable-content grammar or runtime body insertion is introduced by this API. The verb decides how to change the existing canonical data.