Mobile Development 10 min read

Design and Implementation of a High‑Performance Flutter Rich Text Editor (Mural)

The article details how the Xianyu team built a feature‑complete, extensible, high‑performance Flutter rich‑text editor called Mural by defining a Slate‑inspired protocol layer, mapping it to a custom widget/render object tree, handling cursor and WidgetSpan selection, using diff‑based updates, and providing a plug‑in architecture for extensions.

Xianyu Technology
Xianyu Technology
Xianyu Technology
Design and Implementation of a High‑Performance Flutter Rich Text Editor (Mural)

This article series introduces the design and implementation of a feature‑complete, extensible, and high‑performance rich‑text editor built with Flutter, covering the protocol layer, rendering layer, custom extensions, and performance optimizations encountered by the Xianyu team.

Protocol layer : Inspired by Slate, the editor defines a robust protocol with concepts such as nested Model, Operation, and Normalizing. These abstractions provide a solid foundation for manipulating rich‑text structures.

Rendering layer : The protocol model is transformed into a Flutter widget tree. The layer handles selection, cursor calculation, gesture and keyboard interaction. For the native TextField , the key widgets are TextSelectionGestureDetector and EditableText , which build a nested widget tree and render text via RenderEditable .

Mural rendering : Mural follows the same overall design but replaces EditableText with MuralEditable . Each element maps to a widget, and its RenderObject implements the abstract class RenderEditorInlineBox . This enables independent rendering of each element.

Cursor and selection rendering : Handling multiple elements requires a three‑step process: (1) Convert the global tap position to a local position using globalToLocal and locate the child element; (2) Use getPositionForOffset on the element’s RenderEditorInlineBox to obtain a TextPosition ; (3) Translate the TextPosition into the protocol’s Path and offset (e.g., Path [0,2] , offset 2 ) and compute the visual cursor offset via getOffsetForCaret .

Supporting WidgetSpan : Custom emojis are rendered as WidgetSpan . In edit mode they are treated as inline‑and‑void elements, with isInline and isVoid returning true . The editor synchronizes the TextValue to the native side using the placeholder character \uFFFC .

Cursor/selection TextBox calculation : To avoid zero‑offset issues when WidgetSpan is present, the editor overrides codeUnitAtVisitor and getSpanForPositionVisitor in the text layout logic.

Keyboard interaction : Input events arrive via TextInputClient.updateEditingState (or updateFloatingCursor on iOS). Instead of rebuilding the entire model on each change, a diff‑based approach records operations and applies only the necessary updates, preventing performance degradation.

Extension capabilities : The editor provides a plug‑in architecture. A custom theme node is implemented by defining a new element and a corresponding normalizer. An undo plug‑in rewrites the Operation.apply method to record history, implements Operation.reverse , and replays reverse operations to restore previous states.

Conclusion : The two‑part series covered the protocol and rendering layers of a Flutter rich‑text editor. Future articles will explore experience‑level optimizations and additional challenges.

FlutterRenderingprotocolCursorMuralRich Text EditorWidgetSpan
Xianyu Technology
Written by

Xianyu Technology

Official account of the Xianyu technology team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.