Skip to content

Widgets inside shapes ​

A shape can mount Flutter controls in declared content slots. The shape registry owns slot geometry and portable fallback content; DiagramFlutterExtension.widgets owns Flutter builders. Durable control values live in ShapeElement.values and use the same validation, admission and history as other document changes. ShapeWidget, ShapeWidgetBuilder, ShapeWidgetContext, ShapeValueEdit and DiagramFlutterExtension are in package:vyuh_diagram_flutter/extension_api.dart.

Open Controls inside a node to try the controls card with a slider, dropdown and counter button.

On this page ​

CategoryWhat you can do
Define widget contentPlace a widget part, declare values and portable fallback content.
Register controlsConnect widget keys to Flutter builders.
Read and edit valuesRead current values and submit validated updates.
Continuous editingPreview a drag and commit it as one undo action.
Lifecycle and exportHandle focus, replacement and exported fallback content.
Pointer behaviorKeep controls interactive and other areas draggable.

Compose a shape visually ​

One shape, several regions and layersA rectangular shape has title, body and footer regions. The body contains a padded widget slot. Content layout arranges regions; layers supply their content. Ports attach to the outside contour.Divide the spaceCompose the contentTitle regionFooter regionWidget slotInset inside the bodyPorts attach to the contour.SurfaceOutline, fill and borderText + visual layersStories, regions, images, dividersWidget slotLive Flutter controls, or fallbackLayers share the shape coordinates.
Content layout arranges regions inside the shape. Layers supply their text, artwork and controls.

Start with the outline, divide the interior into content regions with layout parts, then choose the content for each region. Use the same terms whether the content is portable diagram text and artwork or live Flutter controls.

TermMeaningAPI
OutlineThe outer frame and visible contour.ShapeElement.frame, ShapeDefinition.outline
Content regionA meaningful area such as title, body or footer. These are names you choose, not special shape types.Named text slots and part IDs
Content layoutHow regions and their content share the shape's interior.ShapeRow, ShapeColumn, ShapeStack, ShapeSized, ShapePadding, ShapeOutside
Region contentWhat appears there: text, surfaces, images, artwork or controls.ShapeText, ShapeSurface, ShapeDivider, ShapeImage; ShapeWidget for a live widget
Diagram layoutHow whole shapes are positioned on the canvas.Graph layout, including LayoutExtension

A shape's body declares its content layout. A column can give the title a fixed height, let the body fill the remaining space, and reserve a footer below it. A row can arrange an icon beside the title; a stack can layer content in the same region. See shape anatomy for the complete set of parts and resizing rules.

A ShapeWidget is an ordinary body part, so a live control is placed with the same rows, columns and padding as text and artwork. There is no grid part; nest rows and columns for a fixed grid-like arrangement, and use Flutter's Row, Column, Stack or a grid inside a widget's own region.

The outline controls the visible contour. Surfaces, images and widgets are clipped to it; text clips to its own region. Ports declare connection attachment points separately from content layout.

Draw artwork with ShapeSurface parts: a surface's outline can be a ShapeOutline.polygon(points) for closed straight-sided artwork or a ShapeOutline.normalized(path) for open lines and Bézier curves, in unit coordinates of its region, with its own fixed ShapeStyle. Use ShapeStack for multiple pieces. Canvas and SVG output use the same path geometry.

The widget builder receives the region's dimensions and arranges its own controls inside that space. The diagram handles moving, resizing and rotating the owning shape. The portable fallback part occupies the same region when no builder is available and in exported output.

Declare a slot ​

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

final controlDefinition = ShapeDefinition(
  type: 'host.control',
  outline: const ShapeOutline.rect(),
  values: {
    'progress': const NumberValueField(
      defaultValue: 35,
      minimum: 0,
      maximum: 100,
    ),
  },
  body: ShapePadding(
    12,
    child: ShapeWidget(
      'host.slider',
      id: 'control',
      fallback: ShapeSurface(
        style: const ShapeStyle(
          fill: DiagramColor(0xFFEAF1FF),
          strokeWidth: 0,
        ),
        child: ShapePadding(
          8,
          child: const ShapeDivider(edge: ShapeEdge.bottom),
        ),
      ),
    ),
  ),
);

The control has no text part, so it hosts no text and the inspector shows no paragraph or font controls. Its outline gives it fill and stroke; its value gives it a progress control.

ShapeWidget argumentTypeContract
widgetKeyStringRequired positional builder lookup key; nonempty without surrounding whitespace.
fallbackShapePartRequired portable content, laid out inside the widget's region. Any part except another widget; it may contain text slots.
idString?Optional stable name for the region, such as a port's targetLayerId.

Fallback text uses declared story slots and the shared paragraph layout. See shape values and definitions for field schemas and shape creation.

Register Flutter builders ​

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

final controller = DiagramController(
  configuration: DiagramConfiguration(
    extensions: [
      DiagramFlutterExtension(
        id: 'host.controls',
        shapes: [controlDefinition],
        widgets: {
          'host.slider': (context, shape) => ProgressControl(shape: shape),
        },
      ),
    ],
  ),
);
DiagramCanvas(controller: controller);

A Flutter extension carries its portable contributions, here the shape definition, together with its widget builders. A pure Dart host that loads the same documents installs DiagramExtension(id: 'host.controls', shapes: [controlDefinition]) instead; its shapes then show their fallback. The builder receives tight slot dimensions. Lay out your control locally; the canvas applies camera movement, rotation, clipping and scene stacking.

APIArguments / resultContract
ShapeWidgetRegistry(builders)Map<String, ShapeWidgetBuilder>Copies the map immutably. Keys must be nonempty without surrounding whitespace.
const ShapeWidgetRegistry.empty()No argumentsDefault registry; all widget slots display their portable fallback.
builderFor(key)String → ShapeWidgetBuilder?Returns null when the host has no builder. The mounted canvas retains the fallback.
require(key)String → ShapeWidgetBuilderThrows StateError for an absent key.
ShapeWidgetBuilderWidget Function(BuildContext, ShapeWidgetContext)Builds the live presentation of one resolved slot.

Builders are Flutter extension contributions, never document content. A document can be loaded without its widget builders, provided its shape definitions are registered.

ShapeWidgetContext ​

The canvas supplies a fresh immutable context when canonical state changes. Read values from that context instead of keeping a second authoritative copy in widget state. Widget state may retain transient UI resources, such as focus or an active edit handle.

MemberType / argumentsContract
ownerResolvedShapeResolved shape snapshot and its canonical source.
slotResolvedShapeWidgetSlotResolved bounds and slot definition.
valuesMap<String, Object?>Immutable canonical values from the owner.
readOnlyboolHost read-only state for this projection.
enabledboolFalse when read-only or locked. Use it to disable controls. Live callbacks still recheck ownership and admission.

Update widget values ​

Use a discrete update for a button or dropdown, and a continuous edit for a drag. Both participate in the drawing’s validation and undo history.

MemberType / argumentsContract
setValues(patch)Map<String, Object?> → EditResultPerforms the setValues verb on this owner, merging the supplied fields as one discrete edit. Unknown fields or invalid field values are rejected; a read-only or locked context returns a denied decision.
beginValueEdit()→ ShapeValueEditStarts a bounded continuous edit for this owner on an interaction lease. Throws StateError when read-only or unavailable.

enabled is a UI hint, not an authorization boundary. The diagram's rules remain in the canonical verb path; controls should handle denied results when the application needs to explain them.

A slider with one undo step ​

Use setValues for discrete actions such as buttons, dropdown changes and keyboard slider adjustments. A drag owns a ShapeValueEdit until it commits or cancels; each preview performs setValues on its interaction lease:

Edit memberArguments / resultContract
isActiveboolWhether the owned interaction lease is active.
preview(patch)Map<String, Object?> → EditResultPerforms setValues with the patch as a preview. Each patch starts from the original values, not the previous preview; include every field being changed together. An inactive edit returns a denied decision.
commit()→ EditResultCommits the accepted preview as one history operation.
cancel()voidCancels the preview without adding an undo entry.
dart
import 'package:vyuh_diagram_flutter/extension_api.dart';

class ProgressControl extends StatefulWidget {
  const ProgressControl({super.key, required this.shape});
  final ShapeWidgetContext shape;

  @override
  State<ProgressControl> createState() => _ProgressControlState();
}

class _ProgressControlState extends State<ProgressControl> {
  ShapeValueEdit? _edit;

  void _cancel() {
    _edit?.cancel();
    _edit = null;
  }

  @override
  void dispose() {
    _cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final shape = widget.shape;
    return Listener(
      onPointerCancel: (_) => _cancel(),
      child: Slider(
        value: (shape.values['progress'] as num).toDouble(),
        max: 100,
        onChangeStart: shape.enabled ? (_) {
          _cancel();
          _edit = shape.beginValueEdit();
        } : null,
        onChanged: shape.enabled ? (value) {
          final edit = _edit;
          if (edit != null) {
            edit.preview({'progress': value});
          } else {
            shape.setValues({'progress': value});
          }
        } : null,
        onChangeEnd: shape.enabled ? (_) {
          _edit?.commit();
          _edit = null;
        } : null,
      ),
    );
  }
}

This example uses Flutter's Slider, StatefulWidget and Listener; import your Flutter UI library and package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart. Provide the usual UI-library ancestors required by your controls. The complete playground example is in widget_card.dart.

Focus, lifecycle and output ​

Replacing a mounted slot's builder cancels its active value edit and restores the starting values before the replacement builder receives its context. Late callbacks from the old edit cannot commit or cancel a successor edit.

The mounted canvas interleaves controls with ordinary scene paint and clips them to their slot, the owner outline and clipping ancestors. Selection handles, ports and higher scene content retain their input priority. The accessibility tree retains the object selection entry and ordinary story text, while omitting fallback text replaced by a live slot. Native controls supply their own Flutter semantics; when a builder is absent, the fallback text is announced again. This follows the same block filtering used by paint.

Mounted fallback text is also excluded from canvas text editing. Enter, clicks, caret movement and selections target the visible text regions; headless verbs can still update the canonical fallback for portable output.

The playground controls card is selectable, movable, resizable and rotatable. Its content belongs entirely to the widget, so its body has no text part. The inspector exposes its registered custom values, fill, border and locking, without paragraph or font controls. Because these follow from the anatomy, the inspector and the verbs always agree.

Focused native controls own their keyboard input; Escape cancels the active value edit and returns focus to the canvas when it reaches the slot host. Starting a value edit from a pointer-only control gives its slot keyboard focus when no child already has it, so Escape can cancel a slider drag too. The slot does not add a Tab stop, and an already focused child keeps its focus.

Use ordinary Flutter controls and preserve their keyboard and input-method behavior. Text controls should keep the composing range intact and apply a diagram value after composition finishes.

Idle offscreen slots may unmount. Focused or pointer-active slots remain mounted until their interaction ends, preserving control state during panning. Deletion, unmounting, read-only/locked transitions and pointer cancellation cancel active edits. Release widget resources in dispose; do not store Flutter objects in shape values.

Fallback parts provide portable geometry and text for export. They are static declarations, with no automatic value binding or Flutter-widget conversion. Export uses these parts, not widget screenshots. See Output for SVG text and resource limits.

Backgrounds and pointer ownership ​

The shape definition owns the node background and border. Keep the native widget root transparent (for example, Material(color: Colors.transparent)). Padding, labels and other space without a control gesture select and drag the owning node. Sliders, buttons, dropdowns and text fields retain their native gestures.

Keep decorative overlays inside IgnorePointer. A full-slot GestureDetector or AbsorbPointer can block diagram input. Interactive controls stay outside that wrapper. Use the shape’s fill and border rather than painting a duplicate widget background.