Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
94 changes: 94 additions & 0 deletions samples/SharpEquations.Avalonia.MathTools.Demo/Formulas.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
namespace SharpEquations.Avalonia.MathTools
{
using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>A formula of the library: its subject, its name and the formula in UnicodeMath.</summary>
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; }
}

/// <summary>The library of formulas, by subject.</summary>
public static class Formulas
{
public static IReadOnlyList<Formula> 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 Ψ=ĤΨ"),
};

/// <summary>The subjects, in the order of the library.</summary>
public static IReadOnlyList<string> Subjects { get; } = All.Select(f => f.Subject).Distinct().ToList();

/// <summary>The formulas of a subject (all of them for null) whose name or subject contains the text, ignoring case.</summary>
public static IEnumerable<Formula> 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)));
}
}
117 changes: 117 additions & 0 deletions samples/SharpEquations.Avalonia.MathTools.Demo/FormulasPage.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// The library of formulas: each one drawn by a <see cref="MathView"/>, filtered by subject and by name, and opened in
/// the calculator with a click.
/// </summary>
public sealed class FormulasPage : UserControl
{
private static readonly EquationOptions Display = new EquationOptions { Display = true };

private readonly Action<Equation> open;
private readonly List<(Formula Formula, Button Card)> cards = new List<(Formula, Button)>();
private readonly List<ToggleButton> subjects = new List<ToggleButton>();
private string? subject;

public FormulasPage(Action<Equation> 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 },
});
}

/// <summary>The search box: the formulas whose name or subject contain its text.</summary>
public TextBox Search { get; }

/// <summary>The formulas shown, after the subject and the search.</summary>
public IEnumerable<Formula> Shown => cards.Where(c => c.Card.IsVisible).Select(c => c.Formula);

/// <summary>Shows the formulas of a subject, or all of them for null.</summary>
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();
}

/// <summary>Opens a formula in the calculator, as a click on it does.</summary>
public void Open(Formula formula) => open(Equation.Parse(formula.UnicodeMath, EquationFormat.UnicodeMath, Display));

private void Filter()
{
var shown = new HashSet<Formula>(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;
}
}
}
29 changes: 19 additions & 10 deletions samples/SharpEquations.Avalonia.MathTools.Demo/MathToolsView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ namespace SharpEquations.Avalonia.MathTools
using SharpEquations;

/// <summary>
/// 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.
/// </summary>
public sealed class MathToolsView : UserControl
{
Expand All @@ -20,24 +20,33 @@ 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;
}

/// <summary>The page of the scientific keypad.</summary>
public CalculatorPage Calculator { get; }

/// <summary>The page of the library of formulas.</summary>
public FormulasPage FormulaLibrary { get; }

/// <summary>The tabs of the pages.</summary>
public TabControl Tabs { get; }

private static TabItem Tab(string header, Control page) => new TabItem
{
Header = new TextBlock { Text = header, FontSize = 15, FontWeight = FontWeight.SemiBold },
Expand Down
41 changes: 41 additions & 0 deletions tests/SharpEquations.Avalonia.Tests/MathTools/FormulasTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>The formulas of the library parse without errors, and their UnicodeMath and LaTeX read back.</summary>
public class FormulasTests
{
private static readonly EquationOptions Display = new EquationOptions { Display = true };

public static System.Collections.Generic.IEnumerable<string> 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
Original file line number Diff line number Diff line change
Expand Up @@ -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<MathToolsView>().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
Loading