From 1c5fe54e86fe1c2d242b0e52a3a6ff0447439d54 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 00:49:59 +0000 Subject: [PATCH 1/3] [no-new-version] Math tools: a library of formulas The second page of the math tools: 48 formulas of algebra, geometry, trigonometry, calculus, statistics and physics, each drawn by a MathView, filtered by subject and by name, and opened in the calculator with a click. Tests: every formula parses without errors and its LaTeX reads back, the search, and a formula opened from the window. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SyVio8afJsGwHW9i5v5HDs --- AGENTS.md | 2 +- README.md | 3 +- .../Formulas.cs | 94 +++++++++++++++ .../FormulasPage.cs | 112 ++++++++++++++++++ .../MathToolsView.cs | 29 +++-- .../MathTools/FormulasTests.cs | 40 +++++++ .../MathTools/MathToolsWindowTests.cs | 23 ++++ 7 files changed, 291 insertions(+), 12 deletions(-) create mode 100644 samples/SharpEquations.Avalonia.MathTools.Demo/Formulas.cs create mode 100644 samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs create mode 100644 tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs diff --git a/AGENTS.md b/AGENTS.md index 3194c48..60c3fa4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ generated parser, on netstandard2.0): keep it so, and keep containers and other | `src/SharpEquations.Avalonia/` | The controls for Avalonia: `MathEditBox` (the editor), `MathView`; embeds Libertinus Math | | `samples/SharpEquations.Avalonia.Demo/` | A window with the editor (`DemoView`, shared with the web demo) | | `samples/SharpEquations.Avalonia.Browser.Demo/` | The same demo on WebAssembly (needs the `wasm-tools` workload, so not in the solution; `.github/workflows/demo.yml` publishes it to GitHub Pages) | -| `samples/SharpEquations.Avalonia.MathTools.Demo/` | A second app, math tools around the editor (`MathToolsView`, a page each: the scientific keypad) | +| `samples/SharpEquations.Avalonia.MathTools.Demo/` | A second app, math tools around the editor (`MathToolsView`, a page each: the scientific keypad, the library of formulas) | | `samples/SharpEquations.Avalonia.Browser.MathTools.Demo/` | The math tools on WebAssembly (as the web demo, not in the solution build; `demo.yml` publishes them under `math/`) | | `benchmarks/SharpEquations.Benchmarks/` | BenchmarkDotNet benchmarks of reading, writing and converting the corpora | | `tests/SharpEquations.Tests/` | TUnit tests, in folders by subject (PublicApi, MathML, Omml, Mtef, UnicodeMath, LaTeX, Property, Regression) | diff --git a/README.md b/README.md index ad6cfff..780ce40 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,8 @@ three projects, so that the core keeps no dependency: `SharpEquations.Editing` ( UnicodeMath and LaTeX, and `samples/SharpEquations.Avalonia.Browser.Demo` the same demo in the browser, on WebAssembly (published to GitHub Pages from `main`; to run it locally: `dotnet workload install wasm-tools`, then `dotnet run --project samples/SharpEquations.Avalonia.Browser.Demo`). `samples/SharpEquations.Avalonia.MathTools.Demo` is a -second app around the editor, a set of math tools (a scientific keypad that types into the editor), with +second app around the editor, a set of math tools (a scientific keypad that types into the editor, a library of +formulas), with `samples/SharpEquations.Avalonia.Browser.MathTools.Demo` in the browser (published under `math/` of the same site). ## Architecture diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/Formulas.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/Formulas.cs new file mode 100644 index 0000000..3d59723 --- /dev/null +++ b/samples/SharpEquations.Avalonia.MathTools.Demo/Formulas.cs @@ -0,0 +1,94 @@ +namespace SharpEquations.Avalonia.MathTools +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// A formula of the library: its subject, its name and the formula in UnicodeMath. + public sealed class Formula + { + public Formula(string subject, string name, string unicodeMath) + { + Subject = subject; + Name = name; + UnicodeMath = unicodeMath; + } + + public string Subject { get; } + + public string Name { get; } + + public string UnicodeMath { get; } + } + + /// The library of formulas, by subject. + public static class Formulas + { + public static IReadOnlyList All { get; } = new[] + { + new Formula("Algebra", "Quadratic formula", "x=(-b±√(b^2-4ac))/2a"), + new Formula("Algebra", "Difference of squares", "a^2-b^2=(a+b)(a-b)"), + new Formula("Algebra", "Square of a sum", "(a+b)^2=a^2+2ab+b^2"), + new Formula("Algebra", "Binomial theorem", "(a+b)^n=∑_(k=0)^n (n¦k) a^k b^(n-k)"), + new Formula("Algebra", "Arithmetic series", "∑_(k=1)^n k=(n(n+1))/2"), + new Formula("Algebra", "Geometric series", "∑_(k=0)^(n-1) ar^k=a (1-r^n)/(1-r)"), + new Formula("Algebra", "Logarithm of a product", "log_b⁡(xy)=log_b⁡x+log_b⁡y"), + new Formula("Algebra", "Change of base", "log_b⁡x=(ln⁡x)/(ln⁡b)"), + new Formula("Algebra", "Inverse of a 2×2 matrix", "(■(a&b@c&d))^(-1)=1/(ad-bc) (■(d&-b@-c&a))"), + + new Formula("Geometry", "Pythagorean theorem", "a^2+b^2=c^2"), + new Formula("Geometry", "Area of a circle", "A=πr^2"), + new Formula("Geometry", "Circumference", "C=2πr"), + new Formula("Geometry", "Area of a triangle", "A=1/2 bh"), + new Formula("Geometry", "Heron's formula", "A=√(s(s-a)(s-b)(s-c))"), + new Formula("Geometry", "Volume of a sphere", "V=4/3 πr^3"), + new Formula("Geometry", "Surface of a sphere", "S=4πr^2"), + new Formula("Geometry", "Volume of a cone", "V=1/3 πr^2 h"), + new Formula("Geometry", "Distance between two points", "d=√((x_2-x_1)^2+(y_2-y_1)^2)"), + + new Formula("Trigonometry", "Pythagorean identity", "sin^2⁡θ+cos^2⁡θ=1"), + new Formula("Trigonometry", "Tangent", "tan⁡θ=sin⁡θ/cos⁡θ"), + new Formula("Trigonometry", "Sine of a sum", "sin⁡(α+β)=sin⁡α cos⁡β+cos⁡α sin⁡β"), + new Formula("Trigonometry", "Cosine of a sum", "cos⁡(α+β)=cos⁡α cos⁡β-sin⁡α sin⁡β"), + new Formula("Trigonometry", "Double angle", "sin⁡2θ=2 sin⁡θ cos⁡θ"), + new Formula("Trigonometry", "Law of sines", "a/sin⁡α=b/sin⁡β=c/sin⁡γ"), + new Formula("Trigonometry", "Law of cosines", "c^2=a^2+b^2-2ab cos⁡γ"), + new Formula("Trigonometry", "Euler's formula", "e^(iθ)=cos⁡θ+i sin⁡θ"), + + new Formula("Calculus", "Definition of the derivative", "f′(x)=lim┬(h→0) (f(x+h)-f(x))/h"), + new Formula("Calculus", "Product rule", "(fg)′=f′g+fg′"), + new Formula("Calculus", "Chain rule", "ⅆ/ⅆx f(g(x))=f′(g(x)) g′(x)"), + new Formula("Calculus", "Power rule", "ⅆ/ⅆx x^n=nx^(n-1)"), + new Formula("Calculus", "Fundamental theorem", "∫_a^b f′(x) ⅆx=f(b)-f(a)"), + new Formula("Calculus", "Integration by parts", "∫ u ⅆv=uv-∫ v ⅆu"), + new Formula("Calculus", "Taylor series", "f(x)=∑_(n=0)^∞ (f^((n)) (a))/n! (x-a)^n"), + new Formula("Calculus", "Gaussian integral", "∫_-∞^∞ e^(-x^2) ⅆx=√π"), + new Formula("Calculus", "A limit", "lim┬(x→0) sin⁡x/x=1"), + + new Formula("Statistics", "Mean", "x̄=1/n ∑_(i=1)^n x_i"), + new Formula("Statistics", "Standard deviation", "σ=√(1/N ∑_(i=1)^N (x_i-μ)^2)"), + new Formula("Statistics", "Binomial coefficient", "(n¦k)=n!/(k!(n-k)!)"), + new Formula("Statistics", "Normal distribution", "f(x)=1/(σ√2π) e^(-1/2 ((x-μ)/σ)^2)"), + new Formula("Statistics", "Bayes' theorem", "P(A|B)=(P(B|A) P(A))/P(B)"), + + new Formula("Physics", "Newton's second law", "𝐅=m𝐚"), + new Formula("Physics", "Mass–energy equivalence", "E=mc^2"), + new Formula("Physics", "Kinetic energy", "E_k=1/2 mv^2"), + new Formula("Physics", "Uniform acceleration", "s=v_0 t+1/2 at^2"), + new Formula("Physics", "Universal gravitation", "F=G (m_1 m_2)/r^2"), + new Formula("Physics", "Coulomb's law", "F=1/(4πε_0) (q_1 q_2)/r^2"), + new Formula("Physics", "Ideal gas law", "pV=nRT"), + new Formula("Physics", "Schrödinger equation", "iℏ ∂/∂t Ψ=ĤΨ"), + }; + + /// The subjects, in the order of the library. + public static IReadOnlyList Subjects { get; } = All.Select(f => f.Subject).Distinct().ToList(); + + /// The formulas of a subject (all of them for null) whose name or subject contains the text, ignoring case. + public static IEnumerable Find(string? subject, string? text) => All.Where(f => + (subject == null || f.Subject == subject) + && (string.IsNullOrWhiteSpace(text) + || f.Name.Contains(text!.Trim(), StringComparison.OrdinalIgnoreCase) + || f.Subject.Contains(text!.Trim(), StringComparison.OrdinalIgnoreCase))); + } +} diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs new file mode 100644 index 0000000..7acefa9 --- /dev/null +++ b/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs @@ -0,0 +1,112 @@ +namespace SharpEquations.Avalonia.MathTools +{ + using System; + using System.Collections.Generic; + using System.Linq; + using global::Avalonia; + using global::Avalonia.Controls; + using global::Avalonia.Controls.Primitives; + using global::Avalonia.Layout; + using global::Avalonia.Media; + using SharpEquations; + + /// + /// The library of formulas: each one drawn by a , filtered by subject and by name, and opened in + /// the calculator with a click. + /// + public sealed class FormulasPage : UserControl + { + private static readonly EquationOptions Display = new EquationOptions { Display = true }; + + private readonly Action open; + private readonly List<(Formula Formula, Button Card)> cards = new List<(Formula, Button)>(); + private readonly List subjects = new List(); + private string? subject; + + public FormulasPage(Action open) + { + this.open = open; + + Search = new TextBox { PlaceholderText = "Search a formula", Width = 260 }; + Search.TextChanged += (_, _) => Filter(); + + var chips = Ui.Row(Search); + foreach (var name in new string?[] { null }.Concat(Formulas.Subjects)) + { + var chip = new ToggleButton + { + Content = name ?? "All", + IsChecked = name == null, + CornerRadius = new CornerRadius(16), + Padding = new Thickness(14, 5), + }; + chip.Click += (_, _) => ShowSubject(name); + subjects.Add(chip); + chips.Children.Add(chip); + } + + var list = new WrapPanel { Orientation = Orientation.Horizontal, ItemSpacing = 12, LineSpacing = 12 }; + foreach (var formula in Formulas.All) + { + var card = Card(formula); + cards.Add((formula, card)); + list.Children.Add(card); + } + + Content = Ui.Card("Formulas", "A library of formulas of algebra, geometry, trigonometry, calculus, statistics and physics: click one to open it in the calculator", new StackPanel + { + Spacing = 16, + Children = { chips, list }, + }); + } + + /// The search box: the formulas whose name or subject contain its text. + public TextBox Search { get; } + + /// The formulas shown, after the subject and the search. + public IEnumerable Shown => cards.Where(c => c.Card.IsVisible).Select(c => c.Formula); + + /// Shows the formulas of a subject, or all of them for null. + public void ShowSubject(string? name) + { + subject = name; + for (var i = 0; i < subjects.Count; i++) + subjects[i].IsChecked = (i == 0 ? null : Formulas.Subjects[i - 1]) == name; + Filter(); + } + + /// Opens a formula in the calculator, as a click on it does. + public void Open(Formula formula) => open(Equation.Parse(formula.UnicodeMath, EquationFormat.UnicodeMath, Display)); + + private void Filter() + { + var shown = new HashSet(Formulas.Find(subject, Search.Text)); + foreach (var (formula, card) in cards) + card.IsVisible = shown.Contains(formula); + } + + private Button Card(Formula formula) + { + var caption = Ui.Muted(formula.Subject.ToUpperInvariant() + " · " + formula.Name, 11.5); + var view = Ui.Math(formula.UnicodeMath, 19, display: true); + view.HorizontalAlignment = HorizontalAlignment.Left; + var button = new Button + { + Width = 352, + MinHeight = 108, + CornerRadius = new CornerRadius(12), + Padding = new Thickness(14, 10), + BorderThickness = new Thickness(1), + Focusable = false, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Top, + Content = new StackPanel { Spacing = 8, Children = { caption, view } }, + }; + button.Bind(Button.BackgroundProperty, button.GetResourceObservable("CodeBrush")); + button.Bind(Button.BorderBrushProperty, button.GetResourceObservable("CardBorderBrush")); + ToolTip.SetTip(button, "Open in the calculator"); + button.Click += (_, _) => Open(formula); + return button; + } + } +} diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs index 39d35cd..ae66ffb 100644 --- a/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs +++ b/samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs @@ -9,8 +9,8 @@ namespace SharpEquations.Avalonia.MathTools using SharpEquations; /// - /// The math tools, a page each: the scientific keypad around the equation editor. The desktop window and the - /// WebAssembly page show the same view. + /// The math tools, a page each: the scientific keypad around the equation editor, and the library of formulas. The + /// desktop window and the WebAssembly page show the same view. /// public sealed class MathToolsView : UserControl { @@ -20,17 +20,20 @@ public MathToolsView() { Ui.AddBrushes(Resources); Calculator = new CalculatorPage(); - - var tabs = new TabControl + Tabs = new TabControl { Margin = new Thickness(20, 12, 20, 0) }; + FormulaLibrary = new FormulasPage(equation => { - Margin = new Thickness(20, 12, 20, 0), - ItemsSource = new[] - { - Tab("Calculator", Calculator), - }, + Calculator.Editor.Equation = equation; + Tabs.SelectedIndex = 0; + Calculator.Editor.Focus(); + }); + Tabs.ItemsSource = new[] + { + Tab("Calculator", Calculator), + Tab("Formulas", FormulaLibrary), }; - var root = new DockPanel { Children = { Ui.Docked(Header(), Dock.Top), tabs } }; + var root = new DockPanel { Children = { Ui.Docked(Header(), Dock.Top), Tabs } }; root.Bind(Panel.BackgroundProperty, root.GetResourceObservable("PageBrush")); Content = root; } @@ -38,6 +41,12 @@ public MathToolsView() /// The page of the scientific keypad. public CalculatorPage Calculator { get; } + /// The page of the library of formulas. + public FormulasPage FormulaLibrary { get; } + + /// The tabs of the pages. + public TabControl Tabs { get; } + private static TabItem Tab(string header, Control page) => new TabItem { Header = new TextBlock { Text = header, FontSize = 15, FontWeight = FontWeight.SemiBold }, diff --git a/tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs b/tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs new file mode 100644 index 0000000..8389220 --- /dev/null +++ b/tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs @@ -0,0 +1,40 @@ +#if NET10_0_OR_GREATER +namespace SharpEquations.Avalonia.Tests.MathTools +{ + using System.Linq; + using System.Threading.Tasks; + using SharpEquations; + using SharpEquations.Avalonia.MathTools; + + /// The formulas of the library parse without errors, and their LaTeX reads back. + public class FormulasTests + { + private static readonly EquationOptions Display = new EquationOptions { Display = true }; + + public static System.Collections.Generic.IEnumerable Names() => Formulas.All.Select(f => f.Name); + + [Test] + [MethodDataSource(nameof(Names))] + public async Task FormulaParses(string name) + { + var formula = Formulas.All.Single(f => f.Name == name); + var equation = Equation.Parse(formula.UnicodeMath, EquationFormat.UnicodeMath, Display); + await Assert.That(equation.ToMathML().Descendants().Any(e => e.Name.LocalName == "merror")).IsFalse(); + await Assert.That(Equation.TryParse(equation.ToString(EquationFormat.LaTeX), EquationFormat.LaTeX, out _)).IsTrue(); + } + + [Test] + public async Task NamesAreUnique() => await Assert.That(Formulas.All.Select(f => f.Name).Distinct().Count()).IsEqualTo(Formulas.All.Count); + + [Test] + public async Task FindBySubjectAndName() + { + await Assert.That(Formulas.Find(null, null).Count()).IsEqualTo(Formulas.All.Count); + await Assert.That(Formulas.Find("Geometry", null).All(f => f.Subject == "Geometry")).IsTrue(); + await Assert.That(Formulas.Find(null, " PYTHAGOREAN ").Select(f => f.Name)).IsEquivalentTo(new[] { "Pythagorean theorem", "Pythagorean identity" }); + await Assert.That(Formulas.Find("Trigonometry", "pythagorean").Single().Name).IsEqualTo("Pythagorean identity"); + await Assert.That(Formulas.Find(null, "calculus").Count()).IsEqualTo(Formulas.Find("Calculus", null).Count()); + } + } +} +#endif diff --git a/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs b/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs index ef42b8b..c4b2dbc 100644 --- a/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs +++ b/tests/SharpEquations.Avalonia.Tests/MathTools/MathToolsWindowTests.cs @@ -45,6 +45,29 @@ public async Task KeypadTypesIntoTheEditor() await Assert.That(equation).IsEqualTo(Equation.Parse("sin(x)^2+cos(x)^2=1", EquationFormat.UnicodeMath, new EquationOptions { Display = true })); await Assert.That(unicodeMath).IsNotNull(); } + + [Test] + public async Task FormulaOpensInTheCalculator() + { + var (shown, equation, tab) = await Headless.Run(() => + { + var window = new MainWindow(); + window.Show(); + var view = window.GetVisualDescendants().OfType().Single(); + view.Tabs.SelectedIndex = 1; + view.FormulaLibrary.Search.Text = "law"; + Save(window, "math-tools-formulas"); + var names = view.FormulaLibrary.Shown.Select(f => f.Name).ToList(); + view.FormulaLibrary.Open(Formulas.All.Single(f => f.Name == "Law of cosines")); + var result = (names, view.Calculator.Editor.Equation, view.Tabs.SelectedIndex); + window.Close(); + return result; + }); + await Assert.That(shown).Contains("Law of sines"); + await Assert.That(shown).DoesNotContain("Pythagorean theorem"); + await Assert.That(equation).IsEqualTo(Equation.Parse("c^2=a^2+b^2-2ab cos⁡γ", EquationFormat.UnicodeMath, new EquationOptions { Display = true })); + await Assert.That(tab).IsEqualTo(0); + } } } #endif From 81864604285f095ca1beb80fdc045849effe6fdf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 07:45:36 +0000 Subject: [PATCH 2/3] [no-new-version] Math tools: the search filters the formulas as it is typed TextChanged is raised later, on the dispatcher: the formulas follow the Text property instead, as the value of the unit converter does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SyVio8afJsGwHW9i5v5HDs --- .../SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs b/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs index 7acefa9..1820ded 100644 --- a/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs +++ b/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs @@ -28,7 +28,12 @@ public FormulasPage(Action open) this.open = open; Search = new TextBox { PlaceholderText = "Search a formula", Width = 260 }; - Search.TextChanged += (_, _) => Filter(); + // the property, not TextChanged, which Avalonia raises later, on the dispatcher + Search.PropertyChanged += (_, e) => + { + if (e.Property == TextBox.TextProperty) + Filter(); + }; var chips = Ui.Row(Search); foreach (var name in new string?[] { null }.Concat(Formulas.Subjects)) From 0e0fb2a9d911414338fdb2e91346a7ab69aeda86 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 18:19:08 +0000 Subject: [PATCH 3/3] [no-new-version] Math tools: the UnicodeMath of every formula reads back again Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SyVio8afJsGwHW9i5v5HDs --- tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs b/tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs index 8389220..f725fcb 100644 --- a/tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs +++ b/tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs @@ -6,7 +6,7 @@ namespace SharpEquations.Avalonia.Tests.MathTools using SharpEquations; using SharpEquations.Avalonia.MathTools; - /// The formulas of the library parse without errors, and their LaTeX reads back. + /// The formulas of the library parse without errors, and their UnicodeMath and LaTeX read back. public class FormulasTests { private static readonly EquationOptions Display = new EquationOptions { Display = true }; @@ -20,6 +20,7 @@ public async Task FormulaParses(string name) var formula = Formulas.All.Single(f => f.Name == name); var equation = Equation.Parse(formula.UnicodeMath, EquationFormat.UnicodeMath, Display); await Assert.That(equation.ToMathML().Descendants().Any(e => e.Name.LocalName == "merror")).IsFalse(); + await Assert.That(Equation.Parse(equation.ToString(EquationFormat.UnicodeMath), EquationFormat.UnicodeMath, Display)).IsEqualTo(equation); await Assert.That(Equation.TryParse(equation.ToString(EquationFormat.LaTeX), EquationFormat.LaTeX, out _)).IsTrue(); }