diff --git a/src/Shared/HandyControl_Shared/Controls/Other/DashBorder.cs b/src/Shared/HandyControl_Shared/Controls/Other/DashBorder.cs new file mode 100644 index 00000000..df616137 --- /dev/null +++ b/src/Shared/HandyControl_Shared/Controls/Other/DashBorder.cs @@ -0,0 +1,670 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using HandyControl.Data; +using HandyControl.Expression.Drawing; +using HandyControl.Tools; +using HandyControl.Tools.Extension; + +namespace HandyControl.Controls +{ + public class DashBorder : Decorator + { + private bool _useComplexRenderCodePath; + + private Pen GeometryPenCache { get; set; } + + private Pen LeftPenCache { get; set; } + + private Pen RightPenCache { get; set; } + + private Pen TopPenCache { get; set; } + + private Pen BottomPenCache { get; set; } + + private StreamGeometry BackgroundGeometryCache { get; set; } + + private StreamGeometry BorderGeometryCache { get; set; } + + private static void OnClearPenCache(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var border = (DashBorder)d; + border.LeftPenCache = null; + border.RightPenCache = null; + border.TopPenCache = null; + border.BottomPenCache = null; + border.GeometryPenCache = null; + } + + public static readonly DependencyProperty BorderThicknessProperty = DependencyProperty.Register( + "BorderThickness", typeof(Thickness), typeof(DashBorder), new FrameworkPropertyMetadata(default(Thickness), + FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, + OnClearPenCache)); + + public Thickness BorderThickness + { + get => (Thickness) GetValue(BorderThicknessProperty); + set => SetValue(BorderThicknessProperty, value); + } + + public static readonly DependencyProperty BorderDashThicknessProperty = DependencyProperty.Register( + "BorderDashThickness", typeof(double), typeof(DashBorder), new FrameworkPropertyMetadata(ValueBoxes.Double0Box, + FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, + OnClearPenCache)); + + public double BorderDashThickness + { + get => (double) GetValue(BorderDashThicknessProperty); + set => SetValue(BorderDashThicknessProperty, value); + } + + public static readonly DependencyProperty PaddingProperty = DependencyProperty.Register( + "Padding", typeof(Thickness), typeof(DashBorder), new FrameworkPropertyMetadata(default(Thickness), + FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); + + public Thickness Padding + { + get => (Thickness) GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } + + public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register( + "CornerRadius", typeof(CornerRadius), typeof(DashBorder), new FrameworkPropertyMetadata(default(CornerRadius), + FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); + + public CornerRadius CornerRadius + { + get => (CornerRadius) GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + public static readonly DependencyProperty BorderBrushProperty = DependencyProperty.Register( + "BorderBrush", typeof(Brush), typeof(DashBorder), new FrameworkPropertyMetadata(default(Brush), + FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender, + OnClearPenCache)); + + public Brush BorderBrush + { + get => (Brush) GetValue(BorderBrushProperty); + set => SetValue(BorderBrushProperty, value); + } + + public static readonly DependencyProperty BackgroundProperty = DependencyProperty.Register( + "Background", typeof(Brush), typeof(DashBorder), new FrameworkPropertyMetadata(default(Brush), + FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender, + OnClearPenCache)); + + public Brush Background + { + get => (Brush) GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + public static readonly DependencyProperty BorderDashArrayProperty = DependencyProperty.Register( + "BorderDashArray", typeof(DoubleCollection), typeof(DashBorder), new FrameworkPropertyMetadata(default(DoubleCollection), + FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, + OnClearPenCache)); + + public DoubleCollection BorderDashArray + { + get => (DoubleCollection) GetValue(BorderDashArrayProperty); + set => SetValue(BorderDashArrayProperty, value); + } + + public static readonly DependencyProperty BorderDashCapProperty = DependencyProperty.Register( + "BorderDashCap", typeof(PenLineCap), typeof(DashBorder), new FrameworkPropertyMetadata(default(PenLineCap), + FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, + OnClearPenCache)); + + public PenLineCap BorderDashCap + { + get => (PenLineCap) GetValue(BorderDashCapProperty); + set => SetValue(BorderDashCapProperty, value); + } + + public static readonly DependencyProperty BorderDashOffsetProperty = DependencyProperty.Register( + "BorderDashOffset", typeof(double), typeof(DashBorder), new FrameworkPropertyMetadata(ValueBoxes.Double0Box, + FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, + OnClearPenCache)); + + public double BorderDashOffset + { + get => (double) GetValue(BorderDashOffsetProperty); + set => SetValue(BorderDashOffsetProperty, value); + } + + private static Size ConvertThickness2Size(Thickness th) => new Size(th.Left + th.Right, th.Top + th.Bottom); + + private static Rect DeflateRect(Rect rt, Thickness thick) => new Rect(rt.Left + thick.Left, + rt.Top + thick.Top, + Math.Max(0.0, rt.Width - thick.Left - thick.Right), + Math.Max(0.0, rt.Height - thick.Top - thick.Bottom)); + + private static bool AreUniformCorners(CornerRadius borderRadii) + { + var topLeft = borderRadii.TopLeft; + return MathHelper.AreClose(topLeft, borderRadii.TopRight) && + MathHelper.AreClose(topLeft, borderRadii.BottomLeft) && + MathHelper.AreClose(topLeft, borderRadii.BottomRight); + } + + private static void GenerateGeometry(StreamGeometryContext ctx, Rect rect, in Radii radii) + { + var topLeft = new Point(radii.LeftTop, 0); + var topRight = new Point(rect.Width - radii.RightTop, 0); + var rightTop = new Point(rect.Width, radii.TopRight); + var rightBottom = new Point(rect.Width, rect.Height - radii.BottomRight); + var bottomRight = new Point(rect.Width - radii.RightBottom, rect.Height); + var bottomLeft = new Point(radii.LeftBottom, rect.Height); + var leftBottom = new Point(0, rect.Height - radii.BottomLeft); + var leftTop = new Point(0, radii.TopLeft); + + // top edge + if (topLeft.X > topRight.X) + { + var v = radii.LeftTop / (radii.LeftTop + radii.RightTop) * rect.Width; + topLeft.X = v; + topRight.X = v; + } + + // right edge + if (rightTop.Y > rightBottom.Y) + { + var v = radii.TopRight / (radii.TopRight + radii.BottomRight) * rect.Height; + rightTop.Y = v; + rightBottom.Y = v; + } + + // bottom edge + if (bottomRight.X < bottomLeft.X) + { + var v = radii.LeftBottom / (radii.LeftBottom + radii.RightBottom) * rect.Width; + bottomRight.X = v; + bottomLeft.X = v; + } + + // left edge + if (leftBottom.Y < leftTop.Y) + { + var v = radii.TopLeft / (radii.TopLeft + radii.BottomLeft) * rect.Height; + leftBottom.Y = v; + leftTop.Y = v; + } + + // add on offsets + var 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, true); + + // Top line + ctx.LineTo(topRight, true, false); + + // Upper-right corner + var radiusX = rect.TopRight.X - topRight.X; + var radiusY = rightTop.Y - rect.TopRight.Y; + if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY)) + { + ctx.ArcTo(rightTop, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false); + } + + // Right line + ctx.LineTo(rightBottom, true, false); + + // Lower-right corner + radiusX = rect.BottomRight.X - bottomRight.X; + radiusY = rect.BottomRight.Y - rightBottom.Y; + if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY)) + { + ctx.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false); + } + + // Bottom line + ctx.LineTo(bottomLeft, true, false); + + // Lower-left corner + radiusX = bottomLeft.X - rect.BottomLeft.X; + radiusY = rect.BottomLeft.Y - leftBottom.Y; + if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY)) + { + ctx.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false); + } + + // Left line + ctx.LineTo(leftTop, true, false); + + // Upper-left corner + radiusX = topLeft.X - rect.TopLeft.X; + radiusY = leftTop.Y - rect.TopLeft.Y; + if (!MathHelper.IsZero(radiusX) || !MathHelper.IsZero(radiusY)) + { + ctx.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false); + } + } + + protected override Size MeasureOverride(Size constraint) + { + var child = Child; + var borderThickness = BorderThickness; + var padding = Padding; + + if (UseLayoutRounding) + { + var dpiScaleX = DpiHelper.DeviceDpiX; + var dpiScaleY = DpiHelper.DeviceDpiY; + + borderThickness = new Thickness( + DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX), + DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY), + DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX), + DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY)); + } + + var borderSize = ConvertThickness2Size(borderThickness); + var paddingSize = ConvertThickness2Size(padding); + var mySize = new Size(); + + if (child != null) + { + var combined = new Size(borderSize.Width + paddingSize.Width, borderSize.Height + paddingSize.Height); + var childConstraint = new Size(Math.Max(0.0, constraint.Width - combined.Width), Math.Max(0.0, constraint.Height - combined.Height)); + + child.Measure(childConstraint); + var childSize = child.DesiredSize; + + mySize.Width = childSize.Width + combined.Width; + mySize.Height = childSize.Height + combined.Height; + } + else + { + mySize = new Size(borderSize.Width + paddingSize.Width, borderSize.Height + paddingSize.Height); + } + + return mySize; + } + + protected override Size ArrangeOverride(Size arrangeSize) + { + var borderThickness = BorderThickness; + + if (UseLayoutRounding) + { + var dpiScaleX = DpiHelper.DeviceDpiX; + var dpiScaleY = DpiHelper.DeviceDpiY; + + borderThickness = new Thickness( + DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX), + DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY), + DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX), + DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY)); + } + + var boundRect = new Rect(arrangeSize); + var innerRect = DeflateRect(boundRect, borderThickness); + + var child = Child; + if (child != null) + { + var padding = Padding; + var childRect = DeflateRect(innerRect, padding); + child.Arrange(childRect); + } + + var radii = CornerRadius; + var borderBrush = BorderBrush; + var uniformCorners = AreUniformCorners(radii); + + _useComplexRenderCodePath = !uniformCorners; + if (!_useComplexRenderCodePath && borderBrush != null) + { + _useComplexRenderCodePath = !MathHelper.IsZero(radii.TopLeft) && !borderThickness.IsUniform(); + } + + if (_useComplexRenderCodePath) + { + var innerRadii = new Radii(radii, borderThickness, false); + StreamGeometry backgroundGeometry = null; + + if (!MathHelper.IsZero(innerRect.Width) && !MathHelper.IsZero(innerRect.Height)) + { + backgroundGeometry = new StreamGeometry(); + + using (var ctx = backgroundGeometry.Open()) + { + GenerateGeometry(ctx, innerRect, innerRadii); + } + + backgroundGeometry.Freeze(); + BackgroundGeometryCache = backgroundGeometry; + } + else + { + BackgroundGeometryCache = null; + } + + if (!MathHelper.IsZero(boundRect.Width) && !MathHelper.IsZero(boundRect.Height)) + { + var outerRadii = new Radii(radii, borderThickness, true); + var borderGeometry = new StreamGeometry(); + + using (var ctx = borderGeometry.Open()) + { + GenerateGeometry(ctx, boundRect, outerRadii); + + if (backgroundGeometry != null) + { + GenerateGeometry(ctx, innerRect, innerRadii); + } + } + + borderGeometry.Freeze(); + BorderGeometryCache = borderGeometry; + } + else + { + BorderGeometryCache = null; + } + } + else + { + BackgroundGeometryCache = null; + BorderGeometryCache = null; + } + + return arrangeSize; + } + + protected override void OnRender(DrawingContext drawingContext) + { + var background = Background; + var borderBrush = BorderBrush; + var useLayoutRounding = UseLayoutRounding; + + if (_useComplexRenderCodePath) + { + var borderGeometry = BorderGeometryCache; + if (borderGeometry != null && borderBrush != null) + { + var pen = GeometryPenCache; + if (pen == null) + { + pen = new Pen + { + Brush = borderBrush, + Thickness = BorderDashThickness, + DashCap = BorderDashCap, + DashStyle = new DashStyle(BorderDashArray, BorderDashOffset) + }; + + if (borderBrush.IsFrozen) + { + pen.Freeze(); + } + + GeometryPenCache = pen; + } + + drawingContext.DrawGeometry(null, pen, borderGeometry); + } + + var backgroundGeometry = BackgroundGeometryCache; + if (backgroundGeometry != null && background != null) + { + drawingContext.DrawGeometry(background, null, backgroundGeometry); + } + } + else + { + var dpiScaleX = DpiHelper.DeviceDpiX; + var dpiScaleY = DpiHelper.DeviceDpiY; + + var borderThickness = BorderThickness; + var cornerRadius = CornerRadius; + var outerCornerRadius = cornerRadius.TopLeft; + var roundedCorners = !MathHelper.IsZero(outerCornerRadius); + + if (!borderThickness.IsZero() && borderBrush != null) + { + var pen = LeftPenCache; + if (pen == null) + { + pen = new Pen + { + Brush = borderBrush, + Thickness = useLayoutRounding + ? DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX) + : borderThickness.Left, + DashCap = BorderDashCap, + DashStyle = new DashStyle(BorderDashArray, BorderDashOffset) + }; + + if (borderBrush.IsFrozen) + { + pen.Freeze(); + } + + LeftPenCache = pen; + } + + double halfThickness; + var renderSize = RenderSize; + + if (borderThickness.IsUniform()) + { + halfThickness = pen.Thickness * 0.5; + var rect = new Rect(new Point(halfThickness, halfThickness), new Point(renderSize.Width - halfThickness, renderSize.Height - halfThickness)); + + if (roundedCorners) + { + drawingContext.DrawRoundedRectangle(null, pen, rect, outerCornerRadius, outerCornerRadius); + } + else + { + drawingContext.DrawRectangle(null, pen, rect); + } + } + else + { + if (MathHelper.GreaterThan(borderThickness.Left, 0)) + { + halfThickness = pen.Thickness * 0.5; + drawingContext.DrawLine(pen, new Point(halfThickness, 0), new Point(halfThickness, renderSize.Height)); + } + + if (MathHelper.GreaterThan(borderThickness.Right, 0)) + { + pen = RightPenCache; + if (pen == null) + { + pen = new Pen + { + Brush = borderBrush, + Thickness = useLayoutRounding + ? DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX) + : borderThickness.Right, + DashCap = BorderDashCap, + DashStyle = new DashStyle(BorderDashArray, BorderDashOffset) + }; + + if (borderBrush.IsFrozen) + { + pen.Freeze(); + } + + RightPenCache = pen; + } + + halfThickness = pen.Thickness * 0.5; + drawingContext.DrawLine(pen, new Point(renderSize.Width - halfThickness, 0), new Point(renderSize.Width - halfThickness, renderSize.Height)); + } + + if (MathHelper.GreaterThan(borderThickness.Top, 0)) + { + pen = TopPenCache; + if (pen == null) + { + pen = new Pen + { + Brush = borderBrush, + Thickness = useLayoutRounding + ? DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY) + : borderThickness.Top, + DashCap = BorderDashCap, + DashStyle = new DashStyle(BorderDashArray, BorderDashOffset) + }; + + if (borderBrush.IsFrozen) + { + pen.Freeze(); + } + + TopPenCache = pen; + } + + halfThickness = pen.Thickness * 0.5; + drawingContext.DrawLine(pen, new Point(0, halfThickness), new Point(renderSize.Width, halfThickness)); + } + + if (MathHelper.GreaterThan(borderThickness.Bottom, 0)) + { + pen = BottomPenCache; + if (pen == null) + { + pen = new Pen + { + Brush = borderBrush, + Thickness = useLayoutRounding + ? DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY) + : borderThickness.Bottom, + DashCap = BorderDashCap, + DashStyle = new DashStyle(BorderDashArray, BorderDashOffset) + }; + + if (borderBrush.IsFrozen) + { + pen.Freeze(); + } + + BottomPenCache = pen; + } + + halfThickness = pen.Thickness * 0.5; + drawingContext.DrawLine(pen, new Point(0, renderSize.Height - halfThickness), new Point(renderSize.Width, renderSize.Height - halfThickness)); + } + } + } + + if (background != null) + { + Point ptTL, ptBR; + + if (useLayoutRounding) + { + ptTL = new Point(DpiHelper.RoundLayoutValue(borderThickness.Left, dpiScaleX), DpiHelper.RoundLayoutValue(borderThickness.Top, dpiScaleY)); + ptBR = new Point(RenderSize.Width - DpiHelper.RoundLayoutValue(borderThickness.Right, dpiScaleX), + RenderSize.Height - DpiHelper.RoundLayoutValue(borderThickness.Bottom, dpiScaleY)); + } + else + { + ptTL = new Point(borderThickness.Left, borderThickness.Top); + ptBR = new Point(RenderSize.Width - borderThickness.Right, RenderSize.Height - borderThickness.Bottom); + } + + if (ptBR.X > ptTL.X && ptBR.Y > ptTL.Y) + { + if (roundedCorners) + { + var innerRadii = new Radii(cornerRadius, borderThickness, false); + var innerCornerRadius = innerRadii.TopLeft; + drawingContext.DrawRoundedRectangle(background, null, new Rect(ptTL, ptBR), innerCornerRadius, innerCornerRadius); + } + else + { + drawingContext.DrawRectangle(background, null, new Rect(ptTL, ptBR)); + } + } + } + } + } + + private readonly struct Radii + { + internal Radii(CornerRadius radii, Thickness borders, bool outer) + { + var left = 0.5 * borders.Left; + var top = 0.5 * borders.Top; + var right = 0.5 * borders.Right; + var bottom = 0.5 * borders.Bottom; + + if (outer) + { + if (MathHelper.IsZero(radii.TopLeft)) + { + LeftTop = TopLeft = 0.0; + } + else + { + LeftTop = radii.TopLeft + left; + TopLeft = radii.TopLeft + top; + } + if (MathHelper.IsZero(radii.TopRight)) + { + TopRight = RightTop = 0.0; + } + else + { + TopRight = radii.TopRight + top; + RightTop = radii.TopRight + right; + } + if (MathHelper.IsZero(radii.BottomRight)) + { + RightBottom = BottomRight = 0.0; + } + else + { + RightBottom = radii.BottomRight + right; + BottomRight = radii.BottomRight + bottom; + } + if (MathHelper.IsZero(radii.BottomLeft)) + { + BottomLeft = LeftBottom = 0.0; + } + else + { + BottomLeft = radii.BottomLeft + bottom; + LeftBottom = radii.BottomLeft + left; + } + } + else + { + LeftTop = Math.Max(0.0, radii.TopLeft - left); + TopLeft = Math.Max(0.0, radii.TopLeft - top); + TopRight = Math.Max(0.0, radii.TopRight - top); + RightTop = Math.Max(0.0, radii.TopRight - right); + RightBottom = Math.Max(0.0, radii.BottomRight - right); + BottomRight = Math.Max(0.0, radii.BottomRight - bottom); + BottomLeft = Math.Max(0.0, radii.BottomLeft - bottom); + LeftBottom = Math.Max(0.0, radii.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; + } + } +} \ No newline at end of file diff --git a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems index 3b30bb3a..8f9f96d6 100644 --- a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems +++ b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems @@ -33,6 +33,7 @@ +