Navigation and motion
Move the viewport with controller.view and play transient effects with controller.effects. Both act on the mounted canvas. Neither moves elements, changes the document, nor adds undo entries. Geometry verbs take the same Motion to animate elements to their new places.
dart
// Jump to the selection.
controller.view.show(const ViewTarget.selection());
// Travel to an element.
await controller.view.show(ViewTarget.element('review'), motion: Motion.smooth);
// Particles flowing along a connector.
final flow = controller.effects.play(const Effect.flow(), on: 'review-to-approval');
flow.stop();On this page
| Category | What you can do |
|---|---|
| Show a target | Jump or travel to a point, rectangle, element, region, the selection or everything. |
| Motion | Choose how the viewport travels, and interrupt it. |
| Animated geometry | Animate moved and resized elements. |
| Pointer and keyboard | Configure edge auto-pan and Arrow-key movement. |
| Coordinates | Convert pointer positions into drawing coordinates. |
| Read the view | Observe camera position, zoom and viewport. |
| Observe changes | React to view and effect changes without tracking document edits. |
| Effects | Play effects on shapes and connectors; see Animation & effects. |
Show a target
dart
Future<bool> show(ViewTarget target, {Motion? motion})
void stop()controller.view.show moves the viewport to one target. Without motion it jumps; with motion it travels. The future completes with true once the target is shown, and with false when the target names missing content or the move is interrupted.
| Target | Shows |
|---|---|
ViewTarget.point(point, {zoom}) | Centers a world point. Keeps the current zoom unless zoom is given. |
ViewTarget.rectangle(rectangle, {padding = 48}) | Fits a world rectangle. |
ViewTarget.element(id, {padding = 48}) | Fits an element's bounds. |
ViewTarget.region(id, regionId, {padding = 48}) | Fits a named region of a shape or text element. |
ViewTarget.selection({padding = 48}) | Fits the current selection. |
ViewTarget.all({padding = 48}) | Fits every element in the diagram. |
| Quantity | Meaning |
|---|---|
point, rectangle | World coordinates in the document. |
padding | Logical screen pixels kept free on each side of the viewport. |
zoom | Scale factor: 1 means 100%; clamped to the camera's zoom limits. |
regionId is a shape-layer definition ID or a text slot ID, such as a card's title. Rotated regions fit their axis-aligned world bounds. Fitting preserves aspect ratio and chooses the smaller of the horizontal and vertical scales within zoom limits. Finite canvas bounds may constrain the requested center. A target that names a missing element, region or an empty selection returns false and leaves the viewport where it is.
Mount DiagramCanvas(controller: controller), editable or view-only, before calling show; a detached or disposed controller throws StateError. For initial placement, wait until the canvas has completed its first layout, for example in a post-frame callback. The target is captured when the move starts; it does not follow an element that moves later.
Motion
dart
const Motion({
Duration duration = const Duration(milliseconds: 450),
MotionCurve curve = MotionCurve.easeInOutCubic,
})Motion is a plain description in the core library, shared by the camera and the geometry verbs. A zero or negative duration presents the change at once.
| Preset | Duration | Curve |
|---|---|---|
Motion.none | 0 ms | — |
Motion.quick | 200 ms | MotionCurve.easeOut |
Motion.smooth | 450 ms | MotionCurve.easeInOutCubic |
Motion.slow | 900 ms | MotionCurve.easeInOutCubic |
MotionCurve.cubic(x1, y1, x2, y2) is an easing curve as CSS cubic-bezier describes it: from (0, 0) to (1, 1) through two control points whose x lies within 0 and 1; y may overshoot. The named curves are linear, ease, easeIn, easeOut, easeInOut and easeInOutCubic.
dart
final shown = await controller.view.show(
const ViewTarget.region('approval', 'title', padding: 64),
motion: const Motion(
duration: Duration(milliseconds: 600),
curve: MotionCurve.cubic(0.215, 0.61, 0.355, 1),
),
);
if (!shown) {
// The region is missing, or another move interrupted this one.
}Only one move runs at a time. Starting another move, a jump, controller.view.stop(), a user pan or zoom, or any other camera change such as controller.setCamera interrupts the current move, which then completes with false. stop leaves the camera where it is and is safe to call when nothing is moving. Unmounting the canvas also interrupts a move. A zero duration, or a device with animations turned off, shows the target immediately.
Overshooting curves may overshoot position, but zoom stays within camera limits. Targets must have finite coordinates and zoom, and curves finite control points; an invalid target or curve completes with ArgumentError without changing the camera.
Animated geometry
move, resize, setElementBounds and layout accept a motion. The document takes the final geometry at once: undo, persistence, rules, agents and hit testing see the target. The mounted canvas then animates the rendered geometry from where it showed the elements to their committed place:
dart
controller.edits.setElementBounds(
'charger-load',
const DiagramRect.fromLTWH(0, 100, 126, 8),
motion: Motion.smooth,
);
await controller.edits.layout(Layout.layered(), motion: Motion.slow);| Aspect | Behavior |
|---|---|
| What moves | Every element the verb moved or resized, with its contents; text and ports follow. Many elements animate at once. |
| Connectors | Attached connectors re-route every frame. Only the moving elements and their connectors are resolved again. |
| Selection | Selection chrome follows the animated geometry. |
| Pointer input | Hits the committed geometry, even mid-animation. |
| A new change | Retargets smoothly from the shown frame. A change without motion jumps. |
| Batches | Verbs inside a batch, including batch(history: false), animate when the batch commits. |
| Reduced motion | With the platform's reduce-motion setting (MediaQuery.disableAnimations), elements jump. |
| No canvas | A headless controller ignores motion. |
Verbs performed by name take a Motion or a preset name: controller.edits.perform([id], 'move', {'delta': delta, 'motion': 'smooth'}).
Pointer and keyboard navigation
While dragging an element, handle, marquee or connection, entering the padding inside a canvas edge starts auto-pan. Speed increases toward the edge and continues while the pointer stays there. Corners pan on both axes. Leaving the edge area, releasing or cancelling the drag stops auto-pan. Hover alone does not move the canvas. The active edit follows the camera and remains one undo step.
Escape cancels the active gesture, including modifier combinations such as Space-drag panning, Shift rotation and Shift+Alt resizing. Document previews roll back without an undo entry; marquee selection returns to its starting selection. Panning stops at the current camera position. Remaining pointer movement and release cannot resume or commit the canceled gesture.
With canvas focus, Arrow keys move selected elements by one world unit in the pressed direction. Shift+Arrow moves them by ten world units; holding a key repeats movement. With no selection, neither shortcut moves the canvas. Text editing and focused widget controls retain their own arrow-key behavior. Selection movement supports undo and respects locks and host admission.
dart
final controller = DiagramController(
configuration: DiagramConfiguration(
extensions: [
EditorExtension(),
NavigationExtension(
edgePanEnabled: true,
edgePadding: 48,
maximumPanSpeed: 600,
keyboardStep: 1,
shiftMultiplier: 10,
fitOnAttach: true,
),
],
),
);NavigationExtension (id vyuh.navigation) owns NavigationOptions, with the same fields; without it the canvas uses these defaults, except fitOnAttach, which defaults to false. fitOnAttach fits the content when the canvas first has a viewport. Change the values at runtime with controller.edits.setOptions(controller.options<NavigationOptions>().copyWith(...)). Distances use logical screen pixels; maximumPanSpeed uses pixels per second. Numeric values must be finite and positive. Set edgePanEnabled: false to disable pointer auto-pan. Fit canvas bounds still constrain navigation. Only move and resize drags auto-pan by default. Shape manipulators, custom controls, and other gestures leave the camera stationary. Custom tools can explicitly opt in with DiagramToolGesture(autoPan: true, ...); edgePanEnabled remains the global gate.
For touch pan and pinch, see DiagramCanvas.
Coordinates
dart
DiagramPoint screenToWorld(DiagramPoint screenPoint)
DiagramPoint globalToWorld(Offset globalPosition)| Method | Argument | Return |
|---|---|---|
screenToWorld | Point relative to the canvas's top-left, in logical pixels. | World point under that canvas position. |
globalToWorld | Flutter global pointer position, such as details.globalPosition. | World point under that pointer. |
Both require a mounted canvas; globalToWorld also throws StateError before the canvas has been laid out. Do not pass a global pointer coordinate to screenToWorld when the canvas is embedded below a header or beside a panel.
camera
dart
DiagramCameraState get cameraReturns this controller's camera snapshot. It is never null; until a canvas is mounted and laid out, viewportSize is zero. Check isAttached to know whether a canvas is mounted. The immutable snapshot exposes:
| Property | Type | Meaning |
|---|---|---|
center | DiagramPoint | World point at the viewport center. |
zoom | double | Current projection scale. |
viewportSize | DiagramSize | Laid-out canvas size in logical pixels. |
minZoom | double | Lower zoom limit; camera-state constructor default 0.25. |
maxZoom | double | Upper zoom limit; camera-state constructor default 4. |
DiagramCameraState.copyWith(...) creates a snapshot; it does not update a controller. Use controller.view.show to change the mounted camera.
Observing the camera and effects
controller.camera and controller.effects.playing are observable. Read them inside an Observer to rebuild when they change. The camera changes after each new camera state, including layout, user navigation and motion; identical camera geometry does not notify. Effects change when they start, change or stop, not on every frame. These observations are separate from document events and undo/redo.
dart
Observer(
builder: (_) => Text('${(controller.camera.zoom * 100).round()}%'),
);Effects
controller.effects plays transient effects on the mounted canvas: flows and pulses along connectors, and pulses, halos, scaling and fades on shapes. See Animation & effects for the five kinds, their values, repeat counts, reduced motion and custom effects.
dart
final flow = controller.effects.play(const Effect.flow(), on: connectorId);
controller.effects.play(const Effect.halo(), on: shapeId);
flow.stop();Selecting inside groups
Click an element in a group to select its group. Double-click it without modifiers to enter the group and select that child; subsequent clicks select children within that group. Press Escape to return to the containing group.
Cmd-click (macOS) or Ctrl-click selects a child without entering its group; Shift-click adds or removes selection. Then drag, delete or edit the child normally without affecting siblings. Select a child before clicking its text to edit.
On connection labels, Shift-click toggles the connector selection and Command/Ctrl-click selects the connector directly. These modified clicks do not start label editing. Use an unmodified double-click to edit the label.
Outside text editing, modifier-clicking a checklist marker selects its text owner without checking or unchecking the item. An ordinary click still toggles it.