A best-in-class, fully custom-drawn Rich Text Editor for .NET MAUI (and future .NET platforms). All rendering is performed on a SkiaSharp canvas — zero native UI controls. Architecture mirrors KumikoUI: platform-independent core → swappable drawing backend → platform host.
Workflow: Each phase is implemented, reviewed, and committed to git before proceeding to the next.
- Phase 0 — Repository & Solution Scaffolding
- Phase 1 — Core Abstractions & Document Model
- Phase 2 — Text Layout Engine & Basic Rendering
- Phase 3 — Caret, Cursor & Text Navigation
- Phase 4 — Text Selection
- Phase 5 — Text Input & Basic Editing
- Phase 6 — Clipboard Operations
- Phase 7 — Undo / Redo
- Phase 8 — Character Formatting
- Phase 9 — Paragraph Formatting
- Phase 10 — Font Manipulation
- Phase 11 — Hyperlinks
- Phase 12 — Image Support
- Phase 13 — Table Support
- Phase 14 — Command System & Events
- Phase 15 — Toolbar
- Phase 16 — Context Menu
- Phase 17 — HTML Import / Export
- Phase 18 — Theming & Styling
- Phase 19 — Accessibility, Localization & RTL
- Phase 20 — Testing Infrastructure
- Phase 21 — CI / CD Pipeline
- Phase 22 — Documentation & README
- Phase 23 — Sample Application
- Phase 24 — Performance & Polish
- Phase 25 — UX Parity Hardening (Post-Review)
- Phase 26 — Toolbar & Selection Quality Fixes (Post-Review II)
Set up the repository, solution file, project structure, and build infrastructure mirroring the KumikoUI pattern.
- Create
MintedTextEditor.slnwith solution folders:src,tests,samples,[ Solution ],Build - Create
Directory.Build.propswith shared NuGet metadata (Authors, License, SourceLink, PackageIcon, README) - Create
.gitignore(Visual Studio / .NET / macOS / JetBrains) - Create
LICENSE(MIT) - Create placeholder
README.md - Create
images/folder with logo placeholder
-
src/MintedTextEditor.Core/MintedTextEditor.Core.csproj—net10.0, platform-independent core library -
src/MintedTextEditor.SkiaSharp/MintedTextEditor.SkiaSharp.csproj—net10.0, SkiaSharp drawing implementation, references Core -
src/MintedTextEditor.Maui/MintedTextEditor.Maui.csproj— multi-targeted MAUI host (net10.0-android,net10.0-ios,net10.0-maccatalyst,net10.0-windows10.0.19041.0), references Core + SkiaSharp -
tests/MintedTextEditor.Core.Tests/MintedTextEditor.Core.Tests.csproj—net10.0, xUnit + coverlet -
samples/SampleApp.Maui/SampleApp.Maui.csproj— MAUI sample application
- Create empty marker classes / namespaces so all projects compile
- Verify
dotnet buildsucceeds for the entire solution
-
git init, initial commit with scaffolding
Define the platform-independent rendering abstractions and the rich-text document model that all higher layers build upon.
-
IDrawingContextinterface — mirrors KumikoUI pattern:DrawRect,FillRect,DrawRoundRect,FillRoundRect,DrawLine,DrawText,DrawTextInRect,MeasureText,GetFontMetrics,ClipRect,Save,Restore,Translate,DrawImage - Extended text drawing:
DrawTextRun(draw a run of styled text at a position),MeasureTextRun -
ITextShaperinterface — for advanced text shaping (ligatures, complex scripts); simple pass-through default implementation - Primitives:
EditorRect,EditorSize,EditorPoint,EditorColor,EditorPaint,EditorFont,EditorFontMetrics - Enums:
TextAlignment,VerticalAlignment,PaintStyle -
PaintCache— pooled/cached paint objects to avoid per-frame allocations
-
Document— root container; holds a list ofBlockelements -
Block(abstract) — base for block-level elements (paragraph, heading, list item, table, horizontal rule) -
Paragraph : Block— holds anInlineCollection(list ofInlineelements) -
Inline(abstract) — base for inline content -
TextRun : Inline— a contiguous span of text with a singleTextStyle -
ImageInline : Inline— an inline image with source, alt text, width, height -
HyperlinkInline : Inline— wraps child inlines with a URL and optional title -
LineBreak : Inline— explicit line break within a paragraph -
TextStyle— immutable value type: font family, font size, bold, italic, underline, strikethrough, subscript, superscript, text color, highlight color, baseline offset -
ParagraphStyle— alignment (left/center/right/justify), indent level, line spacing, space before/after, list type (none/bullet/number), heading level (0=none, 1-6), text direction (LTR/RTL) -
DocumentPosition— (blockIndex, inlineIndex, offset) for cursor addressing -
TextRange— (start: DocumentPosition, end: DocumentPosition) for selections - Document change notifications:
IDocumentChangeListener/DocumentChangedevent
-
DocumentEditor— stateless helper methods for safe document mutations:InsertText(Document, DocumentPosition, string, TextStyle) → DocumentPositionDeleteRange(Document, TextRange) → DocumentPositionSplitBlock(Document, DocumentPosition) → DocumentPositionMergeBlocks(Document, int blockIndex) → DocumentPositionApplyTextStyle(Document, TextRange, Action<TextStyle>) → voidApplyParagraphStyle(Document, TextRange, Action<ParagraphStyle>) → void
-
Documentcreation and block manipulation tests -
TextRunsplitting and merging tests -
DocumentEditor.InsertText/DeleteRangeround-trip tests -
DocumentPositioncomparison and navigation tests -
TextStyleimmutability and equality tests
- Commit: "Phase 1 — Core abstractions & document model"
Build the layout engine that converts the document model into positioned visual lines, and render them using the drawing abstractions.
-
LayoutLine— a single visual line: list ofLayoutRunitems, y-offset, line height, baseline -
LayoutRun— a positioned text segment: text, x-offset, width, associatedTextRunreference, style -
LayoutBlock— layout result for oneBlock: list ofLayoutLine, total height, block index -
DocumentLayout— full layout result: list ofLayoutBlock, total document height, viewport width -
TextLayoutEngine— performs word-wrapping and line-breaking:- Input:
Document+ viewport width +IDrawingContext(for measuring) - Output:
DocumentLayout - Supports: word-wrap, character-wrap fallback for long words, respects paragraph indent levels
- Input:
-
LayoutCache— caches layout results per block; invalidates selectively on edits
-
SkiaDrawingContext : IDrawingContext, IDisposable— maps all drawing calls toSKCanvas, with per-frame paint/font caching (mirrors KumikoUI'sSkiaDrawingContext) -
DrawTextRun/MeasureTextRunimplementation usingSKFontandSKPaint
-
DocumentRenderer— traversesDocumentLayoutand draws each run viaIDrawingContext- Draws backgrounds (highlight colors)
- Draws text runs with appropriate styles
- Draws block decorations (list bullets/numbers, heading emphasis)
- Handles vertical scrolling offset
- Clips to viewport bounds
- Layout engine: single-paragraph word-wrap tests
- Layout engine: multi-paragraph layout tests
- Layout engine: inline style boundary alignment tests
- Layout cache invalidation tests
- Commit: "Phase 2 — Text layout engine & basic rendering"
Implement the blinking caret, hit-testing from pixel coordinates to document positions, and keyboard/pointer navigation.
-
HitTestResult— (DocumentPosition, LayoutLine, LayoutRun, isAtLineEnd, isAfterLastBlock) -
DocumentHitTester— given (x, y) andDocumentLayout, returnsHitTestResult- Character-level hit testing within runs
- Snap-to-nearest-character logic
- Line-level hit testing for click-on-margin behavior
-
Caret— currentDocumentPosition, preferred X for vertical navigation, blink state -
CaretRenderer— draws the caret line at the correct pixel position usingDocumentLayout - Caret blink timer integration (on/off cycle, reset on input)
- Arrow keys: left/right (character), up/down (visual line), maintaining preferred X
- Word navigation: Ctrl/Cmd + Left/Right (jump by word boundary)
- Line navigation: Home/End (line start/end)
- Document navigation: Ctrl+Home / Ctrl+End
- Page navigation: PageUp / PageDown (viewport-height steps)
- Pointer click: single-click positions caret via hit testing
- Ensure caret remains visible: auto-scroll viewport when caret moves beyond visible area
-
EditorPointerEventArgs— (x, y, action, button, modifiers, clickCount, timestamp) -
EditorKeyEventArgs— (key, character, modifiers, isKeyDown) -
EditorKeyenum — arrows, Home, End, PageUp, PageDown, Tab, Enter, Escape, Backspace, Delete, A-Z, etc. -
InputActionenum — Pressed, Released, Moved, Scroll, DoubleTap, LongPress -
InputModifiers[Flags] — None, Shift, Control, Alt, Meta -
EditorInputController— central input dispatcher (mirrors KumikoUI'sGridInputController)
- Hit testing: click within a word returns correct position
- Hit testing: click in margin snaps to line start/end
- Navigation: arrow keys in single-line and multi-line scenarios
- Navigation: word-jump boundaries
- Navigation: Ctrl+Home / Ctrl+End
- Commit: "Phase 3 — Caret, cursor & text navigation"
Extend the caret into a selection range with visual highlighting and keyboard/pointer selection gestures.
-
Selection— anchor position + active position (caret) forming aTextRange; supports zero-length (caret-only) and ranged selection -
SelectionRenderer— draws selection highlight rectangles across multiple lines - Selection normalization: ordered start/end regardless of anchor vs. active direction
- Shift + Arrow keys: extend selection character/line/word at a time
- Shift + Home/End: extend selection to line boundaries
- Shift + Ctrl+Home/End: extend to document start/end
- Ctrl/Cmd + A: select all
- Mouse/touch drag: click-and-drag to select a range
- Double-click/tap: select word
- Triple-click/tap: select paragraph/block
- Shift + click: extend selection from anchor to click position
-
GetSelectedText(Document, TextRange) → string— extracts plain text from selection -
GetSelectedDocument(Document, TextRange) → Document— extracts a sub-document (for rich copy)
- Shift+arrow extends selection
- Double-click selects word
- Triple-click selects paragraph
- Select-all returns entire document text
- Drag selection across multiple blocks
- Commit: "Phase 4 — Text selection"
Handle keyboard text entry, backspace, delete, Enter (paragraph split), and typed-over selections.
- Character input: insert typed character at caret position
- If selection is active, delete selection first, then insert
- Backspace: delete character before caret (or delete selection)
- Delete: delete character after caret (or delete selection)
- Enter: split current block at caret position (
DocumentEditor.SplitBlock) - Backspace at start of block: merge with previous block (
DocumentEditor.MergeBlocks) - Delete at end of block: merge with next block
- Tab key behavior (configurable: insert tab character, increase indent, or move focus)
- Hidden
KeyboardProxyoverlay for keyboard capture — transparent 1×1Viewfocused on touch, sits above theSKCanvasViewin aGrid - Platform-specific keyboard configuration (Android
BaseInputConnectionIME, iOS/Mac CatalystUIKeyInput+UIKeyCommandfor nav keys) - Translate platform text events into
EditorKeyEventArgs/ character input →EditorInputController
- Insert character at various positions
- Backspace and delete at boundaries
- Enter splits paragraph correctly
- Type-over-selection replaces selected text
- Backspace at block boundary merges blocks
- Commit: "Phase 5 — Text input & basic editing"
Implement cut, copy, and paste with both plain text and rich text support.
-
IClipboardProviderinterface —SetTextAsync(string),GetTextAsync() -
ClipboardOperations— static helpers:CopyAsync,CutAsync,PasteAsyncusingIClipboardProvider - Keyboard shortcuts: Ctrl/Cmd + C (copy), Ctrl/Cmd + X (cut), Ctrl/Cmd + V (paste)
- Rich paste: if clipboard contains rich content (HTML/RTF), parse and insert styled content
- Plain paste: insert plain text with current caret style
- Paste with style matching: Ctrl/Cmd + Shift + V (paste as plain text)
-
MauiClipboardProvider : IClipboardProvider— wrapsClipboard.Defaultfrom MAUI Essentials
- Copy selected text, verify clipboard content
- Cut removes selection and places on clipboard
- Paste inserts at caret
- Paste replaces selection
- Commit: "Phase 6 — Clipboard operations"
Implement a robust undo/redo stack for all document mutations.
-
IUndoableActioninterface —Execute(),Undo(),Redo(),Description(string),MergeWith(IUndoableAction) → bool - Concrete actions:
InsertTextAction,DeleteRangeAction,SplitBlockAction,MergeBlocksAction,ApplyStyleAction,CompositeAction -
UndoManager— manages undo/redo stacks with configurable max depthPush(IUndoableAction)— adds action, clears redo stackUndo()— pops from undo, pushes to redo, restores stateRedo()— pops from redo, pushes to undo, re-appliesCanUndo/CanRedopropertiesUndoStackChangedevent
- Action merging: consecutive character inserts merge into a single action (with timeout)
-
DocumentEditorintegration: all mutations route through undo system - Keyboard shortcuts: Ctrl/Cmd + Z (undo), Ctrl/Cmd + Y / Ctrl/Cmd + Shift + Z (redo)
- Insert text, undo, verify original state
- Undo + redo round-trip
- Action merging: rapid typing groups into single undo step
- Max stack depth eviction
- Style changes are undoable
- Commit: "Phase 7 — Undo / redo"
Apply inline text styles: bold, italic, underline, strikethrough, subscript, superscript.
-
FormattingEngine— applies/removes/toggles character formats over aTextRange:ToggleBold(Document, TextRange)ToggleItalic(Document, TextRange)ToggleUnderline(Document, TextRange)ToggleStrikethrough(Document, TextRange)ToggleSubscript(Document, TextRange)ToggleSuperscript(Document, TextRange)ClearFormatting(Document, TextRange)— reset to default text style
- Toggle logic: if entire range is already bold → remove bold; otherwise → apply bold
- When no selection: set "pending style" so next typed character inherits the toggled format
- Keyboard shortcuts: Ctrl/Cmd + B (bold), Ctrl/Cmd + I (italic), Ctrl/Cmd + U (underline)
-
DocumentRendererdraws underline decoration (line below text baseline) -
DocumentRendererdraws strikethrough decoration (line through text center) -
DocumentRendererhandles superscript/subscript (reduced font size + baseline offset) -
SkiaDrawingContextsupports bold/italic font resolution viaSKTypeface
- Toggle bold on a range: verify
TextStyle.IsBoldon affected runs - Toggle on partially-formatted range: splits runs correctly
- Clear formatting resets all styles
- Pending style applies to next typed character
- Underline and strikethrough render at correct positions
- Commit: "Phase 8 — Character formatting"
Block-level formatting: alignment, lists, indentation, headings.
-
ParagraphFormattingEngine:SetAlignment(Document, TextRange, TextAlignment)— Left, Center, Right, JustifyToggleBulletList(Document, TextRange)— toggle unordered listToggleNumberList(Document, TextRange)— toggle ordered listIncreaseIndent(Document, TextRange)— increase indent levelDecreaseIndent(Document, TextRange)— decrease indent levelSetHeadingLevel(Document, TextRange, int level)— 0 = normal, 1-6 = headingSetParagraphFormat(Document, TextRange, string format)— "Normal", "Heading1"–"Heading6", "Quote"SetLineSpacing(Document, TextRange, float spacing)— 1.0, 1.5, 2.0, etc.
- Bullet rendering: draw bullet glyph (•) at indent position before paragraph
- Number rendering: draw sequential number at indent position before paragraph
- Indent rendering: apply left margin based on indent level
- Heading rendering: apply heading-level font sizes and weights
- Justify alignment: distribute extra space between words on each line (except last line)
- Block quote rendering: left border + background tint
- Set alignment on paragraph, verify rendering positions
- Toggle bullet list on/off
- Nested indent levels
- Heading levels apply correct font sizes
- Number list sequential numbering across multiple paragraphs
- Commit: "Phase 9 — Paragraph formatting"
Font family, font size, text color, and highlight/background color.
-
FontFormattingEngine:ApplyFontFamily(Document, TextRange, string family)ApplyFontSize(Document, TextRange, float size)ApplyTextColor(Document, TextRange, EditorColor color)ApplyHighlightColor(Document, TextRange, EditorColor color)RemoveHighlightColor(Document, TextRange)
- Query current format at caret:
GetCurrentTextStyle(Document, DocumentPosition) → TextStyle - Query format of selection:
GetTextStyleForRange(Document, TextRange) → TextStyle?(returns null for mixed values)
-
SkiaDrawingContextresolves font families toSKTypefacewith fallback chain - Draw highlight backgrounds per-run before text
- Text color per-run
- Apply font family to range splits runs
- Apply font size to range
- Apply text color
- Apply highlight color and verify background rendering
- Mixed-format query returns null
- Commit: "Phase 10 — Font manipulation"
Insert, edit, remove, detect, and open hyperlinks.
-
HyperlinkInlinein document model (from Phase 1) — URL, title, child inlines -
HyperlinkEngine:InsertHyperlink(Document, TextRange, string url, string? title)— wraps selected text (or inserts new text) as a hyperlinkEditHyperlink(Document, HyperlinkInline, string newUrl, string? newTitle)RemoveHyperlink(Document, TextRange)— unwraps hyperlink, keeps textGetHyperlinkAtPosition(Document, DocumentPosition) → HyperlinkInline?
- Auto-detect URLs while typing (optional, configurable)
- Open hyperlink action:
IHyperlinkHandlerinterface for platform-specific URL opening
- Hyperlink text rendered with underline + accent color (configurable)
- Hover/pointer-over cursor change indication
- Ctrl/Cmd + click to open hyperlink
-
MauiHyperlinkHandler : IHyperlinkHandler— usesLauncher.Default.OpenAsync(uri)
-
HyperlinkClickedevent with URL and cancel support -
IsHyperlinkSelectedChangedevent
- Insert hyperlink wraps text
- Remove hyperlink preserves text
- Edit hyperlink URL
- Auto-detect URL on space after typing "https://..."
- Hit-test returns hyperlink at position
- Commit: "Phase 11 — Hyperlinks"
Insert, display, resize, and manage inline images.
-
ImageInline(from Phase 1) — source (byte[] or stream reference), alt text, width, height, aspect ratio lock -
IImageProviderinterface —LoadImageAsync(source) → object(returns platform-specific image handle)
-
ImageEngine:InsertImage(Document, DocumentPosition, ImageSource) → ImageInlineRemoveImage(Document, ImageInline)ResizeImage(Document, ImageInline, float width, float height)ReplaceImage(Document, ImageInline, ImageSource)
-
DocumentRendererdrawsImageInlineusingIDrawingContext.DrawImage - Image selection: click on image selects it; draw selection handles (8 resize grips)
- Drag resize handles to resize (maintain aspect ratio by default, free-resize with Shift)
- Image placeholder while loading
-
MauiImageProvider : IImageProvider— loads from file path, stream, or gallery picker -
ImageRequestedevent (mirrors Syncfusion) for custom image source dialogs
- Load images as
SKImage/SKBitmapfor rendering - Scale/clip images to fit layout bounds
- Insert image at position
- Resize image maintains aspect ratio
- Remove image from document
- Layout accounts for image dimensions in line height
- Commit: "Phase 12 — Image support"
Insert and edit tables with rows, columns, cell merging, and styling.
-
TableBlock : Block— rows × columns grid ofTableCell -
TableCell— contains a mini-Document(list ofBlock), column span, row span, background color, borders -
TableRow— list ofTableCell, row height -
TableStyle— border style, cell padding, header row style
-
TableEngine:InsertTable(Document, DocumentPosition, int rows, int cols) → TableBlockInsertRow(TableBlock, int afterRowIndex)InsertColumn(TableBlock, int afterColIndex)DeleteRow(TableBlock, int rowIndex)DeleteColumn(TableBlock, int colIndex)MergeCells(TableBlock, range)SplitCell(TableBlock, cell)SetCellBackground(TableCell, EditorColor)
- Table layout engine: column widths (auto, fixed, percentage), row heights
- Cell content layout: each cell lays out its own document
- Draw table borders, cell backgrounds
- Tab / Shift+Tab key navigates between cells
- Caret navigation within and between cells
- User-resizable table columns via drag handles (persisted column widths)
- User-resizable table rows via drag handles (persisted row heights)
- Tapping below a terminal table moves caret to a writable paragraph below the table
- Align keyboard behavior with common editors (ProseMirror/Tiptap, CKEditor 5, Lexical):
- Tab/Shift+Tab moves cell-to-cell
- Structural table edit commands are exposed from keyboard when focus is inside a cell
- Users can move the caret out of a table without using pointer/context menu
- Add table escape behavior:
- From boundary cells, keyboard navigation can move before/after the table
- If no adjacent block exists, create an empty paragraph to avoid caret trapping
- Add keyboard shortcuts for structural operations while in a table:
- Insert row above/below
- Insert column left/right
- Delete current row/column
- Delete current table
- Insert table with specified dimensions
- Add/remove rows and columns
- Cell merging across columns
- Tab navigation between cells
- Shift+Tab reverse navigation between cells
- Keyboard escape from last cell to block after table
- Keyboard structural shortcuts (insert/delete row/column, delete table)
- Nested content in cells
- Commit: "Phase 13 — Table support"
Formalize all editor actions as commands (ICommand) and expose a rich event system.
-
IEditorCommand—Execute(EditorContext),CanExecute(EditorContext),Name,Description -
EditorContext— bundles Document, Selection, UndoManager, FormattingEngine, etc. - Built-in commands (one per action):
ToggleBoldCommand,ToggleItalicCommand,ToggleUnderlineCommand,ToggleStrikethroughCommandToggleSubscriptCommand,ToggleSuperscriptCommandAlignLeftCommand,AlignCenterCommand,AlignRightCommand,AlignJustifyCommandToggleBulletListCommand,ToggleNumberListCommandIncreaseIndentCommand,DecreaseIndentCommandUndoCommand,RedoCommandCopyCommand,CutCommand,PasteCommandSelectAllCommand,ClearFormattingCommandInsertHyperlinkCommand,RemoveHyperlinkCommand,OpenHyperlinkCommandInsertImageCommand,RemoveImageCommandInsertTableCommandApplyFontFamilyCommand,ApplyFontSizeCommandApplyTextColorCommand,ApplyHighlightColorCommandSetHeadingLevelCommand,SetParagraphFormatCommand
-
EditorCommandRegistry— register, look up, and invoke commands by name - Key binding system: map keyboard shortcuts to commands (configurable)
-
SelectionChanged— fires when caret/selection moves -
TextChanged— fires on any document content change -
FontFamilyChanged— fires when font family at caret changes -
FontSizeChanged— fires when font size at caret changes -
FontAttributesChanged— fires when bold/italic state at caret changes -
TextDecorationsChanged— fires when underline/strikethrough at caret changes -
TextFormattingChanged— aggregate event for any formatting change -
HorizontalTextAlignmentChanged— fires when paragraph alignment changes -
ListTypeChanged— fires when list type at caret changes -
TextColorChanged,HighlightTextColorChanged -
HyperlinkClicked,IsHyperlinkSelectedChanged -
IsReadOnlyChanged -
ContentLoaded— fires after HTML/content import completes -
ImageInserted,ImageRemoved
- Commands execute and update document
- CanExecute returns false when inappropriate (e.g., copy with no selection)
- Events fire on corresponding actions
- Custom commands can be registered
- Commit: "Phase 14 — Command system & events"
A fully custom-drawn toolbar supporting auto-generated and user-customizable button layouts.
-
ToolbarItem(abstract) — base for toolbar elements -
ToolbarButton : ToolbarItem— icon, label, associatedIEditorCommand, toggle state -
ToolbarSeparator : ToolbarItem— visual divider -
ToolbarDropdown : ToolbarItem— dropdown picker (font family, font size, heading level, color picker) -
ToolbarColorPicker : ToolbarItem— color swatch grid for text color / highlight color -
ToolbarGroup— logical grouping of items -
ToolbarDefinition— configurable list ofToolbarItem/ToolbarGroupitems with layout mode:-
Wrap— items wrap to multiple rows on desktop -
Scroll— horizontal scroll on mobile -
Overflow— overflow items collapse into a "more" menu
-
- Auto-generated default toolbar matching Telerik/Syncfusion feature set:
- Group: Undo, Redo
- Separator
- Group: Font Family dropdown, Font Size dropdown
- Separator
- Group: Bold, Italic, Underline, Strikethrough
- Separator
- Group: Text Color, Highlight Color
- Separator
- Group: Align Left, Align Center, Align Right, Align Justify
- Separator
- Group: Bullet List, Number List, Decrease Indent, Increase Indent
- Separator
- Group: Subscript, Superscript
- Separator
- Group: Insert Hyperlink, Insert Image, Insert Table
- Separator
- Group: Edit actions dropdown (Copy/Cut/Paste/Select All)
- Separator
- Group: Object actions dropdown (Open/Remove Link, Remove Image)
- Separator
- Group: Table actions dropdown (insert/delete row/column, delete table)
- Separator
- Group: Heading dropdown, Clear Formatting
-
ToolbarRenderer— fully custom-drawn toolbar usingIDrawingContext - Button hit testing and press/hover states
- Toggle state visual (depressed/highlighted for active bold, italic, etc.)
- Dropdown rendering: popup overlay with selectable items
- Color picker rendering: grid of color swatches
- Icon rendering normalized to avoid clipping/cutoff across button sizes
- Toolbar responds to selection changes (updates toggle states, current font display)
-
ShowToolbarproperty — show/hide toolbar -
ToolbarItemscollection — customizable set of items
The toolbar is rendered entirely inside the SKCanvasView paint surface — no native XAML controls.
-
ToolbarRendererwired intoMintedEditorView.OnPaintSurface— drawn above the document viewport -
ShowToolbarbindable property onMintedEditorView(defaulttrue) — controls toolbar visibility -
ToolbarDefinitionbindable property onMintedEditorView— swaps activeToolbarRendererinstance - Touch routing in
OnCanvasTouch— Y <toolbarH→HitTest→IEditorCommand.Execute(EditorContext)→InvalidateCanvas() - Document viewport offset by
toolbarHso document content never overlaps toolbar - Theme colours forwarded each frame:
ToolbarBackground,ToolbarButtonColor,ToolbarActiveColor,ToolbarSeparatorColor - Async icon loading via
FileSystem.OpenAppPackageFileAsync→SKBitmap.Decode→_iconCache -
IconResolverdelegate set on_toolbarRendererso icons render as bitmaps; unknown keys fall back to text labels - Icon key → bundle filename mapping for all 20 default toolbar icons (handles
number-list→tb_ordered_list.png,clear-formatting→tb_clear_format.png) - XAML sample (
MainPage.xaml) uses only<minted:MintedEditorView ShowToolbar="True"/>— no native toolbar overlay
- Default toolbar generates all expected items
- Button click executes associated command
- Toggle state reflects current selection format
- Dropdown selection applies formatting
- Custom toolbar definition renders correctly
- Commit: "Phase 15 — Toolbar"
- Commit: "Canvas-integrated toolbar with async icon loading"
Right-click / long-press context menu with standard and extensible items.
-
ContextMenuItem— label, icon, associatedIEditorCommand, enabled state, separator-after flag -
ContextMenuDefinition— ordered list ofContextMenuItem - Default context menu:
- Cut, Copy, Paste
- Separator
- Select All
- Separator
- Insert Hyperlink / Edit Hyperlink / Remove Hyperlink (context-dependent)
- Open Hyperlink (if hyperlink selected)
- Separator
- Insert Image
- Insert Table (if applicable)
- Extensibility:
ContextMenuItemsRequestedevent allows adding/removing items before display -
ContextMenuRenderer— fully custom-drawn popup usingIDrawingContext- Rounded rectangle background with shadow
- Hover highlight on items
- Keyboard navigation (arrow keys, Enter to select, Escape to dismiss)
- Right-click (secondary button) on desktop
- Long-press on mobile
- Positioned at pointer location, clamped to viewport bounds
- Context menu shows correct items for context (hyperlink vs. no hyperlink)
- Menu item click executes command
- Menu dismisses on click outside or Escape
- Custom items added via event
- Commit: "Phase 16 — Context menu"
Load content from HTML strings/streams and export document to HTML.
-
HtmlImporter— parses HTML string/stream intoDocumentmodel- Supported tags:
<p>,<br>,<strong>/<b>,<em>/<i>,<u>,<s>/<del>/<strike>,<sub>,<sup>,<span>(with inline styles),<a>,<img>,<h1>–<h6>,<ul>,<ol>,<li>,<blockquote>,<table>,<tr>,<td>,<th>,<div> - Parse inline CSS:
font-family,font-size,color,background-color,text-align,text-decoration,font-weight,font-style - Graceful handling of unsupported tags (preserve content, ignore formatting)
- Uses a lightweight/simple HTML parser — no heavy dependency (consider custom or
HtmlAgilityPack)
- Supported tags:
-
HtmlExporter— serializesDocumentto clean, semantic HTML string- Generates minimal inline styles (only non-default attributes)
- Produces well-formed HTML5
- Options: include/exclude
<html>/<body>wrapper, CSS class mode vs. inline style mode
-
LoadHtml(string html)— replace document content from HTML -
LoadHtml(Stream stream)— replace document content from HTML stream -
GetHtml() → string— export current document as HTML -
GetHtml(TextRange) → string— export selection as HTML
- Round-trip:
Document→ HTML →Documentpreserves content and styles - Parse complex HTML with nested formatting
- Export generates valid HTML5
- Unsupported tags don't crash parser
- Inline CSS is correctly mapped to
TextStyle
- Commit: "Phase 17 — HTML import / export"
Comprehensive theming system with built-in themes and full customization.
-
EditorThemeModeenum —Light,Dark,HighContrast -
EditorStyle— comprehensive style class covering all visual properties:- Editor background, border color, border width
- Default text color, font family, font size
- Caret color, caret width
- Selection highlight color, selection text color
- Hyperlink color, hyperlink hover color
- Toolbar background, toolbar button color, toolbar separator color, toolbar hover color, toolbar active/toggle color
- Context menu background, context menu text color, context menu hover color
- Scrollbar track/thumb colors
- Focus ring color
- Line spacing, paragraph spacing
- Padding (top, right, bottom, left)
-
EditorThemefactory —Create(EditorThemeMode)returns preconfiguredEditorStyleCreateLight()— clean white themeCreateDark()— dark mode themeCreateHighContrast()— high-contrast accessibility theme
- Runtime theme switching: changing theme triggers full re-render
- Custom theme support: users supply their own
EditorStyle - System theme follow mode in MAUI view (
UseSystemTheme) with automatic light/dark selection - Theme-aware default text rendering so dark mode no longer renders default text as black
- Theme-aware hyperlink color application in layout/render pipeline
- Font-family/font-size changes at collapsed caret apply to subsequently typed text (pending style behavior)
- All three built-in themes create valid styles
- Custom style overrides are respected
- Theme switch triggers re-layout and re-render
- Commit: "Phase 18 — Theming & styling"
Make the editor accessible, localizable, and support right-to-left text.
- Semantic properties on the MAUI view (
SemanticProperties.Description,AutomationProperties) - Screen-reader announcements for formatting changes
- High-contrast theme support (from Phase 18)
- Keyboard-only navigation for all features (toolbar, context menu, editor)
- Focus indicator styling
- ARIA-equivalent descriptions for toolbar buttons
- Resource files for all UI strings (toolbar tooltips, context menu labels, dialog prompts)
-
IStringLocalizerintegration or simple resource-based approach - Default language: English
- Support for plugging in additional languages
-
TextDirectiononParagraphStyle(LTR / RTL / Auto) - RTL text layout: right-aligned by default, mirrored indent
- RTL caret behavior: caret on right side, moves right-to-left
- Mixed LTR/RTL content within a single paragraph (bidirectional text)
- Toolbar mirroring in RTL mode
- Semantic properties are set on the MAUI view
- Localized strings load correctly
- RTL paragraph renders text right-to-left
- Bidirectional text scenario
- Commit: "Phase 19 — Accessibility, localization & RTL"
Comprehensive test coverage across all layers.
- Document model creation, mutation, and validation
- Text layout engine: word-wrap, multi-paragraph, inline styles
- Caret navigation: all directions, word/line/document boundaries
- Selection: keyboard and pointer gestures, multi-line, word/paragraph select
- Text input: insert, delete, backspace, Enter, Tab, type-over-selection
- Clipboard: copy, cut, paste (mock clipboard)
- Undo/redo: all operation types, merging, stack limits
- Character formatting: all toggle operations, pending style
- Paragraph formatting: alignment, lists, indent, headings
- Font formatting: family, size, color, highlight
- Hyperlinks: CRUD, auto-detect, hit-test
- Images: insert, resize, remove, layout impact
- Tables: CRUD rows/columns, cell merging, Tab navigation
- Commands: execute, canExecute, registry
- HTML import: parse various HTML structures
- HTML export: serialize and validate output
- Theming: all built-in themes, custom overrides
- Hit-testing: various viewport positions
- Full editing flow: type text → format → undo → redo → export HTML → re-import → compare
- Large document: performance baseline for layout/render with 10,000+ paragraphs
-
MockDrawingContext : IDrawingContext— records draw calls for assertion -
MockClipboardProvider : IClipboardProvider— in-memory clipboard -
TestDocumentBuilder— fluent API for creating test documents
- Commit: "Phase 20 — Testing infrastructure"
GitHub Actions workflows for build, test, and NuGet package publishing.
- Trigger: push/PR to
mainanddevelopbranches (ignore**.md,docs/**) - Concurrency: cancel in-progress runs for same ref
- Job 1 — Build & Test (Core + SkiaSharp) on
ubuntu-latest:- Setup .NET 10
- Restore, build, test (
MintedTextEditor.Core.Tests) - Upload test results artifact (TRX)
- Pack dry-run to validate packaging
- Job 2 — Build (MAUI) on
windows-latest(requires Windows for Windows TFM):- Setup .NET 10 + MAUI workloads (cached)
- Restore, build
MintedTextEditor.Maui - Pack dry-run to validate packaging
- Trigger: push tag
v*orworkflow_dispatchwith version input - Resolve version from tag or input
- Job 1 — Pack Core & SkiaSharp on
ubuntu-latest:- Build + pack with source-link, symbol packages
- Upload
.nupkgand.snupkgartifacts
- Job 2 — Pack MAUI on
windows-latest:- Build + pack with source-link, symbol packages
- Upload
.nupkgand.snupkgartifacts
- Job 3 — Publish to NuGet.org:
- Download all package artifacts
dotnet nuget pushto NuGet.org usingNUGET_API_KEYsecret
- Job 4 — Create GitHub Release:
- Generate release notes
- Attach
.nupkgfiles to release
- Commit: "Phase 21 — CI/CD pipeline"
Comprehensive documentation for users and contributors.
- Project overview and tagline
- Feature highlights with screenshots/GIFs
- Architecture diagram (Core → SkiaSharp → MAUI layers)
- Quick start guide:
- NuGet install commands
builder.UseMintedTextEditor()inMauiProgram.cs- XAML and C# usage examples
- Feature comparison table vs. Telerik / Syncfusion
- Platform support matrix (iOS, Android, Mac Catalyst, Windows)
- Configuration reference (toolbar customization, theming, events)
- Contributing guide link
- License badge, NuGet badge, CI badge
-
docs/getting-started.md— installation, basic setup, first editor -
docs/document-model.md— explains Block, Inline, TextRun, etc. -
docs/formatting.md— character and paragraph formatting guide -
docs/toolbar-customization.md— custom toolbar layouts -
docs/theming.md— built-in themes, custom themes -
docs/commands-events.md— command system, event reference -
docs/html-interop.md— HTML import/export -
docs/images-hyperlinks.md— image and hyperlink handling -
docs/tables.md— table creation and editing -
docs/accessibility.md— accessibility features and guidelines -
docs/architecture.md— in-depth architecture overview for contributors
-
CONTRIBUTING.md— contribution guidelines, code style, PR process -
CODE_OF_CONDUCT.md— standard code of conduct -
CHANGELOG.md— version history (initially empty template)
- Commit: "Phase 22 — Documentation & README"
A .NET MAUI sample app demonstrating all editor features.
- Main page: full-featured editor with default toolbar
- Demo pages:
- Basic text editing
- Rich formatting showcase (all character + paragraph styles)
- Custom toolbar layout
- HTML import/export (textarea for HTML source)
- Load/save document
- Theme switcher (Light / Dark / High Contrast)
- Hyperlink and image demo
- Table editing demo
- Read-only mode demo
- Event monitor (displays fired events in a log panel)
- Custom command demo
- Localization demo
- Navigation:
ShellorTabbedPagewith demo list - Supports all MAUI platforms (iOS, Android, Mac Catalyst, Windows)
- Commit: "Phase 23 — Sample application"
Final optimization pass and quality improvements.
- Profile rendering: ensure <16ms frame time for typical documents
- Virtualized rendering: only render visible blocks/lines (skip off-screen content)
- Layout caching: only re-layout changed blocks, not entire document
- Paint object pooling: ensure zero per-frame allocations for paints/fonts
- Large document support: test with 50,000+ paragraph documents
- Image downsampling for display —
IImageProviderinterface withDownsampleAsync+IsAvailable - Lazy layout: defer layout of off-screen content
- Smooth scrolling with inertia —
InertialScroller(5-sample window, friction=0.92, 60fps tick) - Mouse wheel / trackpad scroll
- Touch-based panning with velocity tracking — integrated in
MintedEditorView.OnCanvasTouch - Scrollbar rendering: vertical scrollbar (track + thumb) —
DocumentRenderer.RenderScrollbar; optional viaShowScrollbarbindable property - Scroll-to-position API
- Word-wrap toggle —
TextLayoutEngine.WordWrap;IsWordWrapbindable property onMintedEditorView - Read-only mode —
IsReadOnlyproperty disables all editing (implemented in prior phase) - Placeholder text when document is empty
- Line numbers (optional gutter) —
DocumentRenderer.RenderLineNumbersGutter;ShowLineNumbersbindable property - Find & Replace —
FindReplaceEnginewithFind,FindNext,FindPrevious,FindAll,Replace,ReplaceAll;FindOptions(MatchCase, WholeWord, WrapAround) - Spell-check integration point —
ISpellCheckProviderinterface - Print support integration point —
IPrintProviderinterface - Focus management —
EditorFocusChangedevent;Focus()/Unfocus()overrides onMintedEditorView
- Commit: "Phase 24 — Performance & polish"
Targeted fixes identified during end-to-end QA against Telerik/Syncfusion/CKEditor/Tiptap interaction patterns.
- Resize columns by drag handle with persisted per-column widths
- Resize rows by drag handle with persisted per-row heights
- Insert/delete row and column from toolbar-accessible controls
- Delete table from toolbar-accessible controls
- Ensure users can place caret and type below a terminal table via tap
- Expose all core editing/insert/table/object operations through toolbar controls
- Add context-aware action dropdowns (Edit/Object/Table) to reduce toolbar clutter
- Improve icon fit and control sizing to prevent clipped/cutoff glyph rendering
- Add configurable system-theme following behavior for MAUI host
- Fix dark-mode readability by honoring theme default text color for default-styled runs
- Fix collapsed-caret font switching so subsequent typing uses selected font settings
- Focused core test suites: table/input/toolbar/font/theme/layout
- MAUI iOS simulator build validation after UX parity changes
- Commit: "Phase 25 — UX parity hardening"
Targeted fixes identified during end-to-end QA on mobile (iOS/Android) and desktop targets.
- Fix blurry/low-resolution toolbar icon rendering — use high-quality
SKPaintfilter when drawingSKBitmapicons inSkiaDrawingContext.DrawImage - Increase SVG rasterization target size from
max(64, 36×scale)tomax(96, ButtonSize×2×scale)so icons are always sharp at HiDPI density - Increase bitmap icon draw area from 56 % of button size to 72 % so icons are more legible
- Replace all hardcoded pixel offsets in vector icon helpers (
DrawVectorIcon) with proportional fractions of the currentrectso icons render correctly at everyButtonSize - Add a
Save/ClipRect/Restoreguard around each vector-icon drawing call to guarantee icons never draw outside their allocated button rectangle - Fix undo/redo arc representation to use proportional curves rather than plain lines
- Add
MaxRowsproperty toToolbarDefinition(0 = unlimited) and parallelMaxRowsproperty onToolbarRenderer - When
LayoutMode == Wrap && MaxRows > 0, items that would start on a row beyondMaxRowsare collected as overflow items instead of being drawn - Reserve a fixed
OverflowButtonWidthslot at the trailing edge of the last allowed row and render a…button there - Extend
HitTestto return the synthetic overflow button; tapping it opens a drop-down panel listing all overflow items - Overflow panel items fire their normal commands just like regular toolbar items
- Expose
ToolbarMaxRowsbindable property onMintedEditorView(default 0 = unlimited)
- Add
DocumentEditor.NormalizePosition(Document doc, DocumentPosition pos)— converts aDocumentPositionto a canonical(blockIndex, inlineIndex, offset)by walking inline lengths so stale inline indices after run-splitting are corrected - Call
NormalizePositiononSelection.AnchorandSelection.ActiveinsideMintedEditorView.ExecuteToolbarCommandafter every formatting command that touches the document - Guard
SelectionRenderer.Renderwith an inline-length bounds check so an out-of-range offset never throws and visually shows the nearest valid position instead
- Toolbar overflow:
ToolbarRendererTests— verify items beyondMaxRowsare excluded and the overflow button rect is populated - Selection normalization: add cases to
SelectionTestsverifying anchor/active remain correct after bold/italic/underline toggles that split runs
- Commit: "Phase 26 — Toolbar & selection quality fixes"
┌──────────────────────────────────────────────────────────────┐
│ MintedTextEditor.Maui │
│ ┌────────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ EditorView │ │ Keyboard │ │ MAUI Providers │ │
│ │ (SKCanvasView) │ │ Proxy │ │ (Clipboard, │ │
│ │ │ │ (hidden │ │ Hyperlink, │ │
│ │ │ │ Entry) │ │ Image) │ │
│ └───────┬─────────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ Touch/Pointer │ Key events │ │
└──────────┼────────────────────┼───────────────────┼────────────┘
│ │ │
▼ ▼ │
┌──────────────────────────────────────────────────────────────┐
│ MintedTextEditor.SkiaSharp │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ SkiaDrawingContext : IDrawingContext │ │
│ │ (SKCanvas, cached SKPaint/SKFont/SKTypeface per frame) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ implements
▼
┌──────────────────────────────────────────────────────────────┐
│ MintedTextEditor.Core │
│ │
│ Rendering/ Document/ Editing/ │
│ ├─ IDrawingContext ├─ Document ├─ Caret │
│ ├─ EditorPaint ├─ Block ├─ Selection │
│ ├─ EditorFont ├─ Paragraph ├─ DocumentEditor │
│ ├─ EditorRect ├─ TextRun ├─ UndoManager │
│ ├─ EditorColor ├─ ImageInline ├─ EditSession │
│ └─ PaintCache ├─ HyperlinkInl. └─ IClipboardProv. │
│ ├─ TableBlock │
│ Layout/ ├─ TextStyle Formatting/ │
│ ├─ TextLayoutEngine ├─ ParaStyle ├─ FormattingEngine │
│ ├─ LayoutLine └─ DocPosition ├─ ParagraphFmtEng. │
│ ├─ LayoutRun ├─ FontFmtEngine │
│ ├─ LayoutBlock Input/ ├─ HyperlinkEngine │
│ ├─ DocumentLayout ├─ EditorInput ├─ ImageEngine │
│ └─ LayoutCache ├─ HitTester └─ TableEngine │
│ └─ InputEvents │
│ Commands/ Toolbar/ Html/ │
│ ├─ IEditorCommand ├─ ToolbarItem ├─ HtmlImporter │
│ ├─ CommandRegistry ├─ ToolbarDef. └─ HtmlExporter │
│ └─ Built-in cmds └─ ToolbarRndr │
│ │
│ Theming/ ContextMenu/ Events/ │
│ ├─ EditorTheme ├─ ContextMenu └─ (all editor │
│ └─ EditorStyle └─ CtxMenuRndr events) │
└──────────────────────────────────────────────────────────────┘
| Feature | Telerik | Syncfusion | MintedTextEditor |
|---|---|---|---|
| Bold / Italic / Underline | ✅ | ✅ | Phase 8 |
| Strikethrough | ✅ | ✅ | Phase 8 |
| Subscript / Superscript | ✅ | ✅ | Phase 8 |
| Font Family | ✅ | ✅ | Phase 10 |
| Font Size | ✅ | ✅ | Phase 10 |
| Text Color | ✅ | ✅ | Phase 10 |
| Highlight Color | ✅ | ✅ | Phase 10 |
| Text Alignment (L/C/R/J) | ✅ | ✅ | Phase 9 |
| Bullet List | ✅ | ✅ | Phase 9 |
| Numbered List | ✅ | ✅ | Phase 9 |
| Indent / Outdent | ✅ | ✅ | Phase 9 |
| Headings (H1–H6) | ❌ | ✅ | Phase 9 |
| Hyperlink CRUD | ✅ | ✅ | Phase 11 |
| Open Hyperlink | ✅ | ✅ | Phase 11 |
| Image Insert / Edit | ✅ | ✅ | Phase 12 |
| Image Resize | ✅ | ❌ | Phase 12 |
| Tables | ❌ | ❌ | Phase 13 |
| Undo / Redo | ✅ | ✅ | Phase 7 |
| Copy / Cut / Paste | ✅ | ✅ | Phase 6 |
| Select All | ✅ | ✅ | Phase 4 |
| Clear Formatting | ✅ | ❌ | Phase 8 |
| Toolbar (auto-generated) | ✅ | ✅ | Phase 15 |
| Toolbar (custom layout) | ✅ | ✅ | Phase 15 |
| Context Menu | ✅ | ❌ | Phase 16 |
| Commands (ICommand) | ✅ | ❌ | Phase 14 |
| Rich Event System | ✅ | ✅ | Phase 14 |
| HTML Import / Export | ✅ | ❌ | Phase 17 |
| Theming (Light/Dark/HC) | ✅ | ✅ | Phase 18 |
| RTL Support | ❌ | ✅ | Phase 19 |
| Localization | ❌ | ✅ | Phase 19 |
| Accessibility | ✅ | ✅ | Phase 19 |
| Read-Only Mode | ✅ | ❌ | Phase 24 |
| Placeholder Text | ❌ | ❌ | Phase 24 |
| Find & Replace | ❌ | ❌ | Phase 24 |
| Spell Check Integration | ❌ | ❌ | Phase 24 |
| Word Wrap Toggle | ❌ | ✅ | Phase 24 |
| Cross-platform Core | ❌ | ❌ | ✅ (by design) |
| Swappable Drawing Backend | ❌ | ❌ | ✅ (by design) |
| No Native UI Controls | ❌ | ❌ | ✅ (by design) |
-
Document Model: Tree-based (
Document→Block→Inline→TextRun) rather than flat-buffer. Enables clean structural operations (split/merge paragraphs, table cells as sub-documents). -
Undo System: Action-based (command pattern) rather than state-snapshot. Lower memory usage, supports fine-grained merging of character inserts.
-
Layout Engine: Block-level caching — only re-layout blocks that changed. Each block's layout is independent, enabling efficient partial updates.
-
Rendering Pipeline:
Document→TextLayoutEngine→DocumentLayout→DocumentRenderer→IDrawingContext. Clean one-way data flow. -
Input Architecture: Platform events →
EditorInputController→ command dispatch. Mirrors KumikoUI'sGridInputControllerpattern for clean separation. -
Toolbar: Fully custom-drawn on the same canvas (not native widgets). Consistent look across platforms, themeable, extensible.
-
HTML as Interchange Format: HTML is the primary import/export format (matching Telerik). Rich clipboard also uses HTML.
-
Extensibility Points:
IDrawingContext(rendering backend),IClipboardProvider(clipboard),IHyperlinkHandler(link opening),IImageProvider(image loading),ISpellCheckProvider(spell-check),IEditorCommand(custom commands), events for all operations.