Skip to content

Commit

Permalink
Replace ClipBorder with clip converter for button styles and game ico…
Browse files Browse the repository at this point in the history
…n to fix rendering bugs

MahApps/MahApps.Metro#4524
  • Loading branch information
RayCarrot committed Oct 31, 2024
1 parent 41e572f commit 5b86f01
Show file tree
Hide file tree
Showing 3 changed files with 319 additions and 23 deletions.
30 changes: 23 additions & 7 deletions src/RayCarrot.RCP.Metro/UI/Controls/GameIcon/GameIcon.xaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:RayCarrot.RCP.Metro"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls">
xmlns:local="clr-namespace:RayCarrot.RCP.Metro">

<local:ClipGeometryConverter x:Key="ClipGeometryConverter" />

<Style TargetType="{x:Type local:GameIcon}">
<Setter Property="Focusable" Value="False" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:GameIcon}">
<mah:ClipBorder CornerRadius="{TemplateBinding CornerRadius}"
Width="{TemplateBinding IconWidth}"
Height="{TemplateBinding IconHeight}">
<Grid>
<Border x:Name="RootBorder"
CornerRadius="{TemplateBinding CornerRadius}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid x:Name="RootGrid"
Width="{TemplateBinding IconWidth}"
Height="{TemplateBinding IconHeight}">
<Grid.Clip>
<MultiBinding Converter="{StaticResource ClipGeometryConverter}">
<Binding ElementName="RootGrid" Path="ActualWidth" Mode="OneWay" />
<Binding ElementName="RootGrid" Path="ActualHeight" Mode="OneWay" />
<Binding ElementName="RootBorder" Path="CornerRadius" Mode="OneWay" />
<Binding ElementName="RootBorder" Path="BorderThickness" Mode="OneWay" />
<Binding ElementName="RootBorder" Path="Padding" Mode="OneWay" />
</MultiBinding>
</Grid.Clip>

<!-- Game icon -->
<Image Source="{TemplateBinding Source}"
Width="{TemplateBinding IconWidth}"
Expand All @@ -24,7 +38,9 @@
Width="{TemplateBinding IconWidth}"
Height="{TemplateBinding IconHeight}" />
</Grid>
</mah:ClipBorder>

</Border>

</ControlTemplate>
</Setter.Value>
</Setter>
Expand Down
258 changes: 258 additions & 0 deletions src/RayCarrot.RCP.Metro/UI/Converters/Other/ClipGeometryConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using MahApps.Metro.Controls;

namespace RayCarrot.RCP.Metro;

// For use instead of ClipBorder, see https://github.com/MahApps/MahApps.Metro/issues/4524
public class ClipGeometryConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length != 5)
return DependencyProperty.UnsetValue;

if (values[0] is not double width
|| values[1] is not double height
|| values[2] is not CornerRadius cornerRadius
|| values[3] is not Thickness borderThickness
|| values[4] is not Thickness padding)
{
return DependencyProperty.UnsetValue;
}

if (width <= 0 || height <= 0)
return DependencyProperty.UnsetValue;

StreamGeometry clipGeometry = new();
BorderInfo childBorderInfo = new(cornerRadius, borderThickness, padding, false);

using (StreamGeometryContext ctx = clipGeometry.Open())
GenerateGeometry(ctx, new Rect(0, 0, width, height), childBorderInfo);

// Freeze the geometry for better performance
clipGeometry.Freeze();

return clipGeometry;
}

/// <inheritdoc />
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

internal static void GenerateGeometry(StreamGeometryContext ctx, Rect rect, BorderInfo radii)
{
//
// compute the coordinates of the key points
//

Point topLeft = new Point(radii.LeftTop, 0);
Point topRight = new Point(rect.Width - radii.RightTop, 0);
Point rightTop = new Point(rect.Width, radii.TopRight);
Point rightBottom = new Point(rect.Width, rect.Height - radii.BottomRight);
Point bottomRight = new Point(rect.Width - radii.RightBottom, rect.Height);
Point bottomLeft = new Point(radii.LeftBottom, rect.Height);
Point leftBottom = new Point(0, rect.Height - radii.BottomLeft);
Point leftTop = new Point(0, radii.TopLeft);

//
// check keypoints for overlap and resolve by partitioning radii according to
// the percentage of each one.
//

// top edge is handled here
if (topLeft.X > topRight.X)
{
double v = (radii.LeftTop) / (radii.LeftTop + radii.RightTop) * rect.Width;
topLeft.X = v;
topRight.X = v;
}

// right edge
if (rightTop.Y > rightBottom.Y)
{
double v = (radii.TopRight) / (radii.TopRight + radii.BottomRight) * rect.Height;
rightTop.Y = v;
rightBottom.Y = v;
}

// bottom edge
if (bottomRight.X < bottomLeft.X)
{
double v = (radii.LeftBottom) / (radii.LeftBottom + radii.RightBottom) * rect.Width;
bottomRight.X = v;
bottomLeft.X = v;
}

// left edge
if (leftBottom.Y < leftTop.Y)
{
double v = (radii.TopLeft) / (radii.TopLeft + radii.BottomLeft) * rect.Height;
leftBottom.Y = v;
leftTop.Y = v;
}

//
// add on offsets
//

Vector offset = new Vector(rect.TopLeft.X, rect.TopLeft.Y);
topLeft += offset;
topRight += offset;
rightTop += offset;
rightBottom += offset;
bottomRight += offset;
bottomLeft += offset;
leftBottom += offset;
leftTop += offset;

//
// create the border geometry
//
ctx.BeginFigure(topLeft, true /* is filled */, true /* is closed */);

// Top line
ctx.LineTo(topRight, true /* is stroked */, false /* is smooth join */);

// Upper-right corner
double radiusX = rect.TopRight.X - topRight.X;
double radiusY = rightTop.Y - rect.TopRight.Y;
if (!Utils.IsZero(radiusX)
|| !Utils.IsZero(radiusY))
{
ctx.ArcTo(rightTop, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
}

// Right line
ctx.LineTo(rightBottom, true /* is stroked */, false /* is smooth join */);

// Lower-right corner
radiusX = rect.BottomRight.X - bottomRight.X;
radiusY = rect.BottomRight.Y - rightBottom.Y;
if (!Utils.IsZero(radiusX)
|| !Utils.IsZero(radiusY))
{
ctx.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
}

// Bottom line
ctx.LineTo(bottomLeft, true /* is stroked */, false /* is smooth join */);

// Lower-left corner
radiusX = bottomLeft.X - rect.BottomLeft.X;
radiusY = rect.BottomLeft.Y - leftBottom.Y;
if (!Utils.IsZero(radiusX)
|| !Utils.IsZero(radiusY))
{
ctx.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
}

// Left line
ctx.LineTo(leftTop, true /* is stroked */, false /* is smooth join */);

// Upper-left corner
radiusX = topLeft.X - rect.TopLeft.X;
radiusY = leftTop.Y - rect.TopLeft.Y;
if (!Utils.IsZero(radiusX)
|| !Utils.IsZero(radiusY))
{
ctx.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false);
}
}

internal struct BorderInfo
{
#region Fields

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;

#endregion

#region Construction / Initialization

/// <summary>
/// Encapsulates the details of each of the core points of the border which is calculated
/// based on the given CornerRadius, BorderThickness, Padding and a flag to indicate whether
/// the inner or outer border is to be calculated.
/// </summary>
/// <param name="corners">CornerRadius</param>
/// <param name="borders">BorderThickness</param>
/// <param name="padding">Padding</param>
/// <param name="isOuterBorder">Flag to indicate whether outer or inner border needs
/// to be calculated</param>
internal BorderInfo(CornerRadius corners, Thickness borders, Thickness padding, bool isOuterBorder)
{
double left = 0.5 * borders.Left + padding.Left;
double top = 0.5 * borders.Top + padding.Top;
double right = 0.5 * borders.Right + padding.Right;
double bottom = 0.5 * borders.Bottom + padding.Bottom;

if (isOuterBorder)
{
if (corners.TopLeft.IsZero())
{
LeftTop = TopLeft = 0.0;
}
else
{
LeftTop = corners.TopLeft + left;
TopLeft = corners.TopLeft + top;
}

if (corners.TopRight.IsZero())
{
TopRight = RightTop = 0.0;
}
else
{
TopRight = corners.TopRight + top;
RightTop = corners.TopRight + right;
}

if (corners.BottomRight.IsZero())
{
RightBottom = BottomRight = 0.0;
}
else
{
RightBottom = corners.BottomRight + right;
BottomRight = corners.BottomRight + bottom;
}

if (corners.BottomLeft.IsZero())
{
BottomLeft = LeftBottom = 0.0;
}
else
{
BottomLeft = corners.BottomLeft + bottom;
LeftBottom = corners.BottomLeft + left;
}
}
else
{
LeftTop = Math.Max(0.0, corners.TopLeft - left);
TopLeft = Math.Max(0.0, corners.TopLeft - top);
TopRight = Math.Max(0.0, corners.TopRight - top);
RightTop = Math.Max(0.0, corners.TopRight - right);
RightBottom = Math.Max(0.0, corners.BottomRight - right);
BottomRight = Math.Max(0.0, corners.BottomRight - bottom);
BottomLeft = Math.Max(0.0, corners.BottomLeft - bottom);
LeftBottom = Math.Max(0.0, corners.BottomLeft - left);
}
}

#endregion
}
}
Loading

0 comments on commit 5b86f01

Please sign in to comment.