diff --git a/CSharpMath.Avalonia/CSharpMath.Avalonia.csproj b/CSharpMath.Avalonia/CSharpMath.Avalonia.csproj
index 79b5c4e5..9046f8bc 100644
--- a/CSharpMath.Avalonia/CSharpMath.Avalonia.csproj
+++ b/CSharpMath.Avalonia/CSharpMath.Avalonia.csproj
@@ -6,6 +6,7 @@
$(PackageTags) avalonia
$(DefineConstants) Avalonia
+
diff --git a/CSharpMath.Maui/CSharpMath.Maui.csproj b/CSharpMath.Maui/CSharpMath.Maui.csproj
index bed8856a..207cda7c 100644
--- a/CSharpMath.Maui/CSharpMath.Maui.csproj
+++ b/CSharpMath.Maui/CSharpMath.Maui.csproj
@@ -1,6 +1,6 @@
-
+
net10.0;net10.0-android
$(TargetFrameworks);net10.0-ios;net10.0-maccatalyst
$(TargetFrameworks);net10.0-windows10.0.19041.0
@@ -18,7 +18,8 @@
$(PackageTags) maui
$(DefineConstants) Maui
-
+
+
diff --git a/CSharpMath.Rendering.Benchmarks/CSharpMath.Rendering.Benchmarks.csproj b/CSharpMath.Rendering.Benchmarks/CSharpMath.Rendering.Benchmarks.csproj
index dfa64d71..7a1561d0 100644
--- a/CSharpMath.Rendering.Benchmarks/CSharpMath.Rendering.Benchmarks.csproj
+++ b/CSharpMath.Rendering.Benchmarks/CSharpMath.Rendering.Benchmarks.csproj
@@ -9,6 +9,9 @@
+
+
+
-
\ No newline at end of file
+
diff --git a/CSharpMath.Rendering.Benchmarks/Program.cs b/CSharpMath.Rendering.Benchmarks/Program.cs
index cbd0371b..d6f39b29 100644
--- a/CSharpMath.Rendering.Benchmarks/Program.cs
+++ b/CSharpMath.Rendering.Benchmarks/Program.cs
@@ -1,4 +1,8 @@
+using Avalonia;
+using Avalonia.Headless;
using BenchmarkDotNet.Attributes;
+using CSharpMath.Atom;
+using CSharpMath.SkiaSharp;
namespace CSharpMath.Rendering.Benchmarks {
using SkiaSharp;
using Data = Tests.TestRenderingMathData;
@@ -41,4 +45,73 @@ static void Main(string[] args) {
#endif
}
}
-}
\ No newline at end of file
+
+ [MemoryDiagnoser]
+ public class ViewUpdateChurnBenchmarks {
+ const string Sample = @"x_1 = 1.234e50";
+ [Params(100, 1000)] public int Iterations;
+ readonly MathPainter painter = new();
+ readonly MathList content = new MathPainter { LaTeX = Sample }.Content!;
+
+ [Benchmark]
+ public void LaTeXAssignment() {
+ for (var i = 0; i < Iterations; i++) {
+ painter.LaTeX = Sample;
+ painter.Measure(1000);
+ }
+ }
+
+ [Benchmark]
+ public void ContentAssignment() {
+ for (var i = 0; i < Iterations; i++) {
+ painter.Content = content;
+ painter.Measure(1000);
+ }
+ }
+
+ [Benchmark]
+ public void DirectPainterReuse() {
+ painter.LaTeX = Sample;
+ for (var i = 0; i < Iterations; i++) painter.Measure(1000);
+ }
+ }
+
+ [MemoryDiagnoser]
+ public class AvaloniaViewUpdateChurnBenchmarks {
+ const string FirstSample = @"x_1 = 1.234e50";
+ const string SecondSample = @"x_2 = 5.678e90";
+ [Params(100, 1000)] public int Iterations;
+ static AvaloniaViewUpdateChurnBenchmarks() {
+ _ = CSharpMath.Rendering.BackEnd.Fonts.GlobalTypefaces.ToString();
+ AppBuilder.Configure()
+ .UseHeadless(new AvaloniaHeadlessPlatformOptions())
+ .SetupWithoutStarting();
+ }
+ sealed class BenchmarkApplication : Application { }
+ readonly CSharpMath.Avalonia.MathView view = new();
+ readonly MathList firstContent = new MathPainter { LaTeX = FirstSample }.Content!;
+ readonly MathList secondContent = new MathPainter { LaTeX = SecondSample }.Content!;
+
+ [Benchmark]
+ public void ViewLaTeXAssignment() {
+ for (var i = 0; i < Iterations; i++) {
+ view.LaTeX = i % 2 == 0 ? FirstSample : SecondSample;
+ view.Measure(new Size(1000, 1000));
+ }
+ }
+
+ [Benchmark]
+ public void ViewContentAssignment() {
+ for (var i = 0; i < Iterations; i++) {
+ view.Content = i % 2 == 0 ? firstContent : secondContent;
+ view.Measure(new Size(1000, 1000));
+ }
+ }
+
+ [Benchmark]
+ public void DirectPainterReuse() {
+ view.Painter.LaTeX = FirstSample;
+ for (var i = 0; i < Iterations; i++) view.Painter.Measure(1000);
+ }
+ }
+}
diff --git a/CSharpMath.Rendering/FrontEnd/Painter.cs b/CSharpMath.Rendering/FrontEnd/Painter.cs
index 02acaa6f..4518c503 100644
--- a/CSharpMath.Rendering/FrontEnd/Painter.cs
+++ b/CSharpMath.Rendering/FrontEnd/Painter.cs
@@ -50,7 +50,7 @@ public Painter() {
Atom.LineStyle __style = Atom.LineStyle.Display;
public Atom.LineStyle LineStyle { get => __style; set { __style = value; SetRedisplay(); } }
TContent? __content;
- public TContent? Content { get => __content; set { __content = value; SetRedisplay(); } }
+ public TContent? Content { get => __content; set { __content = value; ErrorMessage = null; SetRedisplay(); } }
public string? LaTeX { get => Content is null ? "" : ContentToLaTeX(Content); set => (Content, ErrorMessage) = LaTeXToContent(value ?? ""); }
#endregion Redisplaying properties
@@ -132,4 +132,4 @@ GlyphBoxColor is var (glyph, textRun) ? Nullable((WrapColor(glyph), WrapColor(te
public Painter ShallowClone() => (Painter)MemberwiseClone();
#endregion Methods
}
-}
\ No newline at end of file
+}
diff --git a/CSharpMath.Uno/CSharpMath.Uno.csproj b/CSharpMath.Uno/CSharpMath.Uno.csproj
index 50d7416f..8f77c4ae 100644
--- a/CSharpMath.Uno/CSharpMath.Uno.csproj
+++ b/CSharpMath.Uno/CSharpMath.Uno.csproj
@@ -10,6 +10,7 @@
$(DefineConstants) Uno
+
diff --git a/CSharpMath.Xaml.Tests/CSharpMath.Xaml.Tests.csproj b/CSharpMath.Xaml.Tests/CSharpMath.Xaml.Tests.csproj
index 6d43aaf7..c2d39500 100644
--- a/CSharpMath.Xaml.Tests/CSharpMath.Xaml.Tests.csproj
+++ b/CSharpMath.Xaml.Tests/CSharpMath.Xaml.Tests.csproj
@@ -4,5 +4,6 @@
+
-
\ No newline at end of file
+
diff --git a/CSharpMath.Xaml.Tests/Test.cs b/CSharpMath.Xaml.Tests/Test.cs
index e0689fdf..3cf83149 100644
--- a/CSharpMath.Xaml.Tests/Test.cs
+++ b/CSharpMath.Xaml.Tests/Test.cs
@@ -11,6 +11,10 @@ namespace CSharpMath.Xaml.Tests {
using Rendering.BackEnd;
using Rendering.FrontEnd;
using Rendering.Text;
+ interface ITestDiagnostics {
+ void Reset();
+ int Count(string name);
+ }
public abstract class Test
where TBindingMode : struct, Enum
where TMathView : TBaseView, ICSharpMathAPI, new()
@@ -32,8 +36,18 @@ void SetAndRaise(ref T field, T value, [System.Runtime.CompilerServices.Calle
protected abstract TBindingMode OneWayToSource { get; }
protected abstract TBindingMode TwoWay { get; }
protected abstract TView ParseFromXaml(string xaml) where TView : TBaseView, new();
+ protected abstract TBaseView CreateInstrumentedMathView();
protected abstract void SetBindingContext(TBaseView view, object viewModel);
protected abstract IDisposable SetBinding(TBaseView view, TProperty property, string viewModelProperty, TBindingMode bindingMode);
+ static void AssertUpdateCounts(TBaseView view, bool contentSource, Action update) {
+ var diagnostics = Assert.IsAssignableFrom(view);
+ diagnostics.Reset();
+ update();
+ Assert.Equal(contentSource ? 1 : 0, diagnostics.Count("ContentToLaTeX"));
+ Assert.Equal(contentSource ? 0 : 1, diagnostics.Count("LaTeXToContent"));
+ Assert.Equal(1, diagnostics.Count("MeasureInvalidation"));
+ Assert.Equal(1, diagnostics.Count("VisualInvalidation"));
+ }
IDisposable SetBinding(TView view, string propertyName, TBindingMode? bindingMode = null) where TView : TBaseView =>
SetBinding(view,
(TProperty)typeof(TView)
@@ -52,6 +66,9 @@ static void Test(TView view, TContent content)
}
Test(new TMathView(), new MathList(new Atom.Atoms.Number("1")));
Test(new TTextView(), (TextAtom)new TextAtom.Text("1"));
+ var countedView = CreateInstrumentedMathView();
+ var counted = Assert.IsAssignableFrom>(countedView);
+ AssertUpdateCounts(countedView, false, () => counted.LaTeX = "1");
}
[Fact]
public void ContentUpdatesLaTeX() {
@@ -64,6 +81,33 @@ static void Test(TView view, TContent content)
}
Test(new TMathView(), new MathList(new Atom.Atoms.Number("1")));
Test(new TTextView(), (TextAtom)new TextAtom.Text("1"));
+ var countedView = CreateInstrumentedMathView();
+ var counted = Assert.IsAssignableFrom>(countedView);
+ AssertUpdateCounts(countedView, true, () => counted.Content = new MathList(new Atom.Atoms.Number("1")));
+ }
+ [Fact]
+ public void RepeatedContentUpdatesRemainNormalized() {
+ var view = new TMathView();
+ for (var i = 0; i < 8; i++) {
+ view.Content = new MathList(new Atom.Atoms.Number((i + 1).ToString()));
+ Assert.Equal((i + 1).ToString(), view.LaTeX);
+ Assert.Null(view.ErrorMessage);
+ }
+ var text = new TTextView();
+ for (var i = 0; i < 8; i++) {
+ text.LaTeX = "a b";
+ Assert.NotNull(text.Content);
+ Assert.Null(text.ErrorMessage);
+ }
+ }
+ [Fact]
+ public void ValidContentClearsPreviousError() {
+ var view = new TMathView();
+ view.LaTeX = @"\missingcommand";
+ Assert.NotNull(view.ErrorMessage);
+ view.Content = new MathList(new Atom.Atoms.Number("1"));
+ Assert.Null(view.ErrorMessage);
+ Assert.Equal("1", view.LaTeX);
}
[Fact]
public void ContentIsBindable() {
@@ -259,4 +303,4 @@ public void LaTeXIsContent() {
Assert.Equal(new TextAtom.List(new TextAtom[] { new TextAtom.Text("a"), new TextAtom.ControlSpace(), new TextAtom.Text("b") }), text.Content);
}
}
-}
\ No newline at end of file
+}
diff --git a/CSharpMath.Xaml.Tests/TestAvalonia.cs b/CSharpMath.Xaml.Tests/TestAvalonia.cs
index ed23d512..60419c22 100644
--- a/CSharpMath.Xaml.Tests/TestAvalonia.cs
+++ b/CSharpMath.Xaml.Tests/TestAvalonia.cs
@@ -9,6 +9,12 @@
namespace CSharpMath.Xaml.Tests {
using Avalonia;
+ public class InstrumentedMathView : MathView, ITestDiagnostics {
+ readonly Dictionary counts = new();
+ internal override void OnDiagnostic(string name) => counts[name] = counts.GetValueOrDefault(name) + 1;
+ public void Reset() => counts.Clear();
+ public int Count(string name) => counts.GetValueOrDefault(name);
+ }
public class TestAvalonia
: Test {
protected override Display.IDisplay GetDisplay(Control control) {
@@ -27,10 +33,11 @@ public class TestAvalonia
protected override BindingMode Default => BindingMode.Default;
protected override BindingMode OneWayToSource => BindingMode.OneWayToSource;
protected override BindingMode TwoWay => BindingMode.TwoWay;
+ protected override Control CreateInstrumentedMathView() => new InstrumentedMathView();
protected override TView ParseFromXaml(string xaml) => AvaloniaRuntimeXamlLoader.Parse(xaml);
protected override IDisposable SetBinding(Control view, AvaloniaProperty property, string viewModelProperty, BindingMode bindingMode) =>
view.Bind(property, new Binding(viewModelProperty) { Mode = bindingMode });
protected override void SetBindingContext(Control view, object viewModel) =>
view.DataContext = viewModel;
}
-}
\ No newline at end of file
+}
diff --git a/CSharpMath.Xaml.Tests/TestMaui.cs b/CSharpMath.Xaml.Tests/TestMaui.cs
index addb21a4..f14667d1 100644
--- a/CSharpMath.Xaml.Tests/TestMaui.cs
+++ b/CSharpMath.Xaml.Tests/TestMaui.cs
@@ -9,6 +9,13 @@ namespace CSharpMath.Xaml.Tests {
using Maui;
using Microsoft.Maui.Controls.Xaml;
+ public class InstrumentedMauiMathView : MathView, ITestDiagnostics {
+ readonly Dictionary counts = new();
+ internal override void OnDiagnostic(string name) => counts[name] = counts.GetValueOrDefault(name) + 1;
+ public void Reset() => counts.Clear();
+ public int Count(string name) => counts.GetValueOrDefault(name);
+ }
+
public class TestMaui
: Test {
@@ -57,6 +64,7 @@ public bool DispatchDelayed(TimeSpan delay, Action action) {
protected override BindingMode Default => BindingMode.Default;
protected override BindingMode OneWayToSource => BindingMode.OneWayToSource;
protected override BindingMode TwoWay => BindingMode.TwoWay;
+ protected override GraphicsView CreateInstrumentedMathView() => new InstrumentedMauiMathView();
protected override TView ParseFromXaml(string xaml) {
var view = new TView();
view.LoadFromXaml(xaml);
@@ -74,4 +82,4 @@ protected override IDisposable SetBinding(GraphicsView view, BindableProperty pr
protected override void SetBindingContext(GraphicsView view, object viewModel) =>
view.BindingContext = viewModel;
}
-}
\ No newline at end of file
+}
diff --git a/CSharpMath.Xaml.Tests/TestUno.cs b/CSharpMath.Xaml.Tests/TestUno.cs
new file mode 100644
index 00000000..e9bfbe9b
--- /dev/null
+++ b/CSharpMath.Xaml.Tests/TestUno.cs
@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+using Microsoft.UI.Xaml.Data;
+using Xunit;
+
+namespace CSharpMath.Xaml.Tests {
+ public class TestUno {
+ [Fact]
+ public void BindingExpressionMarkerPreventsCounterpartAssignment() {
+ var binding = RuntimeHelpers.GetUninitializedObject(typeof(BindingExpression));
+ Assert.False(Uno.UnoPropertySynchronization.ShouldAssignCounterpart(binding));
+ Assert.True(Uno.UnoPropertySynchronization.ShouldAssignCounterpart(null));
+ Assert.True(Uno.UnoPropertySynchronization.ShouldAssignCounterpart(new object()));
+ }
+ }
+}
diff --git a/CSharpMath.Xaml/Views.cs b/CSharpMath.Xaml/Views.cs
index 09bdd917..e8a59a94 100644
--- a/CSharpMath.Xaml/Views.cs
+++ b/CSharpMath.Xaml/Views.cs
@@ -40,6 +40,12 @@ namespace CSharpMath.Maui {
using MathPainter = CSharpMath.SkiaSharp.MathPainter;
using TextPainter = CSharpMath.SkiaSharp.TextPainter;
namespace CSharpMath.Uno {
+#endif
+#if Uno
+ internal static class UnoPropertySynchronization {
+ internal static bool ShouldAssignCounterpart(object? localValue) =>
+ localValue is not Microsoft.UI.Xaml.Data.BindingExpression;
+ }
[Microsoft.UI.Xaml.Markup.ContentProperty(Name = nameof(LaTeX))]
#endif
public partial class BaseView<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TPainter,
@@ -47,6 +53,9 @@ public partial class BaseView<[DynamicallyAccessedMembers(DynamicallyAccessedMem
TContent> : XInheritControl, ICSharpMathAPI
where TPainter : Painter, new() where TContent : class {
public TPainter Painter { get; } = new TPainter();
+ bool _synchronizingProperties;
+ // Internal test seam; production views do not carry diagnostic state.
+ internal virtual void OnDiagnostic(string _) { }
protected static readonly TPainter staticPainter = new TPainter();
@@ -65,8 +74,8 @@ static BaseView() {
GlyphBoxColorProperty = CreateProperty, (XColor glyph, XColor textRun)?>(nameof(GlyphBoxColor), false,
p => p.GlyphBoxColor is var (glyph, textRun) ? Nullable((XCanvasColorToXColor(glyph), XCanvasColorToXColor(textRun))) : null,
(p, v) => p.GlyphBoxColor = v is var (glyph, textRun) ? Nullable((XColorToXCanvasColor(glyph), XColorToXCanvasColor(textRun))) : null),
- ContentProperty = CreateProperty, TContent?>(nameof(Content), true, p => p.Content, (p, v) => p.Content = v, (b, v) => { if (b.Painter.ErrorMessage == null) b.LaTeX = b.Painter.LaTeX; }),
- LaTeXProperty = CreateProperty, string?>(nameof(LaTeX), true, p => p.LaTeX, (p, v) => p.LaTeX = v, (b, v) => (b.Content, b.ErrorMessage) = (b.Painter.Content, b.Painter.ErrorMessage)),
+ ContentProperty = CreateProperty, TContent?>(nameof(Content), true, p => p.Content, (p, v) => p.Content = v, SynchronizeFromContent),
+ LaTeXProperty = CreateProperty, string?>(nameof(LaTeX), true, p => p.LaTeX, (p, v) => p.LaTeX = v, SynchronizeFromLaTeX),
DisplayErrorInlineProperty = CreateProperty, bool>(nameof(DisplayErrorInline), true, p => p.DisplayErrorInline, (p, v) => p.DisplayErrorInline = v),
FontSizeProperty = CreateProperty, float>(nameof(FontSize), true, p => p.FontSize, (p, v) => p.FontSize = v),
ErrorFontSizeProperty = CreateProperty, float?>(nameof(ErrorFontSize), true, p => p.ErrorFontSize, (p, v) => p.ErrorFontSize = v),
@@ -97,13 +106,20 @@ static XProperty CreateProperty<
where TThis : BaseView {
var defaultValue = defaultValueGet(staticPainter);
void PropertyChanged(TThis @this, object? newValue) {
+ // Counterpart updates only mirror the already-updated painter state;
+ // they must not parse/serialize it a second time or invalidate twice.
+ if (@this._synchronizingProperties) return;
// We need to support nullable classes and structs, so we cannot forbid null here
// So this use of the null-forgiving operator should be blamed on non-generic PropertyChanged handlers
var @new = (TValue)newValue!;
+ @this.OnDiagnostic(propertyName == nameof(Content) ? "ContentToLaTeX" : propertyName == nameof(LaTeX) ? "LaTeXToContent" : propertyName);
propertySet(@this.Painter, @new);
- if (affectsMeasure) @this.InvalidateMeasure();
+ if (affectsMeasure) @this.InvalidateMeasureTracked();
#if Avalonia
- updateOtherProperty?.Invoke(@this, @new); // Re-assign LaTeX from Content or vice versa
+ @this._synchronizingProperties = true;
+ try { updateOtherProperty?.Invoke(@this, @new); } // Re-assign LaTeX from Content or vice versa
+ finally { @this._synchronizingProperties = false; }
+ @this.OnDiagnostic("VisualInvalidation");
@this.InvalidateVisual();
}
var prop = XProperty.Register(propertyName, defaultValue);
@@ -112,6 +128,19 @@ void PropertyChanged(TThis @this, object? newValue) {
}
static string GetPropertyName(XProperty prop) => prop.Name;
}
+ static void SynchronizeFromContent(BaseView view, TContent? _) {
+ if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX);
+ view.ErrorMessage = view.Painter.ErrorMessage;
+ }
+ static void SynchronizeFromLaTeX(BaseView view, string? _) {
+ if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX);
+ view.SetSynchronizedValue(ContentProperty, view.Painter.Content);
+ view.ErrorMessage = view.Painter.ErrorMessage;
+ }
+ void InvalidateMeasureTracked() { OnDiagnostic("MeasureInvalidation"); InvalidateMeasure(); }
+ void SetSynchronizedValue(XProperty property, object? value) {
+ SetValue(property, value);
+ }
public BaseView() {
// Respect built-in styles
Styles.Add(new global::Avalonia.Styling.Style(global::Avalonia.Styling.Selectors.Is>) {
@@ -163,7 +192,10 @@ public override void Render(global::Avalonia.Media.DrawingContext context) {
base.Render(context);
var canvas = new XCanvas(context, Bounds.Size);
#elif Maui
- updateOtherProperty?.Invoke(@this, @new); // Re-assign LaTeX from Content or vice versa
+ @this._synchronizingProperties = true;
+ try { updateOtherProperty?.Invoke(@this, @new); } // Re-assign LaTeX from Content or vice versa
+ finally { @this._synchronizingProperties = false; }
+ @this.OnDiagnostic("VisualInvalidation");
@this.Invalidate();
}
return XProperty.Create(propertyName, typeof(TValue), typeof(TThis), defaultValue,
@@ -171,6 +203,17 @@ public override void Render(global::Avalonia.Media.DrawingContext context) {
}
static string GetPropertyName(XProperty prop) => prop.PropertyName;
}
+ static void SynchronizeFromContent(BaseView view, TContent? _) {
+ if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX);
+ view.ErrorMessage = view.Painter.ErrorMessage;
+ }
+ static void SynchronizeFromLaTeX(BaseView view, string? _) {
+ if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX);
+ view.SetSynchronizedValue(ContentProperty, view.Painter.Content);
+ view.ErrorMessage = view.Painter.ErrorMessage;
+ }
+ void InvalidateMeasureTracked() { OnDiagnostic("MeasureInvalidation"); InvalidateMeasure(); }
+ void SetSynchronizedValue(XProperty property, object? value) => SetValue(property, value);
protected override Microsoft.Maui.Graphics.Size MeasureOverride(double widthConstraint, double heightConstraint) =>
Painter.Measure((float)widthConstraint) is { } rect
? new(Math.Min(rect.Width, widthConstraint), Math.Min(rect.Height, heightConstraint)) // We can't allocate too big of a GraphicsView on MAUI Windows.
@@ -208,7 +251,13 @@ void Draw(XCanvas_Canvas rawCanvas) {
// dirtyRect may be larger than Width and Height on Windows which leads to incorrect alignments
var canvas = (rawCanvas, new Microsoft.Maui.Graphics.SizeF((float)Width, (float)Height));
#elif Uno
- // Uno/WinUI bindings are gone when a property is assigned unlike Avalonia and MAUI, so we omit updateOtherProperty.
+ // Keep the same guarded counterpart update on Uno/WinUI. The guard
+ // prevents a second painter conversion; SetValue is the dependency
+ // property value update used by Uno for this control's properties.
+ @this._synchronizingProperties = true;
+ try { updateOtherProperty?.Invoke(@this, @new); }
+ finally { @this._synchronizingProperties = false; }
+ @this.OnDiagnostic("VisualInvalidation");
@this.Invalidate();
}
return XProperty.Register(propertyName, typeof(TValue), typeof(TThis),
@@ -216,6 +265,20 @@ void Draw(XCanvas_Canvas rawCanvas) {
}
static string GetPropertyName(XProperty prop) => ""; // unsupported :(
}
+ static void SynchronizeFromContent(BaseView view, TContent? _) {
+ if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX);
+ view.ErrorMessage = view.Painter.ErrorMessage;
+ }
+ static void SynchronizeFromLaTeX(BaseView view, string? _) {
+ if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX);
+ view.SetSynchronizedValue(ContentProperty, view.Painter.Content);
+ view.ErrorMessage = view.Painter.ErrorMessage;
+ }
+ void InvalidateMeasureTracked() { OnDiagnostic("MeasureInvalidation"); InvalidateMeasure(); }
+ void SetSynchronizedValue(XProperty property, object? value) {
+ if (!UnoPropertySynchronization.ShouldAssignCounterpart(ReadLocalValue(property))) return;
+ SetValue(property, value);
+ }
public BaseView() {
PointerPressed += (sender, e) => {
if (e.GetCurrentPoint(this) is { Properties.IsLeftButtonPressed: true } point && EnablePanning)
@@ -321,4 +384,4 @@ protected override void RenderOverride(XCanvas canvas, Windows.Foundation.Size a
}
public partial class MathView : BaseView { }
public partial class TextView : BaseView { }
-}
\ No newline at end of file
+}
diff --git a/ReadMe.md b/ReadMe.md
index 0301d2e0..3fb5beac 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -358,6 +358,10 @@ Dependency|Used by|License
[@FoggyFinder](https://github.com/FoggyFinder)
+### Rendering update benchmarks
+
+`CSharpMath.Rendering.Benchmarks` includes a `ViewUpdateChurnBenchmarks` benchmark for the representative `x_1 = 1.234e50` input. It compares repeated LaTeX assignment, repeated content assignment, and measuring a persistent painter after one initial assignment at 100 and 1000 iterations. Run it in Release with BenchmarkDotNet and inspect the generated report; timings are intentionally not recorded here because they depend on the machine and renderer. For high-frequency updates, prefer reusing a parsed content object or painter when the input source permits it.
+
Thanks for reading.