Browse Source

Merge branch 'master' into feature/assetLoaderTryPattern

pull/10496/head
Benedikt Stebner 3 years ago
committed by GitHub
parent
commit
a826d586df
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.Desktop.slnf
  2. 9
      azure-pipelines-integrationtests.yml
  3. 2
      samples/ControlCatalog/Pages/PointerCanvas.cs
  4. 1
      samples/IntegrationTestApp/IntegrationTestApp.csproj
  5. 1
      src/Avalonia.Base/Media/DrawingContext.cs
  6. 2
      src/Avalonia.Base/Media/DrawingGroup.cs
  7. 2
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  8. 21
      src/Avalonia.Base/StyledElement.cs
  9. 13
      src/Avalonia.Base/Styling/OrSelector.cs
  10. 15
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  11. 10
      src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs
  12. 17
      src/Avalonia.Base/Visual.cs
  13. 9
      src/Avalonia.Controls/MaskedTextBox.cs
  14. 17
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  15. 2
      src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
  16. 16
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs
  17. 43
      src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs
  18. 9
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  19. 5
      src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs
  20. 43
      src/Skia/Avalonia.Skia/GeometryGroupImpl.cs
  21. 31
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  22. 5
      src/Skia/Avalonia.Skia/LineGeometryImpl.cs
  23. 4
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  24. 5
      src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs
  25. 90
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  26. 21
      src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
  27. 43
      tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs
  28. 58
      tests/Avalonia.RenderTests/Shapes/PathTests.cs
  29. BIN
      tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png
  30. BIN
      tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png

1
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",

9
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

2
samples/ControlCatalog/Pages/PointerCanvas.cs

@ -114,7 +114,7 @@ public class PointerCanvas : Control
private string? _status;
public static readonly DirectProperty<PointerCanvas, string?> StatusProperty =
AvaloniaProperty.RegisterDirect<PointerCanvas, string?>(nameof(DrawOnlyPoints), c => c.Status, (c, v) => c.Status = v,
AvaloniaProperty.RegisterDirect<PointerCanvas, string?>(nameof(Status), c => c.Status, (c, v) => c.Status = v,
defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
public string? Status

1
samples/IntegrationTestApp/IntegrationTestApp.csproj

@ -19,6 +19,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />

1
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

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

@ -107,8 +107,6 @@ namespace Avalonia.Media
private readonly DrawingGroup _drawingGroup;
private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
private Matrix _transform;
private bool _disposed;
// Root drawing created by this DrawingContext.

2
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)

21
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()

13
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;

15
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<List<string>> _classes = new Lazy<List<string>>(() => new List<string>());
private List<string>? _classes;
private Type? _targetType;
private string? _selectorString;
@ -81,7 +80,7 @@ namespace Avalonia.Styling
/// <summary>
/// The style classes which the selector matches.
/// </summary>
public IList<string> Classes => _classes.Value;
public IList<string> Classes => _classes ??= new();
/// <inheritdoc/>
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(":"))
{

10
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];

17
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();
}
}
}
/// <summary>
/// Computes the <see cref="HasMirrorTransform"/> value according to the
/// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/>

9
src/Avalonia.Controls/MaskedTextBox.cs

@ -31,9 +31,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<string?> MaskProperty =
AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty);
public static new readonly StyledProperty<char> PasswordCharProperty =
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PasswordChar), '\0');
public static readonly StyledProperty<char> PromptCharProperty =
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_');
@ -51,6 +48,12 @@ namespace Avalonia.Controls
private bool _resetOnSpace = true;
static MaskedTextBox()
{
PasswordCharProperty
.OverrideDefaultValue<MaskedTextBox>('\0');
}
public MaskedTextBox() { }
/// <summary>

17
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();
}
}
}
}
}

2
src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs

@ -15,7 +15,7 @@ namespace Avalonia.Diagnostics.Controls
(o, v) => o.Header = v);
public static readonly DirectProperty<ThicknessEditor, bool> IsPresentProperty =
AvaloniaProperty.RegisterDirect<ThicknessEditor, bool>(nameof(Header), o => o.IsPresent,
AvaloniaProperty.RegisterDirect<ThicknessEditor, bool>(nameof(IsPresent), o => o.IsPresent,
(o, v) => o.IsPresent = v);
public static readonly DirectProperty<ThicknessEditor, double> LeftProperty =

16
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<ICompiledBindingPathElement> _elements = new List<ICompiledBindingPathElement>();
private readonly ICompiledBindingPathElement[] _elements;
public CompiledBindingPath() { }
public CompiledBindingPath()
=> _elements = Array.Empty<ICompiledBindingPathElement>();
internal CompiledBindingPath(IEnumerable<ICompiledBindingPathElement> bindingPath, object rawSource)
internal CompiledBindingPath(ICompiledBindingPathElement[] elements, object rawSource)
{
_elements = new List<ICompiledBindingPathElement>(bindingPath);
_elements = elements;
RawSource = rawSource;
}
@ -78,13 +78,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
internal IEnumerable<ICompiledBindingPathElement> Elements => _elements;
internal SourceMode SourceMode => _elements.OfType<IControlSourceBindingPathElement>().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<ICompiledBindingPathElement>) _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

43
src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs

@ -8,11 +8,25 @@ namespace Avalonia.Skia
/// </summary>
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; }
}
}

9
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);
}
/// <inheritdoc />

5
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;
}
}

43
src/Skia/Avalonia.Skia/GeometryGroupImpl.cs

@ -11,26 +11,51 @@ namespace Avalonia.Skia
{
public GeometryGroupImpl(FillRule fillRule, IReadOnlyList<Geometry> 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; }
}
}

31
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!);
/// <inheritdoc />
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; }
/// <inheritdoc />
public bool FillContains(Point point)
{
return PathContainsCore(EffectivePath, point);
return PathContainsCore(FillPath, point);
}
/// <inheritdoc />
@ -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
/// <inheritdoc />
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);
}
/// <inheritdoc />
@ -128,7 +125,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
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
/// <inheritdoc />
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;

5
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)));

4
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);
}
/// <inheritdoc />

5
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;
}
}

90
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;
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
/// <param name="path">An existing Skia <see cref="SKPath"/>.</param>
/// <param name="stroke">An existing Skia <see cref="SKPath"/> for the stroke.</param>
/// <param name="fill">An existing Skia <see cref="SKPath"/> for the fill, can also be null or the same as the stroke</param>
/// <param name="bounds">Precomputed path bounds.</param>
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();
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
/// <param name="path">An existing Skia <see cref="SKPath"/>.</param>
public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect())
private StreamGeometryImpl(SKPath path) : this(path, path, default(Rect))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
public StreamGeometryImpl() : this(CreateEmptyPath(), default)
public StreamGeometryImpl() : this(CreateEmptyPath())
{
}
/// <inheritdoc />
public override SKPath EffectivePath => _effectivePath;
public override SKPath? StrokePath => _strokePath;
/// <inheritdoc />
public override SKPath? FillPath => _fillPath;
/// <inheritdoc />
public override Rect Bounds => _bounds;
@ -47,7 +51,9 @@ namespace Avalonia.Skia
/// <inheritdoc />
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);
}
/// <inheritdoc />
@ -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);
/// <summary>
/// Initializes a new instance of the <see cref="StreamContext"/> class.
@ -83,52 +92,79 @@ namespace Avalonia.Skia
public StreamContext(StreamGeometryImpl geometryImpl)
{
_geometryImpl = geometryImpl;
_path = _geometryImpl._effectivePath;
}
/// <inheritdoc />
/// <remarks>Will update bounds of passed geometry.</remarks>
public void Dispose()
{
_geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect();
_geometryImpl._bounds = Stroke.TightBounds.ToAvaloniaRect();
_geometryImpl.InvalidateCaches();
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
@ -136,14 +172,16 @@ namespace Avalonia.Skia
{
if (isClosed)
{
_path.Close();
Stroke.Close();
if (Duplicate)
Fill.Close();
}
}
/// <inheritdoc />
public void SetFillRule(FillRule fillRule)
{
_path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding;
Fill.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding;
}
}
}

21
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);
}
}
/// <inheritdoc />
public override SKPath? EffectivePath { get; }
public override SKPath? StrokePath { get; }
/// <inheritdoc />
public override SKPath? FillPath { get; }
/// <inheritdoc />
public IGeometryImpl SourceGeometry { get; }

43
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<ThemedControl>().Class("foo"))
{
Setters = { new Setter(StyledElement.ThemeProperty, theme) }
});
root.LayoutManager.ExecuteLayoutPass();
var border = Assert.IsType<Border>(target.VisualChild);
var inner = Assert.IsType<Border>(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<ThemedControl>((o, n) => new Border());
var template = new FuncControlTemplate<ThemedControl>(
(o, n) => new Border() { Child = new Border() });
return new ControlTheme
{

58
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();
}
}
}

BIN
tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

BIN
tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

Loading…
Cancel
Save