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_bx+log_by"),
+ new Formula("Algebra", "Change of base", "log_bx=(lnx)/(lnb)"),
+ 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", "sin2θ=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) sinx/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..1820ded
--- /dev/null
+++ b/samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs
@@ -0,0 +1,117 @@
+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 };
+ // 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))
+ {
+ 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..f725fcb
--- /dev/null
+++ b/tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs
@@ -0,0 +1,41 @@
+#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 UnicodeMath and LaTeX read 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.Parse(equation.ToString(EquationFormat.UnicodeMath), EquationFormat.UnicodeMath, Display)).IsEqualTo(equation);
+ 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