Browse Source

Merge branch 'master' into fix_macos_ctrl

pull/5169/head
Max Katz 4 years ago
committed by GitHub
parent
commit
db59b2ecfb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
  2. 37
      src/Avalonia.Base/GeometryCollection.cs
  3. 18
      src/Avalonia.Base/Media/DrawingCollection.cs
  4. 420
      src/Avalonia.Base/Media/DrawingGroup.cs
  5. 129
      src/Avalonia.Base/Media/FormattedText.cs
  6. 45
      src/Avalonia.Base/Media/GeometryCollection.cs
  7. 12
      src/Avalonia.Base/Media/GeometryDrawing.cs
  8. 58
      src/Avalonia.Base/Media/GeometryGroup.cs
  9. 10
      src/Avalonia.Base/Media/GlyphRun.cs
  10. 3
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  11. 95
      src/Avalonia.Base/Styling/Styles.cs
  12. 4
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  13. 26
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  14. 22
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  15. 14
      tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs
  16. 2
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  17. 2
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  18. 35
      tests/Avalonia.RenderTests/Media/GlyphRunTests.cs
  19. 4
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  20. BIN
      tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png
  21. BIN
      tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

4
samples/RenderDemo/Pages/FormattedTextPage.axaml.cs

@ -55,6 +55,10 @@ namespace RenderDemo.Pages
formattedText.SetFontStyle(FontStyle.Italic, 28, 28); formattedText.SetFontStyle(FontStyle.Italic, 28, 28);
context.DrawText(formattedText, new Point(10, 0)); context.DrawText(formattedText, new Point(10, 0));
var geometry = formattedText.BuildGeometry(new Point(10 + formattedText.Width + 10, 0));
context.DrawGeometry(gradient, null, geometry);
} }
} }
} }

37
src/Avalonia.Base/GeometryCollection.cs

@ -1,37 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using Avalonia.Animation;
#nullable enable
namespace Avalonia.Media
{
public class GeometryCollection : Animatable, IList<Geometry>, IReadOnlyList<Geometry>
{
private List<Geometry> _inner;
public GeometryCollection() => _inner = new List<Geometry>();
public GeometryCollection(IEnumerable<Geometry> collection) => _inner = new List<Geometry>(collection);
public GeometryCollection(int capacity) => _inner = new List<Geometry>(capacity);
public Geometry this[int index]
{
get => _inner[index];
set => _inner[index] = value;
}
public int Count => _inner.Count;
public bool IsReadOnly => false;
public void Add(Geometry item) => _inner.Add(item);
public void Clear() => _inner.Clear();
public bool Contains(Geometry item) => _inner.Contains(item);
public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
public IEnumerator<Geometry> GetEnumerator() => _inner.GetEnumerator();
public int IndexOf(Geometry item) => _inner.IndexOf(item);
public void Insert(int index, Geometry item) => _inner.Insert(index, item);
public bool Remove(Geometry item) => _inner.Remove(item);
public void RemoveAt(int index) => _inner.RemoveAt(index);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

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

@ -0,0 +1,18 @@
using System.Collections.Generic;
using Avalonia.Collections;
namespace Avalonia.Media
{
public sealed class DrawingCollection : AvaloniaList<Drawing>
{
public DrawingCollection()
{
ResetBehavior = ResetBehavior.Remove;
}
public DrawingCollection(IEnumerable<Drawing> items) : base(items)
{
ResetBehavior = ResetBehavior.Remove;
}
}
}

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

@ -1,6 +1,10 @@
using Avalonia.Collections; using System;
using System.Collections.Generic;
using Avalonia.Media.Imaging;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -18,6 +22,14 @@ namespace Avalonia.Media
public static readonly StyledProperty<IBrush> OpacityMaskProperty = public static readonly StyledProperty<IBrush> OpacityMaskProperty =
AvaloniaProperty.Register<DrawingGroup, IBrush>(nameof(OpacityMask)); AvaloniaProperty.Register<DrawingGroup, IBrush>(nameof(OpacityMask));
public static readonly DirectProperty<DrawingGroup, DrawingCollection> ChildrenProperty =
AvaloniaProperty.RegisterDirect<DrawingGroup, DrawingCollection>(
nameof(Children),
o => o.Children,
(o, v) => o.Children = v);
private DrawingCollection _children = new DrawingCollection();
public double Opacity public double Opacity
{ {
get => GetValue(OpacityProperty); get => GetValue(OpacityProperty);
@ -42,8 +54,23 @@ namespace Avalonia.Media
set => SetValue(OpacityMaskProperty, value); set => SetValue(OpacityMaskProperty, value);
} }
/// <summary>
/// Gets or sets the collection that contains the child geometries.
/// </summary>
[Content] [Content]
public AvaloniaList<Drawing> Children { get; } = new AvaloniaList<Drawing>(); public DrawingCollection Children
{
get => _children;
set
{
SetAndRaise(ChildrenProperty, ref _children, value);
}
}
public DrawingContext Open()
{
return new DrawingContext(new DrawingGroupDrawingContext(this));
}
public override void Draw(DrawingContext context) public override void Draw(DrawingContext context)
{ {
@ -75,5 +102,394 @@ namespace Avalonia.Media
return rect; return rect;
} }
private class DrawingGroupDrawingContext : IDrawingContextImpl
{
private readonly DrawingGroup _drawingGroup;
private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
private Matrix _transform;
private bool _disposed;
// Root drawing created by this DrawingContext.
//
// If there is only a single child of the root DrawingGroup, _rootDrawing
// will reference the single child, and the root _currentDrawingGroup
// value will be null. Otherwise, _rootDrawing will reference the
// root DrawingGroup, and be the same value as the root _currentDrawingGroup.
//
// Either way, _rootDrawing always references the root drawing.
protected Drawing? _rootDrawing;
// Current DrawingGroup that new children are added to
protected DrawingGroup? _currentDrawingGroup;
// Previous values of _currentDrawingGroup
private Stack<DrawingGroup?>? _previousDrawingGroupStack;
public DrawingGroupDrawingContext(DrawingGroup drawingGroup)
{
_drawingGroup = drawingGroup;
}
public Matrix Transform
{
get => _transform;
set
{
_transform = value;
PushTransform(new MatrixTransform(value));
}
}
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{
if ((brush == null) && (pen == null))
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateEllipseGeometry(rect);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
if (((brush == null) && (pen == null)) || (geometry == null))
{
return;
}
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
if (foreground == null || glyphRun == null)
{
return;
}
// Add a GlyphRunDrawing to the Drawing graph
GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing
{
Foreground = foreground,
GlyphRun = glyphRun,
};
// Add Drawing to the Drawing graph
AddDrawing(glyphRunDrawing);
}
public void DrawLine(IPen pen, Point p1, Point p2)
{
if (pen == null)
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry));
}
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default)
{
if ((brush == null) && (pen == null))
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void Clear(Color color)
{
throw new NotImplementedException();
}
public IDrawingContextLayerImpl CreateLayer(Size size)
{
throw new NotImplementedException();
}
public void Custom(ICustomDrawOperation custom)
{
throw new NotImplementedException();
}
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
{
throw new NotImplementedException();
}
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{
throw new NotImplementedException();
}
public void PopBitmapBlendMode()
{
throw new NotImplementedException();
}
public void PopClip()
{
throw new NotImplementedException();
}
public void PopGeometryClip()
{
throw new NotImplementedException();
}
public void PopOpacity()
{
throw new NotImplementedException();
}
public void PopOpacityMask()
{
throw new NotImplementedException();
}
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
{
throw new NotImplementedException();
}
public void PushClip(Rect clip)
{
throw new NotImplementedException();
}
public void PushClip(RoundedRect clip)
{
throw new NotImplementedException();
}
public void PushGeometryClip(IGeometryImpl clip)
{
throw new NotImplementedException();
}
public void PushOpacity(double opacity)
{
throw new NotImplementedException();
}
public void PushOpacityMask(IBrush mask, Rect bounds)
{
throw new NotImplementedException();
}
public void Dispose()
{
// Dispose may be called multiple times without throwing
// an exception.
if (!_disposed)
{
// Match any outstanding Push calls with a Pop
if (_previousDrawingGroupStack != null)
{
int stackCount = _previousDrawingGroupStack.Count;
for (int i = 0; i < stackCount; i++)
{
Pop();
}
}
// Call CloseCore with the root DrawingGroup's children
DrawingCollection rootChildren;
if (_currentDrawingGroup != null)
{
// If we created a root DrawingGroup because multiple elements
// exist at the root level, provide it's Children collection
// directly.
rootChildren = _currentDrawingGroup.Children;
}
else
{
// Create a new DrawingCollection if we didn't create a
// root DrawingGroup because the root level only contained
// a single child.
//
// This collection is needed by DrawingGroup.Open because
// Open always replaces it's Children collection. It isn't
// strictly needed for Append, but always using a collection
// simplifies the TransactionalAppend implementation (i.e.,
// a seperate implemention isn't needed for a single element)
rootChildren = new DrawingCollection();
//
// We may need to opt-out of inheritance through the new Freezable.
// This is controlled by this.CanBeInheritanceContext.
//
if (_rootDrawing != null)
{
rootChildren.Add(_rootDrawing);
}
}
// Inform our derived classes that Close was called
_drawingGroup.Children = rootChildren;
_disposed = true;
}
}
/// <summary>
/// Pop
/// </summary>
private void Pop()
{
// Verify that Pop hasn't been called too many times
if ((_previousDrawingGroupStack == null) || (_previousDrawingGroupStack.Count == 0))
{
throw new InvalidOperationException("DrawingGroupStack count missmatch.");
}
// Restore the previous value of the current drawing group
_currentDrawingGroup = _previousDrawingGroupStack.Pop();
}
/// <summary>
/// PushTransform -
/// Push a Transform which will apply to all drawing operations until the corresponding
/// Pop.
/// </summary>
/// <param name="transform"> The Transform to push. </param>
private void PushTransform(Transform transform)
{
// Instantiate a new drawing group and set it as the _currentDrawingGroup
var drawingGroup = PushNewDrawingGroup();
// Set the transform on the new DrawingGroup
drawingGroup.Transform = transform;
}
/// <summary>
/// Creates a new DrawingGroup for a Push* call by setting the
/// _currentDrawingGroup to a newly instantiated DrawingGroup,
/// and saving the previous _currentDrawingGroup value on the
/// _previousDrawingGroupStack.
/// </summary>
private DrawingGroup PushNewDrawingGroup()
{
// Instantiate a new drawing group
DrawingGroup drawingGroup = new DrawingGroup();
// Add it to the drawing graph, like any other Drawing
AddDrawing(drawingGroup);
// Lazily allocate the stack when it is needed because many uses
// of DrawingDrawingContext will have a depth of one.
if (null == _previousDrawingGroupStack)
{
_previousDrawingGroupStack = new Stack<DrawingGroup?>(2);
}
// Save the previous _currentDrawingGroup value.
//
// If this is the first call, the value of _currentDrawingGroup
// will be null because AddDrawing doesn't create a _currentDrawingGroup
// for the first drawing. Having null on the stack is valid, and simply
// denotes that this new DrawingGroup is the first child in the root
// DrawingGroup. It is also possible for the first value on the stack
// to be non-null, which means that the root DrawingGroup has other
// children.
_previousDrawingGroupStack.Push(_currentDrawingGroup);
// Set this drawing group as the current one so that subsequent drawing's
// are added as it's children until Pop is called.
_currentDrawingGroup = drawingGroup;
return drawingGroup;
}
/// <summary>
/// Contains the functionality common to GeometryDrawing operations of
/// instantiating the GeometryDrawing, setting it's Freezable state,
/// and Adding it to the Drawing Graph.
/// </summary>
private void AddNewGeometryDrawing(IBrush? brush, IPen? pen, Geometry? geometry)
{
if (geometry == null)
{
throw new ArgumentNullException(nameof(geometry));
}
// Instantiate the GeometryDrawing
GeometryDrawing geometryDrawing = new GeometryDrawing
{
// We may need to opt-out of inheritance through the new Freezable.
// This is controlled by this.CanBeInheritanceContext.
Brush = brush,
Pen = pen,
Geometry = geometry
};
// Add it to the drawing graph
AddDrawing(geometryDrawing);
}
/// <summary>
/// Adds a new Drawing to the DrawingGraph.
///
/// This method avoids creating a DrawingGroup for the common case
/// where only a single child exists in the root DrawingGroup.
/// </summary>
private void AddDrawing(Drawing newDrawing)
{
if (newDrawing == null)
{
throw new ArgumentNullException(nameof(newDrawing));
}
if (_rootDrawing == null)
{
// When a DrawingGroup is set, it should be made the root if
// a root drawing didnt exist.
Contract.Requires<NotSupportedException>(_currentDrawingGroup == null);
// If this is the first Drawing being added, avoid creating a DrawingGroup
// and set this drawing as the root drawing. This optimizes the common
// case where only a single child exists in the root DrawingGroup.
_rootDrawing = newDrawing;
}
else if (_currentDrawingGroup == null)
{
// When the second drawing is added at the root level, set a
// DrawingGroup as the root and add both drawings to it.
// Instantiate the DrawingGroup
_currentDrawingGroup = new DrawingGroup();
// Add both Children
_currentDrawingGroup.Children.Add(_rootDrawing);
_currentDrawingGroup.Children.Add(newDrawing);
// Set the new DrawingGroup as the current
_rootDrawing = _currentDrawingGroup;
}
else
{
// If there already is a current drawing group, then simply add
// the new drawing too it.
_currentDrawingGroup.Children.Add(newDrawing);
}
}
}
} }
} }

129
src/Avalonia.Base/Media/FormattedText.cs

@ -1223,7 +1223,7 @@ namespace Avalonia.Media
public double OverhangTrailing public double OverhangTrailing
{ {
get get
{ {
return BlackBoxMetrics.OverhangTrailing; return BlackBoxMetrics.OverhangTrailing;
} }
} }
@ -1252,6 +1252,46 @@ namespace Avalonia.Media
} }
} }
/// <summary>
/// Obtains geometry for the text, including underlines and strikethroughs.
/// </summary>
/// <param name="origin">The left top origin of the resulting geometry.</param>
/// <returns>The geometry returned contains the combined geometry
/// of all of the glyphs, underlines and strikeThroughs that represent the formatted text.
/// Overlapping contours are merged by performing a Boolean union operation.</returns>
public Geometry? BuildGeometry(Point origin)
{
GeometryGroup? accumulatedGeometry = null;
var lineOrigin = origin;
DrawingGroup drawing = new DrawingGroup();
using (var ctx = drawing.Open())
{
using (var enumerator = GetEnumerator())
{
while (enumerator.MoveNext())
{
var currentLine = enumerator.Current;
if (currentLine != null)
{
currentLine.Draw(ctx, lineOrigin);
AdvanceLineOrigin(ref lineOrigin, currentLine);
}
}
}
}
Transform? transform = new TranslateTransform(origin.X, origin.Y);
// recursively go down the DrawingGroup to build up the geometry
CombineGeometryRecursive(drawing, ref transform, ref accumulatedGeometry);
return accumulatedGeometry;
}
/// <summary> /// <summary>
/// Draws the text object /// Draws the text object
/// </summary> /// </summary>
@ -1284,6 +1324,93 @@ namespace Avalonia.Media
} }
} }
private void CombineGeometryRecursive(Drawing drawing, ref Transform? transform, ref GeometryGroup? accumulatedGeometry)
{
if (drawing is DrawingGroup group)
{
transform = group.Transform;
if (group.Children is DrawingCollection children)
{
// recursively go down for DrawingGroup
foreach (var child in children)
{
CombineGeometryRecursive(child, ref transform, ref accumulatedGeometry);
}
}
}
else
{
if (drawing is GlyphRunDrawing glyphRunDrawing)
{
// process glyph run
var glyphRun = glyphRunDrawing.GlyphRun;
if (glyphRun != null)
{
var glyphRunGeometry = glyphRun.BuildGeometry();
glyphRunGeometry.Transform = transform;
if (accumulatedGeometry == null)
{
accumulatedGeometry = new GeometryGroup
{
FillRule = FillRule.NonZero
};
}
accumulatedGeometry.Children.Add(glyphRunGeometry);
}
}
else
{
if (drawing is GeometryDrawing geometryDrawing)
{
// process geometry (i.e. TextDecoration on the line)
var geometry = geometryDrawing.Geometry;
if (geometry != null)
{
geometry.Transform = transform;
if (geometry is LineGeometry lineGeometry)
{
// For TextDecoration drawn by DrawLine(), the geometry is a LineGeometry which has no
// bounding area. So this line won't show up. Work aroud it by increase the Bounding rect
// to be Pen's thickness
var bounds = lineGeometry.Bounds;
if (bounds.Height == 0)
{
bounds = bounds.WithHeight(geometryDrawing.Pen?.Thickness ?? 0);
}
else if (bounds.Width == 0)
{
bounds = bounds.WithWidth(geometryDrawing.Pen?.Thickness ?? 0);
}
// convert the line geometry into a rectangle geometry
// we lost line cap info here
geometry = new RectangleGeometry(bounds);
}
if (accumulatedGeometry == null)
{
accumulatedGeometry = new GeometryGroup
{
FillRule = FillRule.NonZero
};
}
accumulatedGeometry.Children.Add(geometry);
}
}
}
}
}
private CachedMetrics DrawAndCalculateMetrics(DrawingContext? drawingContext, Point drawingOffset, bool getBlackBoxMetrics) private CachedMetrics DrawAndCalculateMetrics(DrawingContext? drawingContext, Point drawingOffset, bool getBlackBoxMetrics)
{ {
var metrics = new CachedMetrics(); var metrics = new CachedMetrics();

45
src/Avalonia.Base/Media/GeometryCollection.cs

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
#nullable enable
namespace Avalonia.Media
{
public sealed class GeometryCollection : AvaloniaList<Geometry>
{
public GeometryCollection()
{
ResetBehavior = ResetBehavior.Remove;
this.ForEachItem(
x =>
{
Parent?.Invalidate();
},
x =>
{
Parent?.Invalidate();
},
() => throw new NotSupportedException());
}
public GeometryCollection(IEnumerable<Geometry> items) : base(items)
{
ResetBehavior = ResetBehavior.Remove;
this.ForEachItem(
x =>
{
Parent?.Invalidate();
},
x =>
{
Parent?.Invalidate();
},
() => throw new NotSupportedException());
}
public GeometryGroup? Parent { get; set; }
}
}

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

@ -21,14 +21,14 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Defines the <see cref="Brush"/> property. /// Defines the <see cref="Brush"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IBrush> BrushProperty = public static readonly StyledProperty<IBrush?> BrushProperty =
AvaloniaProperty.Register<GeometryDrawing, IBrush>(nameof(Brush), Brushes.Transparent); AvaloniaProperty.Register<GeometryDrawing, IBrush?>(nameof(Brush), Brushes.Transparent);
/// <summary> /// <summary>
/// Defines the <see cref="Pen"/> property. /// Defines the <see cref="Pen"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Pen> PenProperty = public static readonly StyledProperty<Pen?> PenProperty =
AvaloniaProperty.Register<GeometryDrawing, Pen>(nameof(Pen)); AvaloniaProperty.Register<GeometryDrawing, Pen?>(nameof(Pen));
/// <summary> /// <summary>
/// Gets or sets the <see cref="Avalonia.Media.Geometry"/> that describes the shape of this <see cref="GeometryDrawing"/>. /// Gets or sets the <see cref="Avalonia.Media.Geometry"/> that describes the shape of this <see cref="GeometryDrawing"/>.
@ -43,7 +43,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets the <see cref="Avalonia.Media.IBrush"/> used to fill the interior of the shape described by this <see cref="GeometryDrawing"/>. /// Gets or sets the <see cref="Avalonia.Media.IBrush"/> used to fill the interior of the shape described by this <see cref="GeometryDrawing"/>.
/// </summary> /// </summary>
public IBrush Brush public IBrush? Brush
{ {
get => GetValue(BrushProperty); get => GetValue(BrushProperty);
set => SetValue(BrushProperty, value); set => SetValue(BrushProperty, value);
@ -52,7 +52,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets the <see cref="Avalonia.Media.IPen"/> used to stroke this <see cref="GeometryDrawing"/>. /// Gets or sets the <see cref="Avalonia.Media.IPen"/> used to stroke this <see cref="GeometryDrawing"/>.
/// </summary> /// </summary>
public IPen Pen public IPen? Pen
{ {
get => GetValue(PenProperty); get => GetValue(PenProperty);
set => SetValue(PenProperty, value); set => SetValue(PenProperty, value);

58
src/Avalonia.Base/GeometryGroup.cs → src/Avalonia.Base/Media/GeometryGroup.cs

@ -1,7 +1,4 @@
using System; using Avalonia.Metadata;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
#nullable enable #nullable enable
@ -13,29 +10,36 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public class GeometryGroup : Geometry public class GeometryGroup : Geometry
{ {
public static readonly DirectProperty<GeometryGroup, GeometryCollection?> ChildrenProperty = public static readonly DirectProperty<GeometryGroup, GeometryCollection> ChildrenProperty =
AvaloniaProperty.RegisterDirect<GeometryGroup, GeometryCollection?> ( AvaloniaProperty.RegisterDirect<GeometryGroup, GeometryCollection> (
nameof(Children), nameof(Children),
o => o.Children, o => o.Children,
(o, v) => o.Children = v); (o, v)=> o.Children = v);
public static readonly StyledProperty<FillRule> FillRuleProperty = public static readonly StyledProperty<FillRule> FillRuleProperty =
AvaloniaProperty.Register<GeometryGroup, FillRule>(nameof(FillRule)); AvaloniaProperty.Register<GeometryGroup, FillRule>(nameof(FillRule));
private GeometryCollection? _children; private GeometryCollection _children;
private bool _childrenSet;
public GeometryGroup()
{
_children = new GeometryCollection
{
Parent = this
};
}
/// <summary> /// <summary>
/// Gets or sets the collection that contains the child geometries. /// Gets or sets the collection that contains the child geometries.
/// </summary> /// </summary>
[Content] [Content]
public GeometryCollection? Children public GeometryCollection Children
{ {
get => _children ??= (!_childrenSet ? new GeometryCollection() : null); get => _children;
set set
{ {
SetAndRaise(ChildrenProperty, ref _children, value); OnChildrenChanged(_children, value);
_childrenSet = true; SetAndRaise(ChildrenProperty, ref _children, value);
} }
} }
@ -52,16 +56,28 @@ namespace Avalonia.Media
public override Geometry Clone() public override Geometry Clone()
{ {
var result = new GeometryGroup { FillRule = FillRule, Transform = Transform }; var result = new GeometryGroup { FillRule = FillRule, Transform = Transform };
if (_children?.Count > 0)
if (_children.Count > 0)
{
result.Children = new GeometryCollection(_children); result.Children = new GeometryCollection(_children);
}
return result; return result;
} }
protected void OnChildrenChanged(GeometryCollection oldChildren, GeometryCollection newChildren)
{
oldChildren.Parent = null;
newChildren.Parent = this;
}
protected override IGeometryImpl? CreateDefiningGeometry() protected override IGeometryImpl? CreateDefiningGeometry()
{ {
if (_children?.Count > 0) if (_children.Count > 0)
{ {
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
return factory.CreateGeometryGroup(FillRule, _children); return factory.CreateGeometryGroup(FillRule, _children);
} }
@ -72,10 +88,18 @@ namespace Avalonia.Media
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == ChildrenProperty || change.Property == FillRuleProperty) switch (change.Property.Name)
{ {
InvalidateGeometry(); case nameof(FillRule):
case nameof(Children):
InvalidateGeometry();
break;
} }
} }
internal void Invalidate()
{
InvalidateGeometry();
}
} }
} }

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

@ -202,15 +202,9 @@ namespace Avalonia.Media
{ {
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this, out var scale); var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this);
var geometry = new PlatformGeometry(geometryImpl); return new PlatformGeometry(geometryImpl);
var transform = new MatrixTransform(Matrix.CreateTranslation(geometry.Bounds.Left, -geometry.Bounds.Top) * scale);
geometry.Transform = transform;
return geometry;
} }
/// <summary> /// <summary>

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

@ -62,9 +62,8 @@ namespace Avalonia.Platform
/// Created a geometry implementation for the glyph run. /// Created a geometry implementation for the glyph run.
/// </summary> /// </summary>
/// <param name="glyphRun">The glyph run to build a geometry from.</param> /// <param name="glyphRun">The glyph run to build a geometry from.</param>
/// <param name="scale">The scaling of the produces geometry.</param>
/// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns> /// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale); IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun);
/// <summary> /// <summary>
/// Creates a renderer. /// Creates a renderer.

95
src/Avalonia.Base/Styling/Styles.cs

@ -17,7 +17,7 @@ namespace Avalonia.Styling
IStyle, IStyle,
IResourceProvider IResourceProvider
{ {
private readonly AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>(); private readonly AvaloniaList<IStyle> _styles = new();
private IResourceHost? _owner; private IResourceHost? _owner;
private IResourceDictionary? _resources; private IResourceDictionary? _resources;
private StyleCache? _cache; private StyleCache? _cache;
@ -62,16 +62,18 @@ namespace Avalonia.Styling
{ {
value = value ?? throw new ArgumentNullException(nameof(Resources)); value = value ?? throw new ArgumentNullException(nameof(Resources));
if (Owner is object) var currentOwner = Owner;
if (currentOwner is not null)
{ {
_resources?.RemoveOwner(Owner); _resources?.RemoveOwner(currentOwner);
} }
_resources = value; _resources = value;
if (Owner is object) if (currentOwner is not null)
{ {
_resources.AddOwner(Owner); _resources.AddOwner(currentOwner);
} }
} }
} }
@ -89,7 +91,7 @@ namespace Avalonia.Styling
foreach (var i in this) foreach (var i in this)
{ {
if (i is IResourceProvider p && p.HasResources) if (i is IResourceProvider { HasResources: true })
{ {
return true; return true;
} }
@ -190,7 +192,7 @@ namespace Avalonia.Styling
{ {
owner = owner ?? throw new ArgumentNullException(nameof(owner)); owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner != null) if (Owner is not null)
{ {
throw new InvalidOperationException("The Styles already has a owner."); throw new InvalidOperationException("The Styles already has a owner.");
} }
@ -227,70 +229,81 @@ namespace Avalonia.Styling
} }
} }
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) private static IReadOnlyList<T> ToReadOnlyList<T>(ICollection list)
{ {
static IReadOnlyList<T> ToReadOnlyList<T>(IList list) if (list is IReadOnlyList<T> readOnlyList)
{ {
if (list is IReadOnlyList<T>) return readOnlyList;
{
return (IReadOnlyList<T>)list;
}
else
{
var result = new T[list.Count];
list.CopyTo(result, 0);
return result;
}
} }
void Add(IList items) var result = new T[list.Count];
list.CopyTo(result, 0);
return result;
}
private static void InternalAdd(IList items, IResourceHost? owner, ref StyleCache? cache)
{
if (owner is not null)
{ {
for (var i = 0; i < items.Count; ++i) for (var i = 0; i < items.Count; ++i)
{ {
var style = (IStyle)items[i]!; if (items[i] is IResourceProvider provider)
if (Owner is object && style is IResourceProvider resourceProvider)
{ {
resourceProvider.AddOwner(Owner); provider.AddOwner(owner);
} }
_cache = null;
} }
(Owner as IStyleHost)?.StylesAdded(ToReadOnlyList<IStyle>(items)); (owner as IStyleHost)?.StylesAdded(ToReadOnlyList<IStyle>(items));
}
if (items.Count > 0)
{
cache = null;
} }
}
void Remove(IList items) private static void InternalRemove(IList items, IResourceHost? owner, ref StyleCache? cache)
{
if (owner is not null)
{ {
for (var i = 0; i < items.Count; ++i) for (var i = 0; i < items.Count; ++i)
{ {
var style = (IStyle)items[i]!; if (items[i] is IResourceProvider provider)
if (Owner is object && style is IResourceProvider resourceProvider)
{ {
resourceProvider.RemoveOwner(Owner); provider.RemoveOwner(owner);
} }
_cache = null;
} }
(Owner as IStyleHost)?.StylesRemoved(ToReadOnlyList<IStyle>(items)); (owner as IStyleHost)?.StylesRemoved(ToReadOnlyList<IStyle>(items));
}
if (items.Count > 0)
{
cache = null;
}
}
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
throw new InvalidOperationException("Reset should not be called on Styles.");
} }
var currentOwner = Owner;
switch (e.Action) switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
Add(e.NewItems!); InternalAdd(e.NewItems!, currentOwner, ref _cache);
break; break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
Remove(e.OldItems!); InternalRemove(e.OldItems!, currentOwner, ref _cache);
break; break;
case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Replace:
Remove(e.OldItems!); InternalRemove(e.OldItems!, currentOwner, ref _cache);
Add(e.NewItems!); InternalAdd(e.NewItems!, currentOwner, ref _cache);
break; break;
case NotifyCollectionChangedAction.Reset:
throw new InvalidOperationException("Reset should not be called on Styles.");
} }
CollectionChanged?.Invoke(this, e); CollectionChanged?.Invoke(this, e);

4
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -114,10 +114,8 @@ namespace Avalonia.Headless
return new HeadlessGlyphRunStub(); return new HeadlessGlyphRunStub();
} }
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
scale = Matrix.Identity;
return new HeadlessGeometryStub(new Rect(glyphRun.Size)); return new HeadlessGeometryStub(new Rect(glyphRun.Size));
} }

26
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -62,7 +62,7 @@ namespace Avalonia.Skia
return new CombinedGeometryImpl(combineMode, g1, g2); return new CombinedGeometryImpl(combineMode, g1, g2);
} }
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
{ {
@ -79,21 +79,29 @@ namespace Avalonia.Skia
}; };
SKPath path = new SKPath(); SKPath path = new SKPath();
var matrix = SKMatrix.Identity;
var currentX = 0f; var (currentX, currentY) = glyphRun.BaselineOrigin;
foreach (var glyph in glyphRun.GlyphIndices) for (var i = 0; i < glyphRun.GlyphIndices.Count; i++)
{ {
var p = skFont.GetGlyphPath(glyph); var glyph = glyphRun.GlyphIndices[i];
var glyphPath = skFont.GetGlyphPath(glyph);
path.AddPath(p, currentX, 0); if (!glyphPath.IsEmpty)
{
path.AddPath(glyphPath, (float)currentX, (float)currentY);
}
currentX += p.Bounds.Right; if (glyphRun.GlyphAdvances != null)
{
currentX += glyphRun.GlyphAdvances[i];
}
else
{
currentX += glyphPath.Bounds.Right;
}
} }
scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY);
return new StreamGeometryImpl(path); return new StreamGeometryImpl(path);
} }

22
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -13,6 +13,7 @@ using Avalonia.Media.Imaging;
using SharpDX.DirectWrite; using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun; using GlyphRun = Avalonia.Media.GlyphRun;
using TextAlignment = Avalonia.Media.TextAlignment; using TextAlignment = Avalonia.Media.TextAlignment;
using SharpDX.Mathematics.Interop;
namespace Avalonia namespace Avalonia
{ {
@ -159,7 +160,7 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
{ {
@ -182,10 +183,23 @@ namespace Avalonia.Direct2D1
sink.Close(); sink.Close();
} }
scale = Matrix.Identity; var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin;
return new StreamGeometryImpl(pathGeometry); var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry(
} Direct2D1Factory,
pathGeometry,
new RawMatrix3x2(1.0f, 0.0f, 0.0f, 1.0f, (float)baselineOriginX, (float)baselineOriginY));
return new TransformedGeometryWrapper(transformedGeometry);
}
private class TransformedGeometryWrapper : GeometryImpl
{
public TransformedGeometryWrapper(SharpDX.Direct2D1.TransformedGeometry geometry) : base(geometry)
{
}
}
/// <inheritdoc /> /// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName) public IBitmapImpl LoadBitmap(string fileName)

14
tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs

@ -14,13 +14,21 @@ namespace Avalonia.Visuals.UnitTests.Media
} }
[Fact] [Fact]
public void Children_Can_Be_Set_To_Null() public void Children_Change_Should_Raise_Changed()
{ {
var target = new GeometryGroup(); var target = new GeometryGroup();
target.Children = null; var children = new GeometryCollection();
Assert.Null(target.Children); target.Children = children;
var isCalled = false;
target.Changed += (s, e) => isCalled = true;
children.Add(new StreamGeometry());
Assert.True(isCalled);
} }
} }
} }

2
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@ -121,7 +121,7 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

2
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -117,7 +117,7 @@ namespace Avalonia.Benchmarks
return new NullGlyphRun(); return new NullGlyphRun();
} }
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

35
tests/Avalonia.RenderTests/Media/GlyphRunTests.cs

@ -23,26 +23,28 @@ namespace Avalonia.Direct2D1.RenderTests.Media
[Fact] [Fact]
public async Task Should_Render_GlyphRun_Geometry() public async Task Should_Render_GlyphRun_Geometry()
{ {
Decorator target = new Decorator var control = new GlyphRunGeometryControl
{ {
Padding = new Thickness(8), [TextElement.ForegroundProperty] = new LinearGradientBrush
Width = 200,
Height = 100,
Child = new GlyphRunGeometryControl
{ {
[TextElement.ForegroundProperty] = new LinearGradientBrush StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative),
{ EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative),
StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative), GradientStops =
EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative),
GradientStops =
{ {
new GradientStop { Color = Colors.Red, Offset = 0 }, new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 } new GradientStop { Color = Colors.Blue, Offset = 1 }
} }
}
} }
}; };
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 190,
Height = 120,
Child = control
};
await RenderToFile(target); await RenderToFile(target);
CompareImages(); CompareImages();
@ -50,8 +52,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media
public class GlyphRunGeometryControl : Control public class GlyphRunGeometryControl : Control
{ {
private readonly Geometry _geometry;
public GlyphRunGeometryControl() public GlyphRunGeometryControl()
{ {
var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface; var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface;
@ -62,19 +62,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media
var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices); var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices);
_geometry = glyphRun.BuildGeometry(); Geometry = glyphRun.BuildGeometry();
} }
protected override Size MeasureOverride(Size availableSize) public Geometry Geometry { get; }
{
return _geometry.Bounds.Size;
}
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
var foreground = TextElement.GetForeground(this); var foreground = TextElement.GetForeground(this);
context.DrawGeometry(foreground, null, _geometry); context.DrawGeometry(foreground, null, Geometry);
} }
} }
} }

4
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -122,10 +122,8 @@ namespace Avalonia.UnitTests
return Mock.Of<IGlyphRunImpl>(); return Mock.Of<IGlyphRunImpl>();
} }
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
scale = Matrix.Identity;
return Mock.Of<IGeometryImpl>(); return Mock.Of<IGeometryImpl>();
} }

BIN
tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Loading…
Cancel
Save