FluentEditor

Fluent Editor

Flutter Version License Version Live Demo GitHub Sponsors Product Hunt

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_navigation
utils/cursor_utils
utils/fragment_operations
utils/handler_helpers
utils/list_marker_types
utils/node_operations
utils/resolve_selection
widgets/dialogs/author_info_dialog
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_color_button
widgets/editor/fluent_context_menu
widgets/editor/fluent_font_selector_widget
widgets/editor/fluent_font_size_selector_widget
widgets/editor/fluent_formatting_bar
widgets/editor/fluent_paragraph_spacing_button
widgets/editor/fluent_paragraph_spacing_dialog
widgets/editor/fluent_paragraph_style_selector
widgets/editor/fluent_positioned_sidebar
widgets/editor/fluent_toolbar_widget
widgets/editor/fluent_unified_sidebar
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_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