diff --git a/Avalonia.sln b/Avalonia.sln index 8d2479a663..071d0457b8 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -206,14 +206,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -486,10 +486,6 @@ Global {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.Build.0 = Debug|Any CPU {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.ActiveCfg = Release|Any CPU {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.Build.0 = Release|Any CPU - {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.Build.0 = Release|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -502,6 +498,10 @@ Global {EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Debug|Any CPU.Build.0 = Debug|Any CPU {EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.ActiveCfg = Release|Any CPU {EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.Build.0 = Release|Any CPU + {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -558,6 +558,7 @@ Global {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props index d000af1bf6..4929578b60 100644 --- a/build/SourceGenerators.props +++ b/build/SourceGenerators.props @@ -1,7 +1,7 @@ diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 13751b56b5..d98a068d84 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Globalization; using System.Linq; @@ -53,7 +53,11 @@ namespace ControlCatalog.NetCore else if (args.Contains("--full-headless")) { return builder - .UseHeadless(true) + .UseHeadless(new AvaloniaHeadlessPlatformOptions + { + UseHeadlessDrawing = true, + UseCompositor = true + }) .AfterSetup(_ => { DispatcherTimer.RunOnce(async () => @@ -63,12 +67,11 @@ namespace ControlCatalog.NetCore var tc = window.GetLogicalDescendants().OfType().First(); foreach (var page in tc.Items.Cast().ToList()) { - // Skip DatePicker because of some layout bug in grid - if (page.Header.ToString() == "DatePicker") + if (page.Header.ToString() == "DatePicker" || page.Header.ToString() == "TreeView") continue; Console.WriteLine("Selecting " + page.Header); tc.SelectedItem = page; - await Task.Delay(500); + await Task.Delay(50); } Console.WriteLine("Selecting the first page"); tc.SelectedItem = tc.Items.OfType().First(); @@ -77,7 +80,7 @@ namespace ControlCatalog.NetCore for (var c = 0; c < 3; c++) { GC.Collect(2, GCCollectionMode.Forced); - await Task.Delay(500); + await Task.Delay(50); } void FormatMem(string metric, long bytes) @@ -87,7 +90,6 @@ namespace ControlCatalog.NetCore FormatMem("GC allocated bytes", GC.GetTotalMemory(true)); FormatMem("WorkingSet64", Process.GetCurrentProcess().WorkingSet64); - }, TimeSpan.FromSeconds(1)); }) .StartWithClassicDesktopLifetime(args); @@ -111,10 +113,11 @@ namespace ControlCatalog.NetCore { EnableMultiTouch = true, UseDBusMenu = true, - EnableIme = true, + EnableIme = true }) .With(new Win32PlatformOptions { + EnableMultitouch = true }) .UseSkia() .AfterSetup(builder => diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 40591c2d24..7461e78c33 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -13,6 +13,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml b/samples/ControlCatalog/Pages/CompositionPage.axaml new file mode 100644 index 0000000000..22c5c88941 --- /dev/null +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml @@ -0,0 +1,45 @@ + + + Implicit animations + + + + + + + + + + + + + + + + + + + + + + + Resize me + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs new file mode 100644 index 0000000000..18069ca857 --- /dev/null +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class CompositionPage : UserControl +{ + private ImplicitAnimationCollection _implicitAnimations; + + public CompositionPage() + { + AvaloniaXamlLoader.Load(this); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + this.FindControl("Items").Items = CreateColorItems(); + } + + private List CreateColorItems() + { + var list = new List(); + + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 185, 0))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 231, 72, 86))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 120, 215))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 153, 188))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 122, 117, 116))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 118, 118, 118))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 141, 0))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 232, 17, 35))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 99, 177))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 45, 125, 154))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 93, 90, 88))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 76, 74, 72))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 247, 99, 12))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 234, 0, 94))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 142, 140, 216))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 183, 195))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 104, 118, 138))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 105, 121, 126))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 202, 80, 16))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 195, 0, 82))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 107, 105, 214))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 3, 131, 135))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 81, 92, 107))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 74, 84, 89))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 218, 59, 1))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 227, 0, 140))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 135, 100, 184))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 178, 148))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 86, 124, 115))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 100, 124, 100))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 239, 105, 80))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 191, 0, 119))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 116, 77, 169))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 1, 133, 116))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 72, 104, 96))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 82, 94, 84))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 209, 52, 56))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 194, 57, 179))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 177, 70, 194))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 204, 106))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 73, 130, 5))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 132, 117, 69))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 67, 67))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 154, 0, 137))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 136, 23, 152))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 137, 62))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 124, 16))); + list.Add(new CompositionPageColorItem(Color.FromArgb(255, 126, 115, 95))); + + return list; + } + + private void EnsureImplicitAnimations() + { + if (_implicitAnimations == null) + { + var compositor = ElementComposition.GetElementVisual(this)!.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var rotationAnimation = compositor.CreateScalarKeyFrameAnimation(); + rotationAnimation.Target = "RotationAngle"; + rotationAnimation.InsertKeyFrame(.5f, 0.160f); + rotationAnimation.InsertKeyFrame(1f, 0f); + rotationAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var animationGroup = compositor.CreateAnimationGroup(); + animationGroup.Add(offsetAnimation); + animationGroup.Add(rotationAnimation); + + _implicitAnimations = compositor.CreateImplicitAnimationCollection(); + _implicitAnimations["Offset"] = animationGroup; + } + } + + public static void SetEnableAnimations(Border border, bool value) + { + + var page = border.FindAncestorOfType(); + if (page == null) + { + border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); }; + return; + } + + if (ElementComposition.GetElementVisual(page) == null) + return; + + page.EnsureImplicitAnimations(); + ElementComposition.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = + page._implicitAnimations; + } +} + +public class CompositionPageColorItem +{ + public Color Color { get; private set; } + + public SolidColorBrush ColorBrush + { + get { return new SolidColorBrush(Color); } + } + + public String ColorHexValue + { + get { return Color.ToString().Substring(3).ToUpperInvariant(); } + } + + public CompositionPageColorItem(Color color) + { + Color = color; + } +} \ No newline at end of file diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs index 8054b06964..8f4e02df01 100644 --- a/samples/RenderDemo/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -29,6 +29,10 @@ namespace RenderDemo .With(new Win32PlatformOptions { OverlayPopups = true, + }) + .With(new X11PlatformOptions + { + UseCompositor = true }) .UsePlatformDetect() .LogToTrace(); diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index 1d898261a3..19dc7b4ab6 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -29,6 +29,9 @@ namespace Avalonia.Android _thread = new Thread(Loop); _thread.Start(); } + + + public bool RunsInBackground => true; public event Action Tick { diff --git a/src/Avalonia.Base/Animation/Easings/CubicBezier.cs b/src/Avalonia.Base/Animation/Easings/CubicBezier.cs new file mode 100644 index 0000000000..5c2487a516 --- /dev/null +++ b/src/Avalonia.Base/Animation/Easings/CubicBezier.cs @@ -0,0 +1,306 @@ +// ReSharper disable InconsistentNaming +// Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc + +using System; +using Avalonia.Utilities; + +// ReSharper disable CompareOfFloatsByEqualityOperator +// ReSharper disable CommentTypo +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable TooWideLocalVariableScope +// ReSharper disable UnusedMember.Global +#pragma warning disable 649 + +namespace Avalonia.Animation.Easings +{ + /// + /// Represents a cubic bezier curve and can compute Y coordinate for a given X + /// + internal unsafe struct CubicBezier + { + const int CUBIC_BEZIER_SPLINE_SAMPLES = 11; + double ax_; + double bx_; + double cx_; + + double ay_; + double by_; + double cy_; + + double start_gradient_; + double end_gradient_; + + double range_min_; + double range_max_; + private bool monotonically_increasing_; + + fixed double spline_samples_[CUBIC_BEZIER_SPLINE_SAMPLES]; + + public CubicBezier(double p1x, double p1y, double p2x, double p2y) : this() + { + InitCoefficients(p1x, p1y, p2x, p2y); + InitGradients(p1x, p1y, p2x, p2y); + InitRange(p1y, p2y); + InitSpline(); + } + + public readonly double SampleCurveX(double t) + { + // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. + return ((ax_ * t + bx_) * t + cx_) * t; + } + + readonly double SampleCurveY(double t) + { + return ((ay_ * t + by_) * t + cy_) * t; + } + + readonly double SampleCurveDerivativeX(double t) + { + return (3.0 * ax_ * t + 2.0 * bx_) * t + cx_; + } + + readonly double SampleCurveDerivativeY(double t) + { + return (3.0 * ay_ * t + 2.0 * by_) * t + cy_; + } + + public readonly double SolveWithEpsilon(double x, double epsilon) + { + if (x < 0.0) + return 0.0 + start_gradient_ * x; + if (x > 1.0) + return 1.0 + end_gradient_ * (x - 1.0); + return SampleCurveY(SolveCurveX(x, epsilon)); + } + + void InitCoefficients(double p1x, + double p1y, + double p2x, + double p2y) + { + // Calculate the polynomial coefficients, implicit first and last control + // points are (0,0) and (1,1). + cx_ = 3.0 * p1x; + bx_ = 3.0 * (p2x - p1x) - cx_; + ax_ = 1.0 - cx_ - bx_; + + cy_ = 3.0 * p1y; + by_ = 3.0 * (p2y - p1y) - cy_; + ay_ = 1.0 - cy_ - by_; + +#if DEBUG + // Bezier curves with x-coordinates outside the range [0,1] for internal + // control points may have multiple values for t for a given value of x. + // In this case, calls to SolveCurveX may produce ambiguous results. + monotonically_increasing_ = p1x >= 0 && p1x <= 1 && p2x >= 0 && p2x <= 1; +#endif + } + + void InitGradients(double p1x, + double p1y, + double p2x, + double p2y) + { + // End-point gradients are used to calculate timing function results + // outside the range [0, 1]. + // + // There are four possibilities for the gradient at each end: + // (1) the closest control point is not horizontally coincident with regard to + // (0, 0) or (1, 1). In this case the line between the end point and + // the control point is tangent to the bezier at the end point. + // (2) the closest control point is coincident with the end point. In + // this case the line between the end point and the far control + // point is tangent to the bezier at the end point. + // (3) both internal control points are coincident with an endpoint. There + // are two special case that fall into this category: + // CubicBezier(0, 0, 0, 0) and CubicBezier(1, 1, 1, 1). Both are + // equivalent to linear. + // (4) the closest control point is horizontally coincident with the end + // point, but vertically distinct. In this case the gradient at the + // end point is Infinite. However, this causes issues when + // interpolating. As a result, we break down to a simple case of + // 0 gradient under these conditions. + + if (p1x > 0) + start_gradient_ = p1y / p1x; + else if (p1y == 0 && p2x > 0) + start_gradient_ = p2y / p2x; + else if (p1y == 0 && p2y == 0) + start_gradient_ = 1; + else + start_gradient_ = 0; + + if (p2x < 1) + end_gradient_ = (p2y - 1) / (p2x - 1); + else if (p2y == 1 && p1x < 1) + end_gradient_ = (p1y - 1) / (p1x - 1); + else if (p2y == 1 && p1y == 1) + end_gradient_ = 1; + else + end_gradient_ = 0; + } + + const double kBezierEpsilon = 1e-7; + + void InitRange(double p1y, double p2y) + { + range_min_ = 0; + range_max_ = 1; + if (0 <= p1y && p1y < 1 && 0 <= p2y && p2y <= 1) + return; + + double epsilon = kBezierEpsilon; + + // Represent the function's derivative in the form at^2 + bt + c + // as in sampleCurveDerivativeY. + // (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros + // but does not actually give the slope of the curve.) + double a = 3.0 * ay_; + double b = 2.0 * by_; + double c = cy_; + + // Check if the derivative is constant. + if (Math.Abs(a) < epsilon && Math.Abs(b) < epsilon) + return; + + // Zeros of the function's derivative. + double t1; + double t2 = 0; + + if (Math.Abs(a) < epsilon) + { + // The function's derivative is linear. + t1 = -c / b; + } + else + { + // The function's derivative is a quadratic. We find the zeros of this + // quadratic using the quadratic formula. + double discriminant = b * b - 4 * a * c; + if (discriminant < 0) + return; + double discriminant_sqrt = Math.Sqrt(discriminant); + t1 = (-b + discriminant_sqrt) / (2 * a); + t2 = (-b - discriminant_sqrt) / (2 * a); + } + + double sol1 = 0; + double sol2 = 0; + + // If the solution is in the range [0,1] then we include it, otherwise we + // ignore it. + + // An interesting fact about these beziers is that they are only + // actually evaluated in [0,1]. After that we take the tangent at that point + // and linearly project it out. + if (0 < t1 && t1 < 1) + sol1 = SampleCurveY(t1); + + if (0 < t2 && t2 < 1) + sol2 = SampleCurveY(t2); + + range_min_ = Math.Min(Math.Min(range_min_, sol1), sol2); + range_max_ = Math.Max(Math.Max(range_max_, sol1), sol2); + } + + void InitSpline() + { + double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); + for (int i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) + { + spline_samples_[i] = SampleCurveX(i * delta_t); + } + } + + const int kMaxNewtonIterations = 4; + + + public readonly double SolveCurveX(double x, double epsilon) + { + if (x < 0 || x > 1) + throw new ArgumentException(); + + double t0 = 0; + double t1 = 0; + double t2 = x; + double x2 = 0; + double d2; + int i; + +#if DEBUG + if (!monotonically_increasing_) + throw new InvalidOperationException(); +#endif + + // Linear interpolation of spline curve for initial guess. + double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); + for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) + { + if (x <= spline_samples_[i]) + { + t1 = delta_t * i; + t0 = t1 - delta_t; + t2 = t0 + (t1 - t0) * (x - spline_samples_[i - 1]) / + (spline_samples_[i] - spline_samples_[i - 1]); + break; + } + } + + // Perform a few iterations of Newton's method -- normally very fast. + // See https://en.wikipedia.org/wiki/Newton%27s_method. + double newton_epsilon = Math.Min(kBezierEpsilon, epsilon); + for (i = 0; i < kMaxNewtonIterations; i++) + { + x2 = SampleCurveX(t2) - x; + if (Math.Abs(x2) < newton_epsilon) + return t2; + d2 = SampleCurveDerivativeX(t2); + if (Math.Abs(d2) < kBezierEpsilon) + break; + t2 = t2 - x2 / d2; + } + + if (Math.Abs(x2) < epsilon) + return t2; + + // Fall back to the bisection method for reliability. + while (t0 < t1) + { + x2 = SampleCurveX(t2); + if (Math.Abs(x2 - x) < epsilon) + return t2; + if (x > x2) + t0 = t2; + else + t1 = t2; + t2 = (t1 + t0) * .5; + } + + // Failure. + return t2; + } + + public readonly double Solve(double x) + { + return SolveWithEpsilon(x, kBezierEpsilon); + } + + public readonly double SlopeWithEpsilon(double x, double epsilon) + { + x = MathUtilities.Clamp(x, 0.0, 1.0); + double t = SolveCurveX(x, epsilon); + double dx = SampleCurveDerivativeX(t); + double dy = SampleCurveDerivativeY(t); + return dy / dx; + } + + public readonly double Slope(double x) + { + return SlopeWithEpsilon(x, kBezierEpsilon); + } + + public readonly double RangeMin => range_min_; + public readonly double RangeMax => range_max_; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs b/src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs new file mode 100644 index 0000000000..71582fa448 --- /dev/null +++ b/src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs @@ -0,0 +1,27 @@ +using System; + +namespace Avalonia.Animation.Easings; + +public class CubicBezierEasing : IEasing +{ + private CubicBezier _bezier; + //cubic-bezier(0.25, 0.1, 0.25, 1.0) + internal CubicBezierEasing(Point controlPoint1, Point controlPoint2) + { + ControlPoint1 = controlPoint1; + ControlPoint2 = controlPoint2; + if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1) + throw new ArgumentException(); + _bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y); + } + + public Point ControlPoint2 { get; set; } + public Point ControlPoint1 { get; set; } + + internal static IEasing Ease { get; } = new CubicBezierEasing(new Point(0.25, 0.1), new Point(0.25, 1)); + + double IEasing.Ease(double progress) + { + return _bezier.Solve(progress); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index a07e0e3667..15feed388b 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -3,10 +3,13 @@ net6.0;netstandard2.0 Avalonia.Base Avalonia - True + True + true + $(BaseIntermediateOutputPath)\GeneratedFiles + @@ -32,6 +35,11 @@ - + + + + + + diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs index a6a5953827..cefbf642be 100644 --- a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs +++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index 803b8d60dc..267c403ab7 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -1434,7 +1434,7 @@ namespace Avalonia.Collections.Pooled /// /// Returns the internal buffers to the ArrayPool. /// - public void Dispose() + public virtual void Dispose() { ReturnArray(); _size = 0; diff --git a/src/Avalonia.Base/Platform/IPlatformGpu.cs b/src/Avalonia.Base/Platform/IPlatformGpu.cs new file mode 100644 index 0000000000..0507dea1d7 --- /dev/null +++ b/src/Avalonia.Base/Platform/IPlatformGpu.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Platform; + +[Unstable] +public interface IPlatformGpu +{ + IPlatformGpuContext PrimaryContext { get; } +} + +[Unstable] +public interface IPlatformGpuContext : IDisposable +{ + IDisposable EnsureCurrent(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs new file mode 100644 index 0000000000..80e64118ee --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations; + + +/// +/// The base class for both key-frame and expression animation instances +/// Is responsible for activation tracking and for subscribing to properties used in dependencies +/// +internal abstract class AnimationInstanceBase : IAnimationInstance +{ + private List<(ServerObject obj, CompositionProperty member)>? _trackedObjects; + protected PropertySetSnapshot Parameters { get; } + public ServerObject TargetObject { get; } + protected CompositionProperty Property { get; private set; } = null!; + private bool _invalidated; + + public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters) + { + Parameters = parameters; + TargetObject = target; + } + + protected void Initialize(CompositionProperty property, HashSet<(string name, string member)> trackedObjects) + { + if (trackedObjects.Count > 0) + { + _trackedObjects = new (); + foreach (var t in trackedObjects) + { + var obj = Parameters.GetObjectParameter(t.name); + if (obj is ServerObject tracked) + { + var off = tracked.GetCompositionProperty(t.member); + if (off == null) +#if DEBUG + throw new InvalidCastException("Attempting to subscribe to unknown field"); +#else + continue; +#endif + _trackedObjects.Add((tracked, off)); + } + } + } + + Property = property; + } + + public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property); + protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue); + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + _invalidated = false; + return EvaluateCore(now, currentValue); + } + + public virtual void Activate() + { + if (_trackedObjects != null) + foreach (var tracked in _trackedObjects) + tracked.obj.SubscribeToInvalidation(tracked.member, this); + } + + public virtual void Deactivate() + { + if (_trackedObjects != null) + foreach (var tracked in _trackedObjects) + tracked.obj.UnsubscribeFromInvalidation(tracked.member, this); + } + + public void Invalidate() + { + if (_invalidated) + return; + _invalidated = true; + TargetObject.NotifyAnimatedValueChanged(Property); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs new file mode 100644 index 0000000000..c5102a2d7d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -0,0 +1,75 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable CheckNamespace + +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + /// + /// This is the base class for ExpressionAnimation and KeyFrameAnimation. + /// + /// + /// Use the method to start the animation. + /// Value parameters (as opposed to reference parameters which are set using ) + /// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called. + /// Changing the value of the variable after is called will not affect + /// the value of the ExpressionAnimation. + /// See the remarks section of ExpressionAnimation for additional information. + /// + public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase + { + private readonly CompositionPropertySet _propertySet; + internal CompositionAnimation(Compositor compositor) : base(compositor, null!) + { + _propertySet = new CompositionPropertySet(compositor); + } + + /// + /// Clears all of the parameters of the animation. + /// + public void ClearAllParameters() => _propertySet.ClearAll(); + + /// + /// Clears a parameter from the animation. + /// + public void ClearParameter(string key) => _propertySet.Clear(key); + + void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value); + + public void SetColorParameter(string key, Media.Color value) => SetVariant(key, value); + + public void SetMatrix3x2Parameter(string key, Matrix3x2 value) => SetVariant(key, value); + + public void SetMatrix4x4Parameter(string key, Matrix4x4 value) => SetVariant(key, value); + + public void SetQuaternionParameter(string key, Quaternion value) => SetVariant(key, value); + + public void SetReferenceParameter(string key, CompositionObject compositionObject) => + _propertySet.Set(key, compositionObject); + + public void SetScalarParameter(string key, float value) => SetVariant(key, value); + + public void SetVector2Parameter(string key, Vector2 value) => SetVariant(key, value); + + public void SetVector3Parameter(string key, Vector3 value) => SetVariant(key, value); + + public void SetVector4Parameter(string key, Vector4 value) => SetVariant(key, value); + + public string? Target { get; set; } + + internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, + ExpressionVariant? finalValue); + + internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot(); + + void ICompositionAnimationBase.InternalOnly() + { + + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs new file mode 100644 index 0000000000..89f8ba411d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Transport; + + +namespace Avalonia.Rendering.Composition.Animations +{ + public class CompositionAnimationGroup : CompositionObject, ICompositionAnimationBase + { + internal List Animations { get; } = new List(); + void ICompositionAnimationBase.InternalOnly() + { + + } + + public void Add(CompositionAnimation value) => Animations.Add(value); + public void Remove(CompositionAnimation value) => Animations.Remove(value); + public void RemoveAll() => Animations.Clear(); + + public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs new file mode 100644 index 0000000000..163f4e99ba --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -0,0 +1,53 @@ +// ReSharper disable CheckNamespace +using System; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + /// + /// A Composition Animation that uses a mathematical equation to calculate the value for an animating property every frame. + /// + /// + /// The core of ExpressionAnimations allows a developer to define a mathematical equation that can be used to calculate the value + /// of a targeted animating property each frame. + /// This contrasts s, which use an interpolator to define how the animating + /// property changes over time. The mathematical equation can be defined using references to properties + /// of Composition objects, mathematical functions and operators and Input. + /// Use the method to start the animation. + /// + public class ExpressionAnimation : CompositionAnimation + { + private string? _expression; + private Expression? _parsedExpression; + + internal ExpressionAnimation(Compositor compositor) : base(compositor) + { + } + + /// + /// The mathematical equation specifying how the animated value is calculated each frame. + /// The Expression is the core of an and represents the equation + /// the system will use to calculate the value of the animation property each frame. + /// The equation is set on this property in the form of a string. + /// Although expressions can be defined by simple mathematical equations such as "2+2", + /// the real power lies in creating mathematical relationships where the input values can change frame over frame. + /// + public string? Expression + { + get => _expression; + set + { + _expression = value; + _parsedExpression = null; + } + } + + private Expression ParsedExpression => _parsedExpression ??= ExpressionParser.Parse(_expression.AsSpan()); + + internal override IAnimationInstance CreateInstance( + ServerObject targetObject, ExpressionVariant? finalValue) + => new ExpressionAnimationInstance(ParsedExpression, + targetObject, finalValue, CreateSnapshot()); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs new file mode 100644 index 0000000000..764bac9931 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + + /// + /// Server-side counterpart of with values baked-in. + /// + internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance + { + private readonly Expression _expression; + private ExpressionVariant _startingValue; + private readonly ExpressionVariant? _finalValue; + + protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) + { + var ctx = new ExpressionEvaluationContext + { + Parameters = Parameters, + Target = TargetObject, + ForeignFunctionInterface = BuiltInExpressionFfi.Instance, + StartingValue = _startingValue, + FinalValue = _finalValue ?? _startingValue, + CurrentValue = currentValue + }; + return _expression.Evaluate(ref ctx); + } + + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property) + { + _startingValue = startingValue; + var hs = new HashSet<(string, string)>(); + _expression.CollectReferences(hs); + base.Initialize(property, hs); + } + + public ExpressionAnimationInstance(Expression expression, + ServerObject target, + ExpressionVariant? finalValue, + PropertySetSnapshot parameters) : base(target, parameters) + { + _expression = expression; + _finalValue = finalValue; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs new file mode 100644 index 0000000000..4e1972f2c6 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal interface IAnimationInstance + { + ServerObject TargetObject { get; } + ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); + void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property); + void Activate(); + void Deactivate(); + void Invalidate(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs new file mode 100644 index 0000000000..87e5ad757a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs @@ -0,0 +1,15 @@ +// ReSharper disable CheckNamespace + +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + /// + /// Base class for composition animations. + /// + public interface ICompositionAnimationBase + { + internal void InternalOnly(); + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs new file mode 100644 index 0000000000..f4bcc6ff38 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + /// + /// A collection of animations triggered when a condition is met. + /// + /// + /// Implicit animations let you drive animations by specifying trigger conditions rather than requiring the manual definition of animation behavior. + /// They help decouple animation start logic from core app logic. You define animations and the events that should trigger these animations. + /// Currently the only available trigger is animated property change. + /// + /// When expression is used in ImplicitAnimationCollection a special keyword `this.FinalValue` will represent + /// the final value of the animated property that was changed + /// + public class ImplicitAnimationCollection : CompositionObject, IDictionary + { + private Dictionary _inner = new Dictionary(); + private IDictionary _innerface; + internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!) + { + _innerface = _inner; + } + + public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator(); + + void ICollection>.Add(KeyValuePair item) => _innerface.Add(item); + + public void Clear() => _inner.Clear(); + + bool ICollection>.Contains(KeyValuePair item) => _innerface.Contains(item); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => _innerface.CopyTo(array, arrayIndex); + + bool ICollection>.Remove(KeyValuePair item) => _innerface.Remove(item); + + public int Count => _inner.Count; + + bool ICollection>.IsReadOnly => _innerface.IsReadOnly; + + public void Add(string key, ICompositionAnimationBase value) => _inner.Add(key, value); + + public bool ContainsKey(string key) => _inner.ContainsKey(key); + + public bool Remove(string key) => _inner.Remove(key); + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out ICompositionAnimationBase value) => + _inner.TryGetValue(key, out value); + + public ICompositionAnimationBase this[string key] + { + get => _inner[key]; + set => _inner[key] = value; + } + + ICollection IDictionary.Keys => _innerface.Keys; + + ICollection IDictionary.Values => + _innerface.Values; + + // UWP compat + public uint Size => (uint) Count; + + public IReadOnlyDictionary GetView() => + new Dictionary(this); + + public bool HasKey(string key) => ContainsKey(key); + public void Insert(string key, ICompositionAnimationBase animation) => Add(key, animation); + + public ICompositionAnimationBase? Lookup(string key) + { + _inner.TryGetValue(key, out var rv); + return rv; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs new file mode 100644 index 0000000000..a4eeacef32 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs @@ -0,0 +1,76 @@ +using System; +using System.Numerics; + +namespace Avalonia.Rendering.Composition.Animations +{ + /// + /// An interface to define interpolation logic for a particular type + /// + internal interface IInterpolator + { + T Interpolate(T from, T to, float progress); + } + + class ScalarInterpolator : IInterpolator + { + public float Interpolate(float @from, float to, float progress) => @from + (to - @from) * progress; + + public static ScalarInterpolator Instance { get; } = new ScalarInterpolator(); + } + + class Vector2Interpolator : IInterpolator + { + public Vector2 Interpolate(Vector2 @from, Vector2 to, float progress) + => Vector2.Lerp(@from, to, progress); + + public static Vector2Interpolator Instance { get; } = new Vector2Interpolator(); + } + + class Vector3Interpolator : IInterpolator + { + public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress) + => Vector3.Lerp(@from, to, progress); + + public static Vector3Interpolator Instance { get; } = new Vector3Interpolator(); + } + + class Vector4Interpolator : IInterpolator + { + public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress) + => Vector4.Lerp(@from, to, progress); + + public static Vector4Interpolator Instance { get; } = new Vector4Interpolator(); + } + + class QuaternionInterpolator : IInterpolator + { + public Quaternion Interpolate(Quaternion @from, Quaternion to, float progress) + => Quaternion.Lerp(@from, to, progress); + + public static QuaternionInterpolator Instance { get; } = new QuaternionInterpolator(); + } + + class ColorInterpolator : IInterpolator + { + static byte Lerp(float a, float b, float p) => (byte) Math.Max(0, Math.Min(255, (p * (b - a) + a))); + + public static Avalonia.Media.Color + LerpRGB(Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + new Avalonia.Media.Color(Lerp(to.A, @from.A, progress), + Lerp(to.R, @from.R, progress), + Lerp(to.G, @from.G, progress), + Lerp(to.B, @from.B, progress)); + + public Avalonia.Media.Color Interpolate(Avalonia.Media.Color @from, Avalonia.Media.Color to, float progress) + => LerpRGB(@from, to, progress); + + public static ColorInterpolator Instance { get; } = new ColorInterpolator(); + } + + class BooleanInterpolator : IInterpolator + { + public bool Interpolate(bool @from, bool to, float progress) => progress >= 1 ? to : @from; + + public static BooleanInterpolator Instance { get; } = new BooleanInterpolator(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs new file mode 100644 index 0000000000..49b3ab753a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs @@ -0,0 +1,134 @@ +using System; +using Avalonia.Animation; +using Avalonia.Animation.Easings; + +namespace Avalonia.Rendering.Composition.Animations +{ + + /// + /// A time-based animation with one or more key frames. + /// These frames are markers, allowing developers to specify values at specific times for the animating property. + /// KeyFrame animations can be further customized by specifying how the animation interpolates between keyframes. + /// + public abstract class KeyFrameAnimation : CompositionAnimation + { + private TimeSpan _duration = TimeSpan.FromMilliseconds(1); + + internal KeyFrameAnimation(Compositor compositor) : base(compositor) + { + } + + /// + /// The delay behavior of the key frame animation. + /// + public AnimationDelayBehavior DelayBehavior { get; set; } + + /// + /// Delay before the animation starts after is called. + /// + public System.TimeSpan DelayTime { get; set; } + + /// + /// The direction the animation is playing. + /// The Direction property allows you to drive your animation from start to end or end to start or alternate + /// between start and end or end to start if animation has an greater than one. + /// This gives an easy way for customizing animation definitions. + /// + public PlaybackDirection Direction { get; set; } + + /// + /// The duration of the animation. + /// Minimum allowed value is 1ms and maximum allowed value is 24 days. + /// + public TimeSpan Duration + { + get => _duration; + set + { + if (_duration < TimeSpan.FromMilliseconds(1) || _duration > TimeSpan.FromDays(1)) + throw new ArgumentException("Minimum allowed value is 1ms and maximum allowed value is 24 days."); + _duration = value; + } + } + + /// + /// The iteration behavior for the key frame animation. + /// + public AnimationIterationBehavior IterationBehavior { get; set; } + + /// + /// The number of times to repeat the key frame animation. + /// + public int IterationCount { get; set; } = 1; + + /// + /// Specifies how to set the property value when animation is stopped + /// + public AnimationStopBehavior StopBehavior { get; set; } + + private protected abstract IKeyFrames KeyFrames { get; } + + /// + /// Inserts an expression keyframe. + /// + /// + /// The time the key frame should occur at, expressed as a percentage of the animation Duration. Allowed value is from 0.0 to 1.0. + /// + /// The expression used to calculate the value of the key frame. + /// The easing function to use when interpolating between frames. + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, + Easing? easingFunction = null) => + KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction ?? Compositor.DefaultEasing); + } + + /// + /// Specifies the animation delay behavior. + /// + public enum AnimationDelayBehavior + { + /// + /// If a DelayTime is specified, it delays starting the animation according to delay time and after delay + /// has expired it applies animation to the object property. + /// + SetInitialValueAfterDelay, + /// + /// Applies the initial value of the animation (i.e. the value at Keyframe 0) to the object before the delay time + /// is elapsed (when there is a DelayTime specified), it then delays starting the animation according to the DelayTime. + /// + SetInitialValueBeforeDelay + } + + /// + /// Specifies if the animation should loop. + /// + public enum AnimationIterationBehavior + { + /// + /// The animation should loop the specified number of times. + /// + Count, + /// + /// The animation should loop forever. + /// + Forever + } + + /// + /// Specifies the behavior of an animation when it stops. + /// + public enum AnimationStopBehavior + { + /// + /// Leave the animation at its current value. + /// + LeaveCurrentValue, + /// + /// Reset the animation to its initial value. + /// + SetToInitialValue, + /// + /// Set the animation to its final value. + /// + SetToFinalValue + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs new file mode 100644 index 0000000000..0c0fcfaf2b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using Avalonia.Animation; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + /// + /// Server-side counterpart of KeyFrameAnimation with values baked-in + /// + class KeyFrameAnimationInstance : AnimationInstanceBase, IAnimationInstance where T : struct + { + private readonly IInterpolator _interpolator; + private readonly ServerKeyFrame[] _keyFrames; + private readonly ExpressionVariant? _finalValue; + private readonly AnimationDelayBehavior _delayBehavior; + private readonly TimeSpan _delayTime; + private readonly PlaybackDirection _direction; + private readonly TimeSpan _duration; + private readonly AnimationIterationBehavior _iterationBehavior; + private readonly int _iterationCount; + private readonly AnimationStopBehavior _stopBehavior; + private TimeSpan _startedAt; + private T _startingValue; + private readonly TimeSpan _totalDuration; + private bool _finished; + + public KeyFrameAnimationInstance( + IInterpolator interpolator, ServerKeyFrame[] keyFrames, + PropertySetSnapshot snapshot, ExpressionVariant? finalValue, + ServerObject target, + AnimationDelayBehavior delayBehavior, TimeSpan delayTime, + PlaybackDirection direction, TimeSpan duration, + AnimationIterationBehavior iterationBehavior, + int iterationCount, AnimationStopBehavior stopBehavior) : base(target, snapshot) + { + _interpolator = interpolator; + _keyFrames = keyFrames; + _finalValue = finalValue; + _delayBehavior = delayBehavior; + _delayTime = delayTime; + _direction = direction; + _duration = duration; + _iterationBehavior = iterationBehavior; + _iterationCount = iterationCount; + _stopBehavior = stopBehavior; + if (_iterationBehavior == AnimationIterationBehavior.Count) + _totalDuration = delayTime.Add(TimeSpan.FromTicks(iterationCount * _duration.Ticks)); + if (_keyFrames.Length == 0) + throw new InvalidOperationException("Animation has no key frames"); + if(_duration.Ticks <= 0) + throw new InvalidOperationException("Invalid animation duration"); + } + + + protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) + { + var starting = ExpressionVariant.Create(_startingValue); + var ctx = new ExpressionEvaluationContext + { + Parameters = Parameters, + Target = TargetObject, + CurrentValue = currentValue, + FinalValue = _finalValue ?? starting, + StartingValue = starting, + ForeignFunctionInterface = BuiltInExpressionFfi.Instance + }; + var elapsed = now - _startedAt; + var res = EvaluateImpl(elapsed, currentValue, ref ctx); + + if (_iterationBehavior == AnimationIterationBehavior.Count + && !_finished + && elapsed > _totalDuration) + { + // Active check? + TargetObject.Compositor.RemoveFromClock(this); + _finished = true; + } + return res; + } + + private ExpressionVariant EvaluateImpl(TimeSpan elapsed, ExpressionVariant currentValue, ref ExpressionEvaluationContext ctx) + { + if (elapsed < _delayTime) + { + if (_delayBehavior == AnimationDelayBehavior.SetInitialValueBeforeDelay) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[0])); + return currentValue; + } + + elapsed -= _delayTime; + var iterationNumber = elapsed.Ticks / _duration.Ticks; + if (_iterationBehavior == AnimationIterationBehavior.Count + && iterationNumber >= _iterationCount) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[_keyFrames.Length - 1])); + + + var evenIterationNumber = iterationNumber % 2 == 0; + elapsed = TimeSpan.FromTicks(elapsed.Ticks % _duration.Ticks); + + var reverse = + _direction == PlaybackDirection.Alternate + ? !evenIterationNumber + : _direction == PlaybackDirection.AlternateReverse + ? evenIterationNumber + : _direction == PlaybackDirection.Reverse; + + var iterationProgress = elapsed.TotalSeconds / _duration.TotalSeconds; + if (reverse) + iterationProgress = 1 - iterationProgress; + + var left = new ServerKeyFrame + { + Value = _startingValue + }; + var right = _keyFrames[_keyFrames.Length - 1]; + for (var c = 0; c < _keyFrames.Length; c++) + { + var kf = _keyFrames[c]; + if (kf.Key < iterationProgress) + { + // this is the last frame + if (c == _keyFrames.Length - 1) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, kf)); + + left = kf; + right = _keyFrames[c + 1]; + break; + } + } + + var keyProgress = Math.Max(0, Math.Min(1, (iterationProgress - left.Key) / (right.Key - left.Key))); + + var easedKeyProgress = (float)right.EasingFunction.Ease(keyProgress); + if (float.IsNaN(easedKeyProgress) || float.IsInfinity(easedKeyProgress)) + return currentValue; + + return ExpressionVariant.Create(_interpolator.Interpolate( + GetKeyFrame(ref ctx, left), + GetKeyFrame(ref ctx, right), + easedKeyProgress + )); + } + + T GetKeyFrame(ref ExpressionEvaluationContext ctx, ServerKeyFrame f) + { + if (f.Expression != null) + return f.Expression.Evaluate(ref ctx).CastOrDefault(); + else + return f.Value; + } + + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property) + { + _startedAt = startedAt; + _startingValue = startingValue.CastOrDefault(); + var hs = new HashSet<(string, string)>(); + + // TODO: Update subscriptions based on the current keyframe rather than keeping subscriptions to all of them + foreach (var frame in _keyFrames) + frame.Expression?.CollectReferences(hs); + Initialize(property, hs); + } + + public override void Activate() + { + TargetObject.Compositor.AddToClock(this); + base.Activate(); + } + + public override void Deactivate() + { + TargetObject.Compositor.RemoveFromClock(this); + base.Deactivate(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs new file mode 100644 index 0000000000..369cc80b95 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using Avalonia.Animation.Easings; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + + /// + /// Collection of composition animation key frames + /// + /// + class KeyFrames : List>, IKeyFrames + { + void Validate(float key) + { + if (key < 0 || key > 1) + throw new ArgumentException("Key frame key"); + if (Count > 0 && this[Count - 1].NormalizedProgressKey > key) + throw new ArgumentException("Key frame key " + key + " is less than the previous one"); + } + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, IEasing easingFunction) + { + Validate(normalizedProgressKey); + Add(new KeyFrame + { + NormalizedProgressKey = normalizedProgressKey, + Expression = Expression.Parse(value), + EasingFunction = easingFunction + }); + } + + public void Insert(float normalizedProgressKey, T value, IEasing easingFunction) + { + Validate(normalizedProgressKey); + Add(new KeyFrame + { + NormalizedProgressKey = normalizedProgressKey, + Value = value, + EasingFunction = easingFunction + }); + } + + public ServerKeyFrame[] Snapshot() + { + var frames = new ServerKeyFrame[Count]; + for (var c = 0; c < Count; c++) + { + var f = this[c]; + frames[c] = new ServerKeyFrame + { + Expression = f.Expression, + Value = f.Value, + EasingFunction = f.EasingFunction, + Key = f.NormalizedProgressKey + }; + } + return frames; + } + } + + /// + /// Composition animation key frame + /// + struct KeyFrame + { + public float NormalizedProgressKey; + public T Value; + public Expression Expression; + public IEasing EasingFunction; + } + + /// + /// Server-side composition animation key frame + /// + struct ServerKeyFrame + { + public T Value; + public Expression? Expression; + public IEasing EasingFunction; + public float Key; + } + + interface IKeyFrames + { + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, IEasing easingFunction); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs new file mode 100644 index 0000000000..fc6cfc9f3d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + /// + /// A snapshot of properties used by an animation + /// + internal class PropertySetSnapshot : IExpressionParameterCollection, IExpressionObject + { + private readonly Dictionary _dic; + + public struct Value + { + public ExpressionVariant Variant; + public IExpressionObject Object; + + public Value(IExpressionObject o) + { + Object = o; + Variant = default; + } + + public static implicit operator Value(ExpressionVariant v) => new Value + { + Variant = v + }; + } + + public PropertySetSnapshot(Dictionary dic) + { + _dic = dic; + } + + public ExpressionVariant GetParameter(string name) + { + _dic.TryGetValue(name, out var v); + return v.Variant; + } + + public IExpressionObject GetObjectParameter(string name) + { + _dic.TryGetValue(name, out var v); + return v.Object; + } + + public ExpressionVariant GetProperty(string name) => GetParameter(name); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs new file mode 100644 index 0000000000..282973c26a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia.Collections; +using Avalonia.Collections.Pooled; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.Composition; + +/// +/// A renderer that utilizes to render the visual tree +/// +public class CompositingRenderer : IRendererWithCompositor +{ + private readonly IRenderRoot _root; + private readonly Compositor _compositor; + CompositionDrawingContext _recorder = new(); + DrawingContext _recordingContext; + private HashSet _dirty = new(); + private HashSet _recalculateChildren = new(); + private readonly CompositionTarget _target; + private bool _queuedUpdate; + private Action _update; + private Action _invalidateScene; + + /// + /// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered. + /// + public bool RenderOnlyOnRenderThread { get; set; } = true; + + public CompositingRenderer(IRenderRoot root, + Compositor compositor) + { + _root = root; + _compositor = compositor; + _recordingContext = new DrawingContext(_recorder); + _target = compositor.CreateCompositionTarget(root.CreateRenderTarget); + _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); + _update = Update; + _invalidateScene = InvalidateScene; + } + + /// + public bool DrawFps + { + get => _target.DrawFps; + set => _target.DrawFps = value; + } + + /// + public bool DrawDirtyRects + { + get => _target.DrawDirtyRects; + set => _target.DrawDirtyRects = value; + } + + /// + public event EventHandler? SceneInvalidated; + + void QueueUpdate() + { + if(_queuedUpdate) + return; + _queuedUpdate = true; + Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition); + } + + /// + public void AddDirty(IVisual visual) + { + _dirty.Add((Visual)visual); + QueueUpdate(); + } + + /// + public IEnumerable HitTest(Point p, IVisual root, Func? filter) + { + var res = _target.TryHitTest(p, filter); + if(res == null) + yield break; + for (var index = res.Count - 1; index >= 0; index--) + { + var v = res[index]; + if (v is CompositionDrawListVisual dv) + { + if (filter == null || filter(dv.Visual)) + yield return dv.Visual; + } + } + } + + /// + public IVisual? HitTestFirst(Point p, IVisual root, Func? filter) + { + // TODO: Optimize + return HitTest(p, root, filter).FirstOrDefault(); + } + + /// + public void RecalculateChildren(IVisual visual) + { + _recalculateChildren.Add((Visual)visual); + QueueUpdate(); + } + + private void SyncChildren(Visual v) + { + //TODO: Optimize by moving that logic to Visual itself + if(v.CompositionVisual == null) + return; + var compositionChildren = v.CompositionVisual.Children; + var visualChildren = (AvaloniaList)v.GetVisualChildren(); + + PooledList<(IVisual visual, int index)>? sortedChildren = null; + if (v.HasNonUniformZIndexChildren && visualChildren.Count > 1) + { + sortedChildren = new (visualChildren.Count); + for (var c = 0; c < visualChildren.Count; c++) + sortedChildren.Add((visualChildren[c], c)); + + // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. + sortedChildren.Sort(static (lhs, rhs) => + { + var result = lhs.visual.ZIndex.CompareTo(rhs.visual.ZIndex); + return result == 0 ? lhs.index.CompareTo(rhs.index) : result; + }); + } + + if (compositionChildren.Count == visualChildren.Count) + { + bool mismatch = false; + if (v.HasNonUniformZIndexChildren) + { + + + } + + if (sortedChildren != null) + for (var c = 0; c < visualChildren.Count; c++) + { + if (!ReferenceEquals(compositionChildren[c], ((Visual)sortedChildren[c].visual).CompositionVisual)) + { + mismatch = true; + break; + } + } + else + for (var c = 0; c < visualChildren.Count; c++) + if (!ReferenceEquals(compositionChildren[c], ((Visual)visualChildren[c]).CompositionVisual)) + { + mismatch = true; + break; + } + + + if (!mismatch) + { + sortedChildren?.Dispose(); + return; + } + } + + compositionChildren.Clear(); + if (sortedChildren != null) + { + foreach (var ch in sortedChildren) + { + var compositionChild = ((Visual)ch.visual).CompositionVisual; + if (compositionChild != null) + compositionChildren.Add(compositionChild); + } + sortedChildren.Dispose(); + } + else + foreach (var ch in v.GetVisualChildren()) + { + var compositionChild = ((Visual)ch).CompositionVisual; + if (compositionChild != null) + compositionChildren.Add(compositionChild); + } + } + + private void InvalidateScene() => + SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); + + private void Update() + { + _queuedUpdate = false; + foreach (var visual in _dirty) + { + var comp = visual.CompositionVisual; + if(comp == null) + continue; + + // TODO: Optimize all of that by moving to the Visual itself, so we won't have to recalculate every time + comp.Offset = new Vector3((float)visual.Bounds.Left, (float)visual.Bounds.Top, 0); + comp.Size = new Vector2((float)visual.Bounds.Width, (float)visual.Bounds.Height); + comp.Visible = visual.IsVisible; + comp.Opacity = (float)visual.Opacity; + comp.ClipToBounds = visual.ClipToBounds; + comp.Clip = visual.Clip?.PlatformImpl; + comp.OpacityMask = visual.OpacityMask; + + var renderTransform = Matrix.Identity; + + if (visual.HasMirrorTransform) + renderTransform = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); + + if (visual.RenderTransform != null) + { + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var offset = Matrix.CreateTranslation(origin); + renderTransform *= (-offset) * visual.RenderTransform.Value * (offset); + } + + + + comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); + + _recorder.BeginUpdate(comp.DrawList); + visual.Render(_recordingContext); + comp.DrawList = _recorder.EndUpdate(); + + SyncChildren(visual); + } + foreach(var v in _recalculateChildren) + if (!_dirty.Contains(v)) + SyncChildren(v); + _dirty.Clear(); + _recalculateChildren.Clear(); + _target.Size = _root.ClientSize; + _target.Scaling = _root.RenderScaling; + Compositor.InvokeOnNextCommit(_invalidateScene); + } + + public void Resized(Size size) + { + } + + public void Paint(Rect rect) + { + Update(); + _target.RequestRedraw(); + if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground) + Compositor.RequestCommitAsync().Wait(); + else + _target.ImmediateUIThreadRender(); + } + + public void Start() => _target.IsEnabled = true; + + public void Stop() + { + _target.IsEnabled = false; + } + + public void Dispose() + { + Stop(); + _target.Dispose(); + + // Wait for the composition batch to be applied and rendered to guarantee that + // render target is not used anymore and can be safely disposed + if (Compositor.Loop.RunsInBackground) + _compositor.RequestCommitAsync().Wait(); + } + + /// + /// The associated object + /// + public Compositor Compositor => _compositor; +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs new file mode 100644 index 0000000000..47cfcd325b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -0,0 +1,68 @@ +using System; +using System.Numerics; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.Composition; + + +/// +/// A composition visual that holds a list of drawing commands issued by +/// +internal class CompositionDrawListVisual : CompositionContainerVisual +{ + /// + /// The associated + /// + public Visual Visual { get; } + + private bool _drawListChanged; + private CompositionDrawList? _drawList; + + /// + /// The list of drawing commands + /// + public CompositionDrawList? DrawList + { + get => _drawList; + set + { + _drawList?.Dispose(); + _drawList = value; + _drawListChanged = true; + RegisterForSerialization(); + } + } + + private protected override void SerializeChangesCore(BatchStreamWriter writer) + { + writer.Write((byte)(_drawListChanged ? 1 : 0)); + if (_drawListChanged) + { + writer.WriteObject(DrawList?.Clone()); + _drawListChanged = false; + } + base.SerializeChangesCore(writer); + } + + internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server) + { + Visual = visual; + } + + internal override bool HitTest(Point pt, Func? filter) + { + if (DrawList == null) + return false; + if (filter != null && !filter(Visual)) + return false; + if (Visual is ICustomHitTest custom) + return custom.HitTest(pt); + foreach (var op in DrawList) + if (op.Item.HitTest(pt)) + return true; + return false; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs new file mode 100644 index 0000000000..f529ee9cff --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -0,0 +1,141 @@ +using System; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition +{ + /// + /// Base class of the composition API representing a node in the visual tree structure. + /// Composition objects are the visual tree structure on which all other features of the composition API use and build on. + /// The API allows developers to define and create one or many objects each representing a single node in a Visual tree. + /// + public abstract class CompositionObject : IDisposable + { + /// + /// The collection of implicit animations attached to this object. + /// + public ImplicitAnimationCollection? ImplicitAnimations { get; set; } + + private protected InlineDictionary PendingAnimations; + internal CompositionObject(Compositor compositor, ServerObject server) + { + Compositor = compositor; + Server = server; + } + + /// + /// The associated Compositor + /// + public Compositor Compositor { get; } + internal ServerObject Server { get; } + public bool IsDisposed { get; private set; } + private bool _registeredForSerialization; + + private static void ThrowInvalidOperation() => + throw new InvalidOperationException("There is no server-side counterpart for this object"); + + public void Dispose() + { + RegisterForSerialization(); + IsDisposed = true; + } + + /// + /// Connects an animation with the specified property of the object and starts the animation. + /// + public void StartAnimation(string propertyName, CompositionAnimation animation) + => StartAnimation(propertyName, animation, null); + + internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue) + { + throw new ArgumentException("Unknown property " + propertyName); + } + + /// + /// Starts an animation group. + /// The StartAnimationGroup method on CompositionObject lets you start CompositionAnimationGroup. + /// All the animations in the group will be started at the same time on the object. + /// + public void StartAnimationGroup(ICompositionAnimationBase grp) + { + if (grp is CompositionAnimation animation) + { + if(animation.Target == null) + throw new ArgumentException("Animation Target can't be null"); + StartAnimation(animation.Target, animation); + } + else if (grp is CompositionAnimationGroup group) + { + foreach (var a in group.Animations) + { + if (a.Target == null) + throw new ArgumentException("Animation Target can't be null"); + StartAnimation(a.Target, a); + } + } + } + + bool StartAnimationGroupPart(CompositionAnimation animation, string target, ExpressionVariant finalValue) + { + if(animation.Target == null) + throw new ArgumentException("Animation Target can't be null"); + if (animation.Target == target) + { + StartAnimation(animation.Target, animation, finalValue); + return true; + } + else + { + StartAnimation(animation.Target, animation); + return false; + } + } + + internal bool StartAnimationGroup(ICompositionAnimationBase grp, string target, ExpressionVariant finalValue) + { + if (grp is CompositionAnimation animation) + return StartAnimationGroupPart(animation, target, finalValue); + if (grp is CompositionAnimationGroup group) + { + var matched = false; + foreach (var a in group.Animations) + { + if (a.Target == null) + throw new ArgumentException("Animation Target can't be null"); + if (StartAnimationGroupPart(a, target, finalValue)) + matched = true; + } + + return matched; + } + + throw new ArgumentException(); + } + + protected void RegisterForSerialization() + { + if (Server == null) + throw new InvalidOperationException("The object doesn't have an associated server counterpart"); + + if(_registeredForSerialization) + return; + _registeredForSerialization = true; + Compositor.RegisterForSerialization(this); + } + + internal void SerializeChanges(BatchStreamWriter writer) + { + _registeredForSerialization = false; + SerializeChangesCore(writer); + } + + private protected virtual void SerializeChangesCore(BatchStreamWriter writer) + { + if (Server is IDisposable) + writer.Write((byte)(IsDisposed ? 1 : 0)); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs new file mode 100644 index 0000000000..ee4552d154 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + /// + /// s are s that allow storage of key values pairs + /// that can be shared across the application and are not tied to the lifetime of another composition object. + /// s are most commonly used with animations, where they maintain key-value pairs + /// that are referenced to drive portions of composition animations. s + /// provide the ability to insert key-value pairs or retrieve a value for a given key. + /// does not support a delete function – ensure you use + /// to store values that will be shared across the application. + /// + public class CompositionPropertySet : CompositionObject + { + private readonly Dictionary _variants = new Dictionary(); + private readonly Dictionary _objects = new Dictionary(); + + internal CompositionPropertySet(Compositor compositor) : base(compositor, null!) + { + } + + internal void Set(string key, ExpressionVariant value) + { + _objects.Remove(key); + _variants[key] = value; + } + + /* + For INTERNAL USE by CompositionAnimation ONLY, we DON'T support expression + paths like SomeParam.SomePropertyObject.SomeValue + */ + internal void Set(string key, CompositionObject obj) + { + _objects[key] = obj ?? throw new ArgumentNullException(nameof(obj)); + _variants.Remove(key); + } + + public void InsertColor(string propertyName, Avalonia.Media.Color value) => Set(propertyName, value); + + public void InsertMatrix3x2(string propertyName, Matrix3x2 value) => Set(propertyName, value); + + public void InsertMatrix4x4(string propertyName, Matrix4x4 value) => Set(propertyName, value); + + public void InsertQuaternion(string propertyName, Quaternion value) => Set(propertyName, value); + + public void InsertScalar(string propertyName, float value) => Set(propertyName, value); + public void InsertVector2(string propertyName, Vector2 value) => Set(propertyName, value); + + public void InsertVector3(string propertyName, Vector3 value) => Set(propertyName, value); + + public void InsertVector4(string propertyName, Vector4 value) => Set(propertyName, value); + + + CompositionGetValueStatus TryGetVariant(string key, out T value) where T : struct + { + value = default; + if (!_variants.TryGetValue(key, out var v)) + return _objects.ContainsKey(key) + ? CompositionGetValueStatus.TypeMismatch + : CompositionGetValueStatus.NotFound; + + return v.TryCast(out value) ? CompositionGetValueStatus.Succeeded : CompositionGetValueStatus.TypeMismatch; + } + + public CompositionGetValueStatus TryGetColor(string propertyName, out Avalonia.Media.Color value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetMatrix3x2(string propertyName, out Matrix3x2 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetMatrix4x4(string propertyName, out Matrix4x4 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetQuaternion(string propertyName, out Quaternion value) + => TryGetVariant(propertyName, out value); + + + public CompositionGetValueStatus TryGetScalar(string propertyName, out float value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector2(string propertyName, out Vector2 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector3(string propertyName, out Vector3 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector4(string propertyName, out Vector4 value) + => TryGetVariant(propertyName, out value); + + + public void InsertBoolean(string propertyName, bool value) => Set(propertyName, value); + + public CompositionGetValueStatus TryGetBoolean(string propertyName, out bool value) + => TryGetVariant(propertyName, out value); + + internal void ClearAll() + { + _objects.Clear(); + _variants.Clear(); + } + + internal void Clear(string key) + { + _objects.Remove(key); + _variants.Remove(key); + } + + internal PropertySetSnapshot Snapshot() => + SnapshotCore(1); + + private PropertySetSnapshot SnapshotCore(int allowedNestingLevel) + { + var dic = new Dictionary(_objects.Count + _variants.Count); + foreach (var o in _objects) + { + if (o.Value is CompositionPropertySet ps) + { + if (allowedNestingLevel <= 0) + throw new InvalidOperationException("PropertySet depth limit reached"); + dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(allowedNestingLevel - 1)); + } + else if (o.Value.Server == null) + throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); + else + dic[o.Key] = new PropertySetSnapshot.Value(o.Value.Server); + } + + foreach (var v in _variants) + dic[v.Key] = v.Value; + + return new PropertySetSnapshot(dic); + } + } + + public enum CompositionGetValueStatus + { + Succeeded, + TypeMismatch, + NotFound + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs new file mode 100644 index 0000000000..25bbd4dc88 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Collections.Pooled; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.Composition +{ + /// + /// Represents the composition output (e. g. a window, embedded control, entire screen) + /// + public partial class CompositionTarget + { + partial void OnRootChanged() + { + if (Root != null) + Root.Root = this; + } + + partial void OnRootChanging() + { + if (Root != null) + Root.Root = null; + } + + /// + /// Attempts to perform a hit-tst + /// + /// + /// + /// + public PooledList? TryHitTest(Point point, Func? filter) + { + Server.Readback.NextRead(); + if (Root == null) + return null; + var res = new PooledList(); + HitTestCore(Root, point, res, filter); + return res; + } + + /// + /// Attempts to transform a point to a particular CompositionVisual coordinate space + /// + /// + public Point? TryTransformToVisual(CompositionVisual visual, Point point) + { + if (visual.Root != this) + return null; + var v = visual; + var m = Matrix.Identity; + while (v != null) + { + if (!TryGetInvertedTransform(v, out var cm)) + return null; + m = m * cm; + v = v.Parent; + } + + return point * m; + } + + bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix) + { + var m = visual.TryGetServerTransform(); + if (m == null) + { + matrix = default; + return false; + } + + var m33 = MatrixUtils.ToMatrix(m.Value); + return m33.TryInvert(out matrix); + } + + bool TryTransformTo(CompositionVisual visual, ref Point v) + { + if (TryGetInvertedTransform(visual, out var m)) + { + v = v * m; + return true; + } + + return false; + } + + bool HitTestCore(CompositionVisual visual, Point point, PooledList result, + Func? filter) + { + //TODO: Check readback too + if (visual.Visible == false) + return false; + if (!TryTransformTo(visual, ref point)) + return false; + + if (visual.ClipToBounds + && (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y)) + return false; + if (visual.Clip?.FillContains(point) == false) + return false; + + bool success = false; + // Hit-test the current node + if (visual.HitTest(point, filter)) + { + result.Add(visual); + success = true; + } + + // Inspect children too + if (visual is CompositionContainerVisual cv) + for (var c = cv.Children.Count - 1; c >= 0; c--) + { + var ch = cv.Children[c]; + var hit = HitTestCore(ch, point, result, filter); + if (hit) + return true; + } + + return success; + + } + + /// + /// Registers the composition target for explicit redraw + /// + public void RequestRedraw() => RegisterForSerialization(); + + /// + /// Performs composition directly on the UI thread + /// + internal void ImmediateUIThreadRender() + { + Compositor.RequestCommitAsync(); + Compositor.Server.Render(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs new file mode 100644 index 0000000000..1bdae44cb9 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Threading; + + +namespace Avalonia.Rendering.Composition +{ + /// + /// The Compositor class manages communication between UI-thread and render-thread parts of the composition engine. + /// It also serves as a factory to create UI-thread parts of various composition objects + /// + public partial class Compositor + { + internal IRenderLoop Loop { get; } + private ServerCompositor _server; + private bool _implicitBatchCommitQueued; + private Action _implicitBatchCommit; + private BatchStreamObjectPool _batchObjectPool = new(); + private BatchStreamMemoryPool _batchMemoryPool = new(); + private List _objectsForSerialization = new(); + internal ServerCompositor Server => _server; + internal IEasing DefaultEasing { get; } + private List? _invokeOnNextCommit; + private readonly Stack> _invokeListPool = new(); + + /// + /// Creates a new compositor on a specified render loop that would use a particular GPU + /// + /// + /// + public Compositor(IRenderLoop loop, IPlatformGpu? gpu) + { + Loop = loop; + _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); + _implicitBatchCommit = ImplicitBatchCommit; + + DefaultEasing = new CubicBezierEasing(new Point(0.25f, 0.1f), new Point(0.25f, 1f)); + } + + /// + /// Creates a new CompositionTarget + /// + /// A factory method to create IRenderTarget to be called from the render thread + /// + public CompositionTarget CreateCompositionTarget(Func renderTargetFactory) + { + return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); + } + + /// + /// Requests pending changes in the composition objects to be serialized and sent to the render thread + /// + /// A task that completes when sent changes are applied and rendered on the render thread + public Task RequestCommitAsync() + { + Dispatcher.UIThread.VerifyAccess(); + var batch = new Batch(); + + using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) + { + foreach (var obj in _objectsForSerialization) + { + writer.WriteObject(obj.Server); + obj.SerializeChanges(writer); +#if DEBUG_COMPOSITOR_SERIALIZATION + writer.Write(BatchStreamDebugMarkers.ObjectEndMagic); + writer.WriteObject(BatchStreamDebugMarkers.ObjectEndMarker); +#endif + } + _objectsForSerialization.Clear(); + } + + batch.CommitedAt = Server.Clock.Elapsed; + _server.EnqueueBatch(batch); + if (_invokeOnNextCommit != null) + ScheduleCommitCallbacks(batch.Completed); + + return batch.Completed; + } + + async void ScheduleCommitCallbacks(Task task) + { + var list = _invokeOnNextCommit; + _invokeOnNextCommit = null; + await task; + foreach (var i in list!) + i(); + list.Clear(); + _invokeListPool.Push(list); + } + + public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); + + public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this); + + public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this) + { + Expression = expression + }; + + public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); + + public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); + + private void QueueImplicitBatchCommit() + { + if(_implicitBatchCommitQueued) + return; + _implicitBatchCommitQueued = true; + Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch); + } + + private void ImplicitBatchCommit() + { + _implicitBatchCommitQueued = false; + RequestCommitAsync(); + } + + internal void RegisterForSerialization(CompositionObject compositionObject) + { + Dispatcher.UIThread.VerifyAccess(); + _objectsForSerialization.Add(compositionObject); + QueueImplicitBatchCommit(); + } + + internal void InvokeOnNextCommit(Action action) + { + _invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new(); + _invokeOnNextCommit.Add(action); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs new file mode 100644 index 0000000000..caf074dd6b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -0,0 +1,24 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + /// + /// A node in the visual tree that can have children. + /// + public partial class CompositionContainerVisual : CompositionVisual + { + public CompositionVisualCollection Children { get; private set; } = null!; + + partial void InitializeDefaultsExtra() + { + Children = new CompositionVisualCollection(this, Server.Children); + } + + private protected override void OnRootChangedCore() + { + foreach (var ch in Children) + ch.Root = Root; + base.OnRootChangedCore(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs new file mode 100644 index 0000000000..432a0832f2 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -0,0 +1,102 @@ +using System; +using Avalonia.Collections.Pooled; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +/// +/// A list of serialized drawing commands +/// +internal class CompositionDrawList : PooledList> +{ + public Size? Size { get; set; } + + public CompositionDrawList() + { + + } + + public CompositionDrawList(int capacity) : base(capacity) + { + + } + + public override void Dispose() + { + foreach(var item in this) + item.Dispose(); + base.Dispose(); + } + + public CompositionDrawList Clone() + { + var clone = new CompositionDrawList(Count) { Size = Size }; + foreach (var r in this) + clone.Add(r.Clone()); + return clone; + } + + public void Render(CompositorDrawingContextProxy canvas) + { + foreach (var cmd in this) + { + canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList; + cmd.Item.Render(canvas); + } + + canvas.VisualBrushDrawList = null; + } +} + +/// +/// An helper class for building +/// +internal class CompositionDrawListBuilder +{ + private CompositionDrawList? _operations; + private bool _owns; + + public void Reset(CompositionDrawList? previousOperations) + { + _operations = previousOperations; + _owns = false; + } + + public int Count => _operations?.Count ?? 0; + public CompositionDrawList? DrawOperations => _operations; + + void MakeWritable(int atIndex) + { + if(_owns) + return; + _owns = true; + var newOps = new CompositionDrawList(_operations?.Count ?? Math.Max(1, atIndex)); + if (_operations != null) + { + for (var c = 0; c < atIndex; c++) + newOps.Add(_operations[c].Clone()); + } + + _operations = newOps; + } + + public void ReplaceDrawOperation(int index, IDrawOperation node) + { + MakeWritable(index); + DrawOperations!.Add(RefCountable.Create(node)); + } + + public void AddDrawOperation(IDrawOperation node) + { + MakeWritable(Count); + DrawOperations!.Add(RefCountable.Create(node)); + } + + public void TrimTo(int count) + { + if (count < Count) + _operations!.RemoveRange(count, _operations.Count - count); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs new file mode 100644 index 0000000000..d7c1ef125d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Avalonia.VisualTree; +namespace Avalonia.Rendering.Composition; + +/// +/// An IDrawingContextImpl implementation that builds +/// +internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport +{ + private CompositionDrawListBuilder _builder = new(); + private int _drawOperationIndex; + + /// + public Matrix Transform { get; set; } = Matrix.Identity; + + /// + public void Clear(Color color) + { + // Cannot clear a deferred scene. + } + + /// + public void Dispose() + { + // Nothing to do here since we allocate no unmanaged resources. + } + + public void BeginUpdate(CompositionDrawList? list) + { + _builder.Reset(list); + _drawOperationIndex = 0; + } + + public CompositionDrawList EndUpdate() + { + _builder.TrimTo(_drawOperationIndex); + return _builder.DrawOperations!; + } + + /// + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) + { + Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + var next = NextDrawAs(); + + if (next == null || + !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) + { + Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) + { + // This method is currently only used to composite layers so shouldn't be called here. + throw new NotSupportedException(); + } + + /// + public void DrawLine(IPen pen, Point p1, Point p2) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) + { + Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, + BoxShadows boxShadows = default) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) + { + Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, material, rect)) + { + Add(new ExperimentalAcrylicNode(Transform, material, rect)); + } + else + { + ++_drawOperationIndex; + } + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) + { + Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + public void Custom(ICustomDrawOperation custom) + { + var next = NextDrawAs(); + if (next == null || !next.Item.Equals(Transform, custom)) + Add(new CustomDrawOperation(custom, Transform)); + else + ++_drawOperationIndex; + } + + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) + { + Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); + } + + else + { + ++_drawOperationIndex; + } + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); + } + + /// + public void PopClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new ClipNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopGeometryClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new GeometryClipNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopBitmapBlendMode() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new BitmapBlendModeNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopOpacity() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new OpacityNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopOpacityMask() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null, null)) + { + Add(new OpacityMaskNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushClip(Rect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushClip(RoundedRect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushGeometryClip(IGeometryImpl? clip) + { + if (clip is null) + return; + + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new GeometryClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushOpacity(double opacity) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(opacity)) + { + Add(new OpacityNode(opacity)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushOpacityMask(IBrush mask, Rect bounds) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(mask, bounds)) + { + Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(blendingMode)) + { + Add(new BitmapBlendModeNode(blendingMode)); + } + else + { + ++_drawOperationIndex; + } + } + + private void Add(T node) where T : class, IDrawOperation + { + if (_drawOperationIndex < _builder.Count) + { + _builder.ReplaceDrawOperation(_drawOperationIndex, node); + } + else + { + _builder.AddDrawOperation(node); + } + + ++_drawOperationIndex; + } + + private IRef? NextDrawAs() where T : class, IDrawOperation + { + return _drawOperationIndex < _builder.Count + ? _builder.DrawOperations![_drawOperationIndex] as IRef + : null; + } + + private IDisposable? CreateChildScene(IBrush? brush) + { + if (brush is VisualBrush visualBrush) + { + var visual = visualBrush.Visual; + + if (visual != null) + { + // TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer + // We should directly reference the corresponding CompositionVisual (which should + // be attached to the same composition target) like UWP does. + // Render-able visuals shouldn't be dangling unattached + (visual as IVisualBrushInitialize)?.EnsureInitialized(); + + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(null); + ImmediateRenderer.Render(visual, new DrawingContext(recorder)); + var drawList = recorder.EndUpdate(); + drawList.Size = visual.Bounds.Size; + + return drawList; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs new file mode 100644 index 0000000000..1397a20fb6 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs @@ -0,0 +1,14 @@ +namespace Avalonia.Rendering.Composition; + +/// +/// Enables access to composition visual objects that back XAML elements in the XAML composition tree. +/// +public static class ElementComposition +{ + /// + /// Gets CompositionVisual that backs a Visual + /// + /// + /// + public static CompositionVisual? GetElementVisual(Visual visual) => visual.CompositionVisual; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Enums.cs b/src/Avalonia.Base/Rendering/Composition/Enums.cs new file mode 100644 index 0000000000..e349845cbf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Enums.cs @@ -0,0 +1,120 @@ +using System; + +namespace Avalonia.Rendering.Composition +{ + public enum CompositionBlendMode + { + /// No regions are enabled. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_clr.svg) + Clear, + + /// Only the source will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src.svg) + Src, + + /// Only the destination will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst.svg) + Dst, + + /// Source is placed over the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-over.svg) + SrcOver, + + /// Destination is placed over the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-over.svg) + DstOver, + + /// The source that overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-in.svg) + SrcIn, + + /// Destination which overlaps the source, replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-in.svg) + DstIn, + + /// Source is placed, where it falls outside of the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-out.svg) + SrcOut, + + /// Destination is placed, where it falls outside of the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-out.svg) + DstOut, + + /// Source which overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-atop.svg) + SrcATop, + + /// Destination which overlaps the source replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-atop.svg) + DstATop, + + /// The non-overlapping regions of source and destination are combined. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_xor.svg) + Xor, + + /// Display the sum of the source image and destination image. [Porter Duff Compositing Operators] + Plus, + + /// Multiplies all components (= alpha and color). [Separable Blend Modes] + Modulate, + + /// Multiplies the complements of the backdrop and source CompositionColorvalues, then complements the result. [Separable Blend Modes] + Screen, + + /// Multiplies or screens the colors, depending on the backdrop CompositionColorvalue. [Separable Blend Modes] + Overlay, + + /// Selects the darker of the backdrop and source colors. [Separable Blend Modes] + Darken, + + /// Selects the lighter of the backdrop and source colors. [Separable Blend Modes] + Lighten, + + /// Brightens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes] + ColorDodge, + + /// Darkens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes] + ColorBurn, + + /// Multiplies or screens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes] + HardLight, + + /// Darkens or lightens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes] + SoftLight, + + /// Subtracts the darker of the two constituent colors from the lighter color. [Separable Blend Modes] + Difference, + + /// Produces an effect similar to that of the Difference mode but lower in contrast. [Separable Blend Modes] + Exclusion, + + /// The source CompositionColoris multiplied by the destination CompositionColorand replaces the destination [Separable Blend Modes] + Multiply, + + /// Creates a CompositionColorwith the hue of the source CompositionColorand the saturation and luminosity of the backdrop color. [Non-Separable Blend Modes] + Hue, + + /// Creates a CompositionColorwith the saturation of the source CompositionColorand the hue and luminosity of the backdrop color. [Non-Separable Blend Modes] + Saturation, + + /// Creates a CompositionColorwith the hue and saturation of the source CompositionColorand the luminosity of the backdrop color. [Non-Separable Blend Modes] + Color, + + /// Creates a CompositionColorwith the luminosity of the source CompositionColorand the hue and saturation of the backdrop color. [Non-Separable Blend Modes] + Luminosity, + } + + public enum CompositionGradientExtendMode + { + Clamp, + Wrap, + Mirror + } + + [Flags] + public enum CompositionTileMode + { + None = 0, + TileX = 1, + TileY = 2, + FlipX = 4, + FlipY = 8, + Tile = TileX | TileY, + Flip = FlipX | FlipY + } + + public enum CompositionStretch + { + None = 0, + Fill = 1, + //TODO: Uniform, UniformToFill + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs new file mode 100644 index 0000000000..44347d2c7a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Expressions +{ + /// + /// Built-in functions for Foreign Function Interface available from composition animation expressions + /// + internal class BuiltInExpressionFfi : IExpressionForeignFunctionInterface + { + private readonly DelegateExpressionFfi _registry; + + static float Lerp(float a, float b, float p) => p * (b - a) + a; + + static Matrix3x2 Inverse(Matrix3x2 m) + { + Matrix3x2.Invert(m, out var r); + return r; + } + + static Matrix4x4 Inverse(Matrix4x4 m) + { + Matrix4x4.Invert(m, out var r); + return r; + } + + static float SmoothStep(float edge0, float edge1, float x) + { + var t = MathUtilities.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); + } + + static Vector2 SmoothStep(Vector2 edge0, Vector2 edge1, Vector2 x) + { + return new Vector2( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y) + + ); + } + static Vector3 SmoothStep(Vector3 edge0, Vector3 edge1, Vector3 x) + { + return new Vector3( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y), + SmoothStep(edge0.Z, edge1.Z, x.Z) + + ); + } + + static Vector4 SmoothStep(Vector4 edge0, Vector4 edge1, Vector4 x) + { + return new Vector4( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y), + SmoothStep(edge0.Z, edge1.Z, x.Z), + SmoothStep(edge0.W, edge1.W, x.W) + ); + } + + private BuiltInExpressionFfi() + { + _registry = new DelegateExpressionFfi + { + {"Abs", (float f) => Math.Abs(f)}, + {"Abs", (Vector2 v) => Vector2.Abs(v)}, + {"Abs", (Vector3 v) => Vector3.Abs(v)}, + {"Abs", (Vector4 v) => Vector4.Abs(v)}, + + {"ACos", (float f) => (float) Math.Acos(f)}, + {"ASin", (float f) => (float) Math.Asin(f)}, + {"ATan", (float f) => (float) Math.Atan(f)}, + {"Ceil", (float f) => (float) Math.Ceiling(f)}, + + {"Clamp", (float a1, float a2, float a3) => MathUtilities.Clamp(a1, a2, a3)}, + {"Clamp", (Vector2 a1, Vector2 a2, Vector2 a3) => Vector2.Clamp(a1, a2, a3)}, + {"Clamp", (Vector3 a1, Vector3 a2, Vector3 a3) => Vector3.Clamp(a1, a2, a3)}, + {"Clamp", (Vector4 a1, Vector4 a2, Vector4 a3) => Vector4.Clamp(a1, a2, a3)}, + + {"Concatenate", (Quaternion a1, Quaternion a2) => Quaternion.Concatenate(a1, a2)}, + {"Cos", (float a) => (float) Math.Cos(a)}, + + /* + TODO: + ColorHsl(Float h, Float s, Float l) + ColorLerpHSL(Color colorTo, CompositionColorcolorFrom, Float progress) + */ + + { + "ColorLerp", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + ColorInterpolator.LerpRGB(to, from, progress) + }, + { + "ColorLerpRGB", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + ColorInterpolator.LerpRGB(to, from, progress) + }, + { + "ColorRGB", (float a, float r, float g, float b) => Avalonia.Media.Color.FromArgb( + (byte) MathUtilities.Clamp(a, 0, 255), + (byte) MathUtilities.Clamp(r, 0, 255), + (byte) MathUtilities.Clamp(g, 0, 255), + (byte) MathUtilities.Clamp(b, 0, 255) + ) + }, + + {"Distance", (Vector2 a1, Vector2 a2) => Vector2.Distance(a1, a2)}, + {"Distance", (Vector3 a1, Vector3 a2) => Vector3.Distance(a1, a2)}, + {"Distance", (Vector4 a1, Vector4 a2) => Vector4.Distance(a1, a2)}, + + {"DistanceSquared", (Vector2 a1, Vector2 a2) => Vector2.DistanceSquared(a1, a2)}, + {"DistanceSquared", (Vector3 a1, Vector3 a2) => Vector3.DistanceSquared(a1, a2)}, + {"DistanceSquared", (Vector4 a1, Vector4 a2) => Vector4.DistanceSquared(a1, a2)}, + + {"Floor", (float v) => (float) Math.Floor(v)}, + + {"Inverse", (Matrix3x2 v) => Inverse(v)}, + {"Inverse", (Matrix4x4 v) => Inverse(v)}, + + + {"Length", (Vector2 a1) => a1.Length()}, + {"Length", (Vector3 a1) => a1.Length()}, + {"Length", (Vector4 a1) => a1.Length()}, + {"Length", (Quaternion a1) => a1.Length()}, + + {"LengthSquared", (Vector2 a1) => a1.LengthSquared()}, + {"LengthSquared", (Vector3 a1) => a1.LengthSquared()}, + {"LengthSquared", (Vector4 a1) => a1.LengthSquared()}, + {"LengthSquared", (Quaternion a1) => a1.LengthSquared()}, + + {"Lerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, + {"Lerp", (Vector2 a1, Vector2 a2, float a3) => Vector2.Lerp(a1, a2, a3)}, + {"Lerp", (Vector3 a1, Vector3 a2, float a3) => Vector3.Lerp(a1, a2, a3)}, + {"Lerp", (Vector4 a1, Vector4 a2, float a3) => Vector4.Lerp(a1, a2, a3)}, + + + {"Ln", (float f) => (float) Math.Log(f)}, + {"Log10", (float f) => (float) Math.Log10(f)}, + + {"Matrix3x2.CreateFromScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, + {"Matrix3x2.CreateFromTranslation", (Vector2 v) => Matrix3x2.CreateTranslation(v)}, + {"Matrix3x2.CreateRotation", (float v) => Matrix3x2.CreateRotation(v)}, + {"Matrix3x2.CreateScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, + {"Matrix3x2.CreateSkew", (float a1, float a2, Vector2 a3) => Matrix3x2.CreateSkew(a1, a2, a3)}, + {"Matrix3x2.CreateTranslation", (Vector2 v) => Matrix3x2.CreateScale(v)}, + { + "Matrix3x2", (float m11, float m12, float m21, float m22, float m31, float m32) => + new Matrix3x2(m11, m12, m21, m22, m31, m32) + }, + {"Matrix4x4.CreateFromAxisAngle", (Vector3 v, float angle) => Matrix4x4.CreateFromAxisAngle(v, angle)}, + {"Matrix4x4.CreateFromScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4.CreateFromTranslation", (Vector3 v) => Matrix4x4.CreateTranslation(v)}, + {"Matrix4x4.CreateScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4.CreateTranslation", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4", (Matrix3x2 m) => new Matrix4x4(m)}, + { + "Matrix4x4", + (float m11, float m12, float m13, float m14, + float m21, float m22, float m23, float m24, + float m31, float m32, float m33, float m34, + float m41, float m42, float m43, float m44) => + new Matrix4x4( + m11, m12, m13, m14, + m21, m22, m23, m24, + m31, m32, m33, m34, + m41, m42, m43, m44) + }, + + + {"Max", (float a1, float a2) => Math.Max(a1, a2)}, + {"Max", (Vector2 a1, Vector2 a2) => Vector2.Max(a1, a2)}, + {"Max", (Vector3 a1, Vector3 a2) => Vector3.Max(a1, a2)}, + {"Max", (Vector4 a1, Vector4 a2) => Vector4.Max(a1, a2)}, + + + {"Min", (float a1, float a2) => Math.Min(a1, a2)}, + {"Min", (Vector2 a1, Vector2 a2) => Vector2.Min(a1, a2)}, + {"Min", (Vector3 a1, Vector3 a2) => Vector3.Min(a1, a2)}, + {"Min", (Vector4 a1, Vector4 a2) => Vector4.Min(a1, a2)}, + + {"Mod", (float a, float b) => a % b}, + + {"Normalize", (Quaternion a) => Quaternion.Normalize(a)}, + {"Normalize", (Vector2 a) => Vector2.Normalize(a)}, + {"Normalize", (Vector3 a) => Vector3.Normalize(a)}, + {"Normalize", (Vector4 a) => Vector4.Normalize(a)}, + + {"Pow", (float a, float b) => (float) Math.Pow(a, b)}, + {"Quaternion.CreateFromAxisAngle", (Vector3 a, float b) => Quaternion.CreateFromAxisAngle(a, b)}, + {"Quaternion", (float a, float b, float c, float d) => new Quaternion(a, b, c, d)}, + + {"Round", (float a) => (float) Math.Round(a)}, + + {"Scale", (Matrix3x2 a, float b) => a * b}, + {"Scale", (Matrix4x4 a, float b) => a * b}, + {"Scale", (Vector2 a, float b) => a * b}, + {"Scale", (Vector3 a, float b) => a * b}, + {"Scale", (Vector4 a, float b) => a * b}, + + {"Sin", (float a) => (float) Math.Sin(a)}, + + {"SmoothStep", (float a1, float a2, float a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector2 a1, Vector2 a2, Vector2 a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector3 a1, Vector3 a2, Vector3 a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector4 a1, Vector4 a2, Vector4 a3) => SmoothStep(a1, a2, a3)}, + + // I have no idea how to do a spherical interpolation for a scalar value, so we are doing a linear one + {"Slerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, + {"Slerp", (Quaternion a1, Quaternion a2, float a3) => Quaternion.Slerp(a1, a2, a3)}, + + {"Sqrt", (float a) => (float) Math.Sqrt(a)}, + {"Square", (float a) => a * a}, + {"Tan", (float a) => (float) Math.Tan(a)}, + + {"ToRadians", (float a) => (float) (a * Math.PI / 180)}, + {"ToDegrees", (float a) => (float) (a * 180d / Math.PI)}, + + {"Transform", (Vector2 a, Matrix3x2 b) => Vector2.Transform(a, b)}, + {"Transform", (Vector3 a, Matrix4x4 b) => Vector3.Transform(a, b)}, + + {"Vector2", (float a, float b) => new Vector2(a, b)}, + {"Vector3", (float a, float b, float c) => new Vector3(a, b, c)}, + {"Vector3", (Vector2 v2, float z) => new Vector3(v2, z)}, + {"Vector4", (float a, float b, float c, float d) => new Vector4(a, b, c, d)}, + {"Vector4", (Vector2 v2, float z, float w) => new Vector4(v2, z, w)}, + {"Vector4", (Vector3 v3, float w) => new Vector4(v3, w)}, + }; + } + + public bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result) => + _registry.Call(name, arguments, out result); + + public static BuiltInExpressionFfi Instance { get; } = new BuiltInExpressionFfi(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs new file mode 100644 index 0000000000..85c6141409 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Expressions +{ + /// + /// Foreign function interface for composition animations based on calling delegates + /// + internal class DelegateExpressionFfi : IExpressionForeignFunctionInterface, IEnumerable + { + struct FfiRecord + { + public VariantType[] Types; + public Func, ExpressionVariant> Delegate; + } + + private readonly Dictionary>> + _registry = new Dictionary>>(); + + public bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result) + { + result = default; + if (!_registry.TryGetValue(name, out var nameGroup)) + return false; + if (!nameGroup.TryGetValue(arguments.Count, out var countGroup)) + return false; + foreach (var record in countGroup) + { + var match = true; + for (var c = 0; c < arguments.Count; c++) + { + if (record.Types[c] != arguments[c].Type) + { + match = false; + break; + } + } + + if (match) + { + result = record.Delegate(arguments); + return true; + } + } + + return false; + } + + // Stub for collection initializer + IEnumerator IEnumerable.GetEnumerator() => Array.Empty().GetEnumerator(); + + void Add(string name, Func, ExpressionVariant> cb, + params Type[] types) + { + if (!_registry.TryGetValue(name, out var nameGroup)) + _registry[name] = nameGroup = + new Dictionary>(); + if (!nameGroup.TryGetValue(types.Length, out var countGroup)) + nameGroup[types.Length] = countGroup = new List(); + + countGroup.Add(new FfiRecord + { + Types = types.Select(t => TypeMap[t]).ToArray(), + Delegate = cb + }); + } + + static readonly Dictionary TypeMap = new Dictionary + { + [typeof(bool)] = VariantType.Boolean, + [typeof(float)] = VariantType.Scalar, + [typeof(Vector2)] = VariantType.Vector2, + [typeof(Vector3)] = VariantType.Vector3, + [typeof(Vector4)] = VariantType.Vector4, + [typeof(Matrix3x2)] = VariantType.Matrix3x2, + [typeof(Matrix4x4)] = VariantType.Matrix4x4, + [typeof(Quaternion)] = VariantType.Quaternion, + [typeof(Color)] = VariantType.Color + }; + + public void Add(string name, Func cb) where T1 : struct + { + Add(name, args => cb(args[0].CastOrDefault()), typeof(T1)); + } + + public void Add(string name, Func cb) where T1 : struct where T2 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault()), typeof(T1), typeof(T2)); + } + + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault(), args[2].CastOrDefault()), typeof(T1), typeof(T2), + typeof(T3)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); + } + + + public void Add(string name, + Func cb) + where T1 : struct + where T2 : struct + where T3 : struct + where T4 : struct + where T5 : struct + where T6 : struct + where T7 : struct + where T8 : struct + where T9 : struct + where T10 : struct + where T11 : struct + where T12 : struct + where T13 : struct + where T14 : struct + where T15 : struct + where T16 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault() + ), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), + typeof(T5), typeof(T6), typeof(T7), typeof(T8), + typeof(T9), typeof(T10), typeof(T11), typeof(T12), + typeof(T13), typeof(T14), typeof(T15), typeof(T16) + ); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs new file mode 100644 index 0000000000..5abba00365 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Expressions +{ + /// + /// A parsed composition expression + /// + internal abstract class Expression + { + public abstract ExpressionType Type { get; } + public static Expression Parse(string expression) + { + return ExpressionParser.Parse(expression.AsSpan()); + } + + public abstract ExpressionVariant Evaluate(ref ExpressionEvaluationContext context); + + public virtual void CollectReferences(HashSet<(string parameter, string property)> references) + { + + } + + protected abstract string Print(); + public override string ToString() => Print(); + + internal static string OperatorName(ExpressionType t) + { + var attr = typeof(ExpressionType).GetMember(t.ToString())[0] + .GetCustomAttribute(); + if (attr != null) + return attr.Name; + return t.ToString(); + } + } + + internal class PrettyPrintStringAttribute : Attribute + { + public string Name { get; } + + public PrettyPrintStringAttribute(string name) + { + Name = name; + } + } + + internal enum ExpressionType + { + // Binary operators + [PrettyPrintString("+")] + Add, + [PrettyPrintString("-")] + Subtract, + [PrettyPrintString("/")] + Divide, + [PrettyPrintString("*")] + Multiply, + [PrettyPrintString(">")] + MoreThan, + [PrettyPrintString("<")] + LessThan, + [PrettyPrintString(">=")] + MoreThanOrEqual, + [PrettyPrintString("<=")] + LessThanOrEqual, + [PrettyPrintString("&&")] + LogicalAnd, + [PrettyPrintString("||")] + LogicalOr, + [PrettyPrintString("%")] + Remainder, + [PrettyPrintString("==")] + Equals, + [PrettyPrintString("!=")] + NotEquals, + // Unary operators + [PrettyPrintString("!")] + Not, + [PrettyPrintString("-")] + UnaryMinus, + // The rest + MemberAccess, + Parameter, + FunctionCall, + Keyword, + Constant, + ConditionalExpression + } + + internal enum ExpressionKeyword + { + StartingValue, + CurrentValue, + FinalValue, + Target, + Pi, + True, + False + } + + internal class ConditionalExpression : Expression + { + public Expression Condition { get; } + public Expression TruePart { get; } + public Expression FalsePart { get; } + public override ExpressionType Type => ExpressionType.ConditionalExpression; + + public ConditionalExpression(Expression condition, Expression truePart, Expression falsePart) + { + Condition = condition; + TruePart = truePart; + FalsePart = falsePart; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + var cond = Condition.Evaluate(ref context); + if (cond.Type == VariantType.Boolean && cond.Boolean) + return TruePart.Evaluate(ref context); + return FalsePart.Evaluate(ref context); + } + + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Condition.CollectReferences(references); + TruePart.CollectReferences(references); + FalsePart.CollectReferences(references); + } + + protected override string Print() => $"({Condition}) ? ({TruePart}) : ({FalsePart})"; + } + + internal class ConstantExpression : Expression + { + public float Constant { get; } + public override ExpressionType Type => ExpressionType.Constant; + + public ConstantExpression(float constant) + { + Constant = constant; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) => Constant; + + protected override string Print() => Constant.ToString(CultureInfo.InvariantCulture); + } + + internal class FunctionCallExpression : Expression + { + public string Name { get; } + public List Parameters { get; } + public override ExpressionType Type => ExpressionType.FunctionCall; + + public FunctionCallExpression(string name, List parameters) + { + Name = name; + Parameters = parameters; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (context.ForeignFunctionInterface == null) + return default; + var args = new List(); + foreach (var expr in Parameters) + args.Add(expr.Evaluate(ref context)); + if (!context.ForeignFunctionInterface.Call(Name, args, out var res)) + return default; + return res; + } + + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + foreach(var arg in Parameters) + arg.CollectReferences(references); + } + + protected override string Print() + { + return Name + "( (" + string.Join("), (", Parameters) + ") )"; + } + } + + internal class MemberAccessExpression : Expression + { + public override ExpressionType Type => ExpressionType.MemberAccess; + public Expression Target { get; } + public string Member { get; } + + public MemberAccessExpression(Expression target, string member) + { + Target = target; + Member = string.Intern(member); + } + + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Target.CollectReferences(references); + if (Target is ParameterExpression pe) + references.Add((pe.Name, Member)); + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Target is KeywordExpression ke + && ke.Keyword == ExpressionKeyword.Target) + { + return context.Target.GetProperty(Member); + } + + if (Target is ParameterExpression pe) + { + var obj = context.Parameters?.GetObjectParameter(pe.Name); + if (obj != null) + { + return obj.GetProperty(Member); + } + } + // Those are considered immutable + return Target.Evaluate(ref context).GetProperty(Member); + } + + protected override string Print() + { + return "(" + Target.ToString() + ")." + Member; + } + } + + internal class ParameterExpression : Expression + { + public string Name { get; } + public override ExpressionType Type => ExpressionType.Parameter; + + public ParameterExpression(string name) + { + Name = name; + } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + return context.Parameters?.GetParameter(Name) ?? default; + } + + protected override string Print() + { + return "{" + Name + "}"; + } + } + + internal class KeywordExpression : Expression + { + public override ExpressionType Type => ExpressionType.Keyword; + public ExpressionKeyword Keyword { get; } + + public KeywordExpression(ExpressionKeyword keyword) + { + Keyword = keyword; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Keyword == ExpressionKeyword.StartingValue) + return context.StartingValue; + if (Keyword == ExpressionKeyword.CurrentValue) + return context.CurrentValue; + if (Keyword == ExpressionKeyword.FinalValue) + return context.FinalValue; + if (Keyword == ExpressionKeyword.Target) + // should be handled by MemberAccess + return default; + if (Keyword == ExpressionKeyword.True) + return true; + if (Keyword == ExpressionKeyword.False) + return false; + if (Keyword == ExpressionKeyword.Pi) + return (float) Math.PI; + return default; + } + + protected override string Print() + { + return "[" + Keyword + "]"; + } + } + + internal class UnaryExpression : Expression + { + public Expression Parameter { get; } + public override ExpressionType Type { get; } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Type == ExpressionType.Not) + return !Parameter.Evaluate(ref context); + if (Type == ExpressionType.UnaryMinus) + return -Parameter.Evaluate(ref context); + return default; + } + + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Parameter.CollectReferences(references); + } + + protected override string Print() + { + return OperatorName(Type) + Parameter; + } + + public UnaryExpression(Expression parameter, ExpressionType type) + { + Parameter = parameter; + Type = type; + } + } + + internal class BinaryExpression : Expression + { + public Expression Left { get; } + public Expression Right { get; } + public override ExpressionType Type { get; } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + var left = Left.Evaluate(ref context); + var right = Right.Evaluate(ref context); + if (Type == ExpressionType.Add) + return left + right; + if (Type == ExpressionType.Subtract) + return left - right; + if (Type == ExpressionType.Multiply) + return left * right; + if (Type == ExpressionType.Divide) + return left / right; + if (Type == ExpressionType.Remainder) + return left % right; + if (Type == ExpressionType.MoreThan) + return left > right; + if (Type == ExpressionType.LessThan) + return left < right; + if (Type == ExpressionType.MoreThanOrEqual) + return left > right; + if (Type == ExpressionType.LessThanOrEqual) + return left < right; + if (Type == ExpressionType.LogicalAnd) + return left.And(right); + if (Type == ExpressionType.LogicalOr) + return left.Or(right); + if (Type == ExpressionType.Equals) + return left.EqualsTo(right); + if (Type == ExpressionType.NotEquals) + return left.NotEqualsTo(right); + return default; + } + + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Left.CollectReferences(references); + Right.CollectReferences(references); + } + + protected override string Print() + { + return "(" + Left + OperatorName(Type) + Right + ")"; + } + + public BinaryExpression(Expression left, Expression right, ExpressionType type) + { + Left = left; + Right = right; + Type = type; + } + } + + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs new file mode 100644 index 0000000000..9d23551e43 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal struct ExpressionEvaluationContext + { + public ExpressionVariant StartingValue { get; set; } + public ExpressionVariant CurrentValue { get; set; } + public ExpressionVariant FinalValue { get; set; } + public IExpressionObject Target { get; set; } + public IExpressionParameterCollection Parameters { get; set; } + public IExpressionForeignFunctionInterface ForeignFunctionInterface { get; set; } + } + + internal interface IExpressionObject + { + ExpressionVariant GetProperty(string name); + } + + internal interface IExpressionParameterCollection + { + public ExpressionVariant GetParameter(string name); + + public IExpressionObject GetObjectParameter(string name); + } + + internal interface IExpressionForeignFunctionInterface + { + bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs new file mode 100644 index 0000000000..6a207a3bf7 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class ExpressionParseException : Exception + { + public int Position { get; } + + public ExpressionParseException(string message, int position) : base(message) + { + Position = position; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs new file mode 100644 index 0000000000..5924bb8f1b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +// ReSharper disable StringLiteralTypo + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class ExpressionParser + { + public static Expression Parse(ReadOnlySpan s) + { + var p = new TokenParser(s); + var parsed = ParseTillTerminator(ref p, "", false, false, out _); + p.SkipWhitespace(); + if (p.Length != 0) + throw new ExpressionParseException("Unexpected data ", p.Position); + return parsed; + } + + private static ReadOnlySpan Dot => ".".AsSpan(); + static bool TryParseAtomic(ref TokenParser parser, + [MaybeNullWhen(returnValue: false)] out Expression expr) + { + // We can parse keywords, parameter names and constants + expr = null; + if (parser.TryParseKeywordLowerCase("this.startingvalue")) + expr = new KeywordExpression(ExpressionKeyword.StartingValue); + else if(parser.TryParseKeywordLowerCase("this.currentvalue")) + expr = new KeywordExpression(ExpressionKeyword.CurrentValue); + else if(parser.TryParseKeywordLowerCase("this.finalvalue")) + expr = new KeywordExpression(ExpressionKeyword.FinalValue); + else if(parser.TryParseKeywordLowerCase("pi")) + expr = new KeywordExpression(ExpressionKeyword.Pi); + else if(parser.TryParseKeywordLowerCase("true")) + expr = new KeywordExpression(ExpressionKeyword.True); + else if(parser.TryParseKeywordLowerCase("false")) + expr = new KeywordExpression(ExpressionKeyword.False); + else if (parser.TryParseKeywordLowerCase("this.target")) + expr = new KeywordExpression(ExpressionKeyword.Target); + + if (expr != null) + return true; + + if (parser.TryParseIdentifier(out var identifier)) + { + expr = new ParameterExpression(identifier.ToString()); + return true; + } + + if(parser.TryParseFloat(out var scalar)) + { + expr = new ConstantExpression(scalar); + return true; + } + + return false; + + } + + static bool TryParseOperator(ref TokenParser parser, out ExpressionType op) + { + op = (ExpressionType) (-1); + if (parser.TryConsume("||")) + op = ExpressionType.LogicalOr; + else if (parser.TryConsume("&&")) + op = ExpressionType.LogicalAnd; + else if (parser.TryConsume(">=")) + op = ExpressionType.MoreThanOrEqual; + else if (parser.TryConsume("<=")) + op = ExpressionType.LessThanOrEqual; + else if (parser.TryConsume("==")) + op = ExpressionType.Equals; + else if (parser.TryConsume("!=")) + op = ExpressionType.NotEquals; + else if (parser.TryConsumeAny("+-/*><%".AsSpan(), out var sop)) + { +#pragma warning disable CS8509 + op = sop switch +#pragma warning restore CS8509 + { + '+' => ExpressionType.Add, + '-' => ExpressionType.Subtract, + '/' => ExpressionType.Divide, + '*' => ExpressionType.Multiply, + '<' => ExpressionType.LessThan, + '>' => ExpressionType.MoreThan, + '%' => ExpressionType.Remainder + }; + } + else + return false; + + return true; + } + + + struct ExpressionOperatorGroup + { + private List _expressions; + private List _operators; + private Expression? _first; + + public bool NotEmpty => !Empty; + public bool Empty => _expressions == null && _first == null; + + public void AppendFirst(Expression expr) + { + if (NotEmpty) + throw new InvalidOperationException(); + _first = expr; + } + + public void AppendWithOperator(Expression expr, ExpressionType op) + { + if (_expressions == null) + { + if (_first == null) + throw new InvalidOperationException(); + _expressions = new List(); + _expressions.Add(_first); + _first = null; + _operators = new List(); + } + _expressions.Add(expr); + _operators.Add(op); + } + + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/ + private static readonly ExpressionType[][] OperatorPrecedenceGroups = new[] + { + // multiplicative + new[] {ExpressionType.Multiply, ExpressionType.Divide, ExpressionType.Remainder}, + // additive + new[] {ExpressionType.Add, ExpressionType.Subtract}, + // relational + new[] {ExpressionType.MoreThan, ExpressionType.MoreThanOrEqual, ExpressionType.LessThan, ExpressionType.LessThanOrEqual}, + // equality + new[] {ExpressionType.Equals, ExpressionType.NotEquals}, + // conditional AND + new[] {ExpressionType.LogicalAnd}, + // conditional OR + new[]{ ExpressionType.LogicalOr}, + }; + + private static readonly ExpressionType[][] OperatorPrecedenceGroupsReversed = + OperatorPrecedenceGroups.Reverse().ToArray(); + + // a*b+c [a,b,c] [*,+], call with (0, 2) + // ToExpression(a*b) + ToExpression(c) + // a+b*c -> ToExpression(a) + ToExpression(b*c) + Expression ToExpression(int from, int to) + { + if (to - from == 0) + return _expressions[from]; + + if (to - from == 1) + return new BinaryExpression(_expressions[from], _expressions[to], _operators[from]); + + foreach (var grp in OperatorPrecedenceGroupsReversed) + { + for (var c = from; c < to; c++) + { + var currentOperator = _operators[c]; + foreach(var operatorFromGroup in grp) + if (currentOperator == operatorFromGroup) + { + // We are dividing the expression right here + var left = ToExpression(from, c); + var right = ToExpression(c + 1, to); + return new BinaryExpression(left, right, currentOperator); + } + } + } + + // We shouldn't ever get here, if we are, there is something wrong in the code + throw new ExpressionParseException("Expression parsing algorithm bug in ToExpression", 0); + } + + public Expression ToExpression() + { + if (_expressions == null) + return _first ?? throw new InvalidOperationException(); + return ToExpression(0, _expressions.Count - 1); + } + } + + static Expression ParseTillTerminator(ref TokenParser parser, string terminatorChars, + bool throwOnTerminator, + bool throwOnEnd, + out char? token) + { + ExpressionOperatorGroup left = default; + token = null; + while (true) + { + if (parser.TryConsumeAny(terminatorChars.AsSpan(), out var consumedToken)) + { + if (throwOnTerminator || left.Empty) + throw new ExpressionParseException($"Unexpected '{token}'", parser.Position - 1); + token = consumedToken; + return left.ToExpression(); + } + parser.SkipWhitespace(); + if (parser.Length == 0) + { + if (throwOnEnd || left.Empty) + throw new ExpressionParseException("Unexpected end of expression", parser.Position); + return left.ToExpression(); + } + + ExpressionType? op = null; + if (left.NotEmpty) + { + if (parser.TryConsume('?')) + { + var truePart = ParseTillTerminator(ref parser, ":", + false, true, out _); + // pass through the current parsing rules to consume the rest + var falsePart = ParseTillTerminator(ref parser, terminatorChars, throwOnTerminator, throwOnEnd, + out token); + + return new ConditionalExpression(left.ToExpression(), truePart, falsePart); + } + + // We expect a binary operator here + if (!TryParseOperator(ref parser, out var sop)) + throw new ExpressionParseException("Unexpected token", parser.Position); + op = sop; + } + + // We expect an expression to be parsed (either due to expecting a binary operator or parsing the first part + var applyNegation = false; + while (parser.TryConsume('!')) + applyNegation = !applyNegation; + + var applyUnaryMinus = false; + while (parser.TryConsume('-')) + applyUnaryMinus = !applyUnaryMinus; + + Expression? parsed; + + if (parser.TryConsume('(')) + parsed = ParseTillTerminator(ref parser, ")", false, true, out _); + else if (parser.TryParseCall(out var functionName)) + { + var parameterList = new List(); + while (true) + { + parameterList.Add(ParseTillTerminator(ref parser, ",)", false, true, out var closingToken)); + if (closingToken == ')') + break; + if (closingToken != ',') + throw new ExpressionParseException("Unexpected end of the expression", parser.Position); + } + + parsed = new FunctionCallExpression(functionName.ToString(), parameterList); + } + else if (TryParseAtomic(ref parser, out parsed)) + { + // do nothing + } + else + throw new ExpressionParseException("Unexpected token", parser.Position); + + + // Parse any following member accesses + while (parser.TryConsume('.')) + { + if(!parser.TryParseIdentifier(out var memberName)) + throw new ExpressionParseException("Unexpected token", parser.Position); + + parsed = new MemberAccessExpression(parsed, memberName.ToString()); + } + + // Apply ! operator + if (applyNegation) + parsed = new UnaryExpression(parsed, ExpressionType.Not); + + if (applyUnaryMinus) + { + if(parsed is ConstantExpression constexpr) + parsed = new ConstantExpression(-constexpr.Constant); + else parsed = new UnaryExpression(parsed, ExpressionType.UnaryMinus); + } + + if (left.Empty) + left.AppendFirst(parsed); + else + left.AppendWithOperator(parsed, op!.Value); + } + + + + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs new file mode 100644 index 0000000000..334f975aa0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Expressions; + +internal class ExpressionTrackedObjects : IEnumerable +{ + private List _list = new(); + private HashSet _hashSet = new(); + + public void Add(IExpressionObject obj, string member) + { + if (_hashSet.Add(obj)) + _list.Add(obj); + } + + public void Clear() + { + _list.Clear(); + _hashSet.Clear(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_list).GetEnumerator(); + } + + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + + public struct Pool + { + private Stack _stack = new(); + + public Pool() + { + } + + public ExpressionTrackedObjects Get() + { + if (_stack.Count > 0) + return _stack.Pop(); + return new ExpressionTrackedObjects(); + } + + public void Return(ExpressionTrackedObjects obj) + { + _stack.Clear(); + _stack.Push(obj); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs new file mode 100644 index 0000000000..7b900534d8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -0,0 +1,730 @@ +using System; +using System.Globalization; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal enum VariantType + { + Invalid, + Boolean, + Scalar, + Double, + Vector2, + Vector3, + Vector4, + AvaloniaMatrix, + Matrix3x2, + Matrix4x4, + Quaternion, + Color + } + + /// + /// A VARIANT type used in expression animations. Can represent multiple value types + /// + [StructLayout(LayoutKind.Explicit)] + internal struct ExpressionVariant + { + [FieldOffset(0)] public VariantType Type; + + [FieldOffset(4)] public bool Boolean; + [FieldOffset(4)] public float Scalar; + [FieldOffset(4)] public double Double; + [FieldOffset(4)] public Vector2 Vector2; + [FieldOffset(4)] public Vector3 Vector3; + [FieldOffset(4)] public Vector4 Vector4; + [FieldOffset(4)] public Matrix AvaloniaMatrix; + [FieldOffset(4)] public Matrix3x2 Matrix3x2; + [FieldOffset(4)] public Matrix4x4 Matrix4x4; + [FieldOffset(4)] public Quaternion Quaternion; + [FieldOffset(4)] public Color Color; + + + public ExpressionVariant GetProperty(string property) + { + if (Type == VariantType.Vector2) + { + if (ReferenceEquals(property, "X")) + return Vector2.X; + if (ReferenceEquals(property, "Y")) + return Vector2.Y; + return default; + } + + if (Type == VariantType.Vector3) + { + if (ReferenceEquals(property, "X")) + return Vector3.X; + if (ReferenceEquals(property, "Y")) + return Vector3.Y; + if (ReferenceEquals(property, "Z")) + return Vector3.Z; + if(ReferenceEquals(property, "XY")) + return new Vector2(Vector3.X, Vector3.Y); + if(ReferenceEquals(property, "YX")) + return new Vector2(Vector3.Y, Vector3.X); + if(ReferenceEquals(property, "XZ")) + return new Vector2(Vector3.X, Vector3.Z); + if(ReferenceEquals(property, "ZX")) + return new Vector2(Vector3.Z, Vector3.X); + if(ReferenceEquals(property, "YZ")) + return new Vector2(Vector3.Y, Vector3.Z); + if(ReferenceEquals(property, "ZY")) + return new Vector2(Vector3.Z, Vector3.Y); + return default; + } + + if (Type == VariantType.Vector4) + { + if (ReferenceEquals(property, "X")) + return Vector4.X; + if (ReferenceEquals(property, "Y")) + return Vector4.Y; + if (ReferenceEquals(property, "Z")) + return Vector4.Z; + if (ReferenceEquals(property, "W")) + return Vector4.W; + return default; + } + + if (Type == VariantType.Matrix3x2) + { + if (ReferenceEquals(property, "M11")) + return Matrix3x2.M11; + if (ReferenceEquals(property, "M12")) + return Matrix3x2.M12; + if (ReferenceEquals(property, "M21")) + return Matrix3x2.M21; + if (ReferenceEquals(property, "M22")) + return Matrix3x2.M22; + if (ReferenceEquals(property, "M31")) + return Matrix3x2.M31; + if (ReferenceEquals(property, "M32")) + return Matrix3x2.M32; + return default; + } + + if (Type == VariantType.AvaloniaMatrix) + { + if (ReferenceEquals(property, "M11")) + return AvaloniaMatrix.M11; + if (ReferenceEquals(property, "M12")) + return AvaloniaMatrix.M12; + if (ReferenceEquals(property, "M21")) + return AvaloniaMatrix.M21; + if (ReferenceEquals(property, "M22")) + return AvaloniaMatrix.M22; + if (ReferenceEquals(property, "M31")) + return AvaloniaMatrix.M31; + if (ReferenceEquals(property, "M32")) + return AvaloniaMatrix.M32; + return default; + } + + if (Type == VariantType.Matrix4x4) + { + if (ReferenceEquals(property, "M11")) + return Matrix4x4.M11; + if (ReferenceEquals(property, "M12")) + return Matrix4x4.M12; + if (ReferenceEquals(property, "M13")) + return Matrix4x4.M13; + if (ReferenceEquals(property, "M14")) + return Matrix4x4.M14; + if (ReferenceEquals(property, "M21")) + return Matrix4x4.M21; + if (ReferenceEquals(property, "M22")) + return Matrix4x4.M22; + if (ReferenceEquals(property, "M23")) + return Matrix4x4.M23; + if (ReferenceEquals(property, "M24")) + return Matrix4x4.M24; + if (ReferenceEquals(property, "M31")) + return Matrix4x4.M31; + if (ReferenceEquals(property, "M32")) + return Matrix4x4.M32; + if (ReferenceEquals(property, "M33")) + return Matrix4x4.M33; + if (ReferenceEquals(property, "M34")) + return Matrix4x4.M34; + if (ReferenceEquals(property, "M41")) + return Matrix4x4.M41; + if (ReferenceEquals(property, "M42")) + return Matrix4x4.M42; + if (ReferenceEquals(property, "M43")) + return Matrix4x4.M43; + if (ReferenceEquals(property, "M44")) + return Matrix4x4.M44; + return default; + } + + if (Type == VariantType.Quaternion) + { + if (ReferenceEquals(property, "X")) + return Quaternion.X; + if (ReferenceEquals(property, "Y")) + return Quaternion.Y; + if (ReferenceEquals(property, "Z")) + return Quaternion.Z; + if (ReferenceEquals(property, "W")) + return Quaternion.W; + return default; + } + + if (Type == VariantType.Color) + { + if (ReferenceEquals(property, "A")) + return Color.A; + if (ReferenceEquals(property, "R")) + return Color.R; + if (ReferenceEquals(property, "G")) + return Color.G; + if (ReferenceEquals(property, "B")) + return Color.B; + return default; + } + + return default; + } + + public static implicit operator ExpressionVariant(bool value) => + new ExpressionVariant + { + Type = VariantType.Boolean, + Boolean = value + }; + + public static implicit operator ExpressionVariant(float scalar) => + new ExpressionVariant + { + Type = VariantType.Scalar, + Scalar = scalar + }; + + public static implicit operator ExpressionVariant(double d) => + new ExpressionVariant + { + Type = VariantType.Double, + Double = d + }; + + + public static implicit operator ExpressionVariant(Vector2 value) => + new ExpressionVariant + { + Type = VariantType.Vector2, + Vector2 = value + }; + + + public static implicit operator ExpressionVariant(Vector3 value) => + new ExpressionVariant + { + Type = VariantType.Vector3, + Vector3 = value + }; + + + public static implicit operator ExpressionVariant(Vector4 value) => + new ExpressionVariant + { + Type = VariantType.Vector4, + Vector4 = value + }; + + public static implicit operator ExpressionVariant(Matrix3x2 value) => + new ExpressionVariant + { + Type = VariantType.Matrix3x2, + Matrix3x2 = value + }; + + public static implicit operator ExpressionVariant(Matrix value) => + new ExpressionVariant + { + Type = VariantType.Matrix3x2, + AvaloniaMatrix = value + }; + + public static implicit operator ExpressionVariant(Matrix4x4 value) => + new ExpressionVariant + { + Type = VariantType.Matrix4x4, + Matrix4x4 = value + }; + + public static implicit operator ExpressionVariant(Quaternion value) => + new ExpressionVariant + { + Type = VariantType.Quaternion, + Quaternion = value + }; + + public static implicit operator ExpressionVariant(Avalonia.Media.Color value) => + new ExpressionVariant + { + Type = VariantType.Color, + Color = value + }; + + public static ExpressionVariant operator +(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type != right.Type || left.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar) + return left.Scalar + right.Scalar; + + if (left.Type == VariantType.Double) + return left.Double + right.Double; + + if (left.Type == VariantType.Vector2) + return left.Vector2 + right.Vector2; + + if (left.Type == VariantType.Vector3) + return left.Vector3 + right.Vector3; + + if (left.Type == VariantType.Vector4) + return left.Vector4 + right.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return left.Matrix3x2 + right.Matrix3x2; + + if (left.Type == VariantType.Matrix4x4) + return left.Matrix4x4 + right.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return left.Quaternion + right.Quaternion; + + return default; + } + + public static ExpressionVariant operator -(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type != right.Type || left.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar) + return left.Scalar - right.Scalar; + + if (left.Type == VariantType.Double) + return left.Double - right.Double; + + if (left.Type == VariantType.Vector2) + return left.Vector2 - right.Vector2; + + if (left.Type == VariantType.Vector3) + return left.Vector3 - right.Vector3; + + if (left.Type == VariantType.Vector4) + return left.Vector4 - right.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return left.Matrix3x2 - right.Matrix3x2; + + if (left.Type == VariantType.Matrix4x4) + return left.Matrix4x4 - right.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return left.Quaternion - right.Quaternion; + + return default; + } + + public static ExpressionVariant operator -(ExpressionVariant left) + { + + if (left.Type == VariantType.Scalar) + return -left.Scalar; + + if (left.Type == VariantType.Double) + return -left.Double; + + if (left.Type == VariantType.Vector2) + return -left.Vector2; + + if (left.Type == VariantType.Vector3) + return -left.Vector3; + + if (left.Type == VariantType.Vector4) + return -left.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return -left.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return -left.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return -left.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return -left.Quaternion; + + return default; + } + + public static ExpressionVariant operator *(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar * right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double * right.Double; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) + return left.Vector2 * right.Vector2; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) + return left.Vector2 * right.Scalar; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) + return left.Vector3 * right.Vector3; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) + return left.Vector3 * right.Scalar; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) + return left.Vector4 * right.Vector4; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) + return left.Vector4 * right.Scalar; + + if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Matrix3x2) + return left.Matrix3x2 * right.Matrix3x2; + + if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Scalar) + return left.Matrix3x2 * right.Scalar; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix * right.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4) + return left.Matrix4x4 * right.Matrix4x4; + + if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Scalar) + return left.Matrix4x4 * right.Scalar; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) + return left.Quaternion * right.Quaternion; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Scalar) + return left.Quaternion * right.Scalar; + + return default; + } + + public static ExpressionVariant operator /(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar / right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double / right.Double; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) + return left.Vector2 / right.Vector2; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) + return left.Vector2 / right.Scalar; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) + return left.Vector3 / right.Vector3; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) + return left.Vector3 / right.Scalar; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) + return left.Vector4 / right.Vector4; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) + return left.Vector4 / right.Scalar; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) + return left.Quaternion / right.Quaternion; + + return default; + } + + public ExpressionVariant EqualsTo(ExpressionVariant right) + { + if (Type != right.Type || Type == VariantType.Invalid) + return default; + + if (Type == VariantType.Scalar) + return Scalar == right.Scalar; + + + if (Type == VariantType.Double) + return Double == right.Double; + + if (Type == VariantType.Vector2) + return Vector2 == right.Vector2; + + if (Type == VariantType.Vector3) + return Vector3 == right.Vector3; + + if (Type == VariantType.Vector4) + return Vector4 == right.Vector4; + + if (Type == VariantType.Boolean) + return Boolean == right.Boolean; + + if (Type == VariantType.Matrix3x2) + return Matrix3x2 == right.Matrix3x2; + + if (Type == VariantType.AvaloniaMatrix) + return AvaloniaMatrix == right.AvaloniaMatrix; + + if (Type == VariantType.Matrix4x4) + return Matrix4x4 == right.Matrix4x4; + + if (Type == VariantType.Quaternion) + return Quaternion == right.Quaternion; + + return default; + } + + public ExpressionVariant NotEqualsTo(ExpressionVariant right) + { + var r = EqualsTo(right); + if (r.Type == VariantType.Boolean) + return !r.Boolean; + return default; + } + + public static ExpressionVariant operator !(ExpressionVariant v) + { + if (v.Type == VariantType.Boolean) + return !v.Boolean; + return default; + } + + public static ExpressionVariant operator %(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar % right.Scalar; + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double % right.Double; + return default; + } + + public static ExpressionVariant operator <(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar < right.Scalar; + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double < right.Double; + return default; + } + + public static ExpressionVariant operator >(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar > right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double > right.Double; + return default; + } + + public ExpressionVariant And(ExpressionVariant right) + { + if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) + return Boolean && right.Boolean; + return default; + } + + public ExpressionVariant Or(ExpressionVariant right) + { + if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) + return Boolean && right.Boolean; + return default; + } + + public bool TryCast(out T res) where T : struct + { + if (typeof(T) == typeof(bool)) + { + if (Type == VariantType.Boolean) + { + res = (T) (object) Boolean; + return true; + } + } + + if (typeof(T) == typeof(float)) + { + if (Type == VariantType.Scalar) + { + res = (T) (object) Scalar; + return true; + } + } + + if (typeof(T) == typeof(double)) + { + if (Type == VariantType.Double) + { + res = (T) (object) Double; + return true; + } + } + + if (typeof(T) == typeof(Vector2)) + { + if (Type == VariantType.Vector2) + { + res = (T) (object) Vector2; + return true; + } + } + + if (typeof(T) == typeof(Vector3)) + { + if (Type == VariantType.Vector3) + { + res = (T) (object) Vector3; + return true; + } + } + + if (typeof(T) == typeof(Vector4)) + { + if (Type == VariantType.Vector4) + { + res = (T) (object) Vector4; + return true; + } + } + + if (typeof(T) == typeof(Matrix3x2)) + { + if (Type == VariantType.Matrix3x2) + { + res = (T) (object) Matrix3x2; + return true; + } + } + + if (typeof(T) == typeof(Matrix)) + { + if (Type == VariantType.AvaloniaMatrix) + { + res = (T) (object) Matrix3x2; + return true; + } + } + + if (typeof(T) == typeof(Matrix4x4)) + { + if (Type == VariantType.Matrix4x4) + { + res = (T) (object) Matrix4x4; + return true; + } + } + + if (typeof(T) == typeof(Quaternion)) + { + if (Type == VariantType.Quaternion) + { + res = (T) (object) Quaternion; + return true; + } + } + + if (typeof(T) == typeof(Avalonia.Media.Color)) + { + if (Type == VariantType.Color) + { + res = (T) (object) Color; + return true; + } + } + + res = default(T); + return false; + } + + public static ExpressionVariant Create(T v) where T : struct + { + if (typeof(T) == typeof(bool)) + return (bool) (object) v; + + if (typeof(T) == typeof(float)) + return (float) (object) v; + + if (typeof(T) == typeof(Vector2)) + return (Vector2) (object) v; + + if (typeof(T) == typeof(Vector3)) + return (Vector3) (object) v; + + if (typeof(T) == typeof(Vector4)) + return (Vector4) (object) v; + + if (typeof(T) == typeof(Matrix3x2)) + return (Matrix3x2) (object) v; + + if (typeof(T) == typeof(Matrix)) + return (Matrix) (object) v; + + if (typeof(T) == typeof(Matrix4x4)) + return (Matrix4x4) (object) v; + + if (typeof(T) == typeof(Quaternion)) + return (Quaternion) (object) v; + + if (typeof(T) == typeof(Avalonia.Media.Color)) + return (Avalonia.Media.Color) (object) v; + + throw new ArgumentException("Invalid variant type: " + typeof(T)); + } + + public T CastOrDefault() where T : struct + { + TryCast(out var r); + return r; + } + + public override string ToString() + { + if (Type == VariantType.Boolean) + return Boolean.ToString(); + if (Type == VariantType.Scalar) + return Scalar.ToString(CultureInfo.InvariantCulture); + if (Type == VariantType.Double) + return Double.ToString(CultureInfo.InvariantCulture); + if (Type == VariantType.Vector2) + return Vector2.ToString(); + if (Type == VariantType.Vector3) + return Vector3.ToString(); + if (Type == VariantType.Vector4) + return Vector4.ToString(); + if (Type == VariantType.Quaternion) + return Quaternion.ToString(); + if (Type == VariantType.Matrix3x2) + return Matrix3x2.ToString(); + if (Type == VariantType.AvaloniaMatrix) + return AvaloniaMatrix.ToString(); + if (Type == VariantType.Matrix4x4) + return Matrix4x4.ToString(); + if (Type == VariantType.Color) + return Color.ToString(); + if (Type == VariantType.Invalid) + return "Invalid"; + return "Unknown"; + } + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs new file mode 100644 index 0000000000..27782c8c2c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs @@ -0,0 +1,259 @@ +using System; +using System.Globalization; + +namespace Avalonia.Rendering.Composition.Expressions +{ + /// + /// Helper class for composition expression parser + /// + internal ref struct TokenParser + { + private ReadOnlySpan _s; + public int Position { get; private set; } + public TokenParser(ReadOnlySpan s) + { + _s = s; + Position = 0; + } + + public void SkipWhitespace() + { + while (true) + { + if (_s.Length > 0 && char.IsWhiteSpace(_s[0])) + Advance(1); + else + return; + } + } + + static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z'); + + public bool TryConsume(char c) + { + SkipWhitespace(); + if (_s.Length == 0 || _s[0] != c) + return false; + + Advance(1); + return true; + } + public bool TryConsume(string s) + { + SkipWhitespace(); + if (_s.Length < s.Length) + return false; + for (var c = 0; c < s.Length; c++) + { + if (_s[c] != s[c]) + return false; + } + + Advance(s.Length); + return true; + } + + public bool TryConsumeAny(ReadOnlySpan chars, out char token) + { + SkipWhitespace(); + token = default; + if (_s.Length == 0) + return false; + + foreach (var c in chars) + { + if (c == _s[0]) + { + token = c; + Advance(1); + return true; + } + } + + return false; + } + + + public bool TryParseKeyword(string keyword) + { + SkipWhitespace(); + if (keyword.Length > _s.Length) + return false; + for(var c=0; c keyword.Length && IsAlphaNumeric(_s[keyword.Length])) + return false; + + Advance(keyword.Length); + return true; + } + + public bool TryParseKeywordLowerCase(string keywordInLowerCase) + { + SkipWhitespace(); + if (keywordInLowerCase.Length > _s.Length) + return false; + for(var c=0; c keywordInLowerCase.Length && IsAlphaNumeric(_s[keywordInLowerCase.Length])) + return false; + + Advance(keywordInLowerCase.Length); + return true; + } + + public void Advance(int c) + { + _s = _s.Slice(c); + Position += c; + } + + public int Length => _s.Length; + + public bool TryParseIdentifier(ReadOnlySpan extraValidChars, out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch)) + len++; + else + { + var found = false; + foreach(var vc in extraValidChars) + if (vc == ch) + { + found = true; + break; + } + + if (found) + len++; + else + break; + } + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public bool TryParseIdentifier(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch)) + len++; + else + break; + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public bool TryParseCall(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch<= 'Z') || ch == '.') + len++; + else + break; + } + + res = _s.Slice(0, len); + + // Find '(' + for (var c = len; c < _s.Length; c++) + { + if(char.IsWhiteSpace(_s[c])) + continue; + if(_s[c]=='(') + { + Advance(c + 1); + return true; + } + + return false; + + } + + return false; + + } + + + public bool TryParseFloat(out float res) + { + res = 0; + SkipWhitespace(); + if (_s.Length == 0) + return false; + + var len = 0; + var dotCount = 0; + for (var c = 0; c < _s.Length; c++) + { + var ch = _s[c]; + if (ch >= '0' && ch <= '9') + len = c + 1; + else if (ch == '.' && dotCount == 0) + { + len = c + 1; + dotCount++; + } + else + break; + } + + var span = _s.Slice(0, len); + +#if NETSTANDARD2_0 + if (!float.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#else + if (!float.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#endif + Advance(len); + return true; + } + + public override string ToString() => _s.ToString(); + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs new file mode 100644 index 0000000000..2cb500cae4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -0,0 +1,66 @@ +using System.Numerics; + +namespace Avalonia.Rendering.Composition +{ + static class MatrixUtils + { + public static Matrix4x4 ComputeTransform(Vector2 size, Vector2 anchorPoint, Vector3 centerPoint, + Matrix4x4 transformMatrix, Vector3 scale, float rotationAngle, Quaternion orientation, Vector3 offset) + { + // The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work + + var anchor = size * anchorPoint; + var mat = Matrix4x4.CreateTranslation(-anchor.X, -anchor.Y, 0); + + var center = new Vector3(centerPoint.X, centerPoint.Y, centerPoint.Z); + + if (!transformMatrix.IsIdentity) + mat = transformMatrix * mat; + + + if (scale != new Vector3(1, 1, 1)) + mat *= Matrix4x4.CreateScale(scale, center); + + //TODO: RotationAxis support + if (rotationAngle != 0) + mat *= Matrix4x4.CreateRotationZ(rotationAngle, center); + + if (orientation != Quaternion.Identity) + { + if (centerPoint != default) + { + mat *= Matrix4x4.CreateTranslation(-center) + * Matrix4x4.CreateFromQuaternion(orientation) + * Matrix4x4.CreateTranslation(center); + } + else + mat *= Matrix4x4.CreateFromQuaternion(orientation); + } + + if (offset != default) + mat *= Matrix4x4.CreateTranslation(offset); + + return mat; + } + + public static Matrix4x4 ToMatrix4x4(Matrix matrix) => + new Matrix4x4( + (float)matrix.M11, (float)matrix.M12, 0, (float)matrix.M13, + (float)matrix.M21, (float)matrix.M22, 0, (float)matrix.M23, + 0, 0, 1, 0, + (float)matrix.M31, (float)matrix.M32, 0, (float)matrix.M33 + ); + + public static Matrix ToMatrix(Matrix4x4 matrix44) => + new Matrix( + matrix44.M11, + matrix44.M12, + matrix44.M14, + matrix44.M21, + matrix44.M22, + matrix44.M24, + matrix44.M41, + matrix44.M42, + matrix44.M44); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs b/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs new file mode 100644 index 0000000000..282c0e113d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; + +namespace Avalonia.Rendering.Composition.Server; + +internal class CompositionProperty +{ + private static volatile int s_NextId = 1; + public int Id { get; private set; } + + public static CompositionProperty Register() => new() + { + Id = Interlocked.Increment(ref s_NextId) + }; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs new file mode 100644 index 0000000000..e261507f60 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -0,0 +1,179 @@ +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +/// +/// A bunch of hacks to make the existing rendering operations and IDrawingContext +/// to work with composition rendering infrastructure. +/// 1) Keeps and applies the transform of the current visual since drawing operations think that +/// they have information about the full render transform (they are not) +/// 2) Keeps the draw list for the VisualBrush contents of the current drawing operation. +/// +internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport +{ + private IDrawingContextImpl _impl; + private readonly VisualBrushRenderer _visualBrushRenderer; + + public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer) + { + _impl = impl; + _visualBrushRenderer = visualBrushRenderer; + } + + // This is a hack to make it work with the current way of handling visual brushes + public CompositionDrawList? VisualBrushDrawList + { + get => _visualBrushRenderer.VisualBrushDrawList; + set => _visualBrushRenderer.VisualBrushDrawList = value; + } + + public Matrix PostTransform { get; set; } = Matrix.Identity; + + public void Dispose() + { + _impl.Dispose(); + } + + Matrix _transform; + public Matrix Transform + { + get => _transform; + set => _impl.Transform = (_transform = value) * PostTransform; + } + + public void Clear(Color color) + { + _impl.Clear(color); + } + + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + } + + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + _impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); + } + + public void DrawLine(IPen pen, Point p1, Point p2) + { + _impl.DrawLine(pen, p1, p2); + } + + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + _impl.DrawGeometry(brush, pen, geometry); + } + + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) + { + _impl.DrawRectangle(brush, pen, rect, boxShadows); + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + _impl.DrawEllipse(brush, pen, rect); + } + + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + _impl.DrawGlyphRun(foreground, glyphRun); + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + return _impl.CreateLayer(size); + } + + public void PushClip(Rect clip) + { + _impl.PushClip(clip); + } + + public void PushClip(RoundedRect clip) + { + _impl.PushClip(clip); + } + + public void PopClip() + { + _impl.PopClip(); + } + + public void PushOpacity(double opacity) + { + _impl.PushOpacity(opacity); + } + + public void PopOpacity() + { + _impl.PopOpacity(); + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + _impl.PushOpacityMask(mask, bounds); + } + + public void PopOpacityMask() + { + _impl.PopOpacityMask(); + } + + public void PushGeometryClip(IGeometryImpl clip) + { + _impl.PushGeometryClip(clip); + } + + public void PopGeometryClip() + { + _impl.PopGeometryClip(); + } + + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + _impl.PushBitmapBlendMode(blendingMode); + } + + public void PopBitmapBlendMode() + { + _impl.PopBitmapBlendMode(); + } + + public void Custom(ICustomDrawOperation custom) + { + _impl.Custom(custom); + } + + public class VisualBrushRenderer : IVisualBrushRenderer + { + public CompositionDrawList? VisualBrushDrawList { get; set; } + public Size GetRenderTargetSize(IVisualBrush brush) + { + return VisualBrushDrawList?.Size ?? Size.Empty; + } + + public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) + { + if (VisualBrushDrawList != null) + { + foreach (var cmd in VisualBrushDrawList) + cmd.Item.Render(context); + } + } + } + + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic) + acrylic.DrawRectangle(material, rect); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs new file mode 100644 index 0000000000..7585710540 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -0,0 +1,76 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +/// +/// An FPS counter helper that can draw itself on the render thread +/// +internal class FpsCounter +{ + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private int _framesThisSecond; + private int _totalFrames; + private int _fps; + private TimeSpan _lastFpsUpdate; + const int FirstChar = 32; + const int LastChar = 126; + // ASCII chars + private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; + + public FpsCounter(GlyphTypeface typeface) + { + for (var c = FirstChar; c <= LastChar; c++) + { + var s = new string((char)c, 1); + var glyph = typeface.GetGlyph((uint)(s[0])); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + } + } + + public void FpsTick() => _framesThisSecond++; + + public void RenderFps(IDrawingContextImpl context, string aux) + { + var now = _stopwatch.Elapsed; + var elapsed = now - _lastFpsUpdate; + + ++_framesThisSecond; + ++_totalFrames; + + if (elapsed.TotalSeconds > 1) + { + _fps = (int)(_framesThisSecond / elapsed.TotalSeconds); + _framesThisSecond = 0; + _lastFpsUpdate = now; + } + + var fpsLine = $"Frame #{_totalFrames:00000000} FPS: {_fps:000} " + aux; + double width = 0; + double height = 0; + foreach (var ch in fpsLine) + { + var run = _runs[ch - FirstChar]; + width += run.Size.Width; + height = Math.Max(height, run.Size.Height); + } + + var rect = new Rect(0, 0, width + 3, height + 3); + + context.DrawRectangle(Brushes.Black, null, rect); + + double offset = 0; + foreach (var ch in fpsLine) + { + var run = _runs[ch - FirstChar]; + context.Transform = Matrix.CreateTranslation(offset, 0); + context.DrawGlyphRun(Brushes.White, run); + offset += run.Size.Width; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs new file mode 100644 index 0000000000..c9592b70ab --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -0,0 +1,46 @@ +namespace Avalonia.Rendering.Composition.Server +{ + /// + /// A helper class used to manage the current slots for writing data from the render thread + /// and reading it from the UI thread. + /// Used mostly by hit-testing which needs to know the last transform of the visual + /// + internal class ReadbackIndices + { + private readonly object _lock = new object(); + public int ReadIndex { get; private set; } = 0; + public int WriteIndex { get; private set; } = 1; + public int WrittenIndex { get; private set; } = 0; + public ulong ReadRevision { get; private set; } + public ulong LastWrittenRevision { get; private set; } + + public void NextRead() + { + lock (_lock) + { + if (ReadRevision < LastWrittenRevision) + { + ReadIndex = WrittenIndex; + ReadRevision = LastWrittenRevision; + } + } + } + + public void CompleteWrite(ulong writtenRevision) + { + lock (_lock) + { + for (var c = 0; c < 3; c++) + { + if (c != WriteIndex && c != ReadIndex) + { + WrittenIndex = WriteIndex; + LastWrittenRevision = writtenRevision; + WriteIndex = c; + return; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs new file mode 100644 index 0000000000..f7152293cc --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs @@ -0,0 +1,44 @@ +using System.Numerics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + /// + /// Server-side counterpart of . + /// Mostly propagates update and render calls, but is also responsible + /// for updating adorners in deferred manner + /// + internal partial class ServerCompositionContainerVisual : ServerCompositionVisual + { + public ServerCompositionVisualCollection Children { get; private set; } = null!; + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + { + base.RenderCore(canvas, currentTransformedClip); + + foreach (var ch in Children) + { + ch.Render(canvas, currentTransformedClip); + } + } + + public override void Update(ServerCompositionTarget root) + { + base.Update(root); + foreach (var child in Children) + { + if (child.AdornedVisual != null) + root.EnqueueAdornerUpdate(child); + else + child.Update(root); + } + + IsDirtyComposition = false; + } + + partial void Initialize() + { + Children = new ServerCompositionVisualCollection(Compositor); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs new file mode 100644 index 0000000000..93a5226f83 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -0,0 +1,75 @@ +using System; +using System.Numerics; +using Avalonia.Collections.Pooled; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +/// +/// Server-side counterpart of +/// +internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual +{ +#if DEBUG + // This is needed for debugging purposes so we could see inspect the associated visual from debugger + public readonly Visual UiVisual; +#endif + private CompositionDrawList? _renderCommands; + + public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor) + { +#if DEBUG + UiVisual = v; +#endif + } + + Rect? _contentBounds; + + public override Rect OwnContentBounds + { + get + { + if (_contentBounds == null) + { + var rect = Rect.Empty; + if(_renderCommands!=null) + foreach (var cmd in _renderCommands) + rect = rect.Union(cmd.Item.Bounds); + _contentBounds = rect; + } + + return _contentBounds.Value; + } + } + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + { + if (reader.Read() == 1) + { + _renderCommands?.Dispose(); + _renderCommands = reader.ReadObject(); + _contentBounds = null; + } + base.DeserializeChangesCore(reader, commitedAt); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + { + if (_renderCommands != null) + { + _renderCommands.Render(canvas); + } + base.RenderCore(canvas, currentTransformedClip); + } + +#if DEBUG + public override string ToString() + { + return UiVisual.GetType().ToString(); + } +#endif +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs new file mode 100644 index 0000000000..462a193a86 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract class ServerCompositionSurface : ServerObject + { + protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs new file mode 100644 index 0000000000..0fde86e484 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Threading; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server +{ + /// + /// Server-side counterpart of the + /// That's the place where we update visual transforms, track dirty rects and actually do rendering + /// + internal partial class ServerCompositionTarget : IDisposable + { + private readonly ServerCompositor _compositor; + private readonly Func _renderTargetFactory; + private static long s_nextId = 1; + public long Id { get; } + public ulong Revision { get; private set; } + private IRenderTarget? _renderTarget; + private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); + private Rect _dirtyRect; + private Random _random = new(); + private Size _layerSize; + private IDrawingContextLayerImpl? _layer; + private bool _redrawRequested; + private bool _disposed; + private HashSet _attachedVisuals = new(); + private Queue _adornerUpdateQueue = new(); + + + public ReadbackIndices Readback { get; } = new(); + public int RenderedVisuals { get; set; } + + public ServerCompositionTarget(ServerCompositor compositor, Func renderTargetFactory) : + base(compositor) + { + _compositor = compositor; + _renderTargetFactory = renderTargetFactory; + Id = Interlocked.Increment(ref s_nextId); + } + + partial void OnIsEnabledChanged() + { + if (IsEnabled) + { + _compositor.AddCompositionTarget(this); + foreach (var v in _attachedVisuals) + v.Activate(); + } + else + { + _compositor.RemoveCompositionTarget(this); + foreach (var v in _attachedVisuals) + v.Deactivate(); + } + } + + partial void DeserializeChangesExtra(BatchStreamReader c) + { + _redrawRequested = true; + } + + public void Render() + { + if (_disposed) + { + Compositor.RemoveCompositionTarget(this); + return; + } + + if (Root == null) + return; + _renderTarget ??= _renderTargetFactory(); + + Compositor.UpdateServerTime(); + + if(_dirtyRect.IsEmpty && !_redrawRequested) + return; + + Revision++; + + // Update happens in a separate phase to extend dirty rect if needed + Root.Update(this); + + while (_adornerUpdateQueue.Count > 0) + { + var adorner = _adornerUpdateQueue.Dequeue(); + adorner.Update(this); + } + + Readback.CompleteWrite(Revision); + + _redrawRequested = false; + using (var targetContext = _renderTarget.CreateDrawingContext(null)) + { + var layerSize = Size * Scaling; + if (layerSize != _layerSize || _layer == null) + { + _layer?.Dispose(); + _layer = null; + _layer = targetContext.CreateLayer(Size); + _layerSize = layerSize; + } + + if (!_dirtyRect.IsEmpty) + { + var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); + using (var context = _layer.CreateDrawingContext(visualBrushHelper)) + { + context.PushClip(_dirtyRect); + context.Clear(Colors.Transparent); + Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect); + context.PopClip(); + } + } + + targetContext.Clear(Colors.Transparent); + targetContext.Transform = Matrix.Identity; + if (_layer.CanBlit) + _layer.Blit(targetContext); + else + targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, + new Rect(_layerSize), + new Rect(Size), BitmapInterpolationMode.LowQuality); + + + if (DrawDirtyRects) + { + targetContext.DrawRectangle(new ImmutableSolidColorBrush( + new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), + (byte)_random.Next(255))) + , null, _dirtyRect); + } + + if (DrawFps) + { + var nativeMem = ByteSizeHelper.ToString((ulong)( + (Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) * + Compositor.BatchMemoryPool.BufferSize), false); + var managedMem = ByteSizeHelper.ToString((ulong)( + (Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) * + Compositor.BatchObjectPool.ArraySize * + IntPtr.Size), false); + _fpsCounter.RenderFps(targetContext, $"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}"); + } + RenderedVisuals = 0; + + _dirtyRect = Rect.Empty; + } + } + + public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling); + + private static Rect SnapToDevicePixels(Rect rect, double scale) + { + return new Rect( + new Point( + Math.Floor(rect.X * scale) / scale, + Math.Floor(rect.Y * scale) / scale), + new Point( + Math.Ceiling(rect.Right * scale) / scale, + Math.Ceiling(rect.Bottom * scale) / scale)); + } + + public void AddDirtyRect(Rect rect) + { + if(rect.IsEmpty) + return; + var snapped = SnapToDevicePixels(rect, Scaling); + _dirtyRect = _dirtyRect.Union(snapped); + _redrawRequested = true; + } + + public void Invalidate() + { + _redrawRequested = true; + } + + public void Dispose() + { + if(_disposed) + return; + _disposed = true; + using (_compositor.GpuContext?.EnsureCurrent()) + { + if (_layer != null) + { + _layer.Dispose(); + _layer = null; + } + + _renderTarget?.Dispose(); + _renderTarget = null; + } + _compositor.RemoveCompositionTarget(this); + } + + public void AddVisual(ServerCompositionVisual visual) + { + if (_attachedVisuals.Add(visual) && IsEnabled) + visual.Activate(); + } + + public void RemoveVisual(ServerCompositionVisual visual) + { + if (_attachedVisuals.Remove(visual) && IsEnabled) + visual.Deactivate(); + if(visual.IsVisibleInFrame) + AddDirtyRect(visual.TransformedOwnContentBounds); + } + + public void EnqueueAdornerUpdate(ServerCompositionVisual visual) => _adornerUpdateQueue.Enqueue(visual); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs new file mode 100644 index 0000000000..c5af74e2dd --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs @@ -0,0 +1,76 @@ +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositionVisual +{ + protected bool IsDirtyComposition; + private bool _combinedTransformDirty; + private bool _clipSizeDirty; + + private const CompositionVisualChangedFields CompositionFieldsMask + = CompositionVisualChangedFields.Opacity + | CompositionVisualChangedFields.OpacityAnimated + | CompositionVisualChangedFields.OpacityMaskBrush + | CompositionVisualChangedFields.Clip + | CompositionVisualChangedFields.ClipToBounds + | CompositionVisualChangedFields.ClipToBoundsAnimated + | CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated; + + private const CompositionVisualChangedFields CombinedTransformFieldsMask = + CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated + | CompositionVisualChangedFields.AnchorPoint + | CompositionVisualChangedFields.AnchorPointAnimated + | CompositionVisualChangedFields.CenterPoint + | CompositionVisualChangedFields.CenterPointAnimated + | CompositionVisualChangedFields.AdornedVisual + | CompositionVisualChangedFields.TransformMatrix + | CompositionVisualChangedFields.Scale + | CompositionVisualChangedFields.ScaleAnimated + | CompositionVisualChangedFields.RotationAngle + | CompositionVisualChangedFields.RotationAngleAnimated + | CompositionVisualChangedFields.Orientation + | CompositionVisualChangedFields.OrientationAnimated + | CompositionVisualChangedFields.Offset + | CompositionVisualChangedFields.OffsetAnimated; + + private const CompositionVisualChangedFields ClipSizeDirtyMask = + CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated + | CompositionVisualChangedFields.ClipToBounds + | CompositionVisualChangedFields.ClipToBoundsAnimated; + + partial void OnFieldsDeserialized(CompositionVisualChangedFields changed) + { + if ((changed & CompositionFieldsMask) != 0) + IsDirtyComposition = true; + if ((changed & CombinedTransformFieldsMask) != 0) + _combinedTransformDirty = true; + if ((changed & ClipSizeDirtyMask) != 0) + _clipSizeDirty = true; + } + + public override void NotifyAnimatedValueChanged(CompositionProperty offset) + { + base.NotifyAnimatedValueChanged(offset); + if (offset == s_IdOfClipToBoundsProperty + || offset == s_IdOfOpacityProperty + || offset == s_IdOfSizeProperty) + IsDirtyComposition = true; + + if (offset == s_IdOfSizeProperty + || offset == s_IdOfAnchorPointProperty + || offset == s_IdOfCenterPointProperty + || offset == s_IdOfAdornedVisualProperty + || offset == s_IdOfTransformMatrixProperty + || offset == s_IdOfScaleProperty + || offset == s_IdOfRotationAngleProperty + || offset == s_IdOfOrientationProperty + || offset == s_IdOfOffsetProperty) + _combinedTransformDirty = true; + + if (offset == s_IdOfClipToBoundsProperty + || offset == s_IdOfSizeProperty) + _clipSizeDirty = true; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs new file mode 100644 index 0000000000..6fdf105e58 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -0,0 +1,246 @@ +using System; +using System.Numerics; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server +{ + /// + /// Server-side counterpart. + /// Is responsible for computing the transformation matrix, for applying various visual + /// properties before calling visual-specific drawing code and for notifying the + /// for new dirty rects + /// + partial class ServerCompositionVisual : ServerObject + { + private bool _isDirtyForUpdate; + private Rect _oldOwnContentBounds; + private bool _isBackface; + private Rect? _transformedClipBounds; + private Rect _combinedTransformedClipBounds; + + protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + { + + } + + public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + { + if(Visible == false || IsVisibleInFrame == false) + return; + if(Opacity == 0) + return; + + currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds); + if(currentTransformedClip.IsEmpty) + return; + + Root!.RenderedVisuals++; + + var transform = GlobalTransformMatrix; + canvas.PostTransform = MatrixUtils.ToMatrix(transform); + canvas.Transform = Matrix.Identity; + if (Opacity != 1) + canvas.PushOpacity(Opacity); + var boundsRect = new Rect(new Size(Size.X, Size.Y)); + if(ClipToBounds) + canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); + if (Clip != null) + canvas.PushGeometryClip(Clip); + if(OpacityMaskBrush != null) + canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); + + RenderCore(canvas, currentTransformedClip); + + // Hack to force invalidation of SKMatrix + canvas.PostTransform = MatrixUtils.ToMatrix(transform); + canvas.Transform = Matrix.Identity; + + if (OpacityMaskBrush != null) + canvas.PopOpacityMask(); + if (Clip != null) + canvas.PopGeometryClip(); + if (ClipToBounds) + canvas.PopClip(); + if(Opacity != 1) + canvas.PopOpacity(); + } + + private ReadbackData _readback0, _readback1, _readback2; + + /// + /// Obtains "readback" data - the data that is sent from the render thread to the UI thread + /// in non-blocking manner. Used mostly by hit-testing + /// + public ref ReadbackData GetReadback(int idx) + { + if (idx == 0) + return ref _readback0; + if (idx == 1) + return ref _readback1; + return ref _readback2; + } + + public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity; + public Matrix4x4 GlobalTransformMatrix { get; private set; } + + public virtual void Update(ServerCompositionTarget root) + { + if(Parent == null && Root == null) + return; + + var wasVisible = IsVisibleInFrame; + + // Calculate new parent-relative transform + if (_combinedTransformDirty) + { + CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, + // HACK: Ignore RenderTransform set by the adorner layer + AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + _combinedTransformDirty = false; + } + + var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity; + + var newTransform = CombinedTransformMatrix * parentTransform; + + // Check if visual was moved and recalculate face orientation + var positionChanged = false; + if (GlobalTransformMatrix != newTransform) + { + _isBackface = Vector3.Transform( + new Vector3(0, 0, float.PositiveInfinity), GlobalTransformMatrix).Z <= 0; + positionChanged = true; + } + + var oldTransformedContentBounds = TransformedOwnContentBounds; + var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds; + + var dirtyOldBounds = false; + if (_parent?.IsDirtyComposition == true) + { + IsDirtyComposition = true; + _isDirtyForUpdate = true; + dirtyOldBounds = true; + } + + GlobalTransformMatrix = newTransform; + + var ownBounds = OwnContentBounds; + if (ownBounds != _oldOwnContentBounds || positionChanged) + { + _oldOwnContentBounds = ownBounds; + if (ownBounds.IsEmpty) + TransformedOwnContentBounds = default; + else + TransformedOwnContentBounds = + ownBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); + } + + if (_clipSizeDirty || positionChanged) + { + _transformedClipBounds = ClipToBounds + ? new Rect(new Size(Size.X, Size.Y)) + .TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)) + : null; + + _clipSizeDirty = false; + } + + _combinedTransformedClipBounds = Parent?._combinedTransformedClipBounds ?? new Rect(Root!.Size); + if (_transformedClipBounds != null) + _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value); + + EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); + + IsVisibleInFrame = Visible && EffectiveOpacity > 0.04 && !_isBackface && + !_combinedTransformedClipBounds.IsEmpty; + + if (wasVisible != IsVisibleInFrame) + _isDirtyForUpdate = true; + + // Invalidate previous rect and queue new rect based on visibility + if (positionChanged) + { + if (wasVisible) + dirtyOldBounds = true; + + if (IsVisibleInFrame) + _isDirtyForUpdate = true; + } + + // Invalidate new bounds + if (IsVisibleInFrame && _isDirtyForUpdate) + { + dirtyOldBounds = true; + AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds)); + } + + if (dirtyOldBounds && wasVisible) + AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds)); + + + _isDirtyForUpdate = false; + + // Update readback indices + var i = Root!.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = root.Revision; + readback.Matrix = CombinedTransformMatrix; + readback.TargetId = Root.Id; + readback.Visible = IsVisibleInFrame; + } + + void AddDirtyRect(Rect rc) + { + if(rc == Rect.Empty) + return; + Root?.AddDirtyRect(rc); + } + + /// + /// Data that can be read from the UI thread + /// + public struct ReadbackData + { + public Matrix4x4 Matrix; + public ulong Revision; + public long TargetId; + public bool Visible; + } + + partial void DeserializeChangesExtra(BatchStreamReader c) + { + ValuesInvalidated(); + } + + partial void OnRootChanging() + { + if (Root != null) + Root.RemoveVisual(this); + } + + partial void OnRootChanged() + { + if (Root != null) + Root.AddVisual(this); + } + + protected override void ValuesInvalidated() + { + _isDirtyForUpdate = true; + Root?.Invalidate(); + } + + public bool IsVisibleInFrame { get; set; } + public double EffectiveOpacity { get; set; } + public Rect TransformedOwnContentBounds { get; set; } + public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y); + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs new file mode 100644 index 0000000000..564f792ebe --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + /// + /// Server-side counterpart of the . + /// 1) manages deserialization of changes received from the UI thread + /// 2) triggers animation ticks + /// 3) asks composition targets to render themselves + /// + internal class ServerCompositor : IRenderLoopTask + { + private readonly IRenderLoop _renderLoop; + private readonly Queue _batches = new Queue(); + public long LastBatchId { get; private set; } + public Stopwatch Clock { get; } = Stopwatch.StartNew(); + public TimeSpan ServerNow { get; private set; } + private List _activeTargets = new(); + private HashSet _activeAnimations = new(); + private List _animationsToUpdate = new(); + internal BatchStreamObjectPool BatchObjectPool; + internal BatchStreamMemoryPool BatchMemoryPool; + private object _lock = new object(); + public IPlatformGpuContext? GpuContext { get; } + + public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu, + BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) + { + GpuContext = platformGpu?.PrimaryContext; + _renderLoop = renderLoop; + BatchObjectPool = batchObjectPool; + BatchMemoryPool = batchMemoryPool; + _renderLoop.Add(this); + } + + public void EnqueueBatch(Batch batch) + { + lock (_batches) + _batches.Enqueue(batch); + } + + internal void UpdateServerTime() => ServerNow = Clock.Elapsed; + + List _reusableToCompleteList = new(); + void ApplyPendingBatches() + { + while (true) + { + Batch batch; + lock (_batches) + { + if(_batches.Count == 0) + break; + batch = _batches.Dequeue(); + } + + using (var stream = new BatchStreamReader(batch.Changes, BatchMemoryPool, BatchObjectPool)) + { + while (!stream.IsObjectEof) + { + var target = (ServerObject)stream.ReadObject()!; + target.DeserializeChanges(stream, batch); +#if DEBUG_COMPOSITOR_SERIALIZATION + if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) + throw new InvalidOperationException( + $"Object {target.GetType()} failed to deserialize properly on object stream"); + if(stream.Read() != BatchStreamDebugMarkers.ObjectEndMagic) + throw new InvalidOperationException( + $"Object {target.GetType()} failed to deserialize properly on data stream"); +#endif + } + } + + _reusableToCompleteList.Add(batch); + LastBatchId = batch.SequenceId; + } + } + + void CompletePendingBatches() + { + foreach(var batch in _reusableToCompleteList) + batch.Complete(); + _reusableToCompleteList.Clear(); + } + + bool IRenderLoopTask.NeedsUpdate => false; + + void IRenderLoopTask.Update(TimeSpan time) + { + } + + public void Render() + { + lock (_lock) + { + RenderCore(); + } + } + + private void RenderCore() + { + ApplyPendingBatches(); + + foreach(var animation in _activeAnimations) + _animationsToUpdate.Add(animation); + + foreach(var animation in _animationsToUpdate) + animation.Invalidate(); + + _animationsToUpdate.Clear(); + + foreach (var t in _activeTargets) + t.Render(); + + CompletePendingBatches(); + } + + public void AddCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Add(target); + } + + public void RemoveCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Remove(target); + } + + public void AddToClock(IAnimationInstance animationInstance) => + _activeAnimations.Add(animationInstance); + + public void RemoveFromClock(IAnimationInstance animationInstance) => + _activeAnimations.Remove(animationInstance); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs new file mode 100644 index 0000000000..39d6a8dc70 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + /// + /// A server-side list container capable of receiving changes from the UI thread + /// Right now it's quite dumb since it always receives the full list + /// + class ServerList : ServerObject where T : ServerObject + { + public List List { get; } = new List(); + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + { + if (reader.Read() == 1) + { + List.Clear(); + var count = reader.Read(); + for (var c = 0; c < count; c++) + List.Add(reader.ReadObject()); + } + base.DeserializeChangesCore(reader, commitedAt); + } + + public override long LastChangedBy + { + get + { + var seq = base.LastChangedBy; + foreach (var i in List) + seq = Math.Max(i.LastChangedBy, seq); + return seq; + } + } + + public List.Enumerator GetEnumerator() => List.GetEnumerator(); + + public ServerList(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs new file mode 100644 index 0000000000..93ea8e8dee --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server +{ + /// + /// Server-side counterpart. + /// Is responsible for animation activation and invalidation + /// + internal abstract class ServerObject : IExpressionObject + { + public ServerCompositor Compositor { get; } + + public virtual long LastChangedBy => ItselfLastChangedBy; + public long ItselfLastChangedBy { get; private set; } + private uint _activationCount; + public bool IsActive => _activationCount != 0; + private InlineDictionary _subscriptions; + private InlineDictionary _animations; + + private class ServerObjectSubscriptionStore + { + public bool IsValid; + public RefTrackingDictionary? Subscribers; + + public void Invalidate() + { + if (IsValid) + return; + IsValid = false; + if (Subscribers != null) + foreach (var sub in Subscribers) + sub.Key.Invalidate(); + } + } + + public ServerObject(ServerCompositor compositor) + { + Compositor = compositor; + } + + public virtual ExpressionVariant GetPropertyForAnimation(string name) + { + return default; + } + + ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + + public void Activate() + { + _activationCount++; + if (_activationCount == 1) + Activated(); + } + + public void Deactivate() + { +#if DEBUG + if (_activationCount == 0) + throw new InvalidOperationException(); +#endif + _activationCount--; + if (_activationCount == 0) + Deactivated(); + } + + protected void Activated() + { + foreach(var kp in _animations) + kp.Value.Activate(); + } + + protected void Deactivated() + { + foreach(var kp in _animations) + kp.Value.Deactivate(); + } + + void InvalidateSubscriptions(CompositionProperty property) + { + if(_subscriptions.TryGetValue(property, out var subs)) + subs.Invalidate(); + } + + protected void SetValue(CompositionProperty prop, out T field, T value) + { + field = value; + InvalidateSubscriptions(prop); + } + + protected T GetValue(CompositionProperty prop, ref T field) + { + if (_subscriptions.TryGetValue(prop, out var subs)) + subs.IsValid = true; + return field; + } + + protected void SetAnimatedValue(CompositionProperty prop, ref T field, + TimeSpan commitedAt, IAnimationInstance animation) where T : struct + { + if (IsActive && _animations.TryGetValue(prop, out var oldAnimation)) + oldAnimation.Deactivate(); + _animations[prop] = animation; + + animation.Initialize(commitedAt, ExpressionVariant.Create(field), prop); + if(IsActive) + animation.Activate(); + + InvalidateSubscriptions(prop); + } + + protected void SetAnimatedValue(CompositionProperty property, out T field, T value) + { + if (_animations.TryGetAndRemoveValue(property, out var animation) && IsActive) + animation.Deactivate(); + field = value; + InvalidateSubscriptions(property); + } + + protected T GetAnimatedValue(CompositionProperty property, ref T field) where T : struct + { + if (_subscriptions.TryGetValue(property, out var subscriptions)) + subscriptions.IsValid = true; + + if (_animations.TryGetValue(property, out var animation)) + field = animation.Evaluate(Compositor.ServerNow, ExpressionVariant.Create(field)) + .CastOrDefault(); + + return field; + } + + public virtual void NotifyAnimatedValueChanged(CompositionProperty prop) + { + InvalidateSubscriptions(prop); + ValuesInvalidated(); + } + + protected virtual void ValuesInvalidated() + { + + } + + public void SubscribeToInvalidation(CompositionProperty member, IAnimationInstance animation) + { + if (!_subscriptions.TryGetValue(member, out var store)) + _subscriptions[member] = store = new ServerObjectSubscriptionStore(); + if (store.Subscribers == null) + store.Subscribers = new(); + store.Subscribers.AddRef(animation); + } + + public void UnsubscribeFromInvalidation(CompositionProperty member, IAnimationInstance animation) + { + if(_subscriptions.TryGetValue(member, out var store)) + store.Subscribers?.ReleaseRef(animation); + } + + public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null; + + protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + { + if (this is IDisposable disp + && reader.Read() == 1) + disp.Dispose(); + } + + public void DeserializeChanges(BatchStreamReader reader, Batch batch) + { + DeserializeChangesCore(reader, batch.CommitedAt); + ValuesInvalidated(); + ItselfLastChangedBy = batch.SequenceId; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs new file mode 100644 index 0000000000..e69768d3bf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Rendering.Composition.Transport +{ + /// + /// Represents a group of serialized changes from the UI thread to be atomically applied at the render thread + /// + internal class Batch + { + private static long _nextSequenceId = 1; + private static ConcurrentBag _pool = new(); + public long SequenceId { get; } + + public Batch() + { + SequenceId = Interlocked.Increment(ref _nextSequenceId); + if (!_pool.TryTake(out var lst)) + lst = new BatchStreamData(); + Changes = lst; + } + private TaskCompletionSource _tcs = new TaskCompletionSource(); + public BatchStreamData Changes { get; private set; } + public TimeSpan CommitedAt { get; set; } + + public void Complete() + { + _pool.Add(Changes); + Changes = null!; + + _tcs.TrySetResult(0); + } + + public Task Completed => _tcs.Task; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs new file mode 100644 index 0000000000..65237473fb --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport; + +/// +/// The batch data is separated into 2 "streams": +/// - objects: CLR reference types that are references to either server-side or common objects +/// - structs: blittable types like int, Matrix, Color +/// Each "stream" consists of memory segments that are pooled +/// +internal class BatchStreamData +{ + public Queue> Objects { get; } = new(); + public Queue> Structs { get; } = new(); +} + +public struct BatchStreamSegment +{ + public TData Data { get; set; } + public int ElementCount { get; set; } +} + +internal class BatchStreamWriter : IDisposable +{ + private readonly BatchStreamData _output; + private readonly BatchStreamMemoryPool _memoryPool; + private readonly BatchStreamObjectPool _objectPool; + + private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentDataSegment; + + public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + { + _output = output; + _memoryPool = memoryPool; + _objectPool = objectPool; + } + + void CommitDataSegment() + { + if (_currentDataSegment.Data != IntPtr.Zero) + _output.Structs.Enqueue(_currentDataSegment); + _currentDataSegment = new (); + } + + void NextDataSegment() + { + CommitDataSegment(); + _currentDataSegment.Data = _memoryPool.Get(); + } + + void CommitObjectSegment() + { + if (_currentObjectSegment.Data != null) + _output.Objects.Enqueue(_currentObjectSegment!); + _currentObjectSegment = new(); + } + + void NextObjectSegment() + { + CommitObjectSegment(); + _currentObjectSegment.Data = _objectPool.Get(); + } + + public unsafe void Write(T item) where T : unmanaged + { + var size = Unsafe.SizeOf(); + if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize) + NextDataSegment(); + *(T*)((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount) = item; + _currentDataSegment.ElementCount += size; + } + + public void WriteObject(object? item) + { + if (_currentObjectSegment.Data == null || + _currentObjectSegment.ElementCount >= _currentObjectSegment.Data.Length) + NextObjectSegment(); + _currentObjectSegment.Data![_currentObjectSegment.ElementCount] = item; + _currentObjectSegment.ElementCount++; + } + + public void Dispose() + { + CommitDataSegment(); + CommitObjectSegment(); + } +} + +internal class BatchStreamReader : IDisposable +{ + private readonly BatchStreamData _input; + private readonly BatchStreamMemoryPool _memoryPool; + private readonly BatchStreamObjectPool _objectPool; + + private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentDataSegment; + private int _memoryOffset, _objectOffset; + + public BatchStreamReader(BatchStreamData input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + { + _input = input; + _memoryPool = memoryPool; + _objectPool = objectPool; + } + + public unsafe T Read() where T : unmanaged + { + var size = Unsafe.SizeOf(); + if (_currentDataSegment.Data == IntPtr.Zero) + { + if (_input.Structs.Count == 0) + throw new EndOfStreamException(); + _currentDataSegment = _input.Structs.Dequeue(); + _memoryOffset = 0; + } + + if (_memoryOffset + size > _currentDataSegment.ElementCount) + throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); + + var rv = *(T*)((byte*)_currentDataSegment.Data + _memoryOffset); + _memoryOffset += size; + if (_memoryOffset == _currentDataSegment.ElementCount) + { + _memoryPool.Return(_currentDataSegment.Data); + _currentDataSegment = new(); + } + + return rv; + } + + public T ReadObject() where T : class? => (T)ReadObject()!; + + public object? ReadObject() + { + if (_currentObjectSegment.Data == null) + { + if (_input.Objects.Count == 0) + throw new EndOfStreamException(); + _currentObjectSegment = _input.Objects.Dequeue()!; + _objectOffset = 0; + } + + var rv = _currentObjectSegment.Data![_objectOffset]; + _objectOffset++; + if (_objectOffset == _currentObjectSegment.ElementCount) + { + _objectPool.Return(_currentObjectSegment.Data); + _currentObjectSegment = new(); + } + + return rv; + } + + public bool IsObjectEof => _currentObjectSegment.Data == null && _input.Objects.Count == 0; + + public bool IsStructEof => _currentDataSegment.Data == IntPtr.Zero && _input.Structs.Count == 0; + + public void Dispose() + { + if (_currentDataSegment.Data != IntPtr.Zero) + { + _memoryPool.Return(_currentDataSegment.Data); + _currentDataSegment = new(); + } + + while (_input.Structs.Count > 0) + _memoryPool.Return(_input.Structs.Dequeue().Data); + + if (_currentObjectSegment.Data != null) + { + _objectPool.Return(_currentObjectSegment.Data); + _currentObjectSegment = new(); + } + + while (_input.Objects.Count > 0) + _objectPool.Return(_input.Objects.Dequeue().Data); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs new file mode 100644 index 0000000000..32b4ed3026 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using Avalonia.Threading; + +namespace Avalonia.Rendering.Composition.Transport; + +/// +/// A pool that keeps a number of elements that was used in the last 10 seconds +/// +internal abstract class BatchStreamPoolBase : IDisposable +{ + readonly Stack _pool = new(); + bool _disposed; + int _usage; + readonly int[] _usageStatistics = new int[10]; + int _usageStatisticsSlot; + + public int CurrentUsage => _usage; + public int CurrentPool => _pool.Count; + + public BatchStreamPoolBase(bool needsFinalize, Action>? startTimer = null) + { + if(!needsFinalize) + GC.SuppressFinalize(needsFinalize); + + var updateRef = new WeakReference>(this); + StartUpdateTimer(startTimer, updateRef); + } + + static void StartUpdateTimer(Action>? startTimer, WeakReference> updateRef) + { + Func timerProc = () => + { + if (updateRef.TryGetTarget(out var target)) + { + target.UpdateStatistics(); + return true; + } + + return false; + }; + if (startTimer != null) + startTimer(timerProc); + else + DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)); + } + + private void UpdateStatistics() + { + lock (_pool) + { + var maximumUsage = _usageStatistics.Max(); + var recentlyUsedPooledSlots = maximumUsage - _usage; + var keepSlots = Math.Max(recentlyUsedPooledSlots, 10); + while (keepSlots < _pool.Count) + DestroyItem(_pool.Pop()); + + _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; + _usageStatistics[_usageStatisticsSlot] = 0; + } + } + + protected abstract T CreateItem(); + + protected virtual void DestroyItem(T item) + { + + } + + public T Get() + { + lock (_pool) + { + _usage++; + if (_usageStatistics[_usageStatisticsSlot] < _usage) + _usageStatistics[_usageStatisticsSlot] = _usage; + + if (_pool.Count != 0) + return _pool.Pop(); + } + + return CreateItem(); + } + + public void Return(T item) + { + lock (_pool) + { + _usage--; + if (!_disposed) + { + _pool.Push(item); + return; + } + } + + DestroyItem(item); + } + + public void Dispose() + { + lock (_pool) + { + _disposed = true; + foreach (var item in _pool) + DestroyItem(item); + _pool.Clear(); + } + } + + ~BatchStreamPoolBase() + { + Dispose(); + } +} + +internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class? +{ + public int ArraySize { get; } + + public BatchStreamObjectPool(int arraySize = 128, Action>? startTimer = null) : base(false, startTimer) + { + ArraySize = arraySize; + } + + protected override T[] CreateItem() + { + return new T[ArraySize]; + } + + protected override void DestroyItem(T[] item) + { + Array.Clear(item, 0, item.Length); + } +} + +internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase +{ + public int BufferSize { get; } + + public BatchStreamMemoryPool(int bufferSize = 1024, Action>? startTimer = null) : base(true, startTimer) + { + BufferSize = bufferSize; + } + + protected override IntPtr CreateItem() => Marshal.AllocHGlobal(BufferSize); + + protected override void DestroyItem(IntPtr item) => Marshal.FreeHGlobal(item); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs new file mode 100644 index 0000000000..7d21b03f24 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class BatchStreamDebugMarkers +{ + public static object ObjectEndMarker = new object(); + public static Guid ObjectEndMagic = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs new file mode 100644 index 0000000000..e295c3c2c8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs @@ -0,0 +1,98 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + /// + /// A helper class used from generated UI-thread-side collections of composition objects. + /// + // NOTE: This should probably be a base class since TServer isn't used anymore and it was the reason why + // it couldn't be exposed as a base class + class ServerListProxyHelper : IList + where TServer : ServerObject + where TClient : CompositionObject + { + private readonly IRegisterForSerialization _parent; + private bool _changed; + + public interface IRegisterForSerialization + { + void RegisterForSerialization(); + } + + public ServerListProxyHelper(IRegisterForSerialization parent) + { + _parent = parent; + } + + private readonly List _list = new List(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(TClient item) => Insert(_list.Count, item); + + public void Clear() + { + _list.Clear(); + _changed = true; + _parent.RegisterForSerialization(); + } + + public bool Contains(TClient item) => _list.Contains(item); + + public void CopyTo(TClient[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public bool Remove(TClient item) + { + var idx = _list.IndexOf(item); + if (idx == -1) + return false; + RemoveAt(idx); + return true; + } + + public int Count => _list.Count; + public bool IsReadOnly => false; + public int IndexOf(TClient item) => _list.IndexOf(item); + + public void Insert(int index, TClient item) + { + _list.Insert(index, item); + _changed = true; + _parent.RegisterForSerialization(); + } + + public void RemoveAt(int index) + { + _list.RemoveAt(index); + _changed = true; + _parent.RegisterForSerialization(); + } + + public TClient this[int index] + { + get => _list[index]; + set + { + _list[index] = value; + _changed = true; + _parent.RegisterForSerialization(); + } + } + + public void Serialize(BatchStreamWriter writer) + { + writer.Write((byte)(_changed ? 1 : 0)); + if (_changed) + { + writer.Write(_list.Count); + foreach (var el in _list) + writer.WriteObject(el.Server); + } + _changed = false; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs new file mode 100644 index 0000000000..f9e1eae2ab --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -0,0 +1,56 @@ +using System; +using System.Numerics; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.Composition +{ + /// + /// The base visual object in the composition visual hierarchy. + /// + public abstract partial class CompositionVisual + { + private IBrush? _opacityMask; + + private protected virtual void OnRootChangedCore() + { + } + + partial void OnRootChanged() => OnRootChangedCore(); + + partial void OnParentChanged() => Root = Parent?.Root; + + public IBrush? OpacityMask + { + get => _opacityMask; + set + { + if (_opacityMask == value) + return; + OpacityMaskBrush = (_opacityMask = value)?.ToImmutable(); + } + } + + internal Matrix4x4? TryGetServerTransform() + { + if (Root == null) + return null; + var i = Root.Server.Readback; + ref var readback = ref Server.GetReadback(i.ReadIndex); + + // CompositionVisual wasn't visible or wasn't even attached to the composition target during the lat frame + if (!readback.Visible || readback.Revision < i.ReadRevision) + return null; + + // CompositionVisual was reparented (potential race here) + if (readback.TargetId != Root.Server.Id) + return null; + + return readback.Matrix; + } + + internal object? Tag { get; set; } + + internal virtual bool HitTest(Point point, Func? filter) => true; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs new file mode 100644 index 0000000000..60ebd9271c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -0,0 +1,73 @@ +using System; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + /// + /// A collection of CompositionVisual objects + /// + public partial class CompositionVisualCollection : CompositionObject + { + private CompositionVisual _owner; + internal CompositionVisualCollection(CompositionVisual parent, ServerCompositionVisualCollection server) : base(parent.Compositor, server) + { + _owner = parent; + InitializeDefaults(); + } + + public void InsertAbove(CompositionVisual newChild, CompositionVisual sibling) + { + var idx = _list.IndexOf(sibling); + if (idx == -1) + throw new InvalidOperationException(); + + Insert(idx + 1, newChild); + } + + public void InsertBelow(CompositionVisual newChild, CompositionVisual sibling) + { + var idx = _list.IndexOf(sibling); + if (idx == -1) + throw new InvalidOperationException(); + Insert(idx, newChild); + } + + public void InsertAtTop(CompositionVisual newChild) => Insert(_list.Count, newChild); + + public void InsertAtBottom(CompositionVisual newChild) => Insert(0, newChild); + + public void RemoveAll() => Clear(); + + partial void OnAdded(CompositionVisual item) => item.Parent = _owner; + + partial void OnBeforeReplace(CompositionVisual oldItem, CompositionVisual newItem) + { + if (oldItem != newItem) + OnBeforeAdded(newItem); + } + + partial void OnReplace(CompositionVisual oldItem, CompositionVisual newItem) + { + if (oldItem != newItem) + { + OnRemoved(oldItem); + OnAdded(newItem); + } + } + + partial void OnRemoved(CompositionVisual item) => item.Parent = null; + + partial void OnBeforeClear() + { + foreach (var i in this) + i.Parent = null; + } + + partial void OnBeforeAdded(CompositionVisual item) + { + if (item.Parent != null) + throw new InvalidOperationException("Visual already has a parent"); + item.Parent = item; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs index 82d3892975..d0d3dd9715 100644 --- a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs @@ -59,6 +59,8 @@ namespace Avalonia.Rendering } } + public bool RunsInBackground => true; + /// /// Starts the timer. /// diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index 82be0a1a0f..4236763e3b 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -272,16 +272,18 @@ namespace Avalonia.Rendering } } + Scene? TryGetChildScene(IRef? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene; + /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; + return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty; } /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; + var childScene = TryGetChildScene(_currentDraw); if (childScene != null) { diff --git a/src/Avalonia.Base/Rendering/IRenderLoop.cs b/src/Avalonia.Base/Rendering/IRenderLoop.cs index 9838967261..e500ecdf8b 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoop.cs +++ b/src/Avalonia.Base/Rendering/IRenderLoop.cs @@ -27,5 +27,7 @@ namespace Avalonia.Rendering /// /// The update task. void Remove(IRenderLoopTask i); + + bool RunsInBackground { get; } } } diff --git a/src/Avalonia.Base/Rendering/IRenderTimer.cs b/src/Avalonia.Base/Rendering/IRenderTimer.cs index ee74c345be..07af7eeec8 100644 --- a/src/Avalonia.Base/Rendering/IRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/IRenderTimer.cs @@ -18,5 +18,10 @@ namespace Avalonia.Rendering /// switch execution to the right thread. /// event Action Tick; + + /// + /// Indicates if the timer ticks on a non-UI thread + /// + bool RunsInBackground { get; } } } diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index e998f78d5c..8d6aabf440 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -1,6 +1,7 @@ using System; using Avalonia.VisualTree; using System.Collections.Generic; +using Avalonia.Rendering.Composition; namespace Avalonia.Rendering { @@ -87,4 +88,9 @@ namespace Avalonia.Rendering /// void Stop(); } + + public interface IRendererWithCompositor : IRenderer + { + Compositor Compositor { get; } + } } diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 54b2ce5a25..79ef52586d 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -331,7 +331,11 @@ namespace Avalonia.Rendering if (_updateTransformedBounds) visual.TransformedBounds = transformed; - foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)) + var childrenEnumerable = visual.HasNonUniformZIndexChildren + ? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance) + : (IEnumerable)visual.VisualChildren; + + foreach (var child in childrenEnumerable) { var childBounds = GetTransformedBounds(child); diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index a5d7e15f93..c66fec92aa 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -87,6 +87,8 @@ namespace Avalonia.Rendering } } + public bool RunsInBackground => Timer.RunsInBackground; + private void TimerTick(TimeSpan time) { if (Interlocked.CompareExchange(ref _inTick, 1, 0) == 0) diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs index cd3dac699a..e81966ce81 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.VisualTree; @@ -9,14 +10,21 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class BrushDrawOperation : DrawOperation { - public BrushDrawOperation(Rect bounds, Matrix transform) + public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux) : base(bounds, transform) { + Aux = aux; } /// - /// Gets a collection of child scenes that are needed to draw visual brushes. + /// Auxiliary data required to draw the brush /// - public abstract IDictionary? ChildScenes { get; } + public IDisposable? Aux { get; } + + public override void Dispose() + { + Aux?.Dispose(); + base.Dispose(); + } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 5225b85020..07082e4ac3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; @@ -456,7 +457,7 @@ namespace Avalonia.Rendering.SceneGraph return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef : null; } - private IDictionary? CreateChildScene(IBrush? brush) + private IDisposable? CreateChildScene(IBrush? brush) { var visualBrush = brush as VisualBrush; @@ -469,7 +470,7 @@ namespace Avalonia.Rendering.SceneGraph (visual as IVisualBrushInitialize)?.EnsureInitialized(); var scene = new Scene(visual); _sceneBuilder.UpdateAll(scene); - return new Dictionary { { visualBrush.Visual, scene } }; + return scene; } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index c1fc6a81f6..4600653b9d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -17,14 +17,13 @@ namespace Avalonia.Rendering.SceneGraph IBrush? brush, IPen? pen, Rect rect, - IDictionary? childScenes = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform) + IDisposable? aux = null) + : base(rect.Inflate(pen?.Thickness ?? 0), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - ChildScenes = childScenes; } /// @@ -47,8 +46,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Rect Rect { get; } - public override IDictionary? ChildScenes { get; } - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, Rect rect) { return transform == Transform && diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 70748989d6..4b43f93aee 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -23,14 +24,13 @@ namespace Avalonia.Rendering.SceneGraph IBrush? brush, IPen? pen, IGeometryImpl geometry, - IDictionary? childScenes = null) - : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform) + IDisposable? aux) + : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; - ChildScenes = childScenes; } /// @@ -53,9 +53,6 @@ namespace Avalonia.Rendering.SceneGraph /// public IGeometryImpl Geometry { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index d6da087120..9199611ed6 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -23,13 +24,12 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IBrush foreground, GlyphRun glyphRun, - IDictionary? childScenes = null) - : base(new Rect(glyphRun.Size), transform) + IDisposable? aux = null) + : base(new Rect(glyphRun.Size), transform, aux) { Transform = transform; Foreground = foreground.ToImmutable(); GlyphRun = glyphRun; - ChildScenes = childScenes; } /// @@ -47,9 +47,6 @@ namespace Avalonia.Rendering.SceneGraph /// public GlyphRun GlyphRun { get; } - /// - public override IDictionary? ChildScenes { get; } - /// public override void Render(IDrawingContextImpl context) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index a9e1ce8ed7..ee5ec0a5fc 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -25,14 +25,13 @@ namespace Avalonia.Rendering.SceneGraph IPen pen, Point p1, Point p2, - IDictionary? childScenes = null) - : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) + IDisposable? aux = null) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux) { Transform = transform; Pen = pen.ToImmutable(); P1 = p1; P2 = p2; - ChildScenes = childScenes; } /// @@ -55,9 +54,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Point P2 { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 4b6e7d2254..549c1fd7de 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Platform; using Avalonia.VisualTree; @@ -17,12 +18,11 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity mask to push. /// The bounds of the mask. /// Child scenes for drawing visual brushes. - public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary? childScenes = null) - : base(Rect.Empty, Matrix.Identity) + public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) + : base(Rect.Empty, Matrix.Identity, aux) { Mask = mask.ToImmutable(); MaskBounds = bounds; - ChildScenes = childScenes; } /// @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() - : base(Rect.Empty, Matrix.Identity) + : base(Rect.Empty, Matrix.Identity, null) { } @@ -44,8 +44,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Rect? MaskBounds { get; } - /// - public override IDictionary? ChildScenes { get; } /// public override bool HitTest(Point p) => false; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index 3279c3a549..7b79c446f9 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -26,14 +27,13 @@ namespace Avalonia.Rendering.SceneGraph IPen? pen, RoundedRect rect, BoxShadows boxShadows, - IDictionary? childScenes = null) - : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform) + IDisposable? aux = null) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - ChildScenes = childScenes; BoxShadows = boxShadows; } @@ -62,9 +62,6 @@ namespace Avalonia.Rendering.SceneGraph /// public BoxShadows BoxShadows { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs index e4d5a1ca68..0ceb44ed75 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs @@ -275,26 +275,36 @@ namespace Avalonia.Rendering.SceneGraph else if (visualChildren.Count > 1) { var count = visualChildren.Count; - var sortedChildren = new (IVisual visual, int index)[count]; - for (var i = 0; i < count; i++) + if (visual.HasNonUniformZIndexChildren) { - sortedChildren[i] = (visualChildren[i], i); - } - - // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. - Array.Sort(sortedChildren, (lhs, rhs) => - { - var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); - - return result == 0 ? lhs.index.CompareTo(rhs.index) : result; - }); - - foreach (var child in sortedChildren) - { - var childNode = GetOrCreateChildNode(scene, child.Item1, node); - Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + var sortedChildren = new (IVisual visual, int index)[count]; + + for (var i = 0; i < count; i++) + { + sortedChildren[i] = (visualChildren[i], i); + } + + // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. + Array.Sort(sortedChildren, (lhs, rhs) => + { + var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); + + return result == 0 ? lhs.index.CompareTo(rhs.index) : result; + }); + + foreach (var child in sortedChildren) + { + var childNode = GetOrCreateChildNode(scene, child.Item1, node); + Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + } } + else + foreach (var child in visualChildren) + { + var childNode = GetOrCreateChildNode(scene, child, node); + Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + } } node.SubTreeUpdated = true; diff --git a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs index 86595754e9..cd43a3ef20 100644 --- a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs @@ -43,6 +43,8 @@ namespace Avalonia.Rendering } } + public bool RunsInBackground => true; + void LoopProc() { var lastTick = _st.Elapsed; @@ -51,7 +53,7 @@ namespace Avalonia.Rendering var now = _st.Elapsed; var timeTillNextTick = lastTick + _timeBetweenTicks - now; if (timeTillNextTick.TotalMilliseconds > 1) Thread.Sleep(timeTillNextTick); - lastTick = now; + lastTick = now = _st.Elapsed; lock (_lock) { if (_count == 0) diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs index 69c3ae7319..5f20206200 100644 --- a/src/Avalonia.Base/Size.cs +++ b/src/Avalonia.Base/Size.cs @@ -52,6 +52,17 @@ namespace Avalonia _width = width; _height = height; } + +#if !BUILDTASK + /// + /// Initializes a new instance of the structure. + /// + /// The vector to take values from. + public Size(System.Numerics.Vector2 vector2) : this(vector2.X, vector2.Y) + { + + } +#endif /// /// Gets the aspect ratio of the size. diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index a93e4f406d..b4bf603f74 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -62,10 +62,20 @@ namespace Avalonia.Threading /// public static readonly DispatcherPriority Render = new(5); + /// + /// The job will be processed with the same priority as composition batch commit. + /// + public static readonly DispatcherPriority CompositionBatch = new(6); + + /// + /// The job will be processed with the same priority as composition updates. + /// + public static readonly DispatcherPriority Composition = new(7); + /// /// The job will be processed with the same priority as render. /// - public static readonly DispatcherPriority Layout = new(6); + public static readonly DispatcherPriority Layout = new(8); /// /// The job will be processed with the same priority as data binding. @@ -75,7 +85,7 @@ namespace Avalonia.Threading /// /// The job will be processed before other asynchronous operations. /// - public static readonly DispatcherPriority Send = new(7); + public static readonly DispatcherPriority Send = new(9); /// /// Maximum possible priority diff --git a/src/Avalonia.Dialogs/ByteSizeHelper.cs b/src/Avalonia.Base/Utilities/ByteSizeHelper.cs similarity index 70% rename from src/Avalonia.Dialogs/ByteSizeHelper.cs rename to src/Avalonia.Base/Utilities/ByteSizeHelper.cs index d849e33399..edaf94231b 100644 --- a/src/Avalonia.Dialogs/ByteSizeHelper.cs +++ b/src/Avalonia.Base/Utilities/ByteSizeHelper.cs @@ -1,10 +1,11 @@ using System; -namespace Avalonia.Dialogs +namespace Avalonia.Utilities { internal static class ByteSizeHelper { - private const string formatTemplate = "{0}{1:0.#} {2}"; + private const string formatTemplateSeparated = "{0}{1:0.#} {2}"; + private const string formatTemplate = "{0}{1:0.#}{2}"; private static readonly string[] Prefixes = { @@ -19,11 +20,11 @@ namespace Avalonia.Dialogs "YB" }; - public static string ToString(ulong bytes) + public static string ToString(ulong bytes, bool separate) { if (bytes == 0) { - return string.Format(formatTemplate, null, 0, Prefixes[0]); + return string.Format(separate ? formatTemplateSeparated : formatTemplate, null, 0, Prefixes[0]); } var absSize = Math.Abs((double)bytes); diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 596cbf1d7e..3d5be806e1 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -251,6 +251,20 @@ namespace Avalonia.Utilities return val; } } + + /// + /// Clamps a value between a minimum and maximum value. + /// + /// The value. + /// The minimum value. + /// The maximum value. + /// The clamped value. + public static float Clamp(float value, float min, float max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } /// /// Clamps a value between a minimum and maximum value. diff --git a/src/Avalonia.Base/Utilities/Ref.cs b/src/Avalonia.Base/Utilities/Ref.cs index 7209f02720..95a1c23883 100644 --- a/src/Avalonia.Base/Utilities/Ref.cs +++ b/src/Avalonia.Base/Utilities/Ref.cs @@ -159,7 +159,7 @@ namespace Avalonia.Utilities ~Ref() { - _counter?.Release(); + Dispose(); } public T Item diff --git a/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs new file mode 100644 index 0000000000..9400e37f21 --- /dev/null +++ b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Avalonia.Utilities; + +/// +/// Maintains a set of objects with reference counts +/// +internal class RefTrackingDictionary : Dictionary where TKey : class +{ + /// + /// Increase reference count for a key by 1. + /// + /// true if key was added to the dictionary, false otherwise + public bool AddRef(TKey key) + { +#if NET5_0_OR_GREATER + ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(this, key, out var _); + count++; +#else + TryGetValue(key, out var count); + count++; + this[key] = count; +#endif + return count == 1; + } + + /// + /// Decrease reference count for a key by 1. + /// + /// true if key was removed to the dictionary, false otherwise + public bool ReleaseRef(TKey key) + { +#if NET5_0_OR_GREATER + ref var count = ref CollectionsMarshal.GetValueRefOrNullRef(this, key); + if (Unsafe.IsNullRef(ref count)) +#if DEBUG + throw new InvalidOperationException("Attempting to release a non-referenced object"); +#else + return false; +#endif // DEBUG + count--; + if (count == 0) + { + Remove(key); + return true; + } + + return false; +#else + if (!TryGetValue(key, out var count)) +#if DEBUG + throw new InvalidOperationException("Attempting to release a non-referenced object"); +#else + return false; +#endif // DEBUG + count--; + if (count == 0) + { + Remove(key); + return true; + } + + this[key] = count; + return false; +#endif + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs new file mode 100644 index 0000000000..7d6a21c136 --- /dev/null +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Avalonia.Utilities; + +public struct InlineDictionary : IEnumerable> where TKey : class where TValue : class +{ + object? _data; + TValue? _value; + + void SetCore(TKey key, TValue value, bool overwrite) + { + if (key == null) + throw new ArgumentNullException(); + if (_data == null) + { + _data = key; + _value = value; + } + else if (_data is KeyValuePair[] arr) + { + var free = -1; + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + if (overwrite) + { + arr[c] = new(key, value); + return; + } + else + throw new ArgumentException("Key already exists in dictionary"); + } + + if (arr[c].Key == null) + free = c; + } + + if (free != -1) + { + arr[free] = new KeyValuePair(key, value); + return; + } + + // Upgrade to dictionary + var newDic = new Dictionary(); + foreach (var kvp in arr) + newDic.Add(kvp.Key!, kvp.Value!); + newDic.Add(key, value); + _data = newDic; + } + else if (_data is Dictionary dic) + { + if (overwrite) + dic[key] = value; + else + dic.Add(key, value); + } + else + { + // We have a single element, upgrade to array + arr = new KeyValuePair[6]; + arr[0] = new KeyValuePair((TKey)_data, _value); + arr[1] = new KeyValuePair(key, value); + _data = arr; + _value = null; + } + } + + public void Add(TKey key, TValue value) => SetCore(key, value, false); + public void Set(TKey key, TValue value) => SetCore(key, value, true); + + public TValue this[TKey key] + { + get + { + if (TryGetValue(key, out var rv)) + return rv; + throw new KeyNotFoundException(); + } + set => Set(key, value); + } + + public bool Remove(TKey key) + { + if (_data == key) + { + _data = null; + _value = null; + return true; + } + else if (_data is KeyValuePair[] arr) + { + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + arr[c] = default; + return true; + } + } + + return false; + } + else if (_data is Dictionary dic) + return dic.Remove(key); + + return false; + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)]out TValue value) + { + if (_data == key) + { + value = _value!; + return true; + } + else if (_data is KeyValuePair[] arr) + { + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + value = arr[c].Value!; + return true; + } + } + + value = null; + return false; + } + else if (_data is Dictionary dic) + return dic.TryGetValue(key, out value); + + value = null; + return false; + } + + + public bool TryGetAndRemoveValue(TKey key, [MaybeNullWhen(false)]out TValue value) + { + if (_data == key) + { + value = _value!; + _value = null; + _data = null; + return true; + } + else if (_data is KeyValuePair[] arr) + { + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + value = arr[c].Value!; + arr[c] = default; + return true; + } + } + + value = null; + return false; + } + else if (_data is Dictionary dic) + { + if (!dic.TryGetValue(key, out value)) + return false; + dic.Remove(key); + } + + value = null; + return false; + } + + public TValue GetAndRemove(TKey key) + { + if (TryGetAndRemoveValue(key, out var v)) + return v; + throw new KeyNotFoundException(); + } + + public struct Enumerator : IEnumerator> + { + private Dictionary.Enumerator _inner; + private readonly KeyValuePair[]? _arr; + private KeyValuePair _first; + private int _index; + private Type _type; + enum Type + { + Empty, Single, Array, Dictionary + } + + public Enumerator(InlineDictionary parent) + { + _arr = null; + _first = default; + _index = -1; + _inner = default; + if (parent._data is Dictionary inner) + { + _inner = inner.GetEnumerator(); + _type = Type.Dictionary; + } + else if (parent._data is KeyValuePair[] arr) + { + _type = Type.Array; + _arr = arr; + } + else if (parent._data != null) + { + _type = Type.Single; + _first = new((TKey)parent._data!, parent._value!); + } + else + _type = Type.Empty; + + } + + public bool MoveNext() + { + if (_type == Type.Single) + { + if (_index != -1) + return false; + _index = 0; + } + else if (_type == Type.Array) + { + var next = _index + 1; + if (_arr!.Length - 1 < next || _arr[next].Key == null) + return false; + _index = next; + return true; + } + else if (_type == Type.Dictionary) + return _inner.MoveNext(); + + return false; + } + + public void Reset() + { + _index = -1; + if(_type == Type.Dictionary) + ((IEnumerator)_inner).Reset(); + } + + public KeyValuePair Current + { + get + { + if (_type == Type.Single) + return _first!; + if (_type == Type.Array) + return _arr![_index]!; + if (_type == Type.Dictionary) + return _inner.Current; + throw new InvalidOperationException(); + } + } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + } + + public Enumerator GetEnumerator() => new Enumerator(this); + + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + +} diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index bdf8723b81..8feba116f0 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -1,3 +1,7 @@ + + +#nullable enable + using System; using System.Collections; using System.Collections.Specialized; @@ -8,11 +12,10 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; using Avalonia.Utilities; using Avalonia.VisualTree; - -#nullable enable - namespace Avalonia { /// @@ -288,6 +291,10 @@ namespace Avalonia /// protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot); + internal CompositionDrawListVisual? CompositionVisual { get; private set; } + + public bool HasNonUniformZIndexChildren { get; private set; } + /// /// Gets a value indicating whether this control is attached to a visual root. /// @@ -434,10 +441,17 @@ namespace Avalonia } EnableTransitions(); + if (_visualRoot.Renderer is IRendererWithCompositor compositingRenderer) + { + AttachToCompositor(compositingRenderer.Compositor); + } OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); + if (ZIndex != 0 && this.GetVisualParent() is Visual parent) + parent.HasNonUniformZIndexChildren = true; + var visualChildren = VisualChildren; if (visualChildren != null) @@ -454,6 +468,17 @@ namespace Avalonia } } + internal CompositionVisual AttachToCompositor(Compositor compositor) + { + if (CompositionVisual == null || CompositionVisual.Compositor != compositor) + { + CompositionVisual = new CompositionDrawListVisual(compositor, + new ServerCompositionDrawListVisual(compositor.Server, this), this); + } + + return CompositionVisual; + } + /// /// Calls the method /// for this control and all of its visual descendants. @@ -472,6 +497,12 @@ namespace Avalonia DisableTransitions(); OnDetachedFromVisualTree(e); + if (CompositionVisual != null) + { + CompositionVisual.DrawList = null; + CompositionVisual = null; + } + DetachedFromVisualTree?.Invoke(this, e); e.Root?.Renderer?.AddDirty(this); @@ -566,7 +597,7 @@ namespace Avalonia { newValue.Changed += sender.RenderTransformChanged; } - + sender.InvalidateVisual(); } } @@ -596,6 +627,9 @@ namespace Avalonia { var sender = e.Sender as IVisual; var parent = sender?.VisualParent; + if (sender?.ZIndex != 0 && parent is Visual parentVisual) + parentVisual.HasNonUniformZIndexChildren = true; + sender?.InvalidateVisual(); parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); } diff --git a/src/Avalonia.Base/VisualTree/IVisual.cs b/src/Avalonia.Base/VisualTree/IVisual.cs index 3b053fab38..fdd2d187b8 100644 --- a/src/Avalonia.Base/VisualTree/IVisual.cs +++ b/src/Avalonia.Base/VisualTree/IVisual.cs @@ -79,6 +79,11 @@ namespace Avalonia.VisualTree /// Gets a value indicating whether to apply mirror transform on this control. /// bool HasMirrorTransform { get; } + + /// + /// Gets a value indicating whether to sort children when rendering this control + /// + bool HasNonUniformZIndexChildren { get; } /// /// Gets or sets the render transform of the control. diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml new file mode 100644 index 0000000000..e0e177da44 --- /dev/null +++ b/src/Avalonia.Base/composition-schema.xml @@ -0,0 +1,46 @@ + + + System.Numerics + Avalonia.Rendering.Composition.Server + Avalonia.Rendering.Composition.Transport + Avalonia.Rendering.Composition.Animations + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index 28cb3e34b2..a93d3fa7dd 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -146,7 +146,7 @@ namespace Avalonia.Automation.Peers protected override string? GetAccessKeyCore() => AutomationProperties.GetAccessKey(Owner); protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name; - protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds); + protected override Rect GetBoundingRectangleCore() => GetBounds(Owner); protected override string GetClassNameCore() => Owner.GetType().Name; protected override bool HasKeyboardFocusCore() => Owner.IsFocused; protected override bool IsContentElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Content; @@ -160,9 +160,19 @@ namespace Avalonia.Automation.Peers return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore(); } - private static Rect GetBounds(TransformedBounds? bounds) + private static Rect GetBounds(Control control) { - return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default; + var root = control.GetVisualRoot(); + + if (root is null) + return default; + + var transform = control.TransformToVisual(root); + + if (!transform.HasValue) + return default; + + return new Rect(control.Bounds.Size).TransformToAABB(transform.Value); } private void Initialize() @@ -182,12 +192,14 @@ namespace Avalonia.Automation.Peers if (parent is Control c) (GetOrCreate(c) as ControlAutomationPeer)?.InvalidateChildren(); } - else if (e.Property == Visual.TransformedBoundsProperty) + else if (e.Property == Visual.BoundsProperty || + e.Property == Visual.RenderTransformProperty || + e.Property == Visual.RenderTransformOriginProperty) { RaisePropertyChangedEvent( AutomationElementIdentifiers.BoundingRectangleProperty, - GetBounds((TransformedBounds?)e.OldValue), - GetBounds((TransformedBounds?)e.NewValue)); + null, + GetBounds(Owner)); } else if (e.Property == Visual.VisualParentProperty) { diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 5ad4e39baf..57fb7226e8 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -164,6 +164,9 @@ namespace Avalonia.Controls.Primitives private void UpdateAdornedElement(Visual adorner, Visual? adorned) { + if (adorner.CompositionVisual != null) + adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual; + var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) @@ -184,11 +187,18 @@ namespace Avalonia.Controls.Primitives adorner.SetValue(s_adornedElementInfoProperty, info); } - info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x => - { - info.Bounds = x; - InvalidateMeasure(); - }); + if (adorner.CompositionVisual != null) + info.Subscription = adorned.GetObservable(BoundsProperty).Subscribe(x => + { + info.Bounds = new TransformedBounds(new Rect(adorned.Bounds.Size), new Rect(adorned.Bounds.Size), Matrix.Identity); + InvalidateMeasure(); + }); + else + info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x => + { + info.Bounds = x; + InvalidateMeasure(); + }); } } diff --git a/src/Avalonia.Dialogs/FileSizeStringConverter.cs b/src/Avalonia.Dialogs/FileSizeStringConverter.cs index c2cdf1e502..144f50fd9c 100644 --- a/src/Avalonia.Dialogs/FileSizeStringConverter.cs +++ b/src/Avalonia.Dialogs/FileSizeStringConverter.cs @@ -12,7 +12,7 @@ namespace Avalonia.Dialogs { if (value is long size && size > 0) { - return ByteSizeHelper.ToString((ulong)size); + return Avalonia.Utilities.ByteSizeHelper.ToString((ulong)size, true); } return ""; diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs index a217a67bc6..a76a84ba5a 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using System.Runtime.InteropServices; using Avalonia.Controls.Platform; using Avalonia.Threading; +using Avalonia.Utilities; namespace Avalonia.Dialogs { @@ -60,7 +61,7 @@ namespace Avalonia.Dialogs if (displayName == null & x.VolumeSizeBytes > 0) { - displayName = $"{ByteSizeHelper.ToString(x.VolumeSizeBytes)} Volume"; + displayName = $"{ByteSizeHelper.ToString(x.VolumeSizeBytes, true)} Volume"; }; try diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs index bd215296c8..cc7d5ef30d 100644 --- a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs +++ b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -20,7 +20,11 @@ namespace Avalonia var tcpServer = new TcpListener(host == null ? IPAddress.Loopback : IPAddress.Parse(host), port); tcpServer.Start(); return builder - .UseHeadless(false) + .UseHeadless(new AvaloniaHeadlessPlatformOptions + { + UseCompositor = true, + UseHeadlessDrawing = false + }) .AfterSetup(_ => { var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime); diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 8e83b0c7b6..4fcec6fd52 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -7,12 +7,14 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; namespace Avalonia.Headless { public static class AvaloniaHeadlessPlatform { + internal static Compositor Compositor { get; private set; } class RenderTimer : DefaultRenderTimer { private readonly int _framesPerSecond; @@ -55,7 +57,7 @@ namespace Avalonia.Headless public ITrayIconImpl CreateTrayIcon() => null; } - internal static void Initialize() + internal static void Initialize(AvaloniaHeadlessPlatformOptions opts) { AvaloniaLocator.CurrentMutable .Bind().ToConstant(new HeadlessPlatformThreadingInterface()) @@ -70,6 +72,8 @@ namespace Avalonia.Headless .Bind().ToSingleton() .Bind().ToConstant(new HeadlessWindowingPlatform()) .Bind().ToSingleton(); + if (opts.UseCompositor) + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), null); } @@ -81,15 +85,21 @@ namespace Avalonia.Headless } } - + + public class AvaloniaHeadlessPlatformOptions + { + public bool UseCompositor { get; set; } = true; + public bool UseHeadlessDrawing { get; set; } = true; + } + public static class AvaloniaHeadlessPlatformExtensions { - public static T UseHeadless(this T builder, bool headlessDrawing = true) + public static T UseHeadless(this T builder, AvaloniaHeadlessPlatformOptions opts) where T : AppBuilderBase, new() { - if (headlessDrawing) + if(opts.UseHeadlessDrawing) builder.UseRenderingSubsystem(HeadlessPlatformRenderInterface.Initialize, "Headless"); - return builder.UseWindowingSubsystem(AvaloniaHeadlessPlatform.Initialize, "Headless"); + return builder.UseWindowingSubsystem(() => AvaloniaHeadlessPlatform.Initialize(opts), "Headless"); } } } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 059a9a4e8f..5576368240 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Numerics; using System.Runtime.InteropServices; using Avalonia.Media; using Avalonia.Platform; @@ -349,6 +350,7 @@ namespace Avalonia.Headless } public Matrix Transform { get; set; } + public void Clear(Color color) { @@ -416,7 +418,6 @@ namespace Avalonia.Headless public void DrawLine(IPen pen, Point p1, Point p2) { - throw new NotImplementedException(); } public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 6fbdf8dffd..742df3324b 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -12,6 +12,7 @@ using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Utilities; @@ -54,7 +55,9 @@ namespace Avalonia.Headless public Action ScalingChanged { get; set; } public IRenderer CreateRenderer(IRenderRoot root) - => new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); + => AvaloniaHeadlessPlatform.Compositor != null + ? new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor) + : new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService()); public void Invalidate(Rect rect) { diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 6cbe888d9b..b45fe5559b 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -8,6 +8,8 @@ using Avalonia.Native.Interop; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using JetBrains.Annotations; namespace Avalonia.Native { @@ -21,6 +23,7 @@ namespace Avalonia.Native static extern IntPtr CreateAvaloniaNative(); internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice(); + [CanBeNull] internal static Compositor Compositor { get; private set; } public Size DoubleClickSize => new Size(4, 4); @@ -110,7 +113,6 @@ namespace Avalonia.Native .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) - .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) @@ -118,24 +120,36 @@ namespace Avalonia.Native .Bind().ToConstant(applicationPlatform) .Bind().ToConstant(new MacOSNativeMenuCommands(_factory.CreateApplicationCommands())); + var renderLoop = new RenderLoop(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(renderLoop); + var hotkeys = AvaloniaLocator.Current.GetService(); hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers)); hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers)); hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); - + if (_options.UseGpu) { try { - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(_platformGl = new AvaloniaNativePlatformOpenGlInterface(_factory.ObtainGlDisplay())); + _platformGl = new AvaloniaNativePlatformOpenGlInterface(_factory.ObtainGlDisplay()); + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(_platformGl) + .Bind().ToConstant(_platformGl); + } catch (Exception) { // ignored } } + + + if (_options.UseDeferredRendering && _options.UseCompositor) + { + Compositor = new Compositor(renderLoop, _platformGl); + } } public ITrayIconImpl CreateTrayIcon() diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 10619d675b..61889aa9e4 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -39,6 +39,11 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; + + /// + /// Enables new compositing rendering with UWP-like API + /// + public bool UseCompositor { get; set; } = true; /// /// Determines whether to use GPU for rendering in your project. The default value is true. diff --git a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs index 3b3d8836fd..14d27a90e9 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs @@ -3,6 +3,7 @@ using Avalonia.OpenGL; using Avalonia.Native.Interop; using System.Drawing; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using Avalonia.Threading; namespace Avalonia.Native @@ -37,6 +38,7 @@ namespace Avalonia.Native internal GlContext MainContext { get; } public IGlContext PrimaryContext => MainContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public bool CanShareContexts => true; public bool CanCreateContexts => true; diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index bb0af0151e..34de439c94 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -13,6 +13,7 @@ using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; namespace Avalonia.Native @@ -366,13 +367,18 @@ namespace Avalonia.Native public IRenderer CreateRenderer(IRenderRoot root) { + var customRendererFactory = AvaloniaLocator.Current.GetService(); + var loop = AvaloniaLocator.Current.GetService(); + if (customRendererFactory != null) + return customRendererFactory.Create(root, loop); + if (_deferredRendering) { - var loop = AvaloniaLocator.Current.GetService(); - var customRendererFactory = AvaloniaLocator.Current.GetService(); - - if (customRendererFactory != null) - return customRendererFactory.Create(root, loop); + if (AvaloniaNativePlatform.Compositor != null) + return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor) + { + RenderOnlyOnRenderThread = false + }; return new DeferredRenderer(root, loop); } diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs index 476f65a774..a6d8c1e98d 100644 --- a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Logging; +using Avalonia.Platform; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl @@ -12,6 +13,7 @@ namespace Avalonia.OpenGL.Egl public EglContext PrimaryEglContext { get; } public IGlContext PrimaryContext => PrimaryEglContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public EglPlatformOpenGlInterface(EglDisplay display) { diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index 50868db873..a52a6535da 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -1,8 +1,9 @@ using System; +using Avalonia.Platform; namespace Avalonia.OpenGL { - public interface IGlContext : IDisposable + public interface IGlContext : IPlatformGpuContext { GlVersion Version { get; } GlInterface GlInterface { get; } diff --git a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs index 5ee5df1e85..4ff7997b03 100644 --- a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs +++ b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs @@ -1,8 +1,10 @@ +using Avalonia.Platform; + namespace Avalonia.OpenGL { - public interface IPlatformOpenGlInterface + public interface IPlatformOpenGlInterface : IPlatformGpu { - IGlContext PrimaryContext { get; } + new IGlContext PrimaryContext { get; } IGlContext CreateSharedContext(); bool CanShareContexts { get; } bool CanCreateContexts { get; } diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index 6735a32ffe..0968adc799 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Logging; using Avalonia.OpenGL; +using Avalonia.Platform; namespace Avalonia.X11.Glx { @@ -14,6 +15,7 @@ namespace Avalonia.X11.Glx public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryContext); public GlxContext DeferredContext { get; private set; } public IGlContext PrimaryContext => DeferredContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public static bool TryInitialize(X11Info x11, IList glProfiles) { diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 1e5789e540..edb320d4f0 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -14,6 +14,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.X11; using Avalonia.X11.Glx; using static Avalonia.X11.XLib; @@ -29,6 +30,7 @@ namespace Avalonia.X11 public XI2Manager XI2; public X11Info Info { get; private set; } public IX11Screens X11Screens { get; private set; } + public Compositor Compositor { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } @@ -100,7 +102,13 @@ namespace Avalonia.X11 GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } - + var gl = AvaloniaLocator.Current.GetService(); + if (gl != null) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + + if (options.UseCompositor) + Compositor = new Compositor(AvaloniaLocator.Current.GetService()!, gl); + } public IntPtr DeferredDisplay { get; set; } @@ -228,6 +236,8 @@ namespace Avalonia /// public bool UseDeferredRendering { get; set; } = true; + public bool UseCompositor { get; set; } = true; + /// /// Determines whether to use IME. /// IME would be enabled by default if the current user input language is one of the following: Mandarin, Japanese, Vietnamese or Korean. diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 69761ae516..2f92448f4b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -19,6 +19,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11.Glx; using static Avalonia.X11.XLib; @@ -387,13 +388,15 @@ namespace Avalonia.X11 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); - - return _platform.Options.UseDeferredRendering ? - new DeferredRenderer(root, loop) - { - RenderOnlyOnRenderThread = true - } : - (IRenderer)new X11ImmediateRendererProxy(root, loop); + + return _platform.Options.UseDeferredRendering + ? _platform.Options.UseCompositor + ? new CompositingRenderer(root, this._platform.Compositor) + : new DeferredRenderer(root, loop) + { + RenderOnlyOnRenderThread = true + } + : (IRenderer)new X11ImmediateRendererProxy(root, loop); } void OnEvent(ref XEvent ev) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 297eef9e80..a88bfc3651 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -8,6 +8,9 @@ using Avalonia.Controls; namespace Avalonia.Markup.Parsers { + /// + /// Parser for composition expressions + /// internal class ExpressionParser { private readonly bool _enableValidation; diff --git a/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs b/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs index 1a06d47744..7b9feab2e3 100644 --- a/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs +++ b/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs @@ -12,5 +12,6 @@ namespace Avalonia.Web.Blazor public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed); public event Action? Tick; + public bool RunsInBackground => false; } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index a0f98bbbc9..a7f1d9c3e5 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; @@ -75,6 +76,12 @@ namespace Avalonia.Direct2D1.Media set { _deviceContext.Transform = value.ToDirect2D(); } } + public Matrix4x4 Transform4x4 + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + /// public void Clear(Color color) { diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs index b948495b99..1d0880a468 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs @@ -2,12 +2,14 @@ using System; using System.Linq; using Avalonia.Logging; using Avalonia.OpenGL; +using Avalonia.Platform; namespace Avalonia.Win32.OpenGl { class WglPlatformOpenGlInterface : IPlatformOpenGlInterface { public WglContext PrimaryContext { get; } + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; IGlContext IPlatformOpenGlInterface.PrimaryContext => PrimaryContext; public IGlContext CreateSharedContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, PrimaryContext); diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 2a3f4a3384..39a742d1ac 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,6 +1,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; +using Avalonia.Platform; using Avalonia.Win32.OpenGl; using Avalonia.Win32.WinRT.Composition; @@ -9,45 +10,53 @@ namespace Avalonia.Win32 static class Win32GlManager { - public static void Initialize() + public static IPlatformOpenGlInterface Initialize() { - AvaloniaLocator.CurrentMutable.Bind().ToLazy(() => + var gl = InitializeCore(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + return gl; + } + + static IPlatformOpenGlInterface InitializeCore() + { + + var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); + if (opts.UseWgl) { - var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); - if (opts.UseWgl) - { - var wgl = WglPlatformOpenGlInterface.TryCreate(); - return wgl; - } + var wgl = WglPlatformOpenGlInterface.TryCreate(); + return wgl; + } - if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) - { - var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); + if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) + { + var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - if (egl != null) + if (egl != null) + { + if (opts.EglRendererBlacklist != null) { - if (opts.EglRendererBlacklist != null) + foreach (var item in opts.EglRendererBlacklist) { - foreach (var item in opts.EglRendererBlacklist) + if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) { - if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) - { - return null; - } + return null; } } - - if (opts.UseWindowsUIComposition) - { - WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); - } } - return egl; + if (opts.UseWindowsUIComposition) + { + WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); + } } - return null; - }); + return egl; + } + + return null; } + + } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 63bb921125..73ef50052c 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -14,6 +14,7 @@ using Avalonia.Input.Platform; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; @@ -49,6 +50,8 @@ namespace Avalonia /// public bool UseDeferredRendering { get; set; } = true; + public bool UseCompositor { get; set; } = true; + /// /// Enables ANGLE for Windows. For every Windows version that is above Windows 7, the default is true otherwise it's false. /// @@ -133,6 +136,8 @@ namespace Avalonia.Win32 public static bool UseDeferredRendering => Options.UseDeferredRendering; internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } + + internal static Compositor Compositor { get; private set; } public Size DoubleClickSize => new Size( UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK), @@ -175,12 +180,15 @@ namespace Avalonia.Win32 .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()) .Bind().ToConstant(s_instance); - Win32GlManager.Initialize(); + var gl = Win32GlManager.Initialize(); _uiThread = Thread.CurrentThread; if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + + if (Options.UseCompositor) + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), gl); } public bool HasMessages() diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 76af12e8ca..5334b90d62 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -302,5 +302,6 @@ namespace Avalonia.Win32.WinRT.Composition } public event Action Tick; + public bool RunsInBackground => true; } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 259f5c996b..2f1a116af7 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -15,6 +15,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Win32.Automation; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; @@ -543,6 +544,9 @@ namespace Avalonia.Win32 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); + if (Win32Platform.Compositor != null) + return new CompositingRenderer(root, Win32Platform.Compositor); + return Win32Platform.UseDeferredRendering ? _isUsingComposition ? new DeferredRenderer(root, loop) diff --git a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs index df73479a65..eb124fd450 100644 --- a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs +++ b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs @@ -28,6 +28,8 @@ namespace Avalonia.iOS } public Thread TimerThread { get; } + + public bool RunsInBackground => true; private void OnLinkTick() { diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index bd1969081d..906bbc29e7 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using Avalonia.OpenGL; +using Avalonia.Platform; using OpenGLES; namespace Avalonia.iOS @@ -9,6 +10,7 @@ namespace Avalonia.iOS { public IGlContext PrimaryContext => Context; public IGlContext CreateSharedContext() => throw new NotSupportedException(); + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public bool CanShareContexts => false; public bool CanCreateContexts => false; public IGlContext CreateContext() => throw new System.NotSupportedException(); diff --git a/src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs b/src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs new file mode 100644 index 0000000000..72311b4d18 --- /dev/null +++ b/src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs @@ -0,0 +1,23 @@ +using System.IO; +using System.Xml.Serialization; +using Microsoft.CodeAnalysis; + +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + [Generator(LanguageNames.CSharp)] + public class CompositionRoslynGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var schema = + context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); + var configs = schema.Select((t, _) => + (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); + context.RegisterSourceOutput(configs, (spc, config) => + { + var generator = new Generator(new RoslynCompositionGeneratorSink(spc), config); + generator.Generate(); + }); + } + } +} \ No newline at end of file diff --git a/src/tools/DevGenerators/CompositionGenerator/Config.cs b/src/tools/DevGenerators/CompositionGenerator/Config.cs new file mode 100644 index 0000000000..d1fc691a8b --- /dev/null +++ b/src/tools/DevGenerators/CompositionGenerator/Config.cs @@ -0,0 +1,124 @@ +#nullable disable +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + + + [XmlRoot("NComposition")] + public class GConfig + { + [XmlElement("Using")] + public List Usings { get; set; } = new List(); + + [XmlElement(typeof(GManualClass), ElementName = "Manual")] + public List ManualClasses { get; set; } = new List(); + + [XmlElement(typeof(GClass), ElementName = "Object")] + [XmlElement(typeof(GBrush), ElementName = "Brush")] + [XmlElement(typeof(GList), ElementName = "List")] + public List Classes { get; set; } = new List(); + + [XmlElement(typeof(GAnimationType), ElementName = "KeyFrameAnimation")] + public List KeyFrameAnimations { get; set; } = new List(); + } + + public class GUsing + { + [XmlText] + public string Name { get; set; } + } + + public class GManualClass + { + [XmlAttribute] + public string Name { get; set; } + + + [XmlAttribute] + public bool Passthrough { get; set; } + + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GImplements + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GClass + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string Inherits { get; set; } + + [XmlAttribute] + public string ChangesBase { get; set; } + + [XmlAttribute] + public string ServerBase { get; set; } + + [XmlAttribute] + public bool CustomCtor { get; set; } + + [XmlAttribute] + public bool CustomServerCtor { get; set; } + + [XmlElement(typeof(GImplements), ElementName = "Implements")] + public List Implements { get; set; } = new List(); + + [XmlAttribute] + public bool Abstract { get; set; } + + [XmlElement(typeof(GProperty), ElementName = "Property")] + public List Properties { get; set; } = new List(); + } + + public class GBrush : GClass + { + [XmlAttribute] + public bool CustomUpdate { get; set; } + + public GBrush() + { + Inherits = "CompositionBrush"; + } + } + + public class GList : GClass + { + [XmlAttribute] + public string ItemType { get; set; } + } + + public class GProperty + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + [XmlAttribute] + public string DefaultValue { get; set; } + [XmlAttribute] + public bool Animated { get; set; } + [XmlAttribute] + public bool InternalSet { get; set; } + [XmlAttribute] + public bool Internal { get; set; } + } + + public class GAnimationType + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/tools/DevGenerators/CompositionGenerator/Extensions.cs b/src/tools/DevGenerators/CompositionGenerator/Extensions.cs new file mode 100644 index 0000000000..d88e9b4600 --- /dev/null +++ b/src/tools/DevGenerators/CompositionGenerator/Extensions.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public static class Extensions + { + public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static EnumDeclarationSyntax AddModifiers(this EnumDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + + + public static string WithLowerFirst(this string s) + { + if (string.IsNullOrEmpty(s)) + return s; + return char.ToLowerInvariant(s[0]) + s.Substring(1); + } + + public static ExpressionSyntax MemberAccess(params string[] identifiers) + { + if (identifiers == null || identifiers.Length == 0) + throw new ArgumentException(); + var expr = (ExpressionSyntax)IdentifierName(identifiers[0]); + for (var c = 1; c < identifiers.Length; c++) + expr = MemberAccess(expr, identifiers[c]); + return expr; + } + + public static ExpressionSyntax MemberAccess(ExpressionSyntax expr, params string[] identifiers) + { + foreach (var i in identifiers) + expr = MemberAccess(expr, i); + return expr; + } + + public static MemberAccessExpressionSyntax MemberAccess(ExpressionSyntax expr, string identifier) => + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, expr, IdentifierName(identifier)); + + public static ExpressionSyntax ConditionalMemberAccess(ExpressionSyntax expr, string member, bool checkNull) + { + if (checkNull) + return ConditionalAccessExpression(expr, MemberBindingExpression(IdentifierName(member))); + return MemberAccess(expr, member); + } + + public static ClassDeclarationSyntax WithBaseType(this ClassDeclarationSyntax cl, string bt) + { + return cl.AddBaseListTypes(SimpleBaseType(SyntaxFactory.ParseTypeName(bt))); + } + + public static string StripPrefix(this string s, string prefix) => string.IsNullOrEmpty(s) + ? s + : s.StartsWith(prefix) + ? s.Substring(prefix.Length) + : s; + } +} \ No newline at end of file diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.KeyFrameAnimation.cs new file mode 100644 index 0000000000..7ad40f68e4 --- /dev/null +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.KeyFrameAnimation.cs @@ -0,0 +1,59 @@ +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + void GenerateAnimations() + { + var code = $@"using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition +{{ +"; + + foreach (var a in _config.KeyFrameAnimations) + { + var name = a.Name ?? a.Type; + + code += $@" + public class {name}KeyFrameAnimation : KeyFrameAnimation + {{ + public {name}KeyFrameAnimation(Compositor compositor) : base(compositor) + {{ + }} + + internal override IAnimationInstance CreateInstance(Avalonia.Rendering.Composition.Server.ServerObject targetObject, ExpressionVariant? finalValue) + {{ + return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(), + finalValue?.CastOrDefault<{a.Type}>(), targetObject, + DelayBehavior, DelayTime, Direction, Duration, IterationBehavior, + IterationCount, StopBehavior); + }} + + private KeyFrames<{a.Type}> _keyFrames = new KeyFrames<{a.Type}>(); + private protected override IKeyFrames KeyFrames => _keyFrames; + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value, Avalonia.Animation.Easings.IEasing easingFunction) + {{ + _keyFrames.Insert(normalizedProgressKey, value, easingFunction); + }} + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value) + {{ + _keyFrames.Insert(normalizedProgressKey, value, Compositor.DefaultEasing); + }} + }} + + public partial class Compositor + {{ + public {name}KeyFrameAnimation Create{name}KeyFrameAnimation() => new {name}KeyFrameAnimation(this); + }} +"; + } + + code += "}"; + _output.AddSource("CompositionAnimations.cs", code); + } + } +} \ No newline at end of file diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs new file mode 100644 index 0000000000..e0ea5b20ae --- /dev/null +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs @@ -0,0 +1,123 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + private const string ListProxyTemplate = @" +class Template +{ + private ServerListProxyHelper _list = null!; + + void ServerListProxyHelper.IRegisterForSerialization.RegisterForSerialization() => RegisterForSerialization(); + + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _list).GetEnumerator(); + + public void Add(ItemTypeName item) + { + OnBeforeAdded(item); + _list.Add(item); + OnAdded(item); + } + + public void Clear() + { + OnBeforeClear(); + _list.Clear(); + OnClear(); + } + + public bool Contains(ItemTypeName item) => _list.Contains(item); + + public void CopyTo(ItemTypeName[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public bool Remove(ItemTypeName item) + { + var removed = _list.Remove(item); + if(removed) + OnRemoved(item); + return removed; + } + + public int Count => _list.Count; + + public bool IsReadOnly => _list.IsReadOnly; + + public int IndexOf(ItemTypeName item) => _list.IndexOf(item); + + public void Insert(int index, ItemTypeName item) + { + OnBeforeAdded(item); + _list.Insert(index, item); + OnAdded(item); + } + + public void RemoveAt(int index) + { + var item = _list[index]; + _list.RemoveAt(index); + OnRemoved(item); + } + + public ItemTypeName this[int index] + { + get => _list[index]; + set + { + var old = _list[index]; + OnBeforeReplace(old, value); + _list[index] = value; + OnReplace(old, value); + } + } + + partial void OnBeforeAdded(ItemTypeName item); + partial void OnAdded(ItemTypeName item); + partial void OnRemoved(ItemTypeName item); + partial void OnBeforeClear(); + partial void OnBeforeReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnClear(); + private protected override void SerializeChangesCore(BatchStreamWriter writer) + {{ + _list.Serialize(writer); + base.SerializeChangesCore(writer); + }} +"; + + private ClassDeclarationSyntax AppendListProxy(GList list, ClassDeclarationSyntax cl) + { + + var itemType = list.ItemType; + var serverItemType = ServerName(itemType); + + cl = cl.AddBaseListTypes(SimpleBaseType( + ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IRegisterForSerialization")), + SimpleBaseType(ParseTypeName("IList<" + itemType + ">")) + ); + var code = ListProxyTemplate.Replace("ListTypeName", list.Name) + .Replace("ItemTypeName", itemType); + + var parsed = ParseCompilationUnit(code); + var parsedClass = (ClassDeclarationSyntax)parsed.Members.First(); + + cl = cl.AddMembers(parsedClass.Members.ToArray()); + + var defs = cl.Members.OfType().First(m => m.Identifier.Text == "InitializeDefaults"); + + cl = cl.ReplaceNode(defs.Body, defs.Body.AddStatements( + + ParseStatement($"_list = new ServerListProxyHelper<{itemType}, {serverItemType}>(this);"))); + + return cl; + } + + } +} \ No newline at end of file diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs new file mode 100644 index 0000000000..b53c247991 --- /dev/null +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs @@ -0,0 +1,66 @@ +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + static void CleanDirectory(string path) + { + Directory.CreateDirectory(path); + Directory.Delete(path, true); + Directory.CreateDirectory(path); + } + + CompilationUnitSyntax Unit() + => CompilationUnit().WithUsings(List(new[] + { + "System", + "System.Text", + "System.Collections", + "System.Collections.Generic" + } + .Concat(_config.Usings + .Select(x => x.Name)).Select(u => UsingDirective(IdentifierName(u))))); + + void SaveTo(CompilationUnitSyntax unit, params string[] path) + { + var text = @" +#nullable enable +#pragma warning disable CS0108, CS0114 + +" + + + unit.NormalizeWhitespace().ToFullString(); + _output.AddSource(string.Join("_", path), text); + } + + + SyntaxToken Semicolon() => Token(SyntaxKind.SemicolonToken); + + + FieldDeclarationSyntax DeclareConstant(string type, string name, LiteralExpressionSyntax value) + => FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(EqualsValueClause(value)) + )) + ).WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.ConstKeyword))); + + FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) => + DeclareField(type, name, null, modifiers); + + FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax initializer, + params SyntaxKind[] modifiers) => + FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(initializer)))) + .WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(modifiers.Select(x => Token(x)))); + } +} \ No newline at end of file diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs new file mode 100644 index 0000000000..18f1d1c1e5 --- /dev/null +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -0,0 +1,569 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Avalonia.SourceGenerator.CompositionGenerator.Extensions; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + private readonly ICompositionGeneratorSink _output; + private readonly GConfig _config; + private readonly HashSet _objects; + private readonly HashSet _brushes; + private readonly Dictionary _manuals; + public Generator(ICompositionGeneratorSink output, GConfig config) + { + _output = output; + _config = config; + _manuals = _config.ManualClasses.ToDictionary(x => x.Name); + _objects = new HashSet(_config.ManualClasses.Select(x => x.Name) + .Concat(_config.Classes.Select(x => x.Name))); + _brushes = new HashSet(_config.Classes.OfType().Select(x => x.Name)) {"CompositionBrush"}; + } + + + + public void Generate() + { + foreach (var cl in _config.Classes) + GenerateClass(cl); + + GenerateAnimations(); + } + + + + string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject"; + string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet"; + string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields"; + string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name; + string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst(); + string CompositionPropertyField(GProperty prop) => "s_IdOf" + prop.Name + "Property"; + + ExpressionSyntax ClientProperty(GClass c, GProperty p) => + MemberAccess(ServerName(c.Name), CompositionPropertyField(p)); + + void GenerateClass(GClass cl) + { + var list = cl as GList; + + var unit = Unit(); + + var clientNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition")); + var serverNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Server")); + var transportNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Transport")); + + var inherits = cl.Inherits ?? "CompositionObject"; + var abstractModifier = cl.Abstract ? new[] {SyntaxKind.AbstractKeyword} : null; + + var client = ClassDeclaration(cl.Name) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(inherits); + + var serverName = ServerName(cl.Name); + var serverBase = cl.ServerBase ?? ServerName(cl.Inherits); + if (list != null) + serverBase = "ServerList<" + ServerName(list.ItemType) + ">"; + + var server = ClassDeclaration(serverName) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(serverBase); + + string changesName = ChangesName(cl.Name); + string changedFieldsTypeName = ChangedFieldsTypeName(cl); + string changedFieldsName = ChangedFieldsFieldName(cl); + + if (cl.Properties.Count > 0) + client = client + .AddMembers(DeclareField(changedFieldsTypeName, changedFieldsName)); + + + if (!cl.CustomCtor) + { + client = client.AddMembers(PropertyDeclaration(ParseTypeName(serverName), "Server") + .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword) + .AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Semicolon()))); + client = client.AddMembers( + ConstructorDeclaration(cl.Name) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("Compositor")), + Parameter(Identifier("server")).WithType(ParseTypeName(serverName)), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + Argument(IdentifierName("server")), + })))).WithBody(Block( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("Server"), + CastExpression(ParseTypeName(serverName), IdentifierName("server")))), + ExpressionStatement(InvocationExpression(IdentifierName("InitializeDefaults"))) + ))); + } + + if (!cl.CustomServerCtor) + { + server = server.AddMembers( + ConstructorDeclaration(serverName) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("ServerCompositor")), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + })))).WithBody(Block(ParseStatement("Initialize();")))); + } + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "Initialize") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + + var changesVarName = "c"; + var changesVar = IdentifierName(changesVarName); + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "DeserializeChangesExtra") + .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader"))) + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + var resetBody = Block(); + var startAnimationBody = Block(); + var serverGetPropertyBody = Block(); + var serverGetCompositionPropertyBody = Block(); + var serializeMethodBody = SerializeChangesPrologue(cl); + var deserializeMethodBody = DeserializeChangesPrologue(cl); + + var defaultsMethodBody = Block(ParseStatement("InitializeDefaultsExtra();")); + + foreach (var prop in cl.Properties) + { + var fieldName = PropertyBackingFieldName(prop); + var propType = ParseTypeName(prop.Type); + var filteredPropertyType = prop.Type.TrimEnd('?'); + var isObject = _objects.Contains(filteredPropertyType); + var isNullable = prop.Type.EndsWith("?"); + bool isPassthrough = false; + + client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable); + + var animatedServer = prop.Animated; + + var serverPropertyType = ((isObject ? "Server" : "") + prop.Type); + if (_manuals.TryGetValue(filteredPropertyType, out var manual)) + { + if (manual.Passthrough) + { + isPassthrough = true; + serverPropertyType = prop.Type; + } + + if (manual.ServerName != null) + serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); + } + + if (animatedServer) + server = server.AddMembers( + DeclareField(serverPropertyType, fieldName), + PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression(IdentifierName("GetAnimatedValue"), + ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName(CompositionPropertyField(prop))), + Argument(null, Token(SyntaxKind.RefKeyword), IdentifierName(fieldName)) + } + ))))) + .WithSemicolonToken(Semicolon()) + ); + else + { + server = server + .AddMembers(DeclareField(serverPropertyType, fieldName)) + .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement( + InvocationExpression(IdentifierName("GetValue"), + ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName(CompositionPropertyField(prop))), + Argument(null, Token(SyntaxKind.RefKeyword), IdentifierName(fieldName)) + } + )))))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement($"changed = true;")) + ), + ExpressionStatement(InvocationExpression(IdentifierName("SetValue"), + ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName(CompositionPropertyField(prop))), + Argument(null, Token(SyntaxKind.OutKeyword), IdentifierName(fieldName)), + Argument(IdentifierName("value")) + } + )))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + } + + resetBody = resetBody.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); + + serializeMethodBody = ApplySerializeField(serializeMethodBody,cl, prop, isObject, isPassthrough); + deserializeMethodBody = ApplyDeserializeField(deserializeMethodBody,cl, prop, serverPropertyType, isObject); + + if (animatedServer) + { + startAnimationBody = ApplyStartAnimation(startAnimationBody, cl, prop); + } + + + serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); + serverGetCompositionPropertyBody = ApplyGetProperty(serverGetCompositionPropertyBody, prop, CompositionPropertyField(prop)); + + server = server.AddMembers(DeclareField("CompositionProperty", CompositionPropertyField(prop), + EqualsValueClause(ParseExpression("CompositionProperty.Register()")), + SyntaxKind.InternalKeyword, SyntaxKind.StaticKeyword)); + + if (prop.DefaultValue != null) + { + defaultsMethodBody = defaultsMethodBody.AddStatements( + ExpressionStatement( + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(prop.Name), ParseExpression(prop.DefaultValue)))); + } + } + + if (cl.Properties.Count > 0) + { + server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt){{}}") + !) + .WithBody(ApplyDeserializeChangesEpilogue(deserializeMethodBody, cl))); + server = server.AddMembers(MethodDeclaration(ParseTypeName("void"), "OnFieldsDeserialized") + .WithParameterList(ParameterList(SingletonSeparatedList(Parameter(Identifier("changed")) + .WithType(ParseTypeName(ChangedFieldsTypeName(cl)))))) + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + } + + client = client.AddMembers( + MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)) + .AddMembers( + MethodDeclaration(ParseTypeName("void"), "InitializeDefaultsExtra") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + if (cl.Properties.Count > 0) + { + serializeMethodBody = serializeMethodBody.AddStatements(SerializeChangesEpilogue(cl)); + client = client.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"private protected override void SerializeChangesCore(BatchStreamWriter writer){{}}")!) + .WithBody(serializeMethodBody)); + } + + if (list != null) + client = AppendListProxy(list, client); + + if (startAnimationBody.Statements.Count != 0) + client = WithStartAnimation(client, startAnimationBody); + + server = WithGetPropertyForAnimation(server, serverGetPropertyBody); + server = WithGetCompositionProperty(server, serverGetCompositionPropertyBody); + + if(cl.Implements.Count > 0) + foreach (var impl in cl.Implements) + { + client = client.WithBaseList(client.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.Name)))); + if (impl.ServerName != null) + server = server.WithBaseList( + server.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName)))); + + client = client.AddMembers( + ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;")); + } + + + SaveTo(unit.AddMembers(GenerateChangedFieldsEnum(cl)), "Transport", + ChangedFieldsTypeName(cl) + ".generated.cs"); + + SaveTo(unit.AddMembers(clientNs.AddMembers(client)), + cl.Name + ".generated.cs"); + SaveTo(unit.AddMembers(serverNs.AddMembers(server)), + "Server", "Server" + cl.Name + ".generated.cs"); + } + + private ClassDeclarationSyntax GenerateClientProperty(ClassDeclarationSyntax client, GClass cl, GProperty prop, + TypeSyntax propType, bool isObject, bool isNullable) + { + var fieldName = PropertyBackingFieldName(prop); + return client + .AddMembers(DeclareField(prop.Type, fieldName)) + .AddMembers(PropertyDeclaration(propType, prop.Name) + .AddModifiers(prop.Internal ? SyntaxKind.InternalKeyword : SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement("changed = true;"), + GeneratePropertySetterAssignment(cl, prop, isObject, isNullable)) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )).WithModifiers(TokenList(prop.InternalSet ? new[]{Token(SyntaxKind.InternalKeyword)} : Array.Empty())) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + } + + EnumDeclarationSyntax GenerateChangedFieldsEnum(GClass cl) + { + var changedFieldsEnum = EnumDeclaration(Identifier(ChangedFieldsTypeName(cl))); + int count = 0; + + void AddValue(string name) + { + var value = 1ul << count; + changedFieldsEnum = changedFieldsEnum.AddMembers( + EnumMemberDeclaration(name) + .WithEqualsValue(EqualsValueClause(ParseExpression(value.ToString())))); + count++; + } + + foreach (var prop in cl.Properties) + { + AddValue(prop.Name); + + if (prop.Animated) + AddValue(prop.Name + "Animated"); + } + + var baseType = count <= 8 ? "byte" : count <= 16 ? "ushort" : count <= 32 ? "uint" : "ulong"; + return changedFieldsEnum.AddBaseListTypes(SimpleBaseType(ParseTypeName(baseType))) + .AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("System.Flags"))))); + } + + StatementSyntax GeneratePropertySetterAssignment(GClass cl, GProperty prop, bool isObject, bool isNullable) + { + var code = @$" + // Update the backing value + {PropertyBackingFieldName(prop)} = value; + + // Register object for serialization in the next batch + {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}; + RegisterForSerialization(); +"; + if (prop.Animated) + { + code += @$" + // Reset previous animation if any + PendingAnimations.Remove({ClientProperty(cl, prop)}); + {ChangedFieldsFieldName(cl)} &= ~{ChangedFieldsTypeName(cl)}.{prop.Name}Animated; + // Check for implicit animations + if(ImplicitAnimations != null && ImplicitAnimations.TryGetValue(""{prop.Name}"", out var animation) == true) + {{ + // Animation affects only current property + if(animation is CompositionAnimation a) + {{ + {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; + PendingAnimations[{ClientProperty(cl, prop)}] = a.CreateInstance(this.Server, value); + }} + // Animation is triggered by the current field, but does not necessary affects it + StartAnimationGroup(animation, ""{prop.Name}"", value); + }} +"; + } + + return ParseStatement("{\n" + code + "\n}"); + } + + BlockSyntax ApplyStartAnimation(BlockSyntax body, GClass cl, GProperty prop) + { + var code = $@" +if (propertyName == ""{prop.Name}"") +{{ +var current = {PropertyBackingFieldName(prop)}; +var server = animation.CreateInstance(this.Server, finalValue); +PendingAnimations[{ClientProperty(cl, prop)}] = server; +{ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; +RegisterForSerialization(); +return; +}} +"; + return body.AddStatements(ParseStatement(code)); + } + + private static HashSet VariantPropertyTypes = new HashSet + { + "bool", + "float", + "Vector2", + "Vector3", + "Vector4", + "Matrix", + "Matrix3x2", + "Matrix4x4", + "Quaternion", + "Color", + "Avalonia.Media.Color" + }; + + BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop, string? expr = null) + { + if (VariantPropertyTypes.Contains(prop.Type)) + return body.AddStatements( + ParseStatement($"if(name == \"{prop.Name}\")\n return {expr ?? prop.Name};\n") + ); + + return body; + } + + private BlockSyntax SerializeChangesPrologue(GClass cl) + { + return Block( + ParseStatement("base.SerializeChangesCore(writer);"), + ParseStatement($"writer.Write({ChangedFieldsFieldName(cl)});") + ); + } + + private BlockSyntax SerializeChangesEpilogue(GClass cl) => + Block(ParseStatement(ChangedFieldsFieldName(cl) + " = default;")); + + BlockSyntax ApplySerializeField(BlockSyntax body, GClass cl, GProperty prop, bool isObject, bool isPassthrough) + { + var changedFields = ChangedFieldsFieldName(cl); + var changedFieldsType = ChangedFieldsTypeName(cl); + + var code = ""; + if (prop.Animated) + { + code = $@" + if(({changedFields} & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) + writer.WriteObject(PendingAnimations.GetAndRemove({ClientProperty(cl, prop)})); + else "; + } + + code += $@" + if(({changedFields} & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) + writer.Write{(isObject ? "Object" : "")}({PropertyBackingFieldName(prop)}{(isObject && !isPassthrough ? "?.Server!":"")}); +"; + return body.AddStatements(ParseStatement(code)); + } + + private BlockSyntax DeserializeChangesPrologue(GClass cl) + { + return Block(ParseStatement($@" +base.DeserializeChangesCore(reader, commitedAt); +DeserializeChangesExtra(reader); +var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); +")); + } + + private BlockSyntax ApplyDeserializeChangesEpilogue(BlockSyntax body, GClass cl) + { + return body.AddStatements(ParseStatement("OnFieldsDeserialized(changed);")); + } + + BlockSyntax ApplyDeserializeField(BlockSyntax body, GClass cl, GProperty prop, string serverType, bool isObject) + { + var changedFieldsType = ChangedFieldsTypeName(cl); + var code = ""; + if (prop.Animated) + { + code = $@" + if((changed & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) + SetAnimatedValue({CompositionPropertyField(prop)}, ref {PropertyBackingFieldName(prop)}, commitedAt, reader.ReadObject()); + else "; + } + + var readValueCode = $"reader.Read{(isObject ? "Object" : "")}<{serverType}>()"; + code += $@" + if((changed & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) +"; + if (prop.Animated) + code += $"SetAnimatedValue({CompositionPropertyField(prop)}, out {PropertyBackingFieldName(prop)}, {readValueCode});"; + else code += $"{prop.Name} = {readValueCode};"; + return body.AddStatements(ParseStatement(code)); + } + + ClassDeclarationSyntax WithGetPropertyForAnimation(ClassDeclarationSyntax cl, BlockSyntax body) + { + if (body.Statements.Count == 0) + return cl; + body = body.AddStatements( + ParseStatement("return base.GetPropertyForAnimation(name);")); + var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( + $"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) + .WithBody(body); + + return cl.AddMembers(method); + } + + ClassDeclarationSyntax WithGetCompositionProperty(ClassDeclarationSyntax cl, BlockSyntax body) + { + if (body.Statements.Count == 0) + return cl; + body = body.AddStatements( + ParseStatement("return base.GetCompositionProperty(name);")); + var method = ((MethodDeclarationSyntax)ParseMemberDeclaration( + $"public override CompositionProperty? GetCompositionProperty(string name){{}}")) + .WithBody(body); + + return cl.AddMembers(method); + } + + ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body) + { + body = body.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess("base", "StartAnimation"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("propertyName")), + Argument(IdentifierName("animation")), + Argument(IdentifierName("finalValue")), + })))) + ); + return cl.AddMembers( + ((MethodDeclarationSyntax) ParseMemberDeclaration( + "internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}")) + .WithBody(body)); + + + } + + } +} \ No newline at end of file diff --git a/src/tools/DevGenerators/CompositionGenerator/ICompositionGeneratorSink.cs b/src/tools/DevGenerators/CompositionGenerator/ICompositionGeneratorSink.cs new file mode 100644 index 0000000000..085a4041be --- /dev/null +++ b/src/tools/DevGenerators/CompositionGenerator/ICompositionGeneratorSink.cs @@ -0,0 +1,6 @@ +namespace Avalonia.SourceGenerator.CompositionGenerator; + +public interface ICompositionGeneratorSink +{ + void AddSource(string name, string code); +} \ No newline at end of file diff --git a/src/tools/DevGenerators/CompositionGenerator/RoslynCompositionGeneratorSink.cs b/src/tools/DevGenerators/CompositionGenerator/RoslynCompositionGeneratorSink.cs new file mode 100644 index 0000000000..6fec3faf93 --- /dev/null +++ b/src/tools/DevGenerators/CompositionGenerator/RoslynCompositionGeneratorSink.cs @@ -0,0 +1,15 @@ +using Microsoft.CodeAnalysis; + +namespace Avalonia.SourceGenerator.CompositionGenerator; + +class RoslynCompositionGeneratorSink : ICompositionGeneratorSink +{ + private readonly SourceProductionContext _ctx; + + public RoslynCompositionGeneratorSink(SourceProductionContext ctx) + { + _ctx = ctx; + } + + public void AddSource(string name, string code) => _ctx.AddSource(name, code); +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/tools/DevGenerators/DevGenerators.csproj similarity index 74% rename from src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj rename to src/tools/DevGenerators/DevGenerators.csproj index b5c955a8a6..30da940514 100644 --- a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj +++ b/src/tools/DevGenerators/DevGenerators.csproj @@ -3,6 +3,7 @@ netstandard2.0 enable + false @@ -11,8 +12,8 @@ all - - + + diff --git a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs b/src/tools/DevGenerators/SubtypesFactoryGenerator.cs similarity index 100% rename from src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs rename to src/tools/DevGenerators/SubtypesFactoryGenerator.cs diff --git a/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs b/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs new file mode 100644 index 0000000000..a1b55257e6 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks.Dataflow; +using Avalonia.Rendering.Composition.Transport; +using Xunit; + +namespace Avalonia.Base.UnitTests.Composition; + +public class BatchStreamTests +{ + [Fact] + public void BatchStreamCorrectlyWritesAndReadsData() + { + var data = new BatchStreamData(); + var memPool = new BatchStreamMemoryPool(100, _ => { }); + var objPool = new BatchStreamObjectPool(10, _ => { }); + + var guids = new List(); + var objects = new List(); + for (var c = 0; c < 453; c++) + { + guids.Add(Guid.NewGuid()); + objects.Add(new object()); + } + + using (var writer = new BatchStreamWriter(data, memPool, objPool)) + { + foreach(var guid in guids) + writer.Write(guid); + foreach (var obj in objects) + writer.WriteObject(obj); + } + + using (var reader = new BatchStreamReader(data, memPool, objPool)) + { + foreach (var guid in guids) + Assert.Equal(guid, reader.Read()); + foreach (var obj in objects) + Assert.Equal(obj, reader.ReadObject()); + } + + + + } +} \ No newline at end of file diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 15ca78fdac..ecbdd5bade 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -137,7 +137,7 @@ namespace Avalonia.IntegrationTests.Appium { var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - using (OpenWindow(new PixelSize(1400, 100), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) + using (OpenWindow(new PixelSize(800, 100), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) { mainWindow.Click(); diff --git a/tests/Avalonia.RenderTests/ManualRenderTimer.cs b/tests/Avalonia.RenderTests/ManualRenderTimer.cs new file mode 100644 index 0000000000..0dc994aaa5 --- /dev/null +++ b/tests/Avalonia.RenderTests/ManualRenderTimer.cs @@ -0,0 +1,19 @@ +using Avalonia.Rendering; +using System.Threading.Tasks; +using System; + + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests +#endif +{ + public class ManualRenderTimer : IRenderTimer + { + public event Action Tick; + public bool RunsInBackground => false; + public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero); + public Task TriggerBackgroundTick() => Task.Run(TriggerTick); + } +} \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 39250f2aa7..4d6b313ffc 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -8,8 +8,13 @@ using Xunit; using Avalonia.Platform; using System.Threading.Tasks; using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; using System.Threading; using Avalonia.Media; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; @@ -38,7 +43,7 @@ namespace Avalonia.Direct2D1.RenderTests new TestThreadingInterface(); private static readonly IAssetLoader assetLoader = new AssetLoader(); - + static TestBase() { #if AVALONIA_SKIA @@ -84,6 +89,7 @@ namespace Avalonia.Direct2D1.RenderTests var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png"); var deferredPath = Path.Combine(OutputPath, testName + ".deferred.out.png"); + var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png"); var factory = AvaloniaLocator.Current.GetService(); var pixelSize = new PixelSize((int)target.Width, (int)target.Height); var size = new Size(target.Width, target.Height); @@ -96,7 +102,8 @@ namespace Avalonia.Direct2D1.RenderTests bitmap.Render(target); bitmap.Save(immediatePath); } - + + using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpiVector)) using (var renderer = new DeferredRenderer(target, rtb)) { @@ -107,9 +114,30 @@ namespace Avalonia.Direct2D1.RenderTests // Do the deferred render on a background thread to expose any threading errors in // the deferred rendering path. await Task.Run((Action)renderer.UnitTestRender); + threadingInterface.MainThread = Thread.CurrentThread; rtb.Save(deferredPath); } + + var timer = new ManualRenderTimer(); + + var compositor = new Compositor(new RenderLoop(timer, Dispatcher.UIThread), null); + using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpiVector)) + { + var root = new TestRenderRoot(dpiVector.X / 96, rtb); + using (var renderer = new CompositingRenderer(root, compositor) { RenderOnlyOnRenderThread = false}) + { + root.Initialize(renderer, target); + renderer.Start(); + Dispatcher.UIThread.RunJobs(); + timer.TriggerTick(); + } + + // Free pools + for (var c = 0; c < 11; c++) + TestThreadingInterface.RunTimers(); + rtb.Save(compositedPath); + } } protected void CompareImages([CallerMemberName] string testName = "") @@ -117,13 +145,16 @@ namespace Avalonia.Direct2D1.RenderTests var expectedPath = Path.Combine(OutputPath, testName + ".expected.png"); var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png"); var deferredPath = Path.Combine(OutputPath, testName + ".deferred.out.png"); + var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png"); using (var expected = Image.Load(expectedPath)) using (var immediate = Image.Load(immediatePath)) using (var deferred = Image.Load(deferredPath)) + using (var composited = Image.Load(compositedPath)) { var immediateError = CompareImages(immediate, expected); var deferredError = CompareImages(deferred, expected); + var compositedError = CompareImages(composited, expected); if (immediateError > 0.022) { @@ -134,6 +165,11 @@ namespace Avalonia.Direct2D1.RenderTests { Assert.True(false, deferredPath + ": Error = " + deferredError); } + + if (compositedError > 0.022) + { + Assert.True(false, compositedPath + ": Error = " + compositedError); + } } } @@ -233,9 +269,25 @@ namespace Avalonia.Direct2D1.RenderTests // No-op } + private static List s_timers = new(); + + public static void RunTimers() + { + lock (s_timers) + { + foreach(var t in s_timers.ToList()) + t.Invoke(); + } + } + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { - throw new NotImplementedException(); + var act = () => tick(); + lock (s_timers) s_timers.Add(act); + return Disposable.Create(() => + { + lock (s_timers) s_timers.Remove(act); + }); } } } diff --git a/tests/Avalonia.RenderTests/TestRenderRoot.cs b/tests/Avalonia.RenderTests/TestRenderRoot.cs new file mode 100644 index 0000000000..8f2b324d9c --- /dev/null +++ b/tests/Avalonia.RenderTests/TestRenderRoot.cs @@ -0,0 +1,48 @@ +using Avalonia.Rendering; +using System.Threading.Tasks; +using System; +using Avalonia.Controls; +using Avalonia.Platform; + + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests +#endif +{ + public class TestRenderRoot : Decorator, IRenderRoot + { + private readonly IRenderTarget _renderTarget; + public Size ClientSize { get; private set; } + public IRenderer Renderer { get; private set; } + public double RenderScaling { get; } + + public TestRenderRoot(double scaling, IRenderTarget renderTarget) + { + _renderTarget = renderTarget; + RenderScaling = scaling; + } + + public void Initialize(IRenderer renderer, Control child) + { + Renderer = renderer; + Child = child; + Width = child.Width; + Height = child.Height; + ClientSize = new Size(Width, Height); + Measure(ClientSize); + Arrange(new Rect(ClientSize)); + } + + public IRenderTarget CreateRenderTarget() => _renderTarget; + + public void Invalidate(Rect rect) + { + } + + public Point PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); + + public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); + } +} \ No newline at end of file