markdraw
An Excalidraw-inspired drawing widget for Flutter with a human-readable markdown serialization format.
Cross-platform: iOS, Android, Web, macOS, Windows, Linux.
Features
- Hand-drawn aesthetic powered by
rough_flutter - Full element set: rectangles, ellipses, diamonds, lines, arrows, freedraw, text, images, frames
- Arrow binding with elbow routing
- Element grouping, locking, and frames with clip regions
- Selection, multi-select, resize, rotate, and nudge
- Undo/redo with drag coalescing
- Copy/paste via system clipboard
- Export to PNG and SVG (with embedded
.markdrawfor round-trip) - Excalidraw JSON import/export (
.excalidrawand.excalidrawlib) - Reusable element libraries (
.markdrawlib) - Google Fonts integration with bundled Excalifont
- Responsive layout: desktop toolbar + side panels, compact bottom bar + sheets
- Keyboard shortcuts for every tool
- Pinch-to-zoom and scroll-wheel zoom
- Dark mode theming
- Live markdown split-pane editor with bidirectional sync
- Human-readable
.markdrawfile format (git-friendly, diffable)
Quick Start
A minimal drawing canvas in six lines:
import 'package:flutter/material.dart' hide Element, SelectionOverlay;
import 'package:markdraw/markdraw.dart' hide TextAlign;
void main() {
runApp(MarkdrawApp(
home: (context, themeMode, onThemeModeChanged) => MarkdrawEditor(
currentThemeMode: themeMode,
onThemeModeChanged: onThemeModeChanged,
),
));
}
This gives you a full editor with toolbar, property panel, zoom controls, keyboard shortcuts, and undo/redo — no file I/O wiring needed.
Full Example with File I/O
The example app adds file open/save, PNG/SVG export, image import, and library management via MarkdrawFileHandler:
import 'package:flutter/material.dart' hide Element, SelectionOverlay;
import 'package:markdraw/markdraw.dart' hide TextAlign;
void main() {
runApp(MarkdrawApp(
home: (context, themeMode, onThemeModeChanged) => _CanvasPage(
themeMode: themeMode,
onThemeModeChanged: onThemeModeChanged,
),
));
}
class _CanvasPage extends StatefulWidget {
const _CanvasPage({
required this.themeMode,
required this.onThemeModeChanged,
});
final ThemeMode themeMode;
final void Function(ThemeMode) onThemeModeChanged;
@override
State<_CanvasPage> createState() => _CanvasPageState();
}
class _CanvasPageState extends State<_CanvasPage> {
final _controller = MarkdrawController();
late final _files = MarkdrawFileHandler(controller: _controller);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MarkdrawEditor(
controller: _controller,
onSave: _files.save,
onSaveAs: _files.saveAs,
onOpen: _files.open,
onExportPng: _files.exportPng,
onExportSvg: _files.exportSvg,
onImportImage: () => _files.importImage(context),
onImportLibrary: _files.importLibrary,
onExportLibrary: _files.exportLibrary,
onThemeModeChanged: widget.onThemeModeChanged,
currentThemeMode: widget.themeMode,
);
}
}
API Overview
MarkdrawEditor
The main widget. All callbacks are optional — omit any you don't need.
| Parameter | Type | Description |
|---|---|---|
controller |
MarkdrawController? |
External controller; one is created internally if omitted |
config |
MarkdrawEditorConfig |
UI and behavior configuration |
onSave |
VoidCallback? |
Save to current file |
onSaveAs |
VoidCallback? |
Save-as dialog |
onOpen |
VoidCallback? |
Open file dialog |
onExportPng |
VoidCallback? |
Export as PNG |
onExportSvg |
VoidCallback? |
Export as SVG |
onImportImage |
VoidCallback? |
Import an image |
onImportLibrary |
VoidCallback? |
Import a library file |
onExportLibrary |
VoidCallback? |
Export the library |
onThemeModeChanged |
void Function(ThemeMode)? |
Theme toggle callback |
currentThemeMode |
ThemeMode? |
Current theme mode |
onSceneChanged |
void Function(Scene)? |
Called on every scene mutation |
MarkdrawController
Manages editor state, tools, history, and serialization. Create one to control the editor programmatically:
final controller = MarkdrawController();
// Switch tools
controller.switchTool(ToolType.rectangle);
// Undo/redo
controller.undo();
controller.redo();
// Zoom
controller.zoomIn(canvasSize);
controller.zoomToFit(canvasSize);
controller.resetZoom();
// Serialize
final markdraw = controller.serializeScene();
final json = controller.serializeScene(format: DocumentFormat.excalidraw);
// Load
controller.loadFromContent(content, 'drawing.markdraw');
// Export
final pngBytes = await controller.exportPng();
final svgString = controller.exportSvg();
// Listen for changes
controller.addListener(() { /* rebuild */ });
MarkdrawEditorConfig
Immutable configuration passed to the editor or controller:
MarkdrawEditorConfig(
tools: [ToolType.select, ToolType.rectangle, ToolType.text],
initialBackground: '#f5f5f5',
showToolbar: true,
showPropertyPanel: true,
showZoomControls: true,
showHelpButton: true,
showLibraryPanel: true,
showMarkdownButton: true,
showMenu: true,
compactBreakpoint: 600.0,
minZoom: 0.1,
maxZoom: 30.0,
)
MarkdrawFileHandler
Wires file_picker and platform I/O to a controller. Provides save, saveAs, open, exportPng, exportSvg, importImage, importLibrary, and exportLibrary methods.
MarkdrawApp
A lightweight MaterialApp wrapper that manages ThemeMode state and passes it to your builder:
MarkdrawApp(
title: 'My Drawing App',
initialThemeMode: ThemeMode.system,
colorSchemeSeed: Colors.blue,
home: (context, themeMode, onThemeModeChanged) => YourWidget(...),
)
The .markdraw Format
Drawings are saved as markdown files with embedded sketch blocks — human-readable, git-friendly, and diffable:
---
markdraw: 1
background: "#ffffff"
grid: 20
---
# Architecture Overview
Here's how the services connect:
```sketch
rect "Auth Service" id=auth at 100,200 size 160x80 fill=#e3f2fd rounded
rect "API Gateway" id=gateway at 350,200 size 160x80 fill=#fff3e0 rounded
arrow from auth to gateway label="JWT tokens" stroke=dashed
ellipse "Database" id=db at 225,400 size 120x80 fill=#e8f5e9
arrow from gateway to db label="queries"
```
The auth service handles OAuth2 flows.
- Prose sections are preserved untouched (only
sketchblocks are parsed) - Elements map 1:1 to Excalidraw types
- IDs enable arrow binding and bound text
- Colors, stroke style, fill style, opacity, font, and lock state are inline properties
Excalidraw Interop
Import and export .excalidraw JSON files:
// Import
controller.loadFromContent(jsonString, 'drawing.excalidraw');
// Export
final json = controller.serializeScene(format: DocumentFormat.excalidraw);
Library files (.excalidrawlib) are also supported:
controller.importLibraryFromContent(content, 'shapes.excalidrawlib');
final libJson = controller.exportLibraryContent(
format: DocumentFormat.excalidrawLibrary,
);
Theming
MarkdrawApp manages ThemeMode state internally. Wire it to MarkdrawEditor via the builder:
MarkdrawApp(
home: (context, themeMode, onThemeModeChanged) => MarkdrawEditor(
currentThemeMode: themeMode,
onThemeModeChanged: onThemeModeChanged,
),
)
If you manage your own MaterialApp, pass ThemeMode and a toggle callback directly to MarkdrawEditor.
Platform Notes
The ios/, android/, macos/, windows/, and linux/ directories are gitignored. After cloning, run flutter create . to regenerate them, then apply these manual changes:
- iOS: Add
<key>UISupportsDocumentBrowser</key><true/>toios/Runner/Info.plist— required for file picker save dialogs to work correctly.
Keyboard Shortcuts
| Key | Tool / Action |
|---|---|
V |
Select |
R |
Rectangle |
E |
Ellipse |
D |
Diamond |
L |
Line |
A |
Arrow |
P |
Pencil (freedraw) |
T |
Text |
H |
Hand (pan) |
F |
Frame |
Ctrl+Z |
Undo |
Ctrl+Shift+Z |
Redo |
Ctrl+C / Ctrl+V |
Copy / Paste |
Ctrl+X |
Cut |
Ctrl+D |
Duplicate |
Ctrl+A |
Select all |
Ctrl+G |
Group |
Ctrl+Shift+G |
Ungroup |
Ctrl+Shift+L |
Toggle lock |
Delete / Backspace |
Delete selected |
Ctrl+S |
Save |
Ctrl+Shift+S |
Save as |
Ctrl+O |
Open |
Ctrl++ / Ctrl+- |
Zoom in / out |
Ctrl+0 |
Reset zoom |
Ctrl+Shift+1 |
Zoom to fit |
Ctrl+Shift+2 |
Zoom to selection |
? |
Help dialog |
Development
# Run all tests
flutter test
# Run tests and check output
flutter test 2>&1 > /tmp/test.txt
# Static analysis
flutter analyze
# Generate coverage report
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
Roadmap
See ROADMAP.md for the full development plan.
License
MIT — see LICENSE.
Libraries
- markdraw
- An Excalidraw-inspired drawing widget with human-readable markdown serialization.