Build a custom shape from scratch
Build a review card with a title, description, embedded table, typed property and connection ports. Set up the canvas using Set up the canvas, then replace lib/main.dart with this example. No custom painter or editor is needed. The Flutter import exposes the anatomy and port declarations; tables and UI are explicit optional imports.
dart
import 'package:material_ui/material_ui.dart';
import 'package:vyuh_diagram_extension_editor/vyuh_diagram_extension_editor.dart';
import 'package:vyuh_diagram_extension_table/vyuh_diagram_extension_table.dart';
import 'package:vyuh_diagram_extension_table_flutter/vyuh_diagram_extension_table_flutter.dart';
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';
final reviewDefinition = ShapeDefinition(
type: 'tutorial.reviewTable',
name: 'Review table',
size: const DiagramSize(320, 240),
minimumSize: const DiagramSize(200, 160),
behavior: const ShapeBehavior(textSizing: ShapeTextSizing.growHeight),
values: {
'priority': const NumberValueField(
defaultValue: 1,
minimum: 1,
maximum: 5,
integer: true,
),
},
connection: ShapeConnectionDefinition.fixedPorts([
PortDefinition.onSide(id: 'input', side: PortSide.left),
PortDefinition.onSide(id: 'output', side: PortSide.right),
]),
body: ShapeColumn([
ShapeSized(
extent: 40,
child: ShapeSurface(
style: const ShapeStyle(fill: DiagramColor(0xFFEAF1FF)),
child: const ShapeText(
'title',
text: 'Review',
textType: PlainTextDefinition(maxLines: 1),
),
),
),
ShapeSized(
extent: 48,
child: const ShapeText(
'description',
text: 'Describe the review',
verticalPadding: 8,
),
),
const ShapeTable('items', rows: 2, columns: 2),
]),
);
DiagramController createReviewTableController() {
final controller = DiagramController(
configuration: DiagramConfiguration(
extensions: [
// This screen mounts its own inspector beside the canvas.
EditorExtension(inspector: InspectorPlacement.none),
TableFlutterExtension(),
customShapeTutorialExtension(),
],
),
);
controller.createShape(reviewDefinition.type, at: DiagramPoint.zero);
return controller;
}
void main() => runApp(
MaterialApp(
theme: ThemeData(splashFactory: InkRipple.splashFactory),
home: const Scaffold(body: ReviewTableEditor()),
),
);
class ReviewTableEditor extends StatefulWidget {
const ReviewTableEditor({super.key});
@override
State<ReviewTableEditor> createState() => _ReviewTableEditorState();
}
class _ReviewTableEditorState extends State<ReviewTableEditor> {
late final controller = createReviewTableController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Row(
children: [
Expanded(child: DiagramCanvas(controller: controller)),
SizedBox(width: 280, child: DiagramInspector(controller: controller)),
],
);
}
DiagramExtension customShapeTutorialExtension() =>
DiagramExtension(id: 'tutorial.review', shapes: [reviewDefinition]);1. Choose the element boundary
This is one shape. Its title, description and table share transforms and history. ShapeSurface is artwork inside the shape, not a separately selectable child. Use frames or groups for independently selectable document elements.
Definitions declare the anatomy and defaults. Instances own geometry, values and editable stories. Rebuilding a definition does not reset authored text.
2. Compose the body
ShapeColumn gives the title and description fixed heights; the table fills the remaining region. Nest ShapeRow, ShapePadding and ShapeStack for richer layouts. ShapeSurface parts with polygon or path outlines add portable artwork. See shape anatomy.
Named text slots become regions of the owner's canonical story, not separate editing controllers. ShapeTable contributes a table to that story. Cells own styled body paragraphs; headings, lists and nested tables are forbidden. Content minimums can grow the table and its owner. A fixed description region can overflow; use a flexible region when it should share available space.
3. Declare properties and inspector behavior
priority is persisted and constrained to integers from 1 to 5. The inspector discovers declared values and the appearance and text editing its anatomy supports: fill, stroke and corner radius from its default rectangle outline, text from its text parts and table. Keep constraints in the definition rather than copying them into private forms. The standard editors always follow the shape's anatomy and never replace table controls. Add domain properties, nested records and custom editors with properties:; see properties and the inspector.
Domain values are not automatically rendered as text. Update a slot explicitly with controller.edits.setText(id, text, slot: 'title'), or display values through a ShapeWidget part. Declare a verb on the shape for a domain edit that belongs in one Undo step.
4. Connect and save
This shape uses ports-only connections. Pass fromPort: 'output' or toPort: 'input' to controller.connect. The framework places the ports and routes the connectors. Save controller.save(DiagramJsonCodec.new) and restore with the same registry. Documents contain values and content, not executable callbacks. Keep type and slot identities stable; a document only loads against the grammar that admits it.
5. Add controls through the shared owners
| Need | Existing integration |
|---|---|
| Properties outside the canvas | DiagramInspector and the properties that perform your verbs |
| Flutter controls inside a region | A ShapeWidget part and Flutter widget builders; see embedded controls |
| Toolbar above the canvas | Host UI performing verbs |
Paint-only effects inside or around a shape are visuals, not interactive controls. Embedded widgets are shape content, not selection-only buttons outside the shape. A general declarative interactive overlay grammar is not yet available. Do not draw fake buttons in ShapeSurface and expect controls. See the proposed API reference.
6. Handle failures and test
Check EditResult.allowed and show its reason for expected denials. Creation helpers can throw DiagramChangeDenied; malformed imports can throw FormatException; stale edits can raise revision conflicts. Keep user messages at the host boundary and technical diagnostics in logging. Never silently retry a stale command against the latest revision.
Test title and cell editing, growth, minimum size, ports, locking, Undo/Redo, JSON reopening, rotation and export. Invalid property values must leave content and history unchanged. The repository compiles this tutorial alongside its other public learning examples.