A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

213 lines
7.9 KiB

using System;
namespace Avalonia
{
public struct RoundedRect
{
public bool Equals(RoundedRect other)
{
return Rect.Equals(other.Rect) && RadiiTopLeft.Equals(other.RadiiTopLeft) && RadiiTopRight.Equals(other.RadiiTopRight) && RadiiBottomLeft.Equals(other.RadiiBottomLeft) && RadiiBottomRight.Equals(other.RadiiBottomRight);
}
public override bool Equals(object? obj)
{
return obj is RoundedRect other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = Rect.GetHashCode();
hashCode = (hashCode * 397) ^ RadiiTopLeft.GetHashCode();
hashCode = (hashCode * 397) ^ RadiiTopRight.GetHashCode();
hashCode = (hashCode * 397) ^ RadiiBottomLeft.GetHashCode();
hashCode = (hashCode * 397) ^ RadiiBottomRight.GetHashCode();
return hashCode;
}
}
public static bool operator ==(RoundedRect left, RoundedRect right) => left.Equals(right);
public static bool operator !=(RoundedRect left, RoundedRect right) => !left.Equals(right);
public Rect Rect { get; }
public Vector RadiiTopLeft { get; }
public Vector RadiiTopRight { get; }
public Vector RadiiBottomLeft { get; }
public Vector RadiiBottomRight { get; }
public RoundedRect(Rect rect, Vector radiiTopLeft, Vector radiiTopRight, Vector radiiBottomRight, Vector radiiBottomLeft)
{
Rect = rect;
RadiiTopLeft = radiiTopLeft;
RadiiTopRight = radiiTopRight;
RadiiBottomRight = radiiBottomRight;
RadiiBottomLeft = radiiBottomLeft;
}
public RoundedRect(Rect rect, double radiusTopLeft, double radiusTopRight, double radiusBottomRight,
double radiusBottomLeft)
: this(rect,
new Vector(radiusTopLeft, radiusTopLeft),
new Vector(radiusTopRight, radiusTopRight),
new Vector(radiusBottomRight, radiusBottomRight),
new Vector(radiusBottomLeft, radiusBottomLeft)
)
{
}
public RoundedRect(Rect rect, Vector radii) : this(rect, radii, radii, radii, radii)
{
}
public RoundedRect(Rect rect, double radiusX, double radiusY) : this(rect, new Vector(radiusX, radiusY))
{
}
public RoundedRect(Rect rect, double radius) : this(rect, radius, radius)
{
}
public RoundedRect(Rect rect) : this(rect, 0)
{
}
public RoundedRect(in Rect bounds, in CornerRadius radius) : this(bounds,
radius.TopLeft, radius.TopRight,
radius.BottomRight, radius.BottomLeft)
{
}
public static implicit operator RoundedRect(Rect r) => new RoundedRect(r);
public bool IsRounded => RadiiTopLeft != default || RadiiTopRight != default || RadiiBottomRight != default ||
RadiiBottomLeft != default;
public bool IsUniform =>
RadiiTopLeft.Equals(RadiiTopRight) &&
RadiiTopLeft.Equals(RadiiBottomRight) &&
RadiiTopLeft.Equals(RadiiBottomLeft);
public RoundedRect Inflate(double dx, double dy)
{
return Deflate(-dx, -dy);
}
public unsafe RoundedRect Deflate(double dx, double dy)
{
if (!IsRounded)
return new RoundedRect(Rect.Deflate(new Thickness(dx, dy)));
// Ported from SKRRect
var left = Rect.X + dx;
var top = Rect.Y + dy;
var right = left + Rect.Width - dx * 2;
var bottom = top + Rect.Height - dy * 2;
var radii = stackalloc Vector[4];
radii[0] = RadiiTopLeft;
radii[1] = RadiiTopRight;
radii[2] = RadiiBottomRight;
radii[3] = RadiiBottomLeft;
bool degenerate = false;
if (right <= left) {
degenerate = true;
left = right = (left + right)*0.5;
}
if (bottom <= top) {
degenerate = true;
top = bottom = (top + bottom) * 0.5;
}
if (degenerate)
{
return new RoundedRect(new Rect(left, top, right - left, bottom - top));
}
for (var c = 0; c < 4; c++)
{
var rx = Math.Max(0, radii[c].X - dx);
var ry = Math.Max(0, radii[c].Y - dy);
if (rx == 0 || ry == 0)
radii[c] = default;
else
radii[c] = new Vector(rx, ry);
}
return new RoundedRect(new Rect(left, top, right - left, bottom - top),
radii[0], radii[1], radii[2], radii[3]);
}
/// <summary>
/// This method should be used internally to check for the rect emptiness
/// Once we add support for WPF-like empty rects, there will be an actual implementation
/// For now it's internal to keep some loud community members happy about the API being pretty
/// </summary>
internal bool IsEmpty() => this == default;
private static bool IsOutsideCorner(double dx, double dy, double radius)
{
return (dx < 0) && (dy < 0) && (dx * dx + dy * dy > radius * radius);
}
/// <summary>
/// Determines whether a point is in the bounds of the rounded rectangle, exclusive of the
/// rounded rectangle's bottom/right edge.
/// </summary>
/// <param name="p">The point.</param>
/// <returns>true if the point is in the bounds of the rounded rectangle; otherwise false.</returns>
public bool ContainsExclusive(Point p)
{
// Do a simple rectangular bounds check first
if (!Rect.ContainsExclusive(p))
return false;
// If any radii totals exceed available bounds, determine a scale factor that needs to be applied
var scaleFactor = 1.0;
if (Rect.Width > 0)
{
var radiiWidth = Math.Max(RadiiTopLeft.X + RadiiTopRight.X, RadiiBottomLeft.X + RadiiBottomRight.X);
if (radiiWidth > Rect.Width)
scaleFactor = Math.Min(scaleFactor, Rect.Width / radiiWidth);
}
if (Rect.Height > 0)
{
var radiiHeight = Math.Max(RadiiTopLeft.Y + RadiiBottomLeft.Y, RadiiTopRight.Y + RadiiBottomRight.Y);
if (radiiHeight > Rect.Height)
scaleFactor = Math.Min(scaleFactor, Rect.Height / radiiHeight);
}
// Before corner hit-testing, make the point relative to the bounds' upper-left
p = new Point(p.X - Rect.X, p.Y - Rect.Y);
// Top-left corner
var radius = Math.Min(RadiiTopLeft.X, RadiiTopLeft.Y) * scaleFactor;
if (IsOutsideCorner(p.X - radius, p.Y - radius, radius))
return false;
// Top-right corner
radius = Math.Min(RadiiTopRight.X, RadiiTopRight.Y) * scaleFactor;
if (IsOutsideCorner(Rect.Width - radius - p.X, p.Y - radius, radius))
return false;
// Bottom-right corner
radius = Math.Min(RadiiBottomRight.X, RadiiBottomRight.Y) * scaleFactor;
if (IsOutsideCorner(Rect.Width - radius - p.X, Rect.Height - radius - p.Y, radius))
return false;
// Bottom-left corner
radius = Math.Min(RadiiBottomLeft.X, RadiiBottomLeft.Y) * scaleFactor;
if (IsOutsideCorner(p.X - radius, Rect.Height - radius - p.Y, radius))
return false;
return true;
}
}
}