Visitar URL original
feat(richeditbox): implement Skia support by morning4coffe-dev · Pull Request #23924 · unoplatform/uno · GitHub
Skip to content

feat(richeditbox): implement Skia support - #23924

Merged
MartinZikmund merged 236 commits into
masterfrom
dev/doti/port-richeditbox
Oct 10, 2026
Merged

MartinZikmund merged 236 commits into
masterfrom
dev/doti/port-richeditbox

Conversation

@morning4coffe-dev

@morning4coffe-dev morning4coffe-dev commented Jul 30, 2026 •

Copy link
Copy Markdown
Member

GitHub Issue: closes #3848

Dependency: #23731 merged into master on September 20. This PR targets master and adds the RichEditBox layer; shared RichTextBlock implementations are reused, not copied.

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.
  • Uses WinUI's hyperlink launch policy: file/res are blocked, HTTP(S) is trusted, and other absolute schemes require confirmation. The cross-platform confirmation adapter is scoped to the editor's XamlRoot; the browser adapter additionally rejects JavaScript execution URLs.
  • 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
  • TextBoxBaseAutomationPeer.cpp and WindowLessSiteHost.cpp host/provider policy; the private windowless text provider is represented by a managed adapter, not claimed as a source port
  • TextBoxBase.cpp/.h editing, focus, flyout, proofing, gripper, and selection behavior
  • Launcher.cpp scheme eligibility and trusted/untrusted launch policy
  • TextSelectionManager.cpp, TextControlFlyoutHelper.*, 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/UIKit/DOM renderers are not introduced.
  • 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

The following includes validation from earlier revisions. Current review-follow-up results and platform limits are recorded in the review replies.

  • 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.

@morning4coffe-dev
morning4coffe-dev force-pushed the dev/doti/port-richeditbox branch from 722c575 to dc23773 Compare July 31, 2026 16:42
@github-actions

github-actions Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @morning4coffe-dev's task in 4m 38s —— View job


Code Review — feat(richeditbox): implement Skia support

Tasks

  • Load context and prior review findings
  • Read key changed files
  • Review: accessibility patterns (RichEditBoxAutomationPeer.cs, SkiaAccessibilityBase.cs)
  • Review: resource management (Win32ClipboardExtension.cs)
  • Review: nullable safety in test files
  • Review: async/cancellation discipline, event declarations, platform suffix correctness
  • Post inline comments
  • Summarize findings

Verdict: fix-first — two medium accessibility gaps will silently break non-Win32 Skia AT clients (macOS VoiceOver, X11 AT-SPI) and should be addressed before merge. One has a companion test that locks in the broken behaviour and must be updated together.

Issues by severity

# Severity File Summary
1 Medium RichEditBoxAutomationPeer.cs:45 PatternInterface.TextEdit missing from GetPatternCore — macOS/X11 AT discovery fails silently
2 Medium SkiaAccessibilityBase.cs:420 NotifyAutomationEvent gates UpdateTextValue on Value pattern, which RichEditBox doesn't advertise — VoiceOver text sync dropped
3 Medium Given_RichEditBox.Automation.skia.cs:53 Test asserts GetPattern(TextEdit) is null, encoding the gap from finding #1 — must be updated when the fix lands or it will permanently lock in the broken state
4 Low Win32ClipboardExtension.cs:126 ThreadLocal<HWND> without trackAllValues: true leaks message-only windows on thread-pool thread exit
5 Low (×5) Given_RichEditBox.Automation.skia.cs, Given_AccessibleTextBox.cs Nullable dereferences after Assert.IsNotNull — static flow analysis doesn't narrow through MSTest assertions; add !-suppressors

Cross-cutting observations

Solid areas confirmed:

  • event Action<T> correctly avoided — TextInputPlugin.InputConnectionCreated uses EventHandler<TextInputConnectionCreatedEventArgs>. ✅
  • All new async void paths (PasteFromClipboard, BeginPasteFromClipboard) are wrapped in comprehensive try/catch. ✅
  • Platform suffix discipline is correct: Skia-specific files use .skia.cs, Win32 in Uno.UI.Runtime.Skia.Win32, etc. No native-platform code leaks into shared files. ✅
  • RTF parser security limits (MaxRtfInputLength = 16 MB, depth/group/output budgets, FeatureConfiguration.RichEditBox.MaxRtfImportCharacters) are well-bounded. ✅
  • [GeneratedDependencyProperty] used correctly in RichEditBox.Properties.skia.cs with FrameworkPropertyMetadata, correct callbacks, default(T) for primitive defaults, and AcceptsReturn = true matching WinUI's RichEditBox contract (not TextBox's false). ✅

Finding #1 + #3 are coupled: adding PatternInterface.TextEdit to GetPatternCore in RichEditBoxAutomationPeer.cs will cause the test at Given_RichEditBox.Automation.skia.cs:53 to fail. Both must change together in one commit.

Finding #2 is independent and the fix is contained to SkiaAccessibilityBase.NotifyAutomationEvent — broadening the TextEditTextChanged / TextPatternOnTextChanged guard from GetPattern(Value) to also fall back to peer as IValueProvider.

@github-actions

github-actions Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @morning4coffe-dev's task in 4m 46s —— View job


Code Review — feat(richeditbox): implement Skia support

Tasks

  • Load context and prior review findings
  • Read key changed files (accessibility peers, clipboard extension, test files)
  • Check if previous findings were fixed
  • Review new commits for fresh issues
  • Post inline comments
  • Summarize findings

Verdict: fix-first — the two medium accessibility gaps from prior reviews remain open and should be addressed before merge. The Win32ClipboardExtension HWND leak and nullable suppressors are low-severity but straightforward.

Status of prior findings

# Severity Status Finding
1 Medium Still open PatternInterface.TextEdit missing from RichEditBoxAutomationPeer.GetPatternCore — macOS/X11 AT discovery broken
2 Medium Still open SkiaAccessibilityBase.NotifyAutomationEvent gates on Value pattern; VoiceOver text sync dropped for RichEditBox
3 Medium Still open Companion test at Given_RichEditBox.Automation.skia.cs:53 asserts TextEdit returns null, locking in finding #1
4 Low Still open ThreadLocal<HWND> without trackAllValues: true leaks message-only HWNDs
5 Low (×5) Still open Nullable dereferences in test files after Assert.IsNotNull (Given_RichEditBox.Automation.skia.cs ×4, Given_AccessibleTextBox.cs ×1)

What the latest commits addressed

The recent fix(richeditbox): resolve CI regressions commits (Jul 31–Aug 1) correctly fixed:

  • Stream position restore for bitmap and unknown-data clipboard setters (SetBitmap, SetUnknownData)
  • Various Skia CI test regressions (IME, math glyph assembly, movement probes, performance, scaling)
  • Missing [PlatformCondition] on a When_UnicodeMath_* test that cannot run against native RichEdit

These are all correct and do not introduce new issues.

Cross-cutting observations

Solid areas confirmed (unchanged):

  • event Action<T> correctly avoided — TextInputPlugin.InputConnectionCreated uses EventHandler<TextInputConnectionCreatedEventArgs>. ✅
  • All async void paths are wrapped in comprehensive try/catch. ✅
  • Platform suffix discipline is correct; Skia-specific files use .skia.cs, Win32 in Uno.UI.Runtime.Skia.Win32. ✅
  • RTF parser security limits are well-bounded. ✅
  • [GeneratedDependencyProperty] used correctly in RichEditBox.Properties.skia.cs. ✅

Findings #1 and #3 are coupled: adding PatternInterface.TextEdit to GetPatternCore will cause the test at Given_RichEditBox.Automation.skia.cs:53 to fail — both must change in the same commit.

MartinZikmund and others added 3 commits October 10, 2026 07:52
Keeps master's unpaired-surrogate behavior (d751043) after the rebase
folded it into ReadCodepoint, so script and font fallback never see a
surrogate code unit as a code point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TzDba3gSK3uYpvLmmBmaa
Compares a lambda-local rect so CodeQL no longer reports the captured
bounds comparison as identical values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TzDba3gSK3uYpvLmmBmaa
The single 2 MiB line is one glyph run whose vertex data exceeds WebGPU's
256 MB buffer limit, and wgpu aborts the process on the resulting
validation error, losing the rest of the shard. That is a backend
limitation (one op is never split across vertex arena chunks), not a
RichEditBox one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TzDba3gSK3uYpvLmmBmaa

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@MartinZikmund

Copy link
Copy Markdown
Member

Rebased onto master (d2ea69f60bc) and force-pushed with a lease on @MartinZikmund's behalf; head is now f41df4d6277.

  • Conflict: only UnicodeText.cs. Master's d751043a118 (lone surrogates) and this PR's 137c4f12985 fixed the same loop. I kept the PR's ReadCodepoint and moved master's behavior into it: a lone surrogate now reads as U+FFFD, so script and font fallback never get a bare surrogate (de7f8340eeb). Master's Given_UnicodeText tests cover this.
  • CodeQL "comparison of identical values" in RichEditBoxUiaClient: this was a false positive. The bounds check now uses a rect local to the lambda (5756c744f36).
  • New Desktop Linux WebGPU leg (added on master after this PR's last green run): the run aborted in When_Bounded_Layout_Ends_Large_Story_Is_Released. The 2 MiB line is a single glyph run whose vertex data (about 857 MB) is over WebGPU's 256 MB buffer limit, and wgpu panics and aborts the process when that happens. That limit comes from the backend (PackVerts never splits a single op), not from RichEditBox, so the test now skips on WebGPU (f41df4d6277).
  • The three a11y validation threads are left open, as their last comments say.

Built locally in strict mode: Uno.UI.RuntimeTests.Skia, Runtime.Win32/MacOS/WebAssembly.Browser and RichEditBoxUiaClient. CI is running.

🤖 Generated with Claude Code

@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

@nventive-devops

Copy link
Copy Markdown
Contributor

The build 238733 found UI Test snapshots differences: skia-linux-screenshots: 118, skia-windows-screenshots: 1139

Details
  • skia-linux-screenshots: 118 changed over 2458

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • ButtonClippingTestsControl.png-dark
    • ButtonClippingTestsControl.png
    • ClipboardTests.png-dark
    • DisplayInformation.png-dark
    • Buttons.png-dark
    • Buttons.png
    • CalendarView_Theming.png-dark
    • CalendarView_Theming.png
    • CompositionEffectBrush.png-dark
    • CompositionEffectBrush.png
    • CompositionNineGridBrush_Source_Changes.png-dark
    • CompositionNineGridBrush_Source_Changes.png
    • ElementLevelTheme.png-dark
    • ElementLevelTheme.png
    • DynamicFont.png-dark
    • DynamicFont.png
    • DropDownButtonPage.png-dark
    • Examples.png
    • Focus_FocusVisual_Properties.png-dark
    • Focus_FocusVisual_Properties.png
  • skia-windows-screenshots: 1139 changed over 2486

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • AnimatedBackVisualSourceSamplePage.png-dark
    • AppWindowFeatures.png
    • Attributed_text_Simple.png-dark
    • AutoBorderStretchwithleftmargin.png-dark
    • AutoBorderStretchwithleftmargin.png
    • AutomationProperties_AutomationId.png
    • AutoSizedPathCentered.png
    • AutoSuggestBox_Keyboard.png
    • BasicEnability.png-dark
    • Basics_Automated.png-dark
    • Basics_Automated.png
    • BorderCenteredwithmargins.png-dark
    • BorderCenteredwithmargins.png
    • Border_Simple_No_Background_With_TextBox.png-dark
    • Border_Simple_No_Background_With_TextBox.png
    • Button_Events.png-dark
    • Button_Events.png
    • ComboBox_Legacy.png-dark
    • ComboBox_Legacy.png
    • CommandBarFlyoutMainPage.png-dark

@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

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

6 participants