diff --git a/WinFormsApp.Example/Form1.Designer.cs b/WinFormsApp.Example/Form1.Designer.cs new file mode 100644 index 00000000..5a8bbbc8 --- /dev/null +++ b/WinFormsApp.Example/Form1.Designer.cs @@ -0,0 +1,39 @@ +namespace WinFormsApp.Example; + +partial class Form1 +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Text = "Form1"; + } + + #endregion +} diff --git a/WinFormsApp.Example/Form1.cs b/WinFormsApp.Example/Form1.cs new file mode 100644 index 00000000..a1e92270 --- /dev/null +++ b/WinFormsApp.Example/Form1.cs @@ -0,0 +1,19 @@ +using WinFormsMath.Controls; + +namespace WinFormsApp.Example; + +public partial class Form1 : Form +{ + public Form1() + { + InitializeComponent(); + var control = new FormulaControl(); + Controls.Add(control); + + control.Top = 5; + control.Left = 5; + control.Width = Width - 5; + control.Height = Height - 5; + control.FormulaText = @"\sqrt 2"; + } +} diff --git a/WinFormsApp.Example/Program.cs b/WinFormsApp.Example/Program.cs new file mode 100644 index 00000000..54c884a3 --- /dev/null +++ b/WinFormsApp.Example/Program.cs @@ -0,0 +1,16 @@ +namespace WinFormsApp.Example; + +static class Program +{ + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new Form1()); + } +} diff --git a/WinFormsApp.Example/WinFormsApp.Example.csproj b/WinFormsApp.Example/WinFormsApp.Example.csproj new file mode 100644 index 00000000..e3bb5b60 --- /dev/null +++ b/WinFormsApp.Example/WinFormsApp.Example.csproj @@ -0,0 +1,15 @@ + + + + WinExe + net7.0-windows + enable + true + enable + + + + + + + \ No newline at end of file diff --git a/XamlMath.All.sln b/XamlMath.All.sln index 8f2d7851..9d560490 100644 --- a/XamlMath.All.sln +++ b/XamlMath.All.sln @@ -82,6 +82,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "api", "api", "{3F2FED19-93A api\XamlMath.Shared.net6.0.verified.cs = api\XamlMath.Shared.net6.0.verified.cs EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinFormsMath", "src\WinFormsMath\WinFormsMath.csproj", "{4236794D-83A5-46B8-8F1C-8777D525DCD9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinFormsApp.Example", "WinFormsApp.Example\WinFormsApp.Example.csproj", "{7AD0440C-A9E8-4153-883C-B595E8B7346F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -124,6 +128,14 @@ Global {54F77EF7-F918-46F8-A615-56BC0BFE8AAB}.Debug|Any CPU.Build.0 = Debug|Any CPU {54F77EF7-F918-46F8-A615-56BC0BFE8AAB}.Release|Any CPU.ActiveCfg = Release|Any CPU {54F77EF7-F918-46F8-A615-56BC0BFE8AAB}.Release|Any CPU.Build.0 = Release|Any CPU + {4236794D-83A5-46B8-8F1C-8777D525DCD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4236794D-83A5-46B8-8F1C-8777D525DCD9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4236794D-83A5-46B8-8F1C-8777D525DCD9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4236794D-83A5-46B8-8F1C-8777D525DCD9}.Release|Any CPU.Build.0 = Release|Any CPU + {7AD0440C-A9E8-4153-883C-B595E8B7346F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AD0440C-A9E8-4153-883C-B595E8B7346F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AD0440C-A9E8-4153-883C-B595E8B7346F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AD0440C-A9E8-4153-883C-B595E8B7346F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/WinFormsMath/Controls/FormulaControl.cs b/src/WinFormsMath/Controls/FormulaControl.cs new file mode 100644 index 00000000..096be400 --- /dev/null +++ b/src/WinFormsMath/Controls/FormulaControl.cs @@ -0,0 +1,22 @@ +using WinFormsMath.Parsers; +using WinFormsMath.Rendering; +using XamlMath; +using XamlMath.Rendering; + +namespace WinFormsMath.Controls; + +public class FormulaControl : Control +{ + public string FormulaText { get; set; } // TODO: Naming? + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + + var texFormula = WinFormsFormulaParser.Instance.Parse(FormulaText); + var environment = WinFormsTeXEnvironment.Create(); + + var renderer = new WinFormsRenderer(e.Graphics); + texFormula.RenderTo(renderer, environment, 0.0, 0.0); + } +} diff --git a/src/WinFormsMath/Fonts/WinFormsGlyphTypeface.cs b/src/WinFormsMath/Fonts/WinFormsGlyphTypeface.cs new file mode 100644 index 00000000..a5f52b0c --- /dev/null +++ b/src/WinFormsMath/Fonts/WinFormsGlyphTypeface.cs @@ -0,0 +1,5 @@ +using XamlMath.Fonts; + +namespace WinFormsMath.Fonts; + +internal record WinFormsGlyphTypeface(Font Font) : IFontTypeface; diff --git a/src/WinFormsMath/Fonts/WinFormsMathFontProvider.cs b/src/WinFormsMath/Fonts/WinFormsMathFontProvider.cs new file mode 100644 index 00000000..4a17397a --- /dev/null +++ b/src/WinFormsMath/Fonts/WinFormsMathFontProvider.cs @@ -0,0 +1,30 @@ +using System.Drawing.Text; +using XamlMath.Fonts; +using XamlMath.Utils; + +namespace WinFormsMath.Fonts; + +/// A font provider implementation specifically for the WinFormsMath assembly. +internal class WinFormsMathFontProvider : IFontProvider +{ + private WinFormsMathFontProvider() {} + + public static WinFormsMathFontProvider Instance = new(); + + private const string FontsDirectory = "WinFormsMath.Fonts"; + + public unsafe IFontTypeface ReadFontFile(string fontFileName) + { + using var resource = typeof(WinFormsMathFontProvider).Assembly.ReadResource($"{FontsDirectory}.{fontFileName}"); + using var byteStream = new MemoryStream(); + resource.CopyTo(byteStream); + var bytes = byteStream.ToArray(); + + var c = new PrivateFontCollection(); // TODO: Dispose? + fixed (byte* p = bytes) + c.AddMemoryFont((IntPtr)p, bytes.Length); + + var ff = c.Families.Single(); + return new WinFormsGlyphTypeface(new Font(ff, 1.0f)); + } +} diff --git a/src/WinFormsMath/Fonts/WinFormsSystemFont.cs b/src/WinFormsMath/Fonts/WinFormsSystemFont.cs new file mode 100644 index 00000000..24d7f142 --- /dev/null +++ b/src/WinFormsMath/Fonts/WinFormsSystemFont.cs @@ -0,0 +1,114 @@ +using XamlMath; +using XamlMath.Exceptions; +using XamlMath.Fonts; +using XamlMath.Utils; + +namespace WinFormsMath.Fonts; + +internal class WinFormsSystemFont : ITeXFont +{ + private readonly Lazy _font; + + public WinFormsSystemFont(double size, FontFamily fontFamily) + { + Size = size; + _font = new Lazy(() => new Font(fontFamily, (float)Size)); + } + + public bool SupportsMetrics => false; + + public double Size { get; } + + public ExtensionChar GetExtension(CharInfo charInfo, TexStyle style) => + throw MethodNotSupported(nameof(GetExtension)); + + public CharFont? GetLigature(CharFont leftChar, CharFont rightChar) => null; + + public CharInfo GetNextLargerCharInfo(CharInfo charInfo, TexStyle style) => + throw MethodNotSupported(nameof(GetNextLargerCharInfo)); + + public Result GetDefaultCharInfo(char character, TexStyle style) => + Result.Error(MethodNotSupported(nameof(this.GetDefaultCharInfo))); + + public Result GetCharInfo(char character, string textStyle, TexStyle style) + { + var font = _font.Value; + var metrics = GetFontMetrics(character, font); + return Result.Ok( + new CharInfo(character, new WinFormsGlyphTypeface(font), 1.0, TexFontUtilities.NoFontId, metrics)); + } + + public Result GetCharInfo(CharFont charFont, TexStyle style) => + Result.Error(MethodNotSupported(nameof(this.GetCharInfo))); + + public Result GetCharInfo(string name, TexStyle style) => + Result.Error(MethodNotSupported(nameof(GetCharInfo))); + + public double GetKern(CharFont leftChar, CharFont rightChar, TexStyle style) => 0.0; + + public double GetQuad(int fontId, TexStyle style) => throw MethodNotSupported(nameof(GetQuad)); + + public double GetSkew(CharFont charFont, TexStyle style) => throw MethodNotSupported(nameof(GetSkew)); + + public bool HasSpace(int fontId) => throw MethodNotSupported(nameof(HasSpace)); + + public bool HasNextLarger(CharInfo charInfo) => throw MethodNotSupported(nameof(HasNextLarger)); + + public bool IsExtensionChar(CharInfo charInfo) => throw MethodNotSupported(nameof(IsExtensionChar)); + + public int GetMuFontId() => throw MethodNotSupported(nameof(GetMuFontId)); + + public double GetXHeight(TexStyle style, int fontId) => throw MethodNotSupported(nameof(GetXHeight)); + + public double GetSpace(TexStyle style) => throw MethodNotSupported(nameof(GetSpace)); + + public double GetAxisHeight(TexStyle style) => throw MethodNotSupported(nameof(GetAxisHeight)); + + public double GetBigOpSpacing1(TexStyle style) => throw MethodNotSupported(nameof(GetBigOpSpacing1)); + + public double GetBigOpSpacing2(TexStyle style) => throw MethodNotSupported(nameof(GetBigOpSpacing2)); + + public double GetBigOpSpacing3(TexStyle style) => throw MethodNotSupported(nameof(GetBigOpSpacing3)); + + public double GetBigOpSpacing4(TexStyle style) => throw MethodNotSupported(nameof(GetBigOpSpacing4)); + + public double GetBigOpSpacing5(TexStyle style) => throw MethodNotSupported(nameof(GetBigOpSpacing5)); + + public double GetSub1(TexStyle style) => throw MethodNotSupported(nameof(GetSub1)); + + public double GetSub2(TexStyle style) => throw MethodNotSupported(nameof(GetSub2)); + + public double GetSubDrop(TexStyle style) => throw MethodNotSupported(nameof(GetSubDrop)); + + public double GetSup1(TexStyle style) => throw MethodNotSupported(nameof(GetSup1)); + + public double GetSup2(TexStyle style) => throw MethodNotSupported(nameof(GetSup2)); + + public double GetSup3(TexStyle style) => throw MethodNotSupported(nameof(GetSup3)); + + public double GetSupDrop(TexStyle style) => throw MethodNotSupported(nameof(GetSupDrop)); + + public double GetNum1(TexStyle style) => throw MethodNotSupported(nameof(GetNum1)); + + public double GetNum2(TexStyle style) => throw MethodNotSupported(nameof(GetNum2)); + + public double GetNum3(TexStyle style) => throw MethodNotSupported(nameof(GetNum3)); + + public double GetDenom1(TexStyle style) => throw MethodNotSupported(nameof(GetDenom1)); + + public double GetDenom2(TexStyle style) => throw MethodNotSupported(nameof(GetDenom2)); + + public double GetDefaultLineThickness(TexStyle style) => throw MethodNotSupported(nameof(GetDefaultLineThickness)); + + private static TexNotSupportedException MethodNotSupported(string callerMethod) + { + return new TexNotSupportedException( + $"Call of method {callerMethod} on {nameof(WinFormsSystemFont)} is not supported"); + } + + private static TeXFontMetrics GetFontMetrics(char c, Font font) + { + var size = TextRenderer.MeasureText(c.ToString(), font); + return new TeXFontMetrics(size.Width, size.Height, 0.0, size.Width, 1.0); + } +} diff --git a/src/WinFormsMath/Parsers/WinFormsFormulaParser.cs b/src/WinFormsMath/Parsers/WinFormsFormulaParser.cs new file mode 100644 index 00000000..2f999938 --- /dev/null +++ b/src/WinFormsMath/Parsers/WinFormsFormulaParser.cs @@ -0,0 +1,22 @@ +using WinFormsMath.Rendering; +using XamlMath; + +namespace WinFormsMath.Parsers; + +public static class WinFormsFormulaParser +{ + public static TexFormulaParser Instance { get; } + static WinFormsFormulaParser() + { + var predefinedFormulae = LoadPredefinedFormulae(); + Instance = new TexFormulaParser(WinFormsBrushFactory.Instance, predefinedFormulae); + } + + private static IReadOnlyDictionary> LoadPredefinedFormulae() + { + var predefinedFormulaParser = new TexPredefinedFormulaParser(WinFormsBrushFactory.Instance); + var predefinedFormulae = new Dictionary>(); + predefinedFormulaParser.Parse(predefinedFormulae); + return predefinedFormulae; + } +} diff --git a/src/WinFormsMath/Rendering/WinFormsBrush.cs b/src/WinFormsMath/Rendering/WinFormsBrush.cs new file mode 100644 index 00000000..d557add6 --- /dev/null +++ b/src/WinFormsMath/Rendering/WinFormsBrush.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using XamlMath.Colors; +using XamlMath.Rendering; + +namespace WinFormsMath.Rendering; + +public record WinFormsBrush : GenericBrush +{ + private WinFormsBrush(Brush brush) : base(brush) + { + } + + public static WinFormsBrush FromBrush(Brush value) => new(value); +} + +public class WinFormsBrushFactory : IBrushFactory +{ + public static readonly WinFormsBrushFactory Instance = new(); + + private WinFormsBrushFactory() {} + + public IBrush FromColor(RgbaColor color) => + new SolidBrush( + Color.FromArgb(color.A, color.R, color.G, color.B)).ToPlatform(); +} + +public static class WinFormsBrushExtensions +{ + public static Brush? ToWinForms(this IBrush? brush) => ((WinFormsBrush?)brush)?.Value; + [return: NotNullIfNotNull(nameof(brush))] + public static IBrush? ToPlatform(this Brush? brush) => brush == null ? null : WinFormsBrush.FromBrush(brush); +} diff --git a/src/WinFormsMath/Rendering/WinFormsRectangleExtensions.cs b/src/WinFormsMath/Rendering/WinFormsRectangleExtensions.cs new file mode 100644 index 00000000..1f55e6b8 --- /dev/null +++ b/src/WinFormsMath/Rendering/WinFormsRectangleExtensions.cs @@ -0,0 +1,7 @@ +namespace WinFormsMath.Rendering; + +internal static class WinFormsRectangleExtensions +{ + public static RectangleF ToWinForms(this XamlMath.Rendering.Rectangle rectangle) => + new((float)rectangle.X, (float)rectangle.Y, (float)rectangle.Width, (float)rectangle.Height); +} diff --git a/src/WinFormsMath/Rendering/WinFormsRenderer.cs b/src/WinFormsMath/Rendering/WinFormsRenderer.cs new file mode 100644 index 00000000..305758ae --- /dev/null +++ b/src/WinFormsMath/Rendering/WinFormsRenderer.cs @@ -0,0 +1,84 @@ +using System.Runtime.InteropServices; +using WinFormsMath.Fonts; +using XamlMath; +using XamlMath.Boxes; +using XamlMath.Rendering; +using XamlMath.Rendering.Transformations; +using Rectangle = XamlMath.Rendering.Rectangle; + +namespace WinFormsMath.Rendering; + +public class WinFormsRenderer : IElementRenderer +{ + private readonly Graphics _graphics; + private float Scale => 20f * _graphics.DpiX / 96f; // TODO: Figure out what to do with sizes properly + + public WinFormsRenderer(Graphics graphics) + { + _graphics = graphics; + } + + public void RenderElement(Box box, double x, double y) + { + // TODO: RenderBackground(box, x, y); + box.RenderTo(this, x, y); + } + + [DllImport("gdi32.dll")] + public static extern int GetTextCharacterExtra( + IntPtr hdc // DC handle + ); + + public void RenderCharacter(CharInfo info, double x, double y, IBrush? foreground) + { + var font = ((WinFormsGlyphTypeface)info.Font).Font; + var newF = new Font(font.FontFamily, (float)info.Size * Scale, GraphicsUnit.Pixel); + var brush = foreground.ToWinForms() ?? Brushes.Black; // TODO: Make IBrush disposable? + + int mm; + var hdc = _graphics.GetHdc(); + try + { + mm = GetTextCharacterExtra(hdc); + } + finally + { + _graphics.ReleaseHdc(hdc); + } + + var metric = _graphics.MeasureString(info.Character.ToString(), newF); + + // Renderer wants upper left corner from us, while we have baseline here. Let's convert. + var ff = newF.FontFamily; + var lineSpace = ff.GetLineSpacing(font.Style); + var ascent = ff.GetCellAscent(font.Style); + var fontBaseline = newF.GetHeight(_graphics.DpiX) * ascent / lineSpace; + + var baselineX = x * Scale; // TODO: It looks like we should subtract several pixels here for some reason, + // I don't understand why. Probably a font metric issue. It's possible + // that we won't be able to get the right metrics using WinForms without + // falling back to DirectX? + var baselineY = y * Scale; + var topY = baselineY - fontBaseline; + + + _graphics.DrawString(info.Character.ToString(), newF, brush, (float)baselineX, (float)topY); + } + + public void RenderRectangle(Rectangle rectangle, IBrush? foreground) + { + var brush = foreground.ToWinForms() ?? Brushes.Black; // TODO: Make IBrush disposable? + _graphics.FillRectangle(brush, GeometryHelper.ScaleRectangle(Scale, rectangle).ToWinForms()); + } + + public void RenderTransformed(Box box, IEnumerable transforms, double x, double y) + { + // TODO: Implement this correctly. For now, Avalonia ignores this, so I assume it's safe for us to ignore it as + // well. + RenderElement(box, x, y); + } + + public void FinishRendering() + { + } +} diff --git a/src/WinFormsMath/Rendering/WinFormsTeXEnvironment.cs b/src/WinFormsMath/Rendering/WinFormsTeXEnvironment.cs new file mode 100644 index 00000000..4747dc4c --- /dev/null +++ b/src/WinFormsMath/Rendering/WinFormsTeXEnvironment.cs @@ -0,0 +1,37 @@ +using WinFormsMath.Fonts; +using XamlMath; + +namespace WinFormsMath.Rendering; + +public static class WinFormsTeXEnvironment +{ + /// Creates an instance of for a Windows Forms program. + /// Initial style for the formula content. + /// Formula font size. + /// Name of the system font to use for the \text blocks. + /// Foreground color. Black if not specified. + /// Background color. + public static TexEnvironment Create( + TexStyle style = TexStyle.Display, + double scale = 20.0, + string systemTextFontName = "Arial", + Brush? foreground = null, + Brush? background = null) + { + var mathFont = new DefaultTexFont(WinFormsMathFontProvider.Instance, scale); + var textFont = GetSystemFont(systemTextFontName, scale); + + return new TexEnvironment( + style, + mathFont, + textFont, + background.ToPlatform(), + foreground.ToPlatform()); + } + + private static WinFormsSystemFont GetSystemFont(string fontName, double size) + { + var fontFamily = FontFamily.Families.First(ff => ff.Name == fontName); + return new WinFormsSystemFont(size, fontFamily); + } +} diff --git a/src/WinFormsMath/WinFormsMath.csproj b/src/WinFormsMath/WinFormsMath.csproj new file mode 100644 index 00000000..c6283c96 --- /dev/null +++ b/src/WinFormsMath/WinFormsMath.csproj @@ -0,0 +1,19 @@ + + + + net7.0-windows + enable + true + enable + true + + + + + + + + + + + diff --git a/src/WpfMath.Example/MainWindow.xaml.cs b/src/WpfMath.Example/MainWindow.xaml.cs index 8f40a242..ca1ced79 100644 --- a/src/WpfMath.Example/MainWindow.xaml.cs +++ b/src/WpfMath.Example/MainWindow.xaml.cs @@ -22,6 +22,8 @@ private static ComboBoxItem DemoFormula(string name, string text) => private readonly IList _testFormulas = new[] { + DemoFormula("Simple", @"\sqrt 2"), + DemoFormula("Integral 1", @"\int_0^{\infty}{x^{2n} e^{-a x^2} \, dx} = \frac{2n-1}{2a} \int_0^{\infty}{x^{2(n-1)} e^{-a x^2} \, dx} = \frac{(2n-1)!!}{2^{n+1}} \sqrt{\frac{\pi}{a^{2n+1}}}"), DemoFormula("Integral 1", @"\int_0^{\infty}{x^{2n} e^{-a x^2} \, dx} = \frac{2n-1}{2a} \int_0^{\infty}{x^{2(n-1)} e^{-a x^2} \, dx} = \frac{(2n-1)!!}{2^{n+1}} \sqrt{\frac{\pi}{a^{2n+1}}}"), DemoFormula("Integral 2", @"\int_a^b{f(x) \, dx} = (b - a) \sum_{n = 1}^{\infty} {\sum_{m = 1}^{2^n - 1} { ( { - 1} )^{m + 1} } } 2^{ - n} f(a + m ( {b - a} )2^{-n} )"), DemoFormula("Integral 3", @"L = \int_a^\infty \sqrt[4]{ \left\vert \sum_{i,j=1}^ng_{ij}\left\(\gamma(t)\right\) \left\[\frac{d}{dt}x^i\circ\gamma(t) \right\] \left\{\frac{d}{dt}x^j\circ\gamma(t) \right\} \right\|} \, dt"), diff --git a/src/XamlMath.Shared/XamlMath.Shared.csproj b/src/XamlMath.Shared/XamlMath.Shared.csproj index 8b8ec76b..4873ee0e 100644 --- a/src/XamlMath.Shared/XamlMath.Shared.csproj +++ b/src/XamlMath.Shared/XamlMath.Shared.csproj @@ -24,8 +24,9 @@ - + +