Skip to content

Properties and the inspector ​

Everything a person can edit in the inspector is a property: a key, a label, a group, how to read its value from a subject, the verb that sets a new one and how the value is presented. Built-in styles, shape value fields, connector route options, tables, rich text and your own domain data all use the same property grammar, so they share one inspector, one layout and one undo history.

The inspector shows the properties that apply to the current selection. A change is always one undoable controller edit, even when several elements are selected. When selected elements disagree, the property shows as Mixed. Shared fields intersect the selected kinds' ranges and choices. Incompatible ranges are disabled with a reason; a mixed choice has no active option.

Application code uses typed keys from the main application import:

dart
final fill = controller.properties().require(ShapeProperties.fill);
if (fill.isEnabled) fill.set(const DiagramColor(0xff2457ff)).requireAllowed();
final score = controller.properties(ids: ['service']).find(
  ShapeProperties.value<double>('score'),
);

find returns null when a field does not apply to every affected item; require reports that case clearly. Fields capture a revision, so resolve them again after an edit. Shape-specific declarations belong in ShapeDefinition(properties: [...]); each kind retains its own read function and verb arguments when shared fields edit several kinds together.

Showing the inspector takes only the editor's main library. Declaring properties uses the property grammar (DiagramProperty, DiagramPropertyGroup, DiagramPropertyScope, DiagramProperties, DiagramPropertySource) and custom editors use DiagramPropertyEditor, all from package:vyuh_diagram_flutter/extension_api.dart. Controls for custom editors, such as DiagramNumericProperty, are in package:vyuh_diagram_extension_editor/controls.dart.

DiagramPropertyField<T>, DiagramPropertyGesture<T> and typed keys are in vyuh_diagram_flutter.dart or the headless vyuh_diagram_core.dart. The DiagramPropertyEditor.of<T> callback receives a field of type T directly.

dart
Row(children: [
  Expanded(child: DiagramCanvas(controller: controller)),
  SizedBox(width: 280, child: DiagramInspector(controller: controller)),
])

Import DiagramInspector from package:vyuh_diagram_extension_editor/vyuh_diagram_extension_editor.dart. The property grammar itself is part of vyuh_diagram_core and has no Flutter dependency.

Presentations ​

Each constructor pairs a value type with its presentation:

ConstructorValuePresentation
DiagramProperty.textStringA text field
DiagramProperty.numberdoublemin, max, step, unit, precision and a stepper, field, slider, percentage or cell style; bounds narrows the range per subject
DiagramProperty.toggleboolA switch, a compact switch, or with an icon a toolbar button or a button in the section header
DiagramProperty.choiceTThe options a subject offers, as swatches, a menu or buttons
DiagramProperty.colorDiagramColorA color with alpha
DiagramProperty.fillDiagramFillNo fill, a solid color or a gradient
DiagramProperty.point, DiagramProperty.sizeDiagramPoint, DiagramSizeAn x/y or width/height pair
DiagramProperty.actionnoneA verb performed without a value
DiagramProperty.customTAn editor chosen by T
DiagramProperty.forValueas storedThe standard control of a ValueField

The inspector draws a choice's swatches by the option values' type: stroke patterns, routes, arrowheads, ink presets, grid patterns, text directions, label presentations and font families each have their own swatch; other options show their icon or label.

A property reads a subject and names the verb that sets a new value and the verb's arguments for that value:

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

DiagramProperty.number<ShapeElement>(
  'shape-corner-radius',
  label: 'Corner radius',
  group: DiagramPropertyGroup.style,
  read: (shape) => shape.style.cornerRadius,
  verb: 'setStyle',
  args: (shape, radius) => {'cornerRadius': radius},
  max: 48,
  unit: 'px',
)

The subject type decides where the property applies: ShapeElement, ConnectorElement, TextElement or DiagramElement for selected elements, ConnectorLabel for a selected label, DiagramTextSubject for the text being edited, and an options type such as GridOptions, or CanvasBounds, for the canvas. visible and enabled predicates receive the same subject. A property applies to every selected subject of its type, so a multi-selection edits them together.

Properties of elements take verb: and args:. Properties of values that are not elements, such as GridOptions, a ConnectorLabel or a DiagramTextSubject, take update: (subject, value) => newSubject instead. The inspector then applies the new value through its owner: options through controller.setOptions, the canvas bounds through controller.setBounds, a label through setConnectorLabel and text through replaceText. editLabel names the undo entry for a value.

A locked element offers only its lock, and a creation tool only the fill, stroke and corners of the shapes it will create.

Shape properties ​

A shape's properties follow from its anatomy: fill and stroke from its outline, corner radius from a rounded outline, sides from a polygon, points from an editable path, an image from an image value, text from its text parts and one standard control per declared value through DiagramProperty.forValue. Route options are value fields too, so a route's options get the same controls.

Add your own properties from a DiagramPropertySource. This Asset shape stores an owner, picked from a host list:

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

const assetGroup = DiagramPropertyGroup(
  'asset',
  label: 'Asset',
  after: 'appearance',
);

final owner = DiagramProperty.custom<ShapeElement, Owner>(
  'asset-owner',
  label: 'Owner',
  group: assetGroup,
  read: (shape) => Owner(shape.values['owner']! as String),
  verb: 'setValues',
  args: (shape, owner) => {
    'values': {'owner': owner.name},
  },
  check: (owner) => owner.name.trim().isEmpty ? 'Choose an owner' : null,
);

final class AssetProperties implements DiagramPropertySource {
  @override
  Iterable<DiagramProperty<Object?, Object?>> propertiesFor(
    DiagramPropertyScope scope,
  ) => [owner];
}

final asset = ShapeDefinition(
  type: 'asset',
  name: 'Asset',
  values: {'owner': TextValueField(defaultValue: 'Unassigned')},
);

check rejects a value before its verb is performed; the inspector shows the reason under the control. A property compares values with == to detect a mixed selection; pass equals for values such as case-insensitive names.

Write an editor ​

A custom property's value type chooses its editor. Register an editor for Owner. It receives the resolved field: its value, whether the selection is mixed, whether it is enabled, and set to perform the property's verb through the controller. An editor can render inline or open its own dialog, sheet or route:

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

final class Owner {
  const Owner(this.name);
  final String name;

  @override
  bool operator ==(Object other) => other is Owner && other.name == name;

  @override
  int get hashCode => name.hashCode;
}

const owners = ['Ada', 'Grace', 'Linus'];

final ownerPicker = DiagramPropertyEditor.of<Owner>((context, field) {
  return TextButton(
    onPressed: field.isEnabled
        ? () async {
            final revision = field.revision;
            final chosen = await showDialog<String>(
              context: context,
              builder: (context) => SimpleDialog(
                title: const Text('Owner'),
                children: [
                  for (final name in owners)
                    SimpleDialogOption(
                      onPressed: () => Navigator.pop(context, name),
                      child: Text(name),
                    ),
                ],
              ),
            );
            if (chosen == null) return;
            final result = field.set(
              Owner(chosen),
              expectedRevision: revision,
            );
            if (result.code == DiagramDenialCode.conflict) {
              // The diagram changed while the dialog was open.
            }
          }
        : null,
    child: Text(field.isMixed ? 'Mixed' : field.value!.name),
  );
});

DiagramInspector(
  controller: controller,
  properties: DiagramProperties(sources: [AssetProperties()]),
  editors: [ownerPicker],
)

The captured field revision is checked by default; expectedRevision can make that guard explicit when the value is chosen after asynchronous work. If the diagram changed in the meantime, set returns a denial with DiagramDenialCode.conflict and changes nothing. For continuous input such as a drag, call field.begin(), preview each value and commit once (or cancel): the gesture performs the verb on an interaction lease, each preview starting over from the baseline, and the drag is one undo entry.

The inspector places a custom editor beside its label by default. Pass layout: DiagramPropertyLayout.stacked for a label above, or full when the editor draws its own label. Without an editor for its type, a custom property shows its summary.

Groups ​

A DiagramPropertyGroup is a section. The inspector shows each group once, in the order its first property appears. A property contributed to an existing group joins its end; a new group starts after the group it names. Its label titles the section, and its spacing sets the gaps before the section, under its title and after it.

Icons are DiagramIcon values: a symbolic name plus an optional portable svg glyph. Give a property or option icon: const DiagramIcon('asset') and pass icons: {'asset': Icons.inventory_2} to the inspector to resolve the name. The inspector uses a host icon for the name first, then the editor's icon for core names, and otherwise draws the declared glyph with DiagramIconGlyph, so an extension that ships its own glyph, such as rich text's RichTextIcons, needs no host mapping. Set searchable: true to offer a filter by property label.

Contribute from an extension ​

An extension contributes properties by overriding propertiesFor(scope). The grid extension offers Show grid, Grid type, Snap to grid and the grid size for its own GridOptions in the canvas group. Anything an extension installs can contribute too by implementing DiagramPropertySource: a connector route provider, a content provider or a text formatting object. The connectors extension surfaces each route's options in the Routing section, rich text adds marks, headings and lists, and the table extension adds its Table section after Appearance.

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

final class ReviewRoute extends DiagramConnectorRouteProvider
    implements DiagramPropertySource {
  // ...
  @override
  Iterable<DiagramProperty<Object?, Object?>> propertiesFor(
    DiagramPropertyScope scope,
  ) => [reviewLaneProperty];
}

A Flutter extension renders its custom properties by listing an editor for each value type in propertyEditors:; the inspector finds them in the extensions installed in the diagram's configuration:

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

DiagramFlutterExtension(
  id: 'app.assets',
  shapes: [asset],
  propertyEditors: [ownerPicker],
)

Without the inspector ​

The grammar works headlessly. Resolve the fields for the current selection and set values through them, for example from a command palette or a test:

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

final fields = DiagramProperties().resolve(DiagramPropertyScope(controller));
final width = fields.firstWhere((field) => field.key == 'shape-stroke-width');
width.set(4).requireAllowed();

See the property inspector reference for the built-in sections and standard controls.