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.
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.
Xianyu Technology
Official account of the Xianyu technology team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.