quds_office_editor
Embeddable Word, Excel, PowerPoint, and PDF surfaces for Flutter — painted as
custom RenderBox editors, not TextField, ListView, or InteractiveViewer.
This is the interaction half of
Quds Office Suite.
Documents, formulas, pagination, PdfFile, and pdf_widgets live in
quds_office_engine
(re-exported from this package).
You draw your ribbon, file menu, and window chrome. This package paints the document — Office or PDF.

Why a custom RenderBox
A real Office canvas is one engine: caret, BiDi, IME, pagination, tables, selection, and zoom must agree on the same coordinates.
Flutter’s text widgets are excellent for forms. They are the wrong primitive for a paginated Word page, a virtualized sheet, or a slide stage with motion.
| Surface widget | Controller | What it paints |
|---|---|---|
QudsWordEditor |
WordEditorController |
Paginated pages, caret, tables, comments, headers/footers, OMML, pictures |
QudsSheetEditor |
SheetEditorController |
Virtualized grid, formula bar, freeze panes, fill handle, charts |
QudsSlideEditor |
SlideEditorController |
Slide stage, transform handles, snap guides, animations, slideshow |
QudsPdfViewer |
PdfViewerController |
Read-only PDF pages, zoom, find, copy, follow links |
QudsPdfEditor |
PdfEditorController |
Same canvas plus markup, form fill, page ops, incremental save |
┌─ your Scaffold / desktop frame ──────────────────────────┐
│ toolbarBuilder (optional — your ribbon) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ QudsWordEditor / QudsSheetEditor / QudsSlideEditor │ │
│ │ RenderBox · IME · selection · undo │ │
│ └─────────────────────────────────────────────────────┘ │
│ statusBarBuilder (optional) │
└──────────────────────────────────────────────────────────┘
Languages and writing directions
The editor is built for many languages and many writing directions, not a single locale.
- IME composition — Latin, Arabic, Hebrew, CJK, and other system IMEs
compose on the custom caret. There is no
EditableTexton the page. - Logical caret + visual runs — the caret walks grapheme clusters; painting follows the engine’s UAX #9 BiDi and Arabic shaping.
- Paragraph and section direction — LTR, RTL, and mixed-direction
documents. Windows/Linux-style Ctrl+Shift side shortcuts flip paragraph
direction (
officeParagraphDirectionFromSides). - Sheet direction — worksheets can display right-to-left or left-to-right.
- Host chrome locale —
OfficeStringsis a typed string table. ShipOfficeStrings.english, use the bundled second locale, or pass your own translations. The canvas itself does not hardcode UI copy. - Surface direction —
OfficeSurfaceConfig.textDirectionsets the default for chrome and new content; users can still mix directions inside a file.
const OfficeSurfaceConfig(
textDirection: TextDirection.rtl,
strings: OfficeStrings.english, // or your own OfficeStrings(...)
);
The same document bytes open with the same layout in any host language. Locale only changes chrome labels, not pagination.
Install
dependencies:
quds_office_editor: ^0.3.0
flutter pub add quds_office_editor
Requires Flutter 3.44+ and Dart 3.12+. The engine comes along as
quds_office_engine: ^0.3.1.
Host fonts
The package ships Liberation Sans/Serif/Mono (PDF Standard 14 / Arial / Times / Courier substitutes) and Noto Naskh Arabic. Register them once before paint:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await OfficeHostFonts.ensureRegistered();
runApp(const MyApp());
}
Aliases such as Helvetica, Arial, Calibri, Times New Roman, and Courier New
resolve to those faces. When a PDF embeds its own /FontFile*, that face still
wins for glyph shape and advances.
Word
import 'dart:typed_data';
import 'package:flutter/widgets.dart';
import 'package:quds_office_editor/quds_office_editor.dart';
class WordHost extends StatefulWidget {
const WordHost({super.key, this.bytes});
final Uint8List? bytes;
@override
State<WordHost> createState() => _WordHostState();
}
class _WordHostState extends State<WordHost> {
late final WordEditorController controller = widget.bytes == null
? WordEditorController(
config: const OfficeSurfaceConfig(
textDirection: TextDirection.ltr,
strings: OfficeStrings.english,
theme: OfficeTheme.light,
),
)
: WordEditorController.fromBytes(widget.bytes!);
@override
void initState() {
super.initState();
if (widget.bytes == null) {
controller.insertHeading(text: 'New document');
}
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return QudsWordEditor(
controller: controller,
toolbarBuilder: (context, c) {
return const SizedBox.shrink(); // put your Home ribbon here
},
);
}
}
Save:
final Uint8List docx = controller.saveBytes();
The Word canvas supports:
- Logical caret + IME without
EditableText - Bold / italic / underline / color / highlight / superscript / subscript
- Alignment, lists, indent, line spacing, LTR / RTL sections
- Page size and orientation per section (portrait and landscape pages in one document; continuous column slices share one paper size)
- Horizontal pan and side gutters when a landscape page is wider than the view
- Interactive rulers (margins, indents, tabs)
- Tables (resize, select cells, merge), pictures, charts, diagrams
- Comments, footnotes/endnotes pane, hyperlinks, headers & footers
- Page breaks, section breaks, TOC
- OMML equations
- Find / replace, print, text statistics
- Undo / redo, copy / cut / paste
Orientation is a section property. Toggling landscape updates the caret section and its continuous siblings (a heading plus a two-column body), not a single laid-out page.
Excel
final controller = SheetEditorController.fromBytes(xlsxBytes);
QudsSheetEditor(
controller: controller,
frozenRows: 1,
frozenCols: 1,
);
The grid is virtualized (VirtualViewport) so large sheets stay light.
- A1 selection, fill handle, row/column insert-delete
- In-cell editor + formula bar (
showFormulaBar) - Formula engine from the engine package (
SUM,IF,VLOOKUP, …) - Freeze panes, RTL or LTR sheets, number formats
- Embedded charts from the selection
- Recalc on edit; isolate open for large workbooks
controller.recalculateWorkbook();
final Uint8List xlsx = controller.saveBytes();
PowerPoint
final controller = SlideEditorController.fromBytes(pptxBytes);
QudsSlideEditor(controller: controller);
Edit mode:
- Move / resize / rotate with transform handles
- Snap guides when edges align
- Shape text with the same inline editor used in cells
- Tables, pictures, charts, z-order
- Entrance / emphasis / exit / motion paths
- Slide transitions (fade, push, wipe, Morph, …)
Slideshow:
controller.startShow(from: 0); // F5 in the studio example
controller.showNext(); // click / →
controller.showPrevious(); // reverse the last motion
controller.endShow(); // Esc
showPrevious() reverses that animation or that transition — a fly-in
flies back out, a push slides back, a mid-fade retreats from the current
frame. It is not a jump to the previous slide’s final state.
PDF viewer and editor
QudsPdfViewer is display-only (viewing / selecting): zoom, find, copy,
outline jump, follow URI. It has no saveBytes.
QudsPdfEditor extends the same canvas: highlights, notes, ink, AcroForm
values, page insert/delete/rotate, XFDF, incremental save.
final viewer = PdfViewerController.fromBytes(pdfBytes);
QudsPdfViewer(controller: viewer);
final editor = PdfEditorController.fromBytes(pdfBytes);
editor.highlightSelection();
final Uint8List saved = editor.saveBytes();
The page is RenderPdfCanvas. Host chrome (File / View / Annotate / Form)
stays outside the widget. Body-text reflow is out of scope.
Theming, modes, and chrome
const OfficeSurfaceConfig(
mode: OfficeInteractionMode.editing, // or selecting, viewing
theme: OfficeTheme.dark,
textDirection: TextDirection.ltr,
strings: OfficeStrings.english,
showRulers: true,
showFormulaBar: true,
showGridlines: true,
showSlideHandles: true,
showFindChrome: false,
showNotesPane: false,
showNavigationPane: false,
enableUndo: true,
);
| Mode | Caret / IME | Selection | Mutations |
|---|---|---|---|
editing |
Yes | Yes | Yes |
selecting |
No | Yes | No |
viewing |
No | No | No |
OfficeTheme.light and OfficeTheme.dark ship as starting palettes. Host
chrome (ribbons, panes) is your Material or Cupertino. Only the page, grid,
and stage are custom paint.
OfficeStrings is a complete typed table (file, home, insert, layout, review,
view, find, notes, …). Provide another language by constructing
OfficeStrings(...). The bundled extras are a starting point, not a limit.
Density (OfficeDensity) and form factor (OfficeFormFactor) let a host
tighten chrome on phone vs desktop without changing the document model.
Load, save, progress
await controller.loadBytesAsync(
bytes,
password: password,
onProgress: (p) => setState(() => label = p.stage),
);
final Uint8List out = controller.saveBytes(password: optionalPassword);
Heavy opens run through OfficeIsolateOpen so a large deck does not freeze
the first frame. Password-protected OOXML packages open when a password is
supplied.
The engine’s PDF export is one call away from any controller’s model:
final Uint8List pdf = OfficePdfExport.fromBytes(controller.saveBytes());
What this package refuses to be
The document surface is not a stack of Flutter text widgets.
| Forbidden on the canvas | Why |
|---|---|
TextField / TextFormField / EditableText |
Caret, BiDi, and pagination must be one engine |
SelectableRegion |
Selection is owned by the controller |
ListView / SingleChildScrollView / InteractiveViewer |
Virtualization and zoom are custom |
Use those widgets in your toolbar. Do not drop them onto the page.
Studio example
A full workspace (File / Home / Insert / Layout / Review / View, sample
library, OS window frame) lives under example/:
cd packages/quds_office_editor/example
flutter run
See example/README.md for a guided tour of Word, Excel, and PowerPoint.
Architecture
flowchart TB
Host[Host chrome — ribbons, dialogs]
W[QudsWordEditor]
X[QudsSheetEditor]
P[QudsSlideEditor]
WC[WordEditorController]
XC[SheetEditorController]
PC[SlideEditorController]
Engine[quds_office_engine models + IO]
Host --> W & X & P
W --> WC --> Engine
X --> XC --> Engine
P --> PC --> Engine
| File | Role |
|---|---|
office_editors.dart |
Embeddable widgets + shortcuts + IME |
office_controller.dart |
Commands, selection, undo, load/save |
office_theme.dart |
Tokens, modes, localizable strings |
office_clipboard.dart |
OOXML-aware copy / cut / paste |
render_word_canvas.dart |
Paginated Word RenderBox |
render_sheet_grid.dart |
Virtualized sheet RenderBox |
render_slide_stage.dart |
Slide + slideshow RenderBox |
render_pdf_canvas.dart |
Shared PDF page RenderBox |
office_ruler.dart |
Interactive Word ruler |
word_notes_pane.dart |
Footnotes / endnotes host pane |
Design rules
- Engine owns the model — the editor never forks a second document format.
- RenderBox only on the canvas — no scrolling or text widgets as the page.
- Host owns chrome — ribbons, dialogs, and file pickers stay in your app.
- Type-safe commands — undoable mutations go through the controller, not ad-hoc widget state.
License
MIT. See LICENSE.
Libraries
- quds_office_editor
- Flutter RenderBox interactive engine for Quds Office Suite.