Skip to content

Equation editor for Avalonia: MathEditBox, MathView, and their packages - #33

Open
simonebortolin wants to merge 35 commits into
claude/xslt-generatorfrom
claude/focused-clarke-f4k4a9
Open

simonebortolin wants to merge 35 commits into
claude/xslt-generatorfrom
claude/focused-clarke-f4k4a9

Conversation

@simonebortolin

@simonebortolin simonebortolin commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

A real-time equation editor for Avalonia in the manner of the RichEditBox of WinUI in math mode (math only): UnicodeMath or LaTeX typed at the caret builds up into the equation when a space or an operator ends an argument, and the equation is read and written as MathML (GetMath/SetMath) or as an Equation. The design is in docs/EDITOR.md.

Projects

Project Target Content
SharpEquations.Editing netstandard2.0, net10.0, no UI MathNode tree with runs of linear text, caret, selection, undo (a bounded stack of immutable trees), build up and build down
SharpEquations.Rendering netstandard2.0, net10.0, no UI Layout of MathML Core over the OpenType MATH table; glyph boxes from the glyf/CFF outlines, ssty variants from GSUB; caret and selection geometry
SharpEquations.Avalonia net8.0, net10.0 MathEditBox, MathView, the floating bar (MathQuickBar); Cambria Math on Windows, embedded Libertinus Math (OFL) elsewhere
samples/SharpEquations.Avalonia.Demo net10.0 A window with the tools of the equations of Word

Features

  • Build up as in Word; linear view (Ctrl+Shift+L); UnicodeMath or LaTeX input (read as Word reads it)
  • Caret through the slots, Up/Down between slots and lines; Backspace selects a structure, then deletes it; undo/redo
  • Copy in any format (GetSelectedText, CopyTextFormat: MathML pastes into Word, Copying event), paste of MathML/UnicodeMath/LaTeX
  • Equation numbers on the right (#(1), \tag{1}, EquationNumber)
  • Lines: Shift+Enter makes an equation array aligned at &; automatic line breaks of long display equations before the operators
  • Matrices and tables: insert/delete rows and columns, Tab between cells
  • Completion of commands and a floating bar (completions, structures for the selection, table commands, quick symbols)
  • Accessibility: MathEditBoxAutomationPeer, the equation as UnicodeMath

Core

The core's only change is new public API: EquationCommand and EquationCommands (the control words of UnicodeMath and the commands of LaTeX with their symbols), in PublicAPI.Unshipped.txt, checked against the parsers by PublicApi/EquationCommandsTests. It calls for a minor version: VersionPrefix is still 1.0.5 and the commits say [no-new-version], so the bump is to be made before merging.

Release

The CI tests the editor (tests/SharpEquations.Avalonia.Tests, net10.0, and net8.0 on Linux), and packs and publishes the three packages of the editor with the core, with one version. Their public API is tracked in PublicAPI.*.txt. The package IDs SharpEquations.Editing, .Rendering and .Avalonia are new on nuget.org: the Trusted Publishing policy of NUGET_USER has to cover them.

Tests

  • Editor: 84 tests on net10.0, 83 on net8.0 (the demo test is net10.0 only). They cover the UnicodeMathML corpus, the LaTeX of latex2mathml, the OMML of Word documents, the MTEF and mml2tex data, the controls in a headless Avalonia session with Skia, and the demo window.
    • Loading an equation into the editor, and building it down to linear text and up again, gives the same equation (UnicodeMath corpus at 100%).
    • Typing one character at a time gives the equation of the whole text for 93% of the UnicodeMath corpus and 90% of the LaTeX corpus.
  • dotnet test --solution SharpEquations.slnx: 5673 passed. No test was run on Windows or macOS, or with a real window.

Generated by Claude Code

claude and others added 27 commits November 9, 2026 00:00
A real-time equation editor in the manner of the RichEditBox of WinUI in math
mode (math only): UnicodeMath or LaTeX typed at the caret builds up when a
space or an operator ends an argument, and the equation is read and written as
MathML (GetMath/SetMath) or as an Equation. Three projects, the core unchanged:

- SharpEquations.Editing (netstandard2.0, net10.0, no UI): the tree of
  MathNodes with a row in every slot and runs of linear text, the caret, the
  selection, undo, build up (with the nodes before the run written back as
  typed, or as placeholders) and build down to linear text.
- SharpEquations.Rendering (netstandard2.0, net10.0, no UI): the layout of
  MathML Core over the OpenType MATH table, with the ink boxes read from the
  glyf or CFF outlines of the font, and the geometry of caret and selection.
- SharpEquations.Avalonia (net8.0, net10.0): MathEditBox, MathView, the fonts
  (Cambria Math on Windows, the embedded Libertinus Math elsewhere, OFL).

With a demo window, the tests of the three (the controls in a headless
session with Skia), CI for them, and docs/EDITOR.md updated to the design as
built.

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

- Copy: GetSelectedText in any format (UnicodeMath, LaTeX, MathML, OMML),
  CopyTextFormat (MathML pastes as an equation into Word), the Copying event.
- Equation numbers: #(1) or \tag{1} typed, or EquationNumber; the number at
  the right edge of the line (MathLayoutOptions.LineWidth, side="left").
- Commands: InsertStructure, InsertFences, InsertMatrix, InsertTemplate, Tab
  through the cells of a table, UndoLimit (the history forgets the oldest).
- Accessibility: MathEditBoxAutomationPeer, the equation as UnicodeMath.
- LaTeX typing: a control word followed by a space waits for its argument,
  and words of LaTeX are written back as \operatorname (86% to 90% of the
  latex2mathml corpus typed a character at a time).
- Tests on the other corpora of the repository (latex2mathml, the OMML of
  Word documents, mml2tex, MTEF), of undo and delete, of the demo window.
- A demo with the tools of the equations of Word.
- CI packs and publishes the three packages of the editor with the core;
  their public API is tracked with PublicApiAnalyzers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiaNyPsgRSRubdbT58H
…s, script variants, floating bar

- Core: EquationCommand and EquationCommands (public, unshipped): the control
  words of UnicodeMath and the commands of LaTeX with their symbols, checked
  against the parsers by PublicApi/EquationCommandsTests.
- Editor: completion of commands (CommandPrefix, GetCompletions, Complete);
  lines (InsertLineBreak, Shift+Enter: equation arrays aligned at &, joined by
  Backspace and Delete, built up as lines of an array); rows and columns of
  tables (InsertTableRow, InsertTableColumn, DeleteTableRow,
  DeleteTableColumn).
- Rendering: alignment marks (malignmark) in tables, equation arrays in display
  style; the ssty variants of the GSUB table in scripts (ScriptStyles); cmap
  and advances read from the font file.
- Avalonia: the floating bar (MathQuickBar) with completions, structures for
  the selection, table commands and quick symbols; the glyph typeface asked
  for at each drawing and the embedded font registered with the current font
  manager, so that no released typeface is drawn with.
- Demo and docs updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiaNyPsgRSRubdbT58H
A display equation wider than its line (MathLayoutOptions.LineWidth, the width
of the control) breaks before its binary operators and relations, the last one
that fits each line, as Word does, with the lines after the first indented
(BreakLines = false keeps one line). The row stays one row for the editor: the
geometry has a line for each caret position, so that the caret, the selection
(a rectangle a line), clicks and Up and Down
(IMathGeometry.GetPositionOnAdjacentLine) follow the lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiaNyPsgRSRubdbT58H
The floating bar took a light background of its own and the foreground of the
theme, light in the dark theme: its text could not be read. It now takes the
colours of the flyouts of the application's theme (Fluent, Simple), or a light
or a dark set by the theme variant, and follows a change of variant; the
background of text that does not parse (merror) is dark in the dark theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiaNyPsgRSRubdbT58H
…F-8 bytes

Word writes the "MathML" clipboard format as UTF-8 bytes; read as a string of
Windows, as UTF-16, each pair of bytes became a CJK character, and the MathML
that did not parse was inserted as text, before the plain text of the
clipboard. The format is now read and written as bytes (MathClipboard: UTF-8,
or UTF-16 with its byte order mark), and MathML or OMML that does not parse
inserts nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiaNyPsgRSRubdbT58H
samples/SharpEquations.Avalonia.Browser.Demo runs the demo in the browser with
Avalonia.Browser: Inter for the text and the embedded Libertinus Math as its
fallback, System.Private.Xml kept whole for the OMML stylesheets, a splash with
the download progress. The desktop window and the page share DemoView, now
redesigned: a header, cards, a ribbon of structures drawn as formulas, symbols
by group, a gallery of examples rendered with MathView, the outputs with a copy
button, the light and the dark theme, and a layout for narrow screens.

The project needs the wasm-tools workload, so it is not in the solution:
.github/workflows/demo.yml builds it and publishes it to GitHub Pages from main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyVio7wKz4nEifoqJzu9wA
The default math font is Libertinus Math everywhere but Windows, so "Default"
and "Libertinus Math" were the same font, and Cambria Math was offered where it
is not installed: the change of font showed nothing. The demo embeds STIX Two
Math (SIL Open Font License), names the default font, offers Cambria Math only
where it is a math font, and applies the font to the gallery too.
DemoTests.FontChangesTheLayout checks it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyVio7wKz4nEifoqJzu9wA
…efault font to another and back

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyVio7wKz4nEifoqJzu9wA
… new structures of LaTeX in the commands

After the rebase on main, the LaTeX reader reads the arguments of a command without
braces (\underset ab, \xrightarrow[y] x, \boxed \Box, \not a): the editor no longer builds
up at a space while a command still awaits its arguments, counted from EquationCommands.
The commands list the structures the reader now reads: the extensible arrows of amsmath
and mathtools, the arrows under a base, \dddot, \ddddot, \mathscr, \not, \substack,
\prescript and \ce, \pu of mhchem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyVioB6ZwExLPtzuBzNTgX
…with, by name

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyVio7wKz4nEifoqJzu9wA
…e web demo

ctop.xsl, which came from main after the branch, is compiled by the generator
too (CtopStylesheet). The stylesheets now count code points as XPath does, not
UTF-16 units as XslCompiledTransform did: an emoji (🍕) is an identifier of one
letter, written italic in OMML in one run with its neighbours, and goes through
OMML and back as the same equation; OmmlAstralTests pins that now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyVio7wKz4nEifoqJzu9wA
…h the MATH table

Cambria and Cambria Math share their glyphs in cambria.ttc, so the number of
glyphs matched Cambria, the first face, which has no MATH table: Cambria Math
was never a math font, the default font of Windows was Libertinus Math, and
choosing Cambria Math changed nothing. Of the faces with the number of glyphs,
FontFile now reads the one with a MATH table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyVio7wKz4nEifoqJzu9wA
…ound for XslCompiledTransform

With the stylesheets compiled to C#, nothing generates code at run time: the
release build of the web demo is compiled to WebAssembly ahead of time, and is
trimmed with no warning, without keeping System.Private.Xml whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyVio7wKz4nEifoqJzu9wA
…hen run by hand

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyVio7wKz4nEifoqJzu9wA
Right click, a long press on touch, the Menu key or Shift+F10 open a context
menu: undo, redo, cut, copy, paste, copy as MathML/UnicodeMath/LaTeX/OMML,
select all, linear/professional, build up/down, new line, equation number and
the rows and columns of a table. A right click outside the selection moves the
caret first; a read-only editor has only the copy commands and select all.
Apps change it with ContextMenuOpening or CreateContextMenu, or turn it off
with IsContextMenuEnabled.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiaNyPsgRSRubdbT58H
MathAutoCorrect replaces \alpha and a space by α (the commands of EquationCommands
with a symbol) and the sequences of Word (-> → ≤ ≠ ± …) as they are typed, with a list
of entries the application changes; MathEditor.AutoCorrect, and MathEditBox.AutoCorrect
and IsAutoCorrectEnabled. An operator that starts a sequence waits for the next
character to build up, and an undo right after a replacement gives back what was typed.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiaNyPsgRSRubdbT58H
…at each keystroke

- MathLayoutCache (Rendering, public): the boxes of the nodes laid out, by node reference, style and stretch,
  with the paths of their rows relative to the node (anchored where the node is), so that after an edit only
  the nodes it replaced are laid out again; the glyphs, advances and ink boxes of the font asked of it once
  (GlyphMetrics). MathLayoutEngine.Layout(root, font, options, cache).
- MathEditBox lays out again only when the tree, the font, the size or the width changed, redraws the equation
  only when the tree or the selection changed, and draws the caret in a layer of its own, which alone is drawn
  again when it blinks or moves. MathView keeps the tree of its equation and a cache.
- MathLayout finds the rows by path without building strings.
- Benchmarks of the editor (EditorBenchmarks), and LayoutCacheTests: the layout with the cache is the layout
  without it, glyph for glyph and caret for caret, on the corpus, typing, and edits inside structures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8QQiaNyPsgRSRubdbT58H
@simonebortolin simonebortolin changed the title Claude/focused clarke f4k4a9 Equation editor for Avalonia: MathEditBox, MathView, and their packages Nov 11, 2026
@simonebortolin
simonebortolin added this pull request to stack #34 November 11, 2026 00:08
…onia

# Conflicts:
#	tests/SharpEquations.Tests/Omml/OmmlAstralTests.cs
…esheet

The Browser demo, in the solution since 9894ade, needs the wasm-tools workload,
which the test and CodeQL jobs do not install (NETSDK1147 on Linux and macOS): it
stays in the solution, built by demo.yml only. The merge of claude/xslt-generator
declared CtopStylesheet a second time; it is declared once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyVioBuiMiSScbUYtdXLXq
…emoji bitmaps) has the line's box

The layout read the ink box of a glyph from the outlines of its font; a font with
bitmaps alone (Noto Color Emoji as CBDT, the one of the Linux runners) has none, so an
emoji was 0 high and the corpus test failed there.

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

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Pre-release 1.1.0-pr.33.117 is ready: https://github.com/simonebortolin/SharpEquations/releases/tag/pr-33-117-1

NuGet package of 2e11345. Updated on every push, deleted when this PR is closed.

…en they differ

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

The font Avalonia matches to a code point (TryMatchCharacter) can change with the
fonts it has loaded since: the layout without a cache, which asked again at every
layout, drew some characters (│, ┬, ▭, ᅲ, emoji) with another font than the one
the layout cache kept, and LayoutCacheTests failed on the Linux and macOS runners.
The first font matched to a code point is kept. The offset of the cmap subtable is
no longer -1 while it is being looked for.

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

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants