Browse Source

Merge branch 'master' into better-type-res-error

pull/11347/head
Yoh Deadfall 3 years ago
committed by GitHub
parent
commit
6ff496d65c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 71
      nukebuild/RefAssemblyGenerator.cs
  2. 2
      readme.md
  3. 2
      samples/ControlCatalog/Pages/TabControlPage.xaml.cs
  4. 2
      samples/ControlCatalog/ViewModels/TabControlPageViewModel.cs
  5. 2
      samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
  6. 6
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  7. 8
      samples/RenderDemo/Pages/PathMeasurementPage.cs
  8. 2
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  9. 2
      src/Avalonia.Base/Collections/Pooled/PooledStack.cs
  10. 4
      src/Avalonia.Base/Input/Cursor.cs
  11. 80
      src/Avalonia.Base/Media/Brush.cs
  12. 18
      src/Avalonia.Base/Media/ConicGradientBrush.cs
  13. 4
      src/Avalonia.Base/Media/DashStyle.cs
  14. 4
      src/Avalonia.Base/Media/Drawing.cs
  15. 61
      src/Avalonia.Base/Media/DrawingBrush.cs
  16. 2
      src/Avalonia.Base/Media/DrawingGroup.cs
  17. 4
      src/Avalonia.Base/Media/Effects/IEffect.cs
  18. 70
      src/Avalonia.Base/Media/Geometry.cs
  19. 2
      src/Avalonia.Base/Media/GeometryDrawing.cs
  20. 12
      src/Avalonia.Base/Media/GlyphRun.cs
  21. 2
      src/Avalonia.Base/Media/GlyphRunDrawing.cs
  22. 42
      src/Avalonia.Base/Media/GradientBrush.cs
  23. 2
      src/Avalonia.Base/Media/IAffectsRender.cs
  24. 10
      src/Avalonia.Base/Media/IImageBrush.cs
  25. 25
      src/Avalonia.Base/Media/IImmutableGlyphRunReference.cs
  26. 2
      src/Avalonia.Base/Media/IMutableBrush.cs
  27. 2
      src/Avalonia.Base/Media/IMutableExperimentalAcrylicMaterial.cs
  28. 2
      src/Avalonia.Base/Media/ITransform.cs
  29. 27
      src/Avalonia.Base/Media/ImageBrush.cs
  30. 2
      src/Avalonia.Base/Media/ImageDrawing.cs
  31. 10
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  32. 2
      src/Avalonia.Base/Media/Imaging/IBitmap.cs
  33. 2
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  34. 12
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  35. 4
      src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs
  36. 2
      src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs
  37. 18
      src/Avalonia.Base/Media/LinearGradientBrush.cs
  38. 112
      src/Avalonia.Base/Media/Pen.cs
  39. 6
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  40. 18
      src/Avalonia.Base/Media/RadialGradientBrush.cs
  41. 20
      src/Avalonia.Base/Media/SolidColorBrush.cs
  42. 2
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  43. 210
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  44. 61
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  45. 52
      src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs
  46. 23
      src/Avalonia.Base/Media/TileBrush.cs
  47. 20
      src/Avalonia.Base/Media/Transform.cs
  48. 64
      src/Avalonia.Base/Media/VisualBrush.cs
  49. 2
      src/Avalonia.Base/Metadata/PrivateApiAttribute.cs
  50. 2
      src/Avalonia.Base/Platform/IBitmapImpl.cs
  51. 6
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  52. 2
      src/Avalonia.Base/Platform/IDrawingContextWithAcrylicLikeSupport.cs
  53. 3
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  54. 12
      src/Avalonia.Base/Reactive/Observable.cs
  55. 7
      src/Avalonia.Base/Rect.cs
  56. 61
      src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleCompositionBrush.cs
  57. 24
      src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleContentBrush.cs
  58. 37
      src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleImageBrush.cs
  59. 20
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  60. 14
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  61. 5
      src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs
  62. 11
      src/Avalonia.Base/Rendering/Composition/CompositionExperimentalAcrylicVisual.cs
  63. 28
      src/Avalonia.Base/Rendering/Composition/CompositionObject.cs
  64. 3
      src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs
  65. 2
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  66. 9
      src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs
  67. 2
      src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs
  68. 47
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  69. 129
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs
  70. 37
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs
  71. 371
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  72. 68
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs
  73. 47
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs
  74. 123
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs
  75. 12
      src/Avalonia.Base/Rendering/Composition/Drawing/ICompositionRenderResource.cs
  76. 81
      src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs
  77. 28
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs
  78. 61
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs
  79. 29
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs
  80. 35
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs
  81. 65
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs
  82. 214
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs
  83. 27
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs
  84. 25
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs
  85. 30
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs
  86. 349
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs
  87. 136
      src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs
  88. 12
      src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionSimplePen.cs
  89. 56
      src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs
  90. 10
      src/Avalonia.Base/Rendering/Composition/ICompositorSerializable.cs
  91. 2
      src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs
  92. 9
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  93. 24
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  94. 26
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs
  95. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs
  96. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  97. 22
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs
  98. 23
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  99. 11
      src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs
  100. 32
      src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs

71
nukebuild/RefAssemblyGenerator.cs

@ -63,50 +63,51 @@ public class RefAssemblyGenerator
}); });
} }
static bool HasPrivateApi(IEnumerable<CustomAttribute> attrs) => attrs.Any(a =>
a.AttributeType.FullName == "Avalonia.Metadata.PrivateApiAttribute");
static void ProcessType(TypeDefinition type, MethodReference obsoleteCtor) static void ProcessType(TypeDefinition type, MethodReference obsoleteCtor)
{ {
foreach (var nested in type.NestedTypes) foreach (var nested in type.NestedTypes)
ProcessType(nested, obsoleteCtor); ProcessType(nested, obsoleteCtor);
if (type.IsInterface)
var hideMethods = (type.IsInterface && type.Name.EndsWith("Impl"))
|| HasPrivateApi(type.CustomAttributes);
var injectMethod = hideMethods
|| type.CustomAttributes.Any(a =>
a.AttributeType.FullName == "Avalonia.Metadata.NotClientImplementableAttribute");
if (injectMethod)
{ {
var hideMethods = type.Name.EndsWith("Impl") type.Methods.Add(new MethodDefinition(
|| (type.HasCustomAttributes && type.CustomAttributes.Any(a => "(This interface or abstract class is -not- implementable by user code !)",
a.AttributeType.FullName == "Avalonia.Metadata.PrivateApiAttribute")); MethodAttributes.Assembly
| MethodAttributes.Abstract
| MethodAttributes.NewSlot
| MethodAttributes.HideBySig, type.Module.TypeSystem.Void));
}
var injectMethod = hideMethods var forceUnstable = type.CustomAttributes.FirstOrDefault(a =>
|| type.CustomAttributes.Any(a => a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute");
a.AttributeType.FullName == "Avalonia.Metadata.NotClientImplementableAttribute");
foreach (var m in type.Methods)
if (hideMethods) {
{ if (hideMethods || HasPrivateApi(m.CustomAttributes))
foreach (var m in type.Methods)
{
var dflags = MethodAttributes.Public | MethodAttributes.Family | MethodAttributes.FamORAssem |
MethodAttributes.FamANDAssem | MethodAttributes.Assembly;
m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly;
}
}
if(injectMethod)
{ {
type.Methods.Add(new MethodDefinition("NotClientImplementable", var dflags = MethodAttributes.Public | MethodAttributes.Family | MethodAttributes.FamORAssem |
MethodAttributes.Assembly MethodAttributes.FamANDAssem | MethodAttributes.Assembly;
| MethodAttributes.Abstract m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly;
| MethodAttributes.NewSlot
| MethodAttributes.HideBySig, type.Module.TypeSystem.Void));
} }
MarkAsUnstable(m, obsoleteCtor, forceUnstable);
var forceUnstable = type.CustomAttributes.FirstOrDefault(a =>
a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute");
foreach (var m in type.Methods)
MarkAsUnstable(m, obsoleteCtor, forceUnstable);
foreach (var m in type.Properties)
MarkAsUnstable(m, obsoleteCtor, forceUnstable);
foreach (var m in type.Events)
MarkAsUnstable(m, obsoleteCtor, forceUnstable);
} }
foreach (var m in type.Properties)
MarkAsUnstable(m, obsoleteCtor, forceUnstable);
foreach (var m in type.Events)
MarkAsUnstable(m, obsoleteCtor, forceUnstable);
} }
static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute unstableAttribute) static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute unstableAttribute)

2
readme.md

@ -120,6 +120,8 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
<a href="https://opencollective.com/Avalonia/sponsor/9/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/9/avatar.svg"></a> <a href="https://opencollective.com/Avalonia/sponsor/9/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/9/avatar.svg"></a>
<a href="https://baseheadinc.com/" target="_blank"><img height="50" src="https://baseheadinc.com/wp-content/uploads/2020/09/BH-Logo-for-Site-Header-New.png"></a> <a href="https://baseheadinc.com/" target="_blank"><img height="50" src="https://baseheadinc.com/wp-content/uploads/2020/09/BH-Logo-for-Site-Header-New.png"></a>
<a href="https://aws.amazon.com/developer/language/net/" target="_blank"><img src="https://github.com/AvaloniaUI/Avalonia/assets/552074/7771d8b9-ef84-4503-9889-033a87d2c852"></a>
## Commercial Support ## Commercial Support
We have a range of [support plans available](https://avaloniaui.net/support) for those looking to partner with the creators of Avalonia, enabling access to the best support at every step of the development process. We have a range of [support plans available](https://avaloniaui.net/support) for those looking to partner with the creators of Avalonia, enabling access to the best support at every step of the development process.

2
samples/ControlCatalog/Pages/TabControlPage.xaml.cs

@ -49,7 +49,7 @@ namespace ControlCatalog.Pages
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private static IBitmap LoadBitmap(string uri) private static Bitmap LoadBitmap(string uri)
{ {
var assets = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); var assets = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
return new Bitmap(assets.Open(new Uri(uri))); return new Bitmap(assets.Open(new Uri(uri)));

2
samples/ControlCatalog/ViewModels/TabControlPageViewModel.cs

@ -21,6 +21,6 @@ public class TabControlPageViewModelItem
{ {
public string? Header { get; set; } public string? Header { get; set; }
public string? Text { get; set; } public string? Text { get; set; }
public IBitmap? Image { get; set; } public Bitmap? Image { get; set; }
public bool IsEnabled { get; set; } = true; public bool IsEnabled { get; set; } = true;
} }

2
samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs

@ -216,7 +216,6 @@ namespace ControlCatalog.ViewModels
{ {
var animation = new Animation var animation = new Animation
{ {
FillMode = FillMode.Forward,
Children = Children =
{ {
new KeyFrame new KeyFrame
@ -247,7 +246,6 @@ namespace ControlCatalog.ViewModels
to.IsVisible = true; to.IsVisible = true;
var animation = new Animation var animation = new Animation
{ {
FillMode = FillMode.Forward,
Children = Children =
{ {
new KeyFrame new KeyFrame

6
samples/RenderDemo/Pages/CustomSkiaPage.cs

@ -27,11 +27,11 @@ namespace RenderDemo.Pages
class CustomDrawOp : ICustomDrawOperation class CustomDrawOp : ICustomDrawOperation
{ {
private readonly GlyphRun _noSkia; private readonly IImmutableGlyphRunReference _noSkia;
public CustomDrawOp(Rect bounds, GlyphRun noSkia) public CustomDrawOp(Rect bounds, GlyphRun noSkia)
{ {
_noSkia = noSkia; _noSkia = noSkia.TryCreateImmutableGlyphRunReference();
Bounds = bounds; Bounds = bounds;
} }
@ -48,7 +48,7 @@ namespace RenderDemo.Pages
{ {
var leaseFeature = context.TryGetFeature<ISkiaSharpApiLeaseFeature>(); var leaseFeature = context.TryGetFeature<ISkiaSharpApiLeaseFeature>();
if (leaseFeature == null) if (leaseFeature == null)
context.DrawGlyphRun(Brushes.Black, _noSkia.PlatformImpl); context.DrawGlyphRun(Brushes.Black, _noSkia);
else else
{ {
using var lease = leaseFeature.Lease(); using var lease = leaseFeature.Lease();

8
samples/RenderDemo/Pages/PathMeasurementPage.cs

@ -53,15 +53,15 @@ namespace RenderDemo.Pages
bitmapCtx.DrawGeometry(null, strokePen, basePath); bitmapCtx.DrawGeometry(null, strokePen, basePath);
var length = basePath.PlatformImpl.ContourLength; var length = basePath.ContourLength;
if (basePath.PlatformImpl.TryGetSegment(length * 0.05, length * 0.2, true, out var dst1)) if (basePath.TryGetSegment(length * 0.05, length * 0.2, true, out var dst1))
bitmapCtx.DrawGeometry(null, strokePen1, dst1); bitmapCtx.DrawGeometry(null, strokePen1, dst1);
if (basePath.PlatformImpl.TryGetSegment(length * 0.2, length * 0.8, true, out var dst2)) if (basePath.TryGetSegment(length * 0.2, length * 0.8, true, out var dst2))
bitmapCtx.DrawGeometry(null, strokePen2, dst2); bitmapCtx.DrawGeometry(null, strokePen2, dst2);
if (basePath.PlatformImpl.TryGetSegment(length * 0.8, length * 0.95, true, out var dst3)) if (basePath.TryGetSegment(length * 0.8, length * 0.95, true, out var dst3))
bitmapCtx.DrawGeometry(null, strokePen3, dst3); bitmapCtx.DrawGeometry(null, strokePen3, dst3);
var pathBounds = basePath.GetRenderBounds(strokePen); var pathBounds = basePath.GetRenderBounds(strokePen);

2
src/Avalonia.Base/Collections/Pooled/PooledList.cs

@ -1423,7 +1423,7 @@ namespace Avalonia.Collections.Pooled
private static bool ShouldClear(ClearMode mode) private static bool ShouldClear(ClearMode mode)
{ {
#if NETCOREAPP2_1 #if NETCOREAPP2_1_OR_GREATER
return mode == ClearMode.Always return mode == ClearMode.Always
|| (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences<T>()); || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences<T>());
#else #else

2
src/Avalonia.Base/Collections/Pooled/PooledStack.cs

@ -596,7 +596,7 @@ namespace Avalonia.Collections.Pooled
private static bool ShouldClear(ClearMode mode) private static bool ShouldClear(ClearMode mode)
{ {
#if NETCOREAPP2_1 #if NETCOREAPP2_1_OR_GREATER
return mode == ClearMode.Always return mode == ClearMode.Always
|| (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences<T>()); || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences<T>());
#else #else

4
src/Avalonia.Base/Input/Cursor.cs

@ -55,12 +55,12 @@ namespace Avalonia.Input
{ {
} }
public Cursor(IBitmap cursor, PixelPoint hotSpot) public Cursor(Bitmap cursor, PixelPoint hotSpot)
: this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot), "BitmapCursor") : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot), "BitmapCursor")
{ {
} }
public ICursorImpl PlatformImpl { get; } internal ICursorImpl PlatformImpl { get; }
public void Dispose() => PlatformImpl.Dispose(); public void Dispose() => PlatformImpl.Dispose();

80
src/Avalonia.Base/Media/Brush.cs

@ -4,6 +4,10 @@ using Avalonia.Animation;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -11,7 +15,7 @@ namespace Avalonia.Media
/// Describes how an area is painted. /// Describes how an area is painted.
/// </summary> /// </summary>
[TypeConverter(typeof(BrushConverter))] [TypeConverter(typeof(BrushConverter))]
public abstract class Brush : Animatable, IBrush public abstract class Brush : Animatable, IBrush, ICompositionRenderResource<IBrush>, ICompositorSerializable
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Opacity"/> property. /// Defines the <see cref="Opacity"/> property.
@ -30,14 +34,10 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public static readonly StyledProperty<RelativePoint> TransformOriginProperty = public static readonly StyledProperty<RelativePoint> TransformOriginProperty =
AvaloniaProperty.Register<Brush, RelativePoint>(nameof(TransformOrigin)); AvaloniaProperty.Register<Brush, RelativePoint>(nameof(TransformOrigin));
/// <inheritdoc/>
public event EventHandler? Invalidated;
static Brush() static Brush()
{ {
Animation.Animation.RegisterAnimator<BaseBrushAnimator>(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType)); Animation.Animation.RegisterAnimator<BaseBrushAnimator>(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType));
AffectsRender<Brush>(OpacityProperty, TransformProperty);
} }
/// <summary> /// <summary>
@ -92,31 +92,57 @@ namespace Avalonia.Media
throw new FormatException($"Invalid brush string: '{s}'."); throw new FormatException($"Invalid brush string: '{s}'.");
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == TransformProperty)
_resource.ProcessPropertyChangeNotification(change);
/// <summary> RegisterForSerialization();
/// Marks a property as affecting the brush's visual representation.
/// </summary> base.OnPropertyChanged(change);
/// <param name="properties">The properties.</param> }
/// <remarks>
/// After a call to this method in a brush's static constructor, any change to the private protected void RegisterForSerialization() =>
/// property will cause the <see cref="Invalidated"/> event to be raised on the brush. _resource.RegisterForInvalidationOnAllCompositors(this);
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties) private CompositorResourceHolder<ServerCompositionSimpleBrush> _resource;
where T : Brush
IBrush ICompositionRenderResource<IBrush>.GetForCompositor(Compositor c) => _resource.GetForCompositor(c);
internal abstract Func<Compositor, ServerCompositionSimpleBrush> Factory { get; }
void ICompositionRenderResource.AddRefOnCompositor(Compositor c)
{ {
var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>( if (_resource.CreateOrAddRef(c, this, out _, Factory))
static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty)); OnReferencedFromCompositor(c);
}
foreach (var property in properties) protected virtual void OnReferencedFromCompositor(Compositor c)
{ {
property.Changed.Subscribe(invalidateObserver); if (Transform is ICompositionRenderResource<ITransform> resource)
} resource.AddRefOnCompositor(c);
} }
/// <summary> void ICompositionRenderResource.ReleaseOnCompositor(Compositor c)
/// Raises the <see cref="Invalidated"/> event. {
/// </summary> if(_resource.Release(c))
/// <param name="e">The event args.</param> OnUnreferencedFromCompositor(c);
protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); }
protected virtual void OnUnreferencedFromCompositor(Compositor c)
{
if (Transform is ICompositionRenderResource<ITransform> resource)
resource.ReleaseOnCompositor(c);
}
SimpleServerObject? ICompositorSerializable.TryGetServer(Compositor c) => _resource.TryGetForCompositor(c);
private protected virtual void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
ServerCompositionSimpleBrush.SerializeAllChanges(writer, Opacity, TransformOrigin, Transform.GetServer(c));
}
void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer) => SerializeChanges(c, writer);
} }
} }

18
src/Avalonia.Base/Media/ConicGradientBrush.cs

@ -1,4 +1,8 @@
using System;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -23,11 +27,6 @@ namespace Avalonia.Media
nameof(Angle), nameof(Angle),
0); 0);
static ConicGradientBrush()
{
AffectsRender<ConicGradientBrush>(CenterProperty, AngleProperty);
}
/// <summary> /// <summary>
/// Gets or sets the center point of the gradient. /// Gets or sets the center point of the gradient.
/// </summary> /// </summary>
@ -51,5 +50,14 @@ namespace Avalonia.Media
{ {
return new ImmutableConicGradientBrush(this); return new ImmutableConicGradientBrush(this);
} }
internal override Func<Compositor, ServerCompositionSimpleBrush> Factory =>
static c => new ServerCompositionSimpleConicGradientBrush(c.Server);
private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
base.SerializeChanges(c, writer);
ServerCompositionSimpleConicGradientBrush.SerializeAllChanges(writer, Angle, Center);
}
} }
} }

4
src/Avalonia.Base/Media/DashStyle.cs

@ -13,7 +13,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Represents the sequence of dashes and gaps that will be applied by a <see cref="Pen"/>. /// Represents the sequence of dashes and gaps that will be applied by a <see cref="Pen"/>.
/// </summary> /// </summary>
public class DashStyle : Animatable, IDashStyle, IAffectsRender public class DashStyle : Animatable, IDashStyle
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Dashes"/> property. /// Defines the <see cref="Dashes"/> property.
@ -104,7 +104,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Raised when the dash style changes. /// Raised when the dash style changes.
/// </summary> /// </summary>
public event EventHandler? Invalidated; internal event EventHandler? Invalidated;
/// <summary> /// <summary>
/// Returns an immutable clone of the <see cref="DashStyle"/>. /// Returns an immutable clone of the <see cref="DashStyle"/>.

4
src/Avalonia.Base/Media/Drawing.cs

@ -9,7 +9,9 @@
/// Draws this drawing to the given <see cref="DrawingContext"/>. /// Draws this drawing to the given <see cref="DrawingContext"/>.
/// </summary> /// </summary>
/// <param name="context">The drawing context.</param> /// <param name="context">The drawing context.</param>
public abstract void Draw(DrawingContext context); public void Draw(DrawingContext context) => DrawCore(context);
internal abstract void DrawCore(DrawingContext context);
/// <summary> /// <summary>
/// Gets the drawing's bounding rectangle. /// Gets the drawing's bounding rectangle.

61
src/Avalonia.Base/Media/DrawingBrush.cs

@ -1,26 +1,25 @@
using System;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Paints an area with an <see cref="Drawing"/>. /// Paints an area with an <see cref="Drawing"/>.
/// </summary> /// </summary>
public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender public class DrawingBrush : TileBrush, ISceneBrush
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Drawing"/> property. /// Defines the <see cref="Drawing"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Drawing?> DrawingProperty = public static readonly StyledProperty<Drawing?> DrawingProperty =
AvaloniaProperty.Register<DrawingBrush, Drawing?>(nameof(Drawing)); AvaloniaProperty.Register<DrawingBrush, Drawing?>(nameof(Drawing));
static DrawingBrush()
{
AffectsRender<DrawingBrush>(DrawingProperty);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DrawingBrush"/> class. /// Initializes a new instance of the <see cref="DrawingBrush"/> class.
/// </summary> /// </summary>
@ -51,16 +50,52 @@ namespace Avalonia.Media
if (Drawing == null) if (Drawing == null)
return null; return null;
using var recorder = new RenderDataDrawingContext(null);
Drawing?.Draw(recorder);
return recorder.GetImmediateSceneBrushContent(this, null, true);
}
internal override Func<Compositor, ServerCompositionSimpleBrush> Factory =>
static c => new ServerCompositionSimpleContentBrush(c.Server);
private InlineDictionary<Compositor, CompositionRenderDataSceneBrushContent?> _renderDataDictionary;
protected override void OnReferencedFromCompositor(Compositor c)
{
_renderDataDictionary.Add(c, CreateServerContent(c));
base.OnReferencedFromCompositor(c);
}
protected override void OnUnreferencedFromCompositor(Compositor c)
{
if (_renderDataDictionary.TryGetAndRemoveValue(c, out var content))
content?.RenderData.Dispose();
base.OnUnreferencedFromCompositor(c);
}
private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
base.SerializeChanges(c, writer);
if (_renderDataDictionary.TryGetValue(c, out var content))
writer.WriteObject(content);
else
writer.WriteObject(null);
}
CompositionRenderDataSceneBrushContent? CreateServerContent(Compositor c)
{
if (Drawing == null)
return null;
var recorder = new CompositionDrawingContext(); using var recorder = new RenderDataDrawingContext(c);
recorder.BeginUpdate(null);
Drawing?.Draw(recorder); Drawing?.Draw(recorder);
var drawList = recorder.EndUpdate(); var renderData = recorder.GetRenderResults();
if (drawList == null) if (renderData == null)
return null; return null;
return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, return new CompositionRenderDataSceneBrushContent(
drawList.CalculateBounds(), true); (ServerCompositionSimpleContentBrush)((ICompositionRenderResource<IBrush>)this).GetForCompositor(c),
renderData, null, true);
} }
} }
} }

2
src/Avalonia.Base/Media/DrawingGroup.cs

@ -69,7 +69,7 @@ namespace Avalonia.Media
public DrawingContext Open() => new DrawingGroupDrawingContext(this); public DrawingContext Open() => new DrawingGroupDrawingContext(this);
public override void Draw(DrawingContext context) internal override void DrawCore(DrawingContext context)
{ {
var bounds = GetBounds(); var bounds = GetBounds();

4
src/Avalonia.Base/Media/Effects/IEffect.cs

@ -2,16 +2,18 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Metadata;
namespace Avalonia.Media; namespace Avalonia.Media;
[TypeConverter(typeof(EffectConverter))] [TypeConverter(typeof(EffectConverter))]
[NotClientImplementable]
public interface IEffect public interface IEffect
{ {
} }
public interface IMutableEffect : IEffect, IAffectsRender public interface IMutableEffect : IEffect
{ {
/// <summary> /// <summary>
/// Creates an immutable clone of the effect. /// Creates an immutable clone of the effect.

70
src/Avalonia.Base/Media/Geometry.cs

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.Platform; using Avalonia.Platform;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using Avalonia.Reactive; using Avalonia.Reactive;
@ -40,7 +41,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets the platform-specific implementation of the geometry. /// Gets the platform-specific implementation of the geometry.
/// </summary> /// </summary>
public IGeometryImpl? PlatformImpl internal IGeometryImpl? PlatformImpl
{ {
get get
{ {
@ -192,6 +193,12 @@ namespace Avalonia.Media
control?.InvalidateGeometry(); control?.InvalidateGeometry();
} }
/// <summary>
/// Gets the geometry's total length as if all its contours are placed
/// in a straight line.
/// </summary>
public double ContourLength => PlatformImpl?.ContourLength ?? 0;
/// <summary> /// <summary>
/// Combines the two geometries using the specified <see cref="GeometryCombineMode"/> and applies the specified transform to the resulting geometry. /// Combines the two geometries using the specified <see cref="GeometryCombineMode"/> and applies the specified transform to the resulting geometry.
/// </summary> /// </summary>
@ -204,6 +211,67 @@ namespace Avalonia.Media
{ {
return new CombinedGeometry(combineMode, geometry1, geometry2, transform); return new CombinedGeometry(combineMode, geometry1, geometry2, transform);
} }
/// <summary>
/// Attempts to get the corresponding point at the
/// specified distance
/// </summary>
/// <param name="distance">The contour distance to get from.</param>
/// <param name="point">The point in the specified distance.</param>
/// <returns>If there's valid point at the specified distance.</returns>
public bool TryGetPointAtDistance(double distance, out Point point)
{
if (PlatformImpl == null)
{
point = default;
return false;
}
return PlatformImpl.TryGetPointAtDistance(distance, out point);
}
/// <summary>
/// Attempts to get the corresponding point and
/// tangent from the specified distance along the
/// contour of the geometry.
/// </summary>
/// <param name="distance">The contour distance to get from.</param>
/// <param name="point">The point in the specified distance.</param>
/// <param name="tangent">The tangent in the specified distance.</param>
/// <returns>If there's valid point and tangent at the specified distance.</returns>
public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent)
{
if (PlatformImpl == null)
{
point = tangent = default;
return false;
}
return PlatformImpl.TryGetPointAndTangentAtDistance(distance, out point, out tangent);
}
/// <summary>
/// Attempts to get the corresponding path segment
/// given by the two distances specified.
/// Imagine it like snipping a part of the current
/// geometry.
/// </summary>
/// <param name="startDistance">The contour distance to start snipping from.</param>
/// <param name="stopDistance">The contour distance to stop snipping to.</param>
/// <param name="startOnBeginFigure">If ture, the resulting snipped path will start with a BeginFigure call.</param>
/// <param name="segmentGeometry">The resulting snipped path.</param>
/// <returns>If the snipping operation is successful.</returns>
public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure,
[NotNullWhen(true)] out Geometry? segmentGeometry)
{
segmentGeometry = null;
if (PlatformImpl == null)
return false;
if (!PlatformImpl.TryGetSegment(startDistance, stopDistance, startOnBeginFigure, out var segment))
return false;
segmentGeometry = new PlatformGeometry(segment);
return true;
}
} }
public class GeometryTypeConverter : TypeConverter public class GeometryTypeConverter : TypeConverter

2
src/Avalonia.Base/Media/GeometryDrawing.cs

@ -58,7 +58,7 @@ namespace Avalonia.Media
set => SetValue(PenProperty, value); set => SetValue(PenProperty, value);
} }
public override void Draw(DrawingContext context) internal override void DrawCore(DrawingContext context)
{ {
if (Geometry != null) if (Geometry != null)
{ {

12
src/Avalonia.Base/Media/GlyphRun.cs

@ -155,6 +155,8 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public Rect Bounds => new Rect(new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height)); public Rect Bounds => new Rect(new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height));
public Rect InkBounds => PlatformImpl.Item.Bounds;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@ -214,7 +216,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// The platform implementation of the <see cref="GlyphRun"/>. /// The platform implementation of the <see cref="GlyphRun"/>.
/// </summary> /// </summary>
public IRef<IGlyphRunImpl> PlatformImpl internal IRef<IGlyphRunImpl> PlatformImpl
=> _platformImpl ??= CreateGlyphRunImpl(); => _platformImpl ??= CreateGlyphRunImpl();
/// <summary> /// <summary>
@ -728,7 +730,7 @@ namespace Avalonia.Media
clusterLength++; clusterLength++;
i--; i--;
if(characterIndex >= 0) if (characterIndex >= 0)
{ {
codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
@ -827,8 +829,7 @@ namespace Avalonia.Media
GlyphTypeface, GlyphTypeface,
FontRenderingEmSize, FontRenderingEmSize,
GlyphInfos, GlyphInfos,
BaselineOrigin, BaselineOrigin);
Bounds);
_platformImpl = RefCountable.Create(platformImpl); _platformImpl = RefCountable.Create(platformImpl);
@ -851,5 +852,8 @@ namespace Avalonia.Media
{ {
return PlatformImpl.Item.GetIntersections(lowerLimit, upperLimit); return PlatformImpl.Item.GetIntersections(lowerLimit, upperLimit);
} }
public IImmutableGlyphRunReference? TryCreateImmutableGlyphRunReference()
=> new ImmutableGlyphRunReference(PlatformImpl.Clone());
} }
} }

2
src/Avalonia.Base/Media/GlyphRunDrawing.cs

@ -20,7 +20,7 @@
set => SetValue(GlyphRunProperty, value); set => SetValue(GlyphRunProperty, value);
} }
public override void Draw(DrawingContext context) internal override void DrawCore(DrawingContext context)
{ {
if (GlyphRun == null) if (GlyphRun == null)
{ {

42
src/Avalonia.Base/Media/GradientBrush.cs

@ -5,8 +5,11 @@ using System.ComponentModel;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Media.Immutable;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -29,12 +32,6 @@ namespace Avalonia.Media
private IDisposable? _gradientStopsSubscription; private IDisposable? _gradientStopsSubscription;
static GradientBrush()
{
GradientStopsProperty.Changed.Subscribe(GradientStopsChanged);
AffectsRender<LinearGradientBrush>(SpreadMethodProperty);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GradientBrush"/> class. /// Initializes a new instance of the <see cref="GradientBrush"/> class.
/// </summary> /// </summary>
@ -63,37 +60,46 @@ namespace Avalonia.Media
/// <inheritdoc/> /// <inheritdoc/>
IReadOnlyList<IGradientStop> IGradientBrush.GradientStops => GradientStops; IReadOnlyList<IGradientStop> IGradientBrush.GradientStops => GradientStops;
private static void GradientStopsChanged(AvaloniaPropertyChangedEventArgs e)
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
if (e.Sender is GradientBrush brush) if (change.Property == GradientStopsProperty)
{ {
var oldValue = (GradientStops?)e.OldValue; var (oldValue, newValue) = change.GetOldAndNewValue<GradientStops?>();
var newValue = (GradientStops?)e.NewValue;
if (oldValue != null) if (oldValue != null)
{ {
oldValue.CollectionChanged -= brush.GradientStopsChanged; oldValue.CollectionChanged -= GradientStopsChanged;
brush._gradientStopsSubscription?.Dispose(); _gradientStopsSubscription?.Dispose();
} }
if (newValue != null) if (newValue != null)
{ {
newValue.CollectionChanged += brush.GradientStopsChanged; newValue.CollectionChanged += GradientStopsChanged;
brush._gradientStopsSubscription = newValue.TrackItemPropertyChanged(brush.GradientStopChanged); _gradientStopsSubscription = newValue.TrackItemPropertyChanged(GradientStopChanged);
} }
brush.RaiseInvalidated(EventArgs.Empty);
} }
base.OnPropertyChanged(change);
} }
private void GradientStopsChanged(object? sender, NotifyCollectionChangedEventArgs e) private void GradientStopsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
RaiseInvalidated(EventArgs.Empty); RegisterForSerialization();
} }
private void GradientStopChanged(Tuple<object?, PropertyChangedEventArgs> e) private void GradientStopChanged(Tuple<object?, PropertyChangedEventArgs> e)
{ {
RaiseInvalidated(EventArgs.Empty); RegisterForSerialization();
}
private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
base.SerializeChanges(c, writer);
writer.Write(SpreadMethod);
writer.Write(GradientStops.Count);
foreach (var stop in GradientStops)
// TODO: Technically it allocates, so it would be better to sync stops individually
writer.WriteObject(new ImmutableGradientStop(stop.Offset, stop.Color));
} }
public abstract IImmutableBrush ToImmutable(); public abstract IImmutableBrush ToImmutable();

2
src/Avalonia.Base/Media/IAffectsRender.cs

@ -6,7 +6,7 @@ namespace Avalonia.Media
/// Signals to a self-rendering control that changes to the resource should invoke /// Signals to a self-rendering control that changes to the resource should invoke
/// <see cref="Visual.InvalidateVisual"/>. /// <see cref="Visual.InvalidateVisual"/>.
/// </summary> /// </summary>
public interface IAffectsRender internal interface IAffectsRender
{ {
/// <summary> /// <summary>
/// Raised when the resource changes visually. /// Raised when the resource changes visually.

10
src/Avalonia.Base/Media/IImageBrush.cs

@ -1,5 +1,7 @@
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -12,6 +14,12 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets the image to draw. /// Gets the image to draw.
/// </summary> /// </summary>
IBitmap? Source { get; } IImageBrushSource? Source { get; }
}
[NotClientImplementable]
public interface IImageBrushSource
{
internal IRef<IBitmapImpl>? Bitmap { get; }
} }
} }

25
src/Avalonia.Base/Media/IImmutableGlyphRunReference.cs

@ -0,0 +1,25 @@
using System;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media;
public interface IImmutableGlyphRunReference : IDisposable
{
internal IRef<IGlyphRunImpl>? GlyphRun { get; }
}
internal class ImmutableGlyphRunReference : IImmutableGlyphRunReference
{
public ImmutableGlyphRunReference(IRef<IGlyphRunImpl>? glyphRun)
{
GlyphRun = glyphRun;
}
public IRef<IGlyphRunImpl>? GlyphRun { get; private set; }
public void Dispose()
{
GlyphRun?.Dispose();
GlyphRun = null;
}
}

2
src/Avalonia.Base/Media/IMutableBrush.cs

@ -7,7 +7,7 @@ namespace Avalonia.Media
/// Represents a mutable brush which can return an immutable clone of itself. /// Represents a mutable brush which can return an immutable clone of itself.
/// </summary> /// </summary>
[NotClientImplementable] [NotClientImplementable]
internal interface IMutableBrush : IBrush, IAffectsRender internal interface IMutableBrush : IBrush
{ {
/// <summary> /// <summary>
/// Creates an immutable clone of the brush. /// Creates an immutable clone of the brush.

2
src/Avalonia.Base/Media/IMutableExperimentalAcrylicMaterial.cs

@ -6,7 +6,7 @@ namespace Avalonia.Media
/// Represents a mutable brush which can return an immutable clone of itself. /// Represents a mutable brush which can return an immutable clone of itself.
/// </summary> /// </summary>
[NotClientImplementable] [NotClientImplementable]
public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IAffectsRender public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial
{ {
/// <summary> /// <summary>
/// Creates an immutable clone of the brush. /// Creates an immutable clone of the brush.

2
src/Avalonia.Base/Media/ITransform.cs

@ -1,8 +1,10 @@
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Metadata;
namespace Avalonia.Media namespace Avalonia.Media
{ {
[TypeConverter(typeof(TransformConverter))] [TypeConverter(typeof(TransformConverter))]
[NotClientImplementable]
public interface ITransform public interface ITransform
{ {
Matrix Value { get; } Matrix Value { get; }

27
src/Avalonia.Base/Media/ImageBrush.cs

@ -1,5 +1,9 @@
using System;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -11,13 +15,8 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Defines the <see cref="Visual"/> property. /// Defines the <see cref="Visual"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IBitmap?> SourceProperty = public static readonly StyledProperty<IImageBrushSource?> SourceProperty =
AvaloniaProperty.Register<ImageBrush, IBitmap?>(nameof(Source)); AvaloniaProperty.Register<ImageBrush, IImageBrushSource?>(nameof(Source));
static ImageBrush()
{
AffectsRender<ImageBrush>(SourceProperty);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageBrush"/> class. /// Initializes a new instance of the <see cref="ImageBrush"/> class.
@ -30,7 +29,7 @@ namespace Avalonia.Media
/// Initializes a new instance of the <see cref="ImageBrush"/> class. /// Initializes a new instance of the <see cref="ImageBrush"/> class.
/// </summary> /// </summary>
/// <param name="source">The image to draw.</param> /// <param name="source">The image to draw.</param>
public ImageBrush(IBitmap? source) public ImageBrush(IImageBrushSource? source)
{ {
Source = source; Source = source;
} }
@ -38,7 +37,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets the image to draw. /// Gets or sets the image to draw.
/// </summary> /// </summary>
public IBitmap? Source public IImageBrushSource? Source
{ {
get { return GetValue(SourceProperty); } get { return GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); } set { SetValue(SourceProperty, value); }
@ -49,5 +48,15 @@ namespace Avalonia.Media
{ {
return new ImmutableImageBrush(this); return new ImmutableImageBrush(this);
} }
internal override Func<Compositor, ServerCompositionSimpleBrush> Factory =>
static c => new ServerCompositionSimpleImageBrush(c.Server);
private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
base.SerializeChanges(c, writer);
var clonedRef = Source?.Bitmap?.Clone();
writer.WriteObject(clonedRef);
}
} }
} }

2
src/Avalonia.Base/Media/ImageDrawing.cs

@ -37,7 +37,7 @@ namespace Avalonia.Media
set => SetValue(RectProperty, value); set => SetValue(RectProperty, value);
} }
public override void Draw(DrawingContext context) internal override void DrawCore(DrawingContext context)
{ {
var imageSource = ImageSource; var imageSource = ImageSource;
var rect = Rect; var rect = Rect;

10
src/Avalonia.Base/Media/Imaging/Bitmap.cs

@ -10,7 +10,7 @@ namespace Avalonia.Media.Imaging
/// <summary> /// <summary>
/// Holds a bitmap image. /// Holds a bitmap image.
/// </summary> /// </summary>
public class Bitmap : IBitmap public class Bitmap : IBitmap, IImageBrushSource
{ {
private bool _isTranscoded; private bool _isTranscoded;
/// <summary> /// <summary>
@ -72,7 +72,7 @@ namespace Avalonia.Media.Imaging
/// Initializes a new instance of the <see cref="Bitmap"/> class. /// Initializes a new instance of the <see cref="Bitmap"/> class.
/// </summary> /// </summary>
/// <param name="impl">A platform-specific bitmap implementation.</param> /// <param name="impl">A platform-specific bitmap implementation.</param>
public Bitmap(IRef<IBitmapImpl> impl) internal Bitmap(IRef<IBitmapImpl> impl)
{ {
PlatformImpl = impl.Clone(); PlatformImpl = impl.Clone();
} }
@ -139,7 +139,9 @@ namespace Avalonia.Media.Imaging
/// <summary> /// <summary>
/// Gets the platform-specific bitmap implementation. /// Gets the platform-specific bitmap implementation.
/// </summary> /// </summary>
public IRef<IBitmapImpl> PlatformImpl { get; } internal IRef<IBitmapImpl> PlatformImpl { get; }
IRef<IBitmapImpl> IBitmap.PlatformImpl => PlatformImpl;
/// <summary> /// <summary>
/// Saves the bitmap to a file. /// Saves the bitmap to a file.
@ -237,5 +239,7 @@ namespace Avalonia.Media.Imaging
{ {
return AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); return AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
} }
IRef<IBitmapImpl> IImageBrushSource.Bitmap => PlatformImpl;
} }
} }

2
src/Avalonia.Base/Media/Imaging/IBitmap.cs

@ -10,7 +10,7 @@ namespace Avalonia.Media.Imaging
/// Represents a bitmap image. /// Represents a bitmap image.
/// </summary> /// </summary>
[NotClientImplementable] [NotClientImplementable]
public interface IBitmap : IImage, IDisposable internal interface IBitmap : IImage, IDisposable
{ {
/// <summary> /// <summary>
/// Gets the dots per inch (DPI) of the image. /// Gets the dots per inch (DPI) of the image.

2
src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs

@ -38,7 +38,7 @@ namespace Avalonia.Media.Imaging
/// <summary> /// <summary>
/// Gets the platform-specific bitmap implementation. /// Gets the platform-specific bitmap implementation.
/// </summary> /// </summary>
public new IRef<IRenderTargetBitmapImpl> PlatformImpl { get; } internal new IRef<IRenderTargetBitmapImpl> PlatformImpl { get; }
/// <summary> /// <summary>
/// Renders a visual to the <see cref="RenderTargetBitmap"/>. /// Renders a visual to the <see cref="RenderTargetBitmap"/>.

12
src/Avalonia.Base/Media/ImmediateDrawingContext.cs

@ -67,7 +67,7 @@ namespace Avalonia.Media
/// </summary> /// </summary>
/// <param name="source">The bitmap.</param> /// <param name="source">The bitmap.</param>
/// <param name="rect">The rect in the output to draw to.</param> /// <param name="rect">The rect in the output to draw to.</param>
public void DrawBitmap(IBitmap source, Rect rect) public void DrawBitmap(Bitmap source, Rect rect)
{ {
_ = source ?? throw new ArgumentNullException(nameof(source)); _ = source ?? throw new ArgumentNullException(nameof(source));
DrawBitmap(source, new Rect(source.Size), rect); DrawBitmap(source, new Rect(source.Size), rect);
@ -79,10 +79,10 @@ namespace Avalonia.Media
/// <param name="source">The bitmap.</param> /// <param name="source">The bitmap.</param>
/// <param name="sourceRect">The rect in the image to draw.</param> /// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param> /// <param name="destRect">The rect in the output to draw to.</param>
public void DrawBitmap(IBitmap source, Rect sourceRect, Rect destRect) public void DrawBitmap(Bitmap source, Rect sourceRect, Rect destRect)
{ {
_ = source ?? throw new ArgumentNullException(nameof(source)); _ = source ?? throw new ArgumentNullException(nameof(source));
PlatformImpl.DrawBitmap(source.PlatformImpl, 1, sourceRect, destRect); PlatformImpl.DrawBitmap(source.PlatformImpl.Item, 1, sourceRect, destRect);
} }
/// <summary> /// <summary>
@ -180,11 +180,11 @@ namespace Avalonia.Media
/// </summary> /// </summary>
/// <param name="foreground">The foreground brush.</param> /// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run.</param> /// <param name="glyphRun">The glyph run.</param>
public void DrawGlyphRun(IImmutableBrush foreground, IRef<IGlyphRunImpl> glyphRun) public void DrawGlyphRun(IImmutableBrush foreground, IImmutableGlyphRunReference glyphRun)
{ {
_ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun));
if (glyphRun.GlyphRun != null)
PlatformImpl.DrawGlyphRun(foreground, glyphRun); PlatformImpl.DrawGlyphRun(foreground, glyphRun.GlyphRun.Item);
} }
/// <summary> /// <summary>

4
src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs

@ -23,7 +23,7 @@ namespace Avalonia.Media.Immutable
/// </param> /// </param>
/// <param name="tileMode">The tile mode.</param> /// <param name="tileMode">The tile mode.</param>
public ImmutableImageBrush( public ImmutableImageBrush(
IBitmap? source, Bitmap? source,
AlignmentX alignmentX = AlignmentX.Center, AlignmentX alignmentX = AlignmentX.Center,
AlignmentY alignmentY = AlignmentY.Center, AlignmentY alignmentY = AlignmentY.Center,
RelativeRect? destinationRect = null, RelativeRect? destinationRect = null,
@ -58,6 +58,6 @@ namespace Avalonia.Media.Immutable
} }
/// <inheritdoc/> /// <inheritdoc/>
public IBitmap? Source { get; } public IImageBrushSource? Source { get; }
} }
} }

2
src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs

@ -21,7 +21,7 @@ namespace Avalonia.Media.Immutable
/// How the source rectangle will be stretched to fill the destination rect. /// How the source rectangle will be stretched to fill the destination rect.
/// </param> /// </param>
/// <param name="tileMode">The tile mode.</param> /// <param name="tileMode">The tile mode.</param>
protected ImmutableTileBrush( protected internal ImmutableTileBrush(
AlignmentX alignmentX, AlignmentX alignmentX,
AlignmentY alignmentY, AlignmentY alignmentY,
RelativeRect destinationRect, RelativeRect destinationRect,

18
src/Avalonia.Base/Media/LinearGradientBrush.cs

@ -1,4 +1,8 @@
using System;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -23,11 +27,6 @@ namespace Avalonia.Media
nameof(EndPoint), nameof(EndPoint),
RelativePoint.BottomRight); RelativePoint.BottomRight);
static LinearGradientBrush()
{
AffectsRender<LinearGradientBrush>(StartPointProperty, EndPointProperty);
}
/// <summary> /// <summary>
/// Gets or sets the start point for the gradient. /// Gets or sets the start point for the gradient.
/// </summary> /// </summary>
@ -51,5 +50,14 @@ namespace Avalonia.Media
{ {
return new ImmutableLinearGradientBrush(this); return new ImmutableLinearGradientBrush(this);
} }
internal override Func<Compositor, ServerCompositionSimpleBrush> Factory =>
static c => new ServerCompositionSimpleLinearGradientBrush(c.Server);
private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
base.SerializeChanges(c, writer);
ServerCompositionSimpleLinearGradientBrush.SerializeAllChanges(writer, StartPoint, EndPoint);
}
} }
} }

112
src/Avalonia.Base/Media/Pen.cs

@ -1,5 +1,9 @@
using System; using System;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
@ -7,7 +11,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Describes how a stroke is drawn. /// Describes how a stroke is drawn.
/// </summary> /// </summary>
public sealed class Pen : AvaloniaObject, IPen public sealed class Pen : AvaloniaObject, IPen, ICompositionRenderResource<IPen>, ICompositorSerializable
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Brush"/> property. /// Defines the <see cref="Brush"/> property.
@ -45,9 +49,7 @@ namespace Avalonia.Media
public static readonly StyledProperty<double> MiterLimitProperty = public static readonly StyledProperty<double> MiterLimitProperty =
AvaloniaProperty.Register<Pen, double>(nameof(MiterLimit), 10.0); AvaloniaProperty.Register<Pen, double>(nameof(MiterLimit), 10.0);
private EventHandler? _invalidated; private DashStyle? _subscribedToDashes;
private IAffectsRender? _subscribedToBrush;
private IAffectsRender? _subscribedToDashes;
private TargetWeakEventSubscriber<Pen, EventArgs>? _weakSubscriber; private TargetWeakEventSubscriber<Pen, EventArgs>? _weakSubscriber;
/// <summary> /// <summary>
@ -110,8 +112,8 @@ namespace Avalonia.Media
set => SetValue(BrushProperty, value); set => SetValue(BrushProperty, value);
} }
private static readonly WeakEvent<IAffectsRender, EventArgs> InvalidatedWeakEvent = private static readonly WeakEvent<DashStyle, EventArgs> InvalidatedWeakEvent =
WeakEvent.Register<IAffectsRender>( WeakEvent.Register<DashStyle>(
(s, h) => s.Invalidated += h, (s, h) => s.Invalidated += h,
(s, h) => s.Invalidated -= h); (s, h) => s.Invalidated -= h);
@ -161,23 +163,6 @@ namespace Avalonia.Media
set => SetValue(MiterLimitProperty, value); set => SetValue(MiterLimitProperty, value);
} }
/// <summary>
/// Raised when the pen changes.
/// </summary>
public event EventHandler? Invalidated
{
add
{
_invalidated += value;
UpdateSubscriptions();
}
remove
{
_invalidated -= value;
UpdateSubscriptions();
}
}
/// <summary> /// <summary>
/// Creates an immutable clone of the brush. /// Creates an immutable clone of the brush.
/// </summary> /// </summary>
@ -192,48 +177,79 @@ namespace Avalonia.Media
LineJoin, LineJoin,
MiterLimit); MiterLimit);
} }
void RegisterForSerialization()
{
_resource.RegisterForInvalidationOnAllCompositors(this);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
_invalidated?.Invoke(this, EventArgs.Empty); RegisterForSerialization();
if(change.Property == BrushProperty)
UpdateSubscription(ref _subscribedToBrush, Brush); if (change.Property == BrushProperty)
_resource.ProcessPropertyChangeNotification(change);
if(change.Property == DashStyleProperty) if(change.Property == DashStyleProperty)
UpdateSubscription(ref _subscribedToDashes, DashStyle); UpdateDashStyleSubscription();
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
} }
void UpdateSubscription(ref IAffectsRender? field, object? value) void UpdateDashStyleSubscription()
{
var newValue = _resource.IsAttached ? DashStyle as DashStyle : null;
if(ReferenceEquals(_subscribedToDashes, newValue))
return;
if (_subscribedToDashes != null && _weakSubscriber != null)
{
InvalidatedWeakEvent.Unsubscribe(_subscribedToDashes, _weakSubscriber);
_subscribedToDashes = null;
}
if (newValue != null)
{
_weakSubscriber ??= new TargetWeakEventSubscriber<Pen, EventArgs>(
this, static (target, _, ev, _) =>
{
if (ev == InvalidatedWeakEvent)
target.RegisterForSerialization();
});
InvalidatedWeakEvent.Subscribe(newValue, _weakSubscriber);
_subscribedToDashes = newValue;
}
}
private CompositorResourceHolder<ServerCompositionSimplePen> _resource;
IPen ICompositionRenderResource<IPen>.GetForCompositor(Compositor c) => _resource.GetForCompositor(c);
void ICompositionRenderResource.AddRefOnCompositor(Compositor c)
{ {
if ((_invalidated == null || field != value) && field != null) if (_resource.CreateOrAddRef(c, this, out _, static c => new ServerCompositionSimplePen(c.Server)))
{ {
if (_weakSubscriber != null) (Brush as ICompositionRenderResource)?.AddRefOnCompositor(c);
InvalidatedWeakEvent.Unsubscribe(field, _weakSubscriber); UpdateDashStyleSubscription();
field = null;
} }
}
if (_invalidated != null && field != value && value is IAffectsRender affectsRender) void ICompositionRenderResource.ReleaseOnCompositor(Compositor c)
{
if (_resource.Release(c))
{ {
if (_weakSubscriber == null) (Brush as ICompositionRenderResource)?.ReleaseOnCompositor(c);
{ UpdateDashStyleSubscription();
_weakSubscriber = new TargetWeakEventSubscriber<Pen, EventArgs>(
this, static (target, _, ev, _) =>
{
if (ev == InvalidatedWeakEvent)
target._invalidated?.Invoke(target, EventArgs.Empty);
});
}
InvalidatedWeakEvent.Subscribe(affectsRender, _weakSubscriber);
field = affectsRender;
} }
} }
void UpdateSubscriptions() SimpleServerObject? ICompositorSerializable.TryGetServer(Compositor c) => _resource.TryGetForCompositor(c);
void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer)
{ {
UpdateSubscription(ref _subscribedToBrush, Brush); ServerCompositionSimplePen.SerializeAllChanges(writer,
UpdateSubscription(ref _subscribedToDashes, DashStyle); Brush.GetServer(c), DashStyle?.ToImmutable(), LineCap, LineJoin, MiterLimit, Thickness);
} }
} }
} }

6
src/Avalonia.Base/Media/PlatformDrawingContext.cs

@ -10,7 +10,7 @@ using Avalonia.Utilities;
namespace Avalonia.Media; namespace Avalonia.Media;
internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport internal sealed class PlatformDrawingContext : DrawingContext
{ {
private readonly IDrawingContextImpl _impl; private readonly IDrawingContextImpl _impl;
private readonly bool _ownsImpl; private readonly bool _ownsImpl;
@ -45,7 +45,7 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi
protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect); protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect);
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect) => internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect) =>
_impl.DrawBitmap(source, opacity, sourceRect, destRect); _impl.DrawBitmap(source.Item, opacity, sourceRect, destRect);
public override void Custom(ICustomDrawOperation custom) public override void Custom(ICustomDrawOperation custom)
{ {
@ -66,7 +66,7 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi
_ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun));
if (foreground != null) if (foreground != null)
_impl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); _impl.DrawGlyphRun(foreground, glyphRun.PlatformImpl.Item);
} }
protected override void PushClipCore(RoundedRect rect) => _impl.PushClip(rect); protected override void PushClipCore(RoundedRect rect) => _impl.PushClip(rect);

18
src/Avalonia.Base/Media/RadialGradientBrush.cs

@ -1,4 +1,8 @@
using System;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -31,11 +35,6 @@ namespace Avalonia.Media
nameof(Radius), nameof(Radius),
0.5); 0.5);
static RadialGradientBrush()
{
AffectsRender<RadialGradientBrush>(CenterProperty, GradientOriginProperty, RadiusProperty);
}
/// <summary> /// <summary>
/// Gets or sets the start point for the gradient. /// Gets or sets the start point for the gradient.
/// </summary> /// </summary>
@ -71,5 +70,14 @@ namespace Avalonia.Media
{ {
return new ImmutableRadialGradientBrush(this); return new ImmutableRadialGradientBrush(this);
} }
internal override Func<Compositor, ServerCompositionSimpleBrush> Factory =>
static c => new ServerCompositionSimpleRadialGradientBrush(c.Server);
private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
base.SerializeChanges(c, writer);
ServerCompositionSimpleRadialGradientBrush.SerializeAllChanges(writer, Center, GradientOrigin, Radius);
}
} }
} }

20
src/Avalonia.Base/Media/SolidColorBrush.cs

@ -1,5 +1,9 @@
using System;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -13,12 +17,7 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public static readonly StyledProperty<Color> ColorProperty = public static readonly StyledProperty<Color> ColorProperty =
AvaloniaProperty.Register<SolidColorBrush, Color>(nameof(Color)); AvaloniaProperty.Register<SolidColorBrush, Color>(nameof(Color));
static SolidColorBrush()
{
AffectsRender<SolidColorBrush>(ColorProperty);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SolidColorBrush"/> class. /// Initializes a new instance of the <see cref="SolidColorBrush"/> class.
/// </summary> /// </summary>
@ -84,5 +83,14 @@ namespace Avalonia.Media
{ {
return new ImmutableSolidColorBrush(this); return new ImmutableSolidColorBrush(this);
} }
internal override Func<Compositor, ServerCompositionSimpleBrush> Factory =>
static c => new ServerCompositionSimpleSolidColorBrush(c.Server);
private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
base.SerializeChanges(c, writer);
ServerCompositionSimpleSolidColorBrush.SerializeAllChanges(writer, Color);
}
} }
} }

2
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -64,7 +64,7 @@ namespace Avalonia.Media.TextFormatting
if (Properties.BackgroundBrush != null) if (Properties.BackgroundBrush != null)
{ {
drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size)); drawingContext.DrawRectangle(Properties.BackgroundBrush, null, GlyphRun.Bounds);
} }
drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun); drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun);

210
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -13,6 +13,7 @@ namespace Avalonia.Media.TextFormatting
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
private readonly TextTrimming _textTrimming; private readonly TextTrimming _textTrimming;
private readonly TextLine[] _textLines; private readonly TextLine[] _textLines;
private readonly CachedMetrics _metrics = new();
private int _textSourceLength; private int _textSourceLength;
@ -151,12 +152,95 @@ namespace Avalonia.Media.TextFormatting
=> _textLines; => _textLines;
/// <summary> /// <summary>
/// Gets the bounds of the layout. /// The distance from the top of the first line to the bottom of the last line.
/// </summary> /// </summary>
/// <value> public double Height
/// The bounds. {
/// </value> get
public Rect Bounds { get; private set; } {
return _metrics.Height;
}
}
/// <summary>
/// The distance from the topmost black pixel of the first line
/// to the bottommost black pixel of the last line.
/// </summary>
public double Extent
{
get
{
return _metrics.Extent;
}
}
/// <summary>
/// The distance from the top of the first line to the baseline of the first line.
/// </summary>
public double Baseline
{
get
{
return _metrics.Baseline;
}
}
/// <summary>
/// The distance from the bottom of the last line to the extent bottom.
/// </summary>
public double OverhangAfter
{
get
{
return _metrics.OverhangAfter;
}
}
/// <summary>
/// The maximum distance from the leading black pixel to the leading alignment point of a line.
/// </summary>
public double OverhangLeading
{
get
{
return _metrics.OverhangLeading;
}
}
/// <summary>
/// The maximum distance from the trailing black pixel to the trailing alignment point of a line.
/// </summary>
public double OverhangTrailing
{
get
{
return _metrics.OverhangTrailing;
}
}
/// <summary>
/// The maximum advance width between the leading and trailing alignment points of a line,
/// excluding the width of whitespace characters at the end of the line.
/// </summary>
public double Width
{
get
{
return _metrics.Width;
}
}
/// <summary>
/// The maximum advance width between the leading and trailing alignment points of a line,
/// including the width of whitespace characters at the end of the line.
/// </summary>
public double WidthIncludingTrailingWhitespace
{
get
{
return _metrics.WidthIncludingTrailingWhitespace;
}
}
/// <summary> /// <summary>
/// Draws the text layout. /// Draws the text layout.
@ -382,7 +466,7 @@ namespace Avalonia.Media.TextFormatting
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 || var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
y > Bounds.Bottom; y > Height;
if (textPosition == textLine.FirstTextSourceIndex + textLine.Length) if (textPosition == textLine.FirstTextSourceIndex + textLine.Length)
{ {
@ -422,41 +506,25 @@ namespace Avalonia.Media.TextFormatting
textRunStyle, textWrapping, lineHeight, 0, letterSpacing); textRunStyle, textWrapping, lineHeight, 0, letterSpacing);
} }
/// <summary> private TextLine[] CreateTextLines()
/// Updates the current bounds.
/// </summary>
/// <param name="textLine">The text line.</param>
/// <param name="left">The current left.</param>
/// <param name="width">The current width.</param>
/// <param name="height">The current height.</param>
private static void UpdateBounds(TextLine textLine, ref double left, ref double width, ref double height)
{ {
var lineWidth = textLine.WidthIncludingTrailingWhitespace; var objectPool = FormattingObjectPool.Instance;
if (width < lineWidth)
{
width = lineWidth;
}
var start = textLine.Start; var lineStartOfLongestLine = double.MaxValue;
var origin = new Point();
var first = true;
if (left > start) double accBlackBoxLeft, accBlackBoxTop, accBlackBoxRight, accBlackBoxBottom;
{
left = start;
}
height += textLine.Height;
}
private TextLine[] CreateTextLines() accBlackBoxLeft = accBlackBoxTop = double.MaxValue;
{ accBlackBoxRight = accBlackBoxBottom = double.MinValue;
var objectPool = FormattingObjectPool.Instance;
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
Bounds = new Rect(0, 0, 0, textLine.Height); UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
return new TextLine[] { textLine }; return new TextLine[] { textLine };
} }
@ -465,8 +533,6 @@ namespace Avalonia.Media.TextFormatting
try try
{ {
double left = double.PositiveInfinity, width = 0.0, height = 0.0;
_textSourceLength = 0; _textSourceLength = 0;
TextLine? previousLine = null; TextLine? previousLine = null;
@ -487,7 +553,8 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(emptyTextLine); textLines.Add(emptyTextLine);
UpdateBounds(emptyTextLine, ref left, ref width, ref height); UpdateMetrics(emptyTextLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
} }
break; break;
@ -497,7 +564,7 @@ namespace Avalonia.Media.TextFormatting
//Fulfill max height constraint //Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight)
&& height + textLine.Height > MaxHeight) && Height + textLine.Height > MaxHeight)
{ {
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None) if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{ {
@ -519,7 +586,8 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(textLine); textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height); UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
previousLine = textLine; previousLine = textLine;
@ -528,7 +596,7 @@ namespace Avalonia.Media.TextFormatting
{ {
if (textLine.TextLineBreak is { IsSplit: true }) if (textLine.TextLineBreak is { IsSplit: true })
{ {
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(WidthIncludingTrailingWhitespace));
} }
break; break;
@ -546,18 +614,17 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(textLine); textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height); UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
} }
Bounds = new Rect(left, 0, width, height);
if (_paragraphProperties.TextAlignment == TextAlignment.Justify) if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{ {
var justificationWidth = MaxWidth; var justificationWidth = MaxWidth;
if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap) if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap)
{ {
justificationWidth = width; justificationWidth = WidthIncludingTrailingWhitespace;
} }
if (justificationWidth > 0) if (justificationWidth > 0)
@ -582,6 +649,46 @@ namespace Avalonia.Media.TextFormatting
} }
} }
private void UpdateMetrics(
TextLine currentLine,
ref double lineStartOfLongestLine,
ref Point origin,
ref bool first,
ref double accBlackBoxLeft,
ref double accBlackBoxTop,
ref double accBlackBoxRight,
ref double accBlackBoxBottom)
{
var blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading;
var blackBoxRight = origin.X + currentLine.Start + currentLine.Width - currentLine.OverhangTrailing;
var blackBoxBottom = origin.Y + currentLine.Height + currentLine.OverhangAfter;
var blackBoxTop = blackBoxBottom - currentLine.Extent;
accBlackBoxLeft = Math.Min(accBlackBoxLeft, blackBoxLeft);
accBlackBoxRight = Math.Max(accBlackBoxRight, blackBoxRight);
accBlackBoxBottom = Math.Max(accBlackBoxBottom, blackBoxBottom);
accBlackBoxTop = Math.Min(accBlackBoxTop, blackBoxTop);
_metrics.OverhangAfter = currentLine.OverhangAfter;
_metrics.Height += currentLine.Height;
_metrics.Width = Math.Max(_metrics.Width, currentLine.Width);
_metrics.WidthIncludingTrailingWhitespace = Math.Max(_metrics.WidthIncludingTrailingWhitespace, currentLine.WidthIncludingTrailingWhitespace);
lineStartOfLongestLine = Math.Min(lineStartOfLongestLine, currentLine.Start);
_metrics.Extent = accBlackBoxBottom - accBlackBoxTop;
_metrics.OverhangLeading = accBlackBoxLeft - lineStartOfLongestLine;
_metrics.OverhangTrailing = _metrics.Width - (accBlackBoxRight - lineStartOfLongestLine);
if (first)
{
_metrics.Baseline = currentLine.Baseline;
first = false;
}
origin = origin.WithY(origin.Y + currentLine.Height);
}
/// <summary> /// <summary>
/// Gets the <see cref="TextCollapsingProperties"/> for current text trimming mode. /// Gets the <see cref="TextCollapsingProperties"/> for current text trimming mode.
/// </summary> /// </summary>
@ -605,5 +712,24 @@ namespace Avalonia.Media.TextFormatting
line.Dispose(); line.Dispose();
} }
} }
private class CachedMetrics
{
// vertical
public double Height;
public double Baseline;
// horizontal
public double Width;
public double WidthIncludingTrailingWhitespace;
// vertical bounding box metrics
public double Extent;
public double OverhangAfter;
// horizontal bounding box metrics
public double OverhangLeading;
public double OverhangTrailing;
}
} }
} }

61
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -51,7 +51,7 @@ namespace Avalonia.Media.TextFormatting
public override double Baseline => _textLineMetrics.TextBaseline; public override double Baseline => _textLineMetrics.TextBaseline;
/// <inheritdoc/> /// <inheritdoc/>
public override double Extent => _textLineMetrics.Height; public override double Extent => _textLineMetrics.Extent;
/// <inheritdoc/> /// <inheritdoc/>
public override double Height => _textLineMetrics.Height; public override double Height => _textLineMetrics.Height;
@ -60,13 +60,13 @@ namespace Avalonia.Media.TextFormatting
public override int NewLineLength => _textLineMetrics.NewlineLength; public override int NewLineLength => _textLineMetrics.NewlineLength;
/// <inheritdoc/> /// <inheritdoc/>
public override double OverhangAfter => 0; public override double OverhangAfter => _textLineMetrics.OverhangAfter;
/// <inheritdoc/> /// <inheritdoc/>
public override double OverhangLeading => 0; public override double OverhangLeading => _textLineMetrics.OverhangLeading;
/// <inheritdoc/> /// <inheritdoc/>
public override double OverhangTrailing => 0; public override double OverhangTrailing => _textLineMetrics.OverhangTrailing;
/// <inheritdoc/> /// <inheritdoc/>
public override int TrailingWhitespaceLength => _textLineMetrics.TrailingWhitespaceLength; public override int TrailingWhitespaceLength => _textLineMetrics.TrailingWhitespaceLength;
@ -87,13 +87,18 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in _textRuns) foreach (var textRun in _textRuns)
{ {
if (textRun is DrawableTextRun drawable) switch (textRun)
{ {
var offsetY = GetBaselineOffset(this, drawable); case DrawableTextRun drawableTextRun:
{
var offsetY = GetBaselineOffset(this, drawableTextRun);
drawableTextRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
drawable.Draw(drawingContext, new Point(currentX, currentY + offsetY)); currentX += drawableTextRun.Size.Width;
currentX += drawable.Size.Width; break;
}
} }
} }
} }
@ -174,10 +179,12 @@ namespace Avalonia.Media.TextFormatting
distance -= Start; distance -= Start;
var lastIndex = _textRuns.Length - 1; var lastIndex = _textRuns.Length - 1;
var lineLength = Length;
if (_textRuns[lastIndex] is TextEndOfLine) if (_textRuns[lastIndex] is TextEndOfLine textEndOfLine)
{ {
lastIndex--; lastIndex--;
lineLength -= textEndOfLine.Length;
} }
var currentPosition = FirstTextSourceIndex; var currentPosition = FirstTextSourceIndex;
@ -205,7 +212,7 @@ namespace Avalonia.Media.TextFormatting
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{ {
currentPosition = Length - lastRun.Length; currentPosition = lineLength - lastRun.Length;
} }
return GetRunCharacterHit(lastRun, currentPosition, distance); return GetRunCharacterHit(lastRun, currentPosition, distance);
@ -703,7 +710,7 @@ namespace Avalonia.Media.TextFormatting
//In case a run only contains a linebreak we don't want to skip it. //In case a run only contains a linebreak we don't want to skip it.
if (currentRun is ShapedTextRun shaped) if (currentRun is ShapedTextRun shaped)
{ {
if(currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
{ {
continue; continue;
} }
@ -1431,9 +1438,10 @@ namespace Avalonia.Media.TextFormatting
var lineGap = fontMetrics.LineGap * scale; var lineGap = fontMetrics.LineGap * scale;
var height = descent - ascent + lineGap; var height = descent - ascent + lineGap;
var lineHeight = _paragraphProperties.LineHeight; var lineHeight = _paragraphProperties.LineHeight;
var bounds = new Rect();
for (var index = 0; index < _textRuns.Length; index++) for (var index = 0; index < _textRuns.Length; index++)
{ {
switch (_textRuns[index]) switch (_textRuns[index])
@ -1441,6 +1449,9 @@ namespace Avalonia.Media.TextFormatting
case ShapedTextRun textRun: case ShapedTextRun textRun:
{ {
var textMetrics = textRun.TextMetrics; var textMetrics = textRun.TextMetrics;
var glyphRun = textRun.GlyphRun;
bounds = bounds.Union(glyphRun.InkBounds);
if (fontRenderingEmSize < textMetrics.FontRenderingEmSize) if (fontRenderingEmSize < textMetrics.FontRenderingEmSize)
{ {
@ -1486,18 +1497,22 @@ namespace Avalonia.Media.TextFormatting
ascent = -drawableTextRun.Baseline; ascent = -drawableTextRun.Baseline;
} }
bounds = bounds.Union(new Rect(new Point(bounds.Right, 0), drawableTextRun.Size));
break; break;
} }
} }
} }
var overhangAfter = Math.Max(0, bounds.Bottom - height);
var width = widthIncludingWhitespace; var width = widthIncludingWhitespace;
for (var i = _textRuns.Length - 1; i >= 0; i--) for (var i = _textRuns.Length - 1; i >= 0; i--)
{ {
var currentRun = _textRuns[i]; var currentRun = _textRuns[i];
if(currentRun is ShapedTextRun shapedText) if (currentRun is ShapedTextRun shapedText)
{ {
var glyphRun = shapedText.GlyphRun; var glyphRun = shapedText.GlyphRun;
var glyphRunMetrics = glyphRun.Metrics; var glyphRunMetrics = glyphRun.Metrics;
@ -1518,6 +1533,9 @@ namespace Avalonia.Media.TextFormatting
} }
var start = GetParagraphOffsetX(width, widthIncludingWhitespace); var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
var overhangLeading = Math.Max(0, bounds.Left - start);
var overhangTrailing = Math.Max(0, bounds.Width - widthIncludingWhitespace);
var hasOverflowed = overhangLeading + widthIncludingWhitespace + overhangTrailing > _paragraphWidth;
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
{ {
@ -1527,8 +1545,21 @@ namespace Avalonia.Media.TextFormatting
} }
} }
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start, return new TextLineMetrics
-ascent, trailingWhitespaceLength, width, widthIncludingWhitespace); {
HasOverflowed = hasOverflowed,
Height = height,
Extent = bounds.Height,
NewlineLength = newLineLength,
Start = start,
TextBaseline = -ascent,
TrailingWhitespaceLength = trailingWhitespaceLength,
Width = width,
WidthIncludingTrailingWhitespace = widthIncludingWhitespace,
OverhangLeading= overhangLeading,
OverhangTrailing= overhangTrailing,
OverhangAfter = overhangAfter
};
} }
/// <summary> /// <summary>

52
src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs

@ -5,59 +5,65 @@
/// that holds information about ascent, descent, line gap, size and origin of the text line. /// that holds information about ascent, descent, line gap, size and origin of the text line.
/// </summary> /// </summary>
public readonly record struct TextLineMetrics public readonly record struct TextLineMetrics
{ {
public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline,
int trailingWhitespaceLength, double width,
double widthIncludingTrailingWhitespace)
{
HasOverflowed = hasOverflowed;
Height = height;
NewlineLength = newlineLength;
Start = start;
TextBaseline = textBaseline;
TrailingWhitespaceLength = trailingWhitespaceLength;
Width = width;
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
}
/// <summary> /// <summary>
/// Gets a value that indicates whether content of the line overflows the specified paragraph width. /// Gets a value that indicates whether content of the line overflows the specified paragraph width.
/// </summary> /// </summary>
public bool HasOverflowed { get; } public bool HasOverflowed { get; init; }
/// <summary> /// <summary>
/// Gets the height of a line of text. /// Gets the height of a line of text.
/// </summary> /// </summary>
public double Height { get; } public double Height { get; init; }
/// <summary> /// <summary>
/// Gets the number of newline characters at the end of a line. /// Gets the number of newline characters at the end of a line.
/// </summary> /// </summary>
public int NewlineLength { get; } public int NewlineLength { get; init; }
/// <summary> /// <summary>
/// Gets the distance from the start of a paragraph to the starting point of a line. /// Gets the distance from the start of a paragraph to the starting point of a line.
/// </summary> /// </summary>
public double Start { get; } public double Start { get; init; }
/// <summary> /// <summary>
/// Gets the distance from the top to the baseline of the line of text. /// Gets the distance from the top to the baseline of the line of text.
/// </summary> /// </summary>
public double TextBaseline { get; } public double TextBaseline { get; init; }
/// <summary> /// <summary>
/// Gets the number of whitespace code points beyond the last non-blank character in a line. /// Gets the number of whitespace code points beyond the last non-blank character in a line.
/// </summary> /// </summary>
public int TrailingWhitespaceLength { get; } public int TrailingWhitespaceLength { get; init; }
/// <summary> /// <summary>
/// Gets the width of a line of text, excluding trailing whitespace characters. /// Gets the width of a line of text, excluding trailing whitespace characters.
/// </summary> /// </summary>
public double Width { get; } public double Width { get; init; }
/// <summary> /// <summary>
/// Gets the width of a line of text, including trailing whitespace characters. /// Gets the width of a line of text, including trailing whitespace characters.
/// </summary> /// </summary>
public double WidthIncludingTrailingWhitespace { get; } public double WidthIncludingTrailingWhitespace { get; init; }
/// <summary>
/// Gets the distance from the top-most to bottom-most black pixel in a line.
/// </summary>
public double Extent { get; init; }
/// <summary>
/// Gets the distance that black pixels extend beyond the bottom alignment edge of a line.
/// </summary>
public double OverhangAfter { get; init; }
/// <summary>
/// Gets the distance that black pixels extend prior to the left leading alignment edge of the line.
/// </summary>
public double OverhangLeading { get; init; }
/// <summary>
/// Gets the distance that black pixels extend following the right trailing alignment edge of the line.
/// </summary>
public double OverhangTrailing { get; init; }
} }
} }

23
src/Avalonia.Base/Media/TileBrush.cs

@ -1,4 +1,7 @@
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -73,18 +76,7 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public static readonly StyledProperty<TileMode> TileModeProperty = public static readonly StyledProperty<TileMode> TileModeProperty =
AvaloniaProperty.Register<TileBrush, TileMode>(nameof(TileMode)); AvaloniaProperty.Register<TileBrush, TileMode>(nameof(TileMode));
static TileBrush()
{
AffectsRender<TileBrush>(
AlignmentXProperty,
AlignmentYProperty,
DestinationRectProperty,
SourceRectProperty,
StretchProperty,
TileModeProperty);
}
/// <summary> /// <summary>
/// Gets or sets the horizontal alignment of a tile in the destination. /// Gets or sets the horizontal alignment of a tile in the destination.
/// </summary> /// </summary>
@ -139,5 +131,12 @@ namespace Avalonia.Media
get { return (TileMode)GetValue(TileModeProperty); } get { return (TileMode)GetValue(TileModeProperty); }
set { SetValue(TileModeProperty, value); } set { SetValue(TileModeProperty, value); }
} }
private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
base.SerializeChanges(c, writer);
ServerCompositionSimpleTileBrush.SerializeAllChanges(writer, AlignmentX, AlignmentY, DestinationRect, SourceRect,
Stretch, TileMode);
}
} }
} }

20
src/Avalonia.Base/Media/Transform.cs

@ -2,6 +2,10 @@ using System;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Media namespace Avalonia.Media
@ -9,7 +13,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Represents a transform on an <see cref="Visual"/>. /// Represents a transform on an <see cref="Visual"/>.
/// </summary> /// </summary>
public abstract class Transform : Animatable, IMutableTransform public abstract class Transform : Animatable, IMutableTransform, ICompositionRenderResource<ITransform>, ICompositorSerializable
{ {
static Transform() static Transform()
{ {
@ -62,5 +66,19 @@ namespace Avalonia.Media
{ {
return Value.ToString(); return Value.ToString();
} }
private CompositorResourceHolder<ServerCompositionSimpleTransform> _resource;
ITransform ICompositionRenderResource<ITransform>.GetForCompositor(Compositor c) => _resource.GetForCompositor(c);
SimpleServerObject? ICompositorSerializable.TryGetServer(Compositor c) => _resource.TryGetForCompositor(c);
void ICompositionRenderResource.AddRefOnCompositor(Compositor c)
{
_resource.CreateOrAddRef(c, this, out _, static (cc) => new ServerCompositionSimpleTransform(cc.Server));
}
void ICompositionRenderResource.ReleaseOnCompositor(Compositor c) => _resource.Release(c);
void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer) =>
ServerCompositionSimpleTransform.SerializeAllChanges(writer, Value);
} }
} }

64
src/Avalonia.Base/Media/VisualBrush.cs

@ -1,25 +1,25 @@
using System;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Paints an area with an <see cref="Visual"/>. /// Paints an area with an <see cref="Visual"/>.
/// </summary> /// </summary>
public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender public class VisualBrush : TileBrush, ISceneBrush
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Visual"/> property. /// Defines the <see cref="Visual"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Visual?> VisualProperty = public static readonly StyledProperty<Visual?> VisualProperty =
AvaloniaProperty.Register<VisualBrush, Visual?>(nameof(Visual)); AvaloniaProperty.Register<VisualBrush, Visual?>(nameof(Visual));
static VisualBrush()
{
AffectsRender<VisualBrush>(VisualProperty);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="VisualBrush"/> class. /// Initializes a new instance of the <see cref="VisualBrush"/> class.
@ -54,15 +54,55 @@ namespace Avalonia.Media
if (Visual is IVisualBrushInitialize initialize) if (Visual is IVisualBrushInitialize initialize)
initialize.EnsureInitialized(); initialize.EnsureInitialized();
var recorder = new CompositionDrawingContext(); using var recorder = new RenderDataDrawingContext(null);
recorder.BeginUpdate(null);
ImmediateRenderer.Render(recorder, Visual, Visual.Bounds); ImmediateRenderer.Render(recorder, Visual, Visual.Bounds);
var drawList = recorder.EndUpdate(); return recorder.GetImmediateSceneBrushContent(this, new(Visual.Bounds.Size), false);
if (drawList == null) }
internal override Func<Compositor, ServerCompositionSimpleBrush> Factory =>
static c => new ServerCompositionSimpleContentBrush(c.Server);
private InlineDictionary<Compositor, CompositionRenderDataSceneBrushContent?> _renderDataDictionary;
protected override void OnReferencedFromCompositor(Compositor c)
{
_renderDataDictionary.Add(c, CreateServerContent(c));
base.OnReferencedFromCompositor(c);
}
protected override void OnUnreferencedFromCompositor(Compositor c)
{
if (_renderDataDictionary.TryGetAndRemoveValue(c, out var content))
content?.RenderData.Dispose();
base.OnUnreferencedFromCompositor(c);
}
private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
base.SerializeChanges(c, writer);
if (_renderDataDictionary.TryGetValue(c, out var content))
writer.WriteObject(content);
else
writer.WriteObject(null);
}
CompositionRenderDataSceneBrushContent? CreateServerContent(Compositor c)
{
if (Visual == null)
return null; return null;
return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, if (Visual is IVisualBrushInitialize initialize)
new(Visual.Bounds.Size), false); initialize.EnsureInitialized();
using var recorder = new RenderDataDrawingContext(c);
ImmediateRenderer.Render(recorder, Visual, Visual.Bounds);
var renderData = recorder.GetRenderResults();
if (renderData == null)
return null;
return new CompositionRenderDataSceneBrushContent(
(ServerCompositionSimpleContentBrush)((ICompositionRenderResource<IBrush>)this).GetForCompositor(c),
renderData, new(Visual.Bounds.Size), false);
} }
} }
} }

2
src/Avalonia.Base/Metadata/PrivateApiAttribute.cs

@ -2,7 +2,7 @@ using System;
namespace Avalonia.Metadata; namespace Avalonia.Metadata;
[AttributeUsage(AttributeTargets.Interface)] [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method)]
public sealed class PrivateApiAttribute : Attribute public sealed class PrivateApiAttribute : Attribute
{ {

2
src/Avalonia.Base/Platform/IBitmapImpl.cs

@ -7,7 +7,7 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Defines the platform-specific interface for a <see cref="Avalonia.Media.Imaging.Bitmap"/>. /// Defines the platform-specific interface for a <see cref="Avalonia.Media.Imaging.Bitmap"/>.
/// </summary> /// </summary>
[Unstable] [PrivateApi]
public interface IBitmapImpl : IDisposable public interface IBitmapImpl : IDisposable
{ {
/// <summary> /// <summary>

6
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -35,7 +35,7 @@ namespace Avalonia.Platform
/// <param name="opacity">The opacity to draw with.</param> /// <param name="opacity">The opacity to draw with.</param>
/// <param name="sourceRect">The rect in the image to draw.</param> /// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param> /// <param name="destRect">The rect in the output to draw to.</param>
void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect); void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect);
/// <summary> /// <summary>
/// Draws a bitmap image. /// Draws a bitmap image.
@ -44,7 +44,7 @@ namespace Avalonia.Platform
/// <param name="opacityMask">The opacity mask to draw with.</param> /// <param name="opacityMask">The opacity mask to draw with.</param>
/// <param name="opacityMaskRect">The destination rect for the opacity mask.</param> /// <param name="opacityMaskRect">The destination rect for the opacity mask.</param>
/// <param name="destRect">The rect in the output to draw to.</param> /// <param name="destRect">The rect in the output to draw to.</param>
void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect);
/// <summary> /// <summary>
/// Draws a line. /// Draws a line.
@ -94,7 +94,7 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
/// <param name="foreground">The foreground.</param> /// <param name="foreground">The foreground.</param>
/// <param name="glyphRun">The glyph run.</param> /// <param name="glyphRun">The glyph run.</param>
void DrawGlyphRun(IBrush? foreground, IRef<IGlyphRunImpl> glyphRun); void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun);
/// <summary> /// <summary>
/// Creates a new <see cref="IRenderTargetBitmapImpl"/> that can be used as a render layer /// Creates a new <see cref="IRenderTargetBitmapImpl"/> that can be used as a render layer

2
src/Avalonia.Base/Platform/IDrawingContextWithAcrylicLikeSupport.cs

@ -1,7 +1,9 @@
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
[PrivateApi]
public interface IDrawingContextWithAcrylicLikeSupport public interface IDrawingContextWithAcrylicLikeSupport
{ {
void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect); void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect);

3
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -169,9 +169,8 @@ namespace Avalonia.Platform
/// <param name="fontRenderingEmSize">The font rendering em size.</param> /// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="glyphInfos">The list of glyphs.</param> /// <param name="glyphInfos">The list of glyphs.</param>
/// <param name="baselineOrigin">The baseline origin of the run. Can be null.</param> /// <param name="baselineOrigin">The baseline origin of the run. Can be null.</param>
/// <param name="bounds">the conservative bounding box of the run</param>
/// <returns>An <see cref="IGlyphRunImpl"/>.</returns> /// <returns>An <see cref="IGlyphRunImpl"/>.</returns>
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds); IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin);
/// <summary> /// <summary>
/// Creates a backend-specific object using a low-level API graphics context /// Creates a backend-specific object using a low-level API graphics context

12
src/Avalonia.Base/Reactive/Observable.cs

@ -165,6 +165,18 @@ internal static class Observable
}); });
} }
public static IObservable<T> FromEventPattern<T>(Action<EventHandler<T>> addHandler, Action<EventHandler<T>> removeHandler) where T : EventArgs
{
return Create<T>(observer =>
{
var handler = new Action<T>(observer.OnNext);
var converted = new EventHandler<T>((_, args) => handler(args));
addHandler(converted);
return Disposable.Create(() => removeHandler(converted));
});
}
public static IObservable<T> Return<T>(T value) public static IObservable<T> Return<T>(T value)
{ {
return new ReturnImpl<T>(value); return new ReturnImpl<T>(value);

7
src/Avalonia.Base/Rect.cs

@ -607,5 +607,12 @@ namespace Avalonia
); );
} }
} }
/// <summary>
/// This method should be used internally to check for the rect emptiness
/// Once we add support for WPF-like empty rects, there will be an actual implementation
/// For now it's internal to keep some loud community members happy about the API being pretty
/// </summary>
internal bool IsEmpty() => this == default;
} }
} }

61
src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleCompositionBrush.cs

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Transport;
// ReSharper disable CheckNamespace
namespace Avalonia.Rendering.Composition.Server
{
internal partial class ServerCompositionSimpleBrush : IBrush
{
ITransform? IBrush.Transform => Transform;
}
class ServerCompositionSimpleGradientBrush : ServerCompositionSimpleBrush, IGradientBrush
{
internal ServerCompositionSimpleGradientBrush(ServerCompositor compositor) : base(compositor)
{
}
private readonly List<IGradientStop> _gradientStops = new();
public IReadOnlyList<IGradientStop> GradientStops => _gradientStops;
public GradientSpreadMethod SpreadMethod { get; private set; }
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{
base.DeserializeChangesCore(reader, committedAt);
SpreadMethod = reader.Read<GradientSpreadMethod>();
_gradientStops.Clear();
var count = reader.Read<int>();
for (var c = 0; c < count; c++)
_gradientStops.Add(reader.ReadObject<ImmutableGradientStop>());
}
}
partial class ServerCompositionSimpleConicGradientBrush : IConicGradientBrush
{
}
partial class ServerCompositionSimpleLinearGradientBrush : ILinearGradientBrush
{
}
partial class ServerCompositionSimpleRadialGradientBrush : IRadialGradientBrush
{
}
partial class ServerCompositionSimpleSolidColorBrush : ISolidColorBrush
{
}
}

24
src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleContentBrush.cs

@ -0,0 +1,24 @@
using System;
using Avalonia.Media;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Rendering.Composition.Server;
internal class ServerCompositionSimpleContentBrush : ServerCompositionSimpleTileBrush, ITileBrush, ISceneBrush
{
private CompositionRenderDataSceneBrushContent? _content;
internal ServerCompositionSimpleContentBrush(ServerCompositor compositor) : base(compositor)
{
}
// TODO: Figure out something about disposable
public ISceneBrushContent? CreateContent() => _content;
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{
base.DeserializeChangesCore(reader, committedAt);
_content = reader.ReadObject<CompositionRenderDataSceneBrushContent?>();
}
}

37
src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleImageBrush.cs

@ -0,0 +1,37 @@
using System;
using System.Data;
using System.IO;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Server;
internal class ServerCompositionSimpleImageBrush : ServerCompositionSimpleTileBrush,
IImageBrush, IImageBrushSource
{
public IImageBrushSource? Source => this;
public IRef<IBitmapImpl>? Bitmap { get; private set; }
internal ServerCompositionSimpleImageBrush(ServerCompositor compositor) : base(compositor)
{
}
public override void Dispose()
{
Bitmap?.Dispose();
Bitmap = null;
base.Dispose();
}
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{
base.DeserializeChangesCore(reader, committedAt);
Bitmap?.Dispose();
Bitmap = null;
Bitmap = reader.ReadObject<IRef<IBitmapImpl>>();
}
}

20
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Collections.Pooled; using Avalonia.Collections.Pooled;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.VisualTree; using Avalonia.VisualTree;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see> // Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -20,8 +21,7 @@ public class CompositingRenderer : IRendererWithCompositor
{ {
private readonly IRenderRoot _root; private readonly IRenderRoot _root;
private readonly Compositor _compositor; private readonly Compositor _compositor;
private readonly CompositionDrawingContext _recorder = new(); private readonly RenderDataDrawingContext _recorder;
private readonly DrawingContext _recordingContext;
private readonly HashSet<Visual> _dirty = new(); private readonly HashSet<Visual> _dirty = new();
private readonly HashSet<Visual> _recalculateChildren = new(); private readonly HashSet<Visual> _recalculateChildren = new();
private readonly Action _update; private readonly Action _update;
@ -55,7 +55,7 @@ public class CompositingRenderer : IRendererWithCompositor
{ {
_root = root; _root = root;
_compositor = compositor; _compositor = compositor;
_recordingContext = _recorder; _recorder = new(compositor);
CompositionTarget = compositor.CreateCompositionTarget(surfaces); CompositionTarget = compositor.CreateCompositionTarget(surfaces);
CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor);
_update = Update; _update = Update;
@ -276,10 +276,16 @@ public class CompositingRenderer : IRendererWithCompositor
comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform);
_recorder.BeginUpdate(comp.DrawList); try
visual.Render(_recordingContext); {
comp.DrawList = _recorder.EndUpdate(); visual.Render(_recorder);
comp.DrawList = _recorder.GetRenderResults();
}
finally
{
_recorder.Reset();
}
SyncChildren(visual); SyncChildren(visual);
} }
foreach(var v in _recalculateChildren) foreach(var v in _recalculateChildren)

14
src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs

@ -1,9 +1,6 @@
using System;
using System.Numerics;
using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport; using Avalonia.Rendering.Composition.Transport;
using Avalonia.VisualTree;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see> // Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -21,12 +18,12 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
public Visual Visual { get; } public Visual Visual { get; }
private bool _drawListChanged; private bool _drawListChanged;
private CompositionDrawList? _drawList; private CompositionRenderData? _drawList;
/// <summary> /// <summary>
/// The list of drawing commands /// The list of drawing commands
/// </summary> /// </summary>
public CompositionDrawList? DrawList public CompositionRenderData? DrawList
{ {
get => _drawList; get => _drawList;
set set
@ -43,7 +40,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
writer.Write((byte)(_drawListChanged ? 1 : 0)); writer.Write((byte)(_drawListChanged ? 1 : 0));
if (_drawListChanged) if (_drawListChanged)
{ {
writer.WriteObject(DrawList?.Clone()); writer.WriteObject(DrawList?.Server);
_drawListChanged = false; _drawListChanged = false;
} }
base.SerializeChangesCore(writer); base.SerializeChangesCore(writer);
@ -64,9 +61,6 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
return custom.HitTest(pt); return custom.HitTest(pt);
} }
foreach (var op in DrawList!) return DrawList?.HitTest(pt) ?? false;
if (op.Item.HitTest(pt))
return true;
return false;
} }
} }

5
src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs

@ -1,10 +1,11 @@
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia.Rendering.Composition; namespace Avalonia.Rendering.Composition;
public class CompositionDrawingSurface : CompositionSurface public class CompositionDrawingSurface : CompositionSurface, IDisposable
{ {
internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server!; internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server!;
internal CompositionDrawingSurface(Compositor compositor) : base(compositor, new ServerCompositionDrawingSurface(compositor.Server)) internal CompositionDrawingSurface(Compositor compositor) : base(compositor, new ServerCompositionDrawingSurface(compositor.Server))
@ -57,4 +58,6 @@ public class CompositionDrawingSurface : CompositionSurface
{ {
Dispatcher.UIThread.Post(Dispose); Dispatcher.UIThread.Post(Dispose);
} }
public new void Dispose() => base.Dispose();
} }

11
src/Avalonia.Base/Rendering/Composition/CompositionExperimentalAcrylicVisual.cs

@ -0,0 +1,11 @@
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition;
internal partial class CompositionExperimentalAcrylicVisual
{
internal CompositionExperimentalAcrylicVisual(Compositor compositor, Visual visual) : base(compositor,
new ServerCompositionExperimentalAcrylicVisual(compositor.Server, visual), visual)
{
}
}

28
src/Avalonia.Base/Rendering/Composition/CompositionObject.cs

@ -1,4 +1,6 @@
using System; using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
@ -14,7 +16,7 @@ namespace Avalonia.Rendering.Composition
/// Composition objects are the visual tree structure on which all other features of the composition API use and build on. /// 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 <see cref="CompositionVisual" /> objects each representing a single node in a Visual tree. /// The API allows developers to define and create one or many <see cref="CompositionVisual" /> objects each representing a single node in a Visual tree.
/// </summary> /// </summary>
public abstract class CompositionObject : IDisposable public abstract class CompositionObject : ICompositorSerializable
{ {
/// <summary> /// <summary>
/// The collection of implicit animations attached to this object. /// The collection of implicit animations attached to this object.
@ -22,7 +24,7 @@ namespace Avalonia.Rendering.Composition
public ImplicitAnimationCollection? ImplicitAnimations { get; set; } public ImplicitAnimationCollection? ImplicitAnimations { get; set; }
private protected InlineDictionary<CompositionProperty, IAnimationInstance> PendingAnimations; private protected InlineDictionary<CompositionProperty, IAnimationInstance> PendingAnimations;
internal CompositionObject(Compositor compositor, ServerObject? server) internal CompositionObject(Compositor compositor, SimpleServerObject? server)
{ {
Compositor = compositor; Compositor = compositor;
Server = server; Server = server;
@ -32,16 +34,25 @@ namespace Avalonia.Rendering.Composition
/// The associated Compositor /// The associated Compositor
/// </summary> /// </summary>
public Compositor Compositor { get; } public Compositor Compositor { get; }
internal ServerObject? Server { get; }
SimpleServerObject ICompositorSerializable.TryGetServer(Compositor c)
{
Debug.Assert(c == Compositor);
return Server ?? ThrowInvalidOperation();
}
internal SimpleServerObject? Server { get; }
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
private bool _registeredForSerialization; private bool _registeredForSerialization;
private static void ThrowInvalidOperation() => [MethodImpl(MethodImplOptions.NoInlining)]
private static SimpleServerObject ThrowInvalidOperation() =>
throw new InvalidOperationException("There is no server-side counterpart for this object"); throw new InvalidOperationException("There is no server-side counterpart for this object");
public void Dispose() protected internal void Dispose()
{ {
RegisterForSerialization(); if (!IsDisposed && Server != null)
Compositor.DisposeOnNextBatch(Server);
IsDisposed = true; IsDisposed = true;
} }
@ -128,16 +139,15 @@ namespace Avalonia.Rendering.Composition
Compositor.RegisterForSerialization(this); Compositor.RegisterForSerialization(this);
} }
internal void SerializeChanges(BatchStreamWriter writer) void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer)
{ {
Debug.Assert(c == Compositor);
_registeredForSerialization = false; _registeredForSerialization = false;
SerializeChangesCore(writer); SerializeChangesCore(writer);
} }
private protected virtual void SerializeChangesCore(BatchStreamWriter writer) private protected virtual void SerializeChangesCore(BatchStreamWriter writer)
{ {
if (Server is IDisposable)
writer.Write((byte)(IsDisposed ? 1 : 0));
} }
} }
} }

3
src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport; using Avalonia.Rendering.Composition.Transport;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see> // Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -130,7 +131,7 @@ namespace Avalonia.Rendering.Composition
else if (o.Value.Server == null) else if (o.Value.Server == null)
throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed");
else else
dic[o.Key] = new PropertySetSnapshot.Value(o.Value.Server); dic[o.Key] = new PropertySetSnapshot.Value((ServerObject)o.Value.Server);
} }
foreach (var v in _variants) foreach (var v in _variants)

2
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@ -11,7 +11,7 @@ namespace Avalonia.Rendering.Composition
/// <summary> /// <summary>
/// Represents the composition output (e. g. a window, embedded control, entire screen) /// Represents the composition output (e. g. a window, embedded control, entire screen)
/// </summary> /// </summary>
public partial class CompositionTarget internal partial class CompositionTarget
{ {
partial void OnRootChanged() partial void OnRootChanged()
{ {

9
src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs

@ -0,0 +1,9 @@
using Avalonia.Media;
// ReSharper disable CheckNamespace
namespace Avalonia.Rendering.Composition.Server;
internal partial class ServerCompositionSimpleTransform : ITransform
{
}

2
src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs

@ -12,7 +12,7 @@ public partial class Compositor
/// </summary> /// </summary>
/// <param name="surfaces">A factory method to create IRenderTarget to be called from the render thread</param> /// <param name="surfaces">A factory method to create IRenderTarget to be called from the render thread</param>
/// <returns></returns> /// <returns></returns>
public CompositionTarget CreateCompositionTarget(Func<IEnumerable<object>> surfaces) internal CompositionTarget CreateCompositionTarget(Func<IEnumerable<object>> surfaces)
{ {
return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces, DiagnosticTextRenderer)); return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces, DiagnosticTextRenderer));
} }

47
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@ -5,6 +5,7 @@ using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Animation.Easings; using Avalonia.Animation.Easings;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
@ -29,8 +30,10 @@ namespace Avalonia.Rendering.Composition
private Action _commit; private Action _commit;
private BatchStreamObjectPool<object?> _batchObjectPool = new(); private BatchStreamObjectPool<object?> _batchObjectPool = new();
private BatchStreamMemoryPool _batchMemoryPool = new(); private BatchStreamMemoryPool _batchMemoryPool = new();
private List<CompositionObject> _objectsForSerialization = new(); private Queue<ICompositorSerializable> _objectSerializationQueue = new();
private HashSet<ICompositorSerializable> _objectSerializationHashSet = new();
private Queue<Action> _invokeBeforeCommitWrite = new(), _invokeBeforeCommitRead = new(); private Queue<Action> _invokeBeforeCommitWrite = new(), _invokeBeforeCommitRead = new();
private HashSet<IDisposable> _disposeOnNextBatch = new();
internal ServerCompositor Server => _server; internal ServerCompositor Server => _server;
private Task? _pendingBatch; private Task? _pendingBatch;
private readonly object _pendingBatchLock = new(); private readonly object _pendingBatchLock = new();
@ -44,11 +47,13 @@ namespace Avalonia.Rendering.Composition
internal event Action? AfterCommit; internal event Action? AfterCommit;
/// <summary> /// <summary>
/// Creates a new compositor on a specified render loop that would use a particular GPU /// Creates a new compositor on a specified render loop that would use a particular GPU
/// </summary> /// </summary>
/// <param name="loop"></param> /// <param name="loop"></param>
/// <param name="gpu"></param> /// <param name="gpu"></param>
[PrivateApi]
public Compositor(IRenderLoop loop, IPlatformGraphics? gpu) public Compositor(IRenderLoop loop, IPlatformGraphics? gpu)
{ {
Loop = loop; Loop = loop;
@ -112,16 +117,30 @@ namespace Avalonia.Rendering.Composition
using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool))
{ {
foreach (var obj in _objectsForSerialization) while(_objectSerializationQueue.TryDequeue(out var obj))
{ {
writer.WriteObject(obj.Server); var serverObject = obj.TryGetServer(this);
obj.SerializeChanges(writer); if (serverObject != null)
{
writer.WriteObject(serverObject);
obj.SerializeChanges(this, writer);
#if DEBUG_COMPOSITOR_SERIALIZATION #if DEBUG_COMPOSITOR_SERIALIZATION
writer.Write(BatchStreamDebugMarkers.ObjectEndMagic); writer.Write(BatchStreamDebugMarkers.ObjectEndMagic);
writer.WriteObject(BatchStreamDebugMarkers.ObjectEndMarker); writer.WriteObject(BatchStreamDebugMarkers.ObjectEndMarker);
#endif #endif
}
}
_objectSerializationHashSet.Clear();
if (_disposeOnNextBatch.Count != 0)
{
writer.WriteObject(ServerCompositor.RenderThreadDisposeStartMarker);
writer.Write(_disposeOnNextBatch.Count);
foreach (var d in _disposeOnNextBatch)
writer.WriteObject(d);
_disposeOnNextBatch.Clear();
} }
_objectsForSerialization.Clear();
if (_pendingServerCompositorJobs.Count > 0) if (_pendingServerCompositorJobs.Count > 0)
{ {
writer.WriteObject(ServerCompositor.RenderThreadJobsStartMarker); writer.WriteObject(ServerCompositor.RenderThreadJobsStartMarker);
@ -152,13 +171,20 @@ namespace Avalonia.Rendering.Composition
} }
} }
internal void RegisterForSerialization(CompositionObject compositionObject) internal void RegisterForSerialization(ICompositorSerializable compositionObject)
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();
_objectsForSerialization.Add(compositionObject); if(_objectSerializationHashSet.Add(compositionObject))
_objectSerializationQueue.Enqueue(compositionObject);
RequestCommitAsync(); RequestCommitAsync();
} }
internal void DisposeOnNextBatch(SimpleServerObject obj)
{
if (obj is IDisposable disposable && _disposeOnNextBatch.Add(disposable))
RequestCommitAsync();
}
/// <summary> /// <summary>
/// Enqueues a callback to be called before the next scheduled commit. /// Enqueues a callback to be called before the next scheduled commit.
/// If there is no scheduled commit it automatically schedules one /// If there is no scheduled commit it automatically schedules one
@ -227,5 +253,8 @@ namespace Avalonia.Rendering.Composition
return new CompositionInterop(this, feature); return new CompositionInterop(this, feature);
} }
})); }));
internal bool UnitTestIsRegisteredForSerialization(ICompositorSerializable serializable) =>
_objectSerializationHashSet.Contains(serializable);
} }
} }

129
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs

@ -1,129 +0,0 @@
using System;
using Avalonia.Collections.Pooled;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
namespace Avalonia.Rendering.Composition.Drawing;
/// <summary>
/// A list of serialized drawing commands
/// </summary>
internal class CompositionDrawList : PooledList<IRef<IDrawOperation>>
{
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);
foreach (var r in this)
clone.Add(r.Clone());
return clone;
}
public void Render(IDrawingContextImpl canvas)
{
foreach (var cmd in this)
{
if (cmd.Item is IDrawOperationWithTransform hasTransform)
canvas.Transform = hasTransform.Transform;
cmd.Item.Render(canvas);
}
}
public void Render(IDrawingContextImpl canvas, Matrix transform)
{
foreach (var cmd in this)
{
if (cmd.Item is IDrawOperationWithTransform hasTransform)
canvas.Transform = hasTransform.Transform * transform;
cmd.Item.Render(canvas);
}
}
public Rect CalculateBounds()
{
var rect = default(Rect);
foreach (var cmd in this)
rect = rect.Union(cmd.Item.Bounds);
return rect;
}
public bool HitTest(Point pt)
{
foreach (var op in this)
if (op.Item.HitTest(pt))
return true;
return false;
}
}
/// <summary>
/// An helper class for building <see cref="CompositionDrawList"/>
/// </summary>
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);
}
}

37
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs

@ -1,37 +0,0 @@
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Drawing;
internal class CompositionDrawListSceneBrushContent : ISceneBrushContent
{
private readonly CompositionDrawList _drawList;
public CompositionDrawListSceneBrushContent(ImmutableTileBrush brush, CompositionDrawList drawList, Rect rect, bool useScalableRasterization)
{
Brush = brush;
Rect = rect;
UseScalableRasterization = useScalableRasterization;
_drawList = drawList;
}
public ITileBrush Brush { get; }
public Rect Rect { get; }
public double Opacity => Brush.Opacity;
public ITransform? Transform => Brush.Transform;
public RelativePoint TransformOrigin => Brush.TransformOrigin;
public void Dispose() => _drawList.Dispose();
public void Render(IDrawingContextImpl context, Matrix? transform)
{
if (transform.HasValue)
_drawList.Render(context, transform.Value);
else
_drawList.Render(context);
}
public bool UseScalableRasterization { get; }
}

371
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs

@ -1,371 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
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.Threading;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
namespace Avalonia.Rendering.Composition;
/// <summary>
/// An IDrawingContextImpl implementation that builds <see cref="CompositionDrawList"/>
/// </summary>
internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport
{
private CompositionDrawListBuilder _builder = new();
private int _drawOperationIndex;
private static ThreadSafeObjectPool<Stack<Matrix>> TransformStackPool { get; } =
ThreadSafeObjectPool<Stack<Matrix>>.Default;
private Stack<Matrix>? _transforms;
private static ThreadSafeObjectPool<Stack<bool>> OpacityMaskPopStackPool { get; } =
ThreadSafeObjectPool<Stack<bool>>.Default;
private Stack<bool>? _needsToPopOpacityMask;
public Matrix Transform { get; set; } = Matrix.Identity;
public void BeginUpdate(CompositionDrawList? list)
{
_builder.Reset(list);
_drawOperationIndex = 0;
}
public CompositionDrawList? EndUpdate()
{
// Make sure that any pending pop operations are completed
Dispose();
_builder.TrimTo(_drawOperationIndex);
return _builder.DrawOperations;
}
protected override void DisposeCore()
{
if (_transforms != null)
{
_transforms.Clear();
TransformStackPool.ReturnAndSetNull(ref _transforms);
}
if (_needsToPopOpacityMask != null)
{
_needsToPopOpacityMask.Clear();
_needsToPopOpacityMask = null;
}
}
protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
var next = NextDrawAs<GeometryNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, geometry))
{
Add(new GeometryNode(Transform, ConvertBrush(brush), pen, geometry));
}
else
{
++_drawOperationIndex;
}
}
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
{
var next = NextDrawAs<ImageNode>();
if (next == null ||
!next.Item.Equals(Transform, source, opacity, sourceRect, destRect))
{
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
protected override void DrawLineCore(IPen? pen, Point p1, Point p2)
{
if (pen is null)
{
return;
}
var next = NextDrawAs<LineNode>();
if (next == null || !next.Item.Equals(Transform, pen, p1, p2))
{
Add(new LineNode(Transform, pen, p1, p2));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rect,
BoxShadows boxShadows = default)
{
var next = NextDrawAs<RectangleNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows))
{
Add(new RectangleNode(Transform, ConvertBrush(brush), pen, rect, boxShadows));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
var next = NextDrawAs<ExperimentalAcrylicNode>();
if (next == null || !next.Item.Equals(Transform, material, rect))
{
Add(new ExperimentalAcrylicNode(Transform, material, rect));
}
else
{
++_drawOperationIndex;
}
}
protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect)
{
var next = NextDrawAs<EllipseNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect))
{
Add(new EllipseNode(Transform, ConvertBrush(brush), pen, rect));
}
else
{
++_drawOperationIndex;
}
}
public override void Custom(ICustomDrawOperation custom)
{
var next = NextDrawAs<CustomDrawOperation>();
if (next == null || !next.Item.Equals(Transform, custom))
Add(new CustomDrawOperation(custom, Transform));
else
++_drawOperationIndex;
}
public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
{
if (foreground is null)
{
return;
}
var next = NextDrawAs<GlyphRunNode>();
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun.PlatformImpl))
{
Add(new GlyphRunNode(Transform, ConvertBrush(foreground)!, glyphRun.PlatformImpl));
}
else
{
++_drawOperationIndex;
}
}
protected override void PushTransformCore(Matrix matrix)
{
_transforms ??= TransformStackPool.Get();
_transforms.Push(Transform);
Transform = matrix * Transform;
}
protected override void PopTransformCore() =>
Transform = (_transforms ?? throw new InvalidOperationException()).Pop();
protected override void PopClipCore()
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new ClipNode());
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
protected override void PopGeometryClipCore()
{
var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new GeometryClipNode());
}
else
{
++_drawOperationIndex;
}
}
protected override void PopOpacityCore()
{
var next = NextDrawAs<OpacityNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new OpacityNode());
}
else
{
++_drawOperationIndex;
}
}
protected override void PopOpacityMaskCore()
{
if (!_needsToPopOpacityMask!.Pop())
return;
var next = NextDrawAs<OpacityMaskNode>();
if (next == null || !next.Item.Equals(null, null))
{
Add(new OpacityMaskPopNode());
}
else
{
++_drawOperationIndex;
}
}
protected override void PushClipCore(Rect clip)
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new ClipNode(Transform, clip));
}
else
{
++_drawOperationIndex;
}
}
protected override void PushClipCore(RoundedRect clip)
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new ClipNode(Transform, clip));
}
else
{
++_drawOperationIndex;
}
}
protected override void PushGeometryClipCore(Geometry clip)
{
if (clip.PlatformImpl is null)
return;
var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Item.Equals(Transform, clip.PlatformImpl))
{
Add(new GeometryClipNode(Transform, clip.PlatformImpl));
}
else
{
++_drawOperationIndex;
}
}
protected override void PushOpacityCore(double opacity, Rect bounds)
{
var next = NextDrawAs<OpacityNode>();
if (next == null || !next.Item.Equals(opacity, bounds))
{
Add(new OpacityNode(opacity, bounds));
}
else
{
++_drawOperationIndex;
}
}
protected override void PushOpacityMaskCore(IBrush mask, Rect bounds)
{
var next = NextDrawAs<OpacityMaskNode>();
bool needsToPop = true;
if (next == null || !next.Item.Equals(mask, bounds))
{
var immutableMask = ConvertBrush(mask);
if (immutableMask != null)
Add(new OpacityMaskNode(immutableMask, bounds));
else
needsToPop = false;
}
else
{
++_drawOperationIndex;
}
_needsToPopOpacityMask ??= OpacityMaskPopStackPool.Get();
_needsToPopOpacityMask.Push(needsToPop);
}
private void Add<T>(T node) where T : class, IDrawOperation
{
if (_drawOperationIndex < _builder.Count)
{
_builder.ReplaceDrawOperation(_drawOperationIndex, node);
}
else
{
_builder.AddDrawOperation(node);
}
++_drawOperationIndex;
}
private IRef<T>? NextDrawAs<T>() where T : class, IDrawOperation
{
return _drawOperationIndex < _builder.Count
? _builder.DrawOperations![_drawOperationIndex] as IRef<T>
: null;
}
private IImmutableBrush? ConvertBrush(IBrush? brush)
{
if (brush is IMutableBrush mutable)
return mutable.ToImmutable();
if (brush is ISceneBrush sceneBrush)
return sceneBrush.CreateContent();
return (IImmutableBrush?)brush;
}
}

68
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition.Drawing.Nodes;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Drawing;
internal class CompositionRenderData : ICompositorSerializable, IDisposable
{
private readonly Compositor _compositor;
public CompositionRenderData(Compositor compositor)
{
_compositor = compositor;
Server = new ServerCompositionRenderData(compositor.Server);
}
public ServerCompositionRenderData Server { get; }
private PooledInlineList<ICompositionRenderResource> _resources;
private PooledInlineList<IRenderDataItem> _items;
private bool _itemsSent;
public void AddResource(ICompositionRenderResource resource) => _resources.Add(resource);
public void Add(IRenderDataItem item) => _items.Add(item);
public void Dispose()
{
if (!_itemsSent)
{
foreach(var i in _items)
if (i is IDisposable disp)
disp.Dispose();
}
_items.Dispose();
_itemsSent = false;
foreach(var r in _resources)
r.ReleaseOnCompositor(_compositor);
_resources.Dispose();
_compositor.DisposeOnNextBatch(Server);
}
public SimpleServerObject TryGetServer(Compositor c) => Server;
public void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
writer.Write(_items.Count);
foreach (var item in _items)
writer.WriteObject(item);
_itemsSent = true;
}
public bool HitTest(Point pt)
{
foreach (var op in _items)
{
if (op.HitTest(pt))
return true;
}
return false;
}
}

47
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs

@ -0,0 +1,47 @@
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Drawing;
internal class CompositionRenderDataSceneBrushContent : ISceneBrushContent
{
public CompositionRenderData RenderData { get; }
private Rect? _rect;
public CompositionRenderDataSceneBrushContent(ITileBrush brush, CompositionRenderData renderData, Rect? rect,
bool useScalableRasterization)
{
Brush = brush;
_rect = rect;
UseScalableRasterization = useScalableRasterization;
RenderData = renderData;
}
public ITileBrush Brush { get; }
public Rect Rect => _rect ?? (RenderData.Server?.Bounds ?? default);
public double Opacity => Brush.Opacity;
public ITransform? Transform => Brush.Transform;
public RelativePoint TransformOrigin => Brush.TransformOrigin;
public void Dispose()
{
// No-op on server
}
public void Render(IDrawingContextImpl context, Matrix? transform)
{
if (transform.HasValue)
{
var oldTransform = context.Transform;
context.Transform = transform.Value * oldTransform;
RenderData.Server.Render(context);
context.Transform = oldTransform;
}
else
RenderData.Server.Render(context);
}
public bool UseScalableRasterization { get; }
}

123
src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs

@ -0,0 +1,123 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Avalonia.Media;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Drawing;
internal class CompositorRefCountableResource<T> where T : SimpleServerObject
{
public T Value { get; private set; }
public int RefCount { get; private set; }
public CompositorRefCountableResource(T value)
{
Value = value;
RefCount = 1;
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void ThrowInvalidOperation() => throw new InvalidOperationException("This resource is disposed");
public void AddRef()
{
if (RefCount <= 0)
ThrowInvalidOperation();
RefCount++;
}
public bool Release(Compositor c)
{
if (RefCount <= 0)
ThrowInvalidOperation();
RefCount--;
if (RefCount == 0)
{
c.DisposeOnNextBatch(Value);
return true;
}
return false;
}
}
internal struct CompositorResourceHolder<T> where T : SimpleServerObject
{
private InlineDictionary<Compositor, CompositorRefCountableResource<T>> _dictionary;
public bool IsAttached => _dictionary.HasEntries;
public bool CreateOrAddRef(Compositor compositor, ICompositorSerializable owner, out T resource, Func<Compositor, T> factory)
{
if (_dictionary.TryGetValue(compositor, out var handle))
{
handle.AddRef();
resource = handle.Value;
return false;
}
resource = factory(compositor);
_dictionary.Add(compositor, new CompositorRefCountableResource<T>(resource));
compositor.RegisterForSerialization(owner);
return true;
}
public T? TryGetForCompositor(Compositor compositor)
{
if (_dictionary.TryGetValue(compositor, out var handle))
return handle.Value;
return default;
}
public T GetForCompositor(Compositor compositor)
{
if (_dictionary.TryGetValue(compositor, out var handle))
return handle.Value;
ThrowDoesNotExist();
return default;
}
[MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn]
static void ThrowDoesNotExist() => throw new InvalidOperationException("This resource doesn't exist on that compositor");
public bool Release(Compositor compositor)
{
if (!_dictionary.TryGetValue(compositor, out var handle))
ThrowDoesNotExist();
if (handle.Release(compositor))
{
_dictionary.Remove(compositor);
return true;
}
return false;
}
public void ProcessPropertyChangeNotification(AvaloniaPropertyChangedEventArgs change)
{
if (change.OldValue is ICompositionRenderResource oldResource)
TransitiveReleaseAll(oldResource);
if (change.NewValue is ICompositionRenderResource newResource)
TransitiveAddRefAll(newResource);
}
public void TransitiveReleaseAll(ICompositionRenderResource oldResource)
{
foreach(var kv in _dictionary)
oldResource.ReleaseOnCompositor(kv.Key);
}
public void TransitiveAddRefAll(ICompositionRenderResource newResource)
{
foreach (var kv in _dictionary)
newResource.AddRefOnCompositor(kv.Key);
}
public void RegisterForInvalidationOnAllCompositors(ICompositorSerializable serializable)
{
foreach (var kv in _dictionary)
kv.Key.RegisterForSerialization(serializable);
}
}

12
src/Avalonia.Base/Rendering/Composition/Drawing/ICompositionRenderResource.cs

@ -0,0 +1,12 @@
namespace Avalonia.Rendering.Composition.Drawing;
internal interface ICompositionRenderResource
{
void AddRefOnCompositor(Compositor c);
void ReleaseOnCompositor(Compositor c);
}
internal interface ICompositionRenderResource<T> : ICompositionRenderResource where T : class
{
T GetForCompositor(Compositor c);
}

81
src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing.Nodes;
using Avalonia.Threading;
namespace Avalonia.Rendering.Composition.Drawing;
internal class ImmediateRenderDataSceneBrushContent : ISceneBrushContent
{
private List<IRenderDataItem>? _items;
private readonly ThreadSafeObjectPool<List<IRenderDataItem>> _pool;
public ImmediateRenderDataSceneBrushContent(ITileBrush brush, List<IRenderDataItem> items, Rect? rect,
bool useScalableRasterization, ThreadSafeObjectPool<List<IRenderDataItem>> pool)
{
Brush = brush;
_items = items;
_pool = pool;
UseScalableRasterization = useScalableRasterization;
if (rect == null)
{
foreach (var i in _items)
rect = Rect.Union(rect, i.Bounds);
rect = ServerCompositionRenderData.ApplyRenderBoundsRounding(rect);
}
Rect = rect ?? default;
}
public ITileBrush Brush { get; }
public Rect Rect { get; }
public double Opacity => Brush.Opacity;
public ITransform? Transform => Brush.Transform;
public RelativePoint TransformOrigin => Brush.TransformOrigin;
public void Dispose()
{
if(_items == null)
return;
foreach (var i in _items)
(i as IDisposable)?.Dispose();
_items.Clear();
_pool.ReturnAndSetNull(ref _items);
}
void Render(IDrawingContextImpl context)
{
if (_items == null)
return;
var ctx = new RenderDataNodeRenderContext(context);
try
{
foreach (var i in _items)
i.Invoke(ref ctx);
}
finally
{
ctx.Dispose();
}
}
public void Render(IDrawingContextImpl context, Matrix? transform)
{
if (transform.HasValue)
{
var oldTransform = context.Transform;
context.Transform = transform.Value * oldTransform;
Render(context);
context.Transform = oldTransform;
}
else
Render(context);
}
public bool UseScalableRasterization { get; }
}

28
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs

@ -0,0 +1,28 @@
using System;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
class RenderDataBitmapNode : IRenderDataItem, IDisposable
{
public IRef<IBitmapImpl>? Bitmap { get; set; }
public double Opacity { get; set; }
public Rect SourceRect { get; set; }
public Rect DestRect { get; set; }
public bool HitTest(Point p) => DestRect.Contains(p);
public void Invoke(ref RenderDataNodeRenderContext context)
{
if (Bitmap != null)
context.Context.DrawBitmap(Bitmap.Item, Opacity, SourceRect, DestRect);
}
public Rect? Bounds => DestRect;
public void Dispose()
{
Bitmap?.Dispose();
Bitmap = null;
}
}

61
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs

@ -0,0 +1,61 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
class RenderDataEllipseNode :RenderDataBrushAndPenNode
{
public Rect Rect { get; set; }
bool Contains(double dx, double dy, double radiusX, double radiusY)
{
var rx2 = radiusX * radiusX;
var ry2 = radiusY * radiusY;
var distance = ry2 * dx * dx + rx2 * dy * dy;
return distance <= rx2 * ry2;
}
public override bool HitTest(Point p)
{
var center = Rect.Center;
var strokeThickness = ClientPen?.Thickness ?? 0;
var rx = Rect.Width / 2 + strokeThickness / 2;
var ry = Rect.Height / 2 + strokeThickness / 2;
var dx = p.X - center.X;
var dy = p.Y - center.Y;
if (Math.Abs(dx) > rx || Math.Abs(dy) > ry)
{
return false;
}
if (ServerBrush != null)
{
return Contains(dx, dy, rx, ry);
}
else if (strokeThickness > 0)
{
bool inStroke = Contains(dx, dy, rx, ry);
rx = Rect.Width / 2 - strokeThickness / 2;
ry = Rect.Height / 2 - strokeThickness / 2;
bool inInner = Contains(dx, dy, rx, ry);
return inStroke && !inInner;
}
return false;
}
public override void Invoke(ref RenderDataNodeRenderContext context) =>
context.Context.DrawEllipse(ServerBrush, ServerPen, Rect);
public override Rect? Bounds => Rect.Inflate(ServerPen?.Thickness ?? 0);
}

29
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs

@ -0,0 +1,29 @@
using System.Diagnostics;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
class RenderDataGeometryNode : RenderDataBrushAndPenNode
{
public IGeometryImpl? Geometry { get; set; }
public override bool HitTest(Point p)
{
if (Geometry == null)
return false;
return (ServerBrush != null // null check is safe
&& Geometry.FillContains(p)) ||
(ClientPen != null && Geometry.StrokeContains(ClientPen, p));
}
public override void Invoke(ref RenderDataNodeRenderContext context)
{
Debug.Assert(Geometry != null);
context.Context.DrawGeometry(ServerBrush, ServerPen, Geometry!);
}
public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen).CalculateBoundsWithLineCaps(ServerPen) ?? default;
}

35
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs

@ -0,0 +1,35 @@
using System;
using System.Diagnostics;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
class RenderDataGlyphRunNode : IRenderDataItemWithServerResources, IDisposable
{
public IBrush? ServerBrush { get; set; }
// Dispose only happens once, so it's safe to have one reference
public IRef<IGlyphRunImpl>? GlyphRun { get; set; }
public bool HitTest(Point p) => GlyphRun?.Item.Bounds.ContainsExclusive(p) ?? false;
public void Invoke(ref RenderDataNodeRenderContext context)
{
Debug.Assert(GlyphRun!.Item != null);
context.Context.DrawGlyphRun(ServerBrush, GlyphRun.Item);
}
public Rect? Bounds => GlyphRun?.Item?.Bounds ?? default;
public void Collect(IRenderDataServerResourcesCollector collector)
{
collector.AddRenderDataServerResource(ServerBrush);
}
public void Dispose()
{
GlyphRun?.Dispose();
GlyphRun = null;
}
}

65
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs

@ -0,0 +1,65 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
class RenderDataLineNode : IRenderDataItemWithServerResources
{
public IPen? ServerPen { get; set; }
public IPen? ClientPen { get; set; }
public Point P1 { get; set; }
public Point P2 { get; set; }
public bool HitTest(Point p)
{
if (ClientPen == null)
return false;
var halfThickness = ClientPen.Thickness / 2;
var minX = Math.Min(P1.X, P2.X) - halfThickness;
var maxX = Math.Max(P1.X, P2.X) + halfThickness;
var minY = Math.Min(P1.Y, P2.Y) - halfThickness;
var maxY = Math.Max(P1.Y, P2.Y) + halfThickness;
if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY)
return false;
var a = P1;
var b = P2;
//If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse.
//The distance from a point to a straight line is defined as the
//length of the vector formed by the point and the closest point of the segment
Vector ap = p - a;
var dot1 = Vector.Dot(b - a, ap);
if (dot1 < 0)
return ap.Length <= ClientPen.Thickness / 2;
Vector bp = p - b;
var dot2 = Vector.Dot(a - b, bp);
if (dot2 < 0)
return bp.Length <= halfThickness;
var bXaX = b.X - a.X;
var bYaY = b.Y - a.Y;
var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) /
(Math.Sqrt(bXaX * bXaX + bYaY * bYaY));
return Math.Abs(distance) <= halfThickness;
}
public void Invoke(ref RenderDataNodeRenderContext context)
=> context.Context.DrawLine(ServerPen, P1, P2);
public Rect? Bounds => LineBoundsHelper.CalculateBounds(P1, P2, ServerPen!);
public void Collect(IRenderDataServerResourcesCollector collector)
{
collector.AddRenderDataServerResource(ServerPen);
}
}

214
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs

@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
enum RenderDataPopNodeType
{
Transform,
Clip,
GeometryClip,
Opacity,
OpacityMask
}
interface IRenderDataServerResourcesCollector
{
void AddRenderDataServerResource(object? obj);
}
interface IRenderDataItemWithServerResources : IRenderDataItem
{
void Collect(IRenderDataServerResourcesCollector collector);
}
struct RenderDataNodeRenderContext : IDisposable
{
private Stack<Matrix>? _stack;
private static readonly ThreadSafeObjectPool<Stack<Matrix>> s_matrixStackPool = new();
public RenderDataNodeRenderContext(IDrawingContextImpl context)
{
Context = context;
}
public IDrawingContextImpl Context { get; }
public Stack<Matrix> MatrixStack
{
get => _stack ??= s_matrixStackPool.Get();
}
public void Dispose()
{
if (_stack != null)
{
_stack.Clear();
s_matrixStackPool.ReturnAndSetNull(ref _stack);
}
}
}
interface IRenderDataItem
{
/// <summary>
/// Renders the node to a drawing context.
/// </summary>
/// <param name="context">The drawing context.</param>
void Invoke(ref RenderDataNodeRenderContext context);
/// <summary>
/// Gets the bounds of the visible content in the node in global coordinates.
/// </summary>
Rect? Bounds { get; }
/// <summary>
/// Hit test the geometry in this node.
/// </summary>
/// <param name="p">The point in global coordinates.</param>
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
/// <remarks>
/// This method does not recurse to childs, if you want
/// to hit test children they must be hit tested manually.
/// </remarks>
bool HitTest(Point p);
}
class RenderDataCustomNode : IRenderDataItem
{
public ICustomDrawOperation? Operation { get; set; }
public bool HitTest(Point p) => Operation?.HitTest(p) ?? false;
public void Invoke(ref RenderDataNodeRenderContext context) => Operation?.Render(new(context.Context, false));
public Rect? Bounds => Operation?.Bounds;
}
abstract class RenderDataPushNode : IRenderDataItem, IDisposable
{
public PooledInlineList<IRenderDataItem> Children;
public abstract void Push(ref RenderDataNodeRenderContext context);
public abstract void Pop(ref RenderDataNodeRenderContext context);
public void Invoke(ref RenderDataNodeRenderContext context)
{
if (Children.Count == 0)
return;
Push(ref context);
foreach (var ch in Children)
ch.Invoke(ref context);
Pop(ref context);
}
public virtual Rect? Bounds
{
get
{
if (Children.Count == 0)
return null;
Rect? union = null;
foreach (var i in Children)
union = Rect.Union(union, i.Bounds);
return union;
}
}
public virtual bool HitTest(Point p)
{
if (Children.Count == 0)
return false;
foreach(var ch in Children)
if (ch.HitTest(p))
return true;
return false;
}
public void Dispose()
{
if (Children.Count > 0)
{
foreach(var ch in Children)
if (ch is RenderDataPushNode node)
node.Dispose();
Children.Dispose();
}
}
}
class RenderDataClipNode : RenderDataPushNode
{
public RoundedRect Rect { get; set; }
public override void Push(ref RenderDataNodeRenderContext context) =>
context.Context.PushClip(Rect);
public override void Pop(ref RenderDataNodeRenderContext context) =>
context.Context.PopClip();
public override bool HitTest(Point p)
{
if (!Rect.Rect.Contains(p))
return false;
return base.HitTest(p);
}
}
class RenderDataGeometryClipNode : RenderDataPushNode
{
public IGeometryImpl? Geometry { get; set; }
public bool Contains(Point p) => Geometry?.FillContains(p) ?? false;
public override void Push(ref RenderDataNodeRenderContext context)
{
if (Geometry != null)
context.Context.PushGeometryClip(Geometry);
}
public override void Pop(ref RenderDataNodeRenderContext context)
{
if (Geometry != null)
context.Context.PopGeometryClip();
}
public override bool HitTest(Point p)
{
if (Geometry != null && !Geometry.FillContains(p))
return false;
return base.HitTest(p);
}
}
class RenderDataOpacityNode : RenderDataPushNode
{
public double Opacity { get; set; }
public Rect BoundsRect { get; set; }
public override void Push(ref RenderDataNodeRenderContext context)
{
if (Opacity != 1)
context.Context.PushOpacity(Opacity, BoundsRect);
}
public override void Pop(ref RenderDataNodeRenderContext context)
{
if (Opacity != 1)
context.Context.PopOpacity();
}
}
abstract class RenderDataBrushAndPenNode : IRenderDataItemWithServerResources
{
public IBrush? ServerBrush { get; set; }
public IPen? ServerPen { get; set; }
public IPen? ClientPen { get; set; }
public void Collect(IRenderDataServerResourcesCollector collector)
{
collector.AddRenderDataServerResource(ServerBrush);
collector.AddRenderDataServerResource(ServerPen);
}
public abstract void Invoke(ref RenderDataNodeRenderContext context);
public abstract Rect? Bounds { get; }
public abstract bool HitTest(Point p);
}

27
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs

@ -0,0 +1,27 @@
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
class RenderDataPushMatrixNode : RenderDataPushNode
{
public Matrix Matrix { get; set; }
public override void Push(ref RenderDataNodeRenderContext context)
{
var current = context.Context.Transform;
context.MatrixStack.Push(current);
context.Context.Transform = Matrix * current;
}
public override void Pop(ref RenderDataNodeRenderContext context)
{
context.Context.Transform = context.MatrixStack.Pop();
}
public override bool HitTest(Point p)
{
if (Matrix.TryInvert(out var inverted))
return base.HitTest(p.Transform(inverted));
return false;
}
public override Rect? Bounds => base.Bounds?.TransformToAABB(Matrix);
}

25
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs

@ -0,0 +1,25 @@
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
class RenderDataOpacityMaskNode : RenderDataPushNode, IRenderDataItemWithServerResources
{
public IBrush? ServerBrush { get; set; }
public Rect BoundsRect { get; set; }
public void Collect(IRenderDataServerResourcesCollector collector)
{
collector.AddRenderDataServerResource(ServerBrush);
}
public override void Push(ref RenderDataNodeRenderContext context)
{
if (ServerBrush != null)
context.Context.PushOpacityMask(ServerBrush, BoundsRect);
}
public override void Pop(ref RenderDataNodeRenderContext context) =>
context.Context.PopOpacityMask();
}

30
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs

@ -0,0 +1,30 @@
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
class RenderDataRectangleNode : RenderDataBrushAndPenNode
{
public RoundedRect Rect { get; set; }
public BoxShadows BoxShadows { get; set; }
public override bool HitTest(Point p)
{
if (ServerBrush != null) // it's safe to check for null
{
var rect = Rect.Rect.Inflate((ClientPen?.Thickness / 2) ?? 0);
return rect.ContainsExclusive(p);
}
else
{
var borderRect = Rect.Rect.Inflate((ClientPen?.Thickness / 2) ?? 0);
var emptyRect = Rect.Rect.Deflate((ClientPen?.Thickness / 2) ?? 0);
return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p);
}
}
public override void Invoke(ref RenderDataNodeRenderContext context) =>
context.Context.DrawRectangle(ServerBrush, ServerPen, Rect, BoxShadows);
public override Rect? Bounds => BoxShadows.TransformBounds(Rect.Rect).Inflate((ServerPen?.Thickness ?? 0) / 2);
}

349
src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs

@ -0,0 +1,349 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing.Nodes;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Drawing;
internal class RenderDataDrawingContext : DrawingContext
{
private readonly Compositor? _compositor;
private CompositionRenderData? _renderData;
private HashSet<object>? _resourcesHashSet;
private static readonly ThreadSafeObjectPool<HashSet<object>> s_hashSetPool = new();
private CompositionRenderData RenderData
{
get
{
Debug.Assert(_compositor != null);
return _renderData ??= new(_compositor);
}
}
struct ParentStackItem
{
public RenderDataPushNode? Node;
public List<IRenderDataItem> Items;
}
private List<IRenderDataItem>? _currentItemList;
private static readonly ThreadSafeObjectPool<List<IRenderDataItem>> s_listPool = new();
private Stack<ParentStackItem>? _parentNodeStack;
private static readonly ThreadSafeObjectPool<Stack<ParentStackItem>> s_parentStackPool = new();
public RenderDataDrawingContext(Compositor? compositor)
{
_compositor = compositor;
}
void Add(IRenderDataItem item)
{
_currentItemList ??= s_listPool.Get();
_currentItemList.Add(item);
}
void Push(RenderDataPushNode? node = null)
{
// Push a fake no-op node so something could be popped by the corresponding Pop call
// Since there is no nesting, we don't update the item list
if (node == null)
{
(_parentNodeStack ??= s_parentStackPool.Get()).Push(default);
return;
}
Add(node);
(_parentNodeStack ??= s_parentStackPool.Get()).Push(new ParentStackItem
{
Node = node,
Items = _currentItemList!
});
_currentItemList = null;
}
void Pop<T>() where T : IRenderDataItem
{
var parent = _parentNodeStack!.Pop();
// No-op node
if (parent.Node == null)
return;
if (!(parent.Node is T))
throw new InvalidOperationException("Invalid Pop operation");
foreach(var item in _currentItemList!)
parent.Node.Children.Add(item);
_currentItemList.Clear();
s_listPool.ReturnAndSetNull(ref _currentItemList);
_currentItemList = parent.Items;
}
void AddResource(object? resource)
{
if (_compositor == null)
return;
if (resource == null
|| resource is IImmutableBrush
|| resource is ImmutablePen
|| resource is ImmutableTransform)
return;
if (resource is ICompositionRenderResource renderResource)
{
_resourcesHashSet ??= s_hashSetPool.Get();
if (!_resourcesHashSet.Add(renderResource))
return;
renderResource.AddRefOnCompositor(_compositor);
RenderData.AddResource(renderResource);
return;
}
throw new InvalidOperationException(resource.GetType().FullName + " can not be used with this DrawingContext");
}
protected override void DrawLineCore(IPen? pen, Point p1, Point p2)
{
if(pen == null)
return;
AddResource(pen);
Add(new RenderDataLineNode
{
ClientPen = pen,
ServerPen = pen.GetServer(_compositor),
P1 = p1,
P2 = p2
});
}
protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
if (brush == null && pen == null)
return;
AddResource(brush);
AddResource(pen);
Add(new RenderDataGeometryNode
{
ServerBrush = brush.GetServer(_compositor),
ServerPen = pen.GetServer(_compositor),
ClientPen = pen,
Geometry = geometry
});
}
protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default)
{
if (rrect.IsEmpty())
return;
if(brush == null && pen == null && boxShadows == default)
return;
AddResource(brush);
AddResource(pen);
Add(new RenderDataRectangleNode
{
ServerBrush = brush.GetServer(_compositor),
ServerPen = pen.GetServer(_compositor),
ClientPen = pen,
Rect = rrect,
BoxShadows = boxShadows
});
}
protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect)
{
if (rect.IsEmpty())
return;
if(brush == null && pen == null)
return;
AddResource(brush);
AddResource(pen);
Add(new RenderDataEllipseNode
{
ServerBrush = brush.GetServer(_compositor),
ServerPen = pen.GetServer(_compositor),
ClientPen = pen,
Rect = rect,
});
}
public override void Custom(ICustomDrawOperation custom) => Add(new RenderDataCustomNode());
public override void DrawGlyphRun(IBrush? foreground, GlyphRun? glyphRun)
{
if (foreground == null || glyphRun == null)
return;
AddResource(foreground);
Add(new RenderDataGlyphRunNode
{
ServerBrush = foreground.GetServer(_compositor),
GlyphRun = glyphRun.PlatformImpl.Clone()
});
}
protected override void PushClipCore(RoundedRect rect) => Push(new RenderDataClipNode
{
Rect = rect
});
protected override void PushClipCore(Rect rect) => Push(new RenderDataClipNode
{
Rect = rect
});
protected override void PushGeometryClipCore(Geometry? clip)
{
if (clip == null)
Push();
else
Push(new RenderDataGeometryClipNode
{
Geometry = clip?.PlatformImpl
});
}
protected override void PushOpacityCore(double opacity, Rect bounds)
{
if (opacity == 1)
Push();
else
Push(new RenderDataOpacityNode
{
Opacity = opacity,
BoundsRect = bounds
});
}
protected override void PushOpacityMaskCore(IBrush? mask, Rect bounds)
{
if(mask == null)
Push();
else
{
AddResource(mask);
Push(new RenderDataOpacityMaskNode
{
ServerBrush = mask.GetServer(_compositor),
BoundsRect = bounds
});
}
}
protected override void PushTransformCore(Matrix matrix)
{
if (matrix.IsIdentity)
Push();
else
Push(new RenderDataPushMatrixNode()
{
Matrix = matrix
});
}
protected override void PopClipCore() => Pop<RenderDataClipNode>();
protected override void PopGeometryClipCore() => Pop<RenderDataGeometryClipNode>();
protected override void PopOpacityCore() => Pop<RenderDataOpacityNode>();
protected override void PopOpacityMaskCore() => Pop<RenderDataOpacityMaskNode>();
protected override void PopTransformCore() => Pop<RenderDataPushMatrixNode>();
internal override void DrawBitmap(IRef<IBitmapImpl>? source, double opacity, Rect sourceRect, Rect destRect)
{
if (source == null || sourceRect.IsEmpty() || destRect.IsEmpty())
return;
Add(new RenderDataBitmapNode
{
Bitmap = source.Clone(),
Opacity = opacity,
SourceRect = sourceRect,
DestRect = destRect
});
}
void FlushStack()
{
// Flush stack
if (_parentNodeStack != null)
{
// TODO: throw error, unbalanced stack
while (_parentNodeStack.Count > 0)
Pop<IRenderDataItem>();
}
}
public CompositionRenderData? GetRenderResults()
{
Debug.Assert(_compositor != null);
FlushStack();
// Transfer items to RenderData
if (_currentItemList is { Count: > 0 })
{
foreach (var i in _currentItemList)
RenderData.Add(i);
_currentItemList.Clear();
}
var rv = _renderData;
_renderData = null;
_resourcesHashSet?.Clear();
if (rv != null)
_compositor.RegisterForSerialization(rv);
return rv;
}
public ImmediateRenderDataSceneBrushContent? GetImmediateSceneBrushContent(ITileBrush brush, Rect? rect, bool useScalableRasterization)
{
Debug.Assert(_compositor == null);
Debug.Assert(_resourcesHashSet == null);
Debug.Assert(_renderData == null);
FlushStack();
if (_currentItemList == null || _currentItemList.Count == 0)
return null;
var itemList = _currentItemList;
_currentItemList = null;
return new ImmediateRenderDataSceneBrushContent(brush, itemList, rect, useScalableRasterization, s_listPool);
}
public void Reset()
{
// This means that render data should be discarded
if (_renderData != null)
{
_renderData.Dispose();
_renderData = null;
}
_currentItemList?.Clear();
_parentNodeStack?.Clear();
_resourcesHashSet?.Clear();
}
protected override void DisposeCore()
{
Reset();
if (_resourcesHashSet != null)
s_hashSetPool.ReturnAndSetNull(ref _resourcesHashSet);
}
}

136
src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing.Nodes;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Drawing;
class ServerCompositionRenderData : SimpleServerRenderResource
{
private PooledInlineList<IRenderDataItem> _items;
private PooledInlineList<IServerRenderResource> _referencedResources;
private Rect? _bounds;
private bool _boundsValid;
private static readonly ThreadSafeObjectPool<Collector> s_resourceHashSetPool = new();
public ServerCompositionRenderData(ServerCompositor compositor) : base(compositor)
{
}
class Collector : IRenderDataServerResourcesCollector
{
public readonly HashSet<IServerRenderResource> Resources = new();
public void AddRenderDataServerResource(object? obj)
{
if (obj is IServerRenderResource res)
Resources.Add(res);
}
}
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{
Reset();
var count = reader.Read<int>();
_items.EnsureCapacity(count);
for (var c = 0; c < count; c++)
_items.Add(reader.ReadObject<IRenderDataItem>());
var collector = s_resourceHashSetPool.Get();
foreach(var item in _items)
if (item is IRenderDataItemWithServerResources resourceItem)
resourceItem.Collect(collector);
foreach (var r in collector.Resources)
{
_referencedResources.Add(r);
r.AddObserver(this);
}
collector.Resources.Clear();
s_resourceHashSetPool.ReturnAndSetNull(ref collector);
base.DeserializeChangesCore(reader, committedAt);
}
public Rect? Bounds
{
get
{
if (!_boundsValid)
{
_bounds = CalculateRenderBounds();
_boundsValid = true;
}
return _bounds;
}
}
private Rect? CalculateRenderBounds()
{
Rect? totalBounds = null;
foreach (var item in _items)
totalBounds = Rect.Union(totalBounds, item.Bounds);
return ApplyRenderBoundsRounding(totalBounds);
}
public static Rect? ApplyRenderBoundsRounding(Rect? rect)
{
if (rect != null)
{
var r = rect.Value;
// I don't believe that it's correct to do here (rather than in CompositionVisual),
// but it's the old behavior, so I'm keeping it for now
return new Rect(
new Point(Math.Floor(r.X), Math.Floor(r.Y)),
new Point(Math.Ceiling(r.Right), Math.Ceiling(r.Bottom)));
}
return null;
}
public override void DependencyQueuedInvalidate(IServerRenderResource sender)
{
_boundsValid = false;
base.DependencyQueuedInvalidate(sender);
}
public void Render(IDrawingContextImpl context)
{
var ctx = new RenderDataNodeRenderContext(context);
try
{
foreach (var item in _items)
item.Invoke(ref ctx);
}
finally
{
ctx.Dispose();
}
}
void Reset()
{
_bounds = null;
_boundsValid = false;
foreach (var r in _referencedResources)
r.RemoveObserver(this);
_referencedResources.Dispose();
foreach(var i in _items)
if (i is IDisposable disp)
disp.Dispose();
_items.Dispose();
}
public override void Dispose()
{
Reset();
base.Dispose();
}
}

12
src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionSimplePen.cs

@ -0,0 +1,12 @@
using System;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Rendering.Composition.Server;
internal partial class ServerCompositionSimplePen : IPen
{
IDashStyle? IPen.DashStyle => DashStyle;
}

56
src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs

@ -0,0 +1,56 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Avalonia.Media;
using Avalonia.Media.Immutable;
namespace Avalonia.Rendering.Composition.Drawing;
static class ServerResourceHelperExtensions
{
public static IBrush? GetServer(this IBrush? brush, Compositor? compositor)
{
if (compositor == null)
return brush;
if (brush == null)
return null;
if (brush is IImmutableBrush immutable)
return immutable;
if (brush is ICompositionRenderResource<IBrush> resource)
return resource.GetForCompositor(compositor);
ThrowNotCompatible(brush);
return null;
}
public static IPen? GetServer(this IPen? pen, Compositor? compositor)
{
if (compositor == null)
return pen;
if (pen == null)
return null;
if (pen is ImmutablePen immutable)
return immutable;
if (pen is ICompositionRenderResource<IPen> resource)
return resource.GetForCompositor(compositor);
ThrowNotCompatible(pen);
return null;
}
[MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn]
static void ThrowNotCompatible(object o) =>
throw new InvalidOperationException(o.GetType() + " is not compatible with composition");
public static ITransform? GetServer(this ITransform? transform, Compositor? compositor)
{
if (compositor == null)
return transform;
if (transform == null)
return null;
if (transform is ImmutableTransform immutable)
return immutable;
if (transform is ICompositionRenderResource<ITransform> resource)
resource.GetForCompositor(compositor);
ThrowNotCompatible(transform);
return null;
}
}

10
src/Avalonia.Base/Rendering/Composition/ICompositorSerializable.cs

@ -0,0 +1,10 @@
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Rendering.Composition;
internal interface ICompositorSerializable
{
SimpleServerObject? TryGetServer(Compositor c);
void SerializeChanges(Compositor c, BatchStreamWriter writer);
}

2
src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs

@ -68,7 +68,7 @@ namespace Avalonia.Rendering.Composition.Server
var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' ';
var run = _runs[effectiveChar - FirstChar]; var run = _runs[effectiveChar - FirstChar];
context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0); context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0);
context.DrawGlyphRun(foreground, run.PlatformImpl); context.DrawGlyphRun(foreground, run.PlatformImpl.Item);
offset += run.Bounds.Width; offset += run.Bounds.Width;
} }

9
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -2,6 +2,7 @@ using System;
using System.Numerics; using System.Numerics;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.SceneGraph;
@ -53,12 +54,12 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
_impl.Clear(color); _impl.Clear(color);
} }
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect) public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
{ {
_impl.DrawBitmap(source, opacity, sourceRect, destRect); _impl.DrawBitmap(source, opacity, sourceRect, destRect);
} }
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{ {
_impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); _impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect);
} }
@ -83,7 +84,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
_impl.DrawEllipse(brush, pen, rect); _impl.DrawEllipse(brush, pen, rect);
} }
public void DrawGlyphRun(IBrush? foreground, IRef<IGlyphRunImpl> glyphRun) public void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun)
{ {
_impl.DrawGlyphRun(foreground, glyphRun); _impl.DrawGlyphRun(foreground, glyphRun);
} }
@ -145,6 +146,8 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
{ {
if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic) if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic)
acrylic.DrawRectangle(material, rect); acrylic.DrawRectangle(material, rect);
else
_impl.DrawRectangle(new ImmutableSolidColorBrush(material.FallbackColor), null, rect);
} }
public void PushEffect(IEffect effect) public void PushEffect(IEffect effect)

24
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs

@ -20,7 +20,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
// This is needed for debugging purposes so we could see inspect the associated visual from debugger // This is needed for debugging purposes so we could see inspect the associated visual from debugger
public readonly Visual UiVisual; public readonly Visual UiVisual;
#endif #endif
private CompositionDrawList? _renderCommands; private ServerCompositionRenderData? _renderCommands;
public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor) public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor)
{ {
@ -29,32 +29,14 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
#endif #endif
} }
Rect? _contentBounds; public override Rect OwnContentBounds => _renderCommands?.Bounds ?? default;
public override Rect OwnContentBounds
{
get
{
if (_contentBounds == null)
{
var rect = default(Rect);
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 committedAt) protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{ {
if (reader.Read<byte>() == 1) if (reader.Read<byte>() == 1)
{ {
_renderCommands?.Dispose(); _renderCommands?.Dispose();
_renderCommands = reader.ReadObject<CompositionDrawList?>(); _renderCommands = reader.ReadObject<ServerCompositionRenderData?>();
_contentBounds = null;
} }
base.DeserializeChangesCore(reader, committedAt); base.DeserializeChangesCore(reader, committedAt);
} }

26
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs

@ -0,0 +1,26 @@
using Avalonia.Media.Immutable;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Server;
internal partial class ServerCompositionExperimentalAcrylicVisual
{
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
var cornerRadius = CornerRadius;
canvas.DrawRectangle(
Material,
new RoundedRect(
new Rect(0, 0, Size.X, Size.Y),
cornerRadius.TopLeft, cornerRadius.TopRight,
cornerRadius.BottomRight, cornerRadius.BottomLeft));
base.RenderCore(canvas, currentTransformedClip);
}
public override Rect OwnContentBounds => new(0, 0, Size.X, Size.Y);
public ServerCompositionExperimentalAcrylicVisual(ServerCompositor compositor, Visual v) : base(compositor, v)
{
}
}

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs

@ -13,7 +13,7 @@ internal partial class ServerCompositionSurfaceVisual
var bmp = Surface.Bitmap.Item; var bmp = Surface.Bitmap.Item;
//TODO: add a way to always render the whole bitmap instead of just assuming 96 DPI //TODO: add a way to always render the whole bitmap instead of just assuming 96 DPI
canvas.DrawBitmap(Surface.Bitmap, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect( canvas.DrawBitmap(Surface.Bitmap.Item, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect(
new Size(Size.X, Size.Y))); new Size(Size.X, Size.Y)));
} }

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -179,7 +179,7 @@ namespace Avalonia.Rendering.Composition.Server
if (_layer.CanBlit) if (_layer.CanBlit)
_layer.Blit(targetContext); _layer.Blit(targetContext);
else else
targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, targetContext.DrawBitmap(_layer, 1,
new Rect(_layerSize), new Rect(_layerSize),
new Rect(Size)); new Rect(Size));

22
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs

@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace Avalonia.Rendering.Composition.Server;
partial class ServerCompositor
{
private Queue<IServerRenderResource> _renderResourcesInvalidationQueue = new();
private HashSet<IServerRenderResource> _renderResourcesInvalidationSet = new();
public void ApplyEnqueuedRenderResourceChanges()
{
while (_renderResourcesInvalidationQueue.TryDequeue(out var obj))
obj.QueuedInvalidate();
_renderResourcesInvalidationSet.Clear();
}
public void EnqueueRenderResourceForInvalidation(IServerRenderResource resource)
{
if (_renderResourcesInvalidationSet.Add(resource))
_renderResourcesInvalidationQueue.Enqueue(resource);
}
}

23
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@ -18,7 +18,7 @@ namespace Avalonia.Rendering.Composition.Server
/// 2) triggers animation ticks /// 2) triggers animation ticks
/// 3) asks composition targets to render themselves /// 3) asks composition targets to render themselves
/// </summary> /// </summary>
internal class ServerCompositor : IRenderLoopTask internal partial class ServerCompositor : IRenderLoopTask
{ {
private readonly IRenderLoop _renderLoop; private readonly IRenderLoop _renderLoop;
@ -35,6 +35,7 @@ namespace Avalonia.Rendering.Composition.Server
private object _lock = new object(); private object _lock = new object();
private Thread? _safeThread; private Thread? _safeThread;
public PlatformRenderInterfaceContextManager RenderInterface { get; } public PlatformRenderInterfaceContextManager RenderInterface { get; }
internal static readonly object RenderThreadDisposeStartMarker = new();
internal static readonly object RenderThreadJobsStartMarker = new(); internal static readonly object RenderThreadJobsStartMarker = new();
internal static readonly object RenderThreadJobsEndMarker = new(); internal static readonly object RenderThreadJobsEndMarker = new();
@ -79,8 +80,14 @@ namespace Avalonia.Rendering.Composition.Server
ReadServerJobs(stream); ReadServerJobs(stream);
continue; continue;
} }
if (readObject == RenderThreadDisposeStartMarker)
{
ReadDisposeJobs(stream);
continue;
}
var target = (ServerObject)readObject!; var target = (SimpleServerObject)readObject!;
target.DeserializeChanges(stream, batch); target.DeserializeChanges(stream, batch);
#if DEBUG_COMPOSITOR_SERIALIZATION #if DEBUG_COMPOSITOR_SERIALIZATION
if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker)
@ -105,6 +112,16 @@ namespace Avalonia.Rendering.Composition.Server
_receivedJobQueue.Enqueue((Action)readObject!); _receivedJobQueue.Enqueue((Action)readObject!);
} }
void ReadDisposeJobs(BatchStreamReader reader)
{
var count = reader.Read<int>();
while (count > 0)
{
(reader.ReadObject() as IDisposable)?.Dispose();
count--;
}
}
void ExecuteServerJobs() void ExecuteServerJobs()
{ {
while(_receivedJobQueue.Count > 0) while(_receivedJobQueue.Count > 0)
@ -160,6 +177,8 @@ namespace Avalonia.Rendering.Composition.Server
animation.OnTick(); animation.OnTick();
_clockItemsToUpdate.Clear(); _clockItemsToUpdate.Clear();
ApplyEnqueuedRenderResourceChanges();
try try
{ {

11
src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs

@ -26,17 +26,6 @@ namespace Avalonia.Rendering.Composition.Server
base.DeserializeChangesCore(reader, committedAt); base.DeserializeChangesCore(reader, committedAt);
} }
public override long LastChangedBy
{
get
{
var seq = base.LastChangedBy;
foreach (var i in List)
seq = Math.Max(i.LastChangedBy, seq);
return seq;
}
}
public List<T>.Enumerator GetEnumerator() => List.GetEnumerator(); public List<T>.Enumerator GetEnumerator() => List.GetEnumerator();
public ServerList(ServerCompositor compositor) : base(compositor) public ServerList(ServerCompositor compositor) : base(compositor)

32
src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs

@ -15,12 +15,8 @@ namespace Avalonia.Rendering.Composition.Server
/// Server-side <see cref="CompositionObject" /> counterpart. /// Server-side <see cref="CompositionObject" /> counterpart.
/// Is responsible for animation activation and invalidation /// Is responsible for animation activation and invalidation
/// </summary> /// </summary>
internal abstract class ServerObject : IExpressionObject internal abstract class ServerObject : SimpleServerObject, IExpressionObject
{ {
public ServerCompositor Compositor { get; }
public virtual long LastChangedBy => ItselfLastChangedBy;
public long ItselfLastChangedBy { get; private set; }
private uint _activationCount; private uint _activationCount;
public bool IsActive => _activationCount != 0; public bool IsActive => _activationCount != 0;
private InlineDictionary<CompositionProperty, ServerObjectSubscriptionStore> _subscriptions; private InlineDictionary<CompositionProperty, ServerObjectSubscriptionStore> _subscriptions;
@ -42,9 +38,8 @@ namespace Avalonia.Rendering.Composition.Server
} }
} }
public ServerObject(ServerCompositor compositor) public ServerObject(ServerCompositor compositor) : base(compositor)
{ {
Compositor = compositor;
} }
public virtual ExpressionVariant GetPropertyForAnimation(string name) public virtual ExpressionVariant GetPropertyForAnimation(string name)
@ -90,13 +85,13 @@ namespace Avalonia.Rendering.Composition.Server
subs.Invalidate(); subs.Invalidate();
} }
protected void SetValue<T>(CompositionProperty prop, out T field, T value) protected new void SetValue<T>(CompositionProperty prop, ref T field, T value)
{ {
field = value; field = value;
InvalidateSubscriptions(prop); InvalidateSubscriptions(prop);
} }
protected T GetValue<T>(CompositionProperty prop, ref T field) protected new T GetValue<T>(CompositionProperty prop, ref T field)
{ {
if (_subscriptions.TryGetValue(prop, out var subs)) if (_subscriptions.TryGetValue(prop, out var subs))
subs.IsValid = true; subs.IsValid = true;
@ -143,11 +138,6 @@ namespace Avalonia.Rendering.Composition.Server
ValuesInvalidated(); ValuesInvalidated();
} }
protected virtual void ValuesInvalidated()
{
}
public void SubscribeToInvalidation(CompositionProperty member, IAnimationInstance animation) public void SubscribeToInvalidation(CompositionProperty member, IAnimationInstance animation)
{ {
if (!_subscriptions.TryGetValue(member, out var store)) if (!_subscriptions.TryGetValue(member, out var store))
@ -164,19 +154,5 @@ namespace Avalonia.Rendering.Composition.Server
} }
public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null; public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null;
protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{
if (this is IDisposable disp
&& reader.Read<byte>() == 1)
disp.Dispose();
}
public void DeserializeChanges(BatchStreamReader reader, Batch batch)
{
DeserializeChangesCore(reader, batch.CommittedAt);
ValuesInvalidated();
ItselfLastChangedBy = batch.SequenceId;
}
} }
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save