Skip to content

Rich Text extension ​

Every text slot is filled by exactly one text definition: a shape's ShapeText part, a table's cells, a connector's labels and a standalone text element. There are two:

  • PlainTextDefinition (type text, in core): runs and paragraph breaks, with inline font, size and color. No lists, headings or marks.
  • RichTextDefinition (type richText, from the optional Rich Text extension): marks, headings, lists, checklists and font sizing, as its RichTextGrammar allows.

Both take maxLines: 1 is a single line (Enter finishes editing), N allows at most N typed paragraphs (soft wrapping does not count), and 0, the default, is unlimited. Both share the engine's selection, transforms, history and persistence.

Install Rich Text explicitly ​

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

final controller = DiagramController(
  configuration: DiagramConfiguration(
    extensions: [EditorExtension(), RichTextExtension()],
  ),
);
final id = controller.createRichText(
  at: const DiagramPoint(200, 120),
  text: 'Edit this Rich Text',
);
// Mount DiagramCanvas(controller: controller).
// Dispose the controller when its owner is disposed.

The Flutter foundation defaults to Plain Text. The Rich Text package is an optional commercial extension; it is not a runtime dependency of Flutter or core. RichTextExtension({grammar = const RichTextGrammar(), tool = true}), id vyuh.rich_text, contributes the rich text type allowing grammar through textTypes, and its creation tool unless tool is false. Because text elements are a core concept, installing the rich text type is what makes standalone richText elements: their element type is the text type's name. The core card, standard shapes, connector labels and the default text element stay plain until an application reconfigures them, as Rich text in shapes shows.

Compose Rich Text inside a shape ​

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

final note = ShapeDefinition(
  type: 'app.note',
  body: ShapeColumn([
    const ShapeText(
      'title',
      text: 'Plain title',
      textType: PlainTextDefinition(maxLines: 1),
    ),
    const ShapeText(
      'body',
      text: 'Formatted body',
      textType: RichTextDefinition(grammar: RichTextGrammar(headings: false)),
    ),
  ]),
);
final controller = DiagramController(
  configuration: DiagramConfiguration(
    extensions: [
      EditorExtension(),
      RichTextExtension(),
      DiagramExtension(id: 'app.notes', shapes: [note]),
    ],
  ),
);

Each slot selects its own text and editing behavior; a slot without textType holds plain text. A table chooses the text of all its cells with ShapeTable(..., textType: ...), and a connector its labels with ConnectorLabelingCapability(textType: ...). Admission rejects paragraphs a slot's text does not admit, such as a list item in a plain slot or a second paragraph in a single-line slot.

Rich text in shapes ​

A shape declares its text parts; which text fills them is configuration. ShapeDefinition.withText(text, {slot}) returns a copy of a shape whose text parts hold text: every text part, or only the one filling slot. The shape never checks what is installed and never changes by itself. The copy keeps its type, so an application that installs it in its own extension replaces the definition it was made from, a core shape's included:

dart
const rich = RichTextDefinition();
final controller = DiagramController(
  configuration: DiagramConfiguration(
    extensions: [
      EditorExtension(),
      RichTextExtension(),
      DiagramExtension(
        id: 'app.rich_shapes',
        shapes: [
          ShapeLibraries.standard.shape(Shapes.comment).withText(rich),
          ShapeLibraries.standard.shape(Shapes.rectangle).withText(rich),
          // Only the card's description; its title stays one plain line.
          ShapeLibraries.standard
              .shape(Shapes.card)
              .withText(rich, slot: 'body'),
        ],
      ),
    ],
  ),
);

Comments, rectangles and cards then take headings, marks, lists and checklists through the same shortcuts, input rules and inspector controls as standalone rich text, and the Comment tool creates rich comments. An application's own comment-like shape works the same way, either declaring ShapeText('body', textType: const RichTextDefinition()) or configuring a plain declaration with withText:

dart
final note = ShapeDefinition(
  type: 'app.note',
  name: 'Note',
  body: const ShapeText(
    'body',
    verticalAlignment: DiagramTextVerticalAlignment.top,
    placeholder: 'Write a note…',
  ),
);

DiagramExtension(
  id: 'app.notes',
  shapes: [note.withText(const RichTextDefinition())],
);

The rules:

  • Replacing a core shape. An application extension's definition for a core shape type replaces the core default. Two application extensions defining the same type still fail assembly.
  • Rich text must be installed. Every text type a shape, table, connector label or text element holds must be installed; RichTextExtension() installs rich text, with its codec, verbs and tool. A shape configured for rich text without it fails assembly with an ArgumentError naming the shape and slot.
  • Saved documents. A rich shape persists paragraph and run attributes in the rich text envelope, {"type": "richText", "version": 1, "data": …}. Plain text is rich text without attributes, so documents saved with plain comments load unchanged into rich comments and save back byte for byte. The reverse is strict, as JSON loading is everywhere: rich attributes in a shape configured for plain text are rejected, never silently dropped. Without RichTextExtension() decoding throws a FormatException (no codec for richText); with it, admitting the document throws a StateError because the plain slot does not admit the attributes.

What the Rich Text definition contributes ​

RichTextDefinition owns everything rich about the slots it fills:

  • RichTextGrammar: which marks, headings, lists, checklists and font sizes the slot allows.
  • Keyboard shortcuts for marks, sizes, headings and lists, and Tab to nest list items.
  • Input rules that turn a typed - , * , 1. or [] into a list item.
  • Paragraph editing: Enter continues or exits a list, Backspace unlists an empty item.
  • List and checklist markers, heading presentation and accessible descriptions.
  • Inspector controls for marks, headings and lists.
  • Story verbs: setMark, setHeading, setList, changeIndent, adjustFontSize, clearFormatting and setChecked, declared by RichTextDefinition.storyVerbs. Every element holding rich text offers them, including shapes whose table cells are rich, without registering anything on an extension.

Standalone text elements share the core's standard placeholder, sizes and behavior. For text with its own sizes or behavior, such as ShapeBehavior(rotatable: false, resizable: false) to remove rotation and resize handles, place a rich ShapeText in a shape instead. See shape behavior.

Why there is no separate Rich Text Flutter package ​

Formatting changes portable text runs and paragraphs; it is not a Flutter widget. The text definition decides what a key chord means. The shared Flutter adapter paints the resulting text, manages selection and IME, translates physical keys, and offers each chord to the text filling the edited slot. Plain text handles no formatting chords.

The Rich Text extension needs no custom painter or overlay, so it has no separate Flutter extension. A future floating formatting toolbar, custom popup, or Flutter-specific overlay would contribute a DiagramFlutterExtension. That UI belongs in an optional Flutter/UI package; model formatting rules remain in the pure Dart Rich Text extension. Not every extension requires a second package.

JSON persists authored text data. SVG consumes the shared resolved text output. Neither format requires a Flutter widget to interpret the Rich Text definition.

Editing verbs ​

Perform range and paragraph formatting verbs on the controller, by name or through the typed RichTextVerbs extension on DiagramController, available once the Rich Text package is imported. See the text reference for exact signatures, scope and denial handling. A rich verb is denied unless every touched paragraph is in a rich text slot whose grammar admits the result. Installing Rich Text does not turn any plain slot into Rich Text.

VerbArgumentsSymmetry
setMarkmark: TextMark, on: boolsetter
setHeadingheading: HeadingLevelsetter
setListkind: ParagraphKind; ParagraphKind.paragraph removes the listsetter
changeIndentdelta: intsetter
adjustFontSizedelta: doublesetter
clearFormattingnonederived, reversed by replaceText
setCheckedblockId, checked: boolsetter

Every verb except setChecked also accepts storyId, start and end. There are no toggle verbs: a toggle control performs the setter with the opposite of the current value, such as setMark(..., on: !isBold).

dart
controller.edits.setMark(elementId: id, mark: TextMark.bold, on: true);
controller.edits.perform([id], 'setHeading', {'heading': HeadingLevel.h2});

Text inside any shape ​

Named ShapeText slots let a card have separate title and body regions while keeping one canonical story. A slot whose text has maxLines: 1 holds one paragraph and exits editing on Enter; a rich single-line slot accepts inline formatting only. A multiline rich slot supports paragraphs, nested ordered/unordered lists and checklists. Default line height is 1.2. Wrapped visual lines share that line height; successive authored blocks have a separate 6-unit after-gap. The final block adds no trailing gap to the text box. Empty paragraphs retain the same line and caret geometry as populated paragraphs.

The platform keyboard follows the active region: single-line slots request a text keyboard with a Done action; multiline slots request a multiline keyboard with a Newline action. Done exits a single-line slot without inserting a paragraph. Moving between slots updates the existing IME connection; ordinary caret movement does not reconfigure it. These platform-contract checks do not certify real-device mobile selection, keyboard viewport handling or IME behavior.

Inside an active text region, touch-and-hold selects a word; dragging after the hold extends selection by whole words using the same resolved geometry as mouse selection. A finger move before the hold threshold does not perform mouse-style drag selection. A second finger transfers control to canvas navigation and exits text editing through the shared lifecycle. Selection handles, a mobile clipboard menu and keyboard occlusion remain open; touch navigation and text selection still need real-device validation.

Text reflows in each declared region. The definition's text sizing policy decides whether the owner grows or retains its bounds. Display, selection and editing consume the same resolved lines rather than measuring a separate text widget.

The rich text model ​

The text model in vyuh_diagram_types is generic. A TextParagraph holds runs, alignment, direction, role and slot, plus optional extension-owned TextParagraphAttributes. A TextRun holds text, font family, size and color, plus optional TextRunAttributes, which contribute to the portable TextRunAppearance (weight, italic, underline, strikethrough and script) that shaping, painting and export read.

Rich text owns its structure and marks. ParagraphKind, HeadingLevel, TextMark and DiagramListMarkerStyle come from the Rich Text package's main library, stored as RichParagraphAttributes and RichRunAttributes. Code that builds or reads rich paragraphs directly imports package:vyuh_diagram_extension_rich_text/extension_api.dart, which holds the attributes, the builders below, RichTextFormatting, RichTextMarkers, RichTextCodec, richTextVerbs, richTextProperties and RichTextIcons:

APIPurpose
richParagraph(id:, runs:, kind:, heading:, nestingLevel:, checked:, markerStyle:, ...)A paragraph with rich structure; all-default structure carries no attributes.
richRun(text, marks: {...}, fontFamily:, fontSize:, color:)A run with marks; no marks carries no attributes.
RichTextParagraph on TextParagraphkind, heading, nestingLevel, checked, markerStyle and copyWithRich(...).
RichTextRun on TextRunmarks and withMarks(...).
RichTextCodecPersists the attributes; RichTextDefinition.codec returns it.
dart
import 'package:vyuh_diagram_extension_rich_text/extension_api.dart';
import 'package:vyuh_diagram_extension_rich_text/vyuh_diagram_extension_rich_text.dart';

final item = richParagraph(
  id: 'item',
  kind: ParagraphKind.checklistItem,
  runs: [richRun('Pack lunch', marks: {TextMark.bold})],
);
final checked = item.copyWithRich(checked: true);

Plain text admits no attributes. Every installed text definition's codec joins DiagramExtensionRegistry.codecs, so saving through controller.save(DiagramJsonCodec.new) writes rich attributes in a {type, version, data} envelope and reopening requires Rich Text to be installed.

TextRun ​

dart
TextRun(
  String text, {
  TextRunAttributes? attributes,
  String? fontFamily,
  double? fontSize,
  DiagramColor? color,
})
PropertyDefaultMeaning
textRequiredInline text, with UTF-16 offsets used by editing verbs.
attributesnullExtension-owned inline presentation, such as rich text's marks.
fontFamilynullInherits the renderer's font family.
fontSizenullUses the standard diagram font size.
colornullInherits the text region foreground; an explicit color paints the glyphs, not the owner rectangle.

copyWith preserves omitted properties. Explicit null clears fontFamily, fontSize, or color back to inheritance. Colors persist through JSON, text splitting, merging, and paragraph editing. Clear formatting also clears the explicit color.

Inspector formatting follows the current selection across all text owners:

SelectionInline properties (font, size, color, marks)Paragraph properties
Text rangeOnly selected characters, including ranges across blocksBlocks touched by the range
Caret while editingNext typed characters; existing content is unchangedThe caret's block
Element or connector labelThe whole storyAll applicable blocks; titles retain their single-line contract

Formatting is stored in canonical runs and blocks and survives JSON export/import. Caret-only typing preferences belong to the mounted text editor until text is inserted; they are not serialized as document content.

Paragraph verbs ​

For inline formatting in rich text, use controller.edits.setMark(elementId: id, mark: TextMark.bold, on: true) or controller.edits.clearFormatting(elementId: id). Both accept optional storyId, start = 0, and end arguments. Omit the range to affect the story's text, or supply UTF-16 offsets to format selected characters only. Range endpoints may be supplied in either order and are clamped to the story. Unselected runs and paragraph structure are preserved. Clear formatting removes explicit marks, font, size, and color from the selected text. A collapsed range does not change document formatting or configure future typing.

Format paragraphs through controller verbs without building replacement stories. These verbs work for shape text, text boxes, and connector labels.

VerbFormatting argumentPurpose
setTextAlignmentpositional ParagraphAlignmentAlign ordinary paragraphs.
setTextDirectionpositional ParagraphDirectionSet reading direction.
setListkind: ParagraphKindMake ordered, unordered, or checklist items, or ordinary paragraphs with ParagraphKind.paragraph (rich text).
changeIndentdelta: intAdjust list nesting (rich text).

Each requires the element ID (positional for the core verbs, elementId: for the rich verbs) and accepts an optional storyId, start = 0, and optional end UTF-16 offsets. Omitted end reaches the story's end. A range ending at the next paragraph's start excludes that paragraph. Lists retain fixed alignment.

dart
controller.setList(
  elementId: textId,
  kind: ParagraphKind.checklistItem,
);

For shapes, text boxes, and connectors with one label, the story is inferred. Supply storyId for a connector with multiple labels; omitting it denies the edit without changing any label. The same inference applies to setChecked, which still requires blockId.

Verbs return an admission result and record a changed operation as one undo entry. Locked or unavailable targets are denied, and invalid arguments are denied with DiagramDenialCode.unsupportedOperation. A list control performs setList with ParagraphKind.paragraph when the paragraphs are already of that kind, restoring ordinary paragraphs.

The replaceText verb ​

Publishes one replacement story through the same admission and history pipeline for shape text, standalone text and connector labels. Build after with the pure DiagramTextCommands helpers, then perform the verb by name with the current before story. It has no typed wrapper; setText is the typed text setter.

dart
controller.edits.perform([id], 'replaceText', {
  'before': before,
  'after': after,
  'label': 'Edit text', // Optional.
  'selectionAfter': selection, // Optional StorySelection.
});
ArgumentContract
idOwning element ID. For a connector label, this is the connector ID; before.id identifies its label story.
beforeCurrent canonical story. A stale story is denied with conflict without publishing or adding history.
afterReplacement with the same story ID. The target's paragraph/region grammar is validated before publication.
labelDescription of the history operation.
selectionAfterOptional text selection referring to this element and story. A different identity is denied with unsupportedOperation. Anchor and focus offsets are independently clamped to the resulting story's UTF-16 length, preserving range direction.

The result is an EditResult: missing, locked or noneditable targets are denied, as are edits rejected by host admission. requireAllowed() converts a denied result into DiagramChangeDenied. Calls through a disposed controller throw StateError. To combine it with other edits in one undo step, call it inside controller.batch. Rich text verbs such as clearFormatting build on it, so replaceText expresses their reverse.

The published document, resolved scene and mapped selection agree before observers receive the update. Undo/redo restores the admitted story and selection; offset correction does not create another history entry. This verb does not mount an editor, request keyboard focus or establish an IME connection.

See the range-formatting example for a complete call.

Lists and checklists ​

The paragraph inspector offers bullets, numbers and checklists. While editing, it changes the selected paragraphs; with an element selected, it changes the applicable story blocks. Single-line regions do not accept lists.

Input at the start of a paragraphResult
- or * Bulleted item
1. Numbered item
[] or [ ] Unchecked checklist item
Enter after an itemNext item of the same kind and nesting level; new checkboxes start unchecked
Tab / Shift+TabNest beneath the preceding item / outdent
Enter on an empty itemOutdent one level, or leave the list at the root
Click a checkboxToggle its checked state, with undo/redo

Prefixes also work before existing text: move to the start, type the marker and its trailing space, and the paragraph becomes a list item while retaining its text and formatting. The caret stays before that text. Immediate Backspace restores the literal marker. Typing elsewhere in an existing line does not automatically convert it. These shortcuts are shared by all multiline paragraph regions. Each slot's RichTextGrammar controls allowed list kinds; shortcut spellings are fixed.

List kinds can be mixed at nested levels. Kind, nesting depth, checked state, and formatted text runs persist through document JSON export/import.

Grammar configuration ​

Inline appearance is validated at the same story-admission boundary for every text owner. A run's fontSize may be null (inherit) or finite and strictly positive. Its color may be null or an unsigned 32-bit ARGB value, including transparent. These validity rules do not clamp authored text to inspector font size limits. Invalid edits and imports throw StateError before publication.

Direct paragraph-layout calls also validate their placement inputs before shaping: region slot IDs must be nonempty and unique, bounds must be finite with nonnegative dimensions, and block spacing and list indentation must be finite and nonnegative. Region foreground and default font size must be valid. Invalid layout arguments throw ArgumentError; duplicate slots are never silently merged.

RuleConfiguration
Plain versus rich text of a shape slotShapeText.textType
Plain versus rich text of table cellsShapeTable.textType
Plain versus rich text of connector labelsConnectorLabelingCapability.textType
Single-line, limited or unlimited paragraphsmaxLines on PlainTextDefinition or RichTextDefinition
Allowed marks, font sizes, headings, bulleted, numbered and checklist blocksRichTextDefinition.grammar (RichTextGrammar)
Maximum list nesting depthNo host-configurable limit yet
Input shortcut spellingsShared built-in mapping; not configurable yet

Each slot chooses its text; there is no registry-wide text grammar. A note with a plain title and a rich body that allows bullets and numbers but not checklists:

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

final note = ShapeDefinition(
  type: 'app.note',
  body: ShapeColumn([
    const ShapeText('title', textType: PlainTextDefinition(maxLines: 1)),
    const ShapeText(
      'body',
      textType: RichTextDefinition(
        grammar: RichTextGrammar(checklists: false),
      ),
    ),
  ]),
);

RichTextGrammar permissions (bullets, numberedLists, checklists, headings, marks and fontSizing) default to true. Ordinary paragraphs are always allowed. A single-line slot restricts rich content to one ordinary paragraph with inline formatting: no headings or list structure. fontSizing: false requires inherited run sizes, and marks: false requires unmarked runs. Plain text admits no marks, headings or lists. These restrictions apply to imports and verbs too. Disabled list shortcuts remain literal text and their inspector actions are hidden. Canonical admission rejects forbidden blocks from API edits or imports; it does not silently discard or flatten content. Maximum nesting depth and custom shortcut spellings are not yet configurable.

Standalone text ​

Per-slot grammar and headings ​

A single-line rich label can permit marks but require a fixed inherited font size:

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

const label = ShapeText(
  'label',
  textType: RichTextDefinition(
    maxLines: 1,
    grammar: RichTextGrammar(fontSizing: false),
  ),
);

Semantic headings use the heading of RichTextParagraph (HeadingLevel.none, h1–h6), independently of the shape's title/body slot. They are ordinary body paragraphs with an outline level, not list items. controller.setHeading applies a level to the selected range, or the whole story when the range is omitted. Inline marks and authored font sizes are retained; heading typography is derived during layout. Enter at a heading's end creates a body paragraph; splitting within a heading preserves its level. JSON, clipboard, and undo/redo retain heading levels.

While editing multiline text, use Cmd/Ctrl + Alt/Option + 1–6 for headings and 0 for body text. Cmd/Ctrl + Shift + 7/8/9 toggles numbers/bullets/checklists. Alignment uses Cmd/Ctrl + Alt/Option + Shift + L/E/R/J (start/center/end/justify) to leave browser location, search, reload, and hard reload shortcuts available. The demo header's keyboard icon lists marks, font-size, alignment, navigation, and canvas shortcuts. Unavailable formatting is hidden by the inspector and rejected by canonical admission.

TextElement owns text without a shape surface; its type is the name of an installed text type, text (plain) or richText. It extends FramedElement, sharing rectangle-based placement and transformation mechanics with shapes. It has no style, ports, path, image, or child-clipping properties. Text inside a card remains a story in that card's declared regions; both owners use the same paragraph verbs, resolved lines, selection, and IME pipeline.

dart
final caption = controller.createText(
  id: 'caption',
  at: const DiagramPoint(150, 100),
  size: const DiagramSize(220, 80),
  text: 'A little room for ideas',
);

controller.edits.createText returns an EditResult whose id is the element ID, and creates one undoable edit. Supply textStory instead of text when you need rich formatting. Read the resulting TextElement with controller.document.textById(caption); its stored fields are listed below.

Constructor fieldDefaultMeaning
idRequiredStable element identity.
frameRequiredRectangle in the owner's coordinate system.
textStoryRequiredCanonical paragraphs, lists, runs, and formatting.
rotation0Rotation in radians.
parentIdnullOptional containing element; text cannot own children itself.
zIndex0Stacking order.
textSizingModeDiagramTextSizingMode.fixedWidthWrapped fixed width or intrinsic autoWidth.

type defaults to Shapes.text and is fixed after creation. copyWith preserves identity and omitted fields; explicit parentId: null removes its parent. DiagramDocument.textById retrieves standalone text, while framedElementById retrieves either text or a shape.

The text tool starts a clicked box in auto-width mode; dragging creates a wrapping box. Auto-width does not limit paragraph count. Both modes support Enter-created paragraphs and use the configured sizing policy for height. Standalone text uses the core's standard geometry for every installed text type; see text elements.

Text verbs ​

Plain-text replacement ​

controller.edits.setText(String id, String text, {String? slot, String? storyId}) returns EditResult. Every text part offers setText; slot names the part, so setText(id, 'Orders', slot: 'title') performs title.setText. Table cells are not text parts: set their text with the table verb setCellText.

The verb retains the story ID and existing paragraph IDs where possible, allocates IDs for additional paragraphs, and resets marks, lists, direction and alignment to plain-paragraph defaults. Role and slot bindings are retained. CRLF, CR and LF split paragraphs; a trailing newline creates an empty paragraph. An empty string leaves one empty paragraph.

Omit storyId for a shape, standalone text or a connector with exactly one label. Specify it for a connector with multiple labels. Omit slot only when the selected story has one slot; otherwise supply an existing effective slot ID. Missing or ambiguous targets return unavailableTarget without publication. Other slots retain their original content. Registry-invalid content, such as a newline in a single-line slot, is rejected by admission. Undo restores the whole previous story in one step. Use replaceText when preserving rich formatting.

Bidirectional editing boundary

With a collapsed caret, plain Left/Right traverses shaped visual caret positions. Shift+Left/Right extends the range through those positions. Canonical selections retain anchor and focus affinity through platform input, selection clamping and history. Shaped caret geometry, canvas caret painting and IME placement consume it at soft wraps. Pointer caret placement and dragging, vertical navigation, and Home/End preserve wrap affinity. Plain range collapse chooses visual endpoints, and Home/End uses shaped visual edges. Alt/Ctrl+Left/Right follows shaped word intervals; adding Shift preserves the selection anchor. Word deletion remains a logical content operation. Paragraphs support explicit left-to-right and right-to-left base direction. Automatic direction detection is not provided.

Paragraph direction ​

Set TextParagraph.direction to ParagraphDirection.ltr (the default) or ParagraphDirection.rtl. TextStory.paragraph accepts the same direction argument. Direction is saved with the paragraph and inherited when Enter splits it. Start/end alignment and list gutters follow that direction.

dart
controller.setTextDirection(
  'notes',
  ParagraphDirection.rtl,
);
ArgumentMeaning
idRequired text owner ID (positional).
directionPositional. Explicit paragraph base direction. Inline mixed-direction runs still use normal text shaping.
storyIdOptional story ID. Inferred when the owner has one story; required to choose among multiple connector labels.
start, endOptional UTF-16 range; all touched paragraphs are formatted. Omit to format the whole story.

The verb uses normal validation and undo/redo. The inspector's Direction control formats selected paragraphs while editing, or the entire story otherwise.

The text verbs are ordinary controller verbs: they report every outcome as an EditResult, and throw StateError only after the controller is disposed. Pure Dart hosts call the same verbs. They edit the existing story in a shape, standalone text element, or connector label. The canvas paragraph editor and checklist interaction use these same verbs. Text layout comes from the diagram’s configured text backend; no canvas is required.

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

final before = DiagramTextTarget.resolve(
  controller.document.elementById(elementId),
  storyId,
)!.story;
final after = DiagramTextCommands.replaceText(story: before, blockIndex: 0, start: 0, end: before.blocks.first.plainText.length, text: 'Updated by the host');
final result = controller.edits.perform([elementId], 'replaceText', {'before': before, 'after': after});

replaceText arguments ​

Perform it by name with the owning element as the single target; it returns an EditResult.

ArgumentTypeRequired / defaultMeaning
beforeTextStoryRequiredExpected current story. A stale value is denied with conflict before publication.
afterTextStoryRequiredReplacement with the same story ID; registry and document validation still apply.
labelString'Edit text'History and change-event description.
selectionAfterStorySelection?nullOptional selection in this same owner/story; otherwise preserve current selection.

Missing or non-editable targets return a denied result. Locks and host admission remain effective. A changed story ID or a selection targeting a different story is denied with unsupportedOperation. Successful edits publish state and resolved geometry atomically and participate in undo/redo. Calls after the controller is disposed throw StateError.

Several stories in one undo step ​

Wrap several replaceText calls in controller.batch to publish them as one undo entry. A failed edit leaves the entire batch unpublished.

dart
controller.batch('Update invitation', () {
  controller.perform([titleOwner], 'replaceText', {'before': oldTitle, 'after': newTitle});
  controller.perform([bodyOwner], 'replaceText', {'before': oldBody, 'after': newBody});
});

Stories may belong to shapes, text elements or connector labels. A changed story identity or a stale expected story is denied; call requireAllowed() on each edit to withdraw the whole batch. Missing/noneditable targets and permission denials reject the whole batch. Registry grammar and fixed domain validation still apply to the complete result.

setChecked(...) → EditResult ​

ArgumentTypeMeaning
elementIdStringRequired text owner ID.
storyIdString?Inferred when the owner has one story; supply it for multiple connector labels.
blockIdStringRequired checklist-item ID, independent of line wrapping or block index.
checkedboolRequired checked state.

Sets the item's checked state as one undoable edit; clicking a checkbox performs it with the opposite of the current state. Missing, non-editable, non-checklist or plain targets return a denied result. This verb does not convert paragraphs into lists; use controller.setList for that. Use replaceText for custom structural or run-formatting edits that need an explicit replacement story.

Text measurement and baseline contracts are covered in API reference.

Font-size editing bounds ​

ShapeDefinition.fontSizeRange and ConnectorLabelingCapability.fontSizeRange declare one DiagramTextFontSizeRange; standalone text uses the core's standard range (DiagramTextFontSizeRange()). The resolved text target carries it to the inspector, selected-object shortcuts, selected-text formatting, and pending caret formatting.

MemberTypeContract
minimumdoubleDefaults to 8; finite and positive.
maximumdoubleDefaults to 96; finite and at least minimum.
constrain(size)double → doubleClamps a finite positive requested size to the range.
adjust(size, delta)(double, double) → doubleAdds a finite delta and clamps the result; the original size must be finite and positive.

Invalid declarations fail registry admission with ArgumentError. These are editing bounds: importing or resolving an already-authored size does not silently rewrite it. Run sizes still require finite positive values under document admission.

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

ShapeDefinition(
  type: 'app.note',
  body: const ShapeText('body', textType: RichTextDefinition()),
  fontSizeRange: const DiagramTextFontSizeRange(minimum: 12, maximum: 48),
)

Whole-story paragraph formatting ​

When end is omitted, paragraph verbs include the final empty paragraph, including one created by a trailing newline. An explicit end remains exclusive: a range ending at the next paragraph's start does not format that paragraph. This applies equally to Flutter and headless controllers.

Adjust font size ​

Use the same controller verb in Flutter or headless Dart:

dart
controller.adjustFontSize(
  elementId: textId,
  delta: 2,
  start: 0,
  end: 5,
);

The verb adjusts existing runs within the character range as one undo step. Omit the range to adjust all existing text. Runs without an explicit size start from the resolved target’s font size, and results are bounded by the target’s registered DiagramTextFontSizeRange. Connector labels normally use 14. Reversed ranges are normalized. A collapsed range does not set future typing style. Invalid numeric arguments are denied without publishing content or clearing redo.