diff --git a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs index 25e29c67a9..97a9320c95 100644 --- a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs +++ b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs @@ -55,6 +55,10 @@ namespace RenderDemo.Pages formattedText.SetFontStyle(FontStyle.Italic, 28, 28); context.DrawText(formattedText, new Point(10, 0)); + + var geometry = formattedText.BuildGeometry(new Point(10 + formattedText.Width + 10, 0)); + + context.DrawGeometry(gradient, null, geometry); } } } diff --git a/src/Avalonia.Base/GeometryCollection.cs b/src/Avalonia.Base/GeometryCollection.cs deleted file mode 100644 index 0bd02d5438..0000000000 --- a/src/Avalonia.Base/GeometryCollection.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using Avalonia.Animation; - -#nullable enable - -namespace Avalonia.Media -{ - public class GeometryCollection : Animatable, IList, IReadOnlyList - { - private List _inner; - - public GeometryCollection() => _inner = new List(); - public GeometryCollection(IEnumerable collection) => _inner = new List(collection); - public GeometryCollection(int capacity) => _inner = new List(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 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(); - } -} diff --git a/src/Avalonia.Base/Media/DrawingCollection.cs b/src/Avalonia.Base/Media/DrawingCollection.cs new file mode 100644 index 0000000000..a76f7743cc --- /dev/null +++ b/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 + { + public DrawingCollection() + { + ResetBehavior = ResetBehavior.Remove; + } + + public DrawingCollection(IEnumerable items) : base(items) + { + ResetBehavior = ResetBehavior.Remove; + } + } +} diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index eeb6318ebd..603bb1c1c1 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/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.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -18,6 +22,14 @@ namespace Avalonia.Media public static readonly StyledProperty OpacityMaskProperty = AvaloniaProperty.Register(nameof(OpacityMask)); + public static readonly DirectProperty ChildrenProperty = + AvaloniaProperty.RegisterDirect( + nameof(Children), + o => o.Children, + (o, v) => o.Children = v); + + private DrawingCollection _children = new DrawingCollection(); + public double Opacity { get => GetValue(OpacityProperty); @@ -42,8 +54,23 @@ namespace Avalonia.Media set => SetValue(OpacityMaskProperty, value); } + /// + /// Gets or sets the collection that contains the child geometries. + /// [Content] - public AvaloniaList Children { get; } = new AvaloniaList(); + 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) { @@ -75,5 +102,394 @@ namespace Avalonia.Media return rect; } + + private class DrawingGroupDrawingContext : IDrawingContextImpl + { + private readonly DrawingGroup _drawingGroup; + private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); + + 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? _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 source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + throw new NotImplementedException(); + } + + public void DrawBitmap(IRef 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; + } + } + + /// + /// Pop + /// + 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(); + } + + /// + /// PushTransform - + /// Push a Transform which will apply to all drawing operations until the corresponding + /// Pop. + /// + /// The Transform to push. + 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; + } + + /// + /// 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. + /// + 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(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; + } + + /// + /// Contains the functionality common to GeometryDrawing operations of + /// instantiating the GeometryDrawing, setting it's Freezable state, + /// and Adding it to the Drawing Graph. + /// + 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); + } + + /// + /// 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. + /// + 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(_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); + } + } + } } } diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 7bdf59def0..5480336f84 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -1223,7 +1223,7 @@ namespace Avalonia.Media public double OverhangTrailing { get - { + { return BlackBoxMetrics.OverhangTrailing; } } @@ -1252,6 +1252,46 @@ namespace Avalonia.Media } } + /// + /// Obtains geometry for the text, including underlines and strikethroughs. + /// + /// The left top origin of the resulting geometry. + /// 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. + 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; + } + /// /// Draws the text object /// @@ -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) { var metrics = new CachedMetrics(); diff --git a/src/Avalonia.Base/Media/GeometryCollection.cs b/src/Avalonia.Base/Media/GeometryCollection.cs new file mode 100644 index 0000000000..2afa191dcf --- /dev/null +++ b/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 + { + public GeometryCollection() + { + ResetBehavior = ResetBehavior.Remove; + + this.ForEachItem( + x => + { + Parent?.Invalidate(); + }, + x => + { + Parent?.Invalidate(); + }, + () => throw new NotSupportedException()); + } + + public GeometryCollection(IEnumerable items) : base(items) + { + ResetBehavior = ResetBehavior.Remove; + + this.ForEachItem( + x => + { + Parent?.Invalidate(); + }, + x => + { + Parent?.Invalidate(); + }, + () => throw new NotSupportedException()); + } + + public GeometryGroup? Parent { get; set; } + } +} diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index 08e62df2cc..7df7d25954 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -21,14 +21,14 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty BrushProperty = - AvaloniaProperty.Register(nameof(Brush), Brushes.Transparent); + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register(nameof(Brush), Brushes.Transparent); /// /// Defines the property. /// - public static readonly StyledProperty PenProperty = - AvaloniaProperty.Register(nameof(Pen)); + public static readonly StyledProperty PenProperty = + AvaloniaProperty.Register(nameof(Pen)); /// /// Gets or sets the that describes the shape of this . @@ -43,7 +43,7 @@ namespace Avalonia.Media /// /// Gets or sets the used to fill the interior of the shape described by this . /// - public IBrush Brush + public IBrush? Brush { get => GetValue(BrushProperty); set => SetValue(BrushProperty, value); @@ -52,7 +52,7 @@ namespace Avalonia.Media /// /// Gets or sets the used to stroke this . /// - public IPen Pen + public IPen? Pen { get => GetValue(PenProperty); set => SetValue(PenProperty, value); diff --git a/src/Avalonia.Base/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs similarity index 64% rename from src/Avalonia.Base/GeometryGroup.cs rename to src/Avalonia.Base/Media/GeometryGroup.cs index b90c9c6d8a..0326e606f4 100644 --- a/src/Avalonia.Base/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Metadata; +using Avalonia.Metadata; using Avalonia.Platform; #nullable enable @@ -13,29 +10,36 @@ namespace Avalonia.Media /// public class GeometryGroup : Geometry { - public static readonly DirectProperty ChildrenProperty = - AvaloniaProperty.RegisterDirect ( + public static readonly DirectProperty ChildrenProperty = + AvaloniaProperty.RegisterDirect ( nameof(Children), o => o.Children, - (o, v) => o.Children = v); + (o, v)=> o.Children = v); public static readonly StyledProperty FillRuleProperty = AvaloniaProperty.Register(nameof(FillRule)); - private GeometryCollection? _children; - private bool _childrenSet; + private GeometryCollection _children; + + public GeometryGroup() + { + _children = new GeometryCollection + { + Parent = this + }; + } /// /// Gets or sets the collection that contains the child geometries. /// [Content] - public GeometryCollection? Children + public GeometryCollection Children { - get => _children ??= (!_childrenSet ? new GeometryCollection() : null); + get => _children; set { - SetAndRaise(ChildrenProperty, ref _children, value); - _childrenSet = true; + OnChildrenChanged(_children, value); + SetAndRaise(ChildrenProperty, ref _children, value); } } @@ -52,16 +56,28 @@ namespace Avalonia.Media public override Geometry Clone() { var result = new GeometryGroup { FillRule = FillRule, Transform = Transform }; - if (_children?.Count > 0) + + if (_children.Count > 0) + { result.Children = new GeometryCollection(_children); + } + return result; } + protected void OnChildrenChanged(GeometryCollection oldChildren, GeometryCollection newChildren) + { + oldChildren.Parent = null; + + newChildren.Parent = this; + } + protected override IGeometryImpl? CreateDefiningGeometry() { - if (_children?.Count > 0) + if (_children.Count > 0) { var factory = AvaloniaLocator.Current.GetRequiredService(); + return factory.CreateGeometryGroup(FillRule, _children); } @@ -72,10 +88,18 @@ namespace Avalonia.Media { 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(); + } } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 25c35a28e5..6f1fa03990 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -202,15 +202,9 @@ namespace Avalonia.Media { var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this, out var scale); + var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this); - var geometry = new PlatformGeometry(geometryImpl); - - var transform = new MatrixTransform(Matrix.CreateTranslation(geometry.Bounds.Left, -geometry.Bounds.Top) * scale); - - geometry.Transform = transform; - - return geometry; + return new PlatformGeometry(geometryImpl); } /// diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index bfa9e70fce..e39a4e23df 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -62,9 +62,8 @@ namespace Avalonia.Platform /// Created a geometry implementation for the glyph run. /// /// The glyph run to build a geometry from. - /// The scaling of the produces geometry. /// The geometry returned contains the combined geometry of all glyphs in the glyph run. - IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale); + IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun); /// /// Creates a renderer. diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 7c0bc4ad7f..e4c3371007 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -17,7 +17,7 @@ namespace Avalonia.Styling IStyle, IResourceProvider { - private readonly AvaloniaList _styles = new AvaloniaList(); + private readonly AvaloniaList _styles = new(); private IResourceHost? _owner; private IResourceDictionary? _resources; private StyleCache? _cache; @@ -62,16 +62,18 @@ namespace Avalonia.Styling { 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; - 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) { - if (i is IResourceProvider p && p.HasResources) + if (i is IResourceProvider { HasResources: true }) { return true; } @@ -190,7 +192,7 @@ namespace Avalonia.Styling { owner = owner ?? throw new ArgumentNullException(nameof(owner)); - if (Owner != null) + if (Owner is not null) { 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 ToReadOnlyList(ICollection list) { - static IReadOnlyList ToReadOnlyList(IList list) + if (list is IReadOnlyList readOnlyList) { - if (list is IReadOnlyList) - { - return (IReadOnlyList)list; - } - else - { - var result = new T[list.Count]; - list.CopyTo(result, 0); - return result; - } + return readOnlyList; } - 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) { - var style = (IStyle)items[i]!; - - if (Owner is object && style is IResourceProvider resourceProvider) + if (items[i] is IResourceProvider provider) { - resourceProvider.AddOwner(Owner); + provider.AddOwner(owner); } - - _cache = null; } - (Owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); + (owner as IStyleHost)?.StylesAdded(ToReadOnlyList(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) { - var style = (IStyle)items[i]!; - - if (Owner is object && style is IResourceProvider resourceProvider) + if (items[i] is IResourceProvider provider) { - resourceProvider.RemoveOwner(Owner); + provider.RemoveOwner(owner); } - - _cache = null; } - (Owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); + (owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(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) { case NotifyCollectionChangedAction.Add: - Add(e.NewItems!); + InternalAdd(e.NewItems!, currentOwner, ref _cache); break; case NotifyCollectionChangedAction.Remove: - Remove(e.OldItems!); + InternalRemove(e.OldItems!, currentOwner, ref _cache); break; case NotifyCollectionChangedAction.Replace: - Remove(e.OldItems!); - Add(e.NewItems!); + InternalRemove(e.OldItems!, currentOwner, ref _cache); + InternalAdd(e.NewItems!, currentOwner, ref _cache); break; - case NotifyCollectionChangedAction.Reset: - throw new InvalidOperationException("Reset should not be called on Styles."); } CollectionChanged?.Invoke(this, e); diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 6471b87bfd..059a9a4e8f 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -114,10 +114,8 @@ namespace Avalonia.Headless 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)); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 727677c82e..91fe4fc085 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -62,7 +62,7 @@ namespace Avalonia.Skia 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) { @@ -79,21 +79,29 @@ namespace Avalonia.Skia }; 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); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 04025f92e4..7f1af46e97 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -13,6 +13,7 @@ using Avalonia.Media.Imaging; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; using TextAlignment = Avalonia.Media.TextAlignment; +using SharpDX.Mathematics.Interop; namespace Avalonia { @@ -159,7 +160,7 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); 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) { @@ -182,10 +183,23 @@ namespace Avalonia.Direct2D1 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) + { + + } + } /// public IBitmapImpl LoadBitmap(string fileName) diff --git a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs index 8f80238903..fb4c35a1a8 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs @@ -14,13 +14,21 @@ namespace Avalonia.Visuals.UnitTests.Media } [Fact] - public void Children_Can_Be_Set_To_Null() + public void Children_Change_Should_Raise_Changed() { 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); } } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 183177495a..1f0b82b465 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -121,7 +121,7 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 51e75b6611..0193f5d772 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -117,7 +117,7 @@ namespace Avalonia.Benchmarks return new NullGlyphRun(); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs index 6a8884a33a..1b0193bfdb 100644 --- a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -23,26 +23,28 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Fact] public async Task Should_Render_GlyphRun_Geometry() { - Decorator target = new Decorator + var control = new GlyphRunGeometryControl { - Padding = new Thickness(8), - Width = 200, - Height = 100, - Child = new GlyphRunGeometryControl + [TextElement.ForegroundProperty] = new LinearGradientBrush { - [TextElement.ForegroundProperty] = new LinearGradientBrush - { - StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative), - EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative), - GradientStops = + StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative), + EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative), + GradientStops = { new GradientStop { Color = Colors.Red, Offset = 0 }, new GradientStop { Color = Colors.Blue, Offset = 1 } } - } } }; + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 190, + Height = 120, + Child = control + }; + await RenderToFile(target); CompareImages(); @@ -50,8 +52,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media public class GlyphRunGeometryControl : Control { - private readonly Geometry _geometry; - public GlyphRunGeometryControl() { var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface; @@ -62,19 +62,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices); - _geometry = glyphRun.BuildGeometry(); + Geometry = glyphRun.BuildGeometry(); } - protected override Size MeasureOverride(Size availableSize) - { - return _geometry.Bounds.Size; - } + public Geometry Geometry { get; } public override void Render(DrawingContext context) { var foreground = TextElement.GetForeground(this); - context.DrawGeometry(foreground, null, _geometry); + context.DrawGeometry(foreground, null, Geometry); } } } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index c385e1c3eb..bf4ac9c1f6 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -122,10 +122,8 @@ namespace Avalonia.UnitTests return Mock.Of(); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { - scale = Matrix.Identity; - return Mock.Of(); } diff --git a/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index 7f1e0d29a1..004265112f 100644 Binary files a/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png and b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png differ diff --git a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index a8f3aa9277..407b67b8a0 100644 Binary files a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png and b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png differ