committed by
GitHub
24 changed files with 601 additions and 137 deletions
@ -0,0 +1,279 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Controls.Utils |
||||
|
{ |
||||
|
internal class BorderRenderHelper |
||||
|
{ |
||||
|
private bool _useComplexRendering; |
||||
|
private StreamGeometry _backgroundGeometryCache; |
||||
|
private StreamGeometry _borderGeometryCache; |
||||
|
|
||||
|
public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius) |
||||
|
{ |
||||
|
if (borderThickness.IsUniform && cornerRadius.IsUniform) |
||||
|
{ |
||||
|
_backgroundGeometryCache = null; |
||||
|
_borderGeometryCache = null; |
||||
|
_useComplexRendering = false; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_useComplexRendering = true; |
||||
|
|
||||
|
var boundRect = new Rect(finalSize); |
||||
|
var innerRect = boundRect.Deflate(borderThickness); |
||||
|
var innerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, false); |
||||
|
|
||||
|
StreamGeometry backgroundGeometry = null; |
||||
|
|
||||
|
if (innerRect.Width != 0 && innerRect.Height != 0) |
||||
|
{ |
||||
|
backgroundGeometry = new StreamGeometry(); |
||||
|
|
||||
|
using (var ctx = backgroundGeometry.Open()) |
||||
|
{ |
||||
|
CreateGeometry(ctx, innerRect, innerCoordinates); |
||||
|
} |
||||
|
|
||||
|
_backgroundGeometryCache = backgroundGeometry; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_backgroundGeometryCache = null; |
||||
|
} |
||||
|
|
||||
|
if (boundRect.Width != 0 && innerRect.Height != 0) |
||||
|
{ |
||||
|
var outerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, true); |
||||
|
var borderGeometry = new StreamGeometry(); |
||||
|
|
||||
|
using (var ctx = borderGeometry.Open()) |
||||
|
{ |
||||
|
CreateGeometry(ctx, boundRect, outerCoordinates); |
||||
|
|
||||
|
if (backgroundGeometry != null) |
||||
|
{ |
||||
|
CreateGeometry(ctx, innerRect, innerCoordinates); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_borderGeometryCache = borderGeometry; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_borderGeometryCache = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush) |
||||
|
{ |
||||
|
if (_useComplexRendering) |
||||
|
{ |
||||
|
var backgroundGeometry = _backgroundGeometryCache; |
||||
|
if (backgroundGeometry != null) |
||||
|
{ |
||||
|
context.DrawGeometry(background, null, backgroundGeometry); |
||||
|
} |
||||
|
|
||||
|
var borderGeometry = _borderGeometryCache; |
||||
|
if (borderGeometry != null) |
||||
|
{ |
||||
|
context.DrawGeometry(borderBrush, null, borderGeometry); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var borderThickness = borders.Left; |
||||
|
var cornerRadius = (float)radii.TopLeft; |
||||
|
var rect = new Rect(size); |
||||
|
|
||||
|
if (background != null) |
||||
|
{ |
||||
|
context.FillRectangle(background, rect.Deflate(borders), cornerRadius); |
||||
|
} |
||||
|
|
||||
|
if (borderBrush != null && borderThickness > 0) |
||||
|
{ |
||||
|
context.DrawRectangle(new Pen(borderBrush, borderThickness), rect.Deflate(borderThickness), cornerRadius); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderCoordinates borderCoordinates) |
||||
|
{ |
||||
|
var topLeft = new Point(borderCoordinates.LeftTop, 0); |
||||
|
var topRight = new Point(boundRect.Width - borderCoordinates.RightTop, 0); |
||||
|
var rightTop = new Point(boundRect.Width, borderCoordinates.TopRight); |
||||
|
var rightBottom = new Point(boundRect.Width, boundRect.Height - borderCoordinates.BottomRight); |
||||
|
var bottomRight = new Point(boundRect.Width - borderCoordinates.RightBottom, boundRect.Height); |
||||
|
var bottomLeft = new Point(borderCoordinates.LeftBottom, boundRect.Height); |
||||
|
var leftBottom = new Point(0, boundRect.Height - borderCoordinates.BottomLeft); |
||||
|
var leftTop = new Point(0, borderCoordinates.TopLeft); |
||||
|
|
||||
|
|
||||
|
if (topLeft.X > topRight.X) |
||||
|
{ |
||||
|
var scaledX = borderCoordinates.LeftTop / (borderCoordinates.LeftTop + borderCoordinates.RightTop) * boundRect.Width; |
||||
|
topLeft = new Point(scaledX, topLeft.Y); |
||||
|
topRight = new Point(scaledX, topRight.Y); |
||||
|
} |
||||
|
|
||||
|
if (rightTop.Y > rightBottom.Y) |
||||
|
{ |
||||
|
var scaledY = borderCoordinates.TopRight / (borderCoordinates.TopRight + borderCoordinates.BottomRight) * boundRect.Height; |
||||
|
rightTop = new Point(rightTop.X, scaledY); |
||||
|
rightBottom = new Point(rightBottom.X, scaledY); |
||||
|
} |
||||
|
|
||||
|
if (bottomRight.X < bottomLeft.X) |
||||
|
{ |
||||
|
var scaledX = borderCoordinates.LeftBottom / (borderCoordinates.LeftBottom + borderCoordinates.RightBottom) * boundRect.Width; |
||||
|
bottomRight = new Point(scaledX, bottomRight.Y); |
||||
|
bottomLeft = new Point(scaledX, bottomLeft.Y); |
||||
|
} |
||||
|
|
||||
|
if (leftBottom.Y < leftTop.Y) |
||||
|
{ |
||||
|
var scaledY = borderCoordinates.TopLeft / (borderCoordinates.TopLeft + borderCoordinates.BottomLeft) * boundRect.Height; |
||||
|
leftBottom = new Point(leftBottom.X, scaledY); |
||||
|
leftTop = new Point(leftTop.X, scaledY); |
||||
|
} |
||||
|
|
||||
|
var offset = new Vector(boundRect.TopLeft.X, boundRect.TopLeft.Y); |
||||
|
topLeft += offset; |
||||
|
topRight += offset; |
||||
|
rightTop += offset; |
||||
|
rightBottom += offset; |
||||
|
bottomRight += offset; |
||||
|
bottomLeft += offset; |
||||
|
leftBottom += offset; |
||||
|
leftTop += offset; |
||||
|
|
||||
|
context.BeginFigure(topLeft, true); |
||||
|
|
||||
|
//Top
|
||||
|
context.LineTo(topRight); |
||||
|
|
||||
|
//TopRight corner
|
||||
|
var radiusX = boundRect.TopRight.X - topRight.X; |
||||
|
var radiusY = rightTop.Y - boundRect.TopRight.Y; |
||||
|
if (radiusX != 0 || radiusY != 0) |
||||
|
{ |
||||
|
context.ArcTo(rightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise); |
||||
|
} |
||||
|
|
||||
|
//Right
|
||||
|
context.LineTo(rightBottom); |
||||
|
|
||||
|
//BottomRight corner
|
||||
|
radiusX = boundRect.BottomRight.X - bottomRight.X; |
||||
|
radiusY = boundRect.BottomRight.Y - rightBottom.Y; |
||||
|
if (radiusX != 0 || radiusY != 0) |
||||
|
{ |
||||
|
context.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); |
||||
|
} |
||||
|
|
||||
|
//Bottom
|
||||
|
context.LineTo(bottomLeft); |
||||
|
|
||||
|
//BottomLeft corner
|
||||
|
radiusX = bottomLeft.X - boundRect.BottomLeft.X; |
||||
|
radiusY = boundRect.BottomLeft.Y - leftBottom.Y; |
||||
|
if (radiusX != 0 || radiusY != 0) |
||||
|
{ |
||||
|
context.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); |
||||
|
} |
||||
|
|
||||
|
//Left
|
||||
|
context.LineTo(leftTop); |
||||
|
|
||||
|
//TopLeft corner
|
||||
|
radiusX = topLeft.X - boundRect.TopLeft.X; |
||||
|
radiusY = leftTop.Y - boundRect.TopLeft.Y; |
||||
|
|
||||
|
if (radiusX != 0 || radiusY != 0) |
||||
|
{ |
||||
|
context.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); |
||||
|
} |
||||
|
|
||||
|
context.EndFigure(true); |
||||
|
} |
||||
|
|
||||
|
private struct BorderCoordinates |
||||
|
{ |
||||
|
internal BorderCoordinates(CornerRadius cornerRadius, Thickness borderThickness, bool isOuter) |
||||
|
{ |
||||
|
var left = 0.5 * borderThickness.Left; |
||||
|
var top = 0.5 * borderThickness.Top; |
||||
|
var right = 0.5 * borderThickness.Right; |
||||
|
var bottom = 0.5 * borderThickness.Bottom; |
||||
|
|
||||
|
if (isOuter) |
||||
|
{ |
||||
|
if (cornerRadius.TopLeft == 0) |
||||
|
{ |
||||
|
LeftTop = TopLeft = 0.0; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
LeftTop = cornerRadius.TopLeft + left; |
||||
|
TopLeft = cornerRadius.TopLeft + top; |
||||
|
} |
||||
|
if (cornerRadius.TopRight == 0) |
||||
|
{ |
||||
|
TopRight = RightTop = 0; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
TopRight = cornerRadius.TopRight + top; |
||||
|
RightTop = cornerRadius.TopRight + right; |
||||
|
} |
||||
|
if (cornerRadius.BottomRight == 0) |
||||
|
{ |
||||
|
RightBottom = BottomRight = 0; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
RightBottom = cornerRadius.BottomRight + right; |
||||
|
BottomRight = cornerRadius.BottomRight + bottom; |
||||
|
} |
||||
|
if (cornerRadius.BottomLeft == 0) |
||||
|
{ |
||||
|
BottomLeft = LeftBottom = 0; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
BottomLeft = cornerRadius.BottomLeft + bottom; |
||||
|
LeftBottom = cornerRadius.BottomLeft + left; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
LeftTop = Math.Max(0, cornerRadius.TopLeft - left); |
||||
|
TopLeft = Math.Max(0, cornerRadius.TopLeft - top); |
||||
|
TopRight = Math.Max(0, cornerRadius.TopRight - top); |
||||
|
RightTop = Math.Max(0, cornerRadius.TopRight - right); |
||||
|
RightBottom = Math.Max(0, cornerRadius.BottomRight - right); |
||||
|
BottomRight = Math.Max(0, cornerRadius.BottomRight - bottom); |
||||
|
BottomLeft = Math.Max(0, cornerRadius.BottomLeft - bottom); |
||||
|
LeftBottom = Math.Max(0, cornerRadius.BottomLeft - left); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal readonly double LeftTop; |
||||
|
internal readonly double TopLeft; |
||||
|
internal readonly double TopRight; |
||||
|
internal readonly double RightTop; |
||||
|
internal readonly double RightBottom; |
||||
|
internal readonly double BottomRight; |
||||
|
internal readonly double BottomLeft; |
||||
|
internal readonly double LeftBottom; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,97 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
public struct CornerRadius |
||||
|
{ |
||||
|
public CornerRadius(double uniformRadius) |
||||
|
{ |
||||
|
TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius; |
||||
|
|
||||
|
} |
||||
|
public CornerRadius(double top, double bottom) |
||||
|
{ |
||||
|
TopLeft = TopRight = top; |
||||
|
BottomLeft = BottomRight = bottom; |
||||
|
} |
||||
|
public CornerRadius(double topLeft, double topRight, double bottomRight, double bottomLeft) |
||||
|
{ |
||||
|
TopLeft = topLeft; |
||||
|
TopRight = topRight; |
||||
|
BottomRight = bottomRight; |
||||
|
BottomLeft = bottomLeft; |
||||
|
} |
||||
|
|
||||
|
public double TopLeft { get; } |
||||
|
public double TopRight { get; } |
||||
|
public double BottomRight { get; } |
||||
|
public double BottomLeft { get; } |
||||
|
public bool IsEmpty => TopLeft.Equals(0) && IsUniform; |
||||
|
public bool IsUniform => TopLeft.Equals(TopRight) && BottomLeft.Equals(BottomRight) && TopRight.Equals(BottomRight); |
||||
|
|
||||
|
public override bool Equals(object obj) |
||||
|
{ |
||||
|
if (obj is CornerRadius) |
||||
|
{ |
||||
|
return this == (CornerRadius)obj; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public override int GetHashCode() |
||||
|
{ |
||||
|
return TopLeft.GetHashCode() ^ TopRight.GetHashCode() ^ BottomLeft.GetHashCode() ^ BottomRight.GetHashCode(); |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
return $"{TopLeft},{TopRight},{BottomRight},{BottomLeft}"; |
||||
|
} |
||||
|
|
||||
|
public static CornerRadius Parse(string s, CultureInfo culture) |
||||
|
{ |
||||
|
var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) |
||||
|
.Select(x => x.Trim()) |
||||
|
.ToList(); |
||||
|
|
||||
|
switch (parts.Count) |
||||
|
{ |
||||
|
case 1: |
||||
|
var uniform = double.Parse(parts[0], culture); |
||||
|
return new CornerRadius(uniform); |
||||
|
case 2: |
||||
|
var top = double.Parse(parts[0], culture); |
||||
|
var bottom = double.Parse(parts[1], culture); |
||||
|
return new CornerRadius(top, bottom); |
||||
|
case 4: |
||||
|
var topLeft = double.Parse(parts[0], culture); |
||||
|
var topRight = double.Parse(parts[1], culture); |
||||
|
var bottomRight = double.Parse(parts[2], culture); |
||||
|
var bottomLeft = double.Parse(parts[3], culture); |
||||
|
return new CornerRadius(topLeft, topRight, bottomRight, bottomLeft); |
||||
|
default: |
||||
|
{ |
||||
|
throw new FormatException("Invalid CornerRadius."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static bool operator ==(CornerRadius cr1, CornerRadius cr2) |
||||
|
{ |
||||
|
return cr1.TopLeft.Equals(cr2.TopLeft) |
||||
|
&& cr1.TopRight.Equals(cr2.TopRight) |
||||
|
&& cr1.BottomRight.Equals(cr2.BottomRight) |
||||
|
&& cr1.BottomLeft.Equals(cr2.BottomLeft); |
||||
|
} |
||||
|
|
||||
|
public static bool operator !=(CornerRadius cr1, CornerRadius cr2) |
||||
|
{ |
||||
|
return !(cr1 == cr2); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Globalization; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.Converters |
||||
|
{ |
||||
|
public class CornerRadiusTypeConverter : TypeConverter |
||||
|
{ |
||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
||||
|
{ |
||||
|
return sourceType == typeof(string); |
||||
|
} |
||||
|
|
||||
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
||||
|
{ |
||||
|
return CornerRadius.Parse((string)value, culture); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System.Globalization; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Visuals.UnitTests |
||||
|
{ |
||||
|
public class CornerRadiusTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Parse_Parses_Single_Uniform_Radius() |
||||
|
{ |
||||
|
var result = CornerRadius.Parse("3.4", CultureInfo.InvariantCulture); |
||||
|
|
||||
|
Assert.Equal(new CornerRadius(3.4), result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Parse_Parses_Top_Bottom() |
||||
|
{ |
||||
|
var result = CornerRadius.Parse("1.1,2.2", CultureInfo.InvariantCulture); |
||||
|
|
||||
|
Assert.Equal(new CornerRadius(1.1, 2.2), result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Parse_Parses_TopLeft_TopRight_BottomRight_BottomLeft() |
||||
|
{ |
||||
|
var result = CornerRadius.Parse("1.1,2.2,3.3,4.4", CultureInfo.InvariantCulture); |
||||
|
|
||||
|
Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Parse_Accepts_Spaces() |
||||
|
{ |
||||
|
var result = CornerRadius.Parse("1.1 2.2 3.3 4.4", CultureInfo.InvariantCulture); |
||||
|
|
||||
|
Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
Loading…
Reference in new issue