Skip to content

Releases: centwon/WinUIRichEditor

1.2.0 — 동결 이후 첫 기능 릴리스 (추가만, breaking 없음)

Choose a tag to compare

@centwon centwon released this 20 Sep 03:19
29b43a7

공개 표면은 추가만(바인딩 가능한 의존성 속성 7개)이라 코드 수정 없이 올릴 수 있다. 다만 동작 변경이 몇 가지 있고 두 가지는 기존 문서에서 눈에 보인다 — 아래를 먼저 읽을 것. 전체 목록은 CHANGELOG.

⚠ 올릴 때 확인할 것

  • 줄 간격이 HWP 기준: LineSpacing은 "글자 크기 × 값"이고, 설정 없는 문단은 **160%**로 그려진다(전에는 폰트 고유 높이 ≈120%). 문단 기본 아래 여백은 10px → 0. 저장 형식은 그대로지만 기존 문서가 더 넓게 보인다.
  • 제목의 굵게·크기가 글자 속성: 제목 안에서 굵게를 끄거나 크기를 바꿀 수 있고, 제목을 다시 적용하면 제목 모양으로 되돌아간다. 옛 문서는 편집기가 받을 때 한 번 변환한다("HeadingFormat": 1) — 직렬화기는 변환하지 않으므로, 편집기 없이 옛 파일을 HTML/RTF로 바로 바꾸는 호스트는 결과가 달라진다.
  • 굵게·기울임·밑줄·취소선 토글이 Word 방식(전부 그 서식이면 끄고, 아니면 모두 켠다).
  • 인쇄와 SavePdf가 글자로 나간다(전에는 페이지마다 비트맵) — 프린터 해상도, PDF에서 글자 선택·검색 가능.
  • 문서가 아닌 입력을 거부한다: LoadJson/LoadPackageAsync가 예외를 던지고 열린 문서를 지킨다(전에는 조용히 빈 문서로 바꾸고 "저장됨"으로 표시해 다음 저장이 원본을 덮었다).
  • 링크는 http/https만 연다(문서 속 ms-msdt: 같은 프로토콜 링크가 클릭 한 번에 실행되던 것 — 보안).
  • 그 밖에: 페이지 설정이 호스트 기본값에서 시작 · AllowImages/AllowTables가 리치 붙여넣기에도 적용 · 언두 예산이 요소 수 기준 · 영어 UI의 셀 세로 가운데 "Middle".

새 기능

벡터 인쇄·PDF(AOT에서도 인쇄 대화상자) · 표·그림을 끌어서 옮기기(Ctrl=복사) · 그림을 가로·세로로 따로 늘리는 손잡이 · 한 셀 선택(F5)과 Shift+방향키 셀 블록 · 찾기 UI(Ctrl+F/H·F3, 뷰의 찾기 막대) · XAML에서 바인딩할 수 있는 툴바·뷰 속성 7개 · 벡터 툴바 아이콘.

검증

테스트 391 → 815. AOT self-contained 게시 후 JIT↔AOT 동등성 스윕(두 페이지 모두 문서 바이트 동일, AOT 전용 fault는 알려진 LineMetricsOf 하나) · NuGet 소비자 스모크 15/15(PackageReference로 세운 별도 앱에서 빌드·실행).

1.1.1 — AOT에서 깨져 있던 줄 메트릭

Choose a tag to compare

@centwon centwon released this 06 Sep 21:49
96c99dd

1.1.0은 Native AOT에서 깨진 채 나갔다. 그것이 이 릴리스의 이유다.

CanvasTextLayout.LineMetrics가 반환하는 구조체 배열이 bool을 품어 non-blittable이고, CsWinRT가 AOT에서 이 배열을 마샬링하지 못한다. 이 API를 쓰는 8곳이 전부 "줄 없음"으로 폴백해서 AOT 빌드는:

  • 줄바꿈을 넘는 세로 캐럿 이동이 동작하지 않고
  • 글머리표가 베이스라인 정렬을 잃고
  • 페이지네이션이 모든 문단을 한 줄로 쟀다

빌드도 되고 렌더도 되니 멀쩡해 보였다 — 1.1.0의 릴리스 노트조차 "AOT 재실측"을 통과로 적었다. 게시와 렌더만 봤기 때문이다. JIT 빌드는 영향이 없다(이 API는 AOT에서만 실패한다).

올릴 때

  • Breaking 없음. 공개 표면은 변화조차 없다 — PublicAPI.Shipped.txt 567줄 그대로. 코드 수정 없이 올라간다.
  • Native AOT로 게시하는 소비자는 올릴 것.
  • ⚠ 동작 변경: 자릿수가 int를 넘는 RTF 제어어 파라미터는 더 이상 "손상"이 아니다 — TryParse가 거부하던 일부 파일이 열린다(손상 판정은 이제 절단만 본다). 상류 AvaloniaRichEditor와 수렴한 결정으로, malformed 토큰은 damaged 문서가 아니다.
  • 최소 요구 Microsoft.WindowsAppSDK.WinUI는 2.2.1 그대로.

그 밖의 수정

외부 감사 보고서 31건을 코드와 1:1 대조해 나온 수정 13건 — 표 열 리사이즈·TableBlock.Extract 크래시, RTF 토큰화가 문서를 통째로 날리던 것, Backspace/Delete/화살표의 서로게이트 페어 분할, 셀 안 이미지 우클릭 미인식, 셀 안 블록↔인라인 변환 무동작(양방향), CSS 퍼센트 색상, UIA 텍스트/선택 알림 부재(스크린 리더). 메모리: 문서에서 빠진 문단을 붙잡던 곁캐시, 언두 체크포인트의 표 격자 할당 −40%(표가 지배적인 문서). 게시 어셈블리에서 빌드 머신 절대 경로 제거.

검증

  • 테스트 304 → 391, 빌드 0 warn / 0 err, 퍼즈 4000시드 클린
  • AOT 동등성 실측(신규 tools/fault-sweep.ps1): 같은 편집 순서를 JIT와 AOT 게시본에 주입해 ① 라이브러리가 보고한 fault 집합 ② 되읽은 문서를 비교 — AOT 전용 fault는 줄 메트릭 폴백 하나뿐이고 문서는 두 페이지에서 바이트 동일. 1.1.0이 놓친 것이 정확히 이 비교다.
  • NuGet 소비자 스모크 15/15 — 별도 unpackaged WinUI 3 앱이 PackageReference로 빌드·실행
  • 언두/찾기 축에 검증 44개 신설(그 전엔 Undo()·FindNext를 호출하는 테스트가 하나도 없었다)

전체 변경은 CHANGELOG.md 참조.

1.0.0 — public API frozen

Choose a tag to compare

@centwon centwon released this 31 Jul 15:12

The public API is frozen. From here the surface follows SemVer — no breaking change without a major bump — and it is tracked in PublicAPI.Shipped.txt (553 entries), so a change to it shows up as a build warning and a diff rather than shipping unnoticed.

What made this 1.0 is verification depth, not new features. The editor has been feature-complete for a while; this release is what a full comparison against AvaloniaRichEditor 1.0, four audit passes and a randomized edit-sequence fuzz turned up — 34 defects, several of them data-losing.

⚠️ Upgrading from 0.9.x

One breaking removal:

  • Formatters.RoundTripHarness is gone. It was a development tool that only used the public formatter API, so it moved into the demo project. Removing public API is breaking, which is why it happens at the freeze rather than after it.

Two behaviour changes — check these if you host the control:

  • A damaged document now throws instead of reading as an empty one. LoadJson / LoadJsonAsync / LoadPackageAsync raise JsonException / InvalidDataException. Swallowing the error was the outcome that lost data: a host could not tell "this file was empty" from "this file is damaged", showed a blank editor, and the next save overwrote a recoverable file with nothing. If you call these, handle the exception — the open document is left alone when they throw.
  • RTF table output changed shape. A horizontal merge is now expressed geometrically (one cell whose \cellx sits at the merged edge) because HWP dissolves the flag form that Word also accepts, and a table nested in a cell is now real \itap nesting instead of flattened text.

Fixed

Reachable from a paste — any pasted or opened file is untrusted input:

  • A single <td colspan="100000000"> exhausted memory in the HTML importer. rowspan was bounded by the rows that exist; colspan had no ceiling at all.
  • A .flow/JSON document declaring a huge table exhausted memory before a cell was read — the constructor was sized from the file's own numbers, and everything it allocated was discarded three lines later.
  • A JSON null inside Blocks / Cells / Inlines threw NullReferenceException.
  • Ordinary Word RTF imported with text missing. The pending run was flushed at an ignorable group's closing brace, by which point the skipped destination was active and threw it away — and Word writes such groups routinely (bookmarks, fields, a nested table's properties). A nested table's row definition also restarted the row mid-cell, discarding what the parent cell had accumulated.

Data loss in round trips:

  • A document of indented list items exported to HTML came back as literal markup text. Indent the only list item in a document and the export opens <ol><ol><li>…; the importer looked at direct <li> children only, found nothing, produced zero blocks, and the raw-text fallback dumped the whole file as text.
  • Several paragraphs in a table cell collapsed into one on every RTF round trip (and on every Word/HWP paste): intra-cell \par was read as a newline rather than a paragraph break.
  • An inline table alone in its paragraph — the ordinary "treat as character" shape — swallowed the paragraph before it when reloaded, in both HTML and RTF.
  • Separators accumulated as content on each save/load cycle: a space after an inline table in HTML, a newline before a nested table in RTF, a <br> after a block element in a cell.

Interoperability (verified in Word, HWP and a browser):

  • Horizontal cell merges dissolved in HWP.
  • Inline tables now survive an HTML and an RTF round trip, and sit inside the text line in browsers and Word instead of stretching to a full-width band.
  • Cell background colour is written to and read from RTF (\clcbpat) — it was dropped even by our own reader.

Editing:

  • The toolbar silently took keyboard focus: the caret vanished and typing stopped, while the buttons still appeared to work because the commands ran against the remembered caret position. Nothing in the strip takes focus now, and pickers hand it back when they close.
  • The caret was drawn below the text, and vertical arrow movement broke, at line spacing above 100% — the caret is placed on the line's baseline now, and movement steps over the line rather than over the caret.
  • Turning on a list detached an inline table or image in the same paragraph, leaving the caret pointing into a subtree no longer in the document.
  • A block image inside a table cell could not be selected, resized or deleted at all.
  • GetPlainText() and GetImageCount() reached only one level deep, so text inside nested and inline tables was invisible — including to assistive technology, which reads the former.

Added

  • Nine behaviour flags became dependency properties, so they can be bound and styled: AllowImages, AllowTables, AllowRichPaste, AllowFindReplace, AllowLocalFileImages, AllowRemoteImagesOnPaste, AutoLinkOnType, MaxRecommendedImages, ShowFormattingMenu. Same names, same defaults.
  • RichEditor.FocusEditor() — returns keyboard focus to the editing surface without moving the caret. Focus lives on an inner canvas, so Focus() on the control does not reach it.
  • XML documentation now ships with the package, so the library's comments reach IntelliSense.
  • A randomized edit-sequence fuzz over the document model (DocumentFuzzTests): structural invariants after every step, plus a two-cycle round-trip comparison through all four formats. It found three of the defects above.

Verified

Build 0 warnings / 0 errors · 106 headless tests · Native AOT publish builds, runs and renders (self-contained, 15 MB native exe) · package contents checked (dll + XML docs + README, no stray files) · a separate consumer app consuming the NuGet package builds and runs (13/13 checks) · Word, HWP and browser interoperability checked by hand.

Known limitations

  • HWP does not implement RTF nested tables. It drops \nestcell and runs the nested cells' text together. Our output is byte-identical to what AvaloniaRichEditor 1.0 emits, and Word reads it correctly.
  • A table whose every row is merged identically reads back from RTF as one wide column — nothing in the file then reveals the underlying grid. It renders identically.
  • A nested table's column widths come back at the default: they live in an ignorable group.
  • HTML loses one space inside a merged cell on the second round trip. Deliberately not fixed at 1.0 — HTML whitespace handling is separately tuned for browser copy, Word paste and <pre>, and that risk was not worth a cosmetic space on release day.

Requires Windows App SDK 2.2.1 or later. Apps that publish self-contained must reference the Microsoft.WindowsAppSDK meta-package (only it carries the redistributable runtime).

Full detail, including the reasoning behind each fix, is in CHANGELOG.md.

WinUIRichEditor 0.9.1

Choose a tag to compare

@centwon centwon released this 29 Jul 03:35

⚠️ 0.9.0 대신 이 버전을 쓰세요. 0.9.0의 유일한 실질 차이는 잘못 올라간 SDK 하한선입니다.

Fixed

최소 요구 Microsoft.WindowsAppSDK.WinUI 2.3.2 → 2.2.1로 환원

0.9.0이 요구한 2.3.2는 소비자가 쓰는 메타패키지 Microsoft.WindowsAppSDK가 실제로 주는 WinUI보다 높습니다:

메타패키지 딸려오는 WinUI
2.2.0 2.2.1
2.3.1 2.3.0

그래서 프레임워크 의존 앱이 자기가 배포하는 런타임보다 높은 WinUI로 컴파일되어 어긋났고, self-contained로 전환하거나 편집기를 되돌리는 수밖에 없었습니다.

라이브러리는 2.3에서 추가된 API를 하나도 쓰지 않습니다. 2.2에서 실측 확인했습니다 — 솔루션 빌드 0/0, 테스트 74/74, AOT self-contained 게시 성공(81MB, 네이티브 exe 14.2MB), 실행 + Win2D 렌더 정상.

하한선에 적용되는 규칙 두 가지를 csproj 주석에 못박았습니다:

  1. 실제로 빌드·테스트·AOT가 통과하는 가장 낮은 버전일 것
  2. 소비자 메타패키지가 주는 WinUI 버전을 넘지 말 것

문서

소비자 앱이 self-contained 배포(WindowsAppSDKSelfContained=true)를 한다면 메타패키지 Microsoft.WindowsAppSDK를 참조해야 한다는 점을 README에 명시했습니다. Microsoft.WindowsAppSDK.Runtime(재배포 런타임)은 메타패키지에만 딸려옵니다 — 이 라이브러리가 분할 컴포넌트를 참조하는 건 컴파일 표면만 필요한 라이브러리의 사정이고, 앱이 그걸 따라 하면 번들할 런타임이 없어집니다.

0.9.0의 내용

기능·수정 사항은 모두 0.9.1에 포함됩니다. 0.9.0 릴리스 노트 참조.

WinUIRichEditor 0.9.0

Choose a tag to compare

@centwon centwon released this 25 Jul 23:25

원본 AvaloniaRichEditor와의 양방향 수렴(파리티 갭 분석 → 원본→포트 소스 호환 → 포트→원본 역이식 → 원본 감사 결과의 역방향 스윕)과 HWP/Word 상호운용 교정이 이 릴리스의 축입니다. 6·7차 전수 리뷰까지 포함해 결함 20건을 고쳤고, 테스트는 67 → 74로 늘었습니다.

⚠️ 업그레이드 전 확인 (breaking / 동작 변경)

  • 최소 요구 Microsoft.WindowsAppSDK.WinUI 2.2.1 → 2.3.2. 2.2.x에 머무는 앱은 설치할 수 없습니다.
  • 동기 ParseHtml/LoadHtml/InsertHtml이 원격(http) 이미지를 더 이상 받지 않습니다. UI를 최대 5초 멈추던 동기 다운로드를 제거했습니다. 원격 이미지가 필요하면 ParseHtmlAsync/LoadHtmlAsync를 쓰세요.
  • AllowRemoteImagesOnPaste가 붙여넣기 전용이 아닙니다 — LoadHtml/LoadHtmlAsync/InsertHtml까지 관장합니다. 끈 호스트가 LoadHtmlAsync에서 실제로 원격 요청을 보내던 버그의 수정입니다.
  • 목록 토글 해제가 ListLevel/ListMarker까지 지웁니다. 중첩 목록을 끄면 들여쓰기도 함께 사라집니다(종전에는 마커만 사라지고 레벨×20px 들여쓰기가 유령처럼 남았습니다).
  • RTF 출력 형태가 크게 바뀌었습니다 — 행 정의 2회 방출, 모든 문단에 명시 정렬(\ql 포함), 인라인 표를 실제 \trowd 행으로 승격. HWP/Word가 표와 정렬을 제대로 읽도록 하기 위한 변경입니다.
  • 로컬라이제이션 키 ListNone 제거, 툴바 ▾ 드롭다운과 우클릭 메뉴의 "없음" 항목 제거.

주요 추가

  • 읽기 전용 뷰어 캐럿 옵트인 ShowCaretWhenReadOnly(기본 off, 깜빡이지 않음)
  • 원본 API 소스 호환 계층: SetFontFamily / InsertImageBytes / PasteFromClipboardAsync 별칭, FocusDocumentEnd(), InsertInlineTable(r,c), InsertImageFromFileAsync(nint), RichEditorView.ShowStatusBar / ZoomFactor

검증

빌드 0 warn / 0 err · 테스트 74/74 · Native AOT self-contained 게시 재검증(85.2MB, 네이티브 exe 14.2MB, 관리형 IL 어셈블리 0개, 실행 + Win2D 렌더 확인) · HWP 붙여넣기 사람 검증(블록 표·인라인 표·정렬)

전체 변경 내역은 CHANGELOG.md 참조.

v0.8.1 — printing fixes

Choose a tag to compare

@centwon centwon released this 23 Jul 00:32

NuGet: WinUIRichEditor 0.8.1

dotnet add package WinUIRichEditor --version 0.8.1

A patch release fixing printing (RichEditorPrintHelper).

⚠️ Behavior change: ShowPrintUIAsync's dpi default drops from 300 to 150. If you rely on the
default resolution, pass it explicitly. See Changed below.


Fixed

  • RichEditorPrintHelper.ShowPrintUIAsync never printed: the dialog opened but preview stayed on
    "loading" and no output was produced. The PrintDocument and its IPrintDocumentSource were locals,
    and nothing in the print system holds a managed reference back to them — they were collectable as soon
    as the method awaited, taking the pagination/preview/page callbacks with them. Both are now rooted in
    static fields for the life of the job and released on PrintTask.Completed.
  • ShowPrintUIAsync returned false after the dialog had already opened, so callers with a fallback path
    (e.g. export to PDF) showed it on top of a live print dialog. It now reports success once the dialog has
    asked for the document, regardless of what ShowPrintUIForWindowAsync reports afterwards.
  • Page rasters no longer go through WriteableBitmap.PixelBuffer.AsStream(), which only covers buffers
    backed by managed arrays under CsWinRT (.NET 5+) and not the native buffer XAML returns; they are encoded
    to an in-memory PNG and decoded through BitmapImage instead. PDF export was never affected — it uses a
    separate raster path.

Changed

  • ShowPrintUIAsync renders every page before opening the dialog — the PrintDocument callbacks are
    synchronous and image decoding cannot be awaited inside them. Its dpi default drops from 300 to 150
    (matching SavePdf), holding roughly 8 MB per A4 page; lower it further for very long documents.
  • ShowPrintUIAsync now returns false immediately on hosts with dynamic code disabled (Native AOT, or
    any build setting PublishAot, which bakes IsDynamicCodeSupported=false into runtimeconfig.json).
    PrintManagerInterop.ShowPrintUIForWindowAsync casts its result to IAsyncOperation<bool> through
    IDynamicInterfaceCastable, and CsWinRT 2.2 refuses to resolve that ABI helper without dynamic code —
    the throw lands after the native call has already put the dialog on screen, stranding a window with no
    document behind it. Neither CsWinRTAotOptimizerEnabled (it emits CCW vtables only) nor manual
    registration (WinRT.TypeExtensions.RegisterHelperType and the ABI types are internal) covers that
    instantiation, so AOT callers need their own path — e.g. SavePdf and a viewer.

v0.8.0 — first release (AvaloniaRichEditor 0.8.0 parity)

Choose a tag to compare

@centwon centwon released this 23 Jul 00:32

NuGet: WinUIRichEditor 0.8.0

dotnet add package WinUIRichEditor --version 0.8.0

Requires .NET 10 (net10.0-windows10.0.26100.0), Windows App SDK 2.2+, Win2D 1.4.0.


First published release — feature parity with AvaloniaRichEditor 0.8.0. The port was developed in phases
(see Project_Roadmap.md):

Phase 0 — Scaffold

  • WinUI 3 class library src/WinUIRichEditor + unpackaged WinExe demo samples/WinUIRichEditor.Demo,
    solution WinUIRichEditor.slnx. .NET 10, Win2D 1.4.0, IsAotCompatible, code-only control.
  • Verified the Win2D CanvasControl + CanvasTextLayout rendering pipeline end to end.

Phase 1 — Document model + formatters

  • Ported Documents/ (FlowDocument, Paragraph, Run, TableBlock, …) and Formatters/
    (JSON source-gen serializer, .flow package, HTML, RTF, PDF) — byte-compatible document format.
  • Type swaps: IBrush?→Windows.UI.Color?, TextDecorationCollection→[Flags] TextDecorationFlags,
    FontWeight enum→Windows.UI.Text.FontWeight struct, Avalonia brushes→WinRT colors,
    Bitmap→CanvasBitmap (decode deferred to the render layer; natural size parsed from headers via ImageInfo).

Phase 2 — Rendering engine

  • RichEditor : ContentControl hosting a ScrollViewer + CanvasVirtualControl.
  • BuildTextLayout → CanvasTextLayout with per-range formatting; one layout drives render/caret/hit-test.
  • Inline images via SpacerInlineObject : ICanvasTextInlineObject (U+FFFC space reservation) + post-draw
    bitmap from character regions; async image decode cache (ImageCache).
  • Recursive table rendering (LayoutTable), inline tables, run-background highlight.

Phase 3 — Input

  • Focus, pointer hit-testing (HitTest → offset), caret (blink, GetCaretPosition), drag/Shift selection.
  • Self-contained editing core: text insert, Backspace/Delete (+ paragraph merge), Enter split,
    Shift+Enter soft break, arrow/Home/End navigation, Ctrl+A.

Phase 4 — IME

  • Korean/CJK composition via CoreTextEditContext (works in WinUI 3 desktop on Windows 26100).
  • IME buffer = caret paragraph; composing text applied by TextUpdating, underline via FormatUpdating.
  • Hybrid input: CharacterReceived is suppressed only while composing (plain English still arrives there).

Phase 5 — Editing features

  • Undo/redo (UndoManager, snapshot + coalescing), Ctrl+Z/Y.
  • Clipboard: copy/cut/paste (internal rich → external HTML → plain text), CF_HTML out.
  • Formatting commands + demo toolbar: bold/italic/underline/strikethrough, foreground/highlight color,
    alignment, headings, blockquote, bullet/numbered lists, indent.
  • Find / replace with wrap-around and caret scroll-into-view.
  • File open/save (.flow/.json/.html) via pickers + HWND interop.
  • Right-click context menu, block image insert, table cell editing (click-in, type, IME,
    row auto-grow).

Parity completion — tables, images, page view, host controls

  • Tables: Tab cell navigation; right-click row/column insert·delete, cell merge/split; draw-to-size
    toolbar grid picker; column-width & row-height resize (drag borders) for top-level and nested/inline
    tables; table block selection; block↔inline ("treat as character") toggle; inline-table cell editing +
    full arrow-key caret traversal (horizontal cell entry/exit and vertical row navigation / box exit).
  • Images: clipboard image paste; block & inline image selection with aspect-locked resize handles
    (resize cursor); right-click size presets / replace / save / delete; inline image insert.
  • Clipboard: RTF, image, and Excel/TSV→table paste connected.
  • Formatting UI: font/size/color pickers, custom line spacing render + commands, hyperlink edit
    dialog (ContentDialog), format painter.
  • Page view / print / PDF: PageSize/Orientation/ShowPageBoundaries, stacked page render with
    line-aware page breaks, headers/footers/page numbers; RenderPrintPage, SavePdf (rasterized → PDF).
  • Host controls: drop-in RichEditorView + RichEditorToolbar; EditorMode presets + feature flags;
    RichEditorLocalization (KO/EN); accessibility automation peer (IValueProvider).
  • Convenience API: ToHtml/LoadHtml, ToRtf/LoadRtf, ToJson/LoadJson(Async),
    SavePackageAsync/LoadPackageAsync, Clear, GetPlainText, CanUndo/CanRedo, InsertHtml,
    GetCaretFormat, GetStatus, StatusChanged.
  • Tests: tests/WinUIRichEditor.Tests (xUnit.v3, headless dotnet test, OutputType=Exe) — 26
    model/formatter tests. Model/formatter FontWeight defaults moved to plain struct values
    (FontWeightValues) so that layer constructs without activating the WinUI runtime.

Parity polish (2026-06-24)

  • System font enumeration: DefaultFontFamily now defaults to the OS UI font (e.g. "맑은 고딕" on
    Korean Windows) via SystemFontInfo (P/Invoke). New RichEditor.FontFamilyChoices enumerates the
    installed fonts through CanvasFontSet.GetSystemFontSet() with names localized to the OS UI language;
    the toolbar combo and a new right-click font submenu are built from it (each item rendered in its own face).
  • IME: candidate-window placement fixed — CaretScreenRect now returns true physical screen coordinates
    (TransformToVisual(null) × rasterization scale + GetActiveWindow/ClientToScreen client origin), so the
    IME UI tracks the caret. Composition now integrates with undo (PushUndo("ime") coalesces one composition
    into a single undo group; CompositionCompleted resets the key). Table-cell IME verified.
  • Engine-level zoom: new RichEditor.Zoom ([0.25–5.0]) scales the Win2D drawing session and canvas and
    folds into ViewToDoc/DocToView, so text stays crisp at any factor and hit-testing/caret stay correct
    (continuous mode reflows to the zoomed width).
  • Host controls: RichEditorView gained a page/zoom chrome bar — zoom combo, paper-size combo
    (Continuous/A4/A3/A5/B4/B5/Letter/Legal/Tabloid → PageSize + page outline), orientation combo, and a page
    count in the status bar.

Native AOT — works end-to-end

  • Self-contained Native AOT publish (samples/.../PublishProfiles/win-x64.pubxml: PublishAot +
    SelfContained + PublishSingleFile + PublishTrimmed, WindowsAppSDKSelfContained=true) builds
    (0 trim/AOT warnings, ~14 MB native exe), runs, and renders
    — Win2D CanvasTextLayout/CanvasDevice
    and CanvasFontSet all work under AOT. The earlier "crashes at startup in combase 0x80004005" was a
    framework-dependent-only limitation; the self-contained bundle supplies WinRT activation.
  • Build workaround (still required): GenerateLibraryLayout on the library + an MSBuild target stripping
    the stale windowsappsdk.winui\1.8 PRIs that Win2D 1.4.0 pulls in (conflicts with WinAppSDK 2.2's PRI).
    Removable once a WinAppSDK-2.x-aligned Win2D ships (latest is still 1.4.0, depending on WinUI 1.8.x).

API surface, toolbar composition & document-format verification (2026-06-25)

  • Events & appearance: added TextChanged / SelectionChanged / DocumentChanged (driven off the
    PushUndo mutation choke point + a selection snapshot, flushed via RaiseStatusChanged) and
    SelectionBrush / CaretBrush dependency properties (the previously hard-coded caret/selection colors).
  • Icon abstraction: ported RichEditorIcon (45-value enum, original ordinals) + RichEditorIcons.Provider
    host override. Built-in icons are Segoe Fluent Icons FontIcons (ToolbarIcons), resolved
    Provider > FontIcon > text — FontIcon works in the code-only control (only SymbolIcon had crashed).
  • Toolbar composition → original parity: LeadingItems/TrailingItems host slots + a custom
    ToolbarWrapPanel (WinUI ships no WrapPanel); 40-swatch palette + hex-input color pickers with a
    caret-reflecting swatch; list combo-boxes [icon | marker preview | ▾ style menu] + public
    SetListStyle(ListMarkerStyle); line-spacing box [↕ | editable % | ▾ presets | ▲▼ steppers]. Removed the
    quote toolbar button (the original has none — available via right-click / ToggleQuote()).
  • Objects: one image insert button; image and table gained a checkbox "treat as character"
    (ToggleMenuFlyoutItem) toggling block↔inline (ConvertImageBlockToInline/ConvertInlineImageToBlock).
    InsertTable now sizes equal columns to the document (or enclosing cell) content width.
  • View file actions: RichEditorView gained ShowFileActions + Export/Import (JSON/.flow/HTML/RTF via the
    picker, WindowHandle for unpackaged interop) + a host-handled PrintRequested, in the toolbar's trailing slot.
  • Demo: rebuilt as a four-page nav shell — control / read-only / control+toolbar / view.
  • Document format: confirmed the .flow/JSON format is byte-compatible with AvaloniaRichEditor by loading
    a .flow saved by the original (paragraphs + image + table round-tripped). Found that the original stores
    colors as CSS named colors ("Blue"), so ColorUtil was expanded from ~10 names to the full CSS/X11 set.
  • Context menu → original parity: rebuilt with organized Character-format and Paragraph submenus (size,
    color, highlight, font, clear; alignment, list + bullet/number styles, heading, indent, quote), a Margin
    submenu, clipboard Delete, Undo/Redo, inserts, a concise dedicated link menu (with copy-address), a
    read-only branch, and RichEditorIcons glyphs on items.
  • Two-step table insert: pick rows×columns from the 8×10 hover grid (toolbar button or context menu —
    the menu opens the grid as a flyout, since WinUI menus can't host a grid), then drag from the caret on the
    document to set the table's width/height. The table inserts at the caret with the picked rows×columns; the
    rubber-band (anchored at the caret) previews the grid at the dragged size, and a plain click uses the
    default s...
Read more