using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform;
namespace LanMountainDesktop.Controls;
///
/// A Decorator that renders a border with continuous "Squircle" corners (super-ellipse).
/// Ported and adapted from SeiWoLauncherPro for Avalonia 11.
///
public class SmoothBorder : Decorator
{
public static readonly StyledProperty BackgroundProperty =
Border.BackgroundProperty.AddOwner();
public static readonly StyledProperty BorderBrushProperty =
Border.BorderBrushProperty.AddOwner();
public static readonly StyledProperty BorderThicknessProperty =
Border.BorderThicknessProperty.AddOwner();
public static readonly StyledProperty CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner();
public static readonly StyledProperty SmoothnessProperty =
AvaloniaProperty.Register(nameof(Smoothness), 0.6);
public IBrush? Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
public IBrush? BorderBrush
{
get => GetValue(BorderBrushProperty);
set => SetValue(BorderBrushProperty, value);
}
public Thickness BorderThickness
{
get => GetValue(BorderThicknessProperty);
set => SetValue(BorderThicknessProperty, value);
}
public CornerRadius CornerRadius
{
get => GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public double Smoothness
{
get => GetValue(SmoothnessProperty);
set => SetValue(SmoothnessProperty, value);
}
static SmoothBorder()
{
AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty, SmoothnessProperty);
AffectsMeasure(BorderThicknessProperty);
}
protected override Size MeasureOverride(Size constraint)
{
var padding = BorderThickness;
if (Child != null)
{
Child.Measure(constraint.Deflate(padding));
return Child.DesiredSize.Inflate(padding);
}
return new Size(padding.Left + padding.Right, padding.Top + padding.Bottom);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (Child != null)
{
var padding = BorderThickness;
Child.Arrange(new Rect(finalSize).Deflate(padding));
Child.Clip = CreateSquircle(new Rect(0, 0, finalSize.Width - padding.Left - padding.Right, finalSize.Height - padding.Top - padding.Bottom), CornerRadius, Smoothness);
}
return finalSize;
}
public override void Render(DrawingContext context)
{
var rect = new Rect(Bounds.Size);
if (rect.Width <= 0 || rect.Height <= 0) return;
var geometry = CreateSquircle(rect, CornerRadius, Smoothness);
if (Background != null)
{
context.DrawGeometry(Background, null, geometry);
}
if (BorderBrush != null && BorderThickness != default)
{
// Simple implementation for uniform thickness
var pen = new Pen(BorderBrush, BorderThickness.Left);
context.DrawGeometry(null, pen, geometry);
}
// Apply clipping to children if needed
// Note: In Avalonia 11, we usually set Clip property on the child or use a Clip content property.
}
private static Geometry CreateSquircle(Rect rect, CornerRadius radius, double smoothness)
{
smoothness = Math.Clamp(smoothness, 0, 1);
var geometry = new StreamGeometry();
using (var ctx = geometry.Open())
{
// Top-left starting point
double pTL = radius.TopLeft * (1 + smoothness);
ctx.BeginFigure(new Point(rect.Left + pTL, rect.Top), true);
// Top-right corner
DrawCorner(ctx, rect, radius.TopRight, smoothness, Corner.TopRight);
// Bottom-right corner
DrawCorner(ctx, rect, radius.BottomRight, smoothness, Corner.BottomRight);
// Bottom-left corner
DrawCorner(ctx, rect, radius.BottomLeft, smoothness, Corner.BottomLeft);
// Top-left corner (closing)
DrawCorner(ctx, rect, radius.TopLeft, smoothness, Corner.TopLeft);
ctx.EndFigure(true);
}
return geometry;
}
private enum Corner { TopRight, BottomRight, BottomLeft, TopLeft }
private static void DrawCorner(StreamGeometryContext ctx, Rect rect, double radius, double smoothness, Corner corner)
{
if (radius <= 0)
{
Point pt = corner switch {
Corner.TopRight => rect.TopRight,
Corner.BottomRight => rect.BottomRight,
Corner.BottomLeft => rect.BottomLeft,
Corner.TopLeft => rect.TopLeft,
_ => default
};
ctx.LineTo(pt);
return;
}
double p = radius * (1 + smoothness);
double theta = 45 * smoothness;
double radTheta = theta * (Math.PI / 180.0);
double radBeta = (90 * (1 - smoothness)) * (Math.PI / 180.0);
double c = radius * Math.Tan(radTheta / 2) * Math.Cos(radTheta);
double d = radius * Math.Tan(radTheta / 2) * Math.Sin(radTheta);
double arcSeg = Math.Sin(radBeta / 2) * radius * Math.Sqrt(2);
double b = (p - arcSeg - c - d) / 3;
double a = 2 * b;
// Points relative to corner
Point[] points = corner switch
{
Corner.TopRight => new[] {
new Point(rect.Right - (p - a - b - c), rect.Top + d),
new Point(rect.Right - (p - a), rect.Top),
new Point(rect.Right - (p - a - b), rect.Top),
new Point(rect.Right, rect.Top + p),
new Point(rect.Right, rect.Top + p - a - b),
new Point(rect.Right, rect.Top + p - a)
},
Corner.BottomRight => new[] {
new Point(rect.Right - d, rect.Bottom - (p - a - b - c)),
new Point(rect.Right, rect.Bottom - (p - a)),
new Point(rect.Right, rect.Bottom - (p - a - b)),
new Point(rect.Right - p, rect.Bottom),
new Point(rect.Right - (p - a - b), rect.Bottom),
new Point(rect.Right - (p - a), rect.Bottom)
},
Corner.BottomLeft => new[] {
new Point(rect.Left + (p - a - b - c), rect.Bottom - d),
new Point(rect.Left + (p - a), rect.Bottom),
new Point(rect.Left + (p - a - b), rect.Bottom),
new Point(rect.Left, rect.Bottom - p),
new Point(rect.Left, rect.Bottom - (p - a - b)),
new Point(rect.Left, rect.Bottom - (p - a))
},
Corner.TopLeft => new[] {
new Point(rect.Left + d, rect.Top + (p - a - b - c)),
new Point(rect.Left, rect.Top + (p - a)),
new Point(rect.Left, rect.Top + (p - a - b)),
new Point(rect.Left + p, rect.Top),
new Point(rect.Left + (p - a - b), rect.Top),
new Point(rect.Left + (p - a), rect.Top)
},
_ => throw new ArgumentOutOfRangeException()
};
// 1. Line to start of segment
ctx.LineTo(corner switch {
Corner.TopRight => new Point(rect.Right - p, rect.Top),
Corner.BottomRight => new Point(rect.Right, rect.Bottom - p),
Corner.BottomLeft => new Point(rect.Left + p, rect.Bottom),
Corner.TopLeft => new Point(rect.Left, rect.Top + p),
_ => default
});
// 2. First Bezier
ctx.CubicBezierTo(points[1], points[2], points[0]);
// 3. Arc
double startAngle = corner switch {
Corner.TopRight => 270, Corner.BottomRight => 0, Corner.BottomLeft => 90, Corner.TopLeft => 180, _ => 0
};
double arcEndAngle = startAngle + 90 - theta;
double endRad = arcEndAngle * (Math.PI / 180.0);
Point center = corner switch {
Corner.TopRight => new Point(rect.Right - radius, rect.Top + radius),
Corner.BottomRight => new Point(rect.Right - radius, rect.Bottom - radius),
Corner.BottomLeft => new Point(rect.Left + radius, rect.Bottom - radius),
Corner.TopLeft => new Point(rect.Left + radius, rect.Top + radius),
_ => default
};
Point arcEnd = new Point(center.X + radius * Math.Cos(endRad), center.Y + radius * Math.Sin(endRad));
ctx.ArcTo(arcEnd, new Size(radius, radius), 0, false, SweepDirection.Clockwise);
// 4. Second Bezier
ctx.CubicBezierTo(points[4], points[5], points[3]);
}
}