Fluent Editor
A powerful and feature-rich word processor for Flutter applications, inspired by nature 🍃.
Features
- Rich Text Editing: Bold, italic, underline, strikethrough, superscript, subscript, small caps
- Paragraph Styles: Headings (H1–H6), normal text, paragraph formatting
- Lists: Ordered and unordered lists with nested sublists and checkboxes
- Tables: Create and edit tables with cell spanning (colspan/rowspan)
- Images: Insert and resize images with inline and block positioning
- Links: Insert and manage hyperlinks
- Colors: Text color and highlight color support
- Alignment: Left, center, right, and justify text alignment
- Export: Export to DOCX, ODT, PDF, HTML, Markdown, and plain text
- Import: Import from DOCX, ODT, HTML, and Markdown
- Undo/Redo: Full undo/redo history with intelligent action grouping
- Selection: Mouse and keyboard selection support
- Word Count: Real-time word and character count
- Clipboard: Cut, copy, and paste with formatting support
- Plugin System: Extensible architecture for custom functionality
Tested On
- Web: Chrome
- Windows: Windows 11
- Linux: Ubuntu, Debian, Fedora
- macOS: macOS 13+
- iOS: iOS 15+
- Android: Android 12+
Plugins
| Plugin | Description |
|---|---|
fluent_editor_comments |
Comments and annotations plugin — anchor comments to text ranges, reply, resolve, and manage discussion threads. |
fluent_editor_review |
Track Changes / Review plugin — captures additions and deletions as reviewable suggestions with accept/reject workflow. |
fluent_editor_character_map |
Special character grid, math symbols, and emoji picker. |
Getting Started
Add Fluent Editor to your pubspec.yaml:
dependencies:
fluent_editor:
git:
url: https://github.com/exusr/fluent-editor.git
Basic Usage
import 'package:flutter/material.dart';
import 'package:fluent_editor/fluent_editor.dart';
class MyEditor extends StatefulWidget {
@override
State<MyEditor> createState() => _MyEditorState();
}
class _MyEditorState extends State<MyEditor> {
late final FluentDocument _document;
@override
void initState() {
super.initState();
_document = FluentDocument();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: FluentEditor(
document: _document,
toolbarMode: FluentToolbarMode.fixed,
labels: const FluentEditorLabels(), // English defaults
),
);
}
}
Usage with Plugins
Fluent Editor uses a plugin architecture where external libraries register themselves through the FluentEditorPlugin API. The core editor has zero dependencies on any plugin library.
Comments + Review (Track Changes)
import 'package:fluent_editor/fluent_editor.dart';
import 'package:fluent_editor_comments/fluent_editor_comments.dart';
import 'package:fluent_editor_review/fluent_editor_review.dart';
class EditorWithPlugins extends StatefulWidget {
@override
State<EditorWithPlugins> createState() => _EditorWithPluginsState();
}
class _EditorWithPluginsState extends State<EditorWithPlugins> {
late final FluentDocument _document;
final _commentProvider = FluentCommentProvider();
final _suggestionController = FluentSuggestionController();
@override
void initState() {
super.initState();
_document = FluentDocument();
_document.commentProvider = _commentProvider;
}
@override
void dispose() {
_commentProvider.dispose();
_suggestionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FluentEditor(
document: _document,
plugins: [
// Comments plugin — adds comment highlights and context menu actions
FluentCommentPlugin(provider: _commentProvider),
// Review plugin — adds Editing/Review mode toggle to the toolbar
FluentSuggestionPlugin(controller: _suggestionController),
],
// Bubble toolbar actions (e.g. "Add comment" on text selection)
bubbleActions: [
CommentBubbleAction(
document: _document,
provider: _commentProvider,
),
],
);
}
}
Unified Sidebar
When both plugins are registered, the editor automatically merges comments and suggestions into a single unified sidebar (FluentUnifiedSidebar). Items are displayed in document order and scroll-synced with the text:
FluentEditor(
document: _document,
plugins: [
FluentCommentPlugin(provider: _commentProvider),
FluentSuggestionPlugin(controller: _suggestionController),
],
// The sidebar is automatically resolved from registered plugins.
);
Localization
All UI labels are localizable. The core editor uses FluentEditorLabels, and each plugin uses its own labels class that falls back to the core labels:
FluentEditor(
document: _document,
labels: FluentEditorLabels(
file: 'Archivo',
edit: 'Editar',
insert: 'Insertar',
format: 'Formato',
sidebarTitle: 'Actividades y Revisiones',
emptySidebarMessage: 'No hay comentarios ni sugerencias.',
),
plugins: [
FluentCommentPlugin(
provider: _commentProvider,
labels: FluentCommentLabels(
addCommentLabel: 'Añadir comentario',
resolveButton: 'Resolver',
),
),
FluentSuggestionPlugin(
controller: _suggestionController,
labels: SuggestionLabels(
editingMode: 'Edición',
suggestingMode: 'Revisión',
additionLabel: 'Adición',
deletionLabel: 'Eliminación',
),
),
],
);
Creating a Document Programmatically
final document = FluentDocument();
// Add a heading
final heading = Paragraph()
..styleName = 'heading1'
..fragments = [Fragment('Annual Report')];
document.content.nodes.add(heading);
// Add a formatted paragraph
final p = Paragraph()
..fragments = [
Fragment('This is '),
Fragment('bold text')..bold = true,
Fragment(' and '),
Fragment('italic text.')..italic = true,
];
document.content.nodes.add(p);
// Add a list
final listItem1 = ListItem(bulletType: 'ordered', indexList: [1])
..children = [Paragraph()..fragments = [Fragment('First item')]];
final listItem2 = ListItem(bulletType: 'ordered', indexList: [2])
..children = [Paragraph()..fragments = [Fragment('Second item')]];
final list = FluentList(listType: 'ordered')
..items = [listItem1, listItem2];
document.content.nodes.add(list);
// Add a 2×2 table
final table = FluentTable()
..rows = [
FluentRow()
..cells = [
FluentCell()..fragments = [Fragment('Header 1')],
FluentCell()..fragments = [Fragment('Header 2')],
],
FluentRow()
..cells = [
FluentCell()..fragments = [Fragment('Data 1')],
FluentCell()..fragments = [Fragment('Data 2')],
],
];
document.content.nodes.add(table);
// Add an image
final image = FluentImage(src: 'https://example.com/image.png')
..width = 300
..height = 200;
document.content.nodes.add(image);
// Add a link inside a paragraph
final link = Link(url: 'https://example.com')
..fragments = [Fragment('Click here')];
final linkParagraph = Paragraph()
..fragments = [Fragment('Visit '), link, Fragment(' for more info')];
document.content.nodes.add(linkParagraph);
// Add a horizontal rule
document.content.nodes.add(HorizontalRule());
Plugin System
The plugin API allows external libraries to extend Fluent Editor without introducing compile-time coupling. See DOCUMENTATION.md for the full API reference.
Creating a Custom Plugin
import 'package:fluent_editor/plugins/plugin_api.dart';
class MyPlugin extends FluentEditorPlugin {
@override
String get id => 'com.example.my_plugin';
@override
String get version => '1.0.0';
// Register custom toolbar buttons
@override
List<FluentUiContribution> get ui => [
FluentUiContribution(
id: 'my_plugin.toolbar_button',
location: FluentPluginUiLocation.toolbar,
order: 5,
builder: (context, document) => IconButton(
icon: const Icon(Icons.star),
onPressed: () { /* custom action */ },
),
),
];
// Intercept text operations (return true = handled)
@override
bool onInsertCharacter(String character, FluentDocument document) => false;
@override
bool onBackspace(FluentDocument document, {bool ctrl = false, bool lineStart = false}) => false;
// Contribute sidebar items
@override
List<FluentSidebarItem> buildSidebarItems(BuildContext context, FluentDocument document) => [];
// Lifecycle hooks
@override
void attach(FluentPluginContext context) { /* called on registration */ }
@override
void detach(FluentPluginContext context) { /* called on disposal */ }
}
Working with Selection
final cursor = document.cursor;
if (cursor.isCollapsed) {
print('Cursor at ${cursor.anchorId}:${cursor.anchorOffset}');
} else {
print('Selection from ${cursor.anchorId}:${cursor.anchorOffset} '
'to ${cursor.focusId}:${cursor.focusOffset}');
}
// Move the cursor
cursor.moveTo(fragmentId, offset);
// Extend selection
cursor.focusTo(targetFragmentId, targetOffset);
Additional Information
Documentation
For detailed technical documentation, see DOCUMENTATION.md.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License — see the LICENSE file for details.
Issues
If you find any bugs or have feature requests, please open an issue on GitHub.
Libraries
- comments/comment_provider
- controllers/document_language_controller
- core/constants
- core/paragraph_registry
- core/types
- cursor
- factories
- fluent_document
- fluent_editor
- handlers/dialog_presenter
- handlers/event_handler
- handlers/handle_arrow_key
- handlers/handle_backspace
- handlers/handle_clear_formatting
- handlers/handle_clipboard
- handlers/handle_delete
- handlers/handle_enter
- handlers/handle_font_family
- handlers/handle_font_size
- handlers/handle_formats
- handlers/handle_highlight_color
- handlers/handle_insert_character
- handlers/handle_insert_node
- handlers/handle_paragraph_spacing
- handlers/handle_paragraph_style
- handlers/handle_replace_selection
- handlers/handle_select_all
- handlers/handle_tab
- handlers/handle_text_align
- handlers/handle_text_color
- input/composition_detector_stub
- input/composition_detector_web
- input/ime_connection_manager
- input/ime_handler
- input/ime_state_manager
- input/utils/ime_text_utils
- localization/fluent_editor_labels
- models/document_language
- plugins/builtin_plugin
- plugins/plugin_api
- renderers/render_fluent_node
- renderers/render_fragment
- renderers/render_paragraph
- renderers/style_hook
- selection_manager
- services/docx_exporter
- services/export_service
- services/export_service_web_html
- services/export_service_web_stub
- services/import_docx_service
- services/import_html_service
- services/import_markdown_service
- services/import_odt_service
- services/import_service
- services/odt_exporter
- services/pdf_font_provider
- styles
- suggestions/suggestion_provider
- suggestions/suggestion_style_hook
- undo_redo/document_delta
- undo_redo/undo_redo_manager
- utils/color_utils
- utils/cursor_utils
- utils/fragment_operations
- utils/handler_helpers
- utils/list_marker_types
- utils/node_operations
- utils/resolve_selection
- widgets/dialogs/image_drop_stub
- widgets/dialogs/image_drop_web
- widgets/dialogs/image_insert_dialog
- widgets/dialogs/list_marker_dialog
- widgets/editor/fluent_bubble_toolbar
- widgets/editor/fluent_font_selector_widget
- widgets/editor/fluent_font_size_selector_widget
- widgets/editor/fluent_formatting_bar
- widgets/editor/fluent_link_dialog
- widgets/editor/fluent_paragraph_spacing_dialog
- widgets/editor/fluent_paragraph_style_selector
- widgets/editor/fluent_toolbar_widget
- widgets/fluent_document_widget
- widgets/node_widget_builder
- widgets/nodes/fluent_cell_widget
- widgets/nodes/fluent_fragment_widget
- widgets/nodes/fluent_hr_widget
- widgets/nodes/fluent_image_widget
- widgets/nodes/fluent_link_widget
- widgets/nodes/fluent_list_item_widget
- widgets/nodes/fluent_list_widget
- widgets/nodes/fluent_paragraph_widget
- widgets/nodes/fluent_table_widget
- widgets/nodes/virtualized_selectable_area
- widgets/shared/color_picker_widgets
- widgets/toolbar/language_selector_widget