diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 4a7a329fc6..477aaec6a8 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -21,6 +21,7 @@ "src\\Avalonia.Desktop\\Avalonia.Desktop.csproj", "src\\Avalonia.Diagnostics\\Avalonia.Diagnostics.csproj", "src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj", + "src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj", "src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj", "src\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", "src\\Avalonia.Headless\\Avalonia.Headless.csproj", diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 5735da19ab..7221fe4657 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -1,11 +1,3 @@ -# Starter pipeline -# Start with a minimal pipeline that you can customize to build and deploy your code. -# Add steps that build, run tests, deploy, and more: -# https://aka.ms/yaml - -trigger: -- master - jobs: - job: Mac pool: @@ -25,6 +17,7 @@ jobs: - script: system_profiler SPDisplaysDataType |grep Resolution - script: | + sudo xcode-select -s /Applications/Xcode.app/Contents/Developer pkill node appium & pkill IntegrationTestApp diff --git a/samples/ControlCatalog/Pages/PointerCanvas.cs b/samples/ControlCatalog/Pages/PointerCanvas.cs index 653590fe64..32e46af9dd 100644 --- a/samples/ControlCatalog/Pages/PointerCanvas.cs +++ b/samples/ControlCatalog/Pages/PointerCanvas.cs @@ -114,7 +114,7 @@ public class PointerCanvas : Control private string? _status; public static readonly DirectProperty StatusProperty = - AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.Status, (c, v) => c.Status = v, + AvaloniaProperty.RegisterDirect(nameof(Status), c => c.Status, (c, v) => c.Status = v, defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); public string? Status diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj index 0a761d70ba..1356eeb526 100644 --- a/samples/IntegrationTestApp/IntegrationTestApp.csproj +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 31a16dc69c..f2106f2f86 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -277,7 +277,6 @@ namespace Avalonia.Media private readonly record struct RestoreState : IDisposable { private readonly DrawingContext _context; - private readonly Matrix _matrix; private readonly PushedStateType _type; public enum PushedStateType diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 812d315912..a41054202e 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -107,8 +107,6 @@ namespace Avalonia.Media private readonly DrawingGroup _drawingGroup; private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - private Matrix _transform; - private bool _disposed; // Root drawing created by this DrawingContext. diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 01299e4ffa..df3a70b3e6 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -225,7 +225,7 @@ public class CompositingRenderer : IRendererWithCompositor sortedChildren.Dispose(); } else - foreach (var ch in v.GetVisualChildren()) + foreach (var ch in visualChildren) { var compositionChild = ch.CompositionVisual; if (compositionChild != null) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 82e948eea8..cbdf3c3c1e 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -803,8 +803,11 @@ namespace Avalonia if (theme.HasChildren) { - foreach (var child in theme.Children) - ApplyStyle(child, null, type); + var children = theme.Children; + for (var i = 0; i < children.Count; i++) + { + ApplyStyle(children[i], null, type); + } } } @@ -816,8 +819,11 @@ namespace Avalonia if (host.IsStylesInitialized) { - foreach (var style in host.Styles) - ApplyStyle(style, host, FrameType.Style); + var styles = host.Styles; + for (var i = 0; i < styles.Count; ++i) + { + ApplyStyle(styles[i], host, FrameType.Style); + } } } @@ -826,8 +832,11 @@ namespace Avalonia if (style is Style s) s.TryAttach(this, host, type); - foreach (var child in style.Children) - ApplyStyle(child, host, type); + var children = style.Children; + for (var i = 0; i < children.Count; i++) + { + ApplyStyle(children[i], host, type); + } } private void ReevaluateImplicitTheme() diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index c5ef9a0b2b..3b0aa03492 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -71,9 +71,9 @@ namespace Avalonia.Styling var activators = new OrActivatorBuilder(); var neverThisInstance = false; - foreach (var selector in _selectors) + for (var i = 0; i < _selectors.Count; i++) { - var match = selector.Match(control, parent, subscribe); + var match = _selectors[i].Match(control, parent, subscribe); switch (match.Result) { @@ -108,16 +108,19 @@ namespace Avalonia.Styling internal override void ValidateNestingSelector(bool inControlTheme) { - foreach (var selector in _selectors) - selector.ValidateNestingSelector(inControlTheme); + for (var i = 0; i < _selectors.Count; i++) + { + _selectors[i].ValidateNestingSelector(inControlTheme); + } } private Type? EvaluateTargetType() { Type? result = null; - foreach (var selector in _selectors) + for (var i = 0; i < _selectors.Count; i++) { + var selector = _selectors[i]; if (selector.TargetType == null) { return null; diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 7883fd23ab..f8670cfdb3 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Controls; using Avalonia.Styling.Activators; using Avalonia.Utilities; @@ -15,7 +14,7 @@ namespace Avalonia.Styling internal class TypeNameAndClassSelector : Selector { private readonly Selector? _previous; - private readonly Lazy> _classes = new Lazy>(() => new List()); + private List? _classes; private Type? _targetType; private string? _selectorString; @@ -81,7 +80,7 @@ namespace Avalonia.Styling /// /// The style classes which the selector matches. /// - public IList Classes => _classes.Value; + public IList Classes => _classes ??= new(); /// public override string ToString(Style? owner) @@ -122,16 +121,16 @@ namespace Avalonia.Styling return SelectorMatch.NeverThisInstance; } - if (_classes.IsValueCreated && _classes.Value.Count > 0) + if (_classes is { Count: > 0 }) { if (subscribe) { - var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value); + var observable = new StyleClassActivator(control.Classes, _classes); return new SelectorMatch(observable); } - if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes)) + if (!StyleClassActivator.AreClassesMatching(control.Classes, _classes)) { return SelectorMatch.NeverThisInstance; } @@ -172,9 +171,9 @@ namespace Avalonia.Styling builder.Append(Name); } - if (_classes.IsValueCreated && _classes.Value.Count > 0) + if (_classes is { Count: > 0 }) { - foreach (var c in Classes) + foreach (var c in _classes) { if (!c.StartsWith(":")) { diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs index 5cac2ef658..0589abb2dd 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs @@ -304,13 +304,9 @@ namespace Avalonia.Utilities { if (_entryCount == _entries!.Length) { - const double growthFactor = 1.2; - var newSize = (int)(_entryCount * growthFactor); - - if (newSize == _entryCount) - { - newSize++; - } + var newSize = _entryCount == DefaultInitialCapacity ? + DefaultInitialCapacity * 2 : + (int)(_entryCount * 1.5); var destEntries = new Entry[newSize]; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 8b0cc06136..df0c5b100f 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -732,6 +732,23 @@ namespace Avalonia } } + internal override void OnTemplatedParentControlThemeChanged() + { + base.OnTemplatedParentControlThemeChanged(); + + var count = VisualChildren.Count; + var templatedParent = TemplatedParent; + + for (var i = 0; i < count; ++i) + { + if (VisualChildren[i] is StyledElement child && + child.TemplatedParent == templatedParent) + { + child.OnTemplatedParentControlThemeChanged(); + } + } + } + /// /// Computes the value according to the /// and diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index 5a3eb47ce4..d397a581dc 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -31,9 +31,6 @@ namespace Avalonia.Controls public static readonly StyledProperty MaskProperty = AvaloniaProperty.Register(nameof(Mask), string.Empty); - public static new readonly StyledProperty PasswordCharProperty = - AvaloniaProperty.Register(nameof(PasswordChar), '\0'); - public static readonly StyledProperty PromptCharProperty = AvaloniaProperty.Register(nameof(PromptChar), '_'); @@ -51,6 +48,12 @@ namespace Avalonia.Controls private bool _resetOnSpace = true; + static MaskedTextBox() + { + PasswordCharProperty + .OverrideDefaultValue('\0'); + } + public MaskedTextBox() { } /// diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index d8874832bd..8253342782 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -405,22 +405,5 @@ namespace Avalonia.Controls.Primitives } } } - - internal override void OnTemplatedParentControlThemeChanged() - { - base.OnTemplatedParentControlThemeChanged(); - - var count = VisualChildren.Count; - var templatedParent = TemplatedParent; - - for (var i = 0; i < count; ++i) - { - if (VisualChildren[i] is TemplatedControl child && - child.TemplatedParent == templatedParent) - { - child.OnTemplatedParentControlThemeChanged(); - } - } - } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs index ec7e91c8be..c9189a886d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs @@ -15,7 +15,7 @@ namespace Avalonia.Diagnostics.Controls (o, v) => o.Header = v); public static readonly DirectProperty IsPresentProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o.IsPresent, + AvaloniaProperty.RegisterDirect(nameof(IsPresent), o => o.IsPresent, (o, v) => o.IsPresent = v); public static readonly DirectProperty LeftProperty = diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index 2b62d33349..f196231a6b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Data.Core; @@ -13,13 +12,14 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { public class CompiledBindingPath { - private readonly List _elements = new List(); + private readonly ICompiledBindingPathElement[] _elements; - public CompiledBindingPath() { } + public CompiledBindingPath() + => _elements = Array.Empty(); - internal CompiledBindingPath(IEnumerable bindingPath, object rawSource) + internal CompiledBindingPath(ICompiledBindingPathElement[] elements, object rawSource) { - _elements = new List(bindingPath); + _elements = elements; RawSource = rawSource; } @@ -78,13 +78,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings internal IEnumerable Elements => _elements; - internal SourceMode SourceMode => _elements.OfType().Any() + internal SourceMode SourceMode => Array.Exists(_elements, e => e is IControlSourceBindingPathElement) ? SourceMode.Control : SourceMode.Data; internal object RawSource { get; } public override string ToString() - => string.Concat(_elements); + => string.Concat((IEnumerable) _elements); } public class CompiledBindingPathBuilder @@ -169,7 +169,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return this; } - public CompiledBindingPath Build() => new CompiledBindingPath(_elements, _rawSource); + public CompiledBindingPath Build() => new CompiledBindingPath(_elements.ToArray(), _rawSource); } public interface ICompiledBindingPathElement diff --git a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs index 170cc9d420..1a4f467f40 100644 --- a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs @@ -8,11 +8,25 @@ namespace Avalonia.Skia /// internal class CombinedGeometryImpl : GeometryImpl { - public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public CombinedGeometryImpl(SKPath? stroke, SKPath? fill) { - var path1 = (g1.PlatformImpl as GeometryImpl)?.EffectivePath; - var path2 = (g2.PlatformImpl as GeometryImpl)?.EffectivePath; + StrokePath = stroke; + FillPath = fill; + Bounds = (stroke ?? fill)?.TightBounds.ToAvaloniaRect() ?? default; + } + + public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + if (g1.PlatformImpl is GeometryImpl i1 + && g2.PlatformImpl is GeometryImpl i2 + && TryCreate(combineMode, i1, i2) is { } result) + return result; + + return new(null, null); + } + public static CombinedGeometryImpl? TryCreate(GeometryCombineMode combineMode, GeometryImpl g1, GeometryImpl g2) + { var op = combineMode switch { GeometryCombineMode.Intersect => SKPathOp.Intersect, @@ -21,13 +35,28 @@ namespace Avalonia.Skia _ => SKPathOp.Union }; - var path = path1?.Op(path2, op); + var stroke = + g1.StrokePath != null && g2.StrokePath != null + ? g1.StrokePath.Op(g2.StrokePath, op) + : null; + + SKPath? fill = null; + if (g1.FillPath != null && g2.FillPath != null) + { + // Reuse stroke if fill paths are the same + if (ReferenceEquals(g1.FillPath, g1.StrokePath) && ReferenceEquals(g2.FillPath, g2.StrokePath)) + fill = stroke; + else + fill = g1.FillPath.Op(g2.FillPath, op); + } - EffectivePath = path; - Bounds = path?.Bounds.ToAvaloniaRect() ?? default; + if (stroke == null && fill == null) + return null; + return new CombinedGeometryImpl(stroke, fill); } public override Rect Bounds { get; } - public override SKPath? EffectivePath { get; } + public override SKPath? StrokePath { get; } + public override SKPath? FillPath { get; } } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index db7b068543..af0231579c 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -230,20 +230,21 @@ namespace Avalonia.Skia var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; - if (brush is not null) + if (brush is not null && impl.FillPath != null) { using (var fill = CreatePaint(_fillPaint, brush, size)) { - Canvas.DrawPath(impl.EffectivePath, fill.Paint); + Canvas.DrawPath(impl.FillPath, fill.Paint); } } if (pen is not null + && impl.StrokePath != null && TryCreatePaint(_strokePaint, pen, size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke) { using (stroke) { - Canvas.DrawPath(impl.EffectivePath, stroke.Paint); + Canvas.DrawPath(impl.StrokePath, stroke.Paint); } } } @@ -639,7 +640,7 @@ namespace Avalonia.Skia { CheckLease(); Canvas.Save(); - Canvas.ClipPath(((GeometryImpl)clip).EffectivePath, SKClipOperation.Intersect, true); + Canvas.ClipPath(((GeometryImpl)clip).FillPath, SKClipOperation.Intersect, true); } /// diff --git a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs index ac05691c67..64976d4eea 100644 --- a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs @@ -8,14 +8,15 @@ namespace Avalonia.Skia internal class EllipseGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath FillPath => StrokePath; public EllipseGeometryImpl(Rect rect) { var path = new SKPath(); path.AddOval(rect.ToSKRect()); - EffectivePath = path; + StrokePath = path; Bounds = rect; } } diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs index 2828f9a9c1..01be42bad0 100644 --- a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -11,26 +11,51 @@ namespace Avalonia.Skia { public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) { - var path = new SKPath + var fillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd; + var count = children.Count; + + var stroke = new SKPath { - FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd, + FillType = fillType }; - - var count = children.Count; + bool requiresFillPass = false; for (var i = 0; i < count; ++i) { - if (children[i].PlatformImpl is GeometryImpl { EffectivePath: { } effectivePath }) + if (children[i].PlatformImpl is GeometryImpl geo) { - path.AddPath(effectivePath); + if (geo.StrokePath != null) + stroke.AddPath(geo.StrokePath); + if (!ReferenceEquals(geo.StrokePath, geo.FillPath)) + requiresFillPass = true; } } + + StrokePath = stroke; + + if (requiresFillPass) + { + var fill = new SKPath + { + FillType = fillType + }; + + for (var i = 0; i < count; ++i) + { + if (children[i].PlatformImpl is GeometryImpl { FillPath: { } fillPath }) + fill.AddPath(fillPath); + } + + FillPath = fill; + } + else + FillPath = stroke; - EffectivePath = path; - Bounds = path.Bounds.ToAvaloniaRect(); + Bounds = stroke.TightBounds.ToAvaloniaRect(); } public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath FillPath { get; } } } diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 34270c2078..aee84d1346 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -14,7 +14,7 @@ namespace Avalonia.Skia private PathCache _pathCache; private SKPathMeasure? _cachedPathMeasure; - private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(EffectivePath!); + private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(StrokePath!); /// public abstract Rect Bounds { get; } @@ -24,19 +24,20 @@ namespace Avalonia.Skia { get { - if (EffectivePath is null) + if (StrokePath is null) return 0; return CachedPathMeasure.Length; } } - public abstract SKPath? EffectivePath { get; } + public abstract SKPath? StrokePath { get; } + public abstract SKPath? FillPath { get; } /// public bool FillContains(Point point) { - return PathContainsCore(EffectivePath, point); + return PathContainsCore(FillPath, point); } /// @@ -74,7 +75,7 @@ namespace Avalonia.Skia var paint = SKPaintCache.Shared.Get(); paint.IsStroke = true; paint.StrokeWidth = strokeWidth; - paint.GetFillPath(EffectivePath, strokePath); + paint.GetFillPath(StrokePath, strokePath); SKPaintCache.Shared.ReturnReset(paint); @@ -96,14 +97,10 @@ namespace Avalonia.Skia /// public IGeometryImpl? Intersect(IGeometryImpl geometry) { - if (EffectivePath is { } path - && (geometry as GeometryImpl)?.EffectivePath is { } otherPath - && path.Op(otherPath, SKPathOp.Intersect) is { } result) - { - return new StreamGeometryImpl(result); - } - - return null; + var other = geometry as GeometryImpl; + if (other == null) + return null; + return CombinedGeometryImpl.TryCreate(GeometryCombineMode.Intersect, this, other); } /// @@ -128,7 +125,7 @@ namespace Avalonia.Skia /// public bool TryGetPointAtDistance(double distance, out Point point) { - if (EffectivePath is null) + if (StrokePath is null) { point = new Point(); return false; @@ -142,7 +139,7 @@ namespace Avalonia.Skia /// public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) { - if (EffectivePath is null) + if (StrokePath is null) { point = new Point(); tangent = new Point(); @@ -158,7 +155,7 @@ namespace Avalonia.Skia public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, [NotNullWhen(true)] out IGeometryImpl? segmentGeometry) { - if (EffectivePath is null) + if (StrokePath is null) { segmentGeometry = null; return false; @@ -172,7 +169,7 @@ namespace Avalonia.Skia if (res) { - segmentGeometry = new StreamGeometryImpl(_skPathSegment); + segmentGeometry = new StreamGeometryImpl(_skPathSegment, null); } return res; diff --git a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs index b102a4c119..2ac0a2e18c 100644 --- a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs @@ -9,7 +9,8 @@ namespace Avalonia.Skia internal class LineGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath? FillPath => null; public LineGeometryImpl(Point p1, Point p2) { @@ -17,7 +18,7 @@ namespace Avalonia.Skia path.MoveTo(p1.ToSKPoint()); path.LineTo(p2.ToSKPoint()); - EffectivePath = path; + StrokePath = path; Bounds = new Rect( new Point(Math.Min(p1.X, p2.X), Math.Min(p1.Y, p2.Y)), new Point(Math.Max(p1.X, p2.X), Math.Max(p1.Y, p2.Y))); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index ab1c6b8816..9c4b326f14 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -65,7 +65,7 @@ namespace Avalonia.Skia public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) { - return new CombinedGeometryImpl(combineMode, g1, g2); + return CombinedGeometryImpl.ForceCreate(combineMode, g1, g2); } public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) @@ -104,7 +104,7 @@ namespace Avalonia.Skia SKFontCache.Shared.Return(skFont); - return new StreamGeometryImpl(path); + return new StreamGeometryImpl(path, path); } /// diff --git a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs index 93d453e8f0..2d127e07c9 100644 --- a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs @@ -8,14 +8,15 @@ namespace Avalonia.Skia internal class RectangleGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath? FillPath => StrokePath; public RectangleGeometryImpl(Rect rect) { var path = new SKPath(); path.AddRect(rect.ToSKRect()); - EffectivePath = path; + StrokePath = path; Bounds = rect; } } diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 0c3289767e..eb081c4f50 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -10,36 +11,39 @@ namespace Avalonia.Skia internal class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { private Rect _bounds; - private readonly SKPath _effectivePath; + private readonly SKPath _strokePath; + private SKPath? _fillPath; /// /// Initializes a new instance of the class. /// - /// An existing Skia . + /// An existing Skia for the stroke. + /// An existing Skia for the fill, can also be null or the same as the stroke /// Precomputed path bounds. - public StreamGeometryImpl(SKPath path, Rect bounds) + public StreamGeometryImpl(SKPath stroke, SKPath? fill, Rect? bounds = null) { - _effectivePath = path; - _bounds = bounds; + _strokePath = stroke; + _fillPath = fill; + _bounds = bounds ?? stroke.TightBounds.ToAvaloniaRect(); } - /// - /// Initializes a new instance of the class. - /// - /// An existing Skia . - public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect()) + private StreamGeometryImpl(SKPath path) : this(path, path, default(Rect)) { + } /// /// Initializes a new instance of the class. /// - public StreamGeometryImpl() : this(CreateEmptyPath(), default) + public StreamGeometryImpl() : this(CreateEmptyPath()) { } - + /// - public override SKPath EffectivePath => _effectivePath; + public override SKPath? StrokePath => _strokePath; + + /// + public override SKPath? FillPath => _fillPath; /// public override Rect Bounds => _bounds; @@ -47,7 +51,9 @@ namespace Avalonia.Skia /// public IStreamGeometryImpl Clone() { - return new StreamGeometryImpl(_effectivePath.Clone(), Bounds); + var stroke = _strokePath.Clone(); + var fill = _fillPath == _strokePath ? stroke : _fillPath.Clone(); + return new StreamGeometryImpl(stroke, fill, Bounds); } /// @@ -74,7 +80,10 @@ namespace Avalonia.Skia private class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; - private readonly SKPath _path; + private SKPath Stroke => _geometryImpl._strokePath; + private SKPath Fill => _geometryImpl._fillPath ??= new(); + private bool _isFilled; + private bool Duplicate => _isFilled && !ReferenceEquals(_geometryImpl._fillPath, Stroke); /// /// Initializes a new instance of the class. @@ -83,52 +92,79 @@ namespace Avalonia.Skia public StreamContext(StreamGeometryImpl geometryImpl) { _geometryImpl = geometryImpl; - _path = _geometryImpl._effectivePath; } /// /// Will update bounds of passed geometry. public void Dispose() { - _geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect(); + _geometryImpl._bounds = Stroke.TightBounds.ToAvaloniaRect(); _geometryImpl.InvalidateCaches(); } /// public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { - _path.ArcTo( + var arc = isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small; + var sweep = sweepDirection == SweepDirection.Clockwise + ? SKPathDirection.Clockwise + : SKPathDirection.CounterClockwise; + Stroke.ArcTo( (float)size.Width, (float)size.Height, (float)rotationAngle, - isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small, - sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise, + arc, + sweep, (float)point.X, (float)point.Y); + if(Duplicate) + Fill.ArcTo( + (float)size.Width, + (float)size.Height, + (float)rotationAngle, + arc, + sweep, + (float)point.X, + (float)point.Y); } /// public void BeginFigure(Point startPoint, bool isFilled) { - _path.MoveTo((float)startPoint.X, (float)startPoint.Y); + if (!isFilled) + { + if (Stroke == Fill) + _geometryImpl._fillPath = Stroke.Clone(); + } + + _isFilled = isFilled; + Stroke.MoveTo((float)startPoint.X, (float)startPoint.Y); + if(Duplicate) + Fill.MoveTo((float)startPoint.X, (float)startPoint.Y); } /// public void CubicBezierTo(Point point1, Point point2, Point point3) { - _path.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); + Stroke.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); + if(Duplicate) + Fill.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); } /// public void QuadraticBezierTo(Point point1, Point point2) { - _path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); + Stroke.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); + if(Duplicate) + Fill.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); } /// public void LineTo(Point point) { - _path.LineTo((float)point.X, (float)point.Y); + Stroke.LineTo((float)point.X, (float)point.Y); + if(Duplicate) + Fill.LineTo((float)point.X, (float)point.Y); } /// @@ -136,14 +172,16 @@ namespace Avalonia.Skia { if (isClosed) { - _path.Close(); + Stroke.Close(); + if (Duplicate) + Fill.Close(); } } /// public void SetFillRule(FillRule fillRule) { - _path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; + Fill.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; } } } diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index fb3c2e403f..2dee8c318c 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -17,16 +17,27 @@ namespace Avalonia.Skia { SourceGeometry = source; Transform = transform; + var matrix = transform.ToSKMatrix(); - var transformedPath = source.EffectivePath.Clone(); - transformedPath?.Transform(transform.ToSKMatrix()); - - EffectivePath = transformedPath; + var transformedPath = StrokePath = source.StrokePath.Clone(); + transformedPath?.Transform(matrix); + Bounds = transformedPath?.TightBounds.ToAvaloniaRect() ?? default; + + if (ReferenceEquals(source.StrokePath, source.FillPath)) + FillPath = transformedPath; + else if (source.FillPath != null) + { + FillPath = transformedPath = source.FillPath.Clone(); + transformedPath.Transform(matrix); + } } /// - public override SKPath? EffectivePath { get; } + public override SKPath? StrokePath { get; } + + /// + public override SKPath? FillPath { get; } /// public IGeometryImpl SourceGeometry { get; } diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 60603937d9..6ba648af78 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -569,6 +569,46 @@ public class StyledElementTests_Theming Assert.Equal(Brushes.Green, border.Background); } + [Fact] + public void TemplatedParent_Theme_Change_Applies_Recursively_To_VisualChildren() + { + var theme = CreateDerivedTheme(); + var target = CreateTarget(); + + Assert.Null(target.Theme); + Assert.Null(target.Template); + + var root = CreateRoot(target, theme.BasedOn); + + Assert.NotNull(target.Theme); + Assert.NotNull(target.Template); + + root.Styles.Add(new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(StyledElement.ThemeProperty, theme) } + }); + + root.LayoutManager.ExecuteLayoutPass(); + + var border = Assert.IsType(target.VisualChild); + var inner = Assert.IsType(border.Child); + + Assert.Equal(Brushes.Red, border.Background); + Assert.Equal(Brushes.Red, inner.Background); + + Assert.Equal(null, inner.BorderBrush); + Assert.Equal(null, inner.BorderBrush); + + target.Classes.Add("foo"); + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(Brushes.Green, border.Background); + Assert.Equal(Brushes.Green, inner.Background); + + Assert.Equal(Brushes.Cyan, inner.BorderBrush); + Assert.Equal(Brushes.Cyan, inner.BorderBrush); + } + private static ThemedControl CreateTarget() { return new ThemedControl(); @@ -595,7 +635,8 @@ public class StyledElementTests_Theming private static ControlTheme CreateTheme(string tag = "theme") { - var template = new FuncControlTemplate((o, n) => new Border()); + var template = new FuncControlTemplate( + (o, n) => new Border() { Child = new Border() }); return new ControlTheme { diff --git a/tests/Avalonia.RenderTests/Shapes/PathTests.cs b/tests/Avalonia.RenderTests/Shapes/PathTests.cs index bac16cca88..bf375121de 100644 --- a/tests/Avalonia.RenderTests/Shapes/PathTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PathTests.cs @@ -376,5 +376,63 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes await RenderToFile(target); CompareImages(); } + + [Fact] + public async Task BeginFigure_IsFilled_Is_Respected() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new Path + { + Fill = Brushes.Black, + Stroke = Brushes.Black, + StrokeThickness = 10, + Data = new PathGeometry() + { + Figures = new() + { + new PathFigure + { + IsFilled = false, IsClosed = false, + StartPoint = new Point(170,170), + Segments = new () + { + new LineSegment + { + Point = new Point(60, 170) + }, + new LineSegment + { + Point = new Point(60, 60) + } + } + }, + new PathFigure + { + IsFilled = true, IsClosed = true, + StartPoint = new Point(60,20), + Segments = new () + { + new LineSegment + { + Point = new Point(20, 60) + }, + new LineSegment + { + Point = new Point(100, 60) + } + } + } + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } } } diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png new file mode 100644 index 0000000000..d67d71c520 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png b/tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png new file mode 100644 index 0000000000..d67d71c520 Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png differ