Skip to content

feat(richeditbox): implement Skia support - #23924

Draft
morning4coffe-dev wants to merge 61 commits into
masterfrom
dev/doti/port-richeditbox
Draft

feat(richeditbox): implement Skia support#23924
morning4coffe-dev wants to merge 61 commits into
masterfrom
dev/doti/port-richeditbox

Conversation

@morning4coffe-dev

Copy link
Copy Markdown
Member

GitHub Issue: closes #3848

PR Type:

✨ Feature

What changed? 🚀

This adds a functional WinUI-compatible RichEditBox implementation for Uno's Skia targets: Desktop, Skia-on-Android/iOS, and Skia WebAssembly.

User-visible behavior

  • Adds interactive rich-text editing with keyboard, pointer, touch grippers, selection, caret scrolling, formatting accelerators, spell-check integration, and IME composition.
  • Implements the public Text Object Model surface used by RichEditTextDocument, ITextRange, ITextSelection, character/paragraph formats, tracked ranges, clipboard operations, streams, and undo/redo.
  • Renders character and paragraph formatting, lists, tabs, links, inline images, bidirectional text, MathML/Unicode math, OpenType MATH variants/assemblies, and large documents through bounded incremental layout.
  • Adds Text/Text2 automation, rich text-object children, native Win32 UIA integration, and a standalone external UIA probe.
  • Adds safe bounded RTF import/export, including editable table metadata, object fallbacks, active-destination stripping, hyperlink export allowlisting, and parser/resource limits.
  • Adds eight SamplesApp pages covering basic editing, character/paragraph formatting, events, formatting accelerators, interactive editing, automation, and the all-features surface.

What came from /winui-port

The public/MUX-observable control behavior was ported and aligned from WinUI sources using the /winui-port workflow. The changed files retain MUX references or source-location comments for:

  • RichEditBox_Partial.cpp control/template behavior
  • RichEditBoxAutomationPeer_Partial.cpp automation class/control semantics
  • TextAdapter_Partial.cpp and Text/Text2 pattern behavior
  • TextBoxBase.cpp/.h editing, focus, flyout, proofing, gripper, and selection behavior
  • TextSelectionManager.cpp, TextControlFlyoutHelper.*, UIElement, and CoreServices behavior shared with the existing text stack

Public parity tests are intentionally unsuffixed where they can run against both Uno and native WinUI.

What had to be implemented from scratch

WinUI delegates the backing editor to non-public Windows RichEdit/Text Services and COM internals. Those APIs are not public or portable, so they could not be losslessly copied into Uno. The following cross-platform pieces are managed Uno implementations built from scratch against the public observable contract:

  • piece-based text story storage, indexed formatting runs, range gravity/rebasing, and bounded undo history
  • TOM range/selection navigation, formatting, geometry, clipboard, and stream behavior
  • retained/incremental rich-text shaping and layout over the shared Skia TextBlock pipeline
  • bounded RTF parsing/writing, table-control preservation, image/object fallbacks, and active-content sanitization
  • MathML/Unicode-math projection and OpenType MATH metrics, variants, and glyph assemblies
  • browser/native IME session coordination, candidate geometry, platform input proxies, and composition ownership
  • rich text-object automation adapters and platform UIA provider integration

Uno does not expose private ITextDocument2/ITextRange2 COM identity, OLE hosting, marshaling, apartment, reference-counting, or Windows RichEdit implementation details. The public WinUI API and observable behavior remain the compatibility boundary.

Scope and safeguards

  • Skia renderer only; native Android views, native UIKit controls, and the native WASM DOM renderer remain maintenance-only.
  • Large stories switch to bounded paragraph layout; list state is checkpointed so local edits remain incremental.
  • Async clipboard providers are bounded, canceled when superseded, and generation-gated before commit.
  • RTF/MathML inputs enforce size, nesting, token, run, image/object, and projected-output budgets.
  • Unsafe external-template, macro/form, mail-merge, embedded-font, active-field, and blocked-scheme content is not re-emitted as active RTF.
  • The existing IValueProvider ABI is retained, but RichEditBox continues to advertise the WinUI Text/Text2 patterns rather than a Value pattern.

Coverage

  • Adds approximately 750 runtime-test methods across public WinUI parity, Skia behavior, rendering, editing, IME, clipboard, RTF, math, automation, and scaling suites.
  • Adds a signed native-WinUI test path and external Win32 UIA client for reference validation.
  • Adds Uno-specific RichEditBox documentation and a platform support/limitations matrix.

Validation performed

  • Analyzer-strict net10.0 Skia SamplesApp build.
  • Targeted Skia runtime tests for editing, RTF tables, streams, clipboard ordering/cancellation, IME restart, automation input limits, math, bounded layout, and security budgets.
  • Skia WebAssembly publish, including C#/TypeScript interop generation.
  • Signed x64 native WinUI SamplesApp MSIX build and filtered native-reference execution. WinUI flattens edited RTF tables, so Uno's safer table-descriptor preservation tests are explicitly marked as Uno-specific contracts.
  • DocFX 2.73.2 rendered articles/controls/RichEditBox.html with no page-specific diagnostics. The full local docs build retains the known missing external/generated TOC baseline.

PR Checklist ✅

@github-actions github-actions Bot added platform/wasm 🌐 Categorizes an issue or PR as relevant to the WebAssembly platform platform/android 🤖 Categorizes an issue or PR as relevant to the Android platform platform/macos 🍏 Categorizes an issue or PR as relevant to the macOS platform platform/ios 🍎 Categorizes an issue or PR as relevant to the iOS platform area/skia ✏️ Categorizes an issue or PR as relevant to Skia area/build Categorizes an issue or PR as relevant to build infrastructure labels Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @morning4coffe-dev's task in 15m 47s —— View job


Code Review — feat(richeditbox): implement Skia support

Verdict: fix-first — two medium accessibility gaps should be addressed before merge; the resource leak is low-severity but straightforward to fix.


Findings (inline comments posted)

# Severity File Finding
1 Medium RichEditBoxAutomationPeer.cs:42 PatternInterface.TextEdit removed from GetPatternCore — Win32 has a GetTextEditProvider fallback, but macOS and X11 UIA providers do not. Non-Win32 Skia accessibility silently advertises no TextEdit pattern despite TextAdapter implementing ITextEditProvider.
2 Medium SkiaAccessibilityBase.cs:415 NotifyTextEditTextChangedEvent is a no-op at the base class level; additionally, UpdateTextValue (which calls uno_accessibility_update_value on macOS) gates on GetPattern(PatternInterface.Value), which returns null for RichEditBox since Value was intentionally removed from GetPatternCore. VoiceOver text sync is silently dropped for RichEditBox.
3 Low Win32ClipboardExtension.cs:126 ThreadLocal<HWND> created without trackAllValues: true — threads that exit without calling DestroyWindow leak message-only windows. The field's Dispose can't enumerate values to clean up.

Cross-cutting notes

Solid areas:

  • All async void paths (PasteFromClipboard, BeginPasteFromClipboard) are wrapped in comprehensive try/catch with correct exception-type handling and re-throw on fatal exceptions.
  • event Action<T> violation in TextInputPlugin was correctly converted to EventHandler<TextInputConnectionCreatedEventArgs> — no new violations introduced.
  • RichEditBox.Properties.skia.cs DP registrations use FrameworkPropertyMetadata, correct callback signatures, and default(T) for primitive defaults. AcceptsReturn defaults to true (correct WinUI behavior for RichEditBox, unlike TextBox).
  • RTF parser security limits (MaxRtfInputLength = 16 MB, MaxGroupDepth = 256, MaxParsedGroups = 65 536) and FeatureConfiguration.RichEditBox.MaxRtfImportCharacters are appropriately bounded.
  • Generated stubs correctly updated — __SKIA__ removed from [Uno.NotImplemented] attributes and #if guards in the Generated/ folder following the standard pattern.
  • ITextBoxViewHost abstraction is a clean seam that keeps TextBlock.OwningTextBox decoupled from the concrete TextBox type.

Scope note: This is a large initial implementation (~4 800-line RTF codec + full editing engine). The Win32 path is well-exercised by the new Uno.UI.RuntimeTests.Win32UiaClient harness. The two medium findings above specifically affect macOS/X11 Skia accessibility, which would otherwise be silently incomplete at the IAccessible/AT-SPI layer.

@github-actions github-actions Bot added area/automation Categorizes an issue or PR as relevant to project automation kind/documentation platform/x11 🐧 Categorizes an issue or PR as relevant to X11 labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

WinAppSDK sync generator drift detected on 722c575

The Uno.WinAppSDKSyncGenerator produced changes that are not committed on this PR.

To apply automatically (PRs from branches in this repo only): comment /apply-sync-gen on this PR and a bot will push chore: Sync generator run to the head branch.

To apply manually (required for PRs from forks):

  1. Download the sync-generator-patch artifact from this run.
  2. In your local checkout of the PR branch:
    git apply sync-generator.patch
    git add -A
    git commit -m "chore: Sync generator run"
    git push
    

Or re-run the generator locally on Windows via build\run-api-sync-tool.cmd.


protected override object GetPatternCore(PatternInterface patternInterface)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PatternInterface.TextEdit was removed from GetPatternCore. Win32 has a dedicated fallback in Win32RawElementProvider.GetTextEditProvider() that retrieves it via the Text pattern, so Win32 UIA keeps working. However, non-Win32 Skia targets (macOS NSAccessibility / X11 AT-SPI) discover patterns via GetPattern(PatternInterface.TextEdit), and that now returns null for RichEditBox. Since TextAdapter (the m_textPattern object) implements ITextEditProvider, an explicit fallback for TextEdit identical to what the Win32 provider does would close the gap:

Suggested change
protected override object GetPatternCore(PatternInterface patternInterface)
protected override object GetPatternCore(PatternInterface patternInterface)
{
if (patternInterface == PatternInterface.Text
|| patternInterface == PatternInterface.Text2
|| patternInterface == PatternInterface.TextEdit)
{

Fix this →

}
}

public virtual void NotifyTextEditTextChangedEvent(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The base NotifyTextEditTextChangedEvent is a no-op, which is fine for the base class. However, none of the macOS, X11, or WASM subclasses override it, so TextEdit composition events are silently dropped on those platforms. That's expected for a first-pass Skia implementation, but the existing OnAutomationEvent handler at line 400-407 fires UpdateTextValue via peer.GetPattern(PatternInterface.Value) — and RichEditBox intentionally does not advertise Value pattern. macOS VoiceOver therefore won't pick up RichEditBox text changes via UpdateTextValue.

Consider propagating rich text updates from NotifyTextEditTextChangedEvent to UpdateTextValue using the peer's IValueProvider directly (cast peer as IValueProvider) as a bridge, or override this method in MacOSAccessibility to push the updated text. The Win32 path is unaffected since its UpdateTextValue is a no-op.

}

_hwnd = PInvoke.CreateWindowEx(
_clipboardWindows = new ThreadLocal<HWND>(CreateClipboardWindow);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ThreadLocal<HWND> creates a new message-only HWND for every thread that calls GetClipboardWindow(). The created HWNDs are never destroyed when their threads exit because ThreadLocal<T> does not call DestroyWindow on eviction. On a long-running app that calls clipboard APIs from short-lived thread-pool threads this accumulates orphaned HWNDs (capped by the system limit of ~10,000 HWNDs per process, not a practical problem in normal use, but still a leak).

To clean up, use new ThreadLocal<HWND>(CreateClipboardWindow, trackAllValues: true) and call DestroyWindow on each tracked value in the singleton's (currently absent) disposal path, or pump a WM_CLOSE / WM_DESTROY via PostThreadMessage when each thread exits. If clipboard APIs are only called from the UI thread in practice, a simpler alternative is to drop ThreadLocal and go back to a single HWND (the previous design) with a comment explaining why multi-thread access is not expected.

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-23924/docs/index.html

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23924/wasm-skia-net9/index.html

var valueProvider = peer?.GetPattern(PatternInterface.Value) as IValueProvider;

Assert.IsNotNull(peer);
Assert.AreEqual("Notes", peer.GetName());

Assert.IsNotNull(textProvider);
Assert.IsNotNull(textProvider2);
Assert.AreEqual(SupportedTextSelection.Single, textProvider.SupportedTextSelection);
Assert.AreEqual(1, selection.Length);
Assert.AreEqual("ell", selection[0].GetText(-1));

var caret = textProvider2.GetCaretRange(out var isActive);
fake.SimulateCompositionStart();
fake.SimulateCompositionUpdate("nihao", cursorPosition: 2, resolvedLength: 2);

Assert.AreEqual("nihao", textEditProvider.GetActiveComposition().GetText(-1));
Assert.IsNotNull(peer);
Assert.IsNotNull(textProvider);

var firstChildren = textProvider.DocumentRange.GetChildren();
Assert.AreEqual("logo", imageRange.GetText(-1));
Assert.IsNotNull(linkTextChild);
Assert.IsNotNull(imageTextChild);
Assert.AreSame(peer, linkTextChild.TextContainer.AutomationPeer);
Assert.IsNotNull(linkTextChild);
Assert.IsNotNull(imageTextChild);
Assert.AreSame(peer, linkTextChild.TextContainer.AutomationPeer);
Assert.AreSame(peer, imageTextChild.TextContainer.AutomationPeer);
var textProvider = peer?.GetPattern(PatternInterface.Text) as ITextProvider;
Assert.IsNotNull(peer);
Assert.IsNotNull(textProvider);
var children = textProvider.DocumentRange.GetChildren();
var peer = FrameworkElementAutomationPeer.CreatePeerForElement(sut);
var textProvider = peer?.GetPattern(PatternInterface.Text) as ITextProvider;
Assert.IsNotNull(textProvider);
var range = textProvider.DocumentRange.FindText("context", backward: false, ignoreCase: false);
@nventive-devops

Copy link
Copy Markdown
Contributor

The build 225049 found UI Test snapshots differences: skia-linux-screenshots: 57, skia-windows-screenshots: 2348, wasm: 11

Details
  • skia-linux-screenshots: 57 changed over 2348

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • Buttons.png-dark
    • Buttons.png
    • Gamepad_CurrentReading.png-dark
    • Gamepad_CurrentReading.png
    • Gamepad_Enumeration.png-dark
    • Gamepad_Enumeration.png
    • ImageIconPage.png-dark
    • ButtonClippingTestsControl.png-dark
    • ButtonClippingTestsControl.png
    • CalendarView_Theming.png-dark
    • CalendarView_Theming.png
    • ClipboardTests.png-dark
    • ContextRequested.png-dark
    • ContextRequested.png
    • DisplayInformation.png-dark
    • Examples.png-dark
    • Examples.png
    • DropDownButtonPage.png-dark
    • ExpanderColorValidationPage.png-dark
    • DropDownButtonPage.png
  • skia-windows-screenshots: 2348 changed over 2406

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • AndroidWindowInsets.png-dark
    • AndroidWindowInsets.png
    • AnimatedIconPage.png-dark
    • Accessibility_ScreenReader.png-dark
    • Accessibility_ScreenReader.png
    • AppBarToggleButtonTest.png
    • ApplicationViewMode.png-dark
    • ApplicationViewSizing.png-dark
    • AppBarButtonWithIconTest.png
    • AppBar_KeyBoard.png-dark
    • AppWindowPositionAndSize.png
    • Attributed_text_Simple.png
    • Arrange_Performance01.png
    • ApplicationViewSpanningRectsPage.png-dark
    • AppWindowFeatures.png
    • ArcSegment.png-dark
    • AutomationProperties_AutomationId.png
    • AutoBorderStretchwithrightmargin.png-dark
    • AuthenticationBroker_Demo.png
    • AppXamlDefinedResources.png
  • wasm: 11 changed over 1076

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • UITests.Shared.Windows_UI_Xaml_Controls.MediaPlayerElement.MediaPlayerElement_Ogg_Extension
    • UITests.Uno_Web.Http.CookieManagerTests
    • UITests.Windows_UI_Xaml_Media_Animation.ColorAnimation_Background
    • SamplesApp.Windows_UI_Xaml_Controls.ListView.ListViewSelectedItems
    • SamplesApp.Wasm.Windows_UI_Xaml_Controls.ListView.ListView_IsSelected
    • UITests.Shared.Microsoft_UI_Xaml_Controls.ExpanderTests.WinUIExpanderPage
    • UITests.Shared.Windows_UI_Xaml_Controls.MediaPlayerElement.MediaPlayerElement_Minimal
    • UITests.Windows_UI_Xaml_Controls.CalendarView.CalendarView_Theming
    • Uno.UI.Samples.Content.UITests.WebView.WebView_AnchorNavigation
    • SamplesApp.Microsoft_UI_Xaml_Controls.WebView2Tests.WebView2_EnableDevTools
    • UITests.Microsoft_UI_Xaml_Controls.WebView2Tests.WebView2_NavigationProperties

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/automation Categorizes an issue or PR as relevant to project automation area/build Categorizes an issue or PR as relevant to build infrastructure area/skia ✏️ Categorizes an issue or PR as relevant to Skia kind/documentation platform/android 🤖 Categorizes an issue or PR as relevant to the Android platform platform/ios 🍎 Categorizes an issue or PR as relevant to the iOS platform platform/macos 🍏 Categorizes an issue or PR as relevant to the macOS platform platform/wasm 🌐 Categorizes an issue or PR as relevant to the WebAssembly platform platform/x11 🐧 Categorizes an issue or PR as relevant to X11

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement RichEditBox

3 participants